oh-my-opencode 5.1.40 → 5.1.42

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 (147) hide show
  1. package/README.de.md +118 -0
  2. package/README.es.md +118 -0
  3. package/README.fr.md +118 -0
  4. package/README.it.md +118 -0
  5. package/README.ja.md +2 -1
  6. package/README.ko.md +2 -1
  7. package/README.md +3 -2
  8. package/README.pt-br.md +118 -0
  9. package/README.ru.md +2 -1
  10. package/README.zh-cn.md +2 -1
  11. package/dist/cli/index.js +115 -27
  12. package/dist/cli/service-auth/index.d.ts +3 -0
  13. package/dist/cli-node/index.js +115 -27
  14. package/dist/index.js +1467 -1389
  15. package/dist/skills/browser/runtime/omowright/manifest.json +1 -1
  16. package/dist/skills/frontend/SKILL.md +6 -6
  17. package/dist/skills/frontend/references/design/README.md +2 -2
  18. package/dist/skills/frontend/references/perfection/README.md +2 -2
  19. package/dist/skills/frontend/scripts/perfection/accent-stripes/declarations.ts +22 -13
  20. package/dist/skills/frontend/scripts/perfection/accent-stripes/findings.ts +7 -6
  21. package/dist/skills/frontend/scripts/perfection/accent-stripes/lint.ts +18 -24
  22. package/dist/skills/frontend/scripts/perfection/accent-stripes/markup.ts +14 -7
  23. package/dist/skills/frontend/scripts/perfection/accent-stripes/selector.ts +5 -1
  24. package/dist/skills/frontend/scripts/perfection/accent-stripes/tailwind.ts +32 -14
  25. package/dist/skills/frontend/scripts/perfection/check-accent-stripes.test.ts +15 -6
  26. package/dist/skills/frontend/scripts/perfection/check-accent-stripes.ts +1 -1
  27. package/dist/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/inline-ok.tsx +1 -0
  28. package/dist/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/neutral-and-flat.css +1 -1
  29. package/dist/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/tailwind-ok.tsx +5 -2
  30. package/dist/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/uniform-and-rings.css +2 -2
  31. package/dist/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/accent-outlines.css +4 -0
  32. package/dist/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/accent-outlines.tsx +8 -0
  33. package/dist/skills/frontend/scripts/perfection/fixtures/accent-stripes/{negative → positive}/flat-stripe-side.tsx +3 -1
  34. package/dist/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/report-callouts.html +8 -0
  35. package/dist/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/square-surfaces.css +8 -0
  36. package/dist/skills/ulw-research/SKILL.md +10 -2
  37. package/dist/skills/ulw-research/references/deliverable-phase.md +10 -0
  38. package/dist/skills/ulw-research/references/report-visualization.md +23 -0
  39. package/dist/tui.js +108 -26
  40. package/package.json +14 -14
  41. package/packages/lsp-core/src/lsp/fixtures/disposed-stdin-epipe-probe.ts +28 -0
  42. package/packages/lsp-core/src/lsp/process-pipe-errors.test.ts +21 -0
  43. package/packages/lsp-core/src/lsp/process.ts +16 -0
  44. package/packages/lsp-daemon/dist/cli.js +10 -0
  45. package/packages/lsp-daemon/dist/client.js +10 -0
  46. package/packages/lsp-daemon/dist/index.js +10 -0
  47. package/packages/lsp-tools-mcp/dist/cli.js +10 -0
  48. package/packages/lsp-tools-mcp/dist/lsp/manager.js +10 -0
  49. package/packages/lsp-tools-mcp/dist/mcp.js +10 -0
  50. package/packages/lsp-tools-mcp/dist/tools.js +10 -0
  51. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  52. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +84 -9
  53. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  54. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  55. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  56. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  57. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  58. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  59. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  60. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  61. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
  62. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +10 -0
  63. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  64. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  65. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  66. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  67. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  68. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  69. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  70. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  71. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  72. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  73. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  74. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  75. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  76. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  77. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  78. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  79. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  80. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  81. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  82. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  83. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  84. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  85. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  86. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  87. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  88. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  89. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  90. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  91. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  92. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  93. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  94. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  95. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  96. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  97. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  98. package/packages/omo-codex/plugin/package-lock.json +12 -12
  99. package/packages/omo-codex/plugin/package.json +1 -1
  100. package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
  101. package/packages/omo-codex/plugin/skills/frontend/SKILL.md +6 -6
  102. package/packages/omo-codex/plugin/skills/frontend/references/design/README.md +2 -2
  103. package/packages/omo-codex/plugin/skills/frontend/references/perfection/README.md +2 -2
  104. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/declarations.ts +22 -13
  105. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/findings.ts +7 -6
  106. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/lint.ts +18 -24
  107. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/markup.ts +14 -7
  108. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/selector.ts +5 -1
  109. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/tailwind.ts +32 -14
  110. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/check-accent-stripes.ts +1 -1
  111. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/inline-ok.tsx +1 -0
  112. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/neutral-and-flat.css +1 -1
  113. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/tailwind-ok.tsx +5 -2
  114. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/uniform-and-rings.css +2 -2
  115. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/accent-outlines.css +4 -0
  116. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/accent-outlines.tsx +8 -0
  117. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/{negative → positive}/flat-stripe-side.tsx +3 -1
  118. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/report-callouts.html +8 -0
  119. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/square-surfaces.css +8 -0
  120. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +10 -2
  121. package/packages/omo-codex/plugin/skills/ulw-research/references/deliverable-phase.md +10 -0
  122. package/packages/omo-codex/plugin/skills/ulw-research/references/report-visualization.md +23 -0
  123. package/packages/omo-codex/scripts/install-dist/install-local.mjs +86 -11
  124. package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +1 -1
  125. package/packages/shared-skills/skills/frontend/SKILL.md +6 -6
  126. package/packages/shared-skills/skills/frontend/references/design/README.md +2 -2
  127. package/packages/shared-skills/skills/frontend/references/perfection/README.md +2 -2
  128. package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/declarations.ts +22 -13
  129. package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/findings.ts +7 -6
  130. package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/lint.ts +18 -24
  131. package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/markup.ts +14 -7
  132. package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/selector.ts +5 -1
  133. package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/tailwind.ts +32 -14
  134. package/packages/shared-skills/skills/frontend/scripts/perfection/check-accent-stripes.test.ts +15 -6
  135. package/packages/shared-skills/skills/frontend/scripts/perfection/check-accent-stripes.ts +1 -1
  136. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/inline-ok.tsx +1 -0
  137. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/neutral-and-flat.css +1 -1
  138. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/tailwind-ok.tsx +5 -2
  139. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/uniform-and-rings.css +2 -2
  140. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/accent-outlines.css +4 -0
  141. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/accent-outlines.tsx +8 -0
  142. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/{negative → positive}/flat-stripe-side.tsx +3 -1
  143. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/report-callouts.html +8 -0
  144. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/square-surfaces.css +8 -0
  145. package/packages/shared-skills/skills/ulw-research/SKILL.md +10 -2
  146. package/packages/shared-skills/skills/ulw-research/references/deliverable-phase.md +10 -0
  147. package/packages/shared-skills/skills/ulw-research/references/report-visualization.md +23 -0
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // omo-codex-install:b408b576ee9f2c827cd2733d966cd724fd27f029332824185037c443d3f8dd57:45ee374af9a88da9712fc3e3f8e220dbdc302cebb6f4025884612f07180b8a02
2
+ // omo-codex-install:4d55fdfa5d8b8b2fa08c2b8d004c4fa2b39e08b429c29148b9f810ce43827a22:e7a2266fd69e56b66a85bbbb1c3d99018a8b8647bb2c30a5afb575cff97118dc
3
3
  var __esm = (fn, res, err) => () => {
4
4
  if (fn)
5
5
  try {
@@ -10036,7 +10036,7 @@ var package_default;
10036
10036
  var init_package = __esm(() => {
10037
10037
  package_default = {
10038
10038
  name: "@oh-my-opencode/omo-codex",
10039
- version: "5.1.40",
10039
+ version: "5.1.42",
10040
10040
  type: "module",
10041
10041
  private: true,
10042
10042
  description: "Codex harness adapter for oh-my-openagent. Vendored Codex plugin namespace (omo) + TypeScript installer + telemetry.",
@@ -19486,6 +19486,19 @@ var OmoHarnessIdSchema = _enum(OMO_CONFIG_HARNESS_IDS);
19486
19486
  var OMO_CONFIG_LEGACY_HARNESS_ALIASES = { senpi: "native" };
19487
19487
  var OMO_CONFIG_LEGACY_HARNESS_IDS = Object.keys(OMO_CONFIG_LEGACY_HARNESS_ALIASES);
19488
19488
 
19489
+ // packages/omo-config-core/src/schema/memory-agreements.ts
19490
+ var OMO_MEMORY_AGREEMENTS_DEFAULTS = { enabled: true, needs_human_threshold: 3, max_items: 64 };
19491
+ var OmoMemoryAgreementsSchema = object({
19492
+ enabled: boolean2().default(OMO_MEMORY_AGREEMENTS_DEFAULTS.enabled),
19493
+ needs_human_threshold: number2().int().min(1).default(OMO_MEMORY_AGREEMENTS_DEFAULTS.needs_human_threshold),
19494
+ max_items: number2().int().min(8).max(256).default(OMO_MEMORY_AGREEMENTS_DEFAULTS.max_items)
19495
+ }).strict();
19496
+ var OmoMemoryAgreementsLayerSchema = object({
19497
+ enabled: boolean2().optional(),
19498
+ needs_human_threshold: number2().int().min(1).optional(),
19499
+ max_items: number2().int().min(8).max(256).optional()
19500
+ }).strict();
19501
+
19489
19502
  // packages/omo-config-core/src/schema/memory.ts
19490
19503
  var OmoMemoryReflectionTriggerSchema = object({
19491
19504
  step_count: number2().int().nonnegative().default(25),
@@ -19629,6 +19642,7 @@ var OmoMemoryAgentOverridesSchema = object({
19629
19642
  facts: OmoMemoryFactsLayerSchema.optional(),
19630
19643
  dream: OmoMemoryDreamLayerSchema.optional(),
19631
19644
  people: OmoMemoryPeopleLayerSchema.optional(),
19645
+ agreements: OmoMemoryAgreementsLayerSchema.optional(),
19632
19646
  soul: OmoMemorySoulLayerSchema.optional(),
19633
19647
  write_notice: OmoMemoryWriteNoticeLayerSchema.optional(),
19634
19648
  sync: OmoMemorySyncLayerSchema.optional(),
@@ -19637,9 +19651,11 @@ var OmoMemoryAgentOverridesSchema = object({
19637
19651
  projection: OmoMemoryProjectionLayerSchema.optional(),
19638
19652
  compile_warn_tokens: number2().int().positive().optional()
19639
19653
  }).strict();
19654
+ var OmoMemoryAutoIdentitySchema = _enum(["path", "git"]);
19640
19655
  var OmoMemorySettingsSchema = object({
19641
19656
  enabled: boolean2().default(true),
19642
19657
  agent: string2().min(1).default("auto"),
19658
+ auto_identity: OmoMemoryAutoIdentitySchema.default("path"),
19643
19659
  reflection: OmoMemoryReflectionSchema.default({
19644
19660
  enabled: true,
19645
19661
  trigger: { step_count: 25, on_compaction: true },
@@ -19659,6 +19675,7 @@ var OmoMemorySettingsSchema = object({
19659
19675
  auto_select_max_chars: 150000
19660
19676
  }),
19661
19677
  people: OmoMemoryPeopleSchema.default({ enabled: true, max_entries: 40, max_entry_chars: 200 }),
19678
+ agreements: OmoMemoryAgreementsSchema.default(OMO_MEMORY_AGREEMENTS_DEFAULTS),
19662
19679
  soul: OmoMemorySoulSchema.default({ edit_notice: true }),
19663
19680
  write_notice: OmoMemoryWriteNoticeSchema.default({ enabled: true }),
19664
19681
  sync: OmoMemorySyncSchema.default({ enabled: true }),
@@ -19680,11 +19697,13 @@ var OmoMemorySettingsSchema = object({
19680
19697
  var OmoMemorySettingsLayerSchema = object({
19681
19698
  enabled: boolean2().optional(),
19682
19699
  agent: string2().min(1).optional(),
19700
+ auto_identity: OmoMemoryAutoIdentitySchema.optional(),
19683
19701
  reflection: OmoMemoryReflectionLayerSchema.optional(),
19684
19702
  nudge: OmoMemoryNudgeLayerSchema.optional(),
19685
19703
  facts: OmoMemoryFactsLayerSchema.optional(),
19686
19704
  dream: OmoMemoryDreamLayerSchema.optional(),
19687
19705
  people: OmoMemoryPeopleLayerSchema.optional(),
19706
+ agreements: OmoMemoryAgreementsLayerSchema.optional(),
19688
19707
  soul: OmoMemorySoulLayerSchema.optional(),
19689
19708
  write_notice: OmoMemoryWriteNoticeLayerSchema.optional(),
19690
19709
  sync: OmoMemorySyncLayerSchema.optional(),
@@ -19717,6 +19736,7 @@ function isRecord6(value) {
19717
19736
  }
19718
19737
  var OmoModelProfileInputSchema = object({
19719
19738
  display_name: string2().optional(),
19739
+ caution: string2().optional(),
19720
19740
  family: _enum(["daily", "geeky"]).optional(),
19721
19741
  tier: _enum(["normal", "heavy"]).optional(),
19722
19742
  models: array(union([string2(), OmoFallbackModelObjectSchema])).optional()
@@ -19988,6 +20008,7 @@ var OmoGatewaySectionSchema = record(string2(), unknown()).describe("Chat-surfac
19988
20008
  // packages/omo-config-core/src/schema/config.ts
19989
20009
  var OmoOpenCodeHarnessConfigSchema = record(string2(), unknown());
19990
20010
  var OmoDisabledSkillsSchema = array(string2());
20011
+ var OmoExcludedModelsSchema = array(string2());
19991
20012
  var OmoTypedHarnessConfigSchema = object({
19992
20013
  formatOnMutation: OmoFormatOnMutationLayerSchema.optional(),
19993
20014
  gateway: OmoGatewaySectionSchema.optional(),
@@ -20003,7 +20024,8 @@ var OmoTypedHarnessConfigSchema = object({
20003
20024
  telemetry: OmoTelemetrySettingsLayerSchema.optional(),
20004
20025
  side_panel: OmoSidePanelSettingsLayerSchema.optional(),
20005
20026
  computer: OmoComputerSettingsLayerSchema.optional(),
20006
- disabled_skills: OmoDisabledSkillsSchema.optional()
20027
+ disabled_skills: OmoDisabledSkillsSchema.optional(),
20028
+ exclude_models: OmoExcludedModelsSchema.optional()
20007
20029
  }).strict();
20008
20030
  var OmoConfigProfileSchema = object({
20009
20031
  formatOnMutation: OmoFormatOnMutationLayerSchema.optional(),
@@ -20020,6 +20042,7 @@ var OmoConfigProfileSchema = object({
20020
20042
  side_panel: OmoSidePanelSettingsLayerSchema.optional(),
20021
20043
  computer: OmoComputerSettingsLayerSchema.optional(),
20022
20044
  disabled_skills: OmoDisabledSkillsSchema.optional(),
20045
+ exclude_models: OmoExcludedModelsSchema.optional(),
20023
20046
  "[opencode]": OmoOpenCodeHarnessConfigSchema.optional(),
20024
20047
  "[native]": OmoTypedHarnessConfigSchema.optional(),
20025
20048
  "[senpi]": OmoTypedHarnessConfigSchema.optional(),
@@ -20042,6 +20065,7 @@ var OmoConfigSchema = object({
20042
20065
  side_panel: OmoSidePanelSettingsSchema.optional(),
20043
20066
  computer: OmoComputerSettingsSchema.optional(),
20044
20067
  disabled_skills: OmoDisabledSkillsSchema.optional(),
20068
+ exclude_models: OmoExcludedModelsSchema.optional(),
20045
20069
  "[opencode]": OmoOpenCodeHarnessConfigSchema.optional(),
20046
20070
  "[native]": OmoTypedHarnessConfigSchema.optional(),
20047
20071
  "[senpi]": OmoTypedHarnessConfigSchema.optional(),
@@ -20067,6 +20091,7 @@ var OmoConfigLayerSchema = object({
20067
20091
  side_panel: OmoSidePanelSettingsLayerSchema.optional(),
20068
20092
  computer: OmoComputerSettingsLayerSchema.optional(),
20069
20093
  disabled_skills: OmoDisabledSkillsSchema.optional(),
20094
+ exclude_models: OmoExcludedModelsSchema.optional(),
20070
20095
  "[opencode]": OmoOpenCodeHarnessConfigSchema.optional(),
20071
20096
  "[native]": OmoTypedHarnessConfigSchema.optional(),
20072
20097
  "[senpi]": OmoTypedHarnessConfigSchema.optional(),
@@ -20190,6 +20215,50 @@ var DEFAULT_READ_FILE_SYSTEM = {
20190
20215
  realpathSync: realpathSync2
20191
20216
  };
20192
20217
 
20218
+ // packages/omo-config-core/src/loader/disabled-skills.ts
20219
+ function toRecord(value) {
20220
+ if (value === null || typeof value !== "object" || Array.isArray(value))
20221
+ return;
20222
+ return value;
20223
+ }
20224
+ function namesAt(record, key) {
20225
+ const value = record?.[key];
20226
+ if (!Array.isArray(value))
20227
+ return [];
20228
+ return value.filter((entry) => typeof entry === "string" && entry.trim() !== "");
20229
+ }
20230
+ function harnessBlockKeys(harness) {
20231
+ const canonical = canonicalHarnessName(harness);
20232
+ const legacy = Object.entries(OMO_CONFIG_LEGACY_HARNESS_ALIASES).filter(([, target]) => target === canonical).map(([name]) => harnessBlockKey(name));
20233
+ return [harnessBlockKey(canonical), ...legacy];
20234
+ }
20235
+ function scopesOf(config, harness) {
20236
+ if (harness === undefined)
20237
+ return [config];
20238
+ return [config, ...harnessBlockKeys(harness).map((key) => toRecord(config[key]))];
20239
+ }
20240
+ function collectExcludedModels(options) {
20241
+ return collectUnionedList("exclude_models", options);
20242
+ }
20243
+ function collectUnionedList(key, options) {
20244
+ const names = new Set;
20245
+ for (const layer of options.layers) {
20246
+ const config = toRecord(layer.config);
20247
+ if (config === undefined)
20248
+ continue;
20249
+ const profile = options.profile === undefined ? undefined : toRecord(toRecord(config["profiles"])?.[options.profile]);
20250
+ const scopes = [
20251
+ ...scopesOf(config, options.harness),
20252
+ ...profile === undefined ? [] : scopesOf(profile, options.harness)
20253
+ ];
20254
+ for (const scope of scopes) {
20255
+ for (const name of namesAt(scope, key))
20256
+ names.add(name.trim());
20257
+ }
20258
+ }
20259
+ return [...names];
20260
+ }
20261
+
20193
20262
  // node_modules/.bun/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
20194
20263
  function createScanner(text, ignoreTrivia = false) {
20195
20264
  const len = text.length;
@@ -21262,7 +21331,7 @@ function stripUnrecognizedKeys(record, issues) {
21262
21331
  }
21263
21332
  return { issuePaths, stripped };
21264
21333
  }
21265
- function toRecord(value) {
21334
+ function toRecord2(value) {
21266
21335
  if (value === null || typeof value !== "object" || Array.isArray(value))
21267
21336
  return null;
21268
21337
  const record = {};
@@ -21277,7 +21346,7 @@ var validateLayerRecord = (record) => {
21277
21346
  };
21278
21347
  function validateConfigLayer(path, data) {
21279
21348
  const sanitized = sanitizeUnsafeKeys(data);
21280
- const record = toRecord(sanitized.value);
21349
+ const record = toRecord2(sanitized.value);
21281
21350
  const unsafeIssuePaths = sanitized.issues.flatMap((issue) => issue.keys.map((key) => [...issue.path, key].join(".")));
21282
21351
  const unsafeDiagnostics = unsafeIssuePaths.length === 0 ? [] : [{ kind: "unknown-keys", message: `Ignored unknown keys in ${path}: ${unsafeIssuePaths.join(", ")}`, path, issuePaths: unsafeIssuePaths }];
21283
21352
  const validation = OmoConfigLayerSchema.safeParse(sanitized.value);
@@ -21417,7 +21486,7 @@ function resolveOmoProfileName(options = {}) {
21417
21486
  const env = options.env ?? process.env;
21418
21487
  return profileName(options.profile) ?? profileName(env["OMO_PROFILE"]) ?? profileName(env["OCX_PROFILE"]) ?? profileNameFromOpenCodeConfigDir(env["OPENCODE_CONFIG_DIR"]);
21419
21488
  }
21420
- function toRecord2(value) {
21489
+ function toRecord3(value) {
21421
21490
  if (value === null || typeof value !== "object" || Array.isArray(value))
21422
21491
  return;
21423
21492
  return Object.fromEntries(Object.entries(value));
@@ -21438,13 +21507,13 @@ function harnessLayer(config, harness) {
21438
21507
  const legacyKeys = Object.entries(OMO_CONFIG_LEGACY_HARNESS_ALIASES).filter(([, target]) => target === canonical).map(([legacy]) => harnessBlockKey(legacy));
21439
21508
  let layer = {};
21440
21509
  for (const key of [...legacyKeys, harnessBlockKey(canonical)]) {
21441
- layer = mergeOmoConfigRecords(layer, toRecord2(config[key]) ?? {});
21510
+ layer = mergeOmoConfigRecords(layer, toRecord3(config[key]) ?? {});
21442
21511
  }
21443
21512
  return layer;
21444
21513
  }
21445
21514
  function resolveOmoConfigView(options) {
21446
- const profiles = toRecord2(options.config["profiles"]);
21447
- const profile = options.profile === undefined ? undefined : toRecord2(profiles?.[options.profile]);
21515
+ const profiles = toRecord3(options.config["profiles"]);
21516
+ const profile = options.profile === undefined ? undefined : toRecord3(profiles?.[options.profile]);
21448
21517
  const diagnostics = profile === undefined && options.profile !== undefined ? [{
21449
21518
  kind: "profile",
21450
21519
  message: `Activated omo profile "${options.profile}" does not exist; using the base configuration`,
@@ -21585,9 +21654,15 @@ function loadOmoConfig(options = {}) {
21585
21654
  });
21586
21655
  const finalInput = mergeOmoConfigRecords(DEFAULT_RAW_CONFIG, resolved.config);
21587
21656
  const finalConfig = OmoConfigSchema.safeParse(finalInput);
21657
+ const excludeModels = collectExcludedModels({
21658
+ layers,
21659
+ ...options.harness === undefined ? {} : { harness: options.harness },
21660
+ ...resolved.profile === undefined ? {} : { profile: resolved.profile }
21661
+ });
21662
+ const withExcludedModels = (config) => excludeModels.length === 0 ? config : { ...config, exclude_models: [...excludeModels] };
21588
21663
  if (finalConfig.success) {
21589
21664
  return {
21590
- config: stripResolutionControlKeys(finalConfig.data),
21665
+ config: withExcludedModels(stripResolutionControlKeys(finalConfig.data)),
21591
21666
  diagnostics: [...diagnostics, ...resolved.diagnostics],
21592
21667
  layers,
21593
21668
  ...resolved.profile === undefined ? {} : { profile: resolved.profile },
@@ -21600,7 +21675,7 @@ function loadOmoConfig(options = {}) {
21600
21675
  });
21601
21676
  if (pruned.ok) {
21602
21677
  return {
21603
- config: stripResolutionControlKeys(OmoConfigSchema.parse(pruned.config)),
21678
+ config: withExcludedModels(stripResolutionControlKeys(OmoConfigSchema.parse(pruned.config))),
21604
21679
  diagnostics: [...diagnostics, ...resolved.diagnostics, ...invalidValueDiagnostics(MERGED_OMO_CONFIG_PATH, pruned.dropped)],
21605
21680
  layers,
21606
21681
  ...resolved.profile === undefined ? {} : { profile: resolved.profile },
@@ -5,5 +5,5 @@
5
5
  "index.js": "d67d3a847fe0bbc43e824feded48b280890d31879538309e671cd5f671133995",
6
6
  "page-bundle.js": "bfd51e8383cf47200d57387bbbed258bce5a3a3d00c38fc108034d2888e23f0c"
7
7
  },
8
- "stagedAtUtc": "2026-10-20T14:11:05.519Z"
8
+ "stagedAtUtc": "2026-10-22T17:51:22.861Z"
9
9
  }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: frontend
3
- description: "Builds, styles, and polishes web UI and UX. Use for any frontend, page, component, styling, layout, animation, or visual-quality task, or when asked to make an interface look or feel a certain way."
3
+ description: "Builds, styles, and polishes anything drawn with HTML, CSS, or SVG: web UI, pages, components, HTML reports rendered to PDF or print, figures, PNG/SVG diagrams, slides, and images for Notion or Slack. Use for any styling, layout, animation, or visual-quality task, or when asked to make something look or feel a certain way."
4
4
  ---
5
5
 
6
6
  # Frontend
@@ -25,12 +25,12 @@ This file is a router, not a rulebook. The rules live in four rulesets under `re
25
25
 
26
26
  **For implementation work, design + perfection load together.** Beauty with a 2 MB bundle fails; Lighthouse 100 that looks like AI slop fails. Both win or neither does.
27
27
 
28
- ## Draft guard — read before the first line of UI
28
+ ## Draft guard — read before the first line of HTML or CSS
29
29
 
30
30
  Some tells regenerate by default, so they are kept out of the first draft rather than left for QA to find.
31
31
 
32
- - **No coloured edge on a rounded surface, decorative or state.** A card, callout, stat tile, testimonial, alert, or list row never carries a coloured stripe on one side: `border-top: 4px solid <accent>` or `border-l-4 border-indigo-500` on a rounded box, a coloured `::before`/`::after` bar inside one, an inset `box-shadow` stripe, or a hard-stop gradient band or gradient `border-image` along one edge. The radius bends the stripe around the corner, and the result reads as generated whether the stripe decorates the card or marks it selected, focused, or active. Give the card its identity another way: a tonal surface (one ink at a low alpha, or the next step of the surface ramp), an eyebrow or heading set in the accent ink, a leading glyph, or more space between groups. Encode state with one ink at many alphas (hover/selected/active washes), a check glyph for selection, and tonal layering for focus. The coloured edges that stay allowed: keyboard `focus-visible` rings, validation states on form inputs, neutral 1px hairlines and dividers, and table rules. This covers pre-existing instances on any surface you touch.
33
- - **Lint before visual QA.** Run `bun $SKILL_DIR/scripts/perfection/check-accent-stripes.ts <changed files or dirs>` on every stylesheet and component you wrote. It reads CSS/SCSS/Less, HTML, JSX/TSX, Vue, Svelte, Astro, Tailwind classes (including `@apply` and `cva`/`cn` strings), inline styles, and styled-components. Exit 1 means a stripe is in the draft: remove the stripe, do not argue with the finding.
32
+ - **No coloured bar on one side of a box, rounded or square, and no accent outline on a rounded card, decorative or state.** A card, callout, warning, cover block, stat tile, testimonial, alert, or list row never carries a coloured stripe on one side: `border-left: 4px solid <accent>` or `border-l-4 border-indigo-500`, a coloured `::before`/`::after` bar inside it, an inset `box-shadow` stripe, or a hard-stop gradient band or gradient `border-image` along one edge. A rounded card never wears a 2px-or-wider accent outline either (`border: 2px solid var(--primary)`, `ring-2 ring-indigo-500`, an inset ring). Both read as generated whether they decorate the box or mark it selected, focused, or active, and an accent underline under a heading, tab, or link counts too: use a neutral 1px rule, or set the text itself in the accent ink. Give the card its identity another way: a tonal surface (one ink at a low alpha, or the next step of the surface ramp), an eyebrow or heading set in the accent ink, a leading glyph, or more space between groups. Encode state with one ink at many alphas (hover/selected/active washes), a check glyph for selection, and tonal layering for focus. The coloured edges that stay allowed: keyboard `focus-visible` rings, validation states on form inputs, neutral 1px hairlines and dividers between rows or sections, table rules, and a blockquote's neutral rule. This covers pre-existing instances on any surface you touch.
33
+ - **Lint before visual QA.** Run `bun $SKILL_DIR/scripts/perfection/check-accent-stripes.ts <changed files or dirs>` on every stylesheet, component, and HTML file you wrote, including a report or figure you will print, screenshot, or render to an image. It reads CSS/SCSS/Less, HTML, JSX/TSX, Vue, Svelte, Astro, Tailwind classes (including `@apply` and `cva`/`cn` strings), inline styles, and styled-components. Exit 1 means a stripe is in the draft: remove the stripe, do not argue with the finding.
34
34
 
35
35
  ## Design System and Component Workflow
36
36
 
@@ -145,7 +145,7 @@ Domains: `product` `style` `typography` `color` `landing` `chart` `ux` `react` `
145
145
  - **Concrete reference = contract.** When a screenshot, mockup, or annotated reference exists, match its pixels, copy, component structure, and responsive intent unless the user accepts a deviation.
146
146
  - **Never weaken UX OR flatten the surface to buy points.** No dropping animations, hiding content, simplifying interactions, or replacing rendered/lit material with flat fills and flat geometric primitives for a Lighthouse score or a deadline. Hit 100 AND keep the surface dimensional — both, or neither.
147
147
  - **No emojis as icons.** SVG icon sets only (Lucide, Heroicons, Radix, Phosphor).
148
- - **No coloured edges on rounded surfaces.** A decorative accent stripe on a card and a `border-l-2 border-primary` stripe on a selected row are the same tell, and so is a primary-tinted outline on a focused card. The Draft guard above carries the replacements and the allowed exceptions; `check-accent-stripes.ts` must exit 0 before `/visual-qa`. Applies to code you write AND to pre-existing instances on any surface you touch — sweep them out.
148
+ - **No coloured edge bars, no accent outlines on cards.** A decorative accent stripe on a callout, rounded or square, and a `border-l-2 border-primary` stripe on a selected row are the same tell, and so is a primary-tinted outline on a focused card. The Draft guard above carries the replacements and the allowed exceptions; `check-accent-stripes.ts` must exit 0 before `/visual-qa`. Applies to code you write AND to pre-existing instances on any surface you touch — sweep them out.
149
149
  - **GPU-composited animation only** — `transform`, `opacity`, `filter`; never animate layout properties.
150
150
  - **Slop animation is forbidden — motion serves meaning.** Every animation or hover must map to a real interaction, state change, or affordance. A hover that changes nothing, motion on a non-interactive element, or a decorative micro-animation with no informational purpose is slop — do not add it.
151
151
  - **Done is the `/visual-qa` gate, not your own glance.** A frontend design task is verified through `/visual-qa` - its HIG-based checklist, its capture matrix (every page and every sibling surface, phone and desktop, light and dark, scrolled-end, motion driven and inspected) and its dual-oracle completion gate on fresh evidence. This skill does not keep its own list of visual checks; the verifier owns it.
@@ -160,4 +160,4 @@ Domains: `product` `style` `typography` `color` `landing` `chart` `ux` `react` `
160
160
 
161
161
  ## Activation
162
162
 
163
- Use for any frontend, web UI, UX, visual, design, styling, layout, animation, performance, accessibility, or SEO work — building, redesigning, auditing, or generating mockups. Not for backend, CLI, or pure-logic tasks with no visual surface.
163
+ Use for any frontend, web UI, UX, visual, design, styling, layout, animation, performance, accessibility, or SEO work, and for every HTML/CSS/SVG artifact that ends up as a PDF, print page, report, figure, diagram, slide, or image — building, redesigning, auditing, or generating mockups. Not for backend, CLI, or pure-logic tasks with no visual surface.
@@ -202,7 +202,7 @@ Triggers: an animated, shader, particle, or grid background; a hero that must fe
202
202
  ## Anti-patterns — do not do these
203
203
 
204
204
  - **Don't load nothing and just freestyle.** That produces the exact "generic AI SaaS slop" — purple-blue gradient backgrounds, rounded-2xl-on-everything, three feature cards in a grid, generic Inter font, lorem ipsum. The skills exist precisely to prevent this.
205
- - **Don't put a coloured edge on a rounded surface, for decoration or for state.** A feature card with `border-top: 4px solid` accent, a callout with `border-l-4 border-indigo-500`, a stat tile with a coloured `::before` bar, `border-l-2 border-primary` on the selected row, a primary outline on the focused card: the radius bends the stripe and the result is the generic AI tell. Identity comes from a tonal surface, an eyebrow or heading in the accent ink, a glyph, or spacing; state is ink-alpha washes, a glyph for selection, tonal layering for focus. `focus-visible` rings, input validation states, neutral hairlines, and table rules are the only coloured or ruled edges allowed. Includes pre-existing instances on surfaces you touch.
205
+ - **Don't put a coloured bar on one side of a box, rounded or square, or an accent outline on a rounded card, for decoration or for state.** A feature card with `border-top: 4px solid` accent, a callout or cover block with `border-l-4 border-indigo-500`, a stat tile with a coloured `::before` bar, `border-l-2 border-primary` on the selected row, a 2px primary outline on the focused card: each is the generic AI tell. Identity comes from a tonal surface, an eyebrow or heading in the accent ink, a glyph, or spacing; state is ink-alpha washes, a glyph for selection, tonal layering for focus. `focus-visible` rings, input validation states, neutral hairlines, table rules, and a blockquote's neutral rule are the only coloured or ruled edges allowed. Includes pre-existing instances on surfaces you touch.
206
206
  - **Don't ignore the user's named brand.** If they say "Linear-style" and you build something that doesn't match Linear's actual aesthetic (purple, ultra-tight spacing, mono accents, etc.), you have failed the routing.
207
207
  - **Don't apply a Layer B brand verbatim if the project is not that brand.** The DESIGN.md captures *inspiration* — extract the tokens (palette, type scale, component patterns) and apply them to the project's own content. Do not copy logos or trademarked imagery.
208
208
  - **Don't use imagegen skills to write code.** They are explicitly image-only. The agent has been observed trying to "describe" the image as React code — that is the wrong skill, switch to `image-to-code-skill.md` instead.
@@ -222,7 +222,7 @@ Once references are loaded, before writing any UI code:
222
222
  8. **All tokens trace back to `DESIGN.md`.** No orphan hex codes, no magic px values. If you need a new token, update `DESIGN.md` first.
223
223
  9. **New reusable patterns (used 2+ times) get documented back into `DESIGN.md` Section 5.**
224
224
  10. **No generic-default drift (expressive briefs).** The shipped CSS must use the `DESIGN.md` material, not the model's priors. Load the declared fonts (do not silently fall back to Inter or system fonts), build elevated surfaces from the declared multi-layer recipe (not a lone `backdrop-filter: blur`), and color from the ramp stops (not one tint at varied opacity). Grep your styles before QA: finding `Inter`, a single blur on "glass", or one brand hex reused everywhere means you regressed to priors — fix it before declaring done.
225
- 11. **No coloured edge stripes.** Run `bun ../../scripts/perfection/check-accent-stripes.ts <changed files or dirs>` (from this directory; `$SKILL_DIR/scripts/perfection/check-accent-stripes.ts` from anywhere). It must exit 0 before Phase Final; every finding is a decorative or state stripe on a rounded surface to replace, per the anti-pattern above.
225
+ 11. **No coloured edge stripes.** Run `bun ../../scripts/perfection/check-accent-stripes.ts <changed files or dirs>` (from this directory; `$SKILL_DIR/scripts/perfection/check-accent-stripes.ts` from anywhere). It must exit 0 before Phase Final; every finding is a decorative or state stripe, or an accent outline, to replace, per the anti-pattern above.
226
226
 
227
227
  ## Quick lookup table — most common requests
228
228
 
@@ -67,7 +67,7 @@ During the audit loop, after Lighthouse scores pass, run a Design System Complia
67
67
  - **Spacing**: every margin/padding/gap value must be a multiple of the base unit (4px) and ideally use a declared token.
68
68
  - **Components**: any component used 2+ times must be documented in `DESIGN.md` Section 5. If it isn't, add it.
69
69
  - **Depth**: if `DESIGN.md` says "borders-only", there must be zero `box-shadow` declarations. If "tonal-shift", zero borders for surface separation.
70
- - **Accent stripes**: run `bun ../../scripts/perfection/check-accent-stripes.ts <src dirs>`. It flags a coloured one-side border (including a gradient `border-image`), a coloured `::before`/`::after` bar, an inset `box-shadow` stripe, or a hard-stop gradient band along one edge of a rounded surface, in CSS, Tailwind, inline styles, and styled-components, whether the stripe is decorative or marks selected/focused/active. Each finding is a violation — identity belongs to tonal surfaces, headings, glyphs, and spacing; state belongs to ink-alpha washes and glyphs. Keyboard `focus-visible` rings, input validation states, neutral hairlines, and table rules are exempt, and the script already skips them.
70
+ - **Accent stripes**: run `bun ../../scripts/perfection/check-accent-stripes.ts <src dirs>`. It flags a coloured one-side border (including a gradient `border-image`), a coloured `::before`/`::after` bar, an inset `box-shadow` stripe, or a hard-stop gradient band along one edge of any box, rounded or square, and a 2px-or-wider accent outline (border or inset ring) on a rounded card, in CSS, Tailwind, inline styles, and styled-components, whether the stripe is decorative or marks selected/focused/active. Each finding is a violation — identity belongs to tonal surfaces, headings, glyphs, and spacing; state belongs to ink-alpha washes and glyphs. Keyboard `focus-visible` rings, input validation states, neutral hairlines, table rules, and a blockquote's neutral rule are exempt, and the script already skips them.
71
71
 
72
72
  A page that scores Lighthouse 100 but uses 14 undeclared hex codes and 8 magic spacing values is **NOT DONE**. The design system is the architecture — Lighthouse measures the performance of that architecture.
73
73
 
@@ -92,7 +92,7 @@ Quick audit via the cross-platform Python CLI (macOS, Linux, Windows):
92
92
  uv run $SKILL_DIR/scripts/perfection/lighthouse-audit.py https://localhost:3000
93
93
  uv run $SKILL_DIR/scripts/perfection/lighthouse-audit.py https://localhost:3000 --threshold 95
94
94
  uv run $SKILL_DIR/scripts/perfection/lighthouse-audit.py https://localhost:3000 --desktop-only
95
- bun $SKILL_DIR/scripts/perfection/check-accent-stripes.ts src/ # exit 1 = coloured edge stripe on a rounded surface
95
+ bun $SKILL_DIR/scripts/perfection/check-accent-stripes.ts src/ # exit 1 = coloured one-side bar, or accent outline on a rounded card
96
96
  ```
97
97
 
98
98
  Or use the TypeScript approach directly in your test suite:
@@ -72,10 +72,6 @@ function collectEdges(declarations: readonly Declaration[]): Map<Side, Edge> {
72
72
  }
73
73
 
74
74
  export type Corner = "tl" | "tr" | "br" | "bl"
75
- export const ALL_CORNERS: ReadonlySet<Corner> = new Set(["tl", "tr", "br", "bl"])
76
- export const SIDE_CORNERS: Readonly<Record<Side, readonly [Corner, Corner]>> = {
77
- top: ["tl", "tr"], right: ["tr", "br"], bottom: ["br", "bl"], left: ["tl", "bl"],
78
- }
79
75
  const CORNER_LONGHAND: Readonly<Record<string, Corner>> = {
80
76
  "top-left": "tl", "top-right": "tr", "bottom-right": "br", "bottom-left": "bl",
81
77
  "start-start": "tl", "start-end": "tr", "end-end": "br", "end-start": "bl",
@@ -104,20 +100,33 @@ export function roundedCorners(declarations: readonly Declaration[]): Set<Corner
104
100
  return corners
105
101
  }
106
102
 
107
- export function hasEdgeStripe(declarations: readonly Declaration[], corners: ReadonlySet<Corner> = ALL_CORNERS): boolean {
108
- const bySide = collectEdges(declarations)
109
- const edges = [...bySide.values()]
110
- const isStripe = (e: Edge) => !e.hidden && (e.width ?? 0) >= MIN_STRIPE_PX && e.color === "chromatic"
111
- const bent = SIDES.some((s) => {
112
- const edge = bySide.get(s)
113
- return edge !== undefined && isStripe(edge) && SIDE_CORNERS[s].some((c) => corners.has(c))
114
- })
115
- if (!bent) return false
103
+ const isStripe = (e: Edge) => !e.hidden && (e.width ?? 0) >= MIN_STRIPE_PX && e.color === "chromatic"
104
+
105
+ export function hasEdgeStripe(declarations: readonly Declaration[]): boolean {
106
+ const edges = [...collectEdges(declarations).values()]
107
+ if (!edges.some(isStripe)) return false
116
108
  const first = edges[0]
117
109
  const uniform = edges.every((e) => e.width === first?.width && e.color === first?.color && e.hidden === first?.hidden)
118
110
  return !uniform
119
111
  }
120
112
 
113
+ function isInsetRing(value: string): boolean {
114
+ return splitTopLevel(value, ",").some((layer) => {
115
+ const tokens = splitTopLevel(layer, " ")
116
+ if (!tokens.some((t) => t.toLowerCase() === "inset")) return false
117
+ const [x = 0, y = 0, blur = 0, spread = 0] = tokens.map(parseLength).filter((n): n is number => n !== undefined)
118
+ return x === 0 && y === 0 && blur === 0 && spread >= MIN_STRIPE_PX && tokens.some((t) => looksLikeColor(t) && classifyColor(t) === "chromatic")
119
+ })
120
+ }
121
+
122
+ /** A uniform accent border (not a gradient border-image) or inset ring of MIN_STRIPE_PX or more. */
123
+ export function hasAccentOutline(declarations: readonly Declaration[]): boolean {
124
+ const image = declarations.some(({ prop }) => prop === "border-image" || prop === "border-image-source")
125
+ const edges = [...collectEdges(declarations).values()]
126
+ const frame = !image && edges.every((e) => isStripe(e) && e.width === edges[0]?.width)
127
+ return frame || declarations.some(({ prop, value }) => prop === "box-shadow" && isInsetRing(value))
128
+ }
129
+
121
130
  export function hasRadius(declarations: readonly Declaration[]): boolean {
122
131
  return roundedCorners(declarations).size > 0
123
132
  }
@@ -1,4 +1,4 @@
1
- export type RuleId = "edge-stripe" | "pseudo-bar" | "inset-shadow-stripe" | "gradient-stripe"
1
+ export type RuleId = "edge-stripe" | "pseudo-bar" | "inset-shadow-stripe" | "gradient-stripe" | "accent-outline"
2
2
 
3
3
  export type Finding = {
4
4
  readonly file: string
@@ -9,14 +9,15 @@ export type Finding = {
9
9
  }
10
10
 
11
11
  export const RULE_MESSAGES: Readonly<Record<RuleId, string>> = {
12
- "edge-stripe": "coloured one-side border on a rounded surface; the corners bend the stripe",
13
- "pseudo-bar": "coloured ::before/::after bar on a rounded surface",
14
- "inset-shadow-stripe": "inset box-shadow stripe on a rounded surface",
15
- "gradient-stripe": "hard-stop gradient band along one edge of a rounded surface",
12
+ "edge-stripe": "coloured one-side border on a box, rounded or square",
13
+ "pseudo-bar": "coloured ::before/::after bar along one side of a box",
14
+ "inset-shadow-stripe": "inset box-shadow stripe along one side of a box",
15
+ "gradient-stripe": "hard-stop gradient band along one edge of a box",
16
+ "accent-outline": "accent-tinted full outline on a rounded card",
16
17
  }
17
18
 
18
19
  export const FIX_HINT =
19
- "drop the stripe: separate with a tonal surface (one ink at a low alpha), an eyebrow or heading, a leading glyph, or spacing"
20
+ "drop the stripe or outline: separate with a tonal surface (one ink at a low alpha), an eyebrow or heading, a leading glyph, or spacing"
20
21
 
21
22
  export function lineLocator(text: string): (offset: number) => { line: number; column: number } {
22
23
  const starts = [0]
@@ -1,9 +1,9 @@
1
1
  import { type CssRule, type Declaration, parseCss } from "./css-parse"
2
- import { ALL_CORNERS, type Corner, hasEdgeStripe, hasInsetShadowStripe, hasRadius, isPseudoBar, roundedCorners } from "./declarations"
2
+ import { hasAccentOutline, hasEdgeStripe, hasInsetShadowStripe, hasRadius, isPseudoBar, roundedCorners } from "./declarations"
3
3
  import { type Finding, lineLocator, type RuleId } from "./findings"
4
4
  import { hasGradientEdgeBand } from "./gradient"
5
5
  import { classGroups, cssTemplates, inlineStyles, styleBlocks } from "./markup"
6
- import { type Compound, compoundCovers, isExemptSelector, parseCompound, selectorParts } from "./selector"
6
+ import { type Compound, compoundCovers, isExemptSelector, isExemptTag, parseCompound, selectorParts } from "./selector"
7
7
  import { checkClassSet } from "./tailwind"
8
8
 
9
9
  export type SourceFile = { readonly path: string; readonly text: string }
@@ -25,18 +25,14 @@ function radiusIndex(rules: readonly CssRule[]): Compound[] {
25
25
  return rules.filter(ruleHasRadius).flatMap((r) => selectorParts(r.selector).map(parseCompound))
26
26
  }
27
27
 
28
- const NO_CORNERS: ReadonlySet<Corner> = new Set()
29
-
30
- function hostCorners(part: string, rule: CssRule, index: readonly Compound[]): ReadonlySet<Corner> {
31
- if (!parseCompound(part).pseudoElement) {
32
- if (hasRadius(rule.declarations)) return roundedCorners(rule.declarations)
33
- if (ruleHasRadius(rule)) return ALL_CORNERS
34
- }
35
- const host = parseCompound(part.replace(/::?(before|after)\b/gi, ""))
36
- return index.some((general) => compoundCovers(general, host)) ? ALL_CORNERS : NO_CORNERS
28
+ function isRoundedHost(part: string, rule: CssRule, index: readonly Compound[]): boolean {
29
+ if (ruleHasRadius(rule)) return true
30
+ const host = parseCompound(part)
31
+ return index.some((general) => compoundCovers(general, host))
37
32
  }
38
33
 
39
34
  const EDGE_PROP = /^border(?:-(?:top|right|bottom|left|block|inline|width|color|image)(?:-[\w-]+)?)?$/
35
+ const OUTLINE_PROP = /^(?:border(?:-width|-color)?|box-shadow)$/
40
36
  const BACKGROUND_PROP = /^background(?:-color|-image)?$/
41
37
 
42
38
  function declarationOffset(declarations: readonly Declaration[], prop: RegExp, fallback: number): number {
@@ -45,9 +41,10 @@ function declarationOffset(declarations: readonly Declaration[], prop: RegExp, f
45
41
 
46
42
  type Hit = { readonly id: RuleId; readonly offset: number }
47
43
 
48
- function declarationHits(declarations: readonly Declaration[], corners: ReadonlySet<Corner>, fallback: number): Hit[] {
44
+ function declarationHits(declarations: readonly Declaration[], rounded: boolean, fallback: number): Hit[] {
49
45
  const hits: Hit[] = []
50
- if (hasEdgeStripe(declarations, corners)) hits.push({ id: "edge-stripe", offset: declarationOffset(declarations, EDGE_PROP, fallback) })
46
+ if (hasEdgeStripe(declarations)) hits.push({ id: "edge-stripe", offset: declarationOffset(declarations, EDGE_PROP, fallback) })
47
+ else if (rounded && hasAccentOutline(declarations)) hits.push({ id: "accent-outline", offset: declarationOffset(declarations, OUTLINE_PROP, fallback) })
51
48
  if (hasInsetShadowStripe(declarations)) hits.push({ id: "inset-shadow-stripe", offset: declarationOffset(declarations, /^box-shadow$/, fallback) })
52
49
  if (hasGradientEdgeBand(declarations)) hits.push({ id: "gradient-stripe", offset: declarationOffset(declarations, BACKGROUND_PROP, fallback) })
53
50
  return hits
@@ -57,17 +54,14 @@ function checkRule(rule: CssRule, index: readonly Compound[]): Hit[] {
57
54
  const found = new Map<RuleId, number>()
58
55
  const add = (hit: Hit) => found.set(hit.id, found.get(hit.id) ?? hit.offset)
59
56
  for (const part of selectorParts(rule.selector)) {
60
- const corners = isExemptSelector(part) ? NO_CORNERS : hostCorners(part, rule, index)
61
- if (corners.size === 0) continue
57
+ if (isExemptSelector(part)) continue
62
58
  if (parseCompound(part).pseudoElement) {
63
59
  if (isPseudoBar(rule.declarations)) add({ id: "pseudo-bar", offset: declarationOffset(rule.declarations, BACKGROUND_PROP, rule.offset) })
64
60
  continue
65
61
  }
66
- declarationHits(rule.declarations, corners, rule.offset).forEach(add)
67
- const ownRounding = rule.applyClasses.some((c) => c.startsWith("rounded"))
68
- for (const id of checkClassSet(ownRounding ? rule.applyClasses : ["rounded", ...rule.applyClasses])) {
69
- add({ id, offset: rule.applyOffset ?? rule.offset })
70
- }
62
+ const rounded = isRoundedHost(part, rule, index)
63
+ declarationHits(rule.declarations, rounded, rule.offset).forEach(add)
64
+ for (const id of checkClassSet(rule.applyClasses, rounded)) add({ id, offset: rule.applyOffset ?? rule.offset })
71
65
  }
72
66
  return [...found].map(([id, offset]) => ({ id, offset }))
73
67
  }
@@ -97,13 +91,13 @@ export function lintFiles(files: readonly SourceFile[]): Finding[] {
97
91
  }
98
92
  if (STYLESHEET.test(file.path)) continue
99
93
  for (const group of classGroups(file.text, SCRIPT.test(file.path))) {
94
+ if (isExemptTag(group.tag)) continue
100
95
  for (const id of checkClassSet(group.tokens)) report(group.offset, id)
101
96
  }
102
97
  for (const style of inlineStyles(file.text)) {
103
- const own = roundedCorners(style.declarations)
104
- const corners = own.size > 0 ? own : style.tagRounded ? ALL_CORNERS : NO_CORNERS
105
- if (corners.size === 0) continue
106
- for (const hit of declarationHits(style.declarations, corners, style.offset)) report(style.offset, hit.id)
98
+ if (isExemptTag(style.tag)) continue
99
+ const rounded = style.tagRounded || roundedCorners(style.declarations).size > 0
100
+ for (const hit of declarationHits(style.declarations, rounded, style.offset)) report(style.offset, hit.id)
107
101
  }
108
102
  }
109
103
  return findings.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column)
@@ -1,8 +1,8 @@
1
1
  import type { Declaration } from "./css-parse"
2
2
  import { looksLikeClassList } from "./tailwind"
3
3
 
4
- export type ClassGroup = { readonly tokens: readonly string[]; readonly offset: number }
5
- export type InlineStyle = { readonly declarations: readonly Declaration[]; readonly offset: number; readonly tagRounded: boolean }
4
+ export type ClassGroup = { readonly tokens: readonly string[]; readonly offset: number; readonly tag: string }
5
+ export type InlineStyle = { readonly declarations: readonly Declaration[]; readonly offset: number; readonly tagRounded: boolean; readonly tag: string }
6
6
  export type StyleBlock = { readonly css: string; readonly offset: number }
7
7
 
8
8
  const UNITLESS = new Set(["opacity", "z-index", "flex", "flex-grow", "flex-shrink", "font-weight", "line-height", "order"])
@@ -51,6 +51,13 @@ function stripInterpolation(literal: string): string {
51
51
  return literal.replace(/\$\{[^}]*\}/g, " ")
52
52
  }
53
53
 
54
+ /** Name of the element whose open tag contains `offset` ("" outside a tag, e.g. a cva string). */
55
+ function tagNameAt(text: string, offset: number): string {
56
+ const open = text.lastIndexOf("<", offset)
57
+ if (open < 0 || text.slice(open, offset).includes(">")) return ""
58
+ return /^<([A-Za-z][\w.-]*)/.exec(text.slice(open))?.[1] ?? ""
59
+ }
60
+
54
61
  function tokensOf(text: string): string[] {
55
62
  return text.split(/\s+/).filter(Boolean)
56
63
  }
@@ -83,11 +90,11 @@ export function classGroups(text: string, scanLiterals: boolean): ClassGroup[] {
83
90
  if (ch === '"' || ch === "'") {
84
91
  const close = text.indexOf(ch, start + 1)
85
92
  if (close < 0) continue
86
- groups.push({ tokens: tokensOf(text.slice(start + 1, close)), offset: start })
93
+ groups.push({ tokens: tokensOf(text.slice(start + 1, close)), offset: start, tag: tagNameAt(text, start) })
87
94
  covered.push([start, close])
88
95
  } else if (ch === "{") {
89
96
  const { end, literals } = readBalanced(text, start)
90
- groups.push({ tokens: literals.flatMap(tokensOf), offset: start })
97
+ groups.push({ tokens: literals.flatMap(tokensOf), offset: start, tag: tagNameAt(text, start) })
91
98
  covered.push([start, end])
92
99
  }
93
100
  }
@@ -96,7 +103,7 @@ export function classGroups(text: string, scanLiterals: boolean): ClassGroup[] {
96
103
  const offset = m.index ?? 0
97
104
  if (covered.some(([a, b]) => offset >= a && offset <= b)) continue
98
105
  const body = stripInterpolation(m[2] ?? "")
99
- if (looksLikeClassList(body)) groups.push({ tokens: tokensOf(body), offset })
106
+ if (looksLikeClassList(body)) groups.push({ tokens: tokensOf(body), offset, tag: "" })
100
107
  }
101
108
  return groups
102
109
  }
@@ -136,13 +143,13 @@ export function inlineStyles(text: string): InlineStyle[] {
136
143
  for (const m of text.matchAll(/\bstyle\s*=\s*\{\{([\s\S]*?)\}\}/g)) {
137
144
  const offset = m.index ?? 0
138
145
  const end = offset + m[0].length
139
- styles.push({ declarations: objectDeclarations(m[1] ?? ""), offset, tagRounded: tagIsRounded(text, offset, end) })
146
+ styles.push({ declarations: objectDeclarations(m[1] ?? ""), offset, tagRounded: tagIsRounded(text, offset, end), tag: tagNameAt(text, offset) })
140
147
  }
141
148
  for (const m of text.matchAll(/\bstyle\s*=\s*(["'])([\s\S]*?)\1/g)) {
142
149
  const offset = m.index ?? 0
143
150
  const body = (m[2] ?? "").trim()
144
151
  const declarations = body.startsWith("{") ? objectDeclarations(body) : cssDeclarations(body)
145
- styles.push({ declarations, offset, tagRounded: tagIsRounded(text, offset, offset + m[0].length) })
152
+ styles.push({ declarations, offset, tagRounded: tagIsRounded(text, offset, offset + m[0].length), tag: tagNameAt(text, offset) })
146
153
  }
147
154
  return styles
148
155
  }
@@ -27,9 +27,13 @@ export function parseCompound(part: string): Compound {
27
27
  }
28
28
  }
29
29
 
30
+ export function isExemptTag(tag: string): boolean {
31
+ return EXEMPT_TAGS.has(tag.toLowerCase())
32
+ }
33
+
30
34
  export function isExemptSelector(part: string): boolean {
31
35
  if (EXEMPT_STATE.test(part)) return true
32
- return EXEMPT_TAGS.has(parseCompound(part).tag)
36
+ return isExemptTag(parseCompound(part).tag)
33
37
  }
34
38
 
35
39
  export function selectorParts(selector: string): string[] {