@ai-content-space/loopx 0.1.2 → 0.1.4

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 (69) hide show
  1. package/README.md +422 -57
  2. package/README.zh-CN.md +485 -0
  3. package/assets/logo.svg +89 -0
  4. package/package.json +5 -1
  5. package/plugins/loopx/.codex-plugin/plugin.json +1 -1
  6. package/plugins/loopx/scripts/plugin-install.test.mjs +14 -0
  7. package/plugins/loopx/skills/archive/SKILL.md +49 -0
  8. package/plugins/loopx/skills/build/SKILL.md +111 -9
  9. package/plugins/loopx/skills/clarify/SKILL.md +129 -8
  10. package/plugins/loopx/skills/debug/SKILL.md +296 -0
  11. package/plugins/loopx/skills/debug/condition-based-waiting.md +115 -0
  12. package/plugins/loopx/skills/debug/defense-in-depth.md +122 -0
  13. package/plugins/loopx/skills/debug/find-polluter.sh +63 -0
  14. package/plugins/loopx/skills/debug/root-cause-tracing.md +169 -0
  15. package/plugins/loopx/skills/go-style/SKILL.md +71 -0
  16. package/plugins/loopx/skills/kratos/SKILL.md +74 -0
  17. package/plugins/loopx/skills/kratos/references/advanced-features.md +314 -0
  18. package/plugins/loopx/skills/kratos/references/architecture.md +488 -0
  19. package/plugins/loopx/skills/kratos/references/configuration.md +399 -0
  20. package/plugins/loopx/skills/kratos/references/http-customization.md +512 -0
  21. package/plugins/loopx/skills/kratos/references/middleware-logging.md +400 -0
  22. package/plugins/loopx/skills/kratos/references/proto-api-design.md +432 -0
  23. package/plugins/loopx/skills/kratos/references/security-auth.md +411 -0
  24. package/plugins/loopx/skills/kratos/references/troubleshooting.md +385 -0
  25. package/plugins/loopx/skills/plan/SKILL.md +24 -3
  26. package/plugins/loopx/skills/review/SKILL.md +98 -1
  27. package/plugins/loopx/skills/tdd/SKILL.md +371 -0
  28. package/plugins/loopx/skills/tdd/testing-anti-patterns.md +299 -0
  29. package/plugins/loopx/skills/verify/SKILL.md +139 -0
  30. package/scripts/codex-stop-hook.mjs +71 -0
  31. package/scripts/codex-workflow-hook.mjs +248 -0
  32. package/skills/archive/SKILL.md +49 -0
  33. package/skills/build/SKILL.md +111 -9
  34. package/skills/clarify/SKILL.md +129 -8
  35. package/skills/debug/SKILL.md +296 -0
  36. package/skills/debug/condition-based-waiting.md +115 -0
  37. package/skills/debug/defense-in-depth.md +122 -0
  38. package/skills/debug/find-polluter.sh +63 -0
  39. package/skills/debug/root-cause-tracing.md +169 -0
  40. package/skills/go-style/SKILL.md +71 -0
  41. package/skills/kratos/SKILL.md +74 -0
  42. package/skills/kratos/references/advanced-features.md +314 -0
  43. package/skills/kratos/references/architecture.md +488 -0
  44. package/skills/kratos/references/configuration.md +399 -0
  45. package/skills/kratos/references/http-customization.md +512 -0
  46. package/skills/kratos/references/middleware-logging.md +400 -0
  47. package/skills/kratos/references/proto-api-design.md +432 -0
  48. package/skills/kratos/references/security-auth.md +411 -0
  49. package/skills/kratos/references/troubleshooting.md +385 -0
  50. package/skills/plan/SKILL.md +20 -3
  51. package/skills/review/SKILL.md +98 -1
  52. package/skills/tdd/SKILL.md +371 -0
  53. package/skills/tdd/testing-anti-patterns.md +299 -0
  54. package/skills/verify/SKILL.md +139 -0
  55. package/src/build-runtime.mjs +311 -26
  56. package/src/build-stop-gate.mjs +94 -0
  57. package/src/cli.mjs +57 -5
  58. package/src/codex-exec-runtime.mjs +105 -5
  59. package/src/context-manifest.mjs +172 -0
  60. package/src/html-views.mjs +316 -0
  61. package/src/install-discovery.mjs +352 -5
  62. package/src/next-skill.mjs +57 -5
  63. package/src/plan-runtime.mjs +102 -122
  64. package/src/review-runtime.mjs +558 -0
  65. package/src/runtime-maintenance.mjs +429 -14
  66. package/src/template-governance.mjs +223 -0
  67. package/src/workflow.mjs +2341 -120
  68. package/src/workspace-context.mjs +166 -0
  69. package/src/workspace-memory.mjs +69 -0
