oh-my-opencode 4.17.1 → 4.18.1

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 (213) hide show
  1. package/.agents/skills/codex-qa/SKILL.md +2 -0
  2. package/.agents/skills/codex-qa/scripts/lsp-e2e.sh +3654 -0
  3. package/.agents/skills/opencode-qa/scripts/lsp-e2e.sh +3071 -0
  4. package/.agents/skills/work-with-pr/SKILL.md +16 -37
  5. package/.agents/skills/work-with-pr-workspace/evals/evals.json +3 -3
  6. package/.opencode/skills/work-with-pr/SKILL.md +16 -37
  7. package/.opencode/skills/work-with-pr-workspace/evals/evals.json +3 -3
  8. package/dist/cli/get-local-version/types.d.ts +1 -1
  9. package/dist/cli/index.js +498 -165
  10. package/dist/cli-node/index.js +498 -165
  11. package/dist/index.js +425 -392
  12. package/dist/skills/frontend/SKILL.md +1 -1
  13. package/dist/skills/frontend/references/design/README.md +9 -0
  14. package/dist/skills/frontend/references/design/design-system-architecture.md +4 -2
  15. package/dist/skills/frontend/references/design/layout-skill.md +107 -0
  16. package/dist/skills/programming/SKILL.md +12 -2
  17. package/package.json +17 -16
  18. package/packages/lsp-core/package.json +4 -0
  19. package/packages/lsp-core/src/index.ts +1 -0
  20. package/packages/lsp-core/src/lsp/cleanup-errors.test.ts +18 -0
  21. package/packages/lsp-core/src/lsp/cleanup-errors.ts +12 -3
  22. package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +261 -0
  23. package/packages/lsp-core/src/lsp/client-wrapper.test.ts +63 -0
  24. package/packages/lsp-core/src/lsp/client-wrapper.ts +35 -5
  25. package/packages/lsp-core/src/lsp/client.ts +262 -80
  26. package/packages/lsp-core/src/lsp/config-loader.ts +5 -17
  27. package/packages/lsp-core/src/lsp/connection.ts +12 -6
  28. package/packages/lsp-core/src/lsp/directory-diagnostics.test.ts +104 -0
  29. package/packages/lsp-core/src/lsp/directory-diagnostics.ts +60 -27
  30. package/packages/lsp-core/src/lsp/errors.ts +11 -0
  31. package/packages/lsp-core/src/lsp/fixtures/diagnostics-freshness-contract-probe.ts +283 -0
  32. package/packages/lsp-core/src/lsp/fixtures/workspace-edit-contract-probe.ts +196 -0
  33. package/packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs +215 -0
  34. package/packages/lsp-core/src/lsp/formatters.ts +3 -0
  35. package/packages/lsp-core/src/lsp/json-rpc-connection-cancellation.test.ts +97 -0
  36. package/packages/lsp-core/src/lsp/json-rpc-connection.ts +73 -5
  37. package/packages/lsp-core/src/lsp/server-install-state.ts +3 -6
  38. package/packages/lsp-core/src/lsp/transport-protocol.ts +52 -0
  39. package/packages/lsp-core/src/lsp/transport.ts +96 -70
  40. package/packages/lsp-core/src/lsp/workspace-apply-edit-failure.ts +19 -0
  41. package/packages/lsp-core/src/lsp/workspace-apply-edit-lease.integration.test.ts +214 -0
  42. package/packages/lsp-core/src/lsp/workspace-apply-edit-sync.integration.test.ts +113 -0
  43. package/packages/lsp-core/src/lsp/workspace-apply-edit-test-support.ts +163 -0
  44. package/packages/lsp-core/src/lsp/workspace-apply-edit.integration.test.ts +163 -0
  45. package/packages/lsp-core/src/lsp/workspace-document-state.test.ts +67 -0
  46. package/packages/lsp-core/src/lsp/workspace-document-state.ts +368 -0
  47. package/packages/lsp-core/src/lsp/workspace-edit-adversarial.test.ts +113 -0
  48. package/packages/lsp-core/src/lsp/workspace-edit-commit.test.ts +140 -0
  49. package/packages/lsp-core/src/lsp/workspace-edit-commit.ts +220 -0
  50. package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.test.ts +56 -0
  51. package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.ts +30 -0
  52. package/packages/lsp-core/src/lsp/workspace-edit-fingerprint.ts +44 -0
  53. package/packages/lsp-core/src/lsp/workspace-edit-options.test.ts +147 -0
  54. package/packages/lsp-core/src/lsp/workspace-edit-parse-helpers.ts +59 -0
  55. package/packages/lsp-core/src/lsp/workspace-edit-parser.ts +130 -0
  56. package/packages/lsp-core/src/lsp/workspace-edit-path.ts +98 -0
  57. package/packages/lsp-core/src/lsp/workspace-edit-plan-types.ts +60 -0
  58. package/packages/lsp-core/src/lsp/workspace-edit-plan.ts +73 -0
  59. package/packages/lsp-core/src/lsp/workspace-edit-prevalidation.test.ts +174 -0
  60. package/packages/lsp-core/src/lsp/workspace-edit-resource-parser.ts +89 -0
  61. package/packages/lsp-core/src/lsp/workspace-edit-simulation.ts +183 -0
  62. package/packages/lsp-core/src/lsp/workspace-edit-snapshot.ts +53 -0
  63. package/packages/lsp-core/src/lsp/workspace-edit-text.ts +125 -0
  64. package/packages/lsp-core/src/lsp/workspace-edit-types.ts +121 -0
  65. package/packages/lsp-core/src/lsp/workspace-edit.characterization.test.ts +95 -0
  66. package/packages/lsp-core/src/lsp/workspace-edit.ts +49 -200
  67. package/packages/lsp-core/src/lsp/workspace-mutation-controller.ts +182 -0
  68. package/packages/lsp-core/src/mcp.ts +18 -7
  69. package/packages/lsp-core/src/missing-dependency-result.test.ts +105 -0
  70. package/packages/lsp-core/src/missing-dependency-result.ts +57 -0
  71. package/packages/lsp-core/src/post-edit/index.ts +1 -0
  72. package/packages/lsp-core/src/post-edit/orchestration.test.ts +157 -0
  73. package/packages/lsp-core/src/post-edit/orchestration.ts +178 -0
  74. package/packages/lsp-core/src/request-context.test.ts +171 -0
  75. package/packages/lsp-core/src/request-context.ts +222 -9
  76. package/packages/lsp-core/src/tool-surface.test.ts +4 -1
  77. package/packages/lsp-core/src/tools/diagnostics.ts +32 -13
  78. package/packages/lsp-core/src/tools/navigation.ts +12 -12
  79. package/packages/lsp-core/src/tools/rename.ts +10 -15
  80. package/packages/lsp-core/src/tools/symbols.ts +11 -11
  81. package/packages/lsp-core/src/tools/types.ts +2 -1
  82. package/packages/lsp-daemon/dist/cli.js +3114 -747
  83. package/packages/lsp-daemon/dist/client.d.ts +105 -0
  84. package/packages/lsp-daemon/dist/client.js +5851 -0
  85. package/packages/lsp-daemon/dist/daemon-client.d.ts +11 -6
  86. package/packages/lsp-daemon/dist/daemon-client.js +113 -30
  87. package/packages/lsp-daemon/dist/daemon-server.d.ts +1 -0
  88. package/packages/lsp-daemon/dist/daemon-server.js +40 -15
  89. package/packages/lsp-daemon/dist/ensure-daemon.d.ts +8 -7
  90. package/packages/lsp-daemon/dist/ensure-daemon.js +67 -44
  91. package/packages/lsp-daemon/dist/index.d.ts +2 -2
  92. package/packages/lsp-daemon/dist/index.js +2862 -754
  93. package/packages/lsp-daemon/dist/ipc-protocol.d.ts +46 -0
  94. package/packages/lsp-daemon/dist/ipc-protocol.js +187 -0
  95. package/packages/lsp-daemon/dist/lock.js +14 -4
  96. package/packages/lsp-daemon/dist/ownership.d.ts +49 -0
  97. package/packages/lsp-daemon/dist/ownership.js +168 -0
  98. package/packages/lsp-daemon/dist/paths.d.ts +33 -9
  99. package/packages/lsp-daemon/dist/paths.js +72 -33
  100. package/packages/lsp-daemon/dist/proxy.d.ts +3 -0
  101. package/packages/lsp-daemon/dist/proxy.js +54 -3
  102. package/packages/lsp-daemon/dist/request-routing.d.ts +7 -2
  103. package/packages/lsp-daemon/dist/request-routing.js +71 -22
  104. package/packages/lsp-daemon/dist/run-daemon.js +9 -2
  105. package/packages/lsp-daemon/dist/runtime-contract.d.ts +21 -0
  106. package/packages/lsp-daemon/dist/runtime-contract.js +58 -0
  107. package/packages/lsp-daemon/dist/socket-jsonrpc.js +6 -1
  108. package/packages/lsp-daemon/package.json +12 -3
  109. package/packages/lsp-tools-mcp/dist/cli.js +2115 -442
  110. package/packages/lsp-tools-mcp/dist/lsp/manager.js +1741 -148
  111. package/packages/lsp-tools-mcp/dist/mcp.js +2127 -454
  112. package/packages/lsp-tools-mcp/dist/request-context.js +176 -6
  113. package/packages/lsp-tools-mcp/dist/tools.js +2118 -446
  114. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  115. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  116. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  117. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  118. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  119. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  120. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  121. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  122. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  123. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  124. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +55 -0
  125. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +2959 -944
  126. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook-cli.js +0 -4
  127. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.d.ts +5 -2
  128. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.js +41 -62
  129. package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.d.ts +1 -1
  130. package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.js +24 -15
  131. package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.d.ts +3 -7
  132. package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.js +23 -49
  133. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  134. package/packages/omo-codex/plugin/components/lsp/package.json +3 -2
  135. package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.mjs +31 -1
  136. package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.test.mjs +76 -0
  137. package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.mjs +201 -0
  138. package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.test.mjs +55 -0
  139. package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +0 -4
  140. package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +49 -71
  141. package/packages/omo-codex/plugin/components/lsp/src/daemon-cli-path.ts +26 -15
  142. package/packages/omo-codex/plugin/components/lsp/src/lsp-session-state.ts +26 -64
  143. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-unavailable.test.ts +16 -17
  144. package/packages/omo-codex/plugin/components/lsp/test/codex-hook.test.ts +30 -4
  145. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +19 -5
  146. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.5.md +1 -1
  147. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +8 -6
  148. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  149. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  150. package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +1 -1
  151. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  152. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
  153. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  154. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  155. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  156. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  157. package/packages/omo-codex/plugin/components/ultrawork/directive.md +37 -10
  158. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  159. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  160. package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +37 -10
  161. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +37 -10
  162. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
  163. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  164. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +2 -2
  165. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +10 -9
  166. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  167. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  168. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  169. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
  170. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  171. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  172. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  173. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  174. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  175. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  176. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  177. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  178. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  179. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
  180. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  181. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  182. package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
  183. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  184. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
  185. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  186. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  187. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  188. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  189. package/packages/omo-codex/plugin/package-lock.json +26 -14
  190. package/packages/omo-codex/plugin/package.json +1 -1
  191. package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +2 -3
  192. package/packages/omo-codex/plugin/scripts/build-components.mjs +13 -1
  193. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +1 -1
  194. package/packages/omo-codex/plugin/skills/frontend/SKILL.md +1 -1
  195. package/packages/omo-codex/plugin/skills/frontend/references/design/README.md +9 -0
  196. package/packages/omo-codex/plugin/skills/frontend/references/design/design-system-architecture.md +4 -2
  197. package/packages/omo-codex/plugin/skills/frontend/references/design/layout-skill.md +107 -0
  198. package/packages/omo-codex/plugin/skills/programming/SKILL.md +12 -2
  199. package/packages/omo-codex/plugin/skills/start-work/SKILL.md +1 -1
  200. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +37 -10
  201. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +2 -2
  202. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +10 -9
  203. package/packages/omo-codex/plugin/test/aggregate-build.test.mjs +8 -0
  204. package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +128 -15
  205. package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +10 -0
  206. package/packages/omo-codex/plugin/test/lsp-prebuild-layouts.test.mjs +2 -0
  207. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +1 -1
  208. package/packages/omo-codex/scripts/install-dist/install-local.mjs +351 -74
  209. package/packages/shared-skills/skills/frontend/SKILL.md +1 -1
  210. package/packages/shared-skills/skills/frontend/references/design/README.md +9 -0
  211. package/packages/shared-skills/skills/frontend/references/design/design-system-architecture.md +4 -2
  212. package/packages/shared-skills/skills/frontend/references/design/layout-skill.md +107 -0
  213. package/packages/shared-skills/skills/programming/SKILL.md +12 -2
