yapout 0.4.1 → 0.5.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.
Files changed (2) hide show
  1. package/dist/index.js +132 -117
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -12,8 +12,11 @@ import { Command as Command16 } from "commander";
12
12
  // src/commands/login.ts
13
13
  import { Command } from "commander";
14
14
  import http from "http";
15
+ import { createHash } from "crypto";
16
+ import { hostname, userInfo } from "os";
15
17
  import chalk from "chalk";
16
18
  import open from "open";
19
+ import { anyApi as anyApi2 } from "convex/server";
17
20
 
18
21
  // src/lib/config.ts
19
22
  import { homedir } from "os";
@@ -165,6 +168,16 @@ function getAppUrl() {
165
168
  return process.env.YAPOUT_APP_URL || "https://yapout.vercel.app";
166
169
  }
167
170
 
171
+ // src/lib/convex.ts
172
+ import { ConvexHttpClient } from "convex/browser";
173
+ import { anyApi } from "convex/server";
174
+ function createConvexClient(token) {
175
+ const url = getConvexUrl();
176
+ const client = new ConvexHttpClient(url);
177
+ client.setAuth(token);
178
+ return client;
179
+ }
180
+
168
181
  // src/lib/protocol.ts
169
182
  import { execSync, spawnSync } from "child_process";
170
183
  import { platform, homedir as homedir2 } from "os";
@@ -413,6 +426,7 @@ function registerProtocolHandler() {
413
426
  }
414
427
 
415
428
  // src/commands/login.ts
