@papi-ai/server 0.7.24 → 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.
Files changed (3) hide show
  1. package/dist/index.js +603 -196
  2. package/dist/prompts.js +30 -149
  3. 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
@@ -9944,7 +9980,8 @@ function taskBranchName(taskId) {
9944
9980
  return `feat/${taskId}`;
9945
9981
  }
9946
9982
  function cycleBranchName(cycleNumber, module) {
9947
- return `feat/cycle-${cycleNumber}-${module.toLowerCase().replace(/\s+/g, "-")}`;
9983
+ const slug = module.toLowerCase().replace(/&/g, "and").replace(/&/g, "and").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
9984
+ return `feat/cycle-${cycleNumber}-${slug}`;
9948
9985
  }
9949
9986
  function getHeadCommitSha(cwd) {
9950
9987
  try {
@@ -10294,17 +10331,133 @@ function formatReport(report) {
10294
10331
  lines.push("Need recovery? In the dashboard: Settings \u2192 Reset connection. Or surgically clean .mcp.json: `npx @papi-ai/server reset`.");
10295
10332
  return lines.join("\n");
10296
10333
  }
10297
- async function runDoctor() {
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 = []) {
10298
10437
  const report = diagnose();
10299
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");
10300
10442
  return 0;
10301
10443
  }
10302
- var SECRET_VARS, __testing;
10444
+ var SECRET_VARS, WEDGED_IDLE_TX_SECONDS, WEDGED_ACTIVE_SECONDS, __testing;
10303
10445
  var init_doctor = __esm({
10304
10446
  "src/cli/doctor.ts"() {
10305
10447
  "use strict";
10306
10448
  SECRET_VARS = /* @__PURE__ */ new Set(["PAPI_DATA_API_KEY", "DATABASE_URL", "PAPI_ENDPOINT"]);
10307
- __testing = { redact, findMcpJson, diagnose, formatReport };
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
+ };
10308
10461
  }
10309
10462
  });
10310
10463
 
@@ -10421,9 +10574,253 @@ var init_reset = __esm({
10421
10574
  }
10422
10575
  });
10423
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
+
10424
10821
  // src/index.ts
10425
- import { readFileSync as readFileSync12 } from "fs";
10426
- import { dirname as dirname5, join as join20 } from "path";
10822
+ import { readFileSync as readFileSync13 } from "fs";
10823
+ import { dirname as dirname5, join as join21 } from "path";
10427
10824
  import { fileURLToPath as fileURLToPath3 } from "url";
10428
10825
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10429
10826
  import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
@@ -11378,7 +11775,8 @@ function estimateCost(model, inputTokens, outputTokens) {
11378
11775
  if (!rates) return 0;
11379
11776
  return inputTokens * rates.input + outputTokens * rates.output;
11380
11777
  }
