oh-my-opencode 4.5.12 → 4.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/.agents/skills/opencode-qa/SKILL.md +194 -0
  2. package/.agents/skills/opencode-qa/references/cli-commands.md +188 -0
  3. package/.agents/skills/opencode-qa/references/db-investigation.md +197 -0
  4. package/.agents/skills/opencode-qa/references/events-hooks.md +110 -0
  5. package/.agents/skills/opencode-qa/references/sdk.md +96 -0
  6. package/.agents/skills/opencode-qa/references/server-api.md +200 -0
  7. package/.agents/skills/opencode-qa/references/testing-harness.md +218 -0
  8. package/.agents/skills/opencode-qa/references/tui-tmux.md +52 -0
  9. package/.agents/skills/opencode-qa/scripts/db-session-by-id.sh +53 -0
  10. package/.agents/skills/opencode-qa/scripts/db-session-by-name.sh +57 -0
  11. package/.agents/skills/opencode-qa/scripts/db-session-by-text.sh +158 -0
  12. package/.agents/skills/opencode-qa/scripts/export-roundtrip.sh +57 -0
  13. package/.agents/skills/opencode-qa/scripts/lib/common.sh +216 -0
  14. package/.agents/skills/opencode-qa/scripts/server-smoke.sh +64 -0
  15. package/.agents/skills/opencode-qa/scripts/sse-hook-probe.sh +106 -0
  16. package/.agents/skills/opencode-qa/scripts/tui-smoke.sh +89 -0
  17. package/README.ja.md +13 -3
  18. package/README.ko.md +13 -3
  19. package/README.md +24 -14
  20. package/README.ru.md +13 -3
  21. package/README.zh-cn.md +13 -3
  22. package/bin/oh-my-opencode.js +4 -3
  23. package/bin/oh-my-opencode.test.ts +35 -7
  24. package/bin/platform.d.ts +1 -1
  25. package/bin/platform.js +4 -4
  26. package/bin/platform.test.ts +31 -9
  27. package/bin/version-mismatch.js +47 -0
  28. package/bin/version-mismatch.test.ts +120 -0
  29. package/dist/cli/cleanup-command.d.ts +4 -0
  30. package/dist/cli/cleanup.d.ts +11 -0
  31. package/dist/cli/cli-program.d.ts +2 -1
  32. package/dist/cli/codex-ulw-loop.d.ts +12 -0
  33. package/dist/cli/doctor/checks/tui-plugin-config.d.ts +2 -0
  34. package/dist/cli/index.js +2189 -529
  35. package/dist/cli/install-codex/codex-cache.d.ts +1 -0
  36. package/dist/cli/install-codex/codex-cleanup-config.d.ts +6 -0
  37. package/dist/cli/install-codex/codex-cleanup.d.ts +21 -0
  38. package/dist/cli/install-codex/codex-config-permissions.d.ts +1 -0
  39. package/dist/cli/install-codex/codex-config-reasoning.d.ts +2 -0
  40. package/dist/cli/install-codex/codex-config-toml.d.ts +2 -1
  41. package/dist/cli/install-codex/codex-installation-detection.d.ts +36 -0
  42. package/dist/cli/install-codex/codex-model-catalog.d.ts +13 -0
  43. package/dist/cli/install-codex/codex-package-layout.d.ts +1 -0
  44. package/dist/cli/install-codex/codex-project-local-cleanup-best-effort.d.ts +7 -0
  45. package/dist/cli/install-codex/codex-project-local-cleanup.d.ts +35 -0
  46. package/dist/cli/install-codex/git-bash.d.ts +35 -0
  47. package/dist/cli/install-codex/index.d.ts +4 -0
  48. package/dist/cli/install-codex/toml-section-editor.d.ts +2 -0
  49. package/dist/cli/install-codex/types.d.ts +20 -0
  50. package/dist/cli/run/event-state.d.ts +1 -0
  51. package/dist/cli/run/poll-for-completion.d.ts +1 -0
  52. package/dist/cli/run/prompt-start.d.ts +7 -0
  53. package/dist/cli/star-request.d.ts +9 -0
  54. package/dist/config/schema/hooks.d.ts +0 -1
  55. package/dist/create-hooks.d.ts +0 -1
  56. package/dist/features/background-agent/concurrency.d.ts +1 -0
  57. package/dist/features/background-agent/process-cleanup.d.ts +6 -0
  58. package/dist/features/builtin-skills/skills/debugging.d.ts +2 -0
  59. package/dist/features/builtin-skills/skills/index.d.ts +1 -0
  60. package/dist/features/claude-code-session-state/state.d.ts +1 -0
  61. package/dist/features/opencode-skill-loader/index.d.ts +1 -0
  62. package/dist/features/opencode-skill-loader/opencode-config-skills-reader.d.ts +5 -0
  63. package/dist/features/tmux-subagent/attachable-session-status.d.ts +1 -1
  64. package/dist/features/tmux-subagent/session-status-parser.d.ts +1 -0
  65. package/dist/hooks/comment-checker/cli.d.ts +1 -0
  66. package/dist/hooks/index.d.ts +0 -1
  67. package/dist/hooks/tasks-todowrite-disabler/constants.d.ts +1 -1
  68. package/dist/index.js +1077 -563
  69. package/dist/plugin/hooks/create-core-hooks.d.ts +0 -1
  70. package/dist/plugin/hooks/create-session-hooks.d.ts +1 -2
  71. package/dist/plugin/messages-transform.d.ts +8 -1
  72. package/dist/plugin/user-abort-interrupted-recovery-guard.d.ts +6 -0
  73. package/dist/shared/command-executor/execute-hook-command.d.ts +2 -0
  74. package/dist/shared/prompt-async-gate/recent-dispatches.d.ts +14 -0
  75. package/dist/shared/prompt-async-gate/semantic-dedupe.d.ts +7 -0
  76. package/dist/shared/prompt-async-gate/session-idle-dispatch.d.ts +1 -0
  77. package/dist/shared/prompt-async-gate/timing.d.ts +1 -0
  78. package/dist/shared/prompt-async-gate/types.d.ts +2 -0
  79. package/dist/shared/prompt-async-gate.d.ts +1 -1
  80. package/dist/tools/skill/description-formatter.d.ts +5 -1
  81. package/dist/tools/skill/types.d.ts +1 -0
  82. package/package.json +22 -18
  83. package/packages/ast-grep-mcp/dist/cli.js +53 -9
  84. package/packages/git-bash-mcp/dist/cli.js +367 -0
  85. package/packages/lsp-tools-mcp/dist/lsp/process.js +1 -1
  86. package/packages/omo-codex/plugin/.mcp.json +11 -0
  87. package/packages/omo-codex/plugin/components/comment-checker/README.md +1 -1
  88. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +29 -0
  89. package/packages/omo-codex/plugin/components/git-bash/package.json +23 -0
  90. package/packages/omo-codex/plugin/components/git-bash/src/cli.ts +33 -0
  91. package/packages/omo-codex/plugin/components/git-bash/src/codex-hook.ts +180 -0
  92. package/packages/omo-codex/plugin/components/git-bash/src/index.ts +10 -0
  93. package/packages/omo-codex/plugin/components/git-bash/test/codex-hook.test.ts +195 -0
  94. package/packages/omo-codex/plugin/components/git-bash/tsconfig.build.json +13 -0
  95. package/packages/omo-codex/plugin/components/git-bash/tsconfig.json +25 -0
  96. package/packages/omo-codex/plugin/components/lsp/README.md +1 -1
  97. package/packages/omo-codex/plugin/components/lsp/src/cli.ts +5 -5
  98. package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +33 -0
  99. package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +19 -27
  100. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-cli.test.ts +28 -0
  101. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-errors.test.ts +55 -0
  102. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +7 -5
  103. package/packages/omo-codex/plugin/components/rules/README.md +1 -1
  104. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus.md +6 -4
  105. package/packages/omo-codex/plugin/components/rules/bundled-rules/windows-git-bash.md +10 -0
  106. package/packages/omo-codex/plugin/components/rules/src/post-compact-budget.ts +0 -2
  107. package/packages/omo-codex/plugin/components/rules/test/package-smoke.test.ts +3 -1
  108. package/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts +97 -0
  109. package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +6 -5
  110. package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +22 -0
  111. package/packages/omo-codex/plugin/components/ultrawork/CHANGELOG.md +1 -1
  112. package/packages/omo-codex/plugin/components/ultrawork/README.md +3 -3
  113. package/packages/omo-codex/plugin/components/ultrawork/agents/codex-ultrawork-reviewer.toml +4 -1
  114. package/packages/omo-codex/plugin/components/ultrawork/agents/librarian.toml +8 -7
  115. package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +9 -8
  116. package/packages/omo-codex/plugin/components/ultrawork/directive.md +32 -6
  117. package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +27 -4
  118. package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +25 -0
  119. package/packages/omo-codex/plugin/components/ulw-loop/README.md +1 -1
  120. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +28 -205
  121. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +231 -0
  122. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +12 -1
  123. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint.test.ts +19 -1
  124. package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +102 -5
  125. package/packages/omo-codex/plugin/hooks/hooks.json +35 -2
  126. package/packages/omo-codex/plugin/model-catalog.json +49 -0
  127. package/packages/omo-codex/plugin/package-lock.json +19 -0
  128. package/packages/omo-codex/plugin/package.json +3 -1
  129. package/packages/omo-codex/plugin/scripts/auto-update.mjs +159 -0
  130. package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +16 -1
  131. package/packages/omo-codex/plugin/scripts/build-components.mjs +2 -1
  132. package/packages/omo-codex/plugin/scripts/migrate-codex-config.mjs +269 -0
  133. package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +89 -0
  134. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +6 -6
  135. package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +6 -6
  136. package/packages/omo-codex/plugin/skills/lcx-report-bug/SKILL.md +127 -0
  137. package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +9 -0
  138. package/packages/omo-codex/plugin/skills/refactor/SKILL.md +6 -6
  139. package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +6 -6
  140. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +33 -8
  141. package/packages/omo-codex/plugin/skills/start-work/SKILL.md +25 -5
  142. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +28 -205
  143. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +231 -0
  144. package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +17 -17
  145. package/packages/omo-codex/plugin/test/aggregate.test.mjs +188 -20
  146. package/packages/omo-codex/plugin/test/auto-update.test.mjs +129 -0
  147. package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +58 -11
  148. package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +34 -0
  149. package/packages/omo-codex/plugin/test/mcp-research-servers.test.mjs +21 -0
  150. package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +146 -0
  151. package/packages/omo-codex/plugin/test/node-install-surface.test.mjs +48 -0
  152. package/packages/omo-codex/plugin/test/subagent-guidance.test.mjs +76 -0
  153. package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +67 -0
  154. package/packages/omo-codex/plugin/test/sync-skills.test.mjs +54 -2
  155. package/packages/omo-codex/scripts/install/cache.mjs +5 -3
  156. package/packages/omo-codex/scripts/install/cli-args.mjs +112 -0
  157. package/packages/omo-codex/scripts/install/config.mjs +23 -1
  158. package/packages/omo-codex/scripts/install/delegated-command.mjs +25 -0
  159. package/packages/omo-codex/scripts/install/git-bash.mjs +99 -0
  160. package/packages/omo-codex/scripts/install/git-bash.test.mjs +174 -0
  161. package/packages/omo-codex/scripts/install/legacy-bins.mjs +1 -0
  162. package/packages/omo-codex/scripts/install/mcp-runtime-cache.mjs +5 -1
  163. package/packages/omo-codex/scripts/install/model-catalog.mjs +66 -0
  164. package/packages/omo-codex/scripts/install/multi-agent-v2-config.mjs +7 -1
  165. package/packages/omo-codex/scripts/install/permissions.d.mts +1 -0
  166. package/packages/omo-codex/scripts/install/permissions.mjs +26 -0
  167. package/packages/omo-codex/scripts/install/project-local-cleanup.mjs +229 -0
  168. package/packages/omo-codex/scripts/install/reasoning-config.mjs +72 -0
  169. package/packages/omo-codex/scripts/install/source-package-build.mjs +20 -0
  170. package/packages/omo-codex/scripts/install/toml-editor.mjs +19 -2
  171. package/packages/omo-codex/scripts/install-bin-links.test.mjs +23 -0
  172. package/packages/omo-codex/scripts/install-cli-args.test.mjs +146 -0
  173. package/packages/omo-codex/scripts/install-config-autonomous.test.mjs +48 -0
  174. package/packages/omo-codex/scripts/install-config-reasoning.test.mjs +141 -0
  175. package/packages/omo-codex/scripts/install-config.test.mjs +205 -0
  176. package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +157 -0
  177. package/packages/omo-codex/scripts/install-local-git-bash-preflight.test.mjs +145 -0
  178. package/packages/omo-codex/scripts/install-local.mjs +91 -8
  179. package/packages/omo-codex/scripts/install-local.test.mjs +15 -0
  180. package/packages/omo-codex/scripts/install-mcp-runtime.test.mjs +60 -0
  181. package/packages/omo-codex/scripts/install-packaged-local.test.mjs +67 -0
  182. package/packages/omo-codex/scripts/install-project-local-cleanup.test.mjs +277 -0
  183. package/packages/shared-skills/skills/lcx-report-bug/SKILL.md +127 -0
  184. package/packages/shared-skills/skills/lcx-report-bug/agents/openai.yaml +9 -0
  185. package/packages/shared-skills/skills/review-work/SKILL.md +33 -8
  186. package/packages/shared-skills/skills/start-work/SKILL.md +25 -5
  187. package/packages/shared-skills/skills/ulw-plan/SKILL.md +11 -11
  188. package/postinstall.mjs +36 -3
  189. package/dist/hooks/context-window-monitor.d.ts +0 -19
