oh-my-opencode 4.11.0 → 4.11.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 (61) hide show
  1. package/dist/agents/atlas/agent.d.ts +3 -2
  2. package/dist/agents/sisyphus/glm-5-2.d.ts +4 -0
  3. package/dist/agents/sisyphus/index.d.ts +1 -0
  4. package/dist/agents/sisyphus-agent-config.d.ts +1 -0
  5. package/dist/agents/sisyphus-junior/agent.d.ts +1 -1
  6. package/dist/agents/sisyphus-junior/glm-5-2.d.ts +1 -0
  7. package/dist/agents/sisyphus-junior/index.d.ts +1 -0
  8. package/dist/cli/index.js +25 -26
  9. package/dist/cli-node/index.js +25 -26
  10. package/dist/hooks/keyword-detector/ultrawork/glm.d.ts +2 -0
  11. package/dist/hooks/keyword-detector/ultrawork/index.d.ts +4 -2
  12. package/dist/hooks/keyword-detector/ultrawork/source-detector.d.ts +5 -4
  13. package/dist/index.js +953 -31
  14. package/dist/skills/init-deep/SKILL.md +21 -26
  15. package/dist/skills/programming/SKILL.md +25 -121
  16. package/dist/skills/programming/references/code-smells.md +390 -0
  17. package/package.json +13 -13
  18. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  19. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +26 -12
  20. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  21. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  22. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +8 -4
  23. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  24. package/packages/omo-codex/plugin/components/codegraph/src/hook.ts +8 -7
  25. package/packages/omo-codex/plugin/components/codegraph/test/hook.test.ts +11 -10
  26. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  27. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  28. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  29. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  30. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  31. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  32. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  33. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  34. package/packages/omo-codex/plugin/components/rules/dist/cli.js +2 -2
  35. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  36. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  37. package/packages/omo-codex/plugin/components/rules/src/sparkshell-awareness.ts +2 -2
  38. package/packages/omo-codex/plugin/components/rules/test/sparkshell-awareness.test.ts +3 -0
  39. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  40. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
  41. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  42. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  43. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  44. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  45. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +2 -2
  46. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  47. package/packages/omo-codex/plugin/hooks/hooks.json +19 -19
  48. package/packages/omo-codex/plugin/package-lock.json +12 -12
  49. package/packages/omo-codex/plugin/package.json +1 -1
  50. package/packages/omo-codex/plugin/scripts/auto-update.mjs +1 -1
  51. package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +21 -26
  52. package/packages/omo-codex/plugin/skills/programming/SKILL.md +25 -121
  53. package/packages/omo-codex/plugin/skills/programming/references/code-smells.md +390 -0
  54. package/packages/omo-codex/plugin/test/auto-update.test.mjs +1 -1
  55. package/packages/omo-codex/plugin/test/bootstrap-setup.test.mjs +31 -0
  56. package/packages/omo-codex/scripts/install-dist/install-local.mjs +30 -13
  57. package/packages/omo-codex/scripts/install-git-bash-mcp-env.test.mjs +24 -0
  58. package/packages/omo-codex/scripts/install-mcp-runtime.test.mjs +36 -0
  59. package/packages/shared-skills/skills/init-deep/SKILL.md +21 -26
  60. package/packages/shared-skills/skills/programming/SKILL.md +25 -121
  61. package/packages/shared-skills/skills/programming/references/code-smells.md +390 -0
