@papi-ai/server 0.7.83 → 0.7.84
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 +18 -6
- package/dist/index.js +653 -171
- package/package.json +3 -3
|
@@ -214,6 +214,7 @@ __export(git_exports, {
|
|
|
214
214
|
listGroupedCycleBranches: () => listGroupedCycleBranches,
|
|
215
215
|
listOpenPullRequests: () => listOpenPullRequests,
|
|
216
216
|
listOrphanFeatBranches: () => listOrphanFeatBranches,
|
|
217
|
+
memberBranchSlug: () => memberBranchSlug,
|
|
217
218
|
mergePullRequest: () => mergePullRequest,
|
|
218
219
|
normalizeGitUrl: () => normalizeGitUrl,
|
|
219
220
|
pickModuleCycleBranch: () => pickModuleCycleBranch,
|
|
@@ -1106,9 +1107,20 @@ function detectUnrecordedCommits(cwd, baseBranch) {
|
|
|
1106
1107
|
function taskBranchName(taskId) {
|
|
1107
1108
|
return `feat/${taskId}`;
|
|
1108
1109
|
}
|
|
1109
|
-
function cycleBranchName(cycleNumber, module) {
|
|
1110
|
+
function cycleBranchName(cycleNumber, module, memberSlug) {
|
|
1110
1111
|
const slug = module.toLowerCase().replace(/&/g, "and").replace(/&/g, "and").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1111
|
-
|
|
1112
|
+
const member = memberSlug ? `-${memberSlug}` : "";
|
|
1113
|
+
return `feat/cycle-${cycleNumber}-${slug}${member}`;
|
|
1114
|
+
}
|
|
1115
|
+
function memberBranchSlug(member) {
|
|
1116
|
+
const sanitise = (raw) => raw.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 20).replace(/-+$/g, "");
|
|
1117
|
+
const fromName = member.displayName ? sanitise(member.displayName) : "";
|
|
1118
|
+
if (fromName) return fromName;
|
|
1119
|
+
const localPart = member.email ? member.email.split("@")[0] : "";
|
|
1120
|
+
const fromEmail = localPart ? sanitise(localPart) : "";
|
|
1121
|
+
if (fromEmail) return fromEmail;
|
|
1122
|
+
const fromId = member.userId ? sanitise(member.userId).slice(0, 8) : "";
|
|
1123
|
+
return fromId || void 0;
|
|
1112
1124
|
}
|
|
1113
1125
|
function getHeadCommitSha(cwd) {
|
|
1114
1126
|
try {
|
|
@@ -1231,9 +1243,9 @@ function isBranchContentAlreadyInBase(cwd, branch, baseBranch) {
|
|
|
1231
1243
|
return false;
|
|
1232
1244
|
}
|
|
1233
1245
|
}
|
|
1234
|
-
function pickModuleCycleBranch(candidates, cycleNumber, module) {
|
|
1246
|
+
function pickModuleCycleBranch(candidates, cycleNumber, module, memberSlug) {
|
|
1235
1247
|
if (candidates.length === 0) return void 0;
|
|
1236
|
-
const expected = cycleBranchName(cycleNumber, module);
|
|
1248
|
+
const expected = cycleBranchName(cycleNumber, module, memberSlug);
|
|
1237
1249
|
return candidates.find((b) => b === expected);
|
|
1238
1250
|
}
|
|
1239
1251
|
function listOrphanFeatBranches(cwd, baseBranch) {
|
|
@@ -2431,8 +2443,8 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
2431
2443
|
async listContributors() {
|
|
2432
2444
|
return this.invoke("listContributors", []);
|
|
2433
2445
|
}
|
|
2434
|
-
async addContributorByEmail(email) {
|
|
2435
|
-
return this.invoke("addContributorByEmail", [email]);
|
|
2446
|
+
async addContributorByEmail(email, role) {
|
|
2447
|
+
return this.invoke("addContributorByEmail", [email, role]);
|
|
2436
2448
|
}
|
|
2437
2449
|
async removeContributorByEmail(email) {
|
|
2438
2450
|
return this.invoke("removeContributorByEmail", [email]);
|
package/dist/index.js
CHANGED
|
@@ -760,7 +760,7 @@ function parsePhaseBlock(block) {
|
|
|
760
760
|
if (isNaN(order)) return null;
|
|
761
761
|
return { id, slug, label, description, status, order };
|
|
762
762
|
}
|
|
763
|
-
var CAPABILITY_REGISTRY, CAPABILITY_KEYS, SENSITIVE_CHANGELOG_PATTERNS, CONTRIBUTOR_PRICING_URL, HOSTED_APP_URL, HOSTED_MCP_URL, HOSTED_SUPABASE_URL, SEVERITY_ORDER, NONE_RE, AD_HOC_PLACEHOLDER_RE, MIN_REASON_LENGTH, LOW_SEVERITIES, WAB_WINDOW_DAYS, WAB_DEFAULT_WEEKS, WAB_WEEK_MS, PRODUCT_MARKER, MECHANICS_MARKER, CHECK_VALUE_MAX, VALID_EFFORT_SIZES, SECTION_HEADERS, VALID_EFFORT_SIZES2, EFFORT_SCALE, NONE_PATTERN, NONE_PATTERN2, VALID_STATUSES, PHASES_START, PHASES_END;
|
|
763
|
+
var CAPABILITY_REGISTRY, CAPABILITY_KEYS, ASSIGNABLE_CONTRIBUTOR_ROLES, SENSITIVE_CHANGELOG_PATTERNS, CONTRIBUTOR_PRICING_URL, HOSTED_APP_URL, HOSTED_MCP_URL, HOSTED_SUPABASE_URL, SEVERITY_ORDER, NONE_RE, AD_HOC_PLACEHOLDER_RE, MIN_REASON_LENGTH, LOW_SEVERITIES, WAB_WINDOW_DAYS, WAB_DEFAULT_WEEKS, WAB_WEEK_MS, PRODUCT_MARKER, MECHANICS_MARKER, CHECK_VALUE_MAX, VALID_EFFORT_SIZES, SECTION_HEADERS, VALID_EFFORT_SIZES2, EFFORT_SCALE, NONE_PATTERN, NONE_PATTERN2, VALID_STATUSES, PHASES_START, PHASES_END;
|
|
764
764
|
var init_dist = __esm({
|
|
765
765
|
"../shared/dist/index.js"() {
|
|
766
766
|
"use strict";
|
|
@@ -803,6 +803,7 @@ var init_dist = __esm({
|
|
|
803
803
|
{ key: "papiMetaFraming", label: "PAPI self-referential framing", description: "Includes PAPI-meta build-discipline framing in handoff output.", step: "build" }
|
|
804
804
|
];
|
|
805
805
|
CAPABILITY_KEYS = CAPABILITY_REGISTRY.map((c) => c.key);
|
|
806
|
+
ASSIGNABLE_CONTRIBUTOR_ROLES = ["editor", "viewer"];
|
|
806
807
|
SENSITIVE_CHANGELOG_PATTERNS = [
|
|
807
808
|
// ── Test-user machinery ────────────────────────────────────────────────
|
|
808
809
|
/@test\.papi\.dev/i,
|
|
@@ -949,6 +950,7 @@ __export(git_exports, {
|
|
|
949
950
|
listGroupedCycleBranches: () => listGroupedCycleBranches,
|
|
950
951
|
listOpenPullRequests: () => listOpenPullRequests,
|
|
951
952
|
listOrphanFeatBranches: () => listOrphanFeatBranches,
|
|
953
|
+
memberBranchSlug: () => memberBranchSlug,
|
|
952
954
|
mergePullRequest: () => mergePullRequest,
|
|
953
955
|
normalizeGitUrl: () => normalizeGitUrl,
|
|
954
956
|
pickModuleCycleBranch: () => pickModuleCycleBranch,
|
|
@@ -1841,9 +1843,20 @@ function detectUnrecordedCommits(cwd, baseBranch) {
|
|
|
1841
1843
|
function taskBranchName(taskId) {
|
|
1842
1844
|
return `feat/${taskId}`;
|
|
1843
1845
|
}
|
|
1844
|
-
function cycleBranchName(cycleNumber, module) {
|
|
1846
|
+
function cycleBranchName(cycleNumber, module, memberSlug) {
|
|
1845
1847
|
const slug = module.toLowerCase().replace(/&/g, "and").replace(/&/g, "and").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1846
|
-
|
|
1848
|
+
const member = memberSlug ? `-${memberSlug}` : "";
|
|
1849
|
+
return `feat/cycle-${cycleNumber}-${slug}${member}`;
|
|
1850
|
+
}
|
|
1851
|
+
function memberBranchSlug(member) {
|
|
1852
|
+
const sanitise = (raw) => raw.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 20).replace(/-+$/g, "");
|
|
1853
|
+
const fromName = member.displayName ? sanitise(member.displayName) : "";
|
|
1854
|
+
if (fromName) return fromName;
|
|
1855
|
+
const localPart = member.email ? member.email.split("@")[0] : "";
|
|
1856
|
+
const fromEmail = localPart ? sanitise(localPart) : "";
|
|
1857
|
+
if (fromEmail) return fromEmail;
|
|
1858
|
+
const fromId = member.userId ? sanitise(member.userId).slice(0, 8) : "";
|
|
1859
|
+
return fromId || void 0;
|
|
1847
1860
|
}
|
|
1848
1861
|
function getHeadCommitSha(cwd) {
|
|
1849
1862
|
try {
|
|
@@ -1966,9 +1979,9 @@ function isBranchContentAlreadyInBase(cwd, branch, baseBranch) {
|
|
|
1966
1979
|
return false;
|
|
1967
1980
|
}
|
|
1968
1981
|
}
|
|
1969
|
-
function pickModuleCycleBranch(candidates, cycleNumber, module) {
|
|
1982
|
+
function pickModuleCycleBranch(candidates, cycleNumber, module, memberSlug) {
|
|
1970
1983
|
if (candidates.length === 0) return void 0;
|
|
1971
|
-
const expected = cycleBranchName(cycleNumber, module);
|
|
1984
|
+
const expected = cycleBranchName(cycleNumber, module, memberSlug);
|
|
1972
1985
|
return candidates.find((b2) => b2 === expected);
|
|
1973
1986
|
}
|
|
1974
1987
|
function listOrphanFeatBranches(cwd, baseBranch) {
|
|
@@ -3232,8 +3245,8 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
3232
3245
|
async listContributors() {
|
|
3233
3246
|
return this.invoke("listContributors", []);
|
|
3234
3247
|
}
|
|
3235
|
-
async addContributorByEmail(email) {
|
|
3236
|
-
return this.invoke("addContributorByEmail", [email]);
|
|
3248
|
+
async addContributorByEmail(email, role) {
|
|
3249
|
+
return this.invoke("addContributorByEmail", [email, role]);
|
|
3237
3250
|
}
|
|
3238
3251
|
async removeContributorByEmail(email) {
|
|
3239
3252
|
return this.invoke("removeContributorByEmail", [email]);
|
|
@@ -10155,6 +10168,38 @@ function computeCarryForwardStaleness(log2, doneTaskIds) {
|
|
|
10155
10168
|
].join("\n");
|
|
10156
10169
|
}
|
|
10157
10170
|
|
|
10171
|
+
// src/lib/refs.ts
|
|
10172
|
+
var MAX_LABEL = 55;
|
|
10173
|
+
function truncateLabel(title, max = MAX_LABEL) {
|
|
10174
|
+
if (title.length <= max) return title;
|
|
10175
|
+
return `${title.slice(0, max - 1).trimEnd()}\u2026`;
|
|
10176
|
+
}
|
|
10177
|
+
function formatRef(id, title) {
|
|
10178
|
+
const label = title?.trim();
|
|
10179
|
+
if (!label) return id;
|
|
10180
|
+
if (label.toLowerCase() === id.toLowerCase()) return id;
|
|
10181
|
+
return `${id} (${truncateLabel(label)})`;
|
|
10182
|
+
}
|
|
10183
|
+
function formatRefList(ids, titles) {
|
|
10184
|
+
return ids.map((id) => formatRef(id, lookupTitle(id, titles))).join(", ");
|
|
10185
|
+
}
|
|
10186
|
+
function lookupTitle(id, titles) {
|
|
10187
|
+
if (!titles) return void 0;
|
|
10188
|
+
return titles.get(id) ?? titles.get(id.toLowerCase()) ?? titles.get(id.toUpperCase());
|
|
10189
|
+
}
|
|
10190
|
+
function titleMap(entities) {
|
|
10191
|
+
const map = /* @__PURE__ */ new Map();
|
|
10192
|
+
for (const e of entities) {
|
|
10193
|
+
if (!e.title) continue;
|
|
10194
|
+
for (const key of [e.id, e.displayId]) {
|
|
10195
|
+
if (!key) continue;
|
|
10196
|
+
map.set(key, e.title);
|
|
10197
|
+
map.set(key.toLowerCase(), e.title);
|
|
10198
|
+
}
|
|
10199
|
+
}
|
|
10200
|
+
return map;
|
|
10201
|
+
}
|
|
10202
|
+
|
|
10158
10203
|
// src/lib/blocker.ts
|
|
10159
10204
|
function idMatches(candidate, ref) {
|
|
10160
10205
|
if (!candidate) return false;
|
|
@@ -10188,16 +10233,31 @@ function isBlockerResolved(blocker, ctx) {
|
|
|
10188
10233
|
return false;
|
|
10189
10234
|
}
|
|
10190
10235
|
}
|
|
10191
|
-
function formatBlockerWaiting(blocker) {
|
|
10236
|
+
function formatBlockerWaiting(blocker, refTitle) {
|
|
10237
|
+
const ref = formatRef(blocker.ref, refTitle);
|
|
10192
10238
|
switch (blocker.type) {
|
|
10193
10239
|
case "depends-on":
|
|
10194
|
-
return `waiting on ${
|
|
10240
|
+
return `waiting on ${ref} \u2014 clears when that task is Done`;
|
|
10195
10241
|
case "owner-action":
|
|
10196
|
-
return `waiting on owner action ${
|
|
10242
|
+
return `waiting on owner action ${ref} \u2014 clears when you complete it`;
|
|
10243
|
+
case "decision-gate":
|
|
10244
|
+
return `waiting on decision ${ref} \u2014 clears when that decision is resolved`;
|
|
10245
|
+
default:
|
|
10246
|
+
return `waiting on ${ref}`;
|
|
10247
|
+
}
|
|
10248
|
+
}
|
|
10249
|
+
function resolveBlockerTitle(blocker, ctx) {
|
|
10250
|
+
switch (blocker.type) {
|
|
10251
|
+
case "depends-on":
|
|
10252
|
+
return ctx.tasks.find(
|
|
10253
|
+
(t) => idMatches(t.displayId, blocker.ref) || idMatches(t.id, blocker.ref)
|
|
10254
|
+
)?.title;
|
|
10197
10255
|
case "decision-gate":
|
|
10198
|
-
return
|
|
10256
|
+
return ctx.decisions.find(
|
|
10257
|
+
(d) => idMatches(d.displayId, blocker.ref) || idMatches(d.id, blocker.ref)
|
|
10258
|
+
)?.title;
|
|
10199
10259
|
default:
|
|
10200
|
-
return
|
|
10260
|
+
return void 0;
|
|
10201
10261
|
}
|
|
10202
10262
|
}
|
|
10203
10263
|
|
|
@@ -10428,11 +10488,22 @@ async function parkDecisionConflicts(adapter2, conflicts, newTaskIdMap, blockedC
|
|
|
10428
10488
|
if (conflicts.length === 0) return [];
|
|
10429
10489
|
const scheduled = new Set(scheduledTaskIds.map((id) => id.toLowerCase()));
|
|
10430
10490
|
const results = [];
|
|
10491
|
+
let adTitles = /* @__PURE__ */ new Map();
|
|
10492
|
+
try {
|
|
10493
|
+
if (adapter2.getActiveDecisions) adTitles = titleMap(await adapter2.getActiveDecisions());
|
|
10494
|
+
} catch {
|
|
10495
|
+
}
|
|
10431
10496
|
for (const conflict of conflicts) {
|
|
10432
10497
|
const resolvedTaskId = newTaskIdMap.get(conflict.taskId) ?? conflict.taskId;
|
|
10433
|
-
const
|
|
10498
|
+
const adTitle = lookupTitle(conflict.adId, adTitles);
|
|
10499
|
+
const base = { ...conflict, resolvedTaskId, parked: false, adTitle };
|
|
10434
10500
|
if (scheduled.has(resolvedTaskId.toLowerCase())) {
|
|
10435
|
-
|
|
10501
|
+
const [scheduledTask] = await adapter2.getTasks([resolvedTaskId]).catch(() => []);
|
|
10502
|
+
results.push({
|
|
10503
|
+
...base,
|
|
10504
|
+
taskTitle: scheduledTask?.title,
|
|
10505
|
+
skipReason: "scheduled in this cycle \u2014 left in the cycle, conflict still surfaced"
|
|
10506
|
+
});
|
|
10436
10507
|
continue;
|
|
10437
10508
|
}
|
|
10438
10509
|
try {
|
|
@@ -10441,6 +10512,7 @@ async function parkDecisionConflicts(adapter2, conflicts, newTaskIdMap, blockedC
|
|
|
10441
10512
|
results.push({ ...base, skipReason: "task not found" });
|
|
10442
10513
|
continue;
|
|
10443
10514
|
}
|
|
10515
|
+
base.taskTitle = task.title;
|
|
10444
10516
|
if (!PARKABLE_STATUSES.has(task.status)) {
|
|
10445
10517
|
results.push({ ...base, skipReason: `status is ${task.status} \u2014 left as-is` });
|
|
10446
10518
|
continue;
|
|
@@ -10453,7 +10525,7 @@ async function parkDecisionConflicts(adapter2, conflicts, newTaskIdMap, blockedC
|
|
|
10453
10525
|
blocker: {
|
|
10454
10526
|
type: "decision-gate",
|
|
10455
10527
|
ref: conflict.adId,
|
|
10456
|
-
reason: conflict.conflict || `Contradicts ${conflict.adId} \u2014 awaiting the owner's decision.`,
|
|
10528
|
+
reason: conflict.conflict || `Contradicts ${formatRef(conflict.adId, adTitle)} \u2014 awaiting the owner's decision.`,
|
|
10457
10529
|
blockedCycle
|
|
10458
10530
|
}
|
|
10459
10531
|
});
|
|
@@ -10780,7 +10852,8 @@ async function computeResurfacedBlockers(adapter2, tasks, decisions, currentCycl
|
|
|
10780
10852
|
const resolvedLines = [];
|
|
10781
10853
|
for (const t of blocked) {
|
|
10782
10854
|
if (t.blocker && isBlockerResolved(t.blocker, ctx)) {
|
|
10783
|
-
|
|
10855
|
+
const waiting = formatBlockerWaiting(t.blocker, resolveBlockerTitle(t.blocker, ctx));
|
|
10856
|
+
resolvedLines.push(`- **${t.displayId}** (${t.title}) \u2014 ${waiting} \u2192 now RESOLVED, ready to unblock`);
|
|
10784
10857
|
}
|
|
10785
10858
|
}
|
|
10786
10859
|
return resolvedLines.length > 0 ? resolvedLines.join("\n") : void 0;
|
|
@@ -13085,6 +13158,22 @@ var planTool = {
|
|
|
13085
13158
|
]
|
|
13086
13159
|
}
|
|
13087
13160
|
};
|
|
13161
|
+
function formatDecisionConflicts(conflicts) {
|
|
13162
|
+
if (conflicts.length === 0) return [];
|
|
13163
|
+
const lines = [""];
|
|
13164
|
+
lines.push(`\u{1F500} **${conflicts.length} decision(s) need your call \u2014 work was NOT dropped.**`);
|
|
13165
|
+
lines.push("These tasks cut against a live Active Decision. An AD is *active* \u2014 it can be superseded, so the planner parked the work instead of shelving it:");
|
|
13166
|
+
for (const c of conflicts) {
|
|
13167
|
+
const ad = formatRef(c.adId, c.adTitle);
|
|
13168
|
+
const state = c.parked ? `Blocked behind ${ad}` : `still ${c.skipReason ?? "unparked"}`;
|
|
13169
|
+
lines.push(`- **${formatRef(c.resolvedTaskId, c.taskTitle)} vs ${ad}** \u2014 ${c.conflict} _(${state})_`);
|
|
13170
|
+
if (c.options.length > 0) lines.push(` Options: ${c.options.join(" | ")}`);
|
|
13171
|
+
if (c.recommendation) lines.push(` Planner recommends: ${c.recommendation}`);
|
|
13172
|
+
}
|
|
13173
|
+
lines.push("");
|
|
13174
|
+
lines.push('Decide: `strategy_change` with `mode: "capture"` to supersede or modify the AD (parked tasks auto-unblock once it moves), or `board_edit` to cancel the task if the AD stands.');
|
|
13175
|
+
return lines;
|
|
13176
|
+
}
|
|
13088
13177
|
function formatPlanResult(result) {
|
|
13089
13178
|
const modeLabel = result.mode === "bootstrap" ? "Bootstrap" : "Full";
|
|
13090
13179
|
const cycleLabel = `Cycle ${result.cycleNumber + 1}`;
|
|
@@ -13131,20 +13220,7 @@ function formatPlanResult(result) {
|
|
|
13131
13220
|
lines.push('To apply these cancellations, re-run `plan` with `mode: "apply"` and `confirm_cancellations: true`.');
|
|
13132
13221
|
lines.push("To keep these tasks, do nothing \u2014 they remain on the board.");
|
|
13133
13222
|
}
|
|
13134
|
-
|
|
13135
|
-
if (conflicts.length > 0) {
|
|
13136
|
-
lines.push("");
|
|
13137
|
-
lines.push(`\u{1F500} **${conflicts.length} decision(s) need your call \u2014 work was NOT dropped.**`);
|
|
13138
|
-
lines.push("These tasks cut against a live Active Decision. An AD is *active* \u2014 it can be superseded, so the planner parked the work instead of shelving it:");
|
|
13139
|
-
for (const c of conflicts) {
|
|
13140
|
-
const state = c.parked ? `Blocked behind ${c.adId}` : `still ${c.skipReason ?? "unparked"}`;
|
|
13141
|
-
lines.push(`- **${c.resolvedTaskId} vs ${c.adId}** \u2014 ${c.conflict} _(${state})_`);
|
|
13142
|
-
if (c.options.length > 0) lines.push(` Options: ${c.options.join(" | ")}`);
|
|
13143
|
-
if (c.recommendation) lines.push(` Planner recommends: ${c.recommendation}`);
|
|
13144
|
-
}
|
|
13145
|
-
lines.push("");
|
|
13146
|
-
lines.push('Decide: `strategy_change` with `mode: "capture"` to supersede or modify the AD (parked tasks auto-unblock once it moves), or `board_edit` to cancel the task if the AD stands.');
|
|
13147
|
-
}
|
|
13223
|
+
lines.push(...formatDecisionConflicts(result.decisionConflicts ?? []));
|
|
13148
13224
|
if (result.skipHandoffs) {
|
|
13149
13225
|
const taskCount = result.writeSummary?.taskIds.length ?? 0;
|
|
13150
13226
|
lines.push("", `Next: run \`handoff_generate\` to create BUILD HANDOFFs for your ${taskCount} cycle task(s), then \`build_list\` to start building.`);
|
|
@@ -13381,6 +13457,85 @@ ${result.userMessage}
|
|
|
13381
13457
|
}
|
|
13382
13458
|
}
|
|
13383
13459
|
|
|
13460
|
+
// src/lib/capabilities.ts
|
|
13461
|
+
init_dist();
|
|
13462
|
+
|
|
13463
|
+
// src/lib/directive-builders.ts
|
|
13464
|
+
function buildPrReviewerDirective(caps) {
|
|
13465
|
+
if (!isCapabilityEnabled(caps, "prReviewer")) return null;
|
|
13466
|
+
return `
|
|
13467
|
+
|
|
13468
|
+
**Quality Gate:** no auto-review attached. PAPI's standard pre-accept step is a code review of the branch diff \u2014 run \`review_submit\` with \`dispatch:"subagent"\` to auto-review, or attach \`auto_review\` findings. Risk-tier work (auth, data, migrations, CI) should always carry one.`;
|
|
13469
|
+
}
|
|
13470
|
+
function buildChangelogDirective(caps, inner) {
|
|
13471
|
+
if (!isCapabilityEnabled(caps, "changelog")) return null;
|
|
13472
|
+
return inner;
|
|
13473
|
+
}
|
|
13474
|
+
function buildDiscoveredIssuesDirective(caps, issueLines) {
|
|
13475
|
+
if (!isCapabilityEnabled(caps, "discoveredIssues")) return null;
|
|
13476
|
+
if (issueLines.length === 0) return null;
|
|
13477
|
+
return [
|
|
13478
|
+
"",
|
|
13479
|
+
"---",
|
|
13480
|
+
"",
|
|
13481
|
+
`## Discovered Issues (${issueLines.length})`,
|
|
13482
|
+
"",
|
|
13483
|
+
...issueLines,
|
|
13484
|
+
"",
|
|
13485
|
+
"*These issues were logged during builds \u2014 triage them in the next plan.*"
|
|
13486
|
+
].join("\n");
|
|
13487
|
+
}
|
|
13488
|
+
function buildModelRecommendationDirective(caps, tierBlock) {
|
|
13489
|
+
if (!isCapabilityEnabled(caps, "modelRecommendation")) return null;
|
|
13490
|
+
return tierBlock;
|
|
13491
|
+
}
|
|
13492
|
+
function buildVerifyHealthCheckDirective(caps) {
|
|
13493
|
+
if (!isCapabilityEnabled(caps, "verifyHealthCheck")) return null;
|
|
13494
|
+
return `
|
|
13495
|
+
|
|
13496
|
+
**Health check:** before you consider the cycle shipped, verify cycle state (plan validity, review coverage, branch hygiene) \u2014 run the \`papi-verify\` skill or a quick \`board_view\` pass.`;
|
|
13497
|
+
}
|
|
13498
|
+
function buildGestaltPreBuildDirective(caps) {
|
|
13499
|
+
if (!isCapabilityEnabled(caps, "gestaltPreBuild")) return null;
|
|
13500
|
+
return `
|
|
13501
|
+
|
|
13502
|
+
**Gestalt pre-build:** on the first task of a multi-task cycle, read every task's BUILD HANDOFF together (shared files, sequencing, module split) before building \u2014 one-time check at cycle start.`;
|
|
13503
|
+
}
|
|
13504
|
+
function buildBatchBuildRollupDirective(caps) {
|
|
13505
|
+
if (!isCapabilityEnabled(caps, "batchBuildRollup")) return null;
|
|
13506
|
+
return `
|
|
13507
|
+
|
|
13508
|
+
**Batch rollup:** after the final task of a batch, emit a cycle-level rollup \u2014 build summary table + discovered issues grouped by severity + next action.`;
|
|
13509
|
+
}
|
|
13510
|
+
function buildSecurityScanDirective(caps) {
|
|
13511
|
+
if (!isCapabilityEnabled(caps, "securityScan")) return null;
|
|
13512
|
+
return `
|
|
13513
|
+
|
|
13514
|
+
**Security scan:** risk-tier changes (auth, data, migrations, secrets, endpoints) should carry a security pass \u2014 OWASP top-10 + secret-exposure check on the changed surface before merge.`;
|
|
13515
|
+
}
|
|
13516
|
+
function buildDeployHookDirective(caps, deployCommand) {
|
|
13517
|
+
if (!isCapabilityEnabled(caps, "deployHook")) return null;
|
|
13518
|
+
const command = deployCommand?.trim() || void 0;
|
|
13519
|
+
if (!command) return null;
|
|
13520
|
+
return `
|
|
13521
|
+
|
|
13522
|
+
---
|
|
13523
|
+
|
|
13524
|
+
## Deploy \u2014 run after this release
|
|
13525
|
+
|
|
13526
|
+
The post-release deploy hook is on and a deploy command is configured, so ship the merged release now.
|
|
13527
|
+
|
|
13528
|
+
Run your deploy command yourself:
|
|
13529
|
+
\`\`\`
|
|
13530
|
+
${command}
|
|
13531
|
+
\`\`\`
|
|
13532
|
+
PAPI never runs this command itself (AD-58) \u2014 you run it in your own environment. Turn the "Post-release deploy" capability off in the dashboard, or unset PAPI_DEPLOY, to stop this reminder.`;
|
|
13533
|
+
}
|
|
13534
|
+
function buildPapiMetaFramingDirective(caps, inner) {
|
|
13535
|
+
if (!isCapabilityEnabled(caps, "papiMetaFraming")) return null;
|
|
13536
|
+
return inner;
|
|
13537
|
+
}
|
|
13538
|
+
|
|
13384
13539
|
// src/services/strategy.ts
|
|
13385
13540
|
import { randomUUID as randomUUID5, createHash as createHash4 } from "crypto";
|
|
13386
13541
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
@@ -13737,8 +13892,10 @@ ${lines.join("\n")}
|
|
|
13737
13892
|
}
|
|
13738
13893
|
let notesWithAlignment = input.notes || "";
|
|
13739
13894
|
let detectedAdIds = [];
|
|
13895
|
+
let adTitles = /* @__PURE__ */ new Map();
|
|
13740
13896
|
try {
|
|
13741
13897
|
const ads = await adapter2.getActiveDecisions();
|
|
13898
|
+
adTitles = titleMap(ads);
|
|
13742
13899
|
const ideaCombined = `${input.text} ${input.notes ?? ""}`;
|
|
13743
13900
|
const alignmentMatches = findAdAlignmentMatches(ideaCombined, ads);
|
|
13744
13901
|
if (alignmentMatches.length > 0) {
|
|
@@ -13828,7 +13985,7 @@ AD-CONFLICT: ${conflictsWithAd} \u2014 submitter flagged this as cutting against
|
|
|
13828
13985
|
blocker: {
|
|
13829
13986
|
type: "decision-gate",
|
|
13830
13987
|
ref: conflictsWithAd,
|
|
13831
|
-
reason: `Submitted as cutting against ${conflictsWithAd}. Awaiting the owner's decision.`,
|
|
13988
|
+
reason: `Submitted as cutting against ${formatRef(conflictsWithAd, lookupTitle(conflictsWithAd, adTitles))}. Awaiting the owner's decision.`,
|
|
13832
13989
|
blockedCycle: health.totalCycles
|
|
13833
13990
|
}
|
|
13834
13991
|
});
|
|
@@ -13847,21 +14004,36 @@ AD-CONFLICT: ${conflictsWithAd} \u2014 submitter flagged this as cutting against
|
|
|
13847
14004
|
routing: "task",
|
|
13848
14005
|
task,
|
|
13849
14006
|
message: `${task.id}: "${task.title}" \u2014 ${landing}${typeNote}${visibilityNote}${mismatchNote}`,
|
|
13850
|
-
...adIds.length > 0 ? {
|
|
14007
|
+
...adIds.length > 0 ? {
|
|
14008
|
+
adConflicts: {
|
|
14009
|
+
adIds,
|
|
14010
|
+
explicit: Boolean(conflictsWithAd),
|
|
14011
|
+
gated,
|
|
14012
|
+
// task-3347: id → title for exactly the ADs named above, so the tool
|
|
14013
|
+
// layer renders `AD-12 (Single board view)` without a second read.
|
|
14014
|
+
adTitles: Object.fromEntries(
|
|
14015
|
+
adIds.flatMap((id) => {
|
|
14016
|
+
const title = lookupTitle(id, adTitles);
|
|
14017
|
+
return title ? [[id, title]] : [];
|
|
14018
|
+
})
|
|
14019
|
+
)
|
|
14020
|
+
}
|
|
14021
|
+
} : {}
|
|
13851
14022
|
};
|
|
13852
14023
|
}
|
|
13853
|
-
function buildAdConflictNote(conflictsWithAd, detectedAdIds, gated) {
|
|
14024
|
+
function buildAdConflictNote(conflictsWithAd, detectedAdIds, gated, adTitles) {
|
|
13854
14025
|
if (conflictsWithAd) {
|
|
13855
|
-
const
|
|
14026
|
+
const ref = formatRef(conflictsWithAd, lookupTitle(conflictsWithAd, adTitles));
|
|
14027
|
+
const state = gated ? `Parked as **Blocked** behind a decision gate on ${ref} \u2014 it auto-unblocks once that AD is superseded, modified, or reaffirmed.` : `Recorded against ${ref} (gate could not be applied \u2014 the AD-CONFLICT note is on the task).`;
|
|
13856
14028
|
return `
|
|
13857
14029
|
|
|
13858
|
-
\u{1F500} **Conflicts with ${
|
|
14030
|
+
\u{1F500} **Conflicts with ${ref}.** ${state}
|
|
13859
14031
|
**Tell the user now** \u2014 an AD is *active*, so this is their call to make, not yours. Offer both paths: supersede the AD via \`strategy_change\`, or leave the AD standing and drop the task.`;
|
|
13860
14032
|
}
|
|
13861
14033
|
if (detectedAdIds.length > 0) {
|
|
13862
14034
|
return `
|
|
13863
14035
|
|
|
13864
|
-
\u{1F500} **Possible conflict with ${detectedAdIds
|
|
14036
|
+
\u{1F500} **Possible conflict with ${formatRefList(detectedAdIds, adTitles)}** (keyword match \u2014 verify it is real).
|
|
13865
14037
|
If it IS a real contradiction, **do not shelve the idea on those grounds**: surface it to the user and re-submit with \`conflicts_with_ad\` so the task is gated on the decision instead of quietly competing in the backlog.`;
|
|
13866
14038
|
}
|
|
13867
14039
|
return "";
|
|
@@ -14332,7 +14504,8 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
|
|
|
14332
14504
|
pendingRecs,
|
|
14333
14505
|
registeredDocs,
|
|
14334
14506
|
docsWithPendingActions,
|
|
14335
|
-
decisionScores
|
|
14507
|
+
decisionScores,
|
|
14508
|
+
allClosedTasks
|
|
14336
14509
|
] = await Promise.all([
|
|
14337
14510
|
adapter2.readProductBrief(),
|
|
14338
14511
|
// Strategy review needs to see retired ADs to triage/restore them as needed.
|
|
@@ -14361,7 +14534,15 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
|
|
|
14361
14534
|
// Doc registry — docs with pending actions for staleness audit
|
|
14362
14535
|
adapter2.searchDocs?.({ hasPendingActions: true, limit: 20 })?.catch(() => []) ?? Promise.resolve([]),
|
|
14363
14536
|
// task-2264: decision risk scores + movement, so the review can show trajectory.
|
|
14364
|
-
adapter2.getDecisionScorePatterns?.()?.catch(() => []) ?? Promise.resolve([])
|
|
14537
|
+
adapter2.getDecisionScorePatterns?.()?.catch(() => []) ?? Promise.resolve([]),
|
|
14538
|
+
// task-3331 (C363): UNWINDOWED closed-task ids, purely to exclude already-closed
|
|
14539
|
+
// tasks from the repeat-deferral detector. Deliberately NOT reusing
|
|
14540
|
+
// recentDoneTasks: that set is windowed with `cycleSince: lastReviewCycleNum`
|
|
14541
|
+
// because the surfaces it feeds (review-window comments) genuinely want the
|
|
14542
|
+
// window. Carry-forward history spans many cycles, so a task that shipped BEFORE
|
|
14543
|
+
// the window was invisible to the exclusion and kept being flagged. Compact +
|
|
14544
|
+
// Cancelled, matching what services/plan.ts already does correctly.
|
|
14545
|
+
adapter2.queryBoard({ status: ["Done", "Cancelled"], compact: true }).catch(() => [])
|
|
14365
14546
|
]);
|
|
14366
14547
|
const pendingAgendaTopics = await (adapter2.getPendingAgendaTopics?.().catch(() => []) ?? Promise.resolve([]));
|
|
14367
14548
|
const tasks = [...activeTasks, ...recentDoneTasks];
|
|
@@ -14643,7 +14824,7 @@ ${lines.join("\n")}`;
|
|
|
14643
14824
|
{ label: "docActionStaleness", hasData: docActionStalenessText !== void 0 }
|
|
14644
14825
|
]);
|
|
14645
14826
|
const doneTaskIds = new Set(
|
|
14646
|
-
|
|
14827
|
+
allClosedTasks.map((t) => t.displayId ?? t.id).filter((id) => !!id)
|
|
14647
14828
|
);
|
|
14648
14829
|
const earnedPushback = computeEarnedPushback({ reports, log: recentLog, doneTaskIds });
|
|
14649
14830
|
const context = {
|
|
@@ -15792,6 +15973,35 @@ function toDateLabel(value) {
|
|
|
15792
15973
|
const d = value instanceof Date ? value : new Date(value);
|
|
15793
15974
|
return Number.isNaN(d.getTime()) ? "" : d.toISOString().slice(0, 10);
|
|
15794
15975
|
}
|
|
15976
|
+
var STRATEGY_REVIEW_CHANNEL_ENV = "DISCORD_STRATEGY_REVIEW_CHANNEL_ID";
|
|
15977
|
+
function buildStrategyReviewPostDirective(cycleNumber) {
|
|
15978
|
+
const channelId = process.env[STRATEGY_REVIEW_CHANNEL_ENV]?.trim();
|
|
15979
|
+
if (!channelId) return null;
|
|
15980
|
+
return [
|
|
15981
|
+
"",
|
|
15982
|
+
"---",
|
|
15983
|
+
"## \u{1F4E3} Post the patch notes to Discord #papi-strategy-review",
|
|
15984
|
+
"",
|
|
15985
|
+
`This review closed **Cycle ${cycleNumber}**. Post a curated, safety-gated **rich embed** (NOT plain content, so links do not unfurl) to #papi-strategy-review (id \`${channelId}\`) via \`mcp__discord__send_embed\`. Run \`/patch-notes\` \u2014 it owns the mechanics.`,
|
|
15986
|
+
"",
|
|
15987
|
+
"**LOCKED TEMPLATE \u2014 every patch note uses this exact shape (do NOT improvise the structure):**",
|
|
15988
|
+
"- `title`: `PAPI Patch Notes v<version>`",
|
|
15989
|
+
"- `description`: `Covering Cycles <start>\u2013<end>`",
|
|
15990
|
+
"- `color`: `#6C5CE7` (PAPI purple)",
|
|
15991
|
+
"- `fields`: one per non-empty category, `inline: false`, `\u2022 ` bullets \u2014 \u{1F680} New Features, \u{1F6E0} Improvements, \u{1F41B} Bug Fixes, \u2699\uFE0F Under the Hood, \u{1F52E} What's Coming (always included)",
|
|
15992
|
+
"- `footer`: `PAPI v<version> \xB7 <date>`",
|
|
15993
|
+
"",
|
|
15994
|
+
"This is the DEEP multi-cycle note covering the whole review window, not a single release. Aim for 5-7 items in the strong sections. Strip task-IDs, branch names and internal framing; only changes an existing user would notice.",
|
|
15995
|
+
"",
|
|
15996
|
+
"Then:",
|
|
15997
|
+
"1. Run the build-in-public safety gate: NO external usernames, NO contributor or private work, NO owner cost/commercial data, NO failure-rate or funnel percentages.",
|
|
15998
|
+
"2. Show the draft embed for approval BEFORE posting.",
|
|
15999
|
+
`3. On approval, send the embed to channel \`${channelId}\`.`,
|
|
16000
|
+
"4. Then run `/content-pack --source=strategy_review` \u2014 a review is the richest content source PAPI produces, and it is the cadence the Blog and Substack lanes exist for.",
|
|
16001
|
+
"",
|
|
16002
|
+
"Skip only if nothing user-facing shipped in the whole window, and say so explicitly rather than moving on silently."
|
|
16003
|
+
].join("\n");
|
|
16004
|
+
}
|
|
15795
16005
|
var reviewPrepareCache = new PerCallerCache();
|
|
15796
16006
|
var strategyReviewTool = {
|
|
15797
16007
|
name: "strategy_review",
|
|
@@ -15973,6 +16183,19 @@ ${recLines.join("\n")}
|
|
|
15973
16183
|
}
|
|
15974
16184
|
} catch {
|
|
15975
16185
|
}
|
|
16186
|
+
let caps = {};
|
|
16187
|
+
try {
|
|
16188
|
+
const info = adapter2.getProjectInfo ? await adapter2.getProjectInfo() : null;
|
|
16189
|
+
caps = info?.capabilities ?? {};
|
|
16190
|
+
} catch {
|
|
16191
|
+
caps = {};
|
|
16192
|
+
}
|
|
16193
|
+
const postDirective = buildChangelogDirective(
|
|
16194
|
+
caps,
|
|
16195
|
+
buildStrategyReviewPostDirective(result.cycleNumber)
|
|
16196
|
+
);
|
|
16197
|
+
const postStep = postDirective ? `4. Post the patch notes to Discord \u2014 see the directive at the end of this output.
|
|
16198
|
+
5. Then run \`plan\` for the next cycle.` : `4. Then run \`plan\` for the next cycle.`;
|
|
15976
16199
|
const actionReminder = `
|
|
15977
16200
|
|
|
15978
16201
|
---
|
|
@@ -15981,8 +16204,10 @@ ${recLines.join("\n")}
|
|
|
15981
16204
|
1. Review the AD changes above \u2014 are any resolved/modified ADs still referenced by active tasks?
|
|
15982
16205
|
2. Act on strategic recommendations \u2014 cancel, defer, or reprioritise tasks as suggested.
|
|
15983
16206
|
3. Run \`strategy_change\` if any recommendation requires an AD update not already captured.
|
|
15984
|
-
|
|
15985
|
-
const response = textResponse(
|
|
16207
|
+
` + postStep;
|
|
16208
|
+
const response = textResponse(
|
|
16209
|
+
header + result.displayText + slackNote + recsCloseOut + actionReminder + (postDirective ?? "")
|
|
16210
|
+
);
|
|
15986
16211
|
return {
|
|
15987
16212
|
...response,
|
|
15988
16213
|
...contextBytes !== void 0 ? { _contextBytes: contextBytes } : {},
|
|
@@ -16386,7 +16611,17 @@ var BOARD_COLUMNS = [
|
|
|
16386
16611
|
{ name: "createdAt", header: "Created", value: (t) => t.createdAt ?? "-" },
|
|
16387
16612
|
{ name: "source", header: "Source", value: (t) => t.source ?? "-" }
|
|
16388
16613
|
];
|
|
16389
|
-
var
|
|
16614
|
+
var ASSIGNEE_FIELD = "assignee";
|
|
16615
|
+
function assigneeColumn(nameOf) {
|
|
16616
|
+
return {
|
|
16617
|
+
name: ASSIGNEE_FIELD,
|
|
16618
|
+
header: "Assignee",
|
|
16619
|
+
// An unresolved id still renders as a short id rather than blank: "someone holds
|
|
16620
|
+
// this" is the fact that matters, and a blank cell reads as unclaimed.
|
|
16621
|
+
value: (t) => t.assigneeId ? nameOf.get(t.assigneeId) ?? t.assigneeId.slice(0, 8) : "\u2014"
|
|
16622
|
+
};
|
|
16623
|
+
}
|
|
16624
|
+
var BOARD_FIELDS = [...BOARD_COLUMNS.map((c) => c.name), ASSIGNEE_FIELD];
|
|
16390
16625
|
var BOARD_SINGLE_LABELS = {
|
|
16391
16626
|
id: "Task",
|
|
16392
16627
|
title: "Title",
|
|
@@ -16445,6 +16680,11 @@ var boardViewTool = {
|
|
|
16445
16680
|
enum: ["full", "summary"],
|
|
16446
16681
|
description: 'Output mode: "full" (default) shows task table, "summary" shows counts only.'
|
|
16447
16682
|
},
|
|
16683
|
+
assignee: {
|
|
16684
|
+
type: "string",
|
|
16685
|
+
enum: ["me", "unclaimed"],
|
|
16686
|
+
description: 'Filter by who holds the task on a shared project. "me" shows only your own claims; "unclaimed" shows the open pool nobody has taken. Omit to see everything. On a multi-member project the board also gains an Assignee column showing who holds each task.'
|
|
16687
|
+
},
|
|
16448
16688
|
fields: {
|
|
16449
16689
|
type: "string",
|
|
16450
16690
|
description: `Optional sparse-field projection \u2014 comma-separated column names, e.g. "title,status". Returns ONLY those columns (id is always included so rows stay addressable) which is far cheaper on session context than the full table. Allowed: ${BOARD_FIELDS.join(", ")}. An unknown name is an error.`
|
|
@@ -16657,11 +16897,16 @@ function formatSingleTask(t, fields) {
|
|
|
16657
16897
|
if (t.why?.trim()) lines.push(`- **Why:** ${t.why.trim()}`);
|
|
16658
16898
|
return lines.join("\n");
|
|
16659
16899
|
}
|
|
16660
|
-
function formatBoard(result, fields) {
|
|
16900
|
+
function formatBoard(result, fields, assignee) {
|
|
16661
16901
|
if (result.tasks.length === 0) {
|
|
16662
16902
|
return "No tasks found.";
|
|
16663
16903
|
}
|
|
16664
|
-
const
|
|
16904
|
+
const available = assignee ? [
|
|
16905
|
+
...BOARD_COLUMNS.slice(0, 4),
|
|
16906
|
+
assignee,
|
|
16907
|
+
...BOARD_COLUMNS.slice(4)
|
|
16908
|
+
] : BOARD_COLUMNS;
|
|
16909
|
+
const columns = selectColumns(available, fields);
|
|
16665
16910
|
const headers = columns.map((c) => c.header);
|
|
16666
16911
|
const rows = result.tasks.map((t) => columns.map((c) => c.value(t)));
|
|
16667
16912
|
const lines = [];
|
|
@@ -16690,6 +16935,38 @@ function formatSummary(summary) {
|
|
|
16690
16935
|
}
|
|
16691
16936
|
return lines.join("\n");
|
|
16692
16937
|
}
|
|
16938
|
+
async function resolveBoardMembership(adapter2) {
|
|
16939
|
+
const empty = { isMultiMember: false, nameOf: /* @__PURE__ */ new Map(), callerUserId: null };
|
|
16940
|
+
if (!adapterSupports(adapter2, "listContributors")) return empty;
|
|
16941
|
+
let contributors;
|
|
16942
|
+
try {
|
|
16943
|
+
contributors = await adapter2.listContributors();
|
|
16944
|
+
} catch {
|
|
16945
|
+
return empty;
|
|
16946
|
+
}
|
|
16947
|
+
if (contributors.length <= 1) return empty;
|
|
16948
|
+
const nameOf = new Map(
|
|
16949
|
+
contributors.map((c) => [c.userId, c.displayName || c.email || c.userId.slice(0, 8)])
|
|
16950
|
+
);
|
|
16951
|
+
let callerUserId = null;
|
|
16952
|
+
try {
|
|
16953
|
+
if (adapterSupports(adapter2, "getOwnerIdentity")) {
|
|
16954
|
+
callerUserId = (await adapter2.getOwnerIdentity()).callerUserId ?? null;
|
|
16955
|
+
} else if (adapterSupports(adapter2, "getProjectOwnerUserId")) {
|
|
16956
|
+
callerUserId = await adapter2.getProjectOwnerUserId();
|
|
16957
|
+
}
|
|
16958
|
+
} catch {
|
|
16959
|
+
callerUserId = null;
|
|
16960
|
+
}
|
|
16961
|
+
return { isMultiMember: true, nameOf, callerUserId };
|
|
16962
|
+
}
|
|
16963
|
+
function applyAssigneeFilter(result, filter, callerUserId) {
|
|
16964
|
+
if (!filter) return result;
|
|
16965
|
+
const tasks = result.tasks.filter(
|
|
16966
|
+
(t) => filter === "unclaimed" ? !t.assigneeId : t.assigneeId === callerUserId
|
|
16967
|
+
);
|
|
16968
|
+
return { ...result, tasks, returned: tasks.length, total: tasks.length, hasMore: false };
|
|
16969
|
+
}
|
|
16693
16970
|
async function handleBoardView(adapter2, args) {
|
|
16694
16971
|
const mode = args.mode;
|
|
16695
16972
|
const projection = resolveFields(args.fields, BOARD_FIELDS, "board_view");
|
|
@@ -16708,6 +16985,18 @@ async function handleBoardView(adapter2, args) {
|
|
|
16708
16985
|
const meta2 = emitMeta ? { total: 1, filtered: 1, returned: 1, limit: null, offset: 0 } : null;
|
|
16709
16986
|
return textResponse(withMeta(formatSingleTask(task, fields), meta2));
|
|
16710
16987
|
}
|
|
16988
|
+
const membership = await resolveBoardMembership(adapter2);
|
|
16989
|
+
const assigneeFilterRaw = typeof args.assignee === "string" ? args.assignee.trim().toLowerCase() : void 0;
|
|
16990
|
+
if (assigneeFilterRaw !== void 0 && assigneeFilterRaw !== "me" && assigneeFilterRaw !== "unclaimed") {
|
|
16991
|
+
return errorResponse(
|
|
16992
|
+
`Unknown assignee filter "${args.assignee}". Use assignee="me" for your own claims, or assignee="unclaimed" for the open pool.`
|
|
16993
|
+
);
|
|
16994
|
+
}
|
|
16995
|
+
if (assigneeFilterRaw === "me" && !membership.callerUserId) {
|
|
16996
|
+
return errorResponse(
|
|
16997
|
+
'assignee="me" needs an identified caller, and this connection has none. Use assignee="unclaimed" to see the open pool.'
|
|
16998
|
+
);
|
|
16999
|
+
}
|
|
16711
17000
|
const result = await viewBoard(adapter2, void 0, {
|
|
16712
17001
|
phase: args.phase,
|
|
16713
17002
|
status: args.status,
|
|
@@ -16716,7 +17005,12 @@ async function handleBoardView(adapter2, args) {
|
|
|
16716
17005
|
query: args.query,
|
|
16717
17006
|
cycle: args.cycle
|
|
16718
17007
|
});
|
|
16719
|
-
|
|
17008
|
+
const filtered = applyAssigneeFilter(result, assigneeFilterRaw, membership.callerUserId);
|
|
17009
|
+
let output = formatBoard(
|
|
17010
|
+
filtered,
|
|
17011
|
+
fields,
|
|
17012
|
+
membership.isMultiMember ? assigneeColumn(membership.nameOf) : null
|
|
17013
|
+
);
|
|
16720
17014
|
if (!fields) {
|
|
16721
17015
|
try {
|
|
16722
17016
|
const comments = await adapter2.getRecentTaskComments?.(30);
|
|
@@ -18658,6 +18952,118 @@ async function ensureMcpJsonGitignored(projectRoot) {
|
|
|
18658
18952
|
}
|
|
18659
18953
|
}
|
|
18660
18954
|
|
|
18955
|
+
// src/tools/join.ts
|
|
18956
|
+
var joinTool = {
|
|
18957
|
+
name: "join",
|
|
18958
|
+
description: "Join a project that already exists, as a new team member. Run this instead of `setup` when someone else already set the project up \u2014 setup is for creating a project, and running it as a joiner writes scaffolding into a repo that already has it. `join` confirms which project you are bound to, shows the Active Decisions and canon docs you are expected to read, and shows the task pool you can claim from. It writes NOTHING to your repo. Does not call the Anthropic API.",
|
|
18959
|
+
annotations: { title: "Join Project", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
18960
|
+
inputSchema: {
|
|
18961
|
+
type: "object",
|
|
18962
|
+
properties: {
|
|
18963
|
+
limit: {
|
|
18964
|
+
type: "integer",
|
|
18965
|
+
description: "Maximum claimable tasks to list (default 10)."
|
|
18966
|
+
}
|
|
18967
|
+
},
|
|
18968
|
+
required: []
|
|
18969
|
+
}
|
|
18970
|
+
};
|
|
18971
|
+
function formatDecisions(decisions) {
|
|
18972
|
+
if (decisions.length === 0) {
|
|
18973
|
+
return "_No Active Decisions recorded yet._";
|
|
18974
|
+
}
|
|
18975
|
+
const shown = decisions.slice(0, 15);
|
|
18976
|
+
const lines = shown.map((d) => `- **${d.id}** \u2014 ${d.title ?? "(untitled)"}${d.confidence ? ` _(${d.confidence})_` : ""}`);
|
|
18977
|
+
const more = decisions.length > shown.length ? `
|
|
18978
|
+
- _\u2026and ${decisions.length - shown.length} more. Run \`ad_view\` for the full set._` : "";
|
|
18979
|
+
return lines.join("\n") + more;
|
|
18980
|
+
}
|
|
18981
|
+
function formatDocs(docs) {
|
|
18982
|
+
if (docs.length === 0) {
|
|
18983
|
+
return "_No registered canon docs. Ask the project owner what to read._";
|
|
18984
|
+
}
|
|
18985
|
+
return docs.slice(0, 12).map((d) => `- \`${d.path}\`${d.title ? ` \u2014 ${d.title}` : ""}`).join("\n");
|
|
18986
|
+
}
|
|
18987
|
+
function formatPool(tasks, limit) {
|
|
18988
|
+
if (tasks.length === 0) {
|
|
18989
|
+
return "_The shared backlog is empty right now \u2014 ask the owner what to pick up._";
|
|
18990
|
+
}
|
|
18991
|
+
const rows = tasks.slice(0, limit).map(
|
|
18992
|
+
(t) => `| ${t.id} | ${t.title} | ${t.priority ?? "-"} | ${t.complexity ?? "-"} |`
|
|
18993
|
+
);
|
|
18994
|
+
const more = tasks.length > limit ? `
|
|
18995
|
+
|
|
18996
|
+
_\u2026and ${tasks.length - limit} more in the pool._` : "";
|
|
18997
|
+
return `| Task | Summary | Priority | Effort |
|
|
18998
|
+
|---|---|---|---|
|
|
18999
|
+
${rows.join("\n")}${more}`;
|
|
19000
|
+
}
|
|
19001
|
+
var JOIN_POINTER = "Run `join` instead \u2014 it shows you the project, its Active Decisions, the docs to read, and the tasks you can claim, without writing anything to the repo.";
|
|
19002
|
+
async function handleJoin(adapter2, config2, args) {
|
|
19003
|
+
const limit = typeof args.limit === "number" && args.limit > 0 ? Math.floor(args.limit) : 10;
|
|
19004
|
+
let projectLabel = "this project";
|
|
19005
|
+
try {
|
|
19006
|
+
const brief = adapterSupports(adapter2, "readProductBrief") ? await adapter2.readProductBrief() : "";
|
|
19007
|
+
const heading = brief.match(/^#\s+(.+)$/m)?.[1]?.trim();
|
|
19008
|
+
if (heading) projectLabel = heading;
|
|
19009
|
+
} catch {
|
|
19010
|
+
}
|
|
19011
|
+
const gate = await resolveOwnerGate(adapter2, config2);
|
|
19012
|
+
const [decisions, docs, pool] = await Promise.all([
|
|
19013
|
+
adapter2.getActiveDecisions().catch(() => []),
|
|
19014
|
+
// SECURITY: visibility scoping is delegated to searchDocs' requesterUserId
|
|
19015
|
+
// (task-1977) rather than reading rows directly — a contributors-tier or
|
|
19016
|
+
// private doc must not become readable just because someone joined. Passing
|
|
19017
|
+
// the caller means a non-owner sees public docs plus the contributors-tier
|
|
19018
|
+
// docs they are actually a cohort member of; private is excluded fail-closed.
|
|
19019
|
+
adapterSupports(adapter2, "searchDocs") ? adapter2.searchDocs({ requesterUserId: gate.callerUserId ?? void 0, limit: 12 }).catch(() => []) : Promise.resolve([]),
|
|
19020
|
+
// The claimable pool is the shared backlog: unassigned AND not yet pulled into
|
|
19021
|
+
// anyone's cycle. Same invariant task_claim enforces (assigneeId == null &&
|
|
19022
|
+
// cycle == null), so nothing is listed here that a claim would then refuse.
|
|
19023
|
+
adapter2.queryBoard({ status: ["Backlog"] }).catch(() => [])
|
|
19024
|
+
]);
|
|
19025
|
+
const claimable = pool.filter((t) => !t.assigneeId && t.cycle == null);
|
|
19026
|
+
let conventionLine = "";
|
|
19027
|
+
try {
|
|
19028
|
+
if (adapterSupports(adapter2, "listConventions")) {
|
|
19029
|
+
const conventions = await adapter2.listConventions();
|
|
19030
|
+
if (conventions.length > 0) {
|
|
19031
|
+
conventionLine = `
|
|
19032
|
+
|
|
19033
|
+
## Conventions that ride every build
|
|
19034
|
+
` + conventions.slice(0, 8).map((c) => `- ${c.rule}`).join("\n");
|
|
19035
|
+
}
|
|
19036
|
+
}
|
|
19037
|
+
} catch {
|
|
19038
|
+
}
|
|
19039
|
+
const roleLine = gate.callerIsOwner ? `You are the **owner** of this project.` : `You are a **member** of this project.`;
|
|
19040
|
+
return textResponse(
|
|
19041
|
+
`# Joined \u2014 ${projectLabel}
|
|
19042
|
+
|
|
19043
|
+
${roleLine} Nothing has been written to your repo \u2014 \`join\` is read-only, and this project's harness files are already committed by whoever set it up.
|
|
19044
|
+
|
|
19045
|
+
**If that is not the project you expected, stop here.** Your connection is bound to the wrong project; fix \`PAPI_PROJECT_ID\` (local) or the \`x-papi-project-id\` header (remote), or run \`project_switch\`, before writing anything.
|
|
19046
|
+
|
|
19047
|
+
## Decisions already made
|
|
19048
|
+
These are settled stances, not suggestions. Read them before proposing a different approach \u2014 if you disagree with one, say so rather than working around it.
|
|
19049
|
+
|
|
19050
|
+
${formatDecisions(decisions)}
|
|
19051
|
+
|
|
19052
|
+
## Canon docs to read
|
|
19053
|
+
${formatDocs(docs)}` + conventionLine + `
|
|
19054
|
+
|
|
19055
|
+
## Work you can claim
|
|
19056
|
+
${claimable.length} task(s) in the shared backlog. Claim one with \`task_claim <task-id>\` \u2014 it becomes yours and shows against your name on the board.
|
|
19057
|
+
|
|
19058
|
+
${formatPool(claimable, limit)}
|
|
19059
|
+
|
|
19060
|
+
## Your next move
|
|
19061
|
+
Run \`orient\` to see where the project stands and what your own cycle looks like. You get your OWN cycle numbering \u2014 your cycle 1 is independent of everyone else's.
|
|
19062
|
+
|
|
19063
|
+
Do NOT run \`setup\`. It is for creating a project, and this one already exists.`
|
|
19064
|
+
);
|
|
19065
|
+
}
|
|
19066
|
+
|
|
18661
19067
|
// src/tools/setup.ts
|
|
18662
19068
|
var setupTool = {
|
|
18663
19069
|
name: "setup",
|
|
@@ -18875,8 +19281,36 @@ function formatSetupNarration(result) {
|
|
|
18875
19281
|
...steps.map((s, i) => `${i + 1}. ${s}`)
|
|
18876
19282
|
];
|
|
18877
19283
|
}
|
|
19284
|
+
async function refuseIfJoiner(adapter2, config2) {
|
|
19285
|
+
let gate;
|
|
19286
|
+
try {
|
|
19287
|
+
gate = await resolveOwnerGate(adapter2, config2);
|
|
19288
|
+
} catch {
|
|
19289
|
+
return null;
|
|
19290
|
+
}
|
|
19291
|
+
if (!gate.enforced || gate.callerIsOwner) return null;
|
|
19292
|
+
if (gate.resolutionError || !gate.callerUserId) return null;
|
|
19293
|
+
let alreadySetUp = false;
|
|
19294
|
+
try {
|
|
19295
|
+
if (adapterSupports(adapter2, "readProductBrief")) {
|
|
19296
|
+
alreadySetUp = (await adapter2.readProductBrief()).trim().length > 0;
|
|
19297
|
+
}
|
|
19298
|
+
} catch {
|
|
19299
|
+
return null;
|
|
19300
|
+
}
|
|
19301
|
+
if (!alreadySetUp) return null;
|
|
19302
|
+
return `This project is already set up, and you are joining it rather than creating it.
|
|
19303
|
+
|
|
19304
|
+
\`setup\` writes the project harness \u2014 AGENTS.md, CLAUDE.md, .claude/settings.json, docs/ \u2014 into your repo. Those files are already here and committed by whoever set the project up, so running setup would rewrite shared files and record "first" Active Decisions into a registry that already has them.
|
|
19305
|
+
|
|
19306
|
+
${JOIN_POINTER}
|
|
19307
|
+
|
|
19308
|
+
If you genuinely need to re-scaffold this project, ask its owner to run setup.`;
|
|
19309
|
+
}
|
|
18878
19310
|
async function handleSetup(adapter2, config2, args, clientName) {
|
|
18879
19311
|
const toolMode = args.mode;
|
|
19312
|
+
const joinerRefusal = await refuseIfJoiner(adapter2, config2);
|
|
19313
|
+
if (joinerRefusal) return errorResponse(joinerRefusal);
|
|
18880
19314
|
const REQUIRED_FIELDS = ["project_name"];
|
|
18881
19315
|
const missing = REQUIRED_FIELDS.filter((f) => !args[f] || typeof args[f] === "string" && !args[f].trim());
|
|
18882
19316
|
if (missing.length > 0) {
|
|
@@ -19154,89 +19588,10 @@ Examples (use whichever provider you have): ${spec.examples}.
|
|
|
19154
19588
|
_Recommendation only \u2014 PAPI never selects or runs a model. Policy: XS/S \u2192 cheap, M/L \u2192 capable, XL \u2192 frontier._`;
|
|
19155
19589
|
}
|
|
19156
19590
|
|
|
19157
|
-
// src/lib/capabilities.ts
|
|
19158
|
-
init_dist();
|
|
19159
|
-
|
|
19160
19591
|
// src/tools/build.ts
|
|
19161
19592
|
init_dist();
|
|
19162
19593
|
init_dist();
|
|
19163
19594
|
|
|
19164
|
-
// src/lib/directive-builders.ts
|
|
19165
|
-
function buildPrReviewerDirective(caps) {
|
|
19166
|
-
if (!isCapabilityEnabled(caps, "prReviewer")) return null;
|
|
19167
|
-
return `
|
|
19168
|
-
|
|
19169
|
-
**Quality Gate:** no auto-review attached. PAPI's standard pre-accept step is a code review of the branch diff \u2014 run \`review_submit\` with \`dispatch:"subagent"\` to auto-review, or attach \`auto_review\` findings. Risk-tier work (auth, data, migrations, CI) should always carry one.`;
|
|
19170
|
-
}
|
|
19171
|
-
function buildChangelogDirective(caps, inner) {
|
|
19172
|
-
if (!isCapabilityEnabled(caps, "changelog")) return null;
|
|
19173
|
-
return inner;
|
|
19174
|
-
}
|
|
19175
|
-
function buildDiscoveredIssuesDirective(caps, issueLines) {
|
|
19176
|
-
if (!isCapabilityEnabled(caps, "discoveredIssues")) return null;
|
|
19177
|
-
if (issueLines.length === 0) return null;
|
|
19178
|
-
return [
|
|
19179
|
-
"",
|
|
19180
|
-
"---",
|
|
19181
|
-
"",
|
|
19182
|
-
`## Discovered Issues (${issueLines.length})`,
|
|
19183
|
-
"",
|
|
19184
|
-
...issueLines,
|
|
19185
|
-
"",
|
|
19186
|
-
"*These issues were logged during builds \u2014 triage them in the next plan.*"
|
|
19187
|
-
].join("\n");
|
|
19188
|
-
}
|
|
19189
|
-
function buildModelRecommendationDirective(caps, tierBlock) {
|
|
19190
|
-
if (!isCapabilityEnabled(caps, "modelRecommendation")) return null;
|
|
19191
|
-
return tierBlock;
|
|
19192
|
-
}
|
|
19193
|
-
function buildVerifyHealthCheckDirective(caps) {
|
|
19194
|
-
if (!isCapabilityEnabled(caps, "verifyHealthCheck")) return null;
|
|
19195
|
-
return `
|
|
19196
|
-
|
|
19197
|
-
**Health check:** before you consider the cycle shipped, verify cycle state (plan validity, review coverage, branch hygiene) \u2014 run the \`papi-verify\` skill or a quick \`board_view\` pass.`;
|
|
19198
|
-
}
|
|
19199
|
-
function buildGestaltPreBuildDirective(caps) {
|
|
19200
|
-
if (!isCapabilityEnabled(caps, "gestaltPreBuild")) return null;
|
|
19201
|
-
return `
|
|
19202
|
-
|
|
19203
|
-
**Gestalt pre-build:** on the first task of a multi-task cycle, read every task's BUILD HANDOFF together (shared files, sequencing, module split) before building \u2014 one-time check at cycle start.`;
|
|
19204
|
-
}
|
|
19205
|
-
function buildBatchBuildRollupDirective(caps) {
|
|
19206
|
-
if (!isCapabilityEnabled(caps, "batchBuildRollup")) return null;
|
|
19207
|
-
return `
|
|
19208
|
-
|
|
19209
|
-
**Batch rollup:** after the final task of a batch, emit a cycle-level rollup \u2014 build summary table + discovered issues grouped by severity + next action.`;
|
|
19210
|
-
}
|
|
19211
|
-
function buildSecurityScanDirective(caps) {
|
|
19212
|
-
if (!isCapabilityEnabled(caps, "securityScan")) return null;
|
|
19213
|
-
return `
|
|
19214
|
-
|
|
19215
|
-
**Security scan:** risk-tier changes (auth, data, migrations, secrets, endpoints) should carry a security pass \u2014 OWASP top-10 + secret-exposure check on the changed surface before merge.`;
|
|
19216
|
-
}
|
|
19217
|
-
function buildDeployHookDirective(caps, deployCommand) {
|
|
19218
|
-
if (!isCapabilityEnabled(caps, "deployHook")) return null;
|
|
19219
|
-
const command = deployCommand?.trim() || void 0;
|
|
19220
|
-
if (!command) return null;
|
|
19221
|
-
return `
|
|
19222
|
-
|
|
19223
|
-
---
|
|
19224
|
-
|
|
19225
|
-
## Deploy \u2014 run after this release
|
|
19226
|
-
|
|
19227
|
-
The post-release deploy hook is on and a deploy command is configured, so ship the merged release now.
|
|
19228
|
-
|
|
19229
|
-
Run your deploy command yourself:
|
|
19230
|
-
\`\`\`
|
|
19231
|
-
${command}
|
|
19232
|
-
\`\`\`
|
|
19233
|
-
PAPI never runs this command itself (AD-58) \u2014 you run it in your own environment. Turn the "Post-release deploy" capability off in the dashboard, or unset PAPI_DEPLOY, to stop this reminder.`;
|
|
19234
|
-
}
|
|
19235
|
-
function buildPapiMetaFramingDirective(caps, inner) {
|
|
19236
|
-
if (!isCapabilityEnabled(caps, "papiMetaFraming")) return null;
|
|
19237
|
-
return inner;
|
|
19238
|
-
}
|
|
19239
|
-
|
|
19240
19595
|
// src/services/build.ts
|
|
19241
19596
|
init_dist();
|
|
19242
19597
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
@@ -21335,6 +21690,20 @@ async function safeUpdateBlocker(adapter2, taskId, blocker) {
|
|
|
21335
21690
|
} catch {
|
|
21336
21691
|
}
|
|
21337
21692
|
}
|
|
21693
|
+
async function resolveMemberBranchSlug(adapter2) {
|
|
21694
|
+
if (!adapterSupports(adapter2, "listContributors")) return void 0;
|
|
21695
|
+
try {
|
|
21696
|
+
const members = await adapter2.listContributors();
|
|
21697
|
+
if (members.length <= 1) return void 0;
|
|
21698
|
+
const callerUserId = adapterSupports(adapter2, "getOwnerIdentity") ? (await adapter2.getOwnerIdentity()).callerUserId ?? null : adapterSupports(adapter2, "getProjectOwnerUserId") ? await adapter2.getProjectOwnerUserId() : null;
|
|
21699
|
+
if (!callerUserId) return void 0;
|
|
21700
|
+
const me = members.find((m) => m.userId === callerUserId);
|
|
21701
|
+
if (!me) return void 0;
|
|
21702
|
+
return memberBranchSlug({ displayName: me.displayName, email: me.email, userId: me.userId });
|
|
21703
|
+
} catch {
|
|
21704
|
+
return void 0;
|
|
21705
|
+
}
|
|
21706
|
+
}
|
|
21338
21707
|
async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
21339
21708
|
const task = await adapter2.getTask(taskId);
|
|
21340
21709
|
if (!task) {
|
|
@@ -21437,13 +21806,14 @@ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
|
|
|
21437
21806
|
}
|
|
21438
21807
|
const inFlightTaskBranch = !depBranchReuse && branchExists(config2.projectRoot, taskBranchName(taskId)) ? taskBranchName(taskId) : void 0;
|
|
21439
21808
|
const useSharedBranch = !depBranchReuse && !inFlightTaskBranch && !!task.module && cycleNumber > 0;
|
|
21809
|
+
const memberSlug = useSharedBranch ? await resolveMemberBranchSlug(adapter2) : void 0;
|
|
21440
21810
|
let originCycleBranch;
|
|
21441
21811
|
if (useSharedBranch && hasRemote(config2.projectRoot)) {
|
|
21442
21812
|
const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
|
|
21443
21813
|
const remoteCycleBranches = listGroupedCycleBranches(config2.projectRoot, cycleNumber, baseBranch);
|
|
21444
|
-
originCycleBranch = pickModuleCycleBranch(remoteCycleBranches, cycleNumber, task.module);
|
|
21814
|
+
originCycleBranch = pickModuleCycleBranch(remoteCycleBranches, cycleNumber, task.module, memberSlug);
|
|
21445
21815
|
}
|
|
21446
|
-
const featureBranch = depBranchReuse ? depBranchReuse.branch : inFlightTaskBranch ? inFlightTaskBranch : useSharedBranch ? originCycleBranch ?? cycleBranchName(cycleNumber, task.module) : taskBranchName(taskId);
|
|
21816
|
+
const featureBranch = depBranchReuse ? depBranchReuse.branch : inFlightTaskBranch ? inFlightTaskBranch : useSharedBranch ? originCycleBranch ?? cycleBranchName(cycleNumber, task.module, memberSlug) : taskBranchName(taskId);
|
|
21447
21817
|
if (depBranchReuse) {
|
|
21448
21818
|
branchLines.push(
|
|
21449
21819
|
`Reusing branch '${depBranchReuse.branch}' from dependency ${depBranchReuse.upstreamId} \u2014 commits will stack for a single PR.`
|
|
@@ -24749,7 +25119,10 @@ Re-submit with \`notes: "... Reference: <path>"\` to link one, or ignore if none
|
|
|
24749
25119
|
const conflictNote = result.adConflicts ? buildAdConflictNote(
|
|
24750
25120
|
result.adConflicts.explicit ? result.adConflicts.adIds[0] : void 0,
|
|
24751
25121
|
result.adConflicts.explicit ? [] : result.adConflicts.adIds,
|
|
24752
|
-
result.adConflicts.gated
|
|
25122
|
+
result.adConflicts.gated,
|
|
25123
|
+
// task-3347: resolved alongside the ids, so the directive names the
|
|
25124
|
+
// decision the user is being asked to rule on.
|
|
25125
|
+
new Map(Object.entries(result.adConflicts.adTitles ?? {}))
|
|
24753
25126
|
) : "";
|
|
24754
25127
|
return textResponse(`${result.message}${overrideNote}${branchNote}${truncateWarning}${refNudge}${conflictNote}`);
|
|
24755
25128
|
}
|
|
@@ -27972,6 +28345,29 @@ ${writeNote}
|
|
|
27972
28345
|
return textResponse(output);
|
|
27973
28346
|
}
|
|
27974
28347
|
|
|
28348
|
+
// src/lib/workspace-mode.ts
|
|
28349
|
+
init_git();
|
|
28350
|
+
var cache3 = /* @__PURE__ */ new Map();
|
|
28351
|
+
function resolveWorkspaceMode(projectRoot) {
|
|
28352
|
+
const cached2 = cache3.get(projectRoot);
|
|
28353
|
+
if (cached2) return cached2;
|
|
28354
|
+
let result;
|
|
28355
|
+
if (!isGitAvailable()) {
|
|
28356
|
+
result = { mode: "no-git", gitBacked: false, reason: "git-not-installed" };
|
|
28357
|
+
} else if (!isGitRepo(projectRoot)) {
|
|
28358
|
+
result = { mode: "no-git", gitBacked: false, reason: "not-a-repo" };
|
|
28359
|
+
} else {
|
|
28360
|
+
result = { mode: "git", gitBacked: true, reason: null };
|
|
28361
|
+
}
|
|
28362
|
+
cache3.set(projectRoot, result);
|
|
28363
|
+
return result;
|
|
28364
|
+
}
|
|
28365
|
+
function formatWorkspaceModeLine(result) {
|
|
28366
|
+
if (result.gitBacked) return void 0;
|
|
28367
|
+
const because = result.reason === "git-not-installed" ? "no version control was detected on this machine" : "this folder is not under version control";
|
|
28368
|
+
return `**Mode:** running without version control \u2014 ${because}. The full loop works: plan, build, review and release all run normally, and your work is saved to PAPI either way. Version control would add branch-per-task, change history and pull-request review on top.`;
|
|
28369
|
+
}
|
|
28370
|
+
|
|
27975
28371
|
// src/services/health.ts
|
|
27976
28372
|
function computeZoomOutWarning(cycleNumber, lastZoomOutCycle) {
|
|
27977
28373
|
if (cycleNumber <= 0) return "";
|
|
@@ -28309,15 +28705,16 @@ async function findUnblockCandidates(adapter2, currentCycle) {
|
|
|
28309
28705
|
const blocker = blocked.blocker;
|
|
28310
28706
|
if (!blocker) continue;
|
|
28311
28707
|
const resolved = isBlockerResolved(blocker, ctx);
|
|
28708
|
+
const blockerTitle = resolveBlockerTitle(blocker, ctx) ?? "";
|
|
28312
28709
|
candidates.push({
|
|
28313
28710
|
blockedId: blocked.displayId ?? blocked.id,
|
|
28314
28711
|
blockedTitle: blocked.title,
|
|
28315
28712
|
blockerId: blocker.ref,
|
|
28316
|
-
blockerTitle
|
|
28713
|
+
blockerTitle,
|
|
28317
28714
|
blockerCycle: blocker.blockedCycle,
|
|
28318
28715
|
typed: {
|
|
28319
28716
|
kind: blocker.type,
|
|
28320
|
-
waiting: formatBlockerWaiting(blocker),
|
|
28717
|
+
waiting: formatBlockerWaiting(blocker, blockerTitle),
|
|
28321
28718
|
resolved
|
|
28322
28719
|
}
|
|
28323
28720
|
});
|
|
@@ -28383,9 +28780,9 @@ function formatUnblockSection(candidates) {
|
|
|
28383
28780
|
lines.push("");
|
|
28384
28781
|
for (const c of resolved) {
|
|
28385
28782
|
if (c.typed) {
|
|
28386
|
-
lines.push(`- **${c.blockedId}** (${c.blockedTitle}) \u2014 ${c.typed.kind}: ${c.blockerId} now resolved \u2705`);
|
|
28783
|
+
lines.push(`- **${c.blockedId}** (${c.blockedTitle}) \u2014 ${c.typed.kind}: ${formatRef(c.blockerId, c.blockerTitle)} now resolved \u2705`);
|
|
28387
28784
|
} else {
|
|
28388
|
-
lines.push(`- **${c.blockedId}** (${c.blockedTitle}) \u2014 blocker **${c.blockerId}** Done in C${c.blockerCycle}`);
|
|
28785
|
+
lines.push(`- **${c.blockedId}** (${c.blockedTitle}) \u2014 blocker **${formatRef(c.blockerId, c.blockerTitle)}** Done in C${c.blockerCycle}`);
|
|
28389
28786
|
}
|
|
28390
28787
|
}
|
|
28391
28788
|
}
|
|
@@ -29005,6 +29402,13 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
|
|
|
29005
29402
|
lines.push(`**Connection:** ${statusIcon} ${statusLabel}`);
|
|
29006
29403
|
lines.push("");
|
|
29007
29404
|
}
|
|
29405
|
+
if (projectRoot) {
|
|
29406
|
+
const modeLine = formatWorkspaceModeLine(resolveWorkspaceMode(projectRoot));
|
|
29407
|
+
if (modeLine) {
|
|
29408
|
+
lines.push(modeLine);
|
|
29409
|
+
lines.push("");
|
|
29410
|
+
}
|
|
29411
|
+
}
|
|
29008
29412
|
const harness = detectHarness(clientName);
|
|
29009
29413
|
const roleNote = connectOnlyRoleNote(harness);
|
|
29010
29414
|
if (roleNote) {
|
|
@@ -29349,6 +29753,41 @@ async function computeTeamSummary(adapter2, contributorsInput) {
|
|
|
29349
29753
|
const reviewQueue = tasks.filter((t) => t.status === "In Review").length;
|
|
29350
29754
|
return `**Team:** ${members} members \xB7 ${pool} in pool \xB7 ${inFlight} in flight \xB7 ${reviewQueue} in review`;
|
|
29351
29755
|
}
|
|
29756
|
+
async function computeCohortVisibility(adapter2, config2, contributorsInput) {
|
|
29757
|
+
if (!adapterSupports(adapter2, "listContributors")) return void 0;
|
|
29758
|
+
let members;
|
|
29759
|
+
try {
|
|
29760
|
+
members = (await (contributorsInput ?? adapter2.listContributors())).length;
|
|
29761
|
+
} catch {
|
|
29762
|
+
return void 0;
|
|
29763
|
+
}
|
|
29764
|
+
if (members <= 1) return void 0;
|
|
29765
|
+
let gate;
|
|
29766
|
+
try {
|
|
29767
|
+
gate = await resolveOwnerGate(adapter2, config2);
|
|
29768
|
+
} catch {
|
|
29769
|
+
return void 0;
|
|
29770
|
+
}
|
|
29771
|
+
if (gate.enforced && !gate.callerIsOwner) return void 0;
|
|
29772
|
+
let tasks;
|
|
29773
|
+
try {
|
|
29774
|
+
tasks = await adapter2.queryBoard({ compact: true });
|
|
29775
|
+
} catch {
|
|
29776
|
+
return void 0;
|
|
29777
|
+
}
|
|
29778
|
+
const open = tasks.filter(
|
|
29779
|
+
(t) => t.status !== "Done" && t.status !== "Cancelled" && t.status !== "Archived"
|
|
29780
|
+
);
|
|
29781
|
+
if (open.length === 0) return void 0;
|
|
29782
|
+
const publicCount = open.filter((t) => !t.visibility || t.visibility === "public").length;
|
|
29783
|
+
const contributorCount = publicCount + open.filter((t) => t.visibility === "contributors").length;
|
|
29784
|
+
const hiddenFromContributors = open.length - contributorCount;
|
|
29785
|
+
if (hiddenFromContributors === 0) {
|
|
29786
|
+
return `**Cohort visibility:** your ${members - 1} teammate(s) can see the whole active board (${open.length} tasks).`;
|
|
29787
|
+
}
|
|
29788
|
+
const pct = Math.round(hiddenFromContributors / open.length * 100);
|
|
29789
|
+
return `**Cohort visibility:** of ${open.length} active tasks, a contributor sees ${contributorCount} and a public-tier viewer sees ${publicCount} \u2014 **${hiddenFromContributors} (${pct}%) are private and invisible to your team.**`;
|
|
29790
|
+
}
|
|
29352
29791
|
async function computeReleaseHistory(adapter2, contributorsInput) {
|
|
29353
29792
|
if (!adapterSupports(adapter2, "listContributors")) return void 0;
|
|
29354
29793
|
let contributors;
|
|
@@ -29654,7 +30093,7 @@ async function handleOrient(rawAdapter, config2, args = {}, clientName, serverVe
|
|
|
29654
30093
|
return adWords.some((w) => docText.includes(w));
|
|
29655
30094
|
});
|
|
29656
30095
|
const cycleLabel = `C${doc.cycleUpdated ?? doc.cycleCreated}`;
|
|
29657
|
-
const adRef = relatedAds.length > 0 ? ` \u2014 may relate to ${relatedAds.map((a) => a.displayId).join(", ")}` : "";
|
|
30096
|
+
const adRef = relatedAds.length > 0 ? ` \u2014 may relate to ${relatedAds.map((a) => formatRef(a.displayId, a.title)).join(", ")}` : "";
|
|
29658
30097
|
const pendingActions = doc.actions?.filter((a) => a.status === "pending") ?? [];
|
|
29659
30098
|
lines.push(`- **${doc.title}** [${cycleLabel}${adRef}]`);
|
|
29660
30099
|
for (const action of pendingActions.slice(0, 2)) {
|
|
@@ -29761,8 +30200,8 @@ async function handleOrient(rawAdapter, config2, args = {}, clientName, serverVe
|
|
|
29761
30200
|
const mismatches = detectBoardMismatches(config2.projectRoot, allTasks);
|
|
29762
30201
|
if (mismatches.codeAhead.length > 0 || mismatches.staleInProgress.length > 0) {
|
|
29763
30202
|
const lines = ["\n\n## Reconciliation"];
|
|
29764
|
-
for (const m of mismatches.codeAhead) lines.push(`\u26A0\uFE0F **${m.displayId}** \u2014 branch \`${m.branch}\` merged to main but task is still Backlog. Run \`board_edit\` to mark Done.`);
|
|
29765
|
-
for (const m of mismatches.staleInProgress) lines.push(`\u26A0\uFE0F **${m.displayId}** \u2014 In Progress but no feature branch found. May be stale or built without \`build_execute\`.`);
|
|
30203
|
+
for (const m of mismatches.codeAhead) lines.push(`\u26A0\uFE0F **${formatRef(m.displayId, m.title)}** \u2014 branch \`${m.branch}\` merged to main but task is still Backlog. Run \`board_edit\` to mark Done.`);
|
|
30204
|
+
for (const m of mismatches.staleInProgress) lines.push(`\u26A0\uFE0F **${formatRef(m.displayId, m.title)}** \u2014 In Progress but no feature branch found. May be stale or built without \`build_execute\`.`);
|
|
29766
30205
|
reconciliationNote2 = lines.join("\n");
|
|
29767
30206
|
}
|
|
29768
30207
|
} catch {
|
|
@@ -29776,7 +30215,7 @@ async function handleOrient(rawAdapter, config2, args = {}, clientName, serverVe
|
|
|
29776
30215
|
lines.push(`${merged.length} In-Progress task(s) already merged to the default branch \u2014 close them out with \`build_execute\` complete (or \`board_edit\` to mark Done):`);
|
|
29777
30216
|
for (const m of merged) {
|
|
29778
30217
|
const ref = m.pr ? `${m.pr} (\`${m.commit}\`)` : `\`${m.commit}\``;
|
|
29779
|
-
lines.push(`- \u26A0\uFE0F **${m.displayId}** \u2014 merged via ${ref}: ${m.subject}`);
|
|
30218
|
+
lines.push(`- \u26A0\uFE0F **${formatRef(m.displayId, m.title)}** \u2014 merged via ${ref}: ${m.subject}`);
|
|
29780
30219
|
}
|
|
29781
30220
|
mergedInProgressNote2 = lines.join("\n");
|
|
29782
30221
|
}
|
|
@@ -29909,7 +30348,7 @@ ${versionDrift}` : "";
|
|
|
29909
30348
|
}
|
|
29910
30349
|
tracker.mark("parallel-tail");
|
|
29911
30350
|
const sharedContributorsPromise = adapterSupports(adapter2, "listContributors") ? adapter2.listContributors().catch(() => []) : Promise.resolve([]);
|
|
29912
|
-
const [unblockCandidates, subAgents, teamSummaryLine, releaseHistoryLine, carryForwardRefs] = await Promise.all([
|
|
30351
|
+
const [unblockCandidates, subAgents, teamSummaryLine, releaseHistoryLine, cohortVisibilityLine, carryForwardRefs] = await Promise.all([
|
|
29913
30352
|
tracked("unblock-candidates", () => findUnblockCandidates(adapter2, currentCycle))().catch(() => []),
|
|
29914
30353
|
// task-1866: discover project sub-agents for the orient surface (read-only, never throws).
|
|
29915
30354
|
listAgents(config2.projectRoot),
|
|
@@ -29918,6 +30357,8 @@ ${versionDrift}` : "";
|
|
|
29918
30357
|
// byte-identical there.
|
|
29919
30358
|
tracked("team-summary", () => computeTeamSummary(adapter2, sharedContributorsPromise))().catch(() => void 0),
|
|
29920
30359
|
tracked("release-history", () => computeReleaseHistory(adapter2, sharedContributorsPromise))().catch(() => void 0),
|
|
30360
|
+
// task-3339 (C363): owner-only readout of how much of the board the cohort cannot see.
|
|
30361
|
+
tracked("cohort-visibility", () => computeCohortVisibility(adapter2, config2, sharedContributorsPromise))().catch(() => void 0),
|
|
29921
30362
|
// task-2751 (C332): resolve every task-NNNN mentioned in the Carry-Forward
|
|
29922
30363
|
// prose to its title so orient can name it inline. Built from the board
|
|
29923
30364
|
// already in hand — no extra query.
|
|
@@ -29927,7 +30368,7 @@ ${versionDrift}` : "";
|
|
|
29927
30368
|
const unblockNote = unblockSection ? `
|
|
29928
30369
|
|
|
29929
30370
|
${unblockSection}` : "";
|
|
29930
|
-
const teamSummary = [teamSummaryLine, releaseHistoryLine].filter(Boolean).join("\n") || void 0;
|
|
30371
|
+
const teamSummary = [teamSummaryLine, releaseHistoryLine, cohortVisibilityLine].filter(Boolean).join("\n") || void 0;
|
|
29931
30372
|
let deferredGateNote = "";
|
|
29932
30373
|
if (deepHousekeeping) {
|
|
29933
30374
|
try {
|
|
@@ -31059,15 +31500,36 @@ function readLlmResponse(args) {
|
|
|
31059
31500
|
}
|
|
31060
31501
|
|
|
31061
31502
|
// src/tools/ad-view.ts
|
|
31062
|
-
|
|
31063
|
-
|
|
31064
|
-
|
|
31065
|
-
|
|
31066
|
-
|
|
31067
|
-
|
|
31068
|
-
|
|
31069
|
-
|
|
31070
|
-
|
|
31503
|
+
function adColumns(all) {
|
|
31504
|
+
const titles = titleMap(all);
|
|
31505
|
+
return [
|
|
31506
|
+
{ name: "id", header: "AD", value: (d) => d.id },
|
|
31507
|
+
{ name: "title", header: "Title", value: (d) => d.title },
|
|
31508
|
+
{ name: "confidence", header: "Confidence", value: (d) => d.confidence },
|
|
31509
|
+
{ name: "superseded", header: "Superseded", value: (d) => d.superseded ? "yes" : "no" },
|
|
31510
|
+
{
|
|
31511
|
+
name: "supersededBy",
|
|
31512
|
+
header: "Superseded By",
|
|
31513
|
+
value: (d) => d.supersededBy ? formatRef(d.supersededBy, lookupTitle(d.supersededBy, titles)) : "-"
|
|
31514
|
+
},
|
|
31515
|
+
{ name: "body", header: "Body", value: (d) => d.body.replace(/\s+/g, " ").trim() }
|
|
31516
|
+
];
|
|
31517
|
+
}
|
|
31518
|
+
var AD_FIELDS = adColumns([]).map((c) => c.name);
|
|
31519
|
+
function supersededNote(d, all) {
|
|
31520
|
+
if (!d.superseded) return "";
|
|
31521
|
+
const by = d.supersededBy;
|
|
31522
|
+
if (!by) return " [SUPERSEDED by unknown]";
|
|
31523
|
+
return ` [SUPERSEDED by ${formatRef(by, lookupTitle(by, titleMap(all)))}]`;
|
|
31524
|
+
}
|
|
31525
|
+
function formatAdBody(d, all) {
|
|
31526
|
+
return `## ${d.id}: ${d.title} [${d.confidence}]${supersededNote(d, all)}
|
|
31527
|
+
|
|
31528
|
+
${d.body}`;
|
|
31529
|
+
}
|
|
31530
|
+
function formatAdList(decisions, all) {
|
|
31531
|
+
return decisions.map((d) => formatAdBody(d, all)).join("\n\n---\n\n");
|
|
31532
|
+
}
|
|
31071
31533
|
var adViewTool = {
|
|
31072
31534
|
name: "ad_view",
|
|
31073
31535
|
description: "View one or all Active Decisions with full bodies. Use when you need to read the complete reasoning and evidence behind a specific AD before running strategy_change.",
|
|
@@ -31130,16 +31592,11 @@ async function handleAdView(adapter2, args) {
|
|
|
31130
31592
|
}
|
|
31131
31593
|
const meta2 = emitMeta ? { total: decisions.length, filtered: 1, returned: 1, limit: null, offset: 0 } : null;
|
|
31132
31594
|
if (fields) {
|
|
31133
|
-
const columns = selectColumns(
|
|
31595
|
+
const columns = selectColumns(adColumns(decisions), fields);
|
|
31134
31596
|
const body = columns.map((c) => `- **${c.header}:** ${c.value(target)}`).join("\n");
|
|
31135
31597
|
return textResponse(withMeta(body, meta2));
|
|
31136
31598
|
}
|
|
31137
|
-
|
|
31138
|
-
return textResponse(
|
|
31139
|
-
withMeta(`## ${target.id}: ${target.title} [${target.confidence}]${supersededNote}
|
|
31140
|
-
|
|
31141
|
-
${target.body}`, meta2)
|
|
31142
|
-
);
|
|
31599
|
+
return textResponse(withMeta(formatAdBody(target, decisions), meta2));
|
|
31143
31600
|
}
|
|
31144
31601
|
const filtered = includeSuperseded ? decisions : decisions.filter((d) => !d.superseded);
|
|
31145
31602
|
const paged = limit === null && offset === 0 ? filtered : filtered.slice(offset, limit === null ? void 0 : offset + limit);
|
|
@@ -31148,16 +31605,11 @@ ${target.body}`, meta2)
|
|
|
31148
31605
|
return textResponse(withMeta("No active decisions found.", meta));
|
|
31149
31606
|
}
|
|
31150
31607
|
if (fields) {
|
|
31151
|
-
const columns = selectColumns(
|
|
31608
|
+
const columns = selectColumns(adColumns(decisions), fields);
|
|
31152
31609
|
const rows = paged.map((d) => columns.map((c) => c.value(d)));
|
|
31153
31610
|
return textResponse(withMeta(renderTable(columns.map((c) => c.header), rows), meta));
|
|
31154
31611
|
}
|
|
31155
|
-
const formatted = paged
|
|
31156
|
-
const supersededNote = d.superseded ? ` [SUPERSEDED by ${d.supersededBy}]` : "";
|
|
31157
|
-
return `## ${d.id}: ${d.title} [${d.confidence}]${supersededNote}
|
|
31158
|
-
|
|
31159
|
-
${d.body}`;
|
|
31160
|
-
}).join("\n\n---\n\n");
|
|
31612
|
+
const formatted = formatAdList(paged, decisions);
|
|
31161
31613
|
return textResponse(withMeta(`# Active Decisions (${paged.length})
|
|
31162
31614
|
|
|
31163
31615
|
${formatted}`, meta));
|
|
@@ -31478,14 +31930,24 @@ On hosted/remote sessions this is now your DEFAULT project \u2014 calls without
|
|
|
31478
31930
|
}
|
|
31479
31931
|
|
|
31480
31932
|
// src/tools/contributor.ts
|
|
31933
|
+
init_dist();
|
|
31481
31934
|
var contributorAddTool = {
|
|
31482
31935
|
name: "contributor_add",
|
|
31483
|
-
description:
|
|
31936
|
+
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", cannot release. Calling it again with a different role promotes or demotes that member.`,
|
|
31484
31937
|
annotations: { title: "Add Contributor", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
31485
31938
|
inputSchema: {
|
|
31486
31939
|
type: "object",
|
|
31487
31940
|
properties: {
|
|
31488
|
-
email: { type: "string", description: 'Email of the PAPI account to add (e.g. "wes@example.com").' }
|
|
31941
|
+
email: { type: "string", description: 'Email of the PAPI account to add (e.g. "wes@example.com").' },
|
|
31942
|
+
role: {
|
|
31943
|
+
type: "string",
|
|
31944
|
+
enum: [...ASSIGNABLE_CONTRIBUTOR_ROLES],
|
|
31945
|
+
description: `Membership role. "editor" can run the full build/review/release cycle; "viewer" has read-only visibility and is REFUSED by the release gate. 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.`
|
|
31946
|
+
},
|
|
31947
|
+
project: {
|
|
31948
|
+
type: "string",
|
|
31949
|
+
description: "Project id (UUID) or slug to add this contributor to, overriding the session project for THIS call only. Must be a project on your account \u2014 fails closed otherwise. Does NOT change your default project."
|
|
31950
|
+
}
|
|
31489
31951
|
},
|
|
31490
31952
|
required: ["email"]
|
|
31491
31953
|
}
|
|
@@ -31550,21 +32012,37 @@ function requireEmail(args) {
|
|
|
31550
32012
|
return EMAIL_SHAPE.test(email) ? email : null;
|
|
31551
32013
|
}
|
|
31552
32014
|
async function handleContributorAdd(adapter2, config2, args) {
|
|
31553
|
-
const denied = await denyUnlessOwner(adapter2, config2);
|
|
31554
|
-
if (denied) return errorResponse(denied);
|
|
31555
32015
|
const email = requireEmail(args);
|
|
31556
32016
|
if (!email) return errorResponse('A valid email is required. Example: contributor_add email="wes@example.com"');
|
|
32017
|
+
const rawRole = typeof args.role === "string" ? args.role.trim().toLowerCase() : void 0;
|
|
32018
|
+
if (rawRole !== void 0 && !ASSIGNABLE_CONTRIBUTOR_ROLES.includes(rawRole)) {
|
|
32019
|
+
return errorResponse(
|
|
32020
|
+
`Invalid role "${args.role}". Assignable roles: ${ASSIGNABLE_CONTRIBUTOR_ROLES.join(", ")}. Project ownership cannot be granted through contributor_add.`
|
|
32021
|
+
);
|
|
32022
|
+
}
|
|
32023
|
+
let target = adapter2;
|
|
32024
|
+
let overrideNote = "";
|
|
32025
|
+
try {
|
|
32026
|
+
({ adapter: target, overrideNote } = await resolvePerCallProjectAdapter(adapter2, args));
|
|
32027
|
+
} catch (err) {
|
|
32028
|
+
if (err instanceof ProjectResolutionError) return errorResponse(err.message);
|
|
32029
|
+
throw err;
|
|
32030
|
+
}
|
|
32031
|
+
const denied = await denyUnlessOwner(target, config2);
|
|
32032
|
+
if (denied) return errorResponse(denied);
|
|
31557
32033
|
try {
|
|
31558
|
-
const entry = await
|
|
32034
|
+
const entry = await target.addContributorByEmail(email, rawRole);
|
|
31559
32035
|
const name = entry.displayName ? ` (${entry.displayName})` : "";
|
|
31560
|
-
const upsell = await resolveContributorUpsell(
|
|
32036
|
+
const upsell = await resolveContributorUpsell(target);
|
|
31561
32037
|
const upsellSuffix = upsell ? `
|
|
31562
32038
|
|
|
31563
32039
|
${upsell}` : "";
|
|
32040
|
+
const where = overrideNote ? ` ${overrideNote}` : " on this project";
|
|
32041
|
+
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 are a working teammate rather than an observer.`;
|
|
31564
32042
|
return textResponse(
|
|
31565
|
-
`\u2705
|
|
32043
|
+
`\u2705 **${entry.email ?? email}**${name} is now a **${entry.role}**${where}.
|
|
31566
32044
|
|
|
31567
|
-
|
|
32045
|
+
` + roleLine + upsellSuffix
|
|
31568
32046
|
);
|
|
31569
32047
|
} catch (err) {
|
|
31570
32048
|
return errorResponse(err instanceof Error ? err.message : String(err));
|
|
@@ -32069,12 +32547,13 @@ async function readPapiResource(adapter2, uri) {
|
|
|
32069
32547
|
const ads = await adapter2.getActiveDecisions({ includeRetired: true });
|
|
32070
32548
|
const target = ads.find((d) => d.id === adId);
|
|
32071
32549
|
if (!target) throw new Error(`Active Decision not found in this project: ${adId}`);
|
|
32072
|
-
const
|
|
32550
|
+
const titles = titleMap(ads);
|
|
32551
|
+
const supersededNote2 = target.superseded ? ` [SUPERSEDED by ${target.supersededBy ? formatRef(target.supersededBy, lookupTitle(target.supersededBy, titles)) : "unknown"}]` : "";
|
|
32073
32552
|
const supersedes = ads.filter((d) => d.supersededBy === target.id).map((d) => d.id);
|
|
32074
32553
|
const chain = supersedes.length > 0 ? `
|
|
32075
32554
|
|
|
32076
|
-
_Supersedes: ${supersedes
|
|
32077
|
-
return md(`## ${target.id}: ${target.title} [${target.confidence}]${
|
|
32555
|
+
_Supersedes: ${formatRefList(supersedes, titles)}_` : "";
|
|
32556
|
+
return md(`## ${target.id}: ${target.title} [${target.confidence}]${supersededNote2}
|
|
32078
32557
|
|
|
32079
32558
|
${target.body}${chain}`);
|
|
32080
32559
|
}
|
|
@@ -32302,6 +32781,7 @@ var PAPI_TOOLS = [
|
|
|
32302
32781
|
boardArchiveTool,
|
|
32303
32782
|
boardEditTool,
|
|
32304
32783
|
setupTool,
|
|
32784
|
+
joinTool,
|
|
32305
32785
|
buildListTool,
|
|
32306
32786
|
buildDescribeTool,
|
|
32307
32787
|
buildExecuteTool,
|
|
@@ -32467,6 +32947,8 @@ function createServer(adapter2, config2, observation) {
|
|
|
32467
32947
|
return handleBoardEdit(adapter2, safeArgs);
|
|
32468
32948
|
case "setup":
|
|
32469
32949
|
return handleSetup(adapter2, config2, safeArgs, server2.getClientVersion()?.name);
|
|
32950
|
+
case "join":
|
|
32951
|
+
return handleJoin(adapter2, config2, safeArgs);
|
|
32470
32952
|
case "build_list":
|
|
32471
32953
|
return handleBuildList(adapter2, config2, safeArgs);
|
|
32472
32954
|
case "build_describe":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@papi-ai/server",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.84",
|
|
4
4
|
"description": "PAPI MCP server — AI-powered sprint planning, build execution, and strategy review for software projects",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"mcpName": "io.github.getpapi/papi",
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@anthropic-ai/sdk": "^0.82.0",
|
|
53
53
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
54
|
-
"@papi-ai/adapter-pg": "^0.2.
|
|
55
|
-
"@papi-ai/shared": "^0.2.
|
|
54
|
+
"@papi-ai/adapter-pg": "^0.2.15",
|
|
55
|
+
"@papi-ai/shared": "^0.2.4",
|
|
56
56
|
"@papi-ai/skills": "^0.1.0",
|
|
57
57
|
"js-yaml": "^4.1.0"
|
|
58
58
|
},
|