claudeos-core 2.1.0 → 2.2.0

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 (35) hide show
  1. package/CHANGELOG.md +456 -0
  2. package/README.de.md +33 -39
  3. package/README.es.md +33 -39
  4. package/README.fr.md +33 -39
  5. package/README.hi.md +33 -39
  6. package/README.ja.md +33 -39
  7. package/README.ko.md +37 -43
  8. package/README.md +37 -43
  9. package/README.ru.md +35 -39
  10. package/README.vi.md +33 -39
  11. package/README.zh-CN.md +33 -39
  12. package/bin/commands/init.js +81 -45
  13. package/content-validator/index.js +6 -1
  14. package/lib/env-parser.js +317 -0
  15. package/lib/memory-scaffold.js +7 -5
  16. package/package.json +1 -1
  17. package/pass-prompts/templates/angular/pass3.md +28 -13
  18. package/pass-prompts/templates/common/claude-md-scaffold.md +644 -0
  19. package/pass-prompts/templates/common/pass3-footer.md +185 -0
  20. package/pass-prompts/templates/common/pass4.md +6 -3
  21. package/pass-prompts/templates/common/staging-override.md +1 -1
  22. package/pass-prompts/templates/java-spring/pass3.md +31 -21
  23. package/pass-prompts/templates/kotlin-spring/pass3.md +34 -22
  24. package/pass-prompts/templates/node-express/pass3.md +30 -21
  25. package/pass-prompts/templates/node-fastify/pass3.md +28 -14
  26. package/pass-prompts/templates/node-nestjs/pass3.md +29 -14
  27. package/pass-prompts/templates/node-nextjs/pass3.md +34 -21
  28. package/pass-prompts/templates/node-vite/pass3.md +30 -13
  29. package/pass-prompts/templates/python-django/pass3.md +32 -21
  30. package/pass-prompts/templates/python-fastapi/pass3.md +33 -21
  31. package/pass-prompts/templates/python-flask/pass3.md +31 -13
  32. package/pass-prompts/templates/vue-nuxt/pass3.md +32 -13
  33. package/plan-installer/index.js +8 -0
  34. package/plan-installer/prompt-generator.js +18 -1
  35. package/plan-installer/stack-detector.js +16 -0
@@ -495,14 +495,14 @@ async function runPass3Split(ctx) {
495
495
 
496
496
  // ═══ Stage 3b: CLAUDE.md + standard/ + .claude/rules/ ═══════════
497
497
  //
498
- // 단일 배치 (도메인 ≤ 15): 기존 "3b" marker 유지 (backward-compatible).
499
- // 다중 배치 (도메인 > 15): "3b-core" 먼저 실행 "3b-1", "3b-2", ...
498
+ // Single batch (domains ≤ 15): keep legacy "3b" marker (backward-compatible).
499
+ // Multi-batch (domains > 15): run "3b-core" first, then "3b-1", "3b-2", ...
500
500
  //
501
- // 3b-core 분리 이유: 다중 배치의 배치가 "CLAUDE.md + 공통 standard
502
- // + 15 도메인"을 세션에 모두 처리하면 단일 스테이지 부하가 ~70-80
503
- // 파일까지 치솟음. 관측된 overflow 임계(약 40 파일)보다 2배 가까이 큼.
504
- // 공통 파일을 별도 스테이지로 빼서 스테이지가 ~50 파일 이하로
505
- // 유지되도록 보장.
501
+ // Rationale for splitting 3b-core: if the first multi-batch stage handled
502
+ // "CLAUDE.md + common standards + 15 domains" in a single session, that
503
+ // single stage would hit ~70-80 files close to 2x the observed overflow
504
+ // threshold (~40 files). Splitting the common files into their own stage
505
+ // keeps every stage under ~50 files.
506
506
  if (isBatched) {
507
507
  await runStage("3b-core", "core common files (CLAUDE.md + common standard + common rules)",
508
508
  buildStageCorePrompt("3b", batches),
@@ -522,16 +522,16 @@ async function runPass3Split(ctx) {
522
522
  problems.push("claudeos-core/standard/00.core/01.project-overview.md is empty");
523
523
  }
524
524
  }
525
- // 3b-core 공통 rules 생성 — rules 카운트 검증은 3b-N에서.
525
+ // 3b-core generates only common rules — rules-count validation happens in 3b-N.
526
526
  return problems;
527
527
  },
528
528
  }
529
529
  );
530
530
  }
531
531
 
