substrate-ai 0.6.0 → 0.6.1

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-IDOmPys1.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");
@@ -543,6 +969,51 @@ function buildProviderConfig(adapterId, cliPath, subscriptionRouting) {
543
969
  subscription_routing: subscriptionRouting
544
970
  };
545
971
  }
972
+ /**
973
+ * Formats a detected project profile as a human-readable string.
974
+ * For single projects: shows stack, build, and test commands.
975
+ * For monorepos: shows the tool, root commands, and per-package breakdown.
976
+ */
977
+ function formatProjectProfile(profile) {
978
+ const lines = ["", " Detected project profile:"];
979
+ const { project } = profile;
980
+ if (project.type === "monorepo") {
981
+ lines.push(` Type: monorepo (${project.tool ?? "unknown"})`);
982
+ lines.push(` Build: ${project.buildCommand}`);
983
+ lines.push(` Test: ${project.testCommand}`);
984
+ if (project.packages && project.packages.length > 0) {
985
+ lines.push(" Packages:");
986
+ for (const pkg of project.packages) lines.push(` ${pkg.path} ${pkg.language}`);
987
+ }
988
+ } else {
989
+ const lang = project.language ?? "unknown";
990
+ const stackStr = project.framework ? `${lang} (${project.framework})` : lang;
991
+ lines.push(` Stack: ${stackStr}`);
992
+ lines.push(` Build: ${project.buildCommand}`);
993
+ lines.push(` Test: ${project.testCommand}`);
994
+ }
995
+ return lines.join("\n");
996
+ }
997
+ /**
998
+ * Prompts the user to accept or decline the detected project profile.
999
+ * In non-interactive mode, always returns true (auto-accept).
1000
+ */
1001
+ async function promptProfileConfirmation(nonInteractive) {
1002
+ if (nonInteractive) return true;
1003
+ const readline = await import("readline");
1004
+ const rl = readline.createInterface({
1005
+ input: process.stdin,
1006
+ output: process.stdout
1007
+ });
1008
+ return new Promise((resolve$2) => {
1009
+ rl.question("\n Accept detected project profile? [Y/n]: ", (answer) => {
1010
+ rl.close();
1011
+ const trimmed = answer.trim().toLowerCase();
1012
+ if (trimmed === "" || trimmed === "y" || trimmed === "yes") resolve$2(true);
1013
+ else resolve$2(false);
1014
+ });
1015
+ });
1016
+ }
546
1017
  async function promptSubscriptionRouting(providerName, nonInteractive) {
547
1018
  if (nonInteractive) return "auto";
548
1019
  const readline = await import("readline");
@@ -559,9 +1030,9 @@ async function promptSubscriptionRouting(providerName, nonInteractive) {
559
1030
  });
560
1031
  });
561
1032
  }
562
- async function directoryExists(path$2) {
1033
+ async function directoryExists(path$3) {
563
1034
  try {
564
- await access(path$2);
1035
+ await access(path$3);
565
1036
  return true;
566
1037
  } catch {
567
1038
  return false;
@@ -630,6 +1101,33 @@ async function runInitAction(options) {
630
1101
  await writeFile(configPath, configHeader + yaml.dump(config), "utf-8");
631
1102
  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
1103
  await writeFile(routingPolicyPath, routingHeader + yaml.dump(routingPolicy), "utf-8");
1104
+ const projectProfilePath = join(substrateDir, "project-profile.yaml");
1105
+ let detectedProfile = null;
1106
+ let projectProfileWritten = false;
1107
+ try {
1108
+ detectedProfile = await detectProjectProfile(dbRoot);
1109
+ } catch (err) {
1110
+ logger$18.warn({ err }, "Project profile detection failed; skipping");
1111
+ }
1112
+ if (detectedProfile === null) {
1113
+ if (outputFormat !== "json") process.stdout.write(" No project stack detected. Create .substrate/project-profile.yaml manually to enable polyglot support.\n");
1114
+ } else {
1115
+ if (outputFormat !== "json") process.stdout.write(formatProjectProfile(detectedProfile) + "\n");
1116
+ let profileExists = false;
1117
+ try {
1118
+ await access(projectProfilePath);
1119
+ profileExists = true;
1120
+ } catch {}
1121
+ if (profileExists && !force) {
1122
+ if (outputFormat !== "json") process.stdout.write(" .substrate/project-profile.yaml already exists — skipping (use --force to overwrite)\n");
1123
+ } else {
1124
+ const accepted = await promptProfileConfirmation(nonInteractive);
1125
+ if (accepted) {
1126
+ await writeProjectProfile(projectProfilePath, detectedProfile);
1127
+ projectProfileWritten = true;
1128
+ } else if (outputFormat !== "json") process.stdout.write(" Profile not written. Create .substrate/project-profile.yaml manually to enable polyglot support.\n");
1129
+ }
1130
+ }
633
1131
  await scaffoldBmadFramework(projectRoot, force, outputFormat);
634
1132
  const localManifest = join(packPath, "manifest.yaml");
635
1133
  let scaffolded = false;
@@ -671,7 +1169,7 @@ async function runInitAction(options) {
671
1169
  });
672
1170
  await initSchema(dbAdapter);
673
1171
  await dbAdapter.close();
674
- await scaffoldClaudeMd(projectRoot);
1172
+ await scaffoldClaudeMd(projectRoot, detectedProfile);
675
1173
  await scaffoldStatuslineScript(projectRoot);
676
1174
  await scaffoldClaudeSettings(projectRoot);
677
1175
  await scaffoldClaudeCommands(projectRoot, outputFormat);
@@ -705,7 +1203,9 @@ async function runInitAction(options) {
705
1203
  scaffolded,
706
1204
  configPath,
707
1205
  routingPolicyPath,
708
- doltInitialized
1206
+ doltInitialized,
1207
+ projectProfile: detectedProfile ?? null,
1208
+ projectProfileWritten
709
1209
  }, "json", true) + "\n");
710
1210
  else {
711
1211
  process.stdout.write(`\n Substrate initialized successfully!\n\n`);
@@ -2969,7 +3469,7 @@ async function runSupervisorAction(options, deps = {}) {
2969
3469
  await initSchema(expAdapter);
2970
3470
  const { runRunAction: runPipeline } = await import(
2971
3471
  /* @vite-ignore */
2972
- "../run-IU38JGTV.js"
3472
+ "../run-DTOsG7PJ.js"
2973
3473
  );
2974
3474
  const runStoryFn = async (opts) => {
2975
3475
  const exitCode = await runPipeline({
@@ -3817,8 +4317,8 @@ function registerMetricsCommand(program, _version = "0.0.0", projectRoot = proce
3817
4317
  * This function now always returns an empty snapshot.
3818
4318
  */
3819
4319
  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`);
4320
+ const { existsSync: fileExists$1 } = await import("node:fs");
4321
+ 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
4322
  return { storyMetrics: [] };
3823
4323
  }
3824
4324
  const BATCH_SIZE = 100;