claudeos-core 2.4.2 → 2.4.4

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/CHANGELOG.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  Quick navigation to recent releases:
6
6
 
7
+ - [`2.4.4`](#244--2026-05-04) — Translation polish for `docs/{lang}/` × 8 non-Korean languages + broken `#quick-start` anchor fix
8
+ - [`2.4.3`](#243--2026-04-27) — Skills catalog reconciliation (MANIFEST ↔ §6 sync) + `STALE_PATH` naming-convention placeholder exemption
7
9
  - [`2.4.2`](#242--2026-04-26) — README structural tightening + 9-language re-sync (same-day after v2.4.1 docs overhaul)
8
10
  - [`2.4.1`](#241--2026-04-26) — Documentation overhaul, 10-language localization, fixture sanitization (post-release docs)
9
11
  - [`2.4.0`](#240--2026-04-25) — Session Continuity Protocol (v2.4 series feature 1 of 3)
@@ -24,6 +26,32 @@ For older entries scroll past v1.5.0 or use the GitHub blame view.
24
26
 
25
27
  ---
26
28
 
29
+ ## [2.4.4] — 2026-05-04
30
+
31
+ Documentation-only release. Translation polish across 8 non-Korean language `docs/{lang}/` directories. Test suite remains 736 / 736 pass.
32
+
33
+ - **`docs/{zh-CN,ja,es,vi,hi,ru,fr,de}/` × 12 files each polished** — translation naturalness polish (calque elimination, em-dash reduction, passive → active, pronoun-overuse reduction, noun-verb redundancy, native dev-blog tone). Four rounds applied per language: initial polish → deep audit → final polish → file-by-file verification. Line counts within ±2 per file; code blocks, CLI commands, anchor links, version strings all byte-identical.
34
+ - **Broken `#quick-start` anchor fixed in 5 languages** — `docs/{ja,zh-CN,vi,hi,ru}/{architecture,commands}.md` linked to `README.{lang}.md#quick-start`, but those READMEs use localized headings (`クイックスタート` / `快速开始` / `Bắt đầu nhanh` / `त्वरित शुरुआत` / `Быстрый старт`). 15 anchor instances corrected to native GitHub slugs.
35
+ - **English source minor fixes** — `docs/verification.md` malformed `content-validator` table (header had 2 columns, data rows merged Check ID into description) and Section 5 heading (`Disk ↔ Master Plan` → `Disk ↔ sync-map.json`, reflecting the v2.1.0 master-plan removal).
36
+ - **Version** — `package.json`, `package-lock.json` (top-level + `packages.""`), `CLAUDE.md`, and 10 `docs/{lang,en}/manual-installation.md` files bumped to `2.4.4`.
37
+
38
+ ---
39
+
40
+ ## [2.4.3] — 2026-04-27
41
+
42
+ Skills catalog reconciliation. Closes a structural gap where Pass 3c-core registers per-domain orchestrators (`{category}/02.domains.md`) and their sub-skills (`{category}/domains/{name}.md`) inconsistently — leading to `MANIFEST_DRIFT` advisories. No template, scanner, prompt, or pass-pipeline behavior change. Test suite remains 736 / 736 pass.
43
+
44
+ - **NEW `manifest-generator/skills-sync.js`** — deterministic post-Pass-3 sync step (~270 lines, pure CommonJS, no new deps). Three pure functions wired into `manifest-generator/index.js` `main()` between `sync-map.json` write and `stale-report.json` initialization:
45
+ - `discoverPerDomainCatalogs` — walks `claudeos-core/skills/{category}/domains/`, sorts alphabetically.
46
+ - `patchManifestPerDomainSections` — ensures `MANIFEST.md` contains a canonical `### Per-domain notes` section per category. Idempotent: section current → no write; line stale → replace only the domain-list paragraph (sibling content preserved); section missing → append fresh.
47
+ - `patchClaudeMdSkillsSection` — ensures each MANIFEST-registered category-root orchestrator is mentioned in §6 Skills sub-section. Sub-skills excluded by design (the v2.3.0 `MANIFEST_DRIFT` exemption handles transitive coverage). §6 detection is multilingual via heading-number matching (`## 6.` plus first `### *Skills*` sub-heading).
48
+ - **Design invariants** — deterministic (no LLM); idempotent (3 consecutive runs produce MD5-identical files); append-only with respect to user edits; failure-isolated (errors logged; `manifest-generator`'s primary outputs unaffected); domain ordering alphabetical (OS-independent reproducibility).
49
+ - **`STALE_PATH` naming-convention placeholder exemption** — `content-validator`'s `hasPlaceholder` predicate (introduced v2.3.0, extended v2.4.0 with `/.../` ellipsis) gains a fourth placeholder family: naming-convention tokens (`camelCase`, `PascalCase`, `kebab-case`, `snake_case`). LLMs writing naming-convention docs routinely cite `src/.../camelCase.tsx` as a template — the convention name IS the lesson, not a path claim. Word-boundary anchored (`\b...\b`), so embedded substrings like `myCamelCaseUtil.ts` are NOT skipped.
50
+ - **Migration** — purely additive. First run after upgrade: existing projects with an LLM-ordered domain list see ONE alphabetic-reordering write; subsequent runs no-op. Projects without `domains/` are unaffected. Projects emitting the naming-convention false positive will see those `STALE_PATH` entries disappear on the next `health` run.
51
+ - **Files changed** — `manifest-generator/skills-sync.js` (NEW), `manifest-generator/index.js` (+2 lines: import + try/catch wrapper), `content-validator/index.js` (one regex line in `hasPlaceholder`), `package.json` / `package-lock.json` (`2.4.2` → `2.4.3`).
52
+
53
+ ---
54
+
27
55
  ## [2.4.2] — 2026-04-26
28
56
 
29
57
  Same-day documentation patch on top of v2.4.1. **No source code, scanner, template, or validator change.** Test suite remains 736 / 736 pass.
package/README.hi.md CHANGED
@@ -336,7 +336,7 @@ Pipeline **तीन stages** में चलती है, और LLM call क
336
336
  **2. Step B — 4-Pass Claude pipeline (Step A के facts के दायरे में)।**
337
337
  - **Pass 1** हर domain group के representative files पढ़ता है और प्रति domain करीब 50–100 conventions निकालता है — response wrappers, logging libraries, error handling, naming conventions, test patterns वगैरह। यह domain group पर एक बार चलता है (`max 4 domains, 40 files per group`), इसलिए context कभी overflow नहीं होता।
338
338
  - **Pass 2** सारे per-domain analysis को project-wide picture में merge कर देता है, और जहाँ disagreement हो वहाँ dominant convention चुन लेता है।
339
- - **Pass 3** `CLAUDE.md`, `.claude/rules/`, `claudeos-core/standard/`, skills और guides लिखता है। यह stages में split होता है (`3a` facts → `3b-core/3b-N` rules+standards → `3c-core/3c-N` skills+guides → `3d-aux` database+mcp-guide), जिससे `pass2-merged.json` बड़ा होने पर भी हर stage का prompt LLM के context window में आ जाता है। 16 या उससे ज्यादा domains वाले projects के लिए 3b/3c को ≤15-domain batches में sub-divide कर दिया जाता है।
339
+ - **Pass 3** `CLAUDE.md`, `.claude/rules/`, `claudeos-core/standard/`, skills और guides लिखता है। यह stages में split होता है (`3a` facts → `3b-core/3b-N` rules+standards → `3c-core/3c-N` skills+guides → `3d-aux` database+mcp-guide), जिससे `pass2-merged.json` बड़ा होने पर भी हर stage का prompt LLM के context window में आ जाता है। 16 या उससे ज़्यादा domains वाले projects के लिए 3b/3c को ≤15-domain batches में sub-divide कर दिया जाता है।
340
340
  - **Pass 4** L4 memory layer को seed करता है (`decision-log.md`, `failure-patterns.md`, `compaction.md`, `auto-rule-update.md`) और कुछ universal scaffold rules जोड़ता है। Pass 4 को **`CLAUDE.md` modify करने की इजाज़त नहीं है** — Pass 3 का Section 8 ही authoritative है।
341
341
 
342
342
  **3. Step C — Verification (deterministic, कोई LLM नहीं)।** पाँच validators output check करते हैं:
package/README.ja.md CHANGED
@@ -148,7 +148,7 @@ an XML-driven MyBatis persistence layer and JWT-based authentication.
148
148
  | Test Stack | JUnit Jupiter 5, Mockito, AssertJ, rest-assured, spring-mock-mvc |
149
149
  ```
150
150
 
151
- 上の表の値はすべて、正確な dependency の座標も、`dev.db` というファイル名も、`V1__create_tables.sql` というマイグレーション名も、「no JPA」という事実も、Claude がファイルを書く前にスキャナが `build.gradle`、`application.properties`、ソースツリーから直接読み取った内容です。推測した値は 1 つも入っていません。
151
+ 上の表の値はすべて、正確な dependency の座標も、`dev.db` というファイル名も、`V1__create_tables.sql` というマイグレーション名も、「no JPA」という注記も、Claude がファイルを書く前にスキャナが `build.gradle`、`application.properties`、ソースツリーから直接読み取った内容です。推測した値は 1 つも入っていません。
152
152
 
153
153
  </details>
154
154
 
package/README.ko.md CHANGED
@@ -329,7 +329,7 @@ ClaudeOS-Core는 일반적인 Claude Code 워크플로를 거꾸로 뒤집습니
329
329
  이 도구: 코드가 스택을 분석 → 확정된 사실을 Claude에게 전달 → Claude가 그 사실만으로 문서 작성
330
330
  ```
331
331
 
332
- 파이프라인은 **3 단계**로 동작합니다. LLM 호출 앞뒤 모두에 코드가 자리잡고 있습니다:
332
+ 파이프라인은 **3단계**로 동작합니다. LLM 호출 앞뒤 모두에 코드가 자리잡고 있습니다:
333
333
 
334
334
  **1. Step A — Scanner (일관된 동작, LLM 없음).** Node.js scanner가 프로젝트 루트를 순회하면서 `package.json`, `build.gradle`, `pom.xml`, `pyproject.toml`을 읽고, `.env*` 파일을 파싱합니다 (`PASSWORD/SECRET/TOKEN/JWT_SECRET/...` 같은 민감 변수는 자동으로 가립니다). 그런 다음 아키텍처 패턴을 분류하고 (Java 5개 패턴 A/B/C/D/E, Kotlin CQRS / 멀티모듈, Next.js App vs Pages Router, FSD, components 패턴), 도메인을 찾고, 존재하는 모든 소스 파일 경로의 명시적 allowlist를 만듭니다. 결과는 `project-analysis.json` 한 파일에 모이고, 이후 모든 단계는 이걸 단일 source of truth로 삼습니다.
335
335
 
@@ -344,8 +344,8 @@ async function runPass3Split(ctx) {
344
344
  //
345
345
  // Per-domain type lookup uses `domainTypeMap` from
346
346
  // `project-analysis.json`. Domains not in either set fall back to
347
- // `backend` (the dominant case in real-world projects) — this
348
- // happens only when the analysis JSON is malformed or absent.
347
+ // `backend` (the dominant project type) — this happens only when
348
+ // the analysis JSON is malformed or absent.
349
349
  const typeOf = (name) => {
350
350
  if (domainTypeMap.frontend.has(name)) return "frontend";
351
351
  // backend is the default when the domain is unknown to both sets.
@@ -486,7 +486,7 @@ async function runPass3Split(ctx) {
486
486
  // was removed because master plans are an internal tool backup not consumed
487
487
  // by Claude Code at runtime, and aggregating 30+ files in a single session
488
488
  // was the primary source of "Prompt is too long" failures on mid-sized
489
- // projects (observed on an 18-domain production run).
489
+ // projects (observed on 18-domain-class projects).
490
490
  //
491
491
  // The subStage parameter is kept for forward-compat: if a future version
492
492
  // reintroduces master plans via Node-side aggregation, this helper can
@@ -463,11 +463,28 @@ async function main() {
463
463
  // refuse it on most platforms — `.` and `..` are the only legal
464
464
  // dot-only directory names). Three consecutive dots in a path segment
465
465
  // are unambiguous placeholder signal.
466
+ //
467
+ // v2.4.3 — Naming-convention placeholders (`camelCase`, `PascalCase`,
468
+ // `kebab-case`, `snake_case`) added as placeholder markers.
469
+ //
470
+ // LLMs writing naming-convention docs frequently illustrate file naming
471
+ // rules using the convention name itself as the example filename, e.g.
472
+ // `src/admin/pages/mgmt/camelCase.tsx` (e.g. `userMgt.tsx`). The reader
473
+ // is expected to substitute their actual filename — the `camelCase` token
474
+ // is the lesson, not a path claim. The same pattern shows up for
475
+ // `PascalCase.tsx` (component naming), `kebab-case.tsx` (sometimes used
476
+ // for slugs), and `snake_case.py`/`.rb` (less common in TS but seen in
477
+ // multi-language standards docs). All four are unambiguous placeholders
478
+ // when used as a filename stem — no real production file would carry
479
+ // these literal names. Word-boundary anchored to avoid matching real
480
+ // identifiers that happen to contain these substrings (e.g. a real
481
+ // `myCamelCaseUtil.ts` should NOT be skipped — only standalone tokens).
466
482
  const hasPlaceholder = (p) =>
467
483
  /\{[^}]+\}/.test(p) || // {domain} style
468
484
  /X{3,}/.test(p) || /Xxx/.test(p) || // XXX+ anywhere, or Xxx token
469
485
  /\*/.test(p) || // glob star
470
- /\/\.\.\.\//.test(p); // /.../ ellipsis path segment (v2.4.0)
486
+ /\/\.\.\.\//.test(p) || // /.../ ellipsis path segment (v2.4.0)
487
+ /\b(?:camelCase|PascalCase|kebab-case|snake_case)\b/.test(p); // v2.4.3 naming-convention tokens
471
488
 
472
489
  // File-level exclusion: some generated rule files are DESIGNED to cite
473
490
  // convention-trap paths as teaching examples — they tell the reader
@@ -625,8 +642,8 @@ async function main() {
625
642
 
626
643
  // Pull every `claudeos-core/skills/...` path that appears inside
627
644
  // a backtick span in the MANIFEST. This catches the table's
628
- // "entry" column regardless of the heading language ("등록된
629
- // 스킬" / "Registered Skills" / "登録済みスキル" all match).
645
+ // "entry" column regardless of the heading language — match works
646
+ // on the path token only, not on any per-language heading text.
630
647
  const SKILL_PATH_RE = /`(claudeos-core\/skills\/[\w\-./]+\.md)`/g;
631
648
  const registered = new Set();
632
649
  let m;
@@ -33,7 +33,7 @@ const EXPECTED_OUTPUTS = [
33
33
  // no longer generated. Master plans were an internal tool backup not
34
34
  // consumed by Claude Code at runtime, and aggregating many files in a
35
35
  // single session caused "Prompt is too long" failures on mid-sized
36
- // projects (confirmed on an 18-domain production run).
36
+ // projects (observed on 18-domain-class projects).
37
37
  ];
38
38
 
39
39
  function readSafe(p) {
@@ -22,6 +22,7 @@ const matter = require("gray-matter");
22
22
  const { glob } = require("glob");
23
23
  const { parseFileBlocks, parseCodeBlocks, CODE_BLOCK_PLANS } = require("../lib/plan-parser");
24
24
  const { updateStaleReport } = require("../lib/stale-report");
25
+ const { syncSkillsCatalog } = require("./skills-sync");
25
26
 
26
27
  const ROOT = process.env.CLAUDEOS_ROOT || path.resolve(__dirname, "../..");
27
28
  const GEN = path.join(ROOT, "claudeos-core/generated");
@@ -161,6 +162,16 @@ async function main() {
161
162
  // to match the declared v2.1.0 contract "plan/ directory is no longer
162
163
  // created during init".
163
164
 
165
+ // ─── Skills catalog reconciliation (v2.4.3) ─────────────
166
+ // Deterministically reconcile MANIFEST.md ↔ CLAUDE.md §6 cross-references
167
+ // that LLM stages routinely drift on. Failure here is logged but not
168
+ // fatal — manifest-generator's primary outputs above are already on disk.
169
+ try {
170
+ syncSkillsCatalog(ROOT);
171
+ } catch (e) {
172
+ console.log(` ⚠️ skills-sync: unexpected error (${e.message || e})`);
173
+ }
174
+
164
175
  // ─── Initialize stale-report.json (preserve existing sub-tool results) ──
165
176
  updateStaleReport(GEN, "generatedAt", new Date().toISOString(), { totalIssues: 0, status: "initial" });
166
177
  console.log(" ✅ stale-report.json — initialized");
@@ -0,0 +1,393 @@
1
+ /**
2
+ * ClaudeOS-Core — Skills Sync (manifest-generator hook)
3
+ *
4
+ * Role: After all Pass 3 stages complete, deterministically reconcile two
5
+ * cross-references that LLMs (3b-core / 3c-core) routinely drift on:
6
+ *
7
+ * 1. MANIFEST.md "Per-domain notes" section
8
+ * - When `claudeos-core/skills/{category}/domains/` exists with N files,
9
+ * ensure MANIFEST.md surfaces the per-domain catalog explicitly.
10
+ * - Pattern matches the backend-style MANIFEST: a self-describing
11
+ * section listing all domain stems by name.
12
+ *
13
+ * 2. CLAUDE.md §6 Skills sub-section
14
+ * - When MANIFEST.md registers a category-root orchestrator
15
+ * (`{category}/{NN}.{name}.md`), ensure §6 mentions it.
16
+ * - Sub-skills (`{category}/{stem}/{file}.md`) are NOT added — they
17
+ * belong only in MANIFEST per the convention.
18
+ *
19
+ * Design principles:
20
+ * - Deterministic (no LLM)
21
+ * - Idempotent (safe to run repeatedly)
22
+ * - Append-only (never deletes user-edited content)
23
+ * - Failure-isolated (errors logged, don't break manifest-generator)
24
+ * - Multilingual (§6 detection by heading number, not text)
25
+ */
26
+
27
+ const fs = require("fs");
28
+ const path = require("path");
29
+
30
+ // ─── Per-domain catalog discovery ────────────────────────────────────
31
+ //
32
+ // Walk every category under skills/ and find ones with a domains/ folder.
33
+ // Returns: [{ categoryDir, categoryRel, domains: [stem...] }, ...]
34
+ function discoverPerDomainCatalogs(skillsDir, rootRel) {
35
+ const catalogs = [];
36
+ if (!fs.existsSync(skillsDir)) return catalogs;
37
+
38
+ for (const cat of fs.readdirSync(skillsDir, { withFileTypes: true })) {
39
+ if (!cat.isDirectory()) continue;
40
+ const domainsDir = path.join(skillsDir, cat.name, "domains");
41
+ if (!fs.existsSync(domainsDir)) continue;
42
+ if (!fs.statSync(domainsDir).isDirectory()) continue;
43
+
44
+ const domains = fs
45
+ .readdirSync(domainsDir)
46
+ .filter((f) => f.endsWith(".md") && !f.startsWith("."))
47
+ .map((f) => f.replace(/\.md$/, ""))
48
+ .sort();
49
+
50
+ if (domains.length === 0) continue;
51
+
52
+ catalogs.push({
53
+ categoryName: cat.name,
54
+ categoryRel: `${rootRel}/${cat.name}`,
55
+ domains,
56
+ });
57
+ }
58
+ return catalogs;
59
+ }
60
+
61
+ // ─── MANIFEST.md patcher ─────────────────────────────────────────────
62
+ //
63
+ // For each category with domains/, ensure MANIFEST.md contains a
64
+ // "Per-domain notes" section listing the domain stems.
65
+ //
66
+ // Idempotency:
67
+ // - If section exists AND already lists all current domains → no-op.
68
+ // - If section exists but domain list is stale → replace ONLY the list
69
+ // paragraph, preserving any user-added bullet/explanation below.
70
+ // - If section missing → append at end of category section (or end of
71
+ // file if category section structure is unrecognized).
72
+ //
73
+ // Returns: { patched: boolean, addedSections: [...], updatedLists: [...] }
74
+ function patchManifestPerDomainSections(manifestPath, catalogs) {
75
+ if (!fs.existsSync(manifestPath)) {
76
+ return { patched: false, reason: "MANIFEST.md not found" };
77
+ }
78
+
79
+ let content = fs.readFileSync(manifestPath, "utf-8");
80
+ const original = content;
81
+ const addedSections = [];
82
+ const updatedLists = [];
83
+
84
+ for (const cat of catalogs) {
85
+ // Build the canonical domain list paragraph.
86
+ const listLine =
87
+ `\`${cat.categoryRel}/domains/{domain}.md\` — one file per domain. ` +
88
+ `${cat.domains.length} domain${cat.domains.length === 1 ? "" : "s"} (${cat.domains.join(", ")}).`;
89
+
90
+ // Detection regex: "### Per-domain notes" heading anywhere in file.
91
+ // Multilingual variants are not auto-detected here — we use the
92
+ // canonical English heading. If a project later adopts a different
93
+ // heading, the section will simply be appended fresh, which is safe
94
+ // (idempotency loss in that case is the cost of multilingual support).
95
+ const sectionRe =
96
+ /###\s+Per-domain\s+notes\s*\n([\s\S]*?)(?=\n###\s|\n##\s|$)/;
97
+
98
+ const match = sectionRe.exec(content);
99
+
100
+ if (match) {
101
+ // Section exists. Check if our category's domain line is already
102
+ // present and current.
103
+ const sectionBody = match[1];
104
+ const categoryLineRe = new RegExp(
105
+ // Match a line containing the category path + "/domains/" followed
106
+ // by a domain count. Capture full line for replacement.
107
+ `(?:^|\\n)([^\\n]*\`${escapeRegex(cat.categoryRel)}/domains/[^\`]*\`[^\\n]*)`,
108
+ "m"
109
+ );
110
+ const lineMatch = categoryLineRe.exec(sectionBody);
111
+
112
+ if (lineMatch) {
113
+ // Line exists for this category. Replace if stale.
114
+ if (lineMatch[1].trim() !== listLine) {
115
+ const newBody = sectionBody.replace(lineMatch[1], listLine);
116
+ content = content.replace(sectionBody, newBody);
117
+ updatedLists.push(cat.categoryName);
118
+ }
119
+ // else: already correct, skip silently.
120
+ } else {
121
+ // Section exists but no line for this category. Append the line
122
+ // at the start of the section body (after the heading).
123
+ const insertion = `\n${listLine}\n`;
124
+ const newBody = insertion + sectionBody;
125
+ content = content.replace(sectionBody, newBody);
126
+ addedSections.push(cat.categoryName);
127
+ }
128
+ } else {
129
+ // No "### Per-domain notes" section anywhere. Append a fresh section
130
+ // at the end of the file.
131
+ const fresh = `\n### Per-domain notes\n\n${listLine}\n`;
132
+ content = content.trimEnd() + "\n" + fresh;
133
+ addedSections.push(cat.categoryName);
134
+ }
135
+ }
136
+
137
+ if (content === original) {
138
+ return { patched: false, addedSections: [], updatedLists: [] };
139
+ }
140
+
141
+ fs.writeFileSync(manifestPath, content);
142
+ return { patched: true, addedSections, updatedLists };
143
+ }
144
+
145
+ // ─── CLAUDE.md §6 Skills patcher ─────────────────────────────────────
146
+ //
147
+ // Extract orchestrators from MANIFEST.md (category-root files only),
148
+ // ensure §6 Skills sub-section mentions each one. Append missing entries
149
+ // after the existing bullet list.
150
+ //
151
+ // Section detection:
152
+ // - §6 = first heading starting with "## 6." (any language after that)
153
+ // - Skills sub-section = first "### " heading inside §6 whose text
154
+ // contains "Skills" (covers EN/KO/JA/etc — most translations keep
155
+ // the word "Skills")
156
+ //
157
+ // Idempotency:
158
+ // - For each orchestrator path, search the WHOLE §6 Skills sub-section
159
+ // body. If the path is already mentioned anywhere, skip.
160
+ // - Otherwise, append a new bullet at end of sub-section.
161
+ function patchClaudeMdSkillsSection(claudeMdPath, manifestPath) {
162
+ if (!fs.existsSync(claudeMdPath)) {
163
+ return { patched: false, reason: "CLAUDE.md not found" };
164
+ }
165
+ if (!fs.existsSync(manifestPath)) {
166
+ return { patched: false, reason: "MANIFEST.md not found" };
167
+ }
168
+
169
+ const manifestContent = fs.readFileSync(manifestPath, "utf-8");
170
+
171
+ // Extract orchestrators from MANIFEST.
172
+ // An orchestrator is a path of the form:
173
+ // claudeos-core/skills/{category}/{NN}.{name}.md
174
+ // (sub-folder paths like {category}/{stem}/file.md are NOT orchestrators)
175
+ const orchestrators = [];
176
+ const seen = new Set();
177
+ // Match backtick-quoted paths in MANIFEST. Then filter to category-root.
178
+ // CRITICAL: the inline description capture must stay on the SAME line.
179
+ // We use [^\S\n] for "horizontal whitespace" to exclude newlines, and
180
+ // [^|`\n] for the description content (already excludes \n).
181
+ const pathRe =
182
+ /`(claudeos-core\/skills\/[^/`]+\/[^/`]+\.md)`(?:[^\S\n]*\|[^\S\n]*([^|`\n]+))?/g;
183
+ let m;
184
+ while ((m = pathRe.exec(manifestContent)) !== null) {
185
+ const fullPath = m[1];
186
+ // Exclude MANIFEST.md itself and any path with sub-folder.
187
+ if (fullPath.endsWith("/MANIFEST.md")) continue;
188
+ // Verify it's category-root: 4 segments exactly
189
+ // claudeos-core/skills/{category}/{file}.md
190
+ const parts = fullPath.split("/");
191
+ if (parts.length !== 4) continue;
192
+ if (seen.has(fullPath)) continue;
193
+ seen.add(fullPath);
194
+
195
+ // Try to capture the description from the same MANIFEST table row.
196
+ // We look ONLY on the same line (until the next newline) to avoid
197
+ // accidentally absorbing content from following sections (e.g.
198
+ // "### Per-domain notes" heading appearing right after).
199
+ const afterPath = manifestContent.slice(m.index + m[0].length);
200
+ const lineEnd = afterPath.indexOf("\n");
201
+ const sameLine = lineEnd === -1 ? afterPath : afterPath.slice(0, lineEnd);
202
+ let description = (m[2] || "").trim();
203
+ if (!description) {
204
+ // Look for `| description |` or `| description` pattern on same line.
205
+ const descMatch = /^\s*\|\s*([^|\n]+?)\s*(?:\||$)/.exec(sameLine);
206
+ if (descMatch) description = descMatch[1].trim();
207
+ }
208
+
209
+ orchestrators.push({ path: fullPath, description });
210
+ }
211
+
212
+ if (orchestrators.length === 0) {
213
+ return { patched: false, reason: "no orchestrators found in MANIFEST" };
214
+ }
215
+
216
+ let content = fs.readFileSync(claudeMdPath, "utf-8");
217
+ const original = content;
218
+
219
+ // Locate §6 Skills sub-section.
220
+ // Step 1: find "## 6." heading position
221
+ const section6Re = /^##\s+6\.\s+[^\n]*$/m;
222
+ const s6Match = section6Re.exec(content);
223
+ if (!s6Match) {
224
+ return { patched: false, reason: "§6 heading not found in CLAUDE.md" };
225
+ }
226
+ const s6Start = s6Match.index;
227
+
228
+ // Find end of §6: next "## " heading at same level
229
+ const afterS6 = content.slice(s6Start + s6Match[0].length);
230
+ const nextSectionRe = /\n##\s+/;
231
+ const nextMatch = nextSectionRe.exec(afterS6);
232
+ const s6End = nextMatch
233
+ ? s6Start + s6Match[0].length + nextMatch.index
234
+ : content.length;
235
+
236
+ const section6 = content.slice(s6Start, s6End);
237
+
238
+ // Step 2: find Skills sub-section inside §6
239
+ // Match "### " line containing word "Skills" (case-insensitive).
240
+ // Capture body until next "### " or end of section.
241
+ const skillsSubRe = /^###\s+[^\n]*Skills[^\n]*$/im;
242
+ const skillsMatch = skillsSubRe.exec(section6);
243
+ if (!skillsMatch) {
244
+ return {
245
+ patched: false,
246
+ reason: "Skills sub-section not found in CLAUDE.md §6",
247
+ };
248
+ }
249
+
250
+ const skillsHeadingStart = skillsMatch.index;
251
+ const skillsBodyStart = skillsHeadingStart + skillsMatch[0].length;
252
+ const afterSkills = section6.slice(skillsBodyStart);
253
+ const nextSubRe = /\n###\s+/;
254
+ const nextSubMatch = nextSubRe.exec(afterSkills);
255
+ const skillsBodyEnd = nextSubMatch
256
+ ? skillsBodyStart + nextSubMatch.index
257
+ : section6.length;
258
+
259
+ const skillsBody = section6.slice(skillsBodyStart, skillsBodyEnd);
260
+
261
+ // Step 3: for each orchestrator, check if mentioned in skillsBody.
262
+ // We accept any mention (path appears in body) as "covered".
263
+ const missing = [];
264
+ for (const orch of orchestrators) {
265
+ if (skillsBody.includes(orch.path)) continue;
266
+ missing.push(orch);
267
+ }
268
+
269
+ if (missing.length === 0) {
270
+ return { patched: false, addedOrchestrators: [] };
271
+ }
272
+
273
+ // Step 4: build new bullets and append before the trailing whitespace
274
+ // of skillsBody (so they sit at the end of the bullet list, before
275
+ // the blank line that precedes the next sub-section).
276
+ const newBullets = missing
277
+ .map((o) => {
278
+ const desc = o.description || "orchestrator";
279
+ return `- \`${o.path}\` — ${desc}`;
280
+ })
281
+ .join("\n");
282
+
283
+ // Trim trailing whitespace from skillsBody, append bullets, restore
284
+ // a trailing blank line for separation.
285
+ const trimmed = skillsBody.replace(/\s+$/, "");
286
+ const newSkillsBody = `${trimmed}\n${newBullets}\n\n`;
287
+
288
+ // Reassemble §6
289
+ const newSection6 =
290
+ section6.slice(0, skillsBodyStart) +
291
+ newSkillsBody +
292
+ section6.slice(skillsBodyEnd);
293
+
294
+ // Reassemble full file
295
+ content =
296
+ content.slice(0, s6Start) + newSection6 + content.slice(s6End);
297
+
298
+ if (content === original) {
299
+ return { patched: false, addedOrchestrators: [] };
300
+ }
301
+
302
+ fs.writeFileSync(claudeMdPath, content);
303
+ return {
304
+ patched: true,
305
+ addedOrchestrators: missing.map((o) => o.path),
306
+ };
307
+ }
308
+
309
+ // ─── Helpers ─────────────────────────────────────────────────────────
310
+
311
+ function escapeRegex(s) {
312
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
313
+ }
314
+
315
+ // ─── Public entry point ──────────────────────────────────────────────
316
+ //
317
+ // Called from manifest-generator/index.js main() after rule-manifest.json
318
+ // + sync-map.json + stale-report.json are written.
319
+ //
320
+ // Errors are logged but do NOT throw — manifest-generator must remain
321
+ // robust to partial-state projects (e.g. small projects without a
322
+ // domains/ folder, or projects that haven't run Pass 3 yet).
323
+ function syncSkillsCatalog(rootDir) {
324
+ const skillsDir = path.join(rootDir, "claudeos-core/skills");
325
+ const sharedManifestPath = path.join(skillsDir, "00.shared/MANIFEST.md");
326
+ const claudeMdPath = path.join(rootDir, "CLAUDE.md");
327
+
328
+ const result = {
329
+ perDomain: { patched: false },
330
+ skillsSection: { patched: false },
331
+ };
332
+
333
+ // Step 1: discover per-domain catalogs.
334
+ let catalogs;
335
+ try {
336
+ catalogs = discoverPerDomainCatalogs(skillsDir, "claudeos-core/skills");
337
+ } catch (e) {
338
+ console.log(` ⚠️ skills-sync: discovery failed (${e.message})`);
339
+ return result;
340
+ }
341
+
342
+ // Step 2: patch MANIFEST.md per-domain sections.
343
+ if (catalogs.length > 0) {
344
+ try {
345
+ result.perDomain = patchManifestPerDomainSections(
346
+ sharedManifestPath,
347
+ catalogs
348
+ );
349
+ if (result.perDomain.patched) {
350
+ const added = result.perDomain.addedSections || [];
351
+ const updated = result.perDomain.updatedLists || [];
352
+ if (added.length) {
353
+ console.log(
354
+ ` ✅ MANIFEST.md — Per-domain section added for [${added.join(", ")}]`
355
+ );
356
+ }
357
+ if (updated.length) {
358
+ console.log(
359
+ ` ✅ MANIFEST.md — Per-domain list refreshed for [${updated.join(", ")}]`
360
+ );
361
+ }
362
+ }
363
+ } catch (e) {
364
+ console.log(` ⚠️ skills-sync: MANIFEST patch failed (${e.message})`);
365
+ }
366
+ }
367
+
368
+ // Step 3: patch CLAUDE.md §6 Skills sub-section.
369
+ try {
370
+ result.skillsSection = patchClaudeMdSkillsSection(
371
+ claudeMdPath,
372
+ sharedManifestPath
373
+ );
374
+ if (result.skillsSection.patched) {
375
+ const added = result.skillsSection.addedOrchestrators || [];
376
+ console.log(
377
+ ` ✅ CLAUDE.md — §6 Skills updated (${added.length} orchestrator${added.length === 1 ? "" : "s"} added)`
378
+ );
379
+ }
380
+ } catch (e) {
381
+ console.log(` ⚠️ skills-sync: CLAUDE.md patch failed (${e.message})`);
382
+ }
383
+
384
+ return result;
385
+ }
386
+
387
+ module.exports = {
388
+ syncSkillsCatalog,
389
+ // Exposed for unit tests
390
+ discoverPerDomainCatalogs,
391
+ patchManifestPerDomainSections,
392
+ patchClaudeMdSkillsSection,
393
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeos-core",
3
- "version": "2.4.2",
3
+ "version": "2.4.4",
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": {
@@ -209,7 +209,7 @@ async function scanJavaDomains(stack, ROOT) {
209
209
  // v2.4.0 — Deep-sweep fallback (Pattern B/D only).
210
210
  //
211
211
  // Pre-v2.4.0: standard globs assume `{domain}/{layer}/X.java`. This
212
- // misses two real-world layouts:
212
+ // misses two non-canonical layouts:
213
213
  // (a) Multi-module split: `front/{domain}/{layer}/` for HTTP
214
214
  // layer + `core/{domain}/{layer}/` for service/dao layer.
215
215
  // Standard glob `**/{domain}/{layer}/` actually matches BOTH
@@ -234,7 +234,7 @@ async function scanJavaDomains(stack, ROOT) {
234
234
  const standardCount = svc.length + agg.length + mpr.length + dto.length + xml.length;
235
235
  if (standardCount === 0 && (p === "B" || p === "D")) {
236
236
  const deepFiles = (await glob(`src/main/java/**/${dn}/**/*.java`, { cwd: ROOT })).map(norm);
237
- // v2.4.0 — extended layer recognition. Real-world enterprise codebases
237
+ // v2.4.0 — extended layer recognition. Enterprise codebases
238
238
  // commonly include implementation/support layers beyond the canonical
239
239
  // controller/service/mapper/dto trio. Files in `factory/`, `strategy/`,
240
240
  // `impl/`, `helper/`, etc. were previously dropped by deep-sweep