@@ -32,7 +32,7 @@ Every implementation must choose one of these branches before UI code changes:
32
32
  - **Embedded references:** use `references/design/_INDEX.md` to shortlist 2-3 plausible Layer B references, then read exactly one Layer A style skill and one Layer B reference in full — every line, no partial reads (they are 200-500 lines; a sliced read produces the flattened token set this gate exists to prevent). Log the shortlist, the pick, and why. Use `open-design` only when the curated set has no fit; add `ui-ux-db` lookups for palette/type/domain questions.
33
33
  - **Lazyweb real-product screens:** READ `references/design/lazyweb.md` FIRST and run its recipe verbatim — do not improvise curl calls against lazyweb.com; the recipe mints its own anonymous token. Log the queries run, how many screens you actually VIEWED, and the layout grammar harvested — never pixel copies.
34
34
  - **Imagen concept drafts:** generate 2-3 imagen concept drafts, each seeded with the loaded Layer A + Layer B tokens (palette, type, material); pick the strongest and treat the chosen draft as the reference-fidelity contract. Log the draft paths and the pick.
35
- Synthesize every lane into `DESIGN.md`. Treat sources as source material, not mood labels: extract tokens, layout grammar, component anatomy, interaction states, motion, and taste decisions, then recombine them into project-specific primitives. Never freestyle past the selected references, never copy logos or brand-specific copy. Then run the Primitive Showcase Gate (`references/design/README.md` Phase 0) before any product screen.
35
+ Synthesize every lane into `DESIGN.md`. Treat sources as source material, not mood labels: extract tokens, layout grammar, component anatomy, interaction states, motion, and taste decisions, then recombine them into project-specific primitives. Before laying out sections, inventory the content blocks and assign each a job — hook, explain, prove, compare, convert, navigate, retain — then order sections by the visitor's decision path, not by visual symmetry. Never freestyle past the selected references, never copy logos or brand-specific copy. Then run the Primitive Showcase Gate (`references/design/README.md` Phase 0) before any product screen.
36
36
  3. **Existing project with `DESIGN.md` or a component system:** read it, follow it, and update it before implementation only when the requested work needs a new token, primitive, state, motion rule, accessibility constraint, accepted debt, or reference-fidelity requirement.
