@papi-ai/server 0.7.65 → 0.7.67

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.
@@ -1785,6 +1785,12 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
1785
1785
  markFeedbackNotified(ids, userId) {
1786
1786
  return this.invoke("markFeedbackNotified", [ids, userId]);
1787
1787
  }
1788
+ // task-2438: the caller's own bug/idea reports. The data-proxy ignores the
1789
+ // client-supplied userId and scopes to the bearer-validated caller, so a user
1790
+ // can only ever read their OWN reports (same pattern as getUnnotifiedResolvedFeedback).
1791
+ listMyBugReports(userId) {
1792
+ return this.invoke("listMyBugReports", [userId]);
1793
+ }
1788
1794
  // --- Doc Registry ---
1789
1795
  registerDoc(entry) {
1790
1796
  return this.invoke("registerDoc", [entry]);
package/dist/index.js CHANGED
@@ -1902,6 +1902,12 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
1902
1902
  markFeedbackNotified(ids, userId) {
1903
1903
  return this.invoke("markFeedbackNotified", [ids, userId]);
1904
1904
  }
1905
+ // task-2438: the caller's own bug/idea reports. The data-proxy ignores the
1906
+ // client-supplied userId and scopes to the bearer-validated caller, so a user
1907
+ // can only ever read their OWN reports (same pattern as getUnnotifiedResolvedFeedback).
1908
+ listMyBugReports(userId) {
1909
+ return this.invoke("listMyBugReports", [userId]);
1910
+ }
1905
1911
  // --- Doc Registry ---
1906
1912
  registerDoc(entry) {
1907
1913
  return this.invoke("registerDoc", [entry]);
@@ -4334,9 +4340,9 @@ __export(doctor_exports, {
4334
4340
  __testing: () => __testing,
4335
4341
  runDoctor: () => runDoctor
4336
4342
  });
4337
- import { existsSync as existsSync11, readFileSync as readFileSync13 } from "fs";
4343
+ import { existsSync as existsSync12, readFileSync as readFileSync14 } from "fs";
4338
4344
  import { homedir as homedir4 } from "os";
4339
- import { join as join20 } from "path";
4345
+ import { join as join21 } from "path";
4340
4346
  function redact(name, value) {
4341
4347
  if (!value) return "(empty)";
4342
4348
  if (SECRET_VARS.has(name)) {
@@ -4347,14 +4353,14 @@ function redact(name, value) {
4347
4353
  }
4348
4354
  function findMcpJson() {
4349
4355
  const candidates = [
4350
- join20(process.cwd(), ".mcp.json"),
4351
- join20(homedir4(), ".claude", ".mcp.json"),
4352
- join20(homedir4(), ".mcp.json")
4356
+ join21(process.cwd(), ".mcp.json"),
4357
+ join21(homedir4(), ".claude", ".mcp.json"),
4358
+ join21(homedir4(), ".mcp.json")
4353
4359
  ];
4354
4360
  for (const path7 of candidates) {
4355
- if (!existsSync11(path7)) continue;
4361
+ if (!existsSync12(path7)) continue;
4356
4362
  try {
4357
- const raw = readFileSync13(path7, "utf-8");
4363
+ const raw = readFileSync14(path7, "utf-8");
4358
4364
  const parsed = JSON.parse(raw);
4359
4365
  const papiEntry = parsed.papi ?? parsed.mcpServers?.papi;
4360
4366
  if (!papiEntry) continue;
@@ -4632,17 +4638,17 @@ __export(reset_exports, {
4632
4638
  removePapiEntry: () => removePapiEntry,
4633
4639
  runReset: () => runReset
4634
4640
  });
4635
- import { existsSync as existsSync12, readFileSync as readFileSync14, writeFileSync as writeFileSync6 } from "fs";
4641
+ import { existsSync as existsSync13, readFileSync as readFileSync15, writeFileSync as writeFileSync7 } from "fs";
4636
4642
  import { homedir as homedir5 } from "os";
4637
- import { join as join21 } from "path";
4643
+ import { join as join22 } from "path";
4638
4644
  import { createInterface } from "readline/promises";
4639
4645
  function findResetTarget() {
4640
4646
  for (const path7 of CANDIDATE_PATHS()) {
4641
- if (!existsSync12(path7)) continue;
4647
+ if (!existsSync13(path7)) continue;
4642
4648
  let raw;
4643
4649
  let parsed;
4644
4650
  try {
4645
- raw = readFileSync14(path7, "utf-8");
4651
+ raw = readFileSync15(path7, "utf-8");
4646
4652
  parsed = JSON.parse(raw);
4647
4653
  } catch {
4648
4654
  continue;
@@ -4713,7 +4719,7 @@ async function runReset(args = []) {
4713
4719
  }
4714
4720
  }
4715
4721
  try {
4716
- writeFileSync6(target.path, removePapiEntry(target), "utf-8");
4722
+ writeFileSync7(target.path, removePapiEntry(target), "utf-8");
4717
4723
  process.stdout.write(`
4718
4724
  \u2713 Removed papi entry from ${target.path}
4719
4725
  `);
@@ -4730,9 +4736,9 @@ var init_reset = __esm({
4730
4736
  "src/cli/reset.ts"() {
4731
4737
  "use strict";
4732
4738
  CANDIDATE_PATHS = () => [
4733
- join21(process.cwd(), ".mcp.json"),
4734
- join21(homedir5(), ".claude", ".mcp.json"),
4735
- join21(homedir5(), ".mcp.json")
4739
+ join22(process.cwd(), ".mcp.json"),
4740
+ join22(homedir5(), ".claude", ".mcp.json"),
4741
+ join22(homedir5(), ".mcp.json")
4736
4742
  ];
4737
4743
  }
4738
4744
  });
@@ -4743,9 +4749,9 @@ __export(audit_exports, {
4743
4749
  __testing: () => __testing2,
4744
4750
  runAudit: () => runAudit
4745
4751
  });
4746
- import { existsSync as existsSync13, readFileSync as readFileSync15, readdirSync as readdirSync7 } from "fs";
4752
+ import { existsSync as existsSync14, readFileSync as readFileSync16, readdirSync as readdirSync7 } from "fs";
4747
4753
  import { homedir as homedir6 } from "os";
4748
- import { join as join22 } from "path";
4754
+ import { join as join23 } from "path";
4749
4755
  function safeListDirs(dir) {
4750
4756
  try {
4751
4757
  return readdirSync7(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort((a, b2) => a.localeCompare(b2));
@@ -4761,10 +4767,10 @@ function safeListFiles(dir, ext) {
4761
4767
  }
4762
4768
  }
4763
4769
  function readMcp(projectPath) {
4764
- const path7 = join22(projectPath, ".mcp.json");
4765
- if (!existsSync13(path7)) return { servers: [] };
4770
+ const path7 = join23(projectPath, ".mcp.json");
4771
+ if (!existsSync14(path7)) return { servers: [] };
4766
4772
  try {
4767
- const parsed = JSON.parse(readFileSync15(path7, "utf-8"));
4773
+ const parsed = JSON.parse(readFileSync16(path7, "utf-8"));
4768
4774
  const mcpServers = parsed.mcpServers ?? {};
4769
4775
  const servers = Object.keys(mcpServers);
4770
4776
  if (parsed.papi && !servers.includes("papi")) servers.push("papi");
@@ -4796,18 +4802,18 @@ function auditProjectSync(projectPath, name) {
4796
4802
  path: projectPath,
4797
4803
  papiProjectId,
4798
4804
  mcpServers: servers,
4799
- skills: safeListDirs(join22(projectPath, ".claude", "skills")),
4800
- agentSkills: safeListDirs(join22(projectPath, ".agents", "skills")),
4801
- agents: safeListFiles(join22(projectPath, ".claude", "agents"), ".md"),
4802
- hooks: safeListFiles(join22(projectPath, ".claude", "hooks"), ".sh")
4805
+ skills: safeListDirs(join23(projectPath, ".claude", "skills")),
4806
+ agentSkills: safeListDirs(join23(projectPath, ".agents", "skills")),
4807
+ agents: safeListFiles(join23(projectPath, ".claude", "agents"), ".md"),
4808
+ hooks: safeListFiles(join23(projectPath, ".claude", "hooks"), ".sh")
4803
4809
  };
4804
4810
  }
4805
4811
  function discoverProjects() {
4806
4812
  const out = [];
4807
4813
  for (const root of PROJECT_ROOTS) {
4808
4814
  for (const name of safeListDirs(root)) {
4809
- const path7 = join22(root, name);
4810
- if (existsSync13(join22(path7, ".mcp.json")) || existsSync13(join22(path7, ".claude"))) {
4815
+ const path7 = join23(root, name);
4816
+ if (existsSync14(join23(path7, ".mcp.json")) || existsSync14(join23(path7, ".claude"))) {
4811
4817
  out.push({ name, path: path7 });
4812
4818
  }
4813
4819
  }
@@ -4818,9 +4824,9 @@ function readGlobalSkills() {
4818
4824
  return safeListDirs(GLOBAL_SKILLS_DIR);
4819
4825
  }
4820
4826
  function readGlobalMcpServers() {
4821
- if (!existsSync13(GLOBAL_CLAUDE_JSON)) return [];
4827
+ if (!existsSync14(GLOBAL_CLAUDE_JSON)) return [];
4822
4828
  try {
4823
- const parsed = JSON.parse(readFileSync15(GLOBAL_CLAUDE_JSON, "utf-8"));
4829
+ const parsed = JSON.parse(readFileSync16(GLOBAL_CLAUDE_JSON, "utf-8"));
4824
4830
  const servers = parsed.mcpServers ?? {};
4825
4831
  return Object.keys(servers).sort((a, b2) => a.localeCompare(b2));
4826
4832
  } catch {
@@ -4972,9 +4978,9 @@ var PROJECT_ROOTS, GLOBAL_SKILLS_DIR, GLOBAL_CLAUDE_JSON, IDLE_WINDOW_DAYS, GLOB
4972
4978
  var init_audit = __esm({
4973
4979
  "src/cli/audit.ts"() {
4974
4980
  "use strict";
4975
- PROJECT_ROOTS = [join22(homedir6(), "Ai-App-Projects"), join22(homedir6(), "android-projects")];
4976
- GLOBAL_SKILLS_DIR = join22(homedir6(), ".claude", "skills");
4977
- GLOBAL_CLAUDE_JSON = join22(homedir6(), ".claude.json");
4981
+ PROJECT_ROOTS = [join23(homedir6(), "Ai-App-Projects"), join23(homedir6(), "android-projects")];
4982
+ GLOBAL_SKILLS_DIR = join23(homedir6(), ".claude", "skills");
4983
+ GLOBAL_CLAUDE_JSON = join23(homedir6(), ".claude.json");
4978
4984
  IDLE_WINDOW_DAYS = 30;
4979
4985
  GLOBALIZE_THRESHOLD = 3;
4980
4986
  __testing2 = { readMcp, computeFlags, formatReport: formatReport2, discoverProjects, auditProjectSync };
@@ -4986,8 +4992,8 @@ var setup_exports = {};
4986
4992
  __export(setup_exports, {
4987
4993
  runSetup: () => runSetup
4988
4994
  });
4989
- import { existsSync as existsSync14, readFileSync as readFileSync16, writeFileSync as writeFileSync7, chmodSync as chmodSync2, statSync as statSync8 } from "fs";
4990
- import { join as join23 } from "path";
4995
+ import { existsSync as existsSync15, readFileSync as readFileSync17, writeFileSync as writeFileSync8, chmodSync as chmodSync2, statSync as statSync8 } from "fs";
4996
+ import { join as join24 } from "path";
4991
4997
  function baseUrl() {
4992
4998
  const fromEnv = process.env["PAPI_HOST"] ?? process.env["PAPI_BASE_URL"];
4993
4999
  if (fromEnv) return fromEnv.replace(/\/$/, "");
@@ -5019,11 +5025,11 @@ function sleep(ms) {
5019
5025
  return new Promise((resolve3) => setTimeout(resolve3, ms));
5020
5026
  }
5021
5027
  function writeMcpJson(opts) {
5022
- const path7 = join23(process.cwd(), ".mcp.json");
5028
+ const path7 = join24(process.cwd(), ".mcp.json");
5023
5029
  let parsed = {};
5024
- if (existsSync14(path7)) {
5030
+ if (existsSync15(path7)) {
5025
5031
  try {
5026
- parsed = JSON.parse(readFileSync16(path7, "utf-8"));
5032
+ parsed = JSON.parse(readFileSync17(path7, "utf-8"));
5027
5033
  } catch {
5028
5034
  throw new Error(`.mcp.json at ${path7} is not valid JSON. Fix it or remove it before re-running setup.`);
5029
5035
  }
@@ -5044,7 +5050,7 @@ function writeMcpJson(opts) {
5044
5050
  }
5045
5051
  mcpServers.papi = papiEntry;
5046
5052
  parsed.mcpServers = mcpServers;
5047
- writeFileSync7(path7, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
5053
+ writeFileSync8(path7, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
5048
5054
  try {
5049
5055
  const mode = statSync8(path7).mode & 511;
5050
5056
  if (mode !== 384) chmodSync2(path7, 384);
@@ -5154,8 +5160,8 @@ var init_setup = __esm({
5154
5160
  });
5155
5161
 
5156
5162
  // src/index.ts
5157
- import { readFileSync as readFileSync17 } from "fs";
5158
- import { dirname as dirname6, join as join24, basename as basename2 } from "path";
5163
+ import { readFileSync as readFileSync18 } from "fs";
5164
+ import { dirname as dirname6, join as join25, basename as basename2 } from "path";
5159
5165
  import { fileURLToPath as fileURLToPath4 } from "url";
5160
5166
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5161
5167
  import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
@@ -8193,9 +8199,9 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
8193
8199
  }
8194
8200
 
8195
8201
  // src/server.ts
8196
- import { readFileSync as readFileSync12 } from "fs";
8202
+ import { readFileSync as readFileSync13 } from "fs";
8197
8203
  import { access as access4, readdir as readdir4, readFile as readFile9 } from "fs/promises";
8198
- import { join as join19, dirname as dirname5 } from "path";
8204
+ import { join as join20, dirname as dirname5 } from "path";
8199
8205
  import { fileURLToPath as fileURLToPath3 } from "url";
8200
8206
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8201
8207
  import {
@@ -19255,12 +19261,21 @@ ${command}
19255
19261
  \`\`\`
19256
19262
  PAPI never runs this command itself (AD-58) \u2014 you run it in your own environment. Turn the "Post-release deploy" capability off in the dashboard, or unset PAPI_DEPLOY, to stop this reminder.`;
19257
19263
  }
19264
+ function buildPapiMetaFramingDirective(caps, inner) {
19265
+ if (!isCapabilityEnabled(caps, "papiMetaFraming")) return null;
19266
+ return inner;
19267
+ }
19258
19268
 
19259
19269
  // src/services/build.ts
19260
19270
  import { randomUUID as randomUUID11 } from "crypto";
19261
19271
  import { readdirSync as readdirSync5, existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, mkdirSync as mkdirSync2 } from "fs";
19262
19272
  import { join as join11 } from "path";
19263
19273
 
19274
+ // src/lib/db-only-notices.ts
19275
+ var DB_ONLY_START_NOTICE = "No git repo detected \u2014 running this cycle in your project database only. Your work stays in the working tree; run `git init` (and add a remote) to enable branches, commits and PR review.";
19276
+ var DB_ONLY_COMPLETE_NOTICE = "No git repo \u2014 build recorded in your project database only (no commit or PR). Run `git init` (and add a remote) to enable git-backed commits and PR review.";
19277
+ var DB_ONLY_RELEASE_NOTICE = "No git repo detected \u2014 this release closed the cycle in your project database only. No tag, branch merge, or CHANGELOG was created. Run `git init` (and add a remote) to enable git-backed releases (tags, merges and changelog).";
19278
+
19264
19279
  // src/lib/harness-capability.ts
19265
19280
  var HARNESS_REGISTRY = {
19266
19281
  // Local stdio CLI agents — PAPI runs git on the user's machine. Confirmed in telemetry.
@@ -20419,6 +20434,38 @@ Run \`project_switch <slug>\` to switch the active PAPI project, or verify your
20419
20434
  if (gateDecision.action === "block") {
20420
20435
  return errorResponse(gateDecision.message);
20421
20436
  }
20437
+ if (!isGitAvailable() || !isGitRepo(config2.projectRoot)) {
20438
+ tracker.mark("db-only-close-cycle");
20439
+ let closed;
20440
+ try {
20441
+ closed = await closeCycleState(config2, adapter2, version, void 0, {
20442
+ force: force ?? false,
20443
+ callerUserId: gate.callerUserId
20444
+ });
20445
+ } catch (err) {
20446
+ return errorResponse(err instanceof Error ? err.message : String(err));
20447
+ }
20448
+ await completeRelease(tracker, {
20449
+ cycleClosed: closed.resolvedCycleNum > 0 ? closed.resolvedCycleNum : null,
20450
+ version,
20451
+ caps,
20452
+ branchMerges: [],
20453
+ changelogEmitted: false
20454
+ });
20455
+ const cyclePart = closed.resolvedCycleNum > 0 ? `Cycle ${closed.resolvedCycleNum}` : "The cycle";
20456
+ const warningsBlock = closed.warnings.length > 0 ? `
20457
+ \u26A0\uFE0F Warnings: ${closed.warnings.join("; ")}
20458
+ ` : "";
20459
+ return textResponse(
20460
+ `## Release ${version} \u2014 cycle closed (database only)
20461
+
20462
+ ${cyclePart} is now marked **complete** in PAPI, so \`orient\` will no longer flag "Release has not been run."
20463
+ ` + warningsBlock + `
20464
+ ${DB_ONLY_RELEASE_NOTICE}
20465
+
20466
+ Next: cycle closed! Run \`plan\` to start your next cycle.`
20467
+ );
20468
+ }
20422
20469
  tracker.mark("create-release");
20423
20470
  const result = await createRelease(config2, branch, version, adapter2, void 0, {
20424
20471
  force: force ?? false,
@@ -21100,8 +21147,12 @@ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
21100
21147
  throw err;
21101
21148
  }
21102
21149
  const branchLines = [];
21150
+ const startCaps = adapter2.getProjectInfo ? (await adapter2.getProjectInfo().catch(() => null))?.capabilities ?? {} : {};
21151
+ const autoBranchEnabled = isCapabilityEnabled(startCaps, "autoBranch");
21103
21152
  if (options.light) {
21104
21153
  branchLines.push("Light mode: skipping branch creation \u2014 working on current branch.");
21154
+ } else if (!autoBranchEnabled) {
21155
+ branchLines.push("Auto branch: skipped (Auto branch capability off) \u2014 working on current branch.");
21105
21156
  } else if (!hasLocalWorkspace()) {
21106
21157
  const cycleHealth = await adapter2.getCycleHealth().catch(() => null);
21107
21158
  const hosted = hostedStartSteps(clientName, taskId, task.module, cycleHealth?.totalCycles ?? 0, config2.baseBranch);
@@ -21272,6 +21323,12 @@ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
21272
21323
  }
21273
21324
  }
21274
21325
  }
21326
+ } else {
21327
+ if (!isGitAvailable() || !isGitRepo(config2.projectRoot)) {
21328
+ branchLines.push(DB_ONLY_START_NOTICE);
21329
+ } else {
21330
+ branchLines.push("Auto-commit off (PAPI_AUTO_COMMIT=false) \u2014 skipping branch creation; working on the current branch.");
21331
+ }
21275
21332
  }
21276
21333
  if (task.status !== "In Progress") {
21277
21334
  await adapter2.updateTaskStatus(taskId, "In Progress");
@@ -21408,6 +21465,9 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
21408
21465
  if (!task) {
21409
21466
  throw new Error(`Task "${taskId}" not found on the Cycle Board.`);
21410
21467
  }
21468
+ const completeCaps = adapter2.getProjectInfo ? (await adapter2.getProjectInfo().catch(() => null))?.capabilities ?? {} : {};
21469
+ const autoCommitEnabled = isCapabilityEnabled(completeCaps, "autoCommit");
21470
+ const autoPushEnabled = isCapabilityEnabled(completeCaps, "autoPush");
21411
21471
  assertDeployVerification(config2, input, { deployingNow: options.light === true });
21412
21472
  const [healthResult, priorCount] = await Promise.all([
21413
21473
  adapter2.getCycleHealth().catch(() => ({ totalCycles: 0 })),
@@ -21682,11 +21742,16 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
21682
21742
  }
21683
21743
  }
21684
21744
  const statusNote = input.completed === "yes" ? options.light ? `Task "${task.title}" (${taskId}) marked Done (light mode \u2014 no review needed).` : `Task "${task.title}" (${taskId}) marked In Review \u2014 ready for your sign-off via \`review_submit\`.` : `Task "${task.title}" (${taskId}) status unchanged (completed: ${input.completed}).`;
21745
+ const dbOnlyMode = hasLocalWorkspace() && (!isGitAvailable() || !isGitRepo(config2.projectRoot));
21685
21746
  let commitLine;
21686
- if (config2.autoCommit) {
21687
- commitLine = autoCommit(config2, taskId, task.title, task.buildHandoff?.filesLikelyTouched);
21688
- } else {
21747
+ if (!autoCommitEnabled) {
21748
+ commitLine = "Auto-commit: skipped (Auto commit capability off).";
21749
+ } else if (!config2.autoCommit) {
21689
21750
  commitLine = "Auto-commit: skipped (PAPI_AUTO_COMMIT=false).";
21751
+ } else if (dbOnlyMode) {
21752
+ commitLine = DB_ONLY_COMPLETE_NOTICE;
21753
+ } else {
21754
+ commitLine = autoCommit(config2, taskId, task.title, task.buildHandoff?.filesLikelyTouched);
21690
21755
  }
21691
21756
  if (isGitAvailable() && isGitRepo(config2.projectRoot)) {
21692
21757
  const sha = getHeadCommitSha(config2.projectRoot);
@@ -21709,6 +21774,9 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
21709
21774
  let prLines = [];
21710
21775
  if (options.light) {
21711
21776
  prLines.push("Light mode: skipping push and PR creation.");
21777
+ } else if (!autoPushEnabled) {
21778
+ prLines.push("Auto push & PR: skipped (Auto push & PR capability off).");
21779
+ } else if (dbOnlyMode) {
21712
21780
  } else if (config2.autoCommit && input.completed === "yes") {
21713
21781
  prLines = pushAndCreatePR(config2, taskId, task.title, clientName, task.module, cycleNumber);
21714
21782
  }
@@ -21830,8 +21898,10 @@ var OWNER_NAME = process.env["PAPI_OWNER"] ?? "cathalos92";
21830
21898
  var MODULE_INSTRUCTIONS = {
21831
21899
  Dashboard: `**\u26A0\uFE0F MANDATORY \u2014 Dashboard Module Rules (skip = rework)**
21832
21900
 
21833
- **STEP 0 \u2014 Dispatch the design agent for the visual layer (do NOT hand-write UI in the main build context).**
21834
- Any \`.tsx\` that renders visible UI must be built by the \`frontend-design-engineer\` subagent (dispatch via the Task tool), not inline here. It works in an isolated window loaded only with brand canon (DESIGN.md / PRODUCT.md / brand-book), runs design-critique before and after, builds via the \`frontend-design\` / \`impeccable\` skills, self-checks the falsifiable anti-slop blocklist, and verifies in-browser at populated / empty / mobile states. You remain responsible for data, routes, types, and tests \u2014 hand the visual layer to the agent and integrate the report it returns. Components hand-written in this context produce generic output that fails review. (If \`.claude/agents/frontend-design-engineer.md\` is absent \u2014 e.g. a fresh external install \u2014 run STEPs 1-4 below yourself.)
21901
+ **STEP 0 \u2014 If the frontend-design pack is installed, dispatch the design agent for the visual layer (otherwise build it yourself, STEPs 1-4).**
21902
+ This step is conditional \u2014 PAPI does NOT ship the design pack on a hosted/remote connection, so do not assume the agent exists. Check for \`.claude/agents/frontend-design-engineer.md\`:
21903
+ - **Present:** any \`.tsx\` that renders visible UI should be built by the \`frontend-design-engineer\` subagent (dispatch via the Task tool), not inline here. It works in an isolated window loaded with your project's own brand canon (your DESIGN.md / PRODUCT.md), runs design-critique before and after, builds via the \`frontend-design\` / \`impeccable\` skills, self-checks the falsifiable anti-slop blocklist, and verifies in-browser at populated / empty / mobile states. You remain responsible for data, routes, types, and tests \u2014 hand the visual layer to the agent and integrate the report it returns.
21904
+ - **Absent (default on a hosted PAPI harness, and on any project where you haven't opted in):** the pack is opt-in, not required. Install it by running \`setup\` locally on a frontend project (it detects the stack and installs the agent + design-critique skill + guard hook), or copy it from the PAPI server package under \`design-assets/\`. Until then, do NOT dispatch an agent you don't have \u2014 follow STEPs 1-4 below yourself using the generally-available \`frontend-design\` / \`impeccable\` skills.
21835
21905
 
21836
21906
  **STEP 1 \u2014 BEFORE writing any code:**
21837
21907
  If you use the \`impeccable\` skill (recommended for dashboard work), it reads two root files for design context: **PRODUCT.md** (strategic \u2014 brand, users, product purpose, design principles) and **DESIGN.md** (visual tokens \u2014 palette, typography, elevation, components). Run \`impeccable init\` to create them if they don't exist yet. Every visual decision must align with these files; if your output contradicts them, it is wrong. (The legacy \`.impeccable.md\` is no longer read by the skill.)
@@ -21906,6 +21976,7 @@ import { readdirSync as readdirSync6, existsSync as existsSync8, readFileSync as
21906
21976
  import { join as join12, relative } from "path";
21907
21977
  import { homedir as homedir3 } from "os";
21908
21978
  import { randomUUID as randomUUID12 } from "crypto";
21979
+ import { docDeletionBlockMessage } from "@papi-ai/shared";
21909
21980
  var docRegisterTool = {
21910
21981
  name: "doc_register",
21911
21982
  description: "Register or update a document in the doc registry. Called after finalising a research/planning doc, or when build_execute detects unregistered docs. Stores metadata and structured summary \u2014 not full content. Re-registering an existing doc updates its summary, tags, actions, type, and status (upsert). Visibility and owner are not changed on re-register.",
@@ -22300,9 +22371,161 @@ ${userNotes}` : referenceLine;
22300
22371
  - ${remainingNote}`
22301
22372
  );
22302
22373
  }
22374
+ var docDeleteTool = {
22375
+ name: "doc_delete",
22376
+ description: "Permanently delete a registered doc. Guarded: ALLOWED only for the doc's creator or the project owner, and only when nothing depends on it \u2014 a delete is BLOCKED if another doc supersedes it, or a task references it (doc_ref) or a linked action. The guard never cascade-deletes dependents; resolve the dependency first. Identify the doc by `doc_path` (preferred) or `doc_id`.",
22377
+ annotations: { title: "Delete Doc", readOnlyHint: false, destructiveHint: true, openWorldHint: false },
22378
+ inputSchema: {
22379
+ type: "object",
22380
+ properties: {
22381
+ doc_path: { type: "string", description: 'Path of the doc to delete (e.g. "docs/research/funding-landscape.md"). Either this or doc_id is required.' },
22382
+ doc_id: { type: "string", description: "UUID of the doc to delete. Either this or doc_path is required." }
22383
+ },
22384
+ required: []
22385
+ }
22386
+ };
22387
+ async function handleDocDelete(adapter2, config2, args) {
22388
+ if (!adapter2.deleteDoc) {
22389
+ return errorResponse("Doc deletion not available \u2014 requires the pg adapter.");
22390
+ }
22391
+ const docPath = args.doc_path?.trim();
22392
+ const docId = args.doc_id?.trim();
22393
+ if (!docPath && !docId) {
22394
+ return errorResponse("Either doc_path or doc_id is required.");
22395
+ }
22396
+ const gate = await resolveOwnerGate(adapter2, config2);
22397
+ const requesterUserId = gate.enforced ? gate.callerUserId : gate.ownerUserId ?? gate.callerUserId;
22398
+ const result = await adapter2.deleteDoc(docId ?? docPath, requesterUserId);
22399
+ if (!result.deleted) {
22400
+ const reason = result.reason ?? "not_authorized";
22401
+ return errorResponse(`Delete blocked \u2014 ${docDeletionBlockMessage(reason)}`);
22402
+ }
22403
+ return textResponse(`**Deleted:** ${docId ?? docPath}`);
22404
+ }
22405
+ var docReorderTool = {
22406
+ name: "doc_reorder",
22407
+ description: "Persist the display order of docs within this project. Pass `doc_ids` as the full desired sequence of doc UUIDs \u2014 each doc's position is saved so the dashboard renders them in that order. Ids not in this project are ignored.",
22408
+ annotations: { title: "Reorder Docs", readOnlyHint: false, destructiveHint: false, openWorldHint: false },
22409
+ inputSchema: {
22410
+ type: "object",
22411
+ properties: {
22412
+ doc_ids: {
22413
+ type: "array",
22414
+ items: { type: "string" },
22415
+ description: "Ordered list of doc UUIDs \u2014 index 0 renders first."
22416
+ }
22417
+ },
22418
+ required: ["doc_ids"]
22419
+ }
22420
+ };
22421
+ async function handleDocReorder(adapter2, args) {
22422
+ if (!adapter2.reorderDocs) {
22423
+ return errorResponse("Doc reorder not available \u2014 requires the pg adapter.");
22424
+ }
22425
+ const docIds = args.doc_ids;
22426
+ if (!Array.isArray(docIds) || docIds.some((id) => typeof id !== "string")) {
22427
+ return errorResponse("doc_ids is required and must be an array of doc UUID strings.");
22428
+ }
22429
+ if (docIds.length === 0) {
22430
+ return errorResponse("doc_ids must contain at least one doc UUID.");
22431
+ }
22432
+ await adapter2.reorderDocs(docIds);
22433
+ return textResponse(`**Reordered ${docIds.length} doc(s).** New order persisted.`);
22434
+ }
22303
22435
 
22304
22436
  // src/tools/build.ts
22305
22437
  init_git();
22438
+
22439
+ // src/lib/build-checkpoint.ts
22440
+ import { createHash as createHash4 } from "crypto";
22441
+ import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync8, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "fs";
22442
+ import { join as join13 } from "path";
22443
+ var BUILD_CHECKPOINT_VERSION = 1;
22444
+ function cwdHash(cwd) {
22445
+ return createHash4("sha256").update(realpathOrSelf(cwd)).digest("hex").slice(0, 12);
22446
+ }
22447
+ function safeTaskId(taskId) {
22448
+ return taskId.replace(/[^a-zA-Z0-9._-]/g, "_");
22449
+ }
22450
+ function checkpointDir(cwd) {
22451
+ return join13(cwd, ".papi", "state");
22452
+ }
22453
+ function checkpointPath(cwd, taskId) {
22454
+ return join13(checkpointDir(cwd), `build-${safeTaskId(taskId)}.${cwdHash(cwd)}.json`);
22455
+ }
22456
+ function writeBuildCheckpoint(input) {
22457
+ try {
22458
+ const dir = checkpointDir(input.cwd);
22459
+ if (!existsSync9(dir)) {
22460
+ mkdirSync3(dir, { recursive: true });
22461
+ }
22462
+ const checkpoint = {
22463
+ version: BUILD_CHECKPOINT_VERSION,
22464
+ taskId: input.taskId,
22465
+ branch: input.branch,
22466
+ step: input.step,
22467
+ lastCommitSha: input.lastCommitSha,
22468
+ modifiedFiles: input.modifiedFiles,
22469
+ cwd: realpathOrSelf(input.cwd),
22470
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
22471
+ };
22472
+ writeFileSync4(checkpointPath(input.cwd, input.taskId), JSON.stringify(checkpoint, null, 2) + "\n", "utf-8");
22473
+ } catch {
22474
+ }
22475
+ }
22476
+ function readBuildCheckpoint(key) {
22477
+ try {
22478
+ const path7 = checkpointPath(key.cwd, key.taskId);
22479
+ if (!existsSync9(path7)) return null;
22480
+ const parsed = JSON.parse(readFileSync8(path7, "utf-8"));
22481
+ if (!parsed || parsed.version !== BUILD_CHECKPOINT_VERSION || parsed.taskId !== key.taskId) {
22482
+ return null;
22483
+ }
22484
+ return {
22485
+ version: BUILD_CHECKPOINT_VERSION,
22486
+ taskId: parsed.taskId,
22487
+ branch: parsed.branch ?? null,
22488
+ step: "branch_ready",
22489
+ lastCommitSha: parsed.lastCommitSha ?? null,
22490
+ modifiedFiles: Array.isArray(parsed.modifiedFiles) ? parsed.modifiedFiles : [],
22491
+ cwd: typeof parsed.cwd === "string" ? parsed.cwd : realpathOrSelf(key.cwd),
22492
+ updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : ""
22493
+ };
22494
+ } catch {
22495
+ return null;
22496
+ }
22497
+ }
22498
+ function clearBuildCheckpoint(key) {
22499
+ try {
22500
+ const path7 = checkpointPath(key.cwd, key.taskId);
22501
+ if (existsSync9(path7)) {
22502
+ unlinkSync3(path7);
22503
+ }
22504
+ } catch {
22505
+ }
22506
+ }
22507
+ function formatResumeNote(cp) {
22508
+ const files = cp.modifiedFiles.filter((f) => f && f.trim());
22509
+ const fileList = files.length > 0 ? files.slice(0, 12).map((f) => ` - ${f}`).join("\n") + (files.length > 12 ? `
22510
+ - \u2026+${files.length - 12} more` : "") : " _(none recorded)_";
22511
+ const lines = [
22512
+ "> **\u21BB Resuming from checkpoint** \u2014 this task is already In Progress; you are NOT starting clean.",
22513
+ `> - Branch: \`${cp.branch ?? "unknown"}\``,
22514
+ "> - Last step: branch ready",
22515
+ `> - Last commit: ${cp.lastCommitSha ? cp.lastCommitSha.slice(0, 8) : "none"}`,
22516
+ `> - Checkpoint saved: ${cp.updatedAt || "unknown"}`,
22517
+ ">",
22518
+ "> Modified files at last checkpoint:",
22519
+ ...fileList.split("\n").map((l) => `> ${l}`),
22520
+ ">",
22521
+ "> Review the existing branch and changes above before writing new code \u2014 do not re-do work already committed.",
22522
+ "",
22523
+ ""
22524
+ ];
22525
+ return lines.join("\n");
22526
+ }
22527
+
22528
+ // src/tools/build.ts
22306
22529
  var buildListTool = {
22307
22530
  name: "build_list",
22308
22531
  description: "List cycle tasks that have BUILD HANDOFFs ready for execution. Shows task ID, title, status, priority, and complexity. In Progress tasks appear first, then Backlog. Does not call the Anthropic API.",
@@ -22640,10 +22863,23 @@ async function handleBuildExecute(adapter2, config2, args, clientName) {
22640
22863
  if (scopeTask) {
22641
22864
  tracker.setStreamScope({ taskId: scopeTask.displayId ?? scopeTask.id, cycle: scopeTask.cycle ?? null });
22642
22865
  }
22866
+ let resumeNote = "";
22867
+ if (scopeTask?.status === "In Progress") {
22868
+ const existing = readBuildCheckpoint({ cwd: config2.projectRoot, taskId });
22869
+ if (existing) resumeNote = formatResumeNote(existing);
22870
+ }
22643
22871
  await tracker.recordStep("started");
22644
22872
  const result = await startBuild(adapter2, config2, taskId, { light }, clientName);
22645
22873
  tracker.setStreamScope({ taskId: result.task.displayId ?? result.task.id, cycle: result.task.cycle ?? null });
22646
22874
  await tracker.recordStep("branch_ready");
22875
+ writeBuildCheckpoint({
22876
+ cwd: config2.projectRoot,
22877
+ taskId,
22878
+ branch: getCurrentBranch(config2.projectRoot),
22879
+ step: "branch_ready",
22880
+ lastCommitSha: getHeadCommitSha(config2.projectRoot),
22881
+ modifiedFiles: getModifiedFiles(config2.projectRoot)
22882
+ });
22647
22883
  tracker.mark("start_decorate_handoff");
22648
22884
  const branchInfo = result.branchLines.length > 0 ? result.branchLines.map((l) => `> ${l}`).join("\n") + "\n\n" : "";
22649
22885
  const phaseNote = result.phaseChanges.length > 0 ? "\n\n" + result.phaseChanges.map((c) => `Phase auto-updated: ${c.phaseId} ${c.oldStatus} \u2192 ${c.newStatus}`).join("\n") : "";
@@ -22723,7 +22959,8 @@ If this build fixes any of these, pass their UUIDs in \`fixed_issues\` on comple
22723
22959
  formatModelRecommendation(result.task.buildHandoff?.effort ?? result.task.complexity)
22724
22960
  ) ?? "";
22725
22961
  const gestaltNote = buildGestaltPreBuildDirective(caps) ?? "";
22726
- return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + modelNote + gestaltNote + adSection + moduleInstructions + moduleContext + dogfoodSection + openIssuesSection + verificationNote + buildDisciplineNote + chainInstruction + phaseNote + filesToWriteSection);
22962
+ const buildDisciplineSection = buildPapiMetaFramingDirective(caps, buildDisciplineNote) ?? "";
22963
+ return textResponse(resumeNote + header + serializeBuildHandoff(result.task.buildHandoff) + modelNote + gestaltNote + adSection + moduleInstructions + moduleContext + dogfoodSection + openIssuesSection + verificationNote + buildDisciplineSection + chainInstruction + phaseNote + filesToWriteSection);
22727
22964
  } catch (err) {
22728
22965
  if (isNoHandoffError(err)) {
22729
22966
  const lines = [
@@ -22861,6 +23098,7 @@ Your report was NOT discarded and the task is NOT yet Done \u2014 re-send with \
22861
23098
  preview
22862
23099
  }, { light }, clientName);
22863
23100
  tracker.mark("complete_format");
23101
+ clearBuildCheckpoint({ cwd: config2.projectRoot, taskId });
22864
23102
  tracker.setStreamScope({ taskId: result.task.displayId ?? result.task.id, cycle: result.cycleNumber });
22865
23103
  await tracker.recordStep("report_written");
22866
23104
  if ((result.autoTriagedCount ?? 0) > 0) {
@@ -23461,6 +23699,43 @@ This ${type} is visible to PAPI maintainers.${followUpLine}${autoRouteLine}`
23461
23699
  const truncateWarning = notesTruncated ? ` (notes truncated to ${MAX_NOTES_LENGTH} chars)` : "";
23462
23700
  return textResponse(`\u{1F41B} ${task.id}: "${task.title}" \u2014 ${severityLabel} bug added to backlog${overrideNote}${branchNote}. Will be picked up by next plan.${truncateWarning}`);
23463
23701
  }
23702
+ var bugListTool = {
23703
+ name: "bug_list",
23704
+ description: "List the upstream bug/idea reports YOU filed to the PAPI maintainers (via the `bug` tool with report=true or an auto-routed PAPI bug), newest first. Read-only. Shows each report's id, kind (bug/idea), triage status, description, and when it was filed. Scoped to your own submissions only \u2014 never another user's. Requires a database adapter (pg or hosted proxy); the local md adapter has no upstream store. Does not call the Anthropic API.",
23705
+ annotations: { title: "List My Bug Reports", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
23706
+ inputSchema: {
23707
+ type: "object",
23708
+ properties: {
23709
+ limit: {
23710
+ type: "integer",
23711
+ description: "Optional maximum number of reports to return. Omit to return all. Reserved for future pagination \u2014 current behaviour returns all your reports newest-first regardless of value."
23712
+ }
23713
+ },
23714
+ required: []
23715
+ }
23716
+ };
23717
+ async function handleBugList(adapter2, config2) {
23718
+ if (!adapter2.listMyBugReports) {
23719
+ return errorResponse(
23720
+ "Listing your bug reports requires a database adapter (pg or hosted proxy). The md adapter has no upstream report store."
23721
+ );
23722
+ }
23723
+ const reports = await adapter2.listMyBugReports(config2.userId);
23724
+ if (reports.length === 0) {
23725
+ return textResponse("You haven't filed any bug or idea reports yet. Use the `bug` tool to report a PAPI bug or suggest an idea.");
23726
+ }
23727
+ const lines = reports.map((r) => {
23728
+ const kind = r.type === "idea" ? "\u{1F4A1} idea" : "\u{1F41B} bug";
23729
+ const when = r.createdAt.slice(0, 10);
23730
+ return `- \`${r.id}\` \xB7 ${kind} \xB7 ${r.status} \xB7 ${when}
23731
+ ${r.description}`;
23732
+ });
23733
+ return textResponse(
23734
+ `**Your reports (${reports.length}, newest first)**
23735
+
23736
+ ${lines.join("\n")}`
23737
+ );
23738
+ }
23464
23739
 
23465
23740
  // src/tools/ad-hoc.ts
23466
23741
  init_git();
@@ -23707,13 +23982,13 @@ _To correct: board_edit ${result.task.id} with updated fields._`
23707
23982
  init_git();
23708
23983
 
23709
23984
  // src/services/reconcile.ts
23710
- import { readFileSync as readFileSync8 } from "fs";
23711
- import { join as join13 } from "path";
23985
+ import { readFileSync as readFileSync9 } from "fs";
23986
+ import { join as join14 } from "path";
23712
23987
  function loadDocsIndex(projectRoot) {
23713
23988
  if (!hasLocalWorkspace()) return "";
23714
23989
  try {
23715
- const indexPath = join13(projectRoot, "docs", "INDEX.md");
23716
- const raw = readFileSync8(indexPath, "utf8");
23990
+ const indexPath = join14(projectRoot, "docs", "INDEX.md");
23991
+ const raw = readFileSync9(indexPath, "utf8");
23717
23992
  const rows = raw.split("\n").filter((l) => l.startsWith("| ["));
23718
23993
  if (rows.length === 0) return "";
23719
23994
  const entries = rows.map((row) => {
@@ -24288,8 +24563,8 @@ Produce your analysis and structured output above. Present Part 1 to the user an
24288
24563
  }
24289
24564
 
24290
24565
  // src/tools/review.ts
24291
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
24292
- import { join as join14 } from "path";
24566
+ import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
24567
+ import { join as join15 } from "path";
24293
24568
  init_git();
24294
24569
 
24295
24570
  // src/services/review.ts
@@ -24518,11 +24793,11 @@ ${task.buildReport}` : "### Build Report\n(none recorded)";
24518
24793
  ${diff}
24519
24794
  \`\`\`` : "### Branch diff vs base\n(no diff resolved \u2014 not a git repo, no base ref, or no committed changes)";
24520
24795
  let projectContext = "";
24521
- const ctxPath = join14(config2.projectRoot, ".agents", "papi-context.md");
24522
- if (existsSync9(ctxPath)) {
24796
+ const ctxPath = join15(config2.projectRoot, ".agents", "papi-context.md");
24797
+ if (existsSync10(ctxPath)) {
24523
24798
  try {
24524
24799
  projectContext = `### Project context (.agents/papi-context.md)
24525
- ${readFileSync9(ctxPath, "utf-8")}
24800
+ ${readFileSync10(ctxPath, "utf-8")}
24526
24801
 
24527
24802
  `;
24528
24803
  } catch {
@@ -24682,8 +24957,8 @@ function mergeAfterAccept(config2, taskId) {
24682
24957
  };
24683
24958
  }
24684
24959
  const details = [];
24685
- const papiDir = join14(config2.projectRoot, ".papi");
24686
- if (existsSync9(papiDir)) {
24960
+ const papiDir = join15(config2.projectRoot, ".papi");
24961
+ if (existsSync10(papiDir)) {
24687
24962
  try {
24688
24963
  const commitResult = stageDirAndCommit(
24689
24964
  config2.projectRoot,
@@ -26265,7 +26540,7 @@ function formatDeferredGateSection(sweep) {
26265
26540
 
26266
26541
  // src/tools/agent-list.ts
26267
26542
  import { readdir as readdir2, readFile as readFile7 } from "fs/promises";
26268
- import { join as join15 } from "path";
26543
+ import { join as join16 } from "path";
26269
26544
  var NO_AGENTS_HINT = "No project sub-agents found in `.claude/agents/`. Add a `*.md` file with `name` + `description` frontmatter \u2014 see the 1926-Census marketing sub-agent for a reference implementation.";
26270
26545
  function parseAgentFrontmatter(content) {
26271
26546
  const match = content.match(/^---\n([\s\S]*?)\n---/);
@@ -26277,7 +26552,7 @@ function parseAgentFrontmatter(content) {
26277
26552
  return { name: nameMatch?.[1].trim(), description };
26278
26553
  }
26279
26554
  async function listAgents(projectRoot) {
26280
- const agentsDir = join15(projectRoot, ".claude", "agents");
26555
+ const agentsDir = join16(projectRoot, ".claude", "agents");
26281
26556
  let files;
26282
26557
  try {
26283
26558
  files = await readdir2(agentsDir);
@@ -26288,7 +26563,7 @@ async function listAgents(projectRoot) {
26288
26563
  for (const file of files.filter((f) => f.endsWith(".md"))) {
26289
26564
  let content;
26290
26565
  try {
26291
- content = await readFile7(join15(agentsDir, file), "utf-8");
26566
+ content = await readFile7(join16(agentsDir, file), "utf-8");
26292
26567
  } catch {
26293
26568
  continue;
26294
26569
  }
@@ -26296,7 +26571,7 @@ async function listAgents(projectRoot) {
26296
26571
  agents.push({
26297
26572
  name: meta?.name ?? file.replace(/\.md$/, ""),
26298
26573
  description: meta?.description ?? "",
26299
- path: join15(".claude", "agents", file)
26574
+ path: join16(".claude", "agents", file)
26300
26575
  });
26301
26576
  }
26302
26577
  agents.sort((a, b2) => a.name.localeCompare(b2.name));
@@ -26437,8 +26712,8 @@ async function verifyProject(adapter2) {
26437
26712
  // src/tools/orient.ts
26438
26713
  import { execFile as execFile2 } from "child_process";
26439
26714
  import { promisify as promisify2 } from "util";
26440
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync4, existsSync as existsSync10 } from "fs";
26441
- import { join as join16 } from "path";
26715
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync5, existsSync as existsSync11 } from "fs";
26716
+ import { join as join17 } from "path";
26442
26717
  var execFileAsync2 = promisify2(execFile2);
26443
26718
  var GIT_DEPENDENT_ENVS = /* @__PURE__ */ new Set(["hosted", "api"]);
26444
26719
  var VALID_ENVS = /* @__PURE__ */ new Set(["local-cli", "hosted", "api", "unknown"]);
@@ -26797,8 +27072,8 @@ async function getLatestGitTag(projectRoot) {
26797
27072
  }
26798
27073
  async function checkNpmVersionDrift() {
26799
27074
  try {
26800
- const pkgPath = join16(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
26801
- const pkg = JSON.parse(readFileSync10(pkgPath, "utf-8"));
27075
+ const pkgPath = join17(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
27076
+ const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
26802
27077
  const localVersion = pkg.version;
26803
27078
  const packageName = pkg.name;
26804
27079
  const { stdout } = await execFileAsync2("npm", ["view", packageName, "version"], {
@@ -27472,9 +27747,9 @@ function enrichClaudeMd(projectRoot, cycleNumber, adapterType, collector, client
27472
27747
 
27473
27748
  \u{1F4DD} **CLAUDE.md enriched** \u2014 added ${tierNames2.join(" + ")} guidance for cycle ${cycleNumber}+ projects.`;
27474
27749
  }
27475
- const claudeMdPath = join16(projectRoot, "CLAUDE.md");
27476
- if (!existsSync10(claudeMdPath)) return "";
27477
- const content = readFileSync10(claudeMdPath, "utf-8");
27750
+ const claudeMdPath = join17(projectRoot, "CLAUDE.md");
27751
+ if (!existsSync11(claudeMdPath)) return "";
27752
+ const content = readFileSync11(claudeMdPath, "utf-8");
27478
27753
  const additions = [];
27479
27754
  if (cycleNumber >= 6 && !content.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1)) {
27480
27755
  additions.push(dedupeEnrichmentBlob(content, CLAUDE_MD_TIER_1));
@@ -27483,7 +27758,7 @@ function enrichClaudeMd(projectRoot, cycleNumber, adapterType, collector, client
27483
27758
  additions.push(dedupeEnrichmentBlob(content, CLAUDE_MD_TIER_2));
27484
27759
  }
27485
27760
  if (additions.length === 0) return "";
27486
- writeFileSync4(claudeMdPath, content + additions.join(""), "utf-8");
27761
+ writeFileSync5(claudeMdPath, content + additions.join(""), "utf-8");
27487
27762
  const tierNames = [];
27488
27763
  if (additions.some((a) => a.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1))) tierNames.push("Established (batch building, strategy reviews, AD lifecycle)");
27489
27764
  if (additions.some((a) => a.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T2))) tierNames.push("Mature (idea pipeline, doc registry, advanced patterns)");
@@ -28206,11 +28481,11 @@ ${result.userMessage}
28206
28481
  }
28207
28482
 
28208
28483
  // src/tools/scope-brief.ts
28209
- import { readFileSync as readFileSync11, statSync as statSync7 } from "fs";
28484
+ import { readFileSync as readFileSync12, statSync as statSync7 } from "fs";
28210
28485
 
28211
28486
  // src/services/scope-brief.ts
28212
- import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync3 } from "fs";
28213
- import { join as join17, dirname as dirname4 } from "path";
28487
+ import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync4 } from "fs";
28488
+ import { join as join18, dirname as dirname4 } from "path";
28214
28489
  var SCOPE_BRIEF_SYSTEM = `You are a technical scoping tool. You receive a brief-class task (too large to build directly) and decompose it into a structured scope document.
28215
28490
 
28216
28491
  A scope document must:
@@ -28267,14 +28542,14 @@ async function applyScopeBrief(adapter2, input) {
28267
28542
  }
28268
28543
  const slug = input.taskId.replace(/[^a-z0-9-]/g, "-").toLowerCase();
28269
28544
  const relPath = `docs/scopes/${slug}.md`;
28270
- const absPath = join17(input.projectRoot, relPath);
28545
+ const absPath = join18(input.projectRoot, relPath);
28271
28546
  const docBody = addFrontmatter(docContent, task, input.cycleNumber);
28272
28547
  const collector = new FileWriteCollector();
28273
28548
  if (input.adapterType === "proxy") {
28274
28549
  collector.add({ path: relPath, content: docBody, mode: "overwrite" });
28275
28550
  } else {
28276
- mkdirSync3(dirname4(absPath), { recursive: true });
28277
- writeFileSync5(absPath, docBody, "utf-8");
28551
+ mkdirSync4(dirname4(absPath), { recursive: true });
28552
+ writeFileSync6(absPath, docBody, "utf-8");
28278
28553
  }
28279
28554
  const taskCount = countSubTasks(docContent);
28280
28555
  const summary = buildSummary(task, taskCount);
@@ -28445,7 +28720,7 @@ function readLlmResponse(args) {
28445
28720
  if (statSync7(filePath).size > MAX_RESPONSE_FILE_BYTES) {
28446
28721
  throw new Error(`llm_response_file exceeds ${MAX_RESPONSE_FILE_BYTES} bytes`);
28447
28722
  }
28448
- const body = readFileSync11(filePath, "utf-8");
28723
+ const body = readFileSync12(filePath, "utf-8");
28449
28724
  return body.trim() ? body : null;
28450
28725
  } catch (err) {
28451
28726
  throw new Error(`could not read llm_response_file: ${err instanceof Error ? err.message : String(err)}`);
@@ -28640,6 +28915,7 @@ async function handleDiscoveredIssueResolve(adapter2, args) {
28640
28915
  import path6 from "path";
28641
28916
 
28642
28917
  // src/services/entitlements.ts
28918
+ import { evaluateContributorGate } from "@papi-ai/shared";
28643
28919
  var FREE_PROJECT_CAP = 3;
28644
28920
  var PAID_TIERS = /* @__PURE__ */ new Set(["pro", "team"]);
28645
28921
  var PRICING_URL = "https://getpapi.ai/pricing";
@@ -28672,11 +28948,9 @@ async function enforceProjectCap(adapter2, target) {
28672
28948
  if (projects.length >= FREE_PROJECT_CAP) return projectCapMessage(projects.length);
28673
28949
  return null;
28674
28950
  }
28675
- async function enforceContributorGate(adapter2) {
28951
+ async function resolveContributorUpsell(adapter2) {
28676
28952
  const tier = await resolveTier(adapter2);
28677
- if (tier === null) return null;
28678
- if (tier === "team") return null;
28679
- return contributorTeamMessage(tier);
28953
+ return evaluateContributorGate(tier).upsell ?? null;
28680
28954
  }
28681
28955
  function projectCapMessage(currentCount) {
28682
28956
  return `**You're on the Free plan (${currentCount} of ${FREE_PROJECT_CAP} projects).**
@@ -28685,11 +28959,6 @@ Free covers up to ${FREE_PROJECT_CAP} projects. To run more, upgrade to Pro for
28685
28959
 
28686
28960
  Your existing projects are untouched, and you can keep working in any of them.`;
28687
28961
  }
28688
- function contributorTeamMessage(tier) {
28689
- return `**Adding people to a project is a Team feature.**
28690
-
28691
- You're on the ${tier} plan. Shared projects, roles, and the Quality Gate come with Team. Read-only viewer seats are free, so you only pay for who is actually building: ${PRICING_URL}`;
28692
- }
28693
28962
 
28694
28963
  // src/tools/project.ts
28695
28964
  function workspacePapiDir(config2) {
@@ -28883,17 +29152,19 @@ function requireEmail(args) {
28883
29152
  async function handleContributorAdd(adapter2, config2, args) {
28884
29153
  const denied = await denyUnlessOwner(adapter2, config2);
28885
29154
  if (denied) return errorResponse(denied);
28886
- const tierDenied = await enforceContributorGate(adapter2);
28887
- if (tierDenied) return errorResponse(tierDenied);
28888
29155
  const email = requireEmail(args);
28889
29156
  if (!email) return errorResponse('A valid email is required. Example: contributor_add email="wes@example.com"');
28890
29157
  try {
28891
29158
  const entry = await adapter2.addContributorByEmail(email);
28892
29159
  const name = entry.displayName ? ` (${entry.displayName})` : "";
29160
+ const upsell = await resolveContributorUpsell(adapter2);
29161
+ const upsellSuffix = upsell ? `
29162
+
29163
+ ${upsell}` : "";
28893
29164
  return textResponse(
28894
29165
  `\u2705 Added **${entry.email ?? email}**${name} as a contributor.
28895
29166
 
28896
- They now have contributors-tier visibility on this project. Roles and invites land with MU-2 \u2014 today membership is the whole model.`
29167
+ They now have contributors-tier visibility on this project. Roles and invites land with MU-2 \u2014 today membership is the whole model.` + upsellSuffix
28897
29168
  );
28898
29169
  } catch (err) {
28899
29170
  return errorResponse(err instanceof Error ? err.message : String(err));
@@ -29158,19 +29429,19 @@ Its build reports, comments, and history moved with it; the cycle assignment was
29158
29429
 
29159
29430
  // src/services/harness-inventory.ts
29160
29431
  import { readdir as readdir3, readFile as readFile8, stat as stat3 } from "fs/promises";
29161
- import { join as join18 } from "path";
29162
- import { createHash as createHash4 } from "crypto";
29432
+ import { join as join19 } from "path";
29433
+ import { createHash as createHash5 } from "crypto";
29163
29434
  var RECOMMENDED_HOOKS = ["stop-release-check.sh", "claude-md-size-guard.sh"];
29164
29435
  async function computeFingerprint(root) {
29165
29436
  const parts = [];
29166
29437
  for (const sub of [".claude/skills", ".claude/agents", ".claude/hooks"]) {
29167
- const dir = join18(root, sub);
29438
+ const dir = join19(root, sub);
29168
29439
  try {
29169
29440
  const names = (await readdir3(dir)).sort((a, b2) => a.localeCompare(b2));
29170
29441
  for (const name of names) {
29171
29442
  let mtime = "";
29172
29443
  try {
29173
- mtime = String(Math.floor((await stat3(join18(dir, name))).mtimeMs));
29444
+ mtime = String(Math.floor((await stat3(join19(dir, name))).mtimeMs));
29174
29445
  } catch {
29175
29446
  }
29176
29447
  parts.push(`${sub}/${name}:${mtime}`);
@@ -29185,11 +29456,11 @@ async function computeFingerprint(root) {
29185
29456
  } catch {
29186
29457
  parts.push("manifest:none");
29187
29458
  }
29188
- return createHash4("sha256").update(parts.join("|")).digest("hex").slice(0, 16);
29459
+ return createHash5("sha256").update(parts.join("|")).digest("hex").slice(0, 16);
29189
29460
  }
29190
29461
  async function readSkillDescription(skillDir) {
29191
29462
  try {
29192
- const content = await readFile8(join18(skillDir, "SKILL.md"), "utf-8");
29463
+ const content = await readFile8(join19(skillDir, "SKILL.md"), "utf-8");
29193
29464
  const fm = content.match(/^---\n([\s\S]*?)\n---/);
29194
29465
  if (!fm) return void 0;
29195
29466
  const desc = fm[1].match(/^description:\s*[>|]?\s*\n?([\s\S]*?)(?=\n\w+:|\n---|$)/m);
@@ -29209,17 +29480,17 @@ async function scanInventory(root, toolDefs) {
29209
29480
  version = loadManifest().packageVersion;
29210
29481
  } catch {
29211
29482
  }
29212
- const skillsDir = join18(root, ".claude", "skills");
29483
+ const skillsDir = join19(root, ".claude", "skills");
29213
29484
  try {
29214
29485
  const dirents = await readdir3(skillsDir, { withFileTypes: true });
29215
29486
  for (const d of dirents.filter((e) => e.isDirectory())) {
29216
29487
  entries.push({
29217
29488
  kind: "skill",
29218
29489
  name: d.name,
29219
- description: await readSkillDescription(join18(skillsDir, d.name)),
29490
+ description: await readSkillDescription(join19(skillsDir, d.name)),
29220
29491
  version,
29221
29492
  status: stale.has(d.name) ? "stale_fork" : "ok",
29222
- path: join18(".claude", "skills", d.name)
29493
+ path: join19(".claude", "skills", d.name)
29223
29494
  });
29224
29495
  }
29225
29496
  } catch {
@@ -29235,9 +29506,9 @@ async function scanInventory(root, toolDefs) {
29235
29506
  }
29236
29507
  const present = /* @__PURE__ */ new Set();
29237
29508
  try {
29238
- for (const f of (await readdir3(join18(root, ".claude", "hooks"))).filter((n) => n.endsWith(".sh"))) {
29509
+ for (const f of (await readdir3(join19(root, ".claude", "hooks"))).filter((n) => n.endsWith(".sh"))) {
29239
29510
  present.add(f);
29240
- entries.push({ kind: "hook", name: f, status: "ok", path: join18(".claude", "hooks", f) });
29511
+ entries.push({ kind: "hook", name: f, status: "ok", path: join19(".claude", "hooks", f) });
29241
29512
  }
29242
29513
  } catch {
29243
29514
  }
@@ -29636,6 +29907,7 @@ var PAPI_TOOLS = [
29636
29907
  buildCancelTool,
29637
29908
  ideaTool,
29638
29909
  bugTool,
29910
+ bugListTool,
29639
29911
  adHocTool,
29640
29912
  boardReconcileTool,
29641
29913
  releaseTool,
@@ -29651,6 +29923,8 @@ var PAPI_TOOLS = [
29651
29923
  docSearchTool,
29652
29924
  docScanTool,
29653
29925
  docActionPromoteTool,
29926
+ docDeleteTool,
29927
+ docReorderTool,
29654
29928
  getSiblingAdsTool,
29655
29929
  handoffGenerateTool,
29656
29930
  scopeBriefTool,
@@ -29677,7 +29951,7 @@ function createServer(adapter2, config2) {
29677
29951
  const __pkgDir = dirname5(__pkgFilename);
29678
29952
  let serverVersion = "unknown";
29679
29953
  try {
29680
- const pkg = JSON.parse(readFileSync12(join19(__pkgDir, "..", "package.json"), "utf-8"));
29954
+ const pkg = JSON.parse(readFileSync13(join20(__pkgDir, "..", "package.json"), "utf-8"));
29681
29955
  serverVersion = pkg.version ?? "unknown";
29682
29956
  } catch {
29683
29957
  }
@@ -29695,7 +29969,7 @@ function createServer(adapter2, config2) {
29695
29969
  }
29696
29970
  const __filename = fileURLToPath3(import.meta.url);
29697
29971
  const __dirname2 = dirname5(__filename);
29698
- const skillsDir = join19(__dirname2, "..", "skills");
29972
+ const skillsDir = join20(__dirname2, "..", "skills");
29699
29973
  function parseSkillFrontmatter(content) {
29700
29974
  const match = content.match(/^---\n([\s\S]*?)\n---/);
29701
29975
  if (!match) return null;
@@ -29713,7 +29987,7 @@ function createServer(adapter2, config2) {
29713
29987
  const mdFiles = files.filter((f) => f.endsWith(".md"));
29714
29988
  const prompts = [];
29715
29989
  for (const file of mdFiles) {
29716
- const content = await readFile9(join19(skillsDir, file), "utf-8");
29990
+ const content = await readFile9(join20(skillsDir, file), "utf-8");
29717
29991
  const meta = parseSkillFrontmatter(content);
29718
29992
  if (meta) {
29719
29993
  prompts.push({ name: meta.name, description: meta.description });
@@ -29729,7 +30003,7 @@ function createServer(adapter2, config2) {
29729
30003
  try {
29730
30004
  const files = await readdir4(skillsDir);
29731
30005
  for (const file of files.filter((f) => f.endsWith(".md"))) {
29732
- const content = await readFile9(join19(skillsDir, file), "utf-8");
30006
+ const content = await readFile9(join20(skillsDir, file), "utf-8");
29733
30007
  const meta = parseSkillFrontmatter(content);
29734
30008
  if (meta?.name === name) {
29735
30009
  const body = content.replace(/^---\n[\s\S]*?\n---\n*/, "");
@@ -29816,6 +30090,8 @@ function createServer(adapter2, config2) {
29816
30090
  return handleIdea(adapter2, config2, safeArgs);
29817
30091
  case "bug":
29818
30092
  return handleBug(adapter2, config2, safeArgs);
30093
+ case "bug_list":
30094
+ return handleBugList(adapter2, config2);
29819
30095
  case "ad_hoc":
29820
30096
  return handleAdHoc(adapter2, config2, safeArgs);
29821
30097
  case "board_reconcile":
@@ -29847,6 +30123,10 @@ function createServer(adapter2, config2) {
29847
30123
  return handleDocScan(adapter2, config2, safeArgs);
29848
30124
  case "doc_action_promote":
29849
30125
  return handleDocActionPromote(adapter2, safeArgs);
30126
+ case "doc_delete":
30127
+ return handleDocDelete(adapter2, config2, safeArgs);
30128
+ case "doc_reorder":
30129
+ return handleDocReorder(adapter2, safeArgs);
29850
30130
  case "get_sibling_ads":
29851
30131
  return handleGetSiblingAds(adapter2, safeArgs);
29852
30132
  case "handoff_generate":
@@ -30423,7 +30703,7 @@ async function dispatchRequest(args) {
30423
30703
  var __dirname = dirname6(fileURLToPath4(import.meta.url));
30424
30704
  var pkgVersion = "unknown";
30425
30705
  try {
30426
- const pkg = JSON.parse(readFileSync17(join24(__dirname, "..", "package.json"), "utf-8"));
30706
+ const pkg = JSON.parse(readFileSync18(join25(__dirname, "..", "package.json"), "utf-8"));
30427
30707
  pkgVersion = pkg.version;
30428
30708
  } catch {
30429
30709
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papi-ai/server",
3
- "version": "0.7.65",
3
+ "version": "0.7.67",
4
4
  "description": "PAPI MCP server — AI-powered sprint planning, build execution, and strategy review for software projects",
5
5
  "license": "Elastic-2.0",
6
6
  "mcpName": "io.github.getpapi/papi",