@@ -1,8 +1,51 @@
1
- import { mkdir, rename } from 'node:fs/promises';
2
- import { existsSync } from 'node:fs';
3
- import { join, resolve } from 'node:path';
1
+ import { mkdir, readFile, readdir, rename, writeFile } from 'node:fs/promises';
2
+ import { existsSync, readdirSync } from 'node:fs';
3
+ import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
4
4
 
5
- import { inspectInstallState, verifyInstallState } from './install-discovery.mjs';
5
+ import { getTemplateBaselinePath, inspectInstallState, verifyInstallState } from './install-discovery.mjs';
6
+ import { inspectTemplateGovernance } from './template-governance.mjs';
7
+ import { inspectWorkspaceContext } from './workspace-context.mjs';
8
+
9
+ const WORKFLOW_SCHEMA_VERSION = 1;
10
+
11
+ const STAGES = {
12
+ CLARIFY: 'clarify',
13
+ PLAN: 'plan',
14
+ BUILD: 'build',
15
+ REVIEW: 'review',
16
+ DONE: 'done',
17
+ };
18
+
19
+ const APPROVAL_STATES = {
20
+ NOT_REQUESTED: 'not-requested',
21
+ REQUESTED: 'requested',
22
+ APPROVED: 'approved',
23
+ };
24
+
25
+ const TRANSITIONS = {
26
+ NONE: 'none',
27
+ CLARIFY_TO_PLAN: 'clarify->plan',
28
+ PLAN_TO_BUILD: 'plan->build',
29
+ BUILD_TO_REVIEW: 'build->review',
30
+ REVIEW_TO_DONE: 'review->done',
31
+ };
32
+
33
+ const CHANGE_ARTIFACT_FILE_MAP = {
34
+ proposal: 'proposal.md',
35
+ specDelta: 'spec-delta.md',
36
+ design: 'design.md',
37
+ tasks: 'tasks.md',
38
+ slices: 'slices.json',
39
+ graph: 'artifact-graph.json',
40
+ };
41
+
42
+ function normalizeSlug(raw) {
43
+ return String(raw || '')
44
+ .trim()
45
+ .toLowerCase()
46
+ .replace(/[^a-z0-9]+/g, '-')
47
+ .replace(/^-+|-+$/g, '');
48
+ }
6
49
 
