@papi-ai/server 0.7.12 → 0.7.14
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/index.js +729 -67
- package/dist/prompts.js +31 -0
- package/package.json +1 -2
package/dist/index.js
CHANGED
|
@@ -10,7 +10,12 @@ var __export = (target, all) => {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
// ../shared/dist/index.js
|
|
13
|
-
|
|
13
|
+
function isLiveDecision(d) {
|
|
14
|
+
if (d.superseded === true) return false;
|
|
15
|
+
if (d.outcome != null && RETIRED_DECISION_OUTCOMES.includes(d.outcome)) return false;
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
var VALID_TRANSITIONS, RETIRED_DECISION_OUTCOMES;
|
|
14
19
|
var init_dist = __esm({
|
|
15
20
|
"../shared/dist/index.js"() {
|
|
16
21
|
"use strict";
|
|
@@ -25,6 +30,7 @@ var init_dist = __esm({
|
|
|
25
30
|
"Cancelled": [],
|
|
26
31
|
"Deferred": ["Backlog", "Cancelled"]
|
|
27
32
|
};
|
|
33
|
+
RETIRED_DECISION_OUTCOMES = ["resolved", "abandoned", "superseded"];
|
|
28
34
|
}
|
|
29
35
|
});
|
|
30
36
|
|
|
@@ -1441,12 +1447,13 @@ async function detectReviewPatterns(reviews, currentCycle, window = 5, clusterer
|
|
|
1441
1447
|
function hasReviewPatterns(patterns) {
|
|
1442
1448
|
return patterns.recurringFeedback.length > 0 || patterns.requestChangesRate >= 50;
|
|
1443
1449
|
}
|
|
1444
|
-
var VALID_TRANSITIONS2, TASK_TYPE_TIERS, VALID_EFFORT_SIZES, SECTION_HEADERS, YAML_MARKER, YAML_START, YAML_END, VALID_EFFORT_SIZES2, HEADER_SENTINEL, TABLE_HEADER, TABLE_SEPARATOR, PREV_TABLE_HEADER, LEGACY_TABLE_HEADER, SECTION_HEADING, FILE_TEMPLATE, COST_SECTION_HEADING, COST_TABLE_SEPARATOR, FILE_HEADING, ACCURACY_HEADER, ACCURACY_SEPARATOR, VELOCITY_HEADER, VELOCITY_SEPARATOR, EFFORT_SCALE, NONE_PATTERN, HEADER_SENTINEL2, VALID_STAGES, VALID_VERDICTS, STAGE_DISPLAY, VALID_STATUSES, PHASES_START, PHASES_END, YAML_MARKER2, YAML_START2, YAML_END2, VALID_STATUSES2, YAML_MARKER3, YAML_START3, YAML_END3, MdFileAdapter, NONE_PATTERN2;
|
|
1450
|
+
var VALID_TRANSITIONS2, isLiveDecision2, TASK_TYPE_TIERS, VALID_EFFORT_SIZES, SECTION_HEADERS, YAML_MARKER, YAML_START, YAML_END, VALID_EFFORT_SIZES2, HEADER_SENTINEL, TABLE_HEADER, TABLE_SEPARATOR, PREV_TABLE_HEADER, LEGACY_TABLE_HEADER, SECTION_HEADING, FILE_TEMPLATE, COST_SECTION_HEADING, COST_TABLE_SEPARATOR, FILE_HEADING, ACCURACY_HEADER, ACCURACY_SEPARATOR, VELOCITY_HEADER, VELOCITY_SEPARATOR, EFFORT_SCALE, NONE_PATTERN, HEADER_SENTINEL2, VALID_STAGES, VALID_VERDICTS, STAGE_DISPLAY, VALID_STATUSES, PHASES_START, PHASES_END, YAML_MARKER2, YAML_START2, YAML_END2, VALID_STATUSES2, YAML_MARKER3, YAML_START3, YAML_END3, MdFileAdapter, NONE_PATTERN2;
|
|
1445
1451
|
var init_dist2 = __esm({
|
|
1446
1452
|
"../adapter-md/dist/index.js"() {
|
|
1447
1453
|
"use strict";
|
|
1448
1454
|
init_dist();
|
|
1449
1455
|
VALID_TRANSITIONS2 = VALID_TRANSITIONS;
|
|
1456
|
+
isLiveDecision2 = isLiveDecision;
|
|
1450
1457
|
TASK_TYPE_TIERS = {
|
|
1451
1458
|
bug: 1,
|
|
1452
1459
|
task: 1,
|
|
@@ -1557,11 +1564,18 @@ ${TABLE_SEPARATOR}
|
|
|
1557
1564
|
async getCycleHealth() {
|
|
1558
1565
|
return parseCycleHealth(await this.read("PLANNING_LOG.md"));
|
|
1559
1566
|
}
|
|
1560
|
-
/**
|
|
1561
|
-
|
|
1567
|
+
/**
|
|
1568
|
+
* Read Active Decisions from ACTIVE_DECISIONS.md.
|
|
1569
|
+
*
|
|
1570
|
+
* Default filters out retired ADs (outcome ∈ abandoned/superseded/resolved or superseded=true).
|
|
1571
|
+
* Pass { includeRetired: true } for management/triage surfaces. See PapiAdapter docstring.
|
|
1572
|
+
*/
|
|
1573
|
+
async getActiveDecisions(options) {
|
|
1562
1574
|
const content = await this.readOptional("ACTIVE_DECISIONS.md");
|
|
1563
1575
|
if (!content) return [];
|
|
1564
|
-
|
|
1576
|
+
const all = parseActiveDecisions(content);
|
|
1577
|
+
if (options?.includeRetired) return all;
|
|
1578
|
+
return all.filter(isLiveDecision2);
|
|
1565
1579
|
}
|
|
1566
1580
|
/** Read cycle log entries (newest first), optionally limited to {@link limit} entries. */
|
|
1567
1581
|
async getCycleLog(limit) {
|
|
@@ -2068,6 +2082,102 @@ ${footer}`);
|
|
|
2068
2082
|
await this.write("STRATEGY_RECOMMENDATIONS.md", updated);
|
|
2069
2083
|
}
|
|
2070
2084
|
// -------------------------------------------------------------------------
|
|
2085
|
+
// Strategy Review Agenda (markdown persistence)
|
|
2086
|
+
// -------------------------------------------------------------------------
|
|
2087
|
+
async addAgendaTopic(input) {
|
|
2088
|
+
const id = randomUUID6();
|
|
2089
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2090
|
+
const full = {
|
|
2091
|
+
id,
|
|
2092
|
+
topic: input.topic,
|
|
2093
|
+
source: input.source,
|
|
2094
|
+
sourceCycle: input.sourceCycle,
|
|
2095
|
+
status: "pending",
|
|
2096
|
+
createdAt
|
|
2097
|
+
};
|
|
2098
|
+
const content = await this.readOptional("STRATEGY_REVIEW_AGENDA.md");
|
|
2099
|
+
const header = "# Strategy Review Agenda\n\n<!-- PAPI-ADAPTER: parse the yaml block below -->\n\n<!-- PAPI-YAML-START -->\ntopics:\n";
|
|
2100
|
+
const footer = "<!-- PAPI-YAML-END -->\n";
|
|
2101
|
+
const entry = [
|
|
2102
|
+
` - id: ${full.id}`,
|
|
2103
|
+
` topic: ${JSON.stringify(full.topic)}`,
|
|
2104
|
+
` source: ${full.source}`,
|
|
2105
|
+
full.sourceCycle != null ? ` source_cycle: ${full.sourceCycle}` : null,
|
|
2106
|
+
` status: ${full.status}`,
|
|
2107
|
+
` created_at: ${full.createdAt}`
|
|
2108
|
+
].filter(Boolean).join("\n");
|
|
2109
|
+
if (!content) {
|
|
2110
|
+
await this.write("STRATEGY_REVIEW_AGENDA.md", `${header}${entry}
|
|
2111
|
+
${footer}`);
|
|
2112
|
+
} else {
|
|
2113
|
+
const insertPoint = content.indexOf("<!-- PAPI-YAML-END -->");
|
|
2114
|
+
if (insertPoint === -1) {
|
|
2115
|
+
await this.write("STRATEGY_REVIEW_AGENDA.md", `${header}${entry}
|
|
2116
|
+
${footer}`);
|
|
2117
|
+
} else {
|
|
2118
|
+
const updated = content.slice(0, insertPoint) + entry + "\n" + content.slice(insertPoint);
|
|
2119
|
+
await this.write("STRATEGY_REVIEW_AGENDA.md", updated);
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
return full;
|
|
2123
|
+
}
|
|
2124
|
+
async getPendingAgendaTopics() {
|
|
2125
|
+
const content = await this.readOptional("STRATEGY_REVIEW_AGENDA.md");
|
|
2126
|
+
if (!content) return [];
|
|
2127
|
+
const yamlStart = content.indexOf("<!-- PAPI-YAML-START -->");
|
|
2128
|
+
const yamlEnd = content.indexOf("<!-- PAPI-YAML-END -->");
|
|
2129
|
+
if (yamlStart === -1 || yamlEnd === -1) return [];
|
|
2130
|
+
const yamlBlock = content.slice(yamlStart + "<!-- PAPI-YAML-START -->".length, yamlEnd).trim();
|
|
2131
|
+
const entries = yamlBlock.split(/(?=\s+-\s+id:)/);
|
|
2132
|
+
const topics = [];
|
|
2133
|
+
for (const block of entries) {
|
|
2134
|
+
const idMatch = block.match(/id:\s+(.+)/);
|
|
2135
|
+
const topicMatch = block.match(/topic:\s+(.+)/);
|
|
2136
|
+
const sourceMatch = block.match(/source:\s+(\S+)/);
|
|
2137
|
+
const statusMatch = block.match(/status:\s+(\S+)/);
|
|
2138
|
+
const createdMatch = block.match(/created_at:\s+(.+)/);
|
|
2139
|
+
const sourceCycleMatch = block.match(/source_cycle:\s+(\d+)/);
|
|
2140
|
+
if (!idMatch || !topicMatch || !sourceMatch || !statusMatch || !createdMatch) continue;
|
|
2141
|
+
if (statusMatch[1].trim() !== "pending") continue;
|
|
2142
|
+
let parsedTopic = topicMatch[1].trim();
|
|
2143
|
+
if (parsedTopic.startsWith('"') && parsedTopic.endsWith('"')) {
|
|
2144
|
+
try {
|
|
2145
|
+
parsedTopic = JSON.parse(parsedTopic);
|
|
2146
|
+
} catch {
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
topics.push({
|
|
2150
|
+
id: idMatch[1].trim(),
|
|
2151
|
+
topic: parsedTopic,
|
|
2152
|
+
source: sourceMatch[1].trim(),
|
|
2153
|
+
sourceCycle: sourceCycleMatch ? parseInt(sourceCycleMatch[1], 10) : void 0,
|
|
2154
|
+
status: "pending",
|
|
2155
|
+
createdAt: createdMatch[1].trim()
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
return topics;
|
|
2159
|
+
}
|
|
2160
|
+
async markAgendaTopicsAddressed(ids, cycleNumber) {
|
|
2161
|
+
if (ids.length === 0) return;
|
|
2162
|
+
const content = await this.readOptional("STRATEGY_REVIEW_AGENDA.md");
|
|
2163
|
+
if (!content) return;
|
|
2164
|
+
let updated = content;
|
|
2165
|
+
const addressedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2166
|
+
for (const id of ids) {
|
|
2167
|
+
const statusPattern = new RegExp(`(\\s+-\\s+id:\\s+${id}\\n(?:.*\\n)*?\\s+status:\\s+)pending`);
|
|
2168
|
+
updated = updated.replace(statusPattern, `$1addressed`);
|
|
2169
|
+
const insertionAnchor = new RegExp(`(\\s+-\\s+id:\\s+${id}\\n(?:.*\\n)*?\\s+created_at:\\s+[^\\n]+)\\n`);
|
|
2170
|
+
const match = updated.match(insertionAnchor);
|
|
2171
|
+
if (match && !match[0].includes("addressed_at:")) {
|
|
2172
|
+
updated = updated.replace(insertionAnchor, `$1
|
|
2173
|
+
addressed_at: ${addressedAt}
|
|
2174
|
+
addressed_in_review: ${cycleNumber}
|
|
2175
|
+
`);
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
await this.write("STRATEGY_REVIEW_AGENDA.md", updated);
|
|
2179
|
+
}
|
|
2180
|
+
// -------------------------------------------------------------------------
|
|
2071
2181
|
// Decision Events & Scores (markdown persistence)
|
|
2072
2182
|
// -------------------------------------------------------------------------
|
|
2073
2183
|
async appendDecisionEvent(event) {
|
|
@@ -6256,11 +6366,31 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
|
6256
6366
|
lastFullMode: 0
|
|
6257
6367
|
};
|
|
6258
6368
|
}
|
|
6259
|
-
|
|
6369
|
+
/**
|
|
6370
|
+
* Read Active Decisions for this project.
|
|
6371
|
+
*
|
|
6372
|
+
* Default filters out retired ADs (outcome ∈ abandoned/superseded/resolved or superseded=true).
|
|
6373
|
+
* Pass { includeRetired: true } for management/triage surfaces. See PapiAdapter docstring.
|
|
6374
|
+
*
|
|
6375
|
+
* task-1546 (C242 hot-fix): closes the bug where retired ADs leaked into context surfaces.
|
|
6376
|
+
*/
|
|
6377
|
+
async getActiveDecisions(options) {
|
|
6378
|
+
if (options?.includeRetired) {
|
|
6379
|
+
const rows2 = await this.sql`
|
|
6380
|
+
SELECT id, display_id, title, confidence, superseded, superseded_by, created_cycle, modified_cycle, body, outcome, revision_count
|
|
6381
|
+
FROM active_decisions
|
|
6382
|
+
WHERE project_id = ${this.projectId}
|
|
6383
|
+
ORDER BY display_id
|
|
6384
|
+
LIMIT 200
|
|
6385
|
+
`;
|
|
6386
|
+
return rows2.map(rowToActiveDecision);
|
|
6387
|
+
}
|
|
6260
6388
|
const rows = await this.sql`
|
|
6261
6389
|
SELECT id, display_id, title, confidence, superseded, superseded_by, created_cycle, modified_cycle, body, outcome, revision_count
|
|
6262
6390
|
FROM active_decisions
|
|
6263
6391
|
WHERE project_id = ${this.projectId}
|
|
6392
|
+
AND superseded = false
|
|
6393
|
+
AND (outcome IS NULL OR outcome NOT IN ('abandoned', 'superseded', 'resolved'))
|
|
6264
6394
|
ORDER BY display_id
|
|
6265
6395
|
LIMIT 200 -- bounded: ADs are bounded by project lifecycle, 200 is a safe ceiling
|
|
6266
6396
|
`;
|
|
@@ -7569,6 +7699,50 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
7569
7699
|
UPDATE strategy_recommendations
|
|
7570
7700
|
SET status = 'actioned', dismissal_reason = ${reason}, updated_at = now()
|
|
7571
7701
|
WHERE id = ${id} AND project_id = ${this.projectId}
|
|
7702
|
+
`;
|
|
7703
|
+
}
|
|
7704
|
+
// -------------------------------------------------------------------------
|
|
7705
|
+
// Strategy Review Agenda
|
|
7706
|
+
// -------------------------------------------------------------------------
|
|
7707
|
+
async addAgendaTopic(input) {
|
|
7708
|
+
const [row] = await this.sql`
|
|
7709
|
+
INSERT INTO strategy_review_agenda (project_id, topic, source, source_cycle)
|
|
7710
|
+
VALUES (${this.projectId}, ${input.topic}, ${input.source}, ${input.sourceCycle ?? null})
|
|
7711
|
+
RETURNING id, topic, source, source_cycle, status, created_at
|
|
7712
|
+
`;
|
|
7713
|
+
return {
|
|
7714
|
+
id: row.id,
|
|
7715
|
+
topic: row.topic,
|
|
7716
|
+
source: row.source,
|
|
7717
|
+
sourceCycle: row.source_cycle ?? void 0,
|
|
7718
|
+
status: row.status,
|
|
7719
|
+
createdAt: row.created_at
|
|
7720
|
+
};
|
|
7721
|
+
}
|
|
7722
|
+
async getPendingAgendaTopics() {
|
|
7723
|
+
const rows = await this.sql`
|
|
7724
|
+
SELECT id, topic, source, source_cycle, status, created_at, addressed_at, addressed_in_review
|
|
7725
|
+
FROM strategy_review_agenda
|
|
7726
|
+
WHERE project_id = ${this.projectId} AND status = 'pending'
|
|
7727
|
+
ORDER BY created_at ASC
|
|
7728
|
+
`;
|
|
7729
|
+
return rows.map((r) => ({
|
|
7730
|
+
id: r.id,
|
|
7731
|
+
topic: r.topic,
|
|
7732
|
+
source: r.source,
|
|
7733
|
+
sourceCycle: r.source_cycle ?? void 0,
|
|
7734
|
+
status: "pending",
|
|
7735
|
+
createdAt: r.created_at,
|
|
7736
|
+
addressedAt: r.addressed_at ?? void 0,
|
|
7737
|
+
addressedInReview: r.addressed_in_review ?? void 0
|
|
7738
|
+
}));
|
|
7739
|
+
}
|
|
7740
|
+
async markAgendaTopicsAddressed(ids, cycleNumber) {
|
|
7741
|
+
if (ids.length === 0) return;
|
|
7742
|
+
await this.sql`
|
|
7743
|
+
UPDATE strategy_review_agenda
|
|
7744
|
+
SET status = 'addressed', addressed_at = now(), addressed_in_review = ${cycleNumber}
|
|
7745
|
+
WHERE project_id = ${this.projectId} AND id = ANY(${ids}::uuid[])
|
|
7572
7746
|
`;
|
|
7573
7747
|
}
|
|
7574
7748
|
// -------------------------------------------------------------------------
|
|
@@ -8435,8 +8609,8 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
8435
8609
|
getCycleHealth() {
|
|
8436
8610
|
return this.invoke("getCycleHealth");
|
|
8437
8611
|
}
|
|
8438
|
-
getActiveDecisions() {
|
|
8439
|
-
return this.invoke("getActiveDecisions");
|
|
8612
|
+
getActiveDecisions(options) {
|
|
8613
|
+
return this.invoke("getActiveDecisions", [options ?? {}]);
|
|
8440
8614
|
}
|
|
8441
8615
|
getCycleLog(limit) {
|
|
8442
8616
|
return this.invoke("getCycleLog", [limit]);
|
|
@@ -8761,6 +8935,7 @@ __export(git_exports, {
|
|
|
8761
8935
|
getHeadCommitSha: () => getHeadCommitSha,
|
|
8762
8936
|
getLatestTag: () => getLatestTag,
|
|
8763
8937
|
getOriginRepoSlug: () => getOriginRepoSlug,
|
|
8938
|
+
getPullRequestUrl: () => getPullRequestUrl,
|
|
8764
8939
|
getUnmergedBranches: () => getUnmergedBranches,
|
|
8765
8940
|
gitPull: () => gitPull,
|
|
8766
8941
|
gitPush: () => gitPush,
|
|
@@ -8770,9 +8945,11 @@ __export(git_exports, {
|
|
|
8770
8945
|
isGhAvailable: () => isGhAvailable,
|
|
8771
8946
|
isGitAvailable: () => isGitAvailable,
|
|
8772
8947
|
isGitRepo: () => isGitRepo,
|
|
8948
|
+
listGroupedCycleBranches: () => listGroupedCycleBranches,
|
|
8773
8949
|
mergePullRequest: () => mergePullRequest,
|
|
8774
8950
|
resolveBaseBranch: () => resolveBaseBranch,
|
|
8775
8951
|
runAutoCommit: () => runAutoCommit,
|
|
8952
|
+
squashMergePullRequest: () => squashMergePullRequest,
|
|
8776
8953
|
stageAllAndCommit: () => stageAllAndCommit,
|
|
8777
8954
|
stageDirAndCommit: () => stageDirAndCommit,
|
|
8778
8955
|
tagExists: () => tagExists,
|
|
@@ -9226,6 +9403,68 @@ function runAutoCommit(projectRoot, commitFn) {
|
|
|
9226
9403
|
return `Auto-commit failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
9227
9404
|
}
|
|
9228
9405
|
}
|
|
9406
|
+
function getPullRequestUrl(cwd, branch) {
|
|
9407
|
+
try {
|
|
9408
|
+
const output = execFileSync(
|
|
9409
|
+
"gh",
|
|
9410
|
+
["pr", "view", branch, "--json", "url", "--jq", ".url"],
|
|
9411
|
+
{ cwd, encoding: "utf-8" }
|
|
9412
|
+
).trim();
|
|
9413
|
+
return output || null;
|
|
9414
|
+
} catch {
|
|
9415
|
+
return null;
|
|
9416
|
+
}
|
|
9417
|
+
}
|
|
9418
|
+
function squashMergePullRequest(cwd, branch) {
|
|
9419
|
+
const repo = getOriginRepoSlug(cwd);
|
|
9420
|
+
const baseArgs = ["pr", "merge", branch, "--squash", "--delete-branch"];
|
|
9421
|
+
if (repo) baseArgs.push("--repo", repo);
|
|
9422
|
+
for (let attempt = 1; attempt <= MERGE_MAX_RETRIES; attempt++) {
|
|
9423
|
+
try {
|
|
9424
|
+
execFileSync("gh", baseArgs, { cwd, encoding: "utf-8" });
|
|
9425
|
+
return { success: true, message: `Squash-merged PR for '${branch}' and deleted branch.` };
|
|
9426
|
+
} catch (err) {
|
|
9427
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
9428
|
+
if (msg.includes("not mergeable") && attempt < MERGE_MAX_RETRIES) {
|
|
9429
|
+
sleepSync(MERGE_RETRY_DELAY_MS);
|
|
9430
|
+
continue;
|
|
9431
|
+
}
|
|
9432
|
+
return { success: false, message: `PR squash-merge failed: ${msg}` };
|
|
9433
|
+
}
|
|
9434
|
+
}
|
|
9435
|
+
return { success: false, message: "PR squash-merge failed: max retries exceeded" };
|
|
9436
|
+
}
|
|
9437
|
+
function listGroupedCycleBranches(cwd, cycleNum, baseBranch) {
|
|
9438
|
+
const prefix = `feat/cycle-${cycleNum}-`;
|
|
9439
|
+
try {
|
|
9440
|
+
const remoteOutput = execFileSync(
|
|
9441
|
+
"git",
|
|
9442
|
+
["ls-remote", "--heads", "origin", `${prefix}*`],
|
|
9443
|
+
{ cwd, encoding: "utf-8" }
|
|
9444
|
+
).trim();
|
|
9445
|
+
if (!remoteOutput) return [];
|
|
9446
|
+
const remoteBranches = remoteOutput.split("\n").map((line) => line.split(" ")[1]?.replace("refs/heads/", "").trim()).filter((b2) => !!b2 && b2.startsWith(prefix));
|
|
9447
|
+
return remoteBranches.filter((branch) => {
|
|
9448
|
+
try {
|
|
9449
|
+
const branchTip = execFileSync(
|
|
9450
|
+
"git",
|
|
9451
|
+
["rev-parse", `origin/${branch}`],
|
|
9452
|
+
{ cwd, encoding: "utf-8" }
|
|
9453
|
+
).trim();
|
|
9454
|
+
execFileSync(
|
|
9455
|
+
"git",
|
|
9456
|
+
["merge-base", "--is-ancestor", branchTip, baseBranch],
|
|
9457
|
+
{ cwd, stdio: "ignore" }
|
|
9458
|
+
);
|
|
9459
|
+
return false;
|
|
9460
|
+
} catch {
|
|
9461
|
+
return true;
|
|
9462
|
+
}
|
|
9463
|
+
});
|
|
9464
|
+
} catch {
|
|
9465
|
+
return getUnmergedBranches(cwd, baseBranch).filter((b2) => b2.startsWith(prefix));
|
|
9466
|
+
}
|
|
9467
|
+
}
|
|
9229
9468
|
function getFilesChangedFromBase(cwd, baseBranch) {
|
|
9230
9469
|
try {
|
|
9231
9470
|
const mergeBase = execFileSync("git", ["merge-base", baseBranch, "HEAD"], { cwd, encoding: "utf-8" }).trim();
|
|
@@ -9277,6 +9516,8 @@ function loadConfig() {
|
|
|
9277
9516
|
const lightMode = process.env.PAPI_LIGHT_MODE === "true";
|
|
9278
9517
|
const projectOwner = process.env.PAPI_OWNER ?? "Cathal";
|
|
9279
9518
|
const skipProjectSpecificRules = process.env.PAPI_SKIP_PROJECT_RULES === "true";
|
|
9519
|
+
const userId = process.env.PAPI_USER_ID || void 0;
|
|
9520
|
+
const telemetryEnabled = process.env.PAPI_TELEMETRY !== "off" && process.env.PAPI_TELEMETRY !== "false";
|
|
9280
9521
|
const papiEndpoint = process.env.PAPI_ENDPOINT;
|
|
9281
9522
|
const dataEndpoint = process.env.PAPI_DATA_ENDPOINT;
|
|
9282
9523
|
const databaseUrl = process.env.DATABASE_URL;
|
|
@@ -9287,9 +9528,18 @@ function loadConfig() {
|
|
|
9287
9528
|
adapterType = "proxy";
|
|
9288
9529
|
console.error("[papi] PAPI_PROJECT_ID detected \u2014 switching to proxy adapter (md adapter blocked for external users).");
|
|
9289
9530
|
}
|
|
9290
|
-
if (
|
|
9531
|
+
if (adapterType === "md" && !userId) {
|
|
9291
9532
|
throw new Error(
|
|
9292
|
-
|
|
9533
|
+
`PAPI requires a free account to run in local mode.
|
|
9534
|
+
|
|
9535
|
+
Create your account at https://getpapi.ai/setup \u2014 it takes under a minute.
|
|
9536
|
+
Your project data stays on your machine. The account lets PAPI identify you
|
|
9537
|
+
and unlocks dashboard features when you're ready.
|
|
9538
|
+
|
|
9539
|
+
After signing up, add this to your .mcp.json env config:
|
|
9540
|
+
"PAPI_USER_ID": "your-email@example.com"
|
|
9541
|
+
|
|
9542
|
+
Already have an account? Make sure PAPI_USER_ID is set in your .mcp.json env config.`
|
|
9293
9543
|
);
|
|
9294
9544
|
}
|
|
9295
9545
|
return {
|
|
@@ -9303,7 +9553,9 @@ function loadConfig() {
|
|
|
9303
9553
|
papiEndpoint,
|
|
9304
9554
|
lightMode,
|
|
9305
9555
|
projectOwner,
|
|
9306
|
-
skipProjectSpecificRules
|
|
9556
|
+
skipProjectSpecificRules,
|
|
9557
|
+
userId,
|
|
9558
|
+
telemetryEnabled
|
|
9307
9559
|
};
|
|
9308
9560
|
}
|
|
9309
9561
|
|
|
@@ -9396,7 +9648,25 @@ async function createAdapter(optionsOrType, maybePapiDir) {
|
|
|
9396
9648
|
console.error("[papi] Set PAPI_USER_ID in your .mcp.json env to fix this.");
|
|
9397
9649
|
}
|
|
9398
9650
|
}
|
|
9399
|
-
|
|
9651
|
+
let skipCreate = false;
|
|
9652
|
+
if (userId) {
|
|
9653
|
+
const bySlug = await pgAdapter.listProjects({ slug });
|
|
9654
|
+
const userDup = bySlug.find((p) => p.user_id === userId);
|
|
9655
|
+
if (userDup) {
|
|
9656
|
+
console.error(`[papi] \u26A0 Project '${slug}' already exists for this user (id: ${userDup.id}).`);
|
|
9657
|
+
console.error(`[papi] Update PAPI_PROJECT_ID=${userDup.id} in .mcp.json to avoid a duplicate.`);
|
|
9658
|
+
skipCreate = true;
|
|
9659
|
+
}
|
|
9660
|
+
}
|
|
9661
|
+
if (!skipCreate) {
|
|
9662
|
+
await pgAdapter.createProject({ id: projectId, slug, name: slug, papi_dir: papiDir, user_id: userId });
|
|
9663
|
+
}
|
|
9664
|
+
} else if (existing.user_id) {
|
|
9665
|
+
const configuredUserId = process.env["PAPI_USER_ID"] ?? detectUserId();
|
|
9666
|
+
if (configuredUserId && existing.user_id !== configuredUserId) {
|
|
9667
|
+
console.error(`[papi] \u26A0 PAPI_PROJECT_ID=${projectId} belongs to a different user.`);
|
|
9668
|
+
console.error("[papi] Run papi setup or update PAPI_PROJECT_ID in .mcp.json.");
|
|
9669
|
+
}
|
|
9400
9670
|
}
|
|
9401
9671
|
await pgAdapter.close();
|
|
9402
9672
|
} catch {
|
|
@@ -10299,6 +10569,20 @@ async function sendSlackWebhook(webhookUrl, summary, header = "PAPI Strategy Rev
|
|
|
10299
10569
|
}
|
|
10300
10570
|
|
|
10301
10571
|
// src/prompts.ts
|
|
10572
|
+
var AD_REJECTION_RULES = `**AD Minting Guard \u2014 REJECT observations dressed as decisions.**
|
|
10573
|
+
|
|
10574
|
+
An Active Decision expresses a *stance* the project is taking \u2014 a choice between alternatives that constrains future work. Reject any candidate AD whose body asserts:
|
|
10575
|
+
(a) the existence, identity, or status of a person, user, or external entity (e.g. "User X is building Y", "Customer Z is active");
|
|
10576
|
+
(b) a metric or measurement (e.g. "Signups grew 3x last cycle", "Latency dropped to 200ms");
|
|
10577
|
+
(c) a fact about the current state of the world that could be confirmed or denied by a query rather than challenged by argument (e.g. "External user feedback is now flowing", "The /admin route exists").
|
|
10578
|
+
|
|
10579
|
+
If a candidate AD body could be invalidated by running a SQL query, refreshing a dashboard, or checking a log \u2014 it is an observation, not a decision. Capture it as a build report finding, a dogfood observation, a cycle log note, or a registered doc instead. Do NOT mint it as an AD.
|
|
10580
|
+
|
|
10581
|
+
**Positive example (valid AD):** "Pricing tier strategy: free engine + paid intelligence. Decision: keep cycles free, charge for strategy reviews and analytics. Why: telemetry shows engagement clusters around intelligence surfaces, not engine surfaces." \u2014 this is a stance with alternatives.
|
|
10582
|
+
|
|
10583
|
+
**Negative example (reject):** "External user feedback is now flowing. Stonebridge Systems is actively building." \u2014 this is a fact about the current state of the world. Capture as dogfood/signal observation; do not mint.
|
|
10584
|
+
|
|
10585
|
+
This rule applies to: new ADs proposed during planning (Step 9), strategy review AD updates (section 5), and strategy_change AD updates. If you find an existing AD that violates this rule during housekeeping, propose deleting it (action: "delete") with a one-line rationale.`;
|
|
10302
10586
|
var PLAN_SYSTEM = `You are the PAPI Cycle Planner \u2014 an autonomous planning engine for software projects.
|
|
10303
10587
|
You receive project context and produce a planning cycle output with a BUILD HANDOFF.
|
|
10304
10588
|
|
|
@@ -10524,6 +10808,9 @@ Standard planning cycle with full board review.
|
|
|
10524
10808
|
|
|
10525
10809
|
9. **Active Decisions** \u2014 If any AD needs updating: Type A (confidence change), Type B (modification), or Type C (reversal/supersede).
|
|
10526
10810
|
**AD Quality Bar:** ADs are for product and architecture choices that constrain future work \u2014 technology selections, data model designs, UX principles, strategic positioning. They are NOT for: process preferences (commit style, PR size), configuration choices (linter rules, tab width), or temporary workarounds. If a decision doesn't affect what gets built or how it's architected, it's not an AD. Apply this bar when proposing new ADs and when triaging existing ones.
|
|
10811
|
+
|
|
10812
|
+
${AD_REJECTION_RULES}
|
|
10813
|
+
|
|
10527
10814
|
**\u2192 PERSIST:** EVERY AD you created, updated, or confirmed with changes MUST appear in \`activeDecisions\` array in Part 2. Include the full replacement body with ### heading.
|
|
10528
10815
|
|
|
10529
10816
|
### Operational Quality Rules
|
|
@@ -10761,6 +11048,9 @@ Standard planning cycle with full board review.
|
|
|
10761
11048
|
|
|
10762
11049
|
9. **Active Decisions** \u2014 If any AD needs updating: Type A (confidence change), Type B (modification), or Type C (reversal/supersede).
|
|
10763
11050
|
**AD Quality Bar:** ADs are for product and architecture choices that constrain future work \u2014 technology selections, data model designs, UX principles, strategic positioning. They are NOT for: process preferences (commit style, PR size), configuration choices (linter rules, tab width), or temporary workarounds. If a decision doesn't affect what gets built or how it's architected, it's not an AD. Apply this bar when proposing new ADs and when triaging existing ones.
|
|
11051
|
+
|
|
11052
|
+
${AD_REJECTION_RULES}
|
|
11053
|
+
|
|
10764
11054
|
**\u2192 PERSIST:** EVERY AD you created, updated, or confirmed with changes MUST appear in \`activeDecisions\` array in Part 2. Include the full replacement body with ### heading.
|
|
10765
11055
|
|
|
10766
11056
|
### Operational Quality Rules
|
|
@@ -10835,6 +11125,9 @@ function buildPlanUserMessage(ctx) {
|
|
|
10835
11125
|
}) : PLAN_FULL_INSTRUCTIONS;
|
|
10836
11126
|
parts.push(instructions);
|
|
10837
11127
|
}
|
|
11128
|
+
if (ctx.foundationalTasksGuidance) {
|
|
11129
|
+
parts.push("", ctx.foundationalTasksGuidance);
|
|
11130
|
+
}
|
|
10838
11131
|
if (ctx.skipHandoffs) {
|
|
10839
11132
|
parts.push(
|
|
10840
11133
|
"",
|
|
@@ -11130,6 +11423,8 @@ You MUST cover these 5 sections. Each is mandatory.
|
|
|
11130
11423
|
- Note any hierarchy/phase issues worth correcting (1-2 bullets max)
|
|
11131
11424
|
- Delete ADs that are legacy, process-level, or redundant without discussion
|
|
11132
11425
|
|
|
11426
|
+
${AD_REJECTION_RULES}
|
|
11427
|
+
|
|
11133
11428
|
**Registered Documents:** If a "### Registered Documents" section is present in context, scan it for: (a) research findings that contradict current ADs or strategy, (b) unactioned research that should influence the next plan. Reference relevant docs by title in your review. If unregistered docs are listed, flag 1-2 that look strategically relevant and suggest registering them.
|
|
11134
11429
|
|
|
11135
11430
|
## CONDITIONAL SECTIONS (include only when genuinely useful \u2014 most reviews should have 0-2 of these)
|
|
@@ -11356,6 +11651,9 @@ function buildReviewUserMessage(ctx) {
|
|
|
11356
11651
|
if (ctx.docActionStaleness) {
|
|
11357
11652
|
parts.push("### Doc Action Staleness", "", ctx.docActionStaleness, "");
|
|
11358
11653
|
}
|
|
11654
|
+
if (ctx.pendingAgendaTopics) {
|
|
11655
|
+
parts.push("### Queued Agenda Topics", "", "_Topics queued via `strategy_agenda` since the last review. Address each one in this review \u2014 they will be auto-marked as addressed on apply._", "", ctx.pendingAgendaTopics, "");
|
|
11656
|
+
}
|
|
11359
11657
|
return parts.join("\n");
|
|
11360
11658
|
}
|
|
11361
11659
|
function parseReviewStructuredOutput(raw) {
|
|
@@ -11436,6 +11734,8 @@ The JSON must be valid. Only include ADs that need changes \u2014 omit unchanged
|
|
|
11436
11734
|
For new ADs, use the next available AD number.
|
|
11437
11735
|
The body field must be the COMPLETE replacement text for the AD block (including the ### heading line).
|
|
11438
11736
|
|
|
11737
|
+
${AD_REJECTION_RULES}
|
|
11738
|
+
|
|
11439
11739
|
## PHASE UPDATES
|
|
11440
11740
|
|
|
11441
11741
|
If the strategic change affects the project's phase structure, include a phaseUpdates array.
|
|
@@ -12042,6 +12342,64 @@ function stripTasksForPlan(tasks) {
|
|
|
12042
12342
|
hasHandoff: !!buildHandoff
|
|
12043
12343
|
}));
|
|
12044
12344
|
}
|
|
12345
|
+
var BRIEF_SECTIONS = [
|
|
12346
|
+
{ name: "title", pattern: /^#\s+\S/m },
|
|
12347
|
+
{
|
|
12348
|
+
name: "target audience",
|
|
12349
|
+
pattern: /\b(target users?|audience|for whom|who (it'?s for|uses|it serves|we're building))/i
|
|
12350
|
+
},
|
|
12351
|
+
{
|
|
12352
|
+
name: "problem statement",
|
|
12353
|
+
pattern: /\b(problem|pain point|why it matters|what problem|solves?)/i
|
|
12354
|
+
},
|
|
12355
|
+
{
|
|
12356
|
+
name: "solution / vision",
|
|
12357
|
+
pattern: /\b(solution|approach|vision|what (it|we) do|how it works|value proposition)/i
|
|
12358
|
+
},
|
|
12359
|
+
{
|
|
12360
|
+
name: "GTM / distribution",
|
|
12361
|
+
pattern: /\b(GTM|go[- ]to[- ]market|distribution|channel|pricing|how (users?|people) (discover|find|reach))/i
|
|
12362
|
+
}
|
|
12363
|
+
];
|
|
12364
|
+
function assessBriefThinness(brief) {
|
|
12365
|
+
const TEMPLATE_MARKER = "*Describe your project's core value proposition here.*";
|
|
12366
|
+
const briefWithoutTemplate = brief.replace(TEMPLATE_MARKER, "");
|
|
12367
|
+
const populated = [];
|
|
12368
|
+
const missing = [];
|
|
12369
|
+
for (const section of BRIEF_SECTIONS) {
|
|
12370
|
+
if (section.pattern.test(briefWithoutTemplate)) populated.push(section.name);
|
|
12371
|
+
else missing.push(section.name);
|
|
12372
|
+
}
|
|
12373
|
+
return { populatedSections: populated, missingSections: missing };
|
|
12374
|
+
}
|
|
12375
|
+
function computeFoundationalTasksGuidance(cycleNumber, brief) {
|
|
12376
|
+
if (cycleNumber > 1) return void 0;
|
|
12377
|
+
const { populatedSections, missingSections } = assessBriefThinness(brief);
|
|
12378
|
+
if (populatedSections.length >= 4) return void 0;
|
|
12379
|
+
const populatedList = populatedSections.length > 0 ? populatedSections.join(", ") : "none";
|
|
12380
|
+
const missingList = missingSections.join(", ");
|
|
12381
|
+
return [
|
|
12382
|
+
"## FOUNDATIONAL TASKS GUIDANCE",
|
|
12383
|
+
"",
|
|
12384
|
+
`The project brief is thin \u2014 only ${populatedSections.length} of 5 key sections are clearly populated (${populatedList}). Missing or weak sections: ${missingList}.`,
|
|
12385
|
+
"",
|
|
12386
|
+
"Generate 3-5 foundational research/discovery tasks targeting the MISSING sections. These help the Owner fill in their brief before the product gets built on top of shaky foundations.",
|
|
12387
|
+
"",
|
|
12388
|
+
"Rules for foundational tasks:",
|
|
12389
|
+
"- **ADDITIONS, not replacements.** Include them ALONGSIDE the user's backlog items. If the user submitted 3 ideas, the cycle should have 3 user items + 3-5 foundational items (total 6-8 tasks).",
|
|
12390
|
+
'- **Target specific missing sections.** Do NOT generate "refine audience" if audience is already populated. Each foundational task must close a specific gap above.',
|
|
12391
|
+
"- **Task type: `research` or `discovery`** \u2014 deliverable is a findings doc, not shipped code.",
|
|
12392
|
+
"- **Effort: S or M** \u2014 foundational work should be timeboxed, not open-ended.",
|
|
12393
|
+
'- **WHY in the handoff must tie to the specific gap.** Example: "Brief is thin on GTM \u2014 knowing how users discover the product is a prerequisite for scoping distribution work." Not generic.',
|
|
12394
|
+
"- **Cap at 5 foundational tasks** to avoid drowning the user.",
|
|
12395
|
+
"- **Cycle 2+ will NOT receive this guidance** \u2014 even if the brief stays thin. Foundational work is a one-shot onboarding primitive, not a recurring pattern.",
|
|
12396
|
+
"",
|
|
12397
|
+
"In the structured output:",
|
|
12398
|
+
"- Emit foundational tasks via the `newTasks` array (mark `reviewed: true`, type `research` or `discovery`).",
|
|
12399
|
+
"- Generate full BUILD HANDOFFs for each in `cycleHandoffs` \u2014 use `new-N` IDs to reference them.",
|
|
12400
|
+
"- User-submitted backlog tasks remain in `cycleHandoffs` as usual \u2014 do NOT drop them in favour of foundational tasks."
|
|
12401
|
+
].join("\n");
|
|
12402
|
+
}
|
|
12045
12403
|
function detectBoardFlags(tasks) {
|
|
12046
12404
|
let hasBugTasks = false;
|
|
12047
12405
|
let hasResearchTasks = false;
|
|
@@ -12320,7 +12678,8 @@ ${lines.join("\n")}`;
|
|
|
12320
12678
|
preAssignedTasks: preAssignedTextLean,
|
|
12321
12679
|
recentlyShippedCapabilities: recentlyShippedLean,
|
|
12322
12680
|
strategyReviewCadence,
|
|
12323
|
-
candidateTaskFullNotes: candidateTaskFullNotesLean
|
|
12681
|
+
candidateTaskFullNotes: candidateTaskFullNotesLean,
|
|
12682
|
+
foundationalTasksGuidance: computeFoundationalTasksGuidance(health.totalCycles, productBrief)
|
|
12324
12683
|
};
|
|
12325
12684
|
const { label: leanTierLabel } = applyContextTier(ctx2, health.totalCycles);
|
|
12326
12685
|
ctx2.contextTier = leanTierLabel;
|
|
@@ -12482,7 +12841,8 @@ ${logLines}`);
|
|
|
12482
12841
|
preAssignedTasks: preAssignedText,
|
|
12483
12842
|
recentlyShippedCapabilities: formatRecentlyShippedCapabilities(reports),
|
|
12484
12843
|
strategyReviewCadence: strategyReviewCadenceFull,
|
|
12485
|
-
candidateTaskFullNotes: formatCandidateTaskFullNotes(plannerTasks)
|
|
12844
|
+
candidateTaskFullNotes: formatCandidateTaskFullNotes(plannerTasks),
|
|
12845
|
+
foundationalTasksGuidance: computeFoundationalTasksGuidance(health.totalCycles, productBrief)
|
|
12486
12846
|
};
|
|
12487
12847
|
const { label: fullTierLabel } = applyContextTier(ctx, health.totalCycles);
|
|
12488
12848
|
ctx.contextTier = fullTierLabel;
|
|
@@ -13838,7 +14198,8 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
|
|
|
13838
14198
|
docsWithPendingActions
|
|
13839
14199
|
] = await Promise.all([
|
|
13840
14200
|
adapter2.readProductBrief(),
|
|
13841
|
-
|
|
14201
|
+
// Strategy review needs to see retired ADs to triage/restore them as needed.
|
|
14202
|
+
adapter2.getActiveDecisions({ includeRetired: true }),
|
|
13842
14203
|
adapter2.getBuildReportsSince(lastReviewCycleNum),
|
|
13843
14204
|
adapter2.getCycleLogSince(lastReviewCycleNum),
|
|
13844
14205
|
adapter2.queryBoard({
|
|
@@ -13863,6 +14224,7 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
|
|
|
13863
14224
|
// Doc registry — docs with pending actions for staleness audit
|
|
13864
14225
|
adapter2.searchDocs?.({ hasPendingActions: true, limit: 20 })?.catch(() => []) ?? Promise.resolve([])
|
|
13865
14226
|
]);
|
|
14227
|
+
const pendingAgendaTopics = await (adapter2.getPendingAgendaTopics?.().catch(() => []) ?? Promise.resolve([]));
|
|
13866
14228
|
const tasks = [...activeTasks, ...recentDoneTasks];
|
|
13867
14229
|
const existingAdIds = new Set(decisions.map((d) => d.id));
|
|
13868
14230
|
const survivingPendingRecs = [];
|
|
@@ -14100,6 +14462,15 @@ ${deferred.join("\n")}`);
|
|
|
14100
14462
|
}
|
|
14101
14463
|
}
|
|
14102
14464
|
} catch {
|
|
14465
|
+
}
|
|
14466
|
+
let pendingAgendaText;
|
|
14467
|
+
if (pendingAgendaTopics.length > 0) {
|
|
14468
|
+
const lines = pendingAgendaTopics.map((t, i) => {
|
|
14469
|
+
const cycleSuffix = t.sourceCycle != null ? ` (queued Cycle ${t.sourceCycle})` : "";
|
|
14470
|
+
return `${i + 1}. ${t.topic} _[${t.source}${cycleSuffix}]_`;
|
|
14471
|
+
});
|
|
14472
|
+
pendingAgendaText = `${pendingAgendaTopics.length} topic(s) queued via strategy_agenda:
|
|
14473
|
+
${lines.join("\n")}`;
|
|
14103
14474
|
}
|
|
14104
14475
|
logDataSourceSummary("strategy_review_audit", [
|
|
14105
14476
|
{ label: "discoveryCanvas", hasData: discoveryCanvasText !== void 0 },
|
|
@@ -14140,7 +14511,8 @@ ${deferred.join("\n")}`);
|
|
|
14140
14511
|
recentPlans: recentPlansText,
|
|
14141
14512
|
unregisteredDocs: unregisteredDocsText,
|
|
14142
14513
|
taskComments: taskCommentsText,
|
|
14143
|
-
docActionStaleness: docActionStalenessText
|
|
14514
|
+
docActionStaleness: docActionStalenessText,
|
|
14515
|
+
pendingAgendaTopics: pendingAgendaText
|
|
14144
14516
|
};
|
|
14145
14517
|
const BUDGET_SOFT2 = 5e4;
|
|
14146
14518
|
const BUDGET_HARD2 = 6e4;
|
|
@@ -14329,7 +14701,7 @@ ${cleanContent}`;
|
|
|
14329
14701
|
try {
|
|
14330
14702
|
const recs = extractRecommendations(data, cycleNumber);
|
|
14331
14703
|
if (recs.length > 0) {
|
|
14332
|
-
const existingAds = await adapter2.getActiveDecisions().catch(() => []);
|
|
14704
|
+
const existingAds = await adapter2.getActiveDecisions({ includeRetired: true }).catch(() => []);
|
|
14333
14705
|
const existingAdIds = new Set(existingAds.map((ad) => ad.id));
|
|
14334
14706
|
const filteredRecs = recs.filter((rec) => {
|
|
14335
14707
|
if (rec.target && /^AD-\d+$/.test(rec.target)) {
|
|
@@ -14455,6 +14827,15 @@ async function processReviewOutput(adapter2, rawOutput, cycleNumber) {
|
|
|
14455
14827
|
await adapter2.clearPendingReviewResponse?.();
|
|
14456
14828
|
} catch {
|
|
14457
14829
|
}
|
|
14830
|
+
try {
|
|
14831
|
+
if (adapter2.getPendingAgendaTopics && adapter2.markAgendaTopicsAddressed) {
|
|
14832
|
+
const pending = await adapter2.getPendingAgendaTopics();
|
|
14833
|
+
if (pending.length > 0) {
|
|
14834
|
+
await adapter2.markAgendaTopicsAddressed(pending.map((t) => t.id), cycleNumber);
|
|
14835
|
+
}
|
|
14836
|
+
}
|
|
14837
|
+
} catch {
|
|
14838
|
+
}
|
|
14458
14839
|
const webhookUrl = process.env.PAPI_SLACK_WEBHOOK_URL;
|
|
14459
14840
|
slackWarning = await sendSlackWebhook(webhookUrl, buildSlackSummary(data));
|
|
14460
14841
|
}
|
|
@@ -14854,7 +15235,8 @@ async function prepareStrategyChange(adapter2, text) {
|
|
|
14854
15235
|
try {
|
|
14855
15236
|
const [brief, decisions, readPhases, boardTasks, reports, previousReviews] = await Promise.all([
|
|
14856
15237
|
adapter2.readProductBrief(),
|
|
14857
|
-
|
|
15238
|
+
// Strategy review needs ALL ADs (live + retired) for housekeeping.
|
|
15239
|
+
adapter2.getActiveDecisions({ includeRetired: true }),
|
|
14858
15240
|
adapter2.readPhases(),
|
|
14859
15241
|
adapter2.queryBoard().catch(() => []),
|
|
14860
15242
|
adapter2.getRecentBuildReports(15).catch(() => []),
|
|
@@ -14899,7 +15281,7 @@ async function captureDecision(adapter2, input) {
|
|
|
14899
15281
|
adId = input.adId;
|
|
14900
15282
|
adAction = "updated";
|
|
14901
15283
|
} else {
|
|
14902
|
-
const existingAds = await adapter2.getActiveDecisions();
|
|
15284
|
+
const existingAds = await adapter2.getActiveDecisions({ includeRetired: true });
|
|
14903
15285
|
const maxNum = existingAds.reduce((max, ad) => {
|
|
14904
15286
|
const match = ad.id.match(/^AD-(\d+)$/);
|
|
14905
15287
|
return match ? Math.max(max, parseInt(match[1], 10)) : max;
|
|
@@ -14975,6 +15357,34 @@ var strategyReviewTool = {
|
|
|
14975
15357
|
required: []
|
|
14976
15358
|
}
|
|
14977
15359
|
};
|
|
15360
|
+
var strategyAgendaTool = {
|
|
15361
|
+
name: "strategy_agenda",
|
|
15362
|
+
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.',
|
|
15363
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
15364
|
+
inputSchema: {
|
|
15365
|
+
type: "object",
|
|
15366
|
+
properties: {
|
|
15367
|
+
mode: {
|
|
15368
|
+
type: "string",
|
|
15369
|
+
enum: ["add", "list"],
|
|
15370
|
+
description: '"add" to queue a topic (requires `topic`). "list" returns all pending topics. Defaults to "list" when omitted.'
|
|
15371
|
+
},
|
|
15372
|
+
topic: {
|
|
15373
|
+
type: "string",
|
|
15374
|
+
description: 'The topic to queue (mode "add" only). One sentence describing what the next strategy review should consider.'
|
|
15375
|
+
},
|
|
15376
|
+
source: {
|
|
15377
|
+
type: "string",
|
|
15378
|
+
description: 'Optional origin label \u2014 e.g. "manual", "carry-forward", "idea". Defaults to "manual".'
|
|
15379
|
+
},
|
|
15380
|
+
source_cycle: {
|
|
15381
|
+
type: "number",
|
|
15382
|
+
description: 'Optional cycle number this topic originated from (mode "add" only).'
|
|
15383
|
+
}
|
|
15384
|
+
},
|
|
15385
|
+
required: []
|
|
15386
|
+
}
|
|
15387
|
+
};
|
|
14978
15388
|
var strategyChangeTool = {
|
|
14979
15389
|
name: "strategy_change",
|
|
14980
15390
|
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.',
|
|
@@ -15131,6 +15541,49 @@ ${result.userMessage}
|
|
|
15131
15541
|
return errorResponse(err instanceof Error ? err.message : String(err));
|
|
15132
15542
|
}
|
|
15133
15543
|
}
|
|
15544
|
+
async function handleStrategyAgenda(adapter2, _config, args) {
|
|
15545
|
+
const mode = args.mode ?? "list";
|
|
15546
|
+
if (!adapter2.addAgendaTopic || !adapter2.getPendingAgendaTopics) {
|
|
15547
|
+
return errorResponse("strategy_agenda is not supported by the current adapter.");
|
|
15548
|
+
}
|
|
15549
|
+
try {
|
|
15550
|
+
if (mode === "add") {
|
|
15551
|
+
const topic = args.topic;
|
|
15552
|
+
if (!topic || !topic.trim()) {
|
|
15553
|
+
return errorResponse('topic is required for mode "add". Describe what the next strategy review should consider.');
|
|
15554
|
+
}
|
|
15555
|
+
const source = (args.source ?? "manual").trim() || "manual";
|
|
15556
|
+
const sourceCycle = typeof args.source_cycle === "number" ? args.source_cycle : void 0;
|
|
15557
|
+
const entry = await adapter2.addAgendaTopic({ topic: topic.trim(), source, sourceCycle });
|
|
15558
|
+
return textResponse(
|
|
15559
|
+
`**Agenda Topic Queued**
|
|
15560
|
+
|
|
15561
|
+
${entry.topic}
|
|
15562
|
+
|
|
15563
|
+
Source: ${entry.source}${entry.sourceCycle != null ? ` (Cycle ${entry.sourceCycle})` : ""}
|
|
15564
|
+
ID: ${entry.id}
|
|
15565
|
+
|
|
15566
|
+
This topic will surface in the next \`strategy_review\`.`
|
|
15567
|
+
);
|
|
15568
|
+
}
|
|
15569
|
+
const topics = await adapter2.getPendingAgendaTopics();
|
|
15570
|
+
if (topics.length === 0) {
|
|
15571
|
+
return textResponse('No pending agenda topics. Use `strategy_agenda` with `mode: "add"` to queue one.');
|
|
15572
|
+
}
|
|
15573
|
+
const lines = topics.map((t, i) => {
|
|
15574
|
+
const cycleSuffix = t.sourceCycle != null ? ` (Cycle ${t.sourceCycle})` : "";
|
|
15575
|
+
return `${i + 1}. ${t.topic}
|
|
15576
|
+
_source: ${t.source}${cycleSuffix} \xB7 queued ${t.createdAt.slice(0, 10)}_`;
|
|
15577
|
+
});
|
|
15578
|
+
return textResponse(
|
|
15579
|
+
`**Pending Agenda (${topics.length})** \u2014 surfaces at next strategy review
|
|
15580
|
+
|
|
15581
|
+
${lines.join("\n\n")}`
|
|
15582
|
+
);
|
|
15583
|
+
} catch (err) {
|
|
15584
|
+
return errorResponse(err instanceof Error ? err.message : String(err));
|
|
15585
|
+
}
|
|
15586
|
+
}
|
|
15134
15587
|
async function handleStrategyChange(adapter2, _config, args) {
|
|
15135
15588
|
const toolMode = args.mode;
|
|
15136
15589
|
try {
|
|
@@ -15402,7 +15855,7 @@ var boardArchiveTool = {
|
|
|
15402
15855
|
};
|
|
15403
15856
|
var boardEditTool = {
|
|
15404
15857
|
name: "board_edit",
|
|
15405
|
-
description: "Edit fields on an existing task. Supports title, priority, complexity, module, epic, phase, notes, status, and maturity. Pass task_id plus any fields to update. Does not call the Anthropic API.",
|
|
15858
|
+
description: "Edit fields on an existing task. Supports title, priority, complexity, module, epic, phase, notes (with notes_mode for append/replace/clear), status, and maturity. Pass task_id plus any fields to update. Does not call the Anthropic API.",
|
|
15406
15859
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
15407
15860
|
inputSchema: {
|
|
15408
15861
|
type: "object",
|
|
@@ -15439,7 +15892,12 @@ var boardEditTool = {
|
|
|
15439
15892
|
},
|
|
15440
15893
|
notes: {
|
|
15441
15894
|
type: "string",
|
|
15442
|
-
description: "
|
|
15895
|
+
description: "Note content. Default behaviour is append \u2014 see notes_mode to control."
|
|
15896
|
+
},
|
|
15897
|
+
notes_mode: {
|
|
15898
|
+
type: "string",
|
|
15899
|
+
enum: ["append", "replace", "clear"],
|
|
15900
|
+
description: "How to apply the notes value. append (default) = add a new dated entry above existing notes; replace = overwrite all existing notes; clear = empty the notes field (notes value ignored)."
|
|
15443
15901
|
},
|
|
15444
15902
|
status: {
|
|
15445
15903
|
type: "string",
|
|
@@ -15642,6 +16100,10 @@ async function handleBoardEdit(adapter2, args) {
|
|
|
15642
16100
|
changes.push(field);
|
|
15643
16101
|
}
|
|
15644
16102
|
}
|
|
16103
|
+
const notesMode = args.notes_mode;
|
|
16104
|
+
if (notesMode === "clear" && !changes.includes("notes")) {
|
|
16105
|
+
changes.push("notes");
|
|
16106
|
+
}
|
|
15645
16107
|
if (changes.length === 0) {
|
|
15646
16108
|
return errorResponse("No fields to update. Pass at least one field (title, priority, complexity, module, epic, phase, notes, status, maturity).");
|
|
15647
16109
|
}
|
|
@@ -15650,6 +16112,35 @@ async function handleBoardEdit(adapter2, args) {
|
|
|
15650
16112
|
if (!task) {
|
|
15651
16113
|
return errorResponse(`Task ${taskId} not found.`);
|
|
15652
16114
|
}
|
|
16115
|
+
if (changes.includes("notes")) {
|
|
16116
|
+
const incoming = args.notes ?? "";
|
|
16117
|
+
const existing = task.notes ?? "";
|
|
16118
|
+
const mode = notesMode ?? "append";
|
|
16119
|
+
if (mode === "clear") {
|
|
16120
|
+
updates.notes = "";
|
|
16121
|
+
} else if (mode === "replace") {
|
|
16122
|
+
updates.notes = incoming;
|
|
16123
|
+
} else {
|
|
16124
|
+
const trimmed = incoming.trim();
|
|
16125
|
+
if (trimmed.length === 0) {
|
|
16126
|
+
delete updates.notes;
|
|
16127
|
+
const idx = changes.indexOf("notes");
|
|
16128
|
+
if (idx >= 0) changes.splice(idx, 1);
|
|
16129
|
+
} else {
|
|
16130
|
+
const health = await adapter2.getCycleHealth().catch(() => null);
|
|
16131
|
+
const activeCycle = health?.totalCycles ?? null;
|
|
16132
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
16133
|
+
const stamp = activeCycle != null ? `[C${activeCycle} ${date}]` : `[${date}]`;
|
|
16134
|
+
const entry = `${stamp} ${trimmed}`;
|
|
16135
|
+
updates.notes = existing.trim().length > 0 ? `${entry}
|
|
16136
|
+
|
|
16137
|
+
${existing}` : entry;
|
|
16138
|
+
}
|
|
16139
|
+
}
|
|
16140
|
+
if (changes.length === 0) {
|
|
16141
|
+
return errorResponse("No fields to update. Pass at least one field (title, priority, complexity, module, epic, phase, notes, status, maturity).");
|
|
16142
|
+
}
|
|
16143
|
+
}
|
|
15653
16144
|
if (updates.status === "Backlog" && task.cycle != null) {
|
|
15654
16145
|
updates.cycle = void 0;
|
|
15655
16146
|
updates.cycle = null;
|
|
@@ -16671,9 +17162,8 @@ async function prepareSetup(adapter2, config2, input) {
|
|
|
16671
17162
|
);
|
|
16672
17163
|
}
|
|
16673
17164
|
const TEMPLATE_MARKER = "*Describe your project's core value proposition here.*";
|
|
16674
|
-
|
|
16675
|
-
|
|
16676
|
-
}
|
|
17165
|
+
const briefHasRealContent = existingBrief.trim().length > 0 && !existingBrief.includes(TEMPLATE_MARKER);
|
|
17166
|
+
const briefAlreadyExists = briefHasRealContent && !input.force;
|
|
16677
17167
|
const detectedCodebaseType = detectCodebaseType(config2.projectRoot);
|
|
16678
17168
|
const autoDetected = input.existingProject === void 0 || input.existingProject === false;
|
|
16679
17169
|
const isExistingProject = input.existingProject === true || autoDetected && detectedCodebaseType === "existing_codebase";
|
|
@@ -16686,7 +17176,7 @@ async function prepareSetup(adapter2, config2, input) {
|
|
|
16686
17176
|
}
|
|
16687
17177
|
codebaseSummary = formatCodebaseSummary(scan, sourceContents);
|
|
16688
17178
|
}
|
|
16689
|
-
const briefPrompt = {
|
|
17179
|
+
const briefPrompt = briefAlreadyExists ? void 0 : {
|
|
16690
17180
|
system: PRODUCT_BRIEF_SYSTEM,
|
|
16691
17181
|
user: buildProductBriefPrompt({
|
|
16692
17182
|
projectName: input.projectName,
|
|
@@ -16740,15 +17230,29 @@ async function prepareSetup(adapter2, config2, input) {
|
|
|
16740
17230
|
initialTasksPrompt,
|
|
16741
17231
|
codebaseSummary,
|
|
16742
17232
|
detectedCodebaseType,
|
|
16743
|
-
autoDetected: autoDetected && detectedCodebaseType !== "new_project"
|
|
17233
|
+
autoDetected: autoDetected && detectedCodebaseType !== "new_project",
|
|
17234
|
+
briefAlreadyExists
|
|
16744
17235
|
};
|
|
16745
17236
|
}
|
|
16746
17237
|
async function applySetup(adapter2, config2, input, briefText, adSeedText, conventionsText, initialTasksText) {
|
|
16747
17238
|
const createdProject = await scaffoldPapiDir(adapter2, config2, input);
|
|
16748
|
-
|
|
16749
|
-
|
|
17239
|
+
const TEMPLATE_MARKER = "*Describe your project's core value proposition here.*";
|
|
17240
|
+
let effectiveBriefText = briefText;
|
|
17241
|
+
if (!effectiveBriefText.trim()) {
|
|
17242
|
+
let existingBrief = "";
|
|
17243
|
+
try {
|
|
17244
|
+
existingBrief = await adapter2.readProductBrief();
|
|
17245
|
+
} catch {
|
|
17246
|
+
existingBrief = "";
|
|
17247
|
+
}
|
|
17248
|
+
const existingIsReal = existingBrief.trim().length > 0 && !existingBrief.includes(TEMPLATE_MARKER);
|
|
17249
|
+
if (existingIsReal && !input.force) {
|
|
17250
|
+
effectiveBriefText = existingBrief;
|
|
17251
|
+
} else {
|
|
17252
|
+
throw new Error("brief_response is required and cannot be empty.");
|
|
17253
|
+
}
|
|
16750
17254
|
}
|
|
16751
|
-
const { seededAds, warnings } = await applySetupOutputs(adapter2, config2, input,
|
|
17255
|
+
const { seededAds, warnings } = await applySetupOutputs(adapter2, config2, input, effectiveBriefText, adSeedText, conventionsText);
|
|
16752
17256
|
let createdTasks = 0;
|
|
16753
17257
|
if (initialTasksText?.trim()) {
|
|
16754
17258
|
try {
|
|
@@ -16974,10 +17478,7 @@ PAPI needs just 3 things: project name, what it does, and who it's for.`
|
|
|
16974
17478
|
const input = extractInput(args);
|
|
16975
17479
|
try {
|
|
16976
17480
|
if (toolMode === "apply") {
|
|
16977
|
-
const briefResponse = args.brief_response;
|
|
16978
|
-
if (!briefResponse || !briefResponse.trim()) {
|
|
16979
|
-
return errorResponse('brief_response is required for mode "apply".');
|
|
16980
|
-
}
|
|
17481
|
+
const briefResponse = args.brief_response ?? "";
|
|
16981
17482
|
const adSeedResponse = args.ad_seed_response;
|
|
16982
17483
|
const conventionsResponse = args.conventions_response;
|
|
16983
17484
|
const initialTasksResponse = args.initial_tasks_response;
|
|
@@ -17010,6 +17511,12 @@ PAPI needs just 3 things: project name, what it does, and who it's for.`
|
|
|
17010
17511
|
""
|
|
17011
17512
|
);
|
|
17012
17513
|
}
|
|
17514
|
+
if (result.briefAlreadyExists) {
|
|
17515
|
+
sections.push(
|
|
17516
|
+
`**Existing brief detected:** this project already has a Product Brief (e.g. from the web wizard). Setup will preserve it and skip brief generation \u2014 no \`brief_response\` needed in the apply step. To overwrite the brief instead, re-run setup with \`force: true\`.`,
|
|
17517
|
+
""
|
|
17518
|
+
);
|
|
17519
|
+
}
|
|
17013
17520
|
if (inferredDefaults.length > 0) {
|
|
17014
17521
|
sections.push(
|
|
17015
17522
|
`**Defaults applied** (override by re-running setup with these fields):`,
|
|
@@ -17020,30 +17527,37 @@ PAPI needs just 3 things: project name, what it does, and who it's for.`
|
|
|
17020
17527
|
sections.push(
|
|
17021
17528
|
`Generate the outputs below, then call \`setup\` again with:`,
|
|
17022
17529
|
`- \`mode\`: "apply"`,
|
|
17023
|
-
`- \`brief_response\`: your Product Brief markdown
|
|
17530
|
+
result.briefPrompt ? `- \`brief_response\`: your Product Brief markdown` : "",
|
|
17024
17531
|
result.adSeedPrompt ? `- \`ad_seed_response\`: your AD seed JSON array` : "",
|
|
17025
17532
|
result.conventionsPrompt ? `- \`conventions_response\`: your conventions markdown` : "",
|
|
17026
17533
|
result.initialTasksPrompt ? `- \`initial_tasks_response\`: your initial tasks JSON array` : "",
|
|
17027
17534
|
`- Plus all the original setup fields (project_name, description, target_users${isExisting ? ", existing_project: true" : ""})`,
|
|
17028
17535
|
"",
|
|
17029
|
-
|
|
17030
|
-
|
|
17031
|
-
|
|
17032
|
-
|
|
17033
|
-
|
|
17536
|
+
`---`
|
|
17537
|
+
);
|
|
17538
|
+
let sectionNum = 0;
|
|
17539
|
+
if (result.briefPrompt) {
|
|
17540
|
+
sectionNum++;
|
|
17541
|
+
sections.push(
|
|
17542
|
+
"",
|
|
17543
|
+
`### ${sectionNum}. Product Brief`,
|
|
17544
|
+
"",
|
|
17545
|
+
`<system_prompt>
|
|
17034
17546
|
${result.briefPrompt.system}
|
|
17035
17547
|
</system_prompt>`,
|
|
17036
|
-
|
|
17037
|
-
|
|
17548
|
+
"",
|
|
17549
|
+
`<context>
|
|
17038
17550
|
${result.briefPrompt.user}
|
|
17039
17551
|
</context>`
|
|
17040
|
-
|
|
17552
|
+
);
|
|
17553
|
+
}
|
|
17041
17554
|
if (result.adSeedPrompt) {
|
|
17555
|
+
sectionNum++;
|
|
17042
17556
|
sections.push(
|
|
17043
17557
|
"",
|
|
17044
17558
|
`---`,
|
|
17045
17559
|
"",
|
|
17046
|
-
`###
|
|
17560
|
+
`### ${sectionNum}. Active Decision Seeds`,
|
|
17047
17561
|
"",
|
|
17048
17562
|
`<system_prompt>
|
|
17049
17563
|
${result.adSeedPrompt.system}
|
|
@@ -17055,11 +17569,12 @@ ${result.adSeedPrompt.user}
|
|
|
17055
17569
|
);
|
|
17056
17570
|
}
|
|
17057
17571
|
if (result.conventionsPrompt) {
|
|
17572
|
+
sectionNum++;
|
|
17058
17573
|
sections.push(
|
|
17059
17574
|
"",
|
|
17060
17575
|
`---`,
|
|
17061
17576
|
"",
|
|
17062
|
-
`###
|
|
17577
|
+
`### ${sectionNum}. Conventions`,
|
|
17063
17578
|
"",
|
|
17064
17579
|
`<system_prompt>
|
|
17065
17580
|
${result.conventionsPrompt.system}
|
|
@@ -17071,11 +17586,12 @@ ${result.conventionsPrompt.user}
|
|
|
17071
17586
|
);
|
|
17072
17587
|
}
|
|
17073
17588
|
if (result.initialTasksPrompt) {
|
|
17589
|
+
sectionNum++;
|
|
17074
17590
|
sections.push(
|
|
17075
17591
|
"",
|
|
17076
17592
|
`---`,
|
|
17077
17593
|
"",
|
|
17078
|
-
`###
|
|
17594
|
+
`### ${sectionNum}. Initial Backlog Tasks`,
|
|
17079
17595
|
"",
|
|
17080
17596
|
`<system_prompt>
|
|
17081
17597
|
${result.initialTasksPrompt.system}
|
|
@@ -17182,6 +17698,23 @@ function isNoHandoffError(err) {
|
|
|
17182
17698
|
function isBlockedError(err) {
|
|
17183
17699
|
return err instanceof Error && err.code === "BLOCKED";
|
|
17184
17700
|
}
|
|
17701
|
+
function computeScopeDriftSignal(predicted, changed) {
|
|
17702
|
+
if (!predicted || predicted.length === 0) return null;
|
|
17703
|
+
if (changed.length === 0) return null;
|
|
17704
|
+
const basename2 = (p) => {
|
|
17705
|
+
const parts = p.split(/[\\/]/);
|
|
17706
|
+
return parts[parts.length - 1] ?? p;
|
|
17707
|
+
};
|
|
17708
|
+
const predictedNames = new Set(predicted.map(basename2).filter(Boolean));
|
|
17709
|
+
const unexpected = changed.filter((c) => !predictedNames.has(basename2(c)));
|
|
17710
|
+
const fraction = unexpected.length / changed.length;
|
|
17711
|
+
const triggered = fraction > 0.5 || unexpected.length > 5;
|
|
17712
|
+
if (!triggered) return null;
|
|
17713
|
+
const sample = unexpected.slice(0, 5).join(", ");
|
|
17714
|
+
const more = unexpected.length > 5 ? ` (+${unexpected.length - 5} more)` : "";
|
|
17715
|
+
const pct = Math.round(fraction * 100);
|
|
17716
|
+
return `${unexpected.length}/${changed.length} changed files (${pct}%) outside FILES LIKELY TOUCHED: ${sample}${more}`;
|
|
17717
|
+
}
|
|
17185
17718
|
function getUnresolvedDeps(task, allTasks) {
|
|
17186
17719
|
if (!task.dependsOn) return [];
|
|
17187
17720
|
const deps = task.dependsOn.split(",").map((d) => d.trim()).filter(Boolean);
|
|
@@ -17243,6 +17776,11 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
|
|
|
17243
17776
|
if (task.status === "Done" || task.status === "Archived") {
|
|
17244
17777
|
throw new Error(`Task "${taskId}" (${task.title}) is already ${task.status}. Cannot execute a completed task.`);
|
|
17245
17778
|
}
|
|
17779
|
+
if (task.status === "In Review") {
|
|
17780
|
+
throw new Error(
|
|
17781
|
+
`Task "${taskId}" (${task.title}) is already In Review \u2014 it has been built and is awaiting sign-off. Run \`review_submit\` instead of re-building. If the build genuinely needs rework, use \`review_submit\` with verdict \`request-changes\` first.`
|
|
17782
|
+
);
|
|
17783
|
+
}
|
|
17246
17784
|
if (!task.buildHandoff) {
|
|
17247
17785
|
const err = new Error(`Task "${taskId}" (${task.title}) has no BUILD HANDOFF.`);
|
|
17248
17786
|
err.code = "NO_HANDOFF";
|
|
@@ -17487,18 +18025,37 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
17487
18025
|
}
|
|
17488
18026
|
}
|
|
17489
18027
|
let autoTriagedCount = 0;
|
|
18028
|
+
const autoTriagedIds = [];
|
|
18029
|
+
const autoTriagedDupes = [];
|
|
17490
18030
|
if (input.discoveredIssues && input.discoveredIssues !== "None" && typeof adapter2.createTask === "function") {
|
|
17491
18031
|
const issueLines = input.discoveredIssues.split(/\n|;/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
18032
|
+
const backlogTitleMap = /* @__PURE__ */ new Map();
|
|
18033
|
+
try {
|
|
18034
|
+
const backlog = await adapter2.queryBoard({ status: ["Backlog"] });
|
|
18035
|
+
for (const t of backlog) {
|
|
18036
|
+
const normalized = t.title.replace(/^\[Auto-triaged\]\s*/i, "").trim().toLowerCase();
|
|
18037
|
+
if (normalized && !backlogTitleMap.has(normalized)) {
|
|
18038
|
+
backlogTitleMap.set(normalized, t.displayId);
|
|
18039
|
+
}
|
|
18040
|
+
}
|
|
18041
|
+
} catch {
|
|
18042
|
+
}
|
|
17492
18043
|
for (const line of issueLines) {
|
|
17493
18044
|
const sevMatch = line.match(/^(P[0-3])[\s:]+/i);
|
|
17494
18045
|
if (!sevMatch) continue;
|
|
17495
18046
|
const severityLabel = sevMatch[1].toUpperCase();
|
|
17496
|
-
const priority = severityLabel === "P0"
|
|
18047
|
+
const priority = severityLabel === "P0" ? "P0 Critical" : severityLabel === "P1" ? "P1 High" : severityLabel === "P2" ? "P2 Medium" : "P3 Low";
|
|
17497
18048
|
const titleRaw = line.replace(/^P[0-3][\s:]+/i, "").trim();
|
|
17498
18049
|
const title = titleRaw.length > 120 ? titleRaw.slice(0, 120) : titleRaw;
|
|
17499
18050
|
if (!title) continue;
|
|
18051
|
+
const normalized = title.toLowerCase();
|
|
18052
|
+
const dupId = backlogTitleMap.get(normalized);
|
|
18053
|
+
if (dupId) {
|
|
18054
|
+
autoTriagedDupes.push(dupId);
|
|
18055
|
+
continue;
|
|
18056
|
+
}
|
|
17500
18057
|
try {
|
|
17501
|
-
await adapter2.createTask({
|
|
18058
|
+
const created = await adapter2.createTask({
|
|
17502
18059
|
uuid: "",
|
|
17503
18060
|
displayId: "",
|
|
17504
18061
|
title: `[Auto-triaged] ${title}`,
|
|
@@ -17515,6 +18072,10 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
17515
18072
|
createdCycle: cycleNumber
|
|
17516
18073
|
});
|
|
17517
18074
|
autoTriagedCount++;
|
|
18075
|
+
if (created?.displayId) {
|
|
18076
|
+
autoTriagedIds.push(created.displayId);
|
|
18077
|
+
backlogTitleMap.set(normalized, created.displayId);
|
|
18078
|
+
}
|
|
17518
18079
|
} catch {
|
|
17519
18080
|
}
|
|
17520
18081
|
}
|
|
@@ -17620,6 +18181,8 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
17620
18181
|
const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
|
|
17621
18182
|
const changed = getFilesChangedFromBase(config2.projectRoot, baseBranch);
|
|
17622
18183
|
if (changed.length > 0) report.filesChanged = changed;
|
|
18184
|
+
const drift = computeScopeDriftSignal(task.buildHandoff?.filesLikelyTouched, changed);
|
|
18185
|
+
if (drift) report.scopeDriftSignal = drift;
|
|
17623
18186
|
}
|
|
17624
18187
|
let prLines = [];
|
|
17625
18188
|
if (options.light) {
|
|
@@ -17711,6 +18274,8 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
17711
18274
|
dogfoodResolvedCount: dogfoodResolvedCount > 0 ? dogfoodResolvedCount : void 0,
|
|
17712
18275
|
learningsLinkedCount: learningsLinkedCount > 0 ? learningsLinkedCount : void 0,
|
|
17713
18276
|
autoTriagedCount: autoTriagedCount > 0 ? autoTriagedCount : void 0,
|
|
18277
|
+
autoTriagedIds: autoTriagedIds.length > 0 ? autoTriagedIds : void 0,
|
|
18278
|
+
autoTriagedDupes: autoTriagedDupes.length > 0 ? autoTriagedDupes : void 0,
|
|
17714
18279
|
reportWriteVerified
|
|
17715
18280
|
};
|
|
17716
18281
|
}
|
|
@@ -18188,6 +18753,7 @@ function formatCompleteResult(result) {
|
|
|
18188
18753
|
`**Discovered Issues:** ${result.report.discoveredIssues}`,
|
|
18189
18754
|
`**Architecture Notes:** ${result.report.architectureNotes}`,
|
|
18190
18755
|
...result.report.deadEnds ? [`**Dead Ends:** ${result.report.deadEnds}`] : [],
|
|
18756
|
+
...result.report.scopeDriftSignal ? [`**Scope drift signal:** ${result.report.scopeDriftSignal}`] : [],
|
|
18191
18757
|
`**Scope Accuracy:** ${result.scopeAccuracy}`,
|
|
18192
18758
|
"",
|
|
18193
18759
|
"---",
|
|
@@ -18214,8 +18780,16 @@ function formatCompleteResult(result) {
|
|
|
18214
18780
|
if (result.learningsLinkedCount) {
|
|
18215
18781
|
lines.push("", `Linked ${result.learningsLinkedCount} unactioned learning(s) to this task.`);
|
|
18216
18782
|
}
|
|
18217
|
-
if (result.autoTriagedCount) {
|
|
18218
|
-
|
|
18783
|
+
if (result.autoTriagedCount || result.autoTriagedDupes?.length) {
|
|
18784
|
+
const parts = [];
|
|
18785
|
+
if (result.autoTriagedCount) {
|
|
18786
|
+
const ids = result.autoTriagedIds?.length ? ` (${result.autoTriagedIds.join(", ")})` : "";
|
|
18787
|
+
parts.push(`\u{1F516} Auto-triaged ${result.autoTriagedCount} discovered issue(s) to Backlog${ids}.`);
|
|
18788
|
+
}
|
|
18789
|
+
if (result.autoTriagedDupes?.length) {
|
|
18790
|
+
parts.push(`Skipped ${result.autoTriagedDupes.length} duplicate issue(s) already in Backlog: ${result.autoTriagedDupes.join(", ")}.`);
|
|
18791
|
+
}
|
|
18792
|
+
lines.push("", parts.join(" "));
|
|
18219
18793
|
}
|
|
18220
18794
|
if (result.reportWriteVerified === false) {
|
|
18221
18795
|
lines.push("", "\u26A0\uFE0F Build report write could not be verified \u2014 the report may not have been persisted. Run `build_list` to check, and resubmit if missing.");
|
|
@@ -19830,7 +20404,41 @@ function generateChangelog(version, commits) {
|
|
|
19830
20404
|
${commitList}
|
|
19831
20405
|
`;
|
|
19832
20406
|
}
|
|
19833
|
-
|
|
20407
|
+
function mergeGroupedCycleBranches(config2, cycleNum, baseBranch) {
|
|
20408
|
+
const branches = listGroupedCycleBranches(config2.projectRoot, cycleNum, baseBranch);
|
|
20409
|
+
if (branches.length === 0) return [];
|
|
20410
|
+
if (!isGhAvailable()) {
|
|
20411
|
+
throw new Error(
|
|
20412
|
+
`Release blocked: ${branches.length} unmerged grouped cycle branch(es) detected (${branches.join(", ")}) but \`gh\` CLI is not available. Install gh and re-run release.`
|
|
20413
|
+
);
|
|
20414
|
+
}
|
|
20415
|
+
const results = [];
|
|
20416
|
+
for (const branch of branches) {
|
|
20417
|
+
let prUrl = getPullRequestUrl(config2.projectRoot, branch);
|
|
20418
|
+
if (!prUrl) {
|
|
20419
|
+
const moduleName = branch.replace(`feat/cycle-${cycleNum}-`, "");
|
|
20420
|
+
const prCreate = createPullRequest(
|
|
20421
|
+
config2.projectRoot,
|
|
20422
|
+
branch,
|
|
20423
|
+
baseBranch,
|
|
20424
|
+
`feat(cycle-${cycleNum}): merge shared cycle branch \u2014 ${moduleName}`,
|
|
20425
|
+
`Automated PR created at release time for shared cycle branch \`${branch}\` (Cycle ${cycleNum}).`
|
|
20426
|
+
);
|
|
20427
|
+
if (!prCreate.success) {
|
|
20428
|
+
results.push({ branch, prUrl: null, status: "failed", message: prCreate.message });
|
|
20429
|
+
continue;
|
|
20430
|
+
}
|
|
20431
|
+
prUrl = prCreate.message;
|
|
20432
|
+
}
|
|
20433
|
+
const merge = squashMergePullRequest(config2.projectRoot, branch);
|
|
20434
|
+
if (merge.success) {
|
|
20435
|
+
deleteLocalBranch(config2.projectRoot, branch);
|
|
20436
|
+
}
|
|
20437
|
+
results.push({ branch, prUrl, status: merge.success ? "merged" : "failed", message: merge.message });
|
|
20438
|
+
}
|
|
20439
|
+
return results;
|
|
20440
|
+
}
|
|
20441
|
+
async function createRelease(config2, branch, version, adapter2, cycleNum) {
|
|
19834
20442
|
if (!isGitAvailable()) {
|
|
19835
20443
|
throw new Error("git is not available.");
|
|
19836
20444
|
}
|
|
@@ -19841,10 +20449,13 @@ async function createRelease(config2, branch, version, adapter2) {
|
|
|
19841
20449
|
throw new Error("working directory has uncommitted changes. Commit or stash them before releasing.");
|
|
19842
20450
|
}
|
|
19843
20451
|
const warnings = [];
|
|
20452
|
+
const resolvedCycleNum = cycleNum && cycleNum > 0 ? cycleNum : (() => {
|
|
20453
|
+
const m = version.match(/^v0\.(\d+)\./);
|
|
20454
|
+
return m ? parseInt(m[1], 10) : 0;
|
|
20455
|
+
})();
|
|
19844
20456
|
if (adapter2) {
|
|
19845
20457
|
try {
|
|
19846
|
-
const
|
|
19847
|
-
const currentCycle = versionMatch ? parseInt(versionMatch[1], 10) : 0;
|
|
20458
|
+
const currentCycle = resolvedCycleNum;
|
|
19848
20459
|
if (currentCycle > 0) {
|
|
19849
20460
|
await adapter2.createCycle({
|
|
19850
20461
|
id: `cycle-${currentCycle}`,
|
|
@@ -19873,6 +20484,24 @@ async function createRelease(config2, branch, version, adapter2) {
|
|
|
19873
20484
|
warnings.push(`git pull failed: ${pull.message}. Run manually.`);
|
|
19874
20485
|
}
|
|
19875
20486
|
}
|
|
20487
|
+
let groupedBranchMerges;
|
|
20488
|
+
if (resolvedCycleNum > 0) {
|
|
20489
|
+
const mergeResults = mergeGroupedCycleBranches(config2, resolvedCycleNum, branch);
|
|
20490
|
+
if (mergeResults.length > 0) {
|
|
20491
|
+
groupedBranchMerges = mergeResults;
|
|
20492
|
+
const failures = mergeResults.filter((r) => r.status === "failed");
|
|
20493
|
+
if (failures.length > 0) {
|
|
20494
|
+
const detail = failures.map((r) => `${r.branch}: ${r.message}`).join("; ");
|
|
20495
|
+
throw new Error(`Release blocked: grouped cycle branch merge failed \u2014 ${detail}. Resolve conflicts and retry release.`);
|
|
20496
|
+
}
|
|
20497
|
+
if (hasRemote(config2.projectRoot)) {
|
|
20498
|
+
const repull = gitPull(config2.projectRoot);
|
|
20499
|
+
if (!repull.success) {
|
|
20500
|
+
warnings.push(`Post-merge pull failed: ${repull.message}. Run manually.`);
|
|
20501
|
+
}
|
|
20502
|
+
}
|
|
20503
|
+
}
|
|
20504
|
+
}
|
|
19876
20505
|
if (tagExists(config2.projectRoot, version)) {
|
|
19877
20506
|
throw new Error(`tag "${version}" already exists. Use a different version.`);
|
|
19878
20507
|
}
|
|
@@ -19909,7 +20538,8 @@ async function createRelease(config2, branch, version, adapter2) {
|
|
|
19909
20538
|
commitNote,
|
|
19910
20539
|
tagMessage: tagResult.message,
|
|
19911
20540
|
pushNotes,
|
|
19912
|
-
warnings: warnings.length > 0 ? warnings : void 0
|
|
20541
|
+
warnings: warnings.length > 0 ? warnings : void 0,
|
|
20542
|
+
...groupedBranchMerges ? { groupedBranchMerges } : {}
|
|
19913
20543
|
};
|
|
19914
20544
|
}
|
|
19915
20545
|
|
|
@@ -19965,7 +20595,9 @@ async function handleRelease(adapter2, config2, args) {
|
|
|
19965
20595
|
return errorResponse(`version must start with "v" (got "${version}"). Example: "v0.1.0-alpha"`);
|
|
19966
20596
|
}
|
|
19967
20597
|
try {
|
|
19968
|
-
const
|
|
20598
|
+
const cycleMatch = version.match(/^v0\.(\d+)\./);
|
|
20599
|
+
const cycleNum = cycleMatch ? parseInt(cycleMatch[1], 10) : void 0;
|
|
20600
|
+
const result = await createRelease(config2, branch, version, adapter2, cycleNum);
|
|
19969
20601
|
const lines = [
|
|
19970
20602
|
`## Release ${result.version}`,
|
|
19971
20603
|
"",
|
|
@@ -19978,13 +20610,17 @@ async function handleRelease(adapter2, config2, args) {
|
|
|
19978
20610
|
lines.push("", "---", "");
|
|
19979
20611
|
lines.push(...result.pushNotes);
|
|
19980
20612
|
}
|
|
20613
|
+
if (result.groupedBranchMerges?.length) {
|
|
20614
|
+
lines.push("", "**Shared cycle branches merged:**");
|
|
20615
|
+
for (const m of result.groupedBranchMerges) {
|
|
20616
|
+
lines.push(`- \`${m.branch}\` \u2014 ${m.message}${m.prUrl ? ` (${m.prUrl})` : ""}`);
|
|
20617
|
+
}
|
|
20618
|
+
}
|
|
19981
20619
|
if (result.warnings?.length) {
|
|
19982
20620
|
lines.push("", "\u26A0\uFE0F Warnings: " + result.warnings.join("; "));
|
|
19983
20621
|
}
|
|
19984
20622
|
try {
|
|
19985
|
-
|
|
19986
|
-
const cycleNum = cycleMatch ? parseInt(cycleMatch[1], 10) : 0;
|
|
19987
|
-
if (cycleNum > 0) {
|
|
20623
|
+
if (cycleNum && cycleNum > 0) {
|
|
19988
20624
|
const reports = await adapter2.getBuildReportsSince(cycleNum);
|
|
19989
20625
|
const EMPTY = /* @__PURE__ */ new Set(["None", "none", "N/A", "", "null"]);
|
|
19990
20626
|
const issues = reports.filter((r) => r.discoveredIssues && !EMPTY.has(r.discoveredIssues.trim())).map((r) => `- **${r.taskId}** (${r.taskName}): ${r.discoveredIssues}`);
|
|
@@ -19997,10 +20633,10 @@ async function handleRelease(adapter2, config2, args) {
|
|
|
19997
20633
|
}
|
|
19998
20634
|
if (rawObservations && rawObservations.length > 0 && adapter2.writeDogfoodEntries) {
|
|
19999
20635
|
try {
|
|
20000
|
-
const
|
|
20001
|
-
const
|
|
20636
|
+
const cycleMatch2 = version.match(/^v0\.(\d+)\./);
|
|
20637
|
+
const cycleNum2 = cycleMatch2 ? parseInt(cycleMatch2[1], 10) : 0;
|
|
20002
20638
|
const entries = rawObservations.map((obs) => ({
|
|
20003
|
-
cycleNumber:
|
|
20639
|
+
cycleNumber: cycleNum2,
|
|
20004
20640
|
category: obs.category,
|
|
20005
20641
|
content: obs.content,
|
|
20006
20642
|
sourceTool: "release",
|
|
@@ -20256,6 +20892,10 @@ function mergeAfterAccept(config2, taskId) {
|
|
|
20256
20892
|
}
|
|
20257
20893
|
const featureBranch = taskBranchName(taskId);
|
|
20258
20894
|
const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
|
|
20895
|
+
if (!branchExists(config2.projectRoot, featureBranch)) {
|
|
20896
|
+
lines.push(`Task is on a shared cycle branch \u2014 will be merged at release time.`);
|
|
20897
|
+
return lines;
|
|
20898
|
+
}
|
|
20259
20899
|
const papiDir = join7(config2.projectRoot, ".papi");
|
|
20260
20900
|
if (existsSync4(papiDir)) {
|
|
20261
20901
|
try {
|
|
@@ -20431,8 +21071,9 @@ ${result.handoffRegenPrompt.userMessage}
|
|
|
20431
21071
|
}
|
|
20432
21072
|
const version = `v0.${result.currentCycle}.0`;
|
|
20433
21073
|
const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
|
|
20434
|
-
const releaseResult = await createRelease(config2, baseBranch, version, adapter2);
|
|
21074
|
+
const releaseResult = await createRelease(config2, baseBranch, version, adapter2, result.currentCycle);
|
|
20435
21075
|
const pushInfo = releaseResult.pushNotes.join(" ");
|
|
21076
|
+
const groupedMergeNote = releaseResult.groupedBranchMerges?.length ? "\n" + releaseResult.groupedBranchMerges.map((r) => `- Merged shared branch \`${r.branch}\` via PR: ${r.prUrl ?? "n/a"}`).join("\n") : "";
|
|
20436
21077
|
autoReleaseNote = `
|
|
20437
21078
|
|
|
20438
21079
|
---
|
|
@@ -20442,7 +21083,7 @@ ${result.handoffRegenPrompt.userMessage}
|
|
|
20442
21083
|
- Version: **${releaseResult.version}**
|
|
20443
21084
|
- ${releaseResult.commitNote}
|
|
20444
21085
|
- ${releaseResult.tagMessage}
|
|
20445
|
-
- ${pushInfo}` + (releaseResult.warnings?.length ? `
|
|
21086
|
+
- ${pushInfo}` + groupedMergeNote + (releaseResult.warnings?.length ? `
|
|
20446
21087
|
- Warnings: ${releaseResult.warnings.join(", ")}` : "") + `
|
|
20447
21088
|
|
|
20448
21089
|
Run \`plan\` to create Cycle ${result.currentCycle + 1}.`;
|
|
@@ -20689,7 +21330,7 @@ function countByStatus(tasks) {
|
|
|
20689
21330
|
async function getHealthSummary(adapter2) {
|
|
20690
21331
|
const health = await adapter2.getCycleHealth();
|
|
20691
21332
|
const activeTasks = await adapter2.queryBoard({
|
|
20692
|
-
status: ["Backlog", "In Cycle", "Ready", "In Progress", "In Review", "Blocked"]
|
|
21333
|
+
status: ["Backlog", "In Cycle", "Ready", "In Progress", "In Review", "Blocked", "Deferred"]
|
|
20693
21334
|
});
|
|
20694
21335
|
const logEntries = await adapter2.getCycleLog(3);
|
|
20695
21336
|
const cycleNumber = health.totalCycles;
|
|
@@ -20809,7 +21450,7 @@ ${lines.join("\n")}`;
|
|
|
20809
21450
|
}
|
|
20810
21451
|
let decisionLifecycleSection = "";
|
|
20811
21452
|
try {
|
|
20812
|
-
const decisions = await adapter2.getActiveDecisions();
|
|
21453
|
+
const decisions = await adapter2.getActiveDecisions({ includeRetired: true });
|
|
20813
21454
|
const lifecycleSummary = formatDecisionLifecycleSummary(decisions);
|
|
20814
21455
|
if (lifecycleSummary) {
|
|
20815
21456
|
decisionLifecycleSection = `**Lifecycle:** ${lifecycleSummary}`;
|
|
@@ -21575,7 +22216,19 @@ ${versionDrift}` : "";
|
|
|
21575
22216
|
const learnings = await adapter2.getCycleLearnings?.({ category: "issue", limit: 30 });
|
|
21576
22217
|
if (learnings) {
|
|
21577
22218
|
const byRecency = (a, b2) => (b2.createdAt ?? "").localeCompare(a.createdAt ?? "");
|
|
21578
|
-
const
|
|
22219
|
+
const candidateLearnings = learnings.filter((l) => !l.actionTaken);
|
|
22220
|
+
const referencedTaskIds = Array.from(new Set(candidateLearnings.map((l) => l.taskId).filter(Boolean)));
|
|
22221
|
+
let closedTaskIds = /* @__PURE__ */ new Set();
|
|
22222
|
+
if (referencedTaskIds.length > 0) {
|
|
22223
|
+
try {
|
|
22224
|
+
const tasks = await adapter2.getTasks(referencedTaskIds);
|
|
22225
|
+
closedTaskIds = new Set(
|
|
22226
|
+
tasks.filter((t) => t.status === "Done" || t.status === "Cancelled").map((t) => t.id)
|
|
22227
|
+
);
|
|
22228
|
+
} catch {
|
|
22229
|
+
}
|
|
22230
|
+
}
|
|
22231
|
+
const unactionedAll = candidateLearnings.filter((l) => !closedTaskIds.has(l.taskId)).map((l) => ({ ...l, severity: l.severity ?? "P3" }));
|
|
21579
22232
|
const allAlerts = unactionedAll.filter((l) => l.severity === "P0" || l.severity === "P1").sort(byRecency);
|
|
21580
22233
|
const allLowSev = unactionedAll.filter((l) => l.severity === "P2" || l.severity === "P3").sort(byRecency);
|
|
21581
22234
|
const totalP2 = allLowSev.filter((l) => l.severity === "P2").length;
|
|
@@ -22488,16 +23141,19 @@ function emitToolCall(projectId, toolName, durationMs, extra) {
|
|
|
22488
23141
|
metadata: { duration_ms: durationMs, ...extra }
|
|
22489
23142
|
});
|
|
22490
23143
|
}
|
|
22491
|
-
function emitMdAdapterPing(toolName, extra) {
|
|
23144
|
+
function emitMdAdapterPing(toolName, extra, userId, projectSlug) {
|
|
22492
23145
|
if (!isEnabled()) return;
|
|
22493
23146
|
const installId = getInstallId();
|
|
22494
23147
|
if (!installId) return;
|
|
23148
|
+
const resolvedUserId = userId ?? process.env["PAPI_USER_ID"] ?? void 0;
|
|
22495
23149
|
const body = {
|
|
22496
23150
|
install_id: installId,
|
|
22497
23151
|
tool_name: toolName,
|
|
22498
23152
|
papi_version: process.env["npm_package_version"] ?? null,
|
|
22499
23153
|
metadata: extra ?? {}
|
|
22500
23154
|
};
|
|
23155
|
+
if (resolvedUserId) body["user_id"] = resolvedUserId;
|
|
23156
|
+
if (projectSlug) body["project_slug"] = projectSlug;
|
|
22501
23157
|
fetch(`${TELEMETRY_SUPABASE_URL}/rest/v1/md_adapter_pings`, {
|
|
22502
23158
|
method: "POST",
|
|
22503
23159
|
headers: {
|
|
@@ -22525,6 +23181,7 @@ var TOOLS_REQUIRING_PAPI = /* @__PURE__ */ new Set([
|
|
|
22525
23181
|
"plan",
|
|
22526
23182
|
"strategy_review",
|
|
22527
23183
|
"strategy_change",
|
|
23184
|
+
"strategy_agenda",
|
|
22528
23185
|
"board_view",
|
|
22529
23186
|
"board_deprioritise",
|
|
22530
23187
|
"board_archive",
|
|
@@ -22617,6 +23274,7 @@ function createServer(adapter2, config2) {
|
|
|
22617
23274
|
planTool,
|
|
22618
23275
|
strategyReviewTool,
|
|
22619
23276
|
strategyChangeTool,
|
|
23277
|
+
strategyAgendaTool,
|
|
22620
23278
|
boardViewTool,
|
|
22621
23279
|
boardDeprioritiseTool,
|
|
22622
23280
|
boardArchiveTool,
|
|
@@ -22683,6 +23341,9 @@ function createServer(adapter2, config2) {
|
|
|
22683
23341
|
case "strategy_change":
|
|
22684
23342
|
result = await handleStrategyChange(adapter2, config2, safeArgs);
|
|
22685
23343
|
break;
|
|
23344
|
+
case "strategy_agenda":
|
|
23345
|
+
result = await handleStrategyAgenda(adapter2, config2, safeArgs);
|
|
23346
|
+
break;
|
|
22686
23347
|
case "board_view":
|
|
22687
23348
|
result = await handleBoardView(adapter2, safeArgs);
|
|
22688
23349
|
break;
|
|
@@ -22777,7 +23438,8 @@ function createServer(adapter2, config2) {
|
|
|
22777
23438
|
} catch {
|
|
22778
23439
|
}
|
|
22779
23440
|
if (config2.adapterType === "md") {
|
|
22780
|
-
|
|
23441
|
+
const mdProjectSlug = config2.projectRoot ? config2.projectRoot.split("/").pop() : void 0;
|
|
23442
|
+
emitMdAdapterPing(name, { duration_ms: elapsed, success: !isError }, config2.userId, mdProjectSlug);
|
|
22781
23443
|
}
|
|
22782
23444
|
const telemetryProjectId = process.env["PAPI_PROJECT_ID"];
|
|
22783
23445
|
if (telemetryProjectId) {
|