claudeos-core 2.0.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +188 -0
- package/README.de.md +994 -880
- package/README.es.md +993 -880
- package/README.fr.md +993 -880
- package/README.hi.md +993 -880
- package/README.ja.md +993 -880
- package/README.ko.md +159 -47
- package/README.md +159 -46
- package/README.ru.md +993 -880
- package/README.vi.md +161 -48
- package/README.zh-CN.md +992 -880
- package/bin/cli.js +7 -2
- package/bin/commands/init.js +775 -147
- package/bin/commands/memory.js +17 -5
- package/bootstrap.sh +81 -81
- package/lib/expected-outputs.js +6 -7
- package/lib/memory-scaffold.js +84 -46
- package/lib/plan-parser.js +12 -0
- package/manifest-generator/index.js +16 -18
- package/package.json +1 -1
- package/pass-prompts/templates/angular/pass3.md +2 -10
- package/pass-prompts/templates/common/pass3-phase1.md +131 -0
- package/pass-prompts/templates/common/pass3a-facts.md +143 -0
- package/pass-prompts/templates/common/pass3b-core-header.md +58 -0
- package/pass-prompts/templates/common/pass3c-skills-guide-header.md +53 -0
- package/pass-prompts/templates/common/pass3d-plan-aux-header.md +57 -0
- package/pass-prompts/templates/common/pass4.md +4 -19
- package/pass-prompts/templates/java-spring/pass3.md +5 -15
- package/pass-prompts/templates/kotlin-spring/pass3.md +5 -15
- package/pass-prompts/templates/node-express/pass3.md +5 -14
- package/pass-prompts/templates/node-fastify/pass3.md +2 -10
- package/pass-prompts/templates/node-nestjs/pass3.md +5 -13
- package/pass-prompts/templates/node-nextjs/pass3.md +5 -14
- package/pass-prompts/templates/node-vite/pass3.md +95 -103
- package/pass-prompts/templates/python-django/pass3.md +5 -14
- package/pass-prompts/templates/python-fastapi/pass3.md +5 -14
- package/pass-prompts/templates/python-flask/pass3.md +95 -103
- package/pass-prompts/templates/vue-nuxt/pass3.md +2 -10
- package/plan-installer/pass3-context-builder.js +258 -0
- package/plan-installer/prompt-generator.js +9 -1
- package/plan-validator/index.js +23 -8
- package/sync-checker/index.js +44 -0
package/bin/commands/init.js
CHANGED
|
@@ -80,6 +80,627 @@ function makePassTicker(label, startTime, opts = {}) {
|
|
|
80
80
|
return { onTick, clearLine, tickMs: isTTY ? 1000 : 15000 };
|
|
81
81
|
}
|
|
82
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
|
+
// 단일 배치 (도메인 ≤ 15): 기존 "3b" marker 유지 (backward-compatible).
|
|
499
|
+
// 다중 배치 (도메인 > 15): "3b-core" 먼저 실행 후 "3b-1", "3b-2", ...
|
|
500
|
+
//
|
|
501
|
+
// 3b-core 분리 이유: 다중 배치의 첫 배치가 "CLAUDE.md + 공통 standard
|
|
502
|
+
// + 15 도메인"을 한 세션에 모두 처리하면 단일 스테이지 부하가 ~70-80
|
|
503
|
+
// 파일까지 치솟음. 관측된 overflow 임계(약 40 파일)보다 2배 가까이 큼.
|
|
504
|
+
// 공통 파일을 별도 스테이지로 빼서 각 스테이지가 ~50 파일 이하로
|
|
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는 공통 rules만 생성 — rules 카운트 검증은 3b-N에서.
|
|
526
|
+
return problems;
|
|
527
|
+
},
|
|
528
|
+
}
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// 도메인별 배치 루프.
|
|
533
|
+
// 단일 배치: stageId "3b", 공통 파일 포함 (기존 동작).
|
|
534
|
+
// 다중 배치: stageId "3b-1", "3b-2", ..., 공통 파일은 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
|
+
// 배치별 프롬프트: 원래 3b 헤더에 이번 배치에서만 처리할 도메인 목록 주입.
|
|
543
|
+
// 다중 배치에서는 모든 배치가 "도메인 특화 파일만" 생성 (공통 파일은 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
|
+
// 단일 배치: 공통 파일 검증 (3b-core가 없으므로 여기서 체크).
|
|
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
|
+
// 모든 배치에서 rules/ 생성 확인 (최소 1개는 staged-rules가 통과해야 함)
|
|
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
|
+
// 단일 배치 (도메인 ≤ 15): 기존 "3c" marker 유지 (guide + skills 함께).
|
|
585
|
+
// 다중 배치 (도메인 > 15): "3c-core" 먼저 실행 후 "3c-1", "3c-2", ...
|
|
586
|
+
//
|
|
587
|
+
// 3c-core 분리 이유: guide 9개 + 공통 skills는 도메인 수 무관하게 고정.
|
|
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
|
+
// 도메인별 skills 배치 루프.
|
|
615
|
+
// 단일 배치: guide + skills 함께 (기존 동작).
|
|
616
|
+
// 다중 배치: skills만, guide는 이미 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
|
+
// 단일 배치: guide 검증도 여기서 수행 (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
|
+
// Skills 최종 검증: 단일 배치 / 마지막 배치에서만 전체 검증.
|
|
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는 원래 standard/rules/skills/guide 를 master plan 으로 집계하고
|
|
666
|
+
// database/mcp-guide stub 을 만들던 스테이지였다. 하지만 master plan 자체는
|
|
667
|
+
// Claude Code 런타임에서 읽히지 않는 도구 내부 백업/관리용 파일이었고,
|
|
668
|
+
// 도메인 수가 많아지면 단일 세션 집계에서 Prompt is too long 발생 원인이
|
|
669
|
+
// 됐다 (18 도메인 실측에서 3d-standard가 32 파일 집계 시점에 실패).
|
|
670
|
+
// master plan 생성을 중단하는 것이 안정성 측면에서 올바른
|
|
671
|
+
// 결정이며, 필요시 사용자가 직접 스크립트로 집계 가능하다.
|
|
672
|
+
//
|
|
673
|
+
// 결과적으로 3d는 aux 만 남음:
|
|
674
|
+
// 3d-aux → database/ + mcp-guide/ (프로젝트 특성 설명 stub)
|
|
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 계(단일 1 or core+N) + 3c 계(단일 1 or core+N) + 3d-aux (1)
|
|
695
|
+
// 단일 배치: 1 + 1 + 1 + 1 = 4
|
|
696
|
+
// 다중 배치: 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
|
+
|
|
83
704
|
async function cmdInit(parsedArgs) {
|
|
84
705
|
const totalStart = Date.now();
|
|
85
706
|
// Tracks whether we just wiped generated state via --force or "fresh" resume
|
|
@@ -236,7 +857,6 @@ async function cmdInit(parsedArgs) {
|
|
|
236
857
|
"claudeos-core/skills/20.frontend-page/scaffold-page-feature",
|
|
237
858
|
"claudeos-core/skills/50.testing",
|
|
238
859
|
"claudeos-core/skills/90.experimental",
|
|
239
|
-
"claudeos-core/plan",
|
|
240
860
|
"claudeos-core/guide/01.onboarding",
|
|
241
861
|
"claudeos-core/guide/02.usage",
|
|
242
862
|
"claudeos-core/guide/03.troubleshooting",
|
|
@@ -432,6 +1052,36 @@ async function cmdInit(parsedArgs) {
|
|
|
432
1052
|
}
|
|
433
1053
|
log("");
|
|
434
1054
|
|
|
1055
|
+
// ─── [5.5] v2.1: Build pass3-context.json (slim summary for Pass 3) ──
|
|
1056
|
+
// Writes a small (<5 KB) structured summary derived from project-analysis.json
|
|
1057
|
+
// plus pass2-merged.json signals (size, top-level keys). Pass 3 prompts
|
|
1058
|
+
// reference this INSTEAD OF re-reading pass2-merged.json repeatedly, which
|
|
1059
|
+
// was the primary cause of `Prompt is too long` failures on large projects.
|
|
1060
|
+
//
|
|
1061
|
+
// Silent-on-failure: if pass3-context-builder returns null (e.g.
|
|
1062
|
+
// project-analysis.json missing), we skip writing and let Pass 3 fall back
|
|
1063
|
+
// to the pre-v2.1 behavior of reading pass2-merged.json directly.
|
|
1064
|
+
try {
|
|
1065
|
+
const { buildPass3Context } = require("../../plan-installer/pass3-context-builder");
|
|
1066
|
+
const pass3Ctx = buildPass3Context(GENERATED_DIR);
|
|
1067
|
+
if (pass3Ctx) {
|
|
1068
|
+
const { writeFileSafe: wfsCtx } = require("../../lib/safe-fs");
|
|
1069
|
+
const ctxPath = path.join(GENERATED_DIR, "pass3-context.json");
|
|
1070
|
+
const wrote = wfsCtx(ctxPath, JSON.stringify(pass3Ctx, null, 2));
|
|
1071
|
+
if (wrote) {
|
|
1072
|
+
const sizeKB = Math.round(JSON.stringify(pass3Ctx).length / 1024);
|
|
1073
|
+
const p2KB = pass3Ctx.pass2Merged.sizeKB;
|
|
1074
|
+
log(` 📄 pass3-context.json built (${sizeKB} KB summary of ${p2KB} KB pass2-merged.json)`);
|
|
1075
|
+
if (pass3Ctx.pass2Merged.large) {
|
|
1076
|
+
log(` ⚠️ pass2-merged.json is large (${p2KB} KB). Pass 3 will rely heavily on pass3-context.json to avoid context overflow.`);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
} catch (e) {
|
|
1081
|
+
log(` ⚠️ pass3-context.json build skipped: ${e.message} (Pass 3 will fall back to pass2-merged.json)`);
|
|
1082
|
+
}
|
|
1083
|
+
log("");
|
|
1084
|
+
|
|
435
1085
|
// ─── [6] Pass 3: Generate + verify ─────────────────────────
|
|
436
1086
|
header("[6] Pass 3 — Generating all files...");
|
|
437
1087
|
|
|
@@ -461,16 +1111,28 @@ async function cmdInit(parsedArgs) {
|
|
|
461
1111
|
}
|
|
462
1112
|
}
|
|
463
1113
|
|
|
464
|
-
// Stale marker detection
|
|
465
|
-
//
|
|
1114
|
+
// Stale marker detection (Pass 3). Drops the marker and re-runs Pass 3 if
|
|
1115
|
+
// any of the following is true:
|
|
1116
|
+
// (a) CLAUDE.md was deleted externally (original check),
|
|
1117
|
+
// (b) any of EXPECTED_GUIDE_FILES is missing or BOM-aware empty,
|
|
1118
|
+
// (c) any entry in expected-outputs (standard sentinel / skills / plan) is
|
|
1119
|
+
// missing or empty.
|
|
1120
|
+
//
|
|
1121
|
+
// (b) and (c) were previously only enforced as Pass 3 post-generation Guards
|
|
1122
|
+
// 3 (H2/H1), which gate marker *creation* on fresh runs. Projects that were
|
|
1123
|
+
// initialized on a pre-v2.0.0 release (before those guards existed) can end
|
|
1124
|
+
// up with a marker on disk even though guide/ or standard/skills/plan are
|
|
1125
|
+
// empty. Without this stale check, such projects hit a permanent
|
|
1126
|
+
// content-validator fail loop because init sees the marker and skips Pass 3
|
|
1127
|
+
// forever. This mirrors the Pass 4 dropStalePass4Marker pattern below.
|
|
466
1128
|
//
|
|
467
|
-
// Unlink is surfaced as InitError on failure (symmetric with Pass 4
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
//
|
|
471
|
-
//
|
|
472
|
-
|
|
473
|
-
log(
|
|
1129
|
+
// Unlink is surfaced as InitError on failure (symmetric with Pass 4).
|
|
1130
|
+
// Silently ignoring the error would leave the stale marker in place, and
|
|
1131
|
+
// the `if (fileExists(pass3Marker))` check below would accept it — skipping
|
|
1132
|
+
// Pass 3 while outputs are still incomplete. That silent-skip is the exact
|
|
1133
|
+
// bug class this audit round closes.
|
|
1134
|
+
function dropStalePass3Marker(reasonLog) {
|
|
1135
|
+
log(reasonLog);
|
|
474
1136
|
try { fs.unlinkSync(pass3Marker); } catch (e) {
|
|
475
1137
|
log(` ❌ Failed to delete stale pass3-complete.json: ${e.code || e.message}`);
|
|
476
1138
|
throw new InitError(
|
|
@@ -480,140 +1142,95 @@ async function cmdInit(parsedArgs) {
|
|
|
480
1142
|
);
|
|
481
1143
|
}
|
|
482
1144
|
}
|
|
483
|
-
|
|
484
1145
|
if (fileExists(pass3Marker)) {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
);
|
|
1146
|
+
// v2.1.1: split-mode partial marker 보호.
|
|
1147
|
+
// { mode: "split", groupsCompleted: [...], completedAt: undefined } 형태의
|
|
1148
|
+
// 중간 진행 마커는 stale로 판정하면 안 됨. 3b까지만 완료된 정상 상태에서도
|
|
1149
|
+
// guide/skills/plan이 비어있어서 기존 로직이 잘못 stale 판정하고 marker를
|
|
1150
|
+
// 삭제하면, runPass3Split의 resume 로직이 완료된 스테이지를 못 읽고
|
|
1151
|
+
// 3a부터 전체 재실행하게 됨 (correctness는 OK이지만 토큰 2배 낭비).
|
|
1152
|
+
let markerIsSplitPartial = false;
|
|
1153
|
+
try {
|
|
1154
|
+
const parsed = JSON.parse(readFile(pass3Marker));
|
|
1155
|
+
markerIsSplitPartial =
|
|
1156
|
+
parsed &&
|
|
1157
|
+
parsed.mode === "split" &&
|
|
1158
|
+
Array.isArray(parsed.groupsCompleted) &&
|
|
1159
|
+
!parsed.completedAt;
|
|
1160
|
+
} catch (_e) {
|
|
1161
|
+
// malformed marker → stale check로 넘어감 (이전 동작 유지)
|
|
538
1162
|
}
|
|
539
1163
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
)
|
|
1164
|
+
if (markerIsSplitPartial) {
|
|
1165
|
+
log(` ↪️ split-mode partial marker detected — runPass3Split will resume`);
|
|
1166
|
+
} else if (!fileExists(claudeMdPath)) {
|
|
1167
|
+
dropStalePass3Marker(" ⚠️ pass3-complete.json exists but CLAUDE.md is missing — treating marker as stale, re-running Pass 3");
|
|
1168
|
+
} else {
|
|
1169
|
+
const guideDirForStale = path.join(PROJECT_ROOT, "claudeos-core/guide");
|
|
1170
|
+
const staleMissingGuides = EXPECTED_GUIDE_FILES.filter(g => {
|
|
1171
|
+
const fp = path.join(guideDirForStale, g);
|
|
1172
|
+
if (!fileExists(fp)) return true;
|
|
1173
|
+
try {
|
|
1174
|
+
return fs.readFileSync(fp, "utf-8").replace(/^\uFEFF/, "").trim().length === 0;
|
|
1175
|
+
} catch (_e) { return true; }
|
|
1176
|
+
});
|
|
1177
|
+
if (staleMissingGuides.length > 0) {
|
|
1178
|
+
dropStalePass3Marker(
|
|
1179
|
+
` ⚠️ 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`
|
|
1180
|
+
);
|
|
1181
|
+
} else {
|
|
1182
|
+
const staleMissingOutputs = findMissingOutputs(PROJECT_ROOT);
|
|
1183
|
+
if (staleMissingOutputs.length > 0) {
|
|
1184
|
+
dropStalePass3Marker(
|
|
1185
|
+
` ⚠️ pass3-complete.json exists but ${staleMissingOutputs.length} required output(s) are missing or empty — treating marker as stale, re-running Pass 3`
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
554
1189
|
}
|
|
1190
|
+
}
|
|
555
1191
|
|
|
556
|
-
|
|
557
|
-
|
|
1192
|
+
// Pass 3 split mode resolution.
|
|
1193
|
+
//
|
|
1194
|
+
// Pass 3 runs as 4 sequential `claude -p` calls (3a facts, 3b core, 3c
|
|
1195
|
+
// skills+guide, 3d-aux database/mcp-guide), each with fresh context.
|
|
1196
|
+
// This eliminates `Prompt is too long` failures by making output
|
|
1197
|
+
// accumulation overflow structurally impossible — each stage starts
|
|
1198
|
+
// with a clean context window. For projects with 16+ domains, 3b and 3c
|
|
1199
|
+
// are further split into core + batched sub-stages.
|
|
1200
|
+
//
|
|
1201
|
+
// Single-call mode (previously toggled by CLAUDEOS_PASS3_SPLIT=0) was
|
|
1202
|
+
// removed because empirical data showed it failed reliably on projects
|
|
1203
|
+
// with more than ~5 domains (output accumulation overflow was not
|
|
1204
|
+
// predictable from input size alone), and split mode is structurally
|
|
1205
|
+
// immune to this failure mode with bounded token overhead.
|
|
1206
|
+
log(` 🚀 Pass 3 split mode (3a → 3b → 3c → 3d-aux)`);
|
|
1207
|
+
try {
|
|
1208
|
+
const ctxPath = path.join(GENERATED_DIR, "pass3-context.json");
|
|
1209
|
+
if (fileExists(ctxPath)) {
|
|
1210
|
+
const ctx = JSON.parse(readFile(ctxPath));
|
|
1211
|
+
const rec = ctx && ctx.splitRecommendation;
|
|
1212
|
+
if (rec) {
|
|
1213
|
+
log(` • estimated ${rec.estimatedFileCount} files from ${rec.totalDomains} domains`);
|
|
1214
|
+
}
|
|
558
1215
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
|
1216
|
+
} catch (_e) { /* best-effort log only */ }
|
|
1217
|
+
|
|
1218
|
+
if (!fileExists(pass3Marker)) {
|
|
1219
|
+
await runPass3Split({
|
|
1220
|
+
GENERATED_DIR, PROJECT_ROOT, TOOLS_DIR,
|
|
1221
|
+
pass3Marker, claudeMdPath,
|
|
1222
|
+
injectProjectRoot, readFile, fileExists,
|
|
1223
|
+
runClaudePromptAsync, makePassTicker, formatElapsed,
|
|
1224
|
+
log, countFiles,
|
|
1225
|
+
EXPECTED_GUIDE_FILES, findMissingOutputs,
|
|
1226
|
+
lang, stepTimes,
|
|
578
1227
|
});
|
|
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
1228
|
completedSteps++;
|
|
616
|
-
progressBar(completedSteps, `Pass 3 complete (
|
|
1229
|
+
progressBar(completedSteps, `Pass 3 complete (split mode)`);
|
|
1230
|
+
log("");
|
|
1231
|
+
} else {
|
|
1232
|
+
log(" ⏭️ pass3-complete.json already exists, skipping");
|
|
1233
|
+
completedSteps++;
|
|
617
1234
|
}
|
|
618
1235
|
log("");
|
|
619
1236
|
|
|
@@ -623,13 +1240,14 @@ async function cmdInit(parsedArgs) {
|
|
|
623
1240
|
const pass4Marker = path.join(GENERATED_DIR, "pass4-memory.json");
|
|
624
1241
|
const pass4PromptFile = path.join(GENERATED_DIR, "pass4-prompt.md");
|
|
625
1242
|
|
|
626
|
-
const { scaffoldMemory, scaffoldRules, appendClaudeMdL4Memory, scaffoldMasterPlans, scaffoldDocWritingGuide } = require("../../lib/memory-scaffold");
|
|
1243
|
+
const { scaffoldMemory, scaffoldRules, appendClaudeMdL4Memory, scaffoldMasterPlans, scaffoldDocWritingGuide, scaffoldSkillsManifest } = require("../../lib/memory-scaffold");
|
|
627
1244
|
const { writeFileSafe } = require("../../lib/safe-fs");
|
|
628
1245
|
|
|
629
1246
|
const memoryPath = path.join(PROJECT_ROOT, "claudeos-core/memory");
|
|
630
1247
|
const planPath = path.join(PROJECT_ROOT, "claudeos-core/plan");
|
|
631
1248
|
const rulesPath = path.join(PROJECT_ROOT, ".claude/rules");
|
|
632
1249
|
const standardCorePath = path.join(PROJECT_ROOT, "claudeos-core/standard/00.core");
|
|
1250
|
+
const skillsSharedPath = path.join(PROJECT_ROOT, "claudeos-core/skills/00.shared");
|
|
633
1251
|
|
|
634
1252
|
function applyStaticFallback() {
|
|
635
1253
|
try {
|
|
@@ -637,6 +1255,7 @@ async function cmdInit(parsedArgs) {
|
|
|
637
1255
|
scaffoldRules(rulesPath, { lang });
|
|
638
1256
|
scaffoldDocWritingGuide(standardCorePath, { lang });
|
|
639
1257
|
scaffoldMasterPlans(planPath, memoryPath, { lang });
|
|
1258
|
+
scaffoldSkillsManifest(skillsSharedPath, { lang });
|
|
640
1259
|
appendClaudeMdL4Memory(claudeMdPath, { lang });
|
|
641
1260
|
} catch (err) {
|
|
642
1261
|
// When lang !== "en", translation is REQUIRED. If it fails, we surface
|
|
@@ -668,9 +1287,9 @@ async function cmdInit(parsedArgs) {
|
|
|
668
1287
|
".claude/rules/60.memory/03.compaction.md",
|
|
669
1288
|
".claude/rules/60.memory/04.auto-rule-update.md",
|
|
670
1289
|
],
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
1290
|
+
// Note: master plan files are no longer generated (previously this
|
|
1291
|
+
// included "claudeos-core/plan/50.memory-master.md"). The marker schema
|
|
1292
|
+
// still accepts an optional planFiles field for backward compatibility.
|
|
674
1293
|
claudeMdAppended: true,
|
|
675
1294
|
}, null, 2);
|
|
676
1295
|
return writeFileSafe(pass4Marker, markerBody);
|
|
@@ -743,15 +1362,22 @@ async function cmdInit(parsedArgs) {
|
|
|
743
1362
|
if (fileExists(stagedBeforeP4)) fs.rmSync(stagedBeforeP4, { recursive: true, force: true });
|
|
744
1363
|
|
|
745
1364
|
const t4 = Date.now();
|
|
746
|
-
// Pass 4
|
|
747
|
-
//
|
|
748
|
-
//
|
|
749
|
-
//
|
|
750
|
-
//
|
|
751
|
-
//
|
|
1365
|
+
// Pass 4's prompt lists 12 required outputs (pass4.md §§1-12). Of these:
|
|
1366
|
+
// - 11 are file creations (#1-10 + #12)
|
|
1367
|
+
// - 1 is an append to existing CLAUDE.md (#11, not a new file)
|
|
1368
|
+
// Of the 11 file creations, only 5 are visible to countFiles() during
|
|
1369
|
+
// the run:
|
|
1370
|
+
// - #1-4 (4 memory files) → observable
|
|
1371
|
+
// - #5-10 (6 rule files) go to .staged-rules/ under
|
|
1372
|
+
// claudeos-core/generated/, which countFiles() skips → invisible
|
|
1373
|
+
// - #12 (1 standard doc-writing-guide) → observable
|
|
1374
|
+
// So totalExpected = 5. (v2.0.x had 6 because plan/50.memory-master.md
|
|
1375
|
+
// was also generated; master plan generation was removed in v2.1.0.)
|
|
1376
|
+
// The final "100%" shows up on the outer progressBar once the staged
|
|
1377
|
+
// move + marker are done.
|
|
752
1378
|
const ticker4 = makePassTicker("Pass 4", t4, {
|
|
753
1379
|
baselineCount: countFiles(),
|
|
754
|
-
totalExpected:
|
|
1380
|
+
totalExpected: 5,
|
|
755
1381
|
});
|
|
756
1382
|
const ok4 = await runClaudePromptAsync(prompt4, {
|
|
757
1383
|
onTick: ticker4.onTick,
|
|
@@ -790,6 +1416,7 @@ async function cmdInit(parsedArgs) {
|
|
|
790
1416
|
const ruleR = scaffoldRules(rulesPath, { lang });
|
|
791
1417
|
const docR = scaffoldDocWritingGuide(standardCorePath, { lang });
|
|
792
1418
|
const planR = scaffoldMasterPlans(planPath, memoryPath, { lang });
|
|
1419
|
+
const manifestR = scaffoldSkillsManifest(skillsSharedPath, { lang });
|
|
793
1420
|
const claudeOk = appendClaudeMdL4Memory(claudeMdPath, { lang });
|
|
794
1421
|
// Collect all statuses into one flat array for summary reporting.
|
|
795
1422
|
gapResults = [
|
|
@@ -797,6 +1424,7 @@ async function cmdInit(parsedArgs) {
|
|
|
797
1424
|
...ruleR,
|
|
798
1425
|
...planR,
|
|
799
1426
|
{ file: docR.file, status: docR.status },
|
|
1427
|
+
{ file: "skills/00.shared/" + manifestR.file, status: manifestR.status },
|
|
800
1428
|
{ file: "CLAUDE.md#(L4)", status: claudeOk ? "present-or-appended" : "error" },
|
|
801
1429
|
];
|
|
802
1430
|
} catch (err) {
|