substrate-ai 0.6.0 → 0.6.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/cli/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { AdapterTelemetryPersistence, AppError, DEFAULT_CONFIG, DEFAULT_ROUTING_POLICY, DoltClient, DoltNotInstalled, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, FileStateStore, GitClient, GrammarLoader, IngestionServer, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SUBSTRATE_OWNED_SETTINGS_KEYS, SymbolParser, VALID_PHASES, WorkGraphRepository, buildPipelineStatusOutput, checkDoltInstalled, createConfigSystem, createContextCompiler, createDatabaseAdapter, createDispatcher, createDoltClient, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStateStore, createStopAfterGate, createTelemetryAdvisor, detectCycles, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, initSchema, initializeDolt, isSyncAdapter, parseDbTimestampAsUtc, registerHealthCommand, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-B1WEe6SY.js";
2
+ import { AdapterTelemetryPersistence, AppError, DEFAULT_CONFIG, DEFAULT_ROUTING_POLICY, DoltClient, DoltNotInstalled, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, FileStateStore, GitClient, GrammarLoader, IngestionServer, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SUBSTRATE_OWNED_SETTINGS_KEYS, SymbolParser, VALID_PHASES, WorkGraphRepository, buildPipelineStatusOutput, checkDoltInstalled, createConfigSystem, createContextCompiler, createDatabaseAdapter, createDispatcher, createDoltClient, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStateStore, createStopAfterGate, createTelemetryAdvisor, detectCycles, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, initSchema, initializeDolt, isSyncAdapter, parseDbTimestampAsUtc, registerHealthCommand, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runSolutioningPhase, validateStopAfterFromConflict } from "../run-DCmne2q6.js";
3
3
  import { createLogger } from "../logger-D2fS2ccL.js";
4
4
  import { AdapterRegistry } from "../adapter-registry-D2zdMwVu.js";
5
5
  import { CURRENT_CONFIG_FORMAT_VERSION, CURRENT_TASK_GRAPH_VERSION, PartialSubstrateConfigSchema } from "../config-migrator-DtZW1maj.js";
@@ -17,9 +17,11 @@ import { access, mkdir, readFile, writeFile } from "fs/promises";
17
17
  import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, unlinkSync, writeFileSync } from "fs";
18
18
  import yaml from "js-yaml";
19
19
  import { createRequire } from "node:module";
20
+ import * as path$2 from "node:path";
20
21
  import * as path$1 from "node:path";
21
22
  import { isAbsolute, join as join$1 } from "node:path";
22
23
  import { existsSync as existsSync$1, mkdirSync as mkdirSync$1, readFileSync as readFileSync$1, writeFileSync as writeFileSync$1 } from "node:fs";
24
+ import * as fs from "node:fs/promises";
23
25
  import { access as access$1, readFile as readFile$1 } from "node:fs/promises";
24
26
  import { createInterface } from "node:readline";
25
27
  import { homedir } from "os";
@@ -257,6 +259,417 @@ function registerAdaptersCommand(program, version, registry) {
257
259
  });
258
260
  }
259
261
 
