@papi-ai/server 0.7.71 → 0.7.73

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
  }
@@ -11207,6 +11214,55 @@ function formatBlockerWaiting(blocker) {
11207
11214
  }
11208
11215
  }
11209
11216
 
11217
+ // src/lib/tool-telemetry.ts
11218
+ var INSTRUMENTED_TOOLS = /* @__PURE__ */ new Set([
11219
+ "plan",
11220
+ "orient",
11221
+ "build_execute",
11222
+ "review_submit",
11223
+ "release",
11224
+ "strategy_review"
11225
+ ]);
11226
+ var WRAPPER_INSTRUMENTED_TOOLS = new Set(
11227
+ [...INSTRUMENTED_TOOLS].filter((t) => t !== "plan")
11228
+ );
11229
+ var PLAN_PREPARE_TOOL_NAME = "plan:prepare";
11230
+ function measureResultBytes(content) {
11231
+ let total = 0;
11232
+ for (const part of content) {
11233
+ if (typeof part.text === "string") total += Buffer.byteLength(part.text, "utf-8");
11234
+ }
11235
+ return total;
11236
+ }
11237
+ function recordToolRun(adapter2, sample) {
11238
+ if (sample.toolName !== PLAN_PREPARE_TOOL_NAME && !WRAPPER_INSTRUMENTED_TOOLS.has(sample.toolName)) return;
11239
+ if (typeof adapter2.insertToolRun !== "function") return;
11240
+ try {
11241
+ const promise = adapter2.insertToolRun({
11242
+ toolName: sample.toolName,
11243
+ contextBytes: sample.contextBytes,
11244
+ durationMs: sample.durationMs,
11245
+ cycleNumber: sample.cycleNumber ?? null,
11246
+ source: "mcp-server"
11247
+ });
11248
+ if (promise && typeof promise.catch === "function") {
11249
+ promise.catch((err) => {
11250
+ console.error(`[telemetry] insertToolRun failed (non-blocking): ${err instanceof Error ? err.message : String(err)}`);
11251
+ });
11252
+ }
11253
+ } catch (err) {
11254
+ console.error(`[telemetry] insertToolRun threw (non-blocking): ${err instanceof Error ? err.message : String(err)}`);
11255
+ }
11256
+ }
11257
+ function recordPlanPrepareRun(adapter2, sample) {
11258
+ recordToolRun(adapter2, {
11259
+ toolName: PLAN_PREPARE_TOOL_NAME,
11260
+ contextBytes: sample.contextBytes,
11261
+ durationMs: sample.durationMs,
11262
+ cycleNumber: sample.cycleNumber ?? null
11263
+ });
11264
+ }
11265
+
11210
11266
  // src/lib/visibility-inheritance.ts
