oh-my-harness 0.18.0 β†’ 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +30 -340
  2. package/dist/catalog/blocks/index.js +2 -0
  3. package/dist/catalog/blocks/loop-guard.d.ts +10 -0
  4. package/dist/catalog/blocks/loop-guard.js +120 -0
  5. package/dist/catalog/blocks/tdd-guard.js +1 -1
  6. package/dist/cli/commands/config.js +11 -2
  7. package/dist/cli/commands/loop.d.ts +54 -0
  8. package/dist/cli/commands/loop.js +221 -0
  9. package/dist/cli/index.js +56 -0
  10. package/dist/cli/tui/open-browser.d.ts +19 -0
  11. package/dist/cli/tui/open-browser.js +34 -0
  12. package/dist/cli/tui/provider-setup.js +158 -75
  13. package/dist/core/generator.js +64 -3
  14. package/dist/core/harness-converter-v2.js +24 -0
  15. package/dist/core/harness-schema.d.ts +118 -1
  16. package/dist/core/harness-schema.js +45 -0
  17. package/dist/core/merged-config.d.ts +33 -0
  18. package/dist/core/uninstall.js +15 -0
  19. package/dist/generators/codex-config.js +2 -1
  20. package/dist/generators/loop-assets.d.ts +14 -0
  21. package/dist/generators/loop-assets.js +15 -0
  22. package/dist/loop/classify.d.ts +36 -0
  23. package/dist/loop/classify.js +46 -0
  24. package/dist/loop/ledger.d.ts +19 -0
  25. package/dist/loop/ledger.js +33 -0
  26. package/dist/loop/protocol.d.ts +15 -0
  27. package/dist/loop/protocol.js +81 -0
  28. package/dist/loop/runtime.d.ts +35 -0
  29. package/dist/loop/runtime.js +112 -0
  30. package/dist/loop/state.d.ts +79 -0
  31. package/dist/loop/state.js +256 -0
  32. package/dist/loop/stop.d.ts +36 -0
  33. package/dist/loop/stop.js +127 -0
  34. package/dist/loop/supervisor.d.ts +27 -0
  35. package/dist/loop/supervisor.js +188 -0
  36. package/dist/loop/worktree.d.ts +38 -0
  37. package/dist/loop/worktree.js +166 -0
  38. package/dist/nl/config-store.d.ts +3 -1
  39. package/dist/nl/config-store.js +6 -1
  40. package/dist/nl/list-models.d.ts +15 -0
  41. package/dist/nl/list-models.js +75 -0
  42. package/dist/nl/parse-intent.js +13 -1
  43. package/dist/nl/provider-registry.d.ts +6 -0
  44. package/dist/nl/provider-registry.js +59 -30
  45. package/dist/nl/providers/claude-api.js +1 -1
  46. package/dist/nl/providers/codex-oauth-api.d.ts +2 -1
  47. package/dist/nl/providers/codex-oauth-api.js +3 -3
  48. package/dist/nl/providers/codex-oauth.js +1 -1
  49. package/dist/nl/providers/openai-api.d.ts +16 -1
  50. package/dist/nl/providers/openai-api.js +67 -23
  51. package/dist/nl/providers/openrouter-oauth.d.ts +13 -0
  52. package/dist/nl/providers/openrouter-oauth.js +38 -0
  53. package/package.json +1 -1
package/README.md CHANGED
@@ -2,46 +2,40 @@
2
2
 
3
3
  # 🐴 oh-my-harness
4
4
 
5
- **Tame your AI coding agents with natural language.**
5
+ **CLAUDE.md is a request. oh-my-harness is enforcement.**
6
+
7
+ One command turns "TDD enforced, block dangerous commands" into hooks that actually **block** your AI coding agent β€” for Claude Code, Codex and Pi at once.
6
8
 
