@cubis/foundry 0.3.14 → 0.3.16

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.
Files changed (3) hide show
  1. package/README.md +42 -1
  2. package/bin/cubis.js +396 -40
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -20,6 +20,23 @@ Compatibility binaries are still shipped for migration:
20
20
  - `cubiskill`
21
21
  - `cubis`
22
22
 
23
+ ## Quick Setup (Simple)
24
+
25
+ ```bash
26
+ # 1) Install CLI
27
+ npm install -g @cubis/foundry
28
+
29
+ # 2) Set Postman key once (recommended: env mode)
30
+ export POSTMAN_API_KEY="<your-postman-api-key>"
31
+
32
+ # 3) Install workflow bundle for your platform
33
+ cbx workflows install --platform codex --bundle agent-environment-setup --postman --yes
34
+
35
+ # 4) Optional: install for other platforms too
36
+ cbx workflows install --platform antigravity --bundle agent-environment-setup --postman --yes
37
+ cbx workflows install --platform copilot --bundle agent-environment-setup --postman --yes
38
+ ```
39
+
23
40
  ## Command Model
24
41
 
25
42
  `workflows` is now the primary command group.
@@ -36,13 +53,23 @@ cbx workflows install --platform antigravity --dry-run
36
53
  cbx workflows install --platform antigravity --terminal-integration --terminal-verifier codex
37
54
  cbx workflows install --platform codex --postman
38
55
  cbx workflows install --platform codex --postman --postman-workspace-id null
56
+ cbx workflows install --platform codex --postman --postman-api-key "<key>"
57
+ cbx workflows install --platform antigravity --postman
58
+ cbx workflows install --platform copilot --postman
39
59
  ```
40
60
 
41
61
  Install bootstrap behavior:
42
62
  - `cbx workflows install` now also bootstraps `ENGINEERING_RULES.md` and `TECH.md` (creates when missing; keeps existing files unless explicitly regenerated).
43
- - Optional `--postman` bootstrap creates `postman_setting.json` and installs/configures the Postman skill.
63
+ - Optional `--postman` bootstrap creates `postman_setting.json` and installs/configures the Postman skill/MCP for Codex, Antigravity, and Copilot.
44
64
  - Use `cbx rules init --platform <platform> --overwrite` to force-regenerate both files.
45
65
 
66
+ Postman setup behavior:
67
+ - `postman_setting.json` is generated in project root (or `~/.cbx/postman_setting.json` with `--scope global`).
68
+ - Env-first auth is supported: when `POSTMAN_API_KEY` is set, generated settings keep `apiKey: null` and MCP config uses `Bearer ${POSTMAN_API_KEY}`.
69
+ - Inline auth is supported with `--postman-api-key <key>`.
70
+ - `--postman-workspace-id null` writes JSON `null` for `defaultWorkspaceId`.
71
+ - In project scope, `postman_setting.json` is auto-added to `.gitignore` (no duplicate entries).
72
+
46
73
  `rules` manages strict engineering policy and a generated codebase tech map:
47
74
 
48
75
  ```bash
@@ -60,6 +87,14 @@ What `cbx rules init` does:
60
87
  - Generates `TECH.md` at workspace root by scanning the current codebase.
61
88
  - Preserves user content outside managed markers.
62
89
 
90
+ `TECH.md` scanner coverage (deterministic, no AI calls):
91
+ - Language/file signals from workspace scan.
92
+ - JS/TS package signals from `package.json` (including nested/monorepo package files).
93
+ - Flutter/Dart package signals from `pubspec.yaml`.
94
+ - Go module signals from `go.mod`.
95
+ - Python package signals from `requirements*.txt` and `pyproject.toml`.
96
+ - Rust crate signals from `Cargo.toml`.
97
+
63
98
  ### Deprecated Alias
64
99
 
65
100
  `cbx skills ...` still works for one minor cycle and prints a deprecation notice.
@@ -317,6 +352,12 @@ Run attribute validation (strict, fails on warnings):
317
352
  npm run test:attributes:strict
