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
@@ -24,9 +24,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // scripts/implement.ts
27
- var import_child_process2 = require("child_process");
28
- var fs4 = __toESM(require("fs"));
29
- var path3 = __toESM(require("path"));
27
+ var import_child_process3 = require("child_process");
28
+ var fs5 = __toESM(require("fs"));
29
+ var path4 = __toESM(require("path"));
30
30
 
31
31
  // scripts/lib/git-utils.ts
32
32
  var import_child_process = require("child_process");
@@ -82,6 +82,20 @@ function cleanGitState(workspacePath) {
82
82
  }
83
83
  }
84
84
  }
85
+ function execGit(args, cwd, timeoutMs) {
86
+ try {
87
+ return (0, import_child_process.execFileSync)("git", args, {
88
+ cwd,
89
+ encoding: "utf-8",
90
+ stdio: ["pipe", "pipe", "pipe"],
91
+ ...timeoutMs ? { timeout: timeoutMs } : {}
92
+ }).trim();
93
+ } catch (error) {
94
+ const execError = error;
95
+ const stderr = typeof execError.stderr === "string" ? execError.stderr : execError.stderr?.toString() ?? "";
96
+ throw new Error(`git ${args[0]} failed: ${stderr || execError.message}`);
97
+ }
98
+ }
85
99
  function exec(cmd, cwd, timeoutMs) {
86
100
  try {
87
101
  return (0, import_child_process.execSync)(cmd, {
@@ -97,6 +111,41 @@ ${execError.stderr || execError.message}`);
97
111
  }
98
112
  }
99
113
 
114
+ // scripts/lib/shell-safe.ts
115
+ var SAFE_BRANCH_RE = /^[a-zA-Z0-9._\-/]+$/;
116
+ function truncate(s, max = 100) {
117
+ return s.length > max ? s.slice(0, max) + "..." : s;
118
+ }
119
+ var _rejectionCounts = { branch: 0, repo: 0, commitMsg: 0 };
120
+ function recordRejection(type) {
121
+ _rejectionCounts[type]++;
122
+ console.warn(`[shell-safe] Rejection recorded: type=${type} total=${_rejectionCounts[type]}`);
123
+ }
124
+ function validateBranchName(name) {
125
+ if (!name || name.trim() === "") {
126
+ console.warn("[shell-safe] validateBranchName: rejecting empty branch name");
127
+ recordRejection("branch");
128
+ throw new Error("Branch name cannot be empty");
129
+ }
130
+ if (!SAFE_BRANCH_RE.test(name)) {
131
+ console.warn(`[shell-safe] Rejected branch name: ${truncate(name)}`);
132
+ recordRejection("branch");
133
+ throw new Error(
134
+ `Invalid branch name: "${truncate(name)}" \u2014 only alphanumerics, dots, dashes, underscores, and slashes are allowed`
135
+ );
136
+ }
137
+ return name;
138
+ }
139
+ function sanitizeForCommitMessage(msg) {
140
+ const original = msg;
141
+ let sanitized = msg.replace(/"/g, "\u201C\u201D"[0]).replace(/`/g, "\u2018").replace(/\$\(/g, "(").replace(/\$/g, "").replace(/\\/g, "/").replace(/!/g, "").replace(/[\n\r]/g, " ").trim();
142
+ if (sanitized !== original) {
143
+ console.warn(`[shell-safe] sanitizeForCommitMessage modified input: ${truncate(original)}`);
144
+ recordRejection("commitMsg");
145
+ }
146
+ return sanitized;
147
+ }
148
+
100
149
  // scripts/lib/json-lock.ts
101
150
  var fs2 = __toESM(require("fs"));
102
151
  function atomicWriteFileSync(filePath, content) {
@@ -105,21 +154,116 @@ function atomicWriteFileSync(filePath, content) {
105
154
  fs2.renameSync(tmpPath, filePath);
106
155
  }
107
156
 
108
- // scripts/lib/paths.ts
109
- var path2 = __toESM(require("path"));
157
+ // scripts/lib/ai-provider.ts
158
+ var import_child_process2 = require("child_process");
110
159
  var fs3 = __toESM(require("fs"));
160
+ var path2 = __toESM(require("path"));
161
+ var CREDENTIALS_DIR = path2.join(process.env.HOME || "", ".vibebusiness");
162
+ var CREDENTIALS_FILE = path2.join(CREDENTIALS_DIR, "credentials.json");
163
+ function loadCredentials() {
164
+ try {
165
+ if (fs3.existsSync(CREDENTIALS_FILE)) {
166
+ return JSON.parse(fs3.readFileSync(CREDENTIALS_FILE, "utf-8"));
167
+ }
168
+ } catch {
169
+ }
170
+ return {};
171
+ }
172
+ function detectClaudeCLI(customPath) {
173
+ const claudeBin = customPath || "claude";
174
+ try {
175
+ const version = (0, import_child_process2.execSync)(`${claudeBin} --version 2>/dev/null`, {
176
+ timeout: 1e4,
177
+ encoding: "utf-8"
178
+ }).trim();
179
+ return { found: true, path: claudeBin, version };
180
+ } catch {
181
+ const commonPaths = [
182
+ "/usr/local/bin/claude",
183
+ path2.join(process.env.HOME || "", ".claude", "bin", "claude"),
184
+ path2.join(process.env.HOME || "", ".local", "bin", "claude")
185
+ ];
186
+ for (const p of commonPaths) {
187
+ try {
188
+ if (fs3.existsSync(p)) {
189
+ const version = (0, import_child_process2.execSync)(`${p} --version 2>/dev/null`, {
190
+ timeout: 1e4,
191
+ encoding: "utf-8"
192
+ }).trim();
193
+ return { found: true, path: p, version };
194
+ }
195
+ } catch {
196
+ continue;
197
+ }
198
+ }
199
+ return { found: false, path: claudeBin };
200
+ }
201
+ }
202
+ function detectBYOKKeys() {
203
+ if (process.env.ANTHROPIC_API_KEY) {
204
+ return { provider: "anthropic-api", key: process.env.ANTHROPIC_API_KEY, source: "env" };
205
+ }
206
+ if (process.env.OPENAI_API_KEY) {
207
+ return { provider: "openai-api", key: process.env.OPENAI_API_KEY, source: "env" };
208
+ }
209
+ if (process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY) {
210
+ return { provider: "google-api", key: process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY, source: "env" };
211
+ }
212
+ const creds = loadCredentials();
213
+ if (creds.ANTHROPIC_API_KEY) {
214
+ process.env.ANTHROPIC_API_KEY = creds.ANTHROPIC_API_KEY;
215
+ return { provider: "anthropic-api", key: creds.ANTHROPIC_API_KEY, source: "credentials" };
216
+ }
217
+ if (creds.OPENAI_API_KEY) {
218
+ process.env.OPENAI_API_KEY = creds.OPENAI_API_KEY;
219
+ return { provider: "openai-api", key: creds.OPENAI_API_KEY, source: "credentials" };
220
+ }
221
+ if (creds.GOOGLE_API_KEY || creds.GEMINI_API_KEY) {
222
+ const key = creds.GOOGLE_API_KEY || creds.GEMINI_API_KEY;
223
+ process.env.GOOGLE_API_KEY = key;
224
+ return { provider: "google-api", key, source: "credentials" };
225
+ }
226
+ return null;
227
+ }
228
+ var CONFIG_DIR = path2.join(process.env.HOME || "", ".ai-analyst");
229
+ var PROVIDER_CONFIG_FILE = path2.join(CONFIG_DIR, "provider.json");
230
+ function requireClaudeCLI(feature) {
231
+ const cli = detectClaudeCLI();
232
+ if (cli.found) {
233
+ return cli.path;
234
+ }
235
+ const featureLabel = feature ? ` (${feature})` : "";
236
+ const byok = detectBYOKKeys();
237
+ if (byok) {
238
+ throw new Error(
239
+ `This feature${featureLabel} requires Claude Code CLI for file and tool access.
240
+ Your API key is detected and works for reasoning tasks (heartbeat, idea evaluation),
241
+ but codebase analysis and implementation need the full CLI.
242
+ Install it with: npm install -g @anthropic-ai/claude-code`
243
+ );
244
+ }
245
+ throw new Error(
246
+ `No AI provider found${featureLabel}.
247
+ Install Claude Code CLI: npm install -g @anthropic-ai/claude-code
248
+ Or set an API key: export ANTHROPIC_API_KEY=sk-ant-...`
249
+ );
250
+ }
251
+
252
+ // scripts/lib/paths.ts
253
+ var path3 = __toESM(require("path"));
254
+ var fs4 = __toESM(require("fs"));
111
255
  function findProjectRoot() {
112
256
  const cwd = process.cwd();
113
- if (fs3.existsSync(path2.join(cwd, "data", "config.json"))) {
257
+ if (fs4.existsSync(path3.join(cwd, "data", "config.json"))) {
114
258
  return cwd;
115
259
  }
116
260
  try {
117
- const entries = fs3.readdirSync(cwd, { withFileTypes: true });
261
+ const entries = fs4.readdirSync(cwd, { withFileTypes: true });
118
262
  for (const entry of entries) {
119
263
  if (entry.isDirectory()) {
120
- const candidate = path2.join(cwd, entry.name, "data", "config.json");
121
- if (fs3.existsSync(candidate)) {
122
- return path2.join(cwd, entry.name);
264
+ const candidate = path3.join(cwd, entry.name, "data", "config.json");
265
+ if (fs4.existsSync(candidate)) {
266
+ return path3.join(cwd, entry.name);
123
267
  }
124
268
  }
125
269
  }
@@ -127,9 +271,9 @@ function findProjectRoot() {
127
271
  }
128
272
  let dir = cwd;
129
273
  for (let i = 0; i < 5; i++) {
130
- const parent = path2.dirname(dir);
274
+ const parent = path3.dirname(dir);
131
275
  if (parent === dir) break;
132
- if (fs3.existsSync(path2.join(parent, "data", "config.json"))) {
276
+ if (fs4.existsSync(path3.join(parent, "data", "config.json"))) {
133
277
  return parent;
134
278
  }
135
279
  dir = parent;
@@ -137,26 +281,26 @@ function findProjectRoot() {
137
281
  return cwd;
138
282
  }
139
283
  var PROJECT_DIR = findProjectRoot();
140
- var DATA_DIR = path2.join(PROJECT_DIR, "data");
141
- var STATUS_FILE = path2.join(PROJECT_DIR, "STATUS.md");
142
- var TODO_FILE = path2.join(PROJECT_DIR, "TODO.md");
143
- var MEMORY_FILE = path2.join(PROJECT_DIR, "MEMORY.md");
144
- var IDEAS_FILE = path2.join(DATA_DIR, "ideas.json");
145
- var GOALS_FILE = path2.join(DATA_DIR, "goals.json");
146
- var SESSIONS_FILE = path2.join(DATA_DIR, "sessions.json");
147
- var CONFIG_FILE = path2.join(DATA_DIR, "config.json");
148
- var BUSINESS_CONTEXT_FILE = path2.join(DATA_DIR, "business-context.json");
149
- var HYPOTHESES_FILE = path2.join(DATA_DIR, "hypotheses.json");
150
- var IMPLEMENTATIONS_FILE = path2.join(DATA_DIR, "implementations.json");
151
- var ROADMAP_FILE = path2.join(DATA_DIR, "roadmap.json");
152
- var COMPETITORS_FILE = path2.join(DATA_DIR, "competitors.json");
153
- var POSITIONING_FILE = path2.join(DATA_DIR, "positioning.json");
154
- var PAGES_FILE = path2.join(DATA_DIR, "pages.json");
155
- var PAYMENTS_FILE = path2.join(DATA_DIR, "payments.json");
156
- var POSTHOG_FILE = path2.join(DATA_DIR, "posthog.json");
157
- var SOCIAL_FILE = path2.join(DATA_DIR, "social.json");
158
- var TEMPLATES_DIR = path2.join(__dirname, "..", "..", "templates");
159
- var COMMAND_TEMPLATES_DIR = path2.join(TEMPLATES_DIR, "commands");
284
+ var DATA_DIR = path3.join(PROJECT_DIR, "data");
285
+ var STATUS_FILE = path3.join(PROJECT_DIR, "STATUS.md");
286
+ var TODO_FILE = path3.join(PROJECT_DIR, "TODO.md");
287
+ var MEMORY_FILE = path3.join(PROJECT_DIR, "MEMORY.md");
288
+ var IDEAS_FILE = path3.join(DATA_DIR, "ideas.json");
289
+ var GOALS_FILE = path3.join(DATA_DIR, "goals.json");
290
+ var SESSIONS_FILE = path3.join(DATA_DIR, "sessions.json");
291
+ var CONFIG_FILE = path3.join(DATA_DIR, "config.json");
292
+ var BUSINESS_CONTEXT_FILE = path3.join(DATA_DIR, "business-context.json");
293
+ var HYPOTHESES_FILE = path3.join(DATA_DIR, "hypotheses.json");
294
+ var IMPLEMENTATIONS_FILE = path3.join(DATA_DIR, "implementations.json");
295
+ var ROADMAP_FILE = path3.join(DATA_DIR, "roadmap.json");
296
+ var COMPETITORS_FILE = path3.join(DATA_DIR, "competitors.json");
297
+ var POSITIONING_FILE = path3.join(DATA_DIR, "positioning.json");
298
+ var PAGES_FILE = path3.join(DATA_DIR, "pages.json");
299
+ var PAYMENTS_FILE = path3.join(DATA_DIR, "payments.json");
300
+ var POSTHOG_FILE = path3.join(DATA_DIR, "posthog.json");
301
+ var SOCIAL_FILE = path3.join(DATA_DIR, "social.json");
302
+ var TEMPLATES_DIR = path3.join(__dirname, "..", "..", "templates");
303
+ var COMMAND_TEMPLATES_DIR = path3.join(TEMPLATES_DIR, "commands");
160
304
 
161
305
  // scripts/implement.ts
162
306
  function parseArgs() {
@@ -202,7 +346,7 @@ function parseArgs() {
202
346
  return { ideaId, repoName, scope, skipPr, createPrOnly, timeout, model, workspacePath };
203
347
  }
204
348
  function loadJson(filePath) {
205
- return JSON.parse(fs4.readFileSync(filePath, "utf-8"));
349
+ return JSON.parse(fs5.readFileSync(filePath, "utf-8"));
206
350
  }
207
351
  function saveJson(filePath, data) {
208
352
  atomicWriteFileSync(filePath, JSON.stringify(data, null, 2));
@@ -266,7 +410,7 @@ function detectTargetRepo(idea, config) {
266
410
  const filesAnalyzed = idea.source.files_analyzed || [];
267
411
  for (const file of filesAnalyzed) {
268
412
  for (const repo of config.repos) {
269
- if (file.includes(repo.name) || file.includes(path3.basename(repo.path))) {
413
+ if (file.includes(repo.name) || file.includes(path4.basename(repo.path))) {
270
414
  return repo;
271
415
  }
272
416
  }
@@ -280,47 +424,48 @@ function detectTargetRepo(idea, config) {
280
424
  return config.repos[0];
281
425
  }
282
426
  function setupWorkspace(repo, workspaceDir, implId) {
283
- const repoWorkspace = path3.join(workspaceDir, repo.name);
284
- if (!fs4.existsSync(workspaceDir)) {
427
+ const repoWorkspace = path4.join(workspaceDir, repo.name);
428
+ if (!fs5.existsSync(workspaceDir)) {
285
429
  addLog(implId, `Creating workspace directory: ${workspaceDir}`);
286
- fs4.mkdirSync(workspaceDir, { recursive: true });
430
+ fs5.mkdirSync(workspaceDir, { recursive: true });
287
431
  }
288
- if (fs4.existsSync(repoWorkspace)) {
432
+ if (fs5.existsSync(repoWorkspace)) {
289
433
  cleanGitState(repoWorkspace);
290
434
  if (repo.github_url) {
291
435
  try {
292
436
  const currentOrigin = exec("git remote get-url origin", repoWorkspace);
293
437
  if (!currentOrigin.includes("github.com")) {
294
438
  addLog(implId, `Repointing origin from local path to ${repo.github_url}`);
295
- exec(`git remote set-url origin ${repo.github_url}`, repoWorkspace);
439
+ execGit(["remote", "set-url", "origin", repo.github_url], repoWorkspace);
296
440
  }
297
441
  } catch {
298
- exec(`git remote add origin ${repo.github_url}`, repoWorkspace);
442
+ execGit(["remote", "add", "origin", repo.github_url], repoWorkspace);
299
443
  }
300
444
  }
301
445
  addLog(implId, `Updating existing clone at ${repoWorkspace}`);
302
- exec(`git fetch origin`, repoWorkspace);
303
- exec(`git checkout ${repo.default_branch || "main"}`, repoWorkspace);
304
- exec(`git pull origin ${repo.default_branch || "main"}`, repoWorkspace);
446
+ exec("git fetch origin", repoWorkspace);
447
+ execGit(["checkout", repo.default_branch || "main"], repoWorkspace);
448
+ execGit(["pull", "origin", repo.default_branch || "main"], repoWorkspace);
305
449
  } else {
306
450
  const cloneSource = repo.github_url || repo.path;
307
451
  if (!cloneSource) {
308
452
  throw new Error(`No github_url or path configured for repo: ${repo.name}`);
309
453
  }
310
454
  addLog(implId, `Cloning ${cloneSource} to ${repoWorkspace}`);
311
- exec(`git clone ${cloneSource} ${repoWorkspace}`);
455
+ execGit(["clone", cloneSource, repoWorkspace]);
312
456
  }
313
457
  return repoWorkspace;
314
458
  }
315
459
  function createBranch(workspacePath, branchName, defaultBranch, implId) {
460
+ validateBranchName(branchName);
316
461
  addLog(implId, `Creating branch: ${branchName}`);
317
- exec(`git checkout ${defaultBranch}`, workspacePath);
462
+ execGit(["checkout", defaultBranch], workspacePath);
318
463
  try {
319
- exec(`git rev-parse --verify ${branchName}`, workspacePath);
464
+ execGit(["rev-parse", "--verify", branchName], workspacePath);
320
465
  addLog(implId, `Branch ${branchName} already exists, checking out`);
321
- exec(`git checkout ${branchName}`, workspacePath);
466
+ execGit(["checkout", branchName], workspacePath);
322
467
  } catch {
323
- exec(`git checkout -b ${branchName}`, workspacePath);
468
+ execGit(["checkout", "-b", branchName], workspacePath);
324
469
  }
325
470
  }
326
471
  function generatePrompt(idea, scopeBase64) {
@@ -413,6 +558,7 @@ IMPORTANT:
413
558
  `;
414
559
  }
415
560
  async function runImplementation(workspacePath, prompt, implId, timeoutMs = 3e5, options = {}) {
561
+ requireClaudeCLI("implementation");
416
562
  const modelLabel = options.model ? ` (model: ${options.model})` : "";
417
563
  addLog(implId, `Running Claude Code to implement changes (timeout: ${timeoutMs / 1e3}s${modelLabel})...`);
418
564
  const claudeArgs = [
@@ -427,7 +573,7 @@ async function runImplementation(workspacePath, prompt, implId, timeoutMs = 3e5,
427
573
  }
428
574
  claudeArgs.push("-p", prompt);
429
575
  return new Promise((resolve) => {
430
- const claude = (0, import_child_process2.spawn)("claude", claudeArgs, {
576
+ const claude = (0, import_child_process3.spawn)("claude", claudeArgs, {
431
577
  cwd: workspacePath,
432
578
  env: { ...process.env },
433
579
  stdio: ["ignore", "pipe", "pipe"]
@@ -502,7 +648,7 @@ function hasChanges(workspacePath) {
502
648
  }
503
649
  function createPR(workspacePath, branchName, defaultBranch, idea, implId) {
504
650
  try {
505
- const ahead = exec(`git rev-list --count ${defaultBranch}..${branchName}`, workspacePath);
651
+ const ahead = execGit(["rev-list", "--count", `${defaultBranch}..${branchName}`], workspacePath);
506
652
  if (parseInt(ahead, 10) === 0) {
507
653
  addLog(implId, "No commits to push, skipping PR creation");
508
654
  return null;
@@ -511,7 +657,7 @@ function createPR(workspacePath, branchName, defaultBranch, idea, implId) {
511
657
  addLog(implId, "Could not determine commit count, attempting PR anyway");
512
658
  }
513
659
  addLog(implId, `Pushing branch ${branchName} to origin`);
514
- exec(`git push -u origin ${branchName}`, workspacePath, 3e4);
660
+ execGit(["push", "-u", "origin", branchName], workspacePath, 3e4);
515
661
  addLog(implId, "Creating PR with gh CLI");
516
662
  const title = `[AI] ${idea.title}`;
517
663
  const body = `## Summary
@@ -532,11 +678,11 @@ This PR was automatically generated by the AI Product Manager.
532
678
  Idea ID: \`${idea.id}\`
533
679
  `;
534
680
  try {
535
- const prOutput = exec(
536
- `gh pr create --title "${title.replace(/"/g, '\\"')}" --body "${body.replace(/"/g, '\\"')}" --base ${defaultBranch}`,
537
- workspacePath,
538
- 3e4
539
- );
681
+ const prOutput = (0, import_child_process3.execFileSync)(
682
+ "gh",
683
+ ["pr", "create", "--title", title, "--body", body, "--base", defaultBranch],
684
+ { cwd: workspacePath, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 3e4 }
685
+ ).trim();
540
686
  const urlMatch = prOutput.match(/https:\/\/github\.com\/[^\s]+\/pull\/(\d+)/);
541
687
  if (urlMatch) {
542
688
  return {
@@ -593,18 +739,18 @@ async function main() {
593
739
  const existingImpl = implData.implementations.find(
594
740
  (i) => i.idea_id === ideaId && i.status !== "failed" && i.status !== "completed"
595
741
  );
596
- const workspacePath = workspacePathOverride || path3.join(config.workspace_dir, targetRepo.name);
742
+ const workspacePath = workspacePathOverride || path4.join(config.workspace_dir, targetRepo.name);
597
743
  const isWorktreeMode = !!workspacePathOverride;
598
744
  const impl = existingImpl || createImplementation(ideaId, targetRepo.name, branchName, workspacePath);
599
745
  try {
600
746
  cleanGitState(workspacePath);
601
747
  if (isWorktreeMode && scope) {
602
- if (!fs4.existsSync(workspacePath)) {
748
+ if (!fs5.existsSync(workspacePath)) {
603
749
  throw new Error(`Worktree path does not exist: ${workspacePath}`);
604
750
  }
605
751
  addLog(impl.id, `Using pre-configured worktree: ${workspacePath}`);
606
752
  } else if (scope || createPrOnly) {
607
- if (!fs4.existsSync(workspacePath)) {
753
+ if (!fs5.existsSync(workspacePath)) {
608
754
  updateImplementation(impl.id, { status: "cloning" });
609
755
  setupWorkspace(targetRepo, config.workspace_dir, impl.id);
610
756
  }
@@ -613,20 +759,21 @@ async function main() {
613
759
  exec("git fetch origin", workspacePath);
614
760
  } catch {
615
761
  }
762
+ validateBranchName(branchName);
616
763
  addLog(impl.id, `Checking out branch: ${branchName}`);
617
764
  try {
618
- exec(`git checkout ${branchName}`, workspacePath);
765
+ execGit(["checkout", branchName], workspacePath);
619
766
  addLog(impl.id, `Merging origin/${defaultBranch} into ${branchName}...`);
620
767
  try {
621
- exec(`git merge origin/${defaultBranch} --no-edit`, workspacePath);
768
+ execGit(["merge", `origin/${defaultBranch}`, "--no-edit"], workspacePath);
622
769
  } catch {
623
770
  addLog(impl.id, "Merge conflict detected, aborting merge");
624
- exec("git merge --abort", workspacePath);
771
+ execGit(["merge", "--abort"], workspacePath);
625
772
  }
626
773
  } catch {
627
- exec(`git checkout ${defaultBranch}`, workspacePath);
628
- exec(`git pull origin ${defaultBranch}`, workspacePath);
629
- exec(`git checkout -b ${branchName}`, workspacePath);
774
+ execGit(["checkout", defaultBranch], workspacePath);
775
+ execGit(["pull", "origin", defaultBranch], workspacePath);
776
+ execGit(["checkout", "-b", branchName], workspacePath);
630
777
  }
631
778
  } else {
632
779
  updateImplementation(impl.id, { status: "cloning" });
@@ -670,21 +817,22 @@ async function main() {
670
817
  addLog(impl.id, `Skipping junk file: ${file}`);
671
818
  continue;
672
819
  }
673
- exec(`git add "${file}"`, workspacePath);
820
+ execGit(["add", file], workspacePath);
674
821
  }
675
822
  } else {
676
823
  exec("git add -A", workspacePath);
677
824
  }
678
825
  const staged = exec("git diff --cached --stat", workspacePath);
679
826
  if (staged.trim().length > 0) {
680
- const commitMsg = scope ? `feat: ${idea.title} (sub-task)
827
+ const safeTitle = sanitizeForCommitMessage(idea.title);
828
+ const commitMsg = scope ? `feat: ${safeTitle} (sub-task)
681
829
 
682
830
  Implemented by AI Product Manager
683
- Idea: ${idea.id}` : `feat: ${idea.title}
831
+ Idea: ${idea.id}` : `feat: ${safeTitle}
684
832
 
685
833
  Implemented by AI Product Manager
686
834
  Idea: ${idea.id}`;
687
- exec(`git commit -m "${commitMsg}"`, workspacePath);
835
+ execGit(["commit", "-m", commitMsg], workspacePath);
688
836
  } else {
689
837
  addLog(impl.id, "No staged changes after filtering, skipping commit");
690
838
  }
@@ -694,7 +842,7 @@ Idea: ${idea.id}`;
694
842
  if (!isWorktreeMode) {
695
843
  try {
696
844
  addLog(impl.id, `Pushing branch ${branchName} to origin`);
697
- exec(`git push -u origin ${branchName}`, workspacePath, 3e4);
845
+ execGit(["push", "-u", "origin", branchName], workspacePath, 3e4);
698
846
  } catch (pushError) {
699
847
  addLog(impl.id, `Warning: Failed to push branch: ${pushError.message}`);
700
848
  }