37
37
  4. **Existing project with UI but no `DESIGN.md` and no reusable component layer:** STOP and ask the user one focused question: should you preserve the current look with copy-nearby styling, or extract a real `DESIGN.md` plus reusable components before continuing? Do not silently choose.
38
38
 
@@ -169,6 +169,12 @@ Triggers (mid-conversation, not initial): "you keep leaving placeholders", "stop
169
169
 
170
170
  **Action:** Add `output-skill.md` on top of whatever is currently loaded. This stacks cleanly — it is purely about output completeness, not visual style.
171
171
 
172
+ ### Step 8 — Is the screen an app shell, not a scroll-the-page site?
173
+
174
+ Triggers: dashboard, settings, mail/inbox, list-detail, command surface, split panes, fixed sidebar + scrolling body, admin console — or the user reports a layout that breaks when content gets long, empty, or unbroken (panel won't scroll, footer pushed off-screen, horizontal overflow on mobile).
175
+
176
+ **Action:** Add `layout-skill.md` on top of whatever style skill you selected in Steps 1-2. It carries scroll-ownership doctrine, the two silently-failing CSS contracts (`min-block-size: 0` scroll shells, `minmax(min(…),100%)` grids), the named-primitive vocabulary, container-vs-viewport routing, and the content-stress matrix. It adds no visual direction — the style skill still owns taste. Skip it for pure scroll-the-document marketing pages, where `taste-skill` layout guidance already fits.
177
+
172
178
  ## Stacking rules (read this once, internalize it)
173
179
 
174
180
  1. **At most one Layer A *style* skill at a time.** A layout cannot be both `minimalist-skill` and `brutalist-skill` simultaneously — they encode opposite spacing and typography philosophies. Pick one.
@@ -177,6 +183,7 @@ Triggers (mid-conversation, not initial): "you keep leaving placeholders", "stop
177
183
  4. **`redesign-skill.md` replaces a style-skill** when the task is auditing, not building. Stack a Layer B brand if the user wants a specific direction.
178
184
  5. **`image-to-code-skill.md` pairs with one imagegen skill** for the full flow.
179
185
  6. **Layer B (brand DESIGN.md) is orthogonal to Layer A.** You can pair any Layer A skill with any Layer B brand. Use Layer B as the source of color/type/component tokens; let Layer A drive the execution discipline.
186
+ 7. **`layout-skill.md` stacks on top of any style skill** for app-shell / dashboard / split-pane work. It owns spatial structure and scroll ownership only — no visual direction — so it never conflicts with the style skill you picked.
180
187
 
181
188
  ## Anti-patterns — do not do these
182
189
 
@@ -218,6 +225,8 @@ Once references are loaded, before writing any UI code:
218
225
  | "Generate a brand identity board for {company}" | `imagegen-brandkit.md` |
219
226
  | "Stop using placeholders" | Add `output-skill.md` to current stack |
220
227
  | "Also output a DESIGN.md doc" | Add `stitch-skill.md` to current stack |
228
+ | "Build a dashboard / settings / inbox / app shell" | one style skill (usually `taste-skill.md`) + `layout-skill.md` |
229
+ | "Panel won't scroll / footer pushed off-screen / mobile overflow" | Add `layout-skill.md` to current stack |
221
230
 
222
231
  ## Phase Final — Design QA (MANDATORY, runs after implementation)
223
232
 
@@ -116,7 +116,7 @@ All spacing derives from a base of **4px**.
116
116
  - Breakpoints: sm 640px, md 768px, lg 1024px, xl 1280px, 2xl 1536px
117
117
 
118
118
  ### Rules
119
- - No magic numbers. Every spacing value maps to a token.
119
+ - Tokenize design *intent* spacing steps, content width, gutters, section gaps, density steps. Keep browser *mechanics* raw: `auto`, `%`, `min-content`, `max-content`, `fit-content`, `clamp()`, viewport/container units, intrinsic sizing. A `clamp(1rem, 4vw, 2rem)` gap or a `minmax(min(16rem, 100%), 1fr)` track is mechanics, not a magic number — do not force it into a token.
120
120
  - Asymmetric spacing is intentional, not accidental — document why.
121
121
 
122
122
  ## 5. Components
@@ -130,6 +130,7 @@ Document reusable patterns before implementation for greenfield work, and as the
130
130
  - **States**: default, hover, active, focus, disabled, loading, empty, error
131
131
  - **Accessibility**: keyboard, ARIA, contrast
132
132
  - **Motion**: entry/exit animations
133
+ - **Layout**: spatial primitive (stack / cluster / sidebar / grid / shell…) and, if the component scrolls or pins anything, its scroll owner
133
134
 
134
135
  Greenfield starts with the primitives you are about to build, assembled from
135
136
  the selected references' component anatomy and adapted to the user's product.
@@ -214,13 +215,14 @@ After every component implementation, check:
214
215
 
215
216
  - [ ] All colors reference tokens from Section 2. No raw hex outside `DESIGN.md`.
216
217
  - [ ] All font sizes match Section 3 scale. No arbitrary sizes.
217
- - [ ] All spacing values are multiples of `--space-1` (4px). No magic numbers.
218
+ - [ ] Spacing intent maps to a token (Section 4); browser mechanics (`clamp()`, intrinsic sizing, container units) stay raw. No arbitrary px.
218
219
  - [ ] Interactive elements have all required states from Section 5 and Section 6.
219
220
  - [ ] Depth treatment matches the chosen strategy from Section 7.
220
221
  - [ ] Component reused 2+ times? Documented in Section 5.
221
222
  - [ ] Motion follows the timing table. No arbitrary durations.
222
223
  - [ ] Component visual QA passed for each primitive and required state before product screens were composed.
223
224
  - [ ] Section 8 accessibility constraints hold for the new component; any new debt is recorded in Section 8, not silently accepted.
225
+ - [ ] Survives content stress: empty, long label, unbroken string. Reflows to one readable column at 375px with no horizontal scroll of primary content.
224
226
 
225
227
  ## Memory Management
226
228
 
@@ -0,0 +1,107 @@
1
+ ---
2
+ name: layout-skill
3
+ description: "Layer A layout-mechanics reference. Stacks on any style skill when the screen is an app shell, dashboard, settings, list-detail, mail/inbox, or any layout with fixed regions plus a scrolling body — or when a layout breaks under long, empty, or unbroken content. Owns spatial structure and scroll ownership; owns zero visual taste. Load it alongside a style skill; it does not replace one."
4
+ ---
5
+
6
+ # Layout Mechanics
7
+
8
+ Style skills decide how a surface looks. This file decides how it is *built to hold content* — what scrolls, what stays fixed, how regions shrink, and which layouts survive real data. It stacks on top of any Layer A style skill (`taste-skill`, `minimalist-skill`, `soft-skill`, `brutalist-skill`, `gpt-tasteskill`) and any Layer B brand. It adds ZERO visual direction — color, type, shadow, radius, and motion still come from the style skill and `DESIGN.md`.
9
+
10
+ Load this when the screen is an **application shell**, not a scroll-the-whole-document marketing page: dashboards, settings, list-detail, mail/inbox, command surfaces, split panes, sidebars — or when a page that looked fine breaks the moment content gets long, empty, or unbroken.
11
+
12
+ ## 1. Scroll ownership — decide this BEFORE writing layout CSS
13
+
14
+ The single most common agent-built layout bug is an app shell where the wrong thing scrolls: the whole page scrolls when only a panel should, two panels fight over the scrollbar, or a "fixed" header scrolls away. Prevent it by naming ownership up front, in `DESIGN.md` and in the component:
15
+
16
+ - **What scrolls?** Name the ONE element that owns vertical scroll for this region.
17
+ - **What stays fixed?** Header, sidebar, footer, toolbar — list them.
18
+ - **Where is height determined?** The scroll container needs a bounded height ancestor, or it will grow instead of scroll.
19
+
20
+ Rules:
21
+
22
+ - **One scroll container per region unless each extra one has a named job.** Nested scrollbars with no declared responsibility are a defect — the user loses track of what a wheel/trackpad gesture will move.
23
+ - Full-height shells are bounded by `100dvh`/`100dvb` (dynamic viewport units), never `100vh` — `vh` causes the iOS Safari address-bar jump.
24
+ - A sticky element (`position: sticky`) follows document scroll; a fixed shell region (grid row/column with `overflow: auto` body) owns its own scroll. Do not mix the two models in one region without reason.
25
+
26
+ ## 2. The two CSS contracts agents get wrong
27
+
28
+ These two are worth memorizing verbatim. They fail *silently* — the layout looks right until content arrives.
29
+
30
+ **Bounded scroll shell** (fixed header/footer, scrolling body):
31
+
32
+ ```css
33
+ .shell {
34
+ display: grid;
35
+ grid-template-rows: auto minmax(0, 1fr) auto; /* header / body / footer */
36
+ max-block-size: 100dvb;
37
+ }
38
+ .shell__body {
39
+ min-block-size: 0; /* WITHOUT THIS the grid child refuses to shrink and overflow never fires */
40
+ overflow: auto;
41
+ }
42
+ ```
43
+
44
+ The `min-block-size: 0` (or `min-height: 0`) on the scroll child is the fix for "why won't my panel scroll — it just pushes the footer off-screen." A grid/flex child's default `min-*-size: auto` refuses to shrink below its content. The same applies to a flex column: the scrollable child needs `min-height: 0`.
45
+
46
+ **Overflow-safe intrinsic grid** (repeat as many columns as fit, no media queries):
47
+
48
+ ```css
49
+ .grid {
50
+ display: grid;
51
+ gap: var(--gap);
52
+ grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
53
+ }
54
+ ```
55
+
56
+ The inner `min(16rem, 100%)` is load-bearing: plain `minmax(16rem, 1fr)` forces a 16rem track even when the container is 12rem wide, causing horizontal overflow on narrow screens. `min(…, 100%)` lets the track collapse below its floor when space is genuinely tight. Use `auto-fit` to stretch the last row, `auto-fill` to keep empty tracks.
57
+
58
+ ## 3. Named layout primitives (shared vocabulary)
59
+
60
+ Build shells from these named primitives instead of ad-hoc fl/grid. Naming them makes `DESIGN.md` Section 5 and subagent handoffs precise ("wrap it in a `sidebar` with a 20rem aside" beats "put it on the left, roughly"). Lineage: Every Layout + web.dev one-line layouts.
61
+
62
+ | Primitive | Spatial job | Core mechanic |
63
+ |---|---|---|
64
+ | **stack** | Vertical rhythm between siblings | flex column + `gap`, or `> * + *` margin |
65
+ | **cluster** | Wrapping row of items (tags, actions) | `flex-wrap: wrap` + `gap`; wraps before overflow |
66
+ | **content-limiter** | Readable prose measure inside a fluid parent | `max-inline-size: ~65ch; margin-inline: auto` |
67
+ | **sidebar** | Narrow aside + fluid main, wraps when tight | flex; aside fixed basis, main `min-inline-size` floor, both `flex-wrap` |
68
+ | **switcher** | N equal regions: row when roomy, stack when tight, NO breakpoint | flex + `min()` basis so it flips at a content threshold |
69
+ | **cover** | Centered region between optional header/footer, min viewport tall | grid rows `auto 1fr auto`, `min-block-size: 100dvb` |
70
+ | **frame** | Media held to an aspect ratio | `aspect-ratio` + `object-fit: cover` |
71
+ | **reel** | Row that scrolls horizontally instead of wrapping | `overflow-inline: auto` + `scroll-snap`; declare keyboard access |
72
+ | **imposter** | Overlay centered over a parent without changing document order | `position: absolute` + translate; do not use to reorder focus |
73
+ | **overlay-stack** | Several layers intentionally in one cell | single grid cell, all children `grid-area: 1/1` |
74
+ | **scroll-body-shell** | Fixed shell regions, only the body scrolls | §2 bounded scroll shell |
75
+ | **fixed-sidenav-shell** | Side nav stays put, main scrolls | grid columns `auto 1fr`, main is the scroll owner |
76
+ | **list-detail** | Explorable list beside its detail region | two-column grid, each pane's scroll ownership named |
77
+ | **sticky-aside** | Support content stays visible during a long read | `position: sticky; top:` on the aside, document scroll |
78
+
79
+ ## 4. Container-local vs viewport-level responsiveness
80
+
81
+ Ask **what the layout is responding to** before reaching for a breakpoint:
82
+
83
+ - The component should adapt to *its own available width* (a card that's wide in main but narrow in a rail) → wrap it in `container-type: inline-size` and use `@container`. This is correct far more often than agents assume, because a component rarely knows the viewport it lands in.
84
+ - The *page frame itself* changes (sidebar collapses, columns drop) → `@media`.
85
+
86
+ Prefer intrinsic adaptation (`switcher`, `sidebar`, intrinsic grid, `clamp()`) over any query — the best breakpoint is often none. Use breakpoint *names* for layout states, never device names (`--bp-wide`, not `--bp-ipad`).
87
+
88
+ ## 5. Recipes are spatial models, not product categories
89
+
90
+ Choose a layout by the screen's spatial shape, not its product label. A settings page and a docs app both want `fixed-sidenav-shell`; a support inbox and a file browser both want `list-detail`; a metrics view and a photo gallery both want an intrinsic grid. Do not invent a bespoke "dashboard layout" when `page-grid + intrinsic grid + cluster` already covers it — and do not force marketing-page structure (hero, zigzag, bento) onto a task app.
91
+
92
+ ## 6. Content stress — the layout is not done until it survives this
93
+
94
+ Landing pages fail on taste; app shells fail on *content*. Before declaring any layout done, stress every region against:
95
+
96
+ - **Empty** — no rows, no avatar, no value. Does the region collapse gracefully or leave a broken frame?
97
+ - **Long label** — a 40-char name in a 12-char slot. Truncate (`text-overflow: ellipsis`) or wrap by design, never by accident.
98
+ - **Long paragraph** — does the measure stay readable, or does text run 200 chars wide?
99
+ - **Unbroken string** — a URL or token with no spaces. Needs `overflow-wrap: anywhere` / `min-inline-size: 0`, or it forces horizontal scroll.
100
+ - **Reflow** — at 375px width the layout reflows to a single readable column with NO horizontal scrollbar. Two-dimensional scrolling of primary content is a fail.
101
+ - **Direction** — if the app supports RTL, the layout uses logical properties (`margin-inline`, `inset-inline-start`) so it mirrors correctly.
102
+
103
+ A layout that only holds the happy-path mock is not finished. Drive these states in `/visual-qa` alongside the interaction states the style skill requires.
104
+
105
+ ## Boundary
106
+
107
+ This file owns spatial structure only. It never sets color, typography, shadow, radius, or motion values — those trace to `DESIGN.md` and the loaded style skill. If you find yourself adding a brand color to a layout primitive, stop: the primitive stays layout-only and the styling wraps or composes around it.
@@ -104,9 +104,17 @@ Mocks are a last resort, not a default. The priority order:
104
104
  - **Deterministic**: no `sleep`, no wall-clock dependence, no order dependence (`-shuffle=on`, pytest-randomly, vitest random seed). Inject a `Clock`. Subscribe to the event, do not poll for it. Time-based flake is a bug, not a test issue.
105
105
  - **Isolated**: every test starts from a known fixture and tears down. `t.TempDir()`, `t.Setenv()`, transactional rollback for DB tests. Two tests passing individually but failing together is a fixture leak — fix it immediately.
106
106
 
107
- ### Prompt tests follow the same rule
107
+ ### Prompt tests: NEVER assert prose
108
108
 
109
- When tests cover LLM prompts or agent outputs, assert on **parsed structure, decisions, or rule data**, never on exact prompt strings. Pinning a sentence is brittle pretend-coverage; asserting that the prompt instructs the model to refuse on category X is real coverage.
109
+ **FORBIDDEN NO EXCEPTIONS: a test MUST NOT assert natural-language prompt text.** `expect(prompt).toContain("based on GPT-5.6")`, `not.toContain("old wording")`, `toMatchSnapshot()` on prose, grepping a sentence fragment — every one of these is pretend-coverage. It stays green while the behavior it claims to guard breaks, then blocks every legitimate rewording until someone bumps the pinned string. A reviewer MUST block it as HIGH; deleting such a test is a fix, not a coverage loss. "A nearby test already does it" is not a defense — that test is the disease, not the convention.
110
+
111
+ Assert ONLY what a machine consumes:
112
+
113
+ - the builder's routing decision — `expect(getPromptSource(model)).toBe("gpt-5-6")`, never the sentence that routing produces
114
+ - a structural token the runtime dispatches on — a tool name, a tag like `<agent-identity>`, a parsed frontmatter field
115
+ - the conditional the code enforces — skill loaded → tool present; `verbose=false` → directive absent
116
+
117
+ If no machine consumes the text, there is no seam: write NO test and say so in the PR; review guards prose. When you DELEGATE test-writing, hand the child the behavior the test must distinguish ("fails if override precedence breaks"), never a ready-made assertion string, prompt fragment, or marker to copy — a prescribed mechanism that is wrong gets implemented faithfully, and the error ships with a green suite.
110
118
 
111
119
  ### Anti-patterns the skill rejects
112
120
 
@@ -119,6 +127,8 @@ When tests cover LLM prompts or agent outputs, assert on **parsed structure, dec
119
127
  | Snapshot tests for everything | Locks formatting, not behavior. | Snapshots for *structure* (CLI help, JSON shape). Assertions for *behavior*. |
120
128
  | Removing a failing test to "unblock CI" | You just deleted a bug report. | Fix the code or fix the test — never delete to silence. |
121
129
  | `assert result is not None` and stopping there | Passes when result is garbage. | Assert the *value*, not its existence. |
130
+ | Expected value derived from the output under test (`expect(config.prompt).toBe(getPrompt(config.model))` when the criterion is about `config.prompt`) | Recomputes a projection of the output and compares it to itself — passes even when the artifact is built from the wrong input. | Derive the expected value from the test's *input*: `expect(config.prompt).toBe(getPrompt(inputModel))` (independent known-good builder fed the fixture's input), or a stable builder routing decision. |
131
+ | Override/precedence fixture equal to its fallback (override == system default) | The assertion passes whether or not the code honored the override — precedence is never exercised. | Make every value the code must select, preserve, or override differ from its fallback. Prove it: temporarily force the regression the test names, watch it fail, revert. |
122
132
  | Single happy-path E2E, no edges | Most bugs live on edges. | Edges are unit-test territory — but include at least one E2E that exercises an error path. |
123
133
 
124
134
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-opencode",
3
- "version": "4.17.1",
3
+ "version": "4.18.1",
4
4
  "description": "The Best AI Agent Harness - Batteries-Included OpenCode Plugin with Multi-Model Orchestration, Parallel Background Agents, and Crafted LSP/AST Tools",
5
5
  "main": "./dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -107,8 +107,10 @@
107
107
  "build": "bun run script/build.ts",
108
108
  "build:cli-node": "bun run script/build-cli-node.ts",
109
109
  "build:codex-install": "bun run script/build-codex-install.ts",
110
+ "install:codex-dev": "bun run script/build-codex-install.ts && bun run script/install-codex-dev.ts",
110
111
  "build:codex-plugin": "npm --prefix packages/omo-codex/plugin ci && bun run --cwd packages/omo-codex/plugin build",
111
- "build:senpi-plugin": "node packages/omo-senpi/plugin/scripts/build-extension.mjs && node packages/omo-senpi/plugin/scripts/sync-skills.mjs && node packages/omo-senpi/plugin/scripts/embed-directive.mjs --check",
112
+ "build:senpi-plugin": "bun run build:lsp-daemon && bun run build:senpi-plugin:stage",
113
+ "build:senpi-plugin:stage": "node packages/omo-senpi/plugin/scripts/stage-lsp-daemon-runtime.mjs && node packages/omo-senpi/plugin/scripts/build-extension.mjs && node packages/omo-senpi/plugin/scripts/sync-skills.mjs && node packages/omo-senpi/plugin/scripts/embed-directive.mjs --check && node packages/omo-senpi/plugin/scripts/build-install.mjs",
112
114
  "build:materialize-frontend": "node packages/omo-codex/plugin/scripts/materialize-shared-upstreams.mjs --strict",
113
115
  "build:shared-skills-assets": "bun run build:materialize-frontend && rm -rf dist/skills && cp -R packages/shared-skills/skills dist/skills",
114
116
  "build:lsp-tools-mcp": "npm --prefix packages/lsp-tools-mcp ci && npm --prefix packages/lsp-tools-mcp run build",
@@ -130,7 +132,7 @@
130
132
  "typecheck:script": "tsgo --noEmit -p script/tsconfig.json",
131
133
  "test": "bun test",
132
134
  "test:codex": "bun run build:codex-install && bun run build:git-bash-mcp && bun run build:lsp-tools-mcp && bun run build:lsp-daemon && npm --prefix packages/lsp-tools-mcp test && npm --prefix packages/omo-codex/plugin ci && npm --prefix packages/omo-codex/plugin/components/ulw-loop test && bun run --cwd packages/omo-codex/plugin build && npm --prefix packages/omo-codex/plugin/components/codegraph run typecheck && npm --prefix packages/omo-codex/plugin/components/codegraph test && node scripts/check-third-party-notices.mjs --ship && bun test packages/omo-opencode/src/cli/cli-installer.platform.test.ts packages/omo-codex/src/install/codex-cache.test.ts packages/omo-codex/src/install/codex-cleanup.test.ts packages/omo-codex/src/install/codex-config-agent-cleanup.test.ts packages/omo-codex/src/install/codex-config-autonomous-features.test.ts packages/omo-codex/src/install/codex-config-reasoning.test.ts packages/omo-codex/src/install/codex-config-toml.test.ts packages/omo-codex/src/install/codex-project-local-cleanup.test.ts packages/omo-codex/src/install/install-codex-project-local-cleanup.test.ts packages/omo-codex/src/install/install-codex.test.ts packages/omo-codex/src/install/install-codex-packaged.test.ts packages/omo-codex/src/install/link-cached-plugin-agents.test.ts packages/omo-codex/src/**/*.test.ts packages/utils/src/jsonc-parser.test.ts packages/utils/src/frontmatter.test.ts packages/hashline-core/src/hash-computation.test.ts packages/hashline-core/src/smoke-untested-modules.test.ts packages/rules-engine/src/index.test.ts packages/rules-engine/src/security-boundary.test.ts packages/agents-md-core/src/injector.test.ts packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts && node --test packages/omo-codex/plugin/test/*.test.mjs packages/omo-codex/scripts/install-cache-copy.test.mjs packages/omo-codex/scripts/install-cli-args.test.mjs packages/omo-codex/scripts/install-delegated-command.test.mjs packages/omo-codex/scripts/install-config-autonomous-features.test.mjs packages/omo-codex/scripts/install-config-autonomous.test.mjs packages/omo-codex/scripts/install-config-reasoning.test.mjs packages/omo-codex/scripts/install-config.test.mjs packages/omo-codex/scripts/install-hook-targets.test.mjs packages/omo-codex/scripts/install-project-local-cleanup.test.mjs packages/omo-codex/scripts/install-lazycodex-version-stamp.test.mjs packages/omo-codex/scripts/install-local-entrypoint.test.mjs packages/omo-codex/scripts/install-local-git-bash-preflight.test.mjs packages/omo-codex/scripts/install-local.test.mjs packages/omo-codex/scripts/install-marketplace-cache.test.mjs packages/omo-codex/scripts/install-mcp-context7-runtime.test.mjs packages/omo-codex/scripts/install-mcp-runtime.test.mjs packages/omo-codex/scripts/install-packaged-local.test.mjs packages/omo-codex/scripts/install-generated-bundle.test.mjs packages/omo-codex/scripts/install-agent-links.test.mjs packages/omo-codex/scripts/install-bin-links.test.mjs",
133
- "test:senpi": "node packages/omo-senpi/plugin/scripts/build-extension.mjs && node packages/omo-senpi/plugin/scripts/sync-skills.mjs && node packages/omo-senpi/plugin/scripts/embed-directive.mjs --check && bun test packages/omo-senpi",
135
+ "test:senpi": "bun run build:senpi-plugin && tsgo --noEmit -p packages/omo-senpi/tsconfig.json && bun test packages/omo-senpi",
134
136
  "test:windows-codex": "bun run test:codex",
135
137
  "build:git-bash-mcp": "bun run --cwd packages/git-bash-mcp build"
136
138
  },