429
+ var CLI_VERSION = "0.5.0";
416
430
  function startCallbackServer() {
417
431
  return new Promise((resolve11) => {
418
432
  let resolveData;
@@ -493,6 +507,17 @@ var loginCommand = new Command("login").description("Authenticate with yapout").
493
507
  ` (expires in ${daysLeft} day${daysLeft === 1 ? "" : "s"})`
494
508
  )
495
509
  );
510
+ try {
511
+ const client = createConvexClient(creds.token);
512
+ const deviceId = createHash("sha256").update(hostname() + userInfo().username).digest("hex").slice(0, 16);
513
+ await client.mutation(anyApi2.functions.devices.registerDevice, {
514
+ deviceId,
515
+ name: hostname(),
516
+ cliVersion: CLI_VERSION
517
+ });
518
+ } catch {
519
+ console.warn(chalk.dim("Note: Could not register device. This is non-fatal."));
520
+ }
496
521
  try {
497
522
  registerProtocolHandler();
498
523
  console.log(
@@ -546,16 +571,6 @@ function requireAuth() {
546
571
  return creds;
547
572
  }
548
573
 
549
- // src/lib/convex.ts
550
- import { ConvexHttpClient } from "convex/browser";
551
- import { anyApi } from "convex/server";
552
- function createConvexClient(token) {
553
- const url = getConvexUrl();
554
- const client = new ConvexHttpClient(url);
555
- client.setAuth(token);
556
- return client;
557
- }
558
-
559
574
  // src/lib/prompts.ts
560
575
  import { select } from "@inquirer/prompts";
561
576
  async function pickProject(projects) {
@@ -916,7 +931,7 @@ import { Command as Command7 } from "commander";
916
931
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
917
932
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
918
933
  import { ConvexHttpClient as ConvexHttpClient2 } from "convex/browser";
919
- import { anyApi as anyApi2 } from "convex/server";
934
+ import { anyApi as anyApi3 } from "convex/server";
920
935
 
921
936
  // src/mcp/tools/init.ts
922
937
  import { z } from "zod";
@@ -936,7 +951,7 @@ function registerInitTool(server, ctx) {
936
951
  const defaultBranch = getDefaultBranch(ctx.cwd);
937
952
  const projectName = args.name || repoFullName.split("/")[1] || "unnamed";
938
953
  const result = await ctx.client.mutation(
939
- anyApi2.functions.projects.createProjectFromCli,
954
+ anyApi3.functions.projects.createProjectFromCli,
940
955
  {
941
956
  name: projectName,
942
957
  githubRepoFullName: repoFullName,
@@ -1024,7 +1039,7 @@ function registerCompactTool(server, ctx) {
1024
1039
  let lastUpdated;
1025
1040
  try {
1026
1041
  const project = await ctx.client.query(
1027
- anyApi2.functions.projects.getProject,
1042
+ anyApi3.functions.projects.getProject,
1028
1043
  { projectId: ctx.projectId }
1029
1044
  );
1030
1045
  currentContext = project?.contextSummary ?? void 0;
@@ -1067,7 +1082,7 @@ function registerUpdateContextTool(server, ctx) {
1067
1082
  };
1068
1083
  }
1069
1084
  await ctx.client.mutation(
1070
- anyApi2.functions.projects.updateProjectContext,
1085
+ anyApi3.functions.projects.updateProjectContext,
1071
1086
  {
1072
1087
  projectId: ctx.projectId,
1073
1088
  summary: args.summary
@@ -1137,7 +1152,7 @@ function registerQueueTool(server, ctx) {
1137
1152
  };
1138
1153
  }
1139
1154
  const data = await ctx.client.query(
1140
- anyApi2.functions.workQueue.getWorkQueue,
1155
+ anyApi3.functions.workQueue.getWorkQueue,
1141
1156
  { projectId: ctx.projectId }
1142
1157
  );
1143
1158
  if (!data) {
@@ -1246,7 +1261,7 @@ function registerGetBriefTool(server, ctx) {
1246
1261
  }
1247
1262
  try {
1248
1263
  const data = await ctx.client.query(
1249
- anyApi2.functions.findings.getFindingBrief,
1264
+ anyApi3.functions.findings.getFindingBrief,
1250
1265
  { findingId: itemId }
1251
1266
  );
1252
1267
  if (data) {
@@ -1260,7 +1275,7 @@ function registerGetBriefTool(server, ctx) {
1260
1275
  }
1261
1276
  try {
1262
1277
  const bundle = await ctx.client.query(
1263
- anyApi2.functions.bundles.getBundle,
1278
+ anyApi3.functions.bundles.getBundle,
1264
1279
  { bundleId: itemId }
1265
1280
  );
1266
1281
  if (bundle) {
@@ -1431,7 +1446,7 @@ function formatBundleBrief(bundle, projectContext) {
1431
1446
  async function detectWorkItemKind(client, workItemId) {
1432
1447
  try {
1433
1448
  const bundle = await client.query(
1434
- anyApi2.functions.bundles.getBundle,
1449
+ anyApi3.functions.bundles.getBundle,
1435
1450
  { bundleId: workItemId }
1436
1451
  );
1437
1452
  if (bundle) {
@@ -1485,7 +1500,7 @@ function registerClaimTool(server, ctx) {
1485
1500
  }
1486
1501
  async function claimStandalone(ctx, args, findingId) {
1487
1502
  const briefData = await ctx.client.query(
1488
- anyApi2.functions.findings.getFindingBrief,
1503
+ anyApi3.functions.findings.getFindingBrief,
1489
1504
  { findingId }
1490
1505
  );
1491
1506
  if (!briefData) {
@@ -1506,11 +1521,11 @@ async function claimStandalone(ctx, args, findingId) {
1506
1521
  const slug = slugify(finding.title);
1507
1522
  const branchName = linearIssueId ? `${prefix}/${linearIssueId.toLowerCase()}-${slug}` : `${prefix}/${slug}`;
1508
1523
  const localClaim = await ctx.client.mutation(
1509
- anyApi2.functions.findings.claimFindingLocal,
1524
+ anyApi3.functions.findings.claimFindingLocal,
1510
1525
  { findingId, branchName }
1511
1526
  );
1512
1527
  const claim = await ctx.client.mutation(
1513
- anyApi2.functions.workQueue.claimForImplementation,
1528
+ anyApi3.functions.workQueue.claimForImplementation,
1514
1529
  {
1515
1530
  projectId: ctx.projectId,
1516
1531
  workItemId: findingId,
@@ -1520,7 +1535,7 @@ async function claimStandalone(ctx, args, findingId) {
1520
1535
  if (linearIssueId && ctx.projectId) {
1521
1536
  try {
1522
1537
  await ctx.client.action(
1523
- anyApi2.functions.linearStatusMutations.moveIssueStatus,
1538
+ anyApi3.functions.linearStatusMutations.moveIssueStatus,
1524
1539
  {
1525
1540
  projectId: ctx.projectId,
1526
1541
  linearIssueId,
@@ -1577,11 +1592,11 @@ async function claimBundle(ctx, args, bundleId, bundleData) {
1577
1592
  };
1578
1593
  }
1579
1594
  const localClaim = await ctx.client.mutation(
1580
- anyApi2.functions.findings.claimFindingLocal,
1595
+ anyApi3.functions.findings.claimFindingLocal,
1581
1596
  { findingId: primaryFinding._id, branchName }
1582
1597
  );
1583
1598
  const claim = await ctx.client.mutation(
1584
- anyApi2.functions.workQueue.claimForImplementation,
1599
+ anyApi3.functions.workQueue.claimForImplementation,
1585
1600
  {
1586
1601
  projectId: ctx.projectId,
1587
1602
  workItemId: bundleId,
@@ -1592,7 +1607,7 @@ async function claimBundle(ctx, args, bundleId, bundleData) {
1592
1607
  if (f.linearIssueId && ctx.projectId) {
1593
1608
  try {
1594
1609
  await ctx.client.action(
1595
- anyApi2.functions.linearStatusMutations.moveIssueStatus,
1610
+ anyApi3.functions.linearStatusMutations.moveIssueStatus,
1596
1611
  {
1597
1612
  projectId: ctx.projectId,
1598
1613
  linearIssueId: f.linearIssueId,
@@ -1606,7 +1621,7 @@ async function claimBundle(ctx, args, bundleId, bundleData) {
1606
1621
  let projectContext;
1607
1622
  try {
1608
1623
  const briefData = await ctx.client.query(
1609
- anyApi2.functions.findings.getFindingBrief,
1624
+ anyApi3.functions.findings.getFindingBrief,
1610
1625
  { findingId: primaryFinding._id }
1611
1626
  );
1612
1627
  projectContext = briefData?.projectContext;
@@ -1646,7 +1661,7 @@ async function claimBundle(ctx, args, bundleId, bundleData) {
1646
1661
  async function getDefaultBranchForProject(ctx) {
1647
1662
  try {
1648
1663
  const data = await ctx.client.query(
1649
- anyApi2.functions.projects.getProject,
1664
+ anyApi3.functions.projects.getProject,
1650
1665
  { projectId: ctx.projectId }
1651
1666
  );
1652
1667
  return data?.githubDefaultBranch || "main";
@@ -1665,7 +1680,7 @@ async function setupWorktree(ctx, itemId, branchName, defaultBranch, brief, pipe
1665
1680
  writeBrief(worktreePath, brief);
1666
1681
  try {
1667
1682
  await ctx.client.mutation(
1668
- anyApi2.functions.pipelineRuns.reportDaemonEvent,
1683
+ anyApi3.functions.pipelineRuns.reportDaemonEvent,
1669
1684
  {
1670
1685
  pipelineRunId,
1671
1686
  event: "worktree_created",
@@ -1701,7 +1716,7 @@ function writeBrief(dir, brief) {
1701
1716
  async function reportClaimEvents(ctx, pipelineRunId, title, branchName) {
1702
1717
  try {
1703
1718
  await ctx.client.mutation(
1704
- anyApi2.functions.pipelineRuns.reportDaemonEvent,
1719
+ anyApi3.functions.pipelineRuns.reportDaemonEvent,
1705
1720
  {
1706
1721
  pipelineRunId,
1707
1722
  event: "daemon_claimed",
@@ -1709,7 +1724,7 @@ async function reportClaimEvents(ctx, pipelineRunId, title, branchName) {
1709
1724
  }
1710
1725
  );
1711
1726
  await ctx.client.mutation(
1712
- anyApi2.functions.pipelineRuns.reportDaemonEvent,
1727
+ anyApi3.functions.pipelineRuns.reportDaemonEvent,
1713
1728
  {
1714
1729
  pipelineRunId,
1715
1730
  event: "branch_created",
@@ -1736,7 +1751,7 @@ function registerEventTool(server, ctx) {
1736
1751
  async (args) => {
1737
1752
  try {
1738
1753
  await ctx.client.mutation(
1739
- anyApi2.functions.pipelineRuns.reportDaemonEvent,
1754
+ anyApi3.functions.pipelineRuns.reportDaemonEvent,
1740
1755
  {
1741
1756
  pipelineRunId: args.pipelineRunId,
1742
1757
  event: args.event,
@@ -1993,7 +2008,7 @@ function registerShipTool(server, ctx) {
1993
2008
  }
1994
2009
  try {
1995
2010
  await ctx.client.mutation(
1996
- anyApi2.functions.pipelineRuns.reportDaemonEvent,
2011
+ anyApi3.functions.pipelineRuns.reportDaemonEvent,
1997
2012
  {
1998
2013
  pipelineRunId: args.pipelineRunId,
1999
2014
  event: "push_completed",
@@ -2002,7 +2017,7 @@ function registerShipTool(server, ctx) {
2002
2017
  );
2003
2018
  if (prUrl) {
2004
2019
  await ctx.client.mutation(
2005
- anyApi2.functions.pipelineRuns.reportDaemonEvent,
2020
+ anyApi3.functions.pipelineRuns.reportDaemonEvent,
2006
2021
  {
2007
2022
  pipelineRunId: args.pipelineRunId,
2008
2023
  event: "pr_opened",
@@ -2014,7 +2029,7 @@ function registerShipTool(server, ctx) {
2014
2029
  }
2015
2030
  try {
2016
2031
  await ctx.client.mutation(
2017
- anyApi2.functions.pipelineRuns.completePipelineLocal,
2032
+ anyApi3.functions.pipelineRuns.completePipelineLocal,
2018
2033
  {
2019
2034
  pipelineRunId: args.pipelineRunId,
2020
2035
  githubPrNumber: prNumber,
@@ -2030,7 +2045,7 @@ function registerShipTool(server, ctx) {
2030
2045
  if (ctx.projectId) {
2031
2046
  try {
2032
2047
  await ctx.client.action(
2033
- anyApi2.functions.linearStatusMutations.moveIssueStatus,
2048
+ anyApi3.functions.linearStatusMutations.moveIssueStatus,
2034
2049
  {
2035
2050
  projectId: ctx.projectId,
2036
2051
  linearIssueId: linearId,
@@ -2042,7 +2057,7 @@ function registerShipTool(server, ctx) {
2042
2057
  if (prUrl) {
2043
2058
  try {
2044
2059
  await ctx.client.action(
2045
- anyApi2.functions.linearStatusMutations.addLinearComment,
2060
+ anyApi3.functions.linearStatusMutations.addLinearComment,
2046
2061
  {
2047
2062
  projectId: ctx.projectId,
2048
2063
  linearIssueId: linearId,
@@ -2060,7 +2075,7 @@ function registerShipTool(server, ctx) {
2060
2075
  result.worktreeCleaned = true;
2061
2076
  try {
2062
2077
  await ctx.client.mutation(
2063
- anyApi2.functions.pipelineRuns.reportDaemonEvent,
2078
+ anyApi3.functions.pipelineRuns.reportDaemonEvent,
2064
2079
  {
2065
2080
  pipelineRunId: args.pipelineRunId,
2066
2081
  event: "worktree_cleaned",
@@ -2151,7 +2166,7 @@ function registerCheckTool(server, ctx) {
2151
2166
  if (args.pipelineRunId) {
2152
2167
  try {
2153
2168
  await ctx.client.mutation(
2154
- anyApi2.functions.pipelineRuns.reportDaemonEvent,
2169
+ anyApi3.functions.pipelineRuns.reportDaemonEvent,
2155
2170
  {
2156
2171
  pipelineRunId: args.pipelineRunId,
2157
2172
  event: "running_check",
@@ -2171,7 +2186,7 @@ function registerCheckTool(server, ctx) {
2171
2186
  if (args.pipelineRunId) {
2172
2187
  try {
2173
2188
  await ctx.client.mutation(
2174
- anyApi2.functions.pipelineRuns.reportDaemonEvent,
2189
+ anyApi3.functions.pipelineRuns.reportDaemonEvent,
2175
2190
  {
2176
2191
  pipelineRunId: args.pipelineRunId,
2177
2192
  event: passed ? "check_passed" : "check_failed",
@@ -2215,11 +2230,11 @@ function registerBundleTool(server, ctx) {
2215
2230
  },
2216
2231
  async (args) => {
2217
2232
  const result = await ctx.client.mutation(
2218
- anyApi2.functions.bundles.createBundle,
2233
+ anyApi3.functions.bundles.createBundle,
2219
2234
  { leadFindingId: args.withFinding, joiningFindingId: args.findingId }
2220
2235
  );
2221
2236
  const bundledBrief = await ctx.client.query(
2222
- anyApi2.functions.bundles.getBundledBrief,
2237
+ anyApi3.functions.bundles.getBundledBrief,
2223
2238
  { bundleId: result.bundleId }
2224
2239
  );
2225
2240
  if (!bundledBrief) {
@@ -2316,7 +2331,7 @@ After calling this tool, you should:
2316
2331
  }
2317
2332
  try {
2318
2333
  const result = await ctx.client.mutation(
2319
- anyApi2.functions.localPipeline.claimForEnrichment,
2334
+ anyApi3.functions.localPipeline.claimForEnrichment,
2320
2335
  {
2321
2336
  projectId,
2322
2337
  ...args.findingId ? { findingId: args.findingId } : {}
@@ -2376,7 +2391,7 @@ function registerGetExistingFindingsTool(server, ctx) {
2376
2391
  }
2377
2392
  try {
2378
2393
  const findings = await ctx.client.query(
2379
- anyApi2.functions.localPipeline.getExistingFindingTitles,
2394
+ anyApi3.functions.localPipeline.getExistingFindingTitles,
2380
2395
  { projectId }
2381
2396
  );
2382
2397
  if (!findings || findings.length === 0) {
@@ -2471,14 +2486,13 @@ The finding transitions: enriching \u2192 enriched \u2192 ready.`,
2471
2486
  ).optional().describe("Only meaningful Q&A from the conversation \u2014 deviations from expected scope, scoping decisions, etc. Omit if you had no questions."),
2472
2487
  isOversized: z11.boolean().optional().describe("Set to true if this finding is too large for a single PR"),
2473
2488
  suggestedSplit: z11.array(z11.string()).optional().describe("If oversized: suggested sub-finding titles for breaking it down"),
2474
- level: z11.enum(["project", "issue"]).optional().describe("Override the finding's level if enrichment reveals it should be reclassified"),
2475
2489
  nature: z11.enum(["implementable", "operational", "spike"]).optional().describe("Override the finding's nature if enrichment reveals it should be reclassified"),
2476
2490
  sessionId: z11.string().optional().describe("Bulk enrichment session ID (from yapout_start_enrichment). Updates session stats.")
2477
2491
  },
2478
2492
  async (args) => {
2479
2493
  try {
2480
2494
  await ctx.client.mutation(
2481
- anyApi2.functions.localPipeline.saveLocalEnrichment,
2495
+ anyApi3.functions.localPipeline.saveLocalEnrichment,
2482
2496
  {
2483
2497
  findingId: args.findingId,
2484
2498
  title: args.title,
@@ -2488,16 +2502,15 @@ The finding transitions: enriching \u2192 enriched \u2192 ready.`,
2488
2502
  clarifications: args.clarifications,
2489
2503
  isOversized: args.isOversized,
2490
2504
  suggestedSplit: args.suggestedSplit,
2491
- level: args.level,
2492
2505
  nature: args.nature
2493
2506
  }
2494
2507
  );
2495
2508
  await ctx.client.action(
2496
- anyApi2.functions.localPipeline.syncFindingToLinearLocal,
2509
+ anyApi3.functions.localPipeline.syncFindingToLinearLocal,
2497
2510
  { findingId: args.findingId }
2498
2511
  );
2499
2512
  const finding = await ctx.client.query(
2500
- anyApi2.functions.findings.getFinding,
2513
+ anyApi3.functions.findings.getFinding,
2501
2514
  { findingId: args.findingId }
2502
2515
  );
2503
2516
  if (args.sessionId) {
@@ -2556,7 +2569,7 @@ function registerSyncToLinearTool(server, ctx) {
2556
2569
  async (args) => {
2557
2570
  try {
2558
2571
  await ctx.client.action(
2559
- anyApi2.functions.localPipeline.syncFindingToLinearLocal,
2572
+ anyApi3.functions.localPipeline.syncFindingToLinearLocal,
2560
2573
  { findingId: args.findingId }
2561
2574
  );
2562
2575
  return {
@@ -2614,7 +2627,7 @@ function registerSubmitYapSessionTool(server, ctx) {
2614
2627
  }
2615
2628
  try {
2616
2629
  const captureId = await ctx.client.mutation(
2617
- anyApi2.functions.captures.createFromYapSession,
2630
+ anyApi3.functions.captures.createFromYapSession,
2618
2631
  {
2619
2632
  projectId: ctx.projectId,
2620
2633
  title: args.title,
@@ -2748,11 +2761,11 @@ Mark a finding as NOT ENRICHED (isEnriched: false or omitted) when:
2748
2761
  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.
2749
2762
 
2750
2763
  PRESENTING THE FINAL PICTURE:
2751
- When the conversation feels complete, present a summary grouped by project:
2764
+ When the conversation feels complete, present a summary grouped by bundles and standalone issues:
2752
2765
 
2753
2766
  "Here's what I've gathered from our conversation:
2754
2767
 
2755
- **[Project Name]** \u2014 [one-line description]
2768
+ **[Bundle Name]** \u2014 [one-line description]
2756
2769
  1. [Child issue] \u2014 enriched \u2713
2757
2770
  2. [Child issue] \u2014 enriched \u2713
2758
2771
  3. [Child issue] \u2014 needs enrichment (we didn't discuss scope)
@@ -2768,7 +2781,7 @@ SUBMITTING:
2768
2781
  On confirmation, call yapout_extract_from_yap with the full data.
2769
2782
 
2770
2783
  For each finding, provide:
2771
- - title, description, sourceQuote, type, priority, confidence, level, nature
2784
+ - title, description, sourceQuote, type, priority, confidence, nature
2772
2785
  - isEnriched: your honest assessment (see above)
2773
2786
 
2774
2787
  For ENRICHED findings (isEnriched: true), also include:
@@ -2777,8 +2790,8 @@ For ENRICHED findings (isEnriched: true), also include:
2777
2790
  - 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.
2778
2791
  - clarifications: relevant Q&A from the conversation that shaped the finding
2779
2792
 
2780
- For PROJECT-level findings, also include:
2781
- - projectDescription: what this body of work accomplishes
2793
+ For findings with CHILDREN (bundles), also include:
2794
+ - bundleDescription: what this body of work accomplishes
2782
2795
  - suggestedOrder: implementation sequencing ("Phase 1: A, then Phase 2: B+C")
2783
2796
  - children: array of child issues (each with their own enrichment data)
2784
2797
 
@@ -2786,16 +2799,16 @@ For children with DEPENDENCIES, include:
2786
2799
  - dependsOn: array of sibling indices (0-based) that must complete first
2787
2800
 
2788
2801
  Structure:
2789
- - Projects include children inline \u2014 never create child issues as separate top-level findings
2802
+ - Findings with children become bundles \u2014 never create child issues as separate top-level findings
2790
2803
  - Standalone issues go at the top level
2791
2804
  - The hierarchy you produce should be the final structure
2792
2805
 
2793
2806
  Call yapout_extract_from_yap with:
2794
2807
  - sessionTitle: descriptive title for this session
2795
2808
  - sessionTranscript: clean summary of the conversation (meeting-notes style)
2796
- - findings: the array (projects with children, standalone issues)
2809
+ - findings: the array (bundles with children, standalone issues)
2797
2810
 
2798
- This creates findings (enriched or draft based on your assessment) and project decomposition \u2014 all in one call. Enriched findings appear in the work queue ready for the user to sync to Linear.
2811
+ 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.
2799
2812
 
2800
2813
  Only fall back to yapout_submit_yap_session if the session was purely exploratory with no clear actionable findings.
2801
2814
 
@@ -2831,7 +2844,7 @@ var childSchema = z15.object({
2831
2844
  title: z15.string().describe("Child issue title"),
2832
2845
  description: z15.string().describe("What needs to be done and why"),
2833
2846
  sourceQuote: z15.string().describe("Relevant excerpt from the conversation"),
2834
- type: z15.enum(["feature", "bug", "chore"]).describe("Finding category"),
2847
+ type: z15.enum(["feature", "bug", "chore", "spike"]).describe("Finding category"),
2835
2848
  priority: z15.enum(["urgent", "high", "medium", "low"]).describe("Priority level"),
2836
2849
  confidence: z15.number().min(0).max(1).describe("Confidence this is a clear, actionable finding (0-1)"),
2837
2850
  nature: z15.enum(["implementable", "operational", "spike"]).describe("What kind of work"),
@@ -2858,22 +2871,21 @@ function registerExtractFromYapTool(server, ctx) {
2858
2871
  title: z15.string().describe("Finding title"),
2859
2872
  description: z15.string().describe("What needs to be done and why"),
2860
2873
  sourceQuote: z15.string().describe("Relevant excerpt from the conversation"),
2861
- type: z15.enum(["feature", "bug", "chore"]).describe("Finding category (do not use spike \u2014 use nature instead)"),
2874
+ type: z15.enum(["feature", "bug", "chore", "spike"]).describe("Finding category (do not use spike \u2014 use nature instead)"),
2862
2875
  priority: z15.enum(["urgent", "high", "medium", "low"]).describe("Priority level"),
2863
2876
  confidence: z15.number().min(0).max(1).describe("Your confidence this is a clear, actionable finding (0-1)"),
2864
- level: z15.enum(["project", "issue"]).describe("project = body of work with children, issue = single unit"),
2865
2877
  nature: z15.enum(["implementable", "operational", "spike"]).describe("implementable = code changes, operational = manual task, spike = needs scoping"),
2866
2878
  sourceFindingId: z15.string().optional().describe("ID of an existing finding this scopes (e.g., scoping a spike)"),
2867
- // Issue-level enrichment (when level === "issue")
2879
+ // Enrichment data for standalone findings
2868
2880
  isEnriched: z15.boolean().optional().describe("For standalone issues: was this thoroughly discussed? true = enriched, false = draft"),
2869
- enrichedDescription: z15.string().optional().describe("Clean description for Linear (standalone issues only, required if isEnriched)"),
2870
- acceptanceCriteria: z15.array(z15.string()).optional().describe("Testable acceptance criteria (standalone issues only, required if isEnriched)"),
2871
- implementationBrief: z15.string().optional().describe("Technical context (standalone issues only, optional)"),
2872
- clarifications: z15.array(clarificationSchema).optional().describe("Relevant Q&A from conversation (standalone issues only)"),
2873
- // Project-level fields (when level === "project")
2874
- projectDescription: z15.string().optional().describe("What this body of work accomplishes (projects only)"),
2875
- suggestedOrder: z15.string().optional().describe("Implementation order: 'Phase 1: A, then Phase 2: B+C' (projects only)"),
2876
- children: z15.array(childSchema).optional().describe("Child issues for project-level decisions")
2881
+ enrichedDescription: z15.string().optional().describe("Clean description for Linear (required if isEnriched)"),
2882
+ acceptanceCriteria: z15.array(z15.string()).optional().describe("Testable acceptance criteria (required if isEnriched)"),
2883
+ implementationBrief: z15.string().optional().describe("Technical context (optional)"),
2884
+ clarifications: z15.array(clarificationSchema).optional().describe("Relevant Q&A from conversation"),
2885
+ // Bundle fields when a finding has children, a bundle is created
2886
+ bundleDescription: z15.string().optional().describe("What this body of work accomplishes (only for findings with children)"),
2887
+ suggestedOrder: z15.string().optional().describe("Implementation order: 'Phase 1: A, then Phase 2: B+C' (only for findings with children)"),
2888
+ children: z15.array(childSchema).optional().describe("Child issues \u2014 when present, a bundle is created from this finding")
2877
2889
  })
2878
2890
  ).describe("Findings from the conversation. Projects include children inline.")
2879
2891
  },
@@ -2891,7 +2903,7 @@ function registerExtractFromYapTool(server, ctx) {
2891
2903
  }
2892
2904
  try {
2893
2905
  const result = await ctx.client.mutation(
2894
- anyApi2.functions.captures.extractFromYapSession,
2906
+ anyApi3.functions.captures.extractFromYapSession,
2895
2907
  {
2896
2908
  projectId: ctx.projectId,
2897
2909
  title: args.sessionTitle,
@@ -2903,8 +2915,8 @@ function registerExtractFromYapTool(server, ctx) {
2903
2915
  let draftCount = 0;
2904
2916
  let totalFindings = 0;
2905
2917
  for (const item of result.items) {
2906
- if (item.level === "project") {
2907
- for (const child of item.children ?? []) {
2918
+ if (item.children && item.children.length > 0) {
2919
+ for (const child of item.children) {
2908
2920
  totalFindings++;
2909
2921
  if (child.findingStatus === "enriched") enrichedCount++;
2910
2922
  else draftCount++;
@@ -2954,35 +2966,36 @@ function registerExtractFromYapTool(server, ctx) {
2954
2966
  );
2955
2967
  }
2956
2968
 
2957
- // src/mcp/tools/save-project-enrichment.ts
2969
+ // src/mcp/tools/decompose-finding.ts
2958
2970
  import { z as z16 } from "zod";
2959
- function registerSaveProjectEnrichmentTool(server, ctx) {
2971
+ function registerDecomposeFindingTool(server, ctx) {
2960
2972
  server.tool(
2961
- "yapout_save_project_enrichment",
2962
- `Save the decomposition of a project-level finding into implementable child issues.
2973
+ "yapout_decompose_finding",
2974
+ `Decompose a finding into a bundle with child issues.
2963
2975
 
2964
- Call this after enriching a finding with level: "project". The agent should have:
2976
+ Call this when enrichment reveals a finding is too large for a single PR.
2977
+ The agent should have:
2965
2978
  1. Read the codebase extensively
2966
2979
  2. Asked the user scoping questions
2967
2980
  3. Produced a set of child issues with dependencies
2968
2981
 
2969
- This tool creates child findings in yapout, transitions the original
2970
- finding to "decomposed", and returns the child finding IDs for subsequent Linear sync.
2982
+ This tool creates a bundle in yapout, assigns child findings to it, and
2983
+ archives the original finding. Returns the bundle ID and child finding IDs.
2971
2984
 
2972
2985
  Each child issue's implementation brief must be detailed enough to stand alone as a
2973
2986
  full spec \u2014 schema changes, files to modify, edge cases, and acceptance criteria.`,
2974
2987
  {
2975
- findingId: z16.string().describe("The project-level finding being decomposed (from yapout_get_unenriched_finding)"),
2976
- projectDescription: z16.string().describe("Description for the project \u2014 what this body of work accomplishes"),
2988
+ findingId: z16.string().describe("The finding being decomposed (from yapout_get_unenriched_finding)"),
2989
+ bundleDescription: z16.string().describe("Description for the bundle \u2014 what this body of work accomplishes"),
2977
2990
  suggestedOrder: z16.string().describe("Human-readable implementation order (e.g. 'Phase 1: A, then Phase 2: B+C in parallel, then Phase 3: D')"),
2978
- linearProjectId: z16.string().optional().describe("Existing Linear project ID to associate child issues with. Omit to skip Linear project association \u2014 issues will be synced without a project."),
2991
+ linearProjectId: z16.string().optional().describe("Existing Linear project ID to associate with. Omit to skip Linear project association."),
2979
2992
  issues: z16.array(
2980
2993
  z16.object({
2981
2994
  title: z16.string().describe("Child issue title"),
2982
2995
  description: z16.string().describe("What needs to be done and why"),
2983
2996
  acceptanceCriteria: z16.array(z16.string()).describe("Testable acceptance criteria"),
2984
2997
  implementationBrief: z16.string().describe("Full spec: files to modify, schema changes, edge cases, approach"),
2985
- type: z16.enum(["feature", "bug", "chore"]).describe("Category of work"),
2998
+ type: z16.enum(["feature", "bug", "chore", "spike"]).describe("Category of work"),
2986
2999
  priority: z16.enum(["urgent", "high", "medium", "low"]).describe("Priority level"),
2987
3000
  dependsOn: z16.array(z16.number()).describe("Indices (0-based) of issues in this array that must be completed first")
2988
3001
  })
@@ -3002,10 +3015,10 @@ full spec \u2014 schema changes, files to modify, edge cases, and acceptance cri
3002
3015
  }
3003
3016
  try {
3004
3017
  const result = await ctx.client.mutation(
3005
- anyApi2.functions.localPipeline.saveProjectEnrichment,
3018
+ anyApi3.functions.localPipeline.decomposeIntoBundle,
3006
3019
  {
3007
3020
  findingId: args.findingId,
3008
- projectDescription: args.projectDescription,
3021
+ bundleDescription: args.bundleDescription,
3009
3022
  suggestedOrder: args.suggestedOrder,
3010
3023
  linearProjectId: args.linearProjectId,
3011
3024
  issues: args.issues
@@ -3018,10 +3031,12 @@ full spec \u2014 schema changes, files to modify, edge cases, and acceptance cri
3018
3031
  text: JSON.stringify(
3019
3032
  {
3020
3033
  findingId: result.findingId,
3021
- status: "decomposed",
3034
+ bundleId: result.bundleId,
3035
+ status: "archived",
3036
+ archivedReason: "decomposed",
3022
3037
  childFindingIds: result.childFindingIds,
3023
3038
  suggestedOrder: result.suggestedOrder,
3024
- message: `Project decomposed into ${result.childFindingIds.length} child issues. Original finding marked as decomposed. Child findings are in draft status \u2014 enrich and sync each one individually, or approve them for the user to review.`
3039
+ message: `Finding decomposed into bundle with ${result.childFindingIds.length} child issues. Original finding archived. Child findings are in draft status \u2014 enrich and sync each one individually, or approve them for the user to review.`
3025
3040
  },
3026
3041
  null,
3027
3042
  2
@@ -3034,7 +3049,7 @@ full spec \u2014 schema changes, files to modify, edge cases, and acceptance cri
3034
3049
  content: [
3035
3050
  {
3036
3051
  type: "text",
3037
- text: `Error saving project enrichment: ${err.message}`
3052
+ text: `Error decomposing finding: ${err.message}`
3038
3053
  }
3039
3054
  ],
3040
3055
  isError: true
@@ -3073,7 +3088,7 @@ The finding must be in "enriching" or "enriched" status. It will be transitioned
3073
3088
  }
3074
3089
  try {
3075
3090
  const result = await ctx.client.mutation(
3076
- anyApi2.functions.findings.markDuplicate,
3091
+ anyApi3.functions.findings.markDuplicate,
3077
3092
  {
3078
3093
  findingId: args.findingId,
3079
3094
  duplicateOfLinearId: args.duplicateOfLinearId,
@@ -3136,7 +3151,7 @@ and issue counts so you can ask the user for confirmation before creating a new
3136
3151
  }
3137
3152
  try {
3138
3153
  const project = await ctx.client.query(
3139
- anyApi2.functions.projects.getProject,
3154
+ anyApi3.functions.projects.getProject,
3140
3155
  { projectId: ctx.projectId }
3141
3156
  );
3142
3157
  if (!project?.linearTeamId) {
@@ -3151,7 +3166,7 @@ and issue counts so you can ask the user for confirmation before creating a new
3151
3166
  };
3152
3167
  }
3153
3168
  const projects = await ctx.client.action(
3154
- anyApi2.functions.linearProjectsMutations.fetchProjectsDetailed,
3169
+ anyApi3.functions.linearProjectsMutations.fetchProjectsDetailed,
3155
3170
  { teamId: project.linearTeamId }
3156
3171
  );
3157
3172
  return {
@@ -3213,7 +3228,7 @@ Optionally filter by tags, capture, or explicit finding IDs.`,
3213
3228
  }
3214
3229
  try {
3215
3230
  const allFindings = await ctx.client.query(
3216
- anyApi2.functions.findings.getProjectFindings,
3231
+ anyApi3.functions.findings.getProjectFindings,
3217
3232
  { projectId }
3218
3233
  );
3219
3234
  let drafts = (allFindings ?? []).filter((f) => f.status === "draft");
@@ -3283,7 +3298,7 @@ When done=true, all findings have been processed.`,
3283
3298
  if (args.skip && args.skipFindingId) {
3284
3299
  try {
3285
3300
  await ctx.client.mutation(
3286
- anyApi2.functions.localPipeline.releaseEnrichmentClaim,
3301
+ anyApi3.functions.localPipeline.releaseEnrichmentClaim,
3287
3302
  { findingId: args.skipFindingId }
3288
3303
  );
3289
3304
  updateSessionStats(args.sessionId, {
@@ -3293,7 +3308,7 @@ When done=true, all findings have been processed.`,
3293
3308
  }
3294
3309
  }
3295
3310
  const allFindings = await ctx.client.query(
3296
- anyApi2.functions.findings.getProjectFindings,
3311
+ anyApi3.functions.findings.getProjectFindings,
3297
3312
  { projectId: session.projectId }
3298
3313
  );
3299
3314
  let drafts = (allFindings ?? []).filter(
@@ -3335,7 +3350,7 @@ When done=true, all findings have been processed.`,
3335
3350
  }
3336
3351
  const next = drafts[0];
3337
3352
  const claimResult = await ctx.client.mutation(
3338
- anyApi2.functions.localPipeline.claimForEnrichment,
3353
+ anyApi3.functions.localPipeline.claimForEnrichment,
3339
3354
  { projectId: session.projectId, findingId: next._id }
3340
3355
  );
3341
3356
  if (!claimResult) {
@@ -3416,7 +3431,7 @@ The bundle and all its findings transition to "enriching" status.`,
3416
3431
  async (args) => {
3417
3432
  try {
3418
3433
  const result = await ctx.client.mutation(
3419
- anyApi2.functions.bundles.claimBundleForEnrichment,
3434
+ anyApi3.functions.bundles.claimBundleForEnrichment,
3420
3435
  { bundleId: args.bundleId }
3421
3436
  );
3422
3437
  if (!result) {
@@ -3474,7 +3489,7 @@ Call yapout_sync_bundle_to_linear afterwards to create the Linear project.`,
3474
3489
  async (args) => {
3475
3490
  try {
3476
3491
  await ctx.client.mutation(
3477
- anyApi2.functions.bundles.saveBundleEnrichment,
3492
+ anyApi3.functions.bundles.saveBundleEnrichment,
3478
3493
  {
3479
3494
  bundleId: args.bundleId,
3480
3495
  title: args.title,
@@ -3561,7 +3576,7 @@ async function startMcpServer() {
3561
3576
  registerSubmitYapSessionTool(server, ctx);
3562
3577
  registerStartYapTool(server, ctx);
3563
3578
  registerExtractFromYapTool(server, ctx);
3564
- registerSaveProjectEnrichmentTool(server, ctx);
3579
+ registerDecomposeFindingTool(server, ctx);
3565
3580
  registerMarkDuplicateTool(server, ctx);
3566
3581
  registerGetLinearProjectsTool(server, ctx);
3567
3582
  registerStartEnrichmentTool(server, ctx);
@@ -3655,14 +3670,14 @@ import chalk11 from "chalk";
3655
3670
  import { ConvexHttpClient as ConvexHttpClient3 } from "convex/browser";
3656
3671
 
3657
3672
  // src/daemon/watcher.ts
3658
- import { anyApi as anyApi4 } from "convex/server";
3673
+ import { anyApi as anyApi5 } from "convex/server";
3659
3674
  import { execSync as execSync4 } from "child_process";
3660
3675
  import { existsSync as existsSync9, mkdirSync as mkdirSync8 } from "fs";
3661
3676
  import { join as join10 } from "path";
3662
3677
  import { hostname as osHostname } from "os";
3663
3678
 
3664
3679
  // src/daemon/heartbeat.ts
3665
- import { anyApi as anyApi3 } from "convex/server";
3680
+ import { anyApi as anyApi4 } from "convex/server";
3666
3681
  var HEARTBEAT_INTERVAL = 3e4;
3667
3682
  var Heartbeat = class {
3668
3683
  client;
@@ -3690,7 +3705,7 @@ var Heartbeat = class {
3690
3705
  if (currentTicketId) args.currentTicketId = currentTicketId;
3691
3706
  if (currentBranch) args.currentBranch = currentBranch;
3692
3707
  if (worktreePath) args.worktreePath = worktreePath;
3693
- await this.client.mutation(anyApi3.functions.agents.agentHeartbeat, args);
3708
+ await this.client.mutation(anyApi4.functions.agents.agentHeartbeat, args);
3694
3709
  } catch {
3695
3710
  }
3696
3711
  }
@@ -3997,7 +4012,7 @@ var Watcher = class {
3997
4012
  );
3998
4013
  process.exit(1);
3999
4014
  }
4000
- await this.client.mutation(anyApi4.functions.agents.registerAgent, {
4015
+ await this.client.mutation(anyApi5.functions.agents.registerAgent, {
4001
4016
  projectId: this.options.projectId,
4002
4017
  sessionId: this.sessionId,
4003
4018
  machineHostname: this.getHostname()
@@ -4033,7 +4048,7 @@ Waiting for ${this.spawner.activeCount} agent(s) to finish...`
4033
4048
  await this.spawner.gracefulShutdown();
4034
4049
  }
4035
4050
  try {
4036
- await this.client.mutation(anyApi4.functions.agents.unregisterAgent, {
4051
+ await this.client.mutation(anyApi5.functions.agents.unregisterAgent, {
4037
4052
  sessionId: this.sessionId
4038
4053
  });
4039
4054
  } catch {
@@ -4047,7 +4062,7 @@ Waiting for ${this.spawner.activeCount} agent(s) to finish...`
4047
4062
  }
4048
4063
  this.heartbeat.stop();
4049
4064
  this.spawner.forceKill();
4050
- this.client.mutation(anyApi4.functions.agents.unregisterAgent, {
4065
+ this.client.mutation(anyApi5.functions.agents.unregisterAgent, {
4051
4066
  sessionId: this.sessionId
4052
4067
  }).catch(() => {
4053
4068
  });
@@ -4089,7 +4104,7 @@ Waiting for ${this.spawner.activeCount} agent(s) to finish...`
4089
4104
  }
4090
4105
  async checkForEnrichmentWork(maxSlots) {
4091
4106
  const tickets = await this.client.query(
4092
- anyApi4.functions.localPipeline.getUnenrichedTickets,
4107
+ anyApi5.functions.localPipeline.getUnenrichedTickets,
4093
4108
  { projectId: this.options.projectId }
4094
4109
  );
4095
4110
  if (!tickets || tickets.length === 0) return;
@@ -4118,7 +4133,7 @@ Waiting for ${this.spawner.activeCount} agent(s) to finish...`
4118
4133
  }
4119
4134
  async checkForImplementationWork(maxSlots) {
4120
4135
  const data = await this.client.query(
4121
- anyApi4.functions.tickets.getLocalQueuedTickets,
4136
+ anyApi5.functions.tickets.getLocalQueuedTickets,
4122
4137
  { projectId: this.options.projectId }
4123
4138
  );
4124
4139
  if (!data || data.ready.length === 0) return;
@@ -4191,7 +4206,7 @@ Waiting for ${this.spawner.activeCount} agent(s) to finish...`
4191
4206
  if (wt.ticketId) {
4192
4207
  try {
4193
4208
  const ticket = await this.client.query(
4194
- anyApi4.functions.tickets.getTicket,
4209
+ anyApi5.functions.tickets.getTicket,
4195
4210
  { ticketId: wt.ticketId }
4196
4211
  );
4197
4212
  if (ticket && (ticket.status === "failed" || ticket.status === "workflow2_done")) {
@@ -4376,15 +4391,15 @@ var queueCommand = new Command11("queue").description("Show pipeline state \u201
4376
4391
  process.exit(1);
4377
4392
  }
4378
4393
  const client = createConvexClient(creds.token);
4379
- const { anyApi: anyApi5 } = await import("convex/server");
4394
+ const { anyApi: anyApi6 } = await import("convex/server");
4380
4395
  const [queueData, unenriched, pending] = await Promise.all([
4381
- client.query(anyApi5.functions.tickets.getLocalQueuedTickets, {
4396
+ client.query(anyApi6.functions.tickets.getLocalQueuedTickets, {
4382
4397
  projectId: mapping.projectId
4383
4398
  }),
4384
- client.query(anyApi5.functions.localPipeline.getUnenrichedTickets, {
4399
+ client.query(anyApi6.functions.localPipeline.getUnenrichedTickets, {
4385
4400
  projectId: mapping.projectId
4386
4401
  }),
4387
- client.query(anyApi5.functions.localPipeline.getPendingSources, {
4402
+ client.query(anyApi6.functions.localPipeline.getPendingSources, {
4388
4403
  projectId: mapping.projectId
4389
4404
  })
4390
4405
  ]);
@@ -4475,9 +4490,9 @@ var nextCommand = new Command12("next").description("Claim the highest priority
4475
4490
  process.exit(1);
4476
4491
  }
4477
4492
  const client = createConvexClient(creds.token);
4478
- const { anyApi: anyApi5 } = await import("convex/server");
4493
+ const { anyApi: anyApi6 } = await import("convex/server");
4479
4494
  const data = await client.query(
4480
- anyApi5.functions.tickets.getLocalQueuedTickets,
4495
+ anyApi6.functions.tickets.getLocalQueuedTickets,
4481
4496
  { projectId: mapping.projectId }
4482
4497
  );
4483
4498
  if (!data?.ready || data.ready.length === 0) {
@@ -4502,7 +4517,7 @@ Claimed: ${ref} "${ticket.title}"`));
4502
4517
  }
4503
4518
  console.log(`Branch: ${chalk13.cyan(branchName)}`);
4504
4519
  const brief = await client.query(
4505
- anyApi5.functions.tickets.getTicketBrief,
4520
+ anyApi6.functions.tickets.getTicketBrief,
4506
4521
  { ticketId: ticket.ticketId }
4507
4522
  );
4508
4523
  if (brief) {
@@ -4578,7 +4593,7 @@ var recapCommand = new Command13("recap").description("Show a summary of recent
4578
4593
  process.exit(1);
4579
4594
  }
4580
4595
  const client = createConvexClient(creds.token);
4581
- const { anyApi: anyApi5 } = await import("convex/server");
4596
+ const { anyApi: anyApi6 } = await import("convex/server");
4582
4597
  const now = /* @__PURE__ */ new Date();
4583
4598
  const todayStart = new Date(
4584
4599
  now.getFullYear(),
@@ -4588,7 +4603,7 @@ var recapCommand = new Command13("recap").description("Show a summary of recent
4588
4603
  const weekStart = todayStart - 6 * 24 * 60 * 60 * 1e3;
4589
4604
  const since = opts.week ? weekStart : todayStart;
4590
4605
  const data = await client.query(
4591
- anyApi5.functions.localPipeline.getRecentActivity,
4606
+ anyApi6.functions.localPipeline.getRecentActivity,
4592
4607
  { projectId: mapping.projectId, since }
4593
4608
  );
4594
4609
  if (!data) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yapout",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "yapout CLI — link local repos, authenticate, and manage projects",
5
5
  "type": "module",
6
6
  "bin": {