7
50
  export function resolveLoopxRoot(cwd) {
8
51
  return join(resolve(cwd), '.loopx');
@@ -16,33 +59,384 @@ export function resolveLegacyRoot(cwd) {
16
59
  return join(resolve(cwd), '.codex-helper');
17
60
  }
18
61
 
62
+ function existsExactPath(path) {
63
+ const parent = dirname(path);
64
+ const name = basename(path);
65
+ if (!existsSync(parent)) {
66
+ return false;
67
+ }
68
+ try {
69
+ return readdirSync(parent).includes(name);
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+
19
75
  export async function ensureLoopxRoot(cwd) {
20
76
  const root = resolveLoopxRoot(cwd);
21
77
  const uppercaseRoot = resolveUppercaseLoopxRoot(cwd);
22
- if (!existsSync(root) && existsSync(uppercaseRoot)) {
78
+ if (!existsExactPath(root) && existsExactPath(uppercaseRoot)) {
23
79
  await rename(uppercaseRoot, root);
24
80
  }
25
81
  await mkdir(root, { recursive: true });
26
82
  return root;
27
83
  }
28
84
 
85
+ async function readJsonIfExists(path) {
86
+ if (!existsSync(path)) {
87
+ return null;
88
+ }
89
+ return JSON.parse(await readFile(path, 'utf8'));
90
+ }
91
+
92
+ async function readTextIfExists(path) {
93
+ if (!existsSync(path)) {
94
+ return null;
95
+ }
96
+ return readFile(path, 'utf8');
97
+ }
98
+
99
+ function parseFrontmatter(text) {
100
+ if (!text?.startsWith('---\n')) {
101
+ return {};
102
+ }
103
+ const end = text.indexOf('\n---\n', 4);
104
+ if (end === -1) {
105
+ return {};
106
+ }
107
+ const result = {};
108
+ for (const line of text.slice(4, end).split('\n')) {
109
+ if (!line || /^\s/.test(line)) {
110
+ continue;
111
+ }
112
+ const separator = line.indexOf(':');
113
+ if (separator === -1) {
114
+ continue;
115
+ }
116
+ const key = line.slice(0, separator).trim();
117
+ const rawValue = line.slice(separator + 1).trim();
118
+ if (rawValue === 'true' || rawValue === 'false') {
119
+ result[key] = rawValue === 'true';
120
+ } else {
121
+ result[key] = rawValue;
122
+ }
123
+ }
124
+ return result;
125
+ }
126
+
127
+ function resolveRuntimePath(cwd, rawPath, fallback) {
128
+ if (!rawPath) {
129
+ return fallback;
130
+ }
131
+ if (isAbsolute(rawPath)) {
132
+ return rawPath;
133
+ }
134
+ return resolve(cwd, rawPath);
135
+ }
136
+
137
+ function artifactPathFromGraph(cwd, graph, key, fallback) {
138
+ const snakeKey = key === 'specDelta' ? 'spec_delta' : key;
139
+ const rawPath = graph?.change_artifacts?.[snakeKey] || graph?.artifacts?.[key]?.path;
140
+ return resolveRuntimePath(cwd, rawPath, fallback);
141
+ }
142
+
143
+ function createChangeArtifactPaths(cwd, changeRoot, graph = null) {
144
+ return {
145
+ root: changeRoot,
146
+ proposal: artifactPathFromGraph(cwd, graph, 'proposal', join(changeRoot, CHANGE_ARTIFACT_FILE_MAP.proposal)),
147
+ specDelta: artifactPathFromGraph(cwd, graph, 'specDelta', join(changeRoot, CHANGE_ARTIFACT_FILE_MAP.specDelta)),
148
+ design: artifactPathFromGraph(cwd, graph, 'design', join(changeRoot, CHANGE_ARTIFACT_FILE_MAP.design)),
149
+ tasks: artifactPathFromGraph(cwd, graph, 'tasks', join(changeRoot, CHANGE_ARTIFACT_FILE_MAP.tasks)),
150
+ graph: artifactPathFromGraph(cwd, graph, 'graph', join(changeRoot, CHANGE_ARTIFACT_FILE_MAP.graph)),
151
+ };
152
+ }
153
+
154
+ async function findActiveChangeForWorkflow(cwd, slug) {
155
+ const normalized = normalizeSlug(slug);
156
+ const activeRoot = join(resolveLoopxRoot(cwd), 'changes', 'active');
157
+ if (!existsSync(activeRoot)) {
158
+ return null;
159
+ }
160
+ const entries = await readdir(activeRoot, { withFileTypes: true });
161
+ const candidates = [];
162
+ for (const entry of entries) {
163
+ if (!entry.isDirectory()) {
164
+ continue;
165
+ }
166
+ const changeRoot = join(activeRoot, entry.name);
167
+ const graphPath = join(changeRoot, CHANGE_ARTIFACT_FILE_MAP.graph);
168
+ const graph = await readJsonIfExists(graphPath);
169
+ const specDeltaPath = join(changeRoot, CHANGE_ARTIFACT_FILE_MAP.specDelta);
170
+ const specDeltaText = await readTextIfExists(specDeltaPath);
171
+ const specDeltaMeta = parseFrontmatter(specDeltaText);
172
+ let score = 0;
173
+ if (normalizeSlug(graph?.slug || graph?.workflow) === normalized) {
174
+ score += 100;
175
+ }
176
+ if (normalizeSlug(specDeltaMeta.slug) === normalized) {
177
+ score += 80;
178
+ }
179
+ if (normalizeSlug(entry.name).startsWith(`${normalized}-`) || normalizeSlug(entry.name) === normalized) {
180
+ score += 20;
181
+ }
182
+ if (score === 0) {
183
+ continue;
184
+ }
185
+ const paths = createChangeArtifactPaths(cwd, changeRoot, graph);
186
+ const changeId = normalizeSlug(graph?.change_id || graph?.change || specDeltaMeta.change_id || entry.name);
187
+ candidates.push({ score, changeId, paths, rootName: entry.name });
188
+ }
189
+ candidates.sort((left, right) => right.score - left.score || right.rootName.localeCompare(left.rootName));
190
+ return candidates[0] || null;
191
+ }
192
+
193
+ async function inferReviewState(workflowRoot) {
194
+ const reviewText = await readTextIfExists(join(workflowRoot, 'review.md'))
195
+ || await readTextIfExists(join(workflowRoot, 'review-report.md'))
196
+ || '';
197
+ const reviewMeta = parseFrontmatter(reviewText);
198
+ const rawVerdict = String(reviewMeta.verdict || '').toLowerCase();
199
+ const textVerdict = /(^|\n)\s*(REQUEST\s+CHANGES|NO-?GO)\s*($|\n)/i.test(reviewText)
200
+ ? 'request-changes'
201
+ : /(^|\n)\s*(APPROVE|GO)\s*($|\n)/i.test(reviewText)
202
+ ? 'approve'
203
+ : 'none';
204
+ const reviewVerdict = rawVerdict === 'go' || rawVerdict.includes('approve')
205
+ ? 'approve'
206
+ : rawVerdict.includes('request') || rawVerdict === 'no-go' || rawVerdict === 'nogo'
207
+ ? 'request-changes'
208
+ : textVerdict;
209
+ if (reviewVerdict === 'approve') {
210
+ return {
211
+ current_stage: STAGES.REVIEW,
212
+ stage_status: 'awaiting-approval',
213
+ review_status: 'in-review',
214
+ review_verdict: 'approve',
215
+ rollback_target: 'none',
216
+ rollback_rationale: null,
217
+ pending_user_decision: TRANSITIONS.REVIEW_TO_DONE,
218
+ requested_transition: TRANSITIONS.NONE,
219
+ last_confirmed_transition: TRANSITIONS.BUILD_TO_REVIEW,
220
+ approval: {
221
+ plan: APPROVAL_STATES.APPROVED,
222
+ build: APPROVAL_STATES.APPROVED,
223
+ review: APPROVAL_STATES.APPROVED,
224
+ rollback: APPROVAL_STATES.NOT_REQUESTED,
225
+ complete: APPROVAL_STATES.REQUESTED,
226
+ },
227
+ };
228
+ }
229
+ return null;
230
+ }
231
+
232
+ async function inferExecutionStatus(workflowRoot) {
233
+ const text = await readTextIfExists(join(workflowRoot, 'execution-record.md'));
234
+ if (!text) {
235
+ return 'missing';
236
+ }
237
+ const meta = parseFrontmatter(text);
238
+ if (meta.execution_approved_for_review === true || meta.status === 'review-ready' || /## Verification Evidence/i.test(text)) {
239
+ return 'complete';
240
+ }
241
+ return 'partial';
242
+ }
243
+
244
+ function createMigratedWorkflowBaseState(slug, legacyState, change) {
245
+ const profile = legacyState.clarify_profile || legacyState.profile || 'standard';
246
+ return {
247
+ schema_version: WORKFLOW_SCHEMA_VERSION,
248
+ slug,
249
+ current_stage: STAGES.CLARIFY,
250
+ stage_status: 'blocked',
251
+ clarify_profile: profile,
252
+ clarify_target_ambiguity_threshold: legacyState.clarify_target_ambiguity_threshold ?? 0.2,
253
+ clarify_max_rounds: legacyState.clarify_max_rounds ?? 15,
254
+ clarify_current_round: legacyState.clarify_current_round ?? legacyState.current_round ?? 0,
255
+ clarify_ambiguity_score: legacyState.clarify_ambiguity_score ?? legacyState.ambiguity_score ?? 1,
256
+ clarify_pressure_pass_complete: Boolean(legacyState.clarify_pressure_pass_complete ?? legacyState.pressure_pass_complete),
257
+ clarify_non_goals_resolved: Boolean(legacyState.clarify_non_goals_resolved ?? legacyState.non_goals_resolved),
258
+ clarify_decision_boundaries_resolved: Boolean(legacyState.clarify_decision_boundaries_resolved ?? legacyState.decision_boundaries_resolved),
259
+ ambiguity_items: Array.isArray(legacyState.ambiguity_items) ? legacyState.ambiguity_items : [],
260
+ unresolved_ambiguity_count: Number(legacyState.unresolved_ambiguity_count ?? 0),
261
+ plan_package_status: 'missing',
262
+ plan_current_iteration: 0,
263
+ plan_max_iterations: 3,
264
+ plan_consensus_mode: true,
265
+ plan_deliberate_mode: false,
266
+ plan_interactive_mode: false,
267
+ plan_principles_resolved: false,
268
+ plan_options_reviewed: false,
269
+ plan_architect_review_status: 'not-started',
270
+ plan_critic_verdict: 'none',
271
+ plan_acceptance_criteria_testable: false,
272
+ plan_verification_steps_resolved: false,
273
+ plan_execution_inputs_resolved: false,
274
+ plan_docs_status: 'missing',
275
+ plan_docs_artifact_paths: null,
276
+ plan_review_artifact_paths: [],
277
+ plan_review_history: [],
278
+ plan_blockers: [],
279
+ plan_source_spec_path: null,
280
+ change_id: change?.changeId || `chg-${slug}`,
281
+ change_artifacts_status: change ? 'complete' : 'missing',
282
+ change_artifact_paths: change?.paths || null,
283
+ spec_delta_status: change ? 'complete' : 'missing',
284
+ spec_sync_status: 'pending',
285
+ archive_status: 'pending',
286
+ archived_change_path: null,
287
+ archived_spec_paths: [],
288
+ build_run_id: null,
289
+ build_current_iteration: 0,
290
+ build_max_iterations: 5,
291
+ build_parallel_mode: false,
292
+ build_lane_statuses: [],
293
+ build_verification_status: 'pending',
294
+ build_architect_verification_status: 'not-started',
295
+ build_deslop_status: 'pending',
296
+ build_regression_status: 'pending',
297
+ build_blockers: [],
298
+ build_progress_artifact_paths: [],
299
+ build_support_evidence_paths: [],
300
+ build_no_deslop: false,
301
+ autopilot_current_phase: 'none',
302
+ autopilot_phase_history: [],
303
+ autopilot_blockers: [],
304
+ autopilot_run_path: null,
305
+ autopilot_completed: false,
306
+ review_status: 'not-started',
307
+ rollback_target: 'none',
308
+ rollback_rationale: null,
309
+ pending_user_decision: TRANSITIONS.NONE,
310
+ requested_transition: TRANSITIONS.NONE,
311
+ last_confirmed_transition: TRANSITIONS.NONE,
312
+ approval: {
313
+ plan: APPROVAL_STATES.NOT_REQUESTED,
314
+ build: APPROVAL_STATES.NOT_REQUESTED,
315
+ review: APPROVAL_STATES.NOT_REQUESTED,
316
+ rollback: APPROVAL_STATES.NOT_REQUESTED,
317
+ complete: APPROVAL_STATES.NOT_REQUESTED,
318
+ },
319
+ execution_record_status: 'missing',
320
+ review_verdict: 'none',
321
+ completion_confirmed: false,
322
+ active_run_id: null,
323
+ spec_artifact_path: null,
324
+ plan_artifact_path: null,
325
+ test_spec_artifact_path: null,
326
+ };
327
+ }
328
+
329
+ async function migrateLegacyWorkflowState(cwd, slug, workflowRoot, legacyState) {
330
+ const change = await findActiveChangeForWorkflow(cwd, slug);
331
+ const reviewState = await inferReviewState(workflowRoot);
332
+ const canonicalPlanPath = join(resolveLoopxRoot(cwd), 'plans', `prd-${slug}.md`);
333
+ const canonicalTestSpecPath = join(resolveLoopxRoot(cwd), 'plans', `test-spec-${slug}.md`);
334
+ const baseState = createMigratedWorkflowBaseState(slug, legacyState, change);
335
+ const planDocsComplete = ['plan.md', 'architecture.md', 'development-plan.md', 'test-plan.md']
336
+ .every((name) => existsSync(join(workflowRoot, name)));
337
+ const executionRecordStatus = await inferExecutionStatus(workflowRoot);
338
+ const planState = planDocsComplete ? {
339
+ current_stage: STAGES.PLAN,
340
+ stage_status: 'awaiting-approval',
341
+ plan_package_status: 'complete',
342
+ plan_current_iteration: 1,
343
+ plan_principles_resolved: true,
344
+ plan_options_reviewed: true,
345
+ plan_architect_review_status: 'complete',
346
+ plan_critic_verdict: 'approve',
347
+ plan_acceptance_criteria_testable: true,
348
+ plan_verification_steps_resolved: true,
349
+ plan_execution_inputs_resolved: true,
350
+ plan_docs_status: 'complete',
351
+ approval: {
352
+ plan: APPROVAL_STATES.APPROVED,
353
+ build: APPROVAL_STATES.NOT_REQUESTED,
354
+ review: APPROVAL_STATES.NOT_REQUESTED,
355
+ rollback: APPROVAL_STATES.NOT_REQUESTED,
356
+ complete: APPROVAL_STATES.NOT_REQUESTED,
357
+ },
358
+ } : {};
359
+ const buildState = executionRecordStatus === 'complete' ? {
360
+ current_stage: STAGES.BUILD,
361
+ stage_status: 'awaiting-approval',
362
+ build_current_iteration: 1,
363
+ build_parallel_mode: true,
364
+ build_verification_status: 'complete',
365
+ build_architect_verification_status: 'approved',
366
+ build_deslop_status: 'complete',
367
+ build_regression_status: 'passed',
368
+ review_status: 'ready-for-review',
369
+ execution_record_status: 'complete',
370
+ approval: {
371
+ ...(planState.approval || baseState.approval),
372
+ build: APPROVAL_STATES.APPROVED,
373
+ review: APPROVAL_STATES.NOT_REQUESTED,
374
+ },
375
+ } : {};
376
+ const migrated = {
377
+ ...baseState,
378
+ ...legacyState,
379
+ ...planState,
380
+ ...buildState,
381
+ schema_version: WORKFLOW_SCHEMA_VERSION,
382
+ slug,
383
+ clarify_profile: legacyState.clarify_profile || legacyState.profile || 'standard',
384
+ plan_artifact_path: existsSync(canonicalPlanPath) ? canonicalPlanPath : join(workflowRoot, 'plan.md'),
385
+ test_spec_artifact_path: existsSync(canonicalTestSpecPath) ? canonicalTestSpecPath : join(workflowRoot, 'test-plan.md'),
386
+ execution_record_status: executionRecordStatus,
387
+ ...(reviewState || {}),
388
+ };
389
+ await writeFile(join(workflowRoot, 'state.json'), `${JSON.stringify(migrated, null, 2)}\n`);
390
+ return {
391
+ slug,
392
+ migrated: true,
393
+ reason: 'migrated_legacy_workflow_schema',
394
+ current_stage: migrated.current_stage,
395
+ change_id: migrated.change_id,
396
+ };
397
+ }
398
+
399
+ async function migrateLegacyWorkflowStates(cwd) {
400
+ const workflowsRoot = join(resolveLoopxRoot(cwd), 'workflows');
401
+ if (!existsSync(workflowsRoot)) {
402
+ return [];
403
+ }
404
+ const entries = await readdir(workflowsRoot, { withFileTypes: true });
405
+ const migrations = [];
406
+ for (const entry of entries) {
407
+ if (!entry.isDirectory()) {
408
+ continue;
409
+ }
410
+ const slug = normalizeSlug(entry.name);
411
+ const workflowRoot = join(workflowsRoot, entry.name);
412
+ const state = await readJsonIfExists(join(workflowRoot, 'state.json'));
413
+ if (!state || state.schema_version === WORKFLOW_SCHEMA_VERSION) {
414
+ continue;
415
+ }
416
+ migrations.push(await migrateLegacyWorkflowState(cwd, slug, workflowRoot, state));
417
+ }
418
+ return migrations;
419
+ }
420
+
29
421
  export async function migrateLegacyRuntime(cwd) {
30
422
  const legacyRoot = resolveLegacyRoot(cwd);
31
423
  const loopxRoot = resolveLoopxRoot(cwd);
32
424
  const uppercaseRoot = resolveUppercaseLoopxRoot(cwd);
33
- const legacyExists = existsSync(legacyRoot);
34
- const loopxExists = existsSync(loopxRoot);
35
- const uppercaseExists = existsSync(uppercaseRoot);
425
+ const legacyExists = existsExactPath(legacyRoot);
426
+ const loopxExists = existsExactPath(loopxRoot);
427
+ const uppercaseExists = existsExactPath(uppercaseRoot);
36
428
 
37
429
  if (!legacyExists && !uppercaseExists) {
430
+ const workflowStateMigrations = loopxExists ? await migrateLegacyWorkflowStates(cwd) : [];
38
431
  return {
39
- migrated: false,
432
+ migrated: workflowStateMigrations.length > 0,
40
433
  legacyExists: false,
41
434
  uppercaseExists: false,
42
435
  loopxExists,
43
436
  loopxRoot,
44
437
  legacyRoot,
45
- reason: 'legacy_root_missing',
438
+ workflowStateMigrations,
439
+ reason: workflowStateMigrations.length > 0 ? 'migrated_legacy_workflow_schema' : 'legacy_root_missing',
46
440
  };
47
441
  }
48
442
 
@@ -52,6 +446,7 @@ export async function migrateLegacyRuntime(cwd) {
52
446
 
53
447
  if (uppercaseExists && !loopxExists) {
54
448
  await rename(uppercaseRoot, loopxRoot);
449
+ const workflowStateMigrations = await migrateLegacyWorkflowStates(cwd);
55
450
  return {
56
451
  migrated: true,
57
452
  legacyExists,
@@ -59,11 +454,13 @@ export async function migrateLegacyRuntime(cwd) {
59
454
  loopxExists: true,
60
455
  loopxRoot,
61
456
  legacyRoot,
457
+ workflowStateMigrations,
62
458
  reason: 'migrated_uppercase_loopx_runtime',
63
459
  };
64
460
  }
65
461
 
66
462
  await rename(legacyRoot, loopxRoot);
463
+ const workflowStateMigrations = await migrateLegacyWorkflowStates(cwd);
67
464
  return {
68
465
  migrated: true,
69
466
  legacyExists: true,
@@ -71,6 +468,7 @@ export async function migrateLegacyRuntime(cwd) {
71
468
  loopxExists: true,
72
469
  loopxRoot,
73
470
  legacyRoot,
471
+ workflowStateMigrations,
74
472
  reason: 'migrated_legacy_runtime',
75
473
  };
76
474
  }
@@ -81,16 +479,33 @@ export async function doctorRuntime(cwd, env = process.env) {
81
479
  const uppercaseRoot = resolveUppercaseLoopxRoot(cwd);
82
480
  const installState = await inspectInstallState(env);
83
481
  const installCheck = await verifyInstallState(env);
482
+ const installTemplateBaselinePath = getTemplateBaselinePath(env);
483
+ const workspaceTemplateBaselinePath = join(loopxRoot, 'template-hashes.json');
484
+ const templateGovernance = await inspectTemplateGovernance(
485
+ existsSync(installTemplateBaselinePath) ? installTemplateBaselinePath : workspaceTemplateBaselinePath,
486
+ );
487
+ const workflowHookPath = join(resolve(cwd), 'scripts', 'codex-workflow-hook.mjs');
488
+ const installedWorkflowHookPath = installState.managedArtifacts?.['codex-workflow-hook']?.targetPath
489
+ || join(resolve(env.LOOPX_HOME || env.HOME || process.cwd()), '.codex', 'hooks', 'codex-workflow-hook.mjs');
490
+ const hook = {
491
+ enabled: env.LOOPX_HOOKS !== '0',
492
+ workflowHookPath,
493
+ installedWorkflowHookPath,
494
+ installed: existsSync(installedWorkflowHookPath),
495
+ };
84
496
 
85
497
  return {
86
498
  loopxRoot,
87
499
  legacyRoot,
88
500
  uppercaseRoot,
89
- loopxExists: existsSync(loopxRoot),
90
- legacyExists: existsSync(legacyRoot),
91
- uppercaseExists: existsSync(uppercaseRoot),
92
- mixedRuntimeRoots: existsSync(loopxRoot) && (existsSync(legacyRoot) || existsSync(uppercaseRoot)),
501
+ loopxExists: existsExactPath(loopxRoot),
502
+ legacyExists: existsExactPath(legacyRoot),
503
+ uppercaseExists: existsExactPath(uppercaseRoot),
504
+ mixedRuntimeRoots: existsExactPath(loopxRoot) && (existsExactPath(legacyRoot) || existsExactPath(uppercaseRoot)),
93
505
  installState,
94
506
  installCheck,
507
+ templateGovernance,
508
+ contextSetup: await inspectWorkspaceContext(cwd),
509
+ hook,
95
510
  };
96
511
  }