@papi-ai/server 0.7.70 → 0.7.72

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.
@@ -1621,6 +1621,13 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
1621
1621
  insertPlanRun(entry) {
1622
1622
  return this.invoke("insertPlanRun", [entry]);
1623
1623
  }
1624
+ /** task-2940: forwarded per-tool telemetry write. Requires the deployed
1625
+ * data-proxy to carry the matching `insertToolRun` case — until that deploy
1626
+ * lands, hosted callers fail this invoke and the caller swallows it (telemetry
1627
+ * must never fail a tool call). */
1628
+ insertToolRun(entry) {
1629
+ return this.invoke("insertToolRun", [entry]);
1630
+ }
1624
1631
  getCostSummary(cycleNumber) {
1625
1632
  return this.invoke("getCostSummary", [cycleNumber]);
1626
1633
  }
@@ -1865,9 +1872,12 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
1865
1872
  return this.invoke("getOwnerIdentity", []);
1866
1873
  }
1867
1874
  // --- Contributor cohort (task-2029, C288) ---
1868
- // Owner-only enforcement is BOTH tool-layer (resolveOwnerGate) and
1869
- // server-side in the edge function (auth-derived caller vs project owner)
1870
- // defence in depth, since this client runs on the user's machine.
1875
+ // Owner-only enforcement is BOTH tool-layer (resolveOwnerGate) and server-side
1876
+ // in the edge function (auth-derived caller vs project owner). task-2442 (C351)
1877
+ // added addContributorByEmail/removeContributorByEmail to the proxy WRITE_METHODS
1878
+ // set too, so a viewer is now rejected at the role-gate wall before the handler —
1879
+ // three layers of defence, since this client runs on the user's machine.
1880
+ // listContributors stays a member-open read (task-2377).
1871
1881
  async listContributors() {
1872
1882
  return this.invoke("listContributors", []);
1873
1883
  }
package/dist/index.js CHANGED
@@ -1738,6 +1738,13 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
1738
1738
  insertPlanRun(entry) {
1739
1739
  return this.invoke("insertPlanRun", [entry]);
1740
1740
  }
1741
+ /** task-2940: forwarded per-tool telemetry write. Requires the deployed
1742
+ * data-proxy to carry the matching `insertToolRun` case — until that deploy
1743
+ * lands, hosted callers fail this invoke and the caller swallows it (telemetry
1744
+ * must never fail a tool call). */
1745
+ insertToolRun(entry) {
1746
+ return this.invoke("insertToolRun", [entry]);
1747
+ }
1741
1748
  getCostSummary(cycleNumber) {
1742
1749
  return this.invoke("getCostSummary", [cycleNumber]);
1743
1750
  }
@@ -1982,9 +1989,12 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
1982
1989
  return this.invoke("getOwnerIdentity", []);
1983
1990
  }
1984
1991
  // --- Contributor cohort (task-2029, C288) ---
1985
- // Owner-only enforcement is BOTH tool-layer (resolveOwnerGate) and
1986
- // server-side in the edge function (auth-derived caller vs project owner)
1987
- // defence in depth, since this client runs on the user's machine.
1992
+ // Owner-only enforcement is BOTH tool-layer (resolveOwnerGate) and server-side
1993
+ // in the edge function (auth-derived caller vs project owner). task-2442 (C351)
1994
+ // added addContributorByEmail/removeContributorByEmail to the proxy WRITE_METHODS
1995
+ // set too, so a viewer is now rejected at the role-gate wall before the handler —
1996
+ // three layers of defence, since this client runs on the user's machine.
1997
+ // listContributors stays a member-open read (task-2377).
1988
1998
  async listContributors() {
1989
1999
  return this.invoke("listContributors", []);
1990
2000
  }
@@ -16915,7 +16925,9 @@ async function viewBoard(adapter2, phaseFilter, options) {
16915
16925
  total,
16916
16926
  returned: paged.length,
16917
16927
  offset,
16918
- hasMore: offset + paged.length < total
16928
+ hasMore: offset + paged.length < total,
16929
+ totalUnfiltered: allTasks.length,
16930
+ limit
16919
16931
  };
16920
16932
  }
