yapout 0.1.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +513 -202
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -245,17 +245,46 @@ def main():
245
245
  )
246
246
  elif action == "enrich":
247
247
  prompt = (
248
- f'A ticket has been approved and needs enrichment before implementation. '
249
- f'Call yapout_get_unenriched_ticket with ticketId for "{ticket_id}" to fetch the ticket details.\\n\\n'
250
- f'Read the codebase to understand the project structure, then produce:\\n'
251
- f'1. An implementation brief (3-5 sentences, specific files/patterns involved)\\n'
252
- f'2. An enriched description with technical context\\n'
253
- f"3. Clarifying questions (0-5) where the answer can't be inferred\\n"
254
- f'4. Duplicate check \u2014 call yapout_get_existing_tickets and compare\\n'
255
- f'5. Scope assessment (is this too large for one PR?)\\n\\n'
256
- f'Call yapout_save_enrichment with your analysis. '
257
- f'If no questions were generated, also call yapout_sync_to_linear to create the Linear ticket.'
248
+ f'Use yapout to enrich a finding.\\n'
249
+ f'\\n'
250
+ f'Start with filter: {{ findingIds: ["{ticket_id}"] }}\\n'
251
+ f'\\n'
252
+ f'Call yapout_start_enrichment, then yapout_enrich_next to claim it.\\n'
253
+ f'Read the codebase, ask me questions if needed, then call yapout_save_enrichment\\n'
254
+ f'with a clean description, acceptance criteria, and implementation brief.'
258
255
  )
256
+ elif action == "enrich-bulk":
257
+ tags = qs.get("tags", [""])[0]
258
+ finding_ids = qs.get("findingIds", [""])[0]
259
+ lines = [
260
+ "Use yapout to enrich findings for this project. Start a bulk enrichment session and work through each finding one by one.",
261
+ "",
262
+ "For each finding:",
263
+ "1. Call yapout_start_enrichment to begin" + (" with the filter below" if (tags or finding_ids) else ""),
264
+ "2. Call yapout_enrich_next to get the next finding",
265
+ "3. Read relevant code in the repository to understand the finding's context",
266
+ "4. Ask me any clarifying questions (only if genuinely needed)",
267
+ "5. Call yapout_save_enrichment with:",
268
+ " - A clean, specific title",
269
+ " - A description a senior engineer would write",
270
+ " - Concrete acceptance criteria",
271
+ " - An implementation brief (which files, approach, edge cases)",
272
+ '6. If I say "skip", call yapout_enrich_next with skip=true',
273
+ "7. After saving, if compactionHint is true or every 5 findings, run /compact",
274
+ "8. Repeat until done",
275
+ "",
276
+ "Keep the pace steady. Don't over-ask \u2014 use your judgment from the code.",
277
+ ]
278
+ if finding_ids:
279
+ ids = [fid.strip() for fid in finding_ids.split(",") if fid.strip()]
280
+ lines.append("")
281
+ lines.append("These findings are bundled \u2014 enrich them as one cohesive problem.")
282
+ lines.append(f'Start with filter: {{ findingIds: {json.dumps(ids)} }}')
283
+ elif tags:
284
+ tag_list = [t.strip() for t in tags.split(",") if t.strip()]
285
+ lines.append("")
286
+ lines.append(f'Start with filter: {{ tags: {json.dumps(tag_list)} }}')
287
+ prompt = "\\n".join(lines)
259
288
  elif action == "yap":
260
289
  prompt = "Let's have a yap session"
261
290
  if topic:
@@ -947,7 +976,7 @@ Read the following to build your understanding:
947
976
  - Database schema or type definitions
948
977
  - A few representative source files
949
978
 
950
- Output a single markdown document. Be concise \u2014 this will be given to other Claude Code sessions as context for implementing tickets.
979
+ Output a single markdown document. Be concise \u2014 this will be given to other Claude Code sessions as context for implementing findings.
951
980
 
952
981
  After you produce the summary, call yapout_update_context with the summary text to save it.`;
953
982
  function registerCompactTool(server, ctx) {
@@ -1037,9 +1066,9 @@ import { z as z3 } from "zod";
1037
1066
  function registerQueueTool(server, ctx) {
1038
1067
  server.tool(
1039
1068
  "yapout_queue",
1040
- "List tickets ready for local implementation. Only returns tickets in backlog/unstarted Linear status.",
1069
+ "List findings ready for local implementation. Only returns findings in backlog/unstarted Linear status.",
1041
1070
  {
1042
- includeBlocked: z3.boolean().optional().describe("Show blocked tickets too (default: false)")
1071
+ includeBlocked: z3.boolean().optional().describe("Show blocked findings too (default: false)")
1043
1072
  },
1044
1073
  async (args) => {
1045
1074
  if (!ctx.projectId) {
@@ -1054,7 +1083,7 @@ function registerQueueTool(server, ctx) {
1054
1083
  };
1055
1084
  }
1056
1085
  const data = await ctx.client.query(
1057
- anyApi2.functions.tickets.getLocalQueuedTickets,
1086
+ anyApi2.functions.findings.getQueuedWorkItems,
1058
1087
  { projectId: ctx.projectId }
1059
1088
  );
1060
1089
  if (!data) {
@@ -1067,7 +1096,7 @@ function registerQueueTool(server, ctx) {
1067
1096
  }
1068
1097
  let ready = data.ready;
1069
1098
  ready = ready.filter((t) => t.nature !== "operational");
1070
- const linearIds = ready.map((t) => t.linearTicketId).filter((id) => !!id);
1099
+ const linearIds = ready.map((t) => t.linearIssueId).filter((id) => !!id);
1071
1100
  if (linearIds.length > 0) {
1072
1101
  try {
1073
1102
  const statuses = await ctx.client.action(
@@ -1078,8 +1107,8 @@ function registerQueueTool(server, ctx) {
1078
1107
  statuses.map((s) => [s.linearIssueId, s.statusType])
1079
1108
  );
1080
1109
  ready = ready.filter((t) => {
1081
- if (!t.linearTicketId) return true;
1082
- const type = statusMap.get(t.linearTicketId);
1110
+ if (!t.linearIssueId) return true;
1111
+ const type = statusMap.get(t.linearIssueId);
1083
1112
  if (!type) return true;
1084
1113
  return type === "backlog" || type === "unstarted";
1085
1114
  });
@@ -1114,21 +1143,21 @@ import { z as z4 } from "zod";
1114
1143
  function registerGetBriefTool(server, ctx) {
1115
1144
  server.tool(
1116
1145
  "yapout_get_brief",
1117
- "Fetch the full implementation context for a ticket",
1146
+ "Fetch the full implementation context for a finding",
1118
1147
  {
1119
- ticketId: z4.string().describe("The ticket ID to get the brief for")
1148
+ findingId: z4.string().describe("The finding ID to get the brief for")
1120
1149
  },
1121
1150
  async (args) => {
1122
1151
  const data = await ctx.client.query(
1123
- anyApi2.functions.tickets.getTicketBrief,
1124
- { ticketId: args.ticketId }
1152
+ anyApi2.functions.findings.getFindingBrief,
1153
+ { findingId: args.findingId }
1125
1154
  );
1126
1155
  if (!data) {
1127
1156
  return {
1128
1157
  content: [
1129
1158
  {
1130
1159
  type: "text",
1131
- text: "Ticket not found or you don't have access."
1160
+ text: "Finding not found or you don't have access."
1132
1161
  }
1133
1162
  ],
1134
1163
  isError: true
@@ -1159,16 +1188,16 @@ function slugify(text) {
1159
1188
  return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
1160
1189
  }
1161
1190
  function formatBrief(data) {
1162
- const ticket = data.ticket;
1191
+ const finding = data.finding;
1163
1192
  const sections = [
1164
- `# ${ticket.title}`,
1193
+ `# ${finding.title}`,
1165
1194
  "",
1166
- `**Priority:** ${ticket.priority} | **Type:** ${ticket.type}`
1195
+ `**Priority:** ${finding.priority} | **Type:** ${finding.type}`
1167
1196
  ];
