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.
- package/dist/index.js +513 -202
- 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'
|
|
249
|
-
f'
|
|
250
|
-
f'
|
|
251
|
-
f'
|
|
252
|
-
f'
|
|
253
|
-
f
|
|
254
|
-
f'
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
1082
|
-
const type = statusMap.get(t.
|
|
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
|
|
1146
|
+
"Fetch the full implementation context for a finding",
|
|
1118
1147
|
{
|
|
1119
|
-
|
|
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.
|
|
1124
|
-
{
|
|
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: "
|
|
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
|
|
1191
|
+
const finding = data.finding;
|
|
1163
1192
|
const sections = [
|
|
1164
|
-
`# ${
|
|
1193
|
+
`# ${finding.title}`,
|
|
1165
1194
|
"",
|
|
1166
|
-
`**Priority:** ${
|
|
1195
|
+
`**Priority:** ${finding.priority} | **Type:** ${finding.type}`
|
|
1167
1196
|
];
|
|
1168
|
-
if (
|
|
1169
|
-
sections.push(`**Linear:** ${
|
|
1197
|
+
if (finding.linearIssueUrl) {
|
|
1198
|
+
sections.push(`**Linear:** ${finding.linearIssueUrl}`);
|
|
1170
1199
|
}
|
|
1171
|
-
sections.push("", "## 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
|
|
1234
|
+
"Claim a finding for local implementation. Creates a branch (or worktree), writes the brief, and updates status.",
|
|
1206
1235
|
{
|
|
1207
|
-
|
|
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.
|
|
1224
|
-
{
|
|
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: "
|
|
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
|
|
1238
|
-
const
|
|
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(
|
|
1242
|
-
const branchName =
|
|
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.
|
|
1245
|
-
{
|
|
1273
|
+
anyApi2.functions.findings.claimFindingLocal,
|
|
1274
|
+
{ findingId: args.findingId, branchName }
|
|
1246
1275
|
);
|
|
1247
|
-
if (
|
|
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
|
|
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.
|
|
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
|
|
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,
|
|
1488
|
+
function buildCommitMessage(message, template, finding) {
|
|
1460
1489
|
if (message) return message;
|
|
1461
1490
|
if (template) {
|
|
1462
|
-
return template.replace(/\{\{
|
|
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 =
|
|
1465
|
-
const ref =
|
|
1466
|
-
return `${prefix}(${
|
|
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
|
|
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
|
|
1499
|
-
let
|
|
1500
|
-
let
|
|
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)
|
|
1535
|
+
if (titleMatch) findingTitle = titleMatch[1];
|
|
1507
1536
|
const typeMatch = brief.match(/\*\*Type:\*\* (\w+)/);
|
|
1508
|
-
if (typeMatch)
|
|
1537
|
+
if (typeMatch) findingType = typeMatch[1];
|
|
1509
1538
|
const linearMatch = brief.match(/\*\*Linear:\*\* .+\/([A-Z]+-\d+)\//);
|
|
1510
|
-
if (linearMatch)
|
|
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:
|
|
1516
|
-
type:
|
|
1517
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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:
|
|
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:
|
|
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
|
|
1810
|
+
"Add a finding to the current finding's bundle so they ship as one PR",
|
|
1782
1811
|
{
|
|
1783
|
-
|
|
1784
|
-
|
|
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.
|
|
1789
|
-
{
|
|
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.
|
|
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.
|
|
1838
|
+
`# Bundle: ${bundledBrief.findings.length} findings`,
|
|
1810
1839
|
""
|
|
1811
1840
|
];
|
|
1812
|
-
for (const t of bundledBrief.
|
|
1813
|
-
sections.push(`## ${t.
|
|
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.
|
|
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
|
-
|
|
1841
|
-
|
|
1869
|
+
findings: bundledBrief.findings.map((t) => ({
|
|
1870
|
+
findingId: t.findingId,
|
|
1842
1871
|
title: t.title
|
|
1843
1872
|
})),
|
|
1844
1873
|
combinedBrief,
|
|
1845
|
-
message: `${bundledBrief.
|
|
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-
|
|
1886
|
+
// src/mcp/tools/get-unenriched-findings.ts
|
|
1858
1887
|
import { z as z10 } from "zod";
|
|
1859
|
-
function
|
|
1888
|
+
function registerGetUnenrichedFindingsTool(server, ctx) {
|
|
1860
1889
|
server.tool(
|
|
1861
|
-
"
|
|
1862
|
-
`Start enriching a
|
|
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
|
|
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
|
|
1869
|
-
3. If the
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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-
|
|
1929
|
-
function
|
|
1957
|
+
// src/mcp/tools/get-existing-findings.ts
|
|
1958
|
+
function registerGetExistingFindingsTool(server, ctx) {
|
|
1930
1959
|
server.tool(
|
|
1931
|
-
"
|
|
1932
|
-
"Fetch all
|
|
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
|
|
1949
|
-
anyApi2.functions.localPipeline.
|
|
1977
|
+
const findings = await ctx.client.query(
|
|
1978
|
+
anyApi2.functions.localPipeline.getExistingFindingTitles,
|
|
1950
1979
|
{ projectId }
|
|
1951
1980
|
);
|
|
1952
|
-
if (!
|
|
1981
|
+
if (!findings || findings.length === 0) {
|
|
1953
1982
|
return {
|
|
1954
1983
|
content: [
|
|
1955
1984
|
{
|
|
1956
1985
|
type: "text",
|
|
1957
|
-
text: "No existing
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
2058
|
+
The finding transitions: enriching \u2192 enriched \u2192 synced.`,
|
|
2000
2059
|
{
|
|
2001
|
-
|
|
2002
|
-
title: z11.string().describe("Refined
|
|
2003
|
-
cleanDescription: z11.string().describe("Human-readable summary for the Linear issue body. Write the kind of
|
|
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
|
|
2013
|
-
suggestedSplit: z11.array(z11.string()).optional().describe("If oversized: suggested sub-
|
|
2014
|
-
level: z11.enum(["project", "issue"]).optional().describe("Override the
|
|
2015
|
-
nature: z11.enum(["implementable", "operational", "spike"]).optional().describe("Override the
|
|
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
|
-
|
|
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.
|
|
2036
|
-
{
|
|
2095
|
+
anyApi2.functions.localPipeline.syncFindingToLinearLocal,
|
|
2096
|
+
{ findingId: args.findingId }
|
|
2037
2097
|
);
|
|
2038
|
-
const
|
|
2039
|
-
anyApi2.functions.
|
|
2040
|
-
{
|
|
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
|
-
|
|
2044
|
-
linearIssueId:
|
|
2045
|
-
|
|
2046
|
-
|
|
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
|
|
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
|
|
2151
|
+
"Trigger Linear issue creation for an enriched finding. The sync runs server-side (encrypted Linear token in Convex).",
|
|
2080
2152
|
{
|
|
2081
|
-
|
|
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.
|
|
2087
|
-
{
|
|
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
|
-
|
|
2097
|
-
message: "
|
|
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
|
|
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
|
|
2144
|
-
anyApi2.functions.
|
|
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
|
-
|
|
2158
|
-
message: "Yap session submitted.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2252
|
-
- Could a developer read this
|
|
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
|
|
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
|
|
2333
|
+
For each finding, you must make an honest call: is this enriched or not?
|
|
2262
2334
|
|
|
2263
|
-
Mark a
|
|
2264
|
-
- The conversation covered it thoroughly enough to produce a clear
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2302
|
-
- enrichedDescription: clean, final description \u2014 write the kind of
|
|
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
|
|
2377
|
+
- clarifications: relevant Q&A from the conversation that shaped the finding
|
|
2306
2378
|
|
|
2307
|
-
For PROJECT-level
|
|
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
|
|
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
|
-
-
|
|
2395
|
+
- findings: the array (projects with children, standalone issues)
|
|
2324
2396
|
|
|
2325
|
-
This creates
|
|
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
|
|
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("
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
2455
|
+
findings: z15.array(
|
|
2384
2456
|
z15.object({
|
|
2385
|
-
title: z15.string().describe("
|
|
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("
|
|
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
|
|
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
|
-
|
|
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("
|
|
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.
|
|
2493
|
+
anyApi2.functions.captures.extractFromYapSession,
|
|
2422
2494
|
{
|
|
2423
2495
|
projectId: ctx.projectId,
|
|
2424
2496
|
title: args.sessionTitle,
|
|
2425
2497
|
transcript: args.sessionTranscript,
|
|
2426
|
-
|
|
2498
|
+
findings: args.findings
|
|
2427
2499
|
}
|
|
2428
2500
|
);
|
|
2429
2501
|
let enrichedCount = 0;
|
|
2430
2502
|
let draftCount = 0;
|
|
2431
|
-
let
|
|
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
|
-
|
|
2436
|
-
if (child.
|
|
2507
|
+
totalFindings++;
|
|
2508
|
+
if (child.findingStatus === "enriched") enrichedCount++;
|
|
2437
2509
|
else draftCount++;
|
|
2438
2510
|
}
|
|
2439
2511
|
} else {
|
|
2440
|
-
|
|
2441
|
-
if (item.
|
|
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
|
-
|
|
2526
|
+
captureId: result.captureId,
|
|
2455
2527
|
items: result.items,
|
|
2456
2528
|
summary: {
|
|
2457
|
-
|
|
2529
|
+
totalFindings,
|
|
2458
2530
|
enriched: enrichedCount,
|
|
2459
2531
|
needsEnrichment: draftCount
|
|
2460
2532
|
},
|
|
2461
|
-
message: `Created ${
|
|
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
|
|
2561
|
+
`Save the decomposition of a project-level finding into implementable child issues.
|
|
2490
2562
|
|
|
2491
|
-
Call this after enriching a
|
|
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
|
|
2497
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2619
|
+
findingId: result.findingId,
|
|
2548
2620
|
status: "decomposed",
|
|
2549
|
-
|
|
2621
|
+
childFindingIds: result.childFindingIds,
|
|
2550
2622
|
suggestedOrder: result.suggestedOrder,
|
|
2551
|
-
message: `Project decomposed into ${result.
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
2675
|
+
anyApi2.functions.findings.markDuplicate,
|
|
2604
2676
|
{
|
|
2605
|
-
|
|
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
|
-
|
|
2688
|
+
findingId: result.findingId,
|
|
2617
2689
|
status: "failed",
|
|
2618
2690
|
duplicateOf: result.duplicateOfLinearId,
|
|
2619
|
-
message: `
|
|
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
|
-
|
|
2758
|
-
|
|
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
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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
|
|
3899
|
-
`
|
|
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}`;
|