ly-workflow-codex 0.2.0 → 0.3.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/dist/cli.mjs CHANGED
@@ -4,7 +4,7 @@ import { homedir } from 'node:os';
4
4
  import ansis from 'ansis';
5
5
  import inquirer from 'inquirer';
6
6
  import { join } from 'pathe';
7
- import { a as i18n, t as AGENTS_SKILLS_DIR, r as readLyConfig, L as LY_PROMPTS_DIR, v as version, P as PACKAGE_NAME, b as initI18n, s as showMainMenu, i as init, l as uninstallWorkflows, B as BIN_NAME } from './shared/ly-workflow-codex.K-E7PMp3.mjs';
7
+ import { a as i18n, q as AGENTS_SKILLS_DIR, r as readLyConfig, L as LY_PROMPTS_DIR, t as readCodexCurrentModel, v as version, P as PACKAGE_NAME, x as sanitizeSpawnableModels, y as sanitizeReasoningEffort, z as sanitizeReviewModel, B as sanitizeModelField, C as sanitizeExecutor, b as initI18n, s as showMainMenu, i as init, l as uninstallWorkflows, D as BIN_NAME } from './shared/ly-workflow-codex.DZD52_5A.mjs';
8
8
  import { execFile, spawn, execSync } from 'node:child_process';
9
9
  import fs from 'fs-extra';
10
10
  import { existsSync } from 'node:fs';
@@ -17,20 +17,28 @@ import 'node:url';
17
17
  const DEPENDENT_COMMANDS = ["@lyx-init", "@lyx-explore", "@lyx-propose", "@lyx-review-plan", "@lyx-archive"];
18
18
  const INSTALL_CMD = ["npm", "install", "-g", "@fission-ai/openspec@latest"];
19
19
  const SHELL_OPT = process.platform === "win32" ? { shell: true } : {};