11211
11267
  var TIER_RANK = {
11212
11268
  public: 0,
@@ -13000,6 +13056,7 @@ async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnl
13000
13056
  console.error(`[plan-perf] contextBytes=${contextBytes2} (handoffs-only)`);
13001
13057
  const planSystemPrompt2 = await getPrompt("plan-system");
13002
13058
  await recordPlanGenerationActive(tracker, incomingCycle);
13059
+ recordPlanPrepareRun(adapter2, { contextBytes: contextBytes2, durationMs: totalMs2, cycleNumber: incomingCycle });
13003
13060
  return {
13004
13061
  mode: "full",
13005
13062
  // apply phase treats it the same
@@ -13061,6 +13118,7 @@ async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnl
13061
13118
  }
13062
13119
  const planSystemPrompt = await getPrompt("plan-system");
13063
13120
  await recordPlanGenerationActive(tracker, incomingCycle);
13121
+ recordPlanPrepareRun(adapter2, { contextBytes, durationMs: totalMs, cycleNumber: incomingCycle });
13064
13122
  return {
13065
13123
  mode,
13066
13124
  cycleNumber,
@@ -16918,7 +16976,9 @@ async function viewBoard(adapter2, phaseFilter, options) {
16918
16976
  total,
16919
16977
  returned: paged.length,
16920
16978
  offset,
16921
- hasMore: offset + paged.length < total
16979
+ hasMore: offset + paged.length < total,
16980
+ totalUnfiltered: allTasks.length,
16981
+ limit
16922
16982
  };
16923
16983
  }
16924
16984
  async function viewBoardSummary(adapter2) {
@@ -16966,7 +17026,87 @@ async function archiveTasks(adapter2, phases, statuses) {
16966
17026
  return { archivedCount: result.archivedCount, phases };
16967
17027
  }
16968
17028
 
17029
+ // src/lib/projection.ts
17030
+ function resolveFields(raw, allowed, toolName) {
17031
+ if (raw === void 0 || raw === null) return { ok: true, fields: null };
17032
+ if (typeof raw !== "string") {
17033
+ return { ok: false, error: `${toolName}: fields must be a comma-separated string (got ${typeof raw}).` };
17034
+ }
17035
+ const trimmed = raw.trim();
17036
+ if (trimmed === "") return { ok: true, fields: null };
17037
+ const canonical = new Map(allowed.map((a) => [a.toLowerCase(), a]));
17038
+ const requested = [];
17039
+ for (const part of trimmed.split(",")) {
17040
+ const token = part.trim();
17041
+ if (token === "") continue;
17042
+ const hit = canonical.get(token.toLowerCase());
17043
+ if (!hit) {
17044
+ return {
17045
+ ok: false,
17046
+ error: `${toolName}: unknown field "${token}". Allowed fields: ${allowed.join(", ")}.`
17047
+ };
17048
+ }
17049
+ if (!requested.includes(hit)) requested.push(hit);
17050
+ }
17051
+ if (requested.length === 0) return { ok: true, fields: null };
17052
+ if (!requested.includes("id")) requested.unshift("id");
17053
+ return { ok: true, fields: requested };
17054
+ }
17055
+ function selectColumns(columns, fields) {
17056
+ if (!fields) return columns;
17057
+ const byName = new Map(columns.map((c) => [c.name, c]));
17058
+ return fields.map((f) => byName.get(f)).filter((c) => c !== void 0);
17059
+ }
17060
+ function wantsMeta(raw) {
17061
+ return raw === true || raw === "true";
17062
+ }
17063
+ function formatMeta(meta) {
17064
+ return `**meta:** ${JSON.stringify(meta)}`;
17065
+ }
17066
+ function withMeta(body, meta) {
17067
+ return meta ? `${body}
17068
+
17069
+ ${formatMeta(meta)}` : body;
17070
+ }
17071
+ function pad(value, width) {
17072
+ return value.length >= width ? value : value + " ".repeat(width - value.length);
17073
+ }
17074
+ function renderTable(headers, rows) {
17075
+ const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
17076
+ const headerLine = headers.map((h, i) => pad(h, widths[i])).join(" | ");
17077
+ const separator = widths.map((w) => "-".repeat(w)).join(" | ");
17078
+ const dataLines = rows.map((row) => row.map((cell, i) => pad(cell, widths[i])).join(" | "));
17079
+ return [headerLine, separator, ...dataLines].join("\n");
17080
+ }
17081
+
16969
17082
  // src/tools/board.ts
17083
+ var BOARD_COLUMNS = [
17084
+ { name: "priority", header: "Priority", value: (t) => t.priority },
17085
+ { name: "id", header: "Task", value: (t) => t.id },
17086
+ { name: "title", header: "Summary", value: (t) => truncateTitle(t.title) },
17087
+ { name: "status", header: "Status", value: (t) => t.status },
17088
+ { name: "cycle", header: "Cycle", value: (t) => t.cycle != null ? String(t.cycle) : "-" },
17089
+ { name: "phase", header: "Phase", value: (t) => t.phase ?? "-" },
17090
+ { name: "module", header: "Module", value: (t) => t.module ?? "-" },
17091
+ { name: "epic", header: "Epic", value: (t) => t.epic ?? "-" },
17092
+ { name: "complexity", header: "Effort", value: (t) => t.complexity ?? "-" },
17093
+ { name: "createdAt", header: "Created", value: (t) => t.createdAt ?? "-" },
17094
+ { name: "source", header: "Source", value: (t) => t.source ?? "-" }
17095
+ ];
17096
+ var BOARD_FIELDS = BOARD_COLUMNS.map((c) => c.name);
17097
+ var BOARD_SINGLE_LABELS = {
17098
+ id: "Task",
17099
+ title: "Title",
17100
+ status: "Status",
17101
+ priority: "Priority",
17102
+ cycle: "Cycle",
17103
+ phase: "Phase",
17104
+ module: "Module",
17105
+ epic: "Epic",
17106
+ complexity: "Effort",
17107
+ createdAt: "Created",
17108
+ source: "Source"
17109
+ };
16970
17110
  var boardViewTool = {
16971
17111
  name: "board_view",
16972
17112
  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 +17151,14 @@ var boardViewTool = {
17011
17151
  type: "string",
17012
17152
  enum: ["full", "summary"],
17013
17153
  description: 'Output mode: "full" (default) shows task table, "summary" shows counts only.'
17154
+ },
17155
+ fields: {
17156
+ type: "string",
17157
+ 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.`
17158
+ },
17159
+ meta: {
17160
+ type: "boolean",
17161
+ description: "Append a {total, filtered, returned, limit, offset} envelope so you can tell a truncated result from a complete one. Default false."
17014
17162
  }
17015
17163
  },
17016
17164
  required: []
@@ -17175,14 +17323,27 @@ var boardEditTool = {
17175
17323
  required: ["task_id"]
17176
17324
  }
17177
17325
  };
17178
- function pad(value, width) {
17179
- return value.length >= width ? value : value + " ".repeat(width - value.length);
17180
- }
17181
17326
  var TITLE_MAX = 80;
17182
17327
  function truncateTitle(title) {
17183
17328
  return title.length > TITLE_MAX ? `${title.slice(0, TITLE_MAX - 1)}\u2026` : title;
17184
17329
  }
17185
- function formatSingleTask(t) {
17330
+ function formatSingleTask(t, fields) {
17331
+ if (fields) {
17332
+ const values2 = {
17333
+ id: t.id,
17334
+ title: t.title,
17335
+ status: t.status,
17336
+ priority: t.priority,
17337
+ cycle: t.cycle != null ? String(t.cycle) : "-",
17338
+ phase: t.phase ?? "-",
17339
+ module: t.module ?? "-",
17340
+ epic: t.epic ?? "-",
17341
+ complexity: t.complexity ?? "-",
17342
+ createdAt: t.createdAt ?? "-",
17343
+ source: t.source ?? "-"
17344
+ };
17345
+ return fields.map((f) => `- **${BOARD_SINGLE_LABELS[f] ?? f}:** ${values2[f]}`).join("\n");
17346
+ }
17186
17347
  const lines = [
17187
17348
  `**${t.id} \u2014 ${t.title}**`,
17188
17349
  "",
@@ -17199,41 +17360,20 @@ function formatSingleTask(t) {
17199
17360
  if (t.why?.trim()) lines.push(`- **Why:** ${t.why.trim()}`);
17200
17361
  return lines.join("\n");
17201
17362
  }
17202
- function formatBoard(result) {
17363
+ function formatBoard(result, fields) {
17203
17364
  if (result.tasks.length === 0) {
17204
17365
  return "No tasks found.";
17205
17366
  }
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
- );
17367
+ const columns = selectColumns(BOARD_COLUMNS, fields);
17368
+ const headers = columns.map((c) => c.header);
17369
+ const rows = result.tasks.map((t) => columns.map((c) => c.value(t)));
17228
17370
  const lines = [];
17229
17371
  lines.push(`**${result.returned} of ${result.total} tasks**`);
17230
17372
  if (result.hasMore) {
17231
17373
  lines.push(`_Showing ${result.offset + 1}\u2013${result.offset + result.returned}. Use offset=${result.offset + result.returned} to see more._`);
17232
17374
  }
17233
17375
  lines.push("");
17234
- lines.push(headerLine);
17235
- lines.push(separator);
17236
- lines.push(...dataLines);
17376
+ lines.push(renderTable(headers, rows));
17237
17377
  return lines.join("\n");
17238
17378
  }
17239
17379
  function formatSummary(summary) {
@@ -17255,15 +17395,21 @@ function formatSummary(summary) {
17255
17395
  }
17256
17396
  async function handleBoardView(adapter2, args) {
17257
17397
  const mode = args.mode;
17398
+ const projection = resolveFields(args.fields, BOARD_FIELDS, "board_view");
17399
+ if (!projection.ok) return errorResponse(projection.error);
17400
+ const fields = projection.fields;
17401
+ const emitMeta = wantsMeta(args.meta);
17258
17402
  if (mode === "summary") {
17259
17403
  const summary = await viewBoardSummary(adapter2);
17260
- return textResponse(formatSummary(summary));
17404
+ const meta2 = emitMeta ? { total: summary.total, filtered: summary.total, returned: summary.total, limit: null, offset: 0 } : null;
17405
+ return textResponse(withMeta(formatSummary(summary), meta2));
17261
17406
  }
17262
17407
  const taskIdArg = args.task_id ?? args.display_id;
17263
17408
  if (taskIdArg) {
17264
17409
  const task = await adapter2.getTask(taskIdArg);
17265
17410
  if (!task) return errorResponse(`Task ${taskIdArg} not found.`);
17266
- return textResponse(formatSingleTask(task));
17411
+ const meta2 = emitMeta ? { total: 1, filtered: 1, returned: 1, limit: null, offset: 0 } : null;
17412
+ return textResponse(withMeta(formatSingleTask(task, fields), meta2));
17267
17413
  }
17268
17414
  const result = await viewBoard(adapter2, void 0, {
17269
17415
  phase: args.phase,
@@ -17273,17 +17419,26 @@ async function handleBoardView(adapter2, args) {
17273
17419
  query: args.query,
17274
17420
  cycle: args.cycle
17275
17421
  });
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;
17422
+ let output = formatBoard(result, fields);
17423
+ if (!fields) {
17424
+ try {
17425
+ const comments = await adapter2.getRecentTaskComments?.(30);
17426
+ if (comments && comments.length > 0) {
17427
+ const taskIds = new Set(result.tasks.map((t) => t.id));
17428
+ const section = formatTaskComments(comments, taskIds, "**Task Comments:**");
17429
+ if (section) output += "\n" + section;
17430
+ }
17431
+ } catch {
17283
17432
  }
17284
- } catch {
17285
17433
  }
17286
- return textResponse(output);
17434
+ const meta = emitMeta ? {
17435
+ total: result.totalUnfiltered,
17436
+ filtered: result.total,
17437
+ returned: result.returned,
17438
+ limit: result.limit,
17439
+ offset: result.offset
17440
+ } : null;
17441
+ return textResponse(withMeta(output, meta));
17287
17442
  }
17288
17443
  async function handleBoardDeprioritise(adapter2, args) {
17289
17444
  const taskId = args.task_id;
@@ -21530,7 +21685,7 @@ async function listBuilds(adapter2, config2) {
21530
21685
  ]);
21531
21686
  const currentCycle = health?.totalCycles ?? 0;
21532
21687
  if (tasks.length === 0) {
21533
- return { sorted: [], inProgress: [], backlog: [], blocked: [], warnings, isEmpty: true, noHandoffs: false, currentCycle };
21688
+ return { sorted: [], inProgress: [], backlog: [], blocked: [], warnings, isEmpty: true, noHandoffs: false, currentCycle, totalTasks: tasks.length };
21534
21689
  }
21535
21690
  const withHandoff = tasks.filter((t) => {
21536
21691
  if (!t.buildHandoff) return false;
@@ -21541,7 +21696,7 @@ async function listBuilds(adapter2, config2) {
21541
21696
  return true;
21542
21697
  });
21543
21698
  if (withHandoff.length === 0) {
21544
- return { sorted: [], inProgress: [], backlog: [], blocked: [], warnings, isEmpty: false, noHandoffs: true, currentCycle };
21699
+ return { sorted: [], inProgress: [], backlog: [], blocked: [], warnings, isEmpty: false, noHandoffs: true, currentCycle, totalTasks: tasks.length };
21545
21700
  }
21546
21701
  const buildable = [];
21547
21702
  const blocked = [];
@@ -21556,7 +21711,7 @@ async function listBuilds(adapter2, config2) {
21556
21711
  const inProgress = buildable.filter((t) => t.status === "In Progress");
21557
21712
  const backlog = buildable.filter((t) => t.status !== "In Progress");
21558
21713
  const sorted = [...inProgress, ...backlog];
21559
- return { sorted, inProgress, backlog, blocked, warnings, isEmpty: false, noHandoffs: sorted.length === 0 && blocked.length === 0, currentCycle };
21714
+ return { sorted, inProgress, backlog, blocked, warnings, isEmpty: false, noHandoffs: sorted.length === 0 && blocked.length === 0, currentCycle, totalTasks: tasks.length };
21560
21715
  }
21561
21716
  async function describeTask(adapter2, taskId) {
21562
21717
  const task = await adapter2.getTask(taskId);
@@ -22976,6 +23131,17 @@ async function handleDocReorder(adapter2, args) {
22976
23131
 
22977
23132
  // src/tools/build.ts
22978
23133
  init_git();
23134
+ var BUILD_LIST_COLUMNS = [
23135
+ { name: "id", header: "Task", value: (t) => t.id },
23136
+ { name: "title", header: "Summary", value: (t) => t.title },
23137
+ { name: "status", header: "Status", value: (t) => t.status },
23138
+ { name: "priority", header: "Priority", value: (t) => t.priority },
23139
+ { name: "complexity", header: "Effort", value: (t) => t.complexity ?? "-" },
23140
+ { name: "module", header: "Module", value: (t) => t.module ?? "-" },
23141
+ { name: "epic", header: "Epic", value: (t) => t.epic ?? "-" },
23142
+ { name: "cycle", header: "Cycle", value: (t) => t.cycle != null ? String(t.cycle) : "-" }
23143
+ ];
23144
+ var BUILD_LIST_FIELDS = BUILD_LIST_COLUMNS.map((c) => c.name);
22979
23145
  var buildListTool = {
22980
23146
  name: "build_list",
22981
23147
  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 +23151,16 @@ var buildListTool = {
22985
23151
  properties: {
22986
23152
  limit: {
22987
23153
  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."
23154
+ minimum: 1,
23155
+ description: "Optional maximum number of tasks to return per section (In Progress / Backlog / Blocked). Omit to return all."
23156
+ },
23157
+ fields: {
23158
+ type: "string",
23159
+ 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.`
23160
+ },
23161
+ meta: {
23162
+ type: "boolean",
23163
+ description: "Append a {total, filtered, returned, limit, offset} envelope so you can tell a truncated result from a complete one. Default false."
22989
23164
  }
22990
23165
  },
22991
23166
  required: []
@@ -23250,50 +23425,74 @@ var AD_CONFLICT_GATE_INSTRUCTION = "\n**AD conflict gate:** If this task conflic
23250
23425
  function hasReportFields(args) {
23251
23426
  return !!(args.completed || args.effort || args.estimated_effort || args.surprises || args.discovered_issues || args.architecture_notes);
23252
23427
  }
23253
- async function handleBuildList(adapter2, config2) {
23428
+ async function handleBuildList(adapter2, config2, args = {}) {
23429
+ const projection = resolveFields(args.fields, BUILD_LIST_FIELDS, "build_list");
23430
+ if (!projection.ok) return errorResponse(projection.error);
23431
+ const fields = projection.fields;
23432
+ const emitMeta = wantsMeta(args.meta);
23433
+ const rawLimit = args.limit;
23434
+ const limit = typeof rawLimit === "number" && Number.isInteger(rawLimit) && rawLimit > 0 ? rawLimit : null;
23435
+ const cap = (rows) => limit === null ? rows : rows.slice(0, limit);
23254
23436
  const result = await listBuilds(adapter2, config2);
23255
23437
  const warningPrefix = result.warnings.length > 0 ? result.warnings.map((w) => `> ${w}`).join("\n") + "\n\n" : "";
23438
+ const emptyMeta = () => emitMeta ? { total: result.totalTasks, filtered: 0, returned: 0, limit, offset: 0 } : null;
23256
23439
  if (result.isEmpty) {
23257
23440
  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.`);
23441
+ 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
23442
  }
23260
- return textResponse(warningPrefix + `**Cycle ${result.currentCycle}** \u2014 Board is empty. Run \`plan\` to create your next cycle.`);
23443
+ return textResponse(withMeta(warningPrefix + `**Cycle ${result.currentCycle}** \u2014 Board is empty. Run \`plan\` to create your next cycle.`, emptyMeta()));
23261
23444
  }
23262
23445
  if (result.noHandoffs) {
23263
- return textResponse(warningPrefix + `**Cycle ${result.currentCycle}** \u2014 No tasks with BUILD HANDOFFs. Run \`plan\` to generate your next cycle tasks.`);
23446
+ return textResponse(withMeta(warningPrefix + `**Cycle ${result.currentCycle}** \u2014 No tasks with BUILD HANDOFFs. Run \`plan\` to generate your next cycle tasks.`, emptyMeta()));
23264
23447
  }
23448
+ const inProgress = cap(result.inProgress);
23449
+ const backlog = cap(result.backlog);
23450
+ const blocked = cap(result.blocked);
23265
23451
  const lines = [];
23266
23452
  if (warningPrefix) {
23267
23453
  lines.push(warningPrefix.trimEnd(), "");
23268
23454
  }
23269
23455
  const totalCount = result.sorted.length + result.blocked.length;
23270
23456
  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}
23457
+ if (fields) {
23458
+ const columns = selectColumns(BUILD_LIST_COLUMNS, fields);
23459
+ const rows = [...inProgress, ...backlog, ...blocked.map((b2) => b2.task)].map((t) => columns.map((c) => c.value(t)));
23460
+ lines.push(renderTable(columns.map((c) => c.header), rows));
23461
+ } else {
23462
+ if (inProgress.length > 0) {
23463
+ lines.push(`## In Progress (${result.inProgress.length})`);
23464
+ lines.push(...inProgress.map(formatListItem));
23465
+ lines.push("");
23466
+ }
23467
+ if (backlog.length > 0) {
23468
+ lines.push(`## Backlog (${result.backlog.length})`);
23469
+ lines.push(...backlog.map(formatListItem));
23470
+ }
23471
+ if (blocked.length > 0) {
23472
+ lines.push("", `## Blocked (${result.blocked.length}) \u2014 waiting on dependencies`);
23473
+ for (const { task, unresolvedDeps } of blocked) {
23474
+ lines.push(`- **${task.id}:** ${task.title}
23284
23475
  Waiting on: ${unresolvedDeps.join(", ")}`);
23476
+ }
23285
23477
  }
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);
23478
+ try {
23479
+ const comments = await adapter2.getRecentTaskComments?.(30);
23480
+ if (comments && comments.length > 0) {
23481
+ const taskIds = new Set([...result.sorted, ...result.blocked.map((b2) => b2.task)].map((t) => t.id));
23482
+ const section = formatTaskComments(comments, taskIds);
23483
+ if (section) lines.push(section);
23484
+ }
23485
+ } catch {
23293
23486
  }
23294
- } catch {
23295
23487
  }
23296
- return textResponse(lines.join("\n"));
23488
+ const meta = emitMeta ? {
23489
+ total: result.totalTasks,
23490
+ filtered: totalCount,
23491
+ returned: inProgress.length + backlog.length + blocked.length,
23492
+ limit,
23493
+ offset: 0
23494
+ } : null;
23495
+ return textResponse(withMeta(lines.join("\n"), meta));
23297
23496
  }
