@herjarsa/omo-meta-governor 0.22.0 → 0.23.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.
package/README.md CHANGED
@@ -1,768 +1,761 @@
1
- # @herjarsa/omo-meta-governor
2
-
3
- Self-judging agent orchestration layer for OpenCode. Observes tool executions,
4
- reads session state, scores progress, and dispatches decisions. Includes **15 custom tools**
5
- that the agent can invoke across CodeGraph, Graphify, AFT, AgentMemory, Magic Context, and SQLite.
6
-
7
- ## Install
8
-
9
- ```bash
10
- npm install @herjarsa/omo-meta-governor
11
- ```
12
-
13
- ## Usage
14
-
15
- Add as a plugin in your OpenCode config:
16
-
17
- ```jsonc
18
- {
19
- "plugins": ["@herjarsa/omo-meta-governor"]
20
- }
21
- ```
22
-
23
- The 15 custom tools register automatically (even without setting enabled:true).
24
- To also enable the governance pipeline (intervention, protocol enforcement):
25
-
26
- ```jsonc
27
- {
28
- "meta_governor": {
29
- "enabled": true,
30
- "intervention": {
31
- "mode": "message",
32
- "minActionForMessage": "warn"
33
- }
34
- }
35
- }
36
- ```
37
-
38
- ## 15 Custom Tools
39
-
40
- The plugin registers 15 tools the LLM can invoke. All available immediately on install.
41
-
42
- ### Code Search & Navigation
43
-
44
- | Tool | What it does | Use case |
45
- |------|-------------|----------|
46
- | `omo_search` | Semantic code search via codegraph/graphify with AFT fallback | Architecture questions, finding features — USE THIS FIRST |
47
- | `omo_find` | Exact symbol lookup (definition + direct callers) via codegraph node | "Find the function `validateToken`" |
48
- | `omo_impact` | Impact analysis: callers, transitive callers, test files, doc files | Run BEFORE modifying a function |
49
- | `omo_path` | Shortest conceptual path between two concepts via graphify | "How does auth connect to database?" |
50
- | `omo_explain` | Plain-language explanation of a concept via graphify | "What is the SwinTransformer?" |
51
- | `omo_outline` | Structural outline of files/directories via AFT | Understanding a new file's structure |
52
-
53
- ### Lesson & Memory
54
-
55
- | Tool | What it does | Use case |
56
- |------|-------------|----------|
57
- | `omo_recall` | Search past lessons via local SQLite FTS5 (fast, always available) | "How did we set up auth before?" |
58
- | `omo_recall_mcp` | Search cross-session memory via AgentMemory | "What did we learn about X in previous sessions?" |
59
- | `omo_remember` | Save a fact/observation to cross-session AgentMemory | "Remember this bug pattern for next time" |
60
-
61
- ### Rules & Notes
62
-
63
- | Tool | What it does | Use case |
64
- |------|-------------|----------|
65
- | `omo_rule` | Save a durable rule to Magic Context (ctx_memory) | "Always use bun:sqlite, not better-sqlite3" |
66
- | `omo_history` | Search git history + past messages via ctx_search | "When did we add this feature?" |
67
- | `omo_note` | Write ephemeral session note via ctx_note | "Currently debugging auth in module X" |
68
-
69
- ### Safety & Status
70
-
71
- | Tool | What it does | Use case |
72
- |------|-------------|----------|
73
- | `omo_checkpoint` | Create a named AFT snapshot before risky changes | Undo protection before refactoring |
74
- | `omo_undo` | Revert to most recent AFT checkpoint | "That broke things, revert it" |
75
- | `omo_health` | Show plugin runtime status: metrics, decisions, errors | "Is the plugin working?" |
76
-
77
- ## Health & Observability
78
-
79
- The plugin exposes a health JSON file at `~/.config/opencode/meta-governor-health.json`:
80
-
81
- ```bash
82
- cat ~/.config/opencode/meta-governor-health.json
83
- ```
84
-
85
- Or the agent can call `omo_health` directly to get a formatted report.
86
-
87
- Structured JSONL logs at `~/.config/opencode/meta-governor.log` with size-based rotation
88
- (10MB max, 5 rotated files).
89
-
90
- ## Persistence
91
-
92
- Lessons learned by the plugin persist in **SQLite** at `~/.omo-meta-governor/meta-governor.db`
93
- with full-text search (FTS5) for fast recall. Zero dependencies needed — uses Bun's built-in
94
- `bun:sqlite`.
95
-
96
- Optionally, the Opción A tools (`omo_remember`, `omo_recall_mcp`, `omo_rule`, `omo_history`,
97
- `omo_note`) can bridge to AgentMemory and Magic Context via `session.prompt()` — the LLM
98
- receives a structured instruction to call the appropriate MCP tool.
99
-
100
- ## Graph Sync (v0.11.0)
101
-
102
- MetaGovernor wires the plugin into the native git hooks of **codegraph** and
103
- **graphify** so each commit automatically reindexes both graphs.
104
-
105
- ### What it does on first load in a project
106
-
107
- 1. **Auto-install** codegraph via `npm i -D @colbymchenry/codegraph` and
108
- graphify via `pip install graphifyy` (falls back to `uv tool install
109
- graphifyy`) if they're not already on PATH.
110
- 2. **Run `codegraph init`** + **`graphify . --no-viz`** to build the initial
111
- indexes for the project.
112
- 3. **Run `graphify hook install`** to wire up the native `post-commit` and
113
- `post-checkout` git hooks.
114
-
115
- ### What it does on each `git commit`
116
-
117
- - **Primary path** (native git hook): `graphify update` runs in background.
118
- - **Backup path** (plugin's `tool.execute.after`): detects `git commit` in
119
- bash commands and runs `codegraph sync -q [path]`.
120
-
121
- ### Process zombie safeguards (v0.22.0)
122
-
123
- Every subprocess the plugin spawns (graphify, codegraph, `aft`, npx, python,
124
- npm/pip) is guaranteed to die after use — on success, error, AND timeout —
125
- including its descendant tree. On Windows this uses `taskkill /pid <pid> /T /F`
126
- (plain `child.kill()` only kills the direct shell, orphaning grandchildren —
127
- the confirmed cause of the Bun/OpenChamber crashes).
128
-
129
- Config: `graphSync.killOrphanedOnInit` (default `true`) — on graph-sync init
130
- the plugin sweeps orphaned `graphify`/`codegraph`/`aft` processes left by
131
- previous crashed runs. Set to `false` to disable the sweep.
132
-
133
- ## Intervention
134
-
135
- MetaGovernor can inject governance decisions into the agent's context.
136
- Enabled when `meta_governor.enabled: true` in config.
137
-
138
- ### Modes
139
-
140
- | Mode | Mechanism | Effect |
141
- |------|-----------|--------|
142
- | `silent` | (none) | Decision is logged only |
143
- | `message` | `experimental.chat.messages.transform` | Injects a synthetic user message visible to the LLM |
144
- | `system` | `experimental.chat.system.transform` | Appends guidance to the system prompt |
145
-
146
- ### Configuration
147
-
148
- ```jsonc
149
- {
150
- "meta_governor": {
151
- "enabled": true,
152
- "intervention": {
153
- "mode": "message",
154
- "minActionForMessage": "warn",
155
- "maxInterventionsPerSession": 3,
156
- "respectDoneSignal": true,
157
- "phaseAwareDoneSignal": true // v0.15.0: multi-phase plan support
158
- }
159
- }
160
- }
161
- ```
162
-
163
- ### Fields
164
-
165
- | Field | Default | Description |
166
- |-------|---------|-------------|
167
- | `mode` | `"message"` | How to inject: `"silent"`, `"message"`, or `"system"` |
168
- | `minActionForMessage` | `"warn"` | Minimum action: `"warn"`, `"escalate"`, or `"stop"` |
169
- | `maxInterventionsPerSession` | `3` | Hard cap on injections per session |
170
- | `respectDoneSignal` | `true` | Stop injecting after terminal signal + Oracle verified |
171
- | `phaseAwareDoneSignal` | `false` | **v0.15.0**: when `true`, only `<promise>PLAN-COMPLETE</promise>` latches intervention. DONE/PHASE-N-COMPLETE are per-phase hints. Recommended for multi-phase plans. |
172
-
173
- ## Skill Priming (v0.20.0)
174
-
175
- Proactive skill-selection nudge: the plugin injects **one** synthetic user message at session
176
- start (or once implementation work begins) prompting the agent to select precise skills for the
177
- task via the **AAS skill catalog** (`aas search_skills` / `get_skill` / `compose_stack`) and/or
178
- the task-appropriate **superpowers** skill — before writing code. Minimal context cost: the
179
- directive forbids enumerating the full catalog.
180
-
181
- ```jsonc
182
- {
183
- "meta_governor": {
184
- "enabled": true,
185
- "skillPriming": {
186
- "enabled": true,
187
- "trigger": "sessionStart", // or "firstImplement" (default)
188
- "router": "both" // "aas", "superpowers", or "both"
189
- }
190
- }
191
- }
192
- ```
193
-
194
- | Field | Default | Description |
195
- |-------|---------|-------------|
196
- | `enabled` | `false` | Master switch for the skill-priming nudge |
197
- | `trigger` | `"firstImplement"` | `"sessionStart"`: first transform call of the session. `"firstImplement"`: once a write/edit-like tool is observed |
198
- | `router` | `"both"` | Which system(s) the directive references: `"aas"`, `"superpowers"`, `"both"` |
199
-
200
- ### Multi-phase plans (v0.15.0)
201
-
202
- For work plans with multiple phases (e.g. Sisyphus/Prometheus work plans),
203
- configure `phaseAwareDoneSignal: true` and emit `<promise>PLAN-COMPLETE</promise>`
204
- only when the **entire** plan is verified done by Oracle. The new markers:
205
-
206
- | Marker | Effect |
207
- |--------|--------|
208
- | `<promise>DONE</promise>` | Per-phase hint. Logged but does NOT latch intervention (when `phaseAwareDoneSignal: true`). |
209
- | `<promise>PHASE-N-COMPLETE</promise>` | Per-phase hint (e.g. `<promise>PHASE-1-COMPLETE</promise>`). Same as DONE — logged, does NOT latch. |
210
- | `<promise>PLAN-COMPLETE</promise>` | Terminal. Latches intervention when Oracle has verified. |
211
-
212
- **Migration**: existing v0.10.0–v0.14.x users keep working without changes (default
213
- `phaseAwareDoneSignal: false` preserves the legacy single-task behavior). Set the
214
- flag to `true` and switch your terminal marker to `PLAN-COMPLETE` to enable
215
- multi-phase governance.
216
-
217
- ## v0.16.0 — Audit remediation: memory hygiene, dead code, tool coverage, CI
218
-
219
- v0.16.0 closes the 50+ findings from the multi-front audit at `.omo/ulw-research/20260727-000530/plan-audit-v0.15.0.md`. The release is **additive in behavior, no breaking API changes** for users — only internal cleanup, dead code removal, and CI hardening.
220
-
221
- ### Highlights
222
-
223
- #### Memory hygiene (F1)
224
-
225
- - **`AuditStateCache`** (`src/audit-state-cache.ts`) — TTL+LRU bounded cache (100 entries, 1h TTL) replaces the bare `Map` that accumulated audit state without bounds. Stale sessions are evicted automatically.
226
- - **`TTLQueue`** (`src/ttl-queue.ts`) — TTL-based expiration for `pendingBotFeedback` and `pendingViolations` queues. Previously unbounded.
227
- - Removed dynamic `require("node:fs")` inside `shouldInjectPlanReminder` — replaced with static ESM imports (no more runtime module resolution failures).
228
-
229
- #### Dead code elimination (F2)
230
-
231
- - `takeAnyDecision()` — deprecated; removed from the active governance pipeline.
232
- - `systemInjection` — now awaited eagerly instead of fire-and-forget, eliminating a silent failure route.
233
- - `logToFile` in `graph-sync.ts` — wired to the real JSONL file logger (was a no-op stub).
234
- - Plugin version — derived from `package.json` at runtime instead of hardcoded "0.13.0" (closes the version-drift bug where `omo_health` reported stale versions).
235
-
236
- #### Tool bug fixes (F3)
237
-
238
- - **AFT checkpoint/undo**: args split on whitespace broke names with spaces. Rewrote arg construction with proper quoting.
239
- - **AFT subcommand**: now uses `options.projectDir` instead of `process.cwd()`.
240
- - **graphify binary override**: `omo_path` / `omo_explain` honored the `graphifyBin` option (was hardcoded).
241
- - **`as never` cast** on `setClient` → proper runtime guard that validates client shape.
242
- - **`session-bridge`**: replaced module-level `_client` with `AsyncLocalStorage` for per-request isolation. Concurrent sessions no longer race on the same client reference.
243
-
244
- #### Test coverage (F4)
245
-
246
- - 22 tests covering all 15 custom tools (`src/custom-tools.test.ts`). Previously the entire public tool surface had zero test coverage.
247
- - 12 tests for `decision-store` (previously untested).
248
-
249
- #### Type/token pipeline (F5)
250
-
251
- - `token-predictor` refactor: dead code (`delegate`/`switch-model`) removed; output is now informational-only as designed.
252
- - Type alignment across `types.ts`, `token-predictor.ts`, `orchestrator.ts`.
253
-
254
- #### CI matrix (F6)
255
-
256
- - `bun run typecheck` now runs on **macos-latest** and **windows-latest** (was Ubuntu-only).
257
- - Removed `package-lock.json` (bun project — canonical is `bun.lock`).
258
- - Secret redaction layer in `logToFile` (JWT, OpenAI keys, Bearer tokens, GitHub PATs, generic key:value patterns).
259
- - Implementation plan renamed `IMPLEMENTATION_PLAN.md` → `ARCHITECTURE.md`.
260
-
261
- #### Final refactors (F7)
262
-
263
- - Score formula documented (header doc with full formula spec).
264
- - **NaN guard** in `score()` — defaults to neutral continue when `iterationRatio` or `ambient.iteration/maxIterations` produce NaN.
265
- - `ACTION_SEVERITY` keyed by `DecisionHandlerOutput["action"]` union literal (was bare `Record<string, number>`).
266
- - `projectHasCodegraph` / `projectHasGraphify` IIFE booleans replaced with lookup-time calls to `graphRetrieval.hasCodegraphDir(cwd)`.
267
- - `extractConcepts` includes file basename for FTS lookup by tool/file name.
268
- - Backup graph-sync uses `triggerReindex` (was `triggerCodegraphSync`) — reindexes both codegraph AND graphify backends.
269
-
270
- ### Test & build status
271
-
272
- - **495/495 tests pass** (up from 487 in v0.15.0/0.15.1).
273
- - `bun run typecheck` clean.
274
- - `bun build.ts` clean (0.34 MB dist).
275
- - `npm pack --dry-run` validated (no forbidden artifacts).
276
-
277
- ### Migration
278
-
279
- No user action required. All changes are internal. The default `phaseAwareDoneSignal` is still `false` for backward compatibility; the v0.15.0 multi-phase behavior is preserved when explicitly enabled.
280
-
281
- ### Deferred to v0.17.0
282
-
283
- - F5.1 — wiring `escalate` action to a real dispatcher (Oracle is recommended but not yet wired).
284
- - F5.4 — `maxLessonsPerSession` enforcement (config field exists but is not enforced).
285
- - F3.6 — Bridge tools lying about delivery (5 tools still return "dispatched" without polling). Recommend the user explicitly request this if delivery verification is critical.
286
-
287
-
288
-
289
-
290
- ## v0.17.0 — Wire escalate to Oracle, enforce lesson cap, verify bridge delivery
291
-
292
- v0.17.0 closes the 3 deferred items from the v0.16.0 audit: **F5.1** (escalate → Oracle), **F5.4** (`maxLessonsPerSession` enforcement), and **F3.6** (bridge tool delivery verification).
293
-
294
- ### Highlights
295
-
296
- #### F5.1 — Escalate action now fires Oracle (v0.17.0)
297
-
298
- When the scoring engine produces an `escalate` action with target `oracle`, the plugin's `tool.execute.after` hook now fires a `session.prompt()` instructing the LLM to invoke `task(subagent_type=oracle)`. The prompt includes the decision reasoning, evidence count, and a verification pass directive. New `buildEscalationPrompt()` function in `session-bridge.ts` is the pure prompt builder (testable in isolation). User-targeted escalations get a separate prompt asking the LLM to summarize for human input.
299
-
300
- ```ts
301
- // Decision flow when score lands in escalate band:
302
- score ≤ -escalateThreshold (default -0.6)
303
- → decision.action = "escalate"
304
- → decision.shouldEscalateTo = "oracle" (or "user" for grave deviations)
305
- → plugin fires session.prompt with buildEscalationPrompt(...)
306
- → LLM invokes Oracle (or summarizes for user)
307
- → Oracle verifies → oracleInvoked=true → governance continues
308
- ```
309
-
310
- #### F5.4 — `maxLessonsPerSession` is now enforced
311
-
312
- The cap (default 20) was a config field that was never enforced. v0.17.0 adds:
313
- - `currentLessonCount` on `LearnFromOutcomeInput` and `MetaGovernorInput`
314
- - `lessonCount` tracked in per-session `AuditState`
315
- - `observeAndLearn()` short-circuits when `currentLessonCount >= maxLessonsPerSession`
316
- - The orchestrator increments `sessionState.lessonCount` after each successful save
317
- - **Cap semantics: inclusive** — when count equals cap, no more lessons are saved
318
-
319
- #### F3.6 — Bridge tool delivery verification
320
-
321
- The 5 bridge tools (`omo_remember`, `omo_recall_mcp`, `omo_rule`, `omo_history`, `omo_note`) previously returned "dispatched" after the `session.prompt()` was queued — without verifying the LLM actually called the MCP tool. v0.17.0 adds:
322
-
323
- - **New `PendingDeliveryRegistry` module** (`src/delivery-registry.ts`) — tracks pending dispatches per session with TTL-based cleanup.
324
- - **`tool.execute.after` hook** marks deliveries when a matching MCP tool call is observed.
325
- - **All 5 bridge tools** now report `deliveryStatus: "delivered" | "pending"` in their tool result and metadata, and briefly poll (1.5s) for fast deliveries.
326
- - When the LLM follows the prompt, the tool returns immediately with `"delivered"`. When it doesn't, the tool returns `"pending"` and the entry expires silently after 10s.
327
-
328
- ```ts
329
- // Bridge tool result metadata now includes:
330
- {
331
- tool: "omo_remember",
332
- ok: true,
333
- deliveryStatus: "delivered" | "pending",
334
- messageID: "...",
335
- durationMs: 1234,
336
- contentLength: 256
337
- }
338
- ```
339
-
340
- ### Test & build status
341
-
342
- - **514/514 tests pass** (up from 495 in v0.16.0 — 5 + 4 + 10 new tests across F5.4, F5.1, F3.6).
343
- - `bun run typecheck` clean.
344
- - `bun build.ts` clean (0.34 MB dist).
345
- - `npm pack --dry-run` validated.
346
-
347
- ### Migration
348
-
349
- No user action required. All changes are internal or additive:
350
- - `deliveryStatus` is an additive metadata field — existing consumers ignore it.
351
- - `maxLessonsPerSession` is now actually enforced — if you have sessions that previously saved more than 20 lessons (e.g. from before the cap was added), this may surprise you. Bump the cap in your config if needed.
352
- - `escalate` action now actively fires Oracle — this is the first version where Oracle is auto-invoked, not just manually invoked by the LLM.
353
-
354
- ### Audit roadmap (status as of v0.17.0)
355
-
356
- | Release | Status | Scope |
357
- |---------|--------|-------|
358
- | v0.15.1 (F0) | ✅ Shipped | Hotfix self-dep + npm pack gate |
359
- | v0.16.0 (F1-F7) | ✅ Shipped | Memory hygiene, dead code, tool coverage, CI |
360
- | v0.17.0 (deferred) | ✅ Shipped | F5.1 escalate, F5.4 cap, F3.6 delivery verify |
361
-
362
- All audit findings are now closed. Future work focuses on new features and user-driven feedback.
363
-
364
-
365
-
366
- ## v0.17.1 — Audit args fix (patch)
367
-
368
- v0.17.1 is a single-bug patch release. The fix addresses an issue discovered during v0.17.0 verification:
369
-
370
- ### The bug
371
-
372
- The `tool.execute.before` hook passed an empty `{}` object as the second argument to `auditToolCall()`. This meant the audit function never saw the tool's args (e.g. file content for write tools) and could never detect:
373
-
374
- - `@ts-ignore` / `@ts-expect-error` directives
375
- - `as any` type assertions
376
- - `catch(e) {}` empty catch blocks
377
-
378
- The hook signature was also incomplete — it didn't receive the `output` parameter that contains the mutable args, even though the SDK provides it.
379
-
380
- ### The fix
381
-
382
- Two changes in `src/plugin.ts`:
383
-
384
- 1. **Hook signature updated** to receive the `output` parameter:
385
- ```ts
386
- "tool.execute.before": async (
387
- toolInput: { tool: string; sessionID: string; callID: string },
388
- _output: { args: unknown },
389
- ): Promise<void> => {
390
- ```
391
-
392
- 2. **Audit call** now passes `_output.args` instead of `{}`:
393
- ```ts
394
- const violations = auditToolCall(toolInput.tool, _output.args, { ... })
395
- ```
396
-
397
- ### Tests
398
-
399
- Added 4 new tests in `src/plugin.test.ts`:
400
- - `@ts-ignore + as any` in args → `no-type-suppression` violation detected and injected
401
- - `catch(e) {}` in args → `no-empty-catch` violation detected and injected
402
- - Clean code → no violation injected (false-positive guard)
403
- - `auditToolCalls: false` → audit short-circuits (regression check)
404
-
405
- ### Test & build status
406
-
407
- - **518/518 tests pass** (up from 514 in v0.17.0 — 4 new audit tests).
408
- - `bun run typecheck` clean.
409
- - `bun build.ts` clean (0.34 MB dist).
410
-
411
- ### Migration
412
-
413
- No user action required. The audit detection now correctly fires when the agent writes forbidden patterns. This means:
414
-
415
- - **If your agent previously wrote `@ts-ignore` without being flagged**: it will now be flagged with `[GRAVE] no-type-suppression: ...` injected as a synthetic user message.
416
- - **If you want to disable the audit**: set `protocolEnforcement.auditToolCalls: false` (already supported).
417
-
418
-
419
-
420
- ## v0.17.2 — Fix escalation dead code + 4 audit gaps
421
-
422
- v0.17.2 closes 4 gaps discovered during live verification of v0.17.0/v0.17.1. The most important: F5.1 (escalate → Oracle) was effectively dead in production due to two compounding bugs.
423
-
424
- ### Highlights
425
-
426
- #### Gap C (CRITICAL) — Escalation now actually fires
427
-
428
- The score formula's `noProgress` and `deviations` inputs were hardcoded as `false` and `[]` in the plugin. This meant the `no-progress-detector` (weight 0.20) and `deviation-detector` (weight 0.20) signals always contributed 0. Combined with default thresholds, the maximum possible score was -0.55 — never reaching `escalateThreshold: 0.6` or `stopThreshold: 0.8`.
429
-
430
- **Fix:**
431
- 1. **Derive `noProgress`** from the recent tool call window. If the last 5 tool calls contain no `write`/`edit`/`task` (i.e. the agent is only reading/grepping without producing artifacts), `noProgress = true`.
432
- 2. **Derive `deviations`** from accumulated protocol violations. The audit hook now stores violations in `state.accumulatedDeviations` (capped at 5 per session); the orchestrator input reads them.
433
- 3. **Lower default thresholds** to match the new worst-case math:
434
- - `escalateThreshold`: 0.6 → 0.45
435
- - `stopThreshold`: 0.8 → 0.55
436
-
437
- Now worst-case state (no oracle, no progress, 2 grave deviations, iteration at limit, stop-advice lessons) produces score ≈ -0.55 → `stop` action fires.
438
-
439
- #### Gap Q (HIGH) — File paths threaded through pipeline
440
-
441
- `orchestrator.ts` was hardcoding `filesChanged: []` instead of `input.filePaths`. This meant lesson extraction never saw the actual changed files, so F7.5's file-basename FTS indexing was empty.
442
-
443
- **Fix:**
444
- 1. Track `recentWriteFilePaths` in AuditState (alongside existing `recentWriteContents`).
445
- 2. Capture `filePath` from `toolInput.args` on write/edit tool calls.
446
- 3. New `MetaGovernorInput.filePaths?: readonly string[]` passed through to `observeAndLearn`.
447
-
448
- #### Gap D (HIGH) — Three config fields now actually do something
449
-
450
- Three fields were in the schema and config projection but NEVER consulted by the logic:
451
-
452
- - `closedLoop.saveLessons` — parallel to `saveDecisions`. When `false`, lessons are skipped (decision records still save).
453
- - `intervention.includeDecisionHistory` — when `true`, `messages.transform` prepends recent intervention texts (capped at `maxHistoryMessages`) so the LLM sees its history of decisions.
454
- - `intervention.maxHistoryMessages` — limit for the above (default 5).
455
-
456
- **Fix:** All three fields now control behavior. Track `recentInterventionTexts` in AuditState, format them into the injection text.
457
-
458
- #### Bonus — iteration-budget signal wired (Oracle finding)
459
-
460
- Oracle flagged a pre-existing gap alongside Gap C: `iteration` was hardcoded `0` in the orchestrator input, making the `iteration-budget` signal (weight 0.15) effectively dead.
461
-
462
- Fix:
463
- - Added `iteration: number` to `AuditState`, incremented per tool call.
464
- - Threaded `iteration: sessionState?.iteration ?? 0` into `MetaGovernorInput`.
465
- - `maxIterations` now reads from config instead of being hardcoded.
466
-
467
- Worst-case score math updated: with iteration at 100% (-0.12), all signals bad, no oracle → score = -0.65 → `stop` action fires.
468
-
469
- #### Gap I (MEDIUM) — `verifyDelivery` return type includes "expired"
470
-
471
- The TypeScript signature was `Promise<"delivered" | "pending">` but the registry could return `"expired"`. The expired case leaked through as `"pending"` silently.
472
-
473
- **Fix:** Signature updated to `Promise<"delivered" | "pending" | "expired">`. Bridge tools now distinguish: `"delivered"` (verified), `"pending"` (still polling), `"expired"` (TTL elapsed).
474
-
475
- ### Test & build status
476
-
477
- - **521/521 tests pass** (up from 518 — 3 new tests for the v0.17.2 fixes).
478
- - `bun run typecheck` clean.
479
- - `bun build.ts` clean (0.34 MB dist).
480
- - `npm pack --dry-run` validated.
481
-
482
- ### Migration
483
-
484
- No user action required. Two behavior changes:
485
-
486
- 1. **Escalation now fires more aggressively.** If your agent has been producing violations and not making progress, expect to see escalate → Oracle prompts more often. This is the intended behavior; v0.17.0 was incorrectly silent.
487
- 2. **`includeDecisionHistory` and `maxHistoryMessages` are now functional.** If you set them in v0.17.0 expecting them to work, they will now actually take effect.
488
-
489
- ### Audit roadmap (status as of v0.17.2)
490
-
491
- | Release | Status | Scope |
492
- |---------|--------|-------|
493
- | v0.15.1 (F0) | ✅ | Hotfix self-dep |
494
- | v0.16.0 (F1-F7) | ✅ | Memory hygiene, dead code, tool coverage, CI |
495
- | v0.17.0 | ✅ | F5.1 escalate, F5.4 cap, F3.6 delivery verify |
496
- | v0.17.1 | ✅ | Audit args fix |
497
- | v0.17.2 | ✅ | Gap C (escalation live), Q (file paths), D (config fields), I (delivery expired) |
498
-
499
-
500
-
501
- ## v0.17.3 — Fix Gap I properly (patch)
502
-
503
- v0.17.3 is a single-bug patch. During live verification of v0.17.2, Gap I was found to be incompletely fixed.
504
-
505
- ### The bug (v0.17.2 cosmetic fix)
506
-
507
- The `verifyDelivery` export signature was widened to include `"expired"` in v0.17.2, and the bridge tools' title/output text was updated to handle it. **BUT the underlying `pollForDelivery` helper was still collapsing `"expired"` → `"pending"` silently:**
508
-
509
- ```ts
510
- // v0.17.2 (BUG):
511
- return status === "delivered" ? "delivered" : "pending"
512
- ```
513
-
514
- So bridge tools could never report `"expired"` to the user, even though the registry correctly tracked it. Live verification confirmed: `deliveryStatus` always showed `"pending"`.
515
-
516
- ### The fix (v0.17.3)
517
-
518
- ```ts
519
- // v0.17.3:
520
- return await pendingRegistryRef.awaitDelivery({ sessionID, mcpTool, timeoutMs })
521
- ```
522
-
523
- Now the actual status from the registry propagates through. `"expired"` flows end-to-end to the bridge tool's `metadata.deliveryStatus` and title.
524
-
525
- ### Tests
526
-
527
- Added 3 RED tests in `src/custom-tools.test.ts`:
528
- - Returns `"expired"` when registry entry exists past timeout (real registry instance)
529
- - Returns `"delivered"` when `markDelivered` fires before timeout
530
- - Returns `"pending"` when no registry is configured
531
-
532
- ### Test & build status
533
-
534
- - **525/525 tests pass** (up from 522 in v0.17.2 — 3 new tests for pollForDelivery).
535
- - `bun run typecheck` clean.
536
- - `bun build.ts` clean (0.34 MB dist).
537
-
538
- ### Migration
539
-
540
- No user action required. Bridge tools will now correctly distinguish all three delivery states:
541
- - `"delivered"` — LLM's MCP tool call was observed within 1.5s
542
- - `"expired"` — TTL elapsed without delivery (entry expires after 10s, but bridge tool sees this immediately as "expired" when polling times out at 1.5s)
543
- - `"pending"` — no registry configured (graceful degradation for tests/mocks)
544
-
545
- ### Audit roadmap (status as of v0.17.3)
546
-
547
- | Release | Status | Scope |
548
- |---------|--------|-------|
549
- | v0.15.1 → v0.17.2 | ✅ | All audit findings + 5 gap fixes |
550
- | v0.17.3 | ✅ | Gap I real fix (pollForDelivery returns "expired") |
551
-
552
- Two remaining gaps documented but require SDK support to fix:
553
- - `recentTurnTokens: []` — token-predictor signal dead (10% of score); needs per-turn token counts from OpenCode SDK
554
- - `agentName` defaults to `"unknown"` — cosmetic, no functional impact
555
-
556
-
557
-
558
- ## v0.18.0 — Audit remediation: 7+ silent config drops + circular ref crash
559
-
560
- v0.18.0 is a thorough-audit patch release. Each fix addresses a bug found by testing every public function with edge cases and adversarial inputs.
561
-
562
- ### Highlights
563
-
564
- | # | Bug | Severity | Fix |
565
- |---|-----|----------|-----|
566
- | 1 | `file-logger.redactData` crashed on circular references with stack overflow | 🔴 CRITICAL | `WeakSet` guard + `try/catch` fallback |
567
- | 2 | `loadOrchestratorConfig` only projected `closedLoop.saveDecisions` — `enabled`, `minSeverityToLearn`, `maxLessonsPerSession`, `saveLessons` were silently dropped | 🔴 CRITICAL | Project all 5 fields |
568
- | 3 | `loadOrchestratorConfig` didn't project `decision.warnMessageTemplate`, `escalateMessageTemplate`, `stopMessageTemplate` | 🟠 HIGH | Project all 3 templates |
569
- | 4 | `loadOrchestratorConfig` didn't project `scoring.paralysisThreshold`, `defaultEscalationTarget` | 🟠 HIGH | Project all fields |
570
- | 5 | `loadOrchestratorConfig` had `memory.timeoutMs` field name mismatch (schema said `agentmemoryTimeoutMs`) | 🟠 HIGH | Accept both names |
571
- | 6 | `isMetaGovernorEnabled` only checked top-level `enabled`, not `meta_governor.enabled` (wrapped shape from `opencode.jsonc`) | 🟠 HIGH | Check both shapes |
572
- | 7 | `createMetricsCollector` crashed when called without config (`config.version` on `undefined`) | 🟠 HIGH | Accept `Partial<MetricsCollectorConfig>` |
573
- | 8 | `metrics.inc` crashed on unknown event names (`bucket.count++` on `undefined`) | 🟠 HIGH | Guard `if (!bucket) return` |
574
- | 9 | `isNewerVersion` returned `false` for `installed=null` (no upgrade triggered for fresh installs) | 🟡 MEDIUM | Return `true` when installed is null AND latest is valid |
575
-
576
- ### Test & build status
577
-
578
- - **557/557 tests pass** (up from 530 in v0.17.3 — 27 new tests for the audit fixes).
579
- - `bun run typecheck` clean.
580
- - `bun build.ts` clean (0.34 MB dist).
581
- - `npm pack --dry-run` validated.
582
-
583
- ### Migration
584
-
585
- No user action required. The fix to `loadOrchestratorConfig` means **users who were setting `closedLoop.maxLessonsPerSession` or other previously-dropped fields will now see those values actually take effect**. If you had a config like `{ "closedLoop": { "maxLessonsPerSession": 50 } }` before v0.18.0, it was silently being overridden to 20. Starting v0.18.0, the value 50 is now respected.
586
-
587
- ### Audit roadmap (status as of v0.18.0)
588
-
589
- | Release | Status | Scope |
590
- |---------|--------|-------|
591
- | v0.15.1 → v0.17.3 | ✅ | All audit findings + deferred items + audit args fix + gap fixes |
592
- | v0.18.0 | ✅ | 7 silent config drops + circular ref crash + metrics crashes + upgrade trigger |
593
-
594
- This release closes the final round of gaps found by a thorough function-by-function audit. The plugin now correctly projects **all** user configuration, handles **all** circular reference cases, and fails safely on **all** missing-input scenarios.
595
-
596
-
597
- ## v0.19.4 — Dual-shape default export: opencode 1.18.x loader compat (patch)
598
-
599
- v0.19.3 shipped a function-only default export hoping to fix the opencode
600
- serve factory-not-invoked bug. Empirical verification (reading
601
- `~/.config/opencode/meta-governor.log`) showed it didn''t help: opencode
602
- 1.18.16 npm-package plugins still loaded the module without ever calling
603
- the factory under `opencode serve`.
604
-
605
- ### The fix
606
-
607
- `src/index.ts` now exports an object that is **both** a Plugin function
608
- **and** a PluginModule:
609
-
610
- ```ts
611
- const _plugin = createMetaGovernorPlugin()
612
- _plugin.id = "omo-meta-governor"
613
- _plugin.server = _plugin
614
- export default _plugin
615
- ```
616
-
617
- Bundled as `var u4=IN(); u4.id="omo-meta-governor"; u4.server=u4; export{...JX=u4...}`.
618
- Whichever path opencode picks (`default(input, options)` or
619
- `default.server(input, options)`), the same callable fires and the hooks
620
- register.
621
-
622
- ### Verification
623
-
624
- After restart of OpenChamber, log shows:
625
- - factory_invoked events (was 0 with v0.19.3)
626
- - config_loaded events (was 0 with v0.19.3)
627
- - intervention / violations / persist calls firing normally
628
-
629
- ### Test & build status
630
-
631
- - 4/4 new tests in `src/index.test.ts` lock down the dual-shape contract.
632
- - `bun run typecheck` clean.
633
- - `bun build.ts` clean (0.35 MB dist).
634
- - `npm publish` to registry OK.
635
-
636
- ### Migration
637
-
638
- No user action required. If you previously set
639
- `intervention.persistToSession: false` in your config, that toggle is
640
- still respected.
641
-
642
- ---
643
-
644
- ## v0.19.7 — Fix npm-published package.json: restore `import` condition in `exports` (patch)
645
-
646
- v0.19.6 shipped a single callable default export with the dual-shape contract (memory #1126) but the published tarball's `package.json` had the `exports` map regenerated to contain only the `types` condition — no `import`/`default`/`require` runtime condition. Under opencode 1.18.16's plugin loader, that caused the npm-package plugin to load the module (module scope ran, `MetaGovernor plugin loaded` logged) but **never invoke the factory** (`factory_invoked` was 0). v0.19.4 had empirically verified the dual-shape fix for `opencode serve`; v0.19.6 on `opencode run` with the same dual-shape bundle in the npm cache was silently broken for the same reason.
647
-
648
- ### The bug
649
-
650
- `package.json` in the v0.19.6 tarball (read from the installed package in `~/.cache/opencode/packages/@herjarsa/omo-meta-governor@latest/...`):
651
-
652
- ```json
653
- "exports": {
654
- ".": { "types": "./dist/index.d.ts" },
655
- "./lib": { "types": "./dist/lib.d.ts" }
656
- }
657
- ```
658
-
659
- vs. the repo's source `package.json` (correct, with `import` condition for runtime resolution):
660
-
661
- ```json
662
- "exports": {
663
- ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
664
- "./lib": { "types": "./dist/lib.d.ts", "import": "./dist/lib.js" }
665
- }
666
- ```
667
-
668
- The published manifest lost the `import` condition (and `peerDependencies: {"@opencode-ai/plugin": ">=1.0.0"}`, `publishConfig`, `repository.url`, `devDependencies.typescript`, `files` README entry, `scripts.test`). When opencode's plugin installer resolves the entry via `exports`, only `"types"` is offered — no runtime condition matches → the loader imports the bundle by `main` fallback but the export contract that uk() (`opencode.util.createPlugin`) inspects isn't satisfied, so the plugin is loaded but never invoked.
669
-
670
- ### How the bug was reproduced
671
-
672
- Cheap-shape probes (`@herjarsa/omg-shape-probe2` … `probe7`) with byte-identical Bun-bundles from `dist/index.js` were published to npm and tested under `opencode run` with XDG-isolated config. All probes with `exports."."` containing `"import"` or `"default"` invoked the factory. The probe replicating the exact published shape (types-only exports) failed to invoke. Bundle SHA-256 identical between probes and the real published package:
673
- ```
674
- E737C676FCC74B6CBA771616058BC5620C1F0364089B1B8626246E96FF02ED69
675
- ```
676
-
677
- ### The fix
678
-
679
- No source/bundle changes — the bundle was correct all along. The malformed published `package.json` was restored from the repo (`git show HEAD:package.json` has the canonical full exports map with `import`) and only the version was bumped to `0.19.7`. Rebuild with `bun build.ts` (bakes version into the bundle), published, verified.
680
-
681
- ### Verification
682
-
683
- XDG-isolated opencode run (`opencode run` with custom config pointing at `@herjarsa/omo-meta-governor@latest`):
684
-
685
- - v0.19.6 (cache populated): log shows `MetaGovernor plugin loaded` only — **no `factory_invoked`**, intervention never runs.
686
- - v0.19.7 (cache cleared, reinstalled): log shows full chain:
687
- ```
688
- [meta-governor] v0.21.1 MetaGovernor plugin loaded
689
- [meta-governor] v0.21.1 factory_invoked
690
- [meta-governor] SessionBridge: OpenCode client hydrated — session.prompt() available
691
- [meta-governor] v0.21.1 config_loaded
692
- ```
693
- > **Note (v0.21.1+)**: every init log line now prepends `v<DEFAULT_VERSION>` (read from `package.json` at build time by Bun). Use this prefix to confirm which release OpenChamber actually loaded — a stale npm cache could otherwise serve an older bundle silently because `@latest` does not force re-fetch if the cache is still valid.
694
-
695
- Updated `~/.cache/opencode/packages/@herjarsa/omo-meta-governor@latest/node_modules/@herjarsa/omo-meta-governor/package.json` now reads `exports."."` = `{ types: "./dist/index.d.ts", import: "./dist/index.js" }` and `peerDependencies` = `{ "@opencode-ai/plugin": ">=1.0.0" }`.
696
-
697
- ### Test & build status
698
-
699
- - `bun run typecheck` clean.
700
- - `bun build.ts` clean (0.34 MB dist, version 0.19.7 baked in).
701
- - `npm publish` to registry OK.
702
- - Empirical harness verification: factory_invoked + config_logged restored.
703
-
704
- ### Migration
705
-
706
- No user action required. If you were running v0.19.6 and the plugin appeared silent (no interventions, no metrics), delete your opencode cache:
707
- ```bash
708
- rm -rf ~/.cache/opencode/packages/@herjarsa/omo-meta-governor@latest
709
- ```
710
- Then restart opencode — it will reinstall v0.19.7 from the registry and the pipeline will fire normally.
711
-
712
- ---
713
-
714
- ## v0.19.3 — opencode serve factory invocation + persistSessionMessage wiring
715
-
716
- This release closes the persistSessionMessage wiring arc (memory #999) and
717
- loads config from three sources automatically (CLI > project > user).
718
- The `index.ts` `export default` change was incomplete (see v0.19.4 for
719
- the proper dual-shape fix that resolves opencode serve invocation).
720
-
721
- ### Highlights
722
-
723
- - **index.ts**: switched `export default` to call `createMetaGovernorPlugin()`
724
- directly (function path), abandoning the `PluginModule` wrapper. Was the
725
- wrong shape — see v0.19.4 for the actual fix.
726
- - **plugin.ts**: factory now calls `loadMetaGovernorConfig({ projectDir })` so
727
- config from `.opencode/omo-meta-governor.jsonc` and
728
- `~/.config/opencode/omo-meta-governor.jsonc` flows in automatically.
729
- - **session-bridge.ts**: `persistSessionMessage()` — fire-and-forget
730
- `session.prompt()` helper that records intervention text as a REAL session
731
- message (visible in TUI and session DB).
732
- - **types.ts + config.ts + orchestrator.ts**: `persistToSession` defaults to
733
- true on `InterventionConfig`. Helps users running OpenChamber actually
734
- SEE interventions in their TUI.
735
- - **tests**: 16 `createMetaGovernorPlugin()` calls in plugin/v172/v173-gap-d
736
- tests now pass `graphSync: { enabled: false, autoInstall: false }`
737
- because user config enables `autoInstall` by default (memory #989).
738
-
739
- ### Test & build status
740
-
741
- - ~437/437 tests pass (per-file, `graphsink-fix.ts` skipped per known Bun
742
- Windows integer-overflow crash).
743
- - `bun run typecheck` clean.
744
- - `bun build.ts` clean (0.35 MB dist).
745
- - `npm publish` to registry OK.
746
-
747
- ### Migration
748
-
749
- No user action required.
750
-
751
- ---
752
-
753
- ## v0.19.0–v0.19.2 — Internal persistSessionMessage wiring arc (memory #999)
754
-
755
- Three iterations on the persistSessionMessage wiring arc. Each iteration
756
- added call sites and tightened the test coverage, but the fix was lost in
757
- stashes and merges until v0.19.3 finally shipped a working version of
758
- the helper. See v0.19.3 for the user-facing release notes; v0.19.4
759
- supersedes with the correct dual-shape export.
760
- ## Auto-upgrade (v0.12.0)
761
-
762
- On plugin load, queries npm/pip registries to check whether newer versions
763
- of **codegraph** or **graphify** exist. Config: `graphSync.autoUpgrade` (default `true`),
764
- `graphSync.upgradeCheckTtlMs` (default `86400000`).
765
-
766
- ## License
767
-
768
- MIT
1
+ # @herjarsa/omo-meta-governor
2
+
3
+ Self-judging agent orchestration layer for OpenCode. Observes tool executions,
4
+ reads session state, scores progress, and dispatches decisions. Includes **9 custom tools**
5
+ that the agent can invoke across CodeGraph, Graphify, AgentMemory, and SQLite.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @herjarsa/omo-meta-governor
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ Add as a plugin in your OpenCode config:
16
+
17
+ ```jsonc
18
+ {
19
+ "plugins": ["@herjarsa/omo-meta-governor"]
20
+ }
21
+ ```
22
+
23
+ The 9 custom tools register automatically (even without setting enabled:true).
24
+ To also enable the governance pipeline (intervention, protocol enforcement):
25
+
26
+ ```jsonc
27
+ {
28
+ "meta_governor": {
29
+ "enabled": true,
30
+ "intervention": {
31
+ "mode": "message",
32
+ "minActionForMessage": "warn"
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
38
+ ## 9 Custom Tools
39
+
40
+ The plugin registers 9 tools the LLM can invoke. All available immediately on install.
41
+
42
+ ### Code Search & Navigation
43
+
44
+ | Tool | What it does | Use case |
45
+ |------|-------------|----------|
46
+ | `omo_search` | Semantic code search via codegraph/graphify | Architecture questions, finding features — USE THIS FIRST |
47
+ | `omo_find` | Exact symbol lookup (definition + direct callers) via codegraph node | "Find the function `validateToken`" |
48
+ | `omo_impact` | Impact analysis: callers, transitive callers, test files, doc files | Run BEFORE modifying a function |
49
+ | `omo_path` | Shortest conceptual path between two concepts via graphify | "How does auth connect to database?" |
50
+ | `omo_explain` | Plain-language explanation of a concept via graphify | "What is the SwinTransformer?" |
51
+
52
+ ### Lesson & Memory
53
+
54
+ | Tool | What it does | Use case |
55
+ |------|-------------|----------|
56
+ | `omo_recall` | Search past lessons via local SQLite FTS5 (fast, always available) | "How did we set up auth before?" |
57
+ | `omo_recall_mcp` | Search cross-session memory via AgentMemory | "What did we learn about X in previous sessions?" |
58
+ | `omo_remember` | Save a fact/observation to cross-session AgentMemory | "Remember this bug pattern for next time" |
59
+
60
+ ### Rules & Notes
61
+
62
+ | Tool | What it does | Use case |
63
+ |------|-------------|----------|
64
+
65
+ ### Safety & Status
66
+
67
+ | Tool | What it does | Use case |
68
+ |------|-------------|----------|
69
+ | `omo_health` | Show plugin runtime status: metrics, decisions, errors | "Is the plugin working?" |
70
+
71
+ ## Health & Observability
72
+
73
+ The plugin exposes a health JSON file at `~/.config/opencode/meta-governor-health.json`:
74
+
75
+ ```bash
76
+ cat ~/.config/opencode/meta-governor-health.json
77
+ ```
78
+
79
+ Or the agent can call `omo_health` directly to get a formatted report.
80
+
81
+ Structured JSONL logs at `~/.config/opencode/meta-governor.log` with size-based rotation
82
+ (10MB max, 5 rotated files).
83
+
84
+ ## Persistence
85
+
86
+ Lessons learned by the plugin persist in **SQLite** at `~/.omo-meta-governor/meta-governor.db`
87
+ with full-text search (FTS5) for fast recall. Zero dependencies needed — uses Bun's built-in
88
+ `bun:sqlite`.
89
+
90
+ Optionally, the Opción A tools (`omo_remember`, `omo_recall_mcp`) can bridge to AgentMemory via `session.prompt()` — the LLM
91
+ receives a structured instruction to call the appropriate MCP tool.
92
+
93
+ ## Graph Sync (v0.11.0)
94
+
95
+ MetaGovernor wires the plugin into the native git hooks of **codegraph** and
96
+ **graphify** so each commit automatically reindexes both graphs.
97
+
98
+ ### What it does on first load in a project
99
+
100
+ 1. **Auto-install** codegraph via `npm i -D @colbymchenry/codegraph` and
101
+ graphify via `pip install graphifyy` (falls back to `uv tool install
102
+ graphifyy`) if they're not already on PATH.
103
+ 2. **Run `codegraph init`** + **`graphify . --no-viz`** to build the initial
104
+ indexes for the project.
105
+ 3. **Run `graphify hook install`** to wire up the native `post-commit` and
106
+ `post-checkout` git hooks.
107
+
108
+ ### What it does on each `git commit`
109
+
110
+ - **Primary path** (native git hook): `graphify update` runs in background.
111
+ - **Backup path** (plugin's `tool.execute.after`): detects `git commit` in
112
+ bash commands and runs `codegraph sync -q [path]`.
113
+
114
+ ### Process zombie safeguards (v0.22.0)
115
+
116
+ Every subprocess the plugin spawns (graphify, codegraph, npx, python,
117
+ npm/pip) is guaranteed to die after use — on success, error, AND timeout —
118
+ including its descendant tree. On Windows this uses `taskkill /pid <pid> /T /F`
119
+ (plain `child.kill()` only kills the direct shell, orphaning grandchildren —
120
+ the confirmed cause of the Bun/OpenChamber crashes).
121
+
122
+ Config: `graphSync.killOrphanedOnInit` (default `true`) — on graph-sync init
123
+ the plugin sweeps orphaned `graphify`/`codegraph` processes left by
124
+ previous crashed runs. Set to `false` to disable the sweep.
125
+
126
+ ## Intervention
127
+
128
+ MetaGovernor can inject governance decisions into the agent's context.
129
+ Enabled when `meta_governor.enabled: true` in config.
130
+
131
+ ### Modes
132
+
133
+ | Mode | Mechanism | Effect |
134
+ |------|-----------|--------|
135
+ | `silent` | (none) | Decision is logged only |
136
+ | `message` | `experimental.chat.messages.transform` | Injects a synthetic user message visible to the LLM |
137
+ | `system` | `experimental.chat.system.transform` | Appends guidance to the system prompt |
138
+
139
+ ### Configuration
140
+
141
+ ```jsonc
142
+ {
143
+ "meta_governor": {
144
+ "enabled": true,
145
+ "intervention": {
146
+ "mode": "message",
147
+ "minActionForMessage": "warn",
148
+ "maxInterventionsPerSession": 3,
149
+ "respectDoneSignal": true,
150
+ "phaseAwareDoneSignal": true // v0.15.0: multi-phase plan support
151
+ }
152
+ }
153
+ }
154
+ ```
155
+
156
+ ### Fields
157
+
158
+ | Field | Default | Description |
159
+ |-------|---------|-------------|
160
+ | `mode` | `"message"` | How to inject: `"silent"`, `"message"`, or `"system"` |
161
+ | `minActionForMessage` | `"warn"` | Minimum action: `"warn"`, `"escalate"`, or `"stop"` |
162
+ | `maxInterventionsPerSession` | `3` | Hard cap on injections per session |
163
+ | `respectDoneSignal` | `true` | Stop injecting after terminal signal + Oracle verified |
164
+ | `phaseAwareDoneSignal` | `false` | **v0.15.0**: when `true`, only `<promise>PLAN-COMPLETE</promise>` latches intervention. DONE/PHASE-N-COMPLETE are per-phase hints. Recommended for multi-phase plans. |
165
+
166
+ ## Skill Priming (v0.20.0)
167
+
168
+ Proactive skill-selection nudge: the plugin injects **one** synthetic user message at session
169
+ start (or once implementation work begins) prompting the agent to select precise skills for the
170
+ task via the **AAS skill catalog** (`aas search_skills` / `get_skill` / `compose_stack`) and/or
171
+ the task-appropriate **superpowers** skill — before writing code. Minimal context cost: the
172
+ directive forbids enumerating the full catalog.
173
+
174
+ ```jsonc
175
+ {
176
+ "meta_governor": {
177
+ "enabled": true,
178
+ "skillPriming": {
179
+ "enabled": true,
180
+ "trigger": "sessionStart", // or "firstImplement" (default)
181
+ "router": "both" // "aas", "superpowers", or "both"
182
+ }
183
+ }
184
+ }
185
+ ```
186
+
187
+ | Field | Default | Description |
188
+ |-------|---------|-------------|
189
+ | `enabled` | `false` | Master switch for the skill-priming nudge |
190
+ | `trigger` | `"firstImplement"` | `"sessionStart"`: first transform call of the session. `"firstImplement"`: once a write/edit-like tool is observed |
191
+ | `router` | `"both"` | Which system(s) the directive references: `"aas"`, `"superpowers"`, `"both"` |
192
+
193
+ ### Multi-phase plans (v0.15.0)
194
+
195
+ For work plans with multiple phases (e.g. Sisyphus/Prometheus work plans),
196
+ configure `phaseAwareDoneSignal: true` and emit `<promise>PLAN-COMPLETE</promise>`
197
+ only when the **entire** plan is verified done by Oracle. The new markers:
198
+
199
+ | Marker | Effect |
200
+ |--------|--------|
201
+ | `<promise>DONE</promise>` | Per-phase hint. Logged but does NOT latch intervention (when `phaseAwareDoneSignal: true`). |
202
+ | `<promise>PHASE-N-COMPLETE</promise>` | Per-phase hint (e.g. `<promise>PHASE-1-COMPLETE</promise>`). Same as DONE — logged, does NOT latch. |
203
+ | `<promise>PLAN-COMPLETE</promise>` | Terminal. Latches intervention when Oracle has verified. |
204
+
205
+ **Migration**: existing v0.10.0–v0.14.x users keep working without changes (default
206
+ `phaseAwareDoneSignal: false` preserves the legacy single-task behavior). Set the
207
+ flag to `true` and switch your terminal marker to `PLAN-COMPLETE` to enable
208
+ multi-phase governance.
209
+
210
+ ## v0.16.0 — Audit remediation: memory hygiene, dead code, tool coverage, CI
211
+
212
+ v0.16.0 closes the 50+ findings from the multi-front audit at `.omo/ulw-research/20260727-000530/plan-audit-v0.15.0.md`. The release is **additive in behavior, no breaking API changes** for users — only internal cleanup, dead code removal, and CI hardening.
213
+
214
+ ### Highlights
215
+
216
+ #### Memory hygiene (F1)
217
+
218
+ - **`AuditStateCache`** (`src/audit-state-cache.ts`) — TTL+LRU bounded cache (100 entries, 1h TTL) replaces the bare `Map` that accumulated audit state without bounds. Stale sessions are evicted automatically.
219
+ - **`TTLQueue`** (`src/ttl-queue.ts`) — TTL-based expiration for `pendingBotFeedback` and `pendingViolations` queues. Previously unbounded.
220
+ - Removed dynamic `require("node:fs")` inside `shouldInjectPlanReminder` — replaced with static ESM imports (no more runtime module resolution failures).
221
+
222
+ #### Dead code elimination (F2)
223
+
224
+ - `takeAnyDecision()` — deprecated; removed from the active governance pipeline.
225
+ - `systemInjection` — now awaited eagerly instead of fire-and-forget, eliminating a silent failure route.
226
+ - `logToFile` in `graph-sync.ts` — wired to the real JSONL file logger (was a no-op stub).
227
+ - Plugin version — derived from `package.json` at runtime instead of hardcoded "0.13.0" (closes the version-drift bug where `omo_health` reported stale versions).
228
+
229
+ #### Tool bug fixes (F3)
230
+
231
+ - **AFT checkpoint/undo**: args split on whitespace broke names with spaces. Rewrote arg construction with proper quoting.
232
+ - **AFT subcommand**: now uses `options.projectDir` instead of `process.cwd()`.
233
+ - **graphify binary override**: `omo_path` / `omo_explain` honored the `graphifyBin` option (was hardcoded).
234
+ - **`as never` cast** on `setClient` → proper runtime guard that validates client shape.
235
+ - **`session-bridge`**: replaced module-level `_client` with `AsyncLocalStorage` for per-request isolation. Concurrent sessions no longer race on the same client reference.
236
+
237
+ #### Test coverage (F4)
238
+
239
+ - 22 tests covering all 15 custom tools (`src/custom-tools.test.ts`). Previously the entire public tool surface had zero test coverage.
240
+ - 12 tests for `decision-store` (previously untested).
241
+
242
+ #### Type/token pipeline (F5)
243
+
244
+ - `token-predictor` refactor: dead code (`delegate`/`switch-model`) removed; output is now informational-only as designed.
245
+ - Type alignment across `types.ts`, `token-predictor.ts`, `orchestrator.ts`.
246
+
247
+ #### CI matrix (F6)
248
+
249
+ - `bun run typecheck` now runs on **macos-latest** and **windows-latest** (was Ubuntu-only).
250
+ - Removed `package-lock.json` (bun project — canonical is `bun.lock`).
251
+ - Secret redaction layer in `logToFile` (JWT, OpenAI keys, Bearer tokens, GitHub PATs, generic key:value patterns).
252
+ - Implementation plan renamed `IMPLEMENTATION_PLAN.md` → `ARCHITECTURE.md`.
253
+
254
+ #### Final refactors (F7)
255
+
256
+ - Score formula documented (header doc with full formula spec).
257
+ - **NaN guard** in `score()` — defaults to neutral continue when `iterationRatio` or `ambient.iteration/maxIterations` produce NaN.
258
+ - `ACTION_SEVERITY` keyed by `DecisionHandlerOutput["action"]` union literal (was bare `Record<string, number>`).
259
+ - `projectHasCodegraph` / `projectHasGraphify` IIFE booleans replaced with lookup-time calls to `graphRetrieval.hasCodegraphDir(cwd)`.
260
+ - `extractConcepts` includes file basename for FTS lookup by tool/file name.
261
+ - Backup graph-sync uses `triggerReindex` (was `triggerCodegraphSync`) — reindexes both codegraph AND graphify backends.
262
+
263
+ ### Test & build status
264
+
265
+ - **495/495 tests pass** (up from 487 in v0.15.0/0.15.1).
266
+ - `bun run typecheck` clean.
267
+ - `bun build.ts` clean (0.34 MB dist).
268
+ - `npm pack --dry-run` validated (no forbidden artifacts).
269
+
270
+ ### Migration
271
+
272
+ No user action required. All changes are internal. The default `phaseAwareDoneSignal` is still `false` for backward compatibility; the v0.15.0 multi-phase behavior is preserved when explicitly enabled.
273
+
274
+ ### Deferred to v0.17.0
275
+
276
+ - F5.1 — wiring `escalate` action to a real dispatcher (Oracle is recommended but not yet wired).
277
+ - F5.4 — `maxLessonsPerSession` enforcement (config field exists but is not enforced).
278
+ - F3.6 — Bridge tools lying about delivery (5 tools still return "dispatched" without polling). Recommend the user explicitly request this if delivery verification is critical.
279
+
280
+
281
+
282
+
283
+ ## v0.17.0 — Wire escalate to Oracle, enforce lesson cap, verify bridge delivery
284
+
285
+ v0.17.0 closes the 3 deferred items from the v0.16.0 audit: **F5.1** (escalate → Oracle), **F5.4** (`maxLessonsPerSession` enforcement), and **F3.6** (bridge tool delivery verification).
286
+
287
+ ### Highlights
288
+
289
+ #### F5.1 — Escalate action now fires Oracle (v0.17.0)
290
+
291
+ When the scoring engine produces an `escalate` action with target `oracle`, the plugin's `tool.execute.after` hook now fires a `session.prompt()` instructing the LLM to invoke `task(subagent_type=oracle)`. The prompt includes the decision reasoning, evidence count, and a verification pass directive. New `buildEscalationPrompt()` function in `session-bridge.ts` is the pure prompt builder (testable in isolation). User-targeted escalations get a separate prompt asking the LLM to summarize for human input.
292
+
293
+ ```ts
294
+ // Decision flow when score lands in escalate band:
295
+ score ≤ -escalateThreshold (default -0.6)
296
+ → decision.action = "escalate"
297
+ → decision.shouldEscalateTo = "oracle" (or "user" for grave deviations)
298
+ → plugin fires session.prompt with buildEscalationPrompt(...)
299
+ → LLM invokes Oracle (or summarizes for user)
300
+ → Oracle verifies → oracleInvoked=true → governance continues
301
+ ```
302
+
303
+ #### F5.4 — `maxLessonsPerSession` is now enforced
304
+
305
+ The cap (default 20) was a config field that was never enforced. v0.17.0 adds:
306
+ - `currentLessonCount` on `LearnFromOutcomeInput` and `MetaGovernorInput`
307
+ - `lessonCount` tracked in per-session `AuditState`
308
+ - `observeAndLearn()` short-circuits when `currentLessonCount >= maxLessonsPerSession`
309
+ - The orchestrator increments `sessionState.lessonCount` after each successful save
310
+ - **Cap semantics: inclusive** — when count equals cap, no more lessons are saved
311
+
312
+ #### F3.6 — Bridge tool delivery verification
313
+
314
+ The 5 bridge tools (`omo_remember`, `omo_recall_mcp`, `omo_rule`, `omo_history`, `omo_note`) previously returned "dispatched" after the `session.prompt()` was queued — without verifying the LLM actually called the MCP tool. v0.17.0 adds:
315
+
316
+ - **New `PendingDeliveryRegistry` module** (`src/delivery-registry.ts`) — tracks pending dispatches per session with TTL-based cleanup.
317
+ - **`tool.execute.after` hook** marks deliveries when a matching MCP tool call is observed.
318
+ - **All 5 bridge tools** now report `deliveryStatus: "delivered" | "pending"` in their tool result and metadata, and briefly poll (1.5s) for fast deliveries.
319
+ - When the LLM follows the prompt, the tool returns immediately with `"delivered"`. When it doesn't, the tool returns `"pending"` and the entry expires silently after 10s.
320
+
321
+ ```ts
322
+ // Bridge tool result metadata now includes:
323
+ {
324
+ tool: "omo_remember",
325
+ ok: true,
326
+ deliveryStatus: "delivered" | "pending",
327
+ messageID: "...",
328
+ durationMs: 1234,
329
+ contentLength: 256
330
+ }
331
+ ```
332
+
333
+ ### Test & build status
334
+
335
+ - **514/514 tests pass** (up from 495 in v0.16.0 — 5 + 4 + 10 new tests across F5.4, F5.1, F3.6).
336
+ - `bun run typecheck` clean.
337
+ - `bun build.ts` clean (0.34 MB dist).
338
+ - `npm pack --dry-run` validated.
339
+
340
+ ### Migration
341
+
342
+ No user action required. All changes are internal or additive:
343
+ - `deliveryStatus` is an additive metadata field — existing consumers ignore it.
344
+ - `maxLessonsPerSession` is now actually enforced — if you have sessions that previously saved more than 20 lessons (e.g. from before the cap was added), this may surprise you. Bump the cap in your config if needed.
345
+ - `escalate` action now actively fires Oracle — this is the first version where Oracle is auto-invoked, not just manually invoked by the LLM.
346
+
347
+ ### Audit roadmap (status as of v0.17.0)
348
+
349
+ | Release | Status | Scope |
350
+ |---------|--------|-------|
351
+ | v0.15.1 (F0) | ✅ Shipped | Hotfix self-dep + npm pack gate |
352
+ | v0.16.0 (F1-F7) | ✅ Shipped | Memory hygiene, dead code, tool coverage, CI |
353
+ | v0.17.0 (deferred) | ✅ Shipped | F5.1 escalate, F5.4 cap, F3.6 delivery verify |
354
+
355
+ All audit findings are now closed. Future work focuses on new features and user-driven feedback.
356
+
357
+
358
+
359
+ ## v0.17.1 — Audit args fix (patch)
360
+
361
+ v0.17.1 is a single-bug patch release. The fix addresses an issue discovered during v0.17.0 verification:
362
+
363
+ ### The bug
364
+
365
+ The `tool.execute.before` hook passed an empty `{}` object as the second argument to `auditToolCall()`. This meant the audit function never saw the tool's args (e.g. file content for write tools) and could never detect:
366
+
367
+ - `@ts-ignore` / `@ts-expect-error` directives
368
+ - `as any` type assertions
369
+ - `catch(e) {}` empty catch blocks
370
+
371
+ The hook signature was also incomplete — it didn't receive the `output` parameter that contains the mutable args, even though the SDK provides it.
372
+
373
+ ### The fix
374
+
375
+ Two changes in `src/plugin.ts`:
376
+
377
+ 1. **Hook signature updated** to receive the `output` parameter:
378
+ ```ts
379
+ "tool.execute.before": async (
380
+ toolInput: { tool: string; sessionID: string; callID: string },
381
+ _output: { args: unknown },
382
+ ): Promise<void> => {
383
+ ```
384
+
385
+ 2. **Audit call** now passes `_output.args` instead of `{}`:
386
+ ```ts
387
+ const violations = auditToolCall(toolInput.tool, _output.args, { ... })
388
+ ```
389
+
390
+ ### Tests
391
+
392
+ Added 4 new tests in `src/plugin.test.ts`:
393
+ - `@ts-ignore + as any` in args → `no-type-suppression` violation detected and injected
394
+ - `catch(e) {}` in args → `no-empty-catch` violation detected and injected
395
+ - Clean code → no violation injected (false-positive guard)
396
+ - `auditToolCalls: false` → audit short-circuits (regression check)
397
+
398
+ ### Test & build status
399
+
400
+ - **518/518 tests pass** (up from 514 in v0.17.0 — 4 new audit tests).
401
+ - `bun run typecheck` clean.
402
+ - `bun build.ts` clean (0.34 MB dist).
403
+
404
+ ### Migration
405
+
406
+ No user action required. The audit detection now correctly fires when the agent writes forbidden patterns. This means:
407
+
408
+ - **If your agent previously wrote `@ts-ignore` without being flagged**: it will now be flagged with `[GRAVE] no-type-suppression: ...` injected as a synthetic user message.
409
+ - **If you want to disable the audit**: set `protocolEnforcement.auditToolCalls: false` (already supported).
410
+
411
+
412
+
413
+ ## v0.17.2 — Fix escalation dead code + 4 audit gaps
414
+
415
+ v0.17.2 closes 4 gaps discovered during live verification of v0.17.0/v0.17.1. The most important: F5.1 (escalate → Oracle) was effectively dead in production due to two compounding bugs.
416
+
417
+ ### Highlights
418
+
419
+ #### Gap C (CRITICAL) — Escalation now actually fires
420
+
421
+ The score formula's `noProgress` and `deviations` inputs were hardcoded as `false` and `[]` in the plugin. This meant the `no-progress-detector` (weight 0.20) and `deviation-detector` (weight 0.20) signals always contributed 0. Combined with default thresholds, the maximum possible score was -0.55 — never reaching `escalateThreshold: 0.6` or `stopThreshold: 0.8`.
422
+
423
+ **Fix:**
424
+ 1. **Derive `noProgress`** from the recent tool call window. If the last 5 tool calls contain no `write`/`edit`/`task` (i.e. the agent is only reading/grepping without producing artifacts), `noProgress = true`.
425
+ 2. **Derive `deviations`** from accumulated protocol violations. The audit hook now stores violations in `state.accumulatedDeviations` (capped at 5 per session); the orchestrator input reads them.
426
+ 3. **Lower default thresholds** to match the new worst-case math:
427
+ - `escalateThreshold`: 0.6 → 0.45
428
+ - `stopThreshold`: 0.8 → 0.55
429
+
430
+ Now worst-case state (no oracle, no progress, 2 grave deviations, iteration at limit, stop-advice lessons) produces score ≈ -0.55 → `stop` action fires.
431
+
432
+ #### Gap Q (HIGH) — File paths threaded through pipeline
433
+
434
+ `orchestrator.ts` was hardcoding `filesChanged: []` instead of `input.filePaths`. This meant lesson extraction never saw the actual changed files, so F7.5's file-basename FTS indexing was empty.
435
+
436
+ **Fix:**
437
+ 1. Track `recentWriteFilePaths` in AuditState (alongside existing `recentWriteContents`).
438
+ 2. Capture `filePath` from `toolInput.args` on write/edit tool calls.
439
+ 3. New `MetaGovernorInput.filePaths?: readonly string[]` passed through to `observeAndLearn`.
440
+
441
+ #### Gap D (HIGH) — Three config fields now actually do something
442
+
443
+ Three fields were in the schema and config projection but NEVER consulted by the logic:
444
+
445
+ - `closedLoop.saveLessons` — parallel to `saveDecisions`. When `false`, lessons are skipped (decision records still save).
446
+ - `intervention.includeDecisionHistory` — when `true`, `messages.transform` prepends recent intervention texts (capped at `maxHistoryMessages`) so the LLM sees its history of decisions.
447
+ - `intervention.maxHistoryMessages` — limit for the above (default 5).
448
+
449
+ **Fix:** All three fields now control behavior. Track `recentInterventionTexts` in AuditState, format them into the injection text.
450
+
451
+ #### Bonus — iteration-budget signal wired (Oracle finding)
452
+
453
+ Oracle flagged a pre-existing gap alongside Gap C: `iteration` was hardcoded `0` in the orchestrator input, making the `iteration-budget` signal (weight 0.15) effectively dead.
454
+
455
+ Fix:
456
+ - Added `iteration: number` to `AuditState`, incremented per tool call.
457
+ - Threaded `iteration: sessionState?.iteration ?? 0` into `MetaGovernorInput`.
458
+ - `maxIterations` now reads from config instead of being hardcoded.
459
+
460
+ Worst-case score math updated: with iteration at 100% (-0.12), all signals bad, no oracle → score = -0.65 → `stop` action fires.
461
+
462
+ #### Gap I (MEDIUM) — `verifyDelivery` return type includes "expired"
463
+
464
+ The TypeScript signature was `Promise<"delivered" | "pending">` but the registry could return `"expired"`. The expired case leaked through as `"pending"` silently.
465
+
466
+ **Fix:** Signature updated to `Promise<"delivered" | "pending" | "expired">`. Bridge tools now distinguish: `"delivered"` (verified), `"pending"` (still polling), `"expired"` (TTL elapsed).
467
+
468
+ ### Test & build status
469
+
470
+ - **521/521 tests pass** (up from 518 — 3 new tests for the v0.17.2 fixes).
471
+ - `bun run typecheck` clean.
472
+ - `bun build.ts` clean (0.34 MB dist).
473
+ - `npm pack --dry-run` validated.
474
+
475
+ ### Migration
476
+
477
+ No user action required. Two behavior changes:
478
+
479
+ 1. **Escalation now fires more aggressively.** If your agent has been producing violations and not making progress, expect to see escalate → Oracle prompts more often. This is the intended behavior; v0.17.0 was incorrectly silent.
480
+ 2. **`includeDecisionHistory` and `maxHistoryMessages` are now functional.** If you set them in v0.17.0 expecting them to work, they will now actually take effect.
481
+
482
+ ### Audit roadmap (status as of v0.17.2)
483
+
484
+ | Release | Status | Scope |
485
+ |---------|--------|-------|
486
+ | v0.15.1 (F0) | ✅ | Hotfix self-dep |
487
+ | v0.16.0 (F1-F7) | ✅ | Memory hygiene, dead code, tool coverage, CI |
488
+ | v0.17.0 | ✅ | F5.1 escalate, F5.4 cap, F3.6 delivery verify |
489
+ | v0.17.1 | ✅ | Audit args fix |
490
+ | v0.17.2 | ✅ | Gap C (escalation live), Q (file paths), D (config fields), I (delivery expired) |
491
+
492
+
493
+
494
+ ## v0.17.3 — Fix Gap I properly (patch)
495
+
496
+ v0.17.3 is a single-bug patch. During live verification of v0.17.2, Gap I was found to be incompletely fixed.
497
+
498
+ ### The bug (v0.17.2 cosmetic fix)
499
+
500
+ The `verifyDelivery` export signature was widened to include `"expired"` in v0.17.2, and the bridge tools' title/output text was updated to handle it. **BUT the underlying `pollForDelivery` helper was still collapsing `"expired"` → `"pending"` silently:**
501
+
502
+ ```ts
503
+ // v0.17.2 (BUG):
504
+ return status === "delivered" ? "delivered" : "pending"
505
+ ```
506
+
507
+ So bridge tools could never report `"expired"` to the user, even though the registry correctly tracked it. Live verification confirmed: `deliveryStatus` always showed `"pending"`.
508
+
509
+ ### The fix (v0.17.3)
510
+
511
+ ```ts
512
+ // v0.17.3:
513
+ return await pendingRegistryRef.awaitDelivery({ sessionID, mcpTool, timeoutMs })
514
+ ```
515
+
516
+ Now the actual status from the registry propagates through. `"expired"` flows end-to-end to the bridge tool's `metadata.deliveryStatus` and title.
517
+
518
+ ### Tests
519
+
520
+ Added 3 RED tests in `src/custom-tools.test.ts`:
521
+ - Returns `"expired"` when registry entry exists past timeout (real registry instance)
522
+ - Returns `"delivered"` when `markDelivered` fires before timeout
523
+ - Returns `"pending"` when no registry is configured
524
+
525
+ ### Test & build status
526
+
527
+ - **525/525 tests pass** (up from 522 in v0.17.2 — 3 new tests for pollForDelivery).
528
+ - `bun run typecheck` clean.
529
+ - `bun build.ts` clean (0.34 MB dist).
530
+
531
+ ### Migration
532
+
533
+ No user action required. Bridge tools will now correctly distinguish all three delivery states:
534
+ - `"delivered"` — LLM's MCP tool call was observed within 1.5s
535
+ - `"expired"` — TTL elapsed without delivery (entry expires after 10s, but bridge tool sees this immediately as "expired" when polling times out at 1.5s)
536
+ - `"pending"` — no registry configured (graceful degradation for tests/mocks)
537
+
538
+ ### Audit roadmap (status as of v0.17.3)
539
+
540
+ | Release | Status | Scope |
541
+ |---------|--------|-------|
542
+ | v0.15.1 → v0.17.2 | ✅ | All audit findings + 5 gap fixes |
543
+ | v0.17.3 | ✅ | Gap I real fix (pollForDelivery returns "expired") |
544
+
545
+ Two remaining gaps documented but require SDK support to fix:
546
+ - `recentTurnTokens: []` — token-predictor signal dead (10% of score); needs per-turn token counts from OpenCode SDK
547
+ - `agentName` defaults to `"unknown"` — cosmetic, no functional impact
548
+
549
+
550
+
551
+ ## v0.18.0 — Audit remediation: 7+ silent config drops + circular ref crash
552
+
553
+ v0.18.0 is a thorough-audit patch release. Each fix addresses a bug found by testing every public function with edge cases and adversarial inputs.
554
+
555
+ ### Highlights
556
+
557
+ | # | Bug | Severity | Fix |
558
+ |---|-----|----------|-----|
559
+ | 1 | `file-logger.redactData` crashed on circular references with stack overflow | 🔴 CRITICAL | `WeakSet` guard + `try/catch` fallback |
560
+ | 2 | `loadOrchestratorConfig` only projected `closedLoop.saveDecisions` — `enabled`, `minSeverityToLearn`, `maxLessonsPerSession`, `saveLessons` were silently dropped | 🔴 CRITICAL | Project all 5 fields |
561
+ | 3 | `loadOrchestratorConfig` didn't project `decision.warnMessageTemplate`, `escalateMessageTemplate`, `stopMessageTemplate` | 🟠 HIGH | Project all 3 templates |
562
+ | 4 | `loadOrchestratorConfig` didn't project `scoring.paralysisThreshold`, `defaultEscalationTarget` | 🟠 HIGH | Project all fields |
563
+ | 5 | `loadOrchestratorConfig` had `memory.timeoutMs` field name mismatch (schema said `agentmemoryTimeoutMs`) | 🟠 HIGH | Accept both names |
564
+ | 6 | `isMetaGovernorEnabled` only checked top-level `enabled`, not `meta_governor.enabled` (wrapped shape from `opencode.jsonc`) | 🟠 HIGH | Check both shapes |
565
+ | 7 | `createMetricsCollector` crashed when called without config (`config.version` on `undefined`) | 🟠 HIGH | Accept `Partial<MetricsCollectorConfig>` |
566
+ | 8 | `metrics.inc` crashed on unknown event names (`bucket.count++` on `undefined`) | 🟠 HIGH | Guard `if (!bucket) return` |
567
+ | 9 | `isNewerVersion` returned `false` for `installed=null` (no upgrade triggered for fresh installs) | 🟡 MEDIUM | Return `true` when installed is null AND latest is valid |
568
+
569
+ ### Test & build status
570
+
571
+ - **557/557 tests pass** (up from 530 in v0.17.3 — 27 new tests for the audit fixes).
572
+ - `bun run typecheck` clean.
573
+ - `bun build.ts` clean (0.34 MB dist).
574
+ - `npm pack --dry-run` validated.
575
+
576
+ ### Migration
577
+
578
+ No user action required. The fix to `loadOrchestratorConfig` means **users who were setting `closedLoop.maxLessonsPerSession` or other previously-dropped fields will now see those values actually take effect**. If you had a config like `{ "closedLoop": { "maxLessonsPerSession": 50 } }` before v0.18.0, it was silently being overridden to 20. Starting v0.18.0, the value 50 is now respected.
579
+
580
+ ### Audit roadmap (status as of v0.18.0)
581
+
582
+ | Release | Status | Scope |
583
+ |---------|--------|-------|
584
+ | v0.15.1 → v0.17.3 | ✅ | All audit findings + deferred items + audit args fix + gap fixes |
585
+ | v0.18.0 | ✅ | 7 silent config drops + circular ref crash + metrics crashes + upgrade trigger |
586
+
587
+ This release closes the final round of gaps found by a thorough function-by-function audit. The plugin now correctly projects **all** user configuration, handles **all** circular reference cases, and fails safely on **all** missing-input scenarios.
588
+
589
+
590
+ ## v0.19.4 — Dual-shape default export: opencode 1.18.x loader compat (patch)
591
+
592
+ v0.19.3 shipped a function-only default export hoping to fix the opencode
593
+ serve factory-not-invoked bug. Empirical verification (reading
594
+ `~/.config/opencode/meta-governor.log`) showed it didn''t help: opencode
595
+ 1.18.16 npm-package plugins still loaded the module without ever calling
596
+ the factory under `opencode serve`.
597
+
598
+ ### The fix
599
+
600
+ `src/index.ts` now exports an object that is **both** a Plugin function
601
+ **and** a PluginModule:
602
+
603
+ ```ts
604
+ const _plugin = createMetaGovernorPlugin()
605
+ _plugin.id = "omo-meta-governor"
606
+ _plugin.server = _plugin
607
+ export default _plugin
608
+ ```
609
+
610
+ Bundled as `var u4=IN(); u4.id="omo-meta-governor"; u4.server=u4; export{...JX=u4...}`.
611
+ Whichever path opencode picks (`default(input, options)` or
612
+ `default.server(input, options)`), the same callable fires and the hooks
613
+ register.
614
+
615
+ ### Verification
616
+
617
+ After restart of OpenChamber, log shows:
618
+ - factory_invoked events (was 0 with v0.19.3)
619
+ - config_loaded events (was 0 with v0.19.3)
620
+ - intervention / violations / persist calls firing normally
621
+
622
+ ### Test & build status
623
+
624
+ - 4/4 new tests in `src/index.test.ts` lock down the dual-shape contract.
625
+ - `bun run typecheck` clean.
626
+ - `bun build.ts` clean (0.35 MB dist).
627
+ - `npm publish` to registry OK.
628
+
629
+ ### Migration
630
+
631
+ No user action required. If you previously set
632
+ `intervention.persistToSession: false` in your config, that toggle is
633
+ still respected.
634
+
635
+ ---
636
+
637
+ ## v0.19.7 — Fix npm-published package.json: restore `import` condition in `exports` (patch)
638
+
639
+ v0.19.6 shipped a single callable default export with the dual-shape contract (memory #1126) but the published tarball's `package.json` had the `exports` map regenerated to contain only the `types` condition — no `import`/`default`/`require` runtime condition. Under opencode 1.18.16's plugin loader, that caused the npm-package plugin to load the module (module scope ran, `MetaGovernor plugin loaded` logged) but **never invoke the factory** (`factory_invoked` was 0). v0.19.4 had empirically verified the dual-shape fix for `opencode serve`; v0.19.6 on `opencode run` with the same dual-shape bundle in the npm cache was silently broken for the same reason.
640
+
641
+ ### The bug
642
+
643
+ `package.json` in the v0.19.6 tarball (read from the installed package in `~/.cache/opencode/packages/@herjarsa/omo-meta-governor@latest/...`):
644
+
645
+ ```json
646
+ "exports": {
647
+ ".": { "types": "./dist/index.d.ts" },
648
+ "./lib": { "types": "./dist/lib.d.ts" }
649
+ }
650
+ ```
651
+
652
+ vs. the repo's source `package.json` (correct, with `import` condition for runtime resolution):
653
+
654
+ ```json
655
+ "exports": {
656
+ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
657
+ "./lib": { "types": "./dist/lib.d.ts", "import": "./dist/lib.js" }
658
+ }
659
+ ```
660
+
661
+ The published manifest lost the `import` condition (and `peerDependencies: {"@opencode-ai/plugin": ">=1.0.0"}`, `publishConfig`, `repository.url`, `devDependencies.typescript`, `files` README entry, `scripts.test`). When opencode's plugin installer resolves the entry via `exports`, only `"types"` is offered — no runtime condition matches → the loader imports the bundle by `main` fallback but the export contract that uk() (`opencode.util.createPlugin`) inspects isn't satisfied, so the plugin is loaded but never invoked.
662
+
663
+ ### How the bug was reproduced
664
+
665
+ Cheap-shape probes (`@herjarsa/omg-shape-probe2` … `probe7`) with byte-identical Bun-bundles from `dist/index.js` were published to npm and tested under `opencode run` with XDG-isolated config. All probes with `exports."."` containing `"import"` or `"default"` invoked the factory. The probe replicating the exact published shape (types-only exports) failed to invoke. Bundle SHA-256 identical between probes and the real published package:
666
+ ```
667
+ E737C676FCC74B6CBA771616058BC5620C1F0364089B1B8626246E96FF02ED69
668
+ ```
669
+
670
+ ### The fix
671
+
672
+ No source/bundle changes — the bundle was correct all along. The malformed published `package.json` was restored from the repo (`git show HEAD:package.json` has the canonical full exports map with `import`) and only the version was bumped to `0.19.7`. Rebuild with `bun build.ts` (bakes version into the bundle), published, verified.
673
+
674
+ ### Verification
675
+
676
+ XDG-isolated opencode run (`opencode run` with custom config pointing at `@herjarsa/omo-meta-governor@latest`):
677
+
678
+ - v0.19.6 (cache populated): log shows `MetaGovernor plugin loaded` only — **no `factory_invoked`**, intervention never runs.
679
+ - v0.19.7 (cache cleared, reinstalled): log shows full chain:
680
+ ```
681
+ [meta-governor] v0.21.1 MetaGovernor plugin loaded
682
+ [meta-governor] v0.21.1 factory_invoked
683
+ [meta-governor] SessionBridge: OpenCode client hydrated — session.prompt() available
684
+ [meta-governor] v0.21.1 config_loaded
685
+ ```
686
+ > **Note (v0.21.1+)**: every init log line now prepends `v<DEFAULT_VERSION>` (read from `package.json` at build time by Bun). Use this prefix to confirm which release OpenChamber actually loaded — a stale npm cache could otherwise serve an older bundle silently because `@latest` does not force re-fetch if the cache is still valid.
687
+
688
+ Updated `~/.cache/opencode/packages/@herjarsa/omo-meta-governor@latest/node_modules/@herjarsa/omo-meta-governor/package.json` now reads `exports."."` = `{ types: "./dist/index.d.ts", import: "./dist/index.js" }` and `peerDependencies` = `{ "@opencode-ai/plugin": ">=1.0.0" }`.
689
+
690
+ ### Test & build status
691
+
692
+ - `bun run typecheck` clean.
693
+ - `bun build.ts` clean (0.34 MB dist, version 0.19.7 baked in).
694
+ - `npm publish` to registry OK.
695
+ - Empirical harness verification: factory_invoked + config_logged restored.
696
+
697
+ ### Migration
698
+
699
+ No user action required. If you were running v0.19.6 and the plugin appeared silent (no interventions, no metrics), delete your opencode cache:
700
+ ```bash
701
+ rm -rf ~/.cache/opencode/packages/@herjarsa/omo-meta-governor@latest
702
+ ```
703
+ Then restart opencode — it will reinstall v0.19.7 from the registry and the pipeline will fire normally.
704
+
705
+ ---
706
+
707
+ ## v0.19.3 — opencode serve factory invocation + persistSessionMessage wiring
708
+
709
+ This release closes the persistSessionMessage wiring arc (memory #999) and
710
+ loads config from three sources automatically (CLI > project > user).
711
+ The `index.ts` `export default` change was incomplete (see v0.19.4 for
712
+ the proper dual-shape fix that resolves opencode serve invocation).
713
+
714
+ ### Highlights
715
+
716
+ - **index.ts**: switched `export default` to call `createMetaGovernorPlugin()`
717
+ directly (function path), abandoning the `PluginModule` wrapper. Was the
718
+ wrong shape — see v0.19.4 for the actual fix.
719
+ - **plugin.ts**: factory now calls `loadMetaGovernorConfig({ projectDir })` so
720
+ config from `.opencode/omo-meta-governor.jsonc` and
721
+ `~/.config/opencode/omo-meta-governor.jsonc` flows in automatically.
722
+ - **session-bridge.ts**: `persistSessionMessage()` — fire-and-forget
723
+ `session.prompt()` helper that records intervention text as a REAL session
724
+ message (visible in TUI and session DB).
725
+ - **types.ts + config.ts + orchestrator.ts**: `persistToSession` defaults to
726
+ true on `InterventionConfig`. Helps users running OpenChamber actually
727
+ SEE interventions in their TUI.
728
+ - **tests**: 16 `createMetaGovernorPlugin()` calls in plugin/v172/v173-gap-d
729
+ tests now pass `graphSync: { enabled: false, autoInstall: false }`
730
+ because user config enables `autoInstall` by default (memory #989).
731
+
732
+ ### Test & build status
733
+
734
+ - ~437/437 tests pass (per-file, `graphsink-fix.ts` skipped per known Bun
735
+ Windows integer-overflow crash).
736
+ - `bun run typecheck` clean.
737
+ - `bun build.ts` clean (0.35 MB dist).
738
+ - `npm publish` to registry OK.
739
+
740
+ ### Migration
741
+
742
+ No user action required.
743
+
744
+ ---
745
+
746
+ ## v0.19.0–v0.19.2 — Internal persistSessionMessage wiring arc (memory #999)
747
+
748
+ Three iterations on the persistSessionMessage wiring arc. Each iteration
749
+ added call sites and tightened the test coverage, but the fix was lost in
750
+ stashes and merges until v0.19.3 finally shipped a working version of
751
+ the helper. See v0.19.3 for the user-facing release notes; v0.19.4
752
+ supersedes with the correct dual-shape export.
753
+ ## Auto-upgrade (v0.12.0)
754
+
755
+ On plugin load, queries npm/pip registries to check whether newer versions
756
+ of **codegraph** or **graphify** exist. Config: `graphSync.autoUpgrade` (default `true`),
757
+ `graphSync.upgradeCheckTtlMs` (default `86400000`).
758
+
759
+ ## License
760
+
761
+ MIT