532
- // 도메인별 배치 루프.
533
- // 단일 배치: stageId "3b", 공통 파일 포함 (기존 동작).
534
- // 다중 배치: stageId "3b-1", "3b-2", ..., 공통 파일은 3b-core에서 이미 처리됨.
532
+ // Per-domain batch loop.
533
+ // Single batch: stageId "3b", common files included (legacy behavior).
534
+ // Multi-batch: stageId "3b-1", "3b-2", ..., common files already handled in 3b-core.
535
535
  for (let bi = 0; bi < batches.length; bi++) {
536
536
  const batchDomains = batches[bi];
537
537
  const stageId = isBatched ? `3b-${bi + 1}` : "3b";
@@ -539,8 +539,8 @@ async function runPass3Split(ctx) {
539
539
  ? `domain batch ${bi + 1}/${batches.length} (${batchDomains.length} domains)`
540
540
  : "core files (CLAUDE.md + standard + rules)";
541
541
 
542
- // 배치별 프롬프트: 원래 3b 헤더에 이번 배치에서만 처리할 도메인 목록 주입.
543
- // 다중 배치에서는 모든 배치가 "도메인 특화 파일만" 생성 (공통 파일은 3b-core에서 처리됨).
542
+ // Per-batch prompt: inject into the original 3b header the list of domains scoped to this batch.
543
+ // In multi-batch mode every batch generates "domain-specific files only" (common files handled in 3b-core).
544
544
  const batchScopeNote = isBatched
545
545
  ? buildBatchScopeNote("3b", bi, batches.length, batchDomains)
546
546
  : "";
@@ -553,7 +553,7 @@ async function runPass3Split(ctx) {
553
553
  expectsStagedRules: true,
554
554
  validate: () => {
555
555
  const problems = [];
556
- // 단일 배치: 공통 파일 검증 (3b-core 없으므로 여기서 체크).
556
+ // Single batch: validate common files here (no 3b-core exists).
557
557
  if (!isBatched) {
558
558
  if (!fileExists(claudeMdPath)) {
559
559
  problems.push("CLAUDE.md was not created");
@@ -568,7 +568,7 @@ async function runPass3Split(ctx) {
568
568
  }
569
569
  }
570
570
  }
571
- // 모든 배치에서 rules/ 생성 확인 (최소 1개는 staged-rules 통과해야 )
571
+ // For every batch, confirm rules/ was generated (at least one staged-rules move must succeed).
572
572
  const rulesDir = path.join(PROJECT_ROOT, ".claude/rules");
573
573
  const rulesCount = countFilesRecursive(rulesDir);
574
574
  if (rulesCount === 0) {
@@ -581,11 +581,11 @@ async function runPass3Split(ctx) {
581
581
 
582
582
  // ═══ Stage 3c: skills/ + guide/ ═══════════════════════════════
583
583
  //
584
- // 단일 배치 (도메인 ≤ 15): 기존 "3c" marker 유지 (guide + skills 함께).
585
- // 다중 배치 (도메인 > 15): "3c-core" 먼저 실행 "3c-1", "3c-2", ...
584
+ // Single batch (domains ≤ 15): keep legacy "3c" marker (guide + skills together).
585
+ // Multi-batch (domains > 15): run "3c-core" first, then "3c-1", "3c-2", ...
586
586
  //
587
- // 3c-core 분리 이유: guide 9개 + 공통 skills 도메인 무관하게 고정.
588
- // 도메인 배치와 섞이면 배치 부하가 다른 배치보다 커짐.
587
+ // Rationale for splitting 3c-core: the 9 guide files + common skills are fixed regardless of domain count.
588
+ // Mixing them into domain batches makes the first batch heavier than the others.
589
589
  if (isBatched) {
590
590
  await runStage("3c-core", "common guides and shared skills",
591
591
  buildStageCorePrompt("3c", batches),
@@ -604,16 +604,16 @@ async function runPass3Split(ctx) {
604
604
  for (const g of missingGuides) {
605
605
  problems.push(`claudeos-core/guide/${g} missing or empty`);
606
606
  }
607
- // skills/ 최종 검증은 마지막 도메인 배치에서 수행.
607
+ // Final skills/ validation is performed in the last domain batch.
608
608
  return problems;
609
609
  },
610
610
  }
611
611
  );
612
612
  }
613
613
 
614
- // 도메인별 skills 배치 루프.
615
- // 단일 배치: guide + skills 함께 (기존 동작).
616
- // 다중 배치: skills만, guide 이미 3c-core에서 처리됨.
614
+ // Per-domain skills batch loop.
615
+ // Single batch: guide + skills together (legacy behavior).
616
+ // Multi-batch: skills only; guide was already handled in 3c-core.
617
617
  for (let bi = 0; bi < batches.length; bi++) {
618
618
  const batchDomains = batches[bi];
619
619
  const stageId = isBatched ? `3c-${bi + 1}` : "3c";
@@ -633,7 +633,7 @@ async function runPass3Split(ctx) {
633
633
  expectsStagedRules: true, // skills occasionally include rule files
634
634
  validate: () => {
635
635
  const problems = [];
636
- // 단일 배치: guide 검증도 여기서 수행 (3c-core가 없으니까).
636
+ // Single batch: validate guide here as well (no 3c-core).
637
637
  if (!isBatched) {
638
638
  const guideDir = path.join(PROJECT_ROOT, "claudeos-core/guide");
639
639
  const missingGuides = EXPECTED_GUIDE_FILES.filter(g => {
@@ -647,7 +647,7 @@ async function runPass3Split(ctx) {
647
647
  problems.push(`claudeos-core/guide/${g} missing or empty`);
648
648
  }
649
649
  }
650
- // Skills 최종 검증: 단일 배치 / 마지막 배치에서만 전체 검증.
650
+ // Final skills validation: full check only in the single-batch case or the last multi-batch.
651
651
  if (!isBatched || bi === batches.length - 1) {
652
652
  const { hasNonEmptyMdRecursive } = require("../../lib/expected-outputs");
653
653
  const skillsDir = path.join(PROJECT_ROOT, "claudeos-core/skills");
@@ -662,16 +662,16 @@ async function runPass3Split(ctx) {
662
662
 
663
663
  // ═══ Stage 3d: plan/ + database/ + mcp-guide/ ═════════════════
664
664
  //
665
- // 3d 원래 standard/rules/skills/guide master plan 으로 집계하고
666
- // database/mcp-guide stub 만들던 스테이지였다. 하지만 master plan 자체는
667
- // Claude Code 런타임에서 읽히지 않는 도구 내부 백업/관리용 파일이었고,
668
- // 도메인 수가 많아지면 단일 세션 집계에서 Prompt is too long 발생 원인이
669
- // 됐다 (18 도메인 실측에서 3d-standard 32 파일 집계 시점에 실패).
670
- // master plan 생성을 중단하는 것이 안정성 측면에서 올바른
671
- // 결정이며, 필요시 사용자가 직접 스크립트로 집계 가능하다.
665
+ // 3d used to aggregate standard/rules/skills/guide into a master plan
666
+ // and generate database/mcp-guide stubs. But the master plan itself was
667
+ // an internal backup/management file never loaded by Claude Code at runtime,
668
+ // and at high domain counts the single-session aggregation caused "Prompt is
669
+ // too long" failures (at 18 domains, 3d-standard failed at the 32-file mark).
670
+ // Dropping master plan generation is the correct call for stability, and
671
+ // users can aggregate manually with their own script when needed.
672
672
  //
673
- // 결과적으로 3d aux 만 남음:
674
- // 3d-aux → database/ + mcp-guide/ (프로젝트 특성 설명 stub)
673
+ // As a result, 3d only keeps the aux stage:
674
+ // 3d-aux → database/ + mcp-guide/ (project-specific stub descriptions)
675
675
 
676
676
  // 3d-aux: database/ + mcp-guide/ (absence is warning-level)
677
677
  await runStage("3d-aux", "aux docs (database + mcp-guide)",
@@ -690,10 +690,10 @@ async function runPass3Split(ctx) {
690
690
  ` Check disk space / permissions on ${GENERATED_DIR}/.`
691
691
  );
692
692
  }
693
- // 스테이지 수 계산
694
- // 3a (1) + 3b (단일 1 or core+N) + 3c (단일 1 or core+N) + 3d-aux (1)
695
- // 단일 배치: 1 + 1 + 1 + 1 = 4
696
- // 다중 배치: 1 + (1 + N) + (1 + N) + 1 = 2N + 4
693
+ // Total stage count
694
+ // 3a (1) + 3b (1 single or core+N) + 3c (1 single or core+N) + 3d-aux (1)
695
+ // Single batch: 1 + 1 + 1 + 1 = 4
696
+ // Multi-batch: 1 + (1 + N) + (1 + N) + 1 = 2N + 4
697
697
  const three3dStages = 1; // aux only (master plan aggregation removed)
698
698
  const totalStages = isBatched
699
699
  ? (1 + 1 + batches.length + 1 + batches.length + three3dStages)
@@ -791,6 +791,42 @@ async function cmdInit(parsedArgs) {
791
791
  wasFreshClean = true;
792
792
  log(" 🔄 Previous results deleted (--force)\n");
793
793
  } else {
794
+ // v2.2.0 upgrade detection: if project was generated with older claudeos-core
795
+ // (pre-2.2.0), default "resume" mode will skip regeneration of existing files
796
+ // per Rule B idempotency, meaning v2.2.0 structural improvements will NOT be
797
+ // picked up. Detect this case by checking CLAUDE.md for v2.2.0 markers.
798
+ const claudeMd = path.join(PROJECT_ROOT, "CLAUDE.md");
799
+ if (fileExists(claudeMd)) {
800
+ try {
801
+ const content = fs.readFileSync(claudeMd, "utf-8");
802
+ // v2.2.0 scaffold enforces EXACTLY 8 top-level `##` sections.
803
+ // Pre-v2.2.0 CLAUDE.md files typically carry 9+ sections (extra
804
+ // "Rules Summary" / "Common Rules" / "Required to Observe"
805
+ // blocks that v2.2.0 forbids). Counting `^## ` headings is a
806
+ // language-independent heuristic that works across all 10
807
+ // supported output languages. False positive (an existing
808
+ // 8-section pre-v2.2.0 CLAUDE.md) is acceptable — the user
809
+ // simply won't see the upgrade warning and can still run
810
+ // `--force` manually.
811
+ const sectionCount = (content.match(/^## /gm) || []).length;
812
+ const hasV220Section8 = sectionCount === 8;
813
+ if (!hasV220Section8) {
814
+ log("\n ⚠️ v2.2.0 upgrade detected");
815
+ log(" ─────────────────────────");
816
+ log(" Your existing CLAUDE.md was generated with an older claudeos-core version.");
817
+ log(" v2.2.0 introduces structural changes that the default 'resume' mode");
818
+ log(" CANNOT apply because existing files are preserved under Rule B (idempotency).");
819
+ log("");
820
+ log(" To fully adopt v2.2.0, choose one of:");
821
+ log(" 1. Rerun with --force: npx claudeos-core init --force");
822
+ log(" (overwrites generated files; your memory/ content is preserved)");
823
+ log(" 2. Choose 'fresh' below (equivalent to --force)");
824
+ log("");
825
+ log(" See CHANGELOG.md Migration section for full details.\n");
826
+ }
827
+ } catch (_) { /* Read error is non-fatal; proceed to resume prompt */ }
828
+ }
829
+
794
830
  const status = { pass1Done: existingPass1.length, pass2Done: pass2Exists };
795
831
  const mode = await selectResumeMode(lang, status);
796
832
  if (!mode) throw new InitError("Cancelled.");
@@ -1143,12 +1179,12 @@ async function cmdInit(parsedArgs) {
1143
1179
  }
1144
1180
  }
1145
1181
  if (fileExists(pass3Marker)) {
1146
- // v2.1.1: split-mode partial marker 보호.
1147
- // { mode: "split", groupsCompleted: [...], completedAt: undefined } 형태의
1148
- // 중간 진행 마커는 stale 판정하면 됨. 3b까지만 완료된 정상 상태에서도
1149
- // guide/skills/plan 비어있어서 기존 로직이 잘못 stale 판정하고 marker를
1150
- // 삭제하면, runPass3Split의 resume 로직이 완료된 스테이지를 읽고
1151
- // 3a부터 전체 재실행하게 (correctness는 OK이지만 토큰 2배 낭비).
1182
+ // v2.1.1: protect split-mode partial markers.
1183
+ // An in-progress marker of the form { mode: "split", groupsCompleted: [...], completedAt: undefined }
1184
+ // must NOT be treated as stale. When the pipeline has normally completed
1185
+ // only through 3b, guide/skills/plan are empty, so the legacy check
1186
+ // falsely flagged the marker as stale. Deleting it made runPass3Split
1187
+ // lose track of completed stages and re-run from 3a (correct but wastes ~2x tokens).
1152
1188
  let markerIsSplitPartial = false;
1153
1189
  try {
1154
1190
  const parsed = JSON.parse(readFile(pass3Marker));
@@ -1158,7 +1194,7 @@ async function cmdInit(parsedArgs) {
1158
1194
  Array.isArray(parsed.groupsCompleted) &&
1159
1195
  !parsed.completedAt;
1160
1196
  } catch (_e) {
1161
- // malformed marker → stale check 넘어감 (이전 동작 유지)
1197
+ // malformed marker → fall through to the stale check (legacy behavior)
1162
1198
  }
1163
1199
 
1164
1200
  if (markerIsSplitPartial) {
@@ -241,6 +241,11 @@ async function main() {
241
241
  }
242
242
 
243
243
  // ─── 6. claudeos-core/plan/** ──────────────────────────
244
+ // v2.1.0+ removed master plan generation; plan/ is optional and is not created
245
+ // during fresh init. If the directory exists (legacy projects, user-authored
246
+ // plan files), we still validate its contents. If it is absent, that is the
247
+ // expected state post-v2.1.0 — do not push a MISSING error (parallel to
248
+ // plan-validator / manifest-generator which were already updated in v2.1.0).
244
249
  console.log(" [6/9] claudeos-core/plan/...");
245
250
  if (fs.existsSync(PLAN_DIR)) {
246
251
  const planFiles = await glob("*.md", { cwd: PLAN_DIR, absolute: true });
@@ -261,7 +266,7 @@ async function main() {
261
266
  }
262
267
  console.log(` ${planFiles.length} files checked`);
263
268
  } else {
264
- errors.push({ file: "claudeos-core/plan/", type: "MISSING", msg: "plan directory not found" });
269
+ console.log(" ⏭️ plan/ not present (expected post-v2.1.0)");
265
270
  }
266
271
 
267
272
  // ─── 7. claudeos-core/database/** ──────────────────────
@@ -0,0 +1,317 @@
1
+ /**
2
+ * env-parser.js — Parse .env* files for factual project configuration.
3
+ *
4
+ * WHY THIS EXISTS:
5
+ * claudeos-core's "LLMs guess, code confirms" principle requires that
6
+ * factual project data (ports, hosts, API targets) be extracted from
7
+ * declarative sources the project itself maintains, not guessed from
8
+ * framework defaults. `.env.example` is such a source — it's the
9
+ * canonical declaration of a project's runtime configuration surface.
10
+ *
11
+ * Historically, stack-detector only parsed .env for DATABASE_URL to
12
+ * identify the DB. Everything else (ports, hosts, API endpoints) fell
13
+ * back to hardcoded framework defaults (e.g., Vite → 5173), which
14
+ * silently produced wrong values whenever a project customized its
15
+ * configuration via .env. This utility closes that gap.
16
+ *
17
+ * SEARCH ORDER:
18
+ * `.env.example` is preferred over actual `.env` files because it is
19
+ * the shape-of-truth committed to VCS: developer-neutral, reflecting
20
+ * the project's intended configuration surface, not one contributor's
21
+ * local overrides.
22
+ */
23
+
24
+ "use strict";
25
+
26
+ const path = require("path");
27
+ const { readFileSafe, existsSafe } = require("./safe-fs");
28
+
29
+ // Search order: public-facing → developer-specific → runtime-specific.
30
+ // .env.example is canonical because it's the committed, intended config.
31
+ const ENV_FILE_ORDER = [
32
+ ".env.example",
33
+ ".env.local.example",
34
+ ".env.development.example",
35
+ ".env.sample",
36
+ ".env.template",
37
+ ".env",
38
+ ".env.local",
39
+ ".env.development",
40
+ ];
41
+
42
+ // Port variable name conventions across frameworks.
43
+ // Ordered by specificity — more specific wins when multiple are present.
44
+ const PORT_VAR_KEYS = [
45
+ // Vite-specific common patterns
46
+ "VITE_PORT",
47
+ "VITE_DEV_PORT",
48
+ "VITE_DEV_SERVER_PORT",
49
+ "VITE_DESKTOP_PORT",
50
+ // Next.js
51
+ "NEXT_PUBLIC_PORT",
52
+ "NEXT_PORT",
53
+ // Nuxt
54
+ "NUXT_PORT",
55
+ "NUXT_PUBLIC_PORT",
56
+ // Angular
57
+ "NG_PORT",
58
+ "NG_DEV_PORT",
59
+ // Node / backend frameworks
60
+ "APP_PORT",
61
+ "SERVER_PORT",
62
+ "HTTP_PORT",
63
+ "DEV_PORT",
64
+ // Python
65
+ "FLASK_RUN_PORT",
66
+ "UVICORN_PORT",
67
+ "DJANGO_PORT",
68
+ // Generic last — lowest priority because "PORT" collides with too many things
69
+ "PORT",
70
+ ];
71
+
72
+ // Host variable conventions.
73
+ const HOST_VAR_KEYS = [
74
+ "VITE_DEV_HOST",
75
+ "VITE_HOST",
76
+ "NEXT_PUBLIC_HOST",
77
+ "NUXT_HOST",
78
+ "APP_HOST",
79
+ "SERVER_HOST",
80
+ "HTTP_HOST",
81
+ "HOST",
82
+ ];
83
+
84
+ // API target / backend proxy conventions.
85
+ const API_TARGET_VAR_KEYS = [
86
+ "VITE_API_TARGET",
87
+ "VITE_API_URL",
88
+ "VITE_API_BASE_URL",
89
+ "NEXT_PUBLIC_API_URL",
90
+ "NEXT_PUBLIC_API_BASE_URL",
91
+ "NUXT_PUBLIC_API_BASE",
92
+ "API_TARGET",
93
+ "API_URL",
94
+ "API_BASE_URL",
95
+ "BACKEND_URL",
96
+ "PROXY_TARGET",
97
+ ];
98
+
99
+ /**
100
+ * Parse .env-style file content into a flat key-value object.
101
+ * Handles: KEY=VALUE, quoted values, inline comments, blank lines, export prefix.
102
+ * Does NOT expand ${VAR} interpolation — we keep raw declared values.
103
+ */
104
+ function parseEnvContent(content) {
105
+ if (!content || typeof content !== "string") return {};
106
+ const result = {};
107
+ const lines = content.split(/\r?\n/);
108
+ for (const rawLine of lines) {
109
+ let line = rawLine.trim();
110
+ if (!line) continue;
111
+ if (line.startsWith("#")) continue;
112
+ // Strip `export` prefix (common in shell-sourced env files)
113
+ if (line.startsWith("export ")) line = line.slice(7).trim();
114
+ const eq = line.indexOf("=");
115
+ if (eq === -1) continue;
116
+ const key = line.slice(0, eq).trim();
117
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
118
+ let value = line.slice(eq + 1).trim();
119
+ // Strip surrounding single or double quotes
120
+ if (
121
+ (value.startsWith('"') && value.endsWith('"') && value.length >= 2) ||
122
+ (value.startsWith("'") && value.endsWith("'") && value.length >= 2)
123
+ ) {
124
+ value = value.slice(1, -1);
125
+ } else {
126
+ // Strip inline comment (only on unquoted values)
127
+ const hashIdx = value.indexOf(" #");
128
+ if (hashIdx !== -1) value = value.slice(0, hashIdx).trim();
129
+ }
130
+ result[key] = value;
131
+ }
132
+ return result;
133
+ }
134
+
135
+ /**
136
+ * Locate the most authoritative env file in a project root.
137
+ * Returns the absolute path, or null if none found.
138
+ */
139
+ function findPrimaryEnvFile(root) {
140
+ for (const name of ENV_FILE_ORDER) {
141
+ const p = path.join(root, name);
142
+ if (existsSafe(p)) return p;
143
+ }
144
+ return null;
145
+ }
146
+
147
+ /**
148
+ * Read and parse the primary env file. Returns { file, vars } or null.
149
+ */
150
+ function readPrimaryEnv(root) {
151
+ const file = findPrimaryEnvFile(root);
152
+ if (!file) return null;
153
+ const content = readFileSafe(file);
154
+ if (!content) return null;
155
+ return {
156
+ file: path.basename(file),
157
+ vars: parseEnvContent(content),
158
+ };
159
+ }
160
+
161
+ /**
162
+ * Extract a port value from parsed env vars. Returns integer or null.
163
+ * First match by PORT_VAR_KEYS ordering wins.
164
+ */
165
+ function extractPort(vars) {
166
+ if (!vars) return null;
167
+ for (const key of PORT_VAR_KEYS) {
168
+ if (key in vars) {
169
+ const n = parseInt(vars[key], 10);
170
+ if (!Number.isNaN(n) && n > 0 && n < 65536) return n;
171
+ }
172
+ }
173
+ return null;
174
+ }
175
+
176
+ /**
177
+ * Extract a host value from parsed env vars. Returns string or null.
178
+ */
179
+ function extractHost(vars) {
180
+ if (!vars) return null;
181
+ for (const key of HOST_VAR_KEYS) {
182
+ if (key in vars && vars[key]) return vars[key];
183
+ }
184
+ return null;
185
+ }
186
+
187
+ /**
188
+ * Extract an API target URL from parsed env vars. Returns string or null.
189
+ */
190
+ function extractApiTarget(vars) {
191
+ if (!vars) return null;
192
+ for (const key of API_TARGET_VAR_KEYS) {
193
+ if (key in vars && vars[key]) return vars[key];
194
+ }
195
+ return null;
196
+ }
197
+
198
+ /**
199
+ * Sensitive variable name patterns. env vars matching any of these patterns
200
+ * are redacted from the `vars` map returned to downstream consumers
201
+ * (stack-detector, prompt-generator, CLAUDE.md scaffold).
202
+ *
203
+ * Even though `.env.example` is conventionally a placeholder file committed
204
+ * to VCS (and should not contain real secrets), projects occasionally check
205
+ * in real values by mistake. claudeos-core piping those values into
206
+ * CLAUDE.md would amplify the leak — CLAUDE.md is committed, shared, and
207
+ * potentially published as part of open-source documentation.
208
+ *
209
+ * Redaction strategy:
210
+ * - Matching keys are kept in the map so consumers can still detect
211
+ * "this variable exists" (e.g., "project declares an API_KEY env var").
212
+ * - Values are replaced with the sentinel string "***REDACTED***".
213
+ * - extractPort / extractHost / extractApiTarget already scan only a
214
+ * whitelist of config-relevant keys (PORT, HOST, API_TARGET, etc.)
215
+ * so sensitive keys cannot leak through those paths regardless of
216
+ * this filter.
217
+ *
218
+ * Patterns are case-insensitive substring matches against the variable name.
219
+ */
220
+ const SENSITIVE_VAR_PATTERNS = [
221
+ /password/i,
222
+ /passwd/i,
223
+ /secret/i,
224
+ /api[_-]?key/i,
225
+ /access[_-]?key/i,
226
+ /private[_-]?key/i,
227
+ /auth[_-]?token/i,
228
+ /token/i, // matches TOKEN, AUTH_TOKEN, GIT_TOKEN, NPM_TOKEN
229
+ // (underscore is a word character in regex \b,
230
+ // so \btoken\b fails to match "_TOKEN" suffix)
231
+ /credential/i,
232
+ /bearer/i,
233
+ /\bsalt\b/i,
234
+ /encryption[_-]?key/i,
235
+ /cert(ificate)?[_-]?key/i,
236
+ /secret[_-]?key/i,
237
+ /client[_-]?secret/i,
238
+ /session[_-]?secret/i,
239
+ /jwt[_-]?secret/i,
240
+ ];
241
+
242
+ /**
243
+ * Returns true if the given env var name matches any sensitive pattern.
244
+ */
245
+ function isSensitiveVarName(name) {
246
+ if (!name || typeof name !== "string") return false;
247
+ return SENSITIVE_VAR_PATTERNS.some(re => re.test(name));
248
+ }
249
+
250
+ /**
251
+ * Redacts sensitive values in an env vars map. Returns a new object;
252
+ * original is not mutated. Preserves keys so "variable exists" signal
253
+ * is kept, but replaces values with a sentinel string.
254
+ *
255
+ * Whitelist exception: DATABASE_URL is kept as-is because stack-detector's
256
+ * db-identification path has always used it and existing project-analysis
257
+ * consumers depend on reading it. (The DB URL contains credentials, but
258
+ * this has been the established behavior since v1.x and changing it would
259
+ * be a breaking change. Downstream consumers that write CLAUDE.md content
260
+ * from vars should still redact it at their layer.)
261
+ */
262
+ function redactSensitiveVars(vars) {
263
+ if (!vars || typeof vars !== "object") return vars;
264
+ const out = {};
265
+ for (const [k, v] of Object.entries(vars)) {
266
+ if (k === "DATABASE_URL") {
267
+ out[k] = v; // documented whitelist for stack-detector back-compat
268
+ } else if (isSensitiveVarName(k)) {
269
+ out[k] = "***REDACTED***";
270
+ } else {
271
+ out[k] = v;
272
+ }
273
+ }
274
+ return out;
275
+ }
276
+
277
+ /**
278
+ * Top-level convenience: read the project's env file and produce the
279
+ * stack.envInfo object consumed by project-analysis.json.
280
+ *
281
+ * Returns null when no env file exists (caller falls back to framework defaults).
282
+ *
283
+ * Sensitive variable values (passwords, secrets, tokens, API keys) are
284
+ * redacted in `vars` via redactSensitiveVars before being returned.
285
+ * extractPort/Host/ApiTarget use a whitelist of config-relevant keys so
286
+ * they are unaffected by redaction.
287
+ */
288
+ function readStackEnvInfo(root) {
289
+ const primary = readPrimaryEnv(root);
290
+ if (!primary) return null;
291
+ const { file, vars } = primary;
292
+ return {
293
+ source: file,
294
+ vars: redactSensitiveVars(vars),
295
+ port: extractPort(vars),
296
+ host: extractHost(vars),
297
+ apiTarget: extractApiTarget(vars),
298
+ };
299
+ }
300
+
301
+ module.exports = {
302
+ parseEnvContent,
303
+ findPrimaryEnvFile,
304
+ readPrimaryEnv,
305
+ extractPort,
306
+ extractHost,
307
+ extractApiTarget,
308
+ readStackEnvInfo,
309
+ isSensitiveVarName,
310
+ redactSensitiveVars,
311
+ // Exported for test visibility:
312
+ ENV_FILE_ORDER,
313
+ PORT_VAR_KEYS,
314
+ HOST_VAR_KEYS,
315
+ API_TARGET_VAR_KEYS,
316
+ SENSITIVE_VAR_PATTERNS,
317
+ };
@@ -720,8 +720,10 @@ function appendClaudeMdL4Memory(claudeMdPath, { lang = "en" } = {}) {
720
720
  // Heading pattern: lines starting with `##` (or more) that contain `(L4)`.
721
721
  // Using a heading-scoped regex avoids false positives when the user has
722
722
  // written `(L4)` elsewhere in body text (e.g. "Layer 4 load balancer (L4)").
723
- // The language-independent `(L4)` token still allows translated headings
724
- // like `## 메모리 (L4)`, `## メモリ (L4)` to be recognised.
723
+ // The language-independent `(L4)` token still allows translated
724
+ // headings (in any of the 10 supported output languages) to be
725
+ // recognised — only the `(L4)` suffix is stable; the preceding
726
+ // "Memory" word varies per language.
725
727
  const MARKER_REGEX = /^#{2,}\s+.*\(L4\)/m;
726
728
  if (!existsSafe(claudeMdPath)) return false;
727
729
  const existing = readFileSafe(claudeMdPath);
@@ -980,7 +982,7 @@ function scaffoldDocWritingGuide(standardCoreDir, { overwrite = false, lang = "e
980
982
  // Pass 3c is expected to generate claudeos-core/skills/00.shared/MANIFEST.md,
981
983
  // but the stack pass3.md templates list it among generation targets without
982
984
  // marking it REQUIRED. On frontend-only or skill-sparse projects Claude may
983
- // omit it, leaving .claude/rules/50.sync/03.skills-sync.md (which names
985
+ // omit it, leaving .claude/rules/50.sync/02.skills-sync.md (which names
984
986
  // MANIFEST.md as the single source of truth for skill registration) pointing
985
987
  // at a non-existent file. This gap-fill creates a minimal stub in Pass 4 if
986
988
  // the file is missing after Pass 3 completes — same contract as
@@ -988,7 +990,7 @@ function scaffoldDocWritingGuide(standardCoreDir, { overwrite = false, lang = "e
988
990
  const SKILLS_MANIFEST_STUB = `# Skill Registry
989
991
 
990
992
  _Single source of truth for registered skills in this project._
991
- _Referenced by: \`.claude/rules/50.sync/03.skills-sync.md\`_
993
+ _Referenced by: \`.claude/rules/50.sync/02.skills-sync.md\`_
992
994
 
993
995
  ## How to register a skill
994
996
 
@@ -1005,7 +1007,7 @@ with its path, purpose, and the orchestrator file that invokes it.
1005
1007
 
1006
1008
  - When a skill file is added/renamed/deleted under \`claudeos-core/skills/\`,
1007
1009
  update this manifest in the same commit.
1008
- - When this manifest is modified, \`.claude/rules/50.sync/03.skills-sync.md\`
1010
+ - When this manifest is modified, \`.claude/rules/50.sync/02.skills-sync.md\`
1009
1011
  is NOT modified — it references this file by path, not by content.
1010
1012
  `;
1011
1013
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeos-core",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Auto-generate Claude Code documentation from your actual source code — Standards, Rules, Skills, and Guides tailored to your project",
5
5
  "main": "bin/cli.js",
6
6
  "bin": {