yapout 0.15.2 → 0.16.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 +351 -616
  2. 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 Command16 } from "commander";
4
+ import { Command as Command17 } from "commander";
5
5
 
6
6
  // src/commands/login.ts
7
7
  import { Command as Command2 } from "commander";
@@ -45,6 +45,31 @@ function getRepoFullName(cwd) {
45
45
  if (httpsMatch) return httpsMatch[1];
46
46
  throw new Error(`Could not parse GitHub repo from remote URL: ${url}`);
47
47
  }
48
+ function getOriginRemoteUrl(cwd) {
49
+ return git("remote get-url origin", cwd);
50
+ }
51
+ function normalizeGitRemote(raw) {
52
+ if (!raw) return null;
53
+ let s = raw.trim();
54
+ if (s.endsWith(".git")) s = s.slice(0, -4);
55
+ s = s.replace(/^git@([^:]+):/, "$1/");
56
+ s = s.replace(/^ssh:\/\/(?:git@)?/, "");
57
+ s = s.replace(/^https?:\/\//, "");
58
+ s = s.replace(/^github\.com\//, "");
59
+ const parts = s.split("/").filter(Boolean);
60
+ if (parts.length < 2) return null;
61
+ const owner = parts[0];
62
+ const repo = parts[1];
63
+ return `https://github.com/${owner}/${repo}`;
64
+ }
65
+ function isInGitRepo(cwd) {
66
+ try {
67
+ git("rev-parse --git-dir", cwd);
68
+ return true;
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
48
73
  function getDefaultBranch(cwd) {
49
74
  try {
50
75
  const ref = git("rev-parse --abbrev-ref origin/HEAD", cwd);
@@ -174,6 +199,43 @@ function getOrCreateDeviceIdentity(defaultName) {
174
199
  writeDeviceIdentity(identity);
175
200
  return identity;
176
201
  }
202
+ function readResourceConfig(cwd) {
203
+ const path = join(resolveRepoRoot(cwd), ".yapout", "config.json");
204
+ if (!existsSync(path)) return null;
205
+ try {
206
+ const raw = JSON.parse(readFileSync(path, "utf-8"));
207
+ if (!raw || typeof raw !== "object") return null;
208
+ if (!raw.resourceId || !raw.canonicalId) return null;
209
+ return raw;
210
+ } catch {
211
+ return null;
212
+ }
213
+ }
214
+ function writeResourceConfig(cwd, cfg) {
215
+ const yapoutDir = join(resolveRepoRoot(cwd), ".yapout");
216
+ if (!existsSync(yapoutDir)) {
217
+ mkdirSync(yapoutDir, { recursive: true });
218
+ }
219
+ const path = join(yapoutDir, "config.json");
220
+ writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n");
221
+ }
222
+ function readActiveExecution(cwd) {
223
+ const path = join(cwd, ".yapout", "active-execution.json");
224
+ if (!existsSync(path)) {
225
+ const fallback = join(resolveRepoRoot(cwd), ".yapout", "active-execution.json");
226
+ if (!existsSync(fallback)) return null;
227
+ try {
228
+ return JSON.parse(readFileSync(fallback, "utf-8"));
229
+ } catch {
230
+ return null;
231
+ }
232
+ }
233
+ try {
234
+ return JSON.parse(readFileSync(path, "utf-8"));
235
+ } catch {
236
+ return null;
237
+ }
238
+ }
177
239
  var WATCH_DEFAULTS = {
178
240
  auto_enrich: true,
179
241
  auto_implement: true,
@@ -296,7 +358,7 @@ function requireAuth() {
296
358
  }
297
359
 
298
360
  // src/commands/serve.ts
299
- var CLI_VERSION = "0.15.2";
361
+ var CLI_VERSION = "0.16.0";
300
362
  var DEFAULT_PORT = 7777;
301
363
  var PORT_RANGE = 10;
302
364
  var HEARTBEAT_MS = 12e4;
@@ -345,7 +407,7 @@ function startServer(payload) {
345
407
  res.writeHead(404, headers);
346
408
  res.end();
347
409
  });
348
- return new Promise((resolve12, reject) => {
410
+ return new Promise((resolve13, reject) => {
349
411
  let attempt = 0;
350
412
  const tryListen = () => {
351
413
  const port = DEFAULT_PORT + attempt;
@@ -361,7 +423,7 @@ function startServer(payload) {
361
423
  server.once("error", onError);
362
424
  server.listen(port, "127.0.0.1", () => {
363
425
  server.removeListener("error", onError);
364
- resolve12({
426
+ resolve13({
365
427
  port,
366
428
  close: () => server.close()
367
429
  });
@@ -518,7 +580,7 @@ var serveCommand = new Command("serve").description(
518
580
  });
519
581
 
520
582
  // src/commands/login.ts
521
- var CLI_VERSION2 = "0.15.2";
583
+ var CLI_VERSION2 = "0.16.0";
522
584
  function safeReturnTo(raw) {
523
585
  if (!raw) return null;
524
586
  try {
@@ -530,7 +592,7 @@ function safeReturnTo(raw) {
530
592
  }
531
593
  }
532
594
  function startCallbackServer() {
533
- return new Promise((resolve12) => {
595
+ return new Promise((resolve13) => {
534
596
  let resolveData;
535
597
  let rejectData;
536
598
  const dataPromise = new Promise((res, rej) => {
@@ -579,7 +641,7 @@ function startCallbackServer() {
579
641
  server.listen(0, () => {
580
642
  const address = server.address();
581
643
  const port = typeof address === "object" && address ? address.port : 0;
582
- resolve12({ port, data: dataPromise });
644
+ resolve13({ port, data: dataPromise });
583
645
  });
584
646
  setTimeout(() => {
585
647
  server.close();
@@ -681,15 +743,6 @@ async function pickProject(projects) {
681
743
  })
682
744
  });
683
745
  }
684
- async function pickOrg(orgs, message = "Which org does this project belong to?") {
685
- return await select({
686
- message,
687
- choices: orgs.map((o) => ({
688
- name: `${o.name} (${o.slug}, ${o.role})`,
689
- value: o
690
- }))
691
- });
692
- }
693
746
 
694
747
  // src/commands/link.ts
695
748
  var CONFIG_YAML_CONTENT = `# yapout local configuration
@@ -716,7 +769,50 @@ branch_prefix: feat
716
769
  # {{ticket.linearTicketId}}, {{ticket.id}}
717
770
  # commit_template: "{{ticket.type}}({{ticket.linearTicketId}}): {{ticket.title}}"
718
771
  `;
719
- var linkCommand = new Command4("link").description("Link the current directory to a yapout project").action(async () => {
772
+ var linkCommand = new Command4("link").description(
773
+ "Link the current Resource into a Group (--group), or link the current directory to a project (legacy)."
774
+ ).option(
775
+ "--group <groupId>",
776
+ "Attach the active Resource into a Group (requires `yapout init` to have run in this repo)"
777
+ ).action(async (options) => {
778
+ if (options.group) {
779
+ await linkResourceToGroup(options.group);
780
+ return;
781
+ }
782
+ await legacyLinkProject();
783
+ });
784
+ async function linkResourceToGroup(groupId) {
785
+ const creds = requireAuth();
786
+ const cwd = resolve2(process.cwd());
787
+ const resourceCfg = readResourceConfig(cwd);
788
+ if (!resourceCfg) {
789
+ console.error(
790
+ chalk5.red("No `.yapout/config.json` found.") + " Run " + chalk5.cyan("yapout init") + " here first."
791
+ );
792
+ process.exit(1);
793
+ }
794
+ const client = createConvexClient(creds.token);
795
+ try {
796
+ await client.mutation(anyApi.functions.resourcesCli.linkResourceToGroup, {
797
+ resourceId: resourceCfg.resourceId,
798
+ groupId
799
+ });
800
+ } catch (err) {
801
+ console.error(
802
+ chalk5.red("Failed to link Resource to Group."),
803
+ err.message
804
+ );
805
+ process.exit(1);
806
+ }
807
+ writeResourceConfig(resolve2(process.cwd()), {
808
+ ...resourceCfg,
809
+ groupId
810
+ });
811
+ console.log(
812
+ chalk5.green("Linked Resource ") + chalk5.cyan(resourceCfg.canonicalId) + chalk5.green(" to Group ") + chalk5.cyan(groupId) + "."
813
+ );
814
+ }
815
+ async function legacyLinkProject() {
720
816
  const creds = requireAuth();
721
817
  const cwd = resolveRepoRoot(resolve2(process.cwd()));
722
818
  const client = createConvexClient(creds.token);
@@ -776,10 +872,7 @@ var linkCommand = new Command4("link").description("Link the current directory t
776
872
  if (existsSync3(gitignorePath)) {
777
873
  const content = readFileSync3(gitignorePath, "utf-8");
778
874
  if (!content.includes(".yapout/")) {
779
- appendFileSync(
780
- gitignorePath,
781
- "\n# yapout local config\n.yapout/\n"
782
- );
875
+ appendFileSync(gitignorePath, "\n# yapout local config\n.yapout/\n");
783
876
  }
784
877
  } else {
785
878
  writeFileSync3(gitignorePath, "# yapout local config\n.yapout/\n");
@@ -803,7 +896,7 @@ var linkCommand = new Command4("link").description("Link the current directory t
803
896
  console.log(
804
897
  chalk5.green(`Linked to ${label}${orgSuffix}.`) + " Claude Code will discover yapout tools automatically."
805
898
  );
806
- });
899
+ }
807
900
  function getCliVersion() {
808
901
  try {
809
902
  const pkg = JSON.parse(
@@ -916,7 +1009,6 @@ import {
916
1009
  readFileSync as readFileSync5,
917
1010
  appendFileSync as appendFileSync2
918
1011
  } from "fs";
919
- import { hostname as hostname4 } from "os";
920
1012
  import chalk8 from "chalk";
921
1013
  var CONFIG_YAML_CONTENT2 = `# yapout local configuration
922
1014
  # See: https://docs.yapout.dev/cli/config
@@ -927,7 +1019,6 @@ var CONFIG_YAML_CONTENT2 = `# yapout local configuration
927
1019
  # - npm run lint
928
1020
  # - npm run typecheck
929
1021
  # - npm run test
930
- # - sf project deploy --dry-run --target-org scratch
931
1022
  post_flight: []
932
1023
 
933
1024
  # Require post_flight checks to pass before yapout_ship succeeds.
@@ -936,170 +1027,89 @@ ship_requires_checks: false
936
1027
 
937
1028
  # Git branch prefix (used by yapout_claim)
938
1029
  branch_prefix: feat
939
-
940
- # Commit message template (optional \u2014 uses sensible default if omitted)
941
- # Available variables: {{ticket.title}}, {{ticket.type}}, {{ticket.priority}},
942
- # {{ticket.linearTicketId}}, {{ticket.id}}
943
- # commit_template: "{{ticket.type}}({{ticket.linearTicketId}}): {{ticket.title}}"
944
1030
  `;
945
- var initCommand = new Command7("init").description("Create a yapout project from the current repo and link it").argument("[name]", "Project name (defaults to repo name)").option("--org <slug>", "Org slug to create the project in (skips picker)").action(async (name, options) => {
1031
+ var initCommand = new Command7("init").description(
1032
+ "Register the current repo as a Resource and start the MCP server scoped to it"
1033
+ ).option(
1034
+ "--group <groupId>",
1035
+ "Optional Group id to attach the Resource to (defaults to the org's migration-bridge group)"
1036
+ ).action(async (options) => {
946
1037
  const creds = requireAuth();
947
- const cwd = resolveRepoRoot(resolve5(process.cwd()));
948
- let repoFullName;
949
- let defaultBranch;
950
- try {
951
- repoFullName = getRepoFullName(cwd);
952
- defaultBranch = getDefaultBranch(cwd);
953
- } catch (err) {
1038
+ const cwd = resolve5(process.cwd());
1039
+ if (!isInGitRepo(cwd)) {
954
1040
  console.error(
955
- chalk8.red("Not a git repo with a GitHub remote."),
956
- err.message
1041
+ chalk8.red("`yapout init` must be run from inside a git repo.") + "\n No .git/ directory found from " + chalk8.dim(cwd) + "."
957
1042
  );
958
1043
  process.exit(1);
959
1044
  }
960
- const projectName = name || repoFullName.split("/")[1] || "unnamed";
961
- const client = createConvexClient(creds.token);
962
- let orgs;
1045
+ const repoRoot = resolveRepoRoot(cwd);
1046
+ let rawRemote;
963
1047
  try {
964
- orgs = await client.query(
965
- anyApi.functions.orgMembers.getMyOrgs,
966
- {}
967
- );
1048
+ rawRemote = getOriginRemoteUrl(repoRoot);
968
1049
  } catch (err) {
969
1050
  console.error(
970
- chalk8.red("Failed to load your orgs."),
971
- err.message
1051
+ chalk8.red("Failed to read origin remote URL.") + "\n " + err.message + "\n Set a GitHub remote (`git remote add origin ...`) before running `yapout init`."
972
1052
  );
973
1053
  process.exit(1);
974
1054
  }
975
- if (!orgs || orgs.length === 0) {
1055
+ const canonicalId = normalizeGitRemote(rawRemote);
1056
+ if (!canonicalId) {
976
1057
  console.error(
977
1058
  chalk8.red(
978
- "You aren't a member of any org. Sign in to the web app once to create your personal org, then re-run."
979
- )
1059
+ `Could not derive a canonical id from the origin remote URL: ${rawRemote}`
1060
+ ) + "\n yapout currently supports github.com remotes only."
980
1061
  );
981
1062
  process.exit(1);
982
1063
  }
983
- let chosenOrgId;
984
- let chosenOrgName;
985
- if (options?.org) {
986
- const match = orgs.find((o) => o.org.slug === options.org);
987
- if (!match) {
988
- console.error(
989
- chalk8.red(`Org "${options.org}" not found among your memberships.`)
990
- );
991
- console.error(
992
- chalk8.dim(
993
- "Available: " + orgs.map((o) => o.org.slug).join(", ")
994
- )
995
- );
996
- process.exit(1);
997
- }
998
- chosenOrgId = match.org._id;
999
- chosenOrgName = match.org.name;
1000
- } else if (orgs.length === 1) {
1001
- chosenOrgId = orgs[0].org._id;
1002
- chosenOrgName = orgs[0].org.name;
1003
- console.log(
1004
- chalk8.dim(`Creating in `) + chalk8.cyan(chosenOrgName)
1005
- );
1006
- } else {
1007
- const picked = await pickOrg(
1008
- orgs.map((o) => ({
1009
- id: o.org._id,
1010
- name: o.org.name,
1011
- slug: o.org.slug,
1012
- role: o.role
1013
- }))
1014
- );
1015
- chosenOrgId = picked.id;
1016
- chosenOrgName = picked.name;
1017
- }
1018
- const device = getOrCreateDeviceIdentity(hostname4());
1064
+ let defaultBranch;
1019
1065
  try {
1020
- await client.mutation(anyApi.functions.devices.registerDevice, {
1021
- deviceId: device.deviceId,
1022
- name: device.name,
1023
- cliVersion: getCliVersion2(),
1024
- machineHostname: hostname4()
1025
- });
1026
- } catch (err) {
1027
- console.warn(
1028
- chalk8.yellow("Warning: device registration failed \u2014 "),
1029
- err.message
1030
- );
1066
+ defaultBranch = getDefaultBranch(repoRoot);
1067
+ } catch {
1068
+ defaultBranch = void 0;
1031
1069
  }
1070
+ const client = createConvexClient(creds.token);
1032
1071
  let result;
1033
1072
  try {
1034
1073
  result = await client.mutation(
1035
- anyApi.functions.projects.createProjectFromCli,
1074
+ anyApi.functions.resourcesCli.findOrCreateRepoResource,
1036
1075
  {
1037
- orgId: chosenOrgId,
1038
- name: projectName,
1039
- githubRepoFullName: repoFullName,
1040
- githubDefaultBranch: defaultBranch
1076
+ canonicalId,
1077
+ remoteUrl: canonicalId,
1078
+ defaultBranch,
1079
+ ...options?.group ? { groupId: options.group } : {}
1041
1080
  }
1042
1081
  );
1043
1082
  } catch (err) {
1044
1083
  console.error(
1045
- chalk8.red("Failed to create project."),
1084
+ chalk8.red("Failed to register Resource."),
1046
1085
  err.message
1047
1086
  );
1048
1087
  process.exit(1);
1049
1088
  }
1050
- try {
1051
- await client.mutation(anyApi.functions.projectCheckouts.linkCheckout, {
1052
- projectId: result.projectId,
1053
- deviceId: device.deviceId,
1054
- localPath: cwd
1055
- });
1056
- } catch (err) {
1057
- console.warn(
1058
- chalk8.yellow("Warning: failed to record project checkout \u2014 "),
1059
- err.message
1060
- );
1061
- }
1062
- setProjectMapping(cwd, {
1063
- projectId: result.projectId,
1064
- projectName: result.projectName,
1089
+ const yapoutDir = join5(repoRoot, ".yapout");
1090
+ if (!existsSync5(yapoutDir)) mkdirSync3(yapoutDir, { recursive: true });
1091
+ writeResourceConfig(repoRoot, {
1092
+ resourceId: result.resourceId,
1093
+ canonicalId: result.canonicalId,
1094
+ groupId: result.groupId,
1095
+ remoteUrl: canonicalId,
1096
+ defaultBranch,
1065
1097
  linkedAt: Date.now()
1066
1098
  });
1067
- let agentToken = null;
1068
- try {
1069
- const agentRes = await client.mutation(
1070
- anyApi.functions.agents.createAgent,
1071
- {
1072
- projectId: result.projectId,
1073
- displayName: `${creds.email}'s default agent (${result.projectName})`,
1074
- roleDescription: "Default agent created at yapout init. Has full tool access.",
1075
- scopes: ["*"],
1076
- label: "yapout init default"
1077
- }
1078
- );
1079
- agentToken = agentRes.rawToken;
1080
- } catch (err) {
1081
- console.warn(
1082
- chalk8.yellow("Warning: failed to provision default agent \u2014 "),
1083
- err.message
1084
- );
1085
- }
1086
- const yapoutDir = join5(cwd, ".yapout");
1087
- if (!existsSync5(yapoutDir)) mkdirSync3(yapoutDir, { recursive: true });
1088
- const configPath = join5(yapoutDir, "config.yml");
1089
- if (!existsSync5(configPath)) {
1090
- writeFileSync5(configPath, CONFIG_YAML_CONTENT2);
1099
+ const configYamlPath = join5(yapoutDir, "config.yml");
1100
+ if (!existsSync5(configYamlPath)) {
1101
+ writeFileSync5(configYamlPath, CONFIG_YAML_CONTENT2);
1091
1102
  }
1092
- const gitignorePath = join5(cwd, ".gitignore");
1103
+ const gitignorePath = join5(repoRoot, ".gitignore");
1093
1104
  if (existsSync5(gitignorePath)) {
1094
1105
  const content = readFileSync5(gitignorePath, "utf-8");
1095
1106
  if (!content.includes(".yapout/")) {
1096
- appendFileSync2(
1097
- gitignorePath,
1098
- "\n# yapout local config\n.yapout/\n"
1099
- );
1107
+ appendFileSync2(gitignorePath, "\n# yapout local config\n.yapout/\n");
1100
1108
  }
1109
+ } else {
1110
+ writeFileSync5(gitignorePath, "# yapout local config\n.yapout/\n");
1101
1111
  }
1102
- const mcpPath = join5(cwd, ".mcp.json");
1112
+ const mcpPath = join5(repoRoot, ".mcp.json");
1103
1113
  let mcpConfig = {};
1104
1114
  if (existsSync5(mcpPath)) {
1105
1115
  try {
@@ -1110,40 +1120,25 @@ var initCommand = new Command7("init").description("Create a yapout project from
1110
1120
  if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
1111
1121
  const existingYapoutServer = mcpConfig.mcpServers.yapout ?? {};
1112
1122
  const existingEnv = existingYapoutServer.env && typeof existingYapoutServer.env === "object" ? existingYapoutServer.env : {};
1113
- const newEnv = { ...existingEnv };
1114
- if (agentToken) {
1115
- newEnv.YAPOUT_AGENT_TOKEN = agentToken;
1116
- }
1117
1123
  mcpConfig.mcpServers.yapout = {
1118
1124
  command: "yapout",
1119
1125
  args: ["mcp-server"],
1120
- ...Object.keys(newEnv).length > 0 ? { env: newEnv } : {}
1126
+ ...Object.keys(existingEnv).length > 0 ? { env: existingEnv } : {}
1121
1127
  };
1122
1128
  writeFileSync5(mcpPath, JSON.stringify(mcpConfig, null, 2) + "\n");
1129
+ const verb = result.created ? "Registered" : "Linked";
1123
1130
  console.log(
1124
- chalk8.green(`Created project "${result.projectName}"`) + chalk8.dim(
1125
- ` in ${chosenOrgName} (${repoFullName}, branch: ${defaultBranch})`
1126
- )
1131
+ chalk8.green(`${verb} Resource `) + chalk8.cyan(canonicalId) + chalk8.dim(` (id: ${result.resourceId}).`)
1127
1132
  );
1128
- if (agentToken) {
1129
- console.log(
1130
- chalk8.dim("Provisioned default agent \u2014 token stored in ") + chalk8.cyan(".mcp.json")
1131
- );
1133
+ if (defaultBranch) {
1134
+ console.log(chalk8.dim(` default branch: ${defaultBranch}`));
1132
1135
  }
1133
1136
  console.log(
1134
- chalk8.dim("Run ") + chalk8.cyan("yapout_compact") + chalk8.dim(" in Claude Code to generate project context.")
1137
+ chalk8.dim(
1138
+ "Claude Code will discover yapout's MCP tools via .mcp.json on next launch."
1139
+ )
1135
1140
  );
1136
1141
  });
1137
- function getCliVersion2() {
1138
- try {
1139
- const pkg = JSON.parse(
1140
- readFileSync5(join5(import.meta.dirname, "..", "package.json"), "utf-8")
1141
- );
1142
- return pkg.version ?? "unknown";
1143
- } catch {
1144
- return "unknown";
1145
- }
1146
- }
1147
1142
 
1148
1143
  // src/commands/mcp-server.ts
1149
1144
  import { Command as Command8 } from "commander";
@@ -2581,7 +2576,7 @@ function truncate(text) {
2581
2576
  ` + lines.slice(-MAX_OUTPUT_LINES).join("\n");
2582
2577
  }
2583
2578
  function runCommand(command, cwd) {
2584
- return new Promise((resolve12) => {
2579
+ return new Promise((resolve13) => {
2585
2580
  const start = Date.now();
2586
2581
  const child = exec(command, {
2587
2582
  cwd,
@@ -2589,7 +2584,7 @@ function runCommand(command, cwd) {
2589
2584
  maxBuffer: 10 * 1024 * 1024
2590
2585
  // 10MB
2591
2586
  }, (error, stdout, stderr) => {
2592
- resolve12({
2587
+ resolve13({
2593
2588
  exitCode: error?.code ?? (error ? 1 : 0),
2594
2589
  stdout: truncate(stdout),
2595
2590
  stderr: truncate(stderr),
@@ -2597,7 +2592,7 @@ function runCommand(command, cwd) {
2597
2592
  });
2598
2593
  });
2599
2594
  child.on("error", () => {
2600
- resolve12({
2595
+ resolve13({
2601
2596
  exitCode: 1,
2602
2597
  stdout: "",
2603
2598
  stderr: `Command timed out after ${COMMAND_TIMEOUT_MS / 1e3}s`,
@@ -2683,91 +2678,8 @@ function registerCheckTool(server, ctx) {
2683
2678
  );
2684
2679
  }
2685
2680
 
2686
- // src/mcp/tools/bundle.ts
2687
- import { z as z10 } from "zod";
2688
- import { join as join10 } from "path";
2689
- import { existsSync as existsSync10, writeFileSync as writeFileSync8, mkdirSync as mkdirSync7 } from "fs";
2690
- function registerBundleTool(server, ctx) {
2691
- server.tool(
2692
- "yapout_bundle",
2693
- "Add a finding to the current finding's bundle so they ship as one PR",
2694
- {
2695
- findingId: z10.string().describe("Finding ID to add to the bundle"),
2696
- withFinding: z10.string().describe("Lead finding ID (currently being worked on)")
2697
- },
2698
- withScopeCheck(ctx, "yapout_bundle", async (args) => {
2699
- const result = await ctx.client.mutation(
2700
- anyApi5.functions.bundles.createBundle,
2701
- { leadFindingId: args.withFinding, joiningFindingId: args.findingId }
2702
- );
2703
- const bundledBrief = await ctx.client.query(
2704
- anyApi5.functions.bundles.getBundledBrief,
2705
- { bundleId: result.bundleId }
2706
- );
2707
- if (!bundledBrief) {
2708
- return {
2709
- content: [
2710
- {
2711
- type: "text",
2712
- text: JSON.stringify({
2713
- bundleId: result.bundleId,
2714
- message: "Bundled successfully but could not fetch combined brief."
2715
- })
2716
- }
2717
- ]
2718
- };
2719
- }
2720
- const sections = [
2721
- `# Bundle: ${bundledBrief.findings.length} findings`,
2722
- ""
2723
- ];
2724
- for (const t of bundledBrief.findings) {
2725
- sections.push(`## ${t.linearIssueId ?? t.findingId}: ${t.title}`);
2726
- sections.push("");
2727
- sections.push(`**Priority:** ${t.priority} | **Type:** ${t.type}`);
2728
- if (t.linearIssueUrl) sections.push(`**Linear:** ${t.linearIssueUrl}`);
2729
- sections.push("", t.description);
2730
- if (t.enrichedDescription) {
2731
- sections.push("", "### Enriched Description", "", t.enrichedDescription);
2732
- }
2733
- if (t.implementationBrief) {
2734
- sections.push("", "### Implementation Brief", "", t.implementationBrief);
2735
- }
2736
- sections.push("", "---", "");
2737
- }
2738
- if (bundledBrief.projectContext) {
2739
- sections.push("## Project Context", "", bundledBrief.projectContext);
2740
- }
2741
- const combinedBrief = sections.join("\n");
2742
- const yapoutDir = join10(ctx.cwd, ".yapout");
2743
- if (!existsSync10(yapoutDir)) mkdirSync7(yapoutDir, { recursive: true });
2744
- writeFileSync8(join10(yapoutDir, "brief.md"), combinedBrief);
2745
- return {
2746
- content: [
2747
- {
2748
- type: "text",
2749
- text: JSON.stringify(
2750
- {
2751
- bundleId: result.bundleId,
2752
- findings: bundledBrief.findings.map((t) => ({
2753
- findingId: t.findingId,
2754
- title: t.title
2755
- })),
2756
- combinedBrief,
2757
- message: `${bundledBrief.findings.length} findings bundled. Brief updated.`
2758
- },
2759
- null,
2760
- 2
2761
- )
2762
- }
2763
- ]
2764
- };
2765
- })
2766
- );
2767
- }
2768
-
2769
2681
  // src/mcp/tools/get-unenriched-findings.ts
2770
- import { z as z11 } from "zod";
2682
+ import { z as z10 } from "zod";
2771
2683
  function registerGetUnenrichedFindingsTool(server, ctx) {
2772
2684
  server.tool(
2773
2685
  "yapout_get_unenriched_finding",
@@ -2781,7 +2693,7 @@ After calling this tool, you should:
2781
2693
  3. If the finding is ambiguous, ask the developer clarifying questions in conversation
2782
2694
  4. When confident, call yapout_save_enrichment with a clean description, acceptance criteria, and implementation brief`,
2783
2695
  {
2784
- findingId: z11.string().optional().describe("Specific finding ID to enrich. If omitted, returns the highest priority draft finding.")
2696
+ findingId: z10.string().optional().describe("Specific finding ID to enrich. If omitted, returns the highest priority draft finding.")
2785
2697
  },
2786
2698
  withScopeCheck(ctx, "yapout_get_unenriched_finding", async (args) => {
2787
2699
  const projectId = ctx.projectId ?? process.env.YAPOUT_PROJECT_ID;
@@ -2895,7 +2807,7 @@ function registerGetExistingFindingsTool(server, ctx) {
2895
2807
  }
2896
2808
 
2897
2809
  // src/mcp/tools/save-enrichment.ts
2898
- import { z as z12 } from "zod";
2810
+ import { z as z11 } from "zod";
2899
2811
 
2900
2812
  // src/mcp/tools/enrichment-session.ts
2901
2813
  var activeSessions = /* @__PURE__ */ new Map();
@@ -2940,22 +2852,22 @@ This tool saves the enrichment, then automatically creates the Linear issue with
2940
2852
 
2941
2853
  The finding transitions: enriching \u2192 enriched \u2192 ready.`,
2942
2854
  {
2943
- findingId: z12.string().describe("The finding ID to enrich (from yapout_get_unenriched_finding)"),
2944
- title: z12.string().describe("Refined finding title \u2014 improve it if the original was vague"),
2945
- cleanDescription: z12.string().describe("Human-readable summary for the Linear issue body. Write the kind of finding a senior engineer would write."),
2946
- acceptanceCriteria: z12.array(z12.string()).describe("List of testable acceptance criteria (each a single clear statement)"),
2947
- implementationBrief: z12.string().describe("Deep technical context for the implementing agent: which files, what approach, edge cases to watch for"),
2948
- clarifications: z12.array(
2949
- z12.object({
2950
- question: z12.string().describe("The question you asked the developer"),
2951
- answer: z12.string().describe("The developer's answer")
2855
+ findingId: z11.string().describe("The finding ID to enrich (from yapout_get_unenriched_finding)"),
2856
+ title: z11.string().describe("Refined finding title \u2014 improve it if the original was vague"),
2857
+ cleanDescription: z11.string().describe("Human-readable summary for the Linear issue body. Write the kind of finding a senior engineer would write."),
2858
+ acceptanceCriteria: z11.array(z11.string()).describe("List of testable acceptance criteria (each a single clear statement)"),
2859
+ implementationBrief: z11.string().describe("Deep technical context for the implementing agent: which files, what approach, edge cases to watch for"),
2860
+ clarifications: z11.array(
2861
+ z11.object({
2862
+ question: z11.string().describe("The question you asked the developer"),
2863
+ answer: z11.string().describe("The developer's answer")
2952
2864
  })
2953
2865
  ).optional().describe("Only meaningful Q&A from the conversation \u2014 deviations from expected scope, scoping decisions, etc. Omit if you had no questions."),
2954
- isOversized: z12.boolean().optional().describe("Set to true if this finding is too large for a single PR"),
2955
- suggestedSplit: z12.array(z12.string()).optional().describe("If oversized: suggested sub-finding titles for breaking it down"),
2956
- nature: z12.enum(["implementable", "operational", "spike"]).optional().describe("Override the finding's nature if enrichment reveals it should be reclassified"),
2957
- cloudSafe: z12.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."),
2958
- sessionId: z12.string().optional().describe("Bulk enrichment session ID (from yapout_start_enrichment). Updates session stats.")
2866
+ isOversized: z11.boolean().optional().describe("Set to true if this finding is too large for a single PR"),
2867
+ suggestedSplit: z11.array(z11.string()).optional().describe("If oversized: suggested sub-finding titles for breaking it down"),
2868
+ nature: z11.enum(["implementable", "operational", "spike"]).optional().describe("Override the finding's nature if enrichment reveals it should be reclassified"),
2869
+ cloudSafe: z11.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."),
2870
+ sessionId: z11.string().optional().describe("Bulk enrichment session ID (from yapout_start_enrichment). Updates session stats.")
2959
2871
  },
2960
2872
  withScopeCheck(ctx, "yapout_save_enrichment", async (args) => {
2961
2873
  try {
@@ -3027,13 +2939,13 @@ The finding transitions: enriching \u2192 enriched \u2192 ready.`,
3027
2939
  }
3028
2940
 
3029
2941
  // src/mcp/tools/sync-to-linear.ts
3030
- import { z as z13 } from "zod";
2942
+ import { z as z12 } from "zod";
3031
2943
  function registerSyncToLinearTool(server, ctx) {
3032
2944
  server.tool(
3033
2945
  "yapout_sync_to_linear",
3034
2946
  "Trigger Linear issue creation for an enriched finding. The sync runs server-side (encrypted Linear token in Convex).",
3035
2947
  {
3036
- findingId: z13.string().describe("The finding ID to sync to Linear")
2948
+ findingId: z12.string().describe("The finding ID to sync to Linear")
3037
2949
  },
3038
2950
  withScopeCheck(ctx, "yapout_sync_to_linear", async (args) => {
3039
2951
  try {
@@ -3073,14 +2985,14 @@ function registerSyncToLinearTool(server, ctx) {
3073
2985
  }
3074
2986
 
3075
2987
  // src/mcp/tools/submit-yap-session.ts
3076
- import { z as z14 } from "zod";
2988
+ import { z as z13 } from "zod";
3077
2989
  function registerSubmitYapSessionTool(server, ctx) {
3078
2990
  server.tool(
3079
2991
  "yapout_submit_yap_session",
3080
2992
  "Submit a yap session transcript for finding extraction",
3081
2993
  {
3082
- title: z14.string().describe("Session title (e.g., 'Notification system brainstorm')"),
3083
- transcript: z14.string().describe("Cleaned conversation transcript")
2994
+ title: z13.string().describe("Session title (e.g., 'Notification system brainstorm')"),
2995
+ transcript: z13.string().describe("Cleaned conversation transcript")
3084
2996
  },
3085
2997
  withScopeCheck(ctx, "yapout_submit_yap_session", async (args) => {
3086
2998
  if (!ctx.projectId) {
@@ -3134,7 +3046,7 @@ function registerSubmitYapSessionTool(server, ctx) {
3134
3046
  }
3135
3047
 
3136
3048
  // src/mcp/tools/start-yap.ts
3137
- import { z as z15 } from "zod";
3049
+ import { z as z14 } from "zod";
3138
3050
  var PERSONA_PRESETS = {
3139
3051
  "tech lead": "You are an experienced tech lead. You care about maintainability, simplicity, and shipping. Challenge over-engineering and vague scope.",
3140
3052
  "qa engineer": "You are a skeptical QA engineer. Focus on error states, edge cases, missing validation, accessibility, and user-facing failure modes.",
@@ -3150,10 +3062,10 @@ function registerStartYapTool(server, ctx) {
3150
3062
  "yapout_start_yap",
3151
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.",
3152
3064
  {
3153
- persona: z15.string().optional().describe(
3065
+ persona: z14.string().optional().describe(
3154
3066
  'Interviewer persona: "tech lead", "qa engineer", "product owner", "end user", or a custom description'
3155
3067
  ),
3156
- context: z15.string().optional().describe("What the developer wants to discuss")
3068
+ context: z14.string().optional().describe("What the developer wants to discuss")
3157
3069
  },
3158
3070
  withScopeCheck(ctx, "yapout_start_yap", async (args) => {
3159
3071
  if (!ctx.projectId) {
@@ -3304,57 +3216,57 @@ Only fall back to yapout_submit_yap_session if the session was purely explorator
3304
3216
  }
3305
3217
 
3306
3218
  // src/mcp/tools/extract-from-yap.ts
3307
- import { z as z16 } from "zod";
3308
- var clarificationSchema = z16.object({
3309
- question: z16.string().describe("The question asked during the conversation"),
3310
- answer: z16.string().describe("The answer given")
3219
+ import { z as z15 } from "zod";
3220
+ var clarificationSchema = z15.object({
3221
+ question: z15.string().describe("The question asked during the conversation"),
3222
+ answer: z15.string().describe("The answer given")
3311
3223
  });
3312
- var childSchema = z16.object({
3313
- title: z16.string().describe("Child issue title"),
3314
- description: z16.string().describe("What needs to be done and why"),
3315
- sourceQuote: z16.string().describe("Relevant excerpt from the conversation"),
3316
- type: z16.enum(["feature", "bug", "chore", "spike"]).describe("Finding category"),
3317
- priority: z16.enum(["urgent", "high", "medium", "low"]).describe("Priority level"),
3318
- confidence: z16.number().min(0).max(1).describe("Confidence this is a clear, actionable finding (0-1)"),
3319
- nature: z16.enum(["implementable", "operational", "spike"]).describe("What kind of work"),
3224
+ var childSchema = z15.object({
3225
+ title: z15.string().describe("Child issue title"),
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"),
3320
3232
  // Enrichment — set isEnriched: true only if the conversation covered this
3321
3233
  // issue thoroughly enough to produce a finding a developer could pick up.
3322
- isEnriched: z16.boolean().optional().describe(
3234
+ isEnriched: z15.boolean().optional().describe(
3323
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)"
3324
3236
  ),
3325
- enrichedDescription: z16.string().optional().describe("Clean, final description for the Linear issue body (required if isEnriched)"),
3326
- acceptanceCriteria: z16.array(z16.string()).optional().describe("Testable acceptance criteria (required if isEnriched)"),
3327
- implementationBrief: z16.string().optional().describe("Technical context: files, approach, edge cases (optional \u2014 the implementing agent reads the codebase anyway)"),
3328
- clarifications: z16.array(clarificationSchema).optional().describe("Relevant Q&A from the conversation that shaped this finding"),
3329
- dependsOn: z16.array(z16.number()).optional().describe("Indices (0-based) of sibling children that must be completed first")
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")
3330
3242
  });
3331
3243
  function registerExtractFromYapTool(server, ctx) {
3332
3244
  server.tool(
3333
3245
  "yapout_extract_from_yap",
3334
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.",
3335
3247
  {
3336
- sessionTitle: z16.string().describe("Descriptive title for this yap session"),
3337
- sessionTranscript: z16.string().describe("Cleaned transcript of the conversation (meeting-notes style)"),
3338
- findings: z16.array(
3339
- z16.object({
3340
- title: z16.string().describe("Finding title"),
3341
- description: z16.string().describe("What needs to be done and why"),
3342
- sourceQuote: z16.string().describe("Relevant excerpt from the conversation"),
3343
- type: z16.enum(["feature", "bug", "chore", "spike"]).describe("Finding category (do not use spike \u2014 use nature instead)"),
3344
- priority: z16.enum(["urgent", "high", "medium", "low"]).describe("Priority level"),
3345
- confidence: z16.number().min(0).max(1).describe("Your confidence this is a clear, actionable finding (0-1)"),
3346
- nature: z16.enum(["implementable", "operational", "spike"]).describe("implementable = code changes, operational = manual task, spike = needs scoping"),
3347
- sourceFindingId: z16.string().optional().describe("ID of an existing finding this scopes (e.g., scoping a spike)"),
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)"),
3348
3260
  // Enrichment data for standalone findings
3349
- isEnriched: z16.boolean().optional().describe("For standalone issues: was this thoroughly discussed? true = enriched, false = draft"),
3350
- enrichedDescription: z16.string().optional().describe("Clean description for Linear (required if isEnriched)"),
3351
- acceptanceCriteria: z16.array(z16.string()).optional().describe("Testable acceptance criteria (required if isEnriched)"),
3352
- implementationBrief: z16.string().optional().describe("Technical context (optional)"),
3353
- clarifications: z16.array(clarificationSchema).optional().describe("Relevant Q&A from conversation"),
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"),
3354
3266
  // Bundle fields — when a finding has children, a bundle is created
3355
- bundleDescription: z16.string().optional().describe("What this body of work accomplishes (only for findings with children)"),
3356
- suggestedOrder: z16.string().optional().describe("Implementation order: 'Phase 1: A, then Phase 2: B+C' (only for findings with children)"),
3357
- children: z16.array(childSchema).optional().describe("Child issues \u2014 when present, a bundle is created from this finding")
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")
3358
3270
  })
3359
3271
  ).describe("Findings from the conversation. Projects include children inline.")
3360
3272
  },
@@ -3436,7 +3348,7 @@ function registerExtractFromYapTool(server, ctx) {
3436
3348
  }
3437
3349
 
3438
3350
  // src/mcp/tools/decompose-finding.ts
3439
- import { z as z17 } from "zod";
3351
+ import { z as z16 } from "zod";
3440
3352
  function registerDecomposeFindingTool(server, ctx) {
3441
3353
  server.tool(
3442
3354
  "yapout_decompose_finding",
@@ -3454,19 +3366,19 @@ archives the original finding. Returns the bundle ID and child finding IDs.
3454
3366
  Each child issue's implementation brief must be detailed enough to stand alone as a
3455
3367
  full spec \u2014 schema changes, files to modify, edge cases, and acceptance criteria.`,
3456
3368
  {
3457
- findingId: z17.string().describe("The finding being decomposed (from yapout_get_unenriched_finding)"),
3458
- bundleDescription: z17.string().describe("Description for the bundle \u2014 what this body of work accomplishes"),
3459
- suggestedOrder: z17.string().describe("Human-readable implementation order (e.g. 'Phase 1: A, then Phase 2: B+C in parallel, then Phase 3: D')"),
3460
- linearProjectId: z17.string().optional().describe("Existing Linear project ID to associate with. Omit to skip Linear project association."),
3461
- issues: z17.array(
3462
- z17.object({
3463
- title: z17.string().describe("Child issue title"),
3464
- description: z17.string().describe("What needs to be done and why"),
3465
- acceptanceCriteria: z17.array(z17.string()).describe("Testable acceptance criteria"),
3466
- implementationBrief: z17.string().describe("Full spec: files to modify, schema changes, edge cases, approach"),
3467
- type: z17.enum(["feature", "bug", "chore", "spike"]).describe("Category of work"),
3468
- priority: z17.enum(["urgent", "high", "medium", "low"]).describe("Priority level"),
3469
- dependsOn: z17.array(z17.number()).describe("Indices (0-based) of issues in this array that must be completed first")
3369
+ findingId: z16.string().describe("The finding being decomposed (from yapout_get_unenriched_finding)"),
3370
+ bundleDescription: z16.string().describe("Description for the bundle \u2014 what this body of work accomplishes"),
3371
+ suggestedOrder: z16.string().describe("Human-readable implementation order (e.g. 'Phase 1: A, then Phase 2: B+C in parallel, then Phase 3: D')"),
3372
+ linearProjectId: z16.string().optional().describe("Existing Linear project ID to associate with. Omit to skip Linear project association."),
3373
+ issues: z16.array(
3374
+ z16.object({
3375
+ title: z16.string().describe("Child issue title"),
3376
+ description: z16.string().describe("What needs to be done and why"),
3377
+ acceptanceCriteria: z16.array(z16.string()).describe("Testable acceptance criteria"),
3378
+ implementationBrief: z16.string().describe("Full spec: files to modify, schema changes, edge cases, approach"),
3379
+ type: z16.enum(["feature", "bug", "chore", "spike"]).describe("Category of work"),
3380
+ priority: z16.enum(["urgent", "high", "medium", "low"]).describe("Priority level"),
3381
+ dependsOn: z16.array(z16.number()).describe("Indices (0-based) of issues in this array that must be completed first")
3470
3382
  })
3471
3383
  ).describe("The child issues produced by decomposition")
3472
3384
  },
@@ -3529,7 +3441,7 @@ full spec \u2014 schema changes, files to modify, edge cases, and acceptance cri
3529
3441
  }
3530
3442
 
3531
3443
  // src/mcp/tools/mark-duplicate.ts
3532
- import { z as z18 } from "zod";
3444
+ import { z as z17 } from "zod";
3533
3445
  function registerMarkDuplicateTool(server, ctx) {
3534
3446
  server.tool(
3535
3447
  "yapout_mark_duplicate",
@@ -3539,9 +3451,9 @@ flow when you discover the work is already tracked in Linear.
3539
3451
  The finding must be in "enriching" or "enriched" status. It will be transitioned to
3540
3452
  "failed" with the duplicate reference stored. Nothing is synced to Linear.`,
3541
3453
  {
3542
- findingId: z18.string().describe("The yapout finding to archive as a duplicate"),
3543
- duplicateOfLinearId: z18.string().describe("The Linear issue identifier it duplicates (e.g. 'ENG-234')"),
3544
- reason: z18.string().describe("Brief explanation of why this is a duplicate")
3454
+ findingId: z17.string().describe("The yapout finding to archive as a duplicate"),
3455
+ duplicateOfLinearId: z17.string().describe("The Linear issue identifier it duplicates (e.g. 'ENG-234')"),
3456
+ reason: z17.string().describe("Brief explanation of why this is a duplicate")
3545
3457
  },
3546
3458
  withScopeCheck(ctx, "yapout_mark_duplicate", async (args) => {
3547
3459
  if (!ctx.projectId) {
@@ -3670,7 +3582,7 @@ and issue counts so you can ask the user for confirmation before creating a new
3670
3582
  }
3671
3583
 
3672
3584
  // src/mcp/tools/start-enrichment.ts
3673
- import { z as z19 } from "zod";
3585
+ import { z as z18 } from "zod";
3674
3586
  function registerStartEnrichmentTool(server, ctx) {
3675
3587
  server.tool(
3676
3588
  "yapout_start_enrichment",
@@ -3681,10 +3593,10 @@ and maintains filter criteria so you don't re-pass them on every call.
3681
3593
 
3682
3594
  Optionally filter by tags, capture, or explicit finding IDs.`,
3683
3595
  {
3684
- filter: z19.object({
3685
- tags: z19.array(z19.string()).optional().describe("Only enrich findings with these tags"),
3686
- captureId: z19.string().optional().describe("Only enrich findings from this capture"),
3687
- findingIds: z19.array(z19.string()).optional().describe("Only enrich these specific findings")
3596
+ filter: z18.object({
3597
+ tags: z18.array(z18.string()).optional().describe("Only enrich findings with these tags"),
3598
+ captureId: z18.string().optional().describe("Only enrich findings from this capture"),
3599
+ findingIds: z18.array(z18.string()).optional().describe("Only enrich these specific findings")
3688
3600
  }).optional().describe("Filter criteria. If omitted, all draft findings in the project are included.")
3689
3601
  },
3690
3602
  withScopeCheck(ctx, "yapout_start_enrichment", async (args) => {
@@ -3740,7 +3652,7 @@ Optionally filter by tags, capture, or explicit finding IDs.`,
3740
3652
  }
3741
3653
 
3742
3654
  // src/mcp/tools/enrich-next.ts
3743
- import { z as z20 } from "zod";
3655
+ import { z as z19 } from "zod";
3744
3656
  function registerEnrichNextTool(server, ctx) {
3745
3657
  server.tool(
3746
3658
  "yapout_enrich_next",
@@ -3751,9 +3663,9 @@ Returns the next unclaimed draft finding matching the session's filter.
3751
3663
 
3752
3664
  When done=true, all findings have been processed.`,
3753
3665
  {
3754
- sessionId: z20.string().describe("Session ID from yapout_start_enrichment"),
3755
- skip: z20.boolean().optional().describe("If true, skip the current finding (release back to draft)"),
3756
- skipFindingId: z20.string().optional().describe("The finding ID to skip (must be in 'enriching' status)")
3666
+ sessionId: z19.string().describe("Session ID from yapout_start_enrichment"),
3667
+ skip: z19.boolean().optional().describe("If true, skip the current finding (release back to draft)"),
3668
+ skipFindingId: z19.string().optional().describe("The finding ID to skip (must be in 'enriching' status)")
3757
3669
  },
3758
3670
  withScopeCheck(ctx, "yapout_enrich_next", async (args) => {
3759
3671
  const session = getSession(args.sessionId);
@@ -3881,125 +3793,8 @@ When done=true, all findings have been processed.`,
3881
3793
  );
3882
3794
  }
3883
3795
 
3884
- // src/mcp/tools/enrich-bundle.ts
3885
- import { z as z21 } from "zod";
3886
- function registerEnrichBundleTool(server, ctx) {
3887
- server.tool(
3888
- "yapout_enrich_bundle",
3889
- `Claim an entire bundle for enrichment. Returns ALL findings in the bundle at once
3890
- with their relationships, source quotes, and project context.
3891
-
3892
- This is for enriching a bundle as a single cohesive unit \u2014 NOT individual findings.
3893
- The agent should understand the full scope, ask questions about the bundle as a whole,
3894
- then call yapout_save_bundle_enrichment with enrichment data for every finding.
3895
-
3896
- The bundle and all its findings transition to "enriching" status.`,
3897
- {
3898
- bundleId: z21.string().describe("The bundle ID to enrich")
3899
- },
3900
- withScopeCheck(ctx, "yapout_enrich_bundle", async (args) => {
3901
- try {
3902
- const result = await ctx.client.mutation(
3903
- anyApi5.functions.bundles.claimBundleForEnrichment,
3904
- { bundleId: args.bundleId }
3905
- );
3906
- if (!result) {
3907
- return {
3908
- content: [{ type: "text", text: "Failed to claim bundle for enrichment." }],
3909
- isError: true
3910
- };
3911
- }
3912
- return {
3913
- content: [
3914
- {
3915
- type: "text",
3916
- text: JSON.stringify(result, null, 2)
3917
- }
3918
- ]
3919
- };
3920
- } catch (err) {
3921
- return {
3922
- content: [{ type: "text", text: `Error claiming bundle: ${err.message}` }],
3923
- isError: true
3924
- };
3925
- }
3926
- })
3927
- );
3928
- }
3929
-
3930
- // src/mcp/tools/save-bundle-enrichment.ts
3931
- import { z as z22 } from "zod";
3932
- function registerSaveBundleEnrichmentTool(server, ctx) {
3933
- server.tool(
3934
- "yapout_save_bundle_enrichment",
3935
- `Save enrichment for an entire bundle at once. Call this after you've analyzed all
3936
- findings in the bundle as a cohesive unit, read the codebase, and asked any questions.
3937
-
3938
- Provide:
3939
- - Bundle-level: overall description, combined acceptance criteria, implementation brief
3940
- - Per-finding: each finding gets its own refined title, description, acceptance criteria, and brief
3941
-
3942
- The bundle and all findings transition from "enriching" to "enriched".
3943
- Call yapout_sync_bundle_to_linear afterwards to create the Linear project.`,
3944
- {
3945
- bundleId: z22.string().describe("The bundle ID"),
3946
- title: z22.string().optional().describe("Refined bundle title (optional, keeps existing if omitted)"),
3947
- enrichedDescription: z22.string().describe("Bundle-level description \u2014 the cohesive story of what this bundle delivers"),
3948
- acceptanceCriteria: z22.array(z22.string()).describe("Bundle-level acceptance criteria"),
3949
- implementationBrief: z22.string().describe("Bundle-level implementation brief \u2014 overall approach, architecture decisions, key files"),
3950
- cloudSafe: z22.boolean().optional().describe("Set true ONLY if the entire bundle is small mechanical work (\u226430 lines total, single-file ideally, text/className/constant edits). Default false. If unsure, false."),
3951
- findings: z22.array(z22.object({
3952
- findingId: z22.string().describe("Finding ID"),
3953
- title: z22.string().describe("Refined finding title"),
3954
- enrichedDescription: z22.string().describe("Finding-specific description"),
3955
- acceptanceCriteria: z22.array(z22.string()).describe("Finding-specific acceptance criteria"),
3956
- implementationBrief: z22.string().describe("Finding-specific implementation brief")
3957
- })).describe("Per-finding enrichment data \u2014 one entry per finding in the bundle")
3958
- },
3959
- withScopeCheck(ctx, "yapout_save_bundle_enrichment", async (args) => {
3960
- try {
3961
- await ctx.client.mutation(
3962
- anyApi5.functions.bundles.saveBundleEnrichment,
3963
- {
3964
- bundleId: args.bundleId,
3965
- title: args.title,
3966
- enrichedDescription: args.enrichedDescription,
3967
- acceptanceCriteria: args.acceptanceCriteria,
3968
- implementationBrief: args.implementationBrief,
3969
- cloudSafe: args.cloudSafe,
3970
- findings: args.findings.map((f) => ({
3971
- findingId: f.findingId,
3972
- title: f.title,
3973
- enrichedDescription: f.enrichedDescription,
3974
- acceptanceCriteria: f.acceptanceCriteria,
3975
- implementationBrief: f.implementationBrief
3976
- }))
3977
- }
3978
- );
3979
- return {
3980
- content: [
3981
- {
3982
- type: "text",
3983
- text: JSON.stringify({
3984
- bundleId: args.bundleId,
3985
- findingsEnriched: args.findings.length,
3986
- message: `Bundle enriched successfully (${args.findings.length} findings). Call yapout_sync_bundle_to_linear to create the Linear project.`
3987
- }, null, 2)
3988
- }
3989
- ]
3990
- };
3991
- } catch (err) {
3992
- return {
3993
- content: [{ type: "text", text: `Error saving bundle enrichment: ${err.message}` }],
3994
- isError: true
3995
- };
3996
- }
3997
- })
3998
- );
3999
- }
4000
-
4001
3796
  // src/mcp/tools/block-enrichment.ts
4002
- import { z as z23 } from "zod";
3797
+ import { z as z20 } from "zod";
4003
3798
  function registerBlockEnrichmentTool(server, ctx) {
4004
3799
  server.tool(
4005
3800
  "yapout_block_enrichment",
@@ -4011,9 +3806,9 @@ The finding must currently be in "enriching" status (claimed via yapout_get_unen
4011
3806
 
4012
3807
  Transitions: enriching \u2192 needs_input. The user sees the blockerReason and questions in the UI, adds context, and resubmits.`,
4013
3808
  {
4014
- findingId: z23.string().describe("The finding ID to block (currently in 'enriching')"),
4015
- blockerReason: z23.string().describe("One sentence: why this finding cannot be enriched as written. State the missing piece concretely."),
4016
- blockerQuestions: z23.array(z23.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.")
3809
+ findingId: z20.string().describe("The finding ID to block (currently in 'enriching')"),
3810
+ blockerReason: z20.string().describe("One sentence: why this finding cannot be enriched as written. State the missing piece concretely."),
3811
+ blockerQuestions: z20.array(z20.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.")
4017
3812
  },
4018
3813
  withScopeCheck(ctx, "yapout_block_enrichment", async (args) => {
4019
3814
  try {
@@ -4060,9 +3855,9 @@ Transitions: enriching \u2192 needs_input. The user sees the blockerReason and q
4060
3855
 
4061
3856
  Same semantics as yapout_block_enrichment but for an entire bundle. Bundle status transitions enriching \u2192 needs_input; child findings revert to draft.`,
4062
3857
  {
4063
- bundleId: z23.string().describe("The bundle ID to block (currently in 'enriching')"),
4064
- blockerReason: z23.string().describe("One sentence: why this bundle cannot be enriched."),
4065
- blockerQuestions: z23.array(z23.string()).min(1).describe("2-5 specific questions the user must answer.")
3858
+ bundleId: z20.string().describe("The bundle ID to block (currently in 'enriching')"),
3859
+ blockerReason: z20.string().describe("One sentence: why this bundle cannot be enriched."),
3860
+ blockerQuestions: z20.array(z20.string()).min(1).describe("2-5 specific questions the user must answer.")
4066
3861
  },
4067
3862
  async (args) => {
4068
3863
  try {
@@ -5163,143 +4958,6 @@ function registerProjectTools(server, ctx) {
5163
4958
  registerTool(server, ctx, listProjectsTool);
5164
4959
  }
5165
4960
 
5166
- // src/mcp/tools/bundles-crud.ts
5167
- var getBundleTool = defineTool({
5168
- name: "yapout_get_bundle",
5169
- description: `Fetch a bundle by id, including its child findings array. Use this when you need full bundle context \u2014 title, description, enrichment data, claim state, child findings \u2014 not just the work-queue summary.`,
5170
- inputSchema: { bundleId: z.string() },
5171
- handler: async (ctx, args) => {
5172
- const bundle = await ctx.client.query(
5173
- anyApi5.functions.bundles.getBundle,
5174
- args
5175
- );
5176
- if (!bundle) {
5177
- return {
5178
- content: [{ type: "text", text: "Bundle not found." }],
5179
- isError: true
5180
- };
5181
- }
5182
- return {
5183
- content: [
5184
- { type: "text", text: JSON.stringify(bundle, null, 2) }
5185
- ]
5186
- };
5187
- }
5188
- });
5189
- var listBundlesTool = defineTool({
5190
- name: "yapout_list_bundles",
5191
- description: `List every bundle in the current project. Returns the bundle records (title, status, claim, archive metadata). For per-bundle child findings, follow up with yapout_get_bundle.`,
5192
- inputSchema: {},
5193
- handler: async (ctx) => {
5194
- if (!ctx.projectId) {
5195
- return {
5196
- content: [{ type: "text", text: "No project linked." }],
5197
- isError: true
5198
- };
5199
- }
5200
- const bundles = await ctx.client.query(
5201
- anyApi5.functions.bundles.getProjectBundles,
5202
- { projectId: ctx.projectId }
5203
- );
5204
- return {
5205
- content: [
5206
- { type: "text", text: JSON.stringify(bundles, null, 2) }
5207
- ]
5208
- };
5209
- }
5210
- });
5211
- var updateBundleTool = defineTool({
5212
- name: "yapout_update_bundle",
5213
- description: `Patch a bundle's editable content fields: title, description. Pass only the fields you want to change. Status / claim / archive go through dedicated tools.`,
5214
- inputSchema: {
5215
- bundleId: z.string(),
5216
- title: z.string().optional(),
5217
- description: z.string().optional()
5218
- },
5219
- handler: async (ctx, args) => {
5220
- await ctx.client.mutation(
5221
- anyApi5.functions.bundles.updateBundle,
5222
- args
5223
- );
5224
- return {
5225
- content: [{ type: "text", text: "Bundle updated." }]
5226
- };
5227
- }
5228
- });
5229
- var archiveBundleTool = defineTool({
5230
- name: "yapout_archive_bundle",
5231
- description: `Archive a bundle. Children are detached (their bundleId cleared) so they can be re-bundled or shipped solo. Use yapout_restore_bundle to bring it back.`,
5232
- inputSchema: { bundleId: z.string() },
5233
- handler: async (ctx, args) => {
5234
- await ctx.client.mutation(
5235
- anyApi5.functions.bundles.archiveBundle,
5236
- args
5237
- );
5238
- return {
5239
- content: [{ type: "text", text: "Bundle archived." }]
5240
- };
5241
- }
5242
- });
5243
- var restoreBundleTool = defineTool({
5244
- name: "yapout_restore_bundle",
5245
- description: `Restore an archived bundle. Defaults to draft; pass to: "ready" to bring it straight back to the open queue.`,
5246
- inputSchema: {
5247
- bundleId: z.string(),
5248
- to: z.enum(["draft", "ready"]).optional()
5249
- },
5250
- handler: async (ctx, args) => {
5251
- await ctx.client.mutation(
5252
- anyApi5.functions.bundles.restoreBundle,
5253
- args
5254
- );
5255
- return {
5256
- content: [{ type: "text", text: "Bundle restored." }]
5257
- };
5258
- }
5259
- });
5260
- var addFindingToBundleTool = defineTool({
5261
- name: "yapout_add_finding_to_bundle",
5262
- description: `Attach an existing finding to a bundle. The finding's bundleId is set; its status is left alone (use yapout_set_finding_status separately if you want to align statuses).`,
5263
- inputSchema: {
5264
- findingId: z.string(),
5265
- bundleId: z.string()
5266
- },
5267
- handler: async (ctx, args) => {
5268
- await ctx.client.mutation(
5269
- anyApi5.functions.findings.addToBundle,
5270
- args
5271
- );
5272
- return {
5273
- content: [{ type: "text", text: "Finding added to bundle." }]
5274
- };
5275
- }
5276
- });
5277
- var removeFindingFromBundleTool = defineTool({
5278
- name: "yapout_remove_finding_from_bundle",
5279
- description: `Detach a finding from its current bundle. The finding stays in the project; only its bundleId is cleared.`,
5280
- inputSchema: { findingId: z.string() },
5281
- handler: async (ctx, args) => {
5282
- await ctx.client.mutation(
5283
- anyApi5.functions.findings.removeFromBundle,
5284
- args
5285
- );
5286
- return {
5287
- content: [
5288
- { type: "text", text: "Finding removed from bundle." }
5289
- ]
5290
- };
5291
- }
5292
- });
5293
- function registerBundleCrudTools(server, ctx) {
5294
- registerTool(server, ctx, getBundleTool);
5295
- registerTool(server, ctx, listBundlesTool);
5296
- registerTool(server, ctx, updateBundleTool);
5297
- registerTool(server, ctx, archiveBundleTool);
5298
- registerTool(server, ctx, restoreBundleTool);
5299
- registerTool(server, ctx, addFindingToBundleTool);
5300
- registerTool(server, ctx, removeFindingFromBundleTool);
5301
- }
5302
-
5303
4961
  // src/mcp/tools/insights.ts
5304
4962
  var listInsightsTool = defineTool({
5305
4963
  name: "yapout_list_insights",
@@ -5471,7 +5129,7 @@ async function startMcpServer() {
5471
5129
  };
5472
5130
  const server = new McpServer({
5473
5131
  name: "yapout",
5474
- version: "0.15.2"
5132
+ version: "0.16.0"
5475
5133
  });
5476
5134
  registerInitTool(server, ctx);
5477
5135
  registerCompactTool(server, ctx);
@@ -5482,7 +5140,6 @@ async function startMcpServer() {
5482
5140
  registerEventTool(server, ctx);
5483
5141
  registerShipTool(server, ctx);
5484
5142
  registerCheckTool(server, ctx);
5485
- registerBundleTool(server, ctx);
5486
5143
  registerGetUnenrichedFindingsTool(server, ctx);
5487
5144
  registerGetExistingFindingsTool(server, ctx);
5488
5145
  registerSaveEnrichmentTool(server, ctx);
@@ -5495,8 +5152,6 @@ async function startMcpServer() {
5495
5152
  registerGetLinearProjectsTool(server, ctx);
5496
5153
  registerStartEnrichmentTool(server, ctx);
5497
5154
  registerEnrichNextTool(server, ctx);
5498
- registerEnrichBundleTool(server, ctx);
5499
- registerSaveBundleEnrichmentTool(server, ctx);
5500
5155
  registerBlockEnrichmentTool(server, ctx);
5501
5156
  registerSessionTools(server, ctx);
5502
5157
  registerCodebaseTools(server, ctx);
@@ -5506,7 +5161,6 @@ async function startMcpServer() {
5506
5161
  registerFindingReadTools(server, ctx);
5507
5162
  registerFindingWriteTools(server, ctx);
5508
5163
  registerProjectTools(server, ctx);
5509
- registerBundleCrudTools(server, ctx);
5510
5164
  registerInsightTools(server, ctx);
5511
5165
  registerPipelineRunTools(server, ctx);
5512
5166
  const transport = new StdioServerTransport();
@@ -6191,9 +5845,89 @@ var pillarTest = new Command15("test").description("Send a test delivery to a pi
6191
5845
  });
6192
5846
  var pillarCommand = new Command15("pillar").description("Manage per-project pillar overrides (extraction / enrichment / intelligence / implementation)").addCommand(pillarList).addCommand(pillarSet).addCommand(pillarTest);
6193
5847
 
5848
+ // src/commands/observe.ts
5849
+ import { Command as Command16 } from "commander";
5850
+ import { resolve as resolve12 } from "path";
5851
+ import chalk16 from "chalk";
5852
+ var observeCommand = new Command16("observe").description("File an observation Raw against the active Execution").argument("<observation...>", "Freeform observation text").option(
5853
+ "--severity <level>",
5854
+ "Severity hint: low | normal | high (default: normal)"
5855
+ ).option(
5856
+ "--area <hint>",
5857
+ "Optional scoping hint for the extraction chain (e.g., 'auth', 'billing')"
5858
+ ).action(
5859
+ async (words, options) => {
5860
+ const creds = requireAuth();
5861
+ const cwd = resolve12(process.cwd());
5862
+ const resourceCfg = readResourceConfig(cwd);
5863
+ if (!resourceCfg) {
5864
+ console.error(
5865
+ chalk16.red("No `.yapout/config.json` found.") + " Run " + chalk16.cyan("yapout init") + " in this repo first."
5866
+ );
5867
+ process.exit(1);
5868
+ }
5869
+ const active = readActiveExecution(cwd);
5870
+ if (!active) {
5871
+ console.error(
5872
+ chalk16.red("No active Execution.") + " `yapout observe` must run from a worktree where " + chalk16.cyan("yapout pick") + " has been called."
5873
+ );
5874
+ process.exit(1);
5875
+ }
5876
+ const body = words.join(" ").trim();
5877
+ if (!body) {
5878
+ console.error(chalk16.red("Observation body cannot be empty."));
5879
+ process.exit(1);
5880
+ }
5881
+ const severityHint = parseSeverity(options.severity);
5882
+ if (options.severity && !severityHint) {
5883
+ console.error(
5884
+ chalk16.red(
5885
+ `Invalid --severity: ${options.severity}. Allowed: low, normal, high.`
5886
+ )
5887
+ );
5888
+ process.exit(1);
5889
+ }
5890
+ const client = createConvexClient(creds.token);
5891
+ let result;
5892
+ try {
5893
+ result = await client.mutation(
5894
+ anyApi.functions.resourcesCli.createAgentObservationRaw,
5895
+ {
5896
+ executionId: active.executionId,
5897
+ body,
5898
+ ...severityHint ? { severityHint } : {},
5899
+ ...options.area ? { area: options.area } : {}
5900
+ }
5901
+ );
5902
+ } catch (err) {
5903
+ console.error(
5904
+ chalk16.red("Failed to file observation."),
5905
+ err.message
5906
+ );
5907
+ process.exit(1);
5908
+ }
5909
+ console.log(
5910
+ chalk16.green("Filed agent observation. ") + chalk16.dim(
5911
+ `(raw: ${result.rawId}, execution: ${result.executionId})`
5912
+ )
5913
+ );
5914
+ console.log(
5915
+ chalk16.dim(
5916
+ "Extraction will turn this into a Request (or link it to an existing one)."
5917
+ )
5918
+ );
5919
+ }
5920
+ );
5921
+ function parseSeverity(raw) {
5922
+ if (!raw) return void 0;
5923
+ const v = raw.toLowerCase();
5924
+ if (v === "low" || v === "normal" || v === "high") return v;
5925
+ return void 0;
5926
+ }
5927
+
6194
5928
  // src/index.ts
6195
- var program = new Command16();
6196
- program.name("yapout").description("yapout \u2014 the PM tool for agents").version("0.15.2");
5929
+ var program = new Command17();
5930
+ program.name("yapout").description("yapout \u2014 the PM tool for agents").version("0.16.0");
6197
5931
  program.addCommand(loginCommand);
6198
5932
  program.addCommand(logoutCommand);
6199
5933
  program.addCommand(initCommand);
@@ -6209,4 +5943,5 @@ program.addCommand(agentCommand);
6209
5943
  program.addCommand(webhookCommand);
6210
5944
  program.addCommand(inboundCommand);
6211
5945
  program.addCommand(pillarCommand);
5946
+ program.addCommand(observeCommand);
6212
5947
  program.parse();