@papi-ai/server 0.7.38 → 0.7.40
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 +56 -0
- package/dist/index.js +240 -31
- package/package.json +1 -1
|
@@ -43,6 +43,7 @@ __export(git_exports, {
|
|
|
43
43
|
getStagedFiles: () => getStagedFiles,
|
|
44
44
|
getTagTarget: () => getTagTarget,
|
|
45
45
|
getTaskIdsOnBranch: () => getTaskIdsOnBranch,
|
|
46
|
+
getTrackedModifiedFiles: () => getTrackedModifiedFiles,
|
|
46
47
|
getUnmergedBranches: () => getUnmergedBranches,
|
|
47
48
|
getUntrackedFiles: () => getUntrackedFiles,
|
|
48
49
|
gitPull: () => gitPull,
|
|
@@ -175,6 +176,25 @@ function getModifiedFiles(cwd) {
|
|
|
175
176
|
return [];
|
|
176
177
|
}
|
|
177
178
|
}
|
|
179
|
+
function getTrackedModifiedFiles(cwd) {
|
|
180
|
+
try {
|
|
181
|
+
const out = execFileSync("git", ["status", "--porcelain"], {
|
|
182
|
+
cwd,
|
|
183
|
+
encoding: "utf-8"
|
|
184
|
+
}).replace(/\n+$/, "");
|
|
185
|
+
if (!out) return [];
|
|
186
|
+
const paths = /* @__PURE__ */ new Set();
|
|
187
|
+
for (const line of out.split("\n")) {
|
|
188
|
+
if (line.length < 4) continue;
|
|
189
|
+
if (line.startsWith("??")) continue;
|
|
190
|
+
const path3 = line.slice(3).trim();
|
|
191
|
+
if (path3) paths.add(path3);
|
|
192
|
+
}
|
|
193
|
+
return Array.from(paths);
|
|
194
|
+
} catch {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
178
198
|
function getUntrackedFiles(cwd) {
|
|
179
199
|
try {
|
|
180
200
|
const out = execFileSync("git", ["ls-files", "--others", "--exclude-standard"], {
|
|
@@ -1324,6 +1344,33 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
1324
1344
|
appendToolMetric(metric) {
|
|
1325
1345
|
return this.invoke("appendToolMetric", [metric]);
|
|
1326
1346
|
}
|
|
1347
|
+
/**
|
|
1348
|
+
* task-2288 (C308): emit a telemetry event via the proxy's /telemetry endpoint
|
|
1349
|
+
* using THIS adapter's per-request bearer (this.apiKey) — the same credential
|
|
1350
|
+
* invoke() uses. This is the fix for the hosted-transport blackout: on the
|
|
1351
|
+
* multi-tenant Railway pod there is no PAPI_DATA_API_KEY env var, so the
|
|
1352
|
+
* env-var emitTelemetryEvent path (lib/telemetry.ts) dropped every event since
|
|
1353
|
+
* 2026-06-16. Fire-and-forget — never throws, never blocks the tool response.
|
|
1354
|
+
* Not routed through invoke() because /telemetry is a distinct endpoint and a
|
|
1355
|
+
* telemetry failure must never surface as a tool error.
|
|
1356
|
+
*/
|
|
1357
|
+
emitTelemetry(event) {
|
|
1358
|
+
fetch(`${this.endpoint}/telemetry`, {
|
|
1359
|
+
method: "POST",
|
|
1360
|
+
headers: {
|
|
1361
|
+
"Content-Type": "application/json",
|
|
1362
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
1363
|
+
},
|
|
1364
|
+
body: JSON.stringify({
|
|
1365
|
+
projectId: event.projectId,
|
|
1366
|
+
toolName: event.toolName,
|
|
1367
|
+
eventType: event.eventType,
|
|
1368
|
+
metadata: event.metadata ?? {}
|
|
1369
|
+
}),
|
|
1370
|
+
signal: AbortSignal.timeout(5e3)
|
|
1371
|
+
}).catch(() => {
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1327
1374
|
readToolMetrics() {
|
|
1328
1375
|
return this.invoke("readToolMetrics");
|
|
1329
1376
|
}
|
|
@@ -1475,6 +1522,15 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
1475
1522
|
submitBugReport(report) {
|
|
1476
1523
|
return this.invoke("submitBugReport", [report]);
|
|
1477
1524
|
}
|
|
1525
|
+
// task-2270: notify-back. The data-proxy ignores the client-supplied userId
|
|
1526
|
+
// and scopes to the bearer-validated caller (same pattern as the owner-action
|
|
1527
|
+
// counts), so a user can only ever read/mark their OWN resolved submissions.
|
|
1528
|
+
getUnnotifiedResolvedFeedback(userId) {
|
|
1529
|
+
return this.invoke("getUnnotifiedResolvedFeedback", [userId]);
|
|
1530
|
+
}
|
|
1531
|
+
markFeedbackNotified(ids, userId) {
|
|
1532
|
+
return this.invoke("markFeedbackNotified", [ids, userId]);
|
|
1533
|
+
}
|
|
1478
1534
|
// --- Doc Registry ---
|
|
1479
1535
|
registerDoc(entry) {
|
|
1480
1536
|
return this.invoke("registerDoc", [entry]);
|
package/dist/index.js
CHANGED
|
@@ -44,6 +44,7 @@ __export(git_exports, {
|
|
|
44
44
|
getStagedFiles: () => getStagedFiles,
|
|
45
45
|
getTagTarget: () => getTagTarget,
|
|
46
46
|
getTaskIdsOnBranch: () => getTaskIdsOnBranch,
|
|
47
|
+
getTrackedModifiedFiles: () => getTrackedModifiedFiles,
|
|
47
48
|
getUnmergedBranches: () => getUnmergedBranches,
|
|
48
49
|
getUntrackedFiles: () => getUntrackedFiles,
|
|
49
50
|
gitPull: () => gitPull,
|
|
@@ -176,6 +177,25 @@ function getModifiedFiles(cwd) {
|
|
|
176
177
|
return [];
|
|
177
178
|
}
|
|
178
179
|
}
|
|
180
|
+
function getTrackedModifiedFiles(cwd) {
|
|
181
|
+
try {
|
|
182
|
+
const out = execFileSync("git", ["status", "--porcelain"], {
|
|
183
|
+
cwd,
|
|
184
|
+
encoding: "utf-8"
|
|
185
|
+
}).replace(/\n+$/, "");
|
|
186
|
+
if (!out) return [];
|
|
187
|
+
const paths = /* @__PURE__ */ new Set();
|
|
188
|
+
for (const line of out.split("\n")) {
|
|
189
|
+
if (line.length < 4) continue;
|
|
190
|
+
if (line.startsWith("??")) continue;
|
|
191
|
+
const path7 = line.slice(3).trim();
|
|
192
|
+
if (path7) paths.add(path7);
|
|
193
|
+
}
|
|
194
|
+
return Array.from(paths);
|
|
195
|
+
} catch {
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
}
|
|
179
199
|
function getUntrackedFiles(cwd) {
|
|
180
200
|
try {
|
|
181
201
|
const out = execFileSync("git", ["ls-files", "--others", "--exclude-standard"], {
|
|
@@ -1325,6 +1345,33 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
1325
1345
|
appendToolMetric(metric) {
|
|
1326
1346
|
return this.invoke("appendToolMetric", [metric]);
|
|
1327
1347
|
}
|
|
1348
|
+
/**
|
|
1349
|
+
* task-2288 (C308): emit a telemetry event via the proxy's /telemetry endpoint
|
|
1350
|
+
* using THIS adapter's per-request bearer (this.apiKey) — the same credential
|
|
1351
|
+
* invoke() uses. This is the fix for the hosted-transport blackout: on the
|
|
1352
|
+
* multi-tenant Railway pod there is no PAPI_DATA_API_KEY env var, so the
|
|
1353
|
+
* env-var emitTelemetryEvent path (lib/telemetry.ts) dropped every event since
|
|
1354
|
+
* 2026-06-16. Fire-and-forget — never throws, never blocks the tool response.
|
|
1355
|
+
* Not routed through invoke() because /telemetry is a distinct endpoint and a
|
|
1356
|
+
* telemetry failure must never surface as a tool error.
|
|
1357
|
+
*/
|
|
1358
|
+
emitTelemetry(event) {
|
|
1359
|
+
fetch(`${this.endpoint}/telemetry`, {
|
|
1360
|
+
method: "POST",
|
|
1361
|
+
headers: {
|
|
1362
|
+
"Content-Type": "application/json",
|
|
1363
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
1364
|
+
},
|
|
1365
|
+
body: JSON.stringify({
|
|
1366
|
+
projectId: event.projectId,
|
|
1367
|
+
toolName: event.toolName,
|
|
1368
|
+
eventType: event.eventType,
|
|
1369
|
+
metadata: event.metadata ?? {}
|
|
1370
|
+
}),
|
|
1371
|
+
signal: AbortSignal.timeout(5e3)
|
|
1372
|
+
}).catch(() => {
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1328
1375
|
readToolMetrics() {
|
|
1329
1376
|
return this.invoke("readToolMetrics");
|
|
1330
1377
|
}
|
|
@@ -1476,6 +1523,15 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
1476
1523
|
submitBugReport(report) {
|
|
1477
1524
|
return this.invoke("submitBugReport", [report]);
|
|
1478
1525
|
}
|
|
1526
|
+
// task-2270: notify-back. The data-proxy ignores the client-supplied userId
|
|
1527
|
+
// and scopes to the bearer-validated caller (same pattern as the owner-action
|
|
1528
|
+
// counts), so a user can only ever read/mark their OWN resolved submissions.
|
|
1529
|
+
getUnnotifiedResolvedFeedback(userId) {
|
|
1530
|
+
return this.invoke("getUnnotifiedResolvedFeedback", [userId]);
|
|
1531
|
+
}
|
|
1532
|
+
markFeedbackNotified(ids, userId) {
|
|
1533
|
+
return this.invoke("markFeedbackNotified", [ids, userId]);
|
|
1534
|
+
}
|
|
1479
1535
|
// --- Doc Registry ---
|
|
1480
1536
|
registerDoc(entry) {
|
|
1481
1537
|
return this.invoke("registerDoc", [entry]);
|
|
@@ -12583,6 +12639,7 @@ function formatPlanResult(result) {
|
|
|
12583
12639
|
);
|
|
12584
12640
|
}
|
|
12585
12641
|
const lines = [];
|
|
12642
|
+
if (result.projectBanner) lines.push(`> ${result.projectBanner}`, "");
|
|
12586
12643
|
lines.push(`${result.strategyReviewWarning}${pullLine}**${modeLabel} Mode \u2014 ${cycleLabel}**`);
|
|
12587
12644
|
if (result.writeSummary) {
|
|
12588
12645
|
const ws = result.writeSummary;
|
|
@@ -12692,7 +12749,9 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
12692
12749
|
source: "mcp-server",
|
|
12693
12750
|
confirmCancellations: args.confirm_cancellations === true
|
|
12694
12751
|
}, tracker);
|
|
12695
|
-
const
|
|
12752
|
+
const planProjectInfo = adapter2.getProjectInfo ? await adapter2.getProjectInfo().catch(() => null) : null;
|
|
12753
|
+
const projectBanner = planProjectInfo ? getProjectConnectionBanner(planProjectInfo.name, planProjectInfo.slug) ?? void 0 : void 0;
|
|
12754
|
+
const response = formatPlanResult({ ...result, contextUtilisation: utilisation, contextBytes, skipHandoffs, projectBanner });
|
|
12696
12755
|
return {
|
|
12697
12756
|
...response,
|
|
12698
12757
|
...contextBytes !== void 0 ? { _contextBytes: contextBytes } : {},
|
|
@@ -16335,6 +16394,9 @@ function substitute(template, vars) {
|
|
|
16335
16394
|
}
|
|
16336
16395
|
return result;
|
|
16337
16396
|
}
|
|
16397
|
+
function shouldWriteClaudeMd(clientName) {
|
|
16398
|
+
return !clientName || /claude/i.test(clientName);
|
|
16399
|
+
}
|
|
16338
16400
|
async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
16339
16401
|
const isPg = config2.adapterType === "pg" || config2.adapterType === "proxy";
|
|
16340
16402
|
const vars = {
|
|
@@ -16400,9 +16462,9 @@ async function scaffoldPapiDir(adapter2, config2, input, collector) {
|
|
|
16400
16462
|
if (!docsIndexExists) {
|
|
16401
16463
|
scaffoldFiles[docsIndexPath] = substitute(DOCS_INDEX_TEMPLATE, vars);
|
|
16402
16464
|
}
|
|
16403
|
-
if (!claudeMdExists) {
|
|
16465
|
+
if (!claudeMdExists && shouldWriteClaudeMd(input.clientName)) {
|
|
16404
16466
|
scaffoldFiles[claudeMdPath] = substitute(CLAUDE_MD_STUB, vars);
|
|
16405
|
-
} else {
|
|
16467
|
+
} else if (claudeMdExists) {
|
|
16406
16468
|
try {
|
|
16407
16469
|
const existing = await readFile4(claudeMdPath, "utf-8");
|
|
16408
16470
|
if (!existing.includes("AGENTS.md")) {
|
|
@@ -16581,7 +16643,7 @@ async function applySetupOutputs(adapter2, config2, input, collector, briefText,
|
|
|
16581
16643
|
}
|
|
16582
16644
|
}
|
|
16583
16645
|
}
|
|
16584
|
-
if (conventionsText?.trim()) {
|
|
16646
|
+
if (conventionsText?.trim() && shouldWriteClaudeMd(input.clientName)) {
|
|
16585
16647
|
const conventionsBlock = `${CONVENTIONS_SENTINEL}
|
|
16586
16648
|
${conventionsText.trim()}
|
|
16587
16649
|
`;
|
|
@@ -17026,7 +17088,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
|
|
|
17026
17088
|
}
|
|
17027
17089
|
}
|
|
17028
17090
|
}
|
|
17029
|
-
{
|
|
17091
|
+
if (shouldWriteClaudeMd(input.clientName)) {
|
|
17030
17092
|
const dogfoodSection = [
|
|
17031
17093
|
"",
|
|
17032
17094
|
"## Dogfood Logging",
|
|
@@ -17260,7 +17322,8 @@ function extractInput(args) {
|
|
|
17260
17322
|
codebaseScan: args.codebase_scan && typeof args.codebase_scan === "object" ? args.codebase_scan : void 0
|
|
17261
17323
|
};
|
|
17262
17324
|
}
|
|
17263
|
-
function formatSuccessResponse(result, constraints) {
|
|
17325
|
+
function formatSuccessResponse(result, constraints, writesClaudeMd = true) {
|
|
17326
|
+
const harnessFiles = writesClaudeMd ? "AGENTS.md, CLAUDE.md" : "AGENTS.md";
|
|
17264
17327
|
const prefix = result.createdProject ? `PAPI project "${result.projectName}" initialised and ` : "";
|
|
17265
17328
|
const briefRegenNote = result.briefRegenerated ? `
|
|
17266
17329
|
|
|
@@ -17277,7 +17340,7 @@ ${result.seededAds} Active Decision${result.seededAds > 1 ? "s" : ""} seeded bas
|
|
|
17277
17340
|
${[created, skipped].filter(Boolean).join(", ")}.${idea}`;
|
|
17278
17341
|
})() : '\n\nNo starter tasks yet. Describe what you want to build, or seed the board with `idea "<what you want to build>"`, so your first `plan` has something to work from.';
|
|
17279
17342
|
const constraintsHint = !constraints ? '\n\nTip: consider adding `constraints` (e.g. "must use PostgreSQL", "HIPAA compliant", "offline-first") to improve Active Decision seeding.' : "";
|
|
17280
|
-
const editorNote = result.cursorScaffolded ? "\n\nCursor detected \u2014 `.cursor/rules/papi.mdc` scaffolded alongside
|
|
17343
|
+
const editorNote = result.cursorScaffolded ? "\n\nCursor detected \u2014 `.cursor/rules/papi.mdc` scaffolded alongside your `AGENTS.md` harness." : "";
|
|
17281
17344
|
const gitignoreNote = result.gitignoreNote ? `
|
|
17282
17345
|
|
|
17283
17346
|
\u{1F512} ${result.gitignoreNote}` : "";
|
|
@@ -17289,7 +17352,7 @@ ${result.warnings.map((w) => `- ${w}`).join("\n")}` : "";
|
|
|
17289
17352
|
return textResponse(
|
|
17290
17353
|
`${prefix}Product Brief generated and saved.${briefRegenNote}${adNote}${taskNote}${constraintsHint}${editorNote}${gitignoreNote}${warningsNote}
|
|
17291
17354
|
|
|
17292
|
-
**Important:** Setup created/modified files (
|
|
17355
|
+
**Important:** Setup created/modified files (${harnessFiles}, .claude/settings.json, docs/). Commit these changes before running \`build_execute\` \u2014 it requires a clean working directory.
|
|
17293
17356
|
|
|
17294
17357
|
Tip: See \`docs/templates/example-project-brief.md\` for an example of a well-written brief.
|
|
17295
17358
|
|
|
@@ -17298,7 +17361,7 @@ Tip: See \`docs/templates/example-project-brief.md\` for an example of a well-wr
|
|
|
17298
17361
|
Next step: run \`plan\` to start your first planning cycle.${filesToWriteSection}`
|
|
17299
17362
|
);
|
|
17300
17363
|
}
|
|
17301
|
-
async function handleSetup(adapter2, config2, args) {
|
|
17364
|
+
async function handleSetup(adapter2, config2, args, clientName) {
|
|
17302
17365
|
const toolMode = args.mode;
|
|
17303
17366
|
const REQUIRED_FIELDS = ["project_name"];
|
|
17304
17367
|
const missing = REQUIRED_FIELDS.filter((f) => !args[f] || typeof args[f] === "string" && !args[f].trim());
|
|
@@ -17310,6 +17373,8 @@ PAPI needs the project name. Description and target users are optional \u2014 th
|
|
|
17310
17373
|
);
|
|
17311
17374
|
}
|
|
17312
17375
|
const input = extractInput(args);
|
|
17376
|
+
input.clientName = clientName;
|
|
17377
|
+
const writesClaudeMd = !clientName || /claude/i.test(clientName);
|
|
17313
17378
|
const tracker = new ProgressTracker(toolMode === "apply" ? "apply_validate" : "prepare_validate");
|
|
17314
17379
|
try {
|
|
17315
17380
|
if (toolMode === "apply") {
|
|
@@ -17321,7 +17386,7 @@ PAPI needs the project name. Description and target users are optional \u2014 th
|
|
|
17321
17386
|
tracker.mark("apply_setup_writeback");
|
|
17322
17387
|
const result = await applySetup(adapter2, config2, input, briefResponse, adSeedResponse, conventionsResponse, initialTasksResponse);
|
|
17323
17388
|
tracker.mark("apply_format_response");
|
|
17324
|
-
return formatSuccessResponse(result, args.constraints);
|
|
17389
|
+
return formatSuccessResponse(result, args.constraints, writesClaudeMd);
|
|
17325
17390
|
}
|
|
17326
17391
|
{
|
|
17327
17392
|
tracker.mark("prepare_setup");
|
|
@@ -17922,6 +17987,64 @@ To override, pass force=true (emits a telemetry warning).`
|
|
|
17922
17987
|
}
|
|
17923
17988
|
return { resolvedCycleNum, warnings, force, skipVersion };
|
|
17924
17989
|
}
|
|
17990
|
+
async function contributorAutoPrRelease(config2, adapter2, version, productionBaseBranch, options) {
|
|
17991
|
+
if (!isGhAvailable()) {
|
|
17992
|
+
throw new Error(
|
|
17993
|
+
`Release blocked \u2014 opening a release PR needs the GitHub CLI (\`gh\`) authenticated locally. Install and \`gh auth login\`, then re-run \`release\`. (As a contributor you cannot release to ${productionBaseBranch} directly; the owner merges your PR.)`
|
|
17994
|
+
);
|
|
17995
|
+
}
|
|
17996
|
+
const cycleNum = await resolveCycleToClose(adapter2, version, options?.callerUserId);
|
|
17997
|
+
const branches = listGroupedCycleBranches(config2.projectRoot, cycleNum, productionBaseBranch);
|
|
17998
|
+
if (branches.length === 0) {
|
|
17999
|
+
throw new Error(
|
|
18000
|
+
`Release blocked \u2014 no cycle branch found to open a PR from (cycle ${cycleNum || "?"}). Make sure your work is committed on a \`feat/cycle-\u2026\` branch and pushed, then re-run \`release\`.`
|
|
18001
|
+
);
|
|
18002
|
+
}
|
|
18003
|
+
const prs = [];
|
|
18004
|
+
for (const branch of branches) {
|
|
18005
|
+
const push = gitPush(config2.projectRoot, branch);
|
|
18006
|
+
if (!push.success) {
|
|
18007
|
+
prs.push({ branch, url: null, error: `push failed: ${push.message}` });
|
|
18008
|
+
continue;
|
|
18009
|
+
}
|
|
18010
|
+
const existing = getPullRequestUrl(config2.projectRoot, branch);
|
|
18011
|
+
if (existing) {
|
|
18012
|
+
prs.push({ branch, url: existing });
|
|
18013
|
+
continue;
|
|
18014
|
+
}
|
|
18015
|
+
const title = `Release ${version}: ${branch}`;
|
|
18016
|
+
const body = `Contributor release PR for cycle ${cycleNum || "?"} (\`${branch}\`).
|
|
18017
|
+
|
|
18018
|
+
Opened by a non-owner editor via \`release\` (task-2244). The project owner reviews and merges this into \`${productionBaseBranch}\`; the contributor's own cycle is already marked complete in PAPI.`;
|
|
18019
|
+
const created = createPullRequest(config2.projectRoot, branch, productionBaseBranch, title, body);
|
|
18020
|
+
prs.push(
|
|
18021
|
+
created.success ? { branch, url: created.message.trim() || getPullRequestUrl(config2.projectRoot, branch) } : { branch, url: null, error: created.message }
|
|
18022
|
+
);
|
|
18023
|
+
}
|
|
18024
|
+
if (!prs.some((p) => p.url)) {
|
|
18025
|
+
const detail = prs.map((p) => `${p.branch}: ${p.error ?? "no PR url returned"}`).join("; ");
|
|
18026
|
+
throw new Error(`Release blocked \u2014 could not open any release PR. ${detail}`);
|
|
18027
|
+
}
|
|
18028
|
+
if (adapter2?.recordContributorReleasePr) {
|
|
18029
|
+
for (const p of prs) {
|
|
18030
|
+
if (!p.url) continue;
|
|
18031
|
+
try {
|
|
18032
|
+
await adapter2.recordContributorReleasePr({
|
|
18033
|
+
prUrl: p.url,
|
|
18034
|
+
branch: p.branch,
|
|
18035
|
+
cycle: cycleNum > 0 ? cycleNum : null,
|
|
18036
|
+
contributorUserId: options?.callerUserId ?? null
|
|
18037
|
+
});
|
|
18038
|
+
} catch (err) {
|
|
18039
|
+
console.error(
|
|
18040
|
+
`[release] recordContributorReleasePr failed for ${p.url} (non-blocking): ${err instanceof Error ? err.message : String(err)}`
|
|
18041
|
+
);
|
|
18042
|
+
}
|
|
18043
|
+
}
|
|
18044
|
+
}
|
|
18045
|
+
const closed = await closeCycleState(config2, adapter2, version, void 0, options);
|
|
18046
|
+
return { prs, cycleClosed: closed.resolvedCycleNum, warnings: closed.warnings };
|
|
18047
|
+
}
|
|
17925
18048
|
async function createRelease(config2, branch, version, adapter2, cycleNum, options) {
|
|
17926
18049
|
const collector = new FileWriteCollector();
|
|
17927
18050
|
if (!isGitAvailable()) {
|
|
@@ -18296,11 +18419,50 @@ async function handleRelease(adapter2, config2, args) {
|
|
|
18296
18419
|
const resolutionNote = gate.resolutionError ? `
|
|
18297
18420
|
|
|
18298
18421
|
Identity resolution failed (${gate.resolutionError}) \u2014 the gate fails closed. Retry once connectivity is restored.` : "";
|
|
18299
|
-
|
|
18300
|
-
|
|
18422
|
+
tracker.mark("contributor-role-gate");
|
|
18423
|
+
const callerRole = adapter2.getContributorRole && gate.callerUserId ? await adapter2.getContributorRole(gate.callerUserId).catch(() => null) : null;
|
|
18424
|
+
if (callerRole !== "editor") {
|
|
18425
|
+
const roleNote = callerRole === "viewer" ? `Your role on this project is "viewer", which cannot release. Ask the owner (or an editor) to release, or ask for editor access.` : `Your identity does not match this project's owner, and you do not have an editor role to open a release PR. If you are a contributor, ask the owner for editor access (or push your branch and open a PR manually). If you ARE the owner on a local (pg) setup, set PAPI_USER_ID to your account UUID in .mcp.json; on the hosted/proxy setup your identity comes from your API key \u2014 check you are using YOUR key for YOUR project.`;
|
|
18426
|
+
return errorResponse(
|
|
18427
|
+
`Release to ${productionBaseBranch} is restricted to the project owner.
|
|
18301
18428
|
|
|
18302
|
-
|
|
18303
|
-
|
|
18429
|
+
` + roleNote + `
|
|
18430
|
+
|
|
18431
|
+
Releasing to a non-base branch (e.g. dev) is unaffected.` + resolutionNote
|
|
18432
|
+
);
|
|
18433
|
+
}
|
|
18434
|
+
tracker.mark("contributor-auto-pr");
|
|
18435
|
+
try {
|
|
18436
|
+
const cr = await contributorAutoPrRelease(config2, adapter2, version, productionBaseBranch, {
|
|
18437
|
+
force: force ?? false,
|
|
18438
|
+
callerUserId: gate.callerUserId
|
|
18439
|
+
});
|
|
18440
|
+
const opened = cr.prs.filter((p) => p.url);
|
|
18441
|
+
const failed = cr.prs.filter((p) => !p.url);
|
|
18442
|
+
const cyclePart = cr.cycleClosed > 0 ? `Cycle ${cr.cycleClosed}` : "Your cycle";
|
|
18443
|
+
const prLines = opened.map((p) => `- \`${p.branch}\` \u2192 ${p.url}`);
|
|
18444
|
+
const failLines = failed.map((p) => `- \`${p.branch}\` \u2014 \u26A0\uFE0F ${p.error ?? "no PR opened"}`);
|
|
18445
|
+
const warnBlock = cr.warnings.length > 0 ? `
|
|
18446
|
+
\u26A0\uFE0F Warnings: ${cr.warnings.join("; ")}
|
|
18447
|
+
` : "";
|
|
18448
|
+
return textResponse(
|
|
18449
|
+
`## Release ${version} \u2014 PR opened for owner review
|
|
18450
|
+
|
|
18451
|
+
You released as an **editor**, so PAPI opened a pull request to \`${productionBaseBranch}\` instead of merging directly. The owner reviews and merges it.
|
|
18452
|
+
|
|
18453
|
+
**Pull request(s):**
|
|
18454
|
+
${prLines.join("\n")}
|
|
18455
|
+
` + (failLines.length > 0 ? `
|
|
18456
|
+
**Could not open:**
|
|
18457
|
+
${failLines.join("\n")}
|
|
18458
|
+
` : "") + warnBlock + `
|
|
18459
|
+
${cyclePart} is now marked **complete** in PAPI \u2014 your work is shipped from your side. Any changes the owner requests come forward as a fast-follow in your next cycle; your closed cycle is not rewound.
|
|
18460
|
+
|
|
18461
|
+
Next: run \`plan\` to start your next cycle.`
|
|
18462
|
+
);
|
|
18463
|
+
} catch (err) {
|
|
18464
|
+
return errorResponse(err instanceof Error ? err.message : String(err));
|
|
18465
|
+
}
|
|
18304
18466
|
}
|
|
18305
18467
|
}
|
|
18306
18468
|
if (isHostedTransport()) {
|
|
@@ -18998,6 +19160,18 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
|
|
|
18998
19160
|
branchLines.push(`Reusing shared cycle branch for ${task.complexity} ${task.module} task.`);
|
|
18999
19161
|
}
|
|
19000
19162
|
} else {
|
|
19163
|
+
const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
|
|
19164
|
+
if (baseBranch !== config2.baseBranch) {
|
|
19165
|
+
branchLines.push(`Base branch '${config2.baseBranch}' not found \u2014 using '${baseBranch}'.`);
|
|
19166
|
+
}
|
|
19167
|
+
const trackedDirty = getTrackedModifiedFiles(config2.projectRoot);
|
|
19168
|
+
if (trackedDirty.length > 0 && currentBranch !== baseBranch) {
|
|
19169
|
+
const shown = trackedDirty.slice(0, 5).join(", ");
|
|
19170
|
+
const more = trackedDirty.length > 5 ? ` (+${trackedDirty.length - 5} more)` : "";
|
|
19171
|
+
throw new Error(
|
|
19172
|
+
`build_execute won't switch branches: the working tree has uncommitted changes on '${currentBranch}' (this task targets '${featureBranch}'). Another session may be working there, and switching would revert those files. Commit or stash them first, or run build_execute from '${baseBranch}'. Dirty: ${shown}${more}.`
|
|
19173
|
+
);
|
|
19174
|
+
}
|
|
19001
19175
|
if (hasUncommittedChanges(config2.projectRoot, AUTO_WRITTEN_PATHS)) {
|
|
19002
19176
|
const { toStash, preservedDocs } = selectAutostashPaths(
|
|
19003
19177
|
getModifiedFiles(config2.projectRoot),
|
|
@@ -19026,10 +19200,6 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
|
|
|
19026
19200
|
}
|
|
19027
19201
|
}
|
|
19028
19202
|
}
|
|
19029
|
-
const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
|
|
19030
|
-
if (baseBranch !== config2.baseBranch) {
|
|
19031
|
-
branchLines.push(`Base branch '${config2.baseBranch}' not found \u2014 using '${baseBranch}'.`);
|
|
19032
|
-
}
|
|
19033
19203
|
const featureBranchExistsLocally = branchExists(config2.projectRoot, featureBranch);
|
|
19034
19204
|
const featureBranchOnOrigin = !!originCycleBranch && originCycleBranch === featureBranch;
|
|
19035
19205
|
const featureBranchExists = featureBranchExistsLocally || featureBranchOnOrigin;
|
|
@@ -19991,7 +20161,12 @@ async function handleBuildExecute(adapter2, config2, args) {
|
|
|
19991
20161
|
tracker.mark("start_decorate_handoff");
|
|
19992
20162
|
const branchInfo = result.branchLines.length > 0 ? result.branchLines.map((l) => `> ${l}`).join("\n") + "\n\n" : "";
|
|
19993
20163
|
const phaseNote = result.phaseChanges.length > 0 ? "\n\n" + result.phaseChanges.map((c) => `Phase auto-updated: ${c.phaseId} ${c.oldStatus} \u2192 ${c.newStatus}`).join("\n") : "";
|
|
19994
|
-
const
|
|
20164
|
+
const projectInfo = adapter2.getProjectInfo ? await adapter2.getProjectInfo().catch(() => null) : null;
|
|
20165
|
+
const projectBanner = projectInfo ? getProjectConnectionBanner(projectInfo.name, projectInfo.slug) : null;
|
|
20166
|
+
const projectLine = projectBanner ? `> ${projectBanner}
|
|
20167
|
+
|
|
20168
|
+
` : "";
|
|
20169
|
+
const header = `${projectLine}${branchInfo}**Executing ${result.task.id}: ${result.task.title}** \u2014 marked In Progress.
|
|
19995
20170
|
|
|
19996
20171
|
---
|
|
19997
20172
|
|
|
@@ -23919,7 +24094,7 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
|
|
|
23919
24094
|
const isGitDependentRec = /\*\*(Build|Review)\*\*/.test(health.recommendedMode);
|
|
23920
24095
|
if (GIT_DEPENDENT_ENVS.has(environment) && isGitDependentRec) {
|
|
23921
24096
|
lines.push(`> **Next action:** ${health.recommendedMode}`);
|
|
23922
|
-
lines.push(`> _Note: \`build_execute\`, \`review_submit\`, and \`release\` require a local
|
|
24097
|
+
lines.push(`> _Note: \`build_execute\`, \`review_submit\`, and \`release\` require a local session in your AI coding tool with git access. From \`${environment}\` you can view state, log ideas, and edit the board \u2014 but the build/review/release loop must run through your local tool._`);
|
|
23923
24098
|
} else {
|
|
23924
24099
|
lines.push(`> **Next action:** ${health.recommendedMode}`);
|
|
23925
24100
|
}
|
|
@@ -24205,6 +24380,22 @@ async function computeReleaseHistory(adapter2) {
|
|
|
24205
24380
|
if (released.length === 0) return void 0;
|
|
24206
24381
|
return `**Recent releases:** ${released.join(" \xB7 ")}`;
|
|
24207
24382
|
}
|
|
24383
|
+
function formatResolvedFeedback(items) {
|
|
24384
|
+
if (items.length === 0) return "";
|
|
24385
|
+
const MAX = 5;
|
|
24386
|
+
const shown = items.slice(0, MAX);
|
|
24387
|
+
const lines = ["\n\n## \u2705 Your feedback shipped"];
|
|
24388
|
+
for (const it of shown) {
|
|
24389
|
+
const kind = it.type === "idea" ? "Idea" : "Bug";
|
|
24390
|
+
const verb = it.status === "wont_fix" ? "closed" : "resolved";
|
|
24391
|
+
const desc = it.description.length > 90 ? `${it.description.slice(0, 87)}\u2026` : it.description;
|
|
24392
|
+
const note = it.resolutionNote?.trim() ? ` \u2014 ${it.resolutionNote.trim()}` : "";
|
|
24393
|
+
lines.push(`- **${kind}** "${desc}" was ${verb}${note}`);
|
|
24394
|
+
}
|
|
24395
|
+
if (items.length > MAX) lines.push(`- \u2026and ${items.length - MAX} more`);
|
|
24396
|
+
lines.push("_Thanks for the signal \u2014 it shaped what shipped._");
|
|
24397
|
+
return lines.join("\n");
|
|
24398
|
+
}
|
|
24208
24399
|
async function handleOrient(adapter2, config2, args = {}) {
|
|
24209
24400
|
const environment = normaliseEnvironment(args.environment);
|
|
24210
24401
|
const deepHousekeeping = args.deep_housekeeping === true;
|
|
@@ -24249,6 +24440,7 @@ async function handleOrient(adapter2, config2, args = {}) {
|
|
|
24249
24440
|
backlogHandoffCount: allTasks.filter((t) => t.cycle !== currentCycle).length
|
|
24250
24441
|
};
|
|
24251
24442
|
const sharedActiveDecisionsPromise = adapter2.getActiveDecisions();
|
|
24443
|
+
const resolvedFeedbackPromise = adapter2.getUnnotifiedResolvedFeedback ? adapter2.getUnnotifiedResolvedFeedback(config2.userId).catch(() => []) : Promise.resolve([]);
|
|
24252
24444
|
const [
|
|
24253
24445
|
proxyVersionOutcome,
|
|
24254
24446
|
p1BacklogOutcome,
|
|
@@ -24627,6 +24819,12 @@ ${versionDrift}` : "";
|
|
|
24627
24819
|
const projectName = projectBannerResult.name;
|
|
24628
24820
|
const sessionGuidanceNote = sessionGuidanceOutcome.status === "fulfilled" ? sessionGuidanceOutcome.value : "";
|
|
24629
24821
|
const deliveryShapeNote = deliveryShapeOutcome.status === "fulfilled" ? deliveryShapeOutcome.value : "";
|
|
24822
|
+
const resolvedFeedback = await resolvedFeedbackPromise;
|
|
24823
|
+
const feedbackResolvedNote = formatResolvedFeedback(resolvedFeedback);
|
|
24824
|
+
if (resolvedFeedback.length > 0 && adapter2.markFeedbackNotified) {
|
|
24825
|
+
void adapter2.markFeedbackNotified(resolvedFeedback.map((f) => f.id), config2.userId).catch(() => {
|
|
24826
|
+
});
|
|
24827
|
+
}
|
|
24630
24828
|
const { reconciliationNote, unrecordedNote, unregisteredDocsNote, staleSkillsNote } = deepHousekeepingOutcome.status === "fulfilled" ? deepHousekeepingOutcome.value : { reconciliationNote: "", unrecordedNote: "", unregisteredDocsNote: "", staleSkillsNote: "" };
|
|
24631
24829
|
let enrichmentNote = "";
|
|
24632
24830
|
const enrichmentCollector = new FileWriteCollector();
|
|
@@ -24675,7 +24873,7 @@ ${section}`;
|
|
|
24675
24873
|
]);
|
|
24676
24874
|
const teamSummary = [teamSummaryLine, releaseHistoryLine].filter(Boolean).join("\n") || void 0;
|
|
24677
24875
|
const deepHint = deepHousekeeping ? "" : "\n\n*Tip: pass `full: true` for Research Signals + version-drift, or `deep_housekeeping: true` to also check orphaned branches, unrecorded commits, unregistered docs, and stale skill forks (implies `full`).*";
|
|
24678
|
-
return textResponse(projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents, projectName, teamSummary) + unblockNote + alertsNote + ttfvNote + reconciliationNote + unrecordedNote + unregisteredDocsNote + staleSkillsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + deepHint + enrichmentFilesSection);
|
|
24876
|
+
return textResponse(projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents, projectName, teamSummary) + unblockNote + feedbackResolvedNote + alertsNote + ttfvNote + reconciliationNote + unrecordedNote + unregisteredDocsNote + staleSkillsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + deepHint + enrichmentFilesSection);
|
|
24679
24877
|
} catch (err) {
|
|
24680
24878
|
const message = err instanceof Error ? err.message : String(err);
|
|
24681
24879
|
const isKnownFriendly = /^(Orient failed|Project not found|No project|Setup required)/i.test(message);
|
|
@@ -26717,7 +26915,7 @@ function createServer(adapter2, config2) {
|
|
|
26717
26915
|
case "board_edit":
|
|
26718
26916
|
return handleBoardEdit(adapter2, safeArgs);
|
|
26719
26917
|
case "setup":
|
|
26720
|
-
return handleSetup(adapter2, config2, safeArgs);
|
|
26918
|
+
return handleSetup(adapter2, config2, safeArgs, server2.getClientVersion()?.name);
|
|
26721
26919
|
case "build_list":
|
|
26722
26920
|
return handleBuildList(adapter2, config2);
|
|
26723
26921
|
case "build_describe":
|
|
@@ -26857,17 +27055,28 @@ ${usageLine(decision.usage)}`;
|
|
|
26857
27055
|
}
|
|
26858
27056
|
const telemetryProjectId = resolveTelemetryProjectId(config2);
|
|
26859
27057
|
if (telemetryProjectId) {
|
|
26860
|
-
|
|
26861
|
-
|
|
26862
|
-
|
|
27058
|
+
const adapterEmit = adapter2.emitTelemetry?.bind(adapter2) ?? null;
|
|
27059
|
+
if (adapterEmit) {
|
|
27060
|
+
adapterEmit({
|
|
27061
|
+
projectId: telemetryProjectId,
|
|
27062
|
+
toolName: name,
|
|
27063
|
+
eventType: "tool_call",
|
|
27064
|
+
metadata: { duration_ms: elapsed, adapter_type: config2.adapterType }
|
|
27065
|
+
});
|
|
27066
|
+
} else {
|
|
27067
|
+
emitToolCall(telemetryProjectId, name, elapsed, {
|
|
27068
|
+
adapter_type: config2.adapterType
|
|
27069
|
+
});
|
|
27070
|
+
}
|
|
26863
27071
|
const isApplyMode = safeArgs.mode === "apply";
|
|
26864
27072
|
if (!isError) {
|
|
26865
|
-
|
|
26866
|
-
|
|
26867
|
-
|
|
26868
|
-
|
|
26869
|
-
|
|
26870
|
-
|
|
27073
|
+
const milestone = name === "setup" && isApplyMode ? "setup_completed" : name === "plan" && isApplyMode ? "plan_completed" : name === "release" ? "release_completed" : null;
|
|
27074
|
+
if (milestone) {
|
|
27075
|
+
if (adapterEmit) {
|
|
27076
|
+
adapterEmit({ projectId: telemetryProjectId, toolName: milestone, eventType: "milestone" });
|
|
27077
|
+
} else {
|
|
27078
|
+
emitMilestone(telemetryProjectId, milestone);
|
|
27079
|
+
}
|
|
26871
27080
|
}
|
|
26872
27081
|
}
|
|
26873
27082
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@papi-ai/server",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.40",
|
|
4
4
|
"description": "PAPI MCP server — AI-powered sprint planning, build execution, and strategy review for software projects",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"mcpName": "io.github.cathalos92/papi",
|