claudeos-core 2.3.1 → 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 (43) hide show
  1. package/CHANGELOG.md +1460 -73
  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 +570 -264
  15. package/content-validator/index.js +185 -12
  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 +203 -20
  21. package/pass-prompts/templates/common/pass3-footer.md +297 -56
  22. package/pass-prompts/templates/common/pass3a-facts.md +48 -3
  23. package/pass-prompts/templates/common/pass4.md +78 -40
  24. package/pass-prompts/templates/java-spring/pass1.md +54 -0
  25. package/pass-prompts/templates/java-spring/pass3.md +20 -19
  26. package/pass-prompts/templates/kotlin-spring/pass1.md +45 -0
  27. package/pass-prompts/templates/kotlin-spring/pass3.md +24 -23
  28. package/pass-prompts/templates/node-express/pass3.md +18 -17
  29. package/pass-prompts/templates/node-fastify/pass3.md +11 -10
  30. package/pass-prompts/templates/node-nestjs/pass3.md +11 -10
  31. package/pass-prompts/templates/node-nextjs/pass3.md +18 -17
  32. package/pass-prompts/templates/node-vite/pass3.md +11 -10
  33. package/pass-prompts/templates/python-django/pass3.md +18 -17
  34. package/pass-prompts/templates/python-fastapi/pass3.md +18 -17
  35. package/pass-prompts/templates/python-flask/pass3.md +9 -8
  36. package/pass-prompts/templates/vue-nuxt/pass3.md +9 -8
  37. package/plan-installer/domain-grouper.js +45 -5
  38. package/plan-installer/index.js +34 -1
  39. package/plan-installer/pass3-context-builder.js +14 -0
  40. package/plan-installer/scanners/scan-frontend.js +2 -1
  41. package/plan-installer/scanners/scan-java.js +98 -2
  42. package/plan-installer/source-paths.js +242 -0
  43. package/plan-installer/stack-detector.js +522 -42
@@ -3,6 +3,17 @@
3
3
  *
4
4
  * Runs the full 4-Pass pipeline: analyze → merge → generate → memory scaffold.
5
5
  * This is the main entry point for project bootstrapping.
6
+ *
7
+ * Refactored internally: cmdInit's 970-line monolith decomposed into ~16 stage
8
+ * helpers (checkPrerequisites, resolveLanguage, applyResumeMode,
9
+ * ensureDirectories, loadDomainGroups, loadPass1Prompts, runPass1Loop,
10
+ * runPass2, buildPass3ContextJson, handlePass3StaleMarker, dispatchPass3,
11
+ * runPass4, runVerificationTools, runLint, runContentValidator,
12
+ * printCompletionBanner). runPass3Split is preserved below unchanged.
13
+ *
14
+ * All string/regex patterns consumed by tests/*.test.js source-parity checks
15
+ * are preserved because the runPass3Split and key stale-marker/pass-4-marker
16
+ * logic are kept in this file verbatim.
6
17
  */
7
18
 
8
19
  const fs = require("fs");
@@ -194,9 +205,46 @@ async function runPass3Split(ctx) {
194
205
  return batches;
195
206
  }
196
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
+
197
244
  const domainOrder = loadDomainOrder();
198
245
  const batches = computeBatches(domainOrder);
199
246
  const isBatched = batches.length > 1;
247
+ const domainTypeMap = loadDomainTypeMap();
200
248
 