7
9
  [![npm version](https://img.shields.io/npm/v/oh-my-harness.svg)](https://www.npmjs.com/package/oh-my-harness)
8
10
  [![npm downloads](https://img.shields.io/npm/dm/oh-my-harness.svg)](https://www.npmjs.com/package/oh-my-harness)
9
11
  [![CI](https://github.com/kyu1204/oh-my-harness/actions/workflows/ci.yml/badge.svg)](https://github.com/kyu1204/oh-my-harness/actions/workflows/ci.yml)
10
12
  [![License: MIT](https://img.shields.io/github/license/kyu1204/oh-my-harness.svg)](LICENSE)
11
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.7+-blue.svg)](https://www.typescriptlang.org/)
12
- [![Node.js](https://img.shields.io/badge/Node.js-20+-green.svg)](https://nodejs.org/)
13
13
 
14
- > Stop hand-writing CLAUDE.md files. Describe your project, get enforced guardrails.
14
+ <img src="docs/demo.gif" alt="omh init generates hooks; an AI agent's commit, rm -rf and untested edit are blocked" width="900">
15
15
 
16
16
  </div>
17
17
 
18
- ---
19
-
20
- ## 😀 The Problem
21
-
22
- Every AI code agent needs configuration files. Claude Code needs `CLAUDE.md` + hooks. Cursor needs `.cursorrules`. Codex needs `AGENTS.md`. You end up:
18
+ ```bash
19
+ npx oh-my-harness init "React + FastAPI, TDD enforced, lint on save"
20
+ ```
23
21
 
24
- - πŸ“‹ Copy-pasting config files between projects
25
- - πŸ”“ Forgetting to set up TDD enforcement hooks
26
- - πŸ’₯ Agents committing code without running tests
27
- - 🎲 Inconsistent behavior across projects
22
+ That is the whole setup. Your agent now hits a wall when it tries to:
28
23
 
29
- ## ✨ The Solution
24
+ | Agent tries to... | Result |
25
+ |---|---|
26
+ | `git commit` while tests fail | β›” **Blocked** |
27
+ | edit `src/foo.ts` before touching `foo.test.ts` | β›” **Blocked** (TDD guard) |
28
+ | run `rm -rf /`, `chmod -R 777`, or any pattern you list | β›” **Blocked** |
29
+ | write into `node_modules/`, `.next/`, `dist/` | β›” **Blocked** |
30
+ | commit on a branch already merged to main | β›” **Blocked** |
31
+ | save a file | βœ… auto-lint |
32
+ | push a branch | βœ… auto-PR |
30
33
 
31
- ```bash
32
- oh-my-harness init "React + FastAPI fullstack, TDD enforced, lint on save"
33
- ```
34
+ Every decision is logged to `.omh/state/events.jsonl` β€” `omh stats` shows what your agent tried and what got stopped.
34
35
 
35
- That's it. oh-my-harness generates **enforced guardrails** β€” not just instructions, but hooks that actually **block** bad behavior:
36
+ ## Why not just write CLAUDE.md?
36
37
 
37
- - ❌ Commit without tests passing? **Blocked.**
38
- - ❌ Edit source without updating tests first? **Blocked.** _(TDD Guard)_
39
- - ❌ Write to `node_modules/` or `.next/`? **Blocked.**
40
- - ❌ Run `rm -rf /`? **Blocked.**
41
- - ❌ Commit on a merged branch? **Blocked.**
42
- - βœ… Auto-lint on every file save? **Done.**
43
- - βœ… Auto-create PR after push? **Done.**
44
- - πŸ“Š Track all hook events for analytics? **Done.**
38
+ Because agents read instructions and then forget them halfway through a long session. A rule in a markdown file is a suggestion; a `PreToolUse` hook that returns `{"decision":"block"}` is a fact. oh-my-harness writes the markdown **and** the hooks, from one `harness.yaml`, and keeps them in sync (`omh sync --check` fails CI when they drift). Side-by-side with rule-sync tools and git hooks: [docs/comparison.md](docs/comparison.md).
45
39
 
46
40
  ---
47
41
 
@@ -64,131 +58,7 @@ omh diff # Preview what `omh sync` would change
64
58
  omh sync --check # Fail (exit 1) if generated files are out of date β€” CI gate
65
59
  ```
66
60
 
67
- ### πŸ”„ Keeping generated files in sync
68
-
69
- `harness.yaml` is the source of truth, so the committed `CLAUDE.md`, hooks, and
70
- runtime configs can drift if someone edits `harness.yaml` without re-running
71
- `omh sync`. Three commands keep them honest:
72
-
73
- | Command | Use |
74
- |---------|-----|
75
- | `omh sync --check` | CI gate β€” exits non-zero (and lists the files) when generated output is stale, writes nothing |
76
- | `omh diff` | Human preview of exactly what `omh sync` would change |
77
- | `omh doctor --strict` | Health check that also fails on drift (plain `omh doctor` warns) |
78
-
79
- ```yaml
80
- # .github/workflows/ci.yml
81
- - run: npx oh-my-harness sync --check # fails the build if the harness is out of date
82
- ```
83
-
84
- The hook manifest records the oh-my-harness version that generated it, so an
85
- upgrade that changes output is surfaced as a "re-run `omh sync`" hint.
86
-
87
- ### πŸ“ What Gets Generated
88
-
89
- ```text
90
- ~/.omh/
91
- └── config.json # AI provider config (global, not per-project)
92
-
93
- your-project/
94
- β”œβ”€β”€ CLAUDE.md # Claude Code instructions (TDD rules, standards)
95
- β”œβ”€β”€ AGENTS.md # Codex CLI instructions (same managed sections)
96
- β”œβ”€β”€ harness.yaml # Your harness config (source of truth)
97
- β”œβ”€β”€ .omh/ # Single source of truth β€” hooks + state
98
- β”‚ β”œβ”€β”€ hooks/
99
- β”‚ β”‚ β”œβ”€β”€ catalog-branch-guard.sh # Blocks commits on merged branches
100
- β”‚ β”‚ β”œβ”€β”€ catalog-tdd-guard.sh # Enforces test-first workflow
101
- β”‚ β”‚ β”œβ”€β”€ catalog-commit-test-gate.sh # Tests must pass before commit
102
- β”‚ β”‚ β”œβ”€β”€ catalog-path-guard.sh # Protects build outputs
103
- β”‚ β”‚ β”œβ”€β”€ catalog-command-guard.sh # Blocks dangerous commands
104
- β”‚ β”‚ β”œβ”€β”€ catalog-lint-on-save.sh # Auto-lint on save
105
- β”‚ β”‚ └── catalog-auto-pr.sh # Auto-create PR after push
106
- β”‚ β”œβ”€β”€ state/ # gitignored β€” log/runtime data
107
- β”‚ β”‚ β”œβ”€β”€ events.jsonl # Unified hook event log (powers omh stats)
108
- β”‚ β”‚ └── tdd-edits.json # TDD guard working state
109
- β”‚ └── manifest.json # Generated-files manifest
110
- β”œβ”€β”€ .claude/
111
- β”‚ β”œβ”€β”€ settings.json # Claude permissions + hooks β†’ .omh/hooks/*.sh
112
- β”‚ └── oh-my-harness.json # Harness init/sync state
113
- └── .codex/
114
- β”œβ”€β”€ config.toml # [features] hooks = true, goals = true
115
- └── hooks.json # Codex hooks β†’ .omh/hooks/*.sh (same scripts)
116
- ```
117
-
118
- ---
119
-
120
- ## βš™οΈ How It Works
121
-
122
- ```text
123
- ~/.omh/config.json β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
124
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
125
- β”‚ β€’ Claude CLI │──▢│ NL Processing │◀── "React + FastAPI
126
- β”‚ β€’ Claude API β”‚ β”‚ (describe your β”‚ TDD enforced"
127
- β”‚ β€’ OpenAI API β”‚ β”‚ β”‚
128
- β”‚ β€’ Gemini API β”‚ β”‚ β”‚
129
- β”‚ β€’ Codex OAuth β”‚ β”‚ β”‚
130
- β”‚ β€’ Codex OAuth β”‚ β”‚ β”‚
131
- β”‚ API β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
132
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
133
- (global AI config) β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
134
- β”‚ Project Detector β”‚ ← Auto-detects language,
135
- β”‚ (14 languages) β”‚ framework, package manager
136
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
137
- β”‚
138
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
139
- β”‚ harness.yaml β”‚ ← Source of truth
140
- β”‚ (editable, git β”‚ (hooks + rules)
141
- β”‚ trackable) β”‚
142
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
143
- β”‚
144
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
145
- β–Ό β–Ό β–Ό
146
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
147
- β”‚CLAUDE.md β”‚ β”‚ Hooks β”‚ β”‚settings. β”‚
148
- β”‚ (rules) β”‚ β”‚(enforce) β”‚ β”‚ json β”‚
149
- β”‚ β”‚ β”‚ β”‚ β”‚(perms) β”‚
150
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
151
- ```
152
-
153
- ### πŸ” Project Detector
154
-
155
- oh-my-harness automatically detects your project type and injects accurate facts into the LLM prompt:
156
-
157
- | Language | Detection | Commands |
158
- |----------|-----------|----------|
159
- | 🟦 TypeScript/JS | package.json, tsconfig | pnpm/npm/yarn test, eslint, tsc |
160
- | 🐍 Python | pyproject.toml, requirements.txt, Pipfile, manage.py, .python-version | pytest, ruff, black, isort, mypy |
161
- | 🍎 Swift | Package.swift, .xcodeproj | swift test, xcodebuild |
162
- | πŸ¦€ Rust | Cargo.toml | cargo test, cargo clippy |
163
- | 🐹 Go | go.mod | go test, golangci-lint |
164
- | β˜• Java/Kotlin | pom.xml, build.gradle | mvn test, ./gradlew test |
165
- | πŸ’Ž Ruby | Gemfile | bundle exec rspec |
166
- | 🐘 PHP | composer.json | phpunit |
167
- | 🎯 Dart/Flutter | pubspec.yaml | dart test, flutter test |
168
- | ⚑ C/C++ | CMakeLists.txt | cmake, make |
169
- | 🟣 C#/.NET | *.csproj | dotnet test |
170
- | πŸ’§ Elixir | mix.exs | mix test |
171
- | πŸ”· Scala | build.sbt | sbt test |
172
- | ⚑ Zig | build.zig | zig build test |
173
-
174
- ### πŸ€– AI Provider Setup
175
-
176
- oh-my-harness supports multiple AI providers for natural language mode:
177
-
178
- | Provider | Setup | Available Models | Default |
179
- |----------|-------|------------------|---------|
180
- | **Claude CLI** | `claude` command installed | Opus 4.6, Sonnet 4.6, Haiku 4.5 | βœ“ |
181
- | **Claude API** | Set `ANTHROPIC_API_KEY` | Opus 4.6, Sonnet 4.6, Haiku 4.5 | Sonnet 4.6 |
182
- | **OpenAI API** | Set `OPENAI_API_KEY` | GPT-5.5, GPT-5.4, GPT-5.4-mini, GPT-5.4-nano, GPT-4.1, GPT-4.1-mini, o3, o4-mini | GPT-5.5 |
183
- | **Gemini API** | Set `GOOGLE_API_KEY` | Gemini 2.5 Pro, Gemini 2.5 Flash, Gemini 2.5 Flash Lite, Gemini 3.1 Pro Preview | Gemini 2.5 Pro |
184
- | **Codex OAuth** | `codex` command installed + `codex login`; runs `codex exec` | GPT-5.5, GPT-5.4, GPT-5.4-mini | GPT-5.5 |
185
- | **Codex OAuth API** | `omh config` device-code login; imports `~/.codex/auth.json` once if present, then uses `~/.omh` | GPT-5.5, GPT-5.4, GPT-5.4-mini | GPT-5.5 |
186
-
187
- Configuration is saved to `~/.omh/config.json` and selected via interactive UI on first use:
188
-
189
- ```bash
190
- omh init # will prompt for AI provider selection and model choice
191
- ```
61
+ πŸ“– Deeper: [what gets generated & CI drift check](docs/generated-files.md) Β· [how it works, project detector, AI providers](docs/how-it-works.md)
192
62
 
193
63
  ---
194
64
 
@@ -215,6 +85,7 @@ All enforcement is powered by **catalog blocks** β€” reusable, parameterized hoo
215
85
  | πŸ—œοΈ `compact-context` | maintenance | Re-injects context on session start |
216
86
  | πŸ“‹ `config-audit` | audit | Audit trail for config changes |
217
87
  | πŸ”” `desktop-notify` | ux | Cross-platform desktop notifications |
88
+ | πŸ” `loop-guard` | quality | Blocks a loop session from writing its own work orders or touching architect-only paths |
218
89
 
219
90
  ### Usage in `harness.yaml`
220
91
 
@@ -261,6 +132,12 @@ on a non-blocking block (e.g. `lint-on-save`) is reported and ignored.
261
132
 
262
133
  ---
263
134
 
135
+ ## πŸ” Autonomous Loop Engine
136
+
137
+ Hand omh a `WORKPLAN.md` and it runs one work order per fresh agent session, in an isolated git worktree, with hard-blocked protocol rules. `omh loop start` / `omh loop status` / `omh loop stop`. Full guide: [docs/loop-engine.md](docs/loop-engine.md).
138
+
139
+ ---
140
+
264
141
  ## πŸ–₯️ Commands
265
142
 
266
143
  ```bash
@@ -288,201 +165,14 @@ omh test # Dry-run verify all hooks
288
165
  omh stats # TUI analytics dashboard
289
166
  ```
290
167
 
291
- ### 🧹 `omh uninstall` β€” Safe generated-file cleanup
292
-
293
- `omh uninstall` removes oh-my-harness generated artifacts while preserving user
294
- content in merged files.
295
-
296
- ```bash
297
- omh uninstall --dry-run
298
- omh uninstall -y
299
- omh uninstall -y --purge
300
- ```
301
-
302
- Safety behavior:
303
-
304
- - Prints the same uninstall plan for dry-run and real execution.
305
- - Recommends backing up before execution; use `--skip-backup-warning` only for
306
- automation that already handles backups.
307
- - Keeps `harness.yaml` by default; `--purge` removes it.
308
- - Preserves user content in `CLAUDE.md`, `AGENTS.md`, `.claude/settings.json`,
309
- `.codex/hooks.json`, `.codex/config.toml`, and user Pi extensions.
310
- - Removes only OMH-owned hook commands that point at this project's
311
- `.omh/hooks` directory.
312
- - Warns when `.codex/config.toml` feature flags (`hooks`/`goals`) are removed,
313
- because manually-owned feature settings cannot be distinguished from OMH
314
- generated settings.
315
- - Warns that `.codex/config.toml` comments may be lost when TOML is rewritten.
316
- - Uses backups for modified files and restores them on stop-on-error failures;
317
- `--continue-on-error` records failures and keeps applying independent
318
- operations.
319
-
320
- ### 🩺 `omh doctor`
321
-
322
- ```text
323
- oh-my-harness: running health checks...
324
- βœ“ .claude/oh-my-harness.json found
325
- βœ“ CLAUDE.md exists with intact markers
326
- βœ“ .claude/settings.json is valid
327
- βœ“ All hook scripts are executable
328
- oh-my-harness: all checks passed
329
- ```
330
-
331
- ### πŸ§ͺ `omh test` β€” Dry-Run Verification
332
-
333
- Simulates hook inputs to verify block/allow behavior without entering Claude Code:
334
-
335
- ```text
336
- β”Œ omh test Harness dry-run verification
337
- β”‚
338
- β—‡ Branch guard
339
- β”‚ βœ“ git commit on feat/my-feature β†’ ALLOWED
340
- β”‚
341
- β—‡ TDD Guard
342
- β”‚ βœ“ src/foo.ts without test β†’ BLOCKED
343
- β”‚ βœ“ tests/unit/foo.test.ts β†’ ALLOWED
344
- β”‚ βœ“ README.md β†’ ALLOWED
345
- β”‚
346
- β—‡ File guards
347
- β”‚ βœ“ node_modules/test-file.js β†’ BLOCKED
348
- β”‚ βœ“ dist/test-file.js β†’ BLOCKED
349
- β”‚ βœ“ src/index.ts β†’ ALLOWED
350
- β”‚
351
- β—‡ Command guards
352
- β”‚ βœ“ "rm -rf /" β†’ BLOCKED
353
- β”‚ βœ“ "npm test" β†’ ALLOWED
354
- β”‚
355
- β”” 14/14 checks passed βœ“
356
- ```
357
-
358
- ### πŸ“Š `omh stats` β€” TUI Analytics Dashboard
359
-
360
- Interactive dashboard powered by [ink](https://github.com/vadimdemedes/ink) with 3 views:
361
-
362
- ```text
363
- [1] Overview [2] Timeline [3] Blocks d:filter r:reload q:quit
364
-
365
- Active: 8 Events: 1213 Block rate: 2%
366
-
367
- branch-guard β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ 0b/202a
368
- tdd-guard β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ 14b/68a
369
- commit-test-gate β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ 0b/199a
370
- path-guard β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ 4b/76a
371
- command-guard β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ 4b/202a
372
-
373
- Dormant (0 hits):
374
- β–‘ lockfile-guard
375
- β–‘ secret-file-guard
376
- ```
377
-
378
- - **Overview** β€” Active blocks with hit bar charts + dormant block detection
379
- - **Timeline** β€” 24-hour heatmap + block rate + peak hour
380
- - **Blocks** β€” Scrollable detail view with params, hits, last block reason
381
-
382
- Keyboard: `1/2/3` views, `↑/↓` scroll, `d` date filter, `r` reload, `q` quit
383
-
384
- ---
385
-
386
- ## πŸ“Š Stateful Hook Logging
387
-
388
- Every hook invocation is recorded in `.omh/state/events.jsonl`:
389
-
390
- ```jsonl
391
- {"ts":"2026-03-18T08:00:00Z","event":"PreToolUse","hook":"catalog-tdd-guard.sh","decision":"block","reason":"TDD β€” foo.test.* ν…ŒμŠ€νŠΈ νŒŒμΌμ„ λ¨Όμ € μˆ˜μ •ν•˜μ„Έμš”"}
392
- {"ts":"2026-03-18T08:00:05Z","event":"PreToolUse","hook":"catalog-command-guard.sh","decision":"allow","reason":""}
393
- ```
394
-
395
- This powers `omh test` live verification and `omh stats` analytics.
396
-
397
- ---
398
-
399
- ## πŸ—οΈ Architecture
400
-
401
- ```text
402
- oh-my-harness/
403
- β”œβ”€β”€ bin/ # CLI entry point
404
- β”œβ”€β”€ src/
405
- β”‚ β”œβ”€β”€ catalog/
406
- β”‚ β”‚ β”œβ”€β”€ blocks/ # 17 building block definitions
407
- β”‚ β”‚ β”œβ”€β”€ types.ts # BuildingBlock, HookEntry schemas
408
- β”‚ β”‚ β”œβ”€β”€ registry.ts # Block discovery & search
409
- β”‚ β”‚ β”œβ”€β”€ template-engine.ts # Handlebars rendering + applyDefaults
410
- β”‚ β”‚ └── converter.ts # HookEntry[] β†’ rendered scripts
411
- β”‚ β”œβ”€β”€ cli/
412
- β”‚ β”‚ β”œβ”€β”€ commands/ # init, doctor, catalog, hook, sync, test
413
- β”‚ β”‚ β”œβ”€β”€ stats/ # TUI dashboard (ink/React)
414
- β”‚ β”‚ β”‚ β”œβ”€β”€ App.tsx # App shell (tab bar, keyboard nav)
415
- β”‚ β”‚ β”‚ β”œβ”€β”€ data.ts # Data aggregation layer
416
- β”‚ β”‚ β”‚ └── components/ # Overview, Timeline, Blocks views
417
- β”‚ β”‚ β”œβ”€β”€ harness-tester.ts # Hook simulation engine
418
- β”‚ β”‚ β”œβ”€β”€ event-logger.ts # events.jsonl read/write/stats
419
- β”‚ β”‚ β”œβ”€β”€ event-verifier.ts # Event-based verification
420
- β”‚ β”‚ β”œβ”€β”€ tui/ # Interactive provider & model selection
421
- β”‚ β”‚ β”œβ”€β”€ provider-setup.ts # Provider configuration UI
422
- β”‚ β”‚ └── tool-checker.ts # Command executable checks
423
- β”‚ β”œβ”€β”€ core/
424
- β”‚ β”‚ β”œβ”€β”€ harness-schema.ts # harness.yaml Zod schema
425
- β”‚ β”‚ β”œβ”€β”€ merged-config.ts # MergedConfig + HooksConfig interfaces
426
- β”‚ β”‚ β”œβ”€β”€ harness-converter-v2.ts # harness.yaml β†’ MergedConfig (catalog pipeline)
427
- β”‚ β”‚ └── generator.ts # Orchestrates all generators
428
- β”‚ β”œβ”€β”€ generators/
429
- β”‚ β”‚ β”œβ”€β”€ claude-md.ts # CLAUDE.md with idempotent markers
430
- β”‚ β”‚ β”œβ”€β”€ hooks.ts # Hook scripts + event logger injection
431
- β”‚ β”‚ β”œβ”€β”€ settings.ts # .claude/settings.json
432
- β”‚ β”‚ └── gitignore.ts # .gitignore updater
433
- β”‚ β”œβ”€β”€ detector/
434
- β”‚ β”‚ β”œβ”€β”€ project-detector.ts # Deterministic project detection
435
- β”‚ β”‚ β”œβ”€β”€ types.ts # ProjectFacts, Detector interface
436
- β”‚ β”‚ └── detectors/ # 14 language detectors
437
- β”‚ β”œβ”€β”€ cli/
438
- β”‚ β”‚ β”œβ”€β”€ tui/ # Interactive provider & model selection
439
- β”‚ β”‚ └── provider-setup.ts # Provider configuration UI
440
- β”‚ └── nl/
441
- β”‚ β”œβ”€β”€ provider-registry.ts # Multi-provider definitions
442
- β”‚ β”œβ”€β”€ config-store.ts # ~/.omh/config.json persistence
443
- β”‚ β”œβ”€β”€ providers/ # Provider implementations
444
- β”‚ β”‚ β”œβ”€β”€ claude-cli.ts
445
- β”‚ β”‚ β”œβ”€β”€ claude-api.ts
446
- β”‚ β”‚ β”œβ”€β”€ openai-api.ts
447
- β”‚ β”‚ └── gemini-api.ts
448
- β”‚ β”œβ”€β”€ parse-intent.ts # LLM prompt integration
449
- β”‚ └── prompt-templates.ts # NL prompt construction
450
- └── tests/ # 900+ tests (unit + integration)
451
- ```
168
+ Details for `uninstall`, `doctor`, `test`, `stats` and hook event logging: [docs/commands.md](docs/commands.md). Internals: [docs/architecture.md](docs/architecture.md) Β· [roadmap](docs/roadmap.md).
452
169
 
453
170
  ---
454
171
 
455
172
  ## πŸ“¦ Requirements
456
173
 
457
174
  - **Node.js** >= 20
458
- - **Claude CLI** (optional, for default NL mode) β€” [Install guide](https://docs.anthropic.com/en/docs/claude-code)
459
- - **API Keys** (optional, for Claude/OpenAI/Gemini API modes) β€” set `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or `GOOGLE_API_KEY`
460
- - **Codex CLI OAuth** (optional, for `codex` CLI-wrapper mode) β€” install `codex` and run `codex login`
461
- - **Codex OAuth API** (optional, experimental direct mode) β€” run `omh config` and choose Codex OAuth API to complete device-code sign-in; credentials are stored under `~/.omh`
462
-
463
- ---
464
-
465
- ## πŸ—ΊοΈ Roadmap
466
-
467
- - [x] `npx oh-my-harness` β€” zero-install usage
468
- - [x] `omh sync` β€” regenerate from harness.yaml
469
- - [x] Building block catalog β€” 17 verified hook templates
470
- - [x] Project detector β€” 14 language auto-detection
471
- - [x] `omh test` β€” dry-run hook verification
472
- - [x] `omh stats` β€” TUI analytics dashboard (ink)
473
- - [x] Stateful hook logging β€” events.jsonl
474
- - [x] TDD Guard β€” enforce test-first workflow
475
- - [x] Multi-provider AI support β€” Claude API, OpenAI, Gemini, Codex OAuth
476
- - [x] Interactive model selection per provider
477
- - [x] GitHub star prompt β€” first-time only
478
- - [x] Codex emitter β€” `AGENTS.md` + `.codex/hooks.json` + `.codex/config.toml`
479
- - [x] Unified `.omh/` layout β€” single source of truth for hooks & state across runtimes
480
- - [x] Pi ([pi.dev](https://pi.dev)) emitter β€” bridge extension (`.pi/extensions/omh-harness.ts`) reusing the same `.omh/hooks/*.sh`
481
- - [x] `ask` mode β€” request approval before executing risky tools (Claude native prompt / Pi `ctx.ui.select`; Codex falls back to block)
482
- - [x] `omh uninstall` β€” remove generated artifacts while preserving user content
483
- - [x] `omh config` β€” view, reconfigure, or reset the saved AI provider (rotate expired keys, switch provider/model)
484
- - [ ] Community harness.yaml registry β€” share and reuse configs
485
- - [ ] `omh modify "change X"` β€” NL config editing
175
+ - **An AI provider** (optional, only for natural-language `omh init`) β€” any one of: the `claude` CLI, an API key for Claude/OpenAI/Gemini/OpenRouter, a ChatGPT subscription via Codex, or a local OpenAI-compatible server (Ollama etc.). See [AI Provider Setup](docs/how-it-works.md#-ai-provider-setup).
486
176
 
487
177
  ---
488
178
 
@@ -15,6 +15,7 @@ import { desktopNotify } from "./desktop-notify.js";
15
15
  import { configAudit } from "./config-audit.js";
16
16
  import { compactContext } from "./compact-context.js";
17
17
  import { worktreeSetup } from "./worktree-setup.js";
18
+ import { loopGuard } from "./loop-guard.js";
18
19
  export const builtinBlocks = [
19
20
  branchGuard,
20
21
  commitTestGate,
@@ -33,4 +34,5 @@ export const builtinBlocks = [
33
34
  configAudit,
34
35
  compactContext,
35
36
  worktreeSetup,
37
+ loopGuard,
36
38
  ];
@@ -0,0 +1,10 @@
1
+ import type { BuildingBlock } from "../types.js";
2
+ /**
3
+ * Enforces the loop protocol's two deterministic bans as hard blocks instead
4
+ * of prose. Both have been violated in practice when they lived only in a
5
+ * ledger: a loop that writes its own work order and then implements it is
6
+ * approving itself, and architect-only files are exactly the ones a loop
7
+ * mis-diagnoses. Fires only in loop sessions β€” the runner exports OMH_LOOP=1,
8
+ * so the architect's own session is never blocked.
9
+ */
10
+ export declare const loopGuard: BuildingBlock;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Enforces the loop protocol's two deterministic bans as hard blocks instead
3
+ * of prose. Both have been violated in practice when they lived only in a
4
+ * ledger: a loop that writes its own work order and then implements it is
5
+ * approving itself, and architect-only files are exactly the ones a loop
6
+ * mis-diagnoses. Fires only in loop sessions β€” the runner exports OMH_LOOP=1,
7
+ * so the architect's own session is never blocked.
8
+ */
9
+ export const loopGuard = {
10
+ id: "loop-guard",
11
+ name: "Loop Guard",
12
+ description: "Blocks a loop session from writing its own work orders or touching architect-only paths",
13
+ category: "quality",
14
+ event: "PreToolUse",
15
+ matcher: "Edit|Write|Bash",
16
+ canBlock: true,
17
+ params: [
18
+ {
19
+ name: "workOrders",
20
+ type: "string",
21
+ description: "Work-order directory the loop must never write into",
22
+ required: false,
23
+ default: "docs/work-orders",
24
+ },
25
+ {
26
+ name: "architectOnly",
27
+ type: "string[]",
28
+ description: "Path prefixes only the architect may edit",
29
+ required: false,
30
+ default: [],
31
+ },
32
+ ],
33
+ template: `#!/bin/bash
34
+ set -euo pipefail
35
+ INPUT=$(cat)
36
+
37
+ # Architect sessions pass through untouched; only the runner exports this.
38
+ [[ "\${OMH_LOOP:-}" != "1" ]] && exit 0
39
+
40
+ TOOL_NAME=$(echo "\$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
41
+ FILE_PATH=$(echo "\$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty' 2>/dev/null)
42
+ COMMAND=$(echo "\$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
43
+
44
+ # Codex apply_patch ships the whole patch in tool_input.command and names the
45
+ # files in "*** {Add|Update|Delete} File: <path>" headers. Collect those
46
+ # paths and treat them like file_path; the Bash write heuristic must NOT run
47
+ # over patch text (it would fire on words inside the diff).
48
+ PATCH_PATHS=()
49
+ if [[ "\$TOOL_NAME" == "apply_patch" && -n "\$COMMAND" ]]; then
50
+ while IFS= read -r _p; do
51
+ _p="\${_p%$'\\r'}"
52
+ [[ -n "\$_p" ]] && PATCH_PATHS+=("\$_p")
53
+ done < <(printf '%s\\n' "\$COMMAND" | sed -nE 's/^\\*\\*\\* ((Add|Update|Delete) File|Move to): (.+)$/\\3/p')
54
+ COMMAND=""
55
+ fi
56
+ [[ -z "\$FILE_PATH" && -z "\$COMMAND" && \${#PATCH_PATHS[@]} -eq 0 ]] && exit 0
57
+
58
+ # Component-boundary matching: wrap both sides in slashes so 'ios' matches
59
+ # ios/App.swift and /repo/ios/... but never src/kiosk.ts.
60
+ _omh_path_under() {
61
+ local file="/\$1/" prefix="\${2%/}"
62
+ [[ "\$file" == *"/\$prefix/"* ]]
63
+ }
64
+
65
+ # Bash coverage: a shell command that mentions a protected path AND carries a
66
+ # write indicator is blocked. Reads (cat/grep of a work order) pass.
67
+ # ponytail: substring heuristic β€” a command writing elsewhere while merely
68
+ # mentioning a protected path is over-blocked; tighten to arg-level parsing if
69
+ # that ever bites. This guards a drifting loop, not a malicious one β€” evasion
70
+ # via split cd chains, variable indirection, pushd, or symlinks is explicitly
71
+ # out of scope: winning that arms race needs a filesystem sandbox, not a hook.
72
+ _omh_bash_writes_to() {
73
+ local cmd="\$1" target="\${2%/}"
74
+ [[ -z "\$cmd" || -z "\$target" ]] && return 1
75
+ [[ "\$cmd" == *"\$target"* ]] || return 1
76
+ # The path is interpolated into an ERE below: escape its metacharacters so
77
+ # "docs/[orders" cannot break the pattern (grep error = silent bypass) and
78
+ # "docs/o+rders" is compared literally.
79
+ target=$(printf '%s' "\$target" | sed -e 's/[][\\.*^$(){}?+|]/\\\\&/g')
80
+ local WRITE_OPS='(tee|mv|cp|rm|touch|truncate|sed[[:space:]]+-i[^[:space:]]*)'
81
+ # write op targeting the protected path directly ...
82
+ echo "\$cmd" | grep -qE '(>|>>)[[:space:]]*[^|&;]*'"\$target"'|(^|[^[:alnum:]_])'"\$WRITE_OPS"'[[:space:]][^|&;]*'"\$target" && return 0
83
+ # ... or a cd into it followed by any write op (relative paths escape the
84
+ # direct pattern once the cwd is inside the protected directory)
85
+ echo "\$cmd" | grep -qE '(^|[^[:alnum:]_])cd[[:space:]][^|&;]*'"\$target" \\
86
+ && echo "\$cmd" | grep -qE '>|(^|[^[:alnum:]_])'"\$WRITE_OPS"'[[:space:]]'
87
+ }
88
+
89
+ # Any path a tool call names: the direct file_path plus apply_patch headers.
90
+ _omh_any_path_under() {
91
+ local prefix="\$1" f
92
+ [[ -n "\$FILE_PATH" ]] && _omh_path_under "\$FILE_PATH" "\$prefix" && return 0
93
+ for f in "\${PATCH_PATHS[@]+"\${PATCH_PATHS[@]}"}"; do
94
+ _omh_path_under "\$f" "\$prefix" && return 0
95
+ done
96
+ return 1
97
+ }
98
+
99
+ WORK_ORDERS='{{{workOrders}}}'
100
+ if _omh_any_path_under "\$WORK_ORDERS" || _omh_bash_writes_to "\$COMMAND" "\$WORK_ORDERS"; then
101
+ REASON="oh-my-harness: loop-guard β€” the loop must not write its own work orders. Mark the task 'BLOCKED: no work order' and move on; the architect writes work orders."
102
+ _log_event "block" "\$REASON"
103
+ _emit_decision "block" "\$REASON"
104
+ exit 0
105
+ fi
106
+
107
+ ARCHITECT_ONLY=({{#each architectOnly}}"{{{this}}}" {{/each}})
108
+ for prefix in "\${ARCHITECT_ONLY[@]+"\${ARCHITECT_ONLY[@]}"}"; do
109
+ [[ -z "\$prefix" ]] && continue
110
+ if _omh_any_path_under "\$prefix" || _omh_bash_writes_to "\$COMMAND" "\$prefix"; then
111
+ REASON="oh-my-harness: loop-guard β€” \$prefix is architect-only. Mark the task 'BLOCKED: architect-only path' and move on."
112
+ _log_event "block" "\$REASON"
113
+ _emit_decision "block" "\$REASON"
114
+ exit 0
115
+ fi
116
+ done
117
+
118
+ exit 0`,
119
+ tags: ["loop", "autonomous", "quality", "guardrail"],
120
+ };
@@ -63,7 +63,7 @@ fi
63
63
  # λŒ€μ‘ ν…ŒμŠ€νŠΈ 파일 확인 β€” ν™•μž₯자 제거
64
64
  BASENAME=$(basename "\$FILE_PATH" | sed -E 's/\\.[^.]+$//')
65
65
 
66
- REASON="oh-my-harness: TDD β€” \${BASENAME} 에 λŒ€μ‘ν•˜λŠ” ν…ŒμŠ€νŠΈ νŒŒμΌμ„ λ¨Όμ € μˆ˜μ •ν•˜μ„Έμš”"
66
+ REASON="oh-my-harness: TDD guard β€” edit the test for \${BASENAME} first, then the source"
67
67
  if [[ ! -f "\$HISTORY_FILE" ]]; then
68
68
  _log_event "block" "\$REASON"
69
69
  _emit_decision "block" "\$REASON"
@@ -7,7 +7,9 @@ function printSummary(config) {
7
7
  console.log(` method: ${chalk.cyan(config.method)}`);
8
8
  if (config.method === "api") {
9
9
  console.log(` model: ${chalk.cyan(config.model ?? "(default)")}`);
10
- console.log(` api key: ${chalk.dim(maskApiKey(config.apiKey))}`);
10
+ if (config.baseUrl)
11
+ console.log(` base url: ${chalk.cyan(config.baseUrl)}`);
12
+ console.log(` api key: ${chalk.dim(config.apiKey ? maskApiKey(config.apiKey) : "(none)")}`);
11
13
  }
12
14
  else if (config.method === "oauth") {
13
15
  console.log(` model: ${chalk.cyan(config.model ?? "(default)")}`);
@@ -72,10 +74,17 @@ export async function configCommand(options = {}) {
72
74
  console.log(chalk.green("Removed ~/.omh/config.json"));
73
75
  return { exitCode: 0 };
74
76
  }
75
- // Default: show current config (if any), then reconfigure.
77
+ // Default: show current config, then ask before launching setup so a plain
78
+ // `omh config` is a safe read; `--yes` skips the prompt.
76
79
  if (existing) {
77
80
  printSummary(existing);
78
81
  console.log("");
82
+ if (!options.yes) {
83
+ const confirm = options.confirm ?? defaultConfirm;
84
+ const ok = await confirm("Reconfigure the AI provider?");
85
+ if (!ok)
86
+ return { exitCode: 0 };
87
+ }
79
88
  }
80
89
  else {
81
90
  console.log("No AI provider configured yet. Let's set one up.\n");
@@ -0,0 +1,54 @@
1
+ import type { LoopConfig } from "../../core/merged-config.js";
2
+ import type { LivenessDeps } from "../../loop/state.js";
3
+ import { stopLoop } from "../../loop/stop.js";
4
+ import type { SupervisorExit } from "../../loop/supervisor.js";
5
+ /**
6
+ * `omh loop` β€” the user-facing (and skill-facing) entry points to the
7
+ * supervisor in src/loop. `start` launches `run` detached; everything else
8
+ * reads or signals the state that `run` maintains.
9
+ */
10
+ export interface LoopResult {
11
+ exitCode: number;
12
+ }
13
+ /** The subset of ChildProcess that start relies on; injectable for tests. */
14
+ export interface ChildLike {
15
+ pid?: number;
16
+ exitCode: number | null;
17
+ unref(): void;
18
+ once(event: "exit", cb: (code: number | null) => void): unknown;
19
+ }
20
+ export type SpawnLike = (cmd: string, args: string[], opts: Record<string, unknown>) => ChildLike;
21
+ export interface LoopStartOptions extends LivenessDeps {
22
+ projectDir?: string;
23
+ spawnImpl?: SpawnLike;
24
+ platform?: NodeJS.Platform;
25
+ /** How long to wait for the supervisor to acquire the run. */
26
+ acquireTimeoutMs?: number;
27
+ }
28
+ export declare function loopStartCommand(options?: LoopStartOptions): Promise<LoopResult>;
29
+ export interface LoopRunOptions {
30
+ projectDir?: string;
31
+ runId: string;
32
+ supervisorImpl?: (o: {
33
+ projectDir: string;
34
+ cfg: LoopConfig;
35
+ runId: string;
36
+ }) => Promise<SupervisorExit>;
37
+ sweepImpl?: () => void;
38
+ }
39
+ export declare function loopRunCommand(options: LoopRunOptions): Promise<LoopResult>;
40
+ export interface LoopStopOptions {
41
+ projectDir?: string;
42
+ now?: boolean;
43
+ stopImpl?: typeof stopLoop;
44
+ }
45
+ export declare function loopStopCommand(options?: LoopStopOptions): Promise<LoopResult>;
46
+ export interface LoopStatusOptions extends LivenessDeps {
47
+ projectDir?: string;
48
+ }
49
+ export declare function loopStatusCommand(options?: LoopStatusOptions): Promise<LoopResult>;
50
+ export interface LoopCleanOptions {
51
+ projectDir?: string;
52
+ branch?: boolean;
53
+ }
54
+ export declare function loopCleanCommand(options?: LoopCleanOptions): Promise<LoopResult>;