@papi-ai/server 0.7.71 → 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
  }
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
  }
@@ -16918,7 +16925,9 @@ async function viewBoard(adapter2, phaseFilter, options) {
16918
16925
  total,
16919
16926
  returned: paged.length,
16920
16927
  offset,
16921
- hasMore: offset + paged.length < total
16928
+ hasMore: offset + paged.length < total,
16929
+ totalUnfiltered: allTasks.length,
16930
+ limit
16922
16931
  };
16923
16932
  }
16924
16933
  async function viewBoardSummary(adapter2) {
@@ -16966,7 +16975,87 @@ async function archiveTasks(adapter2, phases, statuses) {
16966
16975
  return { archivedCount: result.archivedCount, phases };
16967
16976
  }
16968
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
+
16969
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
+ };
16970
17059
  var boardViewTool = {
16971
17060
  name: "board_view",
16972
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.',
@@ -17011,6 +17100,14 @@ var boardViewTool = {
17011
17100
  type: "string",
17012
17101
  enum: ["full", "summary"],
17013
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."
17014
17111
  }
17015
17112
  },
17016
17113
  required: []
@@ -17175,14 +17272,27 @@ var boardEditTool = {
17175
17272
  required: ["task_id"]
17176
17273
  }
17177
17274
  };
17178
- function pad(value, width) {
17179
- return value.length >= width ? value : value + " ".repeat(width - value.length);
17180
- }
17181
17275
  var TITLE_MAX = 80;
17182
17276
  function truncateTitle(title) {
17183
17277
  return title.length > TITLE_MAX ? `${title.slice(0, TITLE_MAX - 1)}\u2026` : title;
17184
17278
  }
17185
- 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
+ }
17186
17296
  const lines = [
17187
17297
  `**${t.id} \u2014 ${t.title}**`,
17188
17298
  "",
@@ -17199,41 +17309,20 @@ function formatSingleTask(t) {
17199
17309
  if (t.why?.trim()) lines.push(`- **Why:** ${t.why.trim()}`);
17200
17310
  return lines.join("\n");
17201
17311
  }
17202
- function formatBoard(result) {
17312
+ function formatBoard(result, fields) {
17203
17313
  if (result.tasks.length === 0) {
17204
17314
  return "No tasks found.";
17205
17315
  }
17206
- const headers = ["Priority", "Task", "Summary", "Status", "Cycle", "Phase", "Module", "Epic", "Effort", "Created", "Source"];
17207
- const rows = result.tasks.map((t) => [
17208
- t.priority,
17209
- t.id,
17210
- truncateTitle(t.title),
17211
- t.status,
17212
- t.cycle != null ? String(t.cycle) : "-",
17213
- t.phase ?? "-",
17214
- t.module ?? "-",
17215
- t.epic ?? "-",
17216
- t.complexity ?? "-",
17217
- t.createdAt ?? "-",
17218
- t.source ?? "-"
17219
- ]);
17220
- const widths = headers.map(
17221
- (h, i) => Math.max(h.length, ...rows.map((r) => r[i].length))
17222
- );
17223
- const headerLine = headers.map((h, i) => pad(h, widths[i])).join(" | ");
17224
- const separator = widths.map((w) => "-".repeat(w)).join(" | ");
17225
- const dataLines = rows.map(
17226
- (row) => row.map((cell, i) => pad(cell, widths[i])).join(" | ")
17227
- );
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)));
17228
17319
  const lines = [];
17229
17320
  lines.push(`**${result.returned} of ${result.total} tasks**`);
17230
17321
  if (result.hasMore) {
17231
17322
  lines.push(`_Showing ${result.offset + 1}\u2013${result.offset + result.returned}. Use offset=${result.offset + result.returned} to see more._`);
17232
17323
  }
17233
17324
  lines.push("");
17234
- lines.push(headerLine);
17235
- lines.push(separator);
17236
- lines.push(...dataLines);
17325
+ lines.push(renderTable(headers, rows));
17237
17326
  return lines.join("\n");
17238
17327
  }
17239
17328
  function formatSummary(summary) {
@@ -17255,15 +17344,21 @@ function formatSummary(summary) {
17255
17344
  }
17256
17345
  async function handleBoardView(adapter2, args) {
17257
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);
17258
17351
  if (mode === "summary") {
17259
17352
  const summary = await viewBoardSummary(adapter2);
17260
- 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));
17261
17355
  }
17262
17356
  const taskIdArg = args.task_id ?? args.display_id;
17263
17357
  if (taskIdArg) {
17264
17358
  const task = await adapter2.getTask(taskIdArg);
17265
17359
  if (!task) return errorResponse(`Task ${taskIdArg} not found.`);
17266
- 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));
17267
17362
  }