@@ -0,0 +1,195 @@
1
+ import { afterEach, describe, expect, it } from "bun:test";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { Readable, Writable } from "node:stream";
6
+
7
+ import {
8
+ applyGitBashPostCompactReset,
9
+ applyGitBashPreToolUseReminder,
10
+ runGitBashHookCli,
11
+ type PostCompactPayload,
12
+ type PreToolUsePayload,
13
+ } from "../src/codex-hook.js";
14
+
15
+ const temporaryDirectories: string[] = [];
16
+
17
+ function createTemporaryDirectory(prefix: string): string {
18
+ const directory = mkdtempSync(join(tmpdir(), prefix));
19
+ temporaryDirectories.push(directory);
20
+ return directory;
21
+ }
22
+
23
+ afterEach(() => {
24
+ for (const directory of temporaryDirectories.splice(0)) {
25
+ rmSync(directory, { recursive: true, force: true });
26
+ }
27
+ });
28
+
29
+ function preToolPayload(toolName: string, sessionId = "session-1"): PreToolUsePayload {
30
+ return {
31
+ cwd: "/repo",
32
+ hook_event_name: "PreToolUse",
33
+ model: "gpt-5.5",
34
+ permission_mode: "default",
35
+ session_id: sessionId,
36
+ tool_input: { command: "pwd" },
37
+ tool_name: toolName,
38
+ tool_use_id: "call-1",
39
+ transcript_path: null,
40
+ turn_id: "turn-1",
41
+ };
42
+ }
43
+
44
+ function postCompactPayload(sessionId = "session-1"): PostCompactPayload {
45
+ return {
46
+ hook_event_name: "PostCompact",
47
+ session_id: sessionId,
48
+ transcript_path: null,
49
+ trigger: "manual",
50
+ };
51
+ }
52
+
53
+ function windowsEnv(): NodeJS.ProcessEnv {
54
+ return { OS: "Windows_NT", ComSpec: "C:\\Windows\\System32\\cmd.exe" };
55
+ }
56
+
57
+ function captureStdout(): { readonly stdout: Writable; readonly read: () => string } {
58
+ let captured = "";
59
+ const stdout = new Writable({
60
+ write(chunk: unknown, _encoding: BufferEncoding, callback: (error?: Error | null) => void): void {
61
+ captured += chunk instanceof Buffer ? chunk.toString() : String(chunk);
62
+ callback();
63
+ },
64
+ });
65
+ return { stdout, read: () => captured };
66
+ }
67
+
68
+ describe("applyGitBashPreToolUseReminder", () => {
69
+ it("#given first Windows Bash call #when hook runs #then emits non-blocking git_bash guidance", () => {
70
+ // given
71
+ const pluginDataRoot = createTemporaryDirectory("omo-git-bash-hook-");
72
+
73
+ // when
74
+ const output = applyGitBashPreToolUseReminder(preToolPayload("Bash"), {
75
+ env: windowsEnv(),
76
+ platform: "linux",
77
+ pluginDataRoot,
78
+ });
79
+
80
+ // then
81
+ const parsed = JSON.parse(output);
82
+ expect(parsed.hookSpecificOutput).toEqual({
83
+ hookEventName: "PreToolUse",
84
+ additionalContext:
85
+ "On Windows, prefer the OMO git_bash MCP for shell commands before using built-in exec_command. Use exec_command only when git_bash is unavailable or for non-shell operations.",
86
+ });
87
+ });
88
+
89
+ it("#given second Windows Bash call in same session #when hook runs #then it stays silent", () => {
90
+ // given
91
+ const pluginDataRoot = createTemporaryDirectory("omo-git-bash-hook-");
92
+ const payload = preToolPayload("Bash");
93
+
94
+ // when
95
+ const first = applyGitBashPreToolUseReminder(payload, { env: windowsEnv(), platform: "linux", pluginDataRoot });
96
+ const second = applyGitBashPreToolUseReminder(payload, { env: windowsEnv(), platform: "linux", pluginDataRoot });
97
+
98
+ // then
99
+ expect(first).toContain("git_bash");
100
+ expect(second).toBe("");
101
+ });
102
+
103
+ it("#given non-Windows Bash call #when hook runs #then it stays silent", () => {
104
+ // given
105
+ const pluginDataRoot = createTemporaryDirectory("omo-git-bash-hook-");
106
+
107
+ // when
108
+ const output = applyGitBashPreToolUseReminder(preToolPayload("Bash"), {
109
+ env: {},
110
+ platform: "darwin",
111
+ pluginDataRoot,
112
+ });
113
+
114
+ // then
115
+ expect(output).toBe("");
116
+ });
117
+
118
+ it("#given non-Bash tool call #when hook runs #then it stays silent", () => {
119
+ // given
120
+ const pluginDataRoot = createTemporaryDirectory("omo-git-bash-hook-");
121
+
122
+ // when
123
+ const output = applyGitBashPreToolUseReminder(preToolPayload("exec_command"), {
124
+ env: windowsEnv(),
125
+ platform: "linux",
126
+ pluginDataRoot,
127
+ });
128
+
129
+ // then
130
+ expect(output).toBe("");
131
+ });
132
+ });
133
+
134
+ describe("applyGitBashPostCompactReset", () => {
135
+ it("#given reminder already emitted #when PostCompact runs #then next Windows Bash call emits reminder again", () => {
136
+ // given
137
+ const pluginDataRoot = createTemporaryDirectory("omo-git-bash-hook-");
138
+ const payload = preToolPayload("Bash");
139
+ const first = applyGitBashPreToolUseReminder(payload, { env: windowsEnv(), platform: "linux", pluginDataRoot });
140
+ const second = applyGitBashPreToolUseReminder(payload, { env: windowsEnv(), platform: "linux", pluginDataRoot });
141
+
142
+ // when
143
+ applyGitBashPostCompactReset(postCompactPayload(), { pluginDataRoot });
144
+ const afterCompact = applyGitBashPreToolUseReminder(payload, {
145
+ env: windowsEnv(),
146
+ platform: "linux",
147
+ pluginDataRoot,
148
+ });
149
+
150
+ // then
151
+ expect(first).toContain("git_bash");
152
+ expect(second).toBe("");
153
+ expect(afterCompact).toContain("git_bash");
154
+ });
155
+ });
156
+
157
+ describe("runGitBashHookCli", () => {
158
+ it("#given Codex PreToolUse stdin on Windows #when CLI hook runs #then it writes reminder JSON", async () => {
159
+ // given
160
+ const pluginDataRoot = createTemporaryDirectory("omo-git-bash-hook-");
161
+ const stdin = Readable.from([JSON.stringify(preToolPayload("Bash"))]);
162
+ const capture = captureStdout();
163
+
164
+ // when
165
+ await runGitBashHookCli(stdin, capture.stdout, "pre-tool-use", {
166
+ env: windowsEnv(),
167
+ platform: "linux",
168
+ pluginDataRoot,
169
+ });
170
+
171
+ // then
172
+ expect(capture.read()).toContain("git_bash MCP");
173
+ });
174
+
175
+ it("#given PostCompact stdin #when CLI hook runs #then it resets the one-shot reminder", async () => {
176
+ // given
177
+ const pluginDataRoot = createTemporaryDirectory("omo-git-bash-hook-");
178
+ const payload = preToolPayload("Bash");
179
+ applyGitBashPreToolUseReminder(payload, { env: windowsEnv(), platform: "linux", pluginDataRoot });
180
+ const resetStdin = Readable.from([JSON.stringify(postCompactPayload())]);
181
+ const capture = captureStdout();
182
+
183
+ // when
184
+ await runGitBashHookCli(resetStdin, capture.stdout, "post-compact", { pluginDataRoot });
185
+ const afterCompact = applyGitBashPreToolUseReminder(payload, {
186
+ env: windowsEnv(),
187
+ platform: "linux",
188
+ pluginDataRoot,
189
+ });
190
+
191
+ // then
192
+ expect(capture.read()).toBe("");
193
+ expect(afterCompact).toContain("git_bash");
194
+ });
195
+ });
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "allowImportingTsExtensions": false,
5
+ "declaration": true,
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "noEmit": false,
9
+ "types": ["node"]
10
+ },
11
+ "include": ["src/**/*"],
12
+ "exclude": ["test/**/*"]
13
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "lib": ["ES2022"],
7
+ "strict": true,
8
+ "exactOptionalPropertyTypes": true,
9
+ "noUncheckedIndexedAccess": true,
10
+ "noPropertyAccessFromIndexSignature": true,
11
+ "verbatimModuleSyntax": true,
12
+ "noImplicitOverride": true,
13
+ "noImplicitReturns": true,
14
+ "noFallthroughCasesInSwitch": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "esModuleInterop": true,
18
+ "allowImportingTsExtensions": true,
19
+ "skipLibCheck": true,
20
+ "forceConsistentCasingInFileNames": true,
21
+ "types": ["node", "bun-types"],
22
+ "noEmit": true
23
+ },
24
+ "include": ["src/**/*", "test/**/*"]
25
+ }
@@ -118,7 +118,7 @@ printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | node dist/cli.j
118
118
  ## Local Codex Installation
