@papi-ai/server 0.7.98 → 0.7.103
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 +68 -1
- package/dist/index.js +192 -34
- package/package.json +1 -1
|
@@ -189,6 +189,7 @@ __export(git_exports, {
|
|
|
189
189
|
checkoutBranch: () => checkoutBranch,
|
|
190
190
|
commitSinglePath: () => commitSinglePath,
|
|
191
191
|
commitStagedOnly: () => commitStagedOnly,
|
|
192
|
+
computeAheadBehind: () => computeAheadBehind,
|
|
192
193
|
createAndCheckoutBranch: () => createAndCheckoutBranch,
|
|
193
194
|
createPullRequest: () => createPullRequest,
|
|
194
195
|
createTag: () => createTag,
|
|
@@ -199,8 +200,10 @@ __export(git_exports, {
|
|
|
199
200
|
detectUnrecordedCommits: () => detectUnrecordedCommits,
|
|
200
201
|
ensureLatestDevelop: () => ensureLatestDevelop,
|
|
201
202
|
ensureTagAtHead: () => ensureTagAtHead,
|
|
203
|
+
fetchBaseBranch: () => fetchBaseBranch,
|
|
202
204
|
findContributorReleasePullRequests: () => findContributorReleasePullRequests,
|
|
203
205
|
findTaskCommitsOnBase: () => findTaskCommitsOnBase,
|
|
206
|
+
getBaseDivergence: () => getBaseDivergence,
|
|
204
207
|
getBranchDiff: () => getBranchDiff,
|
|
205
208
|
getCommitFiles: () => getCommitFiles,
|
|
206
209
|
getCommitsSinceTag: () => getCommitsSinceTag,
|
|
@@ -235,6 +238,7 @@ __export(git_exports, {
|
|
|
235
238
|
hasUnpushedCommits: () => hasUnpushedCommits,
|
|
236
239
|
isBranchContentAlreadyInBase: () => isBranchContentAlreadyInBase,
|
|
237
240
|
isBranchMergedInto: () => isBranchMergedInto,
|
|
241
|
+
isCommitReachable: () => isCommitReachable,
|
|
238
242
|
isGhAvailable: () => isGhAvailable,
|
|
239
243
|
isGitAvailable: () => isGitAvailable,
|
|
240
244
|
isGitRepo: () => isGitRepo,
|
|
@@ -1297,6 +1301,19 @@ function isBranchMergedInto(cwd, branch, baseBranch) {
|
|
|
1297
1301
|
return false;
|
|
1298
1302
|
}
|
|
1299
1303
|
}
|
|
1304
|
+
function isCommitReachable(cwd, commit, baseBranch) {
|
|
1305
|
+
const recordedCommit = commit.trim();
|
|
1306
|
+
if (!recordedCommit) return false;
|
|
1307
|
+
try {
|
|
1308
|
+
execFileSync("git", ["merge-base", "--is-ancestor", recordedCommit, baseBranch], {
|
|
1309
|
+
cwd,
|
|
1310
|
+
stdio: "ignore"
|
|
1311
|
+
});
|
|
1312
|
+
return true;
|
|
1313
|
+
} catch {
|
|
1314
|
+
return false;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1300
1317
|
function isBranchContentAlreadyInBase(cwd, branch, baseBranch) {
|
|
1301
1318
|
const resolveCommit = (ref) => {
|
|
1302
1319
|
try {
|
|
@@ -1323,6 +1340,55 @@ function isBranchContentAlreadyInBase(cwd, branch, baseBranch) {
|
|
|
1323
1340
|
return false;
|
|
1324
1341
|
}
|
|
1325
1342
|
}
|
|
1343
|
+
function fetchBaseBranch(cwd, baseBranch, timeoutMs = GIT_FETCH_TIMEOUT_MS) {
|
|
1344
|
+
if (!isGitAvailable() || !isGitRepo(cwd) || !hasRemote(cwd)) {
|
|
1345
|
+
return { fetched: false, compareRef: baseBranch };
|
|
1346
|
+
}
|
|
1347
|
+
const result = spawnSync("git", ["fetch", "--quiet", "origin", baseBranch], {
|
|
1348
|
+
cwd,
|
|
1349
|
+
encoding: "utf-8",
|
|
1350
|
+
timeout: timeoutMs
|
|
1351
|
+
});
|
|
1352
|
+
if (result.error || result.status !== 0) {
|
|
1353
|
+
const raw = result.error?.message ?? ((result.stderr || "").trim() || `git exited ${result.status}`);
|
|
1354
|
+
const isTimeout = result.signal === "SIGTERM" || /ETIMEDOUT/.test(raw);
|
|
1355
|
+
return {
|
|
1356
|
+
fetched: false,
|
|
1357
|
+
compareRef: baseBranch,
|
|
1358
|
+
warning: isTimeout ? `Fetch from origin timed out after ${Math.round(timeoutMs / 1e3)}s \u2014 comparing against the local '${baseBranch}' ref, which may be stale.` : `Fetch from origin failed \u2014 comparing against the local '${baseBranch}' ref, which may be stale. (${raw})`
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
return { fetched: true, compareRef: `origin/${baseBranch}` };
|
|
1362
|
+
}
|
|
1363
|
+
function computeAheadBehind(cwd, compareRef, ref = "HEAD") {
|
|
1364
|
+
try {
|
|
1365
|
+
const out = execFileSync(
|
|
1366
|
+
"git",
|
|
1367
|
+
["rev-list", "--left-right", "--count", `${compareRef}...${ref}`],
|
|
1368
|
+
{ cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
|
|
1369
|
+
).trim();
|
|
1370
|
+
const [behindStr, aheadStr] = out.split(/\s+/);
|
|
1371
|
+
const behind = parseInt(behindStr, 10);
|
|
1372
|
+
const ahead = parseInt(aheadStr, 10);
|
|
1373
|
+
if (Number.isNaN(behind) || Number.isNaN(ahead)) return null;
|
|
1374
|
+
return { ahead, behind };
|
|
1375
|
+
} catch {
|
|
1376
|
+
return null;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
function getBaseDivergence(cwd, preferredBase, opts) {
|
|
1380
|
+
const baseBranch = resolveBaseBranch(cwd, preferredBase);
|
|
1381
|
+
const fetch2 = fetchBaseBranch(cwd, baseBranch, opts?.timeoutMs);
|
|
1382
|
+
const divergence = computeAheadBehind(cwd, fetch2.compareRef, opts?.ref ?? "HEAD");
|
|
1383
|
+
return {
|
|
1384
|
+
baseBranch,
|
|
1385
|
+
compareRef: fetch2.compareRef,
|
|
1386
|
+
fetched: fetch2.fetched,
|
|
1387
|
+
ahead: divergence?.ahead ?? null,
|
|
1388
|
+
behind: divergence?.behind ?? null,
|
|
1389
|
+
warning: fetch2.warning
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1326
1392
|
function pickModuleCycleBranch(candidates, cycleNumber, module, memberSlug) {
|
|
1327
1393
|
if (candidates.length === 0) return void 0;
|
|
1328
1394
|
const expected = cycleBranchName(cycleNumber, module, memberSlug);
|
|
@@ -1427,7 +1493,7 @@ function getRemoteBranchFiles(cwd, branch, baseBranch) {
|
|
|
1427
1493
|
return [];
|
|
1428
1494
|
}
|
|
1429
1495
|
}
|
|
1430
|
-
var AUTO_WRITTEN_PATHS, GIT_NETWORK_TIMEOUT_MS, MERGE_RETRY_DELAY_MS, MERGE_MAX_RETRIES;
|
|
1496
|
+
var AUTO_WRITTEN_PATHS, GIT_NETWORK_TIMEOUT_MS, MERGE_RETRY_DELAY_MS, MERGE_MAX_RETRIES, GIT_FETCH_TIMEOUT_MS;
|
|
1431
1497
|
var init_git = __esm({
|
|
1432
1498
|
"src/lib/git.ts"() {
|
|
1433
1499
|
"use strict";
|
|
@@ -1435,6 +1501,7 @@ var init_git = __esm({
|
|
|
1435
1501
|
GIT_NETWORK_TIMEOUT_MS = 6e4;
|
|
1436
1502
|
MERGE_RETRY_DELAY_MS = 2e3;
|
|
1437
1503
|
MERGE_MAX_RETRIES = 3;
|
|
1504
|
+
GIT_FETCH_TIMEOUT_MS = 5e3;
|
|
1438
1505
|
}
|
|
1439
1506
|
});
|
|
1440
1507
|
|
package/dist/index.js
CHANGED
|
@@ -850,7 +850,7 @@ var init_dist = __esm({
|
|
|
850
850
|
{ key: "editorRelease", label: "Editors release like the owner", description: "Lets editors run a full release instead of opening a contributor PR.", step: "release", defaultEnabled: false }
|
|
851
851
|
];
|
|
852
852
|
CAPABILITY_KEYS = CAPABILITY_REGISTRY.map((c) => c.key);
|
|
853
|
-
ASSIGNABLE_CONTRIBUTOR_ROLES = ["editor", "viewer"];
|
|
853
|
+
ASSIGNABLE_CONTRIBUTOR_ROLES = ["release_manager", "editor", "viewer"];
|
|
854
854
|
SENSITIVE_CHANGELOG_PATTERNS = [
|
|
855
855
|
// ── Test-user machinery ────────────────────────────────────────────────
|
|
856
856
|
/@test\.papi\.dev/i,
|
|
@@ -948,6 +948,7 @@ __export(git_exports, {
|
|
|
948
948
|
checkoutBranch: () => checkoutBranch,
|
|
949
949
|
commitSinglePath: () => commitSinglePath,
|
|
950
950
|
commitStagedOnly: () => commitStagedOnly,
|
|
951
|
+
computeAheadBehind: () => computeAheadBehind,
|
|
951
952
|
createAndCheckoutBranch: () => createAndCheckoutBranch,
|
|
952
953
|
createPullRequest: () => createPullRequest,
|
|
953
954
|
createTag: () => createTag,
|
|
@@ -958,8 +959,10 @@ __export(git_exports, {
|
|
|
958
959
|
detectUnrecordedCommits: () => detectUnrecordedCommits,
|
|
959
960
|
ensureLatestDevelop: () => ensureLatestDevelop,
|
|
960
961
|
ensureTagAtHead: () => ensureTagAtHead,
|
|
962
|
+
fetchBaseBranch: () => fetchBaseBranch,
|
|
961
963
|
findContributorReleasePullRequests: () => findContributorReleasePullRequests,
|
|
962
964
|
findTaskCommitsOnBase: () => findTaskCommitsOnBase,
|
|
965
|
+
getBaseDivergence: () => getBaseDivergence,
|
|
963
966
|
getBranchDiff: () => getBranchDiff,
|
|
964
967
|
getCommitFiles: () => getCommitFiles,
|
|
965
968
|
getCommitsSinceTag: () => getCommitsSinceTag,
|
|
@@ -994,6 +997,7 @@ __export(git_exports, {
|
|
|
994
997
|
hasUnpushedCommits: () => hasUnpushedCommits,
|
|
995
998
|
isBranchContentAlreadyInBase: () => isBranchContentAlreadyInBase,
|
|
996
999
|
isBranchMergedInto: () => isBranchMergedInto,
|
|
1000
|
+
isCommitReachable: () => isCommitReachable,
|
|
997
1001
|
isGhAvailable: () => isGhAvailable,
|
|
998
1002
|
isGitAvailable: () => isGitAvailable,
|
|
999
1003
|
isGitRepo: () => isGitRepo,
|
|
@@ -2056,6 +2060,19 @@ function isBranchMergedInto(cwd, branch, baseBranch) {
|
|
|
2056
2060
|
return false;
|
|
2057
2061
|
}
|
|
2058
2062
|
}
|
|
2063
|
+
function isCommitReachable(cwd, commit, baseBranch) {
|
|
2064
|
+
const recordedCommit = commit.trim();
|
|
2065
|
+
if (!recordedCommit) return false;
|
|
2066
|
+
try {
|
|
2067
|
+
execFileSync("git", ["merge-base", "--is-ancestor", recordedCommit, baseBranch], {
|
|
2068
|
+
cwd,
|
|
2069
|
+
stdio: "ignore"
|
|
2070
|
+
});
|
|
2071
|
+
return true;
|
|
2072
|
+
} catch {
|
|
2073
|
+
return false;
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2059
2076
|
function isBranchContentAlreadyInBase(cwd, branch, baseBranch) {
|
|
2060
2077
|
const resolveCommit = (ref) => {
|
|
2061
2078
|
try {
|
|
@@ -2082,6 +2099,55 @@ function isBranchContentAlreadyInBase(cwd, branch, baseBranch) {
|
|
|
2082
2099
|
return false;
|
|
2083
2100
|
}
|
|
2084
2101
|
}
|
|
2102
|
+
function fetchBaseBranch(cwd, baseBranch, timeoutMs = GIT_FETCH_TIMEOUT_MS) {
|
|
2103
|
+
if (!isGitAvailable() || !isGitRepo(cwd) || !hasRemote(cwd)) {
|
|
2104
|
+
return { fetched: false, compareRef: baseBranch };
|
|
2105
|
+
}
|
|
2106
|
+
const result = spawnSync("git", ["fetch", "--quiet", "origin", baseBranch], {
|
|
2107
|
+
cwd,
|
|
2108
|
+
encoding: "utf-8",
|
|
2109
|
+
timeout: timeoutMs
|
|
2110
|
+
});
|
|
2111
|
+
if (result.error || result.status !== 0) {
|
|
2112
|
+
const raw = result.error?.message ?? ((result.stderr || "").trim() || `git exited ${result.status}`);
|
|
2113
|
+
const isTimeout = result.signal === "SIGTERM" || /ETIMEDOUT/.test(raw);
|
|
2114
|
+
return {
|
|
2115
|
+
fetched: false,
|
|
2116
|
+
compareRef: baseBranch,
|
|
2117
|
+
warning: isTimeout ? `Fetch from origin timed out after ${Math.round(timeoutMs / 1e3)}s \u2014 comparing against the local '${baseBranch}' ref, which may be stale.` : `Fetch from origin failed \u2014 comparing against the local '${baseBranch}' ref, which may be stale. (${raw})`
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
return { fetched: true, compareRef: `origin/${baseBranch}` };
|
|
2121
|
+
}
|
|
2122
|
+
function computeAheadBehind(cwd, compareRef, ref = "HEAD") {
|
|
2123
|
+
try {
|
|
2124
|
+
const out = execFileSync(
|
|
2125
|
+
"git",
|
|
2126
|
+
["rev-list", "--left-right", "--count", `${compareRef}...${ref}`],
|
|
2127
|
+
{ cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
|
|
2128
|
+
).trim();
|
|
2129
|
+
const [behindStr, aheadStr] = out.split(/\s+/);
|
|
2130
|
+
const behind = parseInt(behindStr, 10);
|
|
2131
|
+
const ahead = parseInt(aheadStr, 10);
|
|
2132
|
+
if (Number.isNaN(behind) || Number.isNaN(ahead)) return null;
|
|
2133
|
+
return { ahead, behind };
|
|
2134
|
+
} catch {
|
|
2135
|
+
return null;
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
function getBaseDivergence(cwd, preferredBase, opts) {
|
|
2139
|
+
const baseBranch = resolveBaseBranch(cwd, preferredBase);
|
|
2140
|
+
const fetch2 = fetchBaseBranch(cwd, baseBranch, opts?.timeoutMs);
|
|
2141
|
+
const divergence = computeAheadBehind(cwd, fetch2.compareRef, opts?.ref ?? "HEAD");
|
|
2142
|
+
return {
|
|
2143
|
+
baseBranch,
|
|
2144
|
+
compareRef: fetch2.compareRef,
|
|
2145
|
+
fetched: fetch2.fetched,
|
|
2146
|
+
ahead: divergence?.ahead ?? null,
|
|
2147
|
+
behind: divergence?.behind ?? null,
|
|
2148
|
+
warning: fetch2.warning
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2085
2151
|
function pickModuleCycleBranch(candidates, cycleNumber, module, memberSlug) {
|
|
2086
2152
|
if (candidates.length === 0) return void 0;
|
|
2087
2153
|
const expected = cycleBranchName(cycleNumber, module, memberSlug);
|
|
@@ -2186,7 +2252,7 @@ function getRemoteBranchFiles(cwd, branch, baseBranch) {
|
|
|
2186
2252
|
return [];
|
|
2187
2253
|
}
|
|
2188
2254
|
}
|
|
2189
|
-
var AUTO_WRITTEN_PATHS, GIT_NETWORK_TIMEOUT_MS, MERGE_RETRY_DELAY_MS, MERGE_MAX_RETRIES;
|
|
2255
|
+
var AUTO_WRITTEN_PATHS, GIT_NETWORK_TIMEOUT_MS, MERGE_RETRY_DELAY_MS, MERGE_MAX_RETRIES, GIT_FETCH_TIMEOUT_MS;
|
|
2190
2256
|
var init_git = __esm({
|
|
2191
2257
|
"src/lib/git.ts"() {
|
|
2192
2258
|
"use strict";
|
|
@@ -2194,6 +2260,7 @@ var init_git = __esm({
|
|
|
2194
2260
|
GIT_NETWORK_TIMEOUT_MS = 6e4;
|
|
2195
2261
|
MERGE_RETRY_DELAY_MS = 2e3;
|
|
2196
2262
|
MERGE_MAX_RETRIES = 3;
|
|
2263
|
+
GIT_FETCH_TIMEOUT_MS = 5e3;
|
|
2197
2264
|
}
|
|
2198
2265
|
});
|
|
2199
2266
|
|
|
@@ -10193,7 +10260,7 @@ function isProjectOwner(callerUserId, ownerUserId) {
|
|
|
10193
10260
|
if (caller.length === 0 || owner.length === 0) return false;
|
|
10194
10261
|
return caller === owner;
|
|
10195
10262
|
}
|
|
10196
|
-
var CYCLE_ROLES = ["owner", "editor"];
|
|
10263
|
+
var CYCLE_ROLES = ["owner", "release_manager", "editor"];
|
|
10197
10264
|
async function resolveCycleGate(adapter2, gate) {
|
|
10198
10265
|
if (!gate.enforced) return { allowed: true, role: null };
|
|
10199
10266
|
if (gate.callerIsOwner) return { allowed: true, role: "owner" };
|
|
@@ -10218,6 +10285,13 @@ async function resolveCycleGate(adapter2, gate) {
|
|
|
10218
10285
|
};
|
|
10219
10286
|
}
|
|
10220
10287
|
}
|
|
10288
|
+
function formatOwnerGateResolutionError(gate) {
|
|
10289
|
+
if (!gate.resolutionError) return null;
|
|
10290
|
+
if (gate.transport === "proxy") {
|
|
10291
|
+
return "Hosted bearer identity could not be verified. Retry once connectivity is restored.";
|
|
10292
|
+
}
|
|
10293
|
+
return `Identity resolution failed (${gate.resolutionError}) \u2014 the gate fails closed. Retry once connectivity is restored.`;
|
|
10294
|
+
}
|
|
10221
10295
|
async function resolveCallerUserId(adapter2, config2) {
|
|
10222
10296
|
const gate = await resolveOwnerGate(adapter2, config2);
|
|
10223
10297
|
if (gate.enforced && !gate.callerUserId) {
|
|
@@ -10229,7 +10303,7 @@ async function resolveOwnerGate(adapter2, config2) {
|
|
|
10229
10303
|
if (adapterSupports(adapter2, "getOwnerIdentity")) {
|
|
10230
10304
|
try {
|
|
10231
10305
|
const identity = await adapter2.getOwnerIdentity();
|
|
10232
|
-
const callerUserId = identity.callerUserId ??
|
|
10306
|
+
const callerUserId = identity.callerUserId ?? null;
|
|
10233
10307
|
return {
|
|
10234
10308
|
enforced: true,
|
|
10235
10309
|
callerIsOwner: isProjectOwner(callerUserId, identity.ownerUserId),
|
|
@@ -10241,7 +10315,7 @@ async function resolveOwnerGate(adapter2, config2) {
|
|
|
10241
10315
|
return {
|
|
10242
10316
|
enforced: true,
|
|
10243
10317
|
callerIsOwner: false,
|
|
10244
|
-
callerUserId:
|
|
10318
|
+
callerUserId: null,
|
|
10245
10319
|
ownerUserId: null,
|
|
10246
10320
|
resolutionError: err instanceof Error ? err.message : String(err),
|
|
10247
10321
|
transport: "proxy"
|
|
@@ -12193,11 +12267,11 @@ ${cleanContent}`;
|
|
|
12193
12267
|
if (!task.buildHandoff) {
|
|
12194
12268
|
await adapter2.updateTask?.(taskId, {
|
|
12195
12269
|
buildHandoff: h.buildHandoff,
|
|
12196
|
-
cycle:
|
|
12270
|
+
cycle: newCycleNumber
|
|
12197
12271
|
});
|
|
12198
12272
|
repairedHandoffs.push(taskId);
|
|
12199
|
-
} else if (task.cycle !==
|
|
12200
|
-
await adapter2.updateTask?.(taskId, { cycle:
|
|
12273
|
+
} else if (task.cycle !== newCycleNumber) {
|
|
12274
|
+
await adapter2.updateTask?.(taskId, { cycle: newCycleNumber });
|
|
12201
12275
|
repairedCycles.push(taskId);
|
|
12202
12276
|
}
|
|
12203
12277
|
} catch {
|
|
@@ -20948,6 +21022,27 @@ async function resolveCycleToClose(adapter2, version, callerUserId) {
|
|
|
20948
21022
|
}
|
|
20949
21023
|
return inferCycleFromVersion(version);
|
|
20950
21024
|
}
|
|
21025
|
+
async function findUnreachableDoneTaskCommits(config2, adapter2, cycleNumber, baseBranch) {
|
|
21026
|
+
if (cycleNumber <= 0 || !adapter2.getBuildReportsSince) return { unreachable: [] };
|
|
21027
|
+
const [tasks, reports] = await Promise.all([
|
|
21028
|
+
adapter2.queryBoard({ cycleSince: cycleNumber, compact: true }),
|
|
21029
|
+
adapter2.getBuildReportsSince(cycleNumber)
|
|
21030
|
+
]);
|
|
21031
|
+
const doneTasks = tasks.filter((task) => task.cycle === cycleNumber && task.status === "Done");
|
|
21032
|
+
const cycleReports = reports.filter((report) => report.cycle === cycleNumber && report.completed === "Yes");
|
|
21033
|
+
const fetch2 = fetchBaseBranch(config2.projectRoot, baseBranch);
|
|
21034
|
+
const unreachable = doneTasks.flatMap((task) => {
|
|
21035
|
+
const report = cycleReports.filter((candidate) => candidate.taskId === task.id || candidate.taskId === task.displayId || candidate.displayId === task.displayId).filter((candidate) => Boolean(candidate.commitSha?.trim())).sort((a, b2) => {
|
|
21036
|
+
const aDate = a.createdAt ?? a.date;
|
|
21037
|
+
const bDate = b2.createdAt ?? b2.date;
|
|
21038
|
+
return aDate.localeCompare(bDate);
|
|
21039
|
+
}).at(-1);
|
|
21040
|
+
const commitSha = report?.commitSha?.trim();
|
|
21041
|
+
if (!commitSha || isCommitReachable(config2.projectRoot, commitSha, fetch2.compareRef)) return [];
|
|
21042
|
+
return [{ taskId: task.displayId, commitSha }];
|
|
21043
|
+
});
|
|
21044
|
+
return { unreachable, fetchWarning: fetch2.warning };
|
|
21045
|
+
}
|
|
20951
21046
|
async function closeCycleState(config2, adapter2, version, cycleNum, options) {
|
|
20952
21047
|
const warnings = [];
|
|
20953
21048
|
const force = options?.force ?? false;
|
|
@@ -21143,7 +21238,7 @@ async function contributorAutoPrRelease(config2, adapter2, version, productionBa
|
|
|
21143
21238
|
const title = `Release ${version}: ${branch}`;
|
|
21144
21239
|
const body = `Contributor release PR for cycle ${cycleNum || "?"} (\`${branch}\`).
|
|
21145
21240
|
|
|
21146
|
-
Opened by a non-owner
|
|
21241
|
+
Opened by a non-owner release-capable member via \`release\` (task-2244). The project owner reviews and merges this into \`${productionBaseBranch}\`; the contributor's own cycle is already marked complete in PAPI.`;
|
|
21147
21242
|
const created = createPullRequest(config2.projectRoot, branch, productionBaseBranch, title, body);
|
|
21148
21243
|
prs.push(
|
|
21149
21244
|
created.success ? { branch, url: created.message.trim() || getPullRequestUrl(config2.projectRoot, branch) } : { branch, url: null, error: created.message }
|
|
@@ -21244,6 +21339,25 @@ async function createRelease(config2, branch, version, adapter2, cycleNum, optio
|
|
|
21244
21339
|
}
|
|
21245
21340
|
}
|
|
21246
21341
|
}
|
|
21342
|
+
if (adapter2 && resolvedCycleNum > 0) {
|
|
21343
|
+
const resolvedBase = resolveBaseBranch(config2.projectRoot, branch);
|
|
21344
|
+
const { unreachable, fetchWarning } = await findUnreachableDoneTaskCommits(
|
|
21345
|
+
config2,
|
|
21346
|
+
adapter2,
|
|
21347
|
+
resolvedCycleNum,
|
|
21348
|
+
resolvedBase
|
|
21349
|
+
);
|
|
21350
|
+
if (fetchWarning) warnings.push(fetchWarning);
|
|
21351
|
+
if (unreachable.length > 0) {
|
|
21352
|
+
const details = unreachable.map(({ taskId, commitSha }) => ` - ${taskId}: ${commitSha}`).join("\n");
|
|
21353
|
+
throw new Error(
|
|
21354
|
+
`Release blocked \u2014 ${unreachable.length} Done task implementation commit(s) are not reachable from ${resolvedBase}:
|
|
21355
|
+
${details}
|
|
21356
|
+
|
|
21357
|
+
Merge the recorded commit into the release branch, or correct the build receipt before retrying release.`
|
|
21358
|
+
);
|
|
21359
|
+
}
|
|
21360
|
+
}
|
|
21247
21361
|
if (adapter2 && resolvedCycleNum > 0) {
|
|
21248
21362
|
try {
|
|
21249
21363
|
const stampedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -21589,6 +21703,7 @@ function sanitiseBranchSuffix(branch) {
|
|
|
21589
21703
|
return branch.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
21590
21704
|
}
|
|
21591
21705
|
var CYCLE_UPDATES_CHANNEL_ENV = "DISCORD_CYCLE_UPDATES_CHANNEL_ID";
|
|
21706
|
+
var RELEASE_CAPABLE_ROLES = /* @__PURE__ */ new Set(["editor", "release_manager"]);
|
|
21592
21707
|
function buildCycleUpdateCurationDirective(version, cycleClosed, projectChannelId) {
|
|
21593
21708
|
const channelId = projectChannelId?.trim() || process.env[CYCLE_UPDATES_CHANNEL_ENV]?.trim();
|
|
21594
21709
|
if (!channelId) return null;
|
|
@@ -21772,9 +21887,10 @@ async function handleRelease(adapter2, config2, args, clientName) {
|
|
|
21772
21887
|
const editorsReleaseLikeOwner = isCapabilityEnabled(releaseCaps, "editorRelease");
|
|
21773
21888
|
const cycleGate = editorsReleaseLikeOwner ? await resolveCycleGate(adapter2, gate) : { allowed: gate.callerIsOwner, role: gate.callerIsOwner ? "owner" : null };
|
|
21774
21889
|
if (gate.enforced && !cycleGate.allowed) {
|
|
21775
|
-
const
|
|
21890
|
+
const resolutionMessage = formatOwnerGateResolutionError(gate);
|
|
21891
|
+
const resolutionNote = resolutionMessage ? `
|
|
21776
21892
|
|
|
21777
|
-
|
|
21893
|
+
${resolutionMessage}` : "";
|
|
21778
21894
|
if (gate.transport === "pg" && gate.callerUserId === null) {
|
|
21779
21895
|
return errorResponse(
|
|
21780
21896
|
`Release needs to know who you are, and this setup has no PAPI_USER_ID yet.
|
|
@@ -21788,17 +21904,18 @@ Then reconnect your AI tool and run release again. (Direct/pg setups read identi
|
|
|
21788
21904
|
}
|
|
21789
21905
|
tracker.mark("contributor-role-gate");
|
|
21790
21906
|
const callerRole = adapter2.getContributorRole && gate.callerUserId ? await adapter2.getContributorRole(gate.callerUserId).catch(() => null) : null;
|
|
21791
|
-
if (callerRole
|
|
21907
|
+
if (!RELEASE_CAPABLE_ROLES.has(callerRole ?? "")) {
|
|
21792
21908
|
const ownerHint = gate.transport === "pg" ? `If you ARE the owner, set PAPI_USER_ID to your account UUID in .mcp.json (getpapi.ai \u2192 Settings \u2192 Account).` : `If you ARE the owner, your identity comes from your API key \u2014 check you are using YOUR key for YOUR project.`;
|
|
21793
|
-
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
|
|
21909
|
+
const roleNote = callerRole === "viewer" ? `Your role on this project is "viewer", which cannot release. Ask the owner (or an editor or release manager) to release, or ask for release access.` : `Your identity does not match this project's owner, and you do not have an editor or release_manager role to open a release PR. If you are a contributor, ask the owner for editor or release_manager access (or push your branch and open a PR manually). ` + ownerHint;
|
|
21794
21910
|
return errorResponse(
|
|
21795
|
-
`Release to ${productionBaseBranch} needs an owner or
|
|
21911
|
+
`Release to ${productionBaseBranch} needs an owner, editor, or release_manager role.
|
|
21796
21912
|
|
|
21797
21913
|
` + roleNote + `
|
|
21798
21914
|
|
|
21799
21915
|
Releasing to a non-base branch (e.g. dev) is unaffected.` + resolutionNote
|
|
21800
21916
|
);
|
|
21801
21917
|
}
|
|
21918
|
+
const releaseRole = callerRole === "release_manager" ? "release manager" : "editor";
|
|
21802
21919
|
tracker.mark("contributor-pr-reconciliation");
|
|
21803
21920
|
try {
|
|
21804
21921
|
const reconciled = await reconcileContributorReleasePrs(config2, adapter2, gate.callerUserId, cycleToClose);
|
|
@@ -21833,7 +21950,7 @@ ${buildHostedGitDirective({
|
|
|
21833
21950
|
return textResponse(
|
|
21834
21951
|
`## Release ${version} \u2014 contributor PR merged
|
|
21835
21952
|
|
|
21836
|
-
PAPI detected that ${latest.prUrl} was merged on GitHub and reconciled the contributor release. GitHub granted the merge permission; your PAPI **
|
|
21953
|
+
PAPI detected that ${latest.prUrl} was merged on GitHub and reconciled the contributor release. GitHub granted the merge permission; your PAPI **${releaseRole}** role granted the release workflow.
|
|
21837
21954
|
|
|
21838
21955
|
${latest.cycle ? `Cycle ${latest.cycle}` : "The contributor cycle"} remains **complete**, and the release is now recorded as **released** in PAPI.` + localHandoff + `
|
|
21839
21956
|
|
|
@@ -21847,7 +21964,7 @@ Next: run \`plan\` to start your next cycle.`
|
|
|
21847
21964
|
|
|
21848
21965
|
${prLines}
|
|
21849
21966
|
|
|
21850
|
-
Your PAPI **
|
|
21967
|
+
Your PAPI **${releaseRole}** role allowed this release PR. Merge permission is controlled separately by GitHub: if GitHub lets you merge, review and merge it there; otherwise a repository maintainer must merge it.
|
|
21851
21968
|
|
|
21852
21969
|
After it is merged, run \`release\` again so PAPI can detect the merge and finish the release record.`
|
|
21853
21970
|
);
|
|
@@ -21857,7 +21974,7 @@ After it is merged, run \`release\` again so PAPI can detect the merge and finis
|
|
|
21857
21974
|
}
|
|
21858
21975
|
if (isHostedTransport()) {
|
|
21859
21976
|
return errorResponse(
|
|
21860
|
-
`Release blocked \u2014 your PAPI
|
|
21977
|
+
`Release blocked \u2014 your PAPI ${releaseRole} role is valid, but this hosted connection cannot access your local checkout or GitHub CLI to open the release PR. Run release from a workspace-connected MCP client for this repository, or ask the project owner to release it.`
|
|
21861
21978
|
);
|
|
21862
21979
|
}
|
|
21863
21980
|
tracker.mark("contributor-auto-pr");
|
|
@@ -21877,7 +21994,7 @@ After it is merged, run \`release\` again so PAPI can detect the merge and finis
|
|
|
21877
21994
|
return textResponse(
|
|
21878
21995
|
`## Release ${version} \u2014 contributor PR opened
|
|
21879
21996
|
|
|
21880
|
-
You released as
|
|
21997
|
+
You released as a **${releaseRole}**, so PAPI opened a pull request to \`${productionBaseBranch}\` instead of merging directly. GitHub merge permission is separate from your PAPI role: merge it yourself if GitHub allows, otherwise a repository maintainer must merge it.
|
|
21881
21998
|
|
|
21882
21999
|
**Pull request(s):**
|
|
21883
22000
|
${prLines.join("\n")}
|
|
@@ -22929,13 +23046,18 @@ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
|
22929
23046
|
if (!task) {
|
|
22930
23047
|
throw new Error(`Task "${taskId}" not found on the Cycle Board.`);
|
|
22931
23048
|
}
|
|
23049
|
+
const branchLines = [];
|
|
23050
|
+
const gate = await resolveOwnerGate(adapter2, config2);
|
|
22932
23051
|
if (task.assigneeId) {
|
|
22933
|
-
const gate = await resolveOwnerGate(adapter2, config2);
|
|
22934
23052
|
if (!gate.callerUserId || gate.callerUserId !== task.assigneeId) {
|
|
22935
23053
|
throw new Error(
|
|
22936
23054
|
`Task "${taskId}" (${task.title}) is claimed by another member \u2014 only its assignee can build it. Have the claimer build it, or run \`task_unclaim\` to release it first.`
|
|
22937
23055
|
);
|
|
22938
23056
|
}
|
|
23057
|
+
} else if (gate.enforced) {
|
|
23058
|
+
branchLines.push(
|
|
23059
|
+
`\u2139\uFE0F **${taskId}** has no assignee \u2014 this is an unclaimed pool task, buildable by anyone. Run \`task_claim\` first for exclusive ownership while you build it. (Not blocking.)`
|
|
23060
|
+
);
|
|
22939
23061
|
}
|
|
22940
23062
|
if (task.status === "Done" || task.status === "Archived") {
|
|
22941
23063
|
throw new Error(`Task "${taskId}" (${task.title}) is already ${task.status}. Cannot execute a completed task.`);
|
|
@@ -22973,7 +23095,6 @@ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
|
22973
23095
|
err.unresolvedDeps = unresolvedDeps;
|
|
22974
23096
|
throw err;
|
|
22975
23097
|
}
|
|
22976
|
-
const branchLines = [];
|
|
22977
23098
|
const startCaps = adapter2.getProjectInfo ? (await adapter2.getProjectInfo().catch(() => null))?.capabilities ?? {} : {};
|
|
22978
23099
|
const autoBranchEnabled = isCapabilityEnabled(startCaps, "autoBranch");
|
|
22979
23100
|
if (options.light) {
|
|
@@ -27490,11 +27611,17 @@ async function recordAdHoc(adapter2, input) {
|
|
|
27490
27611
|
const promoted = targetCycle !== null;
|
|
27491
27612
|
const landInReview = held || promoted && input.stage === "release";
|
|
27492
27613
|
let task;
|
|
27614
|
+
const warnings = [];
|
|
27493
27615
|
if (input.taskId) {
|
|
27494
27616
|
const existing = await adapter2.getTask(input.taskId);
|
|
27495
27617
|
if (!existing) {
|
|
27496
27618
|
throw new Error(`Task "${input.taskId}" not found on the board. Check the task ID and try again.`);
|
|
27497
27619
|
}
|
|
27620
|
+
if (!existing.assigneeId) {
|
|
27621
|
+
warnings.push(
|
|
27622
|
+
`\u2139\uFE0F ${input.taskId} has no assignee \u2014 recording ad-hoc work against an unclaimed pool task. If someone else might be building it too, check for an open PR/branch on the same files before merging.`
|
|
27623
|
+
);
|
|
27624
|
+
}
|
|
27498
27625
|
const updatePayload = {
|
|
27499
27626
|
notes: existing.notes ? `${existing.notes}
|
|
27500
27627
|
[ad-hoc] ${input.notes || "Work recorded via ad_hoc"}` : `[ad-hoc] ${input.notes || "Work recorded via ad_hoc"}`
|
|
@@ -27565,7 +27692,7 @@ async function recordAdHoc(adapter2, input) {
|
|
|
27565
27692
|
scopeAccuracy: "accurate"
|
|
27566
27693
|
};
|
|
27567
27694
|
await adapter2.appendBuildReport(report);
|
|
27568
|
-
return { task, report };
|
|
27695
|
+
return { task, report, warnings: warnings.length > 0 ? warnings : void 0 };
|
|
27569
27696
|
}
|
|
27570
27697
|
|
|
27571
27698
|
// src/tools/ad-hoc.ts
|
|
@@ -27776,6 +27903,7 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
27776
27903
|
} catch {
|
|
27777
27904
|
}
|
|
27778
27905
|
}
|
|
27906
|
+
const warningsNote = result.warnings?.length ? "\n\n" + result.warnings.map((w) => `> ${w}`).join("\n") : "";
|
|
27779
27907
|
const truncateWarning = notesTruncated ? ` (notes truncated to ${MAX_NOTES_LENGTH} chars)` : "";
|
|
27780
27908
|
const taskModule = result.task.module || "Core";
|
|
27781
27909
|
const typeLabel = result.task.taskType || typeRaw;
|
|
@@ -27808,7 +27936,7 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
27808
27936
|
} catch {
|
|
27809
27937
|
}
|
|
27810
27938
|
return textResponse(
|
|
27811
|
-
`**${result.task.id}:** "${result.task.title}" held for review (${effortRaw}, ${priorityRaw}, ${typeLabel}, ${taskModule})${overrideNote}.${truncateWarning}${promoNote} Build report attached
|
|
27939
|
+
`**${result.task.id}:** "${result.task.title}" held for review (${effortRaw}, ${priorityRaw}, ${typeLabel}, ${taskModule})${overrideNote}.${truncateWarning}${promoNote} Build report attached.` + warningsNote + `
|
|
27812
27940
|
|
|
27813
27941
|
## Held for the next cycle \u2014 branch + commit, do NOT merge
|
|
27814
27942
|
The task is recorded **In Review** and pinned to **Cycle ${result.task.cycle}**, so the planner won't re-plan it and it rides that cycle's review \u2192 release bundled with planned work.
|
|
@@ -27826,7 +27954,7 @@ _To correct: board_edit ${result.task.id} with updated fields._` + await recordA
|
|
|
27826
27954
|
);
|
|
27827
27955
|
}
|
|
27828
27956
|
return textResponse(
|
|
27829
|
-
`**${result.task.id}:** "${result.task.title}" recorded (${effortRaw}, ${priorityRaw}, ${typeLabel}, ${taskModule})${overrideNote}.${truncateWarning}${promoNote} Build report attached
|
|
27957
|
+
`**${result.task.id}:** "${result.task.title}" recorded (${effortRaw}, ${priorityRaw}, ${typeLabel}, ${taskModule})${overrideNote}.${truncateWarning}${promoNote} Build report attached.` + warningsNote + `
|
|
27830
27958
|
_To correct: board_edit ${result.task.id} with updated fields._` + await recordAdHocProposal(adapter2, args, { cycleNumber: result.task.cycle ?? 0, sourceTaskId: result.task.id }) + await adHocDecisionSection(target, result.task)
|
|
27831
27959
|
);
|
|
27832
27960
|
}
|
|
@@ -29226,14 +29354,14 @@ ${overlap}`;
|
|
|
29226
29354
|
const roleGate = await resolveCycleGate(adapter2, gate);
|
|
29227
29355
|
const callerMayRelease = editorReleaseEnabled ? roleGate.allowed : !gate.enforced || gate.callerIsOwner;
|
|
29228
29356
|
if (cycleTasks.length > 0 && cycleTasks.every((t) => t.status === "Done") && !callerMayRelease) {
|
|
29229
|
-
const why = roleGate.role === "viewer" ? `your role on this project is "viewer"` : roleGate.role === "editor" && !editorReleaseEnabled ? `your "
|
|
29357
|
+
const why = roleGate.role === "viewer" ? `your role on this project is "viewer"` : (roleGate.role === "editor" || roleGate.role === "release_manager") && !editorReleaseEnabled ? `your "${roleGate.role}" role is valid, but this project keeps auto-release owner-only because "Editors release like the owner" is off` : `your role on this project could not be confirmed`;
|
|
29230
29358
|
const resolutionNote = roleGate.resolutionError ? ` (Role resolution failed: ${roleGate.resolutionError} \u2014 the gate fails closed.)` : "";
|
|
29231
|
-
const nextStep = roleGate.role === "editor" && !editorReleaseEnabled ? `Run \`release\` explicitly to open your contributor PR.` : `Push your branch and open a PR, or ask for editor access.`;
|
|
29359
|
+
const nextStep = (roleGate.role === "editor" || roleGate.role === "release_manager") && !editorReleaseEnabled ? `Run \`release\` explicitly to open your contributor PR.` : `Push your branch and open a PR, or ask for editor or release manager access.`;
|
|
29232
29360
|
autoReleaseNote = `
|
|
29233
29361
|
|
|
29234
29362
|
---
|
|
29235
29363
|
|
|
29236
|
-
\u2705 Verdict recorded. All cycle tasks are Done, but no release was cut \u2014 **auto-release needs an owner or
|
|
29364
|
+
\u2705 Verdict recorded. All cycle tasks are Done, but no release was cut \u2014 **auto-release needs an owner, editor, or release manager role**, and ${why}. ${nextStep}${resolutionNote}`;
|
|
29237
29365
|
} else if (cycleTasks.length > 0 && cycleTasks.every((t) => t.status === "Done")) {
|
|
29238
29366
|
let planRunCount = null;
|
|
29239
29367
|
if (adapterSupports(adapter2, "countPlanRunsForCycle")) {
|
|
@@ -31347,6 +31475,7 @@ async function getHierarchyPosition(adapter2, projectId) {
|
|
|
31347
31475
|
}
|
|
31348
31476
|
}
|
|
31349
31477
|
var GIT_TAG_TIMEOUT_MS = 2e3;
|
|
31478
|
+
var ORIGIN_FETCH_TIMEOUT_MS = 3e3;
|
|
31350
31479
|
async function checkNpmVersionDrift() {
|
|
31351
31480
|
try {
|
|
31352
31481
|
const pkgPath = join18(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
|
|
@@ -31563,6 +31692,17 @@ async function computePendingDecisionsWarning(adapter2, callerUserId, decisions)
|
|
|
31563
31692
|
if (awaiting === 0) return void 0;
|
|
31564
31693
|
return `\u{1F4CB} ${awaiting} decision${awaiting === 1 ? "" : "s"} awaiting your position \u2014 run \`ad_view\` then \`decision_resolve\` to agree/object/abstain.`;
|
|
31565
31694
|
}
|
|
31695
|
+
function formatOriginDivergenceWarning(d) {
|
|
31696
|
+
if (d.behind != null && d.behind > 0) {
|
|
31697
|
+
const aheadNote = d.ahead && d.ahead > 0 ? ` (and ${d.ahead} ahead)` : "";
|
|
31698
|
+
return `\u26A0\uFE0F This checkout is ${d.behind} commit${d.behind === 1 ? "" : "s"} behind \`origin/${d.baseBranch}\`${aheadNote} \u2014 pull before trusting "Done"/merged state here.`;
|
|
31699
|
+
}
|
|
31700
|
+
if (d.warning) return `\u2139\uFE0F ${d.warning}`;
|
|
31701
|
+
if (d.fetched && d.behind == null) {
|
|
31702
|
+
return `\u2139\uFE0F Could not determine how far this checkout is behind \`origin/${d.baseBranch}\` (e.g. a shallow clone) \u2014 treat "Done"/merged state here as unverified.`;
|
|
31703
|
+
}
|
|
31704
|
+
return void 0;
|
|
31705
|
+
}
|
|
31566
31706
|
async function handleOrient(rawAdapter, config2, args = {}, clientName, serverVersion = "unknown") {
|
|
31567
31707
|
let resolvedAdapter = rawAdapter;
|
|
31568
31708
|
let projectOverrideNote = "";
|
|
@@ -31659,6 +31799,7 @@ async function handleOrient(rawAdapter, config2, args = {}, clientName, serverVe
|
|
|
31659
31799
|
ttfvOutcome,
|
|
31660
31800
|
latestTagOutcome,
|
|
31661
31801
|
versionDriftOutcome,
|
|
31802
|
+
originDivergenceOutcome,
|
|
31662
31803
|
researchSignalsOutcome,
|
|
31663
31804
|
recsOutcome,
|
|
31664
31805
|
pendingReviewOutcome,
|
|
@@ -31813,6 +31954,18 @@ async function handleOrient(rawAdapter, config2, args = {}, clientName, serverVe
|
|
|
31813
31954
|
// version-drift is enrichment, gated behind `full`/deep_housekeeping.
|
|
31814
31955
|
tracked("git-tag", async () => getLatestTag(config2.projectRoot, GIT_TAG_TIMEOUT_MS)),
|
|
31815
31956
|
tracked("npm-version-drift", async () => fullEnrichment ? checkNpmVersionDrift() : void 0),
|
|
31957
|
+
// Nova/vibe-tycoon feedback (task-XXXX): how far is this checkout
|
|
31958
|
+
// actually behind origin/<base>? Deliberately UNGATED by full/
|
|
31959
|
+
// deep_housekeeping — runs on every orient call by default, because the
|
|
31960
|
+
// fetch is bounded (ORIGIN_FETCH_TIMEOUT_MS) and fails soft. No remote
|
|
31961
|
+
// configured is the common no-op case: zero network I/O, zero latency.
|
|
31962
|
+
// Escape hatch mirrors PAPI_ORIENT_FANOUT_CONCURRENCY's env-var pattern,
|
|
31963
|
+
// default ON (opposite polarity from deep_housekeeping's opt-in — this
|
|
31964
|
+
// one must not require opting in).
|
|
31965
|
+
tracked("origin-divergence", async () => {
|
|
31966
|
+
if (!hasLocalWorkspace() || process.env.PAPI_ORIENT_ORIGIN_CHECK === "false") return void 0;
|
|
31967
|
+
return getBaseDivergence(config2.projectRoot, config2.baseBranch, { timeoutMs: ORIGIN_FETCH_TIMEOUT_MS });
|
|
31968
|
+
}),
|
|
31816
31969
|
// Research Signals — research docs with pending actions since last strategy review.
|
|
31817
31970
|
// task-2172: heavy (doc search + AD cross-reference) and rarely actioned
|
|
31818
31971
|
// mid-session — gated behind `full`/deep_housekeeping to keep the default lean.
|
|
@@ -32010,6 +32163,9 @@ async function handleOrient(rawAdapter, config2, args = {}, clientName, serverVe
|
|
|
32010
32163
|
if (proxyWarning) buildResult.warnings.push(proxyWarning);
|
|
32011
32164
|
const p1Warning = p1BacklogOutcome.status === "fulfilled" ? p1BacklogOutcome.value : void 0;
|
|
32012
32165
|
if (p1Warning) buildResult.warnings.push(p1Warning);
|
|
32166
|
+
const originDivergence = originDivergenceOutcome.status === "fulfilled" ? originDivergenceOutcome.value : void 0;
|
|
32167
|
+
const originDivergenceWarning = originDivergence ? formatOriginDivergenceWarning(originDivergence) : void 0;
|
|
32168
|
+
if (originDivergenceWarning) buildResult.warnings.push(originDivergenceWarning);
|
|
32013
32169
|
const pendingDecisionsWarning = pendingDecisionsOutcome.status === "fulfilled" ? pendingDecisionsOutcome.value : void 0;
|
|
32014
32170
|
if (pendingDecisionsWarning) buildResult.warnings.push(pendingDecisionsWarning);
|
|
32015
32171
|
const ownerActionsWarning = ownerActionsOutcome.status === "fulfilled" ? ownerActionsOutcome.value : void 0;
|
|
@@ -33350,7 +33506,7 @@ async function resolveDecisionAuthority(adapter2, gate, decisionType) {
|
|
|
33350
33506
|
allowed: isQuorumEligible,
|
|
33351
33507
|
role,
|
|
33352
33508
|
isOwnerOverride: false,
|
|
33353
|
-
...isQuorumEligible ? {} : { resolutionError: "only owner/editor members participate in group resolution" }
|
|
33509
|
+
...isQuorumEligible ? {} : { resolutionError: "only owner/release_manager/editor members participate in group resolution" }
|
|
33354
33510
|
};
|
|
33355
33511
|
}
|
|
33356
33512
|
function normaliseMode(value) {
|
|
@@ -33398,7 +33554,7 @@ var decisionResolveTool = {
|
|
|
33398
33554
|
action: {
|
|
33399
33555
|
type: "string",
|
|
33400
33556
|
enum: ["agree", "object", "abstain", "resolve", "withdraw"],
|
|
33401
|
-
description: 'agree/object/abstain cast your own position on a proposed decision. resolve/withdraw transition the decision itself and require authority \u2014 owner-only in "owner" mode, the configured resolver (or owner override) in "per_type" mode, or full owner/editor quorum (or an owner force-resolve) in "group" mode.'
|
|
33557
|
+
description: 'agree/object/abstain cast your own position on a proposed decision. resolve/withdraw transition the decision itself and require authority \u2014 owner-only in "owner" mode, the configured resolver (or owner override) in "per_type" mode, or full owner/release_manager/editor quorum (or an owner force-resolve) in "group" mode.'
|
|
33402
33558
|
},
|
|
33403
33559
|
comment: { type: "string", description: "Optional note recorded alongside your position or transition." }
|
|
33404
33560
|
},
|
|
@@ -33475,7 +33631,7 @@ async function handleDecisionResolve(adapter2, config2, args) {
|
|
|
33475
33631
|
positions.map((p) => ({ userId: p.userId, status: p.status })),
|
|
33476
33632
|
quorumMembersFrom(contributors)
|
|
33477
33633
|
);
|
|
33478
|
-
quorumNote = quorum.met ? '\n\nEvery owner/editor member has now agreed or abstained \u2014 this decision is ready to `decision_resolve action="resolve"`.' : `
|
|
33634
|
+
quorumNote = quorum.met ? '\n\nEvery owner/release_manager/editor member has now agreed or abstained \u2014 this decision is ready to `decision_resolve action="resolve"`.' : `
|
|
33479
33635
|
|
|
33480
33636
|
Still waiting on: ${quorum.missing.length > 0 ? quorum.missing.join(", ") : "nobody"}${quorum.objected.length > 0 ? `; objected: ${quorum.objected.join(", ")}` : ""}.`;
|
|
33481
33637
|
} catch {
|
|
@@ -34027,7 +34183,7 @@ Mapping after: ${mappingAfter}`
|
|
|
34027
34183
|
init_dist();
|
|
34028
34184
|
var contributorAddTool = {
|
|
34029
34185
|
name: "contributor_add",
|
|
34030
|
-
description: `Add a contributor to a project by email, or change an existing member's role (owner-only). The person must already have a PAPI account. Grants membership on project_contributors. Pass role="editor" for a working teammate who needs to run the full cycle \u2014 the default, "viewer",
|
|
34186
|
+
description: `Add a contributor to a project by email, or change an existing member's role (owner-only). The person must already have a PAPI account. Grants membership on project_contributors. Pass role="release_manager" for a teammate who owns approved releases, or role="editor" for a working teammate who needs to run the full cycle \u2014 the default, "viewer", is read-only. Calling it again with a different role promotes or demotes that member.`,
|
|
34031
34187
|
annotations: { title: "Add Contributor", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
34032
34188
|
inputSchema: {
|
|
34033
34189
|
type: "object",
|
|
@@ -34036,7 +34192,7 @@ var contributorAddTool = {
|
|
|
34036
34192
|
role: {
|
|
34037
34193
|
type: "string",
|
|
34038
34194
|
enum: [...ASSIGNABLE_CONTRIBUTOR_ROLES],
|
|
34039
|
-
description: `Membership role. "editor" can run the full build/review/release cycle; "viewer" has read-only visibility
|
|
34195
|
+
description: `Membership role. "release_manager" can run the build/review cycle and carry approved releases through the release path; "editor" can run the full build/review/release cycle; "viewer" has read-only visibility. Defaults to "viewer" for a new member; omit it when changing nothing to leave an existing member's role untouched. Project ownership cannot be granted here.`
|
|
34040
34196
|
},
|
|
34041
34197
|
project: {
|
|
34042
34198
|
type: "string",
|
|
@@ -34084,7 +34240,8 @@ async function denyUnlessOwner(adapter2, config2) {
|
|
|
34084
34240
|
if (capDenied) return capDenied;
|
|
34085
34241
|
const gate = await resolveOwnerGate(adapter2, config2);
|
|
34086
34242
|
if (gate.enforced && !gate.callerIsOwner) {
|
|
34087
|
-
const
|
|
34243
|
+
const resolutionMessage = formatOwnerGateResolutionError(gate);
|
|
34244
|
+
const note = resolutionMessage ? ` (${resolutionMessage})` : "";
|
|
34088
34245
|
return `Contributor management is restricted to the project owner. Your identity does not match this project's owner.${note}`;
|
|
34089
34246
|
}
|
|
34090
34247
|
return null;
|
|
@@ -34097,7 +34254,8 @@ async function denyUnlessMember(adapter2, config2) {
|
|
|
34097
34254
|
if (gate.callerIsOwner) return null;
|
|
34098
34255
|
const callerRole = gate.callerUserId && adapterSupports(adapter2, "getContributorRole") ? await adapter2.getContributorRole(gate.callerUserId).catch(() => null) : null;
|
|
34099
34256
|
if (callerRole) return null;
|
|
34100
|
-
const
|
|
34257
|
+
const resolutionMessage = formatOwnerGateResolutionError(gate);
|
|
34258
|
+
const note = resolutionMessage ? ` (${resolutionMessage})` : "";
|
|
34101
34259
|
return `Listing contributors is restricted to project members. Your identity does not match this project's owner or any contributor.${note}`;
|
|
34102
34260
|
}
|
|
34103
34261
|
var EMAIL_SHAPE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
@@ -34132,7 +34290,7 @@ async function handleContributorAdd(adapter2, config2, args) {
|
|
|
34132
34290
|
|
|
34133
34291
|
${upsell}` : "";
|
|
34134
34292
|
const where = overrideNote ? ` ${overrideNote}` : " on this project";
|
|
34135
|
-
const roleLine = entry.role === "editor" ? `They can run the full cycle \u2014 plan, build, review and release.` : `**They are a "${entry.role}" and cannot run \`release\`.** Re-run with role="editor" if they
|
|
34293
|
+
const roleLine = entry.role === "release_manager" ? `They can run the build/review cycle and carry approved releases through the release path.` : entry.role === "editor" ? `They can run the full cycle \u2014 plan, build, review and release.` : `**They are a "${entry.role}" and cannot run \`release\`.** Re-run with role="editor" or role="release_manager" if they need to work on the project.`;
|
|
34136
34294
|
return textResponse(
|
|
34137
34295
|
`\u2705 **${entry.email ?? email}**${name} is now a **${entry.role}**${where}.
|
|
34138
34296
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@papi-ai/server",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.103",
|
|
4
4
|
"description": "PAPI MCP server \u2014 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",
|