@ai-content-space/loopx 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +26 -26
  2. package/package.json +6 -2
  3. package/plugins/loopx/.codex-plugin/plugin.json +6 -6
  4. package/plugins/loopx/scripts/plugin-install.test.mjs +25 -8
  5. package/plugins/loopx/skills/autopilot/SKILL.md +90 -0
  6. package/plugins/loopx/skills/build/SKILL.md +118 -0
  7. package/plugins/loopx/skills/clarify/SKILL.md +219 -0
  8. package/plugins/loopx/skills/plan/SKILL.md +238 -0
  9. package/plugins/loopx/skills/{loopx-review → review}/SKILL.md +9 -4
  10. package/skills/ai-slop-cleaner/SKILL.md +114 -0
  11. package/skills/autopilot/SKILL.md +90 -0
  12. package/skills/autoresearch/SKILL.md +68 -0
  13. package/skills/build/SKILL.md +118 -0
  14. package/skills/clarify/SKILL.md +219 -0
  15. package/skills/deep-interview/SKILL.md +461 -0
  16. package/skills/deepsearch/SKILL.md +38 -0
  17. package/skills/plan/SKILL.md +238 -0
  18. package/skills/ralph/SKILL.md +271 -0
  19. package/skills/ralplan/SKILL.md +49 -0
  20. package/skills/{loopx-review → review}/SKILL.md +9 -4
  21. package/src/autopilot-runtime.mjs +152 -0
  22. package/src/build-runtime.mjs +146 -0
  23. package/src/cli.mjs +43 -7
  24. package/src/codex-exec-runtime.mjs +97 -0
  25. package/src/install-discovery.mjs +7 -7
  26. package/src/plan-runtime.mjs +456 -0
  27. package/src/runtime-maintenance.mjs +36 -8
  28. package/src/workflow.mjs +825 -123
  29. package/templates/architecture.md +3 -3
  30. package/templates/development-plan.md +1 -1
  31. package/templates/execution-record.md +1 -1
  32. package/templates/plan.md +4 -4
  33. package/templates/review-report.md +1 -1
  34. package/templates/spec.md +38 -2
  35. package/templates/test-plan.md +1 -1
  36. package/plugins/loopx/skills/loopx-autopilot/SKILL.md +0 -30
  37. package/plugins/loopx/skills/loopx-build/SKILL.md +0 -25
  38. package/plugins/loopx/skills/loopx-clarify/SKILL.md +0 -25
  39. package/plugins/loopx/skills/loopx-plan/SKILL.md +0 -25
  40. package/skills/loopx-autopilot/SKILL.md +0 -30
  41. package/skills/loopx-build/SKILL.md +0 -25
  42. package/skills/loopx-clarify/SKILL.md +0 -25
  43. package/skills/loopx-plan/SKILL.md +0 -25
