vibebusiness 1.2.49 → 1.2.53

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 (101) hide show
  1. package/.next/standalone/.env +1 -1
  2. package/.next/standalone/.next/BUILD_ID +1 -1
  3. package/.next/standalone/.next/app-build-manifest.json +20 -20
  4. package/.next/standalone/.next/app-path-routes-manifest.json +1 -1
  5. package/.next/standalone/.next/build-manifest.json +4 -4
  6. package/.next/standalone/.next/prerender-manifest.json +1 -1
  7. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  9. package/.next/standalone/.next/server/app/_not-found.rsc +1 -1
  10. package/.next/standalone/.next/server/app/api/analyze/route.js +1 -1
  11. package/.next/standalone/.next/server/app/api/analyze/route.js.nft.json +1 -1
  12. package/.next/standalone/.next/server/app/api/config/route.js.nft.json +1 -1
  13. package/.next/standalone/.next/server/app/api/epics/[id]/ideas/route.js.nft.json +1 -1
  14. package/.next/standalone/.next/server/app/api/epics/[id]/route.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/api/epics/route.js.nft.json +1 -1
  16. package/.next/standalone/.next/server/app/api/goals/[id]/kpis/route.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/api/goals/[id]/route.js.nft.json +1 -1
  18. package/.next/standalone/.next/server/app/api/goals/route.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/api/hypotheses/[id]/route.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/api/hypotheses/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 +1 -1
  23. package/.next/standalone/.next/server/app/api/ideas/[id]/implement/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/ideas/[id]/route.js.nft.json +1 -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 +1 -1
  29. package/.next/standalone/.next/server/app/api/kpis/refresh/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/provider-status/route.js +1 -0
  31. package/.next/standalone/.next/server/app/api/provider-status/route.js.nft.json +1 -0
  32. package/.next/standalone/.next/server/app/api/social/[id]/publish/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/social/[id]/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/goals/[id]/page_client-reference-manifest.js +1 -1
  36. package/.next/standalone/.next/server/app/goals/page.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/goals/page_client-reference-manifest.js +1 -1
  38. package/.next/standalone/.next/server/app/hypotheses/[id]/page_client-reference-manifest.js +1 -1
  39. package/.next/standalone/.next/server/app/hypotheses/page.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/hypotheses/page_client-reference-manifest.js +1 -1
  41. package/.next/standalone/.next/server/app/ideas/[id]/page.js +1 -1
  42. package/.next/standalone/.next/server/app/ideas/[id]/page.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/ideas/[id]/page_client-reference-manifest.js +1 -1
  44. package/.next/standalone/.next/server/app/page.js.nft.json +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/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/roadmap/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/sessions/page.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  51. package/.next/standalone/.next/server/app/settings/page.js +1 -1
  52. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/app/settings.html +1 -1
  54. package/.next/standalone/.next/server/app/settings.rsc +2 -2
  55. package/.next/standalone/.next/server/app/social/page.js +1 -1
  56. package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/app/social.html +1 -1
  58. package/.next/standalone/.next/server/app/social.rsc +2 -2
  59. package/.next/standalone/.next/server/app-paths-manifest.json +12 -11
  60. package/.next/standalone/.next/server/chunks/3794.js +92 -17
  61. package/.next/standalone/.next/server/middleware-build-manifest.js +1 -1
  62. package/.next/standalone/.next/server/pages/404.html +1 -1
  63. package/.next/standalone/.next/server/pages/500.html +1 -1
  64. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  65. package/.next/standalone/data/business-context.json +60 -6
  66. package/.next/standalone/data/goals.json +50 -10
  67. package/.next/standalone/data/ideas.json +1278 -597
  68. package/.next/standalone/data/implementations.json +101 -0
  69. package/.next/standalone/data/posthog.json +68 -0
  70. package/.next/standalone/data/sessions.json +5 -4
  71. package/.next/standalone/data/social.json +86 -3
  72. package/.next/standalone/package.json +1 -1
  73. package/.next/standalone/scripts/analyze.ts +10 -0
  74. package/.next/standalone/scripts/implement.ts +37 -30
  75. package/.next/standalone/scripts/skills/social-media.ts +172 -2
  76. package/.next/static/chunks/59-76eecfb6e8216043.js +1 -0
  77. package/.next/static/chunks/app/goals/[id]/page-051064c3146131cc.js +1 -0
  78. package/.next/static/chunks/app/ideas/[id]/page-adaeb619cd0425e9.js +1 -0
  79. package/.next/static/chunks/app/roadmap/[id]/page-3822586c0d64fff1.js +1 -0
  80. package/.next/static/chunks/app/settings/page-2204cc936ec9474b.js +1 -0
  81. package/.next/static/chunks/app/social/page-018893f87b308651.js +1 -0
  82. package/.next/static/chunks/{main-61d2f25883998186.js → main-c99f3473a63aa803.js} +1 -1
  83. package/.next/static/css/d8bd6d69d1ff97e3.css +3 -0
  84. package/dist/bin/vibebusiness.js +12 -0
  85. package/dist/scripts/analyze.js +170 -68
  86. package/dist/scripts/chat.js +137 -36
  87. package/dist/scripts/heartbeat.js +617 -348
  88. package/dist/scripts/implement.js +218 -70
  89. package/dist/scripts/init.js +164 -16
  90. package/dist/scripts/provider.js +428 -0
  91. package/dist/scripts/scan.js +44 -5
  92. package/package.json +1 -1
  93. package/.next/static/chunks/59-a053b1c0e85128de.js +0 -1
  94. package/.next/static/chunks/app/goals/[id]/page-8dbeab5cc8cf0988.js +0 -1
  95. package/.next/static/chunks/app/ideas/[id]/page-89e3625db9017166.js +0 -1
  96. package/.next/static/chunks/app/roadmap/[id]/page-f437e783039534c4.js +0 -1
  97. package/.next/static/chunks/app/settings/page-d2d630a799b6b495.js +0 -1
  98. package/.next/static/chunks/app/social/page-69e480936711ccf2.js +0 -1
  99. package/.next/static/css/179dd3e0738c2fe4.css +0 -3
  100. /package/.next/static/{zIIaTqrawBK1MmHg37JOr → p1Sl4kPMQcYC3c1LgbNSP}/_buildManifest.js +0 -0
  101. /package/.next/static/{zIIaTqrawBK1MmHg37JOr → p1Sl4kPMQcYC3c1LgbNSP}/_ssgManifest.js +0 -0
@@ -9406,7 +9406,11 @@ var RestAPIAdapter = class {
9406
9406
  this.headers[key] = this.interpolateEnvVars(value);
9407
9407
  }
9408
9408
  }
9409
- this.fieldMapping = config.field_mapping || {};
9409
+ const raw = config.field_mapping || {};
9410
+ this.fieldMapping = {};
9411
+ for (const [k, v2] of Object.entries(raw)) {
9412
+ if (typeof v2 === "string") this.fieldMapping[k] = v2;
9413
+ }
9410
9414
  this.transforms = config.transforms || {};
9411
9415
  }
9412
9416
  /**
@@ -9496,7 +9500,7 @@ var ManualAdapter = class {
9496
9500
  };
9497
9501
 
9498
9502
  // src/lib/kpi-adapters/posthog.ts
9499
- var PostHogAdapter = class {
9503
+ var PostHogAdapter = class _PostHogAdapter {
9500
9504
  id;
9501
9505
  type = "posthog";
9502
9506
  fieldMapping;
@@ -9508,7 +9512,8 @@ var PostHogAdapter = class {
9508
9512
  this.fieldMapping = config.field_mapping || {};
9509
9513
  const hostEnv = config.config?.url_env || "POSTHOG_API_HOST";
9510
9514
  const projectEnv = config.config?.project_id_env || "POSTHOG_PROJECT_ID";
9511
- this.host = process.env[hostEnv] || process.env["NEXT_PUBLIC_POSTHOG_HOST"] || "https://us.i.posthog.com";
9515
+ const rawHost = process.env[hostEnv] || process.env["NEXT_PUBLIC_POSTHOG_HOST"] || "https://us.posthog.com";
9516
+ this.host = rawHost.replace("://us.i.posthog.com", "://us.posthog.com");
9512
9517
  this.projectId = process.env[projectEnv] || "";
9513
9518
  this.apiKey = process.env["POSTHOG_PERSONAL_API_KEY"] || "";
9514
9519
  if (!this.apiKey) {
@@ -9527,26 +9532,32 @@ var PostHogAdapter = class {
9527
9532
  const metadata = {};
9528
9533
  const end = endDate || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
9529
9534
  const start = startDate || new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3).toISOString().split("T")[0];
9530
- const eventNames = Array.from(new Set(Object.values(this.fieldMapping)));
9531
- if (eventNames.length === 0) {
9535
+ const queryMap = /* @__PURE__ */ new Map();
9536
+ for (const [kpiId, mapping] of Object.entries(this.fieldMapping)) {
9537
+ const query = _PostHogAdapter.normalizeMapping(mapping);
9538
+ const key = _PostHogAdapter.queryCacheKey(query);
9539
+ const entry = queryMap.get(key);
9540
+ if (entry) {
9541
+ entry.kpiIds.push(kpiId);
9542
+ } else {
9543
+ queryMap.set(key, { query, kpiIds: [kpiId] });
9544
+ }
9545
+ }
9546
+ if (queryMap.size === 0) {
9532
9547
  return { values, metadata, fetched_at: (/* @__PURE__ */ new Date()).toISOString() };
9533
9548
  }