@@ -170,7 +172,6 @@
170
172
  "picocolors": "^1.1.1",
171
173
  "picomatch": "^4.0.4",
172
174
  "posthog-node": "^5.34.3",
173
- "vscode-jsonrpc": "^8.2.1",
174
175
  "zod": "^4.4.3"
175
176
  },
176
177
  "devDependencies": {
@@ -211,18 +212,18 @@
211
212
  "typescript": "^6.0.3"
212
213
  },
213
214
  "optionalDependencies": {
214
- "oh-my-opencode-darwin-arm64": "4.17.1",
215
- "oh-my-opencode-darwin-x64": "4.17.1",
216
- "oh-my-opencode-darwin-x64-baseline": "4.17.1",
217
- "oh-my-opencode-linux-arm64": "4.17.1",
218
- "oh-my-opencode-linux-arm64-musl": "4.17.1",
219
- "oh-my-opencode-linux-x64": "4.17.1",
220
- "oh-my-opencode-linux-x64-baseline": "4.17.1",
221
- "oh-my-opencode-linux-x64-musl": "4.17.1",
222
- "oh-my-opencode-linux-x64-musl-baseline": "4.17.1",
223
- "oh-my-opencode-windows-arm64": "4.17.1",
224
- "oh-my-opencode-windows-x64": "4.17.1",
225
- "oh-my-opencode-windows-x64-baseline": "4.17.1"
215
+ "oh-my-opencode-darwin-arm64": "4.18.1",
216
+ "oh-my-opencode-darwin-x64": "4.18.1",
217
+ "oh-my-opencode-darwin-x64-baseline": "4.18.1",
218
+ "oh-my-opencode-linux-arm64": "4.18.1",
219
+ "oh-my-opencode-linux-arm64-musl": "4.18.1",
220
+ "oh-my-opencode-linux-x64": "4.18.1",
221
+ "oh-my-opencode-linux-x64-baseline": "4.18.1",
222
+ "oh-my-opencode-linux-x64-musl": "4.18.1",
223
+ "oh-my-opencode-linux-x64-musl-baseline": "4.18.1",
224
+ "oh-my-opencode-windows-arm64": "4.18.1",
225
+ "oh-my-opencode-windows-x64": "4.18.1",
226
+ "oh-my-opencode-windows-x64-baseline": "4.18.1"
226
227
  },