@@ -0,0 +1,390 @@
1
+ # Code Smells — Full Reference
2
+
3
+ When any of these smells is detected, **STOP and re-examine your design.** A code smell is not a syntax error — it is a signal that the current structure deserves a second look. The correct response is to assess whether `/refactor` is warranted, fix the smell, or document a SPECIFIC justification for carrying it. "It's fine" is not a justification.
4
+
5
+ ---
6
+
7
+ ## Smell 1 — File exceeds 250 pure LOC
8
+
9
+ ### Why 250
10
+
11
+ At 250 pure LOC a file still fits in one screen on a 32-inch monitor with a 14pt font. A reviewer can hold the whole thing in working memory and spot a cross-cutting bug. At 500 LOC they cannot. At 1000 LOC they stop trying. The number is the cognitive ceiling of a single human reviewer who has not memorized the file.
12
+
13
+ A file past this line is telling you:
14
+
15
+ - The module is doing more than one thing.
16
+ - Multiple cohesive units got merged "to save a file."
17
+ - Re-exports, barrels, and orchestrators got fused into pure-logic units.
18
+ - Every future reader pays a tax to find what they need.
19
+
20
+ ### Measuring pure LOC
21
+
22
+ ```bash
23
+ # Quick (line-comment + blank exclusion):
24
+ awk '!/^[[:space:]]*$/ && !/^[[:space:]]*(\/\/|#|--)/' <file> | wc -l
25
+
26
+ # Authoritative (handles block comments correctly):
27
+ cloc --by-file <file> # the "code" column is the number
28
+ ```
29
+
30
+ ### Required behavior when detected
31
+
32
+ **Creating a file that will exceed 250 pure LOC.** Split it before the first commit. Carve by responsibility, one cohesive unit per file. Use a barrel (`__init__.py`, `mod.rs`, `index.ts`) for re-exports ONLY — never for logic.
33
+
34
+ **Editing a file that already exceeds 250 pure LOC and your edit adds lines.** Refactor the unit you are touching into its own file BEFORE adding the new lines. The split is part of THIS task, not a follow-up someone will never do.
35
+
36
+ **Reading a file that exceeds 250 pure LOC while implementing a feature.** Surface the smell in your reply, propose a concrete split, and ask the user whether to split now or carry the smell.
37
+
38
+ ### Forbidden escapes
39
+
40
+ - Counting comments and blank lines toward the budget. **Pure LOC means code lines.**
41
+ - Splitting by token count (`foo_1.py`, `module_part_A.rs`, `service-2.ts`). Split by what each file DOES.
42
+ - Catch-all dump files: `utils.py`, `helpers.ts`, `lib.rs` (as a logic dump), `common.py`, `shared.ts`.
43
+ - "It's generated, so it's fine." Only true if the file lives in `dist/`, `target/`, `__generated__/`.
44
+ - "It's a test file with many cases." Split by SUT or by behavior cluster.
45
+ - "230 pure LOC, close enough." A 230-LOC file about to grow is already at the limit. Split now.
46
+
47
+ ### Acceptable exceptions (rare, require justification)
48
+
49
+ A file may legitimately exceed 250 pure LOC if **and only if** it is:
50
+
51
+ - A truly indivisible single-responsibility unit (e.g., a generated parser table, a state machine whose states share a single closure). Mark with `// allow: SIZE_OK — <reason>`.
52
+ - A pure data table (translation strings, error code lookup, brand color palette).
53
+
54
+ `// allow: SIZE_OK` without a justifying comment is itself slop.
55
+
56
+ ### Concrete split examples
57
+
58
+ #### Python — BEFORE (`user_service.py`, 412 pure LOC)
59
+
60
+ ```python
61
+ # user_service.py — DOES TOO MUCH
62
+ class UserRepository: ... # 90 LOC of SQLAlchemy
63
+ class UserValidator: ... # 60 LOC of Pydantic + business rules
64
+ class PasswordHasher: ... # 40 LOC of bcrypt wrapper
65
+ class EmailSender: ... # 50 LOC of httpx2 client
66
+ class UserService: ... # 130 LOC orchestrating the four above
67
+ def _build_query(...): ... # 25 LOC helper
68
+ def _format_email(...): ... # 17 LOC helper
69
+ ```
70
+
71
+ #### Python — AFTER (split by responsibility)
72
+
73
+ ```
74
+ src/myapp/users/
75
+ ├── __init__.py # barrel: re-exports UserService only (5 LOC)
76
+ ├── repository.py # UserRepository (~95 LOC)
77
+ ├── validator.py # UserValidator (~65 LOC)
78
+ ├── password.py # PasswordHasher (~45 LOC)
79
+ ├── notifier.py # EmailSender (renamed — the role, not the verb)
80
+ ├── service.py # UserService (orchestrator) (~135 LOC)
81
+ └── _queries.py # _build_query (private) (~30 LOC)
82
+ ```
83
+
84
+ #### Rust — BEFORE (`auth.rs`, 380 pure LOC)
85
+
86
+ ```rust
87
+ // auth.rs — DOES TOO MUCH
88
+ pub struct Session { ... } // 40 LOC
89
+ impl Session { ... } // 90 LOC of methods
90
+ pub struct TokenIssuer { ... } // 30 LOC
91
+ impl TokenIssuer { ... } // 70 LOC
92
+ pub struct RateLimiter { ... } // 50 LOC
93
+ impl RateLimiter { ... } // 70 LOC
94
+ fn parse_authorization_header(...) { ... } // 30 LOC
95
+ ```
96
+
97
+ #### Rust — AFTER
98
+
99
+ ```
100
+ src/auth/
101
+ ├── mod.rs # re-exports Session, TokenIssuer, RateLimiter (8 LOC)
102
+ ├── session.rs # Session + impl (~130 LOC)
103
+ ├── token.rs # TokenIssuer + impl (~100 LOC)
104
+ ├── rate_limit.rs # RateLimiter + impl (~120 LOC)
105
+ └── header.rs # parse_authorization_header (~35 LOC)
106
+ ```
107
+
108
+ #### TypeScript — BEFORE (`api/orders.ts`, 510 pure LOC)
109
+
110
+ ```typescript
111
+ // api/orders.ts — DOES TOO MUCH
112
+ export const OrderSchema = z.object({ ... }) // 30 LOC
113
+ type Order = z.infer<typeof OrderSchema>
114
+ export class OrderRepository { ... } // 110 LOC
115
+ export class PricingEngine { ... } // 130 LOC
116
+ export class TaxCalculator { ... } // 90 LOC
117
+ export class OrderService { ... } // 150 LOC
118
+ ```
119
+
120
+ #### TypeScript — AFTER
121
+
122
+ ```
123
+ src/orders/
124
+ ├── index.ts # barrel (6 LOC)
125
+ ├── schema.ts # OrderSchema + Order type (~35 LOC)
126
+ ├── repository.ts # OrderRepository (~115 LOC)
127
+ ├── pricing.ts # PricingEngine (~135 LOC)
128
+ ├── tax.ts # TaxCalculator (~95 LOC)
129
+ └── service.ts # OrderService (orchestrator) (~155 LOC)
130
+ ```
131
+
132
+ ---
133
+
134
+ ## Smell 2 — Function with more than 3 parameters
135
+
136
+ ### Why 3
137
+
138
+ A function's parameters are its contract with every caller. More than 3 independent inputs overwhelm the caller's working memory and signal one of two design problems:
139
+
140
+ 1. **The function does too much.** It should be two functions.
141
+ 2. **Related parameters belong together.** They should be a typed struct/object — a domain concept, not a parameter bag.
142
+
143
+ ### Workaround detection — THESE COUNT AS THE SAME SMELL
144
+
145
+ Disguising parameter count does not fix the design. The following patterns are the same smell wearing a different hat:
146
+
147
+ **Dict/map smuggling:**
148
+ ```python
149
+ # SMELL — hiding 6 args in a dict
150
+ def create_order(params: dict[str, Any]) -> Order: ...
151
+ ```
152
+ ```typescript
153
+ // SMELL — untyped options bag
154
+ function createOrder(opts: Record<string, unknown>): Order { ... }
155
+ ```
156
+ ```go
157
+ // SMELL — map instead of typed params
158
+ func CreateOrder(params map[string]any) (*Order, error) { ... }
159
+ ```
160
+
161
+ **Variadic/kwargs catch-all:**
162
+ ```python
163
+ # SMELL — hiding real params behind kwargs
164
+ def send_notification(recipient: str, **kwargs) -> None: ...
165
+ ```
166
+ ```typescript
167
+ // SMELL — rest params to avoid naming args
168
+ function sendNotification(recipient: string, ...args: unknown[]): void { ... }
169
+ ```
170
+
171
+ **Config object that wraps positional args:**
172
+ ```python
173
+ # SMELL — "options" object that exists only to bundle what would be positional args
174
+ @dataclass
175
+ class CreateUserOptions:
176
+ name: str
177
+ email: str
178
+ password: str
179
+ role: str
180
+ department: str
181
+ manager_id: int
182
+ # 6 fields, used by exactly one function, no defaults
183
+
184
+ def create_user(opts: CreateUserOptions) -> User: ...
185
+ ```
186
+
187
+ **When the options object is NOT a smell:** when it represents a genuine domain concept reused across multiple call sites with sensible defaults for most fields (e.g., `HttpClientConfig`, `DatabaseConnectionOptions`, `RetryPolicy`).
188
+
189
+ ### The fix
190
+
191
+ Group related parameters into typed value objects with domain names:
192
+
193
+ ```python
194
+ # CLEAN — grouped by domain concept
195
+ @dataclass(frozen=True)
196
+ class UserIdentity:
197
+ name: str
198
+ email: str
199
+
200
+ @dataclass(frozen=True)
201
+ class OrgPlacement:
202
+ role: str
203
+ department: str
204
+ manager_id: int
205
+
206
+ def create_user(identity: UserIdentity, placement: OrgPlacement, password: str) -> User: ...
207
+ # 3 params, each a meaningful concept
208
+ ```
209
+
210
+ ```typescript
211
+ // CLEAN — typed grouping
212
+ interface ShippingDetails {
213
+ readonly address: string;
214
+ readonly city: string;
215
+ readonly zip: string;
216
+ readonly country: string;
217
+ }
218
+
219
+ function createOrder(customer: CustomerId, items: readonly LineItem[], shipping: ShippingDetails): Order { ... }
220
+ // 3 params, shipping is a reusable domain type
221
+ ```
222
+
223
+ ```go
224
+ // CLEAN — struct with domain meaning
225
+ type Placement struct {
226
+ Role string
227
+ Department string
228
+ ManagerID UserID
229
+ }
230
+
231
+ func CreateUser(identity UserIdentity, placement Placement, password string) (*User, error) { ... }
232
+ ```
233
+
234
+ If 4+ truly independent inputs are required, justify it — the justification must name WHY these inputs cannot be grouped, not just "the function needs them all."
235
+
236
+ ---
237
+
238
+ ## Smell 3 — Redundant verification after a destructive action
239
+
240
+ ### Why this is slop
241
+
242
+ The contract of a destructive operation (delete, remove, clear, drop) IS the verification. If the operation returns without error, the thing is gone. Re-querying to "confirm" is:
243
+
244
+ 1. **Dead code.** The check can never fail unless the operation itself is broken — in which case fix the operation, not the caller.
245
+ 2. **Misleading.** It teaches the next reader (human or AI) that the operation is unreliable.
246
+ 3. **Performance waste.** An unnecessary round-trip to the database, filesystem, or data structure.
247
+
248
+ This pattern is the hallmark of AI-generated defensive bloat. LLMs produce it because they optimize for "looking thorough" over "being correct." **Recognize it. Delete it.**
249
+
250
+ ### Examples
251
+
252
+ ```python
253
+ # SLOP — delete then verify deletion
254
+ db.delete(user)
255
+ db.commit()
256
+ remaining = db.query(User).filter_by(id=user.id).first()
257
+ assert remaining is None # the ORM already guaranteed this
258
+
259
+ # CLEAN
260
+ db.delete(user)
261
+ db.commit()
262
+ ```
263
+
264
+ ```typescript
265
+ // SLOP — remove from array then check it's gone
266
+ items = items.filter(i => i.id !== targetId);
267
+ if (items.find(i => i.id === targetId)) {
268
+ throw new Error("removal failed"); // impossible by construction
269
+ }
270
+
271
+ // CLEAN
272
+ items = items.filter(i => i.id !== targetId);
273
+ ```
274
+
275
+ ```go
276
+ // SLOP — delete row then SELECT to confirm
277
+ _, err := db.ExecContext(ctx, "DELETE FROM users WHERE id = $1", id)
278
+ if err != nil { return err }
279
+ row := db.QueryRowContext(ctx, "SELECT id FROM users WHERE id = $1", id)
280
+ if err := row.Scan(&check); err != sql.ErrNoRows {
281
+ return fmt.Errorf("delete verification failed")
282
+ }
283
+
284
+ // CLEAN
285
+ _, err := db.ExecContext(ctx, "DELETE FROM users WHERE id = $1", id)
286
+ if err != nil { return err }
287
+ ```
288
+
289
+ ```rust
290
+ // SLOP — remove from HashMap then check absence
291
+ map.remove(&key);
292
+ if map.contains_key(&key) {
293
+ panic!("removal failed"); // HashMap::remove is not broken
294
+ }
295
+
296
+ // CLEAN
297
+ map.remove(&key);
298
+ ```
299
+
300
+ ### Broader pattern — same smell, different disguise
301
+
302
+ Any of these are the same defect:
303
+
304
+ - Calling a **setter** then immediately calling the **getter** to "confirm" the value changed.
305
+ - **Writing** a file then **reading** it back to "verify" the write.
306
+ - **Inserting** a row then **SELECT-ing** it to "confirm" the insert.
307
+ - **Pushing** to an array then checking `.length` increased by 1.
308
+ - **Assigning** a variable then asserting the variable equals the assigned value.
309
+
310
+ **The contract of the operation IS the verification.** If you cannot trust the operation's return, the defect is in the operation — fix it there, not at the call site.
311
+
312
+ ---
313
+
314
+ ## Smell 4 — Negative-form names and conditions
315
+
316
+ ### Why positive form wins
317
+
318
+ Every negation forces the reader to mentally invert. One negation is tolerable. Two (`if !isNotReady`) is a logic puzzle. Codebases that default to negative naming accumulate double and triple negations that nobody can review confidently.
319
+
320
+ Positive form reads in the direction of intent: "is this ready?" rather than "is this not-not-ready?"
321
+
322
+ ### Naming
323
+
324
+ | Negative (SMELL) | Positive (CLEAN) |
325
+ |---|---|
326
+ | `isNotValid` | `isValid` (invert branch) |
327
+ | `isDisabled` | `isEnabled` |
328
+ | `noErrors` | `isClean` / `errorsResolved` |
329
+ | `notFound` | `found` (invert branch) |
330
+ | `isNotEmpty` | `hasItems` / `isPopulated` |
331
+ | `missingAuth` | `hasAuth` / `isAuthenticated` |
332
+ | `cannotProceed` | `canProceed` (invert branch) |
333
+
334
+ Name the **presence** of the quality you care about, not the absence of its opposite.
335
+
336
+ ### Conditions
337
+
338
+ ```python
339
+ # SMELL — double negative
340
+ if not is_invalid(token):
341
+ proceed()
342
+
343
+ # CLEAN — single positive check
344
+ if is_valid(token):
345
+ proceed()
346
+ ```
347
+
348
+ ```typescript
349
+ // SMELL — negated boolean in branch
350
+ if (!user.isNotVerified) {
351
+ grantAccess();
352
+ }
353
+
354
+ // CLEAN — positive name, direct check
355
+ if (user.isVerified) {
356
+ grantAccess();
357
+ }
358
+ ```
359
+
360
+ ```go
361
+ // SMELL — inverted negative
362
+ if !config.DisableLogging {
363
+ log.Info("starting")
364
+ }
365
+
366
+ // CLEAN — positive flag
367
+ if config.LoggingEnabled {
368
+ log.Info("starting")
369
+ }
370
+ ```
371
+
372
+ ```rust
373
+ // SMELL — negated negative
374
+ if !skip_validation {
375
+ validate(&input)?;
376
+ }
377
+
378
+ // CLEAN — positive gate
379
+ if should_validate {
380
+ validate(&input)?;
381
+ }
382
+ ```
383
+
384
+ ### When negation IS appropriate
385
+
386
+ - **Early returns / guard clauses:** `if !authorized { return Err(...) }` — the negative form IS the intent (reject the bad case).
387
+ - **Filtering out:** `items.filter(|x| !x.is_expired())` — the negation describes the keep/discard decision directly.
388
+ - **Error state names:** `Error`, `Failed`, `Timeout` are negative concepts by nature — do not force them into positive wrappers like `isSuccessAbsent`.
389
+
390
+ The rule is not "never use negation." The rule is: **when you have a choice between naming the presence and naming the absence, name the presence.** The branch logic follows from the name, not the other way around.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-opencode",
3
- "version": "4.11.0",
3
+ "version": "4.11.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",
@@ -170,18 +170,18 @@
170
170
  "typescript": "^6.0.3"