16921
16933
  async function viewBoardSummary(adapter2) {
@@ -16963,7 +16975,87 @@ async function archiveTasks(adapter2, phases, statuses) {
16963
16975
  return { archivedCount: result.archivedCount, phases };
16964
16976
  }
16965
16977
 
16978
+ // src/lib/projection.ts
16979
+ function resolveFields(raw, allowed, toolName) {
16980
+ if (raw === void 0 || raw === null) return { ok: true, fields: null };
16981
+ if (typeof raw !== "string") {
16982
+ return { ok: false, error: `${toolName}: fields must be a comma-separated string (got ${typeof raw}).` };
16983
+ }
16984
+ const trimmed = raw.trim();
16985
+ if (trimmed === "") return { ok: true, fields: null };
16986
+ const canonical = new Map(allowed.map((a) => [a.toLowerCase(), a]));
16987
+ const requested = [];
16988
+ for (const part of trimmed.split(",")) {
16989
+ const token = part.trim();
16990
+ if (token === "") continue;
16991
+ const hit = canonical.get(token.toLowerCase());
16992
+ if (!hit) {
16993
+ return {
16994
+ ok: false,
16995
+ error: `${toolName}: unknown field "${token}". Allowed fields: ${allowed.join(", ")}.`
16996
+ };
16997
+ }
16998
+ if (!requested.includes(hit)) requested.push(hit);
16999
+ }
17000
+ if (requested.length === 0) return { ok: true, fields: null };
17001
+ if (!requested.includes("id")) requested.unshift("id");
17002
+ return { ok: true, fields: requested };
17003
+ }
17004
+ function selectColumns(columns, fields) {
17005
+ if (!fields) return columns;
17006
+ const byName = new Map(columns.map((c) => [c.name, c]));
17007
+ return fields.map((f) => byName.get(f)).filter((c) => c !== void 0);
17008
+ }
17009
+ function wantsMeta(raw) {
17010
+ return raw === true || raw === "true";
17011
+ }
17012
+ function formatMeta(meta) {
17013
+ return `**meta:** ${JSON.stringify(meta)}`;
17014
+ }
17015
+ function withMeta(body, meta) {
17016
+ return meta ? `${body}
17017
+
17018
+ ${formatMeta(meta)}` : body;
17019
+ }
17020
+ function pad(value, width) {
17021
+ return value.length >= width ? value : value + " ".repeat(width - value.length);
17022
+ }
17023
+ function renderTable(headers, rows) {
17024
+ const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
17025
+ const headerLine = headers.map((h, i) => pad(h, widths[i])).join(" | ");
17026
+ const separator = widths.map((w) => "-".repeat(w)).join(" | ");
17027
+ const dataLines = rows.map((row) => row.map((cell, i) => pad(cell, widths[i])).join(" | "));
17028
+ return [headerLine, separator, ...dataLines].join("\n");
17029
+ }
17030
+
16966
17031
  // src/tools/board.ts
17032
+ var BOARD_COLUMNS = [
17033
+ { name: "priority", header: "Priority", value: (t) => t.priority },
17034
+ { name: "id", header: "Task", value: (t) => t.id },
17035
+ { name: "title", header: "Summary", value: (t) => truncateTitle(t.title) },
17036
+ { name: "status", header: "Status", value: (t) => t.status },
17037
+ { name: "cycle", header: "Cycle", value: (t) => t.cycle != null ? String(t.cycle) : "-" },
17038
+ { name: "phase", header: "Phase", value: (t) => t.phase ?? "-" },
17039
+ { name: "module", header: "Module", value: (t) => t.module ?? "-" },
17040
+ { name: "epic", header: "Epic", value: (t) => t.epic ?? "-" },
17041
+ { name: "complexity", header: "Effort", value: (t) => t.complexity ?? "-" },
17042
+ { name: "createdAt", header: "Created", value: (t) => t.createdAt ?? "-" },
17043
+ { name: "source", header: "Source", value: (t) => t.source ?? "-" }
17044
+ ];
17045
+ var BOARD_FIELDS = BOARD_COLUMNS.map((c) => c.name);
17046
+ var BOARD_SINGLE_LABELS = {
17047
+ id: "Task",
17048
+ title: "Title",
17049
+ status: "Status",
17050
+ priority: "Priority",
17051
+ cycle: "Cycle",
17052
+ phase: "Phase",
17053
+ module: "Module",
17054
+ epic: "Epic",
17055
+ complexity: "Effort",
17056
+ createdAt: "Created",
17057
+ source: "Source"
17058
+ };
16967
17059
  var boardViewTool = {
16968
17060
  name: "board_view",
16969
17061
  description: 'View the Board. To find a SPECIFIC task or subset, FILTER FIRST \u2014 do not dump the whole board: pass task_id for one task (full detail), query="<text>" for a title/notes substring match, or cycle=<n> for one cycle. Combine with status/phase. By default shows active tasks only (excludes Done/Cancelled), sorted by priority, limited to 50; titles are truncated in the table (single-task lookup shows the full title). Use status="all" to see everything, mode="summary" for counts only. Does not call the Anthropic API.',
@@ -17008,6 +17100,14 @@ var boardViewTool = {
17008
17100
  type: "string",
17009
17101
  enum: ["full", "summary"],
17010
17102
  description: 'Output mode: "full" (default) shows task table, "summary" shows counts only.'
17103
+ },
17104
+ fields: {
17105
+ type: "string",
17106
+ description: `Optional sparse-field projection \u2014 comma-separated column names, e.g. "title,status". Returns ONLY those columns (id is always included so rows stay addressable) which is far cheaper on session context than the full table. Allowed: ${BOARD_FIELDS.join(", ")}. An unknown name is an error.`
17107
+ },
17108
+ meta: {
17109
+ type: "boolean",
17110
+ description: "Append a {total, filtered, returned, limit, offset} envelope so you can tell a truncated result from a complete one. Default false."
17011
17111
  }
17012
17112
  },
17013
17113
  required: []
@@ -17172,14 +17272,27 @@ var boardEditTool = {
17172
17272
  required: ["task_id"]
17173
17273
  }
17174
17274
  };
17175
- function pad(value, width) {
17176
- return value.length >= width ? value : value + " ".repeat(width - value.length);
17177
- }
17178
17275
  var TITLE_MAX = 80;
17179
17276
  function truncateTitle(title) {
17180
17277
  return title.length > TITLE_MAX ? `${title.slice(0, TITLE_MAX - 1)}\u2026` : title;
17181
17278
  }
