claudeos-core 2.1.1 → 2.3.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.
- package/CHANGELOG.md +1649 -481
- package/CONTRIBUTING.md +92 -92
- package/README.de.md +64 -5
- package/README.es.md +64 -5
- package/README.fr.md +64 -5
- package/README.hi.md +64 -5
- package/README.ja.md +64 -5
- package/README.ko.md +1018 -959
- package/README.md +1020 -960
- package/README.ru.md +66 -5
- package/README.vi.md +1019 -960
- package/README.zh-CN.md +64 -5
- package/bin/cli.js +152 -148
- package/bin/commands/init.js +1673 -1518
- package/bin/commands/lint.js +62 -0
- package/bin/commands/memory.js +438 -438
- package/bin/lib/cli-utils.js +206 -206
- package/claude-md-validator/index.js +184 -0
- package/claude-md-validator/reporter.js +66 -0
- package/claude-md-validator/structural-checks.js +528 -0
- package/content-validator/index.js +666 -436
- package/lib/env-parser.js +317 -0
- package/lib/expected-guides.js +23 -23
- package/lib/expected-outputs.js +90 -90
- package/lib/language-config.js +35 -35
- package/lib/memory-scaffold.js +1058 -1052
- package/lib/plan-parser.js +165 -165
- package/lib/staged-rules.js +118 -118
- package/manifest-generator/index.js +174 -174
- package/package.json +90 -87
- package/pass-json-validator/index.js +337 -337
- package/pass-prompts/templates/angular/pass3.md +28 -13
- package/pass-prompts/templates/common/claude-md-scaffold.md +686 -0
- package/pass-prompts/templates/common/pass3-footer.md +402 -39
- package/pass-prompts/templates/common/pass3b-core-header.md +43 -0
- package/pass-prompts/templates/common/pass4.md +375 -302
- package/pass-prompts/templates/common/staging-override.md +26 -26
- package/pass-prompts/templates/java-spring/pass3.md +31 -21
- package/pass-prompts/templates/kotlin-spring/pass3.md +34 -22
- package/pass-prompts/templates/node-express/pass3.md +30 -21
- package/pass-prompts/templates/node-fastify/pass3.md +28 -14
- package/pass-prompts/templates/node-nestjs/pass3.md +29 -14
- package/pass-prompts/templates/node-nextjs/pass3.md +34 -21
- package/pass-prompts/templates/node-vite/pass1.md +117 -117
- package/pass-prompts/templates/node-vite/pass2.md +78 -78
- package/pass-prompts/templates/node-vite/pass3.md +30 -13
- package/pass-prompts/templates/python-django/pass3.md +32 -21
- package/pass-prompts/templates/python-fastapi/pass3.md +33 -21
- package/pass-prompts/templates/python-flask/pass1.md +119 -119
- package/pass-prompts/templates/python-flask/pass2.md +85 -85
- package/pass-prompts/templates/python-flask/pass3.md +31 -13
- package/pass-prompts/templates/vue-nuxt/pass3.md +32 -13
- package/plan-installer/domain-grouper.js +76 -76
- package/plan-installer/index.js +137 -129
- package/plan-installer/prompt-generator.js +188 -128
- package/plan-installer/scanners/scan-frontend.js +505 -473
- package/plan-installer/scanners/scan-java.js +226 -226
- package/plan-installer/scanners/scan-node.js +57 -57
- package/plan-installer/scanners/scan-python.js +85 -85
- package/plan-installer/stack-detector.js +482 -466
- package/plan-installer/structure-scanner.js +65 -65
- package/sync-checker/index.js +177 -177
package/bin/commands/init.js
CHANGED
|
@@ -1,1518 +1,1673 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ClaudeOS-Core — Init Command
|
|
3
|
-
*
|
|
4
|
-
* Runs the full 4-Pass pipeline: analyze → merge → generate → memory scaffold.
|
|
5
|
-
* This is the main entry point for project bootstrapping.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const fs = require("fs");
|
|
9
|
-
const path = require("path");
|
|
10
|
-
const {
|
|
11
|
-
TOOLS_DIR, PROJECT_ROOT, GENERATED_DIR,
|
|
12
|
-
SUPPORTED_LANGS, LANG_CODES, isValidLang,
|
|
13
|
-
log, header, run, runClaudePrompt, runClaudePromptAsync,
|
|
14
|
-
ensureDir, fileExists, readFile, injectProjectRoot,
|
|
15
|
-
pad, countFiles, countPass1Files,
|
|
16
|
-
} = require("../lib/cli-utils");
|
|
17
|
-
const { selectLangInteractive } = require("../lib/lang-selector");
|
|
18
|
-
const { selectResumeMode } = require("../lib/resume-selector");
|
|
19
|
-
|
|
20
|
-
const { EXPECTED_GUIDE_FILES } = require("../../lib/expected-guides");
|
|
21
|
-
const { findMissingOutputs } = require("../../lib/expected-outputs");
|
|
22
|
-
|
|
23
|
-
class InitError extends Error {
|
|
24
|
-
constructor(msg) { super(msg); this.name = "InitError"; }
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function formatElapsed(ms) {
|
|
28
|
-
const sec = Math.floor(ms / 1000);
|
|
29
|
-
if (sec < 60) return `${sec}s`;
|
|
30
|
-
const min = Math.floor(sec / 60);
|
|
31
|
-
const rem = sec % 60;
|
|
32
|
-
return rem > 0 ? `${min}m ${rem}s` : `${min}m`;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Creates an onTick/clearLine pair for long-running claude -p passes. We can
|
|
36
|
-
// only observe progress externally (elapsed time, sometimes filesystem delta).
|
|
37
|
-
// TTYs get a single \r-rewritten line; CI/piped stdout gets periodic new lines.
|
|
38
|
-
// Modes via opts:
|
|
39
|
-
// - elapsed-only (no baselineCount): ⏳ label running... 45s
|
|
40
|
-
// - file delta (baselineCount set): 📝 label generating... 24 new files | 45s
|
|
41
|
-
// - fixed target (+ totalExpected): 📝 label generating... 8/12 files (67%) | 45s
|
|
42
|
-
function makePassTicker(label, startTime, opts = {}) {
|
|
43
|
-
const isTTY = Boolean(process.stdout.isTTY);
|
|
44
|
-
const { baselineCount, totalExpected } = opts;
|
|
45
|
-
const trackFiles = typeof baselineCount === "number";
|
|
46
|
-
let lastLineLen = 0;
|
|
47
|
-
function onTick() {
|
|
48
|
-
const elapsed = formatElapsed(Date.now() - startTime);
|
|
49
|
-
let line;
|
|
50
|
-
if (!trackFiles) {
|
|
51
|
-
line = ` ⏳ ${label} running... ${elapsed} elapsed`;
|
|
52
|
-
} else {
|
|
53
|
-
const current = countFiles();
|
|
54
|
-
const delta = typeof current === "number" ? Math.max(0, current - baselineCount) : null;
|
|
55
|
-
let progress;
|
|
56
|
-
if (delta === null) progress = "? new files";
|
|
57
|
-
else if (typeof totalExpected === "number" && totalExpected > 0) {
|
|
58
|
-
const capped = Math.min(delta, totalExpected);
|
|
59
|
-
const pct = Math.round((capped / totalExpected) * 100);
|
|
60
|
-
progress = `${capped}/${totalExpected} files (${pct}%)`;
|
|
61
|
-
} else {
|
|
62
|
-
progress = `${delta} new files`;
|
|
63
|
-
}
|
|
64
|
-
line = ` 📝 ${label} generating... ${progress} | ${elapsed} elapsed`;
|
|
65
|
-
}
|
|
66
|
-
if (isTTY) {
|
|
67
|
-
const pad = " ".repeat(Math.max(0, lastLineLen - line.length));
|
|
68
|
-
process.stdout.write("\r" + line + pad);
|
|
69
|
-
lastLineLen = line.length;
|
|
70
|
-
} else {
|
|
71
|
-
log(line);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
function clearLine() {
|
|
75
|
-
if (isTTY && lastLineLen > 0) {
|
|
76
|
-
process.stdout.write("\r" + " ".repeat(lastLineLen) + "\r");
|
|
77
|
-
lastLineLen = 0;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return { onTick, clearLine, tickMs: isTTY ? 1000 : 15000 };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
84
|
-
// v2.1: Pass 3 Split Runner
|
|
85
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
86
|
-
//
|
|
87
|
-
// Splits the monolithic Pass 3 into 4 sequential claude -p calls:
|
|
88
|
-
//
|
|
89
|
-
// 3a: Extract facts from pass2-merged.json into pass3a-facts.md
|
|
90
|
-
// (small document, becomes the shared context for 3b/3c/3d).
|
|
91
|
-
// 3b: Generate CLAUDE.md + standard/ + .claude/rules/ (core files).
|
|
92
|
-
// 3c: Generate skills/ + guide/.
|
|
93
|
-
// 3d: Generate plan/ + database/ + mcp-guide/ (master plans + aux).
|
|
94
|
-
//
|
|
95
|
-
// Each call has fresh context, so the total output volume can far exceed
|
|
96
|
-
// the model's context window without `Prompt is too long` failures.
|
|
97
|
-
// pass3a-facts.md replaces pass2-merged.json as the cross-stage reference,
|
|
98
|
-
// so cross-file consistency (which was the main advantage of the single-call
|
|
99
|
-
// approach) is preserved.
|
|
100
|
-
//
|
|
101
|
-
// Marker schema (pass3-complete.json):
|
|
102
|
-
// {
|
|
103
|
-
// "completedAt": "<ISO timestamp of final stage>",
|
|
104
|
-
// "mode": "split",
|
|
105
|
-
// "groupsCompleted": ["3a", "3b", "3c", "3d"]
|
|
106
|
-
// }
|
|
107
|
-
// Partial marker after interruption:
|
|
108
|
-
// { "mode": "split", "groupsCompleted": ["3a", "3b"] }
|
|
109
|
-
// On re-run, stages in groupsCompleted are skipped.
|
|
110
|
-
//
|
|
111
|
-
// All helper references (injectProjectRoot, fileExists, runClaudePromptAsync,
|
|
112
|
-
// etc.) are passed in via the ctx param rather than being captured by closure,
|
|
113
|
-
// because this function lives outside cmdInit for readability.
|
|
114
|
-
async function runPass3Split(ctx) {
|
|
115
|
-
const {
|
|
116
|
-
GENERATED_DIR, PROJECT_ROOT, TOOLS_DIR,
|
|
117
|
-
pass3Marker, claudeMdPath,
|
|
118
|
-
injectProjectRoot, readFile, fileExists,
|
|
119
|
-
runClaudePromptAsync, makePassTicker, formatElapsed,
|
|
120
|
-
log, countFiles,
|
|
121
|
-
EXPECTED_GUIDE_FILES, findMissingOutputs,
|
|
122
|
-
lang, stepTimes,
|
|
123
|
-
} = ctx;
|
|
124
|
-
|
|
125
|
-
const { writeFileSafe, readFileSafe, existsSafe } = require("../../lib/safe-fs");
|
|
126
|
-
const { moveStagedRules, countFilesRecursive } = require("../../lib/staged-rules");
|
|
127
|
-
|
|
128
|
-
const COMMON_DIR = path.join(TOOLS_DIR, "pass-prompts/templates/common");
|
|
129
|
-
const stagingDir = path.join(GENERATED_DIR, ".staged-rules");
|
|
130
|
-
|
|
131
|
-
// ─── Batch sub-division for 3b/3c on large projects ───────────
|
|
132
|
-
// Even with split mode, a single 3b call that generates standard/ + rules/
|
|
133
|
-
// for 50+ domains can still hit context overflow within that stage's
|
|
134
|
-
// session (output accumulation). We sub-divide 3b and 3c into batches of
|
|
135
|
-
// ~DOMAINS_PER_BATCH domains each when totalDomains > DOMAINS_PER_BATCH.
|
|
136
|
-
//
|
|
137
|
-
// 3a is never batched (single fact sheet).
|
|
138
|
-
// 3d is never batched (master plan aggregation).
|
|
139
|
-
//
|
|
140
|
-
// Threshold rationale: 15 domains ≈ 45-60 output files for 3b alone,
|
|
141
|
-
// which stays within the empirically safe single-session output range.
|
|
142
|
-
// Beyond 15 we split. ceil(N/15) batches, preserving order from
|
|
143
|
-
// domain-groups.json (already balanced by plan-installer).
|
|
144
|
-
const DOMAINS_PER_BATCH = 15;
|
|
145
|
-
|
|
146
|
-
function loadDomainOrder() {
|
|
147
|
-
// Returns an ordered list of domain names for batching 3b/3c.
|
|
148
|
-
// Primary source: domain-groups.json (already balanced).
|
|
149
|
-
// Fallback: project-analysis.json backendDomains + frontendDomains.
|
|
150
|
-
// Final fallback: empty array → single batch (no sub-division).
|
|
151
|
-
try {
|
|
152
|
-
const dgPath = path.join(GENERATED_DIR, "domain-groups.json");
|
|
153
|
-
if (fileExists(dgPath)) {
|
|
154
|
-
const dg = JSON.parse(readFile(dgPath));
|
|
155
|
-
const groups = Array.isArray(dg) ? dg : (dg && dg.groups);
|
|
156
|
-
if (Array.isArray(groups)) {
|
|
157
|
-
const flat = [];
|
|
158
|
-
for (const g of groups) {
|
|
159
|
-
const items = Array.isArray(g.domains) ? g.domains : (Array.isArray(g) ? g : []);
|
|
160
|
-
for (const d of items) {
|
|
161
|
-
const name = typeof d === "string" ? d : (d && d.name);
|
|
162
|
-
if (name) flat.push(name);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
if (flat.length > 0) return flat;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
} catch (_e) { /* fall through to analysis */ }
|
|
169
|
-
|
|
170
|
-
try {
|
|
171
|
-
const paPath = path.join(GENERATED_DIR, "project-analysis.json");
|
|
172
|
-
if (fileExists(paPath)) {
|
|
173
|
-
const pa = JSON.parse(readFile(paPath));
|
|
174
|
-
const backend = Array.isArray(pa.backendDomains) ? pa.backendDomains.map(d => d.name || d) : [];
|
|
175
|
-
const frontend = Array.isArray(pa.frontendDomains) ? pa.frontendDomains.map(d => d.name || d) : [];
|
|
176
|
-
return [...backend, ...frontend].filter(Boolean);
|
|
177
|
-
}
|
|
178
|
-
} catch (_e) { /* fall through */ }
|
|
179
|
-
|
|
180
|
-
return [];
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function computeBatches(domainOrder) {
|
|
184
|
-
// Returns an array of batches, where each batch is an array of domain names.
|
|
185
|
-
// If total <= DOMAINS_PER_BATCH, returns a single-batch array so the caller
|
|
186
|
-
// can use the backward-compatible "3b"/"3c" marker names.
|
|
187
|
-
if (!domainOrder || domainOrder.length <= DOMAINS_PER_BATCH) {
|
|
188
|
-
return [domainOrder || []];
|
|
189
|
-
}
|
|
190
|
-
const batches = [];
|
|
191
|
-
for (let i = 0; i < domainOrder.length; i += DOMAINS_PER_BATCH) {
|
|
192
|
-
batches.push(domainOrder.slice(i, i + DOMAINS_PER_BATCH));
|
|
193
|
-
}
|
|
194
|
-
return batches;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const domainOrder = loadDomainOrder();
|
|
198
|
-
const batches = computeBatches(domainOrder);
|
|
199
|
-
const isBatched = batches.length > 1;
|
|
200
|
-
|
|
201
|
-
if (isBatched) {
|
|
202
|
-
log(` 📦 Batch sub-division enabled: ${domainOrder.length} domains → ${batches.length} batches per stage (3b, 3c)`);
|
|
203
|
-
for (let i = 0; i < batches.length; i++) {
|
|
204
|
-
log(` • batch ${i + 1}: ${batches[i].slice(0, 3).join(", ")}${batches[i].length > 3 ? ", +" + (batches[i].length - 3) + " more" : ""}`);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
// ─── Load existing marker to support resume mid-split ─────────
|
|
210
|
-
// If a prior split run completed 3a and 3b but crashed at 3c, we skip
|
|
211
|
-
// 3a and 3b automatically. This is the split-mode analog of Guard 3
|
|
212
|
-
// stale-marker detection in cmdInit.
|
|
213
|
-
let completedGroups = [];
|
|
214
|
-
if (fileExists(pass3Marker)) {
|
|
215
|
-
try {
|
|
216
|
-
const existing = JSON.parse(readFile(pass3Marker));
|
|
217
|
-
if (existing && existing.mode === "split" && Array.isArray(existing.groupsCompleted)) {
|
|
218
|
-
completedGroups = existing.groupsCompleted.slice();
|
|
219
|
-
if (completedGroups.length > 0) {
|
|
220
|
-
log(` ↪️ Resuming Pass 3 split: ${completedGroups.length} stage(s) already done (${completedGroups.join(", ")})`);
|
|
221
|
-
}
|
|
222
|
-
} else if (existing && existing.mode !== "split") {
|
|
223
|
-
// Previous run was NOT split mode but marker exists and is valid.
|
|
224
|
-
// Caller (cmdInit) should have skipped us already — defensive no-op here.
|
|
225
|
-
log(` ℹ️ Pass 3 marker found (non-split mode), skipping split runner`);
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
} catch (_e) {
|
|
229
|
-
log(" ⚠️ pass3-complete.json malformed, starting Pass 3 split from scratch");
|
|
230
|
-
completedGroups = [];
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Helper: write marker after each stage so partial progress survives crashes.
|
|
235
|
-
function persistMarker(complete) {
|
|
236
|
-
const body = {
|
|
237
|
-
mode: "split",
|
|
238
|
-
groupsCompleted: completedGroups.slice(),
|
|
239
|
-
lastUpdatedAt: new Date().toISOString(),
|
|
240
|
-
};
|
|
241
|
-
if (complete) body.completedAt = body.lastUpdatedAt;
|
|
242
|
-
return writeFileSafe(pass3Marker, JSON.stringify(body, null, 2));
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Helper: build a stage prompt by concatenating the common header with
|
|
246
|
-
// the stack-specific body extracted from the original pass3-prompt.md.
|
|
247
|
-
// For v2.1 we reuse the full stack template body for 3b/3c/3d, letting
|
|
248
|
-
// the stage header restrict scope via its "Scope of this step" section.
|
|
249
|
-
// A future v2.2 could split the stack templates themselves into per-stage
|
|
250
|
-
// sections; for now this preserves template fidelity while bounding scope.
|
|
251
|
-
const pass3PromptFile = path.join(GENERATED_DIR, "pass3-prompt.md");
|
|
252
|
-
if (!fileExists(pass3PromptFile)) {
|
|
253
|
-
throw new InitError("pass3-prompt.md not found. Re-run plan-installer.");
|
|
254
|
-
}
|
|
255
|
-
const fullPass3Body = readFile(pass3PromptFile);
|
|
256
|
-
|
|
257
|
-
function buildStagePrompt(stageHeaderFile, includeStackBody) {
|
|
258
|
-
const headerPath = path.join(COMMON_DIR, stageHeaderFile);
|
|
259
|
-
if (!existsSafe(headerPath)) {
|
|
260
|
-
throw new InitError(
|
|
261
|
-
`Pass 3 split stage header missing: ${stageHeaderFile}\n` +
|
|
262
|
-
` Expected at: ${headerPath}\n` +
|
|
263
|
-
` Re-install claudeos-core or run plan-installer to regenerate templates.`
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
|
-
const header = readFileSafe(headerPath);
|
|
267
|
-
const stackBody = includeStackBody ? ("\n\n" + fullPass3Body) : "";
|
|
268
|
-
return injectProjectRoot(header + stackBody);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Helper: build a batch scope note injected before "## Scope of this step"
|
|
272
|
-
// in 3b/3c stage prompts when the project has been sub-divided into batches.
|
|
273
|
-
// Tells Claude explicitly which domains to process in this particular call,
|
|
274
|
-
// and which common files to include vs skip.
|
|
275
|
-
function buildBatchScopeNote(stageKind, batchIndex, totalBatches, batchDomains) {
|
|
276
|
-
const isLastBatch = batchIndex === totalBatches - 1;
|
|
277
|
-
const domainList = batchDomains.map(d => `\`${d}\``).join(", ");
|
|
278
|
-
|
|
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`;
|
|
282
|
-
|
|
283
|
-
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`;
|
|
291
|
-
} 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`;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (!isLastBatch) {
|
|
302
|
-
note += `\n**CRITICAL**: Other domains exist in the project but will be processed by LATER batches. Do not attempt to process them now — doing so will consume context that later batches need.\n`;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return note + "\n";
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Helper: build a "core common files only" prompt for 3b-core / 3c-core
|
|
309
|
-
// stages. Injected between the stage header and the stack body to restrict
|
|
310
|
-
// scope to project-wide files (CLAUDE.md, common standards, guides, shared
|
|
311
|
-
// skills) — explicitly excluding any domain-specific output that belongs
|
|
312
|
-
// to subsequent batch stages.
|
|
313
|
-
function buildStageCorePrompt(stageKind, batchesList) {
|
|
314
|
-
const totalBatches = batchesList.length;
|
|
315
|
-
const totalDomains = batchesList.reduce((sum, b) => sum + b.length, 0);
|
|
316
|
-
|
|
317
|
-
let coreNote = `## Core common files stage (${stageKind}-core)\n\n`;
|
|
318
|
-
coreNote += `This Pass 3 run has ${totalDomains} domains divided into ${totalBatches} batches.\n`;
|
|
319
|
-
coreNote += `To keep each stage's output within safe context limits, project-wide common\n`;
|
|
320
|
-
coreNote += `files are generated in THIS dedicated core stage, BEFORE the per-domain batches.\n\n`;
|
|
321
|
-
|
|
322
|
-
if (stageKind === "3b") {
|
|
323
|
-
coreNote += `**Scope of ${stageKind}-core**:\n`;
|
|
324
|
-
coreNote += `1. CLAUDE.md at the project root.\n`;
|
|
325
|
-
coreNote += `2. ALL standard/ files that are NOT tied to a specific domain:\n`;
|
|
326
|
-
coreNote += ` - claudeos-core/standard/00.core/*.md (project overview, architecture, conventions)\n`;
|
|
327
|
-
coreNote += ` - claudeos-core/standard/30.security-db/*.md\n`;
|
|
328
|
-
coreNote += ` - claudeos-core/standard/40.infra/*.md\n`;
|
|
329
|
-
coreNote += ` - claudeos-core/standard/50.verification/*.md\n`;
|
|
330
|
-
coreNote += ` - claudeos-core/standard/90.optional/*.md\n`;
|
|
331
|
-
coreNote += ` - (stack-specific common sections as defined in the pass3 template)\n`;
|
|
332
|
-
coreNote += `3. ALL rules files (via staging-override path .claude/rules → generated/.staged-rules):\n`;
|
|
333
|
-
coreNote += ` - common rules that apply regardless of domain.\n\n`;
|
|
334
|
-
coreNote += `**What NOT to generate in ${stageKind}-core**:\n`;
|
|
335
|
-
coreNote += `- Per-domain standard/ files (e.g. \`claudeos-core/standard/10.backend/order-api.md\` for a specific domain "order")\n`;
|
|
336
|
-
coreNote += `- Per-domain rules.\n`;
|
|
337
|
-
coreNote += `- Anything under claudeos-core/skills/, claudeos-core/guide/, claudeos-core/plan/ — those belong to later stages (3c-core, 3c-N, 3d).\n\n`;
|
|
338
|
-
coreNote += `**Per-domain files will be generated in subsequent 3b-1, 3b-2, ... batch stages.**\n`;
|
|
339
|
-
} else if (stageKind === "3c") {
|
|
340
|
-
coreNote += `**Scope of ${stageKind}-core**:\n`;
|
|
341
|
-
coreNote += `1. ALL guide/ files (project-wide, domain-independent):\n`;
|
|
342
|
-
coreNote += ` - claudeos-core/guide/01.onboarding/*.md\n`;
|
|
343
|
-
coreNote += ` - claudeos-core/guide/02.usage/*.md\n`;
|
|
344
|
-
coreNote += ` - claudeos-core/guide/03.troubleshooting/*.md\n`;
|
|
345
|
-
coreNote += ` - claudeos-core/guide/04.architecture/*.md\n`;
|
|
346
|
-
coreNote += `2. COMMON skills only:\n`;
|
|
347
|
-
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`;
|
|
349
|
-
coreNote += `**What NOT to generate in ${stageKind}-core**:\n`;
|
|
350
|
-
coreNote += `- Per-domain skill sub-directories (e.g. \`10.backend-crud/scaffold-order-feature/\`) — those belong to 3c-1, 3c-2, ... batch stages.\n`;
|
|
351
|
-
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`;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
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`;
|
|
356
|
-
|
|
357
|
-
const headerFile = stageKind === "3b" ? "pass3b-core-header.md" : "pass3c-skills-guide-header.md";
|
|
358
|
-
const baseprompt = buildStagePrompt(headerFile, true);
|
|
359
|
-
return baseprompt.replace(/\n## Scope of this step/, `\n${coreNote}\n## Scope of this step`);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Helper: build the prompt for 3d-aux (the only Pass 3d sub-stage that
|
|
363
|
-
// actually runs now). Master plan aggregation (standard/rules/skills/guide)
|
|
364
|
-
// was removed because master plans are an internal tool backup not consumed
|
|
365
|
-
// by Claude Code at runtime, and aggregating 30+ files in a single session
|
|
366
|
-
// was the primary source of "Prompt is too long" failures on mid-sized
|
|
367
|
-
// projects (observed on an 18-domain production run).
|
|
368
|
-
//
|
|
369
|
-
// The subStage parameter is kept for forward-compat: if a future version
|
|
370
|
-
// reintroduces master plans via Node-side aggregation, this helper can
|
|
371
|
-
// route the extra sub-stages back in.
|
|
372
|
-
function build3dSubPrompt(subStage) {
|
|
373
|
-
const baseprompt = buildStagePrompt("pass3d-plan-aux-header.md", true);
|
|
374
|
-
|
|
375
|
-
if (subStage !== "aux") {
|
|
376
|
-
throw new InitError(
|
|
377
|
-
`build3dSubPrompt called with unsupported subStage "${subStage}". ` +
|
|
378
|
-
`Master plan sub-stages (standard/rules/skills/guide) were removed in this version. ` +
|
|
379
|
-
`Only "aux" is supported.`
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
let scopeNote = `## 3d sub-stage: aux\n\n`;
|
|
384
|
-
scopeNote += `Pass 3d now produces only auxiliary documentation (database + mcp-guide). Master plan aggregation (standard/rules/skills/guide → plan/*-master.md) was removed because master plans are an internal tool backup not consumed by Claude Code at runtime, and aggregating many files in a single session was the primary cause of "Prompt is too long" failures.\n`;
|
|
385
|
-
scopeNote += `**You are processing sub-stage \`3d-aux\`.**\n\n`;
|
|
386
|
-
|
|
387
|
-
scopeNote += `### Scope of 3d-aux (exclusively)\n\n`;
|
|
388
|
-
scopeNote += `Generate ONLY auxiliary documentation:\n`;
|
|
389
|
-
scopeNote += `- \`claudeos-core/database/\` — schema docs and SQL reference. If pass3a-facts.md shows no database was detected, write a single \`README.md\` stub explaining this and stop.\n`;
|
|
390
|
-
scopeNote += `- \`claudeos-core/mcp-guide/\` — MCP integration guide. If no relevant MCP servers apply, write \`README.md\` stub and stop.\n\n`;
|
|
391
|
-
scopeNote += `**DO NOT touch \`claudeos-core/plan/\`** — master plans are no longer generated in this version. If the \`plan/\` directory exists from a previous run, leave it untouched (Rule B).\n\n`;
|
|
392
|
-
scopeNote += `Rule B applies. Absence of database/ or mcp-guide/ is warning-level, so README stubs are acceptable.\n`;
|
|
393
|
-
|
|
394
|
-
return baseprompt.replace(/\n## Scope of this step/, `\n${scopeNote}\n## Scope of this step`);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
// Helper: run a single stage with progress ticker, staged-rules move,
|
|
399
|
-
// and (optionally) per-stage output validation.
|
|
400
|
-
async function runStage(stageId, label, promptStr, opts = {}) {
|
|
401
|
-
if (completedGroups.includes(stageId)) {
|
|
402
|
-
log(` ⏭️ ${stageId} (${label}) already complete, skipping`);
|
|
403
|
-
return { skipped: true };
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// Clear stale staging before each stage. 3a doesn't write rules so
|
|
407
|
-
// it's a no-op there; 3b/3c/3d may write staged rules.
|
|
408
|
-
if (fileExists(stagingDir)) {
|
|
409
|
-
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
log("");
|
|
413
|
-
log(` 🚀 ${stageId} — ${label}`);
|
|
414
|
-
const t0 = Date.now();
|
|
415
|
-
const ticker = makePassTicker(`Pass ${stageId}`, t0, { baselineCount: countFiles() });
|
|
416
|
-
const ok = await runClaudePromptAsync(promptStr, {
|
|
417
|
-
onTick: ticker.onTick,
|
|
418
|
-
tickMs: ticker.tickMs,
|
|
419
|
-
});
|
|
420
|
-
ticker.clearLine();
|
|
421
|
-
const elapsed = Date.now() - t0;
|
|
422
|
-
stepTimes.push(elapsed);
|
|
423
|
-
|
|
424
|
-
if (!ok) {
|
|
425
|
-
throw new InitError(
|
|
426
|
-
`Pass ${stageId} (${label}) failed. Check the claude error output above.\n` +
|
|
427
|
-
` Already-completed stages are preserved; re-running will resume from ${stageId}.\n` +
|
|
428
|
-
` If this persists, try: npx claudeos-core init --force`
|
|
429
|
-
);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// Move staged rules after stages that generate rule files (3b, 3c, 3d).
|
|
433
|
-
// Pass 3a only writes pass3a-facts.md, so no staging to move.
|
|
434
|
-
if (opts.expectsStagedRules) {
|
|
435
|
-
const move = moveStagedRules(PROJECT_ROOT);
|
|
436
|
-
if (move.failed > 0) {
|
|
437
|
-
log(` ⚠️ ${stageId} staged-rules: ${move.moved} moved, ${move.failed} failed`);
|
|
438
|
-
for (const err of move.errors) log(` • ${err}`);
|
|
439
|
-
throw new InitError(
|
|
440
|
-
`Pass ${stageId} finished but ${move.failed} rule file(s) could not be moved from staging.\n` +
|
|
441
|
-
` This is usually a transient file-lock issue. Re-run \`npx claudeos-core init\`.`
|
|
442
|
-
);
|
|
443
|
-
} else if (move.moved > 0) {
|
|
444
|
-
log(` 📦 ${stageId} staged-rules: ${move.moved} rule files moved to .claude/rules/`);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// Per-stage output validation (delegates to opts.validate callback).
|
|
449
|
-
if (typeof opts.validate === "function") {
|
|
450
|
-
const problems = opts.validate();
|
|
451
|
-
if (problems && problems.length > 0) {
|
|
452
|
-
const preview = problems.slice(0, 5).map(p => ` • ${p}`).join("\n");
|
|
453
|
-
const more = problems.length > 5 ? `\n • ... and ${problems.length - 5} more` : "";
|
|
454
|
-
throw new InitError(
|
|
455
|
-
`Pass ${stageId} (${label}) produced incomplete output:\n` +
|
|
456
|
-
preview + more + "\n" +
|
|
457
|
-
` Already-completed stages are preserved. Re-run to retry from ${stageId}.`
|
|
458
|
-
);
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// Mark stage complete and persist marker so a crash in a later stage
|
|
463
|
-
// doesn't lose this stage's progress.
|
|
464
|
-
completedGroups.push(stageId);
|
|
465
|
-
const persisted = persistMarker(false);
|
|
466
|
-
if (!persisted) {
|
|
467
|
-
throw new InitError(
|
|
468
|
-
`Pass ${stageId} succeeded but failed to persist progress to pass3-complete.json.\n` +
|
|
469
|
-
` Check disk space / permissions on ${GENERATED_DIR}/.`
|
|
470
|
-
);
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
log(` ✅ ${stageId} complete (${formatElapsed(elapsed)})`);
|
|
474
|
-
return { skipped: false, elapsed };
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// ═══ Stage 3a: Extract facts ═══════════════════════════════════
|
|
478
|
-
const factsFile = path.join(GENERATED_DIR, "pass3a-facts.md");
|
|
479
|
-
await runStage("3a", "fact extraction", buildStagePrompt("pass3a-facts.md", false), {
|
|
480
|
-
expectsStagedRules: false,
|
|
481
|
-
validate: () => {
|
|
482
|
-
const problems = [];
|
|
483
|
-
if (!fileExists(factsFile)) {
|
|
484
|
-
problems.push("pass3a-facts.md was not created");
|
|
485
|
-
} else {
|
|
486
|
-
const content = readFileSafe(factsFile, "");
|
|
487
|
-
const stripped = content.replace(/^\uFEFF/, "").trim();
|
|
488
|
-
if (stripped.length < 500) {
|
|
489
|
-
problems.push(`pass3a-facts.md too short (${stripped.length} chars, expected >= 500)`);
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
return problems;
|
|
493
|
-
},
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
// ═══ Stage 3b: CLAUDE.md + standard/ + .claude/rules/ ═══════════
|
|
497
|
-
//
|
|
498
|
-
//
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
// 3b-core
|
|
502
|
-
// +
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
if (isBatched) {
|
|
507
|
-
await runStage("3b-core", "core common files (CLAUDE.md + common standard + common rules)",
|
|
508
|
-
buildStageCorePrompt("3b", batches),
|
|
509
|
-
{
|
|
510
|
-
expectsStagedRules: true,
|
|
511
|
-
validate: () => {
|
|
512
|
-
const problems = [];
|
|
513
|
-
if (!fileExists(claudeMdPath)) {
|
|
514
|
-
problems.push("CLAUDE.md was not created");
|
|
515
|
-
}
|
|
516
|
-
const stdSentinel = path.join(PROJECT_ROOT, "claudeos-core/standard/00.core/01.project-overview.md");
|
|
517
|
-
if (!fileExists(stdSentinel)) {
|
|
518
|
-
problems.push("claudeos-core/standard/00.core/01.project-overview.md missing");
|
|
519
|
-
} else {
|
|
520
|
-
const c = readFileSafe(stdSentinel, "");
|
|
521
|
-
if (c.replace(/^\uFEFF/, "").trim().length === 0) {
|
|
522
|
-
problems.push("claudeos-core/standard/00.core/01.project-overview.md is empty");
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
// 3b-core
|
|
526
|
-
return problems;
|
|
527
|
-
},
|
|
528
|
-
}
|
|
529
|
-
);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
//
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
for (let bi = 0; bi < batches.length; bi++) {
|
|
536
|
-
const batchDomains = batches[bi];
|
|
537
|
-
const stageId = isBatched ? `3b-${bi + 1}` : "3b";
|
|
538
|
-
const label = isBatched
|
|
539
|
-
? `domain batch ${bi + 1}/${batches.length} (${batchDomains.length} domains)`
|
|
540
|
-
: "core files (CLAUDE.md + standard + rules)";
|
|
541
|
-
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
const batchScopeNote = isBatched
|
|
545
|
-
? buildBatchScopeNote("3b", bi, batches.length, batchDomains)
|
|
546
|
-
: "";
|
|
547
|
-
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;
|
|
551
|
-
|
|
552
|
-
await runStage(stageId, label, promptWithScope, {
|
|
553
|
-
expectsStagedRules: true,
|
|
554
|
-
validate: () => {
|
|
555
|
-
const problems = [];
|
|
556
|
-
//
|
|
557
|
-
if (!isBatched) {
|
|
558
|
-
if (!fileExists(claudeMdPath)) {
|
|
559
|
-
problems.push("CLAUDE.md was not created");
|
|
560
|
-
}
|
|
561
|
-
const stdSentinel = path.join(PROJECT_ROOT, "claudeos-core/standard/00.core/01.project-overview.md");
|
|
562
|
-
if (!fileExists(stdSentinel)) {
|
|
563
|
-
problems.push("claudeos-core/standard/00.core/01.project-overview.md missing");
|
|
564
|
-
} else {
|
|
565
|
-
const c = readFileSafe(stdSentinel, "");
|
|
566
|
-
if (c.replace(/^\uFEFF/, "").trim().length === 0) {
|
|
567
|
-
problems.push("claudeos-core/standard/00.core/01.project-overview.md is empty");
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
//
|
|
572
|
-
const rulesDir = path.join(PROJECT_ROOT, ".claude/rules");
|
|
573
|
-
const rulesCount = countFilesRecursive(rulesDir);
|
|
574
|
-
if (rulesCount === 0) {
|
|
575
|
-
problems.push(".claude/rules/ has 0 files (staging-override may have been ignored)");
|
|
576
|
-
}
|
|
577
|
-
return problems;
|
|
578
|
-
},
|
|
579
|
-
});
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
// ═══ Stage 3c: skills/ + guide/ ═══════════════════════════════
|
|
583
|
-
//
|
|
584
|
-
//
|
|
585
|
-
//
|
|
586
|
-
//
|
|
587
|
-
// 3c-core
|
|
588
|
-
//
|
|
589
|
-
if (isBatched) {
|
|
590
|
-
await runStage("3c-core", "common guides and shared skills",
|
|
591
|
-
buildStageCorePrompt("3c", batches),
|
|
592
|
-
{
|
|
593
|
-
expectsStagedRules: true, // shared skills occasionally include rule files
|
|
594
|
-
validate: () => {
|
|
595
|
-
const problems = [];
|
|
596
|
-
const guideDir = path.join(PROJECT_ROOT, "claudeos-core/guide");
|
|
597
|
-
const missingGuides = EXPECTED_GUIDE_FILES.filter(g => {
|
|
598
|
-
const fp = path.join(guideDir, g);
|
|
599
|
-
if (!fileExists(fp)) return true;
|
|
600
|
-
try {
|
|
601
|
-
return fs.readFileSync(fp, "utf-8").replace(/^\uFEFF/, "").trim().length === 0;
|
|
602
|
-
} catch (_e) { return true; }
|
|
603
|
-
});
|
|
604
|
-
for (const g of missingGuides) {
|
|
605
|
-
problems.push(`claudeos-core/guide/${g} missing or empty`);
|
|
606
|
-
}
|
|
607
|
-
// skills/
|
|
608
|
-
return problems;
|
|
609
|
-
},
|
|
610
|
-
}
|
|
611
|
-
);
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
//
|
|
615
|
-
//
|
|
616
|
-
//
|
|
617
|
-
for (let bi = 0; bi < batches.length; bi++) {
|
|
618
|
-
const batchDomains = batches[bi];
|
|
619
|
-
const stageId = isBatched ? `3c-${bi + 1}` : "3c";
|
|
620
|
-
const label = isBatched
|
|
621
|
-
? `domain skills batch ${bi + 1}/${batches.length} (${batchDomains.length} domains)`
|
|
622
|
-
: "skills and guides";
|
|
623
|
-
|
|
624
|
-
const batchScopeNote = isBatched
|
|
625
|
-
? buildBatchScopeNote("3c", bi, batches.length, batchDomains)
|
|
626
|
-
: "";
|
|
627
|
-
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;
|
|
631
|
-
|
|
632
|
-
await runStage(stageId, label, promptWithScope, {
|
|
633
|
-
expectsStagedRules: true, // skills occasionally include rule files
|
|
634
|
-
validate: () => {
|
|
635
|
-
const problems = [];
|
|
636
|
-
//
|
|
637
|
-
if (!isBatched) {
|
|
638
|
-
const guideDir = path.join(PROJECT_ROOT, "claudeos-core/guide");
|
|
639
|
-
const missingGuides = EXPECTED_GUIDE_FILES.filter(g => {
|
|
640
|
-
const fp = path.join(guideDir, g);
|
|
641
|
-
if (!fileExists(fp)) return true;
|
|
642
|
-
try {
|
|
643
|
-
return fs.readFileSync(fp, "utf-8").replace(/^\uFEFF/, "").trim().length === 0;
|
|
644
|
-
} catch (_e) { return true; }
|
|
645
|
-
});
|
|
646
|
-
for (const g of missingGuides) {
|
|
647
|
-
problems.push(`claudeos-core/guide/${g} missing or empty`);
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
//
|
|
651
|
-
if (!isBatched || bi === batches.length - 1) {
|
|
652
|
-
const { hasNonEmptyMdRecursive } = require("../../lib/expected-outputs");
|
|
653
|
-
const skillsDir = path.join(PROJECT_ROOT, "claudeos-core/skills");
|
|
654
|
-
if (!fileExists(skillsDir) || !hasNonEmptyMdRecursive(skillsDir)) {
|
|
655
|
-
problems.push("claudeos-core/skills/ has no non-empty .md files");
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
return problems;
|
|
659
|
-
},
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
// ═══ Stage 3d: plan/ + database/ + mcp-guide/ ═════════════════
|
|
664
|
-
//
|
|
665
|
-
// 3d
|
|
666
|
-
// database/mcp-guide
|
|
667
|
-
//
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
// master plan
|
|
671
|
-
//
|
|
672
|
-
//
|
|
673
|
-
//
|
|
674
|
-
// 3d-aux → database/ + mcp-guide/ (
|
|
675
|
-
|
|
676
|
-
// 3d-aux: database/ + mcp-guide/ (absence is warning-level)
|
|
677
|
-
await runStage("3d-aux", "aux docs (database + mcp-guide)",
|
|
678
|
-
build3dSubPrompt("aux"),
|
|
679
|
-
{
|
|
680
|
-
expectsStagedRules: false,
|
|
681
|
-
validate: () => [],
|
|
682
|
-
}
|
|
683
|
-
);
|
|
684
|
-
|
|
685
|
-
// ─── Final marker: all 4 stages done ─────────────────────────
|
|
686
|
-
const finalPersist = persistMarker(true);
|
|
687
|
-
if (!finalPersist) {
|
|
688
|
-
throw new InitError(
|
|
689
|
-
"Pass 3 split all stages complete but failed to write final marker.\n" +
|
|
690
|
-
` Check disk space / permissions on ${GENERATED_DIR}/.`
|
|
691
|
-
);
|
|
692
|
-
}
|
|
693
|
-
//
|
|
694
|
-
// 3a (1) + 3b
|
|
695
|
-
//
|
|
696
|
-
//
|
|
697
|
-
const three3dStages = 1; // aux only (master plan aggregation removed)
|
|
698
|
-
const totalStages = isBatched
|
|
699
|
-
? (1 + 1 + batches.length + 1 + batches.length + three3dStages)
|
|
700
|
-
: (1 + 1 + 1 + three3dStages);
|
|
701
|
-
log(` 🎉 Pass 3 split complete: ${completedGroups.length}/${totalStages} stages successful`);
|
|
702
|
-
}
|
|
703
|
-
|
|
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 ───────────────────────────────────
|
|
713
|
-
const hasProjectMarker = [".git", "package.json", "build.gradle", "build.gradle.kts", "pom.xml", "pyproject.toml", "requirements.txt"].some(
|
|
714
|
-
m => fs.existsSync(path.join(PROJECT_ROOT, m))
|
|
715
|
-
);
|
|
716
|
-
if (!hasProjectMarker) {
|
|
717
|
-
log(`\n ⚠️ Warning: ${PROJECT_ROOT} does not look like a project root.`);
|
|
718
|
-
log(" No .git, package.json, build.gradle, or pom.xml found.");
|
|
719
|
-
log(" Run this command from your project directory.\n");
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
const nodeVersion = parseInt(process.versions.node.split(".")[0]);
|
|
723
|
-
if (nodeVersion < 18) {
|
|
724
|
-
throw new InitError(`Node.js v18+ required (current: v${process.versions.node})\n Install: https://nodejs.org/`);
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
const claudeExists = run("claude --version", { silent: true, ignoreError: true });
|
|
728
|
-
if (!claudeExists) {
|
|
729
|
-
throw new InitError("Claude Code CLI not found.\n Install: https://code.claude.com/docs/en/overview\n Then run: claude (and complete authentication)");
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
// Verify Claude is authenticated (quick prompt test)
|
|
733
|
-
const claudeAuth = run('claude -p "echo ok"', { silent: true, ignoreError: true });
|
|
734
|
-
if (!claudeAuth) {
|
|
735
|
-
throw new InitError("Claude Code may not be authenticated.\n Run: claude (and complete authentication)\n Then retry: npx claudeos-core init");
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
// ─── Language selection (required) ────────────────────────────
|
|
739
|
-
let lang = parsedArgs.lang;
|
|
740
|
-
if (!lang) {
|
|
741
|
-
lang = await selectLangInteractive();
|
|
742
|
-
}
|
|
743
|
-
if (!lang) {
|
|
744
|
-
throw new InitError("Cancelled.");
|
|
745
|
-
}
|
|
746
|
-
if (!isValidLang(lang)) {
|
|
747
|
-
throw new InitError(`Unsupported language: "${lang}"\n Supported: ${LANG_CODES.join(", ")}`);
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
// Early incompatibility check: CLAUDEOS_SKIP_TRANSLATION is a test-only
|
|
751
|
-
// env var that short-circuits lib/memory-scaffold.js translation path.
|
|
752
|
-
// If set AND the user chose a non-English language, Pass 4's static fallback
|
|
753
|
-
// and gap-fill would throw mid-run with a confusing "translation skipped"
|
|
754
|
-
// error. Fail fast here with a clear message so the user can unset the var
|
|
755
|
-
// or pick --lang en before the pipeline starts.
|
|
756
|
-
if (process.env.CLAUDEOS_SKIP_TRANSLATION === "1" && lang !== "en") {
|
|
757
|
-
throw new InitError(
|
|
758
|
-
`CLAUDEOS_SKIP_TRANSLATION=1 is set but --lang='${lang}' requires translation.\n` +
|
|
759
|
-
` This env var is a test-only escape hatch that blocks calls to \`claude -p\`\n` +
|
|
760
|
-
` from lib/memory-scaffold.js. Pass 4 would crash later with a hard-to-\n` +
|
|
761
|
-
` diagnose error.\n\n` +
|
|
762
|
-
` Either unset the env var: unset CLAUDEOS_SKIP_TRANSLATION\n` +
|
|
763
|
-
` Or run with English output: npx claudeos-core init --lang en`
|
|
764
|
-
);
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
process.env.CLAUDEOS_LANG = lang;
|
|
768
|
-
|
|
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
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
];
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
}
|
|
872
|
-
log(" ✅ Done\n");
|
|
873
|
-
|
|
874
|
-
// ─── [
|
|
875
|
-
header("[
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
for (
|
|
934
|
-
const
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
const
|
|
974
|
-
const
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
})
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
//
|
|
1092
|
-
//
|
|
1093
|
-
//
|
|
1094
|
-
//
|
|
1095
|
-
//
|
|
1096
|
-
//
|
|
1097
|
-
//
|
|
1098
|
-
//
|
|
1099
|
-
//
|
|
1100
|
-
|
|
1101
|
-
const {
|
|
1102
|
-
const
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
//
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
//
|
|
1128
|
-
//
|
|
1129
|
-
//
|
|
1130
|
-
//
|
|
1131
|
-
//
|
|
1132
|
-
//
|
|
1133
|
-
//
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
//
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
)
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1
|
+
/**
|
|
2
|
+
* ClaudeOS-Core — Init Command
|
|
3
|
+
*
|
|
4
|
+
* Runs the full 4-Pass pipeline: analyze → merge → generate → memory scaffold.
|
|
5
|
+
* This is the main entry point for project bootstrapping.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const {
|
|
11
|
+
TOOLS_DIR, PROJECT_ROOT, GENERATED_DIR,
|
|
12
|
+
SUPPORTED_LANGS, LANG_CODES, isValidLang,
|
|
13
|
+
log, header, run, runClaudePrompt, runClaudePromptAsync,
|
|
14
|
+
ensureDir, fileExists, readFile, injectProjectRoot,
|
|
15
|
+
pad, countFiles, countPass1Files,
|
|
16
|
+
} = require("../lib/cli-utils");
|
|
17
|
+
const { selectLangInteractive } = require("../lib/lang-selector");
|
|
18
|
+
const { selectResumeMode } = require("../lib/resume-selector");
|
|
19
|
+
|
|
20
|
+
const { EXPECTED_GUIDE_FILES } = require("../../lib/expected-guides");
|
|
21
|
+
const { findMissingOutputs } = require("../../lib/expected-outputs");
|
|
22
|
+
|
|
23
|
+
class InitError extends Error {
|
|
24
|
+
constructor(msg) { super(msg); this.name = "InitError"; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function formatElapsed(ms) {
|
|
28
|
+
const sec = Math.floor(ms / 1000);
|
|
29
|
+
if (sec < 60) return `${sec}s`;
|
|
30
|
+
const min = Math.floor(sec / 60);
|
|
31
|
+
const rem = sec % 60;
|
|
32
|
+
return rem > 0 ? `${min}m ${rem}s` : `${min}m`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Creates an onTick/clearLine pair for long-running claude -p passes. We can
|
|
36
|
+
// only observe progress externally (elapsed time, sometimes filesystem delta).
|
|
37
|
+
// TTYs get a single \r-rewritten line; CI/piped stdout gets periodic new lines.
|
|
38
|
+
// Modes via opts:
|
|
39
|
+
// - elapsed-only (no baselineCount): ⏳ label running... 45s
|
|
40
|
+
// - file delta (baselineCount set): 📝 label generating... 24 new files | 45s
|
|
41
|
+
// - fixed target (+ totalExpected): 📝 label generating... 8/12 files (67%) | 45s
|
|
42
|
+
function makePassTicker(label, startTime, opts = {}) {
|
|
43
|
+
const isTTY = Boolean(process.stdout.isTTY);
|
|
44
|
+
const { baselineCount, totalExpected } = opts;
|
|
45
|
+
const trackFiles = typeof baselineCount === "number";
|
|
46
|
+
let lastLineLen = 0;
|
|
47
|
+
function onTick() {
|
|
48
|
+
const elapsed = formatElapsed(Date.now() - startTime);
|
|
49
|
+
let line;
|
|
50
|
+
if (!trackFiles) {
|
|
51
|
+
line = ` ⏳ ${label} running... ${elapsed} elapsed`;
|
|
52
|
+
} else {
|
|
53
|
+
const current = countFiles();
|
|
54
|
+
const delta = typeof current === "number" ? Math.max(0, current - baselineCount) : null;
|
|
55
|
+
let progress;
|
|
56
|
+
if (delta === null) progress = "? new files";
|
|
57
|
+
else if (typeof totalExpected === "number" && totalExpected > 0) {
|
|
58
|
+
const capped = Math.min(delta, totalExpected);
|
|
59
|
+
const pct = Math.round((capped / totalExpected) * 100);
|
|
60
|
+
progress = `${capped}/${totalExpected} files (${pct}%)`;
|
|
61
|
+
} else {
|
|
62
|
+
progress = `${delta} new files`;
|
|
63
|
+
}
|
|
64
|
+
line = ` 📝 ${label} generating... ${progress} | ${elapsed} elapsed`;
|
|
65
|
+
}
|
|
66
|
+
if (isTTY) {
|
|
67
|
+
const pad = " ".repeat(Math.max(0, lastLineLen - line.length));
|
|
68
|
+
process.stdout.write("\r" + line + pad);
|
|
69
|
+
lastLineLen = line.length;
|
|
70
|
+
} else {
|
|
71
|
+
log(line);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function clearLine() {
|
|
75
|
+
if (isTTY && lastLineLen > 0) {
|
|
76
|
+
process.stdout.write("\r" + " ".repeat(lastLineLen) + "\r");
|
|
77
|
+
lastLineLen = 0;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { onTick, clearLine, tickMs: isTTY ? 1000 : 15000 };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
84
|
+
// v2.1: Pass 3 Split Runner
|
|
85
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
86
|
+
//
|
|
87
|
+
// Splits the monolithic Pass 3 into 4 sequential claude -p calls:
|
|
88
|
+
//
|
|
89
|
+
// 3a: Extract facts from pass2-merged.json into pass3a-facts.md
|
|
90
|
+
// (small document, becomes the shared context for 3b/3c/3d).
|
|
91
|
+
// 3b: Generate CLAUDE.md + standard/ + .claude/rules/ (core files).
|
|
92
|
+
// 3c: Generate skills/ + guide/.
|
|
93
|
+
// 3d: Generate plan/ + database/ + mcp-guide/ (master plans + aux).
|
|
94
|
+
//
|
|
95
|
+
// Each call has fresh context, so the total output volume can far exceed
|
|
96
|
+
// the model's context window without `Prompt is too long` failures.
|
|
97
|
+
// pass3a-facts.md replaces pass2-merged.json as the cross-stage reference,
|
|
98
|
+
// so cross-file consistency (which was the main advantage of the single-call
|
|
99
|
+
// approach) is preserved.
|
|
100
|
+
//
|
|
101
|
+
// Marker schema (pass3-complete.json):
|
|
102
|
+
// {
|
|
103
|
+
// "completedAt": "<ISO timestamp of final stage>",
|
|
104
|
+
// "mode": "split",
|
|
105
|
+
// "groupsCompleted": ["3a", "3b", "3c", "3d"]
|
|
106
|
+
// }
|
|
107
|
+
// Partial marker after interruption:
|
|
108
|
+
// { "mode": "split", "groupsCompleted": ["3a", "3b"] }
|
|
109
|
+
// On re-run, stages in groupsCompleted are skipped.
|
|
110
|
+
//
|
|
111
|
+
// All helper references (injectProjectRoot, fileExists, runClaudePromptAsync,
|
|
112
|
+
// etc.) are passed in via the ctx param rather than being captured by closure,
|
|
113
|
+
// because this function lives outside cmdInit for readability.
|
|
114
|
+
async function runPass3Split(ctx) {
|
|
115
|
+
const {
|
|
116
|
+
GENERATED_DIR, PROJECT_ROOT, TOOLS_DIR,
|
|
117
|
+
pass3Marker, claudeMdPath,
|
|
118
|
+
injectProjectRoot, readFile, fileExists,
|
|
119
|
+
runClaudePromptAsync, makePassTicker, formatElapsed,
|
|
120
|
+
log, countFiles,
|
|
121
|
+
EXPECTED_GUIDE_FILES, findMissingOutputs,
|
|
122
|
+
lang, stepTimes,
|
|
123
|
+
} = ctx;
|
|
124
|
+
|
|
125
|
+
const { writeFileSafe, readFileSafe, existsSafe } = require("../../lib/safe-fs");
|
|
126
|
+
const { moveStagedRules, countFilesRecursive } = require("../../lib/staged-rules");
|
|
127
|
+
|
|
128
|
+
const COMMON_DIR = path.join(TOOLS_DIR, "pass-prompts/templates/common");
|
|
129
|
+
const stagingDir = path.join(GENERATED_DIR, ".staged-rules");
|
|
130
|
+
|
|
131
|
+
// ─── Batch sub-division for 3b/3c on large projects ───────────
|
|
132
|
+
// Even with split mode, a single 3b call that generates standard/ + rules/
|
|
133
|
+
// for 50+ domains can still hit context overflow within that stage's
|
|
134
|
+
// session (output accumulation). We sub-divide 3b and 3c into batches of
|
|
135
|
+
// ~DOMAINS_PER_BATCH domains each when totalDomains > DOMAINS_PER_BATCH.
|
|
136
|
+
//
|
|
137
|
+
// 3a is never batched (single fact sheet).
|
|
138
|
+
// 3d is never batched (master plan aggregation).
|
|
139
|
+
//
|
|
140
|
+
// Threshold rationale: 15 domains ≈ 45-60 output files for 3b alone,
|
|
141
|
+
// which stays within the empirically safe single-session output range.
|
|
142
|
+
// Beyond 15 we split. ceil(N/15) batches, preserving order from
|
|
143
|
+
// domain-groups.json (already balanced by plan-installer).
|
|
144
|
+
const DOMAINS_PER_BATCH = 15;
|
|
145
|
+
|
|
146
|
+
function loadDomainOrder() {
|
|
147
|
+
// Returns an ordered list of domain names for batching 3b/3c.
|
|
148
|
+
// Primary source: domain-groups.json (already balanced).
|
|
149
|
+
// Fallback: project-analysis.json backendDomains + frontendDomains.
|
|
150
|
+
// Final fallback: empty array → single batch (no sub-division).
|
|
151
|
+
try {
|
|
152
|
+
const dgPath = path.join(GENERATED_DIR, "domain-groups.json");
|
|
153
|
+
if (fileExists(dgPath)) {
|
|
154
|
+
const dg = JSON.parse(readFile(dgPath));
|
|
155
|
+
const groups = Array.isArray(dg) ? dg : (dg && dg.groups);
|
|
156
|
+
if (Array.isArray(groups)) {
|
|
157
|
+
const flat = [];
|
|
158
|
+
for (const g of groups) {
|
|
159
|
+
const items = Array.isArray(g.domains) ? g.domains : (Array.isArray(g) ? g : []);
|
|
160
|
+
for (const d of items) {
|
|
161
|
+
const name = typeof d === "string" ? d : (d && d.name);
|
|
162
|
+
if (name) flat.push(name);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (flat.length > 0) return flat;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} catch (_e) { /* fall through to analysis */ }
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const paPath = path.join(GENERATED_DIR, "project-analysis.json");
|
|
172
|
+
if (fileExists(paPath)) {
|
|
173
|
+
const pa = JSON.parse(readFile(paPath));
|
|
174
|
+
const backend = Array.isArray(pa.backendDomains) ? pa.backendDomains.map(d => d.name || d) : [];
|
|
175
|
+
const frontend = Array.isArray(pa.frontendDomains) ? pa.frontendDomains.map(d => d.name || d) : [];
|
|
176
|
+
return [...backend, ...frontend].filter(Boolean);
|
|
177
|
+
}
|
|
178
|
+
} catch (_e) { /* fall through */ }
|
|
179
|
+
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function computeBatches(domainOrder) {
|
|
184
|
+
// Returns an array of batches, where each batch is an array of domain names.
|
|
185
|
+
// If total <= DOMAINS_PER_BATCH, returns a single-batch array so the caller
|
|
186
|
+
// can use the backward-compatible "3b"/"3c" marker names.
|
|
187
|
+
if (!domainOrder || domainOrder.length <= DOMAINS_PER_BATCH) {
|
|
188
|
+
return [domainOrder || []];
|
|
189
|
+
}
|
|
190
|
+
const batches = [];
|
|
191
|
+
for (let i = 0; i < domainOrder.length; i += DOMAINS_PER_BATCH) {
|
|
192
|
+
batches.push(domainOrder.slice(i, i + DOMAINS_PER_BATCH));
|
|
193
|
+
}
|
|
194
|
+
return batches;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const domainOrder = loadDomainOrder();
|
|
198
|
+
const batches = computeBatches(domainOrder);
|
|
199
|
+
const isBatched = batches.length > 1;
|
|
200
|
+
|
|
201
|
+
if (isBatched) {
|
|
202
|
+
log(` 📦 Batch sub-division enabled: ${domainOrder.length} domains → ${batches.length} batches per stage (3b, 3c)`);
|
|
203
|
+
for (let i = 0; i < batches.length; i++) {
|
|
204
|
+
log(` • batch ${i + 1}: ${batches[i].slice(0, 3).join(", ")}${batches[i].length > 3 ? ", +" + (batches[i].length - 3) + " more" : ""}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
// ─── Load existing marker to support resume mid-split ─────────
|
|
210
|
+
// If a prior split run completed 3a and 3b but crashed at 3c, we skip
|
|
211
|
+
// 3a and 3b automatically. This is the split-mode analog of Guard 3
|
|
212
|
+
// stale-marker detection in cmdInit.
|
|
213
|
+
let completedGroups = [];
|
|
214
|
+
if (fileExists(pass3Marker)) {
|
|
215
|
+
try {
|
|
216
|
+
const existing = JSON.parse(readFile(pass3Marker));
|
|
217
|
+
if (existing && existing.mode === "split" && Array.isArray(existing.groupsCompleted)) {
|
|
218
|
+
completedGroups = existing.groupsCompleted.slice();
|
|
219
|
+
if (completedGroups.length > 0) {
|
|
220
|
+
log(` ↪️ Resuming Pass 3 split: ${completedGroups.length} stage(s) already done (${completedGroups.join(", ")})`);
|
|
221
|
+
}
|
|
222
|
+
} else if (existing && existing.mode !== "split") {
|
|
223
|
+
// Previous run was NOT split mode but marker exists and is valid.
|
|
224
|
+
// Caller (cmdInit) should have skipped us already — defensive no-op here.
|
|
225
|
+
log(` ℹ️ Pass 3 marker found (non-split mode), skipping split runner`);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
} catch (_e) {
|
|
229
|
+
log(" ⚠️ pass3-complete.json malformed, starting Pass 3 split from scratch");
|
|
230
|
+
completedGroups = [];
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Helper: write marker after each stage so partial progress survives crashes.
|
|
235
|
+
function persistMarker(complete) {
|
|
236
|
+
const body = {
|
|
237
|
+
mode: "split",
|
|
238
|
+
groupsCompleted: completedGroups.slice(),
|
|
239
|
+
lastUpdatedAt: new Date().toISOString(),
|
|
240
|
+
};
|
|
241
|
+
if (complete) body.completedAt = body.lastUpdatedAt;
|
|
242
|
+
return writeFileSafe(pass3Marker, JSON.stringify(body, null, 2));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Helper: build a stage prompt by concatenating the common header with
|
|
246
|
+
// the stack-specific body extracted from the original pass3-prompt.md.
|
|
247
|
+
// For v2.1 we reuse the full stack template body for 3b/3c/3d, letting
|
|
248
|
+
// the stage header restrict scope via its "Scope of this step" section.
|
|
249
|
+
// A future v2.2 could split the stack templates themselves into per-stage
|
|
250
|
+
// sections; for now this preserves template fidelity while bounding scope.
|
|
251
|
+
const pass3PromptFile = path.join(GENERATED_DIR, "pass3-prompt.md");
|
|
252
|
+
if (!fileExists(pass3PromptFile)) {
|
|
253
|
+
throw new InitError("pass3-prompt.md not found. Re-run plan-installer.");
|
|
254
|
+
}
|
|
255
|
+
const fullPass3Body = readFile(pass3PromptFile);
|
|
256
|
+
|
|
257
|
+
function buildStagePrompt(stageHeaderFile, includeStackBody) {
|
|
258
|
+
const headerPath = path.join(COMMON_DIR, stageHeaderFile);
|
|
259
|
+
if (!existsSafe(headerPath)) {
|
|
260
|
+
throw new InitError(
|
|
261
|
+
`Pass 3 split stage header missing: ${stageHeaderFile}\n` +
|
|
262
|
+
` Expected at: ${headerPath}\n` +
|
|
263
|
+
` Re-install claudeos-core or run plan-installer to regenerate templates.`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
const header = readFileSafe(headerPath);
|
|
267
|
+
const stackBody = includeStackBody ? ("\n\n" + fullPass3Body) : "";
|
|
268
|
+
return injectProjectRoot(header + stackBody);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Helper: build a batch scope note injected before "## Scope of this step"
|
|
272
|
+
// in 3b/3c stage prompts when the project has been sub-divided into batches.
|
|
273
|
+
// Tells Claude explicitly which domains to process in this particular call,
|
|
274
|
+
// and which common files to include vs skip.
|
|
275
|
+
function buildBatchScopeNote(stageKind, batchIndex, totalBatches, batchDomains) {
|
|
276
|
+
const isLastBatch = batchIndex === totalBatches - 1;
|
|
277
|
+
const domainList = batchDomains.map(d => `\`${d}\``).join(", ");
|
|
278
|
+
|
|
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`;
|
|
282
|
+
|
|
283
|
+
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`;
|
|
291
|
+
} 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`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!isLastBatch) {
|
|
302
|
+
note += `\n**CRITICAL**: Other domains exist in the project but will be processed by LATER batches. Do not attempt to process them now — doing so will consume context that later batches need.\n`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return note + "\n";
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Helper: build a "core common files only" prompt for 3b-core / 3c-core
|
|
309
|
+
// stages. Injected between the stage header and the stack body to restrict
|
|
310
|
+
// scope to project-wide files (CLAUDE.md, common standards, guides, shared
|
|
311
|
+
// skills) — explicitly excluding any domain-specific output that belongs
|
|
312
|
+
// to subsequent batch stages.
|
|
313
|
+
function buildStageCorePrompt(stageKind, batchesList) {
|
|
314
|
+
const totalBatches = batchesList.length;
|
|
315
|
+
const totalDomains = batchesList.reduce((sum, b) => sum + b.length, 0);
|
|
316
|
+
|
|
317
|
+
let coreNote = `## Core common files stage (${stageKind}-core)\n\n`;
|
|
318
|
+
coreNote += `This Pass 3 run has ${totalDomains} domains divided into ${totalBatches} batches.\n`;
|
|
319
|
+
coreNote += `To keep each stage's output within safe context limits, project-wide common\n`;
|
|
320
|
+
coreNote += `files are generated in THIS dedicated core stage, BEFORE the per-domain batches.\n\n`;
|
|
321
|
+
|
|
322
|
+
if (stageKind === "3b") {
|
|
323
|
+
coreNote += `**Scope of ${stageKind}-core**:\n`;
|
|
324
|
+
coreNote += `1. CLAUDE.md at the project root.\n`;
|
|
325
|
+
coreNote += `2. ALL standard/ files that are NOT tied to a specific domain:\n`;
|
|
326
|
+
coreNote += ` - claudeos-core/standard/00.core/*.md (project overview, architecture, conventions)\n`;
|
|
327
|
+
coreNote += ` - claudeos-core/standard/30.security-db/*.md\n`;
|
|
328
|
+
coreNote += ` - claudeos-core/standard/40.infra/*.md\n`;
|
|
329
|
+
coreNote += ` - claudeos-core/standard/50.verification/*.md\n`;
|
|
330
|
+
coreNote += ` - claudeos-core/standard/90.optional/*.md\n`;
|
|
331
|
+
coreNote += ` - (stack-specific common sections as defined in the pass3 template)\n`;
|
|
332
|
+
coreNote += `3. ALL rules files (via staging-override path .claude/rules → generated/.staged-rules):\n`;
|
|
333
|
+
coreNote += ` - common rules that apply regardless of domain.\n\n`;
|
|
334
|
+
coreNote += `**What NOT to generate in ${stageKind}-core**:\n`;
|
|
335
|
+
coreNote += `- Per-domain standard/ files (e.g. \`claudeos-core/standard/10.backend/order-api.md\` for a specific domain "order")\n`;
|
|
336
|
+
coreNote += `- Per-domain rules.\n`;
|
|
337
|
+
coreNote += `- Anything under claudeos-core/skills/, claudeos-core/guide/, claudeos-core/plan/ — those belong to later stages (3c-core, 3c-N, 3d).\n\n`;
|
|
338
|
+
coreNote += `**Per-domain files will be generated in subsequent 3b-1, 3b-2, ... batch stages.**\n`;
|
|
339
|
+
} else if (stageKind === "3c") {
|
|
340
|
+
coreNote += `**Scope of ${stageKind}-core**:\n`;
|
|
341
|
+
coreNote += `1. ALL guide/ files (project-wide, domain-independent):\n`;
|
|
342
|
+
coreNote += ` - claudeos-core/guide/01.onboarding/*.md\n`;
|
|
343
|
+
coreNote += ` - claudeos-core/guide/02.usage/*.md\n`;
|
|
344
|
+
coreNote += ` - claudeos-core/guide/03.troubleshooting/*.md\n`;
|
|
345
|
+
coreNote += ` - claudeos-core/guide/04.architecture/*.md\n`;
|
|
346
|
+
coreNote += `2. COMMON skills only:\n`;
|
|
347
|
+
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`;
|
|
349
|
+
coreNote += `**What NOT to generate in ${stageKind}-core**:\n`;
|
|
350
|
+
coreNote += `- Per-domain skill sub-directories (e.g. \`10.backend-crud/scaffold-order-feature/\`) — those belong to 3c-1, 3c-2, ... batch stages.\n`;
|
|
351
|
+
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`;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
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`;
|
|
356
|
+
|
|
357
|
+
const headerFile = stageKind === "3b" ? "pass3b-core-header.md" : "pass3c-skills-guide-header.md";
|
|
358
|
+
const baseprompt = buildStagePrompt(headerFile, true);
|
|
359
|
+
return baseprompt.replace(/\n## Scope of this step/, `\n${coreNote}\n## Scope of this step`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Helper: build the prompt for 3d-aux (the only Pass 3d sub-stage that
|
|
363
|
+
// actually runs now). Master plan aggregation (standard/rules/skills/guide)
|
|
364
|
+
// was removed because master plans are an internal tool backup not consumed
|
|
365
|
+
// by Claude Code at runtime, and aggregating 30+ files in a single session
|
|
366
|
+
// was the primary source of "Prompt is too long" failures on mid-sized
|
|
367
|
+
// projects (observed on an 18-domain production run).
|
|
368
|
+
//
|
|
369
|
+
// The subStage parameter is kept for forward-compat: if a future version
|
|
370
|
+
// reintroduces master plans via Node-side aggregation, this helper can
|
|
371
|
+
// route the extra sub-stages back in.
|
|
372
|
+
function build3dSubPrompt(subStage) {
|
|
373
|
+
const baseprompt = buildStagePrompt("pass3d-plan-aux-header.md", true);
|
|
374
|
+
|
|
375
|
+
if (subStage !== "aux") {
|
|
376
|
+
throw new InitError(
|
|
377
|
+
`build3dSubPrompt called with unsupported subStage "${subStage}". ` +
|
|
378
|
+
`Master plan sub-stages (standard/rules/skills/guide) were removed in this version. ` +
|
|
379
|
+
`Only "aux" is supported.`
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
let scopeNote = `## 3d sub-stage: aux\n\n`;
|
|
384
|
+
scopeNote += `Pass 3d now produces only auxiliary documentation (database + mcp-guide). Master plan aggregation (standard/rules/skills/guide → plan/*-master.md) was removed because master plans are an internal tool backup not consumed by Claude Code at runtime, and aggregating many files in a single session was the primary cause of "Prompt is too long" failures.\n`;
|
|
385
|
+
scopeNote += `**You are processing sub-stage \`3d-aux\`.**\n\n`;
|
|
386
|
+
|
|
387
|
+
scopeNote += `### Scope of 3d-aux (exclusively)\n\n`;
|
|
388
|
+
scopeNote += `Generate ONLY auxiliary documentation:\n`;
|
|
389
|
+
scopeNote += `- \`claudeos-core/database/\` — schema docs and SQL reference. If pass3a-facts.md shows no database was detected, write a single \`README.md\` stub explaining this and stop.\n`;
|
|
390
|
+
scopeNote += `- \`claudeos-core/mcp-guide/\` — MCP integration guide. If no relevant MCP servers apply, write \`README.md\` stub and stop.\n\n`;
|
|
391
|
+
scopeNote += `**DO NOT touch \`claudeos-core/plan/\`** — master plans are no longer generated in this version. If the \`plan/\` directory exists from a previous run, leave it untouched (Rule B).\n\n`;
|
|
392
|
+
scopeNote += `Rule B applies. Absence of database/ or mcp-guide/ is warning-level, so README stubs are acceptable.\n`;
|
|
393
|
+
|
|
394
|
+
return baseprompt.replace(/\n## Scope of this step/, `\n${scopeNote}\n## Scope of this step`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
// Helper: run a single stage with progress ticker, staged-rules move,
|
|
399
|
+
// and (optionally) per-stage output validation.
|
|
400
|
+
async function runStage(stageId, label, promptStr, opts = {}) {
|
|
401
|
+
if (completedGroups.includes(stageId)) {
|
|
402
|
+
log(` ⏭️ ${stageId} (${label}) already complete, skipping`);
|
|
403
|
+
return { skipped: true };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Clear stale staging before each stage. 3a doesn't write rules so
|
|
407
|
+
// it's a no-op there; 3b/3c/3d may write staged rules.
|
|
408
|
+
if (fileExists(stagingDir)) {
|
|
409
|
+
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
log("");
|
|
413
|
+
log(` 🚀 ${stageId} — ${label}`);
|
|
414
|
+
const t0 = Date.now();
|
|
415
|
+
const ticker = makePassTicker(`Pass ${stageId}`, t0, { baselineCount: countFiles() });
|
|
416
|
+
const ok = await runClaudePromptAsync(promptStr, {
|
|
417
|
+
onTick: ticker.onTick,
|
|
418
|
+
tickMs: ticker.tickMs,
|
|
419
|
+
});
|
|
420
|
+
ticker.clearLine();
|
|
421
|
+
const elapsed = Date.now() - t0;
|
|
422
|
+
stepTimes.push(elapsed);
|
|
423
|
+
|
|
424
|
+
if (!ok) {
|
|
425
|
+
throw new InitError(
|
|
426
|
+
`Pass ${stageId} (${label}) failed. Check the claude error output above.\n` +
|
|
427
|
+
` Already-completed stages are preserved; re-running will resume from ${stageId}.\n` +
|
|
428
|
+
` If this persists, try: npx claudeos-core init --force`
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Move staged rules after stages that generate rule files (3b, 3c, 3d).
|
|
433
|
+
// Pass 3a only writes pass3a-facts.md, so no staging to move.
|
|
434
|
+
if (opts.expectsStagedRules) {
|
|
435
|
+
const move = moveStagedRules(PROJECT_ROOT);
|
|
436
|
+
if (move.failed > 0) {
|
|
437
|
+
log(` ⚠️ ${stageId} staged-rules: ${move.moved} moved, ${move.failed} failed`);
|
|
438
|
+
for (const err of move.errors) log(` • ${err}`);
|
|
439
|
+
throw new InitError(
|
|
440
|
+
`Pass ${stageId} finished but ${move.failed} rule file(s) could not be moved from staging.\n` +
|
|
441
|
+
` This is usually a transient file-lock issue. Re-run \`npx claudeos-core init\`.`
|
|
442
|
+
);
|
|
443
|
+
} else if (move.moved > 0) {
|
|
444
|
+
log(` 📦 ${stageId} staged-rules: ${move.moved} rule files moved to .claude/rules/`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Per-stage output validation (delegates to opts.validate callback).
|
|
449
|
+
if (typeof opts.validate === "function") {
|
|
450
|
+
const problems = opts.validate();
|
|
451
|
+
if (problems && problems.length > 0) {
|
|
452
|
+
const preview = problems.slice(0, 5).map(p => ` • ${p}`).join("\n");
|
|
453
|
+
const more = problems.length > 5 ? `\n • ... and ${problems.length - 5} more` : "";
|
|
454
|
+
throw new InitError(
|
|
455
|
+
`Pass ${stageId} (${label}) produced incomplete output:\n` +
|
|
456
|
+
preview + more + "\n" +
|
|
457
|
+
` Already-completed stages are preserved. Re-run to retry from ${stageId}.`
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Mark stage complete and persist marker so a crash in a later stage
|
|
463
|
+
// doesn't lose this stage's progress.
|
|
464
|
+
completedGroups.push(stageId);
|
|
465
|
+
const persisted = persistMarker(false);
|
|
466
|
+
if (!persisted) {
|
|
467
|
+
throw new InitError(
|
|
468
|
+
`Pass ${stageId} succeeded but failed to persist progress to pass3-complete.json.\n` +
|
|
469
|
+
` Check disk space / permissions on ${GENERATED_DIR}/.`
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
log(` ✅ ${stageId} complete (${formatElapsed(elapsed)})`);
|
|
474
|
+
return { skipped: false, elapsed };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ═══ Stage 3a: Extract facts ═══════════════════════════════════
|
|
478
|
+
const factsFile = path.join(GENERATED_DIR, "pass3a-facts.md");
|
|
479
|
+
await runStage("3a", "fact extraction", buildStagePrompt("pass3a-facts.md", false), {
|
|
480
|
+
expectsStagedRules: false,
|
|
481
|
+
validate: () => {
|
|
482
|
+
const problems = [];
|
|
483
|
+
if (!fileExists(factsFile)) {
|
|
484
|
+
problems.push("pass3a-facts.md was not created");
|
|
485
|
+
} else {
|
|
486
|
+
const content = readFileSafe(factsFile, "");
|
|
487
|
+
const stripped = content.replace(/^\uFEFF/, "").trim();
|
|
488
|
+
if (stripped.length < 500) {
|
|
489
|
+
problems.push(`pass3a-facts.md too short (${stripped.length} chars, expected >= 500)`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return problems;
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// ═══ Stage 3b: CLAUDE.md + standard/ + .claude/rules/ ═══════════
|
|
497
|
+
//
|
|
498
|
+
// Single batch (domains ≤ 15): keep legacy "3b" marker (backward-compatible).
|
|
499
|
+
// Multi-batch (domains > 15): run "3b-core" first, then "3b-1", "3b-2", ...
|
|
500
|
+
//
|
|
501
|
+
// Rationale for splitting 3b-core: if the first multi-batch stage handled
|
|
502
|
+
// "CLAUDE.md + common standards + 15 domains" in a single session, that
|
|
503
|
+
// single stage would hit ~70-80 files — close to 2x the observed overflow
|
|
504
|
+
// threshold (~40 files). Splitting the common files into their own stage
|
|
505
|
+
// keeps every stage under ~50 files.
|
|
506
|
+
if (isBatched) {
|
|
507
|
+
await runStage("3b-core", "core common files (CLAUDE.md + common standard + common rules)",
|
|
508
|
+
buildStageCorePrompt("3b", batches),
|
|
509
|
+
{
|
|
510
|
+
expectsStagedRules: true,
|
|
511
|
+
validate: () => {
|
|
512
|
+
const problems = [];
|
|
513
|
+
if (!fileExists(claudeMdPath)) {
|
|
514
|
+
problems.push("CLAUDE.md was not created");
|
|
515
|
+
}
|
|
516
|
+
const stdSentinel = path.join(PROJECT_ROOT, "claudeos-core/standard/00.core/01.project-overview.md");
|
|
517
|
+
if (!fileExists(stdSentinel)) {
|
|
518
|
+
problems.push("claudeos-core/standard/00.core/01.project-overview.md missing");
|
|
519
|
+
} else {
|
|
520
|
+
const c = readFileSafe(stdSentinel, "");
|
|
521
|
+
if (c.replace(/^\uFEFF/, "").trim().length === 0) {
|
|
522
|
+
problems.push("claudeos-core/standard/00.core/01.project-overview.md is empty");
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
// 3b-core generates only common rules — rules-count validation happens in 3b-N.
|
|
526
|
+
return problems;
|
|
527
|
+
},
|
|
528
|
+
}
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Per-domain batch loop.
|
|
533
|
+
// Single batch: stageId "3b", common files included (legacy behavior).
|
|
534
|
+
// Multi-batch: stageId "3b-1", "3b-2", ..., common files already handled in 3b-core.
|
|
535
|
+
for (let bi = 0; bi < batches.length; bi++) {
|
|
536
|
+
const batchDomains = batches[bi];
|
|
537
|
+
const stageId = isBatched ? `3b-${bi + 1}` : "3b";
|
|
538
|
+
const label = isBatched
|
|
539
|
+
? `domain batch ${bi + 1}/${batches.length} (${batchDomains.length} domains)`
|
|
540
|
+
: "core files (CLAUDE.md + standard + rules)";
|
|
541
|
+
|
|
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
|
+
: "";
|
|
547
|
+
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;
|
|
551
|
+
|
|
552
|
+
await runStage(stageId, label, promptWithScope, {
|
|
553
|
+
expectsStagedRules: true,
|
|
554
|
+
validate: () => {
|
|
555
|
+
const problems = [];
|
|
556
|
+
// Single batch: validate common files here (no 3b-core exists).
|
|
557
|
+
if (!isBatched) {
|
|
558
|
+
if (!fileExists(claudeMdPath)) {
|
|
559
|
+
problems.push("CLAUDE.md was not created");
|
|
560
|
+
}
|
|
561
|
+
const stdSentinel = path.join(PROJECT_ROOT, "claudeos-core/standard/00.core/01.project-overview.md");
|
|
562
|
+
if (!fileExists(stdSentinel)) {
|
|
563
|
+
problems.push("claudeos-core/standard/00.core/01.project-overview.md missing");
|
|
564
|
+
} else {
|
|
565
|
+
const c = readFileSafe(stdSentinel, "");
|
|
566
|
+
if (c.replace(/^\uFEFF/, "").trim().length === 0) {
|
|
567
|
+
problems.push("claudeos-core/standard/00.core/01.project-overview.md is empty");
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
// For every batch, confirm rules/ was generated (at least one staged-rules move must succeed).
|
|
572
|
+
const rulesDir = path.join(PROJECT_ROOT, ".claude/rules");
|
|
573
|
+
const rulesCount = countFilesRecursive(rulesDir);
|
|
574
|
+
if (rulesCount === 0) {
|
|
575
|
+
problems.push(".claude/rules/ has 0 files (staging-override may have been ignored)");
|
|
576
|
+
}
|
|
577
|
+
return problems;
|
|
578
|
+
},
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// ═══ Stage 3c: skills/ + guide/ ═══════════════════════════════
|
|
583
|
+
//
|
|
584
|
+
// Single batch (domains ≤ 15): keep legacy "3c" marker (guide + skills together).
|
|
585
|
+
// Multi-batch (domains > 15): run "3c-core" first, then "3c-1", "3c-2", ...
|
|
586
|
+
//
|
|
587
|
+
// Rationale for splitting 3c-core: the 9 guide files + common skills are fixed regardless of domain count.
|
|
588
|
+
// Mixing them into domain batches makes the first batch heavier than the others.
|
|
589
|
+
if (isBatched) {
|
|
590
|
+
await runStage("3c-core", "common guides and shared skills",
|
|
591
|
+
buildStageCorePrompt("3c", batches),
|
|
592
|
+
{
|
|
593
|
+
expectsStagedRules: true, // shared skills occasionally include rule files
|
|
594
|
+
validate: () => {
|
|
595
|
+
const problems = [];
|
|
596
|
+
const guideDir = path.join(PROJECT_ROOT, "claudeos-core/guide");
|
|
597
|
+
const missingGuides = EXPECTED_GUIDE_FILES.filter(g => {
|
|
598
|
+
const fp = path.join(guideDir, g);
|
|
599
|
+
if (!fileExists(fp)) return true;
|
|
600
|
+
try {
|
|
601
|
+
return fs.readFileSync(fp, "utf-8").replace(/^\uFEFF/, "").trim().length === 0;
|
|
602
|
+
} catch (_e) { return true; }
|
|
603
|
+
});
|
|
604
|
+
for (const g of missingGuides) {
|
|
605
|
+
problems.push(`claudeos-core/guide/${g} missing or empty`);
|
|
606
|
+
}
|
|
607
|
+
// Final skills/ validation is performed in the last domain batch.
|
|
608
|
+
return problems;
|
|
609
|
+
},
|
|
610
|
+
}
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Per-domain skills batch loop.
|
|
615
|
+
// Single batch: guide + skills together (legacy behavior).
|
|
616
|
+
// Multi-batch: skills only; guide was already handled in 3c-core.
|
|
617
|
+
for (let bi = 0; bi < batches.length; bi++) {
|
|
618
|
+
const batchDomains = batches[bi];
|
|
619
|
+
const stageId = isBatched ? `3c-${bi + 1}` : "3c";
|
|
620
|
+
const label = isBatched
|
|
621
|
+
? `domain skills batch ${bi + 1}/${batches.length} (${batchDomains.length} domains)`
|
|
622
|
+
: "skills and guides";
|
|
623
|
+
|
|
624
|
+
const batchScopeNote = isBatched
|
|
625
|
+
? buildBatchScopeNote("3c", bi, batches.length, batchDomains)
|
|
626
|
+
: "";
|
|
627
|
+
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;
|
|
631
|
+
|
|
632
|
+
await runStage(stageId, label, promptWithScope, {
|
|
633
|
+
expectsStagedRules: true, // skills occasionally include rule files
|
|
634
|
+
validate: () => {
|
|
635
|
+
const problems = [];
|
|
636
|
+
// Single batch: validate guide here as well (no 3c-core).
|
|
637
|
+
if (!isBatched) {
|
|
638
|
+
const guideDir = path.join(PROJECT_ROOT, "claudeos-core/guide");
|
|
639
|
+
const missingGuides = EXPECTED_GUIDE_FILES.filter(g => {
|
|
640
|
+
const fp = path.join(guideDir, g);
|
|
641
|
+
if (!fileExists(fp)) return true;
|
|
642
|
+
try {
|
|
643
|
+
return fs.readFileSync(fp, "utf-8").replace(/^\uFEFF/, "").trim().length === 0;
|
|
644
|
+
} catch (_e) { return true; }
|
|
645
|
+
});
|
|
646
|
+
for (const g of missingGuides) {
|
|
647
|
+
problems.push(`claudeos-core/guide/${g} missing or empty`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
// Final skills validation: full check only in the single-batch case or the last multi-batch.
|
|
651
|
+
if (!isBatched || bi === batches.length - 1) {
|
|
652
|
+
const { hasNonEmptyMdRecursive } = require("../../lib/expected-outputs");
|
|
653
|
+
const skillsDir = path.join(PROJECT_ROOT, "claudeos-core/skills");
|
|
654
|
+
if (!fileExists(skillsDir) || !hasNonEmptyMdRecursive(skillsDir)) {
|
|
655
|
+
problems.push("claudeos-core/skills/ has no non-empty .md files");
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return problems;
|
|
659
|
+
},
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// ═══ Stage 3d: plan/ + database/ + mcp-guide/ ═════════════════
|
|
664
|
+
//
|
|
665
|
+
// 3d used to aggregate standard/rules/skills/guide into a master plan
|
|
666
|
+
// and generate database/mcp-guide stubs. But the master plan itself was
|
|
667
|
+
// an internal backup/management file never loaded by Claude Code at runtime,
|
|
668
|
+
// and at high domain counts the single-session aggregation caused "Prompt is
|
|
669
|
+
// too long" failures (at 18 domains, 3d-standard failed at the 32-file mark).
|
|
670
|
+
// Dropping master plan generation is the correct call for stability, and
|
|
671
|
+
// users can aggregate manually with their own script when needed.
|
|
672
|
+
//
|
|
673
|
+
// As a result, 3d only keeps the aux stage:
|
|
674
|
+
// 3d-aux → database/ + mcp-guide/ (project-specific stub descriptions)
|
|
675
|
+
|
|
676
|
+
// 3d-aux: database/ + mcp-guide/ (absence is warning-level)
|
|
677
|
+
await runStage("3d-aux", "aux docs (database + mcp-guide)",
|
|
678
|
+
build3dSubPrompt("aux"),
|
|
679
|
+
{
|
|
680
|
+
expectsStagedRules: false,
|
|
681
|
+
validate: () => [],
|
|
682
|
+
}
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
// ─── Final marker: all 4 stages done ─────────────────────────
|
|
686
|
+
const finalPersist = persistMarker(true);
|
|
687
|
+
if (!finalPersist) {
|
|
688
|
+
throw new InitError(
|
|
689
|
+
"Pass 3 split all stages complete but failed to write final marker.\n" +
|
|
690
|
+
` Check disk space / permissions on ${GENERATED_DIR}/.`
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
// Total stage count
|
|
694
|
+
// 3a (1) + 3b (1 single or core+N) + 3c (1 single or core+N) + 3d-aux (1)
|
|
695
|
+
// Single batch: 1 + 1 + 1 + 1 = 4
|
|
696
|
+
// Multi-batch: 1 + (1 + N) + (1 + N) + 1 = 2N + 4
|
|
697
|
+
const three3dStages = 1; // aux only (master plan aggregation removed)
|
|
698
|
+
const totalStages = isBatched
|
|
699
|
+
? (1 + 1 + batches.length + 1 + batches.length + three3dStages)
|
|
700
|
+
: (1 + 1 + 1 + three3dStages);
|
|
701
|
+
log(` 🎉 Pass 3 split complete: ${completedGroups.length}/${totalStages} stages successful`);
|
|
702
|
+
}
|
|
703
|
+
|
|
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 ───────────────────────────────────
|
|
713
|
+
const hasProjectMarker = [".git", "package.json", "build.gradle", "build.gradle.kts", "pom.xml", "pyproject.toml", "requirements.txt"].some(
|
|
714
|
+
m => fs.existsSync(path.join(PROJECT_ROOT, m))
|
|
715
|
+
);
|
|
716
|
+
if (!hasProjectMarker) {
|
|
717
|
+
log(`\n ⚠️ Warning: ${PROJECT_ROOT} does not look like a project root.`);
|
|
718
|
+
log(" No .git, package.json, build.gradle, or pom.xml found.");
|
|
719
|
+
log(" Run this command from your project directory.\n");
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const nodeVersion = parseInt(process.versions.node.split(".")[0]);
|
|
723
|
+
if (nodeVersion < 18) {
|
|
724
|
+
throw new InitError(`Node.js v18+ required (current: v${process.versions.node})\n Install: https://nodejs.org/`);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const claudeExists = run("claude --version", { silent: true, ignoreError: true });
|
|
728
|
+
if (!claudeExists) {
|
|
729
|
+
throw new InitError("Claude Code CLI not found.\n Install: https://code.claude.com/docs/en/overview\n Then run: claude (and complete authentication)");
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Verify Claude is authenticated (quick prompt test)
|
|
733
|
+
const claudeAuth = run('claude -p "echo ok"', { silent: true, ignoreError: true });
|
|
734
|
+
if (!claudeAuth) {
|
|
735
|
+
throw new InitError("Claude Code may not be authenticated.\n Run: claude (and complete authentication)\n Then retry: npx claudeos-core init");
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// ─── Language selection (required) ────────────────────────────
|
|
739
|
+
let lang = parsedArgs.lang;
|
|
740
|
+
if (!lang) {
|
|
741
|
+
lang = await selectLangInteractive();
|
|
742
|
+
}
|
|
743
|
+
if (!lang) {
|
|
744
|
+
throw new InitError("Cancelled.");
|
|
745
|
+
}
|
|
746
|
+
if (!isValidLang(lang)) {
|
|
747
|
+
throw new InitError(`Unsupported language: "${lang}"\n Supported: ${LANG_CODES.join(", ")}`);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Early incompatibility check: CLAUDEOS_SKIP_TRANSLATION is a test-only
|
|
751
|
+
// env var that short-circuits lib/memory-scaffold.js translation path.
|
|
752
|
+
// If set AND the user chose a non-English language, Pass 4's static fallback
|
|
753
|
+
// and gap-fill would throw mid-run with a confusing "translation skipped"
|
|
754
|
+
// error. Fail fast here with a clear message so the user can unset the var
|
|
755
|
+
// or pick --lang en before the pipeline starts.
|
|
756
|
+
if (process.env.CLAUDEOS_SKIP_TRANSLATION === "1" && lang !== "en") {
|
|
757
|
+
throw new InitError(
|
|
758
|
+
`CLAUDEOS_SKIP_TRANSLATION=1 is set but --lang='${lang}' requires translation.\n` +
|
|
759
|
+
` This env var is a test-only escape hatch that blocks calls to \`claude -p\`\n` +
|
|
760
|
+
` from lib/memory-scaffold.js. Pass 4 would crash later with a hard-to-\n` +
|
|
761
|
+
` diagnose error.\n\n` +
|
|
762
|
+
` Either unset the env var: unset CLAUDEOS_SKIP_TRANSLATION\n` +
|
|
763
|
+
` Or run with English output: npx claudeos-core init --lang en`
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
process.env.CLAUDEOS_LANG = lang;
|
|
768
|
+
|
|
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
|
+
}
|
|
829
|
+
|
|
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
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
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("");
|
|
866
|
+
|
|
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 });
|
|
871
|
+
}
|
|
872
|
+
log(" ✅ Done\n");
|
|
873
|
+
|
|
874
|
+
// ─── [2] Create directory structure ─────────────────────────
|
|
875
|
+
header("[2] Creating directory structure...");
|
|
876
|
+
const dirs = [
|
|
877
|
+
".claude/rules/00.core",
|
|
878
|
+
".claude/rules/10.backend",
|
|
879
|
+
".claude/rules/20.frontend",
|
|
880
|
+
".claude/rules/30.security-db",
|
|
881
|
+
".claude/rules/40.infra",
|
|
882
|
+
".claude/rules/50.sync",
|
|
883
|
+
"claudeos-core/generated",
|
|
884
|
+
"claudeos-core/standard/00.core",
|
|
885
|
+
"claudeos-core/standard/10.backend-api",
|
|
886
|
+
"claudeos-core/standard/20.frontend-ui",
|
|
887
|
+
"claudeos-core/standard/30.security-db",
|
|
888
|
+
"claudeos-core/standard/40.infra",
|
|
889
|
+
"claudeos-core/standard/50.verification",
|
|
890
|
+
"claudeos-core/standard/90.optional",
|
|
891
|
+
"claudeos-core/skills/00.shared",
|
|
892
|
+
"claudeos-core/skills/10.backend-crud/scaffold-crud-feature",
|
|
893
|
+
"claudeos-core/skills/20.frontend-page/scaffold-page-feature",
|
|
894
|
+
"claudeos-core/skills/50.testing",
|
|
895
|
+
"claudeos-core/skills/90.experimental",
|
|
896
|
+
"claudeos-core/guide/01.onboarding",
|
|
897
|
+
"claudeos-core/guide/02.usage",
|
|
898
|
+
"claudeos-core/guide/03.troubleshooting",
|
|
899
|
+
"claudeos-core/guide/04.architecture",
|
|
900
|
+
"claudeos-core/database",
|
|
901
|
+
"claudeos-core/mcp-guide",
|
|
902
|
+
"claudeos-core/memory",
|
|
903
|
+
".claude/rules/60.memory",
|
|
904
|
+
];
|
|
905
|
+
for (const d of dirs) {
|
|
906
|
+
ensureDir(path.join(PROJECT_ROOT, d));
|
|
907
|
+
}
|
|
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...");
|
|
917
|
+
|
|
918
|
+
let domainGroups;
|
|
919
|
+
try {
|
|
920
|
+
domainGroups = JSON.parse(
|
|
921
|
+
readFile(path.join(GENERATED_DIR, "domain-groups.json"))
|
|
922
|
+
);
|
|
923
|
+
} catch (e) {
|
|
924
|
+
throw new InitError(`domain-groups.json is missing or malformed: ${e.message}\n Re-run plan-installer or check claudeos-core/generated/`);
|
|
925
|
+
}
|
|
926
|
+
const totalGroups = domainGroups.totalGroups;
|
|
927
|
+
if (!totalGroups || typeof totalGroups !== "number" || totalGroups < 1) {
|
|
928
|
+
throw new InitError(`domain-groups.json has invalid totalGroups: ${totalGroups}\n Re-run plan-installer or check claudeos-core/generated/`);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Load pass1 prompts by type
|
|
932
|
+
const pass1Prompts = {};
|
|
933
|
+
for (const type of ["backend", "frontend"]) {
|
|
934
|
+
const promptFile = path.join(GENERATED_DIR, `pass1-${type}-prompt.md`);
|
|
935
|
+
if (fileExists(promptFile)) {
|
|
936
|
+
pass1Prompts[type] = readFile(promptFile);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
// Single-stack backward compatibility
|
|
940
|
+
if (Object.keys(pass1Prompts).length === 0) {
|
|
941
|
+
const fallback = path.join(GENERATED_DIR, "pass1-prompt.md");
|
|
942
|
+
if (fileExists(fallback)) pass1Prompts["backend"] = readFile(fallback);
|
|
943
|
+
}
|
|
944
|
+
|
|
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) {
|
|
956
|
+
const pct = Math.round((step / totalSteps) * 100);
|
|
957
|
+
const elapsed = Date.now() - passStart;
|
|
958
|
+
let eta = "";
|
|
959
|
+
if (stepTimes.length > 0) {
|
|
960
|
+
const avgMs = stepTimes.reduce((a, b) => a + b, 0) / stepTimes.length;
|
|
961
|
+
const remaining = (totalSteps - step) * avgMs;
|
|
962
|
+
eta = ` | ETA ${formatElapsed(remaining)}`;
|
|
963
|
+
}
|
|
964
|
+
const filled = Math.round(pct / 5);
|
|
965
|
+
const bar = "█".repeat(filled) + "░".repeat(20 - filled);
|
|
966
|
+
log(` [${bar}] ${pct}% (${step}/${totalSteps}) ${formatElapsed(elapsed)}${eta} — ${label}`);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
for (let i = 1; i <= totalGroups; i++) {
|
|
970
|
+
const group = domainGroups.groups[i - 1];
|
|
971
|
+
const domainList = (group.domains || []).join(", ") || "(unknown)";
|
|
972
|
+
const estFiles = group.estimatedFiles || 0;
|
|
973
|
+
const groupType = group.type || "backend";
|
|
974
|
+
const icon = groupType === "frontend" ? "🎨" : "⚙️";
|
|
975
|
+
|
|
976
|
+
log("");
|
|
977
|
+
log(
|
|
978
|
+
` ${icon} [Pass 1-${i}/${totalGroups}] ${groupType}: ${domainList} (~${estFiles} files)`
|
|
979
|
+
);
|
|
980
|
+
|
|
981
|
+
const pass1Json = path.join(GENERATED_DIR, `pass1-${i}.json`);
|
|
982
|
+
if (fileExists(pass1Json)) {
|
|
983
|
+
try {
|
|
984
|
+
const existing = JSON.parse(readFile(pass1Json));
|
|
985
|
+
if (existing && existing.analysisPerDomain) {
|
|
986
|
+
log(` ⏭️ pass1-${i}.json already exists, skipping`);
|
|
987
|
+
completedSteps++;
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
} catch (_e) { /* malformed — re-run */ }
|
|
991
|
+
log(` ⚠️ pass1-${i}.json exists but is malformed, re-running`);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// Select prompt for this type
|
|
995
|
+
const template = pass1Prompts[groupType] || pass1Prompts["backend"];
|
|
996
|
+
if (!template) {
|
|
997
|
+
throw new InitError(`No pass1 prompt found for type: ${groupType}`);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Placeholder substitution — use replacement functions (not string form)
|
|
1001
|
+
// so that `$`, `$1`, `$&`, `$$` etc. in domainList are preserved as
|
|
1002
|
+
// literal characters rather than interpreted as regex back-references.
|
|
1003
|
+
// (Same bug class as bug #18 in lib/plan-parser.js replaceFileBlock.)
|
|
1004
|
+
let prompt = template
|
|
1005
|
+
.replace(/\{\{DOMAIN_GROUP\}\}/g, () => domainList)
|
|
1006
|
+
.replace(/\{\{PASS_NUM\}\}/g, () => String(i));
|
|
1007
|
+
prompt = injectProjectRoot(prompt);
|
|
1008
|
+
|
|
1009
|
+
const t1 = Date.now();
|
|
1010
|
+
const ticker1 = makePassTicker(`Pass 1-${i}/${totalGroups}`, t1);
|
|
1011
|
+
const ok = await runClaudePromptAsync(prompt, {
|
|
1012
|
+
onTick: ticker1.onTick,
|
|
1013
|
+
tickMs: ticker1.tickMs,
|
|
1014
|
+
});
|
|
1015
|
+
ticker1.clearLine();
|
|
1016
|
+
const elapsed1 = Date.now() - t1;
|
|
1017
|
+
stepTimes.push(elapsed1);
|
|
1018
|
+
|
|
1019
|
+
if (!ok) {
|
|
1020
|
+
throw new InitError(`Pass 1-${i} failed. Check the claude error output above.\n If this persists, try: npx claudeos-core init --force`);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if (!fileExists(pass1Json)) {
|
|
1024
|
+
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
|
+
}
|
|
1026
|
+
|
|
1027
|
+
completedSteps++;
|
|
1028
|
+
progressBar(completedSteps, `pass1-${i}.json created (${formatElapsed(elapsed1)})`);
|
|
1029
|
+
}
|
|
1030
|
+
log("");
|
|
1031
|
+
|
|
1032
|
+
// ─── [5] Pass 2: Merge analysis results ──────────────────────
|
|
1033
|
+
header("[5] Pass 2 — Merging analysis results...");
|
|
1034
|
+
|
|
1035
|
+
const pass2Json = path.join(GENERATED_DIR, "pass2-merged.json");
|
|
1036
|
+
|
|
1037
|
+
// H3: resume-path structural validation. existsSync alone isn't enough —
|
|
1038
|
+
// a prior crashed run may have left a skeleton (`{}`) or malformed JSON
|
|
1039
|
+
// that passes existsSync but silently poisons Pass 3 (which parses this
|
|
1040
|
+
// file as its analysis input). Mirrors pass1's malformed-detection at
|
|
1041
|
+
// the pass1 loop above, and the "<5 top-level keys = INSUFFICIENT_KEYS"
|
|
1042
|
+
// threshold from pass-json-validator/index.js (ERROR level).
|
|
1043
|
+
let pass2IsValid = false;
|
|
1044
|
+
if (fileExists(pass2Json)) {
|
|
1045
|
+
try {
|
|
1046
|
+
const existing = JSON.parse(readFile(pass2Json));
|
|
1047
|
+
if (existing && typeof existing === "object" && !Array.isArray(existing)
|
|
1048
|
+
&& Object.keys(existing).length >= 5) {
|
|
1049
|
+
pass2IsValid = true;
|
|
1050
|
+
}
|
|
1051
|
+
} catch (_e) { /* malformed — fall through to re-run */ }
|
|
1052
|
+
if (!pass2IsValid) {
|
|
1053
|
+
log(" ⚠️ pass2-merged.json exists but is malformed or incomplete (<5 top-level keys), re-running");
|
|
1054
|
+
try { fs.unlinkSync(pass2Json); } catch (_e) { /* best-effort cleanup */ }
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
if (pass2IsValid) {
|
|
1059
|
+
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));
|
|
1067
|
+
|
|
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);
|
|
1077
|
+
|
|
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
|
+
}
|
|
1081
|
+
|
|
1082
|
+
if (!fileExists(pass2Json)) {
|
|
1083
|
+
throw new InitError("pass2-merged.json was not created. Claude may have run but not produced expected output.");
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
completedSteps++;
|
|
1087
|
+
progressBar(completedSteps, `pass2-merged.json created (${formatElapsed(elapsed2)})`);
|
|
1088
|
+
}
|
|
1089
|
+
log("");
|
|
1090
|
+
|
|
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.
|
|
1100
|
+
try {
|
|
1101
|
+
const { buildPass3Context } = require("../../plan-installer/pass3-context-builder");
|
|
1102
|
+
const pass3Ctx = buildPass3Context(GENERATED_DIR);
|
|
1103
|
+
if (pass3Ctx) {
|
|
1104
|
+
const { writeFileSafe: wfsCtx } = require("../../lib/safe-fs");
|
|
1105
|
+
const ctxPath = path.join(GENERATED_DIR, "pass3-context.json");
|
|
1106
|
+
const wrote = wfsCtx(ctxPath, JSON.stringify(pass3Ctx, null, 2));
|
|
1107
|
+
if (wrote) {
|
|
1108
|
+
const sizeKB = Math.round(JSON.stringify(pass3Ctx).length / 1024);
|
|
1109
|
+
const p2KB = pass3Ctx.pass2Merged.sizeKB;
|
|
1110
|
+
log(` 📄 pass3-context.json built (${sizeKB} KB summary of ${p2KB} KB pass2-merged.json)`);
|
|
1111
|
+
if (pass3Ctx.pass2Merged.large) {
|
|
1112
|
+
log(` ⚠️ pass2-merged.json is large (${p2KB} KB). Pass 3 will rely heavily on pass3-context.json to avoid context overflow.`);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
} catch (e) {
|
|
1117
|
+
log(` ⚠️ pass3-context.json build skipped: ${e.message} (Pass 3 will fall back to pass2-merged.json)`);
|
|
1118
|
+
}
|
|
1119
|
+
log("");
|
|
1120
|
+
|
|
1121
|
+
// ─── [6] Pass 3: Generate + verify ─────────────────────────
|
|
1122
|
+
header("[6] Pass 3 — Generating all files...");
|
|
1123
|
+
|
|
1124
|
+
const pass3Marker = path.join(GENERATED_DIR, "pass3-complete.json");
|
|
1125
|
+
const claudeMdPath = path.join(PROJECT_ROOT, "CLAUDE.md");
|
|
1126
|
+
|
|
1127
|
+
// v1.7.1 → v1.7.2 migration: if CLAUDE.md exists from prior version but marker
|
|
1128
|
+
// is missing, backfill marker to preserve user's existing output.
|
|
1129
|
+
//
|
|
1130
|
+
// Gated by !wasFreshClean: --force and "fresh" resume mode wipe the marker
|
|
1131
|
+
// on purpose to force Pass 3 to re-run. They do NOT delete CLAUDE.md (user
|
|
1132
|
+
// may have manual edits worth preserving in the continue flow), so without
|
|
1133
|
+
// this gate the backfill would fire on a fresh run, re-write the marker,
|
|
1134
|
+
// and Pass 3 would skip — leaving stale CLAUDE.md + regenerated pass1/2 +
|
|
1135
|
+
// wiped rules/, which fails sync-checker and content-validator.
|
|
1136
|
+
if (!wasFreshClean && fileExists(claudeMdPath) && !fileExists(pass3Marker) && fileExists(path.join(GENERATED_DIR, "pass2-merged.json"))) {
|
|
1137
|
+
const { writeFileSafe: wfsMig } = require("../../lib/safe-fs");
|
|
1138
|
+
const backfillOk = wfsMig(pass3Marker, JSON.stringify({
|
|
1139
|
+
completedAt: new Date().toISOString(),
|
|
1140
|
+
backfilled: true,
|
|
1141
|
+
reason: "CLAUDE.md exists from prior version; marker backfilled to prevent regeneration. To force re-run, use --force or pick 'fresh' in the resume prompt.",
|
|
1142
|
+
}, null, 2));
|
|
1143
|
+
if (backfillOk) {
|
|
1144
|
+
log(" ℹ️ Detected existing CLAUDE.md — Pass 3 marker backfilled (use --force to regenerate)");
|
|
1145
|
+
} else {
|
|
1146
|
+
log(" ⚠️ CLAUDE.md exists but marker backfill failed — Pass 3 will re-run");
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// Stale marker detection (Pass 3). Drops the marker and re-runs Pass 3 if
|
|
1151
|
+
// any of the following is true:
|
|
1152
|
+
// (a) CLAUDE.md was deleted externally (original check),
|
|
1153
|
+
// (b) any of EXPECTED_GUIDE_FILES is missing or BOM-aware empty,
|
|
1154
|
+
// (c) any entry in expected-outputs (standard sentinel / skills / plan) is
|
|
1155
|
+
// missing or empty.
|
|
1156
|
+
//
|
|
1157
|
+
// (b) and (c) were previously only enforced as Pass 3 post-generation Guards
|
|
1158
|
+
// 3 (H2/H1), which gate marker *creation* on fresh runs. Projects that were
|
|
1159
|
+
// initialized on a pre-v2.0.0 release (before those guards existed) can end
|
|
1160
|
+
// up with a marker on disk even though guide/ or standard/skills/plan are
|
|
1161
|
+
// empty. Without this stale check, such projects hit a permanent
|
|
1162
|
+
// content-validator fail loop because init sees the marker and skips Pass 3
|
|
1163
|
+
// forever. This mirrors the Pass 4 dropStalePass4Marker pattern below.
|
|
1164
|
+
//
|
|
1165
|
+
// Unlink is surfaced as InitError on failure (symmetric with Pass 4).
|
|
1166
|
+
// Silently ignoring the error would leave the stale marker in place, and
|
|
1167
|
+
// the `if (fileExists(pass3Marker))` check below would accept it — skipping
|
|
1168
|
+
// Pass 3 while outputs are still incomplete. That silent-skip is the exact
|
|
1169
|
+
// bug class this audit round closes.
|
|
1170
|
+
function dropStalePass3Marker(reasonLog) {
|
|
1171
|
+
log(reasonLog);
|
|
1172
|
+
try { fs.unlinkSync(pass3Marker); } catch (e) {
|
|
1173
|
+
log(` ❌ Failed to delete stale pass3-complete.json: ${e.code || e.message}`);
|
|
1174
|
+
throw new InitError(
|
|
1175
|
+
`Could not delete stale pass3-complete.json at:\n ${pass3Marker}\n` +
|
|
1176
|
+
` The file is likely locked by another process (Windows antivirus or a file-watcher).\n` +
|
|
1177
|
+
` Close any editor/AV scanner holding the file and re-run \`npx claudeos-core init\`.`
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
if (fileExists(pass3Marker)) {
|
|
1182
|
+
// v2.1.1: protect split-mode partial markers.
|
|
1183
|
+
// An in-progress marker of the form { mode: "split", groupsCompleted: [...], completedAt: undefined }
|
|
1184
|
+
// must NOT be treated as stale. When the pipeline has normally completed
|
|
1185
|
+
// only through 3b, guide/skills/plan are empty, so the legacy check
|
|
1186
|
+
// falsely flagged the marker as stale. Deleting it made runPass3Split
|
|
1187
|
+
// lose track of completed stages and re-run from 3a (correct but wastes ~2x tokens).
|
|
1188
|
+
let markerIsSplitPartial = false;
|
|
1189
|
+
try {
|
|
1190
|
+
const parsed = JSON.parse(readFile(pass3Marker));
|
|
1191
|
+
markerIsSplitPartial =
|
|
1192
|
+
parsed &&
|
|
1193
|
+
parsed.mode === "split" &&
|
|
1194
|
+
Array.isArray(parsed.groupsCompleted) &&
|
|
1195
|
+
!parsed.completedAt;
|
|
1196
|
+
} catch (_e) {
|
|
1197
|
+
// malformed marker → fall through to the stale check (legacy behavior)
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
if (markerIsSplitPartial) {
|
|
1201
|
+
log(` ↪️ split-mode partial marker detected — runPass3Split will resume`);
|
|
1202
|
+
} else if (!fileExists(claudeMdPath)) {
|
|
1203
|
+
dropStalePass3Marker(" ⚠️ pass3-complete.json exists but CLAUDE.md is missing — treating marker as stale, re-running Pass 3");
|
|
1204
|
+
} else {
|
|
1205
|
+
const guideDirForStale = path.join(PROJECT_ROOT, "claudeos-core/guide");
|
|
1206
|
+
const staleMissingGuides = EXPECTED_GUIDE_FILES.filter(g => {
|
|
1207
|
+
const fp = path.join(guideDirForStale, g);
|
|
1208
|
+
if (!fileExists(fp)) return true;
|
|
1209
|
+
try {
|
|
1210
|
+
return fs.readFileSync(fp, "utf-8").replace(/^\uFEFF/, "").trim().length === 0;
|
|
1211
|
+
} catch (_e) { return true; }
|
|
1212
|
+
});
|
|
1213
|
+
if (staleMissingGuides.length > 0) {
|
|
1214
|
+
dropStalePass3Marker(
|
|
1215
|
+
` ⚠️ pass3-complete.json exists but ${staleMissingGuides.length}/${EXPECTED_GUIDE_FILES.length} guide files are missing or empty — treating marker as stale, re-running Pass 3`
|
|
1216
|
+
);
|
|
1217
|
+
} else {
|
|
1218
|
+
const staleMissingOutputs = findMissingOutputs(PROJECT_ROOT);
|
|
1219
|
+
if (staleMissingOutputs.length > 0) {
|
|
1220
|
+
dropStalePass3Marker(
|
|
1221
|
+
` ⚠️ pass3-complete.json exists but ${staleMissingOutputs.length} required output(s) are missing or empty — treating marker as stale, re-running Pass 3`
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// Pass 3 split mode resolution.
|
|
1229
|
+
//
|
|
1230
|
+
// Pass 3 runs as 4 sequential `claude -p` calls (3a facts, 3b core, 3c
|
|
1231
|
+
// skills+guide, 3d-aux database/mcp-guide), each with fresh context.
|
|
1232
|
+
// This eliminates `Prompt is too long` failures by making output
|
|
1233
|
+
// accumulation overflow structurally impossible — each stage starts
|
|
1234
|
+
// with a clean context window. For projects with 16+ domains, 3b and 3c
|
|
1235
|
+
// are further split into core + batched sub-stages.
|
|
1236
|
+
//
|
|
1237
|
+
// Single-call mode (previously toggled by CLAUDEOS_PASS3_SPLIT=0) was
|
|
1238
|
+
// removed because empirical data showed it failed reliably on projects
|
|
1239
|
+
// with more than ~5 domains (output accumulation overflow was not
|
|
1240
|
+
// predictable from input size alone), and split mode is structurally
|
|
1241
|
+
// immune to this failure mode with bounded token overhead.
|
|
1242
|
+
log(` 🚀 Pass 3 split mode (3a → 3b → 3c → 3d-aux)`);
|
|
1243
|
+
try {
|
|
1244
|
+
const ctxPath = path.join(GENERATED_DIR, "pass3-context.json");
|
|
1245
|
+
if (fileExists(ctxPath)) {
|
|
1246
|
+
const ctx = JSON.parse(readFile(ctxPath));
|
|
1247
|
+
const rec = ctx && ctx.splitRecommendation;
|
|
1248
|
+
if (rec) {
|
|
1249
|
+
log(` • estimated ${rec.estimatedFileCount} files from ${rec.totalDomains} domains`);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
} catch (_e) { /* best-effort log only */ }
|
|
1253
|
+
|
|
1254
|
+
// Decide whether Pass 3 needs to run (from scratch, resumed, or skipped).
|
|
1255
|
+
//
|
|
1256
|
+
// Three states of pass3-complete.json:
|
|
1257
|
+
// (a) absent — fresh run; call runPass3Split.
|
|
1258
|
+
// (b) present + mode:"split" + no completedAt — partial marker from a
|
|
1259
|
+
// prior run that was interrupted mid-pipeline (most commonly a
|
|
1260
|
+
// Pass 3d-aux stream timeout). runPass3Split inspects
|
|
1261
|
+
// groupsCompleted and resumes from the next unstarted stage.
|
|
1262
|
+
// MUST call runPass3Split here.
|
|
1263
|
+
// (c) present + completedAt set — finished; skip.
|
|
1264
|
+
//
|
|
1265
|
+
// Pre-v2.3.0 bug: the code branched only on fileExists(pass3Marker),
|
|
1266
|
+
// so case (b) fell into the skip branch and the remaining stages
|
|
1267
|
+
// (usually 3d-aux → database/ + mcp-guide/) were silently dropped on
|
|
1268
|
+
// re-run. The detection block earlier in this function correctly
|
|
1269
|
+
// identified the partial marker and logged "runPass3Split will
|
|
1270
|
+
// resume", but resumption never actually happened.
|
|
1271
|
+
let pass3NeedsRun = !fileExists(pass3Marker);
|
|
1272
|
+
let pass3IsResume = false;
|
|
1273
|
+
if (!pass3NeedsRun) {
|
|
1274
|
+
try {
|
|
1275
|
+
const parsed = JSON.parse(readFile(pass3Marker));
|
|
1276
|
+
if (parsed && parsed.mode === "split" && !parsed.completedAt) {
|
|
1277
|
+
pass3NeedsRun = true;
|
|
1278
|
+
pass3IsResume = true;
|
|
1279
|
+
}
|
|
1280
|
+
} catch (_e) {
|
|
1281
|
+
// Malformed marker — the earlier stale check already handles this
|
|
1282
|
+
// path (dropStalePass3Marker). If we still reach here with a bad
|
|
1283
|
+
// marker, fall through to skip; runPass3Split's own resume logic
|
|
1284
|
+
// would throw on malformed input.
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
if (pass3NeedsRun) {
|
|
1289
|
+
if (pass3IsResume) {
|
|
1290
|
+
log(" ↪️ Resuming Pass 3 split from partial marker");
|
|
1291
|
+
}
|
|
1292
|
+
await runPass3Split({
|
|
1293
|
+
GENERATED_DIR, PROJECT_ROOT, TOOLS_DIR,
|
|
1294
|
+
pass3Marker, claudeMdPath,
|
|
1295
|
+
injectProjectRoot, readFile, fileExists,
|
|
1296
|
+
runClaudePromptAsync, makePassTicker, formatElapsed,
|
|
1297
|
+
log, countFiles,
|
|
1298
|
+
EXPECTED_GUIDE_FILES, findMissingOutputs,
|
|
1299
|
+
lang, stepTimes,
|
|
1300
|
+
});
|
|
1301
|
+
completedSteps++;
|
|
1302
|
+
progressBar(completedSteps, `Pass 3 complete (split mode)`);
|
|
1303
|
+
log("");
|
|
1304
|
+
} else {
|
|
1305
|
+
log(" ⏭️ pass3-complete.json already complete, skipping");
|
|
1306
|
+
completedSteps++;
|
|
1307
|
+
}
|
|
1308
|
+
log("");
|
|
1309
|
+
|
|
1310
|
+
// ─── [7] Pass 4: L4 memory scaffolding ────────────
|
|
1311
|
+
header("[7] Pass 4 — Memory scaffolding...");
|
|
1312
|
+
|
|
1313
|
+
const pass4Marker = path.join(GENERATED_DIR, "pass4-memory.json");
|
|
1314
|
+
const pass4PromptFile = path.join(GENERATED_DIR, "pass4-prompt.md");
|
|
1315
|
+
|
|
1316
|
+
const { scaffoldMemory, scaffoldRules, scaffoldMasterPlans, scaffoldDocWritingGuide, scaffoldSkillsManifest } = require("../../lib/memory-scaffold");
|
|
1317
|
+
const { writeFileSafe } = require("../../lib/safe-fs");
|
|
1318
|
+
|
|
1319
|
+
const memoryPath = path.join(PROJECT_ROOT, "claudeos-core/memory");
|
|
1320
|
+
const planPath = path.join(PROJECT_ROOT, "claudeos-core/plan");
|
|
1321
|
+
const rulesPath = path.join(PROJECT_ROOT, ".claude/rules");
|
|
1322
|
+
const standardCorePath = path.join(PROJECT_ROOT, "claudeos-core/standard/00.core");
|
|
1323
|
+
const skillsSharedPath = path.join(PROJECT_ROOT, "claudeos-core/skills/00.shared");
|
|
1324
|
+
|
|
1325
|
+
function applyStaticFallback() {
|
|
1326
|
+
try {
|
|
1327
|
+
scaffoldMemory(memoryPath, { lang });
|
|
1328
|
+
scaffoldRules(rulesPath, { lang });
|
|
1329
|
+
scaffoldDocWritingGuide(standardCorePath, { lang });
|
|
1330
|
+
scaffoldMasterPlans(planPath, memoryPath, { lang });
|
|
1331
|
+
scaffoldSkillsManifest(skillsSharedPath, { lang });
|
|
1332
|
+
// v2.3.0: do NOT append an L4 memory section to CLAUDE.md. Pass 3
|
|
1333
|
+
// already authored Section 8 "Common Rules & Memory (L4)" covering
|
|
1334
|
+
// all of the content the append function would have added. An
|
|
1335
|
+
// additional `## N. ... (L4)` section would duplicate the memory
|
|
1336
|
+
// file table (one row per file in §8, another row per file in the
|
|
1337
|
+
// appended §9), triggering [S1]/[M-*]/[F2-*] validator errors.
|
|
1338
|
+
// See CHANGELOG v2.3.0 — "Pass 4 CLAUDE.md append retired."
|
|
1339
|
+
} catch (err) {
|
|
1340
|
+
// When lang !== "en", translation is REQUIRED. If it fails, we surface
|
|
1341
|
+
// a clear error rather than silently writing English (which would
|
|
1342
|
+
// contradict the user's --lang choice).
|
|
1343
|
+
throw new InitError(
|
|
1344
|
+
`Static fallback failed while translating to lang='${lang}':\n` +
|
|
1345
|
+
` ${err.message}\n` +
|
|
1346
|
+
` Ensure the \`claude\` CLI is available and authenticated, then re-run.\n` +
|
|
1347
|
+
` Alternatively re-run with --lang en to skip translation.`
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
const markerBody = JSON.stringify({
|
|
1351
|
+
analyzedAt: new Date().toISOString(),
|
|
1352
|
+
passNum: 4,
|
|
1353
|
+
fallback: true,
|
|
1354
|
+
lang,
|
|
1355
|
+
memoryFiles: [
|
|
1356
|
+
"claudeos-core/memory/decision-log.md",
|
|
1357
|
+
"claudeos-core/memory/failure-patterns.md",
|
|
1358
|
+
"claudeos-core/memory/compaction.md",
|
|
1359
|
+
"claudeos-core/memory/auto-rule-update.md",
|
|
1360
|
+
],
|
|
1361
|
+
ruleFiles: [
|
|
1362
|
+
".claude/rules/00.core/51.doc-writing-rules.md",
|
|
1363
|
+
".claude/rules/00.core/52.ai-work-rules.md",
|
|
1364
|
+
".claude/rules/60.memory/01.decision-log.md",
|
|
1365
|
+
".claude/rules/60.memory/02.failure-patterns.md",
|
|
1366
|
+
".claude/rules/60.memory/03.compaction.md",
|
|
1367
|
+
".claude/rules/60.memory/04.auto-rule-update.md",
|
|
1368
|
+
],
|
|
1369
|
+
// Note: master plan files are no longer generated (previously this
|
|
1370
|
+
// included "claudeos-core/plan/50.memory-master.md"). The marker schema
|
|
1371
|
+
// still accepts an optional planFiles field for backward compatibility.
|
|
1372
|
+
claudeMdAppended: true,
|
|
1373
|
+
}, null, 2);
|
|
1374
|
+
return writeFileSafe(pass4Marker, markerBody);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// M1: validate Pass 4 marker CONTENT, not just existence. Claude can emit
|
|
1378
|
+
// a malformed marker on partial failure (e.g. `{"error":"timeout"}`) that
|
|
1379
|
+
// still satisfies fileExists() and would cause the skip path to accept it
|
|
1380
|
+
// forever. Pass 4 prompt (pass-prompts/templates/common/pass4.md:255-283)
|
|
1381
|
+
// specifies the required structure; we check the minimum subset that
|
|
1382
|
+
// distinguishes a real marker from junk: object shape + passNum === 4 +
|
|
1383
|
+
// non-empty memoryFiles array.
|
|
1384
|
+
function isValidPass4Marker(markerPath) {
|
|
1385
|
+
if (!fileExists(markerPath)) return false;
|
|
1386
|
+
try {
|
|
1387
|
+
const data = JSON.parse(readFile(markerPath));
|
|
1388
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return false;
|
|
1389
|
+
if (data.passNum !== 4) return false;
|
|
1390
|
+
if (!Array.isArray(data.memoryFiles) || data.memoryFiles.length === 0) return false;
|
|
1391
|
+
return true;
|
|
1392
|
+
} catch (_e) { return false; }
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// Stale / invalid marker detection. Delete and re-run if either:
|
|
1396
|
+
// (a) memory/ was deleted externally (original stale-detection), OR
|
|
1397
|
+
// (b) the marker file itself is malformed (M1).
|
|
1398
|
+
//
|
|
1399
|
+
// Unlink can fail on Windows if an AV or file-watcher has the handle open.
|
|
1400
|
+
// We surface that failure to the user — without it, the subsequent
|
|
1401
|
+
// `if (fileExists(pass4Marker))` check below would accept the stale marker
|
|
1402
|
+
// and skip Pass 4 silently, leaving the project with half-scaffolded memory
|
|
1403
|
+
// state (exactly the silent-failure class this audit is eliminating).
|
|
1404
|
+
const memoryAny = fileExists(path.join(PROJECT_ROOT, "claudeos-core/memory/decision-log.md"))
|
|
1405
|
+
|| fileExists(path.join(PROJECT_ROOT, "claudeos-core/memory/compaction.md"));
|
|
1406
|
+
function dropStalePass4Marker(reasonLog) {
|
|
1407
|
+
log(reasonLog);
|
|
1408
|
+
try { fs.unlinkSync(pass4Marker); } catch (e) {
|
|
1409
|
+
log(` ❌ Failed to delete stale pass4-memory.json: ${e.code || e.message}`);
|
|
1410
|
+
throw new InitError(
|
|
1411
|
+
`Could not delete stale pass4-memory.json at:\n ${pass4Marker}\n` +
|
|
1412
|
+
` The file is likely locked by another process (Windows antivirus or a file-watcher).\n` +
|
|
1413
|
+
` Close any editor/AV scanner holding the file and re-run \`npx claudeos-core init\`.`
|
|
1414
|
+
);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
if (fileExists(pass4Marker)) {
|
|
1418
|
+
if (!memoryAny) {
|
|
1419
|
+
dropStalePass4Marker(" ⚠️ pass4-memory.json exists but memory/ is empty — re-running Pass 4");
|
|
1420
|
+
} else if (!isValidPass4Marker(pass4Marker)) {
|
|
1421
|
+
dropStalePass4Marker(" ⚠️ pass4-memory.json exists but is malformed (missing passNum/memoryFiles) — re-running Pass 4");
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
const pass4Start = Date.now();
|
|
1426
|
+
let pass4Label = "Pass 4 complete";
|
|
1427
|
+
|
|
1428
|
+
if (fileExists(pass4Marker)) {
|
|
1429
|
+
log(" ⏭️ pass4-memory.json already exists, skipping");
|
|
1430
|
+
pass4Label = "Pass 4 already present";
|
|
1431
|
+
} else if (!fileExists(pass4PromptFile)) {
|
|
1432
|
+
log(" ⚠️ pass4-prompt.md not found — falling back to static scaffold");
|
|
1433
|
+
if (applyStaticFallback()) { log(" ✅ Memory/Rules/Plans scaffolded + CLAUDE.md appended (static fallback)"); pass4Label = "Pass 4 (static fallback)"; }
|
|
1434
|
+
else { log(" ❌ Static fallback failed to write marker"); pass4Label = "Pass 4 fallback failed"; }
|
|
1435
|
+
} else {
|
|
1436
|
+
let prompt4 = injectProjectRoot(readFile(pass4PromptFile));
|
|
1437
|
+
|
|
1438
|
+
// Same stale-staging guard as Pass 3 (in case a previous Pass 4 crashed
|
|
1439
|
+
// after writing to the staging dir but before the move could run).
|
|
1440
|
+
const stagedBeforeP4 = path.join(GENERATED_DIR, ".staged-rules");
|
|
1441
|
+
if (fileExists(stagedBeforeP4)) fs.rmSync(stagedBeforeP4, { recursive: true, force: true });
|
|
1442
|
+
|
|
1443
|
+
const t4 = Date.now();
|
|
1444
|
+
// Pass 4's prompt lists 12 required outputs (pass4.md §§1-12). Of these:
|
|
1445
|
+
// - 11 are file creations (#1-10 + #12)
|
|
1446
|
+
// - 1 is an append to existing CLAUDE.md (#11, not a new file)
|
|
1447
|
+
// Of the 11 file creations, only 5 are visible to countFiles() during
|
|
1448
|
+
// the run:
|
|
1449
|
+
// - #1-4 (4 memory files) → observable
|
|
1450
|
+
// - #5-10 (6 rule files) go to .staged-rules/ under
|
|
1451
|
+
// claudeos-core/generated/, which countFiles() skips → invisible
|
|
1452
|
+
// - #12 (1 standard doc-writing-guide) → observable
|
|
1453
|
+
// So totalExpected = 5. (v2.0.x had 6 because plan/50.memory-master.md
|
|
1454
|
+
// was also generated; master plan generation was removed in v2.1.0.)
|
|
1455
|
+
// The final "100%" shows up on the outer progressBar once the staged
|
|
1456
|
+
// move + marker are done.
|
|
1457
|
+
const ticker4 = makePassTicker("Pass 4", t4, {
|
|
1458
|
+
baselineCount: countFiles(),
|
|
1459
|
+
totalExpected: 5,
|
|
1460
|
+
});
|
|
1461
|
+
const ok4 = await runClaudePromptAsync(prompt4, {
|
|
1462
|
+
onTick: ticker4.onTick,
|
|
1463
|
+
tickMs: ticker4.tickMs,
|
|
1464
|
+
});
|
|
1465
|
+
ticker4.clearLine();
|
|
1466
|
+
const elapsed4 = Date.now() - t4;
|
|
1467
|
+
|
|
1468
|
+
// Move any rule files Pass 4 wrote to the staging dir. This runs regardless
|
|
1469
|
+
// of pass4Marker status, because Claude may have written rules before
|
|
1470
|
+
// failing to produce the marker. Static fallback (below) writes directly
|
|
1471
|
+
// to .claude/rules/ and is unaffected by the move (no-op on empty staging).
|
|
1472
|
+
const { moveStagedRules: mvP4 } = require("../../lib/staged-rules");
|
|
1473
|
+
const p4Move = mvP4(PROJECT_ROOT);
|
|
1474
|
+
if (p4Move.failed > 0) {
|
|
1475
|
+
log(` ⚠️ Pass 4 staged-rules: ${p4Move.moved} moved, ${p4Move.failed} failed`);
|
|
1476
|
+
for (const err of p4Move.errors) log(` • ${err}`);
|
|
1477
|
+
} else if (p4Move.moved > 0) {
|
|
1478
|
+
log(` 📦 Pass 4 staged-rules: ${p4Move.moved} rule files moved to .claude/rules/`);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
if (!ok4 || !isValidPass4Marker(pass4Marker)) {
|
|
1482
|
+
log(" ⚠️ Pass 4 did not produce a valid pass4-memory.json — using static fallback");
|
|
1483
|
+
if (applyStaticFallback()) { log(" ✅ Memory/Rules/Plans scaffolded + CLAUDE.md appended (static fallback)"); pass4Label = `Pass 4 (static fallback, ${formatElapsed(elapsed4)})`; }
|
|
1484
|
+
else { log(" ❌ Static fallback failed to write marker"); pass4Label = "Pass 4 fallback failed"; }
|
|
1485
|
+
} else {
|
|
1486
|
+
// Claude-driven Pass 4 succeeded. Ensure memory + rules + plans + standard + CLAUDE.md append exist
|
|
1487
|
+
// (Claude may have created them; fill any gaps with static fallback).
|
|
1488
|
+
// scaffoldMemory is skip-safe: it only writes files that don't already exist,
|
|
1489
|
+
// so it won't overwrite Claude's translated content — it only fills gaps.
|
|
1490
|
+
// When lang !== "en", the gap-fill MUST translate: we do not silently
|
|
1491
|
+
// write English into a non-English project (bug #21).
|
|
1492
|
+
let gapResults;
|
|
1493
|
+
try {
|
|
1494
|
+
const memR = scaffoldMemory(memoryPath, { lang });
|
|
1495
|
+
const ruleR = scaffoldRules(rulesPath, { lang });
|
|
1496
|
+
const docR = scaffoldDocWritingGuide(standardCorePath, { lang });
|
|
1497
|
+
const planR = scaffoldMasterPlans(planPath, memoryPath, { lang });
|
|
1498
|
+
const manifestR = scaffoldSkillsManifest(skillsSharedPath, { lang });
|
|
1499
|
+
// v2.3.0: CLAUDE.md is never modified by Pass 4 anymore. See the
|
|
1500
|
+
// matching comment in applyStaticFallback above.
|
|
1501
|
+
// Collect all statuses into one flat array for summary reporting.
|
|
1502
|
+
gapResults = [
|
|
1503
|
+
...memR,
|
|
1504
|
+
...ruleR,
|
|
1505
|
+
...planR,
|
|
1506
|
+
{ file: docR.file, status: docR.status },
|
|
1507
|
+
{ file: "skills/00.shared/" + manifestR.file, status: manifestR.status },
|
|
1508
|
+
];
|
|
1509
|
+
} catch (err) {
|
|
1510
|
+
throw new InitError(
|
|
1511
|
+
`Pass 4 gap-fill failed while translating to lang='${lang}':\n` +
|
|
1512
|
+
` ${err.message}\n` +
|
|
1513
|
+
` Pass 4 Claude succeeded but some files were missing and translation of the ` +
|
|
1514
|
+
`static fallback to ${lang} failed.\n` +
|
|
1515
|
+
` Re-run when \`claude\` CLI is available, or use --lang en.`
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
// Summary: how many were already present (skipped) vs written by gap-fill.
|
|
1519
|
+
// This surfaces the true state regardless of what Claude printed during Pass 4.
|
|
1520
|
+
const skipped = gapResults.filter(r => r.status === "skipped" || r.status === "present-or-appended").length;
|
|
1521
|
+
const written = gapResults.filter(r => r.status === "written").length;
|
|
1522
|
+
const erroredItems = gapResults.filter(r => r.status === "error");
|
|
1523
|
+
const errored = erroredItems.length;
|
|
1524
|
+
log(` ✅ Pass 4 complete (${formatElapsed(elapsed4)})`);
|
|
1525
|
+
if (written > 0 || errored > 0) {
|
|
1526
|
+
log(` 📋 Gap-fill: ${skipped} already present, ${written} created via fallback, ${errored} errored`);
|
|
1527
|
+
} else {
|
|
1528
|
+
log(` 📋 Gap-fill: all ${skipped} expected files already present`);
|
|
1529
|
+
}
|
|
1530
|
+
// Surface which files errored so the user can investigate (instead of
|
|
1531
|
+
// silently rolling them into a count). Common causes: write permission
|
|
1532
|
+
// or disk full. All erroredItems are non-fatal — Pass 4 marker still
|
|
1533
|
+
// gets written, but the user should know what was incomplete.
|
|
1534
|
+
for (const item of erroredItems) {
|
|
1535
|
+
log(` ❌ ${item.file} — write failed (check disk space, permissions)`);
|
|
1536
|
+
}
|
|
1537
|
+
pass4Label = `Pass 4 complete (${formatElapsed(elapsed4)})`;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
// Record Pass 4 in the overall progress bar, regardless of which branch
|
|
1541
|
+
// (skip / static fallback / Claude-driven) ran above. Only feed stepTimes
|
|
1542
|
+
// when we actually did real work, so ETA for future steps stays meaningful.
|
|
1543
|
+
const pass4Elapsed = Date.now() - pass4Start;
|
|
1544
|
+
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...");
|
|
1551
|
+
|
|
1552
|
+
const verifyTools = [
|
|
1553
|
+
{ name: "manifest-generator", script: path.join(TOOLS_DIR, "manifest-generator/index.js") },
|
|
1554
|
+
{ name: "health-checker", script: path.join(TOOLS_DIR, "health-checker/index.js") },
|
|
1555
|
+
];
|
|
1556
|
+
|
|
1557
|
+
for (const t of verifyTools) {
|
|
1558
|
+
if (!fileExists(t.script)) {
|
|
1559
|
+
log(` ⏭️ ${t.name} — not found, skipping`);
|
|
1560
|
+
continue;
|
|
1561
|
+
}
|
|
1562
|
+
const ok = run(`node "${t.script}"`, { ignoreError: true });
|
|
1563
|
+
if (!ok) {
|
|
1564
|
+
log(` ⚠️ ${t.name} reported issues (non-fatal)`);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
log("");
|
|
1568
|
+
|
|
1569
|
+
// ─── Complete ─────────────────────────────────────────────
|
|
1570
|
+
const totalFiles = countFiles();
|
|
1571
|
+
const pass1Files = countPass1Files();
|
|
1572
|
+
|
|
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.
|
|
1582
|
+
try {
|
|
1583
|
+
const { validate } = require("../../claude-md-validator");
|
|
1584
|
+
const { formatSummaryLine } = require("../../claude-md-validator/reporter");
|
|
1585
|
+
const claudeMdPath = path.join(PROJECT_ROOT, "CLAUDE.md");
|
|
1586
|
+
if (fileExists(claudeMdPath)) {
|
|
1587
|
+
log(" [Lint] Validating CLAUDE.md structure...");
|
|
1588
|
+
const report = validate(claudeMdPath);
|
|
1589
|
+
log(` ${formatSummaryLine(report)}`);
|
|
1590
|
+
if (!report.valid) {
|
|
1591
|
+
for (const err of report.errors) {
|
|
1592
|
+
log(` ❌ [${err.id}] ${err.message}`);
|
|
1593
|
+
}
|
|
1594
|
+
log("");
|
|
1595
|
+
log(" Run `npx claudeos-core lint` for full remediation guidance,");
|
|
1596
|
+
log(" or `npx claudeos-core init --force` to regenerate.");
|
|
1597
|
+
}
|
|
1598
|
+
log("");
|
|
1599
|
+
}
|
|
1600
|
+
} catch (e) {
|
|
1601
|
+
// Lint is best-effort; don't block init completion on a lint bug.
|
|
1602
|
+
log(` ⚠️ Lint step skipped: ${e.message || e}`);
|
|
1603
|
+
log("");
|
|
1604
|
+
}
|
|
1605
|
+
|
|
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.
|
|
1618
|
+
try {
|
|
1619
|
+
const cvPath = path.join(__dirname, "..", "..", "content-validator", "index.js");
|
|
1620
|
+
if (fileExists(cvPath)) {
|
|
1621
|
+
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.
|
|
1624
|
+
const { spawnSync } = require("child_process");
|
|
1625
|
+
const result = spawnSync(process.execPath, [cvPath], {
|
|
1626
|
+
cwd: PROJECT_ROOT,
|
|
1627
|
+
env: { ...process.env, CLAUDEOS_ROOT: PROJECT_ROOT },
|
|
1628
|
+
encoding: "utf-8",
|
|
1629
|
+
});
|
|
1630
|
+
// Echo only the final summary line(s) — full output goes to
|
|
1631
|
+
// stale-report.json for later inspection.
|
|
1632
|
+
const out = (result.stdout || "").trim().split(/\r?\n/);
|
|
1633
|
+
const summary = out.slice(-3).join("\n"); // last 3 lines = summary
|
|
1634
|
+
log(summary.split("\n").map((l) => " " + l).join("\n"));
|
|
1635
|
+
if (result.status !== 0) {
|
|
1636
|
+
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");
|
|
1642
|
+
}
|
|
1643
|
+
log("");
|
|
1644
|
+
}
|
|
1645
|
+
} catch (e) {
|
|
1646
|
+
log(` ⚠️ Content check skipped: ${e.message || e}`);
|
|
1647
|
+
log("");
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
log("");
|
|
1651
|
+
const memoryReady = fileExists(path.join(PROJECT_ROOT, "claudeos-core/memory/decision-log.md"));
|
|
1652
|
+
const rulesReady = fileExists(path.join(PROJECT_ROOT, ".claude/rules/60.memory/01.decision-log.md"));
|
|
1653
|
+
const l4Status = (memoryReady && rulesReady) ? "memory + rules" : "partial";
|
|
1654
|
+
log("╔════════════════════════════════════════════════════╗");
|
|
1655
|
+
log("║ ✅ ClaudeOS-Core — Complete ║");
|
|
1656
|
+
log("║ ║");
|
|
1657
|
+
log(`║ Files created: ${pad(String(totalFiles), 29)}║`);
|
|
1658
|
+
log(`║ Domains analyzed: ${pad(totalGroups + " groups", 29)}║`);
|
|
1659
|
+
log(`║ Analysis passes: ${pad(pass1Files + " pass1 files", 29)}║`);
|
|
1660
|
+
log(`║ L4 scaffolded: ${pad(l4Status, 29)}║`);
|
|
1661
|
+
log(`║ Output language: ${pad(SUPPORTED_LANGS[lang] || lang, 29)}║`);
|
|
1662
|
+
log(`║ Total time: ${pad(formatElapsed(Date.now() - totalStart), 29)}║`);
|
|
1663
|
+
log("║ ║");
|
|
1664
|
+
log("║ Verify anytime: ║");
|
|
1665
|
+
log("║ npx claudeos-core health ║");
|
|
1666
|
+
log("║ ║");
|
|
1667
|
+
log("║ Start using: ║");
|
|
1668
|
+
log('║ "Create a CRUD for orders" ║');
|
|
1669
|
+
log("╚════════════════════════════════════════════════════╝");
|
|
1670
|
+
log("");
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
module.exports = { cmdInit, InitError };
|