@litfamily/lithermes 1.0.4 → 1.0.6

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 (53) hide show
  1. package/README.md +100 -85
  2. package/README_Ko-KR.md +108 -95
  3. package/assets/lithermes-plugin/README.md +27 -17
  4. package/assets/lithermes-plugin/__init__.py +5 -1
  5. package/assets/lithermes-plugin/core_contexts.py +18 -4
  6. package/assets/lithermes-plugin/core_routing.py +2 -0
  7. package/assets/lithermes-plugin/diagnostics.py +48 -16
  8. package/assets/lithermes-plugin/payload-version.json +105 -13
  9. package/assets/lithermes-plugin/plugin.yaml +1 -1
  10. package/assets/lithermes-plugin/skills/frontend-ui-ux/SKILL.md +11 -12
  11. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/complete-contract.md +24 -13
  12. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/default-editorial-pixel.json +18 -0
  13. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/operating-lanes.md +1 -1
  14. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/product-direction.md +1 -1
  15. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/production.md +13 -0
  16. package/assets/lithermes-plugin/skills/readme-studio/SKILL.md +73 -0
  17. package/assets/lithermes-plugin/skills/readme-studio/references/decoration-patterns.md +36 -0
  18. package/assets/lithermes-plugin/skills/readme-studio/references/facts.md +11 -0
  19. package/assets/lithermes-plugin/skills/readme-studio/references/motion.md +34 -0
  20. package/assets/lithermes-plugin/skills/readme-studio/references/production-interview.md +17 -0
  21. package/assets/lithermes-plugin/skills/readme-studio/references/type-and-images.md +17 -0
  22. package/assets/lithermes-plugin/skills/readme-studio/scripts/check_facts.py +103 -0
  23. package/assets/lithermes-plugin/skills/readme-studio/templates/cover-section.md +63 -0
  24. package/assets/lithermes-plugin/skills/readme-studio/templates/cover-settings.json +11 -0
  25. package/assets/lithermes-plugin/skills/readme-studio/templates/cover-source.json +12 -0
  26. package/assets/lithermes-plugin/skills/readme-studio/templates/facts.json +1 -0
  27. package/assets/lithermes-plugin/skills/readme-studio/templates/hyperframes/index.html +54 -0
  28. package/assets/lithermes-plugin/skills/readme-studio/templates/hyperframes/index.motion.json +1 -0
  29. package/assets/lithermes-plugin/skills/readme-studio/templates/hyperframes/package-lock.json +1843 -0
  30. package/assets/lithermes-plugin/skills/readme-studio/templates/hyperframes/package.json +6 -0
  31. package/assets/lithermes-plugin/skills/readme-studio/templates/remotion/package-lock.json +3783 -0
  32. package/assets/lithermes-plugin/skills/readme-studio/templates/remotion/package.json +7 -0
  33. package/assets/lithermes-plugin/skills/readme-studio/templates/remotion/src/index.tsx +34 -0
  34. package/assets/lithermes-plugin/skills/readme-studio/templates/typography/outline.mjs +79 -0
  35. package/assets/lithermes-plugin/skills/readme-studio/templates/typography/package-lock.json +133 -0
  36. package/assets/lithermes-plugin/skills/readme-studio/templates/typography/package.json +6 -0
  37. package/assets/lithermes-plugin/skills/visual-qa/references/complete-contract.md +3 -3
  38. package/package.json +1 -1
  39. package/readme-assets/Lucide-LICENSE.txt +43 -0
  40. package/readme-assets/ascii-readme.svg +5 -0
  41. package/readme-assets/badge-version.svg +1 -1
  42. package/readme-assets/cover-motion-still.webp +0 -0
  43. package/readme-assets/cover-motion.webp +0 -0
  44. package/readme-assets/ignition-film.mp4 +0 -0
  45. package/readme-assets/lucide-book-open.svg +14 -0
  46. package/readme-assets/lucide-play.svg +13 -0
  47. package/readme-assets/lucide-shield-check.svg +14 -0
  48. package/src/lib/check.js +75 -2
  49. package/src/lib/config.js +23 -0
  50. package/src/lib/install.js +2 -1
  51. package/src/lib/modelConfig.js +11 -7
  52. package/src/lib/modelRoutePolicy.js +94 -51
  53. package/src/lib/skillPayload.js +1 -1
