@papi-ai/server 0.7.62 → 0.7.63
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 +21 -0
- package/dist/index.js +307 -66
- package/package.json +1 -1
- package/skills/papi-cycle/AGENTS.md +1 -0
|
@@ -60,6 +60,7 @@ __export(git_exports, {
|
|
|
60
60
|
isGitAvailable: () => isGitAvailable,
|
|
61
61
|
isGitRepo: () => isGitRepo,
|
|
62
62
|
listGroupedCycleBranches: () => listGroupedCycleBranches,
|
|
63
|
+
listOpenPullRequests: () => listOpenPullRequests,
|
|
63
64
|
listOrphanFeatBranches: () => listOrphanFeatBranches,
|
|
64
65
|
mergePullRequest: () => mergePullRequest,
|
|
65
66
|
normalizeGitUrl: () => normalizeGitUrl,
|
|
@@ -346,6 +347,26 @@ function isGhAvailable() {
|
|
|
346
347
|
return false;
|
|
347
348
|
}
|
|
348
349
|
}
|
|
350
|
+
function listOpenPullRequests(cwd) {
|
|
351
|
+
if (!isGhAvailable()) return null;
|
|
352
|
+
try {
|
|
353
|
+
const out = execFileSync(
|
|
354
|
+
"gh",
|
|
355
|
+
["pr", "list", "--state", "open", "--limit", "100", "--json", "number,title,author,headRefName,createdAt"],
|
|
356
|
+
{ cwd, encoding: "utf-8" }
|
|
357
|
+
);
|
|
358
|
+
const raw = JSON.parse(out);
|
|
359
|
+
return raw.map((p) => ({
|
|
360
|
+
number: p.number,
|
|
361
|
+
title: p.title,
|
|
362
|
+
author: p.author?.login ?? "unknown",
|
|
363
|
+
headRefName: p.headRefName,
|
|
364
|
+
createdAt: p.createdAt
|
|
365
|
+
}));
|
|
366
|
+
} catch {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
349
370
|
function getOriginRepoSlug(cwd) {
|
|
350
371
|
try {
|
|
351
372
|
const url = execFileSync("git", ["remote", "get-url", "origin"], {
|
package/dist/index.js
CHANGED
|
@@ -61,6 +61,7 @@ __export(git_exports, {
|
|
|
61
61
|
isGitAvailable: () => isGitAvailable,
|
|
62
62
|
isGitRepo: () => isGitRepo,
|
|
63
63
|
listGroupedCycleBranches: () => listGroupedCycleBranches,
|
|
64
|
+
listOpenPullRequests: () => listOpenPullRequests,
|
|
64
65
|
listOrphanFeatBranches: () => listOrphanFeatBranches,
|
|
65
66
|
mergePullRequest: () => mergePullRequest,
|
|
66
67
|
normalizeGitUrl: () => normalizeGitUrl,
|
|
@@ -347,6 +348,26 @@ function isGhAvailable() {
|
|
|
347
348
|
return false;
|
|
348
349
|
}
|
|
349
350
|
}
|
|
351
|
+
function listOpenPullRequests(cwd) {
|
|
352
|
+
if (!isGhAvailable()) return null;
|
|
353
|
+
try {
|
|
354
|
+
const out = execFileSync(
|
|
355
|
+
"gh",
|
|
356
|
+
["pr", "list", "--state", "open", "--limit", "100", "--json", "number,title,author,headRefName,createdAt"],
|
|
357
|
+
{ cwd, encoding: "utf-8" }
|
|
358
|
+
);
|
|
359
|
+
const raw = JSON.parse(out);
|
|
360
|
+
return raw.map((p) => ({
|
|
361
|
+
number: p.number,
|
|
362
|
+
title: p.title,
|
|
363
|
+
author: p.author?.login ?? "unknown",
|
|
364
|
+
headRefName: p.headRefName,
|
|
365
|
+
createdAt: p.createdAt
|
|
366
|
+
}));
|
|
367
|
+
} catch {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
350
371
|
function getOriginRepoSlug(cwd) {
|
|
351
372
|
try {
|
|
352
373
|
const url = execFileSync("git", ["remote", "get-url", "origin"], {
|
|
@@ -2040,6 +2061,76 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
2040
2061
|
}
|
|
2041
2062
|
});
|
|
2042
2063
|
|
|
2064
|
+
// src/lib/reap-orphans.ts
|
|
2065
|
+
var reap_orphans_exports = {};
|
|
2066
|
+
__export(reap_orphans_exports, {
|
|
2067
|
+
formatReapSummary: () => formatReapSummary,
|
|
2068
|
+
isPapiServerCommand: () => isPapiServerCommand,
|
|
2069
|
+
listProcesses: () => listProcesses,
|
|
2070
|
+
reapOrphans: () => reapOrphans,
|
|
2071
|
+
selectReapableOrphans: () => selectReapableOrphans
|
|
2072
|
+
});
|
|
2073
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
2074
|
+
function isPapiServerCommand(command) {
|
|
2075
|
+
return /@papi-ai[/\\]server/.test(command);
|
|
2076
|
+
}
|
|
2077
|
+
function selectReapableOrphans(procs, selfPid) {
|
|
2078
|
+
return procs.filter(
|
|
2079
|
+
(p) => p.pid !== selfPid && p.ppid === 1 && isPapiServerCommand(p.command)
|
|
2080
|
+
);
|
|
2081
|
+
}
|
|
2082
|
+
function listProcesses() {
|
|
2083
|
+
if (process.platform === "win32") return null;
|
|
2084
|
+
try {
|
|
2085
|
+
const out = execFileSync6("ps", ["-A", "-o", "pid=,ppid=,command="], { encoding: "utf-8" });
|
|
2086
|
+
const procs = [];
|
|
2087
|
+
for (const line of out.split("\n")) {
|
|
2088
|
+
const trimmed = line.trim();
|
|
2089
|
+
if (!trimmed) continue;
|
|
2090
|
+
const m = /^(\d+)\s+(\d+)\s+(.*)$/.exec(trimmed);
|
|
2091
|
+
if (!m) continue;
|
|
2092
|
+
procs.push({ pid: Number(m[1]), ppid: Number(m[2]), command: m[3] });
|
|
2093
|
+
}
|
|
2094
|
+
return procs;
|
|
2095
|
+
} catch {
|
|
2096
|
+
return null;
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
function reapOrphans(opts = {}) {
|
|
2100
|
+
const procs = listProcesses();
|
|
2101
|
+
if (procs === null) return { unsupported: true, candidates: [], reaped: [] };
|
|
2102
|
+
const orphans = selectReapableOrphans(procs, process.pid);
|
|
2103
|
+
const candidates = orphans.map((p) => p.pid);
|
|
2104
|
+
const reaped = [];
|
|
2105
|
+
if (!opts.dryRun) {
|
|
2106
|
+
for (const pid of candidates) {
|
|
2107
|
+
try {
|
|
2108
|
+
process.kill(pid, "SIGTERM");
|
|
2109
|
+
reaped.push(pid);
|
|
2110
|
+
} catch {
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
return { unsupported: false, candidates, reaped };
|
|
2115
|
+
}
|
|
2116
|
+
function formatReapSummary(result, dryRun) {
|
|
2117
|
+
if (result.unsupported) {
|
|
2118
|
+
return "Orphan reaper: unsupported on this platform \u2014 skipped (no processes touched).";
|
|
2119
|
+
}
|
|
2120
|
+
if (result.candidates.length === 0) {
|
|
2121
|
+
return "Orphan reaper: no parentless @papi-ai/server processes found.";
|
|
2122
|
+
}
|
|
2123
|
+
if (dryRun) {
|
|
2124
|
+
return `Orphan reaper: ${result.candidates.length} parentless PAPI server process(es) found: ${result.candidates.join(", ")} (run with --reap-orphans to terminate).`;
|
|
2125
|
+
}
|
|
2126
|
+
return `Orphan reaper: terminated ${result.reaped.length} parentless PAPI server process(es): ${result.reaped.join(", ")}.`;
|
|
2127
|
+
}
|
|
2128
|
+
var init_reap_orphans = __esm({
|
|
2129
|
+
"src/lib/reap-orphans.ts"() {
|
|
2130
|
+
"use strict";
|
|
2131
|
+
}
|
|
2132
|
+
});
|
|
2133
|
+
|
|
2043
2134
|
// ../../node_modules/postgres/src/query.js
|
|
2044
2135
|
function cachedError(xs) {
|
|
2045
2136
|
if (originCache.has(xs))
|
|
@@ -4506,12 +4597,16 @@ async function runDoctor(cliArgs2 = []) {
|
|
|
4506
4597
|
const fixPool = cliArgs2.includes("--fix-pool") || cliArgs2.includes("--terminate-wedged");
|
|
4507
4598
|
const pool = await diagnosePool({ fix: fixPool });
|
|
4508
4599
|
process.stdout.write("\n" + formatPoolReport(pool) + "\n");
|
|
4600
|
+
const reap = cliArgs2.includes("--reap-orphans");
|
|
4601
|
+
const reapResult = reapOrphans({ dryRun: !reap });
|
|
4602
|
+
process.stdout.write("\n" + formatReapSummary(reapResult, !reap) + "\n");
|
|
4509
4603
|
return 0;
|
|
4510
4604
|
}
|
|
4511
4605
|
var SECRET_VARS, WEDGED_IDLE_TX_SECONDS, WEDGED_ACTIVE_SECONDS, __testing;
|
|
4512
4606
|
var init_doctor = __esm({
|
|
4513
4607
|
"src/cli/doctor.ts"() {
|
|
4514
4608
|
"use strict";
|
|
4609
|
+
init_reap_orphans();
|
|
4515
4610
|
SECRET_VARS = /* @__PURE__ */ new Set(["PAPI_DATA_API_KEY", "DATABASE_URL", "PAPI_ENDPOINT"]);
|
|
4516
4611
|
WEDGED_IDLE_TX_SECONDS = 300;
|
|
4517
4612
|
WEDGED_ACTIVE_SECONDS = 300;
|
|
@@ -15940,6 +16035,11 @@ Confidence: ${input.confidence}. Captured mid-conversation via strategy_change c
|
|
|
15940
16035
|
}
|
|
15941
16036
|
|
|
15942
16037
|
// src/tools/strategy.ts
|
|
16038
|
+
function toDateLabel(value) {
|
|
16039
|
+
if (typeof value === "string") return value.slice(0, 10);
|
|
16040
|
+
const d = value instanceof Date ? value : new Date(value);
|
|
16041
|
+
return Number.isNaN(d.getTime()) ? "" : d.toISOString().slice(0, 10);
|
|
16042
|
+
}
|
|
15943
16043
|
var reviewPrepareCache = new PerCallerCache();
|
|
15944
16044
|
var strategyReviewTool = {
|
|
15945
16045
|
name: "strategy_review",
|
|
@@ -16241,7 +16341,7 @@ This topic will surface in the next \`strategy_review\`.`
|
|
|
16241
16341
|
const lines = topics.map((t, i) => {
|
|
16242
16342
|
const cycleSuffix = t.sourceCycle != null ? ` (Cycle ${t.sourceCycle})` : "";
|
|
16243
16343
|
return `${i + 1}. ${t.topic}
|
|
16244
|
-
_source: ${t.source}${cycleSuffix} \xB7 queued ${t.createdAt
|
|
16344
|
+
_source: ${t.source}${cycleSuffix} \xB7 queued ${toDateLabel(t.createdAt)}_`;
|
|
16245
16345
|
});
|
|
16246
16346
|
return textResponse(
|
|
16247
16347
|
`**Pending Agenda (${topics.length})** \u2014 surfaces at next strategy review
|
|
@@ -19727,6 +19827,61 @@ async function completeRelease(tracker, opts) {
|
|
|
19727
19827
|
}
|
|
19728
19828
|
await tracker.recordStep("released", { metadata: { version: opts.version } });
|
|
19729
19829
|
}
|
|
19830
|
+
var OPEN_PR_STALE_DAYS = 21;
|
|
19831
|
+
function taskIdFromBranch(headRefName) {
|
|
19832
|
+
const m = /^feat\/(task-\d+)\b/.exec(headRefName);
|
|
19833
|
+
return m ? m[1] : null;
|
|
19834
|
+
}
|
|
19835
|
+
function classifyOpenPr(pr, cycleNum, inReviewCycleTaskIds, nowMs) {
|
|
19836
|
+
const head = pr.headRefName;
|
|
19837
|
+
const ageDays = (nowMs - Date.parse(pr.createdAt)) / 864e5;
|
|
19838
|
+
if (new RegExp(`^feat/cycle-${cycleNum}-`).test(head)) {
|
|
19839
|
+
return { pr, bucket: "cycle-branch", action: `merge into this release \u2014 \`${head}\` is this cycle's branch and still open` };
|
|
19840
|
+
}
|
|
19841
|
+
const taskId = taskIdFromBranch(head);
|
|
19842
|
+
if (taskId && inReviewCycleTaskIds.has(taskId)) {
|
|
19843
|
+
return { pr, bucket: "held-adhoc-this-cycle", action: `MERGE before closing \u2014 ${taskId} is In Review pinned to Cycle ${cycleNum}` };
|
|
19844
|
+
}
|
|
19845
|
+
if (taskId) {
|
|
19846
|
+
return { pr, bucket: "held-adhoc-other", action: "held adhoc not pinned to this cycle \u2014 defer, or merge if ready" };
|
|
19847
|
+
}
|
|
19848
|
+
if (Number.isFinite(ageDays) && ageDays > OPEN_PR_STALE_DAYS) {
|
|
19849
|
+
return { pr, bucket: "stale", action: `review/close \u2014 open ${Math.round(ageDays)}d with no cycle link` };
|
|
19850
|
+
}
|
|
19851
|
+
return { pr, bucket: "external-other", action: "review manually \u2014 merge, defer, or close" };
|
|
19852
|
+
}
|
|
19853
|
+
var OPEN_PR_BUCKET_ORDER = [
|
|
19854
|
+
"held-adhoc-this-cycle",
|
|
19855
|
+
"cycle-branch",
|
|
19856
|
+
"external-other",
|
|
19857
|
+
"stale",
|
|
19858
|
+
"held-adhoc-other"
|
|
19859
|
+
];
|
|
19860
|
+
function buildOpenPrSweepLines(openPrs, cycleNum, inReviewCycleTaskIds, nowMs) {
|
|
19861
|
+
const lines = [];
|
|
19862
|
+
if (openPrs === null) {
|
|
19863
|
+
lines.push("", "**Open-PR sweep:** skipped \u2014 `gh` unavailable. Run `gh pr list` yourself to check for held/external PRs before considering the cycle closed.");
|
|
19864
|
+
} else if (openPrs.length > 0) {
|
|
19865
|
+
const classified = openPrs.map((pr) => classifyOpenPr(pr, cycleNum, inReviewCycleTaskIds, nowMs)).sort((a, b2) => OPEN_PR_BUCKET_ORDER.indexOf(a.bucket) - OPEN_PR_BUCKET_ORDER.indexOf(b2.bucket));
|
|
19866
|
+
lines.push("", `**Open-PR sweep \u2014 ${openPrs.length} open PR(s). Resolve each before considering Cycle ${cycleNum} closed:**`);
|
|
19867
|
+
for (const c of classified) {
|
|
19868
|
+
lines.push(`- #${c.pr.number} \`${c.pr.headRefName}\` by ${c.pr.author} \u2014 [${c.bucket}] ${c.action}`);
|
|
19869
|
+
}
|
|
19870
|
+
}
|
|
19871
|
+
if (inReviewCycleTaskIds.size > 0) {
|
|
19872
|
+
const prByTask = /* @__PURE__ */ new Map();
|
|
19873
|
+
for (const p of openPrs ?? []) {
|
|
19874
|
+
const id = taskIdFromBranch(p.headRefName);
|
|
19875
|
+
if (id) prByTask.set(id, p.number);
|
|
19876
|
+
}
|
|
19877
|
+
const rows = [...inReviewCycleTaskIds].sort().map((id) => {
|
|
19878
|
+
const prNum = prByTask.get(id);
|
|
19879
|
+
return ` - ${id}${prNum ? ` (PR #${prNum})` : " (no open PR found)"}`;
|
|
19880
|
+
});
|
|
19881
|
+
lines.push("", `\u26A0\uFE0F **${inReviewCycleTaskIds.size} task(s) still In Review, pinned to Cycle ${cycleNum} \u2014 their work is NOT merged. Accept/merge or defer before closing:**`, ...rows);
|
|
19882
|
+
}
|
|
19883
|
+
return lines;
|
|
19884
|
+
}
|
|
19730
19885
|
|
|
19731
19886
|
// src/tools/release.ts
|
|
19732
19887
|
init_git();
|
|
@@ -20217,6 +20372,20 @@ Run \`project_switch <slug>\` to switch the active PAPI project, or verify your
|
|
|
20217
20372
|
if (result.warnings?.length) {
|
|
20218
20373
|
lines.push("", "\u26A0\uFE0F Warnings: " + result.warnings.join("; "));
|
|
20219
20374
|
}
|
|
20375
|
+
try {
|
|
20376
|
+
const openPrs = listOpenPullRequests(config2.projectRoot);
|
|
20377
|
+
const closedCycle = result.cycleClosed ?? 0;
|
|
20378
|
+
let inReviewIds = /* @__PURE__ */ new Set();
|
|
20379
|
+
if (closedCycle > 0) {
|
|
20380
|
+
const board = await adapter2.queryBoard();
|
|
20381
|
+
inReviewIds = new Set(
|
|
20382
|
+
board.filter((t) => t.status === "In Review" && t.cycle === closedCycle).map((t) => t.id)
|
|
20383
|
+
);
|
|
20384
|
+
}
|
|
20385
|
+
const sweep = buildOpenPrSweepLines(openPrs, closedCycle, inReviewIds, Date.now());
|
|
20386
|
+
if (sweep.length > 0) lines.push(...sweep);
|
|
20387
|
+
} catch {
|
|
20388
|
+
}
|
|
20220
20389
|
tracker.mark("surface-discovered-issues");
|
|
20221
20390
|
try {
|
|
20222
20391
|
const closedCycle = result.cycleClosed ?? 0;
|
|
@@ -20958,8 +21127,8 @@ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
|
20958
21127
|
} else {
|
|
20959
21128
|
const stashLabel = `papi-autostash/${taskId}-${Math.floor(Date.now() / 1e3)}`;
|
|
20960
21129
|
try {
|
|
20961
|
-
const { execFileSync:
|
|
20962
|
-
|
|
21130
|
+
const { execFileSync: execFileSync7 } = await import("child_process");
|
|
21131
|
+
execFileSync7("git", ["stash", "push", "-u", "-m", stashLabel, "--", ...toStash], {
|
|
20963
21132
|
cwd: config2.projectRoot,
|
|
20964
21133
|
encoding: "utf-8"
|
|
20965
21134
|
});
|
|
@@ -20985,8 +21154,8 @@ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
|
20985
21154
|
if (hasRemote(config2.projectRoot) && !featureBranchExistsLocally) {
|
|
20986
21155
|
if (featureBranchOnOrigin) {
|
|
20987
21156
|
try {
|
|
20988
|
-
const { execFileSync:
|
|
20989
|
-
|
|
21157
|
+
const { execFileSync: execFileSync7 } = await import("child_process");
|
|
21158
|
+
execFileSync7("git", ["fetch", "origin", `${featureBranch}:${featureBranch}`], {
|
|
20990
21159
|
cwd: config2.projectRoot,
|
|
20991
21160
|
encoding: "utf-8",
|
|
20992
21161
|
timeout: 6e4
|
|
@@ -22110,6 +22279,10 @@ var buildExecuteTool = {
|
|
|
22110
22279
|
enum: ["yes", "no", "partial"],
|
|
22111
22280
|
description: "Whether the build was completed. Required for complete."
|
|
22112
22281
|
},
|
|
22282
|
+
acceptance_confirmed: {
|
|
22283
|
+
type: "boolean",
|
|
22284
|
+
description: `task-2833: set true to assert every acceptance criterion in the task's BUILD HANDOFF was met. Required to record a completed:"yes" build when the handoff lists acceptance criteria \u2014 without it, build_execute returns the criteria checklist and does NOT mark the task Done (the report is not discarded; re-send with acceptance_confirmed:true). Tasks with no acceptance criteria, and completed:"partial"/"no", are unaffected.`
|
|
22285
|
+
},
|
|
22113
22286
|
effort: {
|
|
22114
22287
|
type: "string",
|
|
22115
22288
|
enum: ["XS", "S", "M", "L", "XL"],
|
|
@@ -22565,6 +22738,25 @@ async function handleExecuteComplete(adapter2, config2, taskId, args, light = fa
|
|
|
22565
22738
|
if (!parsedEstimatedEffort) {
|
|
22566
22739
|
return errorResponse(`Invalid estimated_effort value "${estimatedEffort}". Must be one of: XS, S, M, L, XL.`);
|
|
22567
22740
|
}
|
|
22741
|
+
const acceptanceConfirmed = args.acceptance_confirmed === true;
|
|
22742
|
+
if (completed === "yes" && !acceptanceConfirmed) {
|
|
22743
|
+
const gateInfo = adapter2.getProjectInfo ? await adapter2.getProjectInfo().catch(() => null) : null;
|
|
22744
|
+
const gateCaps = gateInfo?.capabilities ?? {};
|
|
22745
|
+
const gateTask = isCapabilityEnabled(gateCaps, "acceptanceGate") ? await adapter2.getTask(taskId).catch(() => null) : null;
|
|
22746
|
+
const criteria = (gateTask?.buildHandoff?.acceptanceCriteria ?? []).filter((c) => c && c.trim());
|
|
22747
|
+
if (criteria.length > 0) {
|
|
22748
|
+
const checklist = criteria.map((c) => ` - [ ] ${c}`).join("\n");
|
|
22749
|
+
return textResponse(
|
|
22750
|
+
`**Acceptance criteria not yet confirmed for ${taskId}.**
|
|
22751
|
+
|
|
22752
|
+
This task's BUILD HANDOFF lists ${criteria.length} acceptance criteri${criteria.length === 1 ? "on" : "a"}. Confirm each was met, then re-call \`build_execute\` complete with the SAME report fields plus \`acceptance_confirmed: true\`:
|
|
22753
|
+
|
|
22754
|
+
${checklist}
|
|
22755
|
+
|
|
22756
|
+
Your report was NOT discarded and the task is NOT yet Done \u2014 re-send with \`acceptance_confirmed: true\` to record completion. If a criterion was NOT met, the task is not complete: finish it, or report \`completed: "partial"\` with what remains in \`surprises\`.`
|
|
22757
|
+
);
|
|
22758
|
+
}
|
|
22759
|
+
}
|
|
22568
22760
|
const tracker = new ProgressTracker("complete_validate").bindStream(adapter2, { stage: "build", taskId });
|
|
22569
22761
|
try {
|
|
22570
22762
|
tracker.mark("complete_build");
|
|
@@ -24222,6 +24414,13 @@ Re-run build_execute complete with a production_verification field, then re-subm
|
|
|
24222
24414
|
|
|
24223
24415
|
// src/tools/review.ts
|
|
24224
24416
|
var REVIEW_DISPATCH_THRESHOLD = 50 * 1024;
|
|
24417
|
+
var REVIEW_DISPATCH_CEILING = 40 * 1024;
|
|
24418
|
+
var REVIEW_ECHO_CAP = 4e3;
|
|
24419
|
+
function trimForEcho(text) {
|
|
24420
|
+
if (text.length <= REVIEW_ECHO_CAP) return text;
|
|
24421
|
+
return `${text.slice(0, REVIEW_ECHO_CAP)}
|
|
24422
|
+
\u2026[trimmed ${text.length - REVIEW_ECHO_CAP} chars]`;
|
|
24423
|
+
}
|
|
24225
24424
|
var REVIEW_RUBRIC = [
|
|
24226
24425
|
"You are reviewing a completed PAPI build for acceptance. Judge:",
|
|
24227
24426
|
"- Correctness: does the change do what the build report claims, without obvious bugs?",
|
|
@@ -24551,6 +24750,7 @@ async function handleReviewSubmit(adapter2, config2, args) {
|
|
|
24551
24750
|
const autoDispatchOptIn = args.dispatch !== "inline" && process.env.PAPI_AUTO_DISPATCH !== "false" && isCapabilityEnabled(caps, "prReviewer");
|
|
24552
24751
|
const autoDispatchEligible = !verdict && autoDispatchOptIn;
|
|
24553
24752
|
const capabilityAutoReviewEligible = verdict === "accept" && !autoReview && autoDispatchOptIn;
|
|
24753
|
+
let capabilityReviewSkippedNote = "";
|
|
24554
24754
|
if ((explicitDispatch || autoDispatchEligible || capabilityAutoReviewEligible) && stage === "build-acceptance" && taskId) {
|
|
24555
24755
|
const dispatch = await buildReviewDispatch(
|
|
24556
24756
|
adapter2,
|
|
@@ -24560,8 +24760,16 @@ async function handleReviewSubmit(adapter2, config2, args) {
|
|
|
24560
24760
|
);
|
|
24561
24761
|
if (!dispatch.ok) {
|
|
24562
24762
|
if (explicitDispatch) return errorResponse(dispatch.error);
|
|
24563
|
-
} else if (explicitDispatch ||
|
|
24763
|
+
} else if (explicitDispatch || autoDispatchEligible && dispatch.contextBytes > REVIEW_DISPATCH_THRESHOLD) {
|
|
24564
24764
|
return textResponse(dispatch.prompt);
|
|
24765
|
+
} else if (capabilityAutoReviewEligible) {
|
|
24766
|
+
if (dispatch.contextBytes <= REVIEW_DISPATCH_CEILING) {
|
|
24767
|
+
return textResponse(dispatch.prompt);
|
|
24768
|
+
}
|
|
24769
|
+
const kb = (dispatch.contextBytes / 1024).toFixed(0);
|
|
24770
|
+
capabilityReviewSkippedNote = `
|
|
24771
|
+
|
|
24772
|
+
> \u26A0\uFE0F pr-reviewer auto-review skipped \u2014 the diff/build-report (~${kb} KB) exceeds the ${REVIEW_DISPATCH_CEILING / 1024} KB inline-dispatch ceiling, which would overflow the response and drop the accept (task-2854). Verdict recorded directly. To review the diff explicitly, run \`review_submit ${taskId} build-acceptance accept dispatch:"subagent"\`.`;
|
|
24565
24773
|
}
|
|
24566
24774
|
}
|
|
24567
24775
|
if (explicitDispatch && stage !== "build-acceptance") {
|
|
@@ -24721,36 +24929,51 @@ ${overlap}`;
|
|
|
24721
24929
|
|
|
24722
24930
|
\u2705 Verdict recorded. All cycle tasks are Done, but **auto-release is owner-only** \u2014 your identity does not match this project's owner, so no release was cut. Push your branch and open a PR for the owner to run \`release\`.${resolutionNote}`;
|
|
24723
24931
|
} else if (cycleTasks.length > 0 && cycleTasks.every((t) => t.status === "Done")) {
|
|
24724
|
-
|
|
24725
|
-
|
|
24726
|
-
|
|
24932
|
+
let planRunCount = null;
|
|
24933
|
+
if (typeof adapter2.countPlanRunsForCycle === "function") {
|
|
24934
|
+
try {
|
|
24935
|
+
planRunCount = await adapter2.countPlanRunsForCycle(result.currentCycle);
|
|
24936
|
+
} catch {
|
|
24937
|
+
planRunCount = null;
|
|
24938
|
+
}
|
|
24939
|
+
}
|
|
24940
|
+
if (planRunCount === 0) {
|
|
24727
24941
|
autoReleaseNote = `
|
|
24728
24942
|
|
|
24729
24943
|
---
|
|
24730
24944
|
|
|
24945
|
+
\u26A0\uFE0F **Auto-release skipped** \u2014 Cycle ${result.currentCycle} has **no plan run** (100% injected/adhoc work), so it was never planned. Auto-release only fires for planned cycles, to avoid silently shipping a cycle nobody opened (C340 incident). All tasks are Done \u2014 run \`release\` explicitly to close and ship this cycle.`;
|
|
24946
|
+
} else {
|
|
24947
|
+
const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
|
|
24948
|
+
const unmergedCycleBranches = isGitAvailable() && isGitRepo(config2.projectRoot) ? listGroupedCycleBranches(config2.projectRoot, result.currentCycle, baseBranch) : [];
|
|
24949
|
+
if (unmergedCycleBranches.length > 0) {
|
|
24950
|
+
autoReleaseNote = `
|
|
24951
|
+
|
|
24952
|
+
---
|
|
24953
|
+
|
|
24731
24954
|
\u26A0\uFE0F **Auto-release skipped** \u2014 all tasks are Done but ${unmergedCycleBranches.length} cycle branch(es) not yet merged: \`${unmergedCycleBranches.join("`, `")}\`.
|
|
24732
24955
|
|
|
24733
24956
|
Merge or squash those PRs first, then run \`release\` manually.`;
|
|
24734
|
-
|
|
24735
|
-
|
|
24736
|
-
|
|
24737
|
-
|
|
24738
|
-
|
|
24739
|
-
|
|
24740
|
-
|
|
24741
|
-
|
|
24742
|
-
|
|
24743
|
-
|
|
24744
|
-
|
|
24745
|
-
|
|
24746
|
-
|
|
24747
|
-
|
|
24748
|
-
|
|
24957
|
+
} else {
|
|
24958
|
+
try {
|
|
24959
|
+
const allReviews = await adapter2.getRecentReviews(200);
|
|
24960
|
+
const cycleReviews = allReviews.filter(
|
|
24961
|
+
(r) => r.cycle === result.currentCycle && r.stage === "build-acceptance"
|
|
24962
|
+
);
|
|
24963
|
+
const reviewsWithAutoReview = cycleReviews.filter((r) => r.autoReview);
|
|
24964
|
+
if (reviewsWithAutoReview.length > 0) {
|
|
24965
|
+
const verdictCounts = { pass: 0, warn: 0, fail: 0 };
|
|
24966
|
+
const findingsBySeverity = { error: 0, warning: 0, info: 0 };
|
|
24967
|
+
for (const r of reviewsWithAutoReview) {
|
|
24968
|
+
if (r.autoReview) {
|
|
24969
|
+
verdictCounts[r.autoReview.verdict] = (verdictCounts[r.autoReview.verdict] ?? 0) + 1;
|
|
24970
|
+
for (const f of r.autoReview.findings) {
|
|
24971
|
+
findingsBySeverity[f.severity] = (findingsBySeverity[f.severity] ?? 0) + 1;
|
|
24972
|
+
}
|
|
24749
24973
|
}
|
|
24750
24974
|
}
|
|
24751
|
-
|
|
24752
|
-
|
|
24753
|
-
batchSummaryNote = `
|
|
24975
|
+
const totalFindings = findingsBySeverity.error + findingsBySeverity.warning + findingsBySeverity.info;
|
|
24976
|
+
batchSummaryNote = `
|
|
24754
24977
|
|
|
24755
24978
|
---
|
|
24756
24979
|
|
|
@@ -24758,42 +24981,42 @@ Merge or squash those PRs first, then run \`release\` manually.`;
|
|
|
24758
24981
|
|
|
24759
24982
|
- Verdicts: ${verdictCounts.pass} pass, ${verdictCounts.warn} warn, ${verdictCounts.fail} fail
|
|
24760
24983
|
` + (totalFindings > 0 ? `- Findings: ${findingsBySeverity.error} error${findingsBySeverity.error !== 1 ? "s" : ""}, ${findingsBySeverity.warning} warning${findingsBySeverity.warning !== 1 ? "s" : ""}, ${findingsBySeverity.info} info` : "- No findings logged");
|
|
24984
|
+
}
|
|
24985
|
+
} catch {
|
|
24761
24986
|
}
|
|
24762
|
-
|
|
24763
|
-
|
|
24764
|
-
|
|
24765
|
-
|
|
24766
|
-
if (autoGate.action !== "proceed") {
|
|
24767
|
-
autoReleaseNote = `
|
|
24987
|
+
const version = `v0.${result.currentCycle}.0`;
|
|
24988
|
+
const autoGate = evaluateReleaseGate(caps, config2.gateCommand, void 0);
|
|
24989
|
+
if (autoGate.action !== "proceed") {
|
|
24990
|
+
autoReleaseNote = `
|
|
24768
24991
|
|
|
24769
24992
|
---
|
|
24770
24993
|
|
|
24771
24994
|
\u26A0\uFE0F **Auto-release skipped** \u2014 a release quality gate is configured (\`${config2.gateCommand}\`), and it cannot be run from inside \`review_submit\`.
|
|
24772
24995
|
|
|
24773
24996
|
Run \`release\` manually: PAPI will hand you the gate command, then release once you report it green.`;
|
|
24774
|
-
|
|
24775
|
-
|
|
24776
|
-
|
|
24777
|
-
|
|
24778
|
-
|
|
24779
|
-
|
|
24780
|
-
|
|
24781
|
-
|
|
24782
|
-
|
|
24783
|
-
|
|
24784
|
-
|
|
24785
|
-
|
|
24786
|
-
|
|
24787
|
-
|
|
24788
|
-
|
|
24789
|
-
|
|
24790
|
-
|
|
24791
|
-
|
|
24792
|
-
|
|
24793
|
-
|
|
24794
|
-
|
|
24795
|
-
|
|
24796
|
-
|
|
24997
|
+
} else {
|
|
24998
|
+
const releaseTracker = new ProgressTracker("auto-release").bindStream(adapter2, { stage: "release" });
|
|
24999
|
+
await beginRelease(releaseTracker, result.currentCycle);
|
|
25000
|
+
const releaseResult = await createRelease(config2, baseBranch, version, adapter2, result.currentCycle);
|
|
25001
|
+
await recordReadinessVerified(releaseTracker);
|
|
25002
|
+
await recordQualityGate(releaseTracker, autoGate, caps);
|
|
25003
|
+
await tracker.recordStep("auto_release_triggered", { metadata: { version: releaseResult.version } });
|
|
25004
|
+
const autoChangelogDirective = buildChangelogDirective(
|
|
25005
|
+
caps,
|
|
25006
|
+
buildCycleUpdateCurationDirective(releaseResult.version, releaseResult.cycleClosed ?? 0)
|
|
25007
|
+
);
|
|
25008
|
+
const autoDeployDirective = buildDeployHookDirective(caps, config2.deployCommand);
|
|
25009
|
+
await completeRelease(releaseTracker, {
|
|
25010
|
+
cycleClosed: releaseResult.cycleClosed ?? null,
|
|
25011
|
+
version: releaseResult.version,
|
|
25012
|
+
caps,
|
|
25013
|
+
branchMerges: releaseResult.groupedBranchMerges ?? [],
|
|
25014
|
+
changelogEmitted: Boolean(autoChangelogDirective),
|
|
25015
|
+
deployHookEmitted: Boolean(autoDeployDirective)
|
|
25016
|
+
});
|
|
25017
|
+
const pushInfo = releaseResult.pushNotes.join(" ");
|
|
25018
|
+
const groupedMergeNote = releaseResult.groupedBranchMerges?.length ? "\n" + releaseResult.groupedBranchMerges.map((r) => `- Merged shared branch \`${r.branch}\` via PR: ${r.prUrl ?? "n/a"}`).join("\n") : "";
|
|
25019
|
+
autoReleaseNote = `
|
|
24797
25020
|
|
|
24798
25021
|
---
|
|
24799
25022
|
|
|
@@ -24804,13 +25027,14 @@ Run \`release\` manually: PAPI will hand you the gate command, then release once
|
|
|
24804
25027
|
- ${releaseResult.tagMessage}
|
|
24805
25028
|
- ${pushInfo}` + groupedMergeNote + (releaseResult.warnings?.length ? `
|
|
24806
25029
|
- Warnings: ${releaseResult.warnings.join(", ")}` : "") + // task-2598 (C328): the auto path previously swallowed the curated
|
|
24807
|
-
|
|
24808
|
-
|
|
24809
|
-
|
|
25030
|
+
// cycle-update directive that the manual path emits, so an auto-released
|
|
25031
|
+
// cycle never prompted the Discord post. Same directive, same gate.
|
|
25032
|
+
(autoChangelogDirective ? `
|
|
24810
25033
|
${autoChangelogDirective}` : "") + (autoDeployDirective ? `
|
|
24811
25034
|
${autoDeployDirective}` : "") + `
|
|
24812
25035
|
|
|
24813
25036
|
Run \`plan\` to create Cycle ${result.currentCycle + 1}.`;
|
|
25037
|
+
}
|
|
24814
25038
|
}
|
|
24815
25039
|
}
|
|
24816
25040
|
}
|
|
@@ -24870,9 +25094,9 @@ Next: address the feedback, then run \`build_execute ${taskId}\` to resubmit.`;
|
|
|
24870
25094
|
`**${result.stageLabel}** recorded for ${result.taskId}.
|
|
24871
25095
|
|
|
24872
25096
|
- **Verdict:** ${result.verdict}
|
|
24873
|
-
- **Comments:** ${result.comments}
|
|
25097
|
+
- **Comments:** ${trimForEcho(result.comments)}
|
|
24874
25098
|
|
|
24875
|
-
${statusNote}${autoReviewNote}${securityNote}${unblockNote}${docClosureNote}${regenNote}${mergeNote}${overlapNote}${batchSummaryNote}${autoReleaseNote}${nextStepNote}${phaseNote}`
|
|
25099
|
+
${statusNote}${capabilityReviewSkippedNote}${autoReviewNote}${securityNote}${unblockNote}${docClosureNote}${regenNote}${mergeNote}${overlapNote}${batchSummaryNote}${autoReleaseNote}${nextStepNote}${phaseNote}`
|
|
24876
25100
|
);
|
|
24877
25101
|
} catch (err) {
|
|
24878
25102
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -29738,6 +29962,13 @@ function startHttpTransport(opts) {
|
|
|
29738
29962
|
const ip = clientIp(req);
|
|
29739
29963
|
const origin = req.headers.origin;
|
|
29740
29964
|
const cors = corsHeaders(origin);
|
|
29965
|
+
const pathname = (() => {
|
|
29966
|
+
try {
|
|
29967
|
+
return new URL(req.url ?? "/", "http://internal").pathname;
|
|
29968
|
+
} catch {
|
|
29969
|
+
return req.url ?? "/";
|
|
29970
|
+
}
|
|
29971
|
+
})();
|
|
29741
29972
|
if (req.method === "OPTIONS") {
|
|
29742
29973
|
if (Object.keys(cors).length === 0) {
|
|
29743
29974
|
sendError(res, { status: 403, body: { error: "Origin not allowed" } });
|
|
@@ -29752,12 +29983,12 @@ function startHttpTransport(opts) {
|
|
|
29752
29983
|
sendError(res, { status: 400, body: { error: "HTTPS required" } });
|
|
29753
29984
|
return;
|
|
29754
29985
|
}
|
|
29755
|
-
if (req.method === "GET" &&
|
|
29986
|
+
if (req.method === "GET" && pathname === "/healthz") {
|
|
29756
29987
|
res.writeHead(200, { "Content-Type": "text/plain", ...cors });
|
|
29757
29988
|
res.end("ok");
|
|
29758
29989
|
return;
|
|
29759
29990
|
}
|
|
29760
|
-
if (req.method === "GET" &&
|
|
29991
|
+
if (req.method === "GET" && pathname === "/.well-known/oauth-protected-resource") {
|
|
29761
29992
|
res.writeHead(200, {
|
|
29762
29993
|
"Content-Type": "application/json",
|
|
29763
29994
|
"Cache-Control": "public, max-age=3600",
|
|
@@ -29773,7 +30004,7 @@ function startHttpTransport(opts) {
|
|
|
29773
30004
|
);
|
|
29774
30005
|
return;
|
|
29775
30006
|
}
|
|
29776
|
-
if (req.method === "GET" &&
|
|
30007
|
+
if (req.method === "GET" && pathname === "/.well-known/glama.json") {
|
|
29777
30008
|
res.writeHead(200, {
|
|
29778
30009
|
"Content-Type": "application/json",
|
|
29779
30010
|
"Cache-Control": "public, max-age=3600",
|
|
@@ -29787,7 +30018,7 @@ function startHttpTransport(opts) {
|
|
|
29787
30018
|
);
|
|
29788
30019
|
return;
|
|
29789
30020
|
}
|
|
29790
|
-
if (req.method === "GET" &&
|
|
30021
|
+
if (req.method === "GET" && pathname === "/.well-known/oauth-authorization-server") {
|
|
29791
30022
|
res.writeHead(302, {
|
|
29792
30023
|
Location: `${DASHBOARD_ORIGIN}/.well-known/oauth-authorization-server`,
|
|
29793
30024
|
"Cache-Control": "public, max-age=3600",
|
|
@@ -29796,7 +30027,7 @@ function startHttpTransport(opts) {
|
|
|
29796
30027
|
res.end();
|
|
29797
30028
|
return;
|
|
29798
30029
|
}
|
|
29799
|
-
if (
|
|
30030
|
+
if (pathname !== "/mcp" && pathname !== "/sse") {
|
|
29800
30031
|
sendError(res, { status: 404, body: { error: "Not found" } }, cors);
|
|
29801
30032
|
return;
|
|
29802
30033
|
}
|
|
@@ -30076,6 +30307,7 @@ Options:
|
|
|
30076
30307
|
--yes, -y Skip confirmation prompts (reset only)
|
|
30077
30308
|
--idle-mcp Flag PAPI-idle projects in audit (needs DATABASE_URL; read-only)
|
|
30078
30309
|
--fix-pool Terminate this role's confirmed-wedged DB backends (doctor only; guarded)
|
|
30310
|
+
--reap-orphans Terminate parentless @papi-ai/server processes (doctor only; guarded)
|
|
30079
30311
|
|
|
30080
30312
|
Getting started:
|
|
30081
30313
|
1. Run "npx @papi-ai/server setup" in any project folder
|
|
@@ -30238,6 +30470,15 @@ if (isHttpMode && httpPort !== void 0) {
|
|
|
30238
30470
|
process.stderr.write("[papi] Fatal: stdio mode requires an MCP server instance.\n");
|
|
30239
30471
|
process.exit(1);
|
|
30240
30472
|
}
|
|
30473
|
+
try {
|
|
30474
|
+
const { reapOrphans: reapOrphans2 } = await Promise.resolve().then(() => (init_reap_orphans(), reap_orphans_exports));
|
|
30475
|
+
const swept = reapOrphans2({});
|
|
30476
|
+
if (swept.reaped.length > 0) {
|
|
30477
|
+
process.stderr.write(`[papi] Reaped ${swept.reaped.length} orphaned server process(es): ${swept.reaped.join(", ")}
|
|
30478
|
+
`);
|
|
30479
|
+
}
|
|
30480
|
+
} catch {
|
|
30481
|
+
}
|
|
30241
30482
|
const transport = new StdioServerTransport();
|
|
30242
30483
|
await server.connect(transport);
|
|
30243
30484
|
const projectName = basename2(config.projectRoot);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@papi-ai/server",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.63",
|
|
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.getpapi/papi",
|
|
@@ -38,6 +38,7 @@ When a conversation starts — fresh window, new session, or after context compr
|
|
|
38
38
|
- **All in-cycle, in-module tasks share `feat/cycle-N-<module>`** regardless of complexity. One branch per module per cycle, merged together. Module-less tasks fall back to a per-task branch.
|
|
39
39
|
- **Dependent tasks (any size):** When a task's BUILD HANDOFF lists a `DEPENDS ON` task from the same cycle, `build_execute` automatically reuses the upstream task's branch so commits stack for a single PR. Do not create a separate branch manually.
|
|
40
40
|
- **Commit per task within grouped branches** — traceable git history.
|
|
41
|
+
- **Integration + gate before release when a cycle fans to more than 4 module branches.** A wide cycle has no single point where all branches are proven together, so a collision between two branches only surfaces at release. When a cycle has more than 4 module branches, before `release`: (1) cut an integration branch off your main branch, (2) merge every cycle module branch into it, (3) run your full local gate/test suite on the integrated result, (4) resolve any cross-branch collisions there, (5) release from the integrated branch. This is a habit, not automation — release does not block on branch count.
|
|
41
42
|
- **Never use `build_execute` with `light=true` on shared branches.** Light mode commits directly to the current branch without creating a PR. When a shared branch is squash-merged, those commits are collapsed — any CLAUDE.md or documentation changes are stripped. Use light mode only on isolated single-task branches where no squash-merge will occur.
|
|
42
43
|
|
|
43
44
|
## Plumbing Is Autonomous
|