@papi-ai/server 0.7.75 → 0.7.77
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 +81 -6
- package/dist/index.js +786 -190
- package/dist/prompts.js +5 -5
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1353,10 +1353,21 @@ var init_proxy_adapter = __esm({
|
|
|
1353
1353
|
// (1) local-only
|
|
1354
1354
|
"close",
|
|
1355
1355
|
"initRls",
|
|
1356
|
+
// task-3207 (C357): commitBuildComplete, commitReviewSubmit, commitRelease have no
|
|
1357
|
+
// edge case handler and no ALLOWED_METHODS entry — forwarding them 403s at the edge
|
|
1358
|
+
// with no try/catch at the call site, crashing build_execute/review_submit/release
|
|
1359
|
+
// completion for every hosted user. Restores the intended graceful degradation
|
|
1360
|
+
// (separate appendBuildReport + updateTaskStatus calls) until they're atomically wired.
|
|
1361
|
+
"commitBuildComplete",
|
|
1362
|
+
"commitReviewSubmit",
|
|
1363
|
+
"commitRelease",
|
|
1356
1364
|
// (2) not-yet-wired hosted gaps — shrink as data-proxy handlers land (task-2390).
|
|
1357
1365
|
// getToolCallCount + updatePhaseStatus are ABSENT: they now have edge handlers and
|
|
1358
1366
|
// are served through the forwarder (getToolCallCount = SUP-2026-026 handler #1).
|
|
1359
1367
|
"getContributorRole",
|
|
1368
|
+
// task-3018 (C356): storeDocBody + getDocBodyUsage are now WIRED — edge case
|
|
1369
|
+
// handlers + ALLOWED_METHODS/WRITE_METHODS entries exist, so they forward and
|
|
1370
|
+
// hosted callers get body storage. Removed from this list, as task-3017 required.
|
|
1360
1371
|
// task-2728 (C331): createOwnerAction REMOVED from NO_FORWARD — the PRODUCER half
|
|
1361
1372
|
// of the owner-action queue. Six readers were wired C329 (task-2412) but the
|
|
1362
1373
|
// producer stayed here, so the hosted Owner Action Queue was structurally empty
|
|
@@ -1384,7 +1395,7 @@ var init_proxy_adapter = __esm({
|
|
|
1384
1395
|
// now works for hosted users without exposing one member's owner actions to another.
|
|
1385
1396
|
// task-2393 (C329) — Batch B wired: markCycleLearningResolved (the P1 — hosted
|
|
1386
1397
|
// discovered_issue_resolve hard-errored), correctLatestBuildReportEffort,
|
|
1387
|
-
// updateStageExitCriteria, updateDocAction
|
|
1398
|
+
// updateStageExitCriteria, updateDocAction all have
|
|
1388
1399
|
// edge case handlers + ALLOWED_METHODS/WRITE_METHODS entries now, so they forward.
|
|
1389
1400
|
// task-2489 (C320): recordProgressStep is now wired to the edge data-proxy
|
|
1390
1401
|
// (case handler + ALLOWED_METHODS/WRITE_METHODS entries), so it forwards for
|
|
@@ -1968,9 +1979,19 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
1968
1979
|
return this.invoke("updateDocStatus", [id, status, supersededBy]);
|
|
1969
1980
|
}
|
|
1970
1981
|
// --- Cycle Learnings ---
|
|
1971
|
-
|
|
1972
|
-
|
|
1982
|
+
/**
|
|
1983
|
+
* task-2999 / task-2998 (C356): the edge handler now calls append_cycle_learnings
|
|
1984
|
+
* and returns { id, findingKey, inserted } per row, so hosted callers get the same
|
|
1985
|
+
* per-row signal the pg path does. The [] normalisation stays as a floor for an
|
|
1986
|
+
* edge deployed BEFORE that change — callers must read [] as "no signal
|
|
1987
|
+
* available", never as "nothing was new".
|
|
1988
|
+
*/
|
|
1989
|
+
async appendCycleLearnings(learnings) {
|
|
1990
|
+
const result = await this.invoke("appendCycleLearnings", [learnings]);
|
|
1991
|
+
return Array.isArray(result) ? result : [];
|
|
1973
1992
|
}
|
|
1993
|
+
// task-2998: includeResolved was missing here, so the flag could not even be SENT
|
|
1994
|
+
// to the edge — the hosted caught->fixed ledger had no way to ask for history.
|
|
1974
1995
|
getCycleLearnings(opts) {
|
|
1975
1996
|
return this.invoke("getCycleLearnings", [opts]);
|
|
1976
1997
|
}
|
|
@@ -2207,9 +2228,9 @@ var init_query = __esm({
|
|
|
2207
2228
|
CLOSE = {};
|
|
2208
2229
|
Query = class extends Promise {
|
|
2209
2230
|
constructor(strings, args, handler, canceller, options = {}) {
|
|
2210
|
-
let
|
|
2231
|
+
let resolve4, reject;
|
|
2211
2232
|
super((a, b2) => {
|
|
2212
|
-
|
|
2233
|
+
resolve4 = a;
|
|
2213
2234
|
reject = b2;
|
|
2214
2235
|
});
|
|
2215
2236
|
this.tagged = Array.isArray(strings.raw);
|
|
@@ -2220,7 +2241,7 @@ var init_query = __esm({
|
|
|
2220
2241
|
this.options = options;
|
|
2221
2242
|
this.state = null;
|
|
2222
2243
|
this.statement = null;
|
|
2223
|
-
this.resolve = (x) => (this.active = false,
|
|
2244
|
+
this.resolve = (x) => (this.active = false, resolve4(x));
|
|
2224
2245
|
this.reject = (x) => (this.active = false, reject(x));
|
|
2225
2246
|
this.active = false;
|
|
2226
2247
|
this.cancelled = null;
|
|
@@ -2268,12 +2289,12 @@ var init_query = __esm({
|
|
|
2268
2289
|
if (this.executed && !this.active)
|
|
2269
2290
|
return { done: true };
|
|
2270
2291
|
prev && prev();
|
|
2271
|
-
const promise = new Promise((
|
|
2292
|
+
const promise = new Promise((resolve4, reject) => {
|
|
2272
2293
|
this.cursorFn = (value) => {
|
|
2273
|
-
|
|
2294
|
+
resolve4({ value, done: false });
|
|
2274
2295
|
return new Promise((r) => prev = r);
|
|
2275
2296
|
};
|
|
2276
|
-
this.resolve = () => (this.active = false,
|
|
2297
|
+
this.resolve = () => (this.active = false, resolve4({ done: true }));
|
|
2277
2298
|
this.reject = (x) => (this.active = false, reject(x));
|
|
2278
2299
|
});
|
|
2279
2300
|
this.execute();
|
|
@@ -2871,12 +2892,12 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
|
|
2871
2892
|
x.on("drain", drain);
|
|
2872
2893
|
return x;
|
|
2873
2894
|
}
|
|
2874
|
-
async function cancel({ pid, secret },
|
|
2895
|
+
async function cancel({ pid, secret }, resolve4, reject) {
|
|
2875
2896
|
try {
|
|
2876
2897
|
cancelMessage = bytes_default().i32(16).i32(80877102).i32(pid).i32(secret).end(16);
|
|
2877
2898
|
await connect();
|
|
2878
2899
|
socket.once("error", reject);
|
|
2879
|
-
socket.once("close",
|
|
2900
|
+
socket.once("close", resolve4);
|
|
2880
2901
|
} catch (error2) {
|
|
2881
2902
|
reject(error2);
|
|
2882
2903
|
}
|
|
@@ -3893,7 +3914,7 @@ var init_subscribe = __esm({
|
|
|
3893
3914
|
// ../../node_modules/postgres/src/large.js
|
|
3894
3915
|
import Stream2 from "stream";
|
|
3895
3916
|
function largeObject(sql, oid, mode = 131072 | 262144) {
|
|
3896
|
-
return new Promise(async (
|
|
3917
|
+
return new Promise(async (resolve4, reject) => {
|
|
3897
3918
|
await sql.begin(async (sql2) => {
|
|
3898
3919
|
let finish;
|
|
3899
3920
|
!oid && ([{ oid }] = await sql2`select lo_creat(-1) as oid`);
|
|
@@ -3919,7 +3940,7 @@ function largeObject(sql, oid, mode = 131072 | 262144) {
|
|
|
3919
3940
|
) seek
|
|
3920
3941
|
`
|
|
3921
3942
|
};
|
|
3922
|
-
|
|
3943
|
+
resolve4(lo);
|
|
3923
3944
|
return new Promise(async (r) => finish = r);
|
|
3924
3945
|
async function readable({
|
|
3925
3946
|
highWaterMark = 2048 * 8,
|
|
@@ -4084,8 +4105,8 @@ function Postgres(a, b2) {
|
|
|
4084
4105
|
}
|
|
4085
4106
|
async function reserve() {
|
|
4086
4107
|
const queue = queue_default();
|
|
4087
|
-
const c = open.length ? open.shift() : await new Promise((
|
|
4088
|
-
const query = { reserve:
|
|
4108
|
+
const c = open.length ? open.shift() : await new Promise((resolve4, reject) => {
|
|
4109
|
+
const query = { reserve: resolve4, reject };
|
|
4089
4110
|
queries.push(query);
|
|
4090
4111
|
closed.length && connect(closed.shift(), query);
|
|
4091
4112
|
});
|
|
@@ -4122,9 +4143,9 @@ function Postgres(a, b2) {
|
|
|
4122
4143
|
let uncaughtError, result;
|
|
4123
4144
|
name && await sql2`savepoint ${sql2(name)}`;
|
|
4124
4145
|
try {
|
|
4125
|
-
result = await new Promise((
|
|
4146
|
+
result = await new Promise((resolve4, reject) => {
|
|
4126
4147
|
const x = fn2(sql2);
|
|
4127
|
-
Promise.resolve(Array.isArray(x) ? Promise.all(x) : x).then(
|
|
4148
|
+
Promise.resolve(Array.isArray(x) ? Promise.all(x) : x).then(resolve4, reject);
|
|
4128
4149
|
});
|
|
4129
4150
|
if (uncaughtError)
|
|
4130
4151
|
throw uncaughtError;
|
|
@@ -4181,8 +4202,8 @@ function Postgres(a, b2) {
|
|
|
4181
4202
|
return c.execute(query) ? move(c, busy) : move(c, full);
|
|
4182
4203
|
}
|
|
4183
4204
|
function cancel(query) {
|
|
4184
|
-
return new Promise((
|
|
4185
|
-
query.state ? query.active ? connection_default(options).cancel(query.state,
|
|
4205
|
+
return new Promise((resolve4, reject) => {
|
|
4206
|
+
query.state ? query.active ? connection_default(options).cancel(query.state, resolve4, reject) : query.cancelled = { resolve: resolve4, reject } : (queries.remove(query), query.cancelled = true, query.reject(Errors.generic("57014", "canceling statement due to user request")), resolve4());
|
|
4186
4207
|
});
|
|
4187
4208
|
}
|
|
4188
4209
|
async function end({ timeout = null } = {}) {
|
|
@@ -4201,11 +4222,11 @@ function Postgres(a, b2) {
|
|
|
4201
4222
|
async function close() {
|
|
4202
4223
|
await Promise.all(connections.map((c) => c.end()));
|
|
4203
4224
|
}
|
|
4204
|
-
async function destroy(
|
|
4225
|
+
async function destroy(resolve4) {
|
|
4205
4226
|
await Promise.all(connections.map((c) => c.terminate()));
|
|
4206
4227
|
while (queries.length)
|
|
4207
4228
|
queries.shift().reject(Errors.connection("CONNECTION_DESTROYED", options));
|
|
4208
|
-
|
|
4229
|
+
resolve4();
|
|
4209
4230
|
}
|
|
4210
4231
|
function connect(c, query) {
|
|
4211
4232
|
move(c, connecting);
|
|
@@ -4389,7 +4410,7 @@ __export(doctor_exports, {
|
|
|
4389
4410
|
__testing: () => __testing,
|
|
4390
4411
|
runDoctor: () => runDoctor
|
|
4391
4412
|
});
|
|
4392
|
-
import { existsSync as existsSync12, readFileSync as
|
|
4413
|
+
import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
|
|
4393
4414
|
import { homedir as homedir4 } from "os";
|
|
4394
4415
|
import { join as join21 } from "path";
|
|
4395
4416
|
function redact(name, value) {
|
|
@@ -4409,7 +4430,7 @@ function findMcpJson() {
|
|
|
4409
4430
|
for (const path7 of candidates) {
|
|
4410
4431
|
if (!existsSync12(path7)) continue;
|
|
4411
4432
|
try {
|
|
4412
|
-
const raw =
|
|
4433
|
+
const raw = readFileSync15(path7, "utf-8");
|
|
4413
4434
|
const parsed = JSON.parse(raw);
|
|
4414
4435
|
const papiEntry = parsed.papi ?? parsed.mcpServers?.papi;
|
|
4415
4436
|
if (!papiEntry) continue;
|
|
@@ -4687,7 +4708,7 @@ __export(reset_exports, {
|
|
|
4687
4708
|
removePapiEntry: () => removePapiEntry,
|
|
4688
4709
|
runReset: () => runReset
|
|
4689
4710
|
});
|
|
4690
|
-
import { existsSync as existsSync13, readFileSync as
|
|
4711
|
+
import { existsSync as existsSync13, readFileSync as readFileSync16, writeFileSync as writeFileSync8 } from "fs";
|
|
4691
4712
|
import { homedir as homedir5 } from "os";
|
|
4692
4713
|
import { join as join22 } from "path";
|
|
4693
4714
|
import { createInterface } from "readline/promises";
|
|
@@ -4697,7 +4718,7 @@ function findResetTarget() {
|
|
|
4697
4718
|
let raw;
|
|
4698
4719
|
let parsed;
|
|
4699
4720
|
try {
|
|
4700
|
-
raw =
|
|
4721
|
+
raw = readFileSync16(path7, "utf-8");
|
|
4701
4722
|
parsed = JSON.parse(raw);
|
|
4702
4723
|
} catch {
|
|
4703
4724
|
continue;
|
|
@@ -4768,7 +4789,7 @@ async function runReset(args = []) {
|
|
|
4768
4789
|
}
|
|
4769
4790
|
}
|
|
4770
4791
|
try {
|
|
4771
|
-
|
|
4792
|
+
writeFileSync8(target.path, removePapiEntry(target), "utf-8");
|
|
4772
4793
|
process.stdout.write(`
|
|
4773
4794
|
\u2713 Removed papi entry from ${target.path}
|
|
4774
4795
|
`);
|
|
@@ -4798,7 +4819,7 @@ __export(audit_exports, {
|
|
|
4798
4819
|
__testing: () => __testing2,
|
|
4799
4820
|
runAudit: () => runAudit
|
|
4800
4821
|
});
|
|
4801
|
-
import { existsSync as existsSync14, readFileSync as
|
|
4822
|
+
import { existsSync as existsSync14, readFileSync as readFileSync17, readdirSync as readdirSync7 } from "fs";
|
|
4802
4823
|
import { homedir as homedir6 } from "os";
|
|
4803
4824
|
import { join as join23 } from "path";
|
|
4804
4825
|
function safeListDirs(dir) {
|
|
@@ -4819,7 +4840,7 @@ function readMcp(projectPath) {
|
|
|
4819
4840
|
const path7 = join23(projectPath, ".mcp.json");
|
|
4820
4841
|
if (!existsSync14(path7)) return { servers: [] };
|
|
4821
4842
|
try {
|
|
4822
|
-
const parsed = JSON.parse(
|
|
4843
|
+
const parsed = JSON.parse(readFileSync17(path7, "utf-8"));
|
|
4823
4844
|
const mcpServers = parsed.mcpServers ?? {};
|
|
4824
4845
|
const servers = Object.keys(mcpServers);
|
|
4825
4846
|
if (parsed.papi && !servers.includes("papi")) servers.push("papi");
|
|
@@ -4875,7 +4896,7 @@ function readGlobalSkills() {
|
|
|
4875
4896
|
function readGlobalMcpServers() {
|
|
4876
4897
|
if (!existsSync14(GLOBAL_CLAUDE_JSON)) return [];
|
|
4877
4898
|
try {
|
|
4878
|
-
const parsed = JSON.parse(
|
|
4899
|
+
const parsed = JSON.parse(readFileSync17(GLOBAL_CLAUDE_JSON, "utf-8"));
|
|
4879
4900
|
const servers = parsed.mcpServers ?? {};
|
|
4880
4901
|
return Object.keys(servers).sort((a, b2) => a.localeCompare(b2));
|
|
4881
4902
|
} catch {
|
|
@@ -5041,7 +5062,7 @@ var setup_exports = {};
|
|
|
5041
5062
|
__export(setup_exports, {
|
|
5042
5063
|
runSetup: () => runSetup
|
|
5043
5064
|
});
|
|
5044
|
-
import { existsSync as existsSync15, readFileSync as
|
|
5065
|
+
import { existsSync as existsSync15, readFileSync as readFileSync18, writeFileSync as writeFileSync9, chmodSync as chmodSync2, statSync as statSync9 } from "fs";
|
|
5045
5066
|
import { join as join24 } from "path";
|
|
5046
5067
|
function baseUrl() {
|
|
5047
5068
|
const fromEnv = process.env["PAPI_HOST"] ?? process.env["PAPI_BASE_URL"];
|
|
@@ -5071,14 +5092,14 @@ async function postJson(url, body) {
|
|
|
5071
5092
|
return { status: res.status, data };
|
|
5072
5093
|
}
|
|
5073
5094
|
function sleep(ms) {
|
|
5074
|
-
return new Promise((
|
|
5095
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
5075
5096
|
}
|
|
5076
5097
|
function writeMcpJson(opts) {
|
|
5077
5098
|
const path7 = join24(process.cwd(), ".mcp.json");
|
|
5078
5099
|
let parsed = {};
|
|
5079
5100
|
if (existsSync15(path7)) {
|
|
5080
5101
|
try {
|
|
5081
|
-
parsed = JSON.parse(
|
|
5102
|
+
parsed = JSON.parse(readFileSync18(path7, "utf-8"));
|
|
5082
5103
|
} catch {
|
|
5083
5104
|
throw new Error(`.mcp.json at ${path7} is not valid JSON. Fix it or remove it before re-running setup.`);
|
|
5084
5105
|
}
|
|
@@ -5099,7 +5120,7 @@ function writeMcpJson(opts) {
|
|
|
5099
5120
|
}
|
|
5100
5121
|
mcpServers.papi = papiEntry;
|
|
5101
5122
|
parsed.mcpServers = mcpServers;
|
|
5102
|
-
|
|
5123
|
+
writeFileSync9(path7, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5103
5124
|
try {
|
|
5104
5125
|
const mode = statSync9(path7).mode & 511;
|
|
5105
5126
|
if (mode !== 384) chmodSync2(path7, 384);
|
|
@@ -5209,8 +5230,8 @@ var init_setup = __esm({
|
|
|
5209
5230
|
});
|
|
5210
5231
|
|
|
5211
5232
|
// src/index.ts
|
|
5212
|
-
import { readFileSync as
|
|
5213
|
-
import { dirname as
|
|
5233
|
+
import { readFileSync as readFileSync19 } from "fs";
|
|
5234
|
+
import { dirname as dirname7, join as join25, basename as basename2 } from "path";
|
|
5214
5235
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
5215
5236
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5216
5237
|
import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -5246,6 +5267,15 @@ var HELP_FOOTER_MD = `
|
|
|
5246
5267
|
var STRATEGY_REVIEW_OFFER_GAP = 5;
|
|
5247
5268
|
var STRATEGY_REVIEW_BLOCK_GAP = 7;
|
|
5248
5269
|
var ZOOM_OUT_OFFER_GAP = 25;
|
|
5270
|
+
function isDatabaseBackedAdapter(known) {
|
|
5271
|
+
if (known) return known === "pg" || known === "proxy";
|
|
5272
|
+
try {
|
|
5273
|
+
const { adapterType } = loadConfig();
|
|
5274
|
+
return adapterType === "pg" || adapterType === "proxy";
|
|
5275
|
+
} catch {
|
|
5276
|
+
return process.env.PAPI_ADAPTER !== "md";
|
|
5277
|
+
}
|
|
5278
|
+
}
|
|
5249
5279
|
function loadConfig() {
|
|
5250
5280
|
const projectArgIdx = process.argv.indexOf("--project");
|
|
5251
5281
|
const configuredRoot = projectArgIdx !== -1 ? process.argv[projectArgIdx + 1] : process.env.PAPI_PROJECT_DIR;
|
|
@@ -6311,6 +6341,48 @@ function effortOrdinal(effort) {
|
|
|
6311
6341
|
const normalized = effort.trim().toUpperCase();
|
|
6312
6342
|
return EFFORT_SCALE[normalized];
|
|
6313
6343
|
}
|
|
6344
|
+
function calculateCycleMetrics(reports, currentCycle, window = 5) {
|
|
6345
|
+
const recentReports = reports.filter(
|
|
6346
|
+
(r) => r.cycle > currentCycle - window && r.cycle <= currentCycle
|
|
6347
|
+
);
|
|
6348
|
+
const perCycle = /* @__PURE__ */ new Map();
|
|
6349
|
+
for (const r of recentReports) {
|
|
6350
|
+
const group = perCycle.get(r.cycle) ?? [];
|
|
6351
|
+
group.push(r);
|
|
6352
|
+
perCycle.set(r.cycle, group);
|
|
6353
|
+
}
|
|
6354
|
+
const accuracy = [];
|
|
6355
|
+
const velocity = [];
|
|
6356
|
+
const sortedCycles = [...perCycle.keys()].sort((a, b2) => a - b2);
|
|
6357
|
+
for (const cycle of sortedCycles) {
|
|
6358
|
+
const reps = perCycle.get(cycle);
|
|
6359
|
+
const deltas = [];
|
|
6360
|
+
for (const r of reps) {
|
|
6361
|
+
const actual = effortOrdinal(r.actualEffort);
|
|
6362
|
+
const estimated = effortOrdinal(r.estimatedEffort);
|
|
6363
|
+
if (actual !== void 0 && estimated !== void 0) {
|
|
6364
|
+
deltas.push(actual - estimated);
|
|
6365
|
+
}
|
|
6366
|
+
}
|
|
6367
|
+
if (deltas.length > 0) {
|
|
6368
|
+
accuracy.push({
|
|
6369
|
+
cycle,
|
|
6370
|
+
reports: deltas.length,
|
|
6371
|
+
matchRate: Math.round(deltas.filter((d) => d === 0).length / deltas.length * 100),
|
|
6372
|
+
mae: Math.round(deltas.reduce((s, d) => s + Math.abs(d), 0) / deltas.length * 10) / 10,
|
|
6373
|
+
bias: Math.round(deltas.reduce((s, d) => s + d, 0) / deltas.length * 10) / 10
|
|
6374
|
+
});
|
|
6375
|
+
}
|
|
6376
|
+
velocity.push({
|
|
6377
|
+
cycle,
|
|
6378
|
+
completed: reps.filter((r) => r.completed === "Yes").length,
|
|
6379
|
+
partial: reps.filter((r) => r.completed === "Partial").length,
|
|
6380
|
+
failed: reps.filter((r) => r.completed === "No").length,
|
|
6381
|
+
effortPoints: reps.reduce((s, r) => s + (effortOrdinal(r.actualEffort) ?? 0), 0)
|
|
6382
|
+
});
|
|
6383
|
+
}
|
|
6384
|
+
return { accuracy, velocity };
|
|
6385
|
+
}
|
|
6314
6386
|
function serializeAccuracyRow(a) {
|
|
6315
6387
|
return `| ${a.cycle} | ${a.reports} | ${a.matchRate}% | ${a.mae} | ${a.bias >= 0 ? "+" : ""}${a.bias} |`;
|
|
6316
6388
|
}
|
|
@@ -8254,9 +8326,9 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
8254
8326
|
}
|
|
8255
8327
|
|
|
8256
8328
|
// src/server.ts
|
|
8257
|
-
import { readFileSync as
|
|
8329
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
8258
8330
|
import { access as access4, readdir as readdir4, readFile as readFile9 } from "fs/promises";
|
|
8259
|
-
import { join as join20, dirname as
|
|
8331
|
+
import { join as join20, dirname as dirname6 } from "path";
|
|
8260
8332
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
8261
8333
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
8262
8334
|
import {
|
|
@@ -8389,6 +8461,16 @@ function formatBuildReports(reports, opts) {
|
|
|
8389
8461
|
_\u2026and ${reports.length - capped.length} older build report(s) omitted to bound context size._` : "";
|
|
8390
8462
|
return body + omitted;
|
|
8391
8463
|
}
|
|
8464
|
+
function extractReferencedTaskIds(report) {
|
|
8465
|
+
const prose = [report.surprises, report.architectureNotes, report.deadEnds, report.discoveredIssues].filter((s) => typeof s === "string" && s.length > 0).join("\n");
|
|
8466
|
+
const own = report.taskId?.toLowerCase();
|
|
8467
|
+
const found = /* @__PURE__ */ new Set();
|
|
8468
|
+
for (const m of prose.matchAll(/\btask-\d+\b/gi)) {
|
|
8469
|
+
const id = m[0].toLowerCase();
|
|
8470
|
+
if (id !== own) found.add(id);
|
|
8471
|
+
}
|
|
8472
|
+
return [...found].sort();
|
|
8473
|
+
}
|
|
8392
8474
|
function formatRecentlyShippedCapabilities(reports) {
|
|
8393
8475
|
const completed = reports.filter((r) => r.completed === "Yes" || r.completed === "Partial");
|
|
8394
8476
|
if (completed.length === 0) return void 0;
|
|
@@ -8403,13 +8485,39 @@ function formatRecentlyShippedCapabilities(reports) {
|
|
|
8403
8485
|
}
|
|
8404
8486
|
return parts.join("\n");
|
|
8405
8487
|
});
|
|
8406
|
-
|
|
8488
|
+
const namedBy = /* @__PURE__ */ new Map();
|
|
8489
|
+
const completedIds = new Set(completed.map((r) => r.taskId?.toLowerCase()).filter(Boolean));
|
|
8490
|
+
for (const r of completed) {
|
|
8491
|
+
for (const ref of extractReferencedTaskIds(r)) {
|
|
8492
|
+
if (completedIds.has(ref)) continue;
|
|
8493
|
+
const namers = namedBy.get(ref) ?? [];
|
|
8494
|
+
namers.push(r.taskId);
|
|
8495
|
+
namedBy.set(ref, namers);
|
|
8496
|
+
}
|
|
8497
|
+
}
|
|
8498
|
+
const out = [
|
|
8407
8499
|
`${completed.length} task(s) completed in recent cycles:`,
|
|
8408
8500
|
"",
|
|
8409
8501
|
...lines,
|
|
8410
8502
|
"",
|
|
8411
8503
|
"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."
|
|
8412
|
-
]
|
|
8504
|
+
];
|
|
8505
|
+
if (namedBy.size > 0) {
|
|
8506
|
+
out.push(
|
|
8507
|
+
"",
|
|
8508
|
+
"### \u26A0 Named by a shipped task \u2014 VERIFY BEFORE SCHEDULING",
|
|
8509
|
+
"",
|
|
8510
|
+
"These task IDs are referenced in the build reports above but are NOT themselves",
|
|
8511
|
+
"completed. A discovery is often fixed as a side effect of a sibling task's diff and",
|
|
8512
|
+
"never marked done, so it survives into this plan carrying notes that are no longer",
|
|
8513
|
+
"true (C357 gave task-3043 a P1 slot this way \u2014 task-2998 had already fixed it).",
|
|
8514
|
+
"For each one below: check the naming report and the live code BEFORE scheduling it.",
|
|
8515
|
+
"If it is already fixed, close it with a boardCorrection instead of spending a slot.",
|
|
8516
|
+
"",
|
|
8517
|
+
...[...namedBy.entries()].sort(([a], [b2]) => a.localeCompare(b2)).map(([ref, namers]) => `- **${ref}** \u2014 named by ${namers.join(", ")}`)
|
|
8518
|
+
);
|
|
8519
|
+
}
|
|
8520
|
+
return out.join("\n");
|
|
8413
8521
|
}
|
|
8414
8522
|
function formatCycleLog(entries) {
|
|
8415
8523
|
if (entries.length === 0) return "No cycle log entries yet.";
|
|
@@ -8656,13 +8764,14 @@ function computeSnapshotsFromBuildReports(reports, tasks) {
|
|
|
8656
8764
|
const cycleReports = reportsByCycle.get(sn) ?? [];
|
|
8657
8765
|
const cycleTaskRows = tasksByCycle.get(sn);
|
|
8658
8766
|
const withEffort = cycleReports.filter((r) => r.estimatedEffort && r.actualEffort);
|
|
8659
|
-
const
|
|
8660
|
-
const matchRate = withEffort.length > 0 ? Math.round(accurate / withEffort.length * 100) : 0;
|
|
8767
|
+
const [computedAccuracy] = calculateCycleMetrics(withEffort, sn, 1).accuracy;
|
|
8661
8768
|
const { completed, total, plannedPoints, deliveredPoints } = computeCycleEffort(cycleTaskRows, cycleReports);
|
|
8662
8769
|
snapshots.push({
|
|
8663
8770
|
cycle: sn,
|
|
8664
8771
|
date: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8665
|
-
|
|
8772
|
+
// No report in this cycle carried BOTH an estimate and an actual, so there
|
|
8773
|
+
// is genuinely nothing to measure. Zeros here mean "no data", not "no bias".
|
|
8774
|
+
accuracy: [computedAccuracy ?? { cycle: sn, reports: 0, matchRate: 0, mae: 0, bias: 0 }],
|
|
8666
8775
|
velocity: [{
|
|
8667
8776
|
cycle: sn,
|
|
8668
8777
|
completed,
|
|
@@ -9183,7 +9292,7 @@ var PLAN_FRAGMENT_RESEARCH = `
|
|
|
9183
9292
|
var PLAN_FRAGMENT_BUG = `
|
|
9184
9293
|
**Bug task detection:** When a task's task type is "bug" or the title starts with "Bug:" or "Fix:", apply these rules:
|
|
9185
9294
|
- **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.
|
|
9186
|
-
-
|
|
9295
|
+
- 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):
|
|
9187
9296
|
- **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."
|
|
9188
9297
|
- **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.
|
|
9189
9298
|
- **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."
|
|
@@ -9208,7 +9317,7 @@ var PLAN_FRAGMENT_SPIKE = `
|
|
|
9208
9317
|
- Keep SCOPE BOUNDARY, SECURITY CONSIDERATIONS, and PRE-BUILD VERIFICATION as normal.
|
|
9209
9318
|
- 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.`;
|
|
9210
9319
|
var PLAN_FRAGMENT_DESIGN_BRIEF = `
|
|
9211
|
-
**Design brief task detection:** When a task's task type is "design-brief", generate a DESIGN BRIEF handoff.
|
|
9320
|
+
**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):
|
|
9212
9321
|
- AUDIENCE: Who this design is for \u2014 persona and context of use (e.g. "non-technical Owner, first dashboard visit")
|
|
9213
9322
|
- 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."
|
|
9214
9323
|
- 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.
|
|
@@ -9216,7 +9325,7 @@ var PLAN_FRAGMENT_DESIGN_BRIEF = `
|
|
|
9216
9325
|
Keep SCOPE BOUNDARY, ACCEPTANCE CRITERIA, SECURITY CONSIDERATIONS, and PRE-BUILD VERIFICATION sections as normal.
|
|
9217
9326
|
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."`;
|
|
9218
9327
|
var PLAN_FRAGMENT_RESEARCH_BRIEF = `
|
|
9219
|
-
**Research brief task detection:** When a task's task type is "research-brief", generate a RESEARCH BRIEF handoff.
|
|
9328
|
+
**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):
|
|
9220
9329
|
- 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?")
|
|
9221
9330
|
- TIME-BOX: Maximum effort allowed \u2014 XS or S. Stop when the time-box is hit and report what was found, even if incomplete.
|
|
9222
9331
|
- OUTPUT: Where findings land \u2014 a doc at \`docs/research/[topic]-findings.md\` or inline in the build report. State the path.
|
|
@@ -9224,7 +9333,7 @@ var PLAN_FRAGMENT_RESEARCH_BRIEF = `
|
|
|
9224
9333
|
Keep SCOPE BOUNDARY, ACCEPTANCE CRITERIA, SECURITY CONSIDERATIONS, and PRE-BUILD VERIFICATION as normal.
|
|
9225
9334
|
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."`;
|
|
9226
9335
|
var PLAN_FRAGMENT_MARKETING_BRIEF = `
|
|
9227
|
-
**Marketing brief task detection:** When a task's task type is "marketing-brief", generate a MARKETING BRIEF handoff.
|
|
9336
|
+
**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):
|
|
9228
9337
|
- AUDIENCE: Who this marketing content targets \u2014 persona, awareness level, channel context (e.g. "cold Discord visitor, zero PAPI context")
|
|
9229
9338
|
- CHANNEL: Where this content lives \u2014 Discord, landing page, email, social, etc.
|
|
9230
9339
|
- 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.")
|
|
@@ -9232,7 +9341,7 @@ var PLAN_FRAGMENT_MARKETING_BRIEF = `
|
|
|
9232
9341
|
Keep SCOPE BOUNDARY, ACCEPTANCE CRITERIA, SECURITY CONSIDERATIONS, and PRE-BUILD VERIFICATION as normal.
|
|
9233
9342
|
Add to ACCEPTANCE CRITERIA: "[ ] Message Frame confirmed with Owner before drafting" and "[ ] Final content reviewed by Owner before publishing."`;
|
|
9234
9343
|
var PLAN_FRAGMENT_OPS_BRIEF = `
|
|
9235
|
-
**Ops brief task detection:** When a task's task type is "ops-brief", generate an OPS BRIEF handoff.
|
|
9344
|
+
**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):
|
|
9236
9345
|
- SYSTEM: Which system or service this ops task touches \u2014 Vercel, Railway, Supabase, GitHub Actions, DNS, etc.
|
|
9237
9346
|
- RISK: What could go wrong \u2014 data loss, downtime, broken deployments. Include estimated blast radius (e.g. "affects all authenticated users").
|
|
9238
9347
|
- ROLLBACK PLAN: Exact steps to undo the change if something breaks. Must be specific enough to execute under pressure.
|
|
@@ -10875,8 +10984,9 @@ async function applyHandoffs(adapter2, rawLlmOutput, cycleNumber, force = false)
|
|
|
10875
10984
|
}
|
|
10876
10985
|
const invalidFields = validateHandoffScope(parsed);
|
|
10877
10986
|
if (invalidFields.length > 0) {
|
|
10987
|
+
const scopeMissing = invalidFields.includes("scope");
|
|
10878
10988
|
warnings.push(
|
|
10879
|
-
`Rejected handoff for ${handoff.taskId}: missing or empty ${invalidFields.join(", ")}. Handoffs without explicit scope produce ambiguous builds.`
|
|
10989
|
+
`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.` : "")
|
|
10880
10990
|
);
|
|
10881
10991
|
continue;
|
|
10882
10992
|
}
|
|
@@ -12881,7 +12991,7 @@ async function assertSingleActiveCycle(adapter2, opts = {}) {
|
|
|
12881
12991
|
}
|
|
12882
12992
|
return notes;
|
|
12883
12993
|
}
|
|
12884
|
-
async function validateAndPrepare(adapter2, force, callerUserId) {
|
|
12994
|
+
async function validateAndPrepare(adapter2, force, callerUserId, adapterType) {
|
|
12885
12995
|
let mode;
|
|
12886
12996
|
let cycleNumber;
|
|
12887
12997
|
let strategyReviewWarning = "";
|
|
@@ -12943,7 +13053,7 @@ Run \`strategy_review\` first, or pass \`force: true\` to bypass this gate.`
|
|
|
12943
13053
|
if (err instanceof Error && (err.message.startsWith("Strategy Review") || err.message.startsWith("Cycle ") || err.message.startsWith("Stale reviews"))) {
|
|
12944
13054
|
throw err;
|
|
12945
13055
|
}
|
|
12946
|
-
const isPg =
|
|
13056
|
+
const isPg = isDatabaseBackedAdapter(adapterType);
|
|
12947
13057
|
throw new Error(
|
|
12948
13058
|
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."
|
|
12949
13059
|
);
|
|
@@ -13061,7 +13171,7 @@ async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnl
|
|
|
13061
13171
|
tracker?.mark("validate_and_prepare");
|
|
13062
13172
|
let t = startTimer();
|
|
13063
13173
|
const prepareScope = await resolvePlanScope(adapter2, config2);
|
|
13064
|
-
const { mode, cycleNumber, strategyReviewWarning } = await validateAndPrepare(adapter2, force, prepareScope.callerUserId);
|
|
13174
|
+
const { mode, cycleNumber, strategyReviewWarning } = await validateAndPrepare(adapter2, force, prepareScope.callerUserId, config2.adapterType);
|
|
13065
13175
|
const validateMs = t();
|
|
13066
13176
|
const incomingCycle = cycleNumber + 1;
|
|
13067
13177
|
tracker?.setStreamScope({ cycle: incomingCycle });
|
|
@@ -15281,6 +15391,23 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
|
|
|
15281
15391
|
const cappedBrief = capProductBrief2(productBrief);
|
|
15282
15392
|
const smartBoard = formatBoardForReviewSmart(tasks, lastReviewCycleNum);
|
|
15283
15393
|
const buildReportsText = formatRecentReportsSummary(reports, 10);
|
|
15394
|
+
let surpriseDigestText = "";
|
|
15395
|
+
try {
|
|
15396
|
+
if (adapter2.getCycleLearnings) {
|
|
15397
|
+
const surpriseRows = await adapter2.getCycleLearnings({ category: "surprise", limit: 40 });
|
|
15398
|
+
surpriseDigestText = formatSurpriseDigest(surpriseRows.map((s) => ({
|
|
15399
|
+
summary: s.summary,
|
|
15400
|
+
module: s.module,
|
|
15401
|
+
occurrences: s.occurrences,
|
|
15402
|
+
lastSeenCycle: s.lastSeenCycle,
|
|
15403
|
+
cycleNumber: s.cycleNumber
|
|
15404
|
+
})));
|
|
15405
|
+
}
|
|
15406
|
+
} catch {
|
|
15407
|
+
}
|
|
15408
|
+
const buildReportsWithDigest = surpriseDigestText ? `${buildReportsText}
|
|
15409
|
+
|
|
15410
|
+
${surpriseDigestText}` : buildReportsText;
|
|
15284
15411
|
logDataSourceSummary("strategy_review", [
|
|
15285
15412
|
{ label: "productBrief", hasData: warnIfEmpty("readProductBrief", productBrief) },
|
|
15286
15413
|
{ label: "activeDecisions", hasData: warnIfEmpty("getActiveDecisions", decisions) },
|
|
@@ -15512,7 +15639,7 @@ ${lines.join("\n")}`;
|
|
|
15512
15639
|
lastReviewCycle: lastReviewCycleNum,
|
|
15513
15640
|
productBrief: cappedBrief,
|
|
15514
15641
|
activeDecisions: formatActiveDecisionsForReview(decisions),
|
|
15515
|
-
allBuildReports:
|
|
15642
|
+
allBuildReports: buildReportsWithDigest,
|
|
15516
15643
|
sessionLog: formatCycleLog(recentLog),
|
|
15517
15644
|
board: smartBoard,
|
|
15518
15645
|
earnedPushback,
|
|
@@ -15977,7 +16104,7 @@ async function prepareStrategyReview(adapter2, force, projectRoot, adapterType,
|
|
|
15977
16104
|
};
|
|
15978
16105
|
}
|
|
15979
16106
|
} catch {
|
|
15980
|
-
const isPg =
|
|
16107
|
+
const isPg = isDatabaseBackedAdapter(adapterType);
|
|
15981
16108
|
throw new Error(
|
|
15982
16109
|
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."
|
|
15983
16110
|
);
|
|
@@ -16200,9 +16327,31 @@ function formatRecentReportsSummary(reports, count) {
|
|
|
16200
16327
|
if (issues) lines.push(` _Issues:_ ${issues}`);
|
|
16201
16328
|
const arch = trunc(r.architectureNotes, 200);
|
|
16202
16329
|
if (arch) lines.push(` _Architecture:_ ${arch}`);
|
|
16330
|
+
const dead = trunc(r.deadEnds, 200);
|
|
16331
|
+
if (dead) lines.push(` _Dead ends:_ ${dead}`);
|
|
16203
16332
|
return lines.join("\n");
|
|
16204
16333
|
}).join("\n");
|
|
16205
16334
|
}
|
|
16335
|
+
function formatSurpriseDigest(surprises) {
|
|
16336
|
+
if (surprises.length === 0) return "";
|
|
16337
|
+
const top = [...surprises].sort((a, b2) => (b2.occurrences ?? 1) - (a.occurrences ?? 1) || (b2.lastSeenCycle ?? b2.cycleNumber) - (a.lastSeenCycle ?? a.cycleNumber)).slice(0, 8);
|
|
16338
|
+
const byModule = /* @__PURE__ */ new Map();
|
|
16339
|
+
for (const s of top) {
|
|
16340
|
+
const key = s.module?.trim() || "Unattributed";
|
|
16341
|
+
if (!byModule.has(key)) byModule.set(key, []);
|
|
16342
|
+
byModule.get(key).push(s);
|
|
16343
|
+
}
|
|
16344
|
+
const lines = ["**Assumption failures across cycles (surprises):**"];
|
|
16345
|
+
for (const [module, items] of byModule) {
|
|
16346
|
+
lines.push(`- **${module}**`);
|
|
16347
|
+
for (const s of items) {
|
|
16348
|
+
const summary = s.summary.length > 140 ? `${s.summary.slice(0, 140)}...` : s.summary;
|
|
16349
|
+
const seen = (s.occurrences ?? 1) > 1 ? ` (seen ${s.occurrences}\xD7)` : "";
|
|
16350
|
+
lines.push(` - ${summary} \u2014 C${s.lastSeenCycle ?? s.cycleNumber}${seen}`);
|
|
16351
|
+
}
|
|
16352
|
+
}
|
|
16353
|
+
return lines.join("\n");
|
|
16354
|
+
}
|
|
16206
16355
|
function formatPhasesForReview(phases, currentCycle) {
|
|
16207
16356
|
if (phases.length === 0) return void 0;
|
|
16208
16357
|
const lines = [];
|
|
@@ -16399,13 +16548,13 @@ ${cleanContent}`;
|
|
|
16399
16548
|
${evidenceWarnings.map((w) => `- ${w}`).join("\n")}` : displayText;
|
|
16400
16549
|
return { cycleNumber, displayText: fullText, writeBackFailed };
|
|
16401
16550
|
}
|
|
16402
|
-
async function prepareStrategyChange(adapter2, text) {
|
|
16551
|
+
async function prepareStrategyChange(adapter2, text, adapterType) {
|
|
16403
16552
|
let cycleNumber;
|
|
16404
16553
|
try {
|
|
16405
16554
|
const health = await adapter2.getCycleHealth();
|
|
16406
16555
|
cycleNumber = health.totalCycles;
|
|
16407
16556
|
} catch {
|
|
16408
|
-
const isPg =
|
|
16557
|
+
const isPg = isDatabaseBackedAdapter(adapterType);
|
|
16409
16558
|
throw new Error(
|
|
16410
16559
|
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."
|
|
16411
16560
|
);
|
|
@@ -16924,7 +17073,7 @@ Decision event logged.`
|
|
|
16924
17073
|
return errorResponse("text is required for strategy_change. Describe the strategic shift to apply.");
|
|
16925
17074
|
}
|
|
16926
17075
|
{
|
|
16927
|
-
const result = await prepareStrategyChange(adapter2, text);
|
|
17076
|
+
const result = await prepareStrategyChange(adapter2, text, _config.adapterType);
|
|
16928
17077
|
return textResponse(
|
|
16929
17078
|
`## PAPI Strategy Change \u2014 Prepare Phase (Cycle ${result.cycleNumber})
|
|
16930
17079
|
|
|
@@ -19752,6 +19901,9 @@ import {
|
|
|
19752
19901
|
isCapabilityEnabled
|
|
19753
19902
|
} from "@papi-ai/shared";
|
|
19754
19903
|
|
|
19904
|
+
// src/tools/build.ts
|
|
19905
|
+
import { validateFindings, MIN_REASON_LENGTH } from "@papi-ai/shared";
|
|
19906
|
+
|
|
19755
19907
|
// src/lib/directive-builders.ts
|
|
19756
19908
|
function buildPrReviewerDirective(caps) {
|
|
19757
19909
|
if (!isCapabilityEnabled(caps, "prReviewer")) return null;
|
|
@@ -19832,6 +19984,7 @@ function buildPapiMetaFramingDirective(caps, inner) {
|
|
|
19832
19984
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
19833
19985
|
import { readdirSync as readdirSync5, existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
19834
19986
|
import { join as join12 } from "path";
|
|
19987
|
+
import { splitFindings, findingKey } from "@papi-ai/shared";
|
|
19835
19988
|
|
|
19836
19989
|
// src/lib/db-only-notices.ts
|
|
19837
19990
|
var DB_ONLY_START_NOTICE = "No git repo detected \u2014 running this cycle in your project database only. Your work stays in the working tree; run `git init` (and add a remote) to enable branches, commits and PR review.";
|
|
@@ -22390,6 +22543,8 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
22390
22543
|
} catch {
|
|
22391
22544
|
}
|
|
22392
22545
|
}
|
|
22546
|
+
let appendedRows = [];
|
|
22547
|
+
const findingRows = [];
|
|
22393
22548
|
if (adapter2.appendCycleLearnings) {
|
|
22394
22549
|
const learnings = [];
|
|
22395
22550
|
const taskModule = task.module ?? "";
|
|
@@ -22417,10 +22572,52 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
22417
22572
|
relatedDecision: adIds[0]
|
|
22418
22573
|
});
|
|
22419
22574
|
};
|
|
22420
|
-
|
|
22421
|
-
|
|
22575
|
+
const structured = input.findings ?? [];
|
|
22576
|
+
const pushFinding = (f) => {
|
|
22577
|
+
const kind = f.kind ?? "issue";
|
|
22578
|
+
const summary = (f.summary ?? "").trim();
|
|
22579
|
+
if (!summary) return;
|
|
22580
|
+
const severity = ["P0", "P1", "P2", "P3"].includes(String(f.severity)) ? f.severity : void 0;
|
|
22581
|
+
const learning = {
|
|
22582
|
+
taskId: task.id,
|
|
22583
|
+
cycleNumber,
|
|
22584
|
+
category: kind,
|
|
22585
|
+
severity,
|
|
22586
|
+
summary,
|
|
22587
|
+
detail: f.detail,
|
|
22588
|
+
tags: taskModule ? [taskModule.toLowerCase()] : [],
|
|
22589
|
+
relatedDecision: adIds[0],
|
|
22590
|
+
module: f.module ?? taskModule ?? void 0,
|
|
22591
|
+
disposition: f.disposition,
|
|
22592
|
+
dispositionReason: f.reason
|
|
22593
|
+
};
|
|
22594
|
+
findingRows.push({ finding: f, learning });
|
|
22595
|
+
learnings.push(learning);
|
|
22596
|
+
};
|
|
22597
|
+
if (structured.length > 0) {
|
|
22598
|
+
for (const f of structured) pushFinding(f);
|
|
22599
|
+
} else {
|
|
22600
|
+
const blobs = [
|
|
22601
|
+
[input.discoveredIssues, "issue"],
|
|
22602
|
+
[input.surprises, "surprise"],
|
|
22603
|
+
[input.deadEnds, "dead_end"]
|
|
22604
|
+
];
|
|
22605
|
+
for (const [text, kind] of blobs) {
|
|
22606
|
+
for (const parsed of splitFindings(text, kind)) {
|
|
22607
|
+
pushFinding({
|
|
22608
|
+
kind,
|
|
22609
|
+
// splitFindings yields 'note' for unprefixed content; that is not a
|
|
22610
|
+
// severity the column accepts, so it becomes undefined rather than
|
|
22611
|
+
// being coerced into a P-level it never claimed.
|
|
22612
|
+
severity: parsed.severity === "note" ? void 0 : parsed.severity,
|
|
22613
|
+
summary: parsed.summary,
|
|
22614
|
+
detail: parsed.summary,
|
|
22615
|
+
module: taskModule || void 0
|
|
22616
|
+
});
|
|
22617
|
+
}
|
|
22618
|
+
}
|
|
22619
|
+
}
|
|
22422
22620
|
extractLearning(input.architectureNotes, "architecture");
|
|
22423
|
-
extractLearning(input.deadEnds, "dead_end");
|
|
22424
22621
|
if (input.scopeAccuracy && input.scopeAccuracy !== "accurate") {
|
|
22425
22622
|
learnings.push({
|
|
22426
22623
|
taskId: task.id,
|
|
@@ -22432,47 +22629,48 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
22432
22629
|
}
|
|
22433
22630
|
if (learnings.length > 0) {
|
|
22434
22631
|
try {
|
|
22435
|
-
await adapter2.appendCycleLearnings(learnings);
|
|
22436
|
-
} catch {
|
|
22632
|
+
appendedRows = await adapter2.appendCycleLearnings(learnings) ?? [];
|
|
22633
|
+
} catch (err) {
|
|
22634
|
+
console.error(
|
|
22635
|
+
`[papi] appendCycleLearnings failed for ${task.displayId} (${learnings.length} finding(s) NOT recorded): ${err instanceof Error ? err.message : String(err)}`
|
|
22636
|
+
);
|
|
22437
22637
|
}
|
|
22438
22638
|
}
|
|
22439
22639
|
}
|
|
22440
|
-
const AUTO_TRIAGE_MIN_SEVERITY = /* @__PURE__ */ new Set(["P0", "P1"
|
|
22640
|
+
const AUTO_TRIAGE_MIN_SEVERITY = /* @__PURE__ */ new Set(["P0", "P1"]);
|
|
22441
22641
|
let autoTriagedCount = 0;
|
|
22442
22642
|
let autoTriagedSoftSkipped = 0;
|
|
22443
22643
|
const autoTriagedIds = [];
|
|
22444
22644
|
const autoTriagedDupes = [];
|
|
22445
|
-
|
|
22446
|
-
|
|
22447
|
-
|
|
22448
|
-
|
|
22449
|
-
|
|
22450
|
-
|
|
22451
|
-
|
|
22452
|
-
|
|
22453
|
-
|
|
22454
|
-
|
|
22455
|
-
|
|
22456
|
-
} catch {
|
|
22457
|
-
}
|
|
22458
|
-
for (const line of issueLines) {
|
|
22459
|
-
const sevMatch = line.match(/^(P[0-3])[\s:]+/i);
|
|
22460
|
-
if (!sevMatch) continue;
|
|
22461
|
-
const severityLabel = sevMatch[1].toUpperCase();
|
|
22462
|
-
if (!AUTO_TRIAGE_MIN_SEVERITY.has(severityLabel)) {
|
|
22645
|
+
const insertedByKey = /* @__PURE__ */ new Map();
|
|
22646
|
+
for (const row of appendedRows) {
|
|
22647
|
+
if (row.findingKey) insertedByKey.set(row.findingKey, row.inserted);
|
|
22648
|
+
}
|
|
22649
|
+
const haveInsertSignal = appendedRows.length > 0;
|
|
22650
|
+
if (findingRows.length > 0 && typeof adapter2.createTask === "function") {
|
|
22651
|
+
for (const { finding, learning } of findingRows) {
|
|
22652
|
+
if (learning.category !== "issue") continue;
|
|
22653
|
+
if (finding.disposition !== "filed") continue;
|
|
22654
|
+
const severityLabel = learning.severity;
|
|
22655
|
+
if (!severityLabel || !AUTO_TRIAGE_MIN_SEVERITY.has(severityLabel)) {
|
|
22463
22656
|
autoTriagedSoftSkipped++;
|
|
22464
22657
|
continue;
|
|
22465
22658
|
}
|
|
22466
|
-
const
|
|
22467
|
-
|
|
22468
|
-
|
|
22469
|
-
|
|
22470
|
-
|
|
22471
|
-
|
|
22472
|
-
|
|
22473
|
-
|
|
22659
|
+
const key = findingKey(learning.summary);
|
|
22660
|
+
if (haveInsertSignal && key && insertedByKey.get(key) === false) {
|
|
22661
|
+
autoTriagedDupes.push(learning.summary.slice(0, 60));
|
|
22662
|
+
continue;
|
|
22663
|
+
}
|
|
22664
|
+
if (!haveInsertSignal) {
|
|
22665
|
+
autoTriagedSoftSkipped++;
|
|
22666
|
+
console.error(
|
|
22667
|
+
`[papi] auto-triage skipped for "${learning.summary.slice(0, 60)}" \u2014 the adapter returned no per-row inserted signal, so recurrence cannot be distinguished from a new finding.`
|
|
22668
|
+
);
|
|
22474
22669
|
continue;
|
|
22475
22670
|
}
|
|
22671
|
+
const priority = severityLabel === "P0" ? "P0 Critical" : "P1 High";
|
|
22672
|
+
const title = learning.summary.length > 120 ? learning.summary.slice(0, 120) : learning.summary;
|
|
22673
|
+
if (!title) continue;
|
|
22476
22674
|
try {
|
|
22477
22675
|
const created = await adapter2.createTask({
|
|
22478
22676
|
uuid: "",
|
|
@@ -22481,20 +22679,17 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
22481
22679
|
status: "Backlog",
|
|
22482
22680
|
priority,
|
|
22483
22681
|
complexity: "Small",
|
|
22484
|
-
module: task.module ?? "",
|
|
22682
|
+
module: finding.module ?? task.module ?? "",
|
|
22485
22683
|
phase: task.phase ?? "",
|
|
22486
22684
|
owner: "papi",
|
|
22487
22685
|
reviewed: false,
|
|
22488
22686
|
taskType: "discovery",
|
|
22489
22687
|
source: "build_complete",
|
|
22490
|
-
notes: `Origin: ${task.displayId} (${task.title}), cycle ${cycleNumber}. Original issue: ${
|
|
22688
|
+
notes: `Origin: ${task.displayId} (${task.title}), cycle ${cycleNumber}. Filed rather than fixed${finding.reason ? `: ${finding.reason}` : ""}. Original issue: ${learning.detail ?? learning.summary}`,
|
|
22491
22689
|
createdCycle: cycleNumber
|
|
22492
22690
|
});
|
|
22493
22691
|
autoTriagedCount++;
|
|
22494
|
-
if (created?.displayId)
|
|
22495
|
-
autoTriagedIds.push(created.displayId);
|
|
22496
|
-
backlogTitleMap.set(normalized, created.displayId);
|
|
22497
|
-
}
|
|
22692
|
+
if (created?.displayId) autoTriagedIds.push(created.displayId);
|
|
22498
22693
|
} catch {
|
|
22499
22694
|
}
|
|
22500
22695
|
}
|
|
@@ -22834,15 +23029,118 @@ ${instructions}`;
|
|
|
22834
23029
|
}
|
|
22835
23030
|
|
|
22836
23031
|
// src/tools/doc-registry.ts
|
|
22837
|
-
import { readdirSync as readdirSync6, existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
|
|
22838
|
-
import { join as join13, relative } from "path";
|
|
23032
|
+
import { readdirSync as readdirSync6, existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4 } from "fs";
|
|
23033
|
+
import { join as join13, relative, isAbsolute as isAbsolute2, dirname as dirname4, resolve as resolve3, sep } from "path";
|
|
22839
23034
|
import { homedir as homedir3 } from "os";
|
|
22840
23035
|
import { randomUUID as randomUUID12 } from "crypto";
|
|
22841
23036
|
import { docDeletionBlockMessage } from "@papi-ai/shared";
|
|
22842
23037
|
init_git();
|
|
23038
|
+
|
|
23039
|
+
// src/services/entitlements.ts
|
|
23040
|
+
import { evaluateContributorGate } from "@papi-ai/shared";
|
|
23041
|
+
var FREE_PROJECT_CAP = 3;
|
|
23042
|
+
var DOC_STORAGE_CEILING_BY_TIER = {
|
|
23043
|
+
free: { bytes: 25 * 1024 * 1024, docs: 200 },
|
|
23044
|
+
pro: { bytes: 250 * 1024 * 1024, docs: 2e3 },
|
|
23045
|
+
team: { bytes: 1024 * 1024 * 1024, docs: 1e4 }
|
|
23046
|
+
};
|
|
23047
|
+
var MAX_DOC_BODY_BYTES = 2 * 1024 * 1024;
|
|
23048
|
+
var PAID_TIERS = /* @__PURE__ */ new Set(["pro", "team"]);
|
|
23049
|
+
var PRICING_URL = "https://getpapi.ai/pricing";
|
|
23050
|
+
async function resolveTier(adapter2) {
|
|
23051
|
+
if (typeof adapter2.getMeteredUsage !== "function") return null;
|
|
23052
|
+
try {
|
|
23053
|
+
const usage = await adapter2.getMeteredUsage();
|
|
23054
|
+
return usage?.tier ?? null;
|
|
23055
|
+
} catch {
|
|
23056
|
+
return null;
|
|
23057
|
+
}
|
|
23058
|
+
}
|
|
23059
|
+
function isPaidTier(tier) {
|
|
23060
|
+
return tier !== null && PAID_TIERS.has(tier);
|
|
23061
|
+
}
|
|
23062
|
+
async function enforceProjectCap(adapter2, target) {
|
|
23063
|
+
const tier = await resolveTier(adapter2);
|
|
23064
|
+
if (tier === null || isPaidTier(tier)) return null;
|
|
23065
|
+
if (typeof adapter2.listUserProjects !== "function") return null;
|
|
23066
|
+
let projects;
|
|
23067
|
+
try {
|
|
23068
|
+
projects = await adapter2.listUserProjects();
|
|
23069
|
+
} catch {
|
|
23070
|
+
return null;
|
|
23071
|
+
}
|
|
23072
|
+
const matchesExisting = projects.some(
|
|
23073
|
+
(p) => target.papiDir && p.papi_dir && p.papi_dir === target.papiDir || target.name && p.name && p.name.trim().toLowerCase() === target.name.trim().toLowerCase()
|
|
23074
|
+
);
|
|
23075
|
+
if (matchesExisting) return null;
|
|
23076
|
+
if (projects.length >= FREE_PROJECT_CAP) return projectCapMessage(projects.length);
|
|
23077
|
+
return null;
|
|
23078
|
+
}
|
|
23079
|
+
async function resolveContributorUpsell(adapter2) {
|
|
23080
|
+
const tier = await resolveTier(adapter2);
|
|
23081
|
+
return evaluateContributorGate(tier).upsell ?? null;
|
|
23082
|
+
}
|
|
23083
|
+
function projectCapMessage(currentCount) {
|
|
23084
|
+
return `**You're on the Free plan (${currentCount} of ${FREE_PROJECT_CAP} projects).**
|
|
23085
|
+
|
|
23086
|
+
Free covers up to ${FREE_PROJECT_CAP} projects. To run more, upgrade to Pro for unlimited projects at a flat monthly price (no usage bills): ${PRICING_URL}
|
|
23087
|
+
|
|
23088
|
+
Your existing projects are untouched, and you can keep working in any of them.`;
|
|
23089
|
+
}
|
|
23090
|
+
function humanBytes(n) {
|
|
23091
|
+
if (n >= 1024 * 1024 * 1024) return `${(n / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
23092
|
+
if (n >= 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
23093
|
+
if (n >= 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
23094
|
+
return `${n} bytes`;
|
|
23095
|
+
}
|
|
23096
|
+
async function checkDocStorageCap(adapter2, incomingBytes) {
|
|
23097
|
+
if (incomingBytes > MAX_DOC_BODY_BYTES) {
|
|
23098
|
+
return {
|
|
23099
|
+
storeBody: false,
|
|
23100
|
+
message: `**Body not stored \u2014 this document is ${humanBytes(incomingBytes)}, above the ${humanBytes(MAX_DOC_BODY_BYTES)} per-document limit.**
|
|
23101
|
+
|
|
23102
|
+
The registry entry was saved and everything else is unaffected. Split the document, or keep it as a file in your repo.`
|
|
23103
|
+
};
|
|
23104
|
+
}
|
|
23105
|
+
const tier = await resolveTier(adapter2);
|
|
23106
|
+
if (tier === null) return { storeBody: true };
|
|
23107
|
+
const ceiling = DOC_STORAGE_CEILING_BY_TIER[tier] ?? DOC_STORAGE_CEILING_BY_TIER.free;
|
|
23108
|
+
if (typeof adapter2.getDocBodyUsage !== "function") return { storeBody: true };
|
|
23109
|
+
let usage;
|
|
23110
|
+
try {
|
|
23111
|
+
usage = await adapter2.getDocBodyUsage();
|
|
23112
|
+
} catch {
|
|
23113
|
+
return { storeBody: true };
|
|
23114
|
+
}
|
|
23115
|
+
if (!usage) return { storeBody: true };
|
|
23116
|
+
const upgrade = tier === "free" ? `Upgrade to Pro for ${humanBytes(DOC_STORAGE_CEILING_BY_TIER.pro.bytes)} at a flat monthly price: ${PRICING_URL}` : `See plan limits: ${PRICING_URL}`;
|
|
23117
|
+
if (usage.totalBytes + incomingBytes > ceiling.bytes) {
|
|
23118
|
+
return {
|
|
23119
|
+
storeBody: false,
|
|
23120
|
+
message: `**Body not stored \u2014 you are at your ${tier} plan's document storage ceiling.**
|
|
23121
|
+
|
|
23122
|
+
Using ${humanBytes(usage.totalBytes)} of ${humanBytes(ceiling.bytes)} across ${usage.docCount} stored documents. The registry entry WAS saved, so the doc is still tracked, searchable and linkable \u2014 only the stored copy of its body was skipped.
|
|
23123
|
+
|
|
23124
|
+
${upgrade}`
|
|
23125
|
+
};
|
|
23126
|
+
}
|
|
23127
|
+
if (usage.docCount >= ceiling.docs) {
|
|
23128
|
+
return {
|
|
23129
|
+
storeBody: false,
|
|
23130
|
+
message: `**Body not stored \u2014 you are at your ${tier} plan's stored-document limit.**
|
|
23131
|
+
|
|
23132
|
+
${usage.docCount} of ${ceiling.docs} documents stored (${humanBytes(usage.totalBytes)}). The registry entry WAS saved, so the doc is still tracked, searchable and linkable \u2014 only the stored copy of its body was skipped.
|
|
23133
|
+
|
|
23134
|
+
${upgrade}`
|
|
23135
|
+
};
|
|
23136
|
+
}
|
|
23137
|
+
return { storeBody: true };
|
|
23138
|
+
}
|
|
23139
|
+
|
|
23140
|
+
// src/tools/doc-registry.ts
|
|
22843
23141
|
var docRegisterTool = {
|
|
22844
23142
|
name: "doc_register",
|
|
22845
|
-
description: "Register or update a document in the doc registry. Called after finalising a research/planning doc, or when build_execute detects unregistered docs. Stores metadata
|
|
23143
|
+
description: "Register or update a document in the doc registry. Called after finalising a research/planning doc, or when build_execute detects unregistered docs. Stores metadata, a structured summary, AND the document body (task-3017) so a doc survives a branch switch, an autostash, or a gitignored docs/private/ folder \u2014 pass `body`, or omit it and the file is read from disk when a local workspace is available. Bodies are stored per-user against a plan ceiling; at the ceiling the registration still succeeds and only the body is skipped. Only .md paths are accepted. An untracked doc is still committed at registration to make it durable (or you get a loud warning if it cannot be). Re-registering an existing doc updates its summary, tags, actions, type, and status (upsert). Visibility and owner are not changed on re-register.",
|
|
22846
23144
|
annotations: { title: "Register Doc", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
22847
23145
|
inputSchema: {
|
|
22848
23146
|
type: "object",
|
|
@@ -22854,6 +23152,10 @@ var docRegisterTool = {
|
|
|
22854
23152
|
summary: { type: "string", description: 'Structured 2-4 sentence summary. Format: "Conclusions: ... Open questions: ... Unactioned: ..."' },
|
|
22855
23153
|
tags: { type: "array", items: { type: "string" }, description: "Tags from project vocabulary." },
|
|
22856
23154
|
cycle: { type: "number", description: "Current cycle number." },
|
|
23155
|
+
body: {
|
|
23156
|
+
type: "string",
|
|
23157
|
+
description: "OPTIONAL full markdown body. Omit it and the file is read from disk when a local workspace is available; on a hosted-only session with no body supplied, metadata is registered and the tool says the body was not stored. Stored so the doc survives a branch switch, an autostash, or living in a gitignored folder."
|
|
23158
|
+
},
|
|
22857
23159
|
visibility: { type: "string", enum: ["public", "contributors", "private"], description: 'Visibility tier. Defaults to "private" (owner-only). "public" = shipped with PAPI (anyone can read); "contributors" = team-member tier (shared with the project cohort). Choose private unless you have explicit intent to share. FOLDER CONVENTION: place the doc body to match \u2014 docs/private/ (owner-only, gitignored), docs/contributors/ (team), docs/public/ (everyone). If you omit this param, the tier is inferred from the folder; anything else defaults to private.' },
|
|
22858
23160
|
actions: {
|
|
22859
23161
|
type: "array",
|
|
@@ -22891,6 +23193,19 @@ var docSearchTool = {
|
|
|
22891
23193
|
required: []
|
|
22892
23194
|
}
|
|
22893
23195
|
};
|
|
23196
|
+
var docReadTool = {
|
|
23197
|
+
name: "doc_read",
|
|
23198
|
+
description: "Read a registered document's stored BODY back out of the database, and optionally restore it to disk. This is the recovery path: when a doc is gone from the working tree \u2014 a branch switch, a stash, a gitignored docs/private/ folder that never made it into git \u2014 doc_read is how the content comes back. Identify the doc by path or UUID via `id_or_path`. Pass `write_to_disk: true` to write the body back to its registered path; an existing file with DIFFERENT content is never overwritten (the conflict is reported and nothing is written). Bodies are stored by doc_register from C356 onward, so a doc registered before that, or one whose body was skipped at a storage ceiling, has metadata but no body \u2014 doc_read says so plainly rather than returning an empty document.",
|
|
23199
|
+
annotations: { title: "Read Doc", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
23200
|
+
inputSchema: {
|
|
23201
|
+
type: "object",
|
|
23202
|
+
properties: {
|
|
23203
|
+
id_or_path: { type: "string", description: 'Registered path (e.g. "docs/private/notes.md") or the doc UUID.' },
|
|
23204
|
+
write_to_disk: { type: "boolean", description: "Write the body back to its registered path (default false \u2014 read only). Refuses to overwrite a file whose content differs." }
|
|
23205
|
+
},
|
|
23206
|
+
required: ["id_or_path"]
|
|
23207
|
+
}
|
|
23208
|
+
};
|
|
22894
23209
|
var docScanTool = {
|
|
22895
23210
|
name: "doc_scan",
|
|
22896
23211
|
description: "Scan docs/ and plans directories for unregistered .md files. Returns a list of files not yet in the doc registry. Use this to find docs that need registration.",
|
|
@@ -22922,8 +23237,8 @@ function normalizeDocPath(rawPath, projectRoot) {
|
|
|
22922
23237
|
const isWindowsDrive = /^[A-Za-z]:\//.test(fwd);
|
|
22923
23238
|
const isUnc = /^\/\//.test(fwd);
|
|
22924
23239
|
const isPosixAbs = fwd.startsWith("/");
|
|
22925
|
-
const
|
|
22926
|
-
if (!
|
|
23240
|
+
const isAbsolute3 = isWindowsDrive || isUnc || isPosixAbs;
|
|
23241
|
+
if (!isAbsolute3) return { path: fwd };
|
|
22927
23242
|
if (projectRoot) {
|
|
22928
23243
|
const root = projectRoot.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
22929
23244
|
if (fwd.toLowerCase().startsWith(`${root.toLowerCase()}/`)) {
|
|
@@ -23012,6 +23327,14 @@ async function handleDocRegister(adapter2, args, config2) {
|
|
|
23012
23327
|
continueHint
|
|
23013
23328
|
);
|
|
23014
23329
|
}
|
|
23330
|
+
if (!path7.toLowerCase().endsWith(".md")) {
|
|
23331
|
+
return docRegisterSoftFail(
|
|
23332
|
+
adapterType,
|
|
23333
|
+
"path-validation",
|
|
23334
|
+
`Only .md documents can be registered \u2014 "${path7}" is not markdown.`,
|
|
23335
|
+
"The doc registry indexes markdown notes and research docs. Nothing is blocked: continue your flow, and register the .md version if there is one."
|
|
23336
|
+
);
|
|
23337
|
+
}
|
|
23015
23338
|
try {
|
|
23016
23339
|
let supersededBy;
|
|
23017
23340
|
if (supersededByPath) {
|
|
@@ -23034,6 +23357,44 @@ async function handleDocRegister(adapter2, args, config2) {
|
|
|
23034
23357
|
actions,
|
|
23035
23358
|
visibility
|
|
23036
23359
|
});
|
|
23360
|
+
let bodyNote = "";
|
|
23361
|
+
try {
|
|
23362
|
+
const supplied = typeof args.body === "string" ? args.body : void 0;
|
|
23363
|
+
let body = supplied;
|
|
23364
|
+
if (body === void 0 && hasLocalWorkspace()) {
|
|
23365
|
+
try {
|
|
23366
|
+
const abs = isAbsolute2(entry.path) ? entry.path : join13(config2?.projectRoot ?? process.cwd(), entry.path);
|
|
23367
|
+
if (existsSync9(abs)) body = readFileSync8(abs, "utf8");
|
|
23368
|
+
} catch {
|
|
23369
|
+
}
|
|
23370
|
+
}
|
|
23371
|
+
if (body === void 0) {
|
|
23372
|
+
bodyNote = hasLocalWorkspace() ? "\n\n_Body not stored \u2014 the file could not be read from disk. Pass `body` to store it._" : "\n\n_Body not stored \u2014 no local workspace on this session. Pass `body` to store it._";
|
|
23373
|
+
} else if (typeof adapter2.storeDocBody !== "function") {
|
|
23374
|
+
bodyNote = "\n\n_Body not stored \u2014 this adapter does not support body storage._";
|
|
23375
|
+
} else {
|
|
23376
|
+
const decision = await checkDocStorageCap(adapter2, Buffer.byteLength(body, "utf8"));
|
|
23377
|
+
if (!decision.storeBody) {
|
|
23378
|
+
bodyNote = `
|
|
23379
|
+
|
|
23380
|
+
${decision.message}`;
|
|
23381
|
+
} else {
|
|
23382
|
+
const result = await adapter2.storeDocBody({
|
|
23383
|
+
docId: entry.id,
|
|
23384
|
+
body,
|
|
23385
|
+
// Resolved by resolveDocVisibility above — the SAME resolution the
|
|
23386
|
+
// registry row used, so doc_bodies' denormalised copy cannot disagree
|
|
23387
|
+
// with doc_registry on the very first write.
|
|
23388
|
+
visibility: entry.visibility ?? visibility,
|
|
23389
|
+
ownerUserId: entry.ownerUserId
|
|
23390
|
+
});
|
|
23391
|
+
bodyNote = result.stored ? `
|
|
23392
|
+
- **Body stored:** ${result.byteSize.toLocaleString()} bytes` : "\n- **Body:** unchanged since last registration";
|
|
23393
|
+
}
|
|
23394
|
+
}
|
|
23395
|
+
} catch {
|
|
23396
|
+
bodyNote = "\n\n_Body not stored \u2014 storage was unavailable. The registry entry was saved._";
|
|
23397
|
+
}
|
|
23037
23398
|
const visibilityLabel = entry.visibility === "contributors" ? "team member" : entry.visibility ?? "private";
|
|
23038
23399
|
let durability = "";
|
|
23039
23400
|
try {
|
|
@@ -23048,7 +23409,7 @@ async function handleDocRegister(adapter2, args, config2) {
|
|
|
23048
23409
|
- **Visibility:** ${visibilityLabel}
|
|
23049
23410
|
- **Tags:** ${entry.tags.length > 0 ? entry.tags.join(", ") : "none"}
|
|
23050
23411
|
- **Actions:** ${actions?.length ?? 0} items
|
|
23051
|
-
- **ID:** ${entry.id}` + durability
|
|
23412
|
+
- **ID:** ${entry.id}` + bodyNote + durability
|
|
23052
23413
|
);
|
|
23053
23414
|
} catch (err) {
|
|
23054
23415
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -23077,7 +23438,7 @@ async function handleDocSearch(adapter2, args, config2) {
|
|
|
23077
23438
|
const actionCount = d.actions?.filter((a) => a.status === "pending").length ?? 0;
|
|
23078
23439
|
const actionNote = actionCount > 0 ? ` | ${actionCount} pending action(s)` : "";
|
|
23079
23440
|
const missingNote = root && d.path && !existsSync9(join13(root, d.path)) ? `
|
|
23080
|
-
> \u26A0\uFE0F **File missing on disk** \u2014 the registry points at \`${d.path}\` but nothing is there.
|
|
23441
|
+
> \u26A0\uFE0F **File missing on disk** \u2014 the registry points at \`${d.path}\` but nothing is there. Recover it with \`doc_read\` (\`id_or_path: "${d.path}", write_to_disk: true\`) if its body was stored, or check \`git stash list\` for a papi-autostash entry.` : "";
|
|
23081
23442
|
return `### ${d.title}
|
|
23082
23443
|
**Type:** ${d.type} | **Status:** ${d.status} | **Cycle:** ${d.cycleCreated}${d.cycleUpdated ? `\u2192${d.cycleUpdated}` : ""}${actionNote}
|
|
23083
23444
|
**Path:** ${d.path}${missingNote}
|
|
@@ -23089,6 +23450,96 @@ ${d.summary}
|
|
|
23089
23450
|
|
|
23090
23451
|
${lines.join("\n---\n\n")}`);
|
|
23091
23452
|
}
|
|
23453
|
+
function resolveRestoreTarget(projectRoot, docPath) {
|
|
23454
|
+
const normalized = normalizeDocPath(docPath, projectRoot);
|
|
23455
|
+
if ("error" in normalized) return { error: normalized.error };
|
|
23456
|
+
const root = resolve3(projectRoot);
|
|
23457
|
+
const abs = resolve3(root, normalized.path);
|
|
23458
|
+
if (abs !== root && !abs.startsWith(root + sep)) {
|
|
23459
|
+
return {
|
|
23460
|
+
error: `Refusing to write outside the project root. The registered path \`${docPath}\` resolves to \`${abs}\`, which is not under \`${root}\`.`
|
|
23461
|
+
};
|
|
23462
|
+
}
|
|
23463
|
+
return { abs };
|
|
23464
|
+
}
|
|
23465
|
+
async function handleDocRead(adapter2, config2, args) {
|
|
23466
|
+
if (!adapter2.getDoc || typeof adapter2.getDocBody !== "function") {
|
|
23467
|
+
return errorResponse(
|
|
23468
|
+
"Doc bodies are not available on this adapter \u2014 requires the pg/proxy adapter. Nothing is blocked; the file on disk (if any) is still the copy you have."
|
|
23469
|
+
);
|
|
23470
|
+
}
|
|
23471
|
+
const idOrPath = args.id_or_path?.trim();
|
|
23472
|
+
if (!idOrPath) {
|
|
23473
|
+
return errorResponse("id_or_path is required \u2014 pass the doc's registered path or its UUID.");
|
|
23474
|
+
}
|
|
23475
|
+
const writeToDisk = args.write_to_disk === true;
|
|
23476
|
+
const doc = await adapter2.getDoc(idOrPath);
|
|
23477
|
+
if (!doc) {
|
|
23478
|
+
return errorResponse(
|
|
23479
|
+
`No registered doc matches \`${idOrPath}\` in this project. Check \`doc_search\` for the exact path, or \`doc_scan\` for unregistered files.`
|
|
23480
|
+
);
|
|
23481
|
+
}
|
|
23482
|
+
const gate = await resolveOwnerGate(adapter2, config2);
|
|
23483
|
+
const requesterUserId = gate.enforced ? gate.callerUserId : gate.ownerUserId ?? gate.callerUserId;
|
|
23484
|
+
const stored = await adapter2.getDocBody(doc.id, requesterUserId);
|
|
23485
|
+
const header = `**${doc.title}**
|
|
23486
|
+
- **Path:** ${doc.path}
|
|
23487
|
+
- **Type:** ${doc.type} | **Status:** ${doc.status} | **Visibility:** ${doc.visibility ?? "private"}
|
|
23488
|
+
`;
|
|
23489
|
+
if (!stored) {
|
|
23490
|
+
return textResponse(
|
|
23491
|
+
header + `- **Body:** not stored
|
|
23492
|
+
|
|
23493
|
+
No body is stored for this doc. That happens for three reasons: it was registered before body storage landed (C356); the body was skipped because the account was at its plan storage ceiling; or it was registered from a hosted session with no \`body\` argument and no local file to read.
|
|
23494
|
+
|
|
23495
|
+
**Fix:** re-run \`doc_register\` for \`${doc.path}\` with the file present, or pass \`body\` explicitly.`
|
|
23496
|
+
);
|
|
23497
|
+
}
|
|
23498
|
+
if (!stored.permitted) {
|
|
23499
|
+
return errorResponse(
|
|
23500
|
+
`\`${doc.path}\` has a stored body, but its **${doc.visibility ?? "private"}** tier does not let you read it. ` + (doc.visibility === "private" ? "Private bodies are readable by the doc owner alone \u2014 project membership is not enough, by design (task-3016)." : "Contributor-tier bodies are readable by project members only.")
|
|
23501
|
+
);
|
|
23502
|
+
}
|
|
23503
|
+
const body = stored.body ?? "";
|
|
23504
|
+
let restoreNote = "";
|
|
23505
|
+
if (writeToDisk) {
|
|
23506
|
+
restoreNote = restoreBodyToDisk(config2.projectRoot, doc.path, body);
|
|
23507
|
+
}
|
|
23508
|
+
return textResponse(
|
|
23509
|
+
header + `- **Stored:** ${stored.byteSize.toLocaleString()} bytes, updated ${stored.updatedAt}
|
|
23510
|
+
` + restoreNote + `
|
|
23511
|
+
---
|
|
23512
|
+
|
|
23513
|
+
${body}`
|
|
23514
|
+
);
|
|
23515
|
+
}
|
|
23516
|
+
function restoreBodyToDisk(projectRoot, docPath, body) {
|
|
23517
|
+
if (!hasLocalWorkspace()) {
|
|
23518
|
+
return "- **Not restored:** this session has no local workspace (hosted transport). The body is above \u2014 write it yourself.\n";
|
|
23519
|
+
}
|
|
23520
|
+
const target = resolveRestoreTarget(projectRoot, docPath);
|
|
23521
|
+
if ("error" in target) return `- **Not restored:** ${target.error}
|
|
23522
|
+
`;
|
|
23523
|
+
try {
|
|
23524
|
+
if (existsSync9(target.abs)) {
|
|
23525
|
+
const onDisk = readFileSync8(target.abs, "utf8");
|
|
23526
|
+
if (onDisk === body) {
|
|
23527
|
+
return `- **Already on disk:** \`${docPath}\` is byte-identical to the stored body. Nothing written.
|
|
23528
|
+
`;
|
|
23529
|
+
}
|
|
23530
|
+
return `- \u26A0\uFE0F **Conflict \u2014 nothing written.** A DIFFERENT file already exists at \`${docPath}\` (${Buffer.byteLength(onDisk, "utf8").toLocaleString()} bytes on disk vs ${Buffer.byteLength(body, "utf8").toLocaleString()} stored). Move or delete it first, then re-run with \`write_to_disk: true\`. The stored body is printed below either way.
|
|
23531
|
+
`;
|
|
23532
|
+
}
|
|
23533
|
+
mkdirSync4(dirname4(target.abs), { recursive: true });
|
|
23534
|
+
writeFileSync5(target.abs, body, "utf8");
|
|
23535
|
+
return `- **Restored:** wrote ${Buffer.byteLength(body, "utf8").toLocaleString()} bytes to \`${docPath}\`
|
|
23536
|
+
`;
|
|
23537
|
+
} catch (err) {
|
|
23538
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
23539
|
+
return `- **Not restored:** writing \`${docPath}\` failed \u2014 ${message}. The body is printed below.
|
|
23540
|
+
`;
|
|
23541
|
+
}
|
|
23542
|
+
}
|
|
23092
23543
|
function scanMdFiles(dir, rootDir) {
|
|
23093
23544
|
if (!existsSync9(dir)) return [];
|
|
23094
23545
|
const files = [];
|
|
@@ -23531,6 +23982,24 @@ var buildExecuteTool = {
|
|
|
23531
23982
|
},
|
|
23532
23983
|
required: ["urls", "curl_command", "http_status", "response_excerpt", "verified_at"]
|
|
23533
23984
|
},
|
|
23985
|
+
findings: {
|
|
23986
|
+
type: "array",
|
|
23987
|
+
maxItems: 12,
|
|
23988
|
+
description: 'OPTIONAL, and the highest-leverage field here. One entry per DISCRETE thing you found, instead of (or alongside) the prose blobs. The point is `disposition`: PAPI has never had a verb for "found it, fixed it, moving on", so the only way to record a finding was to file it \u2014 which costs more than fixing it and grows the board faster than the fixes. Use fixed_now when you already fixed it (the row is recorded as caught AND fixed, and is never auto-triaged), filed when it genuinely needs its own task, wont_fix when it is not worth fixing. A P2/P3 you are filing rather than fixing, and any wont_fix, needs a `reason` of at least 12 characters. Omit this array entirely and nothing changes \u2014 the prose blobs are still parsed.',
|
|
23989
|
+
items: {
|
|
23990
|
+
type: "object",
|
|
23991
|
+
properties: {
|
|
23992
|
+
kind: { type: "string", enum: ["issue", "dead_end", "surprise"], description: "What kind of finding. Only `issue` carries a disposition." },
|
|
23993
|
+
severity: { type: "string", enum: ["P0", "P1", "P2", "P3"], description: "Severity, for issues." },
|
|
23994
|
+
summary: { type: "string", maxLength: 200, description: 'One line. No "Pn:" prefix \u2014 severity has its own field.' },
|
|
23995
|
+
detail: { type: "string", maxLength: 600, description: "The verbatim context: what breaks, where, and how it reproduces." },
|
|
23996
|
+
disposition: { type: "string", enum: ["fixed_now", "filed", "wont_fix"], description: "Required for kind=issue. fixed_now = already fixed in this build. filed = needs its own task. wont_fix = not worth fixing." },
|
|
23997
|
+
reason: { type: "string", maxLength: 200, description: "Why you filed rather than fixed, or why it will not be fixed. Required (>= 12 chars) for a filed P2/P3 and for any wont_fix." },
|
|
23998
|
+
module: { type: "string", description: "Module the finding belongs to. Defaults to the task's module." }
|
|
23999
|
+
},
|
|
24000
|
+
required: ["kind", "summary"]
|
|
24001
|
+
}
|
|
24002
|
+
},
|
|
23534
24003
|
preview: {
|
|
23535
24004
|
type: "object",
|
|
23536
24005
|
description: `Optional. For a build that touched USER-FACING UI: tell the owner how to SEE the result locally so reviewing it doesn't mean reading code or asking "show me". urls = the localhost route(s) to open (e.g. ["http://localhost:3000/hub"]); notes = what to look at; screenshot_path = a path to an image you captured. Surfaced verbatim in the completion output. When the diff touched UI and you omit this, the output nudges you to provide it.`,
|
|
@@ -23861,6 +24330,24 @@ If this build fixes any of these, pass their UUIDs in \`fixed_issues\` on comple
|
|
|
23861
24330
|
}
|
|
23862
24331
|
} catch {
|
|
23863
24332
|
}
|
|
24333
|
+
let ruledOutSection = "";
|
|
24334
|
+
try {
|
|
24335
|
+
const moduleTag = result.task.module?.trim();
|
|
24336
|
+
if (moduleTag && adapter2.getCycleLearnings) {
|
|
24337
|
+
const deadEnds = (await adapter2.getCycleLearnings({ category: "dead_end", limit: 25 })).filter((l) => !l.resolvedAt).filter((l) => (l.module ?? "").toLowerCase() === moduleTag.toLowerCase() || l.tags.some((t) => t.toLowerCase() === moduleTag.toLowerCase())).slice(0, 4);
|
|
24338
|
+
if (deadEnds.length > 0) {
|
|
24339
|
+
const rows = deadEnds.map((l) => `- ${l.summary.length > 180 ? `${l.summary.slice(0, 180)}...` : l.summary}`).join("\n");
|
|
24340
|
+
ruledOutSection = `
|
|
24341
|
+
|
|
24342
|
+
---
|
|
24343
|
+
|
|
24344
|
+
**RULED OUT IN ${moduleTag} \u2014 do not re-walk these:**
|
|
24345
|
+
${rows}
|
|
24346
|
+
These approaches were tried in this module and failed. If one looks right, read why it was ruled out before spending time on it.`;
|
|
24347
|
+
}
|
|
24348
|
+
}
|
|
24349
|
+
} catch {
|
|
24350
|
+
}
|
|
23864
24351
|
const moduleInstructions = getModuleInstructions(result.task.module);
|
|
23865
24352
|
const moduleContext = await getModuleContext(adapter2, result.task);
|
|
23866
24353
|
const filesToWriteSection = result.filesToWrite ? formatFilesToWriteSection(result.filesToWrite) : "";
|
|
@@ -23870,7 +24357,7 @@ If this build fixes any of these, pass their UUIDs in \`fixed_issues\` on comple
|
|
|
23870
24357
|
) ?? "";
|
|
23871
24358
|
const gestaltNote = buildGestaltPreBuildDirective(caps) ?? "";
|
|
23872
24359
|
const buildDisciplineSection = buildPapiMetaFramingDirective(caps, buildDisciplineNote) ?? "";
|
|
23873
|
-
return textResponse(resumeNote + header + serializeBuildHandoff(result.task.buildHandoff) + modelNote + gestaltNote + adSection + moduleInstructions + moduleContext + dogfoodSection + openIssuesSection + verificationNote + buildDisciplineSection + chainInstruction + phaseNote + filesToWriteSection);
|
|
24360
|
+
return textResponse(resumeNote + header + serializeBuildHandoff(result.task.buildHandoff) + modelNote + gestaltNote + adSection + moduleInstructions + moduleContext + dogfoodSection + openIssuesSection + ruledOutSection + verificationNote + buildDisciplineSection + chainInstruction + phaseNote + filesToWriteSection);
|
|
23874
24361
|
} catch (err) {
|
|
23875
24362
|
if (isNoHandoffError(err)) {
|
|
23876
24363
|
const lines = [
|
|
@@ -23959,6 +24446,20 @@ async function handleExecuteComplete(adapter2, config2, taskId, args, light = fa
|
|
|
23959
24446
|
if (!parsedEstimatedEffort) {
|
|
23960
24447
|
return errorResponse(`Invalid estimated_effort value "${estimatedEffort}". Must be one of: XS, S, M, L, XL.`);
|
|
23961
24448
|
}
|
|
24449
|
+
const rawFindings = Array.isArray(args.findings) ? args.findings : void 0;
|
|
24450
|
+
const findingsCheck = validateFindings(rawFindings);
|
|
24451
|
+
if (!findingsCheck.ok) {
|
|
24452
|
+
const offenders = findingsCheck.violations.map((v, i) => `${i + 1}. ${v.message}`).join("\n\n");
|
|
24453
|
+
return textResponse(
|
|
24454
|
+
`**${findingsCheck.violations.length} finding${findingsCheck.violations.length === 1 ? "" : "s"} need a disposition or a reason before ${taskId} can complete.**
|
|
24455
|
+
|
|
24456
|
+
You already have the file open and the context loaded \u2014 fixing costs less than filing, triaging, planning and re-contexting it three cycles from now. A P2 or P3 you are filing rather than fixing needs a reason of at least ${MIN_REASON_LENGTH} characters; so does anything marked wont_fix. P0 and P1 file freely.
|
|
24457
|
+
|
|
24458
|
+
${offenders}
|
|
24459
|
+
|
|
24460
|
+
Your report was NOT discarded and the task is NOT yet Done \u2014 fix what you can, set a disposition on the rest, and re-send \`build_execute\` complete with the SAME report fields.`
|
|
24461
|
+
);
|
|
24462
|
+
}
|
|
23962
24463
|
const acceptanceConfirmed = args.acceptance_confirmed === true;
|
|
23963
24464
|
if (completed === "yes" && !acceptanceConfirmed) {
|
|
23964
24465
|
const gateInfo = adapter2.getProjectInfo ? await adapter2.getProjectInfo().catch(() => null) : null;
|
|
@@ -24005,7 +24506,9 @@ Your report was NOT discarded and the task is NOT yet Done \u2014 re-send with \
|
|
|
24005
24506
|
detail: bi.detail ?? ""
|
|
24006
24507
|
})),
|
|
24007
24508
|
productionVerification,
|
|
24008
|
-
preview
|
|
24509
|
+
preview,
|
|
24510
|
+
// task-3001: already gate-validated above, so the service can trust it.
|
|
24511
|
+
findings: rawFindings
|
|
24009
24512
|
}, { light }, clientName);
|
|
24010
24513
|
tracker.mark("complete_format");
|
|
24011
24514
|
tracker.setStreamScope({ taskId: result.task.displayId ?? result.task.id, cycle: result.cycleNumber });
|
|
@@ -24030,9 +24533,10 @@ Your report was NOT discarded and the task is NOT yet Done \u2014 re-send with \
|
|
|
24030
24533
|
}
|
|
24031
24534
|
let fixedResolvedCount = 0;
|
|
24032
24535
|
if (fixedIssues && fixedIssues.length > 0 && typeof adapter2.markCycleLearningResolved === "function") {
|
|
24536
|
+
const resolvedBy = `build:${result.task.displayId ?? taskId}`;
|
|
24033
24537
|
for (const learningId of fixedIssues) {
|
|
24034
24538
|
try {
|
|
24035
|
-
await adapter2.markCycleLearningResolved(learningId,
|
|
24539
|
+
await adapter2.markCycleLearningResolved(learningId, resolvedBy);
|
|
24036
24540
|
fixedResolvedCount++;
|
|
24037
24541
|
} catch {
|
|
24038
24542
|
}
|
|
@@ -27258,8 +27762,8 @@ Path: ${mcpJsonPath}`
|
|
|
27258
27762
|
const fileBody = target.render(envVars);
|
|
27259
27763
|
const wroteToShared = !target.isProjectScoped && !!existingConfig;
|
|
27260
27764
|
if (wroteToShared) {
|
|
27261
|
-
const
|
|
27262
|
-
await writeMcpConfig(mcpJsonPath, target.configPath, existingConfig +
|
|
27765
|
+
const sep2 = existingConfig.endsWith("\n") ? "\n" : "\n\n";
|
|
27766
|
+
await writeMcpConfig(mcpJsonPath, target.configPath, existingConfig + sep2 + fileBody, target.isProjectScoped, config2.adapterType, collector);
|
|
27263
27767
|
} else {
|
|
27264
27768
|
await writeMcpConfig(mcpJsonPath, target.configPath, fileBody, target.isProjectScoped, config2.adapterType, collector);
|
|
27265
27769
|
}
|
|
@@ -27330,8 +27834,8 @@ ${writeNote}
|
|
|
27330
27834
|
const fileBody = target.render(envVars);
|
|
27331
27835
|
const wroteToShared = !target.isProjectScoped && !!existingConfig;
|
|
27332
27836
|
if (wroteToShared) {
|
|
27333
|
-
const
|
|
27334
|
-
await writeMcpConfig(mcpJsonPath, target.configPath, existingConfig +
|
|
27837
|
+
const sep2 = existingConfig.endsWith("\n") ? "\n" : "\n\n";
|
|
27838
|
+
await writeMcpConfig(mcpJsonPath, target.configPath, existingConfig + sep2 + fileBody, target.isProjectScoped, config2.adapterType, collector);
|
|
27335
27839
|
} else {
|
|
27336
27840
|
await writeMcpConfig(mcpJsonPath, target.configPath, fileBody, target.isProjectScoped, config2.adapterType, collector);
|
|
27337
27841
|
}
|
|
@@ -27374,8 +27878,8 @@ ${writeNote}
|
|
|
27374
27878
|
const fileBody = target.render(envVars);
|
|
27375
27879
|
const wroteToShared = !target.isProjectScoped && !!existingConfig;
|
|
27376
27880
|
if (wroteToShared) {
|
|
27377
|
-
const
|
|
27378
|
-
await writeMcpConfig(mcpJsonPath, target.configPath, existingConfig +
|
|
27881
|
+
const sep2 = existingConfig.endsWith("\n") ? "\n" : "\n\n";
|
|
27882
|
+
await writeMcpConfig(mcpJsonPath, target.configPath, existingConfig + sep2 + fileBody, target.isProjectScoped, config2.adapterType, collector);
|
|
27379
27883
|
} else {
|
|
27380
27884
|
await writeMcpConfig(mcpJsonPath, target.configPath, fileBody, target.isProjectScoped, config2.adapterType, collector);
|
|
27381
27885
|
}
|
|
@@ -28159,8 +28663,94 @@ async function verifyProject(adapter2) {
|
|
|
28159
28663
|
// src/tools/orient.ts
|
|
28160
28664
|
import { execFile as execFile2 } from "child_process";
|
|
28161
28665
|
import { promisify as promisify2 } from "util";
|
|
28162
|
-
import { readFileSync as readFileSync11, writeFileSync as
|
|
28666
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync6, existsSync as existsSync11 } from "fs";
|
|
28163
28667
|
import { join as join17 } from "path";
|
|
28668
|
+
|
|
28669
|
+
// src/lib/exit-criteria-evaluators.ts
|
|
28670
|
+
var TEST_ACCOUNT_PATTERN = "ftue-%@test.papi.dev";
|
|
28671
|
+
function sqlOf(adapter2) {
|
|
28672
|
+
const candidate = adapter2.sql;
|
|
28673
|
+
return typeof candidate === "function" ? candidate : void 0;
|
|
28674
|
+
}
|
|
28675
|
+
var evaluateUnaidedFullLoop = async (adapter2, projectId) => {
|
|
28676
|
+
const sql = sqlOf(adapter2);
|
|
28677
|
+
if (!sql) return { met: null, unevaluatedReason: "no direct SQL access on this adapter (hosted path)" };
|
|
28678
|
+
const rows = await sql`
|
|
28679
|
+
SELECT count(DISTINCT p.user_id)::text AS builders, count(*)::text AS cycles
|
|
28680
|
+
FROM cycles c
|
|
28681
|
+
JOIN projects p ON p.id = c.project_id
|
|
28682
|
+
JOIN auth.users u ON u.id = p.user_id
|
|
28683
|
+
WHERE c.status = 'complete'
|
|
28684
|
+
AND p.user_id IS DISTINCT FROM (SELECT user_id FROM projects WHERE id = ${projectId})
|
|
28685
|
+
AND u.email NOT LIKE ${TEST_ACCOUNT_PATTERN}
|
|
28686
|
+
`;
|
|
28687
|
+
const builders2 = Number(rows[0]?.builders ?? 0);
|
|
28688
|
+
const cycles = Number(rows[0]?.cycles ?? 0);
|
|
28689
|
+
return builders2 >= 1 ? {
|
|
28690
|
+
met: null,
|
|
28691
|
+
evidence: `${builders2} non-owner account(s) completed ${cycles} cycle(s) in their own projects; owner and ${TEST_ACCOUNT_PATTERN} excluded. Counted ${(/* @__PURE__ */ new Date()).toISOString()}.`,
|
|
28692
|
+
unevaluatedReason: `mechanically satisfied (${builders2} non-owner builders, ${cycles} completed cycles) \u2014 but "without owner assistance" is a human judgement, so this needs an owner tick via set_criterion_met`
|
|
28693
|
+
} : { met: false, evidence: `0 completed cycles on non-owner projects. Counted ${(/* @__PURE__ */ new Date()).toISOString()}.` };
|
|
28694
|
+
};
|
|
28695
|
+
var evaluateSelfServeActivation = async (adapter2, projectId) => {
|
|
28696
|
+
const sql = sqlOf(adapter2);
|
|
28697
|
+
if (!sql) return { met: null, unevaluatedReason: "no direct SQL access on this adapter (hosted path)" };
|
|
28698
|
+
const rows = await sql`
|
|
28699
|
+
SELECT count(DISTINCT p.user_id)::text AS users
|
|
28700
|
+
FROM cycles c
|
|
28701
|
+
JOIN projects p ON p.id = c.project_id
|
|
28702
|
+
JOIN auth.users u ON u.id = p.user_id
|
|
28703
|
+
WHERE p.user_id IS DISTINCT FROM (SELECT user_id FROM projects WHERE id = ${projectId})
|
|
28704
|
+
AND u.email NOT LIKE ${TEST_ACCOUNT_PATTERN}
|
|
28705
|
+
AND EXISTS (SELECT 1 FROM cycle_tasks t WHERE t.cycle = c.number AND t.project_id = c.project_id)
|
|
28706
|
+
`;
|
|
28707
|
+
const users = Number(rows[0]?.users ?? 0);
|
|
28708
|
+
return users >= 1 ? {
|
|
28709
|
+
met: null,
|
|
28710
|
+
evidence: `${users} non-owner account(s) produced a planned cycle with tasks in their own project; owner and ${TEST_ACCOUNT_PATTERN} excluded. Counted ${(/* @__PURE__ */ new Date()).toISOString()}.`,
|
|
28711
|
+
unevaluatedReason: `mechanically satisfied (${users} non-owner accounts planned in their own project) \u2014 but "ZERO owner intervention" is a human judgement, so this needs an owner tick via set_criterion_met`
|
|
28712
|
+
} : { met: false, evidence: `0 non-owner accounts with a planned cycle. Counted ${(/* @__PURE__ */ new Date()).toISOString()}.` };
|
|
28713
|
+
};
|
|
28714
|
+
var EVALUATORS = {
|
|
28715
|
+
"c4a11d42-b222-45af-a470-08970e1bd6a9": evaluateSelfServeActivation,
|
|
28716
|
+
"59751bcb-f719-4cf6-a4d4-608770999a22": evaluateUnaidedFullLoop
|
|
28717
|
+
};
|
|
28718
|
+
var UNEVALUATABLE = {
|
|
28719
|
+
"e9d984f4-1e75-4486-8605-5dfd07412915": "no evaluator here \u2014 the canonical completion-keyed definition lives in lib/weekly-active-builders.ts (Next app) and must not be re-derived in the server",
|
|
28720
|
+
"c596a082-9ad3-4089-b497-46cd189e892b": "threshold is provisional pending an owner ruling \u2014 no settled bar to evaluate against"
|
|
28721
|
+
};
|
|
28722
|
+
async function evaluateExitCriteria(adapter2, projectId, criteria) {
|
|
28723
|
+
return Promise.all(criteria.map(async (c) => {
|
|
28724
|
+
const evaluator = EVALUATORS[c.id];
|
|
28725
|
+
if (!evaluator) {
|
|
28726
|
+
const reason = UNEVALUATABLE[c.id];
|
|
28727
|
+
if (!reason) return { ...c, met: c.met, autoEvaluated: false };
|
|
28728
|
+
return c.met ? { ...c, met: true, autoEvaluated: false } : { ...c, met: null, unevaluatedReason: reason, autoEvaluated: false };
|
|
28729
|
+
}
|
|
28730
|
+
try {
|
|
28731
|
+
const result = await evaluator(adapter2, projectId);
|
|
28732
|
+
return {
|
|
28733
|
+
...c,
|
|
28734
|
+
met: result.met,
|
|
28735
|
+
evidence: result.evidence ?? c.evidence,
|
|
28736
|
+
unevaluatedReason: result.unevaluatedReason,
|
|
28737
|
+
autoEvaluated: result.met !== null
|
|
28738
|
+
};
|
|
28739
|
+
} catch (err) {
|
|
28740
|
+
return {
|
|
28741
|
+
...c,
|
|
28742
|
+
met: c.met ? true : null,
|
|
28743
|
+
unevaluatedReason: `evaluator failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
28744
|
+
autoEvaluated: false
|
|
28745
|
+
};
|
|
28746
|
+
}
|
|
28747
|
+
}));
|
|
28748
|
+
}
|
|
28749
|
+
function criterionMarker(met) {
|
|
28750
|
+
return met === true ? "[x]" : met === false ? "[ ]" : "[?]";
|
|
28751
|
+
}
|
|
28752
|
+
|
|
28753
|
+
// src/tools/orient.ts
|
|
28164
28754
|
var execFileAsync2 = promisify2(execFile2);
|
|
28165
28755
|
var GIT_DEPENDENT_ENVS = /* @__PURE__ */ new Set(["hosted", "api"]);
|
|
28166
28756
|
var VALID_ENVS = /* @__PURE__ */ new Set(["local-cli", "hosted", "api", "unknown"]);
|
|
@@ -28369,9 +28959,15 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
|
|
|
28369
28959
|
}
|
|
28370
28960
|
if (hierarchy.stageExitCriteria && hierarchy.stageExitCriteria.length > 0) {
|
|
28371
28961
|
const crit = hierarchy.stageExitCriteria;
|
|
28372
|
-
const met = crit.filter((c) => c.met).length;
|
|
28373
28962
|
const total = crit.length;
|
|
28374
|
-
|
|
28963
|
+
const met = crit.filter((c) => c.met === true).length;
|
|
28964
|
+
const unevaluated = crit.filter((c) => c.met === null || c.met === void 0).length;
|
|
28965
|
+
const countLabel = unevaluated > 0 ? `${met}/${total} met, ${unevaluated} unevaluated` : `${met}/${total} met`;
|
|
28966
|
+
lines.push(`**Stage Exit Criteria [${countLabel}]:** ${crit.map((c) => `${criterionMarker(c.met ?? null)} ${c.text}`).join(" | ")}`);
|
|
28967
|
+
for (const c of crit) {
|
|
28968
|
+
const reason = c.unevaluatedReason;
|
|
28969
|
+
if (reason) lines.push(` \u21B3 UNEVALUATED \u2014 ${reason}`);
|
|
28970
|
+
}
|
|
28375
28971
|
if (met === total) {
|
|
28376
28972
|
lines.push(" \u21B3 All exit criteria met \u2014 run `strategy_review` to propose advancing the stage.");
|
|
28377
28973
|
}
|
|
@@ -28487,7 +29083,7 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
|
|
|
28487
29083
|
}
|
|
28488
29084
|
return lines.join("\n").trimEnd();
|
|
28489
29085
|
}
|
|
28490
|
-
async function getHierarchyPosition(adapter2) {
|
|
29086
|
+
async function getHierarchyPosition(adapter2, projectId) {
|
|
28491
29087
|
try {
|
|
28492
29088
|
const [horizons, stages, phases, allTasks] = await Promise.all([
|
|
28493
29089
|
adapter2.readHorizons?.() ?? [],
|
|
@@ -28520,7 +29116,14 @@ async function getHierarchyPosition(adapter2) {
|
|
|
28520
29116
|
stage: activeStage.label,
|
|
28521
29117
|
activePhases: activePhases.map((p) => p.label),
|
|
28522
29118
|
phasesNearingClosure: nearingClosure.map((p) => p.label),
|
|
28523
|
-
|
|
29119
|
+
// task-3008: EVALUATE rather than read the hand-ticked boolean. Best-effort —
|
|
29120
|
+
// a failing evaluator degrades that criterion to unevaluated and leaves the
|
|
29121
|
+
// rest intact, because orient is the first call of every session.
|
|
29122
|
+
stageExitCriteria: await evaluateExitCriteria(
|
|
29123
|
+
adapter2,
|
|
29124
|
+
projectId ?? "",
|
|
29125
|
+
activeStage.exitCriteria ?? []
|
|
29126
|
+
).catch(() => (activeStage.exitCriteria ?? []).map((c) => ({ ...c, autoEvaluated: false })))
|
|
28524
29127
|
};
|
|
28525
29128
|
} catch {
|
|
28526
29129
|
return void 0;
|
|
@@ -28689,7 +29292,7 @@ async function handleOrient(adapter2, config2, args = {}, clientName) {
|
|
|
28689
29292
|
const [buildResult, healthResult, hierarchy] = await Promise.all([
|
|
28690
29293
|
tracked("listBuilds", () => listBuilds(adapter2, config2))(),
|
|
28691
29294
|
tracked("getHealthSummary", () => getHealthSummary(adapter2))(),
|
|
28692
|
-
tracked("getHierarchyPosition", () => getHierarchyPosition(adapter2))()
|
|
29295
|
+
tracked("getHierarchyPosition", () => getHierarchyPosition(adapter2, config2.projectId))()
|
|
28693
29296
|
]);
|
|
28694
29297
|
const currentCycle = buildResult.currentCycle;
|
|
28695
29298
|
const cycleIsComplete = healthResult.latestCycleStatus === "complete";
|
|
@@ -28944,10 +29547,6 @@ async function handleOrient(adapter2, config2, args = {}, clientName) {
|
|
|
28944
29547
|
// Discovered issues — split into Alerts (P0/P1) and Unactioned (P2/P3).
|
|
28945
29548
|
// Older records without a severity field are treated as P3.
|
|
28946
29549
|
tracked("discovered-issues", async () => {
|
|
28947
|
-
try {
|
|
28948
|
-
await adapter2.resolveLearningsForDoneTasks?.();
|
|
28949
|
-
} catch {
|
|
28950
|
-
}
|
|
28951
29550
|
const learnings = await adapter2.getCycleLearnings?.({ category: "issue", limit: 30 });
|
|
28952
29551
|
if (!learnings) return { alertsNote: "", unactionedIssuesNote: "" };
|
|
28953
29552
|
const candidateLearnings = learnings.filter((l) => !l.actionTaken);
|
|
@@ -29256,7 +29855,7 @@ function enrichClaudeMd(projectRoot, cycleNumber, adapterType, collector, client
|
|
|
29256
29855
|
additions.push(dedupeEnrichmentBlob(content, CLAUDE_MD_TIER_2));
|
|
29257
29856
|
}
|
|
29258
29857
|
if (additions.length === 0) return "";
|
|
29259
|
-
|
|
29858
|
+
writeFileSync6(claudeMdPath, content + additions.join(""), "utf-8");
|
|
29260
29859
|
const tierNames = [];
|
|
29261
29860
|
if (additions.some((a) => a.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1))) tierNames.push("Established (batch building, strategy reviews, AD lifecycle)");
|
|
29262
29861
|
if (additions.some((a) => a.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T2))) tierNames.push("Mature (idea pipeline, doc registry, advanced patterns)");
|
|
@@ -30071,8 +30670,8 @@ ${result.userMessage}
|
|
|
30071
30670
|
import { readFileSync as readFileSync12, statSync as statSync7 } from "fs";
|
|
30072
30671
|
|
|
30073
30672
|
// src/services/scope-brief.ts
|
|
30074
|
-
import { writeFileSync as
|
|
30075
|
-
import { join as join18, dirname as
|
|
30673
|
+
import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync5 } from "fs";
|
|
30674
|
+
import { join as join18, dirname as dirname5 } from "path";
|
|
30076
30675
|
var SCOPE_BRIEF_SYSTEM = `You are a technical scoping tool. You receive a brief-class task (too large to build directly) and decompose it into a structured scope document.
|
|
30077
30676
|
|
|
30078
30677
|
A scope document must:
|
|
@@ -30135,8 +30734,8 @@ async function applyScopeBrief(adapter2, input) {
|
|
|
30135
30734
|
if (input.adapterType === "proxy") {
|
|
30136
30735
|
collector.add({ path: relPath, content: docBody, mode: "overwrite" });
|
|
30137
30736
|
} else {
|
|
30138
|
-
|
|
30139
|
-
|
|
30737
|
+
mkdirSync5(dirname5(absPath), { recursive: true });
|
|
30738
|
+
writeFileSync7(absPath, docBody, "utf-8");
|
|
30140
30739
|
}
|
|
30141
30740
|
const taskCount = countSubTasks(docContent);
|
|
30142
30741
|
const summary = buildSummary(task, taskCount);
|
|
@@ -30422,34 +31021,75 @@ ${formatted}`, meta));
|
|
|
30422
31021
|
}
|
|
30423
31022
|
|
|
30424
31023
|
// src/lib/dist-staleness.ts
|
|
30425
|
-
import { statSync as statSync8 } from "fs";
|
|
31024
|
+
import { readFileSync as readFileSync13, statSync as statSync8 } from "fs";
|
|
31025
|
+
import { createHash as createHash5 } from "crypto";
|
|
30426
31026
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
30427
31027
|
var BOOT_MS = Date.now();
|
|
30428
31028
|
var DEFAULT_SKEW_MS = 2e3;
|
|
30429
31029
|
var STAT_TTL_MS = 1e4;
|
|
30430
|
-
|
|
31030
|
+
var SELF_PATH = (() => {
|
|
31031
|
+
try {
|
|
31032
|
+
return fileURLToPath3(import.meta.url);
|
|
31033
|
+
} catch {
|
|
31034
|
+
return null;
|
|
31035
|
+
}
|
|
31036
|
+
})();
|
|
31037
|
+
function hashFile(path7) {
|
|
31038
|
+
try {
|
|
31039
|
+
return createHash5("sha256").update(readFileSync13(path7)).digest("hex");
|
|
31040
|
+
} catch {
|
|
31041
|
+
return null;
|
|
31042
|
+
}
|
|
31043
|
+
}
|
|
31044
|
+
var BOOT_HASH = SELF_PATH ? hashFile(SELF_PATH) : null;
|
|
31045
|
+
function bundleRewritten({ bootMs, distMtimeMs, skewMs = DEFAULT_SKEW_MS }) {
|
|
30431
31046
|
if (distMtimeMs === null || !Number.isFinite(distMtimeMs)) return false;
|
|
30432
31047
|
return distMtimeMs > bootMs + skewMs;
|
|
30433
31048
|
}
|
|
31049
|
+
function isDistStale({ bootHash, distHash }) {
|
|
31050
|
+
if (!bootHash || !distHash) return false;
|
|
31051
|
+
return bootHash !== distHash;
|
|
31052
|
+
}
|
|
31053
|
+
function shouldAnnounce(currentHash2, alreadyAnnouncedHash) {
|
|
31054
|
+
if (!currentHash2) return false;
|
|
31055
|
+
return currentHash2 !== alreadyAnnouncedHash;
|
|
31056
|
+
}
|
|
30434
31057
|
var cached = null;
|
|
30435
|
-
|
|
30436
|
-
|
|
31058
|
+
var currentHash = BOOT_HASH;
|
|
31059
|
+
var announcedHash = null;
|
|
31060
|
+
function computeStale() {
|
|
31061
|
+
if (!SELF_PATH || !BOOT_HASH) return false;
|
|
30437
31062
|
let distMtimeMs = null;
|
|
30438
31063
|
try {
|
|
30439
|
-
distMtimeMs = statSync8(
|
|
31064
|
+
distMtimeMs = statSync8(SELF_PATH).mtimeMs;
|
|
30440
31065
|
} catch {
|
|
30441
|
-
|
|
31066
|
+
return false;
|
|
30442
31067
|
}
|
|
30443
|
-
|
|
31068
|
+
if (!bundleRewritten({ bootMs: BOOT_MS, distMtimeMs })) {
|
|
31069
|
+
currentHash = BOOT_HASH;
|
|
31070
|
+
return false;
|
|
31071
|
+
}
|
|
31072
|
+
currentHash = hashFile(SELF_PATH);
|
|
31073
|
+
return isDistStale({ bootHash: BOOT_HASH, distHash: currentHash });
|
|
31074
|
+
}
|
|
31075
|
+
function checkDistStaleness(now = Date.now()) {
|
|
31076
|
+
if (cached && now - cached.at < STAT_TTL_MS) return cached.stale;
|
|
31077
|
+
const stale = computeStale();
|
|
30444
31078
|
cached = { at: now, stale };
|
|
30445
31079
|
return stale;
|
|
30446
31080
|
}
|
|
30447
31081
|
function stalenessWarning(now = Date.now()) {
|
|
30448
31082
|
const age = Math.round((now - BOOT_MS) / 1e3);
|
|
30449
|
-
return `\u26A0\uFE0F This PAPI MCP server process booted ${age}s ago and packages/server has been REBUILT since. It is still running the code it booted with, so anything you verify through these tools right now reflects the OLD bundle. Reconnect / restart the PAPI MCP server before trusting this output.
|
|
31083
|
+
return `\u26A0\uFE0F This PAPI MCP server process booted ${age}s ago and packages/server has been REBUILT with DIFFERENT code since. It is still running the code it booted with, so anything you verify through these tools right now reflects the OLD bundle. Reconnect / restart the PAPI MCP server before trusting this output. (Shown once per rebuild \u2014 the condition persists until you restart.)
|
|
30450
31084
|
|
|
30451
31085
|
`;
|
|
30452
31086
|
}
|
|
31087
|
+
function consumeStalenessWarning(now = Date.now()) {
|
|
31088
|
+
if (!checkDistStaleness(now)) return null;
|
|
31089
|
+
if (!shouldAnnounce(currentHash, announcedHash)) return null;
|
|
31090
|
+
announcedHash = currentHash;
|
|
31091
|
+
return stalenessWarning(now);
|
|
31092
|
+
}
|
|
30453
31093
|
|
|
30454
31094
|
// src/tools/learning-action.ts
|
|
30455
31095
|
var learningActionTool = {
|
|
@@ -30578,54 +31218,6 @@ async function handleDiscoveredIssueResolve(adapter2, args) {
|
|
|
30578
31218
|
|
|
30579
31219
|
// src/tools/project.ts
|
|
30580
31220
|
import path6 from "path";
|
|
30581
|
-
|
|
30582
|
-
// src/services/entitlements.ts
|
|
30583
|
-
import { evaluateContributorGate } from "@papi-ai/shared";
|
|
30584
|
-
var FREE_PROJECT_CAP = 3;
|
|
30585
|
-
var PAID_TIERS = /* @__PURE__ */ new Set(["pro", "team"]);
|
|
30586
|
-
var PRICING_URL = "https://getpapi.ai/pricing";
|
|
30587
|
-
async function resolveTier(adapter2) {
|
|
30588
|
-
if (typeof adapter2.getMeteredUsage !== "function") return null;
|
|
30589
|
-
try {
|
|
30590
|
-
const usage = await adapter2.getMeteredUsage();
|
|
30591
|
-
return usage?.tier ?? null;
|
|
30592
|
-
} catch {
|
|
30593
|
-
return null;
|
|
30594
|
-
}
|
|
30595
|
-
}
|
|
30596
|
-
function isPaidTier(tier) {
|
|
30597
|
-
return tier !== null && PAID_TIERS.has(tier);
|
|
30598
|
-
}
|
|
30599
|
-
async function enforceProjectCap(adapter2, target) {
|
|
30600
|
-
const tier = await resolveTier(adapter2);
|
|
30601
|
-
if (tier === null || isPaidTier(tier)) return null;
|
|
30602
|
-
if (typeof adapter2.listUserProjects !== "function") return null;
|
|
30603
|
-
let projects;
|
|
30604
|
-
try {
|
|
30605
|
-
projects = await adapter2.listUserProjects();
|
|
30606
|
-
} catch {
|
|
30607
|
-
return null;
|
|
30608
|
-
}
|
|
30609
|
-
const matchesExisting = projects.some(
|
|
30610
|
-
(p) => target.papiDir && p.papi_dir && p.papi_dir === target.papiDir || target.name && p.name && p.name.trim().toLowerCase() === target.name.trim().toLowerCase()
|
|
30611
|
-
);
|
|
30612
|
-
if (matchesExisting) return null;
|
|
30613
|
-
if (projects.length >= FREE_PROJECT_CAP) return projectCapMessage(projects.length);
|
|
30614
|
-
return null;
|
|
30615
|
-
}
|
|
30616
|
-
async function resolveContributorUpsell(adapter2) {
|
|
30617
|
-
const tier = await resolveTier(adapter2);
|
|
30618
|
-
return evaluateContributorGate(tier).upsell ?? null;
|
|
30619
|
-
}
|
|
30620
|
-
function projectCapMessage(currentCount) {
|
|
30621
|
-
return `**You're on the Free plan (${currentCount} of ${FREE_PROJECT_CAP} projects).**
|
|
30622
|
-
|
|
30623
|
-
Free covers up to ${FREE_PROJECT_CAP} projects. To run more, upgrade to Pro for unlimited projects at a flat monthly price (no usage bills): ${PRICING_URL}
|
|
30624
|
-
|
|
30625
|
-
Your existing projects are untouched, and you can keep working in any of them.`;
|
|
30626
|
-
}
|
|
30627
|
-
|
|
30628
|
-
// src/tools/project.ts
|
|
30629
31221
|
function workspacePapiDir(config2) {
|
|
30630
31222
|
if (!hasLocalWorkspace()) return void 0;
|
|
30631
31223
|
return config2.papiDir;
|
|
@@ -31095,7 +31687,7 @@ Its build reports, comments, and history moved with it; the cycle assignment was
|
|
|
31095
31687
|
// src/services/harness-inventory.ts
|
|
31096
31688
|
import { readdir as readdir3, readFile as readFile8, stat as stat3 } from "fs/promises";
|
|
31097
31689
|
import { join as join19 } from "path";
|
|
31098
|
-
import { createHash as
|
|
31690
|
+
import { createHash as createHash6 } from "crypto";
|
|
31099
31691
|
var RECOMMENDED_HOOKS = ["stop-release-check.sh", "claude-md-size-guard.sh"];
|
|
31100
31692
|
async function computeFingerprint(root) {
|
|
31101
31693
|
const parts = [];
|
|
@@ -31121,7 +31713,7 @@ async function computeFingerprint(root) {
|
|
|
31121
31713
|
} catch {
|
|
31122
31714
|
parts.push("manifest:none");
|
|
31123
31715
|
}
|
|
31124
|
-
return
|
|
31716
|
+
return createHash6("sha256").update(parts.join("|")).digest("hex").slice(0, 16);
|
|
31125
31717
|
}
|
|
31126
31718
|
async function readSkillDescription(skillDir) {
|
|
31127
31719
|
try {
|
|
@@ -31591,6 +32183,7 @@ var PAPI_TOOLS = [
|
|
|
31591
32183
|
docActionPromoteTool,
|
|
31592
32184
|
docDeleteTool,
|
|
31593
32185
|
docReorderTool,
|
|
32186
|
+
docReadTool,
|
|
31594
32187
|
getSiblingAdsTool,
|
|
31595
32188
|
handoffGenerateTool,
|
|
31596
32189
|
scopeBriefTool,
|
|
@@ -31614,10 +32207,10 @@ function getToolMetadata() {
|
|
|
31614
32207
|
}
|
|
31615
32208
|
function createServer(adapter2, config2) {
|
|
31616
32209
|
const __pkgFilename = fileURLToPath4(import.meta.url);
|
|
31617
|
-
const __pkgDir =
|
|
32210
|
+
const __pkgDir = dirname6(__pkgFilename);
|
|
31618
32211
|
let serverVersion = "unknown";
|
|
31619
32212
|
try {
|
|
31620
|
-
const pkg = JSON.parse(
|
|
32213
|
+
const pkg = JSON.parse(readFileSync14(join20(__pkgDir, "..", "package.json"), "utf-8"));
|
|
31621
32214
|
serverVersion = pkg.version ?? "unknown";
|
|
31622
32215
|
} catch {
|
|
31623
32216
|
}
|
|
@@ -31634,7 +32227,7 @@ function createServer(adapter2, config2) {
|
|
|
31634
32227
|
);
|
|
31635
32228
|
}
|
|
31636
32229
|
const __filename = fileURLToPath4(import.meta.url);
|
|
31637
|
-
const __dirname2 =
|
|
32230
|
+
const __dirname2 = dirname6(__filename);
|
|
31638
32231
|
const skillsDir = join20(__dirname2, "..", "skills");
|
|
31639
32232
|
function parseSkillFrontmatter(content) {
|
|
31640
32233
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
@@ -31795,6 +32388,8 @@ function createServer(adapter2, config2) {
|
|
|
31795
32388
|
return handleDocDelete(adapter2, config2, safeArgs);
|
|
31796
32389
|
case "doc_reorder":
|
|
31797
32390
|
return handleDocReorder(adapter2, safeArgs);
|
|
32391
|
+
case "doc_read":
|
|
32392
|
+
return handleDocRead(adapter2, config2, safeArgs);
|
|
31798
32393
|
case "get_sibling_ads":
|
|
31799
32394
|
return handleGetSiblingAds(adapter2, safeArgs);
|
|
31800
32395
|
case "handoff_generate":
|
|
@@ -31936,10 +32531,11 @@ ${usageLine(decision.usage)}`;
|
|
|
31936
32531
|
const footer = formatMetricsFooter(elapsed, usage, contextBytes);
|
|
31937
32532
|
result.content.push({ type: "text", text: footer });
|
|
31938
32533
|
try {
|
|
31939
|
-
|
|
32534
|
+
const warning = consumeStalenessWarning();
|
|
32535
|
+
if (warning && result.content.length > 0) {
|
|
31940
32536
|
const first = result.content[0];
|
|
31941
32537
|
if (first && typeof first.text === "string") {
|
|
31942
|
-
first.text =
|
|
32538
|
+
first.text = warning + first.text;
|
|
31943
32539
|
}
|
|
31944
32540
|
}
|
|
31945
32541
|
} catch {
|
|
@@ -32389,10 +32985,10 @@ async function dispatchRequest(args) {
|
|
|
32389
32985
|
}
|
|
32390
32986
|
|
|
32391
32987
|
// src/index.ts
|
|
32392
|
-
var __dirname =
|
|
32988
|
+
var __dirname = dirname7(fileURLToPath5(import.meta.url));
|
|
32393
32989
|
var pkgVersion = "unknown";
|
|
32394
32990
|
try {
|
|
32395
|
-
const pkg = JSON.parse(
|
|
32991
|
+
const pkg = JSON.parse(readFileSync19(join25(__dirname, "..", "package.json"), "utf-8"));
|
|
32396
32992
|
pkgVersion = pkg.version;
|
|
32397
32993
|
} catch {
|
|
32398
32994
|
}
|