yapout 0.16.0 → 0.18.0
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 +1026 -1006
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command18 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/login.ts
|
|
7
7
|
import { Command as Command2 } from "commander";
|
|
@@ -236,6 +236,20 @@ function readActiveExecution(cwd) {
|
|
|
236
236
|
return null;
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
|
+
function writeActiveExecution(cwd, active) {
|
|
240
|
+
const yapoutDir = join(cwd, ".yapout");
|
|
241
|
+
if (!existsSync(yapoutDir)) {
|
|
242
|
+
mkdirSync(yapoutDir, { recursive: true });
|
|
243
|
+
}
|
|
244
|
+
writeFileSync(
|
|
245
|
+
join(yapoutDir, "active-execution.json"),
|
|
246
|
+
JSON.stringify(active, null, 2) + "\n"
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
function clearActiveExecution(cwd) {
|
|
250
|
+
const path = join(cwd, ".yapout", "active-execution.json");
|
|
251
|
+
if (existsSync(path)) unlinkSync(path);
|
|
252
|
+
}
|
|
239
253
|
var WATCH_DEFAULTS = {
|
|
240
254
|
auto_enrich: true,
|
|
241
255
|
auto_implement: true,
|
|
@@ -358,7 +372,7 @@ function requireAuth() {
|
|
|
358
372
|
}
|
|
359
373
|
|
|
360
374
|
// src/commands/serve.ts
|
|
361
|
-
var CLI_VERSION = "0.
|
|
375
|
+
var CLI_VERSION = "0.18.0";
|
|
362
376
|
var DEFAULT_PORT = 7777;
|
|
363
377
|
var PORT_RANGE = 10;
|
|
364
378
|
var HEARTBEAT_MS = 12e4;
|
|
@@ -407,7 +421,7 @@ function startServer(payload) {
|
|
|
407
421
|
res.writeHead(404, headers);
|
|
408
422
|
res.end();
|
|
409
423
|
});
|
|
410
|
-
return new Promise((
|
|
424
|
+
return new Promise((resolve16, reject) => {
|
|
411
425
|
let attempt = 0;
|
|
412
426
|
const tryListen = () => {
|
|
413
427
|
const port = DEFAULT_PORT + attempt;
|
|
@@ -423,7 +437,7 @@ function startServer(payload) {
|
|
|
423
437
|
server.once("error", onError);
|
|
424
438
|
server.listen(port, "127.0.0.1", () => {
|
|
425
439
|
server.removeListener("error", onError);
|
|
426
|
-
|
|
440
|
+
resolve16({
|
|
427
441
|
port,
|
|
428
442
|
close: () => server.close()
|
|
429
443
|
});
|
|
@@ -580,7 +594,7 @@ var serveCommand = new Command("serve").description(
|
|
|
580
594
|
});
|
|
581
595
|
|
|
582
596
|
// src/commands/login.ts
|
|
583
|
-
var CLI_VERSION2 = "0.
|
|
597
|
+
var CLI_VERSION2 = "0.18.0";
|
|
584
598
|
function safeReturnTo(raw) {
|
|
585
599
|
if (!raw) return null;
|
|
586
600
|
try {
|
|
@@ -592,7 +606,7 @@ function safeReturnTo(raw) {
|
|
|
592
606
|
}
|
|
593
607
|
}
|
|
594
608
|
function startCallbackServer() {
|
|
595
|
-
return new Promise((
|
|
609
|
+
return new Promise((resolve16) => {
|
|
596
610
|
let resolveData;
|
|
597
611
|
let rejectData;
|
|
598
612
|
const dataPromise = new Promise((res, rej) => {
|
|
@@ -641,7 +655,7 @@ function startCallbackServer() {
|
|
|
641
655
|
server.listen(0, () => {
|
|
642
656
|
const address = server.address();
|
|
643
657
|
const port = typeof address === "object" && address ? address.port : 0;
|
|
644
|
-
|
|
658
|
+
resolve16({ port, data: dataPromise });
|
|
645
659
|
});
|
|
646
660
|
setTimeout(() => {
|
|
647
661
|
server.close();
|
|
@@ -1511,232 +1525,8 @@ function registerUpdateContextTool(server, ctx) {
|
|
|
1511
1525
|
);
|
|
1512
1526
|
}
|
|
1513
1527
|
|
|
1514
|
-
// src/mcp/tools/
|
|
1515
|
-
import {
|
|
1516
|
-
function formatFinding(f, indent, done) {
|
|
1517
|
-
const ref = f.linearIssueId ? ` ${f.linearIssueId}` : "";
|
|
1518
|
-
const prefix = done ? "\u2713 " : "";
|
|
1519
|
-
return `${indent}${prefix}${f.title} ${f.type}\xB7${f.priority}${ref}`;
|
|
1520
|
-
}
|
|
1521
|
-
function formatWorkItem(item, done) {
|
|
1522
|
-
const lines = [];
|
|
1523
|
-
if (item.kind === "bundle") {
|
|
1524
|
-
const count = item.findings.length;
|
|
1525
|
-
const priority = item.findings.length > 0 ? item.findings.map((f) => f.priority).sort((a, b) => {
|
|
1526
|
-
const order = { urgent: 0, high: 1, medium: 2, low: 3 };
|
|
1527
|
-
return (order[a] ?? 3) - (order[b] ?? 3);
|
|
1528
|
-
})[0] : "medium";
|
|
1529
|
-
const prefix = done ? "\u2713 " : "";
|
|
1530
|
-
const prRef = done && item.pr?.githubPrNumber ? ` PR #${item.pr.githubPrNumber}` : "";
|
|
1531
|
-
lines.push(` ${prefix}\u{1F4E6} ${item.title} (${count} findings) \u2014 ${priority}${prRef}`);
|
|
1532
|
-
for (const f of item.findings) {
|
|
1533
|
-
lines.push(formatFinding(f, " ", done));
|
|
1534
|
-
}
|
|
1535
|
-
} else {
|
|
1536
|
-
const f = item.findings[0];
|
|
1537
|
-
if (f) {
|
|
1538
|
-
const prRef = done && item.pr?.githubPrNumber ? ` PR #${item.pr.githubPrNumber}` : "";
|
|
1539
|
-
const prefix = done ? "\u2713 " : "";
|
|
1540
|
-
const ref = f.linearIssueId ? ` ${f.linearIssueId}` : "";
|
|
1541
|
-
lines.push(` ${prefix}${item.title} ${f.type}\xB7${f.priority}${ref}${prRef}`);
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
return lines.join("\n");
|
|
1545
|
-
}
|
|
1546
|
-
function registerQueueTool(server, ctx) {
|
|
1547
|
-
server.tool(
|
|
1548
|
-
"yapout_queue",
|
|
1549
|
-
"List work items (bundles and standalone findings) ready for local implementation, plus active and done items.",
|
|
1550
|
-
{
|
|
1551
|
-
includeIds: z4.boolean().optional().describe("Include work item IDs in output (default: false)")
|
|
1552
|
-
},
|
|
1553
|
-
withScopeCheck(ctx, "yapout_queue", async (args) => {
|
|
1554
|
-
if (!ctx.projectId) {
|
|
1555
|
-
return {
|
|
1556
|
-
content: [
|
|
1557
|
-
{
|
|
1558
|
-
type: "text",
|
|
1559
|
-
text: "No project linked. Run yapout_init or yapout link first."
|
|
1560
|
-
}
|
|
1561
|
-
],
|
|
1562
|
-
isError: true
|
|
1563
|
-
};
|
|
1564
|
-
}
|
|
1565
|
-
const data = await ctx.client.query(
|
|
1566
|
-
anyApi5.functions.workQueue.getWorkQueue,
|
|
1567
|
-
{ projectId: ctx.projectId }
|
|
1568
|
-
);
|
|
1569
|
-
if (!data) {
|
|
1570
|
-
return {
|
|
1571
|
-
content: [
|
|
1572
|
-
{ type: "text", text: "Could not fetch work queue." }
|
|
1573
|
-
],
|
|
1574
|
-
isError: true
|
|
1575
|
-
};
|
|
1576
|
-
}
|
|
1577
|
-
const sections = [];
|
|
1578
|
-
sections.push(`Ready (${data.ready.length}):`);
|
|
1579
|
-
if (data.ready.length === 0) {
|
|
1580
|
-
sections.push(" (none)");
|
|
1581
|
-
} else {
|
|
1582
|
-
for (const item of data.ready) {
|
|
1583
|
-
const line = formatWorkItem(item);
|
|
1584
|
-
if (args.includeIds) {
|
|
1585
|
-
sections.push(`${line} [${item.id}]`);
|
|
1586
|
-
} else {
|
|
1587
|
-
sections.push(line);
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
}
|
|
1591
|
-
sections.push("");
|
|
1592
|
-
sections.push(`Active (${data.active.length}):`);
|
|
1593
|
-
if (data.active.length === 0) {
|
|
1594
|
-
sections.push(" (none)");
|
|
1595
|
-
} else {
|
|
1596
|
-
for (const item of data.active) {
|
|
1597
|
-
const statusTag = item.status === "failed" ? " \u274C FAILED" : item.status === "review" ? " \u{1F50D} review" : "";
|
|
1598
|
-
const line = formatWorkItem(item);
|
|
1599
|
-
if (args.includeIds) {
|
|
1600
|
-
sections.push(`${line}${statusTag} [${item.id}]`);
|
|
1601
|
-
} else {
|
|
1602
|
-
sections.push(`${line}${statusTag}`);
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
}
|
|
1606
|
-
sections.push("");
|
|
1607
|
-
sections.push(`Done (${data.done.length}):`);
|
|
1608
|
-
if (data.done.length === 0) {
|
|
1609
|
-
sections.push(" (none)");
|
|
1610
|
-
} else {
|
|
1611
|
-
for (const item of data.done) {
|
|
1612
|
-
const line = formatWorkItem(item, true);
|
|
1613
|
-
if (args.includeIds) {
|
|
1614
|
-
sections.push(`${line} [${item.id}]`);
|
|
1615
|
-
} else {
|
|
1616
|
-
sections.push(line);
|
|
1617
|
-
}
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
|
-
if (data.agentStatus.isActive) {
|
|
1621
|
-
sections.push("");
|
|
1622
|
-
sections.push(`Agent: ${data.agentStatus.agentCount} active, ${data.agentStatus.worktreeCount} worktrees`);
|
|
1623
|
-
}
|
|
1624
|
-
const structured = {
|
|
1625
|
-
ready: data.ready.map((item) => ({
|
|
1626
|
-
workItemId: item.id,
|
|
1627
|
-
kind: item.kind,
|
|
1628
|
-
title: item.title,
|
|
1629
|
-
findings: item.findings.map((f) => ({
|
|
1630
|
-
id: f.id,
|
|
1631
|
-
title: f.title,
|
|
1632
|
-
type: f.type,
|
|
1633
|
-
priority: f.priority,
|
|
1634
|
-
linearIssueId: f.linearIssueId
|
|
1635
|
-
}))
|
|
1636
|
-
})),
|
|
1637
|
-
activeCount: data.active.length,
|
|
1638
|
-
doneCount: data.done.length
|
|
1639
|
-
};
|
|
1640
|
-
return {
|
|
1641
|
-
content: [
|
|
1642
|
-
{ type: "text", text: sections.join("\n") },
|
|
1643
|
-
{ type: "text", text: "\n---\nStructured data:\n" + JSON.stringify(structured, null, 2) }
|
|
1644
|
-
]
|
|
1645
|
-
};
|
|
1646
|
-
})
|
|
1647
|
-
);
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
// src/mcp/tools/get-brief.ts
|
|
1651
|
-
import { z as z5 } from "zod";
|
|
1652
|
-
function registerGetBriefTool(server, ctx) {
|
|
1653
|
-
server.tool(
|
|
1654
|
-
"yapout_get_brief",
|
|
1655
|
-
"Fetch the full implementation context for a work item (finding or bundle)",
|
|
1656
|
-
{
|
|
1657
|
-
findingId: z5.string().optional().describe("The finding ID to get the brief for (deprecated: use workItemId)"),
|
|
1658
|
-
workItemId: z5.string().optional().describe("The work item ID (finding or bundle) to get the brief for")
|
|
1659
|
-
},
|
|
1660
|
-
withScopeCheck(ctx, "yapout_get_brief", async (args) => {
|
|
1661
|
-
const itemId = args.workItemId || args.findingId;
|
|
1662
|
-
if (!itemId) {
|
|
1663
|
-
return {
|
|
1664
|
-
content: [
|
|
1665
|
-
{
|
|
1666
|
-
type: "text",
|
|
1667
|
-
text: "Must provide workItemId or findingId."
|
|
1668
|
-
}
|
|
1669
|
-
],
|
|
1670
|
-
isError: true
|
|
1671
|
-
};
|
|
1672
|
-
}
|
|
1673
|
-
try {
|
|
1674
|
-
const data = await ctx.client.query(
|
|
1675
|
-
anyApi5.functions.findings.getFindingBrief,
|
|
1676
|
-
{ findingId: itemId }
|
|
1677
|
-
);
|
|
1678
|
-
if (data) {
|
|
1679
|
-
return {
|
|
1680
|
-
content: [
|
|
1681
|
-
{ type: "text", text: JSON.stringify(data, null, 2) }
|
|
1682
|
-
]
|
|
1683
|
-
};
|
|
1684
|
-
}
|
|
1685
|
-
} catch {
|
|
1686
|
-
}
|
|
1687
|
-
try {
|
|
1688
|
-
const bundle = await ctx.client.query(
|
|
1689
|
-
anyApi5.functions.bundles.getBundle,
|
|
1690
|
-
{ bundleId: itemId }
|
|
1691
|
-
);
|
|
1692
|
-
if (bundle) {
|
|
1693
|
-
const result = {
|
|
1694
|
-
kind: "bundle",
|
|
1695
|
-
bundle: {
|
|
1696
|
-
id: bundle._id,
|
|
1697
|
-
title: bundle.title,
|
|
1698
|
-
description: bundle.description,
|
|
1699
|
-
enrichedDescription: bundle.enrichedDescription,
|
|
1700
|
-
acceptanceCriteria: bundle.acceptanceCriteria,
|
|
1701
|
-
implementationBrief: bundle.implementationBrief
|
|
1702
|
-
},
|
|
1703
|
-
findings: bundle.findings.map((f) => ({
|
|
1704
|
-
id: f._id,
|
|
1705
|
-
title: f.title,
|
|
1706
|
-
description: f.description,
|
|
1707
|
-
priority: f.priority,
|
|
1708
|
-
type: f.type,
|
|
1709
|
-
linearIssueId: f.linearIssueId,
|
|
1710
|
-
linearIssueUrl: f.linearIssueUrl,
|
|
1711
|
-
enrichedDescription: f.enrichedDescription,
|
|
1712
|
-
acceptanceCriteria: f.acceptanceCriteria,
|
|
1713
|
-
implementationBrief: f.implementationBrief,
|
|
1714
|
-
dependsOn: f.dependsOn
|
|
1715
|
-
}))
|
|
1716
|
-
};
|
|
1717
|
-
return {
|
|
1718
|
-
content: [
|
|
1719
|
-
{ type: "text", text: JSON.stringify(result, null, 2) }
|
|
1720
|
-
]
|
|
1721
|
-
};
|
|
1722
|
-
}
|
|
1723
|
-
} catch {
|
|
1724
|
-
}
|
|
1725
|
-
return {
|
|
1726
|
-
content: [
|
|
1727
|
-
{
|
|
1728
|
-
type: "text",
|
|
1729
|
-
text: "Work item not found or you don't have access. Provide a valid finding ID or bundle ID."
|
|
1730
|
-
}
|
|
1731
|
-
],
|
|
1732
|
-
isError: true
|
|
1733
|
-
};
|
|
1734
|
-
})
|
|
1735
|
-
);
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
// src/mcp/tools/claim.ts
|
|
1739
|
-
import { z as z6 } from "zod";
|
|
1528
|
+
// src/mcp/tools/dev-loop.ts
|
|
1529
|
+
import { resolve as resolve6 } from "path";
|
|
1740
1530
|
|
|
1741
1531
|
// src/lib/worktree.ts
|
|
1742
1532
|
import { execSync as execSync2 } from "child_process";
|
|
@@ -1801,75 +1591,540 @@ function removeWorktree(cwd, wtPath) {
|
|
|
1801
1591
|
}
|
|
1802
1592
|
}
|
|
1803
1593
|
|
|
1804
|
-
// src/mcp/tools/
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1594
|
+
// src/mcp/tools/dev-loop.ts
|
|
1595
|
+
var queueTool = defineTool({
|
|
1596
|
+
name: "yapout_queue",
|
|
1597
|
+
description: "List Directives for the repo's bound Resource (data-model-redesign-2026-05-01). Buckets queued / in_progress / review by default; pass includeAll to get drafted / vetted / done / cancelled too.",
|
|
1598
|
+
inputSchema: {
|
|
1599
|
+
includeAll: z.boolean().optional().describe(
|
|
1600
|
+
"Include drafted/vetted/done/cancelled directives (default: false \u2014 only queued/in_progress/review)"
|
|
1601
|
+
),
|
|
1602
|
+
limit: z.number().optional().describe("Max directives to fetch (default: 100)")
|
|
1603
|
+
},
|
|
1604
|
+
handler: async (ctx, args) => {
|
|
1605
|
+
const repoRoot = resolveRepoRoot(ctx.cwd);
|
|
1606
|
+
const cfg = readResourceConfig(repoRoot);
|
|
1607
|
+
if (!cfg) {
|
|
1608
|
+
return {
|
|
1609
|
+
content: [
|
|
1610
|
+
{
|
|
1611
|
+
type: "text",
|
|
1612
|
+
text: "No `.yapout/config.json` found. Run `yapout init` in this repo first."
|
|
1613
|
+
}
|
|
1614
|
+
],
|
|
1615
|
+
isError: true
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
const result = await ctx.client.query(
|
|
1619
|
+
anyApi5.functions.dashboardNewModel.listDirectivesForResource,
|
|
1620
|
+
{
|
|
1621
|
+
resourceId: cfg.resourceId,
|
|
1622
|
+
limit: args.limit ?? 100
|
|
1623
|
+
}
|
|
1814
1624
|
);
|
|
1815
|
-
if (!
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
|
|
1828
|
-
// src/mcp/tools/claim.ts
|
|
1829
|
-
function readBranchPrefix(cwd) {
|
|
1830
|
-
try {
|
|
1831
|
-
const config = readYapoutConfig(cwd);
|
|
1832
|
-
return config.branch_prefix || "feat";
|
|
1833
|
-
} catch {
|
|
1834
|
-
return "feat";
|
|
1835
|
-
}
|
|
1836
|
-
}
|
|
1837
|
-
function slugify(text) {
|
|
1838
|
-
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
1839
|
-
}
|
|
1840
|
-
function formatFindingBrief(data) {
|
|
1841
|
-
const finding = data.finding;
|
|
1842
|
-
const sections = [
|
|
1843
|
-
`# ${finding.title}`,
|
|
1844
|
-
"",
|
|
1845
|
-
`**Priority:** ${finding.priority} | **Type:** ${finding.type}`
|
|
1846
|
-
];
|
|
1847
|
-
if (finding.linearIssueUrl) {
|
|
1848
|
-
sections.push(`**Linear:** ${finding.linearIssueUrl}`);
|
|
1849
|
-
}
|
|
1850
|
-
sections.push("", "## Description", "", finding.description);
|
|
1851
|
-
if (data.enrichedDescription) {
|
|
1852
|
-
sections.push("", "## Enriched Description", "", data.enrichedDescription);
|
|
1853
|
-
}
|
|
1854
|
-
if (data.implementationBrief) {
|
|
1855
|
-
sections.push("", "## Implementation Brief", "", data.implementationBrief);
|
|
1856
|
-
}
|
|
1857
|
-
const qa = data.enrichmentQA;
|
|
1858
|
-
if (qa && qa.length > 0) {
|
|
1859
|
-
sections.push("", "## Q&A");
|
|
1860
|
-
for (const item of qa) {
|
|
1861
|
-
sections.push("", `**Q:** ${item.question}`);
|
|
1862
|
-
sections.push(`**A:** ${item.answer || "(no answer yet)"}`);
|
|
1625
|
+
if (!result) {
|
|
1626
|
+
return {
|
|
1627
|
+
content: [
|
|
1628
|
+
{
|
|
1629
|
+
type: "text",
|
|
1630
|
+
text: "No access to the bound Resource. Check `yapout status` and re-run `yapout init` if needed."
|
|
1631
|
+
}
|
|
1632
|
+
],
|
|
1633
|
+
isError: true
|
|
1634
|
+
};
|
|
1863
1635
|
}
|
|
1636
|
+
const visible = args.includeAll ? result.directives : result.directives.filter(
|
|
1637
|
+
(d) => ["queued", "in_progress", "review"].includes(d.status)
|
|
1638
|
+
);
|
|
1639
|
+
const buckets = {};
|
|
1640
|
+
for (const d of visible) (buckets[d.status] ??= []).push(d);
|
|
1641
|
+
const lines = [];
|
|
1642
|
+
lines.push(
|
|
1643
|
+
`Resource: ${result.resource.remoteUrl ?? result.resource.canonicalId} (${result.resource._id})`
|
|
1644
|
+
);
|
|
1645
|
+
lines.push("");
|
|
1646
|
+
const order = args.includeAll ? [
|
|
1647
|
+
"queued",
|
|
1648
|
+
"in_progress",
|
|
1649
|
+
"review",
|
|
1650
|
+
"vetted",
|
|
1651
|
+
"drafted",
|
|
1652
|
+
"deployed",
|
|
1653
|
+
"done",
|
|
1654
|
+
"cancelled"
|
|
1655
|
+
] : ["queued", "in_progress", "review"];
|
|
1656
|
+
let total = 0;
|
|
1657
|
+
for (const status of order) {
|
|
1658
|
+
const rows = buckets[status];
|
|
1659
|
+
if (!rows || rows.length === 0) continue;
|
|
1660
|
+
total += rows.length;
|
|
1661
|
+
lines.push(`${labelForStatus(status)} (${rows.length}):`);
|
|
1662
|
+
for (const d of rows) {
|
|
1663
|
+
const exec2 = d.latestExecution ? ` [exec ${d.latestExecution._id.slice(-6)} ${d.latestExecution.devStatus}]` : "";
|
|
1664
|
+
lines.push(` ${d._id} ${d.title}${exec2}`);
|
|
1665
|
+
if (d.parentRequestTitles.length > 0) {
|
|
1666
|
+
lines.push(
|
|
1667
|
+
` \u21B3 ${d.parentRequestTitles.join(" | ")}` + (d.parentRequestCount > d.parentRequestTitles.length ? ` (+${d.parentRequestCount - d.parentRequestTitles.length} more)` : "")
|
|
1668
|
+
);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
lines.push("");
|
|
1672
|
+
}
|
|
1673
|
+
if (total === 0) {
|
|
1674
|
+
const filter = args.includeAll ? "" : " (use includeAll=true to include drafted/done)";
|
|
1675
|
+
lines.push(`No directives.${filter}`);
|
|
1676
|
+
}
|
|
1677
|
+
const structured = {
|
|
1678
|
+
resource: result.resource,
|
|
1679
|
+
directives: visible.map((d) => ({
|
|
1680
|
+
directiveId: d._id,
|
|
1681
|
+
title: d.title,
|
|
1682
|
+
status: d.status,
|
|
1683
|
+
actionVerb: d.actionVerb,
|
|
1684
|
+
branchName: d.branchName,
|
|
1685
|
+
parentRequestTitles: d.parentRequestTitles,
|
|
1686
|
+
parentRequestCount: d.parentRequestCount,
|
|
1687
|
+
latestExecution: d.latestExecution
|
|
1688
|
+
}))
|
|
1689
|
+
};
|
|
1690
|
+
return {
|
|
1691
|
+
content: [
|
|
1692
|
+
{ type: "text", text: lines.join("\n") },
|
|
1693
|
+
{
|
|
1694
|
+
type: "text",
|
|
1695
|
+
text: "\n---\nStructured data:\n" + JSON.stringify(structured, null, 2)
|
|
1696
|
+
}
|
|
1697
|
+
]
|
|
1698
|
+
};
|
|
1864
1699
|
}
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1700
|
+
});
|
|
1701
|
+
var pickTool = defineTool({
|
|
1702
|
+
name: "yapout_pick",
|
|
1703
|
+
description: "Claim a queued Directive for the active worktree (data-model-redesign-2026-05-01). Locks the Directive (queued \u2192 in_progress), inserts an Execution row at devStatus=running, creates a `.yapout/worktrees/directive-<short>` git worktree, and writes `.yapout/active-execution.json` inside it.",
|
|
1704
|
+
inputSchema: {
|
|
1705
|
+
directiveId: z.string().describe("Convex `directives._id` to pick"),
|
|
1706
|
+
branch: z.string().optional().describe(
|
|
1707
|
+
"Override branch name (default: `<branch_prefix>/directive-<short-id>` from .yapout/config.yml)"
|
|
1708
|
+
),
|
|
1709
|
+
agent: z.string().optional().describe("Implementer identity tag (default: 'claude')")
|
|
1710
|
+
},
|
|
1711
|
+
handler: async (ctx, args) => {
|
|
1712
|
+
const repoRoot = resolveRepoRoot(ctx.cwd);
|
|
1713
|
+
const cfg = readResourceConfig(repoRoot);
|
|
1714
|
+
if (!cfg) {
|
|
1715
|
+
return {
|
|
1716
|
+
content: [
|
|
1717
|
+
{
|
|
1718
|
+
type: "text",
|
|
1719
|
+
text: "No `.yapout/config.json` found. Run `yapout init` in this repo first."
|
|
1720
|
+
}
|
|
1721
|
+
],
|
|
1722
|
+
isError: true
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
const yapoutCfg = readYapoutConfig(repoRoot);
|
|
1726
|
+
const baseBranch = cfg.defaultBranch ?? (() => {
|
|
1727
|
+
try {
|
|
1728
|
+
return getDefaultBranch(repoRoot);
|
|
1729
|
+
} catch {
|
|
1730
|
+
return "main";
|
|
1731
|
+
}
|
|
1732
|
+
})();
|
|
1733
|
+
const shortId = args.directiveId.slice(-6);
|
|
1734
|
+
const branchName = args.branch ?? `${yapoutCfg.branch_prefix}/directive-${shortId}`;
|
|
1735
|
+
const worktreePath = getWorktreePath(repoRoot, `directive-${shortId}`);
|
|
1736
|
+
let result;
|
|
1737
|
+
try {
|
|
1738
|
+
result = await ctx.client.mutation(
|
|
1739
|
+
anyApi5.functions.resourcesCli.createExecutionForDirective,
|
|
1740
|
+
{
|
|
1741
|
+
directiveId: args.directiveId,
|
|
1742
|
+
worktreePath,
|
|
1743
|
+
branch: branchName,
|
|
1744
|
+
agent: args.agent ?? "claude"
|
|
1745
|
+
}
|
|
1746
|
+
);
|
|
1747
|
+
} catch (err) {
|
|
1748
|
+
return {
|
|
1749
|
+
content: [
|
|
1750
|
+
{
|
|
1751
|
+
type: "text",
|
|
1752
|
+
text: `Failed to pick Directive: ${err.message}`
|
|
1753
|
+
}
|
|
1754
|
+
],
|
|
1755
|
+
isError: true
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
try {
|
|
1759
|
+
createWorktree(
|
|
1760
|
+
repoRoot,
|
|
1761
|
+
`directive-${shortId}`,
|
|
1762
|
+
branchName,
|
|
1763
|
+
baseBranch
|
|
1764
|
+
);
|
|
1765
|
+
} catch (err) {
|
|
1766
|
+
return {
|
|
1767
|
+
content: [
|
|
1768
|
+
{
|
|
1769
|
+
type: "text",
|
|
1770
|
+
text: `Server-side pick succeeded but worktree creation failed: ${err.message}
|
|
1771
|
+
|
|
1772
|
+
Recovery: the Directive is now \`in_progress\` and an Execution row exists (executionId: ${result.executionId}). Resolve the worktree issue and re-run, or flip the Directive back to \`queued\` from the dashboard.`
|
|
1773
|
+
}
|
|
1774
|
+
],
|
|
1775
|
+
isError: true
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
writeActiveExecution(worktreePath, {
|
|
1779
|
+
executionId: result.executionId,
|
|
1780
|
+
directiveId: result.directiveId,
|
|
1781
|
+
branch: branchName,
|
|
1782
|
+
worktreePath,
|
|
1783
|
+
startedAt: Date.now()
|
|
1784
|
+
});
|
|
1785
|
+
return {
|
|
1786
|
+
content: [
|
|
1787
|
+
{
|
|
1788
|
+
type: "text",
|
|
1789
|
+
text: `Picked: ${result.directiveTitle}
|
|
1790
|
+
directive: ${result.directiveId}
|
|
1791
|
+
execution: ${result.executionId}
|
|
1792
|
+
worktree: ${worktreePath}
|
|
1793
|
+
branch: ${branchName}
|
|
1794
|
+
|
|
1795
|
+
Next: cd into the worktree to implement. Use yapout_observe for tangential observations and yapout_submit when ready for review.`
|
|
1796
|
+
}
|
|
1797
|
+
]
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
});
|
|
1801
|
+
var submitTool = defineTool({
|
|
1802
|
+
name: "yapout_submit",
|
|
1803
|
+
description: "Mark the active Execution as submitted and move the parent Directive to review (data-model-redesign-2026-05-01). Reads `.yapout/active-execution.json` from cwd; clears it on success. Pushing/PR opening is out of scope (the harness handles publication).",
|
|
1804
|
+
inputSchema: {},
|
|
1805
|
+
handler: async (ctx) => {
|
|
1806
|
+
const repoRoot = resolveRepoRoot(ctx.cwd);
|
|
1807
|
+
const cfg = readResourceConfig(repoRoot);
|
|
1808
|
+
if (!cfg) {
|
|
1809
|
+
return {
|
|
1810
|
+
content: [
|
|
1811
|
+
{
|
|
1812
|
+
type: "text",
|
|
1813
|
+
text: "No `.yapout/config.json` found. Run `yapout init` in this repo first."
|
|
1814
|
+
}
|
|
1815
|
+
],
|
|
1816
|
+
isError: true
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
const active = readActiveExecution(ctx.cwd);
|
|
1820
|
+
if (!active) {
|
|
1821
|
+
return {
|
|
1822
|
+
content: [
|
|
1823
|
+
{
|
|
1824
|
+
type: "text",
|
|
1825
|
+
text: "No active Execution. yapout_submit must run from a worktree where yapout_pick (or `yapout pick`) was called."
|
|
1826
|
+
}
|
|
1827
|
+
],
|
|
1828
|
+
isError: true
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
let result;
|
|
1832
|
+
try {
|
|
1833
|
+
result = await ctx.client.mutation(
|
|
1834
|
+
anyApi5.functions.resourcesCli.submitExecution,
|
|
1835
|
+
{ executionId: active.executionId }
|
|
1836
|
+
);
|
|
1837
|
+
} catch (err) {
|
|
1838
|
+
return {
|
|
1839
|
+
content: [
|
|
1840
|
+
{
|
|
1841
|
+
type: "text",
|
|
1842
|
+
text: `Failed to submit Execution: ${err.message}`
|
|
1843
|
+
}
|
|
1844
|
+
],
|
|
1845
|
+
isError: true
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
clearActiveExecution(ctx.cwd);
|
|
1849
|
+
return {
|
|
1850
|
+
content: [
|
|
1851
|
+
{
|
|
1852
|
+
type: "text",
|
|
1853
|
+
text: `Submitted Execution ${result.executionId}.
|
|
1854
|
+
Directive ${result.directiveId} is now in \`review\`.`
|
|
1855
|
+
}
|
|
1856
|
+
]
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
});
|
|
1860
|
+
var observeTool = defineTool({
|
|
1861
|
+
name: "yapout_observe",
|
|
1862
|
+
description: "File an `agent_observation` Raw against the active Execution (data-model-redesign-2026-05-01). Bypasses intakeRoutes; lineage anchored via `spawnedByExecutionId`. Use when the implementing agent notices something tangential to the Directive \u2014 extraction will turn it into a Request (or link it to an existing one).",
|
|
1863
|
+
inputSchema: {
|
|
1864
|
+
body: z.string().describe(
|
|
1865
|
+
"Freeform observation text. The extraction pipeline parses this into a Request."
|
|
1866
|
+
),
|
|
1867
|
+
severity: z.enum(["low", "normal", "high"]).optional().describe("Severity hint (default: normal)"),
|
|
1868
|
+
area: z.string().optional().describe(
|
|
1869
|
+
"Optional scoping hint for the extraction chain (e.g., 'auth', 'billing')"
|
|
1870
|
+
)
|
|
1871
|
+
},
|
|
1872
|
+
handler: async (ctx, args) => {
|
|
1873
|
+
const cwd = resolve6(ctx.cwd);
|
|
1874
|
+
const repoRoot = resolveRepoRoot(cwd);
|
|
1875
|
+
const cfg = readResourceConfig(repoRoot);
|
|
1876
|
+
if (!cfg) {
|
|
1877
|
+
return {
|
|
1878
|
+
content: [
|
|
1879
|
+
{
|
|
1880
|
+
type: "text",
|
|
1881
|
+
text: "No `.yapout/config.json` found. Run `yapout init` in this repo first."
|
|
1882
|
+
}
|
|
1883
|
+
],
|
|
1884
|
+
isError: true
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
const active = readActiveExecution(cwd);
|
|
1888
|
+
if (!active) {
|
|
1889
|
+
return {
|
|
1890
|
+
content: [
|
|
1891
|
+
{
|
|
1892
|
+
type: "text",
|
|
1893
|
+
text: "No active Execution. yapout_observe must run from a worktree where yapout_pick (or `yapout pick`) was called."
|
|
1894
|
+
}
|
|
1895
|
+
],
|
|
1896
|
+
isError: true
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
const body = args.body.trim();
|
|
1900
|
+
if (!body) {
|
|
1901
|
+
return {
|
|
1902
|
+
content: [
|
|
1903
|
+
{ type: "text", text: "Observation body cannot be empty." }
|
|
1904
|
+
],
|
|
1905
|
+
isError: true
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
let result;
|
|
1909
|
+
try {
|
|
1910
|
+
result = await ctx.client.mutation(
|
|
1911
|
+
anyApi5.functions.resourcesCli.createAgentObservationRaw,
|
|
1912
|
+
{
|
|
1913
|
+
executionId: active.executionId,
|
|
1914
|
+
body,
|
|
1915
|
+
...args.severity ? { severityHint: args.severity } : {},
|
|
1916
|
+
...args.area ? { area: args.area } : {}
|
|
1917
|
+
}
|
|
1918
|
+
);
|
|
1919
|
+
} catch (err) {
|
|
1920
|
+
return {
|
|
1921
|
+
content: [
|
|
1922
|
+
{
|
|
1923
|
+
type: "text",
|
|
1924
|
+
text: `Failed to file observation: ${err.message}`
|
|
1925
|
+
}
|
|
1926
|
+
],
|
|
1927
|
+
isError: true
|
|
1928
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
return {
|
|
1931
|
+
content: [
|
|
1932
|
+
{
|
|
1933
|
+
type: "text",
|
|
1934
|
+
text: `Filed agent observation (raw: ${result.rawId}, execution: ${result.executionId}).
|
|
1935
|
+
Extraction will turn this into a Request (or link it to an existing one).`
|
|
1936
|
+
}
|
|
1937
|
+
]
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
});
|
|
1941
|
+
function registerDevLoopTools(server, ctx) {
|
|
1942
|
+
registerTool(server, ctx, queueTool);
|
|
1943
|
+
registerTool(server, ctx, pickTool);
|
|
1944
|
+
registerTool(server, ctx, submitTool);
|
|
1945
|
+
registerTool(server, ctx, observeTool);
|
|
1946
|
+
}
|
|
1947
|
+
function labelForStatus(status) {
|
|
1948
|
+
switch (status) {
|
|
1949
|
+
case "queued":
|
|
1950
|
+
return "Ready to pick";
|
|
1951
|
+
case "in_progress":
|
|
1952
|
+
return "In progress";
|
|
1953
|
+
case "review":
|
|
1954
|
+
return "In review";
|
|
1955
|
+
case "vetted":
|
|
1956
|
+
return "Vetted (PM-side)";
|
|
1957
|
+
case "drafted":
|
|
1958
|
+
return "Drafted (PM-side)";
|
|
1959
|
+
case "deployed":
|
|
1960
|
+
return "Deployed";
|
|
1961
|
+
case "done":
|
|
1962
|
+
return "Done";
|
|
1963
|
+
case "cancelled":
|
|
1964
|
+
return "Cancelled";
|
|
1965
|
+
default:
|
|
1966
|
+
return status;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
// src/mcp/tools/get-brief.ts
|
|
1971
|
+
import { z as z4 } from "zod";
|
|
1972
|
+
function registerGetBriefTool(server, ctx) {
|
|
1973
|
+
server.tool(
|
|
1974
|
+
"yapout_get_brief",
|
|
1975
|
+
"Fetch the full implementation context for a work item (finding or bundle)",
|
|
1976
|
+
{
|
|
1977
|
+
findingId: z4.string().optional().describe("The finding ID to get the brief for (deprecated: use workItemId)"),
|
|
1978
|
+
workItemId: z4.string().optional().describe("The work item ID (finding or bundle) to get the brief for")
|
|
1979
|
+
},
|
|
1980
|
+
withScopeCheck(ctx, "yapout_get_brief", async (args) => {
|
|
1981
|
+
const itemId = args.workItemId || args.findingId;
|
|
1982
|
+
if (!itemId) {
|
|
1983
|
+
return {
|
|
1984
|
+
content: [
|
|
1985
|
+
{
|
|
1986
|
+
type: "text",
|
|
1987
|
+
text: "Must provide workItemId or findingId."
|
|
1988
|
+
}
|
|
1989
|
+
],
|
|
1990
|
+
isError: true
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
try {
|
|
1994
|
+
const data = await ctx.client.query(
|
|
1995
|
+
anyApi5.functions.findings.getFindingBrief,
|
|
1996
|
+
{ findingId: itemId }
|
|
1997
|
+
);
|
|
1998
|
+
if (data) {
|
|
1999
|
+
return {
|
|
2000
|
+
content: [
|
|
2001
|
+
{ type: "text", text: JSON.stringify(data, null, 2) }
|
|
2002
|
+
]
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
} catch {
|
|
2006
|
+
}
|
|
2007
|
+
try {
|
|
2008
|
+
const bundle = await ctx.client.query(
|
|
2009
|
+
anyApi5.functions.bundles.getBundle,
|
|
2010
|
+
{ bundleId: itemId }
|
|
2011
|
+
);
|
|
2012
|
+
if (bundle) {
|
|
2013
|
+
const result = {
|
|
2014
|
+
kind: "bundle",
|
|
2015
|
+
bundle: {
|
|
2016
|
+
id: bundle._id,
|
|
2017
|
+
title: bundle.title,
|
|
2018
|
+
description: bundle.description,
|
|
2019
|
+
enrichedDescription: bundle.enrichedDescription,
|
|
2020
|
+
acceptanceCriteria: bundle.acceptanceCriteria,
|
|
2021
|
+
implementationBrief: bundle.implementationBrief
|
|
2022
|
+
},
|
|
2023
|
+
findings: bundle.findings.map((f) => ({
|
|
2024
|
+
id: f._id,
|
|
2025
|
+
title: f.title,
|
|
2026
|
+
description: f.description,
|
|
2027
|
+
priority: f.priority,
|
|
2028
|
+
type: f.type,
|
|
2029
|
+
linearIssueId: f.linearIssueId,
|
|
2030
|
+
linearIssueUrl: f.linearIssueUrl,
|
|
2031
|
+
enrichedDescription: f.enrichedDescription,
|
|
2032
|
+
acceptanceCriteria: f.acceptanceCriteria,
|
|
2033
|
+
implementationBrief: f.implementationBrief,
|
|
2034
|
+
dependsOn: f.dependsOn
|
|
2035
|
+
}))
|
|
2036
|
+
};
|
|
2037
|
+
return {
|
|
2038
|
+
content: [
|
|
2039
|
+
{ type: "text", text: JSON.stringify(result, null, 2) }
|
|
2040
|
+
]
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
2043
|
+
} catch {
|
|
2044
|
+
}
|
|
2045
|
+
return {
|
|
2046
|
+
content: [
|
|
2047
|
+
{
|
|
2048
|
+
type: "text",
|
|
2049
|
+
text: "Work item not found or you don't have access. Provide a valid finding ID or bundle ID."
|
|
2050
|
+
}
|
|
2051
|
+
],
|
|
2052
|
+
isError: true
|
|
2053
|
+
};
|
|
2054
|
+
})
|
|
2055
|
+
);
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
// src/mcp/tools/claim.ts
|
|
2059
|
+
import { z as z5 } from "zod";
|
|
2060
|
+
import { join as join8 } from "path";
|
|
2061
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "fs";
|
|
2062
|
+
|
|
2063
|
+
// src/mcp/tools/session-progress.ts
|
|
2064
|
+
async function reportSessionProgress(ctx, pipelineRunId, message, details) {
|
|
2065
|
+
try {
|
|
2066
|
+
const run = await ctx.client.query(
|
|
2067
|
+
anyApi5.functions.pipelineRuns.getPipelineRun,
|
|
2068
|
+
{ pipelineRunId }
|
|
2069
|
+
);
|
|
2070
|
+
if (!run?.sourceId) return;
|
|
2071
|
+
await ctx.client.mutation(anyApi5.functions.comments.createComment, {
|
|
2072
|
+
findingId: run.sourceId,
|
|
2073
|
+
body: message,
|
|
2074
|
+
isSystem: true,
|
|
2075
|
+
systemEvent: "session_progress",
|
|
2076
|
+
systemEventDetails: details,
|
|
2077
|
+
sessionId: ctx.session.id ?? void 0
|
|
2078
|
+
});
|
|
2079
|
+
} catch {
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
// src/mcp/tools/claim.ts
|
|
2084
|
+
function readBranchPrefix(cwd) {
|
|
2085
|
+
try {
|
|
2086
|
+
const config = readYapoutConfig(cwd);
|
|
2087
|
+
return config.branch_prefix || "feat";
|
|
2088
|
+
} catch {
|
|
2089
|
+
return "feat";
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
function slugify(text) {
|
|
2093
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
2094
|
+
}
|
|
2095
|
+
function formatFindingBrief(data) {
|
|
2096
|
+
const finding = data.finding;
|
|
2097
|
+
const sections = [
|
|
2098
|
+
`# ${finding.title}`,
|
|
2099
|
+
"",
|
|
2100
|
+
`**Priority:** ${finding.priority} | **Type:** ${finding.type}`
|
|
2101
|
+
];
|
|
2102
|
+
if (finding.linearIssueUrl) {
|
|
2103
|
+
sections.push(`**Linear:** ${finding.linearIssueUrl}`);
|
|
2104
|
+
}
|
|
2105
|
+
sections.push("", "## Description", "", finding.description);
|
|
2106
|
+
if (data.enrichedDescription) {
|
|
2107
|
+
sections.push("", "## Enriched Description", "", data.enrichedDescription);
|
|
2108
|
+
}
|
|
2109
|
+
if (data.implementationBrief) {
|
|
2110
|
+
sections.push("", "## Implementation Brief", "", data.implementationBrief);
|
|
2111
|
+
}
|
|
2112
|
+
const qa = data.enrichmentQA;
|
|
2113
|
+
if (qa && qa.length > 0) {
|
|
2114
|
+
sections.push("", "## Q&A");
|
|
2115
|
+
for (const item of qa) {
|
|
2116
|
+
sections.push("", `**Q:** ${item.question}`);
|
|
2117
|
+
sections.push(`**A:** ${item.answer || "(no answer yet)"}`);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
if (data.userContext) {
|
|
2121
|
+
sections.push("", "## User Context", "", data.userContext);
|
|
2122
|
+
}
|
|
2123
|
+
if (data.projectContext) {
|
|
2124
|
+
sections.push("", "## Project Context", "", data.projectContext);
|
|
2125
|
+
}
|
|
2126
|
+
const warnings = data.warnings;
|
|
2127
|
+
if (warnings?.duplicateWarning || warnings?.scopeWarning) {
|
|
1873
2128
|
sections.push("", "## Warnings");
|
|
1874
2129
|
if (warnings.duplicateWarning)
|
|
1875
2130
|
sections.push("", `**Duplicate:** ${warnings.duplicateWarning}`);
|
|
@@ -1959,9 +2214,9 @@ function registerClaimTool(server, ctx) {
|
|
|
1959
2214
|
"yapout_claim",
|
|
1960
2215
|
"Claim a work item (bundle or standalone finding) for local implementation. Creates a branch (or worktree), writes the brief, and updates status.",
|
|
1961
2216
|
{
|
|
1962
|
-
workItemId:
|
|
1963
|
-
findingId:
|
|
1964
|
-
worktree:
|
|
2217
|
+
workItemId: z5.string().describe("The work item ID to claim (bundle ID or finding ID from yapout_queue)"),
|
|
2218
|
+
findingId: z5.string().optional().describe("Deprecated: use workItemId instead. If provided, treated as a standalone finding."),
|
|
2219
|
+
worktree: z5.boolean().optional().describe("Create a git worktree for parallel work (default: false)")
|
|
1965
2220
|
},
|
|
1966
2221
|
withScopeCheck(ctx, "yapout_claim", async (args) => {
|
|
1967
2222
|
if (!ctx.projectId) {
|
|
@@ -2217,17 +2472,17 @@ async function reportClaimEvents(ctx, pipelineRunId, title, branchName) {
|
|
|
2217
2472
|
}
|
|
2218
2473
|
|
|
2219
2474
|
// src/mcp/tools/event.ts
|
|
2220
|
-
import { z as
|
|
2475
|
+
import { z as z6 } from "zod";
|
|
2221
2476
|
function registerEventTool(server, ctx) {
|
|
2222
2477
|
server.tool(
|
|
2223
2478
|
"yapout_event",
|
|
2224
2479
|
"Report a status event back to yapout for the activity feed",
|
|
2225
2480
|
{
|
|
2226
|
-
pipelineRunId:
|
|
2227
|
-
event:
|
|
2481
|
+
pipelineRunId: z6.string().describe("The pipeline run ID"),
|
|
2482
|
+
event: z6.string().describe(
|
|
2228
2483
|
'Event type (e.g., "reading_codebase", "writing_code", "running_tests")'
|
|
2229
2484
|
),
|
|
2230
|
-
message:
|
|
2485
|
+
message: z6.string().describe("Human-readable description")
|
|
2231
2486
|
},
|
|
2232
2487
|
withScopeCheck(ctx, "yapout_event", async (args) => {
|
|
2233
2488
|
try {
|
|
@@ -2263,7 +2518,7 @@ function registerEventTool(server, ctx) {
|
|
|
2263
2518
|
}
|
|
2264
2519
|
|
|
2265
2520
|
// src/mcp/tools/ship.ts
|
|
2266
|
-
import { z as
|
|
2521
|
+
import { z as z7 } from "zod";
|
|
2267
2522
|
|
|
2268
2523
|
// src/lib/github-cli.ts
|
|
2269
2524
|
import { execSync as execSync3 } from "child_process";
|
|
@@ -2342,10 +2597,10 @@ function registerShipTool(server, ctx) {
|
|
|
2342
2597
|
"yapout_ship",
|
|
2343
2598
|
"Commit, push, open a PR, and mark the work item as done. Run yapout_check first if post-flight checks are configured.",
|
|
2344
2599
|
{
|
|
2345
|
-
message:
|
|
2346
|
-
skipPr:
|
|
2347
|
-
pipelineRunId:
|
|
2348
|
-
worktreePath:
|
|
2600
|
+
message: z7.string().optional().describe("Custom commit message (overrides template)"),
|
|
2601
|
+
skipPr: z7.boolean().optional().describe("Just push, don't open a PR"),
|
|
2602
|
+
pipelineRunId: z7.string().describe("The pipeline run ID from yapout_claim"),
|
|
2603
|
+
worktreePath: z7.string().optional().describe("Worktree path (if claiming was done with worktree: true)")
|
|
2349
2604
|
},
|
|
2350
2605
|
withScopeCheck(ctx, "yapout_ship", async (args) => {
|
|
2351
2606
|
const gitCwd = args.worktreePath || ctx.cwd;
|
|
@@ -2565,7 +2820,7 @@ function registerShipTool(server, ctx) {
|
|
|
2565
2820
|
}
|
|
2566
2821
|
|
|
2567
2822
|
// src/mcp/tools/check.ts
|
|
2568
|
-
import { z as
|
|
2823
|
+
import { z as z8 } from "zod";
|
|
2569
2824
|
import { exec } from "child_process";
|
|
2570
2825
|
var COMMAND_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
2571
2826
|
var MAX_OUTPUT_LINES = 100;
|
|
@@ -2576,7 +2831,7 @@ function truncate(text) {
|
|
|
2576
2831
|
` + lines.slice(-MAX_OUTPUT_LINES).join("\n");
|
|
2577
2832
|
}
|
|
2578
2833
|
function runCommand(command, cwd) {
|
|
2579
|
-
return new Promise((
|
|
2834
|
+
return new Promise((resolve16) => {
|
|
2580
2835
|
const start = Date.now();
|
|
2581
2836
|
const child = exec(command, {
|
|
2582
2837
|
cwd,
|
|
@@ -2584,7 +2839,7 @@ function runCommand(command, cwd) {
|
|
|
2584
2839
|
maxBuffer: 10 * 1024 * 1024
|
|
2585
2840
|
// 10MB
|
|
2586
2841
|
}, (error, stdout, stderr) => {
|
|
2587
|
-
|
|
2842
|
+
resolve16({
|
|
2588
2843
|
exitCode: error?.code ?? (error ? 1 : 0),
|
|
2589
2844
|
stdout: truncate(stdout),
|
|
2590
2845
|
stderr: truncate(stderr),
|
|
@@ -2592,7 +2847,7 @@ function runCommand(command, cwd) {
|
|
|
2592
2847
|
});
|
|
2593
2848
|
});
|
|
2594
2849
|
child.on("error", () => {
|
|
2595
|
-
|
|
2850
|
+
resolve16({
|
|
2596
2851
|
exitCode: 1,
|
|
2597
2852
|
stdout: "",
|
|
2598
2853
|
stderr: `Command timed out after ${COMMAND_TIMEOUT_MS / 1e3}s`,
|
|
@@ -2606,9 +2861,9 @@ function registerCheckTool(server, ctx) {
|
|
|
2606
2861
|
"yapout_check",
|
|
2607
2862
|
"Run post-flight checks (lint, test, typecheck) before shipping. Configure commands in .yapout/config.yml",
|
|
2608
2863
|
{
|
|
2609
|
-
commands:
|
|
2610
|
-
pipelineRunId:
|
|
2611
|
-
worktreePath:
|
|
2864
|
+
commands: z8.array(z8.string()).optional().describe("Override: run these commands instead of post_flight config"),
|
|
2865
|
+
pipelineRunId: z8.string().optional().describe("Pipeline run ID for event reporting"),
|
|
2866
|
+
worktreePath: z8.string().optional().describe("Run checks in this worktree directory instead of the repo root")
|
|
2612
2867
|
},
|
|
2613
2868
|
withScopeCheck(ctx, "yapout_check", async (args) => {
|
|
2614
2869
|
const checkCwd = args.worktreePath || ctx.cwd;
|
|
@@ -2679,7 +2934,7 @@ function registerCheckTool(server, ctx) {
|
|
|
2679
2934
|
}
|
|
2680
2935
|
|
|
2681
2936
|
// src/mcp/tools/get-unenriched-findings.ts
|
|
2682
|
-
import { z as
|
|
2937
|
+
import { z as z9 } from "zod";
|
|
2683
2938
|
function registerGetUnenrichedFindingsTool(server, ctx) {
|
|
2684
2939
|
server.tool(
|
|
2685
2940
|
"yapout_get_unenriched_finding",
|
|
@@ -2693,7 +2948,7 @@ After calling this tool, you should:
|
|
|
2693
2948
|
3. If the finding is ambiguous, ask the developer clarifying questions in conversation
|
|
2694
2949
|
4. When confident, call yapout_save_enrichment with a clean description, acceptance criteria, and implementation brief`,
|
|
2695
2950
|
{
|
|
2696
|
-
findingId:
|
|
2951
|
+
findingId: z9.string().optional().describe("Specific finding ID to enrich. If omitted, returns the highest priority draft finding.")
|
|
2697
2952
|
},
|
|
2698
2953
|
withScopeCheck(ctx, "yapout_get_unenriched_finding", async (args) => {
|
|
2699
2954
|
const projectId = ctx.projectId ?? process.env.YAPOUT_PROJECT_ID;
|
|
@@ -2807,7 +3062,7 @@ function registerGetExistingFindingsTool(server, ctx) {
|
|
|
2807
3062
|
}
|
|
2808
3063
|
|
|
2809
3064
|
// src/mcp/tools/save-enrichment.ts
|
|
2810
|
-
import { z as
|
|
3065
|
+
import { z as z10 } from "zod";
|
|
2811
3066
|
|
|
2812
3067
|
// src/mcp/tools/enrichment-session.ts
|
|
2813
3068
|
var activeSessions = /* @__PURE__ */ new Map();
|
|
@@ -2852,22 +3107,22 @@ This tool saves the enrichment, then automatically creates the Linear issue with
|
|
|
2852
3107
|
|
|
2853
3108
|
The finding transitions: enriching \u2192 enriched \u2192 ready.`,
|
|
2854
3109
|
{
|
|
2855
|
-
findingId:
|
|
2856
|
-
title:
|
|
2857
|
-
cleanDescription:
|
|
2858
|
-
acceptanceCriteria:
|
|
2859
|
-
implementationBrief:
|
|
2860
|
-
clarifications:
|
|
2861
|
-
|
|
2862
|
-
question:
|
|
2863
|
-
answer:
|
|
3110
|
+
findingId: z10.string().describe("The finding ID to enrich (from yapout_get_unenriched_finding)"),
|
|
3111
|
+
title: z10.string().describe("Refined finding title \u2014 improve it if the original was vague"),
|
|
3112
|
+
cleanDescription: z10.string().describe("Human-readable summary for the Linear issue body. Write the kind of finding a senior engineer would write."),
|
|
3113
|
+
acceptanceCriteria: z10.array(z10.string()).describe("List of testable acceptance criteria (each a single clear statement)"),
|
|
3114
|
+
implementationBrief: z10.string().describe("Deep technical context for the implementing agent: which files, what approach, edge cases to watch for"),
|
|
3115
|
+
clarifications: z10.array(
|
|
3116
|
+
z10.object({
|
|
3117
|
+
question: z10.string().describe("The question you asked the developer"),
|
|
3118
|
+
answer: z10.string().describe("The developer's answer")
|
|
2864
3119
|
})
|
|
2865
3120
|
).optional().describe("Only meaningful Q&A from the conversation \u2014 deviations from expected scope, scoping decisions, etc. Omit if you had no questions."),
|
|
2866
|
-
isOversized:
|
|
2867
|
-
suggestedSplit:
|
|
2868
|
-
nature:
|
|
2869
|
-
cloudSafe:
|
|
2870
|
-
sessionId:
|
|
3121
|
+
isOversized: z10.boolean().optional().describe("Set to true if this finding is too large for a single PR"),
|
|
3122
|
+
suggestedSplit: z10.array(z10.string()).optional().describe("If oversized: suggested sub-finding titles for breaking it down"),
|
|
3123
|
+
nature: z10.enum(["implementable", "operational", "spike"]).optional().describe("Override the finding's nature if enrichment reveals it should be reclassified"),
|
|
3124
|
+
cloudSafe: z10.boolean().optional().describe("Set to true ONLY if this is a small mechanical change a cloud agent can ship without sandbox testing \u2014 text/copy edits, classname tweaks, single-named-constant changes, \u226430 lines, single file, no logic/type/dependency changes. Default false."),
|
|
3125
|
+
sessionId: z10.string().optional().describe("Bulk enrichment session ID (from yapout_start_enrichment). Updates session stats.")
|
|
2871
3126
|
},
|
|
2872
3127
|
withScopeCheck(ctx, "yapout_save_enrichment", async (args) => {
|
|
2873
3128
|
try {
|
|
@@ -2898,433 +3153,70 @@ The finding transitions: enriching \u2192 enriched \u2192 ready.`,
|
|
|
2898
3153
|
const session2 = getSession(args.sessionId);
|
|
2899
3154
|
if (session2) {
|
|
2900
3155
|
updateSessionStats(args.sessionId, {
|
|
2901
|
-
enriched: session2.stats.enriched + 1
|
|
2902
|
-
});
|
|
2903
|
-
}
|
|
2904
|
-
}
|
|
2905
|
-
const session = args.sessionId ? getSession(args.sessionId) : null;
|
|
2906
|
-
const estimatedTokens = session ? session.stats.enriched * 5e3 : 0;
|
|
2907
|
-
const compactionHint = estimatedTokens > 1e5;
|
|
2908
|
-
const response = {
|
|
2909
|
-
findingId: args.findingId,
|
|
2910
|
-
linearIssueId: finding?.linearIssueId ?? null,
|
|
2911
|
-
linearIssueUrl: finding?.linearIssueUrl ?? null,
|
|
2912
|
-
compactionHint,
|
|
2913
|
-
message: finding?.linearIssueUrl ? `Finding enriched and ready in Linear: ${finding.linearIssueUrl}` : "Finding enriched and ready in Linear."
|
|
2914
|
-
};
|
|
2915
|
-
if (args.isOversized && args.suggestedSplit?.length) {
|
|
2916
|
-
response.warning = `This finding is oversized. Suggested split: ${args.suggestedSplit.join(", ")}`;
|
|
2917
|
-
}
|
|
2918
|
-
return {
|
|
2919
|
-
content: [
|
|
2920
|
-
{
|
|
2921
|
-
type: "text",
|
|
2922
|
-
text: JSON.stringify(response, null, 2)
|
|
2923
|
-
}
|
|
2924
|
-
]
|
|
2925
|
-
};
|
|
2926
|
-
} catch (err) {
|
|
2927
|
-
return {
|
|
2928
|
-
content: [
|
|
2929
|
-
{
|
|
2930
|
-
type: "text",
|
|
2931
|
-
text: `Error saving enrichment: ${err.message}`
|
|
2932
|
-
}
|
|
2933
|
-
],
|
|
2934
|
-
isError: true
|
|
2935
|
-
};
|
|
2936
|
-
}
|
|
2937
|
-
})
|
|
2938
|
-
);
|
|
2939
|
-
}
|
|
2940
|
-
|
|
2941
|
-
// src/mcp/tools/sync-to-linear.ts
|
|
2942
|
-
import { z as z12 } from "zod";
|
|
2943
|
-
function registerSyncToLinearTool(server, ctx) {
|
|
2944
|
-
server.tool(
|
|
2945
|
-
"yapout_sync_to_linear",
|
|
2946
|
-
"Trigger Linear issue creation for an enriched finding. The sync runs server-side (encrypted Linear token in Convex).",
|
|
2947
|
-
{
|
|
2948
|
-
findingId: z12.string().describe("The finding ID to sync to Linear")
|
|
2949
|
-
},
|
|
2950
|
-
withScopeCheck(ctx, "yapout_sync_to_linear", async (args) => {
|
|
2951
|
-
try {
|
|
2952
|
-
await ctx.client.action(
|
|
2953
|
-
anyApi5.functions.localPipeline.syncFindingToLinearLocal,
|
|
2954
|
-
{ findingId: args.findingId }
|
|
2955
|
-
);
|
|
2956
|
-
return {
|
|
2957
|
-
content: [
|
|
2958
|
-
{
|
|
2959
|
-
type: "text",
|
|
2960
|
-
text: JSON.stringify(
|
|
2961
|
-
{
|
|
2962
|
-
success: true,
|
|
2963
|
-
findingId: args.findingId,
|
|
2964
|
-
message: "Finding synced to Linear successfully. It will now appear in the work queue for implementation."
|
|
2965
|
-
},
|
|
2966
|
-
null,
|
|
2967
|
-
2
|
|
2968
|
-
)
|
|
2969
|
-
}
|
|
2970
|
-
]
|
|
2971
|
-
};
|
|
2972
|
-
} catch (err) {
|
|
2973
|
-
return {
|
|
2974
|
-
content: [
|
|
2975
|
-
{
|
|
2976
|
-
type: "text",
|
|
2977
|
-
text: `Error syncing to Linear: ${err.message}`
|
|
2978
|
-
}
|
|
2979
|
-
],
|
|
2980
|
-
isError: true
|
|
2981
|
-
};
|
|
2982
|
-
}
|
|
2983
|
-
})
|
|
2984
|
-
);
|
|
2985
|
-
}
|
|
2986
|
-
|
|
2987
|
-
// src/mcp/tools/submit-yap-session.ts
|
|
2988
|
-
import { z as z13 } from "zod";
|
|
2989
|
-
function registerSubmitYapSessionTool(server, ctx) {
|
|
2990
|
-
server.tool(
|
|
2991
|
-
"yapout_submit_yap_session",
|
|
2992
|
-
"Submit a yap session transcript for finding extraction",
|
|
2993
|
-
{
|
|
2994
|
-
title: z13.string().describe("Session title (e.g., 'Notification system brainstorm')"),
|
|
2995
|
-
transcript: z13.string().describe("Cleaned conversation transcript")
|
|
2996
|
-
},
|
|
2997
|
-
withScopeCheck(ctx, "yapout_submit_yap_session", async (args) => {
|
|
2998
|
-
if (!ctx.projectId) {
|
|
2999
|
-
return {
|
|
3000
|
-
content: [
|
|
3001
|
-
{
|
|
3002
|
-
type: "text",
|
|
3003
|
-
text: "No project linked. Run yapout_init or yapout link first."
|
|
3004
|
-
}
|
|
3005
|
-
],
|
|
3006
|
-
isError: true
|
|
3007
|
-
};
|
|
3008
|
-
}
|
|
3009
|
-
try {
|
|
3010
|
-
const captureId = await ctx.client.mutation(
|
|
3011
|
-
anyApi5.functions.captures.createFromYapSession,
|
|
3012
|
-
{
|
|
3013
|
-
projectId: ctx.projectId,
|
|
3014
|
-
title: args.title,
|
|
3015
|
-
transcript: args.transcript
|
|
3016
|
-
}
|
|
3017
|
-
);
|
|
3018
|
-
return {
|
|
3019
|
-
content: [
|
|
3020
|
-
{
|
|
3021
|
-
type: "text",
|
|
3022
|
-
text: JSON.stringify(
|
|
3023
|
-
{
|
|
3024
|
-
captureId,
|
|
3025
|
-
message: "Yap session submitted. Findings will appear for review shortly."
|
|
3026
|
-
},
|
|
3027
|
-
null,
|
|
3028
|
-
2
|
|
3029
|
-
)
|
|
3030
|
-
}
|
|
3031
|
-
]
|
|
3032
|
-
};
|
|
3033
|
-
} catch (err) {
|
|
3034
|
-
return {
|
|
3035
|
-
content: [
|
|
3036
|
-
{
|
|
3037
|
-
type: "text",
|
|
3038
|
-
text: `Failed to submit yap session: ${err.message}`
|
|
3039
|
-
}
|
|
3040
|
-
],
|
|
3041
|
-
isError: true
|
|
3042
|
-
};
|
|
3043
|
-
}
|
|
3044
|
-
})
|
|
3045
|
-
);
|
|
3046
|
-
}
|
|
3047
|
-
|
|
3048
|
-
// src/mcp/tools/start-yap.ts
|
|
3049
|
-
import { z as z14 } from "zod";
|
|
3050
|
-
var PERSONA_PRESETS = {
|
|
3051
|
-
"tech lead": "You are an experienced tech lead. You care about maintainability, simplicity, and shipping. Challenge over-engineering and vague scope.",
|
|
3052
|
-
"qa engineer": "You are a skeptical QA engineer. Focus on error states, edge cases, missing validation, accessibility, and user-facing failure modes.",
|
|
3053
|
-
"product owner": "You are a product owner. Focus on user value, scope, prioritization, and whether features solve real problems. Push back on technical gold-plating.",
|
|
3054
|
-
"end user": "You are an end user of this application. You're not technical. Focus on usability, clarity, frustration points, and what you'd expect to happen."
|
|
3055
|
-
};
|
|
3056
|
-
function resolvePersona(input5) {
|
|
3057
|
-
const lower = input5.toLowerCase().trim();
|
|
3058
|
-
return PERSONA_PRESETS[lower] ?? input5;
|
|
3059
|
-
}
|
|
3060
|
-
function registerStartYapTool(server, ctx) {
|
|
3061
|
-
server.tool(
|
|
3062
|
-
"yapout_start_yap",
|
|
3063
|
-
"Start a yap session \u2014 a structured AI-guided brainstorming conversation. Call this when the user wants to brainstorm, discuss ideas, or have a yap session. Returns instructions for how to conduct the session. Available personas: tech lead (default), qa engineer, product owner, end user, or any custom description.",
|
|
3064
|
-
{
|
|
3065
|
-
persona: z14.string().optional().describe(
|
|
3066
|
-
'Interviewer persona: "tech lead", "qa engineer", "product owner", "end user", or a custom description'
|
|
3067
|
-
),
|
|
3068
|
-
context: z14.string().optional().describe("What the developer wants to discuss")
|
|
3069
|
-
},
|
|
3070
|
-
withScopeCheck(ctx, "yapout_start_yap", async (args) => {
|
|
3071
|
-
if (!ctx.projectId) {
|
|
3072
|
-
return {
|
|
3073
|
-
content: [
|
|
3074
|
-
{
|
|
3075
|
-
type: "text",
|
|
3076
|
-
text: "No project linked. Run yapout_init or yapout link first."
|
|
3077
|
-
}
|
|
3078
|
-
],
|
|
3079
|
-
isError: true
|
|
3080
|
-
};
|
|
3081
|
-
}
|
|
3082
|
-
const persona = args.persona ?? "tech lead";
|
|
3083
|
-
const personaBlock = resolvePersona(persona);
|
|
3084
|
-
const contextSection = args.context ? `The developer wants to discuss: ${args.context}. Start by reading relevant parts of the codebase, then open with a specific question about their idea.` : "Start by asking the developer what they'd like to discuss. Read the codebase first to understand the project.";
|
|
3085
|
-
const instructions = `You are now in a yapout yap session \u2014 a structured brainstorming conversation about this codebase.
|
|
3086
|
-
|
|
3087
|
-
YOUR ROLE: ${personaBlock}
|
|
3088
|
-
|
|
3089
|
-
WHAT YOU DO:
|
|
3090
|
-
- Have a natural, focused conversation about the developer's idea
|
|
3091
|
-
- Read files from the codebase to inform your questions (package.json, schema, relevant source files)
|
|
3092
|
-
- Ask ONE question at a time \u2014 don't overwhelm
|
|
3093
|
-
- Push back on vague statements: "What do you mean by 'better'? Better for whom?"
|
|
3094
|
-
- Surface edge cases: "What happens when the user has no internet?"
|
|
3095
|
-
- Reference actual code: "I see you have a stateMachines.ts \u2014 should this new status go there?"
|
|
3096
|
-
- When the developer makes a clear decision, acknowledge it and move to the next topic
|
|
3097
|
-
- Keep the conversation productive \u2014 steer away from tangents
|
|
3098
|
-
|
|
3099
|
-
WHAT YOU DON'T DO:
|
|
3100
|
-
- Don't implement anything. Don't write code. Don't edit files. Just discuss.
|
|
3101
|
-
- Don't agree with everything. Challenge ideas that seem undercooked.
|
|
3102
|
-
- Don't ask more than 5 questions without letting the developer steer.
|
|
3103
|
-
- Don't make changes to the codebase, even if the user asks. A yap session produces findings \u2014 implementation happens later through the normal finding pipeline. If the user asks you to "just do it" or "make that change now," push back: "That's what the findings are for. Let's capture it properly so it goes through review." This boundary is critical \u2014 implementing during a yap bypasses every approval gate yapout exists to enforce.
|
|
3104
|
-
|
|
3105
|
-
${contextSection}
|
|
3106
|
-
|
|
3107
|
-
DURING THE SESSION \u2014 COMPLETENESS TRACKING:
|
|
3108
|
-
As the conversation progresses, you are building a mental model of every finding. For each finding, track:
|
|
3109
|
-
- What has been clearly stated (title, scope, priority)
|
|
3110
|
-
- What has been discussed in enough depth to write a finding (enrichment)
|
|
3111
|
-
- What is still vague, contradicted, or unanswered
|
|
3112
|
-
|
|
3113
|
-
The conversation does NOT need to follow a rigid structure. The user may jump between topics, change their mind, go on tangents, or revisit earlier findings. This is normal \u2014 real conversations are not linear. Your job is to follow the thread and keep track of the state of each finding regardless of conversation order.
|
|
3114
|
-
|
|
3115
|
-
If the user contradicts an earlier statement, note it and confirm which version they mean when the time is right. Don't interrupt the flow for minor clarifications \u2014 batch them for later.
|
|
3116
|
-
|
|
3117
|
-
BEFORE SUBMITTING \u2014 GAP FILLING:
|
|
3118
|
-
Before you submit, review your mental model of every finding. For each one, ask yourself:
|
|
3119
|
-
- Could a developer read this finding and start working without asking questions?
|
|
3120
|
-
- Are the acceptance criteria specific enough to verify?
|
|
3121
|
-
- Are there ambiguities the user didn't resolve?
|
|
3122
|
-
|
|
3123
|
-
If there are gaps, ask the user to fill them. Be direct: "Before I submit, I need clarity on a few things..." Group related gaps together rather than asking one at a time.
|
|
3124
|
-
|
|
3125
|
-
You do NOT need to fill every gap. If the conversation didn't cover something deeply enough, mark that finding as NOT enriched \u2014 it will go through the async enrichment pipeline later. Be honest about what you know vs. what you're guessing.
|
|
3126
|
-
|
|
3127
|
-
ENRICHMENT ASSESSMENT:
|
|
3128
|
-
For each finding, you must make an honest call: is this enriched or not?
|
|
3129
|
-
|
|
3130
|
-
Mark a finding as ENRICHED (isEnriched: true) when:
|
|
3131
|
-
- The conversation covered it thoroughly enough to produce a clear finding
|
|
3132
|
-
- You can write an enrichedDescription that a senior engineer would recognize as well-scoped
|
|
3133
|
-
- You can write at least 2-3 testable acceptance criteria
|
|
3134
|
-
- The user explicitly validated the scope (not just mentioned it in passing)
|
|
3135
|
-
|
|
3136
|
-
Mark a finding as NOT ENRICHED (isEnriched: false or omitted) when:
|
|
3137
|
-
- It came up late in the conversation without much discussion
|
|
3138
|
-
- The user mentioned it but didn't elaborate on scope or requirements
|
|
3139
|
-
- You're uncertain about key aspects (what it should do, how it should work)
|
|
3140
|
-
- It's a spike or needs further scoping
|
|
3141
|
-
|
|
3142
|
-
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.
|
|
3143
|
-
|
|
3144
|
-
PRESENTING THE FINAL PICTURE:
|
|
3145
|
-
When the conversation feels complete, present a summary grouped by bundles and standalone issues:
|
|
3146
|
-
|
|
3147
|
-
"Here's what I've gathered from our conversation:
|
|
3148
|
-
|
|
3149
|
-
**[Bundle Name]** \u2014 [one-line description]
|
|
3150
|
-
1. [Child issue] \u2014 enriched \u2713
|
|
3151
|
-
2. [Child issue] \u2014 enriched \u2713
|
|
3152
|
-
3. [Child issue] \u2014 needs enrichment (we didn't discuss scope)
|
|
3153
|
-
|
|
3154
|
-
**Standalone issues:**
|
|
3155
|
-
4. [Issue] \u2014 enriched \u2713
|
|
3156
|
-
|
|
3157
|
-
[Any open questions you still need answered]
|
|
3158
|
-
|
|
3159
|
-
Should I submit this to yapout?"
|
|
3160
|
-
|
|
3161
|
-
SUBMITTING:
|
|
3162
|
-
On confirmation, call yapout_extract_from_yap with the full data.
|
|
3163
|
-
|
|
3164
|
-
For each finding, provide:
|
|
3165
|
-
- title, description, sourceQuote, type, priority, confidence, nature
|
|
3166
|
-
- isEnriched: your honest assessment (see above)
|
|
3167
|
-
|
|
3168
|
-
For ENRICHED findings (isEnriched: true), also include:
|
|
3169
|
-
- enrichedDescription: clean, final description \u2014 write the kind of finding a senior engineer would write
|
|
3170
|
-
- acceptanceCriteria: array of specific, testable statements
|
|
3171
|
-
- 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.
|
|
3172
|
-
- clarifications: relevant Q&A from the conversation that shaped the finding
|
|
3173
|
-
|
|
3174
|
-
For findings with CHILDREN (bundles), also include:
|
|
3175
|
-
- bundleDescription: what this body of work accomplishes
|
|
3176
|
-
- suggestedOrder: implementation sequencing ("Phase 1: A, then Phase 2: B+C")
|
|
3177
|
-
- children: array of child issues (each with their own enrichment data)
|
|
3178
|
-
|
|
3179
|
-
For children with DEPENDENCIES, include:
|
|
3180
|
-
- dependsOn: array of sibling indices (0-based) that must complete first
|
|
3181
|
-
|
|
3182
|
-
Structure:
|
|
3183
|
-
- Findings with children become bundles \u2014 never create child issues as separate top-level findings
|
|
3184
|
-
- Standalone issues go at the top level
|
|
3185
|
-
- The hierarchy you produce should be the final structure
|
|
3186
|
-
|
|
3187
|
-
Call yapout_extract_from_yap with:
|
|
3188
|
-
- sessionTitle: descriptive title for this session
|
|
3189
|
-
- sessionTranscript: clean summary of the conversation (meeting-notes style)
|
|
3190
|
-
- findings: the array (bundles with children, standalone issues)
|
|
3191
|
-
|
|
3192
|
-
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.
|
|
3193
|
-
|
|
3194
|
-
Only fall back to yapout_submit_yap_session if the session was purely exploratory with no clear actionable findings.
|
|
3195
|
-
|
|
3196
|
-
--- BEGIN THE SESSION NOW ---`;
|
|
3197
|
-
return {
|
|
3198
|
-
content: [
|
|
3199
|
-
{
|
|
3200
|
-
type: "text",
|
|
3201
|
-
text: JSON.stringify(
|
|
3202
|
-
{
|
|
3203
|
-
projectId: ctx.projectId,
|
|
3204
|
-
projectName: ctx.projectName,
|
|
3205
|
-
persona,
|
|
3206
|
-
instructions
|
|
3207
|
-
},
|
|
3208
|
-
null,
|
|
3209
|
-
2
|
|
3210
|
-
)
|
|
3156
|
+
enriched: session2.stats.enriched + 1
|
|
3157
|
+
});
|
|
3211
3158
|
}
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
description: z15.string().describe("What needs to be done and why"),
|
|
3227
|
-
sourceQuote: z15.string().describe("Relevant excerpt from the conversation"),
|
|
3228
|
-
type: z15.enum(["feature", "bug", "chore", "spike"]).describe("Finding category"),
|
|
3229
|
-
priority: z15.enum(["urgent", "high", "medium", "low"]).describe("Priority level"),
|
|
3230
|
-
confidence: z15.number().min(0).max(1).describe("Confidence this is a clear, actionable finding (0-1)"),
|
|
3231
|
-
nature: z15.enum(["implementable", "operational", "spike"]).describe("What kind of work"),
|
|
3232
|
-
// Enrichment — set isEnriched: true only if the conversation covered this
|
|
3233
|
-
// issue thoroughly enough to produce a finding a developer could pick up.
|
|
3234
|
-
isEnriched: z15.boolean().optional().describe(
|
|
3235
|
-
"Did the conversation cover this issue deeply enough to produce a complete finding? true = enriched (ready for Linear), false/omitted = draft (needs async enrichment)"
|
|
3236
|
-
),
|
|
3237
|
-
enrichedDescription: z15.string().optional().describe("Clean, final description for the Linear issue body (required if isEnriched)"),
|
|
3238
|
-
acceptanceCriteria: z15.array(z15.string()).optional().describe("Testable acceptance criteria (required if isEnriched)"),
|
|
3239
|
-
implementationBrief: z15.string().optional().describe("Technical context: files, approach, edge cases (optional \u2014 the implementing agent reads the codebase anyway)"),
|
|
3240
|
-
clarifications: z15.array(clarificationSchema).optional().describe("Relevant Q&A from the conversation that shaped this finding"),
|
|
3241
|
-
dependsOn: z15.array(z15.number()).optional().describe("Indices (0-based) of sibling children that must be completed first")
|
|
3242
|
-
});
|
|
3243
|
-
function registerExtractFromYapTool(server, ctx) {
|
|
3244
|
-
server.tool(
|
|
3245
|
-
"yapout_extract_from_yap",
|
|
3246
|
-
"Submit findings from a yap session you conducted. The yap session IS the enrichment \u2014 you participated in the conversation, gathered context, and can assess completeness per-finding. Creates capture, findings (enriched or draft). Enriched findings are ready for the user to sync to Linear from the UI. Draft findings need further enrichment via the async pipeline.",
|
|
3247
|
-
{
|
|
3248
|
-
sessionTitle: z15.string().describe("Descriptive title for this yap session"),
|
|
3249
|
-
sessionTranscript: z15.string().describe("Cleaned transcript of the conversation (meeting-notes style)"),
|
|
3250
|
-
findings: z15.array(
|
|
3251
|
-
z15.object({
|
|
3252
|
-
title: z15.string().describe("Finding title"),
|
|
3253
|
-
description: z15.string().describe("What needs to be done and why"),
|
|
3254
|
-
sourceQuote: z15.string().describe("Relevant excerpt from the conversation"),
|
|
3255
|
-
type: z15.enum(["feature", "bug", "chore", "spike"]).describe("Finding category (do not use spike \u2014 use nature instead)"),
|
|
3256
|
-
priority: z15.enum(["urgent", "high", "medium", "low"]).describe("Priority level"),
|
|
3257
|
-
confidence: z15.number().min(0).max(1).describe("Your confidence this is a clear, actionable finding (0-1)"),
|
|
3258
|
-
nature: z15.enum(["implementable", "operational", "spike"]).describe("implementable = code changes, operational = manual task, spike = needs scoping"),
|
|
3259
|
-
sourceFindingId: z15.string().optional().describe("ID of an existing finding this scopes (e.g., scoping a spike)"),
|
|
3260
|
-
// Enrichment data for standalone findings
|
|
3261
|
-
isEnriched: z15.boolean().optional().describe("For standalone issues: was this thoroughly discussed? true = enriched, false = draft"),
|
|
3262
|
-
enrichedDescription: z15.string().optional().describe("Clean description for Linear (required if isEnriched)"),
|
|
3263
|
-
acceptanceCriteria: z15.array(z15.string()).optional().describe("Testable acceptance criteria (required if isEnriched)"),
|
|
3264
|
-
implementationBrief: z15.string().optional().describe("Technical context (optional)"),
|
|
3265
|
-
clarifications: z15.array(clarificationSchema).optional().describe("Relevant Q&A from conversation"),
|
|
3266
|
-
// Bundle fields — when a finding has children, a bundle is created
|
|
3267
|
-
bundleDescription: z15.string().optional().describe("What this body of work accomplishes (only for findings with children)"),
|
|
3268
|
-
suggestedOrder: z15.string().optional().describe("Implementation order: 'Phase 1: A, then Phase 2: B+C' (only for findings with children)"),
|
|
3269
|
-
children: z15.array(childSchema).optional().describe("Child issues \u2014 when present, a bundle is created from this finding")
|
|
3270
|
-
})
|
|
3271
|
-
).describe("Findings from the conversation. Projects include children inline.")
|
|
3272
|
-
},
|
|
3273
|
-
withScopeCheck(ctx, "yapout_extract_from_yap", async (args) => {
|
|
3274
|
-
if (!ctx.projectId) {
|
|
3159
|
+
}
|
|
3160
|
+
const session = args.sessionId ? getSession(args.sessionId) : null;
|
|
3161
|
+
const estimatedTokens = session ? session.stats.enriched * 5e3 : 0;
|
|
3162
|
+
const compactionHint = estimatedTokens > 1e5;
|
|
3163
|
+
const response = {
|
|
3164
|
+
findingId: args.findingId,
|
|
3165
|
+
linearIssueId: finding?.linearIssueId ?? null,
|
|
3166
|
+
linearIssueUrl: finding?.linearIssueUrl ?? null,
|
|
3167
|
+
compactionHint,
|
|
3168
|
+
message: finding?.linearIssueUrl ? `Finding enriched and ready in Linear: ${finding.linearIssueUrl}` : "Finding enriched and ready in Linear."
|
|
3169
|
+
};
|
|
3170
|
+
if (args.isOversized && args.suggestedSplit?.length) {
|
|
3171
|
+
response.warning = `This finding is oversized. Suggested split: ${args.suggestedSplit.join(", ")}`;
|
|
3172
|
+
}
|
|
3275
3173
|
return {
|
|
3276
3174
|
content: [
|
|
3277
3175
|
{
|
|
3278
3176
|
type: "text",
|
|
3279
|
-
text:
|
|
3177
|
+
text: JSON.stringify(response, null, 2)
|
|
3178
|
+
}
|
|
3179
|
+
]
|
|
3180
|
+
};
|
|
3181
|
+
} catch (err) {
|
|
3182
|
+
return {
|
|
3183
|
+
content: [
|
|
3184
|
+
{
|
|
3185
|
+
type: "text",
|
|
3186
|
+
text: `Error saving enrichment: ${err.message}`
|
|
3280
3187
|
}
|
|
3281
3188
|
],
|
|
3282
3189
|
isError: true
|
|
3283
3190
|
};
|
|
3284
3191
|
}
|
|
3192
|
+
})
|
|
3193
|
+
);
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
// src/mcp/tools/sync-to-linear.ts
|
|
3197
|
+
import { z as z11 } from "zod";
|
|
3198
|
+
function registerSyncToLinearTool(server, ctx) {
|
|
3199
|
+
server.tool(
|
|
3200
|
+
"yapout_sync_to_linear",
|
|
3201
|
+
"Trigger Linear issue creation for an enriched finding. The sync runs server-side (encrypted Linear token in Convex).",
|
|
3202
|
+
{
|
|
3203
|
+
findingId: z11.string().describe("The finding ID to sync to Linear")
|
|
3204
|
+
},
|
|
3205
|
+
withScopeCheck(ctx, "yapout_sync_to_linear", async (args) => {
|
|
3285
3206
|
try {
|
|
3286
|
-
|
|
3287
|
-
anyApi5.functions.
|
|
3288
|
-
{
|
|
3289
|
-
projectId: ctx.projectId,
|
|
3290
|
-
title: args.sessionTitle,
|
|
3291
|
-
transcript: args.sessionTranscript,
|
|
3292
|
-
findings: args.findings
|
|
3293
|
-
}
|
|
3207
|
+
await ctx.client.action(
|
|
3208
|
+
anyApi5.functions.localPipeline.syncFindingToLinearLocal,
|
|
3209
|
+
{ findingId: args.findingId }
|
|
3294
3210
|
);
|
|
3295
|
-
let enrichedCount = 0;
|
|
3296
|
-
let draftCount = 0;
|
|
3297
|
-
let totalFindings = 0;
|
|
3298
|
-
for (const item of result.items) {
|
|
3299
|
-
if (item.children && item.children.length > 0) {
|
|
3300
|
-
for (const child of item.children) {
|
|
3301
|
-
totalFindings++;
|
|
3302
|
-
if (child.findingStatus === "enriched") enrichedCount++;
|
|
3303
|
-
else draftCount++;
|
|
3304
|
-
}
|
|
3305
|
-
} else {
|
|
3306
|
-
totalFindings++;
|
|
3307
|
-
if (item.findingStatus === "enriched") enrichedCount++;
|
|
3308
|
-
else draftCount++;
|
|
3309
|
-
}
|
|
3310
|
-
}
|
|
3311
|
-
const parts = [];
|
|
3312
|
-
if (enrichedCount > 0) parts.push(`${enrichedCount} enriched (ready for Linear)`);
|
|
3313
|
-
if (draftCount > 0) parts.push(`${draftCount} need enrichment`);
|
|
3314
3211
|
return {
|
|
3315
3212
|
content: [
|
|
3316
3213
|
{
|
|
3317
3214
|
type: "text",
|
|
3318
3215
|
text: JSON.stringify(
|
|
3319
3216
|
{
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
totalFindings,
|
|
3324
|
-
enriched: enrichedCount,
|
|
3325
|
-
needsEnrichment: draftCount
|
|
3326
|
-
},
|
|
3327
|
-
message: `Created ${totalFindings} finding${totalFindings === 1 ? "" : "s"}: ${parts.join(", ")}. Review in the yapout work queue.`
|
|
3217
|
+
success: true,
|
|
3218
|
+
findingId: args.findingId,
|
|
3219
|
+
message: "Finding synced to Linear successfully. It will now appear in the work queue for implementation."
|
|
3328
3220
|
},
|
|
3329
3221
|
null,
|
|
3330
3222
|
2
|
|
@@ -3337,7 +3229,7 @@ function registerExtractFromYapTool(server, ctx) {
|
|
|
3337
3229
|
content: [
|
|
3338
3230
|
{
|
|
3339
3231
|
type: "text",
|
|
3340
|
-
text: `
|
|
3232
|
+
text: `Error syncing to Linear: ${err.message}`
|
|
3341
3233
|
}
|
|
3342
3234
|
],
|
|
3343
3235
|
isError: true
|
|
@@ -3348,7 +3240,7 @@ function registerExtractFromYapTool(server, ctx) {
|
|
|
3348
3240
|
}
|
|
3349
3241
|
|
|
3350
3242
|
// src/mcp/tools/decompose-finding.ts
|
|
3351
|
-
import { z as
|
|
3243
|
+
import { z as z12 } from "zod";
|
|
3352
3244
|
function registerDecomposeFindingTool(server, ctx) {
|
|
3353
3245
|
server.tool(
|
|
3354
3246
|
"yapout_decompose_finding",
|
|
@@ -3366,19 +3258,19 @@ archives the original finding. Returns the bundle ID and child finding IDs.
|
|
|
3366
3258
|
Each child issue's implementation brief must be detailed enough to stand alone as a
|
|
3367
3259
|
full spec \u2014 schema changes, files to modify, edge cases, and acceptance criteria.`,
|
|
3368
3260
|
{
|
|
3369
|
-
findingId:
|
|
3370
|
-
bundleDescription:
|
|
3371
|
-
suggestedOrder:
|
|
3372
|
-
linearProjectId:
|
|
3373
|
-
issues:
|
|
3374
|
-
|
|
3375
|
-
title:
|
|
3376
|
-
description:
|
|
3377
|
-
acceptanceCriteria:
|
|
3378
|
-
implementationBrief:
|
|
3379
|
-
type:
|
|
3380
|
-
priority:
|
|
3381
|
-
dependsOn:
|
|
3261
|
+
findingId: z12.string().describe("The finding being decomposed (from yapout_get_unenriched_finding)"),
|
|
3262
|
+
bundleDescription: z12.string().describe("Description for the bundle \u2014 what this body of work accomplishes"),
|
|
3263
|
+
suggestedOrder: z12.string().describe("Human-readable implementation order (e.g. 'Phase 1: A, then Phase 2: B+C in parallel, then Phase 3: D')"),
|
|
3264
|
+
linearProjectId: z12.string().optional().describe("Existing Linear project ID to associate with. Omit to skip Linear project association."),
|
|
3265
|
+
issues: z12.array(
|
|
3266
|
+
z12.object({
|
|
3267
|
+
title: z12.string().describe("Child issue title"),
|
|
3268
|
+
description: z12.string().describe("What needs to be done and why"),
|
|
3269
|
+
acceptanceCriteria: z12.array(z12.string()).describe("Testable acceptance criteria"),
|
|
3270
|
+
implementationBrief: z12.string().describe("Full spec: files to modify, schema changes, edge cases, approach"),
|
|
3271
|
+
type: z12.enum(["feature", "bug", "chore", "spike"]).describe("Category of work"),
|
|
3272
|
+
priority: z12.enum(["urgent", "high", "medium", "low"]).describe("Priority level"),
|
|
3273
|
+
dependsOn: z12.array(z12.number()).describe("Indices (0-based) of issues in this array that must be completed first")
|
|
3382
3274
|
})
|
|
3383
3275
|
).describe("The child issues produced by decomposition")
|
|
3384
3276
|
},
|
|
@@ -3441,7 +3333,7 @@ full spec \u2014 schema changes, files to modify, edge cases, and acceptance cri
|
|
|
3441
3333
|
}
|
|
3442
3334
|
|
|
3443
3335
|
// src/mcp/tools/mark-duplicate.ts
|
|
3444
|
-
import { z as
|
|
3336
|
+
import { z as z13 } from "zod";
|
|
3445
3337
|
function registerMarkDuplicateTool(server, ctx) {
|
|
3446
3338
|
server.tool(
|
|
3447
3339
|
"yapout_mark_duplicate",
|
|
@@ -3451,9 +3343,9 @@ flow when you discover the work is already tracked in Linear.
|
|
|
3451
3343
|
The finding must be in "enriching" or "enriched" status. It will be transitioned to
|
|
3452
3344
|
"failed" with the duplicate reference stored. Nothing is synced to Linear.`,
|
|
3453
3345
|
{
|
|
3454
|
-
findingId:
|
|
3455
|
-
duplicateOfLinearId:
|
|
3456
|
-
reason:
|
|
3346
|
+
findingId: z13.string().describe("The yapout finding to archive as a duplicate"),
|
|
3347
|
+
duplicateOfLinearId: z13.string().describe("The Linear issue identifier it duplicates (e.g. 'ENG-234')"),
|
|
3348
|
+
reason: z13.string().describe("Brief explanation of why this is a duplicate")
|
|
3457
3349
|
},
|
|
3458
3350
|
withScopeCheck(ctx, "yapout_mark_duplicate", async (args) => {
|
|
3459
3351
|
if (!ctx.projectId) {
|
|
@@ -3582,7 +3474,7 @@ and issue counts so you can ask the user for confirmation before creating a new
|
|
|
3582
3474
|
}
|
|
3583
3475
|
|
|
3584
3476
|
// src/mcp/tools/start-enrichment.ts
|
|
3585
|
-
import { z as
|
|
3477
|
+
import { z as z14 } from "zod";
|
|
3586
3478
|
function registerStartEnrichmentTool(server, ctx) {
|
|
3587
3479
|
server.tool(
|
|
3588
3480
|
"yapout_start_enrichment",
|
|
@@ -3593,10 +3485,10 @@ and maintains filter criteria so you don't re-pass them on every call.
|
|
|
3593
3485
|
|
|
3594
3486
|
Optionally filter by tags, capture, or explicit finding IDs.`,
|
|
3595
3487
|
{
|
|
3596
|
-
filter:
|
|
3597
|
-
tags:
|
|
3598
|
-
captureId:
|
|
3599
|
-
findingIds:
|
|
3488
|
+
filter: z14.object({
|
|
3489
|
+
tags: z14.array(z14.string()).optional().describe("Only enrich findings with these tags"),
|
|
3490
|
+
captureId: z14.string().optional().describe("Only enrich findings from this capture"),
|
|
3491
|
+
findingIds: z14.array(z14.string()).optional().describe("Only enrich these specific findings")
|
|
3600
3492
|
}).optional().describe("Filter criteria. If omitted, all draft findings in the project are included.")
|
|
3601
3493
|
},
|
|
3602
3494
|
withScopeCheck(ctx, "yapout_start_enrichment", async (args) => {
|
|
@@ -3652,7 +3544,7 @@ Optionally filter by tags, capture, or explicit finding IDs.`,
|
|
|
3652
3544
|
}
|
|
3653
3545
|
|
|
3654
3546
|
// src/mcp/tools/enrich-next.ts
|
|
3655
|
-
import { z as
|
|
3547
|
+
import { z as z15 } from "zod";
|
|
3656
3548
|
function registerEnrichNextTool(server, ctx) {
|
|
3657
3549
|
server.tool(
|
|
3658
3550
|
"yapout_enrich_next",
|
|
@@ -3663,9 +3555,9 @@ Returns the next unclaimed draft finding matching the session's filter.
|
|
|
3663
3555
|
|
|
3664
3556
|
When done=true, all findings have been processed.`,
|
|
3665
3557
|
{
|
|
3666
|
-
sessionId:
|
|
3667
|
-
skip:
|
|
3668
|
-
skipFindingId:
|
|
3558
|
+
sessionId: z15.string().describe("Session ID from yapout_start_enrichment"),
|
|
3559
|
+
skip: z15.boolean().optional().describe("If true, skip the current finding (release back to draft)"),
|
|
3560
|
+
skipFindingId: z15.string().optional().describe("The finding ID to skip (must be in 'enriching' status)")
|
|
3669
3561
|
},
|
|
3670
3562
|
withScopeCheck(ctx, "yapout_enrich_next", async (args) => {
|
|
3671
3563
|
const session = getSession(args.sessionId);
|
|
@@ -3794,7 +3686,7 @@ When done=true, all findings have been processed.`,
|
|
|
3794
3686
|
}
|
|
3795
3687
|
|
|
3796
3688
|
// src/mcp/tools/block-enrichment.ts
|
|
3797
|
-
import { z as
|
|
3689
|
+
import { z as z16 } from "zod";
|
|
3798
3690
|
function registerBlockEnrichmentTool(server, ctx) {
|
|
3799
3691
|
server.tool(
|
|
3800
3692
|
"yapout_block_enrichment",
|
|
@@ -3806,9 +3698,9 @@ The finding must currently be in "enriching" status (claimed via yapout_get_unen
|
|
|
3806
3698
|
|
|
3807
3699
|
Transitions: enriching \u2192 needs_input. The user sees the blockerReason and questions in the UI, adds context, and resubmits.`,
|
|
3808
3700
|
{
|
|
3809
|
-
findingId:
|
|
3810
|
-
blockerReason:
|
|
3811
|
-
blockerQuestions:
|
|
3701
|
+
findingId: z16.string().describe("The finding ID to block (currently in 'enriching')"),
|
|
3702
|
+
blockerReason: z16.string().describe("One sentence: why this finding cannot be enriched as written. State the missing piece concretely."),
|
|
3703
|
+
blockerQuestions: z16.array(z16.string()).min(1).describe("2-5 specific questions the user must answer before enrichment can succeed. Each must be answerable in a sentence or two.")
|
|
3812
3704
|
},
|
|
3813
3705
|
withScopeCheck(ctx, "yapout_block_enrichment", async (args) => {
|
|
3814
3706
|
try {
|
|
@@ -3855,9 +3747,9 @@ Transitions: enriching \u2192 needs_input. The user sees the blockerReason and q
|
|
|
3855
3747
|
|
|
3856
3748
|
Same semantics as yapout_block_enrichment but for an entire bundle. Bundle status transitions enriching \u2192 needs_input; child findings revert to draft.`,
|
|
3857
3749
|
{
|
|
3858
|
-
bundleId:
|
|
3859
|
-
blockerReason:
|
|
3860
|
-
blockerQuestions:
|
|
3750
|
+
bundleId: z16.string().describe("The bundle ID to block (currently in 'enriching')"),
|
|
3751
|
+
blockerReason: z16.string().describe("One sentence: why this bundle cannot be enriched."),
|
|
3752
|
+
blockerQuestions: z16.array(z16.string()).min(1).describe("2-5 specific questions the user must answer.")
|
|
3861
3753
|
},
|
|
3862
3754
|
async (args) => {
|
|
3863
3755
|
try {
|
|
@@ -5129,12 +5021,12 @@ async function startMcpServer() {
|
|
|
5129
5021
|
};
|
|
5130
5022
|
const server = new McpServer({
|
|
5131
5023
|
name: "yapout",
|
|
5132
|
-
version: "0.
|
|
5024
|
+
version: "0.18.0"
|
|
5133
5025
|
});
|
|
5134
5026
|
registerInitTool(server, ctx);
|
|
5135
5027
|
registerCompactTool(server, ctx);
|
|
5136
5028
|
registerUpdateContextTool(server, ctx);
|
|
5137
|
-
|
|
5029
|
+
registerDevLoopTools(server, ctx);
|
|
5138
5030
|
registerGetBriefTool(server, ctx);
|
|
5139
5031
|
registerClaimTool(server, ctx);
|
|
5140
5032
|
registerEventTool(server, ctx);
|
|
@@ -5144,9 +5036,6 @@ async function startMcpServer() {
|
|
|
5144
5036
|
registerGetExistingFindingsTool(server, ctx);
|
|
5145
5037
|
registerSaveEnrichmentTool(server, ctx);
|
|
5146
5038
|
registerSyncToLinearTool(server, ctx);
|
|
5147
|
-
registerSubmitYapSessionTool(server, ctx);
|
|
5148
|
-
registerStartYapTool(server, ctx);
|
|
5149
|
-
registerExtractFromYapTool(server, ctx);
|
|
5150
5039
|
registerDecomposeFindingTool(server, ctx);
|
|
5151
5040
|
registerMarkDuplicateTool(server, ctx);
|
|
5152
5041
|
registerGetLinearProjectsTool(server, ctx);
|
|
@@ -5174,108 +5063,133 @@ var mcpServerCommand = new Command8("mcp-server").description("Start the MCP ser
|
|
|
5174
5063
|
|
|
5175
5064
|
// src/commands/queue.ts
|
|
5176
5065
|
import { Command as Command9 } from "commander";
|
|
5177
|
-
import { resolve as
|
|
5066
|
+
import { resolve as resolve7 } from "path";
|
|
5178
5067
|
import chalk9 from "chalk";
|
|
5179
|
-
var queueCommand = new Command9("queue").description("Show
|
|
5068
|
+
var queueCommand = new Command9("queue").description("Show Directives for this repo's Resource").option("--all", "Include drafted/vetted/done/cancelled directives", false).option("--limit <n>", "Max directives to fetch (default 100)", "100").action(async (options) => {
|
|
5180
5069
|
const creds = requireAuth();
|
|
5181
|
-
const cwd =
|
|
5182
|
-
const
|
|
5183
|
-
|
|
5070
|
+
const cwd = resolve7(process.cwd());
|
|
5071
|
+
const repoRoot = resolveRepoRoot(cwd);
|
|
5072
|
+
const resourceCfg = readResourceConfig(repoRoot);
|
|
5073
|
+
if (!resourceCfg) {
|
|
5184
5074
|
console.error(
|
|
5185
|
-
chalk9.red("No
|
|
5075
|
+
chalk9.red("No `.yapout/config.json` found.") + " Run " + chalk9.cyan("yapout init") + " in this repo first."
|
|
5186
5076
|
);
|
|
5187
5077
|
process.exit(1);
|
|
5188
5078
|
}
|
|
5079
|
+
const limit = Math.max(1, parseInt(options.limit, 10) || 100);
|
|
5189
5080
|
const client = createConvexClient(creds.token);
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
client.query(
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
for (const t of queueData.ready) {
|
|
5206
|
-
const ref = t.linearTicketId ?? t.ticketId;
|
|
5207
|
-
const prio = colorPriority(t.priority);
|
|
5208
|
-
console.log(
|
|
5209
|
-
` ${chalk9.bold(ref)} ${t.title.slice(0, 45).padEnd(45)} ${prio} ${chalk9.dim(t.type)}`
|
|
5210
|
-
);
|
|
5211
|
-
}
|
|
5212
|
-
console.log();
|
|
5081
|
+
let result;
|
|
5082
|
+
try {
|
|
5083
|
+
result = await client.query(
|
|
5084
|
+
anyApi.functions.dashboardNewModel.listDirectivesForResource,
|
|
5085
|
+
{
|
|
5086
|
+
resourceId: resourceCfg.resourceId,
|
|
5087
|
+
limit
|
|
5088
|
+
}
|
|
5089
|
+
);
|
|
5090
|
+
} catch (err) {
|
|
5091
|
+
console.error(
|
|
5092
|
+
chalk9.red("Failed to load directive queue."),
|
|
5093
|
+
err.message
|
|
5094
|
+
);
|
|
5095
|
+
process.exit(1);
|
|
5213
5096
|
}
|
|
5214
|
-
if (
|
|
5215
|
-
console.
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
);
|
|
5220
|
-
}
|
|
5221
|
-
console.log();
|
|
5097
|
+
if (!result) {
|
|
5098
|
+
console.error(
|
|
5099
|
+
chalk9.red("No access.") + " The Resource bound in `.yapout/config.json` is unreachable. Check `yapout status` and re-run `yapout init` if needed."
|
|
5100
|
+
);
|
|
5101
|
+
process.exit(1);
|
|
5222
5102
|
}
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5103
|
+
console.log();
|
|
5104
|
+
console.log(
|
|
5105
|
+
chalk9.bold(result.resource.remoteUrl ?? result.resource.canonicalId) + chalk9.dim(` (resource ${result.resource._id})`)
|
|
5106
|
+
);
|
|
5107
|
+
const interesting = options.all ? result.directives : result.directives.filter(
|
|
5108
|
+
(d) => ["queued", "in_progress", "review"].includes(d.status)
|
|
5109
|
+
);
|
|
5110
|
+
if (interesting.length === 0) {
|
|
5111
|
+
const filter = options.all ? "" : " (use --all to include drafted/done)";
|
|
5112
|
+
console.log(chalk9.dim(`No directives.${filter}`));
|
|
5231
5113
|
console.log();
|
|
5114
|
+
return;
|
|
5232
5115
|
}
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5116
|
+
const buckets = {};
|
|
5117
|
+
for (const d of interesting) {
|
|
5118
|
+
(buckets[d.status] ??= []).push(d);
|
|
5119
|
+
}
|
|
5120
|
+
const order = options.all ? [
|
|
5121
|
+
"queued",
|
|
5122
|
+
"in_progress",
|
|
5123
|
+
"review",
|
|
5124
|
+
"vetted",
|
|
5125
|
+
"drafted",
|
|
5126
|
+
"deployed",
|
|
5127
|
+
"done",
|
|
5128
|
+
"cancelled"
|
|
5129
|
+
] : ["queued", "in_progress", "review"];
|
|
5130
|
+
for (const status of order) {
|
|
5131
|
+
const rows = buckets[status];
|
|
5132
|
+
if (!rows || rows.length === 0) continue;
|
|
5133
|
+
console.log();
|
|
5134
|
+
console.log(
|
|
5135
|
+
chalk9.bold(labelForStatus2(status)) + chalk9.dim(` (${rows.length})`)
|
|
5136
|
+
);
|
|
5137
|
+
for (const d of rows) {
|
|
5138
|
+
const idTail = d._id.slice(-6);
|
|
5139
|
+
const title = truncate2(d.title, 55).padEnd(55);
|
|
5140
|
+
const verb = d.actionVerb ? chalk9.dim(d.actionVerb) : "";
|
|
5141
|
+
const exec2 = d.latestExecution ? chalk9.dim(
|
|
5142
|
+
`[exec ${d.latestExecution._id.slice(-6)} ${d.latestExecution.devStatus}]`
|
|
5143
|
+
) : "";
|
|
5237
5144
|
console.log(
|
|
5238
|
-
`
|
|
5145
|
+
` ${chalk9.bold(idTail)} ${title} ${verb} ${exec2}`.trimEnd()
|
|
5239
5146
|
);
|
|
5147
|
+
if (d.parentRequestTitles.length > 0) {
|
|
5148
|
+
const parentSummary = d.parentRequestTitles.map((t) => truncate2(t, 40)).join(" | ");
|
|
5149
|
+
console.log(
|
|
5150
|
+
chalk9.dim(
|
|
5151
|
+
` \u21B3 ${parentSummary}` + (d.parentRequestCount > d.parentRequestTitles.length ? ` (+${d.parentRequestCount - d.parentRequestTitles.length} more)` : "")
|
|
5152
|
+
)
|
|
5153
|
+
);
|
|
5154
|
+
}
|
|
5240
5155
|
}
|
|
5241
|
-
console.log();
|
|
5242
|
-
}
|
|
5243
|
-
if ((!queueData?.ready || queueData.ready.length === 0) && (!unenriched || unenriched.length === 0) && (!pending || pending.length === 0)) {
|
|
5244
|
-
console.log(chalk9.dim("Queue is empty. Upload a transcript to get started."));
|
|
5245
|
-
console.log();
|
|
5246
5156
|
}
|
|
5157
|
+
console.log();
|
|
5247
5158
|
});
|
|
5248
|
-
function
|
|
5249
|
-
switch (
|
|
5250
|
-
case "
|
|
5251
|
-
return
|
|
5252
|
-
case "
|
|
5253
|
-
return
|
|
5254
|
-
case "
|
|
5255
|
-
return
|
|
5256
|
-
case "
|
|
5257
|
-
return
|
|
5159
|
+
function labelForStatus2(status) {
|
|
5160
|
+
switch (status) {
|
|
5161
|
+
case "queued":
|
|
5162
|
+
return "Ready to pick";
|
|
5163
|
+
case "in_progress":
|
|
5164
|
+
return "In progress";
|
|
5165
|
+
case "review":
|
|
5166
|
+
return "In review";
|
|
5167
|
+
case "vetted":
|
|
5168
|
+
return "Vetted (PM-side)";
|
|
5169
|
+
case "drafted":
|
|
5170
|
+
return "Drafted (PM-side)";
|
|
5171
|
+
case "deployed":
|
|
5172
|
+
return "Deployed";
|
|
5173
|
+
case "done":
|
|
5174
|
+
return "Done";
|
|
5175
|
+
case "cancelled":
|
|
5176
|
+
return "Cancelled";
|
|
5258
5177
|
default:
|
|
5259
|
-
return
|
|
5178
|
+
return status;
|
|
5260
5179
|
}
|
|
5261
5180
|
}
|
|
5262
|
-
function
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
const m = Math.floor(s / 60);
|
|
5266
|
-
if (m < 60) return `${m}m ago`;
|
|
5267
|
-
const h = Math.floor(m / 60);
|
|
5268
|
-
if (h < 24) return `${h}h ago`;
|
|
5269
|
-
return `${Math.floor(h / 24)}d ago`;
|
|
5181
|
+
function truncate2(s, max) {
|
|
5182
|
+
if (s.length <= max) return s;
|
|
5183
|
+
return s.slice(0, max - 1) + "\u2026";
|
|
5270
5184
|
}
|
|
5271
5185
|
|
|
5272
5186
|
// src/commands/recap.ts
|
|
5273
5187
|
import { Command as Command10 } from "commander";
|
|
5274
|
-
import { resolve as
|
|
5188
|
+
import { resolve as resolve8 } from "path";
|
|
5275
5189
|
import chalk10 from "chalk";
|
|
5276
5190
|
var recapCommand = new Command10("recap").description("Show a summary of recent yapout activity").option("--week", "Show full week summary (default: today)").action(async (opts) => {
|
|
5277
5191
|
const creds = requireAuth();
|
|
5278
|
-
const cwd =
|
|
5192
|
+
const cwd = resolve8(process.cwd());
|
|
5279
5193
|
const mapping = getProjectMapping(cwd);
|
|
5280
5194
|
if (!mapping) {
|
|
5281
5195
|
console.error(
|
|
@@ -5329,44 +5243,13 @@ var recapCommand = new Command10("recap").description("Show a summary of recent
|
|
|
5329
5243
|
console.log();
|
|
5330
5244
|
});
|
|
5331
5245
|
|
|
5332
|
-
// src/commands/
|
|
5246
|
+
// src/commands/agent.ts
|
|
5333
5247
|
import { Command as Command11 } from "commander";
|
|
5334
5248
|
import chalk11 from "chalk";
|
|
5335
|
-
var yapCommand = new Command11("yap").description(
|
|
5336
|
-
"Start a yap session \u2014 brainstorm with an AI that knows your codebase"
|
|
5337
|
-
).action(() => {
|
|
5338
|
-
console.log(chalk11.bold("yapout yap sessions"));
|
|
5339
|
-
console.log();
|
|
5340
|
-
console.log(
|
|
5341
|
-
"Yap sessions run inside Claude \u2014 Desktop, CLI, or web \u2014 anywhere the"
|
|
5342
|
-
);
|
|
5343
|
-
console.log("yapout MCP server is connected.");
|
|
5344
|
-
console.log();
|
|
5345
|
-
console.log("Just tell Claude:");
|
|
5346
|
-
console.log(
|
|
5347
|
-
chalk11.green(` "Let's have a yap session about [topic]"`)
|
|
5348
|
-
);
|
|
5349
|
-
console.log(
|
|
5350
|
-
chalk11.green(
|
|
5351
|
-
' "I want to brainstorm [idea] \u2014 be a skeptical QA engineer"'
|
|
5352
|
-
)
|
|
5353
|
-
);
|
|
5354
|
-
console.log();
|
|
5355
|
-
console.log(chalk11.dim("Personas: tech lead, qa engineer, product owner, end user, or custom"));
|
|
5356
|
-
console.log(
|
|
5357
|
-
chalk11.dim(
|
|
5358
|
-
"Claude will call yapout_start_yap to get instructions and yapout_submit_yap_session when done."
|
|
5359
|
-
)
|
|
5360
|
-
);
|
|
5361
|
-
});
|
|
5362
|
-
|
|
5363
|
-
// src/commands/agent.ts
|
|
5364
|
-
import { Command as Command12 } from "commander";
|
|
5365
|
-
import chalk12 from "chalk";
|
|
5366
5249
|
import { input, confirm } from "@inquirer/prompts";
|
|
5367
|
-
import { resolve as
|
|
5250
|
+
import { resolve as resolve9 } from "path";
|
|
5368
5251
|
async function resolveProjectId() {
|
|
5369
|
-
const cwd =
|
|
5252
|
+
const cwd = resolve9(process.cwd());
|
|
5370
5253
|
const mapping = getProjectMapping(cwd);
|
|
5371
5254
|
if (!mapping) {
|
|
5372
5255
|
throw new Error(
|
|
@@ -5378,7 +5261,7 @@ async function resolveProjectId() {
|
|
|
5378
5261
|
projectName: mapping.projectName
|
|
5379
5262
|
};
|
|
5380
5263
|
}
|
|
5381
|
-
var agentCreate = new
|
|
5264
|
+
var agentCreate = new Command11("create").description("Create a new agent identity with a fresh token").option("--name <name>", "Display name (e.g. 'frontend-implementer')").option("--role <description>", "One-line role description").option(
|
|
5382
5265
|
"--scopes <scopes>",
|
|
5383
5266
|
"Comma-separated list of MCP tool names, or '*' for full access",
|
|
5384
5267
|
"*"
|
|
@@ -5399,23 +5282,23 @@ var agentCreate = new Command12("create").description("Create a new agent identi
|
|
|
5399
5282
|
label: opts.label
|
|
5400
5283
|
}
|
|
5401
5284
|
);
|
|
5402
|
-
console.log(
|
|
5285
|
+
console.log(chalk11.green(`Agent "${name}" created in ${projectName}.`));
|
|
5403
5286
|
console.log("");
|
|
5404
|
-
console.log(
|
|
5405
|
-
console.log(
|
|
5406
|
-
console.log(
|
|
5287
|
+
console.log(chalk11.dim("agentUserId:"), result.agentUserId);
|
|
5288
|
+
console.log(chalk11.dim("tokenId: "), result.tokenId);
|
|
5289
|
+
console.log(chalk11.dim("token: "), chalk11.yellow(result.rawToken));
|
|
5407
5290
|
console.log("");
|
|
5408
5291
|
console.log(
|
|
5409
|
-
|
|
5292
|
+
chalk11.bold("Store this token now."),
|
|
5410
5293
|
"Yapout keeps only the SHA-256 hash; this is the only time it's shown."
|
|
5411
5294
|
);
|
|
5412
5295
|
console.log(
|
|
5413
5296
|
"Pass to the harness via the",
|
|
5414
|
-
|
|
5297
|
+
chalk11.cyan("YAPOUT_AGENT_TOKEN"),
|
|
5415
5298
|
"env var."
|
|
5416
5299
|
);
|
|
5417
5300
|
});
|
|
5418
|
-
var agentList = new
|
|
5301
|
+
var agentList = new Command11("list").description("List agents in the current project").action(async () => {
|
|
5419
5302
|
const creds = requireAuth();
|
|
5420
5303
|
const { projectId, projectName } = await resolveProjectId();
|
|
5421
5304
|
const client = createConvexClient(creds.token);
|
|
@@ -5424,26 +5307,26 @@ var agentList = new Command12("list").description("List agents in the current pr
|
|
|
5424
5307
|
{ projectId }
|
|
5425
5308
|
);
|
|
5426
5309
|
if (rows.length === 0) {
|
|
5427
|
-
console.log(
|
|
5310
|
+
console.log(chalk11.dim(`No agents in ${projectName}.`));
|
|
5428
5311
|
return;
|
|
5429
5312
|
}
|
|
5430
|
-
console.log(
|
|
5313
|
+
console.log(chalk11.bold(`Agents in ${projectName}:`));
|
|
5431
5314
|
for (const a of rows) {
|
|
5432
|
-
const role = a.roleDescription ?
|
|
5433
|
-
console.log(` ${
|
|
5434
|
-
console.log(
|
|
5315
|
+
const role = a.roleDescription ? chalk11.dim(` \u2014 ${a.roleDescription}`) : "";
|
|
5316
|
+
console.log(` ${chalk11.cyan(a.displayName)}${role}`);
|
|
5317
|
+
console.log(chalk11.dim(` id: ${a._id}`));
|
|
5435
5318
|
console.log(
|
|
5436
|
-
|
|
5319
|
+
chalk11.dim(` created: ${new Date(a.createdAt).toLocaleString()}`)
|
|
5437
5320
|
);
|
|
5438
5321
|
}
|
|
5439
5322
|
});
|
|
5440
|
-
var agentRevoke = new
|
|
5323
|
+
var agentRevoke = new Command11("revoke").description("Revoke an agent token by tokenId").argument("<tokenId>", "Token id (from `yapout agent list` or creation output)").action(async (tokenId) => {
|
|
5441
5324
|
const creds = requireAuth();
|
|
5442
5325
|
const client = createConvexClient(creds.token);
|
|
5443
5326
|
await client.mutation(anyApi.functions.agents.revokeToken, { tokenId });
|
|
5444
|
-
console.log(
|
|
5327
|
+
console.log(chalk11.green("Token revoked."));
|
|
5445
5328
|
});
|
|
5446
|
-
var agentRotate = new
|
|
5329
|
+
var agentRotate = new Command11("rotate").description("Issue a new token and revoke the old one (for an existing agent)").argument("<tokenId>", "Old token id to rotate").option(
|
|
5447
5330
|
"--scopes <scopes>",
|
|
5448
5331
|
"Comma-separated MCP tool names or '*'. Defaults to the old token's scopes."
|
|
5449
5332
|
).option("--label <label>", "New token label").action(async (oldTokenId, opts) => {
|
|
@@ -5454,7 +5337,7 @@ var agentRotate = new Command12("rotate").description("Issue a new token and rev
|
|
|
5454
5337
|
{ tokenId: oldTokenId }
|
|
5455
5338
|
);
|
|
5456
5339
|
if (!oldToken) {
|
|
5457
|
-
console.error(
|
|
5340
|
+
console.error(chalk11.red("Old token not found or you don't have access."));
|
|
5458
5341
|
process.exit(1);
|
|
5459
5342
|
}
|
|
5460
5343
|
const scopes = opts.scopes ? opts.scopes === "*" ? ["*"] : opts.scopes.split(",").map((s) => s.trim()).filter(Boolean) : oldToken.scopes;
|
|
@@ -5480,21 +5363,21 @@ var agentRotate = new Command12("rotate").description("Issue a new token and rev
|
|
|
5480
5363
|
tokenId: oldTokenId
|
|
5481
5364
|
});
|
|
5482
5365
|
console.log(
|
|
5483
|
-
|
|
5366
|
+
chalk11.green(`Token rotated for "${oldToken.agentDisplayName ?? oldToken.agentUserId}".`)
|
|
5484
5367
|
);
|
|
5485
|
-
console.log(
|
|
5486
|
-
console.log(
|
|
5368
|
+
console.log(chalk11.dim("new tokenId:"), created.tokenId);
|
|
5369
|
+
console.log(chalk11.dim("token: "), chalk11.yellow(created.rawToken));
|
|
5487
5370
|
console.log("");
|
|
5488
|
-
console.log(
|
|
5371
|
+
console.log(chalk11.bold("Store this token now."), "Old token revoked.");
|
|
5489
5372
|
});
|
|
5490
|
-
var agentCommand = new
|
|
5373
|
+
var agentCommand = new Command11("agent").description("Manage agent identities and tokens").addCommand(agentCreate).addCommand(agentList).addCommand(agentRevoke).addCommand(agentRotate);
|
|
5491
5374
|
|
|
5492
5375
|
// src/commands/webhook.ts
|
|
5493
|
-
import { Command as
|
|
5494
|
-
import
|
|
5376
|
+
import { Command as Command12 } from "commander";
|
|
5377
|
+
import chalk12 from "chalk";
|
|
5495
5378
|
import { input as input2, checkbox } from "@inquirer/prompts";
|
|
5496
5379
|
import { randomBytes } from "crypto";
|
|
5497
|
-
import { resolve as
|
|
5380
|
+
import { resolve as resolve10 } from "path";
|
|
5498
5381
|
var SUPPORTED_EVENTS = [
|
|
5499
5382
|
"finding.status_changed",
|
|
5500
5383
|
"finding.created",
|
|
@@ -5506,7 +5389,7 @@ var SUPPORTED_EVENTS = [
|
|
|
5506
5389
|
"settings.changed"
|
|
5507
5390
|
];
|
|
5508
5391
|
async function resolveProjectId2() {
|
|
5509
|
-
const cwd =
|
|
5392
|
+
const cwd = resolve10(process.cwd());
|
|
5510
5393
|
const mapping = getProjectMapping(cwd);
|
|
5511
5394
|
if (!mapping) {
|
|
5512
5395
|
throw new Error(
|
|
@@ -5515,7 +5398,7 @@ async function resolveProjectId2() {
|
|
|
5515
5398
|
}
|
|
5516
5399
|
return { projectId: mapping.projectId, projectName: mapping.projectName };
|
|
5517
5400
|
}
|
|
5518
|
-
var webhookList = new
|
|
5401
|
+
var webhookList = new Command12("list").description("List outbound webhooks for the current project").action(async () => {
|
|
5519
5402
|
const creds = requireAuth();
|
|
5520
5403
|
const { projectId, projectName } = await resolveProjectId2();
|
|
5521
5404
|
const client = createConvexClient(creds.token);
|
|
@@ -5524,19 +5407,19 @@ var webhookList = new Command13("list").description("List outbound webhooks for
|
|
|
5524
5407
|
{ projectId }
|
|
5525
5408
|
);
|
|
5526
5409
|
if (rows.length === 0) {
|
|
5527
|
-
console.log(
|
|
5410
|
+
console.log(chalk12.dim(`No webhooks for ${projectName}.`));
|
|
5528
5411
|
return;
|
|
5529
5412
|
}
|
|
5530
|
-
console.log(
|
|
5413
|
+
console.log(chalk12.bold(`Webhooks for ${projectName}:`));
|
|
5531
5414
|
for (const w of rows) {
|
|
5532
|
-
const status = w.active ?
|
|
5533
|
-
console.log(` ${
|
|
5534
|
-
console.log(
|
|
5535
|
-
console.log(
|
|
5536
|
-
console.log(
|
|
5415
|
+
const status = w.active ? chalk12.green("active") : chalk12.red(`disabled (${w.consecutiveFailures} failures)`);
|
|
5416
|
+
console.log(` ${chalk12.cyan(w.label ?? w.url)} \u2014 ${status}`);
|
|
5417
|
+
console.log(chalk12.dim(` id: ${w._id}`));
|
|
5418
|
+
console.log(chalk12.dim(` url: ${w.url}`));
|
|
5419
|
+
console.log(chalk12.dim(` events: ${w.events.join(", ")}`));
|
|
5537
5420
|
}
|
|
5538
5421
|
});
|
|
5539
|
-
var webhookCreate = new
|
|
5422
|
+
var webhookCreate = new Command12("create").description("Create an outbound webhook subscription").option("--url <url>", "Webhook target URL").option("--label <label>", "Human-readable label").option(
|
|
5540
5423
|
"--events <list>",
|
|
5541
5424
|
`Comma-separated event names. Available: ${SUPPORTED_EVENTS.join(", ")}`
|
|
5542
5425
|
).option(
|
|
@@ -5563,23 +5446,23 @@ var webhookCreate = new Command13("create").description("Create an outbound webh
|
|
|
5563
5446
|
anyApi.functions.webhooks.createWebhook,
|
|
5564
5447
|
{ projectId, url, label, events, secret }
|
|
5565
5448
|
);
|
|
5566
|
-
console.log(
|
|
5567
|
-
console.log(
|
|
5449
|
+
console.log(chalk12.green(`Webhook created in ${projectName}.`));
|
|
5450
|
+
console.log(chalk12.dim("webhookId:"), result.webhookId);
|
|
5568
5451
|
if (!opts.secret) {
|
|
5569
|
-
console.log(
|
|
5452
|
+
console.log(chalk12.dim("secret: "), chalk12.yellow(secret));
|
|
5570
5453
|
console.log(
|
|
5571
|
-
|
|
5454
|
+
chalk12.bold("Store this secret now."),
|
|
5572
5455
|
"Yapout keeps only the encrypted form; the receiver needs the plaintext to verify HMAC signatures."
|
|
5573
5456
|
);
|
|
5574
5457
|
}
|
|
5575
5458
|
});
|
|
5576
|
-
var webhookDelete = new
|
|
5459
|
+
var webhookDelete = new Command12("delete").description("Delete a webhook subscription").argument("<webhookId>").action(async (webhookId) => {
|
|
5577
5460
|
const creds = requireAuth();
|
|
5578
5461
|
const client = createConvexClient(creds.token);
|
|
5579
5462
|
await client.mutation(anyApi.functions.webhooks.deleteWebhook, { webhookId });
|
|
5580
|
-
console.log(
|
|
5463
|
+
console.log(chalk12.green("Webhook deleted."));
|
|
5581
5464
|
});
|
|
5582
|
-
var webhookTest = new
|
|
5465
|
+
var webhookTest = new Command12("test").description("Inspect recent delivery attempts for a webhook").argument("<webhookId>").option("-n, --limit <n>", "Number of deliveries to show", "20").action(async (webhookId, opts) => {
|
|
5583
5466
|
const creds = requireAuth();
|
|
5584
5467
|
const client = createConvexClient(creds.token);
|
|
5585
5468
|
const limit = parseInt(opts.limit, 10);
|
|
@@ -5588,28 +5471,28 @@ var webhookTest = new Command13("test").description("Inspect recent delivery att
|
|
|
5588
5471
|
{ webhookId, limit }
|
|
5589
5472
|
);
|
|
5590
5473
|
if (rows.length === 0) {
|
|
5591
|
-
console.log(
|
|
5474
|
+
console.log(chalk12.dim("No deliveries yet."));
|
|
5592
5475
|
return;
|
|
5593
5476
|
}
|
|
5594
5477
|
for (const d of rows) {
|
|
5595
|
-
const statusLabel = d.status === "success" ?
|
|
5478
|
+
const statusLabel = d.status === "success" ? chalk12.green(d.status) : d.status === "failed" ? chalk12.red(d.status) : chalk12.yellow(d.status);
|
|
5596
5479
|
const time = new Date(d.createdAt).toLocaleString();
|
|
5597
5480
|
const http3 = d.httpStatus !== void 0 ? ` HTTP ${d.httpStatus}` : "";
|
|
5598
|
-
const err = d.error ?
|
|
5481
|
+
const err = d.error ? chalk12.dim(` (${d.error})`) : "";
|
|
5599
5482
|
console.log(
|
|
5600
|
-
` ${time} \u2014 ${statusLabel} attempt ${d.attempt}${http3} \u2014 ${
|
|
5483
|
+
` ${time} \u2014 ${statusLabel} attempt ${d.attempt}${http3} \u2014 ${chalk12.cyan(d.event)}${err}`
|
|
5601
5484
|
);
|
|
5602
5485
|
}
|
|
5603
5486
|
});
|
|
5604
|
-
var webhookCommand = new
|
|
5487
|
+
var webhookCommand = new Command12("webhook").description("Manage outbound webhook subscriptions").addCommand(webhookList).addCommand(webhookCreate).addCommand(webhookDelete).addCommand(webhookTest);
|
|
5605
5488
|
|
|
5606
5489
|
// src/commands/inbound.ts
|
|
5607
|
-
import { Command as
|
|
5608
|
-
import
|
|
5490
|
+
import { Command as Command13 } from "commander";
|
|
5491
|
+
import chalk13 from "chalk";
|
|
5609
5492
|
import { input as input3 } from "@inquirer/prompts";
|
|
5610
|
-
import { resolve as
|
|
5493
|
+
import { resolve as resolve11 } from "path";
|
|
5611
5494
|
async function resolveProjectId3() {
|
|
5612
|
-
const cwd =
|
|
5495
|
+
const cwd = resolve11(process.cwd());
|
|
5613
5496
|
const mapping = getProjectMapping(cwd);
|
|
5614
5497
|
if (!mapping) {
|
|
5615
5498
|
throw new Error(
|
|
@@ -5618,7 +5501,7 @@ async function resolveProjectId3() {
|
|
|
5618
5501
|
}
|
|
5619
5502
|
return { projectId: mapping.projectId, projectName: mapping.projectName };
|
|
5620
5503
|
}
|
|
5621
|
-
var inboundList = new
|
|
5504
|
+
var inboundList = new Command13("list").description("List inbound webhook tokens for the current project").action(async () => {
|
|
5622
5505
|
const creds = requireAuth();
|
|
5623
5506
|
const { projectId, projectName } = await resolveProjectId3();
|
|
5624
5507
|
const client = createConvexClient(creds.token);
|
|
@@ -5627,20 +5510,20 @@ var inboundList = new Command14("list").description("List inbound webhook tokens
|
|
|
5627
5510
|
{ projectId }
|
|
5628
5511
|
);
|
|
5629
5512
|
if (rows.length === 0) {
|
|
5630
|
-
console.log(
|
|
5513
|
+
console.log(chalk13.dim(`No inbound webhook tokens for ${projectName}.`));
|
|
5631
5514
|
return;
|
|
5632
5515
|
}
|
|
5633
|
-
console.log(
|
|
5516
|
+
console.log(chalk13.bold(`Inbound tokens for ${projectName}:`));
|
|
5634
5517
|
for (const t of rows) {
|
|
5635
|
-
const status = t.revokedAt ?
|
|
5636
|
-
console.log(` ${
|
|
5637
|
-
console.log(
|
|
5518
|
+
const status = t.revokedAt ? chalk13.red("revoked") : t.lastUsedAt ? chalk13.green(`last used ${new Date(t.lastUsedAt).toLocaleString()}`) : chalk13.dim("unused");
|
|
5519
|
+
console.log(` ${chalk13.cyan(t.label)} \u2014 ${status}`);
|
|
5520
|
+
console.log(chalk13.dim(` id: ${t._id}`));
|
|
5638
5521
|
console.log(
|
|
5639
|
-
|
|
5522
|
+
chalk13.dim(` created: ${new Date(t.createdAt).toLocaleString()}`)
|
|
5640
5523
|
);
|
|
5641
5524
|
}
|
|
5642
5525
|
});
|
|
5643
|
-
var inboundCreate = new
|
|
5526
|
+
var inboundCreate = new Command13("create").description("Issue a new inbound webhook token").option("--label <label>", "Human-readable label").action(async (opts) => {
|
|
5644
5527
|
const creds = requireAuth();
|
|
5645
5528
|
const { projectId, projectName } = await resolveProjectId3();
|
|
5646
5529
|
const client = createConvexClient(creds.token);
|
|
@@ -5653,15 +5536,15 @@ var inboundCreate = new Command14("create").description("Issue a new inbound web
|
|
|
5653
5536
|
const cloudUrl = getConvexUrl();
|
|
5654
5537
|
return cloudUrl.replace(/\.convex\.cloud(\/.*)?$/, ".convex.site");
|
|
5655
5538
|
})();
|
|
5656
|
-
console.log(
|
|
5657
|
-
console.log(
|
|
5658
|
-
console.log(
|
|
5539
|
+
console.log(chalk13.green(`Inbound token created in ${projectName}.`));
|
|
5540
|
+
console.log(chalk13.dim("tokenId:"), result.tokenId);
|
|
5541
|
+
console.log(chalk13.dim("token: "), chalk13.yellow(result.rawToken));
|
|
5659
5542
|
console.log("");
|
|
5660
|
-
console.log(
|
|
5543
|
+
console.log(chalk13.bold("Store this token now."), "Yapout keeps only the SHA-256 hash.");
|
|
5661
5544
|
console.log("");
|
|
5662
5545
|
console.log("Usage:");
|
|
5663
5546
|
console.log(
|
|
5664
|
-
` ${
|
|
5547
|
+
` ${chalk13.cyan("curl")} -X POST ${convexSiteUrl}/api/inbound/${projectId} \\`
|
|
5665
5548
|
);
|
|
5666
5549
|
console.log(` -H "Authorization: Bearer ${result.rawToken}" \\`);
|
|
5667
5550
|
console.log(` -H "Content-Type: application/json" \\`);
|
|
@@ -5669,22 +5552,22 @@ var inboundCreate = new Command14("create").description("Issue a new inbound web
|
|
|
5669
5552
|
` -d '{ "content": "<raw text>", "sourceLabel": "${label}" }'`
|
|
5670
5553
|
);
|
|
5671
5554
|
});
|
|
5672
|
-
var inboundRevoke = new
|
|
5555
|
+
var inboundRevoke = new Command13("revoke").description("Revoke an inbound webhook token").argument("<tokenId>").action(async (tokenId) => {
|
|
5673
5556
|
const creds = requireAuth();
|
|
5674
5557
|
const client = createConvexClient(creds.token);
|
|
5675
5558
|
await client.mutation(anyApi.functions.inboundTokens.revokeInboundToken, {
|
|
5676
5559
|
tokenId
|
|
5677
5560
|
});
|
|
5678
|
-
console.log(
|
|
5561
|
+
console.log(chalk13.green("Inbound token revoked."));
|
|
5679
5562
|
});
|
|
5680
|
-
var inboundCommand = new
|
|
5563
|
+
var inboundCommand = new Command13("inbound").description("Manage inbound webhook tokens (long-tail capture sources)").addCommand(inboundList).addCommand(inboundCreate).addCommand(inboundRevoke);
|
|
5681
5564
|
|
|
5682
5565
|
// src/commands/pillar.ts
|
|
5683
|
-
import { Command as
|
|
5684
|
-
import
|
|
5566
|
+
import { Command as Command14 } from "commander";
|
|
5567
|
+
import chalk14 from "chalk";
|
|
5685
5568
|
import { input as input4, select as select2 } from "@inquirer/prompts";
|
|
5686
5569
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
5687
|
-
import { resolve as
|
|
5570
|
+
import { resolve as resolve12 } from "path";
|
|
5688
5571
|
var PILLARS = [
|
|
5689
5572
|
"extraction",
|
|
5690
5573
|
"enrichment",
|
|
@@ -5693,7 +5576,7 @@ var PILLARS = [
|
|
|
5693
5576
|
];
|
|
5694
5577
|
var MODES = ["cloud-default", "disabled", "external-webhook"];
|
|
5695
5578
|
async function resolveProjectId4() {
|
|
5696
|
-
const cwd =
|
|
5579
|
+
const cwd = resolve12(process.cwd());
|
|
5697
5580
|
const mapping = getProjectMapping(cwd);
|
|
5698
5581
|
if (!mapping) {
|
|
5699
5582
|
throw new Error(
|
|
@@ -5705,14 +5588,14 @@ async function resolveProjectId4() {
|
|
|
5705
5588
|
function modeBadge(mode) {
|
|
5706
5589
|
switch (mode) {
|
|
5707
5590
|
case "cloud-default":
|
|
5708
|
-
return
|
|
5591
|
+
return chalk14.dim("cloud");
|
|
5709
5592
|
case "disabled":
|
|
5710
|
-
return
|
|
5593
|
+
return chalk14.gray("disabled");
|
|
5711
5594
|
case "external-webhook":
|
|
5712
|
-
return
|
|
5595
|
+
return chalk14.green("webhook");
|
|
5713
5596
|
}
|
|
5714
5597
|
}
|
|
5715
|
-
var pillarList = new
|
|
5598
|
+
var pillarList = new Command14("list").description("Show current modes for all four pillars").action(async () => {
|
|
5716
5599
|
const creds = requireAuth();
|
|
5717
5600
|
const { projectId, projectName } = await resolveProjectId4();
|
|
5718
5601
|
const client = createConvexClient(creds.token);
|
|
@@ -5720,17 +5603,17 @@ var pillarList = new Command15("list").description("Show current modes for all f
|
|
|
5720
5603
|
anyApi.functions.projectConfig.getProjectConfig,
|
|
5721
5604
|
{ projectId }
|
|
5722
5605
|
);
|
|
5723
|
-
console.log(
|
|
5606
|
+
console.log(chalk14.bold(`Pillars for ${projectName}:`));
|
|
5724
5607
|
for (const p of PILLARS) {
|
|
5725
5608
|
const setting = config?.pillars?.[p] ?? { mode: "cloud-default" };
|
|
5726
|
-
const url = setting.mode === "external-webhook" && setting.webhookUrl ?
|
|
5609
|
+
const url = setting.mode === "external-webhook" && setting.webhookUrl ? chalk14.dim(` \u2192 ${setting.webhookUrl}`) : "";
|
|
5727
5610
|
console.log(` ${p.padEnd(15)} ${modeBadge(setting.mode)}${url}`);
|
|
5728
5611
|
}
|
|
5729
5612
|
});
|
|
5730
|
-
var pillarSet = new
|
|
5613
|
+
var pillarSet = new Command14("set").description("Set a pillar's mode (interactive when setting external-webhook)").argument("<pillar>", "extraction | enrichment | intelligence | implementation").argument("[mode]", "cloud-default | disabled | external-webhook").option("--url <url>", "Webhook URL (for external-webhook mode)").option("--reason <reason>", "Optional reason captured in the audit log").action(
|
|
5731
5614
|
async (pillar, mode, opts) => {
|
|
5732
5615
|
if (!PILLARS.includes(pillar)) {
|
|
5733
|
-
console.error(
|
|
5616
|
+
console.error(chalk14.red(`Unknown pillar "${pillar}".`));
|
|
5734
5617
|
console.error(` Choices: ${PILLARS.join(", ")}`);
|
|
5735
5618
|
process.exit(1);
|
|
5736
5619
|
}
|
|
@@ -5745,7 +5628,7 @@ var pillarSet = new Command15("set").description("Set a pillar's mode (interacti
|
|
|
5745
5628
|
});
|
|
5746
5629
|
}
|
|
5747
5630
|
if (!MODES.includes(chosenMode)) {
|
|
5748
|
-
console.error(
|
|
5631
|
+
console.error(chalk14.red(`Invalid mode "${chosenMode}".`));
|
|
5749
5632
|
console.error(` Choices: ${MODES.join(", ")}`);
|
|
5750
5633
|
process.exit(1);
|
|
5751
5634
|
}
|
|
@@ -5753,7 +5636,7 @@ var pillarSet = new Command15("set").description("Set a pillar's mode (interacti
|
|
|
5753
5636
|
if (chosenMode === "external-webhook") {
|
|
5754
5637
|
const url = opts.url ?? await input4({ message: "Webhook URL:" });
|
|
5755
5638
|
if (!url.trim()) {
|
|
5756
|
-
console.error(
|
|
5639
|
+
console.error(chalk14.red("Webhook URL required."));
|
|
5757
5640
|
process.exit(1);
|
|
5758
5641
|
}
|
|
5759
5642
|
const secret = randomBytes2(32).toString("hex");
|
|
@@ -5769,13 +5652,13 @@ var pillarSet = new Command15("set").description("Set a pillar's mode (interacti
|
|
|
5769
5652
|
}
|
|
5770
5653
|
);
|
|
5771
5654
|
console.log(
|
|
5772
|
-
|
|
5655
|
+
chalk14.green(`Set ${pillar} \u2192 external-webhook in ${projectName}.`)
|
|
5773
5656
|
);
|
|
5774
|
-
console.log(
|
|
5775
|
-
console.log(
|
|
5657
|
+
console.log(chalk14.dim("URL: "), setting.webhookUrl);
|
|
5658
|
+
console.log(chalk14.dim("secret:"), chalk14.yellow(secret));
|
|
5776
5659
|
console.log("");
|
|
5777
5660
|
console.log(
|
|
5778
|
-
|
|
5661
|
+
chalk14.bold("Store this secret now."),
|
|
5779
5662
|
"Yapout encrypts it at rest; this is the only time it's shown. The receiver needs the plaintext to verify HMAC signatures."
|
|
5780
5663
|
);
|
|
5781
5664
|
return;
|
|
@@ -5790,13 +5673,13 @@ var pillarSet = new Command15("set").description("Set a pillar's mode (interacti
|
|
|
5790
5673
|
}
|
|
5791
5674
|
);
|
|
5792
5675
|
console.log(
|
|
5793
|
-
|
|
5676
|
+
chalk14.green(`Set ${pillar} \u2192 ${chosenMode} in ${projectName}.`)
|
|
5794
5677
|
);
|
|
5795
5678
|
}
|
|
5796
5679
|
);
|
|
5797
|
-
var pillarTest = new
|
|
5680
|
+
var pillarTest = new Command14("test").description("Send a test delivery to a pillar's external webhook").argument("<pillar>", "extraction | enrichment | intelligence | implementation").action(async (pillar) => {
|
|
5798
5681
|
if (!PILLARS.includes(pillar)) {
|
|
5799
|
-
console.error(
|
|
5682
|
+
console.error(chalk14.red(`Unknown pillar "${pillar}".`));
|
|
5800
5683
|
process.exit(1);
|
|
5801
5684
|
}
|
|
5802
5685
|
const creds = requireAuth();
|
|
@@ -5809,7 +5692,7 @@ var pillarTest = new Command15("test").description("Send a test delivery to a pi
|
|
|
5809
5692
|
const setting = config?.pillars?.[pillar];
|
|
5810
5693
|
if (!setting || setting.mode !== "external-webhook") {
|
|
5811
5694
|
console.error(
|
|
5812
|
-
|
|
5695
|
+
chalk14.red(
|
|
5813
5696
|
`${pillar} is in "${setting?.mode ?? "cloud-default"}" mode \u2014 set it to external-webhook first via \`yapout pillar set ${pillar} external-webhook\`.`
|
|
5814
5697
|
)
|
|
5815
5698
|
);
|
|
@@ -5817,11 +5700,11 @@ var pillarTest = new Command15("test").description("Send a test delivery to a pi
|
|
|
5817
5700
|
}
|
|
5818
5701
|
if (!setting.webhookUrl || !setting.webhookSecret) {
|
|
5819
5702
|
console.error(
|
|
5820
|
-
|
|
5703
|
+
chalk14.red(`${pillar} is missing webhookUrl or webhookSecret.`)
|
|
5821
5704
|
);
|
|
5822
5705
|
process.exit(1);
|
|
5823
5706
|
}
|
|
5824
|
-
console.log(
|
|
5707
|
+
console.log(chalk14.dim(`POSTing test payload to ${setting.webhookUrl}...`));
|
|
5825
5708
|
const result = await client.action(
|
|
5826
5709
|
anyApi.functions.pillarHooks.testDelivery,
|
|
5827
5710
|
{
|
|
@@ -5831,25 +5714,25 @@ var pillarTest = new Command15("test").description("Send a test delivery to a pi
|
|
|
5831
5714
|
}
|
|
5832
5715
|
);
|
|
5833
5716
|
if (result.ok) {
|
|
5834
|
-
console.log(
|
|
5717
|
+
console.log(chalk14.green(`\u2713 HTTP ${result.httpStatus} \u2014 receiver returned 2xx`));
|
|
5835
5718
|
} else {
|
|
5836
|
-
console.log(
|
|
5837
|
-
if (result.error) console.log(
|
|
5719
|
+
console.log(chalk14.red(`\u26A0 HTTP ${result.httpStatus ?? "n/a"}`));
|
|
5720
|
+
if (result.error) console.log(chalk14.dim(` error: ${result.error}`));
|
|
5838
5721
|
}
|
|
5839
5722
|
if (result.body) {
|
|
5840
|
-
console.log(
|
|
5723
|
+
console.log(chalk14.dim(" body:"));
|
|
5841
5724
|
console.log(
|
|
5842
5725
|
result.body.split("\n").map((l) => ` ${l}`).join("\n")
|
|
5843
5726
|
);
|
|
5844
5727
|
}
|
|
5845
5728
|
});
|
|
5846
|
-
var pillarCommand = new
|
|
5729
|
+
var pillarCommand = new Command14("pillar").description("Manage per-project pillar overrides (extraction / enrichment / intelligence / implementation)").addCommand(pillarList).addCommand(pillarSet).addCommand(pillarTest);
|
|
5847
5730
|
|
|
5848
5731
|
// src/commands/observe.ts
|
|
5849
|
-
import { Command as
|
|
5850
|
-
import { resolve as
|
|
5851
|
-
import
|
|
5852
|
-
var observeCommand = new
|
|
5732
|
+
import { Command as Command15 } from "commander";
|
|
5733
|
+
import { resolve as resolve13 } from "path";
|
|
5734
|
+
import chalk15 from "chalk";
|
|
5735
|
+
var observeCommand = new Command15("observe").description("File an observation Raw against the active Execution").argument("<observation...>", "Freeform observation text").option(
|
|
5853
5736
|
"--severity <level>",
|
|
5854
5737
|
"Severity hint: low | normal | high (default: normal)"
|
|
5855
5738
|
).option(
|
|
@@ -5858,30 +5741,30 @@ var observeCommand = new Command16("observe").description("File an observation R
|
|
|
5858
5741
|
).action(
|
|
5859
5742
|
async (words, options) => {
|
|
5860
5743
|
const creds = requireAuth();
|
|
5861
|
-
const cwd =
|
|
5744
|
+
const cwd = resolve13(process.cwd());
|
|
5862
5745
|
const resourceCfg = readResourceConfig(cwd);
|
|
5863
5746
|
if (!resourceCfg) {
|
|
5864
5747
|
console.error(
|
|
5865
|
-
|
|
5748
|
+
chalk15.red("No `.yapout/config.json` found.") + " Run " + chalk15.cyan("yapout init") + " in this repo first."
|
|
5866
5749
|
);
|
|
5867
5750
|
process.exit(1);
|
|
5868
5751
|
}
|
|
5869
5752
|
const active = readActiveExecution(cwd);
|
|
5870
5753
|
if (!active) {
|
|
5871
5754
|
console.error(
|
|
5872
|
-
|
|
5755
|
+
chalk15.red("No active Execution.") + " `yapout observe` must run from a worktree where " + chalk15.cyan("yapout pick") + " has been called."
|
|
5873
5756
|
);
|
|
5874
5757
|
process.exit(1);
|
|
5875
5758
|
}
|
|
5876
5759
|
const body = words.join(" ").trim();
|
|
5877
5760
|
if (!body) {
|
|
5878
|
-
console.error(
|
|
5761
|
+
console.error(chalk15.red("Observation body cannot be empty."));
|
|
5879
5762
|
process.exit(1);
|
|
5880
5763
|
}
|
|
5881
5764
|
const severityHint = parseSeverity(options.severity);
|
|
5882
5765
|
if (options.severity && !severityHint) {
|
|
5883
5766
|
console.error(
|
|
5884
|
-
|
|
5767
|
+
chalk15.red(
|
|
5885
5768
|
`Invalid --severity: ${options.severity}. Allowed: low, normal, high.`
|
|
5886
5769
|
)
|
|
5887
5770
|
);
|
|
@@ -5901,18 +5784,18 @@ var observeCommand = new Command16("observe").description("File an observation R
|
|
|
5901
5784
|
);
|
|
5902
5785
|
} catch (err) {
|
|
5903
5786
|
console.error(
|
|
5904
|
-
|
|
5787
|
+
chalk15.red("Failed to file observation."),
|
|
5905
5788
|
err.message
|
|
5906
5789
|
);
|
|
5907
5790
|
process.exit(1);
|
|
5908
5791
|
}
|
|
5909
5792
|
console.log(
|
|
5910
|
-
|
|
5793
|
+
chalk15.green("Filed agent observation. ") + chalk15.dim(
|
|
5911
5794
|
`(raw: ${result.rawId}, execution: ${result.executionId})`
|
|
5912
5795
|
)
|
|
5913
5796
|
);
|
|
5914
5797
|
console.log(
|
|
5915
|
-
|
|
5798
|
+
chalk15.dim(
|
|
5916
5799
|
"Extraction will turn this into a Request (or link it to an existing one)."
|
|
5917
5800
|
)
|
|
5918
5801
|
);
|
|
@@ -5925,9 +5808,145 @@ function parseSeverity(raw) {
|
|
|
5925
5808
|
return void 0;
|
|
5926
5809
|
}
|
|
5927
5810
|
|
|
5811
|
+
// src/commands/pick.ts
|
|
5812
|
+
import { Command as Command16 } from "commander";
|
|
5813
|
+
import { resolve as resolve14 } from "path";
|
|
5814
|
+
import chalk16 from "chalk";
|
|
5815
|
+
var pickCommand = new Command16("pick").description(
|
|
5816
|
+
"Claim a queued Directive: create worktree + start an Execution"
|
|
5817
|
+
).argument("<directive-id>", "Convex `directives._id` to pick").option(
|
|
5818
|
+
"--branch <name>",
|
|
5819
|
+
"Override the branch name (default: directive.branchName or `directive/<short-id>`)"
|
|
5820
|
+
).option(
|
|
5821
|
+
"--agent <name>",
|
|
5822
|
+
"Implementer identity tag (default: 'claude')"
|
|
5823
|
+
).action(
|
|
5824
|
+
async (directiveId, options) => {
|
|
5825
|
+
const creds = requireAuth();
|
|
5826
|
+
const cwd = resolve14(process.cwd());
|
|
5827
|
+
const repoRoot = resolveRepoRoot(cwd);
|
|
5828
|
+
const resourceCfg = readResourceConfig(repoRoot);
|
|
5829
|
+
if (!resourceCfg) {
|
|
5830
|
+
console.error(
|
|
5831
|
+
chalk16.red("No `.yapout/config.json` found.") + " Run " + chalk16.cyan("yapout init") + " in this repo first."
|
|
5832
|
+
);
|
|
5833
|
+
process.exit(1);
|
|
5834
|
+
}
|
|
5835
|
+
const yapoutCfg = readYapoutConfig(repoRoot);
|
|
5836
|
+
const baseBranch = resourceCfg.defaultBranch ?? (() => {
|
|
5837
|
+
try {
|
|
5838
|
+
return getDefaultBranch(repoRoot);
|
|
5839
|
+
} catch {
|
|
5840
|
+
return "main";
|
|
5841
|
+
}
|
|
5842
|
+
})();
|
|
5843
|
+
const client = createConvexClient(creds.token);
|
|
5844
|
+
const shortId = directiveId.slice(-6);
|
|
5845
|
+
const branchName = options.branch ?? `${yapoutCfg.branch_prefix}/directive-${shortId}`;
|
|
5846
|
+
const worktreePath = getWorktreePath(repoRoot, `directive-${shortId}`);
|
|
5847
|
+
let result;
|
|
5848
|
+
try {
|
|
5849
|
+
result = await client.mutation(
|
|
5850
|
+
anyApi.functions.resourcesCli.createExecutionForDirective,
|
|
5851
|
+
{
|
|
5852
|
+
directiveId,
|
|
5853
|
+
worktreePath,
|
|
5854
|
+
branch: branchName,
|
|
5855
|
+
agent: options.agent ?? "claude"
|
|
5856
|
+
}
|
|
5857
|
+
);
|
|
5858
|
+
} catch (err) {
|
|
5859
|
+
console.error(
|
|
5860
|
+
chalk16.red("Failed to pick Directive."),
|
|
5861
|
+
err.message
|
|
5862
|
+
);
|
|
5863
|
+
process.exit(1);
|
|
5864
|
+
}
|
|
5865
|
+
try {
|
|
5866
|
+
createWorktree(repoRoot, `directive-${shortId}`, branchName, baseBranch);
|
|
5867
|
+
} catch (err) {
|
|
5868
|
+
console.error(
|
|
5869
|
+
chalk16.red("Server-side pick succeeded but worktree creation failed.")
|
|
5870
|
+
);
|
|
5871
|
+
console.error(chalk16.dim(` ${err.message}`));
|
|
5872
|
+
console.error(
|
|
5873
|
+
chalk16.yellow("Recovery: ") + "the Directive is now `in_progress` and an Execution row exists. Either resolve the worktree issue and re-run with the same id, or flip the Directive back to `queued` from the dashboard."
|
|
5874
|
+
);
|
|
5875
|
+
console.error(chalk16.dim(` executionId: ${result.executionId}`));
|
|
5876
|
+
process.exit(1);
|
|
5877
|
+
}
|
|
5878
|
+
writeActiveExecution(worktreePath, {
|
|
5879
|
+
executionId: result.executionId,
|
|
5880
|
+
directiveId: result.directiveId,
|
|
5881
|
+
branch: branchName,
|
|
5882
|
+
worktreePath,
|
|
5883
|
+
startedAt: Date.now()
|
|
5884
|
+
});
|
|
5885
|
+
console.log(
|
|
5886
|
+
chalk16.green("Picked ") + chalk16.bold(result.directiveTitle) + chalk16.dim(` (id: ${result.directiveId}).`)
|
|
5887
|
+
);
|
|
5888
|
+
console.log(
|
|
5889
|
+
chalk16.dim(" worktree: ") + chalk16.cyan(worktreePath)
|
|
5890
|
+
);
|
|
5891
|
+
console.log(chalk16.dim(" branch: ") + chalk16.cyan(branchName));
|
|
5892
|
+
console.log(chalk16.dim(" execution: ") + chalk16.cyan(result.executionId));
|
|
5893
|
+
console.log();
|
|
5894
|
+
console.log(
|
|
5895
|
+
chalk16.dim("Next: ") + "cd into the worktree and start work. Use " + chalk16.cyan("yapout observe") + " to file tangential observations and " + chalk16.cyan("yapout submit") + " when ready for review."
|
|
5896
|
+
);
|
|
5897
|
+
}
|
|
5898
|
+
);
|
|
5899
|
+
|
|
5900
|
+
// src/commands/submit.ts
|
|
5901
|
+
import { Command as Command17 } from "commander";
|
|
5902
|
+
import { resolve as resolve15 } from "path";
|
|
5903
|
+
import chalk17 from "chalk";
|
|
5904
|
+
var submitCommand = new Command17("submit").description(
|
|
5905
|
+
"Mark the active Execution as submitted and move the Directive to review"
|
|
5906
|
+
).action(async () => {
|
|
5907
|
+
const creds = requireAuth();
|
|
5908
|
+
const cwd = resolve15(process.cwd());
|
|
5909
|
+
const repoRoot = resolveRepoRoot(cwd);
|
|
5910
|
+
const resourceCfg = readResourceConfig(repoRoot);
|
|
5911
|
+
if (!resourceCfg) {
|
|
5912
|
+
console.error(
|
|
5913
|
+
chalk17.red("No `.yapout/config.json` found.") + " Run " + chalk17.cyan("yapout init") + " in this repo first."
|
|
5914
|
+
);
|
|
5915
|
+
process.exit(1);
|
|
5916
|
+
}
|
|
5917
|
+
const active = readActiveExecution(cwd);
|
|
5918
|
+
if (!active) {
|
|
5919
|
+
console.error(
|
|
5920
|
+
chalk17.red("No active Execution.") + " `yapout submit` must run from a worktree where " + chalk17.cyan("yapout pick") + " has been called."
|
|
5921
|
+
);
|
|
5922
|
+
process.exit(1);
|
|
5923
|
+
}
|
|
5924
|
+
const client = createConvexClient(creds.token);
|
|
5925
|
+
let result;
|
|
5926
|
+
try {
|
|
5927
|
+
result = await client.mutation(
|
|
5928
|
+
anyApi.functions.resourcesCli.submitExecution,
|
|
5929
|
+
{ executionId: active.executionId }
|
|
5930
|
+
);
|
|
5931
|
+
} catch (err) {
|
|
5932
|
+
console.error(
|
|
5933
|
+
chalk17.red("Failed to submit Execution."),
|
|
5934
|
+
err.message
|
|
5935
|
+
);
|
|
5936
|
+
process.exit(1);
|
|
5937
|
+
}
|
|
5938
|
+
clearActiveExecution(cwd);
|
|
5939
|
+
console.log(
|
|
5940
|
+
chalk17.green("Submitted Execution. ") + chalk17.dim(`(execution: ${result.executionId})`)
|
|
5941
|
+
);
|
|
5942
|
+
console.log(
|
|
5943
|
+
chalk17.dim("Directive ") + chalk17.cyan(result.directiveId) + chalk17.dim(" is now in `review`.")
|
|
5944
|
+
);
|
|
5945
|
+
});
|
|
5946
|
+
|
|
5928
5947
|
// src/index.ts
|
|
5929
|
-
var program = new
|
|
5930
|
-
program.name("yapout").description("yapout \u2014 the PM tool for agents").version("0.
|
|
5948
|
+
var program = new Command18();
|
|
5949
|
+
program.name("yapout").description("yapout \u2014 the PM tool for agents").version("0.18.0");
|
|
5931
5950
|
program.addCommand(loginCommand);
|
|
5932
5951
|
program.addCommand(logoutCommand);
|
|
5933
5952
|
program.addCommand(initCommand);
|
|
@@ -5937,11 +5956,12 @@ program.addCommand(statusCommand);
|
|
|
5937
5956
|
program.addCommand(mcpServerCommand);
|
|
5938
5957
|
program.addCommand(queueCommand);
|
|
5939
5958
|
program.addCommand(recapCommand);
|
|
5940
|
-
program.addCommand(yapCommand);
|
|
5941
5959
|
program.addCommand(serveCommand);
|
|
5942
5960
|
program.addCommand(agentCommand);
|
|
5943
5961
|
program.addCommand(webhookCommand);
|
|
5944
5962
|
program.addCommand(inboundCommand);
|
|
5945
5963
|
program.addCommand(pillarCommand);
|
|
5946
5964
|
program.addCommand(observeCommand);
|
|
5965
|
+
program.addCommand(pickCommand);
|
|
5966
|
+
program.addCommand(submitCommand);
|
|
5947
5967
|
program.parse();
|