@beastmode-develeap/beastmode 0.1.0 → 0.1.2

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
@@ -37,6 +37,7 @@ var init_schemas = __esm({
37
37
  prod_verification: StageConfigSchema.default({ enabled: false })
38
38
  }).default({}),
39
39
  satisfaction_threshold: z.number().min(0).max(1).default(0.85),
40
+ prod_accept_floor: z.number().min(0).max(1).default(0.65),
40
41
  max_iterations: z.number().int().min(1).max(20).default(5),
41
42
  parallel_coders: z.number().int().min(1).max(10).default(2),
42
43
  fail_fast_threshold: z.number().min(0).max(1).default(0.3)
@@ -2420,6 +2421,7 @@ function mapDaemonToFactory(daemon) {
2420
2421
  const factoryRaw = {
2421
2422
  pipeline: {
2422
2423
  satisfaction_threshold: daemon.verification?.satisfaction_threshold ?? 0.85,
2424
+ prod_accept_floor: daemon.verification?.prod_accept_floor ?? 0.65,
2423
2425
  max_iterations: daemon.convergence?.max_iterations ?? 5,
2424
2426
  fail_fast_threshold: daemon.convergence?.fail_fast_threshold ?? 0.3
2425
2427
  },
@@ -2594,6 +2596,13 @@ function generateMigration(factoryName, daemon, activeState) {
2594
2596
  value: factory.pipeline.satisfaction_threshold
2595
2597
  });
2596
2598
  }
