claudeos-core 2.4.2 → 2.4.3
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 +16 -0
- package/bin/commands/init.js +3 -3
- package/content-validator/index.js +20 -3
- package/lib/expected-outputs.js +1 -1
- package/manifest-generator/index.js +11 -0
- package/manifest-generator/skills-sync.js +393 -0
- package/package.json +1 -1
- package/plan-installer/scanners/scan-java.js +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Quick navigation to recent releases:
|
|
6
6
|
|
|
7
|
+
- [`2.4.3`](#243--2026-04-27) — Skills catalog reconciliation (MANIFEST ↔ §6 sync) + `STALE_PATH` naming-convention placeholder exemption
|
|
7
8
|
- [`2.4.2`](#242--2026-04-26) — README structural tightening + 9-language re-sync (same-day after v2.4.1 docs overhaul)
|
|
8
9
|
- [`2.4.1`](#241--2026-04-26) — Documentation overhaul, 10-language localization, fixture sanitization (post-release docs)
|
|
9
10
|
- [`2.4.0`](#240--2026-04-25) — Session Continuity Protocol (v2.4 series feature 1 of 3)
|
|
@@ -24,6 +25,21 @@ For older entries scroll past v1.5.0 or use the GitHub blame view.
|
|
|
24
25
|
|
|
25
26
|
---
|
|
26
27
|
|
|
28
|
+
## [2.4.3] — 2026-04-27
|
|
29
|
+
|
|
30
|
+
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.
|
|
31
|
+
|
|
32
|
+
- **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:
|
|
33
|
+
- `discoverPerDomainCatalogs` — walks `claudeos-core/skills/{category}/domains/`, sorts alphabetically.
|
|
34
|
+
- `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.
|
|
35
|
+
- `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).
|
|
36
|
+
- **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).
|
|
37
|
+
- **`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.
|
|
38
|
+
- **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.
|
|
39
|
+
- **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`).
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
27
43
|
## [2.4.2] — 2026-04-26
|
|
28
44
|
|
|
29
45
|
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/bin/commands/init.js
CHANGED
|
@@ -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
|
|
348
|
-
//
|
|
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
|
|
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)
|
|
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
|
-
//
|
|
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;
|
package/lib/expected-outputs.js
CHANGED
|
@@ -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 (
|
|
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
|
@@ -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
|
|
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.
|
|
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
|