20
- const OPENSPEC_SKILL_NAMES = [
21
- "openspec-explore",
22
- "openspec-propose",
23
- "openspec-apply-change",
24
- "openspec-archive-change"
25
- ];
26
- function detectOpenspecSkills() {
27
- const roots = [
28
- AGENTS_SKILLS_DIR,
29
- join(process.cwd(), ".agents", "skills")
20
+ const DEFAULT_WORKFLOWS = ["propose", "explore", "apply", "update", "sync", "archive"];
21
+ const WORKFLOW_TO_SKILL = {
22
+ explore: "openspec-explore",
23
+ new: "openspec-new-change",
24
+ continue: "openspec-continue-change",
25
+ apply: "openspec-apply-change",
26
+ update: "openspec-update-change",
27
+ ff: "openspec-ff-change",
28
+ sync: "openspec-sync-specs",
29
+ archive: "openspec-archive-change",
30
+ "bulk-archive": "openspec-bulk-archive-change",
31
+ verify: "openspec-verify-change",
32
+ onboard: "openspec-onboard",
33
+ propose: "openspec-propose"
34
+ };
35
+ function getOpenspecSkillRoots(cwd = process.cwd()) {
36
+ return [
37
+ { scope: "project", path: join(cwd, ".agents", "skills") },
38
+ { scope: "project", path: join(cwd, ".codex", "skills") },
39
+ { scope: "global", path: AGENTS_SKILLS_DIR },
40
+ { scope: "global", path: join(homedir(), ".codex", "skills") }
30
41
  ];
31
- return OPENSPEC_SKILL_NAMES.some(
32
- (name) => roots.some((root) => existsSync(join(root, name, "SKILL.md")))
33
- );
34
42
  }
35
43
  function detectOpenspecCli() {
36
44
  return new Promise((resolve) => {
@@ -47,6 +55,126 @@ function detectOpenspecCli() {
47
55
  });
48
56
  });
49
57
  }
58
+ function execFileText(cmd, args, options = {}) {
59
+ return new Promise((resolve) => {
60
+ execFile(cmd, args, { timeout: 5e3, ...SHELL_OPT, ...options }, (err, stdout, stderr) => {
61
+ resolve({
62
+ ok: !err,
63
+ stdout: stdout?.toString() ?? "",
64
+ stderr: stderr?.toString() ?? "",
65
+ error: err
66
+ });
67
+ });
68
+ });
69
+ }
70
+ function resolveWorkflowDependencies(workflows) {
71
+ const resolved = [...workflows];
72
+ if ((resolved.includes("archive") || resolved.includes("bulk-archive")) && !resolved.includes("sync"))
73
+ resolved.splice(Math.max(resolved.findIndex((w) => w === "archive" || w === "bulk-archive"), 0), 0, "sync");
74
+ return resolved;
75
+ }
76
+ async function readOpenspecProfile(cwd) {
77
+ const result = await execFileText("openspec", ["config", "list", "--json"], { cwd });
78
+ if (!result.ok)
79
+ return { workflows: DEFAULT_WORKFLOWS, source: "fallback" };
80
+ try {
81
+ const parsed = JSON.parse(result.stdout);
82
+ if (!Array.isArray(parsed.workflows) || parsed.workflows.length === 0)
83
+ return { workflows: DEFAULT_WORKFLOWS, source: "fallback" };
84
+ const workflows = parsed.workflows.filter((w) => typeof w === "string" && w.length > 0);
85
+ return { workflows: resolveWorkflowDependencies(workflows), source: "openspec-config" };
86
+ } catch {
87
+ return { workflows: DEFAULT_WORKFLOWS, source: "fallback" };
88
+ }
89
+ }
90
+ function mapWorkflowsToSkills(workflows) {
91
+ return [...new Set(workflows.map((w) => WORKFLOW_TO_SKILL[w]).filter((s) => Boolean(s)))];
92
+ }
93
+ function inspectOpenspecSkills(required, cwd, profileSource) {
94
+ const roots = getOpenspecSkillRoots(cwd);
95
+ const resolved = {};
96
+ const projectHits = /* @__PURE__ */ new Set();
97
+ const missing = [];
98
+ for (const skill of required) {
99
+ for (const root of roots) {
100
+ const skillFile = join(root.path, skill, "SKILL.md");
101
+ if (existsSync(skillFile)) {
102
+ resolved[skill] = skillFile;
103
+ if (root.scope === "project")
104
+ projectHits.add(skill);
105
+ break;
106
+ }
107
+ }
108
+ if (!resolved[skill])
109
+ missing.push(skill);
110
+ }
111
+ let status;
112
+ if (missing.length > 0)
113
+ status = "missing";
114
+ else if (profileSource === "fallback")
115
+ status = "unknown";
116
+ else if (projectHits.size !== required.length)
117
+ status = "global-only";
118
+ else
119
+ status = "project-ready";
120
+ return { status, required, resolved, missing, roots, profileSource };
121
+ }
122
+ async function inspectOpenspecRoot(cwd, cli) {
123
+ if (!cli.installed)
124
+ return { status: "not-checked" };
125
+ const result = await execFileText("openspec", ["doctor", "--json"], { cwd });
126
+ let doctor;
127
+ try {
128
+ doctor = result.stdout ? JSON.parse(result.stdout) : void 0;
129
+ } catch (error) {
130
+ return { status: "unhealthy", error: error instanceof Error ? error.message : String(error) };
131
+ }
132
+ const statuses = doctor?.status ?? [];
133
+ if (!result.ok) {
134
+ if (statuses.some((s) => s.code === "no_openspec_root"))
135
+ return { status: "missing", doctor };
136
+ return {
137
+ status: "unhealthy",
138
+ doctor,
139
+ error: result.stderr || "openspec doctor --json failed"
140
+ };
141
+ }
142
+ const rootHealthy = doctor?.root?.healthy;
143
+ return rootHealthy === true ? { status: "healthy", doctor } : { status: "unhealthy", doctor };
144
+ }
145
+ async function inspectOpenspec(options) {
146
+ const cwd = options?.cwd ?? process.cwd();
147
+ const cli = await detectOpenspecCli();
148
+ const cliStatus = !cli.installed ? "missing" : cli.version === "unknown" ? "unhealthy" : "ok";
149
+ const profile = cli.installed && cliStatus !== "unhealthy" ? await readOpenspecProfile(cwd) : { workflows: DEFAULT_WORKFLOWS, source: "fallback" };
150
+ const required = mapWorkflowsToSkills(profile.workflows);
151
+ const skills = inspectOpenspecSkills(required, cwd, profile.source);
152
+ const root = await inspectOpenspecRoot(cwd, cli);
153
+ const actions = [];
154
+ if (cliStatus === "missing")
155
+ actions.push({ kind: "install-cli" });
156
+ if (skills.status === "global-only")
157
+ actions.push({ kind: "warn-global-only" });
158
+ if (skills.status === "missing") {
159
+ if (root.status === "missing")
160
+ actions.push({ kind: "init-root" });
161
+ else
162
+ actions.push({ kind: "repair-skills", strategy: "update" });
163
+ }
164
+ if (root.status === "missing" && !actions.some((a) => a.kind === "init-root"))
165
+ actions.push({ kind: "init-root" });
166
+ if (root.status === "unhealthy")
167
+ actions.push({ kind: "report-root", doctor: root.doctor });
168
+ return {
169
+ cli: { status: cliStatus, version: cli.version },
170
+ skills,
171
+ root,
172
+ actions
173
+ };
174
+ }
175
+ function isNonInteractive(skipPrompt) {
176
+ return Boolean(skipPrompt || process.env.CI || !process.stdin.isTTY || !process.stdout.isTTY);
177
+ }
50
178
  function printUnavailable() {
51
179
  console.log(ansis.yellow(` ${i18n.t("common:preflight.unavailableList", { list: DEPENDENT_COMMANDS.join(" ") })}`));
52
180
  console.log(ansis.gray(` ${i18n.t("common:preflight.unaffectedNote")}`));
@@ -59,49 +187,118 @@ async function runNpmInstall() {
59
187
  child.on("close", (code) => resolve(code === 0));
60
188
  });
61
189
  }
