@ai-content-space/loopx 0.1.1 → 0.1.3

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 (68) hide show
  1. package/README.md +343 -56
  2. package/README.zh-CN.md +392 -0
  3. package/package.json +4 -1
  4. package/plugins/loopx/.codex-plugin/plugin.json +1 -1
  5. package/plugins/loopx/scripts/plugin-install.test.mjs +1 -0
  6. package/plugins/loopx/skills/archive/SKILL.md +39 -0
  7. package/plugins/loopx/skills/build/SKILL.md +111 -9
  8. package/plugins/loopx/skills/clarify/SKILL.md +121 -1
  9. package/plugins/loopx/skills/debug/SKILL.md +296 -0
  10. package/plugins/loopx/skills/debug/condition-based-waiting.md +115 -0
  11. package/plugins/loopx/skills/debug/defense-in-depth.md +122 -0
  12. package/plugins/loopx/skills/debug/find-polluter.sh +63 -0
  13. package/plugins/loopx/skills/debug/root-cause-tracing.md +169 -0
  14. package/plugins/loopx/skills/go-style/SKILL.md +71 -0
  15. package/plugins/loopx/skills/kratos/SKILL.md +74 -0
  16. package/plugins/loopx/skills/kratos/references/advanced-features.md +314 -0
  17. package/plugins/loopx/skills/kratos/references/architecture.md +488 -0
  18. package/plugins/loopx/skills/kratos/references/configuration.md +399 -0
  19. package/plugins/loopx/skills/kratos/references/http-customization.md +512 -0
  20. package/plugins/loopx/skills/kratos/references/middleware-logging.md +400 -0
  21. package/plugins/loopx/skills/kratos/references/proto-api-design.md +432 -0
  22. package/plugins/loopx/skills/kratos/references/security-auth.md +411 -0
  23. package/plugins/loopx/skills/kratos/references/troubleshooting.md +385 -0
  24. package/plugins/loopx/skills/plan/SKILL.md +22 -2
  25. package/plugins/loopx/skills/review/SKILL.md +98 -1
  26. package/plugins/loopx/skills/tdd/SKILL.md +371 -0
  27. package/plugins/loopx/skills/tdd/testing-anti-patterns.md +299 -0
  28. package/plugins/loopx/skills/verify/SKILL.md +139 -0
  29. package/scripts/codex-stop-hook.mjs +71 -0
  30. package/scripts/codex-workflow-hook.mjs +153 -0
  31. package/skills/archive/SKILL.md +39 -0
  32. package/skills/build/SKILL.md +111 -9
  33. package/skills/clarify/SKILL.md +121 -1
  34. package/skills/debug/SKILL.md +296 -0
  35. package/skills/debug/condition-based-waiting.md +115 -0
  36. package/skills/debug/defense-in-depth.md +122 -0
  37. package/skills/debug/find-polluter.sh +63 -0
  38. package/skills/debug/root-cause-tracing.md +169 -0
  39. package/skills/go-style/SKILL.md +71 -0
  40. package/skills/kratos/SKILL.md +74 -0
  41. package/skills/kratos/references/advanced-features.md +314 -0
  42. package/skills/kratos/references/architecture.md +488 -0
  43. package/skills/kratos/references/configuration.md +399 -0
  44. package/skills/kratos/references/http-customization.md +512 -0
  45. package/skills/kratos/references/middleware-logging.md +400 -0
  46. package/skills/kratos/references/proto-api-design.md +432 -0
  47. package/skills/kratos/references/security-auth.md +411 -0
  48. package/skills/kratos/references/troubleshooting.md +385 -0
  49. package/skills/plan/SKILL.md +22 -2
  50. package/skills/review/SKILL.md +98 -1
  51. package/skills/tdd/SKILL.md +371 -0
  52. package/skills/tdd/testing-anti-patterns.md +299 -0
  53. package/skills/verify/SKILL.md +139 -0
  54. package/src/build-runtime.mjs +303 -26
  55. package/src/build-stop-gate.mjs +94 -0
  56. package/src/cli.mjs +51 -8
  57. package/src/codex-exec-runtime.mjs +105 -5
  58. package/src/context-manifest.mjs +172 -0
  59. package/src/install-discovery.mjs +352 -5
  60. package/src/next-skill.mjs +85 -0
  61. package/src/plan-runtime.mjs +100 -122
  62. package/src/review-runtime.mjs +378 -0
  63. package/src/runtime-maintenance.mjs +428 -14
  64. package/src/template-governance.mjs +223 -0
  65. package/src/workflow.mjs +1947 -118
  66. package/src/workspace-context.mjs +166 -0
  67. package/src/workspace-memory.mjs +69 -0
  68. package/templates/plan.md +6 -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,383 @@ 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_blockers: [],
