yapout 0.4.0 → 0.4.2
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 +621 -242
- 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 = (
|
|
@@ -1086,12 +1087,42 @@ function registerUpdateContextTool(server, ctx) {
|
|
|
1086
1087
|
|
|
1087
1088
|
// src/mcp/tools/queue.ts
|
|
1088
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
|
+
}
|
|
1089
1120
|
function registerQueueTool(server, ctx) {
|
|
1090
1121
|
server.tool(
|
|
1091
1122
|
"yapout_queue",
|
|
1092
|
-
"List findings ready for local implementation
|
|
1123
|
+
"List work items (bundles and standalone findings) ready for local implementation, plus active and done items.",
|
|
1093
1124
|
{
|
|
1094
|
-
|
|
1125
|
+
includeIds: z3.boolean().optional().describe("Include work item IDs in output (default: false)")
|
|
1095
1126
|
},
|
|
1096
1127
|
async (args) => {
|
|
1097
1128
|
if (!ctx.projectId) {
|
|
@@ -1106,55 +1137,84 @@ function registerQueueTool(server, ctx) {
|
|
|
1106
1137
|
};
|
|
1107
1138
|
}
|
|
1108
1139
|
const data = await ctx.client.query(
|
|
1109
|
-
anyApi2.functions.
|
|
1140
|
+
anyApi2.functions.workQueue.getWorkQueue,
|
|
1110
1141
|
{ projectId: ctx.projectId }
|
|
1111
1142
|
);
|
|
1112
1143
|
if (!data) {
|
|
1113
1144
|
return {
|
|
1114
1145
|
content: [
|
|
1115
|
-
{ type: "text", text: "Could not fetch queue." }
|
|
1146
|
+
{ type: "text", text: "Could not fetch work queue." }
|
|
1116
1147
|
],
|
|
1117
1148
|
isError: true
|
|
1118
1149
|
};
|
|
1119
1150
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
ready = ready.filter((t) => {
|
|
1133
|
-
if (!t.linearIssueId) return true;
|
|
1134
|
-
const type = statusMap.get(t.linearIssueId);
|
|
1135
|
-
if (!type) return true;
|
|
1136
|
-
return type === "backlog" || type === "unstarted";
|
|
1137
|
-
});
|
|
1138
|
-
} 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
|
+
}
|
|
1139
1163
|
}
|
|
1140
1164
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
|
|
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
|
+
}
|
|
1148
1179
|
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
)
|
|
1153
|
-
|
|
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
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
if (data.agentStatus.isActive) {
|
|
1195
|
+
sections.push("");
|
|
1196
|
+
sections.push(`Agent: ${data.agentStatus.agentCount} active, ${data.agentStatus.worktreeCount} worktrees`);
|
|
1154
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
|
+
};
|
|
1155
1214
|
return {
|
|
1156
1215
|
content: [
|
|
1157
|
-
{ type: "text", text:
|
|
1216
|
+
{ type: "text", text: sections.join("\n") },
|
|
1217
|
+
{ type: "text", text: "\n---\nStructured data:\n" + JSON.stringify(structured, null, 2) }
|
|
1158
1218
|
]
|
|
1159
1219
|
};
|
|
1160
1220
|
}
|
|
@@ -1166,30 +1226,84 @@ import { z as z4 } from "zod";
|
|
|
1166
1226
|
function registerGetBriefTool(server, ctx) {
|
|
1167
1227
|
server.tool(
|
|
1168
1228
|
"yapout_get_brief",
|
|
1169
|
-
"Fetch the full implementation context for a finding",
|
|
1229
|
+
"Fetch the full implementation context for a work item (finding or bundle)",
|
|
1170
1230
|
{
|
|
1171
|
-
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")
|
|
1172
1233
|
},
|
|
1173
1234
|
async (args) => {
|
|
1174
|
-
const
|
|
1175
|
-
|
|
1176
|
-
{ findingId: args.findingId }
|
|
1177
|
-
);
|
|
1178
|
-
if (!data) {
|
|
1235
|
+
const itemId = args.workItemId || args.findingId;
|
|
1236
|
+
if (!itemId) {
|
|
1179
1237
|
return {
|
|
1180
1238
|
content: [
|
|
1181
1239
|
{
|
|
1182
1240
|
type: "text",
|
|
1183
|
-
text: "
|
|
1241
|
+
text: "Must provide workItemId or findingId."
|
|
1184
1242
|
}
|
|
1185
1243
|
],
|
|
1186
1244
|
isError: true
|
|
1187
1245
|
};
|
|
1188
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
|
+
}
|
|
1189
1299
|
return {
|
|
1190
1300
|
content: [
|
|
1191
|
-
{
|
|
1192
|
-
|
|
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
|
|
1193
1307
|
};
|
|
1194
1308
|
}
|
|
1195
1309
|
);
|
|
@@ -1210,7 +1324,7 @@ function readBranchPrefix(cwd) {
|
|
|
1210
1324
|
function slugify(text) {
|
|
1211
1325
|
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
1212
1326
|
}
|
|
1213
|
-
function
|
|
1327
|
+
function formatFindingBrief(data) {
|
|
1214
1328
|
const finding = data.finding;
|
|
1215
1329
|
const sections = [
|
|
1216
1330
|
`# ${finding.title}`,
|
|
@@ -1251,12 +1365,89 @@ function formatBrief(data) {
|
|
|
1251
1365
|
}
|
|
1252
1366
|
return sections.join("\n");
|
|
1253
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
|
+
}
|
|
1254
1444
|
function registerClaimTool(server, ctx) {
|
|
1255
1445
|
server.tool(
|
|
1256
1446
|
"yapout_claim",
|
|
1257
|
-
"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.",
|
|
1258
1448
|
{
|
|
1259
|
-
|
|
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."),
|
|
1260
1451
|
worktree: z5.boolean().optional().describe("Create a git worktree for parallel work (default: false)")
|
|
1261
1452
|
},
|
|
1262
1453
|
async (args) => {
|
|
@@ -1271,131 +1462,262 @@ function registerClaimTool(server, ctx) {
|
|
|
1271
1462
|
isError: true
|
|
1272
1463
|
};
|
|
1273
1464
|
}
|
|
1274
|
-
const
|
|
1275
|
-
|
|
1276
|
-
{ findingId: args.findingId }
|
|
1277
|
-
);
|
|
1278
|
-
if (!briefData) {
|
|
1465
|
+
const itemId = args.workItemId || args.findingId;
|
|
1466
|
+
if (!itemId) {
|
|
1279
1467
|
return {
|
|
1280
1468
|
content: [
|
|
1281
1469
|
{
|
|
1282
1470
|
type: "text",
|
|
1283
|
-
text: "
|
|
1471
|
+
text: "Must provide workItemId (or deprecated findingId)."
|
|
1284
1472
|
}
|
|
1285
1473
|
],
|
|
1286
1474
|
isError: true
|
|
1287
1475
|
};
|
|
1288
1476
|
}
|
|
1289
|
-
const
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
const branchName = linearIssueId ? `${prefix}/${linearIssueId.toLowerCase()}-${slug}` : `${prefix}/${slug}`;
|
|
1295
|
-
const claim = await ctx.client.mutation(
|
|
1296
|
-
anyApi2.functions.findings.claimFindingLocal,
|
|
1297
|
-
{ findingId: args.findingId, branchName }
|
|
1298
|
-
);
|
|
1299
|
-
if (linearIssueId && ctx.projectId) {
|
|
1300
|
-
try {
|
|
1301
|
-
await ctx.client.action(
|
|
1302
|
-
anyApi2.functions.linearStatusMutations.moveIssueStatus,
|
|
1303
|
-
{
|
|
1304
|
-
projectId: ctx.projectId,
|
|
1305
|
-
linearIssueId,
|
|
1306
|
-
statusType: "started"
|
|
1307
|
-
}
|
|
1308
|
-
);
|
|
1309
|
-
} catch {
|
|
1310
|
-
}
|
|
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);
|
|
1311
1482
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
await ctx.client.mutation(
|
|
1327
|
-
anyApi2.functions.pipelineRuns.reportDaemonEvent,
|
|
1328
|
-
{
|
|
1329
|
-
pipelineRunId: claim.pipelineRunId,
|
|
1330
|
-
event: "worktree_created",
|
|
1331
|
-
message: `Worktree: ${worktreePath}`
|
|
1332
|
-
}
|
|
1333
|
-
);
|
|
1334
|
-
} 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."
|
|
1335
1497
|
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
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
|
+
)
|
|
1354
1557
|
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
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) {
|
|
1361
1593
|
try {
|
|
1362
|
-
await ctx.client.
|
|
1363
|
-
anyApi2.functions.
|
|
1364
|
-
{
|
|
1365
|
-
pipelineRunId: claim.pipelineRunId,
|
|
1366
|
-
event: "daemon_claimed",
|
|
1367
|
-
message: `Claimed finding: ${finding.title}`
|
|
1368
|
-
}
|
|
1369
|
-
);
|
|
1370
|
-
await ctx.client.mutation(
|
|
1371
|
-
anyApi2.functions.pipelineRuns.reportDaemonEvent,
|
|
1594
|
+
await ctx.client.action(
|
|
1595
|
+
anyApi2.functions.linearStatusMutations.moveIssueStatus,
|
|
1372
1596
|
{
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1597
|
+
projectId: ctx.projectId,
|
|
1598
|
+
linearIssueId: f.linearIssueId,
|
|
1599
|
+
statusType: "started"
|
|
1376
1600
|
}
|
|
1377
1601
|
);
|
|
1378
1602
|
} catch {
|
|
1379
1603
|
}
|
|
1380
|
-
return {
|
|
1381
|
-
content: [
|
|
1382
|
-
{
|
|
1383
|
-
type: "text",
|
|
1384
|
-
text: JSON.stringify(
|
|
1385
|
-
{
|
|
1386
|
-
branch: branchName,
|
|
1387
|
-
briefPath: ".yapout/brief.md",
|
|
1388
|
-
brief,
|
|
1389
|
-
pipelineRunId: claim.pipelineRunId
|
|
1390
|
-
},
|
|
1391
|
-
null,
|
|
1392
|
-
2
|
|
1393
|
-
)
|
|
1394
|
-
}
|
|
1395
|
-
]
|
|
1396
|
-
};
|
|
1397
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
|
|
1398
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
|
+
}
|
|
1399
1721
|
}
|
|
1400
1722
|
|
|
1401
1723
|
// src/mcp/tools/event.ts
|
|
@@ -1507,20 +1829,24 @@ async function createPullRequest(title, body, branch, base, repoFullName, cwd) {
|
|
|
1507
1829
|
|
|
1508
1830
|
// src/mcp/tools/ship.ts
|
|
1509
1831
|
import { join as join8 } from "path";
|
|
1510
|
-
import { existsSync as existsSync7, readFileSync as
|
|
1511
|
-
function buildCommitMessage(message, template, finding) {
|
|
1832
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
|
|
1833
|
+
function buildCommitMessage(message, template, finding, allLinearIds) {
|
|
1512
1834
|
if (message) return message;
|
|
1513
1835
|
if (template) {
|
|
1514
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");
|
|
1515
1837
|
}
|
|
1516
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
|
+
}
|
|
1517
1843
|
const ref = finding.linearIssueId ? ` (${finding.linearIssueId})` : "";
|
|
1518
1844
|
return `${prefix}(${finding.type}): ${finding.title}${ref}`;
|
|
1519
1845
|
}
|
|
1520
1846
|
function registerShipTool(server, ctx) {
|
|
1521
1847
|
server.tool(
|
|
1522
1848
|
"yapout_ship",
|
|
1523
|
-
"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.",
|
|
1524
1850
|
{
|
|
1525
1851
|
message: z7.string().optional().describe("Custom commit message (overrides template)"),
|
|
1526
1852
|
skipPr: z7.boolean().optional().describe("Just push, don't open a PR"),
|
|
@@ -1550,24 +1876,53 @@ function registerShipTool(server, ctx) {
|
|
|
1550
1876
|
let findingTitle = branch;
|
|
1551
1877
|
let findingType = "feature";
|
|
1552
1878
|
let findingLinearId;
|
|
1879
|
+
let allLinearIds = [];
|
|
1880
|
+
let isBundle = false;
|
|
1881
|
+
let bundleTitle;
|
|
1553
1882
|
try {
|
|
1554
1883
|
const briefPath = join8(gitCwd, ".yapout", "brief.md");
|
|
1555
1884
|
if (existsSync7(briefPath)) {
|
|
1556
|
-
const brief =
|
|
1557
|
-
const
|
|
1558
|
-
if (
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
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
|
+
}
|
|
1563
1913
|
}
|
|
1564
1914
|
} catch {
|
|
1565
1915
|
}
|
|
1566
|
-
const commitMsg = buildCommitMessage(
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
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
|
+
);
|
|
1571
1926
|
stageAll(gitCwd);
|
|
1572
1927
|
const sha = commit(commitMsg, gitCwd);
|
|
1573
1928
|
push(branch, gitCwd);
|
|
@@ -1576,6 +1931,11 @@ function registerShipTool(server, ctx) {
|
|
|
1576
1931
|
branch,
|
|
1577
1932
|
pushed: true
|
|
1578
1933
|
};
|
|
1934
|
+
if (isBundle) {
|
|
1935
|
+
result.isBundle = true;
|
|
1936
|
+
result.bundleTitle = bundleTitle;
|
|
1937
|
+
result.linearIssueIds = allLinearIds;
|
|
1938
|
+
}
|
|
1579
1939
|
if (!config.ship_requires_checks && config.post_flight.length > 0) {
|
|
1580
1940
|
if (ctx.lastCheckPassedForRun !== args.pipelineRunId) {
|
|
1581
1941
|
result.warning = "Shipped without running post-flight checks.";
|
|
@@ -1587,11 +1947,25 @@ function registerShipTool(server, ctx) {
|
|
|
1587
1947
|
try {
|
|
1588
1948
|
const repoFullName = getRepoFullName(gitCwd);
|
|
1589
1949
|
const diffStats = getDiffStats(defaultBranch, branch, gitCwd);
|
|
1590
|
-
const
|
|
1950
|
+
const prBodyParts = [
|
|
1591
1951
|
`## Summary`,
|
|
1592
|
-
""
|
|
1593
|
-
|
|
1594
|
-
|
|
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(
|
|
1595
1969
|
`## Changes`,
|
|
1596
1970
|
"",
|
|
1597
1971
|
"```",
|
|
@@ -1600,7 +1974,8 @@ function registerShipTool(server, ctx) {
|
|
|
1600
1974
|
"",
|
|
1601
1975
|
"---",
|
|
1602
1976
|
`Implemented via [yapout](https://yapout.dev) daemon`
|
|
1603
|
-
|
|
1977
|
+
);
|
|
1978
|
+
const prBody = prBodyParts.join("\n");
|
|
1604
1979
|
const pr = await createPullRequest(
|
|
1605
1980
|
findingTitle,
|
|
1606
1981
|
prBody,
|
|
@@ -1650,30 +2025,33 @@ function registerShipTool(server, ctx) {
|
|
|
1650
2025
|
} catch (err) {
|
|
1651
2026
|
result.completionError = err.message;
|
|
1652
2027
|
}
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
anyApi2.functions.linearStatusMutations.moveIssueStatus,
|
|
1657
|
-
{
|
|
1658
|
-
projectId: ctx.projectId,
|
|
1659
|
-
linearIssueId: findingLinearId,
|
|
1660
|
-
statusType: "completed"
|
|
1661
|
-
}
|
|
1662
|
-
);
|
|
1663
|
-
} catch {
|
|
1664
|
-
}
|
|
1665
|
-
if (prUrl) {
|
|
2028
|
+
const linearIdsToUpdate = allLinearIds.length > 0 ? allLinearIds : findingLinearId ? [findingLinearId] : [];
|
|
2029
|
+
for (const linearId of linearIdsToUpdate) {
|
|
2030
|
+
if (ctx.projectId) {
|
|
1666
2031
|
try {
|
|
1667
2032
|
await ctx.client.action(
|
|
1668
|
-
anyApi2.functions.linearStatusMutations.
|
|
2033
|
+
anyApi2.functions.linearStatusMutations.moveIssueStatus,
|
|
1669
2034
|
{
|
|
1670
2035
|
projectId: ctx.projectId,
|
|
1671
|
-
linearIssueId:
|
|
1672
|
-
|
|
2036
|
+
linearIssueId: linearId,
|
|
2037
|
+
statusType: "completed"
|
|
1673
2038
|
}
|
|
1674
2039
|
);
|
|
1675
2040
|
} catch {
|
|
1676
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
|
+
}
|
|
1677
2055
|
}
|
|
1678
2056
|
}
|
|
1679
2057
|
if (args.worktreePath) {
|
|
@@ -2093,7 +2471,6 @@ The finding transitions: enriching \u2192 enriched \u2192 ready.`,
|
|
|
2093
2471
|
).optional().describe("Only meaningful Q&A from the conversation \u2014 deviations from expected scope, scoping decisions, etc. Omit if you had no questions."),
|
|
2094
2472
|
isOversized: z11.boolean().optional().describe("Set to true if this finding is too large for a single PR"),
|
|
2095
2473
|
suggestedSplit: z11.array(z11.string()).optional().describe("If oversized: suggested sub-finding titles for breaking it down"),
|
|
2096
|
-
level: z11.enum(["project", "issue"]).optional().describe("Override the finding's level if enrichment reveals it should be reclassified"),
|
|
2097
2474
|
nature: z11.enum(["implementable", "operational", "spike"]).optional().describe("Override the finding's nature if enrichment reveals it should be reclassified"),
|
|
2098
2475
|
sessionId: z11.string().optional().describe("Bulk enrichment session ID (from yapout_start_enrichment). Updates session stats.")
|
|
2099
2476
|
},
|
|
@@ -2110,7 +2487,6 @@ The finding transitions: enriching \u2192 enriched \u2192 ready.`,
|
|
|
2110
2487
|
clarifications: args.clarifications,
|
|
2111
2488
|
isOversized: args.isOversized,
|
|
2112
2489
|
suggestedSplit: args.suggestedSplit,
|
|
2113
|
-
level: args.level,
|
|
2114
2490
|
nature: args.nature
|
|
2115
2491
|
}
|
|
2116
2492
|
);
|
|
@@ -2370,11 +2746,11 @@ Mark a finding as NOT ENRICHED (isEnriched: false or omitted) when:
|
|
|
2370
2746
|
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.
|
|
2371
2747
|
|
|
2372
2748
|
PRESENTING THE FINAL PICTURE:
|
|
2373
|
-
When the conversation feels complete, present a summary grouped by
|
|
2749
|
+
When the conversation feels complete, present a summary grouped by bundles and standalone issues:
|
|
2374
2750
|
|
|
2375
2751
|
"Here's what I've gathered from our conversation:
|
|
2376
2752
|
|
|
2377
|
-
**[
|
|
2753
|
+
**[Bundle Name]** \u2014 [one-line description]
|
|
2378
2754
|
1. [Child issue] \u2014 enriched \u2713
|
|
2379
2755
|
2. [Child issue] \u2014 enriched \u2713
|
|
2380
2756
|
3. [Child issue] \u2014 needs enrichment (we didn't discuss scope)
|
|
@@ -2390,7 +2766,7 @@ SUBMITTING:
|
|
|
2390
2766
|
On confirmation, call yapout_extract_from_yap with the full data.
|
|
2391
2767
|
|
|
2392
2768
|
For each finding, provide:
|
|
2393
|
-
- title, description, sourceQuote, type, priority, confidence,
|
|
2769
|
+
- title, description, sourceQuote, type, priority, confidence, nature
|
|
2394
2770
|
- isEnriched: your honest assessment (see above)
|
|
2395
2771
|
|
|
2396
2772
|
For ENRICHED findings (isEnriched: true), also include:
|
|
@@ -2399,8 +2775,8 @@ For ENRICHED findings (isEnriched: true), also include:
|
|
|
2399
2775
|
- 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.
|
|
2400
2776
|
- clarifications: relevant Q&A from the conversation that shaped the finding
|
|
2401
2777
|
|
|
2402
|
-
For
|
|
2403
|
-
-
|
|
2778
|
+
For findings with CHILDREN (bundles), also include:
|
|
2779
|
+
- bundleDescription: what this body of work accomplishes
|
|
2404
2780
|
- suggestedOrder: implementation sequencing ("Phase 1: A, then Phase 2: B+C")
|
|
2405
2781
|
- children: array of child issues (each with their own enrichment data)
|
|
2406
2782
|
|
|
@@ -2408,16 +2784,16 @@ For children with DEPENDENCIES, include:
|
|
|
2408
2784
|
- dependsOn: array of sibling indices (0-based) that must complete first
|
|
2409
2785
|
|
|
2410
2786
|
Structure:
|
|
2411
|
-
-
|
|
2787
|
+
- Findings with children become bundles \u2014 never create child issues as separate top-level findings
|
|
2412
2788
|
- Standalone issues go at the top level
|
|
2413
2789
|
- The hierarchy you produce should be the final structure
|
|
2414
2790
|
|
|
2415
2791
|
Call yapout_extract_from_yap with:
|
|
2416
2792
|
- sessionTitle: descriptive title for this session
|
|
2417
2793
|
- sessionTranscript: clean summary of the conversation (meeting-notes style)
|
|
2418
|
-
- findings: the array (
|
|
2794
|
+
- findings: the array (bundles with children, standalone issues)
|
|
2419
2795
|
|
|
2420
|
-
This creates findings (enriched or draft based on your assessment) and
|
|
2796
|
+
This creates findings (enriched or draft based on your assessment) and bundles \u2014 all in one call. Enriched findings appear in the work queue ready for the user to sync to Linear.
|
|
2421
2797
|
|
|
2422
2798
|
Only fall back to yapout_submit_yap_session if the session was purely exploratory with no clear actionable findings.
|
|
2423
2799
|
|
|
@@ -2453,7 +2829,7 @@ var childSchema = z15.object({
|
|
|
2453
2829
|
title: z15.string().describe("Child issue title"),
|
|
2454
2830
|
description: z15.string().describe("What needs to be done and why"),
|
|
2455
2831
|
sourceQuote: z15.string().describe("Relevant excerpt from the conversation"),
|
|
2456
|
-
type: z15.enum(["feature", "bug", "chore"]).describe("Finding category"),
|
|
2832
|
+
type: z15.enum(["feature", "bug", "chore", "spike"]).describe("Finding category"),
|
|
2457
2833
|
priority: z15.enum(["urgent", "high", "medium", "low"]).describe("Priority level"),
|
|
2458
2834
|
confidence: z15.number().min(0).max(1).describe("Confidence this is a clear, actionable finding (0-1)"),
|
|
2459
2835
|
nature: z15.enum(["implementable", "operational", "spike"]).describe("What kind of work"),
|
|
@@ -2480,22 +2856,21 @@ function registerExtractFromYapTool(server, ctx) {
|
|
|
2480
2856
|
title: z15.string().describe("Finding title"),
|
|
2481
2857
|
description: z15.string().describe("What needs to be done and why"),
|
|
2482
2858
|
sourceQuote: z15.string().describe("Relevant excerpt from the conversation"),
|
|
2483
|
-
type: z15.enum(["feature", "bug", "chore"]).describe("Finding category (do not use spike \u2014 use nature instead)"),
|
|
2859
|
+
type: z15.enum(["feature", "bug", "chore", "spike"]).describe("Finding category (do not use spike \u2014 use nature instead)"),
|
|
2484
2860
|
priority: z15.enum(["urgent", "high", "medium", "low"]).describe("Priority level"),
|
|
2485
2861
|
confidence: z15.number().min(0).max(1).describe("Your confidence this is a clear, actionable finding (0-1)"),
|
|
2486
|
-
level: z15.enum(["project", "issue"]).describe("project = body of work with children, issue = single unit"),
|
|
2487
2862
|
nature: z15.enum(["implementable", "operational", "spike"]).describe("implementable = code changes, operational = manual task, spike = needs scoping"),
|
|
2488
2863
|
sourceFindingId: z15.string().optional().describe("ID of an existing finding this scopes (e.g., scoping a spike)"),
|
|
2489
|
-
//
|
|
2864
|
+
// Enrichment data for standalone findings
|
|
2490
2865
|
isEnriched: z15.boolean().optional().describe("For standalone issues: was this thoroughly discussed? true = enriched, false = draft"),
|
|
2491
|
-
enrichedDescription: z15.string().optional().describe("Clean description for Linear (
|
|
2492
|
-
acceptanceCriteria: z15.array(z15.string()).optional().describe("Testable acceptance criteria (
|
|
2493
|
-
implementationBrief: z15.string().optional().describe("Technical context (
|
|
2494
|
-
clarifications: z15.array(clarificationSchema).optional().describe("Relevant Q&A from conversation
|
|
2495
|
-
//
|
|
2496
|
-
|
|
2497
|
-
suggestedOrder: z15.string().optional().describe("Implementation order: 'Phase 1: A, then Phase 2: B+C' (
|
|
2498
|
-
children: z15.array(childSchema).optional().describe("Child issues
|
|
2866
|
+
enrichedDescription: z15.string().optional().describe("Clean description for Linear (required if isEnriched)"),
|
|
2867
|
+
acceptanceCriteria: z15.array(z15.string()).optional().describe("Testable acceptance criteria (required if isEnriched)"),
|
|
2868
|
+
implementationBrief: z15.string().optional().describe("Technical context (optional)"),
|
|
2869
|
+
clarifications: z15.array(clarificationSchema).optional().describe("Relevant Q&A from conversation"),
|
|
2870
|
+
// Bundle fields — when a finding has children, a bundle is created
|
|
2871
|
+
bundleDescription: z15.string().optional().describe("What this body of work accomplishes (only for findings with children)"),
|
|
2872
|
+
suggestedOrder: z15.string().optional().describe("Implementation order: 'Phase 1: A, then Phase 2: B+C' (only for findings with children)"),
|
|
2873
|
+
children: z15.array(childSchema).optional().describe("Child issues \u2014 when present, a bundle is created from this finding")
|
|
2499
2874
|
})
|
|
2500
2875
|
).describe("Findings from the conversation. Projects include children inline.")
|
|
2501
2876
|
},
|
|
@@ -2525,8 +2900,8 @@ function registerExtractFromYapTool(server, ctx) {
|
|
|
2525
2900
|
let draftCount = 0;
|
|
2526
2901
|
let totalFindings = 0;
|
|
2527
2902
|
for (const item of result.items) {
|
|
2528
|
-
if (item.
|
|
2529
|
-
for (const child of item.children
|
|
2903
|
+
if (item.children && item.children.length > 0) {
|
|
2904
|
+
for (const child of item.children) {
|
|
2530
2905
|
totalFindings++;
|
|
2531
2906
|
if (child.findingStatus === "enriched") enrichedCount++;
|
|
2532
2907
|
else draftCount++;
|
|
@@ -2576,35 +2951,36 @@ function registerExtractFromYapTool(server, ctx) {
|
|
|
2576
2951
|
);
|
|
2577
2952
|
}
|
|
2578
2953
|
|
|
2579
|
-
// src/mcp/tools/
|
|
2954
|
+
// src/mcp/tools/decompose-finding.ts
|
|
2580
2955
|
import { z as z16 } from "zod";
|
|
2581
|
-
function
|
|
2956
|
+
function registerDecomposeFindingTool(server, ctx) {
|
|
2582
2957
|
server.tool(
|
|
2583
|
-
"
|
|
2584
|
-
`
|
|
2958
|
+
"yapout_decompose_finding",
|
|
2959
|
+
`Decompose a finding into a bundle with child issues.
|
|
2585
2960
|
|
|
2586
|
-
Call this
|
|
2961
|
+
Call this when enrichment reveals a finding is too large for a single PR.
|
|
2962
|
+
The agent should have:
|
|
2587
2963
|
1. Read the codebase extensively
|
|
2588
2964
|
2. Asked the user scoping questions
|
|
2589
2965
|
3. Produced a set of child issues with dependencies
|
|
2590
2966
|
|
|
2591
|
-
This tool creates
|
|
2592
|
-
|
|
2967
|
+
This tool creates a bundle in yapout, assigns child findings to it, and
|
|
2968
|
+
archives the original finding. Returns the bundle ID and child finding IDs.
|
|
2593
2969
|
|
|
2594
2970
|
Each child issue's implementation brief must be detailed enough to stand alone as a
|
|
2595
2971
|
full spec \u2014 schema changes, files to modify, edge cases, and acceptance criteria.`,
|
|
2596
2972
|
{
|
|
2597
|
-
findingId: z16.string().describe("The
|
|
2598
|
-
|
|
2973
|
+
findingId: z16.string().describe("The finding being decomposed (from yapout_get_unenriched_finding)"),
|
|
2974
|
+
bundleDescription: z16.string().describe("Description for the bundle \u2014 what this body of work accomplishes"),
|
|
2599
2975
|
suggestedOrder: z16.string().describe("Human-readable implementation order (e.g. 'Phase 1: A, then Phase 2: B+C in parallel, then Phase 3: D')"),
|
|
2600
|
-
linearProjectId: z16.string().optional().describe("Existing Linear project ID to associate
|
|
2976
|
+
linearProjectId: z16.string().optional().describe("Existing Linear project ID to associate with. Omit to skip Linear project association."),
|
|
2601
2977
|
issues: z16.array(
|
|
2602
2978
|
z16.object({
|
|
2603
2979
|
title: z16.string().describe("Child issue title"),
|
|
2604
2980
|
description: z16.string().describe("What needs to be done and why"),
|
|
2605
2981
|
acceptanceCriteria: z16.array(z16.string()).describe("Testable acceptance criteria"),
|
|
2606
2982
|
implementationBrief: z16.string().describe("Full spec: files to modify, schema changes, edge cases, approach"),
|
|
2607
|
-
type: z16.enum(["feature", "bug", "chore"]).describe("Category of work"),
|
|
2983
|
+
type: z16.enum(["feature", "bug", "chore", "spike"]).describe("Category of work"),
|
|
2608
2984
|
priority: z16.enum(["urgent", "high", "medium", "low"]).describe("Priority level"),
|
|
2609
2985
|
dependsOn: z16.array(z16.number()).describe("Indices (0-based) of issues in this array that must be completed first")
|
|
2610
2986
|
})
|
|
@@ -2624,10 +3000,10 @@ full spec \u2014 schema changes, files to modify, edge cases, and acceptance cri
|
|
|
2624
3000
|
}
|
|
2625
3001
|
try {
|
|
2626
3002
|
const result = await ctx.client.mutation(
|
|
2627
|
-
anyApi2.functions.localPipeline.
|
|
3003
|
+
anyApi2.functions.localPipeline.decomposeIntoBundle,
|
|
2628
3004
|
{
|
|
2629
3005
|
findingId: args.findingId,
|
|
2630
|
-
|
|
3006
|
+
bundleDescription: args.bundleDescription,
|
|
2631
3007
|
suggestedOrder: args.suggestedOrder,
|
|
2632
3008
|
linearProjectId: args.linearProjectId,
|
|
2633
3009
|
issues: args.issues
|
|
@@ -2640,10 +3016,12 @@ full spec \u2014 schema changes, files to modify, edge cases, and acceptance cri
|
|
|
2640
3016
|
text: JSON.stringify(
|
|
2641
3017
|
{
|
|
2642
3018
|
findingId: result.findingId,
|
|
2643
|
-
|
|
3019
|
+
bundleId: result.bundleId,
|
|
3020
|
+
status: "archived",
|
|
3021
|
+
archivedReason: "decomposed",
|
|
2644
3022
|
childFindingIds: result.childFindingIds,
|
|
2645
3023
|
suggestedOrder: result.suggestedOrder,
|
|
2646
|
-
message: `
|
|
3024
|
+
message: `Finding decomposed into bundle with ${result.childFindingIds.length} child issues. Original finding archived. Child findings are in draft status \u2014 enrich and sync each one individually, or approve them for the user to review.`
|
|
2647
3025
|
},
|
|
2648
3026
|
null,
|
|
2649
3027
|
2
|
|
@@ -2656,7 +3034,7 @@ full spec \u2014 schema changes, files to modify, edge cases, and acceptance cri
|
|
|
2656
3034
|
content: [
|
|
2657
3035
|
{
|
|
2658
3036
|
type: "text",
|
|
2659
|
-
text: `Error
|
|
3037
|
+
text: `Error decomposing finding: ${err.message}`
|
|
2660
3038
|
}
|
|
2661
3039
|
],
|
|
2662
3040
|
isError: true
|
|
@@ -3183,7 +3561,7 @@ async function startMcpServer() {
|
|
|
3183
3561
|
registerSubmitYapSessionTool(server, ctx);
|
|
3184
3562
|
registerStartYapTool(server, ctx);
|
|
3185
3563
|
registerExtractFromYapTool(server, ctx);
|
|
3186
|
-
|
|
3564
|
+
registerDecomposeFindingTool(server, ctx);
|
|
3187
3565
|
registerMarkDuplicateTool(server, ctx);
|
|
3188
3566
|
registerGetLinearProjectsTool(server, ctx);
|
|
3189
3567
|
registerStartEnrichmentTool(server, ctx);
|
|
@@ -3267,7 +3645,7 @@ var cleanCommand = new Command9("clean").description("Remove worktrees for compl
|
|
|
3267
3645
|
import { Command as Command10 } from "commander";
|
|
3268
3646
|
import { resolve as resolve7 } from "path";
|
|
3269
3647
|
import {
|
|
3270
|
-
readFileSync as
|
|
3648
|
+
readFileSync as readFileSync6,
|
|
3271
3649
|
writeFileSync as writeFileSync9,
|
|
3272
3650
|
existsSync as existsSync10,
|
|
3273
3651
|
unlinkSync as unlinkSync3
|
|
@@ -3883,7 +4261,7 @@ var LOG_FILE = join11(getYapoutDir(), "watch.log");
|
|
|
3883
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) => {
|
|
3884
4262
|
if (opts.status) {
|
|
3885
4263
|
if (existsSync10(PID_FILE)) {
|
|
3886
|
-
const pid = parseInt(
|
|
4264
|
+
const pid = parseInt(readFileSync6(PID_FILE, "utf-8").trim(), 10);
|
|
3887
4265
|
if (isProcessRunning(pid)) {
|
|
3888
4266
|
console.log(
|
|
3889
4267
|
chalk11.green("Watcher is running") + chalk11.dim(` (PID ${pid})`)
|
|
@@ -3902,7 +4280,7 @@ var watchCommand = new Command10("watch").description("Watch for work and spawn
|
|
|
3902
4280
|
console.log(chalk11.dim("No watcher running"));
|
|
3903
4281
|
return;
|
|
3904
4282
|
}
|
|
3905
|
-
const pid = parseInt(
|
|
4283
|
+
const pid = parseInt(readFileSync6(PID_FILE, "utf-8").trim(), 10);
|
|
3906
4284
|
try {
|
|
3907
4285
|
process.kill(pid, "SIGTERM");
|
|
3908
4286
|
console.log(chalk11.green(`Stopped watcher (PID ${pid})`));
|
|
@@ -4129,7 +4507,7 @@ Claimed: ${ref} "${ticket.title}"`));
|
|
|
4129
4507
|
);
|
|
4130
4508
|
if (brief) {
|
|
4131
4509
|
const briefPath = join12(workDir, ".yapout", "brief.md");
|
|
4132
|
-
const briefContent =
|
|
4510
|
+
const briefContent = formatBrief(ref, ticket, brief);
|
|
4133
4511
|
writeFileSync10(briefPath, briefContent);
|
|
4134
4512
|
console.log(`Brief: ${chalk13.cyan(briefPath)}`);
|
|
4135
4513
|
}
|
|
@@ -4142,7 +4520,7 @@ Claimed: ${ref} "${ticket.title}"`));
|
|
|
4142
4520
|
);
|
|
4143
4521
|
console.log();
|
|
4144
4522
|
});
|
|
4145
|
-
function
|
|
4523
|
+
function formatBrief(ref, ticket, brief) {
|
|
4146
4524
|
const lines = [
|
|
4147
4525
|
`# ${ref}: ${ticket.title}`,
|
|
4148
4526
|
"",
|
|
@@ -4307,9 +4685,10 @@ function buildPrompt(parsed) {
|
|
|
4307
4685
|
switch (parsed.action) {
|
|
4308
4686
|
case "claim":
|
|
4309
4687
|
return [
|
|
4310
|
-
`Use yapout to implement
|
|
4311
|
-
`Claim it with yapout_claim (use worktree mode), read the brief,`,
|
|
4312
|
-
`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.`
|
|
4313
4692
|
].join(" ");
|
|
4314
4693
|
case "enrich":
|
|
4315
4694
|
return [
|