claudeos-core 2.3.1 → 2.3.2

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.
@@ -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");
@@ -701,15 +712,22 @@ async function runPass3Split(ctx) {
701
712
  log(` 🎉 Pass 3 split complete: ${completedGroups.length}/${totalStages} stages successful`);
702
713
  }
703
714
 
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 ───────────────────────────────────
715
+ // ═══════════════════════════════════════════════════════════════════
716
+ // cmdInit stage helpers
717
+ // ═══════════════════════════════════════════════════════════════════
718
+ // The original cmdInit was 970 lines with 77 if-statements and 17 try-blocks
719
+ // in a single function. Below it is decomposed into one helper per pipeline
720
+ // phase. Each helper owns a self-contained step with clear inputs/outputs.
721
+ //
722
+ // Shared state passed across helpers:
723
+ // - { lang, stepTimes, completedSteps (via ref), progressBar, wasFreshClean }
724
+ // Helpers that advance the outer progress bar return a `stepsDelta` value
725
+ // that cmdInit adds to `completedSteps` locally. This preserves the literal
726
+ // `completedSteps++` text at the top-level orchestrator, which several
727
+ // source-parity tests rely on for stale-check region detection.
728
+
729
+ // ─── Stage 1: Prerequisites check ──────────────────────────────────
730
+ function checkPrerequisites() {
713
731
  const hasProjectMarker = [".git", "package.json", "build.gradle", "build.gradle.kts", "pom.xml", "pyproject.toml", "requirements.txt"].some(
714
732
  m => fs.existsSync(path.join(PROJECT_ROOT, m))
715
733
  );
@@ -734,8 +752,10 @@ async function cmdInit(parsedArgs) {
734
752
  if (!claudeAuth) {
735
753
  throw new InitError("Claude Code may not be authenticated.\n Run: claude (and complete authentication)\n Then retry: npx claudeos-core init");
736
754
  }
755
+ }
737
756
 
738
- // ─── Language selection (required) ────────────────────────────
757
+ // ─── Stage 2: Resolve output language ─────────────────────────────
758
+ async function resolveLanguage(parsedArgs) {
739
759
  let lang = parsedArgs.lang;
740
760
  if (!lang) {
741
761
  lang = await selectLangInteractive();
@@ -765,114 +785,109 @@ async function cmdInit(parsedArgs) {
765
785
  }
766
786
 
767
787
  process.env.CLAUDEOS_LANG = lang;
788
+ return lang;
789
+ }
768
790
 
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
- }
791
+ // ─── Stage 3: Resume/Fresh selection ──────────────────────────────
792
+ // Returns { wasFreshClean: boolean } — the caller uses this to gate the
793
+ // v1.7.x migration backfill in dispatchPass3.
794
+ async function applyResumeMode(parsedArgs, lang) {
795
+ let wasFreshClean = false;
829
796
 
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
- }
797
+ if (!fs.existsSync(GENERATED_DIR)) return { wasFreshClean };
798
+
799
+ const existingPass1 = fs.readdirSync(GENERATED_DIR).filter(f => f.startsWith("pass1-") && f.endsWith(".json"));
800
+ const pass2Exists = fileExists(path.join(GENERATED_DIR, "pass2-merged.json"));
801
+ if (existingPass1.length === 0 && !pass2Exists) return { wasFreshClean };
802
+
803
+ if (parsedArgs.force) {
804
+ // --force: clean all generated files for truly fresh start
805
+ const genFiles = fs.readdirSync(GENERATED_DIR).filter(f => f.endsWith(".json") || f.endsWith(".md"));
806
+ for (const f of genFiles) fs.unlinkSync(path.join(GENERATED_DIR, f));
807
+ // Also clean any leftover .staged-rules/ from a prior crashed run
808
+ // (only .json/.md are unlinked above; directories aren't touched).
809
+ const stagedDir = path.join(GENERATED_DIR, ".staged-rules");
810
+ if (fileExists(stagedDir)) fs.rmSync(stagedDir, { recursive: true, force: true });
811
+ // Also wipe .claude/rules/ so Guard 2 (zero-rules detection) can't
812
+ // false-negative on stale rules from a previous run when the fresh
813
+ // Pass 3 run fails silently (e.g. Claude ignores staging-override).
814
+ // Step [2] recreates the subdirs from scratch. Any manual edits the
815
+ // user made to rule files are lost — acceptable under --force
816
+ // ("truly fresh start").
817
+ const rulesDir = path.join(PROJECT_ROOT, ".claude/rules");
818
+ if (fileExists(rulesDir)) fs.rmSync(rulesDir, { recursive: true, force: true });
819
+ wasFreshClean = true;
820
+ log(" 🔄 Previous results deleted (--force)\n");
821
+ return { wasFreshClean };
857
822
  }
858
823
 
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("");
824
+ // v2.2.0 upgrade detection: if project was generated with older claudeos-core
825
+ // (pre-2.2.0), default "resume" mode will skip regeneration of existing files
826
+ // per Rule B idempotency, meaning v2.2.0 structural improvements will NOT be
827
+ // picked up. Detect this case by checking CLAUDE.md for v2.2.0 markers.
828
+ const claudeMd = path.join(PROJECT_ROOT, "CLAUDE.md");
829
+ if (fileExists(claudeMd)) {
830
+ try {
831
+ const content = fs.readFileSync(claudeMd, "utf-8");
832
+ // v2.2.0 scaffold enforces EXACTLY 8 top-level `##` sections.
833
+ // Pre-v2.2.0 CLAUDE.md files typically carry 9+ sections (extra
834
+ // "Rules Summary" / "Common Rules" / "Required to Observe"
835
+ // blocks that v2.2.0 forbids). Counting `^## ` headings is a
836
+ // language-independent heuristic that works across all 10
837
+ // supported output languages. False positive (an existing
838
+ // 8-section pre-v2.2.0 CLAUDE.md) is acceptable — the user
839
+ // simply won't see the upgrade warning and can still run
840
+ // `--force` manually.
841
+ const sectionCount = (content.match(/^## /gm) || []).length;
842
+ const hasV220Section8 = sectionCount === 8;
843
+ if (!hasV220Section8) {
844
+ log("\n ⚠️ v2.2.0 upgrade detected");
845
+ log(" ─────────────────────────");
846
+ log(" Your existing CLAUDE.md was generated with an older claudeos-core version.");
847
+ log(" v2.2.0 introduces structural changes that the default 'resume' mode");
848
+ log(" CANNOT apply because existing files are preserved under Rule B (idempotency).");
849
+ log("");
850
+ log(" To fully adopt v2.2.0, choose one of:");
851
+ log(" 1. Rerun with --force: npx claudeos-core init --force");
852
+ log(" (overwrites generated files; your memory/ content is preserved)");
853
+ log(" 2. Choose 'fresh' below (equivalent to --force)");
854
+ log("");
855
+ log(" See CHANGELOG.md Migration section for full details.\n");
856
+ }
857
+ } catch (_) { /* Read error is non-fatal; proceed to resume prompt */ }
858
+ }
866
859
 
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 });
860
+ const status = { pass1Done: existingPass1.length, pass2Done: pass2Exists };
861
+ const mode = await selectResumeMode(lang, status);
862
+ if (!mode) throw new InitError("Cancelled.");
863
+ if (mode === "fresh") {
864
+ for (const f of existingPass1) fs.unlinkSync(path.join(GENERATED_DIR, f));
865
+ if (pass2Exists) fs.unlinkSync(path.join(GENERATED_DIR, "pass2-merged.json"));
866
+ // Also reset pass 3 & pass 4 markers so they re-run
867
+ const pass3M = path.join(GENERATED_DIR, "pass3-complete.json");
868
+ const pass4M = path.join(GENERATED_DIR, "pass4-memory.json");
869
+ if (fileExists(pass3M)) fs.unlinkSync(pass3M);
870
+ if (fileExists(pass4M)) fs.unlinkSync(pass4M);
871
+ // Clean .staged-rules/ leftover from a prior crashed run (same reason as --force branch).
872
+ const stagedDir = path.join(GENERATED_DIR, ".staged-rules");
873
+ if (fileExists(stagedDir)) fs.rmSync(stagedDir, { recursive: true, force: true });
874
+ // Wipe .claude/rules/ for the same Guard 2 false-negative reason as
875
+ // the --force branch. Step [2] recreates the subdirs; any manual
876
+ // edits are lost — acceptable under an explicit "fresh" choice.
877
+ const rulesDir = path.join(PROJECT_ROOT, ".claude/rules");
878
+ if (fileExists(rulesDir)) fs.rmSync(rulesDir, { recursive: true, force: true });
879
+ wasFreshClean = true;
880
+ } else if (mode === "continue" && existingPass1.length === 0 && pass2Exists) {
881
+ // pass2 exists but no pass1 → pass2 is stale, force re-run
882
+ fs.unlinkSync(path.join(GENERATED_DIR, "pass2-merged.json"));
883
+ log(" ⚠️ pass2-merged.json deleted (no pass1 files to continue from)");
871
884
  }
872
- log(" ✅ Done\n");
873
885
 
874
- // ─── [2] Create directory structure ─────────────────────────
875
- header("[2] Creating directory structure...");
886
+ return { wasFreshClean };
887
+ }
888
+
889
+ // ─── Stage 4: Create directory structure ──────────────────────────
890
+ function ensureDirectories() {
876
891
  const dirs = [
877
892
  ".claude/rules/00.core",
878
893
  ".claude/rules/10.backend",
@@ -905,16 +920,10 @@ async function cmdInit(parsedArgs) {
905
920
  for (const d of dirs) {
906
921
  ensureDir(path.join(PROJECT_ROOT, d));
907
922
  }
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...");
923
+ }
917
924
 
925
+ // ─── Stage 5: Load & validate domain-groups.json ──────────────────
926
+ function loadDomainGroups() {
918
927
  let domainGroups;
919
928
  try {
920
929
  domainGroups = JSON.parse(
@@ -927,8 +936,15 @@ async function cmdInit(parsedArgs) {
927
936
  if (!totalGroups || typeof totalGroups !== "number" || totalGroups < 1) {
928
937
  throw new InitError(`domain-groups.json has invalid totalGroups: ${totalGroups}\n Re-run plan-installer or check claudeos-core/generated/`);
929
938
  }
939
+ if (!domainGroups.groups || totalGroups !== domainGroups.groups.length) {
940
+ throw new InitError(`domain-groups.json is malformed: expected ${totalGroups} groups, found ${domainGroups.groups ? domainGroups.groups.length : 0}`);
941
+ }
942
+ return { domainGroups, totalGroups };
943
+ }
930
944
 
931
- // Load pass1 prompts by type
945
+ // Loads the per-type pass1 prompt templates. Falls back to the single-stack
946
+ // pass1-prompt.md for backward compatibility with older plan-installer output.
947
+ function loadPass1Prompts() {
932
948
  const pass1Prompts = {};
933
949
  for (const type of ["backend", "frontend"]) {
934
950
  const promptFile = path.join(GENERATED_DIR, `pass1-${type}-prompt.md`);
@@ -936,23 +952,17 @@ async function cmdInit(parsedArgs) {
936
952
  pass1Prompts[type] = readFile(promptFile);
937
953
  }
938
954
  }
939
- // Single-stack backward compatibility
940
955
  if (Object.keys(pass1Prompts).length === 0) {
941
956
  const fallback = path.join(GENERATED_DIR, "pass1-prompt.md");
942
957
  if (fileExists(fallback)) pass1Prompts["backend"] = readFile(fallback);
943
958
  }
959
+ return pass1Prompts;
960
+ }
944
961
 
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) {
962
+ // Creates the progressBar closure. Extracted from cmdInit so the bar
963
+ // formatting is testable in isolation if needed.
964
+ function makeProgressBar(totalSteps, passStart, stepTimes) {
965
+ return function progressBar(step, label) {
956
966
  const pct = Math.round((step / totalSteps) * 100);
957
967
  const elapsed = Date.now() - passStart;
958
968
  let eta = "";
@@ -964,7 +974,14 @@ async function cmdInit(parsedArgs) {
964
974
  const filled = Math.round(pct / 5);
965
975
  const bar = "█".repeat(filled) + "░".repeat(20 - filled);
966
976
  log(` [${bar}] ${pct}% (${step}/${totalSteps}) ${formatElapsed(elapsed)}${eta} — ${label}`);
967
- }
977
+ };
978
+ }
979
+
980
+ // ─── Stage 6: Pass 1 — Deep analysis per domain group ─────────────
981
+ // Returns the number of steps to add to the outer completedSteps counter.
982
+ async function runPass1Loop(opts) {
983
+ const { domainGroups, totalGroups, pass1Prompts, progressBar, stepTimes, startingStep } = opts;
984
+ let step = startingStep;
968
985
 
969
986
  for (let i = 1; i <= totalGroups; i++) {
970
987
  const group = domainGroups.groups[i - 1];
@@ -984,7 +1001,7 @@ async function cmdInit(parsedArgs) {
984
1001
  const existing = JSON.parse(readFile(pass1Json));
985
1002
  if (existing && existing.analysisPerDomain) {
986
1003
  log(` ⏭️ pass1-${i}.json already exists, skipping`);
987
- completedSteps++;
1004
+ step++;
988
1005
  continue;
989
1006
  }
990
1007
  } catch (_e) { /* malformed — re-run */ }
@@ -1024,13 +1041,17 @@ async function cmdInit(parsedArgs) {
1024
1041
  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
1042
  }
1026
1043
 
1027
- completedSteps++;
1028
- progressBar(completedSteps, `pass1-${i}.json created (${formatElapsed(elapsed1)})`);
1044
+ step++;
1045
+ progressBar(step, `pass1-${i}.json created (${formatElapsed(elapsed1)})`);
1029
1046
  }
1030
1047
  log("");
1048
+ return step - startingStep;
1049
+ }
1031
1050
 
1032
- // ─── [5] Pass 2: Merge analysis results ──────────────────────
1033
- header("[5] Pass 2 Merging analysis results...");
1051
+ // ─── Stage 7: Pass 2 Merge analysis results ─────────────────────
1052
+ // Returns the number of steps to add to the outer completedSteps counter (0 or 1).
1053
+ async function runPass2(opts) {
1054
+ const { progressBar, stepTimes, nextStep } = opts;
1034
1055
 
1035
1056
  const pass2Json = path.join(GENERATED_DIR, "pass2-merged.json");
1036
1057
 
@@ -1057,46 +1078,47 @@ async function cmdInit(parsedArgs) {
1057
1078
 
1058
1079
  if (pass2IsValid) {
1059
1080
  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));
1081
+ return 1;
1082
+ }
1067
1083
 
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);
1084
+ const pass2PromptFile = path.join(GENERATED_DIR, "pass2-prompt.md");
1085
+ if (!fileExists(pass2PromptFile)) {
1086
+ throw new InitError("pass2-prompt.md not found. Re-run plan-installer.");
1087
+ }
1088
+ let prompt = injectProjectRoot(readFile(pass2PromptFile));
1077
1089
 
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
- }
1090
+ const t2 = Date.now();
1091
+ const ticker2 = makePassTicker("Pass 2", t2);
1092
+ const ok = await runClaudePromptAsync(prompt, {
1093
+ onTick: ticker2.onTick,
1094
+ tickMs: ticker2.tickMs,
1095
+ });
1096
+ ticker2.clearLine();
1097
+ const elapsed2 = Date.now() - t2;
1098
+ stepTimes.push(elapsed2);
1081
1099
 
1082
- if (!fileExists(pass2Json)) {
1083
- throw new InitError("pass2-merged.json was not created. Claude may have run but not produced expected output.");
1084
- }
1100
+ if (!ok) {
1101
+ throw new InitError("Pass 2 failed. Check the claude error output above.\n If this persists, try: npx claudeos-core init --force");
1102
+ }
1085
1103
 
1086
- completedSteps++;
1087
- progressBar(completedSteps, `pass2-merged.json created (${formatElapsed(elapsed2)})`);
1104
+ if (!fileExists(pass2Json)) {
1105
+ throw new InitError("pass2-merged.json was not created. Claude may have run but not produced expected output.");
1088
1106
  }
1089
- log("");
1090
1107
 
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.
1108
+ progressBar(nextStep, `pass2-merged.json created (${formatElapsed(elapsed2)})`);
1109
+ return 1;
1110
+ }
1111
+
1112
+ // ─── Stage 8: Build pass3-context.json (v2.1) ─────────────────────
1113
+ // Writes a small (<5 KB) structured summary derived from project-analysis.json
1114
+ // plus pass2-merged.json signals (size, top-level keys). Pass 3 prompts
1115
+ // reference this INSTEAD OF re-reading pass2-merged.json repeatedly, which
1116
+ // was the primary cause of `Prompt is too long` failures on large projects.
1117
+ //
1118
+ // Silent-on-failure: if pass3-context-builder returns null (e.g.
1119
+ // project-analysis.json missing), we skip writing and let Pass 3 fall back
1120
+ // to the pre-v2.1 behavior of reading pass2-merged.json directly.
1121
+ function buildPass3ContextJson() {
1100
1122
  try {
1101
1123
  const { buildPass3Context } = require("../../plan-installer/pass3-context-builder");
1102
1124
  const pass3Ctx = buildPass3Context(GENERATED_DIR);
@@ -1116,11 +1138,15 @@ async function cmdInit(parsedArgs) {
1116
1138
  } catch (e) {
1117
1139
  log(` ⚠️ pass3-context.json build skipped: ${e.message} (Pass 3 will fall back to pass2-merged.json)`);
1118
1140
  }
1119
- log("");
1120
-
1121
- // ─── [6] Pass 3: Generate + verify ─────────────────────────
1122
- header("[6] Pass 3 — Generating all files...");
1141
+ }
1123
1142
 
1143
+ // ─── Stage 9a: Pass 3 marker pre-processing ───────────────────────
1144
+ // Handles v1.7.x migration backfill + stale-marker detection (guide/outputs).
1145
+ // The stale region below MUST include dropStalePass3Marker, EXPECTED_GUIDE_FILES,
1146
+ // and findMissingOutputs — tested for by tests/pass3-marker.test.js source
1147
+ // parity. The `completedSteps++` sentinel used by that region's regex lives
1148
+ // in cmdInit directly, after dispatchPass3 returns.
1149
+ function handlePass3StaleMarker(wasFreshClean) {
1124
1150
  const pass3Marker = path.join(GENERATED_DIR, "pass3-complete.json");
1125
1151
  const claudeMdPath = path.join(PROJECT_ROOT, "CLAUDE.md");
1126
1152
 
@@ -1224,6 +1250,17 @@ async function cmdInit(parsedArgs) {
1224
1250
  }
1225
1251
  }
1226
1252
  }
1253
+ }
1254
+
1255
+ // ─── Stage 9b: Pass 3 dispatch (decide + run) ─────────────────────
1256
+ // Returns { ran: boolean } so the caller can increment completedSteps
1257
+ // with the literal "completedSteps++" token that the stale-region
1258
+ // source-parity test regex requires.
1259
+ async function dispatchPass3(opts) {
1260
+ const { wasFreshClean, lang, stepTimes, progressBar, nextStep } = opts;
1261
+
1262
+ const pass3Marker = path.join(GENERATED_DIR, "pass3-complete.json");
1263
+ const claudeMdPath = path.join(PROJECT_ROOT, "CLAUDE.md");
1227
1264
 
1228
1265
  // Pass 3 split mode resolution.
1229
1266
  //
@@ -1243,8 +1280,8 @@ async function cmdInit(parsedArgs) {
1243
1280
  try {
1244
1281
  const ctxPath = path.join(GENERATED_DIR, "pass3-context.json");
1245
1282
  if (fileExists(ctxPath)) {
1246
- const ctx = JSON.parse(readFile(ctxPath));
1247
- const rec = ctx && ctx.splitRecommendation;
1283
+ const pctx = JSON.parse(readFile(ctxPath));
1284
+ const rec = pctx && pctx.splitRecommendation;
1248
1285
  if (rec) {
1249
1286
  log(` • estimated ${rec.estimatedFileCount} files from ${rec.totalDomains} domains`);
1250
1287
  }
@@ -1298,17 +1335,20 @@ async function cmdInit(parsedArgs) {
1298
1335
  EXPECTED_GUIDE_FILES, findMissingOutputs,
1299
1336
  lang, stepTimes,
1300
1337
  });
1301
- completedSteps++;
1302
- progressBar(completedSteps, `Pass 3 complete (split mode)`);
1338
+ progressBar(nextStep, `Pass 3 complete (split mode)`);
1303
1339
  log("");
1304
- } else {
1305
- log(" ⏭️ pass3-complete.json already complete, skipping");
1306
- completedSteps++;
1340
+ return { ran: true };
1307
1341
  }
1308
- log("");
1309
1342
 
1310
- // ─── [7] Pass 4: L4 memory scaffolding ────────────
1311
- header("[7] Pass 4 Memory scaffolding...");
1343
+ log(" ⏭️ pass3-complete.json already complete, skipping");
1344
+ return { ran: false };
1345
+ }
1346
+
1347
+ // ─── Stage 10: Pass 4 — L4 memory scaffolding ─────────────────────
1348
+ // Returns 1 unconditionally (Pass 4 always counts as a completed step,
1349
+ // whether skip / static fallback / Claude-driven).
1350
+ async function runPass4(opts) {
1351
+ const { lang, stepTimes, progressBar, nextStep } = opts;
1312
1352
 
1313
1353
  const pass4Marker = path.join(GENERATED_DIR, "pass4-memory.json");
1314
1354
  const pass4PromptFile = path.join(GENERATED_DIR, "pass4-prompt.md");
@@ -1542,13 +1582,12 @@ async function cmdInit(parsedArgs) {
1542
1582
  // when we actually did real work, so ETA for future steps stays meaningful.
1543
1583
  const pass4Elapsed = Date.now() - pass4Start;
1544
1584
  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...");
1585
+ progressBar(nextStep, pass4Label);
1586
+ return 1;
1587
+ }
1551
1588
 
1589
+ // ─── Stage 11: Run external verification tools ────────────────────
1590
+ function runVerificationTools() {
1552
1591
  const verifyTools = [
1553
1592
  { name: "manifest-generator", script: path.join(TOOLS_DIR, "manifest-generator/index.js") },
1554
1593
  { name: "health-checker", script: path.join(TOOLS_DIR, "health-checker/index.js") },
@@ -1564,21 +1603,12 @@ async function cmdInit(parsedArgs) {
1564
1603
  log(` ⚠️ ${t.name} reported issues (non-fatal)`);
1565
1604
  }
1566
1605
  }
1567
- log("");
1568
-
1569
- // ─── Complete ─────────────────────────────────────────────
1570
- const totalFiles = countFiles();
1571
- const pass1Files = countPass1Files();
1606
+ }
1572
1607
 
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.
1608
+ // ─── Stage 12: Structural lint (v2.3.0+) ──────────────────────────
1609
+ // Run the language-invariant CLAUDE.md validator after all passes complete.
1610
+ // Failures do NOT abort the run informational only.
1611
+ function runLint() {
1582
1612
  try {
1583
1613
  const { validate } = require("../../claude-md-validator");
1584
1614
  const { formatSummaryLine } = require("../../claude-md-validator/reporter");
@@ -1602,25 +1632,31 @@ async function cmdInit(parsedArgs) {
1602
1632
  log(` ⚠️ Lint step skipped: ${e.message || e}`);
1603
1633
  log("");
1604
1634
  }
1635
+ }
1605
1636
 
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.
1637
+ // ─── Stage 13: Content integrity (Guard 4 — v2.3.0+) ──────────────
1638
+ // Runs content-validator's path-claim + MANIFEST drift checks as a
1639
+ // non-blocking final step after all passes. These are *advisories*, not
1640
+ // generation failures the documents are usable as-is, the advisories
1641
+ // just flag spots where an LLM may have guessed at a filename or a
1642
+ // skill registration may have drifted. We deliberately do NOT throw or
1643
+ // unset pass3-complete.json here:
1644
+ // - Re-running Pass 3 is not guaranteed to fix LLM hallucinations
1645
+ // (the same fact JSON may trigger the same mis-inference again),
1646
+ // so a throw could deadlock the user in an `init --force` loop.
1647
+ // - content-validator's non-zero exit is preserved so that
1648
+ // `npx claudeos-core health` (and any CI wired to it) still treats
1649
+ // advisories as a real gate. `init` just presents them with softer
1650
+ // UX because by the time `init` finishes, the user's docs are
1651
+ // already on disk and fully usable.
1652
+ function runContentValidator() {
1618
1653
  try {
1619
1654
  const cvPath = path.join(__dirname, "..", "..", "content-validator", "index.js");
1620
1655
  if (fileExists(cvPath)) {
1621
1656
  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.
1657
+ // Run in a child process so its process.exit(1) on advisories does
1658
+ // not terminate init. The exit code is informational for us we
1659
+ // still surface the content as advisories regardless.
1624
1660
  const { spawnSync } = require("child_process");
1625
1661
  const result = spawnSync(process.execPath, [cvPath], {
1626
1662
  cwd: PROJECT_ROOT,
@@ -1634,11 +1670,11 @@ async function cmdInit(parsedArgs) {
1634
1670
  log(summary.split("\n").map((l) => " " + l).join("\n"));
1635
1671
  if (result.status !== 0) {
1636
1672
  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");
1673
+ log(" ℹ️ Content advisories detected these are quality notes,");
1674
+ log(" NOT generation failures. Your generated docs are ready");
1675
+ log(" to use as-is. Review when convenient:");
1676
+ log(" - stale-report.json (full advisory list)");
1677
+ log(" - npx claudeos-core health (standalone gate with exit code)");
1642
1678
  }
1643
1679
  log("");
1644
1680
  }
@@ -1646,11 +1682,17 @@ async function cmdInit(parsedArgs) {
1646
1682
  log(` ⚠️ Content check skipped: ${e.message || e}`);
1647
1683
  log("");
1648
1684
  }
1685
+ }
1649
1686
 
1650
- log("");
1687
+ // ─── Stage 14: Print completion banner ────────────────────────────
1688
+ function printCompletionBanner(opts) {
1689
+ const { lang, totalGroups, totalStart } = opts;
1690
+ const totalFiles = countFiles();
1691
+ const pass1Files = countPass1Files();
1651
1692
  const memoryReady = fileExists(path.join(PROJECT_ROOT, "claudeos-core/memory/decision-log.md"));
1652
1693
  const rulesReady = fileExists(path.join(PROJECT_ROOT, ".claude/rules/60.memory/01.decision-log.md"));
1653
1694
  const l4Status = (memoryReady && rulesReady) ? "memory + rules" : "partial";
1695
+ log("");
1654
1696
  log("╔════════════════════════════════════════════════════╗");
1655
1697
  log("║ ✅ ClaudeOS-Core — Complete ║");
1656
1698
  log("║ ║");
@@ -1670,4 +1712,113 @@ async function cmdInit(parsedArgs) {
1670
1712
  log("");
1671
1713
  }
1672
1714
 
1715
+ // ═══════════════════════════════════════════════════════════════════
1716
+ // Main orchestrator
1717
+ // ═══════════════════════════════════════════════════════════════════
1718
+ async function cmdInit(parsedArgs) {
1719
+ const totalStart = Date.now();
1720
+
1721
+ // ─── Prerequisites check ───────────────────────────────────
1722
+ checkPrerequisites();
1723
+
1724
+ // ─── Language selection ────────────────────────────────────
1725
+ const lang = await resolveLanguage(parsedArgs);
1726
+
1727
+ // ─── Resume / Fresh selection ──────────────────────────────
1728
+ // wasFreshClean: tracks whether we just wiped generated state via --force
1729
+ // or "fresh" resume mode. Used by the Pass 3 backfill guard below:
1730
+ // fresh/force explicitly means "regenerate from scratch", so a leftover
1731
+ // CLAUDE.md from a prior run must NOT cause Pass 3 to be skipped via the
1732
+ // v1.7.x migration backfill.
1733
+ const { wasFreshClean } = await applyResumeMode(parsedArgs, lang);
1734
+
1735
+ log("");
1736
+ log("╔════════════════════════════════════════════════════╗");
1737
+ log("║ ClaudeOS-Core — Bootstrap (4-Pass) ║");
1738
+ log("╚════════════════════════════════════════════════════╝");
1739
+ log(` Project root: ${PROJECT_ROOT}`);
1740
+ log(` Language: ${SUPPORTED_LANGS[lang]} (${lang})`);
1741
+ log("");
1742
+
1743
+ // ─── [1] Install dependencies ──────────────────────────────
1744
+ header("[1] Installing dependencies...");
1745
+ if (!fileExists(path.join(TOOLS_DIR, "node_modules"))) {
1746
+ run("npm install --silent", { cwd: TOOLS_DIR });
1747
+ }
1748
+ log(" ✅ Done\n");
1749
+
1750
+ // ─── [2] Create directory structure ────────────────────────
1751
+ header("[2] Creating directory structure...");
1752
+ ensureDirectories();
1753
+ log(" ✅ Done\n");
1754
+
1755
+ // ─── [3] Run plan-installer ────────────────────────────────
1756
+ header("[3] Analyzing project (plan-installer)...");
1757
+ run(`node "${path.join(TOOLS_DIR, "plan-installer/index.js")}"`);
1758
+ log("");
1759
+
1760
+ // ─── [4] Pass 1: Deep analysis per domain group ────────────
1761
+ header("[4] Pass 1 — Deep analysis per domain group...");
1762
+ const { domainGroups, totalGroups } = loadDomainGroups();
1763
+ const pass1Prompts = loadPass1Prompts();
1764
+
1765
+ // Progress tracking: Pass 1 (N groups) + Pass 2 + Pass 3 + Pass 4 = totalSteps
1766
+ const totalSteps = totalGroups + 3;
1767
+ let completedSteps = 0;
1768
+ const stepTimes = [];
1769
+ const passStart = Date.now();
1770
+ const progressBar = makeProgressBar(totalSteps, passStart, stepTimes);
1771
+
1772
+ const p1Delta = await runPass1Loop({
1773
+ domainGroups, totalGroups, pass1Prompts,
1774
+ progressBar, stepTimes,
1775
+ startingStep: completedSteps,
1776
+ });
1777
+ completedSteps += p1Delta;
1778
+
1779
+ // ─── [5] Pass 2: Merge analysis results ────────────────────
1780
+ header("[5] Pass 2 — Merging analysis results...");
1781
+ const p2Delta = await runPass2({
1782
+ progressBar, stepTimes, nextStep: completedSteps + 1,
1783
+ });
1784
+ completedSteps += p2Delta;
1785
+ log("");
1786
+
1787
+ // ─── [5.5] Build pass3-context.json (v2.1) ─────────────────
1788
+ buildPass3ContextJson();
1789
+ log("");
1790
+
1791
+ // ─── [6] Pass 3: Generate + verify ─────────────────────────
1792
+ header("[6] Pass 3 — Generating all files...");
1793
+ handlePass3StaleMarker(wasFreshClean);
1794
+ const { ran: p3Ran } = await dispatchPass3({
1795
+ wasFreshClean, lang, stepTimes,
1796
+ progressBar, nextStep: completedSteps + 1,
1797
+ });
1798
+ if (p3Ran) completedSteps++;
1799
+ log("");
1800
+
1801
+ // ─── [7] Pass 4: L4 memory scaffolding ─────────────────────
1802
+ header("[7] Pass 4 — Memory scaffolding...");
1803
+ const p4Delta = await runPass4({
1804
+ lang, stepTimes, progressBar, nextStep: completedSteps + 1,
1805
+ });
1806
+ completedSteps += p4Delta;
1807
+ log("");
1808
+
1809
+ // ─── [8] Run verification tools ────────────────────────────
1810
+ header("[8] Running verification tools...");
1811
+ runVerificationTools();
1812
+ log("");
1813
+
1814
+ // ─── Structural lint (v2.3.0+) ─────────────────────────────
1815
+ runLint();
1816
+
1817
+ // ─── Content integrity (Guard 4 — v2.3.0+) ─────────────────
1818
+ runContentValidator();
1819
+
1820
+ // ─── Complete ──────────────────────────────────────────────
1821
+ printCompletionBanner({ lang, totalGroups, totalStart });
1822
+ }
1823
+
1673
1824
  module.exports = { cmdInit, InitError };