119
119
 
120
120
  ```bash
121
- bunx lazycodex install
121
+ npx lazycodex-ai install
122
122
  ```
123
123
 
124
124
  The installer builds and copies the plugin into `~/.codex/plugins/cache/sisyphuslabs/omo/0.1.0`, registers the `sisyphuslabs` marketplace from the `lazycodex` Git repository, and enables:
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
- import { dirname, resolve } from "node:path";
3
+ import { createRequire } from "node:module";
4
4
  import { argv, execPath, stderr } from "node:process";
5
- import { fileURLToPath } from "node:url";
6
5
 
7
- import { runPostToolUseHookCli } from "./codex-hook.js";
6
+ import { runPostToolUseHookCli } from "./codex-hook-cli.js";
8
7
 
9
- const PACKAGE_LSP_MCP_CLI = "../../../../../lsp-tools-mcp/dist/cli.js";
8
+ const require = createRequire(import.meta.url);
9
+ const PACKAGE_LSP_MCP_CLI = "@code-yeongyu/lsp-tools-mcp/dist/cli.js";
10
10
 
11
11
  async function main(): Promise<void> {
12
12
  const [command = "mcp", subcommand = ""] = argv.slice(2);
@@ -31,7 +31,7 @@ main().catch((error: unknown) => {
31
31
  });
32
32
 
33
33
  async function runPackageLspMcpCli(): Promise<void> {
34
- const cliPath = resolve(dirname(fileURLToPath(import.meta.url)), PACKAGE_LSP_MCP_CLI);
34
+ const cliPath = require.resolve(PACKAGE_LSP_MCP_CLI);
35
35
  const child = spawn(execPath, [cliPath, "mcp"], { stdio: "inherit" });
36
36
  await new Promise<void>((resolve, reject) => {
37
37
  child.once("error", reject);
@@ -0,0 +1,33 @@
1
+ import { stdin as processStdin } from "node:process";
2
+
3
+ import { disposeDefaultLspManager } from "@code-yeongyu/lsp-tools-mcp/dist/lsp/manager.js";
4
+
5
+ import { isRecord, runLspPostToolUseHook } from "./codex-hook.js";
6
+
7
+ export async function runPostToolUseHookCli(stdin: NodeJS.ReadStream = processStdin): Promise<void> {
8
+ try {
9
+ const raw = await readStdin(stdin);
10
+ if (!raw.trim()) return;
11
+ let parsed: unknown;
12
+ try {
13
+ parsed = JSON.parse(raw);
14
+ } catch (error) {
15
+ if (error instanceof SyntaxError) return;
16
+ throw error;
17
+ }
18
+ const input = isRecord(parsed) ? parsed : {};
19
+ const output = await runLspPostToolUseHook(input);
20
+ if (output) process.stdout.write(output);
21
+ } finally {
22
+ await disposeDefaultLspManager();
23
+ }
24
+ }
25
+
26
+ async function readStdin(stdin: NodeJS.ReadStream): Promise<string> {
27
+ stdin.setEncoding("utf8");
28
+ let raw = "";
29
+ for await (const chunk of stdin) {
30
+ raw += chunk;
31
+ }
32
+ return raw;
33
+ }
@@ -1,8 +1,6 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { stdin as processStdin } from "node:process";
3
2
 
4
- import { disposeDefaultLspManager } from "../../../../../lsp-tools-mcp/dist/lsp/manager.js";
5
- import { executeLspDiagnostics } from "../../../../../lsp-tools-mcp/dist/tools.js";
3
+ import { executeLspDiagnostics } from "@code-yeongyu/lsp-tools-mcp/dist/tools.js";
6
4
 
7
5
  export type DiagnosticsRunner = (filePath: string) => Promise<string>;
8
6
 
@@ -91,13 +89,29 @@ async function collectDiagnostics(
91
89
  nextIndex += 1;
92
90
  const filePath = filePaths[index];
93
91
  if (filePath === undefined) return;
94
- results[index] = { filePath, diagnostics: (await runDiagnostics(filePath)).trim() };
92
+ results[index] = { filePath, diagnostics: await collectFileDiagnostics(filePath, runDiagnostics) };
95
93
  }
96
94
  }
97
95
  await Promise.all(Array.from({ length: workerCount }, () => worker()));
98
96
  return results;
99
97
  }
100
98
 
99
+ async function collectFileDiagnostics(filePath: string, runDiagnostics: DiagnosticsRunner): Promise<string> {
100
+ try {
101
+ return (await runDiagnostics(filePath)).trim();
102
+ } catch (error) {
103
+ return formatDiagnosticsError(error);
104
+ }
105
+ }
106
+
107
+ function formatDiagnosticsError(error: unknown): string {
108
+ if (error instanceof Error) {
109
+ const message = error.message.trim();
110
+ if (message.length > 0) return message;
111
+ }
112
+ return String(error).trim();
113
+ }
114
+
101
115
  function formatDiagnosticBlock({ filePath, diagnostics }: DiagnosticBlock): string {
102
116
  return `LSP diagnostics after editing ${filePath}:\n\n${formatDiagnosticsForDisplay(diagnostics)}`;
103
117
  }
@@ -191,19 +205,6 @@ export function extractMutatedFilePaths(input: CodexPostToolUseInput): string[]
191
205
  return [...paths];
192
206
  }
193
207
 
194
- export async function runPostToolUseHookCli(stdin: NodeJS.ReadStream = processStdin): Promise<void> {
195
- try {
196
- const raw = await readStdin(stdin);
197
- if (!raw.trim()) return;
198
- const parsed: unknown = JSON.parse(raw);
199
- const input = isRecord(parsed) ? parsed : {};
200
- const output = await runLspPostToolUseHook(input);
201
- if (output) process.stdout.write(output);
202
- } finally {
203
- await disposeDefaultLspManager();
204
- }
205
- }
206
-
207
208
  function isMutationTool(value: unknown): boolean {
208
209
  if (typeof value !== "string") return false;
209
210
  return MUTATION_TOOL_NAMES.has(value.toLowerCase());
@@ -271,15 +272,6 @@ function addPatchFiles(paths: Set<string>, value: unknown): void {
271
272
  }
272
273
  }
273
274
 
274
- function isRecord(value: unknown): value is Record<string, unknown> {
275
+ export function isRecord(value: unknown): value is Record<string, unknown> {
275
276
  return typeof value === "object" && value !== null && !Array.isArray(value);
276
277
  }
277
-
278
- async function readStdin(stdin: NodeJS.ReadStream): Promise<string> {
279
- stdin.setEncoding("utf8");
280
- let raw = "";
281
- for await (const chunk of stdin) {
282
- raw += chunk;
283
- }
284
- return raw;
285
- }
@@ -0,0 +1,28 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import { describe, expect, it } from "vitest";
6
+
7
+ describe("codex PostToolUse hook CLI", () => {
8
+ it("#given malformed post-tool-use stdin #when hook CLI runs #then it no-ops without stderr", () => {
9
+ // given
10
+ const input = "break;\n";
11
+
12
+ // when
13
+ const result = runBuiltHookCli(input);
14
+
15
+ // then
16
+ expect(result.status).toBe(0);
17
+ expect(result.stderr).toBe("");
18
+ expect(result.stdout).toBe("");
19
+ });
20
+ });
21
+
22
+ function runBuiltHookCli(input: string): ReturnType<typeof spawnSync> {
23
+ const cliPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../dist/cli.js");
24
+ return spawnSync(process.execPath, [cliPath, "hook", "post-tool-use"], {
25
+ input,
26
+ encoding: "utf8",
27
+ });
28
+ }
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { runLspPostToolUseHook } from "../src/codex-hook.js";
4
+
5
+ describe("codex PostToolUse diagnostics errors", () => {
6
+ it("#given diagnostics runner throws for a mutated file #when the hook evaluates diagnostics #then it returns blocked output with the thrown message", async () => {
7
+ // given
8
+ const output = await runLspPostToolUseHook(
9
+ {
10
+ tool_name: "write",
11
+ tool_input: { path: "src/missing.ts" },
12
+ tool_response: { ok: true },
13
+ },
14
+ async (filePath) => {
15
+ expect(filePath).toBe("src/missing.ts");
16
+ throw new Error("ENOENT: no such file or directory, open 'src/missing.ts'");
17
+ },
18
+ );
19
+
20
+ // when
21
+ const parsed: unknown = JSON.parse(output);
22
+ if (!isPostToolUseHookOutput(parsed)) throw new TypeError("Expected PostToolUse hook output");
23
+
24
+ // then
25
+ expect(parsed.reason).toBe(
26
+ "LSP diagnostics after editing src/missing.ts:\n\nENOENT: no such file or directory, open 'src/missing.ts'",
27
+ );
28
+ expect(parsed.hookSpecificOutput.additionalContext).toBe(parsed.reason);
29
+ });
30
+ });
31
+
32
+ interface PostToolUseHookOutput {
33
+ readonly decision: "block";
34
+ readonly reason: string;
35
+ readonly hookSpecificOutput: {
36
+ readonly hookEventName: "PostToolUse";
37
+ readonly additionalContext: string;
38
+ };
39
+ }
40
+
41
+ function isPostToolUseHookOutput(value: unknown): value is PostToolUseHookOutput {
42
+ if (!isRecord(value)) return false;
43
+ const hookSpecificOutput = value["hookSpecificOutput"];
44
+ return (
45
+ value["decision"] === "block" &&
46
+ typeof value["reason"] === "string" &&
47
+ isRecord(hookSpecificOutput) &&
48
+ hookSpecificOutput["hookEventName"] === "PostToolUse" &&
49
+ typeof hookSpecificOutput["additionalContext"] === "string"
50
+ );
51
+ }
52
+
53
+ function isRecord(value: unknown): value is Record<string, unknown> {
54
+ return typeof value === "object" && value !== null && !Array.isArray(value);
55
+ }
@@ -56,6 +56,7 @@ describe("plugin package metadata", () => {
56
56
  const hooksJson = readHooksJson("hooks/hooks.json");
57
57
  const mcpJson = readMcpJson(".mcp.json");
58
58
  const cliSource = readFileSync("src/cli.ts", "utf8");
59
+ const codexHookCliSource = readFileSync("src/codex-hook-cli.ts", "utf8");
59
60
  const codexHookSource = readFileSync("src/codex-hook.ts", "utf8");
60
61
  const sourceFiles = readdirSync("src");
61
62
 
@@ -79,11 +80,12 @@ describe("plugin package metadata", () => {
79
80
  expect(lspServer?.command).toBe("node");
80
81
  expect(lspServer?.args).toEqual(["../../../../lsp-tools-mcp/dist/cli.js", "mcp"]);
81
82
  expect(cliSource).not.toContain("./lazy-lsp-mcp.js");
82
- expect(cliSource).not.toContain("@code-yeongyu/lsp-tools-mcp");
83
- expect(cliSource).toContain("../../../../../lsp-tools-mcp/dist/cli.js");
84
- expect(codexHookSource).not.toContain("@code-yeongyu/lsp-tools-mcp");
85
- expect(codexHookSource).toContain("../../../../../lsp-tools-mcp/dist/lsp/manager.js");
86
- expect(codexHookSource).toContain("../../../../../lsp-tools-mcp/dist/tools.js");
83
+ expect(cliSource).toContain("@code-yeongyu/lsp-tools-mcp/dist/cli.js");
84
+ expect(cliSource).not.toContain("../../../../../lsp-tools-mcp/dist/cli.js");
85
+ expect(codexHookCliSource).toContain("@code-yeongyu/lsp-tools-mcp/dist/lsp/manager.js");
86
+ expect(codexHookSource).toContain("@code-yeongyu/lsp-tools-mcp/dist/tools.js");
87
+ expect(codexHookCliSource).not.toContain("../../../../../lsp-tools-mcp/dist/lsp/manager.js");
88
+ expect(codexHookSource).not.toContain("../../../../../lsp-tools-mcp/dist/tools.js");
87
89
  expect(sourceFiles.filter((name) => name.startsWith("lazy-mcp") || name === "lazy-lsp-mcp.ts")).toEqual([]);
88
90
  });
89
91
 
@@ -43,7 +43,7 @@ Prefer strict TypeScript and keep runtime imports ESM-compatible.
43
43
  ## Install Locally
44
44
 
45
45
  ```bash
46
- bunx lazycodex install
46
+ npx lazycodex-ai install
47
47
  ```
48
48
 
49
49
  The local installer builds the plugin and copies a clean cache entry to:
@@ -79,13 +79,15 @@ omo-codex bundles three read-only Codex subagent roles in `CODEX_HOME/agents/`:
79
79
 
80
80
  **Routing:**
81
81
 
82
- - "Where is X?" / "Find code that does Y" -> `spawn_agent(agent_type="explorer", ...)`
83
- - "How does library Z work?" / "What's the API contract?" -> `spawn_agent(agent_type="librarian", ...)`
84
- - 5+ interdependent steps, ambiguous scope, multi-module work -> `spawn_agent(agent_type="plan", ...)`
85
- - Heavy verification of a finished change -> `spawn_agent(agent_type="codex-ultrawork-reviewer", ...)`
82
+ - "Where is X?" / "Find code that does Y" -> `spawn_agent(agent_type="explorer", fork_turns="none", ...)`
83
+ - "How does library Z work?" / "What's the API contract?" -> `spawn_agent(agent_type="librarian", fork_turns="none", ...)`
84
+ - 5+ interdependent steps, ambiguous scope, multi-module work -> `spawn_agent(agent_type="plan", fork_turns="none", ...)`
85
+ - Heavy verification of a finished change -> `spawn_agent(agent_type="codex-ultrawork-reviewer", fork_turns="none", ...)`
86
86
 
87
87
  **Don't duplicate.** Once a subagent is dispatched for a question, do not re-do the same search yourself. Once results return, do not re-verify by repeating their tool calls; integrate and move on.
88
88
 
89
+ **Keep parent liveness visible.** While any child is active, keep the parent visibly alive with brief status updates that include active subagent count, agent names, last heartbeat, and whether the parent is waiting for mailbox updates. Do this during long `wait_agent` cycles so the session does not look idle while children are still running.
90
+
89
91
  # Operating Loop
90
92
 
91
93
  **Explore -> Plan -> Implement -> Verify -> Manually QA.** Loops are short and tight; do not loop back with a draft when the work is yours to do.
@@ -0,0 +1,10 @@
1
+ ---
2
+ description: Windows Git Bash guidance for Codex
3
+ alwaysApply: true
4
+ ---
5
+
6
+ On Windows native Codex sessions, prefer Git Bash for shell commands.
7
+
8
+ Use `shell: "bash"` when `bash.exe` is on PATH. Otherwise use the absolute Git Bash path from `OMO_CODEX_GIT_BASH_PATH` or `C:\Program Files\Git\bin\bash.exe`.
9
+
10
+ Use PowerShell only for Windows-native operations that need PowerShell.
@@ -24,8 +24,6 @@ const MODEL_CONTEXT_BUDGETS: readonly ModelContextBudget[] = [
24
24
  { slug: "gpt-5.5", contextWindowTokens: 272_000, effectivePercent: DEFAULT_EFFECTIVE_CONTEXT_WINDOW_PERCENT },
25
25
  { slug: "gpt-5.4", contextWindowTokens: 272_000, effectivePercent: DEFAULT_EFFECTIVE_CONTEXT_WINDOW_PERCENT },
26
26
  { slug: "gpt-5.4-mini", contextWindowTokens: 272_000, effectivePercent: DEFAULT_EFFECTIVE_CONTEXT_WINDOW_PERCENT },
27
- { slug: "gpt-5.3-codex", contextWindowTokens: 272_000, effectivePercent: DEFAULT_EFFECTIVE_CONTEXT_WINDOW_PERCENT },
28
- { slug: "gpt-5.2", contextWindowTokens: 272_000, effectivePercent: DEFAULT_EFFECTIVE_CONTEXT_WINDOW_PERCENT },
29
27
  {
30
28
  slug: "codex-auto-review",
31
29
  contextWindowTokens: 272_000,
@@ -1,4 +1,4 @@
1
- import { readFileSync } from "node:fs";
1
+ import { readdirSync, readFileSync } from "node:fs";
2
2
  import { describe, expect, it } from "vitest";
3
3
 
4
4
  type PackageJson = {
@@ -51,6 +51,7 @@ describe("plugin package metadata", () => {
51
51
  const pluginJson = readPluginJson(".codex-plugin/plugin.json");
52
52
  const hooksJson = readHooksJson("hooks/hooks.json");
53
53
  const cliSource = readFileSync("src/cli.ts", "utf8");
54
+ const bundledRules = readdirSync("bundled-rules").sort();
54
55
 
55
56
  // when
56
57
  const hookConfig = hooksJson.hooks;
@@ -69,6 +70,7 @@ describe("plugin package metadata", () => {
69
70
  expect(packageJson.dependencies ?? {}).toEqual({ picomatch: "^4.0.3" });
70
71
  expect(packageJson.bin["omo-rules"]).toBe("./dist/cli.js");
71
72
  expect(packageJson.files).toContain("bundled-rules");
73
+ expect(bundledRules).toContain("windows-git-bash.md");
72
74
  expect(pluginJson.hooks).toBe("./hooks/hooks.json");
73
75
  expect(cliSource.startsWith("#!/usr/bin/env node")).toBe(true);
74
76
  expect(commands).toEqual([
@@ -0,0 +1,97 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+
6
+ import { runSessionStartHook, type CodexSessionStartInput } from "../src/codex-hook.js";
7
+ import { findPluginBundledCandidates } from "../src/rules/finder.js";
8
+
9
+ const WINDOWS_RULE_DESCRIPTION = "Windows Git Bash guidance for Codex";
10
+ const WINDOWS_RULE_PATH = "bundled-rules/windows-git-bash.md";
11
+ const WINDOWS_GUIDANCE = "On Windows native Codex sessions, prefer Git Bash for shell commands.";
12
+ const BUNDLED_ONLY_ENV = {
13
+ CODEX_RULES_ENABLED_SOURCES: "plugin-bundled",
14
+ };
15
+ const PROJECT_AND_BUNDLED_ENV = {
16
+ CODEX_RULES_ENABLED_SOURCES: ".omo/rules,plugin-bundled",
17
+ };
18
+ const tempDirectories: string[] = [];
19
+ let originalPluginRoot: string | undefined;
20
+
21
+ afterEach(() => {
22
+ restoreEnv("PLUGIN_ROOT", originalPluginRoot);
23
+ for (const directory of tempDirectories.splice(0)) {
24
+ rmSync(directory, { recursive: true, force: true });
25
+ }
26
+ });
27
+
28
+ function makeProject(): { readonly root: string; readonly pluginData: string } {
29
+ originalPluginRoot = process.env["PLUGIN_ROOT"];
30
+ process.env["PLUGIN_ROOT"] = process.cwd();
31
+ const root = mkdtempSync(join(tmpdir(), "codex-rules-windows-git-bash-project-"));
32
+ const pluginData = mkdtempSync(join(tmpdir(), "codex-rules-windows-git-bash-data-"));
33
+ tempDirectories.push(root, pluginData);
34
+ writeFileSync(join(root, "package.json"), JSON.stringify({ name: "fixture" }));
35
+ return { root, pluginData };
36
+ }
37
+
38
+ function sessionStartInput(root: string): CodexSessionStartInput {
39
+ return {
40
+ session_id: "session-1",
41
+ transcript_path: null,
42
+ cwd: root,
43
+ hook_event_name: "SessionStart",
44
+ model: "gpt-5.5",
45
+ permission_mode: "default",
46
+ source: "startup",
47
+ };
48
+ }
49
+
50
+ function restoreEnv(name: string, value: string | undefined): void {
51
+ if (value === undefined) {
52
+ delete process.env[name];
53
+ return;
54
+ }
55
+ process.env[name] = value;
56
+ }
57
+
58
+ function occurrenceCount(value: string, search: string): number {
59
+ return value.split(search).length - 1;
60
+ }
61
+
62
+ describe("Windows Git Bash bundled rule", () => {
63
+ it("#given packaged bundled rules #when discovering plugin-bundled candidates #then Windows Git Bash rule is included", () => {
64
+ const candidates = findPluginBundledCandidates({ pluginRoot: process.cwd() });
65
+
66
+ expect(candidates.map((candidate) => candidate.relativePath)).toContain(WINDOWS_RULE_PATH);
67
+ });
68
+
69
+ it("#given bundled rules enabled #when SessionStart runs #then Windows Git Bash guidance is injected once", async () => {
70
+ const { root, pluginData } = makeProject();
71
+
72
+ const output = await runSessionStartHook(sessionStartInput(root), {
73
+ pluginDataRoot: pluginData,
74
+ env: BUNDLED_ONLY_ENV,
75
+ });
76
+
77
+ expect(occurrenceCount(output, WINDOWS_GUIDANCE)).toBe(1);
78
+ });
79
+
80
+ it("#given project rule with same description #when static rules load #then project guidance overrides bundled guidance", async () => {
81
+ const { root, pluginData } = makeProject();
82
+ const projectGuidance = "Project-specific Windows shell policy.";
83
+ mkdirSync(join(root, ".omo", "rules"), { recursive: true });
84
+ writeFileSync(
85
+ join(root, ".omo", "rules", "windows-git-bash.md"),
86
+ ["---", `description: ${WINDOWS_RULE_DESCRIPTION}`, "alwaysApply: true", "---", "", projectGuidance].join("\n"),
87
+ );
88
+
89
+ const output = await runSessionStartHook(sessionStartInput(root), {
90
+ pluginDataRoot: pluginData,
91
+ env: PROJECT_AND_BUNDLED_ENV,
92
+ });
93
+
94
+ expect(output).toContain(projectGuidance);
95
+ expect(output).not.toContain(WINDOWS_GUIDANCE);
96
+ });
97
+ });