17268
17363
  const result = await viewBoard(adapter2, void 0, {
17269
17364
  phase: args.phase,
@@ -17273,17 +17368,26 @@ async function handleBoardView(adapter2, args) {
17273
17368
  query: args.query,
17274
17369
  cycle: args.cycle
17275
17370
  });
17276
- let output = formatBoard(result);
17277
- try {
17278
- const comments = await adapter2.getRecentTaskComments?.(30);
17279
- if (comments && comments.length > 0) {
17280
- const taskIds = new Set(result.tasks.map((t) => t.id));
17281
- const section = formatTaskComments(comments, taskIds, "**Task Comments:**");
17282
- 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 {
17283
17381
  }
17284
- } catch {
17285
17382
  }
17286
- 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));
17287
17391
  }
17288
17392
  async function handleBoardDeprioritise(adapter2, args) {
17289
17393
  const taskId = args.task_id;
@@ -21530,7 +21634,7 @@ async function listBuilds(adapter2, config2) {
21530
21634
  ]);
21531
21635
  const currentCycle = health?.totalCycles ?? 0;
21532
21636
  if (tasks.length === 0) {
21533
- 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 };
21534
21638
  }
21535
21639
  const withHandoff = tasks.filter((t) => {
21536
21640
  if (!t.buildHandoff) return false;
@@ -21541,7 +21645,7 @@ async function listBuilds(adapter2, config2) {
21541
21645
  return true;
21542
21646
  });
21543
21647
  if (withHandoff.length === 0) {
21544
- 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 };
21545
21649
  }
21546
21650
  const buildable = [];
21547
21651
  const blocked = [];
@@ -21556,7 +21660,7 @@ async function listBuilds(adapter2, config2) {
21556
21660
  const inProgress = buildable.filter((t) => t.status === "In Progress");
21557
21661
  const backlog = buildable.filter((t) => t.status !== "In Progress");
21558
21662
  const sorted = [...inProgress, ...backlog];
21559
- 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 };
21560
21664
  }
21561
21665
  async function describeTask(adapter2, taskId) {
21562
21666
  const task = await adapter2.getTask(taskId);
@@ -22976,6 +23080,17 @@ async function handleDocReorder(adapter2, args) {
22976
23080
 
22977
23081
  // src/tools/build.ts
22978
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);
22979
23094
  var buildListTool = {
22980
23095
  name: "build_list",
22981
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.",
@@ -22985,7 +23100,16 @@ var buildListTool = {
22985
23100
  properties: {
22986
23101
  limit: {
22987
23102
  type: "integer",
22988
- 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."
22989
23113
  }
22990
23114
  },
22991
23115
  required: []
@@ -23250,50 +23374,74 @@ var AD_CONFLICT_GATE_INSTRUCTION = "\n**AD conflict gate:** If this task conflic
23250
23374
  function hasReportFields(args) {
23251
23375
  return !!(args.completed || args.effort || args.estimated_effort || args.surprises || args.discovered_issues || args.architecture_notes);
23252
23376
  }
23253
- 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);
23254
23385
  const result = await listBuilds(adapter2, config2);
23255
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;
23256
23388
  if (result.isEmpty) {
23257
23389
  if (result.currentCycle === 0) {
23258
- 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()));
23259
23391
  }
23260
- 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()));
23261
23393
  }
23262
23394
  if (result.noHandoffs) {
23263
- 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()));
23264
23396
  }
23397
+ const inProgress = cap(result.inProgress);
23398
+ const backlog = cap(result.backlog);
23399
+ const blocked = cap(result.blocked);
23265
23400
  const lines = [];
23266
23401
  if (warningPrefix) {
23267
23402
  lines.push(warningPrefix.trimEnd(), "");
23268
23403
  }
23269
23404
  const totalCount = result.sorted.length + result.blocked.length;
23270
23405
  lines.push(`**Cycle ${result.currentCycle}** \u2014 ${totalCount} tasks with BUILD HANDOFFs:`, "");
23271
- if (result.inProgress.length > 0) {
23272
- lines.push(`## In Progress (${result.inProgress.length})`);
23273
- lines.push(...result.inProgress.map(formatListItem));
23274
- lines.push("");
23275
- }
23276
- if (result.backlog.length > 0) {
23277
- lines.push(`## Backlog (${result.backlog.length})`);
23278
- lines.push(...result.backlog.map(formatListItem));
23279
- }
23280
- if (result.blocked.length > 0) {
23281
- lines.push("", `## Blocked (${result.blocked.length}) \u2014 waiting on dependencies`);
23282
- for (const { task, unresolvedDeps } of result.blocked) {
23283
- 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}
23284
23424
  Waiting on: ${unresolvedDeps.join(", ")}`);
23425
+ }
23285
23426
  }
23286
- }
23287
- try {
23288
- const comments = await adapter2.getRecentTaskComments?.(30);
23289
- if (comments && comments.length > 0) {
23290
- const taskIds = new Set([...result.sorted, ...result.blocked.map((b2) => b2.task)].map((t) => t.id));
23291
- const section = formatTaskComments(comments, taskIds);
23292
- 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 {
23293
23435
  }
23294
- } catch {
23295
23436
  }
23296
- 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));
23297
23445
  }
23298
23446
  async function handleBuildDescribe(adapter2, args) {
23299
23447
  const taskId = args.task_id;
@@ -28706,7 +28854,7 @@ ${section}`;
28706
28854
  }
