@papi-ai/server 0.5.0 → 0.5.2
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 +1874 -470
- package/dist/prompts.js +1096 -0
- package/package.json +3 -2
- package/skills/check-mcp.md +46 -0
- package/skills/{sprint-status.md → cycle-status.md} +9 -9
- package/skills/papi-audit.md +52 -0
package/dist/index.js
CHANGED
|
@@ -280,6 +280,9 @@ function splitSections(text) {
|
|
|
280
280
|
function parseBulletList(text) {
|
|
281
281
|
return text.split("\n").map((l) => l.replace(/^\s*-\s*/, "").trim()).filter((l) => l.length > 0);
|
|
282
282
|
}
|
|
283
|
+
function parseBulletsOnly(text) {
|
|
284
|
+
return text.split("\n").filter((l) => /^\s*-\s/.test(l)).map((l) => l.replace(/^\s*-\s*/, "").trim()).filter((l) => l.length > 0);
|
|
285
|
+
}
|
|
283
286
|
function parseChecklist(text) {
|
|
284
287
|
return text.split("\n").map((l) => l.replace(/^\s*\[[ x]]\s*/, "").trim()).filter((l) => l.length > 0);
|
|
285
288
|
}
|
|
@@ -314,6 +317,7 @@ function parseBuildHandoff(markdown) {
|
|
|
314
317
|
scopeBoundary: parseBulletList(sections.get("SCOPE BOUNDARY (DO NOT DO THIS)") ?? ""),
|
|
315
318
|
acceptanceCriteria: parseChecklist(sections.get("ACCEPTANCE CRITERIA") ?? ""),
|
|
316
319
|
securityConsiderations: (sections.get("SECURITY CONSIDERATIONS") ?? "").trim(),
|
|
320
|
+
verificationFiles: parseBulletsOnly(sections.get("PRE-BUILD VERIFICATION") ?? ""),
|
|
317
321
|
filesLikelyTouched: parseBulletList(sections.get("FILES LIKELY TOUCHED") ?? ""),
|
|
318
322
|
effort
|
|
319
323
|
};
|
|
@@ -345,6 +349,15 @@ function serializeBuildHandoff(handoff) {
|
|
|
345
349
|
lines.push("");
|
|
346
350
|
lines.push("SECURITY CONSIDERATIONS");
|
|
347
351
|
lines.push(handoff.securityConsiderations);
|
|
352
|
+
if (handoff.verificationFiles && handoff.verificationFiles.length > 0) {
|
|
353
|
+
lines.push("");
|
|
354
|
+
lines.push("PRE-BUILD VERIFICATION");
|
|
355
|
+
lines.push("Before implementing, read these files and check if the functionality already exists:");
|
|
356
|
+
for (const item of handoff.verificationFiles) {
|
|
357
|
+
lines.push(`- ${item}`);
|
|
358
|
+
}
|
|
359
|
+
lines.push('If >80% of the scope is already implemented, call build_execute with completed="yes" and note "already built" in surprises instead of re-implementing.');
|
|
360
|
+
}
|
|
348
361
|
lines.push("");
|
|
349
362
|
lines.push("FILES LIKELY TOUCHED");
|
|
350
363
|
for (const item of handoff.filesLikelyTouched) {
|
|
@@ -832,28 +845,6 @@ function parseCostSnapshots(content) {
|
|
|
832
845
|
}
|
|
833
846
|
return snapshots;
|
|
834
847
|
}
|
|
835
|
-
function serializeCostSnapshot(snapshot) {
|
|
836
|
-
return `| ${snapshot.cycle} | ${snapshot.date} | ${snapshot.totalCostUsd.toFixed(4)} | ${formatNumber(snapshot.totalInputTokens)} | ${formatNumber(snapshot.totalOutputTokens)} | ${formatNumber(snapshot.totalCalls)} |`;
|
|
837
|
-
}
|
|
838
|
-
function writeCostSnapshotToContent(snapshot, content) {
|
|
839
|
-
if (!content.includes(COST_SECTION_HEADING)) {
|
|
840
|
-
return content.trimEnd() + "\n\n" + COST_SECTION_HEADING + "\n\n" + COST_TABLE_HEADER + "\n" + COST_TABLE_SEPARATOR + "\n" + serializeCostSnapshot(snapshot) + "\n";
|
|
841
|
-
}
|
|
842
|
-
const lines = content.split("\n");
|
|
843
|
-
const cyclePrefix = `| ${snapshot.cycle} |`;
|
|
844
|
-
let replaced = false;
|
|
845
|
-
for (let i = 0; i < lines.length; i++) {
|
|
846
|
-
if (lines[i].startsWith(cyclePrefix)) {
|
|
847
|
-
lines[i] = serializeCostSnapshot(snapshot);
|
|
848
|
-
replaced = true;
|
|
849
|
-
break;
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
if (replaced) {
|
|
853
|
-
return lines.join("\n");
|
|
854
|
-
}
|
|
855
|
-
return content.trimEnd() + "\n" + serializeCostSnapshot(snapshot) + "\n";
|
|
856
|
-
}
|
|
857
848
|
function effortOrdinal(effort) {
|
|
858
849
|
const normalized = effort.trim().toUpperCase();
|
|
859
850
|
return EFFORT_SCALE[normalized];
|
|
@@ -1403,7 +1394,7 @@ async function detectReviewPatterns(reviews, currentCycle, window = 5, clusterer
|
|
|
1403
1394
|
function hasReviewPatterns(patterns) {
|
|
1404
1395
|
return patterns.recurringFeedback.length > 0 || patterns.requestChangesRate >= 50;
|
|
1405
1396
|
}
|
|
1406
|
-
var VALID_TRANSITIONS2, TASK_TYPE_TIERS, VALID_EFFORT_SIZES, SECTION_HEADERS, YAML_MARKER, YAML_START, YAML_END, VALID_EFFORT_SIZES2, HEADER_SENTINEL, TABLE_HEADER, TABLE_SEPARATOR, PREV_TABLE_HEADER, LEGACY_TABLE_HEADER, SECTION_HEADING, FILE_TEMPLATE, COST_SECTION_HEADING,
|
|
1397
|
+
var VALID_TRANSITIONS2, TASK_TYPE_TIERS, VALID_EFFORT_SIZES, SECTION_HEADERS, YAML_MARKER, YAML_START, YAML_END, VALID_EFFORT_SIZES2, HEADER_SENTINEL, TABLE_HEADER, TABLE_SEPARATOR, PREV_TABLE_HEADER, LEGACY_TABLE_HEADER, SECTION_HEADING, FILE_TEMPLATE, COST_SECTION_HEADING, COST_TABLE_SEPARATOR, FILE_HEADING, ACCURACY_HEADER, ACCURACY_SEPARATOR, VELOCITY_HEADER, VELOCITY_SEPARATOR, EFFORT_SCALE, NONE_PATTERN, HEADER_SENTINEL2, VALID_STAGES, VALID_VERDICTS, STAGE_DISPLAY, VALID_STATUSES, PHASES_START, PHASES_END, YAML_MARKER2, YAML_START2, YAML_END2, VALID_STATUSES2, YAML_MARKER3, YAML_START3, YAML_END3, MdFileAdapter, NONE_PATTERN2;
|
|
1407
1398
|
var init_dist2 = __esm({
|
|
1408
1399
|
"../adapter-md/dist/index.js"() {
|
|
1409
1400
|
"use strict";
|
|
@@ -1422,6 +1413,7 @@ var init_dist2 = __esm({
|
|
|
1422
1413
|
"SCOPE BOUNDARY (DO NOT DO THIS)",
|
|
1423
1414
|
"ACCEPTANCE CRITERIA",
|
|
1424
1415
|
"SECURITY CONSIDERATIONS",
|
|
1416
|
+
"PRE-BUILD VERIFICATION",
|
|
1425
1417
|
"FILES LIKELY TOUCHED",
|
|
1426
1418
|
"EFFORT"
|
|
1427
1419
|
];
|
|
@@ -1443,7 +1435,6 @@ ${TABLE_HEADER}
|
|
|
1443
1435
|
${TABLE_SEPARATOR}
|
|
1444
1436
|
`;
|
|
1445
1437
|
COST_SECTION_HEADING = "## Cost Summary";
|
|
1446
|
-
COST_TABLE_HEADER = "| Cycle | Date | Total Cost ($) | Input Tokens | Output Tokens | Calls |";
|
|
1447
1438
|
COST_TABLE_SEPARATOR = "|--------|------|----------------|--------------|---------------|-------|";
|
|
1448
1439
|
FILE_HEADING = "# Cycle Methodology Metrics";
|
|
1449
1440
|
ACCURACY_HEADER = "| Cycle | Reports | Match Rate | MAE | Bias |";
|
|
@@ -1724,6 +1715,11 @@ ${TABLE_SEPARATOR}
|
|
|
1724
1715
|
const reports = parseBuildReports(await this.read("BUILD_REPORTS.md"));
|
|
1725
1716
|
return reports.slice(0, count);
|
|
1726
1717
|
}
|
|
1718
|
+
/** Return the number of build reports for a specific task. */
|
|
1719
|
+
async getBuildReportCountForTask(taskId) {
|
|
1720
|
+
const reports = parseBuildReports(await this.read("BUILD_REPORTS.md"));
|
|
1721
|
+
return reports.filter((r) => r.taskId === taskId).length;
|
|
1722
|
+
}
|
|
1727
1723
|
/** Return all build reports from cycles >= {@link cycleNumber}. */
|
|
1728
1724
|
async getBuildReportsSince(cycleNumber) {
|
|
1729
1725
|
const reports = parseBuildReports(await this.read("BUILD_REPORTS.md"));
|
|
@@ -1884,11 +1880,6 @@ ${newSection}
|
|
|
1884
1880
|
const metrics = await this.readToolMetrics();
|
|
1885
1881
|
return aggregateCostSummary(metrics, cycleNumber);
|
|
1886
1882
|
}
|
|
1887
|
-
/** Write a cost snapshot to the Cost Summary section of METRICS.md. */
|
|
1888
|
-
async writeCostSnapshot(snapshot) {
|
|
1889
|
-
const content = await this.readOptional("METRICS.md");
|
|
1890
|
-
await this.write("METRICS.md", writeCostSnapshotToContent(snapshot, content));
|
|
1891
|
-
}
|
|
1892
1883
|
/** Read all cost snapshots from the Cost Summary section of METRICS.md. */
|
|
1893
1884
|
async getCostSnapshots() {
|
|
1894
1885
|
const content = await this.readOptional("METRICS.md");
|
|
@@ -4540,7 +4531,7 @@ function rowToDecisionScore(row) {
|
|
|
4540
4531
|
createdAt: row.created_at
|
|
4541
4532
|
};
|
|
4542
4533
|
}
|
|
4543
|
-
function
|
|
4534
|
+
function rowToCycleLogEntry(row) {
|
|
4544
4535
|
const entry = {
|
|
4545
4536
|
uuid: row.id,
|
|
4546
4537
|
cycleNumber: row.cycle_number,
|
|
@@ -5571,6 +5562,8 @@ CREATE TABLE IF NOT EXISTS strategy_reviews (
|
|
|
5571
5562
|
full_analysis TEXT,
|
|
5572
5563
|
velocity_assessment TEXT,
|
|
5573
5564
|
structured_data JSONB,
|
|
5565
|
+
review_number INTEGER,
|
|
5566
|
+
review_type TEXT,
|
|
5574
5567
|
PRIMARY KEY (id)
|
|
5575
5568
|
);
|
|
5576
5569
|
|
|
@@ -6133,14 +6126,14 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
|
6133
6126
|
ORDER BY cycle_number DESC
|
|
6134
6127
|
LIMIT ${limit}
|
|
6135
6128
|
`;
|
|
6136
|
-
return rows2.map(
|
|
6129
|
+
return rows2.map(rowToCycleLogEntry);
|
|
6137
6130
|
}
|
|
6138
6131
|
const rows = await this.sql`
|
|
6139
6132
|
SELECT * FROM planning_log_entries
|
|
6140
6133
|
WHERE project_id = ${this.projectId}
|
|
6141
6134
|
ORDER BY cycle_number DESC
|
|
6142
6135
|
`;
|
|
6143
|
-
return rows.map(
|
|
6136
|
+
return rows.map(rowToCycleLogEntry);
|
|
6144
6137
|
}
|
|
6145
6138
|
async getCycleLogSince(cycleNumber) {
|
|
6146
6139
|
const rows = await this.sql`
|
|
@@ -6149,7 +6142,7 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
|
6149
6142
|
AND cycle_number >= ${cycleNumber}
|
|
6150
6143
|
ORDER BY cycle_number DESC
|
|
6151
6144
|
`;
|
|
6152
|
-
return rows.map(
|
|
6145
|
+
return rows.map(rowToCycleLogEntry);
|
|
6153
6146
|
}
|
|
6154
6147
|
async setCycleHealth(updates) {
|
|
6155
6148
|
if (updates.boardHealth != null || updates.strategicDirection != null) {
|
|
@@ -6193,11 +6186,21 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6193
6186
|
`;
|
|
6194
6187
|
}
|
|
6195
6188
|
async writeStrategyReview(review) {
|
|
6189
|
+
let reviewNumber = review.reviewNumber ?? null;
|
|
6190
|
+
if (reviewNumber == null && review.cycleNumber > 0) {
|
|
6191
|
+
const [row] = await this.sql`
|
|
6192
|
+
SELECT MAX(review_number) as max_num FROM strategy_reviews
|
|
6193
|
+
WHERE project_id = ${this.projectId} AND cycle_number > 0
|
|
6194
|
+
`;
|
|
6195
|
+
reviewNumber = (row?.max_num ?? 0) + 1;
|
|
6196
|
+
}
|
|
6197
|
+
const reviewType = review.reviewType ?? "scheduled";
|
|
6196
6198
|
await this.sql`
|
|
6197
6199
|
INSERT INTO strategy_reviews (
|
|
6198
6200
|
project_id, cycle_number, cycle_range, title, content, notes,
|
|
6199
6201
|
board_health, strategic_direction, recommendations,
|
|
6200
|
-
full_analysis, velocity_assessment, structured_data
|
|
6202
|
+
full_analysis, velocity_assessment, structured_data,
|
|
6203
|
+
review_number, review_type
|
|
6201
6204
|
)
|
|
6202
6205
|
VALUES (
|
|
6203
6206
|
${this.projectId},
|
|
@@ -6208,10 +6211,12 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6208
6211
|
${review.notes ?? null},
|
|
6209
6212
|
${review.boardHealth ?? null},
|
|
6210
6213
|
${review.strategicDirection ?? null},
|
|
6211
|
-
${review.recommendations ?
|
|
6214
|
+
${review.recommendations ? this.sql.json(review.recommendations) : null},
|
|
6212
6215
|
${review.fullAnalysis ?? null},
|
|
6213
6216
|
${review.velocityAssessment ?? null},
|
|
6214
|
-
${review.structuredData ?
|
|
6217
|
+
${review.structuredData ? this.sql.json(review.structuredData) : null},
|
|
6218
|
+
${reviewNumber},
|
|
6219
|
+
${reviewType}
|
|
6215
6220
|
)
|
|
6216
6221
|
ON CONFLICT (project_id, cycle_number)
|
|
6217
6222
|
DO UPDATE SET
|
|
@@ -6224,7 +6229,9 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6224
6229
|
recommendations = EXCLUDED.recommendations,
|
|
6225
6230
|
full_analysis = EXCLUDED.full_analysis,
|
|
6226
6231
|
velocity_assessment = EXCLUDED.velocity_assessment,
|
|
6227
|
-
structured_data = EXCLUDED.structured_data
|
|
6232
|
+
structured_data = EXCLUDED.structured_data,
|
|
6233
|
+
review_number = EXCLUDED.review_number,
|
|
6234
|
+
review_type = EXCLUDED.review_type
|
|
6228
6235
|
`;
|
|
6229
6236
|
}
|
|
6230
6237
|
async getLastStrategyReviewCycle() {
|
|
@@ -6241,9 +6248,10 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6241
6248
|
const rows2 = await this.sql`
|
|
6242
6249
|
SELECT cycle_number, cycle_range, title, content, notes,
|
|
6243
6250
|
board_health, strategic_direction, full_analysis,
|
|
6244
|
-
velocity_assessment, structured_data, created_at
|
|
6251
|
+
velocity_assessment, structured_data, created_at,
|
|
6252
|
+
review_number, review_type
|
|
6245
6253
|
FROM strategy_reviews
|
|
6246
|
-
WHERE project_id = ${this.projectId}
|
|
6254
|
+
WHERE project_id = ${this.projectId} AND cycle_number > 0
|
|
6247
6255
|
ORDER BY cycle_number DESC
|
|
6248
6256
|
LIMIT ${limit}
|
|
6249
6257
|
`;
|
|
@@ -6258,13 +6266,15 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6258
6266
|
fullAnalysis: r.full_analysis ?? void 0,
|
|
6259
6267
|
velocityAssessment: r.velocity_assessment ?? void 0,
|
|
6260
6268
|
structuredData: r.structured_data ?? void 0,
|
|
6261
|
-
createdAt: r.created_at ?? void 0
|
|
6269
|
+
createdAt: r.created_at ?? void 0,
|
|
6270
|
+
reviewNumber: r.review_number ?? void 0,
|
|
6271
|
+
reviewType: r.review_type ?? void 0
|
|
6262
6272
|
}));
|
|
6263
6273
|
}
|
|
6264
6274
|
const rows = await this.sql`
|
|
6265
6275
|
SELECT cycle_number, cycle_range, title, content, notes,
|
|
6266
6276
|
board_health, strategic_direction, velocity_assessment,
|
|
6267
|
-
structured_data, created_at
|
|
6277
|
+
structured_data, created_at, review_number, review_type
|
|
6268
6278
|
FROM strategy_reviews
|
|
6269
6279
|
WHERE project_id = ${this.projectId}
|
|
6270
6280
|
ORDER BY cycle_number DESC
|
|
@@ -6280,9 +6290,144 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6280
6290
|
strategicDirection: r.strategic_direction ?? void 0,
|
|
6281
6291
|
velocityAssessment: r.velocity_assessment ?? void 0,
|
|
6282
6292
|
structuredData: r.structured_data ?? void 0,
|
|
6283
|
-
createdAt: r.created_at ?? void 0
|
|
6293
|
+
createdAt: r.created_at ?? void 0,
|
|
6294
|
+
reviewNumber: r.review_number ?? void 0,
|
|
6295
|
+
reviewType: r.review_type ?? void 0
|
|
6296
|
+
}));
|
|
6297
|
+
}
|
|
6298
|
+
async savePendingReviewResponse(cycleNumber, rawResponse) {
|
|
6299
|
+
await this.sql`
|
|
6300
|
+
INSERT INTO strategy_reviews (
|
|
6301
|
+
project_id, cycle_number, title, content, full_analysis
|
|
6302
|
+
) VALUES (
|
|
6303
|
+
${this.projectId}, ${0}, ${"[PENDING] Strategy Review"}, ${"Pending write-back retry"}, ${rawResponse}
|
|
6304
|
+
)
|
|
6305
|
+
ON CONFLICT (project_id, cycle_number)
|
|
6306
|
+
DO UPDATE SET
|
|
6307
|
+
full_analysis = ${rawResponse},
|
|
6308
|
+
notes = ${`original_cycle:${cycleNumber}`}
|
|
6309
|
+
`;
|
|
6310
|
+
}
|
|
6311
|
+
async getPendingReviewResponse() {
|
|
6312
|
+
const rows = await this.sql`
|
|
6313
|
+
SELECT full_analysis, notes FROM strategy_reviews
|
|
6314
|
+
WHERE project_id = ${this.projectId} AND cycle_number = 0
|
|
6315
|
+
LIMIT 1
|
|
6316
|
+
`;
|
|
6317
|
+
if (rows.length === 0 || !rows[0].full_analysis) return null;
|
|
6318
|
+
const cycleMatch = rows[0].notes?.match(/original_cycle:(\d+)/);
|
|
6319
|
+
const cycleNumber = cycleMatch ? parseInt(cycleMatch[1], 10) : 0;
|
|
6320
|
+
return { cycleNumber, rawResponse: rows[0].full_analysis };
|
|
6321
|
+
}
|
|
6322
|
+
async clearPendingReviewResponse() {
|
|
6323
|
+
await this.sql`
|
|
6324
|
+
DELETE FROM strategy_reviews
|
|
6325
|
+
WHERE project_id = ${this.projectId} AND cycle_number = 0
|
|
6326
|
+
`;
|
|
6327
|
+
}
|
|
6328
|
+
// -------------------------------------------------------------------------
|
|
6329
|
+
// Doc Registry
|
|
6330
|
+
// -------------------------------------------------------------------------
|
|
6331
|
+
async registerDoc(entry) {
|
|
6332
|
+
const [row] = await this.sql`
|
|
6333
|
+
INSERT INTO doc_registry (
|
|
6334
|
+
project_id, title, type, path, status, summary, tags,
|
|
6335
|
+
cycle_created, cycle_updated, superseded_by, actions
|
|
6336
|
+
) VALUES (
|
|
6337
|
+
${this.projectId}, ${entry.title}, ${entry.type}, ${entry.path},
|
|
6338
|
+
${entry.status}, ${entry.summary}, ${entry.tags},
|
|
6339
|
+
${entry.cycleCreated}, ${entry.cycleUpdated ?? null},
|
|
6340
|
+
${entry.supersededBy ?? null},
|
|
6341
|
+
${entry.actions ? this.sql.json(entry.actions) : this.sql.json([])}
|
|
6342
|
+
)
|
|
6343
|
+
ON CONFLICT (project_id, path)
|
|
6344
|
+
DO UPDATE SET
|
|
6345
|
+
title = EXCLUDED.title,
|
|
6346
|
+
type = EXCLUDED.type,
|
|
6347
|
+
status = EXCLUDED.status,
|
|
6348
|
+
summary = EXCLUDED.summary,
|
|
6349
|
+
tags = EXCLUDED.tags,
|
|
6350
|
+
cycle_updated = EXCLUDED.cycle_updated,
|
|
6351
|
+
superseded_by = EXCLUDED.superseded_by,
|
|
6352
|
+
actions = EXCLUDED.actions,
|
|
6353
|
+
updated_at = now()
|
|
6354
|
+
RETURNING id, created_at, updated_at
|
|
6355
|
+
`;
|
|
6356
|
+
return {
|
|
6357
|
+
...entry,
|
|
6358
|
+
id: row.id,
|
|
6359
|
+
createdAt: row.created_at,
|
|
6360
|
+
updatedAt: row.updated_at
|
|
6361
|
+
};
|
|
6362
|
+
}
|
|
6363
|
+
async searchDocs(input) {
|
|
6364
|
+
const status = input.status ?? "active";
|
|
6365
|
+
const limit = input.limit ?? 10;
|
|
6366
|
+
const keyword = input.keyword ? `%${input.keyword}%` : null;
|
|
6367
|
+
const sinceCycle = input.sinceCycle ?? 0;
|
|
6368
|
+
const hasPending = input.hasPendingActions ?? false;
|
|
6369
|
+
const rows = await this.sql`
|
|
6370
|
+
SELECT * FROM doc_registry
|
|
6371
|
+
WHERE project_id = ${this.projectId}
|
|
6372
|
+
AND status = ${status}
|
|
6373
|
+
AND (${input.type ?? null}::text IS NULL OR type = ${input.type ?? null})
|
|
6374
|
+
AND (${keyword}::text IS NULL OR (title ILIKE ${keyword} OR summary ILIKE ${keyword}))
|
|
6375
|
+
AND (${sinceCycle} = 0 OR COALESCE(cycle_updated, cycle_created) >= ${sinceCycle})
|
|
6376
|
+
AND (${hasPending} = false OR actions::text LIKE '%"pending"%')
|
|
6377
|
+
AND (${input.tags ?? []}::text[] = '{}' OR tags && ${input.tags ?? []})
|
|
6378
|
+
ORDER BY COALESCE(cycle_updated, cycle_created) DESC
|
|
6379
|
+
LIMIT ${limit}
|
|
6380
|
+
`;
|
|
6381
|
+
return rows.map((r) => ({
|
|
6382
|
+
id: r.id,
|
|
6383
|
+
title: r.title,
|
|
6384
|
+
type: r.type,
|
|
6385
|
+
path: r.path,
|
|
6386
|
+
status: r.status,
|
|
6387
|
+
summary: r.summary,
|
|
6388
|
+
tags: r.tags ?? [],
|
|
6389
|
+
cycleCreated: r.cycle_created,
|
|
6390
|
+
cycleUpdated: r.cycle_updated ?? void 0,
|
|
6391
|
+
supersededBy: r.superseded_by ?? void 0,
|
|
6392
|
+
actions: r.actions,
|
|
6393
|
+
createdAt: r.created_at,
|
|
6394
|
+
updatedAt: r.updated_at
|
|
6284
6395
|
}));
|
|
6285
6396
|
}
|
|
6397
|
+
async getDoc(idOrPath) {
|
|
6398
|
+
const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(idOrPath);
|
|
6399
|
+
const rows = isUuid ? await this.sql`
|
|
6400
|
+
SELECT * FROM doc_registry WHERE id = ${idOrPath} AND project_id = ${this.projectId}
|
|
6401
|
+
` : await this.sql`
|
|
6402
|
+
SELECT * FROM doc_registry WHERE path = ${idOrPath} AND project_id = ${this.projectId}
|
|
6403
|
+
`;
|
|
6404
|
+
if (rows.length === 0) return null;
|
|
6405
|
+
const r = rows[0];
|
|
6406
|
+
return {
|
|
6407
|
+
id: r.id,
|
|
6408
|
+
title: r.title,
|
|
6409
|
+
type: r.type,
|
|
6410
|
+
path: r.path,
|
|
6411
|
+
status: r.status,
|
|
6412
|
+
summary: r.summary,
|
|
6413
|
+
tags: r.tags ?? [],
|
|
6414
|
+
cycleCreated: r.cycle_created,
|
|
6415
|
+
cycleUpdated: r.cycle_updated ?? void 0,
|
|
6416
|
+
supersededBy: r.superseded_by ?? void 0,
|
|
6417
|
+
actions: r.actions,
|
|
6418
|
+
createdAt: r.created_at,
|
|
6419
|
+
updatedAt: r.updated_at
|
|
6420
|
+
};
|
|
6421
|
+
}
|
|
6422
|
+
async updateDocStatus(id, status, supersededBy) {
|
|
6423
|
+
await this.sql`
|
|
6424
|
+
UPDATE doc_registry
|
|
6425
|
+
SET status = ${status},
|
|
6426
|
+
superseded_by = ${supersededBy ?? null},
|
|
6427
|
+
updated_at = now()
|
|
6428
|
+
WHERE id = ${id} AND project_id = ${this.projectId}
|
|
6429
|
+
`;
|
|
6430
|
+
}
|
|
6286
6431
|
async writeDogfoodEntries(entries) {
|
|
6287
6432
|
if (entries.length === 0) return;
|
|
6288
6433
|
const values2 = entries.map((entry) => ({
|
|
@@ -6458,8 +6603,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6458
6603
|
${task.reviewed}, ${task.cycle ?? null}, ${task.createdCycle ?? null},
|
|
6459
6604
|
${task.why ?? null}, ${task.dependsOn ?? null}, ${task.notes ?? null},
|
|
6460
6605
|
${task.closureReason ?? null},
|
|
6461
|
-
${
|
|
6462
|
-
${task.buildHandoff ?
|
|
6606
|
+
${this.sql.json(task.stateHistory ?? [])},
|
|
6607
|
+
${task.buildHandoff ? this.sql.json(task.buildHandoff) : null},
|
|
6463
6608
|
${task.buildReport ?? null},
|
|
6464
6609
|
${task.taskType ?? null},
|
|
6465
6610
|
${task.maturity ?? null},
|
|
@@ -6547,9 +6692,9 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6547
6692
|
${normaliseEffort(report.actualEffort)}, ${normaliseEffort(report.estimatedEffort)}, ${report.scopeAccuracy},
|
|
6548
6693
|
${report.surprises}, ${report.discoveredIssues}, ${report.architectureNotes},
|
|
6549
6694
|
${report.commitSha ?? null}, ${report.filesChanged ?? []}, ${report.relatedDecisions ?? []},
|
|
6550
|
-
${report.handoffAccuracy ?
|
|
6695
|
+
${report.handoffAccuracy ? this.sql.json(report.handoffAccuracy) : null},
|
|
6551
6696
|
${report.correctionsCount ?? 0},
|
|
6552
|
-
${report.briefImplications ?
|
|
6697
|
+
${report.briefImplications ? this.sql.json(report.briefImplications) : null},
|
|
6553
6698
|
${report.deadEnds ?? null}
|
|
6554
6699
|
)
|
|
6555
6700
|
`;
|
|
@@ -6563,6 +6708,13 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6563
6708
|
`;
|
|
6564
6709
|
return rows.map(rowToBuildReport);
|
|
6565
6710
|
}
|
|
6711
|
+
async getBuildReportCountForTask(taskId) {
|
|
6712
|
+
const rows = await this.sql`
|
|
6713
|
+
SELECT COUNT(*)::text AS count FROM build_reports
|
|
6714
|
+
WHERE project_id = ${this.projectId} AND task_id = ${taskId}
|
|
6715
|
+
`;
|
|
6716
|
+
return parseInt(rows[0]?.count ?? "0", 10);
|
|
6717
|
+
}
|
|
6566
6718
|
async getBuildReportsSince(cycleNumber) {
|
|
6567
6719
|
const rows = await this.sql`
|
|
6568
6720
|
SELECT * FROM build_reports
|
|
@@ -6601,7 +6753,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6601
6753
|
${review.reviewer}, ${review.verdict}, ${review.cycle},
|
|
6602
6754
|
${review.date}, ${review.comments},
|
|
6603
6755
|
${review.handoffRevision ?? null}, ${review.buildCommitSha ?? null},
|
|
6604
|
-
${review.autoReview ?
|
|
6756
|
+
${review.autoReview ? this.sql.json(review.autoReview) : null}
|
|
6605
6757
|
)
|
|
6606
6758
|
`;
|
|
6607
6759
|
}
|
|
@@ -6824,25 +6976,6 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6824
6976
|
avgCostPerCall: metrics.length > 0 ? totalCostUsd / metrics.length : 0
|
|
6825
6977
|
};
|
|
6826
6978
|
}
|
|
6827
|
-
async writeCostSnapshot(snapshot) {
|
|
6828
|
-
await this.sql`
|
|
6829
|
-
INSERT INTO cost_snapshots (
|
|
6830
|
-
project_id, cycle, date, total_cost_usd,
|
|
6831
|
-
total_input_tokens, total_output_tokens, total_calls
|
|
6832
|
-
) VALUES (
|
|
6833
|
-
${this.projectId}, ${snapshot.cycle}, ${snapshot.date},
|
|
6834
|
-
${snapshot.totalCostUsd}, ${snapshot.totalInputTokens},
|
|
6835
|
-
${snapshot.totalOutputTokens}, ${snapshot.totalCalls}
|
|
6836
|
-
)
|
|
6837
|
-
ON CONFLICT (project_id, cycle)
|
|
6838
|
-
DO UPDATE SET
|
|
6839
|
-
date = EXCLUDED.date,
|
|
6840
|
-
total_cost_usd = EXCLUDED.total_cost_usd,
|
|
6841
|
-
total_input_tokens = EXCLUDED.total_input_tokens,
|
|
6842
|
-
total_output_tokens = EXCLUDED.total_output_tokens,
|
|
6843
|
-
total_calls = EXCLUDED.total_calls
|
|
6844
|
-
`;
|
|
6845
|
-
}
|
|
6846
6979
|
async getCostSnapshots() {
|
|
6847
6980
|
const rows = await this.sql`
|
|
6848
6981
|
SELECT * FROM cost_snapshots
|
|
@@ -6907,7 +7040,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6907
7040
|
${this.projectId}, ${cycle.number}, ${cycle.status},
|
|
6908
7041
|
${cycle.startDate}, ${cycle.endDate ?? null},
|
|
6909
7042
|
${cycle.goals}, ${cycle.boardHealth}, ${resolvedTaskIds},
|
|
6910
|
-
${cycle.contextHashes ?
|
|
7043
|
+
${cycle.contextHashes ? this.sql.json(cycle.contextHashes) : null}
|
|
6911
7044
|
)
|
|
6912
7045
|
ON CONFLICT (project_id, number)
|
|
6913
7046
|
DO UPDATE SET
|
|
@@ -6964,6 +7097,12 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6964
7097
|
await this.sql`
|
|
6965
7098
|
UPDATE horizons SET status = ${status}, updated_at = NOW()
|
|
6966
7099
|
WHERE id = ${horizonId} AND project_id = ${this.projectId}
|
|
7100
|
+
`;
|
|
7101
|
+
}
|
|
7102
|
+
async updatePhaseStatus(phaseId, status) {
|
|
7103
|
+
await this.sql`
|
|
7104
|
+
UPDATE phases SET status = ${status}, updated_at = NOW()
|
|
7105
|
+
WHERE id = ${phaseId} AND project_id = ${this.projectId}
|
|
6967
7106
|
`;
|
|
6968
7107
|
}
|
|
6969
7108
|
async getActiveStage() {
|
|
@@ -7045,7 +7184,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
7045
7184
|
status: "pending",
|
|
7046
7185
|
content: r.content,
|
|
7047
7186
|
createdCycle: r.created_cycle,
|
|
7048
|
-
|
|
7187
|
+
actionedCycle: r.actioned_cycle ?? void 0,
|
|
7049
7188
|
target: r.target ?? void 0
|
|
7050
7189
|
}));
|
|
7051
7190
|
}
|
|
@@ -7148,11 +7287,14 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
7148
7287
|
const over = acc?.over ?? "0";
|
|
7149
7288
|
const matchRate = total > 0 ? Math.round(parseInt(matches, 10) / total * 100) : 0;
|
|
7150
7289
|
const velocityStr = raw.velocity.map((r) => `Cycle ${r.cycle}: ${r.count} tasks`).join(", ");
|
|
7151
|
-
const topSurprises = raw.surprises.slice(0, 3).map((s) => `- ${s.length > 150 ? s.slice(0, 150) + "..." : s}`).join("\n");
|
|
7290
|
+
const topSurprises = raw.surprises.filter((s) => s.text && !["None", "none", "N/A", ""].includes(s.text)).slice(0, 3).map((s) => `- ${s.text.length > 150 ? s.text.slice(0, 150) + "..." : s.text}`).join("\n");
|
|
7291
|
+
const topDeadEnds = raw.surprises.filter((s) => s.deadEnds && !["None", "none", "N/A", ""].includes(s.deadEnds)).slice(0, 3).map((s) => `- ${s.deadEnds.length > 150 ? s.deadEnds.slice(0, 150) + "..." : s.deadEnds}`).join("\n");
|
|
7152
7292
|
const buildIntelligence = `**Estimation:** ${matchRate}% match rate (${matches}/${total}), ${under} under-estimated, ${over} over-estimated.
|
|
7153
7293
|
**Velocity (last 5 cycles):** ${velocityStr || "No data"}
|
|
7154
7294
|
` + (topSurprises ? `**Recent surprises:**
|
|
7155
|
-
${topSurprises}
|
|
7295
|
+
${topSurprises}
|
|
7296
|
+
` : "") + (topDeadEnds ? `**Recent dead ends:**
|
|
7297
|
+
${topDeadEnds}` : "");
|
|
7156
7298
|
const cycleLog = raw.cycleLog.length === 0 ? "No cycle log entries yet." : raw.cycleLog.map(
|
|
7157
7299
|
(r) => `### Cycle ${r.cycle_number} \u2014 ${r.title}
|
|
7158
7300
|
${r.content}` + (r.carry_forward ? `
|
|
@@ -7237,12 +7379,12 @@ ${r.content}` + (r.carry_forward ? `
|
|
|
7237
7379
|
ORDER BY cycle DESC
|
|
7238
7380
|
LIMIT 5
|
|
7239
7381
|
`,
|
|
7240
|
-
// Build intelligence: recent surprises
|
|
7382
|
+
// Build intelligence: recent surprises + dead ends
|
|
7241
7383
|
this.sql`
|
|
7242
|
-
SELECT surprises
|
|
7384
|
+
SELECT surprises, dead_ends
|
|
7243
7385
|
FROM build_reports
|
|
7244
7386
|
WHERE project_id = ${this.projectId}
|
|
7245
|
-
AND surprises NOT IN ('None', 'none', 'N/A', '')
|
|
7387
|
+
AND (surprises NOT IN ('None', 'none', 'N/A', '') OR dead_ends IS NOT NULL)
|
|
7246
7388
|
ORDER BY cycle DESC
|
|
7247
7389
|
LIMIT 10
|
|
7248
7390
|
`,
|
|
@@ -7276,7 +7418,7 @@ ${r.content}` + (r.carry_forward ? `
|
|
|
7276
7418
|
board: [...boardRows],
|
|
7277
7419
|
accuracy: accuracyRows[0] ?? { total: "0", matches: "0", over: "0", under: "0" },
|
|
7278
7420
|
velocity: [...velocityRows],
|
|
7279
|
-
surprises: surpriseRows.map((r) => r.surprises),
|
|
7421
|
+
surprises: surpriseRows.map((r) => ({ text: r.surprises, deadEnds: r.dead_ends })),
|
|
7280
7422
|
cycleLog: [...logRows],
|
|
7281
7423
|
activeDecisions: [...adRows]
|
|
7282
7424
|
});
|
|
@@ -7328,7 +7470,7 @@ ${r.content}` + (r.carry_forward ? `
|
|
|
7328
7470
|
}));
|
|
7329
7471
|
await this.sql`INSERT INTO entity_references ${this.sql(values2)}`;
|
|
7330
7472
|
}
|
|
7331
|
-
async getDecisionUsage(
|
|
7473
|
+
async getDecisionUsage(currentCycle) {
|
|
7332
7474
|
const rows = await this.sql`
|
|
7333
7475
|
SELECT decision_id, reference_count, last_referenced_cycle
|
|
7334
7476
|
FROM v_decision_usage
|
|
@@ -7338,7 +7480,7 @@ ${r.content}` + (r.carry_forward ? `
|
|
|
7338
7480
|
decisionId: r.decision_id,
|
|
7339
7481
|
referenceCount: parseInt(r.reference_count, 10),
|
|
7340
7482
|
lastReferencedCycle: r.last_referenced_cycle,
|
|
7341
|
-
cyclesSinceLastReference:
|
|
7483
|
+
cyclesSinceLastReference: currentCycle - r.last_referenced_cycle
|
|
7342
7484
|
}));
|
|
7343
7485
|
}
|
|
7344
7486
|
async getContextUtilisation() {
|
|
@@ -7435,8 +7577,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
7435
7577
|
depends_on: task.dependsOn ?? null,
|
|
7436
7578
|
notes: task.notes ?? null,
|
|
7437
7579
|
closure_reason: task.closureReason ?? null,
|
|
7438
|
-
state_history:
|
|
7439
|
-
build_handoff: task.buildHandoff ?
|
|
7580
|
+
state_history: this.sql.json(task.stateHistory ?? []),
|
|
7581
|
+
build_handoff: task.buildHandoff ? this.sql.json(task.buildHandoff) : null,
|
|
7440
7582
|
build_report: task.buildReport ?? null,
|
|
7441
7583
|
task_type: task.taskType ?? null,
|
|
7442
7584
|
maturity: task.maturity ?? null,
|
|
@@ -7615,7 +7757,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
7615
7757
|
${projectId}, ${payload.cycle.number}, ${payload.cycle.status},
|
|
7616
7758
|
${payload.cycle.startDate}, ${payload.cycle.endDate ?? null},
|
|
7617
7759
|
${payload.cycle.goals}, ${payload.cycle.boardHealth}, ${sprintTaskIds},
|
|
7618
|
-
${payload.cycle.contextHashes ?
|
|
7760
|
+
${payload.cycle.contextHashes ? this.sql.json(payload.cycle.contextHashes) : null}
|
|
7619
7761
|
)
|
|
7620
7762
|
ON CONFLICT (project_id, number)
|
|
7621
7763
|
DO UPDATE SET
|
|
@@ -7645,10 +7787,57 @@ var proxy_adapter_exports = {};
|
|
|
7645
7787
|
__export(proxy_adapter_exports, {
|
|
7646
7788
|
ProxyPapiAdapter: () => ProxyPapiAdapter
|
|
7647
7789
|
});
|
|
7648
|
-
|
|
7790
|
+
function snakeToCamel(str) {
|
|
7791
|
+
return str.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
7792
|
+
}
|
|
7793
|
+
function transformKeys(obj) {
|
|
7794
|
+
if (obj === null || obj === void 0) return obj;
|
|
7795
|
+
if (Array.isArray(obj)) return obj.map(transformKeys);
|
|
7796
|
+
if (typeof obj === "object" && obj !== null) {
|
|
7797
|
+
const result = {};
|
|
7798
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
7799
|
+
const camelKey = snakeToCamel(key);
|
|
7800
|
+
result[camelKey] = JSONB_PASSTHROUGH_KEYS.has(camelKey) ? value : transformKeys(value);
|
|
7801
|
+
}
|
|
7802
|
+
return result;
|
|
7803
|
+
}
|
|
7804
|
+
return obj;
|
|
7805
|
+
}
|
|
7806
|
+
function fixDisplayIdEntity(obj) {
|
|
7807
|
+
if (obj.displayId !== void 0) {
|
|
7808
|
+
obj.uuid = obj.id;
|
|
7809
|
+
obj.id = obj.displayId;
|
|
7810
|
+
}
|
|
7811
|
+
return obj;
|
|
7812
|
+
}
|
|
7813
|
+
function fixDisplayIdEntities(data) {
|
|
7814
|
+
if (Array.isArray(data)) return data.map((item) => fixDisplayIdEntity(item));
|
|
7815
|
+
if (data && typeof data === "object") return fixDisplayIdEntity(data);
|
|
7816
|
+
return data;
|
|
7817
|
+
}
|
|
7818
|
+
var JSONB_PASSTHROUGH_KEYS, DISPLAY_ID_METHODS, ProxyPapiAdapter;
|
|
7649
7819
|
var init_proxy_adapter = __esm({
|
|
7650
7820
|
"src/proxy-adapter.ts"() {
|
|
7651
7821
|
"use strict";
|
|
7822
|
+
JSONB_PASSTHROUGH_KEYS = /* @__PURE__ */ new Set([
|
|
7823
|
+
"buildHandoff",
|
|
7824
|
+
"stateHistory",
|
|
7825
|
+
"handoffAccuracy",
|
|
7826
|
+
"briefImplications",
|
|
7827
|
+
"autoReview",
|
|
7828
|
+
"structuredData",
|
|
7829
|
+
"data"
|
|
7830
|
+
]);
|
|
7831
|
+
DISPLAY_ID_METHODS = /* @__PURE__ */ new Set([
|
|
7832
|
+
"queryBoard",
|
|
7833
|
+
"getTask",
|
|
7834
|
+
"getTasks",
|
|
7835
|
+
"createTask",
|
|
7836
|
+
"getRecentBuildReports",
|
|
7837
|
+
"getBuildReportsSince",
|
|
7838
|
+
"getRecentReviews",
|
|
7839
|
+
"getActiveDecisions"
|
|
7840
|
+
]);
|
|
7652
7841
|
ProxyPapiAdapter = class {
|
|
7653
7842
|
endpoint;
|
|
7654
7843
|
apiKey;
|
|
@@ -7661,6 +7850,7 @@ var init_proxy_adapter = __esm({
|
|
|
7661
7850
|
/**
|
|
7662
7851
|
* Send an adapter method call to the proxy Edge Function.
|
|
7663
7852
|
* Serializes { projectId, method, args } and deserializes the response.
|
|
7853
|
+
* Results are transformed from snake_case to camelCase to match pg adapter output.
|
|
7664
7854
|
*/
|
|
7665
7855
|
async invoke(method, args = []) {
|
|
7666
7856
|
const url = `${this.endpoint}/invoke`;
|
|
@@ -7691,7 +7881,11 @@ var init_proxy_adapter = __esm({
|
|
|
7691
7881
|
if (!body.ok && body.error) {
|
|
7692
7882
|
throw new Error(`Proxy error on ${method}: ${body.error}`);
|
|
7693
7883
|
}
|
|
7694
|
-
|
|
7884
|
+
let result = transformKeys(body.result);
|
|
7885
|
+
if (DISPLAY_ID_METHODS.has(method)) {
|
|
7886
|
+
result = fixDisplayIdEntities(result);
|
|
7887
|
+
}
|
|
7888
|
+
return result;
|
|
7695
7889
|
}
|
|
7696
7890
|
/** Check if the proxy is reachable. */
|
|
7697
7891
|
async probeConnection() {
|
|
@@ -7838,9 +8032,6 @@ var init_proxy_adapter = __esm({
|
|
|
7838
8032
|
getCostSummary(cycleNumber) {
|
|
7839
8033
|
return this.invoke("getCostSummary", [cycleNumber]);
|
|
7840
8034
|
}
|
|
7841
|
-
writeCostSnapshot(snapshot) {
|
|
7842
|
-
return this.invoke("writeCostSnapshot", [snapshot]);
|
|
7843
|
-
}
|
|
7844
8035
|
getCostSnapshots() {
|
|
7845
8036
|
return this.invoke("getCostSnapshots");
|
|
7846
8037
|
}
|
|
@@ -7979,6 +8170,7 @@ function loadConfig() {
|
|
|
7979
8170
|
const autoCommit2 = process.env.PAPI_AUTO_COMMIT !== "false";
|
|
7980
8171
|
const baseBranch = process.env.PAPI_BASE_BRANCH ?? "main";
|
|
7981
8172
|
const autoPR = process.env.PAPI_AUTO_PR !== "false";
|
|
8173
|
+
const lightMode = process.env.PAPI_LIGHT_MODE === "true";
|
|
7982
8174
|
const papiEndpoint = process.env.PAPI_ENDPOINT;
|
|
7983
8175
|
const dataEndpoint = process.env.PAPI_DATA_ENDPOINT;
|
|
7984
8176
|
const databaseUrl = process.env.DATABASE_URL;
|
|
@@ -7992,15 +8184,29 @@ function loadConfig() {
|
|
|
7992
8184
|
baseBranch,
|
|
7993
8185
|
autoPR,
|
|
7994
8186
|
adapterType,
|
|
7995
|
-
papiEndpoint
|
|
8187
|
+
papiEndpoint,
|
|
8188
|
+
lightMode
|
|
7996
8189
|
};
|
|
7997
8190
|
}
|
|
7998
8191
|
|
|
7999
8192
|
// src/adapter-factory.ts
|
|
8000
8193
|
init_dist2();
|
|
8001
8194
|
import path2 from "path";
|
|
8195
|
+
import { execSync } from "child_process";
|
|
8196
|
+
function detectUserId() {
|
|
8197
|
+
try {
|
|
8198
|
+
const email = execSync("git config user.email", { encoding: "utf8", timeout: 5e3 }).trim();
|
|
8199
|
+
if (email) return email;
|
|
8200
|
+
} catch {
|
|
8201
|
+
}
|
|
8202
|
+
try {
|
|
8203
|
+
const ghUser = execSync("gh api user --jq .email", { encoding: "utf8", timeout: 1e4 }).trim();
|
|
8204
|
+
if (ghUser && ghUser !== "null") return ghUser;
|
|
8205
|
+
} catch {
|
|
8206
|
+
}
|
|
8207
|
+
return void 0;
|
|
8208
|
+
}
|
|
8002
8209
|
var HOSTED_PROXY_ENDPOINT = "https://guewgygcpcmrcoppihzx.supabase.co/functions/v1/data-proxy";
|
|
8003
|
-
var HOSTED_PROXY_KEY = "e9891a0a2225ac376f88ebdad78b4814b52ce0a39a41c5ec";
|
|
8004
8210
|
var PLACEHOLDER_PATTERNS = [
|
|
8005
8211
|
"<YOUR_DATABASE_URL>",
|
|
8006
8212
|
"your-database-url",
|
|
@@ -8060,7 +8266,18 @@ async function createAdapter(optionsOrType, maybePapiDir) {
|
|
|
8060
8266
|
if (!existing) {
|
|
8061
8267
|
const projectRoot = options.projectRoot ?? process.env["PAPI_PROJECT_DIR"] ?? "";
|
|
8062
8268
|
const slug = path2.basename(projectRoot) || "unnamed";
|
|
8063
|
-
|
|
8269
|
+
let userId = process.env["PAPI_USER_ID"] ?? void 0;
|
|
8270
|
+
if (!userId) {
|
|
8271
|
+
userId = detectUserId();
|
|
8272
|
+
if (userId) {
|
|
8273
|
+
console.error(`[papi] Auto-detected user identity: ${userId}`);
|
|
8274
|
+
console.error("[papi] Set PAPI_USER_ID in .mcp.json to make this explicit.");
|
|
8275
|
+
} else {
|
|
8276
|
+
console.error("[papi] \u26A0 No PAPI_USER_ID set and auto-detection failed.");
|
|
8277
|
+
console.error("[papi] Project will have no user scope \u2014 it may be visible to all dashboard users.");
|
|
8278
|
+
console.error("[papi] Set PAPI_USER_ID in your .mcp.json env to fix this.");
|
|
8279
|
+
}
|
|
8280
|
+
}
|
|
8064
8281
|
await pgAdapter.createProject({ id: projectId, slug, name: slug, papi_dir: papiDir, user_id: userId });
|
|
8065
8282
|
}
|
|
8066
8283
|
await pgAdapter.close();
|
|
@@ -8091,7 +8308,17 @@ async function createAdapter(optionsOrType, maybePapiDir) {
|
|
|
8091
8308
|
);
|
|
8092
8309
|
}
|
|
8093
8310
|
const dataEndpoint = process.env["PAPI_DATA_ENDPOINT"] || HOSTED_PROXY_ENDPOINT;
|
|
8094
|
-
const dataApiKey = process.env["PAPI_DATA_API_KEY"]
|
|
8311
|
+
const dataApiKey = process.env["PAPI_DATA_API_KEY"];
|
|
8312
|
+
if (!dataApiKey) {
|
|
8313
|
+
throw new Error(
|
|
8314
|
+
`PAPI_DATA_API_KEY is required for proxy mode.
|
|
8315
|
+
To get your API key:
|
|
8316
|
+
1. Sign in at ${process.env["PAPI_DASHBOARD_URL"] || "https://papi-web-three.vercel.app"} with GitHub
|
|
8317
|
+
2. Your API key is shown on the onboarding page (save it \u2014 shown only once)
|
|
8318
|
+
3. Add PAPI_DATA_API_KEY to your .mcp.json env config
|
|
8319
|
+
If you already have a key, set it in your MCP configuration.`
|
|
8320
|
+
);
|
|
8321
|
+
}
|
|
8095
8322
|
const adapter2 = new ProxyPapiAdapter2({
|
|
8096
8323
|
endpoint: dataEndpoint,
|
|
8097
8324
|
apiKey: dataApiKey,
|
|
@@ -8446,6 +8673,61 @@ function formatReviews(reviews) {
|
|
|
8446
8673
|
- **Comments:** ${r.comments}`
|
|
8447
8674
|
).join("\n\n---\n\n");
|
|
8448
8675
|
}
|
|
8676
|
+
function formatTaskComments(comments, taskIds, heading = "## Task Comments") {
|
|
8677
|
+
const relevant = comments.filter((c) => taskIds.has(c.taskId));
|
|
8678
|
+
if (relevant.length === 0) return "";
|
|
8679
|
+
const byTask = /* @__PURE__ */ new Map();
|
|
8680
|
+
for (const c of relevant) {
|
|
8681
|
+
const list = byTask.get(c.taskId) ?? [];
|
|
8682
|
+
list.push(c);
|
|
8683
|
+
byTask.set(c.taskId, list);
|
|
8684
|
+
}
|
|
8685
|
+
const lines = ["", heading];
|
|
8686
|
+
for (const [taskId, taskComments] of byTask) {
|
|
8687
|
+
for (const c of taskComments.slice(0, 3)) {
|
|
8688
|
+
const date = c.createdAt.split("T")[0];
|
|
8689
|
+
const text = c.content.length > 200 ? c.content.slice(0, 200) + "..." : c.content;
|
|
8690
|
+
lines.push(`- **${taskId}** \u2014 ${c.author} (${date}): "${text}"`);
|
|
8691
|
+
}
|
|
8692
|
+
}
|
|
8693
|
+
return lines.join("\n");
|
|
8694
|
+
}
|
|
8695
|
+
function formatDiscoveryCanvas(canvas) {
|
|
8696
|
+
const sections = [];
|
|
8697
|
+
if (canvas.landscapeReferences && canvas.landscapeReferences.length > 0) {
|
|
8698
|
+
sections.push("**Landscape & References:**");
|
|
8699
|
+
for (const ref of canvas.landscapeReferences) {
|
|
8700
|
+
const url = ref.url ? ` (${ref.url})` : "";
|
|
8701
|
+
const notes = ref.notes ? ` \u2014 ${ref.notes}` : "";
|
|
8702
|
+
sections.push(`- ${ref.name}${url}${notes}`);
|
|
8703
|
+
}
|
|
8704
|
+
}
|
|
8705
|
+
if (canvas.userJourneys && canvas.userJourneys.length > 0) {
|
|
8706
|
+
sections.push("**User Journeys:**");
|
|
8707
|
+
for (const j of canvas.userJourneys) {
|
|
8708
|
+
const priority = j.priority ? ` [${j.priority}]` : "";
|
|
8709
|
+
sections.push(`- **${j.persona}:** ${j.journey}${priority}`);
|
|
8710
|
+
}
|
|
8711
|
+
}
|
|
8712
|
+
if (canvas.mvpBoundary) {
|
|
8713
|
+
sections.push("**MVP Boundary:**", canvas.mvpBoundary);
|
|
8714
|
+
}
|
|
8715
|
+
if (canvas.assumptionsOpenQuestions && canvas.assumptionsOpenQuestions.length > 0) {
|
|
8716
|
+
sections.push("**Assumptions & Open Questions:**");
|
|
8717
|
+
for (const a of canvas.assumptionsOpenQuestions) {
|
|
8718
|
+
const evidence = a.evidence ? ` Evidence: ${a.evidence}` : "";
|
|
8719
|
+
sections.push(`- [${a.status}] ${a.text}${evidence}`);
|
|
8720
|
+
}
|
|
8721
|
+
}
|
|
8722
|
+
if (canvas.successSignals && canvas.successSignals.length > 0) {
|
|
8723
|
+
sections.push("**Success Signals:**");
|
|
8724
|
+
for (const s of canvas.successSignals) {
|
|
8725
|
+
const metric = s.metric ? ` (${s.metric}` + (s.target ? `, target: ${s.target})` : ")") : "";
|
|
8726
|
+
sections.push(`- ${s.signal}${metric}`);
|
|
8727
|
+
}
|
|
8728
|
+
}
|
|
8729
|
+
return sections.length > 0 ? sections.join("\n") : void 0;
|
|
8730
|
+
}
|
|
8449
8731
|
|
|
8450
8732
|
// src/lib/git.ts
|
|
8451
8733
|
import { execFileSync } from "child_process";
|
|
@@ -8470,6 +8752,11 @@ function isGitRepo(cwd) {
|
|
|
8470
8752
|
}
|
|
8471
8753
|
}
|
|
8472
8754
|
function stageDirAndCommit(cwd, dir, message) {
|
|
8755
|
+
try {
|
|
8756
|
+
execFileSync("git", ["check-ignore", "-q", dir], { cwd });
|
|
8757
|
+
return { committed: false, message: `Skipped commit \u2014 '${dir}' is gitignored.` };
|
|
8758
|
+
} catch {
|
|
8759
|
+
}
|
|
8473
8760
|
execFileSync("git", ["add", dir], { cwd });
|
|
8474
8761
|
const staged = execFileSync("git", ["diff", "--cached", "--name-only"], {
|
|
8475
8762
|
cwd,
|
|
@@ -8779,6 +9066,16 @@ function getHeadCommitSha(cwd) {
|
|
|
8779
9066
|
return null;
|
|
8780
9067
|
}
|
|
8781
9068
|
}
|
|
9069
|
+
function runAutoCommit(projectRoot, commitFn) {
|
|
9070
|
+
if (!isGitAvailable()) return "Auto-commit: skipped (git not found).";
|
|
9071
|
+
if (!isGitRepo(projectRoot)) return "Auto-commit: skipped (not a git repository).";
|
|
9072
|
+
try {
|
|
9073
|
+
const result = commitFn();
|
|
9074
|
+
return result.committed ? `Auto-committed: ${result.message}` : `Auto-commit: ${result.message}`;
|
|
9075
|
+
} catch (err) {
|
|
9076
|
+
return `Auto-commit failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
9077
|
+
}
|
|
9078
|
+
}
|
|
8782
9079
|
function getFilesChangedFromBase(cwd, baseBranch) {
|
|
8783
9080
|
try {
|
|
8784
9081
|
const mergeBase = execFileSync("git", ["merge-base", baseBranch, "HEAD"], { cwd, encoding: "utf-8" }).trim();
|
|
@@ -8985,6 +9282,9 @@ SECURITY CONSIDERATIONS
|
|
|
8985
9282
|
REFERENCE DOCS
|
|
8986
9283
|
[Optional \u2014 paths to docs/ files that provide background context for this task. Include only when the task originated from research or scoping work and the doc contains context the builder will need beyond what is in this handoff. Omit this section entirely for tasks that don't need supplementary context.]
|
|
8987
9284
|
|
|
9285
|
+
PRE-BUILD VERIFICATION
|
|
9286
|
+
[List 2-5 specific file paths the builder should read BEFORE implementing to check if the functionality already exists. Derive these from FILES LIKELY TOUCHED \u2014 pick the files most likely to already contain the target functionality. If >80% of the scope is already implemented, the builder should report "already built" instead of re-implementing. Include this section for EVERY task \u2014 it prevents wasted build slots on already-shipped code.]
|
|
9287
|
+
|
|
8988
9288
|
FILES LIKELY TOUCHED
|
|
8989
9289
|
[files]
|
|
8990
9290
|
|
|
@@ -9016,6 +9316,27 @@ After your natural language output, include this EXACT format on its own line:
|
|
|
9016
9316
|
|
|
9017
9317
|
The JSON must be valid. Use null for optional fields that don't apply.
|
|
9018
9318
|
|
|
9319
|
+
## GUIDING PRINCIPLES
|
|
9320
|
+
|
|
9321
|
+
These principles come from 150+ cycles of dogfooding. They shape how the planner should think about planning:
|
|
9322
|
+
|
|
9323
|
+
- **Validate before advancing.** Don't push forward when things aren't proven.
|
|
9324
|
+
- **Every artifact needs a consumer.** If not consumed by the next cycle, it's waste.
|
|
9325
|
+
- **Upstream learning.** Every build informs the next plan.
|
|
9326
|
+
- **Commands surfaced, not memorized.** Always show what's next.
|
|
9327
|
+
- **Tough advisor, not cheerleader.** Push back on scope creep and bad ideas.
|
|
9328
|
+
- **BUILD HANDOFFs are the differentiator.** A third LLM executed tasks from handoffs alone.
|
|
9329
|
+
- **The methodology works.** Plan/build/review cycle produces compounding velocity.
|
|
9330
|
+
|
|
9331
|
+
## DETECT STRATEGIC DECISIONS
|
|
9332
|
+
|
|
9333
|
+
Watch for direction changes, architecture shifts, deprioritisation with reasoning, new principles, or competitive positioning decisions in the project context.
|
|
9334
|
+
|
|
9335
|
+
When detected:
|
|
9336
|
+
1. Flag it in the cycle log: "Strategic direction change detected \u2014 [description]."
|
|
9337
|
+
2. If confirmed by evidence (build reports, AD changes, carry-forward), propose an AD update or new AD in the structured output.
|
|
9338
|
+
3. If mid-cycle context suggests a pivot, note it for the next strategy review rather than over-reacting in the plan.
|
|
9339
|
+
|
|
9019
9340
|
## PERSISTENCE RULES \u2014 READ THIS CAREFULLY
|
|
9020
9341
|
|
|
9021
9342
|
Everything in Part 1 (natural language) is **display-only**. Part 2 (structured JSON) is what gets written to files.
|
|
@@ -9079,12 +9400,18 @@ Standard planning cycle with full board review.
|
|
|
9079
9400
|
1. **Cycle Health Check** \u2014 Flag issues: >7 day gaps, unprocessed discovered issues, AD conflicts, stale In Progress tasks (3+ cycles).
|
|
9080
9401
|
**\u26A0\uFE0F CARRY-FORWARD STALENESS:** Check the latest carry-forward text for items containing "stale", "already exists", "already implemented", or "already built". For each such item that references a specific task ID, check whether the task is still in Backlog. If a carry-forward says a task's deliverables already exist but the task is still Backlog, emit a \`boardCorrections\` entry setting it to Done with \`closureReason: "Auto-closed \u2014 carry-forward indicates deliverables already exist"\`. Log in the cycle log: "Auto-closed task-XXX \u2014 carry-forward confirmed deliverables exist." This prevents scheduling already-shipped tasks.
|
|
9081
9402
|
|
|
9082
|
-
2. **Inbox Triage** \u2014 Find unreviewed tasks (reviewed = false). For each: clean title, fill all fields, check for duplicates, verify alignment with Active Decisions. You
|
|
9403
|
+
2. **Inbox Triage** \u2014 Find unreviewed tasks (reviewed = false). For each: clean title, fill all fields, check for duplicates, verify alignment with Active Decisions. You MUST set priority on unreviewed tasks during triage using these criteria:
|
|
9404
|
+
- **P0 Critical** \u2014 Broken, blocking, or data-loss risk. Fix now.
|
|
9405
|
+
- **P1 High** \u2014 Strategically aligned: directly advances the current horizon/phase goals or Active Decisions.
|
|
9406
|
+
- **P2 Medium** \u2014 Valuable but not strategically urgent: quality improvements, efficiency, polish, infrastructure.
|
|
9407
|
+
- **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
|
|
9408
|
+
Also set complexity using the full range \u2014 **XS, Small, Medium, Large, XL** \u2014 based on actual scope, not conservatively. XS = single-line or config change. Small = one file, < 50 lines. Medium = 2-5 files. Large = cross-module, multiple components. XL = architectural, multi-day.
|
|
9409
|
+
If a task is clearly obsolete, duplicated, or rejected, set its status to "Cancelled" with a \`closureReason\` explaining why.
|
|
9083
9410
|
**\u2192 PERSIST:** For each task you set reviewed: true, corrected fields on, or marked "Cancelled", include it in \`boardCorrections\` in Part 2.
|
|
9084
9411
|
|
|
9085
9412
|
3. **Board Integrity** \u2014 All tasks have complete fields? Priority still accurate? Duplicates? Stale In Progress tasks?
|
|
9086
9413
|
**\u2192 PERSIST:** Include any field corrections (status updates, field fixes) in \`boardCorrections\` in Part 2.
|
|
9087
|
-
**\u26A0\uFE0F PRIORITY LOCK RULE:** Do NOT change the priority of any task that has \`reviewed: true\`. Reviewed tasks have had their priority confirmed by a human. If you believe a reviewed task's priority should change, note your recommendation in the cycle log but do NOT include a priority change in \`boardCorrections\`. You may only set priority on unreviewed tasks (during triage) or on newly created tasks (\`newTasks\` array).
|
|
9414
|
+
**\u26A0\uFE0F PRIORITY LOCK RULE:** Do NOT change the priority of any task that has \`reviewed: true\`. Reviewed tasks have had their priority confirmed by a human. If you believe a reviewed task's priority should change, note your recommendation in the cycle log but do NOT include a priority change in \`boardCorrections\`. You may only set priority on unreviewed tasks (during triage) or on newly created tasks (\`newTasks\` array). Priority values: P0 Critical, P1 High, P2 Medium, P3 Low.
|
|
9088
9415
|
|
|
9089
9416
|
4. **Security Posture Check** \u2014 Review recently completed tasks and current board state for security concerns. Only flag genuine issues \u2014 do not add boilerplate security notes every cycle. Look for:
|
|
9090
9417
|
- Data exposure risks introduced by recent builds (PII in logs, secrets in storage/config)
|
|
@@ -9099,34 +9426,41 @@ Standard planning cycle with full board review.
|
|
|
9099
9426
|
- **Cycle number as signal:** A Cycle 3 project should not be scheduling OAuth, billing, or analytics tasks. Early cycles focus on core functionality and proving the concept works.
|
|
9100
9427
|
- **Phase prerequisites:** If the board has phases, tasks from later phases should only be scheduled when earlier phases have completed tasks (check Done count per phase). A task in "Phase 4: Monetisation" is premature if Phase 2 tasks are still in Backlog.
|
|
9101
9428
|
- **Dependency chain:** If a task's \`dependsOn\` references incomplete tasks, it cannot be scheduled regardless of priority.
|
|
9102
|
-
- **Task maturity:** Tasks with \`maturity: "raw"\` are unscoped ideas
|
|
9103
|
-
- **What to do with premature tasks:** Leave them in Backlog. Do NOT generate BUILD HANDOFFs for them. If a high-priority task fails the maturity gate, note it in the cycle log: "task-XXX deferred \u2014 Phase N prerequisites not met"
|
|
9429
|
+
- **Task maturity:** Tasks with \`maturity: "raw"\` are unscoped ideas from the idea tool. The planner IS the scoping mechanism \u2014 scope them as part of planning. For raw tasks selected for a cycle: (a) derive clear scope, acceptance criteria, and effort from the title, notes, and project context, (b) upgrade them to \`maturity: "investigated"\` via a \`boardCorrections\` entry, and (c) generate a BUILD HANDOFF as normal. For research-type raw tasks, scope the handoff as an investigation task \u2014 the deliverable is findings + follow-up backlog tasks, not code. Only leave a raw task unscheduled if you genuinely cannot derive scope from the available context \u2014 note why in the cycle log. Tasks with \`maturity: "ready"\` or no maturity field are considered cycle-ready. Tasks with \`maturity: "investigated"\` have been scoped but may still need refinement \u2014 schedule them if priority warrants it.
|
|
9430
|
+
- **What to do with premature tasks:** Leave them in Backlog. Do NOT generate BUILD HANDOFFs for them. If a high-priority task fails the maturity gate due to phase prerequisites or dependencies, note it in the cycle log: "task-XXX deferred \u2014 Phase N prerequisites not met". Raw tasks are NOT premature \u2014 they just need scoping (see Task maturity above).
|
|
9104
9431
|
|
|
9105
9432
|
7. **Recommendation** \u2014 Pick ONE task to recommend:
|
|
9106
|
-
**If USER DIRECTION is provided above:** Follow the user's stated focus. Pick the highest-impact task that aligns with their direction. The user knows what they need
|
|
9107
|
-
**Otherwise,
|
|
9108
|
-
-
|
|
9109
|
-
-
|
|
9110
|
-
-
|
|
9111
|
-
-
|
|
9112
|
-
-
|
|
9113
|
-
|
|
9114
|
-
|
|
9115
|
-
**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 from the tasks themselves. The historical average effort from Methodology Trends is a reference point for calibration, not a target or floor. A healthy cycle has 3-5 tasks. A 1-task cycle is almost never correct \u2014 if only 1 task qualifies, check if lower-priority tasks could also ship.
|
|
9433
|
+
**If USER DIRECTION is provided above:** Follow the user's stated focus. Pick the highest-impact task that aligns with their direction. The user knows what they need. Only deviate if a genuine P0 Critical fix exists (broken builds, data loss).
|
|
9434
|
+
**Otherwise, select by priority level then impact:**
|
|
9435
|
+
- **P0 Critical** \u2014 Broken, blocking, or data-loss risk. Always first.
|
|
9436
|
+
- **P1 High** \u2014 Strategically aligned: directly advances the current horizon, phase, or Active Decision goals.
|
|
9437
|
+
- **P2 Medium** \u2014 Valuable but not strategically urgent: quality improvements, efficiency, polish, infra.
|
|
9438
|
+
- **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
|
|
9439
|
+
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.
|
|
9440
|
+
**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.
|
|
9441
|
+
**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 from the tasks themselves. The historical average effort from Methodology Trends is a reference point for calibration, not a target or floor. A healthy cycle has 4-6 tasks. Cycles with fewer than 4 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 5+ tasks \u2014 undersized cycles waste planning overhead relative to the available work. If fewer than 4 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.
|
|
9116
9442
|
|
|
9117
9443
|
8. **Cycle Log** \u2014 Write 5-10 line entry: what was triaged, what was recommended and why, observations, AD updates.
|
|
9118
9444
|
**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.
|
|
9119
9445
|
|
|
9120
9446
|
9. **Active Decisions** \u2014 If any AD needs updating: Type A (confidence change), Type B (modification), or Type C (reversal/supersede).
|
|
9447
|
+
**AD Quality Bar:** ADs are for product and architecture choices that constrain future work \u2014 technology selections, data model designs, UX principles, strategic positioning. They are NOT for: process preferences (commit style, PR size), configuration choices (linter rules, tab width), or temporary workarounds. If a decision doesn't affect what gets built or how it's architected, it's not an AD. Apply this bar when proposing new ADs and when triaging existing ones.
|
|
9121
9448
|
**\u2192 PERSIST:** EVERY AD you created, updated, or confirmed with changes MUST appear in \`activeDecisions\` array in Part 2. Include the full replacement body with ### heading.
|
|
9122
9449
|
|
|
9450
|
+
### Operational Quality Rules
|
|
9451
|
+
- **Idea similarity pause:** When the idea tool finds similar tasks during planning, stop and explain the overlap \u2014 do not silently ignore the similarity warning. Duplicates bloat the board and waste build slots.
|
|
9452
|
+
- **Backlog as steering wheel:** Task priority and notes in the backlog are the user's primary control mechanism over what gets planned. Respect the priority rankings and read task notes carefully \u2014 they contain user intent that shapes scope and scheduling.
|
|
9453
|
+
- **Planning quality is the bar:** Strategy review depth and plan quality set the standard for the product. Do not cut corners on analysis depth, triage thoroughness, or handoff specificity \u2014 these are what users experience as PAPI's value.
|
|
9454
|
+
|
|
9123
9455
|
10. **BUILD HANDOFFs** \u2014 Generate a full BUILD HANDOFF block for the recommended task and up to 4 additional high-priority unblocked tasks (5 total max). Include each handoff in the \`cycleHandoffs\` array in the structured output. The handoffs are written to each task on the board for durability. Remaining tasks will get handoffs in subsequent plans \u2014 do NOT try to cover the entire backlog.
|
|
9124
9456
|
**SKIP existing handoffs:** Tasks marked with "Has BUILD HANDOFF: yes" or "\u2713 handoff" on the board already have a valid handoff from a previous plan. Do NOT regenerate handoffs for these tasks \u2014 omit them from the \`cycleHandoffs\` array entirely. Only generate handoffs for tasks that do NOT have one yet. Exception: if a task's dependencies have been completed since its handoff was written, or a relevant Active Decision has changed, you MAY regenerate its handoff \u2014 but note this explicitly in the cycle log.
|
|
9457
|
+
**Scope pre-check:** Before writing the SCOPE section of each handoff, check whether the described functionality already exists based on the task's context, recent build reports, and the FILES LIKELY TOUCHED. If the infrastructure likely exists (e.g. a status type, a DB constraint, an API route), reduce the scope to only the missing pieces and 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.
|
|
9125
9458
|
**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.
|
|
9126
|
-
**Maturity gate applies here:** Do NOT generate BUILD HANDOFFs for tasks that failed the maturity gate in step 6
|
|
9459
|
+
**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.
|
|
9127
9460
|
**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".
|
|
9128
|
-
**Estimation calibration:**
|
|
9461
|
+
**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).
|
|
9129
9462
|
**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.
|
|
9463
|
+
**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.
|
|
9130
9464
|
**UI/visual task detection:** When a task's title or notes contain keywords suggesting frontend visual work (e.g. "visual", "design", "UI", "styling", "refresh", "frontend", "landing page", "hero", "carousel", "theme", "layout"), apply these handoff additions:
|
|
9131
9465
|
- Add to SCOPE: "Use the \`frontend-design\` skill for implementation \u2014 it produces higher-quality visual output than manual styling."
|
|
9132
9466
|
- Add to ACCEPTANCE CRITERIA: "[ ] Visually verify rendered output in browser before reporting done \u2014 provide localhost URL or screenshot to the user for review."
|
|
@@ -9147,7 +9481,7 @@ Standard planning cycle with full board review.
|
|
|
9147
9481
|
- The North Star changed or was validated in a way that the brief doesn't reflect
|
|
9148
9482
|
- A phase completed that shifts what the product IS (not just what was built)
|
|
9149
9483
|
- The brief describes capabilities, architecture, or direction that are no longer accurate
|
|
9150
|
-
- **
|
|
9484
|
+
- **DRIFT CHECK:** Compare the brief's content against current reality. The brief is drifted if: (a) it describes capabilities that don't exist or have been removed, (b) it references user types, architecture, or positioning that ADs have since changed, (c) the current phase/stage has shifted from what the brief describes, or (d) key metrics or success criteria no longer match the project's direction. Cycle count since last update is a secondary signal only \u2014 a brief updated 15 cycles ago that still accurately describes the product is NOT stale. A brief updated 3 cycles ago that contradicts a recent AD IS drifted.
|
|
9151
9485
|
If any of these apply, include an updated \`productBrief\` in the structured output. Include the FULL updated brief (not a diff). Preserve all existing sections and user-added content; update facts, numbers, and status to reflect current reality. Do not regenerate the brief every cycle \u2014 but do not let it go stale either.
|
|
9152
9486
|
|
|
9153
9487
|
13. **Forward Horizon** \u2014 If a Forward Horizon section is provided in the context below, write a "## Forward Horizon" section in Part 1. Surface 2-3 decisions the team should make before the next phase starts. Each item must be:
|
|
@@ -9168,7 +9502,7 @@ function buildPlanUserMessage(ctx) {
|
|
|
9168
9502
|
parts.push(
|
|
9169
9503
|
`## USER DIRECTION`,
|
|
9170
9504
|
"",
|
|
9171
|
-
`The user has provided the following direction for this cycle. This OVERRIDES the autonomous priority
|
|
9505
|
+
`The user has provided the following direction for this cycle. This OVERRIDES the autonomous priority-based selection in Step 7. Prioritise tasks that align with this direction, even if lower-priority tasks exist on the board.`,
|
|
9172
9506
|
"",
|
|
9173
9507
|
`> ${ctx.focus}`,
|
|
9174
9508
|
""
|
|
@@ -9325,9 +9659,18 @@ IMPORTANT: You are running as a non-interactive API call. Do NOT ask the user qu
|
|
|
9325
9659
|
|
|
9326
9660
|
## OUTPUT PRINCIPLES
|
|
9327
9661
|
|
|
9328
|
-
- **
|
|
9662
|
+
- **Full depth, not thin summaries.** Each mandatory section must be substantive \u2014 multiple paragraphs with specific evidence, not compressed bullet points. The reader should understand cross-cycle patterns, not just individual cycle events. If a section would be under 3 sentences, you haven't gone deep enough.
|
|
9663
|
+
- **Lead with insight, not data recitation.** Open each section with the strategic takeaway or pattern, THEN support it with cycle data and task references. Bad: "C131 built task-700, C132 built task-710." Good: "The last 5 cycles show a clear shift from infrastructure to user-facing work \u2014 80% of tasks were dashboard or onboarding, up from 30% in the prior review window."
|
|
9329
9664
|
- **Cycle data first, conversation context second.** Base your review on build reports, cycle logs, board state, and ADs \u2014 not on whatever was discussed earlier in the conversation. If recent conversation context conflicts with the data, flag it but trust the data.
|
|
9330
|
-
- **Every section earns its place.** If a section has nothing meaningful to say, skip it entirely. Do not write "No issues found" or "No concerns" \u2014 just omit the section.
|
|
9665
|
+
- **Every conditional section earns its place.** If a conditional section has nothing meaningful to say, skip it entirely. Do not write "No issues found" or "No concerns" \u2014 just omit the section. But the 6 mandatory sections MUST appear with full depth regardless.
|
|
9666
|
+
|
|
9667
|
+
## TWO-PHASE DELIVERY
|
|
9668
|
+
|
|
9669
|
+
This review is delivered in two phases:
|
|
9670
|
+
1. **Phase 1 (this output):** Present the full review \u2014 all 6 mandatory sections with complete analysis, plus any relevant conditional sections. Do NOT compress, summarise, or abbreviate. The user needs to read and discuss the full review before actions are taken.
|
|
9671
|
+
2. **Phase 2 (after user discussion):** The structured action breakdown in Part 2 captures concrete next steps. But the user may refine, reject, or add to these after reading Phase 1. The structured output represents your best autonomous assessment \u2014 the user's feedback in conversation refines it.
|
|
9672
|
+
|
|
9673
|
+
Present the full review first. Let the analysis breathe. The user will discuss, push back, and refine before acting on the structured output.
|
|
9331
9674
|
|
|
9332
9675
|
## YOUR JOB \u2014 STRUCTURED COVERAGE
|
|
9333
9676
|
|
|
@@ -9345,7 +9688,7 @@ You MUST cover these 6 sections. Each is mandatory unless explicitly noted as co
|
|
|
9345
9688
|
- Is the product brief still an accurate description of what this product IS and WHERE it's going? If ADs have been created or superseded since the brief was last updated, the brief may be wrong.
|
|
9346
9689
|
- Has the target user changed? Has the scope expanded or contracted in ways the brief doesn't capture?
|
|
9347
9690
|
- Are we building for the right problem? Has evidence emerged (from builds, feedback, or market) that the core problem statement needs revision?
|
|
9348
|
-
-
|
|
9691
|
+
- Assess North Star drift: Does the North Star's key metric and success definition still align with the current phase, active ADs, and recent build directions? A North Star is drifted when: the metric it tracks is no longer the team's focus, the success criteria reference capabilities that have been deprioritised, or ADs have shifted the product direction away from what the North Star describes. Cycle count since last update is a secondary signal \u2014 a stable, accurate North Star is not stale regardless of age.
|
|
9349
9692
|
If this analysis reveals the brief needs updating, you MUST include updated content in \`productBriefUpdates\` in Part 2. Don't just note "the brief is stale" \u2014 write the update.
|
|
9350
9693
|
|
|
9351
9694
|
6. **Active Decision Review + Scoring** \u2014 For each non-superseded AD: is the confidence level still correct? Has evidence emerged that changes anything? Score on 5 dimensions (1-5, lower = better):
|
|
@@ -9355,6 +9698,7 @@ You MUST cover these 6 sections. Each is mandatory unless explicitly noted as co
|
|
|
9355
9698
|
- **scale_cost** \u2014 What this costs at 10x/100x users or data (1=negligible, 5=bottleneck)
|
|
9356
9699
|
- **lock_in** \u2014 Dependency on a specific vendor/tool (1=swappable, 5=deeply coupled)
|
|
9357
9700
|
Only score ADs where you have enough context to evaluate meaningfully \u2014 skip ADs where scoring would be guesswork.
|
|
9701
|
+
**AD Quality Bar:** ADs are for product and architecture choices that constrain future work \u2014 technology selections, data model designs, UX principles, strategic positioning. They are NOT for: process preferences (commit style, PR size), configuration choices (linter rules, tab width), or temporary workarounds. If a decision doesn't affect what gets built or how it's architected, it's not an AD. Flag any existing ADs that fail this bar for deletion via \`activeDecisionUpdates\` with action \`delete\`.
|
|
9358
9702
|
**IMPORTANT:** If your analysis recommends changing an AD's confidence, modifying its body, or creating a new AD, you MUST include it in \`activeDecisionUpdates\` in Part 2. Analysis without persistence is waste \u2014 the next plan won't see your recommendation unless it's in the structured output.
|
|
9359
9703
|
|
|
9360
9704
|
## CONDITIONAL SECTIONS (include only when relevant)
|
|
@@ -9374,6 +9718,7 @@ ${compressionJob}
|
|
|
9374
9718
|
- **Config drift** \u2014 Environment variables referenced in code but not documented, stale .env.example entries, MCP config mismatches between what the server expects and what setup/init generates.
|
|
9375
9719
|
- **Dead dependencies** \u2014 Packages in package.json that are no longer imported anywhere. These add install time and attack surface.
|
|
9376
9720
|
- **Stale prompts or instructions** \u2014 Cycle numbers, AD references, or project-state assumptions in prompts.ts or CLAUDE.md that no longer match reality.
|
|
9721
|
+
- **Stage readiness gaps** \u2014 If the project is approaching or entering an access-widening stage (e.g. Alpha Distribution, Alpha Cohort, Public Launch), check that auth/security phases are complete. Stages that widen who can access the product must have auth hardening and security review as prerequisites \u2014 not post-hoc discoveries.
|
|
9377
9722
|
Report findings in a brief "Architecture Health" section in Part 1. If no issues found, skip the section entirely \u2014 do not write "No issues found".
|
|
9378
9723
|
|
|
9379
9724
|
10. **Discovery Canvas Audit** \u2014 If a Discovery Canvas section is provided in context, audit it for completeness and staleness. For each of the 5 canvas sections (Landscape & References, User Journeys, MVP Boundary, Assumptions & Open Questions, Success Signals):
|
|
@@ -9397,11 +9742,11 @@ ${compressionJob}
|
|
|
9397
9742
|
- If no phase data is provided, skip this section.
|
|
9398
9743
|
Report findings in a "Hierarchy Assessment" section in Part 1. Persist findings in the \`stalePhases\` array in Part 2 (include stage/horizon observations too). If no issues found, omit the section and use an empty array.
|
|
9399
9744
|
|
|
9400
|
-
12. **Structural
|
|
9401
|
-
-
|
|
9402
|
-
- Carry-forward items that have persisted across **3+ cycles** without resolution \u2192 flag as stuck.
|
|
9403
|
-
- ADs with LOW confidence that have
|
|
9404
|
-
|
|
9745
|
+
12. **Structural Drift Detection** \u2014 If decision usage data is provided in context, identify structural decay using drift-based criteria (not pure cycle counts):
|
|
9746
|
+
- **AD drift:** An AD is drifted when its content contradicts recent build evidence, references architecture/capabilities that no longer exist, or has been made redundant by newer ADs. Reference frequency is a secondary signal \u2014 an unreferenced AD that is still accurate is not necessarily stale; an AD referenced last cycle that contradicts shipped code IS drifted.
|
|
9747
|
+
- **Carry-forward drift:** Carry-forward items that have persisted across **3+ cycles** without resolution \u2192 flag as stuck.
|
|
9748
|
+
- **Confidence drift:** ADs with LOW confidence that have not gained supporting evidence within 5 cycles \u2192 flag as unvalidated. ADs where build reports contradict the decision \u2192 flag as confidence should decrease.
|
|
9749
|
+
Use decision usage data as a secondary signal (unreferenced ADs are more likely to be drifted, but verify by checking content alignment). Report findings in a "Structural Drift" section in Part 1. Persist findings in the \`staleDecisions\` array in Part 2. If no issues found, omit the section and use an empty array.
|
|
9405
9750
|
|
|
9406
9751
|
## OUTPUT FORMAT
|
|
9407
9752
|
|
|
@@ -9422,7 +9767,7 @@ Then include conditional sections only if relevant:
|
|
|
9422
9767
|
- **Architecture Health** \u2014 only if issues found
|
|
9423
9768
|
- **Discovery Canvas Audit** \u2014 only if gaps or staleness found
|
|
9424
9769
|
- **Hierarchy Assessment** \u2014 only if hierarchy staleness, phase closure, or stage progression signals detected
|
|
9425
|
-
- **Structural
|
|
9770
|
+
- **Structural Drift** \u2014 only if drifted ADs or stuck carry-forwards found${compressionPart1}
|
|
9426
9771
|
|
|
9427
9772
|
### Part 2: Structured Data Block
|
|
9428
9773
|
After your natural language output, include this EXACT format on its own line:
|
|
@@ -9584,6 +9929,21 @@ function buildReviewUserMessage(ctx) {
|
|
|
9584
9929
|
if (ctx.recommendationEffectiveness) {
|
|
9585
9930
|
parts.push("### Recommendation Follow-Through", "", ctx.recommendationEffectiveness, "");
|
|
9586
9931
|
}
|
|
9932
|
+
if (ctx.adHocCommits) {
|
|
9933
|
+
parts.push("### Ad-hoc Work (Non-Task Commits)", "", ctx.adHocCommits, "");
|
|
9934
|
+
}
|
|
9935
|
+
if (ctx.pendingRecommendations) {
|
|
9936
|
+
parts.push("### Pending Strategy Recommendations", "", ctx.pendingRecommendations, "");
|
|
9937
|
+
}
|
|
9938
|
+
if (ctx.registeredDocs) {
|
|
9939
|
+
parts.push("### Registered Documents", "", ctx.registeredDocs, "");
|
|
9940
|
+
}
|
|
9941
|
+
if (ctx.recentPlans) {
|
|
9942
|
+
parts.push("### Recent Plans (since last review)", "", ctx.recentPlans, "");
|
|
9943
|
+
}
|
|
9944
|
+
if (ctx.unregisteredDocs) {
|
|
9945
|
+
parts.push("### Unregistered Docs", "", ctx.unregisteredDocs, "");
|
|
9946
|
+
}
|
|
9587
9947
|
return parts.join("\n");
|
|
9588
9948
|
}
|
|
9589
9949
|
function parseReviewStructuredOutput(raw) {
|
|
@@ -9848,6 +10208,7 @@ The body format for each AD:
|
|
|
9848
10208
|
- Each AD should be actionable and falsifiable (something the team could decide differently)
|
|
9849
10209
|
- Cover different concerns: architecture, data, deployment, testing strategy, API design, etc.
|
|
9850
10210
|
- Keep each AD body to 4-6 lines \u2014 concise and scannable
|
|
10211
|
+
- **Quality bar:** ADs are for product and architecture choices that constrain future work \u2014 technology selections, data model designs, UX principles, strategic positioning. They are NOT for process preferences, configuration choices, or temporary workarounds.
|
|
9851
10212
|
|
|
9852
10213
|
Return ONLY valid JSON \u2014 no preamble, no code fences, no explanation.`;
|
|
9853
10214
|
function buildAdSeedPrompt(ctx) {
|
|
@@ -9925,8 +10286,8 @@ IMPORTANT: You are running as a non-interactive API call. Do NOT ask questions.
|
|
|
9925
10286
|
|
|
9926
10287
|
Return a JSON array of 3-10 tasks. Each task must have:
|
|
9927
10288
|
- "title": Clear, actionable task title (start with a verb)
|
|
9928
|
-
- "priority": "P1 High", "P2 Medium", or "P3 Low"
|
|
9929
|
-
- "complexity": "XS", "Small", "Medium", or "
|
|
10289
|
+
- "priority": "P0 Critical", "P1 High", "P2 Medium", or "P3 Low"
|
|
10290
|
+
- "complexity": "XS", "Small", "Medium", "Large", or "XL"
|
|
9930
10291
|
- "module": A module name inferred from the codebase (e.g. "Core", "API", "Frontend", "Infra", "Tests")
|
|
9931
10292
|
- "phase": A phase name (e.g. "Phase 1", "Phase 2")
|
|
9932
10293
|
- "notes": 1-2 sentences of context about why this task matters
|
|
@@ -9936,8 +10297,8 @@ Return a JSON array of 3-10 tasks. Each task must have:
|
|
|
9936
10297
|
- Focus on gaps and improvements visible from the codebase structure \u2014 not features the user hasn't asked for
|
|
9937
10298
|
- Common gap categories: missing tests, missing documentation, config improvements, dependency updates, code quality, security hardening
|
|
9938
10299
|
- Do NOT suggest adding PAPI itself or PAPI-specific tasks \u2014 those are handled by the setup flow
|
|
9939
|
-
- Prioritise tasks that reduce risk or unblock future work (P1) over nice-to-haves (P3)
|
|
9940
|
-
-
|
|
10300
|
+
- Prioritise tasks that reduce risk or unblock future work (P0/P1) over nice-to-haves (P3)
|
|
10301
|
+
- Use the full complexity range: XS (config/one-liner), Small (one file), Medium (2-5 files), Large (cross-module), XL (architectural)
|
|
9941
10302
|
- Tasks should be specific enough to execute without further investigation
|
|
9942
10303
|
- Maximum 10 tasks \u2014 fewer is better if the codebase is well-maintained`;
|
|
9943
10304
|
function buildInitialTasksPrompt(inputs) {
|
|
@@ -10051,27 +10412,58 @@ function pushAfterCommit(config2) {
|
|
|
10051
10412
|
return push.success ? `> ${push.message}` : `> Warning: ${push.message}`;
|
|
10052
10413
|
}
|
|
10053
10414
|
function autoCommitPapi(config2, cycleNumber, mode) {
|
|
10054
|
-
|
|
10055
|
-
|
|
10056
|
-
|
|
10057
|
-
|
|
10058
|
-
|
|
10415
|
+
const modeLabel = mode === "bootstrap" ? "Bootstrap" : "Full";
|
|
10416
|
+
return runAutoCommit(
|
|
10417
|
+
config2.projectRoot,
|
|
10418
|
+
() => stageDirAndCommit(config2.projectRoot, ".papi", `papi: Cycle ${cycleNumber} plan \u2014 ${modeLabel}`)
|
|
10419
|
+
);
|
|
10420
|
+
}
|
|
10421
|
+
async function assembleTaskComments(adapter2) {
|
|
10422
|
+
try {
|
|
10423
|
+
const comments = await adapter2.getRecentTaskComments?.(20);
|
|
10424
|
+
if (comments && comments.length > 0) {
|
|
10425
|
+
const byTask = /* @__PURE__ */ new Map();
|
|
10426
|
+
for (const c of comments) {
|
|
10427
|
+
const list = byTask.get(c.taskId) ?? [];
|
|
10428
|
+
list.push(c);
|
|
10429
|
+
byTask.set(c.taskId, list);
|
|
10430
|
+
}
|
|
10431
|
+
const lines = [];
|
|
10432
|
+
for (const [taskId, taskComments] of byTask) {
|
|
10433
|
+
const limited = taskComments.slice(0, 5);
|
|
10434
|
+
for (const c of limited) {
|
|
10435
|
+
const truncated = c.content.length > 200 ? c.content.slice(0, 200) + "..." : c.content;
|
|
10436
|
+
const date = c.createdAt.split("T")[0];
|
|
10437
|
+
lines.push(`- **${taskId}** \u2014 ${c.author} (${date}): "${truncated}"`);
|
|
10438
|
+
}
|
|
10439
|
+
}
|
|
10440
|
+
return lines.join("\n");
|
|
10441
|
+
}
|
|
10442
|
+
} catch {
|
|
10059
10443
|
}
|
|
10444
|
+
return void 0;
|
|
10445
|
+
}
|
|
10446
|
+
async function assembleDiscoveryCanvasText(adapter2) {
|
|
10060
10447
|
try {
|
|
10061
|
-
const
|
|
10062
|
-
|
|
10063
|
-
|
|
10064
|
-
".papi",
|
|
10065
|
-
`papi: Cycle ${cycleNumber} plan \u2014 ${modeLabel}`
|
|
10066
|
-
);
|
|
10067
|
-
return result.committed ? `Auto-committed: ${result.message}` : `Auto-commit: ${result.message}`;
|
|
10068
|
-
} catch (err) {
|
|
10069
|
-
return `Auto-commit failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
10448
|
+
const canvas = await adapter2.readDiscoveryCanvas();
|
|
10449
|
+
return formatDiscoveryCanvas(canvas);
|
|
10450
|
+
} catch {
|
|
10070
10451
|
}
|
|
10452
|
+
return void 0;
|
|
10071
10453
|
}
|
|
10072
|
-
|
|
10073
|
-
|
|
10454
|
+
var REC_EXPIRY_CYCLES = 3;
|
|
10455
|
+
function formatStrategyRecommendations(recs, currentCycle) {
|
|
10456
|
+
const active = [];
|
|
10457
|
+
const expired = [];
|
|
10074
10458
|
for (const rec of recs) {
|
|
10459
|
+
if (currentCycle !== void 0 && rec.createdCycle && currentCycle - rec.createdCycle > REC_EXPIRY_CYCLES) {
|
|
10460
|
+
expired.push(rec);
|
|
10461
|
+
} else {
|
|
10462
|
+
active.push(rec);
|
|
10463
|
+
}
|
|
10464
|
+
}
|
|
10465
|
+
const byType = /* @__PURE__ */ new Map();
|
|
10466
|
+
for (const rec of active) {
|
|
10075
10467
|
const list = byType.get(rec.type) ?? [];
|
|
10076
10468
|
list.push(rec);
|
|
10077
10469
|
byType.set(rec.type, list);
|
|
@@ -10091,43 +10483,13 @@ function formatStrategyRecommendations(recs) {
|
|
|
10091
10483
|
sections.push(`- (Cycle ${item.createdCycle}) ${item.content}${targetSuffix}`);
|
|
10092
10484
|
}
|
|
10093
10485
|
}
|
|
10094
|
-
|
|
10095
|
-
}
|
|
10096
|
-
|
|
10097
|
-
|
|
10098
|
-
if (canvas.landscapeReferences && canvas.landscapeReferences.length > 0) {
|
|
10099
|
-
sections.push("**Landscape & References:**");
|
|
10100
|
-
for (const ref of canvas.landscapeReferences) {
|
|
10101
|
-
const url = ref.url ? ` (${ref.url})` : "";
|
|
10102
|
-
const notes = ref.notes ? ` \u2014 ${ref.notes}` : "";
|
|
10103
|
-
sections.push(`- ${ref.name}${url}${notes}`);
|
|
10104
|
-
}
|
|
10105
|
-
}
|
|
10106
|
-
if (canvas.userJourneys && canvas.userJourneys.length > 0) {
|
|
10107
|
-
sections.push("**User Journeys:**");
|
|
10108
|
-
for (const j of canvas.userJourneys) {
|
|
10109
|
-
const priority = j.priority ? ` [${j.priority}]` : "";
|
|
10110
|
-
sections.push(`- **${j.persona}:** ${j.journey}${priority}`);
|
|
10111
|
-
}
|
|
10112
|
-
}
|
|
10113
|
-
if (canvas.mvpBoundary) {
|
|
10114
|
-
sections.push("**MVP Boundary:**", canvas.mvpBoundary);
|
|
10115
|
-
}
|
|
10116
|
-
if (canvas.assumptionsOpenQuestions && canvas.assumptionsOpenQuestions.length > 0) {
|
|
10117
|
-
sections.push("**Assumptions & Open Questions:**");
|
|
10118
|
-
for (const a of canvas.assumptionsOpenQuestions) {
|
|
10119
|
-
const evidence = a.evidence ? ` Evidence: ${a.evidence}` : "";
|
|
10120
|
-
sections.push(`- [${a.status}] ${a.text}${evidence}`);
|
|
10121
|
-
}
|
|
10122
|
-
}
|
|
10123
|
-
if (canvas.successSignals && canvas.successSignals.length > 0) {
|
|
10124
|
-
sections.push("**Success Signals:**");
|
|
10125
|
-
for (const s of canvas.successSignals) {
|
|
10126
|
-
const metric = s.metric ? ` (${s.metric}` + (s.target ? `, target: ${s.target})` : ")") : "";
|
|
10127
|
-
sections.push(`- ${s.signal}${metric}`);
|
|
10486
|
+
if (expired.length > 0) {
|
|
10487
|
+
sections.push(`**Expired (${expired.length} recs skipped \u2014 older than ${REC_EXPIRY_CYCLES} cycles):**`);
|
|
10488
|
+
for (const item of expired) {
|
|
10489
|
+
sections.push(`- (Cycle ${item.createdCycle}) ${item.content.slice(0, 80)}...`);
|
|
10128
10490
|
}
|
|
10129
10491
|
}
|
|
10130
|
-
return sections.
|
|
10492
|
+
return sections.join("\n");
|
|
10131
10493
|
}
|
|
10132
10494
|
function formatEstimationCalibration(rows) {
|
|
10133
10495
|
const total = rows.reduce((sum, r) => sum + r.count, 0);
|
|
@@ -10311,7 +10673,7 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
|
|
|
10311
10673
|
try {
|
|
10312
10674
|
const pendingRecs = await adapter2.getPendingRecommendations();
|
|
10313
10675
|
if (pendingRecs.length > 0) {
|
|
10314
|
-
strategyRecommendationsText2 = formatStrategyRecommendations(pendingRecs);
|
|
10676
|
+
strategyRecommendationsText2 = formatStrategyRecommendations(pendingRecs, health.totalCycles);
|
|
10315
10677
|
}
|
|
10316
10678
|
} catch {
|
|
10317
10679
|
}
|
|
@@ -10328,36 +10690,9 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
|
|
|
10328
10690
|
]);
|
|
10329
10691
|
timings["total"] = totalTimer();
|
|
10330
10692
|
console.error(`[plan-perf] assembleContext (lean): ${JSON.stringify(timings)}ms`);
|
|
10331
|
-
|
|
10332
|
-
|
|
10333
|
-
|
|
10334
|
-
if (comments && comments.length > 0) {
|
|
10335
|
-
const byTask = /* @__PURE__ */ new Map();
|
|
10336
|
-
for (const c of comments) {
|
|
10337
|
-
const list = byTask.get(c.taskId) ?? [];
|
|
10338
|
-
list.push(c);
|
|
10339
|
-
byTask.set(c.taskId, list);
|
|
10340
|
-
}
|
|
10341
|
-
const lines = [];
|
|
10342
|
-
for (const [taskId, taskComments] of byTask) {
|
|
10343
|
-
const limited = taskComments.slice(0, 5);
|
|
10344
|
-
for (const c of limited) {
|
|
10345
|
-
const truncated = c.content.length > 200 ? c.content.slice(0, 200) + "..." : c.content;
|
|
10346
|
-
const date = c.createdAt.split("T")[0];
|
|
10347
|
-
lines.push(`- **${taskId}** \u2014 ${c.author} (${date}): "${truncated}"`);
|
|
10348
|
-
}
|
|
10349
|
-
}
|
|
10350
|
-
taskCommentsText = lines.join("\n");
|
|
10351
|
-
}
|
|
10352
|
-
} catch {
|
|
10353
|
-
}
|
|
10354
|
-
let discoveryCanvasText;
|
|
10355
|
-
try {
|
|
10356
|
-
const canvas = await adapter2.readDiscoveryCanvas();
|
|
10357
|
-
discoveryCanvasText = formatDiscoveryCanvas(canvas);
|
|
10358
|
-
} catch {
|
|
10359
|
-
}
|
|
10360
|
-
let estimationCalibrationText;
|
|
10693
|
+
const taskCommentsText = await assembleTaskComments(adapter2);
|
|
10694
|
+
const discoveryCanvasText = await assembleDiscoveryCanvasText(adapter2);
|
|
10695
|
+
let estimationCalibrationText;
|
|
10361
10696
|
try {
|
|
10362
10697
|
const calibrationRows = await adapter2.getEstimationCalibration?.();
|
|
10363
10698
|
if (calibrationRows && calibrationRows.length > 0) {
|
|
@@ -10442,35 +10777,8 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
|
|
|
10442
10777
|
]);
|
|
10443
10778
|
timings["total"] = totalTimer();
|
|
10444
10779
|
console.error(`[plan-perf] assembleContext (full): ${JSON.stringify(timings)}ms`);
|
|
10445
|
-
|
|
10446
|
-
|
|
10447
|
-
const canvas = await adapter2.readDiscoveryCanvas();
|
|
10448
|
-
discoveryCanvasTextFull = formatDiscoveryCanvas(canvas);
|
|
10449
|
-
} catch {
|
|
10450
|
-
}
|
|
10451
|
-
let taskCommentsTextFull;
|
|
10452
|
-
try {
|
|
10453
|
-
const comments = await adapter2.getRecentTaskComments?.(20);
|
|
10454
|
-
if (comments && comments.length > 0) {
|
|
10455
|
-
const byTask = /* @__PURE__ */ new Map();
|
|
10456
|
-
for (const c of comments) {
|
|
10457
|
-
const list = byTask.get(c.taskId) ?? [];
|
|
10458
|
-
list.push(c);
|
|
10459
|
-
byTask.set(c.taskId, list);
|
|
10460
|
-
}
|
|
10461
|
-
const lines = [];
|
|
10462
|
-
for (const [taskId, taskComments] of byTask) {
|
|
10463
|
-
const limited = taskComments.slice(0, 5);
|
|
10464
|
-
for (const c of limited) {
|
|
10465
|
-
const truncated = c.content.length > 200 ? c.content.slice(0, 200) + "..." : c.content;
|
|
10466
|
-
const date = c.createdAt.split("T")[0];
|
|
10467
|
-
lines.push(`- **${taskId}** \u2014 ${c.author} (${date}): "${truncated}"`);
|
|
10468
|
-
}
|
|
10469
|
-
}
|
|
10470
|
-
taskCommentsTextFull = lines.join("\n");
|
|
10471
|
-
}
|
|
10472
|
-
} catch {
|
|
10473
|
-
}
|
|
10780
|
+
const discoveryCanvasTextFull = await assembleDiscoveryCanvasText(adapter2);
|
|
10781
|
+
const taskCommentsTextFull = await assembleTaskComments(adapter2);
|
|
10474
10782
|
let ctx = {
|
|
10475
10783
|
mode,
|
|
10476
10784
|
cycleNumber: health.totalCycles,
|
|
@@ -10565,7 +10873,7 @@ ${cleanContent}`;
|
|
|
10565
10873
|
title: t.title,
|
|
10566
10874
|
status: t.status || "Backlog",
|
|
10567
10875
|
priority: t.priority || "P1 High",
|
|
10568
|
-
complexity: t.complexity || "
|
|
10876
|
+
complexity: t.complexity || "Small",
|
|
10569
10877
|
module: t.module || "Core",
|
|
10570
10878
|
epic: t.epic || "Platform",
|
|
10571
10879
|
phase: t.phase || "Phase 1",
|
|
@@ -10671,7 +10979,7 @@ ${cleanContent}`;
|
|
|
10671
10979
|
title: task.title,
|
|
10672
10980
|
status: task.status || "Backlog",
|
|
10673
10981
|
priority: task.priority || "P1 High",
|
|
10674
|
-
complexity: task.complexity || "
|
|
10982
|
+
complexity: task.complexity || "Small",
|
|
10675
10983
|
module: task.module || "Core",
|
|
10676
10984
|
epic: task.epic || "Platform",
|
|
10677
10985
|
phase: task.phase || "Phase 1",
|
|
@@ -11048,6 +11356,106 @@ async function applyPlan(adapter2, config2, rawLlmOutput, mode, cycleNumber, str
|
|
|
11048
11356
|
return Promise.race([workPromise, timeoutPromise]);
|
|
11049
11357
|
}
|
|
11050
11358
|
|
|
11359
|
+
// src/lib/phase-realign.ts
|
|
11360
|
+
function extractPhaseNumber(phaseField) {
|
|
11361
|
+
const match = phaseField.match(/Phase\s+(\d+)/i);
|
|
11362
|
+
return match ? parseInt(match[1], 10) : null;
|
|
11363
|
+
}
|
|
11364
|
+
var STATUS_ORDER = {
|
|
11365
|
+
"Not Started": 0,
|
|
11366
|
+
"In Progress": 1,
|
|
11367
|
+
"Done": 2,
|
|
11368
|
+
"Deferred": 3
|
|
11369
|
+
// Manual override — protected from auto-propagation
|
|
11370
|
+
};
|
|
11371
|
+
function isForwardTransition(currentStatus, newStatus) {
|
|
11372
|
+
const currentOrder = STATUS_ORDER[currentStatus] ?? 99;
|
|
11373
|
+
const newOrder = STATUS_ORDER[newStatus] ?? 99;
|
|
11374
|
+
return newOrder >= currentOrder;
|
|
11375
|
+
}
|
|
11376
|
+
function derivePhaseStatus(tasks) {
|
|
11377
|
+
if (tasks.length === 0) return null;
|
|
11378
|
+
const terminal = /* @__PURE__ */ new Set(["Done", "Cancelled"]);
|
|
11379
|
+
const active = /* @__PURE__ */ new Set(["In Progress", "In Review"]);
|
|
11380
|
+
const allTerminal = tasks.every((t) => terminal.has(t.status));
|
|
11381
|
+
if (allTerminal) return "Done";
|
|
11382
|
+
const hasActive = tasks.some((t) => active.has(t.status));
|
|
11383
|
+
if (hasActive) return "In Progress";
|
|
11384
|
+
const hasDone = tasks.some((t) => terminal.has(t.status));
|
|
11385
|
+
if (hasDone) return "In Progress";
|
|
11386
|
+
return "Not Started";
|
|
11387
|
+
}
|
|
11388
|
+
function realignPhases(phases, tasks) {
|
|
11389
|
+
if (phases.length === 0) {
|
|
11390
|
+
return { changes: [], updatedPhases: phases };
|
|
11391
|
+
}
|
|
11392
|
+
const tasksByLabel = /* @__PURE__ */ new Map();
|
|
11393
|
+
const tasksByOrder = /* @__PURE__ */ new Map();
|
|
11394
|
+
for (const task of tasks) {
|
|
11395
|
+
if (task.phase) {
|
|
11396
|
+
const existing = tasksByLabel.get(task.phase) ?? [];
|
|
11397
|
+
existing.push(task);
|
|
11398
|
+
tasksByLabel.set(task.phase, existing);
|
|
11399
|
+
}
|
|
11400
|
+
const num = extractPhaseNumber(task.phase);
|
|
11401
|
+
if (num !== null) {
|
|
11402
|
+
const existing = tasksByOrder.get(num) ?? [];
|
|
11403
|
+
existing.push(task);
|
|
11404
|
+
tasksByOrder.set(num, existing);
|
|
11405
|
+
}
|
|
11406
|
+
}
|
|
11407
|
+
const changes = [];
|
|
11408
|
+
const result = [];
|
|
11409
|
+
for (const phase of phases) {
|
|
11410
|
+
const phaseTasks = tasksByLabel.get(phase.label) ?? tasksByOrder.get(phase.order) ?? [];
|
|
11411
|
+
const derivedStatus = derivePhaseStatus(phaseTasks);
|
|
11412
|
+
if (derivedStatus !== null && derivedStatus !== phase.status && isForwardTransition(phase.status, derivedStatus)) {
|
|
11413
|
+
changes.push({
|
|
11414
|
+
phaseId: phase.id,
|
|
11415
|
+
oldStatus: phase.status,
|
|
11416
|
+
newStatus: derivedStatus
|
|
11417
|
+
});
|
|
11418
|
+
result.push({ ...phase, status: derivedStatus });
|
|
11419
|
+
} else {
|
|
11420
|
+
result.push(phase);
|
|
11421
|
+
}
|
|
11422
|
+
}
|
|
11423
|
+
return { changes, updatedPhases: result };
|
|
11424
|
+
}
|
|
11425
|
+
function formatPhaseChanges(changes) {
|
|
11426
|
+
if (changes.length === 0) return "";
|
|
11427
|
+
const lines = ["**Phase Realignment:**"];
|
|
11428
|
+
for (const c of changes) {
|
|
11429
|
+
lines.push(`- ${c.phaseId}: ${c.oldStatus} \u2192 ${c.newStatus}`);
|
|
11430
|
+
}
|
|
11431
|
+
return lines.join("\n");
|
|
11432
|
+
}
|
|
11433
|
+
async function propagatePhaseStatus(adapter2) {
|
|
11434
|
+
const [phases, tasks] = await Promise.all([
|
|
11435
|
+
adapter2.readPhases(),
|
|
11436
|
+
adapter2.queryBoard()
|
|
11437
|
+
]);
|
|
11438
|
+
if (phases.length === 0) return [];
|
|
11439
|
+
const horizons = await adapter2.readHorizons?.() ?? [];
|
|
11440
|
+
const stages = await adapter2.readStages?.() ?? [];
|
|
11441
|
+
let eligiblePhases = phases;
|
|
11442
|
+
if (horizons.length > 1 && stages.length > 0) {
|
|
11443
|
+
const h1 = horizons.reduce((min, h) => h.sortOrder < min.sortOrder ? h : min, horizons[0]);
|
|
11444
|
+
const h1StageIds = new Set(stages.filter((s) => s.horizonId === h1.id).map((s) => s.id));
|
|
11445
|
+
eligiblePhases = phases.filter((p) => !p.stageId || h1StageIds.has(p.stageId));
|
|
11446
|
+
}
|
|
11447
|
+
const { changes, updatedPhases } = realignPhases(eligiblePhases, tasks);
|
|
11448
|
+
if (changes.length > 0) {
|
|
11449
|
+
const updatedIds = new Set(updatedPhases.map((p) => p.id));
|
|
11450
|
+
const mergedPhases = [
|
|
11451
|
+
...updatedPhases,
|
|
11452
|
+
...phases.filter((p) => !updatedIds.has(p.id))
|
|
11453
|
+
];
|
|
11454
|
+
await adapter2.writePhases(mergedPhases);
|
|
11455
|
+
}
|
|
11456
|
+
return changes;
|
|
11457
|
+
}
|
|
11458
|
+
|
|
11051
11459
|
// src/tools/plan.ts
|
|
11052
11460
|
var lastPrepareContextHashes;
|
|
11053
11461
|
var lastPrepareUserMessage;
|
|
@@ -11187,6 +11595,10 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
11187
11595
|
};
|
|
11188
11596
|
}
|
|
11189
11597
|
{
|
|
11598
|
+
try {
|
|
11599
|
+
await propagatePhaseStatus(adapter2);
|
|
11600
|
+
} catch {
|
|
11601
|
+
}
|
|
11190
11602
|
const result = await preparePlan(adapter2, config2, filters, focus, force);
|
|
11191
11603
|
lastPrepareContextHashes = result.contextHashes;
|
|
11192
11604
|
lastPrepareUserMessage = result.userMessage;
|
|
@@ -11240,81 +11652,10 @@ ${result.userMessage}
|
|
|
11240
11652
|
// src/services/strategy.ts
|
|
11241
11653
|
init_dist2();
|
|
11242
11654
|
import { randomUUID as randomUUID8, createHash as createHash2 } from "crypto";
|
|
11243
|
-
|
|
11244
|
-
|
|
11245
|
-
|
|
11246
|
-
|
|
11247
|
-
return match ? parseInt(match[1], 10) : null;
|
|
11248
|
-
}
|
|
11249
|
-
function derivePhaseStatus(tasks) {
|
|
11250
|
-
if (tasks.length === 0) return null;
|
|
11251
|
-
const terminal = /* @__PURE__ */ new Set(["Done", "Cancelled"]);
|
|
11252
|
-
const active = /* @__PURE__ */ new Set(["In Progress", "In Review"]);
|
|
11253
|
-
const allTerminal = tasks.every((t) => terminal.has(t.status));
|
|
11254
|
-
if (allTerminal) return "Done";
|
|
11255
|
-
const hasActive = tasks.some((t) => active.has(t.status));
|
|
11256
|
-
if (hasActive) return "In Progress";
|
|
11257
|
-
const hasDone = tasks.some((t) => terminal.has(t.status));
|
|
11258
|
-
if (hasDone) return "In Progress";
|
|
11259
|
-
return "Not Started";
|
|
11260
|
-
}
|
|
11261
|
-
function realignPhases(phases, tasks) {
|
|
11262
|
-
if (phases.length === 0) {
|
|
11263
|
-
return { changes: [], updatedPhases: phases };
|
|
11264
|
-
}
|
|
11265
|
-
const tasksByLabel = /* @__PURE__ */ new Map();
|
|
11266
|
-
const tasksByOrder = /* @__PURE__ */ new Map();
|
|
11267
|
-
for (const task of tasks) {
|
|
11268
|
-
if (task.phase) {
|
|
11269
|
-
const existing = tasksByLabel.get(task.phase) ?? [];
|
|
11270
|
-
existing.push(task);
|
|
11271
|
-
tasksByLabel.set(task.phase, existing);
|
|
11272
|
-
}
|
|
11273
|
-
const num = extractPhaseNumber(task.phase);
|
|
11274
|
-
if (num !== null) {
|
|
11275
|
-
const existing = tasksByOrder.get(num) ?? [];
|
|
11276
|
-
existing.push(task);
|
|
11277
|
-
tasksByOrder.set(num, existing);
|
|
11278
|
-
}
|
|
11279
|
-
}
|
|
11280
|
-
const changes = [];
|
|
11281
|
-
const result = [];
|
|
11282
|
-
for (const phase of phases) {
|
|
11283
|
-
const phaseTasks = tasksByLabel.get(phase.label) ?? tasksByOrder.get(phase.order) ?? [];
|
|
11284
|
-
const derivedStatus = derivePhaseStatus(phaseTasks);
|
|
11285
|
-
if (derivedStatus !== null && derivedStatus !== phase.status) {
|
|
11286
|
-
changes.push({
|
|
11287
|
-
phaseId: phase.id,
|
|
11288
|
-
oldStatus: phase.status,
|
|
11289
|
-
newStatus: derivedStatus
|
|
11290
|
-
});
|
|
11291
|
-
result.push({ ...phase, status: derivedStatus });
|
|
11292
|
-
} else {
|
|
11293
|
-
result.push(phase);
|
|
11294
|
-
}
|
|
11295
|
-
}
|
|
11296
|
-
return { changes, updatedPhases: result };
|
|
11297
|
-
}
|
|
11298
|
-
function formatPhaseChanges(changes) {
|
|
11299
|
-
if (changes.length === 0) return "";
|
|
11300
|
-
const lines = ["**Phase Realignment:**"];
|
|
11301
|
-
for (const c of changes) {
|
|
11302
|
-
lines.push(`- ${c.phaseId}: ${c.oldStatus} \u2192 ${c.newStatus}`);
|
|
11303
|
-
}
|
|
11304
|
-
return lines.join("\n");
|
|
11305
|
-
}
|
|
11306
|
-
async function propagatePhaseStatus(adapter2) {
|
|
11307
|
-
const [phases, tasks] = await Promise.all([
|
|
11308
|
-
adapter2.readPhases(),
|
|
11309
|
-
adapter2.queryBoard()
|
|
11310
|
-
]);
|
|
11311
|
-
if (phases.length === 0) return [];
|
|
11312
|
-
const { changes, updatedPhases } = realignPhases(phases, tasks);
|
|
11313
|
-
if (changes.length > 0) {
|
|
11314
|
-
await adapter2.writePhases(updatedPhases);
|
|
11315
|
-
}
|
|
11316
|
-
return changes;
|
|
11317
|
-
}
|
|
11655
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
11656
|
+
import { existsSync, readdirSync, statSync } from "fs";
|
|
11657
|
+
import { join as join2 } from "path";
|
|
11658
|
+
import { homedir } from "os";
|
|
11318
11659
|
|
|
11319
11660
|
// src/lib/value-report.ts
|
|
11320
11661
|
var MIN_SNAPSHOTS = 5;
|
|
@@ -11556,6 +11897,45 @@ function formatTaskCompact(t) {
|
|
|
11556
11897
|
Reviewed: ${t.reviewed}${t.dependsOn ? ` | Depends on: ${t.dependsOn}` : ""}` + (notesSnippet ? `
|
|
11557
11898
|
Notes: ${notesSnippet}` : "") + "\n";
|
|
11558
11899
|
}
|
|
11900
|
+
function getAdHocCommits(projectRoot, sinceTag) {
|
|
11901
|
+
try {
|
|
11902
|
+
const logArgs = ["log", "--oneline", "--no-merges", "-100"];
|
|
11903
|
+
if (sinceTag) {
|
|
11904
|
+
logArgs.splice(1, 0, `${sinceTag}..HEAD`);
|
|
11905
|
+
}
|
|
11906
|
+
const output = execFileSync2("git", logArgs, {
|
|
11907
|
+
cwd: projectRoot,
|
|
11908
|
+
encoding: "utf-8",
|
|
11909
|
+
timeout: 5e3
|
|
11910
|
+
}).trim();
|
|
11911
|
+
if (!output) return void 0;
|
|
11912
|
+
const allCommits = output.split("\n");
|
|
11913
|
+
const taskPattern = /task-\d+/i;
|
|
11914
|
+
const nonTaskCommits = allCommits.filter((line) => !taskPattern.test(line));
|
|
11915
|
+
if (nonTaskCommits.length === 0) return void 0;
|
|
11916
|
+
const capped = nonTaskCommits.slice(0, 20);
|
|
11917
|
+
const groups = {};
|
|
11918
|
+
const typePattern = /^[a-f0-9]+ (feat|fix|chore|refactor|docs|style|test|ci|perf|build|release)[\s(:]/i;
|
|
11919
|
+
for (const line of capped) {
|
|
11920
|
+
const match = line.match(typePattern);
|
|
11921
|
+
const type = match ? match[1].toLowerCase() : "other";
|
|
11922
|
+
(groups[type] ??= []).push(line);
|
|
11923
|
+
}
|
|
11924
|
+
const lines = [];
|
|
11925
|
+
lines.push(`${nonTaskCommits.length} non-task commits found${nonTaskCommits.length > 20 ? " (showing 20 most recent)" : ""}:
|
|
11926
|
+
`);
|
|
11927
|
+
for (const [type, commits] of Object.entries(groups).sort((a, b2) => b2[1].length - a[1].length)) {
|
|
11928
|
+
lines.push(`**${type}** (${commits.length}):`);
|
|
11929
|
+
for (const c of commits) {
|
|
11930
|
+
lines.push(`- ${c}`);
|
|
11931
|
+
}
|
|
11932
|
+
lines.push("");
|
|
11933
|
+
}
|
|
11934
|
+
return lines.join("\n").trimEnd();
|
|
11935
|
+
} catch {
|
|
11936
|
+
return void 0;
|
|
11937
|
+
}
|
|
11938
|
+
}
|
|
11559
11939
|
async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, projectRoot) {
|
|
11560
11940
|
const lastReviewCycleNum = cycleNumber - cyclesSinceLastReview;
|
|
11561
11941
|
const [
|
|
@@ -11571,7 +11951,9 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
|
|
|
11571
11951
|
currentNorthStar,
|
|
11572
11952
|
canvas,
|
|
11573
11953
|
decisionUsage,
|
|
11574
|
-
recData
|
|
11954
|
+
recData,
|
|
11955
|
+
pendingRecs,
|
|
11956
|
+
registeredDocs
|
|
11575
11957
|
] = await Promise.all([
|
|
11576
11958
|
adapter2.readProductBrief(),
|
|
11577
11959
|
adapter2.getActiveDecisions(),
|
|
@@ -11592,7 +11974,10 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
|
|
|
11592
11974
|
// Previously sequential — now parallel
|
|
11593
11975
|
adapter2.readDiscoveryCanvas().catch(() => ({})),
|
|
11594
11976
|
adapter2.getDecisionUsage(cycleNumber).catch(() => []),
|
|
11595
|
-
adapter2.getRecommendationEffectiveness?.()?.catch(() => []) ?? Promise.resolve([])
|
|
11977
|
+
adapter2.getRecommendationEffectiveness?.()?.catch(() => []) ?? Promise.resolve([]),
|
|
11978
|
+
adapter2.getPendingRecommendations().catch(() => []),
|
|
11979
|
+
// Doc registry — summaries for strategy review context
|
|
11980
|
+
adapter2.searchDocs?.({ status: "active", limit: 10 })?.catch(() => []) ?? Promise.resolve([])
|
|
11596
11981
|
]);
|
|
11597
11982
|
const tasks = [...activeTasks, ...recentDoneTasks];
|
|
11598
11983
|
const recentLog = log;
|
|
@@ -11611,7 +11996,7 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
|
|
|
11611
11996
|
const previousReviewsText = formatPreviousReviews(previousStrategyReviews);
|
|
11612
11997
|
const cappedBrief = capProductBrief2(productBrief);
|
|
11613
11998
|
const smartBoard = formatBoardForReviewSmart(tasks, lastReviewCycleNum);
|
|
11614
|
-
const buildReportsText =
|
|
11999
|
+
const buildReportsText = formatRecentReportsSummary(reports, 10);
|
|
11615
12000
|
logDataSourceSummary("strategy_review", [
|
|
11616
12001
|
{ label: "productBrief", hasData: warnIfEmpty("readProductBrief", productBrief) },
|
|
11617
12002
|
{ label: "activeDecisions", hasData: warnIfEmpty("getActiveDecisions", decisions) },
|
|
@@ -11626,7 +12011,7 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
|
|
|
11626
12011
|
]);
|
|
11627
12012
|
let discoveryCanvasText;
|
|
11628
12013
|
try {
|
|
11629
|
-
const fullCanvasText =
|
|
12014
|
+
const fullCanvasText = formatDiscoveryCanvas(canvas);
|
|
11630
12015
|
if (fullCanvasText) {
|
|
11631
12016
|
const canvasHash = createHash2("md5").update(fullCanvasText).digest("hex");
|
|
11632
12017
|
const lastReview = previousStrategyReviews?.[0];
|
|
@@ -11642,7 +12027,7 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
|
|
|
11642
12027
|
const briefImplicationsFromBuilds = reports.filter((r) => Array.isArray(r.briefImplications) && r.briefImplications.length > 0).flatMap((r) => r.briefImplications.map((bi) => ({ ...bi, taskName: r.taskName, cycle: r.cycle })));
|
|
11643
12028
|
let briefImplicationsText;
|
|
11644
12029
|
if (briefImplicationsFromBuilds.length > 0) {
|
|
11645
|
-
briefImplicationsText = "**Build-Discovered Evidence:**\n" + briefImplicationsFromBuilds.map((bi) => `- [
|
|
12030
|
+
briefImplicationsText = "**Build-Discovered Evidence:**\n" + briefImplicationsFromBuilds.map((bi) => `- [C${bi.cycle} ${bi.taskName}] [${bi.canvasSection}/${bi.type}] ${bi.detail}`).join("\n");
|
|
11646
12031
|
}
|
|
11647
12032
|
let phasesText;
|
|
11648
12033
|
try {
|
|
@@ -11667,12 +12052,90 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
|
|
|
11667
12052
|
}
|
|
11668
12053
|
} catch {
|
|
11669
12054
|
}
|
|
12055
|
+
let pendingRecsText;
|
|
12056
|
+
try {
|
|
12057
|
+
if (pendingRecs.length > 0) {
|
|
12058
|
+
const lines = pendingRecs.map((r) => {
|
|
12059
|
+
const targetSuffix = r.target ? ` \u2192 ${r.target}` : "";
|
|
12060
|
+
return `- [${r.status}] (Cycle ${r.createdCycle}, ${r.type}) ${r.content}${targetSuffix}`;
|
|
12061
|
+
});
|
|
12062
|
+
pendingRecsText = `${pendingRecs.length} pending recommendation(s) from prior reviews:
|
|
12063
|
+
${lines.join("\n")}`;
|
|
12064
|
+
}
|
|
12065
|
+
} catch {
|
|
12066
|
+
}
|
|
12067
|
+
let adHocCommitsText;
|
|
12068
|
+
try {
|
|
12069
|
+
const sinceTag = `v0.${lastReviewCycleNum}.0`;
|
|
12070
|
+
adHocCommitsText = getAdHocCommits(projectRoot, sinceTag);
|
|
12071
|
+
} catch {
|
|
12072
|
+
}
|
|
12073
|
+
let registeredDocsText;
|
|
12074
|
+
try {
|
|
12075
|
+
if (registeredDocs && registeredDocs.length > 0) {
|
|
12076
|
+
const lines = registeredDocs.map(
|
|
12077
|
+
(d) => `- **${d.title}** (${d.type}, ${d.status}) \u2014 ${d.summary}`
|
|
12078
|
+
);
|
|
12079
|
+
registeredDocsText = `${registeredDocs.length} registered doc(s):
|
|
12080
|
+
${lines.join("\n")}`;
|
|
12081
|
+
}
|
|
12082
|
+
} catch {
|
|
12083
|
+
}
|
|
12084
|
+
let recentPlansText;
|
|
12085
|
+
try {
|
|
12086
|
+
const plansDir = join2(homedir(), ".claude", "plans");
|
|
12087
|
+
if (existsSync(plansDir)) {
|
|
12088
|
+
const lastReviewDate = previousStrategyReviews?.[0]?.date ? new Date(previousStrategyReviews[0].date) : /* @__PURE__ */ new Date(0);
|
|
12089
|
+
const planFiles = readdirSync(plansDir).filter((f) => f.endsWith(".md")).map((f) => {
|
|
12090
|
+
const fullPath = join2(plansDir, f);
|
|
12091
|
+
const stat2 = statSync(fullPath);
|
|
12092
|
+
return { name: f, modified: stat2.mtime, size: stat2.size };
|
|
12093
|
+
}).filter((f) => f.modified > lastReviewDate).sort((a, b2) => b2.modified.getTime() - a.modified.getTime()).slice(0, 15);
|
|
12094
|
+
if (planFiles.length > 0) {
|
|
12095
|
+
const lines = planFiles.map((f) => {
|
|
12096
|
+
const kb = (f.size / 1024).toFixed(1);
|
|
12097
|
+
return `- ${f.name} (${kb}KB, ${f.modified.toISOString().slice(0, 10)})`;
|
|
12098
|
+
});
|
|
12099
|
+
recentPlansText = `${planFiles.length} plan file(s) modified since last review:
|
|
12100
|
+
${lines.join("\n")}`;
|
|
12101
|
+
}
|
|
12102
|
+
}
|
|
12103
|
+
} catch {
|
|
12104
|
+
}
|
|
12105
|
+
let unregisteredDocsText;
|
|
12106
|
+
try {
|
|
12107
|
+
const docsDir = join2(projectRoot, "docs");
|
|
12108
|
+
if (existsSync(docsDir)) {
|
|
12109
|
+
const registeredPaths = new Set(
|
|
12110
|
+
(registeredDocs ?? []).map((d) => d.path).filter(Boolean)
|
|
12111
|
+
);
|
|
12112
|
+
const allDocFiles = [];
|
|
12113
|
+
const scanDir = (dir, prefix) => {
|
|
12114
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
12115
|
+
if (entry.isDirectory()) scanDir(join2(dir, entry.name), `${prefix}${entry.name}/`);
|
|
12116
|
+
else if (entry.name.endsWith(".md")) allDocFiles.push(`${prefix}${entry.name}`);
|
|
12117
|
+
}
|
|
12118
|
+
};
|
|
12119
|
+
scanDir(docsDir, "docs/");
|
|
12120
|
+
const unregistered = allDocFiles.filter((f) => !registeredPaths.has(f));
|
|
12121
|
+
if (unregistered.length > 0) {
|
|
12122
|
+
unregisteredDocsText = `${unregistered.length} unregistered doc(s) in docs/:
|
|
12123
|
+
${unregistered.slice(0, 10).map((f) => `- ${f}`).join("\n")}`;
|
|
12124
|
+
}
|
|
12125
|
+
}
|
|
12126
|
+
} catch {
|
|
12127
|
+
}
|
|
11670
12128
|
logDataSourceSummary("strategy_review_audit", [
|
|
11671
12129
|
{ label: "discoveryCanvas", hasData: discoveryCanvasText !== void 0 },
|
|
11672
12130
|
{ label: "briefImplications", hasData: briefImplicationsText !== void 0 },
|
|
11673
12131
|
{ label: "phases", hasData: phasesText !== void 0 },
|
|
11674
12132
|
{ label: "decisionUsage", hasData: decisionUsageText !== void 0 },
|
|
11675
|
-
{ label: "recEffectiveness", hasData: recEffectivenessText !== void 0 }
|
|
12133
|
+
{ label: "recEffectiveness", hasData: recEffectivenessText !== void 0 },
|
|
12134
|
+
{ label: "pendingRecs", hasData: pendingRecsText !== void 0 },
|
|
12135
|
+
{ label: "adHocCommits", hasData: adHocCommitsText !== void 0 },
|
|
12136
|
+
{ label: "registeredDocs", hasData: registeredDocsText !== void 0 },
|
|
12137
|
+
{ label: "recentPlans", hasData: recentPlansText !== void 0 },
|
|
12138
|
+
{ label: "unregisteredDocs", hasData: unregisteredDocsText !== void 0 }
|
|
11676
12139
|
]);
|
|
11677
12140
|
const context = {
|
|
11678
12141
|
sessionNumber: cycleNumber,
|
|
@@ -11692,10 +12155,15 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
|
|
|
11692
12155
|
phases: phasesText,
|
|
11693
12156
|
decisionUsage: decisionUsageText,
|
|
11694
12157
|
northStar: currentNorthStar ?? void 0,
|
|
11695
|
-
recommendationEffectiveness: recEffectivenessText
|
|
12158
|
+
recommendationEffectiveness: recEffectivenessText,
|
|
12159
|
+
adHocCommits: adHocCommitsText,
|
|
12160
|
+
pendingRecommendations: pendingRecsText,
|
|
12161
|
+
registeredDocs: registeredDocsText,
|
|
12162
|
+
recentPlans: recentPlansText,
|
|
12163
|
+
unregisteredDocs: unregisteredDocsText
|
|
11696
12164
|
};
|
|
11697
|
-
const BUDGET_SOFT2 =
|
|
11698
|
-
const BUDGET_HARD2 =
|
|
12165
|
+
const BUDGET_SOFT2 = 5e4;
|
|
12166
|
+
const BUDGET_HARD2 = 6e4;
|
|
11699
12167
|
const compressionSteps = [];
|
|
11700
12168
|
function measureContext(ctx) {
|
|
11701
12169
|
return Object.values(ctx).filter((v) => typeof v === "string").reduce((sum, s) => sum + s.length, 0);
|
|
@@ -11724,13 +12192,31 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
|
|
|
11724
12192
|
}
|
|
11725
12193
|
}
|
|
11726
12194
|
if (contextSize > BUDGET_SOFT2 && context.allBuildReports) {
|
|
11727
|
-
const summary = formatRecentReportsSummary(reports,
|
|
12195
|
+
const summary = formatRecentReportsSummary(reports, 5);
|
|
11728
12196
|
if (summary.length < context.allBuildReports.length) {
|
|
11729
12197
|
context.allBuildReports = summary;
|
|
11730
12198
|
contextSize = measureContext(context);
|
|
11731
|
-
compressionSteps.push("Step 3: build reports
|
|
12199
|
+
compressionSteps.push("Step 3: build reports capped to 5");
|
|
12200
|
+
}
|
|
12201
|
+
}
|
|
12202
|
+
if (contextSize > BUDGET_SOFT2 && context.sessionLog) {
|
|
12203
|
+
const logLines = context.sessionLog.split("\n---\n");
|
|
12204
|
+
if (logLines.length > 5) {
|
|
12205
|
+
context.sessionLog = logLines.slice(0, 5).join("\n---\n") + `
|
|
12206
|
+
|
|
12207
|
+
*(${logLines.length - 5} older cycle log entries omitted for context budget)*`;
|
|
12208
|
+
contextSize = measureContext(context);
|
|
12209
|
+
compressionSteps.push(`Step 4: cycle log capped to 5 (was ${logLines.length})`);
|
|
11732
12210
|
}
|
|
11733
12211
|
}
|
|
12212
|
+
if (contextSize > BUDGET_SOFT2 && context.board) {
|
|
12213
|
+
context.board = context.board.replace(
|
|
12214
|
+
/ Notes: .{100,}/g,
|
|
12215
|
+
(match) => match.slice(0, match.indexOf("Notes: ") + 107) + "..."
|
|
12216
|
+
);
|
|
12217
|
+
contextSize = measureContext(context);
|
|
12218
|
+
compressionSteps.push("Step 5: board task notes truncated to 100 chars");
|
|
12219
|
+
}
|
|
11734
12220
|
if (contextSize > BUDGET_HARD2) {
|
|
11735
12221
|
const entries = Object.entries(context).filter((e) => typeof e[1] === "string").sort((a, b2) => b2[1].length - a[1].length);
|
|
11736
12222
|
if (entries.length > 0) {
|
|
@@ -11739,7 +12225,7 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
|
|
|
11739
12225
|
const newLength = Math.max(value.length - excess, 1e3);
|
|
11740
12226
|
context[key] = value.slice(0, newLength) + "\n\n[truncated \u2014 context budget exceeded]";
|
|
11741
12227
|
contextSize = measureContext(context);
|
|
11742
|
-
compressionSteps.push(`Step
|
|
12228
|
+
compressionSteps.push(`Step 6: truncated ${key} (was ${value.length} chars)`);
|
|
11743
12229
|
}
|
|
11744
12230
|
}
|
|
11745
12231
|
const estimatedTokens = Math.ceil(contextSize / 4);
|
|
@@ -11771,7 +12257,7 @@ ${cleanContent}`;
|
|
|
11771
12257
|
const sd = data;
|
|
11772
12258
|
try {
|
|
11773
12259
|
const currentCanvas = await adapter2.readDiscoveryCanvas();
|
|
11774
|
-
const canvasText =
|
|
12260
|
+
const canvasText = formatDiscoveryCanvas(currentCanvas);
|
|
11775
12261
|
if (canvasText) {
|
|
11776
12262
|
return { ...sd, canvasHash: createHash2("md5").update(canvasText).digest("hex") };
|
|
11777
12263
|
}
|
|
@@ -11904,8 +12390,16 @@ async function processReviewOutput(adapter2, rawOutput, cycleNumber) {
|
|
|
11904
12390
|
phaseChanges = await writeBack2(adapter2, cycleNumber, data, displayText);
|
|
11905
12391
|
} catch (err) {
|
|
11906
12392
|
writeBackFailed = err instanceof Error ? err.message : String(err);
|
|
12393
|
+
try {
|
|
12394
|
+
await adapter2.savePendingReviewResponse?.(cycleNumber, rawOutput);
|
|
12395
|
+
} catch {
|
|
12396
|
+
}
|
|
11907
12397
|
}
|
|
11908
12398
|
if (!writeBackFailed) {
|
|
12399
|
+
try {
|
|
12400
|
+
await adapter2.clearPendingReviewResponse?.();
|
|
12401
|
+
} catch {
|
|
12402
|
+
}
|
|
11909
12403
|
const webhookUrl = process.env.PAPI_SLACK_WEBHOOK_URL;
|
|
11910
12404
|
slackWarning = await sendSlackWebhook(webhookUrl, buildSlackSummary(data));
|
|
11911
12405
|
}
|
|
@@ -11953,6 +12447,28 @@ async function prepareStrategyReview(adapter2, force, projectRoot, adapterType)
|
|
|
11953
12447
|
isPg ? "Could not read cycle health from the database. Check your DATABASE_URL and verify the project exists." : "Could not read cycle health from PLANNING_LOG.md. Run setup first to initialise your PAPI project."
|
|
11954
12448
|
);
|
|
11955
12449
|
}
|
|
12450
|
+
try {
|
|
12451
|
+
const pending = await adapter2.getPendingReviewResponse?.();
|
|
12452
|
+
if (pending) {
|
|
12453
|
+
return {
|
|
12454
|
+
cycleNumber: pending.cycleNumber || cycleNumber,
|
|
12455
|
+
systemPrompt: "",
|
|
12456
|
+
userMessage: `\u26A0\uFE0F **Pending Strategy Review Found**
|
|
12457
|
+
|
|
12458
|
+
A previous strategy review (Cycle ${pending.cycleNumber || cycleNumber}) failed to write back. The raw LLM response has been preserved.
|
|
12459
|
+
|
|
12460
|
+
To retry, call \`strategy_review\` with:
|
|
12461
|
+
- \`mode\`: "apply"
|
|
12462
|
+
- \`llm_response\`: (the response below)
|
|
12463
|
+
- \`cycle_number\`: ${pending.cycleNumber || cycleNumber}
|
|
12464
|
+
|
|
12465
|
+
---
|
|
12466
|
+
|
|
12467
|
+
${pending.rawResponse}`
|
|
12468
|
+
};
|
|
12469
|
+
}
|
|
12470
|
+
} catch {
|
|
12471
|
+
}
|
|
11956
12472
|
let context;
|
|
11957
12473
|
try {
|
|
11958
12474
|
context = await assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, projectRoot);
|
|
@@ -11982,7 +12498,8 @@ async function prepareStrategyReview(adapter2, force, projectRoot, adapterType)
|
|
|
11982
12498
|
return {
|
|
11983
12499
|
cycleNumber,
|
|
11984
12500
|
systemPrompt,
|
|
11985
|
-
userMessage
|
|
12501
|
+
userMessage,
|
|
12502
|
+
contextSizeChars: userMessage.length
|
|
11986
12503
|
};
|
|
11987
12504
|
}
|
|
11988
12505
|
async function applyStrategyReviewOutput(adapter2, rawLlmOutput, cycleNumber) {
|
|
@@ -12066,42 +12583,6 @@ function formatRecentReportsSummary(reports, count) {
|
|
|
12066
12583
|
return `- S${r.cycle} ${r.taskName}: ${effort}${surprises}`;
|
|
12067
12584
|
}).join("\n");
|
|
12068
12585
|
}
|
|
12069
|
-
function formatDiscoveryCanvasForReview(canvas) {
|
|
12070
|
-
const sections = [];
|
|
12071
|
-
if (canvas.landscapeReferences && canvas.landscapeReferences.length > 0) {
|
|
12072
|
-
sections.push("**Landscape & References:**");
|
|
12073
|
-
for (const ref of canvas.landscapeReferences) {
|
|
12074
|
-
const url = ref.url ? ` (${ref.url})` : "";
|
|
12075
|
-
const notes = ref.notes ? ` \u2014 ${ref.notes}` : "";
|
|
12076
|
-
sections.push(`- ${ref.name}${url}${notes}`);
|
|
12077
|
-
}
|
|
12078
|
-
}
|
|
12079
|
-
if (canvas.userJourneys && canvas.userJourneys.length > 0) {
|
|
12080
|
-
sections.push("**User Journeys:**");
|
|
12081
|
-
for (const j of canvas.userJourneys) {
|
|
12082
|
-
const priority = j.priority ? ` [${j.priority}]` : "";
|
|
12083
|
-
sections.push(`- **${j.persona}:** ${j.journey}${priority}`);
|
|
12084
|
-
}
|
|
12085
|
-
}
|
|
12086
|
-
if (canvas.mvpBoundary) {
|
|
12087
|
-
sections.push("**MVP Boundary:**", canvas.mvpBoundary);
|
|
12088
|
-
}
|
|
12089
|
-
if (canvas.assumptionsOpenQuestions && canvas.assumptionsOpenQuestions.length > 0) {
|
|
12090
|
-
sections.push("**Assumptions & Open Questions:**");
|
|
12091
|
-
for (const a of canvas.assumptionsOpenQuestions) {
|
|
12092
|
-
const evidence = a.evidence ? ` Evidence: ${a.evidence}` : "";
|
|
12093
|
-
sections.push(`- [${a.status}] ${a.text}${evidence}`);
|
|
12094
|
-
}
|
|
12095
|
-
}
|
|
12096
|
-
if (canvas.successSignals && canvas.successSignals.length > 0) {
|
|
12097
|
-
sections.push("**Success Signals:**");
|
|
12098
|
-
for (const s of canvas.successSignals) {
|
|
12099
|
-
const metric = s.metric ? ` (${s.metric}` + (s.target ? `, target: ${s.target})` : ")") : "";
|
|
12100
|
-
sections.push(`- ${s.signal}${metric}`);
|
|
12101
|
-
}
|
|
12102
|
-
}
|
|
12103
|
-
return sections.length > 0 ? sections.join("\n") : void 0;
|
|
12104
|
-
}
|
|
12105
12586
|
function formatPhasesForReview(phases, currentCycle) {
|
|
12106
12587
|
if (phases.length === 0) return void 0;
|
|
12107
12588
|
const lines = [];
|
|
@@ -12147,9 +12628,6 @@ async function formatHierarchyForReview(adapter2, currentCycle, prefetchedTasks)
|
|
|
12147
12628
|
existing.push(s);
|
|
12148
12629
|
stagesByHorizon.set(s.horizonId, existing);
|
|
12149
12630
|
}
|
|
12150
|
-
const phasesByStage = /* @__PURE__ */ new Map();
|
|
12151
|
-
for (const p of phases) {
|
|
12152
|
-
}
|
|
12153
12631
|
for (const h of horizons) {
|
|
12154
12632
|
lines.push(`### ${h.label} \u2014 ${h.status}`);
|
|
12155
12633
|
if (h.description) lines.push(`> ${h.description}`);
|
|
@@ -12221,6 +12699,11 @@ function buildStrategyChangeUserMessage(cycleNumber, text, productBrief, activeD
|
|
|
12221
12699
|
if (buildReports && buildReports.length > 0) {
|
|
12222
12700
|
parts.push("### Recent Velocity (last 3 cycles)", "", formatVelocitySummary(buildReports, 3), "");
|
|
12223
12701
|
parts.push("### Recent Build Reports", "", formatRecentReportsSummary(buildReports, 5), "");
|
|
12702
|
+
const briefImplicationsFromBuilds = buildReports.filter((r) => Array.isArray(r.briefImplications) && r.briefImplications.length > 0).flatMap((r) => r.briefImplications.map((bi) => ({ ...bi, taskName: r.taskName, cycle: r.cycle })));
|
|
12703
|
+
if (briefImplicationsFromBuilds.length > 0) {
|
|
12704
|
+
const briefImplicationsText = "**Build-Discovered Evidence:**\n" + briefImplicationsFromBuilds.map((bi) => `- [C${bi.cycle} ${bi.taskName}] [${bi.canvasSection}/${bi.type}] ${bi.detail}`).join("\n");
|
|
12705
|
+
parts.push("### Brief Implications (from builds)", "", briefImplicationsText, "");
|
|
12706
|
+
}
|
|
12224
12707
|
}
|
|
12225
12708
|
if (previousReviews) {
|
|
12226
12709
|
parts.push("### Previous Strategy Reviews", "", previousReviews, "");
|
|
@@ -12306,12 +12789,14 @@ async function prepareStrategyChange(adapter2, text) {
|
|
|
12306
12789
|
tasks = boardTasks;
|
|
12307
12790
|
buildReports = reports;
|
|
12308
12791
|
previousReviewsText = formatPreviousReviews(previousReviews);
|
|
12792
|
+
const hasBriefImplications = reports.some((r) => Array.isArray(r.briefImplications) && r.briefImplications.length > 0);
|
|
12309
12793
|
logDataSourceSummary("strategy_change", [
|
|
12310
12794
|
{ label: "productBrief", hasData: warnIfEmpty("readProductBrief", brief) },
|
|
12311
12795
|
{ label: "activeDecisions", hasData: warnIfEmpty("getActiveDecisions", decisions) },
|
|
12312
12796
|
{ label: "phases", hasData: warnIfEmpty("readPhases", readPhases) },
|
|
12313
12797
|
{ label: "boardTasks", hasData: warnIfEmpty("queryBoard", boardTasks) },
|
|
12314
12798
|
{ label: "buildReports", hasData: warnIfEmpty("getRecentBuildReports", reports) },
|
|
12799
|
+
{ label: "briefImplications", hasData: hasBriefImplications },
|
|
12315
12800
|
{ label: "previousReviews", hasData: previousReviewsText !== void 0 }
|
|
12316
12801
|
]);
|
|
12317
12802
|
} catch (err) {
|
|
@@ -12513,11 +12998,14 @@ ${result.slackWarning}` : "";
|
|
|
12513
12998
|
}
|
|
12514
12999
|
lastReviewUserMessage = result.userMessage;
|
|
12515
13000
|
lastReviewContextBytes = Buffer.byteLength(result.userMessage, "utf-8");
|
|
13001
|
+
const sizeNote = result.contextSizeChars ? `
|
|
13002
|
+
**Context size:** ${result.contextSizeChars.toLocaleString()} chars (~${Math.ceil(result.contextSizeChars / 4).toLocaleString()} tokens)
|
|
13003
|
+
` : "";
|
|
12516
13004
|
return textResponse(
|
|
12517
13005
|
`## PAPI Strategy Review \u2014 Prepare Phase (Cycle ${result.cycleNumber})
|
|
12518
13006
|
|
|
12519
13007
|
Follow the system prompt and context below to generate a strategy review.
|
|
12520
|
-
|
|
13008
|
+
` + sizeNote + `
|
|
12521
13009
|
When done, call \`strategy_review\` again with:
|
|
12522
13010
|
- \`mode\`: "apply"
|
|
12523
13011
|
- \`llm_response\`: your complete output
|
|
@@ -12740,7 +13228,7 @@ var boardViewTool = {
|
|
|
12740
13228
|
};
|
|
12741
13229
|
var boardDeprioritiseTool = {
|
|
12742
13230
|
name: "board_deprioritise",
|
|
12743
|
-
description: `Remove a task from the current cycle.
|
|
13231
|
+
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.`,
|
|
12744
13232
|
inputSchema: {
|
|
12745
13233
|
type: "object",
|
|
12746
13234
|
properties: {
|
|
@@ -12750,8 +13238,8 @@ var boardDeprioritiseTool = {
|
|
|
12750
13238
|
},
|
|
12751
13239
|
action: {
|
|
12752
13240
|
type: "string",
|
|
12753
|
-
enum: ["backlog", "defer", "cancel"],
|
|
12754
|
-
description: `"backlog" = not now, maybe later (preserves handoff). "defer" = valid but premature (hidden from planner). "cancel" = don't want this at all (permanently closed). If omitted, defaults to "backlog" for backwards compatibility.`
|
|
13241
|
+
enum: ["backlog", "defer", "block", "cancel"],
|
|
13242
|
+
description: `"backlog" = not now, maybe later (preserves handoff). "defer" = valid but premature (hidden from planner). "block" = waiting on external dependency (visible but skipped by planner \u2014 reason required). "cancel" = don't want this at all (permanently closed). If omitted, defaults to "backlog" for backwards compatibility.`
|
|
12755
13243
|
},
|
|
12756
13244
|
reason: {
|
|
12757
13245
|
type: "string",
|
|
@@ -12791,6 +13279,60 @@ var boardArchiveTool = {
|
|
|
12791
13279
|
required: []
|
|
12792
13280
|
}
|
|
12793
13281
|
};
|
|
13282
|
+
var boardEditTool = {
|
|
13283
|
+
name: "board_edit",
|
|
13284
|
+
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.",
|
|
13285
|
+
inputSchema: {
|
|
13286
|
+
type: "object",
|
|
13287
|
+
properties: {
|
|
13288
|
+
task_id: {
|
|
13289
|
+
type: "string",
|
|
13290
|
+
description: 'The task ID to edit (e.g. "task-42").'
|
|
13291
|
+
},
|
|
13292
|
+
title: {
|
|
13293
|
+
type: "string",
|
|
13294
|
+
description: "New task title."
|
|
13295
|
+
},
|
|
13296
|
+
priority: {
|
|
13297
|
+
type: "string",
|
|
13298
|
+
enum: ["P0 Critical", "P1 High", "P2 Medium", "P3 Low"],
|
|
13299
|
+
description: "New priority level."
|
|
13300
|
+
},
|
|
13301
|
+
complexity: {
|
|
13302
|
+
type: "string",
|
|
13303
|
+
enum: ["XS", "Small", "Medium", "Large", "XL"],
|
|
13304
|
+
description: "New complexity/effort estimate."
|
|
13305
|
+
},
|
|
13306
|
+
module: {
|
|
13307
|
+
type: "string",
|
|
13308
|
+
description: "New module assignment."
|
|
13309
|
+
},
|
|
13310
|
+
epic: {
|
|
13311
|
+
type: "string",
|
|
13312
|
+
description: "New epic assignment."
|
|
13313
|
+
},
|
|
13314
|
+
phase: {
|
|
13315
|
+
type: "string",
|
|
13316
|
+
description: "New phase assignment."
|
|
13317
|
+
},
|
|
13318
|
+
notes: {
|
|
13319
|
+
type: "string",
|
|
13320
|
+
description: "New notes (replaces existing notes)."
|
|
13321
|
+
},
|
|
13322
|
+
status: {
|
|
13323
|
+
type: "string",
|
|
13324
|
+
enum: ["Backlog", "In Cycle", "Ready", "In Progress", "In Review", "Done", "Blocked", "Cancelled", "Deferred"],
|
|
13325
|
+
description: "New status. Must be a valid transition from the current status."
|
|
13326
|
+
},
|
|
13327
|
+
maturity: {
|
|
13328
|
+
type: "string",
|
|
13329
|
+
enum: ["raw", "investigated", "ready"],
|
|
13330
|
+
description: "New maturity level."
|
|
13331
|
+
}
|
|
13332
|
+
},
|
|
13333
|
+
required: ["task_id"]
|
|
13334
|
+
}
|
|
13335
|
+
};
|
|
12794
13336
|
function pad(value, width) {
|
|
12795
13337
|
return value.length >= width ? value : value + " ".repeat(width - value.length);
|
|
12796
13338
|
}
|
|
@@ -12859,7 +13401,17 @@ async function handleBoardView(adapter2, args) {
|
|
|
12859
13401
|
limit: args.limit,
|
|
12860
13402
|
offset: args.offset
|
|
12861
13403
|
});
|
|
12862
|
-
|
|
13404
|
+
let output = formatBoard(result);
|
|
13405
|
+
try {
|
|
13406
|
+
const comments = await adapter2.getRecentTaskComments?.(30);
|
|
13407
|
+
if (comments && comments.length > 0) {
|
|
13408
|
+
const taskIds = new Set(result.tasks.map((t) => t.id));
|
|
13409
|
+
const section = formatTaskComments(comments, taskIds, "**Task Comments:**");
|
|
13410
|
+
if (section) output += "\n" + section;
|
|
13411
|
+
}
|
|
13412
|
+
} catch {
|
|
13413
|
+
}
|
|
13414
|
+
return textResponse(output);
|
|
12863
13415
|
}
|
|
12864
13416
|
async function handleBoardDeprioritise(adapter2, args) {
|
|
12865
13417
|
const taskId = args.task_id;
|
|
@@ -12872,6 +13424,29 @@ async function handleBoardDeprioritise(adapter2, args) {
|
|
|
12872
13424
|
const reason = args.reason;
|
|
12873
13425
|
const newPriority = args.priority;
|
|
12874
13426
|
const newPhase = args.phase;
|
|
13427
|
+
if (action === "block") {
|
|
13428
|
+
if (!reason) {
|
|
13429
|
+
return errorResponse("reason is required when blocking a task \u2014 explain what external dependency or gate is blocking it.");
|
|
13430
|
+
}
|
|
13431
|
+
try {
|
|
13432
|
+
const task = await adapter2.getTask(taskId);
|
|
13433
|
+
if (!task) return errorResponse(`Task ${taskId} not found.`);
|
|
13434
|
+
const existingNotes = task.notes ? `${task.notes}
|
|
13435
|
+
|
|
13436
|
+
` : "";
|
|
13437
|
+
await adapter2.updateTask(taskId, {
|
|
13438
|
+
status: "Blocked",
|
|
13439
|
+
notes: `${existingNotes}BLOCKED: ${reason}`
|
|
13440
|
+
});
|
|
13441
|
+
return textResponse(`Blocked **${taskId}** (${task.title}).
|
|
13442
|
+
|
|
13443
|
+
Reason: ${reason}
|
|
13444
|
+
|
|
13445
|
+
Task remains visible on the board but will be skipped by the planner and build_list.`);
|
|
13446
|
+
} catch (err) {
|
|
13447
|
+
return errorResponse(err instanceof Error ? err.message : String(err));
|
|
13448
|
+
}
|
|
13449
|
+
}
|
|
12875
13450
|
if (action === "cancel") {
|
|
12876
13451
|
if (!reason) {
|
|
12877
13452
|
return errorResponse("reason is required when cancelling a task.");
|
|
@@ -12930,11 +13505,49 @@ async function handleBoardArchive(adapter2, args) {
|
|
|
12930
13505
|
];
|
|
12931
13506
|
return textResponse(lines.join("\n"));
|
|
12932
13507
|
}
|
|
13508
|
+
var EDITABLE_FIELDS = ["title", "priority", "complexity", "module", "epic", "phase", "notes", "status", "maturity"];
|
|
13509
|
+
async function handleBoardEdit(adapter2, args) {
|
|
13510
|
+
const taskId = args.task_id;
|
|
13511
|
+
if (!taskId) {
|
|
13512
|
+
return errorResponse("task_id is required.");
|
|
13513
|
+
}
|
|
13514
|
+
const updates = {};
|
|
13515
|
+
const changes = [];
|
|
13516
|
+
for (const field of EDITABLE_FIELDS) {
|
|
13517
|
+
if (args[field] !== void 0 && args[field] !== null) {
|
|
13518
|
+
updates[field] = args[field];
|
|
13519
|
+
changes.push(field);
|
|
13520
|
+
}
|
|
13521
|
+
}
|
|
13522
|
+
if (changes.length === 0) {
|
|
13523
|
+
return errorResponse("No fields to update. Pass at least one field (title, priority, complexity, module, epic, phase, notes, status, maturity).");
|
|
13524
|
+
}
|
|
13525
|
+
try {
|
|
13526
|
+
const task = await adapter2.getTask(taskId);
|
|
13527
|
+
if (!task) {
|
|
13528
|
+
return errorResponse(`Task ${taskId} not found.`);
|
|
13529
|
+
}
|
|
13530
|
+
if (updates.status === "Backlog" && task.cycle != null) {
|
|
13531
|
+
updates.cycle = void 0;
|
|
13532
|
+
updates.cycle = null;
|
|
13533
|
+
if (!changes.includes("cycle")) changes.push("cycle \u2192 cleared");
|
|
13534
|
+
}
|
|
13535
|
+
await adapter2.updateTask(taskId, updates);
|
|
13536
|
+
const lines = [
|
|
13537
|
+
`Updated **${taskId}** (${updates.title ?? task.title})`,
|
|
13538
|
+
"",
|
|
13539
|
+
`**Changes:** ${changes.map((f) => `${f} \u2192 ${String(updates[f])}`).join(", ")}`
|
|
13540
|
+
];
|
|
13541
|
+
return textResponse(lines.join("\n"));
|
|
13542
|
+
} catch (err) {
|
|
13543
|
+
return errorResponse(err instanceof Error ? err.message : String(err));
|
|
13544
|
+
}
|
|
13545
|
+
}
|
|
12933
13546
|
|
|
12934
13547
|
// src/services/setup.ts
|
|
12935
13548
|
init_dist2();
|
|
12936
13549
|
import { mkdir, writeFile as writeFile2, readFile as readFile3, readdir, access as access2, stat } from "fs/promises";
|
|
12937
|
-
import { join as
|
|
13550
|
+
import { join as join3, basename, extname } from "path";
|
|
12938
13551
|
|
|
12939
13552
|
// src/templates.ts
|
|
12940
13553
|
var PLANNING_LOG_TEMPLATE = `# PAPI Planning Log
|
|
@@ -13088,15 +13701,15 @@ PAPI tools follow structured flows. The agent manages the cycle workflow automat
|
|
|
13088
13701
|
- **Run tools automatically** \u2014 don't ask the user to invoke MCP tools manually
|
|
13089
13702
|
- Before implementing: silently run \`build_execute <task_id>\` (start phase)
|
|
13090
13703
|
- After implementing: run \`build_execute <task_id>\` (complete phase) with report fields
|
|
13091
|
-
- After build_execute completes:
|
|
13092
|
-
- After
|
|
13093
|
-
- After
|
|
13704
|
+
- After build_execute completes: audit the branch changes for bugs, convention violations, and doc drift (see Post-Build Audit below)
|
|
13705
|
+
- After audit with findings: *MUST* automatically run \`review_submit\` with verdict \`request-changes\` and a concise summary of the audit findings as the changes requested \u2014 the builder fixes these before the task goes to human review
|
|
13706
|
+
- After audit clean: present for human review \u2014 "Ready for your review \u2014 approve or request changes?"
|
|
13094
13707
|
- User approves/requests changes \u2192 run \`review_submit\` behind the scenes
|
|
13095
13708
|
|
|
13096
13709
|
### The Cycle (main flow)
|
|
13097
13710
|
|
|
13098
13711
|
\`\`\`
|
|
13099
|
-
plan \u2192 build_list \u2192 build_execute \u2192
|
|
13712
|
+
plan \u2192 build_list \u2192 build_execute \u2192 audit \u2192 review_list \u2192 review_submit \u2192 build_list
|
|
13100
13713
|
\`\`\`
|
|
13101
13714
|
|
|
13102
13715
|
1. **plan** \u2014 Run at the start of each cycle to generate the cycle plan and populate the board.
|
|
@@ -13106,8 +13719,8 @@ plan \u2192 build_list \u2192 build_execute \u2192 /papi-audit \u2192 review_lis
|
|
|
13106
13719
|
3. **build_execute** (start) \u2014 Creates a feature branch and marks the task In Progress. Returns the build handoff.
|
|
13107
13720
|
Next: Implement the task, then \`build_execute <task_id>\` again with report fields to complete.
|
|
13108
13721
|
4. **build_execute** (complete) \u2014 Submits the build report, commits, and marks the task In Review.
|
|
13109
|
-
Next: Run
|
|
13110
|
-
5.
|
|
13722
|
+
Next: Run the post-build audit automatically.
|
|
13723
|
+
5. **Post-build audit** \u2014 Review branch changes for bugs, convention violations, and doc drift (see Post-Build Audit section below).
|
|
13111
13724
|
Next: If findings exist, run \`review_submit\` with \`request-changes\` and the audit findings. If clean, proceed to \`review_list\`.
|
|
13112
13725
|
6. **review_list** \u2014 Shows tasks pending human review (handoff-review or build-acceptance).
|
|
13113
13726
|
Next: \`review_submit\` to approve, accept, or request changes.
|
|
@@ -13156,15 +13769,99 @@ setup \u2192 plan
|
|
|
13156
13769
|
| \`plan\` | \`build_list\` |
|
|
13157
13770
|
| \`build_list\` | \`build_execute <task_id>\` |
|
|
13158
13771
|
| \`build_execute\` (start) | Implement, then \`build_execute\` (complete) |
|
|
13159
|
-
| \`build_execute\` (complete) |
|
|
13160
|
-
|
|
|
13161
|
-
|
|
|
13772
|
+
| \`build_execute\` (complete) | Post-build audit (automatic) |
|
|
13773
|
+
| Audit (findings) | \`review_submit\` with \`request-changes\` |
|
|
13774
|
+
| Audit (clean) | \`review_list\` |
|
|
13162
13775
|
| \`review_list\` | \`review_submit\` |
|
|
13163
13776
|
| \`review_submit\` (approve/accept) | \`build_list\` |
|
|
13164
13777
|
| \`review_submit\` (request-changes) | \`build_execute\` (redo) or \`build_list\` |
|
|
13165
13778
|
| \`strategy_review\` | \`strategy_change\` (if needed) |
|
|
13166
13779
|
| \`idea\` | Next \`plan\` picks it up |
|
|
13167
13780
|
|
|
13781
|
+
## Post-Build Audit
|
|
13782
|
+
|
|
13783
|
+
After every \`build_execute\` (complete), audit the branch before presenting for human review. This catches bugs and convention violations early.
|
|
13784
|
+
|
|
13785
|
+
1. **Identify changed files:** Run \`git diff origin/main --name-only\` to find modified files. If no changes, report "No changes to audit" and skip.
|
|
13786
|
+
2. **Review each changed file** for:
|
|
13787
|
+
- Logic errors, off-by-one mistakes, incorrect conditions
|
|
13788
|
+
- Unhandled edge cases (null, undefined, empty inputs)
|
|
13789
|
+
- Convention violations defined in this CLAUDE.md
|
|
13790
|
+
- Incorrect type narrowing or unsafe casts
|
|
13791
|
+
3. **Documentation check:** If any \`docs/\` files describe behaviour that the change modified, flag as "Doc drift".
|
|
13792
|
+
4. **Report:** For each issue: file path, severity (Bug/Convention/Doc drift), what's wrong, how to fix.
|
|
13793
|
+
5. **If findings exist:** Run \`review_submit\` with \`request-changes\` and the findings. Fix before human review.
|
|
13794
|
+
6. **If clean:** Present for human review \u2014 "Ready for your review \u2014 approve or request changes?"
|
|
13795
|
+
|
|
13796
|
+
## When to Start a New Conversation
|
|
13797
|
+
|
|
13798
|
+
Start a fresh window when:
|
|
13799
|
+
- **After a release** \u2014 cycle is done, context is heavy. New window orients in seconds via \`orient\`.
|
|
13800
|
+
- **After 3+ tasks built** \u2014 accumulated file reads, diffs, and discussions bloat context. Quality degrades.
|
|
13801
|
+
- **Switching modes** \u2014 going from building to planning, or from strategy review to building. Each mode benefits from clean context.
|
|
13802
|
+
- **After context compression fires** \u2014 if you notice earlier messages are missing, the window is getting stale. Open fresh.
|
|
13803
|
+
|
|
13804
|
+
Stay in the same window when:
|
|
13805
|
+
- Building sequential tasks in a batch (especially XS/S tasks)
|
|
13806
|
+
- Mid-task and not yet complete
|
|
13807
|
+
- Having a strategic discussion that informs the next action
|
|
13808
|
+
|
|
13809
|
+
**Rule of thumb:** If you've been in the same window for 30+ minutes or 3+ tasks, it's time for a fresh one.
|
|
13810
|
+
|
|
13811
|
+
## Housekeeping \u2014 Fix Orphaned Tasks
|
|
13812
|
+
|
|
13813
|
+
On session start, silently check for and fix:
|
|
13814
|
+
1. **Orphaned branches:** \`git branch | grep feat/task\` \u2014 cross-reference with board status. Fix by merging or flagging.
|
|
13815
|
+
2. **In Review tasks with no PR:** If branch is already merged to main, the review_submit step was missed.
|
|
13816
|
+
3. **Stale In Progress:** Branch has no recent commits \u2014 flag it.
|
|
13817
|
+
4. **Config mismatches:** \`.mcp.json\` has DATABASE_URL but PAPI_ADAPTER is still \`md\` \u2014 flag it.
|
|
13818
|
+
|
|
13819
|
+
**Do this automatically and silently.** Report what you found and fixed.
|
|
13820
|
+
|
|
13821
|
+
## Plumbing Is Autonomous
|
|
13822
|
+
|
|
13823
|
+
Board status updates, branch cleanup, orphaned task fixes, commit/PR/merge for housekeeping \u2014 these are mechanical plumbing. **Do them end-to-end without stopping to ask.** Report after the fact.
|
|
13824
|
+
|
|
13825
|
+
## Context Compression Recovery
|
|
13826
|
+
|
|
13827
|
+
When the system compresses prior messages, immediately:
|
|
13828
|
+
1. **Run \`orient\`** \u2014 single call for cycle state
|
|
13829
|
+
2. Check your todo list for in-progress work
|
|
13830
|
+
3. Run housekeeping checks
|
|
13831
|
+
4. **NEVER re-build a task that is already In Review or Done.**
|
|
13832
|
+
5. Continue where you left off \u2014 don't restart or re-plan
|
|
13833
|
+
|
|
13834
|
+
## Branching & PR Convention
|
|
13835
|
+
|
|
13836
|
+
- **XS/S tasks in the same cycle and module:** Group on shared branch. One PR, one merge.
|
|
13837
|
+
- **M/L tasks or different modules:** Own branch per task. Isolated PRs.
|
|
13838
|
+
- **Commit per task within grouped branches** \u2014 traceable git history.
|
|
13839
|
+
|
|
13840
|
+
## Quick Work vs PAPI Work
|
|
13841
|
+
|
|
13842
|
+
PAPI is for planned work. Quick fixes \u2014 just do them. No need for plan or build_execute.
|
|
13843
|
+
|
|
13844
|
+
**After completing quick/ad-hoc work** (bug fixes, config changes, small improvements done outside the cycle), call \`ad_hoc\` to record it. This creates a Done task + build report so the work appears in cycle history and metrics. Don't skip this \u2014 unrecorded work is invisible work.
|
|
13845
|
+
|
|
13846
|
+
## Data Integrity
|
|
13847
|
+
|
|
13848
|
+
- **Use MCP tools for all project data operations.** DB is the source of truth when using the pg adapter.
|
|
13849
|
+
- Do NOT read \`.papi/\` files for context \u2014 use MCP tools.
|
|
13850
|
+
- \`.papi/\` files may be stale when using pg adapter. This is expected.
|
|
13851
|
+
|
|
13852
|
+
## Code Before Claims \u2014 No Assumptions
|
|
13853
|
+
|
|
13854
|
+
**Before making any claim about how the codebase works, read the relevant file first.**
|
|
13855
|
+
|
|
13856
|
+
This includes:
|
|
13857
|
+
- How a feature is implemented ("it works like X") \u2192 read the source
|
|
13858
|
+
- Whether something exists ("there's no baseline migration") \u2192 check the directory
|
|
13859
|
+
- Whether a flow is broken or working \u2192 trace it in code
|
|
13860
|
+
- What a user would experience \u2192 check the actual page/component
|
|
13861
|
+
|
|
13862
|
+
Do NOT rely on memory, prior conversation, or inference. Read first, then answer.
|
|
13863
|
+
If the answer requires checking 2-3 files, check them all before responding.
|
|
13864
|
+
|
|
13168
13865
|
## Process Rules
|
|
13169
13866
|
|
|
13170
13867
|
These rules come from 80+ cycles of dogfooding. They prevent the most common sources of wasted time and rework.
|
|
@@ -13177,6 +13874,11 @@ These rules come from 80+ cycles of dogfooding. They prevent the most common sou
|
|
|
13177
13874
|
- **Test after every build.** Run the project's test suite after implementing. Suggest follow-up tasks from learnings when meaningful.
|
|
13178
13875
|
- **Build patiently.** Validate each phase against the last. Don't rush through implementation \u2014 test through the UI, not just the API.
|
|
13179
13876
|
|
|
13877
|
+
### Security
|
|
13878
|
+
- **Audit before widening access.** Before any build that adds endpoints, modifies auth/RLS, introduces new user types, or changes access controls \u2014 review the security implications first. Fix findings before shipping.
|
|
13879
|
+
- **Flag access-widening changes.** If a build touches auth, RLS policies, API keys, or user-facing access, note "Security surface reviewed" in the build report's \`discovered_issues\` or \`architecture_notes\`.
|
|
13880
|
+
- **Never ship secrets.** Do not commit .env files, API keys, or credentials. Check \`.gitignore\` covers sensitive files before pushing.
|
|
13881
|
+
|
|
13180
13882
|
### Planning & Scope
|
|
13181
13883
|
- **Don't ask premature questions.** If the project is in early cycles, don't ask about deployment accounts, hosting providers, OAuth setup, or commercial features. Focus on building core functionality first.
|
|
13182
13884
|
- **Split large ideas.** If an idea has 3+ concerns, submit it as 2-3 separate ideas so the planner creates properly scoped tasks \u2014 not kitchen-sink handoffs.
|
|
@@ -13185,12 +13887,73 @@ These rules come from 80+ cycles of dogfooding. They prevent the most common sou
|
|
|
13185
13887
|
### Communication
|
|
13186
13888
|
- **Show task names, not just IDs.** When summarising board state or reconciliation, include task names \u2014 e.g. "task-42: Add supplier form" not just "task-42".
|
|
13187
13889
|
- **Surface the next command.** After each step, tell the user what comes next. Commands should be surfaced, not memorised.
|
|
13890
|
+
|
|
13891
|
+
### Stage Readiness
|
|
13892
|
+
- **Access-widening stages require auth/security phases.** Before declaring a stage complete, check if it widens who can access the product (e.g. Alpha Distribution, Alpha Cohort). If so, auth hardening and security review must be completed first \u2014 not discovered after the fact.
|
|
13893
|
+
- **Pattern:** Audit access surface \u2192 fix vulnerabilities \u2192 then widen access. Never ship access-widening without a security phase.
|
|
13894
|
+
`;
|
|
13895
|
+
var CLAUDE_MD_ENRICHMENT_SENTINEL_T1 = "<!-- PAPI_ENRICHMENT_TIER_1 -->";
|
|
13896
|
+
var CLAUDE_MD_ENRICHMENT_SENTINEL_T2 = "<!-- PAPI_ENRICHMENT_TIER_2 -->";
|
|
13897
|
+
var CLAUDE_MD_TIER_1 = `
|
|
13898
|
+
${CLAUDE_MD_ENRICHMENT_SENTINEL_T1}
|
|
13899
|
+
|
|
13900
|
+
## Batch Building (unlocked at cycle 6)
|
|
13901
|
+
|
|
13902
|
+
For cycles with multiple XS/S tasks, batch build them without stopping between each:
|
|
13903
|
+
- Build all XS/S tasks first, then M/L tasks
|
|
13904
|
+
- Group tasks touching the same module onto a shared branch where possible
|
|
13905
|
+
- One commit per task for traceable history, even on shared branches
|
|
13906
|
+
- After all tasks built, batch review them together
|
|
13907
|
+
|
|
13908
|
+
## Strategy Reviews
|
|
13909
|
+
|
|
13910
|
+
Every 5 cycles, PAPI offers a strategy review \u2014 a deep analysis of velocity, estimation accuracy, active decisions, and project direction.
|
|
13911
|
+
|
|
13912
|
+
- **Don't skip them.** They're where compounding value comes from.
|
|
13913
|
+
- Strategy reviews run in their own session \u2014 don't mix with building.
|
|
13914
|
+
- Reviews produce recommendations that feed into the next plan.
|
|
13915
|
+
- If the review recommends AD changes, use \`strategy_change\` to apply them.
|
|
13916
|
+
|
|
13917
|
+
## Active Decision Lifecycle
|
|
13918
|
+
|
|
13919
|
+
Active Decisions (ADs) track architectural and product choices with confidence levels (LOW \u2192 MEDIUM \u2192 HIGH).
|
|
13920
|
+
|
|
13921
|
+
- Check ADs before making architectural choices \u2014 run \`health\` for the AD summary.
|
|
13922
|
+
- ADs are for product/architecture choices only, not process preferences.
|
|
13923
|
+
- When new evidence appears, update AD confidence via \`strategy_change\`.
|
|
13924
|
+
- Supersede rather than overwrite \u2014 old decisions stay as history.
|
|
13925
|
+
`;
|
|
13926
|
+
var CLAUDE_MD_TIER_2 = `
|
|
13927
|
+
${CLAUDE_MD_ENRICHMENT_SENTINEL_T2}
|
|
13928
|
+
|
|
13929
|
+
## Idea Pipeline (unlocked at cycle 21)
|
|
13930
|
+
|
|
13931
|
+
The \`idea\` tool is your backlog intake \u2014 not just for features, but bugs, research, and big ideas.
|
|
13932
|
+
|
|
13933
|
+
- When you discover something during a build, submit it via \`idea\` rather than stopping to fix it.
|
|
13934
|
+
- Include a \`Reference:\` line pointing to relevant docs so the planner has context.
|
|
13935
|
+
- Split large ideas into 2-3 focused submissions for better planner scoping.
|
|
13936
|
+
- The backlog is the steering wheel \u2014 priority + notes shape what gets planned next.
|
|
13937
|
+
|
|
13938
|
+
## Doc Registry
|
|
13939
|
+
|
|
13940
|
+
Docs are first-class entities. When research or planning produces a stable document:
|
|
13941
|
+
- Register it with \`doc_register\` after it's finalised.
|
|
13942
|
+
- Doc summaries travel with tool context \u2014 the planner and strategy review can find relevant docs.
|
|
13943
|
+
- Keep docs current \u2014 update the review header after any change.
|
|
13944
|
+
|
|
13945
|
+
## Advanced Patterns
|
|
13946
|
+
|
|
13947
|
+
- **Cross-project awareness:** If running multiple PAPI projects, learnings transfer across them via shared patterns and the doc registry.
|
|
13948
|
+
- **Dogfood friction:** When something feels painful in the workflow, note it \u2014 the \`idea\` tool turns friction into improvements.
|
|
13949
|
+
- **Deferred tasks are intentional:** Tasks moved to Deferred aren't forgotten \u2014 they're parked for the right time.
|
|
13950
|
+
- **Carry-forward items:** Each plan notes carry-forward from the previous cycle. Check them before planning.
|
|
13188
13951
|
`;
|
|
13189
13952
|
var PAPI_AUDIT_COMMAND_TEMPLATE = `Audit the latest changes in this branch for bugs and compliance with the project's conventions defined in CLAUDE.md.
|
|
13190
13953
|
|
|
13191
13954
|
## Steps
|
|
13192
13955
|
|
|
13193
|
-
1. **Identify changed files**: Run \`git diff
|
|
13956
|
+
1. **Identify changed files**: Run \`git diff main --name-only\` to find all files modified on this branch.
|
|
13194
13957
|
|
|
13195
13958
|
2. **Read each changed file** and review it for:
|
|
13196
13959
|
|
|
@@ -13307,22 +14070,22 @@ async function scaffoldPapiDir(adapter2, config2, input) {
|
|
|
13307
14070
|
await mkdir(config2.papiDir, { recursive: true });
|
|
13308
14071
|
for (const [filename, template] of Object.entries(FILE_TEMPLATES)) {
|
|
13309
14072
|
const content = substitute(template, vars);
|
|
13310
|
-
await writeFile2(
|
|
14073
|
+
await writeFile2(join3(config2.papiDir, filename), content, "utf-8");
|
|
13311
14074
|
}
|
|
13312
14075
|
}
|
|
13313
14076
|
}
|
|
13314
|
-
const commandsDir =
|
|
13315
|
-
const docsDir =
|
|
14077
|
+
const commandsDir = join3(config2.projectRoot, ".claude", "commands");
|
|
14078
|
+
const docsDir = join3(config2.projectRoot, "docs");
|
|
13316
14079
|
await mkdir(commandsDir, { recursive: true });
|
|
13317
14080
|
await mkdir(docsDir, { recursive: true });
|
|
13318
|
-
const claudeMdPath =
|
|
14081
|
+
const claudeMdPath = join3(config2.projectRoot, "CLAUDE.md");
|
|
13319
14082
|
let claudeMdExists = false;
|
|
13320
14083
|
try {
|
|
13321
14084
|
await access2(claudeMdPath);
|
|
13322
14085
|
claudeMdExists = true;
|
|
13323
14086
|
} catch {
|
|
13324
14087
|
}
|
|
13325
|
-
const docsIndexPath =
|
|
14088
|
+
const docsIndexPath = join3(docsDir, "INDEX.md");
|
|
13326
14089
|
let docsIndexExists = false;
|
|
13327
14090
|
try {
|
|
13328
14091
|
await access2(docsIndexPath);
|
|
@@ -13330,9 +14093,9 @@ async function scaffoldPapiDir(adapter2, config2, input) {
|
|
|
13330
14093
|
} catch {
|
|
13331
14094
|
}
|
|
13332
14095
|
const scaffoldFiles = {
|
|
13333
|
-
[
|
|
13334
|
-
[
|
|
13335
|
-
[
|
|
14096
|
+
[join3(commandsDir, "papi-audit.md")]: PAPI_AUDIT_COMMAND_TEMPLATE,
|
|
14097
|
+
[join3(commandsDir, "test.md")]: TEST_COMMAND_TEMPLATE,
|
|
14098
|
+
[join3(docsDir, "README.md")]: substitute(DOCS_README_TEMPLATE, vars)
|
|
13336
14099
|
};
|
|
13337
14100
|
if (!docsIndexExists) {
|
|
13338
14101
|
scaffoldFiles[docsIndexPath] = substitute(DOCS_INDEX_TEMPLATE, vars);
|
|
@@ -13367,7 +14130,7 @@ async function scaffoldPapiDir(adapter2, config2, input) {
|
|
|
13367
14130
|
}
|
|
13368
14131
|
var PAPI_PERMISSION = "mcp__papi__*";
|
|
13369
14132
|
async function ensurePapiPermission(projectRoot) {
|
|
13370
|
-
const settingsPath =
|
|
14133
|
+
const settingsPath = join3(projectRoot, ".claude", "settings.json");
|
|
13371
14134
|
try {
|
|
13372
14135
|
let settings = {};
|
|
13373
14136
|
try {
|
|
@@ -13386,14 +14149,14 @@ async function ensurePapiPermission(projectRoot) {
|
|
|
13386
14149
|
if (!allow.includes(PAPI_PERMISSION)) {
|
|
13387
14150
|
allow.push(PAPI_PERMISSION);
|
|
13388
14151
|
}
|
|
13389
|
-
await mkdir(
|
|
14152
|
+
await mkdir(join3(projectRoot, ".claude"), { recursive: true });
|
|
13390
14153
|
await writeFile2(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
13391
14154
|
} catch {
|
|
13392
14155
|
}
|
|
13393
14156
|
}
|
|
13394
14157
|
async function applySetupOutputs(adapter2, config2, input, briefText, adSeedText, conventionsText) {
|
|
13395
14158
|
if (config2.adapterType !== "pg") {
|
|
13396
|
-
await writeFile2(
|
|
14159
|
+
await writeFile2(join3(config2.papiDir, "PRODUCT_BRIEF.md"), briefText, "utf-8");
|
|
13397
14160
|
}
|
|
13398
14161
|
await adapter2.updateProductBrief(briefText);
|
|
13399
14162
|
const briefPhases = parsePhases(briefText);
|
|
@@ -13437,7 +14200,7 @@ async function applySetupOutputs(adapter2, config2, input, briefText, adSeedText
|
|
|
13437
14200
|
}
|
|
13438
14201
|
if (conventionsText?.trim()) {
|
|
13439
14202
|
try {
|
|
13440
|
-
const claudeMdPath =
|
|
14203
|
+
const claudeMdPath = join3(config2.projectRoot, "CLAUDE.md");
|
|
13441
14204
|
const existing = await readFile3(claudeMdPath, "utf-8");
|
|
13442
14205
|
await writeFile2(claudeMdPath, existing + "\n" + conventionsText.trim() + "\n", "utf-8");
|
|
13443
14206
|
} catch {
|
|
@@ -13505,13 +14268,13 @@ async function scanCodebase(projectRoot) {
|
|
|
13505
14268
|
}
|
|
13506
14269
|
let packageJson;
|
|
13507
14270
|
try {
|
|
13508
|
-
const content = await readFile3(
|
|
14271
|
+
const content = await readFile3(join3(projectRoot, "package.json"), "utf-8");
|
|
13509
14272
|
packageJson = JSON.parse(content);
|
|
13510
14273
|
} catch {
|
|
13511
14274
|
}
|
|
13512
14275
|
let readme;
|
|
13513
14276
|
for (const name of ["README.md", "readme.md", "README.txt", "README"]) {
|
|
13514
|
-
const content = await safeReadFile(
|
|
14277
|
+
const content = await safeReadFile(join3(projectRoot, name), 5e3);
|
|
13515
14278
|
if (content) {
|
|
13516
14279
|
readme = content;
|
|
13517
14280
|
break;
|
|
@@ -13521,7 +14284,7 @@ async function scanCodebase(projectRoot) {
|
|
|
13521
14284
|
let totalFiles = topLevelFiles.length;
|
|
13522
14285
|
for (const dir of topLevelDirs) {
|
|
13523
14286
|
try {
|
|
13524
|
-
const entries = await readdir(
|
|
14287
|
+
const entries = await readdir(join3(projectRoot, dir), { withFileTypes: true });
|
|
13525
14288
|
const files = entries.filter((e) => e.isFile());
|
|
13526
14289
|
const extensions = [...new Set(files.map((f) => extname(f.name).toLowerCase()).filter(Boolean))];
|
|
13527
14290
|
totalFiles += files.length;
|
|
@@ -13707,7 +14470,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
|
|
|
13707
14470
|
}
|
|
13708
14471
|
}
|
|
13709
14472
|
try {
|
|
13710
|
-
const claudeMdPath =
|
|
14473
|
+
const claudeMdPath = join3(config2.projectRoot, "CLAUDE.md");
|
|
13711
14474
|
const existing = await readFile3(claudeMdPath, "utf-8");
|
|
13712
14475
|
if (!existing.includes("Dogfood Logging")) {
|
|
13713
14476
|
const dogfoodSection = [
|
|
@@ -14008,6 +14771,8 @@ init_dist2();
|
|
|
14008
14771
|
|
|
14009
14772
|
// src/services/build.ts
|
|
14010
14773
|
import { randomUUID as randomUUID9 } from "crypto";
|
|
14774
|
+
import { readdirSync as readdirSync2, existsSync as existsSync2 } from "fs";
|
|
14775
|
+
import { join as join4 } from "path";
|
|
14011
14776
|
function capitalizeCompleted(value) {
|
|
14012
14777
|
const map = {
|
|
14013
14778
|
yes: "Yes",
|
|
@@ -14016,25 +14781,11 @@ function capitalizeCompleted(value) {
|
|
|
14016
14781
|
};
|
|
14017
14782
|
return map[value] ?? "No";
|
|
14018
14783
|
}
|
|
14019
|
-
function formatDate(date) {
|
|
14020
|
-
return date.toISOString();
|
|
14021
|
-
}
|
|
14022
14784
|
function autoCommit(config2, taskId, taskTitle) {
|
|
14023
|
-
|
|
14024
|
-
|
|
14025
|
-
|
|
14026
|
-
|
|
14027
|
-
return "Auto-commit: skipped (not a git repository).";
|
|
14028
|
-
}
|
|
14029
|
-
try {
|
|
14030
|
-
const result = stageAllAndCommit(
|
|
14031
|
-
config2.projectRoot,
|
|
14032
|
-
`feat(${taskId}): ${taskTitle}`
|
|
14033
|
-
);
|
|
14034
|
-
return result.committed ? `Auto-committed: ${result.message}` : `Auto-commit: ${result.message}`;
|
|
14035
|
-
} catch (err) {
|
|
14036
|
-
return `Auto-commit failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
14037
|
-
}
|
|
14785
|
+
return runAutoCommit(
|
|
14786
|
+
config2.projectRoot,
|
|
14787
|
+
() => stageAllAndCommit(config2.projectRoot, `feat(${taskId}): ${taskTitle}`)
|
|
14788
|
+
);
|
|
14038
14789
|
}
|
|
14039
14790
|
function pushAndCreatePR(config2, taskId, taskTitle) {
|
|
14040
14791
|
const lines = [];
|
|
@@ -14239,20 +14990,19 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
14239
14990
|
if (!task) {
|
|
14240
14991
|
throw new Error(`Task "${taskId}" not found on the Cycle Board.`);
|
|
14241
14992
|
}
|
|
14242
|
-
|
|
14243
|
-
|
|
14244
|
-
|
|
14245
|
-
|
|
14246
|
-
|
|
14247
|
-
|
|
14248
|
-
}
|
|
14993
|
+
const [healthResult, priorCount] = await Promise.all([
|
|
14994
|
+
adapter2.getCycleHealth().catch(() => ({ totalCycles: 0 })),
|
|
14995
|
+
adapter2.getBuildReportCountForTask(taskId).catch(() => 0)
|
|
14996
|
+
]);
|
|
14997
|
+
const cycleNumber = healthResult.totalCycles;
|
|
14998
|
+
const iterationCount = priorCount + 1;
|
|
14249
14999
|
const now = /* @__PURE__ */ new Date();
|
|
14250
15000
|
const report = {
|
|
14251
15001
|
uuid: randomUUID9(),
|
|
14252
15002
|
createdAt: now.toISOString(),
|
|
14253
15003
|
taskId: task.id,
|
|
14254
15004
|
taskName: task.title,
|
|
14255
|
-
date:
|
|
15005
|
+
date: now.toISOString(),
|
|
14256
15006
|
cycle: cycleNumber,
|
|
14257
15007
|
completed: capitalizeCompleted(input.completed),
|
|
14258
15008
|
actualEffort: input.effort,
|
|
@@ -14264,7 +15014,8 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
14264
15014
|
handoffAccuracy: input.handoffAccuracy,
|
|
14265
15015
|
correctionsCount: input.correctionsCount,
|
|
14266
15016
|
briefImplications: input.briefImplications,
|
|
14267
|
-
deadEnds: input.deadEnds
|
|
15017
|
+
deadEnds: input.deadEnds,
|
|
15018
|
+
iterationCount
|
|
14268
15019
|
};
|
|
14269
15020
|
if (input.relatedDecisions) {
|
|
14270
15021
|
const adIds = input.relatedDecisions.split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -14291,7 +15042,8 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
14291
15042
|
}
|
|
14292
15043
|
const surpriseNote = input.surprises === "None" ? "" : ` Surprises: ${input.surprises}.`;
|
|
14293
15044
|
const issueNote = input.discoveredIssues === "None" ? "" : ` Issues: ${input.discoveredIssues}.`;
|
|
14294
|
-
const
|
|
15045
|
+
const iterNote = iterationCount > 1 ? ` Iterations: ${iterationCount} (${iterationCount - 1} pushback${iterationCount > 2 ? "s" : ""}).` : "";
|
|
15046
|
+
const buildReportSummary = `${capitalizeCompleted(input.completed)}. Effort ${input.effort} vs estimated ${input.estimatedEffort}.${iterNote}${surpriseNote}${issueNote}`;
|
|
14295
15047
|
await adapter2.updateTask(taskId, { buildReport: buildReportSummary });
|
|
14296
15048
|
if (input.completed === "yes") {
|
|
14297
15049
|
if (options.light) {
|
|
@@ -14332,6 +15084,32 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
14332
15084
|
phaseChanges = await propagatePhaseStatus(adapter2);
|
|
14333
15085
|
} catch {
|
|
14334
15086
|
}
|
|
15087
|
+
let docWarning;
|
|
15088
|
+
try {
|
|
15089
|
+
if (adapter2.searchDocs) {
|
|
15090
|
+
const docsDir = join4(config2.projectRoot, "docs");
|
|
15091
|
+
if (existsSync2(docsDir)) {
|
|
15092
|
+
const scanDir = (dir) => {
|
|
15093
|
+
const entries = readdirSync2(dir, { withFileTypes: true });
|
|
15094
|
+
const files = [];
|
|
15095
|
+
for (const e of entries) {
|
|
15096
|
+
const full = join4(dir, e.name);
|
|
15097
|
+
if (e.isDirectory()) files.push(...scanDir(full));
|
|
15098
|
+
else if (e.name.endsWith(".md")) files.push(full.replace(config2.projectRoot + "/", ""));
|
|
15099
|
+
}
|
|
15100
|
+
return files;
|
|
15101
|
+
};
|
|
15102
|
+
const mdFiles = scanDir(docsDir);
|
|
15103
|
+
const registered = await adapter2.searchDocs({ status: "active", limit: 500 });
|
|
15104
|
+
const registeredPaths = new Set(registered.map((d) => d.path));
|
|
15105
|
+
const unregistered = mdFiles.filter((f) => !registeredPaths.has(f));
|
|
15106
|
+
if (unregistered.length > 0) {
|
|
15107
|
+
docWarning = `${unregistered.length} unregistered doc(s) in docs/ \u2014 consider running \`doc_register\` for: ${unregistered.slice(0, 5).join(", ")}${unregistered.length > 5 ? ` (+${unregistered.length - 5} more)` : ""}`;
|
|
15108
|
+
}
|
|
15109
|
+
}
|
|
15110
|
+
}
|
|
15111
|
+
} catch {
|
|
15112
|
+
}
|
|
14335
15113
|
return {
|
|
14336
15114
|
task,
|
|
14337
15115
|
report,
|
|
@@ -14343,7 +15121,8 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
14343
15121
|
discoveredIssues: input.discoveredIssues,
|
|
14344
15122
|
completed: input.completed,
|
|
14345
15123
|
scopeAccuracy: input.scopeAccuracy,
|
|
14346
|
-
phaseChanges
|
|
15124
|
+
phaseChanges,
|
|
15125
|
+
docWarning
|
|
14347
15126
|
};
|
|
14348
15127
|
}
|
|
14349
15128
|
async function cancelBuild(adapter2, taskId, reason) {
|
|
@@ -14384,7 +15163,7 @@ var buildDescribeTool = {
|
|
|
14384
15163
|
};
|
|
14385
15164
|
var buildExecuteTool = {
|
|
14386
15165
|
name: "build_execute",
|
|
14387
|
-
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. Does not call the Anthropic API.",
|
|
15166
|
+
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. 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.",
|
|
14388
15167
|
inputSchema: {
|
|
14389
15168
|
type: "object",
|
|
14390
15169
|
properties: {
|
|
@@ -14448,11 +15227,11 @@ var buildExecuteTool = {
|
|
|
14448
15227
|
},
|
|
14449
15228
|
dead_ends: {
|
|
14450
15229
|
type: "string",
|
|
14451
|
-
description:
|
|
15230
|
+
description: `Approaches tried and rejected during the build, with WHY they failed. Example: "Tried using Supabase realtime subscriptions but Edge Functions can't hold persistent connections \u2014 switched to polling." Include whenever you abandoned an approach. Future builds and plans reference this to avoid repeating blind alleys.`
|
|
14452
15231
|
},
|
|
14453
15232
|
brief_implications: {
|
|
14454
15233
|
type: "array",
|
|
14455
|
-
description: "
|
|
15234
|
+
description: "Strategic learnings discovered during this build that the planner and strategy review should know about. Include when a build reveals: (1) something about assumptions that were wrong, (2) competitive/landscape insights, (3) user journey friction discovered during implementation, (4) MVP boundary implications (something that must/must-not be in v1), or (5) new success signal data. Each entry feeds into the Discovery Canvas and informs future planning.",
|
|
14456
15235
|
items: {
|
|
14457
15236
|
type: "object",
|
|
14458
15237
|
properties: {
|
|
@@ -14488,6 +15267,30 @@ function formatListItem(task) {
|
|
|
14488
15267
|
return `- **${task.id}:** ${task.title}
|
|
14489
15268
|
Status: ${task.status} | Priority: ${task.priority} | Complexity: ${task.complexity}`;
|
|
14490
15269
|
}
|
|
15270
|
+
function filterRelevantADs(ads, task) {
|
|
15271
|
+
const keywords = [];
|
|
15272
|
+
if (task.module) keywords.push(task.module.toLowerCase());
|
|
15273
|
+
if (task.epic) keywords.push(task.epic.toLowerCase());
|
|
15274
|
+
if (task.phase) keywords.push(task.phase.toLowerCase());
|
|
15275
|
+
const titleWords = task.title.toLowerCase().split(/\s+/).filter((w) => w.length > 3);
|
|
15276
|
+
keywords.push(...titleWords);
|
|
15277
|
+
if (keywords.length === 0) return [];
|
|
15278
|
+
return ads.filter((ad) => {
|
|
15279
|
+
if (ad.superseded) return false;
|
|
15280
|
+
const text = `${ad.title} ${ad.body}`.toLowerCase();
|
|
15281
|
+
return keywords.some((kw) => text.includes(kw));
|
|
15282
|
+
});
|
|
15283
|
+
}
|
|
15284
|
+
function formatRelevantADs(ads) {
|
|
15285
|
+
if (ads.length === 0) return "";
|
|
15286
|
+
const lines = ["\n\n---\n\n**ACTIVE DECISIONS (relevant):**"];
|
|
15287
|
+
for (const ad of ads) {
|
|
15288
|
+
const bodyLines = ad.body.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
|
15289
|
+
const summary = bodyLines[0]?.trim().slice(0, 120) ?? "";
|
|
15290
|
+
lines.push(`- **${ad.displayId}: ${ad.title}** [${ad.confidence}] \u2014 ${summary}`);
|
|
15291
|
+
}
|
|
15292
|
+
return lines.join("\n");
|
|
15293
|
+
}
|
|
14491
15294
|
function hasReportFields(args) {
|
|
14492
15295
|
return !!(args.completed || args.effort || args.estimated_effort || args.surprises || args.discovered_issues || args.architecture_notes);
|
|
14493
15296
|
}
|
|
@@ -14525,6 +15328,15 @@ async function handleBuildList(adapter2, config2) {
|
|
|
14525
15328
|
Waiting on: ${unresolvedDeps.join(", ")}`);
|
|
14526
15329
|
}
|
|
14527
15330
|
}
|
|
15331
|
+
try {
|
|
15332
|
+
const comments = await adapter2.getRecentTaskComments?.(30);
|
|
15333
|
+
if (comments && comments.length > 0) {
|
|
15334
|
+
const taskIds = new Set([...result.sorted, ...result.blocked.map((b2) => b2.task)].map((t) => t.id));
|
|
15335
|
+
const section = formatTaskComments(comments, taskIds);
|
|
15336
|
+
if (section) lines.push(section);
|
|
15337
|
+
}
|
|
15338
|
+
} catch {
|
|
15339
|
+
}
|
|
14528
15340
|
return textResponse(lines.join("\n"));
|
|
14529
15341
|
}
|
|
14530
15342
|
async function handleBuildDescribe(adapter2, args) {
|
|
@@ -14550,7 +15362,7 @@ async function handleBuildExecute(adapter2, config2, args) {
|
|
|
14550
15362
|
if (!taskId) {
|
|
14551
15363
|
return errorResponse("task_id is required.");
|
|
14552
15364
|
}
|
|
14553
|
-
const light = args.light === true;
|
|
15365
|
+
const light = args.light === true || config2.lightMode;
|
|
14554
15366
|
if (hasReportFields(args)) {
|
|
14555
15367
|
return handleExecuteComplete(adapter2, config2, taskId, args, light);
|
|
14556
15368
|
}
|
|
@@ -14563,8 +15375,23 @@ async function handleBuildExecute(adapter2, config2, args) {
|
|
|
14563
15375
|
---
|
|
14564
15376
|
|
|
14565
15377
|
`;
|
|
15378
|
+
const verificationFiles = result.task.buildHandoff.verificationFiles ?? [];
|
|
15379
|
+
const verificationNote = verificationFiles.length > 0 ? `
|
|
15380
|
+
|
|
15381
|
+
---
|
|
15382
|
+
|
|
15383
|
+
**PRE-BUILD VERIFICATION:** Before writing any code, read these files and check if the functionality already exists:
|
|
15384
|
+
${verificationFiles.map((f) => `- ${f}`).join("\n")}
|
|
15385
|
+
If >80% of the scope is already implemented, call \`build_execute\` with completed="yes" and note "already built" in surprises instead of re-implementing.` : "";
|
|
14566
15386
|
const chainInstruction = "\n\n---\n\n**IMPORTANT:** After implementing this task, immediately call `build_execute` again with report fields (`completed`, `effort`, `estimated_effort`, `surprises`, `discovered_issues`, `architecture_notes`) to complete the build. Do not wait for user confirmation.";
|
|
14567
|
-
|
|
15387
|
+
let adSection = "";
|
|
15388
|
+
try {
|
|
15389
|
+
const allADs = await adapter2.getActiveDecisions();
|
|
15390
|
+
const relevant = filterRelevantADs(allADs, result.task);
|
|
15391
|
+
adSection = formatRelevantADs(relevant);
|
|
15392
|
+
} catch {
|
|
15393
|
+
}
|
|
15394
|
+
return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + adSection + verificationNote + chainInstruction + phaseNote);
|
|
14568
15395
|
} catch (err) {
|
|
14569
15396
|
if (isNoHandoffError(err)) {
|
|
14570
15397
|
const lines = [
|
|
@@ -14671,6 +15498,9 @@ function formatCompleteResult(result) {
|
|
|
14671
15498
|
lines.push(`Phase auto-updated: ${c.phaseId} ${c.oldStatus} \u2192 ${c.newStatus}`);
|
|
14672
15499
|
}
|
|
14673
15500
|
}
|
|
15501
|
+
if (result.docWarning) {
|
|
15502
|
+
lines.push("", `\u{1F4C4} ${result.docWarning}`);
|
|
15503
|
+
}
|
|
14674
15504
|
const hasDiscoveredIssues = result.discoveredIssues !== "None" && result.discoveredIssues.trim() !== "";
|
|
14675
15505
|
const remaining = result.cycleProgress.total - result.cycleProgress.completed;
|
|
14676
15506
|
if (result.completed !== "yes") {
|
|
@@ -14919,36 +15749,28 @@ async function captureIdea(adapter2, input) {
|
|
|
14919
15749
|
return routeToDiscovery(adapter2, routing, input);
|
|
14920
15750
|
}
|
|
14921
15751
|
}
|
|
14922
|
-
|
|
14923
|
-
|
|
14924
|
-
|
|
14925
|
-
|
|
14926
|
-
|
|
14927
|
-
if (highOverlap.length > 0 && !input.force) {
|
|
14928
|
-
const lines = highOverlap.map(
|
|
15752
|
+
if (!input.force) {
|
|
15753
|
+
try {
|
|
15754
|
+
const similar = await findSimilarTasks(adapter2, input.text);
|
|
15755
|
+
if (similar.length > 0) {
|
|
15756
|
+
const lines = similar.map(
|
|
14929
15757
|
(s) => ` - **${s.id}** [${s.status}]: "${s.title}" (${Math.round(s.coverage * 100)}% keyword overlap)`
|
|
14930
15758
|
);
|
|
14931
|
-
const
|
|
14932
|
-
const
|
|
15759
|
+
const highOverlap = similar.filter((s) => s.coverage >= 0.7);
|
|
15760
|
+
const doneMatch = similar.find((s) => s.status === "Done");
|
|
15761
|
+
const reason = doneMatch ? `This looks like it was **already done** as ${doneMatch.id}.` : highOverlap.length > 0 ? `This looks like a **duplicate** of ${highOverlap[0].id}.` : `Similar tasks already exist on the board.`;
|
|
14933
15762
|
return {
|
|
14934
15763
|
routing: "task",
|
|
14935
|
-
message: `\
|
|
15764
|
+
message: `\u26A0\uFE0F **Paused \u2014 ${reason}**
|
|
14936
15765
|
|
|
14937
|
-
|
|
15766
|
+
Similar tasks found:
|
|
14938
15767
|
${lines.join("\n")}
|
|
14939
15768
|
|
|
14940
|
-
If this is genuinely different, re-run with \`force: true
|
|
15769
|
+
**STOP: Ask the user whether to proceed.** Explain the overlap and let them decide. If the user confirms this is genuinely different, re-run with \`force: true\`. Do NOT proceed without user confirmation.`
|
|
14941
15770
|
};
|
|
14942
15771
|
}
|
|
14943
|
-
|
|
14944
|
-
(s) => ` - **${s.id}** [${s.status}]: "${s.title}" (${Math.round(s.coverage * 100)}% keyword overlap)`
|
|
14945
|
-
);
|
|
14946
|
-
similarWarning = `
|
|
14947
|
-
|
|
14948
|
-
\u26A0\uFE0F **Similar tasks found** \u2014 check before scheduling:
|
|
14949
|
-
${warnLines.join("\n")}`;
|
|
15772
|
+
} catch {
|
|
14950
15773
|
}
|
|
14951
|
-
} catch {
|
|
14952
15774
|
}
|
|
14953
15775
|
const [health, phases] = await Promise.all([
|
|
14954
15776
|
adapter2.getCycleHealth(),
|
|
@@ -14956,13 +15778,17 @@ ${warnLines.join("\n")}`;
|
|
|
14956
15778
|
]);
|
|
14957
15779
|
warnIfEmpty("getCycleHealth (idea)", health);
|
|
14958
15780
|
const phase = input.phase || resolveCurrentPhase(phases);
|
|
15781
|
+
const VALID_PRIORITIES2 = /* @__PURE__ */ new Set(["P0 Critical", "P1 High", "P2 Medium", "P3 Low"]);
|
|
15782
|
+
const VALID_COMPLEXITIES2 = /* @__PURE__ */ new Set(["XS", "Small", "Medium", "Large", "XL"]);
|
|
15783
|
+
const priority = input.priority && VALID_PRIORITIES2.has(input.priority) ? input.priority : "P2 Medium";
|
|
15784
|
+
const complexity = input.complexity && VALID_COMPLEXITIES2.has(input.complexity) ? input.complexity : "Small";
|
|
14959
15785
|
const task = await adapter2.createTask({
|
|
14960
15786
|
uuid: randomUUID10(),
|
|
14961
15787
|
displayId: "",
|
|
14962
15788
|
title: input.text,
|
|
14963
15789
|
status: "Backlog",
|
|
14964
|
-
priority
|
|
14965
|
-
complexity
|
|
15790
|
+
priority,
|
|
15791
|
+
complexity,
|
|
14966
15792
|
module: input.module || "Core",
|
|
14967
15793
|
epic: input.epic || "Platform",
|
|
14968
15794
|
phase,
|
|
@@ -14973,7 +15799,7 @@ ${warnLines.join("\n")}`;
|
|
|
14973
15799
|
taskType: "idea",
|
|
14974
15800
|
maturity: "raw"
|
|
14975
15801
|
});
|
|
14976
|
-
return { routing: "task", task, message: `${task.id}: "${task.title}" \u2014 added to backlog
|
|
15802
|
+
return { routing: "task", task, message: `${task.id}: "${task.title}" \u2014 added to backlog` };
|
|
14977
15803
|
}
|
|
14978
15804
|
var CANVAS_SECTION_LABELS = {
|
|
14979
15805
|
landscape: "Landscape References",
|
|
@@ -15013,7 +15839,7 @@ async function routeToDiscovery(adapter2, section, input) {
|
|
|
15013
15839
|
// src/tools/idea.ts
|
|
15014
15840
|
var ideaTool = {
|
|
15015
15841
|
name: "idea",
|
|
15016
|
-
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. Does not call the Anthropic API.",
|
|
15842
|
+
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.",
|
|
15017
15843
|
inputSchema: {
|
|
15018
15844
|
type: "object",
|
|
15019
15845
|
properties: {
|
|
@@ -15023,7 +15849,7 @@ var ideaTool = {
|
|
|
15023
15849
|
},
|
|
15024
15850
|
notes: {
|
|
15025
15851
|
type: "string",
|
|
15026
|
-
description: 'Additional context, constraints, or reasoning.
|
|
15852
|
+
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.'
|
|
15027
15853
|
},
|
|
15028
15854
|
module: {
|
|
15029
15855
|
type: "string",
|
|
@@ -15037,6 +15863,16 @@ var ideaTool = {
|
|
|
15037
15863
|
type: "string",
|
|
15038
15864
|
description: 'Target phase (default: "Unscoped").'
|
|
15039
15865
|
},
|
|
15866
|
+
priority: {
|
|
15867
|
+
type: "string",
|
|
15868
|
+
enum: ["P0 Critical", "P1 High", "P2 Medium", "P3 Low"],
|
|
15869
|
+
description: 'Priority level. P0 = broken/blocking. P1 = strategically aligned with current goals. P2 = valuable but not urgent. P3 = nice-to-have/speculative. Default: "P2 Medium". Assess based on strategic alignment and impact, not just effort.'
|
|
15870
|
+
},
|
|
15871
|
+
complexity: {
|
|
15872
|
+
type: "string",
|
|
15873
|
+
enum: ["XS", "Small", "Medium", "Large", "XL"],
|
|
15874
|
+
description: 'Estimated complexity. XS = config/one-liner. Small = one file. Medium = 2-5 files. Large = cross-module. XL = architectural. Default: "Small".'
|
|
15875
|
+
},
|
|
15040
15876
|
discovery: {
|
|
15041
15877
|
type: "boolean",
|
|
15042
15878
|
description: "When true, classify the idea and route to Discovery Canvas instead of backlog. Default: false (always creates a backlog task)."
|
|
@@ -15066,6 +15902,8 @@ async function handleIdea(adapter2, config2, args) {
|
|
|
15066
15902
|
module: args.module,
|
|
15067
15903
|
epic: args.epic,
|
|
15068
15904
|
phase: args.phase,
|
|
15905
|
+
priority: args.priority,
|
|
15906
|
+
complexity: args.complexity,
|
|
15069
15907
|
notes: rawNotes,
|
|
15070
15908
|
discovery: args.discovery === true,
|
|
15071
15909
|
force: args.force === true
|
|
@@ -15088,7 +15926,24 @@ async function handleIdea(adapter2, config2, args) {
|
|
|
15088
15926
|
if (result.routing === "task") {
|
|
15089
15927
|
const branchNote = onFeatureBranch ? ` on ${currentBranch} for next cycle planning.` : " for next cycle planning.";
|
|
15090
15928
|
const truncateWarning = notesTruncated ? ` (notes truncated to ${MAX_NOTES_LENGTH} chars)` : "";
|
|
15091
|
-
|
|
15929
|
+
const hasReference = rawNotes?.toLowerCase().includes("reference:") ?? false;
|
|
15930
|
+
let refNudge = "";
|
|
15931
|
+
if (!hasReference && result.task && adapter2.searchDocs) {
|
|
15932
|
+
try {
|
|
15933
|
+
const keywords = text.split(/\s+/).filter((w) => w.length > 3).slice(0, 3).join(" ");
|
|
15934
|
+
const relatedDocs = await adapter2.searchDocs({ keyword: keywords, limit: 3 });
|
|
15935
|
+
if (relatedDocs.length > 0) {
|
|
15936
|
+
const docList = relatedDocs.map((d) => ` - ${d.path} \u2014 ${d.title}`).join("\n");
|
|
15937
|
+
refNudge = `
|
|
15938
|
+
|
|
15939
|
+
\u26A0\uFE0F **No Reference: line in notes.** The planner generates better handoffs when ideas link to source docs. Potentially relevant docs:
|
|
15940
|
+
${docList}
|
|
15941
|
+
Re-submit with \`notes: "... Reference: <path>"\` to link one, or ignore if none are relevant.`;
|
|
15942
|
+
}
|
|
15943
|
+
} catch {
|
|
15944
|
+
}
|
|
15945
|
+
}
|
|
15946
|
+
return textResponse(`${result.message}${branchNote}${truncateWarning}${refNudge}`);
|
|
15092
15947
|
}
|
|
15093
15948
|
return textResponse(result.message);
|
|
15094
15949
|
}
|
|
@@ -15105,9 +15960,9 @@ function resolveCurrentPhase2(phases) {
|
|
|
15105
15960
|
function severityToPriority(severity) {
|
|
15106
15961
|
switch (severity) {
|
|
15107
15962
|
case "critical":
|
|
15108
|
-
return "
|
|
15963
|
+
return "P0 Critical";
|
|
15109
15964
|
case "major":
|
|
15110
|
-
return "
|
|
15965
|
+
return "P1 High";
|
|
15111
15966
|
default:
|
|
15112
15967
|
return "P2 Medium";
|
|
15113
15968
|
}
|
|
@@ -15567,18 +16422,110 @@ async function applyReconcile(adapter2, corrections) {
|
|
|
15567
16422
|
}
|
|
15568
16423
|
return { applied, skipped, details, phaseChanges };
|
|
15569
16424
|
}
|
|
16425
|
+
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0 Critical", "P1 High", "P2 Medium", "P3 Low"]);
|
|
16426
|
+
var VALID_COMPLEXITIES = /* @__PURE__ */ new Set(["XS", "Small", "Medium", "Large", "XL"]);
|
|
16427
|
+
async function prepareRetriage(adapter2) {
|
|
16428
|
+
const health = await adapter2.getCycleHealth();
|
|
16429
|
+
const currentCycle = health.totalCycles;
|
|
16430
|
+
const allTasks = await adapter2.queryBoard({
|
|
16431
|
+
status: ["Backlog", "In Cycle", "Ready", "Blocked"]
|
|
16432
|
+
});
|
|
16433
|
+
if (allTasks.length === 0) {
|
|
16434
|
+
return "No backlog tasks to retriage.";
|
|
16435
|
+
}
|
|
16436
|
+
const lines = [];
|
|
16437
|
+
lines.push(`## Board Retriage \u2014 Cycle ${currentCycle}`);
|
|
16438
|
+
lines.push("");
|
|
16439
|
+
lines.push(`**${allTasks.length} tasks** to reassess priority and complexity.`);
|
|
16440
|
+
lines.push("");
|
|
16441
|
+
try {
|
|
16442
|
+
const ads = await adapter2.readActiveDecisions();
|
|
16443
|
+
if (ads.length > 0) {
|
|
16444
|
+
lines.push("### Active Decisions (strategic context)");
|
|
16445
|
+
for (const ad of ads.slice(0, 10)) {
|
|
16446
|
+
lines.push(`- **${ad.id}:** ${ad.title} [${ad.confidence}]`);
|
|
16447
|
+
}
|
|
16448
|
+
lines.push("");
|
|
16449
|
+
}
|
|
16450
|
+
} catch {
|
|
16451
|
+
}
|
|
16452
|
+
try {
|
|
16453
|
+
const phases = await adapter2.readPhases();
|
|
16454
|
+
const inProgress = phases.filter((p) => p.status === "In Progress");
|
|
16455
|
+
if (inProgress.length > 0) {
|
|
16456
|
+
lines.push("### Active Phases");
|
|
16457
|
+
for (const p of inProgress) {
|
|
16458
|
+
lines.push(`- ${p.label} (${p.status})`);
|
|
16459
|
+
}
|
|
16460
|
+
lines.push("");
|
|
16461
|
+
}
|
|
16462
|
+
} catch {
|
|
16463
|
+
}
|
|
16464
|
+
lines.push("### All Tasks to Retriage");
|
|
16465
|
+
lines.push("");
|
|
16466
|
+
for (const t of allTasks) {
|
|
16467
|
+
const age = currentCycle - (t.createdCycle ?? 0);
|
|
16468
|
+
const notes = t.notes ? ` \u2014 ${t.notes.slice(0, 120)}` : "";
|
|
16469
|
+
lines.push(`- **${t.id}:** ${t.title} [current: ${t.priority} | ${t.complexity} | ${t.module} | ${t.phase}] (${age} cycles old)${notes}`);
|
|
16470
|
+
}
|
|
16471
|
+
return lines.join("\n");
|
|
16472
|
+
}
|
|
16473
|
+
async function applyRetriage(adapter2, retriages) {
|
|
16474
|
+
const details = [];
|
|
16475
|
+
let applied = 0;
|
|
16476
|
+
let skipped = 0;
|
|
16477
|
+
let unchanged = 0;
|
|
16478
|
+
for (const r of retriages) {
|
|
16479
|
+
try {
|
|
16480
|
+
const task = await adapter2.getTask(r.taskId);
|
|
16481
|
+
if (!task) {
|
|
16482
|
+
details.push(`${r.taskId}: skipped \u2014 not found`);
|
|
16483
|
+
skipped++;
|
|
16484
|
+
continue;
|
|
16485
|
+
}
|
|
16486
|
+
if (!VALID_PRIORITIES.has(r.priority)) {
|
|
16487
|
+
details.push(`${r.taskId}: skipped \u2014 invalid priority "${r.priority}"`);
|
|
16488
|
+
skipped++;
|
|
16489
|
+
continue;
|
|
16490
|
+
}
|
|
16491
|
+
if (!VALID_COMPLEXITIES.has(r.complexity)) {
|
|
16492
|
+
details.push(`${r.taskId}: skipped \u2014 invalid complexity "${r.complexity}"`);
|
|
16493
|
+
skipped++;
|
|
16494
|
+
continue;
|
|
16495
|
+
}
|
|
16496
|
+
if (task.priority === r.priority && task.complexity === r.complexity) {
|
|
16497
|
+
details.push(`${r.taskId}: unchanged \u2014 already ${r.priority} / ${r.complexity}`);
|
|
16498
|
+
unchanged++;
|
|
16499
|
+
continue;
|
|
16500
|
+
}
|
|
16501
|
+
const changes = [];
|
|
16502
|
+
if (task.priority !== r.priority) changes.push(`priority ${task.priority} \u2192 ${r.priority}`);
|
|
16503
|
+
if (task.complexity !== r.complexity) changes.push(`complexity ${task.complexity} \u2192 ${r.complexity}`);
|
|
16504
|
+
await adapter2.updateTask(r.taskId, {
|
|
16505
|
+
priority: r.priority,
|
|
16506
|
+
complexity: r.complexity
|
|
16507
|
+
});
|
|
16508
|
+
details.push(`${r.taskId}: ${changes.join(", ")} \u2014 ${r.reason}`);
|
|
16509
|
+
applied++;
|
|
16510
|
+
} catch (err) {
|
|
16511
|
+
details.push(`${r.taskId}: error \u2014 ${err instanceof Error ? err.message : String(err)}`);
|
|
16512
|
+
skipped++;
|
|
16513
|
+
}
|
|
16514
|
+
}
|
|
16515
|
+
return { applied, skipped, unchanged, details };
|
|
16516
|
+
}
|
|
15570
16517
|
|
|
15571
16518
|
// src/tools/board-reconcile.ts
|
|
15572
16519
|
var boardReconcileTool = {
|
|
15573
16520
|
name: "board_reconcile",
|
|
15574
|
-
description:
|
|
16521
|
+
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.',
|
|
15575
16522
|
inputSchema: {
|
|
15576
16523
|
type: "object",
|
|
15577
16524
|
properties: {
|
|
15578
16525
|
mode: {
|
|
15579
16526
|
type: "string",
|
|
15580
|
-
enum: ["prepare", "apply"],
|
|
15581
|
-
description: '"prepare"
|
|
16527
|
+
enum: ["prepare", "apply", "retriage-prepare", "retriage-apply"],
|
|
16528
|
+
description: '"prepare"/"apply" for cleanup. "retriage-prepare"/"retriage-apply" to reassess priority and complexity on backlog tasks. Defaults to "prepare".'
|
|
15582
16529
|
},
|
|
15583
16530
|
llm_response: {
|
|
15584
16531
|
type: "string",
|
|
@@ -15629,6 +16576,47 @@ When done, call \`board_reconcile\` again with:
|
|
|
15629
16576
|
- \`mode\`: "apply"
|
|
15630
16577
|
- \`llm_response\`: your complete output (both parts)
|
|
15631
16578
|
`;
|
|
16579
|
+
var RETRIAGE_PROMPT = `You are the PAPI Board Retriager. Reassess the priority and complexity of every backlog task below using these criteria:
|
|
16580
|
+
|
|
16581
|
+
## Priority Levels
|
|
16582
|
+
- **P0 Critical** \u2014 Broken, blocking, or data-loss risk. Fix now.
|
|
16583
|
+
- **P1 High** \u2014 Strategically aligned: directly advances the current horizon/phase goals or Active Decisions.
|
|
16584
|
+
- **P2 Medium** \u2014 Valuable but not strategically urgent: quality improvements, efficiency, polish, infrastructure.
|
|
16585
|
+
- **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
|
|
16586
|
+
|
|
16587
|
+
## Complexity Levels
|
|
16588
|
+
- **XS** \u2014 Config change, one-liner, toggle.
|
|
16589
|
+
- **Small** \u2014 One file, < 50 lines changed.
|
|
16590
|
+
- **Medium** \u2014 2-5 files, moderate scope.
|
|
16591
|
+
- **Large** \u2014 Cross-module, multiple components.
|
|
16592
|
+
- **XL** \u2014 Architectural, multi-day effort.
|
|
16593
|
+
|
|
16594
|
+
## Rules
|
|
16595
|
+
- Assess priority based on **strategic alignment** (does it advance current goals?), **unlocks other work** (are tasks blocked by this?), **user-facing impact**, and **compounding value** (does it make future work faster?).
|
|
16596
|
+
- Assess complexity based on the **actual scope of the change**, not conservatively. Use the full range.
|
|
16597
|
+
- If a task's current priority and complexity are already correct, still include it with the same values \u2014 this confirms the assessment.
|
|
16598
|
+
|
|
16599
|
+
Your output must have TWO parts:
|
|
16600
|
+
|
|
16601
|
+
### Part 1: Analysis
|
|
16602
|
+
Brief markdown analysis of how priorities should shift and why.
|
|
16603
|
+
|
|
16604
|
+
### Part 2: Structured Output
|
|
16605
|
+
After \`<!-- PAPI_RETRIAGE_OUTPUT -->\`, a JSON block:
|
|
16606
|
+
|
|
16607
|
+
\`\`\`json
|
|
16608
|
+
{
|
|
16609
|
+
"retriages": [
|
|
16610
|
+
{"taskId": "task-123", "priority": "P1 High", "complexity": "Medium", "reason": "Directly advances Phase 2 goals"},
|
|
16611
|
+
{"taskId": "task-124", "priority": "P3 Low", "complexity": "Small", "reason": "Nice-to-have, no strategic urgency"}
|
|
16612
|
+
]
|
|
16613
|
+
}
|
|
16614
|
+
\`\`\`
|
|
16615
|
+
|
|
16616
|
+
When done, call \`board_reconcile\` again with:
|
|
16617
|
+
- \`mode\`: "retriage-apply"
|
|
16618
|
+
- \`llm_response\`: your complete output (both parts)
|
|
16619
|
+
`;
|
|
15632
16620
|
async function handleBoardReconcile(adapter2, config2, args) {
|
|
15633
16621
|
const mode = args.mode ?? "prepare";
|
|
15634
16622
|
if (mode === "prepare") {
|
|
@@ -15694,7 +16682,70 @@ Analyze the backlog above and produce your reconciliation output. Then call \`bo
|
|
|
15694
16682
|
}
|
|
15695
16683
|
return textResponse(lines.join("\n"));
|
|
15696
16684
|
}
|
|
15697
|
-
|
|
16685
|
+
if (mode === "retriage-prepare") {
|
|
16686
|
+
const context = await prepareRetriage(adapter2);
|
|
16687
|
+
if (context === "No backlog tasks to retriage.") {
|
|
16688
|
+
return textResponse(context);
|
|
16689
|
+
}
|
|
16690
|
+
return textResponse(
|
|
16691
|
+
`${RETRIAGE_PROMPT}
|
|
16692
|
+
---
|
|
16693
|
+
|
|
16694
|
+
### Backlog Context
|
|
16695
|
+
|
|
16696
|
+
${context}
|
|
16697
|
+
---
|
|
16698
|
+
|
|
16699
|
+
Assess each task above and produce your retriage output. Then call \`board_reconcile\` with mode "retriage-apply".`
|
|
16700
|
+
);
|
|
16701
|
+
}
|
|
16702
|
+
if (mode === "retriage-apply") {
|
|
16703
|
+
const llmResponse = args.llm_response;
|
|
16704
|
+
if (!llmResponse?.trim()) {
|
|
16705
|
+
return errorResponse("llm_response is required for retriage-apply mode.");
|
|
16706
|
+
}
|
|
16707
|
+
const marker = "<!-- PAPI_RETRIAGE_OUTPUT -->";
|
|
16708
|
+
const markerIdx = llmResponse.indexOf(marker);
|
|
16709
|
+
if (markerIdx === -1) {
|
|
16710
|
+
return errorResponse("Missing <!-- PAPI_RETRIAGE_OUTPUT --> marker in response.");
|
|
16711
|
+
}
|
|
16712
|
+
const jsonPart = llmResponse.slice(markerIdx + marker.length);
|
|
16713
|
+
const jsonMatch = jsonPart.match(/```json\s*([\s\S]*?)\s*```/);
|
|
16714
|
+
if (!jsonMatch) {
|
|
16715
|
+
return errorResponse("No JSON block found after <!-- PAPI_RETRIAGE_OUTPUT --> marker.");
|
|
16716
|
+
}
|
|
16717
|
+
let retriages;
|
|
16718
|
+
try {
|
|
16719
|
+
const parsed = JSON.parse(jsonMatch[1]);
|
|
16720
|
+
retriages = parsed.retriages;
|
|
16721
|
+
if (!Array.isArray(retriages)) {
|
|
16722
|
+
return errorResponse("retriages must be an array.");
|
|
16723
|
+
}
|
|
16724
|
+
} catch (err) {
|
|
16725
|
+
return errorResponse(`Invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
16726
|
+
}
|
|
16727
|
+
const result = await applyRetriage(adapter2, retriages);
|
|
16728
|
+
if (isGitAvailable() && isGitRepo(config2.projectRoot)) {
|
|
16729
|
+
try {
|
|
16730
|
+
stageDirAndCommit(
|
|
16731
|
+
config2.projectRoot,
|
|
16732
|
+
config2.papiDir,
|
|
16733
|
+
`chore: board retriage \u2014 ${result.applied} tasks updated`
|
|
16734
|
+
);
|
|
16735
|
+
} catch {
|
|
16736
|
+
}
|
|
16737
|
+
}
|
|
16738
|
+
const lines = [];
|
|
16739
|
+
lines.push(`## Board Retriage Complete`);
|
|
16740
|
+
lines.push("");
|
|
16741
|
+
lines.push(`**${result.applied} tasks updated**, ${result.skipped} skipped, ${result.unchanged} unchanged.`);
|
|
16742
|
+
lines.push("");
|
|
16743
|
+
for (const d of result.details) {
|
|
16744
|
+
lines.push(`- ${d}`);
|
|
16745
|
+
}
|
|
16746
|
+
return textResponse(lines.join("\n"));
|
|
16747
|
+
}
|
|
16748
|
+
return errorResponse(`Unknown mode: ${mode}. Use "prepare", "apply", "retriage-prepare", or "retriage-apply".`);
|
|
15698
16749
|
}
|
|
15699
16750
|
|
|
15700
16751
|
// src/services/health.ts
|
|
@@ -15818,6 +16869,25 @@ async function getHealthSummary(adapter2) {
|
|
|
15818
16869
|
} catch (_err) {
|
|
15819
16870
|
metricsSection = "Could not read methodology metrics.";
|
|
15820
16871
|
}
|
|
16872
|
+
try {
|
|
16873
|
+
const recentReports = await adapter2.getRecentBuildReports(50);
|
|
16874
|
+
if (recentReports.length > 0) {
|
|
16875
|
+
const taskCounts = /* @__PURE__ */ new Map();
|
|
16876
|
+
for (const r of recentReports) {
|
|
16877
|
+
taskCounts.set(r.taskId, (taskCounts.get(r.taskId) ?? 0) + 1);
|
|
16878
|
+
}
|
|
16879
|
+
const iterCounts = [...taskCounts.values()];
|
|
16880
|
+
const avgIter = iterCounts.reduce((s, c) => s + c, 0) / iterCounts.length;
|
|
16881
|
+
const multiIterTasks = iterCounts.filter((c) => c > 1).length;
|
|
16882
|
+
if (avgIter > 1 || multiIterTasks > 0) {
|
|
16883
|
+
derivedMetricsSection += `
|
|
16884
|
+
|
|
16885
|
+
**Rework**
|
|
16886
|
+
- Average iterations: ${avgIter.toFixed(1)} (${multiIterTasks} task${multiIterTasks !== 1 ? "s" : ""} with pushbacks)`;
|
|
16887
|
+
}
|
|
16888
|
+
}
|
|
16889
|
+
} catch {
|
|
16890
|
+
}
|
|
15821
16891
|
const costSection = "Disabled \u2014 local MCP, no API costs.";
|
|
15822
16892
|
let decisionUsageSection = "";
|
|
15823
16893
|
try {
|
|
@@ -15991,7 +17061,7 @@ async function handleHealth(adapter2) {
|
|
|
15991
17061
|
|
|
15992
17062
|
// src/services/release.ts
|
|
15993
17063
|
import { writeFile as writeFile3 } from "fs/promises";
|
|
15994
|
-
import { join as
|
|
17064
|
+
import { join as join5 } from "path";
|
|
15995
17065
|
var INITIAL_RELEASE_NOTES = `# Changelog
|
|
15996
17066
|
|
|
15997
17067
|
## v0.1.0-alpha \u2014 Initial Release
|
|
@@ -16082,7 +17152,7 @@ async function createRelease(config2, branch, version, adapter2) {
|
|
|
16082
17152
|
const commits = getCommitsSinceTag(config2.projectRoot, latestTag);
|
|
16083
17153
|
changelogContent = generateChangelog(version, commits);
|
|
16084
17154
|
}
|
|
16085
|
-
const changelogPath =
|
|
17155
|
+
const changelogPath = join5(config2.projectRoot, "CHANGELOG.md");
|
|
16086
17156
|
await writeFile3(changelogPath, changelogContent, "utf-8");
|
|
16087
17157
|
const commitResult = stageAllAndCommit(config2.projectRoot, `release: ${version}`);
|
|
16088
17158
|
const commitNote = commitResult.committed ? `Committed CHANGELOG.md.` : `CHANGELOG.md: ${commitResult.message}`;
|
|
@@ -16156,6 +17226,20 @@ async function handleRelease(adapter2, config2, args) {
|
|
|
16156
17226
|
if (result.warnings?.length) {
|
|
16157
17227
|
lines.push("", "\u26A0\uFE0F Warnings: " + result.warnings.join("; "));
|
|
16158
17228
|
}
|
|
17229
|
+
try {
|
|
17230
|
+
const cycleMatch = version.match(/^v0\.(\d+)\./);
|
|
17231
|
+
const cycleNum = cycleMatch ? parseInt(cycleMatch[1], 10) : 0;
|
|
17232
|
+
if (cycleNum > 0) {
|
|
17233
|
+
const reports = await adapter2.getBuildReportsSince(cycleNum);
|
|
17234
|
+
const EMPTY = /* @__PURE__ */ new Set(["None", "none", "N/A", "", "null"]);
|
|
17235
|
+
const issues = reports.filter((r) => r.discoveredIssues && !EMPTY.has(r.discoveredIssues.trim())).map((r) => `- **${r.taskId}** (${r.taskName}): ${r.discoveredIssues}`);
|
|
17236
|
+
if (issues.length > 0) {
|
|
17237
|
+
lines.push("", "---", "", `## Discovered Issues (${issues.length})`, "", ...issues);
|
|
17238
|
+
lines.push("", "*These issues were logged during builds \u2014 triage them in the next plan.*");
|
|
17239
|
+
}
|
|
17240
|
+
}
|
|
17241
|
+
} catch {
|
|
17242
|
+
}
|
|
16159
17243
|
return textResponse(lines.join("\n"));
|
|
16160
17244
|
} catch (err) {
|
|
16161
17245
|
return errorResponse(err instanceof Error ? err.message : String(err));
|
|
@@ -16163,8 +17247,8 @@ async function handleRelease(adapter2, config2, args) {
|
|
|
16163
17247
|
}
|
|
16164
17248
|
|
|
16165
17249
|
// src/tools/review.ts
|
|
16166
|
-
import { existsSync } from "fs";
|
|
16167
|
-
import { join as
|
|
17250
|
+
import { existsSync as existsSync3 } from "fs";
|
|
17251
|
+
import { join as join6 } from "path";
|
|
16168
17252
|
|
|
16169
17253
|
// src/services/review.ts
|
|
16170
17254
|
init_dist2();
|
|
@@ -16402,8 +17486,8 @@ function mergeAfterAccept(config2, taskId) {
|
|
|
16402
17486
|
}
|
|
16403
17487
|
const featureBranch = taskBranchName(taskId);
|
|
16404
17488
|
const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
|
|
16405
|
-
const papiDir =
|
|
16406
|
-
if (
|
|
17489
|
+
const papiDir = join6(config2.projectRoot, ".papi");
|
|
17490
|
+
if (existsSync3(papiDir)) {
|
|
16407
17491
|
try {
|
|
16408
17492
|
const commitResult = stageDirAndCommit(
|
|
16409
17493
|
config2.projectRoot,
|
|
@@ -16692,6 +17776,9 @@ Path: ${mcpJsonPath}`
|
|
|
16692
17776
|
}
|
|
16693
17777
|
|
|
16694
17778
|
// src/tools/orient.ts
|
|
17779
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
17780
|
+
import { readFileSync, writeFileSync, existsSync as existsSync4 } from "fs";
|
|
17781
|
+
import { join as join7 } from "path";
|
|
16695
17782
|
var orientTool = {
|
|
16696
17783
|
name: "orient",
|
|
16697
17784
|
description: "Session orientation \u2014 single call that replaces build_list + health. Returns: cycle number, task counts by status, in-progress/in-review tasks, strategy review cadence, velocity snapshot, and recommended next action. Read-only, does not modify any files.",
|
|
@@ -16836,8 +17923,31 @@ async function getHierarchyPosition(adapter2) {
|
|
|
16836
17923
|
return void 0;
|
|
16837
17924
|
}
|
|
16838
17925
|
}
|
|
17926
|
+
function checkNpmVersionDrift() {
|
|
17927
|
+
try {
|
|
17928
|
+
const pkgPath = join7(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
|
|
17929
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
17930
|
+
const localVersion = pkg.version;
|
|
17931
|
+
const packageName = pkg.name;
|
|
17932
|
+
const published = execFileSync3("npm", ["view", packageName, "version"], {
|
|
17933
|
+
encoding: "utf-8",
|
|
17934
|
+
timeout: 3e3,
|
|
17935
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
17936
|
+
}).trim();
|
|
17937
|
+
if (published && published !== localVersion) {
|
|
17938
|
+
return `\u26A0\uFE0F npm version drift: local v${localVersion} vs published v${published}`;
|
|
17939
|
+
}
|
|
17940
|
+
return null;
|
|
17941
|
+
} catch {
|
|
17942
|
+
return null;
|
|
17943
|
+
}
|
|
17944
|
+
}
|
|
16839
17945
|
async function handleOrient(adapter2, config2) {
|
|
16840
17946
|
try {
|
|
17947
|
+
try {
|
|
17948
|
+
await propagatePhaseStatus(adapter2);
|
|
17949
|
+
} catch {
|
|
17950
|
+
}
|
|
16841
17951
|
const [buildResult, healthResult, hierarchy] = await Promise.all([
|
|
16842
17952
|
listBuilds(adapter2, config2),
|
|
16843
17953
|
getHealthSummary(adapter2),
|
|
@@ -16884,23 +17994,69 @@ async function handleOrient(adapter2, config2) {
|
|
|
16884
17994
|
} catch {
|
|
16885
17995
|
}
|
|
16886
17996
|
}
|
|
16887
|
-
|
|
17997
|
+
const versionDrift = checkNpmVersionDrift();
|
|
17998
|
+
const versionNote = versionDrift ? `
|
|
17999
|
+
${versionDrift}` : "";
|
|
18000
|
+
let recsNote = "";
|
|
18001
|
+
try {
|
|
18002
|
+
const pendingRecs = await adapter2.getPendingRecommendations();
|
|
18003
|
+
if (pendingRecs.length > 0) {
|
|
18004
|
+
recsNote = `
|
|
18005
|
+
**Strategy Recommendations:** ${pendingRecs.length} pending action`;
|
|
18006
|
+
}
|
|
18007
|
+
} catch {
|
|
18008
|
+
}
|
|
18009
|
+
let pendingReviewNote = "";
|
|
18010
|
+
try {
|
|
18011
|
+
const pending = await adapter2.getPendingReviewResponse?.();
|
|
18012
|
+
if (pending) {
|
|
18013
|
+
pendingReviewNote = `
|
|
18014
|
+
\u26A0\uFE0F **Pending Strategy Review:** 1 review failed write-back (Cycle ${pending.cycleNumber}) \u2014 run \`strategy_review\` to retry.`;
|
|
18015
|
+
}
|
|
18016
|
+
} catch {
|
|
18017
|
+
}
|
|
18018
|
+
let enrichmentNote = "";
|
|
18019
|
+
try {
|
|
18020
|
+
enrichmentNote = enrichClaudeMd(config2.projectRoot, healthResult.cycleNumber);
|
|
18021
|
+
} catch {
|
|
18022
|
+
}
|
|
18023
|
+
return textResponse(formatOrientSummary(healthResult, buildInfo, hierarchy) + ttfvNote + recsNote + pendingReviewNote + versionNote + enrichmentNote);
|
|
16888
18024
|
} catch (err) {
|
|
16889
18025
|
const message = err instanceof Error ? err.message : String(err);
|
|
16890
18026
|
return errorResponse(`Orient failed: ${message}`);
|
|
16891
18027
|
}
|
|
16892
18028
|
}
|
|
18029
|
+
function enrichClaudeMd(projectRoot, cycleNumber) {
|
|
18030
|
+
const claudeMdPath = join7(projectRoot, "CLAUDE.md");
|
|
18031
|
+
if (!existsSync4(claudeMdPath)) return "";
|
|
18032
|
+
const content = readFileSync(claudeMdPath, "utf-8");
|
|
18033
|
+
const additions = [];
|
|
18034
|
+
if (cycleNumber >= 6 && !content.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1)) {
|
|
18035
|
+
additions.push(CLAUDE_MD_TIER_1);
|
|
18036
|
+
}
|
|
18037
|
+
if (cycleNumber >= 21 && !content.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T2)) {
|
|
18038
|
+
additions.push(CLAUDE_MD_TIER_2);
|
|
18039
|
+
}
|
|
18040
|
+
if (additions.length === 0) return "";
|
|
18041
|
+
writeFileSync(claudeMdPath, content + additions.join(""), "utf-8");
|
|
18042
|
+
const tierNames = [];
|
|
18043
|
+
if (additions.some((a) => a.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1))) tierNames.push("Established (batch building, strategy reviews, AD lifecycle)");
|
|
18044
|
+
if (additions.some((a) => a.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T2))) tierNames.push("Mature (idea pipeline, doc registry, advanced patterns)");
|
|
18045
|
+
return `
|
|
18046
|
+
|
|
18047
|
+
\u{1F4DD} **CLAUDE.md enriched** \u2014 added ${tierNames.join(" + ")} guidance for cycle ${cycleNumber}+ projects.`;
|
|
18048
|
+
}
|
|
16893
18049
|
|
|
16894
18050
|
// src/tools/hierarchy.ts
|
|
16895
18051
|
var hierarchyUpdateTool = {
|
|
16896
18052
|
name: "hierarchy_update",
|
|
16897
|
-
description: "Update the status of a stage or horizon in the project hierarchy (AD-14). Accepts a level (stage or horizon), a name or ID, and a new status. Does not call the Anthropic API.",
|
|
18053
|
+
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. Does not call the Anthropic API.",
|
|
16898
18054
|
inputSchema: {
|
|
16899
18055
|
type: "object",
|
|
16900
18056
|
properties: {
|
|
16901
18057
|
level: {
|
|
16902
18058
|
type: "string",
|
|
16903
|
-
enum: ["stage", "horizon"],
|
|
18059
|
+
enum: ["phase", "stage", "horizon"],
|
|
16904
18060
|
description: "Which hierarchy level to update."
|
|
16905
18061
|
},
|
|
16906
18062
|
name: {
|
|
@@ -16924,13 +18080,32 @@ async function handleHierarchyUpdate(adapter2, args) {
|
|
|
16924
18080
|
if (!level || !name || !status) {
|
|
16925
18081
|
return errorResponse("Missing required parameters: level, name, status.");
|
|
16926
18082
|
}
|
|
16927
|
-
if (level !== "stage" && level !== "horizon") {
|
|
16928
|
-
return errorResponse(`Invalid level "${level}". Must be "stage" or "horizon".`);
|
|
18083
|
+
if (level !== "phase" && level !== "stage" && level !== "horizon") {
|
|
18084
|
+
return errorResponse(`Invalid level "${level}". Must be "phase", "stage", or "horizon".`);
|
|
16929
18085
|
}
|
|
16930
18086
|
if (!VALID_STATUSES3.has(status)) {
|
|
16931
18087
|
return errorResponse(`Invalid status "${status}". Must be one of: active, completed, deferred.`);
|
|
16932
18088
|
}
|
|
16933
18089
|
try {
|
|
18090
|
+
if (level === "phase") {
|
|
18091
|
+
if (!adapter2.readPhases || !adapter2.updatePhaseStatus) {
|
|
18092
|
+
return errorResponse("Phase management is not supported by the current adapter.");
|
|
18093
|
+
}
|
|
18094
|
+
const phases = await adapter2.readPhases();
|
|
18095
|
+
const phase = phases.find(
|
|
18096
|
+
(p) => p.label.toLowerCase() === name.toLowerCase() || p.id === name || p.slug === name
|
|
18097
|
+
);
|
|
18098
|
+
if (!phase) {
|
|
18099
|
+
const available = phases.map((p) => p.label).join(", ");
|
|
18100
|
+
return errorResponse(`Phase "${name}" not found. Available phases: ${available || "none"}`);
|
|
18101
|
+
}
|
|
18102
|
+
if (phase.status === status) {
|
|
18103
|
+
return textResponse(`Phase "${phase.label}" is already "${status}". No change made.`);
|
|
18104
|
+
}
|
|
18105
|
+
const oldStatus2 = phase.status;
|
|
18106
|
+
await adapter2.updatePhaseStatus(phase.id, status);
|
|
18107
|
+
return textResponse(`Phase updated: **${phase.label}** ${oldStatus2} \u2192 ${status}`);
|
|
18108
|
+
}
|
|
16934
18109
|
if (level === "stage") {
|
|
16935
18110
|
if (!adapter2.readStages || !adapter2.updateStageStatus) {
|
|
16936
18111
|
return errorResponse("Stage management is not supported by the current adapter.");
|
|
@@ -17311,6 +18486,218 @@ ${result.userMessage}
|
|
|
17311
18486
|
}
|
|
17312
18487
|
}
|
|
17313
18488
|
|
|
18489
|
+
// src/tools/doc-registry.ts
|
|
18490
|
+
import { readdirSync as readdirSync3, existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
|
|
18491
|
+
import { join as join8, relative } from "path";
|
|
18492
|
+
import { homedir as homedir2 } from "os";
|
|
18493
|
+
var docRegisterTool = {
|
|
18494
|
+
name: "doc_register",
|
|
18495
|
+
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.",
|
|
18496
|
+
inputSchema: {
|
|
18497
|
+
type: "object",
|
|
18498
|
+
properties: {
|
|
18499
|
+
path: { type: "string", description: 'Relative path from project root (e.g. "docs/research/funding-landscape.md").' },
|
|
18500
|
+
title: { type: "string", description: "Document title." },
|
|
18501
|
+
type: { type: "string", enum: ["research", "audit", "spec", "guide", "architecture", "positioning", "framework", "reference"], description: "Document type." },
|
|
18502
|
+
status: { type: "string", enum: ["active", "draft", "superseded", "actioned", "legacy", "archived"], description: 'Document status. Defaults to "active".' },
|
|
18503
|
+
summary: { type: "string", description: 'Structured 2-4 sentence summary. Format: "Conclusions: ... Open questions: ... Unactioned: ..."' },
|
|
18504
|
+
tags: { type: "array", items: { type: "string" }, description: "Tags from project vocabulary." },
|
|
18505
|
+
cycle: { type: "number", description: "Current cycle number." },
|
|
18506
|
+
actions: {
|
|
18507
|
+
type: "array",
|
|
18508
|
+
items: {
|
|
18509
|
+
type: "object",
|
|
18510
|
+
properties: {
|
|
18511
|
+
description: { type: "string" },
|
|
18512
|
+
status: { type: "string", enum: ["pending", "resolved"] },
|
|
18513
|
+
linkedTaskId: { type: "string" }
|
|
18514
|
+
},
|
|
18515
|
+
required: ["description", "status"]
|
|
18516
|
+
},
|
|
18517
|
+
description: "Actionable findings from the document."
|
|
18518
|
+
},
|
|
18519
|
+
superseded_by_path: { type: "string", description: "Path of the doc that supersedes this one (sets status to superseded)." }
|
|
18520
|
+
},
|
|
18521
|
+
required: ["path", "title", "type", "summary", "cycle"]
|
|
18522
|
+
}
|
|
18523
|
+
};
|
|
18524
|
+
var docSearchTool = {
|
|
18525
|
+
name: "doc_search",
|
|
18526
|
+
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.",
|
|
18527
|
+
inputSchema: {
|
|
18528
|
+
type: "object",
|
|
18529
|
+
properties: {
|
|
18530
|
+
type: { type: "string", description: 'Filter by doc type (e.g. "research", "architecture").' },
|
|
18531
|
+
status: { type: "string", description: 'Filter by status. Defaults to "active".' },
|
|
18532
|
+
tags: { type: "array", items: { type: "string" }, description: "Filter by tags (OR match)." },
|
|
18533
|
+
keyword: { type: "string", description: "Search title and summary text." },
|
|
18534
|
+
has_pending_actions: { type: "boolean", description: "Only docs with unresolved action items." },
|
|
18535
|
+
since_cycle: { type: "number", description: "Docs updated since this cycle." },
|
|
18536
|
+
limit: { type: "number", description: "Max results (default: 10)." }
|
|
18537
|
+
},
|
|
18538
|
+
required: []
|
|
18539
|
+
}
|
|
18540
|
+
};
|
|
18541
|
+
var docScanTool = {
|
|
18542
|
+
name: "doc_scan",
|
|
18543
|
+
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.",
|
|
18544
|
+
inputSchema: {
|
|
18545
|
+
type: "object",
|
|
18546
|
+
properties: {
|
|
18547
|
+
include_plans: {
|
|
18548
|
+
type: "boolean",
|
|
18549
|
+
description: "Also scan ~/.claude/plans/ for plan files (default: false)."
|
|
18550
|
+
}
|
|
18551
|
+
},
|
|
18552
|
+
required: []
|
|
18553
|
+
}
|
|
18554
|
+
};
|
|
18555
|
+
async function handleDocRegister(adapter2, args) {
|
|
18556
|
+
if (!adapter2.registerDoc) {
|
|
18557
|
+
return errorResponse("Doc registry not available \u2014 requires pg adapter.");
|
|
18558
|
+
}
|
|
18559
|
+
const path5 = args.path;
|
|
18560
|
+
const title = args.title;
|
|
18561
|
+
const type = args.type;
|
|
18562
|
+
const status = args.status ?? "active";
|
|
18563
|
+
const summary = args.summary;
|
|
18564
|
+
const tags = args.tags ?? [];
|
|
18565
|
+
const cycle = args.cycle;
|
|
18566
|
+
const actions = args.actions;
|
|
18567
|
+
const supersededByPath = args.superseded_by_path;
|
|
18568
|
+
if (!path5 || !title || !type || !summary || !cycle) {
|
|
18569
|
+
return errorResponse("Required fields: path, title, type, summary, cycle.");
|
|
18570
|
+
}
|
|
18571
|
+
let supersededBy;
|
|
18572
|
+
if (supersededByPath) {
|
|
18573
|
+
const existing = await adapter2.getDoc?.(supersededByPath);
|
|
18574
|
+
if (existing) {
|
|
18575
|
+
supersededBy = existing.id;
|
|
18576
|
+
await adapter2.updateDocStatus?.(existing.id, "superseded", void 0);
|
|
18577
|
+
}
|
|
18578
|
+
}
|
|
18579
|
+
const entry = await adapter2.registerDoc({
|
|
18580
|
+
title,
|
|
18581
|
+
type,
|
|
18582
|
+
path: path5,
|
|
18583
|
+
status: supersededByPath ? "superseded" : status,
|
|
18584
|
+
summary,
|
|
18585
|
+
tags,
|
|
18586
|
+
cycleCreated: cycle,
|
|
18587
|
+
cycleUpdated: cycle,
|
|
18588
|
+
supersededBy,
|
|
18589
|
+
actions
|
|
18590
|
+
});
|
|
18591
|
+
return textResponse(
|
|
18592
|
+
`**Registered:** ${entry.title}
|
|
18593
|
+
- **Path:** ${entry.path}
|
|
18594
|
+
- **Type:** ${entry.type} | **Status:** ${entry.status}
|
|
18595
|
+
- **Tags:** ${entry.tags.length > 0 ? entry.tags.join(", ") : "none"}
|
|
18596
|
+
- **Actions:** ${actions?.length ?? 0} items
|
|
18597
|
+
- **ID:** ${entry.id}`
|
|
18598
|
+
);
|
|
18599
|
+
}
|
|
18600
|
+
async function handleDocSearch(adapter2, args) {
|
|
18601
|
+
if (!adapter2.searchDocs) {
|
|
18602
|
+
return errorResponse("Doc registry not available \u2014 requires pg adapter.");
|
|
18603
|
+
}
|
|
18604
|
+
const input = {
|
|
18605
|
+
type: args.type,
|
|
18606
|
+
status: args.status,
|
|
18607
|
+
tags: args.tags,
|
|
18608
|
+
keyword: args.keyword,
|
|
18609
|
+
hasPendingActions: args.has_pending_actions,
|
|
18610
|
+
sinceCycle: args.since_cycle,
|
|
18611
|
+
limit: args.limit
|
|
18612
|
+
};
|
|
18613
|
+
const docs = await adapter2.searchDocs(input);
|
|
18614
|
+
if (docs.length === 0) {
|
|
18615
|
+
return textResponse("No documents found matching the search criteria.");
|
|
18616
|
+
}
|
|
18617
|
+
const lines = docs.map((d) => {
|
|
18618
|
+
const actionCount = d.actions?.filter((a) => a.status === "pending").length ?? 0;
|
|
18619
|
+
const actionNote = actionCount > 0 ? ` | ${actionCount} pending action(s)` : "";
|
|
18620
|
+
return `### ${d.title}
|
|
18621
|
+
**Type:** ${d.type} | **Status:** ${d.status} | **Cycle:** ${d.cycleCreated}${d.cycleUpdated ? `\u2192${d.cycleUpdated}` : ""}${actionNote}
|
|
18622
|
+
**Path:** ${d.path}
|
|
18623
|
+
**Tags:** ${d.tags.length > 0 ? d.tags.join(", ") : "none"}
|
|
18624
|
+
${d.summary}
|
|
18625
|
+
`;
|
|
18626
|
+
});
|
|
18627
|
+
return textResponse(`**${docs.length} document(s) found:**
|
|
18628
|
+
|
|
18629
|
+
${lines.join("\n---\n\n")}`);
|
|
18630
|
+
}
|
|
18631
|
+
function scanMdFiles(dir, rootDir) {
|
|
18632
|
+
if (!existsSync5(dir)) return [];
|
|
18633
|
+
const files = [];
|
|
18634
|
+
try {
|
|
18635
|
+
const entries = readdirSync3(dir, { withFileTypes: true });
|
|
18636
|
+
for (const entry of entries) {
|
|
18637
|
+
const full = join8(dir, entry.name);
|
|
18638
|
+
if (entry.isDirectory()) {
|
|
18639
|
+
files.push(...scanMdFiles(full, rootDir));
|
|
18640
|
+
} else if (entry.name.endsWith(".md")) {
|
|
18641
|
+
files.push(relative(rootDir, full));
|
|
18642
|
+
}
|
|
18643
|
+
}
|
|
18644
|
+
} catch {
|
|
18645
|
+
}
|
|
18646
|
+
return files;
|
|
18647
|
+
}
|
|
18648
|
+
function extractTitle(filePath) {
|
|
18649
|
+
try {
|
|
18650
|
+
const content = readFileSync2(filePath, "utf-8").slice(0, 1e3);
|
|
18651
|
+
const fmMatch = content.match(/^---[\s\S]*?title:\s*(.+?)$/m);
|
|
18652
|
+
if (fmMatch) return fmMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
18653
|
+
const headingMatch = content.match(/^#+\s+(.+)$/m);
|
|
18654
|
+
if (headingMatch) return headingMatch[1].trim();
|
|
18655
|
+
} catch {
|
|
18656
|
+
}
|
|
18657
|
+
return void 0;
|
|
18658
|
+
}
|
|
18659
|
+
async function handleDocScan(adapter2, config2, args) {
|
|
18660
|
+
if (!adapter2.searchDocs) {
|
|
18661
|
+
return errorResponse("Doc registry not available \u2014 requires pg adapter.");
|
|
18662
|
+
}
|
|
18663
|
+
const includePlans = args.include_plans ?? false;
|
|
18664
|
+
const registered = await adapter2.searchDocs({ limit: 500 });
|
|
18665
|
+
const registeredPaths = new Set(registered.map((d) => d.path));
|
|
18666
|
+
const docsDir = join8(config2.projectRoot, "docs");
|
|
18667
|
+
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
18668
|
+
const unregisteredDocs = docsFiles.filter((f) => !registeredPaths.has(f));
|
|
18669
|
+
let unregisteredPlans = [];
|
|
18670
|
+
if (includePlans) {
|
|
18671
|
+
const plansDir = join8(homedir2(), ".claude", "plans");
|
|
18672
|
+
if (existsSync5(plansDir)) {
|
|
18673
|
+
const planFiles = scanMdFiles(plansDir, plansDir);
|
|
18674
|
+
unregisteredPlans = planFiles.map((f) => `plans/${f}`).filter((f) => !registeredPaths.has(f)).map((f) => ({
|
|
18675
|
+
path: f,
|
|
18676
|
+
title: extractTitle(join8(plansDir, f.replace("plans/", "")))
|
|
18677
|
+
}));
|
|
18678
|
+
}
|
|
18679
|
+
}
|
|
18680
|
+
const lines = [];
|
|
18681
|
+
if (unregisteredDocs.length === 0 && unregisteredPlans.length === 0) {
|
|
18682
|
+
return textResponse("All docs are registered. No unregistered files found.");
|
|
18683
|
+
}
|
|
18684
|
+
if (unregisteredDocs.length > 0) {
|
|
18685
|
+
lines.push(`## Unregistered Docs (${unregisteredDocs.length})`);
|
|
18686
|
+
for (const f of unregisteredDocs) {
|
|
18687
|
+
const title = extractTitle(join8(config2.projectRoot, f));
|
|
18688
|
+
lines.push(`- \`${f}\`${title ? ` \u2014 ${title}` : ""}`);
|
|
18689
|
+
}
|
|
18690
|
+
}
|
|
18691
|
+
if (unregisteredPlans.length > 0) {
|
|
18692
|
+
lines.push("", `## Unregistered Plans (${unregisteredPlans.length})`);
|
|
18693
|
+
for (const p of unregisteredPlans) {
|
|
18694
|
+
lines.push(`- \`${p.path}\`${p.title ? ` \u2014 ${p.title}` : ""}`);
|
|
18695
|
+
}
|
|
18696
|
+
}
|
|
18697
|
+
lines.push("", `Use \`doc_register\` to register these files.`);
|
|
18698
|
+
return textResponse(lines.join("\n"));
|
|
18699
|
+
}
|
|
18700
|
+
|
|
17314
18701
|
// src/lib/telemetry.ts
|
|
17315
18702
|
var TELEMETRY_SUPABASE_URL = "https://guewgygcpcmrcoppihzx.supabase.co";
|
|
17316
18703
|
var TELEMETRY_API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd1ZXdneWdjcGNtcmNvcHBpaHp4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI2Njk2NTMsImV4cCI6MjA4ODI0NTY1M30.V5Jw7wJgiMpSQPa2mt0ftjyye5ynG1qLlam00yPVNJY";
|
|
@@ -17363,6 +18750,7 @@ var TOOLS_REQUIRING_PAPI = /* @__PURE__ */ new Set([
|
|
|
17363
18750
|
"board_view",
|
|
17364
18751
|
"board_deprioritise",
|
|
17365
18752
|
"board_archive",
|
|
18753
|
+
"board_edit",
|
|
17366
18754
|
"build_list",
|
|
17367
18755
|
"build_describe",
|
|
17368
18756
|
"build_execute",
|
|
@@ -17391,6 +18779,7 @@ function createServer(adapter2, config2) {
|
|
|
17391
18779
|
boardViewTool,
|
|
17392
18780
|
boardDeprioritiseTool,
|
|
17393
18781
|
boardArchiveTool,
|
|
18782
|
+
boardEditTool,
|
|
17394
18783
|
setupTool,
|
|
17395
18784
|
buildListTool,
|
|
17396
18785
|
buildDescribeTool,
|
|
@@ -17407,7 +18796,10 @@ function createServer(adapter2, config2) {
|
|
|
17407
18796
|
initTool,
|
|
17408
18797
|
orientTool,
|
|
17409
18798
|
hierarchyUpdateTool,
|
|
17410
|
-
zoomOutTool
|
|
18799
|
+
zoomOutTool,
|
|
18800
|
+
docRegisterTool,
|
|
18801
|
+
docSearchTool,
|
|
18802
|
+
docScanTool
|
|
17411
18803
|
]
|
|
17412
18804
|
}));
|
|
17413
18805
|
server2.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -17457,6 +18849,9 @@ function createServer(adapter2, config2) {
|
|
|
17457
18849
|
case "board_archive":
|
|
17458
18850
|
result = await handleBoardArchive(adapter2, safeArgs);
|
|
17459
18851
|
break;
|
|
18852
|
+
case "board_edit":
|
|
18853
|
+
result = await handleBoardEdit(adapter2, safeArgs);
|
|
18854
|
+
break;
|
|
17460
18855
|
case "setup":
|
|
17461
18856
|
result = await handleSetup(adapter2, config2, safeArgs);
|
|
17462
18857
|
break;
|
|
@@ -17508,6 +18903,15 @@ function createServer(adapter2, config2) {
|
|
|
17508
18903
|
case "zoom_out":
|
|
17509
18904
|
result = await handleZoomOut(adapter2, config2, safeArgs);
|
|
17510
18905
|
break;
|
|
18906
|
+
case "doc_register":
|
|
18907
|
+
result = await handleDocRegister(adapter2, safeArgs);
|
|
18908
|
+
break;
|
|
18909
|
+
case "doc_search":
|
|
18910
|
+
result = await handleDocSearch(adapter2, safeArgs);
|
|
18911
|
+
break;
|
|
18912
|
+
case "doc_scan":
|
|
18913
|
+
result = await handleDocScan(adapter2, config2, safeArgs);
|
|
18914
|
+
break;
|
|
17511
18915
|
default:
|
|
17512
18916
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
|
|
17513
18917
|
}
|