@papi-ai/server 0.7.74 → 0.7.76
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 +16 -3
- package/dist/index.js +768 -184
- package/dist/prompts.js +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1357,6 +1357,9 @@ var init_proxy_adapter = __esm({
|
|
|
1357
1357
|
// getToolCallCount + updatePhaseStatus are ABSENT: they now have edge handlers and
|
|
1358
1358
|
// are served through the forwarder (getToolCallCount = SUP-2026-026 handler #1).
|
|
1359
1359
|
"getContributorRole",
|
|
1360
|
+
// task-3018 (C356): storeDocBody + getDocBodyUsage are now WIRED — edge case
|
|
1361
|
+
// handlers + ALLOWED_METHODS/WRITE_METHODS entries exist, so they forward and
|
|
1362
|
+
// hosted callers get body storage. Removed from this list, as task-3017 required.
|
|
1360
1363
|
// task-2728 (C331): createOwnerAction REMOVED from NO_FORWARD — the PRODUCER half
|
|
1361
1364
|
// of the owner-action queue. Six readers were wired C329 (task-2412) but the
|
|
1362
1365
|
// producer stayed here, so the hosted Owner Action Queue was structurally empty
|
|
@@ -1384,7 +1387,7 @@ var init_proxy_adapter = __esm({
|
|
|
1384
1387
|
// now works for hosted users without exposing one member's owner actions to another.
|
|
1385
1388
|
// task-2393 (C329) — Batch B wired: markCycleLearningResolved (the P1 — hosted
|
|
1386
1389
|
// discovered_issue_resolve hard-errored), correctLatestBuildReportEffort,
|
|
1387
|
-
// updateStageExitCriteria, updateDocAction
|
|
1390
|
+
// updateStageExitCriteria, updateDocAction all have
|
|
1388
1391
|
// edge case handlers + ALLOWED_METHODS/WRITE_METHODS entries now, so they forward.
|
|
1389
1392
|
// task-2489 (C320): recordProgressStep is now wired to the edge data-proxy
|
|
1390
1393
|
// (case handler + ALLOWED_METHODS/WRITE_METHODS entries), so it forwards for
|
|
@@ -1968,9 +1971,19 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
1968
1971
|
return this.invoke("updateDocStatus", [id, status, supersededBy]);
|
|
1969
1972
|
}
|
|
1970
1973
|
// --- Cycle Learnings ---
|
|
1971
|
-
|
|
1972
|
-
|
|
1974
|
+
/**
|
|
1975
|
+
* task-2999 / task-2998 (C356): the edge handler now calls append_cycle_learnings
|
|
1976
|
+
* and returns { id, findingKey, inserted } per row, so hosted callers get the same
|
|
1977
|
+
* per-row signal the pg path does. The [] normalisation stays as a floor for an
|
|
1978
|
+
* edge deployed BEFORE that change — callers must read [] as "no signal
|
|
1979
|
+
* available", never as "nothing was new".
|
|
1980
|
+
*/
|
|
1981
|
+
async appendCycleLearnings(learnings) {
|
|
1982
|
+
const result = await this.invoke("appendCycleLearnings", [learnings]);
|
|
1983
|
+
return Array.isArray(result) ? result : [];
|
|
1973
1984
|
}
|
|
1985
|
+
// task-2998: includeResolved was missing here, so the flag could not even be SENT
|
|
1986
|
+
// to the edge — the hosted caught->fixed ledger had no way to ask for history.
|
|
1974
1987
|
getCycleLearnings(opts) {
|
|
1975
1988
|
return this.invoke("getCycleLearnings", [opts]);
|
|
1976
1989
|
}
|
|
@@ -2207,9 +2220,9 @@ var init_query = __esm({
|
|
|
2207
2220
|
CLOSE = {};
|
|
2208
2221
|
Query = class extends Promise {
|
|
2209
2222
|
constructor(strings, args, handler, canceller, options = {}) {
|
|
2210
|
-
let
|
|
2223
|
+
let resolve4, reject;
|
|
2211
2224
|
super((a, b2) => {
|
|
2212
|
-
|
|
2225
|
+
resolve4 = a;
|
|
2213
2226
|
reject = b2;
|
|
2214
2227
|
});
|
|
2215
2228
|
this.tagged = Array.isArray(strings.raw);
|
|
@@ -2220,7 +2233,7 @@ var init_query = __esm({
|
|
|
2220
2233
|
this.options = options;
|
|
2221
2234
|
this.state = null;
|
|
2222
2235
|
this.statement = null;
|
|
2223
|
-
this.resolve = (x) => (this.active = false,
|
|
2236
|
+
this.resolve = (x) => (this.active = false, resolve4(x));
|
|
2224
2237
|
this.reject = (x) => (this.active = false, reject(x));
|
|
2225
2238
|
this.active = false;
|
|
2226
2239
|
this.cancelled = null;
|
|
@@ -2268,12 +2281,12 @@ var init_query = __esm({
|
|
|
2268
2281
|
if (this.executed && !this.active)
|
|
2269
2282
|
return { done: true };
|
|
2270
2283
|
prev && prev();
|
|
2271
|
-
const promise = new Promise((
|
|
2284
|
+
const promise = new Promise((resolve4, reject) => {
|
|
2272
2285
|
this.cursorFn = (value) => {
|
|
2273
|
-
|
|
2286
|
+
resolve4({ value, done: false });
|
|
2274
2287
|
return new Promise((r) => prev = r);
|
|
2275
2288
|
};
|
|
2276
|
-
this.resolve = () => (this.active = false,
|
|
2289
|
+
this.resolve = () => (this.active = false, resolve4({ done: true }));
|
|
2277
2290
|
this.reject = (x) => (this.active = false, reject(x));
|
|
2278
2291
|
});
|
|
2279
2292
|
this.execute();
|
|
@@ -2871,12 +2884,12 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
|
|
2871
2884
|
x.on("drain", drain);
|
|
2872
2885
|
return x;
|
|
2873
2886
|
}
|
|
2874
|
-
async function cancel({ pid, secret },
|
|
2887
|
+
async function cancel({ pid, secret }, resolve4, reject) {
|
|
2875
2888
|
try {
|
|
2876
2889
|
cancelMessage = bytes_default().i32(16).i32(80877102).i32(pid).i32(secret).end(16);
|
|
2877
2890
|
await connect();
|
|
2878
2891
|
socket.once("error", reject);
|
|
2879
|
-
socket.once("close",
|
|
2892
|
+
socket.once("close", resolve4);
|
|
2880
2893
|
} catch (error2) {
|
|
2881
2894
|
reject(error2);
|
|
2882
2895
|
}
|
|
@@ -3893,7 +3906,7 @@ var init_subscribe = __esm({
|
|
|
3893
3906
|
// ../../node_modules/postgres/src/large.js
|
|
3894
3907
|
import Stream2 from "stream";
|
|
3895
3908
|
function largeObject(sql, oid, mode = 131072 | 262144) {
|
|
3896
|
-
return new Promise(async (
|
|
3909
|
+
return new Promise(async (resolve4, reject) => {
|
|
3897
3910
|
await sql.begin(async (sql2) => {
|
|
3898
3911
|
let finish;
|
|
3899
3912
|
!oid && ([{ oid }] = await sql2`select lo_creat(-1) as oid`);
|
|
@@ -3919,7 +3932,7 @@ function largeObject(sql, oid, mode = 131072 | 262144) {
|
|
|
3919
3932
|
) seek
|
|
3920
3933
|
`
|
|
3921
3934
|
};
|
|
3922
|
-
|
|
3935
|
+
resolve4(lo);
|
|
3923
3936
|
return new Promise(async (r) => finish = r);
|
|
3924
3937
|
async function readable({
|
|
3925
3938
|
highWaterMark = 2048 * 8,
|
|
@@ -4084,8 +4097,8 @@ function Postgres(a, b2) {
|
|
|
4084
4097
|
}
|
|
4085
4098
|
async function reserve() {
|
|
4086
4099
|
const queue = queue_default();
|
|
4087
|
-
const c = open.length ? open.shift() : await new Promise((
|
|
4088
|
-
const query = { reserve:
|
|
4100
|
+
const c = open.length ? open.shift() : await new Promise((resolve4, reject) => {
|
|
4101
|
+
const query = { reserve: resolve4, reject };
|
|
4089
4102
|
queries.push(query);
|
|
4090
4103
|
closed.length && connect(closed.shift(), query);
|
|
4091
4104
|
});
|
|
@@ -4122,9 +4135,9 @@ function Postgres(a, b2) {
|
|
|
4122
4135
|
let uncaughtError, result;
|
|
4123
4136
|
name && await sql2`savepoint ${sql2(name)}`;
|
|
4124
4137
|
try {
|
|
4125
|
-
result = await new Promise((
|
|
4138
|
+
result = await new Promise((resolve4, reject) => {
|
|
4126
4139
|
const x = fn2(sql2);
|
|
4127
|
-
Promise.resolve(Array.isArray(x) ? Promise.all(x) : x).then(
|
|
4140
|
+
Promise.resolve(Array.isArray(x) ? Promise.all(x) : x).then(resolve4, reject);
|
|
4128
4141
|
});
|
|
4129
4142
|
if (uncaughtError)
|
|
4130
4143
|
throw uncaughtError;
|
|
@@ -4181,8 +4194,8 @@ function Postgres(a, b2) {
|
|
|
4181
4194
|
return c.execute(query) ? move(c, busy) : move(c, full);
|
|
4182
4195
|
}
|
|
4183
4196
|
function cancel(query) {
|
|
4184
|
-
return new Promise((
|
|
4185
|
-
query.state ? query.active ? connection_default(options).cancel(query.state,
|
|
4197
|
+
return new Promise((resolve4, reject) => {
|
|
4198
|
+
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
4199
|
});
|
|
4187
4200
|
}
|
|
4188
4201
|
async function end({ timeout = null } = {}) {
|
|
@@ -4201,11 +4214,11 @@ function Postgres(a, b2) {
|
|
|
4201
4214
|
async function close() {
|
|
4202
4215
|
await Promise.all(connections.map((c) => c.end()));
|
|
4203
4216
|
}
|
|
4204
|
-
async function destroy(
|
|
4217
|
+
async function destroy(resolve4) {
|
|
4205
4218
|
await Promise.all(connections.map((c) => c.terminate()));
|
|
4206
4219
|
while (queries.length)
|
|
4207
4220
|
queries.shift().reject(Errors.connection("CONNECTION_DESTROYED", options));
|
|
4208
|
-
|
|
4221
|
+
resolve4();
|
|
4209
4222
|
}
|
|
4210
4223
|
function connect(c, query) {
|
|
4211
4224
|
move(c, connecting);
|
|
@@ -4389,7 +4402,7 @@ __export(doctor_exports, {
|
|
|
4389
4402
|
__testing: () => __testing,
|
|
4390
4403
|
runDoctor: () => runDoctor
|
|
4391
4404
|
});
|
|
4392
|
-
import { existsSync as existsSync12, readFileSync as
|
|
4405
|
+
import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
|
|
4393
4406
|
import { homedir as homedir4 } from "os";
|
|
4394
4407
|
import { join as join21 } from "path";
|
|
4395
4408
|
function redact(name, value) {
|
|
@@ -4409,7 +4422,7 @@ function findMcpJson() {
|
|
|
4409
4422
|
for (const path7 of candidates) {
|
|
4410
4423
|
if (!existsSync12(path7)) continue;
|
|
4411
4424
|
try {
|
|
4412
|
-
const raw =
|
|
4425
|
+
const raw = readFileSync15(path7, "utf-8");
|
|
4413
4426
|
const parsed = JSON.parse(raw);
|
|
4414
4427
|
const papiEntry = parsed.papi ?? parsed.mcpServers?.papi;
|
|
4415
4428
|
if (!papiEntry) continue;
|
|
@@ -4687,7 +4700,7 @@ __export(reset_exports, {
|
|
|
4687
4700
|
removePapiEntry: () => removePapiEntry,
|
|
4688
4701
|
runReset: () => runReset
|
|
4689
4702
|
});
|
|
4690
|
-
import { existsSync as existsSync13, readFileSync as
|
|
4703
|
+
import { existsSync as existsSync13, readFileSync as readFileSync16, writeFileSync as writeFileSync8 } from "fs";
|
|
4691
4704
|
import { homedir as homedir5 } from "os";
|
|
4692
4705
|
import { join as join22 } from "path";
|
|
4693
4706
|
import { createInterface } from "readline/promises";
|
|
@@ -4697,7 +4710,7 @@ function findResetTarget() {
|
|
|
4697
4710
|
let raw;
|
|
4698
4711
|
let parsed;
|
|
4699
4712
|
try {
|
|
4700
|
-
raw =
|
|
4713
|
+
raw = readFileSync16(path7, "utf-8");
|
|
4701
4714
|
parsed = JSON.parse(raw);
|
|
4702
4715
|
} catch {
|
|
4703
4716
|
continue;
|
|
@@ -4768,7 +4781,7 @@ async function runReset(args = []) {
|
|
|
4768
4781
|
}
|
|
4769
4782
|
}
|
|
4770
4783
|
try {
|
|
4771
|
-
|
|
4784
|
+
writeFileSync8(target.path, removePapiEntry(target), "utf-8");
|
|
4772
4785
|
process.stdout.write(`
|
|
4773
4786
|
\u2713 Removed papi entry from ${target.path}
|
|
4774
4787
|
`);
|
|
@@ -4798,7 +4811,7 @@ __export(audit_exports, {
|
|
|
4798
4811
|
__testing: () => __testing2,
|
|
4799
4812
|
runAudit: () => runAudit
|
|
4800
4813
|
});
|
|
4801
|
-
import { existsSync as existsSync14, readFileSync as
|
|
4814
|
+
import { existsSync as existsSync14, readFileSync as readFileSync17, readdirSync as readdirSync7 } from "fs";
|
|
4802
4815
|
import { homedir as homedir6 } from "os";
|
|
4803
4816
|
import { join as join23 } from "path";
|
|
4804
4817
|
function safeListDirs(dir) {
|
|
@@ -4819,7 +4832,7 @@ function readMcp(projectPath) {
|
|
|
4819
4832
|
const path7 = join23(projectPath, ".mcp.json");
|
|
4820
4833
|
if (!existsSync14(path7)) return { servers: [] };
|
|
4821
4834
|
try {
|
|
4822
|
-
const parsed = JSON.parse(
|
|
4835
|
+
const parsed = JSON.parse(readFileSync17(path7, "utf-8"));
|
|
4823
4836
|
const mcpServers = parsed.mcpServers ?? {};
|
|
4824
4837
|
const servers = Object.keys(mcpServers);
|
|
4825
4838
|
if (parsed.papi && !servers.includes("papi")) servers.push("papi");
|
|
@@ -4875,7 +4888,7 @@ function readGlobalSkills() {
|
|
|
4875
4888
|
function readGlobalMcpServers() {
|
|
4876
4889
|
if (!existsSync14(GLOBAL_CLAUDE_JSON)) return [];
|
|
4877
4890
|
try {
|
|
4878
|
-
const parsed = JSON.parse(
|
|
4891
|
+
const parsed = JSON.parse(readFileSync17(GLOBAL_CLAUDE_JSON, "utf-8"));
|
|
4879
4892
|
const servers = parsed.mcpServers ?? {};
|
|
4880
4893
|
return Object.keys(servers).sort((a, b2) => a.localeCompare(b2));
|
|
4881
4894
|
} catch {
|
|
@@ -5041,7 +5054,7 @@ var setup_exports = {};
|
|
|
5041
5054
|
__export(setup_exports, {
|
|
5042
5055
|
runSetup: () => runSetup
|
|
5043
5056
|
});
|
|
5044
|
-
import { existsSync as existsSync15, readFileSync as
|
|
5057
|
+
import { existsSync as existsSync15, readFileSync as readFileSync18, writeFileSync as writeFileSync9, chmodSync as chmodSync2, statSync as statSync9 } from "fs";
|
|
5045
5058
|
import { join as join24 } from "path";
|
|
5046
5059
|
function baseUrl() {
|
|
5047
5060
|
const fromEnv = process.env["PAPI_HOST"] ?? process.env["PAPI_BASE_URL"];
|
|
@@ -5071,14 +5084,14 @@ async function postJson(url, body) {
|
|
|
5071
5084
|
return { status: res.status, data };
|
|
5072
5085
|
}
|
|
5073
5086
|
function sleep(ms) {
|
|
5074
|
-
return new Promise((
|
|
5087
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
5075
5088
|
}
|
|
5076
5089
|
function writeMcpJson(opts) {
|
|
5077
5090
|
const path7 = join24(process.cwd(), ".mcp.json");
|
|
5078
5091
|
let parsed = {};
|
|
5079
5092
|
if (existsSync15(path7)) {
|
|
5080
5093
|
try {
|
|
5081
|
-
parsed = JSON.parse(
|
|
5094
|
+
parsed = JSON.parse(readFileSync18(path7, "utf-8"));
|
|
5082
5095
|
} catch {
|
|
5083
5096
|
throw new Error(`.mcp.json at ${path7} is not valid JSON. Fix it or remove it before re-running setup.`);
|
|
5084
5097
|
}
|
|
@@ -5099,9 +5112,9 @@ function writeMcpJson(opts) {
|
|
|
5099
5112
|
}
|
|
5100
5113
|
mcpServers.papi = papiEntry;
|
|
5101
5114
|
parsed.mcpServers = mcpServers;
|
|
5102
|
-
|
|
5115
|
+
writeFileSync9(path7, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5103
5116
|
try {
|
|
5104
|
-
const mode =
|
|
5117
|
+
const mode = statSync9(path7).mode & 511;
|
|
5105
5118
|
if (mode !== 384) chmodSync2(path7, 384);
|
|
5106
5119
|
} catch {
|
|
5107
5120
|
}
|
|
@@ -5209,9 +5222,9 @@ var init_setup = __esm({
|
|
|
5209
5222
|
});
|
|
5210
5223
|
|
|
5211
5224
|
// src/index.ts
|
|
5212
|
-
import { readFileSync as
|
|
5213
|
-
import { dirname as
|
|
5214
|
-
import { fileURLToPath as
|
|
5225
|
+
import { readFileSync as readFileSync19 } from "fs";
|
|
5226
|
+
import { dirname as dirname7, join as join25, basename as basename2 } from "path";
|
|
5227
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
5215
5228
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5216
5229
|
import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5217
5230
|
import {
|
|
@@ -8254,10 +8267,10 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
8254
8267
|
}
|
|
8255
8268
|
|
|
8256
8269
|
// src/server.ts
|
|
8257
|
-
import { readFileSync as
|
|
8270
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
8258
8271
|
import { access as access4, readdir as readdir4, readFile as readFile9 } from "fs/promises";
|
|
8259
|
-
import { join as join20, dirname as
|
|
8260
|
-
import { fileURLToPath as
|
|
8272
|
+
import { join as join20, dirname as dirname6 } from "path";
|
|
8273
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
8261
8274
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
8262
8275
|
import {
|
|
8263
8276
|
CallToolRequestSchema,
|
|
@@ -8284,6 +8297,8 @@ var UNIVERSAL_FRAME = `PAPI gives this project a structured plan \u2192 build \u
|
|
|
8284
8297
|
|
|
8285
8298
|
5. VERIFY BEFORE DONE. Test the change and confirm it works before reporting a task complete. Report failures honestly, with the output.
|
|
8286
8299
|
|
|
8300
|
+
6. NAME THE PROJECT WHEN YOU KNOW IT. If you know which repo this session is working in, pass \`project="<slug>"\` on the call rather than relying on whatever the connection defaults to. An account with more than one project cannot be resolved from a connection with no project bound \u2014 PAPI will stop and ask which one rather than guess, and answering costs a round trip. If PAPI asks, put the list to the user, then re-call with their choice; run \`project_switch\` to make it stick.
|
|
8301
|
+
|
|
8287
8302
|
PAPI reads and writes all project state through these tools \u2014 they are the source of truth, not local files.`;
|
|
8288
8303
|
|
|
8289
8304
|
// src/lib/response.ts
|
|
@@ -9034,7 +9049,7 @@ PRE-BUILD VERIFICATION
|
|
|
9034
9049
|
[List 2-5 specific file paths the builder should read BEFORE implementing to check if the functionality already exists. Derive these from FILES LIKELY TOUCHED \u2014 pick the files most likely to already contain the target functionality. ALSO mandate a docs sweep, not just file-existence (task-2161): name any docs the builder should check via doc_search or the docs index \u2014 a design/research/status:final doc or a prior task may already cover the work. If >80% of the scope is already implemented, the builder should report "already built" instead of re-implementing. Include this section for EVERY task \u2014 it prevents wasted build slots on already-shipped code.]
|
|
9035
9050
|
|
|
9036
9051
|
FILES LIKELY TOUCHED
|
|
9037
|
-
[files \u2014 real paths only. TEST FILE LOCATIONS (task-2606): PAPI tests are NOT co-located next to source in a src/**/__tests__/ folder. Use the actual layout: server tests \u2192 packages/server/tests/<name>.test.ts; adapter-pg tests \u2192 packages/adapter-pg/src/__tests__/<name>.test.ts; dashboard/root tests \u2192 tests/<area>/<name>.test.ts. Do NOT invent packages/server/src/**/__tests__/ paths \u2014 they do not exist.]
|
|
9052
|
+
[files \u2014 real paths only. TEST FILE LOCATIONS (task-2606): PAPI tests are NOT co-located next to source in a src/**/__tests__/ folder. Use the actual layout: server tests \u2192 packages/server/tests/<name>.test.ts; adapter-pg tests \u2192 packages/adapter-pg/src/__tests__/<name>.test.ts; dashboard/root tests \u2192 tests/<area>/<name>.test.ts. Do NOT invent packages/server/src/**/__tests__/ paths \u2014 they do not exist. MCP RESTART RULE (task-2978): whenever FILES LIKELY TOUCHED includes any \`packages/server/**\` path, append this line verbatim to the handoff \u2014 "MCP RESTART REQUIRED: a mid-session \`npm run mcp-build\` writes a dist the already-booted MCP process never loads, so verifying this change through PAPI's own tools before an explicit MCP reconnect is INVALID. Implement \u2192 mcp-build \u2192 reconnect \u2192 only then verify." This is one line inside this section, not a new section.]
|
|
9038
9053
|
|
|
9039
9054
|
EFFORT
|
|
9040
9055
|
[XS/S/M/L/XL]
|
|
@@ -9048,7 +9063,7 @@ After your natural language output, include this EXACT format on its own line:
|
|
|
9048
9063
|
{
|
|
9049
9064
|
"cycleLogTitle": "string \u2014 short descriptive title WITHOUT 'Cycle N' prefix. Should capture the cycle theme in 3-5 words (e.g. 'MCP Quality + Product Readiness' not 'Cycle 5 \u2014 Board Triage \u2014 Bug Fix'). This is the canonical theme label for the cycle.",
|
|
9050
9065
|
"cycleLogContent": "string \u2014 5-10 line cycle log body in markdown, NO heading (the ### heading is generated automatically)",
|
|
9051
|
-
"cycleLogCarryForward": "string or null \u2014 carry-forward
|
|
9066
|
+
"cycleLogCarryForward": "string or null \u2014 carry-forward for the next cycle, in TWO NAMED SECTIONS, product signal FIRST. Section 1 begins with the literal label 'WHAT SHIPS FOR USERS:' and names, concretely, what a person using this project can now see or do, and how to check it \u2014 never a deploy step. Section 2 begins with the literal label 'RELEASE MECHANICS:' and carries everything operational: deploy, publish, migrations, gate order, branch counts, spot-checks. The mechanics are load-bearing and must NOT be dropped or shortened \u2014 they go second, not away. orient parses these two labels and renders the product half first, so the labels are a contract, not decoration. If a cycle genuinely ships nothing user-visible, say so in one line under section 1 rather than omitting the label.",
|
|
9052
9067
|
"cycleLogNotes": "string or null \u2014 1-3 lines of cycle-level observations: estimation accuracy, recurring blockers, velocity trends, dependency signals. Omit if no noteworthy observations.",
|
|
9053
9068
|
"nextMode": "Full",
|
|
9054
9069
|
"boardHealth": "string \u2014 e.g. 5 tasks (3 backlog, 2 done)",
|
|
@@ -9912,7 +9927,7 @@ After your natural language output, include this EXACT format on its own line:
|
|
|
9912
9927
|
\`\`\`json
|
|
9913
9928
|
{
|
|
9914
9929
|
"sessionLogTitle": "string \u2014 Strategy Review title WITHOUT 'Cycle N' prefix (e.g. 'Strategy Review' not 'Cycle 5 \u2014 Strategy Review')",
|
|
9915
|
-
"sessionLogContent": "string \u2014 5-10 line cycle log body summarizing the review, NO heading (the ### heading is generated automatically)",
|
|
9930
|
+
"sessionLogContent": "string \u2014 5-10 line cycle log body summarizing the review, NO heading (the ### heading is generated automatically). LEAD WITH TANGIBLE PRODUCT INSIGHT: open with what was learned about the product, its users, or the market \u2014 a specific finding a reader can act on. Board hygiene, cadence, cycle counts, branch state and other housekeeping are legitimate content but they TRAIL; they must never be the opening line. If the review's most important finding genuinely is an operational one, say why it matters to the product in the same breath rather than reporting it as admin.",
|
|
9916
9931
|
"velocityAssessment": "string \u2014 2-3 sentence velocity summary",
|
|
9917
9932
|
"strategicRecommendations": "string \u2014 key recommendations in markdown",
|
|
9918
9933
|
"activeDecisionUpdates": [
|
|
@@ -10276,7 +10291,7 @@ REFERENCE DOCS
|
|
|
10276
10291
|
[Optional \u2014 paths to docs/ files with background context. Omit if not needed.]
|
|
10277
10292
|
|
|
10278
10293
|
FILES LIKELY TOUCHED
|
|
10279
|
-
[files \u2014 real paths only. TEST FILE LOCATIONS (task-2606): PAPI tests are NOT co-located next to source in a src/**/__tests__/ folder. Use the actual layout: server tests \u2192 packages/server/tests/<name>.test.ts; adapter-pg tests \u2192 packages/adapter-pg/src/__tests__/<name>.test.ts; dashboard/root tests \u2192 tests/<area>/<name>.test.ts. Do NOT invent packages/server/src/**/__tests__/ paths \u2014 they do not exist.]
|
|
10294
|
+
[files \u2014 real paths only. TEST FILE LOCATIONS (task-2606): PAPI tests are NOT co-located next to source in a src/**/__tests__/ folder. Use the actual layout: server tests \u2192 packages/server/tests/<name>.test.ts; adapter-pg tests \u2192 packages/adapter-pg/src/__tests__/<name>.test.ts; dashboard/root tests \u2192 tests/<area>/<name>.test.ts. Do NOT invent packages/server/src/**/__tests__/ paths \u2014 they do not exist. MCP RESTART RULE (task-2978): whenever FILES LIKELY TOUCHED includes any \`packages/server/**\` path, append this line verbatim to the handoff \u2014 "MCP RESTART REQUIRED: a mid-session \`npm run mcp-build\` writes a dist the already-booted MCP process never loads, so verifying this change through PAPI's own tools before an explicit MCP reconnect is INVALID. Implement \u2192 mcp-build \u2192 reconnect \u2192 only then verify." This is one line inside this section, not a new section.]
|
|
10280
10295
|
|
|
10281
10296
|
EFFORT
|
|
10282
10297
|
[XS/S/M/L/XL]`;
|
|
@@ -10641,9 +10656,9 @@ async function getPrompt(name) {
|
|
|
10641
10656
|
if (!endpoint) {
|
|
10642
10657
|
return LOCAL_PROMPTS[name];
|
|
10643
10658
|
}
|
|
10644
|
-
const
|
|
10645
|
-
if (
|
|
10646
|
-
return
|
|
10659
|
+
const cached2 = cache.get(name);
|
|
10660
|
+
if (cached2 && Date.now() - cached2.fetchedAt < CACHE_TTL_MS) {
|
|
10661
|
+
return cached2.content;
|
|
10647
10662
|
}
|
|
10648
10663
|
try {
|
|
10649
10664
|
const url = endpoint.endsWith("/") ? `${endpoint}${name}` : `${endpoint}/${name}`;
|
|
@@ -10656,19 +10671,19 @@ async function getPrompt(name) {
|
|
|
10656
10671
|
}
|
|
10657
10672
|
const response = await fetch(url, { headers, signal: AbortSignal.timeout(1e4) });
|
|
10658
10673
|
if (!response.ok) {
|
|
10659
|
-
if (
|
|
10674
|
+
if (cached2) return cached2.content;
|
|
10660
10675
|
return LOCAL_PROMPTS[name];
|
|
10661
10676
|
}
|
|
10662
10677
|
const data = await response.json();
|
|
10663
10678
|
const content = data.content;
|
|
10664
10679
|
if (typeof content !== "string" || content.length === 0) {
|
|
10665
|
-
if (
|
|
10680
|
+
if (cached2) return cached2.content;
|
|
10666
10681
|
return LOCAL_PROMPTS[name];
|
|
10667
10682
|
}
|
|
10668
10683
|
cache.set(name, { content, fetchedAt: Date.now() });
|
|
10669
10684
|
return content;
|
|
10670
10685
|
} catch {
|
|
10671
|
-
if (
|
|
10686
|
+
if (cached2) return cached2.content;
|
|
10672
10687
|
return LOCAL_PROMPTS[name];
|
|
10673
10688
|
}
|
|
10674
10689
|
}
|
|
@@ -15279,6 +15294,23 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
|
|
|
15279
15294
|
const cappedBrief = capProductBrief2(productBrief);
|
|
15280
15295
|
const smartBoard = formatBoardForReviewSmart(tasks, lastReviewCycleNum);
|
|
15281
15296
|
const buildReportsText = formatRecentReportsSummary(reports, 10);
|
|
15297
|
+
let surpriseDigestText = "";
|
|
15298
|
+
try {
|
|
15299
|
+
if (adapter2.getCycleLearnings) {
|
|
15300
|
+
const surpriseRows = await adapter2.getCycleLearnings({ category: "surprise", limit: 40 });
|
|
15301
|
+
surpriseDigestText = formatSurpriseDigest(surpriseRows.map((s) => ({
|
|
15302
|
+
summary: s.summary,
|
|
15303
|
+
module: s.module,
|
|
15304
|
+
occurrences: s.occurrences,
|
|
15305
|
+
lastSeenCycle: s.lastSeenCycle,
|
|
15306
|
+
cycleNumber: s.cycleNumber
|
|
15307
|
+
})));
|
|
15308
|
+
}
|
|
15309
|
+
} catch {
|
|
15310
|
+
}
|
|
15311
|
+
const buildReportsWithDigest = surpriseDigestText ? `${buildReportsText}
|
|
15312
|
+
|
|
15313
|
+
${surpriseDigestText}` : buildReportsText;
|
|
15282
15314
|
logDataSourceSummary("strategy_review", [
|
|
15283
15315
|
{ label: "productBrief", hasData: warnIfEmpty("readProductBrief", productBrief) },
|
|
15284
15316
|
{ label: "activeDecisions", hasData: warnIfEmpty("getActiveDecisions", decisions) },
|
|
@@ -15510,7 +15542,7 @@ ${lines.join("\n")}`;
|
|
|
15510
15542
|
lastReviewCycle: lastReviewCycleNum,
|
|
15511
15543
|
productBrief: cappedBrief,
|
|
15512
15544
|
activeDecisions: formatActiveDecisionsForReview(decisions),
|
|
15513
|
-
allBuildReports:
|
|
15545
|
+
allBuildReports: buildReportsWithDigest,
|
|
15514
15546
|
sessionLog: formatCycleLog(recentLog),
|
|
15515
15547
|
board: smartBoard,
|
|
15516
15548
|
earnedPushback,
|
|
@@ -16198,9 +16230,31 @@ function formatRecentReportsSummary(reports, count) {
|
|
|
16198
16230
|
if (issues) lines.push(` _Issues:_ ${issues}`);
|
|
16199
16231
|
const arch = trunc(r.architectureNotes, 200);
|
|
16200
16232
|
if (arch) lines.push(` _Architecture:_ ${arch}`);
|
|
16233
|
+
const dead = trunc(r.deadEnds, 200);
|
|
16234
|
+
if (dead) lines.push(` _Dead ends:_ ${dead}`);
|
|
16201
16235
|
return lines.join("\n");
|
|
16202
16236
|
}).join("\n");
|
|
16203
16237
|
}
|
|
16238
|
+
function formatSurpriseDigest(surprises) {
|
|
16239
|
+
if (surprises.length === 0) return "";
|
|
16240
|
+
const top = [...surprises].sort((a, b2) => (b2.occurrences ?? 1) - (a.occurrences ?? 1) || (b2.lastSeenCycle ?? b2.cycleNumber) - (a.lastSeenCycle ?? a.cycleNumber)).slice(0, 8);
|
|
16241
|
+
const byModule = /* @__PURE__ */ new Map();
|
|
16242
|
+
for (const s of top) {
|
|
16243
|
+
const key = s.module?.trim() || "Unattributed";
|
|
16244
|
+
if (!byModule.has(key)) byModule.set(key, []);
|
|
16245
|
+
byModule.get(key).push(s);
|
|
16246
|
+
}
|
|
16247
|
+
const lines = ["**Assumption failures across cycles (surprises):**"];
|
|
16248
|
+
for (const [module, items] of byModule) {
|
|
16249
|
+
lines.push(`- **${module}**`);
|
|
16250
|
+
for (const s of items) {
|
|
16251
|
+
const summary = s.summary.length > 140 ? `${s.summary.slice(0, 140)}...` : s.summary;
|
|
16252
|
+
const seen = (s.occurrences ?? 1) > 1 ? ` (seen ${s.occurrences}\xD7)` : "";
|
|
16253
|
+
lines.push(` - ${summary} \u2014 C${s.lastSeenCycle ?? s.cycleNumber}${seen}`);
|
|
16254
|
+
}
|
|
16255
|
+
}
|
|
16256
|
+
return lines.join("\n");
|
|
16257
|
+
}
|
|
16204
16258
|
function formatPhasesForReview(phases, currentCycle) {
|
|
16205
16259
|
if (phases.length === 0) return void 0;
|
|
16206
16260
|
const lines = [];
|
|
@@ -19750,6 +19804,9 @@ import {
|
|
|
19750
19804
|
isCapabilityEnabled
|
|
19751
19805
|
} from "@papi-ai/shared";
|
|
19752
19806
|
|
|
19807
|
+
// src/tools/build.ts
|
|
19808
|
+
import { validateFindings, MIN_REASON_LENGTH } from "@papi-ai/shared";
|
|
19809
|
+
|
|
19753
19810
|
// src/lib/directive-builders.ts
|
|
19754
19811
|
function buildPrReviewerDirective(caps) {
|
|
19755
19812
|
if (!isCapabilityEnabled(caps, "prReviewer")) return null;
|
|
@@ -19830,6 +19887,7 @@ function buildPapiMetaFramingDirective(caps, inner) {
|
|
|
19830
19887
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
19831
19888
|
import { readdirSync as readdirSync5, existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
19832
19889
|
import { join as join12 } from "path";
|
|
19890
|
+
import { splitFindings, findingKey } from "@papi-ai/shared";
|
|
19833
19891
|
|
|
19834
19892
|
// src/lib/db-only-notices.ts
|
|
19835
19893
|
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.";
|
|
@@ -22388,6 +22446,8 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
22388
22446
|
} catch {
|
|
22389
22447
|
}
|
|
22390
22448
|
}
|
|
22449
|
+
let appendedRows = [];
|
|
22450
|
+
const findingRows = [];
|
|
22391
22451
|
if (adapter2.appendCycleLearnings) {
|
|
22392
22452
|
const learnings = [];
|
|
22393
22453
|
const taskModule = task.module ?? "";
|
|
@@ -22415,10 +22475,52 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
22415
22475
|
relatedDecision: adIds[0]
|
|
22416
22476
|
});
|
|
22417
22477
|
};
|
|
22418
|
-
|
|
22419
|
-
|
|
22478
|
+
const structured = input.findings ?? [];
|
|
22479
|
+
const pushFinding = (f) => {
|
|
22480
|
+
const kind = f.kind ?? "issue";
|
|
22481
|
+
const summary = (f.summary ?? "").trim();
|
|
22482
|
+
if (!summary) return;
|
|
22483
|
+
const severity = ["P0", "P1", "P2", "P3"].includes(String(f.severity)) ? f.severity : void 0;
|
|
22484
|
+
const learning = {
|
|
22485
|
+
taskId: task.id,
|
|
22486
|
+
cycleNumber,
|
|
22487
|
+
category: kind,
|
|
22488
|
+
severity,
|
|
22489
|
+
summary,
|
|
22490
|
+
detail: f.detail,
|
|
22491
|
+
tags: taskModule ? [taskModule.toLowerCase()] : [],
|
|
22492
|
+
relatedDecision: adIds[0],
|
|
22493
|
+
module: f.module ?? taskModule ?? void 0,
|
|
22494
|
+
disposition: f.disposition,
|
|
22495
|
+
dispositionReason: f.reason
|
|
22496
|
+
};
|
|
22497
|
+
findingRows.push({ finding: f, learning });
|
|
22498
|
+
learnings.push(learning);
|
|
22499
|
+
};
|
|
22500
|
+
if (structured.length > 0) {
|
|
22501
|
+
for (const f of structured) pushFinding(f);
|
|
22502
|
+
} else {
|
|
22503
|
+
const blobs = [
|
|
22504
|
+
[input.discoveredIssues, "issue"],
|
|
22505
|
+
[input.surprises, "surprise"],
|
|
22506
|
+
[input.deadEnds, "dead_end"]
|
|
22507
|
+
];
|
|
22508
|
+
for (const [text, kind] of blobs) {
|
|
22509
|
+
for (const parsed of splitFindings(text, kind)) {
|
|
22510
|
+
pushFinding({
|
|
22511
|
+
kind,
|
|
22512
|
+
// splitFindings yields 'note' for unprefixed content; that is not a
|
|
22513
|
+
// severity the column accepts, so it becomes undefined rather than
|
|
22514
|
+
// being coerced into a P-level it never claimed.
|
|
22515
|
+
severity: parsed.severity === "note" ? void 0 : parsed.severity,
|
|
22516
|
+
summary: parsed.summary,
|
|
22517
|
+
detail: parsed.summary,
|
|
22518
|
+
module: taskModule || void 0
|
|
22519
|
+
});
|
|
22520
|
+
}
|
|
22521
|
+
}
|
|
22522
|
+
}
|
|
22420
22523
|
extractLearning(input.architectureNotes, "architecture");
|
|
22421
|
-
extractLearning(input.deadEnds, "dead_end");
|
|
22422
22524
|
if (input.scopeAccuracy && input.scopeAccuracy !== "accurate") {
|
|
22423
22525
|
learnings.push({
|
|
22424
22526
|
taskId: task.id,
|
|
@@ -22430,47 +22532,48 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
22430
22532
|
}
|
|
22431
22533
|
if (learnings.length > 0) {
|
|
22432
22534
|
try {
|
|
22433
|
-
await adapter2.appendCycleLearnings(learnings);
|
|
22434
|
-
} catch {
|
|
22535
|
+
appendedRows = await adapter2.appendCycleLearnings(learnings) ?? [];
|
|
22536
|
+
} catch (err) {
|
|
22537
|
+
console.error(
|
|
22538
|
+
`[papi] appendCycleLearnings failed for ${task.displayId} (${learnings.length} finding(s) NOT recorded): ${err instanceof Error ? err.message : String(err)}`
|
|
22539
|
+
);
|
|
22435
22540
|
}
|
|
22436
22541
|
}
|
|
22437
22542
|
}
|
|
22438
|
-
const AUTO_TRIAGE_MIN_SEVERITY = /* @__PURE__ */ new Set(["P0", "P1"
|
|
22543
|
+
const AUTO_TRIAGE_MIN_SEVERITY = /* @__PURE__ */ new Set(["P0", "P1"]);
|
|
22439
22544
|
let autoTriagedCount = 0;
|
|
22440
22545
|
let autoTriagedSoftSkipped = 0;
|
|
22441
22546
|
const autoTriagedIds = [];
|
|
22442
22547
|
const autoTriagedDupes = [];
|
|
22443
|
-
|
|
22444
|
-
|
|
22445
|
-
|
|
22446
|
-
|
|
22447
|
-
|
|
22448
|
-
|
|
22449
|
-
|
|
22450
|
-
|
|
22451
|
-
|
|
22452
|
-
|
|
22453
|
-
|
|
22454
|
-
} catch {
|
|
22455
|
-
}
|
|
22456
|
-
for (const line of issueLines) {
|
|
22457
|
-
const sevMatch = line.match(/^(P[0-3])[\s:]+/i);
|
|
22458
|
-
if (!sevMatch) continue;
|
|
22459
|
-
const severityLabel = sevMatch[1].toUpperCase();
|
|
22460
|
-
if (!AUTO_TRIAGE_MIN_SEVERITY.has(severityLabel)) {
|
|
22548
|
+
const insertedByKey = /* @__PURE__ */ new Map();
|
|
22549
|
+
for (const row of appendedRows) {
|
|
22550
|
+
if (row.findingKey) insertedByKey.set(row.findingKey, row.inserted);
|
|
22551
|
+
}
|
|
22552
|
+
const haveInsertSignal = appendedRows.length > 0;
|
|
22553
|
+
if (findingRows.length > 0 && typeof adapter2.createTask === "function") {
|
|
22554
|
+
for (const { finding, learning } of findingRows) {
|
|
22555
|
+
if (learning.category !== "issue") continue;
|
|
22556
|
+
if (finding.disposition !== "filed") continue;
|
|
22557
|
+
const severityLabel = learning.severity;
|
|
22558
|
+
if (!severityLabel || !AUTO_TRIAGE_MIN_SEVERITY.has(severityLabel)) {
|
|
22461
22559
|
autoTriagedSoftSkipped++;
|
|
22462
22560
|
continue;
|
|
22463
22561
|
}
|
|
22464
|
-
const
|
|
22465
|
-
|
|
22466
|
-
|
|
22467
|
-
if (!title) continue;
|
|
22468
|
-
const normalized = title.toLowerCase();
|
|
22469
|
-
const dupId = backlogTitleMap.get(normalized);
|
|
22470
|
-
if (dupId) {
|
|
22471
|
-
autoTriagedDupes.push(dupId);
|
|
22562
|
+
const key = findingKey(learning.summary);
|
|
22563
|
+
if (haveInsertSignal && key && insertedByKey.get(key) === false) {
|
|
22564
|
+
autoTriagedDupes.push(learning.summary.slice(0, 60));
|
|
22472
22565
|
continue;
|
|
22473
22566
|
}
|
|
22567
|
+
if (!haveInsertSignal) {
|
|
22568
|
+
autoTriagedSoftSkipped++;
|
|
22569
|
+
console.error(
|
|
22570
|
+
`[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.`
|
|
22571
|
+
);
|
|
22572
|
+
continue;
|
|
22573
|
+
}
|
|
22574
|
+
const priority = severityLabel === "P0" ? "P0 Critical" : "P1 High";
|
|
22575
|
+
const title = learning.summary.length > 120 ? learning.summary.slice(0, 120) : learning.summary;
|
|
22576
|
+
if (!title) continue;
|
|
22474
22577
|
try {
|
|
22475
22578
|
const created = await adapter2.createTask({
|
|
22476
22579
|
uuid: "",
|
|
@@ -22479,20 +22582,17 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
22479
22582
|
status: "Backlog",
|
|
22480
22583
|
priority,
|
|
22481
22584
|
complexity: "Small",
|
|
22482
|
-
module: task.module ?? "",
|
|
22585
|
+
module: finding.module ?? task.module ?? "",
|
|
22483
22586
|
phase: task.phase ?? "",
|
|
22484
22587
|
owner: "papi",
|
|
22485
22588
|
reviewed: false,
|
|
22486
22589
|
taskType: "discovery",
|
|
22487
22590
|
source: "build_complete",
|
|
22488
|
-
notes: `Origin: ${task.displayId} (${task.title}), cycle ${cycleNumber}. Original issue: ${
|
|
22591
|
+
notes: `Origin: ${task.displayId} (${task.title}), cycle ${cycleNumber}. Filed rather than fixed${finding.reason ? `: ${finding.reason}` : ""}. Original issue: ${learning.detail ?? learning.summary}`,
|
|
22489
22592
|
createdCycle: cycleNumber
|
|
22490
22593
|
});
|
|
22491
22594
|
autoTriagedCount++;
|
|
22492
|
-
if (created?.displayId)
|
|
22493
|
-
autoTriagedIds.push(created.displayId);
|
|
22494
|
-
backlogTitleMap.set(normalized, created.displayId);
|
|
22495
|
-
}
|
|
22595
|
+
if (created?.displayId) autoTriagedIds.push(created.displayId);
|
|
22496
22596
|
} catch {
|
|
22497
22597
|
}
|
|
22498
22598
|
}
|
|
@@ -22832,15 +22932,118 @@ ${instructions}`;
|
|
|
22832
22932
|
}
|
|
22833
22933
|
|
|
22834
22934
|
// src/tools/doc-registry.ts
|
|
22835
|
-
import { readdirSync as readdirSync6, existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
|
|
22836
|
-
import { join as join13, relative } from "path";
|
|
22935
|
+
import { readdirSync as readdirSync6, existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4 } from "fs";
|
|
22936
|
+
import { join as join13, relative, isAbsolute as isAbsolute2, dirname as dirname4, resolve as resolve3, sep } from "path";
|
|
22837
22937
|
import { homedir as homedir3 } from "os";
|
|
22838
22938
|
import { randomUUID as randomUUID12 } from "crypto";
|
|
22839
22939
|
import { docDeletionBlockMessage } from "@papi-ai/shared";
|
|
22840
22940
|
init_git();
|
|
22941
|
+
|
|
22942
|
+
// src/services/entitlements.ts
|
|
22943
|
+
import { evaluateContributorGate } from "@papi-ai/shared";
|
|
22944
|
+
var FREE_PROJECT_CAP = 3;
|
|
22945
|
+
var DOC_STORAGE_CEILING_BY_TIER = {
|
|
22946
|
+
free: { bytes: 25 * 1024 * 1024, docs: 200 },
|
|
22947
|
+
pro: { bytes: 250 * 1024 * 1024, docs: 2e3 },
|
|
22948
|
+
team: { bytes: 1024 * 1024 * 1024, docs: 1e4 }
|
|
22949
|
+
};
|
|
22950
|
+
var MAX_DOC_BODY_BYTES = 2 * 1024 * 1024;
|
|
22951
|
+
var PAID_TIERS = /* @__PURE__ */ new Set(["pro", "team"]);
|
|
22952
|
+
var PRICING_URL = "https://getpapi.ai/pricing";
|
|
22953
|
+
async function resolveTier(adapter2) {
|
|
22954
|
+
if (typeof adapter2.getMeteredUsage !== "function") return null;
|
|
22955
|
+
try {
|
|
22956
|
+
const usage = await adapter2.getMeteredUsage();
|
|
22957
|
+
return usage?.tier ?? null;
|
|
22958
|
+
} catch {
|
|
22959
|
+
return null;
|
|
22960
|
+
}
|
|
22961
|
+
}
|
|
22962
|
+
function isPaidTier(tier) {
|
|
22963
|
+
return tier !== null && PAID_TIERS.has(tier);
|
|
22964
|
+
}
|
|
22965
|
+
async function enforceProjectCap(adapter2, target) {
|
|
22966
|
+
const tier = await resolveTier(adapter2);
|
|
22967
|
+
if (tier === null || isPaidTier(tier)) return null;
|
|
22968
|
+
if (typeof adapter2.listUserProjects !== "function") return null;
|
|
22969
|
+
let projects;
|
|
22970
|
+
try {
|
|
22971
|
+
projects = await adapter2.listUserProjects();
|
|
22972
|
+
} catch {
|
|
22973
|
+
return null;
|
|
22974
|
+
}
|
|
22975
|
+
const matchesExisting = projects.some(
|
|
22976
|
+
(p) => target.papiDir && p.papi_dir && p.papi_dir === target.papiDir || target.name && p.name && p.name.trim().toLowerCase() === target.name.trim().toLowerCase()
|
|
22977
|
+
);
|
|
22978
|
+
if (matchesExisting) return null;
|
|
22979
|
+
if (projects.length >= FREE_PROJECT_CAP) return projectCapMessage(projects.length);
|
|
22980
|
+
return null;
|
|
22981
|
+
}
|
|
22982
|
+
async function resolveContributorUpsell(adapter2) {
|
|
22983
|
+
const tier = await resolveTier(adapter2);
|
|
22984
|
+
return evaluateContributorGate(tier).upsell ?? null;
|
|
22985
|
+
}
|
|
22986
|
+
function projectCapMessage(currentCount) {
|
|
22987
|
+
return `**You're on the Free plan (${currentCount} of ${FREE_PROJECT_CAP} projects).**
|
|
22988
|
+
|
|
22989
|
+
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}
|
|
22990
|
+
|
|
22991
|
+
Your existing projects are untouched, and you can keep working in any of them.`;
|
|
22992
|
+
}
|
|
22993
|
+
function humanBytes(n) {
|
|
22994
|
+
if (n >= 1024 * 1024 * 1024) return `${(n / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
22995
|
+
if (n >= 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
22996
|
+
if (n >= 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
22997
|
+
return `${n} bytes`;
|
|
22998
|
+
}
|
|
22999
|
+
async function checkDocStorageCap(adapter2, incomingBytes) {
|
|
23000
|
+
if (incomingBytes > MAX_DOC_BODY_BYTES) {
|
|
23001
|
+
return {
|
|
23002
|
+
storeBody: false,
|
|
23003
|
+
message: `**Body not stored \u2014 this document is ${humanBytes(incomingBytes)}, above the ${humanBytes(MAX_DOC_BODY_BYTES)} per-document limit.**
|
|
23004
|
+
|
|
23005
|
+
The registry entry was saved and everything else is unaffected. Split the document, or keep it as a file in your repo.`
|
|
23006
|
+
};
|
|
23007
|
+
}
|
|
23008
|
+
const tier = await resolveTier(adapter2);
|
|
23009
|
+
if (tier === null) return { storeBody: true };
|
|
23010
|
+
const ceiling = DOC_STORAGE_CEILING_BY_TIER[tier] ?? DOC_STORAGE_CEILING_BY_TIER.free;
|
|
23011
|
+
if (typeof adapter2.getDocBodyUsage !== "function") return { storeBody: true };
|
|
23012
|
+
let usage;
|
|
23013
|
+
try {
|
|
23014
|
+
usage = await adapter2.getDocBodyUsage();
|
|
23015
|
+
} catch {
|
|
23016
|
+
return { storeBody: true };
|
|
23017
|
+
}
|
|
23018
|
+
if (!usage) return { storeBody: true };
|
|
23019
|
+
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}`;
|
|
23020
|
+
if (usage.totalBytes + incomingBytes > ceiling.bytes) {
|
|
23021
|
+
return {
|
|
23022
|
+
storeBody: false,
|
|
23023
|
+
message: `**Body not stored \u2014 you are at your ${tier} plan's document storage ceiling.**
|
|
23024
|
+
|
|
23025
|
+
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.
|
|
23026
|
+
|
|
23027
|
+
${upgrade}`
|
|
23028
|
+
};
|
|
23029
|
+
}
|
|
23030
|
+
if (usage.docCount >= ceiling.docs) {
|
|
23031
|
+
return {
|
|
23032
|
+
storeBody: false,
|
|
23033
|
+
message: `**Body not stored \u2014 you are at your ${tier} plan's stored-document limit.**
|
|
23034
|
+
|
|
23035
|
+
${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.
|
|
23036
|
+
|
|
23037
|
+
${upgrade}`
|
|
23038
|
+
};
|
|
23039
|
+
}
|
|
23040
|
+
return { storeBody: true };
|
|
23041
|
+
}
|
|
23042
|
+
|
|
23043
|
+
// src/tools/doc-registry.ts
|
|
22841
23044
|
var docRegisterTool = {
|
|
22842
23045
|
name: "doc_register",
|
|
22843
|
-
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
|
|
23046
|
+
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.",
|
|
22844
23047
|
annotations: { title: "Register Doc", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
22845
23048
|
inputSchema: {
|
|
22846
23049
|
type: "object",
|
|
@@ -22852,6 +23055,10 @@ var docRegisterTool = {
|
|
|
22852
23055
|
summary: { type: "string", description: 'Structured 2-4 sentence summary. Format: "Conclusions: ... Open questions: ... Unactioned: ..."' },
|
|
22853
23056
|
tags: { type: "array", items: { type: "string" }, description: "Tags from project vocabulary." },
|
|
22854
23057
|
cycle: { type: "number", description: "Current cycle number." },
|
|
23058
|
+
body: {
|
|
23059
|
+
type: "string",
|
|
23060
|
+
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."
|
|
23061
|
+
},
|
|
22855
23062
|
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.' },
|
|
22856
23063
|
actions: {
|
|
22857
23064
|
type: "array",
|
|
@@ -22889,6 +23096,19 @@ var docSearchTool = {
|
|
|
22889
23096
|
required: []
|
|
22890
23097
|
}
|
|
22891
23098
|
};
|
|
23099
|
+
var docReadTool = {
|
|
23100
|
+
name: "doc_read",
|
|
23101
|
+
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.",
|
|
23102
|
+
annotations: { title: "Read Doc", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
23103
|
+
inputSchema: {
|
|
23104
|
+
type: "object",
|
|
23105
|
+
properties: {
|
|
23106
|
+
id_or_path: { type: "string", description: 'Registered path (e.g. "docs/private/notes.md") or the doc UUID.' },
|
|
23107
|
+
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." }
|
|
23108
|
+
},
|
|
23109
|
+
required: ["id_or_path"]
|
|
23110
|
+
}
|
|
23111
|
+
};
|
|
22892
23112
|
var docScanTool = {
|
|
22893
23113
|
name: "doc_scan",
|
|
22894
23114
|
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.",
|
|
@@ -22920,8 +23140,8 @@ function normalizeDocPath(rawPath, projectRoot) {
|
|
|
22920
23140
|
const isWindowsDrive = /^[A-Za-z]:\//.test(fwd);
|
|
22921
23141
|
const isUnc = /^\/\//.test(fwd);
|
|
22922
23142
|
const isPosixAbs = fwd.startsWith("/");
|
|
22923
|
-
const
|
|
22924
|
-
if (!
|
|
23143
|
+
const isAbsolute3 = isWindowsDrive || isUnc || isPosixAbs;
|
|
23144
|
+
if (!isAbsolute3) return { path: fwd };
|
|
22925
23145
|
if (projectRoot) {
|
|
22926
23146
|
const root = projectRoot.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
22927
23147
|
if (fwd.toLowerCase().startsWith(`${root.toLowerCase()}/`)) {
|
|
@@ -23010,6 +23230,14 @@ async function handleDocRegister(adapter2, args, config2) {
|
|
|
23010
23230
|
continueHint
|
|
23011
23231
|
);
|
|
23012
23232
|
}
|
|
23233
|
+
if (!path7.toLowerCase().endsWith(".md")) {
|
|
23234
|
+
return docRegisterSoftFail(
|
|
23235
|
+
adapterType,
|
|
23236
|
+
"path-validation",
|
|
23237
|
+
`Only .md documents can be registered \u2014 "${path7}" is not markdown.`,
|
|
23238
|
+
"The doc registry indexes markdown notes and research docs. Nothing is blocked: continue your flow, and register the .md version if there is one."
|
|
23239
|
+
);
|
|
23240
|
+
}
|
|
23013
23241
|
try {
|
|
23014
23242
|
let supersededBy;
|
|
23015
23243
|
if (supersededByPath) {
|
|
@@ -23032,6 +23260,44 @@ async function handleDocRegister(adapter2, args, config2) {
|
|
|
23032
23260
|
actions,
|
|
23033
23261
|
visibility
|
|
23034
23262
|
});
|
|
23263
|
+
let bodyNote = "";
|
|
23264
|
+
try {
|
|
23265
|
+
const supplied = typeof args.body === "string" ? args.body : void 0;
|
|
23266
|
+
let body = supplied;
|
|
23267
|
+
if (body === void 0 && hasLocalWorkspace()) {
|
|
23268
|
+
try {
|
|
23269
|
+
const abs = isAbsolute2(entry.path) ? entry.path : join13(config2?.projectRoot ?? process.cwd(), entry.path);
|
|
23270
|
+
if (existsSync9(abs)) body = readFileSync8(abs, "utf8");
|
|
23271
|
+
} catch {
|
|
23272
|
+
}
|
|
23273
|
+
}
|
|
23274
|
+
if (body === void 0) {
|
|
23275
|
+
bodyNote = hasLocalWorkspace() ? "\n\n_Body not stored \u2014 the file could not be read from disk. Pass `body` to store it._" : "\n\n_Body not stored \u2014 no local workspace on this session. Pass `body` to store it._";
|
|
23276
|
+
} else if (typeof adapter2.storeDocBody !== "function") {
|
|
23277
|
+
bodyNote = "\n\n_Body not stored \u2014 this adapter does not support body storage._";
|
|
23278
|
+
} else {
|
|
23279
|
+
const decision = await checkDocStorageCap(adapter2, Buffer.byteLength(body, "utf8"));
|
|
23280
|
+
if (!decision.storeBody) {
|
|
23281
|
+
bodyNote = `
|
|
23282
|
+
|
|
23283
|
+
${decision.message}`;
|
|
23284
|
+
} else {
|
|
23285
|
+
const result = await adapter2.storeDocBody({
|
|
23286
|
+
docId: entry.id,
|
|
23287
|
+
body,
|
|
23288
|
+
// Resolved by resolveDocVisibility above — the SAME resolution the
|
|
23289
|
+
// registry row used, so doc_bodies' denormalised copy cannot disagree
|
|
23290
|
+
// with doc_registry on the very first write.
|
|
23291
|
+
visibility: entry.visibility ?? visibility,
|
|
23292
|
+
ownerUserId: entry.ownerUserId
|
|
23293
|
+
});
|
|
23294
|
+
bodyNote = result.stored ? `
|
|
23295
|
+
- **Body stored:** ${result.byteSize.toLocaleString()} bytes` : "\n- **Body:** unchanged since last registration";
|
|
23296
|
+
}
|
|
23297
|
+
}
|
|
23298
|
+
} catch {
|
|
23299
|
+
bodyNote = "\n\n_Body not stored \u2014 storage was unavailable. The registry entry was saved._";
|
|
23300
|
+
}
|
|
23035
23301
|
const visibilityLabel = entry.visibility === "contributors" ? "team member" : entry.visibility ?? "private";
|
|
23036
23302
|
let durability = "";
|
|
23037
23303
|
try {
|
|
@@ -23046,7 +23312,7 @@ async function handleDocRegister(adapter2, args, config2) {
|
|
|
23046
23312
|
- **Visibility:** ${visibilityLabel}
|
|
23047
23313
|
- **Tags:** ${entry.tags.length > 0 ? entry.tags.join(", ") : "none"}
|
|
23048
23314
|
- **Actions:** ${actions?.length ?? 0} items
|
|
23049
|
-
- **ID:** ${entry.id}` + durability
|
|
23315
|
+
- **ID:** ${entry.id}` + bodyNote + durability
|
|
23050
23316
|
);
|
|
23051
23317
|
} catch (err) {
|
|
23052
23318
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -23075,7 +23341,7 @@ async function handleDocSearch(adapter2, args, config2) {
|
|
|
23075
23341
|
const actionCount = d.actions?.filter((a) => a.status === "pending").length ?? 0;
|
|
23076
23342
|
const actionNote = actionCount > 0 ? ` | ${actionCount} pending action(s)` : "";
|
|
23077
23343
|
const missingNote = root && d.path && !existsSync9(join13(root, d.path)) ? `
|
|
23078
|
-
> \u26A0\uFE0F **File missing on disk** \u2014 the registry points at \`${d.path}\` but nothing is there.
|
|
23344
|
+
> \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.` : "";
|
|
23079
23345
|
return `### ${d.title}
|
|
23080
23346
|
**Type:** ${d.type} | **Status:** ${d.status} | **Cycle:** ${d.cycleCreated}${d.cycleUpdated ? `\u2192${d.cycleUpdated}` : ""}${actionNote}
|
|
23081
23347
|
**Path:** ${d.path}${missingNote}
|
|
@@ -23087,6 +23353,96 @@ ${d.summary}
|
|
|
23087
23353
|
|
|
23088
23354
|
${lines.join("\n---\n\n")}`);
|
|
23089
23355
|
}
|
|
23356
|
+
function resolveRestoreTarget(projectRoot, docPath) {
|
|
23357
|
+
const normalized = normalizeDocPath(docPath, projectRoot);
|
|
23358
|
+
if ("error" in normalized) return { error: normalized.error };
|
|
23359
|
+
const root = resolve3(projectRoot);
|
|
23360
|
+
const abs = resolve3(root, normalized.path);
|
|
23361
|
+
if (abs !== root && !abs.startsWith(root + sep)) {
|
|
23362
|
+
return {
|
|
23363
|
+
error: `Refusing to write outside the project root. The registered path \`${docPath}\` resolves to \`${abs}\`, which is not under \`${root}\`.`
|
|
23364
|
+
};
|
|
23365
|
+
}
|
|
23366
|
+
return { abs };
|
|
23367
|
+
}
|
|
23368
|
+
async function handleDocRead(adapter2, config2, args) {
|
|
23369
|
+
if (!adapter2.getDoc || typeof adapter2.getDocBody !== "function") {
|
|
23370
|
+
return errorResponse(
|
|
23371
|
+
"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."
|
|
23372
|
+
);
|
|
23373
|
+
}
|
|
23374
|
+
const idOrPath = args.id_or_path?.trim();
|
|
23375
|
+
if (!idOrPath) {
|
|
23376
|
+
return errorResponse("id_or_path is required \u2014 pass the doc's registered path or its UUID.");
|
|
23377
|
+
}
|
|
23378
|
+
const writeToDisk = args.write_to_disk === true;
|
|
23379
|
+
const doc = await adapter2.getDoc(idOrPath);
|
|
23380
|
+
if (!doc) {
|
|
23381
|
+
return errorResponse(
|
|
23382
|
+
`No registered doc matches \`${idOrPath}\` in this project. Check \`doc_search\` for the exact path, or \`doc_scan\` for unregistered files.`
|
|
23383
|
+
);
|
|
23384
|
+
}
|
|
23385
|
+
const gate = await resolveOwnerGate(adapter2, config2);
|
|
23386
|
+
const requesterUserId = gate.enforced ? gate.callerUserId : gate.ownerUserId ?? gate.callerUserId;
|
|
23387
|
+
const stored = await adapter2.getDocBody(doc.id, requesterUserId);
|
|
23388
|
+
const header = `**${doc.title}**
|
|
23389
|
+
- **Path:** ${doc.path}
|
|
23390
|
+
- **Type:** ${doc.type} | **Status:** ${doc.status} | **Visibility:** ${doc.visibility ?? "private"}
|
|
23391
|
+
`;
|
|
23392
|
+
if (!stored) {
|
|
23393
|
+
return textResponse(
|
|
23394
|
+
header + `- **Body:** not stored
|
|
23395
|
+
|
|
23396
|
+
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.
|
|
23397
|
+
|
|
23398
|
+
**Fix:** re-run \`doc_register\` for \`${doc.path}\` with the file present, or pass \`body\` explicitly.`
|
|
23399
|
+
);
|
|
23400
|
+
}
|
|
23401
|
+
if (!stored.permitted) {
|
|
23402
|
+
return errorResponse(
|
|
23403
|
+
`\`${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.")
|
|
23404
|
+
);
|
|
23405
|
+
}
|
|
23406
|
+
const body = stored.body ?? "";
|
|
23407
|
+
let restoreNote = "";
|
|
23408
|
+
if (writeToDisk) {
|
|
23409
|
+
restoreNote = restoreBodyToDisk(config2.projectRoot, doc.path, body);
|
|
23410
|
+
}
|
|
23411
|
+
return textResponse(
|
|
23412
|
+
header + `- **Stored:** ${stored.byteSize.toLocaleString()} bytes, updated ${stored.updatedAt}
|
|
23413
|
+
` + restoreNote + `
|
|
23414
|
+
---
|
|
23415
|
+
|
|
23416
|
+
${body}`
|
|
23417
|
+
);
|
|
23418
|
+
}
|
|
23419
|
+
function restoreBodyToDisk(projectRoot, docPath, body) {
|
|
23420
|
+
if (!hasLocalWorkspace()) {
|
|
23421
|
+
return "- **Not restored:** this session has no local workspace (hosted transport). The body is above \u2014 write it yourself.\n";
|
|
23422
|
+
}
|
|
23423
|
+
const target = resolveRestoreTarget(projectRoot, docPath);
|
|
23424
|
+
if ("error" in target) return `- **Not restored:** ${target.error}
|
|
23425
|
+
`;
|
|
23426
|
+
try {
|
|
23427
|
+
if (existsSync9(target.abs)) {
|
|
23428
|
+
const onDisk = readFileSync8(target.abs, "utf8");
|
|
23429
|
+
if (onDisk === body) {
|
|
23430
|
+
return `- **Already on disk:** \`${docPath}\` is byte-identical to the stored body. Nothing written.
|
|
23431
|
+
`;
|
|
23432
|
+
}
|
|
23433
|
+
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.
|
|
23434
|
+
`;
|
|
23435
|
+
}
|
|
23436
|
+
mkdirSync4(dirname4(target.abs), { recursive: true });
|
|
23437
|
+
writeFileSync5(target.abs, body, "utf8");
|
|
23438
|
+
return `- **Restored:** wrote ${Buffer.byteLength(body, "utf8").toLocaleString()} bytes to \`${docPath}\`
|
|
23439
|
+
`;
|
|
23440
|
+
} catch (err) {
|
|
23441
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
23442
|
+
return `- **Not restored:** writing \`${docPath}\` failed \u2014 ${message}. The body is printed below.
|
|
23443
|
+
`;
|
|
23444
|
+
}
|
|
23445
|
+
}
|
|
23090
23446
|
function scanMdFiles(dir, rootDir) {
|
|
23091
23447
|
if (!existsSync9(dir)) return [];
|
|
23092
23448
|
const files = [];
|
|
@@ -23529,6 +23885,24 @@ var buildExecuteTool = {
|
|
|
23529
23885
|
},
|
|
23530
23886
|
required: ["urls", "curl_command", "http_status", "response_excerpt", "verified_at"]
|
|
23531
23887
|
},
|
|
23888
|
+
findings: {
|
|
23889
|
+
type: "array",
|
|
23890
|
+
maxItems: 12,
|
|
23891
|
+
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.',
|
|
23892
|
+
items: {
|
|
23893
|
+
type: "object",
|
|
23894
|
+
properties: {
|
|
23895
|
+
kind: { type: "string", enum: ["issue", "dead_end", "surprise"], description: "What kind of finding. Only `issue` carries a disposition." },
|
|
23896
|
+
severity: { type: "string", enum: ["P0", "P1", "P2", "P3"], description: "Severity, for issues." },
|
|
23897
|
+
summary: { type: "string", maxLength: 200, description: 'One line. No "Pn:" prefix \u2014 severity has its own field.' },
|
|
23898
|
+
detail: { type: "string", maxLength: 600, description: "The verbatim context: what breaks, where, and how it reproduces." },
|
|
23899
|
+
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." },
|
|
23900
|
+
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." },
|
|
23901
|
+
module: { type: "string", description: "Module the finding belongs to. Defaults to the task's module." }
|
|
23902
|
+
},
|
|
23903
|
+
required: ["kind", "summary"]
|
|
23904
|
+
}
|
|
23905
|
+
},
|
|
23532
23906
|
preview: {
|
|
23533
23907
|
type: "object",
|
|
23534
23908
|
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.`,
|
|
@@ -23859,6 +24233,24 @@ If this build fixes any of these, pass their UUIDs in \`fixed_issues\` on comple
|
|
|
23859
24233
|
}
|
|
23860
24234
|
} catch {
|
|
23861
24235
|
}
|
|
24236
|
+
let ruledOutSection = "";
|
|
24237
|
+
try {
|
|
24238
|
+
const moduleTag = result.task.module?.trim();
|
|
24239
|
+
if (moduleTag && adapter2.getCycleLearnings) {
|
|
24240
|
+
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);
|
|
24241
|
+
if (deadEnds.length > 0) {
|
|
24242
|
+
const rows = deadEnds.map((l) => `- ${l.summary.length > 180 ? `${l.summary.slice(0, 180)}...` : l.summary}`).join("\n");
|
|
24243
|
+
ruledOutSection = `
|
|
24244
|
+
|
|
24245
|
+
---
|
|
24246
|
+
|
|
24247
|
+
**RULED OUT IN ${moduleTag} \u2014 do not re-walk these:**
|
|
24248
|
+
${rows}
|
|
24249
|
+
These approaches were tried in this module and failed. If one looks right, read why it was ruled out before spending time on it.`;
|
|
24250
|
+
}
|
|
24251
|
+
}
|
|
24252
|
+
} catch {
|
|
24253
|
+
}
|
|
23862
24254
|
const moduleInstructions = getModuleInstructions(result.task.module);
|
|
23863
24255
|
const moduleContext = await getModuleContext(adapter2, result.task);
|
|
23864
24256
|
const filesToWriteSection = result.filesToWrite ? formatFilesToWriteSection(result.filesToWrite) : "";
|
|
@@ -23868,7 +24260,7 @@ If this build fixes any of these, pass their UUIDs in \`fixed_issues\` on comple
|
|
|
23868
24260
|
) ?? "";
|
|
23869
24261
|
const gestaltNote = buildGestaltPreBuildDirective(caps) ?? "";
|
|
23870
24262
|
const buildDisciplineSection = buildPapiMetaFramingDirective(caps, buildDisciplineNote) ?? "";
|
|
23871
|
-
return textResponse(resumeNote + header + serializeBuildHandoff(result.task.buildHandoff) + modelNote + gestaltNote + adSection + moduleInstructions + moduleContext + dogfoodSection + openIssuesSection + verificationNote + buildDisciplineSection + chainInstruction + phaseNote + filesToWriteSection);
|
|
24263
|
+
return textResponse(resumeNote + header + serializeBuildHandoff(result.task.buildHandoff) + modelNote + gestaltNote + adSection + moduleInstructions + moduleContext + dogfoodSection + openIssuesSection + ruledOutSection + verificationNote + buildDisciplineSection + chainInstruction + phaseNote + filesToWriteSection);
|
|
23872
24264
|
} catch (err) {
|
|
23873
24265
|
if (isNoHandoffError(err)) {
|
|
23874
24266
|
const lines = [
|
|
@@ -23957,6 +24349,20 @@ async function handleExecuteComplete(adapter2, config2, taskId, args, light = fa
|
|
|
23957
24349
|
if (!parsedEstimatedEffort) {
|
|
23958
24350
|
return errorResponse(`Invalid estimated_effort value "${estimatedEffort}". Must be one of: XS, S, M, L, XL.`);
|
|
23959
24351
|
}
|
|
24352
|
+
const rawFindings = Array.isArray(args.findings) ? args.findings : void 0;
|
|
24353
|
+
const findingsCheck = validateFindings(rawFindings);
|
|
24354
|
+
if (!findingsCheck.ok) {
|
|
24355
|
+
const offenders = findingsCheck.violations.map((v, i) => `${i + 1}. ${v.message}`).join("\n\n");
|
|
24356
|
+
return textResponse(
|
|
24357
|
+
`**${findingsCheck.violations.length} finding${findingsCheck.violations.length === 1 ? "" : "s"} need a disposition or a reason before ${taskId} can complete.**
|
|
24358
|
+
|
|
24359
|
+
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.
|
|
24360
|
+
|
|
24361
|
+
${offenders}
|
|
24362
|
+
|
|
24363
|
+
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.`
|
|
24364
|
+
);
|
|
24365
|
+
}
|
|
23960
24366
|
const acceptanceConfirmed = args.acceptance_confirmed === true;
|
|
23961
24367
|
if (completed === "yes" && !acceptanceConfirmed) {
|
|
23962
24368
|
const gateInfo = adapter2.getProjectInfo ? await adapter2.getProjectInfo().catch(() => null) : null;
|
|
@@ -24003,7 +24409,9 @@ Your report was NOT discarded and the task is NOT yet Done \u2014 re-send with \
|
|
|
24003
24409
|
detail: bi.detail ?? ""
|
|
24004
24410
|
})),
|
|
24005
24411
|
productionVerification,
|
|
24006
|
-
preview
|
|
24412
|
+
preview,
|
|
24413
|
+
// task-3001: already gate-validated above, so the service can trust it.
|
|
24414
|
+
findings: rawFindings
|
|
24007
24415
|
}, { light }, clientName);
|
|
24008
24416
|
tracker.mark("complete_format");
|
|
24009
24417
|
tracker.setStreamScope({ taskId: result.task.displayId ?? result.task.id, cycle: result.cycleNumber });
|
|
@@ -24028,9 +24436,10 @@ Your report was NOT discarded and the task is NOT yet Done \u2014 re-send with \
|
|
|
24028
24436
|
}
|
|
24029
24437
|
let fixedResolvedCount = 0;
|
|
24030
24438
|
if (fixedIssues && fixedIssues.length > 0 && typeof adapter2.markCycleLearningResolved === "function") {
|
|
24439
|
+
const resolvedBy = `build:${result.task.displayId ?? taskId}`;
|
|
24031
24440
|
for (const learningId of fixedIssues) {
|
|
24032
24441
|
try {
|
|
24033
|
-
await adapter2.markCycleLearningResolved(learningId,
|
|
24442
|
+
await adapter2.markCycleLearningResolved(learningId, resolvedBy);
|
|
24034
24443
|
fixedResolvedCount++;
|
|
24035
24444
|
} catch {
|
|
24036
24445
|
}
|
|
@@ -24283,7 +24692,7 @@ var ideaTool = {
|
|
|
24283
24692
|
},
|
|
24284
24693
|
project: {
|
|
24285
24694
|
type: "string",
|
|
24286
|
-
description: "Project id (UUID) or slug to write this idea to, overriding the session project for THIS call only. Must be a project on your account \u2014 fails closed otherwise. Use project_switch to change the session default."
|
|
24695
|
+
description: "Project id (UUID) or slug to write this idea to, overriding the session project for THIS call only. Must be a project on your account \u2014 fails closed otherwise. Use project_switch to change the session default. PASS THIS WHENEVER YOU KNOW WHICH REPO THE SESSION IS IN: on a multi-project account a connection with no project bound cannot be resolved, and PAPI will stop and ask rather than guess."
|
|
24287
24696
|
}
|
|
24288
24697
|
},
|
|
24289
24698
|
required: ["text"]
|
|
@@ -24730,7 +25139,7 @@ var backlogImportTool = {
|
|
|
24730
25139
|
},
|
|
24731
25140
|
project: {
|
|
24732
25141
|
type: "string",
|
|
24733
|
-
description: "Project id (UUID) or slug to import into, overriding the session project for THIS call only. Must be a project on your account \u2014 fails closed otherwise."
|
|
25142
|
+
description: "Project id (UUID) or slug to import into, overriding the session project for THIS call only. Must be a project on your account \u2014 fails closed otherwise. PASS THIS WHENEVER YOU KNOW WHICH REPO THE SESSION IS IN: on a multi-project account a connection with no project bound cannot be resolved, and PAPI will stop and ask rather than guess."
|
|
24734
25143
|
}
|
|
24735
25144
|
},
|
|
24736
25145
|
required: ["source"]
|
|
@@ -24938,7 +25347,7 @@ var bugTool = {
|
|
|
24938
25347
|
},
|
|
24939
25348
|
project: {
|
|
24940
25349
|
type: "string",
|
|
24941
|
-
description: "BOARD MODE ONLY. Project id (UUID) or slug to file this bug under, overriding the session project for THIS call only. Must be a project on your account \u2014 fails closed otherwise. It CANNOT redirect an upstream (report=true) submission \u2014 those always go to PAPI maintainers. Use project_switch to change the session default."
|
|
25350
|
+
description: "BOARD MODE ONLY. Project id (UUID) or slug to file this bug under, overriding the session project for THIS call only. Must be a project on your account \u2014 fails closed otherwise. It CANNOT redirect an upstream (report=true) submission \u2014 those always go to PAPI maintainers. Use project_switch to change the session default. PASS THIS WHENEVER YOU KNOW WHICH REPO THE SESSION IS IN: on a multi-project account a connection with no project bound cannot be resolved, and PAPI will stop and ask rather than guess."
|
|
24942
25351
|
}
|
|
24943
25352
|
},
|
|
24944
25353
|
required: ["text"]
|
|
@@ -27256,8 +27665,8 @@ Path: ${mcpJsonPath}`
|
|
|
27256
27665
|
const fileBody = target.render(envVars);
|
|
27257
27666
|
const wroteToShared = !target.isProjectScoped && !!existingConfig;
|
|
27258
27667
|
if (wroteToShared) {
|
|
27259
|
-
const
|
|
27260
|
-
await writeMcpConfig(mcpJsonPath, target.configPath, existingConfig +
|
|
27668
|
+
const sep2 = existingConfig.endsWith("\n") ? "\n" : "\n\n";
|
|
27669
|
+
await writeMcpConfig(mcpJsonPath, target.configPath, existingConfig + sep2 + fileBody, target.isProjectScoped, config2.adapterType, collector);
|
|
27261
27670
|
} else {
|
|
27262
27671
|
await writeMcpConfig(mcpJsonPath, target.configPath, fileBody, target.isProjectScoped, config2.adapterType, collector);
|
|
27263
27672
|
}
|
|
@@ -27328,8 +27737,8 @@ ${writeNote}
|
|
|
27328
27737
|
const fileBody = target.render(envVars);
|
|
27329
27738
|
const wroteToShared = !target.isProjectScoped && !!existingConfig;
|
|
27330
27739
|
if (wroteToShared) {
|
|
27331
|
-
const
|
|
27332
|
-
await writeMcpConfig(mcpJsonPath, target.configPath, existingConfig +
|
|
27740
|
+
const sep2 = existingConfig.endsWith("\n") ? "\n" : "\n\n";
|
|
27741
|
+
await writeMcpConfig(mcpJsonPath, target.configPath, existingConfig + sep2 + fileBody, target.isProjectScoped, config2.adapterType, collector);
|
|
27333
27742
|
} else {
|
|
27334
27743
|
await writeMcpConfig(mcpJsonPath, target.configPath, fileBody, target.isProjectScoped, config2.adapterType, collector);
|
|
27335
27744
|
}
|
|
@@ -27372,8 +27781,8 @@ ${writeNote}
|
|
|
27372
27781
|
const fileBody = target.render(envVars);
|
|
27373
27782
|
const wroteToShared = !target.isProjectScoped && !!existingConfig;
|
|
27374
27783
|
if (wroteToShared) {
|
|
27375
|
-
const
|
|
27376
|
-
await writeMcpConfig(mcpJsonPath, target.configPath, existingConfig +
|
|
27784
|
+
const sep2 = existingConfig.endsWith("\n") ? "\n" : "\n\n";
|
|
27785
|
+
await writeMcpConfig(mcpJsonPath, target.configPath, existingConfig + sep2 + fileBody, target.isProjectScoped, config2.adapterType, collector);
|
|
27377
27786
|
} else {
|
|
27378
27787
|
await writeMcpConfig(mcpJsonPath, target.configPath, fileBody, target.isProjectScoped, config2.adapterType, collector);
|
|
27379
27788
|
}
|
|
@@ -27841,6 +28250,50 @@ function formatUnblockSection(candidates) {
|
|
|
27841
28250
|
return lines.join("\n");
|
|
27842
28251
|
}
|
|
27843
28252
|
|
|
28253
|
+
// src/lib/carry-forward-shape.ts
|
|
28254
|
+
var PRODUCT_MARKER = /WHAT SHIPS FOR USERS\b[^\n:]*:/i;
|
|
28255
|
+
var MECHANICS_MARKER = /RELEASE MECHANICS\b[^\n:]*:/i;
|
|
28256
|
+
function splitCarryForward(raw) {
|
|
28257
|
+
const productMatch = PRODUCT_MARKER.exec(raw);
|
|
28258
|
+
const mechanicsMatch = MECHANICS_MARKER.exec(raw);
|
|
28259
|
+
if (!productMatch && !mechanicsMatch) {
|
|
28260
|
+
return { product: raw.trim(), mechanics: "", split: false };
|
|
28261
|
+
}
|
|
28262
|
+
if (!productMatch && mechanicsMatch) {
|
|
28263
|
+
const head = raw.slice(0, mechanicsMatch.index).trim();
|
|
28264
|
+
const tail = raw.slice(mechanicsMatch.index + mechanicsMatch[0].length).trim();
|
|
28265
|
+
return { product: head, mechanics: tail, split: true };
|
|
28266
|
+
}
|
|
28267
|
+
const productStart = productMatch.index + productMatch[0].length;
|
|
28268
|
+
if (!mechanicsMatch) {
|
|
28269
|
+
return { product: raw.slice(productStart).trim(), mechanics: "", split: true };
|
|
28270
|
+
}
|
|
28271
|
+
if (mechanicsMatch.index < productMatch.index) {
|
|
28272
|
+
return {
|
|
28273
|
+
product: raw.slice(productStart).trim(),
|
|
28274
|
+
mechanics: raw.slice(mechanicsMatch.index + mechanicsMatch[0].length, productMatch.index).trim(),
|
|
28275
|
+
split: true
|
|
28276
|
+
};
|
|
28277
|
+
}
|
|
28278
|
+
return {
|
|
28279
|
+
product: raw.slice(productStart, mechanicsMatch.index).trim(),
|
|
28280
|
+
mechanics: raw.slice(mechanicsMatch.index + mechanicsMatch[0].length).trim(),
|
|
28281
|
+
split: true
|
|
28282
|
+
};
|
|
28283
|
+
}
|
|
28284
|
+
function renderCarryForward(raw, decorate = (t) => t) {
|
|
28285
|
+
const { product, mechanics, split } = splitCarryForward(raw);
|
|
28286
|
+
if (!split) return [decorate(product)];
|
|
28287
|
+
const lines = [];
|
|
28288
|
+
if (product) lines.push(decorate(product));
|
|
28289
|
+
if (mechanics) {
|
|
28290
|
+
if (product) lines.push("");
|
|
28291
|
+
lines.push("**Release mechanics** \u2014 needed at release, not now:");
|
|
28292
|
+
lines.push(decorate(mechanics));
|
|
28293
|
+
}
|
|
28294
|
+
return lines;
|
|
28295
|
+
}
|
|
28296
|
+
|
|
27844
28297
|
// src/lib/deferred-gate.ts
|
|
27845
28298
|
var GATE_PHRASES = [
|
|
27846
28299
|
"depends on",
|
|
@@ -28113,8 +28566,94 @@ async function verifyProject(adapter2) {
|
|
|
28113
28566
|
// src/tools/orient.ts
|
|
28114
28567
|
import { execFile as execFile2 } from "child_process";
|
|
28115
28568
|
import { promisify as promisify2 } from "util";
|
|
28116
|
-
import { readFileSync as readFileSync11, writeFileSync as
|
|
28569
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync6, existsSync as existsSync11 } from "fs";
|
|
28117
28570
|
import { join as join17 } from "path";
|
|
28571
|
+
|
|
28572
|
+
// src/lib/exit-criteria-evaluators.ts
|
|
28573
|
+
var TEST_ACCOUNT_PATTERN = "ftue-%@test.papi.dev";
|
|
28574
|
+
function sqlOf(adapter2) {
|
|
28575
|
+
const candidate = adapter2.sql;
|
|
28576
|
+
return typeof candidate === "function" ? candidate : void 0;
|
|
28577
|
+
}
|
|
28578
|
+
var evaluateUnaidedFullLoop = async (adapter2, projectId) => {
|
|
28579
|
+
const sql = sqlOf(adapter2);
|
|
28580
|
+
if (!sql) return { met: null, unevaluatedReason: "no direct SQL access on this adapter (hosted path)" };
|
|
28581
|
+
const rows = await sql`
|
|
28582
|
+
SELECT count(DISTINCT p.user_id)::text AS builders, count(*)::text AS cycles
|
|
28583
|
+
FROM cycles c
|
|
28584
|
+
JOIN projects p ON p.id = c.project_id
|
|
28585
|
+
JOIN auth.users u ON u.id = p.user_id
|
|
28586
|
+
WHERE c.status = 'complete'
|
|
28587
|
+
AND p.user_id IS DISTINCT FROM (SELECT user_id FROM projects WHERE id = ${projectId})
|
|
28588
|
+
AND u.email NOT LIKE ${TEST_ACCOUNT_PATTERN}
|
|
28589
|
+
`;
|
|
28590
|
+
const builders2 = Number(rows[0]?.builders ?? 0);
|
|
28591
|
+
const cycles = Number(rows[0]?.cycles ?? 0);
|
|
28592
|
+
return builders2 >= 1 ? {
|
|
28593
|
+
met: null,
|
|
28594
|
+
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()}.`,
|
|
28595
|
+
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`
|
|
28596
|
+
} : { met: false, evidence: `0 completed cycles on non-owner projects. Counted ${(/* @__PURE__ */ new Date()).toISOString()}.` };
|
|
28597
|
+
};
|
|
28598
|
+
var evaluateSelfServeActivation = async (adapter2, projectId) => {
|
|
28599
|
+
const sql = sqlOf(adapter2);
|
|
28600
|
+
if (!sql) return { met: null, unevaluatedReason: "no direct SQL access on this adapter (hosted path)" };
|
|
28601
|
+
const rows = await sql`
|
|
28602
|
+
SELECT count(DISTINCT p.user_id)::text AS users
|
|
28603
|
+
FROM cycles c
|
|
28604
|
+
JOIN projects p ON p.id = c.project_id
|
|
28605
|
+
JOIN auth.users u ON u.id = p.user_id
|
|
28606
|
+
WHERE p.user_id IS DISTINCT FROM (SELECT user_id FROM projects WHERE id = ${projectId})
|
|
28607
|
+
AND u.email NOT LIKE ${TEST_ACCOUNT_PATTERN}
|
|
28608
|
+
AND EXISTS (SELECT 1 FROM cycle_tasks t WHERE t.cycle = c.number AND t.project_id = c.project_id)
|
|
28609
|
+
`;
|
|
28610
|
+
const users = Number(rows[0]?.users ?? 0);
|
|
28611
|
+
return users >= 1 ? {
|
|
28612
|
+
met: null,
|
|
28613
|
+
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()}.`,
|
|
28614
|
+
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`
|
|
28615
|
+
} : { met: false, evidence: `0 non-owner accounts with a planned cycle. Counted ${(/* @__PURE__ */ new Date()).toISOString()}.` };
|
|
28616
|
+
};
|
|
28617
|
+
var EVALUATORS = {
|
|
28618
|
+
"c4a11d42-b222-45af-a470-08970e1bd6a9": evaluateSelfServeActivation,
|
|
28619
|
+
"59751bcb-f719-4cf6-a4d4-608770999a22": evaluateUnaidedFullLoop
|
|
28620
|
+
};
|
|
28621
|
+
var UNEVALUATABLE = {
|
|
28622
|
+
"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",
|
|
28623
|
+
"c596a082-9ad3-4089-b497-46cd189e892b": "threshold is provisional pending an owner ruling \u2014 no settled bar to evaluate against"
|
|
28624
|
+
};
|
|
28625
|
+
async function evaluateExitCriteria(adapter2, projectId, criteria) {
|
|
28626
|
+
return Promise.all(criteria.map(async (c) => {
|
|
28627
|
+
const evaluator = EVALUATORS[c.id];
|
|
28628
|
+
if (!evaluator) {
|
|
28629
|
+
const reason = UNEVALUATABLE[c.id];
|
|
28630
|
+
if (!reason) return { ...c, met: c.met, autoEvaluated: false };
|
|
28631
|
+
return c.met ? { ...c, met: true, autoEvaluated: false } : { ...c, met: null, unevaluatedReason: reason, autoEvaluated: false };
|
|
28632
|
+
}
|
|
28633
|
+
try {
|
|
28634
|
+
const result = await evaluator(adapter2, projectId);
|
|
28635
|
+
return {
|
|
28636
|
+
...c,
|
|
28637
|
+
met: result.met,
|
|
28638
|
+
evidence: result.evidence ?? c.evidence,
|
|
28639
|
+
unevaluatedReason: result.unevaluatedReason,
|
|
28640
|
+
autoEvaluated: result.met !== null
|
|
28641
|
+
};
|
|
28642
|
+
} catch (err) {
|
|
28643
|
+
return {
|
|
28644
|
+
...c,
|
|
28645
|
+
met: c.met ? true : null,
|
|
28646
|
+
unevaluatedReason: `evaluator failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
28647
|
+
autoEvaluated: false
|
|
28648
|
+
};
|
|
28649
|
+
}
|
|
28650
|
+
}));
|
|
28651
|
+
}
|
|
28652
|
+
function criterionMarker(met) {
|
|
28653
|
+
return met === true ? "[x]" : met === false ? "[ ]" : "[?]";
|
|
28654
|
+
}
|
|
28655
|
+
|
|
28656
|
+
// src/tools/orient.ts
|
|
28118
28657
|
var execFileAsync2 = promisify2(execFile2);
|
|
28119
28658
|
var GIT_DEPENDENT_ENVS = /* @__PURE__ */ new Set(["hosted", "api"]);
|
|
28120
28659
|
var VALID_ENVS = /* @__PURE__ */ new Set(["local-cli", "hosted", "api", "unknown"]);
|
|
@@ -28323,9 +28862,15 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
|
|
|
28323
28862
|
}
|
|
28324
28863
|
if (hierarchy.stageExitCriteria && hierarchy.stageExitCriteria.length > 0) {
|
|
28325
28864
|
const crit = hierarchy.stageExitCriteria;
|
|
28326
|
-
const met = crit.filter((c) => c.met).length;
|
|
28327
28865
|
const total = crit.length;
|
|
28328
|
-
|
|
28866
|
+
const met = crit.filter((c) => c.met === true).length;
|
|
28867
|
+
const unevaluated = crit.filter((c) => c.met === null || c.met === void 0).length;
|
|
28868
|
+
const countLabel = unevaluated > 0 ? `${met}/${total} met, ${unevaluated} unevaluated` : `${met}/${total} met`;
|
|
28869
|
+
lines.push(`**Stage Exit Criteria [${countLabel}]:** ${crit.map((c) => `${criterionMarker(c.met ?? null)} ${c.text}`).join(" | ")}`);
|
|
28870
|
+
for (const c of crit) {
|
|
28871
|
+
const reason = c.unevaluatedReason;
|
|
28872
|
+
if (reason) lines.push(` \u21B3 UNEVALUATED \u2014 ${reason}`);
|
|
28873
|
+
}
|
|
28329
28874
|
if (met === total) {
|
|
28330
28875
|
lines.push(" \u21B3 All exit criteria met \u2014 run `strategy_review` to propose advancing the stage.");
|
|
28331
28876
|
}
|
|
@@ -28420,7 +28965,7 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
|
|
|
28420
28965
|
const hasCarryForward = health.carryForward !== "None found" && !health.carryForward.startsWith("No carry-forward");
|
|
28421
28966
|
if (hasCarryForward) {
|
|
28422
28967
|
lines.push("## Carry-Forward");
|
|
28423
|
-
lines.push(
|
|
28968
|
+
lines.push(...renderCarryForward(health.carryForward, (t) => annotateTaskRefs(t, taskRefs)));
|
|
28424
28969
|
lines.push("");
|
|
28425
28970
|
}
|
|
28426
28971
|
const hasMetrics = health.metricsSection !== "Could not read methodology metrics." && !health.metricsSection.includes("undefined");
|
|
@@ -28441,7 +28986,7 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
|
|
|
28441
28986
|
}
|
|
28442
28987
|
return lines.join("\n").trimEnd();
|
|
28443
28988
|
}
|
|
28444
|
-
async function getHierarchyPosition(adapter2) {
|
|
28989
|
+
async function getHierarchyPosition(adapter2, projectId) {
|
|
28445
28990
|
try {
|
|
28446
28991
|
const [horizons, stages, phases, allTasks] = await Promise.all([
|
|
28447
28992
|
adapter2.readHorizons?.() ?? [],
|
|
@@ -28474,7 +29019,14 @@ async function getHierarchyPosition(adapter2) {
|
|
|
28474
29019
|
stage: activeStage.label,
|
|
28475
29020
|
activePhases: activePhases.map((p) => p.label),
|
|
28476
29021
|
phasesNearingClosure: nearingClosure.map((p) => p.label),
|
|
28477
|
-
|
|
29022
|
+
// task-3008: EVALUATE rather than read the hand-ticked boolean. Best-effort —
|
|
29023
|
+
// a failing evaluator degrades that criterion to unevaluated and leaves the
|
|
29024
|
+
// rest intact, because orient is the first call of every session.
|
|
29025
|
+
stageExitCriteria: await evaluateExitCriteria(
|
|
29026
|
+
adapter2,
|
|
29027
|
+
projectId ?? "",
|
|
29028
|
+
activeStage.exitCriteria ?? []
|
|
29029
|
+
).catch(() => (activeStage.exitCriteria ?? []).map((c) => ({ ...c, autoEvaluated: false })))
|
|
28478
29030
|
};
|
|
28479
29031
|
} catch {
|
|
28480
29032
|
return void 0;
|
|
@@ -28643,7 +29195,7 @@ async function handleOrient(adapter2, config2, args = {}, clientName) {
|
|
|
28643
29195
|
const [buildResult, healthResult, hierarchy] = await Promise.all([
|
|
28644
29196
|
tracked("listBuilds", () => listBuilds(adapter2, config2))(),
|
|
28645
29197
|
tracked("getHealthSummary", () => getHealthSummary(adapter2))(),
|
|
28646
|
-
tracked("getHierarchyPosition", () => getHierarchyPosition(adapter2))()
|
|
29198
|
+
tracked("getHierarchyPosition", () => getHierarchyPosition(adapter2, config2.projectId))()
|
|
28647
29199
|
]);
|
|
28648
29200
|
const currentCycle = buildResult.currentCycle;
|
|
28649
29201
|
const cycleIsComplete = healthResult.latestCycleStatus === "complete";
|
|
@@ -28898,10 +29450,6 @@ async function handleOrient(adapter2, config2, args = {}, clientName) {
|
|
|
28898
29450
|
// Discovered issues — split into Alerts (P0/P1) and Unactioned (P2/P3).
|
|
28899
29451
|
// Older records without a severity field are treated as P3.
|
|
28900
29452
|
tracked("discovered-issues", async () => {
|
|
28901
|
-
try {
|
|
28902
|
-
await adapter2.resolveLearningsForDoneTasks?.();
|
|
28903
|
-
} catch {
|
|
28904
|
-
}
|
|
28905
29453
|
const learnings = await adapter2.getCycleLearnings?.({ category: "issue", limit: 30 });
|
|
28906
29454
|
if (!learnings) return { alertsNote: "", unactionedIssuesNote: "" };
|
|
28907
29455
|
const candidateLearnings = learnings.filter((l) => !l.actionTaken);
|
|
@@ -29210,7 +29758,7 @@ function enrichClaudeMd(projectRoot, cycleNumber, adapterType, collector, client
|
|
|
29210
29758
|
additions.push(dedupeEnrichmentBlob(content, CLAUDE_MD_TIER_2));
|
|
29211
29759
|
}
|
|
29212
29760
|
if (additions.length === 0) return "";
|
|
29213
|
-
|
|
29761
|
+
writeFileSync6(claudeMdPath, content + additions.join(""), "utf-8");
|
|
29214
29762
|
const tierNames = [];
|
|
29215
29763
|
if (additions.some((a) => a.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1))) tierNames.push("Established (batch building, strategy reviews, AD lifecycle)");
|
|
29216
29764
|
if (additions.some((a) => a.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T2))) tierNames.push("Mature (idea pipeline, doc registry, advanced patterns)");
|
|
@@ -30025,8 +30573,8 @@ ${result.userMessage}
|
|
|
30025
30573
|
import { readFileSync as readFileSync12, statSync as statSync7 } from "fs";
|
|
30026
30574
|
|
|
30027
30575
|
// src/services/scope-brief.ts
|
|
30028
|
-
import { writeFileSync as
|
|
30029
|
-
import { join as join18, dirname as
|
|
30576
|
+
import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync5 } from "fs";
|
|
30577
|
+
import { join as join18, dirname as dirname5 } from "path";
|
|
30030
30578
|
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.
|
|
30031
30579
|
|
|
30032
30580
|
A scope document must:
|
|
@@ -30089,8 +30637,8 @@ async function applyScopeBrief(adapter2, input) {
|
|
|
30089
30637
|
if (input.adapterType === "proxy") {
|
|
30090
30638
|
collector.add({ path: relPath, content: docBody, mode: "overwrite" });
|
|
30091
30639
|
} else {
|
|
30092
|
-
|
|
30093
|
-
|
|
30640
|
+
mkdirSync5(dirname5(absPath), { recursive: true });
|
|
30641
|
+
writeFileSync7(absPath, docBody, "utf-8");
|
|
30094
30642
|
}
|
|
30095
30643
|
const taskCount = countSubTasks(docContent);
|
|
30096
30644
|
const summary = buildSummary(task, taskCount);
|
|
@@ -30375,6 +30923,77 @@ ${d.body}`;
|
|
|
30375
30923
|
${formatted}`, meta));
|
|
30376
30924
|
}
|
|
30377
30925
|
|
|
30926
|
+
// src/lib/dist-staleness.ts
|
|
30927
|
+
import { readFileSync as readFileSync13, statSync as statSync8 } from "fs";
|
|
30928
|
+
import { createHash as createHash5 } from "crypto";
|
|
30929
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
30930
|
+
var BOOT_MS = Date.now();
|
|
30931
|
+
var DEFAULT_SKEW_MS = 2e3;
|
|
30932
|
+
var STAT_TTL_MS = 1e4;
|
|
30933
|
+
var SELF_PATH = (() => {
|
|
30934
|
+
try {
|
|
30935
|
+
return fileURLToPath3(import.meta.url);
|
|
30936
|
+
} catch {
|
|
30937
|
+
return null;
|
|
30938
|
+
}
|
|
30939
|
+
})();
|
|
30940
|
+
function hashFile(path7) {
|
|
30941
|
+
try {
|
|
30942
|
+
return createHash5("sha256").update(readFileSync13(path7)).digest("hex");
|
|
30943
|
+
} catch {
|
|
30944
|
+
return null;
|
|
30945
|
+
}
|
|
30946
|
+
}
|
|
30947
|
+
var BOOT_HASH = SELF_PATH ? hashFile(SELF_PATH) : null;
|
|
30948
|
+
function bundleRewritten({ bootMs, distMtimeMs, skewMs = DEFAULT_SKEW_MS }) {
|
|
30949
|
+
if (distMtimeMs === null || !Number.isFinite(distMtimeMs)) return false;
|
|
30950
|
+
return distMtimeMs > bootMs + skewMs;
|
|
30951
|
+
}
|
|
30952
|
+
function isDistStale({ bootHash, distHash }) {
|
|
30953
|
+
if (!bootHash || !distHash) return false;
|
|
30954
|
+
return bootHash !== distHash;
|
|
30955
|
+
}
|
|
30956
|
+
function shouldAnnounce(currentHash2, alreadyAnnouncedHash) {
|
|
30957
|
+
if (!currentHash2) return false;
|
|
30958
|
+
return currentHash2 !== alreadyAnnouncedHash;
|
|
30959
|
+
}
|
|
30960
|
+
var cached = null;
|
|
30961
|
+
var currentHash = BOOT_HASH;
|
|
30962
|
+
var announcedHash = null;
|
|
30963
|
+
function computeStale() {
|
|
30964
|
+
if (!SELF_PATH || !BOOT_HASH) return false;
|
|
30965
|
+
let distMtimeMs = null;
|
|
30966
|
+
try {
|
|
30967
|
+
distMtimeMs = statSync8(SELF_PATH).mtimeMs;
|
|
30968
|
+
} catch {
|
|
30969
|
+
return false;
|
|
30970
|
+
}
|
|
30971
|
+
if (!bundleRewritten({ bootMs: BOOT_MS, distMtimeMs })) {
|
|
30972
|
+
currentHash = BOOT_HASH;
|
|
30973
|
+
return false;
|
|
30974
|
+
}
|
|
30975
|
+
currentHash = hashFile(SELF_PATH);
|
|
30976
|
+
return isDistStale({ bootHash: BOOT_HASH, distHash: currentHash });
|
|
30977
|
+
}
|
|
30978
|
+
function checkDistStaleness(now = Date.now()) {
|
|
30979
|
+
if (cached && now - cached.at < STAT_TTL_MS) return cached.stale;
|
|
30980
|
+
const stale = computeStale();
|
|
30981
|
+
cached = { at: now, stale };
|
|
30982
|
+
return stale;
|
|
30983
|
+
}
|
|
30984
|
+
function stalenessWarning(now = Date.now()) {
|
|
30985
|
+
const age = Math.round((now - BOOT_MS) / 1e3);
|
|
30986
|
+
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.)
|
|
30987
|
+
|
|
30988
|
+
`;
|
|
30989
|
+
}
|
|
30990
|
+
function consumeStalenessWarning(now = Date.now()) {
|
|
30991
|
+
if (!checkDistStaleness(now)) return null;
|
|
30992
|
+
if (!shouldAnnounce(currentHash, announcedHash)) return null;
|
|
30993
|
+
announcedHash = currentHash;
|
|
30994
|
+
return stalenessWarning(now);
|
|
30995
|
+
}
|
|
30996
|
+
|
|
30378
30997
|
// src/tools/learning-action.ts
|
|
30379
30998
|
var learningActionTool = {
|
|
30380
30999
|
name: "learning_action",
|
|
@@ -30502,54 +31121,6 @@ async function handleDiscoveredIssueResolve(adapter2, args) {
|
|
|
30502
31121
|
|
|
30503
31122
|
// src/tools/project.ts
|
|
30504
31123
|
import path6 from "path";
|
|
30505
|
-
|
|
30506
|
-
// src/services/entitlements.ts
|
|
30507
|
-
import { evaluateContributorGate } from "@papi-ai/shared";
|
|
30508
|
-
var FREE_PROJECT_CAP = 3;
|
|
30509
|
-
var PAID_TIERS = /* @__PURE__ */ new Set(["pro", "team"]);
|
|
30510
|
-
var PRICING_URL = "https://getpapi.ai/pricing";
|
|
30511
|
-
async function resolveTier(adapter2) {
|
|
30512
|
-
if (typeof adapter2.getMeteredUsage !== "function") return null;
|
|
30513
|
-
try {
|
|
30514
|
-
const usage = await adapter2.getMeteredUsage();
|
|
30515
|
-
return usage?.tier ?? null;
|
|
30516
|
-
} catch {
|
|
30517
|
-
return null;
|
|
30518
|
-
}
|
|
30519
|
-
}
|
|
30520
|
-
function isPaidTier(tier) {
|
|
30521
|
-
return tier !== null && PAID_TIERS.has(tier);
|
|
30522
|
-
}
|
|
30523
|
-
async function enforceProjectCap(adapter2, target) {
|
|
30524
|
-
const tier = await resolveTier(adapter2);
|
|
30525
|
-
if (tier === null || isPaidTier(tier)) return null;
|
|
30526
|
-
if (typeof adapter2.listUserProjects !== "function") return null;
|
|
30527
|
-
let projects;
|
|
30528
|
-
try {
|
|
30529
|
-
projects = await adapter2.listUserProjects();
|
|
30530
|
-
} catch {
|
|
30531
|
-
return null;
|
|
30532
|
-
}
|
|
30533
|
-
const matchesExisting = projects.some(
|
|
30534
|
-
(p) => target.papiDir && p.papi_dir && p.papi_dir === target.papiDir || target.name && p.name && p.name.trim().toLowerCase() === target.name.trim().toLowerCase()
|
|
30535
|
-
);
|
|
30536
|
-
if (matchesExisting) return null;
|
|
30537
|
-
if (projects.length >= FREE_PROJECT_CAP) return projectCapMessage(projects.length);
|
|
30538
|
-
return null;
|
|
30539
|
-
}
|
|
30540
|
-
async function resolveContributorUpsell(adapter2) {
|
|
30541
|
-
const tier = await resolveTier(adapter2);
|
|
30542
|
-
return evaluateContributorGate(tier).upsell ?? null;
|
|
30543
|
-
}
|
|
30544
|
-
function projectCapMessage(currentCount) {
|
|
30545
|
-
return `**You're on the Free plan (${currentCount} of ${FREE_PROJECT_CAP} projects).**
|
|
30546
|
-
|
|
30547
|
-
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}
|
|
30548
|
-
|
|
30549
|
-
Your existing projects are untouched, and you can keep working in any of them.`;
|
|
30550
|
-
}
|
|
30551
|
-
|
|
30552
|
-
// src/tools/project.ts
|
|
30553
31124
|
function workspacePapiDir(config2) {
|
|
30554
31125
|
if (!hasLocalWorkspace()) return void 0;
|
|
30555
31126
|
return config2.papiDir;
|
|
@@ -31019,7 +31590,7 @@ Its build reports, comments, and history moved with it; the cycle assignment was
|
|
|
31019
31590
|
// src/services/harness-inventory.ts
|
|
31020
31591
|
import { readdir as readdir3, readFile as readFile8, stat as stat3 } from "fs/promises";
|
|
31021
31592
|
import { join as join19 } from "path";
|
|
31022
|
-
import { createHash as
|
|
31593
|
+
import { createHash as createHash6 } from "crypto";
|
|
31023
31594
|
var RECOMMENDED_HOOKS = ["stop-release-check.sh", "claude-md-size-guard.sh"];
|
|
31024
31595
|
async function computeFingerprint(root) {
|
|
31025
31596
|
const parts = [];
|
|
@@ -31045,7 +31616,7 @@ async function computeFingerprint(root) {
|
|
|
31045
31616
|
} catch {
|
|
31046
31617
|
parts.push("manifest:none");
|
|
31047
31618
|
}
|
|
31048
|
-
return
|
|
31619
|
+
return createHash6("sha256").update(parts.join("|")).digest("hex").slice(0, 16);
|
|
31049
31620
|
}
|
|
31050
31621
|
async function readSkillDescription(skillDir) {
|
|
31051
31622
|
try {
|
|
@@ -31346,9 +31917,9 @@ async function checkMeter(adapter2, toolName, cacheKey) {
|
|
|
31346
31917
|
if (!adapter2.getMeteredUsage) return { blocked: false };
|
|
31347
31918
|
try {
|
|
31348
31919
|
let usage;
|
|
31349
|
-
const
|
|
31350
|
-
if (
|
|
31351
|
-
usage =
|
|
31920
|
+
const cached2 = usageCache.get(cacheKey);
|
|
31921
|
+
if (cached2 && Date.now() - cached2.at < CACHE_TTL_MS3) {
|
|
31922
|
+
usage = cached2.usage;
|
|
31352
31923
|
} else {
|
|
31353
31924
|
usage = await adapter2.getMeteredUsage();
|
|
31354
31925
|
usageCache.set(cacheKey, { usage, at: Date.now() });
|
|
@@ -31515,6 +32086,7 @@ var PAPI_TOOLS = [
|
|
|
31515
32086
|
docActionPromoteTool,
|
|
31516
32087
|
docDeleteTool,
|
|
31517
32088
|
docReorderTool,
|
|
32089
|
+
docReadTool,
|
|
31518
32090
|
getSiblingAdsTool,
|
|
31519
32091
|
handoffGenerateTool,
|
|
31520
32092
|
scopeBriefTool,
|
|
@@ -31537,11 +32109,11 @@ function getToolMetadata() {
|
|
|
31537
32109
|
return PAPI_TOOLS.map((t) => ({ name: t.name, description: t.description }));
|
|
31538
32110
|
}
|
|
31539
32111
|
function createServer(adapter2, config2) {
|
|
31540
|
-
const __pkgFilename =
|
|
31541
|
-
const __pkgDir =
|
|
32112
|
+
const __pkgFilename = fileURLToPath4(import.meta.url);
|
|
32113
|
+
const __pkgDir = dirname6(__pkgFilename);
|
|
31542
32114
|
let serverVersion = "unknown";
|
|
31543
32115
|
try {
|
|
31544
|
-
const pkg = JSON.parse(
|
|
32116
|
+
const pkg = JSON.parse(readFileSync14(join20(__pkgDir, "..", "package.json"), "utf-8"));
|
|
31545
32117
|
serverVersion = pkg.version ?? "unknown";
|
|
31546
32118
|
} catch {
|
|
31547
32119
|
}
|
|
@@ -31557,8 +32129,8 @@ function createServer(adapter2, config2) {
|
|
|
31557
32129
|
"\n\u26A0 PAPI is running in md mode \u2014 your cycles are not visible on the hosted dashboard.\n Configure DATABASE_URL or sign up at https://getpapi.ai/setup to enable observability.\n\n"
|
|
31558
32130
|
);
|
|
31559
32131
|
}
|
|
31560
|
-
const __filename =
|
|
31561
|
-
const __dirname2 =
|
|
32132
|
+
const __filename = fileURLToPath4(import.meta.url);
|
|
32133
|
+
const __dirname2 = dirname6(__filename);
|
|
31562
32134
|
const skillsDir = join20(__dirname2, "..", "skills");
|
|
31563
32135
|
function parseSkillFrontmatter(content) {
|
|
31564
32136
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
@@ -31719,6 +32291,8 @@ function createServer(adapter2, config2) {
|
|
|
31719
32291
|
return handleDocDelete(adapter2, config2, safeArgs);
|
|
31720
32292
|
case "doc_reorder":
|
|
31721
32293
|
return handleDocReorder(adapter2, safeArgs);
|
|
32294
|
+
case "doc_read":
|
|
32295
|
+
return handleDocRead(adapter2, config2, safeArgs);
|
|
31722
32296
|
case "get_sibling_ads":
|
|
31723
32297
|
return handleGetSiblingAds(adapter2, safeArgs);
|
|
31724
32298
|
case "handoff_generate":
|
|
@@ -31859,6 +32433,16 @@ ${usageLine(decision.usage)}`;
|
|
|
31859
32433
|
}
|
|
31860
32434
|
const footer = formatMetricsFooter(elapsed, usage, contextBytes);
|
|
31861
32435
|
result.content.push({ type: "text", text: footer });
|
|
32436
|
+
try {
|
|
32437
|
+
const warning = consumeStalenessWarning();
|
|
32438
|
+
if (warning && result.content.length > 0) {
|
|
32439
|
+
const first = result.content[0];
|
|
32440
|
+
if (first && typeof first.text === "string") {
|
|
32441
|
+
first.text = warning + first.text;
|
|
32442
|
+
}
|
|
32443
|
+
}
|
|
32444
|
+
} catch {
|
|
32445
|
+
}
|
|
31862
32446
|
return result;
|
|
31863
32447
|
});
|
|
31864
32448
|
return server2;
|
|
@@ -32304,10 +32888,10 @@ async function dispatchRequest(args) {
|
|
|
32304
32888
|
}
|
|
32305
32889
|
|
|
32306
32890
|
// src/index.ts
|
|
32307
|
-
var __dirname =
|
|
32891
|
+
var __dirname = dirname7(fileURLToPath5(import.meta.url));
|
|
32308
32892
|
var pkgVersion = "unknown";
|
|
32309
32893
|
try {
|
|
32310
|
-
const pkg = JSON.parse(
|
|
32894
|
+
const pkg = JSON.parse(readFileSync19(join25(__dirname, "..", "package.json"), "utf-8"));
|
|
32311
32895
|
pkgVersion = pkg.version;
|
|
32312
32896
|
} catch {
|
|
32313
32897
|
}
|