claudeos-core 2.3.2 → 2.4.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 (36) hide show
  1. package/CHANGELOG.md +736 -74
  2. package/CODE_OF_CONDUCT.md +15 -0
  3. package/README.de.md +321 -883
  4. package/README.es.md +322 -883
  5. package/README.fr.md +322 -883
  6. package/README.hi.md +322 -883
  7. package/README.ja.md +322 -883
  8. package/README.ko.md +322 -882
  9. package/README.md +321 -883
  10. package/README.ru.md +322 -885
  11. package/README.vi.md +322 -883
  12. package/README.zh-CN.md +321 -881
  13. package/SECURITY.md +51 -0
  14. package/bin/commands/init.js +192 -37
  15. package/content-validator/index.js +97 -4
  16. package/health-checker/index.js +44 -10
  17. package/package.json +92 -90
  18. package/pass-json-validator/index.js +58 -7
  19. package/pass-prompts/templates/angular/pass3.md +15 -14
  20. package/pass-prompts/templates/common/claude-md-scaffold.md +81 -0
  21. package/pass-prompts/templates/common/pass3-footer.md +104 -0
  22. package/pass-prompts/templates/java-spring/pass3.md +19 -18
  23. package/pass-prompts/templates/kotlin-spring/pass3.md +23 -22
  24. package/pass-prompts/templates/node-express/pass3.md +18 -17
  25. package/pass-prompts/templates/node-fastify/pass3.md +11 -10
  26. package/pass-prompts/templates/node-nestjs/pass3.md +11 -10
  27. package/pass-prompts/templates/node-nextjs/pass3.md +18 -17
  28. package/pass-prompts/templates/node-vite/pass3.md +11 -10
  29. package/pass-prompts/templates/python-django/pass3.md +18 -17
  30. package/pass-prompts/templates/python-fastapi/pass3.md +18 -17
  31. package/pass-prompts/templates/python-flask/pass3.md +9 -8
  32. package/pass-prompts/templates/vue-nuxt/pass3.md +9 -8
  33. package/plan-installer/domain-grouper.js +45 -5
  34. package/plan-installer/index.js +11 -1
  35. package/plan-installer/scanners/scan-java.js +98 -2
  36. package/plan-installer/stack-detector.js +44 -0