62
- function isNonInteractive(skipPrompt) {
63
- return Boolean(skipPrompt || process.env.CI || !process.stdin.isTTY || !process.stdout.isTTY);
190
+ async function confirmOpenspecCliInstall(skipPrompt) {
191
+ if (isNonInteractive(skipPrompt)) {
192
+ printUnavailable();
193
+ return false;
194
+ }
195
+ const { confirmed } = await inquirer.prompt([{
196
+ type: "confirm",
197
+ name: "confirmed",
198
+ message: i18n.t("common:preflight.installAsk"),
199
+ default: true
200
+ }]);
201
+ if (!confirmed) {
202
+ printUnavailable();
203
+ return false;
204
+ }
205
+ return true;
64
206
  }
65
- async function checkExternalDeps(options) {
66
- try {
67
- const cli = await detectOpenspecCli();
68
- if (cli.installed) {
69
- if (!detectOpenspecSkills()) {
70
- console.log(ansis.yellow(`\u26A0 ${i18n.t("common:preflight.skillsMissingCodex")}`));
71
- }
72
- return;
73
- }
207
+ async function installCli() {
208
+ const ok = await runNpmInstall();
209
+ if (!ok) {
210
+ console.error(ansis.red(`\u2717 ${i18n.t("common:preflight.installFailed")}`));
211
+ printUnavailable();
212
+ return false;
213
+ }
214
+ const recheck = await detectOpenspecCli();
215
+ if (!recheck.installed) {
216
+ console.log(ansis.yellow(`\u26A0 ${i18n.t("common:preflight.installNotInPath")}`));
217
+ return false;
218
+ }
219
+ return true;
220
+ }
221
+ function printOpenspecInspection(inspection) {
222
+ if (inspection.cli.status === "missing") {
74
223
  console.log(ansis.yellow(`\u26A0 ${i18n.t("common:preflight.cliMissing")}`));
75
- if (isNonInteractive(options?.skipPrompt)) {
76
- printUnavailable();
77
- return;
78
- }
79
- const { confirmed } = await inquirer.prompt([{
80
- type: "confirm",
81
- name: "confirmed",
82
- message: i18n.t("common:preflight.installAsk"),
83
- default: true
84
- }]);
85
- if (!confirmed) {
86
- printUnavailable();
87
- return;
224
+ printUnavailable();
225
+ return;
226
+ }
227
+ if (inspection.cli.status === "unhealthy") {
228
+ console.log(ansis.yellow(`\u26A0 ${i18n.t("common:preflight.cliUnhealthy")}`));
229
+ }
230
+ if (inspection.skills.status === "global-only") {
231
+ console.log(ansis.yellow(`\u26A0 ${i18n.t("common:preflight.skillsGlobalOnly")}`));
232
+ } else if (inspection.skills.status === "missing") {
233
+ console.log(ansis.yellow(`\u26A0 ${i18n.t("common:preflight.skillsMissing", { list: inspection.skills.missing.join(", ") })}`));
234
+ } else if (inspection.skills.status === "unknown") {
235
+ console.log(ansis.yellow(`\u26A0 ${i18n.t("common:preflight.skillsUnknown")}`));
236
+ }
237
+ if (inspection.root.status === "missing") {
238
+ console.log(ansis.yellow(`\u26A0 ${i18n.t("common:preflight.rootMissing")}`));
239
+ } else if (inspection.root.status === "unhealthy") {
240
+ console.error(ansis.red(`\u2717 ${i18n.t("common:preflight.rootUnhealthy")}`));
241
+ }
242
+ }
243
+ async function ensureOpenspec(options) {
244
+ const cwd = options?.cwd ?? process.cwd();
245
+ const executed = [];
246
+ let inspection = await inspectOpenspec({ cwd });
247
+ if (inspection.cli.status === "missing") {
248
+ const confirmed = options?.yes || (options?.confirmInstall ? await options.confirmInstall() : false);
249
+ if (!confirmed)
250
+ return { inspection, executed };
251
+ if (await installCli()) {
252
+ executed.push("install-cli");
253
+ inspection = await inspectOpenspec({ cwd });
254
+ } else {
255
+ return { inspection, executed };
88
256
  }
89
- const ok = await runNpmInstall();
90
- if (!ok) {
91
- console.error(ansis.red(`\u2717 ${i18n.t("common:preflight.installFailed")}`));
92
- printUnavailable();
93
- return;
257
+ }
258
+ if (inspection.cli.status !== "ok")
259
+ return { inspection, executed };
260
+ if (inspection.root.status === "unhealthy")
261
+ return { inspection, executed };
262
+ if (inspection.root.status === "missing") {
263
+ const initResult = await execFileText("openspec", ["init", "--tools", "codex", "--no-animation"], { cwd });
264
+ executed.push("init-root");
265
+ inspection = await inspectOpenspec({ cwd });
266
+ if (!initResult.ok && inspection.root.status === "missing")
267
+ return { inspection, executed };
268
+ }
269
+ if (inspection.skills.status === "missing") {
270
+ const updateResult = await execFileText("openspec", ["update", "--force"], { cwd });
271
+ executed.push("repair-skills:update");
272
+ inspection = await inspectOpenspec({ cwd });
273
+ if (inspection.skills.status === "missing") {
274
+ const initResult = await execFileText("openspec", ["init", "--tools", "codex", "--no-animation"], { cwd });
275
+ executed.push("repair-skills:init");
276
+ inspection = await inspectOpenspec({ cwd });
277
+ if (!updateResult.ok && !initResult.ok && inspection.skills.status === "missing")
278
+ return { inspection, executed };
94
279
  }
95
- const recheck = await detectOpenspecCli();
96
- if (!recheck.installed) {
97
- console.log(ansis.yellow(`\u26A0 ${i18n.t("common:preflight.installNotInPath")}`));
280
+ }
281
+ return { inspection, executed };
282
+ }
283
+ async function checkExternalDeps(options) {
284
+ try {
285
+ if (options?.initOpenspec) {
286
+ const result = await ensureOpenspec({
287
+ yes: options.skipPrompt,
288
+ confirmInstall: () => confirmOpenspecCliInstall(options.skipPrompt)
289
+ });
290
+ printOpenspecInspection(result.inspection);
98
291
  return;
99
292
  }
100
- if (detectOpenspecSkills()) {
101
- console.log(ansis.green(`\u2713 ${i18n.t("common:preflight.installSuccessWithSkills")}`));
102
- } else {
103
- console.log(ansis.green(`\u2713 ${i18n.t("common:preflight.installSuccessNeedInit")}`));
293
+ let inspection = await inspectOpenspec();
294
+ if (inspection.cli.status === "missing") {
295
+ if (!await confirmOpenspecCliInstall(options?.skipPrompt))
296
+ return;
297
+ if (!await installCli())
298
+ return;
299
+ inspection = await inspectOpenspec();
104
300
  }
301
+ printOpenspecInspection(inspection);
105
302
  } catch {
106
303
  }
107
304
  }
@@ -124,6 +321,113 @@ function execSafe(cmd) {
124
321
  return null;
125
322
  }
126
323
  }
324
+ function buildOpenspecSkillsDetail(status2, missing) {
325
+ if (status2 === "project-ready")
326
+ return i18n.t("common:doctor.skillsInitialized");
327
+ if (status2 === "global-only")
328
+ return i18n.t("common:doctor.skillsGlobalOnly");
329
+ if (status2 === "unknown")
330
+ return i18n.t("common:doctor.skillsUnknown");
331
+ return i18n.t("common:doctor.skillsMissing", { list: missing.join(", ") || "unknown" });
332
+ }
333
+ function buildOpenspecRootDetail(status2) {
334
+ if (status2 === "healthy")
335
+ return i18n.t("common:doctor.rootHealthy");
336
+ if (status2 === "missing")
337
+ return i18n.t("common:doctor.rootMissing");
338
+ if (status2 === "unhealthy")
339
+ return i18n.t("common:doctor.rootUnhealthy");
340
+ return i18n.t("common:doctor.rootNotChecked");
341
+ }
342
+ function resolveExecutor(value) {
343
+ const cleaned = sanitizeExecutor(value);
344
+ if (cleaned)
345
+ return { kind: cleaned, invalid: false };
346
+ const invalid = typeof value === "string" && value.trim() !== "";
347
+ return { kind: "main", invalid };
348
+ }
349
+ function assessSubagentModelConfig(codexHost) {
350
+ const spawn = sanitizeSpawnableModels(codexHost?.spawnableModels);
351
+ const reviewExecutor = resolveExecutor(codexHost?.reviewExecutor);
352
+ const codingExecutor = resolveExecutor(codexHost?.codingExecutor);
353
+ const buildField = (input) => {
354
+ const ineffective = input.executorKind === "main";
355
+ const hasValue = Boolean(input.value || input.reasoningEffort);
356
+ return {
357
+ key: input.key,
358
+ value: input.value,
359
+ status: ineffective && hasValue ? "warn" : "ok",
360
+ okKind: hasValue ? ineffective ? "ineffective" : "configured" : "unset",
361
+ reasoningEffortKey: input.reasoningEffortKey,
362
+ reasoningEffort: input.reasoningEffort
363
+ };
364
+ };
365
+ const fields = [
366
+ buildField({
367
+ key: "reviewModel",
368
+ value: sanitizeReviewModel(codexHost?.reviewModel),
369
+ reasoningEffortKey: "reviewReasoningEffort",
370
+ reasoningEffort: sanitizeReasoningEffort(codexHost?.reviewReasoningEffort),
371
+ executorKind: reviewExecutor.kind
372
+ }),
373
+ buildField({
374
+ key: "codingModel",
375
+ value: sanitizeModelField(codexHost?.codingModel),
376
+ reasoningEffortKey: "codingReasoningEffort",
377
+ reasoningEffort: sanitizeReasoningEffort(codexHost?.codingReasoningEffort),
378
+ executorKind: codingExecutor.kind
379
+ })
380
+ ];
381
+ const executors = [
382
+ {
383
+ key: "reviewExecutor",
384
+ kind: reviewExecutor.kind,
385
+ invalid: reviewExecutor.invalid,
386
+ rawValue: typeof codexHost?.reviewExecutor === "string" ? codexHost.reviewExecutor : void 0
387
+ },
388
+ {
389
+ key: "codingExecutor",
390
+ kind: codingExecutor.kind,
391
+ invalid: codingExecutor.invalid,
392
+ rawValue: typeof codexHost?.codingExecutor === "string" ? codexHost.codingExecutor : void 0
393
+ }
394
+ ];
395
+ const raw = codexHost;
396
+ const removedFields = ["reviewModelB", "reviewReasoningEffortB"].filter((key) => raw?.[key] !== void 0);
397
+ const spawnWarn = spawn.state === "empty" || spawn.state === "invalid";
398
+ const fieldWarn = fields.some((f) => f.status === "warn");
399
+ const executorWarn = executors.some((e) => e.invalid);
400
+ return {
401
+ status: spawnWarn || fieldWarn || executorWarn ? "warn" : "ok",
402
+ executors,
403
+ fields,
404
+ spawnState: spawn.state,
405
+ removedFields
406
+ };
407
+ }
408
+ function buildSubagentModelCheckDetail(result) {
409
+ const parts = [];
410
+ for (const e of result.executors) {
411
+ parts.push(e.invalid ? i18n.t("doctor:modelConfig.warnInvalidExecutor", { key: e.key, value: e.rawValue ?? "" }) : i18n.t(e.kind === "main" ? "doctor:modelConfig.executorMain" : "doctor:modelConfig.executorSubagent", { key: e.key }));
412
+ }
413
+ for (const f of result.fields) {
414
+ const modelPart = f.value ? f.okKind === "ineffective" ? i18n.t("doctor:modelConfig.warnIneffective", { key: f.key, model: f.value }) : i18n.t("doctor:modelConfig.okConfigured", { key: f.key, model: f.value }) : i18n.t("doctor:modelConfig.okUnset", { key: f.key });
415
+ const reasoningPart = f.reasoningEffort ? i18n.t("doctor:modelConfig.okReasoningConfigured", { key: f.reasoningEffortKey, effort: f.reasoningEffort }) : i18n.t("doctor:modelConfig.okReasoningUnset", { key: f.reasoningEffortKey });
416
+ parts.push(modelPart, reasoningPart);
417
+ }
418
+ for (const key of result.removedFields)
419
+ parts.push(i18n.t("doctor:modelConfig.removedNote", { key }));
420
+ if (result.spawnState === "empty" || result.spawnState === "invalid")
421
+ parts.push(i18n.t("doctor:modelConfig.warnInvalid"));
422
+ return parts.join("; ");
423
+ }
424
+ function buildSubagentModelHintLines(currentModel) {
425
+ const lines = [i18n.t("doctor:modelConfig.agentNeedConfig")];
426
+ if (currentModel)
427
+ lines.push(i18n.t("doctor:modelConfig.inheritNote", { model: currentModel }));
428
+ lines.push(i18n.t("doctor:modelConfig.verifyHint"));
429
+ return lines;
430
+ }
127
431
  async function doctor() {
128
432
  const checks = [];
129
433
  const nodeVer = process.version;
@@ -137,7 +441,7 @@ async function doctor() {
137
441
  checks.push({
138
442
  label: "config",
139
443
  status: config ? OK : WARN,
140
- detail: config ? `v${config.general?.version || "?"}, lang=${config.general?.language || "?"}` : "Not found (~/.ly/config.toml)"
444
+ detail: config ? `v${config.general?.version || "?"}, lang=${config.general?.language || "?"}` : "Not found (~/.codex/lyx/config.toml)"
141
445
  });
142
446
  const skillsEntries = await dirFiles(AGENTS_SKILLS_DIR);
143
447
  const cmdCount = skillsEntries.filter((f) => f.startsWith("lyx-")).length;
@@ -151,19 +455,30 @@ async function doctor() {
151
455
  checks.push({
152
456
  label: "Roles",
153
457
  status: roleFiles.length >= 2 ? OK : roleFiles.length > 0 ? WARN : FAIL,
154
- detail: roleFiles.length > 0 ? roleFiles.join(", ") : "None (~/.ly/prompts/codex/)"
458
+ detail: roleFiles.length > 0 ? roleFiles.join(", ") : "None (~/.codex/lyx/prompts/codex/)"
155
459
  });
156
- const openspecCli = await detectOpenspecCli();
460
+ const openspec = await inspectOpenspec();
157
461
  checks.push({
158
462
  label: "OpenSpec CLI",
159
- status: openspecCli.installed ? OK : WARN,
160
- detail: openspecCli.installed ? `v${openspecCli.version}` : i18n.t("common:doctor.openspecCliMissing")
463
+ status: openspec.cli.status === "ok" ? OK : WARN,
464
+ detail: openspec.cli.status === "ok" ? `v${openspec.cli.version}` : openspec.cli.status === "unhealthy" ? i18n.t("common:doctor.openspecCliUnhealthy") : i18n.t("common:doctor.openspecCliMissing")
161
465
  });
162
- const hasOpenspecSkills = detectOpenspecSkills();
163
466
  checks.push({
164
467
  label: "OpenSpec skills",
165
- status: hasOpenspecSkills ? OK : WARN,
166
- detail: hasOpenspecSkills ? i18n.t("common:doctor.skillsInitialized") : i18n.t("common:doctor.skillsMissing")
468
+ status: openspec.skills.status === "project-ready" ? OK : WARN,
469
+ detail: buildOpenspecSkillsDetail(openspec.skills.status, openspec.skills.missing)
470
+ });
471
+ checks.push({
472
+ label: "OpenSpec root",
473
+ status: openspec.root.status === "healthy" ? OK : WARN,
474
+ detail: buildOpenspecRootDetail(openspec.root.status)
475
+ });
476
+ const currentModel = await readCodexCurrentModel();
477
+ const modelCheck = assessSubagentModelConfig(config?.codexHost);
478
+ checks.push({
479
+ label: i18n.t("doctor:modelConfig.label"),
480
+ status: modelCheck.status === "warn" ? WARN : OK,
481
+ detail: buildSubagentModelCheckDetail(modelCheck)
167
482
  });
168
483
  console.log();
169
484
  console.log(ansis.cyan.bold(` ly-workflow-codex Doctor v${version}`));
@@ -171,6 +486,8 @@ async function doctor() {
171
486
  for (const { label, status: status2, detail } of checks) {
172
487
  console.log(` ${status2} ${ansis.bold(label.padEnd(20))} ${ansis.gray(detail)}`);
173
488
  }
489
+ for (const line of buildSubagentModelHintLines(currentModel))
490
+ console.log(ansis.gray(` ${line}`));
174
491
  const failures = checks.filter((c) => c.status === FAIL);
175
492
  console.log();
176
493
  if (failures.length === 0) {
@@ -205,16 +522,16 @@ async function status() {
205
522
  }
206
523
  }
207
524
  }
208
- const openspecCli = await detectOpenspecCli();
209
- const hasOpenspecSkills = detectOpenspecSkills();
525
+ const openspec = await inspectOpenspec();
210
526
  console.log();
211
527
  console.log(ansis.cyan.bold(" ly-workflow-codex Status"));
212
528
  console.log();
213
529
  console.log(` ${ansis.bold("Version")} ${installedVer}${installedVer !== latestVer ? ansis.yellow(` (latest: ${latestVer})`) : ansis.green(" (up to date)")}`);
214
530
  console.log(` ${ansis.bold("Commands")} ${cmds.length}`);
215
531
  console.log(` ${ansis.bold("Review model")} ${reviewModel}`);
216
- console.log(` ${ansis.bold("OpenSpec CLI")} ${openspecCli.installed ? `v${openspecCli.version}` : ansis.yellow(i18n.t("common:doctor.openspecCliMissing"))}`);
217
- console.log(` ${ansis.bold("OpenSpec skills")}${hasOpenspecSkills ? ` ${i18n.t("common:doctor.skillsInitialized")}` : ansis.yellow(` ${i18n.t("common:doctor.skillsMissing")}`)}`);
532
+ console.log(` ${ansis.bold("OpenSpec CLI")} ${openspec.cli.status === "ok" ? `v${openspec.cli.version}` : ansis.yellow(i18n.t(openspec.cli.status === "unhealthy" ? "common:doctor.openspecCliUnhealthy" : "common:doctor.openspecCliMissing"))}`);
533
+ console.log(` ${ansis.bold("OpenSpec skills")} ${openspec.skills.status === "project-ready" ? i18n.t("common:doctor.skillsInitialized") : ansis.yellow(buildOpenspecSkillsDetail(openspec.skills.status, openspec.skills.missing))}`);
534
+ console.log(` ${ansis.bold("OpenSpec root")} ${openspec.root.status === "healthy" ? i18n.t("common:doctor.rootHealthy") : ansis.yellow(buildOpenspecRootDetail(openspec.root.status))}`);
218
535
  console.log(` ${ansis.bold("Active tasks")} ${activeTasks > 0 ? ansis.yellow(String(activeTasks)) : "0"}`);
219
536
  console.log();
220
537
  }
@@ -231,6 +548,8 @@ function customizeHelp(sections) {
231
548
  ` ${ansis.cyan(`${BIN_NAME} init`)} | ${ansis.cyan("i")} ${i18n.t("cli:help.commandDescriptions.initConfig")}`,
232
549
  ` ${ansis.cyan(`${BIN_NAME} doctor`)} Check installation health`,
233
550
  ` ${ansis.cyan(`${BIN_NAME} status`)} Show installation overview`,
551
+ ` ${ansis.cyan(`${BIN_NAME} openspec inspect`)} Inspect OpenSpec dependency state`,
552
+ ` ${ansis.cyan(`${BIN_NAME} openspec ensure`)} Ensure OpenSpec CLI/skills/root`,
234
553
  ` ${ansis.cyan(`${BIN_NAME} uninstall`)} Uninstall ${PACKAGE_NAME} (non-interactive)`,
235
554
  "",
236
555
  ansis.gray(` ${i18n.t("cli:help.shortcuts")}`),
@@ -284,26 +603,49 @@ async function setupCommands(cli) {
284
603
  await checkExternalDeps();
285
604
  await showMainMenu();
286
605
  });
287
- cli.command("init", i18n.t("cli:help.commandDescriptions.initConfig")).alias("i").option("--lang, -l <lang>", `${i18n.t("cli:help.optionDescriptions.displayLanguage")} (zh-CN, en)`).option("--force, -f", i18n.t("cli:help.optionDescriptions.forceOverwrite")).option("--skip-prompt, -s", i18n.t("cli:help.optionDescriptions.skipAllPrompts")).option("--workflows, -w <workflows>", i18n.t("cli:help.optionDescriptions.workflows")).option("--install-dir, -d <path>", i18n.t("cli:help.optionDescriptions.installDir")).action(async (options) => {
606
+ cli.command("init", i18n.t("cli:help.commandDescriptions.initConfig")).alias("i").option("--lang, -l <lang>", `${i18n.t("cli:help.optionDescriptions.displayLanguage")} (zh-CN, en)`).option("--force, -f", i18n.t("cli:help.optionDescriptions.forceOverwrite")).option("--skip-prompt, -s", i18n.t("cli:help.optionDescriptions.skipAllPrompts")).option("--workflows, -w <workflows>", i18n.t("cli:help.optionDescriptions.workflows")).option("--install-dir, -d <path>", i18n.t("cli:help.optionDescriptions.installDir")).option("--init-openspec", i18n.t("cli:help.optionDescriptions.initOpenspec")).action(async (options) => {
288
607
  if (options.lang) {
289
608
  await initI18n(options.lang);
290
609
  }
291
- await checkExternalDeps({ skipPrompt: options.skipPrompt });
610
+ await checkExternalDeps({ skipPrompt: options.skipPrompt, initOpenspec: options.initOpenspec });
292
611
  await init(options);
293
612
  });
613
+ cli.command("openspec <action>", "Inspect or ensure OpenSpec dependencies").option("--json", "Output JSON").option("--yes, -y", "Skip confirmation").action(async (action, options) => {
614
+ if (action === "inspect") {
615
+ const result = await inspectOpenspec();
616
+ if (options.json)
617
+ console.log(JSON.stringify(result, null, 2));
618
+ else
619
+ printOpenspecInspection(result);
620
+ return;
621
+ }
622
+ if (action === "ensure") {
623
+ const result = await ensureOpenspec({
624
+ yes: options.yes,
625
+ confirmInstall: () => confirmOpenspecCliInstall()
626
+ });
627
+ if (options.json)
628
+ console.log(JSON.stringify(result, null, 2));
629
+ else
630
+ printOpenspecInspection(result.inspection);
631
+ return;
632
+ }
633
+ console.error(ansis.red(`\u672A\u77E5 openspec \u5B50\u547D\u4EE4: ${action}`));
634
+ process.exitCode = 1;
635
+ });
294
636
  cli.command("doctor", "Check ly-workflow-codex installation health").action(async () => {
295
637
  await doctor();
296
638
  });
297
639
  cli.command("status", "Show ly-workflow-codex installation status").action(async () => {
298
640
  await status();
299
641
  });
300
- cli.command("uninstall", "Uninstall ly-workflow-codex workflows (~/.agents/skills/lyx-*, ~/.ly/prompts/codex/)").option("--yes, -y", "Skip confirmation").action(async (options) => {
642
+ cli.command("uninstall", "Uninstall ly-workflow-codex workflows (~/.agents/skills/lyx-*, ~/.codex/lyx/)").option("--yes, -y", "Skip confirmation").action(async (options) => {
301
643
  const installDir = join(homedir(), ".codex");
302
644
  if (!options.yes) {
303
645
  const { confirm } = await inquirer.prompt([{
304
646
  type: "confirm",
305
647
  name: "confirm",
306
- message: `\u786E\u5B9A\u8981\u5378\u8F7D ${PACKAGE_NAME} \u5417\uFF1F\u5C06\u79FB\u9664 ~/.agents/skills/lyx-*\uFF08\u542B\u65E7 ~/.agents/skills/ly-* \u4E0E ~/.codex/prompts/ly-*.md \u6B8B\u7559\uFF09\u4E0E ~/.ly/prompts/codex/\uFF1B\u5171\u4EAB\u914D\u7F6E ~/.ly/config.toml \u4E0E ~/.ly/ \u5176\u4F59\u5185\u5BB9\uFF08\u542B worktrees\uFF09\u4FDD\u7559\u3002`,
648
+ message: `\u786E\u5B9A\u8981\u5378\u8F7D ${PACKAGE_NAME} \u5417\uFF1F\u5C06\u79FB\u9664 ~/.agents/skills/lyx-*\uFF08\u542B\u65E7 ~/.agents/skills/ly-* \u4E0E ~/.codex/prompts/ly-*.md \u6B8B\u7559\uFF09\u4E0E ~/.codex/lyx/\uFF08\u914D\u7F6E config.toml\u3001\u89D2\u8272\u8BCD prompts/\u3001worktrees\uFF09\uFF1B\u82E5 ~/.codex/lyx/worktrees/ \u4E0B\u5B58\u5728\u672A\u6E05\u7406\u7684\u5B9E\u9645 git worktree\uFF0C\u8BE5\u5B50\u76EE\u5F55\u4F1A\u88AB\u4FDD\u7559\u5E76\u63D0\u793A\u9700\u5148\u624B\u52A8\u6E05\u7406\u3002`,
307
649
  default: false
308
650
  }]);
309
651
  if (!confirm) {
@@ -318,10 +660,8 @@ async function setupCommands(cli) {
318
660
  console.log(ansis.gray(` lyx-* skills: ${result.removedSkills.length} removed`));
319
661
  if (result.removedLegacyPrompts.length > 0)
320
662
  console.log(ansis.gray(` Legacy ~/.codex/prompts residue: ${result.removedLegacyPrompts.length} removed`));
321
- if (result.removedSharedPrompts)
322
- console.log(ansis.gray(" Shared prompts (~/.ly/prompts/codex/): removed"));
323
- if (result.configTomlKept)
324
- console.log(ansis.gray(" Shared config (~/.ly/config.toml): kept (~/.ly is the ly-workflow shared namespace)"));
663
+ if (result.removedPrompts)
664
+ console.log(ansis.gray(" Prompts (~/.codex/lyx/prompts/codex/): removed"));
325
665
  } else {
326
666
  console.error(ansis.red("\u2717 Uninstall failed"));
327
667
  for (const err of result.errors) console.error(ansis.gray(` ${err}`));