201
249
  if (isBatched) {
202
250
  log(` 📦 Batch sub-division enabled: ${domainOrder.length} domains → ${batches.length} batches per stage (3b, 3c)`);
@@ -274,28 +322,100 @@ async function runPass3Split(ctx) {
274
322
  // and which common files to include vs skip.
275
323
  function buildBatchScopeNote(stageKind, batchIndex, totalBatches, batchDomains) {
276
324
  const isLastBatch = batchIndex === totalBatches - 1;
325
+ const isSingleBatch = totalBatches === 1;
277
326
  const domainList = batchDomains.map(d => `\`${d}\``).join(", ");
278
327
 
279
- let note = `## Batch scope (${stageKind}-batch ${batchIndex + 1}/${totalBatches})\n\n`;
280
- note += `This Pass 3 stage has been sub-divided into ${totalBatches} batches to avoid context overflow.\n`;
281
- 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
+ }
282
370
 
283
371
  if (stageKind === "3b") {
284
- note += `**Domains in THIS batch**: ${domainList}\n\n`;
285
- note += `**Rules for this batch**:\n`;
286
- 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`;
287
- note += `2. Generate standard/ entries ONLY for the domains listed above — one section per domain.\n`;
288
- 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`;
289
- 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`;
290
- 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
+ }
291
398
  } else if (stageKind === "3c") {
292
- note += `**Domains in THIS batch**: ${domainList}\n\n`;
293
- note += `**Rules for this batch**:\n`;
294
- 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`;
295
- note += `2. Common skills (00.shared/, orchestrator SKILL.md) are ALREADY GENERATED by 3c-core. DO NOT regenerate.\n`;
296
- 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`;
297
- note += `4. DO NOT generate skills for domains NOT in the above list.\n`;
298
- 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
+ }
299
419
  }
300
420
 
301
421
  if (!isLastBatch) {
@@ -326,7 +446,7 @@ async function runPass3Split(ctx) {
326
446
  coreNote += ` - claudeos-core/standard/00.core/*.md (project overview, architecture, conventions)\n`;
327
447
  coreNote += ` - claudeos-core/standard/30.security-db/*.md\n`;
328
448
  coreNote += ` - claudeos-core/standard/40.infra/*.md\n`;
329
- coreNote += ` - claudeos-core/standard/50.verification/*.md\n`;
449
+ coreNote += ` - claudeos-core/standard/80.verification/*.md\n`;
330
450
  coreNote += ` - claudeos-core/standard/90.optional/*.md\n`;
331
451
  coreNote += ` - (stack-specific common sections as defined in the pass3 template)\n`;
332
452
  coreNote += `3. ALL rules files (via staging-override path .claude/rules → generated/.staged-rules):\n`;
@@ -345,11 +465,13 @@ async function runPass3Split(ctx) {
345
465
  coreNote += ` - claudeos-core/guide/04.architecture/*.md\n`;
346
466
  coreNote += `2. COMMON skills only:\n`;
347
467
  coreNote += ` - claudeos-core/skills/00.shared/*\n`;
348
- 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`;
349
470
  coreNote += `**What NOT to generate in ${stageKind}-core**:\n`;
350
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`;
351
473
  coreNote += `- Anything under plan/, database/, mcp-guide/ — those belong to 3d.\n\n`;
352
- 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`;
353
475
  }
354
476
 
355
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`;
@@ -539,15 +661,23 @@ async function runPass3Split(ctx) {
539
661
  ? `domain batch ${bi + 1}/${batches.length} (${batchDomains.length} domains)`
540
662
  : "core files (CLAUDE.md + standard + rules)";
541
663
 
542
- // Per-batch prompt: inject into the original 3b header the list of domains scoped to this batch.
543
- // In multi-batch mode every batch generates "domain-specific files only" (common files handled in 3b-core).
544
- const batchScopeNote = isBatched
545
- ? buildBatchScopeNote("3b", bi, batches.length, batchDomains)
546
- : "";
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);
547
676
  const baseprompt = buildStagePrompt("pass3b-core-header.md", true);
548
- const promptWithScope = batchScopeNote
549
- ? baseprompt.replace(/\n## Scope of this step/, `\n${batchScopeNote}\n## Scope of this step`)
550
- : baseprompt;
677
+ const promptWithScope = baseprompt.replace(
678
+ /\n## Scope of this step/,
679
+ `\n${batchScopeNote}\n## Scope of this step`
680
+ );
551
681
 
552
682
  await runStage(stageId, label, promptWithScope, {
553
683
  expectsStagedRules: true,
@@ -621,13 +751,16 @@ async function runPass3Split(ctx) {
621
751
  ? `domain skills batch ${bi + 1}/${batches.length} (${batchDomains.length} domains)`
622
752
  : "skills and guides";
623
753
 
624
- const batchScopeNote = isBatched
625
- ? buildBatchScopeNote("3c", bi, batches.length, batchDomains)
626
- : "";
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);
627
759
  const baseprompt = buildStagePrompt("pass3c-skills-guide-header.md", true);
628
- const promptWithScope = batchScopeNote
629
- ? baseprompt.replace(/\n## Scope of this step/, `\n${batchScopeNote}\n## Scope of this step`)
630
- : baseprompt;
760
+ const promptWithScope = baseprompt.replace(
761
+ /\n## Scope of this step/,
762
+ `\n${batchScopeNote}\n## Scope of this step`
763
+ );
631
764
 
632
765
  await runStage(stageId, label, promptWithScope, {
633
766
  expectsStagedRules: true, // skills occasionally include rule files
@@ -701,15 +834,22 @@ async function runPass3Split(ctx) {
701
834
  log(` 🎉 Pass 3 split complete: ${completedGroups.length}/${totalStages} stages successful`);
702
835
  }
703
836
 
704
- async function cmdInit(parsedArgs) {
705
- const totalStart = Date.now();
706
- // Tracks whether we just wiped generated state via --force or "fresh" resume
707
- // mode. Used by the Pass 3 backfill guard below: fresh/force explicitly
708
- // means "regenerate from scratch", so a leftover CLAUDE.md from a prior run
709
- // must NOT cause Pass 3 to be skipped via the v1.7.x migration backfill.
710
- let wasFreshClean = false;
711
-
712
- // ─── Prerequisites check ───────────────────────────────────
837
+ // ═══════════════════════════════════════════════════════════════════
838
+ // cmdInit stage helpers
839
+ // ═══════════════════════════════════════════════════════════════════
840
+ // The original cmdInit was 970 lines with 77 if-statements and 17 try-blocks
841
+ // in a single function. Below it is decomposed into one helper per pipeline
842
+ // phase. Each helper owns a self-contained step with clear inputs/outputs.
843
+ //
844
+ // Shared state passed across helpers:
845
+ // - { lang, stepTimes, completedSteps (via ref), progressBar, wasFreshClean }
846
+ // Helpers that advance the outer progress bar return a `stepsDelta` value
847
+ // that cmdInit adds to `completedSteps` locally. This preserves the literal
848
+ // `completedSteps++` text at the top-level orchestrator, which several
849
+ // source-parity tests rely on for stale-check region detection.
850
+
851
+ // ─── Stage 1: Prerequisites check ──────────────────────────────────
852
+ function checkPrerequisites() {
713
853
  const hasProjectMarker = [".git", "package.json", "build.gradle", "build.gradle.kts", "pom.xml", "pyproject.toml", "requirements.txt"].some(
714
854
  m => fs.existsSync(path.join(PROJECT_ROOT, m))
715
855
  );
@@ -734,8 +874,10 @@ async function cmdInit(parsedArgs) {
734
874
  if (!claudeAuth) {
735
875
  throw new InitError("Claude Code may not be authenticated.\n Run: claude (and complete authentication)\n Then retry: npx claudeos-core init");
736
876
  }
877
+ }
737
878
 
738
- // ─── Language selection (required) ────────────────────────────
879
+ // ─── Stage 2: Resolve output language ─────────────────────────────
880
+ async function resolveLanguage(parsedArgs) {
739
881
  let lang = parsedArgs.lang;
740
882
  if (!lang) {
741
883
  lang = await selectLangInteractive();
@@ -765,114 +907,109 @@ async function cmdInit(parsedArgs) {
765
907
  }
766
908
 
767
909
  process.env.CLAUDEOS_LANG = lang;
910
+ return lang;
911
+ }
768
912
 
769
- // ─── Resume / Fresh selection ────────────────────────────
770
- if (fs.existsSync(GENERATED_DIR)) {
771
- const existingPass1 = fs.readdirSync(GENERATED_DIR).filter(f => f.startsWith("pass1-") && f.endsWith(".json"));
772
- const pass2Exists = fileExists(path.join(GENERATED_DIR, "pass2-merged.json"));
773
-
774
- if (existingPass1.length > 0 || pass2Exists) {
775
- if (parsedArgs.force) {
776
- // --force: clean all generated files for truly fresh start
777
- const genFiles = fs.readdirSync(GENERATED_DIR).filter(f => f.endsWith(".json") || f.endsWith(".md"));
778
- for (const f of genFiles) fs.unlinkSync(path.join(GENERATED_DIR, f));
779
- // Also clean any leftover .staged-rules/ from a prior crashed run
780
- // (only .json/.md are unlinked above; directories aren't touched).
781
- const stagedDir = path.join(GENERATED_DIR, ".staged-rules");
782
- if (fileExists(stagedDir)) fs.rmSync(stagedDir, { recursive: true, force: true });
783
- // Also wipe .claude/rules/ so Guard 2 (zero-rules detection) can't
784
- // false-negative on stale rules from a previous run when the fresh
785
- // Pass 3 run fails silently (e.g. Claude ignores staging-override).
786
- // Step [2] recreates the subdirs from scratch. Any manual edits the
787
- // user made to rule files are lost — acceptable under --force
788
- // ("truly fresh start").
789
- const rulesDir = path.join(PROJECT_ROOT, ".claude/rules");
790
- if (fileExists(rulesDir)) fs.rmSync(rulesDir, { recursive: true, force: true });
791
- wasFreshClean = true;
792
- log(" 🔄 Previous results deleted (--force)\n");
793
- } else {
794
- // v2.2.0 upgrade detection: if project was generated with older claudeos-core
795
- // (pre-2.2.0), default "resume" mode will skip regeneration of existing files
796
- // per Rule B idempotency, meaning v2.2.0 structural improvements will NOT be
797
- // picked up. Detect this case by checking CLAUDE.md for v2.2.0 markers.
798
- const claudeMd = path.join(PROJECT_ROOT, "CLAUDE.md");
799
- if (fileExists(claudeMd)) {
800
- try {
801
- const content = fs.readFileSync(claudeMd, "utf-8");
802
- // v2.2.0 scaffold enforces EXACTLY 8 top-level `##` sections.
803
- // Pre-v2.2.0 CLAUDE.md files typically carry 9+ sections (extra
804
- // "Rules Summary" / "Common Rules" / "Required to Observe"
805
- // blocks that v2.2.0 forbids). Counting `^## ` headings is a
806
- // language-independent heuristic that works across all 10
807
- // supported output languages. False positive (an existing
808
- // 8-section pre-v2.2.0 CLAUDE.md) is acceptable — the user
809
- // simply won't see the upgrade warning and can still run
810
- // `--force` manually.
811
- const sectionCount = (content.match(/^## /gm) || []).length;
812
- const hasV220Section8 = sectionCount === 8;
813
- if (!hasV220Section8) {
814
- log("\n ⚠️ v2.2.0 upgrade detected");
815
- log(" ─────────────────────────");
816
- log(" Your existing CLAUDE.md was generated with an older claudeos-core version.");
817
- log(" v2.2.0 introduces structural changes that the default 'resume' mode");
818
- log(" CANNOT apply because existing files are preserved under Rule B (idempotency).");
819
- log("");
820
- log(" To fully adopt v2.2.0, choose one of:");
821
- log(" 1. Rerun with --force: npx claudeos-core init --force");
822
- log(" (overwrites generated files; your memory/ content is preserved)");
823
- log(" 2. Choose 'fresh' below (equivalent to --force)");
824
- log("");
825
- log(" See CHANGELOG.md Migration section for full details.\n");
826
- }
827
- } catch (_) { /* Read error is non-fatal; proceed to resume prompt */ }
828
- }
913
+ // ─── Stage 3: Resume/Fresh selection ──────────────────────────────
914
+ // Returns { wasFreshClean: boolean } — the caller uses this to gate the
915
+ // v1.7.x migration backfill in dispatchPass3.
916
+ async function applyResumeMode(parsedArgs, lang) {
917
+ let wasFreshClean = false;
829
918
 
830
- const status = { pass1Done: existingPass1.length, pass2Done: pass2Exists };
831
- const mode = await selectResumeMode(lang, status);
832
- if (!mode) throw new InitError("Cancelled.");
833
- if (mode === "fresh") {
834
- for (const f of existingPass1) fs.unlinkSync(path.join(GENERATED_DIR, f));
835
- if (pass2Exists) fs.unlinkSync(path.join(GENERATED_DIR, "pass2-merged.json"));
836
- // Also reset pass 3 & pass 4 markers so they re-run
837
- const pass3M = path.join(GENERATED_DIR, "pass3-complete.json");
838
- const pass4M = path.join(GENERATED_DIR, "pass4-memory.json");
839
- if (fileExists(pass3M)) fs.unlinkSync(pass3M);
840
- if (fileExists(pass4M)) fs.unlinkSync(pass4M);
841
- // Clean .staged-rules/ leftover from a prior crashed run (same reason as --force branch).
842
- const stagedDir = path.join(GENERATED_DIR, ".staged-rules");
843
- if (fileExists(stagedDir)) fs.rmSync(stagedDir, { recursive: true, force: true });
844
- // Wipe .claude/rules/ for the same Guard 2 false-negative reason as
845
- // the --force branch. Step [2] recreates the subdirs; any manual
846
- // edits are lost acceptable under an explicit "fresh" choice.
847
- const rulesDir = path.join(PROJECT_ROOT, ".claude/rules");
848
- if (fileExists(rulesDir)) fs.rmSync(rulesDir, { recursive: true, force: true });
849
- wasFreshClean = true;
850
- } else if (mode === "continue" && existingPass1.length === 0 && pass2Exists) {
851
- // pass2 exists but no pass1 → pass2 is stale, force re-run
852
- fs.unlinkSync(path.join(GENERATED_DIR, "pass2-merged.json"));
853
- log(" ⚠️ pass2-merged.json deleted (no pass1 files to continue from)");
854
- }
855
- }
856
- }
919
+ if (!fs.existsSync(GENERATED_DIR)) return { wasFreshClean };
920
+
921
+ const existingPass1 = fs.readdirSync(GENERATED_DIR).filter(f => f.startsWith("pass1-") && f.endsWith(".json"));
922
+ const pass2Exists = fileExists(path.join(GENERATED_DIR, "pass2-merged.json"));
923
+ if (existingPass1.length === 0 && !pass2Exists) return { wasFreshClean };
924
+
925
+ if (parsedArgs.force) {
926
+ // --force: clean all generated files for truly fresh start
927
+ const genFiles = fs.readdirSync(GENERATED_DIR).filter(f => f.endsWith(".json") || f.endsWith(".md"));
928
+ for (const f of genFiles) fs.unlinkSync(path.join(GENERATED_DIR, f));
929
+ // Also clean any leftover .staged-rules/ from a prior crashed run
930
+ // (only .json/.md are unlinked above; directories aren't touched).
931
+ const stagedDir = path.join(GENERATED_DIR, ".staged-rules");
932
+ if (fileExists(stagedDir)) fs.rmSync(stagedDir, { recursive: true, force: true });
933
+ // Also wipe .claude/rules/ so Guard 2 (zero-rules detection) can't
934
+ // false-negative on stale rules from a previous run when the fresh
935
+ // Pass 3 run fails silently (e.g. Claude ignores staging-override).
936
+ // Step [2] recreates the subdirs from scratch. Any manual edits the
937
+ // user made to rule files are lost — acceptable under --force
938
+ // ("truly fresh start").
939
+ const rulesDir = path.join(PROJECT_ROOT, ".claude/rules");
940
+ if (fileExists(rulesDir)) fs.rmSync(rulesDir, { recursive: true, force: true });
941
+ wasFreshClean = true;
942
+ log(" 🔄 Previous results deleted (--force)\n");
943
+ return { wasFreshClean };
857
944
  }
