@beastmode-develeap/beastmode 0.1.267 → 0.1.269

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.
package/dist/index.js CHANGED
@@ -3223,8 +3223,18 @@ var init_bridge = __esm({
3223
3223
  });
3224
3224
 
3225
3225
  // src/engine/project-record.ts
3226
- import { existsSync as existsSync8, writeFileSync as writeFileSync6, renameSync, readFileSync as readFileSync8, readdirSync as readdirSync3, mkdirSync as mkdirSync6 } from "fs";
3227
- import { join as join8 } from "path";
3226
+ import { existsSync as existsSync8, writeFileSync as writeFileSync6, renameSync, readFileSync as readFileSync8, readdirSync as readdirSync3, mkdirSync as mkdirSync6, unlinkSync } from "fs";
3227
+ import { join as join8, basename as basename3 } from "path";
3228
+ function computeVerifyPort(existingProjects) {
3229
+ let port = 3001;
3230
+ if (existingProjects) {
3231
+ for (const p of existingProjects) {
3232
+ const v = p.deploy?.verify_port;
3233
+ if (typeof v === "number" && v >= port) port = v + 1;
3234
+ }
3235
+ }
3236
+ return port;
3237
+ }
3228
3238
  function detectShape(parsed) {
3229
3239
  if (parsed.github && typeof parsed.github === "object" && parsed.slots && typeof parsed.slots === "object") {
3230
3240
  return "canonical";
@@ -3234,10 +3244,34 @@ function detectShape(parsed) {
3234
3244
  }
3235
3245
  return "http-legacy";
3236
3246
  }
3247
+ function hasMissingCanonicalFields(parsed) {
3248
+ if (!parsed.stack) return true;
3249
+ if (!parsed.plugins) return true;
3250
+ if (!parsed.infra) return true;
3251
+ const deploy = parsed.deploy;
3252
+ if (!deploy?.target) return true;
3253
+ return false;
3254
+ }
3255
+ function normalizeDeploy(existing) {
3256
+ return {
3257
+ ...existing,
3258
+ target: existing?.target || "pr-only",
3259
+ verify_port: existing?.verify_port || 3001,
3260
+ config: existing?.config || {}
3261
+ };
3262
+ }
3237
3263
  function normalizeToCanonical(parsed, name) {
3238
3264
  const shape = detectShape(parsed);
3265
+ const existingDeploy = parsed.deploy;
3239
3266
  if (shape === "canonical") {
3240
- return parsed;
3267
+ const record = parsed;
3268
+ return {
3269
+ ...record,
3270
+ stack: record.stack || DEFAULT_STACK,
3271
+ plugins: record.plugins || [],
3272
+ infra: record.infra || DEFAULT_INFRA,
3273
+ deploy: normalizeDeploy(existingDeploy)
3274
+ };
3241
3275
  }
3242
3276
  if (shape === "cli-legacy") {
3243
3277
  const github = parsed.github;
@@ -3253,10 +3287,13 @@ function normalizeToCanonical(parsed, name) {
3253
3287
  url: "http://127.0.0.1:8080",
3254
3288
  auto_created: true
3255
3289
  },
3256
- deploy: parsed.deploy || { verify_port: 3001 },
3290
+ stack: parsed.stack || DEFAULT_STACK,
3291
+ deploy: normalizeDeploy(existingDeploy),
3257
3292
  pipeline: parsed.pipeline || {},
3258
3293
  models: parsed.models || {},
3294
+ plugins: parsed.plugins || [],
3259
3295
  slots: { max: null },
3296
+ infra: parsed.infra || DEFAULT_INFRA,
3260
3297
  registered_at: parsed.registered_at || (/* @__PURE__ */ new Date()).toISOString()
3261
3298
  };
3262
3299
  }
@@ -3264,34 +3301,85 @@ function normalizeToCanonical(parsed, name) {
3264
3301
  name: parsed.name || name,
3265
3302
  path: parsed.path || "",
3266
3303
  github: {
3267
- repo: parsed.repo || "",
3304
+ repo: parsed.repo || parsed.github?.repo || "",
3268
3305
  default_branch: "main"
3269
3306
  },
3270
- board: { id: null, url: "http://127.0.0.1:8080", auto_created: true },
3271
- deploy: { verify_port: 3001 },
3307
+ board: parsed.board || {
3308
+ id: null,
3309
+ url: "http://127.0.0.1:8080",
3310
+ auto_created: true
3311
+ },
3312
+ stack: parsed.stack || DEFAULT_STACK,
3313
+ deploy: normalizeDeploy(existingDeploy),
3272
3314
  pipeline: parsed.pipeline || {},
3273
3315
  models: parsed.models || {},
3274
- slots: { max: null },
3275
- registered_at: (/* @__PURE__ */ new Date()).toISOString()
3316
+ plugins: parsed.plugins || [],
3317
+ slots: parsed.slots || { max: null },
3318
+ infra: parsed.infra || DEFAULT_INFRA,
3319
+ registered_at: parsed.registered_at || (/* @__PURE__ */ new Date()).toISOString()
3276
3320
  };
3277
3321
  }
