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