23298
23497
  async function handleBuildDescribe(adapter2, args) {
23299
23498
  const taskId = args.task_id;
@@ -28706,7 +28905,7 @@ ${section}`;
28706
28905
  }
28707
28906
  tracker.mark("format-summary");
28708
28907
  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);
28908
+ 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
28909
  } catch (err) {
28711
28910
  const message = err instanceof Error ? err.message : String(err);
28712
28911
  const isKnownFriendly = /^(Orient failed|Project not found|No project|Setup required)/i.test(message);
@@ -29810,6 +30009,15 @@ function readLlmResponse(args) {
29810
30009
  }
29811
30010
 
29812
30011
  // src/tools/ad-view.ts
30012
+ var AD_COLUMNS = [
30013
+ { name: "id", header: "AD", value: (d) => d.id },
30014
+ { name: "title", header: "Title", value: (d) => d.title },
30015
+ { name: "confidence", header: "Confidence", value: (d) => d.confidence },
30016
+ { name: "superseded", header: "Superseded", value: (d) => d.superseded ? "yes" : "no" },
30017
+ { name: "supersededBy", header: "Superseded By", value: (d) => d.supersededBy ?? "-" },
30018
+ { name: "body", header: "Body", value: (d) => d.body.replace(/\s+/g, " ").trim() }
30019
+ ];
30020
+ var AD_FIELDS = AD_COLUMNS.map((c) => c.name);
29813
30021
  var adViewTool = {
29814
30022
  name: "ad_view",
29815
30023
  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 +30032,24 @@ var adViewTool = {
29824
30032
  include_superseded: {
29825
30033
  type: "boolean",
29826
30034
  description: "Include superseded (retired) ADs. Default false."
30035
+ },
30036
+ fields: {
30037
+ type: "string",
30038
+ 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.`
30039
+ },
30040
+ limit: {
30041
+ type: "integer",
30042
+ minimum: 1,
30043
+ description: "Max decisions to return. Omit to return all."
30044
+ },
30045
+ offset: {
30046
+ type: "integer",
30047
+ minimum: 0,
30048
+ description: "Skip the first N decisions (pagination). Default 0."
30049
+ },
30050
+ meta: {
30051
+ type: "boolean",
30052
+ description: "Append a {total, filtered, returned, limit, offset} envelope so you can tell a truncated result from a complete one. Default false."
29827
30053
  }