@@ -0,0 +1,456 @@
1
+ import { join } from 'node:path';
2
+ import { mkdir } from 'node:fs/promises';
3
+
4
+ import { runCodexExecJson } from './codex-exec-runtime.mjs';
5
+
6
+ const DEFAULT_MAX_ITERATIONS = 5;
7
+
8
+ function extractSection(text, heading) {
9
+ const pattern = new RegExp(`## ${heading}\\n\\n([\\s\\S]*?)(?=\\n## |$)`, 'i');
10
+ const match = text.match(pattern);
11
+ return match ? match[1].trim() : '';
12
+ }
13
+
14
+ function bulletsFromSection(section, fallback) {
15
+ const bullets = section
16
+ .split('\n')
17
+ .map((line) => line.trim())
18
+ .filter((line) => line.startsWith('- '))
19
+ .map((line) => line.slice(2).trim())
20
+ .filter(Boolean);
21
+ return bullets.length > 0 ? bullets : fallback;
22
+ }
23
+
24
+ function paragraphFromSection(section, fallback) {
25
+ const paragraph = section
26
+ .split('\n')
27
+ .map((line) => line.trim())
28
+ .filter(Boolean)
29
+ .filter((line) => !line.startsWith('- '))
30
+ .join(' ');
31
+ return paragraph || fallback;
32
+ }
33
+
34
+ function buildSourceSummary(sourceText) {
35
+ return {
36
+ intent: paragraphFromSection(extractSection(sourceText, 'Intent'), 'Translate the approved requirements into a build-ready plan package.'),
37
+ outcome: paragraphFromSection(extractSection(sourceText, 'Desired Outcome'), 'Produce an approved plan package and stop before execution.'),
38
+ inScope: bulletsFromSection(extractSection(sourceText, 'In Scope'), [
39
+ 'Create the approved planning artifacts.',
40
+ 'Keep runtime state machine-checkable.',
41
+ 'Preserve explicit execution approval boundaries.',
42
+ ]),
43
+ nonGoals: bulletsFromSection(extractSection(sourceText, 'Out of Scope / Non-goals'), [
44
+ 'Do not launch execution from plan.',
45
+ 'Do not widen the task beyond approved planning scope.',
46
+ ]),
47
+ acceptance: bulletsFromSection(extractSection(sourceText, 'Testable Acceptance Criteria'), [
48
+ 'Planning outputs are complete and reviewable.',
49
+ 'Verification steps are explicit.',
50
+ ]),
51
+ constraints: bulletsFromSection(extractSection(sourceText, 'Constraints'), [
52
+ 'Preserve existing workflow sequencing.',
53
+ 'Keep planning outputs deterministic and reviewable.',
54
+ ]),
55
+ decisions: bulletsFromSection(extractSection(sourceText, 'Decision Boundaries'), [
56
+ 'Plan stops after approved planning artifacts exist.',
57
+ 'Execution requires explicit downstream approval.',
58
+ ]),
59
+ };
60
+ }
61
+
62
+ function chineseBulletList(items) {
63
+ return items.map((item) => `- ${item}`).join('\n');
64
+ }
65
+
66
+ function plannerDraftFromSource({ slug, sourceText, deliberateMode }) {
67
+ const summary = buildSourceSummary(sourceText);
68
+ const preMortem = deliberateMode
69
+ ? [
70
+ '如果运行时没有独立的规划适配层,真实编排与测试替身会相互污染。',
71
+ '如果 docs 只是附加产物,plan 完成判定会再次与文档契约脱节。',
72
+ '如果 status 只显示 plan_package_status,规划阶段故障仍然无法诊断。',
73
+ ]
74
+ : [];
75
+
76
+ return {
77
+ principles: [
78
+ '运行时行为必须与已发布的 planning contract 对齐。',
79
+ '规划阶段只做规划,不自动进入执行阶段。',
80
+ '完成判定必须机器可检查。',
81
+ '中文 docs 是阻塞产物,不是附加导出。',
82
+ '保持变更范围集中在 plan runtime、status 和测试。',
83
+ ],
84
+ decisionDrivers: [
85
+ '当前 skill contract 与 runtime truth 不一致。',
86
+ 'approved plan 与中文 docs 都是必需产物。',
87
+ '现有 clarify/build/review 行为需要保持稳定。',
88
+ ],
89
+ options: [
90
+ {
91
+ name: 'Embed orchestration in plan runtime',
92
+ pros: ['runtime truth and product contract stay aligned', 'one state machine owns gating and artifacts'],
93
+ cons: ['requires new adapter seam for deterministic testing'],
94
+ },
95
+ {
96
+ name: 'Wrapper consensus around lightweight plan',
97
+ pros: ['smaller immediate diff'],
98
+ cons: ['preserves split truth between wrapper and runtime', 'status/debugging remains fragmented'],
99
+ },
100
+ ],
101
+ planText: [
102
+ `# loopx Plan: ${slug}`,
103
+ '',
104
+ '## Requirements Summary',
105
+ '',
106
+ `- ${summary.intent}`,
107
+ `- ${summary.outcome}`,
108
+ '',
109
+ '## Deliverables',
110
+ '',
111
+ ...summary.acceptance.map((item, index) => `${index + 1}. ${item}`),
112
+ '',
113
+ '## Implementation Steps',
114
+ '',
115
+ '1. Add a plan orchestration adapter for planner, architect, and critic.',
116
+ '2. Record plan iteration, review verdicts, and docs blockers in workflow state.',
117
+ '3. Generate canonical plan artifacts and Chinese docs outputs from the approved planning source.',
118
+ '4. Expose plan-stage progress in CLI status.',
119
+ '5. Add deterministic regression coverage for happy path, iterate path, and docs blockers.',
120
+ '',
121
+ '## Risks',
122
+ '',
123
+ ...summary.constraints.map((item) => `- ${item}`),
124
+ '',
125
+ '## Verification',
126
+ '',
127
+ '- run workflow tests',
128
+ '- run CLI status checks',
129
+ '- prove docs blocking and iteration paths',
130
+ ].join('\n'),
131
+ architectureText: [
132
+ `# loopx Architecture: ${slug}`,
133
+ '',
134
+ '## Intent',
135
+ '',
136
+ `- ${summary.intent}`,
137
+ '',
138
+ '## Boundaries',
139
+ '',
140
+ ...summary.decisions.map((item) => `- ${item}`),
141
+ '',
142
+ '## Chosen Design',
143
+ '',
144
+ '- plan runtime owns the planner -> architect -> critic loop',
145
+ '- a dedicated adapter separates production orchestration from deterministic tests',
146
+ '- canonical plan artifacts remain under `.loopx/plans/`',
147
+ '- required Chinese docs are emitted under `docs/<slug>/`',
148
+ '',
149
+ '## Alternatives Considered',
150
+ '',
151
+ '- keep plan lightweight and wrap it externally',
152
+ '- delay runtime alignment and keep the skill contract aspirational',
153
+ ].join('\n'),
154
+ developmentPlanText: [
155
+ `# loopx Development Plan: ${slug}`,
156
+ '',
157
+ '## Execution Breakdown',
158
+ '',
159
+ '1. Extend plan state schema and status output.',
160
+ '2. Implement planner/architect/critic orchestration with bounded iteration.',
161
+ '3. Emit canonical and docs planning artifacts.',
162
+ '4. Add deterministic test seams and regression coverage.',
163
+ '',
164
+ '## Staffing Guidance',
165
+ '',
166
+ '- owner: plan runtime',
167
+ '- reviewer: architect and critic',
168
+ '- downstream execution: explicit later approval only',
169
+ '',
170
+ '## Sequencing',
171
+ '',
172
+ '- do not run critic before architect completes',
173
+ '- do not approve build until plan blockers are gone',
174
+ '- do not auto-launch execution from plan',
175
+ ].join('\n'),
176
+ testPlanText: [
177
+ `# loopx Test Plan: ${slug}`,
178
+ '',
179
+ '## Unit',
180
+ '',
181
+ '- state initialization for plan consensus mode',
182
+ '- docs artifact path and blocking checks',
183
+ '- planner/architect/critic review artifact recording',
184
+ '',
185
+ '## Integration',
186
+ '',
187
+ '- clarify -> plan happy path',
188
+ '- critic iterate then approve path',
189
+ '- docs missing or non-Chinese blocking path',
190
+ '',
191
+ '## Observability',
192
+ '',
193
+ '- status exposes iteration, architect review status, critic verdict, and docs blockers',
194
+ ].join('\n'),
195
+ docs: {
196
+ architecture: [
197
+ '# 架构文档',
198
+ '',
199
+ '## 目标',
200
+ '',
201
+ '- 将 plan 运行时升级为真实的 Planner / Architect / Critic 规划闭环。',
202
+ '- 在 approved plan 产出后停止,不进入执行阶段。',
203
+ '',
204
+ '## 关键边界',
205
+ '',
206
+ ...summary.decisions.map((item) => `- ${item}`),
207
+ '',
208
+ '## 关键约束',
209
+ '',
210
+ ...summary.constraints.map((item) => `- ${item}`),
211
+ ].join('\n'),
212
+ design: [
213
+ '# 设计文档',
214
+ '',
215
+ '## 设计要点',
216
+ '',
217
+ '- 引入 plan orchestration adapter,隔离真实编排与测试替身。',
218
+ '- 在 workflow state 中记录 iteration、architect review、critic verdict 和 docs blockers。',
219
+ '- 以 `.loopx/plans/` 为 canonical,以 `docs/<slug>/` 为中文规划文档输出。',
220
+ '',
221
+ '## 非目标',
222
+ '',
223
+ ...summary.nonGoals.map((item) => `- ${item}`),
224
+ ].join('\n'),
225
+ testPlan: [
226
+ '# 测试计划',
227
+ '',
228
+ '## 验证范围',
229
+ '',
230
+ ...summary.acceptance.map((item) => `- ${item}`),
231
+ '',
232
+ '## 核心回归',
233
+ '',
234
+ '- happy path: 一轮 approve 完成',
235
+ '- iterate path: Critic 先 iterate 后 approve',
236
+ '- docs blocker: 缺文件或英文占位内容都不能完成',
237
+ ].join('\n'),
238
+ },
239
+ preMortem,
240
+ principlesResolved: true,
241
+ optionsReviewed: true,
242
+ acceptanceCriteriaTestable: true,
243
+ verificationStepsResolved: true,
244
+ };
245
+ }
246
+
247
+ function reviewArtifact(kind, iteration, verdict, findings, extras = {}) {
248
+ return {
249
+ kind,
250
+ iteration,
251
+ verdict,
252
+ findings,
253
+ ...extras,
254
+ };
255
+ }
256
+
257
+ function defaultArchitectReview({ plannerDraft, iteration }) {
258
+ const findings = [
259
+ 'Real planning orchestration needs an adapter seam so production runtime and deterministic tests can share one state machine.',
260
+ 'Plan completion should depend on blocking docs outputs, not only canonical plan artifacts.',
261
+ ];
262
+ return reviewArtifact('architect', iteration, 'approve', findings, {
263
+ status: 'complete',
264
+ strongestObjection: 'Without an explicit adapter boundary, live orchestration and tests will drift or become flaky.',
265
+ tradeoffTension: 'Faithful multi-agent behavior increases runtime complexity, while deterministic tests push toward stronger adapter isolation.',
266
+ });
267
+ }
268
+
269
+ function containsChinese(text) {
270
+ return /[\u3400-\u9fff]/.test(text);
271
+ }
272
+
273
+ function defaultCriticReview({ plannerDraft, iteration }) {
274
+ const findings = [];
275
+ if (!plannerDraft.principlesResolved) {
276
+ findings.push('Planning principles are not explicit.');
277
+ }
278
+ if (!plannerDraft.optionsReviewed) {
279
+ findings.push('Alternatives are not fairly compared.');
280
+ }
281
+ if (!plannerDraft.acceptanceCriteriaTestable) {
282
+ findings.push('Acceptance criteria are not testable.');
283
+ }
284
+ if (!plannerDraft.verificationStepsResolved) {
285
+ findings.push('Verification steps are not concrete.');
286
+ }
287
+ if (!containsChinese(plannerDraft.docs.architecture) || !containsChinese(plannerDraft.docs.design) || !containsChinese(plannerDraft.docs.testPlan)) {
288
+ findings.push('Required docs outputs are not Chinese.');
289
+ }
290
+ return reviewArtifact('critic', iteration, findings.length > 0 ? 'iterate' : 'approve', findings, {
291
+ acceptanceCriteriaTestable: plannerDraft.acceptanceCriteriaTestable,
292
+ verificationStepsResolved: plannerDraft.verificationStepsResolved,
293
+ });
294
+ }
295
+
296
+ function scriptedVerdict(script, index, fallback) {
297
+ if (!Array.isArray(script) || script.length === 0) {
298
+ return fallback;
299
+ }
300
+ const boundedIndex = Math.min(index, script.length - 1);
301
+ return String(script[boundedIndex]).trim().toLowerCase();
302
+ }
303
+
304
+ function scriptedCriticReview({ plannerDraft, iteration }, script, index) {
305
+ const verdict = scriptedVerdict(script, index, 'approve');
306
+ const findings = verdict === 'approve'
307
+ ? ['Structured planning outputs satisfy the scripted approval path.']
308
+ : [`Scripted critic verdict requested: ${verdict}.`];
309
+ return reviewArtifact('critic', iteration, verdict, findings, {
310
+ acceptanceCriteriaTestable: plannerDraft.acceptanceCriteriaTestable,
311
+ verificationStepsResolved: plannerDraft.verificationStepsResolved,
312
+ });
313
+ }
314
+
315
+ export function createScriptedPlanAdapter(script = {}) {
316
+ let architectIndex = 0;
317
+ let criticIndex = 0;
318
+ return {
319
+ async planner(context) {
320
+ return plannerDraftFromSource(context);
321
+ },
322
+ async architect(context) {
323
+ const base = defaultArchitectReview(context);
324
+ const mode = scriptedVerdict(script.architect, architectIndex, 'approve');
325
+ architectIndex += 1;
326
+ return {
327
+ ...base,
328
+ status: mode === 'changes-requested' ? 'changes-requested' : 'complete',
329
+ verdict: mode,
330
+ findings: mode === 'approve' ? base.findings : [`Scripted architect verdict requested: ${mode}.`],
331
+ };
332
+ },
333
+ async critic(context) {
334
+ const result = scriptedCriticReview(context, script.critic, criticIndex);
335
+ criticIndex += 1;
336
+ return result;
337
+ },
338
+ };
339
+ }
340
+
341
+ export function createDefaultPlanAdapter() {
342
+ return createRealPlanAdapter();
343
+ }
344
+
345
+ export function createRealPlanAdapter({ model } = {}) {
346
+ return {
347
+ async planner(context) {
348
+ const outputPath = join(context.root, 'plan-reviews', `planner-iteration-${context.iteration}.json`);
349
+ await mkdir(join(context.root, 'plan-reviews'), { recursive: true });
350
+ const prompt = [
351
+ `You are acting as the real loopx plan runtime for workflow "${context.slug}".`,
352
+ 'Read the source requirements and produce planning content for this workflow.',
353
+ 'Return only raw JSON matching this shape:',
354
+ '{',
355
+ ' "principles": string[],',
356
+ ' "decisionDrivers": string[],',
357
+ ' "options": [{"name": string, "pros": string[], "cons": string[]}],',
358
+ ' "planText": string,',
359
+ ' "architectureText": string,',
360
+ ' "developmentPlanText": string,',
361
+ ' "testPlanText": string,',
362
+ ' "docs": {"architecture": string, "design": string, "testPlan": string},',
363
+ ' "principlesResolved": boolean,',
364
+ ' "optionsReviewed": boolean,',
365
+ ' "acceptanceCriteriaTestable": boolean,',
366
+ ' "verificationStepsResolved": boolean',
367
+ '}',
368
+ `Deliberate mode: ${Boolean(context.deliberateMode)}`,
369
+ '',
370
+ 'Use Chinese for docs.architecture / docs.design / docs.testPlan.',
371
+ 'Do not ask questions. Do not wrap JSON in markdown.',
372
+ '',
373
+ 'Source requirements:',
374
+ context.sourceText,
375
+ ].join('\n');
376
+ return runCodexExecJson({
377
+ cwd: context.cwd,
378
+ prompt,
379
+ outputPath,
380
+ model,
381
+ });
382
+ },
383
+ async architect(context) {
384
+ const outputPath = join(context.root, 'plan-reviews', `architect-iteration-${context.iteration}.json`);
385
+ await mkdir(join(context.root, 'plan-reviews'), { recursive: true });
386
+ const draftText = [
387
+ context.plannerDraft.planText,
388
+ '',
389
+ context.plannerDraft.architectureText,
390
+ '',
391
+ context.plannerDraft.developmentPlanText,
392
+ '',
393
+ context.plannerDraft.testPlanText,
394
+ ].join('\n');
395
+ const prompt = [
396
+ `You are acting as the real loopx architect review for workflow "${context.slug}".`,
397
+ 'Review the provided planning draft and return only raw JSON with this shape:',
398
+ '{',
399
+ ' "status": "complete" | "changes-requested",',
400
+ ' "verdict": "approve" | "iterate" | "reject",',
401
+ ' "findings": string[],',
402
+ ' "strongestObjection": string,',
403
+ ' "tradeoffTension": string',
404
+ '}',
405
+ 'Do not ask questions. Do not wrap JSON in markdown.',
406
+ '',
407
+ 'Planning draft:',
408
+ draftText,
409
+ ].join('\n');
410
+ return runCodexExecJson({
411
+ cwd: context.cwd,
412
+ prompt,
413
+ outputPath,
414
+ model,
415
+ });
416
+ },
417
+ async critic(context) {
418
+ const outputPath = join(context.root, 'plan-reviews', `critic-iteration-${context.iteration}.json`);
419
+ await mkdir(join(context.root, 'plan-reviews'), { recursive: true });
420
+ const draftText = [
421
+ context.plannerDraft.planText,
422
+ '',
423
+ context.plannerDraft.architectureText,
424
+ '',
425
+ context.plannerDraft.developmentPlanText,
426
+ '',
427
+ context.plannerDraft.testPlanText,
428
+ ].join('\n');
429
+ const prompt = [
430
+ `You are acting as the real loopx critic gate for workflow "${context.slug}".`,
431
+ 'Review the planning draft plus architect review and return only raw JSON with this shape:',
432
+ '{',
433
+ ' "verdict": "approve" | "iterate" | "reject",',
434
+ ' "findings": string[],',
435
+ ' "acceptanceCriteriaTestable": boolean,',
436
+ ' "verificationStepsResolved": boolean',
437
+ '}',
438
+ 'Do not ask questions. Do not wrap JSON in markdown.',
439
+ '',
440
+ 'Planning draft:',
441
+ draftText,
442
+ '',
443
+ 'Architect review:',
444
+ JSON.stringify(context.architectReview, null, 2),
445
+ ].join('\n');
446
+ return runCodexExecJson({
447
+ cwd: context.cwd,
448
+ prompt,
449
+ outputPath,
450
+ model,
451
+ });
452
+ },
453
+ };
454
+ }
455
+
456
+ export { DEFAULT_MAX_ITERATIONS };
@@ -4,7 +4,11 @@ import { join, resolve } from 'node:path';
4
4
 
