claudeos-core 1.2.4 → 1.3.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.

Potentially problematic release.


This version of claudeos-core might be problematic. Click here for more details.

Files changed (44) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.de.md +50 -10
  3. package/README.es.md +51 -10
  4. package/README.fr.md +51 -10
  5. package/README.hi.md +51 -10
  6. package/README.ja.md +52 -10
  7. package/README.ko.md +51 -10
  8. package/README.md +62 -15
  9. package/README.ru.md +51 -10
  10. package/README.vi.md +51 -10
  11. package/README.zh-CN.md +51 -10
  12. package/bin/cli.js +171 -36
  13. package/bootstrap.sh +71 -23
  14. package/content-validator/index.js +16 -13
  15. package/health-checker/index.js +4 -3
  16. package/lib/safe-fs.js +110 -0
  17. package/manifest-generator/index.js +13 -7
  18. package/package.json +4 -2
  19. package/pass-json-validator/index.js +3 -5
  20. package/pass-prompts/templates/java-spring/pass1.md +4 -1
  21. package/pass-prompts/templates/java-spring/pass2.md +3 -3
  22. package/pass-prompts/templates/java-spring/pass3.md +42 -5
  23. package/pass-prompts/templates/kotlin-spring/pass1.md +4 -1
  24. package/pass-prompts/templates/kotlin-spring/pass2.md +5 -5
  25. package/pass-prompts/templates/kotlin-spring/pass3.md +42 -5
  26. package/pass-prompts/templates/node-express/pass1.md +4 -1
  27. package/pass-prompts/templates/node-express/pass2.md +4 -1
  28. package/pass-prompts/templates/node-express/pass3.md +44 -6
  29. package/pass-prompts/templates/node-nextjs/pass1.md +14 -4
  30. package/pass-prompts/templates/node-nextjs/pass2.md +6 -4
  31. package/pass-prompts/templates/node-nextjs/pass3.md +45 -6
  32. package/pass-prompts/templates/python-django/pass1.md +4 -2
  33. package/pass-prompts/templates/python-django/pass2.md +4 -4
  34. package/pass-prompts/templates/python-django/pass3.md +42 -5
  35. package/pass-prompts/templates/python-fastapi/pass1.md +4 -1
  36. package/pass-prompts/templates/python-fastapi/pass2.md +4 -4
  37. package/pass-prompts/templates/python-fastapi/pass3.md +42 -5
  38. package/plan-installer/domain-grouper.js +74 -0
  39. package/plan-installer/index.js +35 -1305
  40. package/plan-installer/prompt-generator.js +94 -0
  41. package/plan-installer/stack-detector.js +326 -0
  42. package/plan-installer/structure-scanner.js +783 -0
  43. package/plan-validator/index.js +84 -20
  44. package/sync-checker/index.js +7 -3
package/bin/cli.js CHANGED
@@ -49,42 +49,154 @@ function isValidLang(lang) {
49
49
  return LANG_CODES.includes(lang);
50
50
  }
51
51
 