171
171
  },
172
172
  "optionalDependencies": {
173
- "oh-my-opencode-darwin-arm64": "4.11.0",
174
- "oh-my-opencode-darwin-x64": "4.11.0",
175
- "oh-my-opencode-darwin-x64-baseline": "4.11.0",
176
- "oh-my-opencode-linux-arm64": "4.11.0",
177
- "oh-my-opencode-linux-arm64-musl": "4.11.0",
178
- "oh-my-opencode-linux-x64": "4.11.0",
179
- "oh-my-opencode-linux-x64-baseline": "4.11.0",
180
- "oh-my-opencode-linux-x64-musl": "4.11.0",
181
- "oh-my-opencode-linux-x64-musl-baseline": "4.11.0",
182
- "oh-my-opencode-windows-arm64": "4.11.0",
183
- "oh-my-opencode-windows-x64": "4.11.0",
184
- "oh-my-opencode-windows-x64-baseline": "4.11.0"
173
+ "oh-my-opencode-darwin-arm64": "4.11.1",
174
+ "oh-my-opencode-darwin-x64": "4.11.1",
175
+ "oh-my-opencode-darwin-x64-baseline": "4.11.1",
176
+ "oh-my-opencode-linux-arm64": "4.11.1",
177
+ "oh-my-opencode-linux-arm64-musl": "4.11.1",
178
+ "oh-my-opencode-linux-x64": "4.11.1",
179
+ "oh-my-opencode-linux-x64-baseline": "4.11.1",
180
+ "oh-my-opencode-linux-x64-musl": "4.11.1",
181
+ "oh-my-opencode-linux-x64-musl-baseline": "4.11.1",
182
+ "oh-my-opencode-windows-arm64": "4.11.1",
183
+ "oh-my-opencode-windows-x64": "4.11.1",
184
+ "oh-my-opencode-windows-x64-baseline": "4.11.1"
185
185
  },