858
945
 
859
- log("");
860
- log("╔════════════════════════════════════════════════════╗");
861
- log("║ ClaudeOS-Core Bootstrap (4-Pass) ║");
862
- log("╚════════════════════════════════════════════════════╝");
863
- log(` Project root: ${PROJECT_ROOT}`);
864
- log(` Language: ${SUPPORTED_LANGS[lang]} (${lang})`);
865
- log("");
946
+ // v2.2.0 upgrade detection: if project was generated with older claudeos-core
947
+ // (pre-2.2.0), default "resume" mode will skip regeneration of existing files
948
+ // per Rule B idempotency, meaning v2.2.0 structural improvements will NOT be
949
+ // picked up. Detect this case by checking CLAUDE.md for v2.2.0 markers.
950
+ const claudeMd = path.join(PROJECT_ROOT, "CLAUDE.md");
951
+ if (fileExists(claudeMd)) {
952
+ try {
953
+ const content = fs.readFileSync(claudeMd, "utf-8");
954
+ // v2.2.0 scaffold enforces EXACTLY 8 top-level `##` sections.
955
+ // Pre-v2.2.0 CLAUDE.md files typically carry 9+ sections (extra
956
+ // "Rules Summary" / "Common Rules" / "Required to Observe"
957
+ // blocks that v2.2.0 forbids). Counting `^## ` headings is a
958
+ // language-independent heuristic that works across all 10
959
+ // supported output languages. False positive (an existing
960
+ // 8-section pre-v2.2.0 CLAUDE.md) is acceptable — the user
961
+ // simply won't see the upgrade warning and can still run
962
+ // `--force` manually.
963
+ const sectionCount = (content.match(/^## /gm) || []).length;
964
+ const hasV220Section8 = sectionCount === 8;
965
+ if (!hasV220Section8) {
966
+ log("\n ⚠️ v2.2.0 upgrade detected");
967
+ log(" ─────────────────────────");
968
+ log(" Your existing CLAUDE.md was generated with an older claudeos-core version.");
969
+ log(" v2.2.0 introduces structural changes that the default 'resume' mode");
970
+ log(" CANNOT apply because existing files are preserved under Rule B (idempotency).");
971
+ log("");
972
+ log(" To fully adopt v2.2.0, choose one of:");
973
+ log(" 1. Rerun with --force: npx claudeos-core init --force");
974
+ log(" (overwrites generated files; your memory/ content is preserved)");
975
+ log(" 2. Choose 'fresh' below (equivalent to --force)");
976
+ log("");
977
+ log(" See CHANGELOG.md Migration section for full details.\n");
978
+ }
979
+ } catch (_) { /* Read error is non-fatal; proceed to resume prompt */ }
980
+ }
866
981
 
867
- // ─── [1] Install dependencies ────────────────────────────────
868
- header("[1] Installing dependencies...");
869
- if (!fileExists(path.join(TOOLS_DIR, "node_modules"))) {
870
- run("npm install --silent", { cwd: TOOLS_DIR });
982
+ const status = { pass1Done: existingPass1.length, pass2Done: pass2Exists };
983
+ const mode = await selectResumeMode(lang, status);
984
+ if (!mode) throw new InitError("Cancelled.");
985
+ if (mode === "fresh") {
986
+ for (const f of existingPass1) fs.unlinkSync(path.join(GENERATED_DIR, f));
987
+ if (pass2Exists) fs.unlinkSync(path.join(GENERATED_DIR, "pass2-merged.json"));
988
+ // Also reset pass 3 & pass 4 markers so they re-run
989
+ const pass3M = path.join(GENERATED_DIR, "pass3-complete.json");
990
+ const pass4M = path.join(GENERATED_DIR, "pass4-memory.json");
991
+ if (fileExists(pass3M)) fs.unlinkSync(pass3M);
992
+ if (fileExists(pass4M)) fs.unlinkSync(pass4M);
993
+ // Clean .staged-rules/ leftover from a prior crashed run (same reason as --force branch).
994
+ const stagedDir = path.join(GENERATED_DIR, ".staged-rules");
995
+ if (fileExists(stagedDir)) fs.rmSync(stagedDir, { recursive: true, force: true });
996
+ // Wipe .claude/rules/ for the same Guard 2 false-negative reason as
997
+ // the --force branch. Step [2] recreates the subdirs; any manual
998
+ // edits are lost — acceptable under an explicit "fresh" choice.
999
+ const rulesDir = path.join(PROJECT_ROOT, ".claude/rules");
1000
+ if (fileExists(rulesDir)) fs.rmSync(rulesDir, { recursive: true, force: true });
1001
+ wasFreshClean = true;
1002
+ } else if (mode === "continue" && existingPass1.length === 0 && pass2Exists) {
1003
+ // pass2 exists but no pass1 → pass2 is stale, force re-run
1004
+ fs.unlinkSync(path.join(GENERATED_DIR, "pass2-merged.json"));
1005
+ log(" ⚠️ pass2-merged.json deleted (no pass1 files to continue from)");
871
1006
  }
872
- log(" ✅ Done\n");
873
1007
 
874
- // ─── [2] Create directory structure ─────────────────────────
875
- header("[2] Creating directory structure...");
1008
+ return { wasFreshClean };
1009
+ }
1010
+
1011
+ // ─── Stage 4: Create directory structure ──────────────────────────
1012
+ function ensureDirectories() {
876
1013
  const dirs = [
877
1014
  ".claude/rules/00.core",
878
1015
  ".claude/rules/10.backend",
@@ -882,15 +1019,26 @@ async function cmdInit(parsedArgs) {
882
1019
  ".claude/rules/50.sync",
883
1020
  "claudeos-core/generated",
884
1021
  "claudeos-core/standard/00.core",
885
- "claudeos-core/standard/10.backend-api",
886
- "claudeos-core/standard/20.frontend-ui",
1022
+ "claudeos-core/standard/10.backend",
1023
+ "claudeos-core/standard/20.frontend",
887
1024
  "claudeos-core/standard/30.security-db",
888
1025
  "claudeos-core/standard/40.infra",
889
- "claudeos-core/standard/50.verification",
1026
+ "claudeos-core/standard/80.verification",
890
1027
  "claudeos-core/standard/90.optional",
891
1028
  "claudeos-core/skills/00.shared",
892
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",
893
1040
  "claudeos-core/skills/20.frontend-page/scaffold-page-feature",
1041
+ "claudeos-core/skills/20.frontend-page/domains",
894
1042
  "claudeos-core/skills/50.testing",
895
1043
  "claudeos-core/skills/90.experimental",
896
1044
  "claudeos-core/guide/01.onboarding",
@@ -901,20 +1049,36 @@ async function cmdInit(parsedArgs) {
901
1049
  "claudeos-core/mcp-guide",
902
1050
  "claudeos-core/memory",
903
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",
904
1074
  ];
905
1075
  for (const d of dirs) {
906
1076
  ensureDir(path.join(PROJECT_ROOT, d));
907
1077
  }
908
- log(" ✅ Done\n");
909
-
910
- // ─── [3] Run plan-installer ─────────────────────────
911
- header("[3] Analyzing project (plan-installer)...");
912
- run(`node "${path.join(TOOLS_DIR, "plan-installer/index.js")}"`);
913
- log("");
914
-
915
- // ─── [4] Pass 1: Deep analysis per domain group ──────────────────
916
- header("[4] Pass 1 — Deep analysis per domain group...");
1078
+ }
917
1079
 
1080
+ // ─── Stage 5: Load & validate domain-groups.json ──────────────────
1081
+ function loadDomainGroups() {
918
1082
  let domainGroups;
919
1083
  try {
920
1084
  domainGroups = JSON.parse(
@@ -927,8 +1091,15 @@ async function cmdInit(parsedArgs) {
927
1091
  if (!totalGroups || typeof totalGroups !== "number" || totalGroups < 1) {
928
1092
  throw new InitError(`domain-groups.json has invalid totalGroups: ${totalGroups}\n Re-run plan-installer or check claudeos-core/generated/`);
929
1093
  }
1094
+ if (!domainGroups.groups || totalGroups !== domainGroups.groups.length) {
1095
+ throw new InitError(`domain-groups.json is malformed: expected ${totalGroups} groups, found ${domainGroups.groups ? domainGroups.groups.length : 0}`);
1096
+ }
1097
+ return { domainGroups, totalGroups };
1098
+ }
930
1099
 
931
- // Load pass1 prompts by type
1100
+ // Loads the per-type pass1 prompt templates. Falls back to the single-stack
1101
+ // pass1-prompt.md for backward compatibility with older plan-installer output.
1102
+ function loadPass1Prompts() {
932
1103
  const pass1Prompts = {};
933
1104
  for (const type of ["backend", "frontend"]) {
934
1105
  const promptFile = path.join(GENERATED_DIR, `pass1-${type}-prompt.md`);
@@ -936,23 +1107,17 @@ async function cmdInit(parsedArgs) {
936
1107
  pass1Prompts[type] = readFile(promptFile);
937
1108
  }
938
1109
  }
939
- // Single-stack backward compatibility
940
1110
  if (Object.keys(pass1Prompts).length === 0) {
941
1111
  const fallback = path.join(GENERATED_DIR, "pass1-prompt.md");
942
1112
  if (fileExists(fallback)) pass1Prompts["backend"] = readFile(fallback);
943
1113
  }
1114
+ return pass1Prompts;
1115
+ }
944
1116
 
945
- if (!domainGroups.groups || totalGroups !== domainGroups.groups.length) {
946
- throw new InitError(`domain-groups.json is malformed: expected ${totalGroups} groups, found ${domainGroups.groups ? domainGroups.groups.length : 0}`);
947
- }
948
-
949
- // Progress tracking: Pass 1 (N groups) + Pass 2 + Pass 3 + Pass 4 = totalSteps
950
- const totalSteps = totalGroups + 3;
951
- let completedSteps = 0;
952
- const stepTimes = [];
953
- const passStart = Date.now();
954
-
955
- function progressBar(step, label) {
1117
+ // Creates the progressBar closure. Extracted from cmdInit so the bar
1118
+ // formatting is testable in isolation if needed.
1119
+ function makeProgressBar(totalSteps, passStart, stepTimes) {
1120
+ return function progressBar(step, label) {
956
1121
  const pct = Math.round((step / totalSteps) * 100);
957
1122
  const elapsed = Date.now() - passStart;
958
1123
  let eta = "";
@@ -964,7 +1129,14 @@ async function cmdInit(parsedArgs) {
964
1129
  const filled = Math.round(pct / 5);
965
1130
  const bar = "█".repeat(filled) + "░".repeat(20 - filled);
966
1131
  log(` [${bar}] ${pct}% (${step}/${totalSteps}) ${formatElapsed(elapsed)}${eta} — ${label}`);
967
- }
1132
+ };
1133
+ }
1134
+
1135
+ // ─── Stage 6: Pass 1 — Deep analysis per domain group ─────────────
1136
+ // Returns the number of steps to add to the outer completedSteps counter.
1137
+ async function runPass1Loop(opts) {
1138
+ const { domainGroups, totalGroups, pass1Prompts, progressBar, stepTimes, startingStep } = opts;
1139
+ let step = startingStep;
968
1140
 
969
1141
  for (let i = 1; i <= totalGroups; i++) {
970
1142
  const group = domainGroups.groups[i - 1];
@@ -984,7 +1156,7 @@ async function cmdInit(parsedArgs) {
984
1156
  const existing = JSON.parse(readFile(pass1Json));
985
1157
  if (existing && existing.analysisPerDomain) {
986
1158
  log(` ⏭️ pass1-${i}.json already exists, skipping`);
987
- completedSteps++;
1159
+ step++;
988
1160
  continue;
989
1161
  }
990
1162
  } catch (_e) { /* malformed — re-run */ }
@@ -1024,13 +1196,17 @@ async function cmdInit(parsedArgs) {
1024
1196
  throw new InitError(`pass1-${i}.json was not created. Claude may have run but not produced expected output.\n Ensure the prompt instructs Claude to write to claudeos-core/generated/pass1-${i}.json`);
1025
1197
  }
1026
1198
 
1027
- completedSteps++;
1028
- progressBar(completedSteps, `pass1-${i}.json created (${formatElapsed(elapsed1)})`);
1199
+ step++;
1200
+ progressBar(step, `pass1-${i}.json created (${formatElapsed(elapsed1)})`);
1029
1201
  }
1030
1202
  log("");
1203
+ return step - startingStep;
1204
+ }
1031
1205
 
1032
- // ─── [5] Pass 2: Merge analysis results ──────────────────────
1033
- header("[5] Pass 2 Merging analysis results...");
1206
+ // ─── Stage 7: Pass 2 Merge analysis results ─────────────────────
1207
+ // Returns the number of steps to add to the outer completedSteps counter (0 or 1).
1208
+ async function runPass2(opts) {
1209
+ const { progressBar, stepTimes, nextStep } = opts;
1034
1210
 
1035
1211
  const pass2Json = path.join(GENERATED_DIR, "pass2-merged.json");
1036
1212
 
@@ -1057,46 +1233,47 @@ async function cmdInit(parsedArgs) {
1057
1233
 
1058
1234
  if (pass2IsValid) {
1059
1235
  log(" ⏭️ pass2-merged.json already exists, skipping");
1060
- completedSteps++;
1061
- } else {
1062
- const pass2PromptFile = path.join(GENERATED_DIR, "pass2-prompt.md");
1063
- if (!fileExists(pass2PromptFile)) {
1064
- throw new InitError("pass2-prompt.md not found. Re-run plan-installer.");
1065
- }
1066
- let prompt = injectProjectRoot(readFile(pass2PromptFile));
1236
+ return 1;
1237
+ }
1067
1238
 
1068
- const t2 = Date.now();
1069
- const ticker2 = makePassTicker("Pass 2", t2);
1070
- const ok = await runClaudePromptAsync(prompt, {
1071
- onTick: ticker2.onTick,
1072
- tickMs: ticker2.tickMs,
1073
- });
1074
- ticker2.clearLine();
1075
- const elapsed2 = Date.now() - t2;
1076
- stepTimes.push(elapsed2);
1239
+ const pass2PromptFile = path.join(GENERATED_DIR, "pass2-prompt.md");
1240
+ if (!fileExists(pass2PromptFile)) {
1241
+ throw new InitError("pass2-prompt.md not found. Re-run plan-installer.");
1242
+ }
1243
+ let prompt = injectProjectRoot(readFile(pass2PromptFile));
1077
1244
 
1078
- if (!ok) {
1079
- throw new InitError("Pass 2 failed. Check the claude error output above.\n If this persists, try: npx claudeos-core init --force");
1080
- }
1245
+ const t2 = Date.now();
1246
+ const ticker2 = makePassTicker("Pass 2", t2);
1247
+ const ok = await runClaudePromptAsync(prompt, {
1248
+ onTick: ticker2.onTick,
1249
+ tickMs: ticker2.tickMs,
1250
+ });
1251
+ ticker2.clearLine();
1252
+ const elapsed2 = Date.now() - t2;
1253
+ stepTimes.push(elapsed2);
1081
1254
 
1082
- if (!fileExists(pass2Json)) {
1083
- throw new InitError("pass2-merged.json was not created. Claude may have run but not produced expected output.");
1084
- }
1255
+ if (!ok) {
1256
+ throw new InitError("Pass 2 failed. Check the claude error output above.\n If this persists, try: npx claudeos-core init --force");
1257
+ }
1085
1258
 
1086
- completedSteps++;
1087
- progressBar(completedSteps, `pass2-merged.json created (${formatElapsed(elapsed2)})`);
1259
+ if (!fileExists(pass2Json)) {
1260
+ throw new InitError("pass2-merged.json was not created. Claude may have run but not produced expected output.");
1088
1261
  }
1089
- log("");
1090
1262
 
1091
- // ─── [5.5] v2.1: Build pass3-context.json (slim summary for Pass 3) ──
1092
- // Writes a small (<5 KB) structured summary derived from project-analysis.json
1093
- // plus pass2-merged.json signals (size, top-level keys). Pass 3 prompts
1094
- // reference this INSTEAD OF re-reading pass2-merged.json repeatedly, which
1095
- // was the primary cause of `Prompt is too long` failures on large projects.
1096
- //
1097
- // Silent-on-failure: if pass3-context-builder returns null (e.g.
1098
- // project-analysis.json missing), we skip writing and let Pass 3 fall back
1099
- // to the pre-v2.1 behavior of reading pass2-merged.json directly.
1263
+ progressBar(nextStep, `pass2-merged.json created (${formatElapsed(elapsed2)})`);
1264
+ return 1;
1265
+ }
1266
+
1267
+ // ─── Stage 8: Build pass3-context.json (v2.1) ─────────────────────
1268
+ // Writes a small (<5 KB) structured summary derived from project-analysis.json
1269
+ // plus pass2-merged.json signals (size, top-level keys). Pass 3 prompts
1270
+ // reference this INSTEAD OF re-reading pass2-merged.json repeatedly, which
1271
+ // was the primary cause of `Prompt is too long` failures on large projects.
1272
+ //
1273
+ // Silent-on-failure: if pass3-context-builder returns null (e.g.
1274
+ // project-analysis.json missing), we skip writing and let Pass 3 fall back
1275
+ // to the pre-v2.1 behavior of reading pass2-merged.json directly.
1276
+ function buildPass3ContextJson() {
1100
1277
  try {
1101
1278
  const { buildPass3Context } = require("../../plan-installer/pass3-context-builder");
1102
1279
  const pass3Ctx = buildPass3Context(GENERATED_DIR);
@@ -1116,11 +1293,15 @@ async function cmdInit(parsedArgs) {
1116
1293
  } catch (e) {
1117
1294
  log(` ⚠️ pass3-context.json build skipped: ${e.message} (Pass 3 will fall back to pass2-merged.json)`);
1118
1295
  }
1119
- log("");
1120
-
1121
- // ─── [6] Pass 3: Generate + verify ─────────────────────────
1122
- header("[6] Pass 3 — Generating all files...");
1296
+ }
1123
1297
 
1298
+ // ─── Stage 9a: Pass 3 marker pre-processing ───────────────────────
1299
+ // Handles v1.7.x migration backfill + stale-marker detection (guide/outputs).
1300
+ // The stale region below MUST include dropStalePass3Marker, EXPECTED_GUIDE_FILES,
1301
+ // and findMissingOutputs — tested for by tests/pass3-marker.test.js source
1302
+ // parity. The `completedSteps++` sentinel used by that region's regex lives
1303
+ // in cmdInit directly, after dispatchPass3 returns.
1304
+ function handlePass3StaleMarker(wasFreshClean) {
1124
1305
  const pass3Marker = path.join(GENERATED_DIR, "pass3-complete.json");
1125
1306
  const claudeMdPath = path.join(PROJECT_ROOT, "CLAUDE.md");
1126
1307
 
@@ -1224,6 +1405,17 @@ async function cmdInit(parsedArgs) {
1224
1405
  }
1225
1406
  }
1226
1407
  }
1408
+ }
1409
+
1410
+ // ─── Stage 9b: Pass 3 dispatch (decide + run) ─────────────────────
1411
+ // Returns { ran: boolean } so the caller can increment completedSteps
1412
+ // with the literal "completedSteps++" token that the stale-region
1413
+ // source-parity test regex requires.
1414
+ async function dispatchPass3(opts) {
1415
+ const { wasFreshClean, lang, stepTimes, progressBar, nextStep } = opts;
1416
+
1417
+ const pass3Marker = path.join(GENERATED_DIR, "pass3-complete.json");
1418
+ const claudeMdPath = path.join(PROJECT_ROOT, "CLAUDE.md");
1227
1419
 
1228
1420
  // Pass 3 split mode resolution.
1229
1421
  //
@@ -1243,8 +1435,8 @@ async function cmdInit(parsedArgs) {
1243
1435
  try {
1244
1436
  const ctxPath = path.join(GENERATED_DIR, "pass3-context.json");
1245
1437
  if (fileExists(ctxPath)) {
1246
- const ctx = JSON.parse(readFile(ctxPath));
1247
- const rec = ctx && ctx.splitRecommendation;
1438
+ const pctx = JSON.parse(readFile(ctxPath));
1439
+ const rec = pctx && pctx.splitRecommendation;
1248
1440
  if (rec) {
1249
1441
  log(` • estimated ${rec.estimatedFileCount} files from ${rec.totalDomains} domains`);
1250
1442
  }
@@ -1298,17 +1490,20 @@ async function cmdInit(parsedArgs) {
1298
1490
  EXPECTED_GUIDE_FILES, findMissingOutputs,
1299
1491
  lang, stepTimes,
1300
1492
  });
1301
- completedSteps++;
1302
- progressBar(completedSteps, `Pass 3 complete (split mode)`);
1493
+ progressBar(nextStep, `Pass 3 complete (split mode)`);
1303
1494
  log("");
1304
- } else {
1305
- log(" ⏭️ pass3-complete.json already complete, skipping");
1306
- completedSteps++;
1495
+ return { ran: true };
1307
1496
  }
1308
- log("");
1309
1497
 
1310
- // ─── [7] Pass 4: L4 memory scaffolding ────────────
1311
- header("[7] Pass 4 Memory scaffolding...");
1498
+ log(" ⏭️ pass3-complete.json already complete, skipping");
1499
+ return { ran: false };
1500
+ }
1501
+
1502
+ // ─── Stage 10: Pass 4 — L4 memory scaffolding ─────────────────────
1503
+ // Returns 1 unconditionally (Pass 4 always counts as a completed step,
1504
+ // whether skip / static fallback / Claude-driven).
1505
+ async function runPass4(opts) {
1506
+ const { lang, stepTimes, progressBar, nextStep } = opts;
1312
1507
 
1313
1508
  const pass4Marker = path.join(GENERATED_DIR, "pass4-memory.json");
1314
1509
  const pass4PromptFile = path.join(GENERATED_DIR, "pass4-prompt.md");
@@ -1542,13 +1737,12 @@ async function cmdInit(parsedArgs) {
1542
1737
  // when we actually did real work, so ETA for future steps stays meaningful.
1543
1738
  const pass4Elapsed = Date.now() - pass4Start;
1544
1739
  if (pass4Elapsed > 500) stepTimes.push(pass4Elapsed);
1545
- completedSteps++;
1546
- progressBar(completedSteps, pass4Label);
1547
- log("");
1548
-
1549
- // ─── [8] Run verification tools ───────────────────────────────
1550
- header("[8] Running verification tools...");
1740
+ progressBar(nextStep, pass4Label);
1741
+ return 1;
1742
+ }
1551
1743
 
1744
+ // ─── Stage 11: Run external verification tools ────────────────────
1745
+ function runVerificationTools() {
1552
1746
  const verifyTools = [
1553
1747
  { name: "manifest-generator", script: path.join(TOOLS_DIR, "manifest-generator/index.js") },
1554
1748
  { name: "health-checker", script: path.join(TOOLS_DIR, "health-checker/index.js") },
@@ -1564,21 +1758,12 @@ async function cmdInit(parsedArgs) {
1564
1758
  log(` ⚠️ ${t.name} reported issues (non-fatal)`);
1565
1759
  }
1566
1760
  }
1567
- log("");
1568
-
1569
- // ─── Complete ─────────────────────────────────────────────
1570
- const totalFiles = countFiles();
1571
- const pass1Files = countPass1Files();
1761
+ }
1572
1762
 
1573
- // ─── Structural lint (v2.3.0+) ────────────────────────────
1574
- // Run the language-invariant CLAUDE.md validator after all passes
1575
- // complete. This catches the §9 L4-memory re-declaration anti-pattern
1576
- // and other structural drift the scaffold + prompt-level instructions
1577
- // alone cannot reliably prevent across 10 output languages.
1578
- //
1579
- // Failures do NOT abort the run — the generated content is still
1580
- // useful and the user can either re-run with --force or hand-edit the
1581
- // flagged sections. The report is purely informational here.
1763
+ // ─── Stage 12: Structural lint (v2.3.0+) ──────────────────────────
1764
+ // Run the language-invariant CLAUDE.md validator after all passes complete.
1765
+ // Failures do NOT abort the run informational only.
1766
+ function runLint() {
1582
1767
  try {
1583
1768
  const { validate } = require("../../claude-md-validator");
1584
1769
  const { formatSummaryLine } = require("../../claude-md-validator/reporter");
@@ -1602,25 +1787,31 @@ async function cmdInit(parsedArgs) {
1602
1787
  log(` ⚠️ Lint step skipped: ${e.message || e}`);
1603
1788
  log("");
1604
1789
  }
1790
+ }
1605
1791
 
1606
- // ─── Content integrity (Guard 4 — v2.3.0+) ─────────────────
1607
- // Runs content-validator's path-claim + MANIFEST drift checks as a
1608
- // non-blocking final step after all passes. We deliberately do NOT
1609
- // throw or unset pass3-complete.json here:
1610
- // - Re-running Pass 3 is not guaranteed to fix LLM hallucinations
1611
- // (the same fact JSON may trigger the same mis-inference again),
1612
- // so a throw could deadlock the user in an `init --force` loop.
1613
- // - The report + the non-zero exit of `npx claudeos-core lint`
1614
- // already surface the issues. CI pipelines catch them via exit
1615
- // code; local users see them inline here.
1616
- // When real drift is detected, we print a pointer to the standalone
1617
- // CLI so the user can re-run selectively without repeating init.
1792
+ // ─── Stage 13: Content integrity (Guard 4 — v2.3.0+) ──────────────
1793
+ // Runs content-validator's path-claim + MANIFEST drift checks as a
1794
+ // non-blocking final step after all passes. These are *advisories*, not
1795
+ // generation failures the documents are usable as-is, the advisories
1796
+ // just flag spots where an LLM may have guessed at a filename or a
1797
+ // skill registration may have drifted. We deliberately do NOT throw or
1798
+ // unset pass3-complete.json here:
1799
+ // - Re-running Pass 3 is not guaranteed to fix LLM hallucinations
1800
+ // (the same fact JSON may trigger the same mis-inference again),
1801
+ // so a throw could deadlock the user in an `init --force` loop.
1802
+ // - content-validator's non-zero exit is preserved so that
1803
+ // `npx claudeos-core health` (and any CI wired to it) still treats
1804
+ // advisories as a real gate. `init` just presents them with softer
1805
+ // UX because by the time `init` finishes, the user's docs are
1806
+ // already on disk and fully usable.
1807
+ function runContentValidator() {
1618
1808
  try {
1619
1809
  const cvPath = path.join(__dirname, "..", "..", "content-validator", "index.js");
1620
1810
  if (fileExists(cvPath)) {
1621
1811
  log(" [Content] Checking path-claims and MANIFEST consistency...");
1622
- // Run in a child process so its process.exit(1) on errors does
1623
- // not terminate init. We only surface the exit code as a warning.
1812
+ // Run in a child process so its process.exit(1) on advisories does
1813
+ // not terminate init. The exit code is informational for us we
1814
+ // still surface the content as advisories regardless.
1624
1815
  const { spawnSync } = require("child_process");
1625
1816
  const result = spawnSync(process.execPath, [cvPath], {
1626
1817
  cwd: PROJECT_ROOT,
@@ -1634,11 +1825,11 @@ async function cmdInit(parsedArgs) {
1634
1825
  log(summary.split("\n").map((l) => " " + l).join("\n"));
1635
1826
  if (result.status !== 0) {
1636
1827
  log("");
1637
- log(" ℹ️ Content drift detected. This does NOT invalidate the");
1638
- log(" generated documents, but indicates stale path references");
1639
- log(" or MANIFEST ↔ CLAUDE.md mismatch. Details:");
1640
- log(" - stale-report.json (full error list)");
1641
- log(" - Re-run: node content-validator/index.js");
1828
+ log(" ℹ️ Content advisories detected these are quality notes,");
1829
+ log(" NOT generation failures. Your generated docs are ready");
1830
+ log(" to use as-is. Review when convenient:");
1831
+ log(" - stale-report.json (full advisory list)");
1832
+ log(" - npx claudeos-core health (standalone gate with exit code)");
1642
1833
  }
1643
1834
  log("");
1644
1835
  }
@@ -1646,11 +1837,17 @@ async function cmdInit(parsedArgs) {
1646
1837
  log(` ⚠️ Content check skipped: ${e.message || e}`);
1647
1838
  log("");
1648
1839
  }
1840
+ }
1649
1841
 
1650
- log("");
1842
+ // ─── Stage 14: Print completion banner ────────────────────────────
1843
+ function printCompletionBanner(opts) {
1844
+ const { lang, totalGroups, totalStart } = opts;
1845
+ const totalFiles = countFiles();
1846
+ const pass1Files = countPass1Files();
1651
1847
  const memoryReady = fileExists(path.join(PROJECT_ROOT, "claudeos-core/memory/decision-log.md"));
1652
1848
  const rulesReady = fileExists(path.join(PROJECT_ROOT, ".claude/rules/60.memory/01.decision-log.md"));
1653
1849
  const l4Status = (memoryReady && rulesReady) ? "memory + rules" : "partial";
1850
+ log("");
1654
1851
  log("╔════════════════════════════════════════════════════╗");
1655
1852
  log("║ ✅ ClaudeOS-Core — Complete ║");
1656
1853
  log("║ ║");
@@ -1670,4 +1867,113 @@ async function cmdInit(parsedArgs) {
1670
1867
  log("");
1671
1868
  }
1672
1869
 
1870
+ // ═══════════════════════════════════════════════════════════════════
1871
+ // Main orchestrator
1872
+ // ═══════════════════════════════════════════════════════════════════
1873
+ async function cmdInit(parsedArgs) {
1874
+ const totalStart = Date.now();
1875
+
1876
+ // ─── Prerequisites check ───────────────────────────────────
1877
+ checkPrerequisites();
1878
+
1879
+ // ─── Language selection ────────────────────────────────────
1880
+ const lang = await resolveLanguage(parsedArgs);
1881
+
1882
+ // ─── Resume / Fresh selection ──────────────────────────────
1883
+ // wasFreshClean: tracks whether we just wiped generated state via --force
1884
+ // or "fresh" resume mode. Used by the Pass 3 backfill guard below:
1885
+ // fresh/force explicitly means "regenerate from scratch", so a leftover
1886
+ // CLAUDE.md from a prior run must NOT cause Pass 3 to be skipped via the
1887
+ // v1.7.x migration backfill.
1888
+ const { wasFreshClean } = await applyResumeMode(parsedArgs, lang);
1889
+
1890
+ log("");
1891
+ log("╔════════════════════════════════════════════════════╗");
1892
+ log("║ ClaudeOS-Core — Bootstrap (4-Pass) ║");
1893
+ log("╚════════════════════════════════════════════════════╝");
1894
+ log(` Project root: ${PROJECT_ROOT}`);
1895
+ log(` Language: ${SUPPORTED_LANGS[lang]} (${lang})`);
1896
+ log("");
1897
+
1898
+ // ─── [1] Install dependencies ──────────────────────────────
1899
+ header("[1] Installing dependencies...");
1900
+ if (!fileExists(path.join(TOOLS_DIR, "node_modules"))) {
1901
+ run("npm install --silent", { cwd: TOOLS_DIR });
1902
+ }
1903
+ log(" ✅ Done\n");
1904
+
1905
+ // ─── [2] Create directory structure ────────────────────────
1906
+ header("[2] Creating directory structure...");
1907
+ ensureDirectories();
1908
+ log(" ✅ Done\n");
1909
+
1910
+ // ─── [3] Run plan-installer ────────────────────────────────
1911
+ header("[3] Analyzing project (plan-installer)...");
1912
+ run(`node "${path.join(TOOLS_DIR, "plan-installer/index.js")}"`);
1913
+ log("");
1914
+
1915
+ // ─── [4] Pass 1: Deep analysis per domain group ────────────
1916
+ header("[4] Pass 1 — Deep analysis per domain group...");
1917
+ const { domainGroups, totalGroups } = loadDomainGroups();
1918
+ const pass1Prompts = loadPass1Prompts();
1919
+
1920
+ // Progress tracking: Pass 1 (N groups) + Pass 2 + Pass 3 + Pass 4 = totalSteps
1921
+ const totalSteps = totalGroups + 3;
1922
+ let completedSteps = 0;
1923
+ const stepTimes = [];
1924
+ const passStart = Date.now();
1925
+ const progressBar = makeProgressBar(totalSteps, passStart, stepTimes);
1926
+
1927
+ const p1Delta = await runPass1Loop({
1928
+ domainGroups, totalGroups, pass1Prompts,
1929
+ progressBar, stepTimes,
1930
+ startingStep: completedSteps,
1931
+ });
1932
+ completedSteps += p1Delta;
1933
+
1934
+ // ─── [5] Pass 2: Merge analysis results ────────────────────
1935
+ header("[5] Pass 2 — Merging analysis results...");
1936
+ const p2Delta = await runPass2({
1937
+ progressBar, stepTimes, nextStep: completedSteps + 1,
1938
+ });
1939
+ completedSteps += p2Delta;
1940
+ log("");
1941
+
1942
+ // ─── [5.5] Build pass3-context.json (v2.1) ─────────────────
1943
+ buildPass3ContextJson();
1944
+ log("");
1945
+
1946
+ // ─── [6] Pass 3: Generate + verify ─────────────────────────
1947
+ header("[6] Pass 3 — Generating all files...");
1948
+ handlePass3StaleMarker(wasFreshClean);
1949
+ const { ran: p3Ran } = await dispatchPass3({
1950
+ wasFreshClean, lang, stepTimes,
1951
+ progressBar, nextStep: completedSteps + 1,
1952
+ });
1953
+ if (p3Ran) completedSteps++;
1954
+ log("");
1955
+
1956
+ // ─── [7] Pass 4: L4 memory scaffolding ─────────────────────
1957
+ header("[7] Pass 4 — Memory scaffolding...");
1958
+ const p4Delta = await runPass4({
1959
+ lang, stepTimes, progressBar, nextStep: completedSteps + 1,
1960
+ });
1961
+ completedSteps += p4Delta;
1962
+ log("");
1963
+
1964
+ // ─── [8] Run verification tools ────────────────────────────
1965
+ header("[8] Running verification tools...");
1966
+ runVerificationTools();
1967
+ log("");
1968
+
1969
+ // ─── Structural lint (v2.3.0+) ─────────────────────────────
1970
+ runLint();
1971
+
1972
+ // ─── Content integrity (Guard 4 — v2.3.0+) ─────────────────
1973
+ runContentValidator();
1974
+
1975
+ // ─── Complete ──────────────────────────────────────────────
1976
+ printCompletionBanner({ lang, totalGroups, totalStart });
1977
+ }
1978
+
1673
1979
  module.exports = { cmdInit, InitError };