@papi-ai/server 0.7.23 → 0.7.25
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 +637 -197
- package/dist/prompts.js +30 -149
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4119,6 +4119,10 @@ var init_large = __esm({
|
|
|
4119
4119
|
});
|
|
4120
4120
|
|
|
4121
4121
|
// ../../node_modules/postgres/src/index.js
|
|
4122
|
+
var src_exports = {};
|
|
4123
|
+
__export(src_exports, {
|
|
4124
|
+
default: () => src_default
|
|
4125
|
+
});
|
|
4122
4126
|
import os from "os";
|
|
4123
4127
|
import fs2 from "fs";
|
|
4124
4128
|
function Postgres(a, b2) {
|
|
@@ -4776,6 +4780,7 @@ function rowToToolCallMetric(row) {
|
|
|
4776
4780
|
if (row.cycle_number != null) metric.cycleNumber = row.cycle_number;
|
|
4777
4781
|
if (row.context_bytes != null) metric.contextBytes = row.context_bytes;
|
|
4778
4782
|
if (row.context_utilisation != null) metric.contextUtilisation = parseFloat(row.context_utilisation);
|
|
4783
|
+
if (row.client_name != null) metric.clientName = row.client_name;
|
|
4779
4784
|
return metric;
|
|
4780
4785
|
}
|
|
4781
4786
|
function rowToCostSnapshot(row) {
|
|
@@ -7302,33 +7307,38 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
|
7302
7307
|
}
|
|
7303
7308
|
async getCycleLearnings(opts) {
|
|
7304
7309
|
const limit = opts?.limit ?? 50;
|
|
7310
|
+
const resolvedFilter = opts?.includeResolved ? this.sql`` : this.sql`AND resolved_at IS NULL`;
|
|
7305
7311
|
let rows;
|
|
7306
7312
|
if (opts?.cycleNumber && opts?.category) {
|
|
7307
7313
|
rows = await this.sql`
|
|
7308
|
-
SELECT id, project_id, task_id, cycle_number, category, severity, summary, detail, tags, related_decision, action_taken, action_ref, created_at
|
|
7314
|
+
SELECT id, project_id, task_id, cycle_number, category, severity, summary, detail, tags, related_decision, action_taken, action_ref, resolved_at, resolved_by, created_at
|
|
7309
7315
|
FROM cycle_learnings
|
|
7310
7316
|
WHERE project_id = ${this.projectId} AND cycle_number = ${opts.cycleNumber} AND category = ${opts.category}
|
|
7317
|
+
${resolvedFilter}
|
|
7311
7318
|
ORDER BY created_at DESC LIMIT ${limit}
|
|
7312
7319
|
`;
|
|
7313
7320
|
} else if (opts?.cycleNumber) {
|
|
7314
7321
|
rows = await this.sql`
|
|
7315
|
-
SELECT id, project_id, task_id, cycle_number, category, severity, summary, detail, tags, related_decision, action_taken, action_ref, created_at
|
|
7322
|
+
SELECT id, project_id, task_id, cycle_number, category, severity, summary, detail, tags, related_decision, action_taken, action_ref, resolved_at, resolved_by, created_at
|
|
7316
7323
|
FROM cycle_learnings
|
|
7317
7324
|
WHERE project_id = ${this.projectId} AND cycle_number = ${opts.cycleNumber}
|
|
7325
|
+
${resolvedFilter}
|
|
7318
7326
|
ORDER BY created_at DESC LIMIT ${limit}
|
|
7319
7327
|
`;
|
|
7320
7328
|
} else if (opts?.category) {
|
|
7321
7329
|
rows = await this.sql`
|
|
7322
|
-
SELECT id, project_id, task_id, cycle_number, category, severity, summary, detail, tags, related_decision, action_taken, action_ref, created_at
|
|
7330
|
+
SELECT id, project_id, task_id, cycle_number, category, severity, summary, detail, tags, related_decision, action_taken, action_ref, resolved_at, resolved_by, created_at
|
|
7323
7331
|
FROM cycle_learnings
|
|
7324
7332
|
WHERE project_id = ${this.projectId} AND category = ${opts.category}
|
|
7333
|
+
${resolvedFilter}
|
|
7325
7334
|
ORDER BY created_at DESC LIMIT ${limit}
|
|
7326
7335
|
`;
|
|
7327
7336
|
} else {
|
|
7328
7337
|
rows = await this.sql`
|
|
7329
|
-
SELECT id, project_id, task_id, cycle_number, category, severity, summary, detail, tags, related_decision, action_taken, action_ref, created_at
|
|
7338
|
+
SELECT id, project_id, task_id, cycle_number, category, severity, summary, detail, tags, related_decision, action_taken, action_ref, resolved_at, resolved_by, created_at
|
|
7330
7339
|
FROM cycle_learnings
|
|
7331
7340
|
WHERE project_id = ${this.projectId}
|
|
7341
|
+
${resolvedFilter}
|
|
7332
7342
|
ORDER BY created_at DESC LIMIT ${limit}
|
|
7333
7343
|
`;
|
|
7334
7344
|
}
|
|
@@ -7345,6 +7355,8 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
|
7345
7355
|
relatedDecision: r.related_decision ?? void 0,
|
|
7346
7356
|
actionTaken: r.action_taken ?? void 0,
|
|
7347
7357
|
actionRef: r.action_ref ?? void 0,
|
|
7358
|
+
resolvedAt: r.resolved_at ? r.resolved_at.toISOString() : void 0,
|
|
7359
|
+
resolvedBy: r.resolved_by ?? void 0,
|
|
7348
7360
|
createdAt: r.created_at ? r.created_at.toISOString() : void 0
|
|
7349
7361
|
}));
|
|
7350
7362
|
}
|
|
@@ -7355,6 +7367,21 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
|
7355
7367
|
WHERE id = ${learningId}
|
|
7356
7368
|
AND project_id = ${this.projectId}
|
|
7357
7369
|
AND action_ref IS NULL
|
|
7370
|
+
`;
|
|
7371
|
+
}
|
|
7372
|
+
/**
|
|
7373
|
+
* Mark a cycle_learnings row resolved (task-1541, C277). Idempotent — re-resolving
|
|
7374
|
+
* an already-resolved row is a no-op (preserves the original resolved_at timestamp).
|
|
7375
|
+
* Scoped to the bound project_id so cross-tenant writes are impossible.
|
|
7376
|
+
*/
|
|
7377
|
+
async markCycleLearningResolved(learningId, resolvedBy) {
|
|
7378
|
+
await this.sql`
|
|
7379
|
+
UPDATE cycle_learnings
|
|
7380
|
+
SET resolved_at = now(),
|
|
7381
|
+
resolved_by = ${resolvedBy ?? null}
|
|
7382
|
+
WHERE id = ${learningId}
|
|
7383
|
+
AND project_id = ${this.projectId}
|
|
7384
|
+
AND resolved_at IS NULL
|
|
7358
7385
|
`;
|
|
7359
7386
|
}
|
|
7360
7387
|
async getCycleLearningPatterns() {
|
|
@@ -7570,20 +7597,21 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
|
7570
7597
|
INSERT INTO tool_call_metrics (
|
|
7571
7598
|
project_id, timestamp, tool, duration_ms,
|
|
7572
7599
|
input_tokens, output_tokens, estimated_cost_usd, model, cycle_number,
|
|
7573
|
-
context_bytes, context_utilisation, success
|
|
7600
|
+
context_bytes, context_utilisation, success, client_name
|
|
7574
7601
|
) VALUES (
|
|
7575
7602
|
${this.projectId}, ${metric.timestamp}, ${metric.tool}, ${metric.durationMs},
|
|
7576
7603
|
${metric.inputTokens ?? null}, ${metric.outputTokens ?? null},
|
|
7577
7604
|
${metric.estimatedCostUsd ?? null}, ${metric.model ?? null},
|
|
7578
7605
|
${metric.cycleNumber ?? null},
|
|
7579
7606
|
${metric.contextBytes ?? null}, ${metric.contextUtilisation ?? null},
|
|
7580
|
-
${metric.success ?? true}
|
|
7607
|
+
${metric.success ?? true},
|
|
7608
|
+
${metric.clientName ?? null}
|
|
7581
7609
|
)
|
|
7582
7610
|
`;
|
|
7583
7611
|
}
|
|
7584
7612
|
async readToolMetrics() {
|
|
7585
7613
|
const rows = await this.sql`
|
|
7586
|
-
SELECT timestamp, tool, duration_ms, input_tokens, output_tokens, estimated_cost_usd, model, cycle_number, context_bytes, context_utilisation
|
|
7614
|
+
SELECT timestamp, tool, duration_ms, input_tokens, output_tokens, estimated_cost_usd, model, cycle_number, context_bytes, context_utilisation, client_name
|
|
7587
7615
|
FROM tool_call_metrics
|
|
7588
7616
|
WHERE project_id = ${this.projectId}
|
|
7589
7617
|
ORDER BY timestamp
|
|
@@ -8199,6 +8227,14 @@ ${r.content}` + (r.carry_forward ? `
|
|
|
8199
8227
|
UPDATE projects SET papi_dir = ${papiDir}, updated_at = now() WHERE id = ${this.projectId}
|
|
8200
8228
|
`;
|
|
8201
8229
|
}
|
|
8230
|
+
/**
|
|
8231
|
+
* Public projection of the private ownerUserId helper. Used by verifyProject
|
|
8232
|
+
* (task-1621) to detect legacy pre-multi-user project rows where user_id is
|
|
8233
|
+
* still NULL. Read-only.
|
|
8234
|
+
*/
|
|
8235
|
+
async getProjectOwnerUserId() {
|
|
8236
|
+
return this.ownerUserId();
|
|
8237
|
+
}
|
|
8202
8238
|
// -------------------------------------------------------------------------
|
|
8203
8239
|
// Project lifecycle (task-1888 / 1885-C)
|
|
8204
8240
|
// Scoped to the SAME user_id as the currently-bound project — a stdio user on
|
|
@@ -9346,6 +9382,7 @@ __export(git_exports, {
|
|
|
9346
9382
|
getStagedFiles: () => getStagedFiles,
|
|
9347
9383
|
getTaskIdsOnBranch: () => getTaskIdsOnBranch,
|
|
9348
9384
|
getUnmergedBranches: () => getUnmergedBranches,
|
|
9385
|
+
getUntrackedFiles: () => getUntrackedFiles,
|
|
9349
9386
|
gitPull: () => gitPull,
|
|
9350
9387
|
gitPush: () => gitPush,
|
|
9351
9388
|
hasRemote: () => hasRemote,
|
|
@@ -9473,6 +9510,18 @@ function getModifiedFiles(cwd) {
|
|
|
9473
9510
|
return [];
|
|
9474
9511
|
}
|
|
9475
9512
|
}
|
|
9513
|
+
function getUntrackedFiles(cwd) {
|
|
9514
|
+
try {
|
|
9515
|
+
const out = execFileSync("git", ["ls-files", "--others", "--exclude-standard"], {
|
|
9516
|
+
cwd,
|
|
9517
|
+
encoding: "utf-8"
|
|
9518
|
+
}).replace(/\n+$/, "");
|
|
9519
|
+
if (!out) return [];
|
|
9520
|
+
return out.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
9521
|
+
} catch {
|
|
9522
|
+
return [];
|
|
9523
|
+
}
|
|
9524
|
+
}
|
|
9476
9525
|
function getBranchDiff(cwd, base = "origin/main", maxBytes = 2e5) {
|
|
9477
9526
|
const refs = [`${base}...HEAD`, "main...HEAD"];
|
|
9478
9527
|
for (const ref of refs) {
|
|
@@ -9931,7 +9980,8 @@ function taskBranchName(taskId) {
|
|
|
9931
9980
|
return `feat/${taskId}`;
|
|
9932
9981
|
}
|
|
9933
9982
|
function cycleBranchName(cycleNumber, module) {
|
|
9934
|
-
|
|
9983
|
+
const slug = module.toLowerCase().replace(/&/g, "and").replace(/&/g, "and").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
9984
|
+
return `feat/cycle-${cycleNumber}-${slug}`;
|
|
9935
9985
|
}
|
|
9936
9986
|
function getHeadCommitSha(cwd) {
|
|
9937
9987
|
try {
|
|
@@ -10281,17 +10331,133 @@ function formatReport(report) {
|
|
|
10281
10331
|
lines.push("Need recovery? In the dashboard: Settings \u2192 Reset connection. Or surgically clean .mcp.json: `npx @papi-ai/server reset`.");
|
|
10282
10332
|
return lines.join("\n");
|
|
10283
10333
|
}
|
|
10284
|
-
|
|
10334
|
+
function resolveDatabaseUrl() {
|
|
10335
|
+
if (process.env.DATABASE_URL) return process.env.DATABASE_URL;
|
|
10336
|
+
const mcpEnv = findMcpJson();
|
|
10337
|
+
return mcpEnv?.vars.DATABASE_URL;
|
|
10338
|
+
}
|
|
10339
|
+
function classifyWedged(rows) {
|
|
10340
|
+
const wedged = [];
|
|
10341
|
+
for (const r of rows) {
|
|
10342
|
+
const state2 = (r.state ?? "").toLowerCase();
|
|
10343
|
+
const stateAge = Number(r.state_age ?? 0);
|
|
10344
|
+
const queryAge = Number(r.query_age ?? 0);
|
|
10345
|
+
if (state2.startsWith("idle in transaction") && stateAge > WEDGED_IDLE_TX_SECONDS) {
|
|
10346
|
+
wedged.push({ pid: r.pid, state: r.state ?? "unknown", ageSeconds: Math.round(stateAge), reason: "idle-in-transaction" });
|
|
10347
|
+
} else if (state2 === "active" && queryAge > WEDGED_ACTIVE_SECONDS) {
|
|
10348
|
+
wedged.push({ pid: r.pid, state: r.state ?? "unknown", ageSeconds: Math.round(queryAge), reason: "long-active" });
|
|
10349
|
+
}
|
|
10350
|
+
}
|
|
10351
|
+
return wedged;
|
|
10352
|
+
}
|
|
10353
|
+
async function diagnosePool(opts) {
|
|
10354
|
+
const dbUrl = resolveDatabaseUrl();
|
|
10355
|
+
if (!dbUrl) {
|
|
10356
|
+
return { status: "skipped", detail: "no DATABASE_URL \u2014 DB pool diagnostics skipped", wedged: [], cleanupRequested: opts.fix };
|
|
10357
|
+
}
|
|
10358
|
+
let factory;
|
|
10359
|
+
try {
|
|
10360
|
+
factory = (await Promise.resolve().then(() => (init_src(), src_exports))).default;
|
|
10361
|
+
} catch {
|
|
10362
|
+
return { status: "skipped", detail: "postgres client not resolvable \u2014 DB pool diagnostics skipped", wedged: [], cleanupRequested: opts.fix };
|
|
10363
|
+
}
|
|
10364
|
+
const sql = factory(dbUrl, { max: 1, idle_timeout: 5, connect_timeout: 10 });
|
|
10365
|
+
try {
|
|
10366
|
+
const rows = await sql`
|
|
10367
|
+
SELECT pid,
|
|
10368
|
+
state,
|
|
10369
|
+
EXTRACT(EPOCH FROM (now() - state_change))::int AS state_age,
|
|
10370
|
+
EXTRACT(EPOCH FROM (now() - query_start))::int AS query_age
|
|
10371
|
+
FROM pg_stat_activity
|
|
10372
|
+
WHERE usename = current_user
|
|
10373
|
+
AND backend_type = 'client backend'
|
|
10374
|
+
AND pid <> pg_backend_pid()`;
|
|
10375
|
+
const total = rows.length;
|
|
10376
|
+
const active = rows.filter((r) => (r.state ?? "") === "active").length;
|
|
10377
|
+
const idle = rows.filter((r) => (r.state ?? "") === "idle").length;
|
|
10378
|
+
const idleInTransaction = rows.filter((r) => (r.state ?? "").toLowerCase().startsWith("idle in transaction")).length;
|
|
10379
|
+
const wedged = classifyWedged(rows);
|
|
10380
|
+
if (opts.fix && wedged.length > 0) {
|
|
10381
|
+
for (const w of wedged) {
|
|
10382
|
+
try {
|
|
10383
|
+
await sql`SELECT pg_terminate_backend(pid)
|
|
10384
|
+
FROM pg_stat_activity
|
|
10385
|
+
WHERE pid = ${w.pid} AND usename = current_user`;
|
|
10386
|
+
w.terminated = "yes";
|
|
10387
|
+
} catch {
|
|
10388
|
+
w.terminated = "failed";
|
|
10389
|
+
}
|
|
10390
|
+
}
|
|
10391
|
+
} else if (wedged.length > 0) {
|
|
10392
|
+
for (const w of wedged) w.terminated = "no";
|
|
10393
|
+
}
|
|
10394
|
+
return { status: "ok", total, active, idle, idleInTransaction, wedged, cleanupRequested: opts.fix };
|
|
10395
|
+
} catch (err) {
|
|
10396
|
+
return {
|
|
10397
|
+
status: "error",
|
|
10398
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
10399
|
+
wedged: [],
|
|
10400
|
+
cleanupRequested: opts.fix
|
|
10401
|
+
};
|
|
10402
|
+
} finally {
|
|
10403
|
+
try {
|
|
10404
|
+
await sql.end({ timeout: 5 });
|
|
10405
|
+
} catch {
|
|
10406
|
+
}
|
|
10407
|
+
}
|
|
10408
|
+
}
|
|
10409
|
+
function formatPoolReport(pool) {
|
|
10410
|
+
const lines = [];
|
|
10411
|
+
lines.push("## DB pool / backend state");
|
|
10412
|
+
if (pool.status === "skipped") {
|
|
10413
|
+
lines.push(` \u2022 ${pool.detail}`);
|
|
10414
|
+
return lines.join("\n");
|
|
10415
|
+
}
|
|
10416
|
+
if (pool.status === "error") {
|
|
10417
|
+
lines.push(` \u2022 DB pool check failed: ${pool.detail}`);
|
|
10418
|
+
lines.push(" \u2192 If this is a permissions error, pg_stat_activity / pg_terminate_backend need a role with sufficient privileges.");
|
|
10419
|
+
return lines.join("\n");
|
|
10420
|
+
}
|
|
10421
|
+
lines.push(` Connections for this role: ${pool.total} total \u2014 ${pool.active} active, ${pool.idle} idle, ${pool.idleInTransaction} idle-in-transaction`);
|
|
10422
|
+
if (pool.wedged.length === 0) {
|
|
10423
|
+
lines.push(" \u2713 No wedged backends detected.");
|
|
10424
|
+
return lines.join("\n");
|
|
10425
|
+
}
|
|
10426
|
+
lines.push(` \u26A0 ${pool.wedged.length} wedged backend(s) detected:`);
|
|
10427
|
+
for (const w of pool.wedged) {
|
|
10428
|
+
const verb = w.terminated === "yes" ? "terminated" : w.terminated === "failed" ? "terminate FAILED (insufficient privilege?)" : "would terminate (run with --fix-pool)";
|
|
10429
|
+
lines.push(` pid ${w.pid} \u2014 ${w.reason}, ${w.ageSeconds}s in state "${w.state}" \u2192 ${verb}`);
|
|
10430
|
+
}
|
|
10431
|
+
if (!pool.cleanupRequested) {
|
|
10432
|
+
lines.push(" \u2192 To clear them: `npx @papi-ai/server doctor --fix-pool` (terminates only this role's confirmed-wedged backends).");
|
|
10433
|
+
}
|
|
10434
|
+
return lines.join("\n");
|
|
10435
|
+
}
|
|
10436
|
+
async function runDoctor(cliArgs2 = []) {
|
|
10285
10437
|
const report = diagnose();
|
|
10286
10438
|
process.stdout.write(formatReport(report) + "\n");
|
|
10439
|
+
const fixPool = cliArgs2.includes("--fix-pool") || cliArgs2.includes("--terminate-wedged");
|
|
10440
|
+
const pool = await diagnosePool({ fix: fixPool });
|
|
10441
|
+
process.stdout.write("\n" + formatPoolReport(pool) + "\n");
|
|
10287
10442
|
return 0;
|
|
10288
10443
|
}
|
|
10289
|
-
var SECRET_VARS, __testing;
|
|
10444
|
+
var SECRET_VARS, WEDGED_IDLE_TX_SECONDS, WEDGED_ACTIVE_SECONDS, __testing;
|
|
10290
10445
|
var init_doctor = __esm({
|
|
10291
10446
|
"src/cli/doctor.ts"() {
|
|
10292
10447
|
"use strict";
|
|
10293
10448
|
SECRET_VARS = /* @__PURE__ */ new Set(["PAPI_DATA_API_KEY", "DATABASE_URL", "PAPI_ENDPOINT"]);
|
|
10294
|
-
|
|
10449
|
+
WEDGED_IDLE_TX_SECONDS = 300;
|
|
10450
|
+
WEDGED_ACTIVE_SECONDS = 300;
|
|
10451
|
+
__testing = {
|
|
10452
|
+
redact,
|
|
10453
|
+
findMcpJson,
|
|
10454
|
+
diagnose,
|
|
10455
|
+
formatReport,
|
|
10456
|
+
classifyWedged,
|
|
10457
|
+
formatPoolReport,
|
|
10458
|
+
WEDGED_IDLE_TX_SECONDS,
|
|
10459
|
+
WEDGED_ACTIVE_SECONDS
|
|
10460
|
+
};
|
|
10295
10461
|
}
|
|
10296
10462
|
});
|
|
10297
10463
|
|
|
@@ -10408,9 +10574,253 @@ var init_reset = __esm({
|
|
|
10408
10574
|
}
|
|
10409
10575
|
});
|
|
10410
10576
|
|
|
10577
|
+
// src/cli/audit.ts
|
|
10578
|
+
var audit_exports = {};
|
|
10579
|
+
__export(audit_exports, {
|
|
10580
|
+
__testing: () => __testing2,
|
|
10581
|
+
runAudit: () => runAudit
|
|
10582
|
+
});
|
|
10583
|
+
import { existsSync as existsSync11, readFileSync as readFileSync12, readdirSync as readdirSync7 } from "fs";
|
|
10584
|
+
import { homedir as homedir6 } from "os";
|
|
10585
|
+
import { join as join20 } from "path";
|
|
10586
|
+
function safeListDirs(dir) {
|
|
10587
|
+
try {
|
|
10588
|
+
return readdirSync7(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort((a, b2) => a.localeCompare(b2));
|
|
10589
|
+
} catch {
|
|
10590
|
+
return [];
|
|
10591
|
+
}
|
|
10592
|
+
}
|
|
10593
|
+
function safeListFiles(dir, ext) {
|
|
10594
|
+
try {
|
|
10595
|
+
return readdirSync7(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(ext)).map((e) => e.name).sort((a, b2) => a.localeCompare(b2));
|
|
10596
|
+
} catch {
|
|
10597
|
+
return [];
|
|
10598
|
+
}
|
|
10599
|
+
}
|
|
10600
|
+
function readMcp(projectPath) {
|
|
10601
|
+
const path7 = join20(projectPath, ".mcp.json");
|
|
10602
|
+
if (!existsSync11(path7)) return { servers: [] };
|
|
10603
|
+
try {
|
|
10604
|
+
const parsed = JSON.parse(readFileSync12(path7, "utf-8"));
|
|
10605
|
+
const mcpServers = parsed.mcpServers ?? {};
|
|
10606
|
+
const servers = Object.keys(mcpServers);
|
|
10607
|
+
if (parsed.papi && !servers.includes("papi")) servers.push("papi");
|
|
10608
|
+
servers.sort((a, b2) => a.localeCompare(b2));
|
|
10609
|
+
let papiProjectId;
|
|
10610
|
+
const scanEnv = (entry) => {
|
|
10611
|
+
const env = entry?.env;
|
|
10612
|
+
if (env?.PAPI_PROJECT_ID) papiProjectId = env.PAPI_PROJECT_ID;
|
|
10613
|
+
};
|
|
10614
|
+
for (const entry of Object.values(mcpServers)) scanEnv(entry);
|
|
10615
|
+
scanEnv(parsed.papi);
|
|
10616
|
+
return { servers, papiProjectId };
|
|
10617
|
+
} catch {
|
|
10618
|
+
return { servers: [] };
|
|
10619
|
+
}
|
|
10620
|
+
}
|
|
10621
|
+
async function detectStaleForkNames(projectPath) {
|
|
10622
|
+
try {
|
|
10623
|
+
const { detectStaleForks } = await import("@papi-ai/skills/manifest");
|
|
10624
|
+
return detectStaleForks(projectPath).map((f) => f.name).sort((a, b2) => a.localeCompare(b2));
|
|
10625
|
+
} catch {
|
|
10626
|
+
return [];
|
|
10627
|
+
}
|
|
10628
|
+
}
|
|
10629
|
+
function auditProjectSync(projectPath, name) {
|
|
10630
|
+
const { servers, papiProjectId } = readMcp(projectPath);
|
|
10631
|
+
return {
|
|
10632
|
+
name,
|
|
10633
|
+
path: projectPath,
|
|
10634
|
+
papiProjectId,
|
|
10635
|
+
mcpServers: servers,
|
|
10636
|
+
skills: safeListDirs(join20(projectPath, ".claude", "skills")),
|
|
10637
|
+
agentSkills: safeListDirs(join20(projectPath, ".agents", "skills")),
|
|
10638
|
+
agents: safeListFiles(join20(projectPath, ".claude", "agents"), ".md"),
|
|
10639
|
+
hooks: safeListFiles(join20(projectPath, ".claude", "hooks"), ".sh")
|
|
10640
|
+
};
|
|
10641
|
+
}
|
|
10642
|
+
function discoverProjects() {
|
|
10643
|
+
const out = [];
|
|
10644
|
+
for (const root of PROJECT_ROOTS) {
|
|
10645
|
+
for (const name of safeListDirs(root)) {
|
|
10646
|
+
const path7 = join20(root, name);
|
|
10647
|
+
if (existsSync11(join20(path7, ".mcp.json")) || existsSync11(join20(path7, ".claude"))) {
|
|
10648
|
+
out.push({ name, path: path7 });
|
|
10649
|
+
}
|
|
10650
|
+
}
|
|
10651
|
+
}
|
|
10652
|
+
return out;
|
|
10653
|
+
}
|
|
10654
|
+
function readGlobalSkills() {
|
|
10655
|
+
return safeListDirs(GLOBAL_SKILLS_DIR);
|
|
10656
|
+
}
|
|
10657
|
+
function readGlobalMcpServers() {
|
|
10658
|
+
if (!existsSync11(GLOBAL_CLAUDE_JSON)) return [];
|
|
10659
|
+
try {
|
|
10660
|
+
const parsed = JSON.parse(readFileSync12(GLOBAL_CLAUDE_JSON, "utf-8"));
|
|
10661
|
+
const servers = parsed.mcpServers ?? {};
|
|
10662
|
+
return Object.keys(servers).sort((a, b2) => a.localeCompare(b2));
|
|
10663
|
+
} catch {
|
|
10664
|
+
return [];
|
|
10665
|
+
}
|
|
10666
|
+
}
|
|
10667
|
+
async function checkIdleMcp(projects) {
|
|
10668
|
+
const dbUrl = process.env.DATABASE_URL;
|
|
10669
|
+
const tracked2 = projects.filter((p) => p.papiProjectId);
|
|
10670
|
+
for (const p of projects) p.idleMcp = p.papiProjectId ? "skipped" : "untracked";
|
|
10671
|
+
if (!dbUrl || tracked2.length === 0) return;
|
|
10672
|
+
let factory;
|
|
10673
|
+
try {
|
|
10674
|
+
factory = (await Promise.resolve().then(() => (init_src(), src_exports))).default;
|
|
10675
|
+
} catch {
|
|
10676
|
+
return;
|
|
10677
|
+
}
|
|
10678
|
+
const sql = factory(dbUrl, { max: 1, idle_timeout: 5, connect_timeout: 10 });
|
|
10679
|
+
try {
|
|
10680
|
+
for (const p of tracked2) {
|
|
10681
|
+
try {
|
|
10682
|
+
const rows = await sql`
|
|
10683
|
+
SELECT count(*)::int AS n
|
|
10684
|
+
FROM telemetry_events
|
|
10685
|
+
WHERE project_id = ${p.papiProjectId}
|
|
10686
|
+
AND created_at > now() - make_interval(days => ${IDLE_WINDOW_DAYS})`;
|
|
10687
|
+
p.idleMcp = (rows[0]?.n ?? 0) === 0 ? "idle" : "active";
|
|
10688
|
+
} catch {
|
|
10689
|
+
p.idleMcp = "error";
|
|
10690
|
+
}
|
|
10691
|
+
}
|
|
10692
|
+
} finally {
|
|
10693
|
+
try {
|
|
10694
|
+
await sql.end({ timeout: 5 });
|
|
10695
|
+
} catch {
|
|
10696
|
+
}
|
|
10697
|
+
}
|
|
10698
|
+
}
|
|
10699
|
+
function computeFlags(projects, globalSkills, globalMcp) {
|
|
10700
|
+
const globalSet = new Set(globalSkills);
|
|
10701
|
+
const staleForks = projects.filter((p) => p.staleForks.length > 0).map((p) => ({ project: p.name, skills: p.staleForks }));
|
|
10702
|
+
const redundantWithGlobal = projects.map((p) => ({ project: p.name, skills: p.skills.filter((s) => globalSet.has(s)) })).filter((r) => r.skills.length > 0);
|
|
10703
|
+
const skillToProjects = /* @__PURE__ */ new Map();
|
|
10704
|
+
for (const p of projects) {
|
|
10705
|
+
for (const s of p.skills) {
|
|
10706
|
+
if (globalSet.has(s)) continue;
|
|
10707
|
+
const arr = skillToProjects.get(s) ?? [];
|
|
10708
|
+
arr.push(p.name);
|
|
10709
|
+
skillToProjects.set(s, arr);
|
|
10710
|
+
}
|
|
10711
|
+
}
|
|
10712
|
+
const globalizeCandidates = [...skillToProjects.entries()].filter(([, ps]) => ps.length >= GLOBALIZE_THRESHOLD).map(([skill, ps]) => ({ skill, projects: ps.sort((a, b2) => a.localeCompare(b2)) })).sort((a, b2) => b2.projects.length - a.projects.length);
|
|
10713
|
+
const idleMcp = projects.filter((p) => p.idleMcp === "idle").map((p) => ({ project: p.name }));
|
|
10714
|
+
return { globalSkills, globalMcp, staleForks, redundantWithGlobal, globalizeCandidates, idleMcp };
|
|
10715
|
+
}
|
|
10716
|
+
function formatReport2(projects, flags, idleChecked) {
|
|
10717
|
+
const lines = [];
|
|
10718
|
+
lines.push("PAPI audit \u2014 cross-project harness & config (read-only)");
|
|
10719
|
+
lines.push("");
|
|
10720
|
+
lines.push(`## Projects (${projects.length})`);
|
|
10721
|
+
if (projects.length === 0) {
|
|
10722
|
+
lines.push(" No projects found under ~/Ai-App-Projects or ~/android-projects.");
|
|
10723
|
+
} else {
|
|
10724
|
+
const header = ` ${"Project".padEnd(28)} ${"MCP".padStart(4)} ${"Skills".padStart(6)} ${"AgtSk".padStart(5)} ${"Agents".padStart(6)} ${"Hooks".padStart(5)} ${"Stale".padStart(5)} PAPI`;
|
|
10725
|
+
lines.push(header);
|
|
10726
|
+
lines.push(` ${"-".repeat(header.length - 2)}`);
|
|
10727
|
+
for (const p of projects) {
|
|
10728
|
+
const papi = p.papiProjectId ? idleChecked ? p.idleMcp : "tracked" : "\u2014";
|
|
10729
|
+
lines.push(
|
|
10730
|
+
` ${p.name.slice(0, 28).padEnd(28)} ${String(p.mcpServers.length).padStart(4)} ${String(p.skills.length).padStart(6)} ${String(p.agentSkills.length).padStart(5)} ${String(p.agents.length).padStart(6)} ${String(p.hooks.length).padStart(5)} ${String(p.staleForks.length).padStart(5)} ${papi}`
|
|
10731
|
+
);
|
|
10732
|
+
}
|
|
10733
|
+
}
|
|
10734
|
+
lines.push("");
|
|
10735
|
+
lines.push("## Flags");
|
|
10736
|
+
let any = false;
|
|
10737
|
+
if (flags.globalSkills.length > 0) {
|
|
10738
|
+
any = true;
|
|
10739
|
+
lines.push(` \u2022 Global skills (${flags.globalSkills.length}) load into EVERY project's context window \u2014 token-hygiene review candidates:`);
|
|
10740
|
+
lines.push(` ${flags.globalSkills.join(", ")}`);
|
|
10741
|
+
}
|
|
10742
|
+
if (flags.globalMcp.length > 0) {
|
|
10743
|
+
any = true;
|
|
10744
|
+
lines.push(` \u2022 Global MCP servers (${flags.globalMcp.length}) enabled for every project: ${flags.globalMcp.join(", ")}`);
|
|
10745
|
+
}
|
|
10746
|
+
for (const r of flags.redundantWithGlobal) {
|
|
10747
|
+
any = true;
|
|
10748
|
+
lines.push(` \u2022 ${r.project}: skill(s) exist both project-locally AND globally (redundant unless an intentional fork): ${r.skills.join(", ")}`);
|
|
10749
|
+
}
|
|
10750
|
+
for (const s of flags.staleForks) {
|
|
10751
|
+
any = true;
|
|
10752
|
+
lines.push(` \u2022 ${s.project}: stale skill fork(s) drifted from @papi-ai/skills: ${s.skills.join(", ")}`);
|
|
10753
|
+
}
|
|
10754
|
+
for (const c of flags.globalizeCandidates) {
|
|
10755
|
+
any = true;
|
|
10756
|
+
lines.push(` \u2022 "${c.skill}" is project-local in ${c.projects.length} projects (${c.projects.join(", ")}) \u2014 candidate to promote to ~/.claude/skills.`);
|
|
10757
|
+
}
|
|
10758
|
+
if (idleChecked) {
|
|
10759
|
+
for (const i of flags.idleMcp) {
|
|
10760
|
+
any = true;
|
|
10761
|
+
lines.push(` \u2022 ${i.project}: PAPI MCP idle \u2014 0 tool calls in ${IDLE_WINDOW_DAYS}d. Candidate to remove from active config.`);
|
|
10762
|
+
}
|
|
10763
|
+
} else {
|
|
10764
|
+
lines.push(` \u2022 idle-MCP check skipped \u2014 run with \`--idle-mcp\` (requires DATABASE_URL) to flag projects with 0 telemetry in ${IDLE_WINDOW_DAYS}d.`);
|
|
10765
|
+
}
|
|
10766
|
+
if (!any) lines.push(" \u2713 No hygiene flags raised.");
|
|
10767
|
+
lines.push("");
|
|
10768
|
+
lines.push("## Suggestions (proposals \u2014 nothing is applied automatically)");
|
|
10769
|
+
const suggestions = [];
|
|
10770
|
+
if (flags.globalSkills.length > 8) {
|
|
10771
|
+
suggestions.push(`Move rarely-used global skills out of ~/.claude/skills into a per-project .claude/skills/ (or .claude/skills.local/) so they only load where used.`);
|
|
10772
|
+
}
|
|
10773
|
+
if (flags.redundantWithGlobal.length > 0) {
|
|
10774
|
+
suggestions.push(`For redundant project-local copies of global skills, delete the local copy unless it is an intentional fork (keep forks under .claude/skills.local/ so they are not flagged stale).`);
|
|
10775
|
+
}
|
|
10776
|
+
if (flags.staleForks.length > 0) {
|
|
10777
|
+
suggestions.push(`Re-sync stale skill forks from @papi-ai/skills, or move deliberate overrides under .claude/skills.local/.`);
|
|
10778
|
+
}
|
|
10779
|
+
if (idleChecked && flags.idleMcp.length > 0) {
|
|
10780
|
+
suggestions.push(`Remove the PAPI MCP config from dormant projects (no tool calls in ${IDLE_WINDOW_DAYS}d) to trim every-session connector load.`);
|
|
10781
|
+
}
|
|
10782
|
+
if (suggestions.length === 0) {
|
|
10783
|
+
lines.push(" \u2713 Nothing to suggest.");
|
|
10784
|
+
} else {
|
|
10785
|
+
suggestions.forEach((s, i) => lines.push(` ${i + 1}. ${s}`));
|
|
10786
|
+
}
|
|
10787
|
+
lines.push("");
|
|
10788
|
+
lines.push("Approve any change in Claude Code \u2014 this command never edits files or the database.");
|
|
10789
|
+
return lines.join("\n");
|
|
10790
|
+
}
|
|
10791
|
+
async function runAudit(args = []) {
|
|
10792
|
+
const idleChecked = args.includes("--idle-mcp");
|
|
10793
|
+
const discovered = discoverProjects();
|
|
10794
|
+
const projects = [];
|
|
10795
|
+
for (const { name, path: path7 } of discovered) {
|
|
10796
|
+
const base = auditProjectSync(path7, name);
|
|
10797
|
+
projects.push({ ...base, staleForks: await detectStaleForkNames(path7), idleMcp: "skipped" });
|
|
10798
|
+
}
|
|
10799
|
+
if (idleChecked) {
|
|
10800
|
+
await checkIdleMcp(projects);
|
|
10801
|
+
}
|
|
10802
|
+
const globalSkills = readGlobalSkills();
|
|
10803
|
+
const globalMcp = readGlobalMcpServers();
|
|
10804
|
+
const flags = computeFlags(projects, globalSkills, globalMcp);
|
|
10805
|
+
process.stdout.write(formatReport2(projects, flags, idleChecked) + "\n");
|
|
10806
|
+
return 0;
|
|
10807
|
+
}
|
|
10808
|
+
var PROJECT_ROOTS, GLOBAL_SKILLS_DIR, GLOBAL_CLAUDE_JSON, IDLE_WINDOW_DAYS, GLOBALIZE_THRESHOLD, __testing2;
|
|
10809
|
+
var init_audit = __esm({
|
|
10810
|
+
"src/cli/audit.ts"() {
|
|
10811
|
+
"use strict";
|
|
10812
|
+
PROJECT_ROOTS = [join20(homedir6(), "Ai-App-Projects"), join20(homedir6(), "android-projects")];
|
|
10813
|
+
GLOBAL_SKILLS_DIR = join20(homedir6(), ".claude", "skills");
|
|
10814
|
+
GLOBAL_CLAUDE_JSON = join20(homedir6(), ".claude.json");
|
|
10815
|
+
IDLE_WINDOW_DAYS = 30;
|
|
10816
|
+
GLOBALIZE_THRESHOLD = 3;
|
|
10817
|
+
__testing2 = { readMcp, computeFlags, formatReport: formatReport2, discoverProjects, auditProjectSync };
|
|
10818
|
+
}
|
|
10819
|
+
});
|
|
10820
|
+
|
|
10411
10821
|
// src/index.ts
|
|
10412
|
-
import { readFileSync as
|
|
10413
|
-
import { dirname as dirname5, join as
|
|
10822
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
10823
|
+
import { dirname as dirname5, join as join21 } from "path";
|
|
10414
10824
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
10415
10825
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10416
10826
|
import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -11365,7 +11775,8 @@ function estimateCost(model, inputTokens, outputTokens) {
|
|
|
11365
11775
|
if (!rates) return 0;
|
|
11366
11776
|
return inputTokens * rates.input + outputTokens * rates.output;
|
|
11367
11777
|
}
|
|
11368
|
-
|
|
11778
|
+
var MAX_CLIENT_NAME_LENGTH = 100;
|
|
11779
|
+
function buildMetric(tool, durationMs, usage, cycleNumber, contextBytes, contextUtilisation, clientName) {
|
|
11369
11780
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
11370
11781
|
const metric = { timestamp, tool, durationMs };
|
|
11371
11782
|
if (usage) {
|
|
@@ -11383,6 +11794,9 @@ function buildMetric(tool, durationMs, usage, cycleNumber, contextBytes, context
|
|
|
11383
11794
|
if (contextUtilisation !== void 0) {
|
|
11384
11795
|
metric.contextUtilisation = contextUtilisation;
|
|
11385
11796
|
}
|
|
11797
|
+
if (clientName !== void 0 && clientName.length > 0) {
|
|
11798
|
+
metric.clientName = clientName.slice(0, MAX_CLIENT_NAME_LENGTH);
|
|
11799
|
+
}
|
|
11386
11800
|
return metric;
|
|
11387
11801
|
}
|
|
11388
11802
|
function measureContextUtilisation(inputContext, llmOutput) {
|
|
@@ -11432,6 +11846,7 @@ If a candidate AD body could be invalidated by running a SQL query, refreshing a
|
|
|
11432
11846
|
**Negative example (reject):** "External user feedback is now flowing. Stonebridge Systems is actively building." \u2014 this is a fact about the current state of the world. Capture as dogfood/signal observation; do not mint.
|
|
11433
11847
|
|
|
11434
11848
|
This rule applies to: new ADs proposed during planning (Step 9), strategy review AD updates (section 5), and strategy_change AD updates. If you find an existing AD that violates this rule during housekeeping, propose deleting it (action: "delete") with a one-line rationale.`;
|
|
11849
|
+
var OUTPUT_QUALITY_RUBRIC = `Quality bar \u2014 before emitting, self-score the artifact 1-10 on five dimensions: (1) **Clarity** \u2014 could a third LLM act on it with no extra context? (2) **Scope tightness** \u2014 one focused unit of work, not three bundled together. (3) **Specificity** \u2014 it names concrete files/paths/tasks/ADs, not vague references like "the auth module". (4) **Dependency surfacing** \u2014 prerequisite or upstream items are called out explicitly. (5) **Success-criteria concreteness** \u2014 "done" is testable and observable, not "looks good". Sum to /50. If any single dimension scores \u22644, or the total is <35, revise the artifact and re-score before emitting \u2014 do not ship a below-threshold artifact. This is a self-check gate, not an output field: do NOT add the scores to the artifact or the structured JSON.`;
|
|
11435
11850
|
var PLAN_SYSTEM = `You are the PAPI Cycle Planner \u2014 an autonomous planning engine for software projects.
|
|
11436
11851
|
You receive project context and produce a planning cycle output with a BUILD HANDOFF.
|
|
11437
11852
|
|
|
@@ -11601,152 +12016,6 @@ Before generating cycle tasks, answer: **is this a service or a platform?**
|
|
|
11601
12016
|
If unanswered, default to Platform \u2014 which is often wrong for early-stage projects. If the answer is clear from the brief, mint it as AD-1 and shape tasks accordingly.
|
|
11602
12017
|
|
|
11603
12018
|
**CRITICAL: Bootstrap MUST populate newTasks, productBrief, and activeDecisions (if any ADs were created). These are the only way data gets written to files.**`;
|
|
11604
|
-
var PLAN_FULL_INSTRUCTIONS = `## FULL MODE
|
|
11605
|
-
|
|
11606
|
-
Standard planning cycle with full board review.
|
|
11607
|
-
|
|
11608
|
-
### Steps:
|
|
11609
|
-
1. **Cycle Health Check** \u2014 Flag issues: >7 day gaps, unprocessed discovered issues, AD conflicts, stale In Progress tasks (3+ cycles).
|
|
11610
|
-
**\u26A0\uFE0F CARRY-FORWARD STALENESS (already-built):** Check the latest carry-forward text for items containing "stale", "already exists", "already implemented", or "already built". For each such item that references a specific task ID, check whether the task is still in Backlog. If a carry-forward says a task's deliverables already exist but the task is still Backlog, emit a \`boardCorrections\` entry setting it to Done with \`closureReason: "Auto-closed \u2014 carry-forward indicates deliverables already exist"\`. Log in the cycle log: "Auto-closed task-XXX \u2014 carry-forward confirmed deliverables exist." This prevents scheduling already-shipped tasks.
|
|
11611
|
-
**\u26A0\uFE0F CARRY-FORWARD FORCED RESOLUTION:** If a "Carry-Forward Staleness" section is provided in the context below, it lists task IDs that have been deferred for 3+ consecutive cycles. For each listed task, you MUST take one of these actions \u2014 deferring again without justification is not acceptable:
|
|
11612
|
-
- **Escalate:** Emit a \`boardCorrections\` entry upgrading the task to P1 High (if currently P2+) and include a "Carry-Forward Escalation" paragraph in the cycle log: list each escalated task by ID with a 1-sentence rationale.
|
|
11613
|
-
- **Cancel:** Emit a \`boardCorrections\` entry setting status to "Cancelled" with a \`closureReason\` explaining why the task is no longer worth pursuing.
|
|
11614
|
-
- **Schedule:** Include the task in this cycle's BUILD HANDOFFs. If scheduled, no further action needed.
|
|
11615
|
-
If you defer a stale task again, you MUST provide an explicit justification in the cycle log \u2014 e.g. "task-XXX deferred again: blocked on task-YYY which must ship first."
|
|
11616
|
-
|
|
11617
|
-
2. **Inbox Triage** \u2014 Find unreviewed tasks (reviewed = false). For each: clean title, fill all fields, check for duplicates, verify alignment with Active Decisions. You MUST set priority on unreviewed tasks during triage using these criteria:
|
|
11618
|
-
- **P0 Critical** \u2014 Broken, blocking, or data-loss risk. Fix now.
|
|
11619
|
-
- **P1 High** \u2014 Strategically aligned: directly advances the current horizon/phase goals or Active Decisions.
|
|
11620
|
-
- **P2 Medium** \u2014 Valuable but not strategically urgent: quality improvements, efficiency, polish, infrastructure.
|
|
11621
|
-
- **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
|
|
11622
|
-
**\u26A0\uFE0F PRIORITY RECALIBRATION \u2014 do NOT rubber-stamp the submitted priority.** The priority set at idea submission reflects the submitter's view at that time, which may be outdated by the time the planner runs. For EVERY unreviewed task, evaluate its priority FROM SCRATCH against: (a) current horizon/stage/phase goals, (b) recent Active Decision changes, (c) recently shipped functionality that makes this task more or less urgent. If your assessed priority differs from the submitted one, set the new priority in \`boardCorrections\` and include the change in a **Priority Recalibration** paragraph in your cycle log (Step 8): list each changed task by ID, old priority \u2192 new priority, and a 1-sentence rationale. This paragraph is how the user sees what the planner recalibrated and why. If no priorities changed during triage, omit the paragraph.
|
|
11623
|
-
Also set complexity using the full range \u2014 **XS, Small, Medium, Large, XL** \u2014 based on actual scope, not conservatively. XS = single-line or config change. Small = one file, < 50 lines. Medium = 2-5 files. Large = cross-module, multiple components. XL = architectural, multi-day.
|
|
11624
|
-
**Module classification for cross-cutting tasks:** When a task title contains "audit"/"unfiltered"/"scoping"/"leak" plus a database-entity name (e.g. "audit ... cycle_learnings reads", "unfiltered cycle_tasks queries"), classify the module by the actual code surface that reads/writes the entity \u2014 not by the tool names mentioned in the title. The reasoning surface (e.g. "health" or "strategy_review") is often unrelated to the data-access surface. Resolve this by treating the entity name as the routing signal: tasks touching dashboard read/write paths belong to the Dashboard module even if the title mentions an MCP tool. Misclassification routes the task to the wrong shared cycle branch and surfaces the wrong MODULE INSTRUCTIONS to the builder.
|
|
11625
|
-
If a task is clearly obsolete, duplicated, or rejected, set its status to "Cancelled" with a \`closureReason\` explaining why.
|
|
11626
|
-
**\u2192 PERSIST:** For each task you set reviewed: true, corrected fields on, or marked "Cancelled", include it in \`boardCorrections\` in Part 2.
|
|
11627
|
-
|
|
11628
|
-
3. **Board Integrity** \u2014 All tasks have complete fields? Priority still accurate? Duplicates? Stale In Progress tasks?
|
|
11629
|
-
**\u2192 PERSIST:** Include any field corrections (status updates, field fixes) in \`boardCorrections\` in Part 2.
|
|
11630
|
-
**\u26A0\uFE0F PRIORITY LOCK RULE:** Do NOT change the priority of any task that has \`reviewed: true\`. Reviewed tasks have had their priority confirmed by a human. If you believe a reviewed task's priority should change, note your recommendation in the cycle log but do NOT include a priority change in \`boardCorrections\`. You may only set priority on unreviewed tasks (during triage) or on newly created tasks (\`newTasks\` array). Priority values: P0 Critical, P1 High, P2 Medium, P3 Low.
|
|
11631
|
-
**Priority Drift Check:** For reviewed Backlog tasks, check whether their priority still reflects strategic reality. A task submitted as P2 six cycles ago may now be P1 (strategic context shifted) or P3 (redundant, superseded, or de-prioritised by an AD). For each task where drift is detected, check three signals: (1) Does it still align with the current horizon/stage/phase? (2) Has a recent AD changed the strategic importance of this area? (3) Has a recent build shipped functionality that makes this task redundant or more urgent? If drift is found on 1+ tasks, include a **Priority Drift Suggestions** paragraph in your cycle log: list each drifted task by ID, current priority, suggested priority, and a 1-sentence rationale. Do NOT include priority changes in \`boardCorrections\` \u2014 these are suggestions requiring human confirmation, not auto-corrections. Omit this paragraph entirely if no drift is detected.
|
|
11632
|
-
|
|
11633
|
-
4. **Security Posture Check** \u2014 Review recently completed tasks and current board state for security concerns. Only flag genuine issues \u2014 do not add boilerplate security notes every cycle. Look for:
|
|
11634
|
-
- Data exposure risks introduced by recent builds (PII in logs, secrets in storage/config)
|
|
11635
|
-
- Unprotected endpoints or missing auth/access control in new features
|
|
11636
|
-
- Undocumented secrets or environment variables added without documentation
|
|
11637
|
-
- New dependencies with known vulnerabilities or excessive permissions
|
|
11638
|
-
**\u2192 PERSIST:** If concerns exist, include them in \`cycleLogNotes\` with a \`[SECURITY]\` tag prefix (e.g. "[SECURITY] New /admin endpoint in task-042 has no auth middleware"). If no concerns, omit \u2014 do not write "[SECURITY] No issues found".
|
|
11639
|
-
|
|
11640
|
-
5. **Discovery Gaps** \u2014 If a Discovery Canvas section is provided in the context below, check which sections are populated vs empty. In cycles 1-10, or whenever canvas sections have been empty for 5+ cycles, include a "Discovery Gaps" paragraph in your cycle log suggesting what context would improve planning. Examples: "Your project context would benefit from MVP boundary definition" or "Consider documenting key user journeys." Keep suggestions conversational \u2014 do NOT create tasks for discovery gaps. If all canvas sections are populated, or no Discovery Canvas is provided, skip this step entirely.
|
|
11641
|
-
|
|
11642
|
-
6. **Maturity Gate** \u2014 Before scheduling any task, check whether the project is ready for it:
|
|
11643
|
-
- **Cycle number as signal:** A Cycle 3 project should not be scheduling OAuth, billing, or analytics tasks. Early cycles focus on core functionality and proving the concept works.
|
|
11644
|
-
- **Phase prerequisites:** If the board has phases, tasks from later phases should only be scheduled when earlier phases have completed tasks (check Done count per phase). A task in "Phase 4: Monetisation" is premature if Phase 2 tasks are still in Backlog.
|
|
11645
|
-
- **Dependency chain:** If a task's \`dependsOn\` references incomplete tasks, it cannot be scheduled regardless of priority.
|
|
11646
|
-
- **Task maturity:** Tasks with \`maturity: "raw"\` are unscoped ideas from the idea tool. The planner IS the scoping mechanism \u2014 scope them as part of planning. For raw tasks selected for a cycle: (a) derive clear scope, acceptance criteria, and effort from the title, notes, and project context, (b) upgrade them to \`maturity: "investigated"\` via a \`boardCorrections\` entry, and (c) generate a BUILD HANDOFF as normal. For research-type raw tasks, scope the handoff as an investigation task \u2014 the deliverable is findings + follow-up backlog tasks, not code. Only leave a raw task unscheduled if you genuinely cannot derive scope from the available context \u2014 note why in the cycle log. Tasks with \`maturity: "ready"\` or no maturity field are considered cycle-ready. Tasks with \`maturity: "investigated"\` have been scoped but may still need refinement \u2014 schedule them if priority warrants it.
|
|
11647
|
-
- **What to do with premature tasks:** Leave them in Backlog. Do NOT generate BUILD HANDOFFs for them. If a high-priority task fails the maturity gate due to phase prerequisites or dependencies, note it in the cycle log: "task-XXX deferred \u2014 Phase N prerequisites not met". Raw tasks are NOT premature \u2014 they just need scoping (see Task maturity above).
|
|
11648
|
-
|
|
11649
|
-
7. **Recommendation** \u2014 Select tasks for this cycle:
|
|
11650
|
-
**Pre-assigned tasks:** If a "Pre-Assigned Tasks" section is provided in the context below, those tasks are ALREADY committed to this cycle by the user. Include them automatically \u2014 do NOT re-evaluate whether they belong. Generate BUILD HANDOFFs for each. Count their effort toward the cycle budget. Then fill remaining slots from the backlog using the priority rules and cycle sizing rules below.
|
|
11651
|
-
**If USER DIRECTION is provided above:** Follow the user's stated focus. Pick the highest-impact task that aligns with their direction. The user knows what they need. Only deviate if a genuine P0 Critical fix exists (broken builds, data loss).
|
|
11652
|
-
**Otherwise, select by priority level then impact:**
|
|
11653
|
-
- **P0 Critical** \u2014 Broken, blocking, or data-loss risk. Always first.
|
|
11654
|
-
- **P1 High** \u2014 Strategically aligned: directly advances the current horizon, phase, or Active Decision goals.
|
|
11655
|
-
- **P2 Medium** \u2014 Valuable but not strategically urgent: quality improvements, efficiency, polish, infra.
|
|
11656
|
-
- **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
|
|
11657
|
-
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.
|
|
11658
|
-
**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.
|
|
11659
|
-
**Cycle sizing:** Size the cycle based on what the selected tasks actually require \u2014 not a fixed budget. Select the highest-priority unblocked tasks, estimate each one's effort from its scope, and let the total emerge. The historical average effort from Methodology Trends is a reference point for calibration, not a target or floor. A healthy cycle has 6-10 tasks. Cycles with fewer than 5 tasks require explicit justification in the cycle log \u2014 explain why more tasks could not be included. When the backlog has 10+ tasks, the cycle SHOULD have 6+ tasks \u2014 undersized cycles waste planning overhead relative to the available work. If fewer than 5 tasks qualify after filtering (blocked, deferred, raw), check Deferred tasks \u2014 some may be ready to un-defer via a \`boardCorrections\` entry. A 1-task cycle is almost never correct. Prefer grouping tasks by module or similarity \u2014 reduces context switching and enables shared branches during the build phase.
|
|
11660
|
-
**Theme-driven sizing:** Single-theme cycles (all tasks in the same module or epic) can absorb 25-30 effort points because builders maintain context across tasks. Mixed-theme cycles should stay at 15-20 effort points to limit context switching. Use the theme to determine the budget, not a fixed number.
|
|
11661
|
-
**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\`.
|
|
11662
|
-
**Epic-aware batching:** Epic is the primary grouping signal for theme coherence. When multiple candidate tasks share the same epic (e.g. "Onboarding Redesign", "Dashboard Polish"), prefer co-scheduling them \u2014 they solve connected problems and benefit from shared context during the build. Steps: (1) After filtering by priority, group eligible tasks by epic. (2) If an epic has 3+ eligible tasks, prefer scheduling 2-4 of them together over cherry-picking across epics. (3) Report the epic distribution in the cycle log (e.g. "4 tasks from Onboarding epic, 1 from Platform"). Priority still overrides: a P0 fix from a different epic always takes precedence.
|
|
11663
|
-
**Opportunity clustering:** If backlog tasks have an \`opportunity\` field populated, group them by opportunity before selecting. Tasks sharing the same opportunity solve the same user problem \u2014 co-scheduling them produces more coherent cycles. Report opportunity clusters in the cycle log when present (e.g. "3 tasks clustered under 'planner accuracy' opportunity").
|
|
11664
|
-
|
|
11665
|
-
8. **Cycle Log** \u2014 Write 5-10 line entry: what was triaged, what was recommended and why, observations, AD updates. Include a **Priority Recalibration** paragraph if any unreviewed task priorities were changed during triage (Step 2) \u2014 list each by ID with old \u2192 new priority and rationale. Include a **Priority Drift Suggestions** paragraph if reviewed task drift was detected (Step 3).
|
|
11666
|
-
**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.
|
|
11667
|
-
|
|
11668
|
-
9. **Active Decisions** \u2014 If any AD needs updating: Type A (confidence change), Type B (modification), or Type C (reversal/supersede).
|
|
11669
|
-
**AD Quality Bar:** ADs are for product and architecture choices that constrain future work \u2014 technology selections, data model designs, UX principles, strategic positioning. They are NOT for: process preferences (commit style, PR size), configuration choices (linter rules, tab width), or temporary workarounds. If a decision doesn't affect what gets built or how it's architected, it's not an AD. Apply this bar when proposing new ADs and when triaging existing ones.
|
|
11670
|
-
**Reversal Trigger (required for new ADs):** Every new AD body must include a \`### Reversal Trigger\` section. Wording: "Under what conditions would this stance reverse? Specify: the signal (concrete event or metric threshold); the action (mint new AD, modify, supersede, or abandon); and why writing this now reduces sunk-cost drift later." Example: "Signal: 3+ external users report >10-min setup time. Action: supersede with a streamlined onboarding AD. Why: measuring it now means we act before it becomes a retention problem." Existing ADs without this section remain valid \u2014 do not retroactively add triggers. Only include when creating a new AD or substantially modifying an existing one.
|
|
11671
|
-
|
|
11672
|
-
${AD_REJECTION_RULES}
|
|
11673
|
-
|
|
11674
|
-
**\u2192 PERSIST:** EVERY AD you created, updated, or confirmed with changes MUST appear in \`activeDecisions\` array in Part 2. Include the full replacement body with ### heading.
|
|
11675
|
-
|
|
11676
|
-
### Operational Quality Rules
|
|
11677
|
-
- **Idea similarity pause:** When the idea tool finds similar tasks during planning, stop and explain the overlap \u2014 do not silently ignore the similarity warning. Duplicates bloat the board and waste build slots.
|
|
11678
|
-
- **Backlog as steering wheel:** Task priority and notes in the backlog are the user's primary control mechanism over what gets planned. Respect the priority rankings and read task notes carefully \u2014 they contain user intent that shapes scope and scheduling.
|
|
11679
|
-
- **Planning quality is the bar:** Strategy review depth and plan quality set the standard for the product. Do not cut corners on analysis depth, triage thoroughness, or handoff specificity \u2014 these are what users experience as PAPI's value.
|
|
11680
|
-
|
|
11681
|
-
10. **BUILD HANDOFFs** \u2014 Generate a full BUILD HANDOFF block for every task selected for this cycle. Include each handoff in the \`cycleHandoffs\` array in the structured output. The handoffs are written to each task on the board for durability.
|
|
11682
|
-
**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.
|
|
11683
|
-
**Scope pre-check:** Before writing the SCOPE section of each handoff, cross-reference the task against the "Recently Shipped Capabilities" section in the context below (if present). For each candidate task: (1) check if the task's title or scope overlaps with any recently shipped task, (2) check if the FILES LIKELY TOUCHED overlap with files already modified in recent builds, (3) check the architecture notes from recent builds for patterns that already cover this task's scope. If >80% of a task's scope appears in recently shipped capabilities, recommend cancellation via \`boardCorrections\` or reduce the handoff scope to only the missing pieces \u2014 explicitly note what already exists. C126 task-728 was over-scoped because the planner assumed Blocked status needed creating from scratch \u2014 it already existed in types, DB, orient, and build_list. Over-scoped handoffs waste builder time on verification and cause estimation mismatches.
|
|
11684
|
-
**Reality check (task-1755):** Before scoping a handoff for a candidate task, evaluate whether the implicit assumptions a builder would make actually hold. For each task ask: (a) **Replacement assets** \u2014 does the task assume a copy/image/visual/dataset/fixture that doesn't yet exist? (e.g. "swap the hero illustration" assumes a new illustration is ready); (b) **Required user decisions** \u2014 does it assume an answer to an unresolved question (positioning, copy direction, model choice, pricing) that the user has not yet made?; (c) **Infrastructure dependencies** \u2014 does it assume a service/credential/migration/feature flag that isn't yet provisioned? If ANY assumption is unmet: do NOT generate a handoff for that task. Instead include the task in a **Pre-conditions Missing** paragraph in \`cycleLogNotes\` listing the task ID, the missing dependency, and the suggested unblock (e.g. "task-1234: needs new hero illustration \u2014 file separate asset-creation task or surface as a decision"). Drop it from the cycle entirely; the user can re-add it once the dependency is satisfied. ellies-birthday C5 hit two mid-cycle defers in one cycle from this exact gap (handoffs assumed assets that didn't exist) \u2014 reality check catches that before scope is written, not after work starts.
|
|
11685
|
-
**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.
|
|
11686
|
-
**Maturity gate applies here:** Do NOT generate BUILD HANDOFFs for tasks that failed the maturity gate in step 6 (phase prerequisites not met, dependency chain incomplete). Raw tasks that the planner has scoped and upgraded to "investigated" in step 6 ARE eligible for handoffs.
|
|
11687
|
-
**Intra-cycle dependency detection:** After selecting cycle tasks, check every pair for build-order dependencies. Two tasks A and B have an intra-cycle dependency when A must be built before B because B consumes an artifact A creates \u2014 e.g. A adds a new adapter method that B calls, A creates a DB migration B depends on, A introduces a new shared type B imports, A refactors a utility B modifies. Signals: same module + adjacent scope (one is "add X", another is "use X"), or notes explicitly reference the other task. For each dependency detected:
|
|
11688
|
-
- Populate the DEPENDS ON section in the dependent task's BUILD HANDOFF with the upstream task ID(s).
|
|
11689
|
-
- Add a \`boardCorrections\` entry for the dependent task with \`updates.dependsOn\` set to the comma-separated upstream IDs \u2014 this persists the dependency so the builder's runtime can reuse the upstream branch.
|
|
11690
|
-
- Keep the SCOPE sections independent (each task still has its own deliverable) but note the ordering in "Why now" \u2014 e.g. "depends on task-123 completing the adapter method".
|
|
11691
|
-
Do NOT invent dependencies where tasks merely share a module \u2014 only real build-order coupling counts. Linear chains only \u2014 do not attempt to resolve multi-level graphs. When in doubt, omit the dependency and let the builder discover it.
|
|
11692
|
-
**Dependency Chain section (Part 1 markdown):** When intra-cycle dependencies are detected, include a visible **## Dependency Chain** section in Part 1 markdown immediately before the first BUILD HANDOFF block. List each dependency as an arrow chain with a brief reason: \`task-A \u2192 task-B (B calls the adapter method A creates)\`. Then show the full recommended build sequence for all cycle tasks, including standalone tasks: e.g. \`Build order: task-A \u2192 task-B; task-C standalone; task-D standalone\`. Flag circular dependencies with \u26A0\uFE0F and a note. Omit this section entirely when no intra-cycle dependencies exist \u2014 do not include an empty section.
|
|
11693
|
-
**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".
|
|
11694
|
-
**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).
|
|
11695
|
-
**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.
|
|
11696
|
-
**Full notes lookup:** Notes in the Board section are truncated to 300 chars for concise task selection. When generating a BUILD HANDOFF for a task, check the "Full Notes for Candidate Tasks" section (if present in context) for that task's complete untruncated notes before writing SCOPE, SCOPE BOUNDARY, and PRE-MORTEM. Submitter context, constraints, and reasoning often live past the 300-char cutoff and must not be dropped from the handoff.
|
|
11697
|
-
**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.
|
|
11698
|
-
**Pre-mortem:** For projects with 10+ cycles, include a PRE-MORTEM section in every BUILD HANDOFF with 1-3 bullet points: (a) most likely technical blocker based on module history, (b) integration risk with adjacent systems, (c) scope creep signal \u2014 what the builder might be tempted to expand beyond scope. Draw from \`dead_ends\` and \`surprises\` in recent build reports for the same module. Omit this section entirely for projects with fewer than 10 cycles.
|
|
11699
|
-
**Build order in cycle log:** If any intra-cycle dependencies were detected in this cycle, include a "Build Order" paragraph in \`cycleLogNotes\` showing the recommended build sequence as arrow chains (e.g. "Build order: task-123 \u2192 task-124; task-130 standalone"). Skip this paragraph when no dependencies exist.
|
|
11700
|
-
**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:
|
|
11701
|
-
|
|
11702
|
-
RESEARCH OUTPUT
|
|
11703
|
-
Deliverable: docs/research/[topic]-findings.md (draft path)
|
|
11704
|
-
Review status: pending owner approval
|
|
11705
|
-
Follow-up tasks: DO NOT submit to backlog until owner confirms findings are actionable
|
|
11706
|
-
|
|
11707
|
-
Also add to ACCEPTANCE CRITERIA: "[ ] Findings doc drafted and saved to docs/research/ before submitting any follow-up ideas"
|
|
11708
|
-
|
|
11709
|
-
**Bug task detection:** When a task's task type is "bug" or the title starts with "Bug:" or "Fix:", apply these rules:
|
|
11710
|
-
- **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.
|
|
11711
|
-
- 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."
|
|
11712
|
-
- Add to ACCEPTANCE CRITERIA: "[ ] Fix is targeted \u2014 no unrelated code changed"
|
|
11713
|
-
|
|
11714
|
-
**Idea task detection:** When a task's task type is "idea", add a scope clarification note to the BUILD HANDOFF:
|
|
11715
|
-
- 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."
|
|
11716
|
-
|
|
11717
|
-
**UI/visual task detection:** Apply these additions ONLY to tasks whose PRIMARY scope is frontend visual work \u2014 the task's main deliverable must be a UI change, new component, visual design, or page. Do NOT apply to backend tasks, DB migrations, or prompt/config changes that merely mention a dashboard or page in passing. Signal: the task would fail if no .tsx/.css files were changed. If uncertain, skip the UI additions.
|
|
11718
|
-
When a task IS a UI task (primary scope is visual/frontend):
|
|
11719
|
-
- Add to SCOPE: "Read \`.impeccable.md\` for component patterns, anti-patterns, and dev-loop design rules. Read \`docs/branding/brand-book.html\` for brand identity (positioning, voice, palette as final canon). Use the \`frontend-design\` skill for implementation."
|
|
11720
|
-
- 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."
|
|
11721
|
-
- 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."
|
|
11722
|
-
- If the task involves image selection, add to SCOPE: "Include brand/theme direction constraints for image selection \u2014 pull from \`docs/branding/brand-book.html\` for canonical brand identity."
|
|
11723
|
-
The planner's job is scoping, not design direction. Design decisions happen at build time via \`.impeccable.md\` (dev patterns) + \`docs/branding/brand-book.html\` (brand identity) and the frontend-design skill \u2014 don't try to write design specs in the handoff.
|
|
11724
|
-
|
|
11725
|
-
11. **New Tasks (max 3 per cycle)** \u2014 Actively mine the Recent Build Reports for task candidates. For each report, check:
|
|
11726
|
-
- **Discovered Issues:** If a build report lists a discovered issue and no existing board task covers it, propose a new task.
|
|
11727
|
-
- **Surprises:** If a surprise reveals a gap (e.g. "schema assumed but not verified"), propose a task to close it.
|
|
11728
|
-
- **Architecture Notes:** If a pattern was established that needs follow-up (e.g. "shared service layer created, MCP migration needed"), propose the follow-up.
|
|
11729
|
-
- **Strategy gaps:** If an Active Decision has no board tasks supporting it, propose one.
|
|
11730
|
-
- **Dogfood observations:** If unactioned dogfood entries are listed in context (with IDs), check if any map to existing tasks. If not, propose a new task. **CRITICAL: Include \`dogfood:<uuid>\` in the new task's \`notes\` field** (e.g. \`"notes": "Addresses recurring friction. dogfood:abc12345-..."\`). This links the task to the observation so the pipeline can track what was actioned. Without this annotation, the observation stays unactioned forever.
|
|
11731
|
-
Create new tasks via the \`newTasks\` array in Part 2. Use \`new-N\` IDs in \`cycleHandoffs\` to reference them. **Limit: 3 new tasks per cycle** to prevent backlog bloat.
|
|
11732
|
-
**\u26A0\uFE0F DUPLICATE CHECK:** Before adding a task to \`newTasks\`, scan the Cycle Board above for any existing task with the same or very similar title/scope. If a matching task already exists (even with slightly different wording), do NOT create a duplicate \u2014 reference the existing task ID instead. The board already contains all active tasks; re-creating them wastes IDs and bloats the board.
|
|
11733
|
-
**\u26A0\uFE0F ALREADY-BUILT CHECK:** Before creating a task, check the recent build reports and cycle log for evidence that this capability was already shipped. If a recent build report shows this feature was completed (even under a different task name), do NOT create a new task for it. This is especially important for UI features, data models, and integrations that may already exist.
|
|
11734
|
-
|
|
11735
|
-
12. **Product Brief** \u2014 Check whether the product brief still reflects reality. Update the brief when ANY of these apply:
|
|
11736
|
-
- A new AD was created or an existing AD was superseded that changes product scope, target user, or positioning
|
|
11737
|
-
- The North Star changed or was validated in a way that the brief doesn't reflect
|
|
11738
|
-
- A phase completed that shifts what the product IS (not just what was built)
|
|
11739
|
-
- The brief describes capabilities, architecture, or direction that are no longer accurate
|
|
11740
|
-
- **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.
|
|
11741
|
-
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.
|
|
11742
|
-
|
|
11743
|
-
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:
|
|
11744
|
-
- **Specific** \u2014 reference the upcoming phase by name and the architectural fork or tradeoff involved
|
|
11745
|
-
- **Actionable** \u2014 frame as a decision to make, not a vague warning (e.g. "Decide whether to use WebSockets or SSE for real-time updates before starting Phase 4: Real-Time Features")
|
|
11746
|
-
- **Tied to trajectory** \u2014 based on current board state, ADs, and velocity, not generic advice
|
|
11747
|
-
If the Forward Horizon context is absent or there are no meaningful decisions to surface, omit this section entirely. Do NOT generate generic advice like "plan ahead" or "consider testing".
|
|
11748
|
-
|
|
11749
|
-
**CRITICAL: Review your Part 2 JSON before finishing. Every action from Part 1 must have a corresponding entry in Part 2. If Part 1 mentions corrections, new tasks, AD changes, or handoffs but Part 2 has empty arrays \u2014 you have a persistence bug.**`;
|
|
11750
12019
|
var PLAN_FRAGMENT_DISCOVERY_GAPS = `
|
|
11751
12020
|
5. **Discovery Gaps** \u2014 If a Discovery Canvas section is provided in the context below, check which sections are populated vs empty. In cycles 1-10, or whenever canvas sections have been empty for 5+ cycles, include a "Discovery Gaps" paragraph in your cycle log suggesting what context would improve planning. Examples: "Your project context would benefit from MVP boundary definition" or "Consider documenting key user journeys." Keep suggestions conversational \u2014 do NOT create tasks for discovery gaps. If all canvas sections are populated, or no Discovery Canvas is provided, skip this step entirely.`;
|
|
11752
12021
|
var PLAN_FRAGMENT_RESEARCH = `
|
|
@@ -11839,8 +12108,7 @@ var PLAN_FRAGMENT_FORWARD_HORIZON = `
|
|
|
11839
12108
|
- **Actionable** \u2014 frame as a decision to make, not a vague warning (e.g. "Decide whether to use WebSockets or SSE for real-time updates before starting Phase 4: Real-Time Features")
|
|
11840
12109
|
- **Tied to trajectory** \u2014 based on current board state, ADs, and velocity, not generic advice
|
|
11841
12110
|
If the Forward Horizon context is absent or there are no meaningful decisions to surface, omit this section entirely. Do NOT generate generic advice like "plan ahead" or "consider testing".`;
|
|
11842
|
-
function
|
|
11843
|
-
if (!flags || !ctx) return PLAN_FULL_INSTRUCTIONS;
|
|
12111
|
+
function composeFullModeInstructions(flags, ctx) {
|
|
11844
12112
|
const parts = [
|
|
11845
12113
|
`## FULL MODE
|
|
11846
12114
|
|
|
@@ -11924,6 +12192,7 @@ ${AD_REJECTION_RULES}
|
|
|
11924
12192
|
10. **BUILD HANDOFFs** \u2014 Generate a full BUILD HANDOFF block for every task selected for this cycle. Include each handoff in the \`cycleHandoffs\` array in the structured output. The handoffs are written to each task on the board for durability.
|
|
11925
12193
|
**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.
|
|
11926
12194
|
**Scope pre-check:** Before writing the SCOPE section of each handoff, cross-reference the task against the "Recently Shipped Capabilities" section in the context below (if present). For each candidate task: (1) check if the task's title or scope overlaps with any recently shipped task, (2) check if the FILES LIKELY TOUCHED overlap with files already modified in recent builds, (3) check the architecture notes from recent builds for patterns that already cover this task's scope. If >80% of a task's scope appears in recently shipped capabilities, recommend cancellation via \`boardCorrections\` or reduce the handoff scope to only the missing pieces \u2014 explicitly note what already exists. C126 task-728 was over-scoped because the planner assumed Blocked status needed creating from scratch \u2014 it already existed in types, DB, orient, and build_list. Over-scoped handoffs waste builder time on verification and cause estimation mismatches.
|
|
12195
|
+
**\u26A0\uFE0F PRE-SCOPE GROUNDING (enforced):** The Scope pre-check above is NOT optional \u2014 before writing the SCOPE of any "build/implement X" handoff you MUST ground the scope against current reality. (1) Check the "### Relevant Research Docs" context section (if present) AND docs/INDEX.md for an existing design/research/status:final doc that already covers this capability. If one exists, either (a) reduce the handoff SCOPE to only the genuinely-missing pieces and NAME the existing doc inside the handoff, or (b) emit a \`boardCorrections\` cancellation with a \`closureReason\` citing the doc. (2) The resulting handoff's PRE-BUILD VERIFICATION section MUST name the specific files and/or docs the builder reads to confirm X is not already implemented \u2014 this is required at SCOPING time, not deferred for the builder to discover. (3) When a candidate's scope was reduced or cancelled because an existing doc or shipped capability already covers it, record a "Grounding Check:" line in \`cycleLogNotes\` naming the task ID and what was already covered, so the owner sees what the guard caught. Evidence (C273): task-1873 was sized Large but ~half its scope was already shipped (hub OAuth-default, llms.txt), and task-1874 proposed rebuilding shipped login against a status:final research doc \u2014 both because scope was written from a stale basis without this grounding step. Do NOT build a new code/doc search service \u2014 use the injected context above plus docs/INDEX.md.
|
|
11927
12196
|
**Reality check (task-1755):** Before scoping a handoff for a candidate task, evaluate whether the implicit assumptions a builder would make actually hold. For each task ask: (a) **Replacement assets** \u2014 does the task assume a copy/image/visual/dataset/fixture that doesn't yet exist? (e.g. "swap the hero illustration" assumes a new illustration is ready); (b) **Required user decisions** \u2014 does it assume an answer to an unresolved question (positioning, copy direction, model choice, pricing) that the user has not yet made?; (c) **Infrastructure dependencies** \u2014 does it assume a service/credential/migration/feature flag that isn't yet provisioned? If ANY assumption is unmet: do NOT generate a handoff for that task. Instead include the task in a **Pre-conditions Missing** paragraph in \`cycleLogNotes\` listing the task ID, the missing dependency, and the suggested unblock (e.g. "task-1234: needs new hero illustration \u2014 file separate asset-creation task or surface as a decision"). Drop it from the cycle entirely; the user can re-add it once the dependency is satisfied. ellies-birthday C5 hit two mid-cycle defers in one cycle from this exact gap (handoffs assumed assets that didn't exist) \u2014 reality check catches that before scope is written, not after work starts.
|
|
11928
12197
|
**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.
|
|
11929
12198
|
**Maturity gate applies here:** Do NOT generate BUILD HANDOFFs for tasks that failed the maturity gate in step 6 (phase prerequisites not met, dependency chain incomplete). Raw tasks that the planner has scoped and upgraded to "investigated" in step 6 ARE eligible for handoffs.
|
|
@@ -11934,7 +12203,9 @@ ${AD_REJECTION_RULES}
|
|
|
11934
12203
|
**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.
|
|
11935
12204
|
**Pre-mortem:** For projects with 10+ cycles, include a PRE-MORTEM section in every BUILD HANDOFF with 1-3 bullet points: (a) most likely technical blocker based on module history, (b) integration risk with adjacent systems, (c) scope creep signal \u2014 what the builder might be tempted to expand beyond scope. Draw from \`dead_ends\` and \`surprises\` in recent build reports for the same module. Omit this section entirely for projects with fewer than 10 cycles.
|
|
11936
12205
|
**Intra-cycle dependency detection:** After selecting cycle tasks, check every pair for build-order dependencies. Two tasks A and B have an intra-cycle dependency when A must be built before B because B consumes an artifact A creates \u2014 e.g. A adds a new adapter method that B calls, A creates a DB migration B depends on, A introduces a new shared type B imports, A refactors a utility B modifies. Signals: same module + adjacent scope (one is "add X", another is "use X"), or notes explicitly reference the other task. For each dependency detected: (a) populate the DEPENDS ON section in the dependent task's BUILD HANDOFF with the upstream task ID(s); (b) add a \`boardCorrections\` entry for the dependent task with \`updates.dependsOn\` set to the comma-separated upstream IDs \u2014 this persists the dependency so the builder's runtime can reuse the upstream branch; (c) keep SCOPE sections independent but note the ordering in "Why now". Do NOT invent dependencies where tasks merely share a module \u2014 only real build-order coupling counts. Linear chains only \u2014 no multi-level graph resolution. When in doubt, omit.
|
|
11937
|
-
**
|
|
12206
|
+
**Dependency Chain section (Part 1 markdown):** When intra-cycle dependencies are detected, include a visible **## Dependency Chain** section in Part 1 markdown immediately before the first BUILD HANDOFF block. List each dependency as an arrow chain with a brief reason: \`task-A \u2192 task-B (B calls the adapter method A creates)\`. Then show the full recommended build sequence for all cycle tasks, including standalone tasks: e.g. \`Build order: task-A \u2192 task-B; task-C standalone; task-D standalone\`. Flag circular dependencies with \u26A0\uFE0F and a note. Omit this section entirely when no intra-cycle dependencies exist \u2014 do not include an empty section.
|
|
12207
|
+
**Build order in cycle log:** If intra-cycle dependencies were detected, include a "Build order:" line in \`cycleLogNotes\` showing the recommended sequence as arrow chains (e.g. "Build order: task-123 \u2192 task-124; task-130 standalone"). Skip when no dependencies exist.
|
|
12208
|
+
**Branch-coherence check (task-1908):** In-cycle tasks are routed to one shared branch PER MODULE (\`feat/cycle-N-<module>\`); tasks in different modules land on different branches and merge separately. After writing the handoffs, cross-reference the FILES LIKELY TOUCHED lists: if two selected tasks in DIFFERENT modules name the SAME file, those edits will land on separate branches and collide at merge time (C273 hit two manual merge-conflict resolutions this way on \`docs/architecture/mcp-server-deploy.md\`). For each such overlap, add a "Branch-coherence:" line to \`cycleLogNotes\` naming the two tasks + the shared file and recommend ONE of: consolidate the overlapping tasks into the same module so they share a branch, or sequence them so the second rebases on the first. Do NOT change the one-branch-per-module convention \u2014 flag and recommend only. Same-module overlaps are fine (they already share a branch); only cross-module same-file overlaps are flagged.`);
|
|
11938
12209
|
if (flags.hasResearchTasks) parts.push(PLAN_FRAGMENT_RESEARCH);
|
|
11939
12210
|
if (flags.hasBugTasks) parts.push(PLAN_FRAGMENT_BUG);
|
|
11940
12211
|
if (flags.hasIdeaTasks) parts.push(PLAN_FRAGMENT_IDEA);
|
|
@@ -11946,6 +12217,8 @@ ${AD_REJECTION_RULES}
|
|
|
11946
12217
|
if (flags.hasOpsBriefTasks) parts.push(PLAN_FRAGMENT_OPS_BRIEF);
|
|
11947
12218
|
if (flags.hasUITasks) parts.push(PLAN_FRAGMENT_UI);
|
|
11948
12219
|
parts.push(`
|
|
12220
|
+
**Handoff quality gate (score before emit):** Apply this quality bar to every BUILD HANDOFF \u2014 including any task-type-specific additions above \u2014 before persisting it. ${OUTPUT_QUALITY_RUBRIC} If you had to regenerate a handoff to clear the threshold, you MAY note it in \`cycleLogNotes\` (e.g. "Regenerated task-XXX handoff \u2014 below quality threshold on file specificity").`);
|
|
12221
|
+
parts.push(`
|
|
11949
12222
|
11. **New Tasks (max 3 per cycle)** \u2014 Actively mine the Recent Build Reports for task candidates. For each report, check:
|
|
11950
12223
|
- **Discovered Issues:** If a build report lists a discovered issue and no existing board task covers it, propose a new task.
|
|
11951
12224
|
- **Surprises:** If a surprise reveals a gap (e.g. "schema assumed but not verified"), propose a task to close it.
|
|
@@ -11963,6 +12236,26 @@ ${AD_REJECTION_RULES}
|
|
|
11963
12236
|
**CRITICAL: Review your Part 2 JSON before finishing. Every action from Part 1 must have a corresponding entry in Part 2. If Part 1 mentions corrections, new tasks, AD changes, or handoffs but Part 2 has empty arrays \u2014 you have a persistence bug.**`);
|
|
11964
12237
|
return parts.join("\n");
|
|
11965
12238
|
}
|
|
12239
|
+
var PLAN_FULL_BASELINE_FLAGS = {
|
|
12240
|
+
hasBugTasks: true,
|
|
12241
|
+
hasResearchTasks: true,
|
|
12242
|
+
hasIdeaTasks: true,
|
|
12243
|
+
hasSpikeTasks: false,
|
|
12244
|
+
hasUITasks: true,
|
|
12245
|
+
hasTaskTasks: false,
|
|
12246
|
+
hasDesignBriefTasks: false,
|
|
12247
|
+
hasResearchBriefTasks: false,
|
|
12248
|
+
hasMarketingBriefTasks: false,
|
|
12249
|
+
hasOpsBriefTasks: false
|
|
12250
|
+
};
|
|
12251
|
+
var PLAN_FULL_INSTRUCTIONS = composeFullModeInstructions(PLAN_FULL_BASELINE_FLAGS, {
|
|
12252
|
+
hasDiscoveryCanvas: true,
|
|
12253
|
+
hasHorizonContext: true
|
|
12254
|
+
});
|
|
12255
|
+
function buildPlanFullInstructionsConditional(flags, ctx) {
|
|
12256
|
+
if (!flags || !ctx) return PLAN_FULL_INSTRUCTIONS;
|
|
12257
|
+
return composeFullModeInstructions(flags, ctx);
|
|
12258
|
+
}
|
|
11966
12259
|
function buildPlanUserMessage(ctx) {
|
|
11967
12260
|
const modeLabel = ctx.mode.toUpperCase();
|
|
11968
12261
|
const parts = [
|
|
@@ -12257,6 +12550,7 @@ IMPORTANT: You are running as a non-interactive API call. Do NOT ask the user qu
|
|
|
12257
12550
|
- **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.
|
|
12258
12551
|
- **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.
|
|
12259
12552
|
- **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.
|
|
12553
|
+
- **Recommendation quality gate.** Apply this quality bar to every \`strategicRecommendations\` point and \`actionItems\` entry before emitting it \u2014 a vague recommendation is worse than none. ${OUTPUT_QUALITY_RUBRIC}
|
|
12260
12554
|
|
|
12261
12555
|
## TWO-PHASE DELIVERY
|
|
12262
12556
|
|
|
@@ -18545,6 +18839,9 @@ async function scaffoldPapiDir(adapter2, config2, input) {
|
|
|
18545
18839
|
} catch {
|
|
18546
18840
|
}
|
|
18547
18841
|
}
|
|
18842
|
+
if (config2.adapterType === "proxy") {
|
|
18843
|
+
return true;
|
|
18844
|
+
}
|
|
18548
18845
|
const commandsDir = join5(config2.projectRoot, ".claude", "commands");
|
|
18549
18846
|
const docsDir = join5(config2.projectRoot, "docs");
|
|
18550
18847
|
await mkdir(commandsDir, { recursive: true });
|
|
@@ -18648,9 +18945,6 @@ async function ensurePapiPermission(projectRoot) {
|
|
|
18648
18945
|
}
|
|
18649
18946
|
async function applySetupOutputs(adapter2, config2, input, briefText, adSeedText, conventionsText) {
|
|
18650
18947
|
const warnings = [];
|
|
18651
|
-
if (config2.adapterType !== "pg") {
|
|
18652
|
-
await writeFile2(join5(config2.papiDir, "PRODUCT_BRIEF.md"), briefText, "utf-8");
|
|
18653
|
-
}
|
|
18654
18948
|
await adapter2.updateProductBrief(briefText);
|
|
18655
18949
|
const briefPhases = parsePhases(briefText);
|
|
18656
18950
|
if (briefPhases.length > 0) {
|
|
@@ -18712,7 +19006,7 @@ async function applySetupOutputs(adapter2, config2, input, briefText, adSeedText
|
|
|
18712
19006
|
}
|
|
18713
19007
|
}
|
|
18714
19008
|
}
|
|
18715
|
-
if (conventionsText?.trim()) {
|
|
19009
|
+
if (conventionsText?.trim() && config2.adapterType !== "proxy") {
|
|
18716
19010
|
try {
|
|
18717
19011
|
const claudeMdPath = join5(config2.projectRoot, "CLAUDE.md");
|
|
18718
19012
|
const existing = await readFile4(claudeMdPath, "utf-8");
|
|
@@ -19073,28 +19367,30 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
|
|
|
19073
19367
|
if (msg.startsWith("100% overlap")) throw err;
|
|
19074
19368
|
}
|
|
19075
19369
|
}
|
|
19076
|
-
|
|
19077
|
-
|
|
19078
|
-
|
|
19079
|
-
|
|
19080
|
-
|
|
19081
|
-
|
|
19082
|
-
|
|
19083
|
-
|
|
19084
|
-
|
|
19085
|
-
|
|
19086
|
-
|
|
19087
|
-
|
|
19088
|
-
|
|
19089
|
-
|
|
19090
|
-
|
|
19091
|
-
|
|
19092
|
-
|
|
19093
|
-
|
|
19094
|
-
|
|
19095
|
-
|
|
19370
|
+
if (config2.adapterType !== "proxy") {
|
|
19371
|
+
try {
|
|
19372
|
+
const claudeMdPath = join5(config2.projectRoot, "CLAUDE.md");
|
|
19373
|
+
const existing = await readFile4(claudeMdPath, "utf-8");
|
|
19374
|
+
if (!existing.includes("Dogfood Logging")) {
|
|
19375
|
+
const dogfoodSection = [
|
|
19376
|
+
"",
|
|
19377
|
+
"## Dogfood Logging",
|
|
19378
|
+
"",
|
|
19379
|
+
"After each `release`, append a dogfood entry capturing observations from the cycle.",
|
|
19380
|
+
"Call the adapter method with structured entries for each observation:",
|
|
19381
|
+
"",
|
|
19382
|
+
"- **friction** \u2014 workflow pain points, confusing flows, things that broke or slowed you down",
|
|
19383
|
+
"- **methodology** \u2014 what worked or didn't in the plan/build/review cycle",
|
|
19384
|
+
"- **signal** \u2014 indicators of product-market fit, user value, or growth potential",
|
|
19385
|
+
"- **commercial** \u2014 cost, pricing, or business model observations",
|
|
19386
|
+
"",
|
|
19387
|
+
"This is autonomous plumbing \u2014 log observations after release without asking.",
|
|
19388
|
+
""
|
|
19389
|
+
].join("\n");
|
|
19390
|
+
await writeFile2(claudeMdPath, existing + dogfoodSection, "utf-8");
|
|
19391
|
+
}
|
|
19392
|
+
} catch {
|
|
19096
19393
|
}
|
|
19097
|
-
} catch {
|
|
19098
19394
|
}
|
|
19099
19395
|
if (adapter2.writeDogfoodEntries) {
|
|
19100
19396
|
try {
|
|
@@ -19116,7 +19412,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
|
|
|
19116
19412
|
});
|
|
19117
19413
|
} catch {
|
|
19118
19414
|
}
|
|
19119
|
-
const gitignoreNote = await ensureMcpJsonGitignored(config2.projectRoot);
|
|
19415
|
+
const gitignoreNote = config2.adapterType === "proxy" ? void 0 : await ensureMcpJsonGitignored(config2.projectRoot);
|
|
19120
19416
|
let cursorScaffolded = false;
|
|
19121
19417
|
try {
|
|
19122
19418
|
await access2(join5(config2.projectRoot, ".cursor", "rules", "papi.mdc"));
|
|
@@ -19526,6 +19822,10 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
|
|
|
19526
19822
|
const parts = p.split(/[\\/]/);
|
|
19527
19823
|
return parts[parts.length - 1] ?? p;
|
|
19528
19824
|
};
|
|
19825
|
+
const dirname6 = (p) => {
|
|
19826
|
+
const idx = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"));
|
|
19827
|
+
return idx > 0 ? p.slice(0, idx) : "";
|
|
19828
|
+
};
|
|
19529
19829
|
const predictedNames = new Set(predictedFiles.map(basename2).filter(Boolean));
|
|
19530
19830
|
const scoped = modified.filter((p) => predictedNames.has(basename2(p)));
|
|
19531
19831
|
if (scoped.length === 0) {
|
|
@@ -19533,7 +19833,23 @@ function autoCommit(config2, taskId, taskTitle, predictedFiles) {
|
|
|
19533
19833
|
const predSample = predictedFiles.slice(0, 5).join(", ");
|
|
19534
19834
|
return `Auto-commit: refused \u2014 none of the ${modified.length} modified file(s) intersect FILES LIKELY TOUCHED. Modified: ${modSample}. Expected: ${predSample}. Stage the intended files manually (\`git add <paths>\`) then re-run, or set PAPI_AUTO_COMMIT=false.`;
|
|
19535
19835
|
}
|
|
19536
|
-
|
|
19836
|
+
const untracked = getUntrackedFiles(cwd);
|
|
19837
|
+
const scopedDirs = [...new Set(scoped.map(dirname6).filter((d) => d.length > 0))];
|
|
19838
|
+
const isUnderScopedDir = (p) => scopedDirs.some((d) => p === d || p.startsWith(`${d}/`) || p.startsWith(`${d}\\`));
|
|
19839
|
+
const scopedSet = new Set(scoped);
|
|
19840
|
+
const adjacentUntracked = untracked.filter(
|
|
19841
|
+
(p) => !scopedSet.has(p) && isUnderScopedDir(p)
|
|
19842
|
+
);
|
|
19843
|
+
const toStage = [...scoped, ...adjacentUntracked];
|
|
19844
|
+
const toStageSet = new Set(toStage);
|
|
19845
|
+
const droppedUntracked = untracked.filter((p) => !toStageSet.has(p));
|
|
19846
|
+
let line = safeRun(() => stagePathsAndCommit(cwd, toStage, message)) + ` (scoped to ${scoped.length}/${modified.length} files via FILES LIKELY TOUCHED` + (adjacentUntracked.length > 0 ? ` + ${adjacentUntracked.length} untracked under scoped dir(s)` : "") + `).`;
|
|
19847
|
+
if (droppedUntracked.length > 0) {
|
|
19848
|
+
const sample = droppedUntracked.slice(0, 10).join(", ");
|
|
19849
|
+
const more = droppedUntracked.length > 10 ? ` (+${droppedUntracked.length - 10} more)` : "";
|
|
19850
|
+
line += ` \u26A0\uFE0F ${droppedUntracked.length} untracked file(s) were NOT committed: ${sample}${more}. If they belong to this task, run \`git add <paths> && git commit --amend --no-edit\` before pushing \u2014 otherwise the committed tree may not build on checkout/CI.`;
|
|
19851
|
+
}
|
|
19852
|
+
return line;
|
|
19537
19853
|
}
|
|
19538
19854
|
return safeRun(() => stageAllAndCommit(cwd, message));
|
|
19539
19855
|
}
|
|
@@ -22495,7 +22811,7 @@ function getInstallId() {
|
|
|
22495
22811
|
// src/lib/telemetry.ts
|
|
22496
22812
|
var HOSTED_SUPABASE_URL2 = process.env["PAPI_HOSTED_SUPABASE_URL"] ?? "https://guewgygcpcmrcoppihzx.supabase.co";
|
|
22497
22813
|
var DEFAULT_TELEMETRY_ENDPOINT = `${HOSTED_SUPABASE_URL2}/functions/v1/data-proxy`;
|
|
22498
|
-
var MD_PINGS_SUPABASE_URL = HOSTED_SUPABASE_URL2;
|
|
22814
|
+
var MD_PINGS_SUPABASE_URL = process.env["PAPI_MD_PINGS_URL"] ?? HOSTED_SUPABASE_URL2;
|
|
22499
22815
|
var MD_PINGS_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd1ZXdneWdjcGNtcmNvcHBpaHp4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI2Njk2NTMsImV4cCI6MjA4ODI0NTY1M30.V5Jw7wJgiMpSQPa2mt0ftjyye5ynG1qLlam00yPVNJY";
|
|
22500
22816
|
function isEnabled() {
|
|
22501
22817
|
const val = process.env["PAPI_TELEMETRY"];
|
|
@@ -22565,6 +22881,11 @@ function emitMilestone(projectId, milestone, extra) {
|
|
|
22565
22881
|
metadata: extra
|
|
22566
22882
|
});
|
|
22567
22883
|
}
|
|
22884
|
+
function resolveTelemetryProjectId(config2) {
|
|
22885
|
+
if (config2.projectId && config2.projectId.length > 0) return config2.projectId;
|
|
22886
|
+
const env = process.env["PAPI_PROJECT_ID"];
|
|
22887
|
+
return env && env.length > 0 ? env : void 0;
|
|
22888
|
+
}
|
|
22568
22889
|
|
|
22569
22890
|
// src/services/release.ts
|
|
22570
22891
|
init_git();
|
|
@@ -23197,16 +23518,32 @@ var state = {
|
|
|
23197
23518
|
lastOrientAt: null,
|
|
23198
23519
|
releaseSinceLastOrient: false,
|
|
23199
23520
|
sessionStartedAt: Date.now(),
|
|
23200
|
-
lastReviewListAt: null
|
|
23521
|
+
lastReviewListAt: null,
|
|
23522
|
+
failureTimestamps: [],
|
|
23523
|
+
consecutiveFailures: 0
|
|
23201
23524
|
};
|
|
23202
23525
|
var CONTEXT_BLOAT_CALL_THRESHOLD = 40;
|
|
23203
23526
|
var ORIENT_GAP_MS = 3 * 60 * 60 * 1e3;
|
|
23204
23527
|
var REVIEW_LIST_GUARD_WINDOW_MS = 15 * 60 * 1e3;
|
|
23528
|
+
var FAILURE_WINDOW_MS = 30 * 60 * 1e3;
|
|
23529
|
+
var FAILURE_RATE_THRESHOLD = 8;
|
|
23530
|
+
var CONSECUTIVE_FAILURE_THRESHOLD = 4;
|
|
23205
23531
|
function recordToolCall(name) {
|
|
23206
23532
|
state.toolCallCount++;
|
|
23207
23533
|
if (name === "release") state.releaseSinceLastOrient = true;
|
|
23208
23534
|
if (name === "review_list") state.lastReviewListAt = Date.now();
|
|
23209
23535
|
}
|
|
23536
|
+
function recordToolOutcome(success) {
|
|
23537
|
+
if (success) {
|
|
23538
|
+
state.consecutiveFailures = 0;
|
|
23539
|
+
return;
|
|
23540
|
+
}
|
|
23541
|
+
const now = Date.now();
|
|
23542
|
+
state.consecutiveFailures++;
|
|
23543
|
+
state.failureTimestamps.push(now);
|
|
23544
|
+
const cutoff = now - FAILURE_WINDOW_MS;
|
|
23545
|
+
state.failureTimestamps = state.failureTimestamps.filter((t) => t >= cutoff);
|
|
23546
|
+
}
|
|
23210
23547
|
function wasReviewListSeenRecently(windowMs = REVIEW_LIST_GUARD_WINDOW_MS) {
|
|
23211
23548
|
if (state.lastReviewListAt == null) return false;
|
|
23212
23549
|
return Date.now() - state.lastReviewListAt <= windowMs;
|
|
@@ -23219,8 +23556,22 @@ function getProjectConnectionBanner(projectName, projectSlug) {
|
|
|
23219
23556
|
if (!projectName || !projectSlug) return null;
|
|
23220
23557
|
return `[Connected: ${projectName} (${projectSlug})] \u2014 confirm this is the project you mean before I write to it. If it's wrong, don't proceed: pass \`project=<id>\` on the call to target a different project, or fix the project id in your MCP config (PAPI_PROJECT_ID for local, x-papi-project-id header for remote) and reconnect.`;
|
|
23221
23558
|
}
|
|
23559
|
+
function detectContextDegradation(now = Date.now()) {
|
|
23560
|
+
if (state.consecutiveFailures >= CONSECUTIVE_FAILURE_THRESHOLD) {
|
|
23561
|
+
return `Context degradation (clash): ${state.consecutiveFailures} tool calls failed in a row \u2014 the session may be stuck on a contradiction. Consider a fresh window after this task.`;
|
|
23562
|
+
}
|
|
23563
|
+
const cutoff = now - FAILURE_WINDOW_MS;
|
|
23564
|
+
const recentFailures = state.failureTimestamps.filter((t) => t >= cutoff).length;
|
|
23565
|
+
if (recentFailures >= FAILURE_RATE_THRESHOLD) {
|
|
23566
|
+
const mins = Math.round(FAILURE_WINDOW_MS / 6e4);
|
|
23567
|
+
return `Context degradation (distraction/confusion): ${recentFailures} tool failures in the last ${mins}min. A fresh window often clears this faster than pushing on.`;
|
|
23568
|
+
}
|
|
23569
|
+
return null;
|
|
23570
|
+
}
|
|
23222
23571
|
async function buildSessionGuidance() {
|
|
23223
23572
|
const signals = [];
|
|
23573
|
+
const degradation = detectContextDegradation();
|
|
23574
|
+
if (degradation) signals.push(degradation);
|
|
23224
23575
|
if (state.toolCallCount > CONTEXT_BLOAT_CALL_THRESHOLD) {
|
|
23225
23576
|
signals.push(
|
|
23226
23577
|
`${state.toolCallCount} tool calls this session \u2014 context may be bloated. Consider starting a fresh window.`
|
|
@@ -25106,6 +25457,37 @@ function dedupeEnrichmentBlob(existing, blob) {
|
|
|
25106
25457
|
return output.join("\n");
|
|
25107
25458
|
}
|
|
25108
25459
|
|
|
25460
|
+
// src/lib/verify-project.ts
|
|
25461
|
+
async function verifyProject(adapter2) {
|
|
25462
|
+
const warnings = [];
|
|
25463
|
+
if (adapter2.readHorizons && adapter2.readStages) {
|
|
25464
|
+
try {
|
|
25465
|
+
const [horizons, stages] = await Promise.all([
|
|
25466
|
+
adapter2.readHorizons(),
|
|
25467
|
+
adapter2.readStages()
|
|
25468
|
+
]);
|
|
25469
|
+
if (horizons.length === 0 && stages.length === 0) {
|
|
25470
|
+
warnings.push(
|
|
25471
|
+
"\u26A0\uFE0F Your project predates the AD-14 hierarchy seed (Horizon \u2192 Stage \u2192 Phase \u2192 Task). Hub and Strategy surfaces will be partially empty until horizons + stages are populated. Run `papi setup --backfill-hierarchy` to seed the canonical layout."
|
|
25472
|
+
);
|
|
25473
|
+
}
|
|
25474
|
+
} catch {
|
|
25475
|
+
}
|
|
25476
|
+
}
|
|
25477
|
+
if (adapter2.getProjectOwnerUserId) {
|
|
25478
|
+
try {
|
|
25479
|
+
const ownerUserId = await adapter2.getProjectOwnerUserId();
|
|
25480
|
+
if (ownerUserId === null) {
|
|
25481
|
+
warnings.push(
|
|
25482
|
+
"\u26A0\uFE0F Your project's `projects.user_id` is NULL \u2014 pre-multi-user legacy row. Dashboard ownership and activity attribution will be incomplete until backfilled. Run `papi setup --backfill-user-id` to attach the row to your authenticated user."
|
|
25483
|
+
);
|
|
25484
|
+
}
|
|
25485
|
+
} catch {
|
|
25486
|
+
}
|
|
25487
|
+
}
|
|
25488
|
+
return warnings;
|
|
25489
|
+
}
|
|
25490
|
+
|
|
25109
25491
|
// src/tools/orient.ts
|
|
25110
25492
|
import { execFile } from "child_process";
|
|
25111
25493
|
import { promisify } from "util";
|
|
@@ -25723,6 +26105,11 @@ async function handleOrient(adapter2, config2, args = {}) {
|
|
|
25723
26105
|
if (proxyWarning) buildResult.warnings.push(proxyWarning);
|
|
25724
26106
|
const p1Warning = p1BacklogOutcome.status === "fulfilled" ? p1BacklogOutcome.value : void 0;
|
|
25725
26107
|
if (p1Warning) buildResult.warnings.push(p1Warning);
|
|
26108
|
+
try {
|
|
26109
|
+
const verifyWarnings = await verifyProject(adapter2);
|
|
26110
|
+
for (const w of verifyWarnings) buildResult.warnings.push(w);
|
|
26111
|
+
} catch {
|
|
26112
|
+
}
|
|
25726
26113
|
const ttfvNote = ttfvOutcome.status === "fulfilled" ? ttfvOutcome.value : "";
|
|
25727
26114
|
const latestTag = latestTagOutcome.status === "fulfilled" ? latestTagOutcome.value : "";
|
|
25728
26115
|
const versionDrift = versionDriftOutcome.status === "fulfilled" ? versionDriftOutcome.value : void 0;
|
|
@@ -26800,6 +27187,45 @@ Use \`learning_action mark\` or submit via \`idea\` to close these out.`
|
|
|
26800
27187
|
return errorResponse(`Unknown mode "${mode}". Use "mark" or "list".`);
|
|
26801
27188
|
}
|
|
26802
27189
|
|
|
27190
|
+
// src/tools/discovered-issue-resolve.ts
|
|
27191
|
+
var discoveredIssueResolveTool = {
|
|
27192
|
+
name: "discovered_issue_resolve",
|
|
27193
|
+
description: 'Mark a discovered_issue (cycle_learnings row, category="issue") as resolved. Pass the learning_id you saw in orient / learning_action list output. The row stays in the database for history, but default reads exclude it. Use this when the underlying fix has actually landed \u2014 NOT when you just created a follow-up task (that is `learning_action mark` with action_taken="task_created"). Optional `note` is recorded as resolved_by.',
|
|
27194
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
27195
|
+
inputSchema: {
|
|
27196
|
+
type: "object",
|
|
27197
|
+
properties: {
|
|
27198
|
+
issue_id: {
|
|
27199
|
+
type: "string",
|
|
27200
|
+
description: "The cycle_learnings id (UUID) to mark resolved. Find it via `learning_action list` or recent orient output."
|
|
27201
|
+
},
|
|
27202
|
+
note: {
|
|
27203
|
+
type: "string",
|
|
27204
|
+
description: "Optional resolved_by tag \u2014 e.g. the task id whose merge resolved this issue, or the agent name. Recorded for audit."
|
|
27205
|
+
}
|
|
27206
|
+
},
|
|
27207
|
+
required: ["issue_id"]
|
|
27208
|
+
}
|
|
27209
|
+
};
|
|
27210
|
+
async function handleDiscoveredIssueResolve(adapter2, args) {
|
|
27211
|
+
const issueId = typeof args.issue_id === "string" ? args.issue_id.trim() : "";
|
|
27212
|
+
const note = typeof args.note === "string" ? args.note.trim() : void 0;
|
|
27213
|
+
if (!issueId) {
|
|
27214
|
+
return errorResponse("issue_id is required.");
|
|
27215
|
+
}
|
|
27216
|
+
if (!adapter2.markCycleLearningResolved) {
|
|
27217
|
+
return errorResponse("discovered_issue_resolve requires the pg adapter. Not available in md / proxy mode.");
|
|
27218
|
+
}
|
|
27219
|
+
try {
|
|
27220
|
+
await adapter2.markCycleLearningResolved(issueId, note);
|
|
27221
|
+
return textResponse(
|
|
27222
|
+
`Discovered issue \`${issueId}\` marked resolved${note ? ` (by: \`${note}\`)` : ""}. Future reads will exclude it by default; pass include_resolved=true to surface.`
|
|
27223
|
+
);
|
|
27224
|
+
} catch (err) {
|
|
27225
|
+
return errorResponse(`Failed to resolve issue: ${err instanceof Error ? err.message : String(err)}`);
|
|
27226
|
+
}
|
|
27227
|
+
}
|
|
27228
|
+
|
|
26803
27229
|
// src/tools/project.ts
|
|
26804
27230
|
import path6 from "path";
|
|
26805
27231
|
function workspacePapiDir(config2) {
|
|
@@ -27181,6 +27607,7 @@ var PAPI_TOOLS = [
|
|
|
27181
27607
|
scopeBriefTool,
|
|
27182
27608
|
adViewTool,
|
|
27183
27609
|
learningActionTool,
|
|
27610
|
+
discoveredIssueResolveTool,
|
|
27184
27611
|
projectCreateTool,
|
|
27185
27612
|
projectListTool,
|
|
27186
27613
|
projectSwitchTool,
|
|
@@ -27357,6 +27784,8 @@ function createServer(adapter2, config2) {
|
|
|
27357
27784
|
return handleAdView(adapter2, safeArgs);
|
|
27358
27785
|
case "learning_action":
|
|
27359
27786
|
return handleLearningAction(adapter2, safeArgs);
|
|
27787
|
+
case "discovered_issue_resolve":
|
|
27788
|
+
return handleDiscoveredIssueResolve(adapter2, safeArgs);
|
|
27360
27789
|
case "project_create":
|
|
27361
27790
|
return handleProjectCreate(adapter2, config2, safeArgs);
|
|
27362
27791
|
case "project_list":
|
|
@@ -27381,6 +27810,7 @@ function createServer(adapter2, config2) {
|
|
|
27381
27810
|
console.error(`[papi] Exiting after '${name}' timeout (wedge-recovery #${wedgeRecoveryStats.count}); user must reconnect with /mcp before the next call.`);
|
|
27382
27811
|
process.exit(2);
|
|
27383
27812
|
});
|
|
27813
|
+
recordToolOutcome(false);
|
|
27384
27814
|
return { content: [{ type: "text", text: `Error: ${msg}` }] };
|
|
27385
27815
|
}
|
|
27386
27816
|
throw err;
|
|
@@ -27393,8 +27823,10 @@ function createServer(adapter2, config2) {
|
|
|
27393
27823
|
delete result._contextBytes;
|
|
27394
27824
|
delete result._contextUtilisation;
|
|
27395
27825
|
const isError = result.content.some((c) => c.text.startsWith("Error:") || c.text.startsWith("\u274C"));
|
|
27826
|
+
recordToolOutcome(!isError);
|
|
27396
27827
|
try {
|
|
27397
|
-
const
|
|
27828
|
+
const clientName = server2.getClientVersion()?.name;
|
|
27829
|
+
const metric = buildMetric(name, elapsed, usage, void 0, contextBytes, contextUtilisation, clientName);
|
|
27398
27830
|
metric.success = !isError;
|
|
27399
27831
|
adapter2.appendToolMetric(metric).catch(() => {
|
|
27400
27832
|
});
|
|
@@ -27404,7 +27836,7 @@ function createServer(adapter2, config2) {
|
|
|
27404
27836
|
const mdProjectSlug = config2.projectRoot ? config2.projectRoot.split("/").pop() : void 0;
|
|
27405
27837
|
emitMdAdapterPing(name, { duration_ms: elapsed, success: !isError }, config2.userId, mdProjectSlug);
|
|
27406
27838
|
}
|
|
27407
|
-
const telemetryProjectId =
|
|
27839
|
+
const telemetryProjectId = resolveTelemetryProjectId(config2);
|
|
27408
27840
|
if (telemetryProjectId) {
|
|
27409
27841
|
emitToolCall(telemetryProjectId, name, elapsed, {
|
|
27410
27842
|
adapter_type: config2.adapterType
|
|
@@ -27692,7 +28124,8 @@ async function dispatchRequest(args) {
|
|
|
27692
28124
|
});
|
|
27693
28125
|
const requestConfig = {
|
|
27694
28126
|
...baseConfig,
|
|
27695
|
-
adapterType: "proxy"
|
|
28127
|
+
adapterType: "proxy",
|
|
28128
|
+
projectId: effectiveProjectId
|
|
27696
28129
|
};
|
|
27697
28130
|
const server2 = createServer(adapter2, requestConfig);
|
|
27698
28131
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
|
|
@@ -27731,7 +28164,7 @@ async function dispatchRequest(args) {
|
|
|
27731
28164
|
var __dirname = dirname5(fileURLToPath3(import.meta.url));
|
|
27732
28165
|
var pkgVersion = "unknown";
|
|
27733
28166
|
try {
|
|
27734
|
-
const pkg = JSON.parse(
|
|
28167
|
+
const pkg = JSON.parse(readFileSync13(join21(__dirname, "..", "package.json"), "utf-8"));
|
|
27735
28168
|
pkgVersion = pkg.version;
|
|
27736
28169
|
} catch {
|
|
27737
28170
|
}
|
|
@@ -27745,12 +28178,15 @@ Commands:
|
|
|
27745
28178
|
(none) Start the MCP server (default)
|
|
27746
28179
|
doctor Print a diagnostic of your PAPI environment (read-only)
|
|
27747
28180
|
reset Surgically remove the papi entry from .mcp.json
|
|
28181
|
+
audit Cross-project harness/config audit + token-hygiene flags (read-only)
|
|
27748
28182
|
|
|
27749
28183
|
Options:
|
|
27750
28184
|
--help, -h Show this help message
|
|
27751
28185
|
--version, -v Show version number
|
|
27752
28186
|
--project <dir> Set the project directory
|
|
27753
28187
|
--yes, -y Skip confirmation prompts (reset only)
|
|
28188
|
+
--idle-mcp Flag PAPI-idle projects in audit (needs DATABASE_URL; read-only)
|
|
28189
|
+
--fix-pool Terminate this role's confirmed-wedged DB backends (doctor only; guarded)
|
|
27754
28190
|
|
|
27755
28191
|
Getting started:
|
|
27756
28192
|
1. Sign up at https://getpapi.ai/login
|
|
@@ -27773,12 +28209,16 @@ if (cliArgs.includes("--version") || cliArgs.includes("-v")) {
|
|
|
27773
28209
|
var subcommand = cliArgs.find((arg) => !arg.startsWith("-"));
|
|
27774
28210
|
if (subcommand === "doctor") {
|
|
27775
28211
|
const { runDoctor: runDoctor2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
|
|
27776
|
-
process.exit(await runDoctor2());
|
|
28212
|
+
process.exit(await runDoctor2(cliArgs));
|
|
27777
28213
|
}
|
|
27778
28214
|
if (subcommand === "reset") {
|
|
27779
28215
|
const { runReset: runReset2 } = await Promise.resolve().then(() => (init_reset(), reset_exports));
|
|
27780
28216
|
process.exit(await runReset2(cliArgs));
|
|
27781
28217
|
}
|
|
28218
|
+
if (subcommand === "audit") {
|
|
28219
|
+
const { runAudit: runAudit2 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
|
|
28220
|
+
process.exit(await runAudit2(cliArgs));
|
|
28221
|
+
}
|
|
27782
28222
|
process.on("unhandledRejection", (err) => {
|
|
27783
28223
|
console.error("[papi] unhandledRejection (swallowed):", err instanceof Error ? err.message : err);
|
|
27784
28224
|
});
|