2599
+ if (daemon.verification?.prod_accept_floor !== void 0) {
2600
+ configMappings.push({
2601
+ from: "verification.prod_accept_floor",
2602
+ to: "pipeline.prod_accept_floor",
2603
+ value: factory.pipeline.prod_accept_floor
2604
+ });
2605
+ }
2597
2606
  if (daemon.convergence?.max_iterations !== void 0) {
2598
2607
  configMappings.push({
2599
2608
  from: "convergence.max_iterations",
@@ -2714,7 +2723,7 @@ function generateDaemonConfig(factoryConfig, projectConfig, factoryPath) {
2714
2723
  enabled: true,
2715
2724
  satisfaction_threshold: pipeline.satisfaction_threshold,
2716
2725
  timeout_minutes: 30,
2717
- prod_accept_floor: 0.65
2726
+ prod_accept_floor: pipeline.prod_accept_floor
2718
2727
  },
2719
2728
  review: {
2720
2729
  enabled: humanGates.pr_review !== "disabled",
@@ -4474,6 +4483,11 @@ import { join as join13, basename as basename3, resolve as resolve4, dirname as
4474
4483
  import { homedir } from "os";
4475
4484
  import { execSync as execSync3, spawnSync } from "child_process";
4476
4485
  import http2 from "http";
4486
+ function assertSafeName(name) {
4487
+ if (!name || /[/\\]/.test(name) || name === "." || name === "..") {
4488
+ throw new Error(`Invalid name: ${name}`);
4489
+ }
4490
+ }
4477
4491
  function getBoardUrl2(factoryDir) {
4478
4492
  if (process.env.BEASTMODE_BOARD_URL) return process.env.BEASTMODE_BOARD_URL;
4479
4493
  const configPath = join13(factoryDir, ".beastmode", "config.json");
@@ -4562,6 +4576,69 @@ function getRunsDir(factoryDir) {
4562
4576
  }
4563
4577
  return join13(factoryDir, "runs");
4564
4578
  }
4579
+ function scanStrandedRuns(runsDir, newThreshold) {
4580
+ if (!existsSync15(runsDir)) return [];
4581
+ const stranded = [];
4582
+ const walkRun = (runPath, projectId) => {
4583
+ const ckptPath = join13(runPath, "checkpoint.json");
4584
+ if (!existsSync15(ckptPath)) return;
4585
+ let ckpt;
4586
+ try {
4587
+ ckpt = JSON.parse(readFileSync12(ckptPath, "utf-8"));
4588
+ } catch {
4589
+ return;
4590
+ }
4591
+ const stage = String(ckpt.current_stage || "");
4592
+ if (_TERMINAL_STAGES.has(stage)) return;
4593
+ const bestSat = ckpt.best_satisfaction;
4594
+ if (typeof bestSat !== "number") return;
4595
+ if (bestSat >= newThreshold) return;
4596
+ stranded.push({
4597
+ run_id: String(ckpt.run_id || ""),
4598
+ item_id: typeof ckpt.item_id === "string" || typeof ckpt.item_id === "number" ? String(ckpt.item_id) : null,
4599
+ best_satisfaction: bestSat,
4600
+ project_id: projectId,
4601
+ current_stage: stage
4602
+ });
4603
+ };
4604
+ try {
4605
+ const entries = readdirSync6(runsDir);
4606
+ for (const entry of entries) {
4607
+ const entryPath = join13(runsDir, entry);
4608
+ let stat;
4609
+ try {
4610
+ stat = statSync5(entryPath);
4611
+ } catch {
4612
+ continue;
4613
+ }
4614
+ if (!stat.isDirectory()) continue;
4615
+ if (entry.startsWith("run-")) {
4616
+ walkRun(entryPath, null);
4617
+ } else if (!entry.startsWith(".")) {
4618
+ let subEntries;
4619
+ try {
4620
+ subEntries = readdirSync6(entryPath);
4621
+ } catch {
4622
+ continue;
4623
+ }
4624
+ for (const sub of subEntries) {
4625
+ if (!sub.startsWith("run-")) continue;
4626
+ const subPath = join13(entryPath, sub);
4627
+ try {
4628
+ if (statSync5(subPath).isDirectory()) {
4629
+ walkRun(subPath, entry);
4630
+ }
4631
+ } catch {
4632
+ continue;
4633
+ }
4634
+ }
4635
+ }
4636
+ }
4637
+ } catch {
4638
+ return stranded;
4639
+ }
4640
+ return stranded;
4641
+ }
4565
4642
  function getBoardRoutes(factoryDir) {
4566
4643
  return [
4567
4644
  // ── Status ──
@@ -4778,10 +4855,14 @@ function getBoardRoutes(factoryDir) {
4778
4855
  const boardUrl = getBoardUrl2(factoryDir);
4779
4856
  const bq = scopedQuery(query);
4780
4857
  const poll = await proxyToBoard(boardUrl, "GET", "/api/poll", void 0, bq);
4858
+ const seen = /* @__PURE__ */ new Set();
4781
4859
  const allItems = [];
4782
4860
  for (const [status, items] of Object.entries(poll)) {
4783
4861
  if (Array.isArray(items)) {
4784
4862
  for (const item of items) {
4863
+ const id = String(item.id ?? "");
4864
+ if (id && seen.has(id)) continue;
4865
+ if (id) seen.add(id);
4785
4866
  allItems.push({ ...item, status_bucket: status });
4786
4867
  }
4787
4868
  }
@@ -5041,6 +5122,7 @@ function getBoardRoutes(factoryDir) {
5041
5122
  pattern: "/api/projects/:name",
5042
5123
  handler: (_body, params) => {
5043
5124
  const { name } = params;
5125
+ assertSafeName(name);
5044
5126
  const subDirPath = join13(factoryDir, ".beastmode", "projects", name, "project.json");
5045
5127
  const flatPath = join13(factoryDir, ".beastmode", "projects", `${name}.json`);
5046
5128
  const filePath = existsSync15(subDirPath) ? subDirPath : flatPath;
@@ -5053,6 +5135,7 @@ function getBoardRoutes(factoryDir) {
5053
5135
  pattern: "/api/projects/:name/extensions",
5054
5136
  handler: (_body, params) => {
5055
5137
  const { name } = params;
5138
+ assertSafeName(name);
5056
5139
  const extPath = join13(factoryDir, ".beastmode", "projects", name, "extensions.json");
5057
5140
  if (!existsSync15(extPath)) {
5058
5141
  return { plugins: { add: [], remove: [] }, mcps: { add: {}, remove: [] }, skills: { add: [], remove: [] } };
@@ -5198,6 +5281,7 @@ function getBoardRoutes(factoryDir) {
5198
5281
  pattern: "/api/projects/:name",
5199
5282
  handler: (_body, params) => {
5200
5283
  const { name } = params;
5284
+ assertSafeName(name);
5201
5285
  const projectsDir = join13(factoryDir, ".beastmode", "projects");
5202
5286
  const subDir = join13(projectsDir, name);
5203
5287
  const flatPath = join13(projectsDir, `${name}.json`);
@@ -5252,28 +5336,60 @@ Path: ${projConfig.path}
5252
5336
  handler: () => {
5253
5337
  const runsDir = getRunsDir(factoryDir);
5254
5338
  if (!existsSync15(runsDir)) return { runs: [] };
5255
- const runs = readdirSync6(runsDir).filter((d) => {
5256
- if (d.startsWith(".")) return false;
5339
+ const runEntries = [];
5340
+ for (const entry of readdirSync6(runsDir)) {
5341
+ if (entry.startsWith(".")) continue;
5342
+ const entryPath = join13(runsDir, entry);
5257
5343
  try {
5258
- const stat = readdirSync6(join13(runsDir, d));
5259
- return stat.length > 0;
5344
+ const children = readdirSync6(entryPath);
5345
+ if (entry.startsWith("run-")) {
5346
+ if (children.length > 0) {
5347
+ runEntries.push({ id: entry, dir: entryPath, projectId: "" });
5348
+ }
5349
+ } else {
5350
+ for (const child of children) {
5351
+ if (!child.startsWith("run-") || child.startsWith(".")) continue;
5352
+ const childPath = join13(entryPath, child);
5353
+ try {
5354
+ const grandchildren = readdirSync6(childPath);
5355
+ if (grandchildren.length > 0) {
5356
+ runEntries.push({ id: child, dir: childPath, projectId: entry });
5357
+ }
5358
+ } catch {
5359
+ }
5360
+ }
5361
+ }
5260
5362
  } catch {
5261
- return false;
5363
+ continue;
5262
5364
  }
5263
- }).sort().reverse().map((id) => {
5264
- const manifest = readJsonFile(join13(runsDir, id, "manifest.json"));
5265
- const checkpoint = readJsonFile(join13(runsDir, id, "checkpoint.json"));
5365
+ }
5366
+ const deduped = /* @__PURE__ */ new Map();
5367
+ for (const entry of runEntries) {
5368
+ const existing = deduped.get(entry.id);
5369
+ if (!existing || entry.projectId && !existing.projectId) {
5370
+ deduped.set(entry.id, entry);
5371
+ }
5372
+ }
5373
+ const runs = Array.from(deduped.values()).sort((a, b) => b.id.localeCompare(a.id)).map(({ id, dir, projectId }) => {
5374
+ const manifest = readJsonFile(join13(dir, "manifest.json"));
5375
+ const checkpoint = readJsonFile(join13(dir, "checkpoint.json"));
5376
+ const prodVerif = readJsonFile(join13(dir, "prod-verification.json"));
5266
5377
  const manifestData = manifest;
5378
+ const cpData = checkpoint;
5379
+ const prodAcceptFloor = 0.65;
5380
+ const prodSat = prodVerif?.satisfaction;
5381
+ const prodVerified = typeof prodSat === "number" && prodSat >= prodAcceptFloor || cpData?.current_stage === "done" || cpData?.current_stage === "shipped";
5267
5382
  return {
5268
5383
  id,
5269
- taskName: manifestData?.task_description || manifestData?.item_name || manifestData?.task_name || id,
5270
- projectId: manifestData?.project_id || "",
5384
+ taskName: manifestData?.description || manifestData?.task_description || manifestData?.item_name || manifestData?.task_name || id,
5385
+ projectId: projectId || manifestData?.project_id || "",
5271
5386
  itemId: manifestData?.item_id || manifestData?.monday_item_id || "",
5272
5387
  taskType: manifestData?.task_type || "",
5273
5388
  createdAt: manifestData?.created_at || "",
5274
5389
  manifest: manifest || {},
5275
5390
  checkpoint: checkpoint || {},
5276
- pinned: isRunPinned(runsDir, id)
5391
+ pinned: isRunPinned(runsDir, id),
5392
+ prodVerified
5277
5393
  };
5278
5394
  });
5279
5395
  return { runs };
@@ -5282,9 +5398,20 @@ Path: ${projConfig.path}
5282
5398
  {
5283
5399
  method: "GET",
5284
5400
  pattern: "/api/runs/:id",
5285
- handler: (_body, params) => {
5401
+ handler: (_body, params, query) => {
5286
5402
  const { id } = params;
5287
- const runDir = join13(getRunsDir(factoryDir), id);
5403
+ const runsDir = getRunsDir(factoryDir);
5404
+ let runDir = join13(runsDir, id);
5405
+ if (!existsSync15(runDir)) {
5406
+ for (const proj of readdirSync6(runsDir)) {
5407
+ if (proj.startsWith(".") || proj.startsWith("run-")) continue;
5408
+ const candidate = join13(runsDir, proj, id);
5409
+ if (existsSync15(candidate)) {
5410
+ runDir = candidate;
5411
+ break;
5412
+ }
5413
+ }
5414
+ }
5288
5415
  if (!existsSync15(runDir)) throw new Error(`Run not found: ${id}`);
5289
5416
  const manifest = readJsonFile(join13(runDir, "manifest.json"));
5290
5417
  const checkpoint = readJsonFile(join13(runDir, "checkpoint.json"));
@@ -5305,7 +5432,7 @@ Path: ${projConfig.path}
5305
5432
  const manifestData = manifest;
5306
5433
  return {
5307
5434
  id,
5308
- taskName: manifestData?.task_description || manifestData?.item_name || manifestData?.task_name || id,
5435
+ taskName: manifestData?.description || manifestData?.task_description || manifestData?.item_name || manifestData?.task_name || id,
5309
5436
  projectId: manifestData?.project_id || "",
5310
5437
  itemId: manifestData?.item_id || manifestData?.monday_item_id || "",
5311
5438
  taskType: manifestData?.task_type || "",
@@ -5367,15 +5494,64 @@ Path: ${projConfig.path}
5367
5494
  if (!updates || typeof updates !== "object") {
5368
5495
  throw new Error("Request body must be a JSON object");
5369
5496
  }
5497
+ const force = Boolean(
5498
+ updates.force || updates._force
5499
+ );
5500
+ const updatesClean = { ...updates };
5501
+ delete updatesClean.force;
5502
+ delete updatesClean._force;
5370
5503
  const configPath = join13(factoryDir, ".beastmode", "config.json");
5371
5504
  const current = existsSync15(configPath) ? JSON.parse(readFileSync12(configPath, "utf-8")) : generateDefaults();
5372
- const merged = deepMerge2(current, updates);
5505
+ if (!force) {
5506
+ const oldPipeline = current.pipeline || {};
5507
+ const newPipeline = updatesClean.pipeline || {};
5508
+ const oldSat = typeof oldPipeline.satisfaction_threshold === "number" ? oldPipeline.satisfaction_threshold : null;
5509
+ const newSat = typeof newPipeline.satisfaction_threshold === "number" ? newPipeline.satisfaction_threshold : null;
5510
+ const oldFloor = typeof oldPipeline.prod_accept_floor === "number" ? oldPipeline.prod_accept_floor : null;
5511
+ const newFloor = typeof newPipeline.prod_accept_floor === "number" ? newPipeline.prod_accept_floor : null;
5512
+ const satRaised = oldSat !== null && newSat !== null && newSat > oldSat;
5513
+ const floorRaised = oldFloor !== null && newFloor !== null && newFloor > oldFloor;
5514
+ if (satRaised || floorRaised) {
5515
+ const candidates = [];
5516
+ if (satRaised && newSat !== null) candidates.push(newSat);
5517
+ if (floorRaised && newFloor !== null) candidates.push(newFloor);
5518
+ const effectiveThreshold = Math.max(...candidates);
5519
+ const stranded = scanStrandedRuns(
5520
+ getRunsDir(factoryDir),
5521
+ effectiveThreshold
5522
+ );
5523
+ if (stranded.length > 0) {
5524
+ return {
5525
+ warning: `${stranded.length} active run(s) cannot converge at the new threshold.`,
5526
+ stranded_runs: stranded,
5527
+ confirm_required: true,
5528
+ attempted_threshold: effectiveThreshold,
5529
+ old_threshold: satRaised ? oldSat : oldFloor
5530
+ };
5531
+ }
5532
+ }
5533
+ }
5534
+ const merged = deepMerge2(current, updatesClean);
5373
5535
  writeFileSync12(configPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
5374
- const daemonConfigPath = join13(factoryDir, "config", "beastmode.daemon.json");
5375
- if (existsSync15(daemonConfigPath)) {
5536
+ const daemonConfigPaths = [
5537
+ join13(factoryDir, "config", "beastmode.daemon.json"),
5538
+ join13(factoryDir, "config", "beastmode.docker.json")
5539
+ ].filter(existsSync15);
5540
+ const pipelineToDaemonMap = {
5541
+ satisfaction_threshold: ["verification", "satisfaction_threshold"],
5542
+ prod_accept_floor: ["verification", "prod_accept_floor"],
5543
+ max_iterations: ["convergence", "max_iterations"],
5544
+ fail_fast_threshold: ["convergence", "fail_fast_threshold"]
5545
+ };
5546
+ const daemonFields = ["max_slots", "max_projects", "poll_interval_seconds", "stale_task_hours", "runs_path"];
5547
+ const flatDictSections = [
5548
+ "models",
5549
+ "migration_safety",
5550
+ "alerts"
5551
+ ];
5552
+ for (const daemonConfigPath of daemonConfigPaths) {
5376
5553
  try {
5377
5554
  const daemonConfig = JSON.parse(readFileSync12(daemonConfigPath, "utf-8"));
5378
- const daemonFields = ["max_slots", "max_projects", "poll_interval_seconds", "stale_task_hours", "runs_path"];
5379
5555
  let changed = false;
5380
5556
  for (const field of daemonFields) {
5381
5557
  if (field in merged && merged[field] !== daemonConfig[field]) {
@@ -5384,38 +5560,33 @@ Path: ${projConfig.path}
5384
5560
  }
5385
5561
  }
5386
5562
  if (merged.pipeline && typeof merged.pipeline === "object") {
5387
- if (!daemonConfig.pipeline) daemonConfig.pipeline = {};
5388
- for (const [k, v] of Object.entries(merged.pipeline)) {
5389
- if (daemonConfig.pipeline[k] !== v) {
5390
- daemonConfig.pipeline[k] = v;
5391
- changed = true;
5563
+ const mergedPipeline = merged.pipeline;
5564
+ for (const [srcKey, [section, destKey]] of Object.entries(pipelineToDaemonMap)) {
5565
+ if (!(srcKey in mergedPipeline)) continue;
5566
+ const val = mergedPipeline[srcKey];
5567
+ if (val === void 0) continue;
5568
+ if (!daemonConfig[section] || typeof daemonConfig[section] !== "object") {
5569
+ daemonConfig[section] = {};
5392
5570
  }
5393
- }
5394
- }
5395
- if (merged.models && typeof merged.models === "object") {
5396
- if (!daemonConfig.models) daemonConfig.models = {};
5397
- for (const [k, v] of Object.entries(merged.models)) {
5398
- if (daemonConfig.models[k] !== v) {
5399
- daemonConfig.models[k] = v;
5571
+ const bucket = daemonConfig[section];
5572
+ if (bucket[destKey] !== val) {
5573
+ bucket[destKey] = val;
5400
5574
  changed = true;
5401
5575
  }
5402
5576
  }
5403
5577
  }
5404
- if (merged.migration_safety && typeof merged.migration_safety === "object") {
5405
- if (!daemonConfig.migration_safety) daemonConfig.migration_safety = {};
5406
- for (const [k, v] of Object.entries(merged.migration_safety)) {
5407
- if (daemonConfig.migration_safety[k] !== v) {
5408
- daemonConfig.migration_safety[k] = v;
5409
- changed = true;
5578
+ for (const section of flatDictSections) {
5579
+ const src = merged[section];
5580
+ if (src && typeof src === "object") {
5581
+ if (!daemonConfig[section] || typeof daemonConfig[section] !== "object") {
5582
+ daemonConfig[section] = {};
5410
5583
  }
5411
- }
5412
- }
5413
- if (merged.alerts && typeof merged.alerts === "object") {
5414
- if (!daemonConfig.alerts) daemonConfig.alerts = {};
5415
- for (const [k, v] of Object.entries(merged.alerts)) {
5416
- if (daemonConfig.alerts[k] !== v) {
5417
- daemonConfig.alerts[k] = v;
5418
- changed = true;
5584
+ const bucket = daemonConfig[section];
5585
+ for (const [k, v] of Object.entries(src)) {
5586
+ if (bucket[k] !== v) {
5587
+ bucket[k] = v;
5588
+ changed = true;
5589
+ }
5419
5590
  }
5420
5591
  }
5421
5592
  }
@@ -5727,12 +5898,21 @@ function matchBoardRoute(routes, method, url) {
5727
5898
  }
5728
5899
  return null;
5729
5900
  }
5901
+ var _TERMINAL_STAGES;
5730
5902
  var init_board_api_routes = __esm({
5731
5903
  "src/cli/ui/board-api-routes.ts"() {
5732
5904
  "use strict";
5733
5905
  init_archival();
5734
5906
  init_chat_handler();
5735
5907
  init_engine();
5908
+ _TERMINAL_STAGES = /* @__PURE__ */ new Set([
5909
+ "done",
5910
+ "shipped",
5911
+ "stuck",
5912
+ "stuck_oscillating",
5913
+ "stuck_infra_gap",
5914
+ "ready_for_review"
5915
+ ]);
5736
5916
  }
5737
5917
  });
5738
5918
 
@@ -6725,6 +6905,7 @@ function generateComposeYaml(tag) {
6725
6905
  return `services:
6726
6906
  # Python board API \u2014 data layer (SQLite + WebSocket). Internal only.
6727
6907
  board:
6908
+ platform: linux/amd64
6728
6909
  image: ${GHCR_IMAGE_PREFIX}/board:${tag}
6729
6910
  expose:
6730
6911
  - "8080"
@@ -6741,6 +6922,7 @@ function generateComposeYaml(tag) {
6741
6922
  # Node.js UI server \u2014 board UI with password auth.
6742
6923
  # Access at http://localhost:8420
6743
6924
  ui:
6925
+ platform: linux/amd64
6744
6926
  image: ${GHCR_IMAGE_PREFIX}/ui:${tag}
6745
6927
  ports:
6746
6928
  - "\${UI_PORT:-8420}:8080"
@@ -6752,6 +6934,9 @@ function generateComposeYaml(tag) {
6752
6934
  - BEASTMODE_UI_PASSWORD=\${BEASTMODE_UI_PASSWORD:-}
6753
6935
  volumes:
6754
6936
  - ./.beastmode:/app/.beastmode
6937
+ # Daemon config files. The Settings UI writes pipeline.* changes here
6938
+ # so they propagate to the daemon container on next restart.
6939
+ - ./config:/app/config
6755
6940
  - ./runs:/app/runs
6756
6941
  - ./daemon/logs:/app/daemon/logs:ro
6757
6942
  - \${HOME}/.claude:/root/.claude:ro
@@ -6762,6 +6947,7 @@ function generateComposeYaml(tag) {
6762
6947
 
6763
6948
  # Pipeline daemon \u2014 polls board, runs tasks
6764
6949
  daemon:
6950
+ platform: linux/amd64
6765
6951
  image: ${GHCR_IMAGE_PREFIX}/daemon:${tag}
6766
6952
  env_file:
6767
6953
  - path: .env
@@ -6772,6 +6958,10 @@ function generateComposeYaml(tag) {
6772
6958
  - BEASTMODE_ROOT=/app
6773
6959
  - BEASTMODE_BOARD_URL=http://board:8080
6774
6960
  volumes:
6961
+ # Daemon config files. Mounted from the host so Settings UI writes
6962
+ # (to beastmode.docker.json) are picked up on daemon restart without
6963
+ # rebuilding the image.
6964
+ - ./config:/app/config
6775
6965
  - ./runs:/app/runs
6776
6966
  - ./daemon/logs:/app/daemon/logs
6777
6967
  - /var/run/docker.sock:/var/run/docker.sock