52
- // Interactive language selection (stdin prompt)
52
+ // Interactive language selection (arrow key selector)
53
53
  function selectLangInteractive() {
54
54
  return new Promise((resolve) => {
55
- const readline = require("readline");
56
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
55
+ // Fallback to number input if stdin is not a TTY (e.g., piped input)
56
+ if (!process.stdin.isTTY) {
57
+ const readline = require("readline");
58
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
59
+ log("");
60
+ log("╔══════════════════════════════════════════════════╗");
61
+ log("║ Select generated document language (required) ║");
62
+ log("╚══════════════════════════════════════════════════╝");
63
+ log("");
64
+ log(" Generated files (CLAUDE.md, Standards, Rules,");
65
+ log(" Skills, Guides) will be written in this language.");
66
+ log("");
67
+ LANG_CODES.forEach((code, i) => {
68
+ log(` ${String(i + 1).padStart(2)}. ${code.padEnd(6)} — ${SUPPORTED_LANGS[code]}`);
69
+ });
70
+ log("");
71
+ rl.question(` Enter number (1-${LANG_CODES.length}) or language code: `, (answer) => {
72
+ rl.close();
73
+ const trimmed = answer.trim();
74
+ const num = parseInt(trimmed);
75
+ if (num >= 1 && num <= LANG_CODES.length) { resolve(LANG_CODES[num - 1]); return; }
76
+ if (isValidLang(trimmed)) { resolve(trimmed); return; }
77
+ log(`\n ❌ Invalid selection: "${trimmed}"`);
78
+ process.exit(1);
79
+ });
80
+ return;
81
+ }
82
+
83
+ // Arrow key interactive selector
84
+ let selected = 0;
85
+ const total = LANG_CODES.length;
86
+
87
+ // Description text per language (shown when hovering)
88
+ const DESC = {
89
+ en: "Generated files (CLAUDE.md, Standards, Rules,\n Skills, Guides) will be written in English.",
90
+ ko: "생성되는 파일(CLAUDE.md, Standards, Rules,\n Skills, Guides)이 한국어로 작성됩니다.",
91
+ "zh-CN": "生成的文件(CLAUDE.md、Standards、Rules、\n Skills、Guides)将以简体中文编写。",
92
+ ja: "生成されるファイル(CLAUDE.md、Standards、Rules、\n Skills、Guides)は日本語で作成されます。",
93
+ es: "Los archivos generados (CLAUDE.md, Standards, Rules,\n Skills, Guides) se escribirán en español.",
94
+ vi: "Các file được tạo (CLAUDE.md, Standards, Rules,\n Skills, Guides) sẽ được viết bằng tiếng Việt.",
95
+ hi: "जनरेट की गई फ़ाइलें (CLAUDE.md, Standards, Rules,\n Skills, Guides) हिन्दी में लिखी जाएंगी।",
96
+ ru: "Сгенерированные файлы (CLAUDE.md, Standards, Rules,\n Skills, Guides) будут написаны на русском языке.",
97
+ fr: "Les fichiers générés (CLAUDE.md, Standards, Rules,\n Skills, Guides) seront rédigés en français.",
98
+ de: "Die generierten Dateien (CLAUDE.md, Standards, Rules,\n Skills, Guides) werden auf Deutsch verfasst.",
99
+ };
100
+
101
+ function render() {
102
+ const output = [];
103
+ // Description line (changes with selection)
104
+ const code = LANG_CODES[selected];
105
+ const descLines = (DESC[code] || DESC.en).split("\n");
106
+ descLines.forEach(l => output.push(` ${l}`));
107
+ output.push("");
108
+ // Language list
109
+ const BOLD_CYAN = "\x1b[1;36m";
110
+ const RESET = "\x1b[0m";
111
+ for (let i = 0; i < total; i++) {
112
+ const c = LANG_CODES[i];
113
+ const label = SUPPORTED_LANGS[c];
114
+ const num = String(i + 1).padStart(2);
115
+ if (i === selected) {
116
+ output.push(` ${BOLD_CYAN}❯ ${num}. ${c.padEnd(6)} — ${label}${RESET}`);
117
+ } else {
118
+ output.push(` ${num}. ${c.padEnd(6)} — ${label}`);
119
+ }
120
+ }
121
+ output.push("");
122
+ const DIM = "\x1b[2m";
123
+ output.push(` \x1b[1m↑↓\x1b[0m${DIM} Move ${RESET} \x1b[1mEnter\x1b[0m${DIM} Select ${RESET} \x1b[1mESC\x1b[0m${DIM} Cancel${RESET}`);
124
+ return output;
125
+ }
57
126
 
127
+ // Print header once
58
128
  log("");
59
129
  log("╔══════════════════════════════════════════════════╗");
60
- log("║ Select output language (required) ║");
130
+ log("║ Select generated document language (required) ║");
61
131
  log("╚══════════════════════════════════════════════════╝");
62
132
  log("");
63
- LANG_CODES.forEach((code, i) => {
64
- log(` ${String(i + 1).padStart(2)}. ${code.padEnd(6)} — ${SUPPORTED_LANGS[code]}`);
65
- });
66
- log("");
67
133
 
68
- rl.question(` Enter number (1-${LANG_CODES.length}) or language code: `, (answer) => {
69
- rl.close();
70
- const trimmed = answer.trim();
134
+ // Initial render
135
+ const lines = render();
136
+ const listHeight = lines.length;
137
+ process.stdout.write(lines.join("\n") + "\n");
71
138
 
72
- // Accept number
73
- const num = parseInt(trimmed);
74
- if (num >= 1 && num <= LANG_CODES.length) {
75
- resolve(LANG_CODES[num - 1]);
76
- return;
139
+ // Raw mode for keypress detection
140
+ process.stdin.setRawMode(true);
141
+ process.stdin.resume();
142
+
143
+ process.stdin.on("data", (key) => {
144
+ const k = key.toString();
145
+
146
+ // Ctrl+C
147
+ if (k === "\x03") {
148
+ process.stdin.setRawMode(false);
149
+ log("\n Cancelled.\n");
150
+ process.exit(0);
151
+ }
152
+
153
+ // ESC (single byte only — arrow keys send \x1b[ which is 3 bytes)
154
+ if (k === "\x1b" && key.length === 1) {
155
+ process.stdin.setRawMode(false);
156
+ log("\n Cancelled.\n");
157
+ process.exit(0);
77
158
  }
78
159
 
79
- // Accept language code
80
- if (isValidLang(trimmed)) {
81
- resolve(trimmed);
160
+ // Up arrow
161
+ if (k === "\x1b[A") {
162
+ selected = (selected - 1 + total) % total;
163
+ }
164
+ // Down arrow
165
+ else if (k === "\x1b[B") {
166
+ selected = (selected + 1) % total;
167
+ }
168
+ // Number keys 1-9 (direct jump)
169
+ else if (k >= "1" && k <= "9" && parseInt(k) <= total) {
170
+ selected = parseInt(k) - 1;
171
+ }
172
+ // 0 for 10
173
+ else if (k === "0" && total >= 10) {
174
+ selected = 9;
175
+ }
176
+ // Enter
177
+ else if (k === "\r" || k === "\n") {
178
+ process.stdin.setRawMode(false);
179
+ process.stdin.pause();
180
+ process.stdin.removeAllListeners("data");
181
+ // Clear the list and reprint final selection
182
+ process.stdout.write(`\x1b[${listHeight}A`);
183
+ for (let i = 0; i < listHeight; i++) {
184
+ process.stdout.write("\x1b[2K\n");
185
+ }
186
+ process.stdout.write(`\x1b[${listHeight}A`);
187
+ log(` ✅ ${LANG_CODES[selected]} — ${SUPPORTED_LANGS[LANG_CODES[selected]]}`);
188
+ log("");
189
+ resolve(LANG_CODES[selected]);
82
190
  return;
83
191
  }
192
+ else {
193
+ return; // Ignore other keys
194
+ }
84
195
 
85
- log(`\n ❌ Invalid selection: "${trimmed}"`);
86
- log(` Supported: ${LANG_CODES.join(", ")}\n`);
87
- process.exit(1);
196
+ // Redraw list (clear each line to prevent ghost text from different-length strings)
197
+ process.stdout.write(`\x1b[${listHeight}A`);
198
+ const updated = render();
199
+ process.stdout.write(updated.map(l => `\x1b[2K${l}`).join("\n") + "\n");
88
200
  });
89
201
  });
90
202
  }
@@ -147,7 +259,9 @@ function readFile(p) {
147
259
  }
148
260
 
149
261
  function injectProjectRoot(text) {
150
- return text.replace(/\{\{PROJECT_ROOT\}\}/g, PROJECT_ROOT);
262
+ // Normalize to forward slashes for prompts (Claude interprets backslashes as escapes)
263
+ const normalizedRoot = PROJECT_ROOT.replace(/\\/g, "/");
264
+ return text.replace(/\{\{PROJECT_ROOT\}\}/g, normalizedRoot);
151
265
  }
152
266
 
153
267
  // ─── Command: init ───────────────────────────────────────
@@ -269,6 +383,11 @@ async function cmdInit() {
269
383
  process.exit(1);
270
384
  }
271
385
  const totalGroups = domainGroups.totalGroups;
386
+ if (!totalGroups || typeof totalGroups !== "number" || totalGroups < 1) {
387
+ log(` ❌ domain-groups.json has invalid totalGroups: ${totalGroups}`);
388
+ log(" Re-run plan-installer or check claudeos-core/generated/");
389
+ process.exit(1);
390
+ }
272
391
 
273
392
  // Load pass1 prompts by type
274
393
  const pass1Prompts = {};
@@ -302,8 +421,14 @@ async function cmdInit() {
302
421
 
303
422
  const pass1Json = path.join(GENERATED_DIR, `pass1-${i}.json`);
304
423
  if (fileExists(pass1Json)) {
305
- log(` ⏭️ pass1-${i}.json already exists, skipping`);
306
- continue;
424
+ try {
425
+ const existing = JSON.parse(readFile(pass1Json));
426
+ if (existing && existing.analysisPerDomain) {
427
+ log(` ⏭️ pass1-${i}.json already exists, skipping`);
428
+ continue;
429
+ }
430
+ } catch { /* malformed — re-run */ }
431
+ log(` ⚠️ pass1-${i}.json exists but is malformed, re-running`);
307
432
  }
308
433
 
309
434
  // Select prompt for this type
@@ -342,9 +467,12 @@ async function cmdInit() {
342
467
  if (fileExists(pass2Json)) {
343
468
  log(" ⏭️ pass2-merged.json already exists, skipping");
344
469
  } else {
345
- let prompt = injectProjectRoot(
346
- readFile(path.join(GENERATED_DIR, "pass2-prompt.md"))
347
- );
470
+ const pass2PromptFile = path.join(GENERATED_DIR, "pass2-prompt.md");
471
+ if (!fileExists(pass2PromptFile)) {
472
+ log(" ❌ pass2-prompt.md not found. Re-run plan-installer.");
473
+ process.exit(1);
474
+ }
475
+ let prompt = injectProjectRoot(readFile(pass2PromptFile));
348
476
 
349
477
  const ok = runClaudePrompt(prompt, { ignoreError: true });
350
478
 
@@ -365,9 +493,12 @@ async function cmdInit() {
365
493
  // ─── [6] Pass 3: Generate + verify ─────────────────────────
366
494
  header("[6] Pass 3 — Generating all files...");
367
495
 
368
- let prompt = injectProjectRoot(
369
- readFile(path.join(GENERATED_DIR, "pass3-prompt.md"))
370
- );
496
+ const pass3PromptFile = path.join(GENERATED_DIR, "pass3-prompt.md");
497
+ if (!fileExists(pass3PromptFile)) {
498
+ log(" ❌ pass3-prompt.md not found. Re-run plan-installer.");
499
+ process.exit(1);
500
+ }
501
+ let prompt = injectProjectRoot(readFile(pass3PromptFile));
371
502
 
372
503
  const ok = runClaudePrompt(prompt, { ignoreError: true });
373
504
 
@@ -453,8 +584,12 @@ function countFiles() {
453
584
  try {
454
585
  let count = 0;
455
586
  const skipDirs = ["node_modules", "generated"];
587
+ const visited = new Set();
456
588
  const scan = (dir) => {
457
589
  if (!fs.existsSync(dir)) return;
590
+ const realDir = fs.realpathSync(dir);
591
+ if (visited.has(realDir)) return;
592
+ visited.add(realDir);
458
593
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
459
594
  if (skipDirs.includes(entry.name)) continue;
460
595
  const full = path.join(dir, entry.name);
@@ -465,7 +600,7 @@ function countFiles() {
465
600
  scan(path.join(PROJECT_ROOT, ".claude"));
466
601
  scan(path.join(PROJECT_ROOT, "claudeos-core"));
467
602
  return count;
468
- } catch {
603
+ } catch (e) {
469
604
  return "?";
470
605
  }
471
606
  }
@@ -475,7 +610,7 @@ function countPass1Files() {
475
610
  return fs
476
611
  .readdirSync(GENERATED_DIR)
477
612
  .filter((f) => f.startsWith("pass1-") && f.endsWith(".json")).length;
478
- } catch {
613
+ } catch (e) {
479
614
  return 0;
480
615
  }
481
616
  }
@@ -535,18 +670,18 @@ const args = process.argv.slice(2);
535
670
  const parsedArgs = parseArgs(args);
536
671
  const command = parsedArgs.command;
537
672
 
538
- if (!command || command === "--help" || command === "-h") {
673
+ if (!command || command === "--help") {
539
674
  showHelp();
540
675
  process.exit(0);
541
676
  }
542
677
 
543
- if (command === "--version" || command === "-v") {
678
+ if (command === "--version") {
544
679
  try {
545
680
  const pkg = JSON.parse(
546
681
  readFile(path.join(TOOLS_DIR, "package.json"))
547
682
  );
548
683
  log(`claudeos-core v${pkg.version}`);
549
- } catch {
684
+ } catch (e) {
550
685
  log("claudeos-core (version unknown)");
551
686
  }
552
687
  process.exit(0);
package/bootstrap.sh CHANGED
@@ -23,6 +23,9 @@ GENERATED_DIR="$PROJECT_ROOT/claudeos-core/generated"
23
23
 
24
24
  cd "$PROJECT_ROOT"
25
25
 
26
+ # Cleanup temp files on exit (Ctrl+C, errors, etc.)
27
+ trap 'rm -f "$GENERATED_DIR"/_tmp_*.md "$GENERATED_DIR"/_tmp_*.md.final 2>/dev/null' EXIT
28
+
26
29
  # ─── Language selection (required) ──────────────────────────────
27
30
  SUPPORTED_LANGS=("en" "ko" "zh-CN" "ja" "es" "vi" "hi" "ru" "fr" "de")
28
31
  LANG_LABELS=("English" "한국어 (Korean)" "简体中文 (Chinese Simplified)" "日本語 (Japanese)" "Español (Spanish)" "Tiếng Việt (Vietnamese)" "हिन्दी (Hindi)" "Русский (Russian)" "Français (French)" "Deutsch (German)")
@@ -32,7 +35,7 @@ CLAUDEOS_LANG=""
32
35
  # Parse --lang argument
33
36
  while [[ $# -gt 0 ]]; do
34
37
  case $1 in
35
- --lang) CLAUDEOS_LANG="$2"; shift 2 ;;
38
+ --lang) [ -z "$2" ] && echo " ❌ --lang requires a value" && exit 1; CLAUDEOS_LANG="$2"; shift 2 ;;
36
39
  --lang=*) CLAUDEOS_LANG="${1#*=}"; shift ;;
37
40
  *) echo "⚠️ Unknown argument: $1 (ignored)"; shift ;;
38
41
  esac
@@ -42,17 +45,21 @@ done
42
45
  if [ -z "$CLAUDEOS_LANG" ]; then
43
46
  echo ""
44
47
  echo "╔══════════════════════════════════════════════════╗"
45
- echo "║ Select output language (required) ║"
48
+ echo "║ Select generated document language (required) ║"
46
49
  echo "╚══════════════════════════════════════════════════╝"
47
50
  echo ""
51
+ echo " Generated files (CLAUDE.md, Standards, Rules,"
52
+ echo " Skills, Guides) will be written in this language."
53
+ echo ""
48
54
  for i in "${!SUPPORTED_LANGS[@]}"; do
49
55
  printf " %2d. %-6s — %s\n" "$((i+1))" "${SUPPORTED_LANGS[$i]}" "${LANG_LABELS[$i]}"
50
56
  done
51
57
  echo ""
52
- read -p " Enter number (1-10) or language code: " LANG_INPUT
58
+ LANG_COUNT=${#SUPPORTED_LANGS[@]}
59
+ read -rp " Enter number (1-${LANG_COUNT}) or language code: " LANG_INPUT
53
60
 
54
61
  # Accept number
55
- if [[ "$LANG_INPUT" =~ ^[0-9]+$ ]] && [ "$LANG_INPUT" -ge 1 ] && [ "$LANG_INPUT" -le 10 ]; then
62
+ if [[ "$LANG_INPUT" =~ ^[0-9]+$ ]] && [ "$LANG_INPUT" -ge 1 ] && [ "$LANG_INPUT" -le "$LANG_COUNT" ]; then
56
63
  CLAUDEOS_LANG="${SUPPORTED_LANGS[$((LANG_INPUT-1))]}"
57
64
  else
58
65
  # Accept language code
@@ -76,12 +83,42 @@ fi
76
83
  export CLAUDEOS_LANG
77
84
  export CLAUDEOS_ROOT="$PROJECT_ROOT"
78
85
 
79
- # ─── Prompt placeholder substitution helper ────────────────────
80
- inject_project_root() {
81
- # Use perl with $ENV{} to avoid sed metachar issues with &, \, etc. in paths
82
- export _PROJECT_ROOT="$PROJECT_ROOT"
83
- perl -pe 's/\{\{PROJECT_ROOT\}\}/$ENV{_PROJECT_ROOT}/g'
84
- }
86
+ # ─── Prerequisites check ──────────────────────────────────────
87
+ if ! command -v node &> /dev/null; then
88
+ echo ""
89
+ echo " ❌ Node.js not found."
90
+ echo " Install: https://nodejs.org/"
91
+ echo ""
92
+ exit 1
93
+ fi
94
+
95
+ NODE_MAJOR=$(node -e "console.log(process.versions.node.split('.')[0])")
96
+ if ! [[ "$NODE_MAJOR" =~ ^[0-9]+$ ]] || [ "$NODE_MAJOR" -lt 18 ]; then
97
+ echo ""
98
+ echo " ❌ Node.js v18+ required (current: v$(node --version))"
99
+ echo " Install: https://nodejs.org/"
100
+ echo ""
101
+ exit 1
102
+ fi
103
+
104
+ if ! command -v claude &> /dev/null; then
105
+ echo ""
106
+ echo " ❌ Claude Code CLI not found."
107
+ echo " Install: https://code.claude.com/docs/en/overview"
108
+ echo " Then run: claude (and complete authentication)"
109
+ echo ""
110
+ exit 1
111
+ fi
112
+
113
+ if ! command -v perl &> /dev/null; then
114
+ echo ""
115
+ echo " ❌ perl not found (required for placeholder substitution)."
116
+ echo " Install perl or use the Node.js CLI instead: npx claudeos-core init"
117
+ echo ""
118
+ exit 1
119
+ fi
120
+
121
+
85
122
 
86
123
  echo ""
87
124
  echo "╔════════════════════════════════════════════════════╗"
@@ -176,24 +213,25 @@ for i in $(seq 1 "$TOTAL_GROUPS"); do
176
213
  if [ ! -f "$PROMPT_FILE" ]; then
177
214
  PROMPT_FILE="$GENERATED_DIR/pass1-prompt.md"
178
215
  fi
179
- PASS1_TEMPLATE=$(cat "$PROMPT_FILE")
180
-
181
- # Substitute placeholders via temp file (avoids sed special char issues with &, \, etc.)
216
+ # Substitute placeholders via temp file (avoids sed special char issues and $() newline stripping)
182
217
  TMP_PROMPT="$GENERATED_DIR/_tmp_pass1_prompt.md"
183
- printf '%s' "$PASS1_TEMPLATE" > "$TMP_PROMPT"
218
+ cp "$PROMPT_FILE" "$TMP_PROMPT"
184
219
  # Use perl with $ENV{} for safe literal replacement (no shell interpolation into Perl code)
185
220
  export _DOMAIN_LIST="$DOMAIN_LIST"
186
221
  export _PASS_NUM="$i"
187
222
  perl -pi -e 's/\{\{DOMAIN_GROUP\}\}/$ENV{_DOMAIN_LIST}/g' "$TMP_PROMPT"
188
223
  perl -pi -e 's/\{\{PASS_NUM\}\}/$ENV{_PASS_NUM}/g' "$TMP_PROMPT"
189
- # inject_project_root uses perl with $ENV{} which is safe for special chars in paths
190
- PASS1_PROMPT=$(cat "$TMP_PROMPT" | inject_project_root)
191
- rm -f "$TMP_PROMPT"
224
+ # inject_project_root: pipe through perl, write to final temp file
225
+ export _PROJECT_ROOT="$PROJECT_ROOT"
226
+ perl -pe 's/\{\{PROJECT_ROOT\}\}/$ENV{_PROJECT_ROOT}/g' "$TMP_PROMPT" > "${TMP_PROMPT}.final"
227
+ mv "${TMP_PROMPT}.final" "$TMP_PROMPT"
192
228
 
193
- if ! (cd "$PROJECT_ROOT" && claude -p "$PASS1_PROMPT" --dangerously-skip-permissions); then
229
+ if ! (cd "$PROJECT_ROOT" && cat "$TMP_PROMPT" | claude -p --dangerously-skip-permissions); then
230
+ rm -f "$TMP_PROMPT"
194
231
  echo " ❌ Pass 1-${i} failed. Aborting."
195
232
  exit 1
196
233
  fi
234
+ rm -f "$TMP_PROMPT"
197
235
 
198
236
  # Verify JSON was created
199
237
  if [ ! -f "$GENERATED_DIR/pass1-${i}.json" ]; then
@@ -203,6 +241,7 @@ for i in $(seq 1 "$TOTAL_GROUPS"); do
203
241
 
204
242
  echo " ✅ pass1-${i}.json created"
205
243
  done
244
+ unset _DOMAIN_LIST _PASS_NUM _PROJECT_ROOT
206
245
  echo ""
207
246
 
208
247
  # ─── [5] Pass 2: Merge analysis results ─────────────────────────
@@ -213,12 +252,16 @@ echo "━━━━━━━━━━━━━━━━━━━━━━━━
213
252
  if [ -f "$GENERATED_DIR/pass2-merged.json" ]; then
214
253
  echo " ⏭️ pass2-merged.json already exists, skipping"
215
254
  else
216
- PASS2_PROMPT=$(cat "$GENERATED_DIR/pass2-prompt.md" | inject_project_root)
255
+ TMP_PASS2="$GENERATED_DIR/_tmp_pass2_prompt.md"
256
+ export _PROJECT_ROOT="$PROJECT_ROOT"
257
+ perl -pe 's/\{\{PROJECT_ROOT\}\}/$ENV{_PROJECT_ROOT}/g' "$GENERATED_DIR/pass2-prompt.md" > "$TMP_PASS2"
217
258
 
218
- if ! (cd "$PROJECT_ROOT" && claude -p "$PASS2_PROMPT" --dangerously-skip-permissions); then
259
+ if ! (cd "$PROJECT_ROOT" && cat "$TMP_PASS2" | claude -p --dangerously-skip-permissions); then
260
+ rm -f "$TMP_PASS2"
219
261
  echo " ❌ Pass 2 failed. Aborting."
220
262
  exit 1
221
263
  fi
264
+ rm -f "$TMP_PASS2"
222
265
 
223
266
  if [ ! -f "$GENERATED_DIR/pass2-merged.json" ]; then
224
267
  echo " ❌ pass2-merged.json was not created. Aborting."
@@ -233,17 +276,22 @@ echo ""
233
276
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
234
277
  echo "[6] Pass 3 — Generating all files..."
235
278
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
236
- PASS3_PROMPT=$(cat "$GENERATED_DIR/pass3-prompt.md" | inject_project_root)
279
+ TMP_PASS3="$GENERATED_DIR/_tmp_pass3_prompt.md"
280
+ export _PROJECT_ROOT="$PROJECT_ROOT"
281
+ perl -pe 's/\{\{PROJECT_ROOT\}\}/$ENV{_PROJECT_ROOT}/g' "$GENERATED_DIR/pass3-prompt.md" > "$TMP_PASS3"
237
282
 
238
- if ! (cd "$PROJECT_ROOT" && claude -p "$PASS3_PROMPT" --dangerously-skip-permissions); then
283
+ if ! (cd "$PROJECT_ROOT" && cat "$TMP_PASS3" | claude -p --dangerously-skip-permissions); then
284
+ rm -f "$TMP_PASS3"
239
285
  echo " ❌ Pass 3 failed. Aborting."
240
286
  exit 1
241
287
  fi
288
+ rm -f "$TMP_PASS3"
242
289
 
243
290
  if [ ! -f "$PROJECT_ROOT/CLAUDE.md" ]; then
244
291
  echo " ❌ CLAUDE.md was not created. Pass 3 may have failed silently."
245
292
  exit 1
246
293
  fi
294
+ unset _PROJECT_ROOT
247
295
  echo ""
248
296
 
249
297
  # ─── [7] Run verification tools ───────────────────────────────
@@ -271,7 +319,7 @@ fi
271
319
  echo ""
272
320
 
273
321
  # ─── Complete ───────────────────────────────────────────────
274
- TOTAL_FILES=$(find .claude claudeos-core -type f 2>/dev/null | grep -v node_modules | wc -l | tr -d ' ')
322
+ TOTAL_FILES=$(find .claude claudeos-core -type f 2>/dev/null | grep -v '/node_modules/' | grep -v '/generated/' | wc -l | tr -d ' ')
275
323
  TOTAL_GROUPS_DONE=$TOTAL_GROUPS
276
324
  PASS1_FILES=$(ls -1 "$GENERATED_DIR"/pass1-*.json 2>/dev/null | wc -l | tr -d ' ')
277
325
 
@@ -33,9 +33,9 @@ const GEN_DIR = path.join(ROOT, "claudeos-core/generated");
33
33
  function rel(p) { return path.relative(ROOT, p).replace(/\\/g, "/"); }
34
34
 
35
35
  async function main() {
36
- console.log("\n╔══════════════════════════════════════╗");
36
+ console.log("\n╔═══════════════════════════════════════╗");
37
37
  console.log("║ ClaudeOS-Core — Content Validator ║");
38
- console.log("╚══════════════════════════════════════╝\n");
38
+ console.log("╚═══════════════════════════════════════╝\n");
39
39
 
40
40
  const errors = [];
41
41
  const warnings = [];
@@ -83,7 +83,10 @@ async function main() {
83
83
  const enKeywords = SECTION_KEYWORDS.en;
84
84
  for (let i = 0; i < enKeywords.length; i++) {
85
85
  const candidates = [enKeywords[i], langKeywords[i]].filter(Boolean);
86
- const found = candidates.some(kw => content.toLowerCase().includes(kw.toLowerCase()));
86
+ const found = candidates.some(kw => {
87
+ const re = new RegExp(`(^|#|\\s)${kw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "im");
88
+ return re.test(content);
89
+ });
87
90
  if (!found) {
88
91
  warnings.push({ file: "CLAUDE.md", type: "MISSING_SECTION", msg: `'${enKeywords[i]}' / '${langKeywords[i]}' section is missing` });
89
92
  }
@@ -102,15 +105,13 @@ async function main() {
102
105
  errors.push({ file: r, type: "EMPTY", msg: "Empty file" });
103
106
  continue;
104
107
  }
105
- // All rules (except sync reminders) must have paths: ["**/*"] frontmatter
106
- if (!r.includes("50.sync")) {
107
- const hasFrontmatter = c.startsWith("---");
108
- const hasPathsKey = c.includes("paths:");
109
- if (!hasFrontmatter) {
110
- warnings.push({ file: r, type: "NO_FRONTMATTER", msg: "Missing YAML frontmatter (---)" });
111
- } else if (!hasPathsKey) {
112
- warnings.push({ file: r, type: "NO_PATHS", msg: "Frontmatter exists but missing paths: key — use paths: [\"**/*\"] to ensure rule is always loaded" });
113
- }
108
+ // All rules must have paths: frontmatter (value varies by category — e.g. ["**/*"] for core/backend, scoped patterns for infra/sync)
109
+ const hasFrontmatter = c.startsWith("---");
110
+ const hasPathsKey = c.includes("paths:");
111
+ if (!hasFrontmatter) {
112
+ warnings.push({ file: r, type: "NO_FRONTMATTER", msg: "Missing YAML frontmatter (---)" });
113
+ } else if (!hasPathsKey) {
114
+ warnings.push({ file: r, type: "NO_PATHS", msg: "Frontmatter exists but missing paths: key" });
114
115
  }
115
116
  }
116
117
  console.log(` ${ruleFiles.length} files checked`);
@@ -164,7 +165,9 @@ async function main() {
164
165
  if (!badKeywords.some(kw => c.includes(kw))) {
165
166
  warnings.push({ file: r, type: "NO_BAD_EXAMPLE", msg: "No incorrect example (❌) found" });
166
167
  }
167
- if (!c.includes("|") && !c.includes("rule") && !c.includes("Rule") && !c.includes("table")) {
168
+ // Check for markdown table: at least one line with | col | col | pattern
169
+ const hasMarkdownTable = /\|.+\|.+\|/.test(c);
170
+ if (!hasMarkdownTable) {
168
171
  warnings.push({ file: r, type: "NO_TABLE", msg: "Rules summary table appears to be missing" });
169
172
  }
170
173
  // Kotlin code block check: core and backend standard files should contain ```kotlin blocks
@@ -36,9 +36,9 @@ function run(name, script) {
36
36
  }
37
37
 
38
38
  function main() {
39
- console.log("\n╔══════════════════════════════════════╗");
39
+ console.log("\n╔═══════════════════════════════════════╗");
40
40
  console.log("║ ClaudeOS-Core — Health Checker ║");
41
- console.log("╚══════════════════════════════════════╝\n");
41
+ console.log("╚═══════════════════════════════════════╝\n");
42
42
 
43
43
  // ─── [0] Run manifest-generator first (prerequisite) ──────────────────
44
44
  // Must run first because sync-checker reads sync-map.json
@@ -112,10 +112,11 @@ function main() {
112
112
  }
113
113
  ex.generatedAt = new Date().toISOString();
114
114
  ex.healthCheck = { results, status: hasErr ? "fail" : "pass" };
115
+ if (!ex.summary) ex.summary = {};
115
116
  ex.summary = {
116
117
  ...ex.summary,
117
118
  totalIssues: results.filter((r) => r.status === "fail").length,
118
- status: hasErr ? "fail" : "ok",
119
+ healthStatus: hasErr ? "fail" : "ok",
119
120
  };
120
121
  fs.writeFileSync(rp, JSON.stringify(ex, null, 2));
121
122
  }