claudeos-core 1.7.0 → 2.0.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 +138 -0
- package/CONTRIBUTING.md +92 -59
- package/README.de.md +465 -240
- package/README.es.md +446 -223
- package/README.fr.md +461 -238
- package/README.hi.md +485 -261
- package/README.ja.md +440 -235
- package/README.ko.md +244 -56
- package/README.md +215 -47
- package/README.ru.md +462 -238
- package/README.vi.md +454 -230
- package/README.zh-CN.md +476 -252
- package/bin/cli.js +144 -140
- package/bin/commands/init.js +550 -46
- package/bin/commands/memory.js +426 -0
- package/bin/lib/cli-utils.js +206 -143
- package/bootstrap.sh +81 -390
- package/content-validator/index.js +436 -340
- package/lib/expected-guides.js +23 -0
- package/lib/expected-outputs.js +91 -0
- package/lib/language-config.js +35 -0
- package/lib/memory-scaffold.js +1014 -0
- package/lib/plan-parser.js +153 -149
- package/lib/staged-rules.js +118 -0
- package/manifest-generator/index.js +176 -171
- package/package.json +1 -1
- package/pass-json-validator/index.js +337 -299
- package/pass-prompts/templates/common/pass3-footer.md +16 -0
- package/pass-prompts/templates/common/pass4.md +317 -0
- package/pass-prompts/templates/common/staging-override.md +26 -0
- package/pass-prompts/templates/python-flask/pass1.md +119 -0
- package/pass-prompts/templates/python-flask/pass2.md +85 -0
- package/pass-prompts/templates/python-flask/pass3.md +103 -0
- package/plan-installer/domain-grouper.js +2 -1
- package/plan-installer/prompt-generator.js +120 -96
- package/plan-installer/scanners/scan-frontend.js +219 -10
- package/plan-installer/scanners/scan-java.js +226 -223
- package/plan-installer/scanners/scan-python.js +21 -0
- package/sync-checker/index.js +133 -132
package/bin/commands/init.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ClaudeOS-Core — Init Command
|
|
3
3
|
*
|
|
4
|
-
* Runs the full
|
|
4
|
+
* Runs the full 4-Pass pipeline: analyze → merge → generate → memory scaffold.
|
|
5
5
|
* This is the main entry point for project bootstrapping.
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -10,30 +10,15 @@ const path = require("path");
|
|
|
10
10
|
const {
|
|
11
11
|
TOOLS_DIR, PROJECT_ROOT, GENERATED_DIR,
|
|
12
12
|
SUPPORTED_LANGS, LANG_CODES, isValidLang,
|
|
13
|
-
log, header, run, runClaudePrompt,
|
|
13
|
+
log, header, run, runClaudePrompt, runClaudePromptAsync,
|
|
14
14
|
ensureDir, fileExists, readFile, injectProjectRoot,
|
|
15
15
|
pad, countFiles, countPass1Files,
|
|
16
16
|
} = require("../lib/cli-utils");
|
|
17
17
|
const { selectLangInteractive } = require("../lib/lang-selector");
|
|
18
18
|
const { selectResumeMode } = require("../lib/resume-selector");
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
en: " ⏳ [{{PASS}}] Running claude -p (no output is normal, please wait)...",
|
|
23
|
-
ko: " ⏳ [{{PASS}}] claude -p 실행 중 (출력이 없어도 정상입니다. 잠시 기다려주세요)...",
|
|
24
|
-
"zh-CN": " ⏳ [{{PASS}}] 正在运行 claude -p(没有输出是正常的,请稍候)...",
|
|
25
|
-
ja: " ⏳ [{{PASS}}] claude -p 実行中(出力がなくても正常です。しばらくお待ちください)...",
|
|
26
|
-
es: " ⏳ [{{PASS}}] Ejecutando claude -p (es normal que no haya salida, por favor espere)...",
|
|
27
|
-
vi: " ⏳ [{{PASS}}] Đang chạy claude -p (không có output là bình thường, vui lòng chờ)...",
|
|
28
|
-
hi: " ⏳ [{{PASS}}] claude -p चल रहा है (कोई आउटपुट न होना सामान्य है, कृपया प्रतीक्षा करें)...",
|
|
29
|
-
ru: " ⏳ [{{PASS}}] Выполняется claude -p (отсутствие вывода — это нормально, подождите)...",
|
|
30
|
-
fr: " ⏳ [{{PASS}}] Exécution de claude -p (l'absence de sortie est normale, veuillez patienter)...",
|
|
31
|
-
de: " ⏳ [{{PASS}}] claude -p wird ausgeführt (keine Ausgabe ist normal, bitte warten)...",
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
function claudeWaitMsg(lang, passLabel) {
|
|
35
|
-
return (CLAUDE_WAIT_TMPL[lang] || CLAUDE_WAIT_TMPL.en).replace("{{PASS}}", passLabel);
|
|
36
|
-
}
|
|
20
|
+
const { EXPECTED_GUIDE_FILES } = require("../../lib/expected-guides");
|
|
21
|
+
const { findMissingOutputs } = require("../../lib/expected-outputs");
|
|
37
22
|
|
|
38
23
|
class InitError extends Error {
|
|
39
24
|
constructor(msg) { super(msg); this.name = "InitError"; }
|
|
@@ -47,8 +32,61 @@ function formatElapsed(ms) {
|
|
|
47
32
|
return rem > 0 ? `${min}m ${rem}s` : `${min}m`;
|
|
48
33
|
}
|
|
49
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
|
+
|
|
50
83
|
async function cmdInit(parsedArgs) {
|
|
51
84
|
const totalStart = Date.now();
|
|
85
|
+
// Tracks whether we just wiped generated state via --force or "fresh" resume
|
|
86
|
+
// mode. Used by the Pass 3 backfill guard below: fresh/force explicitly
|
|
87
|
+
// means "regenerate from scratch", so a leftover CLAUDE.md from a prior run
|
|
88
|
+
// must NOT cause Pass 3 to be skipped via the v1.7.x migration backfill.
|
|
89
|
+
let wasFreshClean = false;
|
|
52
90
|
|
|
53
91
|
// ─── Prerequisites check ───────────────────────────────────
|
|
54
92
|
const hasProjectMarker = [".git", "package.json", "build.gradle", "build.gradle.kts", "pom.xml", "pyproject.toml", "requirements.txt"].some(
|
|
@@ -87,6 +125,24 @@ async function cmdInit(parsedArgs) {
|
|
|
87
125
|
if (!isValidLang(lang)) {
|
|
88
126
|
throw new InitError(`Unsupported language: "${lang}"\n Supported: ${LANG_CODES.join(", ")}`);
|
|
89
127
|
}
|
|
128
|
+
|
|
129
|
+
// Early incompatibility check: CLAUDEOS_SKIP_TRANSLATION is a test-only
|
|
130
|
+
// env var that short-circuits lib/memory-scaffold.js translation path.
|
|
131
|
+
// If set AND the user chose a non-English language, Pass 4's static fallback
|
|
132
|
+
// and gap-fill would throw mid-run with a confusing "translation skipped"
|
|
133
|
+
// error. Fail fast here with a clear message so the user can unset the var
|
|
134
|
+
// or pick --lang en before the pipeline starts.
|
|
135
|
+
if (process.env.CLAUDEOS_SKIP_TRANSLATION === "1" && lang !== "en") {
|
|
136
|
+
throw new InitError(
|
|
137
|
+
`CLAUDEOS_SKIP_TRANSLATION=1 is set but --lang='${lang}' requires translation.\n` +
|
|
138
|
+
` This env var is a test-only escape hatch that blocks calls to \`claude -p\`\n` +
|
|
139
|
+
` from lib/memory-scaffold.js. Pass 4 would crash later with a hard-to-\n` +
|
|
140
|
+
` diagnose error.\n\n` +
|
|
141
|
+
` Either unset the env var: unset CLAUDEOS_SKIP_TRANSLATION\n` +
|
|
142
|
+
` Or run with English output: npx claudeos-core init --lang en`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
90
146
|
process.env.CLAUDEOS_LANG = lang;
|
|
91
147
|
|
|
92
148
|
// ─── Resume / Fresh selection ────────────────────────────
|
|
@@ -99,6 +155,19 @@ async function cmdInit(parsedArgs) {
|
|
|
99
155
|
// --force: clean all generated files for truly fresh start
|
|
100
156
|
const genFiles = fs.readdirSync(GENERATED_DIR).filter(f => f.endsWith(".json") || f.endsWith(".md"));
|
|
101
157
|
for (const f of genFiles) fs.unlinkSync(path.join(GENERATED_DIR, f));
|
|
158
|
+
// Also clean any leftover .staged-rules/ from a prior crashed run
|
|
159
|
+
// (only .json/.md are unlinked above; directories aren't touched).
|
|
160
|
+
const stagedDir = path.join(GENERATED_DIR, ".staged-rules");
|
|
161
|
+
if (fileExists(stagedDir)) fs.rmSync(stagedDir, { recursive: true, force: true });
|
|
162
|
+
// Also wipe .claude/rules/ so Guard 2 (zero-rules detection) can't
|
|
163
|
+
// false-negative on stale rules from a previous run when the fresh
|
|
164
|
+
// Pass 3 run fails silently (e.g. Claude ignores staging-override).
|
|
165
|
+
// Step [2] recreates the subdirs from scratch. Any manual edits the
|
|
166
|
+
// user made to rule files are lost — acceptable under --force
|
|
167
|
+
// ("truly fresh start").
|
|
168
|
+
const rulesDir = path.join(PROJECT_ROOT, ".claude/rules");
|
|
169
|
+
if (fileExists(rulesDir)) fs.rmSync(rulesDir, { recursive: true, force: true });
|
|
170
|
+
wasFreshClean = true;
|
|
102
171
|
log(" 🔄 Previous results deleted (--force)\n");
|
|
103
172
|
} else {
|
|
104
173
|
const status = { pass1Done: existingPass1.length, pass2Done: pass2Exists };
|
|
@@ -107,6 +176,20 @@ async function cmdInit(parsedArgs) {
|
|
|
107
176
|
if (mode === "fresh") {
|
|
108
177
|
for (const f of existingPass1) fs.unlinkSync(path.join(GENERATED_DIR, f));
|
|
109
178
|
if (pass2Exists) fs.unlinkSync(path.join(GENERATED_DIR, "pass2-merged.json"));
|
|
179
|
+
// Also reset pass 3 & pass 4 markers so they re-run
|
|
180
|
+
const pass3M = path.join(GENERATED_DIR, "pass3-complete.json");
|
|
181
|
+
const pass4M = path.join(GENERATED_DIR, "pass4-memory.json");
|
|
182
|
+
if (fileExists(pass3M)) fs.unlinkSync(pass3M);
|
|
183
|
+
if (fileExists(pass4M)) fs.unlinkSync(pass4M);
|
|
184
|
+
// Clean .staged-rules/ leftover from a prior crashed run (same reason as --force branch).
|
|
185
|
+
const stagedDir = path.join(GENERATED_DIR, ".staged-rules");
|
|
186
|
+
if (fileExists(stagedDir)) fs.rmSync(stagedDir, { recursive: true, force: true });
|
|
187
|
+
// Wipe .claude/rules/ for the same Guard 2 false-negative reason as
|
|
188
|
+
// the --force branch. Step [2] recreates the subdirs; any manual
|
|
189
|
+
// edits are lost — acceptable under an explicit "fresh" choice.
|
|
190
|
+
const rulesDir = path.join(PROJECT_ROOT, ".claude/rules");
|
|
191
|
+
if (fileExists(rulesDir)) fs.rmSync(rulesDir, { recursive: true, force: true });
|
|
192
|
+
wasFreshClean = true;
|
|
110
193
|
} else if (mode === "continue" && existingPass1.length === 0 && pass2Exists) {
|
|
111
194
|
// pass2 exists but no pass1 → pass2 is stale, force re-run
|
|
112
195
|
fs.unlinkSync(path.join(GENERATED_DIR, "pass2-merged.json"));
|
|
@@ -118,7 +201,7 @@ async function cmdInit(parsedArgs) {
|
|
|
118
201
|
|
|
119
202
|
log("");
|
|
120
203
|
log("╔════════════════════════════════════════════════════╗");
|
|
121
|
-
log("║ ClaudeOS-Core — Bootstrap (
|
|
204
|
+
log("║ ClaudeOS-Core — Bootstrap (4-Pass) ║");
|
|
122
205
|
log("╚════════════════════════════════════════════════════╝");
|
|
123
206
|
log(` Project root: ${PROJECT_ROOT}`);
|
|
124
207
|
log(` Language: ${SUPPORTED_LANGS[lang]} (${lang})`);
|
|
@@ -160,6 +243,8 @@ async function cmdInit(parsedArgs) {
|
|
|
160
243
|
"claudeos-core/guide/04.architecture",
|
|
161
244
|
"claudeos-core/database",
|
|
162
245
|
"claudeos-core/mcp-guide",
|
|
246
|
+
"claudeos-core/memory",
|
|
247
|
+
".claude/rules/60.memory",
|
|
163
248
|
];
|
|
164
249
|
for (const d of dirs) {
|
|
165
250
|
ensureDir(path.join(PROJECT_ROOT, d));
|
|
@@ -205,8 +290,8 @@ async function cmdInit(parsedArgs) {
|
|
|
205
290
|
throw new InitError(`domain-groups.json is malformed: expected ${totalGroups} groups, found ${domainGroups.groups ? domainGroups.groups.length : 0}`);
|
|
206
291
|
}
|
|
207
292
|
|
|
208
|
-
// Progress tracking: Pass 1 (N groups) + Pass 2 + Pass 3 = totalSteps
|
|
209
|
-
const totalSteps = totalGroups +
|
|
293
|
+
// Progress tracking: Pass 1 (N groups) + Pass 2 + Pass 3 + Pass 4 = totalSteps
|
|
294
|
+
const totalSteps = totalGroups + 3;
|
|
210
295
|
let completedSteps = 0;
|
|
211
296
|
const stepTimes = [];
|
|
212
297
|
const passStart = Date.now();
|
|
@@ -256,15 +341,22 @@ async function cmdInit(parsedArgs) {
|
|
|
256
341
|
throw new InitError(`No pass1 prompt found for type: ${groupType}`);
|
|
257
342
|
}
|
|
258
343
|
|
|
259
|
-
// Placeholder substitution
|
|
344
|
+
// Placeholder substitution — use replacement functions (not string form)
|
|
345
|
+
// so that `$`, `$1`, `$&`, `$$` etc. in domainList are preserved as
|
|
346
|
+
// literal characters rather than interpreted as regex back-references.
|
|
347
|
+
// (Same bug class as bug #18 in lib/plan-parser.js replaceFileBlock.)
|
|
260
348
|
let prompt = template
|
|
261
|
-
.replace(/\{\{DOMAIN_GROUP\}\}/g, domainList)
|
|
262
|
-
.replace(/\{\{PASS_NUM\}\}/g, String(i));
|
|
349
|
+
.replace(/\{\{DOMAIN_GROUP\}\}/g, () => domainList)
|
|
350
|
+
.replace(/\{\{PASS_NUM\}\}/g, () => String(i));
|
|
263
351
|
prompt = injectProjectRoot(prompt);
|
|
264
352
|
|
|
265
|
-
log(claudeWaitMsg(lang, `Pass 1-${i}/${totalGroups}`));
|
|
266
353
|
const t1 = Date.now();
|
|
267
|
-
const
|
|
354
|
+
const ticker1 = makePassTicker(`Pass 1-${i}/${totalGroups}`, t1);
|
|
355
|
+
const ok = await runClaudePromptAsync(prompt, {
|
|
356
|
+
onTick: ticker1.onTick,
|
|
357
|
+
tickMs: ticker1.tickMs,
|
|
358
|
+
});
|
|
359
|
+
ticker1.clearLine();
|
|
268
360
|
const elapsed1 = Date.now() - t1;
|
|
269
361
|
stepTimes.push(elapsed1);
|
|
270
362
|
|
|
@@ -285,7 +377,29 @@ async function cmdInit(parsedArgs) {
|
|
|
285
377
|
header("[5] Pass 2 — Merging analysis results...");
|
|
286
378
|
|
|
287
379
|
const pass2Json = path.join(GENERATED_DIR, "pass2-merged.json");
|
|
380
|
+
|
|
381
|
+
// H3: resume-path structural validation. existsSync alone isn't enough —
|
|
382
|
+
// a prior crashed run may have left a skeleton (`{}`) or malformed JSON
|
|
383
|
+
// that passes existsSync but silently poisons Pass 3 (which parses this
|
|
384
|
+
// file as its analysis input). Mirrors pass1's malformed-detection at
|
|
385
|
+
// the pass1 loop above, and the "<5 top-level keys = INSUFFICIENT_KEYS"
|
|
386
|
+
// threshold from pass-json-validator/index.js (ERROR level).
|
|
387
|
+
let pass2IsValid = false;
|
|
288
388
|
if (fileExists(pass2Json)) {
|
|
389
|
+
try {
|
|
390
|
+
const existing = JSON.parse(readFile(pass2Json));
|
|
391
|
+
if (existing && typeof existing === "object" && !Array.isArray(existing)
|
|
392
|
+
&& Object.keys(existing).length >= 5) {
|
|
393
|
+
pass2IsValid = true;
|
|
394
|
+
}
|
|
395
|
+
} catch (_e) { /* malformed — fall through to re-run */ }
|
|
396
|
+
if (!pass2IsValid) {
|
|
397
|
+
log(" ⚠️ pass2-merged.json exists but is malformed or incomplete (<5 top-level keys), re-running");
|
|
398
|
+
try { fs.unlinkSync(pass2Json); } catch (_e) { /* best-effort cleanup */ }
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (pass2IsValid) {
|
|
289
403
|
log(" ⏭️ pass2-merged.json already exists, skipping");
|
|
290
404
|
completedSteps++;
|
|
291
405
|
} else {
|
|
@@ -295,9 +409,13 @@ async function cmdInit(parsedArgs) {
|
|
|
295
409
|
}
|
|
296
410
|
let prompt = injectProjectRoot(readFile(pass2PromptFile));
|
|
297
411
|
|
|
298
|
-
log(claudeWaitMsg(lang, "Pass 2"));
|
|
299
412
|
const t2 = Date.now();
|
|
300
|
-
const
|
|
413
|
+
const ticker2 = makePassTicker("Pass 2", t2);
|
|
414
|
+
const ok = await runClaudePromptAsync(prompt, {
|
|
415
|
+
onTick: ticker2.onTick,
|
|
416
|
+
tickMs: ticker2.tickMs,
|
|
417
|
+
});
|
|
418
|
+
ticker2.clearLine();
|
|
301
419
|
const elapsed2 = Date.now() - t2;
|
|
302
420
|
stepTimes.push(elapsed2);
|
|
303
421
|
|
|
@@ -317,31 +435,413 @@ async function cmdInit(parsedArgs) {
|
|
|
317
435
|
// ─── [6] Pass 3: Generate + verify ─────────────────────────
|
|
318
436
|
header("[6] Pass 3 — Generating all files...");
|
|
319
437
|
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
438
|
+
const pass3Marker = path.join(GENERATED_DIR, "pass3-complete.json");
|
|
439
|
+
const claudeMdPath = path.join(PROJECT_ROOT, "CLAUDE.md");
|
|
440
|
+
|
|
441
|
+
// v1.7.1 → v1.7.2 migration: if CLAUDE.md exists from prior version but marker
|
|
442
|
+
// is missing, backfill marker to preserve user's existing output.
|
|
443
|
+
//
|
|
444
|
+
// Gated by !wasFreshClean: --force and "fresh" resume mode wipe the marker
|
|
445
|
+
// on purpose to force Pass 3 to re-run. They do NOT delete CLAUDE.md (user
|
|
446
|
+
// may have manual edits worth preserving in the continue flow), so without
|
|
447
|
+
// this gate the backfill would fire on a fresh run, re-write the marker,
|
|
448
|
+
// and Pass 3 would skip — leaving stale CLAUDE.md + regenerated pass1/2 +
|
|
449
|
+
// wiped rules/, which fails sync-checker and content-validator.
|
|
450
|
+
if (!wasFreshClean && fileExists(claudeMdPath) && !fileExists(pass3Marker) && fileExists(path.join(GENERATED_DIR, "pass2-merged.json"))) {
|
|
451
|
+
const { writeFileSafe: wfsMig } = require("../../lib/safe-fs");
|
|
452
|
+
const backfillOk = wfsMig(pass3Marker, JSON.stringify({
|
|
453
|
+
completedAt: new Date().toISOString(),
|
|
454
|
+
backfilled: true,
|
|
455
|
+
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.",
|
|
456
|
+
}, null, 2));
|
|
457
|
+
if (backfillOk) {
|
|
458
|
+
log(" ℹ️ Detected existing CLAUDE.md — Pass 3 marker backfilled (use --force to regenerate)");
|
|
459
|
+
} else {
|
|
460
|
+
log(" ⚠️ CLAUDE.md exists but marker backfill failed — Pass 3 will re-run");
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Stale marker detection: if marker exists but CLAUDE.md was deleted externally,
|
|
465
|
+
// treat marker as stale and re-run (user clearly wants regeneration).
|
|
466
|
+
//
|
|
467
|
+
// Unlink is surfaced as InitError on failure (symmetric with Pass 4
|
|
468
|
+
// dropStalePass4Marker). Silently ignoring the error would leave the stale
|
|
469
|
+
// marker in place, and the `if (fileExists(pass3Marker))` check below would
|
|
470
|
+
// accept it — skipping Pass 3 while CLAUDE.md is still missing. That
|
|
471
|
+
// silent-skip is the exact bug class this audit round closes.
|
|
472
|
+
if (fileExists(pass3Marker) && !fileExists(claudeMdPath)) {
|
|
473
|
+
log(" ⚠️ pass3-complete.json exists but CLAUDE.md is missing — treating marker as stale, re-running Pass 3");
|
|
474
|
+
try { fs.unlinkSync(pass3Marker); } catch (e) {
|
|
475
|
+
log(` ❌ Failed to delete stale pass3-complete.json: ${e.code || e.message}`);
|
|
476
|
+
throw new InitError(
|
|
477
|
+
`Could not delete stale pass3-complete.json at:\n ${pass3Marker}\n` +
|
|
478
|
+
` The file is likely locked by another process (Windows antivirus or a file-watcher).\n` +
|
|
479
|
+
` Close any editor/AV scanner holding the file and re-run \`npx claudeos-core init\`.`
|
|
480
|
+
);
|
|
481
|
+
}
|
|
323
482
|
}
|
|
324
|
-
let prompt = injectProjectRoot(readFile(pass3PromptFile));
|
|
325
483
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
484
|
+
if (fileExists(pass3Marker)) {
|
|
485
|
+
log(" ⏭️ pass3-complete.json already exists, skipping");
|
|
486
|
+
completedSteps++;
|
|
487
|
+
} else {
|
|
488
|
+
const pass3PromptFile = path.join(GENERATED_DIR, "pass3-prompt.md");
|
|
489
|
+
if (!fileExists(pass3PromptFile)) {
|
|
490
|
+
throw new InitError("pass3-prompt.md not found. Re-run plan-installer.");
|
|
491
|
+
}
|
|
492
|
+
let prompt = injectProjectRoot(readFile(pass3PromptFile));
|
|
493
|
+
|
|
494
|
+
// Clear any stale .staged-rules/ before running Claude, so we don't
|
|
495
|
+
// accidentally move leftover files from a prior crashed run alongside
|
|
496
|
+
// the new output. Safe no-op when the dir doesn't exist.
|
|
497
|
+
const stagedBeforeP3 = path.join(GENERATED_DIR, ".staged-rules");
|
|
498
|
+
if (fileExists(stagedBeforeP3)) fs.rmSync(stagedBeforeP3, { recursive: true, force: true });
|
|
499
|
+
|
|
500
|
+
const t3 = Date.now();
|
|
501
|
+
// Pass 3 writes many files across .claude/ and claudeos-core/; we can't
|
|
502
|
+
// know the total in advance (stack-dependent), so we show the delta only.
|
|
503
|
+
const ticker3 = makePassTicker("Pass 3", t3, { baselineCount: countFiles() });
|
|
504
|
+
const ok3 = await runClaudePromptAsync(prompt, {
|
|
505
|
+
onTick: ticker3.onTick,
|
|
506
|
+
tickMs: ticker3.tickMs,
|
|
507
|
+
});
|
|
508
|
+
ticker3.clearLine();
|
|
509
|
+
const elapsed3 = Date.now() - t3;
|
|
510
|
+
stepTimes.push(elapsed3);
|
|
511
|
+
|
|
512
|
+
if (!ok3) {
|
|
513
|
+
throw new InitError("Pass 3 failed. Check the claude error output above.\n If this persists, try: npx claudeos-core init --force");
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Move rule files that Pass 3 wrote to the staging dir (workaround for
|
|
517
|
+
// Claude Code's .claude/ sensitive-path block). See lib/staged-rules.js.
|
|
518
|
+
const { moveStagedRules: mvP3, countFilesRecursive } = require("../../lib/staged-rules");
|
|
519
|
+
const p3Move = mvP3(PROJECT_ROOT);
|
|
520
|
+
if (p3Move.failed > 0) {
|
|
521
|
+
log(` ⚠️ Pass 3 staged-rules: ${p3Move.moved} moved, ${p3Move.failed} failed`);
|
|
522
|
+
for (const err of p3Move.errors) log(` • ${err}`);
|
|
523
|
+
} else if (p3Move.moved > 0) {
|
|
524
|
+
log(` 📦 Pass 3 staged-rules: ${p3Move.moved} rule files moved to .claude/rules/`);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Guard 1 (Risk #1): Partial move failure. We do NOT write the pass3
|
|
528
|
+
// completion marker, so the next `init` run re-executes Pass 3 via the
|
|
529
|
+
// continue-mode path. Transient causes (Windows file locks, antivirus
|
|
530
|
+
// scanners) usually clear on retry. The partially-moved rules stay in
|
|
531
|
+
// .claude/rules/ — they're overwritten on re-run.
|
|
532
|
+
if (p3Move.failed > 0) {
|
|
533
|
+
throw new InitError(
|
|
534
|
+
`Pass 3 finished but ${p3Move.failed} rule file(s) could not be moved from staging.\n` +
|
|
535
|
+
` See the warnings above. This is usually a transient file-lock issue.\n` +
|
|
536
|
+
` Re-run \`npx claudeos-core init\` — Pass 3 will retry automatically.`
|
|
537
|
+
);
|
|
538
|
+
}
|
|
331
539
|
|
|
332
|
-
|
|
333
|
-
|
|
540
|
+
// Guard 2 (Risk #2): Empty .claude/rules/. If Claude ignored the
|
|
541
|
+
// staging-override directive and tried to write directly to .claude/,
|
|
542
|
+
// those writes are blocked by Claude Code and the staging dir stays
|
|
543
|
+
// empty. Pass 3 reliably generates at least 00.standard-reference.md,
|
|
544
|
+
// so zero files is a strong signal that generation failed silently.
|
|
545
|
+
const ruleFilesCount = countFilesRecursive(path.join(PROJECT_ROOT, ".claude/rules"));
|
|
546
|
+
if (ruleFilesCount === 0) {
|
|
547
|
+
throw new InitError(
|
|
548
|
+
"Pass 3 produced 0 rule files under .claude/rules/.\n" +
|
|
549
|
+
" This usually means Claude ignored the staging-override directive\n" +
|
|
550
|
+
" and attempted to write to .claude/ directly, where Claude Code's\n" +
|
|
551
|
+
" sensitive-path policy blocks writes.\n" +
|
|
552
|
+
" Re-run with --force: `npx claudeos-core init --force`"
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (!fileExists(claudeMdPath)) {
|
|
557
|
+
throw new InitError("CLAUDE.md was not created. Claude ran but did not produce CLAUDE.md.\n Verify pass3-prompt.md instructs Claude to create CLAUDE.md at project root.");
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Guard 3 (Risk #3): Incomplete generation. Claude occasionally truncates
|
|
561
|
+
// mid-response after writing CLAUDE.md + rules/ but before reaching the
|
|
562
|
+
// guide/ section of the prompt. It also occasionally writes only a heading
|
|
563
|
+
// and truncates before the body — giving us an empty file that satisfies
|
|
564
|
+
// existsSync but fails content-validator's trim-length check. Both cases
|
|
565
|
+
// leave the project permanently broken on subsequent runs (step [8]
|
|
566
|
+
// content-validator errors are non-fatal), so gate the marker here.
|
|
567
|
+
const guideDir = path.join(PROJECT_ROOT, "claudeos-core/guide");
|
|
568
|
+
const missingOrEmptyGuides = EXPECTED_GUIDE_FILES.filter(g => {
|
|
569
|
+
const fp = path.join(guideDir, g);
|
|
570
|
+
if (!fileExists(fp)) return true;
|
|
571
|
+
try {
|
|
572
|
+
// Strip UTF-8 BOM before trim — String.prototype.trim doesn't remove
|
|
573
|
+
// U+FEFF (not in Unicode White_Space). Otherwise a BOM-only file
|
|
574
|
+
// (3 bytes, no text) would pass the empty check and Guard 3 would
|
|
575
|
+
// silently accept it. Mirrors content-validator/index.js:115.
|
|
576
|
+
return fs.readFileSync(fp, "utf-8").replace(/^\uFEFF/, "").trim().length === 0;
|
|
577
|
+
} catch (_e) { return true; } // unreadable counts as missing
|
|
578
|
+
});
|
|
579
|
+
if (missingOrEmptyGuides.length > 0) {
|
|
580
|
+
const preview = missingOrEmptyGuides.slice(0, 5).map(g => ` • claudeos-core/guide/${g}`).join("\n");
|
|
581
|
+
const more = missingOrEmptyGuides.length > 5 ? `\n • ... and ${missingOrEmptyGuides.length - 5} more` : "";
|
|
582
|
+
throw new InitError(
|
|
583
|
+
`Pass 3 produced CLAUDE.md and rules but ${missingOrEmptyGuides.length}/${EXPECTED_GUIDE_FILES.length} guide files are missing or empty:\n` +
|
|
584
|
+
preview + more + "\n" +
|
|
585
|
+
" Claude likely truncated the response before reaching or finishing the guide/ section.\n" +
|
|
586
|
+
" Re-run with --force: `npx claudeos-core init --force`"
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Guard 3 extension (H1): The same truncation pattern can cut off Claude's
|
|
591
|
+
// response AFTER the guide/ section but before standard/, skills/, or
|
|
592
|
+
// plan/. content-validator flags these as ERROR-level but step [8] runs
|
|
593
|
+
// with ignoreError:true so nothing blocks the marker. Validate each
|
|
594
|
+
// directory here — a specific sentinel file for standard/, and a
|
|
595
|
+
// "≥1 non-empty .md" check for skills/ and plan/. database/ and
|
|
596
|
+
// mcp-guide/ are intentionally excluded (validator: WARNING-level; stacks
|
|
597
|
+
// legitimately produce zero files when no DB or MCP integration exists).
|
|
598
|
+
const missingOutputs = findMissingOutputs(PROJECT_ROOT);
|
|
599
|
+
if (missingOutputs.length > 0) {
|
|
600
|
+
const preview = missingOutputs.map(m => ` • ${m}`).join("\n");
|
|
601
|
+
throw new InitError(
|
|
602
|
+
`Pass 3 finished but the following required output(s) are missing or empty:\n` +
|
|
603
|
+
preview + "\n" +
|
|
604
|
+
" Claude likely truncated the response before completing all output sections.\n" +
|
|
605
|
+
" Re-run with --force: `npx claudeos-core init --force`"
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Write completion marker so subsequent `init` runs skip Pass 3 under "continue" mode.
|
|
610
|
+
const { writeFileSafe: wfs } = require("../../lib/safe-fs");
|
|
611
|
+
const markerOk = wfs(pass3Marker, JSON.stringify({ completedAt: new Date().toISOString() }, null, 2));
|
|
612
|
+
if (!markerOk) {
|
|
613
|
+
throw new InitError(`Failed to write ${path.basename(pass3Marker)}. Check disk space and permissions on claudeos-core/generated/.\n Without this marker, subsequent \`init\` runs will regenerate CLAUDE.md.`);
|
|
614
|
+
}
|
|
615
|
+
completedSteps++;
|
|
616
|
+
progressBar(completedSteps, `Pass 3 complete (${formatElapsed(elapsed3)})`);
|
|
617
|
+
}
|
|
618
|
+
log("");
|
|
619
|
+
|
|
620
|
+
// ─── [7] Pass 4: L4 memory scaffolding ────────────
|
|
621
|
+
header("[7] Pass 4 — Memory scaffolding...");
|
|
622
|
+
|
|
623
|
+
const pass4Marker = path.join(GENERATED_DIR, "pass4-memory.json");
|
|
624
|
+
const pass4PromptFile = path.join(GENERATED_DIR, "pass4-prompt.md");
|
|
625
|
+
|
|
626
|
+
const { scaffoldMemory, scaffoldRules, appendClaudeMdL4Memory, scaffoldMasterPlans, scaffoldDocWritingGuide } = require("../../lib/memory-scaffold");
|
|
627
|
+
const { writeFileSafe } = require("../../lib/safe-fs");
|
|
628
|
+
|
|
629
|
+
const memoryPath = path.join(PROJECT_ROOT, "claudeos-core/memory");
|
|
630
|
+
const planPath = path.join(PROJECT_ROOT, "claudeos-core/plan");
|
|
631
|
+
const rulesPath = path.join(PROJECT_ROOT, ".claude/rules");
|
|
632
|
+
const standardCorePath = path.join(PROJECT_ROOT, "claudeos-core/standard/00.core");
|
|
633
|
+
|
|
634
|
+
function applyStaticFallback() {
|
|
635
|
+
try {
|
|
636
|
+
scaffoldMemory(memoryPath, { lang });
|
|
637
|
+
scaffoldRules(rulesPath, { lang });
|
|
638
|
+
scaffoldDocWritingGuide(standardCorePath, { lang });
|
|
639
|
+
scaffoldMasterPlans(planPath, memoryPath, { lang });
|
|
640
|
+
appendClaudeMdL4Memory(claudeMdPath, { lang });
|
|
641
|
+
} catch (err) {
|
|
642
|
+
// When lang !== "en", translation is REQUIRED. If it fails, we surface
|
|
643
|
+
// a clear error rather than silently writing English (which would
|
|
644
|
+
// contradict the user's --lang choice).
|
|
645
|
+
throw new InitError(
|
|
646
|
+
`Static fallback failed while translating to lang='${lang}':\n` +
|
|
647
|
+
` ${err.message}\n` +
|
|
648
|
+
` Ensure the \`claude\` CLI is available and authenticated, then re-run.\n` +
|
|
649
|
+
` Alternatively re-run with --lang en to skip translation.`
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
const markerBody = JSON.stringify({
|
|
653
|
+
analyzedAt: new Date().toISOString(),
|
|
654
|
+
passNum: 4,
|
|
655
|
+
fallback: true,
|
|
656
|
+
lang,
|
|
657
|
+
memoryFiles: [
|
|
658
|
+
"claudeos-core/memory/decision-log.md",
|
|
659
|
+
"claudeos-core/memory/failure-patterns.md",
|
|
660
|
+
"claudeos-core/memory/compaction.md",
|
|
661
|
+
"claudeos-core/memory/auto-rule-update.md",
|
|
662
|
+
],
|
|
663
|
+
ruleFiles: [
|
|
664
|
+
".claude/rules/00.core/51.doc-writing-rules.md",
|
|
665
|
+
".claude/rules/00.core/52.ai-work-rules.md",
|
|
666
|
+
".claude/rules/60.memory/01.decision-log.md",
|
|
667
|
+
".claude/rules/60.memory/02.failure-patterns.md",
|
|
668
|
+
".claude/rules/60.memory/03.compaction.md",
|
|
669
|
+
".claude/rules/60.memory/04.auto-rule-update.md",
|
|
670
|
+
],
|
|
671
|
+
planFiles: [
|
|
672
|
+
"claudeos-core/plan/50.memory-master.md",
|
|
673
|
+
],
|
|
674
|
+
claudeMdAppended: true,
|
|
675
|
+
}, null, 2);
|
|
676
|
+
return writeFileSafe(pass4Marker, markerBody);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// M1: validate Pass 4 marker CONTENT, not just existence. Claude can emit
|
|
680
|
+
// a malformed marker on partial failure (e.g. `{"error":"timeout"}`) that
|
|
681
|
+
// still satisfies fileExists() and would cause the skip path to accept it
|
|
682
|
+
// forever. Pass 4 prompt (pass-prompts/templates/common/pass4.md:255-283)
|
|
683
|
+
// specifies the required structure; we check the minimum subset that
|
|
684
|
+
// distinguishes a real marker from junk: object shape + passNum === 4 +
|
|
685
|
+
// non-empty memoryFiles array.
|
|
686
|
+
function isValidPass4Marker(markerPath) {
|
|
687
|
+
if (!fileExists(markerPath)) return false;
|
|
688
|
+
try {
|
|
689
|
+
const data = JSON.parse(readFile(markerPath));
|
|
690
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return false;
|
|
691
|
+
if (data.passNum !== 4) return false;
|
|
692
|
+
if (!Array.isArray(data.memoryFiles) || data.memoryFiles.length === 0) return false;
|
|
693
|
+
return true;
|
|
694
|
+
} catch (_e) { return false; }
|
|
334
695
|
}
|
|
335
696
|
|
|
336
|
-
|
|
337
|
-
|
|
697
|
+
// Stale / invalid marker detection. Delete and re-run if either:
|
|
698
|
+
// (a) memory/ was deleted externally (original stale-detection), OR
|
|
699
|
+
// (b) the marker file itself is malformed (M1).
|
|
700
|
+
//
|
|
701
|
+
// Unlink can fail on Windows if an AV or file-watcher has the handle open.
|
|
702
|
+
// We surface that failure to the user — without it, the subsequent
|
|
703
|
+
// `if (fileExists(pass4Marker))` check below would accept the stale marker
|
|
704
|
+
// and skip Pass 4 silently, leaving the project with half-scaffolded memory
|
|
705
|
+
// state (exactly the silent-failure class this audit is eliminating).
|
|
706
|
+
const memoryAny = fileExists(path.join(PROJECT_ROOT, "claudeos-core/memory/decision-log.md"))
|
|
707
|
+
|| fileExists(path.join(PROJECT_ROOT, "claudeos-core/memory/compaction.md"));
|
|
708
|
+
function dropStalePass4Marker(reasonLog) {
|
|
709
|
+
log(reasonLog);
|
|
710
|
+
try { fs.unlinkSync(pass4Marker); } catch (e) {
|
|
711
|
+
log(` ❌ Failed to delete stale pass4-memory.json: ${e.code || e.message}`);
|
|
712
|
+
throw new InitError(
|
|
713
|
+
`Could not delete stale pass4-memory.json at:\n ${pass4Marker}\n` +
|
|
714
|
+
` The file is likely locked by another process (Windows antivirus or a file-watcher).\n` +
|
|
715
|
+
` Close any editor/AV scanner holding the file and re-run \`npx claudeos-core init\`.`
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
if (fileExists(pass4Marker)) {
|
|
720
|
+
if (!memoryAny) {
|
|
721
|
+
dropStalePass4Marker(" ⚠️ pass4-memory.json exists but memory/ is empty — re-running Pass 4");
|
|
722
|
+
} else if (!isValidPass4Marker(pass4Marker)) {
|
|
723
|
+
dropStalePass4Marker(" ⚠️ pass4-memory.json exists but is malformed (missing passNum/memoryFiles) — re-running Pass 4");
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const pass4Start = Date.now();
|
|
728
|
+
let pass4Label = "Pass 4 complete";
|
|
729
|
+
|
|
730
|
+
if (fileExists(pass4Marker)) {
|
|
731
|
+
log(" ⏭️ pass4-memory.json already exists, skipping");
|
|
732
|
+
pass4Label = "Pass 4 already present";
|
|
733
|
+
} else if (!fileExists(pass4PromptFile)) {
|
|
734
|
+
log(" ⚠️ pass4-prompt.md not found — falling back to static scaffold");
|
|
735
|
+
if (applyStaticFallback()) { log(" ✅ Memory/Rules/Plans scaffolded + CLAUDE.md appended (static fallback)"); pass4Label = "Pass 4 (static fallback)"; }
|
|
736
|
+
else { log(" ❌ Static fallback failed to write marker"); pass4Label = "Pass 4 fallback failed"; }
|
|
737
|
+
} else {
|
|
738
|
+
let prompt4 = injectProjectRoot(readFile(pass4PromptFile));
|
|
739
|
+
|
|
740
|
+
// Same stale-staging guard as Pass 3 (in case a previous Pass 4 crashed
|
|
741
|
+
// after writing to the staging dir but before the move could run).
|
|
742
|
+
const stagedBeforeP4 = path.join(GENERATED_DIR, ".staged-rules");
|
|
743
|
+
if (fileExists(stagedBeforeP4)) fs.rmSync(stagedBeforeP4, { recursive: true, force: true });
|
|
744
|
+
|
|
745
|
+
const t4 = Date.now();
|
|
746
|
+
// Pass 4 creates 12 files in total, but the 6 rule files go to
|
|
747
|
+
// .staged-rules/ (i.e. under claudeos-core/generated/) which countFiles()
|
|
748
|
+
// deliberately skips. So the ticker can only observe 6 files during the
|
|
749
|
+
// run: 4 memory + 1 plan + 1 standard. We set totalExpected to that
|
|
750
|
+
// observable max; the final "100%" shows up on the outer progressBar
|
|
751
|
+
// once the staged move + marker are done.
|
|
752
|
+
const ticker4 = makePassTicker("Pass 4", t4, {
|
|
753
|
+
baselineCount: countFiles(),
|
|
754
|
+
totalExpected: 6,
|
|
755
|
+
});
|
|
756
|
+
const ok4 = await runClaudePromptAsync(prompt4, {
|
|
757
|
+
onTick: ticker4.onTick,
|
|
758
|
+
tickMs: ticker4.tickMs,
|
|
759
|
+
});
|
|
760
|
+
ticker4.clearLine();
|
|
761
|
+
const elapsed4 = Date.now() - t4;
|
|
762
|
+
|
|
763
|
+
// Move any rule files Pass 4 wrote to the staging dir. This runs regardless
|
|
764
|
+
// of pass4Marker status, because Claude may have written rules before
|
|
765
|
+
// failing to produce the marker. Static fallback (below) writes directly
|
|
766
|
+
// to .claude/rules/ and is unaffected by the move (no-op on empty staging).
|
|
767
|
+
const { moveStagedRules: mvP4 } = require("../../lib/staged-rules");
|
|
768
|
+
const p4Move = mvP4(PROJECT_ROOT);
|
|
769
|
+
if (p4Move.failed > 0) {
|
|
770
|
+
log(` ⚠️ Pass 4 staged-rules: ${p4Move.moved} moved, ${p4Move.failed} failed`);
|
|
771
|
+
for (const err of p4Move.errors) log(` • ${err}`);
|
|
772
|
+
} else if (p4Move.moved > 0) {
|
|
773
|
+
log(` 📦 Pass 4 staged-rules: ${p4Move.moved} rule files moved to .claude/rules/`);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (!ok4 || !isValidPass4Marker(pass4Marker)) {
|
|
777
|
+
log(" ⚠️ Pass 4 did not produce a valid pass4-memory.json — using static fallback");
|
|
778
|
+
if (applyStaticFallback()) { log(" ✅ Memory/Rules/Plans scaffolded + CLAUDE.md appended (static fallback)"); pass4Label = `Pass 4 (static fallback, ${formatElapsed(elapsed4)})`; }
|
|
779
|
+
else { log(" ❌ Static fallback failed to write marker"); pass4Label = "Pass 4 fallback failed"; }
|
|
780
|
+
} else {
|
|
781
|
+
// Claude-driven Pass 4 succeeded. Ensure memory + rules + plans + standard + CLAUDE.md append exist
|
|
782
|
+
// (Claude may have created them; fill any gaps with static fallback).
|
|
783
|
+
// scaffoldMemory is skip-safe: it only writes files that don't already exist,
|
|
784
|
+
// so it won't overwrite Claude's translated content — it only fills gaps.
|
|
785
|
+
// When lang !== "en", the gap-fill MUST translate: we do not silently
|
|
786
|
+
// write English into a non-English project (bug #21).
|
|
787
|
+
let gapResults;
|
|
788
|
+
try {
|
|
789
|
+
const memR = scaffoldMemory(memoryPath, { lang });
|
|
790
|
+
const ruleR = scaffoldRules(rulesPath, { lang });
|
|
791
|
+
const docR = scaffoldDocWritingGuide(standardCorePath, { lang });
|
|
792
|
+
const planR = scaffoldMasterPlans(planPath, memoryPath, { lang });
|
|
793
|
+
const claudeOk = appendClaudeMdL4Memory(claudeMdPath, { lang });
|
|
794
|
+
// Collect all statuses into one flat array for summary reporting.
|
|
795
|
+
gapResults = [
|
|
796
|
+
...memR,
|
|
797
|
+
...ruleR,
|
|
798
|
+
...planR,
|
|
799
|
+
{ file: docR.file, status: docR.status },
|
|
800
|
+
{ file: "CLAUDE.md#(L4)", status: claudeOk ? "present-or-appended" : "error" },
|
|
801
|
+
];
|
|
802
|
+
} catch (err) {
|
|
803
|
+
throw new InitError(
|
|
804
|
+
`Pass 4 gap-fill failed while translating to lang='${lang}':\n` +
|
|
805
|
+
` ${err.message}\n` +
|
|
806
|
+
` Pass 4 Claude succeeded but some files were missing and translation of the ` +
|
|
807
|
+
`static fallback to ${lang} failed.\n` +
|
|
808
|
+
` Re-run when \`claude\` CLI is available, or use --lang en.`
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
// Summary: how many were already present (skipped) vs written by gap-fill.
|
|
812
|
+
// This surfaces the true state regardless of what Claude printed during Pass 4.
|
|
813
|
+
const skipped = gapResults.filter(r => r.status === "skipped" || r.status === "present-or-appended").length;
|
|
814
|
+
const written = gapResults.filter(r => r.status === "written").length;
|
|
815
|
+
const erroredItems = gapResults.filter(r => r.status === "error");
|
|
816
|
+
const errored = erroredItems.length;
|
|
817
|
+
log(` ✅ Pass 4 complete (${formatElapsed(elapsed4)})`);
|
|
818
|
+
if (written > 0 || errored > 0) {
|
|
819
|
+
log(` 📋 Gap-fill: ${skipped} already present, ${written} created via fallback, ${errored} errored`);
|
|
820
|
+
} else {
|
|
821
|
+
log(` 📋 Gap-fill: all ${skipped} expected files already present`);
|
|
822
|
+
}
|
|
823
|
+
// Surface which files errored so the user can investigate (instead of
|
|
824
|
+
// silently rolling them into a count). Common causes: write permission,
|
|
825
|
+
// disk full, or appendClaudeMdL4Memory returned false (CLAUDE.md missing
|
|
826
|
+
// or unwritable). All erroredItems are non-fatal — Pass 4 marker still
|
|
827
|
+
// gets written, but the user should know what was incomplete.
|
|
828
|
+
for (const item of erroredItems) {
|
|
829
|
+
log(` ❌ ${item.file} — write failed (check disk space, permissions)`);
|
|
830
|
+
}
|
|
831
|
+
pass4Label = `Pass 4 complete (${formatElapsed(elapsed4)})`;
|
|
832
|
+
}
|
|
338
833
|
}
|
|
834
|
+
// Record Pass 4 in the overall progress bar, regardless of which branch
|
|
835
|
+
// (skip / static fallback / Claude-driven) ran above. Only feed stepTimes
|
|
836
|
+
// when we actually did real work, so ETA for future steps stays meaningful.
|
|
837
|
+
const pass4Elapsed = Date.now() - pass4Start;
|
|
838
|
+
if (pass4Elapsed > 500) stepTimes.push(pass4Elapsed);
|
|
339
839
|
completedSteps++;
|
|
340
|
-
progressBar(completedSteps,
|
|
840
|
+
progressBar(completedSteps, pass4Label);
|
|
341
841
|
log("");
|
|
342
842
|
|
|
343
|
-
// ─── [
|
|
344
|
-
header("[
|
|
843
|
+
// ─── [8] Run verification tools ───────────────────────────────
|
|
844
|
+
header("[8] Running verification tools...");
|
|
345
845
|
|
|
346
846
|
const verifyTools = [
|
|
347
847
|
{ name: "manifest-generator", script: path.join(TOOLS_DIR, "manifest-generator/index.js") },
|
|
@@ -365,14 +865,18 @@ async function cmdInit(parsedArgs) {
|
|
|
365
865
|
const pass1Files = countPass1Files();
|
|
366
866
|
|
|
367
867
|
log("");
|
|
868
|
+
const memoryReady = fileExists(path.join(PROJECT_ROOT, "claudeos-core/memory/decision-log.md"));
|
|
869
|
+
const rulesReady = fileExists(path.join(PROJECT_ROOT, ".claude/rules/60.memory/01.decision-log.md"));
|
|
870
|
+
const l4Status = (memoryReady && rulesReady) ? "memory + rules" : "partial";
|
|
368
871
|
log("╔════════════════════════════════════════════════════╗");
|
|
369
872
|
log("║ ✅ ClaudeOS-Core — Complete ║");
|
|
370
873
|
log("║ ║");
|
|
371
874
|
log(`║ Files created: ${pad(String(totalFiles), 29)}║`);
|
|
372
875
|
log(`║ Domains analyzed: ${pad(totalGroups + " groups", 29)}║`);
|
|
373
876
|
log(`║ Analysis passes: ${pad(pass1Files + " pass1 files", 29)}║`);
|
|
877
|
+
log(`║ L4 scaffolded: ${pad(l4Status, 29)}║`);
|
|
374
878
|
log(`║ Output language: ${pad(SUPPORTED_LANGS[lang] || lang, 29)}║`);
|
|
375
|
-
log(`║ Total time:
|
|
879
|
+
log(`║ Total time: ${pad(formatElapsed(Date.now() - totalStart), 29)}║`);
|
|
376
880
|
log("║ ║");
|
|
377
881
|
log("║ Verify anytime: ║");
|
|
378
882
|
log("║ npx claudeos-core health ║");
|