262
+ //#endregion
263
+ //#region src/modules/project-profile/detect.ts
264
+ /**
265
+ * Ordered array of build system markers. Detection checks them in priority
266
+ * order — the first matching marker wins at the single-project level.
267
+ */
268
+ const STACK_MARKERS = [
269
+ {
270
+ file: "go.mod",
271
+ language: "go",
272
+ buildTool: "go",
273
+ buildCommand: "go build ./...",
274
+ testCommand: "go test ./..."
275
+ },
276
+ {
277
+ file: "build.gradle.kts",
278
+ language: "kotlin",
279
+ buildTool: "gradle",
280
+ buildCommand: "./gradlew build",
281
+ testCommand: "./gradlew test"
282
+ },
283
+ {
284
+ file: "build.gradle",
285
+ language: "java",
286
+ buildTool: "gradle",
287
+ buildCommand: "./gradlew build",
288
+ testCommand: "./gradlew test"
289
+ },
290
+ {
291
+ file: "pom.xml",
292
+ language: "java",
293
+ buildTool: "maven",
294
+ buildCommand: "mvn compile",
295
+ testCommand: "mvn test"
296
+ },
297
+ {
298
+ file: "Cargo.toml",
299
+ language: "rust",
300
+ buildTool: "cargo",
301
+ buildCommand: "cargo build",
302
+ testCommand: "cargo test"
303
+ },
304
+ {
305
+ file: "pyproject.toml",
306
+ language: "python",
307
+ buildTool: "pip",
308
+ buildCommand: "pip install -e .",
309
+ testCommand: "pytest"
310
+ },
311
+ {
312
+ file: "package.json",
313
+ language: "typescript",
314
+ buildTool: "npm",
315
+ buildCommand: "npm run build",
316
+ testCommand: "npm test"
317
+ }
318
+ ];
319
+ async function fileExists(filePath) {
320
+ try {
321
+ await fs.access(filePath);
322
+ return true;
323
+ } catch {
324
+ return false;
325
+ }
326
+ }
327
+ /**
328
+ * Derives the Node.js build tool from lock file markers.
329
+ * Mirrors the existing package manager detection logic (dispatcher-impl.ts).
330
+ */
331
+ async function detectNodeBuildTool(dir) {
332
+ if (await fileExists(path$2.join(dir, "pnpm-lock.yaml"))) return {
333
+ buildTool: "pnpm",
334
+ buildCommand: "pnpm run build",
335
+ testCommand: "pnpm test"
336
+ };
337
+ if (await fileExists(path$2.join(dir, "yarn.lock"))) return {
338
+ buildTool: "yarn",
339
+ buildCommand: "yarn build",
340
+ testCommand: "yarn test"
341
+ };
342
+ if (await fileExists(path$2.join(dir, "bun.lockb"))) return {
343
+ buildTool: "bun",
344
+ buildCommand: "bun run build",
345
+ testCommand: "bun test"
346
+ };
347
+ return {
348
+ buildTool: "npm",
349
+ buildCommand: "npm run build",
350
+ testCommand: "npm test"
351
+ };
352
+ }
353
+ /**
354
+ * Detects the language and build tool for a single project directory.
355
+ *
356
+ * Iterates `STACK_MARKERS` in priority order, calling `fs.access()` for each
357
+ * marker file. Returns the first match, or falls back to TypeScript/npm if no
358
+ * marker file is found.
359
+ *
360
+ * @param dir - Absolute path to the directory to inspect.
361
+ * @returns A `PackageEntry` describing the detected stack.
362
+ */
363
+ async function detectSingleProjectStack(dir) {
364
+ for (const marker of STACK_MARKERS) {
365
+ const markerPath = path$2.join(dir, marker.file);
366
+ if (!await fileExists(markerPath)) continue;
367
+ if (marker.file === "package.json") {
368
+ const nodeInfo = await detectNodeBuildTool(dir);
369
+ return {
370
+ path: dir,
371
+ language: "typescript",
372
+ buildTool: nodeInfo.buildTool,
373
+ buildCommand: nodeInfo.buildCommand,
374
+ testCommand: nodeInfo.testCommand
375
+ };
376
+ }
377
+ if (marker.file === "pyproject.toml") {
378
+ const hasPoetry = await fileExists(path$2.join(dir, "poetry.lock"));
379
+ return {
380
+ path: dir,
381
+ language: "python",
382
+ buildTool: hasPoetry ? "poetry" : "pip",
383
+ buildCommand: hasPoetry ? "poetry build" : "pip install -e .",
384
+ testCommand: "pytest"
385
+ };
386
+ }
387
+ return {
388
+ path: dir,
389
+ language: marker.language,
390
+ buildTool: marker.buildTool,
391
+ buildCommand: marker.buildCommand,
392
+ testCommand: marker.testCommand
393
+ };
394
+ }
395
+ return {
396
+ path: dir,
397
+ language: "typescript",
398
+ buildTool: "npm",
399
+ buildCommand: "npm run build",
400
+ testCommand: "npm test"
401
+ };
402
+ }
403
+ /**
404
+ * Detects if the project root is a Turborepo monorepo.
405
+ *
406
+ * Checks for `turbo.json` at the root, then enumerates package directories
407
+ * under `apps/` and `packages/`, calling `detectSingleProjectStack()` for each.
408
+ *
409
+ * @param rootDir - Absolute path to the project root.
410
+ * @returns A `ProjectProfile` if Turborepo is detected, otherwise `null`.
411
+ */
412
+ async function detectMonorepoProfile(rootDir) {
413
+ const turboJsonPath = path$2.join(rootDir, "turbo.json");
414
+ if (!await fileExists(turboJsonPath)) return null;
415
+ const packageDirs = [];
416
+ for (const subdir of ["apps", "packages"]) {
417
+ const fullSubdir = path$2.join(rootDir, subdir);
418
+ try {
419
+ const entries = await fs.readdir(fullSubdir, { withFileTypes: true });
420
+ for (const entry of entries) if (entry.isDirectory()) packageDirs.push(path$2.join(subdir, entry.name));
421
+ } catch {}
422
+ }
423
+ const packages = [];
424
+ for (const relPath of packageDirs) {
425
+ const absPath = path$2.join(rootDir, relPath);
426
+ const stackEntry = await detectSingleProjectStack(absPath);
427
+ packages.push({
428
+ ...stackEntry,
429
+ path: relPath
430
+ });
431
+ }
432
+ return { project: {
433
+ type: "monorepo",
434
+ tool: "turborepo",
435
+ buildCommand: "turbo build",
436
+ testCommand: "turbo test",
437
+ packages
438
+ } };
439
+ }
440
+ /**
441
+ * Auto-detects the project profile for the given root directory.
442
+ *
443
+ * First attempts Turborepo monorepo detection. If `turbo.json` is not found,
444
+ * falls back to single-project stack detection.
445
+ *
446
+ * The result is NOT written to disk — detection is purely in-memory.
447
+ *
448
+ * @param rootDir - Absolute path to the project root.
449
+ * @returns A fully populated `ProjectProfile`, or `null` if no recognizable
450
+ * stack markers are found (enabling callers to implement AC7-style
451
+ * graceful no-detection behaviour).
452
+ */
453
+ async function detectProjectProfile(rootDir) {
454
+ const monorepoProfile = await detectMonorepoProfile(rootDir);
455
+ if (monorepoProfile !== null) return monorepoProfile;
456
+ let anyMarkerFound = false;
457
+ for (const marker of STACK_MARKERS) if (await fileExists(path$2.join(rootDir, marker.file))) {
458
+ anyMarkerFound = true;
459
+ break;
460
+ }
461
+ if (!anyMarkerFound) return null;
462
+ const stackEntry = await detectSingleProjectStack(rootDir);
463
+ return { project: {
464
+ type: "single",
465
+ tool: null,
466
+ language: stackEntry.language,
467
+ buildTool: stackEntry.buildTool,
468
+ framework: stackEntry.framework,
469
+ buildCommand: stackEntry.buildCommand ?? "npm run build",
470
+ testCommand: stackEntry.testCommand ?? "npm test",
471
+ packages: []
472
+ } };
473
+ }
474
+
475
+ //#endregion
476
+ //#region src/modules/project-profile/writer.ts
477
+ const PROFILE_HEADER = "# Substrate Project Profile\n# Generated by `substrate init`\n# Edit to override auto-detected settings.\n\n";
478
+ /**
479
+ * Serializes the given `ProjectProfile` to YAML and writes it to `profilePath`.
480
+ *
481
+ * The generated file begins with a human-readable comment header so users can
482
+ * understand the file's purpose at a glance. The YAML content is produced by
483
+ * `js-yaml`'s `dump()` function for consistent formatting.
484
+ *
485
+ * @param profilePath - Absolute path to write the profile YAML file.
486
+ * @param profile - The `ProjectProfile` object to serialize.
487
+ */
488
+ async function writeProjectProfile(profilePath, profile) {
489
+ const yamlContent = yaml.dump(profile);
490
+ await writeFile(profilePath, PROFILE_HEADER + yamlContent, "utf-8");
491
+ }
492
+
493
+ //#endregion
494
+ //#region src/cli/templates/build-dev-notes.ts
495
+ const DEV_WORKFLOW_START_MARKER = "<!-- dev-workflow:start -->";
496
+ const DEV_WORKFLOW_END_MARKER = "<!-- dev-workflow:end -->";
497
+ function detectPackageManager(buildCommand) {
498
+ if (buildCommand.includes("pnpm")) return "pnpm";
499
+ if (buildCommand.includes("yarn")) return "yarn";
500
+ if (buildCommand.includes("bun")) return "bun";
501
+ return "npm";
502
+ }
503
+ function getBuildCmd(pm) {
504
+ switch (pm) {
505
+ case "pnpm": return "pnpm run build";
506
+ case "yarn": return "yarn build";
507
+ case "bun": return "bun run build";
508
+ default: return "npm run build";
509
+ }
510
+ }
511
+ function getTestCmd(pm) {
512
+ switch (pm) {
513
+ case "pnpm": return "pnpm test";
514
+ case "yarn": return "yarn test";
515
+ case "bun": return "bun test";
516
+ default: return "npm test";
517
+ }
518
+ }
519
+ function stackDefaultTestCommand(pkg) {
520
+ if (pkg.testCommand) return pkg.testCommand;
521
+ switch (pkg.language) {
522
+ case "go": return "go test ./...";
523
+ case "rust": return "cargo test";
524
+ case "java":
525
+ case "kotlin":
526
+ if (pkg.buildTool === "maven") return "mvn test";
527
+ return "./gradlew test";
528
+ case "python": return "pytest";
529
+ default: {
530
+ const buildToolPm = pkg.buildTool;
531
+ if (buildToolPm === "pnpm") return "pnpm test";
532
+ if (buildToolPm === "yarn") return "yarn test";
533
+ if (buildToolPm === "bun") return "bun test";
534
+ return "npm test";
535
+ }
536
+ }
537
+ }
538
+ function buildNodeSection(buildCommand) {
539
+ const pm = detectPackageManager(buildCommand);
540
+ const buildCmd = getBuildCmd(pm);
541
+ const testCmd = getTestCmd(pm);
542
+ return [
543
+ "## Dev Workflow",
544
+ "",
545
+ "**Build:** `" + buildCmd + "`",
546
+ "**Test:** `" + testCmd + "`",
547
+ "",
548
+ "### Testing Notes",
549
+ "- Run targeted tests during development to avoid slow feedback loops",
550
+ "- Run the full suite before merging"
551
+ ].join("\n");
552
+ }
553
+ function buildGoSection() {
554
+ return [
555
+ "## Dev Workflow",
556
+ "",
557
+ "**Build:** `go build ./...`",
558
+ "**Test:** `go test ./...`",
559
+ "",
560
+ "### Testing Notes",
561
+ "- Run targeted tests: `go test ./pkg/... -v -run TestFunctionName`",
562
+ "- Run with short flag to skip long-running tests: `go test ./... -short`",
563
+ "- Verbose output: `go test ./... -v`"
564
+ ].join("\n");
565
+ }
566
+ function buildGradleSection() {
567
+ return [
568
+ "## Dev Workflow",
569
+ "",
570
+ "**Build:** `./gradlew build`",
571
+ "**Test:** `./gradlew test`",
572
+ "",
573
+ "### Testing Notes",
574
+ "- Run a specific test class: `./gradlew test --tests \"com.example.ClassName\"`",
575
+ "- Run a specific method: `./gradlew test --tests \"com.example.ClassName.methodName\"`"
576
+ ].join("\n");
577
+ }
578
+ function buildMavenSection() {
579
+ return [
580
+ "## Dev Workflow",
581
+ "",
582
+ "**Build:** `mvn compile`",
583
+ "**Test:** `mvn test`",
584
+ "",
585
+ "### Testing Notes",
586
+ "- Run a specific test class: `mvn test -Dtest=ClassName`",
587
+ "- Run a specific method: `mvn test -Dtest=\"ClassName#methodName\"`"
588
+ ].join("\n");
589
+ }
590
+ function buildCargoSection() {
591
+ return [
592
+ "## Dev Workflow",
593
+ "",
594
+ "**Build:** `cargo build`",
595
+ "**Test:** `cargo test`",
596
+ "",
597
+ "### Testing Notes",
598
+ "- Show test output: `cargo test -- --nocapture`",
599
+ "- Run a specific test: `cargo test test_function_name`",
600
+ "- Run tests in a module: `cargo test --lib test_module`"
601
+ ].join("\n");
602
+ }
603
+ function buildPythonSection(buildCommand) {
604
+ let installCmd;
605
+ if (buildCommand.includes("poetry")) installCmd = "poetry install";
606
+ else installCmd = "pip install -e .";
607
+ return [
608
+ "## Dev Workflow",
609
+ "",
610
+ "**Install:** `" + installCmd + "`",
611
+ "**Test:** `pytest -v`",
612
+ "",
613
+ "### Testing Notes",
614
+ "- Run targeted tests: `pytest -k \"test_name\" -v`",
615
+ "- Run a specific file and test: `pytest tests/test_foo.py::test_bar -v`"
616
+ ].join("\n");
617
+ }
618
+ function buildMonorepoSection(profile) {
619
+ const { project } = profile;
620
+ const lines = [
621
+ "## Dev Workflow",
622
+ "",
623
+ `**Root build:** \`${project.buildCommand}\``,
624
+ `**Root test:** \`${project.testCommand}\``
625
+ ];
626
+ const packages = project.packages ?? [];
627
+ if (packages.length > 0) {
628
+ lines.push("");
629
+ lines.push("### Package Structure");
630
+ lines.push("");
631
+ lines.push("| Package | Language | Framework | Test Command |");
632
+ lines.push("|---------|----------|-----------|--------------|");
633
+ for (const pkg of packages) {
634
+ const lang = pkg.language;
635
+ const framework = pkg.framework ?? "—";
636
+ const testCmd = stackDefaultTestCommand(pkg);
637
+ lines.push(`| ${pkg.path} | ${lang} | ${framework} | ${testCmd} |`);
638
+ }
639
+ }
640
+ return lines.join("\n");
641
+ }
642
+ /**
643
+ * Generates a stack-aware "Dev Workflow" section for inclusion in CLAUDE.md.
644
+ *
645
+ * Returns an empty string when `profile` is null (backward-compatible — the
646
+ * caller should skip prepending the dev workflow block in that case).
647
+ *
648
+ * When a profile is present, returns a string wrapped in
649
+ * `<!-- dev-workflow:start -->` / `<!-- dev-workflow:end -->` markers.
650
+ */
651
+ function buildStackAwareDevNotes(profile) {
652
+ if (!profile) return "";
653
+ const { project } = profile;
654
+ let body;
655
+ if (project.type === "monorepo") body = buildMonorepoSection(profile);
656
+ else {
657
+ const buildTool = project.buildTool;
658
+ const language = project.language;
659
+ if (buildTool === "go" || language === "go") body = buildGoSection();
660
+ else if (buildTool === "gradle") body = buildGradleSection();
661
+ else if (buildTool === "maven") body = buildMavenSection();
662
+ else if (buildTool === "cargo" || language === "rust") body = buildCargoSection();
663
+ else if (language === "python") body = buildPythonSection(project.buildCommand);
664
+ else body = buildNodeSection(project.buildCommand);
665
+ }
666
+ return [
667
+ DEV_WORKFLOW_START_MARKER,
668
+ body,
669
+ DEV_WORKFLOW_END_MARKER
670
+ ].join("\n");
671
+ }
672
+
260
673
  //#endregion