17182
- function formatSingleTask(t) {
17279
+ function formatSingleTask(t, fields) {
17280
+ if (fields) {
17281
+ const values2 = {
17282
+ id: t.id,
17283
+ title: t.title,
17284
+ status: t.status,
17285
+ priority: t.priority,
17286
+ cycle: t.cycle != null ? String(t.cycle) : "-",
17287
+ phase: t.phase ?? "-",
17288
+ module: t.module ?? "-",
17289
+ epic: t.epic ?? "-",
17290
+ complexity: t.complexity ?? "-",
17291
+ createdAt: t.createdAt ?? "-",
17292
+ source: t.source ?? "-"
17293
+ };
17294
+ return fields.map((f) => `- **${BOARD_SINGLE_LABELS[f] ?? f}:** ${values2[f]}`).join("\n");
17295
+ }
17183
17296
  const lines = [
17184
17297
  `**${t.id} \u2014 ${t.title}**`,
17185
17298
  "",
@@ -17196,41 +17309,20 @@ function formatSingleTask(t) {
17196
17309
  if (t.why?.trim()) lines.push(`- **Why:** ${t.why.trim()}`);
17197
17310
  return lines.join("\n");
17198
17311
  }
17199
- function formatBoard(result) {
17312
+ function formatBoard(result, fields) {
17200
17313
  if (result.tasks.length === 0) {
17201
17314
  return "No tasks found.";
17202
17315
  }
17203
- const headers = ["Priority", "Task", "Summary", "Status", "Cycle", "Phase", "Module", "Epic", "Effort", "Created", "Source"];
17204
- const rows = result.tasks.map((t) => [
17205
- t.priority,
17206
- t.id,
17207
- truncateTitle(t.title),
17208
- t.status,
17209
- t.cycle != null ? String(t.cycle) : "-",
17210
- t.phase ?? "-",
17211
- t.module ?? "-",
17212
- t.epic ?? "-",
17213
- t.complexity ?? "-",
17214
- t.createdAt ?? "-",
17215
- t.source ?? "-"
17216
- ]);
17217
- const widths = headers.map(
17218
- (h, i) => Math.max(h.length, ...rows.map((r) => r[i].length))
17219
- );
17220
- const headerLine = headers.map((h, i) => pad(h, widths[i])).join(" | ");
17221
- const separator = widths.map((w) => "-".repeat(w)).join(" | ");
17222
- const dataLines = rows.map(
17223
- (row) => row.map((cell, i) => pad(cell, widths[i])).join(" | ")
17224
- );
17316
+ const columns = selectColumns(BOARD_COLUMNS, fields);
17317
+ const headers = columns.map((c) => c.header);
17318
+ const rows = result.tasks.map((t) => columns.map((c) => c.value(t)));
17225
17319
  const lines = [];
17226
17320
  lines.push(`**${result.returned} of ${result.total} tasks**`);
17227
17321
  if (result.hasMore) {
17228
17322
  lines.push(`_Showing ${result.offset + 1}\u2013${result.offset + result.returned}. Use offset=${result.offset + result.returned} to see more._`);
17229
17323
  }
17230
17324
  lines.push("");
17231
- lines.push(headerLine);
17232
- lines.push(separator);
17233
- lines.push(...dataLines);
17325
+ lines.push(renderTable(headers, rows));
17234
17326
  return lines.join("\n");
17235
17327
  }
17236
17328
  function formatSummary(summary) {
@@ -17252,15 +17344,21 @@ function formatSummary(summary) {
17252
17344
  }
17253
17345
  async function handleBoardView(adapter2, args) {
17254
17346
  const mode = args.mode;
17347
+ const projection = resolveFields(args.fields, BOARD_FIELDS, "board_view");
17348
+ if (!projection.ok) return errorResponse(projection.error);
17349
+ const fields = projection.fields;
17350
+ const emitMeta = wantsMeta(args.meta);
17255
17351
  if (mode === "summary") {
17256
17352
  const summary = await viewBoardSummary(adapter2);
17257
- return textResponse(formatSummary(summary));
17353
+ const meta2 = emitMeta ? { total: summary.total, filtered: summary.total, returned: summary.total, limit: null, offset: 0 } : null;
17354
+ return textResponse(withMeta(formatSummary(summary), meta2));
17258
17355
  }
17259
17356
  const taskIdArg = args.task_id ?? args.display_id;
17260
17357
  if (taskIdArg) {
17261
17358
  const task = await adapter2.getTask(taskIdArg);
17262
17359
  if (!task) return errorResponse(`Task ${taskIdArg} not found.`);
17263
- return textResponse(formatSingleTask(task));
17360
+ const meta2 = emitMeta ? { total: 1, filtered: 1, returned: 1, limit: null, offset: 0 } : null;
17361
+ return textResponse(withMeta(formatSingleTask(task, fields), meta2));
17264
17362
  }
17265
17363
  const result = await viewBoard(adapter2, void 0, {
17266
17364
  phase: args.phase,
@@ -17270,17 +17368,26 @@ async function handleBoardView(adapter2, args) {
17270
17368
  query: args.query,
17271
17369
  cycle: args.cycle
17272
17370
  });
17273
- let output = formatBoard(result);
17274
- try {
17275
- const comments = await adapter2.getRecentTaskComments?.(30);
17276
- if (comments && comments.length > 0) {
17277
- const taskIds = new Set(result.tasks.map((t) => t.id));
17278
- const section = formatTaskComments(comments, taskIds, "**Task Comments:**");
17279
- if (section) output += "\n" + section;
17371
+ let output = formatBoard(result, fields);
17372
+ if (!fields) {
17373
+ try {
17374
+ const comments = await adapter2.getRecentTaskComments?.(30);
17375
+ if (comments && comments.length > 0) {
17376
+ const taskIds = new Set(result.tasks.map((t) => t.id));
17377
+ const section = formatTaskComments(comments, taskIds, "**Task Comments:**");
17378
+ if (section) output += "\n" + section;
17379
+ }
17380
+ } catch {
17280
17381
  }
17281
- } catch {
17282
17382
  }
17283
- return textResponse(output);
17383
+ const meta = emitMeta ? {
17384
+ total: result.totalUnfiltered,
17385
+ filtered: result.total,
17386
+ returned: result.returned,
17387
+ limit: result.limit,
17388
+ offset: result.offset
17389
+ } : null;
17390
+ return textResponse(withMeta(output, meta));
17284
17391
  }
17285
17392
  async function handleBoardDeprioritise(adapter2, args) {
17286
17393
  const taskId = args.task_id;
@@ -21089,6 +21196,18 @@ function clearBuildCheckpoint(key) {
21089
21196
  } catch {
21090
21197
  }
21091
21198
  }
21199
+ function writeBuildCheckpointIfLocal(input) {
21200
+ if (!hasLocalWorkspace()) return;
21201
+ writeBuildCheckpoint(input);
21202
+ }
21203
+ function readBuildCheckpointIfLocal(key) {
21204
+ if (!hasLocalWorkspace()) return null;
21205
+ return readBuildCheckpoint(key);
21206
+ }
21207
+ function clearBuildCheckpointIfLocal(key) {
21208
+ if (!hasLocalWorkspace()) return;
21209
+ clearBuildCheckpoint(key);
21210
+ }
21092
21211
  function formatResumeNote(cp) {
21093
21212
  const files = cp.modifiedFiles.filter((f) => f && f.trim());
21094
21213
  const fileList = files.length > 0 ? files.slice(0, 12).map((f) => ` - ${f}`).join("\n") + (files.length > 12 ? `
@@ -21515,7 +21634,7 @@ async function listBuilds(adapter2, config2) {
21515
21634
  ]);
21516
21635
  const currentCycle = health?.totalCycles ?? 0;
21517
21636
  if (tasks.length === 0) {
21518
- return { sorted: [], inProgress: [], backlog: [], blocked: [], warnings, isEmpty: true, noHandoffs: false, currentCycle };
21637
+ return { sorted: [], inProgress: [], backlog: [], blocked: [], warnings, isEmpty: true, noHandoffs: false, currentCycle, totalTasks: tasks.length };
21519
21638
  }
21520
21639
  const withHandoff = tasks.filter((t) => {
21521
21640
  if (!t.buildHandoff) return false;
@@ -21526,7 +21645,7 @@ async function listBuilds(adapter2, config2) {
21526
21645
  return true;
21527
21646
  });
21528
21647
  if (withHandoff.length === 0) {
21529
- return { sorted: [], inProgress: [], backlog: [], blocked: [], warnings, isEmpty: false, noHandoffs: true, currentCycle };
21648
+ return { sorted: [], inProgress: [], backlog: [], blocked: [], warnings, isEmpty: false, noHandoffs: true, currentCycle, totalTasks: tasks.length };
21530
21649
  }
21531
21650
  const buildable = [];
21532
21651
  const blocked = [];
@@ -21541,7 +21660,7 @@ async function listBuilds(adapter2, config2) {
21541
21660
  const inProgress = buildable.filter((t) => t.status === "In Progress");
21542
21661
  const backlog = buildable.filter((t) => t.status !== "In Progress");
21543
21662
  const sorted = [...inProgress, ...backlog];
21544
- return { sorted, inProgress, backlog, blocked, warnings, isEmpty: false, noHandoffs: sorted.length === 0 && blocked.length === 0, currentCycle };
21663
+ return { sorted, inProgress, backlog, blocked, warnings, isEmpty: false, noHandoffs: sorted.length === 0 && blocked.length === 0, currentCycle, totalTasks: tasks.length };
21545
21664
  }
21546
21665
  async function describeTask(adapter2, taskId) {
21547
21666
  const task = await adapter2.getTask(taskId);
@@ -21858,7 +21977,7 @@ async function startBuild(adapter2, config2, taskId, options = {}, clientName) {
21858
21977
  );
21859
21978
  } catch {
21860
21979
  }
21861
- writeBuildCheckpoint({
21980
+ writeBuildCheckpointIfLocal({
21862
21981
  cwd: config2.projectRoot,
21863
21982
  taskId,
21864
21983
  branch: getCurrentBranch(config2.projectRoot),
@@ -22362,12 +22481,14 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
22362
22481
  }
22363
22482
  } catch {
22364
22483
  }
22365
- try {
22366
- clearActiveTaskScope(config2.projectRoot);
22367
- } catch {
22484
+ if (hasLocalWorkspace()) {
22485
+ try {
22486
+ clearActiveTaskScope(config2.projectRoot);
22487
+ } catch {
22488
+ }
22368
22489
  }
22369
22490
  try {
22370
- clearBuildCheckpoint({ cwd: config2.projectRoot, taskId });
22491
+ clearBuildCheckpointIfLocal({ cwd: config2.projectRoot, taskId });
22371
22492
  } catch {
22372
22493
  }
22373
22494
  return {
@@ -22393,7 +22514,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}, cli
22393
22514
  localPreview
22394
22515
  };
22395
22516
  }
22396
- async function cancelBuild(adapter2, taskId, reason) {
22517
+ async function cancelBuild(adapter2, taskId, reason, projectRoot) {
22397
22518
  const task = await adapter2.getTask(taskId);
22398
22519
  if (!task) {
22399
22520
  throw new Error(`Task "${taskId}" not found.`);
@@ -22402,6 +22523,18 @@ async function cancelBuild(adapter2, taskId, reason) {
22402
22523
  status: "Cancelled",
22403
22524
  closureReason: reason
22404
22525
  });
22526
+ if (projectRoot) {
22527
+ if (hasLocalWorkspace()) {
22528
+ try {
22529
+ clearActiveTaskScope(projectRoot);
22530
+ } catch {
22531
+ }
22532
+ }
22533
+ try {
22534
+ clearBuildCheckpointIfLocal({ cwd: projectRoot, taskId });
22535
+ } catch {
22536
+ }
22537
+ }
22405
22538
  return { task, reason };
22406
22539
  }
22407
22540
 
@@ -22947,6 +23080,17 @@ async function handleDocReorder(adapter2, args) {
22947
23080
 
22948
23081
  // src/tools/build.ts
22949
23082
  init_git();
23083
+ var BUILD_LIST_COLUMNS = [
23084
+ { name: "id", header: "Task", value: (t) => t.id },
23085
+ { name: "title", header: "Summary", value: (t) => t.title },
23086
+ { name: "status", header: "Status", value: (t) => t.status },
23087
+ { name: "priority", header: "Priority", value: (t) => t.priority },
23088
+ { name: "complexity", header: "Effort", value: (t) => t.complexity ?? "-" },
23089
+ { name: "module", header: "Module", value: (t) => t.module ?? "-" },
23090
+ { name: "epic", header: "Epic", value: (t) => t.epic ?? "-" },
23091
+ { name: "cycle", header: "Cycle", value: (t) => t.cycle != null ? String(t.cycle) : "-" }
23092
+ ];
23093
+ var BUILD_LIST_FIELDS = BUILD_LIST_COLUMNS.map((c) => c.name);
22950
23094
  var buildListTool = {
22951
23095
  name: "build_list",
22952
23096
  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.",
@@ -22956,7 +23100,16 @@ var buildListTool = {
22956
23100
  properties: {
22957
23101
  limit: {
22958
23102
  type: "integer",
22959
- description: "Optional maximum number of tasks to return per section. Omit to return all. Reserved for future pagination \u2014 current behaviour returns all matching tasks regardless of value."
23103
+ minimum: 1,
23104
+ description: "Optional maximum number of tasks to return per section (In Progress / Backlog / Blocked). Omit to return all."
23105
+ },
23106
+ fields: {
23107
+ type: "string",
23108
+ description: `Optional sparse-field projection \u2014 comma-separated column names, e.g. "title,status". Returns a compact table of ONLY those columns (id is always included) instead of the full per-task block, which is far cheaper on session context. Allowed: ${BUILD_LIST_FIELDS.join(", ")}. An unknown name is an error.`
23109
+ },
23110
+ meta: {
23111
+ type: "boolean",
23112
+ description: "Append a {total, filtered, returned, limit, offset} envelope so you can tell a truncated result from a complete one. Default false."
22960
23113
  }
22961
23114
  },
22962
23115
  required: []
@@ -23221,50 +23374,74 @@ var AD_CONFLICT_GATE_INSTRUCTION = "\n**AD conflict gate:** If this task conflic
23221
23374
  function hasReportFields(args) {
23222
23375
  return !!(args.completed || args.effort || args.estimated_effort || args.surprises || args.discovered_issues || args.architecture_notes);
23223
23376
  }
23224
- async function handleBuildList(adapter2, config2) {
23377
+ async function handleBuildList(adapter2, config2, args = {}) {
23378
+ const projection = resolveFields(args.fields, BUILD_LIST_FIELDS, "build_list");
23379
+ if (!projection.ok) return errorResponse(projection.error);
23380
+ const fields = projection.fields;
23381
+ const emitMeta = wantsMeta(args.meta);
23382
+ const rawLimit = args.limit;
23383
+ const limit = typeof rawLimit === "number" && Number.isInteger(rawLimit) && rawLimit > 0 ? rawLimit : null;
23384
+ const cap = (rows) => limit === null ? rows : rows.slice(0, limit);
23225
23385
  const result = await listBuilds(adapter2, config2);
23226
23386
  const warningPrefix = result.warnings.length > 0 ? result.warnings.map((w) => `> ${w}`).join("\n") + "\n\n" : "";
23387
+ const emptyMeta = () => emitMeta ? { total: result.totalTasks, filtered: 0, returned: 0, limit, offset: 0 } : null;
23227
23388
  if (result.isEmpty) {
23228
23389
  if (result.currentCycle === 0) {
23229
- return textResponse(warningPrefix + `No tasks found \u2014 this looks like a new project. Run \`setup\` to initialise your project, then \`plan\` to create your first cycle.`);
23390
+ return textResponse(withMeta(warningPrefix + `No tasks found \u2014 this looks like a new project. Run \`setup\` to initialise your project, then \`plan\` to create your first cycle.`, emptyMeta()));
23230
23391
  }
23231
- return textResponse(warningPrefix + `**Cycle ${result.currentCycle}** \u2014 Board is empty. Run \`plan\` to create your next cycle.`);
23392
+ return textResponse(withMeta(warningPrefix + `**Cycle ${result.currentCycle}** \u2014 Board is empty. Run \`plan\` to create your next cycle.`, emptyMeta()));
23232
23393
  }
23233
23394
  if (result.noHandoffs) {
23234
- return textResponse(warningPrefix + `**Cycle ${result.currentCycle}** \u2014 No tasks with BUILD HANDOFFs. Run \`plan\` to generate your next cycle tasks.`);
23395
+ return textResponse(withMeta(warningPrefix + `**Cycle ${result.currentCycle}** \u2014 No tasks with BUILD HANDOFFs. Run \`plan\` to generate your next cycle tasks.`, emptyMeta()));
23235
23396
  }
23397
+ const inProgress = cap(result.inProgress);
23398
+ const backlog = cap(result.backlog);
23399
+ const blocked = cap(result.blocked);
23236
23400
  const lines = [];
23237
23401
  if (warningPrefix) {
23238
23402
  lines.push(warningPrefix.trimEnd(), "");
23239
23403
  }
23240
23404
  const totalCount = result.sorted.length + result.blocked.length;
23241
23405
  lines.push(`**Cycle ${result.currentCycle}** \u2014 ${totalCount} tasks with BUILD HANDOFFs:`, "");
23242
- if (result.inProgress.length > 0) {
23243
- lines.push(`## In Progress (${result.inProgress.length})`);
23244
- lines.push(...result.inProgress.map(formatListItem));
23245
- lines.push("");
23246
- }
23247
- if (result.backlog.length > 0) {
23248
- lines.push(`## Backlog (${result.backlog.length})`);
23249
- lines.push(...result.backlog.map(formatListItem));
23250
- }
23251
- if (result.blocked.length > 0) {
23252
- lines.push("", `## Blocked (${result.blocked.length}) \u2014 waiting on dependencies`);
23253
- for (const { task, unresolvedDeps } of result.blocked) {
23254
- lines.push(`- **${task.id}:** ${task.title}
23406
+ if (fields) {
23407
+ const columns = selectColumns(BUILD_LIST_COLUMNS, fields);
23408
+ const rows = [...inProgress, ...backlog, ...blocked.map((b2) => b2.task)].map((t) => columns.map((c) => c.value(t)));
23409
+ lines.push(renderTable(columns.map((c) => c.header), rows));
23410
+ } else {
23411
+ if (inProgress.length > 0) {
23412
+ lines.push(`## In Progress (${result.inProgress.length})`);
23413
+ lines.push(...inProgress.map(formatListItem));
23414
+ lines.push("");
23415
+ }
23416
+ if (backlog.length > 0) {
23417
+ lines.push(`## Backlog (${result.backlog.length})`);
23418
+ lines.push(...backlog.map(formatListItem));
23419
+ }
23420
+ if (blocked.length > 0) {
23421
+ lines.push("", `## Blocked (${result.blocked.length}) \u2014 waiting on dependencies`);
23422
+ for (const { task, unresolvedDeps } of blocked) {
23423
+ lines.push(`- **${task.id}:** ${task.title}
23255
23424
  Waiting on: ${unresolvedDeps.join(", ")}`);
23425
+ }
23256
23426
  }
23257
- }
23258
- try {
23259
- const comments = await adapter2.getRecentTaskComments?.(30);
23260
- if (comments && comments.length > 0) {
23261
- const taskIds = new Set([...result.sorted, ...result.blocked.map((b2) => b2.task)].map((t) => t.id));
23262
- const section = formatTaskComments(comments, taskIds);
23263
- if (section) lines.push(section);
23427
+ try {
23428
+ const comments = await adapter2.getRecentTaskComments?.(30);
23429
+ if (comments && comments.length > 0) {
23430
+ const taskIds = new Set([...result.sorted, ...result.blocked.map((b2) => b2.task)].map((t) => t.id));
23431
+ const section = formatTaskComments(comments, taskIds);
23432
+ if (section) lines.push(section);
23433
+ }
23434
+ } catch {
23264
23435
  }
23265
- } catch {
23266
23436
  }
23267
- return textResponse(lines.join("\n"));
23437
+ const meta = emitMeta ? {
23438
+ total: result.totalTasks,
23439
+ filtered: totalCount,
23440
+ returned: inProgress.length + backlog.length + blocked.length,
23441
+ limit,
23442
+ offset: 0
23443
+ } : null;
23444
+ return textResponse(withMeta(lines.join("\n"), meta));
23268
23445
  }
23269
23446
  async function handleBuildDescribe(adapter2, args) {
23270
23447
  const taskId = args.task_id;
@@ -23318,7 +23495,7 @@ async function handleBuildExecute(adapter2, config2, args, clientName) {
23318
23495
  }
23319
23496
  let resumeNote = "";
23320
23497
  if (scopeTask?.status === "In Progress") {
23321
- const existing = readBuildCheckpoint({ cwd: config2.projectRoot, taskId });
23498
+ const existing = readBuildCheckpointIfLocal({ cwd: config2.projectRoot, taskId });
23322
23499
  if (existing) resumeNote = formatResumeNote(existing);
23323
23500
  }
23324
23501
  await tracker.recordStep("started");
@@ -23739,7 +23916,7 @@ function formatCompleteResult(result) {
23739
23916
  }
23740
23917
  return lines.join("\n");
23741
23918
  }
23742
- async function handleBuildCancel(adapter2, args) {
23919
+ async function handleBuildCancel(adapter2, config2, args) {
23743
23920
  const taskId = args.task_id;
23744
23921
  if (!taskId) {
23745
23922
  return errorResponse("task_id is required.");
@@ -23749,7 +23926,7 @@ async function handleBuildCancel(adapter2, args) {
23749
23926
  return errorResponse("reason is required.");
23750
23927
  }
23751
23928
  try {
23752
- const result = await cancelBuild(adapter2, taskId, reason);
23929
+ const result = await cancelBuild(adapter2, taskId, reason, config2.projectRoot);
23753
23930
  return textResponse(`Cancelled **${result.task.id}** (${result.task.title}).
23754
23931
 
23755
23932
  Reason: ${result.reason}`);
@@ -28677,7 +28854,7 @@ ${section}`;
28677
28854
  }
28678
28855
  tracker.mark("format-summary");
28679
28856
  const deepHint = deepHousekeeping ? "" : "\n\n*Tip: pass `full: true` for Research Signals + version-drift, or `deep_housekeeping: true` to also check orphaned branches, merged-but-In-Progress tasks, unrecorded commits, unregistered docs, and stale skill forks (implies `full`).*";
28680
- return textResponse(projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents, projectName, teamSummary, clientName, carryForwardRefs) + unblockNote + deferredGateNote + feedbackResolvedNote + alertsNote + ttfvNote + reconciliationNote + mergedInProgressNote + unrecordedNote + unregisteredDocsNote + staleSkillsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + onboardingCoachingNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + deepHint + enrichmentFilesSection);
28857
+ return { ...textResponse(projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents, projectName, teamSummary, clientName, carryForwardRefs) + unblockNote + deferredGateNote + feedbackResolvedNote + alertsNote + ttfvNote + reconciliationNote + mergedInProgressNote + unrecordedNote + unregisteredDocsNote + staleSkillsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + onboardingCoachingNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + deepHint + enrichmentFilesSection), _cycleNumber: healthResult.cycleNumber };
28681
28858
  } catch (err) {
28682
28859
  const message = err instanceof Error ? err.message : String(err);
28683
28860
  const isKnownFriendly = /^(Orient failed|Project not found|No project|Setup required)/i.test(message);
@@ -29781,6 +29958,15 @@ function readLlmResponse(args) {
29781
29958
  }
29782
29959
 
29783
29960
  // src/tools/ad-view.ts
29961
+ var AD_COLUMNS = [
29962
+ { name: "id", header: "AD", value: (d) => d.id },
29963
+ { name: "title", header: "Title", value: (d) => d.title },
29964
+ { name: "confidence", header: "Confidence", value: (d) => d.confidence },
29965
+ { name: "superseded", header: "Superseded", value: (d) => d.superseded ? "yes" : "no" },
29966
+ { name: "supersededBy", header: "Superseded By", value: (d) => d.supersededBy ?? "-" },
29967
+ { name: "body", header: "Body", value: (d) => d.body.replace(/\s+/g, " ").trim() }
29968
+ ];
29969
+ var AD_FIELDS = AD_COLUMNS.map((c) => c.name);
29784
29970
  var adViewTool = {
29785
29971
  name: "ad_view",
29786
29972
  description: "View one or all Active Decisions with full bodies. Use when you need to read the complete reasoning and evidence behind a specific AD before running strategy_change.",
@@ -29795,6 +29981,24 @@ var adViewTool = {
29795
29981
  include_superseded: {
29796
29982
  type: "boolean",
29797
29983
  description: "Include superseded (retired) ADs. Default false."
29984
+ },
29985
+ fields: {
29986
+ type: "string",
29987
+ description: `Optional sparse-field projection \u2014 comma-separated field names, e.g. "title,confidence". Returns a compact table of ONLY those fields (id is always included) instead of full AD bodies, which is far cheaper on session context when you just need the index. Allowed: ${AD_FIELDS.join(", ")}. An unknown name is an error.`
29988
+ },
29989
+ limit: {
29990
+ type: "integer",
29991
+ minimum: 1,
29992
+ description: "Max decisions to return. Omit to return all."
29993
+ },
29994
+ offset: {
29995
+ type: "integer",
29996
+ minimum: 0,
29997
+ description: "Skip the first N decisions (pagination). Default 0."
29998
+ },
29999
+ meta: {
30000
+ type: "boolean",
30001
+ description: "Append a {total, filtered, returned, limit, offset} envelope so you can tell a truncated result from a complete one. Default false."
29798
30002
  }
29799
30003
  },
29800
30004
  required: []
@@ -29803,6 +30007,14 @@ var adViewTool = {
29803
30007
  async function handleAdView(adapter2, args) {
29804
30008
  const adId = args.ad_id;
29805
30009
  const includeSuperseded = args.include_superseded === true;
30010
+ const projection = resolveFields(args.fields, AD_FIELDS, "ad_view");
30011
+ if (!projection.ok) return errorResponse(projection.error);
30012
+ const fields = projection.fields;
30013
+ const emitMeta = wantsMeta(args.meta);
30014
+ const rawLimit = args.limit;
30015
+ const limit = typeof rawLimit === "number" && Number.isInteger(rawLimit) && rawLimit > 0 ? rawLimit : null;
30016
+ const rawOffset = args.offset;
30017
+ const offset = typeof rawOffset === "number" && Number.isInteger(rawOffset) && rawOffset > 0 ? rawOffset : 0;
29806
30018
  let decisions;
29807
30019
  try {
29808
30020
  decisions = await adapter2.getActiveDecisions({ includeRetired: includeSuperseded });
@@ -29815,26 +30027,79 @@ async function handleAdView(adapter2, args) {
29815
30027
  const ids = decisions.map((d) => d.id).join(", ");
29816
30028
  return errorResponse(`AD not found: ${adId}. Available: ${ids || "none"}`);
29817
30029
  }
30030
+ const meta2 = emitMeta ? { total: decisions.length, filtered: 1, returned: 1, limit: null, offset: 0 } : null;
30031
+ if (fields) {
30032
+ const columns = selectColumns(AD_COLUMNS, fields);
30033
+ const body = columns.map((c) => `- **${c.header}:** ${c.value(target)}`).join("\n");
30034
+ return textResponse(withMeta(body, meta2));
30035
+ }
29818
30036
  const supersededNote = target.superseded ? ` [SUPERSEDED by ${target.supersededBy}]` : "";
29819
30037
  return textResponse(
29820
- `## ${target.id}: ${target.title} [${target.confidence}]${supersededNote}
30038
+ withMeta(`## ${target.id}: ${target.title} [${target.confidence}]${supersededNote}
29821
30039
 
29822
- ${target.body}`
30040
+ ${target.body}`, meta2)
29823
30041
  );
29824
30042
  }
29825
30043
  const filtered = includeSuperseded ? decisions : decisions.filter((d) => !d.superseded);
29826
- if (filtered.length === 0) {
29827
- return textResponse("No active decisions found.");
30044
+ const paged = limit === null && offset === 0 ? filtered : filtered.slice(offset, limit === null ? void 0 : offset + limit);
30045
+ const meta = emitMeta ? { total: decisions.length, filtered: filtered.length, returned: paged.length, limit, offset } : null;
30046
+ if (paged.length === 0) {
30047
+ return textResponse(withMeta("No active decisions found.", meta));
30048
+ }
30049
+ if (fields) {
30050
+ const columns = selectColumns(AD_COLUMNS, fields);
30051
+ const rows = paged.map((d) => columns.map((c) => c.value(d)));
30052
+ return textResponse(withMeta(renderTable(columns.map((c) => c.header), rows), meta));
29828
30053
  }
29829
- const formatted = filtered.map((d) => {
30054
+ const formatted = paged.map((d) => {
29830
30055
  const supersededNote = d.superseded ? ` [SUPERSEDED by ${d.supersededBy}]` : "";
29831
30056
  return `## ${d.id}: ${d.title} [${d.confidence}]${supersededNote}
29832
30057
 
29833
30058
  ${d.body}`;
29834
30059
  }).join("\n\n---\n\n");
29835
- return textResponse(`# Active Decisions (${filtered.length})
30060
+ return textResponse(withMeta(`# Active Decisions (${paged.length})
29836
30061
 
29837
- ${formatted}`);
30062
+ ${formatted}`, meta));
30063
+ }
30064
+
30065
+ // src/lib/tool-telemetry.ts
30066
+ var INSTRUMENTED_TOOLS = /* @__PURE__ */ new Set([
30067
+ "plan",
30068
+ "orient",
30069
+ "build_execute",
30070
+ "review_submit",
30071
+ "release",
30072
+ "strategy_review"
30073
+ ]);
30074
+ var WRAPPER_INSTRUMENTED_TOOLS = new Set(
30075
+ [...INSTRUMENTED_TOOLS].filter((t) => t !== "plan")
30076
+ );
30077
+ function measureResultBytes(content) {
30078
+ let total = 0;
30079
+ for (const part of content) {
30080
+ if (typeof part.text === "string") total += Buffer.byteLength(part.text, "utf-8");
30081
+ }
30082
+ return total;
30083
+ }
30084
+ function recordToolRun(adapter2, sample) {
30085
+ if (!WRAPPER_INSTRUMENTED_TOOLS.has(sample.toolName)) return;
30086
+ if (typeof adapter2.insertToolRun !== "function") return;
30087
+ try {
30088
+ const promise = adapter2.insertToolRun({
30089
+ toolName: sample.toolName,
30090
+ contextBytes: sample.contextBytes,
30091
+ durationMs: sample.durationMs,
30092
+ cycleNumber: sample.cycleNumber ?? null,
30093
+ source: "mcp-server"
30094
+ });
30095
+ if (promise && typeof promise.catch === "function") {
30096
+ promise.catch((err) => {
30097
+ console.error(`[telemetry] insertToolRun failed (non-blocking): ${err instanceof Error ? err.message : String(err)}`);
30098
+ });
30099
+ }
30100
+ } catch (err) {
30101
+ console.error(`[telemetry] insertToolRun threw (non-blocking): ${err instanceof Error ? err.message : String(err)}`);
30102
+ }
29838
30103
  }
29839
30104
 
29840
30105
  // src/tools/learning-action.ts
@@ -31131,13 +31396,13 @@ function createServer(adapter2, config2) {
31131
31396
  case "setup":
31132
31397
  return handleSetup(adapter2, config2, safeArgs, server2.getClientVersion()?.name);
31133
31398
  case "build_list":
31134
- return handleBuildList(adapter2, config2);
31399
+ return handleBuildList(adapter2, config2, safeArgs);
31135
31400
  case "build_describe":
31136
31401
  return handleBuildDescribe(adapter2, safeArgs);
31137
31402
  case "build_execute":
31138
31403
  return handleBuildExecute(adapter2, config2, safeArgs, server2.getClientVersion()?.name);
31139
31404
  case "build_cancel":
31140
- return handleBuildCancel(adapter2, safeArgs);
31405
+ return handleBuildCancel(adapter2, config2, safeArgs);
31141
31406
  case "idea":
31142
31407
  return handleIdea(adapter2, config2, safeArgs);
31143
31408
  case "backlog_import":
@@ -31260,9 +31525,17 @@ ${usageLine(decision.usage)}`;
31260
31525
  const usage = result._usage;
31261
31526
  const contextBytes = result._contextBytes;
31262
31527
  const contextUtilisation = result._contextUtilisation;
31528
+ const toolCycleNumber = result._cycleNumber;
31263
31529
  delete result._usage;
31264
31530
  delete result._contextBytes;
31265
31531
  delete result._contextUtilisation;
31532
+ delete result._cycleNumber;
31533
+ recordToolRun(adapter2, {
31534
+ toolName: name,
31535
+ contextBytes: measureResultBytes(result.content),
31536
+ durationMs: elapsed,
31537
+ cycleNumber: toolCycleNumber ?? null
31538
+ });
31266
31539
  const isError = result.content.some((c) => c.text.startsWith("Error:") || c.text.startsWith("\u274C"));
31267
31540
  recordToolOutcome(!isError, callerKey);
31268
31541
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papi-ai/server",
3
- "version": "0.7.70",
3
+ "version": "0.7.72",
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",