28707
28855
  tracker.mark("format-summary");
28708
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`).*";
28709
- 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 };
28710
28858
  } catch (err) {
28711
28859
  const message = err instanceof Error ? err.message : String(err);
28712
28860
  const isKnownFriendly = /^(Orient failed|Project not found|No project|Setup required)/i.test(message);
@@ -29810,6 +29958,15 @@ function readLlmResponse(args) {
29810
29958
  }
29811
29959
 
29812
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);
29813
29970
  var adViewTool = {
29814
29971
  name: "ad_view",
29815
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.",
@@ -29824,6 +29981,24 @@ var adViewTool = {
29824
29981
  include_superseded: {
29825
29982
  type: "boolean",
29826
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."
29827
30002
  }
29828
30003
  },
29829
30004
  required: []
@@ -29832,6 +30007,14 @@ var adViewTool = {
29832
30007
  async function handleAdView(adapter2, args) {
29833
30008
  const adId = args.ad_id;
29834
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;
29835
30018
  let decisions;
29836
30019
  try {
29837
30020
  decisions = await adapter2.getActiveDecisions({ includeRetired: includeSuperseded });
@@ -29844,26 +30027,79 @@ async function handleAdView(adapter2, args) {
29844
30027
  const ids = decisions.map((d) => d.id).join(", ");
29845
30028
  return errorResponse(`AD not found: ${adId}. Available: ${ids || "none"}`);
29846
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
+ }
29847
30036
  const supersededNote = target.superseded ? ` [SUPERSEDED by ${target.supersededBy}]` : "";
29848
30037
  return textResponse(
29849
- `## ${target.id}: ${target.title} [${target.confidence}]${supersededNote}
30038
+ withMeta(`## ${target.id}: ${target.title} [${target.confidence}]${supersededNote}
29850
30039
 
29851
- ${target.body}`
30040
+ ${target.body}`, meta2)
29852
30041
  );
29853
30042
  }
29854
30043
  const filtered = includeSuperseded ? decisions : decisions.filter((d) => !d.superseded);
29855
- if (filtered.length === 0) {
29856
- 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));
29857
30053
  }
29858
- const formatted = filtered.map((d) => {
30054
+ const formatted = paged.map((d) => {
29859
30055
  const supersededNote = d.superseded ? ` [SUPERSEDED by ${d.supersededBy}]` : "";
29860
30056
  return `## ${d.id}: ${d.title} [${d.confidence}]${supersededNote}
29861
30057
 
29862
30058
  ${d.body}`;
29863
30059
  }).join("\n\n---\n\n");
29864
- return textResponse(`# Active Decisions (${filtered.length})
30060
+ return textResponse(withMeta(`# Active Decisions (${paged.length})
29865
30061
 
29866
- ${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
+ }
29867
30103
  }
29868
30104
 
29869
30105
  // src/tools/learning-action.ts
@@ -31160,7 +31396,7 @@ function createServer(adapter2, config2) {
31160
31396
  case "setup":
31161
31397
  return handleSetup(adapter2, config2, safeArgs, server2.getClientVersion()?.name);
31162
31398
  case "build_list":
31163
- return handleBuildList(adapter2, config2);
31399
+ return handleBuildList(adapter2, config2, safeArgs);
31164
31400
  case "build_describe":
31165
31401
  return handleBuildDescribe(adapter2, safeArgs);
31166
31402
  case "build_execute":
@@ -31289,9 +31525,17 @@ ${usageLine(decision.usage)}`;
31289
31525
  const usage = result._usage;
31290
31526
  const contextBytes = result._contextBytes;
31291
31527
  const contextUtilisation = result._contextUtilisation;
31528
+ const toolCycleNumber = result._cycleNumber;
31292
31529
  delete result._usage;
31293
31530
  delete result._contextBytes;
31294
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
+ });
31295
31539
  const isError = result.content.some((c) => c.text.startsWith("Error:") || c.text.startsWith("\u274C"));
31296
31540
  recordToolOutcome(!isError, callerKey);
31297
31541
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papi-ai/server",
3
- "version": "0.7.71",
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",