227
228
  "overrides": {
228
229
  "@earendil-works/pi-agent-core": "0.80.3",
@@ -25,6 +25,10 @@
25
25
  "types": "./src/mcp.ts",
26
26
  "import": "./src/mcp.ts"
27
27
  },
28
+ "./post-edit": {
29
+ "types": "./src/post-edit/index.ts",
30
+ "import": "./src/post-edit/index.ts"
31
+ },
28
32
  "./lsp/*": {
29
33
  "types": "./src/lsp/*.ts",
30
34
  "import": "./src/lsp/*.ts"
@@ -24,5 +24,6 @@ export * from "./lsp/utils.js";
24
24
  export * from "./lsp/workspace-edit.js";
25
25
  export * from "./mcp.js";
26
26
  export * from "./missing-dependency-result.js";
27
+ export * from "./post-edit/index.js";
27
28
  export * from "./request-context.js";
28
29
  export * from "./tools.js";
@@ -0,0 +1,18 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { reportBestEffortCleanupError } from "./cleanup-errors.js";
4
+
5
+ describe("reportBestEffortCleanupError", () => {
6
+ test("#given an injected cleanup logger #when cleanup fails #then the normal diagnostic is emitted without an environment gate", () => {
7
+ // given
8
+ const messages: string[] = [];
9
+
10
+ // when
11
+ reportBestEffortCleanupError("client stop", new Error("connection closed"), (message) => {
12
+ messages.push(message);
13
+ });
14
+
15
+ // then
16
+ expect(messages).toEqual(["[lsp] ignored client stop failure during cleanup: connection closed"]);
17
+ });
18
+ });
@@ -1,5 +1,14 @@
1
- export function reportBestEffortCleanupError(operation: string, error: unknown): void {
2
- if (process.env["CODEX_LSP_DEBUG_CLEANUP"] !== "1") return;
1
+ export type CleanupErrorLogger = (message: string) => void;
2
+
3
+ function writeCleanupError(message: string): void {
4
+ process.stderr.write(`${message}\n`);
5
+ }
6
+
7
+ export function reportBestEffortCleanupError(
8
+ operation: string,
9
+ error: unknown,
10
+ logger: CleanupErrorLogger = writeCleanupError,
11
+ ): void {
3
12
  const message = error instanceof Error ? error.message : String(error);
4
- console.error(`[codex-lsp] ignored ${operation} failure during cleanup: ${message}`);
13
+ logger(`[lsp] ignored ${operation} failure during cleanup: ${message}`);
5
14
  }
@@ -0,0 +1,261 @@
1
+ import { writeFileSync } from "node:fs";
2
+
3
+ import { afterEach, describe, expect, it } from "bun:test";
4
+
5
+ import {
6
+ createWorkspaceEditTestHarness,
7
+ diagnostic,
8
+ readEvents,
9
+ waitForEventCount,
10
+ } from "./workspace-apply-edit-test-support.js";
11
+
12
+ const harness = createWorkspaceEditTestHarness();
13
+
14
+ afterEach(async () => {
15
+ await harness.cleanup();
16
+ });
17
+
18
+ describe("LspClient diagnostics freshness", () => {
19
+ it("#given concurrent diagnostics on a cold file and an exact didOpen publish #when pull is not advertised #then one didOpen opens the file and both requests receive the current diagnostics", async () => {
20
+ const context = await harness.makeClient(
21
+ {
22
+ publishDiagnostics: [
23
+ {
24
+ trigger: "didOpen",
25
+ version: 1,
26
+ diagnostics: [diagnostic("exact-current")],
27
+ },
28
+ ],
29
+ },
30
+ { diagnosticsFreshnessTimeoutMs: 50, versionlessPublishQuiescenceMs: 5 },
31
+ );
32
+
33
+ const [first, second] = await Promise.all([
34
+ context.client.diagnostics(context.source),
35
+ context.client.diagnostics(context.source),
36
+ ]);
37
+
38
+ expect(first.items).toEqual([diagnostic("exact-current")]);
39
+ expect(second.items).toEqual([diagnostic("exact-current")]);
40
+ expect(
41
+ readEvents(context.events).filter(
42
+ (event) => event.type === "clientNotification" && event.method === "textDocument/didOpen",
43
+ ),
44
+ ).toHaveLength(1);
45
+ });
46
+
47
+ it("#given a versionless publish that arrives after the current change #when no newer eligible publish arrives before quiescence #then diagnostics wait for that quiescence window and return the versionless payload", async () => {
48
+ const context = await harness.makeClient(
49
+ {
50
+ publishDiagnostics: [
51
+ {
52
+ trigger: "didChange",
53
+ diagnostics: [diagnostic("post-generation-versionless")],
54
+ },
55
+ ],
56
+ },
57
+ { diagnosticsFreshnessTimeoutMs: 80, versionlessPublishQuiescenceMs: 20 },
58
+ );
59
+ await context.client.openFile(context.source);
60
+ await waitForEventCount(
61
+ context.events,
62
+ (event) => event.type === "serverNotification" && event.method === "textDocument/publishDiagnostics",
63
+ 1,
64
+ );
65
+ writeFileSync(context.source, "const after = 1;\n", "utf-8");
66
+ await context.client.openFile(context.source);
67
+
68
+ const startedAt = Date.now();
69
+ const result = await context.client.diagnostics(context.source);
70
+ const elapsedMs = Date.now() - startedAt;
71
+
72
+ expect(result.items).toEqual([diagnostic("post-generation-versionless")]);
73
+ expect(elapsedMs).toBeGreaterThanOrEqual(15);
74
+ });
75
+
76
+ it("#given only a pre-generation versionless publish #when a newer local version asks for diagnostics #then the stale versionless publish is ignored and the request does not resolve clean", async () => {
77
+ const context = await harness.makeClient(
78
+ {
79
+ publishDiagnostics: [
80
+ {
81
+ trigger: "didOpen",
82
+ diagnostics: [diagnostic("pre-generation-versionless")],
83
+ },
84
+ ],
85
+ },
86
+ { diagnosticsFreshnessTimeoutMs: 300, versionlessPublishQuiescenceMs: 5 },
87
+ );
88
+ await context.client.openFile(context.source);
89
+ expect((await context.client.diagnostics(context.source)).items).toEqual([diagnostic("pre-generation-versionless")]);
90
+ writeFileSync(context.source, "const after = 1;\n", "utf-8");
91
+ await context.client.openFile(context.source);
92
+
93
+ const result = await context.client.diagnostics(context.source);
94
+
95
+ expect(result.items).toEqual([]);
96
+ expect(result.transientError?.kind).toBe("freshness_timeout");
97
+ });
98
+
99
+ it("#given stale and future publish versions #when diagnostics target the current version #then neither stale nor future diagnostics satisfy the request", async () => {
100
+ const stale = await harness.makeClient(
101
+ {
102
+ publishDiagnostics: [
103
+ {
104
+ trigger: "didChange",
105
+ version: 1,
106
+ diagnostics: [diagnostic("stale")],
107
+ },
108
+ ],
109
+ },
110
+ { diagnosticsFreshnessTimeoutMs: 30, versionlessPublishQuiescenceMs: 5 },
111
+ );
112
+ await stale.client.openFile(stale.source);
113
+ writeFileSync(stale.source, "const stale = 1;\n", "utf-8");
114
+ await stale.client.openFile(stale.source);
115
+
116
+ const staleResult = await stale.client.diagnostics(stale.source);
117
+
118
+ expect(staleResult.items).toEqual([]);
119
+ expect(staleResult.transientError?.kind).toBe("freshness_timeout");
120
+
121
+ await harness.cleanup();
122
+
123
+ const future = await harness.makeClient(
124
+ {
125
+ publishDiagnostics: [
126
+ {
127
+ trigger: "didChange",
128
+ version: 3,
129
+ diagnostics: [diagnostic("future")],
130
+ },
131
+ ],
132
+ },
133
+ { diagnosticsFreshnessTimeoutMs: 30, versionlessPublishQuiescenceMs: 5 },
134
+ );
135
+ await future.client.openFile(future.source);
136
+ writeFileSync(future.source, "const future = 1;\n", "utf-8");
137
+ await future.client.openFile(future.source);
138
+
139
+ const futureResult = await future.client.diagnostics(future.source);
140
+
141
+ expect(futureResult.items).toEqual([]);
142
+ expect(futureResult.transientError?.kind).toBe("freshness_timeout");
143
+ });
144
+
145
+ it("#given a pull response overtaken by a later local change #when the stale full report returns first #then diagnostics restart within the same request and resolve from the current version", async () => {
146
+ const context = await harness.makeClient(
147
+ {
148
+ capabilities: { diagnosticProvider: { interFileDependencies: false, workspaceDiagnostics: false } },
149
+ diagnosticResponses: [
150
+ {
151
+ delayMs: 20,
152
+ report: { kind: "full", resultId: "v1", items: [diagnostic("pull-stale")] },
153
+ },
154
+ {
155
+ report: { kind: "full", resultId: "v2", items: [diagnostic("pull-fresh")] },
156
+ },
157
+ ],
158
+ },
159
+ { diagnosticsFreshnessTimeoutMs: 80, versionlessPublishQuiescenceMs: 5 },
160
+ );
161
+ await context.client.openFile(context.source);
162
+
163
+ const pending = context.client.diagnostics(context.source);
164
+ await waitForEventCount(
165
+ context.events,
166
+ (event) => event.type === "clientRequest" && event.method === "textDocument/diagnostic",
167
+ 1,
168
+ );
169
+ writeFileSync(context.source, "const pull_fresh = 1;\n", "utf-8");
170
+ await context.client.openFile(context.source);
171
+
172
+ const result = await pending;
173
+
174
+ expect(result.items).toEqual([diagnostic("pull-fresh")]);
175
+ expect(
176
+ readEvents(context.events).filter(
177
+ (event) => event.type === "clientRequest" && event.method === "textDocument/diagnostic",
178
+ ),
179
+ ).toHaveLength(2);
180
+ });
181
+
182
+ it("#given a full pull report followed by an unchanged report for the same version and resultId #when diagnostics repeat without a local change #then the cached full items are reused", async () => {
183
+ const context = await harness.makeClient(
184
+ {
185
+ capabilities: { diagnosticProvider: { interFileDependencies: false, workspaceDiagnostics: false } },
186
+ diagnosticResponses: [
187
+ {
188
+ report: { kind: "full", resultId: "same-version", items: [diagnostic("cached-full")] },
189
+ },
190
+ {
191
+ report: { kind: "unchanged", resultId: "same-version" },
192
+ },
193
+ ],
194
+ },
195
+ { diagnosticsFreshnessTimeoutMs: 500, versionlessPublishQuiescenceMs: 5 },
196
+ );
197
+ await context.client.openFile(context.source);
198
+
199
+ const first = await context.client.diagnostics(context.source);
200
+ const second = await context.client.diagnostics(context.source);
201
+
202
+ expect(first.items).toEqual([diagnostic("cached-full")]);
203
+ expect(second.items).toEqual([diagnostic("cached-full")]);
204
+ });
205
+
206
+ it("#given a server that claims pull support but closes instead of answering #when diagnostics run #then the transport failure is not labeled clean", async () => {
207
+ const context = await harness.makeClient(
208
+ {
209
+ capabilities: { diagnosticProvider: { interFileDependencies: false, workspaceDiagnostics: false } },
210
+ diagnosticResponses: [{ action: "close" }],
211
+ },
212
+ { diagnosticsFreshnessTimeoutMs: 50, versionlessPublishQuiescenceMs: 5 },
213
+ );
214
+ await context.client.openFile(context.source);
215
+
216
+ await expect(context.client.diagnostics(context.source)).rejects.toThrow(/exited|closed/i);
217
+ });
218
+
219
+ it("#given an advertised pull method that is explicitly unsupported and a matching push publish #when diagnostics run #then the client falls back to push diagnostics", async () => {
220
+ const context = await harness.makeClient(
221
+ {
222
+ capabilities: { diagnosticProvider: { interFileDependencies: false, workspaceDiagnostics: false } },
223
+ publishDiagnostics: [
224
+ {
225
+ trigger: "didOpen",
226
+ version: 1,
227
+ diagnostics: [diagnostic("push-fallback")],
228
+ },
229
+ ],
230
+ diagnosticResponses: [{ error: { code: -32601, message: "Method not found" } }],
231
+ },
232
+ { diagnosticsFreshnessTimeoutMs: 50, versionlessPublishQuiescenceMs: 5 },
233
+ );
234
+
235
+ const result = await context.client.diagnostics(context.source);
236
+
237
+ expect(result.items).toEqual([diagnostic("push-fallback")]);
238
+ });
239
+
240
+ it("#given a diagnostic pull request times out #when the fake server keeps it pending #then the client sends cancel for the LSP request id", async () => {
241
+ const context = await harness.makeClient(
242
+ {
243
+ capabilities: { diagnosticProvider: { interFileDependencies: false, workspaceDiagnostics: false } },
244
+ diagnosticResponses: [{ action: "hang" }],
245
+ },
246
+ { requestTimeoutMs: 30, diagnosticsFreshnessTimeoutMs: 50, versionlessPublishQuiescenceMs: 5 },
247
+ );
248
+
249
+ const result = await context.client.diagnostics(context.source);
250
+ const [cancel] = await waitForEventCount(
251
+ context.events,
252
+ (event) => event.type === "clientNotification" && event.method === "$/cancelRequest",
253
+ 1,
254
+ );
255
+ const events = readEvents(context.events);
256
+ const request = events.find((event) => event.type === "clientRequest" && event.method === "textDocument/diagnostic");
257
+
258
+ expect(result.transientError?.kind).toBe("freshness_timeout");
259
+ expect(cancel?.params).toEqual({ id: request?.id });
260
+ });
261
+ });