@papi-ai/server 0.7.6 → 0.7.7
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 +1356 -744
- 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
|
// -------------------------------------------------------------------------
|
|
@@ -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;
|
|
@@ -9538,7 +9619,7 @@ function formatDetailedTask(t) {
|
|
|
9538
9619
|
return `- **${t.id}:** ${t.title}
|
|
9539
9620
|
Status: ${t.status} | Priority: ${t.priority} | Complexity: ${t.complexity}${typeTag}
|
|
9540
9621
|
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 ? `
|
|
9622
|
+
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
9623
|
Notes: ${notes}` : ""}`;
|
|
9543
9624
|
}
|
|
9544
9625
|
function formatBoardForPlan(tasks, filters, currentCycle) {
|
|
@@ -9945,6 +10026,191 @@ function logDataSourceSummary(service, sources) {
|
|
|
9945
10026
|
console.error(`[data-sources] ${service}: ${populated.length}/${sources.length} sources have data \u2014 empty: ${emptyLabels}`);
|
|
9946
10027
|
}
|
|
9947
10028
|
|
|
10029
|
+
// src/lib/codebase-scan.ts
|
|
10030
|
+
import { execSync as execSync2 } from "child_process";
|
|
10031
|
+
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
10032
|
+
"a",
|
|
10033
|
+
"an",
|
|
10034
|
+
"the",
|
|
10035
|
+
"and",
|
|
10036
|
+
"or",
|
|
10037
|
+
"but",
|
|
10038
|
+
"in",
|
|
10039
|
+
"on",
|
|
10040
|
+
"at",
|
|
10041
|
+
"to",
|
|
10042
|
+
"for",
|
|
10043
|
+
"of",
|
|
10044
|
+
"with",
|
|
10045
|
+
"by",
|
|
10046
|
+
"from",
|
|
10047
|
+
"is",
|
|
10048
|
+
"are",
|
|
10049
|
+
"was",
|
|
10050
|
+
"were",
|
|
10051
|
+
"be",
|
|
10052
|
+
"been",
|
|
10053
|
+
"has",
|
|
10054
|
+
"have",
|
|
10055
|
+
"had",
|
|
10056
|
+
"do",
|
|
10057
|
+
"does",
|
|
10058
|
+
"did",
|
|
10059
|
+
"will",
|
|
10060
|
+
"would",
|
|
10061
|
+
"could",
|
|
10062
|
+
"should",
|
|
10063
|
+
"may",
|
|
10064
|
+
"might",
|
|
10065
|
+
"can",
|
|
10066
|
+
"not",
|
|
10067
|
+
"no",
|
|
10068
|
+
"if",
|
|
10069
|
+
"then",
|
|
10070
|
+
"than",
|
|
10071
|
+
"that",
|
|
10072
|
+
"this",
|
|
10073
|
+
"it",
|
|
10074
|
+
"its",
|
|
10075
|
+
"all",
|
|
10076
|
+
"each",
|
|
10077
|
+
"every",
|
|
10078
|
+
"both",
|
|
10079
|
+
"as",
|
|
10080
|
+
"so",
|
|
10081
|
+
"up",
|
|
10082
|
+
"out",
|
|
10083
|
+
"about",
|
|
10084
|
+
"into",
|
|
10085
|
+
"over",
|
|
10086
|
+
"after",
|
|
10087
|
+
"before",
|
|
10088
|
+
"between",
|
|
10089
|
+
"under",
|
|
10090
|
+
"above",
|
|
10091
|
+
"such",
|
|
10092
|
+
"only",
|
|
10093
|
+
"also",
|
|
10094
|
+
"just",
|
|
10095
|
+
"more",
|
|
10096
|
+
"most",
|
|
10097
|
+
"other",
|
|
10098
|
+
"some",
|
|
10099
|
+
"any",
|
|
10100
|
+
"new",
|
|
10101
|
+
"when",
|
|
10102
|
+
"how",
|
|
10103
|
+
"what",
|
|
10104
|
+
"which",
|
|
10105
|
+
"who",
|
|
10106
|
+
"add",
|
|
10107
|
+
"create",
|
|
10108
|
+
"build",
|
|
10109
|
+
"implement",
|
|
10110
|
+
"make",
|
|
10111
|
+
"update",
|
|
10112
|
+
"fix",
|
|
10113
|
+
"use",
|
|
10114
|
+
"via",
|
|
10115
|
+
"show",
|
|
10116
|
+
"display",
|
|
10117
|
+
"view",
|
|
10118
|
+
"page",
|
|
10119
|
+
"data",
|
|
10120
|
+
"based",
|
|
10121
|
+
"using",
|
|
10122
|
+
"task",
|
|
10123
|
+
"feature",
|
|
10124
|
+
"system",
|
|
10125
|
+
"tool",
|
|
10126
|
+
"mode",
|
|
10127
|
+
"field",
|
|
10128
|
+
"type",
|
|
10129
|
+
"status",
|
|
10130
|
+
"current",
|
|
10131
|
+
"default",
|
|
10132
|
+
"existing",
|
|
10133
|
+
"need",
|
|
10134
|
+
"instead",
|
|
10135
|
+
"allow",
|
|
10136
|
+
"change"
|
|
10137
|
+
]);
|
|
10138
|
+
function extractSearchTerms(title, notes) {
|
|
10139
|
+
const combined = `${title} ${notes ?? ""}`;
|
|
10140
|
+
const camelCase = combined.match(/[a-z][a-zA-Z]{5,}/g) ?? [];
|
|
10141
|
+
const snakeCase = combined.match(/[a-z]+_[a-z_]+/g) ?? [];
|
|
10142
|
+
const hyphenated = combined.match(/[a-z]+-[a-z]+-?[a-z]*/g) ?? [];
|
|
10143
|
+
const filePaths = combined.match(/[\w/.-]+\.(ts|tsx|js|jsx|sql|md)/g) ?? [];
|
|
10144
|
+
const words = combined.toLowerCase().replace(/[^a-z0-9\s_-]/g, " ").split(/\s+/).filter((w) => w.length >= 4 && !STOP_WORDS.has(w));
|
|
10145
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10146
|
+
const terms = [];
|
|
10147
|
+
for (const group of [filePaths, camelCase, snakeCase, hyphenated, words]) {
|
|
10148
|
+
for (const term of group) {
|
|
10149
|
+
const normalized = term.toLowerCase();
|
|
10150
|
+
if (!seen.has(normalized) && normalized.length >= 4) {
|
|
10151
|
+
seen.add(normalized);
|
|
10152
|
+
terms.push(term);
|
|
10153
|
+
}
|
|
10154
|
+
}
|
|
10155
|
+
}
|
|
10156
|
+
return terms.slice(0, 8);
|
|
10157
|
+
}
|
|
10158
|
+
function grepForTerm(projectRoot, term) {
|
|
10159
|
+
try {
|
|
10160
|
+
const result = execSync2(
|
|
10161
|
+
`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`,
|
|
10162
|
+
{ encoding: "utf-8", timeout: 3e3 }
|
|
10163
|
+
);
|
|
10164
|
+
return result.trim().split("\n").filter(Boolean).map(
|
|
10165
|
+
(p) => p.replace(projectRoot + "/", "")
|
|
10166
|
+
);
|
|
10167
|
+
} catch {
|
|
10168
|
+
return [];
|
|
10169
|
+
}
|
|
10170
|
+
}
|
|
10171
|
+
function scanCodebaseForTasks(projectRoot, tasks) {
|
|
10172
|
+
if (tasks.length === 0) return "";
|
|
10173
|
+
const startTime = Date.now();
|
|
10174
|
+
const results = [];
|
|
10175
|
+
for (const task of tasks) {
|
|
10176
|
+
const terms = extractSearchTerms(task.title, task.notes);
|
|
10177
|
+
if (terms.length === 0) continue;
|
|
10178
|
+
const matches = [];
|
|
10179
|
+
for (const term of terms) {
|
|
10180
|
+
if (term.length < 4) continue;
|
|
10181
|
+
const files = grepForTerm(projectRoot, term);
|
|
10182
|
+
if (files.length > 0) {
|
|
10183
|
+
matches.push({ term, files });
|
|
10184
|
+
}
|
|
10185
|
+
if (Date.now() - startTime > 5e3) {
|
|
10186
|
+
console.error(`[codebase-scan] timeout after ${Date.now() - startTime}ms \u2014 partial results returned`);
|
|
10187
|
+
break;
|
|
10188
|
+
}
|
|
10189
|
+
}
|
|
10190
|
+
if (matches.length > 0) {
|
|
10191
|
+
results.push({ taskId: task.id, terms, matches });
|
|
10192
|
+
}
|
|
10193
|
+
if (Date.now() - startTime > 5e3) break;
|
|
10194
|
+
}
|
|
10195
|
+
if (results.length === 0) return "";
|
|
10196
|
+
const elapsed = Date.now() - startTime;
|
|
10197
|
+
console.error(`[codebase-scan] scanned ${tasks.length} tasks in ${elapsed}ms \u2014 ${results.length} with matches`);
|
|
10198
|
+
const lines = [
|
|
10199
|
+
`Codebase scan found existing implementations for ${results.length}/${tasks.length} candidate tasks (${elapsed}ms):`,
|
|
10200
|
+
""
|
|
10201
|
+
];
|
|
10202
|
+
for (const result of results) {
|
|
10203
|
+
lines.push(`**${result.taskId}:**`);
|
|
10204
|
+
for (const match of result.matches.slice(0, 3)) {
|
|
10205
|
+
const fileList = match.files.slice(0, 3).join(", ");
|
|
10206
|
+
const moreCount = match.files.length > 3 ? ` (+${match.files.length - 3} more)` : "";
|
|
10207
|
+
lines.push(` - "${match.term}" \u2192 ${fileList}${moreCount}`);
|
|
10208
|
+
}
|
|
10209
|
+
lines.push("");
|
|
10210
|
+
}
|
|
10211
|
+
return lines.join("\n").trim();
|
|
10212
|
+
}
|
|
10213
|
+
|
|
9948
10214
|
// src/lib/slack.ts
|
|
9949
10215
|
async function sendSlackWebhook(webhookUrl, summary, header = "PAPI Strategy Review") {
|
|
9950
10216
|
if (!webhookUrl) return void 0;
|
|
@@ -10004,6 +10270,9 @@ Task: [title]
|
|
|
10004
10270
|
Cycle: [N]
|
|
10005
10271
|
Why now: [justification]
|
|
10006
10272
|
|
|
10273
|
+
DEPENDS ON
|
|
10274
|
+
[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.]
|
|
10275
|
+
|
|
10007
10276
|
SCOPE (DO THIS)
|
|
10008
10277
|
[specific deliverables \u2014 write for the simplest viable path first]
|
|
10009
10278
|
|
|
@@ -10192,8 +10461,11 @@ Standard planning cycle with full board review.
|
|
|
10192
10461
|
- **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
|
|
10193
10462
|
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
10463
|
**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
|
|
10464
|
+
**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.
|
|
10465
|
+
**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
10466
|
**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\`.
|
|
10467
|
+
**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.
|
|
10468
|
+
**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
10469
|
|
|
10198
10470
|
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
10471
|
**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 +10484,17 @@ Standard planning cycle with full board review.
|
|
|
10212
10484
|
**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
10485
|
**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
10486
|
**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.
|
|
10487
|
+
**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:
|
|
10488
|
+
- Populate the DEPENDS ON section in the dependent task's BUILD HANDOFF with the upstream task ID(s).
|
|
10489
|
+
- 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.
|
|
10490
|
+
- 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".
|
|
10491
|
+
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
10492
|
**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
10493
|
**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
10494
|
**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
10495
|
**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
10496
|
**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.
|
|
10497
|
+
**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
10498
|
**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
10499
|
|
|
10222
10500
|
RESEARCH OUTPUT
|
|
@@ -10234,7 +10512,8 @@ Standard planning cycle with full board review.
|
|
|
10234
10512
|
**Idea task detection:** When a task's task type is "idea", add a scope clarification note to the BUILD HANDOFF:
|
|
10235
10513
|
- 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
10514
|
|
|
10237
|
-
**UI/visual task detection:**
|
|
10515
|
+
**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.
|
|
10516
|
+
When a task IS a UI task (primary scope is visual/frontend):
|
|
10238
10517
|
- 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
10518
|
- 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
10519
|
- 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 +10568,11 @@ var PLAN_FRAGMENT_BUG = `
|
|
|
10289
10568
|
var PLAN_FRAGMENT_IDEA = `
|
|
10290
10569
|
**Idea task detection:** When a task's task type is "idea", add a scope clarification note to the BUILD HANDOFF:
|
|
10291
10570
|
- 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."`;
|
|
10571
|
+
var PLAN_FRAGMENT_TASK = `
|
|
10572
|
+
**Task type detection:** When a task's task type is "task" (generic implementation task), add these handoff sections:
|
|
10573
|
+
- 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").
|
|
10574
|
+
- Add to ACCEPTANCE CRITERIA: "[ ] Scope matches handoff \u2014 no unrelated code changed" and "[ ] Out-of-scope items documented if discovered during implementation."
|
|
10575
|
+
- 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
10576
|
var PLAN_FRAGMENT_SPIKE = `
|
|
10293
10577
|
**Spike task detection:** When a task's task type is "spike" or the title starts with "Spike:", apply these rules:
|
|
10294
10578
|
- Spikes are time-boxed investigations, not implementation tasks. The deliverable is a FINDING, not code.
|
|
@@ -10300,7 +10584,8 @@ var PLAN_FRAGMENT_SPIKE = `
|
|
|
10300
10584
|
- Keep SCOPE BOUNDARY, SECURITY CONSIDERATIONS, and PRE-BUILD VERIFICATION as normal.
|
|
10301
10585
|
- 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
10586
|
var PLAN_FRAGMENT_UI = `
|
|
10303
|
-
**UI/visual task detection:**
|
|
10587
|
+
**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.
|
|
10588
|
+
When a task IS a UI task (primary scope is visual/frontend):
|
|
10304
10589
|
- 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
10590
|
- 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
10591
|
- 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 +10664,11 @@ Standard planning cycle with full board review.
|
|
|
10379
10664
|
- **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
|
|
10380
10665
|
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
10666
|
**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
|
|
10667
|
+
**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.
|
|
10668
|
+
**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
10669
|
**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\`.
|
|
10670
|
+
**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.
|
|
10671
|
+
**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
10672
|
|
|
10385
10673
|
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
10674
|
**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 +10691,14 @@ Standard planning cycle with full board review.
|
|
|
10403
10691
|
**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
10692
|
**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
10693
|
**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
|
|
10694
|
+
**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.
|
|
10695
|
+
**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.
|
|
10696
|
+
**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
10697
|
if (flags.hasResearchTasks) parts.push(PLAN_FRAGMENT_RESEARCH);
|
|
10408
10698
|
if (flags.hasBugTasks) parts.push(PLAN_FRAGMENT_BUG);
|
|
10409
10699
|
if (flags.hasIdeaTasks) parts.push(PLAN_FRAGMENT_IDEA);
|
|
10410
10700
|
if (flags.hasSpikeTasks) parts.push(PLAN_FRAGMENT_SPIKE);
|
|
10701
|
+
if (flags.hasTaskTasks) parts.push(PLAN_FRAGMENT_TASK);
|
|
10411
10702
|
if (flags.hasUITasks) parts.push(PLAN_FRAGMENT_UI);
|
|
10412
10703
|
parts.push(`
|
|
10413
10704
|
11. **New Tasks (max 3 per cycle)** \u2014 Actively mine the Recent Build Reports for task candidates. For each report, check:
|
|
@@ -10472,6 +10763,9 @@ function buildPlanUserMessage(ctx) {
|
|
|
10472
10763
|
);
|
|
10473
10764
|
}
|
|
10474
10765
|
parts.push("", "---", "", "## PROJECT CONTEXT", "");
|
|
10766
|
+
if (ctx.contextTier) {
|
|
10767
|
+
parts.push(`**Context tier:** ${ctx.contextTier}`, "");
|
|
10768
|
+
}
|
|
10475
10769
|
parts.push("### Product Brief", "", ctx.productBrief, "");
|
|
10476
10770
|
if (ctx.northStar) {
|
|
10477
10771
|
parts.push("### North Star (current)", "", ctx.northStar, "");
|
|
@@ -10489,12 +10783,18 @@ function buildPlanUserMessage(ctx) {
|
|
|
10489
10783
|
if (ctx.cycleLog) {
|
|
10490
10784
|
parts.push("### Cycle Log", "", ctx.cycleLog, "");
|
|
10491
10785
|
}
|
|
10786
|
+
if (ctx.strategyReviewCadence) {
|
|
10787
|
+
parts.push("### Strategy Review Cadence (computed from DB)", "", ctx.strategyReviewCadence, "");
|
|
10788
|
+
}
|
|
10492
10789
|
if (ctx.board) {
|
|
10493
10790
|
parts.push("### Board", "", ctx.board, "");
|
|
10494
10791
|
}
|
|
10495
10792
|
if (ctx.preAssignedTasks) {
|
|
10496
10793
|
parts.push("### Pre-Assigned Tasks", "", ctx.preAssignedTasks, "");
|
|
10497
10794
|
}
|
|
10795
|
+
if (ctx.codebaseScan) {
|
|
10796
|
+
parts.push("### Codebase Scan (existing implementations)", "", ctx.codebaseScan, "");
|
|
10797
|
+
}
|
|
10498
10798
|
if (ctx.buildPatterns) {
|
|
10499
10799
|
parts.push("### Build Patterns", "", ctx.buildPatterns, "");
|
|
10500
10800
|
}
|
|
@@ -10845,7 +11145,8 @@ After your natural language output, include this EXACT format on its own line:
|
|
|
10845
11145
|
"category": "friction | methodology | signal | commercial",
|
|
10846
11146
|
"content": "string \u2014 specific observation from using PAPI on this project (e.g. 'deprioritise clears handoffs unnecessarily, wasting planner tokens')"
|
|
10847
11147
|
}
|
|
10848
|
-
]
|
|
11148
|
+
],
|
|
11149
|
+
"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
11150
|
}
|
|
10850
11151
|
\`\`\`
|
|
10851
11152
|
|
|
@@ -11032,7 +11333,8 @@ After your natural language output, include this EXACT format on its own line:
|
|
|
11032
11333
|
},
|
|
11033
11334
|
"oldLabel": "string \u2014 only for modify/remove: the previous phase label so tasks can be migrated"
|
|
11034
11335
|
}
|
|
11035
|
-
]
|
|
11336
|
+
],
|
|
11337
|
+
"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
11338
|
}
|
|
11037
11339
|
\`\`\`
|
|
11038
11340
|
|
|
@@ -11088,6 +11390,9 @@ Task: [title]
|
|
|
11088
11390
|
Cycle: [N]
|
|
11089
11391
|
Why now: [justification]
|
|
11090
11392
|
|
|
11393
|
+
DEPENDS ON
|
|
11394
|
+
[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.]
|
|
11395
|
+
|
|
11091
11396
|
SCOPE (DO THIS)
|
|
11092
11397
|
[specific deliverables \u2014 write for the simplest viable path first]
|
|
11093
11398
|
|
|
@@ -11388,6 +11693,32 @@ async function getPrompt(name) {
|
|
|
11388
11693
|
}
|
|
11389
11694
|
|
|
11390
11695
|
// src/services/plan.ts
|
|
11696
|
+
function determineContextTier(cycleCount) {
|
|
11697
|
+
if (cycleCount <= 5) return 1;
|
|
11698
|
+
if (cycleCount <= 20) return 2;
|
|
11699
|
+
return 3;
|
|
11700
|
+
}
|
|
11701
|
+
function applyContextTier(ctx, cycleCount) {
|
|
11702
|
+
const tier = determineContextTier(cycleCount);
|
|
11703
|
+
const label = tier === 1 ? "Tier 1 (cycles 1-5)" : tier === 2 ? "Tier 2 (cycles 6-20)" : "Tier 3 (cycles 21+)";
|
|
11704
|
+
if (tier <= 2) {
|
|
11705
|
+
ctx.strategyRecommendations = void 0;
|
|
11706
|
+
ctx.dogfoodEntries = void 0;
|
|
11707
|
+
}
|
|
11708
|
+
if (tier === 1) {
|
|
11709
|
+
ctx.methodologyMetrics = void 0;
|
|
11710
|
+
ctx.carryForwardStaleness = void 0;
|
|
11711
|
+
ctx.discoveryCanvas = void 0;
|
|
11712
|
+
ctx.estimationCalibration = void 0;
|
|
11713
|
+
ctx.buildPatterns = void 0;
|
|
11714
|
+
ctx.reviewPatterns = void 0;
|
|
11715
|
+
ctx.horizonContext = void 0;
|
|
11716
|
+
ctx.registeredDocs = void 0;
|
|
11717
|
+
ctx.recentReviews = void 0;
|
|
11718
|
+
ctx.strategyReviewCadence = void 0;
|
|
11719
|
+
}
|
|
11720
|
+
return { tier, label };
|
|
11721
|
+
}
|
|
11391
11722
|
function determineMode(totalCycles) {
|
|
11392
11723
|
if (totalCycles === 0) return "bootstrap";
|
|
11393
11724
|
return "full";
|
|
@@ -11622,6 +11953,7 @@ function detectBoardFlags(tasks) {
|
|
|
11622
11953
|
let hasResearchTasks = false;
|
|
11623
11954
|
let hasIdeaTasks = false;
|
|
11624
11955
|
let hasSpikeTasks = false;
|
|
11956
|
+
let hasTaskTasks = false;
|
|
11625
11957
|
let hasUITasks = false;
|
|
11626
11958
|
const uiKeywords = /\b(visual|design|UI|styling|refresh|frontend|landing page|hero|carousel|theme|layout|cockpit|dashboard|page)\b/i;
|
|
11627
11959
|
for (const t of tasks) {
|
|
@@ -11629,9 +11961,10 @@ function detectBoardFlags(tasks) {
|
|
|
11629
11961
|
if (t.taskType === "research" || /^Research:/i.test(t.title)) hasResearchTasks = true;
|
|
11630
11962
|
if (t.taskType === "idea") hasIdeaTasks = true;
|
|
11631
11963
|
if (t.taskType === "spike" || /^Spike:/i.test(t.title)) hasSpikeTasks = true;
|
|
11964
|
+
if (t.taskType === "task") hasTaskTasks = true;
|
|
11632
11965
|
if (uiKeywords.test(t.title) || uiKeywords.test(t.notes ?? "")) hasUITasks = true;
|
|
11633
11966
|
}
|
|
11634
|
-
return { hasBugTasks, hasResearchTasks, hasIdeaTasks, hasSpikeTasks, hasUITasks };
|
|
11967
|
+
return { hasBugTasks, hasResearchTasks, hasIdeaTasks, hasSpikeTasks, hasTaskTasks, hasUITasks };
|
|
11635
11968
|
}
|
|
11636
11969
|
function detectBoardFlagsFromText(boardText) {
|
|
11637
11970
|
return {
|
|
@@ -11639,6 +11972,7 @@ function detectBoardFlagsFromText(boardText) {
|
|
|
11639
11972
|
hasResearchTasks: /\b(research|Research:)\b/i.test(boardText),
|
|
11640
11973
|
hasIdeaTasks: /\bidea\b/i.test(boardText),
|
|
11641
11974
|
hasSpikeTasks: /\b(spike|Spike:)\b/i.test(boardText),
|
|
11975
|
+
hasTaskTasks: /\btask\b/i.test(boardText),
|
|
11642
11976
|
hasUITasks: /\b(visual|design|UI|styling|refresh|frontend|landing page|hero|carousel|theme|layout|cockpit|dashboard|page)\b/i.test(boardText)
|
|
11643
11977
|
};
|
|
11644
11978
|
}
|
|
@@ -11849,6 +12183,9 @@ ${lines.join("\n")}`;
|
|
|
11849
12183
|
]);
|
|
11850
12184
|
timings["total"] = totalTimer();
|
|
11851
12185
|
console.error(`[plan-perf] assembleContext (lean): ${JSON.stringify(timings)}ms`);
|
|
12186
|
+
const gap = health.cyclesSinceLastStrategyReview;
|
|
12187
|
+
const lastReviewCycle = health.totalCycles - gap;
|
|
12188
|
+
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
12189
|
let ctx2 = {
|
|
11853
12190
|
mode,
|
|
11854
12191
|
cycleNumber: health.totalCycles,
|
|
@@ -11870,8 +12207,12 @@ ${lines.join("\n")}`;
|
|
|
11870
12207
|
boardFlags,
|
|
11871
12208
|
carryForwardStaleness: carryForwardStalenessLean,
|
|
11872
12209
|
preAssignedTasks: preAssignedTextLean,
|
|
11873
|
-
recentlyShippedCapabilities: recentlyShippedLean
|
|
12210
|
+
recentlyShippedCapabilities: recentlyShippedLean,
|
|
12211
|
+
strategyReviewCadence
|
|
11874
12212
|
};
|
|
12213
|
+
const { label: leanTierLabel } = applyContextTier(ctx2, health.totalCycles);
|
|
12214
|
+
ctx2.contextTier = leanTierLabel;
|
|
12215
|
+
console.error(`[plan-perf] context tier: ${leanTierLabel} (cycle ${health.totalCycles})`);
|
|
11875
12216
|
t = startTimer();
|
|
11876
12217
|
const prevHashes2 = contextHashesResult.status === "fulfilled" ? contextHashesResult.value : null;
|
|
11877
12218
|
const { ctx: diffedCtx2, newHashes: newHashes2, savedBytes: savedBytes2 } = applyContextDiff(ctx2, prevHashes2);
|
|
@@ -11933,7 +12274,8 @@ ${lines.join("\n")}`;
|
|
|
11933
12274
|
if (pendingRecsResultFull.status === "fulfilled" && pendingRecsResultFull.value.length > 0) {
|
|
11934
12275
|
strategyRecommendationsText = formatStrategyRecommendations(pendingRecsResultFull.value);
|
|
11935
12276
|
}
|
|
11936
|
-
const
|
|
12277
|
+
const filteredRaw = rawMetricsSnapshots.filter((s) => s.accuracy.length > 0 || s.velocity.length > 0);
|
|
12278
|
+
const metricsSnapshots = filteredRaw.length > 0 ? filteredRaw : computeSnapshotsFromBuildReports(allReportsForPatterns);
|
|
11937
12279
|
const discoveryCanvasTextFull = discoveryCanvasResultFull.status === "fulfilled" ? discoveryCanvasResultFull.value : void 0;
|
|
11938
12280
|
const taskCommentsTextFull = taskCommentsResultFull.status === "fulfilled" ? taskCommentsResultFull.value : void 0;
|
|
11939
12281
|
let registeredDocsTextFull;
|
|
@@ -11969,6 +12311,9 @@ ${lines.join("\n")}`;
|
|
|
11969
12311
|
const targetCycle = health.totalCycles + 1;
|
|
11970
12312
|
const preAssigned = strippedTasks.filter((t2) => t2.cycle === targetCycle);
|
|
11971
12313
|
const preAssignedText = formatPreAssignedTasks(preAssigned, targetCycle);
|
|
12314
|
+
const gapFull = health.cyclesSinceLastStrategyReview;
|
|
12315
|
+
const lastReviewCycleFull = health.totalCycles - gapFull;
|
|
12316
|
+
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
12317
|
let ctx = {
|
|
11973
12318
|
mode,
|
|
11974
12319
|
cycleNumber: health.totalCycles,
|
|
@@ -11992,8 +12337,12 @@ ${lines.join("\n")}`;
|
|
|
11992
12337
|
boardFlags: boardFlagsFull,
|
|
11993
12338
|
carryForwardStaleness: computeCarryForwardStaleness(log),
|
|
11994
12339
|
preAssignedTasks: preAssignedText,
|
|
11995
|
-
recentlyShippedCapabilities: formatRecentlyShippedCapabilities(
|
|
12340
|
+
recentlyShippedCapabilities: formatRecentlyShippedCapabilities(reports),
|
|
12341
|
+
strategyReviewCadence: strategyReviewCadenceFull
|
|
11996
12342
|
};
|
|
12343
|
+
const { label: fullTierLabel } = applyContextTier(ctx, health.totalCycles);
|
|
12344
|
+
ctx.contextTier = fullTierLabel;
|
|
12345
|
+
console.error(`[plan-perf] context tier: ${fullTierLabel} (cycle ${health.totalCycles})`);
|
|
11997
12346
|
const prevHashes = contextHashesResultFull.status === "fulfilled" ? contextHashesResultFull.value : null;
|
|
11998
12347
|
const { ctx: diffedCtx, newHashes, savedBytes } = applyContextDiff(ctx, prevHashes);
|
|
11999
12348
|
ctx = diffedCtx;
|
|
@@ -12183,7 +12532,15 @@ ${cleanContent}`;
|
|
|
12183
12532
|
taskCount: cycleTaskCount > 0 ? cycleTaskCount : void 0,
|
|
12184
12533
|
effortPoints: cycleEffortPoints > 0 ? cycleEffortPoints : void 0
|
|
12185
12534
|
});
|
|
12186
|
-
const
|
|
12535
|
+
const healthUpdates = {
|
|
12536
|
+
totalCycles: newCycleNumber,
|
|
12537
|
+
boardHealth: data.boardHealth,
|
|
12538
|
+
strategicDirection: data.strategicDirection
|
|
12539
|
+
};
|
|
12540
|
+
if (data.nextMode === "Full") {
|
|
12541
|
+
healthUpdates.lastFullMode = newCycleNumber;
|
|
12542
|
+
}
|
|
12543
|
+
const healthPromise = adapter2.setCycleHealth(healthUpdates);
|
|
12187
12544
|
const newTaskIdMap = /* @__PURE__ */ new Map();
|
|
12188
12545
|
const createTasksPromise = (async () => {
|
|
12189
12546
|
if (!data.newTasks || data.newTasks.length === 0) return;
|
|
@@ -12603,6 +12960,17 @@ async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnl
|
|
|
12603
12960
|
}
|
|
12604
12961
|
if (skipHandoffs) context.skipHandoffs = true;
|
|
12605
12962
|
t = startTimer();
|
|
12963
|
+
try {
|
|
12964
|
+
const scanTasks = await adapter2.queryBoard({ status: ["Backlog", "In Cycle", "Ready"] });
|
|
12965
|
+
const candidates = scanTasks.filter((task) => task.priority !== "P3 Low").slice(0, 15).map((task) => ({ id: task.id, title: task.title, notes: task.notes }));
|
|
12966
|
+
const scanResult = scanCodebaseForTasks(config2.projectRoot, candidates);
|
|
12967
|
+
if (scanResult) context.codebaseScan = scanResult;
|
|
12968
|
+
} catch (err) {
|
|
12969
|
+
console.error(`[plan] codebase scan failed (non-critical): ${err instanceof Error ? err.message : err}`);
|
|
12970
|
+
}
|
|
12971
|
+
const scanMs = t();
|
|
12972
|
+
console.error(`[plan-perf] codebaseScan: ${scanMs}ms`);
|
|
12973
|
+
t = startTimer();
|
|
12606
12974
|
const userMessage = buildPlanUserMessage(context);
|
|
12607
12975
|
const buildMessageMs = t();
|
|
12608
12976
|
const totalMs = prepareTimer();
|
|
@@ -12779,6 +13147,7 @@ var lastPrepareSkipHandoffs;
|
|
|
12779
13147
|
var planTool = {
|
|
12780
13148
|
name: "plan",
|
|
12781
13149
|
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`.',
|
|
13150
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
12782
13151
|
inputSchema: {
|
|
12783
13152
|
type: "object",
|
|
12784
13153
|
properties: {
|
|
@@ -12906,24 +13275,30 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
12906
13275
|
return errorResponse('llm_response is required for mode "apply". Pass your complete plan output including the <!-- PAPI_STRUCTURED_OUTPUT --> block.');
|
|
12907
13276
|
}
|
|
12908
13277
|
const planMode = args.plan_mode || "full";
|
|
12909
|
-
const
|
|
13278
|
+
const rawCycleNumber = args.cycle_number != null ? Number(args.cycle_number) : NaN;
|
|
12910
13279
|
const strategyReviewWarning = args.strategy_review_warning || "";
|
|
12911
13280
|
const contextHashes = lastPrepareContextHashes;
|
|
12912
13281
|
const inputContext = lastPrepareUserMessage;
|
|
12913
13282
|
const contextBytes = lastPrepareContextBytes;
|
|
12914
13283
|
const expectedCycleNumber = lastPrepareCycleNumber;
|
|
12915
13284
|
const skipHandoffsCached = lastPrepareSkipHandoffs;
|
|
12916
|
-
lastPrepareContextHashes = void 0;
|
|
12917
|
-
lastPrepareUserMessage = void 0;
|
|
12918
|
-
lastPrepareContextBytes = void 0;
|
|
12919
|
-
lastPrepareCycleNumber = void 0;
|
|
12920
|
-
lastPrepareSkipHandoffs = void 0;
|
|
12921
13285
|
const skipHandoffs = args.skip_handoffs === true || skipHandoffsCached === true;
|
|
13286
|
+
const cycleNumber = !isNaN(rawCycleNumber) ? rawCycleNumber : expectedCycleNumber !== void 0 ? expectedCycleNumber : NaN;
|
|
13287
|
+
if (isNaN(cycleNumber)) {
|
|
13288
|
+
return errorResponse(
|
|
13289
|
+
"cycle_number is required for apply mode. Pass the cycle_number from the prepare phase output."
|
|
13290
|
+
);
|
|
13291
|
+
}
|
|
12922
13292
|
if (expectedCycleNumber !== void 0 && cycleNumber !== expectedCycleNumber) {
|
|
12923
13293
|
return errorResponse(
|
|
12924
13294
|
`cycle_number mismatch: prepare phase returned cycle ${expectedCycleNumber} but apply received ${cycleNumber}. Pass cycle_number: ${expectedCycleNumber} to match the prepare output.`
|
|
12925
13295
|
);
|
|
12926
13296
|
}
|
|
13297
|
+
lastPrepareContextHashes = void 0;
|
|
13298
|
+
lastPrepareUserMessage = void 0;
|
|
13299
|
+
lastPrepareContextBytes = void 0;
|
|
13300
|
+
lastPrepareCycleNumber = void 0;
|
|
13301
|
+
lastPrepareSkipHandoffs = void 0;
|
|
12927
13302
|
const result = await applyPlan(adapter2, config2, llmResponse, planMode, cycleNumber, strategyReviewWarning, contextHashes, { contextBytes: contextBytes ?? void 0, skipHandoffs: skipHandoffs || void 0 });
|
|
12928
13303
|
let utilisation;
|
|
12929
13304
|
if (inputContext) {
|
|
@@ -13122,7 +13497,7 @@ function classifyRecommendation(text) {
|
|
|
13122
13497
|
if (lower.includes("new task") || lower.includes("create task") || lower.includes("add task") || lower.includes("spike")) {
|
|
13123
13498
|
return "new_task";
|
|
13124
13499
|
}
|
|
13125
|
-
if (lower.includes("process") || lower.includes("workflow") || lower.includes("methodology") || lower.includes("retrospective") || lower.includes("dogfood")) {
|
|
13500
|
+
if (lower.includes("process") || lower.includes("workflow") || lower.includes("methodology") || lower.includes("retrospective") || lower.includes("dogfood") || lower.includes("refine")) {
|
|
13126
13501
|
return "process_improvement";
|
|
13127
13502
|
}
|
|
13128
13503
|
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 +14157,12 @@ ${cleanContent}`;
|
|
|
13782
14157
|
} catch {
|
|
13783
14158
|
}
|
|
13784
14159
|
}
|
|
14160
|
+
if (data.northStar && adapter2.upsertNorthStar) {
|
|
14161
|
+
try {
|
|
14162
|
+
await adapter2.upsertNorthStar(data.northStar, cycleNumber);
|
|
14163
|
+
} catch {
|
|
14164
|
+
}
|
|
14165
|
+
}
|
|
13785
14166
|
const compressionThreshold = cycleNumber - 5;
|
|
13786
14167
|
if (compressionThreshold > 0 && data.sessionLogCompressionSummary) {
|
|
13787
14168
|
await adapter2.compressCycleLog(compressionThreshold, data.sessionLogCompressionSummary);
|
|
@@ -13841,7 +14222,7 @@ ${cleanContent}`;
|
|
|
13841
14222
|
try {
|
|
13842
14223
|
const canvas = await adapter2.readDiscoveryCanvas();
|
|
13843
14224
|
const updates = {};
|
|
13844
|
-
|
|
14225
|
+
const populatedSections = [];
|
|
13845
14226
|
if (!canvas.landscapeReferences || canvas.landscapeReferences.length === 0) {
|
|
13846
14227
|
if (data.activeDecisionUpdates?.length) {
|
|
13847
14228
|
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 +14485,17 @@ function formatVelocitySummary(reports, cycleCount) {
|
|
|
14104
14485
|
function formatRecentReportsSummary(reports, count) {
|
|
14105
14486
|
const recent = reports.sort((a, b2) => b2.cycle - a.cycle || b2.date.localeCompare(a.date)).slice(0, count);
|
|
14106
14487
|
if (recent.length === 0) return "No recent build reports.";
|
|
14488
|
+
const trunc = (s, max) => s && s !== "None" ? s.length > max ? s.slice(0, max) + "..." : s : null;
|
|
14107
14489
|
return recent.map((r) => {
|
|
14108
14490
|
const effort = `${r.actualEffort} vs ${r.estimatedEffort}`;
|
|
14109
|
-
const
|
|
14110
|
-
|
|
14491
|
+
const lines = [`- C${r.cycle} ${r.taskName}: ${effort}`];
|
|
14492
|
+
const surprises = trunc(r.surprises, 200);
|
|
14493
|
+
if (surprises) lines.push(` _Surprises:_ ${surprises}`);
|
|
14494
|
+
const issues = trunc(r.discoveredIssues, 200);
|
|
14495
|
+
if (issues) lines.push(` _Issues:_ ${issues}`);
|
|
14496
|
+
const arch = trunc(r.architectureNotes, 200);
|
|
14497
|
+
if (arch) lines.push(` _Architecture:_ ${arch}`);
|
|
14498
|
+
return lines.join("\n");
|
|
14111
14499
|
}).join("\n");
|
|
14112
14500
|
}
|
|
14113
14501
|
function formatPhasesForReview(phases, currentCycle) {
|
|
@@ -14133,7 +14521,7 @@ async function formatHierarchyForReview(adapter2, currentCycle, prefetchedTasks)
|
|
|
14133
14521
|
} catch {
|
|
14134
14522
|
}
|
|
14135
14523
|
if (horizons.length === 0 && phases.length === 0) return void 0;
|
|
14136
|
-
|
|
14524
|
+
const tasksByPhase = /* @__PURE__ */ new Map();
|
|
14137
14525
|
try {
|
|
14138
14526
|
const allTasks = prefetchedTasks ?? await adapter2.queryBoard();
|
|
14139
14527
|
for (const t of allTasks) {
|
|
@@ -14278,6 +14666,12 @@ ${cleanContent}`;
|
|
|
14278
14666
|
const currentPhases = await adapter2.readPhases();
|
|
14279
14667
|
await applyPhaseUpdates(adapter2, currentPhases, data.phaseUpdates);
|
|
14280
14668
|
}
|
|
14669
|
+
if (data.northStar && adapter2.upsertNorthStar) {
|
|
14670
|
+
try {
|
|
14671
|
+
await adapter2.upsertNorthStar(data.northStar, cycleNumber);
|
|
14672
|
+
} catch {
|
|
14673
|
+
}
|
|
14674
|
+
}
|
|
14281
14675
|
} catch (err) {
|
|
14282
14676
|
writeBackFailed = err instanceof Error ? err.message : String(err);
|
|
14283
14677
|
}
|
|
@@ -14400,6 +14794,7 @@ var lastReviewContextBytes;
|
|
|
14400
14794
|
var strategyReviewTool = {
|
|
14401
14795
|
name: "strategy_review",
|
|
14402
14796
|
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.',
|
|
14797
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
14403
14798
|
inputSchema: {
|
|
14404
14799
|
type: "object",
|
|
14405
14800
|
properties: {
|
|
@@ -14427,6 +14822,7 @@ var strategyReviewTool = {
|
|
|
14427
14822
|
var strategyChangeTool = {
|
|
14428
14823
|
name: "strategy_change",
|
|
14429
14824
|
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.',
|
|
14825
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
14430
14826
|
inputSchema: {
|
|
14431
14827
|
type: "object",
|
|
14432
14828
|
properties: {
|
|
@@ -14764,6 +15160,7 @@ async function archiveTasks(adapter2, phases, statuses) {
|
|
|
14764
15160
|
var boardViewTool = {
|
|
14765
15161
|
name: "board_view",
|
|
14766
15162
|
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.',
|
|
15163
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
14767
15164
|
inputSchema: {
|
|
14768
15165
|
type: "object",
|
|
14769
15166
|
properties: {
|
|
@@ -14795,6 +15192,7 @@ var boardViewTool = {
|
|
|
14795
15192
|
var boardDeprioritiseTool = {
|
|
14796
15193
|
name: "board_deprioritise",
|
|
14797
15194
|
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.`,
|
|
15195
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
14798
15196
|
inputSchema: {
|
|
14799
15197
|
type: "object",
|
|
14800
15198
|
properties: {
|
|
@@ -14830,6 +15228,7 @@ var boardDeprioritiseTool = {
|
|
|
14830
15228
|
var boardArchiveTool = {
|
|
14831
15229
|
name: "board_archive",
|
|
14832
15230
|
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.",
|
|
15231
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
14833
15232
|
inputSchema: {
|
|
14834
15233
|
type: "object",
|
|
14835
15234
|
properties: {
|
|
@@ -14848,6 +15247,7 @@ var boardArchiveTool = {
|
|
|
14848
15247
|
var boardEditTool = {
|
|
14849
15248
|
name: "board_edit",
|
|
14850
15249
|
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.",
|
|
15250
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
14851
15251
|
inputSchema: {
|
|
14852
15252
|
type: "object",
|
|
14853
15253
|
properties: {
|
|
@@ -15114,7 +15514,7 @@ async function handleBoardEdit(adapter2, args) {
|
|
|
15114
15514
|
try {
|
|
15115
15515
|
const dogfoodLog = await adapter2.getDogfoodLog?.(50) ?? [];
|
|
15116
15516
|
const linked = dogfoodLog.filter((e) => e.linkedTaskId === taskId || e.linkedTaskId === task.id);
|
|
15117
|
-
const newStatus =
|
|
15517
|
+
const newStatus = "resolved";
|
|
15118
15518
|
await Promise.all(linked.map((e) => adapter2.updateDogfoodEntryStatus(e.id, newStatus)));
|
|
15119
15519
|
} catch {
|
|
15120
15520
|
}
|
|
@@ -15486,6 +15886,7 @@ When the system compresses prior messages, immediately:
|
|
|
15486
15886
|
|
|
15487
15887
|
- **XS/S tasks in the same cycle and module:** Group on shared branch. One PR, one merge.
|
|
15488
15888
|
- **M/L tasks or different modules:** Own branch per task. Isolated PRs.
|
|
15889
|
+
- **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
15890
|
- **Commit per task within grouped branches** \u2014 traceable git history.
|
|
15490
15891
|
- **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
15892
|
|
|
@@ -16285,6 +16686,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
|
|
|
16285
16686
|
var setupTool = {
|
|
16286
16687
|
name: "setup",
|
|
16287
16688
|
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.`,
|
|
16689
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
16288
16690
|
inputSchema: {
|
|
16289
16691
|
type: "object",
|
|
16290
16692
|
properties: {
|
|
@@ -16554,6 +16956,8 @@ import { randomUUID as randomUUID9 } from "crypto";
|
|
|
16554
16956
|
import { readdirSync as readdirSync3, existsSync as existsSync3, readFileSync } from "fs";
|
|
16555
16957
|
import { join as join5 } from "path";
|
|
16556
16958
|
var buildStartTimes = /* @__PURE__ */ new Map();
|
|
16959
|
+
var taskBranchMap = /* @__PURE__ */ new Map();
|
|
16960
|
+
var SHARED_BRANCH_COMPLEXITIES = /* @__PURE__ */ new Set(["XS", "Small"]);
|
|
16557
16961
|
function capitalizeCompleted(value) {
|
|
16558
16962
|
const map = {
|
|
16559
16963
|
yes: "Yes",
|
|
@@ -16573,7 +16977,7 @@ function pushAndCreatePR(config2, taskId, taskTitle) {
|
|
|
16573
16977
|
if (!isGitAvailable() || !isGitRepo(config2.projectRoot)) {
|
|
16574
16978
|
return lines;
|
|
16575
16979
|
}
|
|
16576
|
-
const featureBranch = taskBranchName(taskId);
|
|
16980
|
+
const featureBranch = taskBranchMap.get(taskId) ?? taskBranchName(taskId);
|
|
16577
16981
|
const currentBranch = getCurrentBranch(config2.projectRoot);
|
|
16578
16982
|
if (currentBranch !== featureBranch) {
|
|
16579
16983
|
return lines;
|
|
@@ -16705,10 +17109,38 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
|
|
|
16705
17109
|
if (options.light) {
|
|
16706
17110
|
branchLines.push("Light mode: skipping branch creation \u2014 working on current branch.");
|
|
16707
17111
|
} else if (config2.autoCommit && isGitAvailable() && isGitRepo(config2.projectRoot)) {
|
|
16708
|
-
const
|
|
17112
|
+
const cycleHealth = await adapter2.getCycleHealth().catch(() => null);
|
|
17113
|
+
const cycleNumber = cycleHealth?.totalCycles ?? 0;
|
|
17114
|
+
let depBranchReuse = null;
|
|
17115
|
+
if (task.dependsOn) {
|
|
17116
|
+
const depIds = task.dependsOn.split(",").map((d) => d.trim()).filter(Boolean);
|
|
17117
|
+
for (const depId of depIds) {
|
|
17118
|
+
const mappedBranch = taskBranchMap.get(depId);
|
|
17119
|
+
if (mappedBranch && branchExists(config2.projectRoot, mappedBranch)) {
|
|
17120
|
+
depBranchReuse = { branch: mappedBranch, upstreamId: depId };
|
|
17121
|
+
break;
|
|
17122
|
+
}
|
|
17123
|
+
const fallbackBranch = taskBranchName(depId);
|
|
17124
|
+
if (branchExists(config2.projectRoot, fallbackBranch)) {
|
|
17125
|
+
depBranchReuse = { branch: fallbackBranch, upstreamId: depId };
|
|
17126
|
+
break;
|
|
17127
|
+
}
|
|
17128
|
+
}
|
|
17129
|
+
}
|
|
17130
|
+
const useSharedBranch = !depBranchReuse && SHARED_BRANCH_COMPLEXITIES.has(task.complexity) && !!task.module && cycleNumber > 0;
|
|
17131
|
+
const featureBranch = depBranchReuse ? depBranchReuse.branch : useSharedBranch ? cycleBranchName(cycleNumber, task.module) : taskBranchName(taskId);
|
|
17132
|
+
if (depBranchReuse) {
|
|
17133
|
+
branchLines.push(
|
|
17134
|
+
`Reusing branch '${depBranchReuse.branch}' from dependency ${depBranchReuse.upstreamId} \u2014 commits will stack for a single PR.`
|
|
17135
|
+
);
|
|
17136
|
+
}
|
|
17137
|
+
taskBranchMap.set(taskId, featureBranch);
|
|
16709
17138
|
const currentBranch = getCurrentBranch(config2.projectRoot);
|
|
16710
17139
|
if (currentBranch === featureBranch) {
|
|
16711
17140
|
branchLines.push(`Already on branch '${featureBranch}'.`);
|
|
17141
|
+
if (useSharedBranch) {
|
|
17142
|
+
branchLines.push(`Reusing shared cycle branch for ${task.complexity} ${task.module} task.`);
|
|
17143
|
+
}
|
|
16712
17144
|
} else {
|
|
16713
17145
|
if (hasUncommittedChanges(config2.projectRoot, AUTO_WRITTEN_PATHS)) {
|
|
16714
17146
|
throw new Error("Working directory has uncommitted changes. Please commit or stash them before running `build_execute`.");
|
|
@@ -16717,13 +17149,14 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
|
|
|
16717
17149
|
if (baseBranch !== config2.baseBranch) {
|
|
16718
17150
|
branchLines.push(`Base branch '${config2.baseBranch}' not found \u2014 using '${baseBranch}'.`);
|
|
16719
17151
|
}
|
|
16720
|
-
|
|
17152
|
+
const featureBranchExists = branchExists(config2.projectRoot, featureBranch);
|
|
17153
|
+
if (currentBranch !== baseBranch && !featureBranchExists) {
|
|
16721
17154
|
const checkout = checkoutBranch(config2.projectRoot, baseBranch);
|
|
16722
17155
|
if (!checkout.success) {
|
|
16723
17156
|
branchLines.push(`Warning: ${checkout.message} Proceeding on current branch '${currentBranch}'.`);
|
|
16724
17157
|
}
|
|
16725
17158
|
}
|
|
16726
|
-
if (hasRemote(config2.projectRoot)) {
|
|
17159
|
+
if (hasRemote(config2.projectRoot) && !featureBranchExists) {
|
|
16727
17160
|
const pull = gitPull(config2.projectRoot);
|
|
16728
17161
|
branchLines.push(pull.success ? pull.message : `Warning: ${pull.message}`);
|
|
16729
17162
|
}
|
|
@@ -16733,10 +17166,10 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
|
|
|
16733
17166
|
`Warning: ${unmerged.length} unmerged feature branch${unmerged.length === 1 ? "" : "es"}: ${unmerged.join(", ")}. New branch may diverge if these contain changes needed here.`
|
|
16734
17167
|
);
|
|
16735
17168
|
}
|
|
16736
|
-
if (
|
|
17169
|
+
if (featureBranchExists) {
|
|
16737
17170
|
const checkout = checkoutBranch(config2.projectRoot, featureBranch);
|
|
16738
17171
|
branchLines.push(
|
|
16739
|
-
checkout.success ? `Checked out existing branch '${featureBranch}'.` : `Warning: ${checkout.message}`
|
|
17172
|
+
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
17173
|
);
|
|
16741
17174
|
if (checkout.success) {
|
|
16742
17175
|
branchLines.push(
|
|
@@ -16746,7 +17179,7 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
|
|
|
16746
17179
|
} else {
|
|
16747
17180
|
const create = createAndCheckoutBranch(config2.projectRoot, featureBranch);
|
|
16748
17181
|
branchLines.push(
|
|
16749
|
-
create.success ? `Created branch '${featureBranch}'.` : `Warning: ${create.message}`
|
|
17182
|
+
create.success ? useSharedBranch ? `Created shared cycle branch '${featureBranch}' for ${task.module} XS/S tasks.` : `Created branch '${featureBranch}'.` : `Warning: ${create.message}`
|
|
16750
17183
|
);
|
|
16751
17184
|
if (create.success) {
|
|
16752
17185
|
branchLines.push(
|
|
@@ -16830,11 +17263,27 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
16830
17263
|
completedAt: now.toISOString()
|
|
16831
17264
|
};
|
|
16832
17265
|
buildStartTimes.delete(taskId);
|
|
17266
|
+
taskBranchMap.delete(taskId);
|
|
16833
17267
|
if (input.relatedDecisions) {
|
|
16834
17268
|
const adIds = input.relatedDecisions.split(",").map((s) => s.trim()).filter(Boolean);
|
|
16835
17269
|
if (adIds.length > 0) report.relatedDecisions = adIds;
|
|
16836
17270
|
}
|
|
17271
|
+
if (report.startedAt && report.completedAt && typeof adapter2.getToolCallCount === "function") {
|
|
17272
|
+
try {
|
|
17273
|
+
const count = await adapter2.getToolCallCount(report.startedAt, report.completedAt);
|
|
17274
|
+
if (count > 0) report.toolCallCount = count;
|
|
17275
|
+
} catch {
|
|
17276
|
+
}
|
|
17277
|
+
}
|
|
16837
17278
|
await adapter2.appendBuildReport(report);
|
|
17279
|
+
let reportWriteVerified;
|
|
17280
|
+
if (typeof adapter2.getBuildReportCountForTask === "function") {
|
|
17281
|
+
try {
|
|
17282
|
+
const postWriteCount = await adapter2.getBuildReportCountForTask(taskId);
|
|
17283
|
+
reportWriteVerified = postWriteCount >= iterationCount;
|
|
17284
|
+
} catch {
|
|
17285
|
+
}
|
|
17286
|
+
}
|
|
16838
17287
|
if (adapter2.appendCycleLearnings) {
|
|
16839
17288
|
const learnings = [];
|
|
16840
17289
|
const taskModule = task.module ?? "";
|
|
@@ -16881,6 +17330,39 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
16881
17330
|
}
|
|
16882
17331
|
}
|
|
16883
17332
|
}
|
|
17333
|
+
let autoTriagedCount = 0;
|
|
17334
|
+
if (input.discoveredIssues && input.discoveredIssues !== "None" && typeof adapter2.createTask === "function") {
|
|
17335
|
+
const issueLines = input.discoveredIssues.split(/\n|;/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
17336
|
+
for (const line of issueLines) {
|
|
17337
|
+
const sevMatch = line.match(/^(P[0-3])[\s:]+/i);
|
|
17338
|
+
if (!sevMatch) continue;
|
|
17339
|
+
const severityLabel = sevMatch[1].toUpperCase();
|
|
17340
|
+
const priority = severityLabel === "P0" || severityLabel === "P1" ? "P1 High" : severityLabel === "P2" ? "P2 Medium" : "P3 Low";
|
|
17341
|
+
const titleRaw = line.replace(/^P[0-3][\s:]+/i, "").trim();
|
|
17342
|
+
const title = titleRaw.length > 120 ? titleRaw.slice(0, 120) : titleRaw;
|
|
17343
|
+
if (!title) continue;
|
|
17344
|
+
try {
|
|
17345
|
+
await adapter2.createTask({
|
|
17346
|
+
uuid: "",
|
|
17347
|
+
displayId: "",
|
|
17348
|
+
title: `[Auto-triaged] ${title}`,
|
|
17349
|
+
status: "Backlog",
|
|
17350
|
+
priority,
|
|
17351
|
+
complexity: "Small",
|
|
17352
|
+
module: task.module ?? "",
|
|
17353
|
+
phase: task.phase ?? "",
|
|
17354
|
+
owner: "papi",
|
|
17355
|
+
reviewed: false,
|
|
17356
|
+
taskType: "discovery",
|
|
17357
|
+
source: "build_complete",
|
|
17358
|
+
notes: `Origin: ${task.displayId} (${task.title}), cycle ${cycleNumber}. Original issue: ${line}`,
|
|
17359
|
+
createdCycle: cycleNumber
|
|
17360
|
+
});
|
|
17361
|
+
autoTriagedCount++;
|
|
17362
|
+
} catch {
|
|
17363
|
+
}
|
|
17364
|
+
}
|
|
17365
|
+
}
|
|
16884
17366
|
if (adapter2.updateCycleLearningActionRef && task.notes) {
|
|
16885
17367
|
const learningRefs = task.notes.match(/learning:([a-f0-9-]+)/gi);
|
|
16886
17368
|
if (learningRefs) {
|
|
@@ -16923,6 +17405,52 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
16923
17405
|
await adapter2.updateTaskStatus(taskId, "In Review");
|
|
16924
17406
|
}
|
|
16925
17407
|
}
|
|
17408
|
+
let dogfoodResolvedCount = 0;
|
|
17409
|
+
if (input.completed === "yes" && adapter2.getDogfoodLog && adapter2.updateDogfoodEntryStatus) {
|
|
17410
|
+
try {
|
|
17411
|
+
const dogfoodLog = await adapter2.getDogfoodLog(50);
|
|
17412
|
+
const linked = dogfoodLog.filter(
|
|
17413
|
+
(e) => (e.linkedTaskId === taskId || e.linkedTaskId === task.id) && e.status !== "resolved"
|
|
17414
|
+
);
|
|
17415
|
+
if (linked.length > 0) {
|
|
17416
|
+
await Promise.all(linked.map((e) => adapter2.updateDogfoodEntryStatus(e.id, "resolved")));
|
|
17417
|
+
dogfoodResolvedCount = linked.length;
|
|
17418
|
+
}
|
|
17419
|
+
} catch {
|
|
17420
|
+
}
|
|
17421
|
+
}
|
|
17422
|
+
let learningsLinkedCount = 0;
|
|
17423
|
+
if (input.completed === "yes" && adapter2.getCycleLearnings && adapter2.updateCycleLearningActionRef) {
|
|
17424
|
+
try {
|
|
17425
|
+
const recentLearnings = await adapter2.getCycleLearnings({ limit: 30 });
|
|
17426
|
+
const unactioned = recentLearnings.filter(
|
|
17427
|
+
(l) => !l.actionRef && l.cycleNumber >= cycleNumber - 5
|
|
17428
|
+
);
|
|
17429
|
+
if (unactioned.length > 0) {
|
|
17430
|
+
const taskText = `${task.title} ${task.notes ?? ""}`.toLowerCase();
|
|
17431
|
+
const taskWords = new Set(
|
|
17432
|
+
taskText.match(/\b[a-z]{4,}\b/g) ?? []
|
|
17433
|
+
);
|
|
17434
|
+
const taskModule = (task.module ?? "").toLowerCase();
|
|
17435
|
+
for (const learning of unactioned) {
|
|
17436
|
+
const learningModule = (learning.tags[0] ?? "").toLowerCase();
|
|
17437
|
+
if (!taskModule || !learningModule || taskModule !== learningModule) continue;
|
|
17438
|
+
const learningText = `${learning.summary} ${learning.detail ?? ""}`.toLowerCase();
|
|
17439
|
+
const learningWords = learningText.match(/\b[a-z]{4,}\b/g) ?? [];
|
|
17440
|
+
const hasKeywordOverlap = learningWords.some((w) => taskWords.has(w));
|
|
17441
|
+
if (!hasKeywordOverlap) continue;
|
|
17442
|
+
if (learning.id) {
|
|
17443
|
+
try {
|
|
17444
|
+
await adapter2.updateCycleLearningActionRef(learning.id, task.id);
|
|
17445
|
+
learningsLinkedCount++;
|
|
17446
|
+
} catch {
|
|
17447
|
+
}
|
|
17448
|
+
}
|
|
17449
|
+
}
|
|
17450
|
+
}
|
|
17451
|
+
} catch {
|
|
17452
|
+
}
|
|
17453
|
+
}
|
|
16926
17454
|
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
17455
|
let commitLine;
|
|
16928
17456
|
if (config2.autoCommit) {
|
|
@@ -17023,7 +17551,11 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
17023
17551
|
completed: input.completed,
|
|
17024
17552
|
scopeAccuracy: input.scopeAccuracy,
|
|
17025
17553
|
phaseChanges,
|
|
17026
|
-
docWarning
|
|
17554
|
+
docWarning,
|
|
17555
|
+
dogfoodResolvedCount: dogfoodResolvedCount > 0 ? dogfoodResolvedCount : void 0,
|
|
17556
|
+
learningsLinkedCount: learningsLinkedCount > 0 ? learningsLinkedCount : void 0,
|
|
17557
|
+
autoTriagedCount: autoTriagedCount > 0 ? autoTriagedCount : void 0,
|
|
17558
|
+
reportWriteVerified
|
|
17027
17559
|
};
|
|
17028
17560
|
}
|
|
17029
17561
|
async function cancelBuild(adapter2, taskId, reason) {
|
|
@@ -17116,6 +17648,7 @@ ${instructions}`;
|
|
|
17116
17648
|
var buildListTool = {
|
|
17117
17649
|
name: "build_list",
|
|
17118
17650
|
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.",
|
|
17651
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
17119
17652
|
inputSchema: {
|
|
17120
17653
|
type: "object",
|
|
17121
17654
|
properties: {},
|
|
@@ -17125,6 +17658,7 @@ var buildListTool = {
|
|
|
17125
17658
|
var buildDescribeTool = {
|
|
17126
17659
|
name: "build_describe",
|
|
17127
17660
|
description: "Show the full BUILD HANDOFF for a specific task, including scope, acceptance criteria, and implementation guidance. Does not call the Anthropic API.",
|
|
17661
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
17128
17662
|
inputSchema: {
|
|
17129
17663
|
type: "object",
|
|
17130
17664
|
properties: {
|
|
@@ -17139,6 +17673,7 @@ var buildDescribeTool = {
|
|
|
17139
17673
|
var buildExecuteTool = {
|
|
17140
17674
|
name: "build_execute",
|
|
17141
17675
|
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.",
|
|
17676
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
17142
17677
|
inputSchema: {
|
|
17143
17678
|
type: "object",
|
|
17144
17679
|
properties: {
|
|
@@ -17171,7 +17706,7 @@ var buildExecuteTool = {
|
|
|
17171
17706
|
},
|
|
17172
17707
|
discovered_issues: {
|
|
17173
17708
|
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.`
|
|
17709
|
+
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
17710
|
},
|
|
17176
17711
|
architecture_notes: {
|
|
17177
17712
|
type: "string",
|
|
@@ -17223,6 +17758,7 @@ var buildExecuteTool = {
|
|
|
17223
17758
|
var buildCancelTool = {
|
|
17224
17759
|
name: "build_cancel",
|
|
17225
17760
|
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.",
|
|
17761
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
17226
17762
|
inputSchema: {
|
|
17227
17763
|
type: "object",
|
|
17228
17764
|
properties: {
|
|
@@ -17390,9 +17926,28 @@ If >80% of the scope is already implemented, call \`build_execute\` with complet
|
|
|
17390
17926
|
adSection = formatRelevantADs(relevant);
|
|
17391
17927
|
} catch {
|
|
17392
17928
|
}
|
|
17929
|
+
let dogfoodSection = "";
|
|
17930
|
+
try {
|
|
17931
|
+
if (adapter2.getDogfoodLog) {
|
|
17932
|
+
const dogfoodLog = await adapter2.getDogfoodLog(50);
|
|
17933
|
+
const linked = dogfoodLog.filter(
|
|
17934
|
+
(e) => e.linkedTaskId === result.task.id || e.linkedTaskId === result.task.displayId
|
|
17935
|
+
);
|
|
17936
|
+
if (linked.length > 0) {
|
|
17937
|
+
const entries = linked.map((e) => `- [${e.category}] ${e.content}`).join("\n");
|
|
17938
|
+
dogfoodSection = `
|
|
17939
|
+
|
|
17940
|
+
---
|
|
17941
|
+
|
|
17942
|
+
**DOGFOOD CONTEXT** \u2014 This task was linked to ${linked.length} observation(s):
|
|
17943
|
+
${entries}`;
|
|
17944
|
+
}
|
|
17945
|
+
}
|
|
17946
|
+
} catch {
|
|
17947
|
+
}
|
|
17393
17948
|
const moduleInstructions = getModuleInstructions(result.task.module);
|
|
17394
17949
|
const moduleContext = await getModuleContext(adapter2, result.task);
|
|
17395
|
-
return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + adSection + moduleInstructions + moduleContext + verificationNote + chainInstruction + phaseNote);
|
|
17950
|
+
return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + adSection + moduleInstructions + moduleContext + dogfoodSection + verificationNote + chainInstruction + phaseNote);
|
|
17396
17951
|
} catch (err) {
|
|
17397
17952
|
if (isNoHandoffError(err)) {
|
|
17398
17953
|
const lines = [
|
|
@@ -17499,6 +18054,18 @@ function formatCompleteResult(result) {
|
|
|
17499
18054
|
lines.push(`Phase auto-updated: ${c.phaseId} ${c.oldStatus} \u2192 ${c.newStatus}`);
|
|
17500
18055
|
}
|
|
17501
18056
|
}
|
|
18057
|
+
if (result.dogfoodResolvedCount) {
|
|
18058
|
+
lines.push("", `Resolved ${result.dogfoodResolvedCount} dogfood observation(s) linked to this task.`);
|
|
18059
|
+
}
|
|
18060
|
+
if (result.learningsLinkedCount) {
|
|
18061
|
+
lines.push("", `Linked ${result.learningsLinkedCount} unactioned learning(s) to this task.`);
|
|
18062
|
+
}
|
|
18063
|
+
if (result.autoTriagedCount) {
|
|
18064
|
+
lines.push("", `\u{1F516} Auto-triaged ${result.autoTriagedCount} discovered issue(s) to Backlog.`);
|
|
18065
|
+
}
|
|
18066
|
+
if (result.reportWriteVerified === false) {
|
|
18067
|
+
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.");
|
|
18068
|
+
}
|
|
17502
18069
|
if (result.docWarning) {
|
|
17503
18070
|
lines.push("", `\u{1F4C4} ${result.docWarning}`);
|
|
17504
18071
|
}
|
|
@@ -17638,7 +18205,7 @@ function resolveCurrentPhase(phases) {
|
|
|
17638
18205
|
const sorted = [...phases].sort((a, b2) => a.order - b2.order);
|
|
17639
18206
|
return sorted[0].label;
|
|
17640
18207
|
}
|
|
17641
|
-
var
|
|
18208
|
+
var STOP_WORDS2 = /* @__PURE__ */ new Set([
|
|
17642
18209
|
"a",
|
|
17643
18210
|
"an",
|
|
17644
18211
|
"the",
|
|
@@ -17732,7 +18299,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
|
17732
18299
|
]);
|
|
17733
18300
|
function extractKeywords(text) {
|
|
17734
18301
|
return new Set(
|
|
17735
|
-
text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !
|
|
18302
|
+
text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !STOP_WORDS2.has(w))
|
|
17736
18303
|
);
|
|
17737
18304
|
}
|
|
17738
18305
|
async function findSimilarTasks(adapter2, ideaTitle) {
|
|
@@ -17803,9 +18370,10 @@ ${lines.join("\n")}
|
|
|
17803
18370
|
const VALID_COMPLEXITIES2 = /* @__PURE__ */ new Set(["XS", "Small", "Medium", "Large", "XL"]);
|
|
17804
18371
|
const priority = input.priority && VALID_PRIORITIES2.has(input.priority) ? input.priority : "P2 Medium";
|
|
17805
18372
|
const complexity = input.complexity && VALID_COMPLEXITIES2.has(input.complexity) ? input.complexity : "Small";
|
|
17806
|
-
const VALID_TYPES = /* @__PURE__ */ new Set(["task", "bug", "research", "idea", "spike"]);
|
|
18373
|
+
const VALID_TYPES = /* @__PURE__ */ new Set(["task", "bug", "research", "idea", "spike", "discovery"]);
|
|
17807
18374
|
let taskTitle = input.text;
|
|
17808
18375
|
let taskType = "idea";
|
|
18376
|
+
let typeInferred = false;
|
|
17809
18377
|
if (input.type && VALID_TYPES.has(input.type)) {
|
|
17810
18378
|
taskType = input.type;
|
|
17811
18379
|
} else {
|
|
@@ -17820,6 +18388,20 @@ ${lines.join("\n")}
|
|
|
17820
18388
|
taskType = PREFIX_MAP[key];
|
|
17821
18389
|
taskTitle = input.text.slice(prefixMatch[0].length);
|
|
17822
18390
|
}
|
|
18391
|
+
} else {
|
|
18392
|
+
const searchText = `${input.text} ${input.notes ?? ""}`.toLowerCase();
|
|
18393
|
+
if (/\b(bug|fix|broken|crash|error)\b/.test(searchText)) {
|
|
18394
|
+
taskType = "bug";
|
|
18395
|
+
} else if (/\b(research|investigate|explore|spike)\b/.test(searchText)) {
|
|
18396
|
+
taskType = "research";
|
|
18397
|
+
} else if (/\b(performance|optimize|speed|latency)\b/.test(searchText)) {
|
|
18398
|
+
taskType = "task";
|
|
18399
|
+
} else if (/\b(verify|confirm)\b/.test(searchText)) {
|
|
18400
|
+
taskType = "spike";
|
|
18401
|
+
} else {
|
|
18402
|
+
taskType = "task";
|
|
18403
|
+
}
|
|
18404
|
+
typeInferred = true;
|
|
17823
18405
|
}
|
|
17824
18406
|
}
|
|
17825
18407
|
const task = await adapter2.createTask({
|
|
@@ -17839,7 +18421,8 @@ ${lines.join("\n")}
|
|
|
17839
18421
|
taskType,
|
|
17840
18422
|
maturity: "raw",
|
|
17841
18423
|
docRef: input.docRef,
|
|
17842
|
-
source: "llm"
|
|
18424
|
+
source: "llm",
|
|
18425
|
+
opportunity: input.opportunity
|
|
17843
18426
|
});
|
|
17844
18427
|
if (input.notes && adapter2.updateCycleLearningActionRef) {
|
|
17845
18428
|
const learningRefs = input.notes.match(/learning:([a-f0-9-]+)/gi);
|
|
@@ -17865,7 +18448,27 @@ ${lines.join("\n")}
|
|
|
17865
18448
|
}
|
|
17866
18449
|
}
|
|
17867
18450
|
}
|
|
17868
|
-
|
|
18451
|
+
if (adapter2.getDogfoodLog && adapter2.updateDogfoodEntryStatus) {
|
|
18452
|
+
try {
|
|
18453
|
+
const dogfoodLog = await adapter2.getDogfoodLog(50);
|
|
18454
|
+
const unlinked = dogfoodLog.filter((e) => e.status === "observed" && !e.linkedTaskId);
|
|
18455
|
+
if (unlinked.length > 0) {
|
|
18456
|
+
const taskText = `${task.title} ${input.notes ?? ""}`.toLowerCase();
|
|
18457
|
+
const taskKeywords = taskText.match(/\b[a-z]{4,}\b/g) ?? [];
|
|
18458
|
+
const taskKeywordSet = new Set(taskKeywords);
|
|
18459
|
+
for (const entry of unlinked) {
|
|
18460
|
+
const entryKeywords = entry.content.toLowerCase().match(/\b[a-z]{4,}\b/g) ?? [];
|
|
18461
|
+
const overlap = entryKeywords.filter((w) => taskKeywordSet.has(w));
|
|
18462
|
+
if (overlap.length >= 2) {
|
|
18463
|
+
await adapter2.updateDogfoodEntryStatus(entry.id, "backlog-created", task.id);
|
|
18464
|
+
}
|
|
18465
|
+
}
|
|
18466
|
+
}
|
|
18467
|
+
} catch {
|
|
18468
|
+
}
|
|
18469
|
+
}
|
|
18470
|
+
const typeNote = typeInferred ? ` [type: ${taskType} \u2014 inferred from text]` : "";
|
|
18471
|
+
return { routing: "task", task, message: `${task.id}: "${task.title}" \u2014 added to backlog${typeNote}` };
|
|
17869
18472
|
}
|
|
17870
18473
|
var CANVAS_SECTION_LABELS = {
|
|
17871
18474
|
landscape: "Landscape References",
|
|
@@ -17905,6 +18508,7 @@ async function routeToDiscovery(adapter2, section, input) {
|
|
|
17905
18508
|
// src/tools/idea.ts
|
|
17906
18509
|
var ideaTool = {
|
|
17907
18510
|
name: "idea",
|
|
18511
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
17908
18512
|
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
18513
|
inputSchema: {
|
|
17910
18514
|
type: "object",
|
|
@@ -17915,7 +18519,7 @@ var ideaTool = {
|
|
|
17915
18519
|
},
|
|
17916
18520
|
notes: {
|
|
17917
18521
|
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.'
|
|
18522
|
+
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
18523
|
},
|
|
17920
18524
|
module: {
|
|
17921
18525
|
type: "string",
|
|
@@ -17949,12 +18553,16 @@ var ideaTool = {
|
|
|
17949
18553
|
},
|
|
17950
18554
|
type: {
|
|
17951
18555
|
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.'
|
|
18556
|
+
enum: ["task", "bug", "research", "spike", "discovery"],
|
|
18557
|
+
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
18558
|
},
|
|
17955
18559
|
doc_ref: {
|
|
17956
18560
|
type: "string",
|
|
17957
18561
|
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.'
|
|
18562
|
+
},
|
|
18563
|
+
opportunity: {
|
|
18564
|
+
type: "string",
|
|
18565
|
+
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
18566
|
}
|
|
17959
18567
|
},
|
|
17960
18568
|
required: ["text"]
|
|
@@ -17983,7 +18591,8 @@ async function handleIdea(adapter2, config2, args) {
|
|
|
17983
18591
|
discovery: args.discovery === true,
|
|
17984
18592
|
force: args.force === true,
|
|
17985
18593
|
docRef: args.doc_ref?.trim(),
|
|
17986
|
-
type: args.type
|
|
18594
|
+
type: args.type,
|
|
18595
|
+
opportunity: args.opportunity?.trim()
|
|
17987
18596
|
};
|
|
17988
18597
|
const useGit = isGitAvailable() && isGitRepo(config2.projectRoot);
|
|
17989
18598
|
const currentBranch = useGit ? getCurrentBranch(config2.projectRoot) : null;
|
|
@@ -18090,6 +18699,7 @@ function collectDiagnostics(config2) {
|
|
|
18090
18699
|
var bugTool = {
|
|
18091
18700
|
name: "bug",
|
|
18092
18701
|
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.",
|
|
18702
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
18093
18703
|
inputSchema: {
|
|
18094
18704
|
type: "object",
|
|
18095
18705
|
properties: {
|
|
@@ -18270,6 +18880,7 @@ var VALID_EFFORTS = ["XS", "S", "M", "L", "XL"];
|
|
|
18270
18880
|
var adHocTool = {
|
|
18271
18881
|
name: "ad_hoc",
|
|
18272
18882
|
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.",
|
|
18883
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
18273
18884
|
inputSchema: {
|
|
18274
18885
|
type: "object",
|
|
18275
18886
|
properties: {
|
|
@@ -18480,9 +19091,9 @@ async function prepareReconcile(adapter2) {
|
|
|
18480
19091
|
}
|
|
18481
19092
|
return lines.join("\n");
|
|
18482
19093
|
}
|
|
18483
|
-
var
|
|
19094
|
+
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
19095
|
function tokenize(s) {
|
|
18485
|
-
return s.toLowerCase().replace(/[^a-z0-9\s-]/g, "").split(/\s+/).filter((w) => w.length > 2 && !
|
|
19096
|
+
return s.toLowerCase().replace(/[^a-z0-9\s-]/g, "").split(/\s+/).filter((w) => w.length > 2 && !STOP_WORDS3.has(w));
|
|
18486
19097
|
}
|
|
18487
19098
|
function titleKeywords(title) {
|
|
18488
19099
|
return new Set(tokenize(title));
|
|
@@ -18659,6 +19270,7 @@ async function applyRetriage(adapter2, retriages) {
|
|
|
18659
19270
|
var boardReconcileTool = {
|
|
18660
19271
|
name: "board_reconcile",
|
|
18661
19272
|
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.',
|
|
19273
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
18662
19274
|
inputSchema: {
|
|
18663
19275
|
type: "object",
|
|
18664
19276
|
properties: {
|
|
@@ -18989,451 +19601,118 @@ Assess each task above and produce your retriage output. Then call \`board_recon
|
|
|
18989
19601
|
return errorResponse(`Unknown mode: ${mode}. Use "prepare", "apply", "retriage-prepare", or "retriage-apply".`);
|
|
18990
19602
|
}
|
|
18991
19603
|
|
|
18992
|
-
// src/services/
|
|
18993
|
-
|
|
18994
|
-
|
|
18995
|
-
|
|
18996
|
-
|
|
18997
|
-
|
|
18998
|
-
|
|
18999
|
-
|
|
19000
|
-
|
|
19001
|
-
|
|
19002
|
-
|
|
19003
|
-
|
|
19004
|
-
|
|
19005
|
-
|
|
19006
|
-
|
|
19007
|
-
|
|
19008
|
-
|
|
19009
|
-
|
|
19010
|
-
|
|
19011
|
-
|
|
19012
|
-
|
|
19013
|
-
|
|
19014
|
-
|
|
19015
|
-
|
|
19016
|
-
|
|
19017
|
-
|
|
19018
|
-
|
|
19019
|
-
|
|
19020
|
-
|
|
19021
|
-
|
|
19022
|
-
|
|
19023
|
-
|
|
19024
|
-
|
|
19025
|
-
|
|
19604
|
+
// src/services/release.ts
|
|
19605
|
+
init_git();
|
|
19606
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
19607
|
+
import { join as join6 } from "path";
|
|
19608
|
+
var INITIAL_RELEASE_NOTES = `# Changelog
|
|
19609
|
+
|
|
19610
|
+
## v0.1.0-alpha \u2014 Initial Release
|
|
19611
|
+
|
|
19612
|
+
PAPI MCP Server \u2014 the AI-powered project planning framework.
|
|
19613
|
+
|
|
19614
|
+
### Commands
|
|
19615
|
+
- **setup** \u2014 Initialise a new PAPI project with Product Brief generation
|
|
19616
|
+
- **plan** \u2014 Run cycle planning with embedded BUILD HANDOFFs (Bootstrap + Full modes)
|
|
19617
|
+
- **build_list / build_describe / build_execute / build_cancel** \u2014 Manage build tasks
|
|
19618
|
+
- **board_view / board_deprioritise / board_archive** \u2014 View and manage the Board
|
|
19619
|
+
- **strategy_review / strategy_change** \u2014 Run Strategy Reviews and apply strategic changes
|
|
19620
|
+
- **review_list / review_submit** \u2014 Human review loop for handoffs and builds
|
|
19621
|
+
- **idea** \u2014 Capture ideas as backlog tasks for future triage
|
|
19622
|
+
- **health** \u2014 Cycle Health Summary dashboard
|
|
19623
|
+
- **release** \u2014 Cut versioned releases with git tags and changelogs
|
|
19624
|
+
|
|
19625
|
+
### Features
|
|
19626
|
+
- .md file persistence in .papi/ directory
|
|
19627
|
+
- Bootstrap + Full planning modes with Anthropic API integration
|
|
19628
|
+
- Embedded BUILD HANDOFFs with dual write-back build reports
|
|
19629
|
+
- Auto-commit and auto-PR after builds
|
|
19630
|
+
- Board corrections and Active Decision persistence
|
|
19631
|
+
- Single-purpose MCP tools for optimal LLM tool selection
|
|
19632
|
+
- Consistent error handling across all tools
|
|
19633
|
+
`;
|
|
19634
|
+
function generateChangelog(version, commits) {
|
|
19635
|
+
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
19636
|
+
const commitList = commits.map((c) => `- ${c}`).join("\n");
|
|
19637
|
+
return `# Changelog
|
|
19638
|
+
|
|
19639
|
+
## ${version} \u2014 ${date}
|
|
19640
|
+
|
|
19641
|
+
${commitList}
|
|
19642
|
+
`;
|
|
19643
|
+
}
|
|
19644
|
+
async function createRelease(config2, branch, version, adapter2) {
|
|
19645
|
+
if (!isGitAvailable()) {
|
|
19646
|
+
throw new Error("git is not available.");
|
|
19026
19647
|
}
|
|
19027
|
-
if (
|
|
19028
|
-
|
|
19029
|
-
const freshRatio = (decisionUsage.length - staleCount) / decisionUsage.length;
|
|
19030
|
-
scores.push({ name: "AD freshness", score: Math.round(freshRatio * 100), weight: 0.15 });
|
|
19031
|
-
} else {
|
|
19032
|
-
scores.push({ name: "AD freshness", score: 70, weight: 0.15 });
|
|
19648
|
+
if (!isGitRepo(config2.projectRoot)) {
|
|
19649
|
+
throw new Error("not a git repository.");
|
|
19033
19650
|
}
|
|
19034
|
-
|
|
19035
|
-
|
|
19036
|
-
const worst = scores.reduce((min, s) => s.score < min.score ? s : min, scores[0]);
|
|
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);
|
|
19651
|
+
if (hasUncommittedChanges(config2.projectRoot, AUTO_WRITTEN_PATHS)) {
|
|
19652
|
+
throw new Error("working directory has uncommitted changes. Commit or stash them before releasing.");
|
|
19044
19653
|
}
|
|
19045
|
-
|
|
19046
|
-
|
|
19047
|
-
|
|
19048
|
-
|
|
19049
|
-
|
|
19050
|
-
|
|
19051
|
-
|
|
19052
|
-
|
|
19053
|
-
|
|
19054
|
-
|
|
19055
|
-
|
|
19056
|
-
|
|
19057
|
-
|
|
19058
|
-
|
|
19059
|
-
|
|
19060
|
-
|
|
19061
|
-
|
|
19062
|
-
|
|
19063
|
-
|
|
19064
|
-
|
|
19065
|
-
|
|
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`;
|
|
19654
|
+
const warnings = [];
|
|
19655
|
+
if (adapter2) {
|
|
19656
|
+
try {
|
|
19657
|
+
const versionMatch = version.match(/^v0\.(\d+)\./);
|
|
19658
|
+
const currentCycle = versionMatch ? parseInt(versionMatch[1], 10) : 0;
|
|
19659
|
+
if (currentCycle > 0) {
|
|
19660
|
+
await adapter2.createCycle({
|
|
19661
|
+
id: `cycle-${currentCycle}`,
|
|
19662
|
+
number: currentCycle,
|
|
19663
|
+
status: "complete",
|
|
19664
|
+
startDate: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19665
|
+
endDate: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19666
|
+
goals: [],
|
|
19667
|
+
boardHealth: "",
|
|
19668
|
+
taskIds: []
|
|
19669
|
+
});
|
|
19670
|
+
}
|
|
19671
|
+
} catch (err) {
|
|
19672
|
+
const msg = `createCycle (mark complete) failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
19673
|
+
console.error(`[release] ${msg}`);
|
|
19674
|
+
warnings.push(msg);
|
|
19072
19675
|
}
|
|
19073
19676
|
}
|
|
19074
|
-
const
|
|
19075
|
-
|
|
19076
|
-
|
|
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
|
-
}
|
|
19677
|
+
const checkout = checkoutBranch(config2.projectRoot, branch);
|
|
19678
|
+
if (!checkout.success) {
|
|
19679
|
+
throw new Error(checkout.message);
|
|
19086
19680
|
}
|
|
19087
|
-
|
|
19088
|
-
|
|
19089
|
-
|
|
19090
|
-
|
|
19681
|
+
if (hasRemote(config2.projectRoot)) {
|
|
19682
|
+
const pull = gitPull(config2.projectRoot);
|
|
19683
|
+
if (!pull.success) {
|
|
19684
|
+
warnings.push(`git pull failed: ${pull.message}. Run manually.`);
|
|
19685
|
+
}
|
|
19091
19686
|
}
|
|
19092
|
-
if (
|
|
19093
|
-
|
|
19687
|
+
if (tagExists(config2.projectRoot, version)) {
|
|
19688
|
+
throw new Error(`tag "${version}" already exists. Use a different version.`);
|
|
19094
19689
|
}
|
|
19095
|
-
const
|
|
19096
|
-
|
|
19097
|
-
)
|
|
19098
|
-
|
|
19099
|
-
(t) => t.cycle === cycleNumber && t.status === "In Progress"
|
|
19100
|
-
);
|
|
19101
|
-
const inReviewCycleTasks = activeTasks.filter(
|
|
19102
|
-
(t) => t.cycle === cycleNumber && t.status === "In Review"
|
|
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`;
|
|
19690
|
+
const latestTag = getLatestTag(config2.projectRoot);
|
|
19691
|
+
let changelogContent;
|
|
19692
|
+
if (!latestTag) {
|
|
19693
|
+
changelogContent = INITIAL_RELEASE_NOTES.replace("v0.1.0-alpha", version);
|
|
19112
19694
|
} else {
|
|
19113
|
-
|
|
19695
|
+
const commits = getCommitsSinceTag(config2.projectRoot, latestTag);
|
|
19696
|
+
changelogContent = generateChangelog(version, commits);
|
|
19114
19697
|
}
|
|
19115
|
-
|
|
19116
|
-
|
|
19117
|
-
|
|
19118
|
-
|
|
19119
|
-
|
|
19120
|
-
|
|
19121
|
-
|
|
19122
|
-
} catch {
|
|
19123
|
-
}
|
|
19124
|
-
metricsSection = formatCycleMetrics(snapshots);
|
|
19125
|
-
derivedMetricsSection = formatDerivedMetrics(snapshots, activeTasks);
|
|
19126
|
-
} catch (_err) {
|
|
19127
|
-
metricsSection = "Could not read methodology metrics.";
|
|
19698
|
+
const changelogPath = join6(config2.projectRoot, "CHANGELOG.md");
|
|
19699
|
+
await writeFile3(changelogPath, changelogContent, "utf-8");
|
|
19700
|
+
const commitResult = stageAllAndCommit(config2.projectRoot, `release: ${version}`);
|
|
19701
|
+
const commitNote = commitResult.committed ? `Committed CHANGELOG.md.` : `CHANGELOG.md: ${commitResult.message}`;
|
|
19702
|
+
const tagResult = createTag(config2.projectRoot, version, `Release ${version}`);
|
|
19703
|
+
if (!tagResult.success) {
|
|
19704
|
+
throw new Error(tagResult.message);
|
|
19128
19705
|
}
|
|
19129
|
-
|
|
19130
|
-
|
|
19131
|
-
|
|
19132
|
-
|
|
19133
|
-
|
|
19134
|
-
|
|
19135
|
-
|
|
19136
|
-
|
|
19137
|
-
|
|
19138
|
-
|
|
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).");
|
|
19706
|
+
const pushNotes = [];
|
|
19707
|
+
if (hasRemote(config2.projectRoot)) {
|
|
19708
|
+
const branchPush = gitPush(config2.projectRoot, branch);
|
|
19709
|
+
pushNotes.push(branchPush.success ? `Pushed '${branch}' to origin.` : `Push branch failed: ${branchPush.message}`);
|
|
19710
|
+
if (!branchPush.success) warnings.push(branchPush.message);
|
|
19711
|
+
const tagPush = gitPush(config2.projectRoot, version);
|
|
19712
|
+
pushNotes.push(tagPush.success ? `Pushed tag '${version}' to origin.` : `Push tag failed: ${tagPush.message}`);
|
|
19713
|
+
if (!tagPush.success) warnings.push(tagPush.message);
|
|
19714
|
+
} else {
|
|
19715
|
+
pushNotes.push("Push: skipped (no remote).");
|
|
19437
19716
|
}
|
|
19438
19717
|
return {
|
|
19439
19718
|
version,
|
|
@@ -19449,6 +19728,7 @@ async function createRelease(config2, branch, version, adapter2) {
|
|
|
19449
19728
|
var releaseTool = {
|
|
19450
19729
|
name: "release",
|
|
19451
19730
|
description: "Cut a versioned release \u2014 creates a git tag, generates CHANGELOG.md, and pushes to remote.",
|
|
19731
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
19452
19732
|
inputSchema: {
|
|
19453
19733
|
type: "object",
|
|
19454
19734
|
properties: {
|
|
@@ -19459,6 +19739,27 @@ var releaseTool = {
|
|
|
19459
19739
|
version: {
|
|
19460
19740
|
type: "string",
|
|
19461
19741
|
description: 'The version tag to create (e.g. "v0.1.0-alpha"). Must start with "v".'
|
|
19742
|
+
},
|
|
19743
|
+
observations: {
|
|
19744
|
+
type: "array",
|
|
19745
|
+
description: "Optional dogfood observations from this cycle to persist to the DB. Each entry records friction, methodology signals, or commercial insights.",
|
|
19746
|
+
items: {
|
|
19747
|
+
type: "object",
|
|
19748
|
+
properties: {
|
|
19749
|
+
content: { type: "string", description: "The observation text." },
|
|
19750
|
+
category: {
|
|
19751
|
+
type: "string",
|
|
19752
|
+
enum: ["friction", "methodology", "signal", "commercial"],
|
|
19753
|
+
description: "Observation category."
|
|
19754
|
+
},
|
|
19755
|
+
severity: {
|
|
19756
|
+
type: "string",
|
|
19757
|
+
enum: ["P0", "P1", "P2", "P3"],
|
|
19758
|
+
description: "Optional severity for friction/signal observations."
|
|
19759
|
+
}
|
|
19760
|
+
},
|
|
19761
|
+
required: ["content", "category"]
|
|
19762
|
+
}
|
|
19462
19763
|
}
|
|
19463
19764
|
},
|
|
19464
19765
|
required: ["branch", "version"]
|
|
@@ -19467,6 +19768,7 @@ var releaseTool = {
|
|
|
19467
19768
|
async function handleRelease(adapter2, config2, args) {
|
|
19468
19769
|
const branch = args.branch;
|
|
19469
19770
|
const version = args.version;
|
|
19771
|
+
const rawObservations = args.observations;
|
|
19470
19772
|
if (!branch || !version) {
|
|
19471
19773
|
return errorResponse('both branch and version are required. Example: release branch="main" version="v0.1.0-alpha"');
|
|
19472
19774
|
}
|
|
@@ -19504,6 +19806,23 @@ async function handleRelease(adapter2, config2, args) {
|
|
|
19504
19806
|
}
|
|
19505
19807
|
} catch {
|
|
19506
19808
|
}
|
|
19809
|
+
if (rawObservations && rawObservations.length > 0 && adapter2.writeDogfoodEntries) {
|
|
19810
|
+
try {
|
|
19811
|
+
const cycleMatch = version.match(/^v0\.(\d+)\./);
|
|
19812
|
+
const cycleNum = cycleMatch ? parseInt(cycleMatch[1], 10) : 0;
|
|
19813
|
+
const entries = rawObservations.map((obs) => ({
|
|
19814
|
+
cycleNumber: cycleNum,
|
|
19815
|
+
category: obs.category,
|
|
19816
|
+
content: obs.content,
|
|
19817
|
+
sourceTool: "release",
|
|
19818
|
+
status: "observed"
|
|
19819
|
+
}));
|
|
19820
|
+
await adapter2.writeDogfoodEntries(entries);
|
|
19821
|
+
lines.push("", `Dogfood: ${entries.length} observation(s) saved to DB.`);
|
|
19822
|
+
} catch {
|
|
19823
|
+
lines.push("", "\u26A0\uFE0F Dogfood observations could not be saved to DB \u2014 log them manually in DOGFOOD_LOG.md.");
|
|
19824
|
+
}
|
|
19825
|
+
}
|
|
19507
19826
|
lines.push("", `Next: cycle released! Run \`plan\` to start your next planning cycle.`);
|
|
19508
19827
|
return textResponse(lines.join("\n"));
|
|
19509
19828
|
} catch (err) {
|
|
@@ -19629,7 +19948,6 @@ async function submitReview(adapter2, input) {
|
|
|
19629
19948
|
handoffRegenPrompt = await prepareHandoffRegen(task, input.comments);
|
|
19630
19949
|
}
|
|
19631
19950
|
const stageLabel = input.stage === "handoff-review" ? "Handoff Review" : "Build Acceptance";
|
|
19632
|
-
const slackWarning = void 0;
|
|
19633
19951
|
let phaseChanges = [];
|
|
19634
19952
|
if (newStatus) {
|
|
19635
19953
|
try {
|
|
@@ -19645,7 +19963,6 @@ async function submitReview(adapter2, input) {
|
|
|
19645
19963
|
newStatus,
|
|
19646
19964
|
unblockedTasks,
|
|
19647
19965
|
handoffRegenerated,
|
|
19648
|
-
slackWarning,
|
|
19649
19966
|
handoffRegenPrompt,
|
|
19650
19967
|
currentCycle: cycle,
|
|
19651
19968
|
phaseChanges
|
|
@@ -19656,6 +19973,7 @@ async function submitReview(adapter2, input) {
|
|
|
19656
19973
|
var reviewListTool = {
|
|
19657
19974
|
name: "review_list",
|
|
19658
19975
|
description: "List tasks ready for your sign-off \u2014 shows completed builds waiting for approval or feedback. Does not call the Anthropic API.",
|
|
19976
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
19659
19977
|
inputSchema: {
|
|
19660
19978
|
type: "object",
|
|
19661
19979
|
properties: {},
|
|
@@ -19665,6 +19983,7 @@ var reviewListTool = {
|
|
|
19665
19983
|
var reviewSubmitTool = {
|
|
19666
19984
|
name: "review_submit",
|
|
19667
19985
|
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.",
|
|
19986
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
19668
19987
|
inputSchema: {
|
|
19669
19988
|
type: "object",
|
|
19670
19989
|
properties: {
|
|
@@ -19694,10 +20013,6 @@ var reviewSubmitTool = {
|
|
|
19694
20013
|
type: "string",
|
|
19695
20014
|
description: "Your locally-generated BUILD HANDOFF regen output. Pass this to save a handoff that was regenerated in local mode (no API key)."
|
|
19696
20015
|
},
|
|
19697
|
-
notify: {
|
|
19698
|
-
type: "boolean",
|
|
19699
|
-
description: "Send Slack notification. Default true. Set false for batch middle reviews to avoid spam."
|
|
19700
|
-
},
|
|
19701
20016
|
auto_review: {
|
|
19702
20017
|
type: "object",
|
|
19703
20018
|
description: "Optional automated code review results to attach to this review. Run PR analysis first, then pass findings here.",
|
|
@@ -19814,7 +20129,6 @@ async function handleReviewSubmit(adapter2, config2, args) {
|
|
|
19814
20129
|
const verdict = args.verdict;
|
|
19815
20130
|
const comments = args.comments;
|
|
19816
20131
|
const reviewer = args.reviewer ?? "human";
|
|
19817
|
-
const notify = args.notify !== false;
|
|
19818
20132
|
const rawAutoReview = args.auto_review;
|
|
19819
20133
|
let autoReview;
|
|
19820
20134
|
if (rawAutoReview?.verdict && rawAutoReview?.summary && Array.isArray(rawAutoReview?.findings)) {
|
|
@@ -19857,7 +20171,7 @@ async function handleReviewSubmit(adapter2, config2, args) {
|
|
|
19857
20171
|
try {
|
|
19858
20172
|
const result = await submitReview(
|
|
19859
20173
|
adapter2,
|
|
19860
|
-
{ taskId, stage, verdict, comments, reviewer,
|
|
20174
|
+
{ taskId, stage, verdict, comments, reviewer, autoReview }
|
|
19861
20175
|
);
|
|
19862
20176
|
const statusNote = result.newStatus ? ` Task status updated to **${result.newStatus}**.` : " Task status unchanged.";
|
|
19863
20177
|
const unblockNote = result.unblockedTasks.length > 0 ? `
|
|
@@ -19890,9 +20204,6 @@ ${result.handoffRegenPrompt.userMessage}
|
|
|
19890
20204
|
mergeNote = "\n\n" + mergeLines.map((l) => `> ${l}`).join("\n");
|
|
19891
20205
|
}
|
|
19892
20206
|
}
|
|
19893
|
-
const slackNote = result.slackWarning ? `
|
|
19894
|
-
|
|
19895
|
-
${result.slackWarning}` : "";
|
|
19896
20207
|
let autoReleaseNote = "";
|
|
19897
20208
|
if (stage === "build-acceptance" && verdict === "accept" && result.newStatus === "Done" && result.currentCycle > 0) {
|
|
19898
20209
|
try {
|
|
@@ -19943,7 +20254,7 @@ Next: address the feedback, then run \`build_execute ${taskId}\` to resubmit.`;
|
|
|
19943
20254
|
- **Verdict:** ${result.verdict}
|
|
19944
20255
|
- **Comments:** ${result.comments}
|
|
19945
20256
|
|
|
19946
|
-
${statusNote}${autoReviewNote}${unblockNote}${regenNote}${mergeNote}${
|
|
20257
|
+
${statusNote}${autoReviewNote}${unblockNote}${regenNote}${mergeNote}${autoReleaseNote}${nextStepNote}${phaseNote}`
|
|
19947
20258
|
);
|
|
19948
20259
|
} catch (err) {
|
|
19949
20260
|
return errorResponse(err instanceof Error ? err.message : String(err));
|
|
@@ -19957,6 +20268,7 @@ import path4 from "path";
|
|
|
19957
20268
|
var initTool = {
|
|
19958
20269
|
name: "init",
|
|
19959
20270
|
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.",
|
|
20271
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
19960
20272
|
inputSchema: {
|
|
19961
20273
|
type: "object",
|
|
19962
20274
|
properties: {
|
|
@@ -20082,32 +20394,491 @@ Your existing API key and project ID have been saved to .mcp.json.
|
|
|
20082
20394
|
].join("\n");
|
|
20083
20395
|
return textResponse(output2);
|
|
20084
20396
|
}
|
|
20085
|
-
const output = [
|
|
20086
|
-
`# PAPI \u2014 Account Required`,
|
|
20087
|
-
"",
|
|
20088
|
-
`PAPI needs an account to store your project data.`,
|
|
20089
|
-
"",
|
|
20090
|
-
"## Get Started in 3 Steps",
|
|
20091
|
-
"",
|
|
20092
|
-
"1. **Sign up** at https://getpapi.ai/login",
|
|
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);
|
|
20397
|
+
const output = [
|
|
20398
|
+
`# PAPI \u2014 Account Required`,
|
|
20399
|
+
"",
|
|
20400
|
+
`PAPI needs an account to store your project data.`,
|
|
20401
|
+
"",
|
|
20402
|
+
"## Get Started in 3 Steps",
|
|
20403
|
+
"",
|
|
20404
|
+
"1. **Sign up** at https://getpapi.ai/login",
|
|
20405
|
+
"2. **Complete the onboarding wizard** \u2014 it generates your `.mcp.json` config with your API key and project ID",
|
|
20406
|
+
"3. **Download the config**, place it in your project root, and restart your MCP client",
|
|
20407
|
+
"",
|
|
20408
|
+
"The onboarding wizard generates everything you need \u2014 no manual configuration required.",
|
|
20409
|
+
"",
|
|
20410
|
+
`> Already have an account? Make sure both \`PAPI_PROJECT_ID\` and \`PAPI_DATA_API_KEY\` are set in your .mcp.json.`
|
|
20411
|
+
].join("\n");
|
|
20412
|
+
return textResponse(output);
|
|
20413
|
+
}
|
|
20414
|
+
|
|
20415
|
+
// src/services/health.ts
|
|
20416
|
+
function computeHealthScore(cycleNumber, snapshots, activeTasks, decisionUsage) {
|
|
20417
|
+
if (cycleNumber < 3) return null;
|
|
20418
|
+
const scores = [];
|
|
20419
|
+
const recentSnaps = snapshots.slice(-3);
|
|
20420
|
+
const baselineSnaps = snapshots.slice(-10);
|
|
20421
|
+
if (recentSnaps.length > 0 && baselineSnaps.length > 0) {
|
|
20422
|
+
const avg = (snaps) => snaps.reduce((s, sn) => s + (sn.velocity[0]?.effortPoints ?? 0), 0) / snaps.length;
|
|
20423
|
+
const recentAvg = avg(recentSnaps);
|
|
20424
|
+
const baselineAvg = avg(baselineSnaps);
|
|
20425
|
+
const velocityScore = baselineAvg > 0 ? Math.min(100, Math.round(recentAvg / baselineAvg * 100)) : 50;
|
|
20426
|
+
scores.push({ name: "Velocity", score: velocityScore, weight: 0.25 });
|
|
20427
|
+
} else {
|
|
20428
|
+
scores.push({ name: "Velocity", score: 50, weight: 0.25 });
|
|
20429
|
+
}
|
|
20430
|
+
if (recentSnaps.length > 0) {
|
|
20431
|
+
const avgMatchRate = recentSnaps.reduce((s, sn) => s + (sn.accuracy[0]?.matchRate ?? 0), 0) / recentSnaps.length;
|
|
20432
|
+
scores.push({ name: "Estimation accuracy", score: Math.round(avgMatchRate), weight: 0.25 });
|
|
20433
|
+
} else {
|
|
20434
|
+
scores.push({ name: "Estimation accuracy", score: 50, weight: 0.25 });
|
|
20435
|
+
}
|
|
20436
|
+
const inReviewCount = activeTasks.filter((t) => t.status === "In Review").length;
|
|
20437
|
+
const reviewScore = inReviewCount === 0 ? 100 : inReviewCount <= 2 ? 60 : 20;
|
|
20438
|
+
scores.push({ name: "Review throughput", score: reviewScore, weight: 0.2 });
|
|
20439
|
+
const backlogTasks = activeTasks.filter((t) => t.status === "Backlog");
|
|
20440
|
+
if (backlogTasks.length > 0) {
|
|
20441
|
+
const criticalCount = backlogTasks.filter(
|
|
20442
|
+
(t) => t.priority === "P0 Critical" || t.priority === "P1 High"
|
|
20443
|
+
).length;
|
|
20444
|
+
const criticalRatio = criticalCount / backlogTasks.length;
|
|
20445
|
+
const backlogScore = criticalRatio > 0.5 ? 40 : criticalRatio > 0.3 ? 70 : 90;
|
|
20446
|
+
scores.push({ name: "Backlog health", score: backlogScore, weight: 0.15 });
|
|
20447
|
+
} else {
|
|
20448
|
+
scores.push({ name: "Backlog health", score: 80, weight: 0.15 });
|
|
20449
|
+
}
|
|
20450
|
+
if (decisionUsage.length > 0) {
|
|
20451
|
+
const staleCount = decisionUsage.filter((u) => u.cyclesSinceLastReference >= 10).length;
|
|
20452
|
+
const freshRatio = (decisionUsage.length - staleCount) / decisionUsage.length;
|
|
20453
|
+
scores.push({ name: "AD freshness", score: Math.round(freshRatio * 100), weight: 0.15 });
|
|
20454
|
+
} else {
|
|
20455
|
+
scores.push({ name: "AD freshness", score: 70, weight: 0.15 });
|
|
20456
|
+
}
|
|
20457
|
+
const totalScore = Math.round(scores.reduce((sum, s) => sum + s.score * s.weight, 0));
|
|
20458
|
+
const status = totalScore >= 70 ? "GREEN" : totalScore >= 50 ? "AMBER" : "RED";
|
|
20459
|
+
const worst = scores.reduce((min, s) => s.score < min.score ? s : min, scores[0]);
|
|
20460
|
+
const reason = status === "GREEN" ? "All components healthy" : `${worst.name} below target (${worst.score}/100)`;
|
|
20461
|
+
return { score: totalScore, status, reason };
|
|
20462
|
+
}
|
|
20463
|
+
function countByStatus(tasks) {
|
|
20464
|
+
const counts = /* @__PURE__ */ new Map();
|
|
20465
|
+
for (const task of tasks) {
|
|
20466
|
+
counts.set(task.status, (counts.get(task.status) ?? 0) + 1);
|
|
20467
|
+
}
|
|
20468
|
+
return counts;
|
|
20469
|
+
}
|
|
20470
|
+
async function getHealthSummary(adapter2) {
|
|
20471
|
+
const health = await adapter2.getCycleHealth();
|
|
20472
|
+
const activeTasks = await adapter2.queryBoard({
|
|
20473
|
+
status: ["Backlog", "In Cycle", "Ready", "In Progress", "In Review", "Blocked"]
|
|
20474
|
+
});
|
|
20475
|
+
const logEntries = await adapter2.getCycleLog(3);
|
|
20476
|
+
const cycleNumber = health.totalCycles;
|
|
20477
|
+
const cyclesSinceReview = health.cyclesSinceLastStrategyReview;
|
|
20478
|
+
const reviewDue = health.strategyReviewDue;
|
|
20479
|
+
const reviewGateBlocking = cyclesSinceReview >= 5;
|
|
20480
|
+
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}`;
|
|
20481
|
+
const deferredCount = activeTasks.filter((t) => t.status === "Deferred").length;
|
|
20482
|
+
const nonDeferredTasks = activeTasks.filter((t) => t.status !== "Deferred");
|
|
20483
|
+
const statusCounts = countByStatus(nonDeferredTasks);
|
|
20484
|
+
let boardSummary;
|
|
20485
|
+
if (nonDeferredTasks.length === 0 && deferredCount === 0) {
|
|
20486
|
+
boardSummary = "0 tasks \u2014 board may need reloading";
|
|
20487
|
+
} else {
|
|
20488
|
+
const parts = [];
|
|
20489
|
+
for (const [status, count] of statusCounts) {
|
|
20490
|
+
parts.push(`${count} ${status}`);
|
|
20491
|
+
}
|
|
20492
|
+
boardSummary = `${nonDeferredTasks.length} active tasks \u2014 ${parts.join(", ")}`;
|
|
20493
|
+
if (deferredCount > 0) {
|
|
20494
|
+
boardSummary += ` + ${deferredCount} deferred`;
|
|
20495
|
+
}
|
|
20496
|
+
}
|
|
20497
|
+
const inProgressTasks = activeTasks.filter((t) => t.status === "In Progress");
|
|
20498
|
+
const staleTasks = inProgressTasks.length > 0 ? `${inProgressTasks.length} task(s) In Progress: ${inProgressTasks.map((t) => t.id).join(", ")}` : "No tasks currently In Progress";
|
|
20499
|
+
const inReviewTasks = activeTasks.filter((t) => t.status === "In Review");
|
|
20500
|
+
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";
|
|
20501
|
+
let carryForward = "None found";
|
|
20502
|
+
if (logEntries.length > 0) {
|
|
20503
|
+
const latest = logEntries[0];
|
|
20504
|
+
if (latest.carryForward) {
|
|
20505
|
+
carryForward = latest.carryForward;
|
|
20506
|
+
} else {
|
|
20507
|
+
carryForward = `No carry-forward in Cycle ${latest.cycleNumber}`;
|
|
20508
|
+
}
|
|
20509
|
+
}
|
|
20510
|
+
let recommendedMode;
|
|
20511
|
+
const reasons = [];
|
|
20512
|
+
if (reviewGateBlocking) {
|
|
20513
|
+
reasons.push(`Strategy Review overdue (${cyclesSinceReview} cycles)`);
|
|
20514
|
+
}
|
|
20515
|
+
if (activeTasks.length === 0) {
|
|
20516
|
+
reasons.push("Board is empty \u2014 needs task reload/triage");
|
|
20517
|
+
}
|
|
20518
|
+
const unbuiltCycleTasks = activeTasks.filter(
|
|
20519
|
+
(t) => t.cycle === cycleNumber && (t.status === "In Cycle" || t.status === "Ready")
|
|
20520
|
+
);
|
|
20521
|
+
const inProgressCycleTasks = activeTasks.filter(
|
|
20522
|
+
(t) => t.cycle === cycleNumber && t.status === "In Progress"
|
|
20523
|
+
);
|
|
20524
|
+
const inReviewCycleTasks = activeTasks.filter(
|
|
20525
|
+
(t) => t.cycle === cycleNumber && t.status === "In Review"
|
|
20526
|
+
);
|
|
20527
|
+
if (reasons.length > 0) {
|
|
20528
|
+
recommendedMode = `**Full** \u2014 ${reasons.join("; ")}`;
|
|
20529
|
+
} else if (unbuiltCycleTasks.length > 0) {
|
|
20530
|
+
recommendedMode = `**Build** \u2014 ${unbuiltCycleTasks.length} cycle task(s) not yet started`;
|
|
20531
|
+
} else if (inProgressCycleTasks.length > 0) {
|
|
20532
|
+
recommendedMode = `**Build** \u2014 ${inProgressCycleTasks.length} task(s) in progress`;
|
|
20533
|
+
} else if (inReviewCycleTasks.length > 0) {
|
|
20534
|
+
recommendedMode = `**Review** \u2014 ${inReviewCycleTasks.length} task(s) awaiting review`;
|
|
20535
|
+
} else {
|
|
20536
|
+
recommendedMode = `**Full** \u2014 ready for next cycle`;
|
|
20537
|
+
}
|
|
20538
|
+
let metricsSection;
|
|
20539
|
+
let derivedMetricsSection = "";
|
|
20540
|
+
let snapshots = [];
|
|
20541
|
+
try {
|
|
20542
|
+
try {
|
|
20543
|
+
const reports = await adapter2.getRecentBuildReports(50);
|
|
20544
|
+
snapshots = computeSnapshotsFromBuildReports(reports);
|
|
20545
|
+
} catch {
|
|
20546
|
+
}
|
|
20547
|
+
metricsSection = formatCycleMetrics(snapshots);
|
|
20548
|
+
derivedMetricsSection = formatDerivedMetrics(snapshots, activeTasks);
|
|
20549
|
+
} catch (_err) {
|
|
20550
|
+
metricsSection = "Could not read methodology metrics.";
|
|
20551
|
+
}
|
|
20552
|
+
try {
|
|
20553
|
+
const recentReports = await adapter2.getRecentBuildReports(50);
|
|
20554
|
+
if (recentReports.length > 0) {
|
|
20555
|
+
const taskCounts = /* @__PURE__ */ new Map();
|
|
20556
|
+
for (const r of recentReports) {
|
|
20557
|
+
taskCounts.set(r.taskId, (taskCounts.get(r.taskId) ?? 0) + 1);
|
|
20558
|
+
}
|
|
20559
|
+
const iterCounts = [...taskCounts.values()];
|
|
20560
|
+
const avgIter = iterCounts.reduce((s, c) => s + c, 0) / iterCounts.length;
|
|
20561
|
+
const multiIterTasks = iterCounts.filter((c) => c > 1).length;
|
|
20562
|
+
if (avgIter > 1 || multiIterTasks > 0) {
|
|
20563
|
+
derivedMetricsSection += `
|
|
20564
|
+
|
|
20565
|
+
**Rework**
|
|
20566
|
+
- Average iterations: ${avgIter.toFixed(1)} (${multiIterTasks} task${multiIterTasks !== 1 ? "s" : ""} with pushbacks)`;
|
|
20567
|
+
}
|
|
20568
|
+
}
|
|
20569
|
+
} catch {
|
|
20570
|
+
}
|
|
20571
|
+
const costSection = "Disabled \u2014 local MCP, no API costs.";
|
|
20572
|
+
let decisionUsageSection = "";
|
|
20573
|
+
let decisionUsageEntries = [];
|
|
20574
|
+
try {
|
|
20575
|
+
const usage = await adapter2.getDecisionUsage(cycleNumber);
|
|
20576
|
+
decisionUsageEntries = usage;
|
|
20577
|
+
if (usage.length > 0) {
|
|
20578
|
+
const stale = usage.filter((u) => u.cyclesSinceLastReference >= 5);
|
|
20579
|
+
if (stale.length > 0) {
|
|
20580
|
+
const lines = stale.map(
|
|
20581
|
+
(u) => `- ${u.decisionId}: last referenced Cycle ${u.lastReferencedCycle} (${u.cyclesSinceLastReference} cycles ago)`
|
|
20582
|
+
);
|
|
20583
|
+
decisionUsageSection = `**Stale ADs (5+ cycles unreferenced):**
|
|
20584
|
+
${lines.join("\n")}`;
|
|
20585
|
+
} else {
|
|
20586
|
+
decisionUsageSection = `All ${usage.length} tracked ADs referenced within last 5 cycles.`;
|
|
20587
|
+
}
|
|
20588
|
+
}
|
|
20589
|
+
} catch {
|
|
20590
|
+
}
|
|
20591
|
+
let decisionLifecycleSection = "";
|
|
20592
|
+
try {
|
|
20593
|
+
const decisions = await adapter2.getActiveDecisions();
|
|
20594
|
+
const lifecycleSummary = formatDecisionLifecycleSummary(decisions);
|
|
20595
|
+
if (lifecycleSummary) {
|
|
20596
|
+
decisionLifecycleSection = `**Lifecycle:** ${lifecycleSummary}`;
|
|
20597
|
+
}
|
|
20598
|
+
} catch {
|
|
20599
|
+
}
|
|
20600
|
+
const decisionScoresSection = "";
|
|
20601
|
+
let contextUtilisationSection = "";
|
|
20602
|
+
try {
|
|
20603
|
+
const utilData = await adapter2.getContextUtilisation?.();
|
|
20604
|
+
if (utilData && utilData.length > 0) {
|
|
20605
|
+
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)`);
|
|
20606
|
+
if (lines.length > 0) {
|
|
20607
|
+
contextUtilisationSection = `**Current cycle:**
|
|
20608
|
+
${lines.join("\n")}`;
|
|
20609
|
+
}
|
|
20610
|
+
}
|
|
20611
|
+
} catch {
|
|
20612
|
+
}
|
|
20613
|
+
let northStarSection = "";
|
|
20614
|
+
try {
|
|
20615
|
+
const staleness = await adapter2.getNorthStarStaleness?.();
|
|
20616
|
+
if (staleness) {
|
|
20617
|
+
const cycleGap = cycleNumber - staleness.setCycle;
|
|
20618
|
+
const daysSinceSet = Math.floor((Date.now() - new Date(staleness.setAt).getTime()) / (1e3 * 60 * 60 * 24));
|
|
20619
|
+
northStarSection = `\u2713 North Star set Cycle ${staleness.setCycle} (${cycleGap} cycles, ${daysSinceSet} days ago)`;
|
|
20620
|
+
} else {
|
|
20621
|
+
const setAtCycle = await adapter2.getNorthStarSetCycle?.();
|
|
20622
|
+
if (setAtCycle != null) {
|
|
20623
|
+
northStarSection = `\u2713 North Star set Cycle ${setAtCycle}`;
|
|
20624
|
+
} else if (adapter2.getCurrentNorthStar) {
|
|
20625
|
+
const ns = await adapter2.getCurrentNorthStar();
|
|
20626
|
+
northStarSection = ns ? "" : "\u26A0\uFE0F No North Star set \u2014 consider defining one";
|
|
20627
|
+
}
|
|
20628
|
+
}
|
|
20629
|
+
} catch {
|
|
20630
|
+
}
|
|
20631
|
+
const healthResult = computeHealthScore(cycleNumber, snapshots, activeTasks, decisionUsageEntries);
|
|
20632
|
+
return {
|
|
20633
|
+
cycleNumber,
|
|
20634
|
+
latestCycleStatus: health.latestCycleStatus,
|
|
20635
|
+
connectionStatus: getConnectionStatus(),
|
|
20636
|
+
reviewWarning,
|
|
20637
|
+
boardSummary,
|
|
20638
|
+
staleTasks,
|
|
20639
|
+
inReviewSummary,
|
|
20640
|
+
carryForward,
|
|
20641
|
+
recommendedMode,
|
|
20642
|
+
metricsSection,
|
|
20643
|
+
derivedMetricsSection,
|
|
20644
|
+
costSection,
|
|
20645
|
+
decisionUsageSection,
|
|
20646
|
+
decisionLifecycleSection,
|
|
20647
|
+
decisionScoresSection,
|
|
20648
|
+
contextUtilisationSection,
|
|
20649
|
+
northStarSection,
|
|
20650
|
+
healthScore: healthResult?.score ?? null,
|
|
20651
|
+
healthStatus: healthResult?.status ?? null,
|
|
20652
|
+
healthReason: healthResult?.reason ?? null
|
|
20653
|
+
};
|
|
20654
|
+
}
|
|
20655
|
+
|
|
20656
|
+
// src/tools/orient.ts
|
|
20657
|
+
init_git();
|
|
20658
|
+
|
|
20659
|
+
// src/tools/doc-registry.ts
|
|
20660
|
+
import { readdirSync as readdirSync4, existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
|
|
20661
|
+
import { join as join8, relative } from "path";
|
|
20662
|
+
import { homedir as homedir2 } from "os";
|
|
20663
|
+
var docRegisterTool = {
|
|
20664
|
+
name: "doc_register",
|
|
20665
|
+
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.",
|
|
20666
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
20667
|
+
inputSchema: {
|
|
20668
|
+
type: "object",
|
|
20669
|
+
properties: {
|
|
20670
|
+
path: { type: "string", description: 'Relative path from project root (e.g. "docs/research/funding-landscape.md").' },
|
|
20671
|
+
title: { type: "string", description: "Document title." },
|
|
20672
|
+
type: { type: "string", enum: ["research", "audit", "spec", "guide", "architecture", "positioning", "framework", "reference"], description: "Document type." },
|
|
20673
|
+
status: { type: "string", enum: ["active", "draft", "superseded", "actioned", "legacy", "archived"], description: 'Document status. Defaults to "active".' },
|
|
20674
|
+
summary: { type: "string", description: 'Structured 2-4 sentence summary. Format: "Conclusions: ... Open questions: ... Unactioned: ..."' },
|
|
20675
|
+
tags: { type: "array", items: { type: "string" }, description: "Tags from project vocabulary." },
|
|
20676
|
+
cycle: { type: "number", description: "Current cycle number." },
|
|
20677
|
+
actions: {
|
|
20678
|
+
type: "array",
|
|
20679
|
+
items: {
|
|
20680
|
+
type: "object",
|
|
20681
|
+
properties: {
|
|
20682
|
+
description: { type: "string" },
|
|
20683
|
+
status: { type: "string", enum: ["pending", "resolved"] },
|
|
20684
|
+
linkedTaskId: { type: "string" }
|
|
20685
|
+
},
|
|
20686
|
+
required: ["description", "status"]
|
|
20687
|
+
},
|
|
20688
|
+
description: "Actionable findings from the document."
|
|
20689
|
+
},
|
|
20690
|
+
superseded_by_path: { type: "string", description: "Path of the doc that supersedes this one (sets status to superseded)." }
|
|
20691
|
+
},
|
|
20692
|
+
required: ["path", "title", "type", "summary", "cycle"]
|
|
20693
|
+
}
|
|
20694
|
+
};
|
|
20695
|
+
var docSearchTool = {
|
|
20696
|
+
name: "doc_search",
|
|
20697
|
+
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.",
|
|
20698
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
20699
|
+
inputSchema: {
|
|
20700
|
+
type: "object",
|
|
20701
|
+
properties: {
|
|
20702
|
+
type: { type: "string", description: 'Filter by doc type (e.g. "research", "architecture").' },
|
|
20703
|
+
status: { type: "string", description: 'Filter by status. Defaults to "active".' },
|
|
20704
|
+
tags: { type: "array", items: { type: "string" }, description: "Filter by tags (OR match)." },
|
|
20705
|
+
keyword: { type: "string", description: "Search title and summary text." },
|
|
20706
|
+
has_pending_actions: { type: "boolean", description: "Only docs with unresolved action items." },
|
|
20707
|
+
since_cycle: { type: "number", description: "Docs updated since this cycle." },
|
|
20708
|
+
limit: { type: "number", description: "Max results (default: 10)." }
|
|
20709
|
+
},
|
|
20710
|
+
required: []
|
|
20711
|
+
}
|
|
20712
|
+
};
|
|
20713
|
+
var docScanTool = {
|
|
20714
|
+
name: "doc_scan",
|
|
20715
|
+
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.",
|
|
20716
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
20717
|
+
inputSchema: {
|
|
20718
|
+
type: "object",
|
|
20719
|
+
properties: {
|
|
20720
|
+
include_plans: {
|
|
20721
|
+
type: "boolean",
|
|
20722
|
+
description: "Also scan ~/.claude/plans/ for plan files (default: false)."
|
|
20723
|
+
}
|
|
20724
|
+
},
|
|
20725
|
+
required: []
|
|
20726
|
+
}
|
|
20727
|
+
};
|
|
20728
|
+
async function handleDocRegister(adapter2, args) {
|
|
20729
|
+
if (!adapter2.registerDoc) {
|
|
20730
|
+
return errorResponse("Doc registry not available \u2014 requires pg adapter.");
|
|
20731
|
+
}
|
|
20732
|
+
const path5 = args.path;
|
|
20733
|
+
const title = args.title;
|
|
20734
|
+
const type = args.type;
|
|
20735
|
+
const status = args.status ?? "active";
|
|
20736
|
+
const summary = args.summary;
|
|
20737
|
+
const tags = args.tags ?? [];
|
|
20738
|
+
const cycle = args.cycle;
|
|
20739
|
+
const actions = args.actions;
|
|
20740
|
+
const supersededByPath = args.superseded_by_path;
|
|
20741
|
+
if (!path5 || !title || !type || !summary || !cycle) {
|
|
20742
|
+
return errorResponse("Required fields: path, title, type, summary, cycle.");
|
|
20743
|
+
}
|
|
20744
|
+
let supersededBy;
|
|
20745
|
+
if (supersededByPath) {
|
|
20746
|
+
const existing = await adapter2.getDoc?.(supersededByPath);
|
|
20747
|
+
if (existing) {
|
|
20748
|
+
supersededBy = existing.id;
|
|
20749
|
+
await adapter2.updateDocStatus?.(existing.id, "superseded", void 0);
|
|
20750
|
+
}
|
|
20751
|
+
}
|
|
20752
|
+
const entry = await adapter2.registerDoc({
|
|
20753
|
+
title,
|
|
20754
|
+
type,
|
|
20755
|
+
path: path5,
|
|
20756
|
+
status: supersededByPath ? "superseded" : status,
|
|
20757
|
+
summary,
|
|
20758
|
+
tags,
|
|
20759
|
+
cycleCreated: cycle,
|
|
20760
|
+
cycleUpdated: cycle,
|
|
20761
|
+
supersededBy,
|
|
20762
|
+
actions
|
|
20763
|
+
});
|
|
20764
|
+
return textResponse(
|
|
20765
|
+
`**Registered:** ${entry.title}
|
|
20766
|
+
- **Path:** ${entry.path}
|
|
20767
|
+
- **Type:** ${entry.type} | **Status:** ${entry.status}
|
|
20768
|
+
- **Tags:** ${entry.tags.length > 0 ? entry.tags.join(", ") : "none"}
|
|
20769
|
+
- **Actions:** ${actions?.length ?? 0} items
|
|
20770
|
+
- **ID:** ${entry.id}`
|
|
20771
|
+
);
|
|
20772
|
+
}
|
|
20773
|
+
async function handleDocSearch(adapter2, args) {
|
|
20774
|
+
if (!adapter2.searchDocs) {
|
|
20775
|
+
return errorResponse("Doc registry not available \u2014 requires pg adapter.");
|
|
20776
|
+
}
|
|
20777
|
+
const input = {
|
|
20778
|
+
type: args.type,
|
|
20779
|
+
status: args.status,
|
|
20780
|
+
tags: args.tags,
|
|
20781
|
+
keyword: args.keyword,
|
|
20782
|
+
hasPendingActions: args.has_pending_actions,
|
|
20783
|
+
sinceCycle: args.since_cycle,
|
|
20784
|
+
limit: args.limit
|
|
20785
|
+
};
|
|
20786
|
+
const docs = await adapter2.searchDocs(input);
|
|
20787
|
+
if (docs.length === 0) {
|
|
20788
|
+
return textResponse("No documents found matching the search criteria.");
|
|
20789
|
+
}
|
|
20790
|
+
const lines = docs.map((d) => {
|
|
20791
|
+
const actionCount = d.actions?.filter((a) => a.status === "pending").length ?? 0;
|
|
20792
|
+
const actionNote = actionCount > 0 ? ` | ${actionCount} pending action(s)` : "";
|
|
20793
|
+
return `### ${d.title}
|
|
20794
|
+
**Type:** ${d.type} | **Status:** ${d.status} | **Cycle:** ${d.cycleCreated}${d.cycleUpdated ? `\u2192${d.cycleUpdated}` : ""}${actionNote}
|
|
20795
|
+
**Path:** ${d.path}
|
|
20796
|
+
**Tags:** ${d.tags.length > 0 ? d.tags.join(", ") : "none"}
|
|
20797
|
+
${d.summary}
|
|
20798
|
+
`;
|
|
20799
|
+
});
|
|
20800
|
+
return textResponse(`**${docs.length} document(s) found:**
|
|
20801
|
+
|
|
20802
|
+
${lines.join("\n---\n\n")}`);
|
|
20803
|
+
}
|
|
20804
|
+
function scanMdFiles(dir, rootDir) {
|
|
20805
|
+
if (!existsSync5(dir)) return [];
|
|
20806
|
+
const files = [];
|
|
20807
|
+
try {
|
|
20808
|
+
const entries = readdirSync4(dir, { withFileTypes: true });
|
|
20809
|
+
for (const entry of entries) {
|
|
20810
|
+
const full = join8(dir, entry.name);
|
|
20811
|
+
if (entry.isDirectory()) {
|
|
20812
|
+
files.push(...scanMdFiles(full, rootDir));
|
|
20813
|
+
} else if (entry.name.endsWith(".md")) {
|
|
20814
|
+
files.push(relative(rootDir, full));
|
|
20815
|
+
}
|
|
20816
|
+
}
|
|
20817
|
+
} catch {
|
|
20818
|
+
}
|
|
20819
|
+
return files;
|
|
20820
|
+
}
|
|
20821
|
+
function extractTitle(filePath) {
|
|
20822
|
+
try {
|
|
20823
|
+
const content = readFileSync2(filePath, "utf-8").slice(0, 1e3);
|
|
20824
|
+
const fmMatch = content.match(/^---[\s\S]*?title:\s*(.+?)$/m);
|
|
20825
|
+
if (fmMatch) return fmMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
20826
|
+
const headingMatch = content.match(/^#+\s+(.+)$/m);
|
|
20827
|
+
if (headingMatch) return headingMatch[1].trim();
|
|
20828
|
+
} catch {
|
|
20829
|
+
}
|
|
20830
|
+
return void 0;
|
|
20831
|
+
}
|
|
20832
|
+
async function handleDocScan(adapter2, config2, args) {
|
|
20833
|
+
if (!adapter2.searchDocs) {
|
|
20834
|
+
return errorResponse("Doc registry not available on this adapter.");
|
|
20835
|
+
}
|
|
20836
|
+
const includePlans = args.include_plans ?? false;
|
|
20837
|
+
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
20838
|
+
const registeredPaths = new Set(registered.map((d) => d.path));
|
|
20839
|
+
const docsDir = join8(config2.projectRoot, "docs");
|
|
20840
|
+
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
20841
|
+
const unregisteredDocs = docsFiles.filter((f) => !registeredPaths.has(f));
|
|
20842
|
+
let unregisteredPlans = [];
|
|
20843
|
+
if (includePlans) {
|
|
20844
|
+
const plansDir = join8(homedir2(), ".claude", "plans");
|
|
20845
|
+
if (existsSync5(plansDir)) {
|
|
20846
|
+
const planFiles = scanMdFiles(plansDir, plansDir);
|
|
20847
|
+
unregisteredPlans = planFiles.map((f) => `plans/${f}`).filter((f) => !registeredPaths.has(f)).map((f) => ({
|
|
20848
|
+
path: f,
|
|
20849
|
+
title: extractTitle(join8(plansDir, f.replace("plans/", "")))
|
|
20850
|
+
}));
|
|
20851
|
+
}
|
|
20852
|
+
}
|
|
20853
|
+
const lines = [];
|
|
20854
|
+
if (unregisteredDocs.length === 0 && unregisteredPlans.length === 0) {
|
|
20855
|
+
return textResponse("All docs are registered. No unregistered files found.");
|
|
20856
|
+
}
|
|
20857
|
+
if (unregisteredDocs.length > 0) {
|
|
20858
|
+
lines.push(`## Unregistered Docs (${unregisteredDocs.length})`);
|
|
20859
|
+
for (const f of unregisteredDocs) {
|
|
20860
|
+
const title = extractTitle(join8(config2.projectRoot, f));
|
|
20861
|
+
lines.push(`- \`${f}\`${title ? ` \u2014 ${title}` : ""}`);
|
|
20862
|
+
}
|
|
20863
|
+
}
|
|
20864
|
+
if (unregisteredPlans.length > 0) {
|
|
20865
|
+
lines.push("", `## Unregistered Plans (${unregisteredPlans.length})`);
|
|
20866
|
+
for (const p of unregisteredPlans) {
|
|
20867
|
+
lines.push(`- \`${p.path}\`${p.title ? ` \u2014 ${p.title}` : ""}`);
|
|
20868
|
+
}
|
|
20869
|
+
}
|
|
20870
|
+
lines.push("", `Use \`doc_register\` to register these files.`);
|
|
20871
|
+
return textResponse(lines.join("\n"));
|
|
20101
20872
|
}
|
|
20102
20873
|
|
|
20103
20874
|
// src/tools/orient.ts
|
|
20104
|
-
init_git();
|
|
20105
20875
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
20106
|
-
import { readFileSync as
|
|
20107
|
-
import { join as
|
|
20876
|
+
import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync6 } from "fs";
|
|
20877
|
+
import { join as join9 } from "path";
|
|
20108
20878
|
var orientTool = {
|
|
20109
20879
|
name: "orient",
|
|
20110
20880
|
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.",
|
|
20881
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
20111
20882
|
inputSchema: {
|
|
20112
20883
|
type: "object",
|
|
20113
20884
|
properties: {},
|
|
@@ -20283,8 +21054,8 @@ function getLatestGitTag(projectRoot) {
|
|
|
20283
21054
|
}
|
|
20284
21055
|
function checkNpmVersionDrift() {
|
|
20285
21056
|
try {
|
|
20286
|
-
const pkgPath =
|
|
20287
|
-
const pkg = JSON.parse(
|
|
21057
|
+
const pkgPath = join9(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
|
|
21058
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
|
|
20288
21059
|
const localVersion = pkg.version;
|
|
20289
21060
|
const packageName = pkg.name;
|
|
20290
21061
|
const published = execFileSync3("npm", ["view", packageName, "version"], {
|
|
@@ -20324,6 +21095,17 @@ async function handleOrient(adapter2, config2) {
|
|
|
20324
21095
|
if (!cycleIsComplete && cycleTotal === 0 && cycleDone > 0) {
|
|
20325
21096
|
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
21097
|
}
|
|
21098
|
+
try {
|
|
21099
|
+
const p1BacklogTasks = await adapter2.queryBoard({ status: ["Backlog"], priority: ["P1 High"] });
|
|
21100
|
+
const stalledP1 = p1BacklogTasks.filter(
|
|
21101
|
+
(t) => t.createdCycle != null && currentCycle - t.createdCycle >= 3
|
|
21102
|
+
);
|
|
21103
|
+
if (stalledP1.length > 0) {
|
|
21104
|
+
const ids = stalledP1.map((t) => `${t.displayId} (${currentCycle - (t.createdCycle ?? currentCycle)}+ cycles)`).join(", ");
|
|
21105
|
+
buildResult.warnings.push(`\u26A0\uFE0F P1 tasks stalled 3+ cycles: ${ids}`);
|
|
21106
|
+
}
|
|
21107
|
+
} catch {
|
|
21108
|
+
}
|
|
20327
21109
|
const inProgressItems = buildResult.inProgress.map(
|
|
20328
21110
|
(t) => `- **${t.id}:** ${t.title} (${t.priority} | ${t.complexity})`
|
|
20329
21111
|
);
|
|
@@ -20379,8 +21161,15 @@ ${versionDrift}` : "";
|
|
|
20379
21161
|
try {
|
|
20380
21162
|
const unrecorded = detectUnrecordedCommits(config2.projectRoot, config2.baseBranch);
|
|
20381
21163
|
if (unrecorded.length > 0) {
|
|
21164
|
+
const doneTasks = await adapter2.queryBoard({ status: ["Done"] });
|
|
21165
|
+
const adHocDoneTasks = doneTasks.filter((t) => t.cycle == null);
|
|
21166
|
+
const alreadyRecorded = adHocDoneTasks.length >= unrecorded.length;
|
|
20382
21167
|
const lines = ["\n\n## Unrecorded Work"];
|
|
20383
|
-
|
|
21168
|
+
if (alreadyRecorded) {
|
|
21169
|
+
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.`);
|
|
21170
|
+
} else {
|
|
21171
|
+
lines.push(`${unrecorded.length} commit(s) on ${config2.baseBranch} since last release not captured by \`build_execute\`. Run \`ad_hoc\` to record them.`);
|
|
21172
|
+
}
|
|
20384
21173
|
for (const c of unrecorded) {
|
|
20385
21174
|
lines.push(`- \`${c.hash}\` ${c.message}`);
|
|
20386
21175
|
}
|
|
@@ -20388,6 +21177,23 @@ ${versionDrift}` : "";
|
|
|
20388
21177
|
}
|
|
20389
21178
|
} catch {
|
|
20390
21179
|
}
|
|
21180
|
+
let unregisteredDocsNote = "";
|
|
21181
|
+
try {
|
|
21182
|
+
if (adapter2.searchDocs) {
|
|
21183
|
+
const docsDir = join9(config2.projectRoot, "docs");
|
|
21184
|
+
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
21185
|
+
if (docsFiles.length > 0) {
|
|
21186
|
+
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
21187
|
+
const registeredPaths = new Set(registered.map((d) => d.path));
|
|
21188
|
+
const unregisteredCount = docsFiles.filter((f) => !registeredPaths.has(f)).length;
|
|
21189
|
+
if (unregisteredCount > 0) {
|
|
21190
|
+
unregisteredDocsNote = `
|
|
21191
|
+
\u26A0\uFE0F **${unregisteredCount} unregistered doc(s) in docs/** \u2014 run \`doc_scan\` to review, then \`doc_register\` to index them.`;
|
|
21192
|
+
}
|
|
21193
|
+
}
|
|
21194
|
+
}
|
|
21195
|
+
} catch {
|
|
21196
|
+
}
|
|
20391
21197
|
let recsNote = "";
|
|
20392
21198
|
try {
|
|
20393
21199
|
const pendingRecs = await adapter2.getPendingRecommendations();
|
|
@@ -20421,16 +21227,33 @@ ${versionDrift}` : "";
|
|
|
20421
21227
|
}
|
|
20422
21228
|
} catch {
|
|
20423
21229
|
}
|
|
20424
|
-
|
|
21230
|
+
let unactionedIssuesNote = "";
|
|
21231
|
+
try {
|
|
21232
|
+
const learnings = await adapter2.getCycleLearnings?.({ category: "issue", limit: 20 });
|
|
21233
|
+
if (learnings) {
|
|
21234
|
+
const unactioned = learnings.filter((l) => !l.actionTaken && l.severity && ["P0", "P1", "P2"].includes(l.severity)).slice(0, 5);
|
|
21235
|
+
if (unactioned.length > 0) {
|
|
21236
|
+
const lines = ["\n\n## Unactioned Issues"];
|
|
21237
|
+
for (const issue of unactioned) {
|
|
21238
|
+
const desc = issue.summary.length > 100 ? `${issue.summary.slice(0, 97)}\u2026` : issue.summary;
|
|
21239
|
+
lines.push(`- **${issue.severity}** (C${issue.cycleNumber} / ${issue.taskId}): ${desc}`);
|
|
21240
|
+
}
|
|
21241
|
+
lines.push("_Run `idea` to log these as backlog tasks, or `board_edit` if already handled._");
|
|
21242
|
+
unactionedIssuesNote = lines.join("\n");
|
|
21243
|
+
}
|
|
21244
|
+
}
|
|
21245
|
+
} catch {
|
|
21246
|
+
}
|
|
21247
|
+
return textResponse(formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot) + ttfvNote + reconciliationNote + unrecordedNote + unregisteredDocsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + versionNote + enrichmentNote);
|
|
20425
21248
|
} catch (err) {
|
|
20426
21249
|
const message = err instanceof Error ? err.message : String(err);
|
|
20427
21250
|
return errorResponse(`Orient failed: ${message}`);
|
|
20428
21251
|
}
|
|
20429
21252
|
}
|
|
20430
21253
|
function enrichClaudeMd(projectRoot, cycleNumber) {
|
|
20431
|
-
const claudeMdPath =
|
|
20432
|
-
if (!
|
|
20433
|
-
const content =
|
|
21254
|
+
const claudeMdPath = join9(projectRoot, "CLAUDE.md");
|
|
21255
|
+
if (!existsSync6(claudeMdPath)) return "";
|
|
21256
|
+
const content = readFileSync3(claudeMdPath, "utf-8");
|
|
20434
21257
|
const additions = [];
|
|
20435
21258
|
if (cycleNumber >= 6 && !content.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1)) {
|
|
20436
21259
|
additions.push(CLAUDE_MD_TIER_1);
|
|
@@ -20452,6 +21275,7 @@ function enrichClaudeMd(projectRoot, cycleNumber) {
|
|
|
20452
21275
|
var hierarchyUpdateTool = {
|
|
20453
21276
|
name: "hierarchy_update",
|
|
20454
21277
|
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.",
|
|
21278
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
20455
21279
|
inputSchema: {
|
|
20456
21280
|
type: "object",
|
|
20457
21281
|
properties: {
|
|
@@ -20847,6 +21671,7 @@ async function applyZoomOut(adapter2, llmResponse, cycleNumber) {
|
|
|
20847
21671
|
var zoomOutTool = {
|
|
20848
21672
|
name: "zoom_out",
|
|
20849
21673
|
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.',
|
|
21674
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
20850
21675
|
inputSchema: {
|
|
20851
21676
|
type: "object",
|
|
20852
21677
|
properties: {
|
|
@@ -20921,222 +21746,11 @@ ${result.userMessage}
|
|
|
20921
21746
|
}
|
|
20922
21747
|
}
|
|
20923
21748
|
|
|
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
21749
|
// src/tools/sibling-ads.ts
|
|
21137
21750
|
var getSiblingAdsTool = {
|
|
21138
21751
|
name: "get_sibling_ads",
|
|
21139
21752
|
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.",
|
|
21753
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
21140
21754
|
inputSchema: {
|
|
21141
21755
|
type: "object",
|
|
21142
21756
|
properties: {
|
|
@@ -21318,6 +21932,7 @@ var lastPrepareContextBytes2;
|
|
|
21318
21932
|
var handoffGenerateTool = {
|
|
21319
21933
|
name: "handoff_generate",
|
|
21320
21934
|
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.",
|
|
21935
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
21321
21936
|
inputSchema: {
|
|
21322
21937
|
type: "object",
|
|
21323
21938
|
properties: {
|
|
@@ -21428,12 +22043,14 @@ function isEnabled() {
|
|
|
21428
22043
|
}
|
|
21429
22044
|
function emitTelemetryEvent(event) {
|
|
21430
22045
|
if (!isEnabled()) return;
|
|
22046
|
+
const userId = event.user_id ?? process.env["PAPI_USER_ID"] ?? void 0;
|
|
21431
22047
|
const body = {
|
|
21432
22048
|
project_id: event.project_id,
|
|
21433
22049
|
tool_name: event.tool_name,
|
|
21434
22050
|
event_type: event.event_type,
|
|
21435
22051
|
metadata: event.metadata ?? {}
|
|
21436
22052
|
};
|
|
22053
|
+
if (userId) body["user_id"] = userId;
|
|
21437
22054
|
fetch(`${TELEMETRY_SUPABASE_URL}/rest/v1/telemetry_events`, {
|
|
21438
22055
|
method: "POST",
|
|
21439
22056
|
headers: {
|
|
@@ -21480,7 +22097,6 @@ var TOOLS_REQUIRING_PAPI = /* @__PURE__ */ new Set([
|
|
|
21480
22097
|
"idea",
|
|
21481
22098
|
"bug",
|
|
21482
22099
|
"ad_hoc",
|
|
21483
|
-
"health",
|
|
21484
22100
|
"board_reconcile",
|
|
21485
22101
|
"review_list",
|
|
21486
22102
|
"review_submit",
|
|
@@ -21562,7 +22178,6 @@ function createServer(adapter2, config2) {
|
|
|
21562
22178
|
bugTool,
|
|
21563
22179
|
adHocTool,
|
|
21564
22180
|
boardReconcileTool,
|
|
21565
|
-
healthTool,
|
|
21566
22181
|
releaseTool,
|
|
21567
22182
|
reviewListTool,
|
|
21568
22183
|
reviewSubmitTool,
|
|
@@ -21654,9 +22269,6 @@ function createServer(adapter2, config2) {
|
|
|
21654
22269
|
case "board_reconcile":
|
|
21655
22270
|
result = await handleBoardReconcile(adapter2, config2, safeArgs);
|
|
21656
22271
|
break;
|
|
21657
|
-
case "health":
|
|
21658
|
-
result = await handleHealth(adapter2);
|
|
21659
|
-
break;
|
|
21660
22272
|
case "release":
|
|
21661
22273
|
result = await handleRelease(adapter2, config2, safeArgs);
|
|
21662
22274
|
break;
|