@papi-ai/server 0.7.51 → 0.7.53
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 +26 -10
- package/dist/index.js +421 -119
- package/package.json +1 -1
|
@@ -25,6 +25,7 @@ __export(git_exports, {
|
|
|
25
25
|
detectUnrecordedCommits: () => detectUnrecordedCommits,
|
|
26
26
|
ensureLatestDevelop: () => ensureLatestDevelop,
|
|
27
27
|
ensureTagAtHead: () => ensureTagAtHead,
|
|
28
|
+
findTaskCommitsOnBase: () => findTaskCommitsOnBase,
|
|
28
29
|
getBranchDiff: () => getBranchDiff,
|
|
29
30
|
getCommitsSinceTag: () => getCommitsSinceTag,
|
|
30
31
|
getCurrentBranch: () => getCurrentBranch,
|
|
@@ -718,11 +719,29 @@ function escapeRegexLiteral(s) {
|
|
|
718
719
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
719
720
|
}
|
|
720
721
|
function detectMergedInProgress(cwd, preferredBase, tasks) {
|
|
721
|
-
if (!isGitAvailable() || !isGitRepo(cwd)) return [];
|
|
722
722
|
const inProgress = tasks.filter((t) => t.status === "In Progress");
|
|
723
723
|
if (inProgress.length === 0) return [];
|
|
724
|
+
const landed = findTaskCommitsOnBase(cwd, preferredBase, inProgress.map((t) => t.displayId));
|
|
725
|
+
const hits = [];
|
|
726
|
+
for (const task of inProgress) {
|
|
727
|
+
const hit = landed.get(task.displayId);
|
|
728
|
+
if (!hit) continue;
|
|
729
|
+
hits.push({
|
|
730
|
+
displayId: task.displayId,
|
|
731
|
+
title: task.title,
|
|
732
|
+
commit: hit.commit,
|
|
733
|
+
subject: hit.subject,
|
|
734
|
+
pr: hit.pr
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
return hits;
|
|
738
|
+
}
|
|
739
|
+
function findTaskCommitsOnBase(cwd, preferredBase, displayIds) {
|
|
740
|
+
const out = /* @__PURE__ */ new Map();
|
|
741
|
+
if (displayIds.length === 0) return out;
|
|
742
|
+
if (!isGitAvailable() || !isGitRepo(cwd)) return out;
|
|
724
743
|
const base = resolveDefaultBranch(cwd, preferredBase);
|
|
725
|
-
if (!branchExists(cwd, base)) return
|
|
744
|
+
if (!branchExists(cwd, base)) return out;
|
|
726
745
|
let raw;
|
|
727
746
|
try {
|
|
728
747
|
raw = execFileSync(
|
|
@@ -731,28 +750,25 @@ function detectMergedInProgress(cwd, preferredBase, tasks) {
|
|
|
731
750
|
{ cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
|
|
732
751
|
);
|
|
733
752
|
} catch {
|
|
734
|
-
return
|
|
753
|
+
return out;
|
|
735
754
|
}
|
|
736
755
|
const commits = raw.split("\n").map((line) => {
|
|
737
756
|
const idx = line.indexOf("");
|
|
738
757
|
if (idx === -1) return null;
|
|
739
758
|
return { hash: line.slice(0, idx).trim(), subject: line.slice(idx + 1).trim() };
|
|
740
759
|
}).filter((c) => c !== null && c.hash !== "");
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
const re = new RegExp(`(^|[^\\w-])${escapeRegexLiteral(task.displayId)}([^\\w-]|$)`);
|
|
760
|
+
for (const displayId of displayIds) {
|
|
761
|
+
const re = new RegExp(`(^|[^\\w-])${escapeRegexLiteral(displayId)}([^\\w-]|$)`);
|
|
744
762
|
const hit = commits.find((c) => re.test(c.subject));
|
|
745
763
|
if (!hit) continue;
|
|
746
764
|
const prMatch = hit.subject.match(/#(\d+)/);
|
|
747
|
-
|
|
748
|
-
displayId: task.displayId,
|
|
749
|
-
title: task.title,
|
|
765
|
+
out.set(displayId, {
|
|
750
766
|
commit: hit.hash,
|
|
751
767
|
subject: hit.subject,
|
|
752
768
|
pr: prMatch ? `#${prMatch[1]}` : null
|
|
753
769
|
});
|
|
754
770
|
}
|
|
755
|
-
return
|
|
771
|
+
return out;
|
|
756
772
|
}
|
|
757
773
|
function detectUnrecordedCommits(cwd, baseBranch) {
|
|
758
774
|
if (!isGitAvailable() || !isGitRepo(cwd)) return [];
|
package/dist/index.js
CHANGED
|
@@ -26,6 +26,7 @@ __export(git_exports, {
|
|
|
26
26
|
detectUnrecordedCommits: () => detectUnrecordedCommits,
|
|
27
27
|
ensureLatestDevelop: () => ensureLatestDevelop,
|
|
28
28
|
ensureTagAtHead: () => ensureTagAtHead,
|
|
29
|
+
findTaskCommitsOnBase: () => findTaskCommitsOnBase,
|
|
29
30
|
getBranchDiff: () => getBranchDiff,
|
|
30
31
|
getCommitsSinceTag: () => getCommitsSinceTag,
|
|
31
32
|
getCurrentBranch: () => getCurrentBranch,
|
|
@@ -719,11 +720,29 @@ function escapeRegexLiteral(s) {
|
|
|
719
720
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
720
721
|
}
|
|
721
722
|
function detectMergedInProgress(cwd, preferredBase, tasks) {
|
|
722
|
-
if (!isGitAvailable() || !isGitRepo(cwd)) return [];
|
|
723
723
|
const inProgress = tasks.filter((t) => t.status === "In Progress");
|
|
724
724
|
if (inProgress.length === 0) return [];
|
|
725
|
+
const landed = findTaskCommitsOnBase(cwd, preferredBase, inProgress.map((t) => t.displayId));
|
|
726
|
+
const hits = [];
|
|
727
|
+
for (const task of inProgress) {
|
|
728
|
+
const hit = landed.get(task.displayId);
|
|
729
|
+
if (!hit) continue;
|
|
730
|
+
hits.push({
|
|
731
|
+
displayId: task.displayId,
|
|
732
|
+
title: task.title,
|
|
733
|
+
commit: hit.commit,
|
|
734
|
+
subject: hit.subject,
|
|
735
|
+
pr: hit.pr
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
return hits;
|
|
739
|
+
}
|
|
740
|
+
function findTaskCommitsOnBase(cwd, preferredBase, displayIds) {
|
|
741
|
+
const out = /* @__PURE__ */ new Map();
|
|
742
|
+
if (displayIds.length === 0) return out;
|
|
743
|
+
if (!isGitAvailable() || !isGitRepo(cwd)) return out;
|
|
725
744
|
const base = resolveDefaultBranch(cwd, preferredBase);
|
|
726
|
-
if (!branchExists(cwd, base)) return
|
|
745
|
+
if (!branchExists(cwd, base)) return out;
|
|
727
746
|
let raw;
|
|
728
747
|
try {
|
|
729
748
|
raw = execFileSync(
|
|
@@ -732,28 +751,25 @@ function detectMergedInProgress(cwd, preferredBase, tasks) {
|
|
|
732
751
|
{ cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
|
|
733
752
|
);
|
|
734
753
|
} catch {
|
|
735
|
-
return
|
|
754
|
+
return out;
|
|
736
755
|
}
|
|
737
756
|
const commits = raw.split("\n").map((line) => {
|
|
738
757
|
const idx = line.indexOf("");
|
|
739
758
|
if (idx === -1) return null;
|
|
740
759
|
return { hash: line.slice(0, idx).trim(), subject: line.slice(idx + 1).trim() };
|
|
741
760
|
}).filter((c) => c !== null && c.hash !== "");
|
|
742
|
-
const
|
|
743
|
-
|
|
744
|
-
const re = new RegExp(`(^|[^\\w-])${escapeRegexLiteral(task.displayId)}([^\\w-]|$)`);
|
|
761
|
+
for (const displayId of displayIds) {
|
|
762
|
+
const re = new RegExp(`(^|[^\\w-])${escapeRegexLiteral(displayId)}([^\\w-]|$)`);
|
|
745
763
|
const hit = commits.find((c) => re.test(c.subject));
|
|
746
764
|
if (!hit) continue;
|
|
747
765
|
const prMatch = hit.subject.match(/#(\d+)/);
|
|
748
|
-
|
|
749
|
-
displayId: task.displayId,
|
|
750
|
-
title: task.title,
|
|
766
|
+
out.set(displayId, {
|
|
751
767
|
commit: hit.hash,
|
|
752
768
|
subject: hit.subject,
|
|
753
769
|
pr: prMatch ? `#${prMatch[1]}` : null
|
|
754
770
|
});
|
|
755
771
|
}
|
|
756
|
-
return
|
|
772
|
+
return out;
|
|
757
773
|
}
|
|
758
774
|
function detectUnrecordedCommits(cwd, baseBranch) {
|
|
759
775
|
if (!isGitAvailable() || !isGitRepo(cwd)) return [];
|
|
@@ -13096,7 +13112,7 @@ var planPrepareCache = new PerCallerCache();
|
|
|
13096
13112
|
var planTool = {
|
|
13097
13113
|
name: "plan",
|
|
13098
13114
|
description: 'Run once per cycle to select tasks and generate BUILD HANDOFFs. Call after setup (first time) or after completing all builds AND running release for the previous cycle. Returns prioritised task recommendations with detailed implementation specs. NEVER call when unbuilt cycle tasks exist \u2014 build and release first. First call returns a planning prompt for you to execute (prepare phase). Then call again with mode "apply" and your output to write results. Use skip_handoffs=true for large backlogs \u2014 handoffs are then generated separately via `handoff_generate`.',
|
|
13099
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
13115
|
+
annotations: { title: "Plan Cycle", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
13100
13116
|
inputSchema: {
|
|
13101
13117
|
type: "object",
|
|
13102
13118
|
properties: {
|
|
@@ -13641,15 +13657,34 @@ async function findSimilarTasks(adapter2, ideaTitle) {
|
|
|
13641
13657
|
return matches.sort((a, b2) => b2.coverage - a.coverage).slice(0, 3);
|
|
13642
13658
|
}
|
|
13643
13659
|
var AD_ALIGNMENT_OVERLAP_THRESHOLD = 3;
|
|
13660
|
+
var DOC_FREQ_CEILING = 0.35;
|
|
13661
|
+
function findUbiquitousWords(ads) {
|
|
13662
|
+
const live = ads.filter((a) => !a.superseded);
|
|
13663
|
+
const ubiquitous = /* @__PURE__ */ new Set();
|
|
13664
|
+
if (live.length < 5) return ubiquitous;
|
|
13665
|
+
const docFreq = /* @__PURE__ */ new Map();
|
|
13666
|
+
for (const ad of live) {
|
|
13667
|
+
for (const w of extractKeywords(`${ad.title} ${ad.body}`)) {
|
|
13668
|
+
docFreq.set(w, (docFreq.get(w) ?? 0) + 1);
|
|
13669
|
+
}
|
|
13670
|
+
}
|
|
13671
|
+
const ceiling = live.length * DOC_FREQ_CEILING;
|
|
13672
|
+
for (const [w, df] of docFreq) {
|
|
13673
|
+
if (df > ceiling) ubiquitous.add(w);
|
|
13674
|
+
}
|
|
13675
|
+
return ubiquitous;
|
|
13676
|
+
}
|
|
13644
13677
|
function findAdAlignmentMatches(ideaText, ads) {
|
|
13645
13678
|
const ideaKeywords = extractKeywords(ideaText);
|
|
13646
13679
|
if (ideaKeywords.size < AD_ALIGNMENT_OVERLAP_THRESHOLD) return [];
|
|
13680
|
+
const ubiquitous = findUbiquitousWords(ads);
|
|
13647
13681
|
const matches = [];
|
|
13648
13682
|
for (const ad of ads) {
|
|
13649
13683
|
if (ad.superseded) continue;
|
|
13650
13684
|
const adKeywords = extractKeywords(`${ad.title} ${ad.body}`);
|
|
13651
13685
|
let overlap = 0;
|
|
13652
13686
|
for (const w of ideaKeywords) {
|
|
13687
|
+
if (ubiquitous.has(w)) continue;
|
|
13653
13688
|
if (adKeywords.has(w)) overlap++;
|
|
13654
13689
|
}
|
|
13655
13690
|
if (overlap >= AD_ALIGNMENT_OVERLAP_THRESHOLD) {
|
|
@@ -15494,7 +15529,7 @@ var reviewPrepareCache = new PerCallerCache();
|
|
|
15494
15529
|
var strategyReviewTool = {
|
|
15495
15530
|
name: "strategy_review",
|
|
15496
15531
|
description: 'Run a Strategy Review \u2014 assesses project direction, velocity, and Active Decisions. Produces recommendations and potential AD updates that feed into the next plan. Offered every 5 cycles; hard-blocked at 7+ overdue cycles. Run it in your current conversation \u2014 only start a fresh one if you are genuinely under context pressure (your host just compacted, you are near the context limit, or the session is heavy with build context), not just because a review is next. First call returns a review prompt for you to execute (prepare phase). Then call again with mode "apply" and your output. Pass `force: true` to run before the cadence gate.',
|
|
15497
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
15532
|
+
annotations: { title: "Run Strategy Review", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
15498
15533
|
inputSchema: {
|
|
15499
15534
|
type: "object",
|
|
15500
15535
|
properties: {
|
|
@@ -15531,7 +15566,7 @@ var strategyReviewTool = {
|
|
|
15531
15566
|
var strategyAgendaTool = {
|
|
15532
15567
|
name: "strategy_agenda",
|
|
15533
15568
|
description: 'Queue topics for the next strategy review. Topics surface as input in the next `strategy_review` prepare phase and are automatically marked as addressed after the review completes. Two modes: "add" to queue a topic, "list" to see pending topics. Use this when you spot a strategic question during a build \u2014 capture the topic now instead of losing it.',
|
|
15534
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
15569
|
+
annotations: { title: "Strategy Agenda", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
15535
15570
|
inputSchema: {
|
|
15536
15571
|
type: "object",
|
|
15537
15572
|
properties: {
|
|
@@ -15559,7 +15594,7 @@ var strategyAgendaTool = {
|
|
|
15559
15594
|
var strategyChangeTool = {
|
|
15560
15595
|
name: "strategy_change",
|
|
15561
15596
|
description: 'Apply a strategic shift to the project. Three modes: "capture" for lightweight mid-conversation decision capture (no LLM round-trip), "prepare" to get a change prompt for full analysis, "apply" to persist analysis output. Use "capture" when you detect a strategic decision in conversation and want to persist it quickly without disrupting the build flow. In "capture" mode, pass north_star to directly set/update the project North Star (no decision text needed).',
|
|
15562
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
15597
|
+
annotations: { title: "Change Strategy", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
15563
15598
|
inputSchema: {
|
|
15564
15599
|
type: "object",
|
|
15565
15600
|
properties: {
|
|
@@ -16018,7 +16053,7 @@ async function archiveTasks(adapter2, phases, statuses) {
|
|
|
16018
16053
|
var boardViewTool = {
|
|
16019
16054
|
name: "board_view",
|
|
16020
16055
|
description: 'View the Board. To find a SPECIFIC task or subset, FILTER FIRST \u2014 do not dump the whole board: pass task_id for one task (full detail), query="<text>" for a title/notes substring match, or cycle=<n> for one cycle. Combine with status/phase. By default shows active tasks only (excludes Done/Cancelled), sorted by priority, limited to 50; titles are truncated in the table (single-task lookup shows the full title). Use status="all" to see everything, mode="summary" for counts only. Does not call the Anthropic API.',
|
|
16021
|
-
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
16056
|
+
annotations: { title: "View Board", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
16022
16057
|
inputSchema: {
|
|
16023
16058
|
type: "object",
|
|
16024
16059
|
properties: {
|
|
@@ -16062,7 +16097,7 @@ var boardViewTool = {
|
|
|
16062
16097
|
var boardDeprioritiseTool = {
|
|
16063
16098
|
name: "board_deprioritise",
|
|
16064
16099
|
description: `Remove a task from the current cycle. Four actions: "backlog" (not now, maybe later \u2014 preserves handoff), "defer" (valid but premature \u2014 hidden from planner), "block" (waiting on external dependency \u2014 visible on board but skipped by planner), "cancel" (don't want this \u2014 permanently closed with reason). When a user rejects a task, ALWAYS ask which action they want. Does not call the Anthropic API.`,
|
|
16065
|
-
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
16100
|
+
annotations: { title: "Deprioritise Task", readOnlyHint: false, destructiveHint: true, openWorldHint: false },
|
|
16066
16101
|
inputSchema: {
|
|
16067
16102
|
type: "object",
|
|
16068
16103
|
properties: {
|
|
@@ -16107,7 +16142,7 @@ var boardDeprioritiseTool = {
|
|
|
16107
16142
|
var boardArchiveTool = {
|
|
16108
16143
|
name: "board_archive",
|
|
16109
16144
|
description: "Archive tasks from the Board to the archive file. When both phase and status are provided, only tasks matching BOTH are archived (AND logic). When only one is provided, all matching tasks are archived. Does not call the Anthropic API.",
|
|
16110
|
-
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
16145
|
+
annotations: { title: "Archive Task", readOnlyHint: false, destructiveHint: true, openWorldHint: false },
|
|
16111
16146
|
inputSchema: {
|
|
16112
16147
|
type: "object",
|
|
16113
16148
|
properties: {
|
|
@@ -16126,7 +16161,7 @@ var boardArchiveTool = {
|
|
|
16126
16161
|
var boardEditTool = {
|
|
16127
16162
|
name: "board_edit",
|
|
16128
16163
|
description: "Edit fields on an existing task. Supports title, priority, complexity, module, epic, phase, notes (with notes_mode for append/replace/clear), status, maturity, and cycle (number or null). Pass task_id plus any fields to update. Does not call the Anthropic API.",
|
|
16129
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
16164
|
+
annotations: { title: "Edit Task", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
16130
16165
|
inputSchema: {
|
|
16131
16166
|
type: "object",
|
|
16132
16167
|
properties: {
|
|
@@ -18069,7 +18104,7 @@ async function ensureMcpJsonGitignored(projectRoot) {
|
|
|
18069
18104
|
var setupTool = {
|
|
18070
18105
|
name: "setup",
|
|
18071
18106
|
description: 'Create a new PAPI-tracked project \u2014 generates your Product Brief, Active Decisions, and CLAUDE.md workflow instructions. Run after configuring your MCP credentials (via `init` or manually from getpapi.ai). Only project_name is required \u2014 description and target_users are derived from README, package.json, and commit history when omitted. Set existing_project: true to adopt an existing codebase. ADOPTING AN EXISTING PROJECT OVER A REMOTE/HOSTED CONNECTOR (no local stdio install): PAPI cannot read your filesystem, so YOU (the client) must gather a `codebase_scan` and pass it in \u2014 list top-level dirs/files, the package manifest, the README (first ~3000 chars), and recent commit subjects. Without it, adoption falls back to asking for description/target_users. On a local stdio install PAPI scans the tree itself, so `codebase_scan` is optional there. First call returns prompts (prepare phase), then call again with mode "apply" and your outputs. After setup, run `plan` to start your first cycle.',
|
|
18072
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
18107
|
+
annotations: { title: "Set Up Project", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
18073
18108
|
inputSchema: {
|
|
18074
18109
|
type: "object",
|
|
18075
18110
|
properties: {
|
|
@@ -19068,15 +19103,35 @@ async function createRelease(config2, branch, version, adapter2, cycleNum, optio
|
|
|
19068
19103
|
if (adapter2 && resolvedCycleNum > 0) {
|
|
19069
19104
|
try {
|
|
19070
19105
|
const stampedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
19106
|
+
const resolvedBase = resolveBaseBranch(config2.projectRoot, branch);
|
|
19071
19107
|
const doneCycleTasks = (await adapter2.queryBoard()).filter(
|
|
19072
19108
|
(t) => t.cycle === resolvedCycleNum && t.status === "Done"
|
|
19073
19109
|
);
|
|
19110
|
+
const canVerify = isGitAvailable() && isGitRepo(config2.projectRoot);
|
|
19111
|
+
const landed = canVerify ? findTaskCommitsOnBase(
|
|
19112
|
+
config2.projectRoot,
|
|
19113
|
+
resolvedBase,
|
|
19114
|
+
doneCycleTasks.map((t) => t.displayId)
|
|
19115
|
+
) : /* @__PURE__ */ new Map();
|
|
19116
|
+
const unverified = [];
|
|
19074
19117
|
for (const t of doneCycleTasks) {
|
|
19118
|
+
const hit = landed.get(t.displayId);
|
|
19119
|
+
if (canVerify && !hit) {
|
|
19120
|
+
unverified.push(
|
|
19121
|
+
`${t.displayId}${t.branchName ? ` (branch \`${t.branchName}\`)` : " (no branch recorded)"}`
|
|
19122
|
+
);
|
|
19123
|
+
continue;
|
|
19124
|
+
}
|
|
19075
19125
|
try {
|
|
19076
19126
|
await adapter2.updateTask(t.id, { mergedAt: stampedAt });
|
|
19077
19127
|
} catch {
|
|
19078
19128
|
}
|
|
19079
19129
|
}
|
|
19130
|
+
if (unverified.length > 0) {
|
|
19131
|
+
warnings.push(
|
|
19132
|
+
`\u26A0\uFE0F DATA-INTEGRITY \u2014 ${unverified.length} task(s) are marked **Done** in cycle ${resolvedCycleNum} but their work is NOT on ${resolvedBase}: ${unverified.join("; ")}. They were NOT stamped as merged. Held ad-hoc work is the usual cause (\`ad_hoc\` + \`board_edit\` leaves the branch unmerged by design). Merge the branch(es), or move the task(s) to the next cycle \u2014 do not leave Done unbacked by git.`
|
|
19133
|
+
);
|
|
19134
|
+
}
|
|
19080
19135
|
} catch {
|
|
19081
19136
|
}
|
|
19082
19137
|
}
|
|
@@ -19136,6 +19191,44 @@ async function createRelease(config2, branch, version, adapter2, cycleNum, optio
|
|
|
19136
19191
|
};
|
|
19137
19192
|
}
|
|
19138
19193
|
|
|
19194
|
+
// src/services/release-bookkeeping.ts
|
|
19195
|
+
async function beginRelease(tracker, cycleNum) {
|
|
19196
|
+
tracker.setStreamScope({ cycle: cycleNum ?? null });
|
|
19197
|
+
await tracker.recordStep("release_starting");
|
|
19198
|
+
}
|
|
19199
|
+
async function recordReadinessVerified(tracker) {
|
|
19200
|
+
await tracker.recordStep("readiness_verified");
|
|
19201
|
+
}
|
|
19202
|
+
async function recordQualityGate(tracker, decision, caps) {
|
|
19203
|
+
await tracker.recordStep("quality_gate", {
|
|
19204
|
+
status: decision.stepStatus,
|
|
19205
|
+
capabilityKey: "releaseGate",
|
|
19206
|
+
capabilityEnabled: isCapabilityEnabled(caps, "releaseGate"),
|
|
19207
|
+
metadata: decision.metadata
|
|
19208
|
+
});
|
|
19209
|
+
}
|
|
19210
|
+
async function completeRelease(tracker, opts) {
|
|
19211
|
+
tracker.setStreamScope({ cycle: opts.cycleClosed ?? null });
|
|
19212
|
+
await tracker.recordStep("cycle_complete", { metadata: { version: opts.version } });
|
|
19213
|
+
for (const m of opts.branchMerges ?? []) {
|
|
19214
|
+
await tracker.recordStep("branch_merged", {
|
|
19215
|
+
metadata: { branch: m.branch, prUrl: m.prUrl ?? null }
|
|
19216
|
+
});
|
|
19217
|
+
}
|
|
19218
|
+
await tracker.recordStep("changelog", {
|
|
19219
|
+
capabilityKey: "changelog",
|
|
19220
|
+
capabilityEnabled: isCapabilityEnabled(opts.caps, "changelog"),
|
|
19221
|
+
status: opts.changelogEmitted ? "complete" : "active"
|
|
19222
|
+
});
|
|
19223
|
+
if (opts.deployHookEmitted) {
|
|
19224
|
+
await tracker.recordStep("deploy_hook", {
|
|
19225
|
+
capabilityKey: "deployHook",
|
|
19226
|
+
capabilityEnabled: isCapabilityEnabled(opts.caps, "deployHook")
|
|
19227
|
+
});
|
|
19228
|
+
}
|
|
19229
|
+
await tracker.recordStep("released", { metadata: { version: opts.version } });
|
|
19230
|
+
}
|
|
19231
|
+
|
|
19139
19232
|
// src/tools/release.ts
|
|
19140
19233
|
init_git();
|
|
19141
19234
|
|
|
@@ -19354,7 +19447,7 @@ async function postReleaseToX(version, cycleClosed) {
|
|
|
19354
19447
|
var releaseTool = {
|
|
19355
19448
|
name: "release",
|
|
19356
19449
|
description: "Cut a versioned release \u2014 creates a git tag, generates CHANGELOG.md, and pushes to remote. Pass skipVersion=true to update CHANGELOG and close the cycle without creating a tag or bumping version numbers.",
|
|
19357
|
-
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
19450
|
+
annotations: { title: "Cut Release", readOnlyHint: false, destructiveHint: true, openWorldHint: false },
|
|
19358
19451
|
inputSchema: {
|
|
19359
19452
|
type: "object",
|
|
19360
19453
|
properties: {
|
|
@@ -19425,9 +19518,16 @@ async function handleRelease(adapter2, config2, args) {
|
|
|
19425
19518
|
}
|
|
19426
19519
|
const tracker = new ProgressTracker("validate-args").bindStream(adapter2, { stage: "release" });
|
|
19427
19520
|
try {
|
|
19428
|
-
await tracker.recordStep("release_starting");
|
|
19429
19521
|
tracker.mark("owner-identity-guard");
|
|
19430
19522
|
const gate = await resolveOwnerGate(adapter2, config2);
|
|
19523
|
+
let cycleToClose = null;
|
|
19524
|
+
try {
|
|
19525
|
+
const resolved = await resolveCycleToClose(adapter2, version, gate.callerUserId);
|
|
19526
|
+
cycleToClose = resolved > 0 ? resolved : null;
|
|
19527
|
+
} catch {
|
|
19528
|
+
cycleToClose = null;
|
|
19529
|
+
}
|
|
19530
|
+
await beginRelease(tracker, cycleToClose);
|
|
19431
19531
|
const productionBaseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
|
|
19432
19532
|
if (branch === productionBaseBranch) {
|
|
19433
19533
|
if (gate.enforced && !gate.callerIsOwner) {
|
|
@@ -19492,6 +19592,13 @@ Next: run \`plan\` to start your next cycle.`
|
|
|
19492
19592
|
}
|
|
19493
19593
|
}
|
|
19494
19594
|
}
|
|
19595
|
+
let caps = {};
|
|
19596
|
+
try {
|
|
19597
|
+
const info = adapter2.getProjectInfo ? await adapter2.getProjectInfo() : null;
|
|
19598
|
+
caps = info?.capabilities ?? {};
|
|
19599
|
+
} catch {
|
|
19600
|
+
caps = {};
|
|
19601
|
+
}
|
|
19495
19602
|
if (isHostedTransport()) {
|
|
19496
19603
|
tracker.mark("hosted-close-cycle");
|
|
19497
19604
|
let closed;
|
|
@@ -19504,6 +19611,16 @@ Next: run \`plan\` to start your next cycle.`
|
|
|
19504
19611
|
const message = err instanceof Error ? err.message : String(err);
|
|
19505
19612
|
return errorResponse(message);
|
|
19506
19613
|
}
|
|
19614
|
+
await recordReadinessVerified(tracker);
|
|
19615
|
+
const hostedGate = evaluateReleaseGate(caps, config2.gateCommand, gateResult);
|
|
19616
|
+
await recordQualityGate(tracker, hostedGate, caps);
|
|
19617
|
+
await completeRelease(tracker, {
|
|
19618
|
+
cycleClosed: closed.resolvedCycleNum > 0 ? closed.resolvedCycleNum : null,
|
|
19619
|
+
version,
|
|
19620
|
+
caps,
|
|
19621
|
+
branchMerges: [],
|
|
19622
|
+
changelogEmitted: false
|
|
19623
|
+
});
|
|
19507
19624
|
const tagAnnotation = `Release ${version}`;
|
|
19508
19625
|
const cyclePart = closed.resolvedCycleNum > 0 ? `Cycle ${closed.resolvedCycleNum}` : "The cycle";
|
|
19509
19626
|
const warningsBlock = closed.warnings.length > 0 ? `
|
|
@@ -19564,22 +19681,10 @@ Run \`project_switch <slug>\` to switch the active PAPI project, or verify your
|
|
|
19564
19681
|
console.error(`[release] gh CLI not available \u2014 ${pendingGrouped.length} shared branch(es) will be squash-merged via git: ${pendingGrouped.join(", ")}`);
|
|
19565
19682
|
}
|
|
19566
19683
|
}
|
|
19567
|
-
await tracker
|
|
19568
|
-
let caps = {};
|
|
19569
|
-
try {
|
|
19570
|
-
const info = adapter2.getProjectInfo ? await adapter2.getProjectInfo() : null;
|
|
19571
|
-
caps = info?.capabilities ?? {};
|
|
19572
|
-
} catch {
|
|
19573
|
-
caps = {};
|
|
19574
|
-
}
|
|
19684
|
+
await recordReadinessVerified(tracker);
|
|
19575
19685
|
tracker.mark("quality-gate");
|
|
19576
19686
|
const gateDecision = evaluateReleaseGate(caps, config2.gateCommand, gateResult);
|
|
19577
|
-
await tracker
|
|
19578
|
-
status: gateDecision.stepStatus,
|
|
19579
|
-
capabilityKey: "releaseGate",
|
|
19580
|
-
capabilityEnabled: isCapabilityEnabled(caps, "releaseGate"),
|
|
19581
|
-
metadata: gateDecision.metadata
|
|
19582
|
-
});
|
|
19687
|
+
await recordQualityGate(tracker, gateDecision, caps);
|
|
19583
19688
|
if (gateDecision.action === "directive") {
|
|
19584
19689
|
return textResponse(gateDecision.message);
|
|
19585
19690
|
}
|
|
@@ -19592,13 +19697,6 @@ Run \`project_switch <slug>\` to switch the active PAPI project, or verify your
|
|
|
19592
19697
|
skipVersion: skipVersion ?? false,
|
|
19593
19698
|
callerUserId: gate.callerUserId
|
|
19594
19699
|
});
|
|
19595
|
-
tracker.setStreamScope({ cycle: result.cycleClosed ?? null });
|
|
19596
|
-
await tracker.recordStep("cycle_complete", { metadata: { version: result.version } });
|
|
19597
|
-
for (const m of result.groupedBranchMerges ?? []) {
|
|
19598
|
-
await tracker.recordStep("branch_merged", {
|
|
19599
|
-
metadata: { branch: m.branch, prUrl: m.prUrl ?? null }
|
|
19600
|
-
});
|
|
19601
|
-
}
|
|
19602
19700
|
const lines = [
|
|
19603
19701
|
`## Release ${result.version}${skipVersion ? " (skip version)" : ""}`,
|
|
19604
19702
|
"",
|
|
@@ -19653,21 +19751,16 @@ Run \`project_switch <slug>\` to switch the active PAPI project, or verify your
|
|
|
19653
19751
|
buildCycleUpdateCurationDirective(result.version, result.cycleClosed ?? 0)
|
|
19654
19752
|
);
|
|
19655
19753
|
if (cycleUpdateDirective) lines.push(cycleUpdateDirective);
|
|
19656
|
-
await tracker.recordStep("changelog", {
|
|
19657
|
-
capabilityKey: "changelog",
|
|
19658
|
-
capabilityEnabled: isCapabilityEnabled(caps, "changelog"),
|
|
19659
|
-
status: cycleUpdateDirective ? "complete" : "active"
|
|
19660
|
-
});
|
|
19661
19754
|
const deployDirective = buildDeployHookDirective(caps, config2.deployCommand);
|
|
19662
|
-
if (deployDirective)
|
|
19663
|
-
|
|
19664
|
-
|
|
19665
|
-
|
|
19666
|
-
|
|
19667
|
-
|
|
19668
|
-
|
|
19669
|
-
|
|
19670
|
-
}
|
|
19755
|
+
if (deployDirective) lines.push(deployDirective);
|
|
19756
|
+
await completeRelease(tracker, {
|
|
19757
|
+
cycleClosed: result.cycleClosed ?? null,
|
|
19758
|
+
version: result.version,
|
|
19759
|
+
caps,
|
|
19760
|
+
branchMerges: result.groupedBranchMerges ?? [],
|
|
19761
|
+
changelogEmitted: Boolean(cycleUpdateDirective),
|
|
19762
|
+
deployHookEmitted: Boolean(deployDirective)
|
|
19763
|
+
});
|
|
19671
19764
|
const publishEnabled = isCapabilityEnabled(caps, "publishDirective");
|
|
19672
19765
|
if (publishEnabled) {
|
|
19673
19766
|
try {
|
|
@@ -19695,7 +19788,6 @@ Run \`project_switch <slug>\` to switch the active PAPI project, or verify your
|
|
|
19695
19788
|
const verifyDirective = buildVerifyHealthCheckDirective(caps);
|
|
19696
19789
|
if (verifyDirective) lines.push(verifyDirective);
|
|
19697
19790
|
lines.push("", `Next: cycle released! Run \`plan\` to start your next cycle, or \`idea "<what's next>"\` first if your backlog is thin.`);
|
|
19698
|
-
await tracker.recordStep("released", { metadata: { version: result.version } });
|
|
19699
19791
|
const filesToWriteSection = result.filesToWrite ? formatFilesToWriteSection(result.filesToWrite) : "";
|
|
19700
19792
|
return textResponse(lines.join("\n") + filesToWriteSection);
|
|
19701
19793
|
} catch (err) {
|
|
@@ -19855,32 +19947,94 @@ function capitalizeCompleted(value) {
|
|
|
19855
19947
|
};
|
|
19856
19948
|
return map[value] ?? "No";
|
|
19857
19949
|
}
|
|
19950
|
+
function stripAnnotations(text) {
|
|
19951
|
+
let out = "";
|
|
19952
|
+
let i = 0;
|
|
19953
|
+
while (i < text.length) {
|
|
19954
|
+
const ch = text[i];
|
|
19955
|
+
if (ch === ")") {
|
|
19956
|
+
i++;
|
|
19957
|
+
continue;
|
|
19958
|
+
}
|
|
19959
|
+
if (ch === "(") {
|
|
19960
|
+
let depth = 0;
|
|
19961
|
+
let j = i;
|
|
19962
|
+
for (; j < text.length; j++) {
|
|
19963
|
+
if (text[j] === "(") depth++;
|
|
19964
|
+
else if (text[j] === ")") {
|
|
19965
|
+
depth--;
|
|
19966
|
+
if (depth === 0) {
|
|
19967
|
+
j++;
|
|
19968
|
+
break;
|
|
19969
|
+
}
|
|
19970
|
+
}
|
|
19971
|
+
}
|
|
19972
|
+
if (depth !== 0) {
|
|
19973
|
+
break;
|
|
19974
|
+
}
|
|
19975
|
+
const inner = text.slice(i + 1, j - 1);
|
|
19976
|
+
const prev = i > 0 ? text[i - 1] : "/";
|
|
19977
|
+
const next = text[j] ?? "";
|
|
19978
|
+
const isRouteGroup = (prev === "/" || i === 0) && !/\s/.test(inner) && next === "/";
|
|
19979
|
+
if (isRouteGroup) {
|
|
19980
|
+
out += text.slice(i, j);
|
|
19981
|
+
i = j;
|
|
19982
|
+
continue;
|
|
19983
|
+
}
|
|
19984
|
+
i = j;
|
|
19985
|
+
continue;
|
|
19986
|
+
}
|
|
19987
|
+
out += ch;
|
|
19988
|
+
i++;
|
|
19989
|
+
}
|
|
19990
|
+
return out;
|
|
19991
|
+
}
|
|
19858
19992
|
function sanitisePredictedFiles(raw) {
|
|
19859
19993
|
const out = [];
|
|
19860
19994
|
for (const entry of raw) {
|
|
19861
19995
|
if (typeof entry !== "string") continue;
|
|
19862
|
-
|
|
19863
|
-
|
|
19864
|
-
|
|
19865
|
-
|
|
19866
|
-
|
|
19867
|
-
|
|
19868
|
-
|
|
19869
|
-
|
|
19996
|
+
let working = entry;
|
|
19997
|
+
const altMatches = working.match(/\(\s*or\s+`([^`]+)`\s*\)/gi) ?? [];
|
|
19998
|
+
for (const alt of altMatches) {
|
|
19999
|
+
const inner = alt.match(/`([^`]+)`/);
|
|
20000
|
+
if (inner) out.push(inner[1].trim());
|
|
20001
|
+
working = working.replace(alt, " ");
|
|
20002
|
+
}
|
|
20003
|
+
const cleanedEntry = stripAnnotations(working.replace(/`/g, ""));
|
|
20004
|
+
for (const segment of cleanedEntry.split(/[;,]/)) {
|
|
20005
|
+
const cleaned = segment.trim();
|
|
20006
|
+
if (!cleaned) continue;
|
|
20007
|
+
if (/\s/.test(cleaned)) continue;
|
|
20008
|
+
if (!/[A-Za-z0-9]/.test(cleaned)) continue;
|
|
20009
|
+
out.push(cleaned);
|
|
19870
20010
|
}
|
|
19871
20011
|
}
|
|
19872
20012
|
return Array.from(new Set(out));
|
|
19873
20013
|
}
|
|
20014
|
+
function globToRegExp(entry) {
|
|
20015
|
+
const source = entry.split(/(\*\*|\*)/).map((part) => {
|
|
20016
|
+
if (part === "**") return ".*";
|
|
20017
|
+
if (part === "*") return "[^/]*";
|
|
20018
|
+
return part.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
20019
|
+
}).join("");
|
|
20020
|
+
return new RegExp(`^${source}$`, "i");
|
|
20021
|
+
}
|
|
19874
20022
|
function pathBasename(p) {
|
|
19875
20023
|
const parts = p.replace(/\\/g, "/").replace(/\/+$/, "").split("/");
|
|
19876
20024
|
return parts[parts.length - 1] ?? p;
|
|
19877
20025
|
}
|
|
19878
20026
|
function isPathInPredictedScope(changedPath, predicted) {
|
|
19879
|
-
const
|
|
20027
|
+
const changed = changedPath.replace(/\\/g, "/");
|
|
20028
|
+
const changedLower = changed.toLowerCase();
|
|
19880
20029
|
const changedBase = pathBasename(changedPath);
|
|
19881
20030
|
for (const raw of predicted) {
|
|
19882
20031
|
const entry = raw.replace(/\\/g, "/").replace(/\/+$/, "").trim();
|
|
19883
20032
|
if (!entry) continue;
|
|
20033
|
+
if (entry.includes("*")) {
|
|
20034
|
+
if (globToRegExp(entry).test(changed)) return true;
|
|
20035
|
+
if (!entry.includes("/") && globToRegExp(entry).test(changedBase)) return true;
|
|
20036
|
+
continue;
|
|
20037
|
+
}
|
|
19884
20038
|
if (pathBasename(entry) === changedBase) return true;
|
|
19885
20039
|
const entryLower = entry.toLowerCase();
|
|
19886
20040
|
if (changedLower === entryLower || changedLower.startsWith(`${entryLower}/`)) {
|
|
@@ -19924,23 +20078,27 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
|
|
|
19924
20078
|
};
|
|
19925
20079
|
const cleanedPredicted = sanitisePredictedFiles(predictedFiles);
|
|
19926
20080
|
const scoped = modified.filter((p) => isPathInPredictedScope(p, cleanedPredicted));
|
|
19927
|
-
|
|
20081
|
+
const untracked = getUntrackedFiles(cwd);
|
|
20082
|
+
const scopedSet = new Set(scoped);
|
|
20083
|
+
const untrackedInScope = untracked.filter(
|
|
20084
|
+
(p) => !scopedSet.has(p) && isPathInPredictedScope(p, cleanedPredicted)
|
|
20085
|
+
);
|
|
20086
|
+
if (scoped.length === 0 && untrackedInScope.length === 0) {
|
|
19928
20087
|
const modSample = modified.slice(0, 5).join(", ");
|
|
19929
20088
|
const predSample = cleanedPredicted.slice(0, 5).join(", ");
|
|
19930
20089
|
return `Auto-commit: refused \u2014 none of the ${modified.length} modified file(s) intersect FILES LIKELY TOUCHED. Modified: ${modSample}. Expected: ${predSample}. Stage the intended files manually (\`git add <paths>\`) then re-run, or set PAPI_AUTO_COMMIT=false.`;
|
|
19931
20090
|
}
|
|
19932
|
-
const untracked = getUntrackedFiles(cwd);
|
|
19933
20091
|
const scopedDirs = [...new Set(scoped.map(dirname7).filter((d) => d.length > 0))];
|
|
19934
20092
|
const isUnderScopedDir = (p) => scopedDirs.some((d) => p === d || p.startsWith(`${d}/`) || p.startsWith(`${d}\\`));
|
|
19935
|
-
const
|
|
20093
|
+
const inScopeSet = /* @__PURE__ */ new Set([...scoped, ...untrackedInScope]);
|
|
19936
20094
|
const adjacentUntracked = untracked.filter(
|
|
19937
|
-
(p) => !
|
|
20095
|
+
(p) => !inScopeSet.has(p) && isUnderScopedDir(p)
|
|
19938
20096
|
);
|
|
19939
|
-
const toStage = [...scoped, ...adjacentUntracked];
|
|
20097
|
+
const toStage = [...scoped, ...untrackedInScope, ...adjacentUntracked];
|
|
19940
20098
|
const toStageSet = new Set(toStage);
|
|
19941
20099
|
const droppedUntracked = untracked.filter((p) => !toStageSet.has(p));
|
|
19942
20100
|
const droppedModified = modified.filter((p) => !scopedSet.has(p));
|
|
19943
|
-
let line = safeRun(() => stagePathsAndCommit(cwd, toStage, message)) + ` (scoped to ${scoped.length}/${modified.length} files via FILES LIKELY TOUCHED` + (adjacentUntracked.length > 0 ? ` + ${adjacentUntracked.length} untracked under scoped dir(s)` : "") + `).`;
|
|
20101
|
+
let line = safeRun(() => stagePathsAndCommit(cwd, toStage, message)) + ` (scoped to ${scoped.length}/${modified.length} files via FILES LIKELY TOUCHED` + (untrackedInScope.length > 0 ? ` + ${untrackedInScope.length} new file(s) named in the handoff` : "") + (adjacentUntracked.length > 0 ? ` + ${adjacentUntracked.length} untracked under scoped dir(s)` : "") + `).`;
|
|
19944
20102
|
if (droppedModified.length > 0) {
|
|
19945
20103
|
const sample = droppedModified.slice(0, 10).join(", ");
|
|
19946
20104
|
const more = droppedModified.length > 10 ? ` (+${droppedModified.length - 10} more)` : "";
|
|
@@ -20591,15 +20749,16 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
|
|
|
20591
20749
|
const adIds = report.relatedDecisions ?? [];
|
|
20592
20750
|
const extractLearning = (text, category) => {
|
|
20593
20751
|
if (!text || text === "None" || text.trim().length === 0) return;
|
|
20594
|
-
const dotIdx = text.indexOf(". ");
|
|
20595
|
-
const summary = dotIdx > 0 && dotIdx < 120 ? text.slice(0, dotIdx + 1) : text.slice(0, 120);
|
|
20596
|
-
const tags = [];
|
|
20597
|
-
if (taskModule) tags.push(taskModule.toLowerCase());
|
|
20598
20752
|
let severity;
|
|
20599
20753
|
if (category === "issue") {
|
|
20600
20754
|
const sevMatch = text.match(/^(P[0-3])[\s:]/);
|
|
20601
20755
|
if (sevMatch) severity = sevMatch[1];
|
|
20602
20756
|
}
|
|
20757
|
+
const body = severity ? text.replace(/^P[0-3][\s:]+/, "").trim() : text.trim();
|
|
20758
|
+
const dotIdx = body.indexOf(". ");
|
|
20759
|
+
const summary = dotIdx > 0 ? body.slice(0, dotIdx + 1) : body;
|
|
20760
|
+
const tags = [];
|
|
20761
|
+
if (taskModule) tags.push(taskModule.toLowerCase());
|
|
20603
20762
|
learnings.push({
|
|
20604
20763
|
taskId: task.id,
|
|
20605
20764
|
cycleNumber,
|
|
@@ -21000,7 +21159,7 @@ import { randomUUID as randomUUID12 } from "crypto";
|
|
|
21000
21159
|
var docRegisterTool = {
|
|
21001
21160
|
name: "doc_register",
|
|
21002
21161
|
description: "Register or update a document in the doc registry. Called after finalising a research/planning doc, or when build_execute detects unregistered docs. Stores metadata and structured summary \u2014 not full content. Re-registering an existing doc updates its summary, tags, actions, type, and status (upsert). Visibility and owner are not changed on re-register.",
|
|
21003
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
21162
|
+
annotations: { title: "Register Doc", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
21004
21163
|
inputSchema: {
|
|
21005
21164
|
type: "object",
|
|
21006
21165
|
properties: {
|
|
@@ -21033,7 +21192,7 @@ var docRegisterTool = {
|
|
|
21033
21192
|
var docSearchTool = {
|
|
21034
21193
|
name: "doc_search",
|
|
21035
21194
|
description: "Search the doc registry for documents by type, tags, keyword, or pending actions. Returns summaries, not full content. Use for context gathering in plan, strategy review, and idea dedup.",
|
|
21036
|
-
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
21195
|
+
annotations: { title: "Search Docs", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
21037
21196
|
inputSchema: {
|
|
21038
21197
|
type: "object",
|
|
21039
21198
|
properties: {
|
|
@@ -21051,7 +21210,7 @@ var docSearchTool = {
|
|
|
21051
21210
|
var docScanTool = {
|
|
21052
21211
|
name: "doc_scan",
|
|
21053
21212
|
description: "Scan docs/ and plans directories for unregistered .md files. Returns a list of files not yet in the doc registry. Use this to find docs that need registration.",
|
|
21054
|
-
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
21213
|
+
annotations: { title: "Scan Docs", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
21055
21214
|
inputSchema: {
|
|
21056
21215
|
type: "object",
|
|
21057
21216
|
properties: {
|
|
@@ -21298,7 +21457,7 @@ async function handleDocScan(adapter2, config2, args) {
|
|
|
21298
21457
|
var docActionPromoteTool = {
|
|
21299
21458
|
name: "doc_action_promote",
|
|
21300
21459
|
description: "Promote a single pending action from a registered doc into a Backlog task. The new task gets a `Reference:` line pointing to the source doc, and the doc action is marked resolved with `linkedTaskId` set. Use to close the research-to-action loop \u2014 turn unactioned findings into trackable cycle work. Identify the doc by `doc_path` (preferred) or `doc_id`, and the action by 0-based `action_index` (as listed in `doc_search` output).",
|
|
21301
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
21460
|
+
annotations: { title: "Promote Doc Action", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
21302
21461
|
inputSchema: {
|
|
21303
21462
|
type: "object",
|
|
21304
21463
|
properties: {
|
|
@@ -21397,7 +21556,7 @@ init_git();
|
|
|
21397
21556
|
var buildListTool = {
|
|
21398
21557
|
name: "build_list",
|
|
21399
21558
|
description: "List cycle tasks that have BUILD HANDOFFs ready for execution. Shows task ID, title, status, priority, and complexity. In Progress tasks appear first, then Backlog. Does not call the Anthropic API.",
|
|
21400
|
-
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
21559
|
+
annotations: { title: "List Builds", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
21401
21560
|
inputSchema: {
|
|
21402
21561
|
type: "object",
|
|
21403
21562
|
properties: {
|
|
@@ -21412,7 +21571,7 @@ var buildListTool = {
|
|
|
21412
21571
|
var buildDescribeTool = {
|
|
21413
21572
|
name: "build_describe",
|
|
21414
21573
|
description: "Show the full BUILD HANDOFF for a specific task, including scope, acceptance criteria, and implementation guidance. Does not call the Anthropic API.",
|
|
21415
|
-
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
21574
|
+
annotations: { title: "Describe Build", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
21416
21575
|
inputSchema: {
|
|
21417
21576
|
type: "object",
|
|
21418
21577
|
properties: {
|
|
@@ -21427,7 +21586,7 @@ var buildDescribeTool = {
|
|
|
21427
21586
|
var buildExecuteTool = {
|
|
21428
21587
|
name: "build_execute",
|
|
21429
21588
|
description: "Start or complete a build task. Call with just task_id to start (returns BUILD HANDOFF, creates feature branch, marks In Progress). After implementing the task, you MUST call build_execute again with all report fields (completed, effort, estimated_effort, surprises, discovered_issues, architecture_notes) to finish \u2014 do not wait for user confirmation between start and complete. Never call on tasks that are already In Review or Done. Does not call the Anthropic API. Set light=true to skip branch/PR creation (commits to current branch). Set PAPI_LIGHT_MODE=true in env to default all builds to light mode.",
|
|
21430
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
21589
|
+
annotations: { title: "Run Build", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
21431
21590
|
inputSchema: {
|
|
21432
21591
|
type: "object",
|
|
21433
21592
|
properties: {
|
|
@@ -21541,7 +21700,7 @@ var buildExecuteTool = {
|
|
|
21541
21700
|
var buildCancelTool = {
|
|
21542
21701
|
name: "build_cancel",
|
|
21543
21702
|
description: "Cancel a build task with a reason. Sets the task status to Cancelled and records the closure reason. Does not call the Anthropic API.",
|
|
21544
|
-
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
21703
|
+
annotations: { title: "Cancel Build", readOnlyHint: false, destructiveHint: true, openWorldHint: false },
|
|
21545
21704
|
inputSchema: {
|
|
21546
21705
|
type: "object",
|
|
21547
21706
|
properties: {
|
|
@@ -22089,7 +22248,7 @@ Reason: ${result.reason}`);
|
|
|
22089
22248
|
init_git();
|
|
22090
22249
|
var ideaTool = {
|
|
22091
22250
|
name: "idea",
|
|
22092
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
22251
|
+
annotations: { title: "Capture Idea", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
22093
22252
|
description: "Capture an idea as a Backlog task. The next plan run will triage and scope it. Use anytime to log bugs, feature requests, or improvements without interrupting the current cycle. IMPORTANT: If this idea originates from a research or planning session, you MUST include a Reference: line in notes pointing to the source doc. Without it, the planner has no context and will misinterpret the intent. Does not call the Anthropic API.",
|
|
22094
22253
|
inputSchema: {
|
|
22095
22254
|
type: "object",
|
|
@@ -22306,7 +22465,7 @@ function collectDiagnostics(config2) {
|
|
|
22306
22465
|
var bugTool = {
|
|
22307
22466
|
name: "bug",
|
|
22308
22467
|
description: `Report a bug OR submit an idea. Routing: a bug about PAPI itself (a PAPI tool/MCP error, the connector, a handoff/cycle problem) auto-submits UPSTREAM to PAPI maintainers with diagnostics \u2014 you do NOT need to set report=true for these. A bug in the user's OWN project auto-files as a Backlog task on their board. IMPORTANT: report=true is ONLY for a genuine PAPI-product defect \u2014 it is NOT the catch-all for "not about my app". A bug in the user's harness/editor/OS/git/other tooling (e.g. Claude Code, Codex, VS Code, a shell command) is NOT a PAPI bug: file it on the user's own board (report=false / default) or that tool's own tracker, never upstream. Override the routing explicitly: report=true forces upstream (PAPI-product only), report=false forces the user's own board. Set \`type\` ("bug"/"idea") and optional notify-when-fixed / contact-ok consent for upstream submissions. Does not call the Anthropic API.`,
|
|
22309
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
22468
|
+
annotations: { title: "Report Bug", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
22310
22469
|
inputSchema: {
|
|
22311
22470
|
type: "object",
|
|
22312
22471
|
properties: {
|
|
@@ -22563,7 +22722,7 @@ function inferTaskType(description) {
|
|
|
22563
22722
|
var adHocTool = {
|
|
22564
22723
|
name: "ad_hoc",
|
|
22565
22724
|
description: "Record work done outside the normal cycle. Creates a Done task with a lightweight build report, or associates work with an existing task if task_id is provided (without changing task status \u2014 use build_execute for status transitions). Use for quick fixes, bug patches, or ad-hoc changes. Does not call the Anthropic API.",
|
|
22566
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
22725
|
+
annotations: { title: "Record Ad-Hoc Work", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
22567
22726
|
inputSchema: {
|
|
22568
22727
|
type: "object",
|
|
22569
22728
|
properties: {
|
|
@@ -23053,7 +23212,7 @@ async function applyReconcile(adapter2, corrections) {
|
|
|
23053
23212
|
var boardReconcileTool = {
|
|
23054
23213
|
name: "board_reconcile",
|
|
23055
23214
|
description: 'Holistic board review \u2014 backlog + deferred tasks in one pass. Surfaces strategic context (ADs, phases, docs), grouping signals, merge candidates, priority drift, and stale tasks. "prepare" assembles context for you to analyse; "apply" commits your decisions after user confirmation. Does not call the Anthropic API.',
|
|
23056
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
23215
|
+
annotations: { title: "Reconcile Board", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
23057
23216
|
inputSchema: {
|
|
23058
23217
|
type: "object",
|
|
23059
23218
|
properties: {
|
|
@@ -23546,7 +23705,7 @@ ${diffBlock}`;
|
|
|
23546
23705
|
var reviewListTool = {
|
|
23547
23706
|
name: "review_list",
|
|
23548
23707
|
description: "List tasks ready for your sign-off \u2014 shows completed builds waiting for approval or feedback. Does not call the Anthropic API.",
|
|
23549
|
-
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
23708
|
+
annotations: { title: "List Reviews", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
23550
23709
|
inputSchema: {
|
|
23551
23710
|
type: "object",
|
|
23552
23711
|
properties: {
|
|
@@ -23561,7 +23720,7 @@ var reviewListTool = {
|
|
|
23561
23720
|
var reviewSubmitTool = {
|
|
23562
23721
|
name: "review_submit",
|
|
23563
23722
|
description: "Record a review verdict on a completed build (build-acceptance) or task plan (handoff-review). ALWAYS ask the human for their verdict before calling \u2014 never auto-submit without human input. Accept moves the task to Done, request-changes sends it back for rework, reject discards the build. Updates task status based on the verdict. On handoff-review with suggested changes, returns a prompt to revise the BUILD HANDOFF.\n\nDO NOT use this tool as a substitute for review_list. If you need to see what is pending review, call review_list first. If review_list is unavailable in your tool set, STOP and tell the human their MCP integration is incomplete rather than guessing at the next pending task. (SUP-2026-010.)",
|
|
23564
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
23723
|
+
annotations: { title: "Submit Review", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
23565
23724
|
inputSchema: {
|
|
23566
23725
|
type: "object",
|
|
23567
23726
|
properties: {
|
|
@@ -24015,11 +24174,38 @@ Merge or squash those PRs first, then run \`release\` manually.`;
|
|
|
24015
24174
|
} catch {
|
|
24016
24175
|
}
|
|
24017
24176
|
const version = `v0.${result.currentCycle}.0`;
|
|
24018
|
-
const
|
|
24019
|
-
|
|
24020
|
-
|
|
24021
|
-
|
|
24022
|
-
|
|
24177
|
+
const autoGate = evaluateReleaseGate(caps, config2.gateCommand, void 0);
|
|
24178
|
+
if (autoGate.action !== "proceed") {
|
|
24179
|
+
autoReleaseNote = `
|
|
24180
|
+
|
|
24181
|
+
---
|
|
24182
|
+
|
|
24183
|
+
\u26A0\uFE0F **Auto-release skipped** \u2014 a release quality gate is configured (\`${config2.gateCommand}\`), and it cannot be run from inside \`review_submit\`.
|
|
24184
|
+
|
|
24185
|
+
Run \`release\` manually: PAPI will hand you the gate command, then release once you report it green.`;
|
|
24186
|
+
} else {
|
|
24187
|
+
const releaseTracker = new ProgressTracker("auto-release").bindStream(adapter2, { stage: "release" });
|
|
24188
|
+
await beginRelease(releaseTracker, result.currentCycle);
|
|
24189
|
+
const releaseResult = await createRelease(config2, baseBranch, version, adapter2, result.currentCycle);
|
|
24190
|
+
await recordReadinessVerified(releaseTracker);
|
|
24191
|
+
await recordQualityGate(releaseTracker, autoGate, caps);
|
|
24192
|
+
await tracker.recordStep("auto_release_triggered", { metadata: { version: releaseResult.version } });
|
|
24193
|
+
const autoChangelogDirective = buildChangelogDirective(
|
|
24194
|
+
caps,
|
|
24195
|
+
buildCycleUpdateCurationDirective(releaseResult.version, releaseResult.cycleClosed ?? 0)
|
|
24196
|
+
);
|
|
24197
|
+
const autoDeployDirective = buildDeployHookDirective(caps, config2.deployCommand);
|
|
24198
|
+
await completeRelease(releaseTracker, {
|
|
24199
|
+
cycleClosed: releaseResult.cycleClosed ?? null,
|
|
24200
|
+
version: releaseResult.version,
|
|
24201
|
+
caps,
|
|
24202
|
+
branchMerges: releaseResult.groupedBranchMerges ?? [],
|
|
24203
|
+
changelogEmitted: Boolean(autoChangelogDirective),
|
|
24204
|
+
deployHookEmitted: Boolean(autoDeployDirective)
|
|
24205
|
+
});
|
|
24206
|
+
const pushInfo = releaseResult.pushNotes.join(" ");
|
|
24207
|
+
const groupedMergeNote = releaseResult.groupedBranchMerges?.length ? "\n" + releaseResult.groupedBranchMerges.map((r) => `- Merged shared branch \`${r.branch}\` via PR: ${r.prUrl ?? "n/a"}`).join("\n") : "";
|
|
24208
|
+
autoReleaseNote = `
|
|
24023
24209
|
|
|
24024
24210
|
---
|
|
24025
24211
|
|
|
@@ -24029,9 +24215,15 @@ Merge or squash those PRs first, then run \`release\` manually.`;
|
|
|
24029
24215
|
- ${releaseResult.commitNote}
|
|
24030
24216
|
- ${releaseResult.tagMessage}
|
|
24031
24217
|
- ${pushInfo}` + groupedMergeNote + (releaseResult.warnings?.length ? `
|
|
24032
|
-
- Warnings: ${releaseResult.warnings.join(", ")}` : "") +
|
|
24218
|
+
- Warnings: ${releaseResult.warnings.join(", ")}` : "") + // task-2598 (C328): the auto path previously swallowed the curated
|
|
24219
|
+
// cycle-update directive that the manual path emits, so an auto-released
|
|
24220
|
+
// cycle never prompted the Discord post. Same directive, same gate.
|
|
24221
|
+
(autoChangelogDirective ? `
|
|
24222
|
+
${autoChangelogDirective}` : "") + (autoDeployDirective ? `
|
|
24223
|
+
${autoDeployDirective}` : "") + `
|
|
24033
24224
|
|
|
24034
24225
|
Run \`plan\` to create Cycle ${result.currentCycle + 1}.`;
|
|
24226
|
+
}
|
|
24035
24227
|
}
|
|
24036
24228
|
}
|
|
24037
24229
|
} catch (err) {
|
|
@@ -24113,7 +24305,7 @@ ${statusNote}${autoReviewNote}${securityNote}${unblockNote}${docClosureNote}${re
|
|
|
24113
24305
|
var reviewClaimTool = {
|
|
24114
24306
|
name: "review_claim",
|
|
24115
24307
|
description: "Claim a Pending Review from the shared cross-user review queue so you are the one reviewing it. Atomic first-claim-wins \u2014 two reviewers cannot grab the same build. After claiming, run review_submit to record your verdict. Owner-or-active-member only. Does not call the Anthropic API.",
|
|
24116
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
24308
|
+
annotations: { title: "Claim Review", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
24117
24309
|
inputSchema: {
|
|
24118
24310
|
type: "object",
|
|
24119
24311
|
properties: {
|
|
@@ -24178,7 +24370,7 @@ import path5 from "path";
|
|
|
24178
24370
|
var initTool = {
|
|
24179
24371
|
name: "init",
|
|
24180
24372
|
description: "Write the MCP config file that connects this project to PAPI. Generates .mcp.json (Claude Code default) or the equivalent for Cursor, VS Code, Windsurf, OpenCode, Amazon Q, Kilo Code, Gemini CLI, Codex CLI, or Hermes Agent. Config-only \u2014 does not create any project data. Run this first, then run `setup` to create your PAPI project.",
|
|
24181
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
24373
|
+
annotations: { title: "Initialise Project", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
24182
24374
|
inputSchema: {
|
|
24183
24375
|
type: "object",
|
|
24184
24376
|
properties: {
|
|
@@ -25087,6 +25279,104 @@ function formatUnblockSection(candidates) {
|
|
|
25087
25279
|
return lines.join("\n");
|
|
25088
25280
|
}
|
|
25089
25281
|
|
|
25282
|
+
// src/lib/deferred-gate.ts
|
|
25283
|
+
var GATE_PHRASES = [
|
|
25284
|
+
"depends on",
|
|
25285
|
+
"depends upon",
|
|
25286
|
+
"gated on",
|
|
25287
|
+
"gated behind",
|
|
25288
|
+
"blocked on",
|
|
25289
|
+
"blocked by",
|
|
25290
|
+
"waiting on",
|
|
25291
|
+
"waiting for",
|
|
25292
|
+
"pending",
|
|
25293
|
+
"once",
|
|
25294
|
+
"after",
|
|
25295
|
+
"until",
|
|
25296
|
+
"requires",
|
|
25297
|
+
"needs",
|
|
25298
|
+
"pull condition",
|
|
25299
|
+
"unblocked by"
|
|
25300
|
+
];
|
|
25301
|
+
var CONTEXT_WINDOW = 60;
|
|
25302
|
+
var TASK_REF2 = /\btask-\d+\b/gi;
|
|
25303
|
+
var MAX_PER_BUCKET = 8;
|
|
25304
|
+
function extractGatedTaskRefs(notes) {
|
|
25305
|
+
if (!notes) return [];
|
|
25306
|
+
const out = [];
|
|
25307
|
+
const seen = /* @__PURE__ */ new Set();
|
|
25308
|
+
for (const m of notes.matchAll(TASK_REF2)) {
|
|
25309
|
+
const ref = m[0].toLowerCase();
|
|
25310
|
+
if (seen.has(ref)) continue;
|
|
25311
|
+
const start = Math.max(0, (m.index ?? 0) - CONTEXT_WINDOW);
|
|
25312
|
+
const before = notes.slice(start, m.index).toLowerCase();
|
|
25313
|
+
const hit = GATE_PHRASES.find((p) => before.includes(p));
|
|
25314
|
+
if (!hit) continue;
|
|
25315
|
+
seen.add(ref);
|
|
25316
|
+
const evidence = notes.slice(start, (m.index ?? 0) + m[0].length).trim();
|
|
25317
|
+
out.push({ ref, evidence: evidence.length > 90 ? `\u2026${evidence.slice(-90)}` : evidence });
|
|
25318
|
+
}
|
|
25319
|
+
return out;
|
|
25320
|
+
}
|
|
25321
|
+
async function findDeferredGates(adapter2) {
|
|
25322
|
+
const empty = { unblock: [], zombie: [] };
|
|
25323
|
+
let allTasks = [];
|
|
25324
|
+
try {
|
|
25325
|
+
allTasks = await adapter2.queryBoard();
|
|
25326
|
+
} catch {
|
|
25327
|
+
return empty;
|
|
25328
|
+
}
|
|
25329
|
+
const deferred = allTasks.filter((t) => t.status === "Deferred");
|
|
25330
|
+
if (deferred.length === 0) return empty;
|
|
25331
|
+
const byId = /* @__PURE__ */ new Map();
|
|
25332
|
+
for (const t of allTasks) {
|
|
25333
|
+
if (t.displayId) byId.set(t.displayId.toLowerCase(), t);
|
|
25334
|
+
}
|
|
25335
|
+
const sweep = { unblock: [], zombie: [] };
|
|
25336
|
+
for (const task of deferred) {
|
|
25337
|
+
for (const { ref, evidence } of extractGatedTaskRefs(task.notes ?? "")) {
|
|
25338
|
+
const self = task.displayId?.toLowerCase();
|
|
25339
|
+
if (self && ref === self) continue;
|
|
25340
|
+
const upstream = byId.get(ref);
|
|
25341
|
+
if (!upstream) continue;
|
|
25342
|
+
if (upstream.status !== "Done" && upstream.status !== "Cancelled") continue;
|
|
25343
|
+
const gate = {
|
|
25344
|
+
taskId: task.displayId ?? task.id,
|
|
25345
|
+
taskTitle: task.title,
|
|
25346
|
+
upstreamId: upstream.displayId ?? upstream.id,
|
|
25347
|
+
upstreamTitle: upstream.title,
|
|
25348
|
+
upstreamStatus: upstream.status,
|
|
25349
|
+
evidence
|
|
25350
|
+
};
|
|
25351
|
+
const bucket = upstream.status === "Done" ? sweep.unblock : sweep.zombie;
|
|
25352
|
+
if (bucket.length < MAX_PER_BUCKET) bucket.push(gate);
|
|
25353
|
+
break;
|
|
25354
|
+
}
|
|
25355
|
+
}
|
|
25356
|
+
return sweep;
|
|
25357
|
+
}
|
|
25358
|
+
function formatDeferredGateSection(sweep) {
|
|
25359
|
+
if (sweep.unblock.length === 0 && sweep.zombie.length === 0) return "";
|
|
25360
|
+
const lines = ["## Deferred-Gate Sweep"];
|
|
25361
|
+
if (sweep.unblock.length > 0) {
|
|
25362
|
+
lines.push("");
|
|
25363
|
+
lines.push(`**Unblock candidates (${sweep.unblock.length})** \u2014 the upstream task is Done, so the gate has cleared. Promote with \`board_edit\` \u2192 Backlog.`);
|
|
25364
|
+
for (const g of sweep.unblock) {
|
|
25365
|
+
lines.push(`- **${g.taskId}** (${g.taskTitle}) \u2014 gated on **${g.upstreamId}** which is now Done \u2705`);
|
|
25366
|
+
}
|
|
25367
|
+
}
|
|
25368
|
+
if (sweep.zombie.length > 0) {
|
|
25369
|
+
lines.push("");
|
|
25370
|
+
lines.push(`**Zombie candidates (${sweep.zombie.length})** \u2014 the upstream task was CANCELLED, so this gate can never clear. Cancel or re-scope.`);
|
|
25371
|
+
for (const g of sweep.zombie) {
|
|
25372
|
+
lines.push(`- **${g.taskId}** (${g.taskTitle}) \u2014 gated on **${g.upstreamId}** which was Cancelled \u274C`);
|
|
25373
|
+
}
|
|
25374
|
+
}
|
|
25375
|
+
lines.push("");
|
|
25376
|
+
lines.push("_Advisory only \u2014 nothing was changed. Gates are read from freetext notes, so check the reference before acting._");
|
|
25377
|
+
return lines.join("\n");
|
|
25378
|
+
}
|
|
25379
|
+
|
|
25090
25380
|
// src/tools/agent-list.ts
|
|
25091
25381
|
import { readdir as readdir2, readFile as readFile7 } from "fs/promises";
|
|
25092
25382
|
import { join as join14 } from "path";
|
|
@@ -25129,7 +25419,7 @@ async function listAgents(projectRoot) {
|
|
|
25129
25419
|
var agentListTool = {
|
|
25130
25420
|
name: "agent_list",
|
|
25131
25421
|
description: "List the sub-agents discovered in the project's `.claude/agents/*.md` files (read-only). Returns each agent's name and description so you can see which specialised sub-agents are available before dispatching one via the Task/Agent tool. Discovery only \u2014 does not invoke or manage agents.",
|
|
25132
|
-
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
25422
|
+
annotations: { title: "List Agents", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
25133
25423
|
inputSchema: {
|
|
25134
25424
|
type: "object",
|
|
25135
25425
|
properties: {
|
|
@@ -25308,7 +25598,7 @@ async function runWithLimit(concurrency, tasks) {
|
|
|
25308
25598
|
var orientTool = {
|
|
25309
25599
|
name: "orient",
|
|
25310
25600
|
description: "Session orientation \u2014 run this FIRST at session start before any other tool. Single call that replaces build_list + health. Returns: cycle number, task counts by status, in-progress/in-review tasks, strategy review cadence, velocity snapshot, recommended next action, and a release reminder when all cycle tasks are Done but release has not run. Read-only, does not modify any files. PAPI detects build capability from the connecting harness (clientInfo); pass `environment` only to override that detection for git-dependent recommendations (build_execute, release, review_submit).",
|
|
25311
|
-
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
25601
|
+
annotations: { title: "Orient Session", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
25312
25602
|
inputSchema: {
|
|
25313
25603
|
type: "object",
|
|
25314
25604
|
properties: {
|
|
@@ -25332,6 +25622,7 @@ var orientTool = {
|
|
|
25332
25622
|
var papiTool = {
|
|
25333
25623
|
...orientTool,
|
|
25334
25624
|
name: "papi",
|
|
25625
|
+
annotations: { title: "PAPI Status", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
25335
25626
|
description: 'Say "papi" to check in with Papi \u2014 an alias for `orient`. Run this FIRST at session start: it returns your cycle number, task counts, in-progress/in-review work, strategy-review cadence, a velocity snapshot, and the recommended next action. Identical to `orient` (same inputs, same output); use whichever name you prefer. Read-only.'
|
|
25336
25627
|
};
|
|
25337
25628
|
function countStalledP1(warnings) {
|
|
@@ -26196,6 +26487,17 @@ ${versionDrift}` : "";
|
|
|
26196
26487
|
${section}`;
|
|
26197
26488
|
} catch {
|
|
26198
26489
|
}
|
|
26490
|
+
let deferredGateNote = "";
|
|
26491
|
+
if (deepHousekeeping) {
|
|
26492
|
+
try {
|
|
26493
|
+
const sweep = await tracked("deferred-gates", () => findDeferredGates(adapter2))();
|
|
26494
|
+
const section = formatDeferredGateSection(sweep);
|
|
26495
|
+
if (section) deferredGateNote = `
|
|
26496
|
+
|
|
26497
|
+
${section}`;
|
|
26498
|
+
} catch {
|
|
26499
|
+
}
|
|
26500
|
+
}
|
|
26199
26501
|
tracker.mark("format-summary");
|
|
26200
26502
|
const subAgents = await listAgents(config2.projectRoot);
|
|
26201
26503
|
const [teamSummaryLine, releaseHistoryLine] = await Promise.all([
|
|
@@ -26204,7 +26506,7 @@ ${section}`;
|
|
|
26204
26506
|
]);
|
|
26205
26507
|
const teamSummary = [teamSummaryLine, releaseHistoryLine].filter(Boolean).join("\n") || void 0;
|
|
26206
26508
|
const deepHint = deepHousekeeping ? "" : "\n\n*Tip: pass `full: true` for Research Signals + version-drift, or `deep_housekeeping: true` to also check orphaned branches, merged-but-In-Progress tasks, unrecorded commits, unregistered docs, and stale skill forks (implies `full`).*";
|
|
26207
|
-
return textResponse(projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents, projectName, teamSummary, clientName) + unblockNote + feedbackResolvedNote + alertsNote + ttfvNote + reconciliationNote + mergedInProgressNote + unrecordedNote + unregisteredDocsNote + staleSkillsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + deepHint + enrichmentFilesSection);
|
|
26509
|
+
return textResponse(projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents, projectName, teamSummary, clientName) + unblockNote + deferredGateNote + feedbackResolvedNote + alertsNote + ttfvNote + reconciliationNote + mergedInProgressNote + unrecordedNote + unregisteredDocsNote + staleSkillsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + deepHint + enrichmentFilesSection);
|
|
26208
26510
|
} catch (err) {
|
|
26209
26511
|
const message = err instanceof Error ? err.message : String(err);
|
|
26210
26512
|
const isKnownFriendly = /^(Orient failed|Project not found|No project|Setup required)/i.test(message);
|
|
@@ -26260,7 +26562,7 @@ function enrichClaudeMd(projectRoot, cycleNumber, adapterType, collector, client
|
|
|
26260
26562
|
var hierarchyUpdateTool = {
|
|
26261
26563
|
name: "hierarchy_update",
|
|
26262
26564
|
description: "Update the status of a phase, stage, or horizon in the project hierarchy (AD-14). Accepts a level (phase, stage, or horizon), a name or ID, and a new status. For stages, optionally set exit_criteria \u2014 a checklist defining when the stage is considered done. Does not call the Anthropic API.",
|
|
26263
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
26565
|
+
annotations: { title: "Update Hierarchy", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
26264
26566
|
inputSchema: {
|
|
26265
26567
|
type: "object",
|
|
26266
26568
|
properties: {
|
|
@@ -26671,7 +26973,7 @@ async function applyZoomOut(adapter2, llmResponse, cycleNumber) {
|
|
|
26671
26973
|
var zoomOutTool = {
|
|
26672
26974
|
name: "zoom_out",
|
|
26673
26975
|
description: 'Run a Zoom-Out Retrospective \u2014 a higher-level meta-retrospective that sits above strategy reviews. Analyses the full project arc: every cycle, decision, and pivot. Use when you want to step back and see the big picture after many cycles. First call returns a prompt (prepare phase). Then call again with mode "apply" and your output.',
|
|
26674
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
26976
|
+
annotations: { title: "Zoom Out", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
26675
26977
|
inputSchema: {
|
|
26676
26978
|
type: "object",
|
|
26677
26979
|
properties: {
|
|
@@ -26775,7 +27077,7 @@ ${result.userMessage}
|
|
|
26775
27077
|
var getSiblingAdsTool = {
|
|
26776
27078
|
name: "get_sibling_ads",
|
|
26777
27079
|
description: "Read Active Decisions from sibling PAPI projects that share the same Supabase instance. Requires PAPI_SIBLING_PROJECT_IDS env var (comma-separated project UUIDs). Returns ADs labelled by source project \u2014 useful for cross-project architectural alignment. pg adapter only \u2014 returns an error if using md or proxy adapter.",
|
|
26778
|
-
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
27080
|
+
annotations: { title: "Sibling Decisions", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
26779
27081
|
inputSchema: {
|
|
26780
27082
|
type: "object",
|
|
26781
27083
|
properties: {
|
|
@@ -26852,7 +27154,7 @@ var handoffPrepareCache = new PerCallerCache();
|
|
|
26852
27154
|
var handoffGenerateTool = {
|
|
26853
27155
|
name: "handoff_generate",
|
|
26854
27156
|
description: "Generate BUILD HANDOFFs for cycle tasks that don't have one yet. Run after `plan` (with skip_handoffs=true) or to regenerate stale handoffs. Uses the prepare/apply pattern \u2014 first call returns a prompt, second call persists results.",
|
|
26855
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
27157
|
+
annotations: { title: "Generate Handoff", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
26856
27158
|
inputSchema: {
|
|
26857
27159
|
type: "object",
|
|
26858
27160
|
properties: {
|
|
@@ -27108,7 +27410,7 @@ function buildSummary(task, taskCount) {
|
|
|
27108
27410
|
var scopeBriefTool = {
|
|
27109
27411
|
name: "scope_brief",
|
|
27110
27412
|
description: "Decompose a brief-class task (Large/XL, too large to build directly) into a structured scope document. Runs an LLM pass to produce sub-tasks, writes docs/scopes/<task-id>.md, registers it in the doc registry, and marks the source task as decomposed. Use before planning a cycle that includes brief-class tasks.",
|
|
27111
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
27413
|
+
annotations: { title: "Scope Brief", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
27112
27414
|
inputSchema: {
|
|
27113
27415
|
type: "object",
|
|
27114
27416
|
properties: {
|
|
@@ -27157,7 +27459,7 @@ async function handleScopeBrief(adapter2, config2, args) {
|
|
|
27157
27459
|
var adViewTool = {
|
|
27158
27460
|
name: "ad_view",
|
|
27159
27461
|
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.",
|
|
27160
|
-
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
27462
|
+
annotations: { title: "View Decisions", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
27161
27463
|
inputSchema: {
|
|
27162
27464
|
type: "object",
|
|
27163
27465
|
properties: {
|
|
@@ -27214,7 +27516,7 @@ ${formatted}`);
|
|
|
27214
27516
|
var learningActionTool = {
|
|
27215
27517
|
name: "learning_action",
|
|
27216
27518
|
description: `Mark a cycle learning as actioned (linking it to a task or idea) or list unactioned learnings. Use "mark" to close out a learning after you've submitted an idea or created a task for it. Use "list" to see which learnings from recent cycles still need follow-up.`,
|
|
27217
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
27519
|
+
annotations: { title: "Action Learning", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
27218
27520
|
inputSchema: {
|
|
27219
27521
|
type: "object",
|
|
27220
27522
|
properties: {
|
|
@@ -27300,7 +27602,7 @@ Use \`learning_action mark\` or submit via \`idea\` to close these out.`
|
|
|
27300
27602
|
var discoveredIssueResolveTool = {
|
|
27301
27603
|
name: "discovered_issue_resolve",
|
|
27302
27604
|
description: 'Mark a discovered_issue (cycle_learnings row, category="issue") as resolved. Pass the learning_id you saw in orient / learning_action list output. The row stays in the database for history, but default reads exclude it. Use this when the underlying fix has actually landed \u2014 NOT when you just created a follow-up task (that is `learning_action mark` with action_taken="task_created"). Optional `note` is recorded as resolved_by.',
|
|
27303
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
27605
|
+
annotations: { title: "Resolve Issue", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
27304
27606
|
inputSchema: {
|
|
27305
27607
|
type: "object",
|
|
27306
27608
|
properties: {
|
|
@@ -27399,7 +27701,7 @@ var NO_SUPPORT = "Project lifecycle tools require a hosted (proxy) or PostgreSQL
|
|
|
27399
27701
|
var projectCreateTool = {
|
|
27400
27702
|
name: "project_create",
|
|
27401
27703
|
description: 'Create an EMPTY PAPI project for the current workspace (no plan, no seeded backlog) and return its id. Idempotent: re-running in the same folder (or with the same name) returns the EXISTING project instead of creating a duplicate. Use when "set up papi here" / "create a project" and none matches this folder. No Anthropic API.',
|
|
27402
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
27704
|
+
annotations: { title: "Create Project", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
27403
27705
|
inputSchema: {
|
|
27404
27706
|
type: "object",
|
|
27405
27707
|
properties: {
|
|
@@ -27445,7 +27747,7 @@ Run \`plan\` when you're ready to start a cycle.`
|
|
|
27445
27747
|
var projectListTool = {
|
|
27446
27748
|
name: "project_list",
|
|
27447
27749
|
description: "List the PAPI projects on your account (id, name, slug, mapped folder). Use to see which project you are about to write to, or to find the id/slug to pass to project_switch or the per-call `project` arg. No Anthropic API.",
|
|
27448
|
-
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
27750
|
+
annotations: { title: "List Projects", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
27449
27751
|
inputSchema: {
|
|
27450
27752
|
type: "object",
|
|
27451
27753
|
properties: {
|
|
@@ -27476,7 +27778,7 @@ ${lines.join("\n")}`);
|
|
|
27476
27778
|
var projectSwitchTool = {
|
|
27477
27779
|
name: "project_switch",
|
|
27478
27780
|
description: 'Select a project you own by id or slug and map the current folder to it (sets papi_dir on local stdio). Use when "switch to golf" / "point papi at <project>". Fails closed if the project is not on your account. No Anthropic API.',
|
|
27479
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
27781
|
+
annotations: { title: "Switch Project", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
27480
27782
|
inputSchema: {
|
|
27481
27783
|
type: "object",
|
|
27482
27784
|
properties: {
|
|
@@ -27511,7 +27813,7 @@ On hosted/remote sessions this is now your DEFAULT project \u2014 calls without
|
|
|
27511
27813
|
var contributorAddTool = {
|
|
27512
27814
|
name: "contributor_add",
|
|
27513
27815
|
description: "Add a contributor to the current project by email (owner-only). The person must already have a PAPI account. Grants cohort membership on project_contributors \u2014 contributors-tier visibility, no roles yet.",
|
|
27514
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
27816
|
+
annotations: { title: "Add Contributor", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
27515
27817
|
inputSchema: {
|
|
27516
27818
|
type: "object",
|
|
27517
27819
|
properties: {
|
|
@@ -27523,7 +27825,7 @@ var contributorAddTool = {
|
|
|
27523
27825
|
var contributorRemoveTool = {
|
|
27524
27826
|
name: "contributor_remove",
|
|
27525
27827
|
description: "Remove a contributor from the current project by email (owner-only). Deletes their project_contributors row \u2014 they lose contributors-tier visibility.",
|
|
27526
|
-
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
27828
|
+
annotations: { title: "Remove Contributor", readOnlyHint: false, destructiveHint: true, openWorldHint: false },
|
|
27527
27829
|
inputSchema: {
|
|
27528
27830
|
type: "object",
|
|
27529
27831
|
properties: {
|
|
@@ -27535,7 +27837,7 @@ var contributorRemoveTool = {
|
|
|
27535
27837
|
var contributorListTool = {
|
|
27536
27838
|
name: "contributor_list",
|
|
27537
27839
|
description: "List the current project's contributors (any project member). Shows each member's email, display name, role, and join date. Does not call the Anthropic API.",
|
|
27538
|
-
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
27840
|
+
annotations: { title: "List Contributors", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
27539
27841
|
inputSchema: {
|
|
27540
27842
|
type: "object",
|
|
27541
27843
|
properties: {
|
|
@@ -27641,7 +27943,7 @@ async function handleContributorList(adapter2, config2, _args) {
|
|
|
27641
27943
|
var taskClaimTool = {
|
|
27642
27944
|
name: "task_claim",
|
|
27643
27945
|
description: "Claim a task from the shared org Pool into your personal backlog (assignee = you). Atomic first-claim-wins \u2014 a concurrent double-claim is impossible. Cascades the DEPENDS ON chain: claiming a task also claims its not-Done prerequisites; if any prerequisite is already claimed by another member the whole claim is refused (a build unit is never split across owners). Does not call the Anthropic API.",
|
|
27644
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
27946
|
+
annotations: { title: "Claim Task", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
27645
27947
|
inputSchema: {
|
|
27646
27948
|
type: "object",
|
|
27647
27949
|
properties: {
|
|
@@ -27653,7 +27955,7 @@ var taskClaimTool = {
|
|
|
27653
27955
|
var taskUnclaimTool = {
|
|
27654
27956
|
name: "task_unclaim",
|
|
27655
27957
|
description: "Release a task you claimed back to the shared Pool (clears assignee). Claimer-only and pre-review \u2014 you cannot unclaim another member's task or one that has reached In Review/Done. Does not cascade. Does not call the Anthropic API.",
|
|
27656
|
-
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
27958
|
+
annotations: { title: "Unclaim Task", readOnlyHint: false, destructiveHint: true, openWorldHint: false },
|
|
27657
27959
|
inputSchema: {
|
|
27658
27960
|
type: "object",
|
|
27659
27961
|
properties: {
|
|
@@ -27790,7 +28092,7 @@ async function handleTaskUnclaim(adapter2, config2, args) {
|
|
|
27790
28092
|
var taskMoveTool = {
|
|
27791
28093
|
name: "task_move",
|
|
27792
28094
|
description: "Move a task from the current project to another project you own. Reassigns the task a fresh id in the target (collision-free) and carries its build reports, comments, and history with it; the cycle assignment is cleared so it lands in the target project's backlog. You must own (or have write access to) BOTH projects. Destructive-ish and cross-project, so it requires confirm=true \u2014 without it you get a preview only. Does not call the Anthropic API.",
|
|
27793
|
-
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
28095
|
+
annotations: { title: "Move Task", readOnlyHint: false, destructiveHint: true, openWorldHint: false },
|
|
27794
28096
|
inputSchema: {
|
|
27795
28097
|
type: "object",
|
|
27796
28098
|
properties: {
|
|
@@ -27977,7 +28279,7 @@ async function syncHarnessInventory(adapter2, config2, opts) {
|
|
|
27977
28279
|
var inventorySyncTool = {
|
|
27978
28280
|
name: "inventory_sync",
|
|
27979
28281
|
description: "Sync this project's harness inventory \u2014 skills, sub-agents, hooks, and MCP tools \u2014 to the database so the dashboard can surface it. Gated by a cheap change-fingerprint: a no-op when the harness hasn't changed since the last sync. Set force=true to re-scan and write regardless. Runs automatically at setup and release; use this for an explicit refresh after editing your harness.",
|
|
27980
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
28282
|
+
annotations: { title: "Sync Inventory", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
27981
28283
|
inputSchema: {
|
|
27982
28284
|
type: "object",
|
|
27983
28285
|
properties: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@papi-ai/server",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.53",
|
|
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",
|