5
5
  import { inspectInstallState, verifyInstallState } from './install-discovery.mjs';
6
6
 
7
- export function resolveLoopXRoot(cwd) {
7
+ export function resolveLoopxRoot(cwd) {
8
+ return join(resolve(cwd), '.loopx');
9
+ }
10
+
11
+ export function resolveUppercaseLoopxRoot(cwd) {
8
12
  return join(resolve(cwd), '.LoopX');
9
13
  }
10
14
 
@@ -12,22 +16,29 @@ export function resolveLegacyRoot(cwd) {
12
16
  return join(resolve(cwd), '.codex-helper');
13
17
  }
14
18
 
15
- export async function ensureLoopXRoot(cwd) {
16
- const root = resolveLoopXRoot(cwd);
19
+ export async function ensureLoopxRoot(cwd) {
20
+ const root = resolveLoopxRoot(cwd);
21
+ const uppercaseRoot = resolveUppercaseLoopxRoot(cwd);
22
+ if (!existsSync(root) && existsSync(uppercaseRoot)) {
23
+ await rename(uppercaseRoot, root);
24
+ }
17
25
  await mkdir(root, { recursive: true });
18
26
  return root;
19
27
  }
20
28
 
21
29
  export async function migrateLegacyRuntime(cwd) {
22
30
  const legacyRoot = resolveLegacyRoot(cwd);
23
- const loopxRoot = resolveLoopXRoot(cwd);
31
+ const loopxRoot = resolveLoopxRoot(cwd);
32
+ const uppercaseRoot = resolveUppercaseLoopxRoot(cwd);
24
33
  const legacyExists = existsSync(legacyRoot);
25
34
  const loopxExists = existsSync(loopxRoot);
35
+ const uppercaseExists = existsSync(uppercaseRoot);
26
36
 
27
- if (!legacyExists) {
37
+ if (!legacyExists && !uppercaseExists) {
28
38
  return {
29
39
  migrated: false,
30
40
  legacyExists: false,
41
+ uppercaseExists: false,
31
42
  loopxExists,
32
43
  loopxRoot,
33
44
  legacyRoot,
@@ -35,14 +46,28 @@ export async function migrateLegacyRuntime(cwd) {
35
46
  };
36
47
  }
37
48
 
38
- if (loopxExists) {
49
+ if (loopxExists && (legacyExists || uppercaseExists)) {
39
50
  throw new Error('mixed_runtime_roots_detected');
40
51
  }
41
52
 
53
+ if (uppercaseExists && !loopxExists) {
54
+ await rename(uppercaseRoot, loopxRoot);
55
+ return {
56
+ migrated: true,
57
+ legacyExists,
58
+ uppercaseExists: true,
59
+ loopxExists: true,
60
+ loopxRoot,
61
+ legacyRoot,
62
+ reason: 'migrated_uppercase_loopx_runtime',
63
+ };
64
+ }
65
+
42
66
  await rename(legacyRoot, loopxRoot);
43
67
  return {
44
68
  migrated: true,
45
69
  legacyExists: true,
70
+ uppercaseExists,
46
71
  loopxExists: true,
47
72
  loopxRoot,
48
73
  legacyRoot,
@@ -51,17 +76,20 @@ export async function migrateLegacyRuntime(cwd) {
51
76
  }
52
77
 
53
78
  export async function doctorRuntime(cwd, env = process.env) {
54
- const loopxRoot = resolveLoopXRoot(cwd);
79
+ const loopxRoot = resolveLoopxRoot(cwd);
55
80
  const legacyRoot = resolveLegacyRoot(cwd);
81
+ const uppercaseRoot = resolveUppercaseLoopxRoot(cwd);
56
82
  const installState = await inspectInstallState(env);
57
83
  const installCheck = await verifyInstallState(env);
58
84
 
59
85
  return {
60
86
  loopxRoot,
61
87
  legacyRoot,
88
+ uppercaseRoot,
62
89
  loopxExists: existsSync(loopxRoot),
63
90
  legacyExists: existsSync(legacyRoot),
64
- mixedRuntimeRoots: existsSync(loopxRoot) && existsSync(legacyRoot),
91
+ uppercaseExists: existsSync(uppercaseRoot),
92
+ mixedRuntimeRoots: existsSync(loopxRoot) && (existsSync(legacyRoot) || existsSync(uppercaseRoot)),
65
93
  installState,
66
94
  installCheck,
67
95
  };