3278
3322
  function createProjectRecord(input) {
3323
+ const name = input.name || basename3(input.resolvedPath);
3324
+ let boardId = null;
3325
+ if (input.boardId !== void 0 && input.boardId !== null) {
3326
+ const parsed = typeof input.boardId === "string" ? parseInt(input.boardId, 10) : input.boardId;
3327
+ boardId = isNaN(parsed) ? null : parsed;
3328
+ }
3329
+ let stack = input.stack || { ...DEFAULT_STACK };
3330
+ let gitRemote = input.gitRemote;
3331
+ let plugins = input.plugins;
3332
+ let deployTarget = input.deployTarget;
3333
+ if (!input.stack || gitRemote === void 0 || plugins === void 0 || deployTarget === void 0) {
3334
+ try {
3335
+ const detected = detectStack(input.resolvedPath);
3336
+ if (!input.stack) {
3337
+ stack = {
3338
+ detected: detected.framework,
3339
+ build_command: detected.suggested_commands.build,
3340
+ dev_command: detected.suggested_commands.dev,
3341
+ test_command: detected.suggested_commands.test,
3342
+ install_command: detected.suggested_commands.install,
3343
+ dev_port: detected.dev_port,
3344
+ is_monorepo: detected.is_monorepo,
3345
+ total_packages: detected.total_packages,
3346
+ primary_languages: detected.primary_languages,
3347
+ ...detected.packages ? { packages: detected.packages } : {}
3348
+ };
3349
+ }
3350
+ if (gitRemote === void 0) gitRemote = detected.git_remote || "";
3351
+ if (plugins === void 0) plugins = detected.suggested_plugins || [];
3352
+ if (deployTarget === void 0) deployTarget = detected.suggested_deploy || "pr-only";
3353
+ } catch {
3354
+ if (gitRemote === void 0) gitRemote = "";
3355
+ if (plugins === void 0) plugins = [];
3356
+ if (deployTarget === void 0) deployTarget = "pr-only";
3357
+ }
3358
+ }
3359
+ const verifyPort = input.verifyPort ?? computeVerifyPort(input.existingProjects);
3279
3360
  return {
3280
- name: input.name,
3361
+ name,
3281
3362
  path: input.resolvedPath,
3282
3363
  github: {
3283
- repo: input.gitRemote || "",
3364
+ repo: gitRemote || "",
3284
3365
  default_branch: "main"
3285
3366
  },
3286
3367
  board: {
3287
- id: input.boardId ?? null,
3368
+ id: boardId,
3288
3369
  url: process.env.BEASTMODE_BOARD_URL || "http://127.0.0.1:8080",
3289
- auto_created: !input.boardId
3370
+ auto_created: boardId === null
3371
+ },
3372
+ stack,
3373
+ deploy: {
3374
+ target: deployTarget,
3375
+ verify_port: verifyPort,
3376
+ config: {}
3290
3377
  },
3291
- deploy: { verify_port: input.verifyPort ?? 3001 },
3292
3378
  pipeline: {},
3293
3379
  models: {},
3380
+ plugins,
3294
3381
  slots: { max: null },
3382
+ infra: { credentials: { mode: "factory" }, state_backend: "auto" },
3295
3383
  registered_at: (/* @__PURE__ */ new Date()).toISOString()
3296
3384
  };
3297
3385
  }
@@ -3339,8 +3427,16 @@ function readProjectRecord(projectsDir, name) {
3339
3427
  }
3340
3428
  const shape = detectShape(parsed);
3341
3429
  const record = normalizeToCanonical(parsed, name);
3342
- if (shape !== "canonical" || isFlat) {
3430
+ const needsWrite = shape !== "canonical" || isFlat || hasMissingCanonicalFields(parsed);
3431
+ if (needsWrite) {
3343
3432
  writeProjectRecord(projectsDir, name, record);
3433
+ console.error(`[project-record] Migrated ${name} from ${shape} to canonical schema`);
3434
+ if (isFlat) {
3435
+ try {
3436
+ unlinkSync(flatPath);
3437
+ } catch {
3438
+ }
3439
+ }
3344
3440
  }
3345
3441
  return record;
3346
3442
  }
@@ -3358,7 +3454,7 @@ function listProjectRecords(projectsDir) {
3358
3454
  }
3359
3455
  continue;
3360
3456
  }