318
353
  ```
319
354
 
355
+ Run TECH.md scanner integration tests:
356
+
357
+ ```bash
358
+ npm run test:tech-md
359
+ ```
360
+
320
361
  Run full workflow smoke test:
321
362
 
322
363
  ```bash
package/bin/cubis.js CHANGED
@@ -172,6 +172,54 @@ const TECH_LANGUAGE_BY_EXTENSION = new Map([
172
172
  [".sh", "Shell"],
173
173
  [".ps1", "PowerShell"]
174
174
  ]);
175
+ const TECH_PACKAGE_PREVIEW_LIMIT = 40;
176
+ const TECH_JS_FRAMEWORK_SIGNALS = [
177
+ ["next", "Next.js"],
178
+ ["react", "React"],
179
+ ["vue", "Vue"],
180
+ ["nuxt", "Nuxt"],
181
+ ["svelte", "Svelte"],
182
+ ["@nestjs/core", "NestJS"],
183
+ ["express", "Express"],
184
+ ["fastify", "Fastify"],
185
+ ["hono", "Hono"],
186
+ ["tailwindcss", "Tailwind CSS"],
187
+ ["prisma", "Prisma"],
188
+ ["drizzle-orm", "Drizzle ORM"],
189
+ ["mongoose", "Mongoose"],
190
+ ["typeorm", "TypeORM"],
191
+ ["@playwright/test", "Playwright"],
192
+ ["vitest", "Vitest"],
193
+ ["jest", "Jest"],
194
+ ["cypress", "Cypress"]
195
+ ];
196
+ const TECH_DART_FRAMEWORK_SIGNALS = [
197
+ ["flutter_riverpod", "Riverpod"],
198
+ ["riverpod", "Riverpod"],
199
+ ["go_router", "go_router"],
200
+ ["dio", "Dio"],
201
+ ["freezed", "Freezed"],
202
+ ["bloc", "BLoC"]
203
+ ];
204
+ const TECH_GO_FRAMEWORK_SIGNALS = [
205
+ ["github.com/gofiber/fiber/v2", "Go Fiber"],
206
+ ["github.com/gin-gonic/gin", "Gin"],
207
+ ["github.com/labstack/echo/v4", "Echo"],
208
+ ["github.com/go-chi/chi/v5", "Chi"]
209
+ ];
210
+ const TECH_PYTHON_FRAMEWORK_SIGNALS = [
211
+ ["fastapi", "FastAPI"],
212
+ ["django", "Django"],
213
+ ["flask", "Flask"],
214
+ ["pydantic", "Pydantic"],
215
+ ["sqlalchemy", "SQLAlchemy"]
216
+ ];
217
+ const TECH_RUST_FRAMEWORK_SIGNALS = [
218
+ ["axum", "Axum"],
219
+ ["actix-web", "Actix Web"],
220
+ ["rocket", "Rocket"],
221
+ ["tokio", "Tokio"]
222
+ ];
175
223
 