11381
- function buildMetric(tool, durationMs, usage, cycleNumber, contextBytes, contextUtilisation) {
11778
+ var MAX_CLIENT_NAME_LENGTH = 100;
11779
+ function buildMetric(tool, durationMs, usage, cycleNumber, contextBytes, contextUtilisation, clientName) {
11382
11780
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
11383
11781
  const metric = { timestamp, tool, durationMs };
11384
11782
  if (usage) {
@@ -11396,6 +11794,9 @@ function buildMetric(tool, durationMs, usage, cycleNumber, contextBytes, context
11396
11794
  if (contextUtilisation !== void 0) {
11397
11795
  metric.contextUtilisation = contextUtilisation;
11398
11796
  }
11797
+ if (clientName !== void 0 && clientName.length > 0) {
11798
+ metric.clientName = clientName.slice(0, MAX_CLIENT_NAME_LENGTH);
11799
+ }
11399
11800
  return metric;
11400
11801
  }
11401
11802
  function measureContextUtilisation(inputContext, llmOutput) {
@@ -11445,6 +11846,7 @@ If a candidate AD body could be invalidated by running a SQL query, refreshing a
11445
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.
11446
11847
 
11447
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.`;
11448
11850
  var PLAN_SYSTEM = `You are the PAPI Cycle Planner \u2014 an autonomous planning engine for software projects.
11449
11851
  You receive project context and produce a planning cycle output with a BUILD HANDOFF.
11450
11852
 
@@ -11614,152 +12016,6 @@ Before generating cycle tasks, answer: **is this a service or a platform?**
11614
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.
11615
12017
 
11616
12018
  **CRITICAL: Bootstrap MUST populate newTasks, productBrief, and activeDecisions (if any ADs were created). These are the only way data gets written to files.**`;
11617
- var PLAN_FULL_INSTRUCTIONS = `## FULL MODE
11618
-
11619
- Standard planning cycle with full board review.
11620
-
11621
- ### Steps:
11622
- 1. **Cycle Health Check** \u2014 Flag issues: >7 day gaps, unprocessed discovered issues, AD conflicts, stale In Progress tasks (3+ cycles).
11623
- **\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.
11624
- **\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:
11625
- - **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.
11626
- - **Cancel:** Emit a \`boardCorrections\` entry setting status to "Cancelled" with a \`closureReason\` explaining why the task is no longer worth pursuing.
11627
- - **Schedule:** Include the task in this cycle's BUILD HANDOFFs. If scheduled, no further action needed.
11628
- 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."
11629
-
11630
- 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:
11631
- - **P0 Critical** \u2014 Broken, blocking, or data-loss risk. Fix now.
11632
- - **P1 High** \u2014 Strategically aligned: directly advances the current horizon/phase goals or Active Decisions.
11633
- - **P2 Medium** \u2014 Valuable but not strategically urgent: quality improvements, efficiency, polish, infrastructure.
11634
- - **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
11635
- **\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.
11636
- 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.
11637
- **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.
11638
- If a task is clearly obsolete, duplicated, or rejected, set its status to "Cancelled" with a \`closureReason\` explaining why.
11639
- **\u2192 PERSIST:** For each task you set reviewed: true, corrected fields on, or marked "Cancelled", include it in \`boardCorrections\` in Part 2.
11640
-
11641
- 3. **Board Integrity** \u2014 All tasks have complete fields? Priority still accurate? Duplicates? Stale In Progress tasks?
11642
- **\u2192 PERSIST:** Include any field corrections (status updates, field fixes) in \`boardCorrections\` in Part 2.
11643
- **\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.
11644
- **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.
11645
-
11646
- 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:
11647
- - Data exposure risks introduced by recent builds (PII in logs, secrets in storage/config)
11648
- - Unprotected endpoints or missing auth/access control in new features
11649
- - Undocumented secrets or environment variables added without documentation
11650
- - New dependencies with known vulnerabilities or excessive permissions
11651
- **\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".
11652
-
11653
- 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.
11654
-
11655
- 6. **Maturity Gate** \u2014 Before scheduling any task, check whether the project is ready for it:
11656
- - **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.
11657
- - **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.
11658
- - **Dependency chain:** If a task's \`dependsOn\` references incomplete tasks, it cannot be scheduled regardless of priority.
11659
- - **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.
11660
- - **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).
11661
-
11662
- 7. **Recommendation** \u2014 Select tasks for this cycle:
11663
- **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.
11664
- **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).
11665
- **Otherwise, select by priority level then impact:**
11666
- - **P0 Critical** \u2014 Broken, blocking, or data-loss risk. Always first.
11667
- - **P1 High** \u2014 Strategically aligned: directly advances the current horizon, phase, or Active Decision goals.
11668
- - **P2 Medium** \u2014 Valuable but not strategically urgent: quality improvements, efficiency, polish, infra.
11669
- - **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
11670
- 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.
11671
- **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.
11672
- **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.
11673
- **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.
11674
- **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\`.
11675
- **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.
11676
- **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").
11677
-
11678
- 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).
11679
- **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.
11680
-
11681
- 9. **Active Decisions** \u2014 If any AD needs updating: Type A (confidence change), Type B (modification), or Type C (reversal/supersede).
11682
- **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.
11683
- **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.
11684
-
11685
- ${AD_REJECTION_RULES}
11686
-
11687
- **\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.
11688
-
11689
- ### Operational Quality Rules
11690
- - **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.
11691
- - **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.
11692
- - **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.
11693
-
11694
- 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.
11695
- **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.
11696
- **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.
11697
- **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.
11698
- **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.
11699
- **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.
11700
- **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:
11701
- - Populate the DEPENDS ON section in the dependent task's BUILD HANDOFF with the upstream task ID(s).
11702
- - 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.
11703
- - 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".
11704
- 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.
11705
- **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.
11706
- **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".
11707
- **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).
11708
- **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.
11709
- **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.
11710
- **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.
11711
- **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.
11712
- **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.
11713
- **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:
11714
-
11715
- RESEARCH OUTPUT
11716
- Deliverable: docs/research/[topic]-findings.md (draft path)
11717
- Review status: pending owner approval
11718
- Follow-up tasks: DO NOT submit to backlog until owner confirms findings are actionable
11719
-
11720
- Also add to ACCEPTANCE CRITERIA: "[ ] Findings doc drafted and saved to docs/research/ before submitting any follow-up ideas"
11721
-
11722
- **Bug task detection:** When a task's task type is "bug" or the title starts with "Bug:" or "Fix:", apply these rules:
11723
- - **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.
11724
- - 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."
11725
- - Add to ACCEPTANCE CRITERIA: "[ ] Fix is targeted \u2014 no unrelated code changed"
11726
-
11727
- **Idea task detection:** When a task's task type is "idea", add a scope clarification note to the BUILD HANDOFF:
11728
- - 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."
11729
-
11730
- **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.
11731
- When a task IS a UI task (primary scope is visual/frontend):
11732
- - 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."
11733
- - 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."
11734
- - 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."
11735
- - 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."
11736
- 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.
11737
-
11738
- 11. **New Tasks (max 3 per cycle)** \u2014 Actively mine the Recent Build Reports for task candidates. For each report, check:
11739
- - **Discovered Issues:** If a build report lists a discovered issue and no existing board task covers it, propose a new task.
11740
- - **Surprises:** If a surprise reveals a gap (e.g. "schema assumed but not verified"), propose a task to close it.
11741
- - **Architecture Notes:** If a pattern was established that needs follow-up (e.g. "shared service layer created, MCP migration needed"), propose the follow-up.
11742
- - **Strategy gaps:** If an Active Decision has no board tasks supporting it, propose one.
11743
- - **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.
11744
- 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.
11745
- **\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.
11746
- **\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.
11747
-
11748
- 12. **Product Brief** \u2014 Check whether the product brief still reflects reality. Update the brief when ANY of these apply:
11749
- - A new AD was created or an existing AD was superseded that changes product scope, target user, or positioning
11750
- - The North Star changed or was validated in a way that the brief doesn't reflect
11751
- - A phase completed that shifts what the product IS (not just what was built)
11752
- - The brief describes capabilities, architecture, or direction that are no longer accurate
11753
- - **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.
11754
- 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.
11755
-
11756
- 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:
11757
- - **Specific** \u2014 reference the upcoming phase by name and the architectural fork or tradeoff involved
11758
- - **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")
11759
- - **Tied to trajectory** \u2014 based on current board state, ADs, and velocity, not generic advice
11760
- 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".
11761
-
11762
- **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.**`;
11763
12019
  var PLAN_FRAGMENT_DISCOVERY_GAPS = `
11764
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.`;
11765
12021
  var PLAN_FRAGMENT_RESEARCH = `
@@ -11852,8 +12108,7 @@ var PLAN_FRAGMENT_FORWARD_HORIZON = `
11852
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")
11853
12109
  - **Tied to trajectory** \u2014 based on current board state, ADs, and velocity, not generic advice
11854
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".`;
11855
- function buildPlanFullInstructionsConditional(flags, ctx) {
11856
- if (!flags || !ctx) return PLAN_FULL_INSTRUCTIONS;
12111
+ function composeFullModeInstructions(flags, ctx) {
11857
12112
  const parts = [
11858
12113
  `## FULL MODE
11859
12114
 
@@ -11937,6 +12192,7 @@ ${AD_REJECTION_RULES}
11937
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.
11938
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.
11939
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.
11940
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.
11941
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.
11942
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.
@@ -11947,7 +12203,9 @@ ${AD_REJECTION_RULES}
11947
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.
11948
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.
11949
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.
11950
- **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.`);
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.`);
11951
12209
  if (flags.hasResearchTasks) parts.push(PLAN_FRAGMENT_RESEARCH);
11952
12210
  if (flags.hasBugTasks) parts.push(PLAN_FRAGMENT_BUG);
11953
12211
  if (flags.hasIdeaTasks) parts.push(PLAN_FRAGMENT_IDEA);
@@ -11959,6 +12217,8 @@ ${AD_REJECTION_RULES}
11959
12217
  if (flags.hasOpsBriefTasks) parts.push(PLAN_FRAGMENT_OPS_BRIEF);
11960
12218
  if (flags.hasUITasks) parts.push(PLAN_FRAGMENT_UI);
11961
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(`
11962
12222
  11. **New Tasks (max 3 per cycle)** \u2014 Actively mine the Recent Build Reports for task candidates. For each report, check:
11963
12223
  - **Discovered Issues:** If a build report lists a discovered issue and no existing board task covers it, propose a new task.
11964
12224
  - **Surprises:** If a surprise reveals a gap (e.g. "schema assumed but not verified"), propose a task to close it.
@@ -11976,6 +12236,26 @@ ${AD_REJECTION_RULES}
11976
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.**`);
11977
12237
  return parts.join("\n");
11978
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
+ }
11979
12259
  function buildPlanUserMessage(ctx) {
11980
12260
  const modeLabel = ctx.mode.toUpperCase();
11981
12261
  const parts = [
@@ -12270,6 +12550,7 @@ IMPORTANT: You are running as a non-interactive API call. Do NOT ask the user qu
12270
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.
12271
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.
12272
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}
12273
12554
 
12274
12555
  ## TWO-PHASE DELIVERY
12275
12556
 
@@ -18558,6 +18839,9 @@ async function scaffoldPapiDir(adapter2, config2, input) {
18558
18839
  } catch {
18559
18840
  }
18560
18841
  }
18842
+ if (config2.adapterType === "proxy") {
18843
+ return true;
18844
+ }
18561
18845
  const commandsDir = join5(config2.projectRoot, ".claude", "commands");
18562
18846
  const docsDir = join5(config2.projectRoot, "docs");
18563
18847
  await mkdir(commandsDir, { recursive: true });
@@ -18661,9 +18945,6 @@ async function ensurePapiPermission(projectRoot) {
18661
18945
  }
18662
18946
  async function applySetupOutputs(adapter2, config2, input, briefText, adSeedText, conventionsText) {
18663
18947
  const warnings = [];
18664
- if (config2.adapterType !== "pg") {
18665
- await writeFile2(join5(config2.papiDir, "PRODUCT_BRIEF.md"), briefText, "utf-8");
18666
- }
18667
18948
  await adapter2.updateProductBrief(briefText);
18668
18949
  const briefPhases = parsePhases(briefText);
18669
18950
  if (briefPhases.length > 0) {
@@ -18725,7 +19006,7 @@ async function applySetupOutputs(adapter2, config2, input, briefText, adSeedText
18725
19006
  }
18726
19007
  }
18727
19008
  }
18728
- if (conventionsText?.trim()) {
19009
+ if (conventionsText?.trim() && config2.adapterType !== "proxy") {
18729
19010
  try {
18730
19011
  const claudeMdPath = join5(config2.projectRoot, "CLAUDE.md");
18731
19012
  const existing = await readFile4(claudeMdPath, "utf-8");
@@ -19086,28 +19367,30 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
19086
19367
  if (msg.startsWith("100% overlap")) throw err;
19087
19368
  }
19088
19369
  }
19089
- try {
19090
- const claudeMdPath = join5(config2.projectRoot, "CLAUDE.md");
19091
- const existing = await readFile4(claudeMdPath, "utf-8");
19092
- if (!existing.includes("Dogfood Logging")) {
19093
- const dogfoodSection = [
19094
- "",
19095
- "## Dogfood Logging",
19096
- "",
19097
- "After each `release`, append a dogfood entry capturing observations from the cycle.",
19098
- "Call the adapter method with structured entries for each observation:",
19099
- "",
19100
- "- **friction** \u2014 workflow pain points, confusing flows, things that broke or slowed you down",
19101
- "- **methodology** \u2014 what worked or didn't in the plan/build/review cycle",
19102
- "- **signal** \u2014 indicators of product-market fit, user value, or growth potential",
19103
- "- **commercial** \u2014 cost, pricing, or business model observations",
19104
- "",
19105
- "This is autonomous plumbing \u2014 log observations after release without asking.",
19106
- ""
19107
- ].join("\n");
19108
- await writeFile2(claudeMdPath, existing + dogfoodSection, "utf-8");
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 {
19109
19393
  }
19110
- } catch {
19111
19394
  }
19112
19395
  if (adapter2.writeDogfoodEntries) {
19113
19396
  try {
@@ -19129,7 +19412,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
19129
19412
  });
19130
19413
  } catch {
19131
19414
  }
19132
- const gitignoreNote = await ensureMcpJsonGitignored(config2.projectRoot);
19415
+ const gitignoreNote = config2.adapterType === "proxy" ? void 0 : await ensureMcpJsonGitignored(config2.projectRoot);
19133
19416
  let cursorScaffolded = false;
19134
19417
  try {
19135
19418
  await access2(join5(config2.projectRoot, ".cursor", "rules", "papi.mdc"));
@@ -22528,7 +22811,7 @@ function getInstallId() {
22528
22811
  // src/lib/telemetry.ts
22529
22812
  var HOSTED_SUPABASE_URL2 = process.env["PAPI_HOSTED_SUPABASE_URL"] ?? "https://guewgygcpcmrcoppihzx.supabase.co";
22530
22813
  var DEFAULT_TELEMETRY_ENDPOINT = `${HOSTED_SUPABASE_URL2}/functions/v1/data-proxy`;
22531
- var MD_PINGS_SUPABASE_URL = HOSTED_SUPABASE_URL2;
22814
+ var MD_PINGS_SUPABASE_URL = process.env["PAPI_MD_PINGS_URL"] ?? HOSTED_SUPABASE_URL2;
22532
22815
  var MD_PINGS_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd1ZXdneWdjcGNtcmNvcHBpaHp4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI2Njk2NTMsImV4cCI6MjA4ODI0NTY1M30.V5Jw7wJgiMpSQPa2mt0ftjyye5ynG1qLlam00yPVNJY";
22533
22816
  function isEnabled() {
22534
22817
  const val = process.env["PAPI_TELEMETRY"];
@@ -22598,6 +22881,11 @@ function emitMilestone(projectId, milestone, extra) {
22598
22881
  metadata: extra
22599
22882
  });
22600
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
+ }
22601
22889
 
22602
22890
  // src/services/release.ts
22603
22891
  init_git();
@@ -23230,16 +23518,32 @@ var state = {
23230
23518
  lastOrientAt: null,
23231
23519
  releaseSinceLastOrient: false,
23232
23520
  sessionStartedAt: Date.now(),
23233
- lastReviewListAt: null
23521
+ lastReviewListAt: null,
23522
+ failureTimestamps: [],
23523
+ consecutiveFailures: 0
23234
23524
  };
23235
23525
  var CONTEXT_BLOAT_CALL_THRESHOLD = 40;
23236
23526
  var ORIENT_GAP_MS = 3 * 60 * 60 * 1e3;
23237
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;
23238
23531
  function recordToolCall(name) {
23239
23532
  state.toolCallCount++;
23240
23533
  if (name === "release") state.releaseSinceLastOrient = true;
23241
23534
  if (name === "review_list") state.lastReviewListAt = Date.now();
23242
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
+ }
23243
23547
  function wasReviewListSeenRecently(windowMs = REVIEW_LIST_GUARD_WINDOW_MS) {
23244
23548
  if (state.lastReviewListAt == null) return false;
23245
23549
  return Date.now() - state.lastReviewListAt <= windowMs;
@@ -23252,8 +23556,22 @@ function getProjectConnectionBanner(projectName, projectSlug) {
23252
23556
  if (!projectName || !projectSlug) return null;
23253
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.`;
23254
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
+ }
23255
23571
  async function buildSessionGuidance() {
23256
23572
  const signals = [];
23573
+ const degradation = detectContextDegradation();
23574
+ if (degradation) signals.push(degradation);
23257
23575
  if (state.toolCallCount > CONTEXT_BLOAT_CALL_THRESHOLD) {
23258
23576
  signals.push(
23259
23577
  `${state.toolCallCount} tool calls this session \u2014 context may be bloated. Consider starting a fresh window.`
@@ -25139,6 +25457,37 @@ function dedupeEnrichmentBlob(existing, blob) {
25139
25457
  return output.join("\n");
25140
25458
  }
25141
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
+
25142
25491
  // src/tools/orient.ts
25143
25492
  import { execFile } from "child_process";
25144
25493
  import { promisify } from "util";
@@ -25756,6 +26105,11 @@ async function handleOrient(adapter2, config2, args = {}) {
25756
26105
  if (proxyWarning) buildResult.warnings.push(proxyWarning);
25757
26106
  const p1Warning = p1BacklogOutcome.status === "fulfilled" ? p1BacklogOutcome.value : void 0;
25758
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
+ }
25759
26113
  const ttfvNote = ttfvOutcome.status === "fulfilled" ? ttfvOutcome.value : "";
25760
26114
  const latestTag = latestTagOutcome.status === "fulfilled" ? latestTagOutcome.value : "";
25761
26115
  const versionDrift = versionDriftOutcome.status === "fulfilled" ? versionDriftOutcome.value : void 0;
@@ -26833,6 +27187,45 @@ Use \`learning_action mark\` or submit via \`idea\` to close these out.`
26833
27187
  return errorResponse(`Unknown mode "${mode}". Use "mark" or "list".`);
26834
27188
  }
26835
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
+
26836
27229
  // src/tools/project.ts
26837
27230
  import path6 from "path";
26838
27231
  function workspacePapiDir(config2) {
@@ -27214,6 +27607,7 @@ var PAPI_TOOLS = [
27214
27607
  scopeBriefTool,
27215
27608
  adViewTool,
27216
27609
  learningActionTool,
27610
+ discoveredIssueResolveTool,
27217
27611
  projectCreateTool,
27218
27612
  projectListTool,
27219
27613
  projectSwitchTool,
@@ -27390,6 +27784,8 @@ function createServer(adapter2, config2) {
27390
27784
  return handleAdView(adapter2, safeArgs);
27391
27785
  case "learning_action":
27392
27786
  return handleLearningAction(adapter2, safeArgs);
27787
+ case "discovered_issue_resolve":
27788
+ return handleDiscoveredIssueResolve(adapter2, safeArgs);
27393
27789
  case "project_create":
27394
27790
  return handleProjectCreate(adapter2, config2, safeArgs);
27395
27791
  case "project_list":
@@ -27414,6 +27810,7 @@ function createServer(adapter2, config2) {
27414
27810
  console.error(`[papi] Exiting after '${name}' timeout (wedge-recovery #${wedgeRecoveryStats.count}); user must reconnect with /mcp before the next call.`);
27415
27811
  process.exit(2);
27416
27812
  });
27813
+ recordToolOutcome(false);
27417
27814
  return { content: [{ type: "text", text: `Error: ${msg}` }] };
27418
27815
  }
27419
27816
  throw err;
@@ -27426,8 +27823,10 @@ function createServer(adapter2, config2) {
27426
27823
  delete result._contextBytes;
27427
27824
  delete result._contextUtilisation;
27428
27825
  const isError = result.content.some((c) => c.text.startsWith("Error:") || c.text.startsWith("\u274C"));
27826
+ recordToolOutcome(!isError);
27429
27827
  try {
27430
- const metric = buildMetric(name, elapsed, usage, void 0, contextBytes, contextUtilisation);
27828
+ const clientName = server2.getClientVersion()?.name;
27829
+ const metric = buildMetric(name, elapsed, usage, void 0, contextBytes, contextUtilisation, clientName);
27431
27830
  metric.success = !isError;
27432
27831
  adapter2.appendToolMetric(metric).catch(() => {
27433
27832
  });
@@ -27437,7 +27836,7 @@ function createServer(adapter2, config2) {
27437
27836
  const mdProjectSlug = config2.projectRoot ? config2.projectRoot.split("/").pop() : void 0;
27438
27837
  emitMdAdapterPing(name, { duration_ms: elapsed, success: !isError }, config2.userId, mdProjectSlug);
27439
27838
  }
27440
- const telemetryProjectId = process.env["PAPI_PROJECT_ID"];
27839
+ const telemetryProjectId = resolveTelemetryProjectId(config2);
27441
27840
  if (telemetryProjectId) {
27442
27841
  emitToolCall(telemetryProjectId, name, elapsed, {
27443
27842
  adapter_type: config2.adapterType
@@ -27725,7 +28124,8 @@ async function dispatchRequest(args) {
27725
28124
  });
27726
28125
  const requestConfig = {
27727
28126
  ...baseConfig,
27728
- adapterType: "proxy"
28127
+ adapterType: "proxy",
28128
+ projectId: effectiveProjectId
27729
28129
  };
27730
28130
  const server2 = createServer(adapter2, requestConfig);
27731
28131
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
@@ -27764,7 +28164,7 @@ async function dispatchRequest(args) {
27764
28164
  var __dirname = dirname5(fileURLToPath3(import.meta.url));
27765
28165
  var pkgVersion = "unknown";
27766
28166
  try {
27767
- const pkg = JSON.parse(readFileSync12(join20(__dirname, "..", "package.json"), "utf-8"));
28167
+ const pkg = JSON.parse(readFileSync13(join21(__dirname, "..", "package.json"), "utf-8"));
27768
28168
  pkgVersion = pkg.version;
27769
28169
  } catch {
27770
28170
  }
@@ -27778,12 +28178,15 @@ Commands:
27778
28178
  (none) Start the MCP server (default)
27779
28179
  doctor Print a diagnostic of your PAPI environment (read-only)
27780
28180
  reset Surgically remove the papi entry from .mcp.json
28181
+ audit Cross-project harness/config audit + token-hygiene flags (read-only)
27781
28182
 
27782
28183
  Options:
27783
28184
  --help, -h Show this help message
27784
28185
  --version, -v Show version number
27785
28186
  --project <dir> Set the project directory
27786
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)
27787
28190
 
27788
28191
  Getting started:
27789
28192
  1. Sign up at https://getpapi.ai/login
@@ -27806,12 +28209,16 @@ if (cliArgs.includes("--version") || cliArgs.includes("-v")) {
27806
28209
  var subcommand = cliArgs.find((arg) => !arg.startsWith("-"));
27807
28210
  if (subcommand === "doctor") {
27808
28211
  const { runDoctor: runDoctor2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
27809
- process.exit(await runDoctor2());
28212
+ process.exit(await runDoctor2(cliArgs));
27810
28213
  }
27811
28214
  if (subcommand === "reset") {
27812
28215
  const { runReset: runReset2 } = await Promise.resolve().then(() => (init_reset(), reset_exports));
27813
28216
  process.exit(await runReset2(cliArgs));
27814
28217
  }
28218
+ if (subcommand === "audit") {
28219
+ const { runAudit: runAudit2 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
28220
+ process.exit(await runAudit2(cliArgs));
28221
+ }
27815
28222
  process.on("unhandledRejection", (err) => {
27816
28223
  console.error("[papi] unhandledRejection (swallowed):", err instanceof Error ? err.message : err);
27817
28224
  });