claudeos-core 2.0.2 → 2.1.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 (42) hide show
  1. package/CHANGELOG.md +178 -0
  2. package/README.de.md +994 -880
  3. package/README.es.md +993 -880
  4. package/README.fr.md +993 -880
  5. package/README.hi.md +993 -880
  6. package/README.ja.md +993 -880
  7. package/README.ko.md +159 -47
  8. package/README.md +159 -46
  9. package/README.ru.md +993 -880
  10. package/README.vi.md +161 -48
  11. package/README.zh-CN.md +992 -880
  12. package/bin/cli.js +7 -2
  13. package/bin/commands/init.js +733 -143
  14. package/bin/commands/memory.js +17 -5
  15. package/bootstrap.sh +81 -81
  16. package/lib/expected-outputs.js +6 -7
  17. package/lib/memory-scaffold.js +84 -46
  18. package/lib/plan-parser.js +12 -0
  19. package/manifest-generator/index.js +16 -18
  20. package/package.json +1 -1
  21. package/pass-prompts/templates/angular/pass3.md +2 -10
  22. package/pass-prompts/templates/common/pass3-phase1.md +131 -0
  23. package/pass-prompts/templates/common/pass3a-facts.md +143 -0
  24. package/pass-prompts/templates/common/pass3b-core-header.md +58 -0
  25. package/pass-prompts/templates/common/pass3c-skills-guide-header.md +53 -0
  26. package/pass-prompts/templates/common/pass3d-plan-aux-header.md +57 -0
  27. package/pass-prompts/templates/common/pass4.md +4 -19
  28. package/pass-prompts/templates/java-spring/pass3.md +5 -15
  29. package/pass-prompts/templates/kotlin-spring/pass3.md +5 -15
  30. package/pass-prompts/templates/node-express/pass3.md +5 -14
  31. package/pass-prompts/templates/node-fastify/pass3.md +2 -10
  32. package/pass-prompts/templates/node-nestjs/pass3.md +5 -13
  33. package/pass-prompts/templates/node-nextjs/pass3.md +5 -14
  34. package/pass-prompts/templates/node-vite/pass3.md +95 -103
  35. package/pass-prompts/templates/python-django/pass3.md +5 -14
  36. package/pass-prompts/templates/python-fastapi/pass3.md +5 -14
  37. package/pass-prompts/templates/python-flask/pass3.md +95 -103
  38. package/pass-prompts/templates/vue-nuxt/pass3.md +2 -10
  39. package/plan-installer/pass3-context-builder.js +258 -0
  40. package/plan-installer/prompt-generator.js +9 -1
  41. package/plan-validator/index.js +23 -8
  42. package/sync-checker/index.js +44 -0
