@papi-ai/server 0.5.3 → 0.6.0-alpha.1
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/README.md +28 -0
- package/dist/index.js +964 -202
- package/dist/prompts.js +83 -91
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1556,7 +1556,8 @@ ${TABLE_SEPARATOR}
|
|
|
1556
1556
|
return [];
|
|
1557
1557
|
}
|
|
1558
1558
|
/** Update or insert an Active Decision block by ID. */
|
|
1559
|
-
|
|
1559
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1560
|
+
async updateActiveDecision(id, body, cycleNumber, _action) {
|
|
1560
1561
|
const content = await this.readOptional("ACTIVE_DECISIONS.md") || "## Active Decisions\n\n";
|
|
1561
1562
|
await this.write("ACTIVE_DECISIONS.md", updateActiveDecisionInContent(id, body, content, cycleNumber));
|
|
1562
1563
|
}
|
|
@@ -4442,6 +4443,7 @@ function rowToTask(row) {
|
|
|
4442
4443
|
if (row.task_type != null) task.taskType = row.task_type;
|
|
4443
4444
|
if (row.maturity != null) task.maturity = row.maturity;
|
|
4444
4445
|
if (row.stage_id != null) task.stageId = row.stage_id;
|
|
4446
|
+
if (row.doc_ref != null) task.docRef = row.doc_ref;
|
|
4445
4447
|
return task;
|
|
4446
4448
|
}
|
|
4447
4449
|
function rowToBuildReport(row) {
|
|
@@ -4540,6 +4542,8 @@ function rowToCycleLogEntry(row) {
|
|
|
4540
4542
|
};
|
|
4541
4543
|
if (row.carry_forward != null) entry.carryForward = row.carry_forward;
|
|
4542
4544
|
if (row.notes != null) entry.notes = row.notes;
|
|
4545
|
+
if (row.task_count != null) entry.taskCount = row.task_count;
|
|
4546
|
+
if (row.effort_points != null) entry.effortPoints = row.effort_points;
|
|
4543
4547
|
return entry;
|
|
4544
4548
|
}
|
|
4545
4549
|
function rowToPhase(row) {
|
|
@@ -4670,6 +4674,7 @@ function rowToStage(row) {
|
|
|
4670
4674
|
sortOrder: row.sort_order,
|
|
4671
4675
|
horizonId: row.horizon_id,
|
|
4672
4676
|
projectId: row.project_id,
|
|
4677
|
+
exitCriteria: row.exit_criteria ?? void 0,
|
|
4673
4678
|
createdAt: row.created_at,
|
|
4674
4679
|
updatedAt: row.updated_at
|
|
4675
4680
|
};
|
|
@@ -5366,6 +5371,7 @@ CREATE TABLE IF NOT EXISTS cycle_tasks (
|
|
|
5366
5371
|
task_type TEXT,
|
|
5367
5372
|
maturity TEXT,
|
|
5368
5373
|
stage_id UUID REFERENCES stages(id),
|
|
5374
|
+
doc_ref TEXT,
|
|
5369
5375
|
PRIMARY KEY (id),
|
|
5370
5376
|
UNIQUE (project_id, display_id)
|
|
5371
5377
|
);
|
|
@@ -5411,6 +5417,8 @@ CREATE TABLE IF NOT EXISTS planning_log_entries (
|
|
|
5411
5417
|
content TEXT DEFAULT ''::text NOT NULL,
|
|
5412
5418
|
carry_forward TEXT,
|
|
5413
5419
|
notes TEXT,
|
|
5420
|
+
task_count INTEGER,
|
|
5421
|
+
effort_points INTEGER,
|
|
5414
5422
|
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
|
|
5415
5423
|
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
|
|
5416
5424
|
user_id UUID,
|
|
@@ -6118,6 +6126,19 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
|
6118
6126
|
`;
|
|
6119
6127
|
return rows.map(rowToActiveDecision);
|
|
6120
6128
|
}
|
|
6129
|
+
async getSiblingAds(projectIds) {
|
|
6130
|
+
if (projectIds.length === 0) return [];
|
|
6131
|
+
const rows = await this.sql`
|
|
6132
|
+
SELECT *, project_id FROM active_decisions
|
|
6133
|
+
WHERE project_id = ANY(${projectIds}::uuid[])
|
|
6134
|
+
AND superseded = false
|
|
6135
|
+
ORDER BY project_id, display_id
|
|
6136
|
+
`;
|
|
6137
|
+
return rows.map((row) => ({
|
|
6138
|
+
...rowToActiveDecision(row),
|
|
6139
|
+
sourceProjectId: row.project_id
|
|
6140
|
+
}));
|
|
6141
|
+
}
|
|
6121
6142
|
async getCycleLog(limit) {
|
|
6122
6143
|
if (limit != null) {
|
|
6123
6144
|
const rows2 = await this.sql`
|
|
@@ -6168,21 +6189,25 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6168
6189
|
}
|
|
6169
6190
|
async writeCycleLogEntry(entry) {
|
|
6170
6191
|
await this.sql`
|
|
6171
|
-
INSERT INTO planning_log_entries (project_id, cycle_number, title, content, carry_forward, notes)
|
|
6192
|
+
INSERT INTO planning_log_entries (project_id, cycle_number, title, content, carry_forward, notes, task_count, effort_points)
|
|
6172
6193
|
VALUES (
|
|
6173
6194
|
${this.projectId},
|
|
6174
6195
|
${entry.cycleNumber},
|
|
6175
6196
|
${entry.title},
|
|
6176
6197
|
${entry.content},
|
|
6177
6198
|
${entry.carryForward ?? null},
|
|
6178
|
-
${entry.notes ?? null}
|
|
6199
|
+
${entry.notes ?? null},
|
|
6200
|
+
${entry.taskCount ?? null},
|
|
6201
|
+
${entry.effortPoints ?? null}
|
|
6179
6202
|
)
|
|
6180
6203
|
ON CONFLICT (project_id, cycle_number)
|
|
6181
6204
|
DO UPDATE SET
|
|
6182
6205
|
title = EXCLUDED.title,
|
|
6183
6206
|
content = EXCLUDED.content,
|
|
6184
6207
|
carry_forward = EXCLUDED.carry_forward,
|
|
6185
|
-
notes = EXCLUDED.notes
|
|
6208
|
+
notes = EXCLUDED.notes,
|
|
6209
|
+
task_count = EXCLUDED.task_count,
|
|
6210
|
+
effort_points = EXCLUDED.effort_points
|
|
6186
6211
|
`;
|
|
6187
6212
|
}
|
|
6188
6213
|
async writeStrategyReview(review) {
|
|
@@ -6487,19 +6512,44 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6487
6512
|
WHERE id = ${id} AND project_id = ${this.projectId}
|
|
6488
6513
|
`;
|
|
6489
6514
|
}
|
|
6490
|
-
async updateActiveDecision(id, body, cycleNumber) {
|
|
6515
|
+
async updateActiveDecision(id, body, cycleNumber, action) {
|
|
6516
|
+
const newOutcome = action === "supersede" ? "superseded" : action === "resolve" ? "resolved" : void 0;
|
|
6491
6517
|
if (cycleNumber != null) {
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6518
|
+
if (newOutcome != null) {
|
|
6519
|
+
await this.sql`
|
|
6520
|
+
UPDATE active_decisions
|
|
6521
|
+
SET body = ${body}, modified_cycle = ${cycleNumber},
|
|
6522
|
+
revision_count = revision_count + 1,
|
|
6523
|
+
outcome = ${newOutcome}
|
|
6524
|
+
WHERE project_id = ${this.projectId} AND display_id = ${id}
|
|
6525
|
+
`;
|
|
6526
|
+
} else {
|
|
6527
|
+
await this.sql`
|
|
6528
|
+
UPDATE active_decisions
|
|
6529
|
+
SET body = ${body}, modified_cycle = ${cycleNumber},
|
|
6530
|
+
revision_count = revision_count + 1,
|
|
6531
|
+
outcome = CASE WHEN outcome = 'pending' THEN 'revised' ELSE outcome END
|
|
6532
|
+
WHERE project_id = ${this.projectId} AND display_id = ${id}
|
|
6533
|
+
`;
|
|
6534
|
+
}
|
|
6497
6535
|
} else {
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
|
|
6536
|
+
if (newOutcome != null) {
|
|
6537
|
+
await this.sql`
|
|
6538
|
+
UPDATE active_decisions
|
|
6539
|
+
SET body = ${body},
|
|
6540
|
+
revision_count = revision_count + 1,
|
|
6541
|
+
outcome = ${newOutcome}
|
|
6542
|
+
WHERE project_id = ${this.projectId} AND display_id = ${id}
|
|
6543
|
+
`;
|
|
6544
|
+
} else {
|
|
6545
|
+
await this.sql`
|
|
6546
|
+
UPDATE active_decisions
|
|
6547
|
+
SET body = ${body},
|
|
6548
|
+
revision_count = revision_count + 1,
|
|
6549
|
+
outcome = CASE WHEN outcome = 'pending' THEN 'revised' ELSE outcome END
|
|
6550
|
+
WHERE project_id = ${this.projectId} AND display_id = ${id}
|
|
6551
|
+
`;
|
|
6552
|
+
}
|
|
6503
6553
|
}
|
|
6504
6554
|
}
|
|
6505
6555
|
async upsertActiveDecision(id, body, title, confidence, cycleNumber) {
|
|
@@ -6596,7 +6646,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6596
6646
|
project_id, display_id, title, status, priority, complexity,
|
|
6597
6647
|
module, epic, phase, owner, reviewed, cycle, created_cycle,
|
|
6598
6648
|
why, depends_on, notes, closure_reason, state_history,
|
|
6599
|
-
build_handoff, build_report, task_type, maturity, stage_id
|
|
6649
|
+
build_handoff, build_report, task_type, maturity, stage_id, doc_ref
|
|
6600
6650
|
) VALUES (
|
|
6601
6651
|
${this.projectId}, ${displayId}, ${task.title}, ${task.status}, ${task.priority},
|
|
6602
6652
|
${normaliseComplexity(task.complexity)}, ${task.module}, ${task.epic ?? null}, ${task.phase}, ${task.owner},
|
|
@@ -6608,7 +6658,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6608
6658
|
${task.buildReport ?? null},
|
|
6609
6659
|
${task.taskType ?? null},
|
|
6610
6660
|
${task.maturity ?? null},
|
|
6611
|
-
${task.stageId ?? null}
|
|
6661
|
+
${task.stageId ?? null},
|
|
6662
|
+
${task.docRef ?? null}
|
|
6612
6663
|
)
|
|
6613
6664
|
RETURNING *
|
|
6614
6665
|
`;
|
|
@@ -6638,6 +6689,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6638
6689
|
if (updates.taskType !== void 0) columnMap["task_type"] = updates.taskType;
|
|
6639
6690
|
if (updates.maturity !== void 0) columnMap["maturity"] = updates.maturity;
|
|
6640
6691
|
if (updates.stageId !== void 0) columnMap["stage_id"] = updates.stageId;
|
|
6692
|
+
if (updates.docRef !== void 0) columnMap["doc_ref"] = updates.docRef;
|
|
6641
6693
|
const keys = Object.keys(columnMap);
|
|
6642
6694
|
if (keys.length === 0) return;
|
|
6643
6695
|
await this.sql`
|
|
@@ -6724,6 +6776,86 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6724
6776
|
return rows.map(rowToBuildReport);
|
|
6725
6777
|
}
|
|
6726
6778
|
// -------------------------------------------------------------------------
|
|
6779
|
+
// Cycle Learnings
|
|
6780
|
+
// -------------------------------------------------------------------------
|
|
6781
|
+
async appendCycleLearnings(learnings) {
|
|
6782
|
+
if (learnings.length === 0) return;
|
|
6783
|
+
for (const l of learnings) {
|
|
6784
|
+
await this.sql`
|
|
6785
|
+
INSERT INTO cycle_learnings (
|
|
6786
|
+
project_id, task_id, cycle_number, category, severity,
|
|
6787
|
+
summary, detail, tags, related_decision, action_taken, action_ref
|
|
6788
|
+
) VALUES (
|
|
6789
|
+
${this.projectId}, ${l.taskId}, ${l.cycleNumber}, ${l.category}, ${l.severity ?? null},
|
|
6790
|
+
${l.summary}, ${l.detail ?? null}, ${l.tags}, ${l.relatedDecision ?? null},
|
|
6791
|
+
${l.actionTaken ?? null}, ${l.actionRef ?? null}
|
|
6792
|
+
)
|
|
6793
|
+
`;
|
|
6794
|
+
}
|
|
6795
|
+
}
|
|
6796
|
+
async getCycleLearnings(opts) {
|
|
6797
|
+
const limit = opts?.limit ?? 50;
|
|
6798
|
+
let rows;
|
|
6799
|
+
if (opts?.cycleNumber && opts?.category) {
|
|
6800
|
+
rows = await this.sql`
|
|
6801
|
+
SELECT * FROM cycle_learnings
|
|
6802
|
+
WHERE project_id = ${this.projectId} AND cycle_number = ${opts.cycleNumber} AND category = ${opts.category}
|
|
6803
|
+
ORDER BY created_at DESC LIMIT ${limit}
|
|
6804
|
+
`;
|
|
6805
|
+
} else if (opts?.cycleNumber) {
|
|
6806
|
+
rows = await this.sql`
|
|
6807
|
+
SELECT * FROM cycle_learnings
|
|
6808
|
+
WHERE project_id = ${this.projectId} AND cycle_number = ${opts.cycleNumber}
|
|
6809
|
+
ORDER BY created_at DESC LIMIT ${limit}
|
|
6810
|
+
`;
|
|
6811
|
+
} else if (opts?.category) {
|
|
6812
|
+
rows = await this.sql`
|
|
6813
|
+
SELECT * FROM cycle_learnings
|
|
6814
|
+
WHERE project_id = ${this.projectId} AND category = ${opts.category}
|
|
6815
|
+
ORDER BY created_at DESC LIMIT ${limit}
|
|
6816
|
+
`;
|
|
6817
|
+
} else {
|
|
6818
|
+
rows = await this.sql`
|
|
6819
|
+
SELECT * FROM cycle_learnings
|
|
6820
|
+
WHERE project_id = ${this.projectId}
|
|
6821
|
+
ORDER BY created_at DESC LIMIT ${limit}
|
|
6822
|
+
`;
|
|
6823
|
+
}
|
|
6824
|
+
return rows.map((r) => ({
|
|
6825
|
+
id: r.id,
|
|
6826
|
+
projectId: r.project_id,
|
|
6827
|
+
taskId: r.task_id,
|
|
6828
|
+
cycleNumber: r.cycle_number,
|
|
6829
|
+
category: r.category,
|
|
6830
|
+
severity: r.severity ?? void 0,
|
|
6831
|
+
summary: r.summary,
|
|
6832
|
+
detail: r.detail ?? void 0,
|
|
6833
|
+
tags: r.tags ?? [],
|
|
6834
|
+
relatedDecision: r.related_decision ?? void 0,
|
|
6835
|
+
actionTaken: r.action_taken ?? void 0,
|
|
6836
|
+
actionRef: r.action_ref ?? void 0,
|
|
6837
|
+
createdAt: r.created_at ? r.created_at.toISOString() : void 0
|
|
6838
|
+
}));
|
|
6839
|
+
}
|
|
6840
|
+
async getCycleLearningPatterns() {
|
|
6841
|
+
const rows = await this.sql`
|
|
6842
|
+
SELECT tag,
|
|
6843
|
+
array_agg(DISTINCT cycle_number ORDER BY cycle_number) as cycles,
|
|
6844
|
+
count(DISTINCT cycle_number)::text as frequency
|
|
6845
|
+
FROM cycle_learnings, unnest(tags) AS tag
|
|
6846
|
+
WHERE project_id = ${this.projectId}
|
|
6847
|
+
GROUP BY tag
|
|
6848
|
+
HAVING count(DISTINCT cycle_number) >= 2
|
|
6849
|
+
ORDER BY count(DISTINCT cycle_number) DESC
|
|
6850
|
+
LIMIT 10
|
|
6851
|
+
`;
|
|
6852
|
+
return rows.map((r) => ({
|
|
6853
|
+
tag: r.tag,
|
|
6854
|
+
cycles: r.cycles,
|
|
6855
|
+
frequency: parseInt(r.frequency, 10)
|
|
6856
|
+
}));
|
|
6857
|
+
}
|
|
6858
|
+
// -------------------------------------------------------------------------
|
|
6727
6859
|
// Reviews
|
|
6728
6860
|
// -------------------------------------------------------------------------
|
|
6729
6861
|
async getRecentReviews(count) {
|
|
@@ -7091,6 +7223,12 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
7091
7223
|
await this.sql`
|
|
7092
7224
|
UPDATE stages SET status = ${status}, updated_at = NOW()
|
|
7093
7225
|
WHERE id = ${stageId} AND project_id = ${this.projectId}
|
|
7226
|
+
`;
|
|
7227
|
+
}
|
|
7228
|
+
async updateStageExitCriteria(stageId, exitCriteria) {
|
|
7229
|
+
await this.sql`
|
|
7230
|
+
UPDATE stages SET exit_criteria = ${exitCriteria}, updated_at = NOW()
|
|
7231
|
+
WHERE id = ${stageId} AND project_id = ${this.projectId}
|
|
7094
7232
|
`;
|
|
7095
7233
|
}
|
|
7096
7234
|
async updateHorizonStatus(horizonId, status) {
|
|
@@ -7436,6 +7574,26 @@ ${r.content}` + (r.carry_forward ? `
|
|
|
7436
7574
|
return row?.exists ?? false;
|
|
7437
7575
|
}
|
|
7438
7576
|
// -------------------------------------------------------------------------
|
|
7577
|
+
// Plan Runs (telemetry)
|
|
7578
|
+
// -------------------------------------------------------------------------
|
|
7579
|
+
async insertPlanRun(entry) {
|
|
7580
|
+
await this.sql`
|
|
7581
|
+
INSERT INTO plan_runs (
|
|
7582
|
+
project_id, cycle_number, context_bytes, duration_ms,
|
|
7583
|
+
task_count_in, task_count_out, backlog_depth, notes
|
|
7584
|
+
) VALUES (
|
|
7585
|
+
${this.projectId},
|
|
7586
|
+
${entry.cycleNumber},
|
|
7587
|
+
${entry.contextBytes ?? null},
|
|
7588
|
+
${entry.durationMs ?? null},
|
|
7589
|
+
${entry.taskCountIn ?? null},
|
|
7590
|
+
${entry.taskCountOut ?? null},
|
|
7591
|
+
${entry.backlogDepth ?? null},
|
|
7592
|
+
${entry.notes ?? null}
|
|
7593
|
+
)
|
|
7594
|
+
`;
|
|
7595
|
+
}
|
|
7596
|
+
// -------------------------------------------------------------------------
|
|
7439
7597
|
// Task Comments (for plan context)
|
|
7440
7598
|
// -------------------------------------------------------------------------
|
|
7441
7599
|
async getRecentTaskComments(limit = 20) {
|
|
@@ -7469,6 +7627,20 @@ ${r.content}` + (r.carry_forward ? `
|
|
|
7469
7627
|
cycle_number: ref.cycleNumber
|
|
7470
7628
|
}));
|
|
7471
7629
|
await this.sql`INSERT INTO entity_references ${this.sql(values2)}`;
|
|
7630
|
+
const adRefs = refs.filter((r) => r.entityType === "active_decision");
|
|
7631
|
+
if (adRefs.length > 0) {
|
|
7632
|
+
const cycleNumber = adRefs.reduce((max, r) => Math.max(max, r.cycleNumber ?? 0), 0);
|
|
7633
|
+
if (cycleNumber > 0) {
|
|
7634
|
+
const adIds = adRefs.map((r) => r.entityId);
|
|
7635
|
+
await this.sql`
|
|
7636
|
+
UPDATE active_decisions
|
|
7637
|
+
SET last_referenced_cycle = ${cycleNumber}
|
|
7638
|
+
WHERE project_id = ${this.projectId}
|
|
7639
|
+
AND display_id = ANY(${adIds})
|
|
7640
|
+
AND (last_referenced_cycle IS NULL OR last_referenced_cycle < ${cycleNumber})
|
|
7641
|
+
`;
|
|
7642
|
+
}
|
|
7643
|
+
}
|
|
7472
7644
|
}
|
|
7473
7645
|
async getDecisionUsage(currentCycle) {
|
|
7474
7646
|
const rows = await this.sql`
|
|
@@ -8068,6 +8240,9 @@ var init_proxy_adapter = __esm({
|
|
|
8068
8240
|
actionRecommendation(id, cycleNumber) {
|
|
8069
8241
|
return this.invoke("actionRecommendation", [id, cycleNumber]);
|
|
8070
8242
|
}
|
|
8243
|
+
dismissRecommendation(id, reason) {
|
|
8244
|
+
return this.invoke("dismissRecommendation", [id, reason]);
|
|
8245
|
+
}
|
|
8071
8246
|
// --- Decision Events & Scores ---
|
|
8072
8247
|
appendDecisionEvent(event) {
|
|
8073
8248
|
return this.invoke("appendDecisionEvent", [event]);
|
|
@@ -8301,19 +8476,33 @@ async function createAdapter(optionsOrType, maybePapiDir) {
|
|
|
8301
8476
|
}
|
|
8302
8477
|
case "proxy": {
|
|
8303
8478
|
const { ProxyPapiAdapter: ProxyPapiAdapter2 } = await Promise.resolve().then(() => (init_proxy_adapter(), proxy_adapter_exports));
|
|
8479
|
+
const dashboardUrl = process.env["PAPI_DASHBOARD_URL"] || "https://papi-web-three.vercel.app";
|
|
8304
8480
|
const projectId = process.env["PAPI_PROJECT_ID"];
|
|
8481
|
+
const dataApiKey = process.env["PAPI_DATA_API_KEY"];
|
|
8482
|
+
if (!projectId && !dataApiKey) {
|
|
8483
|
+
throw new Error(
|
|
8484
|
+
`PAPI needs an account to store your project data.
|
|
8485
|
+
|
|
8486
|
+
Get started in 3 steps:
|
|
8487
|
+
1. Sign up at ${dashboardUrl}/login
|
|
8488
|
+
2. Complete the onboarding wizard \u2014 it generates your .mcp.json config
|
|
8489
|
+
3. Download the config, place it in your project root, and restart Claude Code
|
|
8490
|
+
|
|
8491
|
+
Already have an account? Make sure PAPI_DATA_API_KEY and PAPI_PROJECT_ID are set in your .mcp.json env config.`
|
|
8492
|
+
);
|
|
8493
|
+
}
|
|
8305
8494
|
if (!projectId) {
|
|
8306
8495
|
throw new Error(
|
|
8307
|
-
|
|
8496
|
+
`PAPI_PROJECT_ID is required.
|
|
8497
|
+
Visit ${dashboardUrl}/onboard to generate your config, or add PAPI_PROJECT_ID to your .mcp.json env config.`
|
|
8308
8498
|
);
|
|
8309
8499
|
}
|
|
8310
8500
|
const dataEndpoint = process.env["PAPI_DATA_ENDPOINT"] || HOSTED_PROXY_ENDPOINT;
|
|
8311
|
-
const dataApiKey = process.env["PAPI_DATA_API_KEY"];
|
|
8312
8501
|
if (!dataApiKey) {
|
|
8313
8502
|
throw new Error(
|
|
8314
8503
|
`PAPI_DATA_API_KEY is required for proxy mode.
|
|
8315
8504
|
To get your API key:
|
|
8316
|
-
1. Sign in at ${
|
|
8505
|
+
1. Sign up or sign in at ${dashboardUrl}/login
|
|
8317
8506
|
2. Your API key is shown on the onboarding page (save it \u2014 shown only once)
|
|
8318
8507
|
3. Add PAPI_DATA_API_KEY to your .mcp.json env config
|
|
8319
8508
|
If you already have a key, set it in your MCP configuration.`
|
|
@@ -8345,11 +8534,15 @@ If you already have a key, set it in your MCP configuration.`
|
|
|
8345
8534
|
}
|
|
8346
8535
|
|
|
8347
8536
|
// src/server.ts
|
|
8348
|
-
import { access as access4 } from "fs/promises";
|
|
8537
|
+
import { access as access4, readdir as readdir2, readFile as readFile5 } from "fs/promises";
|
|
8538
|
+
import { join as join9, dirname } from "path";
|
|
8539
|
+
import { fileURLToPath } from "url";
|
|
8349
8540
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
8350
8541
|
import {
|
|
8351
8542
|
CallToolRequestSchema,
|
|
8352
|
-
ListToolsRequestSchema
|
|
8543
|
+
ListToolsRequestSchema,
|
|
8544
|
+
ListPromptsRequestSchema,
|
|
8545
|
+
GetPromptRequestSchema
|
|
8353
8546
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
8354
8547
|
|
|
8355
8548
|
// src/lib/response.ts
|
|
@@ -8473,7 +8666,7 @@ function formatDetailedTask(t) {
|
|
|
8473
8666
|
return `- **${t.id}:** ${t.title}
|
|
8474
8667
|
Status: ${t.status} | Priority: ${t.priority} | Complexity: ${t.complexity}${typeTag}
|
|
8475
8668
|
Module: ${t.module} | Epic: ${t.epic} | Phase: ${t.phase} | Owner: ${t.owner}
|
|
8476
|
-
Reviewed: ${t.reviewed}${t.dependsOn ? ` | Depends on: ${t.dependsOn}` : ""}${hasHandoff ? " | Has BUILD HANDOFF: yes" : ""}${notes ? `
|
|
8669
|
+
Reviewed: ${t.reviewed}${t.dependsOn ? ` | Depends on: ${t.dependsOn}` : ""}${hasHandoff ? " | Has BUILD HANDOFF: yes" : ""}${t.docRef ? ` | Doc ref: ${t.docRef}` : ""}${notes ? `
|
|
8477
8670
|
Notes: ${notes}` : ""}`;
|
|
8478
8671
|
}
|
|
8479
8672
|
function formatBoardForPlan(tasks, filters, currentCycle) {
|
|
@@ -8542,6 +8735,36 @@ function trendArrow(current, previous, higherIsBetter) {
|
|
|
8542
8735
|
const improving = higherIsBetter ? current > previous : current < previous;
|
|
8543
8736
|
return improving ? " \u2191" : " \u2193";
|
|
8544
8737
|
}
|
|
8738
|
+
var EFFORT_MAP2 = { XS: 1, S: 2, M: 3, L: 5, XL: 8 };
|
|
8739
|
+
function computeSnapshotsFromBuildReports(reports) {
|
|
8740
|
+
if (reports.length === 0) return [];
|
|
8741
|
+
const byCycleMap = /* @__PURE__ */ new Map();
|
|
8742
|
+
for (const r of reports) {
|
|
8743
|
+
const existing = byCycleMap.get(r.cycle) ?? [];
|
|
8744
|
+
existing.push(r);
|
|
8745
|
+
byCycleMap.set(r.cycle, existing);
|
|
8746
|
+
}
|
|
8747
|
+
const snapshots = [];
|
|
8748
|
+
for (const [sn, cycleReports] of byCycleMap) {
|
|
8749
|
+
const completed = cycleReports.filter((r) => r.completed === "Yes").length;
|
|
8750
|
+
const total = cycleReports.length;
|
|
8751
|
+
const withEffort = cycleReports.filter((r) => r.estimatedEffort && r.actualEffort);
|
|
8752
|
+
const accurate = withEffort.filter((r) => r.estimatedEffort === r.actualEffort).length;
|
|
8753
|
+
const matchRate = withEffort.length > 0 ? Math.round(accurate / withEffort.length * 100) : 0;
|
|
8754
|
+
let effortPoints = 0;
|
|
8755
|
+
for (const r of cycleReports) {
|
|
8756
|
+
effortPoints += EFFORT_MAP2[r.actualEffort] ?? 3;
|
|
8757
|
+
}
|
|
8758
|
+
snapshots.push({
|
|
8759
|
+
cycle: sn,
|
|
8760
|
+
date: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8761
|
+
accuracy: [{ cycle: sn, reports: total, matchRate, mae: 0, bias: 0 }],
|
|
8762
|
+
velocity: [{ cycle: sn, completed, partial: 0, failed: total - completed, effortPoints }]
|
|
8763
|
+
});
|
|
8764
|
+
}
|
|
8765
|
+
snapshots.sort((a, b2) => a.cycle - b2.cycle);
|
|
8766
|
+
return snapshots;
|
|
8767
|
+
}
|
|
8545
8768
|
function formatCycleMetrics(snapshots) {
|
|
8546
8769
|
if (snapshots.length === 0) return "No methodology metrics yet.";
|
|
8547
8770
|
const latest = snapshots[snapshots.length - 1];
|
|
@@ -9298,7 +9521,7 @@ After your natural language output, include this EXACT format on its own line:
|
|
|
9298
9521
|
<!-- PAPI_STRUCTURED_OUTPUT -->
|
|
9299
9522
|
\`\`\`json
|
|
9300
9523
|
{
|
|
9301
|
-
"cycleLogTitle": "string \u2014 short descriptive title WITHOUT 'Cycle N' prefix (e.g. '
|
|
9524
|
+
"cycleLogTitle": "string \u2014 short descriptive title WITHOUT 'Cycle N' prefix. Should capture the cycle theme in 3-5 words (e.g. 'MCP Quality + Product Readiness' not 'Cycle 5 \u2014 Board Triage \u2014 Bug Fix'). This is the canonical theme label for the cycle.",
|
|
9302
9525
|
"cycleLogContent": "string \u2014 5-10 line cycle log body in markdown, NO heading (the ### heading is generated automatically)",
|
|
9303
9526
|
"cycleLogCarryForward": "string or null \u2014 carry-forward items for next cycle",
|
|
9304
9527
|
"cycleLogNotes": "string or null \u2014 1-3 lines of cycle-level observations: estimation accuracy, recurring blockers, velocity trends, dependency signals. Omit if no noteworthy observations.",
|
|
@@ -9439,6 +9662,7 @@ Standard planning cycle with full board review.
|
|
|
9439
9662
|
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
9663
|
**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
9664
|
**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.
|
|
9665
|
+
**Theme coherence:** After selecting candidate tasks, check whether they form a coherent theme \u2014 all serving one goal, phase, or module. Single-theme cycles produce better build quality and less context switching. If the top candidates touch 3+ unrelated modules or epics, prefer regrouping around the highest-priority theme and deferring the outliers. Mixed-theme cycles are acceptable when justified (e.g. a P0 fix alongside P1 feature work), but the justification must appear in the cycle log. Name the theme in 3-5 words \u2014 it becomes the \`cycleLogTitle\`.
|
|
9442
9666
|
|
|
9443
9667
|
8. **Cycle Log** \u2014 Write 5-10 line entry: what was triaged, what was recommended and why, observations, AD updates.
|
|
9444
9668
|
**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.
|
|
@@ -9461,10 +9685,29 @@ Standard planning cycle with full board review.
|
|
|
9461
9685
|
**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).
|
|
9462
9686
|
**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
9687
|
**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.
|
|
9464
|
-
**
|
|
9465
|
-
|
|
9466
|
-
|
|
9467
|
-
|
|
9688
|
+
**Research task detection:** When a task's title starts with "Research:" or the task type is "research", add a RESEARCH OUTPUT section to the BUILD HANDOFF after ACCEPTANCE CRITERIA:
|
|
9689
|
+
|
|
9690
|
+
RESEARCH OUTPUT
|
|
9691
|
+
Deliverable: docs/research/[topic]-findings.md (draft path)
|
|
9692
|
+
Review status: pending owner approval
|
|
9693
|
+
Follow-up tasks: DO NOT submit to backlog until owner confirms findings are actionable
|
|
9694
|
+
|
|
9695
|
+
Also add to ACCEPTANCE CRITERIA: "[ ] Findings doc drafted and saved to docs/research/ before submitting any follow-up ideas"
|
|
9696
|
+
|
|
9697
|
+
**Bug task detection:** When a task's task type is "bug" or the title starts with "Bug:" or "Fix:", apply these rules:
|
|
9698
|
+
- **Auto-P1:** If the task's current priority is P2 or lower, upgrade it to "P1 High" via a boardCorrections entry in Part 2. Note the upgrade in Part 1 analysis.
|
|
9699
|
+
- Add a BLAST RADIUS note to the BUILD HANDOFF SCOPE section: "Bug fix \u2014 minimal blast radius. Change only what is necessary to fix the reported behaviour. Do not refactor surrounding code or expand scope."
|
|
9700
|
+
- Add to ACCEPTANCE CRITERIA: "[ ] Fix is targeted \u2014 no unrelated code changed"
|
|
9701
|
+
|
|
9702
|
+
**Idea task detection:** When a task's task type is "idea", add a scope clarification note to the BUILD HANDOFF:
|
|
9703
|
+
- Add to SCOPE (DO THIS): "This task originated as an idea. Confirm the exact deliverable before implementing \u2014 check task notes and any referenced docs for intent. If scope is unclear, flag it in the build report surprises."
|
|
9704
|
+
|
|
9705
|
+
**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", "cockpit", "dashboard", "page"), apply these handoff additions:
|
|
9706
|
+
- Add to SCOPE: "Read \`.impeccable.md\` for brand palette, design principles, and audience context before writing any code. Use the \`frontend-design\` skill for implementation."
|
|
9707
|
+
- For M/L UI tasks, add to SCOPE: "Use the full UI toolchain: Playground (design preview) \u2192 Frontend-design (build) \u2192 Playwright (verify). The playground is the quality bar. Expect 2-3 iterations."
|
|
9708
|
+
- Add to ACCEPTANCE CRITERIA: "[ ] Visually verify rendered output in browser \u2014 provide localhost URL or screenshot to user for review." and "[ ] No raw IDs, abbreviations, or jargon visible without human-readable labels or tooltips."
|
|
9709
|
+
- If the task involves image selection, add to SCOPE: "Include brand/theme direction constraints for image selection."
|
|
9710
|
+
The planner's job is scoping, not design direction. Design decisions happen at build time via \`.impeccable.md\` and the frontend-design skill \u2014 don't try to write design specs in the handoff.
|
|
9468
9711
|
|
|
9469
9712
|
11. **New Tasks (max 3 per cycle)** \u2014 Actively mine the Recent Build Reports for task candidates. For each report, check:
|
|
9470
9713
|
- **Discovered Issues:** If a build report lists a discovered issue and no existing board task covers it, propose a new task.
|
|
@@ -9659,115 +9902,84 @@ IMPORTANT: You are running as a non-interactive API call. Do NOT ask the user qu
|
|
|
9659
9902
|
|
|
9660
9903
|
## OUTPUT PRINCIPLES
|
|
9661
9904
|
|
|
9662
|
-
- **
|
|
9905
|
+
- **Product impact first, process second.** The reader wants to know: what got better for users, what's broken, what opportunities exist. Internal machinery (AD wording, taxonomy labels, hierarchy status) is secondary \u2014 handle it in a compact appendix, not the main review.
|
|
9663
9906
|
- **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."
|
|
9664
9907
|
- **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.
|
|
9665
|
-
- **
|
|
9908
|
+
- **Be concise and scannable.** Use short paragraphs, bullet points, and clear headings. Avoid walls of text. The review should be readable in 3 minutes, not 15. Format cycle summaries as compact bullet points, not multi-paragraph narratives.
|
|
9909
|
+
- **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.
|
|
9910
|
+
- **AD housekeeping is an appendix, not the centerpiece.** Just list changes and make them. Don't score every AD individually. Don't ask for approval on wording tweaks \u2014 small changes (confidence bumps, deleting stale ADs, fixing wording) should just happen. Only flag ADs that represent a genuine strategic question.
|
|
9666
9911
|
|
|
9667
9912
|
## TWO-PHASE DELIVERY
|
|
9668
9913
|
|
|
9669
|
-
|
|
9670
|
-
|
|
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.
|
|
9914
|
+
1. **Phase 1 (this output):** Present the review \u2014 all 5 mandatory sections plus relevant conditional sections. Be thorough on product gaps and opportunities, compact on housekeeping. The user will discuss and refine before the structured output is applied.
|
|
9915
|
+
2. **Phase 2 (after user discussion):** The structured data in Part 2 captures actions. The user may modify these after reading Phase 1.
|
|
9672
9916
|
|
|
9673
|
-
|
|
9917
|
+
The review should be readable in one sitting. Don't pad sections for depth \u2014 earn every paragraph with a specific insight or actionable finding.
|
|
9674
9918
|
|
|
9675
9919
|
## YOUR JOB \u2014 STRUCTURED COVERAGE
|
|
9676
9920
|
|
|
9677
|
-
You MUST cover these
|
|
9921
|
+
You MUST cover these 5 sections. Each is mandatory.
|
|
9678
9922
|
|
|
9679
|
-
1. **
|
|
9923
|
+
1. **What Got Built & Why It Matters** \u2014 Compact summary of each cycle since last review. For each cycle: 1-2 bullet points on what shipped and the strategic significance. Reference task IDs. Don't write multi-paragraph narratives per cycle \u2014 keep it scannable. End with the cross-cycle pattern or theme.
|
|
9680
9924
|
|
|
9681
|
-
2. **
|
|
9925
|
+
2. **Product Gaps & User Experience** \u2014 This is the MOST IMPORTANT section. Answer these questions with specifics:
|
|
9926
|
+
- If a new user tried this product tomorrow, what would confuse or break for them?
|
|
9927
|
+
- What features are broken, half-built, or misleading on the dashboard/UI?
|
|
9928
|
+
- What's missing that would make the product noticeably better?
|
|
9929
|
+
- What user experience friction has been reported or observed in dogfood/build reports?
|
|
9930
|
+
Surface real problems, not theoretical ones. Reference specific pages, components, or flows that need attention.
|
|
9682
9931
|
|
|
9683
|
-
3. **
|
|
9932
|
+
3. **Opportunities & Growth** \u2014 What's not being built or explored that could move the needle?
|
|
9933
|
+
- Marketing, distribution, community, content opportunities
|
|
9934
|
+
- Methodology improvements that would make cycles more efficient
|
|
9935
|
+
- Features or improvements that would differentiate from competitors
|
|
9936
|
+
- Things the project owner mentioned wanting but that haven't been prioritised
|
|
9684
9937
|
|
|
9685
|
-
4. **
|
|
9938
|
+
4. **Strategic Direction Check** \u2014 Brief assessment (not a deep dive):
|
|
9939
|
+
- Is the North Star still accurate? (1-2 sentences, not a multi-paragraph analysis)
|
|
9940
|
+
- Has the target user or problem statement changed?
|
|
9941
|
+
- What carry-forward items are stuck and why?
|
|
9942
|
+
- If the product brief needs updating, include the update in Part 2.
|
|
9686
9943
|
|
|
9687
|
-
5. **
|
|
9688
|
-
-
|
|
9689
|
-
-
|
|
9690
|
-
-
|
|
9691
|
-
-
|
|
9692
|
-
|
|
9944
|
+
5. **AD & Hierarchy Housekeeping** \u2014 Compact appendix. NOT the focus of the review.
|
|
9945
|
+
- List ADs being deleted, modified, or created \u2014 with the change, not a per-AD essay
|
|
9946
|
+
- Just make small changes (confidence bumps, stale AD deletion, wording fixes) \u2014 don't ask for approval
|
|
9947
|
+
- Only flag ADs that represent a genuine strategic question requiring owner input
|
|
9948
|
+
- Note any hierarchy/phase issues worth correcting (1-2 bullets max)
|
|
9949
|
+
- Delete ADs that are legacy, process-level, or redundant without discussion
|
|
9693
9950
|
|
|
9694
|
-
|
|
9695
|
-
- **effort** \u2014 Implementation cost (1=trivial, 5=major project)
|
|
9696
|
-
- **risk** \u2014 Likelihood of failure or rework (1=safe, 5=unproven)
|
|
9697
|
-
- **reversibility** \u2014 How hard to undo (1=trivial rollback, 5=permanent)
|
|
9698
|
-
- **scale_cost** \u2014 What this costs at 10x/100x users or data (1=negligible, 5=bottleneck)
|
|
9699
|
-
- **lock_in** \u2014 Dependency on a specific vendor/tool (1=swappable, 5=deeply coupled)
|
|
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\`.
|
|
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.
|
|
9951
|
+
## CONDITIONAL SECTIONS (include only when genuinely useful \u2014 most reviews should have 0-2 of these)
|
|
9703
9952
|
|
|
9704
|
-
|
|
9953
|
+
6. **Security Posture Review** \u2014 Only if \`[SECURITY]\` tags exist in recent cycle logs.
|
|
9705
9954
|
|
|
9706
|
-
7. **
|
|
9955
|
+
7. **Dogfood Friction \u2192 Task Conversion** \u2014 Scan dogfood observations for recurring friction (2+ occurrences without a board task). Convert up to 3 to task proposals via \`actionItems\` in Part 2. Skip if nothing recurring.
|
|
9707
9956
|
|
|
9708
|
-
8. **
|
|
9709
|
-
**How to convert friction to tasks:**
|
|
9710
|
-
- In Part 1, write a "Dogfood Friction \u2192 Tasks" subsection listing each friction entry and your decision (convert or skip with reason).
|
|
9711
|
-
- For each converted friction, add an entry to \`actionItems\` in Part 2 with \`type: "submit"\` and a descriptive \`description\` that includes the task title and scope. Example: \`{"description": "Submit task: Fix deprioritise clearing handoffs unnecessarily \u2014 add flag to preserve handoff on deprioritise", "type": "submit", "target": null}\`
|
|
9712
|
-
- If friction points have been addressed by recent builds, note the resolution and skip them.
|
|
9713
|
-
- This closes the loop between "we noticed a problem" and "we created a task to fix it."
|
|
9957
|
+
8. **Architecture Health** \u2014 Only flag genuine broken data paths, config drift, or dead code. Keep it brief \u2014 bullet points, not paragraphs. Skip entirely if nothing found.
|
|
9714
9958
|
${compressionJob}
|
|
9715
|
-
|
|
9716
|
-
- **Broken data paths** \u2014 DB tables that exist but aren't being read by the dashboard, file reads returning empty, API routes with no consumers. Cycle 42 showed an empty product brief going undetected for multiple cycles \u2014 this check catches that class of problem.
|
|
9717
|
-
- **Adapter parity gaps** \u2014 Features implemented in the pg adapter but missing from md (or vice versa). Both adapters must implement the same PapiAdapter interface, but runtime behavior can diverge.
|
|
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.
|
|
9719
|
-
- **Dead dependencies** \u2014 Packages in package.json that are no longer imported anywhere. These add install time and attack surface.
|
|
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.
|
|
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".
|
|
9723
|
-
|
|
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):
|
|
9725
|
-
- If the section is **empty** and the project has run 5+ cycles, flag it as a gap and suggest a specific enrichment prompt (e.g. "Consider defining your MVP boundary \u2014 what's in v1 and what's deferred?").
|
|
9726
|
-
- If the section has content, assess whether it's still accurate given recent builds and decisions. Flag stale assumptions or outdated references.
|
|
9727
|
-
- If no Discovery Canvas is provided in context, note that the canvas hasn't been initialized and recommend starting with the highest-value section for the project's maturity.
|
|
9728
|
-
Report findings in a "Discovery Canvas Audit" section in Part 1. Persist findings in the \`discoveryGaps\` array in Part 2. If no gaps found, omit the section and use an empty array.
|
|
9729
|
-
|
|
9730
|
-
11. **Hierarchy Assessment** \u2014 If hierarchy data (Horizons \u2192 Stages \u2192 Phases with task counts) is provided in context, assess the full project structure:
|
|
9731
|
-
**Phase-level:**
|
|
9732
|
-
- A phase marked "In Progress" with all tasks Done \u2192 flag as ready to close.
|
|
9733
|
-
- A phase marked "Done" with active Backlog/In Progress tasks \u2192 flag as incorrectly closed.
|
|
9734
|
-
- A phase marked "Not Started" while later-ordered phases are active \u2192 flag as out-of-sequence.
|
|
9735
|
-
- If builds in this review window created tasks that don't fit existing phases \u2192 suggest a new phase.
|
|
9736
|
-
**Stage-level:**
|
|
9737
|
-
- If all phases in a stage are Done \u2192 flag the stage as ready to complete. This is a significant milestone.
|
|
9738
|
-
- If the current stage has been active for 15+ cycles \u2192 assess whether it should be split or whether progress is genuinely slow.
|
|
9739
|
-
- If work is happening in phases that belong to a future stage while the current stage has incomplete phases \u2192 flag as scope leak.
|
|
9740
|
-
**Horizon-level:**
|
|
9741
|
-
- If all stages in the active horizon are complete \u2192 flag for Horizon Review (biggest-picture reflection).
|
|
9742
|
-
- If no phase data is provided, skip this section.
|
|
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.
|
|
9744
|
-
|
|
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.
|
|
9959
|
+
Note: Hierarchy assessment and structural drift detection are handled within section 5 (AD & Hierarchy Housekeeping). They do not need their own sections.
|
|
9750
9960
|
|
|
9751
9961
|
## OUTPUT FORMAT
|
|
9752
9962
|
|
|
9753
9963
|
Your output has TWO parts:
|
|
9754
9964
|
|
|
9755
9965
|
### Part 1: Natural Language Output
|
|
9756
|
-
Write your
|
|
9757
|
-
1. **
|
|
9758
|
-
2. **
|
|
9759
|
-
3. **
|
|
9760
|
-
4. **
|
|
9761
|
-
5. **
|
|
9762
|
-
|
|
9763
|
-
|
|
9764
|
-
Then include conditional sections only if relevant:
|
|
9966
|
+
Write your Strategy Review in markdown. Cover the 5 mandatory sections in order:
|
|
9967
|
+
1. **What Got Built & Why It Matters** \u2014 compact cycle summaries (bullets, not essays)
|
|
9968
|
+
2. **Product Gaps & User Experience** \u2014 THE MAIN EVENT. What's broken, confusing, or missing for users.
|
|
9969
|
+
3. **Opportunities & Growth** \u2014 what could move the needle that we're not doing
|
|
9970
|
+
4. **Strategic Direction Check** \u2014 brief North Star + carry-forward + brief update check
|
|
9971
|
+
5. **AD & Hierarchy Housekeeping** \u2014 compact appendix of changes being made
|
|
9972
|
+
|
|
9973
|
+
Then include conditional sections only if genuinely useful:
|
|
9765
9974
|
- **Security Posture Review** \u2014 only if [SECURITY] tags exist
|
|
9766
|
-
- **Dogfood Friction \u2192 Tasks** \u2014 only if
|
|
9767
|
-
- **Architecture Health** \u2014 only if
|
|
9768
|
-
|
|
9769
|
-
|
|
9770
|
-
-
|
|
9975
|
+
- **Dogfood Friction \u2192 Tasks** \u2014 only if recurring unaddressed friction
|
|
9976
|
+
- **Architecture Health** \u2014 only if broken data paths or config drift found${compressionPart1}
|
|
9977
|
+
|
|
9978
|
+
**FORMAT GUIDELINES:**
|
|
9979
|
+
- The entire review should be readable in 3-5 minutes
|
|
9980
|
+
- Use bullet points and short paragraphs, not walls of text
|
|
9981
|
+
- Section 2 (Product Gaps) should be the longest section
|
|
9982
|
+
- Section 5 (AD Housekeeping) should be the shortest \u2014 just a change list
|
|
9771
9983
|
|
|
9772
9984
|
### Part 2: Structured Data Block
|
|
9773
9985
|
After your natural language output, include this EXACT format on its own line:
|
|
@@ -9848,9 +10060,9 @@ Everything in Part 1 (natural language) is **display-only**. Part 2 (structured
|
|
|
9848
10060
|
|
|
9849
10061
|
**If you analysed it in Part 1, it MUST appear in Part 2 to persist. Empty arrays/null = nothing saved.**
|
|
9850
10062
|
|
|
9851
|
-
-
|
|
9852
|
-
-
|
|
9853
|
-
-
|
|
10063
|
+
- AD changes listed in section 5? \u2192 Put them in \`activeDecisionUpdates\` with full body including ### heading. Use \`delete\` action (with empty body) to permanently remove non-strategic ADs. Small changes (confidence bumps, stale deletions) don't need justification essays \u2014 just make them.
|
|
10064
|
+
- Only score ADs in \`decisionScores\` if the review surfaced a genuine strategic question about that AD. Do NOT score every AD \u2014 most reviews should have 0-3 scores at most.
|
|
10065
|
+
- Product brief needs updating? \u2192 Put the full updated brief in \`productBriefUpdates\`.${compressionPersistence}
|
|
9854
10066
|
- Wrote a strategy review in Part 1? \u2192 \`sessionLogTitle\`, \`sessionLogContent\`, \`velocityAssessment\`, \`strategicRecommendations\` must all be populated
|
|
9855
10067
|
- Made recommendations in Part 1? \u2192 Extract each into \`actionItems\` with a specific type (resolve/submit/close/investigate/defer) and target (AD-N, task-NNN, phase name, or null). Every recommendation must have an action item \u2014 this is how they get tracked and surfaced to the next plan
|
|
9856
10068
|
- Converted dogfood friction to tasks in Part 1? \u2192 Each converted friction must appear as an \`actionItem\` with \`type: "submit"\`. If it's not in \`actionItems\`, it won't be tracked \u2014 the next plan will never see it
|
|
@@ -9944,6 +10156,9 @@ function buildReviewUserMessage(ctx) {
|
|
|
9944
10156
|
if (ctx.unregisteredDocs) {
|
|
9945
10157
|
parts.push("### Unregistered Docs", "", ctx.unregisteredDocs, "");
|
|
9946
10158
|
}
|
|
10159
|
+
if (ctx.taskComments) {
|
|
10160
|
+
parts.push("### Task Discussion (Recent Comments)", "", ctx.taskComments, "");
|
|
10161
|
+
}
|
|
9947
10162
|
return parts.join("\n");
|
|
9948
10163
|
}
|
|
9949
10164
|
function parseReviewStructuredOutput(raw) {
|
|
@@ -10657,10 +10872,15 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
|
|
|
10657
10872
|
timings["getPlanContextSummary"] = t();
|
|
10658
10873
|
if (leanSummary) {
|
|
10659
10874
|
t = startTimer();
|
|
10660
|
-
|
|
10875
|
+
let [metricsSnapshots2, reviews2] = await Promise.all([
|
|
10661
10876
|
adapter2.readCycleMetrics(),
|
|
10662
10877
|
adapter2.getRecentReviews(5)
|
|
10663
10878
|
]);
|
|
10879
|
+
try {
|
|
10880
|
+
const reports2 = await adapter2.getRecentBuildReports(50);
|
|
10881
|
+
metricsSnapshots2 = computeSnapshotsFromBuildReports(reports2);
|
|
10882
|
+
} catch {
|
|
10883
|
+
}
|
|
10664
10884
|
timings["metricsAndReviews"] = t();
|
|
10665
10885
|
t = startTimer();
|
|
10666
10886
|
let reviewPatternsText2;
|
|
@@ -10729,7 +10949,7 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
|
|
|
10729
10949
|
return { context: ctx2, contextHashes: newHashes2 };
|
|
10730
10950
|
}
|
|
10731
10951
|
t = startTimer();
|
|
10732
|
-
const [decisions, reportsSinceCycle, log, tasks,
|
|
10952
|
+
const [decisions, reportsSinceCycle, log, tasks, rawMetricsSnapshots, reviews, phases, dogfoodEntries] = await Promise.all([
|
|
10733
10953
|
adapter2.getActiveDecisions(),
|
|
10734
10954
|
adapter2.getBuildReportsSince(health.totalCycles),
|
|
10735
10955
|
adapter2.getCycleLog(3),
|
|
@@ -10743,8 +10963,9 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
|
|
|
10743
10963
|
const reports = reportsSinceCycle.length > 0 ? reportsSinceCycle : await adapter2.getRecentBuildReports(5);
|
|
10744
10964
|
t = startTimer();
|
|
10745
10965
|
let buildPatternsText;
|
|
10966
|
+
let allReportsForPatterns = [];
|
|
10746
10967
|
try {
|
|
10747
|
-
|
|
10968
|
+
allReportsForPatterns = await adapter2.getRecentBuildReports(50);
|
|
10748
10969
|
const patterns = await detectBuildPatterns(allReportsForPatterns, health.totalCycles, 5);
|
|
10749
10970
|
buildPatternsText = hasBuildPatterns(patterns) ? formatBuildPatterns(patterns) : void 0;
|
|
10750
10971
|
} catch {
|
|
@@ -10764,6 +10985,7 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
|
|
|
10764
10985
|
} catch {
|
|
10765
10986
|
}
|
|
10766
10987
|
timings["advisory"] = t();
|
|
10988
|
+
const metricsSnapshots = allReportsForPatterns.length > 0 ? computeSnapshotsFromBuildReports(allReportsForPatterns) : rawMetricsSnapshots.filter((s) => s.accuracy.length > 0 || s.velocity.length > 0);
|
|
10767
10989
|
logDataSourceSummary("plan (full)", [
|
|
10768
10990
|
{ label: "cycleHealth", hasData: !!health },
|
|
10769
10991
|
{ label: "productBrief", hasData: warnIfEmpty("productBrief", productBrief) },
|
|
@@ -10822,7 +11044,7 @@ ${cleanContent}`;
|
|
|
10822
11044
|
pendingRecIds = pending.map((r) => r.id);
|
|
10823
11045
|
} catch {
|
|
10824
11046
|
}
|
|
10825
|
-
const
|
|
11047
|
+
const handoffs2 = (data.cycleHandoffs ?? []).map((h) => {
|
|
10826
11048
|
const parsed = parseBuildHandoff(h.buildHandoff);
|
|
10827
11049
|
if (parsed && !parsed.createdAt) {
|
|
10828
11050
|
parsed.createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -10854,6 +11076,15 @@ ${cleanContent}`;
|
|
|
10854
11076
|
});
|
|
10855
11077
|
}
|
|
10856
11078
|
const reviewedTaskIds = (data.boardCorrections ?? []).filter((c) => "priority" in c.updates).map((c) => c.taskId);
|
|
11079
|
+
let activeStageId;
|
|
11080
|
+
try {
|
|
11081
|
+
const activeStage = await adapter2.getActiveStage?.();
|
|
11082
|
+
activeStageId = activeStage?.id;
|
|
11083
|
+
} catch {
|
|
11084
|
+
}
|
|
11085
|
+
const effortMapLegacy = { XS: 1, S: 2, M: 3, L: 5, XL: 8 };
|
|
11086
|
+
const legacyTaskCount = handoffs2.length;
|
|
11087
|
+
const legacyEffortPoints = handoffs2.reduce((sum, h) => sum + (effortMapLegacy[h.handoff.effort] ?? 3), 0);
|
|
10857
11088
|
const payload = {
|
|
10858
11089
|
cycleNumber: newCycleNumber,
|
|
10859
11090
|
cycleLog: {
|
|
@@ -10862,7 +11093,9 @@ ${cleanContent}`;
|
|
|
10862
11093
|
title: cleanTitle,
|
|
10863
11094
|
content: logBlock,
|
|
10864
11095
|
carryForward: data.cycleLogCarryForward ?? void 0,
|
|
10865
|
-
notes: data.cycleLogNotes ?? void 0
|
|
11096
|
+
notes: data.cycleLogNotes ?? void 0,
|
|
11097
|
+
taskCount: legacyTaskCount > 0 ? legacyTaskCount : void 0,
|
|
11098
|
+
effortPoints: legacyEffortPoints > 0 ? legacyEffortPoints : void 0
|
|
10866
11099
|
},
|
|
10867
11100
|
healthUpdates: {
|
|
10868
11101
|
totalCycles: newCycleNumber,
|
|
@@ -10882,19 +11115,26 @@ ${cleanContent}`;
|
|
|
10882
11115
|
cycle: newCycleNumber,
|
|
10883
11116
|
createdCycle: newCycleNumber,
|
|
10884
11117
|
why: t.why || "",
|
|
10885
|
-
notes: t.notes || ""
|
|
10886
|
-
|
|
10887
|
-
boardCorrections: (data.boardCorrections ?? []).map((c) => ({
|
|
10888
|
-
taskId: c.taskId,
|
|
10889
|
-
updates: c.updates
|
|
11118
|
+
notes: t.notes || "",
|
|
11119
|
+
stageId: activeStageId
|
|
10890
11120
|
})),
|
|
11121
|
+
boardCorrections: (data.boardCorrections ?? []).map((c) => {
|
|
11122
|
+
const updates = c.updates;
|
|
11123
|
+
if ((updates.status === "Deferred" || updates.status === "Cancelled") && !updates.closureReason) {
|
|
11124
|
+
updates.closureReason = "No reason provided";
|
|
11125
|
+
}
|
|
11126
|
+
return {
|
|
11127
|
+
taskId: c.taskId,
|
|
11128
|
+
updates
|
|
11129
|
+
};
|
|
11130
|
+
}),
|
|
10891
11131
|
phases,
|
|
10892
11132
|
activeDecisions: (data.activeDecisions ?? []).map((ad) => ({
|
|
10893
11133
|
id: ad.id,
|
|
10894
11134
|
body: ad.body
|
|
10895
11135
|
})),
|
|
10896
11136
|
pendingRecommendationIds: pendingRecIds,
|
|
10897
|
-
handoffs,
|
|
11137
|
+
handoffs: handoffs2,
|
|
10898
11138
|
cycle,
|
|
10899
11139
|
reviewedTaskIds
|
|
10900
11140
|
};
|
|
@@ -10942,13 +11182,18 @@ async function writeBack(adapter2, _mode, cycleNumber, data, contextHashes) {
|
|
|
10942
11182
|
const logBlock = `### Cycle ${newCycleNumber} \u2014 ${cleanTitle}
|
|
10943
11183
|
|
|
10944
11184
|
${cleanContent}`;
|
|
11185
|
+
const effortMap = { XS: 1, S: 2, M: 3, L: 5, XL: 8 };
|
|
11186
|
+
const cycleTaskCount = handoffs.length;
|
|
11187
|
+
const cycleEffortPoints = handoffs.reduce((sum, h) => sum + (effortMap[h.handoff.effort] ?? 3), 0);
|
|
10945
11188
|
const cycleLogPromise = adapter2.writeCycleLogEntry({
|
|
10946
11189
|
uuid: randomUUID7(),
|
|
10947
11190
|
cycleNumber: newCycleNumber,
|
|
10948
11191
|
title: cleanTitle,
|
|
10949
11192
|
content: logBlock,
|
|
10950
11193
|
carryForward: data.cycleLogCarryForward ?? void 0,
|
|
10951
|
-
notes: data.cycleLogNotes ?? void 0
|
|
11194
|
+
notes: data.cycleLogNotes ?? void 0,
|
|
11195
|
+
taskCount: cycleTaskCount > 0 ? cycleTaskCount : void 0,
|
|
11196
|
+
effortPoints: cycleEffortPoints > 0 ? cycleEffortPoints : void 0
|
|
10952
11197
|
});
|
|
10953
11198
|
const healthPromise = adapter2.getCycleHealth().then(
|
|
10954
11199
|
(health) => adapter2.setCycleHealth({
|
|
@@ -11024,6 +11269,9 @@ ${cleanContent}`;
|
|
|
11024
11269
|
`${correction.taskId}: planner suggested priority change to ${_stripped} \u2014 blocked (reviewed task)`
|
|
11025
11270
|
);
|
|
11026
11271
|
}
|
|
11272
|
+
if ((updates.status === "Deferred" || updates.status === "Cancelled") && !updates.closureReason) {
|
|
11273
|
+
updates.closureReason = "No reason provided";
|
|
11274
|
+
}
|
|
11027
11275
|
if (Object.keys(updates).length > 0) {
|
|
11028
11276
|
await adapter2.updateTask(correction.taskId, updates);
|
|
11029
11277
|
}
|
|
@@ -11213,7 +11461,8 @@ Run \`strategy_review\` first, or pass \`force: true\` to bypass this gate.`
|
|
|
11213
11461
|
}
|
|
11214
11462
|
return { mode, cycleNumber, strategyReviewWarning };
|
|
11215
11463
|
}
|
|
11216
|
-
async function processLlmOutput(adapter2, config2, rawOutput, mode, cycleNumber, contextHashes) {
|
|
11464
|
+
async function processLlmOutput(adapter2, config2, rawOutput, mode, cycleNumber, contextHashes, planRunMeta) {
|
|
11465
|
+
const applyStartMs = Date.now();
|
|
11217
11466
|
const { displayText, data } = parseStructuredOutput(rawOutput);
|
|
11218
11467
|
let resolvedDisplayText = displayText;
|
|
11219
11468
|
let autoCommitNote = "";
|
|
@@ -11236,6 +11485,18 @@ async function processLlmOutput(adapter2, config2, rawOutput, mode, cycleNumber,
|
|
|
11236
11485
|
for (const [placeholder, realId] of newTaskIdMap) {
|
|
11237
11486
|
resolvedDisplayText = resolvedDisplayText.replaceAll(placeholder, realId);
|
|
11238
11487
|
}
|
|
11488
|
+
if (adapter2.insertPlanRun) {
|
|
11489
|
+
const durationMs = Date.now() - applyStartMs + (planRunMeta?.prepareStartMs !== void 0 ? Date.now() - planRunMeta.prepareStartMs : 0);
|
|
11490
|
+
adapter2.insertPlanRun({
|
|
11491
|
+
cycleNumber: cycleNumber + 1,
|
|
11492
|
+
contextBytes: planRunMeta?.contextBytes,
|
|
11493
|
+
durationMs,
|
|
11494
|
+
taskCountIn: planRunMeta?.taskCountIn,
|
|
11495
|
+
taskCountOut: (writeSummary?.handoffs ?? 0) + (writeSummary?.newTasks ?? 0),
|
|
11496
|
+
backlogDepth: planRunMeta?.backlogDepth
|
|
11497
|
+
}).catch(() => {
|
|
11498
|
+
});
|
|
11499
|
+
}
|
|
11239
11500
|
if (mode === "bootstrap") {
|
|
11240
11501
|
try {
|
|
11241
11502
|
await adapter2.appendToolMetric({
|
|
@@ -11321,7 +11582,7 @@ async function preparePlan(adapter2, config2, filters, focus, force) {
|
|
|
11321
11582
|
contextHashes
|
|
11322
11583
|
};
|
|
11323
11584
|
}
|
|
11324
|
-
async function applyPlan(adapter2, config2, rawLlmOutput, mode, cycleNumber, strategyReviewWarning, contextHashes) {
|
|
11585
|
+
async function applyPlan(adapter2, config2, rawLlmOutput, mode, cycleNumber, strategyReviewWarning, contextHashes, planRunMeta) {
|
|
11325
11586
|
const applyTimer = startTimer();
|
|
11326
11587
|
console.error(`[plan-perf] applyPlan: start (llm_response=${rawLlmOutput.length} chars)`);
|
|
11327
11588
|
const APPLY_TIMEOUT_MS = 12e4;
|
|
@@ -11330,7 +11591,7 @@ async function applyPlan(adapter2, config2, rawLlmOutput, mode, cycleNumber, str
|
|
|
11330
11591
|
);
|
|
11331
11592
|
const workPromise = (async () => {
|
|
11332
11593
|
if (config2.adapterType === "pg") {
|
|
11333
|
-
const result = await processLlmOutput(adapter2, config2, rawLlmOutput, mode, cycleNumber, contextHashes);
|
|
11594
|
+
const result = await processLlmOutput(adapter2, config2, rawLlmOutput, mode, cycleNumber, contextHashes, planRunMeta);
|
|
11334
11595
|
const applyMs2 = applyTimer();
|
|
11335
11596
|
console.error(`[plan-perf] applyPlan: total=${applyMs2}ms (pg, no git sync)`);
|
|
11336
11597
|
return { ...result, pullNote: "", strategyReviewWarning };
|
|
@@ -11338,7 +11599,7 @@ async function applyPlan(adapter2, config2, rawLlmOutput, mode, cycleNumber, str
|
|
|
11338
11599
|
const syncResult = await withBaseBranchSync(
|
|
11339
11600
|
{ projectRoot: config2.projectRoot, baseBranch: config2.baseBranch, abortOnConflict: true },
|
|
11340
11601
|
async () => {
|
|
11341
|
-
return processLlmOutput(adapter2, config2, rawLlmOutput, mode, cycleNumber, contextHashes);
|
|
11602
|
+
return processLlmOutput(adapter2, config2, rawLlmOutput, mode, cycleNumber, contextHashes, planRunMeta);
|
|
11342
11603
|
}
|
|
11343
11604
|
);
|
|
11344
11605
|
if (syncResult.abort) {
|
|
@@ -11549,6 +11810,7 @@ function formatPlanResult(result) {
|
|
|
11549
11810
|
if (result.priorityLockNote) lines.push(result.priorityLockNote.trim());
|
|
11550
11811
|
if (result.slackWarning) lines.push(result.slackWarning);
|
|
11551
11812
|
if (result.autoCommitNote) lines.push(result.autoCommitNote.trim());
|
|
11813
|
+
lines.push("", `Next: run \`build_list\` to see your cycle tasks, then \`build_execute <task_id>\` to start building.`);
|
|
11552
11814
|
const response = textResponse(lines.join("\n"));
|
|
11553
11815
|
if (result.contextUtilisation !== void 0) {
|
|
11554
11816
|
return { ...response, _contextUtilisation: result.contextUtilisation };
|
|
@@ -11579,7 +11841,7 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
11579
11841
|
lastPrepareContextHashes = void 0;
|
|
11580
11842
|
lastPrepareUserMessage = void 0;
|
|
11581
11843
|
lastPrepareContextBytes = void 0;
|
|
11582
|
-
const result = await applyPlan(adapter2, config2, llmResponse, planMode, cycleNumber, strategyReviewWarning, contextHashes);
|
|
11844
|
+
const result = await applyPlan(adapter2, config2, llmResponse, planMode, cycleNumber, strategyReviewWarning, contextHashes, { contextBytes: contextBytes ?? void 0 });
|
|
11583
11845
|
let utilisation;
|
|
11584
11846
|
if (inputContext) {
|
|
11585
11847
|
try {
|
|
@@ -11890,7 +12152,7 @@ ${phaseSummary}`);
|
|
|
11890
12152
|
return parts.join("\n");
|
|
11891
12153
|
}
|
|
11892
12154
|
function formatTaskCompact(t) {
|
|
11893
|
-
const notesSnippet = t.notes
|
|
12155
|
+
const notesSnippet = t.notes ?? "";
|
|
11894
12156
|
return `- **${t.id}:** ${t.title}
|
|
11895
12157
|
Status: ${t.status} | Priority: ${t.priority} | Complexity: ${t.complexity}
|
|
11896
12158
|
Module: ${t.module} | Epic: ${t.epic} | Phase: ${t.phase} | Owner: ${t.owner}
|
|
@@ -12125,6 +12387,39 @@ ${unregistered.slice(0, 10).map((f) => `- ${f}`).join("\n")}`;
|
|
|
12125
12387
|
}
|
|
12126
12388
|
} catch {
|
|
12127
12389
|
}
|
|
12390
|
+
let taskCommentsText;
|
|
12391
|
+
try {
|
|
12392
|
+
const comments = await adapter2.getRecentTaskComments?.(50);
|
|
12393
|
+
if (comments && comments.length > 0) {
|
|
12394
|
+
const doneTaskIds = new Set(recentDoneTasks.map((t) => t.id));
|
|
12395
|
+
const inReviewTasks = activeTasks.filter((t) => t.status === "In Review");
|
|
12396
|
+
const reviewWindowTaskIds = /* @__PURE__ */ new Set([
|
|
12397
|
+
...doneTaskIds,
|
|
12398
|
+
...inReviewTasks.map((t) => t.id)
|
|
12399
|
+
]);
|
|
12400
|
+
const filtered = comments.filter((c) => reviewWindowTaskIds.has(c.taskId));
|
|
12401
|
+
if (filtered.length > 0) {
|
|
12402
|
+
const lines = [];
|
|
12403
|
+
const byTask = /* @__PURE__ */ new Map();
|
|
12404
|
+
for (const c of filtered) {
|
|
12405
|
+
const group = byTask.get(c.taskId) ?? [];
|
|
12406
|
+
group.push(c);
|
|
12407
|
+
byTask.set(c.taskId, group);
|
|
12408
|
+
}
|
|
12409
|
+
for (const [taskId, taskComments] of byTask) {
|
|
12410
|
+
lines.push(`**${taskId}:**`);
|
|
12411
|
+
for (const c of taskComments) {
|
|
12412
|
+
const date = c.createdAt ? c.createdAt.slice(0, 10) : "";
|
|
12413
|
+
lines.push(` - [${c.author}${date ? `, ${date}` : ""}] ${c.content}`);
|
|
12414
|
+
}
|
|
12415
|
+
}
|
|
12416
|
+
const raw = lines.join("\n");
|
|
12417
|
+
const MAX_CHARS = 8e3;
|
|
12418
|
+
taskCommentsText = raw.length > MAX_CHARS ? "(older comments truncated)\n" + raw.slice(raw.length - MAX_CHARS) : raw;
|
|
12419
|
+
}
|
|
12420
|
+
}
|
|
12421
|
+
} catch {
|
|
12422
|
+
}
|
|
12128
12423
|
logDataSourceSummary("strategy_review_audit", [
|
|
12129
12424
|
{ label: "discoveryCanvas", hasData: discoveryCanvasText !== void 0 },
|
|
12130
12425
|
{ label: "briefImplications", hasData: briefImplicationsText !== void 0 },
|
|
@@ -12135,7 +12430,8 @@ ${unregistered.slice(0, 10).map((f) => `- ${f}`).join("\n")}`;
|
|
|
12135
12430
|
{ label: "adHocCommits", hasData: adHocCommitsText !== void 0 },
|
|
12136
12431
|
{ label: "registeredDocs", hasData: registeredDocsText !== void 0 },
|
|
12137
12432
|
{ label: "recentPlans", hasData: recentPlansText !== void 0 },
|
|
12138
|
-
{ label: "unregisteredDocs", hasData: unregisteredDocsText !== void 0 }
|
|
12433
|
+
{ label: "unregisteredDocs", hasData: unregisteredDocsText !== void 0 },
|
|
12434
|
+
{ label: "taskComments", hasData: taskCommentsText !== void 0 }
|
|
12139
12435
|
]);
|
|
12140
12436
|
const context = {
|
|
12141
12437
|
sessionNumber: cycleNumber,
|
|
@@ -12160,7 +12456,8 @@ ${unregistered.slice(0, 10).map((f) => `- ${f}`).join("\n")}`;
|
|
|
12160
12456
|
pendingRecommendations: pendingRecsText,
|
|
12161
12457
|
registeredDocs: registeredDocsText,
|
|
12162
12458
|
recentPlans: recentPlansText,
|
|
12163
|
-
unregisteredDocs: unregisteredDocsText
|
|
12459
|
+
unregisteredDocs: unregisteredDocsText,
|
|
12460
|
+
taskComments: taskCommentsText
|
|
12164
12461
|
};
|
|
12165
12462
|
const BUDGET_SOFT2 = 5e4;
|
|
12166
12463
|
const BUDGET_HARD2 = 6e4;
|
|
@@ -12292,7 +12589,7 @@ ${cleanContent}`;
|
|
|
12292
12589
|
if (ad.action === "delete" && adapter2.deleteActiveDecision) {
|
|
12293
12590
|
await adapter2.deleteActiveDecision(ad.id);
|
|
12294
12591
|
} else {
|
|
12295
|
-
await adapter2.updateActiveDecision(ad.id, ad.body, cycleNumber);
|
|
12592
|
+
await adapter2.updateActiveDecision(ad.id, ad.body, cycleNumber, ad.action);
|
|
12296
12593
|
}
|
|
12297
12594
|
const eventType = ad.action === "delete" ? "deleted" : ad.action === "confidence_change" ? "confidence_changed" : ad.action === "supersede" ? "superseded" : ad.action === "new" ? "created" : "modified";
|
|
12298
12595
|
try {
|
|
@@ -12366,6 +12663,41 @@ ${cleanContent}`;
|
|
|
12366
12663
|
} catch {
|
|
12367
12664
|
}
|
|
12368
12665
|
}
|
|
12666
|
+
try {
|
|
12667
|
+
const canvas = await adapter2.readDiscoveryCanvas();
|
|
12668
|
+
const updates = {};
|
|
12669
|
+
let populatedSections = [];
|
|
12670
|
+
if (!canvas.landscapeReferences || canvas.landscapeReferences.length === 0) {
|
|
12671
|
+
if (data.activeDecisionUpdates?.length) {
|
|
12672
|
+
const entries = data.activeDecisionUpdates.filter((ad) => ad.body && ad.action !== "delete").slice(0, 3).map((ad) => ({ name: ad.id, notes: ad.body.slice(0, 200) }));
|
|
12673
|
+
if (entries.length > 0) {
|
|
12674
|
+
updates.landscapeReferences = entries;
|
|
12675
|
+
populatedSections.push("Landscape & References");
|
|
12676
|
+
}
|
|
12677
|
+
}
|
|
12678
|
+
}
|
|
12679
|
+
if (!canvas.mvpBoundary && data.strategicDirection) {
|
|
12680
|
+
updates.mvpBoundary = `Auto-inferred from strategy review: ${data.strategicDirection}`;
|
|
12681
|
+
populatedSections.push("MVP Boundary");
|
|
12682
|
+
}
|
|
12683
|
+
if ((!canvas.assumptionsOpenQuestions || canvas.assumptionsOpenQuestions.length === 0) && data.discoveryGaps?.length) {
|
|
12684
|
+
updates.assumptionsOpenQuestions = data.discoveryGaps.map((gap) => ({
|
|
12685
|
+
text: gap.suggestion,
|
|
12686
|
+
status: "open",
|
|
12687
|
+
evidence: `Auto-inferred: ${gap.section} was ${gap.status}`
|
|
12688
|
+
}));
|
|
12689
|
+
populatedSections.push("Assumptions & Open Questions");
|
|
12690
|
+
}
|
|
12691
|
+
if ((!canvas.successSignals || canvas.successSignals.length === 0) && data.velocityAssessment) {
|
|
12692
|
+
updates.successSignals = [{ signal: "Velocity trend", metric: "tasks/cycle", target: data.velocityAssessment.slice(0, 200) }];
|
|
12693
|
+
populatedSections.push("Success Signals");
|
|
12694
|
+
}
|
|
12695
|
+
if (populatedSections.length > 0) {
|
|
12696
|
+
await adapter2.updateDiscoveryCanvas(updates);
|
|
12697
|
+
}
|
|
12698
|
+
data._canvasPopulated = populatedSections;
|
|
12699
|
+
} catch {
|
|
12700
|
+
}
|
|
12369
12701
|
try {
|
|
12370
12702
|
const [phases, boardTasks] = await Promise.all([
|
|
12371
12703
|
adapter2.readPhases(),
|
|
@@ -12417,9 +12749,18 @@ ${report}`;
|
|
|
12417
12749
|
}
|
|
12418
12750
|
} catch {
|
|
12419
12751
|
}
|
|
12752
|
+
let canvasSection = "";
|
|
12753
|
+
if (data) {
|
|
12754
|
+
const populated = data._canvasPopulated;
|
|
12755
|
+
if (populated && populated.length > 0) {
|
|
12756
|
+
canvasSection = `
|
|
12757
|
+
|
|
12758
|
+
**Discovery Canvas Auto-Populated:** ${populated.join(", ")}`;
|
|
12759
|
+
}
|
|
12760
|
+
}
|
|
12420
12761
|
const fullText = phaseChanges?.length ? `${displayText}
|
|
12421
12762
|
|
|
12422
|
-
${formatPhaseChanges(phaseChanges)}${valueReportSection}` : `${displayText}${valueReportSection}`;
|
|
12763
|
+
${formatPhaseChanges(phaseChanges)}${valueReportSection}${canvasSection}` : `${displayText}${valueReportSection}${canvasSection}`;
|
|
12423
12764
|
return {
|
|
12424
12765
|
cycleNumber,
|
|
12425
12766
|
displayText: fullText,
|
|
@@ -12731,7 +13072,7 @@ ${cleanContent}`;
|
|
|
12731
13072
|
if (ad.action === "delete" && adapter2.deleteActiveDecision) {
|
|
12732
13073
|
await adapter2.deleteActiveDecision(ad.id);
|
|
12733
13074
|
} else {
|
|
12734
|
-
await adapter2.updateActiveDecision(ad.id, ad.body, cycleNumber);
|
|
13075
|
+
await adapter2.updateActiveDecision(ad.id, ad.body, cycleNumber, ad.action);
|
|
12735
13076
|
}
|
|
12736
13077
|
const eventType = ad.action === "delete" ? "deleted" : ad.action === "confidence_change" ? "confidence_changed" : ad.action === "supersede" ? "superseded" : ad.action === "new" ? "created" : "modified";
|
|
12737
13078
|
try {
|
|
@@ -13532,12 +13873,34 @@ async function handleBoardEdit(adapter2, args) {
|
|
|
13532
13873
|
updates.cycle = null;
|
|
13533
13874
|
if (!changes.includes("cycle")) changes.push("cycle \u2192 cleared");
|
|
13534
13875
|
}
|
|
13876
|
+
let autoAssignedCycle = null;
|
|
13877
|
+
if (updates.status === "In Cycle") {
|
|
13878
|
+
const health = await adapter2.getCycleHealth().catch(() => null);
|
|
13879
|
+
const activeCycle = health?.totalCycles ?? null;
|
|
13880
|
+
if (activeCycle != null && activeCycle > 0) {
|
|
13881
|
+
updates.cycle = activeCycle;
|
|
13882
|
+
autoAssignedCycle = activeCycle;
|
|
13883
|
+
if (!changes.includes("cycle")) changes.push(`cycle \u2192 ${activeCycle}`);
|
|
13884
|
+
}
|
|
13885
|
+
}
|
|
13535
13886
|
await adapter2.updateTask(taskId, updates);
|
|
13887
|
+
if ((updates.status === "Done" || updates.status === "Cancelled") && adapter2.updateDogfoodEntryStatus) {
|
|
13888
|
+
try {
|
|
13889
|
+
const dogfoodLog = await adapter2.getDogfoodLog?.(50) ?? [];
|
|
13890
|
+
const linked = dogfoodLog.filter((e) => e.linkedTaskId === taskId || e.linkedTaskId === task.id);
|
|
13891
|
+
const newStatus = updates.status === "Done" ? "actioned" : "dismissed";
|
|
13892
|
+
await Promise.all(linked.map((e) => adapter2.updateDogfoodEntryStatus(e.id, newStatus)));
|
|
13893
|
+
} catch {
|
|
13894
|
+
}
|
|
13895
|
+
}
|
|
13536
13896
|
const lines = [
|
|
13537
13897
|
`Updated **${taskId}** (${updates.title ?? task.title})`,
|
|
13538
13898
|
"",
|
|
13539
13899
|
`**Changes:** ${changes.map((f) => `${f} \u2192 ${String(updates[f])}`).join(", ")}`
|
|
13540
13900
|
];
|
|
13901
|
+
if (autoAssignedCycle != null) {
|
|
13902
|
+
lines.push("", `Auto-assigned to Cycle ${autoAssignedCycle}.`);
|
|
13903
|
+
}
|
|
13541
13904
|
return textResponse(lines.join("\n"));
|
|
13542
13905
|
} catch (err) {
|
|
13543
13906
|
return errorResponse(err instanceof Error ? err.message : String(err));
|
|
@@ -13878,6 +14241,7 @@ These rules come from 80+ cycles of dogfooding. They prevent the most common sou
|
|
|
13878
14241
|
- **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
14242
|
- **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
14243
|
- **Never ship secrets.** Do not commit .env files, API keys, or credentials. Check \`.gitignore\` covers sensitive files before pushing.
|
|
14244
|
+
- **Telemetry opt-out.** PAPI collects anonymous usage data (tool name, duration, project ID). To disable, add \`"PAPI_TELEMETRY": "off"\` to the \`env\` block in your \`.mcp.json\`.
|
|
13881
14245
|
|
|
13882
14246
|
### Planning & Scope
|
|
13883
14247
|
- **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.
|
|
@@ -14031,6 +14395,44 @@ var DOCS_INDEX_TEMPLATE = `# {{project_name}} \u2014 Document Index
|
|
|
14031
14395
|
### Consumer column
|
|
14032
14396
|
Who or what reads this doc: \`planner\`, \`builder\`, \`strategy-review\`, \`user\`, \`onboarding\`.
|
|
14033
14397
|
`;
|
|
14398
|
+
var CURSOR_RULES_TEMPLATE = `---
|
|
14399
|
+
description: PAPI \u2014 Persistent Agentic Planning Intelligence
|
|
14400
|
+
globs:
|
|
14401
|
+
alwaysApply: true
|
|
14402
|
+
---
|
|
14403
|
+
|
|
14404
|
+
# {{project_name}} \u2014 PAPI Workflow
|
|
14405
|
+
|
|
14406
|
+
PAPI gives your project structured planning, building, and reviewing. The agent manages the cycle workflow automatically.
|
|
14407
|
+
|
|
14408
|
+
## Session Start
|
|
14409
|
+
|
|
14410
|
+
Run \`orient\` at the start of every session to get cycle state, in-progress tasks, and recommended next action.
|
|
14411
|
+
|
|
14412
|
+
## The Cycle
|
|
14413
|
+
|
|
14414
|
+
\`plan\` \u2192 \`build_list\` \u2192 \`build_execute\` (start) \u2192 implement \u2192 \`build_execute\` (complete) \u2192 \`review_list\` \u2192 \`review_submit\` \u2192 \`release\`
|
|
14415
|
+
|
|
14416
|
+
- **plan** \u2014 Generate BUILD HANDOFFs for the next cycle. Run once per cycle.
|
|
14417
|
+
- **build_execute \\<task-id\\>** (start) \u2014 Create feature branch, return BUILD HANDOFF.
|
|
14418
|
+
- **build_execute \\<task-id\\>** (complete) \u2014 Submit build report after implementing.
|
|
14419
|
+
- **review_submit** \u2014 Accept, request changes, or reject completed builds.
|
|
14420
|
+
- **release** \u2014 Merge all done tasks when the cycle is complete.
|
|
14421
|
+
|
|
14422
|
+
## Quick Commands
|
|
14423
|
+
|
|
14424
|
+
- \`orient\` \u2014 Current cycle state and recommended next action
|
|
14425
|
+
- \`idea\` \u2014 Add a task or bug to the backlog
|
|
14426
|
+
- \`board_view\` \u2014 See all active tasks
|
|
14427
|
+
- \`health\` \u2014 Deep project metrics
|
|
14428
|
+
|
|
14429
|
+
## Rules
|
|
14430
|
+
|
|
14431
|
+
- Run tools automatically \u2014 don't ask the user to invoke MCP tools manually
|
|
14432
|
+
- Stay within BUILD HANDOFF scope \u2014 don't add unrequested features
|
|
14433
|
+
- Commit per task \u2014 traceable git history
|
|
14434
|
+
- After build_execute completes: run \`review_submit\` with the verdict before presenting for human review
|
|
14435
|
+
`;
|
|
14034
14436
|
|
|
14035
14437
|
// src/services/setup.ts
|
|
14036
14438
|
var FILE_TEMPLATES = {
|
|
@@ -14112,6 +14514,23 @@ async function scaffoldPapiDir(adapter2, config2, input) {
|
|
|
14112
14514
|
} catch {
|
|
14113
14515
|
}
|
|
14114
14516
|
}
|
|
14517
|
+
const cursorDir = join3(config2.projectRoot, ".cursor");
|
|
14518
|
+
let cursorDetected = false;
|
|
14519
|
+
try {
|
|
14520
|
+
await access2(cursorDir);
|
|
14521
|
+
cursorDetected = true;
|
|
14522
|
+
} catch {
|
|
14523
|
+
}
|
|
14524
|
+
if (cursorDetected) {
|
|
14525
|
+
const cursorRulesDir = join3(cursorDir, "rules");
|
|
14526
|
+
const cursorRulesPath = join3(cursorRulesDir, "papi.mdc");
|
|
14527
|
+
await mkdir(cursorRulesDir, { recursive: true });
|
|
14528
|
+
try {
|
|
14529
|
+
await access2(cursorRulesPath);
|
|
14530
|
+
} catch {
|
|
14531
|
+
scaffoldFiles[cursorRulesPath] = substitute(CURSOR_RULES_TEMPLATE, vars);
|
|
14532
|
+
}
|
|
14533
|
+
}
|
|
14115
14534
|
for (const [filepath, content] of Object.entries(scaffoldFiles)) {
|
|
14116
14535
|
await writeFile2(filepath, content, "utf-8");
|
|
14117
14536
|
}
|
|
@@ -14512,11 +14931,18 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
|
|
|
14512
14931
|
});
|
|
14513
14932
|
} catch {
|
|
14514
14933
|
}
|
|
14934
|
+
let cursorScaffolded = false;
|
|
14935
|
+
try {
|
|
14936
|
+
await access2(join3(config2.projectRoot, ".cursor", "rules", "papi.mdc"));
|
|
14937
|
+
cursorScaffolded = true;
|
|
14938
|
+
} catch {
|
|
14939
|
+
}
|
|
14515
14940
|
return {
|
|
14516
14941
|
createdProject,
|
|
14517
14942
|
projectName: input.projectName,
|
|
14518
14943
|
seededAds,
|
|
14519
|
-
createdTasks
|
|
14944
|
+
createdTasks,
|
|
14945
|
+
cursorScaffolded
|
|
14520
14946
|
};
|
|
14521
14947
|
}
|
|
14522
14948
|
|
|
@@ -14624,13 +15050,16 @@ ${result.seededAds} Active Decision${result.seededAds > 1 ? "s" : ""} seeded bas
|
|
|
14624
15050
|
|
|
14625
15051
|
${result.createdTasks} initial backlog task${result.createdTasks > 1 ? "s" : ""} created from codebase analysis.` : "";
|
|
14626
15052
|
const constraintsHint = !constraints ? '\n\nTip: consider adding `constraints` (e.g. "must use PostgreSQL", "HIPAA compliant", "offline-first") to improve Active Decision seeding.' : "";
|
|
15053
|
+
const editorNote = result.cursorScaffolded ? "\n\nCursor detected \u2014 `.cursor/rules/papi.mdc` scaffolded alongside CLAUDE.md." : "";
|
|
14627
15054
|
return textResponse(
|
|
14628
|
-
`${prefix}Product Brief generated and saved.${adNote}${taskNote}${constraintsHint}
|
|
15055
|
+
`${prefix}Product Brief generated and saved.${adNote}${taskNote}${constraintsHint}${editorNote}
|
|
14629
15056
|
|
|
14630
15057
|
**Important:** Setup created/modified files (CLAUDE.md, .claude/settings.json, docs/). Commit these changes before running \`build_execute\` \u2014 it requires a clean working directory.
|
|
14631
15058
|
|
|
14632
15059
|
Tip: See \`docs/templates/example-project-brief.md\` for an example of a well-written brief.
|
|
14633
15060
|
|
|
15061
|
+
**Telemetry notice:** PAPI collects anonymous usage data (tool name, duration, project ID) to improve the product. No code, file contents, or personal data is collected. To opt out, add \`"PAPI_TELEMETRY": "off"\` to the \`env\` block in your \`.mcp.json\`.
|
|
15062
|
+
|
|
14634
15063
|
Next step: run \`plan\` to start your first planning cycle.`
|
|
14635
15064
|
);
|
|
14636
15065
|
}
|
|
@@ -15022,6 +15451,52 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
15022
15451
|
if (adIds.length > 0) report.relatedDecisions = adIds;
|
|
15023
15452
|
}
|
|
15024
15453
|
await adapter2.appendBuildReport(report);
|
|
15454
|
+
if (adapter2.appendCycleLearnings) {
|
|
15455
|
+
const learnings = [];
|
|
15456
|
+
const taskModule = task.module ?? "";
|
|
15457
|
+
const adIds = report.relatedDecisions ?? [];
|
|
15458
|
+
const extractLearning = (text, category) => {
|
|
15459
|
+
if (!text || text === "None" || text.trim().length === 0) return;
|
|
15460
|
+
const dotIdx = text.indexOf(". ");
|
|
15461
|
+
const summary = dotIdx > 0 && dotIdx < 120 ? text.slice(0, dotIdx + 1) : text.slice(0, 120);
|
|
15462
|
+
const tags = [];
|
|
15463
|
+
if (taskModule) tags.push(taskModule.toLowerCase());
|
|
15464
|
+
let severity;
|
|
15465
|
+
if (category === "issue") {
|
|
15466
|
+
const sevMatch = text.match(/^(P[0-3])[\s:]/);
|
|
15467
|
+
if (sevMatch) severity = sevMatch[1];
|
|
15468
|
+
}
|
|
15469
|
+
learnings.push({
|
|
15470
|
+
taskId: task.id,
|
|
15471
|
+
cycleNumber,
|
|
15472
|
+
category,
|
|
15473
|
+
severity,
|
|
15474
|
+
summary,
|
|
15475
|
+
detail: text,
|
|
15476
|
+
tags,
|
|
15477
|
+
relatedDecision: adIds[0]
|
|
15478
|
+
});
|
|
15479
|
+
};
|
|
15480
|
+
extractLearning(input.surprises, "surprise");
|
|
15481
|
+
extractLearning(input.discoveredIssues, "issue");
|
|
15482
|
+
extractLearning(input.architectureNotes, "architecture");
|
|
15483
|
+
extractLearning(input.deadEnds, "dead_end");
|
|
15484
|
+
if (input.scopeAccuracy && input.scopeAccuracy !== "accurate") {
|
|
15485
|
+
learnings.push({
|
|
15486
|
+
taskId: task.id,
|
|
15487
|
+
cycleNumber,
|
|
15488
|
+
category: "estimation_miss",
|
|
15489
|
+
summary: `${input.scopeAccuracy}: effort was ${input.effort} vs estimated ${input.estimatedEffort}`,
|
|
15490
|
+
tags: taskModule ? [taskModule.toLowerCase()] : []
|
|
15491
|
+
});
|
|
15492
|
+
}
|
|
15493
|
+
if (learnings.length > 0) {
|
|
15494
|
+
try {
|
|
15495
|
+
await adapter2.appendCycleLearnings(learnings);
|
|
15496
|
+
} catch {
|
|
15497
|
+
}
|
|
15498
|
+
}
|
|
15499
|
+
}
|
|
15025
15500
|
if (report.relatedDecisions && report.relatedDecisions.length > 0) {
|
|
15026
15501
|
for (const adId of report.relatedDecisions) {
|
|
15027
15502
|
try {
|
|
@@ -15137,6 +15612,80 @@ async function cancelBuild(adapter2, taskId, reason) {
|
|
|
15137
15612
|
return { task, reason };
|
|
15138
15613
|
}
|
|
15139
15614
|
|
|
15615
|
+
// src/tools/module-instructions.ts
|
|
15616
|
+
var MODULE_INSTRUCTIONS = {
|
|
15617
|
+
Dashboard: `**\u26A0\uFE0F MANDATORY \u2014 Dashboard Module Rules (skip = rework)**
|
|
15618
|
+
|
|
15619
|
+
**STEP 1 \u2014 BEFORE writing any code:**
|
|
15620
|
+
Read \`.impeccable.md\` in full. It contains the palette, typography system, audience definitions, brand personality, and design principles. Every visual decision must reference this file. If your output contradicts .impeccable.md, it is wrong.
|
|
15621
|
+
|
|
15622
|
+
Typography rule: **Mono font is for task IDs and code only.** All labels, titles, column headers, status text, and body copy use sans-serif. Four-role type system: Display (40\u201356px, gradient) / Heading (18\u201322px, semibold) / Body (14\u201316px, sans) / Mono (12\u201313px, IDs and code only).
|
|
15623
|
+
|
|
15624
|
+
**STEP 2 \u2014 Use the \`frontend-design\` skill for ALL visual implementation.**
|
|
15625
|
+
DO NOT write CSS, Tailwind classes, or component markup manually. Run the frontend-design skill and let it generate the visual layer. Manual styling produces generic output that fails review every time. This is non-negotiable.
|
|
15626
|
+
For M/L tasks: use the full toolchain \u2014 Playground (design preview) \u2192 Frontend-design (build) \u2192 Playwright (verify). The playground output is the quality bar. If the build looks worse than the playground, the build is wrong.
|
|
15627
|
+
|
|
15628
|
+
**STEP 3 \u2014 Visual quality checklist (verify before committing):**
|
|
15629
|
+
- [ ] Font sizes match the page context \u2014 don't use tiny text (xs/2xs) when surrounding components use sm/base
|
|
15630
|
+
- [ ] Alignment and spacing are consistent \u2014 no orphaned margins, no elements floating off-grid
|
|
15631
|
+
- [ ] Color usage follows .impeccable.md \u2014 stat values use primary/accent colors, NOT gray. Status colors are applied to status indicators.
|
|
15632
|
+
- [ ] Content has hierarchy \u2014 primary info is large and colored, secondary is smaller and muted, tertiary is behind interaction
|
|
15633
|
+
- [ ] No raw task IDs without names, no abbreviations without tooltips, no jargon a non-dev wouldn't understand
|
|
15634
|
+
- [ ] Percentages and numbers have context \u2014 "96%" means nothing without "96% of what" and "is that good?"
|
|
15635
|
+
- [ ] Empty states guide the user with next actions, not just "No data"
|
|
15636
|
+
- [ ] Text doesn't truncate unless there's a clear expand/tooltip interaction
|
|
15637
|
+
- [ ] The component looks intentional next to its siblings \u2014 same surface levels, border treatment, spacing rhythm
|
|
15638
|
+
- [ ] Provide localhost URL or screenshot to user for visual review
|
|
15639
|
+
|
|
15640
|
+
**STEP 4 \u2014 Production concerns:**
|
|
15641
|
+
- New API fetches need cache headers \u2014 check existing pattern in route.ts
|
|
15642
|
+
- Collapsed/hidden sections must lazy-load data, not fetch on page mount
|
|
15643
|
+
- No duplicate data fetching \u2014 check if polling, Realtime, or page load already covers this data
|
|
15644
|
+
- Consider Vercel free tier limits before adding polling or subscriptions`,
|
|
15645
|
+
"MCP Server": `**MODULE INSTRUCTIONS \u2014 MCP Server**
|
|
15646
|
+
- Test changes locally before shipping \u2014 run the tool you modified against a real project.
|
|
15647
|
+
- For tools with prepare\u2192apply flows (plan, strategy_review), verify the full round-trip works.
|
|
15648
|
+
- Don't break existing tool input signatures \u2014 external users depend on them.
|
|
15649
|
+
- Build the server after changes: \`npm run build --workspace=@papi-ai/server\`
|
|
15650
|
+
- Check that tool descriptions are clear \u2014 LLMs read these to decide when and how to call the tool.
|
|
15651
|
+
- If modifying prompts.ts, verify the planner LLM output still matches the expected structured format.
|
|
15652
|
+
- Service logic goes in services/, tool handler + formatting goes in tools/ \u2014 keep the separation clean.`,
|
|
15653
|
+
Core: `**MODULE INSTRUCTIONS \u2014 Core**
|
|
15654
|
+
- Check adapter-pg implementation, not adapter-md. adapter-md is legacy.
|
|
15655
|
+
- Verify the full write\u2192DB\u2192read\u2192consumer path for any data changes.
|
|
15656
|
+
- Run migrations on dev before prod. Test with \`execute_sql\` via Supabase MCP.
|
|
15657
|
+
- When adding adapter interface methods, implement in BOTH adapter-md and adapter-pg.
|
|
15658
|
+
- Build order matters: adapter-md \u2192 adapter-pg \u2192 server.
|
|
15659
|
+
- .papi/ files may be stale \u2014 DB via MCP tools is the source of truth.`,
|
|
15660
|
+
Auth: `**MODULE INSTRUCTIONS \u2014 Auth**
|
|
15661
|
+
- NEVER expose the Supabase service role key in client-side code or API responses.
|
|
15662
|
+
- Test with both authenticated and unauthenticated requests.
|
|
15663
|
+
- Check RLS policies \u2014 service role key bypasses all RLS, so app-layer checks are critical.
|
|
15664
|
+
- /admin and /api/telemetry MUST be owner-only (cathalos92), never any authenticated user.
|
|
15665
|
+
- Multi-provider OAuth: GitHub + Email active, Google in backlog. Test all active providers.`,
|
|
15666
|
+
Architecture: `**MODULE INSTRUCTIONS \u2014 Architecture**
|
|
15667
|
+
- Research tasks produce docs, not code. Deliverable is a findings document.
|
|
15668
|
+
- Save docs to docs/research/ or docs/architecture/ with frontmatter (title, type, created, cycle, status).
|
|
15669
|
+
- Register docs via \`doc_register\` after finalization \u2014 not during drafting.
|
|
15670
|
+
- Do NOT submit follow-up ideas to backlog until the owner confirms findings are actionable.
|
|
15671
|
+
- Include absolute dates and cycle numbers in all documents for future LLM context.`,
|
|
15672
|
+
Data: `**MODULE INSTRUCTIONS \u2014 Data**
|
|
15673
|
+
- SQL views and functions run in Supabase \u2014 test via \`execute_sql\` before deploying.
|
|
15674
|
+
- Verify query performance on real data volumes, not just test data.
|
|
15675
|
+
- Cross-project queries must filter by project_id to prevent data leakage.
|
|
15676
|
+
- When creating views, ensure they respect the same access patterns as the underlying tables.`
|
|
15677
|
+
};
|
|
15678
|
+
function getModuleInstructions(module) {
|
|
15679
|
+
if (!module) return "";
|
|
15680
|
+
const instructions = MODULE_INSTRUCTIONS[module];
|
|
15681
|
+
if (!instructions) return "";
|
|
15682
|
+
return `
|
|
15683
|
+
|
|
15684
|
+
---
|
|
15685
|
+
|
|
15686
|
+
${instructions}`;
|
|
15687
|
+
}
|
|
15688
|
+
|
|
15140
15689
|
// src/tools/build.ts
|
|
15141
15690
|
var buildListTool = {
|
|
15142
15691
|
name: "build_list",
|
|
@@ -15192,15 +15741,15 @@ var buildExecuteTool = {
|
|
|
15192
15741
|
},
|
|
15193
15742
|
surprises: {
|
|
15194
15743
|
type: "string",
|
|
15195
|
-
description: '
|
|
15744
|
+
description: 'What was DIFFERENT from what you expected? Scope changes, wrong assumptions, unexpected complexity, or missing infrastructure. NOT implementation details. Good: "Assumed table had status column but it uses state \u2014 required migration." Bad: "Used CSS keyframes instead of library." If your surprise is about implementation mechanics (which library, which pattern), it is not a surprise \u2014 use "None". Required for complete.'
|
|
15196
15745
|
},
|
|
15197
15746
|
discovered_issues: {
|
|
15198
15747
|
type: "string",
|
|
15199
|
-
description: '
|
|
15748
|
+
description: `Problems found DURING this build that are OUTSIDE this task's scope. Include severity (P0-P3). Good: "P2: Auth middleware doesn't validate token expiry \u2014 affects all protected routes." Bad: "Had to install a dependency." Only real bugs or gaps that need their own task. Use "None" if none. Required for complete.`
|
|
15200
15749
|
},
|
|
15201
15750
|
architecture_notes: {
|
|
15202
15751
|
type: "string",
|
|
15203
|
-
description: '
|
|
15752
|
+
description: 'Patterns established or decisions made during this build that AFFECT FUTURE WORK. Good: "Created shared service layer for cockpit data \u2014 all cockpit components should use it." Bad: "Used React hooks." Only include if future builders need to know this to avoid rework or follow the established pattern. Use "None" if none. Required for complete.'
|
|
15204
15753
|
},
|
|
15205
15754
|
scope_accuracy: {
|
|
15206
15755
|
type: "string",
|
|
@@ -15281,6 +15830,30 @@ function filterRelevantADs(ads, task) {
|
|
|
15281
15830
|
return keywords.some((kw) => text.includes(kw));
|
|
15282
15831
|
});
|
|
15283
15832
|
}
|
|
15833
|
+
async function getModuleContext(adapter2, task) {
|
|
15834
|
+
if (!task.module) return "";
|
|
15835
|
+
try {
|
|
15836
|
+
const [reports, allTasks] = await Promise.all([
|
|
15837
|
+
adapter2.getRecentBuildReports(30),
|
|
15838
|
+
adapter2.queryBoard()
|
|
15839
|
+
]);
|
|
15840
|
+
const taskModuleMap = new Map(allTasks.map((t) => [t.id, t.module]));
|
|
15841
|
+
const moduleReports = reports.filter((r) => taskModuleMap.get(r.taskId) === task.module).slice(0, 5);
|
|
15842
|
+
if (moduleReports.length === 0) return "";
|
|
15843
|
+
const lines = ["\n\n---\n\n**RECENT MODULE CONTEXT** (last builds in " + task.module + "):"];
|
|
15844
|
+
for (const r of moduleReports) {
|
|
15845
|
+
const parts = [`- **${r.taskId}:** ${r.taskName} (Cycle ${r.cycle})`];
|
|
15846
|
+
if (r.surprises && r.surprises !== "None") parts.push(` Surprises: ${r.surprises}`);
|
|
15847
|
+
if (r.discoveredIssues && r.discoveredIssues !== "None") parts.push(` Issues: ${r.discoveredIssues}`);
|
|
15848
|
+
if (r.architectureNotes && r.architectureNotes !== "None") parts.push(` Architecture: ${r.architectureNotes}`);
|
|
15849
|
+
if (parts.length > 1) lines.push(parts.join("\n"));
|
|
15850
|
+
}
|
|
15851
|
+
if (lines.length <= 1) return "";
|
|
15852
|
+
return lines.join("\n");
|
|
15853
|
+
} catch {
|
|
15854
|
+
return "";
|
|
15855
|
+
}
|
|
15856
|
+
}
|
|
15284
15857
|
function formatRelevantADs(ads) {
|
|
15285
15858
|
if (ads.length === 0) return "";
|
|
15286
15859
|
const lines = ["\n\n---\n\n**ACTIVE DECISIONS (relevant):**"];
|
|
@@ -15383,7 +15956,7 @@ async function handleBuildExecute(adapter2, config2, args) {
|
|
|
15383
15956
|
**PRE-BUILD VERIFICATION:** Before writing any code, read these files and check if the functionality already exists:
|
|
15384
15957
|
${verificationFiles.map((f) => `- ${f}`).join("\n")}
|
|
15385
15958
|
If >80% of the scope is already implemented, call \`build_execute\` with completed="yes" and note "already built" in surprises instead of re-implementing.` : "";
|
|
15386
|
-
const chainInstruction =
|
|
15959
|
+
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.\n\n**Build Report Quality Bar:**\n- **surprises**: What was DIFFERENT from expected \u2014 wrong assumptions, scope changes, missing infrastructure. NOT implementation mechanics ("used X library").\n- **discovered_issues**: Bugs/gaps OUTSIDE this task\'s scope, with severity (P0-P3). NOT "had to install a dependency".\n- **architecture_notes**: Patterns/decisions that AFFECT FUTURE WORK. NOT "used React hooks".\n- If nothing meaningful to report for a field, use "None" \u2014 empty signal is better than noise.';
|
|
15387
15960
|
let adSection = "";
|
|
15388
15961
|
try {
|
|
15389
15962
|
const allADs = await adapter2.getActiveDecisions();
|
|
@@ -15391,7 +15964,9 @@ If >80% of the scope is already implemented, call \`build_execute\` with complet
|
|
|
15391
15964
|
adSection = formatRelevantADs(relevant);
|
|
15392
15965
|
} catch {
|
|
15393
15966
|
}
|
|
15394
|
-
|
|
15967
|
+
const moduleInstructions = getModuleInstructions(result.task.module);
|
|
15968
|
+
const moduleContext = await getModuleContext(adapter2, result.task);
|
|
15969
|
+
return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + adSection + moduleInstructions + moduleContext + verificationNote + chainInstruction + phaseNote);
|
|
15395
15970
|
} catch (err) {
|
|
15396
15971
|
if (isNoHandoffError(err)) {
|
|
15397
15972
|
const lines = [
|
|
@@ -15501,22 +16076,39 @@ function formatCompleteResult(result) {
|
|
|
15501
16076
|
if (result.docWarning) {
|
|
15502
16077
|
lines.push("", `\u{1F4C4} ${result.docWarning}`);
|
|
15503
16078
|
}
|
|
16079
|
+
const isResearchTask = result.task.title.toLowerCase().startsWith("research:") || result.task.taskType === "research" || result.task.complexity === "Research";
|
|
16080
|
+
if (result.completed === "yes" && isResearchTask) {
|
|
16081
|
+
lines.push(
|
|
16082
|
+
"",
|
|
16083
|
+
"---",
|
|
16084
|
+
"",
|
|
16085
|
+
"## \u26A0\uFE0F Research Task Complete \u2014 Review Gate",
|
|
16086
|
+
"",
|
|
16087
|
+
"Before submitting follow-up ideas to the backlog:",
|
|
16088
|
+
"",
|
|
16089
|
+
"1. **Draft a findings doc** and save it to `docs/` (e.g. `docs/research/[topic]-findings.md`)",
|
|
16090
|
+
"2. **Surface it to the owner** \u2014 share the doc path and key takeaways for review",
|
|
16091
|
+
"3. **Only submit backlog ideas after the owner confirms** the findings are actionable",
|
|
16092
|
+
"",
|
|
16093
|
+
"Do NOT call `idea` for follow-up tasks until step 3 is complete."
|
|
16094
|
+
);
|
|
16095
|
+
}
|
|
15504
16096
|
const hasDiscoveredIssues = result.discoveredIssues !== "None" && result.discoveredIssues.trim() !== "";
|
|
15505
16097
|
const remaining = result.cycleProgress.total - result.cycleProgress.completed;
|
|
15506
16098
|
if (result.completed !== "yes") {
|
|
15507
16099
|
lines.push("", `Next: run \`build_execute ${result.task.id}\` again with updated report fields to complete this task.`);
|
|
15508
16100
|
} else if (remaining > 0) {
|
|
15509
16101
|
if (hasDiscoveredIssues) {
|
|
15510
|
-
lines.push("", `Next: discovered issues logged \u2014 next \`plan\` will triage them. Run \`build_list\` to see ${remaining} remaining cycle task${remaining === 1 ? "" : "s"},
|
|
16102
|
+
lines.push("", `Next: discovered issues logged \u2014 next \`plan\` will triage them. Run \`build_list\` to see ${remaining} remaining cycle task${remaining === 1 ? "" : "s"}, then \`build_execute <task_id>\` to start the next one.`);
|
|
15511
16103
|
} else {
|
|
15512
16104
|
lines.push("", `Next: run \`build_list\` to see ${remaining} remaining cycle task${remaining === 1 ? "" : "s"}, then \`build_execute <task_id>\` to start the next one.`);
|
|
15513
16105
|
}
|
|
15514
16106
|
lines.push("", "*Tip: Start a new chat for your next build to keep context fresh and avoid hitting context window limits.*");
|
|
15515
16107
|
} else {
|
|
15516
16108
|
if (hasDiscoveredIssues) {
|
|
15517
|
-
lines.push("", "All cycle tasks complete. Discovered issues logged \u2014 run `
|
|
16109
|
+
lines.push("", "All cycle tasks complete. Discovered issues logged \u2014 run `review_list` then `review_submit` to review builds, then `release` to close the cycle.");
|
|
15518
16110
|
} else {
|
|
15519
|
-
lines.push("", "All cycle tasks complete. Run `
|
|
16111
|
+
lines.push("", "All cycle tasks complete. Run `review_list` then `review_submit` to review builds, then `release` to close the cycle.");
|
|
15520
16112
|
}
|
|
15521
16113
|
lines.push("", "*Tip: Start a new chat for your next session to keep context fresh.*");
|
|
15522
16114
|
}
|
|
@@ -15797,8 +16389,21 @@ ${lines.join("\n")}
|
|
|
15797
16389
|
createdCycle: health.totalCycles,
|
|
15798
16390
|
notes: input.notes || "",
|
|
15799
16391
|
taskType: "idea",
|
|
15800
|
-
maturity: "raw"
|
|
16392
|
+
maturity: "raw",
|
|
16393
|
+
docRef: input.docRef
|
|
15801
16394
|
});
|
|
16395
|
+
if (input.notes && adapter2.updateDogfoodEntryStatus) {
|
|
16396
|
+
const dogfoodRefs = input.notes.match(/dogfood:([a-f0-9-]+)/gi);
|
|
16397
|
+
if (dogfoodRefs) {
|
|
16398
|
+
for (const ref of dogfoodRefs) {
|
|
16399
|
+
const entryId = ref.split(":")[1];
|
|
16400
|
+
try {
|
|
16401
|
+
await adapter2.updateDogfoodEntryStatus(entryId, "backlog-created", task.id);
|
|
16402
|
+
} catch {
|
|
16403
|
+
}
|
|
16404
|
+
}
|
|
16405
|
+
}
|
|
16406
|
+
}
|
|
15802
16407
|
return { routing: "task", task, message: `${task.id}: "${task.title}" \u2014 added to backlog` };
|
|
15803
16408
|
}
|
|
15804
16409
|
var CANVAS_SECTION_LABELS = {
|
|
@@ -15880,6 +16485,10 @@ var ideaTool = {
|
|
|
15880
16485
|
force: {
|
|
15881
16486
|
type: "boolean",
|
|
15882
16487
|
description: "Force creation even if a high-overlap duplicate or already-done task is detected. Default: false."
|
|
16488
|
+
},
|
|
16489
|
+
doc_ref: {
|
|
16490
|
+
type: "string",
|
|
16491
|
+
description: 'Path to a reference document (e.g. "docs/research/foo.md"). Stored as a structured field \u2014 replaces the fragile "Reference:" line in notes.'
|
|
15883
16492
|
}
|
|
15884
16493
|
},
|
|
15885
16494
|
required: ["text"]
|
|
@@ -15906,7 +16515,8 @@ async function handleIdea(adapter2, config2, args) {
|
|
|
15906
16515
|
complexity: args.complexity,
|
|
15907
16516
|
notes: rawNotes,
|
|
15908
16517
|
discovery: args.discovery === true,
|
|
15909
|
-
force: args.force === true
|
|
16518
|
+
force: args.force === true,
|
|
16519
|
+
docRef: args.doc_ref?.trim()
|
|
15910
16520
|
};
|
|
15911
16521
|
const useGit = isGitAvailable() && isGitRepo(config2.projectRoot);
|
|
15912
16522
|
const currentBranch = useGit ? getCurrentBranch(config2.projectRoot) : null;
|
|
@@ -16827,42 +17437,11 @@ async function getHealthSummary(adapter2) {
|
|
|
16827
17437
|
let metricsSection;
|
|
16828
17438
|
let derivedMetricsSection = "";
|
|
16829
17439
|
try {
|
|
16830
|
-
let snapshots =
|
|
16831
|
-
|
|
16832
|
-
|
|
16833
|
-
|
|
16834
|
-
|
|
16835
|
-
try {
|
|
16836
|
-
const reports = await adapter2.getRecentBuildReports(50);
|
|
16837
|
-
if (reports.length > 0) {
|
|
16838
|
-
const byCycleMap = /* @__PURE__ */ new Map();
|
|
16839
|
-
for (const r of reports) {
|
|
16840
|
-
const existing = byCycleMap.get(r.cycle) ?? [];
|
|
16841
|
-
existing.push(r);
|
|
16842
|
-
byCycleMap.set(r.cycle, existing);
|
|
16843
|
-
}
|
|
16844
|
-
const effortMap = { XS: 1, S: 2, M: 3, L: 5, XL: 8 };
|
|
16845
|
-
for (const [sn, cycleReports] of byCycleMap) {
|
|
16846
|
-
const completed = cycleReports.filter((r) => r.completed === "Yes").length;
|
|
16847
|
-
const total = cycleReports.length;
|
|
16848
|
-
const withEffort = cycleReports.filter((r) => r.estimatedEffort && r.actualEffort);
|
|
16849
|
-
const accurate = withEffort.filter((r) => r.estimatedEffort === r.actualEffort).length;
|
|
16850
|
-
const matchRate = withEffort.length > 0 ? Math.round(accurate / withEffort.length * 100) : 0;
|
|
16851
|
-
let effortPoints = 0;
|
|
16852
|
-
for (const r of cycleReports) {
|
|
16853
|
-
effortPoints += effortMap[r.actualEffort] ?? 3;
|
|
16854
|
-
}
|
|
16855
|
-
snapshots.push({
|
|
16856
|
-
cycle: sn,
|
|
16857
|
-
date: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16858
|
-
accuracy: [{ cycle: sn, reports: total, matchRate, mae: 0, bias: 0 }],
|
|
16859
|
-
velocity: [{ cycle: sn, completed, partial: 0, failed: total - completed, effortPoints }]
|
|
16860
|
-
});
|
|
16861
|
-
}
|
|
16862
|
-
snapshots.sort((a, b2) => a.cycle - b2.cycle);
|
|
16863
|
-
}
|
|
16864
|
-
} catch {
|
|
16865
|
-
}
|
|
17440
|
+
let snapshots = [];
|
|
17441
|
+
try {
|
|
17442
|
+
const reports = await adapter2.getRecentBuildReports(50);
|
|
17443
|
+
snapshots = computeSnapshotsFromBuildReports(reports);
|
|
17444
|
+
} catch {
|
|
16866
17445
|
}
|
|
16867
17446
|
metricsSection = formatCycleMetrics(snapshots);
|
|
16868
17447
|
derivedMetricsSection = formatDerivedMetrics(snapshots, activeTasks);
|
|
@@ -17240,6 +17819,7 @@ async function handleRelease(adapter2, config2, args) {
|
|
|
17240
17819
|
}
|
|
17241
17820
|
} catch {
|
|
17242
17821
|
}
|
|
17822
|
+
lines.push("", `Next: cycle released! Run \`plan\` to start your next planning cycle.`);
|
|
17243
17823
|
return textResponse(lines.join("\n"));
|
|
17244
17824
|
} catch (err) {
|
|
17245
17825
|
return errorResponse(err instanceof Error ? err.message : String(err));
|
|
@@ -17661,13 +18241,23 @@ Run \`plan\` to create Cycle ${result.currentCycle + 1}.`;
|
|
|
17661
18241
|
const autoReviewNote = autoReview ? `
|
|
17662
18242
|
|
|
17663
18243
|
**Auto-Review:** ${autoReview.verdict} \u2014 ${autoReview.summary}` + (autoReview.findings.length > 0 ? ` (${autoReview.findings.length} finding${autoReview.findings.length === 1 ? "" : "s"})` : "") : "";
|
|
18244
|
+
let nextStepNote = "";
|
|
18245
|
+
if (!autoReleaseNote && stage === "build-acceptance") {
|
|
18246
|
+
if (verdict === "accept") {
|
|
18247
|
+
nextStepNote = "\n\nNext: run `review_list` to check for more pending reviews, or `release` when all reviews are done.";
|
|
18248
|
+
} else {
|
|
18249
|
+
nextStepNote = `
|
|
18250
|
+
|
|
18251
|
+
Next: address the feedback, then run \`build_execute ${taskId}\` to resubmit.`;
|
|
18252
|
+
}
|
|
18253
|
+
}
|
|
17664
18254
|
return textResponse(
|
|
17665
18255
|
`**${result.stageLabel}** recorded for ${result.taskId}.
|
|
17666
18256
|
|
|
17667
18257
|
- **Verdict:** ${result.verdict}
|
|
17668
18258
|
- **Comments:** ${result.comments}
|
|
17669
18259
|
|
|
17670
|
-
${statusNote}${autoReviewNote}${unblockNote}${regenNote}${mergeNote}${slackNote}${autoReleaseNote}${phaseNote}`
|
|
18260
|
+
${statusNote}${autoReviewNote}${unblockNote}${regenNote}${mergeNote}${slackNote}${autoReleaseNote}${nextStepNote}${phaseNote}`
|
|
17671
18261
|
);
|
|
17672
18262
|
} catch (err) {
|
|
17673
18263
|
return errorResponse(err instanceof Error ? err.message : String(err));
|
|
@@ -17817,6 +18407,9 @@ function formatOrientSummary(health, buildInfo, hierarchy) {
|
|
|
17817
18407
|
if (hierarchy.phasesNearingClosure.length > 0) {
|
|
17818
18408
|
lines.push(`**Nearing Closure:** ${hierarchy.phasesNearingClosure.join(", ")}`);
|
|
17819
18409
|
}
|
|
18410
|
+
if (hierarchy.stageExitCriteria && hierarchy.stageExitCriteria.length > 0) {
|
|
18411
|
+
lines.push(`**Stage Exit Criteria:** ${hierarchy.stageExitCriteria.map((c) => `[ ] ${c}`).join(" | ")}`);
|
|
18412
|
+
}
|
|
17820
18413
|
lines.push("");
|
|
17821
18414
|
}
|
|
17822
18415
|
if (health.northStarSection) {
|
|
@@ -17917,7 +18510,8 @@ async function getHierarchyPosition(adapter2) {
|
|
|
17917
18510
|
horizon: activeHorizon.label,
|
|
17918
18511
|
stage: activeStage.label,
|
|
17919
18512
|
activePhases: activePhases.map((p) => p.label),
|
|
17920
|
-
phasesNearingClosure: nearingClosure.map((p) => p.label)
|
|
18513
|
+
phasesNearingClosure: nearingClosure.map((p) => p.label),
|
|
18514
|
+
stageExitCriteria: activeStage.exitCriteria
|
|
17921
18515
|
};
|
|
17922
18516
|
} catch {
|
|
17923
18517
|
return void 0;
|
|
@@ -18020,7 +18614,17 @@ ${versionDrift}` : "";
|
|
|
18020
18614
|
enrichmentNote = enrichClaudeMd(config2.projectRoot, healthResult.cycleNumber);
|
|
18021
18615
|
} catch {
|
|
18022
18616
|
}
|
|
18023
|
-
|
|
18617
|
+
let patternsNote = "";
|
|
18618
|
+
try {
|
|
18619
|
+
const patterns = await adapter2.getCycleLearningPatterns?.();
|
|
18620
|
+
if (patterns && patterns.length > 0) {
|
|
18621
|
+
const top = patterns.slice(0, 3).map((p) => `${p.tag} (${p.frequency} cycles)`).join(", ");
|
|
18622
|
+
patternsNote = `
|
|
18623
|
+
**Recurring patterns:** ${top}`;
|
|
18624
|
+
}
|
|
18625
|
+
} catch {
|
|
18626
|
+
}
|
|
18627
|
+
return textResponse(formatOrientSummary(healthResult, buildInfo, hierarchy) + ttfvNote + recsNote + pendingReviewNote + patternsNote + versionNote + enrichmentNote);
|
|
18024
18628
|
} catch (err) {
|
|
18025
18629
|
const message = err instanceof Error ? err.message : String(err);
|
|
18026
18630
|
return errorResponse(`Orient failed: ${message}`);
|
|
@@ -18050,7 +18654,7 @@ function enrichClaudeMd(projectRoot, cycleNumber) {
|
|
|
18050
18654
|
// src/tools/hierarchy.ts
|
|
18051
18655
|
var hierarchyUpdateTool = {
|
|
18052
18656
|
name: "hierarchy_update",
|
|
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.",
|
|
18657
|
+
description: "Update the status of a phase, stage, or horizon in the project hierarchy (AD-14). Accepts a level (phase, stage, or horizon), a name or ID, and a new status. For stages, optionally set exit_criteria \u2014 a checklist defining when the stage is considered done. Does not call the Anthropic API.",
|
|
18054
18658
|
inputSchema: {
|
|
18055
18659
|
type: "object",
|
|
18056
18660
|
properties: {
|
|
@@ -18067,9 +18671,14 @@ var hierarchyUpdateTool = {
|
|
|
18067
18671
|
type: "string",
|
|
18068
18672
|
enum: ["active", "completed", "deferred"],
|
|
18069
18673
|
description: "The new status to set."
|
|
18674
|
+
},
|
|
18675
|
+
exit_criteria: {
|
|
18676
|
+
type: "array",
|
|
18677
|
+
items: { type: "string" },
|
|
18678
|
+
description: 'Checklist defining when this stage is done (stages only). Each item is a completion condition, e.g. "All P0 tasks shipped". Replaces existing criteria.'
|
|
18070
18679
|
}
|
|
18071
18680
|
},
|
|
18072
|
-
required: ["level", "name"
|
|
18681
|
+
required: ["level", "name"]
|
|
18073
18682
|
}
|
|
18074
18683
|
};
|
|
18075
18684
|
var VALID_STATUSES3 = /* @__PURE__ */ new Set(["active", "completed", "deferred"]);
|
|
@@ -18077,15 +18686,22 @@ async function handleHierarchyUpdate(adapter2, args) {
|
|
|
18077
18686
|
const level = args.level;
|
|
18078
18687
|
const name = args.name;
|
|
18079
18688
|
const status = args.status;
|
|
18080
|
-
|
|
18081
|
-
|
|
18689
|
+
const exitCriteria = args.exit_criteria;
|
|
18690
|
+
if (!level || !name) {
|
|
18691
|
+
return errorResponse("Missing required parameters: level, name.");
|
|
18692
|
+
}
|
|
18693
|
+
if (!status && !exitCriteria) {
|
|
18694
|
+
return errorResponse("Nothing to update. Provide at least one of: status, exit_criteria.");
|
|
18082
18695
|
}
|
|
18083
18696
|
if (level !== "phase" && level !== "stage" && level !== "horizon") {
|
|
18084
18697
|
return errorResponse(`Invalid level "${level}". Must be "phase", "stage", or "horizon".`);
|
|
18085
18698
|
}
|
|
18086
|
-
if (!VALID_STATUSES3.has(status)) {
|
|
18699
|
+
if (status && !VALID_STATUSES3.has(status)) {
|
|
18087
18700
|
return errorResponse(`Invalid status "${status}". Must be one of: active, completed, deferred.`);
|
|
18088
18701
|
}
|
|
18702
|
+
if (exitCriteria !== void 0 && level !== "stage") {
|
|
18703
|
+
return errorResponse("exit_criteria can only be set on stages.");
|
|
18704
|
+
}
|
|
18089
18705
|
try {
|
|
18090
18706
|
if (level === "phase") {
|
|
18091
18707
|
if (!adapter2.readPhases || !adapter2.updatePhaseStatus) {
|
|
@@ -18107,7 +18723,7 @@ async function handleHierarchyUpdate(adapter2, args) {
|
|
|
18107
18723
|
return textResponse(`Phase updated: **${phase.label}** ${oldStatus2} \u2192 ${status}`);
|
|
18108
18724
|
}
|
|
18109
18725
|
if (level === "stage") {
|
|
18110
|
-
if (!adapter2.readStages
|
|
18726
|
+
if (!adapter2.readStages) {
|
|
18111
18727
|
return errorResponse("Stage management is not supported by the current adapter.");
|
|
18112
18728
|
}
|
|
18113
18729
|
const stages = await adapter2.readStages();
|
|
@@ -18118,12 +18734,28 @@ async function handleHierarchyUpdate(adapter2, args) {
|
|
|
18118
18734
|
const available = stages.map((s) => s.label).join(", ");
|
|
18119
18735
|
return errorResponse(`Stage "${name}" not found. Available stages: ${available || "none"}`);
|
|
18120
18736
|
}
|
|
18121
|
-
|
|
18122
|
-
|
|
18737
|
+
const resultLines = [];
|
|
18738
|
+
if (status) {
|
|
18739
|
+
if (!adapter2.updateStageStatus) {
|
|
18740
|
+
return errorResponse("Stage status updates are not supported by the current adapter.");
|
|
18741
|
+
}
|
|
18742
|
+
if (stage.status === status) {
|
|
18743
|
+
resultLines.push(`Stage "${stage.label}" is already "${status}".`);
|
|
18744
|
+
} else {
|
|
18745
|
+
const oldStatus2 = stage.status;
|
|
18746
|
+
await adapter2.updateStageStatus(stage.id, status);
|
|
18747
|
+
resultLines.push(`Stage updated: **${stage.label}** ${oldStatus2} \u2192 ${status}`);
|
|
18748
|
+
}
|
|
18749
|
+
}
|
|
18750
|
+
if (exitCriteria !== void 0) {
|
|
18751
|
+
if (!adapter2.updateStageExitCriteria) {
|
|
18752
|
+
return errorResponse("Exit criteria updates are not supported by the current adapter.");
|
|
18753
|
+
}
|
|
18754
|
+
await adapter2.updateStageExitCriteria(stage.id, exitCriteria);
|
|
18755
|
+
resultLines.push(`Exit criteria set (${exitCriteria.length} item${exitCriteria.length !== 1 ? "s" : ""}):`);
|
|
18756
|
+
exitCriteria.forEach((c) => resultLines.push(` - ${c}`));
|
|
18123
18757
|
}
|
|
18124
|
-
|
|
18125
|
-
await adapter2.updateStageStatus(stage.id, status);
|
|
18126
|
-
return textResponse(`Stage updated: **${stage.label}** ${oldStatus2} \u2192 ${status}`);
|
|
18758
|
+
return textResponse(resultLines.join("\n"));
|
|
18127
18759
|
}
|
|
18128
18760
|
if (!adapter2.readHorizons || !adapter2.updateHorizonStatus) {
|
|
18129
18761
|
return errorResponse("Horizon management is not supported by the current adapter.");
|
|
@@ -18698,11 +19330,87 @@ async function handleDocScan(adapter2, config2, args) {
|
|
|
18698
19330
|
return textResponse(lines.join("\n"));
|
|
18699
19331
|
}
|
|
18700
19332
|
|
|
19333
|
+
// src/tools/sibling-ads.ts
|
|
19334
|
+
var getSiblingAdsTool = {
|
|
19335
|
+
name: "get_sibling_ads",
|
|
19336
|
+
description: "Read Active Decisions from sibling PAPI projects that share the same Supabase instance. Requires PAPI_SIBLING_PROJECT_IDS env var (comma-separated project UUIDs). Returns ADs labelled by source project \u2014 useful for cross-project architectural alignment. pg adapter only \u2014 returns an error if using md or proxy adapter.",
|
|
19337
|
+
inputSchema: {
|
|
19338
|
+
type: "object",
|
|
19339
|
+
properties: {
|
|
19340
|
+
project_ids: {
|
|
19341
|
+
type: "array",
|
|
19342
|
+
items: { type: "string" },
|
|
19343
|
+
description: "Optional explicit list of sibling project UUIDs to query. If omitted, falls back to PAPI_SIBLING_PROJECT_IDS env var."
|
|
19344
|
+
}
|
|
19345
|
+
},
|
|
19346
|
+
required: []
|
|
19347
|
+
}
|
|
19348
|
+
};
|
|
19349
|
+
async function handleGetSiblingAds(adapter2, args) {
|
|
19350
|
+
if (!adapter2.getSiblingAds) {
|
|
19351
|
+
return errorResponse(
|
|
19352
|
+
"get_sibling_ads is only available with the pg adapter (direct Supabase connection). Set PAPI_ENDPOINT or DATABASE_URL to enable."
|
|
19353
|
+
);
|
|
19354
|
+
}
|
|
19355
|
+
let projectIds = [];
|
|
19356
|
+
const argIds = args["project_ids"];
|
|
19357
|
+
if (Array.isArray(argIds) && argIds.length > 0) {
|
|
19358
|
+
projectIds = argIds.filter((id) => typeof id === "string");
|
|
19359
|
+
} else {
|
|
19360
|
+
const envIds = process.env["PAPI_SIBLING_PROJECT_IDS"];
|
|
19361
|
+
if (envIds) {
|
|
19362
|
+
projectIds = envIds.split(",").map((id) => id.trim()).filter(Boolean);
|
|
19363
|
+
}
|
|
19364
|
+
}
|
|
19365
|
+
if (projectIds.length === 0) {
|
|
19366
|
+
return errorResponse(
|
|
19367
|
+
"No sibling project IDs provided. Pass project_ids in the tool call, or set PAPI_SIBLING_PROJECT_IDS=<uuid1>,<uuid2> in your .mcp.json env."
|
|
19368
|
+
);
|
|
19369
|
+
}
|
|
19370
|
+
const ads = await adapter2.getSiblingAds(projectIds);
|
|
19371
|
+
if (ads.length === 0) {
|
|
19372
|
+
return textResponse(
|
|
19373
|
+
`No active decisions found for sibling projects: ${projectIds.join(", ")}
|
|
19374
|
+
|
|
19375
|
+
Check that the project IDs are correct and exist in the same Supabase instance.`
|
|
19376
|
+
);
|
|
19377
|
+
}
|
|
19378
|
+
const byProject = /* @__PURE__ */ new Map();
|
|
19379
|
+
for (const ad of ads) {
|
|
19380
|
+
const group = byProject.get(ad.sourceProjectId) ?? [];
|
|
19381
|
+
group.push(ad);
|
|
19382
|
+
byProject.set(ad.sourceProjectId, group);
|
|
19383
|
+
}
|
|
19384
|
+
const lines = ["# Sibling Project Active Decisions", ""];
|
|
19385
|
+
lines.push(`**Source projects:** ${projectIds.join(", ")}`);
|
|
19386
|
+
lines.push(`**Total ADs:** ${ads.length}`);
|
|
19387
|
+
lines.push("");
|
|
19388
|
+
for (const [projectId, projectAds] of byProject) {
|
|
19389
|
+
lines.push(`## Project: \`${projectId}\``);
|
|
19390
|
+
lines.push(`*${projectAds.length} active decision${projectAds.length !== 1 ? "s" : ""}*`);
|
|
19391
|
+
lines.push("");
|
|
19392
|
+
for (const ad of projectAds) {
|
|
19393
|
+
lines.push(`### ${ad.displayId}: ${ad.title} [Confidence: ${ad.confidence.toUpperCase()}]`);
|
|
19394
|
+
if (ad.body) {
|
|
19395
|
+
const excerpt = ad.body.length > 300 ? ad.body.slice(0, 300) + "\u2026" : ad.body;
|
|
19396
|
+
lines.push(excerpt);
|
|
19397
|
+
}
|
|
19398
|
+
lines.push("");
|
|
19399
|
+
}
|
|
19400
|
+
}
|
|
19401
|
+
lines.push("---");
|
|
19402
|
+
lines.push(
|
|
19403
|
+
"_These ADs are from sibling projects and are read-only. To share learnings, reference relevant ADs in your task notes or update your own ADs accordingly._"
|
|
19404
|
+
);
|
|
19405
|
+
return textResponse(lines.join("\n"));
|
|
19406
|
+
}
|
|
19407
|
+
|
|
18701
19408
|
// src/lib/telemetry.ts
|
|
18702
19409
|
var TELEMETRY_SUPABASE_URL = "https://guewgygcpcmrcoppihzx.supabase.co";
|
|
18703
19410
|
var TELEMETRY_API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd1ZXdneWdjcGNtcmNvcHBpaHp4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI2Njk2NTMsImV4cCI6MjA4ODI0NTY1M30.V5Jw7wJgiMpSQPa2mt0ftjyye5ynG1qLlam00yPVNJY";
|
|
18704
19411
|
function isEnabled() {
|
|
18705
|
-
|
|
19412
|
+
const val = process.env["PAPI_TELEMETRY"];
|
|
19413
|
+
return val !== "false" && val !== "off";
|
|
18706
19414
|
}
|
|
18707
19415
|
function emitTelemetryEvent(event) {
|
|
18708
19416
|
if (!isEnabled()) return;
|
|
@@ -18769,8 +19477,58 @@ var TOOLS_REQUIRING_PAPI = /* @__PURE__ */ new Set([
|
|
|
18769
19477
|
function createServer(adapter2, config2) {
|
|
18770
19478
|
const server2 = new Server(
|
|
18771
19479
|
{ name: "papi", version: "0.1.0" },
|
|
18772
|
-
{ capabilities: { tools: {} } }
|
|
19480
|
+
{ capabilities: { tools: {}, prompts: {} } }
|
|
18773
19481
|
);
|
|
19482
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19483
|
+
const __dirname = dirname(__filename);
|
|
19484
|
+
const skillsDir = join9(__dirname, "..", "skills");
|
|
19485
|
+
function parseSkillFrontmatter(content) {
|
|
19486
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
19487
|
+
if (!match) return null;
|
|
19488
|
+
const fm = match[1];
|
|
19489
|
+
const nameMatch = fm.match(/^name:\s*(.+)$/m);
|
|
19490
|
+
const descMatch = fm.match(/^description:\s*[>|]?\n?([\s\S]*?)(?=\n\w|\n---)/m);
|
|
19491
|
+
if (!nameMatch) return null;
|
|
19492
|
+
const name = nameMatch[1].trim();
|
|
19493
|
+
const description = descMatch ? descMatch[1].replace(/^\s{2}/gm, "").replace(/\n+$/, "").replace(/\n/g, " ").trim() : "";
|
|
19494
|
+
return { name, description };
|
|
19495
|
+
}
|
|
19496
|
+
server2.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
19497
|
+
try {
|
|
19498
|
+
const files = await readdir2(skillsDir);
|
|
19499
|
+
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
19500
|
+
const prompts = [];
|
|
19501
|
+
for (const file of mdFiles) {
|
|
19502
|
+
const content = await readFile5(join9(skillsDir, file), "utf-8");
|
|
19503
|
+
const meta = parseSkillFrontmatter(content);
|
|
19504
|
+
if (meta) {
|
|
19505
|
+
prompts.push({ name: meta.name, description: meta.description });
|
|
19506
|
+
}
|
|
19507
|
+
}
|
|
19508
|
+
return { prompts };
|
|
19509
|
+
} catch {
|
|
19510
|
+
return { prompts: [] };
|
|
19511
|
+
}
|
|
19512
|
+
});
|
|
19513
|
+
server2.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
19514
|
+
const { name } = request.params;
|
|
19515
|
+
try {
|
|
19516
|
+
const files = await readdir2(skillsDir);
|
|
19517
|
+
for (const file of files.filter((f) => f.endsWith(".md"))) {
|
|
19518
|
+
const content = await readFile5(join9(skillsDir, file), "utf-8");
|
|
19519
|
+
const meta = parseSkillFrontmatter(content);
|
|
19520
|
+
if (meta?.name === name) {
|
|
19521
|
+
const body = content.replace(/^---\n[\s\S]*?\n---\n*/, "");
|
|
19522
|
+
return {
|
|
19523
|
+
description: meta.description,
|
|
19524
|
+
messages: [{ role: "user", content: { type: "text", text: body } }]
|
|
19525
|
+
};
|
|
19526
|
+
}
|
|
19527
|
+
}
|
|
19528
|
+
} catch {
|
|
19529
|
+
}
|
|
19530
|
+
throw new Error(`Prompt not found: ${name}`);
|
|
19531
|
+
});
|
|
18774
19532
|
server2.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
18775
19533
|
tools: [
|
|
18776
19534
|
planTool,
|
|
@@ -18799,7 +19557,8 @@ function createServer(adapter2, config2) {
|
|
|
18799
19557
|
zoomOutTool,
|
|
18800
19558
|
docRegisterTool,
|
|
18801
19559
|
docSearchTool,
|
|
18802
|
-
docScanTool
|
|
19560
|
+
docScanTool,
|
|
19561
|
+
getSiblingAdsTool
|
|
18803
19562
|
]
|
|
18804
19563
|
}));
|
|
18805
19564
|
server2.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -18912,6 +19671,9 @@ function createServer(adapter2, config2) {
|
|
|
18912
19671
|
case "doc_scan":
|
|
18913
19672
|
result = await handleDocScan(adapter2, config2, safeArgs);
|
|
18914
19673
|
break;
|
|
19674
|
+
case "get_sibling_ads":
|
|
19675
|
+
result = await handleGetSiblingAds(adapter2, safeArgs);
|
|
19676
|
+
break;
|
|
18915
19677
|
default:
|
|
18916
19678
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
|
|
18917
19679
|
}
|