vibebusiness 1.2.89 → 1.2.91

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 (113) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-build-manifest.json +37 -37
  3. package/.next/standalone/.next/app-path-routes-manifest.json +1 -1
  4. package/.next/standalone/.next/build-manifest.json +2 -2
  5. package/.next/standalone/.next/prerender-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  7. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  8. package/.next/standalone/.next/server/app/_not-found.rsc +1 -1
  9. package/.next/standalone/.next/server/app/api/analyze/route.js.nft.json +1 -1
  10. package/.next/standalone/.next/server/app/api/briefing/route.js.nft.json +1 -1
  11. package/.next/standalone/.next/server/app/api/config/route.js.nft.json +1 -1
  12. package/.next/standalone/.next/server/app/api/epics/[id]/ideas/route.js.nft.json +1 -1
  13. package/.next/standalone/.next/server/app/api/epics/[id]/route.js.nft.json +1 -1
  14. package/.next/standalone/.next/server/app/api/epics/route.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/api/goals/[id]/kpis/route.js.nft.json +1 -1
  16. package/.next/standalone/.next/server/app/api/goals/[id]/route.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/api/goals/route.js.nft.json +1 -1
  18. package/.next/standalone/.next/server/app/api/hypotheses/[id]/route.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/api/hypotheses/route.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/api/ideas/[id]/card/route.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/api/ideas/[id]/comments/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/ideas/[id]/implement/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/ideas/[id]/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/ideas/[id]/transition/route.js +4 -1
  25. package/.next/standalone/.next/server/app/api/ideas/[id]/transition/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/ideas/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/implementations/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/kpis/refresh/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/social/[id]/publish/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/social/[id]/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/briefing/page_client-reference-manifest.js +1 -1
  33. package/.next/standalone/.next/server/app/goals/[id]/page_client-reference-manifest.js +1 -1
  34. package/.next/standalone/.next/server/app/goals/page.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/goals/page_client-reference-manifest.js +1 -1
  36. package/.next/standalone/.next/server/app/hypotheses/[id]/page_client-reference-manifest.js +1 -1
  37. package/.next/standalone/.next/server/app/hypotheses/page.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/hypotheses/page_client-reference-manifest.js +1 -1
  39. package/.next/standalone/.next/server/app/ideas/[id]/page.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/ideas/[id]/page_client-reference-manifest.js +1 -1
  41. package/.next/standalone/.next/server/app/index.html +1 -1
  42. package/.next/standalone/.next/server/app/index.rsc +1 -1
  43. package/.next/standalone/.next/server/app/kanban/page.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/kanban/page_client-reference-manifest.js +1 -1
  45. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  46. package/.next/standalone/.next/server/app/roadmap/[id]/page_client-reference-manifest.js +1 -1
  47. package/.next/standalone/.next/server/app/roadmap/investors/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/roadmap/investors/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/roadmap/page.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/roadmap/page_client-reference-manifest.js +1 -1
  51. package/.next/standalone/.next/server/app/roadmap/public/page.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/roadmap/public/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/app/sessions/page.js.nft.json +1 -1
  54. package/.next/standalone/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  55. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  56. package/.next/standalone/.next/server/app/settings.html +1 -1
  57. package/.next/standalone/.next/server/app/settings.rsc +1 -1
  58. package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
  59. package/.next/standalone/.next/server/app/social.html +1 -1
  60. package/.next/standalone/.next/server/app/social.rsc +2 -2
  61. package/.next/standalone/.next/server/app/updates/[id]/page.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/updates/[id]/page_client-reference-manifest.js +1 -1
  63. package/.next/standalone/.next/server/app/updates/new/page_client-reference-manifest.js +1 -1
  64. package/.next/standalone/.next/server/app/updates/new.html +1 -1
  65. package/.next/standalone/.next/server/app/updates/new.rsc +2 -2
  66. package/.next/standalone/.next/server/app/updates/page.js.nft.json +1 -1
  67. package/.next/standalone/.next/server/app/updates/page_client-reference-manifest.js +1 -1
  68. package/.next/standalone/.next/server/app-paths-manifest.json +17 -17
  69. package/.next/standalone/.next/server/chunks/7151.js +7 -7
  70. package/.next/standalone/.next/server/pages/404.html +1 -1
  71. package/.next/standalone/.next/server/pages/500.html +1 -1
  72. package/.next/standalone/.next/server/pages-manifest.json +1 -1
  73. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  74. package/.next/standalone/data/codebase-snapshot.json +44 -61
  75. package/.next/standalone/data/ideas.json +729 -0
  76. package/.next/standalone/data/sessions.json +32 -0
  77. package/.next/standalone/package.json +1 -1
  78. package/.next/standalone/scripts/analyze.ts +35 -4
  79. package/.next/standalone/templates/commands/growth-activate.md +64 -0
  80. package/.next/standalone/templates/commands/vision.md +68 -0
  81. package/.next/static/chunks/147-271b001e9a7f5e6c.js +1 -0
  82. package/.next/static/chunks/159-82cfa8b54cbd2c11.js +1 -0
  83. package/.next/static/chunks/47-bab84ed5cfb8fd9d.js +1 -0
  84. package/.next/static/chunks/879-ae588e66e8e0b1d0.js +1 -0
  85. package/.next/static/chunks/app/briefing/page-a38c83adfb5ef570.js +1 -0
  86. package/.next/static/chunks/app/goals/[id]/page-ce4e225f25e7294d.js +1 -0
  87. package/.next/static/chunks/app/ideas/[id]/page-6b28cf279c62a569.js +1 -0
  88. package/.next/static/chunks/app/roadmap/[id]/page-381809badf3910a3.js +1 -0
  89. package/.next/static/chunks/app/social/page-2d71af5da2476063.js +1 -0
  90. package/.next/static/chunks/app/updates/new/page-0acea8b7d66043a5.js +1 -0
  91. package/dist/scripts/analyze.js +525 -76
  92. package/dist/scripts/chat.js +10 -0
  93. package/dist/scripts/generate-idea.js +3 -0
  94. package/dist/scripts/heartbeat.js +1896 -353
  95. package/dist/scripts/implement.js +3 -0
  96. package/dist/scripts/init.js +3 -0
  97. package/dist/scripts/scan.js +3 -0
  98. package/dist/scripts/social-routine.js +3 -0
  99. package/package.json +1 -1
  100. package/templates/commands/growth-activate.md +64 -0
  101. package/templates/commands/vision.md +68 -0
  102. package/.next/static/chunks/147-b00f2ac2bbec93ae.js +0 -1
  103. package/.next/static/chunks/159-4ce492ccac6de8f7.js +0 -1
  104. package/.next/static/chunks/47-eba0f8b4f9b17641.js +0 -1
  105. package/.next/static/chunks/879-7fbd95e93ddc4636.js +0 -1
  106. package/.next/static/chunks/app/briefing/page-683aba0c52e910d7.js +0 -1
  107. package/.next/static/chunks/app/goals/[id]/page-40818dc7e710eeda.js +0 -1
  108. package/.next/static/chunks/app/ideas/[id]/page-5f33e0ecf590ddec.js +0 -1
  109. package/.next/static/chunks/app/roadmap/[id]/page-9ba8a537e30c633c.js +0 -1
  110. package/.next/static/chunks/app/social/page-21daeca0cf8af46b.js +0 -1
  111. package/.next/static/chunks/app/updates/new/page-ac5b966024ce0ddc.js +0 -1
  112. /package/.next/static/{UaMf1fmArAE78vyk_zQVK → ixOS0rCuseEALBC-pPhha}/_buildManifest.js +0 -0
  113. /package/.next/static/{UaMf1fmArAE78vyk_zQVK → ixOS0rCuseEALBC-pPhha}/_ssgManifest.js +0 -0