29828
30054
  },
29829
30055
  required: []
@@ -29832,6 +30058,14 @@ var adViewTool = {
29832
30058
  async function handleAdView(adapter2, args) {
29833
30059
  const adId = args.ad_id;
29834
30060
  const includeSuperseded = args.include_superseded === true;
30061
+ const projection = resolveFields(args.fields, AD_FIELDS, "ad_view");
30062
+ if (!projection.ok) return errorResponse(projection.error);
30063
+ const fields = projection.fields;
30064
+ const emitMeta = wantsMeta(args.meta);
30065
+ const rawLimit = args.limit;
30066
+ const limit = typeof rawLimit === "number" && Number.isInteger(rawLimit) && rawLimit > 0 ? rawLimit : null;
30067
+ const rawOffset = args.offset;
30068
+ const offset = typeof rawOffset === "number" && Number.isInteger(rawOffset) && rawOffset > 0 ? rawOffset : 0;
29835
30069
  let decisions;
29836
30070
  try {
29837
30071
  decisions = await adapter2.getActiveDecisions({ includeRetired: includeSuperseded });
@@ -29844,26 +30078,39 @@ async function handleAdView(adapter2, args) {
29844
30078
  const ids = decisions.map((d) => d.id).join(", ");
29845
30079
  return errorResponse(`AD not found: ${adId}. Available: ${ids || "none"}`);
29846
30080
  }
30081
+ const meta2 = emitMeta ? { total: decisions.length, filtered: 1, returned: 1, limit: null, offset: 0 } : null;
30082
+ if (fields) {
30083
+ const columns = selectColumns(AD_COLUMNS, fields);
30084
+ const body = columns.map((c) => `- **${c.header}:** ${c.value(target)}`).join("\n");
30085
+ return textResponse(withMeta(body, meta2));
30086
+ }
29847
30087
  const supersededNote = target.superseded ? ` [SUPERSEDED by ${target.supersededBy}]` : "";
29848
30088
  return textResponse(
29849
- `## ${target.id}: ${target.title} [${target.confidence}]${supersededNote}
30089
+ withMeta(`## ${target.id}: ${target.title} [${target.confidence}]${supersededNote}
29850
30090
 
29851
- ${target.body}`
30091
+ ${target.body}`, meta2)
29852
30092
  );
29853
30093
  }
29854
30094
  const filtered = includeSuperseded ? decisions : decisions.filter((d) => !d.superseded);
29855
- if (filtered.length === 0) {
29856
- return textResponse("No active decisions found.");
30095
+ const paged = limit === null && offset === 0 ? filtered : filtered.slice(offset, limit === null ? void 0 : offset + limit);
30096
+ const meta = emitMeta ? { total: decisions.length, filtered: filtered.length, returned: paged.length, limit, offset } : null;
30097
+ if (paged.length === 0) {
30098
+ return textResponse(withMeta("No active decisions found.", meta));
29857
30099
  }
29858
- const formatted = filtered.map((d) => {
30100
+ if (fields) {
30101
+ const columns = selectColumns(AD_COLUMNS, fields);
30102
+ const rows = paged.map((d) => columns.map((c) => c.value(d)));
30103
+ return textResponse(withMeta(renderTable(columns.map((c) => c.header), rows), meta));
30104
+ }
30105
+ const formatted = paged.map((d) => {
29859
30106
  const supersededNote = d.superseded ? ` [SUPERSEDED by ${d.supersededBy}]` : "";
29860
30107
  return `## ${d.id}: ${d.title} [${d.confidence}]${supersededNote}
29861
30108
 
29862
30109
  ${d.body}`;
29863
30110
  }).join("\n\n---\n\n");
29864
- return textResponse(`# Active Decisions (${filtered.length})
30111
+ return textResponse(withMeta(`# Active Decisions (${paged.length})
29865
30112
 
29866
- ${formatted}`);
30113
+ ${formatted}`, meta));
29867
30114
  }
29868
30115
 
29869
30116
  // src/tools/learning-action.ts
@@ -31160,7 +31407,7 @@ function createServer(adapter2, config2) {
31160
31407
  case "setup":
31161
31408
  return handleSetup(adapter2, config2, safeArgs, server2.getClientVersion()?.name);
31162
31409
  case "build_list":
31163
- return handleBuildList(adapter2, config2);
31410
+ return handleBuildList(adapter2, config2, safeArgs);
31164
31411
  case "build_describe":
31165
31412
  return handleBuildDescribe(adapter2, safeArgs);
31166
31413
  case "build_execute":
@@ -31289,9 +31536,17 @@ ${usageLine(decision.usage)}`;
31289
31536
  const usage = result._usage;
31290
31537
  const contextBytes = result._contextBytes;
31291
31538
  const contextUtilisation = result._contextUtilisation;
31539
+ const toolCycleNumber = result._cycleNumber;
31292
31540
  delete result._usage;
31293
31541
  delete result._contextBytes;
31294
31542
  delete result._contextUtilisation;
31543
+ delete result._cycleNumber;
31544
+ recordToolRun(adapter2, {
31545
+ toolName: name,
31546
+ contextBytes: measureResultBytes(result.content),
31547
+ durationMs: elapsed,
31548
+ cycleNumber: toolCycleNumber ?? null
31549
+ });
31295
31550
  const isError = result.content.some((c) => c.text.startsWith("Error:") || c.text.startsWith("\u274C"));
31296
31551
  recordToolOutcome(!isError, callerKey);
31297
31552
  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.73",
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",