@papi-ai/server 0.7.37 → 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 +244 -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
|
`;
|
|
@@ -17004,6 +17066,10 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
|
|
|
17004
17066
|
priority: task.priority || "P2 Medium",
|
|
17005
17067
|
complexity: task.complexity || "Small",
|
|
17006
17068
|
module: task.module || "Core",
|
|
17069
|
+
// epic is NOT NULL in cycle_tasks; default starter tasks to the
|
|
17070
|
+
// 'Platform' epic (matches the idea tool) rather than leaving it
|
|
17071
|
+
// unset (SUP-2026-020 — omitting it tripped a 23502 on every seed).
|
|
17072
|
+
epic: task.epic || "Platform",
|
|
17007
17073
|
phase: task.phase || "Phase 1",
|
|
17008
17074
|
owner: "TBD",
|
|
17009
17075
|
reviewed: false,
|
|
@@ -17022,7 +17088,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
|
|
|
17022
17088
|
}
|
|
17023
17089
|
}
|
|
17024
17090
|
}
|
|
17025
|
-
{
|
|
17091
|
+
if (shouldWriteClaudeMd(input.clientName)) {
|
|
17026
17092
|
const dogfoodSection = [
|
|
17027
17093
|
"",
|
|
17028
17094
|
"## Dogfood Logging",
|
|
@@ -17256,7 +17322,8 @@ function extractInput(args) {
|
|
|
17256
17322
|
codebaseScan: args.codebase_scan && typeof args.codebase_scan === "object" ? args.codebase_scan : void 0
|
|
17257
17323
|
};
|
|
17258
17324
|
}
|
|
17259
|
-
function formatSuccessResponse(result, constraints) {
|
|
17325
|
+
function formatSuccessResponse(result, constraints, writesClaudeMd = true) {
|
|
17326
|
+
const harnessFiles = writesClaudeMd ? "AGENTS.md, CLAUDE.md" : "AGENTS.md";
|
|
17260
17327
|
const prefix = result.createdProject ? `PAPI project "${result.projectName}" initialised and ` : "";
|
|
17261
17328
|
const briefRegenNote = result.briefRegenerated ? `
|
|
17262
17329
|
|
|
@@ -17273,7 +17340,7 @@ ${result.seededAds} Active Decision${result.seededAds > 1 ? "s" : ""} seeded bas
|
|
|
17273
17340
|
${[created, skipped].filter(Boolean).join(", ")}.${idea}`;
|
|
17274
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.';
|
|
17275
17342
|
const constraintsHint = !constraints ? '\n\nTip: consider adding `constraints` (e.g. "must use PostgreSQL", "HIPAA compliant", "offline-first") to improve Active Decision seeding.' : "";
|
|
17276
|
-
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." : "";
|
|
17277
17344
|
const gitignoreNote = result.gitignoreNote ? `
|
|
17278
17345
|
|
|
17279
17346
|
\u{1F512} ${result.gitignoreNote}` : "";
|
|
@@ -17285,7 +17352,7 @@ ${result.warnings.map((w) => `- ${w}`).join("\n")}` : "";
|
|
|
17285
17352
|
return textResponse(
|
|
17286
17353
|
`${prefix}Product Brief generated and saved.${briefRegenNote}${adNote}${taskNote}${constraintsHint}${editorNote}${gitignoreNote}${warningsNote}
|
|
17287
17354
|
|
|
17288
|
-
**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.
|
|
17289
17356
|
|
|
17290
17357
|
Tip: See \`docs/templates/example-project-brief.md\` for an example of a well-written brief.
|
|
17291
17358
|
|
|
@@ -17294,7 +17361,7 @@ Tip: See \`docs/templates/example-project-brief.md\` for an example of a well-wr
|
|
|
17294
17361
|
Next step: run \`plan\` to start your first planning cycle.${filesToWriteSection}`
|
|
17295
17362
|
);
|
|
17296
17363
|
}
|
|
17297
|
-
async function handleSetup(adapter2, config2, args) {
|
|
17364
|
+
async function handleSetup(adapter2, config2, args, clientName) {
|
|
17298
17365
|
const toolMode = args.mode;
|
|
17299
17366
|
const REQUIRED_FIELDS = ["project_name"];
|
|
17300
17367
|
const missing = REQUIRED_FIELDS.filter((f) => !args[f] || typeof args[f] === "string" && !args[f].trim());
|
|
@@ -17306,6 +17373,8 @@ PAPI needs the project name. Description and target users are optional \u2014 th
|
|
|
17306
17373
|
);
|
|
17307
17374
|
}
|
|
17308
17375
|
const input = extractInput(args);
|
|
17376
|
+
input.clientName = clientName;
|
|
17377
|
+
const writesClaudeMd = !clientName || /claude/i.test(clientName);
|
|
17309
17378
|
const tracker = new ProgressTracker(toolMode === "apply" ? "apply_validate" : "prepare_validate");
|
|
17310
17379
|
try {
|
|
17311
17380
|
if (toolMode === "apply") {
|
|
@@ -17317,7 +17386,7 @@ PAPI needs the project name. Description and target users are optional \u2014 th
|
|
|
17317
17386
|
tracker.mark("apply_setup_writeback");
|
|
17318
17387
|
const result = await applySetup(adapter2, config2, input, briefResponse, adSeedResponse, conventionsResponse, initialTasksResponse);
|
|
17319
17388
|
tracker.mark("apply_format_response");
|
|
17320
|
-
return formatSuccessResponse(result, args.constraints);
|
|
17389
|
+
return formatSuccessResponse(result, args.constraints, writesClaudeMd);
|
|
17321
17390
|
}
|
|
17322
17391
|
{
|
|
17323
17392
|
tracker.mark("prepare_setup");
|
|
@@ -17918,6 +17987,64 @@ To override, pass force=true (emits a telemetry warning).`
|
|
|
17918
17987
|
}
|
|
17919
17988
|
return { resolvedCycleNum, warnings, force, skipVersion };
|
|
17920
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
|
+
}
|
|
17921
18048
|
async function createRelease(config2, branch, version, adapter2, cycleNum, options) {
|
|
17922
18049
|
const collector = new FileWriteCollector();
|
|
17923
18050
|
if (!isGitAvailable()) {
|
|
@@ -18292,11 +18419,50 @@ async function handleRelease(adapter2, config2, args) {
|
|
|
18292
18419
|
const resolutionNote = gate.resolutionError ? `
|
|
18293
18420
|
|
|
18294
18421
|
Identity resolution failed (${gate.resolutionError}) \u2014 the gate fails closed. Retry once connectivity is restored.` : "";
|
|
18295
|
-
|
|
18296
|
-
|
|
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.
|
|
18297
18428
|
|
|
18298
|
-
|
|
18299
|
-
|
|
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
|
+
}
|
|
18300
18466
|
}
|
|
18301
18467
|
}
|
|
18302
18468
|
if (isHostedTransport()) {
|
|
@@ -18994,6 +19160,18 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
|
|
|
18994
19160
|
branchLines.push(`Reusing shared cycle branch for ${task.complexity} ${task.module} task.`);
|
|
18995
19161
|
}
|
|
18996
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
|
+
}
|
|
18997
19175
|
if (hasUncommittedChanges(config2.projectRoot, AUTO_WRITTEN_PATHS)) {
|
|
18998
19176
|
const { toStash, preservedDocs } = selectAutostashPaths(
|
|
18999
19177
|
getModifiedFiles(config2.projectRoot),
|
|
@@ -19022,10 +19200,6 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
|
|
|
19022
19200
|
}
|
|
19023
19201
|
}
|
|
19024
19202
|
}
|
|
19025
|
-
const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
|
|
19026
|
-
if (baseBranch !== config2.baseBranch) {
|
|
19027
|
-
branchLines.push(`Base branch '${config2.baseBranch}' not found \u2014 using '${baseBranch}'.`);
|
|
19028
|
-
}
|
|
19029
19203
|
const featureBranchExistsLocally = branchExists(config2.projectRoot, featureBranch);
|
|
19030
19204
|
const featureBranchOnOrigin = !!originCycleBranch && originCycleBranch === featureBranch;
|
|
19031
19205
|
const featureBranchExists = featureBranchExistsLocally || featureBranchOnOrigin;
|
|
@@ -19987,7 +20161,12 @@ async function handleBuildExecute(adapter2, config2, args) {
|
|
|
19987
20161
|
tracker.mark("start_decorate_handoff");
|
|
19988
20162
|
const branchInfo = result.branchLines.length > 0 ? result.branchLines.map((l) => `> ${l}`).join("\n") + "\n\n" : "";
|
|
19989
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") : "";
|
|
19990
|
-
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.
|
|
19991
20170
|
|
|
19992
20171
|
---
|
|
19993
20172
|
|
|
@@ -23915,7 +24094,7 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
|
|
|
23915
24094
|
const isGitDependentRec = /\*\*(Build|Review)\*\*/.test(health.recommendedMode);
|
|
23916
24095
|
if (GIT_DEPENDENT_ENVS.has(environment) && isGitDependentRec) {
|
|
23917
24096
|
lines.push(`> **Next action:** ${health.recommendedMode}`);
|
|
23918
|
-
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._`);
|
|
23919
24098
|
} else {
|
|
23920
24099
|
lines.push(`> **Next action:** ${health.recommendedMode}`);
|
|
23921
24100
|
}
|
|
@@ -24201,6 +24380,22 @@ async function computeReleaseHistory(adapter2) {
|
|
|
24201
24380
|
if (released.length === 0) return void 0;
|
|
24202
24381
|
return `**Recent releases:** ${released.join(" \xB7 ")}`;
|
|
24203
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
|
+
}
|
|
24204
24399
|
async function handleOrient(adapter2, config2, args = {}) {
|
|
24205
24400
|
const environment = normaliseEnvironment(args.environment);
|
|
24206
24401
|
const deepHousekeeping = args.deep_housekeeping === true;
|
|
@@ -24245,6 +24440,7 @@ async function handleOrient(adapter2, config2, args = {}) {
|
|
|
24245
24440
|
backlogHandoffCount: allTasks.filter((t) => t.cycle !== currentCycle).length
|
|
24246
24441
|
};
|
|
24247
24442
|
const sharedActiveDecisionsPromise = adapter2.getActiveDecisions();
|
|
24443
|
+
const resolvedFeedbackPromise = adapter2.getUnnotifiedResolvedFeedback ? adapter2.getUnnotifiedResolvedFeedback(config2.userId).catch(() => []) : Promise.resolve([]);
|
|
24248
24444
|
const [
|
|
24249
24445
|
proxyVersionOutcome,
|
|
24250
24446
|
p1BacklogOutcome,
|
|
@@ -24623,6 +24819,12 @@ ${versionDrift}` : "";
|
|
|
24623
24819
|
const projectName = projectBannerResult.name;
|
|
24624
24820
|
const sessionGuidanceNote = sessionGuidanceOutcome.status === "fulfilled" ? sessionGuidanceOutcome.value : "";
|
|
24625
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
|
+
}
|
|
24626
24828
|
const { reconciliationNote, unrecordedNote, unregisteredDocsNote, staleSkillsNote } = deepHousekeepingOutcome.status === "fulfilled" ? deepHousekeepingOutcome.value : { reconciliationNote: "", unrecordedNote: "", unregisteredDocsNote: "", staleSkillsNote: "" };
|
|
24627
24829
|
let enrichmentNote = "";
|
|
24628
24830
|
const enrichmentCollector = new FileWriteCollector();
|
|
@@ -24671,7 +24873,7 @@ ${section}`;
|
|
|
24671
24873
|
]);
|
|
24672
24874
|
const teamSummary = [teamSummaryLine, releaseHistoryLine].filter(Boolean).join("\n") || void 0;
|
|
24673
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`).*";
|
|
24674
|
-
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);
|
|
24675
24877
|
} catch (err) {
|
|
24676
24878
|
const message = err instanceof Error ? err.message : String(err);
|
|
24677
24879
|
const isKnownFriendly = /^(Orient failed|Project not found|No project|Setup required)/i.test(message);
|
|
@@ -26713,7 +26915,7 @@ function createServer(adapter2, config2) {
|
|
|
26713
26915
|
case "board_edit":
|
|
26714
26916
|
return handleBoardEdit(adapter2, safeArgs);
|
|
26715
26917
|
case "setup":
|
|
26716
|
-
return handleSetup(adapter2, config2, safeArgs);
|
|
26918
|
+
return handleSetup(adapter2, config2, safeArgs, server2.getClientVersion()?.name);
|
|
26717
26919
|
case "build_list":
|
|
26718
26920
|
return handleBuildList(adapter2, config2);
|
|
26719
26921
|
case "build_describe":
|
|
@@ -26853,17 +27055,28 @@ ${usageLine(decision.usage)}`;
|
|
|
26853
27055
|
}
|
|
26854
27056
|
const telemetryProjectId = resolveTelemetryProjectId(config2);
|
|
26855
27057
|
if (telemetryProjectId) {
|
|
26856
|
-
|
|
26857
|
-
|
|
26858
|
-
|
|
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
|
+
}
|
|
26859
27071
|
const isApplyMode = safeArgs.mode === "apply";
|
|
26860
27072
|
if (!isError) {
|
|
26861
|
-
|
|
26862
|
-
|
|
26863
|
-
|
|
26864
|
-
|
|
26865
|
-
|
|
26866
|
-
|
|
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
|
+
}
|
|
26867
27080
|
}
|
|
26868
27081
|
}
|
|
26869
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",
|