9534
- for (const eventName of eventNames) {
9549
+ for (const [key, { query, kpiIds }] of Array.from(queryMap.entries())) {
9535
9550
  try {
9536
- const count = await this.fetchEventCount(eventName, start, end);
9537
- metadata[eventName] = { count, start, end };
9538
- for (const [kpiId, mappedEvent] of Object.entries(this.fieldMapping)) {
9539
- if (mappedEvent === eventName) {
9540
- values[kpiId] = count;
9541
- }
9551
+ const count = await this.fetchEventCount(query.event, start, end, query.properties);
9552
+ metadata[key] = { count, start, end, event: query.event, properties: query.properties };
9553
+ for (const kpiId of kpiIds) {
9554
+ values[kpiId] = count;
9542
9555
  }
9543
9556
  } catch (error) {
9544
9557
  const msg = error instanceof Error ? error.message : String(error);
9545
- console.warn(`PostHog: failed to fetch "${eventName}": ${msg}`);
9546
- for (const [kpiId, mappedEvent] of Object.entries(this.fieldMapping)) {
9547
- if (mappedEvent === eventName) {
9548
- values[kpiId] = null;
9549
- }
9558
+ console.warn(`PostHog: failed to fetch "${key}": ${msg}`);
9559
+ for (const kpiId of kpiIds) {
9560
+ values[kpiId] = null;
9550
9561
  }
9551
9562
  }
9552
9563
  }
@@ -9556,6 +9567,21 @@ var PostHogAdapter = class {
9556
9567
  fetched_at: (/* @__PURE__ */ new Date()).toISOString()
9557
9568
  };
9558
9569
  }
9570
+ /** Normalize a field_mapping value (string or object) into an EventQuery. */
9571
+ static normalizeMapping(mapping) {
9572
+ if (typeof mapping === "string") {
9573
+ return { event: mapping };
9574
+ }
9575
+ return { event: mapping.event, properties: mapping.properties };
9576
+ }
9577
+ /** Stable cache key for deduplicating identical queries. */
9578
+ static queryCacheKey(query) {
9579
+ if (!query.properties || Object.keys(query.properties).length === 0) {
9580
+ return query.event;
9581
+ }
9582
+ const sortedProps = Object.entries(query.properties).sort(([a], [b]) => a.localeCompare(b));
9583
+ return `${query.event}[${sortedProps.map(([k, v2]) => `${k}=${v2}`).join(",")}]`;
9584
+ }
9559
9585
  async healthCheck() {
9560
9586
  try {
9561
9587
  const res = await fetch(`${this.host}/api/projects/${this.projectId}/`, {
@@ -9569,21 +9595,29 @@ var PostHogAdapter = class {
9569
9595
  }
9570
9596
  }
9571
9597
  /**
9572
- * Fetch total event count from PostHog Trends API for a given date range.
9598
+ * Fetch total event count from PostHog HogQL Query API for a given date range.
9599
+ * Optionally filters by event properties (e.g. { command: "init" }).
9573
9600
  */
9574
- async fetchEventCount(eventName, dateFrom, dateTo) {
9575
- const url = `${this.host}/api/projects/${this.projectId}/insights/trend/`;
9601
+ async fetchEventCount(eventName, dateFrom, dateTo, properties) {
9602
+ const url = `${this.host}/api/projects/${this.projectId}/query/`;
9603
+ const conditions = [
9604
+ `event = '${eventName.replace(/'/g, "\\'")}'`,
9605
+ `timestamp >= toDate('${dateFrom}')`,
9606
+ `timestamp <= toDate('${dateTo}') + INTERVAL 1 DAY`
9607
+ ];
9608
+ if (properties) {
9609
+ for (const [key, value] of Object.entries(properties)) {
9610
+ const safeKey = key.replace(/'/g, "\\'");
9611
+ const safeVal = value.replace(/'/g, "\\'");
9612
+ conditions.push(`properties['${safeKey}'] = '${safeVal}'`);
9613
+ }
9614
+ }
9615
+ const hogql = `SELECT count() AS cnt FROM events WHERE ${conditions.join(" AND ")}`;
9576
9616
  const body = {
9577
- events: [
9578
- {
9579
- id: eventName,
9580
- math: "total",
9581
- type: "events"
9582
- }
9583
- ],
9584
- date_from: dateFrom,
9585
- date_to: dateTo,
9586
- display: "ActionsLineGraph"
9617
+ query: {
9618
+ kind: "HogQLQuery",
9619
+ query: hogql
9620
+ }
9587
9621
  };
9588
9622
  const res = await fetch(url, {
9589
9623
  method: "POST",
@@ -9597,9 +9631,7 @@ var PostHogAdapter = class {
9597
9631
  throw new Error(`PostHog API returned ${res.status}: ${await res.text()}`);
9598
9632
  }
9599
9633
  const data = await res.json();
9600
- const result = data?.result?.[0];
9601
- if (!result) return 0;
9602
- return result.count ?? result.aggregated_value ?? 0;
9634
+ return data?.results?.[0]?.[0] ?? 0;
9603
9635
  }
9604
9636
  };
9605
9637
 
@@ -9613,7 +9645,11 @@ var WaitlistFileAdapter = class {
9613
9645
  fieldMapping;
9614
9646
  constructor(config) {
9615
9647
  this.id = config.id;
9616
- this.fieldMapping = config.field_mapping || {};
9648
+ const raw = config.field_mapping || {};
9649
+ this.fieldMapping = {};
9650
+ for (const [k, v2] of Object.entries(raw)) {
9651
+ if (typeof v2 === "string") this.fieldMapping[k] = v2;
9652
+ }
9617
9653
  const relativePath = config.config?.path || "website/data/waitlist.json";
9618
9654
  this.filePath = path.isAbsolute(relativePath) ? relativePath : path.resolve(process.cwd(), relativePath);
9619
9655
  }
@@ -30597,6 +30633,17 @@ var fs17 = __toESM(require("fs"));
30597
30633
  var import_child_process6 = require("child_process");
30598
30634
  var fs16 = __toESM(require("fs"));
30599
30635
  var path11 = __toESM(require("path"));
30636
+ var CREDENTIALS_DIR = path11.join(process.env.HOME || "", ".vibebusiness");
30637
+ var CREDENTIALS_FILE = path11.join(CREDENTIALS_DIR, "credentials.json");
30638
+ function loadCredentials2() {
30639
+ try {
30640
+ if (fs16.existsSync(CREDENTIALS_FILE)) {
30641
+ return JSON.parse(fs16.readFileSync(CREDENTIALS_FILE, "utf-8"));
30642
+ }
30643
+ } catch {
30644
+ }
30645
+ return {};
30646
+ }
30600
30647
  function detectClaudeCLI(customPath) {
30601
30648
  const claudeBin = customPath || "claude";
30602
30649
  try {
@@ -30629,13 +30676,27 @@ function detectClaudeCLI(customPath) {
30629
30676
  }
30630
30677
  function detectBYOKKeys() {
30631
30678
  if (process.env.ANTHROPIC_API_KEY) {
30632
- return { provider: "anthropic-api", key: process.env.ANTHROPIC_API_KEY };
30679
+ return { provider: "anthropic-api", key: process.env.ANTHROPIC_API_KEY, source: "env" };
30633
30680
  }
30634
30681
  if (process.env.OPENAI_API_KEY) {
30635
- return { provider: "openai-api", key: process.env.OPENAI_API_KEY };
30682
+ return { provider: "openai-api", key: process.env.OPENAI_API_KEY, source: "env" };
30636
30683
  }
30637
30684
  if (process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY) {
30638
- return { provider: "google-api", key: process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY };
30685
+ return { provider: "google-api", key: process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY, source: "env" };
30686
+ }
30687
+ const creds = loadCredentials2();
30688
+ if (creds.ANTHROPIC_API_KEY) {
30689
+ process.env.ANTHROPIC_API_KEY = creds.ANTHROPIC_API_KEY;
30690
+ return { provider: "anthropic-api", key: creds.ANTHROPIC_API_KEY, source: "credentials" };
30691
+ }
30692
+ if (creds.OPENAI_API_KEY) {
30693
+ process.env.OPENAI_API_KEY = creds.OPENAI_API_KEY;
30694
+ return { provider: "openai-api", key: creds.OPENAI_API_KEY, source: "credentials" };
30695
+ }
30696
+ if (creds.GOOGLE_API_KEY || creds.GEMINI_API_KEY) {
30697
+ const key = creds.GOOGLE_API_KEY || creds.GEMINI_API_KEY;
30698
+ process.env.GOOGLE_API_KEY = key;
30699
+ return { provider: "google-api", key, source: "credentials" };
30639
30700
  }
30640
30701
  return null;
30641
30702
  }
@@ -30684,6 +30745,9 @@ function invokeClaudeCLI(options, claudePath) {
30684
30745
  if (options.model) {
30685
30746
  args2.push("--model", options.model);
30686
30747
  }
30748
+ if (options.jsonSchema) {
30749
+ args2.push("--output-format", "json", "--json-schema", JSON.stringify(options.jsonSchema));
30750
+ }
30687
30751
  if (options.claudeFlags) {
30688
30752
  args2.push(...options.claudeFlags);
30689
30753
  }
@@ -30752,7 +30816,18 @@ async function invokeAnthropicAPI(options) {
30752
30816
  };
30753
30817
  }
30754
30818
  try {
30755
- const model = options.model || "claude-sonnet-4-5-20250929";
30819
+ const model = options.model || "claude-sonnet-4-6";
30820
+ let prompt = options.prompt;
30821
+ if (options.jsonSchema) {
30822
+ prompt += `
30823
+
30824
+ IMPORTANT: Respond with valid JSON matching this schema:
30825
+ ${JSON.stringify(options.jsonSchema, null, 2)}
30826
+
30827
+ Output ONLY the JSON object, no markdown code blocks or other text.`;
30828
+ } else if (options.expectJson) {
30829
+ prompt += "\n\nIMPORTANT: Respond with valid JSON only. No markdown code blocks or other text.";
30830
+ }
30756
30831
  const response = await fetch("https://api.anthropic.com/v1/messages", {
30757
30832
  method: "POST",
30758
30833
  headers: {
@@ -30763,7 +30838,7 @@ async function invokeAnthropicAPI(options) {
30763
30838
  body: JSON.stringify({
30764
30839
  model,
30765
30840
  max_tokens: 16384,
30766
- messages: [{ role: "user", content: options.prompt }]
30841
+ messages: [{ role: "user", content: prompt }]
30767
30842
  }),
30768
30843
  signal: AbortSignal.timeout(options.timeoutMs || 3e5)
30769
30844
  });
@@ -30817,6 +30892,27 @@ async function invokeAI(options) {
30817
30892
  };
30818
30893
  }
30819
30894
  }
30895
+ function requireClaudeCLI(feature) {
30896
+ const cli = detectClaudeCLI();
30897
+ if (cli.found) {
30898
+ return cli.path;
30899
+ }
30900
+ const featureLabel = feature ? ` (${feature})` : "";
30901
+ const byok = detectBYOKKeys();
30902
+ if (byok) {
30903
+ throw new Error(
30904
+ `This feature${featureLabel} requires Claude Code CLI for file and tool access.
30905
+ Your API key is detected and works for reasoning tasks (heartbeat, idea evaluation),
30906
+ but codebase analysis and implementation need the full CLI.
30907
+ Install it with: npm install -g @anthropic-ai/claude-code`
30908
+ );
30909
+ }
30910
+ throw new Error(
30911
+ `No AI provider found${featureLabel}.
30912
+ Install Claude Code CLI: npm install -g @anthropic-ai/claude-code
30913
+ Or set an API key: export ANTHROPIC_API_KEY=sk-ant-...`
30914
+ );
30915
+ }
30820
30916
 
30821
30917
  // scripts/lib/social/ai-content-generator.ts
30822
30918
  function readJsonSafe2(filePath, fallback) {
@@ -30878,6 +30974,8 @@ CONTEXT: ${opts.context}
30878
30974
  HISTORICAL PERFORMANCE:
30879
30975
  ${formatPastPerformance(opts.pastPerformance ?? [])}
30880
30976
 
30977
+ IMPORTANT: Only reference facts and numbers from the CONTEXT above. Never fabricate personal history, timelines, or metrics not provided.
30978
+
30881
30979
  Generate ONE tweet (max 280 characters). Focus on being genuinely interesting, not salesy.
30882
30980
 
30883
30981
  Respond in this exact JSON format (no markdown, no code blocks):
@@ -30929,6 +31027,7 @@ RULES:
30929
31027
  - Number threads (1/, 2/, etc.) only if it helps clarity
30930
31028
  - Include #buildinpublic in the first or last tweet only
30931
31029
  - Make each tweet stand alone \u2014 people may see any tweet in isolation
31030
+ - IMPORTANT: Only reference facts and numbers from the CONTEXT above. Never fabricate personal history, timelines, or metrics not provided.
30932
31031
 
30933
31032
  Respond in this exact JSON format (no markdown, no code blocks):
30934
31033
  {
@@ -30992,6 +31091,113 @@ Respond in this exact JSON format (no markdown, no code blocks):
30992
31091
  };
30993
31092
  }
30994
31093
  }
31094
+ async function generateWeekPlan(opts) {
31095
+ const positioning = opts.positioning ?? readJsonSafe2(POSITIONING_FILE, null);
31096
+ const today = /* @__PURE__ */ new Date();
31097
+ const dayOfWeek = today.getDay();
31098
+ const daysUntilMonday = dayOfWeek === 0 ? 1 : dayOfWeek === 1 ? 0 : 8 - dayOfWeek;
31099
+ const monday = new Date(today);
31100
+ monday.setDate(today.getDate() + daysUntilMonday);
31101
+ const weekDates = [];
31102
+ const dayNames = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"];
31103
+ for (let i = 0; i < 7; i++) {
31104
+ const d2 = new Date(monday);
31105
+ d2.setDate(monday.getDate() + i);
31106
+ weekDates.push({ day: dayNames[i], date: d2.toISOString().split("T")[0] });
31107
+ }
31108
+ const prompt = `You are a social media strategist for a developer-focused indie hacker audience on X/Twitter.
31109
+
31110
+ ${ALGORITHM_KNOWLEDGE}
31111
+
31112
+ ${HOOK_PATTERNS}
31113
+
31114
+ PRODUCT CONTEXT:
31115
+ - Product: ${positioning?.current?.tagline ?? "VibeBusiness"}
31116
+ - Value prop: ${positioning?.current?.value_proposition ?? "AI-powered business automation"}
31117
+ - Target audience: ${positioning?.current?.target_audience?.primary ?? "Indie hackers and solo founders"}
31118
+
31119
+ CURRENT GOALS:
31120
+ ${opts.goalsContext || "No specific goals set."}
31121
+
31122
+ RECENTLY SHIPPED:
31123
+ ${opts.recentShips || "No recent ships."}
31124
+
31125
+ KPI HIGHLIGHTS:
31126
+ ${opts.kpiHighlights || "No KPI data yet."}
31127
+
31128
+ HISTORICAL PERFORMANCE:
31129
+ ${formatPastPerformance(opts.pastPerformance ?? [])}
31130
+
31131
+ WEEK DATES:
31132
+ ${weekDates.map((d2) => `${d2.day}: ${d2.date}`).join("\n")}
31133
+
31134
+ CRITICAL \u2014 FACTUAL ACCURACY:
31135
+ - ONLY reference facts, numbers, timelines, and events explicitly present in the context above.
31136
+ - NEVER fabricate personal history, backstory, or timeline details (e.g., "6 months ago I...", "I was stuck for X months").
31137
+ - NEVER invent metrics, user counts, or milestones that aren't in the KPI HIGHLIGHTS or RECENTLY SHIPPED sections.
31138
+ - If the context doesn't provide enough detail for a story hook, use a different hook pattern (question, contrarian, list-tease) instead of inventing a narrative.
31139
+ - It's better to write a shorter, honest tweet than a longer, fabricated one.
31140
+
31141
+ Plan a full week of content (Monday-Sunday). Rules:
31142
+ - 5 content days (Mon-Fri) + 2 rest days (Sat-Sun)
31143
+ - Include 1-2 threads per week (threads get 200-300% more reach)
31144
+ - Mix content types: ship, viral, thread, milestone, insight, question
31145
+ - Align content with current goals (especially behind-schedule ones)
31146
+ - Use diverse hook patterns \u2014 don't repeat the same hook type consecutively
31147
+ - Write FULL tweet text for each content day (max 280 chars for single tweets)
31148
+ - For threads, provide all tweet texts (5-7 tweets each, max 280 chars per tweet)
31149
+ - Each rest day should have type "rest" with empty content
31150
+
31151
+ Respond in this exact JSON format (no markdown, no code blocks):
31152
+ {
31153
+ "days": [
31154
+ {
31155
+ "day": "monday",
31156
+ "date": "YYYY-MM-DD",
31157
+ "type": "thread",
31158
+ "topic": "topic description",
31159
+ "content": "First tweet / hook text",
31160
+ "thread_tweets": ["tweet 1", "tweet 2", "..."],
31161
+ "hook_type": "story",
31162
+ "rationale": "Why this content on this day"
31163
+ },
31164
+ {
31165
+ "day": "tuesday",
31166
+ "date": "YYYY-MM-DD",
31167
+ "type": "viral",
31168
+ "topic": "ship",
31169
+ "content": "Full tweet text here",
31170
+ "hook_type": "bold-claim",
31171
+ "rationale": "Why this content on this day"
31172
+ }
31173
+ ],
31174
+ "strategy_summary": "One paragraph explaining the week's strategy"
31175
+ }`;
31176
+ const result = await invokeAI({ prompt, model: "sonnet" });
31177
+ try {
31178
+ const parsed = JSON.parse(extractJson(result.output));
31179
+ for (const day of parsed.days) {
31180
+ if (day.type === "rest") continue;
31181
+ if (day.content && day.content.length > 280) {
31182
+ day.content = day.content.slice(0, 277).trimEnd() + "...";
31183
+ }
31184
+ if (day.thread_tweets) {
31185
+ day.thread_tweets = day.thread_tweets.map(
31186
+ (t) => t.length > 280 ? t.slice(0, 277).trimEnd() + "..." : t
31187
+ );
31188
+ }
31189
+ }
31190
+ return {
31191
+ days: parsed.days || [],
31192
+ strategy_summary: parsed.strategy_summary || ""
31193
+ };
31194
+ } catch {
31195
+ return {
31196
+ days: [],
31197
+ strategy_summary: "AI failed to generate a valid week plan."
31198
+ };
31199
+ }
31200
+ }
30995
31201
  function extractJson(text) {
30996
31202
  const codeBlockMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
30997
31203
  if (codeBlockMatch) return codeBlockMatch[1].trim();
@@ -31078,6 +31284,7 @@ function createDraft(state, text, source, sourceRef, mediaPaths = null) {
31078
31284
  }
31079
31285
  var SOCIAL_PREFIXES = [
31080
31286
  "social-setup-x",
31287
+ "social-plan-week",
31081
31288
  "social-draft-ship-visual",
31082
31289
  "social-draft-ship",
31083
31290
  "social-draft-update",
@@ -31098,6 +31305,9 @@ async function executeSocialTask(taskId, description, _businessContext) {
31098
31305
  if (taskId === "social-setup-x") {
31099
31306
  return await executeSetupX();
31100
31307
  }
31308
+ if (taskId === "social-plan-week") {
31309
+ return await executePlanWeek();
31310
+ }
31101
31311
  if (taskId === "social-draft-ship-visual") {
31102
31312
  return executeDraftShipVisual();
31103
31313
  }
@@ -31161,6 +31371,10 @@ function readSocialFreshness() {
31161
31371
  bestEngagementRate = best.engagement_rate;
31162
31372
  }
31163
31373
  const threadCount = state.drafts.filter((d2) => d2.is_thread && d2.status === "published").length;
31374
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
31375
+ const scheduledDraftCount = state.drafts.filter(
31376
+ (d2) => d2.status === "draft" && d2.scheduled_date && d2.scheduled_date >= today
31377
+ ).length;
31164
31378
  return {
31165
31379
  configured: state.credentials_valid,
31166
31380
  credentials_valid: state.credentials_valid,
@@ -31172,7 +31386,8 @@ function readSocialFreshness() {
31172
31386
  avg_engagement_rate: avgEngagementRate,
31173
31387
  best_tweet_id: bestTweetId,
31174
31388
  best_engagement_rate: bestEngagementRate,
31175
- thread_count: threadCount
31389
+ thread_count: threadCount,
31390
+ scheduled_draft_count: scheduledDraftCount
31176
31391
  };
31177
31392
  }
31178
31393
  async function executeSetupX() {
@@ -31417,6 +31632,117 @@ async function executeDraftThread(topic, description) {
31417
31632
  ].join("\n")
31418
31633
  };
31419
31634
  }
31635
+ async function executePlanWeek() {
31636
+ const state = loadState3();
31637
+ const pastPerformance = collectEngagementHistory(state);
31638
+ let positioning = null;
31639
+ try {
31640
+ if (fs18.existsSync(POSITIONING_FILE)) {
31641
+ positioning = JSON.parse(fs18.readFileSync(POSITIONING_FILE, "utf-8"));
31642
+ }
31643
+ } catch {
31644
+ }
31645
+ let goalsContext = "";
31646
+ try {
31647
+ if (fs18.existsSync(GOALS_FILE)) {
31648
+ const { goals } = JSON.parse(fs18.readFileSync(GOALS_FILE, "utf-8"));
31649
+ const relevant = goals.filter((g2) => g2.status === "on_track" || g2.status === "at_risk" || g2.status === "behind").map((g2) => {
31650
+ const progress = g2.current_value != null && g2.target_value ? `${Math.round(g2.current_value / g2.target_value * 100)}%` : "unknown";
31651
+ return `- ${g2.title} (${g2.status}, ${progress} complete, deadline: ${g2.deadline})`;
31652
+ });
31653
+ goalsContext = relevant.length > 0 ? relevant.join("\n") : "No active goals.";
31654
+ }
31655
+ } catch {
31656
+ }
31657
+ let recentShips = "";
31658
+ try {
31659
+ if (fs18.existsSync(IDEAS_FILE)) {
31660
+ const { ideas } = JSON.parse(fs18.readFileSync(IDEAS_FILE, "utf-8"));
31661
+ const twoWeeksAgo = new Date(Date.now() - 14 * 24 * 60 * 60 * 1e3).toISOString();
31662
+ const shipped = ideas.filter((i) => i.stage === "shipped" && i.updated_at > twoWeeksAgo).map((i) => `- ${i.title} (shipped ${i.updated_at.split("T")[0]})`);
31663
+ recentShips = shipped.length > 0 ? shipped.join("\n") : "No recent ships.";
31664
+ }
31665
+ } catch {
31666
+ }
31667
+ let kpiHighlights = "";
31668
+ try {
31669
+ if (fs18.existsSync(GOALS_FILE)) {
31670
+ const { goals } = JSON.parse(fs18.readFileSync(GOALS_FILE, "utf-8"));
31671
+ const highlights = [];
31672
+ for (const goal of goals) {
31673
+ for (const kpi of goal.kpis) {
31674
+ if (kpi.current_value != null && kpi.target_value != null) {
31675
+ const pct = Math.round(kpi.current_value / kpi.target_value * 100);
31676
+ highlights.push(`- ${kpi.name}: ${kpi.current_value}/${kpi.target_value} ${kpi.unit} (${pct}%)`);
31677
+ }
31678
+ }
31679
+ }
31680
+ kpiHighlights = highlights.length > 0 ? highlights.join("\n") : "No KPI data.";
31681
+ }
31682
+ } catch {
31683
+ }
31684
+ const plan = await generateWeekPlan({
31685
+ positioning,
31686
+ pastPerformance,
31687
+ goalsContext,
31688
+ recentShips,
31689
+ kpiHighlights
31690
+ });
31691
+ if (plan.days.length === 0) {
31692
+ return { success: false, output: "AI failed to generate a weekly content plan." };
31693
+ }
31694
+ const createdDrafts = [];
31695
+ for (const day of plan.days) {
31696
+ if (day.type === "rest" || !day.content) continue;
31697
+ if (day.type === "thread" && day.thread_tweets && day.thread_tweets.length > 0) {
31698
+ const draft = {
31699
+ id: `draft-${v4_default().slice(0, 8)}`,
31700
+ text: day.thread_tweets[0],
31701
+ source: "thread",
31702
+ source_ref: null,
31703
+ status: "draft",
31704
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
31705
+ published_at: null,
31706
+ tweet_id: null,
31707
+ tweet_url: null,
31708
+ is_thread: true,
31709
+ thread_tweets: day.thread_tweets,
31710
+ scheduled_date: day.date
31711
+ };
31712
+ state.drafts.push(draft);
31713
+ createdDrafts.push({ day: day.day, date: day.date, type: "thread", id: draft.id });
31714
+ } else {
31715
+ const source = ["ship", "milestone", "update", "digest", "insight"].includes(day.topic) ? day.topic : "insight";
31716
+ const draft = createDraft(state, day.content, source, null);
31717
+ draft.scheduled_date = day.date;
31718
+ createdDrafts.push({ day: day.day, date: day.date, type: day.type, id: draft.id });
31719
+ }
31720
+ }
31721
+ saveState3(state);
31722
+ const monday = plan.days.find((d2) => d2.day === "monday")?.date ?? "";
31723
+ const sunday = plan.days.find((d2) => d2.day === "sunday")?.date ?? "";
31724
+ const dateRange = monday && sunday ? `${formatDateShort(monday)} - ${formatDateShort(sunday)}` : "This Week";
31725
+ const lines = [`Weekly Content Plan (${dateRange})`, ""];
31726
+ for (const day of plan.days) {
31727
+ const dayLabel = day.day.charAt(0).toUpperCase() + day.day.slice(1, 3);
31728
+ if (day.type === "rest") {
31729
+ lines.push(`${dayLabel} -- Rest`);
31730
+ } else {
31731
+ const threadInfo = day.type === "thread" && day.thread_tweets ? ` (${day.thread_tweets.length} tweets)` : "";
31732
+ const preview = day.content.length > 60 ? `"${day.content.slice(0, 57)}..."` : `"${day.content}"`;
31733
+ lines.push(`${dayLabel} [${day.type}] ${preview}${threadInfo}`);
31734
+ lines.push(` Hook: ${day.hook_type} | ${day.rationale}`);
31735
+ }
31736
+ }
31737
+ lines.push("");
31738
+ lines.push(`${createdDrafts.length} drafts created. Review and edit at: vibebusiness start -> /social`);
31739
+ return { success: true, output: lines.join("\n") };
31740
+ }
31741
+ function formatDateShort(isoDate) {
31742
+ const d2 = /* @__PURE__ */ new Date(isoDate + "T00:00:00");
31743
+ const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
31744
+ return `${months[d2.getMonth()]} ${d2.getDate()}`;
31745
+ }
31420
31746
  async function executeTrackMetrics() {
31421
31747
  const state = loadState3();
31422
31748
  const credentials = loadCredentials();
@@ -31539,6 +31865,7 @@ async function executeCheckStatus3() {
31539
31865
  }
31540
31866
  }
31541
31867
  lines.push(` Pending drafts: ${freshness.draft_count}`);
31868
+ lines.push(` Scheduled drafts: ${freshness.scheduled_draft_count}`);
31542
31869
  lines.push(` Post streak: ${freshness.post_streak_days} day(s)`);
31543
31870
  lines.push(` Total posts: ${freshness.total_posts}`);
31544
31871
  lines.push(` Threads published: ${freshness.thread_count}`);
@@ -32516,9 +32843,9 @@ function cleanGitState(workspacePath) {
32516
32843
  }
32517
32844
  }
32518
32845
  }
32519
- function exec2(cmd, cwd, timeoutMs) {
32846
+ function execGit(args2, cwd, timeoutMs) {
32520
32847
  try {
32521
- return (0, import_child_process9.execSync)(cmd, {
32848
+ return (0, import_child_process9.execFileSync)("git", args2, {
32522
32849
  cwd,
32523
32850
  encoding: "utf-8",
32524
32851
  stdio: ["pipe", "pipe", "pipe"],
@@ -32526,11 +32853,58 @@ function exec2(cmd, cwd, timeoutMs) {
32526
32853
  }).trim();
32527
32854
  } catch (error) {
32528
32855
  const execError = error;
32529
- throw new Error(`Command failed: ${cmd}
32530
- ${execError.stderr || execError.message}`);
32856
+ const stderr = typeof execError.stderr === "string" ? execError.stderr : execError.stderr?.toString() ?? "";
32857
+ throw new Error(`git ${args2[0]} failed: ${stderr || execError.message}`);
32531
32858
  }
32532
32859
  }
32533
32860
 
32861
+ // scripts/lib/shell-safe.ts
32862
+ var SAFE_BRANCH_RE = /^[a-zA-Z0-9._\-/]+$/;
32863
+ function truncate(s, max2 = 100) {
32864
+ return s.length > max2 ? s.slice(0, max2) + "..." : s;
32865
+ }
32866
+ var _rejectionCounts = { branch: 0, repo: 0, commitMsg: 0 };
32867
+ function recordRejection(type) {
32868
+ _rejectionCounts[type]++;
32869
+ console.warn(`[shell-safe] Rejection recorded: type=${type} total=${_rejectionCounts[type]}`);
32870
+ }
32871
+ function getRejectionCounts() {
32872
+ try {
32873
+ return { ..._rejectionCounts };
32874
+ } catch {
32875
+ return { branch: 0, repo: 0, commitMsg: 0 };
32876
+ }
32877
+ }
32878
+ function resetRejectionCounts() {
32879
+ _rejectionCounts.branch = 0;
32880
+ _rejectionCounts.repo = 0;
32881
+ _rejectionCounts.commitMsg = 0;
32882
+ }
32883
+ function validateBranchName(name) {
32884
+ if (!name || name.trim() === "") {
32885
+ console.warn("[shell-safe] validateBranchName: rejecting empty branch name");
32886
+ recordRejection("branch");
32887
+ throw new Error("Branch name cannot be empty");
32888
+ }
32889
+ if (!SAFE_BRANCH_RE.test(name)) {
32890
+ console.warn(`[shell-safe] Rejected branch name: ${truncate(name)}`);
32891
+ recordRejection("branch");
32892
+ throw new Error(
32893
+ `Invalid branch name: "${truncate(name)}" \u2014 only alphanumerics, dots, dashes, underscores, and slashes are allowed`
32894
+ );
32895
+ }
32896
+ return name;
32897
+ }
32898
+ function sanitizeForCommitMessage(msg) {
32899
+ const original = msg;
32900
+ let sanitized = msg.replace(/"/g, "\u201C\u201D"[0]).replace(/`/g, "\u2018").replace(/\$\(/g, "(").replace(/\$/g, "").replace(/\\/g, "/").replace(/!/g, "").replace(/[\n\r]/g, " ").trim();
32901
+ if (sanitized !== original) {
32902
+ console.warn(`[shell-safe] sanitizeForCommitMessage modified input: ${truncate(original)}`);
32903
+ recordRejection("commitMsg");
32904
+ }
32905
+ return sanitized;
32906
+ }
32907
+
32534
32908
  // scripts/lib/worktree.ts
32535
32909
  function getWorktreePaths(workspaceDir, repoName) {
32536
32910
  return {
@@ -32540,17 +32914,17 @@ function getWorktreePaths(workspaceDir, repoName) {
32540
32914
  }
32541
32915
  function syncBaseClone(baseClonePath, defaultBranch) {
32542
32916
  cleanGitState(baseClonePath);
32543
- exec2("git fetch origin", baseClonePath, 3e4);
32544
- exec2(`git checkout ${defaultBranch}`, baseClonePath);
32545
- exec2(`git reset --hard origin/${defaultBranch}`, baseClonePath);
32917
+ execGit(["fetch", "origin"], baseClonePath, 3e4);
32918
+ execGit(["checkout", validateBranchName(defaultBranch)], baseClonePath);
32919
+ execGit(["reset", "--hard", `origin/${validateBranchName(defaultBranch)}`], baseClonePath);
32546
32920
  }
32547
32921
  function ensureIdeaBranch(baseClonePath, branchName, defaultBranch) {
32548
32922
  try {
32549
- exec2(`git rev-parse --verify ${branchName}`, baseClonePath);
32923
+ execGit(["rev-parse", "--verify", validateBranchName(branchName)], baseClonePath);
32550
32924
  } catch {
32551
- exec2(`git checkout ${defaultBranch}`, baseClonePath);
32552
- exec2(`git checkout -b ${branchName}`, baseClonePath);
32553
- exec2(`git checkout ${defaultBranch}`, baseClonePath);
32925
+ execGit(["checkout", validateBranchName(defaultBranch)], baseClonePath);
32926
+ execGit(["checkout", "-b", validateBranchName(branchName)], baseClonePath);
32927
+ execGit(["checkout", validateBranchName(defaultBranch)], baseClonePath);
32554
32928
  }
32555
32929
  }
32556
32930
  function createWorktree(baseClonePath, worktreeContainerDir, ideaBranch, groupIndex) {
@@ -32561,17 +32935,17 @@ function createWorktree(baseClonePath, worktreeContainerDir, ideaBranch, groupIn
32561
32935
  const worktreePath = path20.join(worktreeContainerDir, `${ideaBranch}-g${groupIndex}`);
32562
32936
  if (fs25.existsSync(worktreePath)) {
32563
32937
  try {
32564
- exec2(`git worktree remove --force "${worktreePath}"`, baseClonePath, 1e4);
32938
+ execGit(["worktree", "remove", "--force", worktreePath], baseClonePath, 1e4);
32565
32939
  } catch {
32566
32940
  fs25.rmSync(worktreePath, { recursive: true, force: true });
32567
32941
  }
32568
32942
  }
32569
32943
  try {
32570
- exec2(`git branch -D "${tempBranch}"`, baseClonePath);
32944
+ execGit(["branch", "-D", tempBranch], baseClonePath);
32571
32945
  } catch {
32572
32946
  }
32573
- exec2(
32574
- `git worktree add -b "${tempBranch}" "${worktreePath}" "${ideaBranch}"`,
32947
+ execGit(
32948
+ ["worktree", "add", "-b", tempBranch, worktreePath, ideaBranch],
32575
32949
  baseClonePath,
32576
32950
  15e3
32577
32951
  );
@@ -32580,13 +32954,13 @@ function createWorktree(baseClonePath, worktreeContainerDir, ideaBranch, groupIn
32580
32954
  function mergeWorktreeIntoIdeaBranch(baseClonePath, ideaBranch, tempBranch) {
32581
32955
  try {
32582
32956
  cleanGitState(baseClonePath);
32583
- exec2(`git checkout "${ideaBranch}"`, baseClonePath);
32584
- exec2(`git merge "${tempBranch}" --no-edit`, baseClonePath, 3e4);
32957
+ execGit(["checkout", ideaBranch], baseClonePath);
32958
+ execGit(["merge", tempBranch, "--no-edit"], baseClonePath, 3e4);
32585
32959
  return true;
32586
32960
  } catch (error) {
32587
32961
  console.log(`[worktree] Merge failed for ${tempBranch} into ${ideaBranch}: ${error.message}`);
32588
32962
  try {
32589
- exec2("git merge --abort", baseClonePath);
32963
+ execGit(["merge", "--abort"], baseClonePath);
32590
32964
  } catch {
32591
32965
  }
32592
32966
  return false;
@@ -32594,7 +32968,7 @@ function mergeWorktreeIntoIdeaBranch(baseClonePath, ideaBranch, tempBranch) {
32594
32968
  }
32595
32969
  function removeWorktree(baseClonePath, worktreePath, tempBranch) {
32596
32970
  try {
32597
- exec2(`git worktree remove --force "${worktreePath}"`, baseClonePath, 1e4);
32971
+ execGit(["worktree", "remove", "--force", worktreePath], baseClonePath, 1e4);
32598
32972
  } catch {
32599
32973
  try {
32600
32974
  fs25.rmSync(worktreePath, { recursive: true, force: true });
@@ -32602,7 +32976,7 @@ function removeWorktree(baseClonePath, worktreePath, tempBranch) {
32602
32976
  }
32603
32977
  }
32604
32978
  try {
32605
- exec2(`git branch -D "${tempBranch}"`, baseClonePath);
32979
+ execGit(["branch", "-D", tempBranch], baseClonePath);
32606
32980
  } catch {
32607
32981
  }
32608
32982
  }
@@ -32622,7 +32996,7 @@ function cleanupIdeaWorktrees(baseClonePath, worktreeContainerDir, ideaBranch) {
32622
32996
  }
32623
32997
  }
32624
32998
  try {
32625
- exec2("git worktree prune", baseClonePath, 1e4);
32999
+ execGit(["worktree", "prune"], baseClonePath, 1e4);
32626
33000
  } catch {
32627
33001
  }
32628
33002
  try {
@@ -34127,6 +34501,7 @@ These tasks generate tweet drafts for the founder's build-in-public journey.
34127
34501
 
34128
34502
  Task prefixes:
34129
34503
  - "social-setup-x" \u2192 Validate X API credentials from .env, save username
34504
+ - "social-plan-week" \u2192 Generate a 7-day content calendar with AI-optimized drafts (5 content days + 2 rest)
34130
34505
  - "social-draft-ship-visual" \u2192 Generate a "just shipped" tweet WITH ship card image or video attached
34131
34506
  - "social-draft-ship" \u2192 Generate a "just shipped" tweet (text only, no media)
34132
34507
  - "social-draft-update" \u2192 Generate a product update draft from positioning data
@@ -34150,6 +34525,7 @@ When to recommend social tasks:
34150
34525
  7. If social.total_posts > 0 AND social.avg_engagement_rate == null \u2192 suggest "social-track-metrics" to start tracking
34151
34526
  8. If social.thread_count == 0 AND social.total_posts >= 3 \u2192 suggest "social-draft-thread-{relevant-topic}" (threads get 200-300% more reach)
34152
34527
  9. Prefer AI-generated drafts (social-draft-viral-*) over template drafts (social-draft-ship/update/milestone) for higher engagement
34528
+ 10. If social.scheduled_draft_count == 0 AND social.configured == true \u2192 suggest "social-plan-week" to plan next week's content calendar
34153
34529
 
34154
34530
  PROMO COPY TASKS:
34155
34531
  The "promo_copy" field shows positioning-driven copy generation status.
@@ -34262,117 +34638,58 @@ Respond with JSON only (no markdown code blocks, just raw JSON):
34262
34638
 
34263
34639
  If there's nothing urgent, set next_action to null and new_todos to an empty array.
34264
34640
  Focus on actionable, specific recommendations. Be concise.`;
34265
- return new Promise((resolve2, reject) => {
34266
- const startTime = Date.now();
34267
- const promptSize = prompt.length;
34268
- log(`Running Claude reasoning (prompt: ${promptSize} chars)...`);
34269
- const reasoningSchema = JSON.stringify({
34270
- type: "object",
34271
- properties: {
34272
- analysis: { type: "string" },
34273
- critical_issue: { type: ["string", "null"] },
34274
- next_action: {
34275
- type: ["object", "null"],
34641
+ const startTime = Date.now();
34642
+ const promptSize = prompt.length;
34643
+ log(`Running Claude reasoning (prompt: ${promptSize} chars)...`);
34644
+ const reasoningSchema = {
34645
+ type: "object",
34646
+ properties: {
34647
+ analysis: { type: "string" },
34648
+ critical_issue: { type: ["string", "null"] },
34649
+ next_action: {
34650
+ type: ["object", "null"],
34651
+ properties: {
34652
+ task_id: { type: "string" },
34653
+ description: { type: "string" },
34654
+ autonomous: { type: "boolean" }
34655
+ },
34656
+ required: ["task_id", "description", "autonomous"]
34657
+ },
34658
+ new_todos: {
34659
+ type: "array",
34660
+ items: {
34661
+ type: "object",
34276
34662
  properties: {
34277
- task_id: { type: "string" },
34663
+ id: { type: "string" },
34278
34664
  description: { type: "string" },
34279
- autonomous: { type: "boolean" }
34665
+ priority: { type: "string" }
34280
34666
  },
34281
- required: ["task_id", "description", "autonomous"]
34282
- },
34283
- new_todos: {
34284
- type: "array",
34285
- items: {
34286
- type: "object",
34287
- properties: {
34288
- id: { type: "string" },
34289
- description: { type: "string" },
34290
- priority: { type: "string" }
34291
- },
34292
- required: ["id", "description", "priority"]
34293
- }
34294
- },
34295
- learnings: { type: "array", items: { type: "string" } }
34296
- },
34297
- required: ["analysis", "next_action"]
34298
- });
34299
- const claude = (0, import_child_process10.spawn)("claude", [
34300
- "--print",
34301
- "--output-format",
34302
- "json",
34303
- "--json-schema",
34304
- reasoningSchema
34305
- ], {
34306
- cwd: ROOT_DIR,
34307
- env: process.env,
34308
- stdio: ["pipe", "pipe", "pipe"]
34309
- });
34310
- log(` Claude process started (PID: ${claude.pid})`);
34311
- claude.stdin.write(prompt);
34312
- claude.stdin.end();
34313
- log(` Prompt sent via stdin (with --output-format json + --json-schema)`);
34314
- let output = "";
34315
- let errorOutput = "";
34316
- let isResolved = false;
34317
- const progressInterval = setInterval(() => {
34318
- if (!isResolved) {
34319
- const elapsed = Math.round((Date.now() - startTime) / 1e3);
34320
- log(` ...Claude thinking (${elapsed}s elapsed)`);
34321
- }
34322
- }, 1e4);
34323
- const timeout = setTimeout(() => {
34324
- if (!isResolved) {
34325
- isResolved = true;
34326
- clearInterval(progressInterval);
34327
- claude.kill("SIGTERM");
34328
- const elapsed = Math.round((Date.now() - startTime) / 1e3);
34329
- log(`Claude reasoning timed out after ${elapsed}s`);
34330
- resolve2(null);
34331
- }
34332
- }, TIMEOUT_MS);
34333
- claude.stdout.on("data", (data) => {
34334
- const chunk = data.toString();
34335
- output += chunk;
34336
- if (output.length === chunk.length) {
34337
- log(` Claude is responding...`);
34338
- }
34339
- });
34340
- claude.stderr.on("data", (data) => {
34341
- const text = data.toString();
34342
- errorOutput += text;
34343
- if (text.trim()) {
34344
- log(` [Claude stderr]: ${text.trim().substring(0, 200)}`);
34345
- }
34346
- });
34347
- claude.on("close", (code) => {
34348
- if (isResolved) return;
34349
- isResolved = true;
34350
- clearInterval(progressInterval);
34351
- clearTimeout(timeout);
34352
- const elapsed = Math.round((Date.now() - startTime) / 1e3);
34353
- if (code === 0) {
34354
- const result = parseCliJsonOutput(output, "Reasoning");
34355
- if (result) {
34356
- log(`Claude completed in ${elapsed}s: ${result.analysis}`);
34357
- resolve2(result);
34358
- } else {
34359
- log(`Claude completed in ${elapsed}s but no valid JSON found`);
34360
- resolve2(null);
34667
+ required: ["id", "description", "priority"]
34361
34668
  }
34362
- } else {
34363
- log(`Claude exited with code ${code} after ${elapsed}s: ${errorOutput}`);
34364
- resolve2(null);
34365
- }
34366
- });
34367
- claude.on("error", (error) => {
34368
- if (isResolved) return;
34369
- isResolved = true;
34370
- clearInterval(progressInterval);
34371
- clearTimeout(timeout);
34372
- log(`Claude spawn error: ${error.message}`);
34373
- resolve2(null);
34374
- });
34669
+ },
34670
+ learnings: { type: "array", items: { type: "string" } }
34671
+ },
34672
+ required: ["analysis", "next_action"]
34673
+ };
34674
+ const aiResult = await invokeAI({
34675
+ prompt,
34676
+ cwd: ROOT_DIR,
34677
+ timeoutMs: TIMEOUT_MS,
34678
+ useStdin: true,
34679
+ jsonSchema: reasoningSchema
34375
34680
  });
34681
+ const elapsed = Math.round((Date.now() - startTime) / 1e3);
34682
+ if (aiResult.error) {
34683
+ log(`Claude reasoning failed after ${elapsed}s: ${aiResult.error}`);
34684
+ return null;
34685
+ }
34686
+ const result = aiResult.provider === "claude-cli" ? parseCliJsonOutput(aiResult.output, "Reasoning") : robustJsonParse(aiResult.output, "Reasoning");
34687
+ if (result) {
34688
+ log(`Claude completed in ${elapsed}s (${aiResult.provider}): ${result.analysis}`);
34689
+ } else {
34690
+ log(`Claude completed in ${elapsed}s but no valid JSON found`);
34691
+ }
34692
+ return result;
34376
34693
  }
34377
34694
  function addTasksToTodo(tasks) {
34378
34695
  if (tasks.length === 0) return;
@@ -35005,98 +35322,63 @@ Respond with JSON only (no markdown code blocks, just raw JSON):
35005
35322
  }
35006
35323
  ]
35007
35324
  }`;
35008
- return new Promise((resolve2) => {
35009
- const TIMEOUT_MS = config.autonomy?.max_sub_task_timeout_ms || 6e5;
35010
- log(`Decomposing idea into sub-tasks via Claude reasoning (timeout: ${Math.round(TIMEOUT_MS / 1e3)}s)...`);
35011
- const startTime = Date.now();
35012
- const decompSchema = JSON.stringify({
35013
- type: "object",
35014
- properties: {
35015
- sub_tasks: {
35016
- type: "array",
35017
- items: {
35018
- type: "object",
35019
- properties: {
35020
- id: { type: "string" },
35021
- title: { type: "string" },
35022
- description: { type: "string" },
35023
- files_to_modify: { type: "array", items: { type: "string" } },
35024
- observability: { type: "string" }
35025
- },
35026
- required: ["id", "title", "description", "files_to_modify"]
35027
- }
35028
- }
35029
- },
35030
- required: ["sub_tasks"]
35031
- });
35032
- const claude = (0, import_child_process10.spawn)("claude", [
35033
- "--print",
35034
- "--output-format",
35035
- "json",
35036
- "--json-schema",
35037
- decompSchema
35038
- ], {
35039
- cwd: ROOT_DIR,
35040
- env: process.env,
35041
- stdio: ["pipe", "pipe", "pipe"]
35042
- });
35043
- claude.stdin.write(prompt);
35044
- claude.stdin.end();
35045
- let output = "";
35046
- let isResolved = false;
35047
- const progressInterval = setInterval(() => {
35048
- const elapsed = Math.round((Date.now() - startTime) / 1e3);
35049
- log(` ...Decomposing (${elapsed}s elapsed, ${output.length} chars received)`);
35050
- }, 15e3);
35051
- const timeout = setTimeout(() => {
35052
- if (!isResolved) {
35053
- isResolved = true;
35054
- clearInterval(progressInterval);
35055
- claude.kill("SIGTERM");
35056
- const elapsed = Math.round((Date.now() - startTime) / 1e3);
35057
- log(`Decomposition timed out after ${elapsed}s (limit: ${Math.round(TIMEOUT_MS / 1e3)}s)`);
35058
- resolve2([]);
35059
- }
35060
- }, TIMEOUT_MS);
35061
- claude.stdout.on("data", (data) => {
35062
- output += data.toString();
35063
- });
35064
- claude.on("close", (code) => {
35065
- if (isResolved) return;
35066
- isResolved = true;
35067
- clearTimeout(timeout);
35068
- clearInterval(progressInterval);
35069
- if (code === 0 && output.trim().length > 0) {
35070
- const parsed = parseCliJsonOutput(output, "Decomposition");
35071
- if (parsed?.sub_tasks?.length) {
35072
- const subTasks = parsed.sub_tasks.slice(0, maxSubTasks).map((st, i) => ({
35073
- id: st.id || `st-${String(i + 1).padStart(3, "0")}`,
35074
- title: st.title,
35075
- description: st.description,
35076
- files_to_modify: st.files_to_modify || [],
35077
- observability: st.observability || void 0,
35078
- status: "pending",
35079
- started_at: null,
35080
- completed_at: null,
35081
- error_message: null,
35082
- commit_hash: null
35083
- }));
35084
- log(`Decomposed into ${subTasks.length} sub-tasks`);
35085
- resolve2(subTasks);
35086
- return;
35325
+ const TIMEOUT_MS = config.autonomy?.max_sub_task_timeout_ms || 6e5;
35326
+ log(`Decomposing idea into sub-tasks via Claude reasoning (timeout: ${Math.round(TIMEOUT_MS / 1e3)}s)...`);
35327
+ const startTime = Date.now();
35328
+ const decompSchema = {
35329
+ type: "object",
35330
+ properties: {
35331
+ sub_tasks: {
35332
+ type: "array",
35333
+ items: {
35334
+ type: "object",
35335
+ properties: {
35336
+ id: { type: "string" },
35337
+ title: { type: "string" },
35338
+ description: { type: "string" },
35339
+ files_to_modify: { type: "array", items: { type: "string" } },
35340
+ observability: { type: "string" }
35341
+ },
35342
+ required: ["id", "title", "description", "files_to_modify"]
35087
35343
  }
35088
35344
  }
35089
- resolve2([]);
35090
- });
35091
- claude.on("error", () => {
35092
- if (!isResolved) {
35093
- isResolved = true;
35094
- clearTimeout(timeout);
35095
- clearInterval(progressInterval);
35096
- resolve2([]);
35097
- }
35098
- });
35345
+ },
35346
+ required: ["sub_tasks"]
35347
+ };
35348
+ const aiResult = await invokeAI({
35349
+ prompt,
35350
+ cwd: ROOT_DIR,
35351
+ timeoutMs: TIMEOUT_MS,
35352
+ useStdin: true,
35353
+ jsonSchema: decompSchema
35099
35354
  });
35355
+ const elapsed = Math.round((Date.now() - startTime) / 1e3);
35356
+ if (aiResult.error) {
35357
+ log(`Decomposition failed after ${elapsed}s: ${aiResult.error}`);
35358
+ return [];
35359
+ }
35360
+ if (!aiResult.output.trim()) {
35361
+ log(`Decomposition returned empty output after ${elapsed}s`);
35362
+ return [];
35363
+ }
35364
+ const parsed = aiResult.provider === "claude-cli" ? parseCliJsonOutput(aiResult.output, "Decomposition") : robustJsonParse(aiResult.output, "Decomposition");
35365
+ if (parsed?.sub_tasks?.length) {
35366
+ const subTasks = parsed.sub_tasks.slice(0, maxSubTasks).map((st, i) => ({
35367
+ id: st.id || `st-${String(i + 1).padStart(3, "0")}`,
35368
+ title: st.title,
35369
+ description: st.description,
35370
+ files_to_modify: st.files_to_modify || [],
35371
+ observability: st.observability || void 0,
35372
+ status: "pending",
35373
+ started_at: null,
35374
+ completed_at: null,
35375
+ error_message: null,
35376
+ commit_hash: null
35377
+ }));
35378
+ log(`Decomposed into ${subTasks.length} sub-tasks`);
35379
+ return subTasks;
35380
+ }
35381
+ return [];
35100
35382
  }
35101
35383
  function deferIdeaWithComment(ideaId, reason) {
35102
35384
  const ideasData = loadJson(IDEAS_FILE, { ideas: [] });
@@ -35446,12 +35728,12 @@ async function executeAutonomousImplementation(ideaId) {
35446
35728
  if (branchName2) {
35447
35729
  try {
35448
35730
  log(`Auto-merging branch ${branchName2} into ${defaultBranch2}...`);
35449
- (0, import_child_process10.execSync)(`git checkout ${defaultBranch2}`, { cwd: workspacePath, encoding: "utf-8", stdio: "pipe" });
35450
- (0, import_child_process10.execSync)(`git merge ${branchName2} --squash --no-edit`, { cwd: workspacePath, encoding: "utf-8", stdio: "pipe" });
35451
- (0, import_child_process10.execSync)(`git commit -m "feat: ${idea.title} (auto-merged from ${branchName2})"`, { cwd: workspacePath, encoding: "utf-8", stdio: "pipe" });
35731
+ (0, import_child_process10.execFileSync)("git", ["checkout", validateBranchName(defaultBranch2)], { cwd: workspacePath, encoding: "utf-8", stdio: "pipe" });
35732
+ (0, import_child_process10.execFileSync)("git", ["merge", validateBranchName(branchName2), "--squash", "--no-edit"], { cwd: workspacePath, encoding: "utf-8", stdio: "pipe" });
35733
+ (0, import_child_process10.execFileSync)("git", ["commit", "-m", `feat: ${sanitizeForCommitMessage(idea.title)} (auto-merged from ${validateBranchName(branchName2)})`], { cwd: workspacePath, encoding: "utf-8", stdio: "pipe" });
35452
35734
  log(`Successfully merged ${branchName2} into ${defaultBranch2}`);
35453
35735
  try {
35454
- (0, import_child_process10.execSync)(`git branch -d ${branchName2}`, { cwd: workspacePath, encoding: "utf-8", stdio: "pipe" });
35736
+ (0, import_child_process10.execFileSync)("git", ["branch", "-d", validateBranchName(branchName2)], { cwd: workspacePath, encoding: "utf-8", stdio: "pipe" });
35455
35737
  log(`Deleted branch ${branchName2}`);
35456
35738
  } catch {
35457
35739
  log(`Could not delete branch ${branchName2} (may need force delete)`);
@@ -35476,8 +35758,8 @@ async function executeAutonomousImplementation(ideaId) {
35476
35758
  } catch (mergeError) {
35477
35759
  log(`Auto-merge failed (likely conflicts): ${mergeError instanceof Error ? mergeError.message : "Unknown"}`);
35478
35760
  try {
35479
- (0, import_child_process10.execSync)(`git merge --abort`, { cwd: workspacePath, encoding: "utf-8", stdio: "pipe" });
35480
- (0, import_child_process10.execSync)(`git checkout ${branchName2}`, { cwd: workspacePath, encoding: "utf-8", stdio: "pipe" });
35761
+ (0, import_child_process10.execFileSync)("git", ["merge", "--abort"], { cwd: workspacePath, encoding: "utf-8", stdio: "pipe" });
35762
+ (0, import_child_process10.execFileSync)("git", ["checkout", validateBranchName(branchName2)], { cwd: workspacePath, encoding: "utf-8", stdio: "pipe" });
35481
35763
  } catch {
35482
35764
  }
35483
35765
  log(`Left idea ${ideaId} in testing stage for manual merge`);
@@ -35640,6 +35922,12 @@ async function executeSuggestEpics() {
35640
35922
  }
35641
35923
  }
35642
35924
  async function executeMetaTask(taskId, description, businessContext, options) {
35925
+ try {
35926
+ requireClaudeCLI("meta-task execution");
35927
+ } catch (err2) {
35928
+ log(`Meta-task skipped: ${err2.message}`);
35929
+ return false;
35930
+ }
35643
35931
  const failureCount = options?.failureCount || 0;
35644
35932
  log(`Executing meta-task via Claude Code: ${taskId} (attempt ${failureCount + 1})`);
35645
35933
  const allowedFiles = [
@@ -35912,6 +36200,12 @@ Return a JSON array of ideas:
35912
36200
  Only output the JSON array, no other text.`;
35913
36201
  }
35914
36202
  async function executeGoalGapResearch(goalId) {
36203
+ try {
36204
+ requireClaudeCLI("goal-gap research");
36205
+ } catch (err2) {
36206
+ log(`Goal-gap research skipped: ${err2.message}`);
36207
+ return false;
36208
+ }
35915
36209
  log(`Executing goal-gap research for ${goalId}...`);
35916
36210
  const goalsData = loadJson(GOALS_FILE, { goals: [] });
35917
36211
  const goal = goalsData.goals.find((g2) => g2.id === goalId);
@@ -36176,49 +36470,23 @@ async function executeShippedEvaluation(ideaId) {
36176
36470
  const liveKPIs = await fetchLiveKPIs();
36177
36471
  const prompt = buildEvaluationPrompt(idea, goal, liveKPIs, daysShipped);
36178
36472
  const TIMEOUT_MS = 12e4;
36179
- const result = await new Promise((resolve2) => {
36180
- const claude = (0, import_child_process10.spawn)("claude", ["--print"], {
36181
- cwd: ROOT_DIR,
36182
- env: process.env,
36183
- stdio: ["pipe", "pipe", "pipe"]
36184
- });
36185
- claude.stdin.write(prompt);
36186
- claude.stdin.end();
36187
- let output = "";
36188
- let isResolved = false;
36189
- const timeout = setTimeout(() => {
36190
- if (!isResolved) {
36191
- isResolved = true;
36192
- claude.kill("SIGTERM");
36193
- log("Evaluation timed out");
36194
- resolve2(null);
36195
- }
36196
- }, TIMEOUT_MS);
36197
- claude.stdout.on("data", (data) => {
36198
- output += data.toString();
36199
- });
36200
- claude.on("close", (code) => {
36201
- if (isResolved) return;
36202
- isResolved = true;
36203
- clearTimeout(timeout);
36204
- if (code === 0 && output.trim()) {
36205
- const parsed = robustJsonParse(output, "Evaluation");
36206
- if (parsed) {
36207
- log(`Evaluation complete: status=${parsed.status}, confidence=${parsed.confidence}`);
36208
- resolve2(parsed);
36209
- return;
36210
- }
36211
- }
36212
- resolve2(null);
36213
- });
36214
- claude.on("error", () => {
36215
- if (!isResolved) {
36216
- isResolved = true;
36217
- clearTimeout(timeout);
36218
- resolve2(null);
36219
- }
36220
- });
36473
+ const aiResult = await invokeAI({
36474
+ prompt,
36475
+ cwd: ROOT_DIR,
36476
+ timeoutMs: TIMEOUT_MS,
36477
+ useStdin: true,
36478
+ expectJson: true
36221
36479
  });
36480
+ let result = null;
36481
+ if (!aiResult.error && aiResult.output.trim()) {
36482
+ const parsed = robustJsonParse(aiResult.output, "Evaluation");
36483
+ if (parsed) {
36484
+ log(`Evaluation complete (${aiResult.provider}): status=${parsed.status}, confidence=${parsed.confidence}`);
36485
+ result = parsed;
36486
+ }
36487
+ } else if (aiResult.error) {
36488
+ log(`Evaluation failed: ${aiResult.error}`);
36489
+ }
36222
36490
  if (!result) {
36223
36491
  log(`Evaluation failed for ${ideaId}`);
36224
36492
  return false;
@@ -36570,6 +36838,14 @@ Respond with JSON only:
36570
36838
  return tasks;
36571
36839
  }
36572
36840
  async function executeSingleResearchTask(task) {
36841
+ try {
36842
+ requireClaudeCLI("research task");
36843
+ } catch (err2) {
36844
+ task.status = "failed";
36845
+ task.error_message = err2.message;
36846
+ task.completed_at = (/* @__PURE__ */ new Date()).toISOString();
36847
+ return task;
36848
+ }
36573
36849
  const TIMEOUT_MS = 3e5;
36574
36850
  log(` Starting research task: ${task.type} - ${task.topic}`);
36575
36851
  task.status = "running";
@@ -36685,30 +36961,21 @@ Respond with JSON only:
36685
36961
  "recommendation": "proceed|revise|reject",
36686
36962
  "recommendation_reasoning": "Why this recommendation"
36687
36963
  }`;
36688
- return new Promise((resolve2) => {
36689
- const claude = (0, import_child_process10.spawn)("claude", ["--print"], {
36690
- cwd: ROOT_DIR,
36691
- env: process.env,
36692
- stdio: ["pipe", "pipe", "pipe"]
36693
- });
36694
- claude.stdin.write(prompt);
36695
- claude.stdin.end();
36696
- let output = "";
36697
- const timeout = setTimeout(() => {
36698
- claude.kill("SIGTERM");
36699
- research.summary = findingsSummary;
36700
- research.recommendation = "revise";
36701
- resolve2();
36702
- }, TIMEOUT_MS);
36703
- claude.stdout.on("data", (data) => {
36704
- output += data.toString();
36705
- });
36706
- claude.on("close", (code) => {
36707
- clearTimeout(timeout);
36708
- if (code === 0 && output.trim()) {
36709
- const parsed = robustJsonParse(output, "ResearchAggregation");
36710
- if (parsed) {
36711
- research.summary = `**Executive Summary:** ${parsed.executive_summary}
36964
+ const aiResult = await invokeAI({
36965
+ prompt,
36966
+ cwd: ROOT_DIR,
36967
+ timeoutMs: TIMEOUT_MS,
36968
+ useStdin: true,
36969
+ expectJson: true
36970
+ });
36971
+ if (aiResult.error || !aiResult.output.trim()) {
36972
+ research.summary = findingsSummary;
36973
+ research.recommendation = "revise";
36974
+ return;
36975
+ }
36976
+ const parsed = robustJsonParse(aiResult.output, "ResearchAggregation");
36977
+ if (parsed) {
36978
+ research.summary = `**Executive Summary:** ${parsed.executive_summary}
36712
36979
 
36713
36980
  **Key Insights:**
36714
36981
  ${parsed.key_insights.map((i) => `- ${i}`).join("\n")}
@@ -36717,25 +36984,12 @@ ${parsed.key_insights.map((i) => `- ${i}`).join("\n")}
36717
36984
  ${parsed.risks_identified?.map((r) => `- ${r}`).join("\n") || "None identified"}
36718
36985
 
36719
36986
  **Reasoning:** ${parsed.recommendation_reasoning}`;
36720
- research.recommendation = parsed.recommendation;
36721
- log(`Research aggregation complete: ${research.recommendation}`);
36722
- } else {
36723
- research.summary = findingsSummary;
36724
- research.recommendation = "revise";
36725
- }
36726
- } else {
36727
- research.summary = findingsSummary;
36728
- research.recommendation = "revise";
36729
- }
36730
- resolve2();
36731
- });
36732
- claude.on("error", () => {
36733
- clearTimeout(timeout);
36734
- research.summary = findingsSummary;
36735
- research.recommendation = "revise";
36736
- resolve2();
36737
- });
36738
- });
36987
+ research.recommendation = parsed.recommendation;
36988
+ log(`Research aggregation complete (${aiResult.provider}): ${research.recommendation}`);
36989
+ } else {
36990
+ research.summary = findingsSummary;
36991
+ research.recommendation = "revise";
36992
+ }
36739
36993
  }
36740
36994
  async function executeIdeaResearch(ideaId, config, businessContext) {
36741
36995
  log(`Executing parallel research for idea: ${ideaId}`);
@@ -37004,6 +37258,20 @@ function generateStatusContent(state, alerts, nextTask, reasoning, systemHealth)
37004
37258
  ## System Health
37005
37259
 
37006
37260
  ${systemHealth.recovery_actions.map((a) => `- ${a}`).join("\n")}
37261
+ ` : "";
37262
+ const rejections = (() => {
37263
+ try {
37264
+ return getRejectionCounts();
37265
+ } catch {
37266
+ return { branch: 0, repo: 0, commitMsg: 0 };
37267
+ }
37268
+ })();
37269
+ const hasRejections = rejections.branch > 0 || rejections.repo > 0 || rejections.commitMsg > 0;
37270
+ const securitySection = hasRejections ? `
37271
+ ## Security
37272
+
37273
+ - Shell input rejections: {branch: ${rejections.branch}, repo: ${rejections.repo}, commitMsg: ${rejections.commitMsg}}
37274
+ - \u26A0\uFE0F Non-zero rejections detected \u2014 investigate source of malformed input
37007
37275
  ` : "";
37008
37276
  const analysisSection = reasoning ? `**Analysis:** ${reasoning.analysis}
37009
37277
 
@@ -37052,7 +37320,7 @@ ${pendingRows}
37052
37320
  ## Alerts
37053
37321
 
37054
37322
  ${alertsSection}
37055
- ${healthSection}
37323
+ ${healthSection}${securitySection}
37056
37324
  ## Recent Activity
37057
37325
 
37058
37326
  | Date | Event |
@@ -37067,6 +37335,7 @@ ${activityRows}
37067
37335
  async function runSingleHeartbeat(beatNumber = 1) {
37068
37336
  const startTime = Date.now();
37069
37337
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
37338
+ resetRejectionCounts();
37070
37339
  capturedLogs = [];
37071
37340
  isCapturingLogs = true;
37072
37341
  try {