1168
- if (ticket.linearUrl) {
1169
- sections.push(`**Linear:** ${ticket.linearUrl}`);
1197
+ if (finding.linearIssueUrl) {
1198
+ sections.push(`**Linear:** ${finding.linearIssueUrl}`);
1170
1199
  }
1171
- sections.push("", "## Description", "", ticket.description);
1200
+ sections.push("", "## Description", "", finding.description);
1172
1201
  if (data.enrichedDescription) {
1173
1202
  sections.push("", "## Enriched Description", "", data.enrichedDescription);
1174
1203
  }
@@ -1202,9 +1231,9 @@ function formatBrief(data) {
1202
1231
  function registerClaimTool(server, ctx) {
1203
1232
  server.tool(
1204
1233
  "yapout_claim",
1205
- "Claim a ticket for local implementation. Creates a branch (or worktree), writes the brief, and updates status.",
1234
+ "Claim a finding for local implementation. Creates a branch (or worktree), writes the brief, and updates status.",
1206
1235
  {
1207
- ticketId: z5.string().describe("The ticket ID to claim"),
1236
+ findingId: z5.string().describe("The finding ID to claim"),
1208
1237
  worktree: z5.boolean().optional().describe("Create a git worktree for parallel work (default: false)")
1209
1238
  },
1210
1239
  async (args) => {
@@ -1220,37 +1249,37 @@ function registerClaimTool(server, ctx) {
1220
1249
  };
1221
1250
  }
1222
1251
  const briefData = await ctx.client.query(
1223
- anyApi2.functions.tickets.getTicketBrief,
1224
- { ticketId: args.ticketId }
1252
+ anyApi2.functions.findings.getFindingBrief,
1253
+ { findingId: args.findingId }
1225
1254
  );
1226
1255
  if (!briefData) {
1227
1256
  return {
1228
1257
  content: [
1229
1258
  {
1230
1259
  type: "text",
1231
- text: "Ticket not found or you don't have access."
1260
+ text: "Finding not found or you don't have access."
1232
1261
  }
1233
1262
  ],
1234
1263
  isError: true
1235
1264
  };
1236
1265
  }
1237
- const ticket = briefData.ticket;
1238
- const linearTicketId = briefData.linearTicketId;
1266
+ const finding = briefData.finding;
1267
+ const linearIssueId = briefData.linearIssueId;
1239
1268
  const defaultBranch = briefData.defaultBranch || "main";
1240
1269
  const prefix = readBranchPrefix(ctx.cwd);
1241
- const slug = slugify(ticket.title);
1242
- const branchName = linearTicketId ? `${prefix}/${linearTicketId.toLowerCase()}-${slug}` : `${prefix}/${slug}`;
1270
+ const slug = slugify(finding.title);
1271
+ const branchName = linearIssueId ? `${prefix}/${linearIssueId.toLowerCase()}-${slug}` : `${prefix}/${slug}`;
1243
1272
  const claim = await ctx.client.mutation(
1244
- anyApi2.functions.tickets.claimTicketLocal,
1245
- { ticketId: args.ticketId, branchName }
1273
+ anyApi2.functions.findings.claimFindingLocal,
1274
+ { findingId: args.findingId, branchName }
1246
1275
  );
1247
- if (linearTicketId && ctx.projectId) {
1276
+ if (linearIssueId && ctx.projectId) {
1248
1277
  try {
1249
1278
  await ctx.client.action(
1250
1279
  anyApi2.functions.linearStatusMutations.moveIssueStatus,
1251
1280
  {
1252
1281
  projectId: ctx.projectId,
1253
- linearIssueId: linearTicketId,
1282
+ linearIssueId,
1254
1283
  statusType: "started"
1255
1284
  }
1256
1285
  );
@@ -1262,7 +1291,7 @@ function registerClaimTool(server, ctx) {
1262
1291
  fetchOrigin(ctx.cwd);
1263
1292
  worktreePath = createWorktree(
1264
1293
  ctx.cwd,
1265
- args.ticketId,
1294
+ args.findingId,
1266
1295
  branchName,
1267
1296
  defaultBranch
1268
1297
  );
@@ -1312,7 +1341,7 @@ function registerClaimTool(server, ctx) {
1312
1341
  {
1313
1342
  pipelineRunId: claim.pipelineRunId,
1314
1343
  event: "daemon_claimed",
1315
- message: `Claimed ticket: ${ticket.title}`
1344
+ message: `Claimed finding: ${finding.title}`
1316
1345
  }
1317
1346
  );
1318
1347
  await ctx.client.mutation(
@@ -1456,19 +1485,19 @@ async function createPullRequest(title, body, branch, base, repoFullName, cwd) {
1456
1485
  // src/mcp/tools/ship.ts
1457
1486
  import { join as join8 } from "path";
1458
1487
  import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
1459
- function buildCommitMessage(message, template, ticket) {
1488
+ function buildCommitMessage(message, template, finding) {
1460
1489
  if (message) return message;
1461
1490
  if (template) {
1462
- return template.replace(/\{\{ticket\.type\}\}/g, ticket.type).replace(/\{\{ticket\.title\}\}/g, ticket.title).replace(/\{\{ticket\.linearTicketId\}\}/g, ticket.linearTicketId ?? "draft").replace(/\{\{ticket\.id\}\}/g, ticket.id ?? "").replace(/\{\{ticket\.priority\}\}/g, ticket.priority ?? "medium");
1491
+ return template.replace(/\{\{finding\.type\}\}/g, finding.type).replace(/\{\{finding\.title\}\}/g, finding.title).replace(/\{\{finding\.linearIssueId\}\}/g, finding.linearIssueId ?? "draft").replace(/\{\{finding\.id\}\}/g, finding.id ?? "").replace(/\{\{finding\.priority\}\}/g, finding.priority ?? "medium");
1463
1492
  }
1464
- const prefix = ticket.type === "bug" ? "fix" : "feat";
1465
- const ref = ticket.linearTicketId ? ` (${ticket.linearTicketId})` : "";
1466
- return `${prefix}(${ticket.type}): ${ticket.title}${ref}`;
1493
+ const prefix = finding.type === "bug" ? "fix" : "feat";
1494
+ const ref = finding.linearIssueId ? ` (${finding.linearIssueId})` : "";
1495
+ return `${prefix}(${finding.type}): ${finding.title}${ref}`;
1467
1496
  }
1468
1497
  function registerShipTool(server, ctx) {
1469
1498
  server.tool(
1470
1499
  "yapout_ship",
1471
- "Commit, push, open a PR, and mark the ticket as done. Run yapout_check first if post-flight checks are configured.",
1500
+ "Commit, push, open a PR, and mark the finding as done. Run yapout_check first if post-flight checks are configured.",
1472
1501
  {
1473
1502
  message: z7.string().optional().describe("Custom commit message (overrides template)"),
1474
1503
  skipPr: z7.boolean().optional().describe("Just push, don't open a PR"),
@@ -1495,26 +1524,26 @@ function registerShipTool(server, ctx) {
1495
1524
  }
1496
1525
  const branch = getCurrentBranch(gitCwd);
1497
1526
  const defaultBranch = getDefaultBranch(gitCwd);
1498
- let ticketTitle = branch;
1499
- let ticketType = "feature";
1500
- let ticketLinearId;
1527
+ let findingTitle = branch;
1528
+ let findingType = "feature";
1529
+ let findingLinearId;
1501
1530
  try {
1502
1531
  const briefPath = join8(gitCwd, ".yapout", "brief.md");
1503
1532
  if (existsSync7(briefPath)) {
1504
1533
  const brief = readFileSync6(briefPath, "utf-8");
1505
1534
  const titleMatch = brief.match(/^# (.+)$/m);
1506
- if (titleMatch) ticketTitle = titleMatch[1];
1535
+ if (titleMatch) findingTitle = titleMatch[1];
1507
1536
  const typeMatch = brief.match(/\*\*Type:\*\* (\w+)/);
1508
- if (typeMatch) ticketType = typeMatch[1];
1537
+ if (typeMatch) findingType = typeMatch[1];
1509
1538
  const linearMatch = brief.match(/\*\*Linear:\*\* .+\/([A-Z]+-\d+)\//);
1510
- if (linearMatch) ticketLinearId = linearMatch[1];
1539
+ if (linearMatch) findingLinearId = linearMatch[1];
1511
1540
  }
1512
1541
  } catch {
1513
1542
  }
1514
1543
  const commitMsg = buildCommitMessage(args.message, config.commit_template, {
1515
- title: ticketTitle,
1516
- type: ticketType,
1517
- linearTicketId: ticketLinearId
1544
+ title: findingTitle,
1545
+ type: findingType,
1546
+ linearIssueId: findingLinearId
1518
1547
  });
1519
1548
  stageAll(gitCwd);
1520
1549
  const sha = commit(commitMsg, gitCwd);
@@ -1538,7 +1567,7 @@ function registerShipTool(server, ctx) {
1538
1567
  const prBody = [
1539
1568
  `## Summary`,
1540
1569
  "",
1541
- ticketTitle,
1570
+ findingTitle,
1542
1571
  "",
1543
1572
  `## Changes`,
1544
1573
  "",
@@ -1550,7 +1579,7 @@ function registerShipTool(server, ctx) {
1550
1579
  `Implemented via [yapout](https://yapout.dev) daemon`
1551
1580
  ].join("\n");
1552
1581
  const pr = await createPullRequest(
1553
- ticketTitle,
1582
+ findingTitle,
1554
1583
  prBody,
1555
1584
  branch,
1556
1585
  defaultBranch,
@@ -1598,13 +1627,13 @@ function registerShipTool(server, ctx) {
1598
1627
  } catch (err) {
1599
1628
  result.completionError = err.message;
1600
1629
  }
1601
- if (ticketLinearId && ctx.projectId) {
1630
+ if (findingLinearId && ctx.projectId) {
1602
1631
  try {
1603
1632
  await ctx.client.action(
1604
1633
  anyApi2.functions.linearStatusMutations.moveIssueStatus,
1605
1634
  {
1606
1635
  projectId: ctx.projectId,
1607
- linearIssueId: ticketLinearId,
1636
+ linearIssueId: findingLinearId,
1608
1637
  statusType: "completed"
1609
1638
  }
1610
1639
  );
@@ -1616,7 +1645,7 @@ function registerShipTool(server, ctx) {
1616
1645
  anyApi2.functions.linearStatusMutations.addLinearComment,
1617
1646
  {
1618
1647
  projectId: ctx.projectId,
1619
- linearIssueId: ticketLinearId,
1648
+ linearIssueId: findingLinearId,
1620
1649
  body: `PR opened: [#${prNumber}](${prUrl})`
1621
1650
  }
1622
1651
  );
@@ -1778,18 +1807,18 @@ import { existsSync as existsSync8, writeFileSync as writeFileSync8, mkdirSync a
1778
1807
  function registerBundleTool(server, ctx) {
1779
1808
  server.tool(
1780
1809
  "yapout_bundle",
1781
- "Add a ticket to the current ticket's bundle so they ship as one PR",
1810
+ "Add a finding to the current finding's bundle so they ship as one PR",
1782
1811
  {
1783
- ticketId: z9.string().describe("Ticket ID to add to the bundle"),
1784
- withTicket: z9.string().describe("Lead ticket ID (currently being worked on)")
1812
+ findingId: z9.string().describe("Finding ID to add to the bundle"),
1813
+ withFinding: z9.string().describe("Lead finding ID (currently being worked on)")
1785
1814
  },
1786
1815
  async (args) => {
1787
1816
  const result = await ctx.client.mutation(
1788
- anyApi2.functions.tickets.bundleTickets,
1789
- { leadTicketId: args.withTicket, joiningTicketId: args.ticketId }
1817
+ anyApi2.functions.bundles.createBundle,
1818
+ { leadFindingId: args.withFinding, joiningFindingId: args.findingId }
1790
1819
  );
1791
1820
  const bundledBrief = await ctx.client.query(
1792
- anyApi2.functions.tickets.getBundledBrief,
1821
+ anyApi2.functions.bundles.getBundledBrief,
1793
1822
  { bundleId: result.bundleId }
1794
1823
  );
1795
1824
  if (!bundledBrief) {
@@ -1806,14 +1835,14 @@ function registerBundleTool(server, ctx) {
1806
1835
  };
1807
1836
  }
1808
1837
  const sections = [
1809
- `# Bundle: ${bundledBrief.tickets.length} tickets`,
1838
+ `# Bundle: ${bundledBrief.findings.length} findings`,
1810
1839
  ""
1811
1840
  ];
1812
- for (const t of bundledBrief.tickets) {
1813
- sections.push(`## ${t.linearTicketId ?? t.ticketId}: ${t.title}`);
1841
+ for (const t of bundledBrief.findings) {
1842
+ sections.push(`## ${t.linearIssueId ?? t.findingId}: ${t.title}`);
1814
1843
  sections.push("");
1815
1844
  sections.push(`**Priority:** ${t.priority} | **Type:** ${t.type}`);
1816
- if (t.linearUrl) sections.push(`**Linear:** ${t.linearUrl}`);
1845
+ if (t.linearIssueUrl) sections.push(`**Linear:** ${t.linearIssueUrl}`);
1817
1846
  sections.push("", t.description);
1818
1847
  if (t.enrichedDescription) {
1819
1848
  sections.push("", "### Enriched Description", "", t.enrichedDescription);
@@ -1837,12 +1866,12 @@ function registerBundleTool(server, ctx) {
1837
1866
  text: JSON.stringify(
1838
1867
  {
1839
1868
  bundleId: result.bundleId,
1840
- tickets: bundledBrief.tickets.map((t) => ({
1841
- ticketId: t.ticketId,
1869
+ findings: bundledBrief.findings.map((t) => ({
1870
+ findingId: t.findingId,
1842
1871
  title: t.title
1843
1872
  })),
1844
1873
  combinedBrief,
1845
- message: `${bundledBrief.tickets.length} tickets bundled. Brief updated.`
1874
+ message: `${bundledBrief.findings.length} findings bundled. Brief updated.`
1846
1875
  },
1847
1876
  null,
1848
1877
  2
@@ -1854,22 +1883,22 @@ function registerBundleTool(server, ctx) {
1854
1883
  );
1855
1884
  }
1856
1885
 
1857
- // src/mcp/tools/get-unenriched-tickets.ts
1886
+ // src/mcp/tools/get-unenriched-findings.ts
1858
1887
  import { z as z10 } from "zod";
1859
- function registerGetUnenrichedTicketsTool(server, ctx) {
1888
+ function registerGetUnenrichedFindingsTool(server, ctx) {
1860
1889
  server.tool(
1861
- "yapout_get_unenriched_ticket",
1862
- `Start enriching a ticket. Returns the next draft ticket (or a specific one by ID) with full context including the original transcript quote, project context, and existing ticket titles for duplicate detection.
1890
+ "yapout_get_unenriched_finding",
1891
+ `Start enriching a finding. Returns the next draft finding (or a specific one by ID) with full context including the original capture quote, project context, and existing finding titles for duplicate detection.
1863
1892
 
1864
- The ticket is locked to "enriching" status \u2014 no other agent can enrich it concurrently.
1893
+ The finding is locked to "enriching" status \u2014 no other agent can enrich it concurrently.
1865
1894
 
1866
1895
  After calling this tool, you should:
1867
1896
  1. Read the relevant parts of the codebase to understand the area affected
1868
- 2. Check for duplicates against the existingTickets list
1869
- 3. If the decision is ambiguous, ask the developer clarifying questions in conversation
1897
+ 2. Check for duplicates against the existingFindings list
1898
+ 3. If the finding is ambiguous, ask the developer clarifying questions in conversation
1870
1899
  4. When confident, call yapout_save_enrichment with a clean description, acceptance criteria, and implementation brief`,
1871
1900
  {
1872
- ticketId: z10.string().optional().describe("Specific ticket ID to enrich. If omitted, returns the highest priority draft ticket.")
1901
+ findingId: z10.string().optional().describe("Specific finding ID to enrich. If omitted, returns the highest priority draft finding.")
1873
1902
  },
1874
1903
  async (args) => {
1875
1904
  const projectId = ctx.projectId ?? process.env.YAPOUT_PROJECT_ID;
@@ -1889,7 +1918,7 @@ After calling this tool, you should:
1889
1918
  anyApi2.functions.localPipeline.claimForEnrichment,
1890
1919
  {
1891
1920
  projectId,
1892
- ...args.ticketId ? { ticketId: args.ticketId } : {}
1921
+ ...args.findingId ? { findingId: args.findingId } : {}
1893
1922
  }
1894
1923
  );
1895
1924
  if (!result) {
@@ -1897,7 +1926,7 @@ After calling this tool, you should:
1897
1926
  content: [
1898
1927
  {
1899
1928
  type: "text",
1900
- text: "No tickets need enrichment. All tickets are either already enriched or in progress."
1929
+ text: "No findings need enrichment. All findings are either already enriched or in progress."
1901
1930
  }
1902
1931
  ]
1903
1932
  };
@@ -1915,7 +1944,7 @@ After calling this tool, you should:
1915
1944
  content: [
1916
1945
  {
1917
1946
  type: "text",
1918
- text: `Error claiming ticket for enrichment: ${err.message}`
1947
+ text: `Error claiming finding for enrichment: ${err.message}`
1919
1948
  }
1920
1949
  ],
1921
1950
  isError: true
@@ -1925,11 +1954,11 @@ After calling this tool, you should:
1925
1954
  );
1926
1955
  }
1927
1956
 
1928
- // src/mcp/tools/get-existing-tickets.ts
1929
- function registerGetExistingTicketsTool(server, ctx) {
1957
+ // src/mcp/tools/get-existing-findings.ts
1958
+ function registerGetExistingFindingsTool(server, ctx) {
1930
1959
  server.tool(
1931
- "yapout_get_existing_tickets",
1932
- "Fetch all ticket titles in the project for duplicate detection during enrichment",
1960
+ "yapout_get_existing_findings",
1961
+ "Fetch all finding titles in the project for duplicate detection during enrichment",
1933
1962
  {},
1934
1963
  async () => {
1935
1964
  const projectId = ctx.projectId ?? process.env.YAPOUT_PROJECT_ID;
@@ -1945,16 +1974,16 @@ function registerGetExistingTicketsTool(server, ctx) {
1945
1974
  };
1946
1975
  }
1947
1976
  try {
1948
- const tickets = await ctx.client.query(
1949
- anyApi2.functions.localPipeline.getExistingTicketTitles,
1977
+ const findings = await ctx.client.query(
1978
+ anyApi2.functions.localPipeline.getExistingFindingTitles,
1950
1979
  { projectId }
1951
1980
  );
1952
- if (!tickets || tickets.length === 0) {
1981
+ if (!findings || findings.length === 0) {
1953
1982
  return {
1954
1983
  content: [
1955
1984
  {
1956
1985
  type: "text",
1957
- text: "No existing tickets in this project."
1986
+ text: "No existing findings in this project."
1958
1987
  }
1959
1988
  ]
1960
1989
  };
@@ -1963,7 +1992,7 @@ function registerGetExistingTicketsTool(server, ctx) {
1963
1992
  content: [
1964
1993
  {
1965
1994
  type: "text",
1966
- text: JSON.stringify(tickets, null, 2)
1995
+ text: JSON.stringify(findings, null, 2)
1967
1996
  }
1968
1997
  ]
1969
1998
  };
@@ -1972,7 +2001,7 @@ function registerGetExistingTicketsTool(server, ctx) {
1972
2001
  content: [
1973
2002
  {
1974
2003
  type: "text",
1975
- text: `Error fetching existing tickets: ${err.message}`
2004
+ text: `Error fetching existing findings: ${err.message}`
1976
2005
  }
1977
2006
  ],
1978
2007
  isError: true
@@ -1984,23 +2013,53 @@ function registerGetExistingTicketsTool(server, ctx) {
1984
2013
 
1985
2014
  // src/mcp/tools/save-enrichment.ts
1986
2015
  import { z as z11 } from "zod";
2016
+
2017
+ // src/mcp/tools/enrichment-session.ts
2018
+ var activeSessions = /* @__PURE__ */ new Map();
2019
+ function createSession(projectId, total, filter) {
2020
+ const sessionId = crypto.randomUUID();
2021
+ const session = {
2022
+ sessionId,
2023
+ projectId,
2024
+ filter,
2025
+ startedAt: Date.now(),
2026
+ stats: { enriched: 0, skipped: 0, total },
2027
+ contextDelivered: false
2028
+ };
2029
+ activeSessions.set(sessionId, session);
2030
+ return session;
2031
+ }
2032
+ function getSession(sessionId) {
2033
+ return activeSessions.get(sessionId);
2034
+ }
2035
+ function updateSessionStats(sessionId, update) {
2036
+ const session = activeSessions.get(sessionId);
2037
+ if (!session) return;
2038
+ Object.assign(session.stats, update);
2039
+ }
2040
+ function markContextDelivered(sessionId) {
2041
+ const session = activeSessions.get(sessionId);
2042
+ if (session) session.contextDelivered = true;
2043
+ }
2044
+
2045
+ // src/mcp/tools/save-enrichment.ts
1987
2046
  function registerSaveEnrichmentTool(server, ctx) {
1988
2047
  server.tool(
1989
2048
  "yapout_save_enrichment",
1990
2049
  `Save enrichment and sync to Linear. Call this after you have read the codebase, asked any necessary clarifying questions, and formulated a clean description, acceptance criteria, and implementation brief.
1991
2050
 
1992
- The ticket must have been claimed via yapout_get_unenriched_ticket first (status must be "enriching").
2051
+ The finding must have been claimed via yapout_get_unenriched_finding first (status must be "enriching").
1993
2052
 
1994
2053
  This tool saves the enrichment, then automatically creates the Linear issue with:
1995
2054
  - Clean description + acceptance criteria as the issue body
1996
2055
  - Clarification Q&A as a branded comment (if any)
1997
2056
  - Implementation brief as attachment metadata
1998
2057
 
1999
- The ticket transitions: enriching \u2192 enriched \u2192 synced.`,
2058
+ The finding transitions: enriching \u2192 enriched \u2192 synced.`,
2000
2059
  {
2001
- ticketId: z11.string().describe("The ticket ID to enrich (from yapout_get_unenriched_ticket)"),
2002
- title: z11.string().describe("Refined ticket title \u2014 improve it if the original was vague"),
2003
- cleanDescription: z11.string().describe("Human-readable summary for the Linear issue body. Write the kind of ticket a senior engineer would write."),
2060
+ findingId: z11.string().describe("The finding ID to enrich (from yapout_get_unenriched_finding)"),
2061
+ title: z11.string().describe("Refined finding title \u2014 improve it if the original was vague"),
2062
+ cleanDescription: z11.string().describe("Human-readable summary for the Linear issue body. Write the kind of finding a senior engineer would write."),
2004
2063
  acceptanceCriteria: z11.array(z11.string()).describe("List of testable acceptance criteria (each a single clear statement)"),
2005
2064
  implementationBrief: z11.string().describe("Deep technical context for the implementing agent: which files, what approach, edge cases to watch for"),
2006
2065
  clarifications: z11.array(
@@ -2009,17 +2068,18 @@ The ticket transitions: enriching \u2192 enriched \u2192 synced.`,
2009
2068
  answer: z11.string().describe("The developer's answer")
2010
2069
  })
2011
2070
  ).optional().describe("Only meaningful Q&A from the conversation \u2014 deviations from expected scope, scoping decisions, etc. Omit if you had no questions."),
2012
- isOversized: z11.boolean().optional().describe("Set to true if this ticket is too large for a single PR"),
2013
- suggestedSplit: z11.array(z11.string()).optional().describe("If oversized: suggested sub-ticket titles for breaking it down"),
2014
- level: z11.enum(["project", "issue"]).optional().describe("Override the decision's level if enrichment reveals it should be reclassified"),
2015
- nature: z11.enum(["implementable", "operational", "spike"]).optional().describe("Override the decision's nature if enrichment reveals it should be reclassified")
2071
+ isOversized: z11.boolean().optional().describe("Set to true if this finding is too large for a single PR"),
2072
+ suggestedSplit: z11.array(z11.string()).optional().describe("If oversized: suggested sub-finding titles for breaking it down"),
2073
+ level: z11.enum(["project", "issue"]).optional().describe("Override the finding's level if enrichment reveals it should be reclassified"),
2074
+ nature: z11.enum(["implementable", "operational", "spike"]).optional().describe("Override the finding's nature if enrichment reveals it should be reclassified"),
2075
+ sessionId: z11.string().optional().describe("Bulk enrichment session ID (from yapout_start_enrichment). Updates session stats.")
2016
2076
  },
2017
2077
  async (args) => {
2018
2078
  try {
2019
2079
  await ctx.client.mutation(
2020
2080
  anyApi2.functions.localPipeline.saveLocalEnrichment,
2021
2081
  {
2022
- ticketId: args.ticketId,
2082
+ findingId: args.findingId,
2023
2083
  title: args.title,
2024
2084
  cleanDescription: args.cleanDescription,
2025
2085
  acceptanceCriteria: args.acceptanceCriteria,
@@ -2032,21 +2092,33 @@ The ticket transitions: enriching \u2192 enriched \u2192 synced.`,
2032
2092
  }
2033
2093
  );
2034
2094
  await ctx.client.action(
2035
- anyApi2.functions.localPipeline.syncTicketToLinearLocal,
2036
- { ticketId: args.ticketId }
2095
+ anyApi2.functions.localPipeline.syncFindingToLinearLocal,
2096
+ { findingId: args.findingId }
2037
2097
  );
2038
- const ticket = await ctx.client.query(
2039
- anyApi2.functions.tickets.getTicket,
2040
- { ticketId: args.ticketId }
2098
+ const finding = await ctx.client.query(
2099
+ anyApi2.functions.findings.getFinding,
2100
+ { findingId: args.findingId }
2041
2101
  );
2102
+ if (args.sessionId) {
2103
+ const session2 = getSession(args.sessionId);
2104
+ if (session2) {
2105
+ updateSessionStats(args.sessionId, {
2106
+ enriched: session2.stats.enriched + 1
2107
+ });
2108
+ }
2109
+ }
2110
+ const session = args.sessionId ? getSession(args.sessionId) : null;
2111
+ const estimatedTokens = session ? session.stats.enriched * 5e3 : 0;
2112
+ const compactionHint = estimatedTokens > 1e5;
2042
2113
  const response = {
2043
- ticketId: args.ticketId,
2044
- linearIssueId: ticket?.linearTicketId ?? null,
2045
- linearUrl: ticket?.linearUrl ?? null,
2046
- message: ticket?.linearUrl ? `Ticket enriched and synced to Linear: ${ticket.linearUrl}` : "Ticket enriched and synced to Linear."
2114
+ findingId: args.findingId,
2115
+ linearIssueId: finding?.linearIssueId ?? null,
2116
+ linearIssueUrl: finding?.linearIssueUrl ?? null,
2117
+ compactionHint,
2118
+ message: finding?.linearIssueUrl ? `Finding enriched and synced to Linear: ${finding.linearIssueUrl}` : "Finding enriched and synced to Linear."
2047
2119
  };
2048
2120
  if (args.isOversized && args.suggestedSplit?.length) {
2049
- response.warning = `This ticket is oversized. Suggested split: ${args.suggestedSplit.join(", ")}`;
2121
+ response.warning = `This finding is oversized. Suggested split: ${args.suggestedSplit.join(", ")}`;
2050
2122
  }
2051
2123
  return {
2052
2124
  content: [
@@ -2076,15 +2148,15 @@ import { z as z12 } from "zod";
2076
2148
  function registerSyncToLinearTool(server, ctx) {
2077
2149
  server.tool(
2078
2150
  "yapout_sync_to_linear",
2079
- "Trigger Linear ticket creation for an enriched ticket. The sync runs server-side (encrypted Linear token in Convex).",
2151
+ "Trigger Linear issue creation for an enriched finding. The sync runs server-side (encrypted Linear token in Convex).",
2080
2152
  {
2081
- ticketId: z12.string().describe("The ticket ID to sync to Linear")
2153
+ findingId: z12.string().describe("The finding ID to sync to Linear")
2082
2154
  },
2083
2155
  async (args) => {
2084
2156
  try {
2085
2157
  await ctx.client.action(
2086
- anyApi2.functions.localPipeline.syncTicketToLinearLocal,
2087
- { ticketId: args.ticketId }
2158
+ anyApi2.functions.localPipeline.syncFindingToLinearLocal,
2159
+ { findingId: args.findingId }
2088
2160
  );
2089
2161
  return {
2090
2162
  content: [
@@ -2093,8 +2165,8 @@ function registerSyncToLinearTool(server, ctx) {
2093
2165
  text: JSON.stringify(
2094
2166
  {
2095
2167
  success: true,
2096
- ticketId: args.ticketId,
2097
- message: "Ticket synced to Linear successfully. It will now appear in the work queue for implementation."
2168
+ findingId: args.findingId,
2169
+ message: "Finding synced to Linear successfully. It will now appear in the work queue for implementation."
2098
2170
  },
2099
2171
  null,
2100
2172
  2
@@ -2122,7 +2194,7 @@ import { z as z13 } from "zod";
2122
2194
  function registerSubmitYapSessionTool(server, ctx) {
2123
2195
  server.tool(
2124
2196
  "yapout_submit_yap_session",
2125
- "Submit a yap session transcript for decision extraction",
2197
+ "Submit a yap session transcript for finding extraction",
2126
2198
  {
2127
2199
  title: z13.string().describe("Session title (e.g., 'Notification system brainstorm')"),
2128
2200
  transcript: z13.string().describe("Cleaned conversation transcript")
@@ -2140,8 +2212,8 @@ function registerSubmitYapSessionTool(server, ctx) {
2140
2212
  };
2141
2213
  }
2142
2214
  try {
2143
- const transcriptId = await ctx.client.mutation(
2144
- anyApi2.functions.transcripts.createFromYapSession,
2215
+ const captureId = await ctx.client.mutation(
2216
+ anyApi2.functions.captures.createFromYapSession,
2145
2217
  {
2146
2218
  projectId: ctx.projectId,
2147
2219
  title: args.title,
@@ -2154,8 +2226,8 @@ function registerSubmitYapSessionTool(server, ctx) {
2154
2226
  type: "text",
2155
2227
  text: JSON.stringify(
2156
2228
  {
2157
- transcriptId,
2158
- message: "Yap session submitted. Decisions will appear for review shortly."
2229
+ captureId,
2230
+ message: "Yap session submitted. Findings will appear for review shortly."
2159
2231
  },
2160
2232
  null,
2161
2233
  2
@@ -2233,46 +2305,46 @@ WHAT YOU DON'T DO:
2233
2305
  - Don't implement anything. Don't write code. Don't edit files. Just discuss.
2234
2306
  - Don't agree with everything. Challenge ideas that seem undercooked.
2235
2307
  - Don't ask more than 5 questions without letting the developer steer.
2236
- - Don't make changes to the codebase, even if the user asks. A yap session produces decisions and tickets \u2014 implementation happens later through the normal ticket pipeline. If the user asks you to "just do it" or "make that change now," push back: "That's what the tickets are for. Let's capture it properly so it goes through review." This boundary is critical \u2014 implementing during a yap bypasses every approval gate yapout exists to enforce.
2308
+ - Don't make changes to the codebase, even if the user asks. A yap session produces findings \u2014 implementation happens later through the normal finding pipeline. If the user asks you to "just do it" or "make that change now," push back: "That's what the findings are for. Let's capture it properly so it goes through review." This boundary is critical \u2014 implementing during a yap bypasses every approval gate yapout exists to enforce.
2237
2309
 
2238
2310
  ${contextSection}
2239
2311
 
2240
2312
  DURING THE SESSION \u2014 COMPLETENESS TRACKING:
2241
- As the conversation progresses, you are building a mental model of every decision. For each decision, track:
2313
+ As the conversation progresses, you are building a mental model of every finding. For each finding, track:
2242
2314
  - What has been clearly stated (title, scope, priority)
2243
- - What has been discussed in enough depth to write a ticket (enrichment)
2315
+ - What has been discussed in enough depth to write a finding (enrichment)
2244
2316
  - What is still vague, contradicted, or unanswered
2245
2317
 
2246
- The conversation does NOT need to follow a rigid structure. The user may jump between topics, change their mind, go on tangents, or revisit earlier decisions. This is normal \u2014 real conversations are not linear. Your job is to follow the thread and keep track of the state of each decision regardless of conversation order.
2318
+ The conversation does NOT need to follow a rigid structure. The user may jump between topics, change their mind, go on tangents, or revisit earlier findings. This is normal \u2014 real conversations are not linear. Your job is to follow the thread and keep track of the state of each finding regardless of conversation order.
2247
2319
 
2248
2320
  If the user contradicts an earlier statement, note it and confirm which version they mean when the time is right. Don't interrupt the flow for minor clarifications \u2014 batch them for later.
2249
2321
 
2250
2322
  BEFORE SUBMITTING \u2014 GAP FILLING:
2251
- Before you submit, review your mental model of every decision. For each one, ask yourself:
2252
- - Could a developer read this ticket and start working without asking questions?
2323
+ Before you submit, review your mental model of every finding. For each one, ask yourself:
2324
+ - Could a developer read this finding and start working without asking questions?
2253
2325
  - Are the acceptance criteria specific enough to verify?
2254
2326
  - Are there ambiguities the user didn't resolve?
2255
2327
 
2256
2328
  If there are gaps, ask the user to fill them. Be direct: "Before I submit, I need clarity on a few things..." Group related gaps together rather than asking one at a time.
2257
2329
 
2258
- You do NOT need to fill every gap. If the conversation didn't cover something deeply enough, mark that decision as NOT enriched \u2014 it will go through the async enrichment pipeline later. Be honest about what you know vs. what you're guessing.
2330
+ You do NOT need to fill every gap. If the conversation didn't cover something deeply enough, mark that finding as NOT enriched \u2014 it will go through the async enrichment pipeline later. Be honest about what you know vs. what you're guessing.
2259
2331
 
2260
2332
  ENRICHMENT ASSESSMENT:
2261
- For each decision, you must make an honest call: is this enriched or not?
2333
+ For each finding, you must make an honest call: is this enriched or not?
2262
2334
 
2263
- Mark a decision as ENRICHED (isEnriched: true) when:
2264
- - The conversation covered it thoroughly enough to produce a clear ticket
2335
+ Mark a finding as ENRICHED (isEnriched: true) when:
2336
+ - The conversation covered it thoroughly enough to produce a clear finding
2265
2337
  - You can write an enrichedDescription that a senior engineer would recognize as well-scoped
2266
2338
  - You can write at least 2-3 testable acceptance criteria
2267
2339
  - The user explicitly validated the scope (not just mentioned it in passing)
2268
2340
 
2269
- Mark a decision as NOT ENRICHED (isEnriched: false or omitted) when:
2341
+ Mark a finding as NOT ENRICHED (isEnriched: false or omitted) when:
2270
2342
  - It came up late in the conversation without much discussion
2271
2343
  - The user mentioned it but didn't elaborate on scope or requirements
2272
2344
  - You're uncertain about key aspects (what it should do, how it should work)
2273
2345
  - It's a spike or needs further scoping
2274
2346
 
2275
- This is a quality gate. Do not inflate your assessment \u2014 unenriched tickets get enriched properly later. Falsely marking something as enriched skips that process and produces bad tickets.
2347
+ This is a quality gate. Do not inflate your assessment \u2014 unenriched findings get enriched properly later. Falsely marking something as enriched skips that process and produces bad findings.
2276
2348
 
2277
2349
  PRESENTING THE FINAL PICTURE:
2278
2350
  When the conversation feels complete, present a summary grouped by project:
@@ -2294,17 +2366,17 @@ Should I submit this to yapout?"
2294
2366
  SUBMITTING:
2295
2367
  On confirmation, call yapout_extract_from_yap with the full data.
2296
2368
 
2297
- For each decision, provide:
2369
+ For each finding, provide:
2298
2370
  - title, description, sourceQuote, type, priority, confidence, level, nature
2299
2371
  - isEnriched: your honest assessment (see above)
2300
2372
 
2301
- For ENRICHED decisions (isEnriched: true), also include:
2302
- - enrichedDescription: clean, final description \u2014 write the kind of ticket a senior engineer would write
2373
+ For ENRICHED findings (isEnriched: true), also include:
2374
+ - enrichedDescription: clean, final description \u2014 write the kind of finding a senior engineer would write
2303
2375
  - acceptanceCriteria: array of specific, testable statements
2304
2376
  - implementationBrief: (optional) technical context \u2014 files, approach, edge cases. Only include if you read the codebase during the session. The implementing agent reads the codebase anyway.
2305
- - clarifications: relevant Q&A from the conversation that shaped the decision
2377
+ - clarifications: relevant Q&A from the conversation that shaped the finding
2306
2378
 
2307
- For PROJECT-level decisions, also include:
2379
+ For PROJECT-level findings, also include:
2308
2380
  - projectDescription: what this body of work accomplishes
2309
2381
  - suggestedOrder: implementation sequencing ("Phase 1: A, then Phase 2: B+C")
2310
2382
  - children: array of child issues (each with their own enrichment data)
@@ -2313,18 +2385,18 @@ For children with DEPENDENCIES, include:
2313
2385
  - dependsOn: array of sibling indices (0-based) that must complete first
2314
2386
 
2315
2387
  Structure:
2316
- - Projects include children inline \u2014 never create child issues as separate top-level decisions
2388
+ - Projects include children inline \u2014 never create child issues as separate top-level findings
2317
2389
  - Standalone issues go at the top level
2318
2390
  - The hierarchy you produce should be the final structure
2319
2391
 
2320
2392
  Call yapout_extract_from_yap with:
2321
2393
  - sessionTitle: descriptive title for this session
2322
2394
  - sessionTranscript: clean summary of the conversation (meeting-notes style)
2323
- - decisions: the array (projects with children, standalone issues)
2395
+ - findings: the array (projects with children, standalone issues)
2324
2396
 
2325
- This creates decisions (as converted \u2014 your participation was the approval), tickets (enriched or draft based on your assessment), and project decomposition \u2014 all in one call. Enriched tickets appear in the work queue ready for the user to sync to Linear.
2397
+ This creates findings (enriched or draft based on your assessment) and project decomposition \u2014 all in one call. Enriched findings appear in the work queue ready for the user to sync to Linear.
2326
2398
 
2327
- Only fall back to yapout_submit_yap_session if the session was purely exploratory with no clear actionable decisions.
2399
+ Only fall back to yapout_submit_yap_session if the session was purely exploratory with no clear actionable findings.
2328
2400
 
2329
2401
  --- BEGIN THE SESSION NOW ---`;
2330
2402
  return {
@@ -2358,39 +2430,39 @@ var childSchema = z15.object({
2358
2430
  title: z15.string().describe("Child issue title"),
2359
2431
  description: z15.string().describe("What needs to be done and why"),
2360
2432
  sourceQuote: z15.string().describe("Relevant excerpt from the conversation"),
2361
- type: z15.enum(["feature", "bug", "chore"]).describe("Decision category"),
2433
+ type: z15.enum(["feature", "bug", "chore"]).describe("Finding category"),
2362
2434
  priority: z15.enum(["urgent", "high", "medium", "low"]).describe("Priority level"),
2363
- confidence: z15.number().min(0).max(1).describe("Confidence this is a clear, actionable decision (0-1)"),
2435
+ confidence: z15.number().min(0).max(1).describe("Confidence this is a clear, actionable finding (0-1)"),
2364
2436
  nature: z15.enum(["implementable", "operational", "spike"]).describe("What kind of work"),
2365
2437
  // Enrichment — set isEnriched: true only if the conversation covered this
2366
- // issue thoroughly enough to produce a ticket a developer could pick up.
2438
+ // issue thoroughly enough to produce a finding a developer could pick up.
2367
2439
  isEnriched: z15.boolean().optional().describe(
2368
- "Did the conversation cover this issue deeply enough to produce a complete ticket? true = enriched (ready for Linear), false/omitted = draft (needs async enrichment)"
2440
+ "Did the conversation cover this issue deeply enough to produce a complete finding? true = enriched (ready for Linear), false/omitted = draft (needs async enrichment)"
2369
2441
  ),
2370
2442
  enrichedDescription: z15.string().optional().describe("Clean, final description for the Linear issue body (required if isEnriched)"),
2371
2443
  acceptanceCriteria: z15.array(z15.string()).optional().describe("Testable acceptance criteria (required if isEnriched)"),
2372
2444
  implementationBrief: z15.string().optional().describe("Technical context: files, approach, edge cases (optional \u2014 the implementing agent reads the codebase anyway)"),
2373
- clarifications: z15.array(clarificationSchema).optional().describe("Relevant Q&A from the conversation that shaped this decision"),
2445
+ clarifications: z15.array(clarificationSchema).optional().describe("Relevant Q&A from the conversation that shaped this finding"),
2374
2446
  dependsOn: z15.array(z15.number()).optional().describe("Indices (0-based) of sibling children that must be completed first")
2375
2447
  });
2376
2448
  function registerExtractFromYapTool(server, ctx) {
2377
2449
  server.tool(
2378
2450
  "yapout_extract_from_yap",
2379
- "Submit decisions from a yap session you conducted. The yap session IS the enrichment \u2014 you participated in the conversation, gathered context, and can assess completeness per-decision. Creates transcript, decisions (as converted), and tickets (enriched or draft). Enriched tickets are ready for the user to sync to Linear from the UI. Draft tickets need further enrichment via the async pipeline.",
2451
+ "Submit findings from a yap session you conducted. The yap session IS the enrichment \u2014 you participated in the conversation, gathered context, and can assess completeness per-finding. Creates capture, findings (enriched or draft). Enriched findings are ready for the user to sync to Linear from the UI. Draft findings need further enrichment via the async pipeline.",
2380
2452
  {
2381
2453
  sessionTitle: z15.string().describe("Descriptive title for this yap session"),
2382
2454
  sessionTranscript: z15.string().describe("Cleaned transcript of the conversation (meeting-notes style)"),
2383
- decisions: z15.array(
2455
+ findings: z15.array(
2384
2456
  z15.object({
2385
- title: z15.string().describe("Decision title"),
2457
+ title: z15.string().describe("Finding title"),
2386
2458
  description: z15.string().describe("What needs to be done and why"),
2387
2459
  sourceQuote: z15.string().describe("Relevant excerpt from the conversation"),
2388
- type: z15.enum(["feature", "bug", "chore"]).describe("Decision category (do not use spike \u2014 use nature instead)"),
2460
+ type: z15.enum(["feature", "bug", "chore"]).describe("Finding category (do not use spike \u2014 use nature instead)"),
2389
2461
  priority: z15.enum(["urgent", "high", "medium", "low"]).describe("Priority level"),
2390
- confidence: z15.number().min(0).max(1).describe("Your confidence this is a clear, actionable decision (0-1)"),
2462
+ confidence: z15.number().min(0).max(1).describe("Your confidence this is a clear, actionable finding (0-1)"),
2391
2463
  level: z15.enum(["project", "issue"]).describe("project = body of work with children, issue = single unit"),
2392
2464
  nature: z15.enum(["implementable", "operational", "spike"]).describe("implementable = code changes, operational = manual task, spike = needs scoping"),
2393
- sourceDecisionId: z15.string().optional().describe("ID of an existing decision this scopes (e.g., scoping a spike)"),
2465
+ sourceFindingId: z15.string().optional().describe("ID of an existing finding this scopes (e.g., scoping a spike)"),
2394
2466
  // Issue-level enrichment (when level === "issue")
2395
2467
  isEnriched: z15.boolean().optional().describe("For standalone issues: was this thoroughly discussed? true = enriched, false = draft"),
2396
2468
  enrichedDescription: z15.string().optional().describe("Clean description for Linear (standalone issues only, required if isEnriched)"),
@@ -2402,7 +2474,7 @@ function registerExtractFromYapTool(server, ctx) {
2402
2474
  suggestedOrder: z15.string().optional().describe("Implementation order: 'Phase 1: A, then Phase 2: B+C' (projects only)"),
2403
2475
  children: z15.array(childSchema).optional().describe("Child issues for project-level decisions")
2404
2476
  })
2405
- ).describe("Decisions from the conversation. Projects include children inline.")
2477
+ ).describe("Findings from the conversation. Projects include children inline.")
2406
2478
  },
2407
2479
  async (args) => {
2408
2480
  if (!ctx.projectId) {
@@ -2418,27 +2490,27 @@ function registerExtractFromYapTool(server, ctx) {
2418
2490
  }
2419
2491
  try {
2420
2492
  const result = await ctx.client.mutation(
2421
- anyApi2.functions.transcripts.extractFromYapSession,
2493
+ anyApi2.functions.captures.extractFromYapSession,
2422
2494
  {
2423
2495
  projectId: ctx.projectId,
2424
2496
  title: args.sessionTitle,
2425
2497
  transcript: args.sessionTranscript,
2426
- decisions: args.decisions
2498
+ findings: args.findings
2427
2499
  }
2428
2500
  );
2429
2501
  let enrichedCount = 0;
2430
2502
  let draftCount = 0;
2431
- let totalTickets = 0;
2503
+ let totalFindings = 0;
2432
2504
  for (const item of result.items) {
2433
2505
  if (item.level === "project") {
2434
2506
  for (const child of item.children ?? []) {
2435
- totalTickets++;
2436
- if (child.ticketStatus === "enriched") enrichedCount++;
2507
+ totalFindings++;
2508
+ if (child.findingStatus === "enriched") enrichedCount++;
2437
2509
  else draftCount++;
2438
2510
  }
2439
2511
  } else {
2440
- totalTickets++;
2441
- if (item.ticketStatus === "enriched") enrichedCount++;
2512
+ totalFindings++;
2513
+ if (item.findingStatus === "enriched") enrichedCount++;
2442
2514
  else draftCount++;
2443
2515
  }
2444
2516
  }
@@ -2451,14 +2523,14 @@ function registerExtractFromYapTool(server, ctx) {
2451
2523
  type: "text",
2452
2524
  text: JSON.stringify(
2453
2525
  {
2454
- transcriptId: result.transcriptId,
2526
+ captureId: result.captureId,
2455
2527
  items: result.items,
2456
2528
  summary: {
2457
- totalTickets,
2529
+ totalFindings,
2458
2530
  enriched: enrichedCount,
2459
2531
  needsEnrichment: draftCount
2460
2532
  },
2461
- message: `Created ${totalTickets} ticket${totalTickets === 1 ? "" : "s"}: ${parts.join(", ")}. Review in the yapout work queue.`
2533
+ message: `Created ${totalFindings} finding${totalFindings === 1 ? "" : "s"}: ${parts.join(", ")}. Review in the yapout work queue.`
2462
2534
  },
2463
2535
  null,
2464
2536
  2
@@ -2486,20 +2558,20 @@ import { z as z16 } from "zod";
2486
2558
  function registerSaveProjectEnrichmentTool(server, ctx) {
2487
2559
  server.tool(
2488
2560
  "yapout_save_project_enrichment",
2489
- `Save the decomposition of a project-level decision into implementable child issues.
2561
+ `Save the decomposition of a project-level finding into implementable child issues.
2490
2562
 
2491
- Call this after enriching a ticket with level: "project". The agent should have:
2563
+ Call this after enriching a finding with level: "project". The agent should have:
2492
2564
  1. Read the codebase extensively
2493
2565
  2. Asked the user scoping questions
2494
2566
  3. Produced a set of child issues with dependencies
2495
2567
 
2496
- This tool creates child decisions and tickets in yapout, transitions the original
2497
- ticket to "decomposed", and returns the child ticket IDs for subsequent Linear sync.
2568
+ This tool creates child findings in yapout, transitions the original
2569
+ finding to "decomposed", and returns the child finding IDs for subsequent Linear sync.
2498
2570
 
2499
2571
  Each child issue's implementation brief must be detailed enough to stand alone as a
2500
2572
  full spec \u2014 schema changes, files to modify, edge cases, and acceptance criteria.`,
2501
2573
  {
2502
- ticketId: z16.string().describe("The project-level ticket being decomposed (from yapout_get_unenriched_ticket)"),
2574
+ findingId: z16.string().describe("The project-level finding being decomposed (from yapout_get_unenriched_finding)"),
2503
2575
  projectDescription: z16.string().describe("Description for the project \u2014 what this body of work accomplishes"),
2504
2576
  suggestedOrder: z16.string().describe("Human-readable implementation order (e.g. 'Phase 1: A, then Phase 2: B+C in parallel, then Phase 3: D')"),
2505
2577
  linearProjectId: z16.string().optional().describe("Existing Linear project ID to associate child issues with. Omit to skip Linear project association \u2014 issues will be synced without a project."),
@@ -2531,7 +2603,7 @@ full spec \u2014 schema changes, files to modify, edge cases, and acceptance cri
2531
2603
  const result = await ctx.client.mutation(
2532
2604
  anyApi2.functions.localPipeline.saveProjectEnrichment,
2533
2605
  {
2534
- ticketId: args.ticketId,
2606
+ findingId: args.findingId,
2535
2607
  projectDescription: args.projectDescription,
2536
2608
  suggestedOrder: args.suggestedOrder,
2537
2609
  linearProjectId: args.linearProjectId,
@@ -2544,11 +2616,11 @@ full spec \u2014 schema changes, files to modify, edge cases, and acceptance cri
2544
2616
  type: "text",
2545
2617
  text: JSON.stringify(
2546
2618
  {
2547
- ticketId: result.ticketId,
2619
+ findingId: result.findingId,
2548
2620
  status: "decomposed",
2549
- childTicketIds: result.childTicketIds,
2621
+ childFindingIds: result.childFindingIds,
2550
2622
  suggestedOrder: result.suggestedOrder,
2551
- message: `Project decomposed into ${result.childTicketIds.length} child issues. Original ticket marked as decomposed. Child tickets are in draft status \u2014 enrich and sync each one individually, or approve them for the user to review.`
2623
+ message: `Project decomposed into ${result.childFindingIds.length} child issues. Original finding marked as decomposed. Child findings are in draft status \u2014 enrich and sync each one individually, or approve them for the user to review.`
2552
2624
  },
2553
2625
  null,
2554
2626
  2
@@ -2576,13 +2648,13 @@ import { z as z17 } from "zod";
2576
2648
  function registerMarkDuplicateTool(server, ctx) {
2577
2649
  server.tool(
2578
2650
  "yapout_mark_duplicate",
2579
- `Mark a ticket as a duplicate of existing Linear work. Use this during any enrichment
2651
+ `Mark a finding as a duplicate of existing Linear work. Use this during any enrichment
2580
2652
  flow when you discover the work is already tracked in Linear.
2581
2653
 
2582
- The ticket must be in "enriching" or "enriched" status. It will be transitioned to
2654
+ The finding must be in "enriching" or "enriched" status. It will be transitioned to
2583
2655
  "failed" with the duplicate reference stored. Nothing is synced to Linear.`,
2584
2656
  {
2585
- ticketId: z17.string().describe("The yapout ticket to archive as a duplicate"),
2657
+ findingId: z17.string().describe("The yapout finding to archive as a duplicate"),
2586
2658
  duplicateOfLinearId: z17.string().describe("The Linear issue identifier it duplicates (e.g. 'ENG-234')"),
2587
2659
  reason: z17.string().describe("Brief explanation of why this is a duplicate")
2588
2660
  },
@@ -2600,9 +2672,9 @@ The ticket must be in "enriching" or "enriched" status. It will be transitioned
2600
2672
  }
2601
2673
  try {
2602
2674
  const result = await ctx.client.mutation(
2603
- anyApi2.functions.tickets.markDuplicate,
2675
+ anyApi2.functions.findings.markDuplicate,
2604
2676
  {
2605
- ticketId: args.ticketId,
2677
+ findingId: args.findingId,
2606
2678
  duplicateOfLinearId: args.duplicateOfLinearId,
2607
2679
  reason: args.reason
2608
2680
  }
@@ -2613,10 +2685,10 @@ The ticket must be in "enriching" or "enriched" status. It will be transitioned
2613
2685
  type: "text",
2614
2686
  text: JSON.stringify(
2615
2687
  {
2616
- ticketId: result.ticketId,
2688
+ findingId: result.findingId,
2617
2689
  status: "failed",
2618
2690
  duplicateOf: result.duplicateOfLinearId,
2619
- message: `Ticket archived as duplicate of ${args.duplicateOfLinearId}. Nothing synced to Linear.`
2691
+ message: `Finding archived as duplicate of ${args.duplicateOfLinearId}. Nothing synced to Linear.`
2620
2692
  },
2621
2693
  null,
2622
2694
  2
@@ -2712,6 +2784,218 @@ and issue counts so you can ask the user for confirmation before creating a new
2712
2784
  );
2713
2785
  }
2714
2786
 
2787
+ // src/mcp/tools/start-enrichment.ts
2788
+ import { z as z18 } from "zod";
2789
+ function registerStartEnrichmentTool(server, ctx) {
2790
+ server.tool(
2791
+ "yapout_start_enrichment",
2792
+ `Start a bulk enrichment session. Returns a session ID and the count of findings to process.
2793
+
2794
+ Use this before calling yapout_enrich_next. The session tracks your progress (enriched/skipped/remaining)
2795
+ and maintains filter criteria so you don't re-pass them on every call.
2796
+
2797
+ Optionally filter by tags, capture, or explicit finding IDs.`,
2798
+ {
2799
+ filter: z18.object({
2800
+ tags: z18.array(z18.string()).optional().describe("Only enrich findings with these tags"),
2801
+ captureId: z18.string().optional().describe("Only enrich findings from this capture"),
2802
+ findingIds: z18.array(z18.string()).optional().describe("Only enrich these specific findings")
2803
+ }).optional().describe("Filter criteria. If omitted, all draft findings in the project are included.")
2804
+ },
2805
+ async (args) => {
2806
+ const projectId = ctx.projectId ?? process.env.YAPOUT_PROJECT_ID;
2807
+ if (!projectId) {
2808
+ return {
2809
+ content: [{ type: "text", text: "No project linked. Run yapout_init or yapout link first." }],
2810
+ isError: true
2811
+ };
2812
+ }
2813
+ try {
2814
+ const allFindings = await ctx.client.query(
2815
+ anyApi2.functions.findings.getProjectFindings,
2816
+ { projectId }
2817
+ );
2818
+ let drafts = (allFindings ?? []).filter((f) => f.status === "draft");
2819
+ if (args.filter?.tags?.length) {
2820
+ const filterTags = new Set(args.filter.tags);
2821
+ drafts = drafts.filter(
2822
+ (f) => (f.tags ?? []).some((t) => filterTags.has(t))
2823
+ );
2824
+ }
2825
+ if (args.filter?.captureId) {
2826
+ const cid = args.filter.captureId;
2827
+ drafts = drafts.filter((f) => f.captureId === cid);
2828
+ }
2829
+ if (args.filter?.findingIds?.length) {
2830
+ const ids = new Set(args.filter.findingIds);
2831
+ drafts = drafts.filter((f) => ids.has(f._id));
2832
+ }
2833
+ const session = createSession(projectId, drafts.length, args.filter);
2834
+ return {
2835
+ content: [
2836
+ {
2837
+ type: "text",
2838
+ text: JSON.stringify({
2839
+ sessionId: session.sessionId,
2840
+ totalFindings: drafts.length,
2841
+ filter: args.filter ?? null,
2842
+ message: drafts.length > 0 ? `Starting enrichment session: ${drafts.length} finding${drafts.length !== 1 ? "s" : ""} to process.` : "No draft findings match the filter. Nothing to enrich."
2843
+ }, null, 2)
2844
+ }
2845
+ ]
2846
+ };
2847
+ } catch (err) {
2848
+ return {
2849
+ content: [{ type: "text", text: `Error starting enrichment: ${err.message}` }],
2850
+ isError: true
2851
+ };
2852
+ }
2853
+ }
2854
+ );
2855
+ }
2856
+
2857
+ // src/mcp/tools/enrich-next.ts
2858
+ import { z as z19 } from "zod";
2859
+ function registerEnrichNextTool(server, ctx) {
2860
+ server.tool(
2861
+ "yapout_enrich_next",
2862
+ `Get the next finding to enrich in a bulk enrichment session.
2863
+
2864
+ If skip=true, releases the current finding back to draft and moves on.
2865
+ Returns the next unclaimed draft finding matching the session's filter.
2866
+
2867
+ When done=true, all findings have been processed.`,
2868
+ {
2869
+ sessionId: z19.string().describe("Session ID from yapout_start_enrichment"),
2870
+ skip: z19.boolean().optional().describe("If true, skip the current finding (release back to draft)"),
2871
+ skipFindingId: z19.string().optional().describe("The finding ID to skip (must be in 'enriching' status)")
2872
+ },
2873
+ async (args) => {
2874
+ const session = getSession(args.sessionId);
2875
+ if (!session) {
2876
+ return {
2877
+ content: [{ type: "text", text: "Session not found. Start a new one with yapout_start_enrichment." }],
2878
+ isError: true
2879
+ };
2880
+ }
2881
+ try {
2882
+ if (args.skip && args.skipFindingId) {
2883
+ try {
2884
+ await ctx.client.mutation(
2885
+ anyApi2.functions.localPipeline.releaseEnrichmentClaim,
2886
+ { findingId: args.skipFindingId }
2887
+ );
2888
+ updateSessionStats(args.sessionId, {
2889
+ skipped: session.stats.skipped + 1
2890
+ });
2891
+ } catch {
2892
+ }
2893
+ }
2894
+ const allFindings = await ctx.client.query(
2895
+ anyApi2.functions.findings.getProjectFindings,
2896
+ { projectId: session.projectId }
2897
+ );
2898
+ let drafts = (allFindings ?? []).filter(
2899
+ (f) => f.status === "draft"
2900
+ );
2901
+ if (session.filter?.tags?.length) {
2902
+ const filterTags = new Set(session.filter.tags);
2903
+ drafts = drafts.filter(
2904
+ (f) => (f.tags ?? []).some((t) => filterTags.has(t))
2905
+ );
2906
+ }
2907
+ if (session.filter?.captureId) {
2908
+ const cid = session.filter.captureId;
2909
+ drafts = drafts.filter((f) => f.captureId === cid);
2910
+ }
2911
+ if (session.filter?.findingIds?.length) {
2912
+ const ids = new Set(session.filter.findingIds);
2913
+ drafts = drafts.filter((f) => ids.has(f._id));
2914
+ }
2915
+ if (drafts.length === 0) {
2916
+ const total = session.stats.enriched + session.stats.skipped;
2917
+ return {
2918
+ content: [
2919
+ {
2920
+ type: "text",
2921
+ text: JSON.stringify({
2922
+ done: true,
2923
+ position: {
2924
+ current: total,
2925
+ total: session.stats.total,
2926
+ enriched: session.stats.enriched,
2927
+ skipped: session.stats.skipped
2928
+ },
2929
+ message: `All done! ${session.stats.enriched} enriched, ${session.stats.skipped} skipped.`
2930
+ }, null, 2)
2931
+ }
2932
+ ]
2933
+ };
2934
+ }
2935
+ const next = drafts[0];
2936
+ const claimResult = await ctx.client.mutation(
2937
+ anyApi2.functions.localPipeline.claimForEnrichment,
2938
+ { projectId: session.projectId, findingId: next._id }
2939
+ );
2940
+ if (!claimResult) {
2941
+ return {
2942
+ content: [
2943
+ {
2944
+ type: "text",
2945
+ text: JSON.stringify({
2946
+ done: false,
2947
+ message: "Finding was claimed by another session. Call yapout_enrich_next again to get the next one.",
2948
+ position: {
2949
+ current: session.stats.enriched + session.stats.skipped,
2950
+ total: session.stats.total,
2951
+ enriched: session.stats.enriched,
2952
+ skipped: session.stats.skipped
2953
+ }
2954
+ }, null, 2)
2955
+ }
2956
+ ]
2957
+ };
2958
+ }
2959
+ const current = session.stats.enriched + session.stats.skipped + 1;
2960
+ const includeContext = !session.contextDelivered;
2961
+ if (includeContext) markContextDelivered(args.sessionId);
2962
+ const existingTitles = (allFindings ?? []).filter((f) => f._id !== next._id && f.status !== "archived").map((f) => f.title);
2963
+ const response = {
2964
+ finding: {
2965
+ findingId: claimResult.findingId,
2966
+ title: claimResult.title,
2967
+ description: claimResult.description,
2968
+ sourceQuote: claimResult.sourceQuote,
2969
+ type: claimResult.type,
2970
+ priority: claimResult.priority
2971
+ },
2972
+ position: {
2973
+ current,
2974
+ total: session.stats.total,
2975
+ enriched: session.stats.enriched,
2976
+ skipped: session.stats.skipped
2977
+ },
2978
+ existingFindingTitles: existingTitles,
2979
+ done: false
2980
+ };
2981
+ if (includeContext && claimResult.projectContext) {
2982
+ response.projectContext = claimResult.projectContext;
2983
+ }
2984
+ return {
2985
+ content: [
2986
+ { type: "text", text: JSON.stringify(response, null, 2) }
2987
+ ]
2988
+ };
2989
+ } catch (err) {
2990
+ return {
2991
+ content: [{ type: "text", text: `Error getting next finding: ${err.message}` }],
2992
+ isError: true
2993
+ };
2994
+ }
2995
+ }
2996
+ );
2997
+ }
2998
+
2715
2999
  // src/mcp/server.ts
2716
3000
  async function startMcpServer() {
2717
3001
  const cwd = process.cwd();
@@ -2754,8 +3038,8 @@ async function startMcpServer() {
2754
3038
  registerShipTool(server, ctx);
2755
3039
  registerCheckTool(server, ctx);
2756
3040
  registerBundleTool(server, ctx);
2757
- registerGetUnenrichedTicketsTool(server, ctx);
2758
- registerGetExistingTicketsTool(server, ctx);
3041
+ registerGetUnenrichedFindingsTool(server, ctx);
3042
+ registerGetExistingFindingsTool(server, ctx);
2759
3043
  registerSaveEnrichmentTool(server, ctx);
2760
3044
  registerSyncToLinearTool(server, ctx);
2761
3045
  registerSubmitYapSessionTool(server, ctx);
@@ -2764,6 +3048,8 @@ async function startMcpServer() {
2764
3048
  registerSaveProjectEnrichmentTool(server, ctx);
2765
3049
  registerMarkDuplicateTool(server, ctx);
2766
3050
  registerGetLinearProjectsTool(server, ctx);
3051
+ registerStartEnrichmentTool(server, ctx);
3052
+ registerEnrichNextTool(server, ctx);
2767
3053
  const transport = new StdioServerTransport();
2768
3054
  await server.connect(transport);
2769
3055
  }
@@ -3855,7 +4141,7 @@ import { Command as Command15 } from "commander";
3855
4141
  import { spawn as spawn2 } from "child_process";
3856
4142
  import { platform as platform2 } from "os";
3857
4143
  import chalk16 from "chalk";
3858
- var VALID_ACTIONS = ["claim", "enrich", "yap", "compact"];
4144
+ var VALID_ACTIONS = ["claim", "enrich", "enrich-bulk", "yap", "compact"];
3859
4145
  function parseYapoutUri(raw) {
3860
4146
  const url = new URL(raw);
3861
4147
  const action = url.hostname;
@@ -3866,13 +4152,15 @@ function parseYapoutUri(raw) {
3866
4152
  }
3867
4153
  const ticketId = url.pathname.replace(/^\//, "") || void 0;
3868
4154
  if ((action === "claim" || action === "enrich") && !ticketId) {
3869
- throw new Error(`Missing ticket ID in URI: ${raw}`);
4155
+ throw new Error(`Missing finding ID in URI: ${raw}`);
3870
4156
  }
3871
4157
  return {
3872
4158
  action,
3873
4159
  ticketId,
3874
4160
  topic: url.searchParams.get("topic") || void 0,
3875
- persona: url.searchParams.get("persona") || void 0
4161
+ persona: url.searchParams.get("persona") || void 0,
4162
+ tags: url.searchParams.get("tags")?.split(",").filter(Boolean) || void 0,
4163
+ findingIds: url.searchParams.get("findingIds")?.split(",").filter(Boolean) || void 0
3876
4164
  };
3877
4165
  }
3878
4166
  function buildPrompt(parsed) {
@@ -3885,19 +4173,42 @@ function buildPrompt(parsed) {
3885
4173
  ].join(" ");
3886
4174
  case "enrich":
3887
4175
  return [
3888
- `A ticket has been approved and needs enrichment before implementation.`,
3889
- `Call yapout_get_unenriched_ticket with ticketId for "${parsed.ticketId}" to fetch the ticket details.`,
4176
+ `Use yapout to enrich a finding.`,
3890
4177
  ``,
3891
- `Read the codebase to understand the project structure, then produce:`,
3892
- `1. An implementation brief (3-5 sentences, specific files/patterns involved)`,
3893
- `2. An enriched description with technical context`,
3894
- `3. Clarifying questions (0-5) where the answer can't be inferred`,
3895
- `4. Duplicate check \u2014 call yapout_get_existing_tickets and compare`,
3896
- `5. Scope assessment (is this too large for one PR?)`,
4178
+ `Start with filter: { findingIds: ["${parsed.ticketId}"] }`,
3897
4179
  ``,
3898
- `Call yapout_save_enrichment with your analysis.`,
3899
- `If no questions were generated, also call yapout_sync_to_linear to create the Linear ticket.`
4180
+ `Call yapout_start_enrichment, then yapout_enrich_next to claim it.`,
4181
+ `Read the codebase, ask me questions if needed, then call yapout_save_enrichment`,
4182
+ `with a clean description, acceptance criteria, and implementation brief.`
3900
4183
  ].join("\n");
4184
+ case "enrich-bulk": {
4185
+ const lines = [
4186
+ "Use yapout to enrich findings for this project. Start a bulk enrichment session and work through each finding one by one.",
4187
+ "",
4188
+ "For each finding:",
4189
+ "1. Call yapout_start_enrichment to begin" + (parsed.tags || parsed.findingIds ? " with the filter below" : ""),
4190
+ "2. Call yapout_enrich_next to get the next finding",
4191
+ "3. Read relevant code in the repository to understand the finding's context",
4192
+ "4. Ask me any clarifying questions (only if genuinely needed)",
4193
+ "5. Call yapout_save_enrichment with:",
4194
+ " - A clean, specific title",
4195
+ " - A description a senior engineer would write",
4196
+ " - Concrete acceptance criteria",
4197
+ " - An implementation brief (which files, approach, edge cases)",
4198
+ '6. If I say "skip", call yapout_enrich_next with skip=true',
4199
+ "7. After saving, if compactionHint is true or every 5 findings, run /compact",
4200
+ "8. Repeat until done",
4201
+ "",
4202
+ "Keep the pace steady. Don't over-ask \u2014 use your judgment from the code."
4203
+ ];
4204
+ if (parsed.findingIds?.length) {
4205
+ lines.push("", `These findings are bundled \u2014 enrich them as one cohesive problem.`);
4206
+ lines.push(`Start with filter: { findingIds: ${JSON.stringify(parsed.findingIds)} }`);
4207
+ } else if (parsed.tags?.length) {
4208
+ lines.push("", `Start with filter: { tags: ${JSON.stringify(parsed.tags)} }`);
4209
+ }
4210
+ return lines.join("\n");
4211
+ }
3901
4212
  case "yap": {
3902
4213
  const parts = ["Let's have a yap session"];
3903
4214
  if (parsed.topic) parts[0] += ` about ${parsed.topic}`;