yapout 0.3.1 → 0.4.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 +746 -206
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -239,9 +239,10 @@ def main():
|
|
|
239
239
|
# Build claude prompt based on action
|
|
240
240
|
if action == "claim":
|
|
241
241
|
prompt = (
|
|
242
|
-
f'Use yapout to implement
|
|
243
|
-
f'Claim it with yapout_claim (use worktree mode), read the brief, '
|
|
244
|
-
f'implement the changes, run yapout_check, then ship with yapout_ship.'
|
|
242
|
+
f'Use yapout to implement work item "{ticket_id}". '
|
|
243
|
+
f'Claim it with yapout_claim using workItemId (use worktree mode), read the brief, '
|
|
244
|
+
f'implement the changes, run yapout_check, then ship with yapout_ship. '
|
|
245
|
+
f'The ID may be a bundle or standalone finding \u2014 yapout_claim auto-detects.'
|
|
245
246
|
)
|
|
246
247
|
elif action == "enrich":
|
|
247
248
|
prompt = (
|
|
@@ -291,6 +292,29 @@ def main():
|
|
|
291
292
|
prompt += f" about {topic}"
|
|
292
293
|
if persona and persona != "tech lead":
|
|
293
294
|
prompt += f" \u2014 be a {persona}"
|
|
295
|
+
elif action == "compact":
|
|
296
|
+
elif action == "enrich-bundle":
|
|
297
|
+
bundle_id = ticket_id
|
|
298
|
+
prompt = (
|
|
299
|
+
f'Use yapout to enrich a bundle as a single cohesive unit.\\n'
|
|
300
|
+
f'\\n'
|
|
301
|
+
f'1. Call yapout_enrich_bundle with bundleId "{bundle_id}" to claim it and get all findings\\n'
|
|
302
|
+
f'2. Read the codebase to understand the full scope of the bundle\\n'
|
|
303
|
+
f'3. Ask me any clarifying questions about the bundle as a whole (not individual findings)\\n'
|
|
304
|
+
f'4. For each finding, formulate:\\n'
|
|
305
|
+
f' - A refined title\\n'
|
|
306
|
+
f' - A specific description\\n'
|
|
307
|
+
f' - Concrete acceptance criteria\\n'
|
|
308
|
+
f' - An implementation brief\\n'
|
|
309
|
+
f'5. Also formulate bundle-level:\\n'
|
|
310
|
+
f' - An overall description of what the bundle delivers\\n'
|
|
311
|
+
f' - Combined acceptance criteria\\n'
|
|
312
|
+
f' - An architecture/approach brief that covers the full scope\\n'
|
|
313
|
+
f'6. Call yapout_save_bundle_enrichment with ALL enrichment data at once\\n'
|
|
314
|
+
f'\\n'
|
|
315
|
+
f'Treat this as ONE problem, not separate findings. Understand how they relate,\\n'
|
|
316
|
+
f'identify dependencies, and write briefs that reference each other.'
|
|
317
|
+
)
|
|
294
318
|
elif action == "compact":
|
|
295
319
|
prompt = "Run yapout_compact to update the project context summary."
|
|
296
320
|
else:
|
|
@@ -1063,12 +1087,42 @@ function registerUpdateContextTool(server, ctx) {
|
|
|
1063
1087
|
|
|
1064
1088
|
// src/mcp/tools/queue.ts
|
|
1065
1089
|
import { z as z3 } from "zod";
|
|
1090
|
+
function formatFinding(f, indent, done) {
|
|
1091
|
+
const ref = f.linearIssueId ? ` ${f.linearIssueId}` : "";
|
|
1092
|
+
const prefix = done ? "\u2713 " : "";
|
|
1093
|
+
return `${indent}${prefix}${f.title} ${f.type}\xB7${f.priority}${ref}`;
|
|
1094
|
+
}
|
|
1095
|
+
function formatWorkItem(item, done) {
|
|
1096
|
+
const lines = [];
|
|
1097
|
+
if (item.kind === "bundle") {
|
|
1098
|
+
const count = item.findings.length;
|
|
1099
|
+
const priority = item.findings.length > 0 ? item.findings.map((f) => f.priority).sort((a, b) => {
|
|
1100
|
+
const order = { urgent: 0, high: 1, medium: 2, low: 3 };
|
|
1101
|
+
return (order[a] ?? 3) - (order[b] ?? 3);
|
|
1102
|
+
})[0] : "medium";
|
|
1103
|
+
const prefix = done ? "\u2713 " : "";
|
|
1104
|
+
const prRef = done && item.pr?.githubPrNumber ? ` PR #${item.pr.githubPrNumber}` : "";
|
|
1105
|
+
lines.push(` ${prefix}\u{1F4E6} ${item.title} (${count} findings) \u2014 ${priority}${prRef}`);
|
|
1106
|
+
for (const f of item.findings) {
|
|
1107
|
+
lines.push(formatFinding(f, " ", done));
|
|
1108
|
+
}
|
|
1109
|
+
} else {
|
|
1110
|
+
const f = item.findings[0];
|
|
1111
|
+
if (f) {
|
|
1112
|
+
const prRef = done && item.pr?.githubPrNumber ? ` PR #${item.pr.githubPrNumber}` : "";
|
|
1113
|
+
const prefix = done ? "\u2713 " : "";
|
|
1114
|
+
const ref = f.linearIssueId ? ` ${f.linearIssueId}` : "";
|
|
1115
|
+
lines.push(` ${prefix}${item.title} ${f.type}\xB7${f.priority}${ref}${prRef}`);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
return lines.join("\n");
|
|
1119
|
+
}
|
|
1066
1120
|
function registerQueueTool(server, ctx) {
|
|
1067
1121
|
server.tool(
|
|
1068
1122
|
"yapout_queue",
|
|
1069
|
-
"List findings ready for local implementation
|
|
1123
|
+
"List work items (bundles and standalone findings) ready for local implementation, plus active and done items.",
|
|
1070
1124
|
{
|
|
1071
|
-
|
|
1125
|
+
includeIds: z3.boolean().optional().describe("Include work item IDs in output (default: false)")
|
|
1072
1126
|
},
|
|
1073
1127
|
async (args) => {
|
|
1074
1128
|
if (!ctx.projectId) {
|
|
@@ -1083,55 +1137,84 @@ function registerQueueTool(server, ctx) {
|
|
|
1083
1137
|
};
|
|
1084
1138
|
}
|
|
1085
1139
|
const data = await ctx.client.query(
|
|
1086
|
-
anyApi2.functions.
|
|
1140
|
+
anyApi2.functions.workQueue.getWorkQueue,
|
|
1087
1141
|
{ projectId: ctx.projectId }
|
|
1088
1142
|
);
|
|
1089
1143
|
if (!data) {
|
|
1090
1144
|
return {
|
|
1091
1145
|
content: [
|
|
1092
|
-
{ type: "text", text: "Could not fetch queue." }
|
|
1146
|
+
{ type: "text", text: "Could not fetch work queue." }
|
|
1093
1147
|
],
|
|
1094
1148
|
isError: true
|
|
1095
1149
|
};
|
|
1096
1150
|
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
ready = ready.filter((t) => {
|
|
1110
|
-
if (!t.linearIssueId) return true;
|
|
1111
|
-
const type = statusMap.get(t.linearIssueId);
|
|
1112
|
-
if (!type) return true;
|
|
1113
|
-
return type === "backlog" || type === "unstarted";
|
|
1114
|
-
});
|
|
1115
|
-
} catch {
|
|
1151
|
+
const sections = [];
|
|
1152
|
+
sections.push(`Ready (${data.ready.length}):`);
|
|
1153
|
+
if (data.ready.length === 0) {
|
|
1154
|
+
sections.push(" (none)");
|
|
1155
|
+
} else {
|
|
1156
|
+
for (const item of data.ready) {
|
|
1157
|
+
const line = formatWorkItem(item);
|
|
1158
|
+
if (args.includeIds) {
|
|
1159
|
+
sections.push(`${line} [${item.id}]`);
|
|
1160
|
+
} else {
|
|
1161
|
+
sections.push(line);
|
|
1162
|
+
}
|
|
1116
1163
|
}
|
|
1117
1164
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1165
|
+
sections.push("");
|
|
1166
|
+
sections.push(`Active (${data.active.length}):`);
|
|
1167
|
+
if (data.active.length === 0) {
|
|
1168
|
+
sections.push(" (none)");
|
|
1169
|
+
} else {
|
|
1170
|
+
for (const item of data.active) {
|
|
1171
|
+
const statusTag = item.status === "failed" ? " \u274C FAILED" : item.status === "review" ? " \u{1F50D} review" : "";
|
|
1172
|
+
const line = formatWorkItem(item);
|
|
1173
|
+
if (args.includeIds) {
|
|
1174
|
+
sections.push(`${line}${statusTag} [${item.id}]`);
|
|
1175
|
+
} else {
|
|
1176
|
+
sections.push(`${line}${statusTag}`);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
sections.push("");
|
|
1181
|
+
sections.push(`Done (${data.done.length}):`);
|
|
1182
|
+
if (data.done.length === 0) {
|
|
1183
|
+
sections.push(" (none)");
|
|
1184
|
+
} else {
|
|
1185
|
+
for (const item of data.done) {
|
|
1186
|
+
const line = formatWorkItem(item, true);
|
|
1187
|
+
if (args.includeIds) {
|
|
1188
|
+
sections.push(`${line} [${item.id}]`);
|
|
1189
|
+
} else {
|
|
1190
|
+
sections.push(line);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1125
1193
|
}
|
|
1126
|
-
if (data.
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
) : null;
|
|
1130
|
-
result.note = daysAgo ? `Project context was last updated ${daysAgo} days ago. Consider running yapout_compact.` : "Project context has never been generated. Run yapout_compact.";
|
|
1194
|
+
if (data.agentStatus.isActive) {
|
|
1195
|
+
sections.push("");
|
|
1196
|
+
sections.push(`Agent: ${data.agentStatus.agentCount} active, ${data.agentStatus.worktreeCount} worktrees`);
|
|
1131
1197
|
}
|
|
1198
|
+
const structured = {
|
|
1199
|
+
ready: data.ready.map((item) => ({
|
|
1200
|
+
workItemId: item.id,
|
|
1201
|
+
kind: item.kind,
|
|
1202
|
+
title: item.title,
|
|
1203
|
+
findings: item.findings.map((f) => ({
|
|
1204
|
+
id: f.id,
|
|
1205
|
+
title: f.title,
|
|
1206
|
+
type: f.type,
|
|
1207
|
+
priority: f.priority,
|
|
1208
|
+
linearIssueId: f.linearIssueId
|
|
1209
|
+
}))
|
|
1210
|
+
})),
|
|
1211
|
+
activeCount: data.active.length,
|
|
1212
|
+
doneCount: data.done.length
|
|
1213
|
+
};
|
|
1132
1214
|
return {
|
|
1133
1215
|
content: [
|
|
1134
|
-
{ type: "text", text:
|
|
1216
|
+
{ type: "text", text: sections.join("\n") },
|
|
1217
|
+
{ type: "text", text: "\n---\nStructured data:\n" + JSON.stringify(structured, null, 2) }
|
|
1135
1218
|
]
|
|
1136
1219
|
};
|
|
1137
1220
|
}
|
|
@@ -1143,30 +1226,84 @@ import { z as z4 } from "zod";
|
|
|
1143
1226
|
function registerGetBriefTool(server, ctx) {
|
|
1144
1227
|
server.tool(
|
|
1145
1228
|
"yapout_get_brief",
|
|
1146
|
-
"Fetch the full implementation context for a finding",
|
|
1229
|
+
"Fetch the full implementation context for a work item (finding or bundle)",
|
|
1147
1230
|
{
|
|
1148
|
-
findingId: z4.string().describe("The finding ID to get the brief for")
|
|
1231
|
+
findingId: z4.string().optional().describe("The finding ID to get the brief for (deprecated: use workItemId)"),
|
|
1232
|
+
workItemId: z4.string().optional().describe("The work item ID (finding or bundle) to get the brief for")
|
|
1149
1233
|
},
|
|
1150
1234
|
async (args) => {
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1153
|
-
{ findingId: args.findingId }
|
|
1154
|
-
);
|
|
1155
|
-
if (!data) {
|
|
1235
|
+
const itemId = args.workItemId || args.findingId;
|
|
1236
|
+
if (!itemId) {
|
|
1156
1237
|
return {
|
|
1157
1238
|
content: [
|
|
1158
1239
|
{
|
|
1159
1240
|
type: "text",
|
|
1160
|
-
text: "
|
|
1241
|
+
text: "Must provide workItemId or findingId."
|
|
1161
1242
|
}
|
|
1162
1243
|
],
|
|
1163
1244
|
isError: true
|
|
1164
1245
|
};
|
|
1165
1246
|
}
|
|
1247
|
+
try {
|
|
1248
|
+
const data = await ctx.client.query(
|
|
1249
|
+
anyApi2.functions.findings.getFindingBrief,
|
|
1250
|
+
{ findingId: itemId }
|
|
1251
|
+
);
|
|
1252
|
+
if (data) {
|
|
1253
|
+
return {
|
|
1254
|
+
content: [
|
|
1255
|
+
{ type: "text", text: JSON.stringify(data, null, 2) }
|
|
1256
|
+
]
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
} catch {
|
|
1260
|
+
}
|
|
1261
|
+
try {
|
|
1262
|
+
const bundle = await ctx.client.query(
|
|
1263
|
+
anyApi2.functions.bundles.getBundle,
|
|
1264
|
+
{ bundleId: itemId }
|
|
1265
|
+
);
|
|
1266
|
+
if (bundle) {
|
|
1267
|
+
const result = {
|
|
1268
|
+
kind: "bundle",
|
|
1269
|
+
bundle: {
|
|
1270
|
+
id: bundle._id,
|
|
1271
|
+
title: bundle.title,
|
|
1272
|
+
description: bundle.description,
|
|
1273
|
+
enrichedDescription: bundle.enrichedDescription,
|
|
1274
|
+
acceptanceCriteria: bundle.acceptanceCriteria,
|
|
1275
|
+
implementationBrief: bundle.implementationBrief
|
|
1276
|
+
},
|
|
1277
|
+
findings: bundle.findings.map((f) => ({
|
|
1278
|
+
id: f._id,
|
|
1279
|
+
title: f.title,
|
|
1280
|
+
description: f.description,
|
|
1281
|
+
priority: f.priority,
|
|
1282
|
+
type: f.type,
|
|
1283
|
+
linearIssueId: f.linearIssueId,
|
|
1284
|
+
linearIssueUrl: f.linearIssueUrl,
|
|
1285
|
+
enrichedDescription: f.enrichedDescription,
|
|
1286
|
+
acceptanceCriteria: f.acceptanceCriteria,
|
|
1287
|
+
implementationBrief: f.implementationBrief,
|
|
1288
|
+
dependsOn: f.dependsOn
|
|
1289
|
+
}))
|
|
1290
|
+
};
|
|
1291
|
+
return {
|
|
1292
|
+
content: [
|
|
1293
|
+
{ type: "text", text: JSON.stringify(result, null, 2) }
|
|
1294
|
+
]
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
} catch {
|
|
1298
|
+
}
|
|
1166
1299
|
return {
|
|
1167
1300
|
content: [
|
|
1168
|
-
{
|
|
1169
|
-
|
|
1301
|
+
{
|
|
1302
|
+
type: "text",
|
|
1303
|
+
text: "Work item not found or you don't have access. Provide a valid finding ID or bundle ID."
|
|
1304
|
+
}
|
|
1305
|
+
],
|
|
1306
|
+
isError: true
|
|
1170
1307
|
};
|
|
1171
1308
|
}
|
|
1172
1309
|
);
|
|
@@ -1187,7 +1324,7 @@ function readBranchPrefix(cwd) {
|
|
|
1187
1324
|
function slugify(text) {
|
|
1188
1325
|
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
1189
1326
|
}
|
|
1190
|
-
function
|
|
1327
|
+
function formatFindingBrief(data) {
|
|
1191
1328
|
const finding = data.finding;
|
|
1192
1329
|
const sections = [
|
|
1193
1330
|
`# ${finding.title}`,
|
|
@@ -1228,12 +1365,89 @@ function formatBrief(data) {
|
|
|
1228
1365
|
}
|
|
1229
1366
|
return sections.join("\n");
|
|
1230
1367
|
}
|
|
1368
|
+
function formatBundleBrief(bundle, projectContext) {
|
|
1369
|
+
const sections = [
|
|
1370
|
+
`# Bundle: ${bundle.title}`,
|
|
1371
|
+
"",
|
|
1372
|
+
`**${bundle.findings.length} findings**`
|
|
1373
|
+
];
|
|
1374
|
+
if (bundle.description) {
|
|
1375
|
+
sections.push("", "## Bundle Description", "", bundle.description);
|
|
1376
|
+
}
|
|
1377
|
+
if (bundle.enrichedDescription) {
|
|
1378
|
+
sections.push("", "## Enriched Description", "", bundle.enrichedDescription);
|
|
1379
|
+
}
|
|
1380
|
+
if (bundle.acceptanceCriteria && bundle.acceptanceCriteria.length > 0) {
|
|
1381
|
+
sections.push("", "## Acceptance Criteria");
|
|
1382
|
+
for (const ac of bundle.acceptanceCriteria) {
|
|
1383
|
+
sections.push(`- [ ] ${ac}`);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
if (bundle.implementationBrief) {
|
|
1387
|
+
sections.push("", "## Implementation Brief", "", bundle.implementationBrief);
|
|
1388
|
+
}
|
|
1389
|
+
sections.push("", "---", "", "## Findings (Execution Order)", "");
|
|
1390
|
+
for (let i = 0; i < bundle.findings.length; i++) {
|
|
1391
|
+
const f = bundle.findings[i];
|
|
1392
|
+
const linearRef = f.linearIssueId ? ` (${f.linearIssueId})` : "";
|
|
1393
|
+
const deps = f.dependsOn && f.dependsOn.length > 0 ? `
|
|
1394
|
+
**Depends on:** ${f.dependsOn.map((d) => {
|
|
1395
|
+
const dep = bundle.findings.find((bf) => bf._id === d);
|
|
1396
|
+
return dep ? dep.title : d;
|
|
1397
|
+
}).join(", ")}` : "";
|
|
1398
|
+
sections.push(`### ${i + 1}. ${f.title}${linearRef}`);
|
|
1399
|
+
sections.push("");
|
|
1400
|
+
sections.push(`**Priority:** ${f.priority} | **Type:** ${f.type}`);
|
|
1401
|
+
if (f.linearIssueUrl) {
|
|
1402
|
+
sections.push(`**Linear:** ${f.linearIssueUrl}`);
|
|
1403
|
+
}
|
|
1404
|
+
if (deps) sections.push(deps);
|
|
1405
|
+
sections.push("");
|
|
1406
|
+
if (f.enrichedDescription) {
|
|
1407
|
+
sections.push(f.enrichedDescription);
|
|
1408
|
+
sections.push("");
|
|
1409
|
+
} else if (f.description) {
|
|
1410
|
+
sections.push(f.description);
|
|
1411
|
+
sections.push("");
|
|
1412
|
+
}
|
|
1413
|
+
if (f.acceptanceCriteria && f.acceptanceCriteria.length > 0) {
|
|
1414
|
+
sections.push("**Acceptance Criteria:**");
|
|
1415
|
+
for (const ac of f.acceptanceCriteria) {
|
|
1416
|
+
sections.push(`- [ ] ${ac}`);
|
|
1417
|
+
}
|
|
1418
|
+
sections.push("");
|
|
1419
|
+
}
|
|
1420
|
+
if (f.implementationBrief) {
|
|
1421
|
+
sections.push("**Implementation Brief:**");
|
|
1422
|
+
sections.push(f.implementationBrief);
|
|
1423
|
+
sections.push("");
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
if (projectContext) {
|
|
1427
|
+
sections.push("---", "", "## Project Context", "", projectContext);
|
|
1428
|
+
}
|
|
1429
|
+
return sections.join("\n");
|
|
1430
|
+
}
|
|
1431
|
+
async function detectWorkItemKind(client, workItemId) {
|
|
1432
|
+
try {
|
|
1433
|
+
const bundle = await client.query(
|
|
1434
|
+
anyApi2.functions.bundles.getBundle,
|
|
1435
|
+
{ bundleId: workItemId }
|
|
1436
|
+
);
|
|
1437
|
+
if (bundle) {
|
|
1438
|
+
return { kind: "bundle", data: bundle };
|
|
1439
|
+
}
|
|
1440
|
+
} catch {
|
|
1441
|
+
}
|
|
1442
|
+
return { kind: "standalone", data: null };
|
|
1443
|
+
}
|
|
1231
1444
|
function registerClaimTool(server, ctx) {
|
|
1232
1445
|
server.tool(
|
|
1233
1446
|
"yapout_claim",
|
|
1234
|
-
"Claim a finding for local implementation. Creates a branch (or worktree), writes the brief, and updates status.",
|
|
1447
|
+
"Claim a work item (bundle or standalone finding) for local implementation. Creates a branch (or worktree), writes the brief, and updates status.",
|
|
1235
1448
|
{
|
|
1236
|
-
|
|
1449
|
+
workItemId: z5.string().describe("The work item ID to claim (bundle ID or finding ID from yapout_queue)"),
|
|
1450
|
+
findingId: z5.string().optional().describe("Deprecated: use workItemId instead. If provided, treated as a standalone finding."),
|
|
1237
1451
|
worktree: z5.boolean().optional().describe("Create a git worktree for parallel work (default: false)")
|
|
1238
1452
|
},
|
|
1239
1453
|
async (args) => {
|
|
@@ -1248,131 +1462,262 @@ function registerClaimTool(server, ctx) {
|
|
|
1248
1462
|
isError: true
|
|
1249
1463
|
};
|
|
1250
1464
|
}
|
|
1251
|
-
const
|
|
1252
|
-
|
|
1253
|
-
{ findingId: args.findingId }
|
|
1254
|
-
);
|
|
1255
|
-
if (!briefData) {
|
|
1465
|
+
const itemId = args.workItemId || args.findingId;
|
|
1466
|
+
if (!itemId) {
|
|
1256
1467
|
return {
|
|
1257
1468
|
content: [
|
|
1258
1469
|
{
|
|
1259
1470
|
type: "text",
|
|
1260
|
-
text: "
|
|
1471
|
+
text: "Must provide workItemId (or deprecated findingId)."
|
|
1261
1472
|
}
|
|
1262
1473
|
],
|
|
1263
1474
|
isError: true
|
|
1264
1475
|
};
|
|
1265
1476
|
}
|
|
1266
|
-
const
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
const branchName = linearIssueId ? `${prefix}/${linearIssueId.toLowerCase()}-${slug}` : `${prefix}/${slug}`;
|
|
1272
|
-
const claim = await ctx.client.mutation(
|
|
1273
|
-
anyApi2.functions.findings.claimFindingLocal,
|
|
1274
|
-
{ findingId: args.findingId, branchName }
|
|
1275
|
-
);
|
|
1276
|
-
if (linearIssueId && ctx.projectId) {
|
|
1277
|
-
try {
|
|
1278
|
-
await ctx.client.action(
|
|
1279
|
-
anyApi2.functions.linearStatusMutations.moveIssueStatus,
|
|
1280
|
-
{
|
|
1281
|
-
projectId: ctx.projectId,
|
|
1282
|
-
linearIssueId,
|
|
1283
|
-
statusType: "started"
|
|
1284
|
-
}
|
|
1285
|
-
);
|
|
1286
|
-
} catch {
|
|
1287
|
-
}
|
|
1477
|
+
const { kind, data: bundleData } = await detectWorkItemKind(ctx.client, itemId);
|
|
1478
|
+
if (kind === "bundle" && bundleData) {
|
|
1479
|
+
return await claimBundle(ctx, args, itemId, bundleData);
|
|
1480
|
+
} else {
|
|
1481
|
+
return await claimStandalone(ctx, args, itemId);
|
|
1288
1482
|
}
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
await ctx.client.mutation(
|
|
1304
|
-
anyApi2.functions.pipelineRuns.reportDaemonEvent,
|
|
1305
|
-
{
|
|
1306
|
-
pipelineRunId: claim.pipelineRunId,
|
|
1307
|
-
event: "worktree_created",
|
|
1308
|
-
message: `Worktree: ${worktreePath}`
|
|
1309
|
-
}
|
|
1310
|
-
);
|
|
1311
|
-
} catch {
|
|
1483
|
+
}
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
async function claimStandalone(ctx, args, findingId) {
|
|
1487
|
+
const briefData = await ctx.client.query(
|
|
1488
|
+
anyApi2.functions.findings.getFindingBrief,
|
|
1489
|
+
{ findingId }
|
|
1490
|
+
);
|
|
1491
|
+
if (!briefData) {
|
|
1492
|
+
return {
|
|
1493
|
+
content: [
|
|
1494
|
+
{
|
|
1495
|
+
type: "text",
|
|
1496
|
+
text: "Finding not found or you don't have access."
|
|
1312
1497
|
}
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1498
|
+
],
|
|
1499
|
+
isError: true
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
const finding = briefData.finding;
|
|
1503
|
+
const linearIssueId = briefData.linearIssueId;
|
|
1504
|
+
const defaultBranch = briefData.defaultBranch || "main";
|
|
1505
|
+
const prefix = readBranchPrefix(ctx.cwd);
|
|
1506
|
+
const slug = slugify(finding.title);
|
|
1507
|
+
const branchName = linearIssueId ? `${prefix}/${linearIssueId.toLowerCase()}-${slug}` : `${prefix}/${slug}`;
|
|
1508
|
+
const localClaim = await ctx.client.mutation(
|
|
1509
|
+
anyApi2.functions.findings.claimFindingLocal,
|
|
1510
|
+
{ findingId, branchName }
|
|
1511
|
+
);
|
|
1512
|
+
const claim = await ctx.client.mutation(
|
|
1513
|
+
anyApi2.functions.workQueue.claimForImplementation,
|
|
1514
|
+
{
|
|
1515
|
+
projectId: ctx.projectId,
|
|
1516
|
+
workItemId: findingId,
|
|
1517
|
+
workItemKind: "standalone"
|
|
1518
|
+
}
|
|
1519
|
+
);
|
|
1520
|
+
if (linearIssueId && ctx.projectId) {
|
|
1521
|
+
try {
|
|
1522
|
+
await ctx.client.action(
|
|
1523
|
+
anyApi2.functions.linearStatusMutations.moveIssueStatus,
|
|
1524
|
+
{
|
|
1525
|
+
projectId: ctx.projectId,
|
|
1526
|
+
linearIssueId,
|
|
1527
|
+
statusType: "started"
|
|
1528
|
+
}
|
|
1529
|
+
);
|
|
1530
|
+
} catch {
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
const brief = formatFindingBrief(briefData);
|
|
1534
|
+
if (args.worktree) {
|
|
1535
|
+
return await setupWorktree(ctx, findingId, branchName, defaultBranch, brief, localClaim.pipelineRunId);
|
|
1536
|
+
}
|
|
1537
|
+
fetchOrigin(ctx.cwd);
|
|
1538
|
+
checkoutNewBranch(branchName, defaultBranch, ctx.cwd);
|
|
1539
|
+
writeBrief(ctx.cwd, brief);
|
|
1540
|
+
reportClaimEvents(ctx, localClaim.pipelineRunId, finding.title, branchName);
|
|
1541
|
+
return {
|
|
1542
|
+
content: [
|
|
1543
|
+
{
|
|
1544
|
+
type: "text",
|
|
1545
|
+
text: JSON.stringify(
|
|
1546
|
+
{
|
|
1547
|
+
kind: "standalone",
|
|
1548
|
+
branch: branchName,
|
|
1549
|
+
briefPath: ".yapout/brief.md",
|
|
1550
|
+
brief,
|
|
1551
|
+
pipelineRunId: localClaim.pipelineRunId,
|
|
1552
|
+
executionPlan: claim.executionPlan
|
|
1553
|
+
},
|
|
1554
|
+
null,
|
|
1555
|
+
2
|
|
1556
|
+
)
|
|
1331
1557
|
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1558
|
+
]
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
async function claimBundle(ctx, args, bundleId, bundleData) {
|
|
1562
|
+
const defaultBranch = await getDefaultBranchForProject(ctx);
|
|
1563
|
+
const prefix = readBranchPrefix(ctx.cwd);
|
|
1564
|
+
const firstLinearId = bundleData.findings.find((f) => f.linearIssueId)?.linearIssueId;
|
|
1565
|
+
const slug = slugify(bundleData.title);
|
|
1566
|
+
const branchName = firstLinearId ? `${prefix}/${firstLinearId.toLowerCase()}-${slug}` : `${prefix}/bundle-${slug}`;
|
|
1567
|
+
const primaryFinding = bundleData.findings[0];
|
|
1568
|
+
if (!primaryFinding) {
|
|
1569
|
+
return {
|
|
1570
|
+
content: [
|
|
1571
|
+
{
|
|
1572
|
+
type: "text",
|
|
1573
|
+
text: "Bundle has no findings."
|
|
1574
|
+
}
|
|
1575
|
+
],
|
|
1576
|
+
isError: true
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
const localClaim = await ctx.client.mutation(
|
|
1580
|
+
anyApi2.functions.findings.claimFindingLocal,
|
|
1581
|
+
{ findingId: primaryFinding._id, branchName }
|
|
1582
|
+
);
|
|
1583
|
+
const claim = await ctx.client.mutation(
|
|
1584
|
+
anyApi2.functions.workQueue.claimForImplementation,
|
|
1585
|
+
{
|
|
1586
|
+
projectId: ctx.projectId,
|
|
1587
|
+
workItemId: bundleId,
|
|
1588
|
+
workItemKind: "bundle"
|
|
1589
|
+
}
|
|
1590
|
+
);
|
|
1591
|
+
for (const f of bundleData.findings) {
|
|
1592
|
+
if (f.linearIssueId && ctx.projectId) {
|
|
1338
1593
|
try {
|
|
1339
|
-
await ctx.client.
|
|
1340
|
-
anyApi2.functions.
|
|
1341
|
-
{
|
|
1342
|
-
pipelineRunId: claim.pipelineRunId,
|
|
1343
|
-
event: "daemon_claimed",
|
|
1344
|
-
message: `Claimed finding: ${finding.title}`
|
|
1345
|
-
}
|
|
1346
|
-
);
|
|
1347
|
-
await ctx.client.mutation(
|
|
1348
|
-
anyApi2.functions.pipelineRuns.reportDaemonEvent,
|
|
1594
|
+
await ctx.client.action(
|
|
1595
|
+
anyApi2.functions.linearStatusMutations.moveIssueStatus,
|
|
1349
1596
|
{
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1597
|
+
projectId: ctx.projectId,
|
|
1598
|
+
linearIssueId: f.linearIssueId,
|
|
1599
|
+
statusType: "started"
|
|
1353
1600
|
}
|
|
1354
1601
|
);
|
|
1355
1602
|
} catch {
|
|
1356
1603
|
}
|
|
1357
|
-
return {
|
|
1358
|
-
content: [
|
|
1359
|
-
{
|
|
1360
|
-
type: "text",
|
|
1361
|
-
text: JSON.stringify(
|
|
1362
|
-
{
|
|
1363
|
-
branch: branchName,
|
|
1364
|
-
briefPath: ".yapout/brief.md",
|
|
1365
|
-
brief,
|
|
1366
|
-
pipelineRunId: claim.pipelineRunId
|
|
1367
|
-
},
|
|
1368
|
-
null,
|
|
1369
|
-
2
|
|
1370
|
-
)
|
|
1371
|
-
}
|
|
1372
|
-
]
|
|
1373
|
-
};
|
|
1374
1604
|
}
|
|
1605
|
+
}
|
|
1606
|
+
let projectContext;
|
|
1607
|
+
try {
|
|
1608
|
+
const briefData = await ctx.client.query(
|
|
1609
|
+
anyApi2.functions.findings.getFindingBrief,
|
|
1610
|
+
{ findingId: primaryFinding._id }
|
|
1611
|
+
);
|
|
1612
|
+
projectContext = briefData?.projectContext;
|
|
1613
|
+
} catch {
|
|
1614
|
+
}
|
|
1615
|
+
const brief = formatBundleBrief(bundleData, projectContext);
|
|
1616
|
+
if (args.worktree) {
|
|
1617
|
+
return await setupWorktree(ctx, bundleId, branchName, defaultBranch, brief, localClaim.pipelineRunId);
|
|
1618
|
+
}
|
|
1619
|
+
fetchOrigin(ctx.cwd);
|
|
1620
|
+
checkoutNewBranch(branchName, defaultBranch, ctx.cwd);
|
|
1621
|
+
writeBrief(ctx.cwd, brief);
|
|
1622
|
+
reportClaimEvents(ctx, localClaim.pipelineRunId, `Bundle: ${bundleData.title}`, branchName);
|
|
1623
|
+
return {
|
|
1624
|
+
content: [
|
|
1625
|
+
{
|
|
1626
|
+
type: "text",
|
|
1627
|
+
text: JSON.stringify(
|
|
1628
|
+
{
|
|
1629
|
+
kind: "bundle",
|
|
1630
|
+
bundleId,
|
|
1631
|
+
bundleTitle: bundleData.title,
|
|
1632
|
+
findingCount: bundleData.findings.length,
|
|
1633
|
+
branch: branchName,
|
|
1634
|
+
briefPath: ".yapout/brief.md",
|
|
1635
|
+
brief,
|
|
1636
|
+
pipelineRunId: localClaim.pipelineRunId,
|
|
1637
|
+
executionPlan: claim.executionPlan
|
|
1638
|
+
},
|
|
1639
|
+
null,
|
|
1640
|
+
2
|
|
1641
|
+
)
|
|
1642
|
+
}
|
|
1643
|
+
]
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
async function getDefaultBranchForProject(ctx) {
|
|
1647
|
+
try {
|
|
1648
|
+
const data = await ctx.client.query(
|
|
1649
|
+
anyApi2.functions.projects.getProject,
|
|
1650
|
+
{ projectId: ctx.projectId }
|
|
1651
|
+
);
|
|
1652
|
+
return data?.githubDefaultBranch || "main";
|
|
1653
|
+
} catch {
|
|
1654
|
+
return "main";
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
async function setupWorktree(ctx, itemId, branchName, defaultBranch, brief, pipelineRunId) {
|
|
1658
|
+
fetchOrigin(ctx.cwd);
|
|
1659
|
+
const worktreePath = createWorktree(
|
|
1660
|
+
ctx.cwd,
|
|
1661
|
+
itemId,
|
|
1662
|
+
branchName,
|
|
1663
|
+
defaultBranch
|
|
1375
1664
|
);
|
|
1665
|
+
writeBrief(worktreePath, brief);
|
|
1666
|
+
try {
|
|
1667
|
+
await ctx.client.mutation(
|
|
1668
|
+
anyApi2.functions.pipelineRuns.reportDaemonEvent,
|
|
1669
|
+
{
|
|
1670
|
+
pipelineRunId,
|
|
1671
|
+
event: "worktree_created",
|
|
1672
|
+
message: `Worktree: ${worktreePath}`
|
|
1673
|
+
}
|
|
1674
|
+
);
|
|
1675
|
+
} catch {
|
|
1676
|
+
}
|
|
1677
|
+
return {
|
|
1678
|
+
content: [
|
|
1679
|
+
{
|
|
1680
|
+
type: "text",
|
|
1681
|
+
text: JSON.stringify(
|
|
1682
|
+
{
|
|
1683
|
+
branch: branchName,
|
|
1684
|
+
worktreePath,
|
|
1685
|
+
briefPath: `${worktreePath}/.yapout/brief.md`,
|
|
1686
|
+
brief,
|
|
1687
|
+
pipelineRunId
|
|
1688
|
+
},
|
|
1689
|
+
null,
|
|
1690
|
+
2
|
|
1691
|
+
)
|
|
1692
|
+
}
|
|
1693
|
+
]
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
function writeBrief(dir, brief) {
|
|
1697
|
+
const yapoutDir = join7(dir, ".yapout");
|
|
1698
|
+
if (!existsSync6(yapoutDir)) mkdirSync6(yapoutDir, { recursive: true });
|
|
1699
|
+
writeFileSync7(join7(yapoutDir, "brief.md"), brief);
|
|
1700
|
+
}
|
|
1701
|
+
async function reportClaimEvents(ctx, pipelineRunId, title, branchName) {
|
|
1702
|
+
try {
|
|
1703
|
+
await ctx.client.mutation(
|
|
1704
|
+
anyApi2.functions.pipelineRuns.reportDaemonEvent,
|
|
1705
|
+
{
|
|
1706
|
+
pipelineRunId,
|
|
1707
|
+
event: "daemon_claimed",
|
|
1708
|
+
message: `Claimed: ${title}`
|
|
1709
|
+
}
|
|
1710
|
+
);
|
|
1711
|
+
await ctx.client.mutation(
|
|
1712
|
+
anyApi2.functions.pipelineRuns.reportDaemonEvent,
|
|
1713
|
+
{
|
|
1714
|
+
pipelineRunId,
|
|
1715
|
+
event: "branch_created",
|
|
1716
|
+
message: `Branch: ${branchName}`
|
|
1717
|
+
}
|
|
1718
|
+
);
|
|
1719
|
+
} catch {
|
|
1720
|
+
}
|
|
1376
1721
|
}
|
|
1377
1722
|
|
|
1378
1723
|
// src/mcp/tools/event.ts
|
|
@@ -1484,20 +1829,24 @@ async function createPullRequest(title, body, branch, base, repoFullName, cwd) {
|
|
|
1484
1829
|
|
|
1485
1830
|
// src/mcp/tools/ship.ts
|
|
1486
1831
|
import { join as join8 } from "path";
|
|
1487
|
-
import { existsSync as existsSync7, readFileSync as
|
|
1488
|
-
function buildCommitMessage(message, template, finding) {
|
|
1832
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
|
|
1833
|
+
function buildCommitMessage(message, template, finding, allLinearIds) {
|
|
1489
1834
|
if (message) return message;
|
|
1490
1835
|
if (template) {
|
|
1491
1836
|
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");
|
|
1492
1837
|
}
|
|
1493
1838
|
const prefix = finding.type === "bug" ? "fix" : "feat";
|
|
1839
|
+
if (allLinearIds && allLinearIds.length > 1) {
|
|
1840
|
+
const refs = allLinearIds.join(", ");
|
|
1841
|
+
return `${prefix}: ${finding.title} (${refs})`;
|
|
1842
|
+
}
|
|
1494
1843
|
const ref = finding.linearIssueId ? ` (${finding.linearIssueId})` : "";
|
|
1495
1844
|
return `${prefix}(${finding.type}): ${finding.title}${ref}`;
|
|
1496
1845
|
}
|
|
1497
1846
|
function registerShipTool(server, ctx) {
|
|
1498
1847
|
server.tool(
|
|
1499
1848
|
"yapout_ship",
|
|
1500
|
-
"Commit, push, open a PR, and mark the
|
|
1849
|
+
"Commit, push, open a PR, and mark the work item as done. Run yapout_check first if post-flight checks are configured.",
|
|
1501
1850
|
{
|
|
1502
1851
|
message: z7.string().optional().describe("Custom commit message (overrides template)"),
|
|
1503
1852
|
skipPr: z7.boolean().optional().describe("Just push, don't open a PR"),
|
|
@@ -1527,24 +1876,53 @@ function registerShipTool(server, ctx) {
|
|
|
1527
1876
|
let findingTitle = branch;
|
|
1528
1877
|
let findingType = "feature";
|
|
1529
1878
|
let findingLinearId;
|
|
1879
|
+
let allLinearIds = [];
|
|
1880
|
+
let isBundle = false;
|
|
1881
|
+
let bundleTitle;
|
|
1530
1882
|
try {
|
|
1531
1883
|
const briefPath = join8(gitCwd, ".yapout", "brief.md");
|
|
1532
1884
|
if (existsSync7(briefPath)) {
|
|
1533
|
-
const brief =
|
|
1534
|
-
const
|
|
1535
|
-
if (
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1885
|
+
const brief = readFileSync5(briefPath, "utf-8");
|
|
1886
|
+
const bundleMatch = brief.match(/^# Bundle: (.+)$/m);
|
|
1887
|
+
if (bundleMatch) {
|
|
1888
|
+
isBundle = true;
|
|
1889
|
+
bundleTitle = bundleMatch[1];
|
|
1890
|
+
findingTitle = bundleTitle;
|
|
1891
|
+
const linearMatches = brief.matchAll(/\(([A-Z]+-\d+)\)/g);
|
|
1892
|
+
for (const match of linearMatches) {
|
|
1893
|
+
if (!allLinearIds.includes(match[1])) {
|
|
1894
|
+
allLinearIds.push(match[1]);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
if (allLinearIds.length > 0) {
|
|
1898
|
+
findingLinearId = allLinearIds[0];
|
|
1899
|
+
}
|
|
1900
|
+
const typeMatch = brief.match(/\*\*Type:\*\* (\w+)/);
|
|
1901
|
+
if (typeMatch) findingType = typeMatch[1];
|
|
1902
|
+
} else {
|
|
1903
|
+
const titleMatch = brief.match(/^# (.+)$/m);
|
|
1904
|
+
if (titleMatch) findingTitle = titleMatch[1];
|
|
1905
|
+
const typeMatch = brief.match(/\*\*Type:\*\* (\w+)/);
|
|
1906
|
+
if (typeMatch) findingType = typeMatch[1];
|
|
1907
|
+
const linearMatch = brief.match(/\*\*Linear:\*\* .+\/([A-Z]+-\d+)\//);
|
|
1908
|
+
if (linearMatch) {
|
|
1909
|
+
findingLinearId = linearMatch[1];
|
|
1910
|
+
allLinearIds = [findingLinearId];
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1540
1913
|
}
|
|
1541
1914
|
} catch {
|
|
1542
1915
|
}
|
|
1543
|
-
const commitMsg = buildCommitMessage(
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1916
|
+
const commitMsg = buildCommitMessage(
|
|
1917
|
+
args.message,
|
|
1918
|
+
config.commit_template,
|
|
1919
|
+
{
|
|
1920
|
+
title: findingTitle,
|
|
1921
|
+
type: findingType,
|
|
1922
|
+
linearIssueId: findingLinearId
|
|
1923
|
+
},
|
|
1924
|
+
allLinearIds.length > 1 ? allLinearIds : void 0
|
|
1925
|
+
);
|
|
1548
1926
|
stageAll(gitCwd);
|
|
1549
1927
|
const sha = commit(commitMsg, gitCwd);
|
|
1550
1928
|
push(branch, gitCwd);
|
|
@@ -1553,6 +1931,11 @@ function registerShipTool(server, ctx) {
|
|
|
1553
1931
|
branch,
|
|
1554
1932
|
pushed: true
|
|
1555
1933
|
};
|
|
1934
|
+
if (isBundle) {
|
|
1935
|
+
result.isBundle = true;
|
|
1936
|
+
result.bundleTitle = bundleTitle;
|
|
1937
|
+
result.linearIssueIds = allLinearIds;
|
|
1938
|
+
}
|
|
1556
1939
|
if (!config.ship_requires_checks && config.post_flight.length > 0) {
|
|
1557
1940
|
if (ctx.lastCheckPassedForRun !== args.pipelineRunId) {
|
|
1558
1941
|
result.warning = "Shipped without running post-flight checks.";
|
|
@@ -1564,11 +1947,25 @@ function registerShipTool(server, ctx) {
|
|
|
1564
1947
|
try {
|
|
1565
1948
|
const repoFullName = getRepoFullName(gitCwd);
|
|
1566
1949
|
const diffStats = getDiffStats(defaultBranch, branch, gitCwd);
|
|
1567
|
-
const
|
|
1950
|
+
const prBodyParts = [
|
|
1568
1951
|
`## Summary`,
|
|
1569
|
-
""
|
|
1570
|
-
|
|
1571
|
-
|
|
1952
|
+
""
|
|
1953
|
+
];
|
|
1954
|
+
if (isBundle && bundleTitle) {
|
|
1955
|
+
prBodyParts.push(`**Bundle:** ${bundleTitle}`);
|
|
1956
|
+
prBodyParts.push("");
|
|
1957
|
+
if (allLinearIds.length > 0) {
|
|
1958
|
+
prBodyParts.push("**Linear Issues:**");
|
|
1959
|
+
for (const id of allLinearIds) {
|
|
1960
|
+
prBodyParts.push(`- ${id}`);
|
|
1961
|
+
}
|
|
1962
|
+
prBodyParts.push("");
|
|
1963
|
+
}
|
|
1964
|
+
} else {
|
|
1965
|
+
prBodyParts.push(findingTitle);
|
|
1966
|
+
prBodyParts.push("");
|
|
1967
|
+
}
|
|
1968
|
+
prBodyParts.push(
|
|
1572
1969
|
`## Changes`,
|
|
1573
1970
|
"",
|
|
1574
1971
|
"```",
|
|
@@ -1577,7 +1974,8 @@ function registerShipTool(server, ctx) {
|
|
|
1577
1974
|
"",
|
|
1578
1975
|
"---",
|
|
1579
1976
|
`Implemented via [yapout](https://yapout.dev) daemon`
|
|
1580
|
-
|
|
1977
|
+
);
|
|
1978
|
+
const prBody = prBodyParts.join("\n");
|
|
1581
1979
|
const pr = await createPullRequest(
|
|
1582
1980
|
findingTitle,
|
|
1583
1981
|
prBody,
|
|
@@ -1627,30 +2025,33 @@ function registerShipTool(server, ctx) {
|
|
|
1627
2025
|
} catch (err) {
|
|
1628
2026
|
result.completionError = err.message;
|
|
1629
2027
|
}
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
anyApi2.functions.linearStatusMutations.moveIssueStatus,
|
|
1634
|
-
{
|
|
1635
|
-
projectId: ctx.projectId,
|
|
1636
|
-
linearIssueId: findingLinearId,
|
|
1637
|
-
statusType: "completed"
|
|
1638
|
-
}
|
|
1639
|
-
);
|
|
1640
|
-
} catch {
|
|
1641
|
-
}
|
|
1642
|
-
if (prUrl) {
|
|
2028
|
+
const linearIdsToUpdate = allLinearIds.length > 0 ? allLinearIds : findingLinearId ? [findingLinearId] : [];
|
|
2029
|
+
for (const linearId of linearIdsToUpdate) {
|
|
2030
|
+
if (ctx.projectId) {
|
|
1643
2031
|
try {
|
|
1644
2032
|
await ctx.client.action(
|
|
1645
|
-
anyApi2.functions.linearStatusMutations.
|
|
2033
|
+
anyApi2.functions.linearStatusMutations.moveIssueStatus,
|
|
1646
2034
|
{
|
|
1647
2035
|
projectId: ctx.projectId,
|
|
1648
|
-
linearIssueId:
|
|
1649
|
-
|
|
2036
|
+
linearIssueId: linearId,
|
|
2037
|
+
statusType: "completed"
|
|
1650
2038
|
}
|
|
1651
2039
|
);
|
|
1652
2040
|
} catch {
|
|
1653
2041
|
}
|
|
2042
|
+
if (prUrl) {
|
|
2043
|
+
try {
|
|
2044
|
+
await ctx.client.action(
|
|
2045
|
+
anyApi2.functions.linearStatusMutations.addLinearComment,
|
|
2046
|
+
{
|
|
2047
|
+
projectId: ctx.projectId,
|
|
2048
|
+
linearIssueId: linearId,
|
|
2049
|
+
body: `PR opened: [#${prNumber}](${prUrl})`
|
|
2050
|
+
}
|
|
2051
|
+
);
|
|
2052
|
+
} catch {
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
1654
2055
|
}
|
|
1655
2056
|
}
|
|
1656
2057
|
if (args.worktreePath) {
|
|
@@ -2055,7 +2456,7 @@ This tool saves the enrichment, then automatically creates the Linear issue with
|
|
|
2055
2456
|
- Clarification Q&A as a branded comment (if any)
|
|
2056
2457
|
- Implementation brief as attachment metadata
|
|
2057
2458
|
|
|
2058
|
-
The finding transitions: enriching \u2192 enriched \u2192
|
|
2459
|
+
The finding transitions: enriching \u2192 enriched \u2192 ready.`,
|
|
2059
2460
|
{
|
|
2060
2461
|
findingId: z11.string().describe("The finding ID to enrich (from yapout_get_unenriched_finding)"),
|
|
2061
2462
|
title: z11.string().describe("Refined finding title \u2014 improve it if the original was vague"),
|
|
@@ -2115,7 +2516,7 @@ The finding transitions: enriching \u2192 enriched \u2192 synced.`,
|
|
|
2115
2516
|
linearIssueId: finding?.linearIssueId ?? null,
|
|
2116
2517
|
linearIssueUrl: finding?.linearIssueUrl ?? null,
|
|
2117
2518
|
compactionHint,
|
|
2118
|
-
message: finding?.linearIssueUrl ? `Finding enriched and
|
|
2519
|
+
message: finding?.linearIssueUrl ? `Finding enriched and ready in Linear: ${finding.linearIssueUrl}` : "Finding enriched and ready in Linear."
|
|
2119
2520
|
};
|
|
2120
2521
|
if (args.isOversized && args.suggestedSplit?.length) {
|
|
2121
2522
|
response.warning = `This finding is oversized. Suggested split: ${args.suggestedSplit.join(", ")}`;
|
|
@@ -2996,6 +3397,121 @@ When done=true, all findings have been processed.`,
|
|
|
2996
3397
|
);
|
|
2997
3398
|
}
|
|
2998
3399
|
|
|
3400
|
+
// src/mcp/tools/enrich-bundle.ts
|
|
3401
|
+
import { z as z20 } from "zod";
|
|
3402
|
+
function registerEnrichBundleTool(server, ctx) {
|
|
3403
|
+
server.tool(
|
|
3404
|
+
"yapout_enrich_bundle",
|
|
3405
|
+
`Claim an entire bundle for enrichment. Returns ALL findings in the bundle at once
|
|
3406
|
+
with their relationships, source quotes, and project context.
|
|
3407
|
+
|
|
3408
|
+
This is for enriching a bundle as a single cohesive unit \u2014 NOT individual findings.
|
|
3409
|
+
The agent should understand the full scope, ask questions about the bundle as a whole,
|
|
3410
|
+
then call yapout_save_bundle_enrichment with enrichment data for every finding.
|
|
3411
|
+
|
|
3412
|
+
The bundle and all its findings transition to "enriching" status.`,
|
|
3413
|
+
{
|
|
3414
|
+
bundleId: z20.string().describe("The bundle ID to enrich")
|
|
3415
|
+
},
|
|
3416
|
+
async (args) => {
|
|
3417
|
+
try {
|
|
3418
|
+
const result = await ctx.client.mutation(
|
|
3419
|
+
anyApi2.functions.bundles.claimBundleForEnrichment,
|
|
3420
|
+
{ bundleId: args.bundleId }
|
|
3421
|
+
);
|
|
3422
|
+
if (!result) {
|
|
3423
|
+
return {
|
|
3424
|
+
content: [{ type: "text", text: "Failed to claim bundle for enrichment." }],
|
|
3425
|
+
isError: true
|
|
3426
|
+
};
|
|
3427
|
+
}
|
|
3428
|
+
return {
|
|
3429
|
+
content: [
|
|
3430
|
+
{
|
|
3431
|
+
type: "text",
|
|
3432
|
+
text: JSON.stringify(result, null, 2)
|
|
3433
|
+
}
|
|
3434
|
+
]
|
|
3435
|
+
};
|
|
3436
|
+
} catch (err) {
|
|
3437
|
+
return {
|
|
3438
|
+
content: [{ type: "text", text: `Error claiming bundle: ${err.message}` }],
|
|
3439
|
+
isError: true
|
|
3440
|
+
};
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
);
|
|
3444
|
+
}
|
|
3445
|
+
|
|
3446
|
+
// src/mcp/tools/save-bundle-enrichment.ts
|
|
3447
|
+
import { z as z21 } from "zod";
|
|
3448
|
+
function registerSaveBundleEnrichmentTool(server, ctx) {
|
|
3449
|
+
server.tool(
|
|
3450
|
+
"yapout_save_bundle_enrichment",
|
|
3451
|
+
`Save enrichment for an entire bundle at once. Call this after you've analyzed all
|
|
3452
|
+
findings in the bundle as a cohesive unit, read the codebase, and asked any questions.
|
|
3453
|
+
|
|
3454
|
+
Provide:
|
|
3455
|
+
- Bundle-level: overall description, combined acceptance criteria, implementation brief
|
|
3456
|
+
- Per-finding: each finding gets its own refined title, description, acceptance criteria, and brief
|
|
3457
|
+
|
|
3458
|
+
The bundle and all findings transition from "enriching" to "enriched".
|
|
3459
|
+
Call yapout_sync_bundle_to_linear afterwards to create the Linear project.`,
|
|
3460
|
+
{
|
|
3461
|
+
bundleId: z21.string().describe("The bundle ID"),
|
|
3462
|
+
title: z21.string().optional().describe("Refined bundle title (optional, keeps existing if omitted)"),
|
|
3463
|
+
enrichedDescription: z21.string().describe("Bundle-level description \u2014 the cohesive story of what this bundle delivers"),
|
|
3464
|
+
acceptanceCriteria: z21.array(z21.string()).describe("Bundle-level acceptance criteria"),
|
|
3465
|
+
implementationBrief: z21.string().describe("Bundle-level implementation brief \u2014 overall approach, architecture decisions, key files"),
|
|
3466
|
+
findings: z21.array(z21.object({
|
|
3467
|
+
findingId: z21.string().describe("Finding ID"),
|
|
3468
|
+
title: z21.string().describe("Refined finding title"),
|
|
3469
|
+
enrichedDescription: z21.string().describe("Finding-specific description"),
|
|
3470
|
+
acceptanceCriteria: z21.array(z21.string()).describe("Finding-specific acceptance criteria"),
|
|
3471
|
+
implementationBrief: z21.string().describe("Finding-specific implementation brief")
|
|
3472
|
+
})).describe("Per-finding enrichment data \u2014 one entry per finding in the bundle")
|
|
3473
|
+
},
|
|
3474
|
+
async (args) => {
|
|
3475
|
+
try {
|
|
3476
|
+
await ctx.client.mutation(
|
|
3477
|
+
anyApi2.functions.bundles.saveBundleEnrichment,
|
|
3478
|
+
{
|
|
3479
|
+
bundleId: args.bundleId,
|
|
3480
|
+
title: args.title,
|
|
3481
|
+
enrichedDescription: args.enrichedDescription,
|
|
3482
|
+
acceptanceCriteria: args.acceptanceCriteria,
|
|
3483
|
+
implementationBrief: args.implementationBrief,
|
|
3484
|
+
findings: args.findings.map((f) => ({
|
|
3485
|
+
findingId: f.findingId,
|
|
3486
|
+
title: f.title,
|
|
3487
|
+
enrichedDescription: f.enrichedDescription,
|
|
3488
|
+
acceptanceCriteria: f.acceptanceCriteria,
|
|
3489
|
+
implementationBrief: f.implementationBrief
|
|
3490
|
+
}))
|
|
3491
|
+
}
|
|
3492
|
+
);
|
|
3493
|
+
return {
|
|
3494
|
+
content: [
|
|
3495
|
+
{
|
|
3496
|
+
type: "text",
|
|
3497
|
+
text: JSON.stringify({
|
|
3498
|
+
bundleId: args.bundleId,
|
|
3499
|
+
findingsEnriched: args.findings.length,
|
|
3500
|
+
message: `Bundle enriched successfully (${args.findings.length} findings). Call yapout_sync_bundle_to_linear to create the Linear project.`
|
|
3501
|
+
}, null, 2)
|
|
3502
|
+
}
|
|
3503
|
+
]
|
|
3504
|
+
};
|
|
3505
|
+
} catch (err) {
|
|
3506
|
+
return {
|
|
3507
|
+
content: [{ type: "text", text: `Error saving bundle enrichment: ${err.message}` }],
|
|
3508
|
+
isError: true
|
|
3509
|
+
};
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
);
|
|
3513
|
+
}
|
|
3514
|
+
|
|
2999
3515
|
// src/mcp/server.ts
|
|
3000
3516
|
async function startMcpServer() {
|
|
3001
3517
|
const cwd = process.cwd();
|
|
@@ -3050,6 +3566,8 @@ async function startMcpServer() {
|
|
|
3050
3566
|
registerGetLinearProjectsTool(server, ctx);
|
|
3051
3567
|
registerStartEnrichmentTool(server, ctx);
|
|
3052
3568
|
registerEnrichNextTool(server, ctx);
|
|
3569
|
+
registerEnrichBundleTool(server, ctx);
|
|
3570
|
+
registerSaveBundleEnrichmentTool(server, ctx);
|
|
3053
3571
|
const transport = new StdioServerTransport();
|
|
3054
3572
|
await server.connect(transport);
|
|
3055
3573
|
}
|
|
@@ -3127,7 +3645,7 @@ var cleanCommand = new Command9("clean").description("Remove worktrees for compl
|
|
|
3127
3645
|
import { Command as Command10 } from "commander";
|
|
3128
3646
|
import { resolve as resolve7 } from "path";
|
|
3129
3647
|
import {
|
|
3130
|
-
readFileSync as
|
|
3648
|
+
readFileSync as readFileSync6,
|
|
3131
3649
|
writeFileSync as writeFileSync9,
|
|
3132
3650
|
existsSync as existsSync10,
|
|
3133
3651
|
unlinkSync as unlinkSync3
|
|
@@ -3743,7 +4261,7 @@ var LOG_FILE = join11(getYapoutDir(), "watch.log");
|
|
|
3743
4261
|
var watchCommand = new Command10("watch").description("Watch for work and spawn Claude Code agents").option("--bg", "Run in background (detached)").option("--stop", "Stop the background watcher").option("--status", "Check if watcher is running").option("--force", "Force kill on Ctrl+C (don't wait for agents)").action(async (opts) => {
|
|
3744
4262
|
if (opts.status) {
|
|
3745
4263
|
if (existsSync10(PID_FILE)) {
|
|
3746
|
-
const pid = parseInt(
|
|
4264
|
+
const pid = parseInt(readFileSync6(PID_FILE, "utf-8").trim(), 10);
|
|
3747
4265
|
if (isProcessRunning(pid)) {
|
|
3748
4266
|
console.log(
|
|
3749
4267
|
chalk11.green("Watcher is running") + chalk11.dim(` (PID ${pid})`)
|
|
@@ -3762,7 +4280,7 @@ var watchCommand = new Command10("watch").description("Watch for work and spawn
|
|
|
3762
4280
|
console.log(chalk11.dim("No watcher running"));
|
|
3763
4281
|
return;
|
|
3764
4282
|
}
|
|
3765
|
-
const pid = parseInt(
|
|
4283
|
+
const pid = parseInt(readFileSync6(PID_FILE, "utf-8").trim(), 10);
|
|
3766
4284
|
try {
|
|
3767
4285
|
process.kill(pid, "SIGTERM");
|
|
3768
4286
|
console.log(chalk11.green(`Stopped watcher (PID ${pid})`));
|
|
@@ -3989,7 +4507,7 @@ Claimed: ${ref} "${ticket.title}"`));
|
|
|
3989
4507
|
);
|
|
3990
4508
|
if (brief) {
|
|
3991
4509
|
const briefPath = join12(workDir, ".yapout", "brief.md");
|
|
3992
|
-
const briefContent =
|
|
4510
|
+
const briefContent = formatBrief(ref, ticket, brief);
|
|
3993
4511
|
writeFileSync10(briefPath, briefContent);
|
|
3994
4512
|
console.log(`Brief: ${chalk13.cyan(briefPath)}`);
|
|
3995
4513
|
}
|
|
@@ -4002,7 +4520,7 @@ Claimed: ${ref} "${ticket.title}"`));
|
|
|
4002
4520
|
);
|
|
4003
4521
|
console.log();
|
|
4004
4522
|
});
|
|
4005
|
-
function
|
|
4523
|
+
function formatBrief(ref, ticket, brief) {
|
|
4006
4524
|
const lines = [
|
|
4007
4525
|
`# ${ref}: ${ticket.title}`,
|
|
4008
4526
|
"",
|
|
@@ -4141,7 +4659,7 @@ import { Command as Command15 } from "commander";
|
|
|
4141
4659
|
import { spawn as spawn2 } from "child_process";
|
|
4142
4660
|
import { platform as platform2 } from "os";
|
|
4143
4661
|
import chalk16 from "chalk";
|
|
4144
|
-
var VALID_ACTIONS = ["claim", "enrich", "enrich-bulk", "yap", "compact"];
|
|
4662
|
+
var VALID_ACTIONS = ["claim", "enrich", "enrich-bulk", "enrich-bundle", "yap", "compact"];
|
|
4145
4663
|
function parseYapoutUri(raw) {
|
|
4146
4664
|
const url = new URL(raw);
|
|
4147
4665
|
const action = url.hostname;
|
|
@@ -4151,8 +4669,8 @@ function parseYapoutUri(raw) {
|
|
|
4151
4669
|
);
|
|
4152
4670
|
}
|
|
4153
4671
|
const ticketId = url.pathname.replace(/^\//, "") || void 0;
|
|
4154
|
-
if ((action === "claim" || action === "enrich") && !ticketId) {
|
|
4155
|
-
throw new Error(`Missing
|
|
4672
|
+
if ((action === "claim" || action === "enrich" || action === "enrich-bundle") && !ticketId) {
|
|
4673
|
+
throw new Error(`Missing ID in URI: ${raw}`);
|
|
4156
4674
|
}
|
|
4157
4675
|
return {
|
|
4158
4676
|
action,
|
|
@@ -4167,9 +4685,10 @@ function buildPrompt(parsed) {
|
|
|
4167
4685
|
switch (parsed.action) {
|
|
4168
4686
|
case "claim":
|
|
4169
4687
|
return [
|
|
4170
|
-
`Use yapout to implement
|
|
4171
|
-
`Claim it with yapout_claim (use worktree mode), read the brief,`,
|
|
4172
|
-
`implement the changes, run yapout_check, then ship with yapout_ship
|
|
4688
|
+
`Use yapout to implement work item "${parsed.ticketId}".`,
|
|
4689
|
+
`Claim it with yapout_claim using workItemId (use worktree mode), read the brief,`,
|
|
4690
|
+
`implement the changes, run yapout_check, then ship with yapout_ship.`,
|
|
4691
|
+
`The ID may be a bundle or standalone finding \u2014 yapout_claim auto-detects.`
|
|
4173
4692
|
].join(" ");
|
|
4174
4693
|
case "enrich":
|
|
4175
4694
|
return [
|
|
@@ -4209,6 +4728,27 @@ function buildPrompt(parsed) {
|
|
|
4209
4728
|
}
|
|
4210
4729
|
return lines.join("\n");
|
|
4211
4730
|
}
|
|
4731
|
+
case "enrich-bundle":
|
|
4732
|
+
return [
|
|
4733
|
+
`Use yapout to enrich a bundle as a single cohesive unit.`,
|
|
4734
|
+
``,
|
|
4735
|
+
`1. Call yapout_enrich_bundle with bundleId "${parsed.ticketId}" to claim it and get all findings`,
|
|
4736
|
+
`2. Read the codebase to understand the full scope of the bundle`,
|
|
4737
|
+
`3. Ask me any clarifying questions about the bundle as a whole (not individual findings)`,
|
|
4738
|
+
`4. For each finding, formulate:`,
|
|
4739
|
+
` - A refined title`,
|
|
4740
|
+
` - A specific description`,
|
|
4741
|
+
` - Concrete acceptance criteria`,
|
|
4742
|
+
` - An implementation brief`,
|
|
4743
|
+
`5. Also formulate bundle-level:`,
|
|
4744
|
+
` - An overall description of what the bundle delivers`,
|
|
4745
|
+
` - Combined acceptance criteria`,
|
|
4746
|
+
` - An architecture/approach brief that covers the full scope`,
|
|
4747
|
+
`6. Call yapout_save_bundle_enrichment with ALL enrichment data at once`,
|
|
4748
|
+
``,
|
|
4749
|
+
`Treat this as ONE problem, not separate findings. Understand how they relate,`,
|
|
4750
|
+
`identify dependencies, and write briefs that reference each other.`
|
|
4751
|
+
].join("\n");
|
|
4212
4752
|
case "yap": {
|
|
4213
4753
|
const parts = ["Let's have a yap session"];
|
|
4214
4754
|
if (parsed.topic) parts[0] += ` about ${parsed.topic}`;
|