278
+ plan_source_spec_path: null,
279
+ change_id: change?.changeId || `chg-${slug}`,
280
+ change_artifacts_status: change ? 'complete' : 'missing',
281
+ change_artifact_paths: change?.paths || null,
282
+ spec_delta_status: change ? 'complete' : 'missing',
283
+ spec_sync_status: 'pending',
284
+ archive_status: 'pending',
285
+ archived_change_path: null,
286
+ archived_spec_paths: [],
287
+ build_run_id: null,
288
+ build_current_iteration: 0,
289
+ build_max_iterations: 5,
290
+ build_parallel_mode: false,
291
+ build_lane_statuses: [],
292
+ build_verification_status: 'pending',
293
+ build_architect_verification_status: 'not-started',
294
+ build_deslop_status: 'pending',
295
+ build_regression_status: 'pending',
296
+ build_blockers: [],
297
+ build_progress_artifact_paths: [],
298
+ build_support_evidence_paths: [],
299
+ build_no_deslop: false,
300
+ autopilot_current_phase: 'none',
301
+ autopilot_phase_history: [],
302
+ autopilot_blockers: [],
303
+ autopilot_run_path: null,
304
+ autopilot_completed: false,
305
+ review_status: 'not-started',
306
+ rollback_target: 'none',
307
+ rollback_rationale: null,
308
+ pending_user_decision: TRANSITIONS.NONE,
309
+ requested_transition: TRANSITIONS.NONE,
310
+ last_confirmed_transition: TRANSITIONS.NONE,
311
+ approval: {
312
+ plan: APPROVAL_STATES.NOT_REQUESTED,
313
+ build: APPROVAL_STATES.NOT_REQUESTED,
314
+ review: APPROVAL_STATES.NOT_REQUESTED,
315
+ rollback: APPROVAL_STATES.NOT_REQUESTED,
316
+ complete: APPROVAL_STATES.NOT_REQUESTED,
317
+ },
318
+ execution_record_status: 'missing',
319
+ review_verdict: 'none',
320
+ completion_confirmed: false,
321
+ active_run_id: null,
322
+ spec_artifact_path: null,
323
+ plan_artifact_path: null,
324
+ test_spec_artifact_path: null,
325
+ };
326
+ }
327
+
328
+ async function migrateLegacyWorkflowState(cwd, slug, workflowRoot, legacyState) {
329
+ const change = await findActiveChangeForWorkflow(cwd, slug);
330
+ const reviewState = await inferReviewState(workflowRoot);
331
+ const canonicalPlanPath = join(resolveLoopxRoot(cwd), 'plans', `prd-${slug}.md`);
332
+ const canonicalTestSpecPath = join(resolveLoopxRoot(cwd), 'plans', `test-spec-${slug}.md`);
333
+ const baseState = createMigratedWorkflowBaseState(slug, legacyState, change);
334
+ const planDocsComplete = ['plan.md', 'architecture.md', 'development-plan.md', 'test-plan.md']
335
+ .every((name) => existsSync(join(workflowRoot, name)));
336
+ const executionRecordStatus = await inferExecutionStatus(workflowRoot);
337
+ const planState = planDocsComplete ? {
338
+ current_stage: STAGES.PLAN,
339
+ stage_status: 'awaiting-approval',
340
+ plan_package_status: 'complete',
341
+ plan_current_iteration: 1,
342
+ plan_principles_resolved: true,
343
+ plan_options_reviewed: true,
344
+ plan_architect_review_status: 'complete',
345
+ plan_critic_verdict: 'approve',
346
+ plan_acceptance_criteria_testable: true,
347
+ plan_verification_steps_resolved: true,
348
+ plan_execution_inputs_resolved: true,
349
+ plan_docs_status: 'complete',
350
+ approval: {
351
+ plan: APPROVAL_STATES.APPROVED,
352
+ build: APPROVAL_STATES.NOT_REQUESTED,
353
+ review: APPROVAL_STATES.NOT_REQUESTED,
354
+ rollback: APPROVAL_STATES.NOT_REQUESTED,
355
+ complete: APPROVAL_STATES.NOT_REQUESTED,
356
+ },
357
+ } : {};
358
+ const buildState = executionRecordStatus === 'complete' ? {
359
+ current_stage: STAGES.BUILD,
360
+ stage_status: 'awaiting-approval',
361
+ build_current_iteration: 1,
362
+ build_parallel_mode: true,
363
+ build_verification_status: 'complete',
364
+ build_architect_verification_status: 'approved',
365
+ build_deslop_status: 'complete',
366
+ build_regression_status: 'passed',
367
+ review_status: 'ready-for-review',
368
+ execution_record_status: 'complete',
369
+ approval: {
370
+ ...(planState.approval || baseState.approval),
371
+ build: APPROVAL_STATES.APPROVED,
372
+ review: APPROVAL_STATES.NOT_REQUESTED,
373
+ },
374
+ } : {};
375
+ const migrated = {
376
+ ...baseState,
377
+ ...legacyState,
378
+ ...planState,
379
+ ...buildState,
380
+ schema_version: WORKFLOW_SCHEMA_VERSION,
381
+ slug,
382
+ clarify_profile: legacyState.clarify_profile || legacyState.profile || 'standard',
383
+ plan_artifact_path: existsSync(canonicalPlanPath) ? canonicalPlanPath : join(workflowRoot, 'plan.md'),
384
+ test_spec_artifact_path: existsSync(canonicalTestSpecPath) ? canonicalTestSpecPath : join(workflowRoot, 'test-plan.md'),
385
+ execution_record_status: executionRecordStatus,
386
+ ...(reviewState || {}),
387
+ };
388
+ await writeFile(join(workflowRoot, 'state.json'), `${JSON.stringify(migrated, null, 2)}\n`);
389
+ return {
390
+ slug,
391
+ migrated: true,
392
+ reason: 'migrated_legacy_workflow_schema',
393
+ current_stage: migrated.current_stage,
394
+ change_id: migrated.change_id,
395
+ };
396
+ }
397
+
398
+ async function migrateLegacyWorkflowStates(cwd) {
399
+ const workflowsRoot = join(resolveLoopxRoot(cwd), 'workflows');
400
+ if (!existsSync(workflowsRoot)) {
401
+ return [];
402
+ }
403
+ const entries = await readdir(workflowsRoot, { withFileTypes: true });
404
+ const migrations = [];
405
+ for (const entry of entries) {
406
+ if (!entry.isDirectory()) {
407
+ continue;
408
+ }
409
+ const slug = normalizeSlug(entry.name);
410
+ const workflowRoot = join(workflowsRoot, entry.name);
411
+ const state = await readJsonIfExists(join(workflowRoot, 'state.json'));
412
+ if (!state || state.schema_version === WORKFLOW_SCHEMA_VERSION) {
413
+ continue;
414
+ }
415
+ migrations.push(await migrateLegacyWorkflowState(cwd, slug, workflowRoot, state));
416
+ }
417
+ return migrations;
418
+ }
419
+
29
420
  export async function migrateLegacyRuntime(cwd) {
30
421
  const legacyRoot = resolveLegacyRoot(cwd);
31
422
  const loopxRoot = resolveLoopxRoot(cwd);
32
423
  const uppercaseRoot = resolveUppercaseLoopxRoot(cwd);
33
- const legacyExists = existsSync(legacyRoot);
34
- const loopxExists = existsSync(loopxRoot);
35
- const uppercaseExists = existsSync(uppercaseRoot);
424
+ const legacyExists = existsExactPath(legacyRoot);
425
+ const loopxExists = existsExactPath(loopxRoot);
426
+ const uppercaseExists = existsExactPath(uppercaseRoot);
36
427
 
37
428
  if (!legacyExists && !uppercaseExists) {
429
+ const workflowStateMigrations = loopxExists ? await migrateLegacyWorkflowStates(cwd) : [];
38
430
  return {
39
- migrated: false,
431
+ migrated: workflowStateMigrations.length > 0,
40
432
  legacyExists: false,
41
433
  uppercaseExists: false,
42
434
  loopxExists,
43
435
  loopxRoot,
44
436
  legacyRoot,
45
- reason: 'legacy_root_missing',
437
+ workflowStateMigrations,
438
+ reason: workflowStateMigrations.length > 0 ? 'migrated_legacy_workflow_schema' : 'legacy_root_missing',
46
439
  };
47
440
  }
48
441
 
@@ -52,6 +445,7 @@ export async function migrateLegacyRuntime(cwd) {
52
445
 
53
446
  if (uppercaseExists && !loopxExists) {
54
447
  await rename(uppercaseRoot, loopxRoot);
448
+ const workflowStateMigrations = await migrateLegacyWorkflowStates(cwd);
55
449
  return {
56
450
  migrated: true,
57
451
  legacyExists,
@@ -59,11 +453,13 @@ export async function migrateLegacyRuntime(cwd) {
59
453
  loopxExists: true,
60
454
  loopxRoot,
61
455
  legacyRoot,
456
+ workflowStateMigrations,
62
457
  reason: 'migrated_uppercase_loopx_runtime',
63
458
  };
64
459
  }
65
460
 
66
461
  await rename(legacyRoot, loopxRoot);
462
+ const workflowStateMigrations = await migrateLegacyWorkflowStates(cwd);
67
463
  return {
68
464
  migrated: true,
69
465
  legacyExists: true,
@@ -71,6 +467,7 @@ export async function migrateLegacyRuntime(cwd) {
71
467
  loopxExists: true,
72
468
  loopxRoot,
73
469
  legacyRoot,
470
+ workflowStateMigrations,
74
471
  reason: 'migrated_legacy_runtime',
75
472
  };
76
473
  }
@@ -81,16 +478,33 @@ export async function doctorRuntime(cwd, env = process.env) {
81
478
  const uppercaseRoot = resolveUppercaseLoopxRoot(cwd);
82
479
  const installState = await inspectInstallState(env);
83
480
  const installCheck = await verifyInstallState(env);
481
+ const installTemplateBaselinePath = getTemplateBaselinePath(env);
482
+ const workspaceTemplateBaselinePath = join(loopxRoot, 'template-hashes.json');
483
+ const templateGovernance = await inspectTemplateGovernance(
484
+ existsSync(installTemplateBaselinePath) ? installTemplateBaselinePath : workspaceTemplateBaselinePath,
485
+ );
486
+ const workflowHookPath = join(resolve(cwd), 'scripts', 'codex-workflow-hook.mjs');
487
+ const installedWorkflowHookPath = installState.managedArtifacts?.['codex-workflow-hook']?.targetPath
488
+ || join(resolve(env.LOOPX_HOME || env.HOME || process.cwd()), '.codex', 'hooks', 'codex-workflow-hook.mjs');
489
+ const hook = {
490
+ enabled: env.LOOPX_HOOKS !== '0',
491
+ workflowHookPath,
492
+ installedWorkflowHookPath,
493
+ installed: existsSync(installedWorkflowHookPath),
494
+ };
84
495
 
85
496
  return {
86
497
  loopxRoot,
87
498
  legacyRoot,
88
499
  uppercaseRoot,
89
- loopxExists: existsSync(loopxRoot),
90
- legacyExists: existsSync(legacyRoot),
91
- uppercaseExists: existsSync(uppercaseRoot),
92
- mixedRuntimeRoots: existsSync(loopxRoot) && (existsSync(legacyRoot) || existsSync(uppercaseRoot)),
500
+ loopxExists: existsExactPath(loopxRoot),
501
+ legacyExists: existsExactPath(legacyRoot),
502
+ uppercaseExists: existsExactPath(uppercaseRoot),
503
+ mixedRuntimeRoots: existsExactPath(loopxRoot) && (existsExactPath(legacyRoot) || existsExactPath(uppercaseRoot)),
93
504
  installState,
94
505
  installCheck,
506
+ templateGovernance,
507
+ contextSetup: await inspectWorkspaceContext(cwd),
508
+ hook,
95
509
  };
96
510
  }