@@ -0,0 +1,258 @@
1
+ /**
2
+ * ClaudeOS-Core — Pass 3 Context Builder
3
+ *
4
+ * Builds a slim `pass3-context.json` that Pass 3 prompts reference INSTEAD OF
5
+ * re-reading the full `pass2-merged.json` repeatedly. pass2-merged.json on
6
+ * large multi-module projects can exceed 300 KB, and Pass 3 historically
7
+ * re-reads it once per file generated (30-50 times), which is the #1 cause
8
+ * of `Prompt is too long` Pass 3 failures.
9
+ *
10
+ * Design constraints:
11
+ * - pass2-merged.json is an LLM-generated free-form JSON: field names and
12
+ * nesting vary by stack and by Claude's interpretation of the pass2
13
+ * template. We CANNOT reliably project specific nested fields out of it.
14
+ * - project-analysis.json IS structured (we wrote it ourselves in
15
+ * plan-installer). We use it as the authoritative source for stack facts.
16
+ * - pass2-merged.json is still useful as a file, so we report its
17
+ * existence/size/top-level key count here as signals — the Pass 3 prompt
18
+ * then knows whether to fall back to reading it for specific details.
19
+ *
20
+ * Output shape (see `buildPass3Context` return) is intentionally flat and
21
+ * small: <5 KB even for large projects, vs. pass2-merged.json at 50-500 KB.
22
+ */
23
+
24
+ "use strict";
25
+
26
+ const fs = require("fs");
27
+ const path = require("path");
28
+
29
+ /**
30
+ * Safely read + parse a JSON file. Returns null on any error.
31
+ * Mirrors lib/safe-fs.js readJsonSafe but inlined to avoid circular-dep risk
32
+ * during tool-time module loading (this file is called from plan-installer
33
+ * after all scanners have run).
34
+ */
35
+ function readJsonSafe(filePath) {
36
+ try {
37
+ const raw = fs.readFileSync(filePath, "utf-8");
38
+ return JSON.parse(raw);
39
+ } catch (_e) {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Describe pass2-merged.json as signals (size, top-level key count, existence)
46
+ * without loading its contents into the context JSON. Pass 3 prompts use these
47
+ * signals to decide whether to read the full file for a specific missing detail.
48
+ */
49
+ function describePass2(pass2MergedPath) {
50
+ let exists = false;
51
+ let sizeBytes = 0;
52
+ let topLevelKeys = [];
53
+ try {
54
+ const stat = fs.statSync(pass2MergedPath);
55
+ exists = true;
56
+ sizeBytes = stat.size;
57
+ try {
58
+ const parsed = JSON.parse(fs.readFileSync(pass2MergedPath, "utf-8"));
59
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
60
+ topLevelKeys = Object.keys(parsed).slice(0, 40); // cap at 40 to keep context small
61
+ }
62
+ } catch (_e) {
63
+ // Malformed pass2-merged.json — leave topLevelKeys empty, still report size.
64
+ }
65
+ } catch (_e) {
66
+ // File doesn't exist yet (e.g. Pass 2 has not run). Still return a valid descriptor.
67
+ }
68
+ return {
69
+ exists,
70
+ sizeBytes,
71
+ sizeKB: Math.round(sizeBytes / 1024),
72
+ topLevelKeys,
73
+ // Heuristic flag: >300 KB is the empirical threshold above which repeated
74
+ // re-reads reliably cause Pass 3 context overflow on 200K-context models.
75
+ large: sizeBytes > 300 * 1024,
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Extract domain summaries from project-analysis.json's domains array.
81
+ * Each entry in `domains` has varying fields depending on the scanner that
82
+ * produced it (scan-java, scan-kotlin, scan-node, scan-python, scan-frontend).
83
+ * We project only the fields that are universally useful for Pass 3 consistency.
84
+ */
85
+ function summarizeDomains(analysisDomains) {
86
+ if (!Array.isArray(analysisDomains)) return [];
87
+ return analysisDomains.map((d) => {
88
+ const summary = {
89
+ name: d.name,
90
+ type: d.type || "backend",
91
+ totalFiles: d.totalFiles || 0,
92
+ };
93
+ // Optional, scanner-specific fields — only include if present.
94
+ if (typeof d.controllers === "number") summary.controllers = d.controllers;
95
+ if (typeof d.services === "number") summary.services = d.services;
96
+ if (typeof d.repositories === "number") summary.repositories = d.repositories;
97
+ if (typeof d.mappers === "number") summary.mappers = d.mappers;
98
+ if (typeof d.dtos === "number") summary.dtos = d.dtos;
99
+ if (typeof d.pages === "number") summary.pages = d.pages;
100
+ if (typeof d.components === "number") summary.components = d.components;
101
+ if (d.pattern) summary.pattern = d.pattern; // Java A-E patterns
102
+ if (d.modulePath) summary.modulePath = d.modulePath; // multi-module Java
103
+ if (d.serverType) summary.serverType = d.serverType; // Kotlin CQRS server types
104
+ if (d.domainName) summary.domainName = d.domainName;
105
+ return summary;
106
+ });
107
+ }
108
+
109
+ /**
110
+ * Extract the port default from project-analysis.stack.port (plan-installer
111
+ * sets this based on framework). Kept as a top-level convenience field so
112
+ * Pass 3 doesn't need to traverse the nested stack object.
113
+ */
114
+ function extractPort(projectAnalysis) {
115
+ return (projectAnalysis && projectAnalysis.stack && projectAnalysis.stack.port) || null;
116
+ }
117
+
118
+ /**
119
+ * Build the slim Pass 3 context object.
120
+ *
121
+ * @param {string} generatedDir - absolute path to claudeos-core/generated/
122
+ * @returns {object|null} context object, or null if project-analysis.json is
123
+ * missing/malformed (caller should then NOT write pass3-context.json — Pass 3
124
+ * will fall back to the full pass2-merged.json path).
125
+ */
126
+ function buildPass3Context(generatedDir) {
127
+ const analysisPath = path.join(generatedDir, "project-analysis.json");
128
+ const pass2Path = path.join(generatedDir, "pass2-merged.json");
129
+
130
+ const analysis = readJsonSafe(analysisPath);
131
+ if (!analysis || typeof analysis !== "object") {
132
+ // Without project-analysis.json we have nothing structured to summarize.
133
+ // Let Pass 3 fall back to reading pass2-merged.json directly.
134
+ return null;
135
+ }
136
+
137
+ const stack = analysis.stack || {};
138
+ const summary = analysis.summary || {};
139
+ const active = analysis.activeDomains || {};
140
+
141
+ const context = {
142
+ _schemaVersion: 1,
143
+ _generatedAt: new Date().toISOString(),
144
+ _purpose:
145
+ "Slim, read-once summary for Pass 3 prompts. Reference this INSTEAD OF " +
146
+ "re-reading pass2-merged.json during file generation. Only consult " +
147
+ "pass2-merged.json for specific method names / exact paths not captured " +
148
+ "here — and only once per detail.",
149
+
150
+ // ─── Stack facts (authoritative — from project-analysis.json) ──────
151
+ stack: {
152
+ language: stack.language || null,
153
+ languageVersion: stack.languageVersion || null,
154
+ framework: stack.framework || null,
155
+ frameworkVersion: stack.frameworkVersion || null,
156
+ buildTool: stack.buildTool || null,
157
+ packageManager: stack.packageManager || null,
158
+ database: stack.database || null,
159
+ orm: stack.orm || null,
160
+ frontend: stack.frontend || null,
161
+ frontendVersion: stack.frontendVersion || null,
162
+ port: extractPort(analysis),
163
+ },
164
+
165
+ // ─── Architecture flags ──────────────────────────────────────────
166
+ architecture: {
167
+ cqrs: !!(stack.architecture && String(stack.architecture).includes("cqrs")),
168
+ bff: Array.isArray(stack.detected) && stack.detected.includes("bff"),
169
+ multiModule: !!stack.multiModule,
170
+ monorepo: stack.monorepo || null,
171
+ workspaces: stack.workspaces || null,
172
+ modules: Array.isArray(stack.modules) ? stack.modules : [],
173
+ rootPackage: analysis.rootPackage || null,
174
+ },
175
+
176
+ // ─── Active domain groups (which 00.core / 10.backend / ... apply) ──
177
+ activeDomains: active,
178
+
179
+ // ─── Template routing (what pass3 templates were combined) ──────────
180
+ templates: analysis.templates || {},
181
+ isMultiStack: !!analysis.isMultiStack,
182
+
183
+ // ─── Domain summary (flat list, small per-domain footprint) ─────────
184
+ domainCount: {
185
+ total: summary.totalDomains || 0,
186
+ backend: summary.backendDomains || 0,
187
+ frontend: summary.frontendDomains || 0,
188
+ },
189
+ backendDomains: summarizeDomains(analysis.backendDomains),
190
+ frontendDomains: summarizeDomains(analysis.frontendDomains),
191
+
192
+ // ─── Frontend stats (if scan-frontend ran) ──────────────────────────
193
+ frontend: analysis.frontend || { exists: false },
194
+
195
+ // ─── pass2-merged.json descriptor (signals, not contents) ───────────
196
+ // Pass 3 reads this to decide whether it's safe to open the full file
197
+ // for a specific missing detail, and how aggressively to summarize first.
198
+ pass2Merged: describePass2(pass2Path),
199
+
200
+ // ─── Split-mode recommendation (informational only) ─────────────────
201
+ // Heuristic that predicts whether single-call Pass 3 would likely hit
202
+ // `Prompt is too long`. Surfaced in init logs as "estimated N files
203
+ // from M domains". As of the current release this does NOT drive the
204
+ // split/single-call decision — init.js defaults to split mode for all
205
+ // projects regardless of this recommendation. Kept for diagnostic
206
+ // visibility and future use (e.g. token budget estimation, UI hints).
207
+ splitRecommendation: (function computeSplit() {
208
+ const pass2Desc = describePass2(pass2Path);
209
+ const backendCount = Array.isArray(analysis.backendDomains) ? analysis.backendDomains.length : 0;
210
+ const frontendCount = Array.isArray(analysis.frontendDomains) ? analysis.frontendDomains.length : 0;
211
+ const totalDomains = backendCount + frontendCount;
212
+
213
+ // Rough estimate: each domain yields 5-8 files (standards + rules + skills),
214
+ // plus ~15 stack-independent files (CLAUDE.md, guides, master plans, etc.).
215
+ const estimatedFileCount = 15 + totalDomains * 6;
216
+
217
+ // Thresholds calibrated from empirical failures:
218
+ // - pass2 > 300KB: historic input-overflow threshold (v2.1 original heuristic)
219
+ // - estimated files > 35: empirical output-accumulation threshold
220
+ // (observed overflow at ~40 files with pass2=35KB — output was the cause)
221
+ const largeInput = pass2Desc.sizeBytes > 300 * 1024;
222
+ const largeOutput = estimatedFileCount > 35;
223
+ const recommend = largeInput || largeOutput;
224
+
225
+ return {
226
+ recommend,
227
+ reasons: [
228
+ largeInput ? `pass2-merged.json is ${pass2Desc.sizeKB} KB (> 300 KB input threshold)` : null,
229
+ largeOutput ? `estimated ${estimatedFileCount} output files (> 35 file threshold) from ${totalDomains} domains` : null,
230
+ ].filter(Boolean),
231
+ estimatedFileCount,
232
+ totalDomains,
233
+ };
234
+ })(),
235
+
236
+ // ─── Fields that pass2-merged.json is authoritative for ─────────────
237
+ // These are all `null` in pass3-context.json by design — Pass 3 must
238
+ // read pass2-merged.json ONCE (during the Phase 1 fact-table fill)
239
+ // to get their actual values. Listed here so Pass 3 knows exactly
240
+ // what to look for instead of free-form scanning.
241
+ needsPass2: {
242
+ responseWrapperLayer: null, // "Controller" | "Aggregator" | "Service"
243
+ responseWrapperMethod: null, // e.g. "makeResponse"
244
+ responseUtilClass: null, // FQN, e.g. "com.company.util.ApiResponseUtil"
245
+ responseUtilMethods: null, // ["success", "fail", ...] exact names
246
+ mapperXmlPath: null, // verbatim, if MyBatis
247
+ aggregatorExists: null,
248
+ aggregatorNaming: null,
249
+ sharedBaseClasses: null, // array of FQNs
250
+ sharedUtilPackage: null,
251
+ authMethod: null, // "JWT" | "Session" | "OAuth2" | ...
252
+ },
253
+ };
254
+
255
+ return context;
256
+ }
257
+
258
+ module.exports = { buildPass3Context, describePass2, summarizeDomains };
@@ -28,6 +28,14 @@ function generatePrompts(templates, lang, templatesDir, generatedDir) {
28
28
  // claudeos-core/generated/.staged-rules/* to bypass Claude Code's sensitive-
29
29
  // path block. The Node.js orchestrator moves the staged files after each pass.
30
30
  const stagingOverride = existsSafe(stagingOverridePath) ? readFileSafe(stagingOverridePath) + "\n" : "";
31
+ // v2.1: Phase 1 "Read Once, Extract Facts" block prepended to every Pass 3
32
+ // prompt. Teaches Claude to read pass2-merged.json exactly once into a
33
+ // compact in-context fact table and reference that table for all subsequent
34
+ // file generation — fixes the `Prompt is too long` failure on large projects
35
+ // caused by 10-20× re-reads of pass2-merged.json. Also includes idempotent
36
+ // skip rules (Rule B) so interrupted Pass 3 runs can resume safely.
37
+ const phase1Path = path.join(commonDir, "pass3-phase1.md");
38
+ const phase1 = existsSafe(phase1Path) ? readFileSafe(phase1Path) + "\n" : "";
31
39
 
32
40
  let langInstruction = "";
33
41
  if (lang && lang !== "en" && existsSafe(langPath)) {
@@ -92,7 +100,7 @@ function generatePrompts(templates, lang, templatesDir, generatedDir) {
92
100
 
93
101
  writeFileSafe(
94
102
  path.join(generatedDir, "pass3-prompt.md"),
95
- header + langInstruction + stagingOverride + combinedBody.trimEnd() + "\n" + footer
103
+ header + langInstruction + stagingOverride + phase1 + combinedBody.trimEnd() + "\n" + footer
96
104
  );
97
105
  console.log(` ✅ pass3-prompt.md${templates.frontend && templates.backend ? " (multi-stack combined)" : ""}`);
98
106
  }
@@ -57,8 +57,14 @@ async function main() {
57
57
  console.log("╚═══════════════════════════════════════╝\n");
58
58
 
59
59
  if (!fs.existsSync(PLAN)) {
60
- console.log(" Plan directory not found");
61
- process.exit(1);
60
+ console.log(" ℹ️ Plan directory not present — nothing to validate.");
61
+ console.log(" (Master plans are no longer generated by default; this is normal.)\n");
62
+ // Emit empty stale-report block so health-checker summary is consistent.
63
+ updateStaleReport(GEN, "planValidation",
64
+ { checkedAt: new Date().toISOString(), mode, total: 0, synced: 0, drift: 0, missing: 0 },
65
+ { planDrift: 0, planMissing: 0 }
66
+ );
67
+ process.exit(0);
62
68
  }
63
69
 
64
70
  let total = 0, synced = 0, drift = 0, missing = 0;
@@ -145,13 +151,22 @@ async function main() {
145
151
  console.log(drift + missing === 0 ? " ✅ All plans in sync\n" : ` ⚠️ ${drift + missing} issues\n`);
146
152
 
147
153
  // ─── Update plan-sync-status.json + stale-report.json ──
154
+ // v2.1.0: master plan generation was removed. When plan/ exists but contains
155
+ // no .md files (legacy directory skeleton), we skip plan-sync-status.json
156
+ // entirely — writing a 147 B file full of zeros is noise. stale-report still
157
+ // records the (passing) validation run so health-checker sees a clean result.
158
+ // Note: plan/ missing entirely is already handled by the early-return at
159
+ // the top of main() (line ~59), so by here PLAN exists — but may be empty.
160
+ const planHasFiles = fs.readdirSync(PLAN).some(f => f.endsWith(".md"));
148
161
  if (fs.existsSync(GEN)) {
149
- fs.writeFileSync(
150
- path.join(GEN, "plan-sync-status.json"),
151
- JSON.stringify({ generatedAt: new Date().toISOString(), lastMode: mode, total, synced, drift, missing, issues: results }, null, 2)
152
- );
153
-
154
- // Also write to stale-report.json for consolidated health reporting
162
+ if (planHasFiles) {
163
+ fs.writeFileSync(
164
+ path.join(GEN, "plan-sync-status.json"),
165
+ JSON.stringify({ generatedAt: new Date().toISOString(), lastMode: mode, total, synced, drift, missing, issues: results }, null, 2)
166
+ );
167
+ }
168
+ // stale-report is always updated so health-checker sees a consistent
169
+ // planValidation entry regardless of plan/ contents.
155
170
  updateStaleReport(GEN, "planValidation",
156
171
  { checkedAt: new Date().toISOString(), mode, total, synced, drift, missing },
157
172
  { planDrift: drift, planMissing: missing }
@@ -50,7 +50,27 @@ async function main() {
50
50
  console.log("║ ClaudeOS-Core — Sync Checker ║");
51
51
  console.log("╚═══════════════════════════════════════╝\n");
52
52
 
53
+ // Master plan directory is optional. If it doesn't exist (new default for
54
+ // claudeos-core, since master plans are no longer generated) AND sync-map
55
+ // has no mappings to validate, sync-checker has nothing to compare against
56
+ // and should skip cleanly. This is a PASS state, not a failure.
57
+ //
58
+ // However, if sync-map.json DOES contain mappings (either because master
59
+ // plans exist, or because a caller wrote mappings directly for testing),
60
+ // we still validate them normally.
61
+ const PLAN_DIR = path.join(ROOT, "claudeos-core/plan");
62
+ const planExists = fs.existsSync(PLAN_DIR);
63
+
53
64
  if (!fs.existsSync(SMP)) {
65
+ // No sync-map at all.
66
+ if (!planExists) {
67
+ console.log(" ℹ️ No plan/ directory and no sync-map.json — nothing to compare; skipping.\n");
68
+ updateStaleReport(GEN, "syncMisses",
69
+ { checkedAt: new Date().toISOString(), unregistered: [], orphaned: [], skipped: true },
70
+ { syncIssues: 0, status: "ok" }
71
+ );
72
+ process.exit(0);
73
+ }
54
74
  console.log(" ❌ sync-map.json not found. Run manifest-generator first.\n");
55
75
  process.exit(1);
56
76
  }
@@ -66,6 +86,30 @@ async function main() {
66
86
  console.log(" ❌ sync-map.json has no mappings array.\n");
67
87
  process.exit(1);
68
88
  }
89
+
90
+ // If sync-map has no mappings AND plan/ directory doesn't exist, skip
91
+ // cleanly — there's no ground truth to validate against and no master plans
92
+ // in use.
93
+ if (sm.mappings.length === 0 && !planExists) {
94
+ console.log(" ℹ️ No plan/ directory and sync-map has no mappings — skipping.\n");
95
+ updateStaleReport(GEN, "syncMisses",
96
+ { checkedAt: new Date().toISOString(), unregistered: [], orphaned: [], skipped: true },
97
+ { syncIssues: 0, status: "ok" }
98
+ );
99
+ process.exit(0);
100
+ }
101
+
102
+ // If sync-map has no mappings but plan/ exists (e.g., empty plan files),
103
+ // skip without raising a warning — there's nothing to validate.
104
+ if (sm.mappings.length === 0) {
105
+ console.log(" ℹ️ sync-map has no mappings — nothing to validate; skipping.\n");
106
+ updateStaleReport(GEN, "syncMisses",
107
+ { checkedAt: new Date().toISOString(), unregistered: [], orphaned: [], skipped: true },
108
+ { syncIssues: 0, status: "ok" }
109
+ );
110
+ process.exit(0);
111
+ }
112
+
69
113
  const reg = new Set(sm.mappings.map((m) => m.sourcePath).filter(Boolean));
70
114
  const issues = { unreg: [], orphan: [] };
71
115