@papi-ai/server 0.7.77 → 0.7.79
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 +84 -12
- package/dist/index.js +1072 -434
- package/dist/prompts.js +47 -4
- package/package.json +1 -1
- package/skills/papi-cycle/papi-strategy/SKILL.md +1 -0
package/dist/index.js
CHANGED
|
@@ -561,11 +561,13 @@ function ensureTagAtHead(cwd, tag, message) {
|
|
|
561
561
|
message: `tag "${tag}" already exists but points at ${target ? target.slice(0, 7) : "an unknown commit"}, not the current HEAD (${head ? head.slice(0, 7) : "unknown"}). If it is left over from an aborted release, delete it and re-run release: \`git tag -d ${tag}\` (and \`git push origin :refs/tags/${tag}\` if it was pushed). Otherwise use a different version.`
|
|
562
562
|
};
|
|
563
563
|
}
|
|
564
|
-
function getLatestTag(cwd) {
|
|
564
|
+
function getLatestTag(cwd, timeoutMs) {
|
|
565
565
|
try {
|
|
566
566
|
return execFileSync("git", ["describe", "--tags", "--abbrev=0"], {
|
|
567
567
|
cwd,
|
|
568
|
-
encoding: "utf-8"
|
|
568
|
+
encoding: "utf-8",
|
|
569
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
570
|
+
...timeoutMs != null ? { timeout: timeoutMs } : {}
|
|
569
571
|
}).trim() || null;
|
|
570
572
|
} catch {
|
|
571
573
|
return null;
|
|
@@ -807,20 +809,26 @@ function findTaskCommitsOnBase(cwd, preferredBase, displayIds) {
|
|
|
807
809
|
try {
|
|
808
810
|
raw = execFileSync(
|
|
809
811
|
"git",
|
|
810
|
-
["log", base, "--format=%h%x01%s", "-n", "1000"],
|
|
812
|
+
["log", base, "--format=%h%x01%s%x01%b%x02", "-n", "1000"],
|
|
811
813
|
{ cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
|
|
812
814
|
);
|
|
813
815
|
} catch {
|
|
814
816
|
return out;
|
|
815
817
|
}
|
|
816
|
-
const commits = raw.split("
|
|
817
|
-
const
|
|
818
|
-
if (
|
|
819
|
-
return {
|
|
820
|
-
|
|
818
|
+
const commits = raw.split("").map((record) => {
|
|
819
|
+
const parts = record.split("");
|
|
820
|
+
if (parts.length < 2) return null;
|
|
821
|
+
return {
|
|
822
|
+
hash: parts[0].trim(),
|
|
823
|
+
subject: parts[1].trim(),
|
|
824
|
+
body: (parts[2] ?? "").trim()
|
|
825
|
+
};
|
|
826
|
+
}).filter(
|
|
827
|
+
(c) => c !== null && c.hash !== ""
|
|
828
|
+
);
|
|
821
829
|
for (const displayId of displayIds) {
|
|
822
830
|
const re = new RegExp(`(^|[^\\w-])${escapeRegexLiteral(displayId)}([^\\w-]|$)`);
|
|
823
|
-
const hit = commits.find((c) => re.test(c.subject));
|
|
831
|
+
const hit = commits.find((c) => re.test(c.subject)) ?? commits.find((c) => re.test(c.body));
|
|
824
832
|
if (!hit) continue;
|
|
825
833
|
const prMatch = hit.subject.match(/#(\d+)/);
|
|
826
834
|
out.set(displayId, {
|
|
@@ -1406,10 +1414,39 @@ var init_proxy_adapter = __esm({
|
|
|
1406
1414
|
endpoint;
|
|
1407
1415
|
apiKey;
|
|
1408
1416
|
projectId;
|
|
1417
|
+
onAuthRejected;
|
|
1409
1418
|
constructor(config2) {
|
|
1410
1419
|
this.endpoint = config2.endpoint.replace(/\/$/, "");
|
|
1411
1420
|
this.apiKey = config2.apiKey;
|
|
1412
1421
|
this.projectId = config2.projectId ?? "";
|
|
1422
|
+
this.onAuthRejected = config2.onAuthRejected;
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* task-1773: bearer-only auth probe. Hits the USER-scoped `project-list` route
|
|
1426
|
+
* (no projectId needed), so it answers exactly one question: does the proxy
|
|
1427
|
+
* still accept this bearer?
|
|
1428
|
+
*
|
|
1429
|
+
* Returns the HTTP status, or 0 when the call could not be made at all
|
|
1430
|
+
* (network error / timeout). Callers MUST treat 0 — and any status that is
|
|
1431
|
+
* neither 2xx nor 401 — as "no signal", never as a rejection: a proxy outage
|
|
1432
|
+
* must not masquerade as a revoked token and force every user to re-auth.
|
|
1433
|
+
*/
|
|
1434
|
+
async probeBearerStatus() {
|
|
1435
|
+
try {
|
|
1436
|
+
const response = await fetch(`${this.endpoint}/project-list`, {
|
|
1437
|
+
method: "POST",
|
|
1438
|
+
headers: {
|
|
1439
|
+
"Content-Type": "application/json",
|
|
1440
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
1441
|
+
},
|
|
1442
|
+
body: "{}",
|
|
1443
|
+
signal: AbortSignal.timeout(5e3)
|
|
1444
|
+
});
|
|
1445
|
+
if (response.status === 401) this.onAuthRejected?.();
|
|
1446
|
+
return response.status;
|
|
1447
|
+
} catch {
|
|
1448
|
+
return 0;
|
|
1449
|
+
}
|
|
1413
1450
|
}
|
|
1414
1451
|
/** Resolved project ID — available after ensureProject() completes. */
|
|
1415
1452
|
getProjectId() {
|
|
@@ -1426,7 +1463,8 @@ var init_proxy_adapter = __esm({
|
|
|
1426
1463
|
return wrapWithForwarding(new _ProxyPapiAdapter({
|
|
1427
1464
|
endpoint: this.endpoint,
|
|
1428
1465
|
apiKey: this.apiKey,
|
|
1429
|
-
projectId
|
|
1466
|
+
projectId,
|
|
1467
|
+
onAuthRejected: this.onAuthRejected
|
|
1430
1468
|
}));
|
|
1431
1469
|
}
|
|
1432
1470
|
/**
|
|
@@ -1513,6 +1551,7 @@ var init_proxy_adapter = __esm({
|
|
|
1513
1551
|
message = errorBody;
|
|
1514
1552
|
}
|
|
1515
1553
|
if (response.status === 401) {
|
|
1554
|
+
this.onAuthRejected?.();
|
|
1516
1555
|
throw new Error(
|
|
1517
1556
|
`Auth: Invalid API key \u2014 PAPI_DATA_API_KEY was rejected by the proxy.
|
|
1518
1557
|
This usually means the key was revoked or replaced. Mint a fresh key in the Connect panel on your PAPI dashboard (https://getpapi.ai/hub), then update PAPI_DATA_API_KEY in your .mcp.json.
|
|
@@ -2091,6 +2130,7 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
2091
2130
|
} catch {
|
|
2092
2131
|
message = errorBody;
|
|
2093
2132
|
}
|
|
2133
|
+
if (response.status === 401) this.onAuthRejected?.();
|
|
2094
2134
|
throw new Error(`Proxy error (${response.status}) on ${route}: ${message}`);
|
|
2095
2135
|
}
|
|
2096
2136
|
const body = await response.json();
|
|
@@ -4412,7 +4452,7 @@ __export(doctor_exports, {
|
|
|
4412
4452
|
});
|
|
4413
4453
|
import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
|
|
4414
4454
|
import { homedir as homedir4 } from "os";
|
|
4415
|
-
import { join as
|
|
4455
|
+
import { join as join22 } from "path";
|
|
4416
4456
|
function redact(name, value) {
|
|
4417
4457
|
if (!value) return "(empty)";
|
|
4418
4458
|
if (SECRET_VARS.has(name)) {
|
|
@@ -4423,9 +4463,9 @@ function redact(name, value) {
|
|
|
4423
4463
|
}
|
|
4424
4464
|
function findMcpJson() {
|
|
4425
4465
|
const candidates = [
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4466
|
+
join22(process.cwd(), ".mcp.json"),
|
|
4467
|
+
join22(homedir4(), ".claude", ".mcp.json"),
|
|
4468
|
+
join22(homedir4(), ".mcp.json")
|
|
4429
4469
|
];
|
|
4430
4470
|
for (const path7 of candidates) {
|
|
4431
4471
|
if (!existsSync12(path7)) continue;
|
|
@@ -4708,9 +4748,9 @@ __export(reset_exports, {
|
|
|
4708
4748
|
removePapiEntry: () => removePapiEntry,
|
|
4709
4749
|
runReset: () => runReset
|
|
4710
4750
|
});
|
|
4711
|
-
import { existsSync as existsSync13, readFileSync as readFileSync16, writeFileSync as
|
|
4751
|
+
import { existsSync as existsSync13, readFileSync as readFileSync16, writeFileSync as writeFileSync9 } from "fs";
|
|
4712
4752
|
import { homedir as homedir5 } from "os";
|
|
4713
|
-
import { join as
|
|
4753
|
+
import { join as join23 } from "path";
|
|
4714
4754
|
import { createInterface } from "readline/promises";
|
|
4715
4755
|
function findResetTarget() {
|
|
4716
4756
|
for (const path7 of CANDIDATE_PATHS()) {
|
|
@@ -4789,7 +4829,7 @@ async function runReset(args = []) {
|
|
|
4789
4829
|
}
|
|
4790
4830
|
}
|
|
4791
4831
|
try {
|
|
4792
|
-
|
|
4832
|
+
writeFileSync9(target.path, removePapiEntry(target), "utf-8");
|
|
4793
4833
|
process.stdout.write(`
|
|
4794
4834
|
\u2713 Removed papi entry from ${target.path}
|
|
4795
4835
|
`);
|
|
@@ -4806,9 +4846,9 @@ var init_reset = __esm({
|
|
|
4806
4846
|
"src/cli/reset.ts"() {
|
|
4807
4847
|
"use strict";
|
|
4808
4848
|
CANDIDATE_PATHS = () => [
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4849
|
+
join23(process.cwd(), ".mcp.json"),
|
|
4850
|
+
join23(homedir5(), ".claude", ".mcp.json"),
|
|
4851
|
+
join23(homedir5(), ".mcp.json")
|
|
4812
4852
|
];
|
|
4813
4853
|
}
|
|
4814
4854
|
});
|
|
@@ -4821,7 +4861,7 @@ __export(audit_exports, {
|
|
|
4821
4861
|
});
|
|
4822
4862
|
import { existsSync as existsSync14, readFileSync as readFileSync17, readdirSync as readdirSync7 } from "fs";
|
|
4823
4863
|
import { homedir as homedir6 } from "os";
|
|
4824
|
-
import { join as
|
|
4864
|
+
import { join as join24 } from "path";
|
|
4825
4865
|
function safeListDirs(dir) {
|
|
4826
4866
|
try {
|
|
4827
4867
|
return readdirSync7(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort((a, b2) => a.localeCompare(b2));
|
|
@@ -4837,7 +4877,7 @@ function safeListFiles(dir, ext) {
|
|
|
4837
4877
|
}
|
|
4838
4878
|
}
|
|
4839
4879
|
function readMcp(projectPath) {
|
|
4840
|
-
const path7 =
|
|
4880
|
+
const path7 = join24(projectPath, ".mcp.json");
|
|
4841
4881
|
if (!existsSync14(path7)) return { servers: [] };
|
|
4842
4882
|
try {
|
|
4843
4883
|
const parsed = JSON.parse(readFileSync17(path7, "utf-8"));
|
|
@@ -4872,18 +4912,18 @@ function auditProjectSync(projectPath, name) {
|
|
|
4872
4912
|
path: projectPath,
|
|
4873
4913
|
papiProjectId,
|
|
4874
4914
|
mcpServers: servers,
|
|
4875
|
-
skills: safeListDirs(
|
|
4876
|
-
agentSkills: safeListDirs(
|
|
4877
|
-
agents: safeListFiles(
|
|
4878
|
-
hooks: safeListFiles(
|
|
4915
|
+
skills: safeListDirs(join24(projectPath, ".claude", "skills")),
|
|
4916
|
+
agentSkills: safeListDirs(join24(projectPath, ".agents", "skills")),
|
|
4917
|
+
agents: safeListFiles(join24(projectPath, ".claude", "agents"), ".md"),
|
|
4918
|
+
hooks: safeListFiles(join24(projectPath, ".claude", "hooks"), ".sh")
|
|
4879
4919
|
};
|
|
4880
4920
|
}
|
|
4881
4921
|
function discoverProjects() {
|
|
4882
4922
|
const out = [];
|
|
4883
4923
|
for (const root of PROJECT_ROOTS) {
|
|
4884
4924
|
for (const name of safeListDirs(root)) {
|
|
4885
|
-
const path7 =
|
|
4886
|
-
if (existsSync14(
|
|
4925
|
+
const path7 = join24(root, name);
|
|
4926
|
+
if (existsSync14(join24(path7, ".mcp.json")) || existsSync14(join24(path7, ".claude"))) {
|
|
4887
4927
|
out.push({ name, path: path7 });
|
|
4888
4928
|
}
|
|
4889
4929
|
}
|
|
@@ -5048,9 +5088,9 @@ var PROJECT_ROOTS, GLOBAL_SKILLS_DIR, GLOBAL_CLAUDE_JSON, IDLE_WINDOW_DAYS, GLOB
|
|
|
5048
5088
|
var init_audit = __esm({
|
|
5049
5089
|
"src/cli/audit.ts"() {
|
|
5050
5090
|
"use strict";
|
|
5051
|
-
PROJECT_ROOTS = [
|
|
5052
|
-
GLOBAL_SKILLS_DIR =
|
|
5053
|
-
GLOBAL_CLAUDE_JSON =
|
|
5091
|
+
PROJECT_ROOTS = [join24(homedir6(), "Ai-App-Projects"), join24(homedir6(), "android-projects")];
|
|
5092
|
+
GLOBAL_SKILLS_DIR = join24(homedir6(), ".claude", "skills");
|
|
5093
|
+
GLOBAL_CLAUDE_JSON = join24(homedir6(), ".claude.json");
|
|
5054
5094
|
IDLE_WINDOW_DAYS = 30;
|
|
5055
5095
|
GLOBALIZE_THRESHOLD = 3;
|
|
5056
5096
|
__testing2 = { readMcp, computeFlags, formatReport: formatReport2, discoverProjects, auditProjectSync };
|
|
@@ -5062,8 +5102,8 @@ var setup_exports = {};
|
|
|
5062
5102
|
__export(setup_exports, {
|
|
5063
5103
|
runSetup: () => runSetup
|
|
5064
5104
|
});
|
|
5065
|
-
import { existsSync as existsSync15, readFileSync as readFileSync18, writeFileSync as
|
|
5066
|
-
import { join as
|
|
5105
|
+
import { existsSync as existsSync15, readFileSync as readFileSync18, writeFileSync as writeFileSync10, chmodSync as chmodSync2, statSync as statSync9 } from "fs";
|
|
5106
|
+
import { join as join25 } from "path";
|
|
5067
5107
|
function baseUrl() {
|
|
5068
5108
|
const fromEnv = process.env["PAPI_HOST"] ?? process.env["PAPI_BASE_URL"];
|
|
5069
5109
|
if (fromEnv) return fromEnv.replace(/\/$/, "");
|
|
@@ -5095,7 +5135,7 @@ function sleep(ms) {
|
|
|
5095
5135
|
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
5096
5136
|
}
|
|
5097
5137
|
function writeMcpJson(opts) {
|
|
5098
|
-
const path7 =
|
|
5138
|
+
const path7 = join25(process.cwd(), ".mcp.json");
|
|
5099
5139
|
let parsed = {};
|
|
5100
5140
|
if (existsSync15(path7)) {
|
|
5101
5141
|
try {
|
|
@@ -5120,7 +5160,7 @@ function writeMcpJson(opts) {
|
|
|
5120
5160
|
}
|
|
5121
5161
|
mcpServers.papi = papiEntry;
|
|
5122
5162
|
parsed.mcpServers = mcpServers;
|
|
5123
|
-
|
|
5163
|
+
writeFileSync10(path7, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5124
5164
|
try {
|
|
5125
5165
|
const mode = statSync9(path7).mode & 511;
|
|
5126
5166
|
if (mode !== 384) chmodSync2(path7, 384);
|
|
@@ -5231,7 +5271,7 @@ var init_setup = __esm({
|
|
|
5231
5271
|
|
|
5232
5272
|
// src/index.ts
|
|
5233
5273
|
import { readFileSync as readFileSync19 } from "fs";
|
|
5234
|
-
import { dirname as dirname7, join as
|
|
5274
|
+
import { dirname as dirname7, join as join26, basename as basename2 } from "path";
|
|
5235
5275
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
5236
5276
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5237
5277
|
import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -5647,9 +5687,12 @@ function parsePlanningLog(content, activeDecisionsContent, cycleLogContent) {
|
|
|
5647
5687
|
var VALID_EFFORT_SIZES = /* @__PURE__ */ new Set(["XS", "S", "M", "L", "XL"]);
|
|
5648
5688
|
var SECTION_HEADERS = [
|
|
5649
5689
|
"SCOPE (DO THIS)",
|
|
5690
|
+
"WHY NOT SIMPLER",
|
|
5650
5691
|
"SCOPE BOUNDARY (DO NOT DO THIS)",
|
|
5651
5692
|
"ACCEPTANCE CRITERIA",
|
|
5693
|
+
"PRE-MORTEM",
|
|
5652
5694
|
"SECURITY CONSIDERATIONS",
|
|
5695
|
+
"DEPLOY VERIFICATION",
|
|
5653
5696
|
"PRE-BUILD VERIFICATION",
|
|
5654
5697
|
"FILES LIKELY TOUCHED",
|
|
5655
5698
|
"EFFORT"
|
|
@@ -5688,7 +5731,7 @@ function parseBulletsOnly(text) {
|
|
|
5688
5731
|
return text.split("\n").filter((l) => /^\s*-\s/.test(l)).map((l) => l.replace(/^\s*-\s*/, "").trim()).filter((l) => l.length > 0);
|
|
5689
5732
|
}
|
|
5690
5733
|
function parseChecklist(text) {
|
|
5691
|
-
return text.split("\n").map((l) => l.replace(/^\s
|
|
5734
|
+
return text.split("\n").map((l) => l.replace(/^\s*(?:[-*+]\s*)?(?:\[[ xX]\]\s*)?/, "").trim()).filter((l) => l.length > 0);
|
|
5692
5735
|
}
|
|
5693
5736
|
function parseBuildHandoff(markdown) {
|
|
5694
5737
|
if (typeof markdown !== "string" || !markdown.trim()) return null;
|
|
@@ -6338,13 +6381,21 @@ var EFFORT_SCALE = {
|
|
|
6338
6381
|
XL: 5
|
|
6339
6382
|
};
|
|
6340
6383
|
function effortOrdinal(effort) {
|
|
6384
|
+
if (typeof effort !== "string") return void 0;
|
|
6341
6385
|
const normalized = effort.trim().toUpperCase();
|
|
6342
6386
|
return EFFORT_SCALE[normalized];
|
|
6343
6387
|
}
|
|
6388
|
+
function isUnparsedEffort(effort) {
|
|
6389
|
+
if (typeof effort !== "string" || effort.trim().length === 0) return false;
|
|
6390
|
+
return effortOrdinal(effort) === void 0;
|
|
6391
|
+
}
|
|
6344
6392
|
function calculateCycleMetrics(reports, currentCycle, window = 5) {
|
|
6345
6393
|
const recentReports = reports.filter(
|
|
6346
6394
|
(r) => r.cycle > currentCycle - window && r.cycle <= currentCycle
|
|
6347
6395
|
);
|
|
6396
|
+
const unparsedEffortCount = recentReports.filter(
|
|
6397
|
+
(r) => isUnparsedEffort(r.actualEffort) || isUnparsedEffort(r.estimatedEffort)
|
|
6398
|
+
).length;
|
|
6348
6399
|
const perCycle = /* @__PURE__ */ new Map();
|
|
6349
6400
|
for (const r of recentReports) {
|
|
6350
6401
|
const group = perCycle.get(r.cycle) ?? [];
|
|
@@ -6381,7 +6432,7 @@ function calculateCycleMetrics(reports, currentCycle, window = 5) {
|
|
|
6381
6432
|
effortPoints: reps.reduce((s, r) => s + (effortOrdinal(r.actualEffort) ?? 0), 0)
|
|
6382
6433
|
});
|
|
6383
6434
|
}
|
|
6384
|
-
return { accuracy, velocity };
|
|
6435
|
+
return { accuracy, velocity, unparsedEffortCount };
|
|
6385
6436
|
}
|
|
6386
6437
|
function serializeAccuracyRow(a) {
|
|
6387
6438
|
return `| ${a.cycle} | ${a.reports} | ${a.matchRate}% | ${a.mae} | ${a.bias >= 0 ? "+" : ""}${a.bias} |`;
|
|
@@ -8031,6 +8082,7 @@ async function createAdapter(optionsOrType, maybePapiDir) {
|
|
|
8031
8082
|
case "pg": {
|
|
8032
8083
|
const { PgAdapter, PgPapiAdapter, configFromEnv } = await import("@papi-ai/adapter-pg");
|
|
8033
8084
|
let projectId = process.env["PAPI_PROJECT_ID"];
|
|
8085
|
+
const projectIdWasPreSupplied = Boolean(projectId);
|
|
8034
8086
|
const projectRoot = options.projectRoot ?? process.env["PAPI_PROJECT_DIR"] ?? process.cwd();
|
|
8035
8087
|
let rootHash = null;
|
|
8036
8088
|
let originUrl = null;
|
|
@@ -8094,6 +8146,26 @@ async function createAdapter(optionsOrType, maybePapiDir) {
|
|
|
8094
8146
|
}
|
|
8095
8147
|
const config2 = papiEndpoint ? { connectionString: papiEndpoint } : configFromEnv();
|
|
8096
8148
|
validateDatabaseUrl(config2.connectionString);
|
|
8149
|
+
if (projectIdWasPreSupplied) {
|
|
8150
|
+
const ownershipProbe = new PgAdapter(config2);
|
|
8151
|
+
try {
|
|
8152
|
+
const owned = await ownershipProbe.findProjectById(projectId, resolveUserId);
|
|
8153
|
+
if (!owned) {
|
|
8154
|
+
throw new Error(
|
|
8155
|
+
`PAPI_PROJECT_ID ${projectId} does not belong to you.
|
|
8156
|
+
|
|
8157
|
+
The project exists under a different owner, or does not exist at all. PAPI refuses to attach to a project you do not own \u2014 writing to it would put your cycles, tasks and Active Decisions into somebody else's project.
|
|
8158
|
+
|
|
8159
|
+
Fix the PAPI_PROJECT_ID in your .mcp.json (or unset it and let PAPI resolve the project from your git remote), then reconnect.`
|
|
8160
|
+
);
|
|
8161
|
+
}
|
|
8162
|
+
} finally {
|
|
8163
|
+
try {
|
|
8164
|
+
await ownershipProbe.close();
|
|
8165
|
+
} catch {
|
|
8166
|
+
}
|
|
8167
|
+
}
|
|
8168
|
+
}
|
|
8097
8169
|
const { ensureSchema } = await import("@papi-ai/adapter-pg");
|
|
8098
8170
|
try {
|
|
8099
8171
|
await ensureSchema(config2);
|
|
@@ -8328,7 +8400,7 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
8328
8400
|
// src/server.ts
|
|
8329
8401
|
import { readFileSync as readFileSync14 } from "fs";
|
|
8330
8402
|
import { access as access4, readdir as readdir4, readFile as readFile9 } from "fs/promises";
|
|
8331
|
-
import { join as
|
|
8403
|
+
import { join as join21, dirname as dirname6 } from "path";
|
|
8332
8404
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
8333
8405
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
8334
8406
|
import {
|
|
@@ -8461,15 +8533,23 @@ function formatBuildReports(reports, opts) {
|
|
|
8461
8533
|
_\u2026and ${reports.length - capped.length} older build report(s) omitted to bound context size._` : "";
|
|
8462
8534
|
return body + omitted;
|
|
8463
8535
|
}
|
|
8464
|
-
function
|
|
8465
|
-
const
|
|
8536
|
+
function extractTaskReferences(report) {
|
|
8537
|
+
const raw = [report.surprises, report.architectureNotes, report.deadEnds, report.discoveredIssues].filter((s) => typeof s === "string" && s.length > 0).join("\n");
|
|
8538
|
+
const prose = raw.replace(/```[\s\S]*?```|~~~[\s\S]*?~~~/g, " ").replace(/`[^`\n]*`/g, " ");
|
|
8466
8539
|
const own = report.taskId?.toLowerCase();
|
|
8467
|
-
const
|
|
8540
|
+
const resolves = /* @__PURE__ */ new Set();
|
|
8541
|
+
for (const m of prose.matchAll(/\bresolves:\s*((?:task-\d+\b\s*,?\s*)+)/gi)) {
|
|
8542
|
+
for (const idMatch of m[1].matchAll(/\btask-\d+\b/gi)) {
|
|
8543
|
+
const id = idMatch[0].toLowerCase();
|
|
8544
|
+
if (id !== own) resolves.add(id);
|
|
8545
|
+
}
|
|
8546
|
+
}
|
|
8547
|
+
const mentions = /* @__PURE__ */ new Set();
|
|
8468
8548
|
for (const m of prose.matchAll(/\btask-\d+\b/gi)) {
|
|
8469
8549
|
const id = m[0].toLowerCase();
|
|
8470
|
-
if (id !== own)
|
|
8550
|
+
if (id !== own && !resolves.has(id)) mentions.add(id);
|
|
8471
8551
|
}
|
|
8472
|
-
return [...
|
|
8552
|
+
return { resolves: [...resolves].sort(), mentions: [...mentions].sort() };
|
|
8473
8553
|
}
|
|
8474
8554
|
function formatRecentlyShippedCapabilities(reports) {
|
|
8475
8555
|
const completed = reports.filter((r) => r.completed === "Yes" || r.completed === "Partial");
|
|
@@ -8485,16 +8565,26 @@ function formatRecentlyShippedCapabilities(reports) {
|
|
|
8485
8565
|
}
|
|
8486
8566
|
return parts.join("\n");
|
|
8487
8567
|
});
|
|
8568
|
+
const resolvedBy = /* @__PURE__ */ new Map();
|
|
8488
8569
|
const namedBy = /* @__PURE__ */ new Map();
|
|
8489
8570
|
const completedIds = new Set(completed.map((r) => r.taskId?.toLowerCase()).filter(Boolean));
|
|
8571
|
+
const record = (into, ref, namer) => {
|
|
8572
|
+
const namers = into.get(ref) ?? [];
|
|
8573
|
+
namers.push(namer);
|
|
8574
|
+
into.set(ref, namers);
|
|
8575
|
+
};
|
|
8490
8576
|
for (const r of completed) {
|
|
8491
|
-
|
|
8577
|
+
const { resolves, mentions } = extractTaskReferences(r);
|
|
8578
|
+
for (const ref of resolves) {
|
|
8492
8579
|
if (completedIds.has(ref)) continue;
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
8580
|
+
record(resolvedBy, ref, r.taskId);
|
|
8581
|
+
}
|
|
8582
|
+
for (const ref of mentions) {
|
|
8583
|
+
if (completedIds.has(ref)) continue;
|
|
8584
|
+
record(namedBy, ref, r.taskId);
|
|
8496
8585
|
}
|
|
8497
8586
|
}
|
|
8587
|
+
for (const ref of resolvedBy.keys()) namedBy.delete(ref);
|
|
8498
8588
|
const out = [
|
|
8499
8589
|
`${completed.length} task(s) completed in recent cycles:`,
|
|
8500
8590
|
"",
|
|
@@ -8502,17 +8592,31 @@ function formatRecentlyShippedCapabilities(reports) {
|
|
|
8502
8592
|
"",
|
|
8503
8593
|
"Cross-reference candidate tasks against this list. If >80% of a candidate task's scope appears here, recommend cancellation or scope reduction instead of scheduling."
|
|
8504
8594
|
];
|
|
8595
|
+
if (resolvedBy.size > 0) {
|
|
8596
|
+
out.push(
|
|
8597
|
+
"",
|
|
8598
|
+
"### \u26A0 Declared resolved by a shipped task \u2014 VERIFY, THEN CLOSE",
|
|
8599
|
+
"",
|
|
8600
|
+
"A shipped report explicitly claimed each of these with `resolves: <task-id>`, but",
|
|
8601
|
+
"the task is not itself marked complete. That is a declaration of intent, not proof:",
|
|
8602
|
+
"confirm against the live code, then close it with a boardCorrection rather than",
|
|
8603
|
+
"spending a cycle slot on work that already shipped.",
|
|
8604
|
+
"",
|
|
8605
|
+
...[...resolvedBy.entries()].sort(([a], [b2]) => a.localeCompare(b2)).map(([ref, namers]) => `- **${ref}** \u2014 declared resolved by ${namers.join(", ")}`)
|
|
8606
|
+
);
|
|
8607
|
+
}
|
|
8505
8608
|
if (namedBy.size > 0) {
|
|
8506
8609
|
out.push(
|
|
8507
8610
|
"",
|
|
8508
8611
|
"### \u26A0 Named by a shipped task \u2014 VERIFY BEFORE SCHEDULING",
|
|
8509
8612
|
"",
|
|
8510
|
-
"These task IDs
|
|
8511
|
-
"completed. A discovery is often fixed as a
|
|
8512
|
-
"never marked done, so it survives into this
|
|
8513
|
-
"true (C357 gave task-3043 a P1 slot this way \u2014
|
|
8514
|
-
"
|
|
8515
|
-
|
|
8613
|
+
"These task IDs merely APPEAR in the build reports above \u2014 no report claimed to have",
|
|
8614
|
+
"resolved them, and they are not themselves completed. A discovery is often fixed as a",
|
|
8615
|
+
"side effect of a sibling task's diff and never marked done, so it survives into this",
|
|
8616
|
+
"plan carrying notes that are no longer true (C357 gave task-3043 a P1 slot this way \u2014",
|
|
8617
|
+
"task-2998 had already fixed it). Weaker signal than the section above: a mention can",
|
|
8618
|
+
'equally mean "related to" or "still blocked by". Read the naming report and the live',
|
|
8619
|
+
"code BEFORE scheduling; never close on a mention alone.",
|
|
8516
8620
|
"",
|
|
8517
8621
|
...[...namedBy.entries()].sort(([a], [b2]) => a.localeCompare(b2)).map(([ref, namers]) => `- **${ref}** \u2014 named by ${namers.join(", ")}`)
|
|
8518
8622
|
);
|
|
@@ -9089,6 +9193,19 @@ If a candidate AD body could be invalidated by running a SQL query, refreshing a
|
|
|
9089
9193
|
**Negative example (reject):** "External user feedback is now flowing. Stonebridge Systems is actively building." \u2014 this is a fact about the current state of the world. Capture as dogfood/signal observation; do not mint.
|
|
9090
9194
|
|
|
9091
9195
|
This rule applies to: new ADs proposed during planning (Step 9), strategy review AD updates (section 5), and strategy_change AD updates. If you find an existing AD that violates this rule during housekeeping, propose deleting it (action: "delete") with a one-line rationale.`;
|
|
9196
|
+
var AD_CONFLICT_SURFACING_RULES = `**A contradiction is NOT a veto \u2014 surface it, never silently shelve it.**
|
|
9197
|
+
|
|
9198
|
+
Active Decisions are *active*: they can be superseded, modified, or abandoned. You do NOT have authority to kill a piece of work simply because it cuts against one. That is the user's call, and they can only make it if you show it to them.
|
|
9199
|
+
|
|
9200
|
+
**Never do any of these to a task solely because it contradicts, competes with, or duplicates the intent of an existing AD:** cancel it; drop it to P3; defer it silently; omit it from the cycle without comment; or reduce its scope down to the part that fits the AD.
|
|
9201
|
+
|
|
9202
|
+
**Instead, emit a \`decisionConflicts\` entry** for it (see the structured-output schema) with: the task, the AD it collides with, one line on what the AD says versus what the task implies, 2-3 concrete options, and your recommendation. Also render a \`## Decisions Needed\` section in Part 1 listing the same, immediately before the BUILD HANDOFFs.
|
|
9203
|
+
|
|
9204
|
+
Apply will park each conflicted task as **Blocked** with a \`decision-gate\` blocker pointing at that AD. It clears automatically the moment the AD is resolved, confirmed, validated, or superseded \u2014 so the work returns to the board on its own once the user has decided. The task is preserved, not killed.
|
|
9205
|
+
|
|
9206
|
+
**Emit a conflict ONLY when the AD is the reason the work would not otherwise proceed.** If the task is fine to schedule, schedule it. If it is genuinely obsolete, duplicated, or already shipped, cancel it on THOSE grounds and say so \u2014 that is not an AD conflict. Do not manufacture conflicts to look thorough; a plan with zero real conflicts should emit an empty array.
|
|
9207
|
+
|
|
9208
|
+
This applies equally to *competing* work: two tasks proposing rival approaches to the same problem is a decision for the user, not a coin-flip for you.`;
|
|
9092
9209
|
var OUTPUT_QUALITY_RUBRIC = `Quality bar \u2014 before emitting, self-score the artifact 1-10 on five dimensions: (1) **Clarity** \u2014 could a third LLM act on it with no extra context? (2) **Scope tightness** \u2014 one focused unit of work, not three bundled together. (3) **Specificity** \u2014 it names concrete files/paths/tasks/ADs, not vague references like "the auth module". (4) **Dependency surfacing** \u2014 prerequisite or upstream items are called out explicitly. (5) **Success-criteria concreteness** \u2014 "done" is testable and observable, not "looks good". Sum to /50. If any single dimension scores \u22644, or the total is <35, revise the artifact and re-score before emitting \u2014 do not ship a below-threshold artifact. This is a self-check gate, not an output field: do NOT add the scores to the artifact or the structured JSON.`;
|
|
9093
9210
|
var PLAN_SYSTEM = `You are the PAPI Cycle Planner \u2014 an autonomous planning engine for software projects.
|
|
9094
9211
|
You receive project context and produce a planning cycle output with a BUILD HANDOFF.
|
|
@@ -9169,7 +9286,8 @@ After your natural language output, include this EXACT format on its own line:
|
|
|
9169
9286
|
"newTasks": [],
|
|
9170
9287
|
"boardCorrections": [],
|
|
9171
9288
|
"productBrief": null,
|
|
9172
|
-
"activeDecisions": []
|
|
9289
|
+
"activeDecisions": [],
|
|
9290
|
+
"decisionConflicts": [{"taskId": "string \u2014 an existing task ID or a newTasks tempId", "adId": "string \u2014 AD-N this collides with", "conflict": "string \u2014 one line: what the AD says vs what this task implies", "options": ["string \u2014 concrete option, e.g. 'Supersede AD-12 and schedule the task'"], "recommendation": "string \u2014 which option you would take and why, in one sentence"}]
|
|
9173
9291
|
}
|
|
9174
9292
|
\`\`\`
|
|
9175
9293
|
|
|
@@ -9206,6 +9324,7 @@ Everything in Part 1 (natural language) is **display-only**. Part 2 (structured
|
|
|
9206
9324
|
- Updated or created Active Decisions in Part 1? \u2192 Put them in \`activeDecisions\` array (with id and full body including ### heading)
|
|
9207
9325
|
- Found board corrections (wrong priority, missing fields, stale status) in Part 1? \u2192 Put them in \`boardCorrections\` array
|
|
9208
9326
|
- Generated BUILD HANDOFFs in Part 1? \u2192 Put them in \`cycleHandoffs\` array
|
|
9327
|
+
- Wrote a \`## Decisions Needed\` section in Part 1? \u2192 Put every entry in \`decisionConflicts\`. Nothing in that array means the user is never asked, and the work is lost. Omit the array (or leave it empty) when there are genuinely no AD conflicts.
|
|
9209
9328
|
- **\`complexity\` uses the LONG forms only** \u2014 "XS", "Small", "Medium", "Large", "XL". Do NOT reuse the handoff EFFORT short-forms (S/M/L) for task complexity.
|
|
9210
9329
|
|
|
9211
9330
|
**Example with populated fields (DO NOT copy literally \u2014 adapt to your actual analysis):**
|
|
@@ -9393,7 +9512,7 @@ Standard planning cycle with full board review.
|
|
|
9393
9512
|
**\u26A0\uFE0F PRIORITY RECALIBRATION \u2014 do NOT rubber-stamp the submitted priority.** The priority set at idea submission reflects the submitter's view at that time, which may be outdated by the time the planner runs. For EVERY unreviewed task, evaluate its priority FROM SCRATCH against: (a) current horizon/stage/phase goals, (b) recent Active Decision changes, (c) recently shipped functionality that makes this task more or less urgent. If your assessed priority differs from the submitted one, set the new priority in \`boardCorrections\` and include the change in a **Priority Recalibration** paragraph in your cycle log (Step 8): list each changed task by ID, old priority \u2192 new priority, and a 1-sentence rationale. This paragraph is how the user sees what the planner recalibrated and why. If no priorities changed during triage, omit the paragraph.
|
|
9394
9513
|
Also set complexity using the full range \u2014 **XS, Small, Medium, Large, XL** \u2014 based on actual scope, not conservatively. XS = single-line or config change. Small = one file, < 50 lines. Medium = 2-5 files. Large = cross-module, multiple components. XL = architectural, multi-day.
|
|
9395
9514
|
**Module classification for cross-cutting tasks:** When a task title contains "audit"/"unfiltered"/"scoping"/"leak" plus a database-entity name (e.g. "audit ... cycle_learnings reads", "unfiltered cycle_tasks queries"), classify the module by the actual code surface that reads/writes the entity \u2014 not by the tool names mentioned in the title. The reasoning surface (e.g. "health" or "strategy_review") is often unrelated to the data-access surface. Resolve this by treating the entity name as the routing signal: tasks touching dashboard read/write paths belong to the Dashboard module even if the title mentions an MCP tool. Misclassification routes the task to the wrong shared cycle branch and surfaces the wrong MODULE INSTRUCTIONS to the builder.
|
|
9396
|
-
If a task is clearly obsolete, duplicated, or rejected, set its status to "Cancelled" with a \`closureReason\` explaining why.
|
|
9515
|
+
If a task is clearly obsolete, duplicated, or rejected, set its status to "Cancelled" with a \`closureReason\` explaining why. **"It contradicts an Active Decision" is NOT one of those reasons** \u2014 route it to \`decisionConflicts\` instead (see the AD Conflict Surfacing rule below step 7).
|
|
9397
9516
|
**\u2192 PERSIST:** For each task you set reviewed: true, corrected fields on, or marked "Cancelled", include it in \`boardCorrections\` in Part 2.
|
|
9398
9517
|
|
|
9399
9518
|
3. **Board Integrity** \u2014 All tasks have complete fields? Priority still accurate? Duplicates? Stale In Progress tasks?
|
|
@@ -9436,6 +9555,10 @@ Standard planning cycle with full board review.
|
|
|
9436
9555
|
**Epic-aware batching:** Epic is the primary grouping signal for theme coherence. When multiple candidate tasks share the same epic (e.g. "Onboarding Redesign", "Dashboard Polish"), prefer co-scheduling them \u2014 they solve connected problems and benefit from shared context during the build. Steps: (1) After filtering by priority, group eligible tasks by epic. (2) If an epic has 3+ eligible tasks, prefer scheduling 2-4 of them together over cherry-picking across epics. (3) Report the epic distribution in the cycle log (e.g. "4 tasks from Onboarding epic, 1 from Platform"). Priority still overrides: a P0 fix from a different epic always takes precedence.
|
|
9437
9556
|
**Opportunity clustering:** If backlog tasks have an \`opportunity\` field populated, group them by opportunity before selecting. Tasks sharing the same opportunity solve the same user problem \u2014 co-scheduling them produces more coherent cycles. Report opportunity clusters in the cycle log when present (e.g. "3 tasks clustered under 'planner accuracy' opportunity").
|
|
9438
9557
|
|
|
9558
|
+
${AD_CONFLICT_SURFACING_RULES}
|
|
9559
|
+
|
|
9560
|
+
**Where this fires:** during Inbox Triage (step 2), the Priority Drift Check (step 3), and selection (step 7). A task whose notes already carry an \`AD-CONFLICT:\` line, or an "AD-N alignment/conflict candidate" note from the idea tool, is a mandatory candidate \u2014 read it, and either schedule it, cancel it on non-AD grounds, or emit the conflict. Do not leave it sitting silently in Backlog for a third cycle.
|
|
9561
|
+
|
|
9439
9562
|
8. **Cycle Log** \u2014 Write 5-10 line entry: what was triaged, what was recommended and why, observations, AD updates. Include a **Priority Recalibration** paragraph if any unreviewed task priorities were changed during triage (Step 2) \u2014 list each by ID with old \u2192 new priority and rationale. Include a **Priority Drift Suggestions** paragraph if reviewed task drift was detected (Step 3).
|
|
9440
9563
|
**Cycle Notes** \u2014 Optionally include 1-3 lines of cycle-level observations in \`cycleLogNotes\`: estimation accuracy patterns, recurring blockers, velocity trends, or dependency signals. These notes persist across cycles so future planning runs can learn from them. Use null if there are no noteworthy observations this cycle.
|
|
9441
9564
|
|
|
@@ -9834,6 +9957,17 @@ function coerceToString(value) {
|
|
|
9834
9957
|
if (value === null || value === void 0) return "";
|
|
9835
9958
|
return JSON.stringify(value, null, 2);
|
|
9836
9959
|
}
|
|
9960
|
+
function coerceCarryForward(value) {
|
|
9961
|
+
if (value === null || value === void 0) return { value: null };
|
|
9962
|
+
if (typeof value === "string") {
|
|
9963
|
+
const trimmed = value.trim();
|
|
9964
|
+
return { value: trimmed.length > 0 ? trimmed : null };
|
|
9965
|
+
}
|
|
9966
|
+
const shape = Array.isArray(value) ? "array" : typeof value;
|
|
9967
|
+
const warning = `cycleLogCarryForward was ${shape}, not a string \u2014 DROPPED rather than persisted. Carry-forward is prose that orient parses for the WHAT SHIPS FOR USERS / RELEASE MECHANICS labels; a non-string value cannot carry them. Re-run plan apply with cycleLogCarryForward as a single string (or null) to record one for this cycle.`;
|
|
9968
|
+
console.error(`[plan] ${warning}`);
|
|
9969
|
+
return { value: null, warning };
|
|
9970
|
+
}
|
|
9837
9971
|
function coerceStructuredOutput(parsed) {
|
|
9838
9972
|
const cycleHandoffs = Array.isArray(parsed.cycleHandoffs) ? parsed.cycleHandoffs.map((h) => {
|
|
9839
9973
|
const { taskId: _t, buildHandoff: _b, ...rest } = h;
|
|
@@ -9873,10 +10007,19 @@ function coerceStructuredOutput(parsed) {
|
|
|
9873
10007
|
body: coerceToString(ad.body)
|
|
9874
10008
|
})) : [];
|
|
9875
10009
|
const cycleTaskIds = Array.isArray(parsed.cycleTaskIds) ? parsed.cycleTaskIds.map((id) => coerceToString(id)) : void 0;
|
|
10010
|
+
const carryForward = coerceCarryForward(parsed.cycleLogCarryForward);
|
|
10011
|
+
const decisionConflicts = Array.isArray(parsed.decisionConflicts) ? parsed.decisionConflicts.map((c) => ({
|
|
10012
|
+
taskId: coerceToString(c.taskId).trim(),
|
|
10013
|
+
adId: coerceToString(c.adId).trim(),
|
|
10014
|
+
conflict: coerceToString(c.conflict).trim(),
|
|
10015
|
+
options: Array.isArray(c.options) ? c.options.map((o) => coerceToString(o)) : [],
|
|
10016
|
+
recommendation: coerceToString(c.recommendation).trim()
|
|
10017
|
+
})).filter((c) => c.taskId.length > 0 && c.adId.length > 0) : void 0;
|
|
9876
10018
|
return {
|
|
9877
10019
|
cycleLogTitle: coerceToString(parsed.cycleLogTitle),
|
|
9878
10020
|
cycleLogContent: coerceToString(parsed.cycleLogContent),
|
|
9879
|
-
cycleLogCarryForward:
|
|
10021
|
+
cycleLogCarryForward: carryForward.value,
|
|
10022
|
+
...carryForward.warning ? { coercionWarnings: [carryForward.warning] } : {},
|
|
9880
10023
|
cycleLogNotes: parsed.cycleLogNotes === null ? null : coerceToString(parsed.cycleLogNotes),
|
|
9881
10024
|
nextMode: "Full",
|
|
9882
10025
|
boardHealth: coerceToString(parsed.boardHealth),
|
|
@@ -9887,7 +10030,8 @@ function coerceStructuredOutput(parsed) {
|
|
|
9887
10030
|
newTasks,
|
|
9888
10031
|
boardCorrections,
|
|
9889
10032
|
productBrief: parsed.productBrief === null ? null : coerceToString(parsed.productBrief),
|
|
9890
|
-
activeDecisions
|
|
10033
|
+
activeDecisions,
|
|
10034
|
+
decisionConflicts
|
|
9891
10035
|
};
|
|
9892
10036
|
}
|
|
9893
10037
|
var REVIEW_SYSTEM_COMPRESSION_SECTION = `
|
|
@@ -9962,6 +10106,7 @@ You MUST cover these 5 sections. Each is mandatory.
|
|
|
9962
10106
|
- Only flag ADs that represent a genuine strategic question requiring owner input
|
|
9963
10107
|
- Note any hierarchy/phase issues worth correcting (1-2 bullets max)
|
|
9964
10108
|
- Delete ADs that are legacy, process-level, or redundant without discussion
|
|
10109
|
+
- **Blocked-on-decision tasks are evidence (task-3219).** If the board carries tasks Blocked behind a \`decision-gate\` on an AD, that AD is actively costing the project work. Resolve it here \u2014 supersede, modify, or explicitly reaffirm it with a one-line reason. Reaffirming is a legitimate answer; leaving it unaddressed is not, because the blocked work stays parked until someone rules.
|
|
9965
10110
|
|
|
9966
10111
|
${AD_REJECTION_RULES}
|
|
9967
10112
|
|
|
@@ -11575,6 +11720,52 @@ function applyContextTier(ctx, cycleCount) {
|
|
|
11575
11720
|
}
|
|
11576
11721
|
return { tier, label };
|
|
11577
11722
|
}
|
|
11723
|
+
var PARKABLE_STATUSES = /* @__PURE__ */ new Set([
|
|
11724
|
+
"Backlog",
|
|
11725
|
+
"In Cycle",
|
|
11726
|
+
"Ready",
|
|
11727
|
+
"Deferred"
|
|
11728
|
+
]);
|
|
11729
|
+
async function parkDecisionConflicts(adapter2, conflicts, newTaskIdMap, blockedCycle, scheduledTaskIds = []) {
|
|
11730
|
+
if (conflicts.length === 0) return [];
|
|
11731
|
+
const scheduled = new Set(scheduledTaskIds.map((id) => id.toLowerCase()));
|
|
11732
|
+
const results = [];
|
|
11733
|
+
for (const conflict of conflicts) {
|
|
11734
|
+
const resolvedTaskId = newTaskIdMap.get(conflict.taskId) ?? conflict.taskId;
|
|
11735
|
+
const base = { ...conflict, resolvedTaskId, parked: false };
|
|
11736
|
+
if (scheduled.has(resolvedTaskId.toLowerCase())) {
|
|
11737
|
+
results.push({ ...base, skipReason: "scheduled in this cycle \u2014 left in the cycle, conflict still surfaced" });
|
|
11738
|
+
continue;
|
|
11739
|
+
}
|
|
11740
|
+
try {
|
|
11741
|
+
const [task] = await adapter2.getTasks([resolvedTaskId]);
|
|
11742
|
+
if (!task) {
|
|
11743
|
+
results.push({ ...base, skipReason: "task not found" });
|
|
11744
|
+
continue;
|
|
11745
|
+
}
|
|
11746
|
+
if (!PARKABLE_STATUSES.has(task.status)) {
|
|
11747
|
+
results.push({ ...base, skipReason: `status is ${task.status} \u2014 left as-is` });
|
|
11748
|
+
continue;
|
|
11749
|
+
}
|
|
11750
|
+
if (task.status === "Deferred") {
|
|
11751
|
+
await adapter2.updateTask(resolvedTaskId, { status: "Backlog" });
|
|
11752
|
+
}
|
|
11753
|
+
await adapter2.updateTask(resolvedTaskId, {
|
|
11754
|
+
status: "Blocked",
|
|
11755
|
+
blocker: {
|
|
11756
|
+
type: "decision-gate",
|
|
11757
|
+
ref: conflict.adId,
|
|
11758
|
+
reason: conflict.conflict || `Contradicts ${conflict.adId} \u2014 awaiting the owner's decision.`,
|
|
11759
|
+
blockedCycle
|
|
11760
|
+
}
|
|
11761
|
+
});
|
|
11762
|
+
results.push({ ...base, parked: true });
|
|
11763
|
+
} catch (err) {
|
|
11764
|
+
results.push({ ...base, skipReason: err instanceof Error ? err.message : String(err) });
|
|
11765
|
+
}
|
|
11766
|
+
}
|
|
11767
|
+
return results;
|
|
11768
|
+
}
|
|
11578
11769
|
function determineMode(totalCycles) {
|
|
11579
11770
|
if (totalCycles === 0) return "bootstrap";
|
|
11580
11771
|
return "full";
|
|
@@ -13086,6 +13277,7 @@ async function processLlmOutput(adapter2, config2, rawOutput, mode, cycleNumber,
|
|
|
13086
13277
|
let writeBackWarnings;
|
|
13087
13278
|
let writeSummary;
|
|
13088
13279
|
let skippedCancellations;
|
|
13280
|
+
let decisionConflicts;
|
|
13089
13281
|
const persistenceFailed = !data;
|
|
13090
13282
|
if (data) {
|
|
13091
13283
|
try {
|
|
@@ -13097,8 +13289,9 @@ async function processLlmOutput(adapter2, config2, rawOutput, mode, cycleNumber,
|
|
|
13097
13289
|
contextHashes,
|
|
13098
13290
|
{ confirmCancellations: planRunMeta?.confirmCancellations === true, ownerUserId: applyScope.callerUserId ?? void 0 }
|
|
13099
13291
|
);
|
|
13100
|
-
|
|
13101
|
-
|
|
13292
|
+
const allWarnings = [...data.coercionWarnings ?? [], ...wbWarnings];
|
|
13293
|
+
if (allWarnings.length > 0) {
|
|
13294
|
+
writeBackWarnings = allWarnings;
|
|
13102
13295
|
}
|
|
13103
13296
|
if (skipped.length > 0) {
|
|
13104
13297
|
skippedCancellations = skipped;
|
|
@@ -13111,6 +13304,19 @@ async function processLlmOutput(adapter2, config2, rawOutput, mode, cycleNumber,
|
|
|
13111
13304
|
for (const [placeholder, realId] of newTaskIdMap) {
|
|
13112
13305
|
resolvedDisplayText = resolvedDisplayText.replaceAll(placeholder, realId);
|
|
13113
13306
|
}
|
|
13307
|
+
if (data.decisionConflicts && data.decisionConflicts.length > 0) {
|
|
13308
|
+
try {
|
|
13309
|
+
const parked = await parkDecisionConflicts(
|
|
13310
|
+
adapter2,
|
|
13311
|
+
data.decisionConflicts,
|
|
13312
|
+
newTaskIdMap,
|
|
13313
|
+
cycleNumber + 1,
|
|
13314
|
+
ws.taskIds
|
|
13315
|
+
);
|
|
13316
|
+
if (parked.length > 0) decisionConflicts = parked;
|
|
13317
|
+
} catch {
|
|
13318
|
+
}
|
|
13319
|
+
}
|
|
13114
13320
|
if (adapter2.insertPlanRun) {
|
|
13115
13321
|
const durationMs = Date.now() - applyStartMs + (planRunMeta?.prepareStartMs !== void 0 ? Date.now() - planRunMeta.prepareStartMs : 0);
|
|
13116
13322
|
adapter2.insertPlanRun({
|
|
@@ -13163,7 +13369,8 @@ async function processLlmOutput(adapter2, config2, rawOutput, mode, cycleNumber,
|
|
|
13163
13369
|
writeBackFailed,
|
|
13164
13370
|
writeBackWarnings,
|
|
13165
13371
|
writeSummary,
|
|
13166
|
-
skippedCancellations
|
|
13372
|
+
skippedCancellations,
|
|
13373
|
+
decisionConflicts
|
|
13167
13374
|
};
|
|
13168
13375
|
}
|
|
13169
13376
|
async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnly, skipHandoffs, tracker, density) {
|
|
@@ -13782,6 +13989,47 @@ async function resolveLlmResponse(inlineResponse, filePath) {
|
|
|
13782
13989
|
return { ok: true, llmResponse: contents };
|
|
13783
13990
|
}
|
|
13784
13991
|
|
|
13992
|
+
// src/lib/overflow.ts
|
|
13993
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
13994
|
+
import { tmpdir } from "os";
|
|
13995
|
+
import { join as join3 } from "path";
|
|
13996
|
+
import { createHash as createHash2 } from "crypto";
|
|
13997
|
+
var DISPATCH_THRESHOLD_BYTES = 50 * 1024;
|
|
13998
|
+
var ELIDE_THRESHOLD_BYTES = 90 * 1024;
|
|
13999
|
+
var INLINE_CEILING_BYTES = 40 * 1024;
|
|
14000
|
+
function byteLength(s) {
|
|
14001
|
+
return Buffer.byteLength(s, "utf-8");
|
|
14002
|
+
}
|
|
14003
|
+
function shouldDispatch(contextBytes, explicit) {
|
|
14004
|
+
if (explicit === true) return true;
|
|
14005
|
+
if (explicit === false) return false;
|
|
14006
|
+
if (process.env.PAPI_AUTO_DISPATCH === "false") return false;
|
|
14007
|
+
return contextBytes > DISPATCH_THRESHOLD_BYTES;
|
|
14008
|
+
}
|
|
14009
|
+
function canSpill(adapterType) {
|
|
14010
|
+
return adapterType !== "proxy";
|
|
14011
|
+
}
|
|
14012
|
+
function spillPath(kind, projectId, callerKey) {
|
|
14013
|
+
const id = createHash2("sha256").update(`${projectId ?? "no-project"}|${callerKey ?? "default"}`).digest("hex").slice(0, 16);
|
|
14014
|
+
return join3(tmpdir(), `papi-${kind}-${id}.md`);
|
|
14015
|
+
}
|
|
14016
|
+
function spill(kind, content, projectId, callerKey) {
|
|
14017
|
+
const path7 = spillPath(kind, projectId, callerKey);
|
|
14018
|
+
writeFileSync2(path7, content, { mode: 384 });
|
|
14019
|
+
return path7;
|
|
14020
|
+
}
|
|
14021
|
+
function planDelivery(input) {
|
|
14022
|
+
const bytes = byteLength(input.payload);
|
|
14023
|
+
if (bytes <= INLINE_CEILING_BYTES) return { mode: "inline", payload: input.payload };
|
|
14024
|
+
if (!canSpill(input.adapterType)) return { mode: "undeliverable", bytes };
|
|
14025
|
+
try {
|
|
14026
|
+
const path7 = spill(input.kind, input.payload, input.projectId, input.callerKey);
|
|
14027
|
+
return { mode: "spilled", path: path7, bytes };
|
|
14028
|
+
} catch {
|
|
14029
|
+
return { mode: "undeliverable", bytes };
|
|
14030
|
+
}
|
|
14031
|
+
}
|
|
14032
|
+
|
|
13785
14033
|
// src/services/session-guidance.ts
|
|
13786
14034
|
var DEFAULT_CALLER_KEY = "__default__";
|
|
13787
14035
|
var sessionStates = /* @__PURE__ */ new Map();
|
|
@@ -13982,20 +14230,20 @@ var PerCallerCache = class {
|
|
|
13982
14230
|
};
|
|
13983
14231
|
|
|
13984
14232
|
// src/lib/plan-prepare-store.ts
|
|
13985
|
-
import { createHash as
|
|
13986
|
-
import { readFileSync as readFileSync2, writeFileSync as
|
|
13987
|
-
import { tmpdir } from "os";
|
|
13988
|
-
import { join as
|
|
14233
|
+
import { createHash as createHash3 } from "crypto";
|
|
14234
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync3, unlinkSync, existsSync, statSync } from "fs";
|
|
14235
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
14236
|
+
import { join as join4 } from "path";
|
|
13989
14237
|
var SPILL_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
13990
14238
|
var DEFAULT_CALLER_KEY3 = "__default__";
|
|
13991
|
-
function
|
|
13992
|
-
const id =
|
|
13993
|
-
return
|
|
14239
|
+
function spillPath2(projectId, callerKey) {
|
|
14240
|
+
const id = createHash3("sha256").update(`${projectId ?? "no-project"}|${callerKey ?? DEFAULT_CALLER_KEY3}`).digest("hex").slice(0, 16);
|
|
14241
|
+
return join4(tmpdir2(), `papi-plan-prepare-${id}.json`);
|
|
13994
14242
|
}
|
|
13995
14243
|
function savePrepareSpill(projectId, callerKey, state) {
|
|
13996
14244
|
try {
|
|
13997
|
-
|
|
13998
|
-
|
|
14245
|
+
writeFileSync3(
|
|
14246
|
+
spillPath2(projectId, callerKey),
|
|
13999
14247
|
JSON.stringify({ savedAt: Date.now(), state }),
|
|
14000
14248
|
{ mode: 384 }
|
|
14001
14249
|
);
|
|
@@ -14003,7 +14251,7 @@ function savePrepareSpill(projectId, callerKey, state) {
|
|
|
14003
14251
|
}
|
|
14004
14252
|
}
|
|
14005
14253
|
function loadPrepareSpill(projectId, callerKey) {
|
|
14006
|
-
const path7 =
|
|
14254
|
+
const path7 = spillPath2(projectId, callerKey);
|
|
14007
14255
|
try {
|
|
14008
14256
|
if (!existsSync(path7)) return void 0;
|
|
14009
14257
|
if (Date.now() - statSync(path7).mtimeMs > SPILL_TTL_MS) {
|
|
@@ -14018,18 +14266,18 @@ function loadPrepareSpill(projectId, callerKey) {
|
|
|
14018
14266
|
}
|
|
14019
14267
|
function clearPrepareSpill(projectId, callerKey) {
|
|
14020
14268
|
try {
|
|
14021
|
-
const path7 =
|
|
14269
|
+
const path7 = spillPath2(projectId, callerKey);
|
|
14022
14270
|
if (existsSync(path7)) unlinkSync(path7);
|
|
14023
14271
|
} catch {
|
|
14024
14272
|
}
|
|
14025
14273
|
}
|
|
14026
14274
|
function contextPath(projectId, callerKey) {
|
|
14027
|
-
const id =
|
|
14028
|
-
return
|
|
14275
|
+
const id = createHash3("sha256").update(`${projectId ?? "no-project"}|${callerKey ?? DEFAULT_CALLER_KEY3}`).digest("hex").slice(0, 16);
|
|
14276
|
+
return join4(tmpdir2(), `papi-plan-context-${id}.md`);
|
|
14029
14277
|
}
|
|
14030
14278
|
function savePrepareContextFile(projectId, callerKey, content) {
|
|
14031
14279
|
const path7 = contextPath(projectId, callerKey);
|
|
14032
|
-
|
|
14280
|
+
writeFileSync3(path7, content, { mode: 384 });
|
|
14033
14281
|
return path7;
|
|
14034
14282
|
}
|
|
14035
14283
|
|
|
@@ -14169,6 +14417,20 @@ function formatPlanResult(result) {
|
|
|
14169
14417
|
lines.push('To apply these cancellations, re-run `plan` with `mode: "apply"` and `confirm_cancellations: true`.');
|
|
14170
14418
|
lines.push("To keep these tasks, do nothing \u2014 they remain on the board.");
|
|
14171
14419
|
}
|
|
14420
|
+
const conflicts = result.decisionConflicts ?? [];
|
|
14421
|
+
if (conflicts.length > 0) {
|
|
14422
|
+
lines.push("");
|
|
14423
|
+
lines.push(`\u{1F500} **${conflicts.length} decision(s) need your call \u2014 work was NOT dropped.**`);
|
|
14424
|
+
lines.push("These tasks cut against a live Active Decision. An AD is *active* \u2014 it can be superseded, so the planner parked the work instead of shelving it:");
|
|
14425
|
+
for (const c of conflicts) {
|
|
14426
|
+
const state = c.parked ? `Blocked behind ${c.adId}` : `still ${c.skipReason ?? "unparked"}`;
|
|
14427
|
+
lines.push(`- **${c.resolvedTaskId} vs ${c.adId}** \u2014 ${c.conflict} _(${state})_`);
|
|
14428
|
+
if (c.options.length > 0) lines.push(` Options: ${c.options.join(" | ")}`);
|
|
14429
|
+
if (c.recommendation) lines.push(` Planner recommends: ${c.recommendation}`);
|
|
14430
|
+
}
|
|
14431
|
+
lines.push("");
|
|
14432
|
+
lines.push('Decide: `strategy_change` with `mode: "capture"` to supersede or modify the AD (parked tasks auto-unblock once it moves), or `board_edit` to cancel the task if the AD stands.');
|
|
14433
|
+
}
|
|
14172
14434
|
if (result.skipHandoffs) {
|
|
14173
14435
|
const taskCount = result.writeSummary?.taskIds.length ?? 0;
|
|
14174
14436
|
lines.push("", `Next: run \`handoff_generate\` to create BUILD HANDOFFs for your ${taskCount} cycle task(s), then \`build_list\` to start building.`);
|
|
@@ -14291,16 +14553,8 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
14291
14553
|
};
|
|
14292
14554
|
planPrepareCache.set(callerKey, prepareState);
|
|
14293
14555
|
savePrepareSpill(adapter2.getProjectId?.(), callerKey, prepareState);
|
|
14294
|
-
const
|
|
14295
|
-
const
|
|
14296
|
-
let dispatch;
|
|
14297
|
-
if (args.dispatch === "inline" || args.dispatch === "subagent") {
|
|
14298
|
-
dispatch = args.dispatch;
|
|
14299
|
-
} else if (autoDispatchEnabled && result.contextBytes !== void 0 && result.contextBytes > autoDispatchThreshold) {
|
|
14300
|
-
dispatch = "subagent";
|
|
14301
|
-
} else {
|
|
14302
|
-
dispatch = "inline";
|
|
14303
|
-
}
|
|
14556
|
+
const explicit = args.dispatch === "inline" ? false : args.dispatch === "subagent" ? true : void 0;
|
|
14557
|
+
const dispatch = shouldDispatch(result.contextBytes ?? 0, explicit) ? "subagent" : "inline";
|
|
14304
14558
|
const modeLabel = result.mode === "bootstrap" ? "Bootstrap" : "Full";
|
|
14305
14559
|
const header = result.strategyReviewWarning ? `${result.strategyReviewWarning}
|
|
14306
14560
|
` : "";
|
|
@@ -14414,10 +14668,10 @@ ${result.userMessage}
|
|
|
14414
14668
|
}
|
|
14415
14669
|
|
|
14416
14670
|
// src/services/strategy.ts
|
|
14417
|
-
import { randomUUID as randomUUID10, createHash as
|
|
14671
|
+
import { randomUUID as randomUUID10, createHash as createHash4 } from "crypto";
|
|
14418
14672
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
14419
14673
|
import { existsSync as existsSync2, readdirSync, statSync as statSync2 } from "fs";
|
|
14420
|
-
import { join as
|
|
14674
|
+
import { join as join5 } from "path";
|
|
14421
14675
|
import { homedir as homedir2 } from "os";
|
|
14422
14676
|
|
|
14423
14677
|
// src/services/idea.ts
|
|
@@ -14767,14 +15021,22 @@ ${lines.join("\n")}
|
|
|
14767
15021
|
}
|
|
14768
15022
|
}
|
|
14769
15023
|
let notesWithAlignment = input.notes || "";
|
|
15024
|
+
let detectedAdIds = [];
|
|
14770
15025
|
try {
|
|
14771
15026
|
const ads = await adapter2.getActiveDecisions();
|
|
14772
15027
|
const ideaCombined = `${input.text} ${input.notes ?? ""}`;
|
|
14773
15028
|
const alignmentMatches = findAdAlignmentMatches(ideaCombined, ads);
|
|
14774
15029
|
if (alignmentMatches.length > 0) {
|
|
14775
15030
|
notesWithAlignment = `${notesWithAlignment}${formatAdAlignmentNote(alignmentMatches)}`.trim();
|
|
15031
|
+
detectedAdIds = alignmentMatches.map((m) => m.displayId);
|
|
14776
15032
|
}
|
|
14777
15033
|
} catch {
|
|
15034
|
+
}
|
|
15035
|
+
const conflictsWithAd = input.conflictsWithAd?.trim();
|
|
15036
|
+
if (conflictsWithAd) {
|
|
15037
|
+
notesWithAlignment = `${notesWithAlignment}
|
|
15038
|
+
|
|
15039
|
+
AD-CONFLICT: ${conflictsWithAd} \u2014 submitter flagged this as cutting against that decision. Owner decides: supersede the AD or drop the task.`.trim();
|
|
14778
15040
|
}
|
|
14779
15041
|
const inherited = await resolveVisibilityFromDocs(adapter2, [input.docRef]);
|
|
14780
15042
|
const task = await adapter2.createTask({
|
|
@@ -14843,17 +15105,52 @@ ${lines.join("\n")}
|
|
|
14843
15105
|
} catch {
|
|
14844
15106
|
}
|
|
14845
15107
|
}
|
|
15108
|
+
let gated = false;
|
|
15109
|
+
if (conflictsWithAd) {
|
|
15110
|
+
try {
|
|
15111
|
+
await adapter2.updateTask(task.id, {
|
|
15112
|
+
status: "Blocked",
|
|
15113
|
+
blocker: {
|
|
15114
|
+
type: "decision-gate",
|
|
15115
|
+
ref: conflictsWithAd,
|
|
15116
|
+
reason: `Submitted as cutting against ${conflictsWithAd}. Awaiting the owner's decision.`,
|
|
15117
|
+
blockedCycle: health.totalCycles
|
|
15118
|
+
}
|
|
15119
|
+
});
|
|
15120
|
+
gated = true;
|
|
15121
|
+
} catch {
|
|
15122
|
+
}
|
|
15123
|
+
}
|
|
14846
15124
|
const typeNote = typeInferred ? ` [type: ${taskType} \u2014 inferred from text]` : "";
|
|
14847
15125
|
const visibilityNote = inherited.visibility !== "public" ? ` [visibility: ${inherited.visibility} \u2014 inherited from source doc]` : "";
|
|
14848
15126
|
const mismatchNote = inherited.mismatchWarning ? `
|
|
14849
15127
|
|
|
14850
15128
|
\u26A0\uFE0F ${inherited.mismatchWarning}` : "";
|
|
15129
|
+
const adIds = conflictsWithAd ? [conflictsWithAd] : detectedAdIds;
|
|
15130
|
+
const landing = gated ? "created and gated on the decision" : "added to backlog";
|
|
14851
15131
|
return {
|
|
14852
15132
|
routing: "task",
|
|
14853
15133
|
task,
|
|
14854
|
-
message: `${task.id}: "${task.title}" \u2014
|
|
15134
|
+
message: `${task.id}: "${task.title}" \u2014 ${landing}${typeNote}${visibilityNote}${mismatchNote}`,
|
|
15135
|
+
...adIds.length > 0 ? { adConflicts: { adIds, explicit: Boolean(conflictsWithAd), gated } } : {}
|
|
14855
15136
|
};
|
|
14856
15137
|
}
|
|
15138
|
+
function buildAdConflictNote(conflictsWithAd, detectedAdIds, gated) {
|
|
15139
|
+
if (conflictsWithAd) {
|
|
15140
|
+
const state = gated ? `Parked as **Blocked** behind a decision gate on ${conflictsWithAd} \u2014 it auto-unblocks once that AD is superseded, modified, or reaffirmed.` : `Recorded against ${conflictsWithAd} (gate could not be applied \u2014 the AD-CONFLICT note is on the task).`;
|
|
15141
|
+
return `
|
|
15142
|
+
|
|
15143
|
+
\u{1F500} **Conflicts with ${conflictsWithAd}.** ${state}
|
|
15144
|
+
**Tell the user now** \u2014 an AD is *active*, so this is their call to make, not yours. Offer both paths: supersede the AD via \`strategy_change\`, or leave the AD standing and drop the task.`;
|
|
15145
|
+
}
|
|
15146
|
+
if (detectedAdIds.length > 0) {
|
|
15147
|
+
return `
|
|
15148
|
+
|
|
15149
|
+
\u{1F500} **Possible conflict with ${detectedAdIds.join(", ")}** (keyword match \u2014 verify it is real).
|
|
15150
|
+
If it IS a real contradiction, **do not shelve the idea on those grounds**: surface it to the user and re-submit with \`conflicts_with_ad\` so the task is gated on the decision instead of quietly competing in the backlog.`;
|
|
15151
|
+
}
|
|
15152
|
+
return "";
|
|
15153
|
+
}
|
|
14857
15154
|
var CANVAS_SECTION_LABELS = {
|
|
14858
15155
|
landscape: "Landscape References",
|
|
14859
15156
|
journeys: "User Journeys",
|
|
@@ -15424,7 +15721,7 @@ ${surpriseDigestText}` : buildReportsText;
|
|
|
15424
15721
|
try {
|
|
15425
15722
|
const fullCanvasText = formatDiscoveryCanvas(canvas);
|
|
15426
15723
|
if (fullCanvasText) {
|
|
15427
|
-
const canvasHash =
|
|
15724
|
+
const canvasHash = createHash4("md5").update(fullCanvasText).digest("hex");
|
|
15428
15725
|
const lastReview = previousStrategyReviews?.[0];
|
|
15429
15726
|
const prevHash = lastReview?.structuredData?.canvasHash;
|
|
15430
15727
|
if (prevHash && prevHash === canvasHash) {
|
|
@@ -15494,11 +15791,11 @@ ${lines.join("\n")}`;
|
|
|
15494
15791
|
}
|
|
15495
15792
|
let recentPlansText;
|
|
15496
15793
|
try {
|
|
15497
|
-
const plansDir =
|
|
15794
|
+
const plansDir = join5(homedir2(), ".claude", "plans");
|
|
15498
15795
|
if (existsSync2(plansDir)) {
|
|
15499
15796
|
const lastReviewDate = previousStrategyReviews?.[0]?.createdAt ? new Date(previousStrategyReviews[0].createdAt) : /* @__PURE__ */ new Date(0);
|
|
15500
15797
|
const planFiles = readdirSync(plansDir).filter((f) => f.endsWith(".md")).map((f) => {
|
|
15501
|
-
const fullPath =
|
|
15798
|
+
const fullPath = join5(plansDir, f);
|
|
15502
15799
|
const stat4 = statSync2(fullPath);
|
|
15503
15800
|
return { name: f, modified: stat4.mtime, size: stat4.size };
|
|
15504
15801
|
}).filter((f) => f.modified > lastReviewDate).sort((a, b2) => b2.modified.getTime() - a.modified.getTime()).slice(0, 15);
|
|
@@ -15515,7 +15812,7 @@ ${lines.join("\n")}`;
|
|
|
15515
15812
|
}
|
|
15516
15813
|
let unregisteredDocsText;
|
|
15517
15814
|
try {
|
|
15518
|
-
const docsDir =
|
|
15815
|
+
const docsDir = join5(projectRoot, "docs");
|
|
15519
15816
|
if (hasLocalWorkspace() && existsSync2(docsDir)) {
|
|
15520
15817
|
const registeredPaths = new Set(
|
|
15521
15818
|
(registeredDocs ?? []).map((d) => d.path).filter(Boolean)
|
|
@@ -15523,7 +15820,7 @@ ${lines.join("\n")}`;
|
|
|
15523
15820
|
const allDocFiles = [];
|
|
15524
15821
|
const scanDir = (dir, prefix) => {
|
|
15525
15822
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
15526
|
-
if (entry.isDirectory()) scanDir(
|
|
15823
|
+
if (entry.isDirectory()) scanDir(join5(dir, entry.name), `${prefix}${entry.name}/`);
|
|
15527
15824
|
else if (entry.name.endsWith(".md")) allDocFiles.push(`${prefix}${entry.name}`);
|
|
15528
15825
|
}
|
|
15529
15826
|
};
|
|
@@ -15780,6 +16077,65 @@ function extractDecisionEvidence(ad, eventType, warnings) {
|
|
|
15780
16077
|
}
|
|
15781
16078
|
return { evidenceRef, metricDelta };
|
|
15782
16079
|
}
|
|
16080
|
+
function asDecisionBatchApplier(adapter2) {
|
|
16081
|
+
const candidate = adapter2;
|
|
16082
|
+
return typeof candidate.applyActiveDecisionUpdates === "function" ? candidate : void 0;
|
|
16083
|
+
}
|
|
16084
|
+
function routeDecisionUpdate(ad, adapter2, cycleNumber, warnings) {
|
|
16085
|
+
const action = ad.action;
|
|
16086
|
+
let route;
|
|
16087
|
+
if (action === "delete" && adapter2.deleteActiveDecision) {
|
|
16088
|
+
route = "delete";
|
|
16089
|
+
} else if (action === "new" && adapter2.upsertActiveDecision) {
|
|
16090
|
+
route = "upsert";
|
|
16091
|
+
} else {
|
|
16092
|
+
route = "update";
|
|
16093
|
+
}
|
|
16094
|
+
const titleMatch = ad.body.match(/^###\s+\S+:\s*([^\n[]+?)(?:\s*\[|$)/m);
|
|
16095
|
+
const confidenceMatch = ad.body.match(/\[Confidence:\s*(HIGH|MEDIUM|LOW)\]/i);
|
|
16096
|
+
const eventType = action === "delete" ? "invalidated" : action === "confidence_change" ? "confidence_changed" : action === "supersede" ? "superseded" : action === "new" ? "created" : "modified";
|
|
16097
|
+
const evidence = extractDecisionEvidence(ad, eventType, warnings);
|
|
16098
|
+
return {
|
|
16099
|
+
id: ad.id,
|
|
16100
|
+
body: ad.body,
|
|
16101
|
+
route,
|
|
16102
|
+
action,
|
|
16103
|
+
title: titleMatch ? titleMatch[1].trim() : ad.id,
|
|
16104
|
+
confidence: confidenceMatch ? confidenceMatch[1].toUpperCase() : "MEDIUM",
|
|
16105
|
+
event: {
|
|
16106
|
+
decisionId: ad.id,
|
|
16107
|
+
eventType,
|
|
16108
|
+
cycle: cycleNumber,
|
|
16109
|
+
source: "strategy_review",
|
|
16110
|
+
sourceRef: `cycle-${cycleNumber}-review`,
|
|
16111
|
+
detail: `Action: ${action}`,
|
|
16112
|
+
evidenceRef: evidence.evidenceRef,
|
|
16113
|
+
metricDelta: evidence.metricDelta
|
|
16114
|
+
}
|
|
16115
|
+
};
|
|
16116
|
+
}
|
|
16117
|
+
async function applyDecisionUpdates(adapter2, updates, cycleNumber, warnings) {
|
|
16118
|
+
if (updates.length === 0) return;
|
|
16119
|
+
const applies = updates.map((ad) => routeDecisionUpdate(ad, adapter2, cycleNumber, warnings));
|
|
16120
|
+
const batch = asDecisionBatchApplier(adapter2);
|
|
16121
|
+
if (batch) {
|
|
16122
|
+
await batch.applyActiveDecisionUpdates(applies, cycleNumber);
|
|
16123
|
+
return;
|
|
16124
|
+
}
|
|
16125
|
+
for (const apply of applies) {
|
|
16126
|
+
if (apply.route === "delete") {
|
|
16127
|
+
await adapter2.deleteActiveDecision(apply.id);
|
|
16128
|
+
} else if (apply.route === "upsert") {
|
|
16129
|
+
await adapter2.upsertActiveDecision(apply.id, apply.body, apply.title, apply.confidence, cycleNumber);
|
|
16130
|
+
} else {
|
|
16131
|
+
await adapter2.updateActiveDecision(apply.id, apply.body, cycleNumber, apply.action);
|
|
16132
|
+
}
|
|
16133
|
+
try {
|
|
16134
|
+
await adapter2.appendDecisionEvent(apply.event);
|
|
16135
|
+
} catch {
|
|
16136
|
+
}
|
|
16137
|
+
}
|
|
16138
|
+
}
|
|
15783
16139
|
async function writeBack2(adapter2, cycleNumber, data, fullAnalysis, warnings) {
|
|
15784
16140
|
const cleanTitle = data.sessionLogTitle.replace(/^(?:Cycle|Session)\s+\d+\s*—\s*/i, "").trim();
|
|
15785
16141
|
const cleanContent = data.sessionLogContent.replace(/^#{1,3}\s+(?:Cycle|Session)\s+\d+\s*—[^\n]*\n*/i, "").trim();
|
|
@@ -15804,7 +16160,7 @@ ${cleanContent}`;
|
|
|
15804
16160
|
const currentCanvas = await adapter2.readDiscoveryCanvas();
|
|
15805
16161
|
const canvasText = formatDiscoveryCanvas(currentCanvas);
|
|
15806
16162
|
if (canvasText) {
|
|
15807
|
-
return { ...sd, canvasHash:
|
|
16163
|
+
return { ...sd, canvasHash: createHash4("md5").update(canvasText).digest("hex") };
|
|
15808
16164
|
}
|
|
15809
16165
|
} catch {
|
|
15810
16166
|
}
|
|
@@ -15821,34 +16177,7 @@ ${cleanContent}`;
|
|
|
15821
16177
|
} catch {
|
|
15822
16178
|
}
|
|
15823
16179
|
if (data.activeDecisionUpdates && data.activeDecisionUpdates.length > 0) {
|
|
15824
|
-
await
|
|
15825
|
-
if (ad.action === "delete" && adapter2.deleteActiveDecision) {
|
|
15826
|
-
await adapter2.deleteActiveDecision(ad.id);
|
|
15827
|
-
} else if (ad.action === "new" && adapter2.upsertActiveDecision) {
|
|
15828
|
-
const titleMatch = ad.body.match(/^###\s+\S+:\s*([^\n[]+?)(?:\s*\[|$)/m);
|
|
15829
|
-
const title = titleMatch ? titleMatch[1].trim() : ad.id;
|
|
15830
|
-
const confidenceMatch = ad.body.match(/\[Confidence:\s*(HIGH|MEDIUM|LOW)\]/i);
|
|
15831
|
-
const confidence = confidenceMatch ? confidenceMatch[1].toUpperCase() : "MEDIUM";
|
|
15832
|
-
await adapter2.upsertActiveDecision(ad.id, ad.body, title, confidence, cycleNumber);
|
|
15833
|
-
} else {
|
|
15834
|
-
await adapter2.updateActiveDecision(ad.id, ad.body, cycleNumber, ad.action);
|
|
15835
|
-
}
|
|
15836
|
-
const eventType = ad.action === "delete" ? "invalidated" : ad.action === "confidence_change" ? "confidence_changed" : ad.action === "supersede" ? "superseded" : ad.action === "new" ? "created" : "modified";
|
|
15837
|
-
const evidence = extractDecisionEvidence(ad, eventType, warnings);
|
|
15838
|
-
try {
|
|
15839
|
-
await adapter2.appendDecisionEvent({
|
|
15840
|
-
decisionId: ad.id,
|
|
15841
|
-
eventType,
|
|
15842
|
-
cycle: cycleNumber,
|
|
15843
|
-
source: "strategy_review",
|
|
15844
|
-
sourceRef: `cycle-${cycleNumber}-review`,
|
|
15845
|
-
detail: `Action: ${ad.action}`,
|
|
15846
|
-
evidenceRef: evidence.evidenceRef,
|
|
15847
|
-
metricDelta: evidence.metricDelta
|
|
15848
|
-
});
|
|
15849
|
-
} catch {
|
|
15850
|
-
}
|
|
15851
|
-
}));
|
|
16180
|
+
await applyDecisionUpdates(adapter2, data.activeDecisionUpdates, cycleNumber, warnings);
|
|
15852
16181
|
}
|
|
15853
16182
|
try {
|
|
15854
16183
|
if (adapter2.confirmPendingActiveDecisions) {
|
|
@@ -16606,37 +16935,45 @@ async function prepareStrategyChange(adapter2, text, adapterType) {
|
|
|
16606
16935
|
async function applyStrategyChangeOutput(adapter2, rawLlmOutput, cycleNumber) {
|
|
16607
16936
|
return processStrategyChangeOutput(adapter2, rawLlmOutput, cycleNumber);
|
|
16608
16937
|
}
|
|
16938
|
+
function asDecisionIdAllocator(adapter2) {
|
|
16939
|
+
const candidate = adapter2;
|
|
16940
|
+
return typeof candidate.allocateActiveDecision === "function" ? candidate : void 0;
|
|
16941
|
+
}
|
|
16942
|
+
var AD_ID_PLACEHOLDER = "{{AD_ID}}";
|
|
16943
|
+
function toAdBodyTemplate(body) {
|
|
16944
|
+
return body.replace(/^(\s*#{1,6}\s+)AD-\d+\b/m, `$1${AD_ID_PLACEHOLDER}`);
|
|
16945
|
+
}
|
|
16609
16946
|
async function captureDecision(adapter2, input) {
|
|
16610
16947
|
const health = await adapter2.getCycleHealth();
|
|
16611
16948
|
const cycleNumber = health.totalCycles;
|
|
16612
|
-
|
|
16613
|
-
|
|
16614
|
-
|
|
16615
|
-
|
|
16616
|
-
|
|
16617
|
-
|
|
16618
|
-
|
|
16619
|
-
|
|
16620
|
-
|
|
16621
|
-
|
|
16622
|
-
|
|
16623
|
-
|
|
16624
|
-
adAction = "created";
|
|
16949
|
+
const supersedesId = input.supersedes?.trim() || void 0;
|
|
16950
|
+
if (supersedesId && input.confidenceOnly) {
|
|
16951
|
+
throw new Error("supersedes cannot be combined with confidence_only \u2014 a confidence bump does not replace a decision.");
|
|
16952
|
+
}
|
|
16953
|
+
if (supersedesId) {
|
|
16954
|
+
const all = await adapter2.getActiveDecisions({ includeRetired: true });
|
|
16955
|
+
if (!all.some((d) => d.id === supersedesId)) {
|
|
16956
|
+
throw new Error(`supersedes: ${supersedesId} does not exist on this project. Check the AD id (e.g. "AD-42").`);
|
|
16957
|
+
}
|
|
16958
|
+
if (supersedesId === input.adId) {
|
|
16959
|
+
throw new Error("An AD cannot supersede itself.");
|
|
16960
|
+
}
|
|
16625
16961
|
}
|
|
16626
16962
|
if (input.confidenceOnly) {
|
|
16627
16963
|
if (!input.adId) {
|
|
16628
16964
|
throw new Error('confidence_only requires adId \u2014 provide the AD to update (e.g. "AD-12")');
|
|
16629
16965
|
}
|
|
16966
|
+
const adId2 = input.adId;
|
|
16630
16967
|
if (adapter2.upsertActiveDecision) {
|
|
16631
16968
|
const existing = await adapter2.getActiveDecisions({ includeRetired: false });
|
|
16632
|
-
const current = existing.find((d) => d.id ===
|
|
16969
|
+
const current = existing.find((d) => d.id === adId2);
|
|
16633
16970
|
const preservedBody = current?.body ?? `- **Decision:** ${input.text}`;
|
|
16634
16971
|
const preservedTitle = current?.title ?? input.text.slice(0, 80);
|
|
16635
|
-
await adapter2.upsertActiveDecision(
|
|
16972
|
+
await adapter2.upsertActiveDecision(adId2, preservedBody, preservedTitle, input.confidence, cycleNumber);
|
|
16636
16973
|
}
|
|
16637
16974
|
try {
|
|
16638
16975
|
await adapter2.appendDecisionEvent({
|
|
16639
|
-
decisionId:
|
|
16976
|
+
decisionId: adId2,
|
|
16640
16977
|
eventType: "modified",
|
|
16641
16978
|
cycle: cycleNumber,
|
|
16642
16979
|
source: "strategy_change",
|
|
@@ -16645,18 +16982,66 @@ async function captureDecision(adapter2, input) {
|
|
|
16645
16982
|
});
|
|
16646
16983
|
} catch {
|
|
16647
16984
|
}
|
|
16648
|
-
return { cycleNumber, adId, adAction: "updated" };
|
|
16985
|
+
return { cycleNumber, adId: adId2, adAction: "updated" };
|
|
16649
16986
|
}
|
|
16650
16987
|
const title = input.text.length > 80 ? input.text.slice(0, 77) + "..." : input.text;
|
|
16651
|
-
const
|
|
16988
|
+
const supersedesLine = supersedesId ? `
|
|
16989
|
+
- **Supersedes:** ${supersedesId}` : "";
|
|
16990
|
+
const bodyTemplate = input.adBody ? toAdBodyTemplate(input.adBody) : `### ${AD_ID_PLACEHOLDER}: ${title} [Confidence: ${input.confidence}]
|
|
16652
16991
|
|
|
16653
|
-
- **Decision:** ${input.text}
|
|
16992
|
+
- **Decision:** ${input.text}${supersedesLine}
|
|
16654
16993
|
- **Evidence:** Captured from conversation, Cycle ${cycleNumber}.
|
|
16655
16994
|
- **Status:** Active`;
|
|
16656
|
-
|
|
16657
|
-
|
|
16995
|
+
let adId;
|
|
16996
|
+
let adAction;
|
|
16997
|
+
if (input.adId) {
|
|
16998
|
+
adId = input.adId;
|
|
16999
|
+
adAction = "updated";
|
|
17000
|
+
const adBody = bodyTemplate.split(AD_ID_PLACEHOLDER).join(adId);
|
|
17001
|
+
if (adapter2.upsertActiveDecision) {
|
|
17002
|
+
await adapter2.upsertActiveDecision(adId, adBody, title, input.confidence, cycleNumber);
|
|
17003
|
+
} else {
|
|
17004
|
+
await adapter2.updateActiveDecision(adId, adBody, cycleNumber);
|
|
17005
|
+
}
|
|
16658
17006
|
} else {
|
|
16659
|
-
|
|
17007
|
+
adAction = "created";
|
|
17008
|
+
const allocator = asDecisionIdAllocator(adapter2);
|
|
17009
|
+
if (allocator) {
|
|
17010
|
+
adId = await allocator.allocateActiveDecision(bodyTemplate, title, input.confidence, cycleNumber);
|
|
17011
|
+
} else {
|
|
17012
|
+
const existingAds = await adapter2.getActiveDecisions({ includeRetired: true });
|
|
17013
|
+
const maxNum = existingAds.reduce((max, ad) => {
|
|
17014
|
+
const match = ad.id.match(/^AD-(\d+)$/);
|
|
17015
|
+
return match ? Math.max(max, parseInt(match[1], 10)) : max;
|
|
17016
|
+
}, 0);
|
|
17017
|
+
adId = `AD-${maxNum + 1}`;
|
|
17018
|
+
const adBody = bodyTemplate.split(AD_ID_PLACEHOLDER).join(adId);
|
|
17019
|
+
if (adapter2.upsertActiveDecision) {
|
|
17020
|
+
await adapter2.upsertActiveDecision(adId, adBody, title, input.confidence, cycleNumber);
|
|
17021
|
+
} else {
|
|
17022
|
+
await adapter2.updateActiveDecision(adId, adBody, cycleNumber);
|
|
17023
|
+
}
|
|
17024
|
+
}
|
|
17025
|
+
}
|
|
17026
|
+
if (supersedesId) {
|
|
17027
|
+
const all = await adapter2.getActiveDecisions({ includeRetired: true });
|
|
17028
|
+
const prior = all.find((d) => d.id === supersedesId);
|
|
17029
|
+
const priorBody = prior?.body ?? "";
|
|
17030
|
+
const note = `
|
|
17031
|
+
|
|
17032
|
+
- **Superseded by:** ${adId} (Cycle ${cycleNumber}) \u2014 ${input.text}`;
|
|
17033
|
+
await adapter2.updateActiveDecision(supersedesId, `${priorBody}${note}`, cycleNumber, "supersede");
|
|
17034
|
+
try {
|
|
17035
|
+
await adapter2.appendDecisionEvent({
|
|
17036
|
+
decisionId: supersedesId,
|
|
17037
|
+
eventType: "superseded",
|
|
17038
|
+
cycle: cycleNumber,
|
|
17039
|
+
source: "strategy_change",
|
|
17040
|
+
sourceRef: `cycle-${cycleNumber}-capture`,
|
|
17041
|
+
detail: `Superseded by ${adId}: ${input.text.slice(0, 180)}`
|
|
17042
|
+
});
|
|
17043
|
+
} catch {
|
|
17044
|
+
}
|
|
16660
17045
|
}
|
|
16661
17046
|
try {
|
|
16662
17047
|
await adapter2.appendDecisionEvent({
|
|
@@ -16677,11 +17062,13 @@ async function captureDecision(adapter2, input) {
|
|
|
16677
17062
|
title: `Decision captured: ${adId}`,
|
|
16678
17063
|
content: `**${adAction === "created" ? "New" : "Updated"} Active Decision** \u2014 ${adId}: ${input.text}
|
|
16679
17064
|
|
|
16680
|
-
Confidence: ${input.confidence}. Captured mid-conversation via strategy_change capture mode (Cycle ${cycleNumber}).`
|
|
17065
|
+
Confidence: ${input.confidence}. Captured mid-conversation via strategy_change capture mode (Cycle ${cycleNumber}).` + (supersedesId ? `
|
|
17066
|
+
|
|
17067
|
+
Supersedes ${supersedesId} (retired, kept as history).` : "")
|
|
16681
17068
|
});
|
|
16682
17069
|
} catch {
|
|
16683
17070
|
}
|
|
16684
|
-
return { cycleNumber, adId, adAction };
|
|
17071
|
+
return { cycleNumber, adId, adAction, supersededId: supersedesId };
|
|
16685
17072
|
}
|
|
16686
17073
|
|
|
16687
17074
|
// src/tools/strategy.ts
|
|
@@ -16797,6 +17184,10 @@ var strategyChangeTool = {
|
|
|
16797
17184
|
type: "boolean",
|
|
16798
17185
|
description: `When true (mode "capture" + ad_id required), only update the confidence level \u2014 leave the AD body unchanged. Use when evidence strength changes but the decision itself hasn't shifted.`
|
|
16799
17186
|
},
|
|
17187
|
+
supersedes: {
|
|
17188
|
+
type: "string",
|
|
17189
|
+
description: 'Existing AD ID this new decision replaces, e.g. "AD-42" (mode "capture" only). The named AD is marked superseded and kept as history \u2014 never overwritten. Use this instead of passing ad_id when the decision has CHANGED rather than been refined.'
|
|
17190
|
+
},
|
|
16800
17191
|
north_star: {
|
|
16801
17192
|
type: "string",
|
|
16802
17193
|
description: 'mode "capture" only \u2014 set/update the project North Star statement directly. orient and the project foundation read it. No decision text required when this is provided.'
|
|
@@ -16893,16 +17284,8 @@ ${recLines.join("\n")}
|
|
|
16893
17284
|
userMessage: result.userMessage,
|
|
16894
17285
|
contextBytes: reviewContextBytes
|
|
16895
17286
|
});
|
|
16896
|
-
const
|
|
16897
|
-
const
|
|
16898
|
-
let dispatch;
|
|
16899
|
-
if (args.dispatch === "inline" || args.dispatch === "subagent") {
|
|
16900
|
-
dispatch = args.dispatch;
|
|
16901
|
-
} else if (autoDispatchEnabled && reviewContextBytes > autoDispatchThreshold) {
|
|
16902
|
-
dispatch = "subagent";
|
|
16903
|
-
} else {
|
|
16904
|
-
dispatch = "inline";
|
|
16905
|
-
}
|
|
17287
|
+
const explicit = args.dispatch === "inline" ? false : args.dispatch === "subagent" ? true : void 0;
|
|
17288
|
+
const dispatch = shouldDispatch(reviewContextBytes, explicit) ? "subagent" : "inline";
|
|
16906
17289
|
if (dispatch === "subagent") {
|
|
16907
17290
|
const dispatchPrompt = buildSubagentDispatchPrompt({
|
|
16908
17291
|
tool: "strategy_review",
|
|
@@ -17030,19 +17413,24 @@ orient and the project foundation will read this value.`
|
|
|
17030
17413
|
const confidence = args.confidence ?? "MEDIUM";
|
|
17031
17414
|
const adBody = args.ad_body;
|
|
17032
17415
|
const confidenceOnly = args.confidence_only === true;
|
|
17416
|
+
const supersedes = args.supersedes?.trim();
|
|
17033
17417
|
const result = await captureDecision(adapter2, {
|
|
17034
17418
|
text: text2.trim(),
|
|
17035
17419
|
adId: adId?.trim(),
|
|
17036
17420
|
confidence,
|
|
17037
17421
|
adBody: adBody?.trim(),
|
|
17038
|
-
confidenceOnly
|
|
17422
|
+
confidenceOnly,
|
|
17423
|
+
supersedes
|
|
17039
17424
|
});
|
|
17040
17425
|
const captureLabel = confidenceOnly ? `Updated confidence on **${result.adId}** to ${confidence} (body preserved)` : `${result.adAction === "created" ? "Created" : "Updated"} **${result.adId}**: ${text2.trim()}
|
|
17041
17426
|
Confidence: ${confidence}`;
|
|
17427
|
+
const supersedeLine = result.supersededId ? `
|
|
17428
|
+
|
|
17429
|
+
**${result.supersededId}** marked superseded by ${result.adId} \u2014 retained as history, not overwritten.` : "";
|
|
17042
17430
|
return textResponse(
|
|
17043
17431
|
`**Decision Captured \u2014 Cycle ${result.cycleNumber}**
|
|
17044
17432
|
|
|
17045
|
-
${captureLabel}
|
|
17433
|
+
${captureLabel}${supersedeLine}
|
|
17046
17434
|
|
|
17047
17435
|
Decision event logged.`
|
|
17048
17436
|
);
|
|
@@ -17153,8 +17541,8 @@ async function viewBoard(adapter2, phaseFilter, options) {
|
|
|
17153
17541
|
const bi = PRIORITY_ORDER.indexOf(b2.priority);
|
|
17154
17542
|
const priorityDiff = (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi);
|
|
17155
17543
|
if (priorityDiff !== 0) return priorityDiff;
|
|
17156
|
-
const aDate = a.createdAt
|
|
17157
|
-
const bDate = b2.createdAt
|
|
17544
|
+
const aDate = a.createdAt ?? "";
|
|
17545
|
+
const bDate = b2.createdAt ?? "";
|
|
17158
17546
|
return bDate.localeCompare(aDate);
|
|
17159
17547
|
});
|
|
17160
17548
|
const total = filtered.length;
|
|
@@ -17508,6 +17896,10 @@ var boardEditTool = {
|
|
|
17508
17896
|
actual_effort: {
|
|
17509
17897
|
$ref: "#/$defs/effortSize",
|
|
17510
17898
|
description: "task-2182: correct the actual effort on this task's LATEST build report (fixes a mis-recorded actual)."
|
|
17899
|
+
},
|
|
17900
|
+
project: {
|
|
17901
|
+
type: "string",
|
|
17902
|
+
description: "Project id (UUID) or slug whose board this task lives on, overriding the session project for THIS call only. Must be a project on your account \u2014 fails closed otherwise. Use project_switch to change the session default. PASS THIS WHENEVER YOU KNOW WHICH REPO THE SESSION IS IN: on a multi-project account a connection with no project bound cannot be resolved, and PAPI will stop and ask rather than guess."
|
|
17511
17903
|
}
|
|
17512
17904
|
},
|
|
17513
17905
|
required: ["task_id"]
|
|
@@ -17761,6 +18153,14 @@ async function handleBoardEdit(adapter2, args) {
|
|
|
17761
18153
|
if (!taskId) {
|
|
17762
18154
|
return errorResponse("task_id is required.");
|
|
17763
18155
|
}
|
|
18156
|
+
let target = adapter2;
|
|
18157
|
+
let overrideNote = "";
|
|
18158
|
+
try {
|
|
18159
|
+
({ adapter: target, overrideNote } = await resolvePerCallProjectAdapter(adapter2, args));
|
|
18160
|
+
} catch (err) {
|
|
18161
|
+
if (err instanceof ProjectResolutionError) return errorResponse(err.message);
|
|
18162
|
+
throw err;
|
|
18163
|
+
}
|
|
17764
18164
|
const updates = {};
|
|
17765
18165
|
const changes = [];
|
|
17766
18166
|
for (const field of EDITABLE_FIELDS) {
|
|
@@ -17788,7 +18188,7 @@ async function handleBoardEdit(adapter2, args) {
|
|
|
17788
18188
|
updates.cycle = null;
|
|
17789
18189
|
changes.push("cycle");
|
|
17790
18190
|
} else if (typeof rawCycle === "number" && Number.isInteger(rawCycle) && rawCycle > 0) {
|
|
17791
|
-
const health = await
|
|
18191
|
+
const health = await target.getCycleHealth().catch(() => null);
|
|
17792
18192
|
const activeCycle = health?.totalCycles ?? 0;
|
|
17793
18193
|
if (rawCycle > activeCycle + 1) {
|
|
17794
18194
|
return errorResponse(
|
|
@@ -17807,7 +18207,7 @@ async function handleBoardEdit(adapter2, args) {
|
|
|
17807
18207
|
return errorResponse("No fields to update. Pass at least one field (title, priority, complexity, module, epic, phase, notes, status, maturity, cycle).");
|
|
17808
18208
|
}
|
|
17809
18209
|
try {
|
|
17810
|
-
const task = await
|
|
18210
|
+
const task = await target.getTask(taskId);
|
|
17811
18211
|
if (!task) {
|
|
17812
18212
|
return errorResponse(`Task ${taskId} not found.`);
|
|
17813
18213
|
}
|
|
@@ -17826,7 +18226,7 @@ async function handleBoardEdit(adapter2, args) {
|
|
|
17826
18226
|
const idx = changes.indexOf("notes");
|
|
17827
18227
|
if (idx >= 0) changes.splice(idx, 1);
|
|
17828
18228
|
} else {
|
|
17829
|
-
const health = await
|
|
18229
|
+
const health = await target.getCycleHealth().catch(() => null);
|
|
17830
18230
|
const activeCycle = health?.totalCycles ?? null;
|
|
17831
18231
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
17832
18232
|
const stamp = activeCycle != null ? `[C${activeCycle} ${date}]` : `[${date}]`;
|
|
@@ -17851,7 +18251,7 @@ ${existing}` : entry;
|
|
|
17851
18251
|
}
|
|
17852
18252
|
let autoAssignedCycle = null;
|
|
17853
18253
|
if (updates.status === "In Cycle") {
|
|
17854
|
-
const health = await
|
|
18254
|
+
const health = await target.getCycleHealth().catch(() => null);
|
|
17855
18255
|
const activeCycle = health?.totalCycles ?? null;
|
|
17856
18256
|
if (activeCycle != null && activeCycle > 0) {
|
|
17857
18257
|
updates.cycle = activeCycle;
|
|
@@ -17863,25 +18263,25 @@ ${existing}` : entry;
|
|
|
17863
18263
|
updates.cancelledBy = "user";
|
|
17864
18264
|
}
|
|
17865
18265
|
if (effortCorrection.estimatedEffort || effortCorrection.actualEffort) {
|
|
17866
|
-
if (!
|
|
18266
|
+
if (!target.correctLatestBuildReportEffort) {
|
|
17867
18267
|
return errorResponse("Correcting build-report effort requires a database adapter (pg). The md adapter does not support it.");
|
|
17868
18268
|
}
|
|
17869
|
-
await
|
|
18269
|
+
await target.correctLatestBuildReportEffort(taskId, effortCorrection);
|
|
17870
18270
|
}
|
|
17871
18271
|
if (Object.keys(updates).length > 0) {
|
|
17872
|
-
await
|
|
18272
|
+
await target.updateTask(taskId, updates);
|
|
17873
18273
|
}
|
|
17874
|
-
if ((updates.status === "Done" || updates.status === "Cancelled") &&
|
|
18274
|
+
if ((updates.status === "Done" || updates.status === "Cancelled") && target.updateDogfoodEntryStatus) {
|
|
17875
18275
|
try {
|
|
17876
|
-
const dogfoodLog = await
|
|
18276
|
+
const dogfoodLog = await target.getDogfoodLog?.(50) ?? [];
|
|
17877
18277
|
const linked = dogfoodLog.filter((e) => e.linkedTaskId === taskId || e.linkedTaskId === task.id);
|
|
17878
18278
|
const newStatus = "resolved";
|
|
17879
|
-
await Promise.all(linked.map((e) =>
|
|
18279
|
+
await Promise.all(linked.map((e) => target.updateDogfoodEntryStatus(e.id, newStatus)));
|
|
17880
18280
|
} catch {
|
|
17881
18281
|
}
|
|
17882
18282
|
}
|
|
17883
18283
|
const lines = [
|
|
17884
|
-
`Updated **${taskId}
|
|
18284
|
+
`Updated **${taskId}**${overrideNote} (${updates.title ?? task.title})`,
|
|
17885
18285
|
"",
|
|
17886
18286
|
`**Changes:** ${changes.map((f) => `${f} \u2192 ${String(updates[f])}`).join(", ")}`
|
|
17887
18287
|
];
|
|
@@ -17896,15 +18296,15 @@ ${existing}` : entry;
|
|
|
17896
18296
|
|
|
17897
18297
|
// src/services/setup.ts
|
|
17898
18298
|
import { mkdir, writeFile as writeFile2, readFile as readFile4, readdir, access as access2, stat as stat2, chmod } from "fs/promises";
|
|
17899
|
-
import { join as
|
|
18299
|
+
import { join as join10, basename, extname, dirname as dirname3, relative } from "path";
|
|
17900
18300
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
17901
18301
|
|
|
17902
18302
|
// src/lib/detect-codebase.ts
|
|
17903
18303
|
import { existsSync as existsSync3 } from "fs";
|
|
17904
18304
|
import { readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
17905
|
-
import { join as
|
|
18305
|
+
import { join as join6 } from "path";
|
|
17906
18306
|
function detectCodebaseType(projectRoot) {
|
|
17907
|
-
if (existsSync3(
|
|
18307
|
+
if (existsSync3(join6(projectRoot, ".git"))) {
|
|
17908
18308
|
return "existing_codebase";
|
|
17909
18309
|
}
|
|
17910
18310
|
const manifests = [
|
|
@@ -17918,7 +18318,7 @@ function detectCodebaseType(projectRoot) {
|
|
|
17918
18318
|
"CMakeLists.txt"
|
|
17919
18319
|
];
|
|
17920
18320
|
for (const manifest of manifests) {
|
|
17921
|
-
if (existsSync3(
|
|
18321
|
+
if (existsSync3(join6(projectRoot, manifest))) {
|
|
17922
18322
|
return "existing_codebase";
|
|
17923
18323
|
}
|
|
17924
18324
|
}
|
|
@@ -17926,7 +18326,7 @@ function detectCodebaseType(projectRoot) {
|
|
|
17926
18326
|
const entries = readdirSync2(projectRoot).filter((f) => !f.startsWith("."));
|
|
17927
18327
|
const fileCount = entries.filter((f) => {
|
|
17928
18328
|
try {
|
|
17929
|
-
return statSync3(
|
|
18329
|
+
return statSync3(join6(projectRoot, f)).isFile();
|
|
17930
18330
|
} catch {
|
|
17931
18331
|
return false;
|
|
17932
18332
|
}
|
|
@@ -17939,20 +18339,20 @@ function detectCodebaseType(projectRoot) {
|
|
|
17939
18339
|
|
|
17940
18340
|
// src/lib/agents-bundle.ts
|
|
17941
18341
|
import { readFileSync as readFileSync3, existsSync as existsSync4, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
|
|
17942
|
-
import { dirname, join as
|
|
18342
|
+
import { dirname, join as join7, resolve } from "path";
|
|
17943
18343
|
import { fileURLToPath } from "url";
|
|
17944
|
-
var PROJECT_BUNDLE_REL =
|
|
18344
|
+
var PROJECT_BUNDLE_REL = join7(".agents", "skills", "papi-cycle");
|
|
17945
18345
|
function bundleDestRel(rel) {
|
|
17946
|
-
return rel === "AGENTS.md" ? "AGENTS.md" :
|
|
18346
|
+
return rel === "AGENTS.md" ? "AGENTS.md" : join7(PROJECT_BUNDLE_REL, rel);
|
|
17947
18347
|
}
|
|
17948
18348
|
function claudeSkillDestRel(rel) {
|
|
17949
|
-
return rel === "AGENTS.md" ? void 0 :
|
|
18349
|
+
return rel === "AGENTS.md" ? void 0 : join7(".claude", "skills", rel);
|
|
17950
18350
|
}
|
|
17951
18351
|
function resolveBundleDir() {
|
|
17952
18352
|
let dir = dirname(fileURLToPath(import.meta.url));
|
|
17953
18353
|
for (let i = 0; i < 5; i++) {
|
|
17954
|
-
const candidate =
|
|
17955
|
-
if (existsSync4(
|
|
18354
|
+
const candidate = join7(dir, "skills", "papi-cycle");
|
|
18355
|
+
if (existsSync4(join7(candidate, "AGENTS.md"))) return candidate;
|
|
17956
18356
|
const parent = resolve(dir, "..");
|
|
17957
18357
|
if (parent === dir) break;
|
|
17958
18358
|
dir = parent;
|
|
@@ -17964,8 +18364,8 @@ function readBundleFiles(bundleDir = resolveBundleDir()) {
|
|
|
17964
18364
|
const files = [];
|
|
17965
18365
|
const walk = (abs, rel) => {
|
|
17966
18366
|
for (const entry of readdirSync3(abs, { withFileTypes: true })) {
|
|
17967
|
-
const childAbs =
|
|
17968
|
-
const childRel = rel ?
|
|
18367
|
+
const childAbs = join7(abs, entry.name);
|
|
18368
|
+
const childRel = rel ? join7(rel, entry.name) : entry.name;
|
|
17969
18369
|
if (entry.isDirectory()) walk(childAbs, childRel);
|
|
17970
18370
|
else if (entry.isFile()) files.push({ rel: childRel, content: readFileSync3(childAbs, "utf8") });
|
|
17971
18371
|
}
|
|
@@ -17974,13 +18374,14 @@ function readBundleFiles(bundleDir = resolveBundleDir()) {
|
|
|
17974
18374
|
return files;
|
|
17975
18375
|
}
|
|
17976
18376
|
function planBundleInstall(projectRoot, projectName, opts = {}) {
|
|
18377
|
+
const canProbe = opts.skipExisting === true && projectRoot !== "";
|
|
17977
18378
|
const out = {};
|
|
17978
18379
|
for (const f of readBundleFiles()) {
|
|
17979
18380
|
const content = f.rel === "AGENTS.md" ? f.content.replace(/\{\{project_name\}\}/g, projectName) : f.content;
|
|
17980
18381
|
for (const rel of [bundleDestRel(f.rel), claudeSkillDestRel(f.rel)]) {
|
|
17981
18382
|
if (!rel) continue;
|
|
17982
|
-
const dest =
|
|
17983
|
-
if (
|
|
18383
|
+
const dest = join7(projectRoot, rel);
|
|
18384
|
+
if (canProbe && existsSync4(dest) && statSync4(dest).isFile()) continue;
|
|
17984
18385
|
out[dest] = content;
|
|
17985
18386
|
}
|
|
17986
18387
|
}
|
|
@@ -17989,19 +18390,19 @@ function planBundleInstall(projectRoot, projectName, opts = {}) {
|
|
|
17989
18390
|
|
|
17990
18391
|
// src/lib/design-bundle.ts
|
|
17991
18392
|
import { readFileSync as readFileSync4, existsSync as existsSync5, statSync as statSync5 } from "fs";
|
|
17992
|
-
import { dirname as dirname2, join as
|
|
18393
|
+
import { dirname as dirname2, join as join8, resolve as resolve2 } from "path";
|
|
17993
18394
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
17994
18395
|
var DESIGN_ASSETS = [
|
|
17995
|
-
{ srcRel:
|
|
17996
|
-
{ srcRel:
|
|
17997
|
-
{ srcRel:
|
|
18396
|
+
{ srcRel: join8("agents", "frontend-design-engineer.md"), destRel: join8(".claude", "agents", "frontend-design-engineer.md"), executable: false },
|
|
18397
|
+
{ srcRel: join8("skills", "design-critique", "SKILL.md"), destRel: join8(".claude", "skills", "design-critique", "SKILL.md"), executable: false },
|
|
18398
|
+
{ srcRel: join8("hooks", "frontend-design-guard.sh"), destRel: join8(".claude", "hooks", "frontend-design-guard.sh"), executable: true }
|
|
17998
18399
|
];
|
|
17999
18400
|
var DESIGN_HOOK_COMMAND = ".claude/hooks/frontend-design-guard.sh";
|
|
18000
18401
|
function resolveDesignAssetsDir() {
|
|
18001
18402
|
let dir = dirname2(fileURLToPath2(import.meta.url));
|
|
18002
18403
|
for (let i = 0; i < 5; i++) {
|
|
18003
|
-
const candidate =
|
|
18004
|
-
if (existsSync5(
|
|
18404
|
+
const candidate = join8(dir, "design-assets");
|
|
18405
|
+
if (existsSync5(join8(candidate, "agents", "frontend-design-engineer.md"))) return candidate;
|
|
18005
18406
|
const parent = resolve2(dir, "..");
|
|
18006
18407
|
if (parent === dir) break;
|
|
18007
18408
|
dir = parent;
|
|
@@ -18013,9 +18414,9 @@ function planDesignInstall(projectRoot, opts = {}) {
|
|
|
18013
18414
|
if (!assetsDir) return [];
|
|
18014
18415
|
const out = [];
|
|
18015
18416
|
for (const asset of DESIGN_ASSETS) {
|
|
18016
|
-
const srcAbs =
|
|
18417
|
+
const srcAbs = join8(assetsDir, asset.srcRel);
|
|
18017
18418
|
if (!existsSync5(srcAbs)) continue;
|
|
18018
|
-
const dest = projectRoot ?
|
|
18419
|
+
const dest = projectRoot ? join8(projectRoot, asset.destRel) : asset.destRel;
|
|
18019
18420
|
if (opts.skipExisting && projectRoot && existsSync5(dest) && statSync5(dest).isFile()) continue;
|
|
18020
18421
|
out.push({ dest, content: readFileSync4(srcAbs, "utf8"), executable: asset.executable });
|
|
18021
18422
|
}
|
|
@@ -18024,9 +18425,9 @@ function planDesignInstall(projectRoot, opts = {}) {
|
|
|
18024
18425
|
|
|
18025
18426
|
// src/lib/skill-detection.ts
|
|
18026
18427
|
import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync6 } from "fs";
|
|
18027
|
-
import { join as
|
|
18428
|
+
import { join as join9 } from "path";
|
|
18028
18429
|
function readPackageJson(projectRoot) {
|
|
18029
|
-
const path7 =
|
|
18430
|
+
const path7 = join9(projectRoot, "package.json");
|
|
18030
18431
|
if (!existsSync6(path7)) return null;
|
|
18031
18432
|
try {
|
|
18032
18433
|
const raw = readFileSync5(path7, "utf-8");
|
|
@@ -18050,7 +18451,7 @@ function detectsFrontendStack(projectRoot) {
|
|
|
18050
18451
|
return hasDependencyMatching(allDeps(readPackageJson(projectRoot)), FRONTEND_DEP_PATTERN);
|
|
18051
18452
|
}
|
|
18052
18453
|
function hasGitHubWorkflows(projectRoot) {
|
|
18053
|
-
const dir =
|
|
18454
|
+
const dir = join9(projectRoot, ".github", "workflows");
|
|
18054
18455
|
if (!existsSync6(dir)) return false;
|
|
18055
18456
|
try {
|
|
18056
18457
|
const entries = readdirSync4(dir);
|
|
@@ -18060,7 +18461,7 @@ function hasGitHubWorkflows(projectRoot) {
|
|
|
18060
18461
|
}
|
|
18061
18462
|
}
|
|
18062
18463
|
function envExampleMentionsStaging(projectRoot) {
|
|
18063
|
-
const path7 =
|
|
18464
|
+
const path7 = join9(projectRoot, ".env.example");
|
|
18064
18465
|
if (!existsSync6(path7)) return false;
|
|
18065
18466
|
try {
|
|
18066
18467
|
const raw = readFileSync5(path7, "utf-8");
|
|
@@ -18070,8 +18471,8 @@ function envExampleMentionsStaging(projectRoot) {
|
|
|
18070
18471
|
}
|
|
18071
18472
|
}
|
|
18072
18473
|
function hasVercelConfig(projectRoot) {
|
|
18073
|
-
if (existsSync6(
|
|
18074
|
-
const vercelDir =
|
|
18474
|
+
if (existsSync6(join9(projectRoot, "vercel.json"))) return true;
|
|
18475
|
+
const vercelDir = join9(projectRoot, ".vercel");
|
|
18075
18476
|
if (!existsSync6(vercelDir)) return false;
|
|
18076
18477
|
try {
|
|
18077
18478
|
return statSync6(vercelDir).isDirectory();
|
|
@@ -18318,6 +18719,7 @@ Active Decisions (ADs) track architectural and product choices with confidence l
|
|
|
18318
18719
|
- ADs are for product/architecture choices only, not process preferences.
|
|
18319
18720
|
- When new evidence appears, update AD confidence via \`strategy_change\`.
|
|
18320
18721
|
- Supersede rather than overwrite \u2014 old decisions stay as history.
|
|
18722
|
+
- **A contradiction is not a veto.** If an idea or task cuts against a live AD, that is a decision for the user \u2014 never your reason to drop it. Submit it with \`idea\` + \`conflicts_with_ad: "AD-N"\`: the task is created and parked behind a decision gate, surfaces in \`orient\` as waiting on the owner, and returns to the board on its own once the AD is superseded, modified, or reaffirmed. Shelving the work instead means the AD never gets tested by the evidence that would have challenged it.
|
|
18321
18723
|
- New ADs should include a \`### Reversal Trigger\` section: specify the signal that would invalidate the stance, the action to take (modify/supersede/abandon), and why writing it now prevents sunk-cost drift later.
|
|
18322
18724
|
`;
|
|
18323
18725
|
var CLAUDE_MD_TIER_2 = `
|
|
@@ -18515,6 +18917,13 @@ function formatFilesToWriteSection(collector) {
|
|
|
18515
18917
|
'- `mode: "create"` \u2014 create the file with `content`. If `skip_if_exists: true`, skip when the file already exists.',
|
|
18516
18918
|
'- `mode: "overwrite"` \u2014 replace the file fully with `content`.',
|
|
18517
18919
|
'- `mode: "append"` \u2014 append `content` to the existing file (create it first if missing).',
|
|
18920
|
+
'- `mode: "merge-json"` \u2014 the file is JSON. If it does not exist, create it with `content`.',
|
|
18921
|
+
" If it DOES exist, parse both, merge `content` INTO the existing object, and write the",
|
|
18922
|
+
" result back. Merge rules: objects merge key-by-key (recursing into nested objects);",
|
|
18923
|
+
" arrays UNION, keeping the existing order and appending only values not already present;",
|
|
18924
|
+
" scalars in `content` do not overwrite an existing value. NEVER remove, reorder or rewrite",
|
|
18925
|
+
" anything already in the file \u2014 this mode only ADDS. If the existing file is malformed",
|
|
18926
|
+
" JSON, leave it untouched and tell the user rather than replacing their file.",
|
|
18518
18927
|
""
|
|
18519
18928
|
].join("\n");
|
|
18520
18929
|
}
|
|
@@ -18546,6 +18955,7 @@ function shouldWriteClaudeMd(clientName) {
|
|
|
18546
18955
|
return !clientName || /claude/i.test(clientName);
|
|
18547
18956
|
}
|
|
18548
18957
|
async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
18958
|
+
const preparedFiles = [];
|
|
18549
18959
|
const isPg = config2.adapterType === "pg" || config2.adapterType === "proxy";
|
|
18550
18960
|
const vars = {
|
|
18551
18961
|
project_name: input.projectName,
|
|
@@ -18555,12 +18965,12 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
18555
18965
|
if (!isPg) {
|
|
18556
18966
|
try {
|
|
18557
18967
|
await access2(config2.papiDir);
|
|
18558
|
-
return false;
|
|
18968
|
+
return { createdProject: false, preparedFiles };
|
|
18559
18969
|
} catch {
|
|
18560
18970
|
await mkdir(config2.papiDir, { recursive: true });
|
|
18561
18971
|
for (const [filename, template] of Object.entries(FILE_TEMPLATES)) {
|
|
18562
18972
|
const content = substitute(template, vars);
|
|
18563
|
-
await writeFile2(
|
|
18973
|
+
await writeFile2(join10(config2.papiDir, filename), content, "utf-8");
|
|
18564
18974
|
}
|
|
18565
18975
|
}
|
|
18566
18976
|
} else {
|
|
@@ -18570,7 +18980,7 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
18570
18980
|
const templateContent = substitute(PRODUCT_BRIEF_TEMPLATE, vars);
|
|
18571
18981
|
await adapter2.updateProductBrief(templateContent);
|
|
18572
18982
|
} else {
|
|
18573
|
-
return false;
|
|
18983
|
+
return { createdProject: false, preparedFiles };
|
|
18574
18984
|
}
|
|
18575
18985
|
} catch {
|
|
18576
18986
|
}
|
|
@@ -18578,13 +18988,13 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
18578
18988
|
const useCollector = config2.adapterType === "proxy";
|
|
18579
18989
|
const docsRel = "docs";
|
|
18580
18990
|
const commandsRel = ".claude/commands";
|
|
18581
|
-
const commandsDir = useCollector ? commandsRel :
|
|
18582
|
-
const docsDir = useCollector ? docsRel :
|
|
18991
|
+
const commandsDir = useCollector ? commandsRel : join10(config2.projectRoot, ".claude", "commands");
|
|
18992
|
+
const docsDir = useCollector ? docsRel : join10(config2.projectRoot, "docs");
|
|
18583
18993
|
if (!useCollector) {
|
|
18584
18994
|
await mkdir(commandsDir, { recursive: true });
|
|
18585
18995
|
await mkdir(docsDir, { recursive: true });
|
|
18586
18996
|
}
|
|
18587
|
-
const claudeMdPath = useCollector ? "CLAUDE.md" :
|
|
18997
|
+
const claudeMdPath = useCollector ? "CLAUDE.md" : join10(config2.projectRoot, "CLAUDE.md");
|
|
18588
18998
|
let claudeMdExists = false;
|
|
18589
18999
|
if (!useCollector) {
|
|
18590
19000
|
try {
|
|
@@ -18593,7 +19003,7 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
18593
19003
|
} catch {
|
|
18594
19004
|
}
|
|
18595
19005
|
}
|
|
18596
|
-
const docsIndexPath = useCollector ? `${docsRel}/INDEX.md` :
|
|
19006
|
+
const docsIndexPath = useCollector ? `${docsRel}/INDEX.md` : join10(docsDir, "INDEX.md");
|
|
18597
19007
|
let docsIndexExists = false;
|
|
18598
19008
|
if (!useCollector) {
|
|
18599
19009
|
try {
|
|
@@ -18603,9 +19013,9 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
18603
19013
|
}
|
|
18604
19014
|
}
|
|
18605
19015
|
const scaffoldFiles = {
|
|
18606
|
-
[useCollector ? `${commandsRel}/papi-audit.md` :
|
|
18607
|
-
[useCollector ? `${commandsRel}/test.md` :
|
|
18608
|
-
[useCollector ? `${docsRel}/README.md` :
|
|
19016
|
+
[useCollector ? `${commandsRel}/papi-audit.md` : join10(commandsDir, "papi-audit.md")]: PAPI_AUDIT_COMMAND_TEMPLATE,
|
|
19017
|
+
[useCollector ? `${commandsRel}/test.md` : join10(commandsDir, "test.md")]: TEST_COMMAND_TEMPLATE,
|
|
19018
|
+
[useCollector ? `${docsRel}/README.md` : join10(docsDir, "README.md")]: substitute(DOCS_README_TEMPLATE, vars)
|
|
18609
19019
|
};
|
|
18610
19020
|
if (!docsIndexExists) {
|
|
18611
19021
|
scaffoldFiles[docsIndexPath] = substitute(DOCS_INDEX_TEMPLATE, vars);
|
|
@@ -18632,7 +19042,7 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
18632
19042
|
if (useCollector) {
|
|
18633
19043
|
scaffoldFiles[".cursor/rules/papi.mdc"] = substitute(CURSOR_RULES_TEMPLATE, vars);
|
|
18634
19044
|
} else {
|
|
18635
|
-
const cursorDir =
|
|
19045
|
+
const cursorDir = join10(config2.projectRoot, ".cursor");
|
|
18636
19046
|
let cursorDetected = false;
|
|
18637
19047
|
try {
|
|
18638
19048
|
await access2(cursorDir);
|
|
@@ -18640,8 +19050,8 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
18640
19050
|
} catch {
|
|
18641
19051
|
}
|
|
18642
19052
|
if (cursorDetected) {
|
|
18643
|
-
const cursorRulesDir =
|
|
18644
|
-
const cursorRulesPath =
|
|
19053
|
+
const cursorRulesDir = join10(cursorDir, "rules");
|
|
19054
|
+
const cursorRulesPath = join10(cursorRulesDir, "papi.mdc");
|
|
18645
19055
|
await mkdir(cursorRulesDir, { recursive: true });
|
|
18646
19056
|
try {
|
|
18647
19057
|
await access2(cursorRulesPath);
|
|
@@ -18659,6 +19069,9 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
18659
19069
|
await writeFile2(filepath, content, "utf-8");
|
|
18660
19070
|
}
|
|
18661
19071
|
}
|
|
19072
|
+
for (const p of Object.keys(scaffoldFiles)) {
|
|
19073
|
+
preparedFiles.push(useCollector ? p : relative(config2.projectRoot, p));
|
|
19074
|
+
}
|
|
18662
19075
|
if (!useCollector && detectsFrontendStack(config2.projectRoot)) {
|
|
18663
19076
|
for (const entry of planDesignInstall(config2.projectRoot, { skipExisting: true })) {
|
|
18664
19077
|
await mkdir(dirname3(entry.dest), { recursive: true });
|
|
@@ -18683,20 +19096,38 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
18683
19096
|
}]);
|
|
18684
19097
|
}
|
|
18685
19098
|
if (useCollector) {
|
|
18686
|
-
collector.add(
|
|
18687
|
-
path: ".claude/settings.json",
|
|
18688
|
-
content: JSON.stringify({ permissions: { allow: [PAPI_PERMISSION] } }, null, 2) + "\n",
|
|
18689
|
-
mode: "create",
|
|
18690
|
-
skip_if_exists: true
|
|
18691
|
-
});
|
|
19099
|
+
collector.add(papiSettingsCollectorEntry());
|
|
18692
19100
|
} else {
|
|
18693
19101
|
await ensurePapiPermission(config2.projectRoot);
|
|
18694
19102
|
}
|
|
18695
|
-
return true;
|
|
19103
|
+
return { createdProject: true, preparedFiles };
|
|
19104
|
+
}
|
|
19105
|
+
var PAPI_PERMISSIONS = ["mcp__papi__*", "mcp__plugin_papi_papi__*"];
|
|
19106
|
+
function papiSettingsCollectorEntry() {
|
|
19107
|
+
return {
|
|
19108
|
+
path: ".claude/settings.json",
|
|
19109
|
+
content: JSON.stringify(mergePapiPermissions({}), null, 2) + "\n",
|
|
19110
|
+
mode: "merge-json"
|
|
19111
|
+
};
|
|
19112
|
+
}
|
|
19113
|
+
function mergePapiPermissions(settings) {
|
|
19114
|
+
if (!settings.permissions || typeof settings.permissions !== "object") {
|
|
19115
|
+
settings.permissions = {};
|
|
19116
|
+
}
|
|
19117
|
+
const perms = settings.permissions;
|
|
19118
|
+
if (!Array.isArray(perms.allow)) {
|
|
19119
|
+
perms.allow = [];
|
|
19120
|
+
}
|
|
19121
|
+
const allow = perms.allow;
|
|
19122
|
+
for (const permission of PAPI_PERMISSIONS) {
|
|
19123
|
+
if (!allow.includes(permission)) {
|
|
19124
|
+
allow.push(permission);
|
|
19125
|
+
}
|
|
19126
|
+
}
|
|
19127
|
+
return settings;
|
|
18696
19128
|
}
|
|
18697
|
-
var PAPI_PERMISSION = "mcp__papi__*";
|
|
18698
19129
|
async function ensurePapiPermission(projectRoot) {
|
|
18699
|
-
const settingsPath =
|
|
19130
|
+
const settingsPath = join10(projectRoot, ".claude", "settings.json");
|
|
18700
19131
|
try {
|
|
18701
19132
|
let settings = {};
|
|
18702
19133
|
try {
|
|
@@ -18704,24 +19135,14 @@ async function ensurePapiPermission(projectRoot) {
|
|
|
18704
19135
|
settings = JSON.parse(existing);
|
|
18705
19136
|
} catch {
|
|
18706
19137
|
}
|
|
18707
|
-
|
|
18708
|
-
|
|
18709
|
-
}
|
|
18710
|
-
const perms = settings.permissions;
|
|
18711
|
-
if (!Array.isArray(perms.allow)) {
|
|
18712
|
-
perms.allow = [];
|
|
18713
|
-
}
|
|
18714
|
-
const allow = perms.allow;
|
|
18715
|
-
if (!allow.includes(PAPI_PERMISSION)) {
|
|
18716
|
-
allow.push(PAPI_PERMISSION);
|
|
18717
|
-
}
|
|
18718
|
-
await mkdir(join9(projectRoot, ".claude"), { recursive: true });
|
|
19138
|
+
mergePapiPermissions(settings);
|
|
19139
|
+
await mkdir(join10(projectRoot, ".claude"), { recursive: true });
|
|
18719
19140
|
await writeFile2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
18720
19141
|
} catch {
|
|
18721
19142
|
}
|
|
18722
19143
|
}
|
|
18723
19144
|
async function ensureDesignHookRegistered(projectRoot) {
|
|
18724
|
-
const settingsPath =
|
|
19145
|
+
const settingsPath = join10(projectRoot, ".claude", "settings.json");
|
|
18725
19146
|
try {
|
|
18726
19147
|
let settings = {};
|
|
18727
19148
|
try {
|
|
@@ -18751,7 +19172,7 @@ async function ensureDesignHookRegistered(projectRoot) {
|
|
|
18751
19172
|
chain.push({ type: "command", command: DESIGN_HOOK_COMMAND });
|
|
18752
19173
|
}
|
|
18753
19174
|
}
|
|
18754
|
-
await mkdir(
|
|
19175
|
+
await mkdir(join10(projectRoot, ".claude"), { recursive: true });
|
|
18755
19176
|
await writeFile2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
18756
19177
|
} catch {
|
|
18757
19178
|
}
|
|
@@ -18857,7 +19278,7 @@ ${conventionsText.trim()}
|
|
|
18857
19278
|
);
|
|
18858
19279
|
} else {
|
|
18859
19280
|
try {
|
|
18860
|
-
const claudeMdPath =
|
|
19281
|
+
const claudeMdPath = join10(config2.projectRoot, "CLAUDE.md");
|
|
18861
19282
|
const existing = await readFile4(claudeMdPath, "utf-8");
|
|
18862
19283
|
if (existing.includes(CONVENTIONS_SENTINEL) || existing.includes(CONVENTIONS_HEADING)) {
|
|
18863
19284
|
warnings.push(
|
|
@@ -18944,13 +19365,13 @@ async function scanCodebase(projectRoot) {
|
|
|
18944
19365
|
}
|
|
18945
19366
|
let packageJson;
|
|
18946
19367
|
try {
|
|
18947
|
-
const content = await readFile4(
|
|
19368
|
+
const content = await readFile4(join10(projectRoot, "package.json"), "utf-8");
|
|
18948
19369
|
packageJson = JSON.parse(content);
|
|
18949
19370
|
} catch {
|
|
18950
19371
|
}
|
|
18951
19372
|
let readme;
|
|
18952
19373
|
for (const name of ["README.md", "readme.md", "README.txt", "README"]) {
|
|
18953
|
-
const content = await safeReadFile(
|
|
19374
|
+
const content = await safeReadFile(join10(projectRoot, name), 5e3);
|
|
18954
19375
|
if (content) {
|
|
18955
19376
|
readme = content;
|
|
18956
19377
|
break;
|
|
@@ -18960,7 +19381,7 @@ async function scanCodebase(projectRoot) {
|
|
|
18960
19381
|
let totalFiles = topLevelFiles.length;
|
|
18961
19382
|
for (const dir of topLevelDirs) {
|
|
18962
19383
|
try {
|
|
18963
|
-
const entries = await readdir(
|
|
19384
|
+
const entries = await readdir(join10(projectRoot, dir), { withFileTypes: true });
|
|
18964
19385
|
const files = entries.filter((e) => e.isFile());
|
|
18965
19386
|
const extensions = [...new Set(files.map((f) => extname(f.name).toLowerCase()).filter(Boolean))];
|
|
18966
19387
|
totalFiles += files.length;
|
|
@@ -19043,7 +19464,7 @@ function formatCodebaseSummary(scan, sourceContents) {
|
|
|
19043
19464
|
}
|
|
19044
19465
|
async function prepareSetup(adapter2, config2, input) {
|
|
19045
19466
|
const prepareCollector = new FileWriteCollector();
|
|
19046
|
-
const createdProject = await scaffoldPapiDir(adapter2, config2, input, prepareCollector);
|
|
19467
|
+
const { createdProject, preparedFiles } = await scaffoldPapiDir(adapter2, config2, input, prepareCollector);
|
|
19047
19468
|
let existingBrief;
|
|
19048
19469
|
try {
|
|
19049
19470
|
existingBrief = await adapter2.readProductBrief();
|
|
@@ -19212,12 +19633,13 @@ async function prepareSetup(adapter2, config2, input) {
|
|
|
19212
19633
|
preScanInstruction,
|
|
19213
19634
|
newProjectInstruction,
|
|
19214
19635
|
warnings: warnings.length > 0 ? warnings : void 0,
|
|
19215
|
-
filesToWrite: prepareCollector.isEmpty() ? void 0 : prepareCollector
|
|
19636
|
+
filesToWrite: prepareCollector.isEmpty() ? void 0 : prepareCollector,
|
|
19637
|
+
preparedFiles
|
|
19216
19638
|
};
|
|
19217
19639
|
}
|
|
19218
19640
|
async function applySetup(adapter2, config2, input, briefText, adSeedText, conventionsText, initialTasksText, northStarText) {
|
|
19219
19641
|
const collector = new FileWriteCollector();
|
|
19220
|
-
const createdProject = await scaffoldPapiDir(adapter2, config2, input, collector);
|
|
19642
|
+
const { createdProject, preparedFiles } = await scaffoldPapiDir(adapter2, config2, input, collector);
|
|
19221
19643
|
let effectiveBriefText = briefText;
|
|
19222
19644
|
let briefRegenerated = false;
|
|
19223
19645
|
if (!effectiveBriefText.trim()) {
|
|
@@ -19346,7 +19768,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
|
|
|
19346
19768
|
collector.add({ path: "CLAUDE.md", content: dogfoodSection, mode: "append" });
|
|
19347
19769
|
} else {
|
|
19348
19770
|
try {
|
|
19349
|
-
const claudeMdPath =
|
|
19771
|
+
const claudeMdPath = join10(config2.projectRoot, "CLAUDE.md");
|
|
19350
19772
|
const existing = await readFile4(claudeMdPath, "utf-8");
|
|
19351
19773
|
if (!existing.includes("Dogfood Logging")) {
|
|
19352
19774
|
await writeFile2(claudeMdPath, existing + dogfoodSection, "utf-8");
|
|
@@ -19391,7 +19813,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
|
|
|
19391
19813
|
cursorScaffolded = true;
|
|
19392
19814
|
} else {
|
|
19393
19815
|
try {
|
|
19394
|
-
await access2(
|
|
19816
|
+
await access2(join10(config2.projectRoot, ".cursor", "rules", "papi.mdc"));
|
|
19395
19817
|
cursorScaffolded = true;
|
|
19396
19818
|
} catch {
|
|
19397
19819
|
}
|
|
@@ -19407,16 +19829,17 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
|
|
|
19407
19829
|
cursorScaffolded,
|
|
19408
19830
|
gitignoreNote,
|
|
19409
19831
|
warnings: warnings.length > 0 ? warnings : void 0,
|
|
19410
|
-
filesToWrite: collector.isEmpty() ? void 0 : collector
|
|
19832
|
+
filesToWrite: collector.isEmpty() ? void 0 : collector,
|
|
19833
|
+
preparedFiles
|
|
19411
19834
|
};
|
|
19412
19835
|
}
|
|
19413
19836
|
async function ensureMcpJsonGitignored(projectRoot) {
|
|
19414
19837
|
try {
|
|
19415
|
-
await access2(
|
|
19838
|
+
await access2(join10(projectRoot, ".git"));
|
|
19416
19839
|
} catch {
|
|
19417
19840
|
return void 0;
|
|
19418
19841
|
}
|
|
19419
|
-
const gitignorePath =
|
|
19842
|
+
const gitignorePath = join10(projectRoot, ".gitignore");
|
|
19420
19843
|
let existing = "";
|
|
19421
19844
|
try {
|
|
19422
19845
|
existing = await readFile4(gitignorePath, "utf-8");
|
|
@@ -19565,8 +19988,14 @@ function extractInput(args) {
|
|
|
19565
19988
|
codebaseScan: args.codebase_scan && typeof args.codebase_scan === "object" ? args.codebase_scan : void 0
|
|
19566
19989
|
};
|
|
19567
19990
|
}
|
|
19991
|
+
function harnessFilesLabel(prepared, writesClaudeMd) {
|
|
19992
|
+
const HARNESS_FILES = ["AGENTS.md", "CLAUDE.md"];
|
|
19993
|
+
const emitted = HARNESS_FILES.filter((f) => prepared?.includes(f));
|
|
19994
|
+
if (emitted.length > 0) return emitted.join(", ");
|
|
19995
|
+
return writesClaudeMd ? "AGENTS.md, CLAUDE.md" : "AGENTS.md";
|
|
19996
|
+
}
|
|
19568
19997
|
function formatSuccessResponse(result, constraints, writesClaudeMd = true) {
|
|
19569
|
-
const harnessFiles =
|
|
19998
|
+
const harnessFiles = harnessFilesLabel(result.preparedFiles, writesClaudeMd);
|
|
19570
19999
|
const prefix = result.createdProject ? `PAPI project "${result.projectName}" initialised and ` : "";
|
|
19571
20000
|
const briefRegenNote = result.briefRegenerated ? `
|
|
19572
20001
|
|
|
@@ -19982,8 +20411,8 @@ function buildPapiMetaFramingDirective(caps, inner) {
|
|
|
19982
20411
|
|
|
19983
20412
|
// src/services/build.ts
|
|
19984
20413
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
19985
|
-
import { readdirSync as readdirSync5, existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as
|
|
19986
|
-
import { join as
|
|
20414
|
+
import { readdirSync as readdirSync5, existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync5, unlinkSync as unlinkSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
20415
|
+
import { join as join13 } from "path";
|
|
19987
20416
|
import { splitFindings, findingKey } from "@papi-ai/shared";
|
|
19988
20417
|
|
|
19989
20418
|
// src/lib/db-only-notices.ts
|
|
@@ -20150,7 +20579,7 @@ init_git();
|
|
|
20150
20579
|
// src/services/release.ts
|
|
20151
20580
|
init_telemetry();
|
|
20152
20581
|
import { writeFile as writeFile3, readFile as readFile5 } from "fs/promises";
|
|
20153
|
-
import { join as
|
|
20582
|
+
import { join as join11 } from "path";
|
|
20154
20583
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
20155
20584
|
import { isSensitiveChangelogLine } from "@papi-ai/shared";
|
|
20156
20585
|
init_git();
|
|
@@ -20624,7 +21053,7 @@ async function createRelease(config2, branch, version, adapter2, cycleNum, optio
|
|
|
20624
21053
|
}
|
|
20625
21054
|
}
|
|
20626
21055
|
const latestTag = getLatestTag(config2.projectRoot);
|
|
20627
|
-
const changelogPath =
|
|
21056
|
+
const changelogPath = join11(config2.projectRoot, "CHANGELOG.md");
|
|
20628
21057
|
if (!latestTag) {
|
|
20629
21058
|
const initialContent = INITIAL_RELEASE_NOTES.replace("v0.1.0-alpha", version);
|
|
20630
21059
|
if (config2.adapterType === "proxy") {
|
|
@@ -20780,33 +21209,12 @@ import { execFile } from "child_process";
|
|
|
20780
21209
|
import { promisify } from "util";
|
|
20781
21210
|
var execFileAsync = promisify(execFile);
|
|
20782
21211
|
var REGISTRY_TIMEOUT_MS = 12e4;
|
|
20783
|
-
|
|
20784
|
-
|
|
20785
|
-
|
|
20786
|
-
|
|
20787
|
-
|
|
20788
|
-
|
|
20789
|
-
message: `Listing not updated \u2014 set MCP_REGISTRY_GITHUB_TOKEN to automate, or run manually: npx mcp-publisher publish`
|
|
20790
|
-
};
|
|
20791
|
-
}
|
|
20792
|
-
try {
|
|
20793
|
-
await execFileAsync("npx", ["mcp-publisher", "publish", "--yes"], {
|
|
20794
|
-
env: { ...process.env, GITHUB_TOKEN: token },
|
|
20795
|
-
timeout: REGISTRY_TIMEOUT_MS
|
|
20796
|
-
});
|
|
20797
|
-
return {
|
|
20798
|
-
registry: "Official MCP Registry",
|
|
20799
|
-
updated: true,
|
|
20800
|
-
message: `Listing updated to ${version}.`
|
|
20801
|
-
};
|
|
20802
|
-
} catch (err) {
|
|
20803
|
-
const detail = err instanceof Error ? err.message.slice(0, 300) : String(err).slice(0, 300);
|
|
20804
|
-
return {
|
|
20805
|
-
registry: "Official MCP Registry",
|
|
20806
|
-
updated: false,
|
|
20807
|
-
message: `Update failed \u2014 run manually: npx mcp-publisher publish. (${detail})`
|
|
20808
|
-
};
|
|
20809
|
-
}
|
|
21212
|
+
function officialMCPRegistryGuidance(version) {
|
|
21213
|
+
return {
|
|
21214
|
+
registry: "Official MCP Registry",
|
|
21215
|
+
updated: false,
|
|
21216
|
+
message: `Listing not updated (manual step). From a checkout of getpapi/papi: \`mcp-publisher login github\` (once), then \`mcp-publisher publish\` \u2014 bump server.json to ${version} first. Requires the Go CLI (brew install mcp-publisher), NOT the npm package of the same name.`
|
|
21217
|
+
};
|
|
20810
21218
|
}
|
|
20811
21219
|
async function updateSmithery(version) {
|
|
20812
21220
|
const key = process.env["SMITHERY_API_KEY"]?.trim();
|
|
@@ -20837,11 +21245,8 @@ async function updateSmithery(version) {
|
|
|
20837
21245
|
}
|
|
20838
21246
|
}
|
|
20839
21247
|
async function updateRegistryListings(version) {
|
|
20840
|
-
const [
|
|
20841
|
-
|
|
20842
|
-
updateSmithery(version)
|
|
20843
|
-
]);
|
|
20844
|
-
return [official, smithery];
|
|
21248
|
+
const [smithery] = await Promise.all([updateSmithery(version)]);
|
|
21249
|
+
return [officialMCPRegistryGuidance(version), smithery];
|
|
20845
21250
|
}
|
|
20846
21251
|
async function updateCursorDirectory(version) {
|
|
20847
21252
|
const token = process.env["CURSOR_DIRECTORY_GITHUB_TOKEN"]?.trim();
|
|
@@ -21459,21 +21864,21 @@ function detectWorktreeCollision(input) {
|
|
|
21459
21864
|
init_git();
|
|
21460
21865
|
|
|
21461
21866
|
// src/lib/build-checkpoint.ts
|
|
21462
|
-
import { createHash as
|
|
21463
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as
|
|
21464
|
-
import { join as
|
|
21867
|
+
import { createHash as createHash5 } from "crypto";
|
|
21868
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
21869
|
+
import { join as join12 } from "path";
|
|
21465
21870
|
var BUILD_CHECKPOINT_VERSION = 1;
|
|
21466
21871
|
function cwdHash(cwd) {
|
|
21467
|
-
return
|
|
21872
|
+
return createHash5("sha256").update(realpathOrSelf(cwd)).digest("hex").slice(0, 12);
|
|
21468
21873
|
}
|
|
21469
21874
|
function safeTaskId(taskId) {
|
|
21470
21875
|
return taskId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
21471
21876
|
}
|
|
21472
21877
|
function checkpointDir(cwd) {
|
|
21473
|
-
return
|
|
21878
|
+
return join12(cwd, ".papi", "state");
|
|
21474
21879
|
}
|
|
21475
21880
|
function checkpointPath(cwd, taskId) {
|
|
21476
|
-
return
|
|
21881
|
+
return join12(checkpointDir(cwd), `build-${safeTaskId(taskId)}.${cwdHash(cwd)}.json`);
|
|
21477
21882
|
}
|
|
21478
21883
|
function writeBuildCheckpoint(input) {
|
|
21479
21884
|
try {
|
|
@@ -21491,7 +21896,7 @@ function writeBuildCheckpoint(input) {
|
|
|
21491
21896
|
cwd: realpathOrSelf(input.cwd),
|
|
21492
21897
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
21493
21898
|
};
|
|
21494
|
-
|
|
21899
|
+
writeFileSync4(checkpointPath(input.cwd, input.taskId), JSON.stringify(checkpoint, null, 2) + "\n", "utf-8");
|
|
21495
21900
|
} catch {
|
|
21496
21901
|
}
|
|
21497
21902
|
}
|
|
@@ -21750,7 +22155,7 @@ function pathBasename(p) {
|
|
|
21750
22155
|
const parts = p.replace(/\\/g, "/").replace(/\/+$/, "").split("/");
|
|
21751
22156
|
return parts[parts.length - 1] ?? p;
|
|
21752
22157
|
}
|
|
21753
|
-
function
|
|
22158
|
+
function matchedPredictedEntry(changedPath, predicted) {
|
|
21754
22159
|
const changed = changedPath.replace(/\\/g, "/");
|
|
21755
22160
|
const changedLower = changed.toLowerCase();
|
|
21756
22161
|
const changedBase = pathBasename(changedPath);
|
|
@@ -21758,17 +22163,20 @@ function isPathInPredictedScope(changedPath, predicted) {
|
|
|
21758
22163
|
const entry = raw.replace(/\\/g, "/").replace(/\/+$/, "").trim();
|
|
21759
22164
|
if (!entry) continue;
|
|
21760
22165
|
if (entry.includes("*")) {
|
|
21761
|
-
if (globToRegExp(entry).test(changed)) return
|
|
21762
|
-
if (!entry.includes("/") && globToRegExp(entry).test(changedBase)) return
|
|
22166
|
+
if (globToRegExp(entry).test(changed)) return entry;
|
|
22167
|
+
if (!entry.includes("/") && globToRegExp(entry).test(changedBase)) return entry;
|
|
21763
22168
|
continue;
|
|
21764
22169
|
}
|
|
21765
|
-
if (pathBasename(entry) === changedBase) return
|
|
22170
|
+
if (pathBasename(entry) === changedBase) return entry;
|
|
21766
22171
|
const entryLower = entry.toLowerCase();
|
|
21767
22172
|
if (changedLower === entryLower || changedLower.startsWith(`${entryLower}/`)) {
|
|
21768
|
-
return
|
|
22173
|
+
return entry;
|
|
21769
22174
|
}
|
|
21770
22175
|
}
|
|
21771
|
-
return
|
|
22176
|
+
return null;
|
|
22177
|
+
}
|
|
22178
|
+
function isPathInPredictedScope(changedPath, predicted) {
|
|
22179
|
+
return matchedPredictedEntry(changedPath, predicted) !== null;
|
|
21772
22180
|
}
|
|
21773
22181
|
function autoCommit(config2, taskId, taskTitle, predictedFiles) {
|
|
21774
22182
|
const cwd = config2.projectRoot;
|
|
@@ -21794,6 +22202,17 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
|
|
|
21794
22202
|
if (staged.length > 0) {
|
|
21795
22203
|
return safeRun(() => commitStagedOnly(cwd, message)) + ` (selective staging respected: ${staged.length} file(s)).`;
|
|
21796
22204
|
}
|
|
22205
|
+
const checkpoint = readBuildCheckpointIfLocal({ cwd, taskId });
|
|
22206
|
+
const headSha = getHeadCommitSha(cwd);
|
|
22207
|
+
if (checkpoint?.lastCommitSha && headSha && checkpoint.lastCommitSha !== headSha) {
|
|
22208
|
+
const leftover = getModifiedFiles(cwd);
|
|
22209
|
+
if (leftover.length === 0) {
|
|
22210
|
+
return "Auto-commit: skipped (builder already committed; working tree clean).";
|
|
22211
|
+
}
|
|
22212
|
+
const sample = leftover.slice(0, 10).join(", ");
|
|
22213
|
+
const more = leftover.length > 10 ? ` (+${leftover.length - 10} more)` : "";
|
|
22214
|
+
return `Auto-commit: skipped \u2014 you already committed during this build, and ${leftover.length} file(s) are still modified. They were NOT committed, because at this point PAPI cannot tell your deliberately-excluded work from a concurrent session's files (task-3054). Left uncommitted: ${sample}${more}. If any belong to ${taskId}, \`git add\` them and re-run build_execute complete.`;
|
|
22215
|
+
}
|
|
21797
22216
|
const modified = getModifiedFiles(cwd);
|
|
21798
22217
|
if (modified.length === 0) {
|
|
21799
22218
|
return "Auto-commit: skipped (no working-tree changes).";
|
|
@@ -21807,7 +22226,9 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
|
|
|
21807
22226
|
const more = outOfScope.length > 10 ? ` (+${outOfScope.length - 10} more)` : "";
|
|
21808
22227
|
return `${commitResult} (staged all ${modified.length} changed file(s)). \u2139\uFE0F Scope drift: ${outOfScope.length} committed file(s) were outside the handoff's FILES LIKELY TOUCHED \u2014 handoff under-predicted: ${sample}${more}.`;
|
|
21809
22228
|
}
|
|
21810
|
-
|
|
22229
|
+
const matches = modified.slice(0, 5).map((p) => `${p} \u2190 ${matchedPredictedEntry(p, cleanedPredicted) ?? "?"}`).join(", ");
|
|
22230
|
+
const extra = modified.length > 5 ? ` (+${modified.length - 5} more)` : "";
|
|
22231
|
+
return `${commitResult} (staged all ${modified.length} changed file(s), each matched to FILES LIKELY TOUCHED: ${matches}${extra}).`;
|
|
21811
22232
|
}
|
|
21812
22233
|
return `${commitResult} (staged all ${modified.length} changed file(s)).`;
|
|
21813
22234
|
}
|
|
@@ -22369,15 +22790,15 @@ function writeActiveTaskScope(projectRoot, taskId, filesLikelyTouched, adapterTy
|
|
|
22369
22790
|
collector.add({ path: ".papi/active-task-scope.txt", content, mode: "overwrite" });
|
|
22370
22791
|
return;
|
|
22371
22792
|
}
|
|
22372
|
-
const papiDir =
|
|
22793
|
+
const papiDir = join13(projectRoot, ".papi");
|
|
22373
22794
|
if (!existsSync8(papiDir)) {
|
|
22374
22795
|
mkdirSync3(papiDir, { recursive: true });
|
|
22375
22796
|
}
|
|
22376
|
-
const scopePath =
|
|
22377
|
-
|
|
22797
|
+
const scopePath = join13(papiDir, "active-task-scope.txt");
|
|
22798
|
+
writeFileSync5(scopePath, content, "utf-8");
|
|
22378
22799
|
}
|
|
22379
22800
|
function clearActiveTaskScope(projectRoot) {
|
|
22380
|
-
const scopePath =
|
|
22801
|
+
const scopePath = join13(projectRoot, ".papi", "active-task-scope.txt");
|
|
22381
22802
|
if (existsSync8(scopePath)) {
|
|
22382
22803
|
unlinkSync3(scopePath);
|
|
22383
22804
|
}
|
|
@@ -22837,29 +23258,39 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
22837
23258
|
let docWarning;
|
|
22838
23259
|
try {
|
|
22839
23260
|
if (adapter2.searchDocs && hasLocalWorkspace() && await ownsLocalWorkspace(adapter2, config2.projectRoot)) {
|
|
22840
|
-
const docsDir =
|
|
23261
|
+
const docsDir = join13(config2.projectRoot, "docs");
|
|
22841
23262
|
if (existsSync8(docsDir)) {
|
|
22842
23263
|
const scanDir = (dir, depth = 0) => {
|
|
22843
23264
|
if (depth > 8) return [];
|
|
22844
23265
|
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
22845
23266
|
const files = [];
|
|
22846
23267
|
for (const e of entries) {
|
|
22847
|
-
const full =
|
|
23268
|
+
const full = join13(dir, e.name);
|
|
22848
23269
|
if (e.isDirectory() && !e.isSymbolicLink()) files.push(...scanDir(full, depth + 1));
|
|
22849
23270
|
else if (e.name.endsWith(".md")) files.push(full.replace(config2.projectRoot + "/", ""));
|
|
22850
23271
|
}
|
|
22851
23272
|
return files;
|
|
22852
23273
|
};
|
|
22853
23274
|
const mdFiles = scanDir(docsDir);
|
|
23275
|
+
let branchDocs;
|
|
23276
|
+
try {
|
|
23277
|
+
branchDocs = new Set(
|
|
23278
|
+
getFilesChangedFromBase(config2.projectRoot, "origin/main").filter((f) => f.startsWith("docs/") && f.endsWith(".md"))
|
|
23279
|
+
);
|
|
23280
|
+
} catch {
|
|
23281
|
+
branchDocs = /* @__PURE__ */ new Set();
|
|
23282
|
+
}
|
|
22854
23283
|
const registered = await adapter2.searchDocs({ status: "all", limit: 500 });
|
|
22855
23284
|
const registeredPaths = new Set(registered.map((d) => d.path));
|
|
22856
|
-
const unregistered = mdFiles.filter(
|
|
23285
|
+
const unregistered = mdFiles.filter(
|
|
23286
|
+
(f) => !registeredPaths.has(f) && branchDocs.has(f)
|
|
23287
|
+
);
|
|
22857
23288
|
if (unregistered.length > 0 && adapter2.registerDoc) {
|
|
22858
23289
|
const autoRegistered = [];
|
|
22859
23290
|
const failed = [];
|
|
22860
23291
|
for (const docPath of unregistered) {
|
|
22861
23292
|
try {
|
|
22862
|
-
const meta = extractDocMeta(
|
|
23293
|
+
const meta = extractDocMeta(join13(config2.projectRoot, docPath), docPath, cycleNumber);
|
|
22863
23294
|
await adapter2.registerDoc({
|
|
22864
23295
|
title: meta.title,
|
|
22865
23296
|
type: meta.type,
|
|
@@ -23029,15 +23460,15 @@ ${instructions}`;
|
|
|
23029
23460
|
}
|
|
23030
23461
|
|
|
23031
23462
|
// src/tools/doc-registry.ts
|
|
23032
|
-
import { readdirSync as readdirSync6, existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as
|
|
23033
|
-
import { join as
|
|
23463
|
+
import { readdirSync as readdirSync6, existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync4 } from "fs";
|
|
23464
|
+
import { join as join14, relative as relative2, isAbsolute as isAbsolute2, dirname as dirname4, resolve as resolve3, sep } from "path";
|
|
23034
23465
|
import { homedir as homedir3 } from "os";
|
|
23035
23466
|
import { randomUUID as randomUUID12 } from "crypto";
|
|
23036
23467
|
import { docDeletionBlockMessage } from "@papi-ai/shared";
|
|
23037
23468
|
init_git();
|
|
23038
23469
|
|
|
23039
23470
|
// src/services/entitlements.ts
|
|
23040
|
-
import { evaluateContributorGate } from "@papi-ai/shared";
|
|
23471
|
+
import { evaluateContributorGate, isSelfHostedDeployment } from "@papi-ai/shared";
|
|
23041
23472
|
var FREE_PROJECT_CAP = 3;
|
|
23042
23473
|
var DOC_STORAGE_CEILING_BY_TIER = {
|
|
23043
23474
|
free: { bytes: 25 * 1024 * 1024, docs: 200 },
|
|
@@ -23060,6 +23491,7 @@ function isPaidTier(tier) {
|
|
|
23060
23491
|
return tier !== null && PAID_TIERS.has(tier);
|
|
23061
23492
|
}
|
|
23062
23493
|
async function enforceProjectCap(adapter2, target) {
|
|
23494
|
+
if (isSelfHostedDeployment(process.env.PAPI_SELF_HOST)) return null;
|
|
23063
23495
|
const tier = await resolveTier(adapter2);
|
|
23064
23496
|
if (tier === null || isPaidTier(tier)) return null;
|
|
23065
23497
|
if (typeof adapter2.listUserProjects !== "function") return null;
|
|
@@ -23170,7 +23602,11 @@ var docRegisterTool = {
|
|
|
23170
23602
|
},
|
|
23171
23603
|
description: "Actionable findings from the document."
|
|
23172
23604
|
},
|
|
23173
|
-
superseded_by_path: { type: "string", description: "Path of the doc that supersedes this one (sets status to superseded)." }
|
|
23605
|
+
superseded_by_path: { type: "string", description: "Path of the doc that supersedes this one (sets status to superseded)." },
|
|
23606
|
+
project: {
|
|
23607
|
+
type: "string",
|
|
23608
|
+
description: "Project id (UUID) or slug to register this doc under, overriding the session project for THIS call only. Must be a project on your account \u2014 fails closed otherwise. Use project_switch to change the session default. PASS THIS WHENEVER YOU KNOW WHICH REPO THE SESSION IS IN: on a multi-project account a connection with no project bound cannot be resolved, and PAPI will stop and ask rather than guess."
|
|
23609
|
+
}
|
|
23174
23610
|
},
|
|
23175
23611
|
required: ["path", "title", "type", "summary", "cycle"]
|
|
23176
23612
|
}
|
|
@@ -23265,7 +23701,7 @@ function ensureDocDurable(path7, projectRoot) {
|
|
|
23265
23701
|
\u26A0\uFE0F **Registered, but NOT durable.** ${reason}
|
|
23266
23702
|
The registry stores metadata and a summary \u2014 not the body. This doc has one copy, on disk, and a branch switch or stash can take it.
|
|
23267
23703
|
**Fix:** ${fix}`;
|
|
23268
|
-
if (!existsSync9(
|
|
23704
|
+
if (!existsSync9(join14(projectRoot, path7))) {
|
|
23269
23705
|
return warn(
|
|
23270
23706
|
`No file exists at \`${path7}\`.`,
|
|
23271
23707
|
`write the doc body to that path, then re-run doc_register.`
|
|
@@ -23327,6 +23763,22 @@ async function handleDocRegister(adapter2, args, config2) {
|
|
|
23327
23763
|
continueHint
|
|
23328
23764
|
);
|
|
23329
23765
|
}
|
|
23766
|
+
let target = adapter2;
|
|
23767
|
+
let overrideNote = "";
|
|
23768
|
+
try {
|
|
23769
|
+
({ adapter: target, overrideNote } = await resolvePerCallProjectAdapter(adapter2, args));
|
|
23770
|
+
} catch (err) {
|
|
23771
|
+
if (err instanceof ProjectResolutionError) return errorResponse(err.message);
|
|
23772
|
+
throw err;
|
|
23773
|
+
}
|
|
23774
|
+
if (!target.registerDoc) {
|
|
23775
|
+
return docRegisterSoftFail(
|
|
23776
|
+
adapterType,
|
|
23777
|
+
"adapter-capability-check",
|
|
23778
|
+
"Doc registry not available on the resolved project adapter \u2014 requires the pg/proxy adapter.",
|
|
23779
|
+
"Continue without it \u2014 nothing is blocked."
|
|
23780
|
+
);
|
|
23781
|
+
}
|
|
23330
23782
|
if (!path7.toLowerCase().endsWith(".md")) {
|
|
23331
23783
|
return docRegisterSoftFail(
|
|
23332
23784
|
adapterType,
|
|
@@ -23338,13 +23790,13 @@ async function handleDocRegister(adapter2, args, config2) {
|
|
|
23338
23790
|
try {
|
|
23339
23791
|
let supersededBy;
|
|
23340
23792
|
if (supersededByPath) {
|
|
23341
|
-
const existing = await
|
|
23793
|
+
const existing = await target.getDoc?.(supersededByPath);
|
|
23342
23794
|
if (existing) {
|
|
23343
23795
|
supersededBy = existing.id;
|
|
23344
|
-
await
|
|
23796
|
+
await target.updateDocStatus?.(existing.id, "superseded", void 0);
|
|
23345
23797
|
}
|
|
23346
23798
|
}
|
|
23347
|
-
const entry = await
|
|
23799
|
+
const entry = await target.registerDoc({
|
|
23348
23800
|
title,
|
|
23349
23801
|
type,
|
|
23350
23802
|
path: path7,
|
|
@@ -23363,23 +23815,23 @@ async function handleDocRegister(adapter2, args, config2) {
|
|
|
23363
23815
|
let body = supplied;
|
|
23364
23816
|
if (body === void 0 && hasLocalWorkspace()) {
|
|
23365
23817
|
try {
|
|
23366
|
-
const abs = isAbsolute2(entry.path) ? entry.path :
|
|
23818
|
+
const abs = isAbsolute2(entry.path) ? entry.path : join14(config2?.projectRoot ?? process.cwd(), entry.path);
|
|
23367
23819
|
if (existsSync9(abs)) body = readFileSync8(abs, "utf8");
|
|
23368
23820
|
} catch {
|
|
23369
23821
|
}
|
|
23370
23822
|
}
|
|
23371
23823
|
if (body === void 0) {
|
|
23372
23824
|
bodyNote = hasLocalWorkspace() ? "\n\n_Body not stored \u2014 the file could not be read from disk. Pass `body` to store it._" : "\n\n_Body not stored \u2014 no local workspace on this session. Pass `body` to store it._";
|
|
23373
|
-
} else if (typeof
|
|
23825
|
+
} else if (typeof target.storeDocBody !== "function") {
|
|
23374
23826
|
bodyNote = "\n\n_Body not stored \u2014 this adapter does not support body storage._";
|
|
23375
23827
|
} else {
|
|
23376
|
-
const decision = await checkDocStorageCap(
|
|
23828
|
+
const decision = await checkDocStorageCap(target, Buffer.byteLength(body, "utf8"));
|
|
23377
23829
|
if (!decision.storeBody) {
|
|
23378
23830
|
bodyNote = `
|
|
23379
23831
|
|
|
23380
23832
|
${decision.message}`;
|
|
23381
23833
|
} else {
|
|
23382
|
-
const result = await
|
|
23834
|
+
const result = await target.storeDocBody({
|
|
23383
23835
|
docId: entry.id,
|
|
23384
23836
|
body,
|
|
23385
23837
|
// Resolved by resolveDocVisibility above — the SAME resolution the
|
|
@@ -23403,7 +23855,7 @@ ${decision.message}`;
|
|
|
23403
23855
|
durability = "";
|
|
23404
23856
|
}
|
|
23405
23857
|
return textResponse(
|
|
23406
|
-
`**Registered:** ${entry.title}
|
|
23858
|
+
`**Registered:** ${entry.title}${overrideNote}
|
|
23407
23859
|
- **Path:** ${entry.path}
|
|
23408
23860
|
- **Type:** ${entry.type} | **Status:** ${entry.status}
|
|
23409
23861
|
- **Visibility:** ${visibilityLabel}
|
|
@@ -23437,7 +23889,7 @@ async function handleDocSearch(adapter2, args, config2) {
|
|
|
23437
23889
|
const lines = docs.map((d) => {
|
|
23438
23890
|
const actionCount = d.actions?.filter((a) => a.status === "pending").length ?? 0;
|
|
23439
23891
|
const actionNote = actionCount > 0 ? ` | ${actionCount} pending action(s)` : "";
|
|
23440
|
-
const missingNote = root && d.path && !existsSync9(
|
|
23892
|
+
const missingNote = root && d.path && !existsSync9(join14(root, d.path)) ? `
|
|
23441
23893
|
> \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.` : "";
|
|
23442
23894
|
return `### ${d.title}
|
|
23443
23895
|
**Type:** ${d.type} | **Status:** ${d.status} | **Cycle:** ${d.cycleCreated}${d.cycleUpdated ? `\u2192${d.cycleUpdated}` : ""}${actionNote}
|
|
@@ -23531,7 +23983,7 @@ function restoreBodyToDisk(projectRoot, docPath, body) {
|
|
|
23531
23983
|
`;
|
|
23532
23984
|
}
|
|
23533
23985
|
mkdirSync4(dirname4(target.abs), { recursive: true });
|
|
23534
|
-
|
|
23986
|
+
writeFileSync6(target.abs, body, "utf8");
|
|
23535
23987
|
return `- **Restored:** wrote ${Buffer.byteLength(body, "utf8").toLocaleString()} bytes to \`${docPath}\`
|
|
23536
23988
|
`;
|
|
23537
23989
|
} catch (err) {
|
|
@@ -23546,11 +23998,11 @@ function scanMdFiles(dir, rootDir) {
|
|
|
23546
23998
|
try {
|
|
23547
23999
|
const entries = readdirSync6(dir, { withFileTypes: true });
|
|
23548
24000
|
for (const entry of entries) {
|
|
23549
|
-
const full =
|
|
24001
|
+
const full = join14(dir, entry.name);
|
|
23550
24002
|
if (entry.isDirectory()) {
|
|
23551
24003
|
files.push(...scanMdFiles(full, rootDir));
|
|
23552
24004
|
} else if (entry.name.endsWith(".md")) {
|
|
23553
|
-
files.push(
|
|
24005
|
+
files.push(relative2(rootDir, full).replace(/\\/g, "/"));
|
|
23554
24006
|
}
|
|
23555
24007
|
}
|
|
23556
24008
|
} catch {
|
|
@@ -23571,7 +24023,7 @@ function extractTitle(filePath) {
|
|
|
23571
24023
|
async function detectUnregisteredDocsNote(adapter2, config2) {
|
|
23572
24024
|
try {
|
|
23573
24025
|
if (!adapter2.searchDocs || !hasLocalWorkspace()) return "";
|
|
23574
|
-
const docsDir =
|
|
24026
|
+
const docsDir = join14(config2.projectRoot, "docs");
|
|
23575
24027
|
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
23576
24028
|
if (docsFiles.length === 0) return "";
|
|
23577
24029
|
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
@@ -23597,17 +24049,17 @@ async function handleDocScan(adapter2, config2, args) {
|
|
|
23597
24049
|
const includePlans = args.include_plans ?? false;
|
|
23598
24050
|
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
23599
24051
|
const registeredPaths = new Set(registered.map((d) => d.path));
|
|
23600
|
-
const docsDir =
|
|
24052
|
+
const docsDir = join14(config2.projectRoot, "docs");
|
|
23601
24053
|
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
23602
24054
|
const unregisteredDocs = docsFiles.filter((f) => !registeredPaths.has(f));
|
|
23603
24055
|
let unregisteredPlans = [];
|
|
23604
24056
|
if (includePlans) {
|
|
23605
|
-
const plansDir =
|
|
24057
|
+
const plansDir = join14(homedir3(), ".claude", "plans");
|
|
23606
24058
|
if (existsSync9(plansDir)) {
|
|
23607
24059
|
const planFiles = scanMdFiles(plansDir, plansDir);
|
|
23608
24060
|
unregisteredPlans = planFiles.map((f) => `plans/${f}`).filter((f) => !registeredPaths.has(f)).map((f) => ({
|
|
23609
24061
|
path: f,
|
|
23610
|
-
title: extractTitle(
|
|
24062
|
+
title: extractTitle(join14(plansDir, f.replace("plans/", "")))
|
|
23611
24063
|
}));
|
|
23612
24064
|
}
|
|
23613
24065
|
}
|
|
@@ -23618,7 +24070,7 @@ async function handleDocScan(adapter2, config2, args) {
|
|
|
23618
24070
|
if (unregisteredDocs.length > 0) {
|
|
23619
24071
|
lines.push(`## Unregistered Docs (${unregisteredDocs.length})`);
|
|
23620
24072
|
for (const f of unregisteredDocs) {
|
|
23621
|
-
const title = extractTitle(
|
|
24073
|
+
const title = extractTitle(join14(config2.projectRoot, f));
|
|
23622
24074
|
lines.push(`- \`${f}\`${title ? ` \u2014 ${title}` : ""}`);
|
|
23623
24075
|
}
|
|
23624
24076
|
}
|
|
@@ -23967,7 +24419,7 @@ var buildExecuteTool = {
|
|
|
23967
24419
|
},
|
|
23968
24420
|
fixed_issues: {
|
|
23969
24421
|
type: "array",
|
|
23970
|
-
description: `cycle_learnings UUIDs of discovered issues this build FIXED.
|
|
24422
|
+
description: `cycle_learnings UUIDs of discovered issues this build FIXED. SEND THIS whenever your work closed an issue listed under "OPEN DISCOVERED ISSUES" in the BUILD HANDOFF \u2014 that block prints the exact UUIDs to copy. Nothing else stamps a fix, so an unreported one is indistinguishable from an unfixed issue: the hub's "What PAPI caught" caught\u2192fixed ledger reads zero until this is passed. Stamps resolved_at via the existing discovered_issue_resolve path \u2014 triage-and-fix at build time, no separate tool call. Distinct from resolves_learnings, which only LINKS a learning to this task without closing it. Do not pass UUIDs for issues you did not actually fix. Best-effort and idempotent.`,
|
|
23971
24423
|
items: { type: "string" }
|
|
23972
24424
|
},
|
|
23973
24425
|
production_verification: {
|
|
@@ -24325,7 +24777,7 @@ ${entries}`;
|
|
|
24325
24777
|
|
|
24326
24778
|
**OPEN DISCOVERED ISSUES** (${top.length} shown):
|
|
24327
24779
|
${rows}
|
|
24328
|
-
|
|
24780
|
+
CONTRACT \u2014 on complete, pass \`fixed_issues\` with the UUID of any issue above that this build closed, and say so even if the answer is none. Closing an issue without stamping it leaves the caught\u2192fixed ledger reading zero, which is what it reads today. Do NOT fix out-of-scope issues just to clear the list \u2014 the ask is to REPORT what you closed, not to close more.`;
|
|
24329
24781
|
}
|
|
24330
24782
|
}
|
|
24331
24783
|
} catch {
|
|
@@ -24572,8 +25024,25 @@ Your report was NOT discarded and the task is NOT yet Done \u2014 re-send with \
|
|
|
24572
25024
|
fixedNote = `
|
|
24573
25025
|
|
|
24574
25026
|
\u2705 Marked ${fixedResolvedCount} discovered issue(s) FIXED \u2014 resolved_at stamped, now counted as fixed on the hub's caught\u2192fixed ledger.`;
|
|
24575
|
-
} else
|
|
24576
|
-
|
|
25027
|
+
} else {
|
|
25028
|
+
let candidates = [];
|
|
25029
|
+
try {
|
|
25030
|
+
if (adapter2.getCycleLearnings) {
|
|
25031
|
+
const open = (await adapter2.getCycleLearnings({ category: "issue", limit: 20 })).filter((l) => !l.resolvedAt && l.id);
|
|
25032
|
+
const moduleTag = result.task?.module?.trim().toLowerCase();
|
|
25033
|
+
candidates = open.filter((l) => !moduleTag || l.tags.some((t) => t.toLowerCase() === moduleTag)).slice(0, 5).map((l) => ` - \`${l.id}\` \xB7 ${l.severity ?? "P3"} \xB7 ${l.summary.slice(0, 120)}`);
|
|
25034
|
+
}
|
|
25035
|
+
} catch {
|
|
25036
|
+
}
|
|
25037
|
+
if (candidates.length > 0) {
|
|
25038
|
+
fixedNote = `
|
|
25039
|
+
|
|
25040
|
+
\u2139\uFE0F No \`fixed_issues\` passed. Open issues in this module that this build could have closed:
|
|
25041
|
+
${candidates.join("\n")}
|
|
25042
|
+
If any are now fixed, re-run complete with their UUIDs in \`fixed_issues\` \u2014 nothing else stamps them, so an unreported fix is indistinguishable from an unfixed issue on the hub ledger.`;
|
|
25043
|
+
} else if (discoveredIssues && discoveredIssues.trim() !== "" && !/^none\b/i.test(discoveredIssues.trim())) {
|
|
25044
|
+
fixedNote = "\n\n\u2139\uFE0F This build filed discovered issues but passed no `fixed_issues`. When a future build fixes one, pass its UUID in `fixed_issues` so it counts as FIXED (not just auto-cleared) on the hub ledger.";
|
|
25045
|
+
}
|
|
24577
25046
|
}
|
|
24578
25047
|
return textResponse(formatCompleteResult(result) + fixedNote + docsNote + batchRollupNote);
|
|
24579
25048
|
} catch (err) {
|
|
@@ -24787,6 +25256,10 @@ var ideaTool = {
|
|
|
24787
25256
|
type: "string",
|
|
24788
25257
|
description: "What user problem does this solve? Auto-fill from problem context in notes when submitting ideas that describe a user pain point. The planner uses this to cluster backlog tasks by opportunity."
|
|
24789
25258
|
},
|
|
25259
|
+
conflicts_with_ad: {
|
|
25260
|
+
type: "string",
|
|
25261
|
+
description: 'The Active Decision this idea cuts against, e.g. "AD-12". USE THIS INSTEAD OF DROPPING THE IDEA. An AD is *active* \u2014 it can be superseded \u2014 so a contradiction is a decision for the user, never a reason for you to shelve work during research or scoping. The task is created and parked Blocked behind a decision-gate on that AD: it shows in `orient` as a decision waiting on the owner, and returns to the board automatically once the AD is superseded, modified, or reaffirmed.'
|
|
25262
|
+
},
|
|
24790
25263
|
project: {
|
|
24791
25264
|
type: "string",
|
|
24792
25265
|
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."
|
|
@@ -24819,7 +25292,8 @@ async function handleIdea(adapter2, config2, args) {
|
|
|
24819
25292
|
force: args.force === true,
|
|
24820
25293
|
docRef: args.doc_ref?.trim(),
|
|
24821
25294
|
type: args.type,
|
|
24822
|
-
opportunity: args.opportunity?.trim()
|
|
25295
|
+
opportunity: args.opportunity?.trim(),
|
|
25296
|
+
conflictsWithAd: args.conflicts_with_ad?.trim()
|
|
24823
25297
|
};
|
|
24824
25298
|
let target = adapter2;
|
|
24825
25299
|
let overrideNote = "";
|
|
@@ -24864,7 +25338,12 @@ Re-submit with \`notes: "... Reference: <path>"\` to link one, or ignore if none
|
|
|
24864
25338
|
} catch {
|
|
24865
25339
|
}
|
|
24866
25340
|
}
|
|
24867
|
-
|
|
25341
|
+
const conflictNote = result.adConflicts ? buildAdConflictNote(
|
|
25342
|
+
result.adConflicts.explicit ? result.adConflicts.adIds[0] : void 0,
|
|
25343
|
+
result.adConflicts.explicit ? [] : result.adConflicts.adIds,
|
|
25344
|
+
result.adConflicts.gated
|
|
25345
|
+
) : "";
|
|
25346
|
+
return textResponse(`${result.message}${overrideNote}${branchNote}${truncateWarning}${refNudge}${conflictNote}`);
|
|
24868
25347
|
}
|
|
24869
25348
|
return textResponse(`${result.message}${overrideNote}`);
|
|
24870
25349
|
}
|
|
@@ -25603,6 +26082,13 @@ init_git();
|
|
|
25603
26082
|
|
|
25604
26083
|
// src/services/ad-hoc.ts
|
|
25605
26084
|
import { randomUUID as randomUUID15 } from "crypto";
|
|
26085
|
+
function resolveAdHocBranch(input) {
|
|
26086
|
+
if (input.held) return `feat/${input.taskId}`;
|
|
26087
|
+
const current = input.currentBranch?.trim();
|
|
26088
|
+
if (!current) return void 0;
|
|
26089
|
+
if (input.baseBranch && current === input.baseBranch.trim()) return void 0;
|
|
26090
|
+
return current;
|
|
26091
|
+
}
|
|
25606
26092
|
function resolveAdHocCycle(cycle, latest, latestComplete) {
|
|
25607
26093
|
if (cycle === void 0) return null;
|
|
25608
26094
|
if (typeof cycle === "number") return cycle;
|
|
@@ -25657,8 +26143,32 @@ async function recordAdHoc(adapter2, input) {
|
|
|
25657
26143
|
...targetCycle !== null ? { cycle: targetCycle } : {},
|
|
25658
26144
|
notes: input.notes ? `[ad-hoc] ${input.notes}` : "[ad-hoc]",
|
|
25659
26145
|
taskType: input.taskType || "task",
|
|
25660
|
-
source: "ad_hoc"
|
|
26146
|
+
source: "ad_hoc",
|
|
26147
|
+
// task-2597: record the branch at creation for unheld work — the branch is
|
|
26148
|
+
// already known. Held work needs the allocated display id first (below).
|
|
26149
|
+
...held ? {} : (() => {
|
|
26150
|
+
const branch = resolveAdHocBranch({
|
|
26151
|
+
held: false,
|
|
26152
|
+
taskId: "",
|
|
26153
|
+
currentBranch: input.currentBranch,
|
|
26154
|
+
baseBranch: input.baseBranch
|
|
26155
|
+
});
|
|
26156
|
+
return branch ? { branchName: branch } : {};
|
|
26157
|
+
})()
|
|
25661
26158
|
});
|
|
26159
|
+
if (held) {
|
|
26160
|
+
const branch = resolveAdHocBranch({ held: true, taskId: task.id });
|
|
26161
|
+
if (branch) {
|
|
26162
|
+
try {
|
|
26163
|
+
await adapter2.updateTask(task.id, { branchName: branch });
|
|
26164
|
+
task = { ...task, branchName: branch };
|
|
26165
|
+
} catch (err) {
|
|
26166
|
+
console.error(
|
|
26167
|
+
`[ad-hoc] branch_name persist skipped for ${task.id} (non-fatal): ` + (err instanceof Error ? err.message : String(err))
|
|
26168
|
+
);
|
|
26169
|
+
}
|
|
26170
|
+
}
|
|
26171
|
+
}
|
|
25662
26172
|
}
|
|
25663
26173
|
const report = {
|
|
25664
26174
|
uuid: randomUUID15(),
|
|
@@ -25748,6 +26258,10 @@ var adHocTool = {
|
|
|
25748
26258
|
hold: {
|
|
25749
26259
|
type: "boolean",
|
|
25750
26260
|
description: "task-2477: held-adhoc. When true, do NOT force-complete or commit to main \u2014 record the task In Review pinned to the NEXT cycle (current + 1) so the planner won't re-plan it, and return a branch/PR directive (commit on feat/<task-id>, never main, leave unmerged) so it rides the next cycle's review \u2192 release bundled with planned work. One-call replacement for the two-call ad_hoc + board_edit stopgap. Takes precedence over `cycle`/`stage`."
|
|
26261
|
+
},
|
|
26262
|
+
project: {
|
|
26263
|
+
type: "string",
|
|
26264
|
+
description: "Project id (UUID) or slug to record this work under, overriding the session project for THIS call only. Must be a project on your account \u2014 fails closed otherwise. Use project_switch to change the session default. PASS THIS WHENEVER YOU KNOW WHICH REPO THE SESSION IS IN: on a multi-project account a connection with no project bound cannot be resolved, and PAPI will stop and ask rather than guess."
|
|
25751
26265
|
}
|
|
25752
26266
|
},
|
|
25753
26267
|
required: []
|
|
@@ -25784,7 +26298,18 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
25784
26298
|
else if (rawCycle === "current" || rawCycle === "next-if-plan-not-run") cycleArg = rawCycle;
|
|
25785
26299
|
const stageArg = args.stage === "release" ? "release" : void 0;
|
|
25786
26300
|
const holdArg = args.hold === true;
|
|
25787
|
-
|
|
26301
|
+
let target = adapter2;
|
|
26302
|
+
let overrideNote = "";
|
|
26303
|
+
try {
|
|
26304
|
+
({ adapter: target, overrideNote } = await resolvePerCallProjectAdapter(adapter2, args));
|
|
26305
|
+
} catch (err) {
|
|
26306
|
+
if (err instanceof ProjectResolutionError) return errorResponse(err.message);
|
|
26307
|
+
throw err;
|
|
26308
|
+
}
|
|
26309
|
+
const gitUsable = !overrideNote && isGitAvailable() && isGitRepo(config2.projectRoot);
|
|
26310
|
+
const currentBranch = gitUsable ? getCurrentBranch(config2.projectRoot) : null;
|
|
26311
|
+
const baseBranch = gitUsable ? resolveBaseBranch(config2.projectRoot, config2.baseBranch) : null;
|
|
26312
|
+
const result = await recordAdHoc(target, {
|
|
25788
26313
|
title: title || "",
|
|
25789
26314
|
taskId,
|
|
25790
26315
|
notes: rawNotes,
|
|
@@ -25797,9 +26322,11 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
25797
26322
|
owner: config2.projectOwner,
|
|
25798
26323
|
cycle: cycleArg,
|
|
25799
26324
|
stage: stageArg,
|
|
25800
|
-
hold: holdArg
|
|
26325
|
+
hold: holdArg,
|
|
26326
|
+
currentBranch,
|
|
26327
|
+
baseBranch
|
|
25801
26328
|
});
|
|
25802
|
-
if (!holdArg &&
|
|
26329
|
+
if (!holdArg && gitUsable) {
|
|
25803
26330
|
try {
|
|
25804
26331
|
stageDirAndCommit(
|
|
25805
26332
|
config2.projectRoot,
|
|
@@ -25817,7 +26344,7 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
25817
26344
|
const branch = `feat/${result.task.id}`;
|
|
25818
26345
|
let collisionBlock = "";
|
|
25819
26346
|
try {
|
|
25820
|
-
const board = await
|
|
26347
|
+
const board = await target.queryBoard({ status: ["In Progress"] });
|
|
25821
26348
|
const otherInProgress = board.filter((t) => t.id !== result.task.id && t.displayId !== result.task.id).map((t) => ({ taskId: t.displayId || t.id, branch: (t.branchName ?? "").trim() })).filter((t) => t.branch.length > 0);
|
|
25822
26349
|
const collision = detectWorktreeCollision({
|
|
25823
26350
|
taskId: result.task.id,
|
|
@@ -25841,7 +26368,7 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
25841
26368
|
} catch {
|
|
25842
26369
|
}
|
|
25843
26370
|
return textResponse(
|
|
25844
|
-
`**${result.task.id}:** "${result.task.title}" held for review (${effortRaw}, ${priorityRaw}, ${typeLabel}, ${taskModule}).${truncateWarning}${promoNote} Build report attached.
|
|
26371
|
+
`**${result.task.id}:** "${result.task.title}" held for review (${effortRaw}, ${priorityRaw}, ${typeLabel}, ${taskModule})${overrideNote}.${truncateWarning}${promoNote} Build report attached.
|
|
25845
26372
|
|
|
25846
26373
|
## Held for the next cycle \u2014 branch + commit, do NOT merge
|
|
25847
26374
|
The task is recorded **In Review** and pinned to **Cycle ${result.task.cycle}**, so the planner won't re-plan it and it rides that cycle's review \u2192 release bundled with planned work.
|
|
@@ -25859,7 +26386,7 @@ _To correct: board_edit ${result.task.id} with updated fields._`
|
|
|
25859
26386
|
);
|
|
25860
26387
|
}
|
|
25861
26388
|
return textResponse(
|
|
25862
|
-
`**${result.task.id}:** "${result.task.title}" recorded (${effortRaw}, ${priorityRaw}, ${typeLabel}, ${taskModule}).${truncateWarning}${promoNote} Build report attached.
|
|
26389
|
+
`**${result.task.id}:** "${result.task.title}" recorded (${effortRaw}, ${priorityRaw}, ${typeLabel}, ${taskModule})${overrideNote}.${truncateWarning}${promoNote} Build report attached.
|
|
25863
26390
|
_To correct: board_edit ${result.task.id} with updated fields._`
|
|
25864
26391
|
);
|
|
25865
26392
|
}
|
|
@@ -25869,11 +26396,11 @@ init_git();
|
|
|
25869
26396
|
|
|
25870
26397
|
// src/services/reconcile.ts
|
|
25871
26398
|
import { readFileSync as readFileSync9 } from "fs";
|
|
25872
|
-
import { join as
|
|
26399
|
+
import { join as join15 } from "path";
|
|
25873
26400
|
function loadDocsIndex(projectRoot) {
|
|
25874
26401
|
if (!hasLocalWorkspace()) return "";
|
|
25875
26402
|
try {
|
|
25876
|
-
const indexPath =
|
|
26403
|
+
const indexPath = join15(projectRoot, "docs", "INDEX.md");
|
|
25877
26404
|
const raw = readFileSync9(indexPath, "utf8");
|
|
25878
26405
|
const rows = raw.split("\n").filter((l) => l.startsWith("| ["));
|
|
25879
26406
|
if (rows.length === 0) return "";
|
|
@@ -26450,7 +26977,7 @@ Produce your analysis and structured output above. Present Part 1 to the user an
|
|
|
26450
26977
|
|
|
26451
26978
|
// src/tools/review.ts
|
|
26452
26979
|
import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
|
|
26453
|
-
import { join as
|
|
26980
|
+
import { join as join16 } from "path";
|
|
26454
26981
|
init_git();
|
|
26455
26982
|
|
|
26456
26983
|
// src/services/review.ts
|
|
@@ -26648,8 +27175,6 @@ Re-run build_execute complete with a production_verification field, then re-subm
|
|
|
26648
27175
|
}
|
|
26649
27176
|
|
|
26650
27177
|
// src/tools/review.ts
|
|
26651
|
-
var REVIEW_DISPATCH_THRESHOLD = 50 * 1024;
|
|
26652
|
-
var REVIEW_DISPATCH_CEILING = 40 * 1024;
|
|
26653
27178
|
var REVIEW_ECHO_CAP = 4e3;
|
|
26654
27179
|
function trimForEcho(text) {
|
|
26655
27180
|
if (text.length <= REVIEW_ECHO_CAP) return text;
|
|
@@ -26679,7 +27204,7 @@ ${task.buildReport}` : "### Build Report\n(none recorded)";
|
|
|
26679
27204
|
${diff}
|
|
26680
27205
|
\`\`\`` : "### Branch diff vs base\n(no diff resolved \u2014 not a git repo, no base ref, or no committed changes)";
|
|
26681
27206
|
let projectContext = "";
|
|
26682
|
-
const ctxPath =
|
|
27207
|
+
const ctxPath = join16(config2.projectRoot, ".agents", "papi-context.md");
|
|
26683
27208
|
if (existsSync10(ctxPath)) {
|
|
26684
27209
|
try {
|
|
26685
27210
|
projectContext = `### Project context (.agents/papi-context.md)
|
|
@@ -26858,7 +27383,7 @@ function mergeAfterAccept(config2, taskId) {
|
|
|
26858
27383
|
};
|
|
26859
27384
|
}
|
|
26860
27385
|
const details = [];
|
|
26861
|
-
const papiDir =
|
|
27386
|
+
const papiDir = join16(config2.projectRoot, ".papi");
|
|
26862
27387
|
if (existsSync10(papiDir)) {
|
|
26863
27388
|
try {
|
|
26864
27389
|
const commitResult = stageDirAndCommit(
|
|
@@ -26997,7 +27522,7 @@ async function handleReviewSubmit(adapter2, config2, args) {
|
|
|
26997
27522
|
if (typeof rawPreset === "string" && rawPreset.length > 0 && !reviewPreset) {
|
|
26998
27523
|
return errorResponse(`review_preset "${rawPreset}" is not valid. Use "gate", "full", or "security-focused".`);
|
|
26999
27524
|
}
|
|
27000
|
-
const autoDispatchOptIn = args.dispatch !== "inline" &&
|
|
27525
|
+
const autoDispatchOptIn = args.dispatch !== "inline" && isCapabilityEnabled(caps, "prReviewer");
|
|
27001
27526
|
const autoDispatchEligible = !verdict && autoDispatchOptIn;
|
|
27002
27527
|
const capabilityAutoReviewEligible = verdict === "accept" && !autoReview && autoDispatchOptIn;
|
|
27003
27528
|
let capabilityReviewSkippedNote = "";
|
|
@@ -27010,16 +27535,27 @@ async function handleReviewSubmit(adapter2, config2, args) {
|
|
|
27010
27535
|
);
|
|
27011
27536
|
if (!dispatch.ok) {
|
|
27012
27537
|
if (explicitDispatch) return errorResponse(dispatch.error);
|
|
27013
|
-
} else if (explicitDispatch || autoDispatchEligible && dispatch.contextBytes
|
|
27538
|
+
} else if (explicitDispatch || autoDispatchEligible && shouldDispatch(dispatch.contextBytes)) {
|
|
27014
27539
|
return textResponse(dispatch.prompt);
|
|
27015
27540
|
} else if (capabilityAutoReviewEligible) {
|
|
27016
|
-
|
|
27017
|
-
|
|
27541
|
+
const delivery = planDelivery({
|
|
27542
|
+
payload: dispatch.prompt,
|
|
27543
|
+
kind: "review-context",
|
|
27544
|
+
hasPendingWrite: true,
|
|
27545
|
+
adapterType: config2.adapterType,
|
|
27546
|
+
projectId: config2.projectId,
|
|
27547
|
+
callerKey: taskId
|
|
27548
|
+
});
|
|
27549
|
+
if (delivery.mode === "inline") {
|
|
27550
|
+
return textResponse(delivery.payload);
|
|
27018
27551
|
}
|
|
27019
27552
|
const kb = (dispatch.contextBytes / 1024).toFixed(0);
|
|
27020
|
-
capabilityReviewSkippedNote = `
|
|
27553
|
+
capabilityReviewSkippedNote = delivery.mode === "spilled" ? `
|
|
27554
|
+
|
|
27555
|
+
> \u{1F4C4} pr-reviewer auto-review was too large to return inline (~${kb} KB), so it was written to a file instead of being dropped (task-3030). Verdict recorded. To run the review, read:
|
|
27556
|
+
> \`${delivery.path}\`` : `
|
|
27021
27557
|
|
|
27022
|
-
> \u26A0\uFE0F pr-reviewer auto-review
|
|
27558
|
+
> \u26A0\uFE0F pr-reviewer auto-review could not be delivered \u2014 the diff/build-report (~${kb} KB) exceeds the inline ceiling and this connection has no shared filesystem to spill to. Verdict recorded. To review the diff explicitly, run \`review_submit ${taskId} build-acceptance accept dispatch:"subagent"\`.`;
|
|
27023
27559
|
}
|
|
27024
27560
|
}
|
|
27025
27561
|
if (explicitDispatch && stage !== "build-acceptance") {
|
|
@@ -28491,7 +29027,7 @@ function formatDeferredGateSection(sweep) {
|
|
|
28491
29027
|
|
|
28492
29028
|
// src/tools/agent-list.ts
|
|
28493
29029
|
import { readdir as readdir2, readFile as readFile7 } from "fs/promises";
|
|
28494
|
-
import { join as
|
|
29030
|
+
import { join as join17 } from "path";
|
|
28495
29031
|
var NO_AGENTS_HINT = "No project sub-agents found in `.claude/agents/`. Add a `*.md` file with `name` + `description` frontmatter \u2014 see the 1926-Census marketing sub-agent for a reference implementation.";
|
|
28496
29032
|
function parseAgentFrontmatter(content) {
|
|
28497
29033
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
@@ -28503,7 +29039,7 @@ function parseAgentFrontmatter(content) {
|
|
|
28503
29039
|
return { name: nameMatch?.[1].trim(), description };
|
|
28504
29040
|
}
|
|
28505
29041
|
async function listAgents(projectRoot) {
|
|
28506
|
-
const agentsDir =
|
|
29042
|
+
const agentsDir = join17(projectRoot, ".claude", "agents");
|
|
28507
29043
|
let files;
|
|
28508
29044
|
try {
|
|
28509
29045
|
files = await readdir2(agentsDir);
|
|
@@ -28514,7 +29050,7 @@ async function listAgents(projectRoot) {
|
|
|
28514
29050
|
for (const file of files.filter((f) => f.endsWith(".md"))) {
|
|
28515
29051
|
let content;
|
|
28516
29052
|
try {
|
|
28517
|
-
content = await readFile7(
|
|
29053
|
+
content = await readFile7(join17(agentsDir, file), "utf-8");
|
|
28518
29054
|
} catch {
|
|
28519
29055
|
continue;
|
|
28520
29056
|
}
|
|
@@ -28522,7 +29058,7 @@ async function listAgents(projectRoot) {
|
|
|
28522
29058
|
agents.push({
|
|
28523
29059
|
name: meta?.name ?? file.replace(/\.md$/, ""),
|
|
28524
29060
|
description: meta?.description ?? "",
|
|
28525
|
-
path:
|
|
29061
|
+
path: join17(".claude", "agents", file)
|
|
28526
29062
|
});
|
|
28527
29063
|
}
|
|
28528
29064
|
agents.sort((a, b2) => a.name.localeCompare(b2.name));
|
|
@@ -28663,8 +29199,8 @@ async function verifyProject(adapter2) {
|
|
|
28663
29199
|
// src/tools/orient.ts
|
|
28664
29200
|
import { execFile as execFile2 } from "child_process";
|
|
28665
29201
|
import { promisify as promisify2 } from "util";
|
|
28666
|
-
import { readFileSync as readFileSync11, writeFileSync as
|
|
28667
|
-
import { join as
|
|
29202
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync7, existsSync as existsSync11 } from "fs";
|
|
29203
|
+
import { join as join18 } from "path";
|
|
28668
29204
|
|
|
28669
29205
|
// src/lib/exit-criteria-evaluators.ts
|
|
28670
29206
|
var TEST_ACCOUNT_PATTERN = "ftue-%@test.papi.dev";
|
|
@@ -28720,6 +29256,7 @@ var UNEVALUATABLE = {
|
|
|
28720
29256
|
"c596a082-9ad3-4089-b497-46cd189e892b": "threshold is provisional pending an owner ruling \u2014 no settled bar to evaluate against"
|
|
28721
29257
|
};
|
|
28722
29258
|
async function evaluateExitCriteria(adapter2, projectId, criteria) {
|
|
29259
|
+
const hasProject = typeof projectId === "string" && projectId.trim() !== "";
|
|
28723
29260
|
return Promise.all(criteria.map(async (c) => {
|
|
28724
29261
|
const evaluator = EVALUATORS[c.id];
|
|
28725
29262
|
if (!evaluator) {
|
|
@@ -28727,6 +29264,14 @@ async function evaluateExitCriteria(adapter2, projectId, criteria) {
|
|
|
28727
29264
|
if (!reason) return { ...c, met: c.met, autoEvaluated: false };
|
|
28728
29265
|
return c.met ? { ...c, met: true, autoEvaluated: false } : { ...c, met: null, unevaluatedReason: reason, autoEvaluated: false };
|
|
28729
29266
|
}
|
|
29267
|
+
if (!hasProject) {
|
|
29268
|
+
return {
|
|
29269
|
+
...c,
|
|
29270
|
+
met: c.met ? true : null,
|
|
29271
|
+
unevaluatedReason: "project id unavailable \u2014 this criterion is measured against a specific project and orient could not resolve one",
|
|
29272
|
+
autoEvaluated: false
|
|
29273
|
+
};
|
|
29274
|
+
}
|
|
28730
29275
|
try {
|
|
28731
29276
|
const result = await evaluator(adapter2, projectId);
|
|
28732
29277
|
return {
|
|
@@ -28737,10 +29282,11 @@ async function evaluateExitCriteria(adapter2, projectId, criteria) {
|
|
|
28737
29282
|
autoEvaluated: result.met !== null
|
|
28738
29283
|
};
|
|
28739
29284
|
} catch (err) {
|
|
29285
|
+
console.error(`[exit-criteria] evaluator for criterion ${c.id} threw:`, err);
|
|
28740
29286
|
return {
|
|
28741
29287
|
...c,
|
|
28742
29288
|
met: c.met ? true : null,
|
|
28743
|
-
unevaluatedReason:
|
|
29289
|
+
unevaluatedReason: "evaluator failed \u2014 see server logs. The stored value is shown and is unverified.",
|
|
28744
29290
|
autoEvaluated: false
|
|
28745
29291
|
};
|
|
28746
29292
|
}
|
|
@@ -29119,9 +29665,13 @@ async function getHierarchyPosition(adapter2, projectId) {
|
|
|
29119
29665
|
// task-3008: EVALUATE rather than read the hand-ticked boolean. Best-effort —
|
|
29120
29666
|
// a failing evaluator degrades that criterion to unevaluated and leaves the
|
|
29121
29667
|
// rest intact, because orient is the first call of every session.
|
|
29668
|
+
// task-3226: pass the id THROUGH, undefined and all. This used to coerce to
|
|
29669
|
+
// `''`, which is not a uuid and blew up every SQL-backed evaluator — the
|
|
29670
|
+
// coercion looked like a safe default and was the bug. evaluateExitCriteria
|
|
29671
|
+
// now reports "project id unavailable" for that case, which is the truth.
|
|
29122
29672
|
stageExitCriteria: await evaluateExitCriteria(
|
|
29123
29673
|
adapter2,
|
|
29124
|
-
projectId
|
|
29674
|
+
projectId,
|
|
29125
29675
|
activeStage.exitCriteria ?? []
|
|
29126
29676
|
).catch(() => (activeStage.exitCriteria ?? []).map((c) => ({ ...c, autoEvaluated: false })))
|
|
29127
29677
|
};
|
|
@@ -29129,21 +29679,10 @@ async function getHierarchyPosition(adapter2, projectId) {
|
|
|
29129
29679
|
return void 0;
|
|
29130
29680
|
}
|
|
29131
29681
|
}
|
|
29132
|
-
|
|
29133
|
-
try {
|
|
29134
|
-
const { stdout } = await execFileAsync2("git", ["describe", "--tags", "--abbrev=0"], {
|
|
29135
|
-
encoding: "utf-8",
|
|
29136
|
-
cwd: projectRoot,
|
|
29137
|
-
timeout: 2e3
|
|
29138
|
-
});
|
|
29139
|
-
return stdout.trim() || null;
|
|
29140
|
-
} catch {
|
|
29141
|
-
return null;
|
|
29142
|
-
}
|
|
29143
|
-
}
|
|
29682
|
+
var GIT_TAG_TIMEOUT_MS = 2e3;
|
|
29144
29683
|
async function checkNpmVersionDrift() {
|
|
29145
29684
|
try {
|
|
29146
|
-
const pkgPath =
|
|
29685
|
+
const pkgPath = join18(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
|
|
29147
29686
|
const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
|
|
29148
29687
|
const localVersion = pkg.version;
|
|
29149
29688
|
const packageName = pkg.name;
|
|
@@ -29486,7 +30025,7 @@ async function handleOrient(adapter2, config2, args = {}, clientName) {
|
|
|
29486
30025
|
// Latest git tag + npm version drift (both exec calls with own timeouts).
|
|
29487
30026
|
// task-2172: git-tag stays (the latest tag is part of the core summary);
|
|
29488
30027
|
// version-drift is enrichment, gated behind `full`/deep_housekeeping.
|
|
29489
|
-
tracked("git-tag", () =>
|
|
30028
|
+
tracked("git-tag", async () => getLatestTag(config2.projectRoot, GIT_TAG_TIMEOUT_MS)),
|
|
29490
30029
|
tracked("npm-version-drift", async () => fullEnrichment ? checkNpmVersionDrift() : void 0),
|
|
29491
30030
|
// Research Signals — research docs with pending actions since last strategy review.
|
|
29492
30031
|
// task-2172: heavy (doc search + AD cross-reference) and rarely actioned
|
|
@@ -29844,7 +30383,7 @@ function enrichClaudeMd(projectRoot, cycleNumber, adapterType, collector, client
|
|
|
29844
30383
|
|
|
29845
30384
|
\u{1F4DD} **CLAUDE.md enriched** \u2014 added ${tierNames2.join(" + ")} guidance for cycle ${cycleNumber}+ projects.`;
|
|
29846
30385
|
}
|
|
29847
|
-
const claudeMdPath =
|
|
30386
|
+
const claudeMdPath = join18(projectRoot, "CLAUDE.md");
|
|
29848
30387
|
if (!existsSync11(claudeMdPath)) return "";
|
|
29849
30388
|
const content = readFileSync11(claudeMdPath, "utf-8");
|
|
29850
30389
|
const additions = [];
|
|
@@ -29855,7 +30394,7 @@ function enrichClaudeMd(projectRoot, cycleNumber, adapterType, collector, client
|
|
|
29855
30394
|
additions.push(dedupeEnrichmentBlob(content, CLAUDE_MD_TIER_2));
|
|
29856
30395
|
}
|
|
29857
30396
|
if (additions.length === 0) return "";
|
|
29858
|
-
|
|
30397
|
+
writeFileSync7(claudeMdPath, content + additions.join(""), "utf-8");
|
|
29859
30398
|
const tierNames = [];
|
|
29860
30399
|
if (additions.some((a) => a.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1))) tierNames.push("Established (batch building, strategy reviews, AD lifecycle)");
|
|
29861
30400
|
if (additions.some((a) => a.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T2))) tierNames.push("Mature (idea pipeline, doc registry, advanced patterns)");
|
|
@@ -30377,6 +30916,10 @@ var zoomOutTool = {
|
|
|
30377
30916
|
enum: ["prepare", "apply"],
|
|
30378
30917
|
description: '"prepare" returns the retrospective prompt. "apply" accepts your output. Defaults to "prepare" when omitted.'
|
|
30379
30918
|
},
|
|
30919
|
+
llm_response_file: {
|
|
30920
|
+
type: "string",
|
|
30921
|
+
description: 'Absolute path to a file containing the zoom-out output (mode "apply" only). LOCAL stdio servers only \u2014 on the hosted connection the server cannot read files on your machine. Use when the response is too large to pass inline (some hosts cap tool inputs around 50KB). Mutually exclusive with llm_response.'
|
|
30922
|
+
},
|
|
30380
30923
|
llm_response: {
|
|
30381
30924
|
type: "string",
|
|
30382
30925
|
description: 'Your raw output from executing the retrospective prompt (mode "apply" only).'
|
|
@@ -30398,10 +30941,12 @@ async function handleZoomOut(adapter2, config2, args) {
|
|
|
30398
30941
|
const toolMode = args.mode;
|
|
30399
30942
|
try {
|
|
30400
30943
|
if (toolMode === "apply") {
|
|
30401
|
-
const
|
|
30402
|
-
|
|
30403
|
-
|
|
30404
|
-
|
|
30944
|
+
const resolved = await resolveLlmResponse(
|
|
30945
|
+
args.llm_response,
|
|
30946
|
+
args.llm_response_file
|
|
30947
|
+
);
|
|
30948
|
+
if (!resolved.ok) return errorResponse(resolved.error);
|
|
30949
|
+
const llmResponse = resolved.llmResponse;
|
|
30405
30950
|
const cycleNumber = typeof args.cycle_number === "number" ? args.cycle_number : 0;
|
|
30406
30951
|
const result = await applyZoomOut(adapter2, llmResponse, cycleNumber);
|
|
30407
30952
|
return textResponse(
|
|
@@ -30416,16 +30961,8 @@ Retrospective saved. Use insights to inform your next \`strategy_review\` or \`p
|
|
|
30416
30961
|
}
|
|
30417
30962
|
{
|
|
30418
30963
|
const result = await prepareZoomOut(adapter2, config2.projectRoot);
|
|
30419
|
-
const
|
|
30420
|
-
const
|
|
30421
|
-
let dispatch;
|
|
30422
|
-
if (args.dispatch === "inline" || args.dispatch === "subagent") {
|
|
30423
|
-
dispatch = args.dispatch;
|
|
30424
|
-
} else if (autoDispatchEnabled && result.contextBytes > autoDispatchThreshold) {
|
|
30425
|
-
dispatch = "subagent";
|
|
30426
|
-
} else {
|
|
30427
|
-
dispatch = "inline";
|
|
30428
|
-
}
|
|
30964
|
+
const explicit = args.dispatch === "inline" ? false : args.dispatch === "subagent" ? true : void 0;
|
|
30965
|
+
const dispatch = shouldDispatch(result.contextBytes, explicit) ? "subagent" : "inline";
|
|
30429
30966
|
if (dispatch === "subagent") {
|
|
30430
30967
|
const dispatchPrompt = buildSubagentDispatchPrompt({
|
|
30431
30968
|
tool: "zoom_out",
|
|
@@ -30670,8 +31207,8 @@ ${result.userMessage}
|
|
|
30670
31207
|
import { readFileSync as readFileSync12, statSync as statSync7 } from "fs";
|
|
30671
31208
|
|
|
30672
31209
|
// src/services/scope-brief.ts
|
|
30673
|
-
import { writeFileSync as
|
|
30674
|
-
import { join as
|
|
31210
|
+
import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync5 } from "fs";
|
|
31211
|
+
import { join as join19, dirname as dirname5 } from "path";
|
|
30675
31212
|
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.
|
|
30676
31213
|
|
|
30677
31214
|
A scope document must:
|
|
@@ -30728,14 +31265,14 @@ async function applyScopeBrief(adapter2, input) {
|
|
|
30728
31265
|
}
|
|
30729
31266
|
const slug = input.taskId.replace(/[^a-z0-9-]/g, "-").toLowerCase();
|
|
30730
31267
|
const relPath = `docs/scopes/${slug}.md`;
|
|
30731
|
-
const absPath =
|
|
31268
|
+
const absPath = join19(input.projectRoot, relPath);
|
|
30732
31269
|
const docBody = addFrontmatter(docContent, task, input.cycleNumber);
|
|
30733
31270
|
const collector = new FileWriteCollector();
|
|
30734
31271
|
if (input.adapterType === "proxy") {
|
|
30735
31272
|
collector.add({ path: relPath, content: docBody, mode: "overwrite" });
|
|
30736
31273
|
} else {
|
|
30737
31274
|
mkdirSync5(dirname5(absPath), { recursive: true });
|
|
30738
|
-
|
|
31275
|
+
writeFileSync8(absPath, docBody, "utf-8");
|
|
30739
31276
|
}
|
|
30740
31277
|
const taskCount = countSubTasks(docContent);
|
|
30741
31278
|
const summary = buildSummary(task, taskCount);
|
|
@@ -31022,7 +31559,7 @@ ${formatted}`, meta));
|
|
|
31022
31559
|
|
|
31023
31560
|
// src/lib/dist-staleness.ts
|
|
31024
31561
|
import { readFileSync as readFileSync13, statSync as statSync8 } from "fs";
|
|
31025
|
-
import { createHash as
|
|
31562
|
+
import { createHash as createHash6 } from "crypto";
|
|
31026
31563
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
31027
31564
|
var BOOT_MS = Date.now();
|
|
31028
31565
|
var DEFAULT_SKEW_MS = 2e3;
|
|
@@ -31036,7 +31573,7 @@ var SELF_PATH = (() => {
|
|
|
31036
31573
|
})();
|
|
31037
31574
|
function hashFile(path7) {
|
|
31038
31575
|
try {
|
|
31039
|
-
return
|
|
31576
|
+
return createHash6("sha256").update(readFileSync13(path7)).digest("hex");
|
|
31040
31577
|
} catch {
|
|
31041
31578
|
return null;
|
|
31042
31579
|
}
|
|
@@ -31686,19 +32223,19 @@ Its build reports, comments, and history moved with it; the cycle assignment was
|
|
|
31686
32223
|
|
|
31687
32224
|
// src/services/harness-inventory.ts
|
|
31688
32225
|
import { readdir as readdir3, readFile as readFile8, stat as stat3 } from "fs/promises";
|
|
31689
|
-
import { join as
|
|
31690
|
-
import { createHash as
|
|
32226
|
+
import { join as join20 } from "path";
|
|
32227
|
+
import { createHash as createHash7 } from "crypto";
|
|
31691
32228
|
var RECOMMENDED_HOOKS = ["stop-release-check.sh", "claude-md-size-guard.sh"];
|
|
31692
32229
|
async function computeFingerprint(root) {
|
|
31693
32230
|
const parts = [];
|
|
31694
32231
|
for (const sub of [".claude/skills", ".claude/agents", ".claude/hooks"]) {
|
|
31695
|
-
const dir =
|
|
32232
|
+
const dir = join20(root, sub);
|
|
31696
32233
|
try {
|
|
31697
32234
|
const names = (await readdir3(dir)).sort((a, b2) => a.localeCompare(b2));
|
|
31698
32235
|
for (const name of names) {
|
|
31699
32236
|
let mtime = "";
|
|
31700
32237
|
try {
|
|
31701
|
-
mtime = String(Math.floor((await stat3(
|
|
32238
|
+
mtime = String(Math.floor((await stat3(join20(dir, name))).mtimeMs));
|
|
31702
32239
|
} catch {
|
|
31703
32240
|
}
|
|
31704
32241
|
parts.push(`${sub}/${name}:${mtime}`);
|
|
@@ -31713,11 +32250,11 @@ async function computeFingerprint(root) {
|
|
|
31713
32250
|
} catch {
|
|
31714
32251
|
parts.push("manifest:none");
|
|
31715
32252
|
}
|
|
31716
|
-
return
|
|
32253
|
+
return createHash7("sha256").update(parts.join("|")).digest("hex").slice(0, 16);
|
|
31717
32254
|
}
|
|
31718
32255
|
async function readSkillDescription(skillDir) {
|
|
31719
32256
|
try {
|
|
31720
|
-
const content = await readFile8(
|
|
32257
|
+
const content = await readFile8(join20(skillDir, "SKILL.md"), "utf-8");
|
|
31721
32258
|
const fm = content.match(/^---\n([\s\S]*?)\n---/);
|
|
31722
32259
|
if (!fm) return void 0;
|
|
31723
32260
|
const desc = fm[1].match(/^description:\s*[>|]?\s*\n?([\s\S]*?)(?=\n\w+:|\n---|$)/m);
|
|
@@ -31737,17 +32274,17 @@ async function scanInventory(root, toolDefs) {
|
|
|
31737
32274
|
version = loadManifest().packageVersion;
|
|
31738
32275
|
} catch {
|
|
31739
32276
|
}
|
|
31740
|
-
const skillsDir =
|
|
32277
|
+
const skillsDir = join20(root, ".claude", "skills");
|
|
31741
32278
|
try {
|
|
31742
32279
|
const dirents = await readdir3(skillsDir, { withFileTypes: true });
|
|
31743
32280
|
for (const d of dirents.filter((e) => e.isDirectory())) {
|
|
31744
32281
|
entries.push({
|
|
31745
32282
|
kind: "skill",
|
|
31746
32283
|
name: d.name,
|
|
31747
|
-
description: await readSkillDescription(
|
|
32284
|
+
description: await readSkillDescription(join20(skillsDir, d.name)),
|
|
31748
32285
|
version,
|
|
31749
32286
|
status: stale.has(d.name) ? "stale_fork" : "ok",
|
|
31750
|
-
path:
|
|
32287
|
+
path: join20(".claude", "skills", d.name)
|
|
31751
32288
|
});
|
|
31752
32289
|
}
|
|
31753
32290
|
} catch {
|
|
@@ -31763,9 +32300,9 @@ async function scanInventory(root, toolDefs) {
|
|
|
31763
32300
|
}
|
|
31764
32301
|
const present = /* @__PURE__ */ new Set();
|
|
31765
32302
|
try {
|
|
31766
|
-
for (const f of (await readdir3(
|
|
32303
|
+
for (const f of (await readdir3(join20(root, ".claude", "hooks"))).filter((n) => n.endsWith(".sh"))) {
|
|
31767
32304
|
present.add(f);
|
|
31768
|
-
entries.push({ kind: "hook", name: f, status: "ok", path:
|
|
32305
|
+
entries.push({ kind: "hook", name: f, status: "ok", path: join20(".claude", "hooks", f) });
|
|
31769
32306
|
}
|
|
31770
32307
|
} catch {
|
|
31771
32308
|
}
|
|
@@ -32045,6 +32582,7 @@ If this is legitimate, reach out at https://getpapi.ai and we'll lift it \u2014
|
|
|
32045
32582
|
}
|
|
32046
32583
|
|
|
32047
32584
|
// src/server.ts
|
|
32585
|
+
var mdModeWarned = false;
|
|
32048
32586
|
var DEFAULT_TOOL_TIMEOUT_MS = parseInt(process.env.PAPI_TOOL_TIMEOUT_MS ?? "30000", 10);
|
|
32049
32587
|
var LONG_TOOL_TIMEOUT_MS = parseInt(process.env.PAPI_LONG_TOOL_TIMEOUT_MS ?? "180000", 10);
|
|
32050
32588
|
var WEDGE_PENDING_FRACTION = Math.min(1, Math.max(0, parseFloat(process.env.PAPI_WEDGE_PENDING_FRACTION ?? "0.6")));
|
|
@@ -32210,7 +32748,7 @@ function createServer(adapter2, config2) {
|
|
|
32210
32748
|
const __pkgDir = dirname6(__pkgFilename);
|
|
32211
32749
|
let serverVersion = "unknown";
|
|
32212
32750
|
try {
|
|
32213
|
-
const pkg = JSON.parse(readFileSync14(
|
|
32751
|
+
const pkg = JSON.parse(readFileSync14(join21(__pkgDir, "..", "package.json"), "utf-8"));
|
|
32214
32752
|
serverVersion = pkg.version ?? "unknown";
|
|
32215
32753
|
} catch {
|
|
32216
32754
|
}
|
|
@@ -32221,14 +32759,15 @@ function createServer(adapter2, config2) {
|
|
|
32221
32759
|
// task-1801: `resources` capability for the PAPI read surface exposed as MCP resources.
|
|
32222
32760
|
{ capabilities: { tools: {}, prompts: {}, resources: {} }, instructions: UNIVERSAL_FRAME }
|
|
32223
32761
|
);
|
|
32224
|
-
if (config2.adapterType === "md") {
|
|
32762
|
+
if (config2.adapterType === "md" && !mdModeWarned) {
|
|
32763
|
+
mdModeWarned = true;
|
|
32225
32764
|
process.stderr.write(
|
|
32226
32765
|
"\n\u26A0 PAPI is running in md mode \u2014 your cycles are not visible on the hosted dashboard.\n Configure DATABASE_URL or sign up at https://getpapi.ai/setup to enable observability.\n\n"
|
|
32227
32766
|
);
|
|
32228
32767
|
}
|
|
32229
32768
|
const __filename = fileURLToPath4(import.meta.url);
|
|
32230
32769
|
const __dirname2 = dirname6(__filename);
|
|
32231
|
-
const skillsDir =
|
|
32770
|
+
const skillsDir = join21(__dirname2, "..", "skills");
|
|
32232
32771
|
function parseSkillFrontmatter(content) {
|
|
32233
32772
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
32234
32773
|
if (!match) return null;
|
|
@@ -32246,7 +32785,7 @@ function createServer(adapter2, config2) {
|
|
|
32246
32785
|
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
32247
32786
|
const prompts = [];
|
|
32248
32787
|
for (const file of mdFiles) {
|
|
32249
|
-
const content = await readFile9(
|
|
32788
|
+
const content = await readFile9(join21(skillsDir, file), "utf-8");
|
|
32250
32789
|
const meta = parseSkillFrontmatter(content);
|
|
32251
32790
|
if (meta) {
|
|
32252
32791
|
prompts.push({ name: meta.name, description: meta.description });
|
|
@@ -32262,7 +32801,7 @@ function createServer(adapter2, config2) {
|
|
|
32262
32801
|
try {
|
|
32263
32802
|
const files = await readdir4(skillsDir);
|
|
32264
32803
|
for (const file of files.filter((f) => f.endsWith(".md"))) {
|
|
32265
|
-
const content = await readFile9(
|
|
32804
|
+
const content = await readFile9(join21(skillsDir, file), "utf-8");
|
|
32266
32805
|
const meta = parseSkillFrontmatter(content);
|
|
32267
32806
|
if (meta?.name === name) {
|
|
32268
32807
|
const body = content.replace(/^---\n[\s\S]*?\n---\n*/, "");
|
|
@@ -32548,6 +33087,7 @@ ${usageLine(decision.usage)}`;
|
|
|
32548
33087
|
// src/transport-http.ts
|
|
32549
33088
|
init_proxy_adapter();
|
|
32550
33089
|
import { createServer as createHttpServer } from "http";
|
|
33090
|
+
import { createHash as createHash8 } from "crypto";
|
|
32551
33091
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
32552
33092
|
var BEARER_PREFIX = "papi_";
|
|
32553
33093
|
var BEARER_REGEX = /^(papi_|papi_oauth_)[a-f0-9]{64}$/;
|
|
@@ -32577,6 +33117,7 @@ var FRIENDLY_GET_HTML = `<!doctype html>
|
|
|
32577
33117
|
<code>https://mcp.getpapi.ai/mcp</code>.</p>
|
|
32578
33118
|
<a class="btn" href="https://getpapi.ai/docs/install">See the install guide \u2192</a>
|
|
32579
33119
|
</div></body></html>`;
|
|
33120
|
+
var KNOWN_INSTALL_CLIENTS = /* @__PURE__ */ new Set(["claude-code-plugin"]);
|
|
32580
33121
|
var MAX_BODY_BYTES = 1 * 1024 * 1024;
|
|
32581
33122
|
var IP_RATE_WINDOW_MS = 6e4;
|
|
32582
33123
|
var IP_RATE_MAX = 60;
|
|
@@ -32615,7 +33156,7 @@ function corsHeaders(origin) {
|
|
|
32615
33156
|
return {
|
|
32616
33157
|
"Access-Control-Allow-Origin": origin,
|
|
32617
33158
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
32618
|
-
"Access-Control-Allow-Headers": "Authorization, Content-Type, x-papi-project-id",
|
|
33159
|
+
"Access-Control-Allow-Headers": "Authorization, Content-Type, x-papi-project-id, x-papi-client",
|
|
32619
33160
|
"Vary": "Origin"
|
|
32620
33161
|
};
|
|
32621
33162
|
}
|
|
@@ -32657,6 +33198,48 @@ function sendError(res, err, extraHeaders = {}) {
|
|
|
32657
33198
|
});
|
|
32658
33199
|
res.end(JSON.stringify(err.body));
|
|
32659
33200
|
}
|
|
33201
|
+
function sendUnauthorized(res, reason, extraHeaders = {}) {
|
|
33202
|
+
const challenge = reason === "invalid_token" ? `Bearer realm="papi", error="invalid_token", error_description="The access token was rejected: it has been revoked or has expired.", resource_metadata="${RESOURCE_METADATA_URL}"` : `Bearer realm="papi", resource_metadata="${RESOURCE_METADATA_URL}"`;
|
|
33203
|
+
sendError(
|
|
33204
|
+
res,
|
|
33205
|
+
{ status: 401, body: { error: "Unauthorized", reason } },
|
|
33206
|
+
{ ...extraHeaders, "WWW-Authenticate": challenge }
|
|
33207
|
+
);
|
|
33208
|
+
}
|
|
33209
|
+
var AUTH_VALID_TTL_MS = 5 * 6e4;
|
|
33210
|
+
var AUTH_REJECTED_TTL_MS = 6e4;
|
|
33211
|
+
var AUTH_CACHE_MAX = 5e3;
|
|
33212
|
+
var authVerdicts = /* @__PURE__ */ new Map();
|
|
33213
|
+
function bearerKey(bearer) {
|
|
33214
|
+
return createHash8("sha256").update(bearer).digest("hex");
|
|
33215
|
+
}
|
|
33216
|
+
function readAuthVerdict(bearer, now = Date.now()) {
|
|
33217
|
+
const hit = authVerdicts.get(bearerKey(bearer));
|
|
33218
|
+
if (!hit) return void 0;
|
|
33219
|
+
if (hit.expires <= now) {
|
|
33220
|
+
authVerdicts.delete(bearerKey(bearer));
|
|
33221
|
+
return void 0;
|
|
33222
|
+
}
|
|
33223
|
+
return hit.verdict;
|
|
33224
|
+
}
|
|
33225
|
+
function recordAuthVerdict(bearer, verdict, now = Date.now()) {
|
|
33226
|
+
if (authVerdicts.size >= AUTH_CACHE_MAX) {
|
|
33227
|
+
for (const [k, v] of authVerdicts) {
|
|
33228
|
+
if (v.expires <= now) authVerdicts.delete(k);
|
|
33229
|
+
}
|
|
33230
|
+
if (authVerdicts.size >= AUTH_CACHE_MAX) {
|
|
33231
|
+
const oldest = authVerdicts.keys().next();
|
|
33232
|
+
if (!oldest.done) authVerdicts.delete(oldest.value);
|
|
33233
|
+
}
|
|
33234
|
+
}
|
|
33235
|
+
const ttl = verdict === "valid" ? AUTH_VALID_TTL_MS : AUTH_REJECTED_TTL_MS;
|
|
33236
|
+
authVerdicts.set(bearerKey(bearer), { verdict, expires: now + ttl });
|
|
33237
|
+
}
|
|
33238
|
+
function classifyAuthProbeStatus(status) {
|
|
33239
|
+
if (status === 401) return "rejected";
|
|
33240
|
+
if (status >= 200 && status < 300) return "valid";
|
|
33241
|
+
return void 0;
|
|
33242
|
+
}
|
|
32660
33243
|
function startHttpTransport(opts) {
|
|
32661
33244
|
const { port, host, baseConfig, pkgVersion: pkgVersion2, dataEndpoint } = opts;
|
|
32662
33245
|
const httpServer = createHttpServer((req, res) => {
|
|
@@ -32758,21 +33341,11 @@ function startHttpTransport(opts) {
|
|
|
32758
33341
|
ip,
|
|
32759
33342
|
status: 401
|
|
32760
33343
|
});
|
|
32761
|
-
|
|
32762
|
-
res,
|
|
32763
|
-
{
|
|
32764
|
-
status: 401,
|
|
32765
|
-
body: {
|
|
32766
|
-
error: "Unauthorized",
|
|
32767
|
-
reason: hasHeader ? "malformed_bearer" : "missing_bearer"
|
|
32768
|
-
}
|
|
32769
|
-
},
|
|
32770
|
-
{
|
|
32771
|
-
"WWW-Authenticate": `Bearer realm="papi", resource_metadata="${RESOURCE_METADATA_URL}"`
|
|
32772
|
-
}
|
|
32773
|
-
);
|
|
33344
|
+
sendUnauthorized(res, hasHeader ? "malformed_bearer" : "missing_bearer");
|
|
32774
33345
|
return;
|
|
32775
33346
|
}
|
|
33347
|
+
const clientHeader = req.headers["x-papi-client"];
|
|
33348
|
+
const installClient = typeof clientHeader === "string" && KNOWN_INSTALL_CLIENTS.has(clientHeader) ? clientHeader : "direct";
|
|
32776
33349
|
const projectIdHeader = req.headers["x-papi-project-id"];
|
|
32777
33350
|
const projectId = typeof projectIdHeader === "string" && projectIdHeader.length > 0 ? projectIdHeader : void 0;
|
|
32778
33351
|
if (req.method !== "POST" && req.method !== "GET") {
|
|
@@ -32815,6 +33388,13 @@ function startHttpTransport(opts) {
|
|
|
32815
33388
|
return;
|
|
32816
33389
|
}
|
|
32817
33390
|
}
|
|
33391
|
+
logEvent({
|
|
33392
|
+
level: "info",
|
|
33393
|
+
msg: "mcp_request",
|
|
33394
|
+
ip,
|
|
33395
|
+
bearer_prefix: bearerPrefix(bearer),
|
|
33396
|
+
install_client: installClient
|
|
33397
|
+
});
|
|
32818
33398
|
void dispatchRequest({
|
|
32819
33399
|
req,
|
|
32820
33400
|
res,
|
|
@@ -32895,6 +33475,18 @@ Example: add \`project="${projects[0].slug}"\` to the tool arguments.`;
|
|
|
32895
33475
|
res.writeHead(200, { "Content-Type": "application/json", ...corsHeaders(origin) });
|
|
32896
33476
|
res.end(JSON.stringify(payload));
|
|
32897
33477
|
}
|
|
33478
|
+
function sendProjectUnverifiable(res, origin, body) {
|
|
33479
|
+
if (res.headersSent) return;
|
|
33480
|
+
const id = (body && typeof body === "object" ? body.id : null) ?? null;
|
|
33481
|
+
const text = 'PAPI couldn\'t verify which project this call belongs to \u2014 the project lookup failed, so it is stopping rather than guessing and writing to the wrong project.\n\nRetry in a moment. If it keeps happening, name the project explicitly with `project="<slug>"` in the tool arguments, or set the x-papi-project-id header.';
|
|
33482
|
+
const payload = {
|
|
33483
|
+
jsonrpc: "2.0",
|
|
33484
|
+
id,
|
|
33485
|
+
result: { content: [{ type: "text", text }], isError: true }
|
|
33486
|
+
};
|
|
33487
|
+
res.writeHead(200, { "Content-Type": "application/json", ...corsHeaders(origin) });
|
|
33488
|
+
res.end(JSON.stringify(payload));
|
|
33489
|
+
}
|
|
32898
33490
|
function resolveEffectiveProjectId(body, headerProjectId) {
|
|
32899
33491
|
const explicitProject = extractProjectOverride(body);
|
|
32900
33492
|
try {
|
|
@@ -32904,17 +33496,59 @@ function resolveEffectiveProjectId(body, headerProjectId) {
|
|
|
32904
33496
|
throw err;
|
|
32905
33497
|
}
|
|
32906
33498
|
}
|
|
33499
|
+
async function resolveAuthVerdict(bearer, dataEndpoint) {
|
|
33500
|
+
const cached2 = readAuthVerdict(bearer);
|
|
33501
|
+
if (cached2) return cached2;
|
|
33502
|
+
const probe = new ProxyPapiAdapter({ endpoint: dataEndpoint, apiKey: bearer });
|
|
33503
|
+
const verdict = classifyAuthProbeStatus(await probe.probeBearerStatus());
|
|
33504
|
+
if (verdict) recordAuthVerdict(bearer, verdict);
|
|
33505
|
+
return verdict;
|
|
33506
|
+
}
|
|
32907
33507
|
async function dispatchRequest(args) {
|
|
32908
33508
|
const { req, res, body, bearer, projectId, ip, baseConfig, dataEndpoint } = args;
|
|
33509
|
+
const calledTool = extractToolName(body);
|
|
33510
|
+
if (calledTool !== void 0) {
|
|
33511
|
+
const authVerdict = await resolveAuthVerdict(bearer, dataEndpoint);
|
|
33512
|
+
if (authVerdict === "rejected") {
|
|
33513
|
+
logEvent({
|
|
33514
|
+
level: "warn",
|
|
33515
|
+
msg: "auth_revoked",
|
|
33516
|
+
ip,
|
|
33517
|
+
bearer_prefix: bearerPrefix(bearer),
|
|
33518
|
+
status: 401,
|
|
33519
|
+
reason: "proxy_rejected_bearer"
|
|
33520
|
+
});
|
|
33521
|
+
if (!res.headersSent) {
|
|
33522
|
+
sendUnauthorized(res, "invalid_token", corsHeaders(req.headers.origin));
|
|
33523
|
+
}
|
|
33524
|
+
return;
|
|
33525
|
+
}
|
|
33526
|
+
}
|
|
32909
33527
|
let effectiveProjectId = resolveEffectiveProjectId(body, projectId);
|
|
32910
33528
|
if (effectiveProjectId === void 0) {
|
|
32911
|
-
const toolName =
|
|
33529
|
+
const toolName = calledTool;
|
|
32912
33530
|
if (toolName && !PROJECT_OPTIONAL_TOOLS.has(toolName)) {
|
|
32913
33531
|
let projects = [];
|
|
33532
|
+
let probeFailed = false;
|
|
32914
33533
|
try {
|
|
32915
|
-
const probe = new ProxyPapiAdapter({
|
|
33534
|
+
const probe = new ProxyPapiAdapter({
|
|
33535
|
+
endpoint: dataEndpoint,
|
|
33536
|
+
apiKey: bearer,
|
|
33537
|
+
onAuthRejected: () => recordAuthVerdict(bearer, "rejected")
|
|
33538
|
+
});
|
|
32916
33539
|
projects = await probe.listUserProjects();
|
|
32917
33540
|
} catch {
|
|
33541
|
+
probeFailed = true;
|
|
33542
|
+
}
|
|
33543
|
+
if (probeFailed) {
|
|
33544
|
+
logEvent({
|
|
33545
|
+
level: "warn",
|
|
33546
|
+
msg: "project_probe_failed",
|
|
33547
|
+
ip,
|
|
33548
|
+
bearer_prefix: bearerPrefix(bearer)
|
|
33549
|
+
});
|
|
33550
|
+
sendProjectUnverifiable(res, req.headers.origin, body);
|
|
33551
|
+
return;
|
|
32918
33552
|
}
|
|
32919
33553
|
if (projects.length === 1) {
|
|
32920
33554
|
effectiveProjectId = projects[0].id;
|
|
@@ -32937,10 +33571,14 @@ async function dispatchRequest(args) {
|
|
|
32937
33571
|
}
|
|
32938
33572
|
}
|
|
32939
33573
|
}
|
|
32940
|
-
const adapter2 =
|
|
33574
|
+
const adapter2 = createProxyAdapter({
|
|
32941
33575
|
endpoint: dataEndpoint,
|
|
32942
33576
|
apiKey: bearer,
|
|
32943
|
-
projectId: effectiveProjectId
|
|
33577
|
+
projectId: effectiveProjectId,
|
|
33578
|
+
// task-1773: a 401 raised mid-tool-call cannot change THIS response — the MCP
|
|
33579
|
+
// transport already owns it — but it marks the bearer so the very next request
|
|
33580
|
+
// short-circuits to a 401 + WWW-Authenticate and the client re-authenticates.
|
|
33581
|
+
onAuthRejected: () => recordAuthVerdict(bearer, "rejected")
|
|
32944
33582
|
});
|
|
32945
33583
|
const requestConfig = {
|
|
32946
33584
|
...baseConfig,
|
|
@@ -32988,7 +33626,7 @@ async function dispatchRequest(args) {
|
|
|
32988
33626
|
var __dirname = dirname7(fileURLToPath5(import.meta.url));
|
|
32989
33627
|
var pkgVersion = "unknown";
|
|
32990
33628
|
try {
|
|
32991
|
-
const pkg = JSON.parse(readFileSync19(
|
|
33629
|
+
const pkg = JSON.parse(readFileSync19(join26(__dirname, "..", "package.json"), "utf-8"));
|
|
32992
33630
|
pkgVersion = pkg.version;
|
|
32993
33631
|
} catch {
|
|
32994
33632
|
}
|