@papi-ai/server 0.5.0 → 0.5.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/dist/index.js +587 -44
- package/package.json +2 -3
package/dist/index.js
CHANGED
|
@@ -280,6 +280,9 @@ function splitSections(text) {
|
|
|
280
280
|
function parseBulletList(text) {
|
|
281
281
|
return text.split("\n").map((l) => l.replace(/^\s*-\s*/, "").trim()).filter((l) => l.length > 0);
|
|
282
282
|
}
|
|
283
|
+
function parseBulletsOnly(text) {
|
|
284
|
+
return text.split("\n").filter((l) => /^\s*-\s/.test(l)).map((l) => l.replace(/^\s*-\s*/, "").trim()).filter((l) => l.length > 0);
|
|
285
|
+
}
|
|
283
286
|
function parseChecklist(text) {
|
|
284
287
|
return text.split("\n").map((l) => l.replace(/^\s*\[[ x]]\s*/, "").trim()).filter((l) => l.length > 0);
|
|
285
288
|
}
|
|
@@ -314,6 +317,7 @@ function parseBuildHandoff(markdown) {
|
|
|
314
317
|
scopeBoundary: parseBulletList(sections.get("SCOPE BOUNDARY (DO NOT DO THIS)") ?? ""),
|
|
315
318
|
acceptanceCriteria: parseChecklist(sections.get("ACCEPTANCE CRITERIA") ?? ""),
|
|
316
319
|
securityConsiderations: (sections.get("SECURITY CONSIDERATIONS") ?? "").trim(),
|
|
320
|
+
verificationFiles: parseBulletsOnly(sections.get("PRE-BUILD VERIFICATION") ?? ""),
|
|
317
321
|
filesLikelyTouched: parseBulletList(sections.get("FILES LIKELY TOUCHED") ?? ""),
|
|
318
322
|
effort
|
|
319
323
|
};
|
|
@@ -345,6 +349,15 @@ function serializeBuildHandoff(handoff) {
|
|
|
345
349
|
lines.push("");
|
|
346
350
|
lines.push("SECURITY CONSIDERATIONS");
|
|
347
351
|
lines.push(handoff.securityConsiderations);
|
|
352
|
+
if (handoff.verificationFiles && handoff.verificationFiles.length > 0) {
|
|
353
|
+
lines.push("");
|
|
354
|
+
lines.push("PRE-BUILD VERIFICATION");
|
|
355
|
+
lines.push("Before implementing, read these files and check if the functionality already exists:");
|
|
356
|
+
for (const item of handoff.verificationFiles) {
|
|
357
|
+
lines.push(`- ${item}`);
|
|
358
|
+
}
|
|
359
|
+
lines.push('If >80% of the scope is already implemented, call build_execute with completed="yes" and note "already built" in surprises instead of re-implementing.');
|
|
360
|
+
}
|
|
348
361
|
lines.push("");
|
|
349
362
|
lines.push("FILES LIKELY TOUCHED");
|
|
350
363
|
for (const item of handoff.filesLikelyTouched) {
|
|
@@ -1422,6 +1435,7 @@ var init_dist2 = __esm({
|
|
|
1422
1435
|
"SCOPE BOUNDARY (DO NOT DO THIS)",
|
|
1423
1436
|
"ACCEPTANCE CRITERIA",
|
|
1424
1437
|
"SECURITY CONSIDERATIONS",
|
|
1438
|
+
"PRE-BUILD VERIFICATION",
|
|
1425
1439
|
"FILES LIKELY TOUCHED",
|
|
1426
1440
|
"EFFORT"
|
|
1427
1441
|
];
|
|
@@ -4540,7 +4554,7 @@ function rowToDecisionScore(row) {
|
|
|
4540
4554
|
createdAt: row.created_at
|
|
4541
4555
|
};
|
|
4542
4556
|
}
|
|
4543
|
-
function
|
|
4557
|
+
function rowToCycleLogEntry(row) {
|
|
4544
4558
|
const entry = {
|
|
4545
4559
|
uuid: row.id,
|
|
4546
4560
|
cycleNumber: row.cycle_number,
|
|
@@ -6133,14 +6147,14 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
|
6133
6147
|
ORDER BY cycle_number DESC
|
|
6134
6148
|
LIMIT ${limit}
|
|
6135
6149
|
`;
|
|
6136
|
-
return rows2.map(
|
|
6150
|
+
return rows2.map(rowToCycleLogEntry);
|
|
6137
6151
|
}
|
|
6138
6152
|
const rows = await this.sql`
|
|
6139
6153
|
SELECT * FROM planning_log_entries
|
|
6140
6154
|
WHERE project_id = ${this.projectId}
|
|
6141
6155
|
ORDER BY cycle_number DESC
|
|
6142
6156
|
`;
|
|
6143
|
-
return rows.map(
|
|
6157
|
+
return rows.map(rowToCycleLogEntry);
|
|
6144
6158
|
}
|
|
6145
6159
|
async getCycleLogSince(cycleNumber) {
|
|
6146
6160
|
const rows = await this.sql`
|
|
@@ -6149,7 +6163,7 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
|
6149
6163
|
AND cycle_number >= ${cycleNumber}
|
|
6150
6164
|
ORDER BY cycle_number DESC
|
|
6151
6165
|
`;
|
|
6152
|
-
return rows.map(
|
|
6166
|
+
return rows.map(rowToCycleLogEntry);
|
|
6153
6167
|
}
|
|
6154
6168
|
async setCycleHealth(updates) {
|
|
6155
6169
|
if (updates.boardHealth != null || updates.strategicDirection != null) {
|
|
@@ -6243,7 +6257,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6243
6257
|
board_health, strategic_direction, full_analysis,
|
|
6244
6258
|
velocity_assessment, structured_data, created_at
|
|
6245
6259
|
FROM strategy_reviews
|
|
6246
|
-
WHERE project_id = ${this.projectId}
|
|
6260
|
+
WHERE project_id = ${this.projectId} AND cycle_number > 0
|
|
6247
6261
|
ORDER BY cycle_number DESC
|
|
6248
6262
|
LIMIT ${limit}
|
|
6249
6263
|
`;
|
|
@@ -6283,6 +6297,139 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6283
6297
|
createdAt: r.created_at ?? void 0
|
|
6284
6298
|
}));
|
|
6285
6299
|
}
|
|
6300
|
+
async savePendingReviewResponse(cycleNumber, rawResponse) {
|
|
6301
|
+
await this.sql`
|
|
6302
|
+
INSERT INTO strategy_reviews (
|
|
6303
|
+
project_id, cycle_number, title, content, full_analysis
|
|
6304
|
+
) VALUES (
|
|
6305
|
+
${this.projectId}, ${0}, ${"[PENDING] Strategy Review"}, ${"Pending write-back retry"}, ${rawResponse}
|
|
6306
|
+
)
|
|
6307
|
+
ON CONFLICT (project_id, cycle_number)
|
|
6308
|
+
DO UPDATE SET
|
|
6309
|
+
full_analysis = ${rawResponse},
|
|
6310
|
+
notes = ${`original_cycle:${cycleNumber}`}
|
|
6311
|
+
`;
|
|
6312
|
+
}
|
|
6313
|
+
async getPendingReviewResponse() {
|
|
6314
|
+
const rows = await this.sql`
|
|
6315
|
+
SELECT full_analysis, notes FROM strategy_reviews
|
|
6316
|
+
WHERE project_id = ${this.projectId} AND cycle_number = 0
|
|
6317
|
+
LIMIT 1
|
|
6318
|
+
`;
|
|
6319
|
+
if (rows.length === 0 || !rows[0].full_analysis) return null;
|
|
6320
|
+
const cycleMatch = rows[0].notes?.match(/original_cycle:(\d+)/);
|
|
6321
|
+
const cycleNumber = cycleMatch ? parseInt(cycleMatch[1], 10) : 0;
|
|
6322
|
+
return { cycleNumber, rawResponse: rows[0].full_analysis };
|
|
6323
|
+
}
|
|
6324
|
+
async clearPendingReviewResponse() {
|
|
6325
|
+
await this.sql`
|
|
6326
|
+
DELETE FROM strategy_reviews
|
|
6327
|
+
WHERE project_id = ${this.projectId} AND cycle_number = 0
|
|
6328
|
+
`;
|
|
6329
|
+
}
|
|
6330
|
+
// -------------------------------------------------------------------------
|
|
6331
|
+
// Doc Registry
|
|
6332
|
+
// -------------------------------------------------------------------------
|
|
6333
|
+
async registerDoc(entry) {
|
|
6334
|
+
const [row] = await this.sql`
|
|
6335
|
+
INSERT INTO doc_registry (
|
|
6336
|
+
project_id, title, type, path, status, summary, tags,
|
|
6337
|
+
cycle_created, cycle_updated, superseded_by, actions
|
|
6338
|
+
) VALUES (
|
|
6339
|
+
${this.projectId}, ${entry.title}, ${entry.type}, ${entry.path},
|
|
6340
|
+
${entry.status}, ${entry.summary}, ${entry.tags},
|
|
6341
|
+
${entry.cycleCreated}, ${entry.cycleUpdated ?? null},
|
|
6342
|
+
${entry.supersededBy ?? null},
|
|
6343
|
+
${entry.actions ? JSON.stringify(entry.actions) : "[]"}
|
|
6344
|
+
)
|
|
6345
|
+
ON CONFLICT (project_id, path)
|
|
6346
|
+
DO UPDATE SET
|
|
6347
|
+
title = EXCLUDED.title,
|
|
6348
|
+
type = EXCLUDED.type,
|
|
6349
|
+
status = EXCLUDED.status,
|
|
6350
|
+
summary = EXCLUDED.summary,
|
|
6351
|
+
tags = EXCLUDED.tags,
|
|
6352
|
+
cycle_updated = EXCLUDED.cycle_updated,
|
|
6353
|
+
superseded_by = EXCLUDED.superseded_by,
|
|
6354
|
+
actions = EXCLUDED.actions,
|
|
6355
|
+
updated_at = now()
|
|
6356
|
+
RETURNING id, created_at, updated_at
|
|
6357
|
+
`;
|
|
6358
|
+
return {
|
|
6359
|
+
...entry,
|
|
6360
|
+
id: row.id,
|
|
6361
|
+
createdAt: row.created_at,
|
|
6362
|
+
updatedAt: row.updated_at
|
|
6363
|
+
};
|
|
6364
|
+
}
|
|
6365
|
+
async searchDocs(input) {
|
|
6366
|
+
const status = input.status ?? "active";
|
|
6367
|
+
const limit = input.limit ?? 10;
|
|
6368
|
+
const keyword = input.keyword ? `%${input.keyword}%` : null;
|
|
6369
|
+
const sinceCycle = input.sinceCycle ?? 0;
|
|
6370
|
+
const hasPending = input.hasPendingActions ?? false;
|
|
6371
|
+
const rows = await this.sql`
|
|
6372
|
+
SELECT * FROM doc_registry
|
|
6373
|
+
WHERE project_id = ${this.projectId}
|
|
6374
|
+
AND status = ${status}
|
|
6375
|
+
AND (${input.type ?? null}::text IS NULL OR type = ${input.type ?? null})
|
|
6376
|
+
AND (${keyword}::text IS NULL OR (title ILIKE ${keyword} OR summary ILIKE ${keyword}))
|
|
6377
|
+
AND (${sinceCycle} = 0 OR COALESCE(cycle_updated, cycle_created) >= ${sinceCycle})
|
|
6378
|
+
AND (${hasPending} = false OR actions::text LIKE '%"pending"%')
|
|
6379
|
+
AND (${input.tags ?? []}::text[] = '{}' OR tags && ${input.tags ?? []})
|
|
6380
|
+
ORDER BY COALESCE(cycle_updated, cycle_created) DESC
|
|
6381
|
+
LIMIT ${limit}
|
|
6382
|
+
`;
|
|
6383
|
+
return rows.map((r) => ({
|
|
6384
|
+
id: r.id,
|
|
6385
|
+
title: r.title,
|
|
6386
|
+
type: r.type,
|
|
6387
|
+
path: r.path,
|
|
6388
|
+
status: r.status,
|
|
6389
|
+
summary: r.summary,
|
|
6390
|
+
tags: r.tags ?? [],
|
|
6391
|
+
cycleCreated: r.cycle_created,
|
|
6392
|
+
cycleUpdated: r.cycle_updated ?? void 0,
|
|
6393
|
+
supersededBy: r.superseded_by ?? void 0,
|
|
6394
|
+
actions: r.actions,
|
|
6395
|
+
createdAt: r.created_at,
|
|
6396
|
+
updatedAt: r.updated_at
|
|
6397
|
+
}));
|
|
6398
|
+
}
|
|
6399
|
+
async getDoc(idOrPath) {
|
|
6400
|
+
const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(idOrPath);
|
|
6401
|
+
const rows = isUuid ? await this.sql`
|
|
6402
|
+
SELECT * FROM doc_registry WHERE id = ${idOrPath} AND project_id = ${this.projectId}
|
|
6403
|
+
` : await this.sql`
|
|
6404
|
+
SELECT * FROM doc_registry WHERE path = ${idOrPath} AND project_id = ${this.projectId}
|
|
6405
|
+
`;
|
|
6406
|
+
if (rows.length === 0) return null;
|
|
6407
|
+
const r = rows[0];
|
|
6408
|
+
return {
|
|
6409
|
+
id: r.id,
|
|
6410
|
+
title: r.title,
|
|
6411
|
+
type: r.type,
|
|
6412
|
+
path: r.path,
|
|
6413
|
+
status: r.status,
|
|
6414
|
+
summary: r.summary,
|
|
6415
|
+
tags: r.tags ?? [],
|
|
6416
|
+
cycleCreated: r.cycle_created,
|
|
6417
|
+
cycleUpdated: r.cycle_updated ?? void 0,
|
|
6418
|
+
supersededBy: r.superseded_by ?? void 0,
|
|
6419
|
+
actions: r.actions,
|
|
6420
|
+
createdAt: r.created_at,
|
|
6421
|
+
updatedAt: r.updated_at
|
|
6422
|
+
};
|
|
6423
|
+
}
|
|
6424
|
+
async updateDocStatus(id, status, supersededBy) {
|
|
6425
|
+
await this.sql`
|
|
6426
|
+
UPDATE doc_registry
|
|
6427
|
+
SET status = ${status},
|
|
6428
|
+
superseded_by = ${supersededBy ?? null},
|
|
6429
|
+
updated_at = now()
|
|
6430
|
+
WHERE id = ${id} AND project_id = ${this.projectId}
|
|
6431
|
+
`;
|
|
6432
|
+
}
|
|
6286
6433
|
async writeDogfoodEntries(entries) {
|
|
6287
6434
|
if (entries.length === 0) return;
|
|
6288
6435
|
const values2 = entries.map((entry) => ({
|
|
@@ -7045,7 +7192,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
7045
7192
|
status: "pending",
|
|
7046
7193
|
content: r.content,
|
|
7047
7194
|
createdCycle: r.created_cycle,
|
|
7048
|
-
|
|
7195
|
+
actionedCycle: r.actioned_cycle ?? void 0,
|
|
7049
7196
|
target: r.target ?? void 0
|
|
7050
7197
|
}));
|
|
7051
7198
|
}
|
|
@@ -7328,7 +7475,7 @@ ${r.content}` + (r.carry_forward ? `
|
|
|
7328
7475
|
}));
|
|
7329
7476
|
await this.sql`INSERT INTO entity_references ${this.sql(values2)}`;
|
|
7330
7477
|
}
|
|
7331
|
-
async getDecisionUsage(
|
|
7478
|
+
async getDecisionUsage(currentCycle) {
|
|
7332
7479
|
const rows = await this.sql`
|
|
7333
7480
|
SELECT decision_id, reference_count, last_referenced_cycle
|
|
7334
7481
|
FROM v_decision_usage
|
|
@@ -7338,7 +7485,7 @@ ${r.content}` + (r.carry_forward ? `
|
|
|
7338
7485
|
decisionId: r.decision_id,
|
|
7339
7486
|
referenceCount: parseInt(r.reference_count, 10),
|
|
7340
7487
|
lastReferencedCycle: r.last_referenced_cycle,
|
|
7341
|
-
cyclesSinceLastReference:
|
|
7488
|
+
cyclesSinceLastReference: currentCycle - r.last_referenced_cycle
|
|
7342
7489
|
}));
|
|
7343
7490
|
}
|
|
7344
7491
|
async getContextUtilisation() {
|
|
@@ -7979,6 +8126,7 @@ function loadConfig() {
|
|
|
7979
8126
|
const autoCommit2 = process.env.PAPI_AUTO_COMMIT !== "false";
|
|
7980
8127
|
const baseBranch = process.env.PAPI_BASE_BRANCH ?? "main";
|
|
7981
8128
|
const autoPR = process.env.PAPI_AUTO_PR !== "false";
|
|
8129
|
+
const lightMode = process.env.PAPI_LIGHT_MODE === "true";
|
|
7982
8130
|
const papiEndpoint = process.env.PAPI_ENDPOINT;
|
|
7983
8131
|
const dataEndpoint = process.env.PAPI_DATA_ENDPOINT;
|
|
7984
8132
|
const databaseUrl = process.env.DATABASE_URL;
|
|
@@ -7992,7 +8140,8 @@ function loadConfig() {
|
|
|
7992
8140
|
baseBranch,
|
|
7993
8141
|
autoPR,
|
|
7994
8142
|
adapterType,
|
|
7995
|
-
papiEndpoint
|
|
8143
|
+
papiEndpoint,
|
|
8144
|
+
lightMode
|
|
7996
8145
|
};
|
|
7997
8146
|
}
|
|
7998
8147
|
|
|
@@ -8000,7 +8149,6 @@ function loadConfig() {
|
|
|
8000
8149
|
init_dist2();
|
|
8001
8150
|
import path2 from "path";
|
|
8002
8151
|
var HOSTED_PROXY_ENDPOINT = "https://guewgygcpcmrcoppihzx.supabase.co/functions/v1/data-proxy";
|
|
8003
|
-
var HOSTED_PROXY_KEY = "e9891a0a2225ac376f88ebdad78b4814b52ce0a39a41c5ec";
|
|
8004
8152
|
var PLACEHOLDER_PATTERNS = [
|
|
8005
8153
|
"<YOUR_DATABASE_URL>",
|
|
8006
8154
|
"your-database-url",
|
|
@@ -8091,7 +8239,12 @@ async function createAdapter(optionsOrType, maybePapiDir) {
|
|
|
8091
8239
|
);
|
|
8092
8240
|
}
|
|
8093
8241
|
const dataEndpoint = process.env["PAPI_DATA_ENDPOINT"] || HOSTED_PROXY_ENDPOINT;
|
|
8094
|
-
const dataApiKey = process.env["PAPI_DATA_API_KEY"]
|
|
8242
|
+
const dataApiKey = process.env["PAPI_DATA_API_KEY"];
|
|
8243
|
+
if (!dataApiKey) {
|
|
8244
|
+
throw new Error(
|
|
8245
|
+
"PAPI_DATA_API_KEY is required for proxy mode.\nTo get your API key:\n 1. Sign in at https://papi-web-three.vercel.app with GitHub\n 2. Your API key is shown on the onboarding page (save it \u2014 shown only once)\n 3. Add PAPI_DATA_API_KEY to your .mcp.json env config\nIf you already have a key, set it in your MCP configuration."
|
|
8246
|
+
);
|
|
8247
|
+
}
|
|
8095
8248
|
const adapter2 = new ProxyPapiAdapter2({
|
|
8096
8249
|
endpoint: dataEndpoint,
|
|
8097
8250
|
apiKey: dataApiKey,
|
|
@@ -8985,6 +9138,9 @@ SECURITY CONSIDERATIONS
|
|
|
8985
9138
|
REFERENCE DOCS
|
|
8986
9139
|
[Optional \u2014 paths to docs/ files that provide background context for this task. Include only when the task originated from research or scoping work and the doc contains context the builder will need beyond what is in this handoff. Omit this section entirely for tasks that don't need supplementary context.]
|
|
8987
9140
|
|
|
9141
|
+
PRE-BUILD VERIFICATION
|
|
9142
|
+
[List 2-5 specific file paths the builder should read BEFORE implementing to check if the functionality already exists. Derive these from FILES LIKELY TOUCHED \u2014 pick the files most likely to already contain the target functionality. If >80% of the scope is already implemented, the builder should report "already built" instead of re-implementing. Include this section for EVERY task \u2014 it prevents wasted build slots on already-shipped code.]
|
|
9143
|
+
|
|
8988
9144
|
FILES LIKELY TOUCHED
|
|
8989
9145
|
[files]
|
|
8990
9146
|
|
|
@@ -9112,7 +9268,8 @@ Standard planning cycle with full board review.
|
|
|
9112
9268
|
- Tier 4: Data visualization
|
|
9113
9269
|
- Tier 5: New capability
|
|
9114
9270
|
Within a tier: smaller effort wins. Justify in 2-3 sentences.
|
|
9115
|
-
**
|
|
9271
|
+
**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.
|
|
9272
|
+
**Cycle sizing:** Size the cycle based on what the selected tasks actually require \u2014 not a fixed budget. Select the highest-priority unblocked tasks, estimate each one's effort from its scope, and let the total emerge from the tasks themselves. The historical average effort from Methodology Trends is a reference point for calibration, not a target or floor. A healthy cycle has 4-6 tasks. Cycles with fewer than 4 tasks require explicit justification in the cycle log \u2014 explain why more tasks could not be included. When the backlog has 10+ tasks, the cycle SHOULD have 5+ tasks \u2014 undersized cycles waste planning overhead relative to the available work. If fewer than 4 tasks qualify after filtering (blocked, deferred, raw), check Deferred tasks \u2014 some may be ready to un-defer via a \`boardCorrections\` entry. A 1-task cycle is almost never correct.
|
|
9116
9273
|
|
|
9117
9274
|
8. **Cycle Log** \u2014 Write 5-10 line entry: what was triaged, what was recommended and why, observations, AD updates.
|
|
9118
9275
|
**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.
|
|
@@ -9122,11 +9279,13 @@ Standard planning cycle with full board review.
|
|
|
9122
9279
|
|
|
9123
9280
|
10. **BUILD HANDOFFs** \u2014 Generate a full BUILD HANDOFF block for the recommended task and up to 4 additional high-priority unblocked tasks (5 total max). Include each handoff in the \`cycleHandoffs\` array in the structured output. The handoffs are written to each task on the board for durability. Remaining tasks will get handoffs in subsequent plans \u2014 do NOT try to cover the entire backlog.
|
|
9124
9281
|
**SKIP existing handoffs:** Tasks marked with "Has BUILD HANDOFF: yes" or "\u2713 handoff" on the board already have a valid handoff from a previous plan. Do NOT regenerate handoffs for these tasks \u2014 omit them from the \`cycleHandoffs\` array entirely. Only generate handoffs for tasks that do NOT have one yet. Exception: if a task's dependencies have been completed since its handoff was written, or a relevant Active Decision has changed, you MAY regenerate its handoff \u2014 but note this explicitly in the cycle log.
|
|
9282
|
+
**Scope pre-check:** Before writing the SCOPE section of each handoff, check whether the described functionality already exists based on the task's context, recent build reports, and the FILES LIKELY TOUCHED. If the infrastructure likely exists (e.g. a status type, a DB constraint, an API route), reduce the scope to only the missing pieces and explicitly note what already exists. C126 task-728 was over-scoped because the planner assumed Blocked status needed creating from scratch \u2014 it already existed in types, DB, orient, and build_list. Over-scoped handoffs waste builder time on verification and cause estimation mismatches.
|
|
9125
9283
|
**Simplest Viable Path rule:** Before writing each BUILD HANDOFF, identify the simplest approach that satisfies the task's goal \u2014 the minimum change, fewest new abstractions, and smallest blast radius. Write the SCOPE (DO THIS) section for that simplest path FIRST. If you believe a more complex approach is warranted (new abstractions, multi-file refactors, framework changes), you MUST include a "WHY NOT SIMPLER" line in the handoff explaining why the simple path is insufficient. If you cannot articulate a concrete reason, use the simpler path. Pay special attention to tasks involving auth, data access, multi-user features, and infrastructure \u2014 these are the most common over-engineering targets.
|
|
9126
9284
|
**Maturity gate applies here:** Do NOT generate BUILD HANDOFFs for tasks that failed the maturity gate in step 6. This includes raw tasks (\`maturity: "raw"\`) and tasks whose phase prerequisites are not met. Only cycle-ready tasks should receive handoffs.
|
|
9127
9285
|
**Security section guidance:** Each handoff includes a SECURITY CONSIDERATIONS section. Populate it when the task involves: data exposure risks (PII, secrets in logs/storage), secrets or credentials handling (API keys, tokens, env vars), auth/access control changes, or dependency security risks (new packages, version changes). For pure refactoring, documentation, prompt-text, or UI-only tasks, write "None \u2014 no security-relevant changes".
|
|
9128
9286
|
**Estimation calibration:** Tasks that wire existing adapter methods, add API routes following established patterns, modify prompts, or make documentation-only changes should be estimated **S** unless they require new abstractions, new DB tables, or multi-file architectural changes. Default to S for pattern-following work. Only use M when genuine new architecture is needed. If an "Estimation Calibration (Historical)" section is provided in the context below, use its data to adjust your estimates \u2014 it shows how often each estimated size matched the actual effort. Pay special attention to systematic over/under-estimation patterns (e.g. if M\u2192S happens frequently, estimate S instead of M for similar work).
|
|
9129
9287
|
**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.
|
|
9288
|
+
**Pre-build verification:** EVERY handoff MUST include a PRE-BUILD VERIFICATION section listing 2-5 specific file paths the builder should read before implementing. Derive these from FILES LIKELY TOUCHED \u2014 pick the files most likely to already contain the target functionality. This is the #1 prevention mechanism for wasted build slots (C120, C125, C126 all scheduled already-shipped work). If the builder finds >80% of the scope already implemented, they report "already built" instead of re-implementing.
|
|
9130
9289
|
**UI/visual task detection:** When a task's title or notes contain keywords suggesting frontend visual work (e.g. "visual", "design", "UI", "styling", "refresh", "frontend", "landing page", "hero", "carousel", "theme", "layout"), apply these handoff additions:
|
|
9131
9290
|
- Add to SCOPE: "Use the \`frontend-design\` skill for implementation \u2014 it produces higher-quality visual output than manual styling."
|
|
9132
9291
|
- Add to ACCEPTANCE CRITERIA: "[ ] Visually verify rendered output in browser before reporting done \u2014 provide localhost URL or screenshot to the user for review."
|
|
@@ -9147,7 +9306,7 @@ Standard planning cycle with full board review.
|
|
|
9147
9306
|
- The North Star changed or was validated in a way that the brief doesn't reflect
|
|
9148
9307
|
- A phase completed that shifts what the product IS (not just what was built)
|
|
9149
9308
|
- The brief describes capabilities, architecture, or direction that are no longer accurate
|
|
9150
|
-
- **
|
|
9309
|
+
- **DRIFT CHECK:** Compare the brief's content against current reality. The brief is drifted if: (a) it describes capabilities that don't exist or have been removed, (b) it references user types, architecture, or positioning that ADs have since changed, (c) the current phase/stage has shifted from what the brief describes, or (d) key metrics or success criteria no longer match the project's direction. Cycle count since last update is a secondary signal only \u2014 a brief updated 15 cycles ago that still accurately describes the product is NOT stale. A brief updated 3 cycles ago that contradicts a recent AD IS drifted.
|
|
9151
9310
|
If any of these apply, include an updated \`productBrief\` in the structured output. Include the FULL updated brief (not a diff). Preserve all existing sections and user-added content; update facts, numbers, and status to reflect current reality. Do not regenerate the brief every cycle \u2014 but do not let it go stale either.
|
|
9152
9311
|
|
|
9153
9312
|
13. **Forward Horizon** \u2014 If a Forward Horizon section is provided in the context below, write a "## Forward Horizon" section in Part 1. Surface 2-3 decisions the team should make before the next phase starts. Each item must be:
|
|
@@ -9345,7 +9504,7 @@ You MUST cover these 6 sections. Each is mandatory unless explicitly noted as co
|
|
|
9345
9504
|
- Is the product brief still an accurate description of what this product IS and WHERE it's going? If ADs have been created or superseded since the brief was last updated, the brief may be wrong.
|
|
9346
9505
|
- Has the target user changed? Has the scope expanded or contracted in ways the brief doesn't capture?
|
|
9347
9506
|
- Are we building for the right problem? Has evidence emerged (from builds, feedback, or market) that the core problem statement needs revision?
|
|
9348
|
-
-
|
|
9507
|
+
- Assess North Star drift: Does the North Star's key metric and success definition still align with the current phase, active ADs, and recent build directions? A North Star is drifted when: the metric it tracks is no longer the team's focus, the success criteria reference capabilities that have been deprioritised, or ADs have shifted the product direction away from what the North Star describes. Cycle count since last update is a secondary signal \u2014 a stable, accurate North Star is not stale regardless of age.
|
|
9349
9508
|
If this analysis reveals the brief needs updating, you MUST include updated content in \`productBriefUpdates\` in Part 2. Don't just note "the brief is stale" \u2014 write the update.
|
|
9350
9509
|
|
|
9351
9510
|
6. **Active Decision Review + Scoring** \u2014 For each non-superseded AD: is the confidence level still correct? Has evidence emerged that changes anything? Score on 5 dimensions (1-5, lower = better):
|
|
@@ -9397,11 +9556,11 @@ ${compressionJob}
|
|
|
9397
9556
|
- If no phase data is provided, skip this section.
|
|
9398
9557
|
Report findings in a "Hierarchy Assessment" section in Part 1. Persist findings in the \`stalePhases\` array in Part 2 (include stage/horizon observations too). If no issues found, omit the section and use an empty array.
|
|
9399
9558
|
|
|
9400
|
-
12. **Structural
|
|
9401
|
-
-
|
|
9402
|
-
- Carry-forward items that have persisted across **3+ cycles** without resolution \u2192 flag as stuck.
|
|
9403
|
-
- ADs with LOW confidence that have
|
|
9404
|
-
|
|
9559
|
+
12. **Structural Drift Detection** \u2014 If decision usage data is provided in context, identify structural decay using drift-based criteria (not pure cycle counts):
|
|
9560
|
+
- **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.
|
|
9561
|
+
- **Carry-forward drift:** Carry-forward items that have persisted across **3+ cycles** without resolution \u2192 flag as stuck.
|
|
9562
|
+
- **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.
|
|
9563
|
+
Use decision usage data as a secondary signal (unreferenced ADs are more likely to be drifted, but verify by checking content alignment). Report findings in a "Structural Drift" section in Part 1. Persist findings in the \`staleDecisions\` array in Part 2. If no issues found, omit the section and use an empty array.
|
|
9405
9564
|
|
|
9406
9565
|
## OUTPUT FORMAT
|
|
9407
9566
|
|
|
@@ -9422,7 +9581,7 @@ Then include conditional sections only if relevant:
|
|
|
9422
9581
|
- **Architecture Health** \u2014 only if issues found
|
|
9423
9582
|
- **Discovery Canvas Audit** \u2014 only if gaps or staleness found
|
|
9424
9583
|
- **Hierarchy Assessment** \u2014 only if hierarchy staleness, phase closure, or stage progression signals detected
|
|
9425
|
-
- **Structural
|
|
9584
|
+
- **Structural Drift** \u2014 only if drifted ADs or stuck carry-forwards found${compressionPart1}
|
|
9426
9585
|
|
|
9427
9586
|
### Part 2: Structured Data Block
|
|
9428
9587
|
After your natural language output, include this EXACT format on its own line:
|
|
@@ -9584,6 +9743,12 @@ function buildReviewUserMessage(ctx) {
|
|
|
9584
9743
|
if (ctx.recommendationEffectiveness) {
|
|
9585
9744
|
parts.push("### Recommendation Follow-Through", "", ctx.recommendationEffectiveness, "");
|
|
9586
9745
|
}
|
|
9746
|
+
if (ctx.adHocCommits) {
|
|
9747
|
+
parts.push("### Ad-hoc Work (Non-Task Commits)", "", ctx.adHocCommits, "");
|
|
9748
|
+
}
|
|
9749
|
+
if (ctx.pendingRecommendations) {
|
|
9750
|
+
parts.push("### Pending Strategy Recommendations", "", ctx.pendingRecommendations, "");
|
|
9751
|
+
}
|
|
9587
9752
|
return parts.join("\n");
|
|
9588
9753
|
}
|
|
9589
9754
|
function parseReviewStructuredOutput(raw) {
|
|
@@ -10069,9 +10234,19 @@ function autoCommitPapi(config2, cycleNumber, mode) {
|
|
|
10069
10234
|
return `Auto-commit failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
10070
10235
|
}
|
|
10071
10236
|
}
|
|
10072
|
-
|
|
10073
|
-
|
|
10237
|
+
var REC_EXPIRY_CYCLES = 3;
|
|
10238
|
+
function formatStrategyRecommendations(recs, currentCycle) {
|
|
10239
|
+
const active = [];
|
|
10240
|
+
const expired = [];
|
|
10074
10241
|
for (const rec of recs) {
|
|
10242
|
+
if (currentCycle !== void 0 && rec.createdCycle && currentCycle - rec.createdCycle > REC_EXPIRY_CYCLES) {
|
|
10243
|
+
expired.push(rec);
|
|
10244
|
+
} else {
|
|
10245
|
+
active.push(rec);
|
|
10246
|
+
}
|
|
10247
|
+
}
|
|
10248
|
+
const byType = /* @__PURE__ */ new Map();
|
|
10249
|
+
for (const rec of active) {
|
|
10075
10250
|
const list = byType.get(rec.type) ?? [];
|
|
10076
10251
|
list.push(rec);
|
|
10077
10252
|
byType.set(rec.type, list);
|
|
@@ -10091,6 +10266,12 @@ function formatStrategyRecommendations(recs) {
|
|
|
10091
10266
|
sections.push(`- (Cycle ${item.createdCycle}) ${item.content}${targetSuffix}`);
|
|
10092
10267
|
}
|
|
10093
10268
|
}
|
|
10269
|
+
if (expired.length > 0) {
|
|
10270
|
+
sections.push(`**Expired (${expired.length} recs skipped \u2014 older than ${REC_EXPIRY_CYCLES} cycles):**`);
|
|
10271
|
+
for (const item of expired) {
|
|
10272
|
+
sections.push(`- (Cycle ${item.createdCycle}) ${item.content.slice(0, 80)}...`);
|
|
10273
|
+
}
|
|
10274
|
+
}
|
|
10094
10275
|
return sections.join("\n");
|
|
10095
10276
|
}
|
|
10096
10277
|
function formatDiscoveryCanvas(canvas) {
|
|
@@ -10311,7 +10492,7 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
|
|
|
10311
10492
|
try {
|
|
10312
10493
|
const pendingRecs = await adapter2.getPendingRecommendations();
|
|
10313
10494
|
if (pendingRecs.length > 0) {
|
|
10314
|
-
strategyRecommendationsText2 = formatStrategyRecommendations(pendingRecs);
|
|
10495
|
+
strategyRecommendationsText2 = formatStrategyRecommendations(pendingRecs, health.totalCycles);
|
|
10315
10496
|
}
|
|
10316
10497
|
} catch {
|
|
10317
10498
|
}
|
|
@@ -11240,6 +11421,7 @@ ${result.userMessage}
|
|
|
11240
11421
|
// src/services/strategy.ts
|
|
11241
11422
|
init_dist2();
|
|
11242
11423
|
import { randomUUID as randomUUID8, createHash as createHash2 } from "crypto";
|
|
11424
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
11243
11425
|
|
|
11244
11426
|
// src/lib/phase-realign.ts
|
|
11245
11427
|
function extractPhaseNumber(phaseField) {
|
|
@@ -11556,6 +11738,45 @@ function formatTaskCompact(t) {
|
|
|
11556
11738
|
Reviewed: ${t.reviewed}${t.dependsOn ? ` | Depends on: ${t.dependsOn}` : ""}` + (notesSnippet ? `
|
|
11557
11739
|
Notes: ${notesSnippet}` : "") + "\n";
|
|
11558
11740
|
}
|
|
11741
|
+
function getAdHocCommits(projectRoot, sinceTag) {
|
|
11742
|
+
try {
|
|
11743
|
+
const logArgs = ["log", "--oneline", "--no-merges", "-100"];
|
|
11744
|
+
if (sinceTag) {
|
|
11745
|
+
logArgs.splice(1, 0, `${sinceTag}..HEAD`);
|
|
11746
|
+
}
|
|
11747
|
+
const output = execFileSync2("git", logArgs, {
|
|
11748
|
+
cwd: projectRoot,
|
|
11749
|
+
encoding: "utf-8",
|
|
11750
|
+
timeout: 5e3
|
|
11751
|
+
}).trim();
|
|
11752
|
+
if (!output) return void 0;
|
|
11753
|
+
const allCommits = output.split("\n");
|
|
11754
|
+
const taskPattern = /task-\d+/i;
|
|
11755
|
+
const nonTaskCommits = allCommits.filter((line) => !taskPattern.test(line));
|
|
11756
|
+
if (nonTaskCommits.length === 0) return void 0;
|
|
11757
|
+
const capped = nonTaskCommits.slice(0, 20);
|
|
11758
|
+
const groups = {};
|
|
11759
|
+
const typePattern = /^[a-f0-9]+ (feat|fix|chore|refactor|docs|style|test|ci|perf|build|release)[\s(:]/i;
|
|
11760
|
+
for (const line of capped) {
|
|
11761
|
+
const match = line.match(typePattern);
|
|
11762
|
+
const type = match ? match[1].toLowerCase() : "other";
|
|
11763
|
+
(groups[type] ??= []).push(line);
|
|
11764
|
+
}
|
|
11765
|
+
const lines = [];
|
|
11766
|
+
lines.push(`${nonTaskCommits.length} non-task commits found${nonTaskCommits.length > 20 ? " (showing 20 most recent)" : ""}:
|
|
11767
|
+
`);
|
|
11768
|
+
for (const [type, commits] of Object.entries(groups).sort((a, b2) => b2[1].length - a[1].length)) {
|
|
11769
|
+
lines.push(`**${type}** (${commits.length}):`);
|
|
11770
|
+
for (const c of commits) {
|
|
11771
|
+
lines.push(`- ${c}`);
|
|
11772
|
+
}
|
|
11773
|
+
lines.push("");
|
|
11774
|
+
}
|
|
11775
|
+
return lines.join("\n").trimEnd();
|
|
11776
|
+
} catch {
|
|
11777
|
+
return void 0;
|
|
11778
|
+
}
|
|
11779
|
+
}
|
|
11559
11780
|
async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, projectRoot) {
|
|
11560
11781
|
const lastReviewCycleNum = cycleNumber - cyclesSinceLastReview;
|
|
11561
11782
|
const [
|
|
@@ -11571,7 +11792,8 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
|
|
|
11571
11792
|
currentNorthStar,
|
|
11572
11793
|
canvas,
|
|
11573
11794
|
decisionUsage,
|
|
11574
|
-
recData
|
|
11795
|
+
recData,
|
|
11796
|
+
pendingRecs
|
|
11575
11797
|
] = await Promise.all([
|
|
11576
11798
|
adapter2.readProductBrief(),
|
|
11577
11799
|
adapter2.getActiveDecisions(),
|
|
@@ -11592,7 +11814,8 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
|
|
|
11592
11814
|
// Previously sequential — now parallel
|
|
11593
11815
|
adapter2.readDiscoveryCanvas().catch(() => ({})),
|
|
11594
11816
|
adapter2.getDecisionUsage(cycleNumber).catch(() => []),
|
|
11595
|
-
adapter2.getRecommendationEffectiveness?.()?.catch(() => []) ?? Promise.resolve([])
|
|
11817
|
+
adapter2.getRecommendationEffectiveness?.()?.catch(() => []) ?? Promise.resolve([]),
|
|
11818
|
+
adapter2.getPendingRecommendations().catch(() => [])
|
|
11596
11819
|
]);
|
|
11597
11820
|
const tasks = [...activeTasks, ...recentDoneTasks];
|
|
11598
11821
|
const recentLog = log;
|
|
@@ -11667,12 +11890,32 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
|
|
|
11667
11890
|
}
|
|
11668
11891
|
} catch {
|
|
11669
11892
|
}
|
|
11893
|
+
let pendingRecsText;
|
|
11894
|
+
try {
|
|
11895
|
+
if (pendingRecs.length > 0) {
|
|
11896
|
+
const lines = pendingRecs.map((r) => {
|
|
11897
|
+
const targetSuffix = r.target ? ` \u2192 ${r.target}` : "";
|
|
11898
|
+
return `- [${r.status}] (Cycle ${r.createdCycle}, ${r.type}) ${r.content}${targetSuffix}`;
|
|
11899
|
+
});
|
|
11900
|
+
pendingRecsText = `${pendingRecs.length} pending recommendation(s) from prior reviews:
|
|
11901
|
+
${lines.join("\n")}`;
|
|
11902
|
+
}
|
|
11903
|
+
} catch {
|
|
11904
|
+
}
|
|
11905
|
+
let adHocCommitsText;
|
|
11906
|
+
try {
|
|
11907
|
+
const sinceTag = `v0.${lastReviewCycleNum}.0`;
|
|
11908
|
+
adHocCommitsText = getAdHocCommits(projectRoot, sinceTag);
|
|
11909
|
+
} catch {
|
|
11910
|
+
}
|
|
11670
11911
|
logDataSourceSummary("strategy_review_audit", [
|
|
11671
11912
|
{ label: "discoveryCanvas", hasData: discoveryCanvasText !== void 0 },
|
|
11672
11913
|
{ label: "briefImplications", hasData: briefImplicationsText !== void 0 },
|
|
11673
11914
|
{ label: "phases", hasData: phasesText !== void 0 },
|
|
11674
11915
|
{ label: "decisionUsage", hasData: decisionUsageText !== void 0 },
|
|
11675
|
-
{ label: "recEffectiveness", hasData: recEffectivenessText !== void 0 }
|
|
11916
|
+
{ label: "recEffectiveness", hasData: recEffectivenessText !== void 0 },
|
|
11917
|
+
{ label: "pendingRecs", hasData: pendingRecsText !== void 0 },
|
|
11918
|
+
{ label: "adHocCommits", hasData: adHocCommitsText !== void 0 }
|
|
11676
11919
|
]);
|
|
11677
11920
|
const context = {
|
|
11678
11921
|
sessionNumber: cycleNumber,
|
|
@@ -11692,7 +11935,9 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
|
|
|
11692
11935
|
phases: phasesText,
|
|
11693
11936
|
decisionUsage: decisionUsageText,
|
|
11694
11937
|
northStar: currentNorthStar ?? void 0,
|
|
11695
|
-
recommendationEffectiveness: recEffectivenessText
|
|
11938
|
+
recommendationEffectiveness: recEffectivenessText,
|
|
11939
|
+
adHocCommits: adHocCommitsText,
|
|
11940
|
+
pendingRecommendations: pendingRecsText
|
|
11696
11941
|
};
|
|
11697
11942
|
const BUDGET_SOFT2 = 8e4;
|
|
11698
11943
|
const BUDGET_HARD2 = 1e5;
|
|
@@ -11904,8 +12149,16 @@ async function processReviewOutput(adapter2, rawOutput, cycleNumber) {
|
|
|
11904
12149
|
phaseChanges = await writeBack2(adapter2, cycleNumber, data, displayText);
|
|
11905
12150
|
} catch (err) {
|
|
11906
12151
|
writeBackFailed = err instanceof Error ? err.message : String(err);
|
|
12152
|
+
try {
|
|
12153
|
+
await adapter2.savePendingReviewResponse?.(cycleNumber, rawOutput);
|
|
12154
|
+
} catch {
|
|
12155
|
+
}
|
|
11907
12156
|
}
|
|
11908
12157
|
if (!writeBackFailed) {
|
|
12158
|
+
try {
|
|
12159
|
+
await adapter2.clearPendingReviewResponse?.();
|
|
12160
|
+
} catch {
|
|
12161
|
+
}
|
|
11909
12162
|
const webhookUrl = process.env.PAPI_SLACK_WEBHOOK_URL;
|
|
11910
12163
|
slackWarning = await sendSlackWebhook(webhookUrl, buildSlackSummary(data));
|
|
11911
12164
|
}
|
|
@@ -11953,6 +12206,28 @@ async function prepareStrategyReview(adapter2, force, projectRoot, adapterType)
|
|
|
11953
12206
|
isPg ? "Could not read cycle health from the database. Check your DATABASE_URL and verify the project exists." : "Could not read cycle health from PLANNING_LOG.md. Run setup first to initialise your PAPI project."
|
|
11954
12207
|
);
|
|
11955
12208
|
}
|
|
12209
|
+
try {
|
|
12210
|
+
const pending = await adapter2.getPendingReviewResponse?.();
|
|
12211
|
+
if (pending) {
|
|
12212
|
+
return {
|
|
12213
|
+
cycleNumber: pending.cycleNumber || cycleNumber,
|
|
12214
|
+
systemPrompt: "",
|
|
12215
|
+
userMessage: `\u26A0\uFE0F **Pending Strategy Review Found**
|
|
12216
|
+
|
|
12217
|
+
A previous strategy review (Cycle ${pending.cycleNumber || cycleNumber}) failed to write back. The raw LLM response has been preserved.
|
|
12218
|
+
|
|
12219
|
+
To retry, call \`strategy_review\` with:
|
|
12220
|
+
- \`mode\`: "apply"
|
|
12221
|
+
- \`llm_response\`: (the response below)
|
|
12222
|
+
- \`cycle_number\`: ${pending.cycleNumber || cycleNumber}
|
|
12223
|
+
|
|
12224
|
+
---
|
|
12225
|
+
|
|
12226
|
+
${pending.rawResponse}`
|
|
12227
|
+
};
|
|
12228
|
+
}
|
|
12229
|
+
} catch {
|
|
12230
|
+
}
|
|
11956
12231
|
let context;
|
|
11957
12232
|
try {
|
|
11958
12233
|
context = await assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, projectRoot);
|
|
@@ -12740,7 +13015,7 @@ var boardViewTool = {
|
|
|
12740
13015
|
};
|
|
12741
13016
|
var boardDeprioritiseTool = {
|
|
12742
13017
|
name: "board_deprioritise",
|
|
12743
|
-
description: `Remove a task from the current cycle.
|
|
13018
|
+
description: `Remove a task from the current cycle. Four actions: "backlog" (not now, maybe later \u2014 preserves handoff), "defer" (valid but premature \u2014 hidden from planner), "block" (waiting on external dependency \u2014 visible on board but skipped by planner), "cancel" (don't want this \u2014 permanently closed with reason). When a user rejects a task, ALWAYS ask which action they want. Does not call the Anthropic API.`,
|
|
12744
13019
|
inputSchema: {
|
|
12745
13020
|
type: "object",
|
|
12746
13021
|
properties: {
|
|
@@ -12750,8 +13025,8 @@ var boardDeprioritiseTool = {
|
|
|
12750
13025
|
},
|
|
12751
13026
|
action: {
|
|
12752
13027
|
type: "string",
|
|
12753
|
-
enum: ["backlog", "defer", "cancel"],
|
|
12754
|
-
description: `"backlog" = not now, maybe later (preserves handoff). "defer" = valid but premature (hidden from planner). "cancel" = don't want this at all (permanently closed). If omitted, defaults to "backlog" for backwards compatibility.`
|
|
13028
|
+
enum: ["backlog", "defer", "block", "cancel"],
|
|
13029
|
+
description: `"backlog" = not now, maybe later (preserves handoff). "defer" = valid but premature (hidden from planner). "block" = waiting on external dependency (visible but skipped by planner \u2014 reason required). "cancel" = don't want this at all (permanently closed). If omitted, defaults to "backlog" for backwards compatibility.`
|
|
12755
13030
|
},
|
|
12756
13031
|
reason: {
|
|
12757
13032
|
type: "string",
|
|
@@ -12872,6 +13147,29 @@ async function handleBoardDeprioritise(adapter2, args) {
|
|
|
12872
13147
|
const reason = args.reason;
|
|
12873
13148
|
const newPriority = args.priority;
|
|
12874
13149
|
const newPhase = args.phase;
|
|
13150
|
+
if (action === "block") {
|
|
13151
|
+
if (!reason) {
|
|
13152
|
+
return errorResponse("reason is required when blocking a task \u2014 explain what external dependency or gate is blocking it.");
|
|
13153
|
+
}
|
|
13154
|
+
try {
|
|
13155
|
+
const task = await adapter2.getTask(taskId);
|
|
13156
|
+
if (!task) return errorResponse(`Task ${taskId} not found.`);
|
|
13157
|
+
const existingNotes = task.notes ? `${task.notes}
|
|
13158
|
+
|
|
13159
|
+
` : "";
|
|
13160
|
+
await adapter2.updateTask(taskId, {
|
|
13161
|
+
status: "Blocked",
|
|
13162
|
+
notes: `${existingNotes}BLOCKED: ${reason}`
|
|
13163
|
+
});
|
|
13164
|
+
return textResponse(`Blocked **${taskId}** (${task.title}).
|
|
13165
|
+
|
|
13166
|
+
Reason: ${reason}
|
|
13167
|
+
|
|
13168
|
+
Task remains visible on the board but will be skipped by the planner and build_list.`);
|
|
13169
|
+
} catch (err) {
|
|
13170
|
+
return errorResponse(err instanceof Error ? err.message : String(err));
|
|
13171
|
+
}
|
|
13172
|
+
}
|
|
12875
13173
|
if (action === "cancel") {
|
|
12876
13174
|
if (!reason) {
|
|
12877
13175
|
return errorResponse("reason is required when cancelling a task.");
|
|
@@ -14008,6 +14306,8 @@ init_dist2();
|
|
|
14008
14306
|
|
|
14009
14307
|
// src/services/build.ts
|
|
14010
14308
|
import { randomUUID as randomUUID9 } from "crypto";
|
|
14309
|
+
import { readdirSync, existsSync } from "fs";
|
|
14310
|
+
import { join as join3 } from "path";
|
|
14011
14311
|
function capitalizeCompleted(value) {
|
|
14012
14312
|
const map = {
|
|
14013
14313
|
yes: "Yes",
|
|
@@ -14247,6 +14547,13 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
14247
14547
|
cycleNumber = 0;
|
|
14248
14548
|
}
|
|
14249
14549
|
const now = /* @__PURE__ */ new Date();
|
|
14550
|
+
let iterationCount = 1;
|
|
14551
|
+
try {
|
|
14552
|
+
const priorReports = await adapter2.getRecentBuildReports(200);
|
|
14553
|
+
const priorForTask = priorReports.filter((r) => r.taskId === taskId);
|
|
14554
|
+
iterationCount = priorForTask.length + 1;
|
|
14555
|
+
} catch {
|
|
14556
|
+
}
|
|
14250
14557
|
const report = {
|
|
14251
14558
|
uuid: randomUUID9(),
|
|
14252
14559
|
createdAt: now.toISOString(),
|
|
@@ -14264,7 +14571,8 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
14264
14571
|
handoffAccuracy: input.handoffAccuracy,
|
|
14265
14572
|
correctionsCount: input.correctionsCount,
|
|
14266
14573
|
briefImplications: input.briefImplications,
|
|
14267
|
-
deadEnds: input.deadEnds
|
|
14574
|
+
deadEnds: input.deadEnds,
|
|
14575
|
+
iterationCount
|
|
14268
14576
|
};
|
|
14269
14577
|
if (input.relatedDecisions) {
|
|
14270
14578
|
const adIds = input.relatedDecisions.split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -14291,7 +14599,8 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
14291
14599
|
}
|
|
14292
14600
|
const surpriseNote = input.surprises === "None" ? "" : ` Surprises: ${input.surprises}.`;
|
|
14293
14601
|
const issueNote = input.discoveredIssues === "None" ? "" : ` Issues: ${input.discoveredIssues}.`;
|
|
14294
|
-
const
|
|
14602
|
+
const iterNote = iterationCount > 1 ? ` Iterations: ${iterationCount} (${iterationCount - 1} pushback${iterationCount > 2 ? "s" : ""}).` : "";
|
|
14603
|
+
const buildReportSummary = `${capitalizeCompleted(input.completed)}. Effort ${input.effort} vs estimated ${input.estimatedEffort}.${iterNote}${surpriseNote}${issueNote}`;
|
|
14295
14604
|
await adapter2.updateTask(taskId, { buildReport: buildReportSummary });
|
|
14296
14605
|
if (input.completed === "yes") {
|
|
14297
14606
|
if (options.light) {
|
|
@@ -14332,6 +14641,32 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
14332
14641
|
phaseChanges = await propagatePhaseStatus(adapter2);
|
|
14333
14642
|
} catch {
|
|
14334
14643
|
}
|
|
14644
|
+
let docWarning;
|
|
14645
|
+
try {
|
|
14646
|
+
if (adapter2.searchDocs) {
|
|
14647
|
+
const docsDir = join3(config2.projectRoot, "docs");
|
|
14648
|
+
if (existsSync(docsDir)) {
|
|
14649
|
+
const scanDir = (dir) => {
|
|
14650
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
14651
|
+
const files = [];
|
|
14652
|
+
for (const e of entries) {
|
|
14653
|
+
const full = join3(dir, e.name);
|
|
14654
|
+
if (e.isDirectory()) files.push(...scanDir(full));
|
|
14655
|
+
else if (e.name.endsWith(".md")) files.push(full.replace(config2.projectRoot + "/", ""));
|
|
14656
|
+
}
|
|
14657
|
+
return files;
|
|
14658
|
+
};
|
|
14659
|
+
const mdFiles = scanDir(docsDir);
|
|
14660
|
+
const registered = await adapter2.searchDocs({ status: "active", limit: 500 });
|
|
14661
|
+
const registeredPaths = new Set(registered.map((d) => d.path));
|
|
14662
|
+
const unregistered = mdFiles.filter((f) => !registeredPaths.has(f));
|
|
14663
|
+
if (unregistered.length > 0) {
|
|
14664
|
+
docWarning = `${unregistered.length} unregistered doc(s) in docs/ \u2014 consider running \`doc_register\` for: ${unregistered.slice(0, 5).join(", ")}${unregistered.length > 5 ? ` (+${unregistered.length - 5} more)` : ""}`;
|
|
14665
|
+
}
|
|
14666
|
+
}
|
|
14667
|
+
}
|
|
14668
|
+
} catch {
|
|
14669
|
+
}
|
|
14335
14670
|
return {
|
|
14336
14671
|
task,
|
|
14337
14672
|
report,
|
|
@@ -14343,7 +14678,8 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
14343
14678
|
discoveredIssues: input.discoveredIssues,
|
|
14344
14679
|
completed: input.completed,
|
|
14345
14680
|
scopeAccuracy: input.scopeAccuracy,
|
|
14346
|
-
phaseChanges
|
|
14681
|
+
phaseChanges,
|
|
14682
|
+
docWarning
|
|
14347
14683
|
};
|
|
14348
14684
|
}
|
|
14349
14685
|
async function cancelBuild(adapter2, taskId, reason) {
|
|
@@ -14384,7 +14720,7 @@ var buildDescribeTool = {
|
|
|
14384
14720
|
};
|
|
14385
14721
|
var buildExecuteTool = {
|
|
14386
14722
|
name: "build_execute",
|
|
14387
|
-
description: "Start or complete a build task. Call with just task_id to start (returns BUILD HANDOFF, creates feature branch, marks In Progress). After implementing the task, you MUST call build_execute again with all report fields (completed, effort, estimated_effort, surprises, discovered_issues, architecture_notes) to finish \u2014 do not wait for user confirmation between start and complete. Does not call the Anthropic API.",
|
|
14723
|
+
description: "Start or complete a build task. Call with just task_id to start (returns BUILD HANDOFF, creates feature branch, marks In Progress). After implementing the task, you MUST call build_execute again with all report fields (completed, effort, estimated_effort, surprises, discovered_issues, architecture_notes) to finish \u2014 do not wait for user confirmation between start and complete. Does not call the Anthropic API. Set light=true to skip branch/PR creation (commits to current branch). Set PAPI_LIGHT_MODE=true in env to default all builds to light mode.",
|
|
14388
14724
|
inputSchema: {
|
|
14389
14725
|
type: "object",
|
|
14390
14726
|
properties: {
|
|
@@ -14550,7 +14886,7 @@ async function handleBuildExecute(adapter2, config2, args) {
|
|
|
14550
14886
|
if (!taskId) {
|
|
14551
14887
|
return errorResponse("task_id is required.");
|
|
14552
14888
|
}
|
|
14553
|
-
const light = args.light === true;
|
|
14889
|
+
const light = args.light === true || config2.lightMode;
|
|
14554
14890
|
if (hasReportFields(args)) {
|
|
14555
14891
|
return handleExecuteComplete(adapter2, config2, taskId, args, light);
|
|
14556
14892
|
}
|
|
@@ -14563,8 +14899,16 @@ async function handleBuildExecute(adapter2, config2, args) {
|
|
|
14563
14899
|
---
|
|
14564
14900
|
|
|
14565
14901
|
`;
|
|
14902
|
+
const verificationFiles = result.task.buildHandoff.verificationFiles ?? [];
|
|
14903
|
+
const verificationNote = verificationFiles.length > 0 ? `
|
|
14904
|
+
|
|
14905
|
+
---
|
|
14906
|
+
|
|
14907
|
+
**PRE-BUILD VERIFICATION:** Before writing any code, read these files and check if the functionality already exists:
|
|
14908
|
+
${verificationFiles.map((f) => `- ${f}`).join("\n")}
|
|
14909
|
+
If >80% of the scope is already implemented, call \`build_execute\` with completed="yes" and note "already built" in surprises instead of re-implementing.` : "";
|
|
14566
14910
|
const chainInstruction = "\n\n---\n\n**IMPORTANT:** After implementing this task, immediately call `build_execute` again with report fields (`completed`, `effort`, `estimated_effort`, `surprises`, `discovered_issues`, `architecture_notes`) to complete the build. Do not wait for user confirmation.";
|
|
14567
|
-
return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + chainInstruction + phaseNote);
|
|
14911
|
+
return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + verificationNote + chainInstruction + phaseNote);
|
|
14568
14912
|
} catch (err) {
|
|
14569
14913
|
if (isNoHandoffError(err)) {
|
|
14570
14914
|
const lines = [
|
|
@@ -14671,6 +15015,9 @@ function formatCompleteResult(result) {
|
|
|
14671
15015
|
lines.push(`Phase auto-updated: ${c.phaseId} ${c.oldStatus} \u2192 ${c.newStatus}`);
|
|
14672
15016
|
}
|
|
14673
15017
|
}
|
|
15018
|
+
if (result.docWarning) {
|
|
15019
|
+
lines.push("", `\u{1F4C4} ${result.docWarning}`);
|
|
15020
|
+
}
|
|
14674
15021
|
const hasDiscoveredIssues = result.discoveredIssues !== "None" && result.discoveredIssues.trim() !== "";
|
|
14675
15022
|
const remaining = result.cycleProgress.total - result.cycleProgress.completed;
|
|
14676
15023
|
if (result.completed !== "yes") {
|
|
@@ -15818,6 +16165,25 @@ async function getHealthSummary(adapter2) {
|
|
|
15818
16165
|
} catch (_err) {
|
|
15819
16166
|
metricsSection = "Could not read methodology metrics.";
|
|
15820
16167
|
}
|
|
16168
|
+
try {
|
|
16169
|
+
const recentReports = await adapter2.getRecentBuildReports(50);
|
|
16170
|
+
if (recentReports.length > 0) {
|
|
16171
|
+
const taskCounts = /* @__PURE__ */ new Map();
|
|
16172
|
+
for (const r of recentReports) {
|
|
16173
|
+
taskCounts.set(r.taskId, (taskCounts.get(r.taskId) ?? 0) + 1);
|
|
16174
|
+
}
|
|
16175
|
+
const iterCounts = [...taskCounts.values()];
|
|
16176
|
+
const avgIter = iterCounts.reduce((s, c) => s + c, 0) / iterCounts.length;
|
|
16177
|
+
const multiIterTasks = iterCounts.filter((c) => c > 1).length;
|
|
16178
|
+
if (avgIter > 1 || multiIterTasks > 0) {
|
|
16179
|
+
derivedMetricsSection += `
|
|
16180
|
+
|
|
16181
|
+
**Rework**
|
|
16182
|
+
- Average iterations: ${avgIter.toFixed(1)} (${multiIterTasks} task${multiIterTasks !== 1 ? "s" : ""} with pushbacks)`;
|
|
16183
|
+
}
|
|
16184
|
+
}
|
|
16185
|
+
} catch {
|
|
16186
|
+
}
|
|
15821
16187
|
const costSection = "Disabled \u2014 local MCP, no API costs.";
|
|
15822
16188
|
let decisionUsageSection = "";
|
|
15823
16189
|
try {
|
|
@@ -15991,7 +16357,7 @@ async function handleHealth(adapter2) {
|
|
|
15991
16357
|
|
|
15992
16358
|
// src/services/release.ts
|
|
15993
16359
|
import { writeFile as writeFile3 } from "fs/promises";
|
|
15994
|
-
import { join as
|
|
16360
|
+
import { join as join4 } from "path";
|
|
15995
16361
|
var INITIAL_RELEASE_NOTES = `# Changelog
|
|
15996
16362
|
|
|
15997
16363
|
## v0.1.0-alpha \u2014 Initial Release
|
|
@@ -16082,7 +16448,7 @@ async function createRelease(config2, branch, version, adapter2) {
|
|
|
16082
16448
|
const commits = getCommitsSinceTag(config2.projectRoot, latestTag);
|
|
16083
16449
|
changelogContent = generateChangelog(version, commits);
|
|
16084
16450
|
}
|
|
16085
|
-
const changelogPath =
|
|
16451
|
+
const changelogPath = join4(config2.projectRoot, "CHANGELOG.md");
|
|
16086
16452
|
await writeFile3(changelogPath, changelogContent, "utf-8");
|
|
16087
16453
|
const commitResult = stageAllAndCommit(config2.projectRoot, `release: ${version}`);
|
|
16088
16454
|
const commitNote = commitResult.committed ? `Committed CHANGELOG.md.` : `CHANGELOG.md: ${commitResult.message}`;
|
|
@@ -16163,8 +16529,8 @@ async function handleRelease(adapter2, config2, args) {
|
|
|
16163
16529
|
}
|
|
16164
16530
|
|
|
16165
16531
|
// src/tools/review.ts
|
|
16166
|
-
import { existsSync } from "fs";
|
|
16167
|
-
import { join as
|
|
16532
|
+
import { existsSync as existsSync2 } from "fs";
|
|
16533
|
+
import { join as join5 } from "path";
|
|
16168
16534
|
|
|
16169
16535
|
// src/services/review.ts
|
|
16170
16536
|
init_dist2();
|
|
@@ -16402,8 +16768,8 @@ function mergeAfterAccept(config2, taskId) {
|
|
|
16402
16768
|
}
|
|
16403
16769
|
const featureBranch = taskBranchName(taskId);
|
|
16404
16770
|
const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
|
|
16405
|
-
const papiDir =
|
|
16406
|
-
if (
|
|
16771
|
+
const papiDir = join5(config2.projectRoot, ".papi");
|
|
16772
|
+
if (existsSync2(papiDir)) {
|
|
16407
16773
|
try {
|
|
16408
16774
|
const commitResult = stageDirAndCommit(
|
|
16409
16775
|
config2.projectRoot,
|
|
@@ -16692,6 +17058,9 @@ Path: ${mcpJsonPath}`
|
|
|
16692
17058
|
}
|
|
16693
17059
|
|
|
16694
17060
|
// src/tools/orient.ts
|
|
17061
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
17062
|
+
import { readFileSync } from "fs";
|
|
17063
|
+
import { join as join6 } from "path";
|
|
16695
17064
|
var orientTool = {
|
|
16696
17065
|
name: "orient",
|
|
16697
17066
|
description: "Session orientation \u2014 single call that replaces build_list + health. Returns: cycle number, task counts by status, in-progress/in-review tasks, strategy review cadence, velocity snapshot, and recommended next action. Read-only, does not modify any files.",
|
|
@@ -16836,6 +17205,25 @@ async function getHierarchyPosition(adapter2) {
|
|
|
16836
17205
|
return void 0;
|
|
16837
17206
|
}
|
|
16838
17207
|
}
|
|
17208
|
+
function checkNpmVersionDrift() {
|
|
17209
|
+
try {
|
|
17210
|
+
const pkgPath = join6(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
|
|
17211
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
17212
|
+
const localVersion = pkg.version;
|
|
17213
|
+
const packageName = pkg.name;
|
|
17214
|
+
const published = execFileSync3("npm", ["view", packageName, "version"], {
|
|
17215
|
+
encoding: "utf-8",
|
|
17216
|
+
timeout: 3e3,
|
|
17217
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
17218
|
+
}).trim();
|
|
17219
|
+
if (published && published !== localVersion) {
|
|
17220
|
+
return `\u26A0\uFE0F npm version drift: local v${localVersion} vs published v${published}`;
|
|
17221
|
+
}
|
|
17222
|
+
return null;
|
|
17223
|
+
} catch {
|
|
17224
|
+
return null;
|
|
17225
|
+
}
|
|
17226
|
+
}
|
|
16839
17227
|
async function handleOrient(adapter2, config2) {
|
|
16840
17228
|
try {
|
|
16841
17229
|
const [buildResult, healthResult, hierarchy] = await Promise.all([
|
|
@@ -16884,7 +17272,28 @@ async function handleOrient(adapter2, config2) {
|
|
|
16884
17272
|
} catch {
|
|
16885
17273
|
}
|
|
16886
17274
|
}
|
|
16887
|
-
|
|
17275
|
+
const versionDrift = checkNpmVersionDrift();
|
|
17276
|
+
const versionNote = versionDrift ? `
|
|
17277
|
+
${versionDrift}` : "";
|
|
17278
|
+
let recsNote = "";
|
|
17279
|
+
try {
|
|
17280
|
+
const pendingRecs = await adapter2.getPendingRecommendations();
|
|
17281
|
+
if (pendingRecs.length > 0) {
|
|
17282
|
+
recsNote = `
|
|
17283
|
+
**Strategy Recommendations:** ${pendingRecs.length} pending action`;
|
|
17284
|
+
}
|
|
17285
|
+
} catch {
|
|
17286
|
+
}
|
|
17287
|
+
let pendingReviewNote = "";
|
|
17288
|
+
try {
|
|
17289
|
+
const pending = await adapter2.getPendingReviewResponse?.();
|
|
17290
|
+
if (pending) {
|
|
17291
|
+
pendingReviewNote = `
|
|
17292
|
+
\u26A0\uFE0F **Pending Strategy Review:** 1 review failed write-back (Cycle ${pending.cycleNumber}) \u2014 run \`strategy_review\` to retry.`;
|
|
17293
|
+
}
|
|
17294
|
+
} catch {
|
|
17295
|
+
}
|
|
17296
|
+
return textResponse(formatOrientSummary(healthResult, buildInfo, hierarchy) + ttfvNote + recsNote + pendingReviewNote + versionNote);
|
|
16888
17297
|
} catch (err) {
|
|
16889
17298
|
const message = err instanceof Error ? err.message : String(err);
|
|
16890
17299
|
return errorResponse(`Orient failed: ${message}`);
|
|
@@ -17311,6 +17720,132 @@ ${result.userMessage}
|
|
|
17311
17720
|
}
|
|
17312
17721
|
}
|
|
17313
17722
|
|
|
17723
|
+
// src/tools/doc-registry.ts
|
|
17724
|
+
var docRegisterTool = {
|
|
17725
|
+
name: "doc_register",
|
|
17726
|
+
description: "Register a document in the doc registry. Called after finalising a research/planning doc, or when build_execute detects unregistered docs. Stores metadata and structured summary \u2014 not full content.",
|
|
17727
|
+
inputSchema: {
|
|
17728
|
+
type: "object",
|
|
17729
|
+
properties: {
|
|
17730
|
+
path: { type: "string", description: 'Relative path from project root (e.g. "docs/research/funding-landscape.md").' },
|
|
17731
|
+
title: { type: "string", description: "Document title." },
|
|
17732
|
+
type: { type: "string", enum: ["research", "audit", "spec", "guide", "architecture", "positioning", "framework", "reference"], description: "Document type." },
|
|
17733
|
+
status: { type: "string", enum: ["active", "draft", "superseded", "actioned", "legacy", "archived"], description: 'Document status. Defaults to "active".' },
|
|
17734
|
+
summary: { type: "string", description: 'Structured 2-4 sentence summary. Format: "Conclusions: ... Open questions: ... Unactioned: ..."' },
|
|
17735
|
+
tags: { type: "array", items: { type: "string" }, description: "Tags from project vocabulary." },
|
|
17736
|
+
cycle: { type: "number", description: "Current cycle number." },
|
|
17737
|
+
actions: {
|
|
17738
|
+
type: "array",
|
|
17739
|
+
items: {
|
|
17740
|
+
type: "object",
|
|
17741
|
+
properties: {
|
|
17742
|
+
description: { type: "string" },
|
|
17743
|
+
status: { type: "string", enum: ["pending", "resolved"] },
|
|
17744
|
+
linkedTaskId: { type: "string" }
|
|
17745
|
+
},
|
|
17746
|
+
required: ["description", "status"]
|
|
17747
|
+
},
|
|
17748
|
+
description: "Actionable findings from the document."
|
|
17749
|
+
},
|
|
17750
|
+
superseded_by_path: { type: "string", description: "Path of the doc that supersedes this one (sets status to superseded)." }
|
|
17751
|
+
},
|
|
17752
|
+
required: ["path", "title", "type", "summary", "cycle"]
|
|
17753
|
+
}
|
|
17754
|
+
};
|
|
17755
|
+
var docSearchTool = {
|
|
17756
|
+
name: "doc_search",
|
|
17757
|
+
description: "Search the doc registry for documents by type, tags, keyword, or pending actions. Returns summaries, not full content. Use for context gathering in plan, strategy review, and idea dedup.",
|
|
17758
|
+
inputSchema: {
|
|
17759
|
+
type: "object",
|
|
17760
|
+
properties: {
|
|
17761
|
+
type: { type: "string", description: 'Filter by doc type (e.g. "research", "architecture").' },
|
|
17762
|
+
status: { type: "string", description: 'Filter by status. Defaults to "active".' },
|
|
17763
|
+
tags: { type: "array", items: { type: "string" }, description: "Filter by tags (OR match)." },
|
|
17764
|
+
keyword: { type: "string", description: "Search title and summary text." },
|
|
17765
|
+
has_pending_actions: { type: "boolean", description: "Only docs with unresolved action items." },
|
|
17766
|
+
since_cycle: { type: "number", description: "Docs updated since this cycle." },
|
|
17767
|
+
limit: { type: "number", description: "Max results (default: 10)." }
|
|
17768
|
+
},
|
|
17769
|
+
required: []
|
|
17770
|
+
}
|
|
17771
|
+
};
|
|
17772
|
+
async function handleDocRegister(adapter2, args) {
|
|
17773
|
+
if (!adapter2.registerDoc) {
|
|
17774
|
+
return errorResponse("Doc registry not available \u2014 requires pg adapter.");
|
|
17775
|
+
}
|
|
17776
|
+
const path5 = args.path;
|
|
17777
|
+
const title = args.title;
|
|
17778
|
+
const type = args.type;
|
|
17779
|
+
const status = args.status ?? "active";
|
|
17780
|
+
const summary = args.summary;
|
|
17781
|
+
const tags = args.tags ?? [];
|
|
17782
|
+
const cycle = args.cycle;
|
|
17783
|
+
const actions = args.actions;
|
|
17784
|
+
const supersededByPath = args.superseded_by_path;
|
|
17785
|
+
if (!path5 || !title || !type || !summary || !cycle) {
|
|
17786
|
+
return errorResponse("Required fields: path, title, type, summary, cycle.");
|
|
17787
|
+
}
|
|
17788
|
+
let supersededBy;
|
|
17789
|
+
if (supersededByPath) {
|
|
17790
|
+
const existing = await adapter2.getDoc?.(supersededByPath);
|
|
17791
|
+
if (existing) {
|
|
17792
|
+
supersededBy = existing.id;
|
|
17793
|
+
await adapter2.updateDocStatus?.(existing.id, "superseded", void 0);
|
|
17794
|
+
}
|
|
17795
|
+
}
|
|
17796
|
+
const entry = await adapter2.registerDoc({
|
|
17797
|
+
title,
|
|
17798
|
+
type,
|
|
17799
|
+
path: path5,
|
|
17800
|
+
status: supersededByPath ? "superseded" : status,
|
|
17801
|
+
summary,
|
|
17802
|
+
tags,
|
|
17803
|
+
cycleCreated: cycle,
|
|
17804
|
+
cycleUpdated: cycle,
|
|
17805
|
+
supersededBy,
|
|
17806
|
+
actions
|
|
17807
|
+
});
|
|
17808
|
+
return textResponse(
|
|
17809
|
+
`**Registered:** ${entry.title}
|
|
17810
|
+
- **Path:** ${entry.path}
|
|
17811
|
+
- **Type:** ${entry.type} | **Status:** ${entry.status}
|
|
17812
|
+
- **Tags:** ${entry.tags.length > 0 ? entry.tags.join(", ") : "none"}
|
|
17813
|
+
- **Actions:** ${actions?.length ?? 0} items
|
|
17814
|
+
- **ID:** ${entry.id}`
|
|
17815
|
+
);
|
|
17816
|
+
}
|
|
17817
|
+
async function handleDocSearch(adapter2, args) {
|
|
17818
|
+
if (!adapter2.searchDocs) {
|
|
17819
|
+
return errorResponse("Doc registry not available \u2014 requires pg adapter.");
|
|
17820
|
+
}
|
|
17821
|
+
const input = {
|
|
17822
|
+
type: args.type,
|
|
17823
|
+
status: args.status,
|
|
17824
|
+
tags: args.tags,
|
|
17825
|
+
keyword: args.keyword,
|
|
17826
|
+
hasPendingActions: args.has_pending_actions,
|
|
17827
|
+
sinceCycle: args.since_cycle,
|
|
17828
|
+
limit: args.limit
|
|
17829
|
+
};
|
|
17830
|
+
const docs = await adapter2.searchDocs(input);
|
|
17831
|
+
if (docs.length === 0) {
|
|
17832
|
+
return textResponse("No documents found matching the search criteria.");
|
|
17833
|
+
}
|
|
17834
|
+
const lines = docs.map((d) => {
|
|
17835
|
+
const actionCount = d.actions?.filter((a) => a.status === "pending").length ?? 0;
|
|
17836
|
+
const actionNote = actionCount > 0 ? ` | ${actionCount} pending action(s)` : "";
|
|
17837
|
+
return `### ${d.title}
|
|
17838
|
+
**Type:** ${d.type} | **Status:** ${d.status} | **Cycle:** ${d.cycleCreated}${d.cycleUpdated ? `\u2192${d.cycleUpdated}` : ""}${actionNote}
|
|
17839
|
+
**Path:** ${d.path}
|
|
17840
|
+
**Tags:** ${d.tags.length > 0 ? d.tags.join(", ") : "none"}
|
|
17841
|
+
${d.summary}
|
|
17842
|
+
`;
|
|
17843
|
+
});
|
|
17844
|
+
return textResponse(`**${docs.length} document(s) found:**
|
|
17845
|
+
|
|
17846
|
+
${lines.join("\n---\n\n")}`);
|
|
17847
|
+
}
|
|
17848
|
+
|
|
17314
17849
|
// src/lib/telemetry.ts
|
|
17315
17850
|
var TELEMETRY_SUPABASE_URL = "https://guewgygcpcmrcoppihzx.supabase.co";
|
|
17316
17851
|
var TELEMETRY_API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd1ZXdneWdjcGNtcmNvcHBpaHp4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI2Njk2NTMsImV4cCI6MjA4ODI0NTY1M30.V5Jw7wJgiMpSQPa2mt0ftjyye5ynG1qLlam00yPVNJY";
|
|
@@ -17407,7 +17942,9 @@ function createServer(adapter2, config2) {
|
|
|
17407
17942
|
initTool,
|
|
17408
17943
|
orientTool,
|
|
17409
17944
|
hierarchyUpdateTool,
|
|
17410
|
-
zoomOutTool
|
|
17945
|
+
zoomOutTool,
|
|
17946
|
+
docRegisterTool,
|
|
17947
|
+
docSearchTool
|
|
17411
17948
|
]
|
|
17412
17949
|
}));
|
|
17413
17950
|
server2.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -17508,6 +18045,12 @@ function createServer(adapter2, config2) {
|
|
|
17508
18045
|
case "zoom_out":
|
|
17509
18046
|
result = await handleZoomOut(adapter2, config2, safeArgs);
|
|
17510
18047
|
break;
|
|
18048
|
+
case "doc_register":
|
|
18049
|
+
result = await handleDocRegister(adapter2, safeArgs);
|
|
18050
|
+
break;
|
|
18051
|
+
case "doc_search":
|
|
18052
|
+
result = await handleDocSearch(adapter2, safeArgs);
|
|
18053
|
+
break;
|
|
17511
18054
|
default:
|
|
17512
18055
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
|
|
17513
18056
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@papi-ai/server",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "PAPI MCP server — AI-powered sprint planning, build execution, and strategy review for software projects",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
8
8
|
"exports": {
|
|
9
|
-
".": "./dist/index.js"
|
|
10
|
-
"./prompts": "./src/prompts.ts"
|
|
9
|
+
".": "./dist/index.js"
|
|
11
10
|
},
|
|
12
11
|
"bin": {
|
|
13
12
|
"papi-server": "./dist/index.js"
|