package/SECURITY.md ADDED
@@ -0,0 +1,51 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ | Version | Supported |
6
+ |---------|--------------------|
7
+ | 2.4.x | :white_check_mark: |
8
+ | < 2.4 | :x: |
9
+
10
+ Only the latest minor release line receives security fixes. Users on older versions are encouraged to upgrade.
11
+
12
+ ## Reporting a Vulnerability
13
+
14
+ **Please do not file public GitHub issues for security vulnerabilities.**
15
+
16
+ Two private channels are available:
17
+
18
+ 1. **Email** — `claudeoscore@gmail.com`
19
+ 2. **GitHub Security Advisories** — [Open a private report](https://github.com/claudeos-core/claudeos-core/security/advisories/new) (preferred; provides a private workspace + CVE coordination)
20
+
21
+ ### What to include
22
+
23
+ - Affected version (`npx claudeos-core --version`)
24
+ - Reproduction steps or proof-of-concept
25
+ - Impact assessment (data exposure / code execution / DoS / etc.)
26
+ - Suggested fix (if any)
27
+
28
+ ### Response timeline
29
+
30
+ | Stage | Target |
31
+ |--------------------|---------|
32
+ | Initial reply | 48 hours |
33
+ | Triage + severity | 7 days |
34
+ | Fix or mitigation | 30 days for high/critical, 90 days for medium/low |
35
+
36
+ We will keep you informed throughout the process and credit you in the release notes (unless you prefer to remain anonymous).
37
+
38
+ ## Scope
39
+
40
+ In scope:
41
+
42
+ - The `claudeos-core` npm package and its CLI (`bin/cli.js`)
43
+ - The 4-Pass pipeline orchestrator (`bin/commands/init.js`)
44
+ - All validators (`claude-md-validator/`, `content-validator/`, `pass-json-validator/`)
45
+ - Generated artifacts (CLAUDE.md, rules, skills, guides) when produced by an unmodified release
46
+
47
+ Out of scope:
48
+
49
+ - Vulnerabilities in third-party dependencies (please report upstream; we will track and update)
50
+ - The `claude` CLI itself (report to Anthropic)
51
+ - User-modified template forks
@@ -205,9 +205,46 @@ async function runPass3Split(ctx) {
205
205
  return batches;
206
206
  }
207
207
 
208
+ // v2.4.0 — Stack-type classification helper.
209
+ //
210
+ // Returns `{ backend: Set<string>, frontend: Set<string>, isMultiStack: boolean }`
211
+ // built from `project-analysis.json`. The maps are used by
212
+ // `buildBatchScopeNote` to emit ALWAYS-typed per-domain paths
213
+ // (`70.domains/{type}/{domain}.md`) regardless of single/multi-stack.
214
+ // Uniform layout means single-stack projects pay a 1-folder depth
215
+ // cost in exchange for zero-migration when the other stack is later
216
+ // added, and validators recognize a single pattern. The `isMultiStack`
217
+ // flag is retained for tools that may want to surface the distinction
218
+ // (e.g. user-facing console output) but is no longer used to branch
219
+ // path generation.
220
+ function loadDomainTypeMap() {
221
+ const result = { backend: new Set(), frontend: new Set(), isMultiStack: false };
222
+ try {
223
+ const paPath = path.join(GENERATED_DIR, "project-analysis.json");
224
+ if (fileExists(paPath)) {
225
+ const pa = JSON.parse(readFile(paPath));
226
+ if (Array.isArray(pa.backendDomains)) {
227
+ for (const d of pa.backendDomains) {
228
+ const n = d && (d.name || d);
229
+ if (n) result.backend.add(n);
230
+ }
231
+ }
232
+ if (Array.isArray(pa.frontendDomains)) {
233
+ for (const d of pa.frontendDomains) {
234
+ const n = d && (d.name || d);
235
+ if (n) result.frontend.add(n);
236
+ }
237
+ }
238
+ }
239
+ } catch (_e) { /* tolerate missing/corrupt analysis — fallback to flat paths */ }
240
+ result.isMultiStack = result.backend.size > 0 && result.frontend.size > 0;
241
+ return result;
242
+ }
243
+
208
244
  const domainOrder = loadDomainOrder();
209
245
  const batches = computeBatches(domainOrder);
210
246
  const isBatched = batches.length > 1;
247
+ const domainTypeMap = loadDomainTypeMap();
211
248
 
212
249
  if (isBatched) {
213
250
  log(` 📦 Batch sub-division enabled: ${domainOrder.length} domains → ${batches.length} batches per stage (3b, 3c)`);
@@ -285,28 +322,100 @@ async function runPass3Split(ctx) {
285
322
  // and which common files to include vs skip.
286
323
  function buildBatchScopeNote(stageKind, batchIndex, totalBatches, batchDomains) {
287
324
  const isLastBatch = batchIndex === totalBatches - 1;
325
+ const isSingleBatch = totalBatches === 1;
288
326
  const domainList = batchDomains.map(d => `\`${d}\``).join(", ");
289
327
 
290
- let note = `## Batch scope (${stageKind}-batch ${batchIndex + 1}/${totalBatches})\n\n`;
291
- note += `This Pass 3 stage has been sub-divided into ${totalBatches} batches to avoid context overflow.\n`;
292
- note += `**You are processing batch ${batchIndex + 1} of ${totalBatches}.**\n\n`;
328
+ // v2.4.0 Always-typed per-domain layout.
329
+ //
330
+ // Every per-domain file lives under a `{type}/` sub-folder
331
+ // (`70.domains/backend/` or `70.domains/frontend/`) regardless of
332
+ // whether the project is single-stack or multi-stack. This is a
333
+ // deliberate uniform-convention choice:
334
+ //
335
+ // - Single-stack projects pay a 1-folder depth cost; in exchange
336
+ // they migrate to multi-stack with ZERO file moves when frontend
337
+ // (or backend) is later added.
338
+ // - LLM never has to decide which form to use → no probabilistic
339
+ // drift between Pass 3 runs.
340
+ // - Validators (content-validator, claude-md-validator) need
341
+ // to recognize only ONE pattern.
342
+ // - Future stack types (mobile, cli, agent, ...) extend naturally
343
+ // by adding new `{type}/` sub-folders.
344
+ //
345
+ // Per-domain type lookup uses `domainTypeMap` from
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.
349
+ const typeOf = (name) => {
350
+ if (domainTypeMap.frontend.has(name)) return "frontend";
351
+ // backend is the default when the domain is unknown to both sets.
352
+ // This happens only on malformed analysis JSON; backend is the
353
+ // safer default since it's the dominant project type.
354
+ return "backend";
355
+ };
356
+ const stdPathFor = (name) => `claudeos-core/standard/70.domains/${typeOf(name)}/${name}.md`;
357
+ const rulePathFor = (name) => `.claude/rules/70.domains/${typeOf(name)}/${name}-rules.md`;
358
+ const stagedRulePathFor = (name) => `claudeos-core/generated/.staged-rules/70.domains/${typeOf(name)}/${name}-rules.md`;
359
+
360
+ let note = isSingleBatch
361
+ ? `## Per-domain scope (${stageKind}, single-batch run)\n\n`
362
+ : `## Batch scope (${stageKind}-batch ${batchIndex + 1}/${totalBatches})\n\n`;
363
+ if (isSingleBatch) {
364
+ note += `${batchDomains.length} domain(s) will be processed in this single Pass 3b stage.\n`;
365
+ note += `Per-domain files are REQUIRED regardless of domain count — they enable domain-scoped \`paths\` glob targeting for rules.\n\n`;
366
+ } else {
367
+ note += `This Pass 3 stage has been sub-divided into ${totalBatches} batches to avoid context overflow.\n`;
368
+ note += `**You are processing batch ${batchIndex + 1} of ${totalBatches}.**\n\n`;
369
+ }
293
370
 
294
371
  if (stageKind === "3b") {
295
- note += `**Domains in THIS batch**: ${domainList}\n\n`;
296
- note += `**Rules for this batch**:\n`;
297
- note += `1. CLAUDE.md and all common standard/ files (00.core/, 30.security-db/, 40.infra/, etc.) are ALREADY GENERATED by the 3b-core stage. DO NOT regenerate them.\n`;
298
- note += `2. Generate standard/ entries ONLY for the domains listed above — one section per domain.\n`;
299
- note += `3. Generate .claude/rules/ (via staging-override path) — ONLY domain-specific rule files for the domains listed above. Common rules are already generated by 3b-core.\n`;
300
- note += `4. DO NOT generate standard/ or rules/ files for domains NOT in the above list those are/will be processed in other batches.\n`;
301
- note += `5. If a file you are about to write already exists with substantive content (Rule B), skip it silently — print \`[SKIP] <path>\` and move on.\n`;
372
+ note += isSingleBatch
373
+ ? `**Domains in scope (all ${batchDomains.length})**: ${domainList}\n\n`
374
+ : `**Domains in THIS batch**: ${domainList}\n\n`;
375
+ // Always show per-domain target paths so the LLM follows the
376
+ // typed sub-folder convention without inferring from domain name.
377
+ note += `**Per-domain target paths (always under \`{type}/\` sub-folder\`backend\` or \`frontend\`):**\n`;
378
+ for (const d of batchDomains) {
379
+ note += `- \`${d}\` → \`${stdPathFor(d)}\` and \`${rulePathFor(d)}\`\n`;
380
+ }
381
+ note += `\n`;
382
+ note += `**Rules**:\n`;
383
+ if (isSingleBatch) {
384
+ // Single-batch (≤15 domains): common files AND per-domain files in same stage.
385
+ note += `1. Generate ALL common files (CLAUDE.md + standard/00.core/* + standard/10.backend/* + standard/30.security-db/* + standard/40.infra/* + standard/80.verification/* + .claude/rules/00.core/* + .claude/rules/10.backend/* + .claude/rules/30.security-db/* + .claude/rules/40.infra/* + .claude/rules/50.sync/*) per the stack pass3 template.\n`;
386
+ note += `2. **ALSO** create ONE NEW per-domain standard file PER domain listed above at \`claudeos-core/standard/70.domains/{type}/{domain}.md\` (use the exact \`{type}\` shown in the per-domain target paths above). **Expected output: ${batchDomains.length} new file(s) under \`70.domains/{type}/\`** — one per domain. Do NOT inline these into the common standards (00.core/, 10.backend/, etc.); per-domain files are DOMAIN-scoped while common files are TOPIC-based, and they must live at separate paths so per-domain rules can scope to them via \`paths\` glob.\n`;
387
+ note += `3. **ALSO** create ONE NEW per-domain rule file PER domain listed above at \`.claude/rules/70.domains/{type}/{domain}-rules.md\` (via staging-override path \`claudeos-core/generated/.staged-rules/70.domains/{type}/{domain}-rules.md\`). Each rule file MUST have a \`paths\` frontmatter glob scoped to that domain's source directories so the rule auto-loads only when editing the relevant files.\n`;
388
+ note += `4. Per-domain file generation is MANDATORY even for ${batchDomains.length}-domain projects (any size below the 15-domain batch threshold). The convention is uniform across all project sizes for consistency: same dirtree shape regardless of scale.\n`;
389
+ note += `5. Rule B idempotent skip applies — if a per-domain file already exists with substantive content, print \`[SKIP] <path>\` and continue.\n`;
390
+ } else {
391
+ // Multi-batch (>15 domains): common files in 3b-core, per-domain in batch stages.
392
+ note += `1. CLAUDE.md and all common standard/ files (00.core/, 30.security-db/, 40.infra/, etc.) are ALREADY GENERATED by the 3b-core stage. DO NOT regenerate them.\n`;
393
+ note += `2. Create ONE NEW per-domain standard file PER domain listed above at \`claudeos-core/standard/70.domains/{type}/{domain}.md\` (use the exact \`{type}\` shown in the per-domain target paths above — \`backend\` or \`frontend\`). **Expected output: ${batchDomains.length} new files** — one per domain in this batch. Do NOT inline these into the common files generated by 3b-core; common files are TOPIC-based, per-domain files are DOMAIN-scoped and must live at separate paths so per-domain rules can scope to them via \`paths\` glob.\n`;
394
+ note += `3. Create ONE NEW per-domain rule file PER domain listed above at \`.claude/rules/70.domains/{type}/{domain}-rules.md\` (via staging-override path \`claudeos-core/generated/.staged-rules/70.domains/{type}/{domain}-rules.md\`). Each rule file MUST have a \`paths\` frontmatter glob scoped to that domain's source directories so the rule auto-loads only when editing the relevant files. Common rules are already generated by 3b-core — do NOT regenerate.\n`;
395
+ note += `4. DO NOT generate standard/ or rules/ files for domains NOT in the above list — those are/will be processed in other batches.\n`;
396
+ note += `5. Rule B idempotent skip applies ONLY to files at the per-domain paths shown above for THIS batch's domains — i.e. resume after a crash. **Do NOT use Rule B as justification for skipping the entire batch** because common files at OTHER paths exist (those are out of scope for this batch). If a per-domain target file does NOT exist for a batch domain, you MUST generate it; emitting "0 new files" for the whole batch when domains were assigned to you is a critical Pass 3 failure mode.\n`;
397
+ }
302
398
  } else if (stageKind === "3c") {
303
- note += `**Domains in THIS batch**: ${domainList}\n\n`;
304
- note += `**Rules for this batch**:\n`;
305
- note += `1. ALL guide/ files (01.onboarding, 02.usage, 03.troubleshooting, 04.architecture) are ALREADY GENERATED by the 3c-core stage. DO NOT regenerate.\n`;
306
- note += `2. Common skills (00.shared/, orchestrator SKILL.md) are ALREADY GENERATED by 3c-core. DO NOT regenerate.\n`;
307
- note += `3. Generate skills/ entries ONLY for the domains listed above — typically under 10.backend-crud/ or 20.frontend-page/ with a per-domain subdirectory.\n`;
308
- note += `4. DO NOT generate skills for domains NOT in the above list.\n`;
309
- note += `5. Rule B idempotent skip applies: if a skill file already exists, print \`[SKIP] <path>\` and move on.\n`;
399
+ note += isSingleBatch
400
+ ? `**Domains in scope (all ${batchDomains.length})**: ${domainList}\n\n`
401
+ : `**Domains in THIS batch**: ${domainList}\n\n`;
402
+ note += `**Rules**:\n`;
403
+ if (isSingleBatch) {
404
+ // Single-batch (≤15 domains): ALL of 3c — guides + common skills + per-domain skill notes — in one stage.
405
+ note += `1. Generate ALL guide/ files (01.onboarding, 02.usage, 03.troubleshooting, 04.architecture) per the stack pass3 template.\n`;
406
+ note += `2. Generate common skills: \`claudeos-core/skills/00.shared/MANIFEST.md\` + the category orchestrator(s) at \`claudeos-core/skills/{category}/01.scaffold-*-feature.md\` + their sub-skill files under \`{category}/scaffold-*-feature/\`.\n`;
407
+ note += `3. **ALSO** generate \`claudeos-core/skills/{category}/02.domains.md\` orchestrator (sibling to the \`domains/\` sub-folder) — REQUIRED for ALL projects regardless of domain count, mirrors the canonical \`01.scaffold-*-feature.md\` ↔ \`scaffold-*-feature/\` pattern.\n`;
408
+ note += `4. **ALSO** generate per-domain skill notes at \`claudeos-core/skills/{category}/domains/{domain}.md\` for EACH of the ${batchDomains.length} domain(s). **Expected output: ${batchDomains.length} new file(s) under \`{category}/domains/\`** — content describes domain-specific anti-patterns, dependencies, naming quirks. Per-domain skill generation is MANDATORY even for ${batchDomains.length}-domain projects (the convention is uniform across all project sizes).\n`;
409
+ note += `5. MUST register every newly created skill file in \`00.shared/MANIFEST.md\` (orchestrator + sub-skills + 02.domains.md + per-domain notes).\n`;
410
+ note += `6. Rule B idempotent skip applies — if a skill file already exists, print \`[SKIP] <path>\` and move on.\n`;
411
+ } else {
412
+ // Multi-batch (>15 domains): guides+common skills in 3c-core, per-domain in batch stages.
413
+ note += `1. ALL guide/ files (01.onboarding, 02.usage, 03.troubleshooting, 04.architecture) are ALREADY GENERATED by the 3c-core stage. DO NOT regenerate.\n`;
414
+ note += `2. Common skills (00.shared/, orchestrator SKILL.md) and \`02.domains.md\` orchestrator are ALREADY GENERATED by 3c-core. DO NOT regenerate.\n`;
415
+ note += `3. Generate per-domain skill notes ONLY for the domains listed above at \`claudeos-core/skills/{category}/domains/{domain}.md\` — typically under 10.backend-crud/ or 20.frontend-page/.\n`;
416
+ note += `4. DO NOT generate skills for domains NOT in the above list.\n`;
417
+ note += `5. Rule B idempotent skip applies: if a skill file already exists, print \`[SKIP] <path>\` and move on.\n`;
418
+ }
310
419
  }
311
420
 
312
421
  if (!isLastBatch) {
@@ -337,7 +446,7 @@ async function runPass3Split(ctx) {
337
446
  coreNote += ` - claudeos-core/standard/00.core/*.md (project overview, architecture, conventions)\n`;
338
447
  coreNote += ` - claudeos-core/standard/30.security-db/*.md\n`;
339
448
  coreNote += ` - claudeos-core/standard/40.infra/*.md\n`;
340
- coreNote += ` - claudeos-core/standard/50.verification/*.md\n`;
449
+ coreNote += ` - claudeos-core/standard/80.verification/*.md\n`;
341
450
  coreNote += ` - claudeos-core/standard/90.optional/*.md\n`;
342
451
  coreNote += ` - (stack-specific common sections as defined in the pass3 template)\n`;
343
452
  coreNote += `3. ALL rules files (via staging-override path .claude/rules → generated/.staged-rules):\n`;
@@ -356,11 +465,13 @@ async function runPass3Split(ctx) {
356
465
  coreNote += ` - claudeos-core/guide/04.architecture/*.md\n`;
357
466
  coreNote += `2. COMMON skills only:\n`;
358
467
  coreNote += ` - claudeos-core/skills/00.shared/*\n`;
359
- coreNote += ` - top-level orchestrator SKILL.md files (e.g. \`10.backend-crud/SKILL.md\` without any subfolder)\n\n`;
468
+ coreNote += ` - top-level orchestrator SKILL.md files (e.g. \`10.backend-crud/SKILL.md\` without any subfolder)\n`;
469
+ coreNote += `3. **Per-domain orchestrator** (REQUIRED whenever batches are non-empty — i.e. ${totalDomains} domains will be processed): for EACH active skill category that will receive per-domain notes, generate the sibling orchestrator file at \`claudeos-core/skills/{category}/02.domains.md\`. The basename stem (\`domains\`) MUST match the \`domains/\` sub-folder name so \`content-validator\`'s standard orchestrator-stem matching covers all sub-skills directly. The orchestrator's content describes the per-domain notes pattern, lists the ${totalDomains} domains that will be processed, and links to \`00.shared/MANIFEST.md\`. Generate it BEFORE 3c-N batches run so it's already in place when sub-skills land. Numbered \`02.\` because \`01.scaffold-*-feature.md\` already occupies the \`01.\` slot at the category root.\n\n`;
360
470
  coreNote += `**What NOT to generate in ${stageKind}-core**:\n`;
361
471
  coreNote += `- Per-domain skill sub-directories (e.g. \`10.backend-crud/scaffold-order-feature/\`) — those belong to 3c-1, 3c-2, ... batch stages.\n`;
472
+ coreNote += `- Per-domain note files (\`10.backend-crud/domains/{domain}.md\`) — those belong to 3c-N batch stages. ${stageKind}-core only generates the parent ORCHESTRATOR (\`02.domains.md\`).\n`;
362
473
  coreNote += `- Anything under plan/, database/, mcp-guide/ — those belong to 3d.\n\n`;
363
- coreNote += `**Per-domain skills will be generated in subsequent 3c-1, 3c-2, ... batch stages.**\n`;
474
+ coreNote += `**Per-domain skill notes will be generated in subsequent 3c-1, 3c-2, ... batch stages, under each category's \`domains/\` sub-folder.**\n`;
364
475
  }
365
476
 
366
477
  coreNote += `\nIf you find yourself about to generate a domain-specific file in this stage: STOP. Emit \`[DEFER] <path> — will be generated in 3b-N / 3c-N batch\` and move on.\n\n`;
@@ -550,15 +661,23 @@ async function runPass3Split(ctx) {
550
661
  ? `domain batch ${bi + 1}/${batches.length} (${batchDomains.length} domains)`
551
662
  : "core files (CLAUDE.md + standard + rules)";
552
663
 
553
- // Per-batch prompt: inject into the original 3b header the list of domains scoped to this batch.
554
- // In multi-batch mode every batch generates "domain-specific files only" (common files handled in 3b-core).
555
- const batchScopeNote = isBatched
556
- ? buildBatchScopeNote("3b", bi, batches.length, batchDomains)
557
- : "";
664
+ // v2.4.0 — Per-domain scope note ALWAYS injected (single OR multi-batch).
665
+ //
666
+ // Previously single-batch projects (≤15 domains) skipped the scope note,
667
+ // and the stack pass3 template alone didn't mandate per-domain file
668
+ // generation under `70.domains/{type}/`. As a result, small projects
669
+ // got common standards but no per-domain files — inconsistent with
670
+ // multi-batch projects and breaking the uniform-convention contract.
671
+ //
672
+ // The note text itself branches on isSingleBatch internally:
673
+ // - Single batch: "Generate common files AND per-domain files"
674
+ // - Multi batch: "Per-domain only (common files done in 3b-core)"
675
+ const batchScopeNote = buildBatchScopeNote("3b", bi, batches.length, batchDomains);
558
676
  const baseprompt = buildStagePrompt("pass3b-core-header.md", true);
559
- const promptWithScope = batchScopeNote
560
- ? baseprompt.replace(/\n## Scope of this step/, `\n${batchScopeNote}\n## Scope of this step`)
561
- : baseprompt;
677
+ const promptWithScope = baseprompt.replace(
678
+ /\n## Scope of this step/,
679
+ `\n${batchScopeNote}\n## Scope of this step`
680
+ );
562
681
 
563
682
  await runStage(stageId, label, promptWithScope, {
564
683
  expectsStagedRules: true,
@@ -632,13 +751,16 @@ async function runPass3Split(ctx) {
632
751
  ? `domain skills batch ${bi + 1}/${batches.length} (${batchDomains.length} domains)`
633
752
  : "skills and guides";
634
753
 
635
- const batchScopeNote = isBatched
636
- ? buildBatchScopeNote("3c", bi, batches.length, batchDomains)
637
- : "";
754
+ // v2.4.0 Per-domain skill scope note ALWAYS injected (single OR multi-batch).
755
+ // Same rationale as Pass 3b: small projects need per-domain skill notes
756
+ // (`{category}/domains/{domain}.md`) and the `02.domains.md` orchestrator
757
+ // for uniform layout across all project sizes.
758
+ const batchScopeNote = buildBatchScopeNote("3c", bi, batches.length, batchDomains);
638
759
  const baseprompt = buildStagePrompt("pass3c-skills-guide-header.md", true);
639
- const promptWithScope = batchScopeNote
640
- ? baseprompt.replace(/\n## Scope of this step/, `\n${batchScopeNote}\n## Scope of this step`)
641
- : baseprompt;
760
+ const promptWithScope = baseprompt.replace(
761
+ /\n## Scope of this step/,
762
+ `\n${batchScopeNote}\n## Scope of this step`
763
+ );
642
764
 
643
765
  await runStage(stageId, label, promptWithScope, {
644
766
  expectsStagedRules: true, // skills occasionally include rule files
@@ -897,15 +1019,26 @@ function ensureDirectories() {
897
1019
  ".claude/rules/50.sync",
898
1020
  "claudeos-core/generated",
899
1021
  "claudeos-core/standard/00.core",
900
- "claudeos-core/standard/10.backend-api",
901
- "claudeos-core/standard/20.frontend-ui",
1022
+ "claudeos-core/standard/10.backend",
1023
+ "claudeos-core/standard/20.frontend",
902
1024
  "claudeos-core/standard/30.security-db",
903
1025
  "claudeos-core/standard/40.infra",
904
- "claudeos-core/standard/50.verification",
1026
+ "claudeos-core/standard/80.verification",
905
1027
  "claudeos-core/standard/90.optional",
906
1028
  "claudeos-core/skills/00.shared",
907
1029
  "claudeos-core/skills/10.backend-crud/scaffold-crud-feature",
1030
+ // v2.4.0 — `domains/` sub-folder under each per-domain skill category.
1031
+ // Pre-created so Pass 3c-N has a stable destination for per-domain
1032
+ // skill notes (`{category}/domains/{domain}.md`) and so the convention
1033
+ // is visible in the post-init dirtree alongside `scaffold-*-feature/`.
1034
+ // The `02.domains.md` orchestrator (sibling at category root) is
1035
+ // generated by Pass 3c-core, not pre-created — the file content is
1036
+ // project-specific. Empty pre-created `domains/` folders are
1037
+ // harmless on filesystems that allow them and self-document the
1038
+ // convention.
1039
+ "claudeos-core/skills/10.backend-crud/domains",
908
1040
  "claudeos-core/skills/20.frontend-page/scaffold-page-feature",
1041
+ "claudeos-core/skills/20.frontend-page/domains",
909
1042
  "claudeos-core/skills/50.testing",
910
1043
  "claudeos-core/skills/90.experimental",
911
1044
  "claudeos-core/guide/01.onboarding",
@@ -916,6 +1049,28 @@ function ensureDirectories() {
916
1049
  "claudeos-core/mcp-guide",
917
1050
  "claudeos-core/memory",
918
1051
  ".claude/rules/60.memory",
1052
+ // v2.4.0 — 80.verification rules (mirror of standard/80.verification).
1053
+ // Pre-created to give Pass 3 LLM a stable destination for verification
1054
+ // rules (testing strategy, build verification reminders that auto-load
1055
+ // when editing test files / build configs). Without this, prior runs
1056
+ // would create them at the LEGACY 50.verification path (cross-namespace
1057
+ // contamination from standard/50.verification), now corrected to the
1058
+ // unified 80.* numbering.
1059
+ ".claude/rules/80.verification",
1060
+ // v2.4.0 — 70.domains/ canonical per-domain folder, ALWAYS typed.
1061
+ // - PLURAL folder (collection of N per-domain files)
1062
+ // - 70 number to avoid 60.* collision with 60.memory
1063
+ // - ALWAYS uses `{type}/` sub-folder (`backend/` or `frontend/`)
1064
+ // regardless of single/multi-stack. Uniform convention prevents
1065
+ // migration when a single-stack project later adds the other
1066
+ // stack, and gives validators a single pattern to recognize.
1067
+ // - Pre-created so Pass 3 LLM has a stable destination for
1068
+ // `claudeos-core/standard/70.domains/{type}/{domain}.md` and
1069
+ // `.claude/rules/70.domains/{type}/{domain}-rules.md` writes.
1070
+ "claudeos-core/standard/70.domains/backend",
1071
+ "claudeos-core/standard/70.domains/frontend",
1072
+ ".claude/rules/70.domains/backend",
1073
+ ".claude/rules/70.domains/frontend",
919
1074
  ];
920
1075
  for (const d of dirs) {
921
1076
  ensureDir(path.join(PROJECT_ROOT, d));
@@ -453,10 +453,21 @@ async function main() {
453
453
  // distinctive signal that essentially never appears in ordinary
454
454
  // identifiers (lowercase `xxx` CAN appear in words like `taxXxxRate`,
455
455
  // but three uppercase X's do not occur outside placeholder convention).
456
+ // v2.4.0 — Ellipsis (`...`) added as a placeholder marker.
457
+ //
458
+ // LLMs commonly use `...` to denote "any subdirectory" in illustrative
459
+ // path examples, e.g. `src/app/api/.../route.ts` to mean "any API
460
+ // route under app/api/". Treating these as STALE_PATH false-positives
461
+ // because `...` doesn't match the literal directory regex `[^/]+\.`,
462
+ // and because `...` can never be a real directory name (filesystems
463
+ // refuse it on most platforms — `.` and `..` are the only legal
464
+ // dot-only directory names). Three consecutive dots in a path segment
465
+ // are unambiguous placeholder signal.
456
466
  const hasPlaceholder = (p) =>
457
467
  /\{[^}]+\}/.test(p) || // {domain} style
458
468
  /X{3,}/.test(p) || /Xxx/.test(p) || // XXX+ anywhere, or Xxx token
459
- /\*/.test(p); // glob star
469
+ /\*/.test(p) || // glob star
470
+ /\/\.\.\.\//.test(p); // /.../ ellipsis path segment (v2.4.0)
460
471
 
461
472
  // File-level exclusion: some generated rule files are DESIGNED to cite
462
473
  // convention-trap paths as teaching examples — they tell the reader
@@ -480,10 +491,55 @@ async function main() {
480
491
  // denylist primed the LLM to cite those exact paths as
481
492
  // educational examples (the denylist has since been removed;
482
493
  // this exclusion is the validator-side defense-in-depth).
494
+ // - 00.core/51.doc-writing-rules.md — the documentation writing
495
+ // rules file (v2.4.0). Same meta-doc class as 52.ai-work-rules.md:
496
+ // it teaches "verify file paths before writing them in documents"
497
+ // and naturally cites example paths (`src/middleware.ts`,
498
+ // `src/app/api/<route>/route.ts`) as illustrations of the rule.
499
+ // Those examples are NOT path claims — they are the lesson. The
500
+ // content-blind validator would otherwise flag every cited example
501
+ // as STALE_PATH on every project that doesn't happen to contain
502
+ // all the cited illustrative files. Excluded for the same reason
503
+ // and via the same mechanism as 52.ai-work-rules.md.
483
504
  const PATH_CLAIM_EXCLUDE_FILES = new Set([
484
505
  "00.core/52.ai-work-rules.md",
506
+ "00.core/51.doc-writing-rules.md",
485
507
  ]);
486
508
 
509
+ // v2.4.0 — Resolve a `src/...` path claim against monorepo workspaces.
510
+ //
511
+ // Pre-v2.4.0 the validator only checked `<ROOT>/<claimed>` directly,
512
+ // which produced false-positive STALE_PATH advisories on Turborepo /
513
+ // pnpm-workspace projects where source files live under
514
+ // `apps/<app-name>/src/...` or `packages/<pkg-name>/src/...`. A rule
515
+ // citing `src/app/layout.tsx` is the natural single-app shorthand
516
+ // even when the actual file is at `apps/<app-name>/src/app/layout.tsx`.
517
+ //
518
+ // Resolution order (first match wins):
519
+ // 1. Direct: `<ROOT>/<claimed>` (single-app project)
520
+ // 2. Monorepo apps: `<ROOT>/apps/*/<claimed>`
521
+ // 3. Monorepo packages: `<ROOT>/packages/*/<claimed>`
522
+ //
523
+ // Returns true on first match, false if no match found anywhere.
524
+ // The monorepo fallback only fires for paths starting with `src/`,
525
+ // which is the conventional workspace-relative form. Non-`src/` paths
526
+ // (e.g., `claudeos-core/skills/...`) are checked direct-only.
527
+ function resolvePathClaim(ROOT, claimed) {
528
+ if (fs.existsSync(path.join(ROOT, claimed))) return true;
529
+ if (!claimed.startsWith("src/")) return false;
530
+ for (const workspace of ["apps", "packages"]) {
531
+ const wsDir = path.join(ROOT, workspace);
532
+ let entries;
533
+ try { entries = fs.readdirSync(wsDir, { withFileTypes: true }); }
534
+ catch (_e) { continue; }
535
+ for (const entry of entries) {
536
+ if (!entry.isDirectory()) continue;
537
+ if (fs.existsSync(path.join(wsDir, entry.name, claimed))) return true;
538
+ }
539
+ }
540
+ return false;
541
+ }
542
+
487
543
  // Strip fenced code blocks (``` and ~~~) so examples inside code
488
544
  // blocks don't trigger the check — they're illustrations, not claims.
489
545
  function stripFences(text) {
@@ -536,8 +592,7 @@ async function main() {
536
592
  seen.add(claimed);
537
593
  if (hasPlaceholder(claimed)) continue;
538
594
  pathClaimsChecked++;
539
- const absolutePath = path.join(ROOT, claimed);
540
- if (!fs.existsSync(absolutePath)) {
595
+ if (!resolvePathClaim(ROOT, claimed)) {
541
596
  pathClaimErrors++;
542
597
  errors.push({
543
598
  file: rel(file),
@@ -646,8 +701,13 @@ async function main() {
646
701
  // file of the form `skills/{category}/*{parent}*.md` (excluding
647
702
  // the sub-skill itself) counts as a plausible orchestrator.
648
703
  function orchestratorFor(subSkillPath) {
704
+ // Sub-skill path forms accepted (v2.4.0 generalization):
705
+ // skills/{category}/{parent-stem}/NN.{name}.md (legacy NN. prefix)
706
+ // skills/{category}/{parent-stem}/SKILL.md (v2.4.0 SKILL.md convention)
707
+ // skills/{category}/{parent-stem}/{name}.md (no NN. prefix)
708
+ // Captures `parent-stem` and the category directory.
649
709
  const m = subSkillPath.match(
650
- /^(claudeos-core\/skills\/[^/]+\/)([^/]+)\/\d+\.[^/]+\.md$/
710
+ /^(claudeos-core\/skills\/[^/]+\/)([^/]+)\/(?:\d+\.)?[^/]+\.md$/
651
711
  );
652
712
  if (!m) return null;
653
713
  return { categoryDir: m[1], stem: m[2] };
@@ -657,15 +717,47 @@ async function main() {
657
717
  // basename (minus leading number + dot) matches the sub-skill
658
718
  // parent stem. This accepts `01.scaffold-crud-feature.md`,
659
719
  // `scaffold-crud-feature.md`, etc.
720
+ // v2.4.0: a category-level `SKILL.md` (the orchestrator file
721
+ // colocated with `{category}/SKILL.md`) is treated as the
722
+ // orchestrator for ALL sub-skills under that category — this
723
+ // matches the new generator convention where each category has
724
+ // a single top-level orchestrator at `{category}/SKILL.md`.
660
725
  if (!ref.startsWith(categoryDir)) return false;
661
726
  const tail = ref.slice(categoryDir.length);
662
727
  // Must be a sibling file, not a nested path.
663
728
  if (tail.includes("/")) return false;
729
+ // v2.4.0 SKILL.md convention: a category-level SKILL.md covers
730
+ // every sub-skill in that category.
731
+ if (tail === "SKILL.md") return true;
664
732
  // Strip leading "NN." if present, then compare stem.
665
733
  const base = tail.replace(/^\d+\./, "").replace(/\.md$/, "");
666
734
  return base === stem;
667
735
  }
668
736
 
737
+ // Pre-compute: is any MANIFEST.md (the global skill registry)
738
+ // referenced anywhere in CLAUDE.md? Used as an additional
739
+ // sub-skill coverage rule below.
740
+ //
741
+ // Observed scenario: Pass 3c sometimes invents new sub-skill
742
+ // folder structures (e.g. `{category}/domains/{domain}.md` for
743
+ // per-domain notes) that weren't anticipated by the orchestrator
744
+ // pattern (which expects `{category}/{parent-stem}/{name}.md` paired
745
+ // with `{category}/{parent-stem}.md`). When this happens, every new
746
+ // sub-skill registration drifts because no sibling orchestrator exists.
747
+ //
748
+ // The architectural intent is that MANIFEST.md IS the registry — if
749
+ // CLAUDE.md §6 tells the reader "see MANIFEST.md for the full list",
750
+ // the reader can navigate to find every sub-skill. So mentioning
751
+ // MANIFEST.md anywhere in CLAUDE.md covers ALL sub-skill paths
752
+ // transitively. Top-level skills (direct `{category}/{file}.md`
753
+ // entries — those that don't match `orchestratorFor`) still require
754
+ // direct mention; this exception only applies to deep paths.
755
+ //
756
+ // Note: `referenced` Set above filters out MANIFEST.md entries (line
757
+ // 666), so we must scan the raw mdStripped text directly to detect
758
+ // MANIFEST.md mention.
759
+ const manifestReferencedGlobally = /`claudeos-core\/skills\/[\w\-./]*MANIFEST\.md`/.test(mdStripped);
760
+
669
761
  for (const p of registered) {
670
762
  if (referenced.has(p)) continue; // direct mention → OK
671
763
 
@@ -676,6 +768,7 @@ async function main() {
676
768
  isOrchestratorReferenced(ref, oc)
677
769
  );
678
770
  if (orchestratorMentioned) continue; // covered via orchestrator
771
+ if (manifestReferencedGlobally) continue; // covered via global MANIFEST
679
772
  }
680
773
 
681
774
  manifestErrors++;
@@ -61,11 +61,29 @@ function main() {
61
61
  console.log();
62
62
 
63
63
  // ─── [1-4] Run verification tools sequentially ────────────────────
64
+ //
65
+ // Tool status tiers (3-way):
66
+ // - default : non-zero exit → "fail" (❌, sets hasErr, blocks `health` exit code)
67
+ // - warnOnly:true : non-zero exit → "warn" (⚠️, does not set hasErr)
68
+ // - softFail:true : non-zero exit → "advisory" (ℹ️, does not set hasErr)
69
+ //
70
+ // The `softFail` tier (v2.4.0) was added for `content-validator` after
71
+ // user feedback: its findings are documentation quality notes
72
+ // (STALE_PATH suggestions, MANIFEST_DRIFT, NO_BAD_EXAMPLE) not generation
73
+ // failures, but pre-fix it surfaced as "❌ fail" alongside the
74
+ // "non-fatal" message — a confusing dual signal. `ℹ️ advisory` separates
75
+ // the visual from real structural failures (plan-validator,
76
+ // sync-checker, manifest-generator).
77
+ //
78
+ // `warnOnly` (existing) and `softFail` (new) are functionally similar at
79
+ // the gate level; the tier name encodes intent: warn = "watch this",
80
+ // advisory = "review when convenient". Both keep the health-check gate
81
+ // green.
64
82
  const tools = [
65
- { name: "plan-validator", script: path.join(TOOLS, "plan-validator/index.js"), desc: "Plan consistency" },
66
- { name: "sync-checker", script: path.join(TOOLS, "sync-checker/index.js"), desc: "Sync status" },
67
- { name: "content-validator", script: path.join(TOOLS, "content-validator/index.js"), desc: "Content quality" },
68
- { name: "pass-json-validator", script: path.join(TOOLS, "pass-json-validator/index.js"), desc: "JSON format", warnOnly: true },
83
+ { name: "plan-validator", script: path.join(TOOLS, "plan-validator/index.js"), desc: "Plan consistency" },
84
+ { name: "sync-checker", script: path.join(TOOLS, "sync-checker/index.js"), desc: "Sync status" },
85
+ { name: "content-validator", script: path.join(TOOLS, "content-validator/index.js"), desc: "Content quality", softFail: true },
86
+ { name: "pass-json-validator", script: path.join(TOOLS, "pass-json-validator/index.js"), desc: "JSON format", warnOnly: true },
69
87
  ];
70
88
 
71
89
  const results = [];
@@ -88,6 +106,9 @@ function main() {
88
106
  if (r.ok) {
89
107
  console.log(" ✅");
90
108
  results.push({ name: t.name, status: "pass" });
109
+ } else if (t.softFail) {
110
+ console.log(" ℹ️");
111
+ results.push({ name: t.name, status: "advisory" });
91
112
  } else if (t.warnOnly) {
92
113
  console.log(" ⚠️");
93
114
  results.push({ name: t.name, status: "warn" });
@@ -101,15 +122,28 @@ function main() {
101
122
  // ─── Results summary ──────────────────────────────────────────
102
123
  console.log("\n ══════════════════════════════");
103
124
  results.forEach((r) => {
104
- const icon = r.status === "pass" ? "✅" : r.status === "fail" ? "❌" : r.status === "warn" ? "⚠️" : "⏭️";
125
+ const icon = r.status === "pass" ? "✅"
126
+ : r.status === "fail" ? "❌"
127
+ : r.status === "warn" ? "⚠️"
128
+ : r.status === "advisory" ? "ℹ️"
129
+ : "⏭️";
105
130
  console.log(` ${icon} ${r.name.padEnd(22)} ${r.status}`);
106
131
  });
107
132
  console.log(" ──────────────────────────────");
108
- console.log(
109
- hasErr
110
- ? ` ⚠️ ${results.filter((r) => r.status === "fail").length} failed`
111
- : " All systems operational"
112
- );
133
+ // Summary line: distinguish real failures (block the gate) from
134
+ // advisory/warn results (informational, gate stays green).
135
+ const failCount = results.filter((r) => r.status === "fail").length;
136
+ const advisoryCount = results.filter((r) => r.status === "advisory").length;
137
+ const warnCount = results.filter((r) => r.status === "warn").length;
138
+ if (hasErr) {
139
+ console.log(` ⚠️ ${failCount} failed`);
140
+ } else {
141
+ const tail = [];
142
+ if (advisoryCount) tail.push(`${advisoryCount} advisory`);
143
+ if (warnCount) tail.push(`${warnCount} warning`);
144
+ const suffix = tail.length ? ` (${tail.join(", ")})` : "";
145
+ console.log(` ✅ All systems operational${suffix}`);
146
+ }
113
147
  console.log(" ══════════════════════════════\n");
114
148
 
115
149
  // ─── Update stale-report.json ────────────────────────────