186
186
  "overrides": {
187
187
  "hono": "^4.12.18",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omo",
3
- "version": "4.11.0",
3
+ "version": "4.11.1",
4
4
  "description": "One Codex plugin namespace for Yeongyu's local Codex components.",
5
5
  "author": {
6
6
  "name": "Yeongyu Kim",
@@ -2144,30 +2144,44 @@ async function exists3(path) {
2144
2144
  import { readFile as readFile10, writeFile as writeFile6 } from "node:fs/promises";
2145
2145
  import { join as join15 } from "node:path";
2146
2146
  var GIT_BASH_ENV_KEY = "OMO_CODEX_GIT_BASH_PATH";
2147
+ var CODEGRAPH_RELATIVE_ARGS = new Set(["components/codegraph/dist/serve.js", "./components/codegraph/dist/serve.js"]);
2147
2148
  async function stampGitBashMcpEnv(input) {
2148
- if (input.platform !== "win32")
2149
- return false;
2150
- const rawOverride = input.env?.[GIT_BASH_ENV_KEY];
2151
- const override = typeof rawOverride === "string" ? rawOverride.trim() : "";
2152
- if (override === "")
2153
- return false;
2154
2149
  const manifestPath = join15(input.pluginRoot, ".mcp.json");
2155
2150
  if (!await fileExistsStrict(manifestPath))
2156
2151
  return false;
2157
2152
  const parsed = JSON.parse(await readFile10(manifestPath, "utf8"));
2158
2153
  if (!isPlainRecord(parsed) || !isPlainRecord(parsed["mcpServers"]))
2159
2154
  return false;
2160
- const gitBashServer = parsed["mcpServers"]["git_bash"];
2161
- if (!isPlainRecord(gitBashServer))
2162
- return false;
2163
- const serverEnv = isPlainRecord(gitBashServer["env"]) ? gitBashServer["env"] : {};
2164
- if (serverEnv[GIT_BASH_ENV_KEY] === override)
2155
+ let changed = stampCodegraphMcpPath(parsed["mcpServers"], input.pluginRoot);
2156
+ if (input.platform === "win32") {
2157
+ const rawOverride = input.env?.[GIT_BASH_ENV_KEY];
2158
+ const override = typeof rawOverride === "string" ? rawOverride.trim() : "";
2159
+ const gitBashServer = parsed["mcpServers"]["git_bash"];
2160
+ if (override !== "" && isPlainRecord(gitBashServer)) {
2161
+ const serverEnv = isPlainRecord(gitBashServer["env"]) ? gitBashServer["env"] : {};
2162
+ if (serverEnv[GIT_BASH_ENV_KEY] !== override) {
2163
+ gitBashServer["env"] = { ...serverEnv, [GIT_BASH_ENV_KEY]: override };
2164
+ changed = true;
2165
+ }
2166
+ }
2167
+ }
2168
+ if (!changed)
2165
2169
  return false;
2166
- gitBashServer["env"] = { ...serverEnv, [GIT_BASH_ENV_KEY]: override };
2167
2170
  await writeFile6(manifestPath, `${JSON.stringify(parsed, null, "\t")}
2168
2171
  `);
2169
2172
  return true;
2170
2173
  }
2174
+ function stampCodegraphMcpPath(mcpServers, pluginRoot) {
2175
+ const codegraphServer = mcpServers["codegraph"];
2176
+ if (!isPlainRecord(codegraphServer) || !Array.isArray(codegraphServer["args"]))
2177
+ return false;
2178
+ const args = codegraphServer["args"];
2179
+ const entrypoint = args[0];
2180
+ if (typeof entrypoint !== "string" || !CODEGRAPH_RELATIVE_ARGS.has(entrypoint))
2181
+ return false;
2182
+ codegraphServer["args"] = [join15(pluginRoot, "components", "codegraph", "dist", "serve.js"), ...args.slice(1)];
2183
+ return true;
2184
+ }
2171
2185
 
2172
2186
  // ../src/install/codex-hook-trust.ts
2173
2187
  import { createHash as createHash3 } from "node:crypto";
@@ -8,7 +8,7 @@
8
8
  "command": "node \"${PLUGIN_ROOT}/components/bootstrap/dist/cli.js\" hook session-start",
9
9
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\bootstrap.ps1\"",
10
10
  "timeout": 30,
11
- "statusMessage": "LazyCodex(4.11.0): Checking Bootstrap Provisioning"
11
+ "statusMessage": "LazyCodex(4.11.1): Checking Bootstrap Provisioning"
12
12
  }
13
13
  ]
14
14
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sisyphuslabs/codex-bootstrap",
3
- "version": "4.11.0",
3
+ "version": "4.11.1",
4
4
  "description": "Codex SessionStart bootstrap component that provisions LazyCodex runtime dependencies from a detached worker.",
5
5
  "type": "module",
6
6
  "private": true,
@@ -1990,7 +1990,6 @@ async function executeCodegraphSessionStartHook(options = {}) {
1990
1990
  const homeDir = resolveHomeDir2(env);
1991
1991
  const config = options.config ?? getCodexOmoConfig({ cwd: projectRoot, env, homeDir });
1992
1992
  if (config.codegraph?.enabled === false) {
1993
- writeHookJson(options.stdout ?? processStdout, "skipped-disabled");
1994
1993
  return { action: "skipped-disabled", exitCode: 0 };
1995
1994
  }
1996
1995
  (options.spawnWorker ?? spawnDetachedWorker)({
@@ -1998,11 +1997,16 @@ async function executeCodegraphSessionStartHook(options = {}) {
1998
1997
  command: process.execPath,
1999
1998
  env: { ...env, [SESSION_START_CWD_ENV]: projectRoot }
2000
1999
  });
2001
- writeHookJson(options.stdout ?? processStdout, "spawned");
2000
+ writeHookJson(options.stdout ?? processStdout);
2002
2001
  return { action: "spawned", exitCode: 0 };
2003
2002
  }
2004
- function writeHookJson(stdout, action) {
2005
- const output = action === "spawned" ? { hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: CODEGRAPH_SESSION_START_NOTICE }, codegraph: { action } } : { hookSpecificOutput: { hookEventName: "SessionStart" }, codegraph: { action } };
2003
+ function writeHookJson(stdout) {
2004
+ const output = {
2005
+ hookSpecificOutput: {
2006
+ hookEventName: "SessionStart",
2007
+ additionalContext: CODEGRAPH_SESSION_START_NOTICE
2008
+ }
2009
+ };
2006
2010
  stdout.write(`${JSON.stringify(output)}
2007
2011
  `);
2008
2012
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sisyphuslabs/codex-codegraph",
3
- "version": "4.11.0",
3
+ "version": "4.11.1",
4
4
  "description": "Codex plugin MCP wrapper for CodeGraph.",
5
5
  "type": "module",
6
6
  "private": true,
@@ -8,7 +8,6 @@ import { getCodexOmoConfig } from "../../../shared/src/config-loader.ts";
8
8
  import { SESSION_START_CWD_ENV } from "./session-start-worker.js";
9
9
  import type {
10
10
  HookStdout,
11
- SessionStartAction,
12
11
  SessionStartHookOptions,
13
12
  SessionStartHookResult,
14
13
  WorkerSpawnInvocation,
@@ -47,7 +46,6 @@ export async function executeCodegraphSessionStartHook(options: SessionStartHook
47
46
  const config = options.config ?? getCodexOmoConfig({ cwd: projectRoot, env, homeDir });
48
47
 
49
48
  if (config.codegraph?.enabled === false) {
50
- writeHookJson(options.stdout ?? processStdout, "skipped-disabled");
51
49
  return { action: "skipped-disabled", exitCode: 0 };
52
50
  }
53
51
 
@@ -56,14 +54,17 @@ export async function executeCodegraphSessionStartHook(options: SessionStartHook
56
54
  command: process.execPath,
57
55
  env: { ...env, [SESSION_START_CWD_ENV]: projectRoot },
58
56
  });
59
- writeHookJson(options.stdout ?? processStdout, "spawned");
57
+ writeHookJson(options.stdout ?? processStdout);
60
58
  return { action: "spawned", exitCode: 0 };
61
59
  }
62
60
 
63
- function writeHookJson(stdout: HookStdout, action: SessionStartAction): void {
64
- const output = action === "spawned"
65
- ? { hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: CODEGRAPH_SESSION_START_NOTICE }, codegraph: { action } }
66
- : { hookSpecificOutput: { hookEventName: "SessionStart" }, codegraph: { action } };
61
+ function writeHookJson(stdout: HookStdout): void {
62
+ const output = {
63
+ hookSpecificOutput: {
64
+ hookEventName: "SessionStart",
65
+ additionalContext: CODEGRAPH_SESSION_START_NOTICE,
66
+ },
67
+ };
67
68
  stdout.write(`${JSON.stringify(output)}\n`);
68
69
  }
69
70
 
@@ -42,7 +42,6 @@ describe("CodeGraph SessionStart hook", () => {
42
42
  hookEventName: "SessionStart",
43
43
  additionalContext: "LazyCodex CodeGraph bootstrap scheduled in background",
44
44
  },
45
- codegraph: { action: "spawned" },
46
45
  });
47
46
  } finally {
48
47
  rmSync(homeDir, { recursive: true, force: true });
@@ -66,10 +65,7 @@ describe("CodeGraph SessionStart hook", () => {
66
65
  // then
67
66
  expect(result).toEqual({ action: "skipped-disabled", exitCode: 0 });
68
67
  expect(spawned).toEqual([]);
69
- expect(JSON.parse(stdout.join(""))).toEqual({
70
- hookSpecificOutput: { hookEventName: "SessionStart" },
71
- codegraph: { action: "skipped-disabled" },
72
- });
68
+ expect(stdout.join("")).toBe("");
73
69
  });
74
70
 
75
71
  it("#given HOME OMO config disables Codex CodeGraph #when SessionStart fires #then it skips without spawning", async () => {
@@ -97,7 +93,7 @@ describe("CodeGraph SessionStart hook", () => {
97
93
  // then
98
94
  expect(result).toEqual({ action: "skipped-disabled", exitCode: 0 });
99
95
  expect(spawned).toEqual([]);
100
- expect(JSON.parse(stdout.join("")).codegraph).toEqual({ action: "skipped-disabled" });
96
+ expect(stdout.join("")).toBe("");
101
97
  } finally {
102
98
  rmSync(homeDir, { recursive: true, force: true });
103
99
  rmSync(workspace, { recursive: true, force: true });
@@ -128,7 +124,7 @@ describe("CodeGraph SessionStart hook", () => {
128
124
  // then
129
125
  expect(result).toEqual({ action: "skipped-disabled", exitCode: 0 });
130
126
  expect(spawned).toEqual([]);
131
- expect(JSON.parse(stdout.join("")).codegraph).toEqual({ action: "skipped-disabled" });
127
+ expect(stdout.join("")).toBe("");
132
128
  } finally {
133
129
  rmSync(homeDir, { recursive: true, force: true });
134
130
  rmSync(workspace, { recursive: true, force: true });
@@ -191,7 +187,12 @@ describe("CodeGraph SessionStart hook", () => {
191
187
  },
192
188
  },
193
189
  ]);
194
- expect(JSON.parse(stdout.join("")).codegraph).toEqual({ action: "spawned" });
190
+ expect(JSON.parse(stdout.join(""))).toEqual({
191
+ hookSpecificOutput: {
192
+ additionalContext: "LazyCodex CodeGraph bootstrap scheduled in background",
193
+ hookEventName: "SessionStart",
194
+ },
195
+ });
195
196
  } finally {
196
197
  rmSync(workspace, { recursive: true, force: true });
197
198
  }
@@ -514,7 +515,7 @@ describe("CodeGraph SessionStart hook", () => {
514
515
  }
515
516
  });
516
517
 
517
- it("#given malformed hook input #when SessionStart fires #then it still emits JSON and exits zero", async () => {
518
+ it("#given malformed hook input with CodeGraph disabled #when SessionStart fires #then it stays silent and exits zero", async () => {
518
519
  // given
519
520
  const stdout: string[] = [];
520
521
  const spawned: WorkerSpawnInvocation[] = [];
@@ -531,7 +532,7 @@ describe("CodeGraph SessionStart hook", () => {
531
532
  // then
532
533
  expect(result.exitCode).toBe(0);
533
534
  expect(spawned).toEqual([]);
534
- expect(JSON.parse(stdout.join("")).codegraph).toEqual({ action: "skipped-disabled" });
535
+ expect(stdout.join("")).toBe("");
535
536
  });
536
537
 
537
538
  it("#given plugin hook config #when inspected #then CodeGraph is registered after bootstrap SessionStart", () => {
@@ -8,7 +8,7 @@
8
8
  "type": "command",
9
9
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook post-tool-use",
10
10
  "timeout": 30,
11
- "statusMessage": "LazyCodex(4.11.0): Checking Comments"
11
+ "statusMessage": "LazyCodex(4.11.1): Checking Comments"
12
12
  }
13
13
  ]
14
14
  }