261
674
  //#region src/cli/commands/init.ts
262
675
  const logger$18 = createLogger("init");
@@ -314,7 +727,7 @@ async function scaffoldBmadFramework(projectRoot, force, outputFormat) {
314
727
  }
315
728
  const CLAUDE_MD_START_MARKER = "<!-- substrate:start -->";
316
729
  const CLAUDE_MD_END_MARKER = "<!-- substrate:end -->";
317
- async function scaffoldClaudeMd(projectRoot) {
730
+ async function scaffoldClaudeMd(projectRoot, profile) {
318
731
  const claudeMdPath = join(projectRoot, "CLAUDE.md");
319
732
  const pkgRoot = findPackageRoot(__dirname);
320
733
  const templateName = "claude-md-substrate-section.md";
@@ -328,6 +741,7 @@ async function scaffoldClaudeMd(projectRoot) {
328
741
  return;
329
742
  }
330
743
  if (!sectionContent.endsWith("\n")) sectionContent += "\n";
744
+ const devNotesSection = buildStackAwareDevNotes(profile ?? null);
331
745
  let existingContent = "";
332
746
  let claudeMdExists = false;
333
747
  try {
@@ -335,11 +749,23 @@ async function scaffoldClaudeMd(projectRoot) {
335
749
  claudeMdExists = true;
336
750
  } catch {}
337
751
  let newContent;
338
- if (!claudeMdExists) newContent = sectionContent;
339
- else if (existingContent.includes(CLAUDE_MD_START_MARKER)) newContent = existingContent.replace(new RegExp(`${CLAUDE_MD_START_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${CLAUDE_MD_END_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`), sectionContent.trimEnd());
752
+ if (!claudeMdExists) if (devNotesSection) newContent = devNotesSection + "\n\n" + sectionContent;
753
+ else newContent = sectionContent;
340
754
  else {
341
- const separator = existingContent.endsWith("\n") ? "\n" : "\n\n";
342
- newContent = existingContent + separator + sectionContent;
755
+ let updatedExisting;
756
+ if (existingContent.includes(CLAUDE_MD_START_MARKER)) updatedExisting = existingContent.replace(new RegExp(`${CLAUDE_MD_START_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${CLAUDE_MD_END_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`), sectionContent.trimEnd());
757
+ else {
758
+ const separator = existingContent.endsWith("\n") ? "\n" : "\n\n";
759
+ updatedExisting = existingContent + separator + sectionContent;
760
+ }
761
+ if (devNotesSection) if (updatedExisting.includes(DEV_WORKFLOW_START_MARKER)) newContent = updatedExisting.replace(new RegExp(`${DEV_WORKFLOW_START_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${DEV_WORKFLOW_END_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`), devNotesSection);
762
+ else if (updatedExisting.includes(CLAUDE_MD_START_MARKER)) newContent = updatedExisting.replace(CLAUDE_MD_START_MARKER, devNotesSection + "\n\n" + CLAUDE_MD_START_MARKER);
763
+ else {
764
+ const sep = updatedExisting.endsWith("\n") ? "\n" : "\n\n";
765
+ newContent = devNotesSection + sep + updatedExisting;
766
+ }
767
+ else if (updatedExisting.includes(DEV_WORKFLOW_START_MARKER)) newContent = updatedExisting.replace(new RegExp(`${DEV_WORKFLOW_START_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${DEV_WORKFLOW_END_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`), "");
768
+ else newContent = updatedExisting;
343
769
  }
344
770
  await writeFile(claudeMdPath, newContent, "utf8");
345
771
  logger$18.info({ claudeMdPath }, "Wrote substrate section to CLAUDE.md");
@@ -486,6 +912,22 @@ async function scaffoldClaudeCommands(projectRoot, outputFormat) {
486
912
  const TaskToolCommandGenerator = resolveExport(taskToolMod, "TaskToolCommandGenerator");
487
913
  const manifestMod = _require(join(installerLibPath, "core", "manifest-generator.js"));
488
914
  const ManifestGenerator = resolveExport(manifestMod, "ManifestGenerator");
915
+ const pathUtilsMod = _require(join(installerLibPath, "ide", "shared", "path-utils.js"));
916
+ const pathUtils = { toDashPath: pathUtilsMod.toDashPath ?? pathUtilsMod.default?.toDashPath };
917
+ const writeDashFallback = async (baseDir, artifacts, acceptTypes) => {
918
+ let written = 0;
919
+ for (const artifact of artifacts) {
920
+ if (!acceptTypes.includes(artifact.type)) continue;
921
+ const content = artifact.content;
922
+ if (!content || !artifact.relativePath) continue;
923
+ const flatName = pathUtils.toDashPath(artifact.relativePath);
924
+ const dest = join(baseDir, flatName);
925
+ mkdirSync(dirname(dest), { recursive: true });
926
+ writeFileSync(dest, content, "utf-8");
927
+ written++;
928
+ }
929
+ return written;
930
+ };
489
931
  const nonCoreModules = scanBmadModules(bmadDir);
490
932
  const allModules = ["core", ...nonCoreModules];
491
933
  try {
@@ -499,13 +941,13 @@ async function scaffoldClaudeCommands(projectRoot, outputFormat) {
499
941
  clearBmadCommandFiles(commandsDir);
500
942
  const agentGen = new AgentCommandGenerator("_bmad");
501
943
  const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, nonCoreModules);
502
- const agentCount = await agentGen.writeDashArtifacts(commandsDir, agentArtifacts);
944
+ const agentCount = typeof agentGen.writeDashArtifacts === "function" ? await agentGen.writeDashArtifacts(commandsDir, agentArtifacts) : await writeDashFallback(commandsDir, agentArtifacts, ["agent-launcher"]);
503
945
  const workflowGen = new WorkflowCommandGenerator("_bmad");
504
946
  const { artifacts: workflowArtifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir);
505
- const workflowCount = await workflowGen.writeDashArtifacts(commandsDir, workflowArtifacts);
947
+ const workflowCount = typeof workflowGen.writeDashArtifacts === "function" ? await workflowGen.writeDashArtifacts(commandsDir, workflowArtifacts) : await writeDashFallback(commandsDir, workflowArtifacts, ["workflow-command", "workflow-launcher"]);
506
948
  const taskToolGen = new TaskToolCommandGenerator("_bmad");
507
949
  const { artifacts: taskToolArtifacts } = await taskToolGen.collectTaskToolArtifacts(bmadDir);
508
- const taskToolCount = await taskToolGen.writeDashArtifacts(commandsDir, taskToolArtifacts);
950
+ const taskToolCount = typeof taskToolGen.writeDashArtifacts === "function" ? await taskToolGen.writeDashArtifacts(commandsDir, taskToolArtifacts) : await writeDashFallback(commandsDir, taskToolArtifacts, ["task", "tool"]);
509
951
  const total = agentCount + workflowCount + taskToolCount;
510
952
  if (outputFormat !== "json") process.stdout.write(`Generated ${String(total)} Claude Code commands (${String(agentCount)} agents, ${String(workflowCount)} workflows, ${String(taskToolCount)} tasks/tools)\n`);
511
953
  logger$18.info({
@@ -543,6 +985,51 @@ function buildProviderConfig(adapterId, cliPath, subscriptionRouting) {
543
985
  subscription_routing: subscriptionRouting
544
986
  };
545
987
  }
988
+ /**
989
+ * Formats a detected project profile as a human-readable string.
990
+ * For single projects: shows stack, build, and test commands.
991
+ * For monorepos: shows the tool, root commands, and per-package breakdown.
992
+ */
993
+ function formatProjectProfile(profile) {
994
+ const lines = ["", " Detected project profile:"];
995
+ const { project } = profile;
996
+ if (project.type === "monorepo") {
997
+ lines.push(` Type: monorepo (${project.tool ?? "unknown"})`);
998
+ lines.push(` Build: ${project.buildCommand}`);
999
+ lines.push(` Test: ${project.testCommand}`);
1000
+ if (project.packages && project.packages.length > 0) {
1001
+ lines.push(" Packages:");
1002
+ for (const pkg of project.packages) lines.push(` ${pkg.path} ${pkg.language}`);
1003
+ }
1004
+ } else {
1005
+ const lang = project.language ?? "unknown";
1006
+ const stackStr = project.framework ? `${lang} (${project.framework})` : lang;
1007
+ lines.push(` Stack: ${stackStr}`);
1008
+ lines.push(` Build: ${project.buildCommand}`);
1009
+ lines.push(` Test: ${project.testCommand}`);
1010
+ }
1011
+ return lines.join("\n");
1012
+ }
1013
+ /**
1014
+ * Prompts the user to accept or decline the detected project profile.
1015
+ * In non-interactive mode, always returns true (auto-accept).
1016
+ */
1017
+ async function promptProfileConfirmation(nonInteractive) {
1018
+ if (nonInteractive) return true;
1019
+ const readline = await import("readline");
1020
+ const rl = readline.createInterface({
1021
+ input: process.stdin,
1022
+ output: process.stdout
1023
+ });
1024
+ return new Promise((resolve$2) => {
1025
+ rl.question("\n Accept detected project profile? [Y/n]: ", (answer) => {
1026
+ rl.close();
1027
+ const trimmed = answer.trim().toLowerCase();
1028
+ if (trimmed === "" || trimmed === "y" || trimmed === "yes") resolve$2(true);
1029
+ else resolve$2(false);
1030
+ });
1031
+ });
1032
+ }
546
1033
  async function promptSubscriptionRouting(providerName, nonInteractive) {
547
1034
  if (nonInteractive) return "auto";
548
1035
  const readline = await import("readline");
@@ -559,9 +1046,9 @@ async function promptSubscriptionRouting(providerName, nonInteractive) {
559
1046
  });
560
1047
  });
561
1048
  }
562
- async function directoryExists(path$2) {
1049
+ async function directoryExists(path$3) {
563
1050
  try {
564
- await access(path$2);
1051
+ await access(path$3);
565
1052
  return true;
566
1053
  } catch {
567
1054
  return false;
@@ -630,6 +1117,33 @@ async function runInitAction(options) {
630
1117
  await writeFile(configPath, configHeader + yaml.dump(config), "utf-8");
631
1118
  const routingHeader = "# Substrate Routing Policy\n# Defines how tasks are routed to AI providers.\n# Customize rules to match your workflow and available agents.\n\n";
632
1119
  await writeFile(routingPolicyPath, routingHeader + yaml.dump(routingPolicy), "utf-8");
1120
+ const projectProfilePath = join(substrateDir, "project-profile.yaml");
1121
+ let detectedProfile = null;
1122
+ let projectProfileWritten = false;
1123
+ try {
1124
+ detectedProfile = await detectProjectProfile(dbRoot);
1125
+ } catch (err) {
1126
+ logger$18.warn({ err }, "Project profile detection failed; skipping");
1127
+ }
1128
+ if (detectedProfile === null) {
1129
+ if (outputFormat !== "json") process.stdout.write(" No project stack detected. Create .substrate/project-profile.yaml manually to enable polyglot support.\n");
1130
+ } else {
1131
+ if (outputFormat !== "json") process.stdout.write(formatProjectProfile(detectedProfile) + "\n");
1132
+ let profileExists = false;
1133
+ try {
1134
+ await access(projectProfilePath);
1135
+ profileExists = true;
1136
+ } catch {}
1137
+ if (profileExists && !force) {
1138
+ if (outputFormat !== "json") process.stdout.write(" .substrate/project-profile.yaml already exists — skipping (use --force to overwrite)\n");
1139
+ } else {
1140
+ const accepted = await promptProfileConfirmation(nonInteractive);
1141
+ if (accepted) {
1142
+ await writeProjectProfile(projectProfilePath, detectedProfile);
1143
+ projectProfileWritten = true;
1144
+ } else if (outputFormat !== "json") process.stdout.write(" Profile not written. Create .substrate/project-profile.yaml manually to enable polyglot support.\n");
1145
+ }
1146
+ }
633
1147
  await scaffoldBmadFramework(projectRoot, force, outputFormat);
634
1148
  const localManifest = join(packPath, "manifest.yaml");
635
1149
  let scaffolded = false;
@@ -671,7 +1185,7 @@ async function runInitAction(options) {
671
1185
  });
672
1186
  await initSchema(dbAdapter);
673
1187
  await dbAdapter.close();
674
- await scaffoldClaudeMd(projectRoot);
1188
+ await scaffoldClaudeMd(projectRoot, detectedProfile);
675
1189
  await scaffoldStatuslineScript(projectRoot);
676
1190
  await scaffoldClaudeSettings(projectRoot);
677
1191
  await scaffoldClaudeCommands(projectRoot, outputFormat);
@@ -705,7 +1219,9 @@ async function runInitAction(options) {
705
1219
  scaffolded,
706
1220
  configPath,
707
1221
  routingPolicyPath,
708
- doltInitialized
1222
+ doltInitialized,
1223
+ projectProfile: detectedProfile ?? null,
1224
+ projectProfileWritten
709
1225
  }, "json", true) + "\n");
710
1226
  else {
711
1227
  process.stdout.write(`\n Substrate initialized successfully!\n\n`);
@@ -2969,7 +3485,7 @@ async function runSupervisorAction(options, deps = {}) {
2969
3485
  await initSchema(expAdapter);
2970
3486
  const { runRunAction: runPipeline } = await import(
2971
3487
  /* @vite-ignore */
2972
- "../run-IU38JGTV.js"
3488
+ "../run-CcUT8-DF.js"
2973
3489
  );
2974
3490
  const runStoryFn = async (opts) => {
2975
3491
  const exitCode = await runPipeline({
@@ -3817,8 +4333,8 @@ function registerMetricsCommand(program, _version = "0.0.0", projectRoot = proce
3817
4333
  * This function now always returns an empty snapshot.
3818
4334
  */
3819
4335
  async function readSqliteSnapshot(dbPath) {
3820
- const { existsSync: fileExists } = await import("node:fs");
3821
- if (fileExists(dbPath)) process.stderr.write(`Warning: Legacy SQLite database found at ${dbPath} but SQLite support has been\nremoved in Epic 29. To migrate historical data, downgrade to Substrate v0.4.x,\nrun 'substrate migrate', then upgrade back to this version.\n`);
4336
+ const { existsSync: fileExists$1 } = await import("node:fs");
4337
+ if (fileExists$1(dbPath)) process.stderr.write(`Warning: Legacy SQLite database found at ${dbPath} but SQLite support has been\nremoved in Epic 29. To migrate historical data, downgrade to Substrate v0.4.x,\nrun 'substrate migrate', then upgrade back to this version.\n`);
3822
4338
  return { storyMetrics: [] };
3823
4339
  }
3824
4340
  const BATCH_SIZE = 100;