@@ -105,8 +105,8 @@ var require_package = __commonJS({
105
105
  var require_main = __commonJS({
106
106
  "node_modules/dotenv/lib/main.js"(exports2, module2) {
107
107
  "use strict";
108
- var fs36 = require("fs");
109
- var path31 = require("path");
108
+ var fs43 = require("fs");
109
+ var path35 = require("path");
110
110
  var os3 = require("os");
111
111
  var crypto5 = require("crypto");
112
112
  var packageJson = require_package();
@@ -244,7 +244,7 @@ var require_main = __commonJS({
244
244
  if (options && options.path && options.path.length > 0) {
245
245
  if (Array.isArray(options.path)) {
246
246
  for (const filepath of options.path) {
247
- if (fs36.existsSync(filepath)) {
247
+ if (fs43.existsSync(filepath)) {
248
248
  possibleVaultPath = filepath.endsWith(".vault") ? filepath : `${filepath}.vault`;
249
249
  }
250
250
  }
@@ -252,15 +252,15 @@ var require_main = __commonJS({
252
252
  possibleVaultPath = options.path.endsWith(".vault") ? options.path : `${options.path}.vault`;
253
253
  }
254
254
  } else {
255
- possibleVaultPath = path31.resolve(process.cwd(), ".env.vault");
255
+ possibleVaultPath = path35.resolve(process.cwd(), ".env.vault");
256
256
  }
257
- if (fs36.existsSync(possibleVaultPath)) {
257
+ if (fs43.existsSync(possibleVaultPath)) {
258
258
  return possibleVaultPath;
259
259
  }
260
260
  return null;
261
261
  }
262
262
  function _resolveHome(envPath) {
263
- return envPath[0] === "~" ? path31.join(os3.homedir(), envPath.slice(1)) : envPath;
263
+ return envPath[0] === "~" ? path35.join(os3.homedir(), envPath.slice(1)) : envPath;
264
264
  }
265
265
  function _configVault(options) {
266
266
  const debug = parseBoolean(process.env.DOTENV_CONFIG_DEBUG || options && options.debug);
@@ -277,7 +277,7 @@ var require_main = __commonJS({
277
277
  return { parsed };
278
278
  }
279
279
  function configDotenv(options) {
280
- const dotenvPath = path31.resolve(process.cwd(), ".env");
280
+ const dotenvPath = path35.resolve(process.cwd(), ".env");
281
281
  let encoding = "utf8";
282
282
  let processEnv = process.env;
283
283
  if (options && options.processEnv != null) {
@@ -305,13 +305,13 @@ var require_main = __commonJS({
305
305
  }
306
306
  let lastError;
307
307
  const parsedAll = {};
308
- for (const path32 of optionPaths) {
308
+ for (const path36 of optionPaths) {
309
309
  try {
310
- const parsed = DotenvModule.parse(fs36.readFileSync(path32, { encoding }));
310
+ const parsed = DotenvModule.parse(fs43.readFileSync(path36, { encoding }));
311
311
  DotenvModule.populate(parsedAll, parsed, options);
312
312
  } catch (e) {
313
313
  if (debug) {
314
- _debug(`Failed to load ${path32} ${e.message}`);
314
+ _debug(`Failed to load ${path36} ${e.message}`);
315
315
  }
316
316
  lastError = e;
317
317
  }
@@ -324,7 +324,7 @@ var require_main = __commonJS({
324
324
  const shortPaths = [];
325
325
  for (const filePath of optionPaths) {
326
326
  try {
327
- const relative = path31.relative(process.cwd(), filePath);
327
+ const relative = path35.relative(process.cwd(), filePath);
328
328
  shortPaths.push(relative);
329
329
  } catch (e) {
330
330
  if (debug) {
@@ -535,7 +535,7 @@ function findProjectRoot() {
535
535
  }
536
536
  return cwd;
537
537
  }
538
- var path2, fs3, PROJECT_DIR, DATA_DIR, STATUS_FILE, TODO_FILE, MEMORY_FILE, IDEAS_FILE, GOALS_FILE, SESSIONS_FILE, CONFIG_FILE, BUSINESS_CONTEXT_FILE, HYPOTHESES_FILE, IMPLEMENTATIONS_FILE, ROADMAP_FILE, COMPETITORS_FILE, POSITIONING_FILE, PAGES_FILE, PAYMENTS_FILE, POSTHOG_FILE, SOCIAL_FILE, TEMPLATES_DIR, COMMAND_TEMPLATES_DIR;
538
+ var path2, fs3, PROJECT_DIR, DATA_DIR, STATUS_FILE, TODO_FILE, MEMORY_FILE, IDEAS_FILE, GOALS_FILE, SESSIONS_FILE, CONFIG_FILE, BUSINESS_CONTEXT_FILE, HYPOTHESES_FILE, IMPLEMENTATIONS_FILE, ROADMAP_FILE, PRODUCT_VISION_FILE, COMPETITORS_FILE, POSITIONING_FILE, PAGES_FILE, PAYMENTS_FILE, POSTHOG_FILE, SOCIAL_FILE, MARKETING_STRATEGIES_FILE, CAMPAIGNS_DIR, TEMPLATES_DIR, COMMAND_TEMPLATES_DIR;
539
539
  var init_paths = __esm({
540
540
  "scripts/lib/paths.ts"() {
541
541
  "use strict";
@@ -554,12 +554,15 @@ var init_paths = __esm({
554
554
  HYPOTHESES_FILE = path2.join(DATA_DIR, "hypotheses.json");
555
555
  IMPLEMENTATIONS_FILE = path2.join(DATA_DIR, "implementations.json");
556
556
  ROADMAP_FILE = path2.join(DATA_DIR, "roadmap.json");
557
+ PRODUCT_VISION_FILE = path2.join(DATA_DIR, "product-vision.json");
557
558
  COMPETITORS_FILE = path2.join(DATA_DIR, "competitors.json");
558
559
  POSITIONING_FILE = path2.join(DATA_DIR, "positioning.json");
559
560
  PAGES_FILE = path2.join(DATA_DIR, "pages.json");
560
561
  PAYMENTS_FILE = path2.join(DATA_DIR, "payments.json");
561
562
  POSTHOG_FILE = path2.join(DATA_DIR, "posthog.json");
562
563
  SOCIAL_FILE = path2.join(DATA_DIR, "social.json");
564
+ MARKETING_STRATEGIES_FILE = path2.join(DATA_DIR, "marketing-strategies.json");
565
+ CAMPAIGNS_DIR = path2.join(DATA_DIR, "campaigns");
563
566
  TEMPLATES_DIR = path2.join(__dirname, "..", "..", "templates");
564
567
  COMMAND_TEMPLATES_DIR = path2.join(TEMPLATES_DIR, "commands");
565
568
  }
@@ -3786,12 +3789,12 @@ var init_request_handler_helper = __esm({
3786
3789
  this.req.destroy(new Error("Request timeout."));
3787
3790
  }
3788
3791
  /* Response event handlers */
3789
- classicResponseHandler(resolve3, reject, res) {
3792
+ classicResponseHandler(resolve4, reject, res) {
3790
3793
  this.res = res;
3791
3794
  const dataStream = this.getResponseDataStream(res);
3792
3795
  dataStream.on("data", (chunk) => this.responseData.push(chunk));
3793
- dataStream.on("end", this.onResponseEndHandler.bind(this, resolve3, reject));
3794
- dataStream.on("close", this.onResponseCloseHandler.bind(this, resolve3, reject));
3796
+ dataStream.on("end", this.onResponseEndHandler.bind(this, resolve4, reject));
3797
+ dataStream.on("close", this.onResponseCloseHandler.bind(this, resolve4, reject));
3795
3798
  if (this.requestData.requestEventDebugHandler) {
3796
3799
  this.requestData.requestEventDebugHandler("response", { res });
3797
3800
  res.on("aborted", (error) => this.requestData.requestEventDebugHandler("response-aborted", { error }));
@@ -3800,7 +3803,7 @@ var init_request_handler_helper = __esm({
3800
3803
  res.on("end", () => this.requestData.requestEventDebugHandler("response-end"));
3801
3804
  }
3802
3805
  }
3803
- onResponseEndHandler(resolve3, reject) {
3806
+ onResponseEndHandler(resolve4, reject) {
3804
3807
  const rateLimit = this.getRateLimitFromResponse(this.res);
3805
3808
  let data;
3806
3809
  try {
@@ -3818,18 +3821,18 @@ var init_request_handler_helper = __esm({
3818
3821
  TwitterApiV2Settings.logger.log(`[${this.requestData.options.method} ${this.hrefPathname}]: Request succeeds with code ${this.res.statusCode}`);
3819
3822
  TwitterApiV2Settings.logger.log("Response body:", data);
3820
3823
  }
3821
- resolve3({
3824
+ resolve4({
3822
3825
  data,
3823
3826
  headers: this.res.headers,
3824
3827
  rateLimit
3825
3828
  });
3826
3829
  }
3827
- onResponseCloseHandler(resolve3, reject) {
3830
+ onResponseCloseHandler(resolve4, reject) {
3828
3831
  const res = this.res;
3829
3832
  if (res.aborted) {
3830
3833
  try {
3831
3834
  this.getParsedResponse(this.res);
3832
- return this.onResponseEndHandler(resolve3, reject);
3835
+ return this.onResponseEndHandler(resolve4, reject);
3833
3836
  } catch (e) {
3834
3837
  return reject(this.createPartialResponseError(e, true));
3835
3838
  }
@@ -3838,14 +3841,14 @@ var init_request_handler_helper = __esm({
3838
3841
  return reject(this.createPartialResponseError(new Error("Response has been interrupted before response could be parsed."), true));
3839
3842
  }
3840
3843
  }
3841
- streamResponseHandler(resolve3, reject, res) {
3844
+ streamResponseHandler(resolve4, reject, res) {
3842
3845
  const code = res.statusCode;
3843
3846
  if (code < 400) {
3844
3847
  if (TwitterApiV2Settings.debug) {
3845
3848
  TwitterApiV2Settings.logger.log(`[${this.requestData.options.method} ${this.hrefPathname}]: Request succeeds with code ${res.statusCode} (starting stream)`);
3846
3849
  }
3847
3850
  const dataStream = this.getResponseDataStream(res);
3848
- resolve3({ req: this.req, res: dataStream, originalResponse: res, requestData: this.requestData });
3851
+ resolve4({ req: this.req, res: dataStream, originalResponse: res, requestData: this.requestData });
3849
3852
  } else {
3850
3853
  this.classicResponseHandler(() => void 0, reject, res);
3851
3854
  }
@@ -3903,7 +3906,7 @@ var init_request_handler_helper = __esm({
3903
3906
  makeRequest() {
3904
3907
  this.buildRequest();
3905
3908
  return new Promise((_resolve, _reject) => {
3906
- const resolve3 = (value) => {
3909
+ const resolve4 = (value) => {
3907
3910
  cleanupListener.emit("complete");
3908
3911
  _resolve(value);
3909
3912
  };
@@ -3915,7 +3918,7 @@ var init_request_handler_helper = __esm({
3915
3918
  const req = this.req;
3916
3919
  req.on("error", this.requestErrorHandler.bind(this, reject));
3917
3920
  req.on("socket", this.onSocketEventHandler.bind(this, reject, cleanupListener));
3918
- req.on("response", this.classicResponseHandler.bind(this, resolve3, reject));
3921
+ req.on("response", this.classicResponseHandler.bind(this, resolve4, reject));
3919
3922
  if (this.requestData.options.timeout) {
3920
3923
  req.on("timeout", this.timeoutErrorHandler.bind(this));
3921
3924
  }
@@ -3934,10 +3937,10 @@ var init_request_handler_helper = __esm({
3934
3937
  }
3935
3938
  makeRequestAndResolveWhenReady() {
3936
3939
  this.buildRequest();
3937
- return new Promise((resolve3, reject) => {
3940
+ return new Promise((resolve4, reject) => {
3938
3941
  const req = this.req;
3939
3942
  req.on("error", this.requestErrorHandler.bind(this, reject));
3940
- req.on("response", this.streamResponseHandler.bind(this, resolve3, reject));
3943
+ req.on("response", this.streamResponseHandler.bind(this, resolve4, reject));
3941
3944
  if (this.requestData.body) {
3942
3945
  req.write(this.requestData.body);
3943
3946
  }
@@ -6296,12 +6299,12 @@ var init_client_v1_read = __esm({
6296
6299
  async function readFileIntoBuffer(file) {
6297
6300
  const handle = await getFileHandle(file);
6298
6301
  if (typeof handle === "number") {
6299
- return new Promise((resolve3, reject) => {
6302
+ return new Promise((resolve4, reject) => {
6300
6303
  fs13.readFile(handle, (err2, data) => {
6301
6304
  if (err2) {
6302
6305
  return reject(err2);
6303
6306
  }
6304
- resolve3(data);
6307
+ resolve4(data);
6305
6308
  });
6306
6309
  });
6307
6310
  } else if (handle instanceof Buffer) {
@@ -6325,11 +6328,11 @@ function getFileHandle(file) {
6325
6328
  }
6326
6329
  async function getFileSizeFromFileHandle(fileHandle) {
6327
6330
  if (typeof fileHandle === "number") {
6328
- const stats = await new Promise((resolve3, reject) => {
6331
+ const stats = await new Promise((resolve4, reject) => {
6329
6332
  fs13.fstat(fileHandle, (err2, stats2) => {
6330
6333
  if (err2)
6331
6334
  reject(err2);
6332
- resolve3(stats2);
6335
+ resolve4(stats2);
6333
6336
  });
6334
6337
  });
6335
6338
  return stats.size;
@@ -6406,7 +6409,7 @@ function getMediaCategoryByMime(name, target) {
6406
6409
  return target === "tweet" ? "TweetImage" : "DmImage";
6407
6410
  }
6408
6411
  function sleepSecs(seconds) {
6409
- return new Promise((resolve3) => setTimeout(resolve3, seconds * 1e3));
6412
+ return new Promise((resolve4) => setTimeout(resolve4, seconds * 1e3));
6410
6413
  }
6411
6414
  async function readNextPartOf(file, chunkLength, bufferOffset = 0, buffer) {
6412
6415
  if (file instanceof Buffer) {
@@ -6418,11 +6421,11 @@ async function readNextPartOf(file, chunkLength, bufferOffset = 0, buffer) {
6418
6421
  }
6419
6422
  let bytesRead;
6420
6423
  if (typeof file === "number") {
6421
- bytesRead = await new Promise((resolve3, reject) => {
6424
+ bytesRead = await new Promise((resolve4, reject) => {
6422
6425
  fs13.read(file, buffer, 0, chunkLength, bufferOffset, (err2, nread) => {
6423
6426
  if (err2)
6424
6427
  reject(err2);
6425
- resolve3(nread);
6428
+ resolve4(nread);
6426
6429
  });
6427
6430
  });
6428
6431
  } else {
@@ -8338,7 +8341,7 @@ var init_client_v2_read = __esm({
8338
8341
  if (runningJob.status === "expired" || runningJob.status === "failed") {
8339
8342
  throw new Error("Job failed to be completed.");
8340
8343
  }
8341
- await new Promise((resolve3) => setTimeout(resolve3, 3500));
8344
+ await new Promise((resolve4) => setTimeout(resolve4, 3500));
8342
8345
  runningJob = (await this.complianceJob(job.id)).data;
8343
8346
  }
8344
8347
  const result = await this.get(job.download_url, void 0, {
@@ -8547,7 +8550,7 @@ var init_client_v2_write = __esm({
8547
8550
  case "in_progress": {
8548
8551
  const waitTime = info === null || info === void 0 ? void 0 : info.check_after_secs;
8549
8552
  if (waitTime && waitTime > 0) {
8550
- await new Promise((resolve3) => setTimeout(resolve3, waitTime * 1e3));
8553
+ await new Promise((resolve4) => setTimeout(resolve4, waitTime * 1e3));
8551
8554
  await this.waitForMediaProcessing(mediaId);
8552
8555
  }
8553
8556
  }
@@ -9579,7 +9582,7 @@ function invokeClaudeCLI(options, claudePath) {
9579
9582
  }
9580
9583
  const startTime = Date.now();
9581
9584
  const timeoutMs = options.timeoutMs || 3e5;
9582
- return new Promise((resolve3) => {
9585
+ return new Promise((resolve4) => {
9583
9586
  const child = (0, import_child_process6.spawn)(bin, args2, {
9584
9587
  cwd: options.cwd || process.cwd(),
9585
9588
  env: process.env,
@@ -9599,7 +9602,7 @@ function invokeClaudeCLI(options, claudePath) {
9599
9602
  }
9600
9603
  const timeout = setTimeout(() => {
9601
9604
  child.kill("SIGTERM");
9602
- resolve3({
9605
+ resolve4({
9603
9606
  output: stdout,
9604
9607
  provider: "claude-cli",
9605
9608
  durationMs: Date.now() - startTime,
@@ -9608,7 +9611,7 @@ function invokeClaudeCLI(options, claudePath) {
9608
9611
  }, timeoutMs);
9609
9612
  child.on("close", (code) => {
9610
9613
  clearTimeout(timeout);
9611
- resolve3({
9614
+ resolve4({
9612
9615
  output: stdout.trim(),
9613
9616
  provider: "claude-cli",
9614
9617
  durationMs: Date.now() - startTime,
@@ -9617,7 +9620,7 @@ function invokeClaudeCLI(options, claudePath) {
9617
9620
  });
9618
9621
  child.on("error", (err2) => {
9619
9622
  clearTimeout(timeout);
9620
- resolve3({
9623
+ resolve4({
9621
9624
  output: "",
9622
9625
  provider: "claude-cli",
9623
9626
  durationMs: Date.now() - startTime,
@@ -9865,10 +9868,10 @@ __export(screenshot_exports, {
9865
9868
  captureIdeaScreenshot: () => captureIdeaScreenshot
9866
9869
  });
9867
9870
  async function captureIdeaScreenshot(ideaId, previewUrl, options) {
9868
- const outputDir = options?.outputDir ?? path28.join(DATA_DIR, "reports");
9871
+ const outputDir = options?.outputDir ?? path32.join(DATA_DIR, "reports");
9869
9872
  const timeoutMs = options?.timeoutMs ?? 15e3;
9870
- const outputPath = path28.join(outputDir, `${ideaId}-screenshot.png`);
9871
- if (fs33.existsSync(outputPath)) return outputPath;
9873
+ const outputPath = path32.join(outputDir, `${ideaId}-screenshot.png`);
9874
+ if (fs40.existsSync(outputPath)) return outputPath;
9872
9875
  try {
9873
9876
  const controller = new AbortController();
9874
9877
  const probeTimeout = setTimeout(() => controller.abort(), 3e3);
@@ -9888,7 +9891,7 @@ async function captureIdeaScreenshot(ideaId, previewUrl, options) {
9888
9891
  });
9889
9892
  await page.goto(previewUrl, { waitUntil: "networkidle", timeout: timeoutMs });
9890
9893
  await page.waitForTimeout(500);
9891
- fs33.mkdirSync(outputDir, { recursive: true });
9894
+ fs40.mkdirSync(outputDir, { recursive: true });
9892
9895
  await page.screenshot({ path: outputPath });
9893
9896
  return outputPath;
9894
9897
  } finally {
@@ -9898,12 +9901,12 @@ async function captureIdeaScreenshot(ideaId, previewUrl, options) {
9898
9901
  return null;
9899
9902
  }
9900
9903
  }
9901
- var fs33, path28;
9904
+ var fs40, path32;
9902
9905
  var init_screenshot = __esm({
9903
9906
  "scripts/lib/screenshot.ts"() {
9904
9907
  "use strict";
9905
- fs33 = __toESM(require("fs"));
9906
- path28 = __toESM(require("path"));
9908
+ fs40 = __toESM(require("fs"));
9909
+ path32 = __toESM(require("path"));
9907
9910
  init_paths();
9908
9911
  }
9909
9912
  });
@@ -9915,10 +9918,10 @@ __export(marketing_integration_exports, {
9915
9918
  integrateMarketingPlanToUI: () => integrateMarketingPlanToUI
9916
9919
  });
9917
9920
  async function addMarketingIdeasToDashboard(plan, dataDir) {
9918
- const ideasPath = path29.join(dataDir, "ideas.json");
9921
+ const ideasPath = path33.join(dataDir, "ideas.json");
9919
9922
  let ideasData = { ideas: [] };
9920
- if (fs34.existsSync(ideasPath)) {
9921
- ideasData = JSON.parse(fs34.readFileSync(ideasPath, "utf-8"));
9923
+ if (fs41.existsSync(ideasPath)) {
9924
+ ideasData = JSON.parse(fs41.readFileSync(ideasPath, "utf-8"));
9922
9925
  }
9923
9926
  const newIdeasPartial = generateIdeasFromPlan(plan);
9924
9927
  let added = 0;
@@ -9969,7 +9972,7 @@ async function addMarketingIdeasToDashboard(plan, dataDir) {
9969
9972
  ideasData.ideas.push(newIdea);
9970
9973
  added++;
9971
9974
  }
9972
- fs34.writeFileSync(ideasPath, JSON.stringify(ideasData, null, 2));
9975
+ fs41.writeFileSync(ideasPath, JSON.stringify(ideasData, null, 2));
9973
9976
  return { added, skipped };
9974
9977
  }
9975
9978
  function generateIdeaId() {
@@ -9978,23 +9981,23 @@ function generateIdeaId() {
9978
9981
  return `idea-${timestamp}-${random}`;
9979
9982
  }
9980
9983
  async function integrateMarketingPlanToUI(planId, dataDir) {
9981
- const planPath = path29.join(dataDir, "marketing-plans", `${planId}.json`);
9982
- if (!fs34.existsSync(planPath)) {
9984
+ const planPath = path33.join(dataDir, "marketing-plans", `${planId}.json`);
9985
+ if (!fs41.existsSync(planPath)) {
9983
9986
  throw new Error(`Plan ${planId} not found`);
9984
9987
  }
9985
- const plan = JSON.parse(fs34.readFileSync(planPath, "utf-8"));
9988
+ const plan = JSON.parse(fs41.readFileSync(planPath, "utf-8"));
9986
9989
  const result = await addMarketingIdeasToDashboard(plan, dataDir);
9987
9990
  console.log(`\u2713 Added ${result.added} marketing ideas to dashboard`);
9988
9991
  if (result.skipped > 0) {
9989
9992
  console.log(` (Skipped ${result.skipped} duplicate ideas)`);
9990
9993
  }
9991
9994
  }
9992
- var fs34, path29;
9995
+ var fs41, path33;
9993
9996
  var init_marketing_integration = __esm({
9994
9997
  "scripts/skills/marketing-integration.ts"() {
9995
9998
  "use strict";
9996
- fs34 = __toESM(require("fs"));
9997
- path29 = __toESM(require("path"));
9999
+ fs41 = __toESM(require("fs"));
10000
+ path33 = __toESM(require("path"));
9998
10001
  init_marketing_strategy();
9999
10002
  }
10000
10003
  });
@@ -10011,9 +10014,9 @@ var init_marketing_integration = __esm({
10011
10014
  })();
10012
10015
 
10013
10016
  // scripts/heartbeat.ts
10014
- var fs35 = __toESM(require("fs"));
10015
- var path30 = __toESM(require("path"));
10016
- var import_child_process11 = require("child_process");
10017
+ var fs42 = __toESM(require("fs"));
10018
+ var path34 = __toESM(require("path"));
10019
+ var import_child_process13 = require("child_process");
10017
10020
  var import_util = require("util");
10018
10021
 
10019
10022
  // src/lib/kpi-adapters/rest-api.ts
@@ -13634,8 +13637,8 @@ function argument(predicate, message) {
13634
13637
  }
13635
13638
  }
13636
13639
  var check = { fail, argument, assert: argument };
13637
- function getPathDefinition(glyph, path31) {
13638
- var _path = path31 || new Path();
13640
+ function getPathDefinition(glyph, path35) {
13641
+ var _path = path35 || new Path();
13639
13642
  return {
13640
13643
  configurable: true,
13641
13644
  get: function() {
@@ -13858,9 +13861,9 @@ function ttfGlyphLoader(font, index, parseGlyph2, data, position, buildPath2) {
13858
13861
  var glyph = new Glyph({ index, font });
13859
13862
  glyph.path = function() {
13860
13863
  parseGlyph2(glyph, data, position);
13861
- var path31 = buildPath2(font.glyphs, glyph);
13862
- path31.unitsPerEm = font.unitsPerEm;
13863
- return path31;
13864
+ var path35 = buildPath2(font.glyphs, glyph);
13865
+ path35.unitsPerEm = font.unitsPerEm;
13866
+ return path35;
13864
13867
  };
13865
13868
  defineDependentProperty(glyph, "xMin", "_xMin");
13866
13869
  defineDependentProperty(glyph, "xMax", "_xMax");
@@ -13873,9 +13876,9 @@ function cffGlyphLoader(font, index, parseCFFCharstring2, charstring) {
13873
13876
  return function() {
13874
13877
  var glyph = new Glyph({ index, font });
13875
13878
  glyph.path = function() {
13876
- var path31 = parseCFFCharstring2(font, glyph, charstring);
13877
- path31.unitsPerEm = font.unitsPerEm;
13878
- return path31;
13879
+ var path35 = parseCFFCharstring2(font, glyph, charstring);
13880
+ path35.unitsPerEm = font.unitsPerEm;
13881
+ return path35;
13879
13882
  };
13880
13883
  return glyph;
13881
13884
  };
@@ -30994,8 +30997,8 @@ async function postTweet(client, text) {
30994
30997
  };
30995
30998
  }
30996
30999
  async function uploadMedia(client, filePath) {
30997
- const fs36 = require("fs");
30998
- if (!fs36.existsSync(filePath)) {
31000
+ const fs43 = require("fs");
31001
+ if (!fs43.existsSync(filePath)) {
30999
31002
  throw new Error(`Media file not found: ${filePath}`);
31000
31003
  }
31001
31004
  const mediaId = await client.v1.uploadMedia(filePath);
@@ -33662,7 +33665,7 @@ function readPromoVideoFreshness() {
33662
33665
  var fs23 = __toESM(require("fs"));
33663
33666
  var path17 = __toESM(require("path"));
33664
33667
  init_paths();
33665
- var CAMPAIGNS_DIR = path17.join(DATA_DIR, "campaigns");
33668
+ var CAMPAIGNS_DIR2 = path17.join(DATA_DIR, "campaigns");
33666
33669
  var CAMPAIGN_PREFIXES = [
33667
33670
  "campaign-launch-",
33668
33671
  "campaign-milestone-",
@@ -33699,14 +33702,14 @@ function detectChannels(businessContext) {
33699
33702
  return channels;
33700
33703
  }
33701
33704
  function saveCampaign(campaign) {
33702
- fs23.mkdirSync(CAMPAIGNS_DIR, { recursive: true });
33703
- const filePath = path17.join(CAMPAIGNS_DIR, `${campaign.id}.json`);
33705
+ fs23.mkdirSync(CAMPAIGNS_DIR2, { recursive: true });
33706
+ const filePath = path17.join(CAMPAIGNS_DIR2, `${campaign.id}.json`);
33704
33707
  fs23.writeFileSync(filePath, JSON.stringify(campaign, null, 2) + "\n");
33705
33708
  }
33706
33709
  function loadCampaigns() {
33707
- if (!fs23.existsSync(CAMPAIGNS_DIR)) return [];
33710
+ if (!fs23.existsSync(CAMPAIGNS_DIR2)) return [];
33708
33711
  try {
33709
- return fs23.readdirSync(CAMPAIGNS_DIR).filter((f) => f.endsWith(".json")).map((f) => JSON.parse(fs23.readFileSync(path17.join(CAMPAIGNS_DIR, f), "utf-8"))).sort((a, b) => b.created_at.localeCompare(a.created_at));
33712
+ return fs23.readdirSync(CAMPAIGNS_DIR2).filter((f) => f.endsWith(".json")).map((f) => JSON.parse(fs23.readFileSync(path17.join(CAMPAIGNS_DIR2, f), "utf-8"))).sort((a, b) => b.created_at.localeCompare(a.created_at));
33710
33713
  } catch {
33711
33714
  return [];
33712
33715
  }
@@ -33970,16 +33973,1087 @@ function readCampaignFreshness() {
33970
33973
  };
33971
33974
  }
33972
33975
 
33976
+ // scripts/skills/growth-campaign.ts
33977
+ var fs26 = __toESM(require("fs"));
33978
+ var path20 = __toESM(require("path"));
33979
+ init_paths();
33980
+ init_ai_provider();
33981
+
33982
+ // scripts/lib/growth/campaign-state.ts
33983
+ var fs24 = __toESM(require("fs"));
33984
+ var path18 = __toESM(require("path"));
33985
+ init_paths();
33986
+ function createGrowthCampaign(params) {
33987
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
33988
+ const campaign = {
33989
+ id: `growth-${params.ideaId}-${today}`,
33990
+ type: "growth",
33991
+ source_idea_id: params.ideaId,
33992
+ source_idea_title: params.ideaTitle,
33993
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
33994
+ status: "researching",
33995
+ product: params.product,
33996
+ research: null,
33997
+ channels: params.channels,
33998
+ sub_tasks: params.subTasks,
33999
+ utm_campaign: `growth-${params.ideaId}-${today}`
34000
+ };
34001
+ fs24.mkdirSync(CAMPAIGNS_DIR, { recursive: true });
34002
+ const filePath = path18.join(CAMPAIGNS_DIR, `${campaign.id}.json`);
34003
+ fs24.writeFileSync(filePath, JSON.stringify(campaign, null, 2) + "\n");
34004
+ return campaign;
34005
+ }
34006
+ function loadGrowthCampaignByIdeaId(ideaId) {
34007
+ const campaigns = listGrowthCampaigns();
34008
+ return campaigns.find((c2) => c2.source_idea_id === ideaId) || null;
34009
+ }
34010
+ function updateGrowthCampaign(campaign) {
34011
+ fs24.mkdirSync(CAMPAIGNS_DIR, { recursive: true });
34012
+ const filePath = path18.join(CAMPAIGNS_DIR, `${campaign.id}.json`);
34013
+ fs24.writeFileSync(filePath, JSON.stringify(campaign, null, 2) + "\n");
34014
+ }
34015
+ function listGrowthCampaigns() {
34016
+ if (!fs24.existsSync(CAMPAIGNS_DIR)) return [];
34017
+ try {
34018
+ return fs24.readdirSync(CAMPAIGNS_DIR).filter((f) => f.startsWith("growth-") && f.endsWith(".json")).map((f) => JSON.parse(fs24.readFileSync(path18.join(CAMPAIGNS_DIR, f), "utf-8"))).sort((a, b) => b.created_at.localeCompare(a.created_at));
34019
+ } catch {
34020
+ return [];
34021
+ }
34022
+ }
34023
+ function loadProductInfo() {
34024
+ try {
34025
+ if (!fs24.existsSync(POSITIONING_FILE)) return null;
34026
+ const positioning = JSON.parse(fs24.readFileSync(POSITIONING_FILE, "utf-8"));
34027
+ const current = positioning?.current;
34028
+ if (!current) return null;
34029
+ return {
34030
+ name: current.product_name || current.name || "Unknown",
34031
+ tagline: current.tagline || "",
34032
+ value_proposition: current.value_proposition || current.positioning_statement || "",
34033
+ differentiators: current.differentiators || current.key_differentiators || [],
34034
+ target_audience: current.target_audience || current.ideal_customer || ""
34035
+ };
34036
+ } catch {
34037
+ return null;
34038
+ }
34039
+ }
34040
+ function loadCompetitors() {
34041
+ try {
34042
+ if (!fs24.existsSync(COMPETITORS_FILE)) return [];
34043
+ const data = JSON.parse(fs24.readFileSync(COMPETITORS_FILE, "utf-8"));
34044
+ return data?.competitors || data || [];
34045
+ } catch {
34046
+ return [];
34047
+ }
34048
+ }
34049
+ function loadMarketingStrategies() {
34050
+ try {
34051
+ if (!fs24.existsSync(MARKETING_STRATEGIES_FILE)) return null;
34052
+ return JSON.parse(fs24.readFileSync(MARKETING_STRATEGIES_FILE, "utf-8"));
34053
+ } catch {
34054
+ return null;
34055
+ }
34056
+ }
34057
+ var DEFAULT_CHANNELS = ["twitter_thread", "linkedin", "reddit"];
34058
+ var CHANNEL_SCHEDULE = {
34059
+ twitter_thread: 0,
34060
+ linkedin: 1,
34061
+ reddit: 2,
34062
+ indie_hackers: 3,
34063
+ show_hn: 5,
34064
+ devto: 7,
34065
+ product_hunt: 7
34066
+ };
34067
+ function resolveChannels(ideaTags, marketingStrategies) {
34068
+ const tagChannels = ideaTags.filter((t) => t.startsWith("channel:")).map((t) => t.replace("channel:", "")).filter((c2) => c2 in CHANNEL_SCHEDULE);
34069
+ let channels = tagChannels.length > 0 ? tagChannels : DEFAULT_CHANNELS;
34070
+ if (marketingStrategies) {
34071
+ const excluded = marketingStrategies.excluded_channels || [];
34072
+ const preferred = marketingStrategies.preferred_channels;
34073
+ if (excluded.length > 0) {
34074
+ channels = channels.filter((c2) => !excluded.includes(c2));
34075
+ }
34076
+ if (preferred && preferred.length > 0) {
34077
+ channels.sort((a, b) => {
34078
+ const aP = preferred.includes(a) ? 0 : 1;
34079
+ const bP = preferred.includes(b) ? 0 : 1;
34080
+ return aP - bP;
34081
+ });
34082
+ }
34083
+ }
34084
+ return channels;
34085
+ }
34086
+ function buildChannelDrafts(channels, ideaTags) {
34087
+ return channels.map((channel) => {
34088
+ const draft = {
34089
+ channel,
34090
+ status: "pending",
34091
+ scheduled_day: CHANNEL_SCHEDULE[channel] ?? 0
34092
+ };
34093
+ if (channel === "reddit") {
34094
+ const subTag = ideaTags.find((t) => t.startsWith("subreddit:"));
34095
+ if (subTag) {
34096
+ draft.subreddit = subTag.replace("subreddit:", "");
34097
+ }
34098
+ }
34099
+ return draft;
34100
+ });
34101
+ }
34102
+
34103
+ // scripts/lib/growth/channel-adapters.ts
34104
+ function productContext(product) {
34105
+ return `PRODUCT CONTEXT:
34106
+ - Name: ${product.name}
34107
+ - Tagline: ${product.tagline}
34108
+ - Value Proposition: ${product.value_proposition}
34109
+ - Differentiators: ${product.differentiators.join(", ")}
34110
+ - Target Audience: ${product.target_audience}`;
34111
+ }
34112
+ function researchContext(research) {
34113
+ if (!research) return "RESEARCH: No research data available yet.";
34114
+ const parts = ["RESEARCH DATA:"];
34115
+ if (research.subreddit_research.length > 0) {
34116
+ parts.push("\nSubreddits:");
34117
+ for (const sub of research.subreddit_research) {
34118
+ parts.push(`- r/${sub.name}: ${sub.subscribers ?? "?"} subscribers, self-promo: ${sub.self_promo_policy}`);
34119
+ if (sub.top_post_patterns.length > 0) {
34120
+ parts.push(` Top patterns: ${sub.top_post_patterns.join("; ")}`);
34121
+ }
34122
+ }
34123
+ }
34124
+ if (research.keyword_research.length > 0) {
34125
+ parts.push("\nKeywords:");
34126
+ for (const kw of research.keyword_research) {
34127
+ parts.push(`- "${kw.keyword}": volume=${kw.volume || "?"}, CPC=${kw.cpc || "?"}, competition=${kw.competition || "?"}`);
34128
+ }
34129
+ }
34130
+ if (research.hn_research) {
34131
+ parts.push("\nHacker News:");
34132
+ parts.push(`- Similar posts: ${research.hn_research.successful_similar_posts.join("; ")}`);
34133
+ parts.push(`- Upvote patterns: ${research.hn_research.upvote_patterns}`);
34134
+ }
34135
+ if (research.competitor_content.length > 0) {
34136
+ parts.push("\nCompetitor Content:");
34137
+ for (const cc of research.competitor_content) {
34138
+ parts.push(`- ${cc.competitor}: ${cc.content_examples.slice(0, 2).join("; ")}`);
34139
+ }
34140
+ }
34141
+ if (research.trending_topics.length > 0) {
34142
+ parts.push(`
34143
+ Trending: ${research.trending_topics.join(", ")}`);
34144
+ }
34145
+ return parts.join("\n");
34146
+ }
34147
+ function buildShowHNPrompt(ctx) {
34148
+ return `You are writing a Show HN post for Hacker News.
34149
+
34150
+ ${productContext(ctx.product)}
34151
+
34152
+ IDEA CONTEXT:
34153
+ - Title: ${ctx.idea.title}
34154
+ - Summary: ${ctx.idea.summary}
34155
+ - Rationale: ${ctx.idea.rationale}
34156
+
34157
+ ${researchContext(ctx.research)}
34158
+
34159
+ HACKER NEWS RULES (CRITICAL \u2014 follow exactly):
34160
+ 1. Title MUST start with "Show HN: " followed by a concise description (max 80 chars total)
34161
+ 2. The HN post body should be 2-4 short paragraphs explaining what you built and why
34162
+ 3. Be technical and honest \u2014 HN readers detect and punish marketing speak instantly
34163
+ 4. Acknowledge limitations openly \u2014 this builds trust
34164
+ 5. Explain the technical approach briefly
34165
+ 6. No exclamation marks, no superlatives, no "revolutionary/game-changing"
34166
+ 7. End with a question to invite discussion (e.g., "Curious what you think about X approach")
34167
+
34168
+ FIRST COMMENT (CRITICAL):
34169
+ - Write a 500-800 character "maker comment" that goes deeper
34170
+ - Share the backstory: why you built this, what problem you hit
34171
+ - Include one specific technical detail or metric
34172
+ - Be genuine and conversational
34173
+
34174
+ OUTPUT FORMAT (respond with valid JSON only):
34175
+ {
34176
+ "title": "Show HN: ...",
34177
+ "body": "The HN post body text",
34178
+ "first_comment": "The maker's first comment",
34179
+ "cta": "A subtle link/reference, NOT a hard sell",
34180
+ "utm_url": "${ctx.productUrl || "https://yoursite.com"}?utm_source=hackernews&utm_medium=show_hn&utm_campaign=${ctx.utmCampaign}",
34181
+ "platform_constraints": "Title max 80 chars. No marketing speak. Technical honesty required."
34182
+ }`;
34183
+ }
34184
+ function buildRedditPrompt(ctx, subreddit) {
34185
+ const subName = subreddit || "relevant subreddit";
34186
+ const subResearch = ctx.research?.subreddit_research.find(
34187
+ (s) => s.name.toLowerCase() === (subreddit || "").toLowerCase()
34188
+ );
34189
+ let subRules = "";
34190
+ if (subResearch) {
34191
+ subRules = `
34192
+ SUBREDDIT RESEARCH for r/${subResearch.name}:
34193
+ - Subscribers: ${subResearch.subscribers ?? "unknown"}
34194
+ - Self-promo policy: ${subResearch.self_promo_policy}
34195
+ - Rules: ${subResearch.rules_summary}
34196
+ - Top post patterns: ${subResearch.top_post_patterns.join("; ")}`;
34197
+ }
34198
+ return `You are writing a Reddit post for r/${subName}.
34199
+
34200
+ ${productContext(ctx.product)}
34201
+
34202
+ IDEA CONTEXT:
34203
+ - Title: ${ctx.idea.title}
34204
+ - Summary: ${ctx.idea.summary}
34205
+ - Rationale: ${ctx.idea.rationale}
34206
+
34207
+ ${researchContext(ctx.research)}
34208
+ ${subRules}
34209
+
34210
+ REDDIT POST RULES (CRITICAL):
34211
+ 1. Title should be a question or story hook \u2014 NOT a product announcement
34212
+ 2. Lead with the problem/pain you experienced, not the solution
34213
+ 3. Provide genuine value FIRST: share insights, data, or a useful approach
34214
+ 4. Mention your product only once, near the end, as "I built X to solve this"
34215
+ 5. Match the subreddit's culture \u2014 read the top posts for tone
34216
+ 6. Use paragraphs (Reddit markdown), not walls of text
34217
+ 7. If the subreddit bans self-promotion, frame as asking for feedback instead
34218
+ 8. Be a community member first, promoter never
34219
+
34220
+ OUTPUT FORMAT (respond with valid JSON only):
34221
+ {
34222
+ "title": "Post title (question or story hook)",
34223
+ "body": "Full post body with Reddit markdown formatting",
34224
+ "cta": "Soft mention of the product, woven into the story",
34225
+ "utm_url": "${ctx.productUrl || "https://yoursite.com"}?utm_source=reddit&utm_medium=r_${subreddit || "post"}&utm_campaign=${ctx.utmCampaign}",
34226
+ "platform_constraints": "Community-first. Value before promotion. Match subreddit tone."
34227
+ }`;
34228
+ }
34229
+ function buildLinkedInPrompt(ctx) {
34230
+ return `You are writing a LinkedIn post.
34231
+
34232
+ ${productContext(ctx.product)}
34233
+
34234
+ IDEA CONTEXT:
34235
+ - Title: ${ctx.idea.title}
34236
+ - Summary: ${ctx.idea.summary}
34237
+ - Rationale: ${ctx.idea.rationale}
34238
+
34239
+ ${researchContext(ctx.research)}
34240
+
34241
+ LINKEDIN ALGORITHM KNOWLEDGE (maximize reach):
34242
+ 1. First line = hook \u2014 must stop the scroll. Use a bold claim, surprising stat, or contrarian take
34243
+ 2. Use short paragraphs (1-2 sentences each) with blank lines between
34244
+ 3. Whitespace is your friend \u2014 dense text gets scrolled past
34245
+ 4. Total length: 1200-1500 characters (sweet spot for engagement)
34246
+ 5. End with a question to drive comments (comments > likes for reach)
34247
+ 6. No external links in the post body (kills reach by 50%)
34248
+ 7. Put the link in the first comment instead
34249
+ 8. Use "I" statements \u2014 personal stories outperform corporate announcements
34250
+ 9. Optional: use a list format (numbered insights) for scanability
34251
+ 10. No hashtags in the post (they look spammy and don't help anymore)
34252
+
34253
+ OUTPUT FORMAT (respond with valid JSON only):
34254
+ {
34255
+ "title": "Not used on LinkedIn, but store the hook line here",
34256
+ "body": "The full LinkedIn post text, formatted with line breaks",
34257
+ "cta": "Question to ask at the end + note to put link in first comment",
34258
+ "utm_url": "${ctx.productUrl || "https://yoursite.com"}?utm_source=linkedin&utm_medium=post&utm_campaign=${ctx.utmCampaign}",
34259
+ "platform_constraints": "No links in body. 1200-1500 chars. Hook first line. End with question."
34260
+ }`;
34261
+ }
34262
+ function buildTwitterThreadPrompt(ctx) {
34263
+ return `You are writing a Twitter/X thread.
34264
+
34265
+ ${productContext(ctx.product)}
34266
+
34267
+ IDEA CONTEXT:
34268
+ - Title: ${ctx.idea.title}
34269
+ - Summary: ${ctx.idea.summary}
34270
+ - Rationale: ${ctx.idea.rationale}
34271
+
34272
+ ${researchContext(ctx.research)}
34273
+
34274
+ TWITTER THREAD RULES (maximize engagement):
34275
+ 1. Tweet 1 (hook): Must be a standalone banger. Bold claim, surprising data, or contrarian take. Max 280 chars.
34276
+ 2. Thread length: 5-7 tweets total (sweet spot \u2014 shorter feels thin, longer loses readers)
34277
+ 3. Each tweet should be self-contained AND flow into the next
34278
+ 4. Use numbers and data where possible \u2014 "We increased X by 47%" > "We improved X"
34279
+ 5. Tweet structure: Hook \u2192 Problem \u2192 Insight 1 \u2192 Insight 2 \u2192 How \u2192 Result \u2192 CTA
34280
+ 6. Last tweet = CTA with link. "If you want to try this: [link]" or "Follow me for more on X"
34281
+ 7. No thread-style numbering (1/7, 2/7) \u2014 it looks robotic
34282
+ 8. Use line breaks within tweets for readability
34283
+ 9. Optional: one tweet can be an image/screenshot placeholder "[screenshot of dashboard]"
34284
+
34285
+ OUTPUT FORMAT (respond with valid JSON only):
34286
+ {
34287
+ "body": "Tweet 1 text (the hook)",
34288
+ "thread_tweets": ["Tweet 2", "Tweet 3", "Tweet 4", "Tweet 5", "Tweet 6 (CTA)"],
34289
+ "cta": "The CTA text from the last tweet",
34290
+ "utm_url": "${ctx.productUrl || "https://yoursite.com"}?utm_source=twitter&utm_medium=thread&utm_campaign=${ctx.utmCampaign}",
34291
+ "platform_constraints": "Max 280 chars per tweet. 5-7 tweets. Hook tweet must stand alone."
34292
+ }`;
34293
+ }
34294
+ function buildIndieHackersPrompt(ctx) {
34295
+ return `You are writing an Indie Hackers post.
34296
+
34297
+ ${productContext(ctx.product)}
34298
+
34299
+ IDEA CONTEXT:
34300
+ - Title: ${ctx.idea.title}
34301
+ - Summary: ${ctx.idea.summary}
34302
+ - Rationale: ${ctx.idea.rationale}
34303
+
34304
+ ${researchContext(ctx.research)}
34305
+
34306
+ INDIE HACKERS RULES:
34307
+ 1. IH audience = founders, solopreneurs, bootstrappers. They want REAL numbers.
34308
+ 2. Use milestone format: "How I [achieved X] \u2014 the full breakdown"
34309
+ 3. Share real metrics: revenue, users, conversion rates, time spent
34310
+ 4. Be transparent about what DIDN'T work \u2014 this builds massive trust
34311
+ 5. Structure: Background \u2192 What I tried \u2192 What worked \u2192 Metrics \u2192 Lessons \u2192 What's next
34312
+ 6. Length: 1500-2500 words (IH readers love detail)
34313
+ 7. End with specific lessons (numbered list) and a question for the community
34314
+ 8. Don't be afraid to show vulnerability \u2014 "I almost gave up when..."
34315
+
34316
+ OUTPUT FORMAT (respond with valid JSON only):
34317
+ {
34318
+ "title": "How I [achieved result] \u2014 [compelling detail]",
34319
+ "body": "Full post body with markdown formatting",
34320
+ "cta": "Question for the community + soft product mention",
34321
+ "utm_url": "${ctx.productUrl || "https://yoursite.com"}?utm_source=indiehackers&utm_medium=post&utm_campaign=${ctx.utmCampaign}",
34322
+ "platform_constraints": "Milestone format. Real numbers required. 1500-2500 words. Transparency wins."
34323
+ }`;
34324
+ }
34325
+ function buildDevToPrompt(ctx) {
34326
+ return `You are writing a Dev.to article.
34327
+
34328
+ ${productContext(ctx.product)}
34329
+
34330
+ IDEA CONTEXT:
34331
+ - Title: ${ctx.idea.title}
34332
+ - Summary: ${ctx.idea.summary}
34333
+ - Rationale: ${ctx.idea.rationale}
34334
+ - Implementation plan: ${ctx.idea.implementation_plan}
34335
+
34336
+ ${researchContext(ctx.research)}
34337
+
34338
+ DEV.TO RULES:
34339
+ 1. Dev.to audience = developers. They want to LEARN something.
34340
+ 2. Frame as a tutorial or technical deep-dive, not a product announcement
34341
+ 3. Title format: "How to [solve problem] with [approach]" or "Building [thing]: [technical detail]"
34342
+ 4. Length: 2000-3000 words with code examples
34343
+ 5. Include architecture decisions and trade-offs ("We chose X over Y because...")
34344
+ 6. Use proper markdown: headers, code blocks with syntax highlighting, lists
34345
+ 7. Add a cover image suggestion and 3-4 relevant tags
34346
+ 8. Front matter should include: title, published, description, tags, cover_image
34347
+ 9. End with "Next Steps" and links to further reading
34348
+
34349
+ OUTPUT FORMAT (respond with valid JSON only):
34350
+ {
34351
+ "title": "How to [approach] \u2014 [benefit]",
34352
+ "body": "Full article body with markdown, code blocks, and headers",
34353
+ "cta": "Next steps section with product link woven in naturally",
34354
+ "utm_url": "${ctx.productUrl || "https://yoursite.com"}?utm_source=devto&utm_medium=article&utm_campaign=${ctx.utmCampaign}",
34355
+ "platform_constraints": "Tutorial format. 2000-3000 words. Code examples required. Dev audience."
34356
+ }`;
34357
+ }
34358
+ function buildProductHuntPrompt(ctx) {
34359
+ return `You are preparing a Product Hunt launch.
34360
+
34361
+ ${productContext(ctx.product)}
34362
+
34363
+ IDEA CONTEXT:
34364
+ - Title: ${ctx.idea.title}
34365
+ - Summary: ${ctx.idea.summary}
34366
+ - Rationale: ${ctx.idea.rationale}
34367
+
34368
+ ${researchContext(ctx.research)}
34369
+
34370
+ PRODUCT HUNT RULES:
34371
+ 1. Tagline: max 60 characters. Benefit-focused, not feature-focused.
34372
+ 2. Description: max 260 characters. What it does + who it's for.
34373
+ 3. First comment (maker comment): 500-800 chars. Story of why you built it.
34374
+ 4. Suggest 5 gallery image concepts (screenshots, diagrams, before/after)
34375
+ 5. Choose 3 topics/categories that fit
34376
+ 6. Hunter's note: write as if someone else is featuring your product
34377
+ 7. Timing: launch at 12:01 AM PST for maximum visibility
34378
+
34379
+ OUTPUT FORMAT (respond with valid JSON only):
34380
+ {
34381
+ "title": "${ctx.product.name}",
34382
+ "body": "260-char description",
34383
+ "first_comment": "Maker's first comment (500-800 chars)",
34384
+ "cta": "60-char tagline",
34385
+ "utm_url": "${ctx.productUrl || "https://yoursite.com"}?utm_source=producthunt&utm_medium=launch&utm_campaign=${ctx.utmCampaign}",
34386
+ "platform_constraints": "Tagline max 60 chars. Description max 260 chars. Launch at 12:01 AM PST.",
34387
+ "gallery_suggestions": ["Suggestion 1", "Suggestion 2", "Suggestion 3", "Suggestion 4", "Suggestion 5"],
34388
+ "topics": ["Topic 1", "Topic 2", "Topic 3"]
34389
+ }`;
34390
+ }
34391
+ function getChannelPrompt(channel, ctx, subreddit) {
34392
+ switch (channel) {
34393
+ case "show_hn":
34394
+ return buildShowHNPrompt(ctx);
34395
+ case "reddit":
34396
+ return buildRedditPrompt(ctx, subreddit);
34397
+ case "linkedin":
34398
+ return buildLinkedInPrompt(ctx);
34399
+ case "twitter_thread":
34400
+ return buildTwitterThreadPrompt(ctx);
34401
+ case "indie_hackers":
34402
+ return buildIndieHackersPrompt(ctx);
34403
+ case "devto":
34404
+ return buildDevToPrompt(ctx);
34405
+ case "product_hunt":
34406
+ return buildProductHuntPrompt(ctx);
34407
+ default:
34408
+ return buildLinkedInPrompt(ctx);
34409
+ }
34410
+ }
34411
+
34412
+ // scripts/lib/growth/research.ts
34413
+ var import_child_process10 = require("child_process");
34414
+ init_paths();
34415
+ function executeGrowthResearch(params) {
34416
+ const channelNames = params.channels.map((c2) => c2.channel);
34417
+ const subreddits = params.channels.filter((c2) => c2.channel === "reddit" && c2.subreddit).map((c2) => c2.subreddit);
34418
+ const prompt = buildResearchPrompt({
34419
+ product: params.product,
34420
+ channels: channelNames,
34421
+ subreddits,
34422
+ ideaTitle: params.ideaTitle,
34423
+ ideaSummary: params.ideaSummary,
34424
+ competitors: params.competitors
34425
+ });
34426
+ const result = (0, import_child_process10.spawnSync)("claude", [
34427
+ "--print",
34428
+ "--dangerously-skip-permissions",
34429
+ "--allowedTools",
34430
+ "WebSearch WebFetch",
34431
+ "--output-format",
34432
+ "text"
34433
+ ], {
34434
+ cwd: PROJECT_DIR,
34435
+ input: prompt,
34436
+ encoding: "utf-8",
34437
+ timeout: 3e5,
34438
+ stdio: ["pipe", "pipe", "pipe"],
34439
+ env: process.env
34440
+ });
34441
+ if (result.status !== 0) {
34442
+ console.error(`[growth-research] Claude Code exited with code ${result.status}`);
34443
+ console.error(`[growth-research] stderr: ${(result.stderr || "").slice(-500)}`);
34444
+ return null;
34445
+ }
34446
+ const output = result.stdout || result.stderr || "";
34447
+ const jsonMatch = output.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/) || [null, output];
34448
+ const jsonStr = (jsonMatch[1] || output).trim();
34449
+ try {
34450
+ const parsed = JSON.parse(jsonStr);
34451
+ parsed.completed_at = (/* @__PURE__ */ new Date()).toISOString();
34452
+ return parsed;
34453
+ } catch {
34454
+ const objMatch = output.match(/\{[\s\S]*\}/);
34455
+ if (objMatch) {
34456
+ try {
34457
+ const parsed = JSON.parse(objMatch[0]);
34458
+ parsed.completed_at = (/* @__PURE__ */ new Date()).toISOString();
34459
+ return parsed;
34460
+ } catch {
34461
+ console.error("[growth-research] Failed to parse research output as JSON");
34462
+ console.error("[growth-research] Raw output:", output.slice(-1e3));
34463
+ return null;
34464
+ }
34465
+ }
34466
+ console.error("[growth-research] No JSON found in output");
34467
+ return null;
34468
+ }
34469
+ }
34470
+ function buildResearchPrompt(params) {
34471
+ const parts = [];
34472
+ parts.push(`You are a growth researcher. Gather REAL data using WebSearch for a marketing campaign.
34473
+
34474
+ PRODUCT: ${params.product.name} \u2014 ${params.product.tagline}
34475
+ VALUE PROP: ${params.product.value_proposition}
34476
+ TARGET AUDIENCE: ${params.product.target_audience}
34477
+
34478
+ CAMPAIGN IDEA: ${params.ideaTitle}
34479
+ ${params.ideaSummary}
34480
+
34481
+ TARGET CHANNELS: ${params.channels.join(", ")}
34482
+ `);
34483
+ if (params.channels.includes("reddit") || params.subreddits.length > 0) {
34484
+ const subs = params.subreddits.length > 0 ? params.subreddits.join(", ") : "Find 2-3 relevant subreddits for this product";
34485
+ parts.push(`REDDIT RESEARCH:
34486
+ - Search for subreddits related to "${params.product.target_audience}"
34487
+ - Target subreddits: ${subs}
34488
+ - For each subreddit: find subscriber count, self-promotion rules, top post patterns
34489
+ - Search: "site:reddit.com ${params.product.name}" to see existing mentions`);
34490
+ }
34491
+ if (params.channels.includes("show_hn")) {
34492
+ parts.push(`HACKER NEWS RESEARCH:
34493
+ - Search: "site:news.ycombinator.com Show HN ${params.product.name}" or similar products
34494
+ - Find 3-5 successful Show HN posts in the same category
34495
+ - Note their titles, upvote counts, and first comment patterns
34496
+ - Check what time of day successful posts were submitted`);
34497
+ }
34498
+ if (params.competitors.length > 0) {
34499
+ const compNames = params.competitors.map((c2) => c2.name || String(c2)).slice(0, 5);
34500
+ parts.push(`COMPETITOR RESEARCH:
34501
+ - Competitors: ${compNames.join(", ")}
34502
+ - Search for their recent social media posts and engagement
34503
+ - Find what content angles work for them`);
34504
+ }
34505
+ parts.push(`KEYWORD RESEARCH:
34506
+ - Find 3-5 keywords related to: "${params.ideaTitle}"
34507
+ - Look for search volume estimates, competition level
34508
+ - Identify high-intent keywords for this audience`);
34509
+ parts.push(`TRENDING TOPICS:
34510
+ - Search for current trends related to: ${params.product.target_audience}
34511
+ - Find trending topics on Twitter/X, Reddit, HN that could be leveraged`);
34512
+ parts.push(`OUTPUT FORMAT \u2014 respond with ONLY valid JSON, no other text:
34513
+ {
34514
+ "completed_at": "${(/* @__PURE__ */ new Date()).toISOString()}",
34515
+ "subreddit_research": [
34516
+ {
34517
+ "name": "subreddit_name",
34518
+ "subscribers": 100000,
34519
+ "rules_summary": "Key rules...",
34520
+ "self_promo_policy": "Allowed on Saturdays / Never / With flair",
34521
+ "top_post_patterns": ["Pattern 1", "Pattern 2"]
34522
+ }
34523
+ ],
34524
+ "keyword_research": [
34525
+ {
34526
+ "keyword": "example keyword",
34527
+ "volume": "1K-10K monthly",
34528
+ "cpc": "$2.50",
34529
+ "competition": "medium",
34530
+ "intent": "commercial"
34531
+ }
34532
+ ],
34533
+ "hn_research": {
34534
+ "successful_similar_posts": ["Show HN: Product (500 pts)", "Show HN: Similar (200 pts)"],
34535
+ "upvote_patterns": "Best posted Tue-Thu, 8-10am PST",
34536
+ "first_comment_patterns": "Technical backstory + specific metrics"
34537
+ },
34538
+ "competitor_content": [
34539
+ {
34540
+ "competitor": "Competitor Name",
34541
+ "content_examples": ["They posted X on LinkedIn (500 likes)", "Their blog on Y"],
34542
+ "engagement_level": "high"
34543
+ }
34544
+ ],
34545
+ "trending_topics": ["Topic 1 trending on X", "Topic 2 popular on HN"]
34546
+ }`);
34547
+ return parts.join("\n\n");
34548
+ }
34549
+
34550
+ // src/lib/social-storage.ts
34551
+ var import_fs = require("fs");
34552
+ var import_path = __toESM(require("path"));
34553
+ function getDataDir() {
34554
+ return process.env.AI_ANALYST_DATA_DIR || import_path.default.join(process.cwd(), "data");
34555
+ }
34556
+ function getSocialFilePath() {
34557
+ return import_path.default.join(getDataDir(), "social.json");
34558
+ }
34559
+ var EMPTY_STATE4 = {
34560
+ last_updated: (/* @__PURE__ */ new Date()).toISOString(),
34561
+ platform: "x",
34562
+ username: null,
34563
+ credentials_valid: false,
34564
+ drafts: [],
34565
+ metrics: {
34566
+ followers_count: null,
34567
+ post_streak_days: 0,
34568
+ total_posts: 0,
34569
+ last_post_date: null
34570
+ }
34571
+ };
34572
+ async function readState() {
34573
+ try {
34574
+ const content = await import_fs.promises.readFile(getSocialFilePath(), "utf-8");
34575
+ return JSON.parse(content);
34576
+ } catch {
34577
+ return { ...EMPTY_STATE4, drafts: [], metrics: { ...EMPTY_STATE4.metrics } };
34578
+ }
34579
+ }
34580
+ async function writeState(state) {
34581
+ state.last_updated = (/* @__PURE__ */ new Date()).toISOString();
34582
+ const dir = getDataDir();
34583
+ try {
34584
+ await import_fs.promises.access(dir);
34585
+ } catch {
34586
+ await import_fs.promises.mkdir(dir, { recursive: true });
34587
+ }
34588
+ await import_fs.promises.writeFile(getSocialFilePath(), JSON.stringify(state, null, 2), "utf-8");
34589
+ }
34590
+ async function createSocialDraft(text, opts) {
34591
+ const state = await readState();
34592
+ const isThread = !!(opts?.threadTweets && opts.threadTweets.length > 0);
34593
+ const draft = {
34594
+ id: `draft-${v4_default().slice(0, 8)}`,
34595
+ text,
34596
+ source: isThread ? "thread" : "manual",
34597
+ source_ref: null,
34598
+ status: "draft",
34599
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
34600
+ published_at: null,
34601
+ tweet_id: null,
34602
+ tweet_url: null,
34603
+ is_thread: isThread,
34604
+ thread_tweets: opts?.threadTweets
34605
+ };
34606
+ state.drafts.push(draft);
34607
+ await writeState(state);
34608
+ return draft;
34609
+ }
34610
+
34611
+ // scripts/skills/growth-campaign.ts
34612
+ var GROWTH_PREFIXES = [
34613
+ "growth-activate-",
34614
+ "growth-research-",
34615
+ "growth-draft-",
34616
+ "growth-publish-",
34617
+ "growth-check-status"
34618
+ ];
34619
+ function isGrowthCampaignTask(taskId) {
34620
+ return GROWTH_PREFIXES.some((prefix) => taskId.startsWith(prefix));
34621
+ }
34622
+ function appendToTodo2(tasks) {
34623
+ let content = "";
34624
+ if (fs26.existsSync(TODO_FILE)) {
34625
+ content = fs26.readFileSync(TODO_FILE, "utf-8");
34626
+ }
34627
+ const newLines = tasks.map(
34628
+ (t) => `- [ ] **${t.taskId}** \u2014 ${t.description}`
34629
+ ).join("\n");
34630
+ if (content.includes("## High Priority")) {
34631
+ const idx = content.indexOf("## High Priority");
34632
+ const nextSection = content.indexOf("\n## ", idx + 1);
34633
+ const insertPos = nextSection > 0 ? nextSection : content.length;
34634
+ content = content.slice(0, insertPos) + "\n" + newLines + "\n" + content.slice(insertPos);
34635
+ } else {
34636
+ content += `
34637
+ ## High Priority
34638
+ ${newLines}
34639
+ `;
34640
+ }
34641
+ fs26.writeFileSync(TODO_FILE, content);
34642
+ }
34643
+ async function executeGrowthCampaignTask(taskId, description, businessContext) {
34644
+ try {
34645
+ if (taskId.startsWith("growth-activate-")) {
34646
+ const ideaId = taskId.replace("growth-activate-", "");
34647
+ return executeActivate(ideaId);
34648
+ }
34649
+ if (taskId.startsWith("growth-research-")) {
34650
+ const ideaId = taskId.replace("growth-research-", "");
34651
+ return executeResearch(ideaId);
34652
+ }
34653
+ if (taskId.startsWith("growth-draft-")) {
34654
+ const rest = taskId.replace("growth-draft-", "");
34655
+ const dashIdx = rest.indexOf("-");
34656
+ if (dashIdx === -1) return { success: false, output: `Invalid draft task ID: ${taskId}` };
34657
+ const ideaId = rest.slice(0, dashIdx);
34658
+ const channel = rest.slice(dashIdx + 1);
34659
+ return await executeDraft(ideaId, channel);
34660
+ }
34661
+ if (taskId.startsWith("growth-publish-")) {
34662
+ const rest = taskId.replace("growth-publish-", "");
34663
+ const dashIdx = rest.indexOf("-");
34664
+ if (dashIdx === -1) return { success: false, output: `Invalid publish task ID: ${taskId}` };
34665
+ const ideaId = rest.slice(0, dashIdx);
34666
+ const channel = rest.slice(dashIdx + 1);
34667
+ return await executePublish2(ideaId, channel);
34668
+ }
34669
+ if (taskId === "growth-check-status") {
34670
+ return executeCheckStatus6();
34671
+ }
34672
+ return { success: false, output: `Unknown growth campaign task: ${taskId}` };
34673
+ } catch (error) {
34674
+ const errMsg = error instanceof Error ? error.message : String(error);
34675
+ return { success: false, output: `Growth campaign task failed: ${errMsg}` };
34676
+ }
34677
+ }
34678
+ function executeActivate(ideaId) {
34679
+ if (!ideaId) {
34680
+ return { success: false, output: "No idea ID provided" };
34681
+ }
34682
+ let idea;
34683
+ try {
34684
+ const ideasData = JSON.parse(fs26.readFileSync(IDEAS_FILE, "utf-8"));
34685
+ idea = ideasData.ideas.find((i) => i.id === ideaId);
34686
+ } catch {
34687
+ return { success: false, output: `Failed to load ideas from ${IDEAS_FILE}` };
34688
+ }
34689
+ if (!idea) {
34690
+ return { success: false, output: `Idea not found: ${ideaId}` };
34691
+ }
34692
+ if (idea.category !== "growth") {
34693
+ return { success: false, output: `Idea ${ideaId} is not a growth idea (category: ${idea.category})` };
34694
+ }
34695
+ if (idea.stage !== "approved") {
34696
+ return { success: false, output: `Idea ${ideaId} is not approved (stage: ${idea.stage})` };
34697
+ }
34698
+ const existing = loadGrowthCampaignByIdeaId(ideaId);
34699
+ if (existing && existing.status !== "completed") {
34700
+ return { success: false, output: `Active growth campaign already exists for ${ideaId}: ${existing.id}` };
34701
+ }
34702
+ const product = loadProductInfo();
34703
+ if (!product) {
34704
+ return { success: false, output: 'No positioning data found. Run "vibebusiness analyze --type=research" first.' };
34705
+ }
34706
+ const marketingStrategies = loadMarketingStrategies();
34707
+ const channels = resolveChannels(idea.tags, marketingStrategies);
34708
+ if (channels.length === 0) {
34709
+ return { success: false, output: "No target channels resolved. Add channel:xxx tags or configure marketing strategies." };
34710
+ }
34711
+ const channelDrafts = buildChannelDrafts(channels, idea.tags);
34712
+ const subTasks = [];
34713
+ subTasks.push({
34714
+ task_id: `growth-research-${ideaId}`,
34715
+ description: `Research channels and competitors for "${idea.title}"`,
34716
+ status: "queued",
34717
+ depends_on: []
34718
+ });
34719
+ for (const ch of channelDrafts) {
34720
+ const channelLabel = ch.subreddit ? `${ch.channel}_${ch.subreddit}` : ch.channel;
34721
+ subTasks.push({
34722
+ task_id: `growth-draft-${ideaId}-${channelLabel}`,
34723
+ description: `Draft ${ch.channel} content for "${idea.title}"`,
34724
+ status: "queued",
34725
+ depends_on: [`growth-research-${ideaId}`]
34726
+ });
34727
+ }
34728
+ for (const ch of channelDrafts) {
34729
+ const channelLabel = ch.subreddit ? `${ch.channel}_${ch.subreddit}` : ch.channel;
34730
+ subTasks.push({
34731
+ task_id: `growth-publish-${ideaId}-${channelLabel}`,
34732
+ description: `Publish ${ch.channel} content for "${idea.title}" (Day ${ch.scheduled_day})`,
34733
+ status: "queued",
34734
+ depends_on: [`growth-draft-${ideaId}-${channelLabel}`]
34735
+ });
34736
+ }
34737
+ const campaign = createGrowthCampaign({
34738
+ ideaId,
34739
+ ideaTitle: idea.title,
34740
+ channels: channelDrafts,
34741
+ subTasks,
34742
+ product
34743
+ });
34744
+ try {
34745
+ const ideasData = JSON.parse(fs26.readFileSync(IDEAS_FILE, "utf-8"));
34746
+ const ideaIdx = ideasData.ideas.findIndex((i) => i.id === ideaId);
34747
+ if (ideaIdx >= 0) {
34748
+ ideasData.ideas[ideaIdx].stage = "in_progress";
34749
+ ideasData.ideas[ideaIdx].updated_at = (/* @__PURE__ */ new Date()).toISOString();
34750
+ fs26.writeFileSync(IDEAS_FILE, JSON.stringify(ideasData, null, 2) + "\n");
34751
+ }
34752
+ } catch {
34753
+ }
34754
+ appendToTodo2([
34755
+ {
34756
+ taskId: `growth-research-${ideaId}`,
34757
+ description: `Research channels and competitors for "${idea.title}"`
34758
+ }
34759
+ ]);
34760
+ const lines = [
34761
+ `Growth campaign created: ${campaign.id}`,
34762
+ `Channels: ${channels.join(", ")}`,
34763
+ `Sub-tasks queued: ${subTasks.length}`,
34764
+ "",
34765
+ "Next: growth-research will run on the next heartbeat.",
34766
+ "Then: drafts and publishing will follow on subsequent heartbeats."
34767
+ ];
34768
+ return { success: true, output: lines.join("\n") };
34769
+ }
34770
+ function executeResearch(ideaId) {
34771
+ const campaign = loadGrowthCampaignByIdeaId(ideaId);
34772
+ if (!campaign) {
34773
+ return { success: false, output: `No growth campaign found for idea ${ideaId}` };
34774
+ }
34775
+ const competitors = loadCompetitors();
34776
+ let idea;
34777
+ try {
34778
+ const ideasData = JSON.parse(fs26.readFileSync(IDEAS_FILE, "utf-8"));
34779
+ idea = ideasData.ideas.find((i) => i.id === ideaId);
34780
+ } catch {
34781
+ }
34782
+ const research = executeGrowthResearch({
34783
+ product: campaign.product,
34784
+ channels: campaign.channels,
34785
+ ideaTitle: idea?.title || campaign.source_idea_title,
34786
+ ideaSummary: idea?.summary || "",
34787
+ competitors
34788
+ });
34789
+ if (!research) {
34790
+ return { success: false, output: "Research failed \u2014 Claude Code did not return valid JSON." };
34791
+ }
34792
+ campaign.research = research;
34793
+ campaign.status = "drafting";
34794
+ const researchTask = campaign.sub_tasks.find((t) => t.task_id === `growth-research-${ideaId}`);
34795
+ if (researchTask) researchTask.status = "completed";
34796
+ updateGrowthCampaign(campaign);
34797
+ const draftTasks = campaign.sub_tasks.filter((t) => t.task_id.startsWith(`growth-draft-${ideaId}`)).map((t) => ({ taskId: t.task_id, description: t.description }));
34798
+ if (draftTasks.length > 0) {
34799
+ appendToTodo2(draftTasks);
34800
+ }
34801
+ return {
34802
+ success: true,
34803
+ output: [
34804
+ `Research completed for campaign ${campaign.id}`,
34805
+ `Subreddits found: ${research.subreddit_research.length}`,
34806
+ `Keywords found: ${research.keyword_research.length}`,
34807
+ `Competitor insights: ${research.competitor_content.length}`,
34808
+ `Trending topics: ${research.trending_topics.length}`,
34809
+ "",
34810
+ `Queued ${draftTasks.length} draft tasks for next heartbeats.`
34811
+ ].join("\n")
34812
+ };
34813
+ }
34814
+ async function executeDraft(ideaId, channel) {
34815
+ const campaign = loadGrowthCampaignByIdeaId(ideaId);
34816
+ if (!campaign) {
34817
+ return { success: false, output: `No growth campaign found for idea ${ideaId}` };
34818
+ }
34819
+ const channelDraft = campaign.channels.find((c2) => {
34820
+ const label = c2.subreddit ? `${c2.channel}_${c2.subreddit}` : c2.channel;
34821
+ return label === channel;
34822
+ });
34823
+ if (!channelDraft) {
34824
+ return { success: false, output: `Channel ${channel} not found in campaign ${campaign.id}` };
34825
+ }
34826
+ let idea;
34827
+ try {
34828
+ const ideasData = JSON.parse(fs26.readFileSync(IDEAS_FILE, "utf-8"));
34829
+ idea = ideasData.ideas.find((i) => i.id === ideaId);
34830
+ } catch {
34831
+ return { success: false, output: `Failed to load ideas from ${IDEAS_FILE}` };
34832
+ }
34833
+ if (!idea) {
34834
+ return { success: false, output: `Idea not found: ${ideaId}` };
34835
+ }
34836
+ const promptCtx = {
34837
+ idea,
34838
+ product: campaign.product,
34839
+ research: campaign.research,
34840
+ utmCampaign: campaign.utm_campaign
34841
+ };
34842
+ const prompt = getChannelPrompt(channelDraft.channel, promptCtx, channelDraft.subreddit);
34843
+ const result = await invokeAI({
34844
+ prompt,
34845
+ timeoutMs: 12e4,
34846
+ expectJson: true
34847
+ });
34848
+ if (result.error) {
34849
+ return { success: false, output: `AI invocation failed for ${channel}: ${result.error}` };
34850
+ }
34851
+ let content;
34852
+ try {
34853
+ const jsonMatch = result.output.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/) || [null, result.output];
34854
+ content = JSON.parse((jsonMatch[1] || result.output).trim());
34855
+ } catch {
34856
+ const objMatch = result.output.match(/\{[\s\S]*\}/);
34857
+ if (objMatch) {
34858
+ try {
34859
+ content = JSON.parse(objMatch[0]);
34860
+ } catch {
34861
+ return { success: false, output: `Failed to parse AI output as JSON for ${channel}` };
34862
+ }
34863
+ } else {
34864
+ return { success: false, output: `No JSON found in AI output for ${channel}` };
34865
+ }
34866
+ }
34867
+ channelDraft.title = content.title || void 0;
34868
+ channelDraft.body = content.body || "";
34869
+ channelDraft.first_comment = content.first_comment || void 0;
34870
+ channelDraft.cta = content.cta || void 0;
34871
+ channelDraft.utm_url = content.utm_url || void 0;
34872
+ channelDraft.platform_constraints = content.platform_constraints || void 0;
34873
+ channelDraft.status = "drafted";
34874
+ channelDraft.drafted_at = (/* @__PURE__ */ new Date()).toISOString();
34875
+ const taskId = `growth-draft-${ideaId}-${channel}`;
34876
+ const subTask = campaign.sub_tasks.find((t) => t.task_id === taskId);
34877
+ if (subTask) subTask.status = "completed";
34878
+ if (channelDraft.channel === "twitter_thread" && channelDraft.body) {
34879
+ try {
34880
+ const threadTweets = content.thread_tweets || [];
34881
+ const draft = await createSocialDraft(channelDraft.body, {
34882
+ threadTweets: threadTweets.length > 0 ? threadTweets : void 0
34883
+ });
34884
+ channelDraft.social_draft_id = draft.id;
34885
+ } catch (err2) {
34886
+ console.error("[growth-campaign] Failed to create social draft:", err2);
34887
+ }
34888
+ }
34889
+ const allDrafted = campaign.channels.every((c2) => c2.status === "drafted" || c2.status === "published");
34890
+ if (allDrafted) {
34891
+ campaign.status = "publishing";
34892
+ }
34893
+ updateGrowthCampaign(campaign);
34894
+ const publishTaskId = `growth-publish-${ideaId}-${channel}`;
34895
+ const publishSubTask = campaign.sub_tasks.find((t) => t.task_id === publishTaskId);
34896
+ if (publishSubTask) {
34897
+ appendToTodo2([{
34898
+ taskId: publishTaskId,
34899
+ description: publishSubTask.description
34900
+ }]);
34901
+ }
34902
+ return {
34903
+ success: true,
34904
+ output: [
34905
+ `Draft generated for ${channelDraft.channel} (campaign ${campaign.id})`,
34906
+ channelDraft.title ? `Title: ${channelDraft.title}` : "",
34907
+ `Body length: ${channelDraft.body?.length || 0} chars`,
34908
+ channelDraft.social_draft_id ? `Twitter draft: ${channelDraft.social_draft_id}` : ""
34909
+ ].filter(Boolean).join("\n")
34910
+ };
34911
+ }
34912
+ async function executePublish2(ideaId, channel) {
34913
+ const campaign = loadGrowthCampaignByIdeaId(ideaId);
34914
+ if (!campaign) {
34915
+ return { success: false, output: `No growth campaign found for idea ${ideaId}` };
34916
+ }
34917
+ const channelDraft = campaign.channels.find((c2) => {
34918
+ const label = c2.subreddit ? `${c2.channel}_${c2.subreddit}` : c2.channel;
34919
+ return label === channel;
34920
+ });
34921
+ if (!channelDraft) {
34922
+ return { success: false, output: `Channel ${channel} not found in campaign ${campaign.id}` };
34923
+ }
34924
+ if (channelDraft.status !== "drafted") {
34925
+ return { success: false, output: `Channel ${channel} is not drafted yet (status: ${channelDraft.status})` };
34926
+ }
34927
+ if (channelDraft.channel === "twitter_thread" && channelDraft.social_draft_id) {
34928
+ appendToTodo2([{
34929
+ taskId: `social-publish-${channelDraft.social_draft_id}`,
34930
+ description: `Publish Twitter thread for growth campaign "${campaign.source_idea_title}"`
34931
+ }]);
34932
+ channelDraft.status = "published";
34933
+ channelDraft.published_at = (/* @__PURE__ */ new Date()).toISOString();
34934
+ const publishTask2 = campaign.sub_tasks.find((t) => t.task_id === `growth-publish-${ideaId}-${channel}`);
34935
+ if (publishTask2) publishTask2.status = "completed";
34936
+ updateGrowthCampaign(campaign);
34937
+ return {
34938
+ success: true,
34939
+ output: `Twitter publish delegated to social-publish-${channelDraft.social_draft_id}`
34940
+ };
34941
+ }
34942
+ const outputDir = path20.join(DATA_DIR, "campaigns", "output");
34943
+ fs26.mkdirSync(outputDir, { recursive: true });
34944
+ const outputFile = path20.join(outputDir, `${campaign.id}-${channel}.md`);
34945
+ const outputLines = [
34946
+ `# ${channelDraft.channel.replace(/_/g, " ").toUpperCase()} \u2014 Ready to Publish`,
34947
+ "",
34948
+ `**Campaign:** ${campaign.source_idea_title}`,
34949
+ `**Channel:** ${channelDraft.channel}`,
34950
+ channelDraft.subreddit ? `**Subreddit:** r/${channelDraft.subreddit}` : "",
34951
+ `**Generated:** ${channelDraft.drafted_at || "unknown"}`,
34952
+ `**UTM URL:** ${channelDraft.utm_url || "N/A"}`,
34953
+ "",
34954
+ "---",
34955
+ ""
34956
+ ];
34957
+ if (channelDraft.title) {
34958
+ outputLines.push(`## Title
34959
+
34960
+ ${channelDraft.title}
34961
+ `);
34962
+ }
34963
+ if (channelDraft.body) {
34964
+ outputLines.push(`## Content
34965
+
34966
+ ${channelDraft.body}
34967
+ `);
34968
+ }
34969
+ if (channelDraft.first_comment) {
34970
+ outputLines.push(`## First Comment
34971
+
34972
+ ${channelDraft.first_comment}
34973
+ `);
34974
+ }
34975
+ if (channelDraft.cta) {
34976
+ outputLines.push(`## CTA
34977
+
34978
+ ${channelDraft.cta}
34979
+ `);
34980
+ }
34981
+ if (channelDraft.platform_constraints) {
34982
+ outputLines.push(`## Platform Notes
34983
+
34984
+ ${channelDraft.platform_constraints}
34985
+ `);
34986
+ }
34987
+ fs26.writeFileSync(outputFile, outputLines.filter((l2) => l2 !== "").join("\n") + "\n");
34988
+ channelDraft.status = "published";
34989
+ channelDraft.published_at = (/* @__PURE__ */ new Date()).toISOString();
34990
+ const publishTask = campaign.sub_tasks.find((t) => t.task_id === `growth-publish-${ideaId}-${channel}`);
34991
+ if (publishTask) publishTask.status = "completed";
34992
+ const allPublished = campaign.channels.every((c2) => c2.status === "published");
34993
+ if (allPublished) {
34994
+ campaign.status = "completed";
34995
+ try {
34996
+ const ideasData = JSON.parse(fs26.readFileSync(IDEAS_FILE, "utf-8"));
34997
+ const ideaIdx = ideasData.ideas.findIndex((i) => i.id === ideaId);
34998
+ if (ideaIdx >= 0 && ideasData.ideas[ideaIdx].stage === "in_progress") {
34999
+ ideasData.ideas[ideaIdx].stage = "testing";
35000
+ ideasData.ideas[ideaIdx].updated_at = (/* @__PURE__ */ new Date()).toISOString();
35001
+ fs26.writeFileSync(IDEAS_FILE, JSON.stringify(ideasData, null, 2) + "\n");
35002
+ }
35003
+ } catch {
35004
+ }
35005
+ }
35006
+ updateGrowthCampaign(campaign);
35007
+ return {
35008
+ success: true,
35009
+ output: [
35010
+ `Content ready for ${channelDraft.channel}: ${outputFile}`,
35011
+ allPublished ? "All channels published \u2014 campaign completed!" : ""
35012
+ ].filter(Boolean).join("\n")
35013
+ };
35014
+ }
35015
+ function executeCheckStatus6() {
35016
+ const campaigns = listGrowthCampaigns();
35017
+ if (campaigns.length === 0) {
35018
+ return { success: true, output: "No growth campaigns found." };
35019
+ }
35020
+ const lines = ["## Growth Campaign Status\n"];
35021
+ for (const campaign of campaigns.slice(0, 10)) {
35022
+ const completed = campaign.sub_tasks.filter((t) => t.status === "completed").length;
35023
+ const total = campaign.sub_tasks.length;
35024
+ const pct = total > 0 ? Math.round(completed / total * 100) : 0;
35025
+ lines.push(`### ${campaign.id} (${campaign.status})`);
35026
+ lines.push(`- Idea: ${campaign.source_idea_title}`);
35027
+ lines.push(`- Created: ${campaign.created_at.split("T")[0]}`);
35028
+ lines.push(`- Channels: ${campaign.channels.map((c2) => c2.channel).join(", ")}`);
35029
+ lines.push(`- Progress: ${completed}/${total} tasks (${pct}%)`);
35030
+ lines.push(`- Research: ${campaign.research ? "completed" : "pending"}`);
35031
+ for (const ch of campaign.channels) {
35032
+ lines.push(` - ${ch.channel}: ${ch.status}${ch.body ? ` (${ch.body.length} chars)` : ""}`);
35033
+ }
35034
+ lines.push("");
35035
+ }
35036
+ return { success: true, output: lines.join("\n") };
35037
+ }
35038
+ function readGrowthCampaignFreshness() {
35039
+ const campaigns = listGrowthCampaigns();
35040
+ return {
35041
+ active_growth_campaigns: campaigns.filter((c2) => c2.status !== "completed").length,
35042
+ completed_growth_campaigns: campaigns.filter((c2) => c2.status === "completed").length,
35043
+ last_growth_campaign_date: campaigns.length > 0 ? campaigns[0].created_at.split("T")[0] : null
35044
+ };
35045
+ }
35046
+
33973
35047
  // scripts/heartbeat.ts
33974
35048
  init_marketing_strategy();
33975
35049
  init_ai_provider();
33976
35050
 
33977
35051
  // scripts/lib/run.ts
33978
- var path18 = __toESM(require("path"));
35052
+ var path21 = __toESM(require("path"));
33979
35053
  function resolveScript(baseDir, scriptName) {
33980
- const isCompiled = path18.extname(__filename) === ".js";
35054
+ const isCompiled = path21.extname(__filename) === ".js";
33981
35055
  const resolvedName = isCompiled ? scriptName.replace(/\.ts$/, ".js") : scriptName;
33982
- const scriptPath = path18.join(baseDir, resolvedName);
35056
+ const scriptPath = path21.join(baseDir, resolvedName);
33983
35057
  return isCompiled ? { command: "node", args: [scriptPath] } : { command: "npx", args: ["tsx", scriptPath] };
33984
35058
  }
33985
35059
 
@@ -34033,8 +35107,8 @@ function groupTasksByConflicts(tasks) {
34033
35107
  }
34034
35108
 
34035
35109
  // scripts/lib/json-lock.ts
34036
- var fs24 = __toESM(require("fs"));
34037
- var path19 = __toESM(require("path"));
35110
+ var fs27 = __toESM(require("fs"));
35111
+ var path22 = __toESM(require("path"));
34038
35112
  var DEFAULT_MAX_WAIT_MS = 1e4;
34039
35113
  var BASE_BACKOFF_MS = 50;
34040
35114
  var MAX_BACKOFF_MS = 150;
@@ -34042,10 +35116,10 @@ function withFileLock(lockPath, fn, maxWaitMs = DEFAULT_MAX_WAIT_MS) {
34042
35116
  const startTime = Date.now();
34043
35117
  while (true) {
34044
35118
  try {
34045
- fs24.mkdirSync(lockPath, { recursive: false });
35119
+ fs27.mkdirSync(lockPath, { recursive: false });
34046
35120
  try {
34047
- fs24.writeFileSync(
34048
- path19.join(lockPath, "owner"),
35121
+ fs27.writeFileSync(
35122
+ path22.join(lockPath, "owner"),
34049
35123
  JSON.stringify({ pid: process.pid, acquired: Date.now() })
34050
35124
  );
34051
35125
  } catch {
@@ -34074,16 +35148,16 @@ function withFileLock(lockPath, fn, maxWaitMs = DEFAULT_MAX_WAIT_MS) {
34074
35148
  }
34075
35149
  function atomicWriteFileSync(filePath, content) {
34076
35150
  const tmpPath = `${filePath}.${process.pid}.tmp`;
34077
- fs24.writeFileSync(tmpPath, content, "utf-8");
34078
- fs24.renameSync(tmpPath, filePath);
35151
+ fs27.writeFileSync(tmpPath, content, "utf-8");
35152
+ fs27.renameSync(tmpPath, filePath);
34079
35153
  }
34080
35154
  function releaseLock(lockPath) {
34081
35155
  try {
34082
- const ownerFile = path19.join(lockPath, "owner");
34083
- if (fs24.existsSync(ownerFile)) {
34084
- fs24.unlinkSync(ownerFile);
35156
+ const ownerFile = path22.join(lockPath, "owner");
35157
+ if (fs27.existsSync(ownerFile)) {
35158
+ fs27.unlinkSync(ownerFile);
34085
35159
  }
34086
- fs24.rmdirSync(lockPath);
35160
+ fs27.rmdirSync(lockPath);
34087
35161
  } catch {
34088
35162
  }
34089
35163
  }
@@ -34094,57 +35168,57 @@ function sleepSync(ms2) {
34094
35168
  }
34095
35169
 
34096
35170
  // scripts/lib/worktree.ts
34097
- var fs26 = __toESM(require("fs"));
34098
- var path21 = __toESM(require("path"));
35171
+ var fs29 = __toESM(require("fs"));
35172
+ var path24 = __toESM(require("path"));
34099
35173
 
34100
35174
  // scripts/lib/git-utils.ts
34101
- var import_child_process10 = require("child_process");
34102
- var fs25 = __toESM(require("fs"));
34103
- var path20 = __toESM(require("path"));
35175
+ var import_child_process11 = require("child_process");
35176
+ var fs28 = __toESM(require("fs"));
35177
+ var path23 = __toESM(require("path"));
34104
35178
  function cleanGitState(workspacePath) {
34105
- if (!fs25.existsSync(workspacePath)) return;
34106
- const gitDir = path20.join(workspacePath, ".git");
34107
- if (!fs25.existsSync(gitDir)) return;
35179
+ if (!fs28.existsSync(workspacePath)) return;
35180
+ const gitDir = path23.join(workspacePath, ".git");
35181
+ if (!fs28.existsSync(gitDir)) return;
34108
35182
  let actualGitDir = gitDir;
34109
35183
  try {
34110
- const stat = fs25.statSync(gitDir);
35184
+ const stat = fs28.statSync(gitDir);
34111
35185
  if (stat.isFile()) {
34112
- const content = fs25.readFileSync(gitDir, "utf-8").trim();
35186
+ const content = fs28.readFileSync(gitDir, "utf-8").trim();
34113
35187
  const match = content.match(/^gitdir:\s+(.+)$/);
34114
35188
  if (match) actualGitDir = match[1];
34115
35189
  }
34116
35190
  } catch {
34117
35191
  return;
34118
35192
  }
34119
- const lockFile = path20.join(actualGitDir, "index.lock");
34120
- if (fs25.existsSync(lockFile)) {
35193
+ const lockFile = path23.join(actualGitDir, "index.lock");
35194
+ if (fs28.existsSync(lockFile)) {
34121
35195
  try {
34122
- const result = (0, import_child_process10.execSync)(
35196
+ const result = (0, import_child_process11.execSync)(
34123
35197
  `lsof "${lockFile}" 2>/dev/null || true`,
34124
35198
  { encoding: "utf-8", timeout: 5e3 }
34125
35199
  ).trim();
34126
35200
  if (!result) {
34127
- fs25.unlinkSync(lockFile);
35201
+ fs28.unlinkSync(lockFile);
34128
35202
  console.log(`[pre-flight] Removed stale .git/index.lock in ${workspacePath}`);
34129
35203
  }
34130
35204
  } catch {
34131
35205
  try {
34132
- fs25.unlinkSync(lockFile);
35206
+ fs28.unlinkSync(lockFile);
34133
35207
  console.log(`[pre-flight] Removed stale .git/index.lock in ${workspacePath}`);
34134
35208
  } catch {
34135
35209
  }
34136
35210
  }
34137
35211
  }
34138
35212
  const staleOps = [
34139
- { marker: path20.join(actualGitDir, "MERGE_HEAD"), abort: "git merge --abort" },
34140
- { marker: path20.join(actualGitDir, "rebase-merge"), abort: "git rebase --abort" },
34141
- { marker: path20.join(actualGitDir, "rebase-apply"), abort: "git rebase --abort" },
34142
- { marker: path20.join(actualGitDir, "CHERRY_PICK_HEAD"), abort: "git cherry-pick --abort" }
35213
+ { marker: path23.join(actualGitDir, "MERGE_HEAD"), abort: "git merge --abort" },
35214
+ { marker: path23.join(actualGitDir, "rebase-merge"), abort: "git rebase --abort" },
35215
+ { marker: path23.join(actualGitDir, "rebase-apply"), abort: "git rebase --abort" },
35216
+ { marker: path23.join(actualGitDir, "CHERRY_PICK_HEAD"), abort: "git cherry-pick --abort" }
34143
35217
  ];
34144
35218
  for (const { marker, abort } of staleOps) {
34145
- if (fs25.existsSync(marker)) {
35219
+ if (fs28.existsSync(marker)) {
34146
35220
  try {
34147
- (0, import_child_process10.execSync)(abort, { cwd: workspacePath, encoding: "utf-8", timeout: 1e4, stdio: "pipe" });
35221
+ (0, import_child_process11.execSync)(abort, { cwd: workspacePath, encoding: "utf-8", timeout: 1e4, stdio: "pipe" });
34148
35222
  console.log(`[pre-flight] Aborted stale operation: ${abort} in ${workspacePath}`);
34149
35223
  } catch {
34150
35224
  }
@@ -34153,7 +35227,7 @@ function cleanGitState(workspacePath) {
34153
35227
  }
34154
35228
  function execGit(args2, cwd, timeoutMs) {
34155
35229
  try {
34156
- return (0, import_child_process10.execFileSync)("git", args2, {
35230
+ return (0, import_child_process11.execFileSync)("git", args2, {
34157
35231
  cwd,
34158
35232
  encoding: "utf-8",
34159
35233
  stdio: ["pipe", "pipe", "pipe"],
@@ -34216,8 +35290,8 @@ function sanitizeForCommitMessage(msg) {
34216
35290
  // scripts/lib/worktree.ts
34217
35291
  function getWorktreePaths(workspaceDir, repoName) {
34218
35292
  return {
34219
- baseClonePath: path21.join(workspaceDir, repoName),
34220
- worktreeContainerDir: path21.join(workspaceDir, `${repoName}-worktrees`)
35293
+ baseClonePath: path24.join(workspaceDir, repoName),
35294
+ worktreeContainerDir: path24.join(workspaceDir, `${repoName}-worktrees`)
34221
35295
  };
34222
35296
  }
34223
35297
  function syncBaseClone(baseClonePath, defaultBranch) {
@@ -34236,16 +35310,16 @@ function ensureIdeaBranch(baseClonePath, branchName, defaultBranch) {
34236
35310
  }
34237
35311
  }
34238
35312
  function createWorktree(baseClonePath, worktreeContainerDir, ideaBranch, groupIndex) {
34239
- if (!fs26.existsSync(worktreeContainerDir)) {
34240
- fs26.mkdirSync(worktreeContainerDir, { recursive: true });
35313
+ if (!fs29.existsSync(worktreeContainerDir)) {
35314
+ fs29.mkdirSync(worktreeContainerDir, { recursive: true });
34241
35315
  }
34242
35316
  const tempBranch = `wt/${ideaBranch}-g${groupIndex}`;
34243
- const worktreePath = path21.join(worktreeContainerDir, `${ideaBranch}-g${groupIndex}`);
34244
- if (fs26.existsSync(worktreePath)) {
35317
+ const worktreePath = path24.join(worktreeContainerDir, `${ideaBranch}-g${groupIndex}`);
35318
+ if (fs29.existsSync(worktreePath)) {
34245
35319
  try {
34246
35320
  execGit(["worktree", "remove", "--force", worktreePath], baseClonePath, 1e4);
34247
35321
  } catch {
34248
- fs26.rmSync(worktreePath, { recursive: true, force: true });
35322
+ fs29.rmSync(worktreePath, { recursive: true, force: true });
34249
35323
  }
34250
35324
  }
34251
35325
  try {
@@ -34279,7 +35353,7 @@ function removeWorktree(baseClonePath, worktreePath, tempBranch) {
34279
35353
  execGit(["worktree", "remove", "--force", worktreePath], baseClonePath, 1e4);
34280
35354
  } catch {
34281
35355
  try {
34282
- fs26.rmSync(worktreePath, { recursive: true, force: true });
35356
+ fs29.rmSync(worktreePath, { recursive: true, force: true });
34283
35357
  } catch {
34284
35358
  }
34285
35359
  }
@@ -34289,12 +35363,12 @@ function removeWorktree(baseClonePath, worktreePath, tempBranch) {
34289
35363
  }
34290
35364
  }
34291
35365
  function cleanupIdeaWorktrees(baseClonePath, worktreeContainerDir, ideaBranch) {
34292
- if (fs26.existsSync(worktreeContainerDir)) {
35366
+ if (fs29.existsSync(worktreeContainerDir)) {
34293
35367
  try {
34294
- const entries = fs26.readdirSync(worktreeContainerDir);
35368
+ const entries = fs29.readdirSync(worktreeContainerDir);
34295
35369
  for (const entry of entries) {
34296
35370
  if (entry.startsWith(`${ideaBranch}-g`)) {
34297
- const wtPath = path21.join(worktreeContainerDir, entry);
35371
+ const wtPath = path24.join(worktreeContainerDir, entry);
34298
35372
  const groupMatch = entry.match(/-g(\d+)$/);
34299
35373
  const tempBranch = `wt/${ideaBranch}-g${groupMatch?.[1] ?? "0"}`;
34300
35374
  removeWorktree(baseClonePath, wtPath, tempBranch);
@@ -34308,9 +35382,9 @@ function cleanupIdeaWorktrees(baseClonePath, worktreeContainerDir, ideaBranch) {
34308
35382
  } catch {
34309
35383
  }
34310
35384
  try {
34311
- const remaining = fs26.readdirSync(worktreeContainerDir);
35385
+ const remaining = fs29.readdirSync(worktreeContainerDir);
34312
35386
  if (remaining.length === 0) {
34313
- fs26.rmdirSync(worktreeContainerDir);
35387
+ fs29.rmdirSync(worktreeContainerDir);
34314
35388
  }
34315
35389
  } catch {
34316
35390
  }
@@ -34320,24 +35394,24 @@ function getTempBranchName(ideaBranch, groupIndex) {
34320
35394
  }
34321
35395
 
34322
35396
  // scripts/lib/telemetry.ts
34323
- var fs27 = __toESM(require("fs"));
34324
- var path22 = __toESM(require("path"));
34325
- var CONFIG_DIR2 = path22.join(
35397
+ var fs30 = __toESM(require("fs"));
35398
+ var path25 = __toESM(require("path"));
35399
+ var CONFIG_DIR2 = path25.join(
34326
35400
  process.env.HOME || process.env.USERPROFILE || "~",
34327
35401
  ".vibebusiness"
34328
35402
  );
34329
- var TELEMETRY_CONFIG_FILE = path22.join(CONFIG_DIR2, "telemetry.json");
34330
- var EVENTS_FILE = path22.join(CONFIG_DIR2, "events.jsonl");
35403
+ var TELEMETRY_CONFIG_FILE = path25.join(CONFIG_DIR2, "telemetry.json");
35404
+ var EVENTS_FILE = path25.join(CONFIG_DIR2, "events.jsonl");
34331
35405
  function getVersion() {
34332
35406
  try {
34333
35407
  let dir = __dirname;
34334
35408
  for (let i = 0; i < 4; i++) {
34335
- const pkgPath = path22.join(dir, "package.json");
34336
- if (fs27.existsSync(pkgPath)) {
34337
- const pkg = JSON.parse(fs27.readFileSync(pkgPath, "utf-8"));
35409
+ const pkgPath = path25.join(dir, "package.json");
35410
+ if (fs30.existsSync(pkgPath)) {
35411
+ const pkg = JSON.parse(fs30.readFileSync(pkgPath, "utf-8"));
34338
35412
  return pkg.version || "0.0.0";
34339
35413
  }
34340
- dir = path22.dirname(dir);
35414
+ dir = path25.dirname(dir);
34341
35415
  }
34342
35416
  } catch {
34343
35417
  }
@@ -34345,8 +35419,8 @@ function getVersion() {
34345
35419
  }
34346
35420
  function getTelemetryConfig() {
34347
35421
  try {
34348
- if (!fs27.existsSync(TELEMETRY_CONFIG_FILE)) return null;
34349
- return JSON.parse(fs27.readFileSync(TELEMETRY_CONFIG_FILE, "utf-8"));
35422
+ if (!fs30.existsSync(TELEMETRY_CONFIG_FILE)) return null;
35423
+ return JSON.parse(fs30.readFileSync(TELEMETRY_CONFIG_FILE, "utf-8"));
34350
35424
  } catch {
34351
35425
  return null;
34352
35426
  }
@@ -34367,31 +35441,31 @@ function trackEvent(event, properties = {}) {
34367
35441
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
34368
35442
  version: getVersion()
34369
35443
  };
34370
- fs27.mkdirSync(CONFIG_DIR2, { recursive: true });
34371
- fs27.appendFileSync(EVENTS_FILE, JSON.stringify(entry) + "\n");
35444
+ fs30.mkdirSync(CONFIG_DIR2, { recursive: true });
35445
+ fs30.appendFileSync(EVENTS_FILE, JSON.stringify(entry) + "\n");
34372
35446
  } catch {
34373
35447
  }
34374
35448
  }
34375
35449
 
34376
35450
  // src/lib/storage.ts
34377
- var import_fs = require("fs");
34378
- var import_path = __toESM(require("path"));
34379
- function getDataDir() {
34380
- return process.env.AI_ANALYST_DATA_DIR || import_path.default.join(process.cwd(), "data");
35451
+ var import_fs2 = require("fs");
35452
+ var import_path2 = __toESM(require("path"));
35453
+ function getDataDir2() {
35454
+ return process.env.AI_ANALYST_DATA_DIR || import_path2.default.join(process.cwd(), "data");
34381
35455
  }
34382
35456
  function getFilePath(filename) {
34383
- return import_path.default.join(getDataDir(), filename);
35457
+ return import_path2.default.join(getDataDir2(), filename);
34384
35458
  }
34385
35459
  async function ensureDataDir() {
34386
35460
  try {
34387
- await import_fs.promises.access(getDataDir());
35461
+ await import_fs2.promises.access(getDataDir2());
34388
35462
  } catch {
34389
- await import_fs.promises.mkdir(getDataDir(), { recursive: true });
35463
+ await import_fs2.promises.mkdir(getDataDir2(), { recursive: true });
34390
35464
  }
34391
35465
  }
34392
35466
  async function readJsonFile(filePath, defaultValue) {
34393
35467
  try {
34394
- const content = await import_fs.promises.readFile(filePath, "utf-8");
35468
+ const content = await import_fs2.promises.readFile(filePath, "utf-8");
34395
35469
  return JSON.parse(content);
34396
35470
  } catch {
34397
35471
  return defaultValue;
@@ -34399,7 +35473,7 @@ async function readJsonFile(filePath, defaultValue) {
34399
35473
  }
34400
35474
  async function writeJsonFile(filePath, data) {
34401
35475
  await ensureDataDir();
34402
- await import_fs.promises.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
35476
+ await import_fs2.promises.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
34403
35477
  }
34404
35478
  async function getIdeas() {
34405
35479
  const store = await readJsonFile(getFilePath("ideas.json"), { ideas: [] });
@@ -34600,8 +35674,8 @@ async function suggestEpics(opts) {
34600
35674
  }
34601
35675
 
34602
35676
  // src/lib/social-hooks.ts
34603
- var import_fs2 = require("fs");
34604
- var import_path2 = __toESM(require("path"));
35677
+ var import_fs3 = require("fs");
35678
+ var import_path3 = __toESM(require("path"));
34605
35679
 
34606
35680
  // src/lib/social.ts
34607
35681
  function scoreDraft2(text) {
@@ -34641,10 +35715,10 @@ function emptySocialState() {
34641
35715
  }
34642
35716
  async function triggerSocialDraftForShip(ideaId, idea) {
34643
35717
  try {
34644
- const socialFile = import_path2.default.join(getDataDir(), "social.json");
35718
+ const socialFile = import_path3.default.join(getDataDir2(), "social.json");
34645
35719
  let state;
34646
35720
  try {
34647
- const raw = await import_fs2.promises.readFile(socialFile, "utf-8");
35721
+ const raw = await import_fs3.promises.readFile(socialFile, "utf-8");
34648
35722
  state = JSON.parse(raw);
34649
35723
  } catch {
34650
35724
  state = emptySocialState();
@@ -34677,8 +35751,8 @@ async function triggerSocialDraftForShip(ideaId, idea) {
34677
35751
  const draft = isHighPotential ? { ...draftBase, high_potential: true } : draftBase;
34678
35752
  state.drafts.push(draft);
34679
35753
  state.last_updated = now;
34680
- await import_fs2.promises.mkdir(import_path2.default.dirname(socialFile), { recursive: true });
34681
- await import_fs2.promises.writeFile(socialFile, JSON.stringify(state, null, 2), "utf-8");
35754
+ await import_fs3.promises.mkdir(import_path3.default.dirname(socialFile), { recursive: true });
35755
+ await import_fs3.promises.writeFile(socialFile, JSON.stringify(state, null, 2), "utf-8");
34682
35756
  console.log(`[social-hooks] ship draft created draft_id=${draftId} idea_id=${ideaId}`);
34683
35757
  } catch (err2) {
34684
35758
  const message = err2 instanceof Error ? err2.message : String(err2);
@@ -34687,22 +35761,22 @@ async function triggerSocialDraftForShip(ideaId, idea) {
34687
35761
  }
34688
35762
 
34689
35763
  // scripts/lib/scaffold.ts
34690
- var fs30 = __toESM(require("fs"));
34691
- var path25 = __toESM(require("path"));
35764
+ var fs33 = __toESM(require("fs"));
35765
+ var path28 = __toESM(require("path"));
34692
35766
  function scaffoldSlashCommands(rootDir) {
34693
- let templatesDir = path25.join(__dirname, "..", "..", "templates", "commands");
34694
- if (!fs30.existsSync(templatesDir)) {
34695
- templatesDir = path25.join(__dirname, "..", "..", "..", "templates", "commands");
34696
- }
34697
- if (!fs30.existsSync(templatesDir)) return;
34698
- const targetDir = path25.join(rootDir, ".claude", "commands");
34699
- fs30.mkdirSync(targetDir, { recursive: true });
34700
- const templates = fs30.readdirSync(templatesDir).filter((f) => f.endsWith(".md"));
35767
+ let templatesDir = path28.join(__dirname, "..", "..", "templates", "commands");
35768
+ if (!fs33.existsSync(templatesDir)) {
35769
+ templatesDir = path28.join(__dirname, "..", "..", "..", "templates", "commands");
35770
+ }
35771
+ if (!fs33.existsSync(templatesDir)) return;
35772
+ const targetDir = path28.join(rootDir, ".claude", "commands");
35773
+ fs33.mkdirSync(targetDir, { recursive: true });
35774
+ const templates = fs33.readdirSync(templatesDir).filter((f) => f.endsWith(".md"));
34701
35775
  let copied = 0;
34702
35776
  for (const file of templates) {
34703
- const src = path25.join(templatesDir, file);
34704
- const dest = path25.join(targetDir, file);
34705
- fs30.copyFileSync(src, dest);
35777
+ const src = path28.join(templatesDir, file);
35778
+ const dest = path28.join(targetDir, file);
35779
+ fs33.copyFileSync(src, dest);
34706
35780
  copied++;
34707
35781
  }
34708
35782
  if (copied > 0) {
@@ -34711,23 +35785,23 @@ function scaffoldSlashCommands(rootDir) {
34711
35785
  }
34712
35786
 
34713
35787
  // scripts/lib/vibe-credits.ts
34714
- var fs31 = __toESM(require("fs"));
34715
- var path26 = __toESM(require("path"));
35788
+ var fs34 = __toESM(require("fs"));
35789
+ var path29 = __toESM(require("path"));
34716
35790
  var WORKER_URL = "https://vibe-credits.luis-e13.workers.dev";
34717
- var CONFIG_DIR3 = path26.join(process.env.HOME || "", ".vibebusiness");
34718
- var BUFFER_FILE = path26.join(CONFIG_DIR3, "vibe-buffer.json");
35791
+ var CONFIG_DIR3 = path29.join(process.env.HOME || "", ".vibebusiness");
35792
+ var BUFFER_FILE = path29.join(CONFIG_DIR3, "vibe-buffer.json");
34719
35793
  var MAX_BUFFER = 5;
34720
35794
  var REQUEST_TIMEOUT_MS = 5e3;
34721
35795
  function getClientVersion() {
34722
35796
  try {
34723
35797
  let dir = __dirname;
34724
35798
  for (let i = 0; i < 4; i++) {
34725
- const pkgPath = path26.join(dir, "package.json");
34726
- if (fs31.existsSync(pkgPath)) {
34727
- const pkg = JSON.parse(fs31.readFileSync(pkgPath, "utf-8"));
35799
+ const pkgPath = path29.join(dir, "package.json");
35800
+ if (fs34.existsSync(pkgPath)) {
35801
+ const pkg = JSON.parse(fs34.readFileSync(pkgPath, "utf-8"));
34728
35802
  return pkg.version || "0.0.0";
34729
35803
  }
34730
- dir = path26.dirname(dir);
35804
+ dir = path29.dirname(dir);
34731
35805
  }
34732
35806
  } catch {
34733
35807
  }
@@ -34736,18 +35810,18 @@ function getClientVersion() {
34736
35810
  var CLIENT_VERSION = getClientVersion();
34737
35811
  function loadBuffer() {
34738
35812
  try {
34739
- if (fs31.existsSync(BUFFER_FILE)) {
34740
- return JSON.parse(fs31.readFileSync(BUFFER_FILE, "utf-8"));
35813
+ if (fs34.existsSync(BUFFER_FILE)) {
35814
+ return JSON.parse(fs34.readFileSync(BUFFER_FILE, "utf-8"));
34741
35815
  }
34742
35816
  } catch {
34743
35817
  }
34744
35818
  return { vibes: 0, last_synced: "" };
34745
35819
  }
34746
35820
  function saveBuffer(buffer) {
34747
- if (!fs31.existsSync(CONFIG_DIR3)) {
34748
- fs31.mkdirSync(CONFIG_DIR3, { recursive: true });
35821
+ if (!fs34.existsSync(CONFIG_DIR3)) {
35822
+ fs34.mkdirSync(CONFIG_DIR3, { recursive: true });
34749
35823
  }
34750
- fs31.writeFileSync(BUFFER_FILE, JSON.stringify(buffer, null, 2));
35824
+ fs34.writeFileSync(BUFFER_FILE, JSON.stringify(buffer, null, 2));
34751
35825
  }
34752
35826
  function syncBuffer(vibesRemaining) {
34753
35827
  const buffered = Math.min(vibesRemaining, MAX_BUFFER);
@@ -34821,14 +35895,14 @@ async function consumeVibe(instanceId) {
34821
35895
  }
34822
35896
 
34823
35897
  // scripts/lib/license.ts
34824
- var fs32 = __toESM(require("fs"));
34825
- var path27 = __toESM(require("path"));
34826
- var CONFIG_DIR4 = path27.join(process.env.HOME || "", ".vibebusiness");
34827
- var LICENSE_FILE = path27.join(CONFIG_DIR4, "license.json");
35898
+ var fs35 = __toESM(require("fs"));
35899
+ var path30 = __toESM(require("path"));
35900
+ var CONFIG_DIR4 = path30.join(process.env.HOME || "", ".vibebusiness");
35901
+ var LICENSE_FILE = path30.join(CONFIG_DIR4, "license.json");
34828
35902
  function loadStoredLicense() {
34829
35903
  try {
34830
- if (fs32.existsSync(LICENSE_FILE)) {
34831
- return JSON.parse(fs32.readFileSync(LICENSE_FILE, "utf-8"));
35904
+ if (fs35.existsSync(LICENSE_FILE)) {
35905
+ return JSON.parse(fs35.readFileSync(LICENSE_FILE, "utf-8"));
34832
35906
  }
34833
35907
  } catch {
34834
35908
  }
@@ -34846,14 +35920,361 @@ function getInstanceId() {
34846
35920
  return generateInstanceId();
34847
35921
  }
34848
35922
 
35923
+ // scripts/lib/vision/validation.ts
35924
+ var fs38 = __toESM(require("fs"));
35925
+ init_paths();
35926
+
35927
+ // scripts/lib/vision/define.ts
35928
+ var fs36 = __toESM(require("fs"));
35929
+ init_ai_provider();
35930
+ init_paths();
35931
+ var PROJECT_DIR2 = PRODUCT_VISION_FILE.replace(/\/data\/product-vision\.json$/, "");
35932
+ function loadProductVision() {
35933
+ if (!fs36.existsSync(PRODUCT_VISION_FILE)) return null;
35934
+ try {
35935
+ return JSON.parse(fs36.readFileSync(PRODUCT_VISION_FILE, "utf-8"));
35936
+ } catch {
35937
+ return null;
35938
+ }
35939
+ }
35940
+
35941
+ // scripts/lib/vision/hypotheses.ts
35942
+ var fs37 = __toESM(require("fs"));
35943
+ init_paths();
35944
+ function loadJson(filePath, defaultValue) {
35945
+ try {
35946
+ if (!fs37.existsSync(filePath)) return defaultValue;
35947
+ return JSON.parse(fs37.readFileSync(filePath, "utf-8"));
35948
+ } catch {
35949
+ return defaultValue;
35950
+ }
35951
+ }
35952
+ function saveJson(filePath, data) {
35953
+ fs37.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
35954
+ }
35955
+ function updateHypothesisEvidence(hypothesisId, evidence, direction) {
35956
+ const store = loadJson(HYPOTHESES_FILE, { hypotheses: [] });
35957
+ const hypothesis = store.hypotheses.find((h3) => h3.id === hypothesisId);
35958
+ if (!hypothesis) return false;
35959
+ const now = (/* @__PURE__ */ new Date()).toISOString();
35960
+ const entry = `[${now.split("T")[0]}] ${evidence}`;
35961
+ if (direction === "for") {
35962
+ hypothesis.evidence_for.push(entry);
35963
+ } else {
35964
+ hypothesis.evidence_against.push(entry);
35965
+ }
35966
+ const forCount = hypothesis.evidence_for.length;
35967
+ const againstCount = hypothesis.evidence_against.length;
35968
+ if (forCount >= 3 && againstCount === 0) {
35969
+ hypothesis.status = "validated";
35970
+ } else if (againstCount >= 2 && forCount <= againstCount) {
35971
+ hypothesis.status = "invalidated";
35972
+ } else if (forCount > 0 || againstCount > 0) {
35973
+ hypothesis.status = "testing";
35974
+ }
35975
+ hypothesis.updated_at = now;
35976
+ saveJson(HYPOTHESES_FILE, store);
35977
+ return true;
35978
+ }
35979
+
35980
+ // scripts/lib/vision/validation.ts
35981
+ function loadJson2(filePath, defaultValue) {
35982
+ try {
35983
+ if (!fs38.existsSync(filePath)) return defaultValue;
35984
+ return JSON.parse(fs38.readFileSync(filePath, "utf-8"));
35985
+ } catch {
35986
+ return defaultValue;
35987
+ }
35988
+ }
35989
+ function evaluateVisionHealth(vision, hypotheses) {
35990
+ const v2 = vision || loadProductVision();
35991
+ const store = loadJson2(HYPOTHESES_FILE, { hypotheses: [] });
35992
+ const hyps = hypotheses || store.hypotheses;
35993
+ if (!v2 || hyps.length === 0) {
35994
+ return {
35995
+ confidence: 0,
35996
+ summary: v2 ? "No hypotheses to evaluate" : "No product vision defined",
35997
+ breakdown: { stated: 0, testing: 0, validated: 0, invalidated: 0, deferred: 0 }
35998
+ };
35999
+ }
36000
+ const breakdown = {
36001
+ stated: 0,
36002
+ testing: 0,
36003
+ validated: 0,
36004
+ invalidated: 0,
36005
+ deferred: 0
36006
+ };
36007
+ for (const h3 of hyps) {
36008
+ breakdown[h3.status] = (breakdown[h3.status] || 0) + 1;
36009
+ }
36010
+ const activeHyps = hyps.filter((h3) => h3.status !== "deferred");
36011
+ if (activeHyps.length === 0) {
36012
+ return { confidence: 50, summary: "All hypotheses deferred", breakdown };
36013
+ }
36014
+ const validated = breakdown.validated || 0;
36015
+ const invalidated = breakdown.invalidated || 0;
36016
+ const testing = breakdown.testing || 0;
36017
+ const stated = breakdown.stated || 0;
36018
+ const total = activeHyps.length;
36019
+ const validatedRatio = validated / total;
36020
+ const invalidatedRatio = invalidated / total;
36021
+ const testedRatio = (validated + invalidated + testing) / total;
36022
+ let confidence = 50;
36023
+ confidence += Math.round(validatedRatio * 40);
36024
+ confidence -= Math.round(invalidatedRatio * 30);
36025
+ confidence += Math.round(testedRatio * 10);
36026
+ confidence = Math.max(0, Math.min(100, confidence));
36027
+ const parts = [];
36028
+ if (validated > 0) parts.push(`${validated} validated`);
36029
+ if (testing > 0) parts.push(`${testing} testing`);
36030
+ if (stated > 0) parts.push(`${stated} untested`);
36031
+ if (invalidated > 0) parts.push(`${invalidated} invalidated`);
36032
+ const summary = `Vision confidence: ${confidence}/100 (${parts.join(", ")})`;
36033
+ return { confidence, summary, breakdown };
36034
+ }
36035
+ function suggestVisionPivot(invalidatedHypotheses) {
36036
+ if (invalidatedHypotheses.length === 0) {
36037
+ return { pivot_areas: [], suggestions: [] };
36038
+ }
36039
+ const pivotAreas = /* @__PURE__ */ new Set();
36040
+ const suggestions = [];
36041
+ for (const h3 of invalidatedHypotheses) {
36042
+ switch (h3.funnel_stage) {
36043
+ case "acquisition":
36044
+ pivotAreas.add("market_category");
36045
+ pivotAreas.add("best_fit_customers");
36046
+ suggestions.push(`Acquisition hypothesis "${h3.title}" failed \u2014 reconsider target audience or market category`);
36047
+ break;
36048
+ case "engagement":
36049
+ pivotAreas.add("value_delivered");
36050
+ pivotAreas.add("unique_attributes");
36051
+ suggestions.push(`Engagement hypothesis "${h3.title}" failed \u2014 reconsider value proposition or unique attributes`);
36052
+ break;
36053
+ case "conversion":
36054
+ pivotAreas.add("call_to_action");
36055
+ pivotAreas.add("plan");
36056
+ suggestions.push(`Conversion hypothesis "${h3.title}" failed \u2014 reconsider onboarding plan or CTA`);
36057
+ break;
36058
+ case "monetization":
36059
+ pivotAreas.add("positioning_statement");
36060
+ suggestions.push(`Monetization hypothesis "${h3.title}" failed \u2014 reconsider pricing or positioning`);
36061
+ break;
36062
+ default:
36063
+ pivotAreas.add("general");
36064
+ suggestions.push(`Hypothesis "${h3.title}" (${h3.funnel_stage}) failed \u2014 review vision alignment`);
36065
+ }
36066
+ }
36067
+ return {
36068
+ pivot_areas: Array.from(pivotAreas),
36069
+ suggestions
36070
+ };
36071
+ }
36072
+ function generateValidationReport() {
36073
+ const vision = loadProductVision();
36074
+ const store = loadJson2(HYPOTHESES_FILE, { hypotheses: [] });
36075
+ const hyps = store.hypotheses;
36076
+ const health = evaluateVisionHealth(vision, hyps);
36077
+ const uncertain = hyps.filter((h3) => h3.status === "stated" || h3.status === "testing").sort((a, b) => {
36078
+ const aEvidence = a.evidence_for.length + a.evidence_against.length;
36079
+ const bEvidence = b.evidence_for.length + b.evidence_against.length;
36080
+ return aEvidence - bEvidence;
36081
+ }).slice(0, 3).map((h3) => ({ id: h3.id, title: h3.title, funnel_stage: h3.funnel_stage }));
36082
+ const invalidated = hyps.filter((h3) => h3.status === "invalidated");
36083
+ const pivot = suggestVisionPivot(invalidated);
36084
+ return {
36085
+ vision_exists: !!vision,
36086
+ one_liner: vision?.one_liner || null,
36087
+ focus_metric: vision?.focus_metric || null,
36088
+ confidence_score: health.confidence,
36089
+ hypotheses: {
36090
+ total: hyps.length,
36091
+ validated: health.breakdown.validated || 0,
36092
+ invalidated: health.breakdown.invalidated || 0,
36093
+ testing: health.breakdown.testing || 0,
36094
+ untested: health.breakdown.stated || 0,
36095
+ top_uncertain: uncertain
36096
+ },
36097
+ pivot_needed: invalidated.length >= 2,
36098
+ pivot_suggestions: pivot.suggestions
36099
+ };
36100
+ }
36101
+ function updateHypothesisFromEvaluation(hypothesisId, verificationStatus, evaluationSummary) {
36102
+ if (!hypothesisId) return false;
36103
+ const direction = verificationStatus === "validated" ? "for" : verificationStatus === "invalidated" ? "against" : verificationStatus === "needs_investigation" ? "against" : "for";
36104
+ const evidence = `[${verificationStatus}] ${evaluationSummary.slice(0, 200)}`;
36105
+ return updateHypothesisEvidence(hypothesisId, evidence, direction);
36106
+ }
36107
+
36108
+ // scripts/lib/vision/acceptance-tests.ts
36109
+ var fs39 = __toESM(require("fs"));
36110
+ var path31 = __toESM(require("path"));
36111
+ var import_child_process12 = require("child_process");
36112
+ init_ai_provider();
36113
+ var PROJECT_DIR3 = path31.resolve(__dirname, "..", "..", "..");
36114
+ async function generateAcceptanceTests(idea, targetRepo) {
36115
+ if (!idea.success_metrics || idea.success_metrics.length === 0) {
36116
+ return { file: null, content: "", error: "No success metrics defined" };
36117
+ }
36118
+ const workspacePath = targetRepo.path;
36119
+ let existingPatterns = "";
36120
+ try {
36121
+ const e2eDir = path31.join(workspacePath, "e2e");
36122
+ const testsDir = path31.join(workspacePath, "tests");
36123
+ const searchDir = fs39.existsSync(e2eDir) ? e2eDir : fs39.existsSync(testsDir) ? testsDir : null;
36124
+ if (searchDir) {
36125
+ const files = fs39.readdirSync(searchDir).filter((f) => f.endsWith(".spec.ts") || f.endsWith(".test.ts"));
36126
+ if (files.length > 0) {
36127
+ const sample = fs39.readFileSync(path31.join(searchDir, files[0]), "utf-8").slice(0, 2e3);
36128
+ existingPatterns = `
36129
+ EXISTING TEST PATTERN (follow this style):
36130
+ \`\`\`typescript
36131
+ ${sample}
36132
+ \`\`\``;
36133
+ }
36134
+ }
36135
+ } catch {
36136
+ }
36137
+ const prompt = `You are a QA engineer. Generate Playwright acceptance tests for a feature.
36138
+
36139
+ IDEA: ${idea.title}
36140
+ SUMMARY: ${idea.summary}
36141
+
36142
+ SUCCESS METRICS (each must have at least one test):
36143
+ ${idea.success_metrics.map((m2, i) => `${i + 1}. ${m2}`).join("\n")}
36144
+
36145
+ IMPLEMENTATION PLAN:
36146
+ ${idea.implementation_plan}
36147
+ ${existingPatterns}
36148
+
36149
+ Generate a complete Playwright test file. Requirements:
36150
+ - Use @playwright/test imports
36151
+ - Each success metric should map to at least one test
36152
+ - Use descriptive test names that reference the metric
36153
+ - Use page.goto(), locators, and expect() assertions
36154
+ - Tests should be independent (no shared state)
36155
+ - Include reasonable timeouts and waits
36156
+ - Use data-testid selectors when specific selectors are unknown
36157
+
36158
+ Respond with ONLY the TypeScript code (no markdown code blocks, no explanation):`;
36159
+ const aiResult = await invokeAI({
36160
+ prompt,
36161
+ cwd: PROJECT_DIR3,
36162
+ timeoutMs: 12e4,
36163
+ useStdin: true
36164
+ });
36165
+ if (aiResult.error || !aiResult.output.trim()) {
36166
+ return { file: null, content: "", error: aiResult.error || "empty output" };
36167
+ }
36168
+ let content = aiResult.output.trim();
36169
+ const codeMatch = content.match(/```(?:typescript|ts)?\s*([\s\S]*?)```/);
36170
+ if (codeMatch) {
36171
+ content = codeMatch[1].trim();
36172
+ }
36173
+ const slug = idea.id.replace(/^idea-/, "").replace(/[^a-z0-9-]/g, "-").slice(0, 40);
36174
+ const fileName = `acceptance-${slug}.spec.ts`;
36175
+ return { file: fileName, content };
36176
+ }
36177
+ function writeAcceptanceTests(testResult, targetRepoPath) {
36178
+ if (!testResult.file || !testResult.content) return null;
36179
+ const e2eDir = path31.join(targetRepoPath, "e2e");
36180
+ if (!fs39.existsSync(e2eDir)) {
36181
+ fs39.mkdirSync(e2eDir, { recursive: true });
36182
+ }
36183
+ const filePath = path31.join(e2eDir, testResult.file);
36184
+ fs39.writeFileSync(filePath, testResult.content + "\n");
36185
+ return filePath;
36186
+ }
36187
+ function runAcceptanceTests(testFile, workspacePath) {
36188
+ const absolutePath = path31.isAbsolute(testFile) ? testFile : path31.join(workspacePath, testFile);
36189
+ if (!fs39.existsSync(absolutePath)) {
36190
+ return {
36191
+ passed: false,
36192
+ passCount: 0,
36193
+ failCount: 0,
36194
+ totalCount: 0,
36195
+ output: `Test file not found: ${absolutePath}`
36196
+ };
36197
+ }
36198
+ try {
36199
+ const npxPath = path31.join(workspacePath, "node_modules", ".bin", "playwright");
36200
+ const useLocalPlaywright = fs39.existsSync(npxPath);
36201
+ const cmd = useLocalPlaywright ? `npx playwright test "${absolutePath}" --reporter=json` : `npx playwright test "${absolutePath}" --reporter=json`;
36202
+ const output = (0, import_child_process12.execSync)(cmd, {
36203
+ cwd: workspacePath,
36204
+ encoding: "utf-8",
36205
+ timeout: 3e5,
36206
+ // 5 min timeout
36207
+ stdio: ["pipe", "pipe", "pipe"]
36208
+ });
36209
+ try {
36210
+ const report = JSON.parse(output);
36211
+ const suites = report.suites || [];
36212
+ let passCount = 0;
36213
+ let failCount = 0;
36214
+ const countTests = (suite) => {
36215
+ for (const spec of suite.specs || []) {
36216
+ for (const test of spec.tests || []) {
36217
+ const lastResult = test.results?.[test.results.length - 1];
36218
+ if (lastResult?.status === "passed") passCount++;
36219
+ else failCount++;
36220
+ }
36221
+ }
36222
+ for (const child of suite.suites || []) {
36223
+ countTests(child);
36224
+ }
36225
+ };
36226
+ for (const suite of suites) countTests(suite);
36227
+ const totalCount = passCount + failCount;
36228
+ return {
36229
+ passed: failCount === 0 && totalCount > 0,
36230
+ passCount,
36231
+ failCount,
36232
+ totalCount,
36233
+ output: output.slice(-2e3)
36234
+ };
36235
+ } catch {
36236
+ return {
36237
+ passed: true,
36238
+ passCount: 1,
36239
+ failCount: 0,
36240
+ totalCount: 1,
36241
+ output: output.slice(-2e3)
36242
+ };
36243
+ }
36244
+ } catch (error) {
36245
+ const err2 = error;
36246
+ const output = (err2.stdout || "") + "\n" + (err2.stderr || "");
36247
+ let passCount = 0;
36248
+ let failCount = 1;
36249
+ const passMatch = output.match(/(\d+) passed/);
36250
+ const failMatch = output.match(/(\d+) failed/);
36251
+ if (passMatch) passCount = parseInt(passMatch[1]);
36252
+ if (failMatch) failCount = parseInt(failMatch[1]);
36253
+ const totalCount = passCount + failCount || 1;
36254
+ return {
36255
+ passed: false,
36256
+ passCount,
36257
+ failCount,
36258
+ totalCount,
36259
+ output: output.slice(-2e3)
36260
+ };
36261
+ }
36262
+ }
36263
+ async function generateAndWriteAcceptanceTests(idea, targetRepo, workspacePath) {
36264
+ const result = await generateAcceptanceTests(idea, targetRepo);
36265
+ if (!result.file) return result;
36266
+ const writtenPath = writeAcceptanceTests(result, workspacePath);
36267
+ return { ...result, writtenPath: writtenPath || void 0 };
36268
+ }
36269
+
34849
36270
  // scripts/heartbeat.ts
34850
36271
  init_paths();
34851
36272
  var ROOT_DIR = PROJECT_DIR;
34852
- var execAsync = (0, import_util.promisify)(import_child_process11.exec);
36273
+ var execAsync = (0, import_util.promisify)(import_child_process13.exec);
34853
36274
  var DEFAULT_MAX_DECOMP_ATTEMPTS = 2;
34854
36275
  function spawnAsync(cmd, args2, options) {
34855
- return new Promise((resolve3, reject) => {
34856
- const child = (0, import_child_process11.spawn)(cmd, args2, {
36276
+ return new Promise((resolve4, reject) => {
36277
+ const child = (0, import_child_process13.spawn)(cmd, args2, {
34857
36278
  cwd: options.cwd,
34858
36279
  stdio: ["pipe", "pipe", "pipe"]
34859
36280
  });
@@ -34899,7 +36320,7 @@ function spawnAsync(cmd, args2, options) {
34899
36320
  reject(err2);
34900
36321
  return;
34901
36322
  }
34902
- resolve3({ stdout, stderr });
36323
+ resolve4({ stdout, stderr });
34903
36324
  });
34904
36325
  });
34905
36326
  }
@@ -34955,17 +36376,17 @@ function log(message) {
34955
36376
  }
34956
36377
  }
34957
36378
  function sleep(ms2) {
34958
- return new Promise((resolve3) => setTimeout(resolve3, ms2));
36379
+ return new Promise((resolve4) => setTimeout(resolve4, ms2));
34959
36380
  }
34960
- function loadJson(filePath, defaultValue) {
36381
+ function loadJson3(filePath, defaultValue) {
34961
36382
  try {
34962
- const content = fs35.readFileSync(filePath, "utf-8");
36383
+ const content = fs42.readFileSync(filePath, "utf-8");
34963
36384
  return JSON.parse(content);
34964
36385
  } catch {
34965
36386
  return defaultValue;
34966
36387
  }
34967
36388
  }
34968
- function saveJson(filePath, data) {
36389
+ function saveJson2(filePath, data) {
34969
36390
  atomicWriteFileSync(filePath, JSON.stringify(data, null, 2));
34970
36391
  }
34971
36392
  var HUMAN_TASK_PATTERNS = [
@@ -35014,7 +36435,7 @@ function isMetaTaskFalseCompletion(output) {
35014
36435
  }
35015
36436
  function reclassifyAsHumanDependent(taskId) {
35016
36437
  try {
35017
- let content = fs35.readFileSync(TODO_FILE, "utf-8");
36438
+ let content = fs42.readFileSync(TODO_FILE, "utf-8");
35018
36439
  const taskPattern = new RegExp(`^- \\[[ x]\\] \`${taskId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\`.*$`, "m");
35019
36440
  const match = content.match(taskPattern);
35020
36441
  if (!match) return;
@@ -35027,7 +36448,7 @@ function reclassifyAsHumanDependent(taskId) {
35027
36448
  const insertIndex = content.indexOf("\n", blockedIndex) + 1;
35028
36449
  content = content.slice(0, insertIndex) + "\n" + uncheckedLine + content.slice(insertIndex);
35029
36450
  }
35030
- fs35.writeFileSync(TODO_FILE, content);
36451
+ fs42.writeFileSync(TODO_FILE, content);
35031
36452
  log(`Reclassified task ${taskId} as human-dependent (moved to Blocked)`);
35032
36453
  } catch (error) {
35033
36454
  log(`Failed to reclassify task: ${error instanceof Error ? error.message : "Unknown"}`);
@@ -35035,7 +36456,7 @@ function reclassifyAsHumanDependent(taskId) {
35035
36456
  }
35036
36457
  function getMetaTaskFailureCount(taskId) {
35037
36458
  try {
35038
- const content = fs35.readFileSync(TODO_FILE, "utf-8");
36459
+ const content = fs42.readFileSync(TODO_FILE, "utf-8");
35039
36460
  const escapedId = taskId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
35040
36461
  const match = content.match(new RegExp(`\`${escapedId}\`.*\\[failed:(\\d+)\\]`));
35041
36462
  return match ? parseInt(match[1], 10) : 0;
@@ -35045,7 +36466,7 @@ function getMetaTaskFailureCount(taskId) {
35045
36466
  }
35046
36467
  function incrementMetaTaskFailureCount(taskId) {
35047
36468
  try {
35048
- let content = fs35.readFileSync(TODO_FILE, "utf-8");
36469
+ let content = fs42.readFileSync(TODO_FILE, "utf-8");
35049
36470
  const escapedId = taskId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
35050
36471
  const linePattern = new RegExp(`^(- \\[[ x]\\] \`${escapedId}\`.*)$`, "m");
35051
36472
  const lineMatch = content.match(linePattern);
@@ -35061,7 +36482,7 @@ function incrementMetaTaskFailureCount(taskId) {
35061
36482
  newLine = `${line} [failed:${newCount}]`;
35062
36483
  }
35063
36484
  content = content.replace(line, newLine);
35064
- fs35.writeFileSync(TODO_FILE, content);
36485
+ fs42.writeFileSync(TODO_FILE, content);
35065
36486
  log(`Meta-task ${taskId} failure count: ${newCount}`);
35066
36487
  return newCount;
35067
36488
  } catch (error) {
@@ -35071,7 +36492,7 @@ function incrementMetaTaskFailureCount(taskId) {
35071
36492
  }
35072
36493
  function parseTodoFile() {
35073
36494
  try {
35074
- const content = fs35.readFileSync(TODO_FILE, "utf-8");
36495
+ const content = fs42.readFileSync(TODO_FILE, "utf-8");
35075
36496
  const tasks = [];
35076
36497
  let currentSection = "high_priority";
35077
36498
  const lines = content.split("\n");
@@ -35103,9 +36524,9 @@ function parseTodoFile() {
35103
36524
  }
35104
36525
  }
35105
36526
  function loadState4() {
35106
- const ideasData = loadJson(IDEAS_FILE, { ideas: [] });
35107
- const goalsData = loadJson(GOALS_FILE, { goals: [] });
35108
- const sessionsData = loadJson(SESSIONS_FILE, { sessions: [] });
36527
+ const ideasData = loadJson3(IDEAS_FILE, { ideas: [] });
36528
+ const goalsData = loadJson3(GOALS_FILE, { goals: [] });
36529
+ const sessionsData = loadJson3(SESSIONS_FILE, { sessions: [] });
35109
36530
  const todos = parseTodoFile();
35110
36531
  return {
35111
36532
  ideas: ideasData.ideas || [],
@@ -35129,12 +36550,12 @@ var DEFAULT_BUSINESS_CONTEXT = {
35129
36550
  key_constraints: []
35130
36551
  };
35131
36552
  function loadBusinessContext() {
35132
- return loadJson(BUSINESS_CONTEXT_FILE, DEFAULT_BUSINESS_CONTEXT);
36553
+ return loadJson3(BUSINESS_CONTEXT_FILE, DEFAULT_BUSINESS_CONTEXT);
35133
36554
  }
35134
36555
  function updateBusinessContext(ctx, kpis) {
35135
36556
  updateFunnelStageStatuses(ctx, kpis);
35136
36557
  ctx.last_auto_update = (/* @__PURE__ */ new Date()).toISOString();
35137
- saveJson(BUSINESS_CONTEXT_FILE, ctx);
36558
+ saveJson2(BUSINESS_CONTEXT_FILE, ctx);
35138
36559
  }
35139
36560
  function checkThreshold(threshold, kpiValue) {
35140
36561
  if (kpiValue === null || kpiValue === void 0) return false;
@@ -35221,10 +36642,10 @@ function captureGitDiffInfo(workspacePath) {
35221
36642
  has_uncommitted_changes: false
35222
36643
  };
35223
36644
  try {
35224
- const status = (0, import_child_process11.execSync)("git status --porcelain", { cwd: workspacePath, encoding: "utf-8" });
36645
+ const status = (0, import_child_process13.execSync)("git status --porcelain", { cwd: workspacePath, encoding: "utf-8" });
35225
36646
  result.has_uncommitted_changes = status.trim().length > 0;
35226
36647
  try {
35227
- const diffStat = (0, import_child_process11.execSync)("git diff --stat HEAD~1 HEAD 2>/dev/null || git diff --stat --cached 2>/dev/null || git diff --stat 2>/dev/null", {
36648
+ const diffStat = (0, import_child_process13.execSync)("git diff --stat HEAD~1 HEAD 2>/dev/null || git diff --stat --cached 2>/dev/null || git diff --stat 2>/dev/null", {
35228
36649
  cwd: workspacePath,
35229
36650
  encoding: "utf-8",
35230
36651
  shell: "/bin/bash"
@@ -35312,7 +36733,7 @@ function checkAlerts(state) {
35312
36733
  }
35313
36734
  }
35314
36735
  }
35315
- const epics = loadJson(ROADMAP_FILE, { epics: [] }).epics || [];
36736
+ const epics = loadJson3(ROADMAP_FILE, { epics: [] }).epics || [];
35316
36737
  for (const epic of epics) {
35317
36738
  if (epic.status === "active" && epic.target_end) {
35318
36739
  const daysOver = daysSince(epic.target_end);
@@ -35408,14 +36829,14 @@ function checkSystemHealth(state, maxRetries) {
35408
36829
  }
35409
36830
  }
35410
36831
  if (health.recovery_actions.some((a) => a.startsWith("STALE SUB-TASK"))) {
35411
- const ideasData = loadJson(IDEAS_FILE, { ideas: [] });
36832
+ const ideasData = loadJson3(IDEAS_FILE, { ideas: [] });
35412
36833
  for (const idea of inProgressIdeas) {
35413
36834
  const stored = ideasData.ideas.find((i) => i.id === idea.id);
35414
36835
  if (stored) {
35415
36836
  stored.implementation.sub_tasks = idea.implementation.sub_tasks;
35416
36837
  }
35417
36838
  }
35418
- saveJson(IDEAS_FILE, ideasData);
36839
+ saveJson2(IDEAS_FILE, ideasData);
35419
36840
  log(`Persisted stale sub-task recovery changes`);
35420
36841
  }
35421
36842
  if (inProgressIdeas.length > 0) {
@@ -35438,7 +36859,7 @@ function checkSystemHealth(state, maxRetries) {
35438
36859
  }
35439
36860
  function autoRecoverStuckIdeas(stuckIdeaIds) {
35440
36861
  if (stuckIdeaIds.length === 0) return;
35441
- const ideasData = loadJson(IDEAS_FILE, { ideas: [] });
36862
+ const ideasData = loadJson3(IDEAS_FILE, { ideas: [] });
35442
36863
  const now = (/* @__PURE__ */ new Date()).toISOString();
35443
36864
  for (const ideaId of stuckIdeaIds) {
35444
36865
  const idea = ideasData.ideas.find((i) => i.id === ideaId);
@@ -35455,10 +36876,10 @@ function autoRecoverStuckIdeas(stuckIdeaIds) {
35455
36876
  });
35456
36877
  log(`Auto-deferred stuck idea: ${ideaId} \u2014 ${idea.title}`);
35457
36878
  }
35458
- saveJson(IDEAS_FILE, ideasData);
36879
+ saveJson2(IDEAS_FILE, ideasData);
35459
36880
  }
35460
36881
  function autoRecoverDeferredParseFailures() {
35461
- const ideasData = loadJson(IDEAS_FILE, { ideas: [] });
36882
+ const ideasData = loadJson3(IDEAS_FILE, { ideas: [] });
35462
36883
  const now = (/* @__PURE__ */ new Date()).toISOString();
35463
36884
  let recovered = 0;
35464
36885
  for (const idea of ideasData.ideas) {
@@ -35485,25 +36906,25 @@ function autoRecoverDeferredParseFailures() {
35485
36906
  recovered++;
35486
36907
  }
35487
36908
  if (recovered > 0) {
35488
- saveJson(IDEAS_FILE, ideasData);
36909
+ saveJson2(IDEAS_FILE, ideasData);
35489
36910
  log(`Auto-recovered ${recovered} deferred idea(s) for retry`);
35490
36911
  }
35491
36912
  }
35492
36913
  async function generateMissingShipCards(ideas) {
35493
36914
  const shippedIdeas = ideas.filter((i) => i.stage === "shipped");
35494
36915
  if (shippedIdeas.length === 0) return;
35495
- const assetsDir = path30.join(__dirname, "assets");
35496
- const hasRegular = fs35.existsSync(path30.join(assetsDir, "Inter-Regular.ttf"));
35497
- const hasBold = fs35.existsSync(path30.join(assetsDir, "Inter-Bold.ttf"));
36916
+ const assetsDir = path34.join(__dirname, "assets");
36917
+ const hasRegular = fs42.existsSync(path34.join(assetsDir, "Inter-Regular.ttf"));
36918
+ const hasBold = fs42.existsSync(path34.join(assetsDir, "Inter-Bold.ttf"));
35498
36919
  if (!hasRegular && !hasBold) return;
35499
- const visualsDir = path30.join(DATA_DIR, "reports", "visuals");
36920
+ const visualsDir = path34.join(DATA_DIR, "reports", "visuals");
35500
36921
  let generated = 0;
35501
36922
  for (const idea of shippedIdeas) {
35502
- const cardPath = path30.join(visualsDir, `${idea.id}-card.png`);
35503
- if (fs35.existsSync(cardPath)) continue;
36923
+ const cardPath = path34.join(visualsDir, `${idea.id}-card.png`);
36924
+ if (fs42.existsSync(cardPath)) continue;
35504
36925
  if (idea.implementation?.preview_url) {
35505
- const screenshotPath = path30.join(DATA_DIR, "reports", `${idea.id}-screenshot.png`);
35506
- if (!fs35.existsSync(screenshotPath)) {
36926
+ const screenshotPath = path34.join(DATA_DIR, "reports", `${idea.id}-screenshot.png`);
36927
+ if (!fs42.existsSync(screenshotPath)) {
35507
36928
  try {
35508
36929
  const { captureIdeaScreenshot: captureIdeaScreenshot2 } = await Promise.resolve().then(() => (init_screenshot(), screenshot_exports));
35509
36930
  await captureIdeaScreenshot2(idea.id, idea.implementation.preview_url);
@@ -35551,7 +36972,7 @@ async function fetchLiveKPIs() {
35551
36972
  }
35552
36973
  }
35553
36974
  function updateGoalsWithKPIs(kpis) {
35554
- const goalsData = loadJson(GOALS_FILE, { goals: [] });
36975
+ const goalsData = loadJson3(GOALS_FILE, { goals: [] });
35555
36976
  const today = formatDate(/* @__PURE__ */ new Date());
35556
36977
  for (const goal of goalsData.goals) {
35557
36978
  let kpisUpdated = 0;
@@ -35594,7 +37015,7 @@ function updateGoalsWithKPIs(kpis) {
35594
37015
  }
35595
37016
  }
35596
37017
  }
35597
- saveJson(GOALS_FILE, goalsData);
37018
+ saveJson2(GOALS_FILE, goalsData);
35598
37019
  log("Updated goals.json with live KPIs");
35599
37020
  }
35600
37021
  function detectMarketingTriggersIfNeeded(goals) {
@@ -35606,17 +37027,17 @@ function detectMarketingTriggersIfNeeded(goals) {
35606
37027
  }
35607
37028
  async function syncMarketingPlansToIdeas() {
35608
37029
  const { addMarketingIdeasToDashboard: addMarketingIdeasToDashboard2 } = await Promise.resolve().then(() => (init_marketing_integration(), marketing_integration_exports));
35609
- const plansDir = path30.join(DATA_DIR, "marketing-plans");
35610
- if (!fs35.existsSync(plansDir)) {
37030
+ const plansDir = path34.join(DATA_DIR, "marketing-plans");
37031
+ if (!fs42.existsSync(plansDir)) {
35611
37032
  return;
35612
37033
  }
35613
- const planFiles = fs35.readdirSync(plansDir).filter((f) => f.endsWith(".json"));
37034
+ const planFiles = fs42.readdirSync(plansDir).filter((f) => f.endsWith(".json"));
35614
37035
  let totalAdded = 0;
35615
37036
  let totalSkipped = 0;
35616
37037
  for (const file of planFiles) {
35617
37038
  try {
35618
- const planPath = path30.join(plansDir, file);
35619
- const plan = JSON.parse(fs35.readFileSync(planPath, "utf-8"));
37039
+ const planPath = path34.join(plansDir, file);
37040
+ const plan = JSON.parse(fs42.readFileSync(planPath, "utf-8"));
35620
37041
  if (plan.status === "approved" || plan.status === "active") {
35621
37042
  const result = await addMarketingIdeasToDashboard2(plan, DATA_DIR);
35622
37043
  totalAdded += result.added;
@@ -35631,10 +37052,10 @@ async function syncMarketingPlansToIdeas() {
35631
37052
  }
35632
37053
  }
35633
37054
  function loadCodebaseSnapshot() {
35634
- const snapshotPath = path30.join(DATA_DIR, "codebase-snapshot.json");
37055
+ const snapshotPath = path34.join(DATA_DIR, "codebase-snapshot.json");
35635
37056
  try {
35636
- if (fs35.existsSync(snapshotPath)) {
35637
- return JSON.parse(fs35.readFileSync(snapshotPath, "utf-8"));
37057
+ if (fs42.existsSync(snapshotPath)) {
37058
+ return JSON.parse(fs42.readFileSync(snapshotPath, "utf-8"));
35638
37059
  }
35639
37060
  } catch {
35640
37061
  }
@@ -35642,8 +37063,8 @@ function loadCodebaseSnapshot() {
35642
37063
  }
35643
37064
  function buildContextForClaude(state, alerts, businessContext) {
35644
37065
  const codebaseSnapshot = loadCodebaseSnapshot();
35645
- const hypotheses = loadJson(HYPOTHESES_FILE, { hypotheses: [] }).hypotheses || [];
35646
- const epics = loadJson(ROADMAP_FILE, { epics: [] }).epics || [];
37066
+ const hypotheses = loadJson3(HYPOTHESES_FILE, { hypotheses: [] }).hypotheses || [];
37067
+ const epics = loadJson3(ROADMAP_FILE, { epics: [] }).epics || [];
35647
37068
  const deriveEpicStatus = (epic) => {
35648
37069
  if (epic.status_override) return epic.status;
35649
37070
  const childIdeas = state.ideas.filter((i) => epic.idea_ids.includes(i.id));
@@ -35794,9 +37215,18 @@ function buildContextForClaude(state, alerts, businessContext) {
35794
37215
  social: readSocialFreshness(),
35795
37216
  promo_copy: readPromoCopyFreshness(),
35796
37217
  promo_video: readPromoVideoFreshness(),
35797
- campaigns: readCampaignFreshness()
37218
+ campaigns: readCampaignFreshness(),
37219
+ growth_campaigns: readGrowthCampaignFreshness(),
37220
+ product_vision: readVisionFreshness()
35798
37221
  };
35799
37222
  }
37223
+ function readVisionFreshness() {
37224
+ try {
37225
+ return generateValidationReport();
37226
+ } catch {
37227
+ return null;
37228
+ }
37229
+ }
35800
37230
  function buildBusinessConstraints(businessContext) {
35801
37231
  if (!businessContext) {
35802
37232
  return `BUSINESS CONTEXT:
@@ -36101,6 +37531,17 @@ Rules:
36101
37531
  5. Ideas without prerequisites (tech debt, infrastructure) can proceed normally.
36102
37532
  6. For revenue ideas: "convert existing free users to paid" > "build new revenue streams" when the user base is tiny.
36103
37533
 
37534
+ PRODUCT VISION ALIGNMENT:
37535
+ The "product_vision" field shows the current product vision status (Obviously Awesome + StoryBrand).
37536
+ Rules:
37537
+ 1. Prioritize ideas that test unvalidated hypotheses over new feature ideas.
37538
+ 2. Every recommended idea must link to a hypothesis or explain why it doesn't need one.
37539
+ 3. Quality gate: do not recommend implementing an idea without clear acceptance criteria (success_metrics).
37540
+ 4. If product_vision.pivot_needed == true, prioritize a "vision-review" task to reassess the vision.
37541
+ 5. If product_vision.vision_exists == false, recommend running "analyze --type=vision" before approving new ideas.
37542
+ 6. If product_vision.confidence_score < 30, focus on hypothesis validation over new feature development.
37543
+ 7. Monitor product_vision.hypotheses.top_uncertain \u2014 these represent the biggest unknowns to resolve.
37544
+
36104
37545
  HYPOTHESIS VALIDATION:
36105
37546
  The "hypotheses" field contains business hypotheses that need validation through experiments.
36106
37547
  Rules:
@@ -36201,9 +37642,9 @@ Focus on actionable, specific recommendations. Be concise.`;
36201
37642
  }
36202
37643
  function addTasksToTodo(tasks) {
36203
37644
  if (tasks.length === 0) return;
36204
- if (!fs35.existsSync(TODO_FILE)) return;
37645
+ if (!fs42.existsSync(TODO_FILE)) return;
36205
37646
  try {
36206
- let content = fs35.readFileSync(TODO_FILE, "utf-8");
37647
+ let content = fs42.readFileSync(TODO_FILE, "utf-8");
36207
37648
  if (!content.includes("## High Priority (Do Now)")) {
36208
37649
  log('TODO.md missing "## High Priority (Do Now)" section \u2014 creating it');
36209
37650
  const scheduledIdx = content.indexOf("## Scheduled");
@@ -36303,15 +37744,15 @@ function addTasksToTodo(tasks) {
36303
37744
  }
36304
37745
  log(`Added TODO: ${task.id} \u2014 ${task.description}`);
36305
37746
  }
36306
- fs35.writeFileSync(TODO_FILE, content);
37747
+ fs42.writeFileSync(TODO_FILE, content);
36307
37748
  } catch (error) {
36308
37749
  log(`Failed to add tasks to TODO.md: ${error instanceof Error ? error.message : "Unknown error"}`);
36309
37750
  }
36310
37751
  }
36311
37752
  function archiveCompletedTodos() {
36312
37753
  try {
36313
- if (!fs35.existsSync(TODO_FILE)) return;
36314
- let content = fs35.readFileSync(TODO_FILE, "utf-8");
37754
+ if (!fs42.existsSync(TODO_FILE)) return;
37755
+ let content = fs42.readFileSync(TODO_FILE, "utf-8");
36315
37756
  const lines = content.split("\n");
36316
37757
  const completedLines = [];
36317
37758
  const newLines = [];
@@ -36337,7 +37778,7 @@ function archiveCompletedTodos() {
36337
37778
  const archiveBlock = completedLines.join("\n") + "\n";
36338
37779
  content = content.slice(0, insertIndex) + archiveBlock + content.slice(insertIndex);
36339
37780
  }
36340
- fs35.writeFileSync(TODO_FILE, content);
37781
+ fs42.writeFileSync(TODO_FILE, content);
36341
37782
  log(`Archived ${completedLines.length} completed TODO(s) to Completed This Week`);
36342
37783
  } catch (error) {
36343
37784
  log(`Failed to archive completed TODOs: ${error instanceof Error ? error.message : "Unknown error"}`);
@@ -36345,8 +37786,8 @@ function archiveCompletedTodos() {
36345
37786
  }
36346
37787
  function pruneStaleScheduledTodos(state) {
36347
37788
  try {
36348
- if (!fs35.existsSync(TODO_FILE)) return;
36349
- const content = fs35.readFileSync(TODO_FILE, "utf-8");
37789
+ if (!fs42.existsSync(TODO_FILE)) return;
37790
+ const content = fs42.readFileSync(TODO_FILE, "utf-8");
36350
37791
  const lines = content.split("\n");
36351
37792
  const prunedIds = [];
36352
37793
  const terminalIdeaIds = new Set(
@@ -36382,7 +37823,7 @@ function pruneStaleScheduledTodos(state) {
36382
37823
  return true;
36383
37824
  });
36384
37825
  if (prunedIds.length === 0) return;
36385
- fs35.writeFileSync(TODO_FILE, newLines.join("\n"));
37826
+ fs42.writeFileSync(TODO_FILE, newLines.join("\n"));
36386
37827
  log(`Pruned ${prunedIds.length} stale TODO(s): ${prunedIds.join(", ")}`);
36387
37828
  } catch (error) {
36388
37829
  log(`Failed to prune stale TODOs: ${error instanceof Error ? error.message : "Unknown error"}`);
@@ -36390,8 +37831,8 @@ function pruneStaleScheduledTodos(state) {
36390
37831
  }
36391
37832
  function deduplicateActiveTodos() {
36392
37833
  try {
36393
- if (!fs35.existsSync(TODO_FILE)) return;
36394
- const content = fs35.readFileSync(TODO_FILE, "utf-8");
37834
+ if (!fs42.existsSync(TODO_FILE)) return;
37835
+ const content = fs42.readFileSync(TODO_FILE, "utf-8");
36395
37836
  const lines = content.split("\n");
36396
37837
  const removedIds = [];
36397
37838
  let currentSection = "";
@@ -36428,7 +37869,7 @@ function deduplicateActiveTodos() {
36428
37869
  if (linesToRemove.size === 0) return;
36429
37870
  const newLines = lines.filter((_, i) => !linesToRemove.has(i));
36430
37871
  const cleaned = newLines.join("\n").replace(/\n{3,}/g, "\n\n");
36431
- fs35.writeFileSync(TODO_FILE, cleaned);
37872
+ fs42.writeFileSync(TODO_FILE, cleaned);
36432
37873
  log(`Deduplicated ${removedIds.length} TODO(s): ${removedIds.join(", ")}`);
36433
37874
  } catch (error) {
36434
37875
  log(`Failed to deduplicate TODOs: ${error instanceof Error ? error.message : "Unknown error"}`);
@@ -36436,8 +37877,8 @@ function deduplicateActiveTodos() {
36436
37877
  }
36437
37878
  function readMemoryContext() {
36438
37879
  try {
36439
- if (!fs35.existsSync(MEMORY_FILE)) return "";
36440
- const content = fs35.readFileSync(MEMORY_FILE, "utf-8");
37880
+ if (!fs42.existsSync(MEMORY_FILE)) return "";
37881
+ const content = fs42.readFileSync(MEMORY_FILE, "utf-8");
36441
37882
  const MAX_TOTAL_LINES = 100;
36442
37883
  const MIN_LEARNING_ENTRIES = 15;
36443
37884
  const learningsSplit = content.indexOf("### Heartbeat Learnings");
@@ -36458,14 +37899,14 @@ function readMemoryContext() {
36458
37899
  }
36459
37900
  function appendToMemory(learnings) {
36460
37901
  if (learnings.length === 0) return;
36461
- if (!fs35.existsSync(MEMORY_FILE)) return;
37902
+ if (!fs42.existsSync(MEMORY_FILE)) return;
36462
37903
  const MAX_LEARNINGS_PER_HEARTBEAT = 2;
36463
37904
  let filtered = learnings.slice(0, MAX_LEARNINGS_PER_HEARTBEAT);
36464
37905
  if (learnings.length > MAX_LEARNINGS_PER_HEARTBEAT) {
36465
37906
  log(`Capped learnings from ${learnings.length} to ${MAX_LEARNINGS_PER_HEARTBEAT}`);
36466
37907
  }
36467
37908
  try {
36468
- let content = fs35.readFileSync(MEMORY_FILE, "utf-8");
37909
+ let content = fs42.readFileSync(MEMORY_FILE, "utf-8");
36469
37910
  const existingLines = content.split("\n").filter((l2) => l2.match(/^- \*\*/));
36470
37911
  const KEY_PHRASE_PATTERN = /(?:idea-[a-f0-9]{8}|idea-[a-z0-9-]+|hyp-[a-z0-9-]+|goal-[a-z0-9-]+|pipeline starvation|critical path|batch triage|cooldown|decompos|timeout|inbox bloat|dedup|meta-task|heartbeat|autonomy gate|false completion)/gi;
36471
37912
  filtered = filtered.filter((learning) => {
@@ -36515,7 +37956,7 @@ function appendToMemory(learnings) {
36515
37956
  const subsectionEnd = subsectionEndMatch ? subsectionIndex + 22 + (subsectionEndMatch.index || 0) : insertPoint;
36516
37957
  content = content.slice(0, subsectionEnd) + newEntries + "\n" + content.slice(subsectionEnd);
36517
37958
  }
36518
- fs35.writeFileSync(MEMORY_FILE, content);
37959
+ fs42.writeFileSync(MEMORY_FILE, content);
36519
37960
  log(`Added ${filtered.length} learning(s) to MEMORY.md`);
36520
37961
  }
36521
37962
  } catch (error) {
@@ -36524,8 +37965,8 @@ function appendToMemory(learnings) {
36524
37965
  }
36525
37966
  function pruneMemoryLearnings() {
36526
37967
  try {
36527
- if (!fs35.existsSync(MEMORY_FILE)) return;
36528
- const content = fs35.readFileSync(MEMORY_FILE, "utf-8");
37968
+ if (!fs42.existsSync(MEMORY_FILE)) return;
37969
+ const content = fs42.readFileSync(MEMORY_FILE, "utf-8");
36529
37970
  const learningsHeader = "### Heartbeat Learnings";
36530
37971
  const learningsIdx = content.indexOf(learningsHeader);
36531
37972
  if (learningsIdx === -1) return;
@@ -36561,7 +38002,7 @@ function pruneMemoryLearnings() {
36561
38002
  if (prunedCount === 0) return;
36562
38003
  const newLearningsBody = headerLine + "\n\n" + finalEntries.join("\n") + "\n";
36563
38004
  const newContent = beforeLearnings + newLearningsBody + afterLearnings;
36564
- fs35.writeFileSync(MEMORY_FILE, newContent);
38005
+ fs42.writeFileSync(MEMORY_FILE, newContent);
36565
38006
  log(`Pruned ${prunedCount} duplicate/stale learnings (${entryLines.length} \u2192 ${finalEntries.length})`);
36566
38007
  } catch (error) {
36567
38008
  log(`Failed to prune memory learnings: ${error instanceof Error ? error.message : "Unknown error"}`);
@@ -36651,8 +38092,8 @@ function determineWork(state) {
36651
38092
  };
36652
38093
  }
36653
38094
  }
36654
- const hypotheses = loadJson(HYPOTHESES_FILE, { hypotheses: [] }).hypotheses || [];
36655
- const epicsForScoring = loadJson(ROADMAP_FILE, { epics: [] }).epics || [];
38095
+ const hypotheses = loadJson3(HYPOTHESES_FILE, { hypotheses: [] }).hypotheses || [];
38096
+ const epicsForScoring = loadJson3(ROADMAP_FILE, { epics: [] }).epics || [];
36656
38097
  const approvedIdeas = state.ideas.filter(
36657
38098
  (i) => i.stage === "approved" && (i.implementation?.decomposition_attempts || 0) < maxRetriesCheck
36658
38099
  ).sort((a, b) => scoreIdea(b, state.goals, hypotheses, epicsForScoring) - scoreIdea(a, state.goals, hypotheses, epicsForScoring));
@@ -36670,7 +38111,7 @@ function determineWork(state) {
36670
38111
  return null;
36671
38112
  }
36672
38113
  function loadConfig() {
36673
- return loadJson(CONFIG_FILE, {
38114
+ return loadJson3(CONFIG_FILE, {
36674
38115
  repos: [],
36675
38116
  workspace_dir: "",
36676
38117
  schedules: {}
@@ -36680,7 +38121,7 @@ function detectTargetRepo(idea, config) {
36680
38121
  const filesAnalyzed = idea.source.files_analyzed || [];
36681
38122
  for (const file of filesAnalyzed) {
36682
38123
  for (const repo of config.repos) {
36683
- if (file.includes(repo.name) || file.includes(path30.basename(repo.path))) {
38124
+ if (file.includes(repo.name) || file.includes(path34.basename(repo.path))) {
36684
38125
  return repo;
36685
38126
  }
36686
38127
  }
@@ -36913,7 +38354,7 @@ Respond with JSON only (no markdown code blocks, just raw JSON):
36913
38354
  return [];
36914
38355
  }
36915
38356
  function deferIdeaWithComment(ideaId, reason) {
36916
- const ideasData = loadJson(IDEAS_FILE, { ideas: [] });
38357
+ const ideasData = loadJson3(IDEAS_FILE, { ideas: [] });
36917
38358
  const idea = ideasData.ideas.find((i) => i.id === ideaId);
36918
38359
  if (!idea) return false;
36919
38360
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -36929,7 +38370,7 @@ function deferIdeaWithComment(ideaId, reason) {
36929
38370
  });
36930
38371
  const ideaIndex = ideasData.ideas.findIndex((i) => i.id === ideaId);
36931
38372
  ideasData.ideas[ideaIndex] = idea;
36932
- saveJson(IDEAS_FILE, ideasData);
38373
+ saveJson2(IDEAS_FILE, ideasData);
36933
38374
  log(`Deferred idea ${ideaId}: ${reason}`);
36934
38375
  return false;
36935
38376
  }
@@ -36973,17 +38414,17 @@ function runTestsForRepo(workspacePath, testCommands, sourceRepoPath) {
36973
38414
  const allOutput = [];
36974
38415
  const env = { ...process.env };
36975
38416
  if (sourceRepoPath) {
36976
- const venvBin = path30.join(sourceRepoPath, ".venv", "bin");
36977
- if (fs35.existsSync(venvBin)) {
38417
+ const venvBin = path34.join(sourceRepoPath, ".venv", "bin");
38418
+ if (fs42.existsSync(venvBin)) {
36978
38419
  env.PATH = `${venvBin}:${env.PATH}`;
36979
- env.VIRTUAL_ENV = path30.join(sourceRepoPath, ".venv");
38420
+ env.VIRTUAL_ENV = path34.join(sourceRepoPath, ".venv");
36980
38421
  log(`Using venv from ${sourceRepoPath}/.venv`);
36981
38422
  }
36982
38423
  }
36983
38424
  for (const cmd of testCommands) {
36984
38425
  log(`Running test: ${cmd}`);
36985
38426
  try {
36986
- const result = (0, import_child_process11.execSync)(cmd, {
38427
+ const result = (0, import_child_process13.execSync)(cmd, {
36987
38428
  cwd: workspacePath,
36988
38429
  encoding: "utf-8",
36989
38430
  env,
@@ -37011,7 +38452,7 @@ async function executeSingleSubTask(subTask, ideaId, idea, ideasData, autonomy,
37011
38452
  subTask.started_at = (/* @__PURE__ */ new Date()).toISOString();
37012
38453
  const lockPath = IDEAS_FILE + ".lock";
37013
38454
  withFileLock(lockPath, () => {
37014
- const freshData = loadJson(IDEAS_FILE, { ideas: [] });
38455
+ const freshData = loadJson3(IDEAS_FILE, { ideas: [] });
37015
38456
  const idx = freshData.ideas.findIndex((i) => i.id === ideaId);
37016
38457
  if (idx !== -1) {
37017
38458
  const st = freshData.ideas[idx].implementation.sub_tasks.find((s) => s.id === subTask.id);
@@ -37019,7 +38460,7 @@ async function executeSingleSubTask(subTask, ideaId, idea, ideasData, autonomy,
37019
38460
  st.status = "in_progress";
37020
38461
  st.started_at = subTask.started_at;
37021
38462
  }
37022
- saveJson(IDEAS_FILE, freshData);
38463
+ saveJson2(IDEAS_FILE, freshData);
37023
38464
  }
37024
38465
  });
37025
38466
  const scopePayload = JSON.stringify({
@@ -37029,14 +38470,14 @@ async function executeSingleSubTask(subTask, ideaId, idea, ideasData, autonomy,
37029
38470
  });
37030
38471
  const scopeBase64 = Buffer.from(scopePayload).toString("base64");
37031
38472
  const timeoutMs = autonomy.max_sub_task_timeout_ms || 3e5;
37032
- const workspacePath = options?.worktreePath || path30.join(config.workspace_dir, targetRepo.name);
38473
+ const workspacePath = options?.worktreePath || path34.join(config.workspace_dir, targetRepo.name);
37033
38474
  const executionStartTime = Date.now();
37034
38475
  const updateSubTaskWithDetails = (status, updateOpts = {}) => {
37035
38476
  const endTime = Date.now();
37036
38477
  const durationMs = endTime - executionStartTime;
37037
38478
  const gitInfo = captureGitDiffInfo(workspacePath);
37038
38479
  withFileLock(lockPath, () => {
37039
- const reloadedIdeas = loadJson(IDEAS_FILE, { ideas: [] });
38480
+ const reloadedIdeas = loadJson3(IDEAS_FILE, { ideas: [] });
37040
38481
  const reloadedIndex = reloadedIdeas.ideas.findIndex((i) => i.id === ideaId);
37041
38482
  if (reloadedIndex !== -1) {
37042
38483
  const reloadedSubTask = reloadedIdeas.ideas[reloadedIndex].implementation.sub_tasks.find(
@@ -37062,7 +38503,7 @@ async function executeSingleSubTask(subTask, ideaId, idea, ideasData, autonomy,
37062
38503
  if (updateOpts.commitHash !== void 0) {
37063
38504
  reloadedSubTask.commit_hash = updateOpts.commitHash;
37064
38505
  }
37065
- saveJson(IDEAS_FILE, reloadedIdeas);
38506
+ saveJson2(IDEAS_FILE, reloadedIdeas);
37066
38507
  }
37067
38508
  }
37068
38509
  });
@@ -37105,9 +38546,20 @@ async function executeSingleSubTask(subTask, ideaId, idea, ideasData, autonomy,
37105
38546
  return false;
37106
38547
  }
37107
38548
  }
38549
+ if (idea.implementation.acceptance_test_file) {
38550
+ try {
38551
+ const acceptResult = runAcceptanceTests(
38552
+ idea.implementation.acceptance_test_file,
38553
+ workspacePath
38554
+ );
38555
+ log(`Acceptance test progress after ${subTask.id}: ${acceptResult.passCount}/${acceptResult.totalCount} passed`);
38556
+ } catch (acceptError) {
38557
+ log(`Acceptance test run skipped: ${acceptError instanceof Error ? acceptError.message : "Unknown"}`);
38558
+ }
38559
+ }
37108
38560
  let commitHash = null;
37109
38561
  try {
37110
- commitHash = (0, import_child_process11.execSync)("git rev-parse HEAD", { cwd: workspacePath, encoding: "utf-8" }).trim();
38562
+ commitHash = (0, import_child_process13.execSync)("git rev-parse HEAD", { cwd: workspacePath, encoding: "utf-8" }).trim();
37111
38563
  } catch {
37112
38564
  }
37113
38565
  updateSubTaskWithDetails("completed", {
@@ -37151,7 +38603,7 @@ async function analyzeAndRetrySubTask(subTask, ideaId, maxRetries) {
37151
38603
  "Be specific about what went wrong and how to fix it. Include any workarounds needed."
37152
38604
  ].filter(Boolean).join("\n");
37153
38605
  try {
37154
- const spawnResult = (0, import_child_process11.spawnSync)("claude", ["--print", "--model", "sonnet", "-p", errorContext], {
38606
+ const spawnResult = (0, import_child_process13.spawnSync)("claude", ["--print", "--model", "sonnet", "-p", errorContext], {
37155
38607
  cwd: ROOT_DIR,
37156
38608
  encoding: "utf-8",
37157
38609
  timeout: 6e4,
@@ -37163,7 +38615,7 @@ async function analyzeAndRetrySubTask(subTask, ideaId, maxRetries) {
37163
38615
  log(`Retry analysis returned empty result for ${subTask.id}`);
37164
38616
  return false;
37165
38617
  }
37166
- const ideasData = loadJson(IDEAS_FILE, { ideas: [] });
38618
+ const ideasData = loadJson3(IDEAS_FILE, { ideas: [] });
37167
38619
  const idea = ideasData.ideas.find((i) => i.id === ideaId);
37168
38620
  if (!idea) return false;
37169
38621
  const st = idea.implementation.sub_tasks.find((s) => s.id === subTask.id);
@@ -37176,20 +38628,20 @@ async function analyzeAndRetrySubTask(subTask, ideaId, maxRetries) {
37176
38628
  st.output_snippet = null;
37177
38629
  st.retry_count = currentRetries + 1;
37178
38630
  st.description = `[Retry ${currentRetries + 1}] ${result}`;
37179
- saveJson(IDEAS_FILE, ideasData);
38631
+ saveJson2(IDEAS_FILE, ideasData);
37180
38632
  log(`Sub-task ${subTask.id} reset to pending for retry (attempt ${currentRetries + 1})`);
37181
38633
  return true;
37182
38634
  } catch (error) {
37183
38635
  log(`Retry analysis failed for ${subTask.id}: ${error instanceof Error ? error.message : "Unknown"}`);
37184
38636
  try {
37185
- const fallbackIdeas = loadJson(IDEAS_FILE, { ideas: [] });
38637
+ const fallbackIdeas = loadJson3(IDEAS_FILE, { ideas: [] });
37186
38638
  const fallbackIdea = fallbackIdeas.ideas.find((i) => i.id === ideaId);
37187
38639
  if (fallbackIdea) {
37188
38640
  const fallbackSt = fallbackIdea.implementation.sub_tasks.find((s) => s.id === subTask.id);
37189
38641
  if (fallbackSt) {
37190
38642
  fallbackSt.retry_count = (fallbackSt.retry_count || 0) + 1;
37191
38643
  fallbackSt.error_message = `Retry analysis failed: ${error instanceof Error ? error.message : "Unknown"}`;
37192
- saveJson(IDEAS_FILE, fallbackIdeas);
38644
+ saveJson2(IDEAS_FILE, fallbackIdeas);
37193
38645
  log(`Incremented retry_count to ${fallbackSt.retry_count} for ${subTask.id} after analysis failure`);
37194
38646
  }
37195
38647
  }
@@ -37209,7 +38661,7 @@ async function executeAutonomousImplementation(ideaId) {
37209
38661
  log(`Autonomy not enabled, skipping implementation for ${ideaId}`);
37210
38662
  return false;
37211
38663
  }
37212
- const ideasData = loadJson(IDEAS_FILE, { ideas: [] });
38664
+ const ideasData = loadJson3(IDEAS_FILE, { ideas: [] });
37213
38665
  let idea = ideasData.ideas.find((i) => i.id === ideaId);
37214
38666
  if (!idea && !ideaId.startsWith("idea-")) {
37215
38667
  const prefixedId = `idea-${ideaId}`;
@@ -37236,7 +38688,7 @@ async function executeAutonomousImplementation(ideaId) {
37236
38688
  idea.implementation.decomposition_attempts = attempts + 1;
37237
38689
  const ideaIdx = ideasData.ideas.findIndex((i) => i.id === ideaId);
37238
38690
  ideasData.ideas[ideaIdx] = idea;
37239
- saveJson(IDEAS_FILE, ideasData);
38691
+ saveJson2(IDEAS_FILE, ideasData);
37240
38692
  log(`Decomposition attempt ${attempts + 1}/${maxDecompAttempts} for ${ideaId}...`);
37241
38693
  const newSubTasks = await decomposeIdea(idea, config);
37242
38694
  if (newSubTasks.length === 0) {
@@ -37247,23 +38699,94 @@ async function executeAutonomousImplementation(ideaId) {
37247
38699
  subTasks = newSubTasks;
37248
38700
  const ideaIndex = ideasData.ideas.findIndex((i) => i.id === ideaId);
37249
38701
  ideasData.ideas[ideaIndex] = idea;
37250
- saveJson(IDEAS_FILE, ideasData);
38702
+ saveJson2(IDEAS_FILE, ideasData);
37251
38703
  log(`Saved ${newSubTasks.length} sub-tasks for ${ideaId}, executing first sub-task now`);
37252
38704
  }
38705
+ if (!idea.implementation.acceptance_tests_generated && idea.success_metrics?.length > 0) {
38706
+ const targetRepoForTests = detectTargetRepo(idea, config);
38707
+ if (targetRepoForTests) {
38708
+ const workspacePath = path34.join(config.workspace_dir, targetRepoForTests.name);
38709
+ try {
38710
+ log(`Generating acceptance tests for ${ideaId}...`);
38711
+ const testResult = await generateAndWriteAcceptanceTests(idea, targetRepoForTests, workspacePath);
38712
+ if (testResult.file && testResult.writtenPath) {
38713
+ idea.implementation.acceptance_test_file = testResult.writtenPath;
38714
+ idea.implementation.acceptance_tests_generated = true;
38715
+ const ideaIndex = ideasData.ideas.findIndex((i) => i.id === ideaId);
38716
+ if (ideaIndex !== -1) {
38717
+ ideasData.ideas[ideaIndex] = idea;
38718
+ saveJson2(IDEAS_FILE, ideasData);
38719
+ }
38720
+ log(`Acceptance tests written to ${testResult.writtenPath}`);
38721
+ } else {
38722
+ log(`Acceptance test generation skipped: ${testResult.error || "no file generated"}`);
38723
+ }
38724
+ } catch (testGenError) {
38725
+ log(`Acceptance test generation failed (non-blocking): ${testGenError instanceof Error ? testGenError.message : "Unknown"}`);
38726
+ }
38727
+ }
38728
+ }
37253
38729
  const pendingTasks = subTasks.filter((st) => st.status === "pending");
37254
38730
  const completedTasks = subTasks.filter((st) => st.status === "completed");
37255
38731
  const failedTasks = subTasks.filter((st) => st.status === "failed");
37256
38732
  const inProgressTasks = subTasks.filter((st) => st.status === "in_progress");
37257
38733
  if (pendingTasks.length === 0 && inProgressTasks.length === 0 && failedTasks.length === 0) {
37258
- log(`All ${completedTasks.length} sub-tasks completed for ${ideaId}, creating PR...`);
38734
+ log(`All ${completedTasks.length} sub-tasks completed for ${ideaId}, running quality gate...`);
37259
38735
  const targetRepo2 = detectTargetRepo(idea, config);
37260
38736
  if (!targetRepo2) {
37261
38737
  log("Could not determine target repo");
37262
38738
  return false;
37263
38739
  }
38740
+ if (idea.implementation.acceptance_test_file) {
38741
+ const workspacePath = path34.join(config.workspace_dir, targetRepo2.name);
38742
+ try {
38743
+ const finalResult = runAcceptanceTests(
38744
+ idea.implementation.acceptance_test_file,
38745
+ workspacePath
38746
+ );
38747
+ const qgIdeas = loadJson3(IDEAS_FILE, { ideas: [] });
38748
+ const qgIdx = qgIdeas.ideas.findIndex((i) => i.id === ideaId);
38749
+ if (qgIdx !== -1) {
38750
+ qgIdeas.ideas[qgIdx].implementation.acceptance_test_results = {
38751
+ passed: finalResult.passCount,
38752
+ failed: finalResult.failCount,
38753
+ last_run: (/* @__PURE__ */ new Date()).toISOString()
38754
+ };
38755
+ saveJson2(IDEAS_FILE, qgIdeas);
38756
+ }
38757
+ if (!finalResult.passed) {
38758
+ log(`QUALITY GATE BLOCKED: ${finalResult.passCount}/${finalResult.totalCount} acceptance tests passed`);
38759
+ const fixTask = {
38760
+ id: `st-fix-acceptance-${Date.now().toString(36)}`,
38761
+ title: "Fix failing acceptance tests",
38762
+ description: `Acceptance tests failed (${finalResult.failCount} failures). Fix the implementation to pass all tests in ${idea.implementation.acceptance_test_file}.
38763
+
38764
+ Failures:
38765
+ ${finalResult.output.slice(-1500)}`,
38766
+ files_to_modify: [],
38767
+ status: "pending",
38768
+ started_at: null,
38769
+ completed_at: null,
38770
+ error_message: null,
38771
+ commit_hash: null
38772
+ };
38773
+ const fixIdeas = loadJson3(IDEAS_FILE, { ideas: [] });
38774
+ const fixIdx = fixIdeas.ideas.findIndex((i) => i.id === ideaId);
38775
+ if (fixIdx !== -1) {
38776
+ fixIdeas.ideas[fixIdx].implementation.sub_tasks.push(fixTask);
38777
+ saveJson2(IDEAS_FILE, fixIdeas);
38778
+ }
38779
+ log(`Queued fix-tests sub-task. Next heartbeat will attempt the fix.`);
38780
+ return false;
38781
+ }
38782
+ log(`QUALITY GATE PASSED: ${finalResult.totalCount}/${finalResult.totalCount} acceptance tests passed`);
38783
+ } catch (qgError) {
38784
+ log(`Quality gate check failed (proceeding with PR): ${qgError instanceof Error ? qgError.message : "Unknown"}`);
38785
+ }
38786
+ }
37264
38787
  try {
37265
38788
  const { command, args: args2 } = resolveScript(__dirname, "implement.ts");
37266
- (0, import_child_process11.execSync)(
38789
+ (0, import_child_process13.execSync)(
37267
38790
  [command, ...args2, `--idea=${ideaId}`, `--repo=${targetRepo2.name}`, "--create-pr-only"].join(" "),
37268
38791
  { cwd: ROOT_DIR, encoding: "utf-8", stdio: "inherit", timeout: 12e4 }
37269
38792
  );
@@ -37271,27 +38794,27 @@ async function executeAutonomousImplementation(ideaId) {
37271
38794
  if (autonomy.auto_merge) {
37272
38795
  const branchName2 = idea.implementation.branch_name;
37273
38796
  const defaultBranch2 = targetRepo2.default_branch || "main";
37274
- const workspacePath = path30.join(config.workspace_dir, targetRepo2.name);
38797
+ const workspacePath = path34.join(config.workspace_dir, targetRepo2.name);
37275
38798
  if (branchName2) {
37276
38799
  try {
37277
38800
  log(`Auto-merging branch ${branchName2} into ${defaultBranch2}...`);
37278
- (0, import_child_process11.execFileSync)("git", ["checkout", validateBranchName(defaultBranch2)], { cwd: workspacePath, encoding: "utf-8", stdio: "pipe" });
37279
- (0, import_child_process11.execFileSync)("git", ["merge", validateBranchName(branchName2), "--squash", "--no-edit"], { cwd: workspacePath, encoding: "utf-8", stdio: "pipe" });
37280
- (0, import_child_process11.execFileSync)("git", ["commit", "-m", `feat: ${sanitizeForCommitMessage(idea.title)} (auto-merged from ${validateBranchName(branchName2)})`], { cwd: workspacePath, encoding: "utf-8", stdio: "pipe" });
38801
+ (0, import_child_process13.execFileSync)("git", ["checkout", validateBranchName(defaultBranch2)], { cwd: workspacePath, encoding: "utf-8", stdio: "pipe" });
38802
+ (0, import_child_process13.execFileSync)("git", ["merge", validateBranchName(branchName2), "--squash", "--no-edit"], { cwd: workspacePath, encoding: "utf-8", stdio: "pipe" });
38803
+ (0, import_child_process13.execFileSync)("git", ["commit", "-m", `feat: ${sanitizeForCommitMessage(idea.title)} (auto-merged from ${validateBranchName(branchName2)})`], { cwd: workspacePath, encoding: "utf-8", stdio: "pipe" });
37281
38804
  log(`Successfully merged ${branchName2} into ${defaultBranch2}`);
37282
38805
  try {
37283
- (0, import_child_process11.execFileSync)("git", ["branch", "-d", validateBranchName(branchName2)], { cwd: workspacePath, encoding: "utf-8", stdio: "pipe" });
38806
+ (0, import_child_process13.execFileSync)("git", ["branch", "-d", validateBranchName(branchName2)], { cwd: workspacePath, encoding: "utf-8", stdio: "pipe" });
37284
38807
  log(`Deleted branch ${branchName2}`);
37285
38808
  } catch {
37286
38809
  log(`Could not delete branch ${branchName2} (may need force delete)`);
37287
38810
  }
37288
- const mergedIdeas = loadJson(IDEAS_FILE, { ideas: [] });
38811
+ const mergedIdeas = loadJson3(IDEAS_FILE, { ideas: [] });
37289
38812
  const mergedIndex = mergedIdeas.ideas.findIndex((i) => i.id === ideaId);
37290
38813
  if (mergedIndex !== -1) {
37291
38814
  mergedIdeas.ideas[mergedIndex].stage = "shipped";
37292
38815
  mergedIdeas.ideas[mergedIndex].updated_at = (/* @__PURE__ */ new Date()).toISOString();
37293
38816
  mergedIdeas.ideas[mergedIndex].implementation.completed_at = (/* @__PURE__ */ new Date()).toISOString();
37294
- saveJson(IDEAS_FILE, mergedIdeas);
38817
+ saveJson2(IDEAS_FILE, mergedIdeas);
37295
38818
  log(`Idea ${ideaId} moved to shipped stage`);
37296
38819
  try {
37297
38820
  const visualResult = await executeMarketingVisualTask(`generate-visual-${ideaId}`, "");
@@ -37312,25 +38835,25 @@ async function executeAutonomousImplementation(ideaId) {
37312
38835
  } catch (mergeError) {
37313
38836
  log(`Auto-merge failed (likely conflicts): ${mergeError instanceof Error ? mergeError.message : "Unknown"}`);
37314
38837
  try {
37315
- (0, import_child_process11.execFileSync)("git", ["merge", "--abort"], { cwd: workspacePath, encoding: "utf-8", stdio: "pipe" });
37316
- (0, import_child_process11.execFileSync)("git", ["checkout", validateBranchName(branchName2)], { cwd: workspacePath, encoding: "utf-8", stdio: "pipe" });
38838
+ (0, import_child_process13.execFileSync)("git", ["merge", "--abort"], { cwd: workspacePath, encoding: "utf-8", stdio: "pipe" });
38839
+ (0, import_child_process13.execFileSync)("git", ["checkout", validateBranchName(branchName2)], { cwd: workspacePath, encoding: "utf-8", stdio: "pipe" });
37317
38840
  } catch {
37318
38841
  }
37319
38842
  log(`Left idea ${ideaId} in testing stage for manual merge`);
37320
38843
  }
37321
38844
  }
37322
38845
  }
37323
- const reloadedIdeas = loadJson(IDEAS_FILE, { ideas: [] });
38846
+ const reloadedIdeas = loadJson3(IDEAS_FILE, { ideas: [] });
37324
38847
  const reloadedIndex = reloadedIdeas.ideas.findIndex((i) => i.id === ideaId);
37325
38848
  if (reloadedIndex !== -1 && reloadedIdeas.ideas[reloadedIndex].stage !== "shipped") {
37326
38849
  reloadedIdeas.ideas[reloadedIndex].stage = "testing";
37327
38850
  reloadedIdeas.ideas[reloadedIndex].updated_at = (/* @__PURE__ */ new Date()).toISOString();
37328
- saveJson(IDEAS_FILE, reloadedIdeas);
38851
+ saveJson2(IDEAS_FILE, reloadedIdeas);
37329
38852
  }
37330
38853
  return true;
37331
38854
  } catch (error) {
37332
38855
  log(`PR creation failed: ${error instanceof Error ? error.message : "Unknown"}`);
37333
- const prIdeas = loadJson(IDEAS_FILE, { ideas: [] });
38856
+ const prIdeas = loadJson3(IDEAS_FILE, { ideas: [] });
37334
38857
  const prIdx = prIdeas.ideas.findIndex((i) => i.id === ideaId);
37335
38858
  if (prIdx !== -1) {
37336
38859
  const attempts = (prIdeas.ideas[prIdx].implementation.pr_creation_attempts || 0) + 1;
@@ -37349,7 +38872,7 @@ async function executeAutonomousImplementation(ideaId) {
37349
38872
  created_at: (/* @__PURE__ */ new Date()).toISOString()
37350
38873
  });
37351
38874
  }
37352
- saveJson(IDEAS_FILE, prIdeas);
38875
+ saveJson2(IDEAS_FILE, prIdeas);
37353
38876
  }
37354
38877
  return false;
37355
38878
  }
@@ -37369,11 +38892,11 @@ async function executeAutonomousImplementation(ideaId) {
37369
38892
  if (targetRepo2) {
37370
38893
  try {
37371
38894
  const { command, args: args2 } = resolveScript(__dirname, "implement.ts");
37372
- (0, import_child_process11.execSync)(
38895
+ (0, import_child_process13.execSync)(
37373
38896
  [command, ...args2, `--idea=${ideaId}`, `--repo=${targetRepo2.name}`, "--create-pr-only"].join(" "),
37374
38897
  { cwd: ROOT_DIR, encoding: "utf-8", stdio: "inherit", timeout: 12e4 }
37375
38898
  );
37376
- const partialIdeas = loadJson(IDEAS_FILE, { ideas: [] });
38899
+ const partialIdeas = loadJson3(IDEAS_FILE, { ideas: [] });
37377
38900
  const partialIdx = partialIdeas.ideas.findIndex((i) => i.id === ideaId);
37378
38901
  if (partialIdx !== -1) {
37379
38902
  const failedList = exhaustedTasks.map((st) => `- ${st.title} (${st.error_message || "unknown error"})`).join("\n");
@@ -37388,7 +38911,7 @@ ${failedList}`,
37388
38911
  });
37389
38912
  partialIdeas.ideas[partialIdx].stage = "testing";
37390
38913
  partialIdeas.ideas[partialIdx].updated_at = (/* @__PURE__ */ new Date()).toISOString();
37391
- saveJson(IDEAS_FILE, partialIdeas);
38914
+ saveJson2(IDEAS_FILE, partialIdeas);
37392
38915
  }
37393
38916
  return true;
37394
38917
  } catch (prError) {
@@ -37410,7 +38933,7 @@ ${failedList}`,
37410
38933
  }
37411
38934
  }
37412
38935
  if (anyRetried) {
37413
- const retriedIdeas = loadJson(IDEAS_FILE, { ideas: [] });
38936
+ const retriedIdeas = loadJson3(IDEAS_FILE, { ideas: [] });
37414
38937
  const retriedIdea = retriedIdeas.ideas.find((i) => i.id === ideaId);
37415
38938
  if (!retriedIdea) return false;
37416
38939
  const retriedPending = retriedIdea.implementation.sub_tasks.filter((st) => st.status === "pending");
@@ -37460,7 +38983,7 @@ ${failedList}`,
37460
38983
  const ideaIndex = ideasData.ideas.findIndex((i) => i.id === ideaId);
37461
38984
  if (ideaIndex !== -1) {
37462
38985
  ideasData.ideas[ideaIndex] = idea;
37463
- saveJson(IDEAS_FILE, ideasData);
38986
+ saveJson2(IDEAS_FILE, ideasData);
37464
38987
  }
37465
38988
  }
37466
38989
  log(`${groups.length} conflict groups, executing in parallel via worktrees`);
@@ -37587,8 +39110,8 @@ ${allowedFiles.map((f) => `- ${f}`).join("\n")}
37587
39110
  6. When modifying TODO.md, preserve the markdown checkbox format: - [ ] or - [x]
37588
39111
  7. Be direct \u2014 read the file, make the edit, done
37589
39112
  `;
37590
- return new Promise((resolve3) => {
37591
- const claude = (0, import_child_process11.spawn)("claude", [
39113
+ return new Promise((resolve4) => {
39114
+ const claude = (0, import_child_process13.spawn)("claude", [
37592
39115
  "--print",
37593
39116
  "--dangerously-skip-permissions",
37594
39117
  "--model",
@@ -37616,7 +39139,7 @@ ${allowedFiles.map((f) => `- ${f}`).join("\n")}
37616
39139
  } catch {
37617
39140
  }
37618
39141
  }, 5e3);
37619
- resolve3(false);
39142
+ resolve4(false);
37620
39143
  }
37621
39144
  }, timeoutMs);
37622
39145
  claude.stdout.on("data", (data) => {
@@ -37634,14 +39157,14 @@ ${allowedFiles.map((f) => `- ${f}`).join("\n")}
37634
39157
  log(`Meta-task false completion detected: ${taskId} \u2014 output indicates blocked state`);
37635
39158
  log(` Output: ${output.substring(0, 200)}`);
37636
39159
  reclassifyAsHumanDependent(taskId);
37637
- resolve3(false);
39160
+ resolve4(false);
37638
39161
  return;
37639
39162
  }
37640
39163
  log(`Meta-task completed: ${output.substring(0, 200)}`);
37641
- resolve3(true);
39164
+ resolve4(true);
37642
39165
  } else {
37643
39166
  log(`Meta-task failed (exit ${code})`);
37644
- resolve3(false);
39167
+ resolve4(false);
37645
39168
  }
37646
39169
  });
37647
39170
  claude.on("error", (err2) => {
@@ -37649,7 +39172,7 @@ ${allowedFiles.map((f) => `- ${f}`).join("\n")}
37649
39172
  isResolved = true;
37650
39173
  clearTimeout(timeout);
37651
39174
  log(`Meta-task error: ${err2.message}`);
37652
- resolve3(false);
39175
+ resolve4(false);
37653
39176
  });
37654
39177
  });
37655
39178
  }
@@ -37657,7 +39180,7 @@ async function executeResearchTask(taskId, description) {
37657
39180
  const topicSlug = taskId.replace("research-", "").replace(/-/g, " ");
37658
39181
  const topic = description || topicSlug;
37659
39182
  log(`Executing research task: ${topic}`);
37660
- const goalsData = loadJson(GOALS_FILE, { goals: [] });
39183
+ const goalsData = loadJson3(GOALS_FILE, { goals: [] });
37661
39184
  const goalsContext = goalsData.goals.map((g2) => {
37662
39185
  const kpiLines = g2.kpis.map(
37663
39186
  (k) => ` - ${k.name}: ${k.current_value ?? "N/A"} / ${k.target_value} ${k.unit}`
@@ -37671,9 +39194,9 @@ ${kpiLines}`;
37671
39194
  goals: goalsContext
37672
39195
  });
37673
39196
  const contextBase64 = Buffer.from(contextPayload).toString("base64");
37674
- return new Promise((resolve3) => {
39197
+ return new Promise((resolve4) => {
37675
39198
  const { command: analyzeCmd, args: analyzeArgs } = resolveScript(__dirname, "analyze.ts");
37676
- const childProcess = (0, import_child_process11.spawn)(analyzeCmd, [
39199
+ const childProcess = (0, import_child_process13.spawn)(analyzeCmd, [
37677
39200
  ...analyzeArgs,
37678
39201
  `--type=research`,
37679
39202
  `--topic=${topic}`,
@@ -37685,22 +39208,22 @@ ${kpiLines}`;
37685
39208
  const timeout = setTimeout(() => {
37686
39209
  childProcess.kill("SIGTERM");
37687
39210
  log("Research task timed out after 15 minutes");
37688
- resolve3(false);
39211
+ resolve4(false);
37689
39212
  }, 9e5);
37690
39213
  childProcess.on("close", (code) => {
37691
39214
  clearTimeout(timeout);
37692
39215
  if (code === 0) {
37693
39216
  log("Research task completed successfully");
37694
- resolve3(true);
39217
+ resolve4(true);
37695
39218
  } else {
37696
39219
  log(`Research task failed with code ${code}`);
37697
- resolve3(false);
39220
+ resolve4(false);
37698
39221
  }
37699
39222
  });
37700
39223
  childProcess.on("error", (error) => {
37701
39224
  clearTimeout(timeout);
37702
39225
  log(`Research task error: ${error.message}`);
37703
- resolve3(false);
39226
+ resolve4(false);
37704
39227
  });
37705
39228
  });
37706
39229
  }
@@ -37726,7 +39249,7 @@ function buildGoalGapPrompt(goal, existingIdeas, liveKPIs, config, businessConte
37726
39249
  const funnelSummary = businessContext.funnels.map(
37727
39250
  (f) => `- **${f.name}** (${f.status}): ${f.key_insight}`
37728
39251
  ).join("\n");
37729
- const goalsForHealth = loadJson(GOALS_FILE, { goals: [] }).goals;
39252
+ const goalsForHealth = loadJson3(GOALS_FILE, { goals: [] }).goals;
37730
39253
  const health = computeFunnelHealth(businessContext, goalsForHealth);
37731
39254
  const earliestBroken = health.earliest_broken_stage || "unknown";
37732
39255
  const activeLawyers = businessContext.funnels.reduce((sum, f) => sum + (f.active_lawyers?.length || 0), 0);
@@ -37823,7 +39346,7 @@ async function executeGoalGapResearch(goalId) {
37823
39346
  return false;
37824
39347
  }
37825
39348
  log(`Executing goal-gap research for ${goalId}...`);
37826
- const goalsData = loadJson(GOALS_FILE, { goals: [] });
39349
+ const goalsData = loadJson3(GOALS_FILE, { goals: [] });
37827
39350
  const goal = goalsData.goals.find((g2) => g2.id === goalId);
37828
39351
  if (!goal) {
37829
39352
  log(`Goal ${goalId} not found`);
@@ -37833,17 +39356,17 @@ async function executeGoalGapResearch(goalId) {
37833
39356
  log(`Goal ${goalId} is not behind or at_risk (status: ${goal.status}), skipping`);
37834
39357
  return false;
37835
39358
  }
37836
- const ideasData = loadJson(IDEAS_FILE, { ideas: [] });
39359
+ const ideasData = loadJson3(IDEAS_FILE, { ideas: [] });
37837
39360
  const existingIdeas = ideasData.ideas.filter((i) => i.goal_id === goalId);
37838
39361
  const liveKPIs = await fetchLiveKPIs();
37839
39362
  const config = loadConfig();
37840
39363
  const businessContext = loadBusinessContext();
37841
39364
  const prompt = buildGoalGapPrompt(goal, existingIdeas, liveKPIs, config, businessContext);
37842
39365
  log(`Goal-gap prompt built (${prompt.length} chars), spawning Claude Code...`);
37843
- return new Promise((resolve3) => {
39366
+ return new Promise((resolve4) => {
37844
39367
  const TIMEOUT_MS = 9e5;
37845
39368
  const startTime = Date.now();
37846
- const claude = (0, import_child_process11.spawn)("claude", [
39369
+ const claude = (0, import_child_process13.spawn)("claude", [
37847
39370
  "--print",
37848
39371
  "--dangerously-skip-permissions"
37849
39372
  ], {
@@ -37867,7 +39390,7 @@ async function executeGoalGapResearch(goalId) {
37867
39390
  clearInterval(progressInterval);
37868
39391
  claude.kill("SIGTERM");
37869
39392
  log("Goal-gap research timed out after 15 minutes");
37870
- resolve3(false);
39393
+ resolve4(false);
37871
39394
  }
37872
39395
  }, TIMEOUT_MS);
37873
39396
  claude.stdout.on("data", (data) => {
@@ -37887,7 +39410,7 @@ async function executeGoalGapResearch(goalId) {
37887
39410
  const elapsed = Math.round((Date.now() - startTime) / 1e3);
37888
39411
  if (code !== 0) {
37889
39412
  log(`Goal-gap research failed with code ${code} after ${elapsed}s`);
37890
- resolve3(false);
39413
+ resolve4(false);
37891
39414
  return;
37892
39415
  }
37893
39416
  log(`Goal-gap research completed in ${elapsed}s, parsing ideas...`);
@@ -37895,7 +39418,7 @@ async function executeGoalGapResearch(goalId) {
37895
39418
  const jsonMatch = sanitized.match(/\[\s*\{[\s\S]*\}\s*\]/);
37896
39419
  if (!jsonMatch) {
37897
39420
  log("No JSON array found in goal-gap research response");
37898
- resolve3(false);
39421
+ resolve4(false);
37899
39422
  return;
37900
39423
  }
37901
39424
  try {
@@ -37937,27 +39460,27 @@ async function executeGoalGapResearch(goalId) {
37937
39460
  }));
37938
39461
  if (newIdeas.length === 0) {
37939
39462
  log("No ideas parsed from goal-gap research");
37940
- resolve3(false);
39463
+ resolve4(false);
37941
39464
  return;
37942
39465
  }
37943
- const freshIdeasData = loadJson(IDEAS_FILE, { ideas: [] });
39466
+ const freshIdeasData = loadJson3(IDEAS_FILE, { ideas: [] });
37944
39467
  freshIdeasData.ideas.push(...newIdeas);
37945
- saveJson(IDEAS_FILE, freshIdeasData);
37946
- const freshGoalsData = loadJson(GOALS_FILE, { goals: [] });
39468
+ saveJson2(IDEAS_FILE, freshIdeasData);
39469
+ const freshGoalsData = loadJson3(GOALS_FILE, { goals: [] });
37947
39470
  const freshGoal = freshGoalsData.goals.find((g2) => g2.id === goalId);
37948
39471
  if (freshGoal) {
37949
39472
  if (!freshGoal.related_ideas) freshGoal.related_ideas = [];
37950
39473
  freshGoal.related_ideas.push(...newIdeas.map((i) => i.id));
37951
- saveJson(GOALS_FILE, freshGoalsData);
39474
+ saveJson2(GOALS_FILE, freshGoalsData);
37952
39475
  }
37953
39476
  log(`Goal-gap research generated ${newIdeas.length} ideas for ${goalId}:`);
37954
39477
  newIdeas.forEach((idea, i) => {
37955
39478
  log(` ${i + 1}. ${idea.title}`);
37956
39479
  });
37957
- resolve3(true);
39480
+ resolve4(true);
37958
39481
  } catch (error) {
37959
39482
  log(`Failed to parse goal-gap research ideas: ${error instanceof Error ? error.message : "Unknown"}`);
37960
- resolve3(false);
39483
+ resolve4(false);
37961
39484
  }
37962
39485
  });
37963
39486
  claude.on("error", (error) => {
@@ -37966,7 +39489,7 @@ async function executeGoalGapResearch(goalId) {
37966
39489
  clearInterval(progressInterval);
37967
39490
  clearTimeout(timeout);
37968
39491
  log(`Goal-gap research error: ${error.message}`);
37969
- resolve3(false);
39492
+ resolve4(false);
37970
39493
  }
37971
39494
  });
37972
39495
  });
@@ -38067,7 +39590,7 @@ Respond with JSON only (no markdown code blocks):
38067
39590
  }
38068
39591
  async function executeShippedEvaluation(ideaId) {
38069
39592
  log(`Starting post-ship evaluation for idea: ${ideaId}`);
38070
- const ideasData = loadJson(IDEAS_FILE, { ideas: [] });
39593
+ const ideasData = loadJson3(IDEAS_FILE, { ideas: [] });
38071
39594
  const idea = ideasData.ideas.find((i) => i.id === ideaId);
38072
39595
  if (!idea) {
38073
39596
  log(`Idea not found: ${ideaId}`);
@@ -38080,7 +39603,7 @@ async function executeShippedEvaluation(ideaId) {
38080
39603
  const daysShipped = daysSince(idea.implementation?.completed_at || idea.updated_at);
38081
39604
  let goal = null;
38082
39605
  if (idea.goal_id) {
38083
- const goalsData = loadJson(GOALS_FILE, { goals: [] });
39606
+ const goalsData = loadJson3(GOALS_FILE, { goals: [] });
38084
39607
  goal = goalsData.goals.find((g2) => g2.id === idea.goal_id) || null;
38085
39608
  }
38086
39609
  const liveKPIs = await fetchLiveKPIs();
@@ -38222,8 +39745,22 @@ ${result.metric_evaluations.map((m2) => `- ${m2.status.toUpperCase()}: ${m2.metr
38222
39745
  });
38223
39746
  ideasData.ideas[ideaIndex].comments = comments;
38224
39747
  }
38225
- saveJson(IDEAS_FILE, ideasData);
39748
+ saveJson2(IDEAS_FILE, ideasData);
38226
39749
  log(`Evaluation saved for ${ideaId}: status=${result.status}, next_eval=${nextEvalDate.toISOString().split("T")[0]}`);
39750
+ if (idea.hypothesis_id) {
39751
+ try {
39752
+ const updated = updateHypothesisFromEvaluation(
39753
+ idea.hypothesis_id,
39754
+ result.status,
39755
+ result.summary || ""
39756
+ );
39757
+ if (updated) {
39758
+ log(`Hypothesis ${idea.hypothesis_id} evidence updated (${result.status})`);
39759
+ }
39760
+ } catch (hypError) {
39761
+ log(`Hypothesis update failed (non-fatal): ${hypError instanceof Error ? hypError.message : "Unknown"}`);
39762
+ }
39763
+ }
38227
39764
  return true;
38228
39765
  }
38229
39766
  function generateResearchTasks(idea, config, businessContext) {
@@ -38466,8 +40003,8 @@ async function executeSingleResearchTask(task) {
38466
40003
  log(` Starting research task: ${task.type} - ${task.topic}`);
38467
40004
  task.status = "running";
38468
40005
  task.started_at = (/* @__PURE__ */ new Date()).toISOString();
38469
- return new Promise((resolve3) => {
38470
- const claude = (0, import_child_process11.spawn)("claude", ["--print", "--dangerously-skip-permissions"], {
40006
+ return new Promise((resolve4) => {
40007
+ const claude = (0, import_child_process13.spawn)("claude", ["--print", "--dangerously-skip-permissions"], {
38471
40008
  cwd: ROOT_DIR,
38472
40009
  env: process.env,
38473
40010
  stdio: ["pipe", "pipe", "pipe"]
@@ -38482,7 +40019,7 @@ async function executeSingleResearchTask(task) {
38482
40019
  task.completed_at = (/* @__PURE__ */ new Date()).toISOString();
38483
40020
  task.error_message = "Timed out after 5 minutes";
38484
40021
  log(` Research task timed out: ${task.type}`);
38485
- resolve3(task);
40022
+ resolve4(task);
38486
40023
  }, TIMEOUT_MS);
38487
40024
  claude.stdout.on("data", (data) => {
38488
40025
  output += data.toString();
@@ -38511,7 +40048,7 @@ async function executeSingleResearchTask(task) {
38511
40048
  task.error_message = errorOutput || `Exited with code ${code}`;
38512
40049
  log(` Research task failed: ${task.type} - ${task.error_message}`);
38513
40050
  }
38514
- resolve3(task);
40051
+ resolve4(task);
38515
40052
  });
38516
40053
  claude.on("error", (error) => {
38517
40054
  clearTimeout(timeout);
@@ -38519,7 +40056,7 @@ async function executeSingleResearchTask(task) {
38519
40056
  task.completed_at = (/* @__PURE__ */ new Date()).toISOString();
38520
40057
  task.error_message = error.message;
38521
40058
  log(` Research task error: ${task.type} - ${error.message}`);
38522
- resolve3(task);
40059
+ resolve4(task);
38523
40060
  });
38524
40061
  });
38525
40062
  }
@@ -38609,7 +40146,7 @@ ${parsed.risks_identified?.map((r) => `- ${r}`).join("\n") || "None identified"}
38609
40146
  }
38610
40147
  async function executeIdeaResearch(ideaId, config, businessContext) {
38611
40148
  log(`Executing parallel research for idea: ${ideaId}`);
38612
- const ideasData = loadJson(IDEAS_FILE, { ideas: [] });
40149
+ const ideasData = loadJson3(IDEAS_FILE, { ideas: [] });
38613
40150
  const idea = ideasData.ideas.find((i) => i.id === ideaId);
38614
40151
  if (!idea) {
38615
40152
  log(`Idea not found: ${ideaId}`);
@@ -38620,7 +40157,7 @@ async function executeIdeaResearch(ideaId, config, businessContext) {
38620
40157
  log(`Auto-transitioning idea ${ideaId} from inbox to under_review for research`);
38621
40158
  idea.stage = "under_review";
38622
40159
  idea.updated_at = (/* @__PURE__ */ new Date()).toISOString();
38623
- saveJson(IDEAS_FILE, ideasData);
40160
+ saveJson2(IDEAS_FILE, ideasData);
38624
40161
  } else {
38625
40162
  log(`Idea ${ideaId} is not in under_review stage (current: ${idea.stage})`);
38626
40163
  return false;
@@ -38663,7 +40200,7 @@ ${research.summary}
38663
40200
  });
38664
40201
  idea.comments = comments;
38665
40202
  }
38666
- saveJson(IDEAS_FILE, ideasData);
40203
+ saveJson2(IDEAS_FILE, ideasData);
38667
40204
  log(`Research saved for idea ${ideaId}: recommendation = ${research.recommendation}`);
38668
40205
  return true;
38669
40206
  }
@@ -38704,7 +40241,7 @@ async function executeTask(task, config, businessContext) {
38704
40241
  const ideaId = taskId.replace("evaluate-shipped-", "");
38705
40242
  if (!ideaId.startsWith("idea-")) {
38706
40243
  log(`Batch evaluation requested (parsed "${ideaId}" from task "${taskId}")`);
38707
- const batchIdeas = loadJson(IDEAS_FILE, { ideas: [] });
40244
+ const batchIdeas = loadJson3(IDEAS_FILE, { ideas: [] });
38708
40245
  const shippedReady = batchIdeas.ideas.filter((i) => {
38709
40246
  if (i.stage !== "shipped") return false;
38710
40247
  const v2 = i.verification;
@@ -38800,6 +40337,12 @@ async function executeTask(task, config, businessContext) {
38800
40337
  trackEvent("heartbeat_task", { taskType: taskTypePrefix, success: result.success });
38801
40338
  return result.success;
38802
40339
  }
40340
+ if (isGrowthCampaignTask(taskId)) {
40341
+ const result = await executeGrowthCampaignTask(taskId, taskDesc, businessContext);
40342
+ log(`Growth campaign task: ${result.success ? "SUCCESS" : "FAILED"} \u2014 ${result.output.slice(0, 200)}`);
40343
+ trackEvent("heartbeat_task", { taskType: taskTypePrefix, success: result.success });
40344
+ return result.success;
40345
+ }
38803
40346
  if (isLaunchCampaignTask(taskId)) {
38804
40347
  const result = await executeLaunchCampaignTask(taskId, taskDesc, businessContext);
38805
40348
  log(`Campaign task: ${result.success ? "SUCCESS" : "FAILED"} \u2014 ${result.output.slice(0, 200)}`);
@@ -38828,7 +40371,7 @@ async function executeTask(task, config, businessContext) {
38828
40371
  }
38829
40372
  }
38830
40373
  async function executeReviewInbox() {
38831
- const ideasData = loadJson(IDEAS_FILE, { ideas: [] });
40374
+ const ideasData = loadJson3(IDEAS_FILE, { ideas: [] });
38832
40375
  const inboxIdeas = ideasData.ideas.filter((i) => i.stage === "inbox");
38833
40376
  if (inboxIdeas.length === 0) {
38834
40377
  log("No ideas in inbox to review");
@@ -38842,17 +40385,17 @@ async function executeReviewInbox() {
38842
40385
  log("Inbox review complete (manual prioritization needed)");
38843
40386
  }
38844
40387
  async function runAnalysis(type) {
38845
- return new Promise((resolve3, reject) => {
40388
+ return new Promise((resolve4, reject) => {
38846
40389
  log(`Running ${type} analysis...`);
38847
40390
  const { command: runCmd, args: runArgs } = resolveScript(__dirname, "analyze.ts");
38848
- const childProcess = (0, import_child_process11.spawn)(runCmd, [...runArgs, `--type=${type}`], {
40391
+ const childProcess = (0, import_child_process13.spawn)(runCmd, [...runArgs, `--type=${type}`], {
38849
40392
  cwd: ROOT_DIR,
38850
40393
  stdio: "inherit"
38851
40394
  });
38852
40395
  childProcess.on("close", (code) => {
38853
40396
  if (code === 0) {
38854
40397
  log(`${type} analysis completed successfully`);
38855
- resolve3();
40398
+ resolve4();
38856
40399
  } else {
38857
40400
  reject(new Error(`Analysis exited with code ${code}`));
38858
40401
  }
@@ -38864,7 +40407,7 @@ async function runAnalysis(type) {
38864
40407
  }
38865
40408
  function updateTodoFile(taskId, completed) {
38866
40409
  try {
38867
- let content = fs35.readFileSync(TODO_FILE, "utf-8");
40410
+ let content = fs42.readFileSync(TODO_FILE, "utf-8");
38868
40411
  const uncheckedPattern = new RegExp(`- \\[ \\] \`${taskId}\``, "g");
38869
40412
  const checkedPattern = new RegExp(`- \\[x\\] \`${taskId}\``, "g");
38870
40413
  if (completed) {
@@ -38872,7 +40415,7 @@ function updateTodoFile(taskId, completed) {
38872
40415
  } else {
38873
40416
  content = content.replace(checkedPattern, `- [ ] \`${taskId}\``);
38874
40417
  }
38875
- fs35.writeFileSync(TODO_FILE, content);
40418
+ fs42.writeFileSync(TODO_FILE, content);
38876
40419
  log(`Updated TODO.md: ${taskId} marked as ${completed ? "completed" : "pending"}`);
38877
40420
  } catch (error) {
38878
40421
  log(`Failed to update TODO.md: ${error instanceof Error ? error.message : "Unknown error"}`);
@@ -38991,8 +40534,8 @@ async function runSingleHeartbeat(beatNumber = 1) {
38991
40534
  if (vibeResult.warning) log(vibeResult.warning);
38992
40535
  log(`Vibe consumed (${vibeResult.vibes_remaining} remaining${vibeResult.plan ? `, plan: ${vibeResult.plan}` : ""}${vibeResult.offline ? ", offline" : ""})`);
38993
40536
  }
38994
- const commandsDir = path30.join(PROJECT_DIR, ".claude", "commands");
38995
- if (!fs35.existsSync(commandsDir)) {
40537
+ const commandsDir = path34.join(PROJECT_DIR, ".claude", "commands");
40538
+ if (!fs42.existsSync(commandsDir)) {
38996
40539
  log("Slash commands missing \u2014 auto-scaffolding...");
38997
40540
  scaffoldSlashCommands(PROJECT_DIR);
38998
40541
  }
@@ -39029,7 +40572,7 @@ async function runSingleHeartbeat(beatNumber = 1) {
39029
40572
  if (systemHealth.all_stuck && !DRY_RUN) {
39030
40573
  log(`Auto-recovering: deferring ${systemHealth.stuck_idea_ids.length} stuck idea(s)...`);
39031
40574
  autoRecoverStuckIdeas(systemHealth.stuck_idea_ids);
39032
- const reloadedIdeas = loadJson(IDEAS_FILE, { ideas: [] });
40575
+ const reloadedIdeas = loadJson3(IDEAS_FILE, { ideas: [] });
39033
40576
  state.ideas = reloadedIdeas.ideas;
39034
40577
  }
39035
40578
  const liveKPIs = await fetchLiveKPIs();
@@ -39040,7 +40583,7 @@ async function runSingleHeartbeat(beatNumber = 1) {
39040
40583
  };
39041
40584
  if (liveKPIs && !DRY_RUN) {
39042
40585
  updateGoalsWithKPIs(liveKPIs);
39043
- const updatedGoals = loadJson(GOALS_FILE, { goals: [] });
40586
+ const updatedGoals = loadJson3(GOALS_FILE, { goals: [] });
39044
40587
  state.goals = updatedGoals.goals;
39045
40588
  updateBusinessContext(businessContext, liveKPIs);
39046
40589
  log("Updated business context with live KPIs");
@@ -39176,8 +40719,8 @@ async function runSingleHeartbeat(beatNumber = 1) {
39176
40719
  }
39177
40720
  }
39178
40721
  if (!inProgressIdeas.some((i) => (i.implementation.sub_tasks || []).some((st) => st.status === "pending"))) {
39179
- const fallbackHypotheses = loadJson(HYPOTHESES_FILE, { hypotheses: [] }).hypotheses || [];
39180
- const fallbackEpics = loadJson(ROADMAP_FILE, { epics: [] }).epics || [];
40722
+ const fallbackHypotheses = loadJson3(HYPOTHESES_FILE, { hypotheses: [] }).hypotheses || [];
40723
+ const fallbackEpics = loadJson3(ROADMAP_FILE, { epics: [] }).epics || [];
39181
40724
  const approvedIdeas = state.ideas.filter(
39182
40725
  (i) => i.stage === "approved" && (i.implementation?.decomposition_attempts || 0) < fallbackMaxRetries
39183
40726
  ).sort((a, b) => scoreIdea(b, state.goals, fallbackHypotheses, fallbackEpics) - scoreIdea(a, state.goals, fallbackHypotheses, fallbackEpics));
@@ -39205,7 +40748,7 @@ async function runSingleHeartbeat(beatNumber = 1) {
39205
40748
  }
39206
40749
  if (!DRY_RUN) {
39207
40750
  const statusContent = generateStatusContent(state, alerts, taskToDo, reasoning, systemHealth);
39208
- fs35.writeFileSync(STATUS_FILE, statusContent);
40751
+ fs42.writeFileSync(STATUS_FILE, statusContent);
39209
40752
  log("Updated STATUS.md");
39210
40753
  } else {
39211
40754
  log("[DRY RUN] Would update STATUS.md");
@@ -39320,13 +40863,13 @@ ${"=".repeat(60)}
39320
40863
  `);
39321
40864
  }
39322
40865
  function saveSessionToJson(summary) {
39323
- const sessionsFile = path30.join(DATA_DIR, "heartbeat-sessions.json");
39324
- const sessionsData = loadJson(sessionsFile, { sessions: [] });
40866
+ const sessionsFile = path34.join(DATA_DIR, "heartbeat-sessions.json");
40867
+ const sessionsData = loadJson3(sessionsFile, { sessions: [] });
39325
40868
  sessionsData.sessions.push(summary);
39326
40869
  if (sessionsData.sessions.length > 50) {
39327
40870
  sessionsData.sessions = sessionsData.sessions.slice(-50);
39328
40871
  }
39329
- saveJson(sessionsFile, sessionsData);
40872
+ saveJson2(sessionsFile, sessionsData);
39330
40873
  log(`Session saved to ${sessionsFile}`);
39331
40874
  }
39332
40875
  async function runContinuousSession() {