@papi-ai/server 0.7.98 → 0.7.104
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 +430 -70
- package/dist/prompts.js +6 -0
- package/package.json +2 -2
|
@@ -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
|
@@ -401,6 +401,7 @@ function parseBuildHandoff(markdown) {
|
|
|
401
401
|
taskTitle,
|
|
402
402
|
cycle,
|
|
403
403
|
whyNow,
|
|
404
|
+
relevantDecisions: parseBulletsOnly(sections.get("RELEVANT ACTIVE DECISIONS") ?? ""),
|
|
404
405
|
scope: parseBulletList(sections.get("SCOPE (DO THIS)") ?? ""),
|
|
405
406
|
scopeBoundary: parseBulletList(sections.get("SCOPE BOUNDARY (DO NOT DO THIS)") ?? ""),
|
|
406
407
|
acceptanceCriteria: parseChecklist(sections.get("ACCEPTANCE CRITERIA") ?? ""),
|
|
@@ -424,6 +425,7 @@ function coerceBuildHandoff(fields, taskId) {
|
|
|
424
425
|
taskTitle: str(fields.taskTitle),
|
|
425
426
|
cycle: typeof fields.cycle === "number" ? fields.cycle : 0,
|
|
426
427
|
whyNow: str(fields.whyNow),
|
|
428
|
+
relevantDecisions: ensureArray(fields.relevantDecisions),
|
|
427
429
|
scope,
|
|
428
430
|
scopeBoundary,
|
|
429
431
|
// task-3223: the structured path is where an object criterion (text + an
|
|
@@ -471,6 +473,14 @@ function serializeBuildHandoff(raw) {
|
|
|
471
473
|
lines.push(`Task: ${handoff.taskTitle}`);
|
|
472
474
|
lines.push(`Cycle: ${handoff.cycle}`);
|
|
473
475
|
lines.push(`Why now: ${handoff.whyNow}`);
|
|
476
|
+
const relevantDecisions = ensureArray(handoff.relevantDecisions);
|
|
477
|
+
if (relevantDecisions.length > 0) {
|
|
478
|
+
lines.push("");
|
|
479
|
+
lines.push("RELEVANT ACTIVE DECISIONS");
|
|
480
|
+
for (const item of relevantDecisions) {
|
|
481
|
+
lines.push(`- ${item}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
474
484
|
lines.push("");
|
|
475
485
|
lines.push("SCOPE (DO THIS)");
|
|
476
486
|
for (const item of ensureArray(handoff.scope)) {
|
|
@@ -850,7 +860,7 @@ var init_dist = __esm({
|
|
|
850
860
|
{ 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
861
|
];
|
|
852
862
|
CAPABILITY_KEYS = CAPABILITY_REGISTRY.map((c) => c.key);
|
|
853
|
-
ASSIGNABLE_CONTRIBUTOR_ROLES = ["editor", "viewer"];
|
|
863
|
+
ASSIGNABLE_CONTRIBUTOR_ROLES = ["release_manager", "editor", "viewer"];
|
|
854
864
|
SENSITIVE_CHANGELOG_PATTERNS = [
|
|
855
865
|
// ── Test-user machinery ────────────────────────────────────────────────
|
|
856
866
|
/@test\.papi\.dev/i,
|
|
@@ -912,6 +922,7 @@ var init_dist = __esm({
|
|
|
912
922
|
CHECK_VALUE_MAX = 200;
|
|
913
923
|
VALID_EFFORT_SIZES = /* @__PURE__ */ new Set(["XS", "S", "M", "L", "XL"]);
|
|
914
924
|
SECTION_HEADERS = [
|
|
925
|
+
"RELEVANT ACTIVE DECISIONS",
|
|
915
926
|
"SCOPE (DO THIS)",
|
|
916
927
|
"WHY NOT SIMPLER",
|
|
917
928
|
"SCOPE BOUNDARY (DO NOT DO THIS)",
|
|
@@ -948,6 +959,7 @@ __export(git_exports, {
|
|
|
948
959
|
checkoutBranch: () => checkoutBranch,
|
|
949
960
|
commitSinglePath: () => commitSinglePath,
|
|
950
961
|
commitStagedOnly: () => commitStagedOnly,
|
|
962
|
+
computeAheadBehind: () => computeAheadBehind,
|
|
951
963
|
createAndCheckoutBranch: () => createAndCheckoutBranch,
|
|
952
964
|
createPullRequest: () => createPullRequest,
|
|
953
965
|
createTag: () => createTag,
|
|
@@ -958,8 +970,10 @@ __export(git_exports, {
|
|
|
958
970
|
detectUnrecordedCommits: () => detectUnrecordedCommits,
|
|
959
971
|
ensureLatestDevelop: () => ensureLatestDevelop,
|
|
960
972
|
ensureTagAtHead: () => ensureTagAtHead,
|
|
973
|
+
fetchBaseBranch: () => fetchBaseBranch,
|
|
961
974
|
findContributorReleasePullRequests: () => findContributorReleasePullRequests,
|
|
962
975
|
findTaskCommitsOnBase: () => findTaskCommitsOnBase,
|
|
976
|
+
getBaseDivergence: () => getBaseDivergence,
|
|
963
977
|
getBranchDiff: () => getBranchDiff,
|
|
964
978
|
getCommitFiles: () => getCommitFiles,
|
|
965
979
|
getCommitsSinceTag: () => getCommitsSinceTag,
|
|
@@ -994,6 +1008,7 @@ __export(git_exports, {
|
|
|
994
1008
|
hasUnpushedCommits: () => hasUnpushedCommits,
|
|
995
1009
|
isBranchContentAlreadyInBase: () => isBranchContentAlreadyInBase,
|
|
996
1010
|
isBranchMergedInto: () => isBranchMergedInto,
|
|
1011
|
+
isCommitReachable: () => isCommitReachable,
|
|
997
1012
|
isGhAvailable: () => isGhAvailable,
|
|
998
1013
|
isGitAvailable: () => isGitAvailable,
|
|
999
1014
|
isGitRepo: () => isGitRepo,
|
|
@@ -2056,6 +2071,19 @@ function isBranchMergedInto(cwd, branch, baseBranch) {
|
|
|
2056
2071
|
return false;
|
|
2057
2072
|
}
|
|
2058
2073
|
}
|
|
2074
|
+
function isCommitReachable(cwd, commit, baseBranch) {
|
|
2075
|
+
const recordedCommit = commit.trim();
|
|
2076
|
+
if (!recordedCommit) return false;
|
|
2077
|
+
try {
|
|
2078
|
+
execFileSync("git", ["merge-base", "--is-ancestor", recordedCommit, baseBranch], {
|
|
2079
|
+
cwd,
|
|
2080
|
+
stdio: "ignore"
|
|
2081
|
+
});
|
|
2082
|
+
return true;
|
|
2083
|
+
} catch {
|
|
2084
|
+
return false;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2059
2087
|
function isBranchContentAlreadyInBase(cwd, branch, baseBranch) {
|
|
2060
2088
|
const resolveCommit = (ref) => {
|
|
2061
2089
|
try {
|
|
@@ -2082,6 +2110,55 @@ function isBranchContentAlreadyInBase(cwd, branch, baseBranch) {
|
|
|
2082
2110
|
return false;
|
|
2083
2111
|
}
|
|
2084
2112
|
}
|
|
2113
|
+
function fetchBaseBranch(cwd, baseBranch, timeoutMs = GIT_FETCH_TIMEOUT_MS) {
|
|
2114
|
+
if (!isGitAvailable() || !isGitRepo(cwd) || !hasRemote(cwd)) {
|
|
2115
|
+
return { fetched: false, compareRef: baseBranch };
|
|
2116
|
+
}
|
|
2117
|
+
const result = spawnSync("git", ["fetch", "--quiet", "origin", baseBranch], {
|
|
2118
|
+
cwd,
|
|
2119
|
+
encoding: "utf-8",
|
|
2120
|
+
timeout: timeoutMs
|
|
2121
|
+
});
|
|
2122
|
+
if (result.error || result.status !== 0) {
|
|
2123
|
+
const raw = result.error?.message ?? ((result.stderr || "").trim() || `git exited ${result.status}`);
|
|
2124
|
+
const isTimeout = result.signal === "SIGTERM" || /ETIMEDOUT/.test(raw);
|
|
2125
|
+
return {
|
|
2126
|
+
fetched: false,
|
|
2127
|
+
compareRef: baseBranch,
|
|
2128
|
+
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})`
|
|
2129
|
+
};
|
|
2130
|
+
}
|
|
2131
|
+
return { fetched: true, compareRef: `origin/${baseBranch}` };
|
|
2132
|
+
}
|
|
2133
|
+
function computeAheadBehind(cwd, compareRef, ref = "HEAD") {
|
|
2134
|
+
try {
|
|
2135
|
+
const out = execFileSync(
|
|
2136
|
+
"git",
|
|
2137
|
+
["rev-list", "--left-right", "--count", `${compareRef}...${ref}`],
|
|
2138
|
+
{ cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
|
|
2139
|
+
).trim();
|
|
2140
|
+
const [behindStr, aheadStr] = out.split(/\s+/);
|
|
2141
|
+
const behind = parseInt(behindStr, 10);
|
|
2142
|
+
const ahead = parseInt(aheadStr, 10);
|
|
2143
|
+
if (Number.isNaN(behind) || Number.isNaN(ahead)) return null;
|
|
2144
|
+
return { ahead, behind };
|
|
2145
|
+
} catch {
|
|
2146
|
+
return null;
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
function getBaseDivergence(cwd, preferredBase, opts) {
|
|
2150
|
+
const baseBranch = resolveBaseBranch(cwd, preferredBase);
|
|
2151
|
+
const fetch2 = fetchBaseBranch(cwd, baseBranch, opts?.timeoutMs);
|
|
2152
|
+
const divergence = computeAheadBehind(cwd, fetch2.compareRef, opts?.ref ?? "HEAD");
|
|
2153
|
+
return {
|
|
2154
|
+
baseBranch,
|
|
2155
|
+
compareRef: fetch2.compareRef,
|
|
2156
|
+
fetched: fetch2.fetched,
|
|
2157
|
+
ahead: divergence?.ahead ?? null,
|
|
2158
|
+
behind: divergence?.behind ?? null,
|
|
2159
|
+
warning: fetch2.warning
|
|
2160
|
+
};
|
|
2161
|
+
}
|
|
2085
2162
|
function pickModuleCycleBranch(candidates, cycleNumber, module, memberSlug) {
|
|
2086
2163
|
if (candidates.length === 0) return void 0;
|
|
2087
2164
|
const expected = cycleBranchName(cycleNumber, module, memberSlug);
|
|
@@ -2186,7 +2263,7 @@ function getRemoteBranchFiles(cwd, branch, baseBranch) {
|
|
|
2186
2263
|
return [];
|
|
2187
2264
|
}
|
|
2188
2265
|
}
|
|
2189
|
-
var AUTO_WRITTEN_PATHS, GIT_NETWORK_TIMEOUT_MS, MERGE_RETRY_DELAY_MS, MERGE_MAX_RETRIES;
|
|
2266
|
+
var AUTO_WRITTEN_PATHS, GIT_NETWORK_TIMEOUT_MS, MERGE_RETRY_DELAY_MS, MERGE_MAX_RETRIES, GIT_FETCH_TIMEOUT_MS;
|
|
2190
2267
|
var init_git = __esm({
|
|
2191
2268
|
"src/lib/git.ts"() {
|
|
2192
2269
|
"use strict";
|
|
@@ -2194,6 +2271,7 @@ var init_git = __esm({
|
|
|
2194
2271
|
GIT_NETWORK_TIMEOUT_MS = 6e4;
|
|
2195
2272
|
MERGE_RETRY_DELAY_MS = 2e3;
|
|
2196
2273
|
MERGE_MAX_RETRIES = 3;
|
|
2274
|
+
GIT_FETCH_TIMEOUT_MS = 5e3;
|
|
2197
2275
|
}
|
|
2198
2276
|
});
|
|
2199
2277
|
|
|
@@ -7505,11 +7583,78 @@ init_dist();
|
|
|
7505
7583
|
|
|
7506
7584
|
// src/lib/formatters.ts
|
|
7507
7585
|
init_dist();
|
|
7586
|
+
var CONFIDENCE_RANK = { HIGH: 0, MEDIUM: 1, LOW: 2 };
|
|
7587
|
+
function rankDecisionsForPlan(decisions) {
|
|
7588
|
+
return [...decisions].sort((a, b2) => {
|
|
7589
|
+
const confDiff = (CONFIDENCE_RANK[a.confidence] ?? 1) - (CONFIDENCE_RANK[b2.confidence] ?? 1);
|
|
7590
|
+
if (confDiff !== 0) return confDiff;
|
|
7591
|
+
return (b2.modifiedCycle ?? b2.createdCycle ?? 0) - (a.modifiedCycle ?? a.createdCycle ?? 0);
|
|
7592
|
+
});
|
|
7593
|
+
}
|
|
7508
7594
|
function formatActiveDecisionsForPlan(decisions) {
|
|
7509
7595
|
if (decisions.length === 0) return "No active decisions.";
|
|
7510
|
-
|
|
7596
|
+
const active = decisions.filter((d) => !d.superseded);
|
|
7597
|
+
if (active.length === 0) return "No active decisions.";
|
|
7598
|
+
const softBudget = Number(process.env.PAPI_AD_CONTEXT_BUDGET) || 4e4;
|
|
7599
|
+
const hardBudget = Math.round(softBudget * 1.3);
|
|
7600
|
+
const ranked = rankDecisionsForPlan(active);
|
|
7601
|
+
const fullBlocks = [];
|
|
7602
|
+
const oneLiners = [];
|
|
7603
|
+
let spent = 0;
|
|
7604
|
+
let degradedFrom = -1;
|
|
7605
|
+
for (let i = 0; i < ranked.length; i++) {
|
|
7606
|
+
const d = ranked[i];
|
|
7607
|
+
const fullBlock = `### ${d.id}: ${d.title} [Confidence: ${d.confidence}]
|
|
7608
|
+
|
|
7609
|
+
${d.body}`;
|
|
7610
|
+
const fullCost = Buffer.byteLength(`${fullBlock}
|
|
7611
|
+
|
|
7612
|
+
`, "utf-8");
|
|
7613
|
+
if (spent + fullCost <= softBudget) {
|
|
7614
|
+
fullBlocks.push(fullBlock);
|
|
7615
|
+
spent += fullCost;
|
|
7616
|
+
continue;
|
|
7617
|
+
}
|
|
7618
|
+
degradedFrom = i;
|
|
7619
|
+
break;
|
|
7620
|
+
}
|
|
7621
|
+
const compactedIds = [];
|
|
7622
|
+
const omittedIds = [];
|
|
7623
|
+
if (degradedFrom >= 0) {
|
|
7624
|
+
for (let i = degradedFrom; i < ranked.length; i++) {
|
|
7625
|
+
const d = ranked[i];
|
|
7626
|
+
const line = `- ${d.id} (${d.title}) [${d.confidence}]`;
|
|
7627
|
+
const cost = Buffer.byteLength(`${line}
|
|
7628
|
+
`, "utf-8");
|
|
7629
|
+
if (spent + cost <= hardBudget) {
|
|
7630
|
+
oneLiners.push(line);
|
|
7631
|
+
compactedIds.push(d.id);
|
|
7632
|
+
spent += cost;
|
|
7633
|
+
} else {
|
|
7634
|
+
omittedIds.push(d.id);
|
|
7635
|
+
}
|
|
7636
|
+
}
|
|
7637
|
+
}
|
|
7638
|
+
const parts = [...fullBlocks];
|
|
7639
|
+
if (oneLiners.length > 0) {
|
|
7640
|
+
parts.push(`### Other Active Decisions (compacted for budget)
|
|
7511
7641
|
|
|
7512
|
-
${
|
|
7642
|
+
${oneLiners.join("\n")}`);
|
|
7643
|
+
}
|
|
7644
|
+
if (compactedIds.length > 0 || omittedIds.length > 0) {
|
|
7645
|
+
const noteLines = [
|
|
7646
|
+
`### Context Budget \u2014 Active Decisions trimmed`,
|
|
7647
|
+
"",
|
|
7648
|
+
`${compactedIds.length + omittedIds.length} of ${ranked.length} Active Decisions were compacted to one-liners to keep this plan payload under ~${Math.round(softBudget / 1024)} KB (lowest confidence/oldest first). Full bodies above are complete and untrimmed.`
|
|
7649
|
+
];
|
|
7650
|
+
if (omittedIds.length > 0) {
|
|
7651
|
+
noteLines.push(
|
|
7652
|
+
`${omittedIds.length} AD one-liner(s) were additionally omitted from the list above for exceeding the hard budget, but are not forgotten: ${omittedIds.join(", ")}. Raise PAPI_AD_CONTEXT_BUDGET in the MCP server env to see their one-liners, or run ad_view for full detail on any specific one.`
|
|
7653
|
+
);
|
|
7654
|
+
}
|
|
7655
|
+
parts.push(noteLines.join("\n"));
|
|
7656
|
+
}
|
|
7657
|
+
return parts.join("\n\n");
|
|
7513
7658
|
}
|
|
7514
7659
|
function formatActiveDecisionsForReview(decisions) {
|
|
7515
7660
|
if (decisions.length === 0) return "No active decisions.";
|
|
@@ -8314,6 +8459,9 @@ Why now: [justification]
|
|
|
8314
8459
|
DEPENDS ON
|
|
8315
8460
|
[Optional \u2014 comma-separated task IDs this task depends on (e.g. "task-123, task-124"). Include only when another task in this same cycle must be built first because this task consumes artifacts it creates (e.g. new adapter method, new type, new migration). The builder will reuse the upstream task's branch so dependent commits stack on the same branch for a single PR. Omit this section entirely if there are no intra-cycle dependencies.]
|
|
8316
8461
|
|
|
8462
|
+
RELEVANT ACTIVE DECISIONS
|
|
8463
|
+
[Optional \u2014 self-check which Active Decisions actually bind the FILES LIKELY TOUCHED and module below, out of the full AD list already in your context. List the top 3-6 as "AD-N (title) \u2014 one-line reason this AD binds the files/approach". Rank by blast radius (an AD that would be VIOLATED by a naive implementation outranks one that's merely thematically adjacent). This is a judgement call \u2014 no server-side scoring feeds this. Omit this section entirely when nothing in the AD list is genuinely relevant to what this task touches; do not force a match.]
|
|
8464
|
+
|
|
8317
8465
|
SCOPE (DO THIS)
|
|
8318
8466
|
[specific deliverables \u2014 write for the simplest viable path first]
|
|
8319
8467
|
|
|
@@ -9649,6 +9797,9 @@ Why now: [justification]
|
|
|
9649
9797
|
DEPENDS ON
|
|
9650
9798
|
[Optional \u2014 comma-separated task IDs this task depends on (e.g. "task-123, task-124"). Include only when another task in this same cycle must be built first because this task consumes artifacts it creates (e.g. new adapter method, new type, new migration). The builder will reuse the upstream task's branch so dependent commits stack on the same branch for a single PR. Omit this section entirely if there are no intra-cycle dependencies.]
|
|
9651
9799
|
|
|
9800
|
+
RELEVANT ACTIVE DECISIONS
|
|
9801
|
+
[Optional \u2014 self-check which Active Decisions actually bind the FILES LIKELY TOUCHED and module below, out of the full AD list already in your context. List the top 3-6 as "AD-N (title) \u2014 one-line reason this AD binds the files/approach". Rank by blast radius (an AD that would be VIOLATED by a naive implementation outranks one that's merely thematically adjacent). This is a judgement call \u2014 no server-side scoring feeds this. Omit this section entirely when nothing in the AD list is genuinely relevant to what this task touches; do not force a match.]
|
|
9802
|
+
|
|
9652
9803
|
SCOPE (DO THIS)
|
|
9653
9804
|
[specific deliverables \u2014 write for the simplest viable path first]
|
|
9654
9805
|
|
|
@@ -10193,7 +10344,7 @@ function isProjectOwner(callerUserId, ownerUserId) {
|
|
|
10193
10344
|
if (caller.length === 0 || owner.length === 0) return false;
|
|
10194
10345
|
return caller === owner;
|
|
10195
10346
|
}
|
|
10196
|
-
var CYCLE_ROLES = ["owner", "editor"];
|
|
10347
|
+
var CYCLE_ROLES = ["owner", "release_manager", "editor"];
|
|
10197
10348
|
async function resolveCycleGate(adapter2, gate) {
|
|
10198
10349
|
if (!gate.enforced) return { allowed: true, role: null };
|
|
10199
10350
|
if (gate.callerIsOwner) return { allowed: true, role: "owner" };
|
|
@@ -10218,6 +10369,13 @@ async function resolveCycleGate(adapter2, gate) {
|
|
|
10218
10369
|
};
|
|
10219
10370
|
}
|
|
10220
10371
|
}
|
|
10372
|
+
function formatOwnerGateResolutionError(gate) {
|
|
10373
|
+
if (!gate.resolutionError) return null;
|
|
10374
|
+
if (gate.transport === "proxy") {
|
|
10375
|
+
return "Hosted bearer identity could not be verified. Retry once connectivity is restored.";
|
|
10376
|
+
}
|
|
10377
|
+
return `Identity resolution failed (${gate.resolutionError}) \u2014 the gate fails closed. Retry once connectivity is restored.`;
|
|
10378
|
+
}
|
|
10221
10379
|
async function resolveCallerUserId(adapter2, config2) {
|
|
10222
10380
|
const gate = await resolveOwnerGate(adapter2, config2);
|
|
10223
10381
|
if (gate.enforced && !gate.callerUserId) {
|
|
@@ -10229,7 +10387,7 @@ async function resolveOwnerGate(adapter2, config2) {
|
|
|
10229
10387
|
if (adapterSupports(adapter2, "getOwnerIdentity")) {
|
|
10230
10388
|
try {
|
|
10231
10389
|
const identity = await adapter2.getOwnerIdentity();
|
|
10232
|
-
const callerUserId = identity.callerUserId ??
|
|
10390
|
+
const callerUserId = identity.callerUserId ?? null;
|
|
10233
10391
|
return {
|
|
10234
10392
|
enforced: true,
|
|
10235
10393
|
callerIsOwner: isProjectOwner(callerUserId, identity.ownerUserId),
|
|
@@ -10241,7 +10399,7 @@ async function resolveOwnerGate(adapter2, config2) {
|
|
|
10241
10399
|
return {
|
|
10242
10400
|
enforced: true,
|
|
10243
10401
|
callerIsOwner: false,
|
|
10244
|
-
callerUserId:
|
|
10402
|
+
callerUserId: null,
|
|
10245
10403
|
ownerUserId: null,
|
|
10246
10404
|
resolutionError: err instanceof Error ? err.message : String(err),
|
|
10247
10405
|
transport: "proxy"
|
|
@@ -10796,6 +10954,23 @@ function idMatches(candidate, ref) {
|
|
|
10796
10954
|
if (!candidate) return false;
|
|
10797
10955
|
return candidate.toLowerCase() === ref.toLowerCase();
|
|
10798
10956
|
}
|
|
10957
|
+
function findDecision(ref, decisions) {
|
|
10958
|
+
return decisions.find((d) => idMatches(d.displayId, ref) || idMatches(d.id, ref));
|
|
10959
|
+
}
|
|
10960
|
+
function resolveCurrentDecision(ref, decisions, maxDepth = 10) {
|
|
10961
|
+
let current = findDecision(ref, decisions);
|
|
10962
|
+
if (!current) return void 0;
|
|
10963
|
+
const visited = /* @__PURE__ */ new Set([current.id]);
|
|
10964
|
+
let depth = 0;
|
|
10965
|
+
while (current?.superseded === true && current.supersededBy && depth < maxDepth) {
|
|
10966
|
+
const next = findDecision(current.supersededBy, decisions);
|
|
10967
|
+
if (!next || visited.has(next.id)) break;
|
|
10968
|
+
visited.add(next.id);
|
|
10969
|
+
current = next;
|
|
10970
|
+
depth += 1;
|
|
10971
|
+
}
|
|
10972
|
+
return current;
|
|
10973
|
+
}
|
|
10799
10974
|
function isBlockerResolved(blocker, ctx) {
|
|
10800
10975
|
if (!blocker || !blocker.ref) return false;
|
|
10801
10976
|
switch (blocker.type) {
|
|
@@ -10810,9 +10985,7 @@ function isBlockerResolved(blocker, ctx) {
|
|
|
10810
10985
|
return action != null && action.completed_at != null;
|
|
10811
10986
|
}
|
|
10812
10987
|
case "decision-gate": {
|
|
10813
|
-
const decision = ctx.decisions
|
|
10814
|
-
(d) => idMatches(d.displayId, blocker.ref) || idMatches(d.id, blocker.ref)
|
|
10815
|
-
);
|
|
10988
|
+
const decision = resolveCurrentDecision(blocker.ref, ctx.decisions);
|
|
10816
10989
|
if (decision?.superseded === true) return true;
|
|
10817
10990
|
const resolvedOutcomes = /* @__PURE__ */ new Set(["validated"]);
|
|
10818
10991
|
if (decision?.outcome && resolvedOutcomes.has(decision.outcome)) return true;
|
|
@@ -10828,9 +11001,7 @@ function isBlockerResolved(blocker, ctx) {
|
|
|
10828
11001
|
}
|
|
10829
11002
|
function blockerNeedsRedecision(blocker, ctx) {
|
|
10830
11003
|
if (!blocker || blocker.type !== "decision-gate" || !blocker.ref) return false;
|
|
10831
|
-
const decision = ctx.decisions
|
|
10832
|
-
(d) => idMatches(d.displayId, blocker.ref) || idMatches(d.id, blocker.ref)
|
|
10833
|
-
);
|
|
11004
|
+
const decision = resolveCurrentDecision(blocker.ref, ctx.decisions);
|
|
10834
11005
|
return decision?.resolutionState === "withdrawn";
|
|
10835
11006
|
}
|
|
10836
11007
|
function formatBlockerWaiting(blocker, refTitle) {
|
|
@@ -10853,9 +11024,7 @@ function resolveBlockerTitle(blocker, ctx) {
|
|
|
10853
11024
|
(t) => idMatches(t.displayId, blocker.ref) || idMatches(t.id, blocker.ref)
|
|
10854
11025
|
)?.title;
|
|
10855
11026
|
case "decision-gate":
|
|
10856
|
-
return ctx.decisions
|
|
10857
|
-
(d) => idMatches(d.displayId, blocker.ref) || idMatches(d.id, blocker.ref)
|
|
10858
|
-
)?.title;
|
|
11027
|
+
return resolveCurrentDecision(blocker.ref, ctx.decisions)?.title;
|
|
10859
11028
|
default:
|
|
10860
11029
|
return void 0;
|
|
10861
11030
|
}
|
|
@@ -12193,11 +12362,11 @@ ${cleanContent}`;
|
|
|
12193
12362
|
if (!task.buildHandoff) {
|
|
12194
12363
|
await adapter2.updateTask?.(taskId, {
|
|
12195
12364
|
buildHandoff: h.buildHandoff,
|
|
12196
|
-
cycle:
|
|
12365
|
+
cycle: newCycleNumber
|
|
12197
12366
|
});
|
|
12198
12367
|
repairedHandoffs.push(taskId);
|
|
12199
|
-
} else if (task.cycle !==
|
|
12200
|
-
await adapter2.updateTask?.(taskId, { cycle:
|
|
12368
|
+
} else if (task.cycle !== newCycleNumber) {
|
|
12369
|
+
await adapter2.updateTask?.(taskId, { cycle: newCycleNumber });
|
|
12201
12370
|
repairedCycles.push(taskId);
|
|
12202
12371
|
}
|
|
12203
12372
|
} catch {
|
|
@@ -17687,7 +17856,11 @@ var boardDeprioritiseTool = {
|
|
|
17687
17856
|
},
|
|
17688
17857
|
blocker_ref: {
|
|
17689
17858
|
type: "string",
|
|
17690
|
-
description: "Required when blocker_type is set. The identifier being waited on: a task display-id (depends-on), an AD/decision id (decision-gate), or an owner_action id (owner-action)."
|
|
17859
|
+
description: "Required when blocker_type is set, EXCEPT owner-action with owner_action_name (that mode creates the owner_action and derives the ref itself). The identifier being waited on: a task display-id (depends-on), an AD/decision id (decision-gate), or an existing owner_action id (owner-action, link mode)."
|
|
17860
|
+
},
|
|
17861
|
+
owner_action_name: {
|
|
17862
|
+
type: "string",
|
|
17863
|
+
description: 'Optional, blocker_type="owner-action" only. When set, CREATES a new owner action with this name and unlocks_task_id set to this task in the same insert, instead of linking to an existing one \u2014 collapses the old create-then-link two-step into one call. Omit blocker_ref in this mode; it is derived from the created row.'
|
|
17691
17864
|
},
|
|
17692
17865
|
defer: {
|
|
17693
17866
|
type: "boolean",
|
|
@@ -17705,15 +17878,26 @@ var boardDeprioritiseTool = {
|
|
|
17705
17878
|
required: ["task_id"],
|
|
17706
17879
|
// task-2802: mirror the handler's guards so the agent sees the dependency up
|
|
17707
17880
|
// front. handleBoardDeprioritise requires `reason` for both block and cancel,
|
|
17708
|
-
// and requires `blocker_ref` whenever `blocker_type` is set
|
|
17709
|
-
//
|
|
17881
|
+
// and requires `blocker_ref` whenever `blocker_type` is set — EXCEPT the
|
|
17882
|
+
// task-3451 owner-action create mode (blocker_type='owner-action' +
|
|
17883
|
+
// owner_action_name), which derives its ref from the row it creates.
|
|
17884
|
+
// Keyed on explicit values — an omitted action defaults to "backlog" and
|
|
17885
|
+
// triggers neither.
|
|
17710
17886
|
allOf: [
|
|
17711
17887
|
{
|
|
17712
17888
|
if: { properties: { action: { enum: ["block", "cancel"] } }, required: ["action"] },
|
|
17713
17889
|
then: { required: ["reason"] }
|
|
17714
17890
|
},
|
|
17715
17891
|
{
|
|
17716
|
-
if: {
|
|
17892
|
+
if: {
|
|
17893
|
+
required: ["blocker_type"],
|
|
17894
|
+
not: {
|
|
17895
|
+
allOf: [
|
|
17896
|
+
{ properties: { blocker_type: { const: "owner-action" } }, required: ["blocker_type"] },
|
|
17897
|
+
{ required: ["owner_action_name"] }
|
|
17898
|
+
]
|
|
17899
|
+
}
|
|
17900
|
+
},
|
|
17717
17901
|
then: { required: ["blocker_ref"] }
|
|
17718
17902
|
}
|
|
17719
17903
|
]
|
|
@@ -18018,13 +18202,18 @@ async function handleBoardDeprioritise(adapter2, args) {
|
|
|
18018
18202
|
return errorResponse("reason is required when blocking a task \u2014 explain what external dependency or gate is blocking it.");
|
|
18019
18203
|
}
|
|
18020
18204
|
const blockerType = args.blocker_type;
|
|
18021
|
-
|
|
18205
|
+
let blockerRef = args.blocker_ref;
|
|
18206
|
+
const ownerActionName = args.owner_action_name;
|
|
18207
|
+
const isOwnerActionCreateMode = blockerType === "owner-action" && Boolean(ownerActionName);
|
|
18022
18208
|
const validTypes = /* @__PURE__ */ new Set(["depends-on", "decision-gate", "owner-action"]);
|
|
18023
18209
|
if (blockerType !== void 0 && !validTypes.has(blockerType)) {
|
|
18024
18210
|
return errorResponse(`blocker_type must be one of: depends-on, decision-gate, owner-action (got "${blockerType}").`);
|
|
18025
18211
|
}
|
|
18026
|
-
if (blockerType !== void 0 && !blockerRef) {
|
|
18027
|
-
return errorResponse("blocker_ref is required when blocker_type is set \u2014 provide the task display-id, decision id, or owner_action id being waited on.");
|
|
18212
|
+
if (blockerType !== void 0 && !blockerRef && !isOwnerActionCreateMode) {
|
|
18213
|
+
return errorResponse("blocker_ref is required when blocker_type is set \u2014 provide the task display-id, decision id, or owner_action id being waited on (or owner_action_name to create a new owner action).");
|
|
18214
|
+
}
|
|
18215
|
+
if (ownerActionName && blockerType !== "owner-action") {
|
|
18216
|
+
return errorResponse('owner_action_name only applies when blocker_type is "owner-action".');
|
|
18028
18217
|
}
|
|
18029
18218
|
try {
|
|
18030
18219
|
const task = await adapter2.getTask(taskId);
|
|
@@ -18037,25 +18226,41 @@ async function handleBoardDeprioritise(adapter2, args) {
|
|
|
18037
18226
|
notes: `${existingNotes}BLOCKED: ${reason}`
|
|
18038
18227
|
};
|
|
18039
18228
|
let typedSuffix = "";
|
|
18040
|
-
if (blockerType !== void 0 && blockerRef) {
|
|
18041
|
-
|
|
18042
|
-
const blockedCycle = health?.totalCycles ?? 0;
|
|
18043
|
-
updates.blocker = {
|
|
18044
|
-
type: blockerType,
|
|
18045
|
-
ref: blockerRef,
|
|
18046
|
-
reason,
|
|
18047
|
-
blockedCycle
|
|
18048
|
-
};
|
|
18049
|
-
typedSuffix = `
|
|
18050
|
-
|
|
18051
|
-
Blocker: **${blockerType}** \u2192 ${blockerRef} (auto-unblock scanned at plan/orient).`;
|
|
18052
|
-
if (blockerType === "owner-action" && adapter2.linkOwnerActionToTask && adapter2.getProjectOwnerUserId) {
|
|
18229
|
+
if (blockerType !== void 0 && (blockerRef || isOwnerActionCreateMode)) {
|
|
18230
|
+
if (isOwnerActionCreateMode && adapter2.createOwnerAction && adapter2.getProjectOwnerUserId) {
|
|
18053
18231
|
try {
|
|
18054
18232
|
const ownerUserId = await adapter2.getProjectOwnerUserId();
|
|
18055
|
-
if (ownerUserId)
|
|
18233
|
+
if (ownerUserId) {
|
|
18234
|
+
const created = await adapter2.createOwnerAction(ownerUserId, {
|
|
18235
|
+
name: ownerActionName,
|
|
18236
|
+
dependency: reason,
|
|
18237
|
+
unlocks_task_id: task.uuid
|
|
18238
|
+
});
|
|
18239
|
+
blockerRef = created.id;
|
|
18240
|
+
}
|
|
18056
18241
|
} catch {
|
|
18057
18242
|
}
|
|
18058
18243
|
}
|
|
18244
|
+
if (blockerRef) {
|
|
18245
|
+
const health = await adapter2.getCycleHealth().catch(() => null);
|
|
18246
|
+
const blockedCycle = health?.totalCycles ?? 0;
|
|
18247
|
+
updates.blocker = {
|
|
18248
|
+
type: blockerType,
|
|
18249
|
+
ref: blockerRef,
|
|
18250
|
+
reason,
|
|
18251
|
+
blockedCycle
|
|
18252
|
+
};
|
|
18253
|
+
typedSuffix = `
|
|
18254
|
+
|
|
18255
|
+
Blocker: **${blockerType}** \u2192 ${blockerRef} (auto-unblock scanned at plan/orient).`;
|
|
18256
|
+
if (blockerType === "owner-action" && !isOwnerActionCreateMode && adapter2.linkOwnerActionToTask && adapter2.getProjectOwnerUserId) {
|
|
18257
|
+
try {
|
|
18258
|
+
const ownerUserId = await adapter2.getProjectOwnerUserId();
|
|
18259
|
+
if (ownerUserId) await adapter2.linkOwnerActionToTask(blockerRef, task.uuid, ownerUserId);
|
|
18260
|
+
} catch {
|
|
18261
|
+
}
|
|
18262
|
+
}
|
|
18263
|
+
}
|
|
18059
18264
|
}
|
|
18060
18265
|
await adapter2.updateTask(taskId, updates);
|
|
18061
18266
|
return textResponse(`Blocked **${taskId}** (${task.title}).
|
|
@@ -20948,6 +21153,27 @@ async function resolveCycleToClose(adapter2, version, callerUserId) {
|
|
|
20948
21153
|
}
|
|
20949
21154
|
return inferCycleFromVersion(version);
|
|
20950
21155
|
}
|
|
21156
|
+
async function findUnreachableDoneTaskCommits(config2, adapter2, cycleNumber, baseBranch) {
|
|
21157
|
+
if (cycleNumber <= 0 || !adapter2.getBuildReportsSince) return { unreachable: [] };
|
|
21158
|
+
const [tasks, reports] = await Promise.all([
|
|
21159
|
+
adapter2.queryBoard({ cycleSince: cycleNumber, compact: true }),
|
|
21160
|
+
adapter2.getBuildReportsSince(cycleNumber)
|
|
21161
|
+
]);
|
|
21162
|
+
const doneTasks = tasks.filter((task) => task.cycle === cycleNumber && task.status === "Done");
|
|
21163
|
+
const cycleReports = reports.filter((report) => report.cycle === cycleNumber && report.completed === "Yes");
|
|
21164
|
+
const fetch2 = fetchBaseBranch(config2.projectRoot, baseBranch);
|
|
21165
|
+
const unreachable = doneTasks.flatMap((task) => {
|
|
21166
|
+
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) => {
|
|
21167
|
+
const aDate = a.createdAt ?? a.date;
|
|
21168
|
+
const bDate = b2.createdAt ?? b2.date;
|
|
21169
|
+
return aDate.localeCompare(bDate);
|
|
21170
|
+
}).at(-1);
|
|
21171
|
+
const commitSha = report?.commitSha?.trim();
|
|
21172
|
+
if (!commitSha || isCommitReachable(config2.projectRoot, commitSha, fetch2.compareRef)) return [];
|
|
21173
|
+
return [{ taskId: task.displayId, commitSha }];
|
|
21174
|
+
});
|
|
21175
|
+
return { unreachable, fetchWarning: fetch2.warning };
|
|
21176
|
+
}
|
|
20951
21177
|
async function closeCycleState(config2, adapter2, version, cycleNum, options) {
|
|
20952
21178
|
const warnings = [];
|
|
20953
21179
|
const force = options?.force ?? false;
|
|
@@ -21143,7 +21369,7 @@ async function contributorAutoPrRelease(config2, adapter2, version, productionBa
|
|
|
21143
21369
|
const title = `Release ${version}: ${branch}`;
|
|
21144
21370
|
const body = `Contributor release PR for cycle ${cycleNum || "?"} (\`${branch}\`).
|
|
21145
21371
|
|
|
21146
|
-
Opened by a non-owner
|
|
21372
|
+
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
21373
|
const created = createPullRequest(config2.projectRoot, branch, productionBaseBranch, title, body);
|
|
21148
21374
|
prs.push(
|
|
21149
21375
|
created.success ? { branch, url: created.message.trim() || getPullRequestUrl(config2.projectRoot, branch) } : { branch, url: null, error: created.message }
|
|
@@ -21244,6 +21470,25 @@ async function createRelease(config2, branch, version, adapter2, cycleNum, optio
|
|
|
21244
21470
|
}
|
|
21245
21471
|
}
|
|
21246
21472
|
}
|
|
21473
|
+
if (adapter2 && resolvedCycleNum > 0) {
|
|
21474
|
+
const resolvedBase = resolveBaseBranch(config2.projectRoot, branch);
|
|
21475
|
+
const { unreachable, fetchWarning } = await findUnreachableDoneTaskCommits(
|
|
21476
|
+
config2,
|
|
21477
|
+
adapter2,
|
|
21478
|
+
resolvedCycleNum,
|
|
21479
|
+
resolvedBase
|
|
21480
|
+
);
|
|
21481
|
+
if (fetchWarning) warnings.push(fetchWarning);
|
|
21482
|
+
if (unreachable.length > 0) {
|
|
21483
|
+
const details = unreachable.map(({ taskId, commitSha }) => ` - ${taskId}: ${commitSha}`).join("\n");
|
|
21484
|
+
throw new Error(
|
|
21485
|
+
`Release blocked \u2014 ${unreachable.length} Done task implementation commit(s) are not reachable from ${resolvedBase}:
|
|
21486
|
+
${details}
|
|
21487
|
+
|
|
21488
|
+
Merge the recorded commit into the release branch, or correct the build receipt before retrying release.`
|
|
21489
|
+
);
|
|
21490
|
+
}
|
|
21491
|
+
}
|
|
21247
21492
|
if (adapter2 && resolvedCycleNum > 0) {
|
|
21248
21493
|
try {
|
|
21249
21494
|
const stampedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -21589,6 +21834,7 @@ function sanitiseBranchSuffix(branch) {
|
|
|
21589
21834
|
return branch.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
21590
21835
|
}
|
|
21591
21836
|
var CYCLE_UPDATES_CHANNEL_ENV = "DISCORD_CYCLE_UPDATES_CHANNEL_ID";
|
|
21837
|
+
var RELEASE_CAPABLE_ROLES = /* @__PURE__ */ new Set(["editor", "release_manager"]);
|
|
21592
21838
|
function buildCycleUpdateCurationDirective(version, cycleClosed, projectChannelId) {
|
|
21593
21839
|
const channelId = projectChannelId?.trim() || process.env[CYCLE_UPDATES_CHANNEL_ENV]?.trim();
|
|
21594
21840
|
if (!channelId) return null;
|
|
@@ -21772,9 +22018,10 @@ async function handleRelease(adapter2, config2, args, clientName) {
|
|
|
21772
22018
|
const editorsReleaseLikeOwner = isCapabilityEnabled(releaseCaps, "editorRelease");
|
|
21773
22019
|
const cycleGate = editorsReleaseLikeOwner ? await resolveCycleGate(adapter2, gate) : { allowed: gate.callerIsOwner, role: gate.callerIsOwner ? "owner" : null };
|
|
21774
22020
|
if (gate.enforced && !cycleGate.allowed) {
|
|
21775
|
-
const
|
|
22021
|
+
const resolutionMessage = formatOwnerGateResolutionError(gate);
|
|
22022
|
+
const resolutionNote = resolutionMessage ? `
|
|
21776
22023
|
|
|
21777
|
-
|
|
22024
|
+
${resolutionMessage}` : "";
|
|
21778
22025
|
if (gate.transport === "pg" && gate.callerUserId === null) {
|
|
21779
22026
|
return errorResponse(
|
|
21780
22027
|
`Release needs to know who you are, and this setup has no PAPI_USER_ID yet.
|
|
@@ -21788,17 +22035,18 @@ Then reconnect your AI tool and run release again. (Direct/pg setups read identi
|
|
|
21788
22035
|
}
|
|
21789
22036
|
tracker.mark("contributor-role-gate");
|
|
21790
22037
|
const callerRole = adapter2.getContributorRole && gate.callerUserId ? await adapter2.getContributorRole(gate.callerUserId).catch(() => null) : null;
|
|
21791
|
-
if (callerRole
|
|
22038
|
+
if (!RELEASE_CAPABLE_ROLES.has(callerRole ?? "")) {
|
|
21792
22039
|
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
|
|
22040
|
+
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
22041
|
return errorResponse(
|
|
21795
|
-
`Release to ${productionBaseBranch} needs an owner or
|
|
22042
|
+
`Release to ${productionBaseBranch} needs an owner, editor, or release_manager role.
|
|
21796
22043
|
|
|
21797
22044
|
` + roleNote + `
|
|
21798
22045
|
|
|
21799
22046
|
Releasing to a non-base branch (e.g. dev) is unaffected.` + resolutionNote
|
|
21800
22047
|
);
|
|
21801
22048
|
}
|
|
22049
|
+
const releaseRole = callerRole === "release_manager" ? "release manager" : "editor";
|
|
21802
22050
|
tracker.mark("contributor-pr-reconciliation");
|
|
21803
22051
|
try {
|
|
21804
22052
|
const reconciled = await reconcileContributorReleasePrs(config2, adapter2, gate.callerUserId, cycleToClose);
|
|
@@ -21833,7 +22081,7 @@ ${buildHostedGitDirective({
|
|
|
21833
22081
|
return textResponse(
|
|
21834
22082
|
`## Release ${version} \u2014 contributor PR merged
|
|
21835
22083
|
|
|
21836
|
-
PAPI detected that ${latest.prUrl} was merged on GitHub and reconciled the contributor release. GitHub granted the merge permission; your PAPI **
|
|
22084
|
+
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
22085
|
|
|
21838
22086
|
${latest.cycle ? `Cycle ${latest.cycle}` : "The contributor cycle"} remains **complete**, and the release is now recorded as **released** in PAPI.` + localHandoff + `
|
|
21839
22087
|
|
|
@@ -21847,7 +22095,7 @@ Next: run \`plan\` to start your next cycle.`
|
|
|
21847
22095
|
|
|
21848
22096
|
${prLines}
|
|
21849
22097
|
|
|
21850
|
-
Your PAPI **
|
|
22098
|
+
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
22099
|
|
|
21852
22100
|
After it is merged, run \`release\` again so PAPI can detect the merge and finish the release record.`
|
|
21853
22101
|
);
|
|
@@ -21857,7 +22105,7 @@ After it is merged, run \`release\` again so PAPI can detect the merge and finis
|
|
|
21857
22105
|
}
|
|
21858
22106
|
if (isHostedTransport()) {
|
|
21859
22107
|
return errorResponse(
|
|
21860
|
-
`Release blocked \u2014 your PAPI
|
|
22108
|
+
`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
22109
|
);
|
|
21862
22110
|
}
|
|
21863
22111
|
tracker.mark("contributor-auto-pr");
|
|
@@ -21877,7 +22125,7 @@ After it is merged, run \`release\` again so PAPI can detect the merge and finis
|
|
|
21877
22125
|
return textResponse(
|
|
21878
22126
|
`## Release ${version} \u2014 contributor PR opened
|
|
21879
22127
|
|
|
21880
|
-
You released as
|
|
22128
|
+
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
22129
|
|
|
21882
22130
|
**Pull request(s):**
|
|
21883
22131
|
${prLines.join("\n")}
|
|
@@ -22929,13 +23177,18 @@ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
|
22929
23177
|
if (!task) {
|
|
22930
23178
|
throw new Error(`Task "${taskId}" not found on the Cycle Board.`);
|
|
22931
23179
|
}
|
|
23180
|
+
const branchLines = [];
|
|
23181
|
+
const gate = await resolveOwnerGate(adapter2, config2);
|
|
22932
23182
|
if (task.assigneeId) {
|
|
22933
|
-
const gate = await resolveOwnerGate(adapter2, config2);
|
|
22934
23183
|
if (!gate.callerUserId || gate.callerUserId !== task.assigneeId) {
|
|
22935
23184
|
throw new Error(
|
|
22936
23185
|
`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
23186
|
);
|
|
22938
23187
|
}
|
|
23188
|
+
} else if (gate.enforced) {
|
|
23189
|
+
branchLines.push(
|
|
23190
|
+
`\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.)`
|
|
23191
|
+
);
|
|
22939
23192
|
}
|
|
22940
23193
|
if (task.status === "Done" || task.status === "Archived") {
|
|
22941
23194
|
throw new Error(`Task "${taskId}" (${task.title}) is already ${task.status}. Cannot execute a completed task.`);
|
|
@@ -22973,7 +23226,6 @@ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
|
22973
23226
|
err.unresolvedDeps = unresolvedDeps;
|
|
22974
23227
|
throw err;
|
|
22975
23228
|
}
|
|
22976
|
-
const branchLines = [];
|
|
22977
23229
|
const startCaps = adapter2.getProjectInfo ? (await adapter2.getProjectInfo().catch(() => null))?.capabilities ?? {} : {};
|
|
22978
23230
|
const autoBranchEnabled = isCapabilityEnabled(startCaps, "autoBranch");
|
|
22979
23231
|
if (options.light) {
|
|
@@ -24656,6 +24908,41 @@ The registry stores metadata and a summary \u2014 not the body. This doc has one
|
|
|
24656
24908
|
return `
|
|
24657
24909
|
- **Durability:** committed \`${path8}\` (was untracked)`;
|
|
24658
24910
|
}
|
|
24911
|
+
var DOC_OVERLAP_COVERAGE_THRESHOLD = 0.6;
|
|
24912
|
+
async function findOverlappingDocs(target, type, title, summary) {
|
|
24913
|
+
if (!target.searchDocs) return [];
|
|
24914
|
+
const newKeywords = extractKeywords(`${title} ${summary}`);
|
|
24915
|
+
if (newKeywords.size < 2) return [];
|
|
24916
|
+
let candidates;
|
|
24917
|
+
try {
|
|
24918
|
+
candidates = await target.searchDocs({ type, status: "active" });
|
|
24919
|
+
} catch {
|
|
24920
|
+
return [];
|
|
24921
|
+
}
|
|
24922
|
+
const matches = [];
|
|
24923
|
+
for (const doc of candidates) {
|
|
24924
|
+
const docKeywords = extractKeywords(`${doc.title} ${doc.summary}`);
|
|
24925
|
+
if (docKeywords.size < 2) continue;
|
|
24926
|
+
let covered = 0;
|
|
24927
|
+
for (const word of newKeywords) if (docKeywords.has(word)) covered++;
|
|
24928
|
+
const coverage = covered / newKeywords.size;
|
|
24929
|
+
if (coverage >= DOC_OVERLAP_COVERAGE_THRESHOLD) {
|
|
24930
|
+
matches.push({ path: doc.path, title: doc.title, coverage });
|
|
24931
|
+
}
|
|
24932
|
+
}
|
|
24933
|
+
return matches.sort((a, b2) => b2.coverage - a.coverage).slice(0, 3);
|
|
24934
|
+
}
|
|
24935
|
+
function overlapReconciliationNote(overlaps) {
|
|
24936
|
+
if (overlaps.length === 0) return "";
|
|
24937
|
+
const named = overlaps.map((o) => `\`${o.path}\` ("${o.title}")`).join(", ");
|
|
24938
|
+
return `
|
|
24939
|
+
|
|
24940
|
+
\u26A0\uFE0F **Possible overlap** with existing active doc(s): ${named} \u2014 verify it is real, then classify:
|
|
24941
|
+
- **AGREES** \u2014 restates the existing doc. Don't duplicate; nothing to register.
|
|
24942
|
+
- **EXTENDS** \u2014 new ground the existing doc doesn't cover. Fine to register as-is.
|
|
24943
|
+
- **CONTRADICTS** \u2014 cuts against the existing doc. Surface to the owner \u2014 never resolve silently (task-3219: a contradiction is not a veto).
|
|
24944
|
+
- **SUPERSEDES** \u2014 this doc should replace the existing one. Re-run doc_register with \`superseded_by_path\` pointing at the overlapping doc's path.`;
|
|
24945
|
+
}
|
|
24659
24946
|
async function handleDocRegister(adapter2, args, config2) {
|
|
24660
24947
|
const adapterType = config2?.adapterType ?? "unknown";
|
|
24661
24948
|
const continueHint = "doc_register is advisory \u2014 your build/plan/review flow is unaffected. Fix the input (or wait for the registry to recover) and re-run doc_register; or just continue without it.";
|
|
@@ -24714,6 +25001,7 @@ async function handleDocRegister(adapter2, args, config2) {
|
|
|
24714
25001
|
);
|
|
24715
25002
|
}
|
|
24716
25003
|
try {
|
|
25004
|
+
const overlaps = supersededByPath ? [] : await findOverlappingDocs(target, type, title, summary);
|
|
24717
25005
|
let supersededBy;
|
|
24718
25006
|
if (supersededByPath) {
|
|
24719
25007
|
const existing = await target.getDoc?.(supersededByPath);
|
|
@@ -24789,7 +25077,7 @@ ${decision.message}`;
|
|
|
24789
25077
|
- **Visibility:** ${visibilityLabel}
|
|
24790
25078
|
- **Tags:** ${entry.tags.length > 0 ? entry.tags.join(", ") : "none"}
|
|
24791
25079
|
- **Actions:** ${actions?.length ?? 0} items
|
|
24792
|
-
- **ID:** ${entry.id}` + bodyNote + durability
|
|
25080
|
+
- **ID:** ${entry.id}` + bodyNote + durability + overlapReconciliationNote(overlaps)
|
|
24793
25081
|
);
|
|
24794
25082
|
} catch (err) {
|
|
24795
25083
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -27490,11 +27778,17 @@ async function recordAdHoc(adapter2, input) {
|
|
|
27490
27778
|
const promoted = targetCycle !== null;
|
|
27491
27779
|
const landInReview = held || promoted && input.stage === "release";
|
|
27492
27780
|
let task;
|
|
27781
|
+
const warnings = [];
|
|
27493
27782
|
if (input.taskId) {
|
|
27494
27783
|
const existing = await adapter2.getTask(input.taskId);
|
|
27495
27784
|
if (!existing) {
|
|
27496
27785
|
throw new Error(`Task "${input.taskId}" not found on the board. Check the task ID and try again.`);
|
|
27497
27786
|
}
|
|
27787
|
+
if (!existing.assigneeId) {
|
|
27788
|
+
warnings.push(
|
|
27789
|
+
`\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.`
|
|
27790
|
+
);
|
|
27791
|
+
}
|
|
27498
27792
|
const updatePayload = {
|
|
27499
27793
|
notes: existing.notes ? `${existing.notes}
|
|
27500
27794
|
[ad-hoc] ${input.notes || "Work recorded via ad_hoc"}` : `[ad-hoc] ${input.notes || "Work recorded via ad_hoc"}`
|
|
@@ -27565,7 +27859,7 @@ async function recordAdHoc(adapter2, input) {
|
|
|
27565
27859
|
scopeAccuracy: "accurate"
|
|
27566
27860
|
};
|
|
27567
27861
|
await adapter2.appendBuildReport(report);
|
|
27568
|
-
return { task, report };
|
|
27862
|
+
return { task, report, warnings: warnings.length > 0 ? warnings : void 0 };
|
|
27569
27863
|
}
|
|
27570
27864
|
|
|
27571
27865
|
// src/tools/ad-hoc.ts
|
|
@@ -27776,6 +28070,7 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
27776
28070
|
} catch {
|
|
27777
28071
|
}
|
|
27778
28072
|
}
|
|
28073
|
+
const warningsNote = result.warnings?.length ? "\n\n" + result.warnings.map((w) => `> ${w}`).join("\n") : "";
|
|
27779
28074
|
const truncateWarning = notesTruncated ? ` (notes truncated to ${MAX_NOTES_LENGTH} chars)` : "";
|
|
27780
28075
|
const taskModule = result.task.module || "Core";
|
|
27781
28076
|
const typeLabel = result.task.taskType || typeRaw;
|
|
@@ -27808,7 +28103,7 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
27808
28103
|
} catch {
|
|
27809
28104
|
}
|
|
27810
28105
|
return textResponse(
|
|
27811
|
-
`**${result.task.id}:** "${result.task.title}" held for review (${effortRaw}, ${priorityRaw}, ${typeLabel}, ${taskModule})${overrideNote}.${truncateWarning}${promoNote} Build report attached
|
|
28106
|
+
`**${result.task.id}:** "${result.task.title}" held for review (${effortRaw}, ${priorityRaw}, ${typeLabel}, ${taskModule})${overrideNote}.${truncateWarning}${promoNote} Build report attached.` + warningsNote + `
|
|
27812
28107
|
|
|
27813
28108
|
## Held for the next cycle \u2014 branch + commit, do NOT merge
|
|
27814
28109
|
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 +28121,7 @@ _To correct: board_edit ${result.task.id} with updated fields._` + await recordA
|
|
|
27826
28121
|
);
|
|
27827
28122
|
}
|
|
27828
28123
|
return textResponse(
|
|
27829
|
-
`**${result.task.id}:** "${result.task.title}" recorded (${effortRaw}, ${priorityRaw}, ${typeLabel}, ${taskModule})${overrideNote}.${truncateWarning}${promoNote} Build report attached
|
|
28124
|
+
`**${result.task.id}:** "${result.task.title}" recorded (${effortRaw}, ${priorityRaw}, ${typeLabel}, ${taskModule})${overrideNote}.${truncateWarning}${promoNote} Build report attached.` + warningsNote + `
|
|
27830
28125
|
_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
28126
|
);
|
|
27832
28127
|
}
|
|
@@ -29226,14 +29521,14 @@ ${overlap}`;
|
|
|
29226
29521
|
const roleGate = await resolveCycleGate(adapter2, gate);
|
|
29227
29522
|
const callerMayRelease = editorReleaseEnabled ? roleGate.allowed : !gate.enforced || gate.callerIsOwner;
|
|
29228
29523
|
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 "
|
|
29524
|
+
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
29525
|
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.`;
|
|
29526
|
+
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
29527
|
autoReleaseNote = `
|
|
29233
29528
|
|
|
29234
29529
|
---
|
|
29235
29530
|
|
|
29236
|
-
\u2705 Verdict recorded. All cycle tasks are Done, but no release was cut \u2014 **auto-release needs an owner or
|
|
29531
|
+
\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
29532
|
} else if (cycleTasks.length > 0 && cycleTasks.every((t) => t.status === "Done")) {
|
|
29238
29533
|
let planRunCount = null;
|
|
29239
29534
|
if (adapterSupports(adapter2, "countPlanRunsForCycle")) {
|
|
@@ -31347,6 +31642,7 @@ async function getHierarchyPosition(adapter2, projectId) {
|
|
|
31347
31642
|
}
|
|
31348
31643
|
}
|
|
31349
31644
|
var GIT_TAG_TIMEOUT_MS = 2e3;
|
|
31645
|
+
var ORIGIN_FETCH_TIMEOUT_MS = 3e3;
|
|
31350
31646
|
async function checkNpmVersionDrift() {
|
|
31351
31647
|
try {
|
|
31352
31648
|
const pkgPath = join18(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
|
|
@@ -31563,6 +31859,17 @@ async function computePendingDecisionsWarning(adapter2, callerUserId, decisions)
|
|
|
31563
31859
|
if (awaiting === 0) return void 0;
|
|
31564
31860
|
return `\u{1F4CB} ${awaiting} decision${awaiting === 1 ? "" : "s"} awaiting your position \u2014 run \`ad_view\` then \`decision_resolve\` to agree/object/abstain.`;
|
|
31565
31861
|
}
|
|
31862
|
+
function formatOriginDivergenceWarning(d) {
|
|
31863
|
+
if (d.behind != null && d.behind > 0) {
|
|
31864
|
+
const aheadNote = d.ahead && d.ahead > 0 ? ` (and ${d.ahead} ahead)` : "";
|
|
31865
|
+
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.`;
|
|
31866
|
+
}
|
|
31867
|
+
if (d.warning) return `\u2139\uFE0F ${d.warning}`;
|
|
31868
|
+
if (d.fetched && d.behind == null) {
|
|
31869
|
+
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.`;
|
|
31870
|
+
}
|
|
31871
|
+
return void 0;
|
|
31872
|
+
}
|
|
31566
31873
|
async function handleOrient(rawAdapter, config2, args = {}, clientName, serverVersion = "unknown") {
|
|
31567
31874
|
let resolvedAdapter = rawAdapter;
|
|
31568
31875
|
let projectOverrideNote = "";
|
|
@@ -31659,6 +31966,7 @@ async function handleOrient(rawAdapter, config2, args = {}, clientName, serverVe
|
|
|
31659
31966
|
ttfvOutcome,
|
|
31660
31967
|
latestTagOutcome,
|
|
31661
31968
|
versionDriftOutcome,
|
|
31969
|
+
originDivergenceOutcome,
|
|
31662
31970
|
researchSignalsOutcome,
|
|
31663
31971
|
recsOutcome,
|
|
31664
31972
|
pendingReviewOutcome,
|
|
@@ -31813,6 +32121,18 @@ async function handleOrient(rawAdapter, config2, args = {}, clientName, serverVe
|
|
|
31813
32121
|
// version-drift is enrichment, gated behind `full`/deep_housekeeping.
|
|
31814
32122
|
tracked("git-tag", async () => getLatestTag(config2.projectRoot, GIT_TAG_TIMEOUT_MS)),
|
|
31815
32123
|
tracked("npm-version-drift", async () => fullEnrichment ? checkNpmVersionDrift() : void 0),
|
|
32124
|
+
// Nova/vibe-tycoon feedback (task-XXXX): how far is this checkout
|
|
32125
|
+
// actually behind origin/<base>? Deliberately UNGATED by full/
|
|
32126
|
+
// deep_housekeeping — runs on every orient call by default, because the
|
|
32127
|
+
// fetch is bounded (ORIGIN_FETCH_TIMEOUT_MS) and fails soft. No remote
|
|
32128
|
+
// configured is the common no-op case: zero network I/O, zero latency.
|
|
32129
|
+
// Escape hatch mirrors PAPI_ORIENT_FANOUT_CONCURRENCY's env-var pattern,
|
|
32130
|
+
// default ON (opposite polarity from deep_housekeeping's opt-in — this
|
|
32131
|
+
// one must not require opting in).
|
|
32132
|
+
tracked("origin-divergence", async () => {
|
|
32133
|
+
if (!hasLocalWorkspace() || process.env.PAPI_ORIENT_ORIGIN_CHECK === "false") return void 0;
|
|
32134
|
+
return getBaseDivergence(config2.projectRoot, config2.baseBranch, { timeoutMs: ORIGIN_FETCH_TIMEOUT_MS });
|
|
32135
|
+
}),
|
|
31816
32136
|
// Research Signals — research docs with pending actions since last strategy review.
|
|
31817
32137
|
// task-2172: heavy (doc search + AD cross-reference) and rarely actioned
|
|
31818
32138
|
// mid-session — gated behind `full`/deep_housekeeping to keep the default lean.
|
|
@@ -32010,6 +32330,9 @@ async function handleOrient(rawAdapter, config2, args = {}, clientName, serverVe
|
|
|
32010
32330
|
if (proxyWarning) buildResult.warnings.push(proxyWarning);
|
|
32011
32331
|
const p1Warning = p1BacklogOutcome.status === "fulfilled" ? p1BacklogOutcome.value : void 0;
|
|
32012
32332
|
if (p1Warning) buildResult.warnings.push(p1Warning);
|
|
32333
|
+
const originDivergence = originDivergenceOutcome.status === "fulfilled" ? originDivergenceOutcome.value : void 0;
|
|
32334
|
+
const originDivergenceWarning = originDivergence ? formatOriginDivergenceWarning(originDivergence) : void 0;
|
|
32335
|
+
if (originDivergenceWarning) buildResult.warnings.push(originDivergenceWarning);
|
|
32013
32336
|
const pendingDecisionsWarning = pendingDecisionsOutcome.status === "fulfilled" ? pendingDecisionsOutcome.value : void 0;
|
|
32014
32337
|
if (pendingDecisionsWarning) buildResult.warnings.push(pendingDecisionsWarning);
|
|
32015
32338
|
const ownerActionsWarning = ownerActionsOutcome.status === "fulfilled" ? ownerActionsOutcome.value : void 0;
|
|
@@ -32144,7 +32467,42 @@ ${section}`;
|
|
|
32144
32467
|
const runtimeIdentityNote = `
|
|
32145
32468
|
|
|
32146
32469
|
${formatRuntimeIdentity(getRuntimeIdentity(config2, serverVersion))}`;
|
|
32147
|
-
|
|
32470
|
+
let effDeepHint = deepHint;
|
|
32471
|
+
let effOnboardingCoachingNote = onboardingCoachingNote;
|
|
32472
|
+
let effStaleSkillsNote = staleSkillsNote;
|
|
32473
|
+
let effResearchSignalsNote = researchSignalsNote;
|
|
32474
|
+
const droppedNotes = [];
|
|
32475
|
+
const assembleOrientOutput = () => projectOverrideLine + projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents, projectName, teamSummary, clientName, carryForwardRefs) + runtimeIdentityNote + unblockNote + deferredGateNote + feedbackResolvedNote + alertsNote + ttfvNote + reconciliationNote + mergedInProgressNote + unrecordedNote + unregisteredDocsNote + effStaleSkillsNote + effResearchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + effOnboardingCoachingNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + effDeepHint + enrichmentFilesSection;
|
|
32476
|
+
const orientBudgetSoft = Number(process.env.PAPI_ORIENT_CONTEXT_BUDGET) || 6e4;
|
|
32477
|
+
let assembled = assembleOrientOutput();
|
|
32478
|
+
if (Buffer.byteLength(assembled, "utf-8") > orientBudgetSoft && effDeepHint) {
|
|
32479
|
+
effDeepHint = "";
|
|
32480
|
+
droppedNotes.push("deep-housekeeping tip");
|
|
32481
|
+
assembled = assembleOrientOutput();
|
|
32482
|
+
}
|
|
32483
|
+
if (Buffer.byteLength(assembled, "utf-8") > orientBudgetSoft && effOnboardingCoachingNote) {
|
|
32484
|
+
effOnboardingCoachingNote = "";
|
|
32485
|
+
droppedNotes.push("onboarding coaching");
|
|
32486
|
+
assembled = assembleOrientOutput();
|
|
32487
|
+
}
|
|
32488
|
+
if (Buffer.byteLength(assembled, "utf-8") > orientBudgetSoft && effStaleSkillsNote) {
|
|
32489
|
+
effStaleSkillsNote = "";
|
|
32490
|
+
droppedNotes.push("stale skills");
|
|
32491
|
+
assembled = assembleOrientOutput();
|
|
32492
|
+
}
|
|
32493
|
+
if (Buffer.byteLength(assembled, "utf-8") > orientBudgetSoft && effResearchSignalsNote) {
|
|
32494
|
+
effResearchSignalsNote = "";
|
|
32495
|
+
droppedNotes.push("research signals");
|
|
32496
|
+
assembled = assembleOrientOutput();
|
|
32497
|
+
}
|
|
32498
|
+
if (droppedNotes.length > 0) {
|
|
32499
|
+
assembled += `
|
|
32500
|
+
|
|
32501
|
+
---
|
|
32502
|
+
|
|
32503
|
+
*Context budget: ${droppedNotes.length} advisory note(s) omitted to keep this orient payload under ~${Math.round(orientBudgetSoft / 1024)} KB \u2014 ${droppedNotes.join(", ")}. Raise \`PAPI_ORIENT_CONTEXT_BUDGET\` in the MCP server env, or re-run without \`deep_housekeeping\`/\`full\` for a smaller payload by default.*`;
|
|
32504
|
+
}
|
|
32505
|
+
return { ...textResponse(assembled), _cycleNumber: healthResult.cycleNumber };
|
|
32148
32506
|
} catch (err) {
|
|
32149
32507
|
const message = err instanceof Error ? err.message : String(err);
|
|
32150
32508
|
const isKnownFriendly = /^(Orient failed|Project not found|No project|Setup required)/i.test(message);
|
|
@@ -33350,7 +33708,7 @@ async function resolveDecisionAuthority(adapter2, gate, decisionType) {
|
|
|
33350
33708
|
allowed: isQuorumEligible,
|
|
33351
33709
|
role,
|
|
33352
33710
|
isOwnerOverride: false,
|
|
33353
|
-
...isQuorumEligible ? {} : { resolutionError: "only owner/editor members participate in group resolution" }
|
|
33711
|
+
...isQuorumEligible ? {} : { resolutionError: "only owner/release_manager/editor members participate in group resolution" }
|
|
33354
33712
|
};
|
|
33355
33713
|
}
|
|
33356
33714
|
function normaliseMode(value) {
|
|
@@ -33398,14 +33756,14 @@ var decisionResolveTool = {
|
|
|
33398
33756
|
action: {
|
|
33399
33757
|
type: "string",
|
|
33400
33758
|
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.'
|
|
33759
|
+
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
33760
|
},
|
|
33403
33761
|
comment: { type: "string", description: "Optional note recorded alongside your position or transition." }
|
|
33404
33762
|
},
|
|
33405
33763
|
required: ["ad_id", "action"]
|
|
33406
33764
|
}
|
|
33407
33765
|
};
|
|
33408
|
-
function
|
|
33766
|
+
function findDecision2(decisions, adId) {
|
|
33409
33767
|
const trimmed = adId.trim();
|
|
33410
33768
|
return decisions.find((d) => d.id === trimmed || d.displayId === trimmed);
|
|
33411
33769
|
}
|
|
@@ -33438,7 +33796,7 @@ async function handleDecisionResolve(adapter2, config2, args) {
|
|
|
33438
33796
|
} catch (err) {
|
|
33439
33797
|
return errorResponse(`Failed to read active decisions: ${err instanceof Error ? err.message : String(err)}`);
|
|
33440
33798
|
}
|
|
33441
|
-
const target =
|
|
33799
|
+
const target = findDecision2(decisions, adId);
|
|
33442
33800
|
if (!target) return errorResponse(`AD not found: ${adId}. Run ad_view to see available decisions.`);
|
|
33443
33801
|
if (target.resolutionState !== "proposed") {
|
|
33444
33802
|
return errorResponse(
|
|
@@ -33475,7 +33833,7 @@ async function handleDecisionResolve(adapter2, config2, args) {
|
|
|
33475
33833
|
positions.map((p) => ({ userId: p.userId, status: p.status })),
|
|
33476
33834
|
quorumMembersFrom(contributors)
|
|
33477
33835
|
);
|
|
33478
|
-
quorumNote = quorum.met ? '\n\nEvery owner/editor member has now agreed or abstained \u2014 this decision is ready to `decision_resolve action="resolve"`.' : `
|
|
33836
|
+
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
33837
|
|
|
33480
33838
|
Still waiting on: ${quorum.missing.length > 0 ? quorum.missing.join(", ") : "nobody"}${quorum.objected.length > 0 ? `; objected: ${quorum.objected.join(", ")}` : ""}.`;
|
|
33481
33839
|
} catch {
|
|
@@ -34027,7 +34385,7 @@ Mapping after: ${mappingAfter}`
|
|
|
34027
34385
|
init_dist();
|
|
34028
34386
|
var contributorAddTool = {
|
|
34029
34387
|
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",
|
|
34388
|
+
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
34389
|
annotations: { title: "Add Contributor", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
34032
34390
|
inputSchema: {
|
|
34033
34391
|
type: "object",
|
|
@@ -34036,7 +34394,7 @@ var contributorAddTool = {
|
|
|
34036
34394
|
role: {
|
|
34037
34395
|
type: "string",
|
|
34038
34396
|
enum: [...ASSIGNABLE_CONTRIBUTOR_ROLES],
|
|
34039
|
-
description: `Membership role. "editor" can run the full build/review/release cycle; "viewer" has read-only visibility
|
|
34397
|
+
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
34398
|
},
|
|
34041
34399
|
project: {
|
|
34042
34400
|
type: "string",
|
|
@@ -34084,7 +34442,8 @@ async function denyUnlessOwner(adapter2, config2) {
|
|
|
34084
34442
|
if (capDenied) return capDenied;
|
|
34085
34443
|
const gate = await resolveOwnerGate(adapter2, config2);
|
|
34086
34444
|
if (gate.enforced && !gate.callerIsOwner) {
|
|
34087
|
-
const
|
|
34445
|
+
const resolutionMessage = formatOwnerGateResolutionError(gate);
|
|
34446
|
+
const note = resolutionMessage ? ` (${resolutionMessage})` : "";
|
|
34088
34447
|
return `Contributor management is restricted to the project owner. Your identity does not match this project's owner.${note}`;
|
|
34089
34448
|
}
|
|
34090
34449
|
return null;
|
|
@@ -34097,7 +34456,8 @@ async function denyUnlessMember(adapter2, config2) {
|
|
|
34097
34456
|
if (gate.callerIsOwner) return null;
|
|
34098
34457
|
const callerRole = gate.callerUserId && adapterSupports(adapter2, "getContributorRole") ? await adapter2.getContributorRole(gate.callerUserId).catch(() => null) : null;
|
|
34099
34458
|
if (callerRole) return null;
|
|
34100
|
-
const
|
|
34459
|
+
const resolutionMessage = formatOwnerGateResolutionError(gate);
|
|
34460
|
+
const note = resolutionMessage ? ` (${resolutionMessage})` : "";
|
|
34101
34461
|
return `Listing contributors is restricted to project members. Your identity does not match this project's owner or any contributor.${note}`;
|
|
34102
34462
|
}
|
|
34103
34463
|
var EMAIL_SHAPE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
@@ -34132,7 +34492,7 @@ async function handleContributorAdd(adapter2, config2, args) {
|
|
|
34132
34492
|
|
|
34133
34493
|
${upsell}` : "";
|
|
34134
34494
|
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
|
|
34495
|
+
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
34496
|
return textResponse(
|
|
34137
34497
|
`\u2705 **${entry.email ?? email}**${name} is now a **${entry.role}**${where}.
|
|
34138
34498
|
|
package/dist/prompts.js
CHANGED
|
@@ -78,6 +78,9 @@ Why now: [justification]
|
|
|
78
78
|
DEPENDS ON
|
|
79
79
|
[Optional \u2014 comma-separated task IDs this task depends on (e.g. "task-123, task-124"). Include only when another task in this same cycle must be built first because this task consumes artifacts it creates (e.g. new adapter method, new type, new migration). The builder will reuse the upstream task's branch so dependent commits stack on the same branch for a single PR. Omit this section entirely if there are no intra-cycle dependencies.]
|
|
80
80
|
|
|
81
|
+
RELEVANT ACTIVE DECISIONS
|
|
82
|
+
[Optional \u2014 self-check which Active Decisions actually bind the FILES LIKELY TOUCHED and module below, out of the full AD list already in your context. List the top 3-6 as "AD-N (title) \u2014 one-line reason this AD binds the files/approach". Rank by blast radius (an AD that would be VIOLATED by a naive implementation outranks one that's merely thematically adjacent). This is a judgement call \u2014 no server-side scoring feeds this. Omit this section entirely when nothing in the AD list is genuinely relevant to what this task touches; do not force a match.]
|
|
83
|
+
|
|
81
84
|
SCOPE (DO THIS)
|
|
82
85
|
[specific deliverables \u2014 write for the simplest viable path first]
|
|
83
86
|
|
|
@@ -1413,6 +1416,9 @@ Why now: [justification]
|
|
|
1413
1416
|
DEPENDS ON
|
|
1414
1417
|
[Optional \u2014 comma-separated task IDs this task depends on (e.g. "task-123, task-124"). Include only when another task in this same cycle must be built first because this task consumes artifacts it creates (e.g. new adapter method, new type, new migration). The builder will reuse the upstream task's branch so dependent commits stack on the same branch for a single PR. Omit this section entirely if there are no intra-cycle dependencies.]
|
|
1415
1418
|
|
|
1419
|
+
RELEVANT ACTIVE DECISIONS
|
|
1420
|
+
[Optional \u2014 self-check which Active Decisions actually bind the FILES LIKELY TOUCHED and module below, out of the full AD list already in your context. List the top 3-6 as "AD-N (title) \u2014 one-line reason this AD binds the files/approach". Rank by blast radius (an AD that would be VIOLATED by a naive implementation outranks one that's merely thematically adjacent). This is a judgement call \u2014 no server-side scoring feeds this. Omit this section entirely when nothing in the AD list is genuinely relevant to what this task touches; do not force a match.]
|
|
1421
|
+
|
|
1416
1422
|
SCOPE (DO THIS)
|
|
1417
1423
|
[specific deliverables \u2014 write for the simplest viable path first]
|
|
1418
1424
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@papi-ai/server",
|
|
3
|
-
"version": "0.7.
|
|
4
|
-
"description": "PAPI MCP server
|
|
3
|
+
"version": "0.7.104",
|
|
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",
|
|
7
7
|
"type": "module",
|