3361
- if (entry.endsWith(".json")) {
3457
+ if (entry.endsWith(".json") && !entry.endsWith(".json.bak") && !entry.includes(".verifier")) {
3362
3458
  const name = entry.slice(0, -5);
3363
3459
  if (!seen.has(name)) {
3364
3460
  seen.add(name);
@@ -3369,9 +3465,23 @@ function listProjectRecords(projectsDir) {
3369
3465
  }
3370
3466
  return records;
3371
3467
  }
3468
+ var DEFAULT_STACK, DEFAULT_INFRA;
3372
3469
  var init_project_record = __esm({
3373
3470
  "src/engine/project-record.ts"() {
3374
3471
  "use strict";
3472
+ init_stack_detector();
3473
+ DEFAULT_STACK = {
3474
+ detected: "unknown",
3475
+ build_command: "",
3476
+ dev_command: "",
3477
+ test_command: "",
3478
+ install_command: "",
3479
+ dev_port: 3e3
3480
+ };
3481
+ DEFAULT_INFRA = {
3482
+ credentials: { mode: "factory" },
3483
+ state_backend: "auto"
3484
+ };
3375
3485
  }
3376
3486
  });
3377
3487
 
@@ -3690,7 +3800,7 @@ var init_api_routes = __esm({
3690
3800
  });
3691
3801
 
3692
3802
  // src/cli/ui/archival.ts
3693
- import { existsSync as existsSync12, mkdirSync as mkdirSync8, readdirSync as readdirSync4, renameSync as renameSync2, readFileSync as readFileSync9, statSync as statSync3, writeFileSync as writeFileSync9, unlinkSync } from "fs";
3803
+ import { existsSync as existsSync12, mkdirSync as mkdirSync8, readdirSync as readdirSync4, renameSync as renameSync2, readFileSync as readFileSync9, statSync as statSync3, writeFileSync as writeFileSync9, unlinkSync as unlinkSync2 } from "fs";
3694
3804
  import { join as join10 } from "path";
3695
3805
  function archiveOldRuns(runsDir, archiveAfterDays) {
3696
3806
  if (archiveAfterDays <= 0 || !existsSync12(runsDir)) return { archived: 0 };
@@ -3731,7 +3841,7 @@ function pinRun(runsDir, runId) {
3731
3841
  function unpinRun(runsDir, runId) {
3732
3842
  const pinFile = join10(runsDir, runId, "pinned");
3733
3843
  if (!existsSync12(pinFile)) return false;
3734
- unlinkSync(pinFile);
3844
+ unlinkSync2(pinFile);
3735
3845
  return true;
3736
3846
  }
3737
3847
  function isRunPinned(runsDir, runId) {
@@ -3760,7 +3870,7 @@ __export(inception_exports, {
3760
3870
  saveArtifact: () => saveArtifact,
3761
3871
  saveInceptionState: () => saveInceptionState
3762
3872
  });
3763
- import { existsSync as existsSync13, mkdirSync as mkdirSync9, writeFileSync as writeFileSync10, readFileSync as readFileSync10, readdirSync as readdirSync5, unlinkSync as unlinkSync2 } from "fs";
3873
+ import { existsSync as existsSync13, mkdirSync as mkdirSync9, writeFileSync as writeFileSync10, readFileSync as readFileSync10, readdirSync as readdirSync5, unlinkSync as unlinkSync3 } from "fs";
3764
3874
  import { join as join11, dirname as dirname4 } from "path";
3765
3875
  import { fileURLToPath } from "url";
3766
3876
  function getMethodologiesDir() {
@@ -3891,10 +4001,10 @@ function migrateInceptionToSession(factoryDir, productName) {
3891
4001
  const src = join11(productDir, file);
3892
4002
  if (existsSync13(src)) {
3893
4003
  writeFileSync10(join11(sessionDir, file), readFileSync10(src, "utf-8"));
3894
- unlinkSync2(src);
4004
+ unlinkSync3(src);
3895
4005
  }
3896
4006
  }
3897
- unlinkSync2(oldInception);
4007
+ unlinkSync3(oldInception);
3898
4008
  return true;
3899
4009
  }
3900
4010
  function listProducts(factoryDir) {
@@ -5129,8 +5239,8 @@ var init_chat_handler = __esm({
5129
5239
  });
5130
5240
 
5131
5241
  // src/cli/ui/board-api-routes.ts
5132
- import { readFileSync as readFileSync13, writeFileSync as writeFileSync13, existsSync as existsSync16, readdirSync as readdirSync8, unlinkSync as unlinkSync3, mkdirSync as mkdirSync12, statSync as statSync6, rmSync as rmSync3 } from "fs";
5133
- import { join as join14, basename as basename4, resolve as resolve4, dirname as dirname5 } from "path";
5242
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync13, existsSync as existsSync16, readdirSync as readdirSync8, unlinkSync as unlinkSync4, mkdirSync as mkdirSync12, statSync as statSync6, rmSync as rmSync3 } from "fs";
5243
+ import { join as join14, basename as basename5, resolve as resolve4, dirname as dirname5 } from "path";
5134
5244
  import { homedir } from "os";
5135
5245
  import { randomUUID as randomUUID2 } from "crypto";
5136
5246
  import { execSync as execSync3, spawnSync } from "child_process";
@@ -5395,14 +5505,6 @@ function _gitHeadSha(repoPath) {
5395
5505
  return "no-git";
5396
5506
  }
5397
5507
  }
5398
- function _hasClaudeCli() {
5399
- try {
5400
- execSync3("which claude", { encoding: "utf-8" });
5401
- return true;
5402
- } catch {
5403
- return false;
5404
- }
5405
- }
5406
5508
  function getBoardRoutes(factoryDir) {
5407
5509
  return [
5408
5510
  // ── Status ──
@@ -5986,6 +6088,34 @@ function getBoardRoutes(factoryDir) {
5986
6088
  return { success: true };
5987
6089
  }
5988
6090
  },
6091
+ // ── Doctor ──
6092
+ {
6093
+ method: "GET",
6094
+ pattern: "/api/doctor",
6095
+ handler: () => {
6096
+ const projectsDir = join14(factoryDir, ".beastmode", "projects");
6097
+ const projects = listProjectRecords(projectsDir);
6098
+ const checks = [];
6099
+ for (const p of projects) {
6100
+ const hasStack = !!(p.stack && p.stack.detected);
6101
+ const hasPlugins = Array.isArray(p.plugins);
6102
+ const hasInfra = !!(p.infra && p.infra.credentials);
6103
+ const hasDeployTarget = !!(p.deploy && p.deploy.target);
6104
+ const isCanonical = hasStack && hasPlugins && hasInfra && hasDeployTarget;
6105
+ checks.push({
6106
+ name: p.name,
6107
+ status: isCanonical ? "ok" : "warning",
6108
+ details: isCanonical ? "canonical schema" : `missing: ${[!hasStack && "stack", !hasPlugins && "plugins", !hasInfra && "infra", !hasDeployTarget && "deploy.target"].filter(Boolean).join(", ")}`
6109
+ });
6110
+ }
6111
+ const overallStatus = checks.every((c) => c.status === "ok") ? "ok" : "warning";
6112
+ return {
6113
+ status: overallStatus,
6114
+ project_count: projects.length,
6115
+ checks
6116
+ };
6117
+ }
6118
+ },
5989
6119
  // ── Projects ──
5990
6120
  {
5991
6121
  method: "GET",
@@ -6118,7 +6248,7 @@ function getBoardRoutes(factoryDir) {
6118
6248
  } else {
6119
6249
  throw new Error("Missing required field: provide either `path` (local) or `github_url` (clone)");
6120
6250
  }
6121
- const projectName = basename4(resolvedPath);
6251
+ const projectName = basename5(resolvedPath);
6122
6252
  const stack = detectStack(resolvedPath);
6123
6253
  let verifyPort = 3001;
6124
6254
  for (const existing of listProjectRecords(projectsDir)) {
@@ -6129,21 +6259,22 @@ function getBoardRoutes(factoryDir) {
6129
6259
  name: projectName,
6130
6260
  resolvedPath,
6131
6261
  gitRemote: stack.git_remote || void 0,
6132
- verifyPort
6262
+ verifyPort,
6263
+ plugins: stack.suggested_plugins || [],
6264
+ deployTarget: stack.suggested_deploy || "pr-only",
6265
+ stack: {
6266
+ detected: stack.framework,
6267
+ build_command: stack.suggested_commands.build,
6268
+ dev_command: stack.suggested_commands.dev,
6269
+ test_command: stack.suggested_commands.test,
6270
+ install_command: stack.suggested_commands.install,
6271
+ dev_port: stack.dev_port,
6272
+ is_monorepo: stack.is_monorepo,
6273
+ total_packages: stack.total_packages,
6274
+ primary_languages: stack.primary_languages,
6275
+ ...stack.packages ? { packages: stack.packages } : {}
6276
+ }
6133
6277
  });
6134
- record.stack = {
6135
- detected: stack.framework,
6136
- build_command: stack.suggested_commands.build,
6137
- dev_command: stack.suggested_commands.dev,
6138
- test_command: stack.suggested_commands.test,
6139
- install_command: stack.suggested_commands.install,
6140
- dev_port: stack.dev_port,
6141
- is_monorepo: stack.is_monorepo,
6142
- total_packages: stack.total_packages,
6143
- primary_languages: stack.primary_languages,
6144
- ...stack.packages ? { packages: stack.packages } : {}
6145
- };
6146
- record.infra = { credentials: { mode: "factory" }, state_backend: "auto" };
6147
6278
  writeProjectRecord(projectsDir, projectName, record);
6148
6279
  const infraDir = join14(projectsDir, projectName, "infra");
6149
6280
  mkdirSync12(join14(infraDir, "terraform"), { recursive: true });
@@ -6165,11 +6296,24 @@ function getBoardRoutes(factoryDir) {
6165
6296
  if (!hasSubDir && !hasFlat) throw new Error(`Project not found: ${name}`);
6166
6297
  if (hasSubDir) rmSync3(subDir, { recursive: true, force: true });
6167
6298
  else if (existsSync16(subDir)) rmSync3(subDir, { recursive: true, force: true });
6168
- if (hasFlat) unlinkSync3(flatPath);
6299
+ if (hasFlat) unlinkSync4(flatPath);
6169
6300
  return { success: true };
6170
6301
  }
6171
6302
  },
6172
6303
  {
6304
+ // Trigger brownfield analysis for a project.
6305
+ //
6306
+ // POST body: { tier?: "quick" | "standard" | "full" }
6307
+ // tier "quick" — writes a lightweight detectStack() placeholder, no Claude invocation.
6308
+ // tier "standard" / "full" (default) — delegates real analysis to the daemon via
6309
+ // a trigger file in the shared .beastmode volume. The daemon has access to the
6310
+ // project source files (/app/project) which the UI container does NOT have.
6311
+ // Query params: force=true — bypass cache and re-run even if HEAD hasn't changed.
6312
+ //
6313
+ // Returns immediately (analysis runs in daemon background):
6314
+ // { analyzing: true, project: string, job_id: string, status: "running" }
6315
+ // { analyzed: true, project: string, cached: true, analyzed_at: string } (cache hit)
6316
+ // { analyzed: true, project: string, tier: "quick" } (quick tier)
6173
6317
  method: "POST",
6174
6318
  pattern: "/api/projects/:name/analyze",
6175
6319
  handler: async (body, params, query) => {
@@ -6207,59 +6351,36 @@ Path: ${projectPath}
6207
6351
  }
6208
6352
  if (!force) {
6209
6353
  const meta = _readAnalyzeMeta(projectsDir, name);
6210
- const currentSha = _gitHeadSha(projectPath);
6211
- if (meta && meta.status === "complete" && meta.target_repo_head_sha === currentSha) {
6212
- return { analyzed: true, project: name, cached: true, analyzed_at: meta.analyzed_at };
6354
+ if (meta && meta.status === "complete") {
6355
+ const currentSha = _gitHeadSha(projectPath);
6356
+ if (currentSha === "no-git" || meta.target_repo_head_sha === currentSha) {
6357
+ return { analyzed: true, project: name, cached: true, analyzed_at: meta.analyzed_at };
6358
+ }
6213
6359
  }
6214
6360
  }
6215
6361
  const existingJob = _analyzeJobs.get(name);
6216
6362
  if (existingJob && existingJob.status === "running") {
6217
6363
  return { analyzing: true, project: name, job_id: existingJob.job_id, status: "running" };
6218
6364
  }
6219
- const writeMeta = (m) => {
6365
+ const triggerPath = join14(subDir, ".analyze-request.json");
6366
+ const inProgressPath = join14(subDir, ".analyze-in-progress.json");
6367
+ if (existsSync16(triggerPath) || existsSync16(inProgressPath)) {
6368
+ const existingTrigger = existsSync16(triggerPath) ? triggerPath : inProgressPath;
6220
6369
  try {
6221
- writeFileSync13(
6222
- join14(subDir, "codebase-guide.meta.json"),
6223
- JSON.stringify(m, null, 2) + "\n",
6224
- "utf-8"
6225
- );
6226
- } catch (err) {
6227
- console.error(`[analyze] Failed to write meta for ${name}:`, err);
6228
- }
6229
- };
6230
- if (!_hasClaudeCli()) {
6231
- const brownfieldPath = join14(subDir, "brownfield.md");
6232
- if (!existsSync16(brownfieldPath)) {
6233
- const { detectStack: detectStackFn } = await Promise.resolve().then(() => (init_engine(), engine_exports));
6234
- let stackInfo = "Unknown stack";
6235
- try {
6236
- const stack = detectStackFn(projectPath);
6237
- const cmds = stack.suggested_commands;
6238
- stackInfo = `Framework: ${stack.framework || "unknown"}
6239
- Build: ${cmds?.build || "unknown"}
6240
- Dev: ${cmds?.dev || "unknown"}`;
6241
- } catch {
6242
- }
6243
- writeFileSync13(brownfieldPath, `# Brownfield Analysis: ${name}
6244
-
6245
- ${stackInfo}
6246
-
6247
- Path: ${projectPath}
6248
- `);
6370
+ const t = JSON.parse(readFileSync13(existingTrigger, "utf-8"));
6371
+ const job2 = {
6372
+ job_id: t.job_id,
6373
+ project: name,
6374
+ status: "running",
6375
+ started_at: t.requested_at
6376
+ };
6377
+ _analyzeJobs.set(name, job2);
6378
+ return { analyzing: true, project: name, job_id: t.job_id, status: "running" };
6379
+ } catch {
6249
6380
  }
6250
- writeMeta({
6251
- analyzed_at: (/* @__PURE__ */ new Date()).toISOString(),
6252
- target_repo_head_sha: _gitHeadSha(projectPath),
6253
- status: "failed",
6254
- error: "Claude CLI not available",
6255
- duration_seconds: 0
6256
- });
6257
- console.warn(`[analyze] Claude CLI unavailable for project ${name}; wrote detectStack() fallback.`);
6258
- return { analyzed: true, project: name, fallback: true };
6259
6381
  }
6260
6382
  const jobId = randomUUID2();
6261
6383
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
6262
- const startMs = Date.now();
6263
6384
  const job = {
6264
6385
  job_id: jobId,
6265
6386
  project: name,
@@ -6267,171 +6388,86 @@ Path: ${projectPath}
6267
6388
  started_at: startedAt
6268
6389
  };
6269
6390
  _analyzeJobs.set(name, job);
6270
- (async () => {
6271
- try {
6272
- const { spawn: spawn4 } = await import("child_process");
6273
- const prompt = [
6274
- "You are the Brownfield Analyst. Analyze the codebase at the current working directory.",
6275
- `Write your output in pyramid format (L0/L1/L2) to the directory: ${subDir}`,
6276
- "Produce exactly these four files:",
6277
- ` - ${join14(subDir, "codebase-guide.md")} (full L2 analysis: architecture, conventions, safe modification points, fragile areas)`,
6278
- ` - ${join14(subDir, "codebase-guide.l1.md")} (paragraph summary only)`,
6279
- ` - ${join14(subDir, "codebase-guide.l0.txt")} (one-sentence summary only)`,
6280
- "Do not write anything else outside the BEASTMODE_OUTPUT_DIR."
6281
- ].join("\n");
6282
- const isRoot = process.getuid?.() === 0;
6283
- const claudeArgs = [
6284
- "-p",
6285
- prompt,
6286
- "--agent",
6287
- "brownfield-analyst",
6288
- "--output-format",
6289
- "stream-json",
6290
- "--verbose",
6291
- "--dangerously-skip-permissions"
6292
- ];
6293
- const spawnCmd = isRoot ? "runuser" : "claude";
6294
- const spawnArgs = isRoot ? ["-u", "node", "--", "claude", ...claudeArgs] : claudeArgs;
6295
- const child = spawn4(spawnCmd, spawnArgs, {
6296
- cwd: projectPath,
6297
- env: {
6298
- ...process.env,
6299
- BEASTMODE_PROJECT: name,
6300
- BEASTMODE_OUTPUT_DIR: subDir,
6301
- ...isRoot ? { HOME: "/home/node" } : {}
6302
- },
6303
- stdio: ["ignore", "pipe", "pipe"]
6304
- });
6305
- child.stdout?.on("data", () => {
6306
- });
6307
- child.stderr?.on("data", () => {
6308
- });
6309
- child.on("close", (code) => {
6310
- const durationSeconds = (Date.now() - startMs) / 1e3;
6311
- const sha = _gitHeadSha(projectPath);
6312
- const guidePath = join14(subDir, "codebase-guide.md");
6313
- const ok = code === 0 && existsSync16(guidePath);
6314
- if (ok) {
6315
- writeMeta({
6316
- analyzed_at: (/* @__PURE__ */ new Date()).toISOString(),
6317
- analyst_version: "brownfield-analyst-v1",
6318
- target_repo_head_sha: sha,
6319
- project_name: name,
6320
- duration_seconds: durationSeconds,
6321
- status: "complete"
6322
- });
6323
- const legacy = join14(subDir, "brownfield.md");
6324
- if (existsSync16(legacy)) {
6325
- try {
6326
- unlinkSync3(legacy);
6327
- } catch {
6328
- }
6329
- }
6330
- _analyzeJobs.set(name, {
6331
- ...job,
6332
- status: "complete",
6333
- completed_at: (/* @__PURE__ */ new Date()).toISOString(),
6334
- duration_seconds: durationSeconds
6335
- });
6336
- } else {
6337
- const errMsg = `claude exited with code ${code}`;
6338
- writeMeta({
6339
- analyzed_at: (/* @__PURE__ */ new Date()).toISOString(),
6340
- target_repo_head_sha: sha,
6341
- status: "failed",
6342
- error: errMsg,
6343
- duration_seconds: durationSeconds
6344
- });
6345
- _analyzeJobs.set(name, {
6346
- ...job,
6347
- status: "failed",
6348
- completed_at: (/* @__PURE__ */ new Date()).toISOString(),
6349
- duration_seconds: durationSeconds,
6350
- error: errMsg
6351
- });
6352
- }
6353
- });
6354
- child.on("error", (err) => {
6355
- const durationSeconds = (Date.now() - startMs) / 1e3;
6356
- const sha = _gitHeadSha(projectPath);
6357
- writeMeta({
6358
- analyzed_at: (/* @__PURE__ */ new Date()).toISOString(),
6359
- target_repo_head_sha: sha,
6360
- status: "failed",
6361
- error: String(err),
6362
- duration_seconds: durationSeconds
6363
- });
6364
- _analyzeJobs.set(name, {
6365
- ...job,
6366
- status: "failed",
6367
- completed_at: (/* @__PURE__ */ new Date()).toISOString(),
6368
- duration_seconds: durationSeconds,
6369
- error: String(err)
6370
- });
6371
- });
6372
- } catch (err) {
6373
- const durationSeconds = (Date.now() - startMs) / 1e3;
6374
- const sha = _gitHeadSha(projectPath);
6375
- writeMeta({
6376
- analyzed_at: (/* @__PURE__ */ new Date()).toISOString(),
6377
- target_repo_head_sha: sha,
6378
- status: "failed",
6379
- error: String(err),
6380
- duration_seconds: durationSeconds
6381
- });
6382
- _analyzeJobs.set(name, {
6383
- ...job,
6384
- status: "failed",
6385
- completed_at: (/* @__PURE__ */ new Date()).toISOString(),
6386
- duration_seconds: durationSeconds,
6387
- error: String(err)
6388
- });
6389
- }
6390
- })();
6391
+ writeFileSync13(
6392
+ triggerPath,
6393
+ JSON.stringify({
6394
+ job_id: jobId,
6395
+ project_name: name,
6396
+ project_path: projectPath,
6397
+ requested_at: startedAt,
6398
+ force
6399
+ }, null, 2) + "\n",
6400
+ "utf-8"
6401
+ );
6391
6402
  return { analyzing: true, project: name, job_id: jobId, status: "running" };
6392
6403
  }
6393
6404
  },
6394
6405
  {
6406
+ // Poll the status of a background brownfield analysis job.
6407
+ //
6408
+ // Returns one of:
6409
+ // { status: "running", job_id: string, started_at: string }
6410
+ // { status: "complete", analyzed_at: string, duration_seconds: number, target_repo_head_sha: string }
6411
+ // { status: "cached", analyzed_at: string, duration_seconds: number, target_repo_head_sha: string }
6412
+ // { status: "failed", job_id?: string, error: string, started_at?: string, completed_at?: string }
6413
+ // { status: "idle" } — no analysis has been requested for this project
6395
6414
  method: "GET",
6396
6415
  pattern: "/api/projects/:name/analyze/status",
6397
6416
  handler: (_body, params) => {
6398
6417
  const { name } = params;
6399
6418
  assertSafeName(name);
6400
6419
  const projectsDir = join14(factoryDir, ".beastmode", "projects");
6401
- const job = _analyzeJobs.get(name);
6402
- const meta = _readAnalyzeMeta(projectsDir, name);
6403
- if (job && job.status === "running") {
6404
- return {
6405
- status: "running",
6406
- job_id: job.job_id,
6407
- started_at: job.started_at
6408
- };
6409
- }
6410
- if (job && job.status === "failed") {
6411
- return {
6412
- status: "failed",
6413
- job_id: job.job_id,
6414
- error: job.error,
6415
- started_at: job.started_at,
6416
- completed_at: job.completed_at,
6417
- duration_seconds: job.duration_seconds
6418
- };
6420
+ const subDir = join14(projectsDir, name);
6421
+ const triggerPath = join14(subDir, ".analyze-request.json");
6422
+ const inProgressPath = join14(subDir, ".analyze-in-progress.json");
6423
+ if (existsSync16(triggerPath) || existsSync16(inProgressPath)) {
6424
+ const job2 = _analyzeJobs.get(name);
6425
+ try {
6426
+ const p = existsSync16(triggerPath) ? triggerPath : inProgressPath;
6427
+ const t = JSON.parse(readFileSync13(p, "utf-8"));
6428
+ if (!job2 || job2.status !== "running") {
6429
+ _analyzeJobs.set(name, { job_id: t.job_id, project: name, status: "running", started_at: t.requested_at });
6430
+ }
6431
+ return { status: "running", job_id: t.job_id, started_at: t.requested_at };
6432
+ } catch {
6433
+ return { status: "running", job_id: job2?.job_id, started_at: job2?.started_at };
6434
+ }
6419
6435
  }
6436
+ const meta = _readAnalyzeMeta(projectsDir, name);
6420
6437
  if (meta && meta.status === "complete") {
6438
+ const job2 = _analyzeJobs.get(name);
6439
+ if (job2?.status === "running") {
6440
+ _analyzeJobs.set(name, { ...job2, status: "complete" });
6441
+ }
6421
6442
  return {
6422
- status: job?.status === "cached" ? "cached" : "complete",
6443
+ status: "complete",
6423
6444
  analyzed_at: meta.analyzed_at,
6424
6445
  duration_seconds: meta.duration_seconds,
6425
6446
  target_repo_head_sha: meta.target_repo_head_sha
6426
6447
  };
6427
6448
  }
6428
6449
  if (meta && meta.status === "failed") {
6450
+ const job2 = _analyzeJobs.get(name);
6451
+ if (job2?.status === "running") {
6452
+ _analyzeJobs.set(name, { ...job2, status: "failed", error: meta.error });
6453
+ }
6429
6454
  return {
6430
6455
  status: "failed",
6431
6456
  analyzed_at: meta.analyzed_at,
6432
6457
  error: meta.error
6433
6458
  };
6434
6459
  }
6460
+ const job = _analyzeJobs.get(name);
6461
+ if (job && job.status === "failed") {
6462
+ return {
6463
+ status: "failed",
6464
+ job_id: job.job_id,
6465
+ error: job.error,
6466
+ started_at: job.started_at,
6467
+ completed_at: job.completed_at,
6468
+ duration_seconds: job.duration_seconds
6469
+ };
6470
+ }
6435
6471
  return { status: "idle" };
6436
6472
  }
6437
6473
  },
@@ -7979,7 +8015,7 @@ __export(sync_claude_creds_exports, {
7979
8015
  });
7980
8016
  import { Command as Command2 } from "commander";
7981
8017
  import { execSync as execSync5, spawnSync as spawnSync2 } from "child_process";
7982
- import { writeFileSync as writeFileSync15, readFileSync as readFileSync16, chmodSync, mkdirSync as mkdirSync14, existsSync as existsSync19, unlinkSync as unlinkSync4 } from "fs";
8018
+ import { writeFileSync as writeFileSync15, readFileSync as readFileSync16, chmodSync, mkdirSync as mkdirSync14, existsSync as existsSync19, unlinkSync as unlinkSync5 } from "fs";
7983
8019
  import { join as join17 } from "path";
7984
8020
  import { homedir as homedir2, platform } from "os";
7985
8021
  function systemdUserDir() {
@@ -8052,7 +8088,7 @@ function removeUrgencyMarker(factoryDir) {
8052
8088
  if (!factoryDir) return;
8053
8089
  const markerPath = urgencyMarkerHostPath(factoryDir);
8054
8090
  try {
8055
- unlinkSync4(markerPath);
8091
+ unlinkSync5(markerPath);
8056
8092
  } catch {
8057
8093
  }
8058
8094
  }
@@ -8158,7 +8194,7 @@ function uninstallAgent() {
8158
8194
  warn(`launchctl bootout returned: ${result.stderr || result.stdout}`);
8159
8195
  }
8160
8196
  try {
8161
- unlinkSync4(plist);
8197
+ unlinkSync5(plist);
8162
8198
  } catch {
8163
8199
  }
8164
8200
  success(`LaunchAgent removed: ${LAUNCH_AGENT_LABEL}`);
@@ -8344,14 +8380,14 @@ function uninstallAgentLinux() {
8344
8380
  let removed = false;
8345
8381
  if (existsSync19(servicePath)) {
8346
8382
  try {
8347
- unlinkSync4(servicePath);
8383
+ unlinkSync5(servicePath);
8348
8384
  removed = true;
8349
8385
  } catch {
8350
8386
  }
8351
8387
  }
8352
8388
  if (existsSync19(timerPath)) {
8353
8389
  try {
8354
- unlinkSync4(timerPath);
8390
+ unlinkSync5(timerPath);
8355
8391
  removed = true;
8356
8392
  } catch {
8357
8393
  }
@@ -8529,7 +8565,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8529
8565
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8530
8566
  import { z as z2 } from "zod";
8531
8567
  import { readFileSync as readFileSync29, writeFileSync as writeFileSync25, existsSync as existsSync32, readdirSync as readdirSync12 } from "fs";
8532
- import { join as join30, resolve as resolve18, basename as basename6 } from "path";
8568
+ import { join as join30, resolve as resolve18, basename as basename7 } from "path";
8533
8569
  import { randomUUID as randomUUID4 } from "crypto";
8534
8570
  function readJsonFile2(filePath) {
8535
8571
  if (!existsSync32(filePath)) return null;
@@ -8848,7 +8884,7 @@ function createMcpServer() {
8848
8884
  if (!existsSync32(resolvedPath)) {
8849
8885
  return { content: [{ type: "text", text: `Directory not found: ${resolvedPath}` }], isError: true };
8850
8886
  }
8851
- const projectName = basename6(resolvedPath);
8887
+ const projectName = basename7(resolvedPath);
8852
8888
  const stack = detectStack(resolvedPath);
8853
8889
  const projectsDir = join30(factoryDir, ".beastmode", "projects");
8854
8890
  let verifyPort = 3001;
@@ -8966,7 +9002,7 @@ init_engine();
8966
9002
  init_file_writer();
8967
9003
  import { Command as Command3 } from "commander";
8968
9004
  import inquirer from "inquirer";
8969
- import { resolve as resolve6, basename as basename5, join as join18 } from "path";
9005
+ import { resolve as resolve6, basename as basename6, join as join18 } from "path";
8970
9006
  import { existsSync as existsSync20, writeFileSync as writeFileSync16, mkdirSync as mkdirSync15, readFileSync as readFileSync17 } from "fs";
8971
9007
 
8972
9008
  // src/cli/utils/docker.ts
@@ -9227,7 +9263,7 @@ function isSourceRepo(dir) {
9227
9263
  async function runInit(name, opts) {
9228
9264
  if (opts.ui) {
9229
9265
  const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
9230
- const factoryName2 = name || basename5(resolve6("."));
9266
+ const factoryName2 = name || basename6(resolve6("."));
9231
9267
  const projectPath2 = opts.project ? resolve6(opts.project) : void 0;
9232
9268
  info("Starting init wizard...");
9233
9269
  const uiServer = await startServer2({
@@ -9260,7 +9296,7 @@ async function runInit(name, opts) {
9260
9296
  const totalSteps = 5;
9261
9297
  header("BeastMode \u2014 Dark Factory Init");
9262
9298
  info("Creating your Custom Dark Factory\n");
9263
- const factoryName = name || basename5(resolve6("."));
9299
+ const factoryName = name || basename6(resolve6("."));
9264
9300
  if (existsSync20(factoryName) && existsSync20(resolve6(factoryName, ".beastmode"))) {
9265
9301
  throw new Error(`Factory already exists at ./${factoryName}. Use 'beastmode config' to modify.`);
9266
9302
  }
@@ -9276,7 +9312,7 @@ async function runInit(name, opts) {
9276
9312
  }
9277
9313
  const templateProjectPath = resolve6(opts.project);
9278
9314
  const templateStack = detectStack(templateProjectPath);
9279
- const templateProjectName = basename5(templateProjectPath);
9315
+ const templateProjectName = basename6(templateProjectPath);
9280
9316
  const actions2 = scaffoldFactory(factoryName, template.config, {
9281
9317
  name: templateProjectName,
9282
9318
  repo: templateStack.git_remote || void 0,
@@ -9471,7 +9507,7 @@ async function runInit(name, opts) {
9471
9507
  success("Board UI password set");
9472
9508
  }
9473
9509
  step(5, totalSteps, "Boot");
9474
- const projectName = basename5(projectPath);
9510
+ const projectName = basename6(projectPath);
9475
9511
  const actions = scaffoldFactory(factoryName, config, {
9476
9512
  name: projectName,
9477
9513
  repo: stack.git_remote || void 0,
@@ -10250,8 +10286,8 @@ for (const artifact of VALID_ARTIFACTS) {
10250
10286
 
10251
10287
  // src/cli/commands/status.ts
10252
10288
  init_status_checker();
10289
+ init_engine();
10253
10290
  init_schemas();
10254
- init_project_record();
10255
10291
  init_display();
10256
10292
  import { Command as Command10 } from "commander";
10257
10293
  import { existsSync as existsSync24, readFileSync as readFileSync21, readdirSync as readdirSync10 } from "fs";
@@ -12572,8 +12608,8 @@ async function runDaemon(opts) {
12572
12608
  const exitCode = await new Promise((resolvePromise) => {
12573
12609
  child.on("exit", (code) => {
12574
12610
  try {
12575
- const { unlinkSync: unlinkSync7 } = __require("fs");
12576
- unlinkSync7(pidFile);
12611
+ const { unlinkSync: unlinkSync8 } = __require("fs");
12612
+ unlinkSync8(pidFile);
12577
12613
  } catch {
12578
12614
  }
12579
12615
  resolvePromise(code ?? 1);
@@ -13214,7 +13250,7 @@ init_display();
13214
13250
  import { Command as Command23 } from "commander";
13215
13251
  import { spawn as spawn3 } from "child_process";
13216
13252
  import { existsSync as existsSync38 } from "fs";
13217
- import { basename as basename7, join as join37, resolve as resolve20 } from "path";
13253
+ import { basename as basename8, join as join37, resolve as resolve20 } from "path";
13218
13254
 
13219
13255
  // src/cli/runner-image-builder.ts
13220
13256
  import { execSync as execSync10 } from "child_process";
@@ -13800,7 +13836,7 @@ import {
13800
13836
  createWriteStream,
13801
13837
  existsSync as existsSync36,
13802
13838
  mkdirSync as mkdirSync22,
13803
- unlinkSync as unlinkSync5,
13839
+ unlinkSync as unlinkSync6,
13804
13840
  writeFileSync as writeFileSync29
13805
13841
  } from "fs";
13806
13842
  import { homedir as homedir4 } from "os";
@@ -13868,7 +13904,7 @@ async function downloadAndExtractRunner(installDir, os, arch) {
13868
13904
  );
13869
13905
  }
13870
13906
  try {
13871
- unlinkSync5(tarball);
13907
+ unlinkSync6(tarball);
13872
13908
  } catch {
13873
13909
  }
13874
13910
  if (os === "darwin") {
@@ -14106,7 +14142,7 @@ import {
14106
14142
  existsSync as existsSync37,
14107
14143
  mkdirSync as mkdirSync23,
14108
14144
  readFileSync as readFileSync34,
14109
- unlinkSync as unlinkSync6,
14145
+ unlinkSync as unlinkSync7,
14110
14146
  writeFileSync as writeFileSync30
14111
14147
  } from "fs";
14112
14148
  import { dirname as dirname9, join as join36 } from "path";
@@ -14198,7 +14234,7 @@ async function restoreWorkflows(projectDir) {
14198
14234
  jobCount: fileState.originals.length
14199
14235
  });
14200
14236
  }
14201
- unlinkSync6(statePath);
14237
+ unlinkSync7(statePath);
14202
14238
  return { nothingToRestore: false, files: resultFiles };
14203
14239
  }
14204
14240
 
@@ -14217,7 +14253,7 @@ function resolveProjectName(projectDir) {
14217
14253
  } catch {
14218
14254
  }
14219
14255
  }
14220
- return basename7(resolve20(projectDir));
14256
+ return basename8(resolve20(projectDir));
14221
14257
  }
14222
14258
  async function runnerSetupAction(opts) {
14223
14259
  if (opts.service !== void 0 && !opts.native) {
@@ -14593,7 +14629,7 @@ runnerCommand.command("build-image").description(
14593
14629
  init_engine();
14594
14630
  import { Command as Command24 } from "commander";
14595
14631
  import { existsSync as existsSync39, mkdirSync as mkdirSync24, readFileSync as readFileSync35, renameSync as renameSync3 } from "fs";
14596
- import { join as join38, resolve as resolve21, basename as basename8 } from "path";
14632
+ import { join as join38, resolve as resolve21, basename as basename9 } from "path";
14597
14633
  import { execSync as execSync13 } from "child_process";
14598
14634
  var DEFAULT_MAX_PROJECTS = 5;
14599
14635
  var MIN_DISK_WARNING_GB = 10;
@@ -14621,7 +14657,7 @@ function getFreeDiskGB() {
14621
14657
  function projectAddAction(factoryDir, projectPath, opts) {
14622
14658
  const resolvedPath = resolve21(projectPath);
14623
14659
  if (!existsSync39(resolvedPath)) throw new Error(`Directory not found: ${resolvedPath}`);
14624
- const projectName = opts.name || basename8(resolvedPath);
14660
+ const projectName = opts.name || basename9(resolvedPath);
14625
14661
  const projectsDir = join38(factoryDir, ".beastmode", "projects");
14626
14662
  if (existsSync39(join38(projectsDir, projectName, "project.json"))) {
14627
14663
  throw new Error(`Project already exists: ${projectName}`);
@@ -14652,9 +14688,15 @@ function projectAddAction(factoryDir, projectPath, opts) {
14652
14688
  if (typeof port === "number" && port >= verifyPort) verifyPort = port + 1;
14653
14689
  }
14654
14690
  const boardId = opts.boardId ? parseInt(opts.boardId, 10) : null;
14655
- const record = createProjectRecord({ name: projectName, resolvedPath, boardId, verifyPort, gitRemote });
14656
- if (detectedStack) {
14657
- record.stack = {
14691
+ const record = createProjectRecord({
14692
+ name: projectName,
14693
+ resolvedPath,
14694
+ boardId,
14695
+ verifyPort,
14696
+ gitRemote,
14697
+ plugins: detectedStack?.suggested_plugins || [],
14698
+ deployTarget: detectedStack?.suggested_deploy || "pr-only",
14699
+ stack: detectedStack ? {
14658
14700
  detected: detectedStack.framework,
14659
14701
  build_command: detectedStack.suggested_commands.build,
14660
14702
  dev_command: detectedStack.suggested_commands.dev,
@@ -14665,8 +14707,8 @@ function projectAddAction(factoryDir, projectPath, opts) {
14665
14707
  total_packages: detectedStack.total_packages,
14666
14708
  primary_languages: detectedStack.primary_languages,
14667
14709
  ...detectedStack.packages ? { packages: detectedStack.packages } : {}
14668
- };
14669
- }
14710
+ } : void 0
14711
+ });
14670
14712
  writeProjectRecord(projectsDir, projectName, record);
14671
14713
  const runsDir = join38(factoryDir, "runs", projectName);
14672
14714
  if (!existsSync39(runsDir)) mkdirSync24(runsDir, { recursive: true });
@@ -14712,7 +14754,7 @@ var projectCommand = new Command24("project").description("Manage projects in th
14712
14754
  projectCommand.command("add <path>").description("Register a project").option("--name <name>", "Override project name").option("--board-id <id>", "Link to existing board ID").action((path, opts) => {
14713
14755
  const factoryDir = resolve21(".");
14714
14756
  projectAddAction(factoryDir, path, opts);
14715
- console.log(`Project registered: ${opts.name || basename8(resolve21(path))}`);
14757
+ console.log(`Project registered: ${opts.name || basename9(resolve21(path))}`);
14716
14758
  });
14717
14759
  projectCommand.command("list").description("List registered projects").action(() => {
14718
14760
  const projects = projectListAction(resolve21("."));