176
224
  function platformInstallsCustomAgents(platformId) {
177
225
  const profile = WORKFLOW_PROFILES[platformId];
@@ -585,6 +633,198 @@ async function upsertEngineeringRulesBlock({
585
633
  };
586
634
  }
587
635
 
636
+ function normalizeTechPackageName(value) {
637
+ if (value === undefined || value === null) return null;
638
+ const normalized = String(value).trim().replace(/^['"]|['"]$/g, "").toLowerCase();
639
+ return normalized || null;
640
+ }
641
+
642
+ function parseTomlSections(content) {
643
+ const sections = new Map();
644
+ let currentSection = "";
645
+ sections.set(currentSection, []);
646
+
647
+ for (const line of content.split(/\r?\n/)) {
648
+ const sectionMatch = line.match(/^\s*\[([^\]]+)\]\s*$/);
649
+ if (sectionMatch) {
650
+ currentSection = sectionMatch[1].trim();
651
+ if (!sections.has(currentSection)) sections.set(currentSection, []);
652
+ continue;
653
+ }
654
+ sections.get(currentSection).push(line);
655
+ }
656
+
657
+ return sections;
658
+ }
659
+
660
+ function parsePubspecDependencyNames(content) {
661
+ const packages = new Set();
662
+ let currentSection = null;
663
+
664
+ for (const rawLine of content.split(/\r?\n/)) {
665
+ const line = rawLine.replace(/\t/g, " ");
666
+ const trimmed = line.trim();
667
+ if (!trimmed || trimmed.startsWith("#")) continue;
668
+
669
+ if (!line.startsWith(" ")) {
670
+ const sectionMatch = trimmed.match(/^([a-zA-Z0-9_]+):\s*$/);
671
+ if (!sectionMatch) {
672
+ currentSection = null;
673
+ continue;
674
+ }
675
+ currentSection = sectionMatch[1];
676
+ continue;
677
+ }
678
+
679
+ if (currentSection !== "dependencies" && currentSection !== "dev_dependencies") continue;
680
+ const depMatch = trimmed.match(/^([a-zA-Z0-9_]+):/);
681
+ if (!depMatch) continue;
682
+ const packageName = normalizeTechPackageName(depMatch[1]);
683
+ if (!packageName || packageName === "flutter" || packageName === "sdk") continue;
684
+ packages.add(packageName);
685
+ }
686
+
687
+ return packages;
688
+ }
689
+
690
+ function parseGoModuleNames(content) {
691
+ const modules = new Set();
692
+ let inRequireBlock = false;
693
+
694
+ for (const rawLine of content.split(/\r?\n/)) {
695
+ const trimmed = rawLine.trim();
696
+ if (!trimmed || trimmed.startsWith("//")) continue;
697
+
698
+ if (trimmed.startsWith("require (")) {
699
+ inRequireBlock = true;
700
+ continue;
701
+ }
702
+ if (inRequireBlock && trimmed === ")") {
703
+ inRequireBlock = false;
704
+ continue;
705
+ }
706
+
707
+ if (inRequireBlock) {
708
+ const moduleName = normalizeTechPackageName(trimmed.split(/\s+/)[0]);
709
+ if (moduleName) modules.add(moduleName);
710
+ continue;
711
+ }
712
+
713
+ if (trimmed.startsWith("require ")) {
714
+ const moduleName = normalizeTechPackageName(trimmed.slice("require ".length).trim().split(/\s+/)[0]);
715
+ if (moduleName) modules.add(moduleName);
716
+ }
717
+ }
718
+
719
+ return modules;
720
+ }
721
+
722
+ function parseRequirementsPackageNames(content) {
723
+ const packages = new Set();
724
+ for (const rawLine of content.split(/\r?\n/)) {
725
+ const withoutComment = rawLine.split("#")[0].trim();
726
+ if (!withoutComment) continue;
727
+ if (withoutComment.startsWith("-")) continue;
728
+
729
+ const cleaned = withoutComment.split(";")[0].trim();
730
+ const match = cleaned.match(/^([A-Za-z0-9_.-]+)/);
731
+ if (!match) continue;
732
+ const packageName = normalizeTechPackageName(match[1]);
733
+ if (packageName) packages.add(packageName);
734
+ }
735
+ return packages;
736
+ }
737
+
738
+ function parsePyprojectPackageNames(content) {
739
+ const packages = new Set();
740
+ const sections = parseTomlSections(content);
741
+
742
+ const projectSection = sections.get("project");
743
+ if (projectSection) {
744
+ const projectBody = projectSection.join("\n");
745
+ const dependenciesArrayMatch = projectBody.match(/dependencies\s*=\s*\[([\s\S]*?)\]/m);
746
+ if (dependenciesArrayMatch) {
747
+ const entries = dependenciesArrayMatch[1].match(/"([^"]+)"|'([^']+)'/g) || [];
748
+ for (const entry of entries) {
749
+ const normalizedEntry = normalizeTechPackageName(entry);
750
+ if (!normalizedEntry) continue;
751
+ const packageName = normalizeTechPackageName(normalizedEntry.split(/[<>=!~\s\[]/)[0]);
752
+ if (packageName) packages.add(packageName);
753
+ }
754
+ }
755
+
756
+ for (const line of projectSection) {
757
+ const trimmed = line.trim();
758
+ const optionalDepsMatch = trimmed.match(
759
+ /^([A-Za-z0-9_.-]+)\s*=\s*\[\s*("([^"]+)"|'([^']+)')/
760
+ );
761
+ if (!optionalDepsMatch) continue;
762
+ const entry = optionalDepsMatch[3] || optionalDepsMatch[4];
763
+ const packageName = normalizeTechPackageName(String(entry).split(/[<>=!~\s\[]/)[0]);
764
+ if (packageName) packages.add(packageName);
765
+ }
766
+ }
767
+
768
+ for (const [sectionName, lines] of sections.entries()) {
769
+ const isPoetryDependencySection =
770
+ sectionName === "tool.poetry.dependencies" ||
771
+ /^tool\.poetry\.group\.[^.]+\.dependencies$/.test(sectionName);
772
+ if (!isPoetryDependencySection) continue;
773
+
774
+ for (const line of lines) {
775
+ const trimmed = line.trim();
776
+ if (!trimmed || trimmed.startsWith("#")) continue;
777
+ const depMatch = trimmed.match(/^([A-Za-z0-9_.-]+)\s*=/);
778
+ if (!depMatch) continue;
779
+ const packageName = normalizeTechPackageName(depMatch[1]);
780
+ if (!packageName || packageName === "python") continue;
781
+ packages.add(packageName);
782
+ }
783
+ }
784
+
785
+ return packages;
786
+ }
787
+
788
+ function parseCargoCrateNames(content) {
789
+ const crates = new Set();
790
+ const sections = parseTomlSections(content);
791
+
792
+ for (const [sectionName, lines] of sections.entries()) {
793
+ const isDependencySection =
794
+ sectionName === "dependencies" ||
795
+ sectionName === "dev-dependencies" ||
796
+ sectionName === "build-dependencies" ||
797
+ sectionName === "workspace.dependencies" ||
798
+ /\.dependencies$/.test(sectionName) ||
799
+ /\.dev-dependencies$/.test(sectionName) ||
800
+ /\.build-dependencies$/.test(sectionName);
801
+ if (!isDependencySection) continue;
802
+
803
+ for (const line of lines) {
804
+ const trimmed = line.trim();
805
+ if (!trimmed || trimmed.startsWith("#")) continue;
806
+ const depMatch = trimmed.match(/^([A-Za-z0-9_-]+)\s*=/);
807
+ if (!depMatch) continue;
808
+ const crateName = normalizeTechPackageName(depMatch[1]);
809
+ if (crateName) crates.add(crateName);
810
+ }
811
+ }
812
+
813
+ return crates;
814
+ }
815
+
816
+ function addFrameworkSignalsFromPackages({ packages, frameworks, signals }) {
817
+ for (const [signal, frameworkLabel] of signals) {
818
+ if (packages.has(signal.toLowerCase())) {
819
+ frameworks.add(frameworkLabel);
820
+ }
821
+ }
822
+ }
823
+
824
+ function toSortedArray(values) {
825
+ return [...values].sort((a, b) => a.localeCompare(b));
826
+ }
827
+
588
828
  async function collectTechSnapshot(rootDir) {
589
829
  const discoveredFiles = [];
590
830
  const queue = [rootDir];
@@ -614,6 +854,12 @@ async function collectTechSnapshot(rootDir) {
614
854
 
615
855
  const languageCounts = new Map();
616
856
  const topDirs = new Set();
857
+ const packageJsonFiles = [];
858
+ const pubspecFiles = [];
859
+ const goModFiles = [];
860
+ const pyprojectFiles = [];
861
+ const requirementsFiles = [];
862
+ const cargoTomlFiles = [];
617
863
 
618
864
  for (const fullPath of discoveredFiles) {
619
865
  const rel = toPosixPath(path.relative(rootDir, fullPath));
@@ -622,15 +868,31 @@ async function collectTechSnapshot(rootDir) {
622
868
 
623
869
  const extension = path.extname(fullPath).toLowerCase();
624
870
  const language = TECH_LANGUAGE_BY_EXTENSION.get(extension);
625
- if (!language) continue;
626
- languageCounts.set(language, (languageCounts.get(language) || 0) + 1);
871
+ if (language) {
872
+ languageCounts.set(language, (languageCounts.get(language) || 0) + 1);
873
+ }
874
+
875
+ const baseName = path.basename(fullPath).toLowerCase();
876
+ if (baseName === "package.json") packageJsonFiles.push(fullPath);
877
+ if (baseName === "pubspec.yaml") pubspecFiles.push(fullPath);
878
+ if (baseName === "go.mod") goModFiles.push(fullPath);
879
+ if (baseName === "pyproject.toml") pyprojectFiles.push(fullPath);
880
+ if (baseName === "cargo.toml") cargoTomlFiles.push(fullPath);
881
+ if (baseName === "requirements.txt" || /^requirements(?:[-_.].+)?\.txt$/.test(baseName)) {
882
+ requirementsFiles.push(fullPath);
883
+ }
627
884
  }
628
885
 
629
886
  const fileExists = (name) => existsSync(path.join(rootDir, name));
630
- const packageJsonPath = path.join(rootDir, "package.json");
887
+ const rootPackageJsonPath = path.join(rootDir, "package.json");
631
888
  const packageScripts = new Map();
632
889
  const frameworks = new Set();
633
890
  const lockfiles = [];
891
+ const javascriptPackages = new Set();
892
+ const dartPackages = new Set();
893
+ const goModules = new Set();
894
+ const pythonPackages = new Set();
895
+ const rustCrates = new Set();
634
896
 
635
897
  if (fileExists("bun.lock") || fileExists("bun.lockb")) lockfiles.push("bun");
636
898
  if (fileExists("pnpm-lock.yaml")) lockfiles.push("pnpm");
@@ -641,54 +903,118 @@ async function collectTechSnapshot(rootDir) {
641
903
  if (fileExists("go.sum")) lockfiles.push("go");
642
904
  if (fileExists("pubspec.lock")) lockfiles.push("pub");
643
905
 
644
- if (fileExists("pubspec.yaml")) frameworks.add("Flutter");
645
- if (fileExists("go.mod")) frameworks.add("Go Modules");
646
- if (fileExists("Cargo.toml")) frameworks.add("Rust Cargo");
647
- if (fileExists("requirements.txt") || fileExists("pyproject.toml")) frameworks.add("Python");
906
+ if (pubspecFiles.length > 0) frameworks.add("Flutter");
907
+ if (goModFiles.length > 0) frameworks.add("Go Modules");
908
+ if (cargoTomlFiles.length > 0) frameworks.add("Rust Cargo");
909
+ if (requirementsFiles.length > 0 || pyprojectFiles.length > 0) frameworks.add("Python");
648
910
 
649
- if (existsSync(packageJsonPath)) {
911
+ for (const packageJsonFile of packageJsonFiles) {
650
912
  try {
651
- const parsed = JSON.parse(await readFile(packageJsonPath, "utf8"));
652
- const scripts = parsed.scripts && typeof parsed.scripts === "object" ? parsed.scripts : {};
653
- for (const [name, command] of Object.entries(scripts)) {
654
- if (typeof command !== "string") continue;
655
- packageScripts.set(name, command);
656
- }
657
-
913
+ const parsed = JSON.parse(await readFile(packageJsonFile, "utf8"));
658
914
  const deps = {
659
915
  ...(parsed.dependencies || {}),
660
916
  ...(parsed.devDependencies || {}),
661
- ...(parsed.peerDependencies || {})
917
+ ...(parsed.peerDependencies || {}),
918
+ ...(parsed.optionalDependencies || {})
662
919
  };
663
- const depNames = new Set(Object.keys(deps));
664
- const frameworkSignals = [
665
- ["next", "Next.js"],
666
- ["react", "React"],
667
- ["vue", "Vue"],
668
- ["nuxt", "Nuxt"],
669
- ["svelte", "Svelte"],
670
- ["@nestjs/core", "NestJS"],
671
- ["express", "Express"],
672
- ["fastify", "Fastify"],
673
- ["hono", "Hono"],
674
- ["tailwindcss", "Tailwind CSS"],
675
- ["prisma", "Prisma"],
676
- ["drizzle-orm", "Drizzle ORM"],
677
- ["mongoose", "Mongoose"],
678
- ["typeorm", "TypeORM"],
679
- ["@playwright/test", "Playwright"],
680
- ["vitest", "Vitest"],
681
- ["jest", "Jest"],
682
- ["cypress", "Cypress"]
683
- ];
684
- for (const [signal, label] of frameworkSignals) {
685
- if (depNames.has(signal)) frameworks.add(label);
920
+ for (const depName of Object.keys(deps)) {
921
+ const normalized = normalizeTechPackageName(depName);
922
+ if (normalized) javascriptPackages.add(normalized);
923
+ }
924
+
925
+ if (path.resolve(packageJsonFile) === path.resolve(rootPackageJsonPath)) {
926
+ const scripts = parsed.scripts && typeof parsed.scripts === "object" ? parsed.scripts : {};
927
+ for (const [name, command] of Object.entries(scripts)) {
928
+ if (typeof command !== "string") continue;
929
+ packageScripts.set(name, command);
930
+ }
686
931
  }
687
932
  } catch {
688
933
  // ignore malformed package.json
689
934
  }
690
935
  }
691
936
 
937
+ for (const pubspecFile of pubspecFiles) {
938
+ try {
939
+ const content = await readFile(pubspecFile, "utf8");
940
+ for (const packageName of parsePubspecDependencyNames(content)) {
941
+ dartPackages.add(packageName);
942
+ }
943
+ } catch {
944
+ // ignore malformed pubspec.yaml
945
+ }
946
+ }
947
+
948
+ for (const goModFile of goModFiles) {
949
+ try {
950
+ const content = await readFile(goModFile, "utf8");
951
+ for (const moduleName of parseGoModuleNames(content)) {
952
+ goModules.add(moduleName);
953
+ }
954
+ } catch {
955
+ // ignore unreadable go.mod
956
+ }
957
+ }
958
+
959
+ for (const pyprojectFile of pyprojectFiles) {
960
+ try {
961
+ const content = await readFile(pyprojectFile, "utf8");
962
+ for (const packageName of parsePyprojectPackageNames(content)) {
963
+ pythonPackages.add(packageName);
964
+ }
965
+ } catch {
966
+ // ignore malformed pyproject.toml
967
+ }
968
+ }
969
+
970
+ for (const requirementsFile of requirementsFiles) {
971
+ try {
972
+ const content = await readFile(requirementsFile, "utf8");
973
+ for (const packageName of parseRequirementsPackageNames(content)) {
974
+ pythonPackages.add(packageName);
975
+ }
976
+ } catch {
977
+ // ignore unreadable requirements file
978
+ }
979
+ }
980
+
981
+ for (const cargoTomlFile of cargoTomlFiles) {
982
+ try {
983
+ const content = await readFile(cargoTomlFile, "utf8");
984
+ for (const crateName of parseCargoCrateNames(content)) {
985
+ rustCrates.add(crateName);
986
+ }
987
+ } catch {
988
+ // ignore malformed Cargo.toml
989
+ }
990
+ }
991
+
992
+ addFrameworkSignalsFromPackages({
993
+ packages: javascriptPackages,
994
+ frameworks,
995
+ signals: TECH_JS_FRAMEWORK_SIGNALS
996
+ });
997
+ addFrameworkSignalsFromPackages({
998
+ packages: dartPackages,
999
+ frameworks,
1000
+ signals: TECH_DART_FRAMEWORK_SIGNALS
1001
+ });
1002
+ addFrameworkSignalsFromPackages({
1003
+ packages: goModules,
1004
+ frameworks,
1005
+ signals: TECH_GO_FRAMEWORK_SIGNALS
1006
+ });
1007
+ addFrameworkSignalsFromPackages({
1008
+ packages: pythonPackages,
1009
+ frameworks,
1010
+ signals: TECH_PYTHON_FRAMEWORK_SIGNALS
1011
+ });
1012
+ addFrameworkSignalsFromPackages({
1013
+ packages: rustCrates,
1014
+ frameworks,
1015
+ signals: TECH_RUST_FRAMEWORK_SIGNALS
1016
+ });
1017
+
692
1018
  const sortedLanguages = [...languageCounts.entries()].sort((a, b) => b[1] - a[1]);
693
1019
  const sortedFrameworks = [...frameworks].sort((a, b) => a.localeCompare(b));
694
1020
  const sortedTopDirs = [...topDirs].sort((a, b) => a.localeCompare(b)).slice(0, 12);
@@ -717,10 +1043,33 @@ async function collectTechSnapshot(rootDir) {
717
1043
  frameworks: sortedFrameworks,
718
1044
  lockfiles: sortedLockfiles,
719
1045
  topDirs: sortedTopDirs,
720
- keyScripts
1046
+ keyScripts,
1047
+ packageSignals: {
1048
+ javascript: toSortedArray(javascriptPackages),
1049
+ dart: toSortedArray(dartPackages),
1050
+ go: toSortedArray(goModules),
1051
+ python: toSortedArray(pythonPackages),
1052
+ rust: toSortedArray(rustCrates)
1053
+ }
721
1054
  };
722
1055
  }
723
1056
 
1057
+ function appendTechPackageSection(lines, heading, packages) {
1058
+ lines.push(`### ${heading}`);
1059
+ if (!packages || packages.length === 0) {
1060
+ lines.push("- None detected.");
1061
+ } else {
1062
+ const preview = packages.slice(0, TECH_PACKAGE_PREVIEW_LIMIT);
1063
+ for (const packageName of preview) {
1064
+ lines.push(`- \`${packageName}\``);
1065
+ }
1066
+ if (packages.length > TECH_PACKAGE_PREVIEW_LIMIT) {
1067
+ lines.push(`- ... (+${packages.length - TECH_PACKAGE_PREVIEW_LIMIT} more)`);
1068
+ }
1069
+ }
1070
+ lines.push("");
1071
+ }
1072
+
724
1073
  function buildTechMd(snapshot) {
725
1074
  const lines = [];
726
1075
  lines.push("# TECH.md");
@@ -750,6 +1099,13 @@ function buildTechMd(snapshot) {
750
1099
  }
751
1100
  lines.push("");
752
1101
 
1102
+ lines.push("## Package Signals");
1103
+ appendTechPackageSection(lines, "JavaScript / TypeScript (package.json)", snapshot.packageSignals.javascript);
1104
+ appendTechPackageSection(lines, "Dart / Flutter (pubspec.yaml)", snapshot.packageSignals.dart);
1105
+ appendTechPackageSection(lines, "Go Modules (go.mod)", snapshot.packageSignals.go);
1106
+ appendTechPackageSection(lines, "Python Packages (requirements / pyproject)", snapshot.packageSignals.python);
1107
+ appendTechPackageSection(lines, "Rust Crates (Cargo.toml)", snapshot.packageSignals.rust);
1108
+
753
1109
  lines.push("## Tooling and Lockfiles");
754
1110
  if (snapshot.lockfiles.length === 0) {
755
1111
  lines.push("- No lockfiles detected.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cubis/foundry",
3
- "version": "0.3.14",
3
+ "version": "0.3.16",
4
4
  "description": "Cubis Foundry CLI for workflow-first AI agent environments",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,6 +18,7 @@
18
18
  "check": "node bin/cubis.js --help",
19
19
  "test:attributes": "node scripts/validate-platform-attributes.mjs",
20
20
  "test:attributes:strict": "node scripts/validate-platform-attributes.mjs --strict",
21
+ "test:tech-md": "node scripts/test-tech-md-scanner.mjs",
21
22
  "test:smoke": "bash scripts/smoke-workflows.sh",
22
23
  "test:all": "npm run test:attributes && npm run test:smoke",
23
24
  "test:all:strict": "npm run test:attributes:strict && npm run test:smoke",