@beastmode-develeap/beastmode 0.1.264 → 0.1.265

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
@@ -4560,6 +4560,7 @@ var init_chat_handler = __esm({
4560
4560
  import { readFileSync as readFileSync12, writeFileSync as writeFileSync12, existsSync as existsSync15, readdirSync as readdirSync6, unlinkSync as unlinkSync3, mkdirSync as mkdirSync11, statSync as statSync5, rmSync as rmSync3 } from "fs";
4561
4561
  import { join as join13, basename as basename3, resolve as resolve4, dirname as dirname5 } from "path";
4562
4562
  import { homedir } from "os";
4563
+ import { randomUUID as randomUUID2 } from "crypto";
4563
4564
  import { execSync as execSync3, spawnSync } from "child_process";
4564
4565
  import http2 from "http";
4565
4566
  function assertSafeName(name) {
@@ -4806,6 +4807,30 @@ function scanStrandedRuns(runsDir, newThreshold) {
4806
4807
  }
4807
4808
  return stranded;
4808
4809
  }
4810
+ function _readAnalyzeMeta(projectsDir, name) {
4811
+ const metaPath = join13(projectsDir, name, "codebase-guide.meta.json");
4812
+ if (!existsSync15(metaPath)) return null;
4813
+ try {
4814
+ return JSON.parse(readFileSync12(metaPath, "utf-8"));
4815
+ } catch {
4816
+ return null;
4817
+ }
4818
+ }
4819
+ function _gitHeadSha(repoPath) {
4820
+ try {
4821
+ return execSync3("git rev-parse HEAD", { cwd: repoPath, encoding: "utf-8" }).trim();
4822
+ } catch {
4823
+ return "no-git";
4824
+ }
4825
+ }
4826
+ function _hasClaudeCli() {
4827
+ try {
4828
+ execSync3("which claude", { encoding: "utf-8" });
4829
+ return true;
4830
+ } catch {
4831
+ return false;
4832
+ }
4833
+ }
4809
4834
  function getBoardRoutes(factoryDir) {
4810
4835
  return [
4811
4836
  // ── Status ──
@@ -5607,36 +5632,270 @@ function getBoardRoutes(factoryDir) {
5607
5632
  {
5608
5633
  method: "POST",
5609
5634
  pattern: "/api/projects/:name/analyze",
5610
- handler: async (_body, params) => {
5635
+ handler: async (body, params, query) => {
5611
5636
  const { name } = params;
5637
+ assertSafeName(name);
5612
5638
  const projectsDir = join13(factoryDir, ".beastmode", "projects");
5613
5639
  const subDirConfigPath = join13(projectsDir, name, "project.json");
5614
5640
  const flatConfigPath = join13(projectsDir, `${name}.json`);
5615
5641
  const projPath = existsSync15(subDirConfigPath) ? subDirConfigPath : existsSync15(flatConfigPath) ? flatConfigPath : null;
5616
5642
  if (!projPath) throw new Error(`Project not found: ${name}`);
5617
5643
  const projConfig = JSON.parse(readFileSync12(projPath, "utf-8"));
5644
+ const projectPath = projConfig.path;
5618
5645
  const subDir = join13(projectsDir, name);
5619
5646
  if (!existsSync15(subDir)) mkdirSync11(subDir, { recursive: true });
5620
- const brownfieldPath = join13(subDir, "brownfield.md");
5621
- if (!existsSync15(brownfieldPath)) {
5622
- const { detectStack: detectStackFn } = await Promise.resolve().then(() => (init_engine(), engine_exports));
5623
- let stackInfo = "Unknown stack";
5624
- try {
5625
- const stack = detectStackFn(projConfig.path);
5626
- const cmds = stack.suggested_commands;
5627
- stackInfo = `Framework: ${stack.framework || "unknown"}
5647
+ const force = query?.force === "true" || query?.force === "1";
5648
+ const tier = body?.tier;
5649
+ if (tier === "quick") {
5650
+ const brownfieldPath = join13(subDir, "brownfield.md");
5651
+ if (!existsSync15(brownfieldPath)) {
5652
+ const { detectStack: detectStackFn } = await Promise.resolve().then(() => (init_engine(), engine_exports));
5653
+ let stackInfo = "Unknown stack";
5654
+ try {
5655
+ const stack = detectStackFn(projectPath);
5656
+ const cmds = stack.suggested_commands;
5657
+ stackInfo = `Framework: ${stack.framework || "unknown"}
5628
5658
  Build: ${cmds?.build || "unknown"}
5629
5659
  Dev: ${cmds?.dev || "unknown"}`;
5630
- } catch {
5660
+ } catch {
5661
+ }
5662
+ writeFileSync12(brownfieldPath, `# Brownfield Analysis: ${name}
5663
+
5664
+ ${stackInfo}
5665
+
5666
+ Path: ${projectPath}
5667
+ `);
5631
5668
  }
5632
- writeFileSync12(brownfieldPath, `# Brownfield Analysis: ${name}
5669
+ return { analyzed: true, project: name, tier: "quick" };
5670
+ }
5671
+ if (!force) {
5672
+ const meta = _readAnalyzeMeta(projectsDir, name);
5673
+ const currentSha = _gitHeadSha(projectPath);
5674
+ if (meta && meta.status === "complete" && meta.target_repo_head_sha === currentSha) {
5675
+ return { analyzed: true, project: name, cached: true, analyzed_at: meta.analyzed_at };
5676
+ }
5677
+ }
5678
+ const existingJob = _analyzeJobs.get(name);
5679
+ if (existingJob && existingJob.status === "running") {
5680
+ return { analyzing: true, project: name, job_id: existingJob.job_id, status: "running" };
5681
+ }
5682
+ const writeMeta = (m) => {
5683
+ try {
5684
+ writeFileSync12(
5685
+ join13(subDir, "codebase-guide.meta.json"),
5686
+ JSON.stringify(m, null, 2) + "\n",
5687
+ "utf-8"
5688
+ );
5689
+ } catch (err) {
5690
+ console.error(`[analyze] Failed to write meta for ${name}:`, err);
5691
+ }
5692
+ };
5693
+ if (!_hasClaudeCli()) {
5694
+ const brownfieldPath = join13(subDir, "brownfield.md");
5695
+ if (!existsSync15(brownfieldPath)) {
5696
+ const { detectStack: detectStackFn } = await Promise.resolve().then(() => (init_engine(), engine_exports));
5697
+ let stackInfo = "Unknown stack";
5698
+ try {
5699
+ const stack = detectStackFn(projectPath);
5700
+ const cmds = stack.suggested_commands;
5701
+ stackInfo = `Framework: ${stack.framework || "unknown"}
5702
+ Build: ${cmds?.build || "unknown"}
5703
+ Dev: ${cmds?.dev || "unknown"}`;
5704
+ } catch {
5705
+ }
5706
+ writeFileSync12(brownfieldPath, `# Brownfield Analysis: ${name}
5633
5707
 
5634
5708
  ${stackInfo}
5635
5709
 
5636
- Path: ${projConfig.path}
5710
+ Path: ${projectPath}
5637
5711
  `);
5712
+ }
5713
+ writeMeta({
5714
+ analyzed_at: (/* @__PURE__ */ new Date()).toISOString(),
5715
+ target_repo_head_sha: _gitHeadSha(projectPath),
5716
+ status: "failed",
5717
+ error: "Claude CLI not available",
5718
+ duration_seconds: 0
5719
+ });
5720
+ console.warn(`[analyze] Claude CLI unavailable for project ${name}; wrote detectStack() fallback.`);
5721
+ return { analyzed: true, project: name, fallback: true };
5722
+ }
5723
+ const jobId = randomUUID2();
5724
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
5725
+ const startMs = Date.now();
5726
+ const job = {
5727
+ job_id: jobId,
5728
+ project: name,
5729
+ status: "running",
5730
+ started_at: startedAt
5731
+ };
5732
+ _analyzeJobs.set(name, job);
5733
+ (async () => {
5734
+ try {
5735
+ const { spawn: spawn4 } = await import("child_process");
5736
+ const prompt = [
5737
+ "You are the Brownfield Analyst. Analyze the codebase at the current working directory.",
5738
+ `Write your output in pyramid format (L0/L1/L2) to the directory: ${subDir}`,
5739
+ "Produce exactly these four files:",
5740
+ ` - ${join13(subDir, "codebase-guide.md")} (full L2 analysis: architecture, conventions, safe modification points, fragile areas)`,
5741
+ ` - ${join13(subDir, "codebase-guide.l1.md")} (paragraph summary only)`,
5742
+ ` - ${join13(subDir, "codebase-guide.l0.txt")} (one-sentence summary only)`,
5743
+ "Do not write anything else outside the BEASTMODE_OUTPUT_DIR."
5744
+ ].join("\n");
5745
+ const isRoot = process.getuid?.() === 0;
5746
+ const claudeArgs = [
5747
+ "-p",
5748
+ prompt,
5749
+ "--agent",
5750
+ "brownfield-analyst",
5751
+ "--output-format",
5752
+ "stream-json",
5753
+ "--verbose",
5754
+ "--dangerously-skip-permissions"
5755
+ ];
5756
+ const spawnCmd = isRoot ? "runuser" : "claude";
5757
+ const spawnArgs = isRoot ? ["-u", "node", "--", "claude", ...claudeArgs] : claudeArgs;
5758
+ const child = spawn4(spawnCmd, spawnArgs, {
5759
+ cwd: projectPath,
5760
+ env: {
5761
+ ...process.env,
5762
+ BEASTMODE_PROJECT: name,
5763
+ BEASTMODE_OUTPUT_DIR: subDir,
5764
+ ...isRoot ? { HOME: "/home/node" } : {}
5765
+ },
5766
+ stdio: ["ignore", "pipe", "pipe"]
5767
+ });
5768
+ child.stdout?.on("data", () => {
5769
+ });
5770
+ child.stderr?.on("data", () => {
5771
+ });
5772
+ child.on("close", (code) => {
5773
+ const durationSeconds = (Date.now() - startMs) / 1e3;
5774
+ const sha = _gitHeadSha(projectPath);
5775
+ const guidePath = join13(subDir, "codebase-guide.md");
5776
+ const ok = code === 0 && existsSync15(guidePath);
5777
+ if (ok) {
5778
+ writeMeta({
5779
+ analyzed_at: (/* @__PURE__ */ new Date()).toISOString(),
5780
+ analyst_version: "brownfield-analyst-v1",
5781
+ target_repo_head_sha: sha,
5782
+ project_name: name,
5783
+ duration_seconds: durationSeconds,
5784
+ status: "complete"
5785
+ });
5786
+ const legacy = join13(subDir, "brownfield.md");
5787
+ if (existsSync15(legacy)) {
5788
+ try {
5789
+ unlinkSync3(legacy);
5790
+ } catch {
5791
+ }
5792
+ }
5793
+ _analyzeJobs.set(name, {
5794
+ ...job,
5795
+ status: "complete",
5796
+ completed_at: (/* @__PURE__ */ new Date()).toISOString(),
5797
+ duration_seconds: durationSeconds
5798
+ });
5799
+ } else {
5800
+ const errMsg = `claude exited with code ${code}`;
5801
+ writeMeta({
5802
+ analyzed_at: (/* @__PURE__ */ new Date()).toISOString(),
5803
+ target_repo_head_sha: sha,
5804
+ status: "failed",
5805
+ error: errMsg,
5806
+ duration_seconds: durationSeconds
5807
+ });
5808
+ _analyzeJobs.set(name, {
5809
+ ...job,
5810
+ status: "failed",
5811
+ completed_at: (/* @__PURE__ */ new Date()).toISOString(),
5812
+ duration_seconds: durationSeconds,
5813
+ error: errMsg
5814
+ });
5815
+ }
5816
+ });
5817
+ child.on("error", (err) => {
5818
+ const durationSeconds = (Date.now() - startMs) / 1e3;
5819
+ const sha = _gitHeadSha(projectPath);
5820
+ writeMeta({
5821
+ analyzed_at: (/* @__PURE__ */ new Date()).toISOString(),
5822
+ target_repo_head_sha: sha,
5823
+ status: "failed",
5824
+ error: String(err),
5825
+ duration_seconds: durationSeconds
5826
+ });
5827
+ _analyzeJobs.set(name, {
5828
+ ...job,
5829
+ status: "failed",
5830
+ completed_at: (/* @__PURE__ */ new Date()).toISOString(),
5831
+ duration_seconds: durationSeconds,
5832
+ error: String(err)
5833
+ });
5834
+ });
5835
+ } catch (err) {
5836
+ const durationSeconds = (Date.now() - startMs) / 1e3;
5837
+ const sha = _gitHeadSha(projectPath);
5838
+ writeMeta({
5839
+ analyzed_at: (/* @__PURE__ */ new Date()).toISOString(),
5840
+ target_repo_head_sha: sha,
5841
+ status: "failed",
5842
+ error: String(err),
5843
+ duration_seconds: durationSeconds
5844
+ });
5845
+ _analyzeJobs.set(name, {
5846
+ ...job,
5847
+ status: "failed",
5848
+ completed_at: (/* @__PURE__ */ new Date()).toISOString(),
5849
+ duration_seconds: durationSeconds,
5850
+ error: String(err)
5851
+ });
5852
+ }
5853
+ })();
5854
+ return { analyzing: true, project: name, job_id: jobId, status: "running" };
5855
+ }
5856
+ },
5857
+ {
5858
+ method: "GET",
5859
+ pattern: "/api/projects/:name/analyze/status",
5860
+ handler: (_body, params) => {
5861
+ const { name } = params;
5862
+ assertSafeName(name);
5863
+ const projectsDir = join13(factoryDir, ".beastmode", "projects");
5864
+ const job = _analyzeJobs.get(name);
5865
+ const meta = _readAnalyzeMeta(projectsDir, name);
5866
+ if (job && job.status === "running") {
5867
+ return {
5868
+ status: "running",
5869
+ job_id: job.job_id,
5870
+ started_at: job.started_at
5871
+ };
5638
5872
  }
5639
- return { analyzed: true, project: name };
5873
+ if (job && job.status === "failed") {
5874
+ return {
5875
+ status: "failed",
5876
+ job_id: job.job_id,
5877
+ error: job.error,
5878
+ started_at: job.started_at,
5879
+ completed_at: job.completed_at,
5880
+ duration_seconds: job.duration_seconds
5881
+ };
5882
+ }
5883
+ if (meta && meta.status === "complete") {
5884
+ return {
5885
+ status: job?.status === "cached" ? "cached" : "complete",
5886
+ analyzed_at: meta.analyzed_at,
5887
+ duration_seconds: meta.duration_seconds,
5888
+ target_repo_head_sha: meta.target_repo_head_sha
5889
+ };
5890
+ }
5891
+ if (meta && meta.status === "failed") {
5892
+ return {
5893
+ status: "failed",
5894
+ analyzed_at: meta.analyzed_at,
5895
+ error: meta.error
5896
+ };
5897
+ }
5898
+ return { status: "idle" };
5640
5899
  }
5641
5900
  },
5642
5901
  // ── Runs ──
@@ -6348,7 +6607,7 @@ function matchBoardRoute(routes, method, url) {
6348
6607
  }
6349
6608
  return null;
6350
6609
  }
6351
- var BinaryResponse, HttpError, _TERMINAL_STAGES;
6610
+ var BinaryResponse, HttpError, _TERMINAL_STAGES, _analyzeJobs;
6352
6611
  var init_board_api_routes = __esm({
6353
6612
  "src/cli/ui/board-api-routes.ts"() {
6354
6613
  "use strict";
@@ -6378,6 +6637,7 @@ var init_board_api_routes = __esm({
6378
6637
  "stuck_infra_gap",
6379
6638
  "ready_for_review"
6380
6639
  ]);
6640
+ _analyzeJobs = /* @__PURE__ */ new Map();
6381
6641
  }
6382
6642
  });
6383
6643
 
@@ -7733,7 +7993,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
7733
7993
  import { z as z2 } from "zod";
7734
7994
  import { readFileSync as readFileSync28, writeFileSync as writeFileSync24, existsSync as existsSync31, readdirSync as readdirSync11, mkdirSync as mkdirSync19 } from "fs";
7735
7995
  import { join as join29, resolve as resolve18, basename as basename5 } from "path";
7736
- import { randomUUID as randomUUID3 } from "crypto";
7996
+ import { randomUUID as randomUUID4 } from "crypto";
7737
7997
  function readJsonFile2(filePath) {
7738
7998
  if (!existsSync31(filePath)) return null;
7739
7999
  try {
@@ -7901,7 +8161,7 @@ function createMcpServer() {
7901
8161
  const factoryDir = getFactoryPath();
7902
8162
  const now = (/* @__PURE__ */ new Date()).toISOString();
7903
8163
  const item = {
7904
- id: randomUUID3(),
8164
+ id: randomUUID4(),
7905
8165
  title,
7906
8166
  description: "",
7907
8167
  status: status || "ready",
@@ -8099,7 +8359,7 @@ function createMcpServer() {
8099
8359
  const factoryDir = getFactoryPath();
8100
8360
  const now = (/* @__PURE__ */ new Date()).toISOString();
8101
8361
  const item = {
8102
- id: randomUUID3(),
8362
+ id: randomUUID4(),
8103
8363
  title,
8104
8364
  description: description || "",
8105
8365
  status: status || "new",
@@ -11533,7 +11793,7 @@ init_schemas();
11533
11793
  import { Command as Command15 } from "commander";
11534
11794
  import { join as join27 } from "path";
11535
11795
  import { existsSync as existsSync29, readFileSync as readFileSync26, writeFileSync as writeFileSync22, mkdirSync as mkdirSync17 } from "fs";
11536
- import { randomUUID as randomUUID2 } from "crypto";
11796
+ import { randomUUID as randomUUID3 } from "crypto";
11537
11797
  var runCommand = new Command15("run").description("Run a single pipeline task").argument("[project]", "Project name (defaults to first project)").option("--task <description>", "Task description").action(async (project, opts) => {
11538
11798
  try {
11539
11799
  await runPipeline(project, opts);
@@ -11596,7 +11856,7 @@ async function runPipeline(projectName, opts) {
11596
11856
  }
11597
11857
  }
11598
11858
  const now = (/* @__PURE__ */ new Date()).toISOString();
11599
- const taskId = randomUUID2();
11859
+ const taskId = randomUUID3();
11600
11860
  const task = {
11601
11861
  id: taskId,
11602
11862
  title: opts.task,