@papi-ai/server 0.7.6 → 0.7.8
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 +1667 -867
- package/dist/prompts.js +46 -7
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -215,6 +215,30 @@ ${after}`;
|
|
|
215
215
|
function parseNorthStar(content) {
|
|
216
216
|
return extractSection(content, "North Star").replace(/^## North Star\s*/m, "").trim();
|
|
217
217
|
}
|
|
218
|
+
function upsertNorthStarInContent(content, statement) {
|
|
219
|
+
const headingPattern = /^## North Star\s*$/m;
|
|
220
|
+
const start = content.search(headingPattern);
|
|
221
|
+
if (start === -1) {
|
|
222
|
+
const cycleLogIdx = content.search(/^## (?:Cycle Log|Sprint Log)/m);
|
|
223
|
+
const newSection = `## North Star
|
|
224
|
+
|
|
225
|
+
${statement}
|
|
226
|
+
|
|
227
|
+
`;
|
|
228
|
+
if (cycleLogIdx === -1) {
|
|
229
|
+
return content.trimEnd() + "\n\n" + newSection;
|
|
230
|
+
}
|
|
231
|
+
return content.slice(0, cycleLogIdx) + newSection + content.slice(cycleLogIdx);
|
|
232
|
+
}
|
|
233
|
+
const afterHeading = content.slice(start);
|
|
234
|
+
const nextSection = afterHeading.slice(1).search(/^## /m);
|
|
235
|
+
const sectionEnd = nextSection === -1 ? content.length : start + nextSection + 1;
|
|
236
|
+
return content.slice(0, start) + `## North Star
|
|
237
|
+
|
|
238
|
+
${statement}
|
|
239
|
+
|
|
240
|
+
` + content.slice(sectionEnd);
|
|
241
|
+
}
|
|
218
242
|
function parseDeferred(content) {
|
|
219
243
|
const section = extractSection(content, "Deferred / Parking Lot");
|
|
220
244
|
return section.split("\n").filter((line) => line.match(/^-\s+/)).map((line) => line.replace(/^-\s+/, "").trim());
|
|
@@ -1428,7 +1452,8 @@ var init_dist2 = __esm({
|
|
|
1428
1452
|
task: 1,
|
|
1429
1453
|
research: 2,
|
|
1430
1454
|
spike: 2,
|
|
1431
|
-
idea: 3
|
|
1455
|
+
idea: 3,
|
|
1456
|
+
discovery: 1
|
|
1432
1457
|
};
|
|
1433
1458
|
VALID_EFFORT_SIZES = /* @__PURE__ */ new Set(["XS", "S", "M", "L", "XL"]);
|
|
1434
1459
|
SECTION_HEADERS = [
|
|
@@ -2174,6 +2199,23 @@ ${footer}`);
|
|
|
2174
2199
|
async getDecisionUsage(_currentCycle) {
|
|
2175
2200
|
return [];
|
|
2176
2201
|
}
|
|
2202
|
+
// --- North Star ---
|
|
2203
|
+
async getCurrentNorthStar() {
|
|
2204
|
+
const content = await this.read("PLANNING_LOG.md");
|
|
2205
|
+
const ns = parseNorthStar(content);
|
|
2206
|
+
return ns || null;
|
|
2207
|
+
}
|
|
2208
|
+
async getNorthStarSetCycle() {
|
|
2209
|
+
return null;
|
|
2210
|
+
}
|
|
2211
|
+
async getNorthStarStaleness() {
|
|
2212
|
+
return null;
|
|
2213
|
+
}
|
|
2214
|
+
async upsertNorthStar(statement, _cycleNumber) {
|
|
2215
|
+
const content = await this.read("PLANNING_LOG.md");
|
|
2216
|
+
const updated = upsertNorthStarInContent(content, statement);
|
|
2217
|
+
await this.write("PLANNING_LOG.md", updated);
|
|
2218
|
+
}
|
|
2177
2219
|
};
|
|
2178
2220
|
NONE_PATTERN2 = /^none\b/i;
|
|
2179
2221
|
}
|
|
@@ -4468,6 +4510,7 @@ function rowToTask(row) {
|
|
|
4468
4510
|
if (row.stage_id != null) task.stageId = row.stage_id;
|
|
4469
4511
|
if (row.doc_ref != null) task.docRef = row.doc_ref;
|
|
4470
4512
|
if (row.source != null) task.source = row.source;
|
|
4513
|
+
if (row.opportunity != null) task.opportunity = row.opportunity;
|
|
4471
4514
|
return task;
|
|
4472
4515
|
}
|
|
4473
4516
|
function rowToBuildReport(row) {
|
|
@@ -4494,6 +4537,7 @@ function rowToBuildReport(row) {
|
|
|
4494
4537
|
if (row.handoff_accuracy != null) report.handoffAccuracy = row.handoff_accuracy;
|
|
4495
4538
|
if (row.brief_implications != null) report.briefImplications = row.brief_implications;
|
|
4496
4539
|
if (row.dead_ends != null) report.deadEnds = row.dead_ends;
|
|
4540
|
+
if (row.tool_call_count != null) report.toolCallCount = row.tool_call_count;
|
|
4497
4541
|
return report;
|
|
4498
4542
|
}
|
|
4499
4543
|
function rowToReview(row) {
|
|
@@ -6094,6 +6138,22 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
|
6094
6138
|
`;
|
|
6095
6139
|
return rows.length > 0 ? { setCycle: rows[0].set_cycle, setAt: rows[0].set_at } : null;
|
|
6096
6140
|
}
|
|
6141
|
+
async upsertNorthStar(statement, _cycleNumber) {
|
|
6142
|
+
const [newRow] = await this.sql`
|
|
6143
|
+
INSERT INTO north_stars (project_id, statement, set_at)
|
|
6144
|
+
VALUES (${this.projectId}, ${statement}, now())
|
|
6145
|
+
RETURNING id
|
|
6146
|
+
`;
|
|
6147
|
+
if (newRow) {
|
|
6148
|
+
await this.sql`
|
|
6149
|
+
UPDATE north_stars
|
|
6150
|
+
SET superseded_by_id = ${newRow.id}, superseded_at = now()
|
|
6151
|
+
WHERE project_id = ${this.projectId}
|
|
6152
|
+
AND superseded_by_id IS NULL
|
|
6153
|
+
AND id != ${newRow.id}
|
|
6154
|
+
`;
|
|
6155
|
+
}
|
|
6156
|
+
}
|
|
6097
6157
|
async getEstimationCalibration() {
|
|
6098
6158
|
const rows = await this.sql`
|
|
6099
6159
|
SELECT estimated_effort, actual_effort, accuracy_label, COUNT(*)::text AS count
|
|
@@ -6666,7 +6726,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6666
6726
|
async queryBoard(options) {
|
|
6667
6727
|
if (!options) {
|
|
6668
6728
|
const rows2 = await this.sql`
|
|
6669
|
-
SELECT id, project_id, display_id, title, status, priority, complexity, module, epic, phase, owner, reviewed, cycle, created_cycle, created_at, why, depends_on, notes, closure_reason, state_history, build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, updated_at
|
|
6729
|
+
SELECT id, project_id, display_id, title, status, priority, complexity, module, epic, phase, owner, reviewed, cycle, created_cycle, created_at, why, depends_on, notes, closure_reason, state_history, build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, opportunity, updated_at
|
|
6670
6730
|
FROM cycle_tasks
|
|
6671
6731
|
WHERE project_id = ${this.projectId}
|
|
6672
6732
|
ORDER BY display_id
|
|
@@ -6707,7 +6767,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6707
6767
|
where = this.sql`${where} AND ${conditions[i]}`;
|
|
6708
6768
|
}
|
|
6709
6769
|
const rows = await this.sql`
|
|
6710
|
-
SELECT id, project_id, display_id, title, status, priority, complexity, module, epic, phase, owner, reviewed, cycle, created_cycle, created_at, why, depends_on, notes, closure_reason, state_history, build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, updated_at
|
|
6770
|
+
SELECT id, project_id, display_id, title, status, priority, complexity, module, epic, phase, owner, reviewed, cycle, created_cycle, created_at, why, depends_on, notes, closure_reason, state_history, build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, opportunity, updated_at
|
|
6711
6771
|
FROM cycle_tasks WHERE ${where} ORDER BY display_id
|
|
6712
6772
|
LIMIT 2000 -- matches no-options path ceiling
|
|
6713
6773
|
`;
|
|
@@ -6715,7 +6775,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6715
6775
|
}
|
|
6716
6776
|
async getTask(id) {
|
|
6717
6777
|
const [row] = await this.sql`
|
|
6718
|
-
SELECT id, project_id, display_id, title, status, priority, complexity, module, epic, phase, owner, reviewed, cycle, created_cycle, created_at, why, depends_on, notes, closure_reason, state_history, build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, updated_at
|
|
6778
|
+
SELECT id, project_id, display_id, title, status, priority, complexity, module, epic, phase, owner, reviewed, cycle, created_cycle, created_at, why, depends_on, notes, closure_reason, state_history, build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, opportunity, updated_at
|
|
6719
6779
|
FROM cycle_tasks
|
|
6720
6780
|
WHERE project_id = ${this.projectId} AND display_id = ${id}
|
|
6721
6781
|
LIMIT 1
|
|
@@ -6725,7 +6785,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6725
6785
|
async getTasks(ids) {
|
|
6726
6786
|
if (ids.length === 0) return [];
|
|
6727
6787
|
const rows = await this.sql`
|
|
6728
|
-
SELECT id, project_id, display_id, title, status, priority, complexity, module, epic, phase, owner, reviewed, cycle, created_cycle, created_at, why, depends_on, notes, closure_reason, state_history, build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, updated_at
|
|
6788
|
+
SELECT id, project_id, display_id, title, status, priority, complexity, module, epic, phase, owner, reviewed, cycle, created_cycle, created_at, why, depends_on, notes, closure_reason, state_history, build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, opportunity, updated_at
|
|
6729
6789
|
FROM cycle_tasks
|
|
6730
6790
|
WHERE project_id = ${this.projectId} AND display_id = ANY(${ids})
|
|
6731
6791
|
LIMIT 2000 -- matches board ceiling; ids[] won't exceed this in practice
|
|
@@ -6745,7 +6805,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6745
6805
|
project_id, display_id, title, status, priority, complexity,
|
|
6746
6806
|
module, epic, phase, owner, reviewed, cycle, created_cycle,
|
|
6747
6807
|
why, depends_on, notes, closure_reason, state_history,
|
|
6748
|
-
build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source
|
|
6808
|
+
build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, opportunity
|
|
6749
6809
|
) VALUES (
|
|
6750
6810
|
${this.projectId}, ${displayId}, ${task.title}, ${task.status}, ${task.priority},
|
|
6751
6811
|
${normaliseComplexity(task.complexity)}, ${task.module}, ${task.epic ?? null}, ${task.phase}, ${task.owner},
|
|
@@ -6759,7 +6819,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6759
6819
|
${task.maturity ?? null},
|
|
6760
6820
|
${task.stageId ?? null},
|
|
6761
6821
|
${task.docRef ?? null},
|
|
6762
|
-
${task.source ?? null}
|
|
6822
|
+
${task.source ?? null},
|
|
6823
|
+
${task.opportunity ?? null}
|
|
6763
6824
|
)
|
|
6764
6825
|
RETURNING *
|
|
6765
6826
|
`;
|
|
@@ -6791,6 +6852,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6791
6852
|
if (updates.stageId !== void 0) columnMap["stage_id"] = updates.stageId;
|
|
6792
6853
|
if (updates.docRef !== void 0) columnMap["doc_ref"] = updates.docRef;
|
|
6793
6854
|
if (updates.source !== void 0) columnMap["source"] = updates.source;
|
|
6855
|
+
if (updates.opportunity !== void 0) columnMap["opportunity"] = updates.opportunity;
|
|
6794
6856
|
const keys = Object.keys(columnMap);
|
|
6795
6857
|
if (keys.length === 0) return;
|
|
6796
6858
|
await this.sql`
|
|
@@ -7190,6 +7252,16 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
7190
7252
|
`;
|
|
7191
7253
|
return rows.map(rowToToolCallMetric);
|
|
7192
7254
|
}
|
|
7255
|
+
async getToolCallCount(startedAt, completedAt) {
|
|
7256
|
+
const rows = await this.sql`
|
|
7257
|
+
SELECT COUNT(*)::text AS count
|
|
7258
|
+
FROM tool_call_metrics
|
|
7259
|
+
WHERE project_id = ${this.projectId}
|
|
7260
|
+
AND timestamp >= ${startedAt}
|
|
7261
|
+
AND timestamp <= ${completedAt}
|
|
7262
|
+
`;
|
|
7263
|
+
return parseInt(rows[0]?.count ?? "0", 10);
|
|
7264
|
+
}
|
|
7193
7265
|
// -------------------------------------------------------------------------
|
|
7194
7266
|
// Cost Summary
|
|
7195
7267
|
// -------------------------------------------------------------------------
|
|
@@ -7631,7 +7703,7 @@ ${r.content}` + (r.carry_forward ? `
|
|
|
7631
7703
|
FROM cycle_tasks
|
|
7632
7704
|
WHERE project_id = ${this.projectId}
|
|
7633
7705
|
AND status NOT IN ('Done', 'Cancelled', 'Archived', 'Deferred')
|
|
7634
|
-
AND (task_type IN ('task', 'bug', 'research') OR task_type IS NULL)
|
|
7706
|
+
AND (task_type IN ('task', 'bug', 'research', 'discovery', 'spike') OR task_type IS NULL)
|
|
7635
7707
|
ORDER BY
|
|
7636
7708
|
CASE priority
|
|
7637
7709
|
WHEN 'P0 Critical' THEN 0
|
|
@@ -7929,7 +8001,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
7929
8001
|
build_report: task.buildReport ?? null,
|
|
7930
8002
|
task_type: task.taskType ?? null,
|
|
7931
8003
|
maturity: task.maturity ?? null,
|
|
7932
|
-
stage_id: task.stageId ?? null
|
|
8004
|
+
stage_id: task.stageId ?? null,
|
|
8005
|
+
opportunity: task.opportunity ?? null
|
|
7933
8006
|
};
|
|
7934
8007
|
});
|
|
7935
8008
|
const taskCols = [
|
|
@@ -7955,7 +8028,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
7955
8028
|
"build_report",
|
|
7956
8029
|
"task_type",
|
|
7957
8030
|
"maturity",
|
|
7958
|
-
"stage_id"
|
|
8031
|
+
"stage_id",
|
|
8032
|
+
"opportunity"
|
|
7959
8033
|
];
|
|
7960
8034
|
const insertedRows = await tx`
|
|
7961
8035
|
INSERT INTO cycle_tasks ${tx(taskRows, ...taskCols)}
|
|
@@ -8567,6 +8641,9 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
8567
8641
|
getNorthStarStaleness() {
|
|
8568
8642
|
return this.invoke("getNorthStarStaleness");
|
|
8569
8643
|
}
|
|
8644
|
+
upsertNorthStar(statement, cycleNumber) {
|
|
8645
|
+
return this.invoke("upsertNorthStar", [statement, cycleNumber]);
|
|
8646
|
+
}
|
|
8570
8647
|
// --- Optional pg-only methods ---
|
|
8571
8648
|
getEstimationCalibration() {
|
|
8572
8649
|
return this.invoke("getEstimationCalibration");
|
|
@@ -8649,6 +8726,7 @@ __export(git_exports, {
|
|
|
8649
8726
|
createAndCheckoutBranch: () => createAndCheckoutBranch,
|
|
8650
8727
|
createPullRequest: () => createPullRequest,
|
|
8651
8728
|
createTag: () => createTag,
|
|
8729
|
+
cycleBranchName: () => cycleBranchName,
|
|
8652
8730
|
deleteLocalBranch: () => deleteLocalBranch,
|
|
8653
8731
|
detectBoardMismatches: () => detectBoardMismatches,
|
|
8654
8732
|
detectUnrecordedCommits: () => detectUnrecordedCommits,
|
|
@@ -9104,6 +9182,9 @@ function detectUnrecordedCommits(cwd, baseBranch) {
|
|
|
9104
9182
|
function taskBranchName(taskId) {
|
|
9105
9183
|
return `feat/${taskId}`;
|
|
9106
9184
|
}
|
|
9185
|
+
function cycleBranchName(cycleNumber, module) {
|
|
9186
|
+
return `feat/cycle-${cycleNumber}-${module.toLowerCase().replace(/\s+/g, "-")}`;
|
|
9187
|
+
}
|
|
9107
9188
|
function getHeadCommitSha(cwd) {
|
|
9108
9189
|
try {
|
|
9109
9190
|
return execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8" }).trim() || null;
|
|
@@ -9143,9 +9224,11 @@ var init_git = __esm({
|
|
|
9143
9224
|
|
|
9144
9225
|
// src/index.ts
|
|
9145
9226
|
import { readFileSync as readFileSync4 } from "fs";
|
|
9227
|
+
import { createServer as createHttpServer } from "http";
|
|
9146
9228
|
import { dirname as dirname2, join as join11 } from "path";
|
|
9147
9229
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
9148
9230
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9231
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
9149
9232
|
import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
|
|
9150
9233
|
import {
|
|
9151
9234
|
CallToolRequestSchema as CallToolRequestSchema2,
|
|
@@ -9168,6 +9251,8 @@ function loadConfig() {
|
|
|
9168
9251
|
const baseBranch = process.env.PAPI_BASE_BRANCH ?? "main";
|
|
9169
9252
|
const autoPR = process.env.PAPI_AUTO_PR !== "false";
|
|
9170
9253
|
const lightMode = process.env.PAPI_LIGHT_MODE === "true";
|
|
9254
|
+
const projectOwner = process.env.PAPI_OWNER ?? "Cathal";
|
|
9255
|
+
const skipProjectSpecificRules = process.env.PAPI_SKIP_PROJECT_RULES === "true";
|
|
9171
9256
|
const papiEndpoint = process.env.PAPI_ENDPOINT;
|
|
9172
9257
|
const dataEndpoint = process.env.PAPI_DATA_ENDPOINT;
|
|
9173
9258
|
const databaseUrl = process.env.DATABASE_URL;
|
|
@@ -9182,7 +9267,9 @@ function loadConfig() {
|
|
|
9182
9267
|
autoPR,
|
|
9183
9268
|
adapterType,
|
|
9184
9269
|
papiEndpoint,
|
|
9185
|
-
lightMode
|
|
9270
|
+
lightMode,
|
|
9271
|
+
projectOwner,
|
|
9272
|
+
skipProjectSpecificRules
|
|
9186
9273
|
};
|
|
9187
9274
|
}
|
|
9188
9275
|
|
|
@@ -9538,7 +9625,7 @@ function formatDetailedTask(t) {
|
|
|
9538
9625
|
return `- **${t.id}:** ${t.title}
|
|
9539
9626
|
Status: ${t.status} | Priority: ${t.priority} | Complexity: ${t.complexity}${typeTag}
|
|
9540
9627
|
Module: ${t.module} | Epic: ${t.epic} | Phase: ${t.phase} | Owner: ${t.owner}
|
|
9541
|
-
Reviewed: ${t.reviewed}${t.dependsOn ? ` | Depends on: ${t.dependsOn}` : ""}${hasHandoff ? " | Has BUILD HANDOFF: yes" : ""}${t.docRef ? ` | Doc ref: ${t.docRef}` : ""}${notes ? `
|
|
9628
|
+
Reviewed: ${t.reviewed}${t.dependsOn ? ` | Depends on: ${t.dependsOn}` : ""}${hasHandoff ? " | Has BUILD HANDOFF: yes" : ""}${t.docRef ? ` | Doc ref: ${t.docRef}` : ""}${t.opportunity ? ` | Opportunity: ${t.opportunity}` : ""}${notes ? `
|
|
9542
9629
|
Notes: ${notes}` : ""}`;
|
|
9543
9630
|
}
|
|
9544
9631
|
function formatBoardForPlan(tasks, filters, currentCycle) {
|
|
@@ -9945,6 +10032,191 @@ function logDataSourceSummary(service, sources) {
|
|
|
9945
10032
|
console.error(`[data-sources] ${service}: ${populated.length}/${sources.length} sources have data \u2014 empty: ${emptyLabels}`);
|
|
9946
10033
|
}
|
|
9947
10034
|
|
|
10035
|
+
// src/lib/codebase-scan.ts
|
|
10036
|
+
import { execSync as execSync2 } from "child_process";
|
|
10037
|
+
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
10038
|
+
"a",
|
|
10039
|
+
"an",
|
|
10040
|
+
"the",
|
|
10041
|
+
"and",
|
|
10042
|
+
"or",
|
|
10043
|
+
"but",
|
|
10044
|
+
"in",
|
|
10045
|
+
"on",
|
|
10046
|
+
"at",
|
|
10047
|
+
"to",
|
|
10048
|
+
"for",
|
|
10049
|
+
"of",
|
|
10050
|
+
"with",
|
|
10051
|
+
"by",
|
|
10052
|
+
"from",
|
|
10053
|
+
"is",
|
|
10054
|
+
"are",
|
|
10055
|
+
"was",
|
|
10056
|
+
"were",
|
|
10057
|
+
"be",
|
|
10058
|
+
"been",
|
|
10059
|
+
"has",
|
|
10060
|
+
"have",
|
|
10061
|
+
"had",
|
|
10062
|
+
"do",
|
|
10063
|
+
"does",
|
|
10064
|
+
"did",
|
|
10065
|
+
"will",
|
|
10066
|
+
"would",
|
|
10067
|
+
"could",
|
|
10068
|
+
"should",
|
|
10069
|
+
"may",
|
|
10070
|
+
"might",
|
|
10071
|
+
"can",
|
|
10072
|
+
"not",
|
|
10073
|
+
"no",
|
|
10074
|
+
"if",
|
|
10075
|
+
"then",
|
|
10076
|
+
"than",
|
|
10077
|
+
"that",
|
|
10078
|
+
"this",
|
|
10079
|
+
"it",
|
|
10080
|
+
"its",
|
|
10081
|
+
"all",
|
|
10082
|
+
"each",
|
|
10083
|
+
"every",
|
|
10084
|
+
"both",
|
|
10085
|
+
"as",
|
|
10086
|
+
"so",
|
|
10087
|
+
"up",
|
|
10088
|
+
"out",
|
|
10089
|
+
"about",
|
|
10090
|
+
"into",
|
|
10091
|
+
"over",
|
|
10092
|
+
"after",
|
|
10093
|
+
"before",
|
|
10094
|
+
"between",
|
|
10095
|
+
"under",
|
|
10096
|
+
"above",
|
|
10097
|
+
"such",
|
|
10098
|
+
"only",
|
|
10099
|
+
"also",
|
|
10100
|
+
"just",
|
|
10101
|
+
"more",
|
|
10102
|
+
"most",
|
|
10103
|
+
"other",
|
|
10104
|
+
"some",
|
|
10105
|
+
"any",
|
|
10106
|
+
"new",
|
|
10107
|
+
"when",
|
|
10108
|
+
"how",
|
|
10109
|
+
"what",
|
|
10110
|
+
"which",
|
|
10111
|
+
"who",
|
|
10112
|
+
"add",
|
|
10113
|
+
"create",
|
|
10114
|
+
"build",
|
|
10115
|
+
"implement",
|
|
10116
|
+
"make",
|
|
10117
|
+
"update",
|
|
10118
|
+
"fix",
|
|
10119
|
+
"use",
|
|
10120
|
+
"via",
|
|
10121
|
+
"show",
|
|
10122
|
+
"display",
|
|
10123
|
+
"view",
|
|
10124
|
+
"page",
|
|
10125
|
+
"data",
|
|
10126
|
+
"based",
|
|
10127
|
+
"using",
|
|
10128
|
+
"task",
|
|
10129
|
+
"feature",
|
|
10130
|
+
"system",
|
|
10131
|
+
"tool",
|
|
10132
|
+
"mode",
|
|
10133
|
+
"field",
|
|
10134
|
+
"type",
|
|
10135
|
+
"status",
|
|
10136
|
+
"current",
|
|
10137
|
+
"default",
|
|
10138
|
+
"existing",
|
|
10139
|
+
"need",
|
|
10140
|
+
"instead",
|
|
10141
|
+
"allow",
|
|
10142
|
+
"change"
|
|
10143
|
+
]);
|
|
10144
|
+
function extractSearchTerms(title, notes) {
|
|
10145
|
+
const combined = `${title} ${notes ?? ""}`;
|
|
10146
|
+
const camelCase = combined.match(/[a-z][a-zA-Z]{5,}/g) ?? [];
|
|
10147
|
+
const snakeCase = combined.match(/[a-z]+_[a-z_]+/g) ?? [];
|
|
10148
|
+
const hyphenated = combined.match(/[a-z]+-[a-z]+-?[a-z]*/g) ?? [];
|
|
10149
|
+
const filePaths = combined.match(/[\w/.-]+\.(ts|tsx|js|jsx|sql|md)/g) ?? [];
|
|
10150
|
+
const words = combined.toLowerCase().replace(/[^a-z0-9\s_-]/g, " ").split(/\s+/).filter((w) => w.length >= 4 && !STOP_WORDS.has(w));
|
|
10151
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10152
|
+
const terms = [];
|
|
10153
|
+
for (const group of [filePaths, camelCase, snakeCase, hyphenated, words]) {
|
|
10154
|
+
for (const term of group) {
|
|
10155
|
+
const normalized = term.toLowerCase();
|
|
10156
|
+
if (!seen.has(normalized) && normalized.length >= 4) {
|
|
10157
|
+
seen.add(normalized);
|
|
10158
|
+
terms.push(term);
|
|
10159
|
+
}
|
|
10160
|
+
}
|
|
10161
|
+
}
|
|
10162
|
+
return terms.slice(0, 8);
|
|
10163
|
+
}
|
|
10164
|
+
function grepForTerm(projectRoot, term) {
|
|
10165
|
+
try {
|
|
10166
|
+
const result = execSync2(
|
|
10167
|
+
`grep -rl --include='*.ts' --include='*.tsx' --include='*.js' --include='*.sql' --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=.git --exclude-dir=.next ${JSON.stringify(term)} ${JSON.stringify(projectRoot)} 2>/dev/null | head -5`,
|
|
10168
|
+
{ encoding: "utf-8", timeout: 3e3 }
|
|
10169
|
+
);
|
|
10170
|
+
return result.trim().split("\n").filter(Boolean).map(
|
|
10171
|
+
(p) => p.replace(projectRoot + "/", "")
|
|
10172
|
+
);
|
|
10173
|
+
} catch {
|
|
10174
|
+
return [];
|
|
10175
|
+
}
|
|
10176
|
+
}
|
|
10177
|
+
function scanCodebaseForTasks(projectRoot, tasks) {
|
|
10178
|
+
if (tasks.length === 0) return "";
|
|
10179
|
+
const startTime = Date.now();
|
|
10180
|
+
const results = [];
|
|
10181
|
+
for (const task of tasks) {
|
|
10182
|
+
const terms = extractSearchTerms(task.title, task.notes);
|
|
10183
|
+
if (terms.length === 0) continue;
|
|
10184
|
+
const matches = [];
|
|
10185
|
+
for (const term of terms) {
|
|
10186
|
+
if (term.length < 4) continue;
|
|
10187
|
+
const files = grepForTerm(projectRoot, term);
|
|
10188
|
+
if (files.length > 0) {
|
|
10189
|
+
matches.push({ term, files });
|
|
10190
|
+
}
|
|
10191
|
+
if (Date.now() - startTime > 5e3) {
|
|
10192
|
+
console.error(`[codebase-scan] timeout after ${Date.now() - startTime}ms \u2014 partial results returned`);
|
|
10193
|
+
break;
|
|
10194
|
+
}
|
|
10195
|
+
}
|
|
10196
|
+
if (matches.length > 0) {
|
|
10197
|
+
results.push({ taskId: task.id, terms, matches });
|
|
10198
|
+
}
|
|
10199
|
+
if (Date.now() - startTime > 5e3) break;
|
|
10200
|
+
}
|
|
10201
|
+
if (results.length === 0) return "";
|
|
10202
|
+
const elapsed = Date.now() - startTime;
|
|
10203
|
+
console.error(`[codebase-scan] scanned ${tasks.length} tasks in ${elapsed}ms \u2014 ${results.length} with matches`);
|
|
10204
|
+
const lines = [
|
|
10205
|
+
`Codebase scan found existing implementations for ${results.length}/${tasks.length} candidate tasks (${elapsed}ms):`,
|
|
10206
|
+
""
|
|
10207
|
+
];
|
|
10208
|
+
for (const result of results) {
|
|
10209
|
+
lines.push(`**${result.taskId}:**`);
|
|
10210
|
+
for (const match of result.matches.slice(0, 3)) {
|
|
10211
|
+
const fileList = match.files.slice(0, 3).join(", ");
|
|
10212
|
+
const moreCount = match.files.length > 3 ? ` (+${match.files.length - 3} more)` : "";
|
|
10213
|
+
lines.push(` - "${match.term}" \u2192 ${fileList}${moreCount}`);
|
|
10214
|
+
}
|
|
10215
|
+
lines.push("");
|
|
10216
|
+
}
|
|
10217
|
+
return lines.join("\n").trim();
|
|
10218
|
+
}
|
|
10219
|
+
|
|
9948
10220
|
// src/lib/slack.ts
|
|
9949
10221
|
async function sendSlackWebhook(webhookUrl, summary, header = "PAPI Strategy Review") {
|
|
9950
10222
|
if (!webhookUrl) return void 0;
|
|
@@ -10004,6 +10276,9 @@ Task: [title]
|
|
|
10004
10276
|
Cycle: [N]
|
|
10005
10277
|
Why now: [justification]
|
|
10006
10278
|
|
|
10279
|
+
DEPENDS ON
|
|
10280
|
+
[Optional \u2014 comma-separated task IDs this task depends on (e.g. "task-123, task-124"). Include only when another task in this same cycle must be built first because this task consumes artifacts it creates (e.g. new adapter method, new type, new migration). The builder will reuse the upstream task's branch so dependent commits stack on the same branch for a single PR. Omit this section entirely if there are no intra-cycle dependencies.]
|
|
10281
|
+
|
|
10007
10282
|
SCOPE (DO THIS)
|
|
10008
10283
|
[specific deliverables \u2014 write for the simplest viable path first]
|
|
10009
10284
|
|
|
@@ -10192,8 +10467,11 @@ Standard planning cycle with full board review.
|
|
|
10192
10467
|
- **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
|
|
10193
10468
|
Within the same priority level, prefer tasks with the highest **impact-to-effort ratio**. Impact is measured by: (a) strategic alignment \u2014 does it advance the current horizon/phase? (b) unlocks other work \u2014 are tasks blocked by this? (c) user-facing \u2014 does it change what users see? (d) compounds over time \u2014 does it make future cycles faster? A high-impact Medium task beats a low-impact Small task at the same priority level. Justify in 2-3 sentences.
|
|
10194
10469
|
**Blocked tasks:** Tasks with status "Blocked" MUST be skipped during task selection \u2014 they are waiting on external dependencies or gates and cannot be built. Do NOT generate BUILD HANDOFFs for blocked tasks. Do NOT recommend blocked tasks. If a blocked task's gate has been resolved (check the notes and recent build reports), emit a \`boardCorrections\` entry to move it back to Backlog. Report blocked task count in the cycle log.
|
|
10195
|
-
**Cycle sizing:** Size the cycle based on what the selected tasks actually require \u2014 not a fixed budget. Select the highest-priority unblocked tasks, estimate each one's effort from its scope, and let the total emerge
|
|
10470
|
+
**Cycle sizing:** Size the cycle based on what the selected tasks actually require \u2014 not a fixed budget. Select the highest-priority unblocked tasks, estimate each one's effort from its scope, and let the total emerge. The historical average effort from Methodology Trends is a reference point for calibration, not a target or floor. A healthy cycle has 6-10 tasks. Cycles with fewer than 5 tasks require explicit justification in the cycle log \u2014 explain why more tasks could not be included. When the backlog has 10+ tasks, the cycle SHOULD have 6+ tasks \u2014 undersized cycles waste planning overhead relative to the available work. If fewer than 5 tasks qualify after filtering (blocked, deferred, raw), check Deferred tasks \u2014 some may be ready to un-defer via a \`boardCorrections\` entry. A 1-task cycle is almost never correct. Prefer grouping tasks by module or similarity \u2014 reduces context switching and enables shared branches during the build phase.
|
|
10471
|
+
**Theme-driven sizing:** Single-theme cycles (all tasks in the same module or epic) can absorb 25-30 effort points because builders maintain context across tasks. Mixed-theme cycles should stay at 15-20 effort points to limit context switching. Use the theme to determine the budget, not a fixed number.
|
|
10196
10472
|
**Theme coherence:** After selecting candidate tasks, check whether they form a coherent theme \u2014 all serving one goal, phase, or module. Single-theme cycles produce better build quality and less context switching. If the top candidates touch 3+ unrelated modules or epics, prefer regrouping around the highest-priority theme and deferring the outliers. Mixed-theme cycles are acceptable when justified (e.g. a P0 fix alongside P1 feature work), but the justification must appear in the cycle log. Name the theme in 3-5 words \u2014 it becomes the \`cycleLogTitle\`.
|
|
10473
|
+
**Epic-aware batching:** Epic is the primary grouping signal for theme coherence. When multiple candidate tasks share the same epic (e.g. "Onboarding Redesign", "Dashboard Polish"), prefer co-scheduling them \u2014 they solve connected problems and benefit from shared context during the build. Steps: (1) After filtering by priority, group eligible tasks by epic. (2) If an epic has 3+ eligible tasks, prefer scheduling 2-4 of them together over cherry-picking across epics. (3) Report the epic distribution in the cycle log (e.g. "4 tasks from Onboarding epic, 1 from Platform"). Priority still overrides: a P0 fix from a different epic always takes precedence.
|
|
10474
|
+
**Opportunity clustering:** If backlog tasks have an \`opportunity\` field populated, group them by opportunity before selecting. Tasks sharing the same opportunity solve the same user problem \u2014 co-scheduling them produces more coherent cycles. Report opportunity clusters in the cycle log when present (e.g. "3 tasks clustered under 'planner accuracy' opportunity").
|
|
10197
10475
|
|
|
10198
10476
|
8. **Cycle Log** \u2014 Write 5-10 line entry: what was triaged, what was recommended and why, observations, AD updates. Include a **Priority Recalibration** paragraph if any unreviewed task priorities were changed during triage (Step 2) \u2014 list each by ID with old \u2192 new priority and rationale. Include a **Priority Drift Suggestions** paragraph if reviewed task drift was detected (Step 3).
|
|
10199
10477
|
**Cycle Notes** \u2014 Optionally include 1-3 lines of cycle-level observations in \`cycleLogNotes\`: estimation accuracy patterns, recurring blockers, velocity trends, or dependency signals. These notes persist across cycles so future planning runs can learn from them. Use null if there are no noteworthy observations this cycle.
|
|
@@ -10212,11 +10490,17 @@ Standard planning cycle with full board review.
|
|
|
10212
10490
|
**Scope pre-check:** Before writing the SCOPE section of each handoff, cross-reference the task against the "Recently Shipped Capabilities" section in the context below (if present). For each candidate task: (1) check if the task's title or scope overlaps with any recently shipped task, (2) check if the FILES LIKELY TOUCHED overlap with files already modified in recent builds, (3) check the architecture notes from recent builds for patterns that already cover this task's scope. If >80% of a task's scope appears in recently shipped capabilities, recommend cancellation via \`boardCorrections\` or reduce the handoff scope to only the missing pieces \u2014 explicitly note what already exists. C126 task-728 was over-scoped because the planner assumed Blocked status needed creating from scratch \u2014 it already existed in types, DB, orient, and build_list. Over-scoped handoffs waste builder time on verification and cause estimation mismatches.
|
|
10213
10491
|
**Simplest Viable Path rule:** Before writing each BUILD HANDOFF, identify the simplest approach that satisfies the task's goal \u2014 the minimum change, fewest new abstractions, and smallest blast radius. Write the SCOPE (DO THIS) section for that simplest path FIRST. If you believe a more complex approach is warranted (new abstractions, multi-file refactors, framework changes), you MUST include a "WHY NOT SIMPLER" line in the handoff explaining why the simple path is insufficient. If you cannot articulate a concrete reason, use the simpler path. Pay special attention to tasks involving auth, data access, multi-user features, and infrastructure \u2014 these are the most common over-engineering targets.
|
|
10214
10492
|
**Maturity gate applies here:** Do NOT generate BUILD HANDOFFs for tasks that failed the maturity gate in step 6 (phase prerequisites not met, dependency chain incomplete). Raw tasks that the planner has scoped and upgraded to "investigated" in step 6 ARE eligible for handoffs.
|
|
10493
|
+
**Intra-cycle dependency detection:** After selecting cycle tasks, check every pair for build-order dependencies. Two tasks A and B have an intra-cycle dependency when A must be built before B because B consumes an artifact A creates \u2014 e.g. A adds a new adapter method that B calls, A creates a DB migration B depends on, A introduces a new shared type B imports, A refactors a utility B modifies. Signals: same module + adjacent scope (one is "add X", another is "use X"), or notes explicitly reference the other task. For each dependency detected:
|
|
10494
|
+
- Populate the DEPENDS ON section in the dependent task's BUILD HANDOFF with the upstream task ID(s).
|
|
10495
|
+
- Add a \`boardCorrections\` entry for the dependent task with \`updates.dependsOn\` set to the comma-separated upstream IDs \u2014 this persists the dependency so the builder's runtime can reuse the upstream branch.
|
|
10496
|
+
- Keep the SCOPE sections independent (each task still has its own deliverable) but note the ordering in "Why now" \u2014 e.g. "depends on task-123 completing the adapter method".
|
|
10497
|
+
Do NOT invent dependencies where tasks merely share a module \u2014 only real build-order coupling counts. Linear chains only \u2014 do not attempt to resolve multi-level graphs. When in doubt, omit the dependency and let the builder discover it.
|
|
10215
10498
|
**Security section guidance:** Each handoff includes a SECURITY CONSIDERATIONS section. Populate it when the task involves: data exposure risks (PII, secrets in logs/storage), secrets or credentials handling (API keys, tokens, env vars), auth/access control changes, or dependency security risks (new packages, version changes). For pure refactoring, documentation, prompt-text, or UI-only tasks, write "None \u2014 no security-relevant changes".
|
|
10216
10499
|
**Estimation calibration:** Estimate **XS** for: copy/text-only changes, single string replacements, config tweaks, and any task where the scope is "change words in an existing file" with no logic changes. Estimate **S** for: wiring existing adapter methods, adding API routes following established patterns, modifying prompts, or documentation-only changes. Default to S for pattern-following work. Only use M when genuine new architecture, new DB tables, or multi-file architectural changes are needed. Historical data shows systematic over-estimation (198 over vs 8 under out of 528 tasks) \u2014 when in doubt, estimate smaller. If an "Estimation Calibration (Historical)" section is provided in the context below, use its data to adjust your estimates \u2014 it shows how often each estimated size matched the actual effort. Pay special attention to systematic over/under-estimation patterns (e.g. if M\u2192S happens frequently, estimate S instead of M for similar work).
|
|
10217
10500
|
**Reference docs:** If a task's notes include a \`Reference:\` path (e.g. \`Reference: docs/architecture/papi-brain-v1.md\`), include a REFERENCE DOCS section in the BUILD HANDOFF with those paths. This tells the builder to read the referenced doc for background context before implementing. Do NOT omit or summarise the reference \u2014 pass it through so the builder can access the full document. Only tasks with explicit \`Reference:\` paths in their notes should have this section.
|
|
10218
10501
|
**Pre-build verification:** EVERY handoff MUST include a PRE-BUILD VERIFICATION section listing 2-5 specific file paths the builder should read before implementing. Derive these from FILES LIKELY TOUCHED \u2014 pick the files most likely to already contain the target functionality. This is the #1 prevention mechanism for wasted build slots (C120, C125, C126 all scheduled already-shipped work). If the builder finds >80% of the scope already implemented, they report "already built" instead of re-implementing.
|
|
10219
10502
|
**Pre-mortem:** For projects with 10+ cycles, include a PRE-MORTEM section in every BUILD HANDOFF with 1-3 bullet points: (a) most likely technical blocker based on module history, (b) integration risk with adjacent systems, (c) scope creep signal \u2014 what the builder might be tempted to expand beyond scope. Draw from \`dead_ends\` and \`surprises\` in recent build reports for the same module. Omit this section entirely for projects with fewer than 10 cycles.
|
|
10503
|
+
**Build order in cycle log:** If any intra-cycle dependencies were detected in this cycle, include a "Build Order" paragraph in \`cycleLogNotes\` showing the recommended build sequence as arrow chains (e.g. "Build order: task-123 \u2192 task-124; task-130 standalone"). Skip this paragraph when no dependencies exist.
|
|
10220
10504
|
**Research task detection:** When a task's title starts with "Research:" or the task type is "research", add a RESEARCH OUTPUT section to the BUILD HANDOFF after ACCEPTANCE CRITERIA:
|
|
10221
10505
|
|
|
10222
10506
|
RESEARCH OUTPUT
|
|
@@ -10234,7 +10518,8 @@ Standard planning cycle with full board review.
|
|
|
10234
10518
|
**Idea task detection:** When a task's task type is "idea", add a scope clarification note to the BUILD HANDOFF:
|
|
10235
10519
|
- Add to SCOPE (DO THIS): "This task originated as an idea. Confirm the exact deliverable before implementing \u2014 check task notes and any referenced docs for intent. If scope is unclear, flag it in the build report surprises."
|
|
10236
10520
|
|
|
10237
|
-
**UI/visual task detection:**
|
|
10521
|
+
**UI/visual task detection:** Apply these additions ONLY to tasks whose PRIMARY scope is frontend visual work \u2014 the task's main deliverable must be a UI change, new component, visual design, or page. Do NOT apply to backend tasks, DB migrations, or prompt/config changes that merely mention a dashboard or page in passing. Signal: the task would fail if no .tsx/.css files were changed. If uncertain, skip the UI additions.
|
|
10522
|
+
When a task IS a UI task (primary scope is visual/frontend):
|
|
10238
10523
|
- Add to SCOPE: "Read \`.impeccable.md\` for brand palette, design principles, and audience context before writing any code. Use the \`frontend-design\` skill for implementation."
|
|
10239
10524
|
- For M/L UI tasks, add to SCOPE: "Use the full UI toolchain: Playground (design preview) \u2192 Frontend-design (build) \u2192 Playwright (verify). The playground is the quality bar. Expect 2-3 iterations."
|
|
10240
10525
|
- Add to ACCEPTANCE CRITERIA: "[ ] Visually verify rendered output in browser \u2014 provide localhost URL or screenshot to user for review." and "[ ] No raw IDs, abbreviations, or jargon visible without human-readable labels or tooltips."
|
|
@@ -10289,6 +10574,11 @@ var PLAN_FRAGMENT_BUG = `
|
|
|
10289
10574
|
var PLAN_FRAGMENT_IDEA = `
|
|
10290
10575
|
**Idea task detection:** When a task's task type is "idea", add a scope clarification note to the BUILD HANDOFF:
|
|
10291
10576
|
- Add to SCOPE (DO THIS): "This task originated as an idea. Confirm the exact deliverable before implementing \u2014 check task notes and any referenced docs for intent. If scope is unclear, flag it in the build report surprises."`;
|
|
10577
|
+
var PLAN_FRAGMENT_TASK = `
|
|
10578
|
+
**Task type detection:** When a task's task type is "task" (generic implementation task), add these handoff sections:
|
|
10579
|
+
- SCOPE (DO THIS) must include: a clear deliverable statement and what "done" looks like (e.g. "User can X", "Function returns Y", "Page renders Z").
|
|
10580
|
+
- Add to ACCEPTANCE CRITERIA: "[ ] Scope matches handoff \u2014 no unrelated code changed" and "[ ] Out-of-scope items documented if discovered during implementation."
|
|
10581
|
+
- Add a SCOPE BOUNDARY (DO NOT DO THIS) section with at least one explicit exclusion \u2014 state what this task is NOT responsible for.`;
|
|
10292
10582
|
var PLAN_FRAGMENT_SPIKE = `
|
|
10293
10583
|
**Spike task detection:** When a task's task type is "spike" or the title starts with "Spike:", apply these rules:
|
|
10294
10584
|
- Spikes are time-boxed investigations, not implementation tasks. The deliverable is a FINDING, not code.
|
|
@@ -10300,7 +10590,8 @@ var PLAN_FRAGMENT_SPIKE = `
|
|
|
10300
10590
|
- Keep SCOPE BOUNDARY, SECURITY CONSIDERATIONS, and PRE-BUILD VERIFICATION as normal.
|
|
10301
10591
|
- Spikes should be estimated conservatively: XS or S. If a spike needs M+ effort, it's not a spike \u2014 reclassify as a research task.`;
|
|
10302
10592
|
var PLAN_FRAGMENT_UI = `
|
|
10303
|
-
**UI/visual task detection:**
|
|
10593
|
+
**UI/visual task detection:** Apply these additions ONLY to tasks whose PRIMARY scope is frontend visual work \u2014 the task's main deliverable must be a UI change, new component, visual design, or page. Do NOT apply to backend tasks, DB migrations, or prompt/config changes that merely mention a dashboard or page in passing. Signal: the task would fail if no .tsx/.css files were changed. If uncertain, skip the UI additions.
|
|
10594
|
+
When a task IS a UI task (primary scope is visual/frontend):
|
|
10304
10595
|
- Add to SCOPE: "Read \`.impeccable.md\` for brand palette, design principles, and audience context before writing any code. Use the \`frontend-design\` skill for implementation."
|
|
10305
10596
|
- For M/L UI tasks, add to SCOPE: "Use the full UI toolchain: Playground (design preview) \u2192 Frontend-design (build) \u2192 Playwright (verify). The playground is the quality bar. Expect 2-3 iterations."
|
|
10306
10597
|
- Add to ACCEPTANCE CRITERIA: "[ ] Visually verify rendered output in browser \u2014 provide localhost URL or screenshot to user for review." and "[ ] No raw IDs, abbreviations, or jargon visible without human-readable labels or tooltips."
|
|
@@ -10379,8 +10670,11 @@ Standard planning cycle with full board review.
|
|
|
10379
10670
|
- **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
|
|
10380
10671
|
Within the same priority level, prefer tasks with the highest **impact-to-effort ratio**. Impact is measured by: (a) strategic alignment \u2014 does it advance the current horizon/phase? (b) unlocks other work \u2014 are tasks blocked by this? (c) user-facing \u2014 does it change what users see? (d) compounds over time \u2014 does it make future cycles faster? A high-impact Medium task beats a low-impact Small task at the same priority level. Justify in 2-3 sentences.
|
|
10381
10672
|
**Blocked tasks:** Tasks with status "Blocked" MUST be skipped during task selection \u2014 they are waiting on external dependencies or gates and cannot be built. Do NOT generate BUILD HANDOFFs for blocked tasks. Do NOT recommend blocked tasks. If a blocked task's gate has been resolved (check the notes and recent build reports), emit a \`boardCorrections\` entry to move it back to Backlog. Report blocked task count in the cycle log.
|
|
10382
|
-
**Cycle sizing:** Size the cycle based on what the selected tasks actually require \u2014 not a fixed budget. Select the highest-priority unblocked tasks, estimate each one's effort from its scope, and let the total emerge
|
|
10673
|
+
**Cycle sizing:** Size the cycle based on what the selected tasks actually require \u2014 not a fixed budget. Select the highest-priority unblocked tasks, estimate each one's effort from its scope, and let the total emerge. The historical average effort from Methodology Trends is a reference point for calibration, not a target or floor. A healthy cycle has 6-10 tasks. Cycles with fewer than 5 tasks require explicit justification in the cycle log \u2014 explain why more tasks could not be included. When the backlog has 10+ tasks, the cycle SHOULD have 6+ tasks \u2014 undersized cycles waste planning overhead relative to the available work. If fewer than 5 tasks qualify after filtering (blocked, deferred, raw), check Deferred tasks \u2014 some may be ready to un-defer via a \`boardCorrections\` entry. A 1-task cycle is almost never correct. Prefer grouping tasks by module or similarity \u2014 reduces context switching and enables shared branches during the build phase.
|
|
10674
|
+
**Theme-driven sizing:** Single-theme cycles (all tasks in the same module or epic) can absorb 25-30 effort points because builders maintain context across tasks. Mixed-theme cycles should stay at 15-20 effort points to limit context switching. Use the theme to determine the budget, not a fixed number.
|
|
10383
10675
|
**Theme coherence:** After selecting candidate tasks, check whether they form a coherent theme \u2014 all serving one goal, phase, or module. Single-theme cycles produce better build quality and less context switching. If the top candidates touch 3+ unrelated modules or epics, prefer regrouping around the highest-priority theme and deferring the outliers. Mixed-theme cycles are acceptable when justified (e.g. a P0 fix alongside P1 feature work), but the justification must appear in the cycle log. Name the theme in 3-5 words \u2014 it becomes the \`cycleLogTitle\`.
|
|
10676
|
+
**Epic-aware batching:** Epic is the primary grouping signal for theme coherence. When multiple candidate tasks share the same epic (e.g. "Onboarding Redesign", "Dashboard Polish"), prefer co-scheduling them \u2014 they solve connected problems and benefit from shared context during the build. Steps: (1) After filtering by priority, group eligible tasks by epic. (2) If an epic has 3+ eligible tasks, prefer scheduling 2-4 of them together over cherry-picking across epics. (3) Report the epic distribution in the cycle log (e.g. "4 tasks from Onboarding epic, 1 from Platform"). Priority still overrides: a P0 fix from a different epic always takes precedence.
|
|
10677
|
+
**Opportunity clustering:** If backlog tasks have an \`opportunity\` field populated, group them by opportunity before selecting. Tasks sharing the same opportunity solve the same user problem \u2014 co-scheduling them produces more coherent cycles. Report opportunity clusters in the cycle log when present (e.g. "3 tasks clustered under 'planner accuracy' opportunity").
|
|
10384
10678
|
|
|
10385
10679
|
8. **Cycle Log** \u2014 Write 5-10 line entry: what was triaged, what was recommended and why, observations, AD updates. Include a **Priority Recalibration** paragraph if any unreviewed task priorities were changed during triage (Step 2) \u2014 list each by ID with old \u2192 new priority and rationale. Include a **Priority Drift Suggestions** paragraph if reviewed task drift was detected (Step 3).
|
|
10386
10680
|
**Cycle Notes** \u2014 Optionally include 1-3 lines of cycle-level observations in \`cycleLogNotes\`: estimation accuracy patterns, recurring blockers, velocity trends, or dependency signals. These notes persist across cycles so future planning runs can learn from them. Use null if there are no noteworthy observations this cycle.
|
|
@@ -10403,11 +10697,14 @@ Standard planning cycle with full board review.
|
|
|
10403
10697
|
**Estimation calibration:** Estimate **XS** for: copy/text-only changes, single string replacements, config tweaks, and any task where the scope is "change words in an existing file" with no logic changes. Estimate **S** for: wiring existing adapter methods, adding API routes following established patterns, modifying prompts, or documentation-only changes. Default to S for pattern-following work. Only use M when genuine new architecture, new DB tables, or multi-file architectural changes are needed. Historical data shows systematic over-estimation (198 over vs 8 under out of 528 tasks) \u2014 when in doubt, estimate smaller. If an "Estimation Calibration (Historical)" section is provided in the context below, use its data to adjust your estimates \u2014 it shows how often each estimated size matched the actual effort. Pay special attention to systematic over/under-estimation patterns (e.g. if M\u2192S happens frequently, estimate S instead of M for similar work).
|
|
10404
10698
|
**Reference docs:** If a task's notes include a \`Reference:\` path (e.g. \`Reference: docs/architecture/papi-brain-v1.md\`), include a REFERENCE DOCS section in the BUILD HANDOFF with those paths. This tells the builder to read the referenced doc for background context before implementing. Do NOT omit or summarise the reference \u2014 pass it through so the builder can access the full document. Only tasks with explicit \`Reference:\` paths in their notes should have this section.
|
|
10405
10699
|
**Pre-build verification:** EVERY handoff MUST include a PRE-BUILD VERIFICATION section listing 2-5 specific file paths the builder should read before implementing. Derive these from FILES LIKELY TOUCHED \u2014 pick the files most likely to already contain the target functionality. This is the #1 prevention mechanism for wasted build slots (C120, C125, C126 all scheduled already-shipped work). If the builder finds >80% of the scope already implemented, they report "already built" instead of re-implementing.
|
|
10406
|
-
**Pre-mortem:** For projects with 10+ cycles, include a PRE-MORTEM section in every BUILD HANDOFF with 1-3 bullet points: (a) most likely technical blocker based on module history, (b) integration risk with adjacent systems, (c) scope creep signal \u2014 what the builder might be tempted to expand beyond scope. Draw from \`dead_ends\` and \`surprises\` in recent build reports for the same module. Omit this section entirely for projects with fewer than 10 cycles
|
|
10700
|
+
**Pre-mortem:** For projects with 10+ cycles, include a PRE-MORTEM section in every BUILD HANDOFF with 1-3 bullet points: (a) most likely technical blocker based on module history, (b) integration risk with adjacent systems, (c) scope creep signal \u2014 what the builder might be tempted to expand beyond scope. Draw from \`dead_ends\` and \`surprises\` in recent build reports for the same module. Omit this section entirely for projects with fewer than 10 cycles.
|
|
10701
|
+
**Intra-cycle dependency detection:** After selecting cycle tasks, check every pair for build-order dependencies. Two tasks A and B have an intra-cycle dependency when A must be built before B because B consumes an artifact A creates \u2014 e.g. A adds a new adapter method that B calls, A creates a DB migration B depends on, A introduces a new shared type B imports, A refactors a utility B modifies. Signals: same module + adjacent scope (one is "add X", another is "use X"), or notes explicitly reference the other task. For each dependency detected: (a) populate the DEPENDS ON section in the dependent task's BUILD HANDOFF with the upstream task ID(s); (b) add a \`boardCorrections\` entry for the dependent task with \`updates.dependsOn\` set to the comma-separated upstream IDs \u2014 this persists the dependency so the builder's runtime can reuse the upstream branch; (c) keep SCOPE sections independent but note the ordering in "Why now". Do NOT invent dependencies where tasks merely share a module \u2014 only real build-order coupling counts. Linear chains only \u2014 no multi-level graph resolution. When in doubt, omit.
|
|
10702
|
+
**Build order in cycle log:** If intra-cycle dependencies were detected, include a "Build order:" line in \`cycleLogNotes\` showing the recommended sequence as arrow chains (e.g. "Build order: task-123 \u2192 task-124; task-130 standalone"). Skip when no dependencies exist.`);
|
|
10407
10703
|
if (flags.hasResearchTasks) parts.push(PLAN_FRAGMENT_RESEARCH);
|
|
10408
10704
|
if (flags.hasBugTasks) parts.push(PLAN_FRAGMENT_BUG);
|
|
10409
10705
|
if (flags.hasIdeaTasks) parts.push(PLAN_FRAGMENT_IDEA);
|
|
10410
10706
|
if (flags.hasSpikeTasks) parts.push(PLAN_FRAGMENT_SPIKE);
|
|
10707
|
+
if (flags.hasTaskTasks) parts.push(PLAN_FRAGMENT_TASK);
|
|
10411
10708
|
if (flags.hasUITasks) parts.push(PLAN_FRAGMENT_UI);
|
|
10412
10709
|
parts.push(`
|
|
10413
10710
|
11. **New Tasks (max 3 per cycle)** \u2014 Actively mine the Recent Build Reports for task candidates. For each report, check:
|
|
@@ -10472,6 +10769,9 @@ function buildPlanUserMessage(ctx) {
|
|
|
10472
10769
|
);
|
|
10473
10770
|
}
|
|
10474
10771
|
parts.push("", "---", "", "## PROJECT CONTEXT", "");
|
|
10772
|
+
if (ctx.contextTier) {
|
|
10773
|
+
parts.push(`**Context tier:** ${ctx.contextTier}`, "");
|
|
10774
|
+
}
|
|
10475
10775
|
parts.push("### Product Brief", "", ctx.productBrief, "");
|
|
10476
10776
|
if (ctx.northStar) {
|
|
10477
10777
|
parts.push("### North Star (current)", "", ctx.northStar, "");
|
|
@@ -10489,12 +10789,18 @@ function buildPlanUserMessage(ctx) {
|
|
|
10489
10789
|
if (ctx.cycleLog) {
|
|
10490
10790
|
parts.push("### Cycle Log", "", ctx.cycleLog, "");
|
|
10491
10791
|
}
|
|
10792
|
+
if (ctx.strategyReviewCadence) {
|
|
10793
|
+
parts.push("### Strategy Review Cadence (computed from DB)", "", ctx.strategyReviewCadence, "");
|
|
10794
|
+
}
|
|
10492
10795
|
if (ctx.board) {
|
|
10493
10796
|
parts.push("### Board", "", ctx.board, "");
|
|
10494
10797
|
}
|
|
10495
10798
|
if (ctx.preAssignedTasks) {
|
|
10496
10799
|
parts.push("### Pre-Assigned Tasks", "", ctx.preAssignedTasks, "");
|
|
10497
10800
|
}
|
|
10801
|
+
if (ctx.codebaseScan) {
|
|
10802
|
+
parts.push("### Codebase Scan (existing implementations)", "", ctx.codebaseScan, "");
|
|
10803
|
+
}
|
|
10498
10804
|
if (ctx.buildPatterns) {
|
|
10499
10805
|
parts.push("### Build Patterns", "", ctx.buildPatterns, "");
|
|
10500
10806
|
}
|
|
@@ -10845,7 +11151,8 @@ After your natural language output, include this EXACT format on its own line:
|
|
|
10845
11151
|
"category": "friction | methodology | signal | commercial",
|
|
10846
11152
|
"content": "string \u2014 specific observation from using PAPI on this project (e.g. 'deprioritise clears handoffs unnecessarily, wasting planner tokens')"
|
|
10847
11153
|
}
|
|
10848
|
-
]
|
|
11154
|
+
],
|
|
11155
|
+
"northStar": "string or null \u2014 the current North Star statement. Include if you assessed it in section 4 and it is still accurate (copy the current statement verbatim). Include the updated version if you revised it. Use null ONLY if no North Star has ever been set for this project."
|
|
10849
11156
|
}
|
|
10850
11157
|
\`\`\`
|
|
10851
11158
|
|
|
@@ -11032,7 +11339,8 @@ After your natural language output, include this EXACT format on its own line:
|
|
|
11032
11339
|
},
|
|
11033
11340
|
"oldLabel": "string \u2014 only for modify/remove: the previous phase label so tasks can be migrated"
|
|
11034
11341
|
}
|
|
11035
|
-
]
|
|
11342
|
+
],
|
|
11343
|
+
"northStar": "string or null \u2014 include the North Star statement if this strategic change defines or revises the project North Star. Use null if the change does not affect the North Star."
|
|
11036
11344
|
}
|
|
11037
11345
|
\`\`\`
|
|
11038
11346
|
|
|
@@ -11088,6 +11396,9 @@ Task: [title]
|
|
|
11088
11396
|
Cycle: [N]
|
|
11089
11397
|
Why now: [justification]
|
|
11090
11398
|
|
|
11399
|
+
DEPENDS ON
|
|
11400
|
+
[Optional \u2014 comma-separated task IDs this task depends on (e.g. "task-123, task-124"). Include only when another task in this same cycle must be built first because this task consumes artifacts it creates (e.g. new adapter method, new type, new migration). The builder will reuse the upstream task's branch so dependent commits stack on the same branch for a single PR. Omit this section entirely if there are no intra-cycle dependencies.]
|
|
11401
|
+
|
|
11091
11402
|
SCOPE (DO THIS)
|
|
11092
11403
|
[specific deliverables \u2014 write for the simplest viable path first]
|
|
11093
11404
|
|
|
@@ -11388,6 +11699,32 @@ async function getPrompt(name) {
|
|
|
11388
11699
|
}
|
|
11389
11700
|
|
|
11390
11701
|
// src/services/plan.ts
|
|
11702
|
+
function determineContextTier(cycleCount) {
|
|
11703
|
+
if (cycleCount <= 5) return 1;
|
|
11704
|
+
if (cycleCount <= 20) return 2;
|
|
11705
|
+
return 3;
|
|
11706
|
+
}
|
|
11707
|
+
function applyContextTier(ctx, cycleCount) {
|
|
11708
|
+
const tier = determineContextTier(cycleCount);
|
|
11709
|
+
const label = tier === 1 ? "Tier 1 (cycles 1-5)" : tier === 2 ? "Tier 2 (cycles 6-20)" : "Tier 3 (cycles 21+)";
|
|
11710
|
+
if (tier <= 2) {
|
|
11711
|
+
ctx.strategyRecommendations = void 0;
|
|
11712
|
+
ctx.dogfoodEntries = void 0;
|
|
11713
|
+
}
|
|
11714
|
+
if (tier === 1) {
|
|
11715
|
+
ctx.methodologyMetrics = void 0;
|
|
11716
|
+
ctx.carryForwardStaleness = void 0;
|
|
11717
|
+
ctx.discoveryCanvas = void 0;
|
|
11718
|
+
ctx.estimationCalibration = void 0;
|
|
11719
|
+
ctx.buildPatterns = void 0;
|
|
11720
|
+
ctx.reviewPatterns = void 0;
|
|
11721
|
+
ctx.horizonContext = void 0;
|
|
11722
|
+
ctx.registeredDocs = void 0;
|
|
11723
|
+
ctx.recentReviews = void 0;
|
|
11724
|
+
ctx.strategyReviewCadence = void 0;
|
|
11725
|
+
}
|
|
11726
|
+
return { tier, label };
|
|
11727
|
+
}
|
|
11391
11728
|
function determineMode(totalCycles) {
|
|
11392
11729
|
if (totalCycles === 0) return "bootstrap";
|
|
11393
11730
|
return "full";
|
|
@@ -11622,6 +11959,7 @@ function detectBoardFlags(tasks) {
|
|
|
11622
11959
|
let hasResearchTasks = false;
|
|
11623
11960
|
let hasIdeaTasks = false;
|
|
11624
11961
|
let hasSpikeTasks = false;
|
|
11962
|
+
let hasTaskTasks = false;
|
|
11625
11963
|
let hasUITasks = false;
|
|
11626
11964
|
const uiKeywords = /\b(visual|design|UI|styling|refresh|frontend|landing page|hero|carousel|theme|layout|cockpit|dashboard|page)\b/i;
|
|
11627
11965
|
for (const t of tasks) {
|
|
@@ -11629,9 +11967,10 @@ function detectBoardFlags(tasks) {
|
|
|
11629
11967
|
if (t.taskType === "research" || /^Research:/i.test(t.title)) hasResearchTasks = true;
|
|
11630
11968
|
if (t.taskType === "idea") hasIdeaTasks = true;
|
|
11631
11969
|
if (t.taskType === "spike" || /^Spike:/i.test(t.title)) hasSpikeTasks = true;
|
|
11970
|
+
if (t.taskType === "task") hasTaskTasks = true;
|
|
11632
11971
|
if (uiKeywords.test(t.title) || uiKeywords.test(t.notes ?? "")) hasUITasks = true;
|
|
11633
11972
|
}
|
|
11634
|
-
return { hasBugTasks, hasResearchTasks, hasIdeaTasks, hasSpikeTasks, hasUITasks };
|
|
11973
|
+
return { hasBugTasks, hasResearchTasks, hasIdeaTasks, hasSpikeTasks, hasTaskTasks, hasUITasks };
|
|
11635
11974
|
}
|
|
11636
11975
|
function detectBoardFlagsFromText(boardText) {
|
|
11637
11976
|
return {
|
|
@@ -11639,6 +11978,7 @@ function detectBoardFlagsFromText(boardText) {
|
|
|
11639
11978
|
hasResearchTasks: /\b(research|Research:)\b/i.test(boardText),
|
|
11640
11979
|
hasIdeaTasks: /\bidea\b/i.test(boardText),
|
|
11641
11980
|
hasSpikeTasks: /\b(spike|Spike:)\b/i.test(boardText),
|
|
11981
|
+
hasTaskTasks: /\btask\b/i.test(boardText),
|
|
11642
11982
|
hasUITasks: /\b(visual|design|UI|styling|refresh|frontend|landing page|hero|carousel|theme|layout|cockpit|dashboard|page)\b/i.test(boardText)
|
|
11643
11983
|
};
|
|
11644
11984
|
}
|
|
@@ -11749,10 +12089,11 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
|
|
|
11749
12089
|
timings["getPlanContextSummary"] = t();
|
|
11750
12090
|
if (leanSummary) {
|
|
11751
12091
|
t = startTimer();
|
|
11752
|
-
|
|
12092
|
+
const [metricsSnapshotsRaw, reviews2] = await Promise.all([
|
|
11753
12093
|
adapter2.readCycleMetrics(),
|
|
11754
12094
|
adapter2.getRecentReviews(5)
|
|
11755
12095
|
]);
|
|
12096
|
+
let metricsSnapshots2 = metricsSnapshotsRaw;
|
|
11756
12097
|
let leanBuildReports = [];
|
|
11757
12098
|
try {
|
|
11758
12099
|
leanBuildReports = await adapter2.getRecentBuildReports(50);
|
|
@@ -11849,6 +12190,9 @@ ${lines.join("\n")}`;
|
|
|
11849
12190
|
]);
|
|
11850
12191
|
timings["total"] = totalTimer();
|
|
11851
12192
|
console.error(`[plan-perf] assembleContext (lean): ${JSON.stringify(timings)}ms`);
|
|
12193
|
+
const gap = health.cyclesSinceLastStrategyReview;
|
|
12194
|
+
const lastReviewCycle = health.totalCycles - gap;
|
|
12195
|
+
const strategyReviewCadence = gap <= 0 ? `\u2713 Strategy review completed this cycle (C${health.totalCycles}). No carry-forward needed.` : gap < 5 ? `\u2713 Strategy review on track \u2014 last review was C${lastReviewCycle} (${gap} cycle(s) ago). Next due: C${lastReviewCycle + 5}.` : `\u26A0\uFE0F Strategy review overdue \u2014 last review was C${lastReviewCycle} (${gap} cycles ago). Due now.`;
|
|
11852
12196
|
let ctx2 = {
|
|
11853
12197
|
mode,
|
|
11854
12198
|
cycleNumber: health.totalCycles,
|
|
@@ -11870,8 +12214,12 @@ ${lines.join("\n")}`;
|
|
|
11870
12214
|
boardFlags,
|
|
11871
12215
|
carryForwardStaleness: carryForwardStalenessLean,
|
|
11872
12216
|
preAssignedTasks: preAssignedTextLean,
|
|
11873
|
-
recentlyShippedCapabilities: recentlyShippedLean
|
|
12217
|
+
recentlyShippedCapabilities: recentlyShippedLean,
|
|
12218
|
+
strategyReviewCadence
|
|
11874
12219
|
};
|
|
12220
|
+
const { label: leanTierLabel } = applyContextTier(ctx2, health.totalCycles);
|
|
12221
|
+
ctx2.contextTier = leanTierLabel;
|
|
12222
|
+
console.error(`[plan-perf] context tier: ${leanTierLabel} (cycle ${health.totalCycles})`);
|
|
11875
12223
|
t = startTimer();
|
|
11876
12224
|
const prevHashes2 = contextHashesResult.status === "fulfilled" ? contextHashesResult.value : null;
|
|
11877
12225
|
const { ctx: diffedCtx2, newHashes: newHashes2, savedBytes: savedBytes2 } = applyContextDiff(ctx2, prevHashes2);
|
|
@@ -11933,7 +12281,8 @@ ${lines.join("\n")}`;
|
|
|
11933
12281
|
if (pendingRecsResultFull.status === "fulfilled" && pendingRecsResultFull.value.length > 0) {
|
|
11934
12282
|
strategyRecommendationsText = formatStrategyRecommendations(pendingRecsResultFull.value);
|
|
11935
12283
|
}
|
|
11936
|
-
const
|
|
12284
|
+
const filteredRaw = rawMetricsSnapshots.filter((s) => s.accuracy.length > 0 || s.velocity.length > 0);
|
|
12285
|
+
const metricsSnapshots = filteredRaw.length > 0 ? filteredRaw : computeSnapshotsFromBuildReports(allReportsForPatterns);
|
|
11937
12286
|
const discoveryCanvasTextFull = discoveryCanvasResultFull.status === "fulfilled" ? discoveryCanvasResultFull.value : void 0;
|
|
11938
12287
|
const taskCommentsTextFull = taskCommentsResultFull.status === "fulfilled" ? taskCommentsResultFull.value : void 0;
|
|
11939
12288
|
let registeredDocsTextFull;
|
|
@@ -11969,6 +12318,9 @@ ${lines.join("\n")}`;
|
|
|
11969
12318
|
const targetCycle = health.totalCycles + 1;
|
|
11970
12319
|
const preAssigned = strippedTasks.filter((t2) => t2.cycle === targetCycle);
|
|
11971
12320
|
const preAssignedText = formatPreAssignedTasks(preAssigned, targetCycle);
|
|
12321
|
+
const gapFull = health.cyclesSinceLastStrategyReview;
|
|
12322
|
+
const lastReviewCycleFull = health.totalCycles - gapFull;
|
|
12323
|
+
const strategyReviewCadenceFull = gapFull <= 0 ? `\u2713 Strategy review completed this cycle (C${health.totalCycles}). No carry-forward needed.` : gapFull < 5 ? `\u2713 Strategy review on track \u2014 last review was C${lastReviewCycleFull} (${gapFull} cycle(s) ago). Next due: C${lastReviewCycleFull + 5}.` : `\u26A0\uFE0F Strategy review overdue \u2014 last review was C${lastReviewCycleFull} (${gapFull} cycles ago). Due now.`;
|
|
11972
12324
|
let ctx = {
|
|
11973
12325
|
mode,
|
|
11974
12326
|
cycleNumber: health.totalCycles,
|
|
@@ -11992,8 +12344,12 @@ ${lines.join("\n")}`;
|
|
|
11992
12344
|
boardFlags: boardFlagsFull,
|
|
11993
12345
|
carryForwardStaleness: computeCarryForwardStaleness(log),
|
|
11994
12346
|
preAssignedTasks: preAssignedText,
|
|
11995
|
-
recentlyShippedCapabilities: formatRecentlyShippedCapabilities(
|
|
12347
|
+
recentlyShippedCapabilities: formatRecentlyShippedCapabilities(reports),
|
|
12348
|
+
strategyReviewCadence: strategyReviewCadenceFull
|
|
11996
12349
|
};
|
|
12350
|
+
const { label: fullTierLabel } = applyContextTier(ctx, health.totalCycles);
|
|
12351
|
+
ctx.contextTier = fullTierLabel;
|
|
12352
|
+
console.error(`[plan-perf] context tier: ${fullTierLabel} (cycle ${health.totalCycles})`);
|
|
11997
12353
|
const prevHashes = contextHashesResultFull.status === "fulfilled" ? contextHashesResultFull.value : null;
|
|
11998
12354
|
const { ctx: diffedCtx, newHashes, savedBytes } = applyContextDiff(ctx, prevHashes);
|
|
11999
12355
|
ctx = diffedCtx;
|
|
@@ -12183,7 +12539,15 @@ ${cleanContent}`;
|
|
|
12183
12539
|
taskCount: cycleTaskCount > 0 ? cycleTaskCount : void 0,
|
|
12184
12540
|
effortPoints: cycleEffortPoints > 0 ? cycleEffortPoints : void 0
|
|
12185
12541
|
});
|
|
12186
|
-
const
|
|
12542
|
+
const healthUpdates = {
|
|
12543
|
+
totalCycles: newCycleNumber,
|
|
12544
|
+
boardHealth: data.boardHealth,
|
|
12545
|
+
strategicDirection: data.strategicDirection
|
|
12546
|
+
};
|
|
12547
|
+
if (data.nextMode === "Full") {
|
|
12548
|
+
healthUpdates.lastFullMode = newCycleNumber;
|
|
12549
|
+
}
|
|
12550
|
+
const healthPromise = adapter2.setCycleHealth(healthUpdates);
|
|
12187
12551
|
const newTaskIdMap = /* @__PURE__ */ new Map();
|
|
12188
12552
|
const createTasksPromise = (async () => {
|
|
12189
12553
|
if (!data.newTasks || data.newTasks.length === 0) return;
|
|
@@ -12603,6 +12967,17 @@ async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnl
|
|
|
12603
12967
|
}
|
|
12604
12968
|
if (skipHandoffs) context.skipHandoffs = true;
|
|
12605
12969
|
t = startTimer();
|
|
12970
|
+
try {
|
|
12971
|
+
const scanTasks = await adapter2.queryBoard({ status: ["Backlog", "In Cycle", "Ready"] });
|
|
12972
|
+
const candidates = scanTasks.filter((task) => task.priority !== "P3 Low").slice(0, 15).map((task) => ({ id: task.id, title: task.title, notes: task.notes }));
|
|
12973
|
+
const scanResult = scanCodebaseForTasks(config2.projectRoot, candidates);
|
|
12974
|
+
if (scanResult) context.codebaseScan = scanResult;
|
|
12975
|
+
} catch (err) {
|
|
12976
|
+
console.error(`[plan] codebase scan failed (non-critical): ${err instanceof Error ? err.message : err}`);
|
|
12977
|
+
}
|
|
12978
|
+
const scanMs = t();
|
|
12979
|
+
console.error(`[plan-perf] codebaseScan: ${scanMs}ms`);
|
|
12980
|
+
t = startTimer();
|
|
12606
12981
|
const userMessage = buildPlanUserMessage(context);
|
|
12607
12982
|
const buildMessageMs = t();
|
|
12608
12983
|
const totalMs = prepareTimer();
|
|
@@ -12779,6 +13154,7 @@ var lastPrepareSkipHandoffs;
|
|
|
12779
13154
|
var planTool = {
|
|
12780
13155
|
name: "plan",
|
|
12781
13156
|
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`.',
|
|
13157
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
12782
13158
|
inputSchema: {
|
|
12783
13159
|
type: "object",
|
|
12784
13160
|
properties: {
|
|
@@ -12906,24 +13282,30 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
12906
13282
|
return errorResponse('llm_response is required for mode "apply". Pass your complete plan output including the <!-- PAPI_STRUCTURED_OUTPUT --> block.');
|
|
12907
13283
|
}
|
|
12908
13284
|
const planMode = args.plan_mode || "full";
|
|
12909
|
-
const
|
|
13285
|
+
const rawCycleNumber = args.cycle_number != null ? Number(args.cycle_number) : NaN;
|
|
12910
13286
|
const strategyReviewWarning = args.strategy_review_warning || "";
|
|
12911
13287
|
const contextHashes = lastPrepareContextHashes;
|
|
12912
13288
|
const inputContext = lastPrepareUserMessage;
|
|
12913
13289
|
const contextBytes = lastPrepareContextBytes;
|
|
12914
13290
|
const expectedCycleNumber = lastPrepareCycleNumber;
|
|
12915
13291
|
const skipHandoffsCached = lastPrepareSkipHandoffs;
|
|
12916
|
-
lastPrepareContextHashes = void 0;
|
|
12917
|
-
lastPrepareUserMessage = void 0;
|
|
12918
|
-
lastPrepareContextBytes = void 0;
|
|
12919
|
-
lastPrepareCycleNumber = void 0;
|
|
12920
|
-
lastPrepareSkipHandoffs = void 0;
|
|
12921
13292
|
const skipHandoffs = args.skip_handoffs === true || skipHandoffsCached === true;
|
|
13293
|
+
const cycleNumber = !isNaN(rawCycleNumber) ? rawCycleNumber : expectedCycleNumber !== void 0 ? expectedCycleNumber : NaN;
|
|
13294
|
+
if (isNaN(cycleNumber)) {
|
|
13295
|
+
return errorResponse(
|
|
13296
|
+
"cycle_number is required for apply mode. Pass the cycle_number from the prepare phase output."
|
|
13297
|
+
);
|
|
13298
|
+
}
|
|
12922
13299
|
if (expectedCycleNumber !== void 0 && cycleNumber !== expectedCycleNumber) {
|
|
12923
13300
|
return errorResponse(
|
|
12924
13301
|
`cycle_number mismatch: prepare phase returned cycle ${expectedCycleNumber} but apply received ${cycleNumber}. Pass cycle_number: ${expectedCycleNumber} to match the prepare output.`
|
|
12925
13302
|
);
|
|
12926
13303
|
}
|
|
13304
|
+
lastPrepareContextHashes = void 0;
|
|
13305
|
+
lastPrepareUserMessage = void 0;
|
|
13306
|
+
lastPrepareContextBytes = void 0;
|
|
13307
|
+
lastPrepareCycleNumber = void 0;
|
|
13308
|
+
lastPrepareSkipHandoffs = void 0;
|
|
12927
13309
|
const result = await applyPlan(adapter2, config2, llmResponse, planMode, cycleNumber, strategyReviewWarning, contextHashes, { contextBytes: contextBytes ?? void 0, skipHandoffs: skipHandoffs || void 0 });
|
|
12928
13310
|
let utilisation;
|
|
12929
13311
|
if (inputContext) {
|
|
@@ -13122,7 +13504,7 @@ function classifyRecommendation(text) {
|
|
|
13122
13504
|
if (lower.includes("new task") || lower.includes("create task") || lower.includes("add task") || lower.includes("spike")) {
|
|
13123
13505
|
return "new_task";
|
|
13124
13506
|
}
|
|
13125
|
-
if (lower.includes("process") || lower.includes("workflow") || lower.includes("methodology") || lower.includes("retrospective") || lower.includes("dogfood")) {
|
|
13507
|
+
if (lower.includes("process") || lower.includes("workflow") || lower.includes("methodology") || lower.includes("retrospective") || lower.includes("dogfood") || lower.includes("refine")) {
|
|
13126
13508
|
return "process_improvement";
|
|
13127
13509
|
}
|
|
13128
13510
|
if (lower.includes("infrastructure") || lower.includes("deploy") || lower.includes("ci/cd") || lower.includes("pipeline") || lower.includes("hosting") || lower.includes("database") || lower.includes("migration")) {
|
|
@@ -13782,6 +14164,12 @@ ${cleanContent}`;
|
|
|
13782
14164
|
} catch {
|
|
13783
14165
|
}
|
|
13784
14166
|
}
|
|
14167
|
+
if (data.northStar && adapter2.upsertNorthStar) {
|
|
14168
|
+
try {
|
|
14169
|
+
await adapter2.upsertNorthStar(data.northStar, cycleNumber);
|
|
14170
|
+
} catch {
|
|
14171
|
+
}
|
|
14172
|
+
}
|
|
13785
14173
|
const compressionThreshold = cycleNumber - 5;
|
|
13786
14174
|
if (compressionThreshold > 0 && data.sessionLogCompressionSummary) {
|
|
13787
14175
|
await adapter2.compressCycleLog(compressionThreshold, data.sessionLogCompressionSummary);
|
|
@@ -13841,7 +14229,7 @@ ${cleanContent}`;
|
|
|
13841
14229
|
try {
|
|
13842
14230
|
const canvas = await adapter2.readDiscoveryCanvas();
|
|
13843
14231
|
const updates = {};
|
|
13844
|
-
|
|
14232
|
+
const populatedSections = [];
|
|
13845
14233
|
if (!canvas.landscapeReferences || canvas.landscapeReferences.length === 0) {
|
|
13846
14234
|
if (data.activeDecisionUpdates?.length) {
|
|
13847
14235
|
const entries = data.activeDecisionUpdates.filter((ad) => ad.body && ad.action !== "delete").slice(0, 3).map((ad) => ({ name: ad.id, notes: ad.body.slice(0, 200) }));
|
|
@@ -14104,10 +14492,17 @@ function formatVelocitySummary(reports, cycleCount) {
|
|
|
14104
14492
|
function formatRecentReportsSummary(reports, count) {
|
|
14105
14493
|
const recent = reports.sort((a, b2) => b2.cycle - a.cycle || b2.date.localeCompare(a.date)).slice(0, count);
|
|
14106
14494
|
if (recent.length === 0) return "No recent build reports.";
|
|
14495
|
+
const trunc = (s, max) => s && s !== "None" ? s.length > max ? s.slice(0, max) + "..." : s : null;
|
|
14107
14496
|
return recent.map((r) => {
|
|
14108
14497
|
const effort = `${r.actualEffort} vs ${r.estimatedEffort}`;
|
|
14109
|
-
const
|
|
14110
|
-
|
|
14498
|
+
const lines = [`- C${r.cycle} ${r.taskName}: ${effort}`];
|
|
14499
|
+
const surprises = trunc(r.surprises, 200);
|
|
14500
|
+
if (surprises) lines.push(` _Surprises:_ ${surprises}`);
|
|
14501
|
+
const issues = trunc(r.discoveredIssues, 200);
|
|
14502
|
+
if (issues) lines.push(` _Issues:_ ${issues}`);
|
|
14503
|
+
const arch = trunc(r.architectureNotes, 200);
|
|
14504
|
+
if (arch) lines.push(` _Architecture:_ ${arch}`);
|
|
14505
|
+
return lines.join("\n");
|
|
14111
14506
|
}).join("\n");
|
|
14112
14507
|
}
|
|
14113
14508
|
function formatPhasesForReview(phases, currentCycle) {
|
|
@@ -14133,7 +14528,7 @@ async function formatHierarchyForReview(adapter2, currentCycle, prefetchedTasks)
|
|
|
14133
14528
|
} catch {
|
|
14134
14529
|
}
|
|
14135
14530
|
if (horizons.length === 0 && phases.length === 0) return void 0;
|
|
14136
|
-
|
|
14531
|
+
const tasksByPhase = /* @__PURE__ */ new Map();
|
|
14137
14532
|
try {
|
|
14138
14533
|
const allTasks = prefetchedTasks ?? await adapter2.queryBoard();
|
|
14139
14534
|
for (const t of allTasks) {
|
|
@@ -14278,6 +14673,12 @@ ${cleanContent}`;
|
|
|
14278
14673
|
const currentPhases = await adapter2.readPhases();
|
|
14279
14674
|
await applyPhaseUpdates(adapter2, currentPhases, data.phaseUpdates);
|
|
14280
14675
|
}
|
|
14676
|
+
if (data.northStar && adapter2.upsertNorthStar) {
|
|
14677
|
+
try {
|
|
14678
|
+
await adapter2.upsertNorthStar(data.northStar, cycleNumber);
|
|
14679
|
+
} catch {
|
|
14680
|
+
}
|
|
14681
|
+
}
|
|
14281
14682
|
} catch (err) {
|
|
14282
14683
|
writeBackFailed = err instanceof Error ? err.message : String(err);
|
|
14283
14684
|
}
|
|
@@ -14400,6 +14801,7 @@ var lastReviewContextBytes;
|
|
|
14400
14801
|
var strategyReviewTool = {
|
|
14401
14802
|
name: "strategy_review",
|
|
14402
14803
|
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 in its own dedicated session \u2014 do not mix with building. 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.',
|
|
14804
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
14403
14805
|
inputSchema: {
|
|
14404
14806
|
type: "object",
|
|
14405
14807
|
properties: {
|
|
@@ -14427,6 +14829,7 @@ var strategyReviewTool = {
|
|
|
14427
14829
|
var strategyChangeTool = {
|
|
14428
14830
|
name: "strategy_change",
|
|
14429
14831
|
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.',
|
|
14832
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
14430
14833
|
inputSchema: {
|
|
14431
14834
|
type: "object",
|
|
14432
14835
|
properties: {
|
|
@@ -14764,6 +15167,7 @@ async function archiveTasks(adapter2, phases, statuses) {
|
|
|
14764
15167
|
var boardViewTool = {
|
|
14765
15168
|
name: "board_view",
|
|
14766
15169
|
description: 'View the Board. By default shows active tasks only (excludes Done/Cancelled), sorted by priority, limited to 50. Use status="all" to see everything. Use mode="summary" for counts only (no task details). Does not call the Anthropic API.',
|
|
15170
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
14767
15171
|
inputSchema: {
|
|
14768
15172
|
type: "object",
|
|
14769
15173
|
properties: {
|
|
@@ -14795,6 +15199,7 @@ var boardViewTool = {
|
|
|
14795
15199
|
var boardDeprioritiseTool = {
|
|
14796
15200
|
name: "board_deprioritise",
|
|
14797
15201
|
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.`,
|
|
15202
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
14798
15203
|
inputSchema: {
|
|
14799
15204
|
type: "object",
|
|
14800
15205
|
properties: {
|
|
@@ -14830,6 +15235,7 @@ var boardDeprioritiseTool = {
|
|
|
14830
15235
|
var boardArchiveTool = {
|
|
14831
15236
|
name: "board_archive",
|
|
14832
15237
|
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.",
|
|
15238
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
14833
15239
|
inputSchema: {
|
|
14834
15240
|
type: "object",
|
|
14835
15241
|
properties: {
|
|
@@ -14848,6 +15254,7 @@ var boardArchiveTool = {
|
|
|
14848
15254
|
var boardEditTool = {
|
|
14849
15255
|
name: "board_edit",
|
|
14850
15256
|
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.",
|
|
15257
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
14851
15258
|
inputSchema: {
|
|
14852
15259
|
type: "object",
|
|
14853
15260
|
properties: {
|
|
@@ -15114,7 +15521,7 @@ async function handleBoardEdit(adapter2, args) {
|
|
|
15114
15521
|
try {
|
|
15115
15522
|
const dogfoodLog = await adapter2.getDogfoodLog?.(50) ?? [];
|
|
15116
15523
|
const linked = dogfoodLog.filter((e) => e.linkedTaskId === taskId || e.linkedTaskId === task.id);
|
|
15117
|
-
const newStatus =
|
|
15524
|
+
const newStatus = "resolved";
|
|
15118
15525
|
await Promise.all(linked.map((e) => adapter2.updateDogfoodEntryStatus(e.id, newStatus)));
|
|
15119
15526
|
} catch {
|
|
15120
15527
|
}
|
|
@@ -15486,6 +15893,7 @@ When the system compresses prior messages, immediately:
|
|
|
15486
15893
|
|
|
15487
15894
|
- **XS/S tasks in the same cycle and module:** Group on shared branch. One PR, one merge.
|
|
15488
15895
|
- **M/L tasks or different modules:** Own branch per task. Isolated PRs.
|
|
15896
|
+
- **Dependent tasks (any size):** When a task's BUILD HANDOFF lists a \`DEPENDS ON\` task from the same cycle, \`build_execute\` automatically reuses the upstream task's branch so commits stack for a single PR. Do not create a separate branch manually.
|
|
15489
15897
|
- **Commit per task within grouped branches** \u2014 traceable git history.
|
|
15490
15898
|
- **Never use \`build_execute\` with \`light=true\` on shared branches.** Light mode commits directly to the current branch without creating a PR. When a shared branch is squash-merged, those commits are collapsed \u2014 any CLAUDE.md or documentation changes are stripped. Use light mode only on isolated single-task branches where no squash-merge will occur.
|
|
15491
15899
|
|
|
@@ -16285,6 +16693,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
|
|
|
16285
16693
|
var setupTool = {
|
|
16286
16694
|
name: "setup",
|
|
16287
16695
|
description: `Initialise a new PAPI project or adopt an existing codebase. Only 3 inputs needed: project name, what it does, and who it's for. Set existing_project: true to adopt an existing codebase \u2014 PAPI scans the project structure and generates a context-aware brief, hierarchy, and initial backlog tasks. First call returns prompts (prepare phase), then call again with mode "apply" and your outputs. After setup, run plan to start your first cycle.`,
|
|
16696
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
16288
16697
|
inputSchema: {
|
|
16289
16698
|
type: "object",
|
|
16290
16699
|
properties: {
|
|
@@ -16554,6 +16963,8 @@ import { randomUUID as randomUUID9 } from "crypto";
|
|
|
16554
16963
|
import { readdirSync as readdirSync3, existsSync as existsSync3, readFileSync } from "fs";
|
|
16555
16964
|
import { join as join5 } from "path";
|
|
16556
16965
|
var buildStartTimes = /* @__PURE__ */ new Map();
|
|
16966
|
+
var taskBranchMap = /* @__PURE__ */ new Map();
|
|
16967
|
+
var SHARED_BRANCH_COMPLEXITIES = /* @__PURE__ */ new Set(["XS", "Small"]);
|
|
16557
16968
|
function capitalizeCompleted(value) {
|
|
16558
16969
|
const map = {
|
|
16559
16970
|
yes: "Yes",
|
|
@@ -16573,7 +16984,7 @@ function pushAndCreatePR(config2, taskId, taskTitle) {
|
|
|
16573
16984
|
if (!isGitAvailable() || !isGitRepo(config2.projectRoot)) {
|
|
16574
16985
|
return lines;
|
|
16575
16986
|
}
|
|
16576
|
-
const featureBranch = taskBranchName(taskId);
|
|
16987
|
+
const featureBranch = taskBranchMap.get(taskId) ?? taskBranchName(taskId);
|
|
16577
16988
|
const currentBranch = getCurrentBranch(config2.projectRoot);
|
|
16578
16989
|
if (currentBranch !== featureBranch) {
|
|
16579
16990
|
return lines;
|
|
@@ -16705,10 +17116,38 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
|
|
|
16705
17116
|
if (options.light) {
|
|
16706
17117
|
branchLines.push("Light mode: skipping branch creation \u2014 working on current branch.");
|
|
16707
17118
|
} else if (config2.autoCommit && isGitAvailable() && isGitRepo(config2.projectRoot)) {
|
|
16708
|
-
const
|
|
17119
|
+
const cycleHealth = await adapter2.getCycleHealth().catch(() => null);
|
|
17120
|
+
const cycleNumber = cycleHealth?.totalCycles ?? 0;
|
|
17121
|
+
let depBranchReuse = null;
|
|
17122
|
+
if (task.dependsOn) {
|
|
17123
|
+
const depIds = task.dependsOn.split(",").map((d) => d.trim()).filter(Boolean);
|
|
17124
|
+
for (const depId of depIds) {
|
|
17125
|
+
const mappedBranch = taskBranchMap.get(depId);
|
|
17126
|
+
if (mappedBranch && branchExists(config2.projectRoot, mappedBranch)) {
|
|
17127
|
+
depBranchReuse = { branch: mappedBranch, upstreamId: depId };
|
|
17128
|
+
break;
|
|
17129
|
+
}
|
|
17130
|
+
const fallbackBranch = taskBranchName(depId);
|
|
17131
|
+
if (branchExists(config2.projectRoot, fallbackBranch)) {
|
|
17132
|
+
depBranchReuse = { branch: fallbackBranch, upstreamId: depId };
|
|
17133
|
+
break;
|
|
17134
|
+
}
|
|
17135
|
+
}
|
|
17136
|
+
}
|
|
17137
|
+
const useSharedBranch = !depBranchReuse && SHARED_BRANCH_COMPLEXITIES.has(task.complexity) && !!task.module && cycleNumber > 0;
|
|
17138
|
+
const featureBranch = depBranchReuse ? depBranchReuse.branch : useSharedBranch ? cycleBranchName(cycleNumber, task.module) : taskBranchName(taskId);
|
|
17139
|
+
if (depBranchReuse) {
|
|
17140
|
+
branchLines.push(
|
|
17141
|
+
`Reusing branch '${depBranchReuse.branch}' from dependency ${depBranchReuse.upstreamId} \u2014 commits will stack for a single PR.`
|
|
17142
|
+
);
|
|
17143
|
+
}
|
|
17144
|
+
taskBranchMap.set(taskId, featureBranch);
|
|
16709
17145
|
const currentBranch = getCurrentBranch(config2.projectRoot);
|
|
16710
17146
|
if (currentBranch === featureBranch) {
|
|
16711
17147
|
branchLines.push(`Already on branch '${featureBranch}'.`);
|
|
17148
|
+
if (useSharedBranch) {
|
|
17149
|
+
branchLines.push(`Reusing shared cycle branch for ${task.complexity} ${task.module} task.`);
|
|
17150
|
+
}
|
|
16712
17151
|
} else {
|
|
16713
17152
|
if (hasUncommittedChanges(config2.projectRoot, AUTO_WRITTEN_PATHS)) {
|
|
16714
17153
|
throw new Error("Working directory has uncommitted changes. Please commit or stash them before running `build_execute`.");
|
|
@@ -16717,13 +17156,14 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
|
|
|
16717
17156
|
if (baseBranch !== config2.baseBranch) {
|
|
16718
17157
|
branchLines.push(`Base branch '${config2.baseBranch}' not found \u2014 using '${baseBranch}'.`);
|
|
16719
17158
|
}
|
|
16720
|
-
|
|
17159
|
+
const featureBranchExists = branchExists(config2.projectRoot, featureBranch);
|
|
17160
|
+
if (currentBranch !== baseBranch && !featureBranchExists) {
|
|
16721
17161
|
const checkout = checkoutBranch(config2.projectRoot, baseBranch);
|
|
16722
17162
|
if (!checkout.success) {
|
|
16723
17163
|
branchLines.push(`Warning: ${checkout.message} Proceeding on current branch '${currentBranch}'.`);
|
|
16724
17164
|
}
|
|
16725
17165
|
}
|
|
16726
|
-
if (hasRemote(config2.projectRoot)) {
|
|
17166
|
+
if (hasRemote(config2.projectRoot) && !featureBranchExists) {
|
|
16727
17167
|
const pull = gitPull(config2.projectRoot);
|
|
16728
17168
|
branchLines.push(pull.success ? pull.message : `Warning: ${pull.message}`);
|
|
16729
17169
|
}
|
|
@@ -16733,10 +17173,10 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
|
|
|
16733
17173
|
`Warning: ${unmerged.length} unmerged feature branch${unmerged.length === 1 ? "" : "es"}: ${unmerged.join(", ")}. New branch may diverge if these contain changes needed here.`
|
|
16734
17174
|
);
|
|
16735
17175
|
}
|
|
16736
|
-
if (
|
|
17176
|
+
if (featureBranchExists) {
|
|
16737
17177
|
const checkout = checkoutBranch(config2.projectRoot, featureBranch);
|
|
16738
17178
|
branchLines.push(
|
|
16739
|
-
checkout.success ? `Checked out existing branch '${featureBranch}'.` : `Warning: ${checkout.message}`
|
|
17179
|
+
checkout.success ? useSharedBranch ? `Checked out shared cycle branch '${featureBranch}' \u2014 reusing for ${task.complexity} ${task.module} task.` : `Checked out existing branch '${featureBranch}'.` : `Warning: ${checkout.message}`
|
|
16740
17180
|
);
|
|
16741
17181
|
if (checkout.success) {
|
|
16742
17182
|
branchLines.push(
|
|
@@ -16746,7 +17186,7 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
|
|
|
16746
17186
|
} else {
|
|
16747
17187
|
const create = createAndCheckoutBranch(config2.projectRoot, featureBranch);
|
|
16748
17188
|
branchLines.push(
|
|
16749
|
-
create.success ? `Created branch '${featureBranch}'.` : `Warning: ${create.message}`
|
|
17189
|
+
create.success ? useSharedBranch ? `Created shared cycle branch '${featureBranch}' for ${task.module} XS/S tasks.` : `Created branch '${featureBranch}'.` : `Warning: ${create.message}`
|
|
16750
17190
|
);
|
|
16751
17191
|
if (create.success) {
|
|
16752
17192
|
branchLines.push(
|
|
@@ -16772,7 +17212,7 @@ function extractDocMeta(absolutePath, relativePath, cycleNumber) {
|
|
|
16772
17212
|
let title = relativePath.split("/").pop()?.replace(".md", "") ?? relativePath;
|
|
16773
17213
|
let type = "reference";
|
|
16774
17214
|
let cycle = cycleNumber;
|
|
16775
|
-
|
|
17215
|
+
const summary = "Auto-registered \u2014 no summary available. Update via doc_register.";
|
|
16776
17216
|
if (relativePath.startsWith("docs/research/")) type = "research";
|
|
16777
17217
|
else if (relativePath.startsWith("docs/architecture/")) type = "architecture";
|
|
16778
17218
|
else if (relativePath.startsWith("docs/audits/")) type = "audit";
|
|
@@ -16830,11 +17270,27 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
16830
17270
|
completedAt: now.toISOString()
|
|
16831
17271
|
};
|
|
16832
17272
|
buildStartTimes.delete(taskId);
|
|
17273
|
+
taskBranchMap.delete(taskId);
|
|
16833
17274
|
if (input.relatedDecisions) {
|
|
16834
17275
|
const adIds = input.relatedDecisions.split(",").map((s) => s.trim()).filter(Boolean);
|
|
16835
17276
|
if (adIds.length > 0) report.relatedDecisions = adIds;
|
|
16836
17277
|
}
|
|
17278
|
+
if (report.startedAt && report.completedAt && typeof adapter2.getToolCallCount === "function") {
|
|
17279
|
+
try {
|
|
17280
|
+
const count = await adapter2.getToolCallCount(report.startedAt, report.completedAt);
|
|
17281
|
+
if (count > 0) report.toolCallCount = count;
|
|
17282
|
+
} catch {
|
|
17283
|
+
}
|
|
17284
|
+
}
|
|
16837
17285
|
await adapter2.appendBuildReport(report);
|
|
17286
|
+
let reportWriteVerified;
|
|
17287
|
+
if (typeof adapter2.getBuildReportCountForTask === "function") {
|
|
17288
|
+
try {
|
|
17289
|
+
const postWriteCount = await adapter2.getBuildReportCountForTask(taskId);
|
|
17290
|
+
reportWriteVerified = postWriteCount >= iterationCount;
|
|
17291
|
+
} catch {
|
|
17292
|
+
}
|
|
17293
|
+
}
|
|
16838
17294
|
if (adapter2.appendCycleLearnings) {
|
|
16839
17295
|
const learnings = [];
|
|
16840
17296
|
const taskModule = task.module ?? "";
|
|
@@ -16881,6 +17337,39 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
16881
17337
|
}
|
|
16882
17338
|
}
|
|
16883
17339
|
}
|
|
17340
|
+
let autoTriagedCount = 0;
|
|
17341
|
+
if (input.discoveredIssues && input.discoveredIssues !== "None" && typeof adapter2.createTask === "function") {
|
|
17342
|
+
const issueLines = input.discoveredIssues.split(/\n|;/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
17343
|
+
for (const line of issueLines) {
|
|
17344
|
+
const sevMatch = line.match(/^(P[0-3])[\s:]+/i);
|
|
17345
|
+
if (!sevMatch) continue;
|
|
17346
|
+
const severityLabel = sevMatch[1].toUpperCase();
|
|
17347
|
+
const priority = severityLabel === "P0" || severityLabel === "P1" ? "P1 High" : severityLabel === "P2" ? "P2 Medium" : "P3 Low";
|
|
17348
|
+
const titleRaw = line.replace(/^P[0-3][\s:]+/i, "").trim();
|
|
17349
|
+
const title = titleRaw.length > 120 ? titleRaw.slice(0, 120) : titleRaw;
|
|
17350
|
+
if (!title) continue;
|
|
17351
|
+
try {
|
|
17352
|
+
await adapter2.createTask({
|
|
17353
|
+
uuid: "",
|
|
17354
|
+
displayId: "",
|
|
17355
|
+
title: `[Auto-triaged] ${title}`,
|
|
17356
|
+
status: "Backlog",
|
|
17357
|
+
priority,
|
|
17358
|
+
complexity: "Small",
|
|
17359
|
+
module: task.module ?? "",
|
|
17360
|
+
phase: task.phase ?? "",
|
|
17361
|
+
owner: "papi",
|
|
17362
|
+
reviewed: false,
|
|
17363
|
+
taskType: "discovery",
|
|
17364
|
+
source: "build_complete",
|
|
17365
|
+
notes: `Origin: ${task.displayId} (${task.title}), cycle ${cycleNumber}. Original issue: ${line}`,
|
|
17366
|
+
createdCycle: cycleNumber
|
|
17367
|
+
});
|
|
17368
|
+
autoTriagedCount++;
|
|
17369
|
+
} catch {
|
|
17370
|
+
}
|
|
17371
|
+
}
|
|
17372
|
+
}
|
|
16884
17373
|
if (adapter2.updateCycleLearningActionRef && task.notes) {
|
|
16885
17374
|
const learningRefs = task.notes.match(/learning:([a-f0-9-]+)/gi);
|
|
16886
17375
|
if (learningRefs) {
|
|
@@ -16923,6 +17412,52 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
16923
17412
|
await adapter2.updateTaskStatus(taskId, "In Review");
|
|
16924
17413
|
}
|
|
16925
17414
|
}
|
|
17415
|
+
let dogfoodResolvedCount = 0;
|
|
17416
|
+
if (input.completed === "yes" && adapter2.getDogfoodLog && adapter2.updateDogfoodEntryStatus) {
|
|
17417
|
+
try {
|
|
17418
|
+
const dogfoodLog = await adapter2.getDogfoodLog(50);
|
|
17419
|
+
const linked = dogfoodLog.filter(
|
|
17420
|
+
(e) => (e.linkedTaskId === taskId || e.linkedTaskId === task.id) && e.status !== "resolved"
|
|
17421
|
+
);
|
|
17422
|
+
if (linked.length > 0) {
|
|
17423
|
+
await Promise.all(linked.map((e) => adapter2.updateDogfoodEntryStatus(e.id, "resolved")));
|
|
17424
|
+
dogfoodResolvedCount = linked.length;
|
|
17425
|
+
}
|
|
17426
|
+
} catch {
|
|
17427
|
+
}
|
|
17428
|
+
}
|
|
17429
|
+
let learningsLinkedCount = 0;
|
|
17430
|
+
if (input.completed === "yes" && adapter2.getCycleLearnings && adapter2.updateCycleLearningActionRef) {
|
|
17431
|
+
try {
|
|
17432
|
+
const recentLearnings = await adapter2.getCycleLearnings({ limit: 30 });
|
|
17433
|
+
const unactioned = recentLearnings.filter(
|
|
17434
|
+
(l) => !l.actionRef && l.cycleNumber >= cycleNumber - 5
|
|
17435
|
+
);
|
|
17436
|
+
if (unactioned.length > 0) {
|
|
17437
|
+
const taskText = `${task.title} ${task.notes ?? ""}`.toLowerCase();
|
|
17438
|
+
const taskWords = new Set(
|
|
17439
|
+
taskText.match(/\b[a-z]{4,}\b/g) ?? []
|
|
17440
|
+
);
|
|
17441
|
+
const taskModule = (task.module ?? "").toLowerCase();
|
|
17442
|
+
for (const learning of unactioned) {
|
|
17443
|
+
const learningModule = (learning.tags[0] ?? "").toLowerCase();
|
|
17444
|
+
if (!taskModule || !learningModule || taskModule !== learningModule) continue;
|
|
17445
|
+
const learningText = `${learning.summary} ${learning.detail ?? ""}`.toLowerCase();
|
|
17446
|
+
const learningWords = learningText.match(/\b[a-z]{4,}\b/g) ?? [];
|
|
17447
|
+
const hasKeywordOverlap = learningWords.some((w) => taskWords.has(w));
|
|
17448
|
+
if (!hasKeywordOverlap) continue;
|
|
17449
|
+
if (learning.id) {
|
|
17450
|
+
try {
|
|
17451
|
+
await adapter2.updateCycleLearningActionRef(learning.id, task.id);
|
|
17452
|
+
learningsLinkedCount++;
|
|
17453
|
+
} catch {
|
|
17454
|
+
}
|
|
17455
|
+
}
|
|
17456
|
+
}
|
|
17457
|
+
}
|
|
17458
|
+
} catch {
|
|
17459
|
+
}
|
|
17460
|
+
}
|
|
16926
17461
|
const statusNote = input.completed === "yes" ? options.light ? `Task "${task.title}" (${taskId}) marked Done (light mode \u2014 no review needed).` : `Task "${task.title}" (${taskId}) marked In Review \u2014 ready for your sign-off via \`review_submit\`.` : `Task "${task.title}" (${taskId}) status unchanged (completed: ${input.completed}).`;
|
|
16927
17462
|
let commitLine;
|
|
16928
17463
|
if (config2.autoCommit) {
|
|
@@ -17023,7 +17558,11 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
17023
17558
|
completed: input.completed,
|
|
17024
17559
|
scopeAccuracy: input.scopeAccuracy,
|
|
17025
17560
|
phaseChanges,
|
|
17026
|
-
docWarning
|
|
17561
|
+
docWarning,
|
|
17562
|
+
dogfoodResolvedCount: dogfoodResolvedCount > 0 ? dogfoodResolvedCount : void 0,
|
|
17563
|
+
learningsLinkedCount: learningsLinkedCount > 0 ? learningsLinkedCount : void 0,
|
|
17564
|
+
autoTriagedCount: autoTriagedCount > 0 ? autoTriagedCount : void 0,
|
|
17565
|
+
reportWriteVerified
|
|
17027
17566
|
};
|
|
17028
17567
|
}
|
|
17029
17568
|
async function cancelBuild(adapter2, taskId, reason) {
|
|
@@ -17080,8 +17619,6 @@ For M/L tasks: use the full toolchain \u2014 Playground (design preview) \u2192
|
|
|
17080
17619
|
- Check adapter-pg implementation, not adapter-md. adapter-md is legacy.
|
|
17081
17620
|
- Verify the full write\u2192DB\u2192read\u2192consumer path for any data changes.
|
|
17082
17621
|
- Run migrations on dev before prod. Test with \`execute_sql\` via Supabase MCP.
|
|
17083
|
-
- When adding adapter interface methods, implement in BOTH adapter-md and adapter-pg.
|
|
17084
|
-
- Build order matters: adapter-md \u2192 adapter-pg \u2192 server.
|
|
17085
17622
|
- .papi/ files may be stale \u2014 DB via MCP tools is the source of truth.`,
|
|
17086
17623
|
Auth: `**MODULE INSTRUCTIONS \u2014 Auth**
|
|
17087
17624
|
- NEVER expose the Supabase service role key in client-side code or API responses.
|
|
@@ -17116,6 +17653,7 @@ ${instructions}`;
|
|
|
17116
17653
|
var buildListTool = {
|
|
17117
17654
|
name: "build_list",
|
|
17118
17655
|
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.",
|
|
17656
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
17119
17657
|
inputSchema: {
|
|
17120
17658
|
type: "object",
|
|
17121
17659
|
properties: {},
|
|
@@ -17125,6 +17663,7 @@ var buildListTool = {
|
|
|
17125
17663
|
var buildDescribeTool = {
|
|
17126
17664
|
name: "build_describe",
|
|
17127
17665
|
description: "Show the full BUILD HANDOFF for a specific task, including scope, acceptance criteria, and implementation guidance. Does not call the Anthropic API.",
|
|
17666
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
17128
17667
|
inputSchema: {
|
|
17129
17668
|
type: "object",
|
|
17130
17669
|
properties: {
|
|
@@ -17139,6 +17678,7 @@ var buildDescribeTool = {
|
|
|
17139
17678
|
var buildExecuteTool = {
|
|
17140
17679
|
name: "build_execute",
|
|
17141
17680
|
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.",
|
|
17681
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
17142
17682
|
inputSchema: {
|
|
17143
17683
|
type: "object",
|
|
17144
17684
|
properties: {
|
|
@@ -17171,7 +17711,7 @@ var buildExecuteTool = {
|
|
|
17171
17711
|
},
|
|
17172
17712
|
discovered_issues: {
|
|
17173
17713
|
type: "string",
|
|
17174
|
-
description: `Problems found DURING this build that are OUTSIDE this task's scope. Include severity (P0-P3). Good: "P2: Auth middleware doesn't validate token expiry \u2014 affects all protected routes." Bad: "Had to install a dependency." Only real bugs or gaps that need their own task. Use "None" if none. Required for complete.`
|
|
17714
|
+
description: `Problems found DURING this build that are OUTSIDE this task's scope. Include severity (P0-P3). Good: "P2: Auth middleware doesn't validate token expiry \u2014 affects all protected routes." Bad: "Had to install a dependency." Only real bugs or gaps that need their own task. Use "None" if none. Required for complete. TIP: When submitting a follow-up idea for a discovered issue, include "learning:<uuid>" in the idea notes to link it to this cycle learning entry \u2014 use the UUID returned in the build completion output.`
|
|
17175
17715
|
},
|
|
17176
17716
|
architecture_notes: {
|
|
17177
17717
|
type: "string",
|
|
@@ -17223,6 +17763,7 @@ var buildExecuteTool = {
|
|
|
17223
17763
|
var buildCancelTool = {
|
|
17224
17764
|
name: "build_cancel",
|
|
17225
17765
|
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.",
|
|
17766
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
17226
17767
|
inputSchema: {
|
|
17227
17768
|
type: "object",
|
|
17228
17769
|
properties: {
|
|
@@ -17390,9 +17931,28 @@ If >80% of the scope is already implemented, call \`build_execute\` with complet
|
|
|
17390
17931
|
adSection = formatRelevantADs(relevant);
|
|
17391
17932
|
} catch {
|
|
17392
17933
|
}
|
|
17934
|
+
let dogfoodSection = "";
|
|
17935
|
+
try {
|
|
17936
|
+
if (adapter2.getDogfoodLog) {
|
|
17937
|
+
const dogfoodLog = await adapter2.getDogfoodLog(50);
|
|
17938
|
+
const linked = dogfoodLog.filter(
|
|
17939
|
+
(e) => e.linkedTaskId === result.task.id || e.linkedTaskId === result.task.displayId
|
|
17940
|
+
);
|
|
17941
|
+
if (linked.length > 0) {
|
|
17942
|
+
const entries = linked.map((e) => `- [${e.category}] ${e.content}`).join("\n");
|
|
17943
|
+
dogfoodSection = `
|
|
17944
|
+
|
|
17945
|
+
---
|
|
17946
|
+
|
|
17947
|
+
**DOGFOOD CONTEXT** \u2014 This task was linked to ${linked.length} observation(s):
|
|
17948
|
+
${entries}`;
|
|
17949
|
+
}
|
|
17950
|
+
}
|
|
17951
|
+
} catch {
|
|
17952
|
+
}
|
|
17393
17953
|
const moduleInstructions = getModuleInstructions(result.task.module);
|
|
17394
17954
|
const moduleContext = await getModuleContext(adapter2, result.task);
|
|
17395
|
-
return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + adSection + moduleInstructions + moduleContext + verificationNote + chainInstruction + phaseNote);
|
|
17955
|
+
return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + adSection + moduleInstructions + moduleContext + dogfoodSection + verificationNote + chainInstruction + phaseNote);
|
|
17396
17956
|
} catch (err) {
|
|
17397
17957
|
if (isNoHandoffError(err)) {
|
|
17398
17958
|
const lines = [
|
|
@@ -17499,6 +18059,18 @@ function formatCompleteResult(result) {
|
|
|
17499
18059
|
lines.push(`Phase auto-updated: ${c.phaseId} ${c.oldStatus} \u2192 ${c.newStatus}`);
|
|
17500
18060
|
}
|
|
17501
18061
|
}
|
|
18062
|
+
if (result.dogfoodResolvedCount) {
|
|
18063
|
+
lines.push("", `Resolved ${result.dogfoodResolvedCount} dogfood observation(s) linked to this task.`);
|
|
18064
|
+
}
|
|
18065
|
+
if (result.learningsLinkedCount) {
|
|
18066
|
+
lines.push("", `Linked ${result.learningsLinkedCount} unactioned learning(s) to this task.`);
|
|
18067
|
+
}
|
|
18068
|
+
if (result.autoTriagedCount) {
|
|
18069
|
+
lines.push("", `\u{1F516} Auto-triaged ${result.autoTriagedCount} discovered issue(s) to Backlog.`);
|
|
18070
|
+
}
|
|
18071
|
+
if (result.reportWriteVerified === false) {
|
|
18072
|
+
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.");
|
|
18073
|
+
}
|
|
17502
18074
|
if (result.docWarning) {
|
|
17503
18075
|
lines.push("", `\u{1F4C4} ${result.docWarning}`);
|
|
17504
18076
|
}
|
|
@@ -17638,7 +18210,7 @@ function resolveCurrentPhase(phases) {
|
|
|
17638
18210
|
const sorted = [...phases].sort((a, b2) => a.order - b2.order);
|
|
17639
18211
|
return sorted[0].label;
|
|
17640
18212
|
}
|
|
17641
|
-
var
|
|
18213
|
+
var STOP_WORDS2 = /* @__PURE__ */ new Set([
|
|
17642
18214
|
"a",
|
|
17643
18215
|
"an",
|
|
17644
18216
|
"the",
|
|
@@ -17732,7 +18304,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
|
17732
18304
|
]);
|
|
17733
18305
|
function extractKeywords(text) {
|
|
17734
18306
|
return new Set(
|
|
17735
|
-
text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !
|
|
18307
|
+
text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !STOP_WORDS2.has(w))
|
|
17736
18308
|
);
|
|
17737
18309
|
}
|
|
17738
18310
|
async function findSimilarTasks(adapter2, ideaTitle) {
|
|
@@ -17799,14 +18371,15 @@ ${lines.join("\n")}
|
|
|
17799
18371
|
]);
|
|
17800
18372
|
warnIfEmpty("getCycleHealth (idea)", health);
|
|
17801
18373
|
const phase = input.phase || resolveCurrentPhase(phases);
|
|
17802
|
-
const
|
|
18374
|
+
const VALID_PRIORITIES3 = /* @__PURE__ */ new Set(["P0 Critical", "P1 High", "P2 Medium", "P3 Low"]);
|
|
17803
18375
|
const VALID_COMPLEXITIES2 = /* @__PURE__ */ new Set(["XS", "Small", "Medium", "Large", "XL"]);
|
|
17804
|
-
const priority = input.priority &&
|
|
18376
|
+
const priority = input.priority && VALID_PRIORITIES3.has(input.priority) ? input.priority : "P2 Medium";
|
|
17805
18377
|
const complexity = input.complexity && VALID_COMPLEXITIES2.has(input.complexity) ? input.complexity : "Small";
|
|
17806
|
-
const
|
|
18378
|
+
const VALID_TYPES2 = /* @__PURE__ */ new Set(["task", "bug", "research", "idea", "spike", "discovery"]);
|
|
17807
18379
|
let taskTitle = input.text;
|
|
17808
18380
|
let taskType = "idea";
|
|
17809
|
-
|
|
18381
|
+
let typeInferred = false;
|
|
18382
|
+
if (input.type && VALID_TYPES2.has(input.type)) {
|
|
17810
18383
|
taskType = input.type;
|
|
17811
18384
|
} else {
|
|
17812
18385
|
const PREFIX_MAP = {
|
|
@@ -17820,6 +18393,20 @@ ${lines.join("\n")}
|
|
|
17820
18393
|
taskType = PREFIX_MAP[key];
|
|
17821
18394
|
taskTitle = input.text.slice(prefixMatch[0].length);
|
|
17822
18395
|
}
|
|
18396
|
+
} else {
|
|
18397
|
+
const searchText = `${input.text} ${input.notes ?? ""}`.toLowerCase();
|
|
18398
|
+
if (/\b(bug|fix|broken|crash|error)\b/.test(searchText)) {
|
|
18399
|
+
taskType = "bug";
|
|
18400
|
+
} else if (/\b(research|investigate|explore|spike)\b/.test(searchText)) {
|
|
18401
|
+
taskType = "research";
|
|
18402
|
+
} else if (/\b(performance|optimize|speed|latency)\b/.test(searchText)) {
|
|
18403
|
+
taskType = "task";
|
|
18404
|
+
} else if (/\b(verify|confirm)\b/.test(searchText)) {
|
|
18405
|
+
taskType = "spike";
|
|
18406
|
+
} else {
|
|
18407
|
+
taskType = "task";
|
|
18408
|
+
}
|
|
18409
|
+
typeInferred = true;
|
|
17823
18410
|
}
|
|
17824
18411
|
}
|
|
17825
18412
|
const task = await adapter2.createTask({
|
|
@@ -17839,7 +18426,8 @@ ${lines.join("\n")}
|
|
|
17839
18426
|
taskType,
|
|
17840
18427
|
maturity: "raw",
|
|
17841
18428
|
docRef: input.docRef,
|
|
17842
|
-
source: "llm"
|
|
18429
|
+
source: "llm",
|
|
18430
|
+
opportunity: input.opportunity
|
|
17843
18431
|
});
|
|
17844
18432
|
if (input.notes && adapter2.updateCycleLearningActionRef) {
|
|
17845
18433
|
const learningRefs = input.notes.match(/learning:([a-f0-9-]+)/gi);
|
|
@@ -17865,7 +18453,27 @@ ${lines.join("\n")}
|
|
|
17865
18453
|
}
|
|
17866
18454
|
}
|
|
17867
18455
|
}
|
|
17868
|
-
|
|
18456
|
+
if (adapter2.getDogfoodLog && adapter2.updateDogfoodEntryStatus) {
|
|
18457
|
+
try {
|
|
18458
|
+
const dogfoodLog = await adapter2.getDogfoodLog(50);
|
|
18459
|
+
const unlinked = dogfoodLog.filter((e) => e.status === "observed" && !e.linkedTaskId);
|
|
18460
|
+
if (unlinked.length > 0) {
|
|
18461
|
+
const taskText = `${task.title} ${input.notes ?? ""}`.toLowerCase();
|
|
18462
|
+
const taskKeywords = taskText.match(/\b[a-z]{4,}\b/g) ?? [];
|
|
18463
|
+
const taskKeywordSet = new Set(taskKeywords);
|
|
18464
|
+
for (const entry of unlinked) {
|
|
18465
|
+
const entryKeywords = entry.content.toLowerCase().match(/\b[a-z]{4,}\b/g) ?? [];
|
|
18466
|
+
const overlap = entryKeywords.filter((w) => taskKeywordSet.has(w));
|
|
18467
|
+
if (overlap.length >= 2) {
|
|
18468
|
+
await adapter2.updateDogfoodEntryStatus(entry.id, "backlog-created", task.id);
|
|
18469
|
+
}
|
|
18470
|
+
}
|
|
18471
|
+
}
|
|
18472
|
+
} catch {
|
|
18473
|
+
}
|
|
18474
|
+
}
|
|
18475
|
+
const typeNote = typeInferred ? ` [type: ${taskType} \u2014 inferred from text]` : "";
|
|
18476
|
+
return { routing: "task", task, message: `${task.id}: "${task.title}" \u2014 added to backlog${typeNote}` };
|
|
17869
18477
|
}
|
|
17870
18478
|
var CANVAS_SECTION_LABELS = {
|
|
17871
18479
|
landscape: "Landscape References",
|
|
@@ -17905,6 +18513,7 @@ async function routeToDiscovery(adapter2, section, input) {
|
|
|
17905
18513
|
// src/tools/idea.ts
|
|
17906
18514
|
var ideaTool = {
|
|
17907
18515
|
name: "idea",
|
|
18516
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
17908
18517
|
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.",
|
|
17909
18518
|
inputSchema: {
|
|
17910
18519
|
type: "object",
|
|
@@ -17915,7 +18524,7 @@ var ideaTool = {
|
|
|
17915
18524
|
},
|
|
17916
18525
|
notes: {
|
|
17917
18526
|
type: "string",
|
|
17918
|
-
description: 'Additional context, constraints, or reasoning. MANDATORY: If this idea comes from a research or planning session, include a "Reference: <path>" line pointing to the source doc. Tasks submitted without references get misinterpreted by the planner \u2014 this is the #1 cause of wasted build slots (C146: task-807 was scoped as landing page copy when it was actually a dashboard UX task, because the source research doc was missing). Use doc_search to find relevant docs before submitting.'
|
|
18527
|
+
description: 'Additional context, constraints, or reasoning. MANDATORY: If this idea comes from a research or planning session, include a "Reference: <path>" line pointing to the source doc. Tasks submitted without references get misinterpreted by the planner \u2014 this is the #1 cause of wasted build slots (C146: task-807 was scoped as landing page copy when it was actually a dashboard UX task, because the source research doc was missing). Use doc_search to find relevant docs before submitting. TIP: If this idea addresses a known cycle learning, include "learning:<uuid>" in the notes (e.g. "learning:abc12345-..."). This links the idea to the learning entry and marks it actioned in the pipeline. Example: "Addresses recurring friction. learning:3f9a1c2e-..."'
|
|
17919
18528
|
},
|
|
17920
18529
|
module: {
|
|
17921
18530
|
type: "string",
|
|
@@ -17949,12 +18558,16 @@ var ideaTool = {
|
|
|
17949
18558
|
},
|
|
17950
18559
|
type: {
|
|
17951
18560
|
type: "string",
|
|
17952
|
-
enum: ["task", "bug", "research", "spike"],
|
|
17953
|
-
description: 'Task type. Defaults to "task". Use "bug" for defects, "research" for investigation tasks, "spike" for time-boxed experiments. The planner uses this to generate type-specific BUILD HANDOFFs.'
|
|
18561
|
+
enum: ["task", "bug", "research", "spike", "discovery"],
|
|
18562
|
+
description: 'Task type. Defaults to "task". Use "bug" for defects, "research" for investigation tasks, "spike" for time-boxed experiments, "discovery" for issues found during a build that need their own task. The planner uses this to generate type-specific BUILD HANDOFFs.'
|
|
17954
18563
|
},
|
|
17955
18564
|
doc_ref: {
|
|
17956
18565
|
type: "string",
|
|
17957
18566
|
description: 'Path to a reference document (e.g. "docs/research/foo.md"). Stored as a structured field \u2014 replaces the fragile "Reference:" line in notes.'
|
|
18567
|
+
},
|
|
18568
|
+
opportunity: {
|
|
18569
|
+
type: "string",
|
|
18570
|
+
description: "What user problem does this solve? Auto-fill from problem context in notes when submitting ideas that describe a user pain point. The planner uses this to cluster backlog tasks by opportunity."
|
|
17958
18571
|
}
|
|
17959
18572
|
},
|
|
17960
18573
|
required: ["text"]
|
|
@@ -17983,7 +18596,8 @@ async function handleIdea(adapter2, config2, args) {
|
|
|
17983
18596
|
discovery: args.discovery === true,
|
|
17984
18597
|
force: args.force === true,
|
|
17985
18598
|
docRef: args.doc_ref?.trim(),
|
|
17986
|
-
type: args.type
|
|
18599
|
+
type: args.type,
|
|
18600
|
+
opportunity: args.opportunity?.trim()
|
|
17987
18601
|
};
|
|
17988
18602
|
const useGit = isGitAvailable() && isGitRepo(config2.projectRoot);
|
|
17989
18603
|
const currentBranch = useGit ? getCurrentBranch(config2.projectRoot) : null;
|
|
@@ -18090,6 +18704,7 @@ function collectDiagnostics(config2) {
|
|
|
18090
18704
|
var bugTool = {
|
|
18091
18705
|
name: "bug",
|
|
18092
18706
|
description: "Report a bug. Two modes: (1) Default \u2014 creates a Backlog task with severity-based priority for the project board. (2) With report=true \u2014 submits a diagnostic bug report with system info for cross-project visibility (external user issue reporting). Does not call the Anthropic API.",
|
|
18707
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
18093
18708
|
inputSchema: {
|
|
18094
18709
|
type: "object",
|
|
18095
18710
|
properties: {
|
|
@@ -18233,16 +18848,16 @@ async function recordAdHoc(adapter2, input) {
|
|
|
18233
18848
|
displayId: "",
|
|
18234
18849
|
title: input.title,
|
|
18235
18850
|
status: "Done",
|
|
18236
|
-
priority: "
|
|
18851
|
+
priority: input.priority || "P2 Medium",
|
|
18237
18852
|
complexity: input.effort === "XS" || input.effort === "S" ? "Small" : "Medium",
|
|
18238
18853
|
module: input.module || "Core",
|
|
18239
18854
|
epic: input.epic || "Platform",
|
|
18240
18855
|
phase,
|
|
18241
|
-
owner: "Cathal",
|
|
18856
|
+
owner: input.owner || "Cathal",
|
|
18242
18857
|
reviewed: true,
|
|
18243
18858
|
createdCycle: cycle,
|
|
18244
18859
|
notes: input.notes ? `[ad-hoc] ${input.notes}` : "[ad-hoc]",
|
|
18245
|
-
taskType: "task",
|
|
18860
|
+
taskType: input.taskType || "task",
|
|
18246
18861
|
source: "owner"
|
|
18247
18862
|
});
|
|
18248
18863
|
}
|
|
@@ -18267,9 +18882,20 @@ async function recordAdHoc(adapter2, input) {
|
|
|
18267
18882
|
|
|
18268
18883
|
// src/tools/ad-hoc.ts
|
|
18269
18884
|
var VALID_EFFORTS = ["XS", "S", "M", "L", "XL"];
|
|
18885
|
+
var VALID_PRIORITIES = ["P0 Critical", "P1 High", "P2 Medium", "P3 Low"];
|
|
18886
|
+
var VALID_TYPES = ["task", "bug", "research", "discovery", "spike", "idea"];
|
|
18887
|
+
function inferTaskType(description) {
|
|
18888
|
+
const lower = description.toLowerCase();
|
|
18889
|
+
if (/\bfix\b|bug|error|crash|broken|regression|defect/.test(lower)) return "bug";
|
|
18890
|
+
if (/research|investigate|explore|analysis|audit|review|assess/.test(lower)) return "research";
|
|
18891
|
+
if (/discover|discovery|found|uncovered/.test(lower)) return "discovery";
|
|
18892
|
+
if (/spike|poc|proof.of.concept|prototype|experiment/.test(lower)) return "spike";
|
|
18893
|
+
return "task";
|
|
18894
|
+
}
|
|
18270
18895
|
var adHocTool = {
|
|
18271
18896
|
name: "ad_hoc",
|
|
18272
18897
|
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.",
|
|
18898
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
18273
18899
|
inputSchema: {
|
|
18274
18900
|
type: "object",
|
|
18275
18901
|
properties: {
|
|
@@ -18297,6 +18923,16 @@ var adHocTool = {
|
|
|
18297
18923
|
epic: {
|
|
18298
18924
|
type: "string",
|
|
18299
18925
|
description: 'Epic this relates to (default: "Platform").'
|
|
18926
|
+
},
|
|
18927
|
+
priority: {
|
|
18928
|
+
type: "string",
|
|
18929
|
+
enum: ["P0 Critical", "P1 High", "P2 Medium", "P3 Low"],
|
|
18930
|
+
description: "Task priority (default: P2 Medium). Use P1 for important fixes, P0 for critical incidents."
|
|
18931
|
+
},
|
|
18932
|
+
type: {
|
|
18933
|
+
type: "string",
|
|
18934
|
+
enum: ["task", "bug", "research", "discovery", "spike", "idea"],
|
|
18935
|
+
description: 'Task type (default: inferred from description \u2014 "fix"/"bug" \u2192 bug, "research"/"investigate" \u2192 research, otherwise task).'
|
|
18300
18936
|
}
|
|
18301
18937
|
},
|
|
18302
18938
|
required: []
|
|
@@ -18312,6 +18948,14 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
18312
18948
|
if (!VALID_EFFORTS.includes(effortRaw)) {
|
|
18313
18949
|
return errorResponse(`effort must be one of: ${VALID_EFFORTS.join(", ")}`);
|
|
18314
18950
|
}
|
|
18951
|
+
const priorityRaw = args.priority || "P2 Medium";
|
|
18952
|
+
if (!VALID_PRIORITIES.includes(priorityRaw)) {
|
|
18953
|
+
return errorResponse(`priority must be one of: ${VALID_PRIORITIES.join(", ")}`);
|
|
18954
|
+
}
|
|
18955
|
+
const typeRaw = args.type || inferTaskType(title || (args.notes ?? ""));
|
|
18956
|
+
if (!VALID_TYPES.includes(typeRaw)) {
|
|
18957
|
+
return errorResponse(`type must be one of: ${VALID_TYPES.join(", ")}`);
|
|
18958
|
+
}
|
|
18315
18959
|
const MAX_NOTES_LENGTH = 2e3;
|
|
18316
18960
|
let rawNotes = args.notes?.trim();
|
|
18317
18961
|
let notesTruncated = false;
|
|
@@ -18325,7 +18969,11 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
18325
18969
|
notes: rawNotes,
|
|
18326
18970
|
effort: effortRaw,
|
|
18327
18971
|
module: args.module,
|
|
18328
|
-
epic: args.epic
|
|
18972
|
+
epic: args.epic,
|
|
18973
|
+
priority: priorityRaw,
|
|
18974
|
+
taskType: typeRaw,
|
|
18975
|
+
// PROJECT-SPECIFIC: owner resolved from config (PAPI_OWNER env var, default 'Cathal')
|
|
18976
|
+
owner: config2.projectOwner
|
|
18329
18977
|
});
|
|
18330
18978
|
if (isGitAvailable() && isGitRepo(config2.projectRoot)) {
|
|
18331
18979
|
try {
|
|
@@ -18338,8 +18986,11 @@ async function handleAdHoc(adapter2, config2, args) {
|
|
|
18338
18986
|
}
|
|
18339
18987
|
}
|
|
18340
18988
|
const truncateWarning = notesTruncated ? ` (notes truncated to ${MAX_NOTES_LENGTH} chars)` : "";
|
|
18989
|
+
const taskModule = result.task.module || "Core";
|
|
18990
|
+
const typeLabel = result.task.taskType || typeRaw;
|
|
18341
18991
|
return textResponse(
|
|
18342
|
-
`**${result.task.id}:** "${result.task.title}"
|
|
18992
|
+
`**${result.task.id}:** "${result.task.title}" recorded (${effortRaw}, ${priorityRaw}, ${typeLabel}, ${taskModule}).${truncateWarning} Build report attached.
|
|
18993
|
+
_To correct: board_edit ${result.task.id} with updated fields._`
|
|
18343
18994
|
);
|
|
18344
18995
|
}
|
|
18345
18996
|
|
|
@@ -18480,9 +19131,9 @@ async function prepareReconcile(adapter2) {
|
|
|
18480
19131
|
}
|
|
18481
19132
|
return lines.join("\n");
|
|
18482
19133
|
}
|
|
18483
|
-
var
|
|
19134
|
+
var STOP_WORDS3 = /* @__PURE__ */ new Set(["the", "a", "an", "and", "or", "for", "in", "on", "to", "of", "is", "with", "from", "by", "vs", "not", "no", "do"]);
|
|
18484
19135
|
function tokenize(s) {
|
|
18485
|
-
return s.toLowerCase().replace(/[^a-z0-9\s-]/g, "").split(/\s+/).filter((w) => w.length > 2 && !
|
|
19136
|
+
return s.toLowerCase().replace(/[^a-z0-9\s-]/g, "").split(/\s+/).filter((w) => w.length > 2 && !STOP_WORDS3.has(w));
|
|
18486
19137
|
}
|
|
18487
19138
|
function titleKeywords(title) {
|
|
18488
19139
|
return new Set(tokenize(title));
|
|
@@ -18562,7 +19213,7 @@ async function applyReconcile(adapter2, corrections) {
|
|
|
18562
19213
|
}
|
|
18563
19214
|
return { applied, skipped, details, phaseChanges };
|
|
18564
19215
|
}
|
|
18565
|
-
var
|
|
19216
|
+
var VALID_PRIORITIES2 = /* @__PURE__ */ new Set(["P0 Critical", "P1 High", "P2 Medium", "P3 Low"]);
|
|
18566
19217
|
var VALID_COMPLEXITIES = /* @__PURE__ */ new Set(["XS", "Small", "Medium", "Large", "XL"]);
|
|
18567
19218
|
async function prepareRetriage(adapter2) {
|
|
18568
19219
|
const health = await adapter2.getCycleHealth();
|
|
@@ -18623,7 +19274,7 @@ async function applyRetriage(adapter2, retriages) {
|
|
|
18623
19274
|
skipped++;
|
|
18624
19275
|
continue;
|
|
18625
19276
|
}
|
|
18626
|
-
if (!
|
|
19277
|
+
if (!VALID_PRIORITIES2.has(r.priority)) {
|
|
18627
19278
|
details.push(`${r.taskId}: skipped \u2014 invalid priority "${r.priority}"`);
|
|
18628
19279
|
skipped++;
|
|
18629
19280
|
continue;
|
|
@@ -18659,6 +19310,7 @@ async function applyRetriage(adapter2, retriages) {
|
|
|
18659
19310
|
var boardReconcileTool = {
|
|
18660
19311
|
name: "board_reconcile",
|
|
18661
19312
|
description: 'Holistic backlog review to group, merge, cancel, defer, or retriage tasks. "prepare"/"apply" for cleanup. "retriage-prepare"/"retriage-apply" to reassess priority and complexity on existing backlog tasks. Does not call the Anthropic API.',
|
|
19313
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
18662
19314
|
inputSchema: {
|
|
18663
19315
|
type: "object",
|
|
18664
19316
|
properties: {
|
|
@@ -18989,451 +19641,118 @@ Assess each task above and produce your retriage output. Then call \`board_recon
|
|
|
18989
19641
|
return errorResponse(`Unknown mode: ${mode}. Use "prepare", "apply", "retriage-prepare", or "retriage-apply".`);
|
|
18990
19642
|
}
|
|
18991
19643
|
|
|
18992
|
-
// src/services/
|
|
18993
|
-
|
|
18994
|
-
|
|
18995
|
-
|
|
18996
|
-
|
|
18997
|
-
|
|
18998
|
-
|
|
18999
|
-
|
|
19000
|
-
|
|
19001
|
-
|
|
19002
|
-
|
|
19003
|
-
|
|
19004
|
-
|
|
19005
|
-
|
|
19644
|
+
// src/services/release.ts
|
|
19645
|
+
init_git();
|
|
19646
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
19647
|
+
import { join as join6 } from "path";
|
|
19648
|
+
var INITIAL_RELEASE_NOTES = `# Changelog
|
|
19649
|
+
|
|
19650
|
+
## v0.1.0-alpha \u2014 Initial Release
|
|
19651
|
+
|
|
19652
|
+
PAPI MCP Server \u2014 the AI-powered project planning framework.
|
|
19653
|
+
|
|
19654
|
+
### Commands
|
|
19655
|
+
- **setup** \u2014 Initialise a new PAPI project with Product Brief generation
|
|
19656
|
+
- **plan** \u2014 Run cycle planning with embedded BUILD HANDOFFs (Bootstrap + Full modes)
|
|
19657
|
+
- **build_list / build_describe / build_execute / build_cancel** \u2014 Manage build tasks
|
|
19658
|
+
- **board_view / board_deprioritise / board_archive** \u2014 View and manage the Board
|
|
19659
|
+
- **strategy_review / strategy_change** \u2014 Run Strategy Reviews and apply strategic changes
|
|
19660
|
+
- **review_list / review_submit** \u2014 Human review loop for handoffs and builds
|
|
19661
|
+
- **idea** \u2014 Capture ideas as backlog tasks for future triage
|
|
19662
|
+
- **health** \u2014 Cycle Health Summary dashboard
|
|
19663
|
+
- **release** \u2014 Cut versioned releases with git tags and changelogs
|
|
19664
|
+
|
|
19665
|
+
### Features
|
|
19666
|
+
- .md file persistence in .papi/ directory
|
|
19667
|
+
- Bootstrap + Full planning modes with Anthropic API integration
|
|
19668
|
+
- Embedded BUILD HANDOFFs with dual write-back build reports
|
|
19669
|
+
- Auto-commit and auto-PR after builds
|
|
19670
|
+
- Board corrections and Active Decision persistence
|
|
19671
|
+
- Single-purpose MCP tools for optimal LLM tool selection
|
|
19672
|
+
- Consistent error handling across all tools
|
|
19673
|
+
`;
|
|
19674
|
+
function generateChangelog(version, commits) {
|
|
19675
|
+
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
19676
|
+
const commitList = commits.map((c) => `- ${c}`).join("\n");
|
|
19677
|
+
return `# Changelog
|
|
19678
|
+
|
|
19679
|
+
## ${version} \u2014 ${date}
|
|
19680
|
+
|
|
19681
|
+
${commitList}
|
|
19682
|
+
`;
|
|
19683
|
+
}
|
|
19684
|
+
async function createRelease(config2, branch, version, adapter2) {
|
|
19685
|
+
if (!isGitAvailable()) {
|
|
19686
|
+
throw new Error("git is not available.");
|
|
19006
19687
|
}
|
|
19007
|
-
if (
|
|
19008
|
-
|
|
19009
|
-
scores.push({ name: "Estimation accuracy", score: Math.round(avgMatchRate), weight: 0.25 });
|
|
19010
|
-
} else {
|
|
19011
|
-
scores.push({ name: "Estimation accuracy", score: 50, weight: 0.25 });
|
|
19688
|
+
if (!isGitRepo(config2.projectRoot)) {
|
|
19689
|
+
throw new Error("not a git repository.");
|
|
19012
19690
|
}
|
|
19013
|
-
|
|
19014
|
-
|
|
19015
|
-
scores.push({ name: "Review throughput", score: reviewScore, weight: 0.2 });
|
|
19016
|
-
const backlogTasks = activeTasks.filter((t) => t.status === "Backlog");
|
|
19017
|
-
if (backlogTasks.length > 0) {
|
|
19018
|
-
const criticalCount = backlogTasks.filter(
|
|
19019
|
-
(t) => t.priority === "P0 Critical" || t.priority === "P1 High"
|
|
19020
|
-
).length;
|
|
19021
|
-
const criticalRatio = criticalCount / backlogTasks.length;
|
|
19022
|
-
const backlogScore = criticalRatio > 0.5 ? 40 : criticalRatio > 0.3 ? 70 : 90;
|
|
19023
|
-
scores.push({ name: "Backlog health", score: backlogScore, weight: 0.15 });
|
|
19024
|
-
} else {
|
|
19025
|
-
scores.push({ name: "Backlog health", score: 80, weight: 0.15 });
|
|
19691
|
+
if (hasUncommittedChanges(config2.projectRoot, AUTO_WRITTEN_PATHS)) {
|
|
19692
|
+
throw new Error("working directory has uncommitted changes. Commit or stash them before releasing.");
|
|
19026
19693
|
}
|
|
19027
|
-
|
|
19028
|
-
|
|
19029
|
-
|
|
19030
|
-
|
|
19031
|
-
|
|
19032
|
-
|
|
19694
|
+
const warnings = [];
|
|
19695
|
+
if (adapter2) {
|
|
19696
|
+
try {
|
|
19697
|
+
const versionMatch = version.match(/^v0\.(\d+)\./);
|
|
19698
|
+
const currentCycle = versionMatch ? parseInt(versionMatch[1], 10) : 0;
|
|
19699
|
+
if (currentCycle > 0) {
|
|
19700
|
+
await adapter2.createCycle({
|
|
19701
|
+
id: `cycle-${currentCycle}`,
|
|
19702
|
+
number: currentCycle,
|
|
19703
|
+
status: "complete",
|
|
19704
|
+
startDate: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19705
|
+
endDate: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19706
|
+
goals: [],
|
|
19707
|
+
boardHealth: "",
|
|
19708
|
+
taskIds: []
|
|
19709
|
+
});
|
|
19710
|
+
}
|
|
19711
|
+
} catch (err) {
|
|
19712
|
+
const msg = `createCycle (mark complete) failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
19713
|
+
console.error(`[release] ${msg}`);
|
|
19714
|
+
warnings.push(msg);
|
|
19715
|
+
}
|
|
19033
19716
|
}
|
|
19034
|
-
const
|
|
19035
|
-
|
|
19036
|
-
|
|
19037
|
-
const reason = status === "GREEN" ? "All components healthy" : `${worst.name} below target (${worst.score}/100)`;
|
|
19038
|
-
return { score: totalScore, status, reason };
|
|
19039
|
-
}
|
|
19040
|
-
function countByStatus(tasks) {
|
|
19041
|
-
const counts = /* @__PURE__ */ new Map();
|
|
19042
|
-
for (const task of tasks) {
|
|
19043
|
-
counts.set(task.status, (counts.get(task.status) ?? 0) + 1);
|
|
19717
|
+
const checkout = checkoutBranch(config2.projectRoot, branch);
|
|
19718
|
+
if (!checkout.success) {
|
|
19719
|
+
throw new Error(checkout.message);
|
|
19044
19720
|
}
|
|
19045
|
-
|
|
19046
|
-
|
|
19047
|
-
|
|
19048
|
-
|
|
19049
|
-
const activeTasks = await adapter2.queryBoard({
|
|
19050
|
-
status: ["Backlog", "In Cycle", "Ready", "In Progress", "In Review", "Blocked"]
|
|
19051
|
-
});
|
|
19052
|
-
const logEntries = await adapter2.getCycleLog(3);
|
|
19053
|
-
const cycleNumber = health.totalCycles;
|
|
19054
|
-
const cyclesSinceReview = health.cyclesSinceLastStrategyReview;
|
|
19055
|
-
const reviewDue = health.strategyReviewDue;
|
|
19056
|
-
const reviewGateBlocking = cyclesSinceReview >= 5;
|
|
19057
|
-
const reviewWarning = reviewGateBlocking ? `\u26A0\uFE0F GATE \u2014 ${cyclesSinceReview} cycles since last Strategy Review. \`plan\` is blocked until \`strategy_review\` runs (or \`force: true\`).` : `\u2713 On track \u2014 ${cyclesSinceReview} cycle(s) since last review. Next due: ${reviewDue}`;
|
|
19058
|
-
const deferredCount = activeTasks.filter((t) => t.status === "Deferred").length;
|
|
19059
|
-
const nonDeferredTasks = activeTasks.filter((t) => t.status !== "Deferred");
|
|
19060
|
-
const statusCounts = countByStatus(nonDeferredTasks);
|
|
19061
|
-
let boardSummary;
|
|
19062
|
-
if (nonDeferredTasks.length === 0 && deferredCount === 0) {
|
|
19063
|
-
boardSummary = "0 tasks \u2014 board may need reloading";
|
|
19064
|
-
} else {
|
|
19065
|
-
const parts = [];
|
|
19066
|
-
for (const [status, count] of statusCounts) {
|
|
19067
|
-
parts.push(`${count} ${status}`);
|
|
19068
|
-
}
|
|
19069
|
-
boardSummary = `${nonDeferredTasks.length} active tasks \u2014 ${parts.join(", ")}`;
|
|
19070
|
-
if (deferredCount > 0) {
|
|
19071
|
-
boardSummary += ` + ${deferredCount} deferred`;
|
|
19721
|
+
if (hasRemote(config2.projectRoot)) {
|
|
19722
|
+
const pull = gitPull(config2.projectRoot);
|
|
19723
|
+
if (!pull.success) {
|
|
19724
|
+
warnings.push(`git pull failed: ${pull.message}. Run manually.`);
|
|
19072
19725
|
}
|
|
19073
19726
|
}
|
|
19074
|
-
|
|
19075
|
-
|
|
19076
|
-
const inReviewTasks = activeTasks.filter((t) => t.status === "In Review");
|
|
19077
|
-
const inReviewSummary = inReviewTasks.length > 0 ? `${inReviewTasks.length} task(s) ready for sign-off: ${inReviewTasks.map((t) => t.id).join(", ")}` : "No tasks waiting for sign-off";
|
|
19078
|
-
let carryForward = "None found";
|
|
19079
|
-
if (logEntries.length > 0) {
|
|
19080
|
-
const latest = logEntries[0];
|
|
19081
|
-
if (latest.carryForward) {
|
|
19082
|
-
carryForward = latest.carryForward;
|
|
19083
|
-
} else {
|
|
19084
|
-
carryForward = `No carry-forward in Cycle ${latest.cycleNumber}`;
|
|
19085
|
-
}
|
|
19727
|
+
if (tagExists(config2.projectRoot, version)) {
|
|
19728
|
+
throw new Error(`tag "${version}" already exists. Use a different version.`);
|
|
19086
19729
|
}
|
|
19087
|
-
|
|
19088
|
-
|
|
19089
|
-
if (
|
|
19090
|
-
|
|
19730
|
+
const latestTag = getLatestTag(config2.projectRoot);
|
|
19731
|
+
let changelogContent;
|
|
19732
|
+
if (!latestTag) {
|
|
19733
|
+
changelogContent = INITIAL_RELEASE_NOTES.replace("v0.1.0-alpha", version);
|
|
19734
|
+
} else {
|
|
19735
|
+
const commits = getCommitsSinceTag(config2.projectRoot, latestTag);
|
|
19736
|
+
changelogContent = generateChangelog(version, commits);
|
|
19091
19737
|
}
|
|
19092
|
-
|
|
19093
|
-
|
|
19738
|
+
const changelogPath = join6(config2.projectRoot, "CHANGELOG.md");
|
|
19739
|
+
await writeFile3(changelogPath, changelogContent, "utf-8");
|
|
19740
|
+
const commitResult = stageAllAndCommit(config2.projectRoot, `release: ${version}`);
|
|
19741
|
+
const commitNote = commitResult.committed ? `Committed CHANGELOG.md.` : `CHANGELOG.md: ${commitResult.message}`;
|
|
19742
|
+
const tagResult = createTag(config2.projectRoot, version, `Release ${version}`);
|
|
19743
|
+
if (!tagResult.success) {
|
|
19744
|
+
throw new Error(tagResult.message);
|
|
19094
19745
|
}
|
|
19095
|
-
const
|
|
19096
|
-
|
|
19097
|
-
|
|
19098
|
-
|
|
19099
|
-
(
|
|
19100
|
-
|
|
19101
|
-
|
|
19102
|
-
(
|
|
19103
|
-
);
|
|
19104
|
-
if (reasons.length > 0) {
|
|
19105
|
-
recommendedMode = `**Full** \u2014 ${reasons.join("; ")}`;
|
|
19106
|
-
} else if (unbuiltCycleTasks.length > 0) {
|
|
19107
|
-
recommendedMode = `**Build** \u2014 ${unbuiltCycleTasks.length} cycle task(s) not yet started`;
|
|
19108
|
-
} else if (inProgressCycleTasks.length > 0) {
|
|
19109
|
-
recommendedMode = `**Build** \u2014 ${inProgressCycleTasks.length} task(s) in progress`;
|
|
19110
|
-
} else if (inReviewCycleTasks.length > 0) {
|
|
19111
|
-
recommendedMode = `**Review** \u2014 ${inReviewCycleTasks.length} task(s) awaiting review`;
|
|
19746
|
+
const pushNotes = [];
|
|
19747
|
+
if (hasRemote(config2.projectRoot)) {
|
|
19748
|
+
const branchPush = gitPush(config2.projectRoot, branch);
|
|
19749
|
+
pushNotes.push(branchPush.success ? `Pushed '${branch}' to origin.` : `Push branch failed: ${branchPush.message}`);
|
|
19750
|
+
if (!branchPush.success) warnings.push(branchPush.message);
|
|
19751
|
+
const tagPush = gitPush(config2.projectRoot, version);
|
|
19752
|
+
pushNotes.push(tagPush.success ? `Pushed tag '${version}' to origin.` : `Push tag failed: ${tagPush.message}`);
|
|
19753
|
+
if (!tagPush.success) warnings.push(tagPush.message);
|
|
19112
19754
|
} else {
|
|
19113
|
-
|
|
19114
|
-
}
|
|
19115
|
-
let metricsSection;
|
|
19116
|
-
let derivedMetricsSection = "";
|
|
19117
|
-
let snapshots = [];
|
|
19118
|
-
try {
|
|
19119
|
-
try {
|
|
19120
|
-
const reports = await adapter2.getRecentBuildReports(50);
|
|
19121
|
-
snapshots = computeSnapshotsFromBuildReports(reports);
|
|
19122
|
-
} catch {
|
|
19123
|
-
}
|
|
19124
|
-
metricsSection = formatCycleMetrics(snapshots);
|
|
19125
|
-
derivedMetricsSection = formatDerivedMetrics(snapshots, activeTasks);
|
|
19126
|
-
} catch (_err) {
|
|
19127
|
-
metricsSection = "Could not read methodology metrics.";
|
|
19128
|
-
}
|
|
19129
|
-
try {
|
|
19130
|
-
const recentReports = await adapter2.getRecentBuildReports(50);
|
|
19131
|
-
if (recentReports.length > 0) {
|
|
19132
|
-
const taskCounts = /* @__PURE__ */ new Map();
|
|
19133
|
-
for (const r of recentReports) {
|
|
19134
|
-
taskCounts.set(r.taskId, (taskCounts.get(r.taskId) ?? 0) + 1);
|
|
19135
|
-
}
|
|
19136
|
-
const iterCounts = [...taskCounts.values()];
|
|
19137
|
-
const avgIter = iterCounts.reduce((s, c) => s + c, 0) / iterCounts.length;
|
|
19138
|
-
const multiIterTasks = iterCounts.filter((c) => c > 1).length;
|
|
19139
|
-
if (avgIter > 1 || multiIterTasks > 0) {
|
|
19140
|
-
derivedMetricsSection += `
|
|
19141
|
-
|
|
19142
|
-
**Rework**
|
|
19143
|
-
- Average iterations: ${avgIter.toFixed(1)} (${multiIterTasks} task${multiIterTasks !== 1 ? "s" : ""} with pushbacks)`;
|
|
19144
|
-
}
|
|
19145
|
-
}
|
|
19146
|
-
} catch {
|
|
19147
|
-
}
|
|
19148
|
-
const costSection = "Disabled \u2014 local MCP, no API costs.";
|
|
19149
|
-
let decisionUsageSection = "";
|
|
19150
|
-
let decisionUsageEntries = [];
|
|
19151
|
-
try {
|
|
19152
|
-
const usage = await adapter2.getDecisionUsage(cycleNumber);
|
|
19153
|
-
decisionUsageEntries = usage;
|
|
19154
|
-
if (usage.length > 0) {
|
|
19155
|
-
const stale = usage.filter((u) => u.cyclesSinceLastReference >= 5);
|
|
19156
|
-
if (stale.length > 0) {
|
|
19157
|
-
const lines = stale.map(
|
|
19158
|
-
(u) => `- ${u.decisionId}: last referenced Cycle ${u.lastReferencedCycle} (${u.cyclesSinceLastReference} cycles ago)`
|
|
19159
|
-
);
|
|
19160
|
-
decisionUsageSection = `**Stale ADs (5+ cycles unreferenced):**
|
|
19161
|
-
${lines.join("\n")}`;
|
|
19162
|
-
} else {
|
|
19163
|
-
decisionUsageSection = `All ${usage.length} tracked ADs referenced within last 5 cycles.`;
|
|
19164
|
-
}
|
|
19165
|
-
}
|
|
19166
|
-
} catch {
|
|
19167
|
-
}
|
|
19168
|
-
let decisionLifecycleSection = "";
|
|
19169
|
-
try {
|
|
19170
|
-
const decisions = await adapter2.getActiveDecisions();
|
|
19171
|
-
const lifecycleSummary = formatDecisionLifecycleSummary(decisions);
|
|
19172
|
-
if (lifecycleSummary) {
|
|
19173
|
-
decisionLifecycleSection = `**Lifecycle:** ${lifecycleSummary}`;
|
|
19174
|
-
}
|
|
19175
|
-
} catch {
|
|
19176
|
-
}
|
|
19177
|
-
const decisionScoresSection = "";
|
|
19178
|
-
let contextUtilisationSection = "";
|
|
19179
|
-
try {
|
|
19180
|
-
const utilData = await adapter2.getContextUtilisation?.();
|
|
19181
|
-
if (utilData && utilData.length > 0) {
|
|
19182
|
-
const lines = utilData.filter((u) => u.cycleNumber === cycleNumber).map((u) => `- ${u.tool}: ${(u.avgUtilisation * 100).toFixed(0)}% utilisation (${(u.avgContextBytes / 1024).toFixed(1)}KB avg context)`);
|
|
19183
|
-
if (lines.length > 0) {
|
|
19184
|
-
contextUtilisationSection = `**Current cycle:**
|
|
19185
|
-
${lines.join("\n")}`;
|
|
19186
|
-
}
|
|
19187
|
-
}
|
|
19188
|
-
} catch {
|
|
19189
|
-
}
|
|
19190
|
-
let northStarSection = "";
|
|
19191
|
-
try {
|
|
19192
|
-
const staleness = await adapter2.getNorthStarStaleness?.();
|
|
19193
|
-
if (staleness) {
|
|
19194
|
-
const cycleGap = cycleNumber - staleness.setCycle;
|
|
19195
|
-
const daysSinceSet = Math.floor((Date.now() - new Date(staleness.setAt).getTime()) / (1e3 * 60 * 60 * 24));
|
|
19196
|
-
northStarSection = `\u2713 North Star set Cycle ${staleness.setCycle} (${cycleGap} cycles, ${daysSinceSet} days ago)`;
|
|
19197
|
-
} else {
|
|
19198
|
-
const setAtCycle = await adapter2.getNorthStarSetCycle?.();
|
|
19199
|
-
if (setAtCycle != null) {
|
|
19200
|
-
northStarSection = `\u2713 North Star set Cycle ${setAtCycle}`;
|
|
19201
|
-
} else if (adapter2.getCurrentNorthStar) {
|
|
19202
|
-
const ns = await adapter2.getCurrentNorthStar();
|
|
19203
|
-
northStarSection = ns ? "" : "\u26A0\uFE0F No North Star set \u2014 consider defining one";
|
|
19204
|
-
}
|
|
19205
|
-
}
|
|
19206
|
-
} catch {
|
|
19207
|
-
}
|
|
19208
|
-
const healthResult = computeHealthScore(cycleNumber, snapshots, activeTasks, decisionUsageEntries);
|
|
19209
|
-
return {
|
|
19210
|
-
cycleNumber,
|
|
19211
|
-
latestCycleStatus: health.latestCycleStatus,
|
|
19212
|
-
connectionStatus: getConnectionStatus(),
|
|
19213
|
-
reviewWarning,
|
|
19214
|
-
boardSummary,
|
|
19215
|
-
staleTasks,
|
|
19216
|
-
inReviewSummary,
|
|
19217
|
-
carryForward,
|
|
19218
|
-
recommendedMode,
|
|
19219
|
-
metricsSection,
|
|
19220
|
-
derivedMetricsSection,
|
|
19221
|
-
costSection,
|
|
19222
|
-
decisionUsageSection,
|
|
19223
|
-
decisionLifecycleSection,
|
|
19224
|
-
decisionScoresSection,
|
|
19225
|
-
contextUtilisationSection,
|
|
19226
|
-
northStarSection,
|
|
19227
|
-
healthScore: healthResult?.score ?? null,
|
|
19228
|
-
healthStatus: healthResult?.status ?? null,
|
|
19229
|
-
healthReason: healthResult?.reason ?? null
|
|
19230
|
-
};
|
|
19231
|
-
}
|
|
19232
|
-
|
|
19233
|
-
// src/tools/health.ts
|
|
19234
|
-
var healthTool = {
|
|
19235
|
-
name: "health",
|
|
19236
|
-
description: "Cycle Health Summary \u2014 shows current cycle number, Strategy Review cadence status (AD-5), board health (task counts by status, stale tasks), last carry-forward items, and recommended next mode. Read-only, does not modify any files.",
|
|
19237
|
-
inputSchema: {
|
|
19238
|
-
type: "object",
|
|
19239
|
-
properties: {},
|
|
19240
|
-
required: []
|
|
19241
|
-
}
|
|
19242
|
-
};
|
|
19243
|
-
function formatHealthSummary(summary) {
|
|
19244
|
-
const lines = [];
|
|
19245
|
-
lines.push(`# Cycle ${summary.cycleNumber} \u2014 Health`);
|
|
19246
|
-
lines.push("");
|
|
19247
|
-
if (summary.connectionStatus !== "offline") {
|
|
19248
|
-
const statusIcon = summary.connectionStatus === "connected" ? "\u2713" : "\u26A0\uFE0F";
|
|
19249
|
-
const statusLabel = summary.connectionStatus === "connected" ? "Supabase connected" : "Supabase degraded \u2014 data may be stale. Check DATABASE_URL in .mcp.json";
|
|
19250
|
-
lines.push(`**Connection:** ${statusIcon} ${statusLabel}`);
|
|
19251
|
-
lines.push("");
|
|
19252
|
-
}
|
|
19253
|
-
lines.push(`> **Next action:** ${summary.recommendedMode}`);
|
|
19254
|
-
lines.push("");
|
|
19255
|
-
lines.push(`**Strategy Review:** ${summary.reviewWarning}`);
|
|
19256
|
-
lines.push("");
|
|
19257
|
-
lines.push(`## Board`);
|
|
19258
|
-
lines.push(summary.boardSummary);
|
|
19259
|
-
const hasInProgress = !summary.staleTasks.startsWith("No tasks");
|
|
19260
|
-
const hasInReview = !summary.inReviewSummary.startsWith("No tasks");
|
|
19261
|
-
if (hasInProgress || hasInReview) {
|
|
19262
|
-
lines.push("");
|
|
19263
|
-
if (hasInProgress) lines.push(`- **In Progress:** ${summary.staleTasks}`);
|
|
19264
|
-
if (hasInReview) lines.push(`- **In Review:** ${summary.inReviewSummary}`);
|
|
19265
|
-
}
|
|
19266
|
-
lines.push("");
|
|
19267
|
-
const hasCarryForward = summary.carryForward !== "None found" && !summary.carryForward.startsWith("No carry-forward");
|
|
19268
|
-
if (hasCarryForward) {
|
|
19269
|
-
lines.push(`## Carry-Forward`);
|
|
19270
|
-
lines.push(summary.carryForward);
|
|
19271
|
-
lines.push("");
|
|
19272
|
-
}
|
|
19273
|
-
const hasMetrics = summary.metricsSection !== "Could not read methodology metrics." && !summary.metricsSection.includes("undefined");
|
|
19274
|
-
if (hasMetrics) {
|
|
19275
|
-
lines.push(`## Trends`);
|
|
19276
|
-
lines.push(summary.metricsSection);
|
|
19277
|
-
lines.push("");
|
|
19278
|
-
}
|
|
19279
|
-
if (summary.derivedMetricsSection) {
|
|
19280
|
-
lines.push(`## Insights`);
|
|
19281
|
-
lines.push(summary.derivedMetricsSection);
|
|
19282
|
-
lines.push("");
|
|
19283
|
-
}
|
|
19284
|
-
const hasCost = summary.costSection !== "No metrics data yet." && summary.costSection !== "Could not read metrics data.";
|
|
19285
|
-
if (hasCost) {
|
|
19286
|
-
lines.push(`## Cost`);
|
|
19287
|
-
lines.push(summary.costSection);
|
|
19288
|
-
lines.push("");
|
|
19289
|
-
}
|
|
19290
|
-
if (summary.northStarSection) {
|
|
19291
|
-
lines.push(`## North Star`);
|
|
19292
|
-
lines.push(summary.northStarSection);
|
|
19293
|
-
lines.push("");
|
|
19294
|
-
}
|
|
19295
|
-
if (summary.decisionUsageSection) {
|
|
19296
|
-
lines.push(`## Decision Usage`);
|
|
19297
|
-
lines.push(summary.decisionUsageSection);
|
|
19298
|
-
if (summary.decisionLifecycleSection) {
|
|
19299
|
-
lines.push(summary.decisionLifecycleSection);
|
|
19300
|
-
}
|
|
19301
|
-
lines.push("");
|
|
19302
|
-
}
|
|
19303
|
-
if (summary.contextUtilisationSection) {
|
|
19304
|
-
lines.push(`## Context Utilisation`);
|
|
19305
|
-
lines.push(summary.contextUtilisationSection);
|
|
19306
|
-
lines.push("");
|
|
19307
|
-
}
|
|
19308
|
-
if (summary.decisionScoresSection) {
|
|
19309
|
-
lines.push(`## Decision Scores`);
|
|
19310
|
-
lines.push(summary.decisionScoresSection);
|
|
19311
|
-
lines.push("");
|
|
19312
|
-
}
|
|
19313
|
-
return lines.join("\n").trimEnd();
|
|
19314
|
-
}
|
|
19315
|
-
async function handleHealth(adapter2) {
|
|
19316
|
-
try {
|
|
19317
|
-
const summary = await getHealthSummary(adapter2);
|
|
19318
|
-
return textResponse(formatHealthSummary(summary));
|
|
19319
|
-
} catch (err) {
|
|
19320
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
19321
|
-
return errorResponse(`Could not read cycle health. Run \`setup\` first to initialise your project. (${message})`);
|
|
19322
|
-
}
|
|
19323
|
-
}
|
|
19324
|
-
|
|
19325
|
-
// src/services/release.ts
|
|
19326
|
-
init_git();
|
|
19327
|
-
import { writeFile as writeFile3 } from "fs/promises";
|
|
19328
|
-
import { join as join6 } from "path";
|
|
19329
|
-
var INITIAL_RELEASE_NOTES = `# Changelog
|
|
19330
|
-
|
|
19331
|
-
## v0.1.0-alpha \u2014 Initial Release
|
|
19332
|
-
|
|
19333
|
-
PAPI MCP Server \u2014 the AI-powered project planning framework.
|
|
19334
|
-
|
|
19335
|
-
### Commands
|
|
19336
|
-
- **setup** \u2014 Initialise a new PAPI project with Product Brief generation
|
|
19337
|
-
- **plan** \u2014 Run cycle planning with embedded BUILD HANDOFFs (Bootstrap + Full modes)
|
|
19338
|
-
- **build_list / build_describe / build_execute / build_cancel** \u2014 Manage build tasks
|
|
19339
|
-
- **board_view / board_deprioritise / board_archive** \u2014 View and manage the Board
|
|
19340
|
-
- **strategy_review / strategy_change** \u2014 Run Strategy Reviews and apply strategic changes
|
|
19341
|
-
- **review_list / review_submit** \u2014 Human review loop for handoffs and builds
|
|
19342
|
-
- **idea** \u2014 Capture ideas as backlog tasks for future triage
|
|
19343
|
-
- **health** \u2014 Cycle Health Summary dashboard
|
|
19344
|
-
- **release** \u2014 Cut versioned releases with git tags and changelogs
|
|
19345
|
-
|
|
19346
|
-
### Features
|
|
19347
|
-
- .md file persistence in .papi/ directory
|
|
19348
|
-
- Bootstrap + Full planning modes with Anthropic API integration
|
|
19349
|
-
- Embedded BUILD HANDOFFs with dual write-back build reports
|
|
19350
|
-
- Auto-commit and auto-PR after builds
|
|
19351
|
-
- Board corrections and Active Decision persistence
|
|
19352
|
-
- Single-purpose MCP tools for optimal LLM tool selection
|
|
19353
|
-
- Consistent error handling across all tools
|
|
19354
|
-
`;
|
|
19355
|
-
function generateChangelog(version, commits) {
|
|
19356
|
-
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
19357
|
-
const commitList = commits.map((c) => `- ${c}`).join("\n");
|
|
19358
|
-
return `# Changelog
|
|
19359
|
-
|
|
19360
|
-
## ${version} \u2014 ${date}
|
|
19361
|
-
|
|
19362
|
-
${commitList}
|
|
19363
|
-
`;
|
|
19364
|
-
}
|
|
19365
|
-
async function createRelease(config2, branch, version, adapter2) {
|
|
19366
|
-
if (!isGitAvailable()) {
|
|
19367
|
-
throw new Error("git is not available.");
|
|
19368
|
-
}
|
|
19369
|
-
if (!isGitRepo(config2.projectRoot)) {
|
|
19370
|
-
throw new Error("not a git repository.");
|
|
19371
|
-
}
|
|
19372
|
-
if (hasUncommittedChanges(config2.projectRoot, AUTO_WRITTEN_PATHS)) {
|
|
19373
|
-
throw new Error("working directory has uncommitted changes. Commit or stash them before releasing.");
|
|
19374
|
-
}
|
|
19375
|
-
const warnings = [];
|
|
19376
|
-
if (adapter2) {
|
|
19377
|
-
try {
|
|
19378
|
-
const versionMatch = version.match(/^v0\.(\d+)\./);
|
|
19379
|
-
const currentCycle = versionMatch ? parseInt(versionMatch[1], 10) : 0;
|
|
19380
|
-
if (currentCycle > 0) {
|
|
19381
|
-
await adapter2.createCycle({
|
|
19382
|
-
id: `cycle-${currentCycle}`,
|
|
19383
|
-
number: currentCycle,
|
|
19384
|
-
status: "complete",
|
|
19385
|
-
startDate: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19386
|
-
endDate: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19387
|
-
goals: [],
|
|
19388
|
-
boardHealth: "",
|
|
19389
|
-
taskIds: []
|
|
19390
|
-
});
|
|
19391
|
-
}
|
|
19392
|
-
} catch (err) {
|
|
19393
|
-
const msg = `createCycle (mark complete) failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
19394
|
-
console.error(`[release] ${msg}`);
|
|
19395
|
-
warnings.push(msg);
|
|
19396
|
-
}
|
|
19397
|
-
}
|
|
19398
|
-
const checkout = checkoutBranch(config2.projectRoot, branch);
|
|
19399
|
-
if (!checkout.success) {
|
|
19400
|
-
throw new Error(checkout.message);
|
|
19401
|
-
}
|
|
19402
|
-
if (hasRemote(config2.projectRoot)) {
|
|
19403
|
-
const pull = gitPull(config2.projectRoot);
|
|
19404
|
-
if (!pull.success) {
|
|
19405
|
-
warnings.push(`git pull failed: ${pull.message}. Run manually.`);
|
|
19406
|
-
}
|
|
19407
|
-
}
|
|
19408
|
-
if (tagExists(config2.projectRoot, version)) {
|
|
19409
|
-
throw new Error(`tag "${version}" already exists. Use a different version.`);
|
|
19410
|
-
}
|
|
19411
|
-
const latestTag = getLatestTag(config2.projectRoot);
|
|
19412
|
-
let changelogContent;
|
|
19413
|
-
if (!latestTag) {
|
|
19414
|
-
changelogContent = INITIAL_RELEASE_NOTES.replace("v0.1.0-alpha", version);
|
|
19415
|
-
} else {
|
|
19416
|
-
const commits = getCommitsSinceTag(config2.projectRoot, latestTag);
|
|
19417
|
-
changelogContent = generateChangelog(version, commits);
|
|
19418
|
-
}
|
|
19419
|
-
const changelogPath = join6(config2.projectRoot, "CHANGELOG.md");
|
|
19420
|
-
await writeFile3(changelogPath, changelogContent, "utf-8");
|
|
19421
|
-
const commitResult = stageAllAndCommit(config2.projectRoot, `release: ${version}`);
|
|
19422
|
-
const commitNote = commitResult.committed ? `Committed CHANGELOG.md.` : `CHANGELOG.md: ${commitResult.message}`;
|
|
19423
|
-
const tagResult = createTag(config2.projectRoot, version, `Release ${version}`);
|
|
19424
|
-
if (!tagResult.success) {
|
|
19425
|
-
throw new Error(tagResult.message);
|
|
19426
|
-
}
|
|
19427
|
-
const pushNotes = [];
|
|
19428
|
-
if (hasRemote(config2.projectRoot)) {
|
|
19429
|
-
const branchPush = gitPush(config2.projectRoot, branch);
|
|
19430
|
-
pushNotes.push(branchPush.success ? `Pushed '${branch}' to origin.` : `Push branch failed: ${branchPush.message}`);
|
|
19431
|
-
if (!branchPush.success) warnings.push(branchPush.message);
|
|
19432
|
-
const tagPush = gitPush(config2.projectRoot, version);
|
|
19433
|
-
pushNotes.push(tagPush.success ? `Pushed tag '${version}' to origin.` : `Push tag failed: ${tagPush.message}`);
|
|
19434
|
-
if (!tagPush.success) warnings.push(tagPush.message);
|
|
19435
|
-
} else {
|
|
19436
|
-
pushNotes.push("Push: skipped (no remote).");
|
|
19755
|
+
pushNotes.push("Push: skipped (no remote).");
|
|
19437
19756
|
}
|
|
19438
19757
|
return {
|
|
19439
19758
|
version,
|
|
@@ -19449,6 +19768,7 @@ async function createRelease(config2, branch, version, adapter2) {
|
|
|
19449
19768
|
var releaseTool = {
|
|
19450
19769
|
name: "release",
|
|
19451
19770
|
description: "Cut a versioned release \u2014 creates a git tag, generates CHANGELOG.md, and pushes to remote.",
|
|
19771
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
19452
19772
|
inputSchema: {
|
|
19453
19773
|
type: "object",
|
|
19454
19774
|
properties: {
|
|
@@ -19459,14 +19779,36 @@ var releaseTool = {
|
|
|
19459
19779
|
version: {
|
|
19460
19780
|
type: "string",
|
|
19461
19781
|
description: 'The version tag to create (e.g. "v0.1.0-alpha"). Must start with "v".'
|
|
19462
|
-
}
|
|
19463
|
-
|
|
19464
|
-
|
|
19465
|
-
|
|
19782
|
+
},
|
|
19783
|
+
observations: {
|
|
19784
|
+
type: "array",
|
|
19785
|
+
description: "Optional dogfood observations from this cycle to persist to the DB. Each entry records friction, methodology signals, or commercial insights.",
|
|
19786
|
+
items: {
|
|
19787
|
+
type: "object",
|
|
19788
|
+
properties: {
|
|
19789
|
+
content: { type: "string", description: "The observation text." },
|
|
19790
|
+
category: {
|
|
19791
|
+
type: "string",
|
|
19792
|
+
enum: ["friction", "methodology", "signal", "commercial"],
|
|
19793
|
+
description: "Observation category."
|
|
19794
|
+
},
|
|
19795
|
+
severity: {
|
|
19796
|
+
type: "string",
|
|
19797
|
+
enum: ["P0", "P1", "P2", "P3"],
|
|
19798
|
+
description: "Optional severity for friction/signal observations."
|
|
19799
|
+
}
|
|
19800
|
+
},
|
|
19801
|
+
required: ["content", "category"]
|
|
19802
|
+
}
|
|
19803
|
+
}
|
|
19804
|
+
},
|
|
19805
|
+
required: ["branch", "version"]
|
|
19806
|
+
}
|
|
19466
19807
|
};
|
|
19467
19808
|
async function handleRelease(adapter2, config2, args) {
|
|
19468
19809
|
const branch = args.branch;
|
|
19469
19810
|
const version = args.version;
|
|
19811
|
+
const rawObservations = args.observations;
|
|
19470
19812
|
if (!branch || !version) {
|
|
19471
19813
|
return errorResponse('both branch and version are required. Example: release branch="main" version="v0.1.0-alpha"');
|
|
19472
19814
|
}
|
|
@@ -19504,6 +19846,23 @@ async function handleRelease(adapter2, config2, args) {
|
|
|
19504
19846
|
}
|
|
19505
19847
|
} catch {
|
|
19506
19848
|
}
|
|
19849
|
+
if (rawObservations && rawObservations.length > 0 && adapter2.writeDogfoodEntries) {
|
|
19850
|
+
try {
|
|
19851
|
+
const cycleMatch = version.match(/^v0\.(\d+)\./);
|
|
19852
|
+
const cycleNum = cycleMatch ? parseInt(cycleMatch[1], 10) : 0;
|
|
19853
|
+
const entries = rawObservations.map((obs) => ({
|
|
19854
|
+
cycleNumber: cycleNum,
|
|
19855
|
+
category: obs.category,
|
|
19856
|
+
content: obs.content,
|
|
19857
|
+
sourceTool: "release",
|
|
19858
|
+
status: "observed"
|
|
19859
|
+
}));
|
|
19860
|
+
await adapter2.writeDogfoodEntries(entries);
|
|
19861
|
+
lines.push("", `Dogfood: ${entries.length} observation(s) saved to DB.`);
|
|
19862
|
+
} catch {
|
|
19863
|
+
lines.push("", "\u26A0\uFE0F Dogfood observations could not be saved to DB \u2014 log them manually in DOGFOOD_LOG.md.");
|
|
19864
|
+
}
|
|
19865
|
+
}
|
|
19507
19866
|
lines.push("", `Next: cycle released! Run \`plan\` to start your next planning cycle.`);
|
|
19508
19867
|
return textResponse(lines.join("\n"));
|
|
19509
19868
|
} catch (err) {
|
|
@@ -19629,7 +19988,6 @@ async function submitReview(adapter2, input) {
|
|
|
19629
19988
|
handoffRegenPrompt = await prepareHandoffRegen(task, input.comments);
|
|
19630
19989
|
}
|
|
19631
19990
|
const stageLabel = input.stage === "handoff-review" ? "Handoff Review" : "Build Acceptance";
|
|
19632
|
-
const slackWarning = void 0;
|
|
19633
19991
|
let phaseChanges = [];
|
|
19634
19992
|
if (newStatus) {
|
|
19635
19993
|
try {
|
|
@@ -19645,7 +20003,6 @@ async function submitReview(adapter2, input) {
|
|
|
19645
20003
|
newStatus,
|
|
19646
20004
|
unblockedTasks,
|
|
19647
20005
|
handoffRegenerated,
|
|
19648
|
-
slackWarning,
|
|
19649
20006
|
handoffRegenPrompt,
|
|
19650
20007
|
currentCycle: cycle,
|
|
19651
20008
|
phaseChanges
|
|
@@ -19656,6 +20013,7 @@ async function submitReview(adapter2, input) {
|
|
|
19656
20013
|
var reviewListTool = {
|
|
19657
20014
|
name: "review_list",
|
|
19658
20015
|
description: "List tasks ready for your sign-off \u2014 shows completed builds waiting for approval or feedback. Does not call the Anthropic API.",
|
|
20016
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
19659
20017
|
inputSchema: {
|
|
19660
20018
|
type: "object",
|
|
19661
20019
|
properties: {},
|
|
@@ -19665,6 +20023,7 @@ var reviewListTool = {
|
|
|
19665
20023
|
var reviewSubmitTool = {
|
|
19666
20024
|
name: "review_submit",
|
|
19667
20025
|
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.",
|
|
20026
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
19668
20027
|
inputSchema: {
|
|
19669
20028
|
type: "object",
|
|
19670
20029
|
properties: {
|
|
@@ -19694,10 +20053,6 @@ var reviewSubmitTool = {
|
|
|
19694
20053
|
type: "string",
|
|
19695
20054
|
description: "Your locally-generated BUILD HANDOFF regen output. Pass this to save a handoff that was regenerated in local mode (no API key)."
|
|
19696
20055
|
},
|
|
19697
|
-
notify: {
|
|
19698
|
-
type: "boolean",
|
|
19699
|
-
description: "Send Slack notification. Default true. Set false for batch middle reviews to avoid spam."
|
|
19700
|
-
},
|
|
19701
20056
|
auto_review: {
|
|
19702
20057
|
type: "object",
|
|
19703
20058
|
description: "Optional automated code review results to attach to this review. Run PR analysis first, then pass findings here.",
|
|
@@ -19814,7 +20169,6 @@ async function handleReviewSubmit(adapter2, config2, args) {
|
|
|
19814
20169
|
const verdict = args.verdict;
|
|
19815
20170
|
const comments = args.comments;
|
|
19816
20171
|
const reviewer = args.reviewer ?? "human";
|
|
19817
|
-
const notify = args.notify !== false;
|
|
19818
20172
|
const rawAutoReview = args.auto_review;
|
|
19819
20173
|
let autoReview;
|
|
19820
20174
|
if (rawAutoReview?.verdict && rawAutoReview?.summary && Array.isArray(rawAutoReview?.findings)) {
|
|
@@ -19857,7 +20211,7 @@ async function handleReviewSubmit(adapter2, config2, args) {
|
|
|
19857
20211
|
try {
|
|
19858
20212
|
const result = await submitReview(
|
|
19859
20213
|
adapter2,
|
|
19860
|
-
{ taskId, stage, verdict, comments, reviewer,
|
|
20214
|
+
{ taskId, stage, verdict, comments, reviewer, autoReview }
|
|
19861
20215
|
);
|
|
19862
20216
|
const statusNote = result.newStatus ? ` Task status updated to **${result.newStatus}**.` : " Task status unchanged.";
|
|
19863
20217
|
const unblockNote = result.unblockedTasks.length > 0 ? `
|
|
@@ -19890,15 +20244,42 @@ ${result.handoffRegenPrompt.userMessage}
|
|
|
19890
20244
|
mergeNote = "\n\n" + mergeLines.map((l) => `> ${l}`).join("\n");
|
|
19891
20245
|
}
|
|
19892
20246
|
}
|
|
19893
|
-
const slackNote = result.slackWarning ? `
|
|
19894
|
-
|
|
19895
|
-
${result.slackWarning}` : "";
|
|
19896
20247
|
let autoReleaseNote = "";
|
|
20248
|
+
let batchSummaryNote = "";
|
|
19897
20249
|
if (stage === "build-acceptance" && verdict === "accept" && result.newStatus === "Done" && result.currentCycle > 0) {
|
|
19898
20250
|
try {
|
|
19899
20251
|
const allTasks = await adapter2.queryBoard();
|
|
19900
20252
|
const cycleTasks = allTasks.filter((t) => t.cycle === result.currentCycle);
|
|
19901
20253
|
if (cycleTasks.length > 0 && cycleTasks.every((t) => t.status === "Done")) {
|
|
20254
|
+
try {
|
|
20255
|
+
const allReviews = await adapter2.getRecentReviews(200);
|
|
20256
|
+
const cycleReviews = allReviews.filter(
|
|
20257
|
+
(r) => r.cycle === result.currentCycle && r.stage === "build-acceptance"
|
|
20258
|
+
);
|
|
20259
|
+
const reviewsWithAutoReview = cycleReviews.filter((r) => r.autoReview);
|
|
20260
|
+
if (reviewsWithAutoReview.length > 0) {
|
|
20261
|
+
const verdictCounts = { pass: 0, warn: 0, fail: 0 };
|
|
20262
|
+
const findingsBySeverity = { error: 0, warning: 0, info: 0 };
|
|
20263
|
+
for (const r of reviewsWithAutoReview) {
|
|
20264
|
+
if (r.autoReview) {
|
|
20265
|
+
verdictCounts[r.autoReview.verdict] = (verdictCounts[r.autoReview.verdict] ?? 0) + 1;
|
|
20266
|
+
for (const f of r.autoReview.findings) {
|
|
20267
|
+
findingsBySeverity[f.severity] = (findingsBySeverity[f.severity] ?? 0) + 1;
|
|
20268
|
+
}
|
|
20269
|
+
}
|
|
20270
|
+
}
|
|
20271
|
+
const totalFindings = findingsBySeverity.error + findingsBySeverity.warning + findingsBySeverity.info;
|
|
20272
|
+
batchSummaryNote = `
|
|
20273
|
+
|
|
20274
|
+
---
|
|
20275
|
+
|
|
20276
|
+
**Cycle ${result.currentCycle} Auto-Review Summary** (${reviewsWithAutoReview.length}/${cycleReviews.length} reviews had auto-review)
|
|
20277
|
+
|
|
20278
|
+
- Verdicts: ${verdictCounts.pass} pass, ${verdictCounts.warn} warn, ${verdictCounts.fail} fail
|
|
20279
|
+
` + (totalFindings > 0 ? `- Findings: ${findingsBySeverity.error} error${findingsBySeverity.error !== 1 ? "s" : ""}, ${findingsBySeverity.warning} warning${findingsBySeverity.warning !== 1 ? "s" : ""}, ${findingsBySeverity.info} info` : "- No findings logged");
|
|
20280
|
+
}
|
|
20281
|
+
} catch {
|
|
20282
|
+
}
|
|
19902
20283
|
const version = `v0.${result.currentCycle}.0`;
|
|
19903
20284
|
const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
|
|
19904
20285
|
const releaseResult = await createRelease(config2, baseBranch, version, adapter2);
|
|
@@ -19943,7 +20324,7 @@ Next: address the feedback, then run \`build_execute ${taskId}\` to resubmit.`;
|
|
|
19943
20324
|
- **Verdict:** ${result.verdict}
|
|
19944
20325
|
- **Comments:** ${result.comments}
|
|
19945
20326
|
|
|
19946
|
-
${statusNote}${autoReviewNote}${unblockNote}${regenNote}${mergeNote}${
|
|
20327
|
+
${statusNote}${autoReviewNote}${unblockNote}${regenNote}${mergeNote}${batchSummaryNote}${autoReleaseNote}${nextStepNote}${phaseNote}`
|
|
19947
20328
|
);
|
|
19948
20329
|
} catch (err) {
|
|
19949
20330
|
return errorResponse(err instanceof Error ? err.message : String(err));
|
|
@@ -19957,6 +20338,7 @@ import path4 from "path";
|
|
|
19957
20338
|
var initTool = {
|
|
19958
20339
|
name: "init",
|
|
19959
20340
|
description: "Initialise PAPI in the current project. Generates a .mcp.json config file with pg adapter settings pointed at the hosted Supabase instance. Run once per project to get started.",
|
|
20341
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
19960
20342
|
inputSchema: {
|
|
19961
20343
|
type: "object",
|
|
19962
20344
|
properties: {
|
|
@@ -19966,148 +20348,607 @@ var initTool = {
|
|
|
19966
20348
|
},
|
|
19967
20349
|
force: {
|
|
19968
20350
|
type: "boolean",
|
|
19969
|
-
description: "Overwrite existing .mcp.json if it already exists. Default: false."
|
|
20351
|
+
description: "Overwrite existing .mcp.json if it already exists. Default: false."
|
|
20352
|
+
}
|
|
20353
|
+
},
|
|
20354
|
+
required: []
|
|
20355
|
+
}
|
|
20356
|
+
};
|
|
20357
|
+
async function ensureGitignoreEntry(projectRoot, entry) {
|
|
20358
|
+
const gitignorePath = path4.join(projectRoot, ".gitignore");
|
|
20359
|
+
let content = "";
|
|
20360
|
+
try {
|
|
20361
|
+
content = await readFile4(gitignorePath, "utf-8");
|
|
20362
|
+
} catch {
|
|
20363
|
+
}
|
|
20364
|
+
const lines = content.split("\n");
|
|
20365
|
+
if (lines.some((line) => line.trim() === entry)) {
|
|
20366
|
+
return;
|
|
20367
|
+
}
|
|
20368
|
+
const separator = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
20369
|
+
await writeFile4(gitignorePath, content + separator + entry + "\n", "utf-8");
|
|
20370
|
+
}
|
|
20371
|
+
async function handleInit(config2, args) {
|
|
20372
|
+
const projectRoot = config2.projectRoot;
|
|
20373
|
+
const mcpJsonPath = path4.join(projectRoot, ".mcp.json");
|
|
20374
|
+
const force = args.force === true;
|
|
20375
|
+
const projectName = args.project_name?.trim() || path4.basename(projectRoot);
|
|
20376
|
+
const usingCwdDefault = !process.env.PAPI_PROJECT_DIR && process.argv.indexOf("--project") === -1;
|
|
20377
|
+
let existingConfig = null;
|
|
20378
|
+
try {
|
|
20379
|
+
await access3(mcpJsonPath);
|
|
20380
|
+
existingConfig = await readFile4(mcpJsonPath, "utf-8");
|
|
20381
|
+
} catch {
|
|
20382
|
+
}
|
|
20383
|
+
if (existingConfig && !force) {
|
|
20384
|
+
try {
|
|
20385
|
+
const parsed = JSON.parse(existingConfig);
|
|
20386
|
+
if (parsed.mcpServers?.papi) {
|
|
20387
|
+
return errorResponse(
|
|
20388
|
+
`.mcp.json already exists with a PAPI server configured.
|
|
20389
|
+
Use \`init\` with \`force: true\` to overwrite, or edit the file manually.
|
|
20390
|
+
Path: ${mcpJsonPath}`
|
|
20391
|
+
);
|
|
20392
|
+
}
|
|
20393
|
+
} catch {
|
|
20394
|
+
if (!force) {
|
|
20395
|
+
return errorResponse(
|
|
20396
|
+
`.mcp.json exists but contains invalid JSON.
|
|
20397
|
+
Use \`init\` with \`force: true\` to overwrite.
|
|
20398
|
+
Path: ${mcpJsonPath}`
|
|
20399
|
+
);
|
|
20400
|
+
}
|
|
20401
|
+
}
|
|
20402
|
+
}
|
|
20403
|
+
const existingApiKey = process.env.PAPI_DATA_API_KEY;
|
|
20404
|
+
const existingProjectId = process.env.PAPI_PROJECT_ID;
|
|
20405
|
+
const isProxyUser = Boolean(existingApiKey) || config2.adapterType === "proxy";
|
|
20406
|
+
const isDatabaseUser = Boolean(process.env.DATABASE_URL) || config2.adapterType === "pg";
|
|
20407
|
+
if (isProxyUser && existingApiKey && existingProjectId) {
|
|
20408
|
+
const mcpConfig = {
|
|
20409
|
+
mcpServers: {
|
|
20410
|
+
papi: {
|
|
20411
|
+
command: "npx",
|
|
20412
|
+
args: ["-y", "@papi-ai/server"],
|
|
20413
|
+
env: {
|
|
20414
|
+
PAPI_PROJECT_ID: existingProjectId,
|
|
20415
|
+
PAPI_DATA_API_KEY: existingApiKey
|
|
20416
|
+
}
|
|
20417
|
+
}
|
|
20418
|
+
}
|
|
20419
|
+
};
|
|
20420
|
+
await writeFile4(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf-8");
|
|
20421
|
+
await ensureGitignoreEntry(projectRoot, ".mcp.json");
|
|
20422
|
+
return textResponse(
|
|
20423
|
+
`# PAPI Initialised \u2014 ${projectName}
|
|
20424
|
+
|
|
20425
|
+
**Config:** \`${mcpJsonPath}\`
|
|
20426
|
+
|
|
20427
|
+
Your existing API key and project ID have been saved to .mcp.json.
|
|
20428
|
+
|
|
20429
|
+
## Next Steps
|
|
20430
|
+
|
|
20431
|
+
1. **Restart your MCP client** to pick up the new config.
|
|
20432
|
+
2. **Run \`setup\`** \u2014 this scaffolds your project with a Product Brief and CLAUDE.md.
|
|
20433
|
+
`
|
|
20434
|
+
);
|
|
20435
|
+
}
|
|
20436
|
+
if (isDatabaseUser) {
|
|
20437
|
+
const projectId = randomUUID14();
|
|
20438
|
+
const mcpConfig = {
|
|
20439
|
+
mcpServers: {
|
|
20440
|
+
papi: {
|
|
20441
|
+
command: "npx",
|
|
20442
|
+
args: ["-y", "@papi-ai/server"],
|
|
20443
|
+
env: {
|
|
20444
|
+
PAPI_PROJECT_DIR: projectRoot,
|
|
20445
|
+
PAPI_ADAPTER: "pg",
|
|
20446
|
+
DATABASE_URL: process.env.DATABASE_URL || "<YOUR_DATABASE_URL>",
|
|
20447
|
+
PAPI_PROJECT_ID: projectId
|
|
20448
|
+
}
|
|
20449
|
+
}
|
|
20450
|
+
}
|
|
20451
|
+
};
|
|
20452
|
+
await writeFile4(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf-8");
|
|
20453
|
+
await ensureGitignoreEntry(projectRoot, ".mcp.json");
|
|
20454
|
+
const output2 = [
|
|
20455
|
+
`# PAPI Initialised \u2014 ${projectName}`,
|
|
20456
|
+
"",
|
|
20457
|
+
`**Project ID:** \`${projectId}\``,
|
|
20458
|
+
`**Config:** \`${mcpJsonPath}\``,
|
|
20459
|
+
"",
|
|
20460
|
+
"## Next Steps",
|
|
20461
|
+
"",
|
|
20462
|
+
...process.env.DATABASE_URL ? ["1. **Restart your MCP client** to pick up the new config."] : ["1. **Set your DATABASE_URL** \u2014 replace `<YOUR_DATABASE_URL>` in `.mcp.json` with your Supabase connection string."],
|
|
20463
|
+
"2. **Run `setup`** \u2014 this scaffolds your project with a Product Brief, Active Decisions, and CLAUDE.md."
|
|
20464
|
+
].join("\n");
|
|
20465
|
+
return textResponse(output2);
|
|
20466
|
+
}
|
|
20467
|
+
const output = [
|
|
20468
|
+
`# PAPI \u2014 Account Required`,
|
|
20469
|
+
"",
|
|
20470
|
+
`PAPI needs an account to store your project data.`,
|
|
20471
|
+
"",
|
|
20472
|
+
"## Get Started in 3 Steps",
|
|
20473
|
+
"",
|
|
20474
|
+
"1. **Sign up** at https://getpapi.ai/login",
|
|
20475
|
+
"2. **Complete the onboarding wizard** \u2014 it generates your `.mcp.json` config with your API key and project ID",
|
|
20476
|
+
"3. **Download the config**, place it in your project root, and restart your MCP client",
|
|
20477
|
+
"",
|
|
20478
|
+
"The onboarding wizard generates everything you need \u2014 no manual configuration required.",
|
|
20479
|
+
"",
|
|
20480
|
+
`> Already have an account? Make sure both \`PAPI_PROJECT_ID\` and \`PAPI_DATA_API_KEY\` are set in your .mcp.json.`
|
|
20481
|
+
].join("\n");
|
|
20482
|
+
return textResponse(output);
|
|
20483
|
+
}
|
|
20484
|
+
|
|
20485
|
+
// src/services/health.ts
|
|
20486
|
+
function computeHealthScore(cycleNumber, snapshots, activeTasks, decisionUsage) {
|
|
20487
|
+
if (cycleNumber < 3) return null;
|
|
20488
|
+
const scores = [];
|
|
20489
|
+
const recentSnaps = snapshots.slice(-3);
|
|
20490
|
+
const baselineSnaps = snapshots.slice(-10);
|
|
20491
|
+
if (recentSnaps.length > 0 && baselineSnaps.length > 0) {
|
|
20492
|
+
const avg = (snaps) => snaps.reduce((s, sn) => s + (sn.velocity[0]?.effortPoints ?? 0), 0) / snaps.length;
|
|
20493
|
+
const recentAvg = avg(recentSnaps);
|
|
20494
|
+
const baselineAvg = avg(baselineSnaps);
|
|
20495
|
+
const velocityScore = baselineAvg > 0 ? Math.min(100, Math.round(recentAvg / baselineAvg * 100)) : 50;
|
|
20496
|
+
scores.push({ name: "Velocity", score: velocityScore, weight: 0.25 });
|
|
20497
|
+
} else {
|
|
20498
|
+
scores.push({ name: "Velocity", score: 50, weight: 0.25 });
|
|
20499
|
+
}
|
|
20500
|
+
if (recentSnaps.length > 0) {
|
|
20501
|
+
const avgMatchRate = recentSnaps.reduce((s, sn) => s + (sn.accuracy[0]?.matchRate ?? 0), 0) / recentSnaps.length;
|
|
20502
|
+
scores.push({ name: "Estimation accuracy", score: Math.round(avgMatchRate), weight: 0.25 });
|
|
20503
|
+
} else {
|
|
20504
|
+
scores.push({ name: "Estimation accuracy", score: 50, weight: 0.25 });
|
|
20505
|
+
}
|
|
20506
|
+
const inReviewCount = activeTasks.filter((t) => t.status === "In Review").length;
|
|
20507
|
+
const reviewScore = inReviewCount === 0 ? 100 : inReviewCount <= 2 ? 60 : 20;
|
|
20508
|
+
scores.push({ name: "Review throughput", score: reviewScore, weight: 0.2 });
|
|
20509
|
+
const backlogTasks = activeTasks.filter((t) => t.status === "Backlog");
|
|
20510
|
+
if (backlogTasks.length > 0) {
|
|
20511
|
+
const criticalCount = backlogTasks.filter(
|
|
20512
|
+
(t) => t.priority === "P0 Critical" || t.priority === "P1 High"
|
|
20513
|
+
).length;
|
|
20514
|
+
const criticalRatio = criticalCount / backlogTasks.length;
|
|
20515
|
+
const backlogScore = criticalRatio > 0.5 ? 40 : criticalRatio > 0.3 ? 70 : 90;
|
|
20516
|
+
scores.push({ name: "Backlog health", score: backlogScore, weight: 0.15 });
|
|
20517
|
+
} else {
|
|
20518
|
+
scores.push({ name: "Backlog health", score: 80, weight: 0.15 });
|
|
20519
|
+
}
|
|
20520
|
+
if (decisionUsage.length > 0) {
|
|
20521
|
+
const staleCount = decisionUsage.filter((u) => u.cyclesSinceLastReference >= 10).length;
|
|
20522
|
+
const freshRatio = (decisionUsage.length - staleCount) / decisionUsage.length;
|
|
20523
|
+
scores.push({ name: "AD freshness", score: Math.round(freshRatio * 100), weight: 0.15 });
|
|
20524
|
+
} else {
|
|
20525
|
+
scores.push({ name: "AD freshness", score: 70, weight: 0.15 });
|
|
20526
|
+
}
|
|
20527
|
+
const totalScore = Math.round(scores.reduce((sum, s) => sum + s.score * s.weight, 0));
|
|
20528
|
+
const status = totalScore >= 70 ? "GREEN" : totalScore >= 50 ? "AMBER" : "RED";
|
|
20529
|
+
const worst = scores.reduce((min, s) => s.score < min.score ? s : min, scores[0]);
|
|
20530
|
+
const reason = status === "GREEN" ? "All components healthy" : `${worst.name} below target (${worst.score}/100)`;
|
|
20531
|
+
return { score: totalScore, status, reason };
|
|
20532
|
+
}
|
|
20533
|
+
function countByStatus(tasks) {
|
|
20534
|
+
const counts = /* @__PURE__ */ new Map();
|
|
20535
|
+
for (const task of tasks) {
|
|
20536
|
+
counts.set(task.status, (counts.get(task.status) ?? 0) + 1);
|
|
20537
|
+
}
|
|
20538
|
+
return counts;
|
|
20539
|
+
}
|
|
20540
|
+
async function getHealthSummary(adapter2) {
|
|
20541
|
+
const health = await adapter2.getCycleHealth();
|
|
20542
|
+
const activeTasks = await adapter2.queryBoard({
|
|
20543
|
+
status: ["Backlog", "In Cycle", "Ready", "In Progress", "In Review", "Blocked"]
|
|
20544
|
+
});
|
|
20545
|
+
const logEntries = await adapter2.getCycleLog(3);
|
|
20546
|
+
const cycleNumber = health.totalCycles;
|
|
20547
|
+
const cyclesSinceReview = health.cyclesSinceLastStrategyReview;
|
|
20548
|
+
const reviewDue = health.strategyReviewDue;
|
|
20549
|
+
const reviewGateBlocking = cyclesSinceReview >= 5;
|
|
20550
|
+
const reviewWarning = reviewGateBlocking ? `\u26A0\uFE0F GATE \u2014 ${cyclesSinceReview} cycles since last Strategy Review. \`plan\` is blocked until \`strategy_review\` runs (or \`force: true\`).` : `\u2713 On track \u2014 ${cyclesSinceReview} cycle(s) since last review. Next due: ${reviewDue}`;
|
|
20551
|
+
const deferredCount = activeTasks.filter((t) => t.status === "Deferred").length;
|
|
20552
|
+
const nonDeferredTasks = activeTasks.filter((t) => t.status !== "Deferred");
|
|
20553
|
+
const statusCounts = countByStatus(nonDeferredTasks);
|
|
20554
|
+
let boardSummary;
|
|
20555
|
+
if (nonDeferredTasks.length === 0 && deferredCount === 0) {
|
|
20556
|
+
boardSummary = "0 tasks \u2014 board may need reloading";
|
|
20557
|
+
} else {
|
|
20558
|
+
const parts = [];
|
|
20559
|
+
for (const [status, count] of statusCounts) {
|
|
20560
|
+
parts.push(`${count} ${status}`);
|
|
20561
|
+
}
|
|
20562
|
+
boardSummary = `${nonDeferredTasks.length} active tasks \u2014 ${parts.join(", ")}`;
|
|
20563
|
+
if (deferredCount > 0) {
|
|
20564
|
+
boardSummary += ` + ${deferredCount} deferred`;
|
|
20565
|
+
}
|
|
20566
|
+
}
|
|
20567
|
+
const inProgressTasks = activeTasks.filter((t) => t.status === "In Progress");
|
|
20568
|
+
const staleTasks = inProgressTasks.length > 0 ? `${inProgressTasks.length} task(s) In Progress: ${inProgressTasks.map((t) => t.id).join(", ")}` : "No tasks currently In Progress";
|
|
20569
|
+
const inReviewTasks = activeTasks.filter((t) => t.status === "In Review");
|
|
20570
|
+
const inReviewSummary = inReviewTasks.length > 0 ? `${inReviewTasks.length} task(s) ready for sign-off: ${inReviewTasks.map((t) => t.id).join(", ")}` : "No tasks waiting for sign-off";
|
|
20571
|
+
let carryForward = "None found";
|
|
20572
|
+
if (logEntries.length > 0) {
|
|
20573
|
+
const latest = logEntries[0];
|
|
20574
|
+
if (latest.carryForward) {
|
|
20575
|
+
carryForward = latest.carryForward;
|
|
20576
|
+
} else {
|
|
20577
|
+
carryForward = `No carry-forward in Cycle ${latest.cycleNumber}`;
|
|
20578
|
+
}
|
|
20579
|
+
}
|
|
20580
|
+
let recommendedMode;
|
|
20581
|
+
const reasons = [];
|
|
20582
|
+
if (reviewGateBlocking) {
|
|
20583
|
+
reasons.push(`Strategy Review overdue (${cyclesSinceReview} cycles)`);
|
|
20584
|
+
}
|
|
20585
|
+
if (activeTasks.length === 0) {
|
|
20586
|
+
reasons.push("Board is empty \u2014 needs task reload/triage");
|
|
20587
|
+
}
|
|
20588
|
+
const unbuiltCycleTasks = activeTasks.filter(
|
|
20589
|
+
(t) => t.cycle === cycleNumber && (t.status === "In Cycle" || t.status === "Ready")
|
|
20590
|
+
);
|
|
20591
|
+
const inProgressCycleTasks = activeTasks.filter(
|
|
20592
|
+
(t) => t.cycle === cycleNumber && t.status === "In Progress"
|
|
20593
|
+
);
|
|
20594
|
+
const inReviewCycleTasks = activeTasks.filter(
|
|
20595
|
+
(t) => t.cycle === cycleNumber && t.status === "In Review"
|
|
20596
|
+
);
|
|
20597
|
+
if (reasons.length > 0) {
|
|
20598
|
+
recommendedMode = `**Full** \u2014 ${reasons.join("; ")}`;
|
|
20599
|
+
} else if (unbuiltCycleTasks.length > 0) {
|
|
20600
|
+
recommendedMode = `**Build** \u2014 ${unbuiltCycleTasks.length} cycle task(s) not yet started`;
|
|
20601
|
+
} else if (inProgressCycleTasks.length > 0) {
|
|
20602
|
+
recommendedMode = `**Build** \u2014 ${inProgressCycleTasks.length} task(s) in progress`;
|
|
20603
|
+
} else if (inReviewCycleTasks.length > 0) {
|
|
20604
|
+
recommendedMode = `**Review** \u2014 ${inReviewCycleTasks.length} task(s) awaiting review`;
|
|
20605
|
+
} else {
|
|
20606
|
+
recommendedMode = `**Full** \u2014 ready for next cycle`;
|
|
20607
|
+
}
|
|
20608
|
+
let metricsSection;
|
|
20609
|
+
let derivedMetricsSection = "";
|
|
20610
|
+
let snapshots = [];
|
|
20611
|
+
try {
|
|
20612
|
+
try {
|
|
20613
|
+
const reports = await adapter2.getRecentBuildReports(50);
|
|
20614
|
+
snapshots = computeSnapshotsFromBuildReports(reports);
|
|
20615
|
+
} catch {
|
|
20616
|
+
}
|
|
20617
|
+
metricsSection = formatCycleMetrics(snapshots);
|
|
20618
|
+
derivedMetricsSection = formatDerivedMetrics(snapshots, activeTasks);
|
|
20619
|
+
} catch (_err) {
|
|
20620
|
+
metricsSection = "Could not read methodology metrics.";
|
|
20621
|
+
}
|
|
20622
|
+
try {
|
|
20623
|
+
const recentReports = await adapter2.getRecentBuildReports(50);
|
|
20624
|
+
if (recentReports.length > 0) {
|
|
20625
|
+
const taskCounts = /* @__PURE__ */ new Map();
|
|
20626
|
+
for (const r of recentReports) {
|
|
20627
|
+
taskCounts.set(r.taskId, (taskCounts.get(r.taskId) ?? 0) + 1);
|
|
20628
|
+
}
|
|
20629
|
+
const iterCounts = [...taskCounts.values()];
|
|
20630
|
+
const avgIter = iterCounts.reduce((s, c) => s + c, 0) / iterCounts.length;
|
|
20631
|
+
const multiIterTasks = iterCounts.filter((c) => c > 1).length;
|
|
20632
|
+
if (avgIter > 1 || multiIterTasks > 0) {
|
|
20633
|
+
derivedMetricsSection += `
|
|
20634
|
+
|
|
20635
|
+
**Rework**
|
|
20636
|
+
- Average iterations: ${avgIter.toFixed(1)} (${multiIterTasks} task${multiIterTasks !== 1 ? "s" : ""} with pushbacks)`;
|
|
20637
|
+
}
|
|
20638
|
+
}
|
|
20639
|
+
} catch {
|
|
20640
|
+
}
|
|
20641
|
+
const costSection = "Disabled \u2014 local MCP, no API costs.";
|
|
20642
|
+
let decisionUsageSection = "";
|
|
20643
|
+
let decisionUsageEntries = [];
|
|
20644
|
+
try {
|
|
20645
|
+
const usage = await adapter2.getDecisionUsage(cycleNumber);
|
|
20646
|
+
decisionUsageEntries = usage;
|
|
20647
|
+
if (usage.length > 0) {
|
|
20648
|
+
const stale = usage.filter((u) => u.cyclesSinceLastReference >= 5);
|
|
20649
|
+
if (stale.length > 0) {
|
|
20650
|
+
const lines = stale.map(
|
|
20651
|
+
(u) => `- ${u.decisionId}: last referenced Cycle ${u.lastReferencedCycle} (${u.cyclesSinceLastReference} cycles ago)`
|
|
20652
|
+
);
|
|
20653
|
+
decisionUsageSection = `**Stale ADs (5+ cycles unreferenced):**
|
|
20654
|
+
${lines.join("\n")}`;
|
|
20655
|
+
} else {
|
|
20656
|
+
decisionUsageSection = `All ${usage.length} tracked ADs referenced within last 5 cycles.`;
|
|
20657
|
+
}
|
|
20658
|
+
}
|
|
20659
|
+
} catch {
|
|
20660
|
+
}
|
|
20661
|
+
let decisionLifecycleSection = "";
|
|
20662
|
+
try {
|
|
20663
|
+
const decisions = await adapter2.getActiveDecisions();
|
|
20664
|
+
const lifecycleSummary = formatDecisionLifecycleSummary(decisions);
|
|
20665
|
+
if (lifecycleSummary) {
|
|
20666
|
+
decisionLifecycleSection = `**Lifecycle:** ${lifecycleSummary}`;
|
|
20667
|
+
}
|
|
20668
|
+
} catch {
|
|
20669
|
+
}
|
|
20670
|
+
const decisionScoresSection = "";
|
|
20671
|
+
let contextUtilisationSection = "";
|
|
20672
|
+
try {
|
|
20673
|
+
const utilData = await adapter2.getContextUtilisation?.();
|
|
20674
|
+
if (utilData && utilData.length > 0) {
|
|
20675
|
+
const lines = utilData.filter((u) => u.cycleNumber === cycleNumber).map((u) => `- ${u.tool}: ${(u.avgUtilisation * 100).toFixed(0)}% utilisation (${(u.avgContextBytes / 1024).toFixed(1)}KB avg context)`);
|
|
20676
|
+
if (lines.length > 0) {
|
|
20677
|
+
contextUtilisationSection = `**Current cycle:**
|
|
20678
|
+
${lines.join("\n")}`;
|
|
20679
|
+
}
|
|
20680
|
+
}
|
|
20681
|
+
} catch {
|
|
20682
|
+
}
|
|
20683
|
+
let northStarSection = "";
|
|
20684
|
+
try {
|
|
20685
|
+
const staleness = await adapter2.getNorthStarStaleness?.();
|
|
20686
|
+
if (staleness) {
|
|
20687
|
+
const cycleGap = cycleNumber - staleness.setCycle;
|
|
20688
|
+
const daysSinceSet = Math.floor((Date.now() - new Date(staleness.setAt).getTime()) / (1e3 * 60 * 60 * 24));
|
|
20689
|
+
northStarSection = `\u2713 North Star set Cycle ${staleness.setCycle} (${cycleGap} cycles, ${daysSinceSet} days ago)`;
|
|
20690
|
+
} else {
|
|
20691
|
+
const setAtCycle = await adapter2.getNorthStarSetCycle?.();
|
|
20692
|
+
if (setAtCycle != null) {
|
|
20693
|
+
northStarSection = `\u2713 North Star set Cycle ${setAtCycle}`;
|
|
20694
|
+
} else if (adapter2.getCurrentNorthStar) {
|
|
20695
|
+
const ns = await adapter2.getCurrentNorthStar();
|
|
20696
|
+
northStarSection = ns ? "" : "\u26A0\uFE0F No North Star set \u2014 consider defining one";
|
|
20697
|
+
}
|
|
20698
|
+
}
|
|
20699
|
+
} catch {
|
|
20700
|
+
}
|
|
20701
|
+
const healthResult = computeHealthScore(cycleNumber, snapshots, activeTasks, decisionUsageEntries);
|
|
20702
|
+
return {
|
|
20703
|
+
cycleNumber,
|
|
20704
|
+
latestCycleStatus: health.latestCycleStatus,
|
|
20705
|
+
connectionStatus: getConnectionStatus(),
|
|
20706
|
+
reviewWarning,
|
|
20707
|
+
boardSummary,
|
|
20708
|
+
staleTasks,
|
|
20709
|
+
inReviewSummary,
|
|
20710
|
+
carryForward,
|
|
20711
|
+
recommendedMode,
|
|
20712
|
+
metricsSection,
|
|
20713
|
+
derivedMetricsSection,
|
|
20714
|
+
costSection,
|
|
20715
|
+
decisionUsageSection,
|
|
20716
|
+
decisionLifecycleSection,
|
|
20717
|
+
decisionScoresSection,
|
|
20718
|
+
contextUtilisationSection,
|
|
20719
|
+
northStarSection,
|
|
20720
|
+
healthScore: healthResult?.score ?? null,
|
|
20721
|
+
healthStatus: healthResult?.status ?? null,
|
|
20722
|
+
healthReason: healthResult?.reason ?? null
|
|
20723
|
+
};
|
|
20724
|
+
}
|
|
20725
|
+
|
|
20726
|
+
// src/tools/orient.ts
|
|
20727
|
+
init_git();
|
|
20728
|
+
|
|
20729
|
+
// src/tools/doc-registry.ts
|
|
20730
|
+
import { readdirSync as readdirSync4, existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
|
|
20731
|
+
import { join as join8, relative } from "path";
|
|
20732
|
+
import { homedir as homedir2 } from "os";
|
|
20733
|
+
var docRegisterTool = {
|
|
20734
|
+
name: "doc_register",
|
|
20735
|
+
description: "Register 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.",
|
|
20736
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
20737
|
+
inputSchema: {
|
|
20738
|
+
type: "object",
|
|
20739
|
+
properties: {
|
|
20740
|
+
path: { type: "string", description: 'Relative path from project root (e.g. "docs/research/funding-landscape.md").' },
|
|
20741
|
+
title: { type: "string", description: "Document title." },
|
|
20742
|
+
type: { type: "string", enum: ["research", "audit", "spec", "guide", "architecture", "positioning", "framework", "reference"], description: "Document type." },
|
|
20743
|
+
status: { type: "string", enum: ["active", "draft", "superseded", "actioned", "legacy", "archived"], description: 'Document status. Defaults to "active".' },
|
|
20744
|
+
summary: { type: "string", description: 'Structured 2-4 sentence summary. Format: "Conclusions: ... Open questions: ... Unactioned: ..."' },
|
|
20745
|
+
tags: { type: "array", items: { type: "string" }, description: "Tags from project vocabulary." },
|
|
20746
|
+
cycle: { type: "number", description: "Current cycle number." },
|
|
20747
|
+
actions: {
|
|
20748
|
+
type: "array",
|
|
20749
|
+
items: {
|
|
20750
|
+
type: "object",
|
|
20751
|
+
properties: {
|
|
20752
|
+
description: { type: "string" },
|
|
20753
|
+
status: { type: "string", enum: ["pending", "resolved"] },
|
|
20754
|
+
linkedTaskId: { type: "string" }
|
|
20755
|
+
},
|
|
20756
|
+
required: ["description", "status"]
|
|
20757
|
+
},
|
|
20758
|
+
description: "Actionable findings from the document."
|
|
20759
|
+
},
|
|
20760
|
+
superseded_by_path: { type: "string", description: "Path of the doc that supersedes this one (sets status to superseded)." }
|
|
20761
|
+
},
|
|
20762
|
+
required: ["path", "title", "type", "summary", "cycle"]
|
|
20763
|
+
}
|
|
20764
|
+
};
|
|
20765
|
+
var docSearchTool = {
|
|
20766
|
+
name: "doc_search",
|
|
20767
|
+
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.",
|
|
20768
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
20769
|
+
inputSchema: {
|
|
20770
|
+
type: "object",
|
|
20771
|
+
properties: {
|
|
20772
|
+
type: { type: "string", description: 'Filter by doc type (e.g. "research", "architecture").' },
|
|
20773
|
+
status: { type: "string", description: 'Filter by status. Defaults to "active".' },
|
|
20774
|
+
tags: { type: "array", items: { type: "string" }, description: "Filter by tags (OR match)." },
|
|
20775
|
+
keyword: { type: "string", description: "Search title and summary text." },
|
|
20776
|
+
has_pending_actions: { type: "boolean", description: "Only docs with unresolved action items." },
|
|
20777
|
+
since_cycle: { type: "number", description: "Docs updated since this cycle." },
|
|
20778
|
+
limit: { type: "number", description: "Max results (default: 10)." }
|
|
20779
|
+
},
|
|
20780
|
+
required: []
|
|
20781
|
+
}
|
|
20782
|
+
};
|
|
20783
|
+
var docScanTool = {
|
|
20784
|
+
name: "doc_scan",
|
|
20785
|
+
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.",
|
|
20786
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
20787
|
+
inputSchema: {
|
|
20788
|
+
type: "object",
|
|
20789
|
+
properties: {
|
|
20790
|
+
include_plans: {
|
|
20791
|
+
type: "boolean",
|
|
20792
|
+
description: "Also scan ~/.claude/plans/ for plan files (default: false)."
|
|
19970
20793
|
}
|
|
19971
20794
|
},
|
|
19972
20795
|
required: []
|
|
19973
20796
|
}
|
|
19974
20797
|
};
|
|
19975
|
-
async function
|
|
19976
|
-
|
|
19977
|
-
|
|
20798
|
+
async function handleDocRegister(adapter2, args) {
|
|
20799
|
+
if (!adapter2.registerDoc) {
|
|
20800
|
+
return errorResponse("Doc registry not available \u2014 requires pg adapter.");
|
|
20801
|
+
}
|
|
20802
|
+
const path5 = args.path;
|
|
20803
|
+
const title = args.title;
|
|
20804
|
+
const type = args.type;
|
|
20805
|
+
const status = args.status ?? "active";
|
|
20806
|
+
const summary = args.summary;
|
|
20807
|
+
const tags = args.tags ?? [];
|
|
20808
|
+
const cycle = args.cycle;
|
|
20809
|
+
const actions = args.actions;
|
|
20810
|
+
const supersededByPath = args.superseded_by_path;
|
|
20811
|
+
if (!path5 || !title || !type || !summary || !cycle) {
|
|
20812
|
+
return errorResponse("Required fields: path, title, type, summary, cycle.");
|
|
20813
|
+
}
|
|
20814
|
+
let supersededBy;
|
|
20815
|
+
if (supersededByPath) {
|
|
20816
|
+
const existing = await adapter2.getDoc?.(supersededByPath);
|
|
20817
|
+
if (existing) {
|
|
20818
|
+
supersededBy = existing.id;
|
|
20819
|
+
await adapter2.updateDocStatus?.(existing.id, "superseded", void 0);
|
|
20820
|
+
}
|
|
20821
|
+
}
|
|
20822
|
+
const entry = await adapter2.registerDoc({
|
|
20823
|
+
title,
|
|
20824
|
+
type,
|
|
20825
|
+
path: path5,
|
|
20826
|
+
status: supersededByPath ? "superseded" : status,
|
|
20827
|
+
summary,
|
|
20828
|
+
tags,
|
|
20829
|
+
cycleCreated: cycle,
|
|
20830
|
+
cycleUpdated: cycle,
|
|
20831
|
+
supersededBy,
|
|
20832
|
+
actions
|
|
20833
|
+
});
|
|
20834
|
+
return textResponse(
|
|
20835
|
+
`**Registered:** ${entry.title}
|
|
20836
|
+
- **Path:** ${entry.path}
|
|
20837
|
+
- **Type:** ${entry.type} | **Status:** ${entry.status}
|
|
20838
|
+
- **Tags:** ${entry.tags.length > 0 ? entry.tags.join(", ") : "none"}
|
|
20839
|
+
- **Actions:** ${actions?.length ?? 0} items
|
|
20840
|
+
- **ID:** ${entry.id}`
|
|
20841
|
+
);
|
|
20842
|
+
}
|
|
20843
|
+
async function handleDocSearch(adapter2, args) {
|
|
20844
|
+
if (!adapter2.searchDocs) {
|
|
20845
|
+
return errorResponse("Doc registry not available \u2014 requires pg adapter.");
|
|
20846
|
+
}
|
|
20847
|
+
const input = {
|
|
20848
|
+
type: args.type,
|
|
20849
|
+
status: args.status,
|
|
20850
|
+
tags: args.tags,
|
|
20851
|
+
keyword: args.keyword,
|
|
20852
|
+
hasPendingActions: args.has_pending_actions,
|
|
20853
|
+
sinceCycle: args.since_cycle,
|
|
20854
|
+
limit: args.limit
|
|
20855
|
+
};
|
|
20856
|
+
const docs = await adapter2.searchDocs(input);
|
|
20857
|
+
if (docs.length === 0) {
|
|
20858
|
+
return textResponse("No documents found matching the search criteria.");
|
|
20859
|
+
}
|
|
20860
|
+
const lines = docs.map((d) => {
|
|
20861
|
+
const actionCount = d.actions?.filter((a) => a.status === "pending").length ?? 0;
|
|
20862
|
+
const actionNote = actionCount > 0 ? ` | ${actionCount} pending action(s)` : "";
|
|
20863
|
+
return `### ${d.title}
|
|
20864
|
+
**Type:** ${d.type} | **Status:** ${d.status} | **Cycle:** ${d.cycleCreated}${d.cycleUpdated ? `\u2192${d.cycleUpdated}` : ""}${actionNote}
|
|
20865
|
+
**Path:** ${d.path}
|
|
20866
|
+
**Tags:** ${d.tags.length > 0 ? d.tags.join(", ") : "none"}
|
|
20867
|
+
${d.summary}
|
|
20868
|
+
`;
|
|
20869
|
+
});
|
|
20870
|
+
return textResponse(`**${docs.length} document(s) found:**
|
|
20871
|
+
|
|
20872
|
+
${lines.join("\n---\n\n")}`);
|
|
20873
|
+
}
|
|
20874
|
+
function scanMdFiles(dir, rootDir) {
|
|
20875
|
+
if (!existsSync5(dir)) return [];
|
|
20876
|
+
const files = [];
|
|
19978
20877
|
try {
|
|
19979
|
-
|
|
20878
|
+
const entries = readdirSync4(dir, { withFileTypes: true });
|
|
20879
|
+
for (const entry of entries) {
|
|
20880
|
+
const full = join8(dir, entry.name);
|
|
20881
|
+
if (entry.isDirectory()) {
|
|
20882
|
+
files.push(...scanMdFiles(full, rootDir));
|
|
20883
|
+
} else if (entry.name.endsWith(".md")) {
|
|
20884
|
+
files.push(relative(rootDir, full));
|
|
20885
|
+
}
|
|
20886
|
+
}
|
|
19980
20887
|
} catch {
|
|
19981
20888
|
}
|
|
19982
|
-
|
|
19983
|
-
if (lines.some((line) => line.trim() === entry)) {
|
|
19984
|
-
return;
|
|
19985
|
-
}
|
|
19986
|
-
const separator = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
19987
|
-
await writeFile4(gitignorePath, content + separator + entry + "\n", "utf-8");
|
|
20889
|
+
return files;
|
|
19988
20890
|
}
|
|
19989
|
-
|
|
19990
|
-
const projectRoot = config2.projectRoot;
|
|
19991
|
-
const mcpJsonPath = path4.join(projectRoot, ".mcp.json");
|
|
19992
|
-
const force = args.force === true;
|
|
19993
|
-
const projectName = args.project_name?.trim() || path4.basename(projectRoot);
|
|
19994
|
-
const usingCwdDefault = !process.env.PAPI_PROJECT_DIR && process.argv.indexOf("--project") === -1;
|
|
19995
|
-
let existingConfig = null;
|
|
20891
|
+
function extractTitle(filePath) {
|
|
19996
20892
|
try {
|
|
19997
|
-
|
|
19998
|
-
|
|
20893
|
+
const content = readFileSync2(filePath, "utf-8").slice(0, 1e3);
|
|
20894
|
+
const fmMatch = content.match(/^---[\s\S]*?title:\s*(.+?)$/m);
|
|
20895
|
+
if (fmMatch) return fmMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
20896
|
+
const headingMatch = content.match(/^#+\s+(.+)$/m);
|
|
20897
|
+
if (headingMatch) return headingMatch[1].trim();
|
|
19999
20898
|
} catch {
|
|
20000
20899
|
}
|
|
20001
|
-
|
|
20002
|
-
|
|
20003
|
-
|
|
20004
|
-
|
|
20005
|
-
|
|
20006
|
-
|
|
20007
|
-
|
|
20008
|
-
|
|
20009
|
-
|
|
20010
|
-
|
|
20011
|
-
|
|
20012
|
-
|
|
20013
|
-
|
|
20014
|
-
|
|
20015
|
-
|
|
20016
|
-
|
|
20017
|
-
|
|
20018
|
-
}
|
|
20900
|
+
return void 0;
|
|
20901
|
+
}
|
|
20902
|
+
async function handleDocScan(adapter2, config2, args) {
|
|
20903
|
+
if (!adapter2.searchDocs) {
|
|
20904
|
+
return errorResponse("Doc registry not available on this adapter.");
|
|
20905
|
+
}
|
|
20906
|
+
const includePlans = args.include_plans ?? false;
|
|
20907
|
+
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
20908
|
+
const registeredPaths = new Set(registered.map((d) => d.path));
|
|
20909
|
+
const docsDir = join8(config2.projectRoot, "docs");
|
|
20910
|
+
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
20911
|
+
const unregisteredDocs = docsFiles.filter((f) => !registeredPaths.has(f));
|
|
20912
|
+
let unregisteredPlans = [];
|
|
20913
|
+
if (includePlans) {
|
|
20914
|
+
const plansDir = join8(homedir2(), ".claude", "plans");
|
|
20915
|
+
if (existsSync5(plansDir)) {
|
|
20916
|
+
const planFiles = scanMdFiles(plansDir, plansDir);
|
|
20917
|
+
unregisteredPlans = planFiles.map((f) => `plans/${f}`).filter((f) => !registeredPaths.has(f)).map((f) => ({
|
|
20918
|
+
path: f,
|
|
20919
|
+
title: extractTitle(join8(plansDir, f.replace("plans/", "")))
|
|
20920
|
+
}));
|
|
20019
20921
|
}
|
|
20020
20922
|
}
|
|
20021
|
-
const
|
|
20022
|
-
|
|
20023
|
-
|
|
20024
|
-
const isDatabaseUser = Boolean(process.env.DATABASE_URL) || config2.adapterType === "pg";
|
|
20025
|
-
if (isProxyUser && existingApiKey && existingProjectId) {
|
|
20026
|
-
const mcpConfig = {
|
|
20027
|
-
mcpServers: {
|
|
20028
|
-
papi: {
|
|
20029
|
-
command: "npx",
|
|
20030
|
-
args: ["-y", "@papi-ai/server"],
|
|
20031
|
-
env: {
|
|
20032
|
-
PAPI_PROJECT_ID: existingProjectId,
|
|
20033
|
-
PAPI_DATA_API_KEY: existingApiKey
|
|
20034
|
-
}
|
|
20035
|
-
}
|
|
20036
|
-
}
|
|
20037
|
-
};
|
|
20038
|
-
await writeFile4(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf-8");
|
|
20039
|
-
await ensureGitignoreEntry(projectRoot, ".mcp.json");
|
|
20040
|
-
return textResponse(
|
|
20041
|
-
`# PAPI Initialised \u2014 ${projectName}
|
|
20042
|
-
|
|
20043
|
-
**Config:** \`${mcpJsonPath}\`
|
|
20044
|
-
|
|
20045
|
-
Your existing API key and project ID have been saved to .mcp.json.
|
|
20046
|
-
|
|
20047
|
-
## Next Steps
|
|
20048
|
-
|
|
20049
|
-
1. **Restart your MCP client** to pick up the new config.
|
|
20050
|
-
2. **Run \`setup\`** \u2014 this scaffolds your project with a Product Brief and CLAUDE.md.
|
|
20051
|
-
`
|
|
20052
|
-
);
|
|
20923
|
+
const lines = [];
|
|
20924
|
+
if (unregisteredDocs.length === 0 && unregisteredPlans.length === 0) {
|
|
20925
|
+
return textResponse("All docs are registered. No unregistered files found.");
|
|
20053
20926
|
}
|
|
20054
|
-
if (
|
|
20055
|
-
|
|
20056
|
-
const
|
|
20057
|
-
|
|
20058
|
-
|
|
20059
|
-
|
|
20060
|
-
args: ["-y", "@papi-ai/server"],
|
|
20061
|
-
env: {
|
|
20062
|
-
PAPI_PROJECT_DIR: projectRoot,
|
|
20063
|
-
PAPI_ADAPTER: "pg",
|
|
20064
|
-
DATABASE_URL: process.env.DATABASE_URL || "<YOUR_DATABASE_URL>",
|
|
20065
|
-
PAPI_PROJECT_ID: projectId
|
|
20066
|
-
}
|
|
20067
|
-
}
|
|
20068
|
-
}
|
|
20069
|
-
};
|
|
20070
|
-
await writeFile4(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf-8");
|
|
20071
|
-
await ensureGitignoreEntry(projectRoot, ".mcp.json");
|
|
20072
|
-
const output2 = [
|
|
20073
|
-
`# PAPI Initialised \u2014 ${projectName}`,
|
|
20074
|
-
"",
|
|
20075
|
-
`**Project ID:** \`${projectId}\``,
|
|
20076
|
-
`**Config:** \`${mcpJsonPath}\``,
|
|
20077
|
-
"",
|
|
20078
|
-
"## Next Steps",
|
|
20079
|
-
"",
|
|
20080
|
-
...process.env.DATABASE_URL ? ["1. **Restart your MCP client** to pick up the new config."] : ["1. **Set your DATABASE_URL** \u2014 replace `<YOUR_DATABASE_URL>` in `.mcp.json` with your Supabase connection string."],
|
|
20081
|
-
"2. **Run `setup`** \u2014 this scaffolds your project with a Product Brief, Active Decisions, and CLAUDE.md."
|
|
20082
|
-
].join("\n");
|
|
20083
|
-
return textResponse(output2);
|
|
20927
|
+
if (unregisteredDocs.length > 0) {
|
|
20928
|
+
lines.push(`## Unregistered Docs (${unregisteredDocs.length})`);
|
|
20929
|
+
for (const f of unregisteredDocs) {
|
|
20930
|
+
const title = extractTitle(join8(config2.projectRoot, f));
|
|
20931
|
+
lines.push(`- \`${f}\`${title ? ` \u2014 ${title}` : ""}`);
|
|
20932
|
+
}
|
|
20084
20933
|
}
|
|
20085
|
-
|
|
20086
|
-
|
|
20087
|
-
|
|
20088
|
-
|
|
20089
|
-
|
|
20090
|
-
|
|
20091
|
-
|
|
20092
|
-
|
|
20093
|
-
"2. **Complete the onboarding wizard** \u2014 it generates your `.mcp.json` config with your API key and project ID",
|
|
20094
|
-
"3. **Download the config**, place it in your project root, and restart your MCP client",
|
|
20095
|
-
"",
|
|
20096
|
-
"The onboarding wizard generates everything you need \u2014 no manual configuration required.",
|
|
20097
|
-
"",
|
|
20098
|
-
`> Already have an account? Make sure both \`PAPI_PROJECT_ID\` and \`PAPI_DATA_API_KEY\` are set in your .mcp.json.`
|
|
20099
|
-
].join("\n");
|
|
20100
|
-
return textResponse(output);
|
|
20934
|
+
if (unregisteredPlans.length > 0) {
|
|
20935
|
+
lines.push("", `## Unregistered Plans (${unregisteredPlans.length})`);
|
|
20936
|
+
for (const p of unregisteredPlans) {
|
|
20937
|
+
lines.push(`- \`${p.path}\`${p.title ? ` \u2014 ${p.title}` : ""}`);
|
|
20938
|
+
}
|
|
20939
|
+
}
|
|
20940
|
+
lines.push("", `Use \`doc_register\` to register these files.`);
|
|
20941
|
+
return textResponse(lines.join("\n"));
|
|
20101
20942
|
}
|
|
20102
20943
|
|
|
20103
20944
|
// src/tools/orient.ts
|
|
20104
|
-
init_git();
|
|
20105
20945
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
20106
|
-
import { readFileSync as
|
|
20107
|
-
import { join as
|
|
20946
|
+
import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync6 } from "fs";
|
|
20947
|
+
import { join as join9 } from "path";
|
|
20108
20948
|
var orientTool = {
|
|
20109
20949
|
name: "orient",
|
|
20110
20950
|
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.",
|
|
20951
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
20111
20952
|
inputSchema: {
|
|
20112
20953
|
type: "object",
|
|
20113
20954
|
properties: {},
|
|
@@ -20283,8 +21124,8 @@ function getLatestGitTag(projectRoot) {
|
|
|
20283
21124
|
}
|
|
20284
21125
|
function checkNpmVersionDrift() {
|
|
20285
21126
|
try {
|
|
20286
|
-
const pkgPath =
|
|
20287
|
-
const pkg = JSON.parse(
|
|
21127
|
+
const pkgPath = join9(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
|
|
21128
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
|
|
20288
21129
|
const localVersion = pkg.version;
|
|
20289
21130
|
const packageName = pkg.name;
|
|
20290
21131
|
const published = execFileSync3("npm", ["view", packageName, "version"], {
|
|
@@ -20324,6 +21165,17 @@ async function handleOrient(adapter2, config2) {
|
|
|
20324
21165
|
if (!cycleIsComplete && cycleTotal === 0 && cycleDone > 0) {
|
|
20325
21166
|
buildResult.warnings.unshift(`\u26A0\uFE0F Cycle ${currentCycle} is complete \u2014 all ${cycleDone} task${cycleDone !== 1 ? "s" : ""} Done. Release has not been run. Run \`release\` now.`);
|
|
20326
21167
|
}
|
|
21168
|
+
try {
|
|
21169
|
+
const p1BacklogTasks = await adapter2.queryBoard({ status: ["Backlog"], priority: ["P1 High"] });
|
|
21170
|
+
const stalledP1 = p1BacklogTasks.filter(
|
|
21171
|
+
(t) => t.createdCycle != null && currentCycle - t.createdCycle >= 3
|
|
21172
|
+
);
|
|
21173
|
+
if (stalledP1.length > 0) {
|
|
21174
|
+
const ids = stalledP1.map((t) => `${t.displayId} (${currentCycle - (t.createdCycle ?? currentCycle)}+ cycles)`).join(", ");
|
|
21175
|
+
buildResult.warnings.push(`\u26A0\uFE0F P1 tasks stalled 3+ cycles: ${ids}`);
|
|
21176
|
+
}
|
|
21177
|
+
} catch {
|
|
21178
|
+
}
|
|
20327
21179
|
const inProgressItems = buildResult.inProgress.map(
|
|
20328
21180
|
(t) => `- **${t.id}:** ${t.title} (${t.priority} | ${t.complexity})`
|
|
20329
21181
|
);
|
|
@@ -20379,12 +21231,36 @@ ${versionDrift}` : "";
|
|
|
20379
21231
|
try {
|
|
20380
21232
|
const unrecorded = detectUnrecordedCommits(config2.projectRoot, config2.baseBranch);
|
|
20381
21233
|
if (unrecorded.length > 0) {
|
|
21234
|
+
const doneTasks = await adapter2.queryBoard({ status: ["Done"] });
|
|
21235
|
+
const adHocDoneTasks = doneTasks.filter((t) => t.cycle == null);
|
|
21236
|
+
const alreadyRecorded = adHocDoneTasks.length >= unrecorded.length;
|
|
20382
21237
|
const lines = ["\n\n## Unrecorded Work"];
|
|
20383
|
-
|
|
21238
|
+
if (alreadyRecorded) {
|
|
21239
|
+
lines.push(`${unrecorded.length} commit(s) on ${config2.baseBranch} since last release not matched to \`build_execute\` commits. ${adHocDoneTasks.length} ad_hoc task(s) already recorded \u2014 this work may already be captured. Verify before running \`ad_hoc\` again.`);
|
|
21240
|
+
} else {
|
|
21241
|
+
lines.push(`${unrecorded.length} commit(s) on ${config2.baseBranch} since last release not captured by \`build_execute\`. Run \`ad_hoc\` to record them.`);
|
|
21242
|
+
}
|
|
20384
21243
|
for (const c of unrecorded) {
|
|
20385
21244
|
lines.push(`- \`${c.hash}\` ${c.message}`);
|
|
20386
21245
|
}
|
|
20387
|
-
unrecordedNote = lines.join("\n");
|
|
21246
|
+
unrecordedNote = lines.join("\n");
|
|
21247
|
+
}
|
|
21248
|
+
} catch {
|
|
21249
|
+
}
|
|
21250
|
+
let unregisteredDocsNote = "";
|
|
21251
|
+
try {
|
|
21252
|
+
if (adapter2.searchDocs) {
|
|
21253
|
+
const docsDir = join9(config2.projectRoot, "docs");
|
|
21254
|
+
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
21255
|
+
if (docsFiles.length > 0) {
|
|
21256
|
+
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
21257
|
+
const registeredPaths = new Set(registered.map((d) => d.path));
|
|
21258
|
+
const unregisteredCount = docsFiles.filter((f) => !registeredPaths.has(f)).length;
|
|
21259
|
+
if (unregisteredCount > 0) {
|
|
21260
|
+
unregisteredDocsNote = `
|
|
21261
|
+
\u26A0\uFE0F **${unregisteredCount} unregistered doc(s) in docs/** \u2014 run \`doc_scan\` to review, then \`doc_register\` to index them.`;
|
|
21262
|
+
}
|
|
21263
|
+
}
|
|
20388
21264
|
}
|
|
20389
21265
|
} catch {
|
|
20390
21266
|
}
|
|
@@ -20421,16 +21297,33 @@ ${versionDrift}` : "";
|
|
|
20421
21297
|
}
|
|
20422
21298
|
} catch {
|
|
20423
21299
|
}
|
|
20424
|
-
|
|
21300
|
+
let unactionedIssuesNote = "";
|
|
21301
|
+
try {
|
|
21302
|
+
const learnings = await adapter2.getCycleLearnings?.({ category: "issue", limit: 20 });
|
|
21303
|
+
if (learnings) {
|
|
21304
|
+
const unactioned = learnings.filter((l) => !l.actionTaken && l.severity && ["P0", "P1", "P2"].includes(l.severity)).slice(0, 5);
|
|
21305
|
+
if (unactioned.length > 0) {
|
|
21306
|
+
const lines = ["\n\n## Unactioned Issues"];
|
|
21307
|
+
for (const issue of unactioned) {
|
|
21308
|
+
const desc = issue.summary.length > 100 ? `${issue.summary.slice(0, 97)}\u2026` : issue.summary;
|
|
21309
|
+
lines.push(`- **${issue.severity}** (C${issue.cycleNumber} / ${issue.taskId}): ${desc}`);
|
|
21310
|
+
}
|
|
21311
|
+
lines.push("_Run `idea` to log these as backlog tasks, or `board_edit` if already handled._");
|
|
21312
|
+
unactionedIssuesNote = lines.join("\n");
|
|
21313
|
+
}
|
|
21314
|
+
}
|
|
21315
|
+
} catch {
|
|
21316
|
+
}
|
|
21317
|
+
return textResponse(formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot) + ttfvNote + reconciliationNote + unrecordedNote + unregisteredDocsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + versionNote + enrichmentNote);
|
|
20425
21318
|
} catch (err) {
|
|
20426
21319
|
const message = err instanceof Error ? err.message : String(err);
|
|
20427
21320
|
return errorResponse(`Orient failed: ${message}`);
|
|
20428
21321
|
}
|
|
20429
21322
|
}
|
|
20430
21323
|
function enrichClaudeMd(projectRoot, cycleNumber) {
|
|
20431
|
-
const claudeMdPath =
|
|
20432
|
-
if (!
|
|
20433
|
-
const content =
|
|
21324
|
+
const claudeMdPath = join9(projectRoot, "CLAUDE.md");
|
|
21325
|
+
if (!existsSync6(claudeMdPath)) return "";
|
|
21326
|
+
const content = readFileSync3(claudeMdPath, "utf-8");
|
|
20434
21327
|
const additions = [];
|
|
20435
21328
|
if (cycleNumber >= 6 && !content.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1)) {
|
|
20436
21329
|
additions.push(CLAUDE_MD_TIER_1);
|
|
@@ -20452,6 +21345,7 @@ function enrichClaudeMd(projectRoot, cycleNumber) {
|
|
|
20452
21345
|
var hierarchyUpdateTool = {
|
|
20453
21346
|
name: "hierarchy_update",
|
|
20454
21347
|
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.",
|
|
21348
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
20455
21349
|
inputSchema: {
|
|
20456
21350
|
type: "object",
|
|
20457
21351
|
properties: {
|
|
@@ -20847,6 +21741,7 @@ async function applyZoomOut(adapter2, llmResponse, cycleNumber) {
|
|
|
20847
21741
|
var zoomOutTool = {
|
|
20848
21742
|
name: "zoom_out",
|
|
20849
21743
|
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.',
|
|
21744
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
20850
21745
|
inputSchema: {
|
|
20851
21746
|
type: "object",
|
|
20852
21747
|
properties: {
|
|
@@ -20921,222 +21816,11 @@ ${result.userMessage}
|
|
|
20921
21816
|
}
|
|
20922
21817
|
}
|
|
20923
21818
|
|
|
20924
|
-
// src/tools/doc-registry.ts
|
|
20925
|
-
import { readdirSync as readdirSync4, existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
|
|
20926
|
-
import { join as join9, relative } from "path";
|
|
20927
|
-
import { homedir as homedir2 } from "os";
|
|
20928
|
-
var docRegisterTool = {
|
|
20929
|
-
name: "doc_register",
|
|
20930
|
-
description: "Register 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.",
|
|
20931
|
-
inputSchema: {
|
|
20932
|
-
type: "object",
|
|
20933
|
-
properties: {
|
|
20934
|
-
path: { type: "string", description: 'Relative path from project root (e.g. "docs/research/funding-landscape.md").' },
|
|
20935
|
-
title: { type: "string", description: "Document title." },
|
|
20936
|
-
type: { type: "string", enum: ["research", "audit", "spec", "guide", "architecture", "positioning", "framework", "reference"], description: "Document type." },
|
|
20937
|
-
status: { type: "string", enum: ["active", "draft", "superseded", "actioned", "legacy", "archived"], description: 'Document status. Defaults to "active".' },
|
|
20938
|
-
summary: { type: "string", description: 'Structured 2-4 sentence summary. Format: "Conclusions: ... Open questions: ... Unactioned: ..."' },
|
|
20939
|
-
tags: { type: "array", items: { type: "string" }, description: "Tags from project vocabulary." },
|
|
20940
|
-
cycle: { type: "number", description: "Current cycle number." },
|
|
20941
|
-
actions: {
|
|
20942
|
-
type: "array",
|
|
20943
|
-
items: {
|
|
20944
|
-
type: "object",
|
|
20945
|
-
properties: {
|
|
20946
|
-
description: { type: "string" },
|
|
20947
|
-
status: { type: "string", enum: ["pending", "resolved"] },
|
|
20948
|
-
linkedTaskId: { type: "string" }
|
|
20949
|
-
},
|
|
20950
|
-
required: ["description", "status"]
|
|
20951
|
-
},
|
|
20952
|
-
description: "Actionable findings from the document."
|
|
20953
|
-
},
|
|
20954
|
-
superseded_by_path: { type: "string", description: "Path of the doc that supersedes this one (sets status to superseded)." }
|
|
20955
|
-
},
|
|
20956
|
-
required: ["path", "title", "type", "summary", "cycle"]
|
|
20957
|
-
}
|
|
20958
|
-
};
|
|
20959
|
-
var docSearchTool = {
|
|
20960
|
-
name: "doc_search",
|
|
20961
|
-
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.",
|
|
20962
|
-
inputSchema: {
|
|
20963
|
-
type: "object",
|
|
20964
|
-
properties: {
|
|
20965
|
-
type: { type: "string", description: 'Filter by doc type (e.g. "research", "architecture").' },
|
|
20966
|
-
status: { type: "string", description: 'Filter by status. Defaults to "active".' },
|
|
20967
|
-
tags: { type: "array", items: { type: "string" }, description: "Filter by tags (OR match)." },
|
|
20968
|
-
keyword: { type: "string", description: "Search title and summary text." },
|
|
20969
|
-
has_pending_actions: { type: "boolean", description: "Only docs with unresolved action items." },
|
|
20970
|
-
since_cycle: { type: "number", description: "Docs updated since this cycle." },
|
|
20971
|
-
limit: { type: "number", description: "Max results (default: 10)." }
|
|
20972
|
-
},
|
|
20973
|
-
required: []
|
|
20974
|
-
}
|
|
20975
|
-
};
|
|
20976
|
-
var docScanTool = {
|
|
20977
|
-
name: "doc_scan",
|
|
20978
|
-
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.",
|
|
20979
|
-
inputSchema: {
|
|
20980
|
-
type: "object",
|
|
20981
|
-
properties: {
|
|
20982
|
-
include_plans: {
|
|
20983
|
-
type: "boolean",
|
|
20984
|
-
description: "Also scan ~/.claude/plans/ for plan files (default: false)."
|
|
20985
|
-
}
|
|
20986
|
-
},
|
|
20987
|
-
required: []
|
|
20988
|
-
}
|
|
20989
|
-
};
|
|
20990
|
-
async function handleDocRegister(adapter2, args) {
|
|
20991
|
-
if (!adapter2.registerDoc) {
|
|
20992
|
-
return errorResponse("Doc registry not available \u2014 requires pg adapter.");
|
|
20993
|
-
}
|
|
20994
|
-
const path5 = args.path;
|
|
20995
|
-
const title = args.title;
|
|
20996
|
-
const type = args.type;
|
|
20997
|
-
const status = args.status ?? "active";
|
|
20998
|
-
const summary = args.summary;
|
|
20999
|
-
const tags = args.tags ?? [];
|
|
21000
|
-
const cycle = args.cycle;
|
|
21001
|
-
const actions = args.actions;
|
|
21002
|
-
const supersededByPath = args.superseded_by_path;
|
|
21003
|
-
if (!path5 || !title || !type || !summary || !cycle) {
|
|
21004
|
-
return errorResponse("Required fields: path, title, type, summary, cycle.");
|
|
21005
|
-
}
|
|
21006
|
-
let supersededBy;
|
|
21007
|
-
if (supersededByPath) {
|
|
21008
|
-
const existing = await adapter2.getDoc?.(supersededByPath);
|
|
21009
|
-
if (existing) {
|
|
21010
|
-
supersededBy = existing.id;
|
|
21011
|
-
await adapter2.updateDocStatus?.(existing.id, "superseded", void 0);
|
|
21012
|
-
}
|
|
21013
|
-
}
|
|
21014
|
-
const entry = await adapter2.registerDoc({
|
|
21015
|
-
title,
|
|
21016
|
-
type,
|
|
21017
|
-
path: path5,
|
|
21018
|
-
status: supersededByPath ? "superseded" : status,
|
|
21019
|
-
summary,
|
|
21020
|
-
tags,
|
|
21021
|
-
cycleCreated: cycle,
|
|
21022
|
-
cycleUpdated: cycle,
|
|
21023
|
-
supersededBy,
|
|
21024
|
-
actions
|
|
21025
|
-
});
|
|
21026
|
-
return textResponse(
|
|
21027
|
-
`**Registered:** ${entry.title}
|
|
21028
|
-
- **Path:** ${entry.path}
|
|
21029
|
-
- **Type:** ${entry.type} | **Status:** ${entry.status}
|
|
21030
|
-
- **Tags:** ${entry.tags.length > 0 ? entry.tags.join(", ") : "none"}
|
|
21031
|
-
- **Actions:** ${actions?.length ?? 0} items
|
|
21032
|
-
- **ID:** ${entry.id}`
|
|
21033
|
-
);
|
|
21034
|
-
}
|
|
21035
|
-
async function handleDocSearch(adapter2, args) {
|
|
21036
|
-
if (!adapter2.searchDocs) {
|
|
21037
|
-
return errorResponse("Doc registry not available \u2014 requires pg adapter.");
|
|
21038
|
-
}
|
|
21039
|
-
const input = {
|
|
21040
|
-
type: args.type,
|
|
21041
|
-
status: args.status,
|
|
21042
|
-
tags: args.tags,
|
|
21043
|
-
keyword: args.keyword,
|
|
21044
|
-
hasPendingActions: args.has_pending_actions,
|
|
21045
|
-
sinceCycle: args.since_cycle,
|
|
21046
|
-
limit: args.limit
|
|
21047
|
-
};
|
|
21048
|
-
const docs = await adapter2.searchDocs(input);
|
|
21049
|
-
if (docs.length === 0) {
|
|
21050
|
-
return textResponse("No documents found matching the search criteria.");
|
|
21051
|
-
}
|
|
21052
|
-
const lines = docs.map((d) => {
|
|
21053
|
-
const actionCount = d.actions?.filter((a) => a.status === "pending").length ?? 0;
|
|
21054
|
-
const actionNote = actionCount > 0 ? ` | ${actionCount} pending action(s)` : "";
|
|
21055
|
-
return `### ${d.title}
|
|
21056
|
-
**Type:** ${d.type} | **Status:** ${d.status} | **Cycle:** ${d.cycleCreated}${d.cycleUpdated ? `\u2192${d.cycleUpdated}` : ""}${actionNote}
|
|
21057
|
-
**Path:** ${d.path}
|
|
21058
|
-
**Tags:** ${d.tags.length > 0 ? d.tags.join(", ") : "none"}
|
|
21059
|
-
${d.summary}
|
|
21060
|
-
`;
|
|
21061
|
-
});
|
|
21062
|
-
return textResponse(`**${docs.length} document(s) found:**
|
|
21063
|
-
|
|
21064
|
-
${lines.join("\n---\n\n")}`);
|
|
21065
|
-
}
|
|
21066
|
-
function scanMdFiles(dir, rootDir) {
|
|
21067
|
-
if (!existsSync6(dir)) return [];
|
|
21068
|
-
const files = [];
|
|
21069
|
-
try {
|
|
21070
|
-
const entries = readdirSync4(dir, { withFileTypes: true });
|
|
21071
|
-
for (const entry of entries) {
|
|
21072
|
-
const full = join9(dir, entry.name);
|
|
21073
|
-
if (entry.isDirectory()) {
|
|
21074
|
-
files.push(...scanMdFiles(full, rootDir));
|
|
21075
|
-
} else if (entry.name.endsWith(".md")) {
|
|
21076
|
-
files.push(relative(rootDir, full));
|
|
21077
|
-
}
|
|
21078
|
-
}
|
|
21079
|
-
} catch {
|
|
21080
|
-
}
|
|
21081
|
-
return files;
|
|
21082
|
-
}
|
|
21083
|
-
function extractTitle(filePath) {
|
|
21084
|
-
try {
|
|
21085
|
-
const content = readFileSync3(filePath, "utf-8").slice(0, 1e3);
|
|
21086
|
-
const fmMatch = content.match(/^---[\s\S]*?title:\s*(.+?)$/m);
|
|
21087
|
-
if (fmMatch) return fmMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
21088
|
-
const headingMatch = content.match(/^#+\s+(.+)$/m);
|
|
21089
|
-
if (headingMatch) return headingMatch[1].trim();
|
|
21090
|
-
} catch {
|
|
21091
|
-
}
|
|
21092
|
-
return void 0;
|
|
21093
|
-
}
|
|
21094
|
-
async function handleDocScan(adapter2, config2, args) {
|
|
21095
|
-
if (!adapter2.searchDocs) {
|
|
21096
|
-
return errorResponse("Doc registry not available on this adapter.");
|
|
21097
|
-
}
|
|
21098
|
-
const includePlans = args.include_plans ?? false;
|
|
21099
|
-
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
21100
|
-
const registeredPaths = new Set(registered.map((d) => d.path));
|
|
21101
|
-
const docsDir = join9(config2.projectRoot, "docs");
|
|
21102
|
-
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
21103
|
-
const unregisteredDocs = docsFiles.filter((f) => !registeredPaths.has(f));
|
|
21104
|
-
let unregisteredPlans = [];
|
|
21105
|
-
if (includePlans) {
|
|
21106
|
-
const plansDir = join9(homedir2(), ".claude", "plans");
|
|
21107
|
-
if (existsSync6(plansDir)) {
|
|
21108
|
-
const planFiles = scanMdFiles(plansDir, plansDir);
|
|
21109
|
-
unregisteredPlans = planFiles.map((f) => `plans/${f}`).filter((f) => !registeredPaths.has(f)).map((f) => ({
|
|
21110
|
-
path: f,
|
|
21111
|
-
title: extractTitle(join9(plansDir, f.replace("plans/", "")))
|
|
21112
|
-
}));
|
|
21113
|
-
}
|
|
21114
|
-
}
|
|
21115
|
-
const lines = [];
|
|
21116
|
-
if (unregisteredDocs.length === 0 && unregisteredPlans.length === 0) {
|
|
21117
|
-
return textResponse("All docs are registered. No unregistered files found.");
|
|
21118
|
-
}
|
|
21119
|
-
if (unregisteredDocs.length > 0) {
|
|
21120
|
-
lines.push(`## Unregistered Docs (${unregisteredDocs.length})`);
|
|
21121
|
-
for (const f of unregisteredDocs) {
|
|
21122
|
-
const title = extractTitle(join9(config2.projectRoot, f));
|
|
21123
|
-
lines.push(`- \`${f}\`${title ? ` \u2014 ${title}` : ""}`);
|
|
21124
|
-
}
|
|
21125
|
-
}
|
|
21126
|
-
if (unregisteredPlans.length > 0) {
|
|
21127
|
-
lines.push("", `## Unregistered Plans (${unregisteredPlans.length})`);
|
|
21128
|
-
for (const p of unregisteredPlans) {
|
|
21129
|
-
lines.push(`- \`${p.path}\`${p.title ? ` \u2014 ${p.title}` : ""}`);
|
|
21130
|
-
}
|
|
21131
|
-
}
|
|
21132
|
-
lines.push("", `Use \`doc_register\` to register these files.`);
|
|
21133
|
-
return textResponse(lines.join("\n"));
|
|
21134
|
-
}
|
|
21135
|
-
|
|
21136
21819
|
// src/tools/sibling-ads.ts
|
|
21137
21820
|
var getSiblingAdsTool = {
|
|
21138
21821
|
name: "get_sibling_ads",
|
|
21139
21822
|
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.",
|
|
21823
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
21140
21824
|
inputSchema: {
|
|
21141
21825
|
type: "object",
|
|
21142
21826
|
properties: {
|
|
@@ -21318,6 +22002,7 @@ var lastPrepareContextBytes2;
|
|
|
21318
22002
|
var handoffGenerateTool = {
|
|
21319
22003
|
name: "handoff_generate",
|
|
21320
22004
|
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.",
|
|
22005
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
21321
22006
|
inputSchema: {
|
|
21322
22007
|
type: "object",
|
|
21323
22008
|
properties: {
|
|
@@ -21428,12 +22113,14 @@ function isEnabled() {
|
|
|
21428
22113
|
}
|
|
21429
22114
|
function emitTelemetryEvent(event) {
|
|
21430
22115
|
if (!isEnabled()) return;
|
|
22116
|
+
const userId = event.user_id ?? process.env["PAPI_USER_ID"] ?? void 0;
|
|
21431
22117
|
const body = {
|
|
21432
22118
|
project_id: event.project_id,
|
|
21433
22119
|
tool_name: event.tool_name,
|
|
21434
22120
|
event_type: event.event_type,
|
|
21435
22121
|
metadata: event.metadata ?? {}
|
|
21436
22122
|
};
|
|
22123
|
+
if (userId) body["user_id"] = userId;
|
|
21437
22124
|
fetch(`${TELEMETRY_SUPABASE_URL}/rest/v1/telemetry_events`, {
|
|
21438
22125
|
method: "POST",
|
|
21439
22126
|
headers: {
|
|
@@ -21480,7 +22167,6 @@ var TOOLS_REQUIRING_PAPI = /* @__PURE__ */ new Set([
|
|
|
21480
22167
|
"idea",
|
|
21481
22168
|
"bug",
|
|
21482
22169
|
"ad_hoc",
|
|
21483
|
-
"health",
|
|
21484
22170
|
"board_reconcile",
|
|
21485
22171
|
"review_list",
|
|
21486
22172
|
"review_submit",
|
|
@@ -21562,7 +22248,6 @@ function createServer(adapter2, config2) {
|
|
|
21562
22248
|
bugTool,
|
|
21563
22249
|
adHocTool,
|
|
21564
22250
|
boardReconcileTool,
|
|
21565
|
-
healthTool,
|
|
21566
22251
|
releaseTool,
|
|
21567
22252
|
reviewListTool,
|
|
21568
22253
|
reviewSubmitTool,
|
|
@@ -21654,9 +22339,6 @@ function createServer(adapter2, config2) {
|
|
|
21654
22339
|
case "board_reconcile":
|
|
21655
22340
|
result = await handleBoardReconcile(adapter2, config2, safeArgs);
|
|
21656
22341
|
break;
|
|
21657
|
-
case "health":
|
|
21658
|
-
result = await handleHealth(adapter2);
|
|
21659
|
-
break;
|
|
21660
22342
|
case "release":
|
|
21661
22343
|
result = await handleRelease(adapter2, config2, safeArgs);
|
|
21662
22344
|
break;
|
|
@@ -21841,5 +22523,123 @@ if (pkgVersion !== "unknown") {
|
|
|
21841
22523
|
}
|
|
21842
22524
|
})();
|
|
21843
22525
|
}
|
|
21844
|
-
var
|
|
21845
|
-
|
|
22526
|
+
var httpPortRaw = process.env["PAPI_HTTP_PORT"] ?? process.env["PORT"];
|
|
22527
|
+
var httpPort = httpPortRaw ? parseInt(httpPortRaw, 10) : void 0;
|
|
22528
|
+
var httpHost = process.env["PORT"] ? "0.0.0.0" : "127.0.0.1";
|
|
22529
|
+
if (httpPort) {
|
|
22530
|
+
if (isNaN(httpPort) || httpPort < 1 || httpPort > 65535) {
|
|
22531
|
+
process.stderr.write(`[papi] Invalid PAPI_HTTP_PORT: "${process.env.PAPI_HTTP_PORT}". Must be a number between 1 and 65535.
|
|
22532
|
+
`);
|
|
22533
|
+
process.exit(1);
|
|
22534
|
+
}
|
|
22535
|
+
const httpToken = process.env.PAPI_HTTP_TOKEN;
|
|
22536
|
+
if (!httpToken) {
|
|
22537
|
+
process.stderr.write("[papi] WARNING: PAPI_HTTP_TOKEN is not set. HTTP transport is unauthenticated \u2014 anyone with the URL can call your PAPI tools. Set PAPI_HTTP_TOKEN to a secret string.\n");
|
|
22538
|
+
}
|
|
22539
|
+
const createServerForRequest = () => {
|
|
22540
|
+
if (adapter && !setupError) {
|
|
22541
|
+
return createServer(adapter, config);
|
|
22542
|
+
}
|
|
22543
|
+
const errorServer = new Server2(
|
|
22544
|
+
{ name: "papi", version: pkgVersion },
|
|
22545
|
+
{ capabilities: { tools: {} } }
|
|
22546
|
+
);
|
|
22547
|
+
const errorMessage = setupError || "Unknown startup error";
|
|
22548
|
+
errorServer.setRequestHandler(ListToolsRequestSchema2, async () => ({
|
|
22549
|
+
tools: [{
|
|
22550
|
+
name: "setup",
|
|
22551
|
+
description: "PAPI is not connected \u2014 run this tool for setup instructions.",
|
|
22552
|
+
inputSchema: { type: "object", properties: {}, required: [] }
|
|
22553
|
+
}]
|
|
22554
|
+
}));
|
|
22555
|
+
errorServer.setRequestHandler(CallToolRequestSchema2, async () => ({
|
|
22556
|
+
content: [{
|
|
22557
|
+
type: "text",
|
|
22558
|
+
text: `# PAPI Connection Error
|
|
22559
|
+
|
|
22560
|
+
${errorMessage}
|
|
22561
|
+
|
|
22562
|
+
## Quick Fix
|
|
22563
|
+
|
|
22564
|
+
If you haven't set up PAPI yet:
|
|
22565
|
+
1. Go to https://getpapi.ai/login and sign up
|
|
22566
|
+
2. Complete the onboarding wizard \u2014 it generates your config
|
|
22567
|
+
3. Copy the config to your project and restart your AI tool
|
|
22568
|
+
|
|
22569
|
+
If you already have an account, check that both **PAPI_PROJECT_ID** and **PAPI_DATA_API_KEY** are set in your .mcp.json env config.`
|
|
22570
|
+
}]
|
|
22571
|
+
}));
|
|
22572
|
+
return errorServer;
|
|
22573
|
+
};
|
|
22574
|
+
const httpServer = createHttpServer((req, res) => {
|
|
22575
|
+
if (req.method === "GET" && req.url === "/healthz") {
|
|
22576
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
22577
|
+
res.end("ok");
|
|
22578
|
+
return;
|
|
22579
|
+
}
|
|
22580
|
+
if (httpToken) {
|
|
22581
|
+
const authHeader = req.headers["authorization"] ?? "";
|
|
22582
|
+
const provided = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
|
|
22583
|
+
if (provided !== httpToken) {
|
|
22584
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
22585
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
22586
|
+
return;
|
|
22587
|
+
}
|
|
22588
|
+
}
|
|
22589
|
+
if (req.url === "/mcp" || req.url === "/sse") {
|
|
22590
|
+
if (req.method === "POST") {
|
|
22591
|
+
const chunks = [];
|
|
22592
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
22593
|
+
req.on("end", () => {
|
|
22594
|
+
let parsedBody;
|
|
22595
|
+
try {
|
|
22596
|
+
parsedBody = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
|
|
22597
|
+
} catch {
|
|
22598
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
22599
|
+
res.end(JSON.stringify({ error: "Invalid JSON body" }));
|
|
22600
|
+
return;
|
|
22601
|
+
}
|
|
22602
|
+
(async () => {
|
|
22603
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
|
|
22604
|
+
const reqServer = createServerForRequest();
|
|
22605
|
+
await reqServer.connect(transport);
|
|
22606
|
+
await transport.handleRequest(req, res, parsedBody);
|
|
22607
|
+
await reqServer.close();
|
|
22608
|
+
})().catch((err) => {
|
|
22609
|
+
process.stderr.write(`[papi] HTTP transport error: ${err instanceof Error ? err.message : String(err)}
|
|
22610
|
+
`);
|
|
22611
|
+
if (!res.headersSent) {
|
|
22612
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
22613
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
22614
|
+
}
|
|
22615
|
+
});
|
|
22616
|
+
});
|
|
22617
|
+
return;
|
|
22618
|
+
}
|
|
22619
|
+
(async () => {
|
|
22620
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
|
|
22621
|
+
const reqServer = createServerForRequest();
|
|
22622
|
+
await reqServer.connect(transport);
|
|
22623
|
+
await transport.handleRequest(req, res);
|
|
22624
|
+
await reqServer.close();
|
|
22625
|
+
})().catch((err) => {
|
|
22626
|
+
process.stderr.write(`[papi] HTTP transport error: ${err instanceof Error ? err.message : String(err)}
|
|
22627
|
+
`);
|
|
22628
|
+
if (!res.headersSent) {
|
|
22629
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
22630
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
22631
|
+
}
|
|
22632
|
+
});
|
|
22633
|
+
return;
|
|
22634
|
+
}
|
|
22635
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
22636
|
+
res.end("Not found");
|
|
22637
|
+
});
|
|
22638
|
+
httpServer.listen(httpPort, httpHost, () => {
|
|
22639
|
+
process.stderr.write(`[papi] HTTP transport listening on http://${httpHost}:${httpPort}/mcp
|
|
22640
|
+
`);
|
|
22641
|
+
});
|
|
22642
|
+
} else {
|
|
22643
|
+
const transport = new StdioServerTransport();
|
|
22644
|
+
await server.connect(transport);
|
|
22645
|
+
}
|