package/src/lib/check.js CHANGED
@@ -1,8 +1,8 @@
1
1
  const fs = require("node:fs");
2
2
  const path = require("node:path");
3
3
  const { spawnSync } = require("node:child_process");
4
- const { configHasLitHermes, inspectHermesCapabilities, readConfig } = require("./config");
5
- const { detectHermesRuntimeRepo, detectHermesVersion, ensureHermesHome, inspectHermesHostCapabilities, LitHermesError } = require("./hermesDiscovery");
4
+ const { configHasLitHermes, inspectHermesCapabilities, readConfig, readExternalSkillDirsConfig } = require("./config");
5
+ const { detectHermesRuntimeRepo, detectHermesVersion, ensureHermesHome, expandHome, inspectHermesHostCapabilities, LitHermesError } = require("./hermesDiscovery");
6
6
  const { formatManagedRoute } = require("./modelRoutePolicy");
7
7
  const { inspectPythonCache } = require("./files");
8
8
  const { assetRoot, pluginDest } = require("./install");
@@ -112,6 +112,75 @@ function loadedStatusMatchesOrigin(output, expectedPluginPath) {
112
112
  }
113
113
  }
114
114
 
115
+ function isRegularSkillFile(file) {
116
+ try {
117
+ return fs.lstatSync(file).isFile();
118
+ } catch {
119
+ return false;
120
+ }
121
+ }
122
+
123
+ // Bounded, non-symlink-following scan for `<HERMES_HOME>/skills/**/<id>/SKILL.md`.
124
+ // Real local-skill trees are shallow, so a small depth/entry cap keeps this cheap
125
+ // while still catching a collision a couple of category folders deep.
126
+ function collectLocalSkillShadows(hermesHome, skillIds, { maxDepth = 3, maxEntries = 2000 } = {}) {
127
+ const remaining = new Set(skillIds);
128
+ const found = new Map();
129
+ let visited = 0;
130
+ function walk(dir, depth) {
131
+ if (!remaining.size || depth > maxDepth || visited >= maxEntries) return;
132
+ let entries;
133
+ try {
134
+ entries = fs.readdirSync(dir, { withFileTypes: true });
135
+ } catch {
136
+ return;
137
+ }
138
+ for (const entry of entries) {
139
+ if (!remaining.size || visited >= maxEntries) return;
140
+ visited += 1;
141
+ if (!entry.isDirectory()) continue;
142
+ const full = path.join(dir, entry.name);
143
+ if (remaining.has(entry.name)) {
144
+ const skillFile = path.join(full, "SKILL.md");
145
+ if (isRegularSkillFile(skillFile)) {
146
+ found.set(entry.name, skillFile);
147
+ remaining.delete(entry.name);
148
+ continue;
149
+ }
150
+ }
151
+ walk(full, depth + 1);
152
+ }
153
+ }
154
+ walk(path.join(hermesHome, "skills"), 0);
155
+ return found;
156
+ }
157
+
158
+ // Hermes resolves a bare skill name (not the `lithermes:<name>` explicit-load form)
159
+ // against `skills.external_dirs` and local `<HERMES_HOME>/skills`; a same-named entry
160
+ // there is picked instead of the LitHermes plugin copy. This is advisory: it never
161
+ // changes doctor's pass/fail status, only surfaces the collision.
162
+ function detectSkillShadows({ configText, hermesHome, skillIds = requiredSkills }) {
163
+ const externalDirs = readExternalSkillDirsConfig(configText)
164
+ .map((dir) => path.resolve(expandHome(dir)));
165
+ const localShadows = collectLocalSkillShadows(hermesHome, skillIds);
166
+ const warnings = [];
167
+ for (const id of skillIds) {
168
+ const externalDir = externalDirs.find((dir) => isRegularSkillFile(path.join(dir, id, "SKILL.md")));
169
+ if (externalDir) {
170
+ warnings.push({ id, path: path.join(externalDir, id, "SKILL.md"), source: "skills.external_dirs" });
171
+ continue;
172
+ }
173
+ const localPath = localShadows.get(id);
174
+ if (localPath) warnings.push({ id, path: localPath, source: "local skills" });
175
+ }
176
+ return warnings;
177
+ }
178
+
179
+ function formatSkillShadowWarning(shadow) {
180
+ return `WARNING: skill "${shadow.id}" bare name resolves to ${shadow.path} (${shadow.source}); `
181
+ + `use lithermes:${shadow.id} to load LitHermes's copy, or remove/rename the other copy.`;
182
+ }
183
+
115
184
  function checkLitHermes(flags = {}) {
116
185
  const { hermesHome, hermesRepo } = ensureHermesHome(flags);
117
186
  const pluginPath = pluginDest(hermesHome);
@@ -187,6 +256,7 @@ function doctorLitHermes(flags = {}) {
187
256
  const unsafeRecursion = recursion.status === "unavailable" && recursion.reason.startsWith("nested delegation");
188
257
  const unsafeModelRoute = modelSafety.status === "blocked";
189
258
  const runtime = capabilities.runtime;
259
+ const skillShadows = detectSkillShadows({ configText, hermesHome });
190
260
  const lines = [
191
261
  `plugin discovery: ${installedPass && configOk ? "PASS" : "PATCH_AVAILABLE"}`,
192
262
  `bundled source: ${bundledPass ? "PASS" : "FAIL"}`,
@@ -198,6 +268,8 @@ function doctorLitHermes(flags = {}) {
198
268
  `installed payload: ${installedPass ? "PASS" : "FAIL"}`,
199
269
  `enabled config: ${configOk ? "PASS" : "PARTIAL"}`,
200
270
  `loaded plugin: ${loaded.status} (${loaded.reason})`,
271
+ `skill shadow check: ${skillShadows.length ? "WARNING" : "PASS"}`,
272
+ ...skillShadows.map(formatSkillShadowWarning),
201
273
  `auto-compaction: ${auto.status}${auto.reason ? ` (${auto.reason})` : ""}`,
202
274
  `concurrency: ${concurrency.status}${concurrency.status === "hard" ? ` (per batch ${concurrency.limit}; async batches ${concurrency.asyncBatchLimit}; potential children ${concurrency.potentialChildCeiling})` : ` (${concurrency.reason})`}`,
203
275
  `recursion: ${recursion.status}${recursion.status === "hard" ? recursion.flatBy === "depth" ? ` (depth ${recursion.limit}, flat)` : ` (configured depth ${recursion.limit}, flat via orchestrator kill switch)` : ` (${recursion.reason})`}`,
@@ -222,6 +294,7 @@ module.exports = {
222
294
  checkGatewaySource,
223
295
  checkLitHermes,
224
296
  checkTuiSource,
297
+ detectSkillShadows,
225
298
  doctorLitHermes,
226
299
  enabledUserJsonIncludesPlugin,
227
300
  loadedStatusMatchesOrigin,
package/src/lib/config.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const fs = require("node:fs");
2
2
  const path = require("node:path");
3
+ const { parseDocument } = require("yaml");
3
4
  const { writeFileAtomic } = require("./files");
4
5
  const modelConfig = require("./modelConfig");
5
6
 
@@ -205,6 +206,27 @@ function clearOutputStyleConfig(text) {
205
206
  return `${updated.replace(/\n*$/, "")}\n`;
206
207
  }
207
208
 
209
+ // Hermes `skills.external_dirs` (config.yaml key, default []): bare-name skill
210
+ // resolution searches these directories, so a same-named directory there can shadow
211
+ // a LitHermes plugin skill (which is only reachable unambiguously as `lithermes:<name>`).
212
+ // Malformed YAML fails closed to an empty list rather than throwing during doctor.
213
+ function readExternalSkillDirsConfig(text) {
214
+ const str = String(text || "");
215
+ if (!str.trim()) return [];
216
+ try {
217
+ const document = parseDocument(str, { strict: true, uniqueKeys: true });
218
+ if (document.errors.length) return [];
219
+ const node = document.getIn(["skills", "external_dirs"], true);
220
+ const value = node && typeof node.toJS === "function" ? node.toJS(document) : node;
221
+ if (!Array.isArray(value)) return [];
222
+ return value
223
+ .filter((entry) => typeof entry === "string" && entry.trim())
224
+ .map((entry) => entry.trim());
225
+ } catch {
226
+ return [];
227
+ }
228
+ }
229
+
208
230
  function writeConfig(hermesHome, text) {
209
231
  fs.mkdirSync(hermesHome, { recursive: true });
210
232
  writeFileAtomic(configPath(hermesHome), text, "utf8");
@@ -220,6 +242,7 @@ module.exports = {
220
242
  enableLitHermesConfig,
221
243
  readConfig,
222
244
  readDisplaySkinConfig,
245
+ readExternalSkillDirsConfig,
223
246
  readOutputStyleConfig,
224
247
  readTuiAgentsNudgeConfig,
225
248
  setDisplaySkinConfig,
@@ -17,6 +17,7 @@ const { assertStateParent, assertPluginParent, defaultHermesHome, defaultHermesR
17
17
  const { NOT_PROBED_HOST_VERSION } = require("./modelConfigBoundary");
18
18
  const { patchInstalledHermes, rollbackPatches } = require("./patch");
19
19
  const {
20
+ ASTRA_MODEL,
20
21
  formatManagedRoute,
21
22
  managedRequest,
22
23
  missingCredentialWarning,
@@ -344,7 +345,7 @@ function installLitHermes(flags = {}) {
344
345
  );
345
346
  }
346
347
  if (dryRun) {
347
- const selectedModel = modelPlan.request?.model || "gpt-6-astra";
348
+ const selectedModel = modelPlan.request?.model || ASTRA_MODEL;
348
349
  const selectedEffort = modelPlan.request?.effort || "xhigh";
349
350
  return {
350
351
  message: [
@@ -6,6 +6,7 @@ const { OPENAI_PROVIDER, RESPONSES_MODE, classifyHostVersion,
6
6
  const {
7
7
  ASTRA_MODEL,
8
8
  MANAGED_PROVIDER_MODELS,
9
+ MODEL_IDS,
9
10
  applyManagedRoute,
10
11
  customChildTransportReason,
11
12
  inspectEffectiveRouteSafety,
@@ -17,8 +18,6 @@ const {
17
18
  unavailableTuiRouteVisibility,
18
19
  } = require("./modelRoutePolicy");
19
20
 
20
- const MODELS = new Set(["gpt-6-astra", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]);
21
-
22
21
  // The route every "configured" verdict is measured against: explicit installer
23
22
  // options win; otherwise the document's own route when it is a catalog route;
24
23
  // otherwise today's shipped default.
@@ -45,6 +44,12 @@ function expectedRoute(options = {}, document = null) {
45
44
  const SAFE_CONTEXT_LENGTH = 372000;
46
45
  const SAFE_COMPRESSION_THRESHOLD = 0.9;
47
46
  const SAFE_AUTO_COMPACTION_LIMIT = SAFE_CONTEXT_LENGTH * SAFE_COMPRESSION_THRESHOLD;
47
+ const LEGACY_CONTEXT_MODELS = new Set([
48
+ "gpt-5.6",
49
+ "gpt-5.6-sol",
50
+ "gpt-5.6-terra",
51
+ "gpt-5.6-luna",
52
+ ]);
48
53
  const MANAGED_SAMPLING_KEYS = ["temperature", "top_p", "top_logprobs"];
49
54
 
50
55
  function hasValue(document, path) {
@@ -156,7 +161,7 @@ function classificationFromDocument(document, fresh) {
156
161
  if (provider === OPENAI_PROVIDER && (dispatch === "gpt-5.4" || dispatch === "gpt-5.5")) {
157
162
  return { class: "managed_legacy", originalDispatchId };
158
163
  }
159
- if (provider === OPENAI_PROVIDER && MODELS.has(dispatch)) {
164
+ if (provider === OPENAI_PROVIDER && MODEL_IDS.has(dispatch)) {
160
165
  return { class: "existing_managed", originalDispatchId };
161
166
  }
162
167
  if (MANAGED_PROVIDER_MODELS[provider]?.has(dispatch)) {
@@ -364,10 +369,9 @@ function planModelConfig(text, options = {}) {
364
369
  }
365
370
  const document = boundary.document;
366
371
  applyManagedRoute(document, route);
367
- // The verified legacy context/compression defaults belong to legacy model
368
- // resets only. Astra has no host-published limit evidence, so an Astra reset
369
- // preserves explicit values and leaves missing values absent.
370
- if (options.reconfigure && route.model !== ASTRA_MODEL && route.childModel !== ASTRA_MODEL) {
372
+ // The verified context/compression defaults apply only to explicit GPT-5.6
373
+ // model resets. GPT-6 model routes have no published evidence for these limits.
374
+ if (options.reconfigure && LEGACY_CONTEXT_MODELS.has(route.model)) {
371
375
  document.setIn(["model", "context_length"], SAFE_CONTEXT_LENGTH);
372
376
  document.setIn(["compression", "threshold"], SAFE_COMPRESSION_THRESHOLD);
373
377
  }
@@ -1,38 +1,77 @@
1
1
  const ASTRA_MODEL = "gpt-6-astra";
2
- const ASTRA_EFFORTS = new Set(["low", "medium", "high", "xhigh", "max"]);
2
+ const GPT6_LEAD_EFFORTS = new Set(["low", "medium", "high", "xhigh", "max", "ultra"]);
3
+ const ASTRA_EFFORTS = GPT6_LEAD_EFFORTS;
3
4
  const PARENT_MODEL = ASTRA_MODEL;
4
5
  const PARENT_EFFORT = "xhigh";
5
- const CHILD_MODEL = "gpt-5.6-luna";
6
+ const SOL_MODEL = "gpt-6-sol";
7
+ const CHILD_MODEL = "gpt-6-luna";
8
+ const LEGACY_SOL_MODEL = "gpt-5.6-sol";
9
+ const LEGACY_CHILD_MODEL = "gpt-5.6-luna";
6
10
  const CHILD_EFFORT = "max";
7
- const LUNA_EFFORTS = new Set(["high", "max"]);
11
+ const GPT6_LUNA_EFFORTS = new Set(["low", "medium", "high", "xhigh", "max"]);
12
+ const LEGACY_LUNA_EFFORTS = new Set(["high", "max"]);
13
+ const LUNA_EFFORTS = LEGACY_LUNA_EFFORTS;
14
+ const LUNA_MODELS = new Set([CHILD_MODEL, LEGACY_CHILD_MODEL]);
8
15
  const TERRA_MODEL = "gpt-5.6-terra";
9
16
  const TERRA_EFFORTS = new Set(["high", "xhigh", "max"]);
17
+ const LEGACY_SOL_EFFORTS = new Set(["high", "xhigh"]);
18
+ const MODEL_EFFORTS = new Map([
19
+ [ASTRA_MODEL, GPT6_LEAD_EFFORTS],
20
+ [SOL_MODEL, GPT6_LEAD_EFFORTS],
21
+ [CHILD_MODEL, GPT6_LUNA_EFFORTS],
22
+ [LEGACY_SOL_MODEL, LEGACY_SOL_EFFORTS],
23
+ [LEGACY_CHILD_MODEL, LEGACY_LUNA_EFFORTS],
24
+ ["gpt-5.6", new Set(["high"])],
25
+ [TERRA_MODEL, TERRA_EFFORTS],
26
+ ]);
10
27
  const CHILD_TRANSPORT_KEYS = ["provider", "base_url", "api_mode"];
11
28
  const { OPENAI_PROVIDER: OPENAI_PROVIDER_ID } = require("./modelConfigBoundary");
12
29
  const XAI_PROVIDER_ID = "xai";
13
30
  // Installer choice catalog (plans/references/installer-choice-contract.md, LitHermes row).
14
- // OpenAI rows: Astra exposes exactly the five Responses efforts; the existing Sol,
15
- // Luna, and general GPT-5.6 rows remain below it and Luna+xhigh is never listed. xAI rows: the
16
- // reasoning-capable models of the Hermes 0.19 static xAI catalog
31
+ // OpenAI rows follow each model's listed effort set, including selectable legacy
32
+ // GPT-5.6 routes. xAI rows use the reasoning-capable models of the Hermes 0.19 catalog
17
33
  // (hermes_cli/models.py `_XAI_STATIC_FALLBACK`), newest first, with the Hermes
18
34
  // reasoning_effort vocabulary (hermes_cli/config.py delegation.reasoning_effort).
19
35
  const ROUTE_CATALOG = {
20
36
  [OPENAI_PROVIDER_ID]: {
21
37
  credentialEnv: null,
22
- label: "OpenAI OAuth Responses (Astra + GPT-5.6)",
38
+ label: "OpenAI OAuth Responses (Astra + Sol + Luna)",
23
39
  rows: [
24
- ...["low", "medium", "high", "xhigh", "max"].map((effort) => ({
40
+ ...["low", "medium", "high", "xhigh", "max", "ultra"].map((effort) => ({
25
41
  effort,
26
42
  model: ASTRA_MODEL,
27
- note: effort === "max"
43
+ note: effort === "ultra"
28
44
  ? "deepest Astra reasoning"
29
45
  : effort === PARENT_EFFORT
30
46
  ? "recommended lead"
31
47
  : "Astra Responses effort",
32
48
  })),
33
- { effort: PARENT_EFFORT, model: "gpt-5.6-sol", note: "existing Sol lead route" },
34
- { effort: CHILD_EFFORT, model: CHILD_MODEL, note: "balanced (recommended helper)" },
35
- { effort: "high", model: CHILD_MODEL, note: "balanced, lighter effort" },
49
+ ...["low", "medium", "high", "xhigh", "max", "ultra"].map((effort) => ({
50
+ effort,
51
+ model: SOL_MODEL,
52
+ note: effort === PARENT_EFFORT ? "coding lead alternative" : "Sol Responses effort",
53
+ })),
54
+ ...["low", "medium", "high", "xhigh", "max"].map((effort) => ({
55
+ effort,
56
+ model: CHILD_MODEL,
57
+ note: effort === CHILD_EFFORT
58
+ ? "balanced (recommended helper)"
59
+ : effort === "high"
60
+ ? "balanced, lighter effort"
61
+ : "Luna Responses effort",
62
+ })),
63
+ ...["high", "xhigh"].map((effort) => ({
64
+ effort,
65
+ model: LEGACY_SOL_MODEL,
66
+ note: "previous-generation Sol lead route",
67
+ })),
68
+ ...["high", "xhigh", "max"].map((effort) => ({
69
+ effort,
70
+ model: TERRA_MODEL,
71
+ note: "previous-generation Terra route",
72
+ })),
73
+ { effort: CHILD_EFFORT, model: LEGACY_CHILD_MODEL, note: "previous-generation balanced helper" },
74
+ { effort: "high", model: LEGACY_CHILD_MODEL, note: "previous-generation lighter helper" },
36
75
  { effort: "high", model: "gpt-5.6", note: "general model" },
37
76
  ],
38
77
  lead: { effort: PARENT_EFFORT, model: PARENT_MODEL },
@@ -55,6 +94,10 @@ const PROVIDER_IDS = Object.keys(ROUTE_CATALOG);
55
94
  const MANAGED_PROVIDER_MODELS = Object.fromEntries(
56
95
  PROVIDER_IDS.map((id) => [id, new Set(ROUTE_CATALOG[id].rows.map((row) => row.model))]),
57
96
  );
97
+ const MODEL_IDS = new Set([
98
+ ...MANAGED_PROVIDER_MODELS[OPENAI_PROVIDER_ID],
99
+ TERRA_MODEL,
100
+ ]);
58
101
  const MANAGED_PROVIDERS = new Set(PROVIDER_IDS);
59
102
  const UNSAFE_ROUTE_CODE = "LITHERMES_UNSAFE_MODEL_ROUTE";
60
103
  const REVIEWER_ROUTE_NAME = String.fromCharCode(109, 111, 109, 117, 115);
@@ -83,42 +126,32 @@ function inspectEffectiveRouteSafety(document, expectedProvider) {
83
126
  const managedRoutes = routes.filter((route) => route.provider === expectedProvider);
84
127
  for (const route of managedRoutes) {
85
128
  if (transportReason && route.route === "global_child") continue;
86
- if (route.model === ASTRA_MODEL && !ASTRA_EFFORTS.has(route.effort)) {
87
- return {
88
- classificationClass: "astra_effort_unsupported",
89
- code: UNSAFE_ROUTE_CODE,
90
- observedSource: `${route.route} model, effort, and provider config`,
91
- reason: `${route.route} Astra effort is unsupported or missing; choose one of: ${[...ASTRA_EFFORTS].join(", ")}`,
92
- route: route.route,
93
- status: "blocked",
94
- };
95
- }
96
- if (route.model === CHILD_MODEL && route.effort === "xhigh") {
97
- return {
98
- classificationClass: "conflicting_luna_xhigh",
99
- code: UNSAFE_ROUTE_CODE,
100
- observedSource: `${route.route} model, effort, and provider config`,
101
- reason: `${route.route} Luna xhigh is a forbidden pair (Luna runs at high or max)`,
102
- route: route.route,
103
- status: "blocked",
104
- };
105
- }
106
- if (route.model === CHILD_MODEL && !LUNA_EFFORTS.has(route.effort)) {
107
- return {
108
- classificationClass: "luna_effort_below_high",
109
- code: UNSAFE_ROUTE_CODE,
110
- observedSource: `${route.route} model, effort, and provider config`,
111
- reason: `${route.route} Luna effort is below high or missing`,
112
- route: route.route,
113
- status: "blocked",
114
- };
115
- }
116
- if (route.model === TERRA_MODEL && !TERRA_EFFORTS.has(route.effort)) {
129
+ const modelEfforts = MODEL_EFFORTS.get(route.model);
130
+ if (modelEfforts && !modelEfforts.has(route.effort)) {
131
+ const isAstra = route.model === ASTRA_MODEL;
132
+ const isLuna = LUNA_MODELS.has(route.model);
133
+ const isLegacyLunaXhigh = route.model === LEGACY_CHILD_MODEL && route.effort === "xhigh";
117
134
  return {
118
- classificationClass: "terra_effort_below_high",
135
+ classificationClass: isAstra
136
+ ? "astra_effort_unsupported"
137
+ : isLegacyLunaXhigh
138
+ ? "conflicting_luna_xhigh"
139
+ : isLuna
140
+ ? "luna_effort_below_high"
141
+ : route.model === TERRA_MODEL
142
+ ? "terra_effort_below_high"
143
+ : "model_effort_unsupported",
119
144
  code: UNSAFE_ROUTE_CODE,
120
145
  observedSource: `${route.route} model, effort, and provider config`,
121
- reason: `${route.route} TERRA effort is below high or missing`,
146
+ reason: isAstra
147
+ ? `${route.route} Astra effort is unsupported or missing; choose one of: ${[...modelEfforts].join(", ")}`
148
+ : isLegacyLunaXhigh
149
+ ? `${route.route} Luna xhigh is a forbidden pair (legacy Luna runs at high or max)`
150
+ : isLuna
151
+ ? `${route.route} Luna effort is unsupported; choose one of: ${[...modelEfforts].join(", ")}`
152
+ : route.model === TERRA_MODEL
153
+ ? `${route.route} TERRA effort is below high or missing`
154
+ : `${route.route} ${route.model} effort is unsupported or missing; choose one of: ${[...modelEfforts].join(", ")}`,
122
155
  route: route.route,
123
156
  status: "blocked",
124
157
  };
@@ -140,7 +173,7 @@ function inspectEffectiveRouteSafety(document, expectedProvider) {
140
173
  }
141
174
  return {
142
175
  observedSource: "parent and global delegation model policy",
143
- reason: "no effective under-effort Luna or TERRA route is configured",
176
+ reason: "no effective model and effort pair outside the model catalog is configured",
144
177
  status: "safe",
145
178
  };
146
179
  }
@@ -162,8 +195,8 @@ function resolveRow(provider, role, model, effort) {
162
195
  const hasModel = model !== undefined;
163
196
  const hasEffort = effort !== undefined;
164
197
  const resolvedModel = hasModel ? String(model) : preset.model;
165
- if (resolvedModel === CHILD_MODEL && hasEffort && effort === "xhigh") {
166
- throw new Error(`${role} route ${CHILD_MODEL} xhigh is a forbidden pair; Luna runs at high or max`);
198
+ if (resolvedModel === LEGACY_CHILD_MODEL && hasEffort && effort === "xhigh") {
199
+ throw new Error(`${role} route ${resolvedModel} xhigh is a forbidden pair; legacy Luna runs at high or max`);
167
200
  }
168
201
  const rowsForModel = catalog.rows.filter((row) => row.model === resolvedModel);
169
202
  if (!rowsForModel.length) {
@@ -244,10 +277,10 @@ function inspectManagedRoute(document, expectedProvider, route = managedRequest(
244
277
  && childModel === route.childModel
245
278
  && childEffort === route.childEffort;
246
279
  const legacyConfigured = parentProvider === expectedProvider
247
- && parentModel === "gpt-5.6-luna"
280
+ && parentModel === LEGACY_CHILD_MODEL
248
281
  && parentEffort === "max"
249
- && childModel === CHILD_MODEL
250
- && LUNA_EFFORTS.has(childEffort);
282
+ && childModel === LEGACY_CHILD_MODEL
283
+ && LEGACY_LUNA_EFFORTS.has(childEffort);
251
284
  if (!configured && !legacyConfigured) {
252
285
  return {
253
286
  status: "unavailable",
@@ -359,8 +392,18 @@ function formatManagedRoute(route) {
359
392
  module.exports = {
360
393
  ASTRA_EFFORTS,
361
394
  ASTRA_MODEL,
395
+ CHILD_MODEL,
396
+ GPT6_LEAD_EFFORTS,
397
+ GPT6_LUNA_EFFORTS,
398
+ LEGACY_LUNA_EFFORTS,
399
+ LEGACY_CHILD_MODEL,
400
+ LEGACY_SOL_MODEL,
401
+ LUNA_EFFORTS,
402
+ LUNA_MODELS,
362
403
  MANAGED_PROVIDER_MODELS,
404
+ MODEL_IDS,
363
405
  OPENAI_PROVIDER_ID,
406
+ SOL_MODEL,
364
407
  PROVIDER_IDS,
365
408
  ROUTE_CATALOG,
366
409
  XAI_PROVIDER_ID,
@@ -16,7 +16,7 @@ const MAX_PAYLOAD_TOTAL_BYTES = 16 * 1024 * 1024;
16
16
  const requiredSkills = [
17
17
  "lit-burnoff-file", "autoconference", "autoresearch", "browser-drive", "comment-checker", "debugging",
18
18
  "deep-interview",
19
- "frontend-ui-ux", "lit-commit", "lit-crucible", "lit-init",
19
+ "frontend-ui-ux", "readme-studio", "lit-commit", "lit-crucible", "lit-init",
20
20
  "lit-korean", "lit-comprehend", "lit-handoff", "lit-plan", "lit-recap",
21
21
  "lit-scientific-visualization", "litgoal", "litresearch", "litwork", "lsp",
22
22
  "lsp-setup", "lit-code", "refactor", "lit-burnoff", "review-work",