oh-my-harness 0.19.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.
- package/README.md +24 -438
- package/dist/catalog/blocks/tdd-guard.js +1 -1
- package/dist/cli/commands/config.js +11 -2
- package/dist/cli/tui/open-browser.d.ts +19 -0
- package/dist/cli/tui/open-browser.js +34 -0
- package/dist/cli/tui/provider-setup.js +158 -75
- package/dist/nl/config-store.d.ts +3 -1
- package/dist/nl/config-store.js +6 -1
- package/dist/nl/list-models.d.ts +15 -0
- package/dist/nl/list-models.js +75 -0
- package/dist/nl/parse-intent.js +13 -1
- package/dist/nl/provider-registry.d.ts +6 -0
- package/dist/nl/provider-registry.js +59 -30
- package/dist/nl/providers/claude-api.js +1 -1
- package/dist/nl/providers/codex-oauth-api.d.ts +2 -1
- package/dist/nl/providers/codex-oauth-api.js +3 -3
- package/dist/nl/providers/codex-oauth.js +1 -1
- package/dist/nl/providers/openai-api.d.ts +16 -1
- package/dist/nl/providers/openai-api.js +67 -23
- package/dist/nl/providers/openrouter-oauth.d.ts +13 -0
- package/dist/nl/providers/openrouter-oauth.js +38 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,46 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
# π΄ oh-my-harness
|
|
4
4
|
|
|
5
|
-
**
|
|
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
|
[](https://www.npmjs.com/package/oh-my-harness)
|
|
8
10
|
[](https://www.npmjs.com/package/oh-my-harness)
|
|
9
11
|
[](https://github.com/kyu1204/oh-my-harness/actions/workflows/ci.yml)
|
|
10
12
|
[](LICENSE)
|
|
11
|
-
[](https://www.typescriptlang.org/)
|
|
12
|
-
[](https://nodejs.org/)
|
|
13
13
|
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
+
## Why not just write CLAUDE.md?
|
|
36
37
|
|
|
37
|
-
-
|
|
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,135 +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
|
-
|
|
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
|
-
β βββ loop/
|
|
107
|
-
β β βββ worktree/ # gitignored β the loop's isolated git worktree (see Loop Engine)
|
|
108
|
-
β βββ state/ # gitignored β log/runtime data
|
|
109
|
-
β β βββ events.jsonl # Unified hook event log (powers omh stats)
|
|
110
|
-
β β βββ loop/ # run.json (lock + identity), stop flag, runs/<id>/events.jsonl
|
|
111
|
-
β β βββ tdd-edits.json # TDD guard working state
|
|
112
|
-
β βββ manifest.json # Generated-files manifest
|
|
113
|
-
βββ .claude/
|
|
114
|
-
β βββ settings.json # Claude permissions + hooks β .omh/hooks/*.sh
|
|
115
|
-
β βββ skills/omh-loop/SKILL.md # "run this as a loop" skill
|
|
116
|
-
β βββ oh-my-harness.json # Harness init/sync state
|
|
117
|
-
βββ .codex/
|
|
118
|
-
βββ config.toml # [features] hooks = true, goals = true
|
|
119
|
-
βββ hooks.json # Codex hooks β .omh/hooks/*.sh (same scripts)
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
---
|
|
123
|
-
|
|
124
|
-
## βοΈ How It Works
|
|
125
|
-
|
|
126
|
-
```text
|
|
127
|
-
~/.omh/config.json βββββββββββββββββββββββ
|
|
128
|
-
ββββββββββββββββββ β β
|
|
129
|
-
β β’ Claude CLI ββββΆβ NL Processing ββββ "React + FastAPI
|
|
130
|
-
β β’ Claude API β β (describe your β TDD enforced"
|
|
131
|
-
β β’ OpenAI API β β β
|
|
132
|
-
β β’ Gemini API β β β
|
|
133
|
-
β β’ Codex OAuth β β β
|
|
134
|
-
β β’ Codex OAuth β β β
|
|
135
|
-
β API β ββββββββββ¬βββββββββββββ
|
|
136
|
-
ββββββββββββββββββ β
|
|
137
|
-
(global AI config) ββββββββββΌβββββββββββββ
|
|
138
|
-
β Project Detector β β Auto-detects language,
|
|
139
|
-
β (14 languages) β framework, package manager
|
|
140
|
-
ββββββββββ¬βββββββββββββ
|
|
141
|
-
β
|
|
142
|
-
ββββββββββΌβββββββββββββ
|
|
143
|
-
β harness.yaml β β Source of truth
|
|
144
|
-
β (editable, git β (hooks + rules)
|
|
145
|
-
β trackable) β
|
|
146
|
-
ββββββββββ¬βββββββββββββ
|
|
147
|
-
β
|
|
148
|
-
ββββββββββββββββΌβββββββββββββββ
|
|
149
|
-
βΌ βΌ βΌ
|
|
150
|
-
ββββββββββββ ββββββββββββ ββββββββββββ
|
|
151
|
-
βCLAUDE.md β β Hooks β βsettings. β
|
|
152
|
-
β (rules) β β(enforce) β β json β
|
|
153
|
-
β β β β β(perms) β
|
|
154
|
-
ββββββββββββ ββββββββββββ ββββββββββββ
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
### π Project Detector
|
|
158
|
-
|
|
159
|
-
oh-my-harness automatically detects your project type and injects accurate facts into the LLM prompt:
|
|
160
|
-
|
|
161
|
-
| Language | Detection | Commands |
|
|
162
|
-
|----------|-----------|----------|
|
|
163
|
-
| π¦ TypeScript/JS | package.json, tsconfig | pnpm/npm/yarn test, eslint, tsc |
|
|
164
|
-
| π Python | pyproject.toml, requirements.txt, Pipfile, manage.py, .python-version | pytest, ruff, black, isort, mypy |
|
|
165
|
-
| π Swift | Package.swift, .xcodeproj | swift test, xcodebuild |
|
|
166
|
-
| π¦ Rust | Cargo.toml | cargo test, cargo clippy |
|
|
167
|
-
| πΉ Go | go.mod | go test, golangci-lint |
|
|
168
|
-
| β Java/Kotlin | pom.xml, build.gradle | mvn test, ./gradlew test |
|
|
169
|
-
| π Ruby | Gemfile | bundle exec rspec |
|
|
170
|
-
| π PHP | composer.json | phpunit |
|
|
171
|
-
| π― Dart/Flutter | pubspec.yaml | dart test, flutter test |
|
|
172
|
-
| β‘ C/C++ | CMakeLists.txt | cmake, make |
|
|
173
|
-
| π£ C#/.NET | *.csproj | dotnet test |
|
|
174
|
-
| π§ Elixir | mix.exs | mix test |
|
|
175
|
-
| π· Scala | build.sbt | sbt test |
|
|
176
|
-
| β‘ Zig | build.zig | zig build test |
|
|
177
|
-
|
|
178
|
-
### π€ AI Provider Setup
|
|
179
|
-
|
|
180
|
-
oh-my-harness supports multiple AI providers for natural language mode:
|
|
181
|
-
|
|
182
|
-
| Provider | Setup | Available Models | Default |
|
|
183
|
-
|----------|-------|------------------|---------|
|
|
184
|
-
| **Claude CLI** | `claude` command installed | Opus 4.6, Sonnet 4.6, Haiku 4.5 | β |
|
|
185
|
-
| **Claude API** | Set `ANTHROPIC_API_KEY` | Opus 4.6, Sonnet 4.6, Haiku 4.5 | Sonnet 4.6 |
|
|
186
|
-
| **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 |
|
|
187
|
-
| **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 |
|
|
188
|
-
| **Codex OAuth** | `codex` command installed + `codex login`; runs `codex exec` | GPT-5.5, GPT-5.4, GPT-5.4-mini | GPT-5.5 |
|
|
189
|
-
| **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 |
|
|
190
|
-
|
|
191
|
-
Configuration is saved to `~/.omh/config.json` and selected via interactive UI on first use:
|
|
192
|
-
|
|
193
|
-
```bash
|
|
194
|
-
omh init # will prompt for AI provider selection and model choice
|
|
195
|
-
```
|
|
61
|
+
π Deeper: [what gets generated & CI drift check](docs/generated-files.md) Β· [how it works, project detector, AI providers](docs/how-it-works.md)
|
|
196
62
|
|
|
197
63
|
---
|
|
198
64
|
|
|
@@ -268,91 +134,7 @@ on a non-blocking block (e.g. `lint-on-save`) is reported and ignored.
|
|
|
268
134
|
|
|
269
135
|
## π Autonomous Loop Engine
|
|
270
136
|
|
|
271
|
-
|
|
272
|
-
any agent session (Claude Code, Codex, Pi) can be told **"run this as a loop"** β the generated `omh-loop` skill turns that
|
|
273
|
-
into a fully set-up autonomous loop, no manual wiring:
|
|
274
|
-
|
|
275
|
-
```text
|
|
276
|
-
you: "ship the remaining Phase B tasks as a loop"
|
|
277
|
-
β
|
|
278
|
-
βΌ omh-loop skill (the session becomes the ARCHITECT)
|
|
279
|
-
1. writes the `loop.ledger` β default WORKPLAN.md; goal gates + task checkboxes
|
|
280
|
-
2. writes `loop.workOrders`/*.md β default docs/work-orders; one exact work order per task
|
|
281
|
-
3. runs `omh loop start` β a detached supervisor; with `isolate: true` (default), in its own git worktree
|
|
282
|
-
4. attaches monitoring β omh loop status, then tail -f .omh/state/loop/runs/<id>/events.jsonl
|
|
283
|
-
β
|
|
284
|
-
βΌ loop (fresh headless session per iteration, cheap model)
|
|
285
|
-
pick next unchecked task β implement its work order exactly β run its
|
|
286
|
-
acceptance commands β tick checkbox + commit β repeat until the sentinel
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
The design follows a battle-tested pattern from real autonomous runs: state
|
|
290
|
-
lives in **files** (ledger + git log), never in a conversation. Each iteration
|
|
291
|
-
is a fresh headless session (`claude -p` / `codex exec --dangerously-bypass-hook-trust` / `pi --print --no-session`)
|
|
292
|
-
on an explicit cheap model, so token use stays flat.
|
|
293
|
-
|
|
294
|
-
### Why it doesn't fall over
|
|
295
|
-
|
|
296
|
-
Every guard below exists because the failure actually happened somewhere:
|
|
297
|
-
|
|
298
|
-
| Guard | Failure it prevents |
|
|
299
|
-
|-------|---------------------|
|
|
300
|
-
| Sentinel = fixed string, whole line, clean exit only | a crashed turn (or a turn merely *mentioning* the sentinel) ending the loop as "complete" |
|
|
301
|
-
| Three **separate** backoffs β usage limit / empty-output failed turn / consecutive `BLOCKED` | a loop once spun **266 iterations** doing nothing because waiting-on-a-human was treated like a failure |
|
|
302
|
-
| `loop-guard` hook blocks the loop writing its own work orders (Edit/Write **and** Bash redirection/`cd`) | self-approval: the loop authoring the spec it then implements |
|
|
303
|
-
| `loop-guard` blocks architect-only paths, listed **by name** | an abstract "don't improvise" is ignored; a named path is obeyed |
|
|
304
|
-
| Worktree isolation (`isolate: true`, default) | the loop and the architect fighting over the same working tree |
|
|
305
|
-
| Ledger seeded once, re-seeded only when the main-tree copy is newer | the loop's progress being rolled back β or a second goal reusing the first goal's ledger |
|
|
306
|
-
|
|
307
|
-
### Configuration
|
|
308
|
-
|
|
309
|
-
On by default. Everything is optional in `harness.yaml`:
|
|
310
|
-
|
|
311
|
-
```yaml
|
|
312
|
-
loop:
|
|
313
|
-
enabled: true # false removes all loop assets on next sync
|
|
314
|
-
ledger: WORKPLAN.md # single source of truth
|
|
315
|
-
workOrders: docs/work-orders # architect-written task specs
|
|
316
|
-
model: sonnet # cheap implementation model (always explicit)
|
|
317
|
-
sentinel: OMH_GOAL_COMPLETE # whole-line completion signal
|
|
318
|
-
interval: 120 # seconds between iterations
|
|
319
|
-
blockedBackoff: 1800 # backoff once stallStreak blocked/idle turns pile up
|
|
320
|
-
limitBackoff: 1800 # backoff after a provider usage limit
|
|
321
|
-
emptyBackoff: 300 # backoff after a crashed (empty-output / timed-out) turn
|
|
322
|
-
stallStreak: 3 # consecutive blocked/idle turns before blockedBackoff
|
|
323
|
-
turnTimeout: 7200 # hard per-turn timeout (seconds); the turn is killed past this
|
|
324
|
-
architectOnly: [] # paths the loop must never touch (name them!)
|
|
325
|
-
isolate: true # run in .omh/loop/worktree on branch omh-loop
|
|
326
|
-
runtime: claude # claude | codex | pi
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
### Operating it
|
|
330
|
-
|
|
331
|
-
```bash
|
|
332
|
-
omh loop start # preflight, then a detached supervisor (the skill does this for you)
|
|
333
|
-
omh loop status # run id, pid, iteration, last event, events path
|
|
334
|
-
tail -f .omh/state/loop/runs/<id>/events.jsonl # progress / blocked / idle / limit / crash
|
|
335
|
-
omh loop stop [--now] # stop flag + SIGTERM to the whole process group (--now: 1s grace)
|
|
336
|
-
omh loop clean [--branch] # remove the worktree and stale state (and optionally the omh-loop branch)
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
The supervisor is TypeScript (`src/loop/`), not a generated script: the runtime
|
|
340
|
-
is spawned from an argv array (no shell), each turn is judged by the **ledger
|
|
341
|
-
diff and git HEAD** rather than by grepping output, and the run lock is a single
|
|
342
|
-
`run.json` created atomically with `link(2)`. POSIX only β it relies on process
|
|
343
|
-
groups and `ps` for identity checks.
|
|
344
|
-
|
|
345
|
-
Known limit: the ledger is seeded into the worktree once per goal (by content
|
|
346
|
-
hash). Editing the main-tree ledger mid-run does nothing until the next start,
|
|
347
|
-
which then re-seeds it β add mid-run tasks as new work orders instead, and edit
|
|
348
|
-
the worktree's ledger if you must.
|
|
349
|
-
|
|
350
|
-
A task the loop cannot finish (needs a human, or 3 failed attempts) is marked
|
|
351
|
-
`BLOCKED: <reason>` in the ledger and skipped β the loop never idles waiting
|
|
352
|
-
for a person. When you unblock it (e.g. fix an architect-only file), the loop
|
|
353
|
-
picks it back up after its backoff. When the goal completes in isolation, merge
|
|
354
|
-
the `omh-loop` branch and **re-verify** β a clean textual merge is not a
|
|
355
|
-
semantic one.
|
|
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).
|
|
356
138
|
|
|
357
139
|
---
|
|
358
140
|
|
|
@@ -383,210 +165,14 @@ omh test # Dry-run verify all hooks
|
|
|
383
165
|
omh stats # TUI analytics dashboard
|
|
384
166
|
```
|
|
385
167
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
`omh uninstall` removes oh-my-harness generated artifacts while preserving user
|
|
389
|
-
content in merged files.
|
|
390
|
-
|
|
391
|
-
```bash
|
|
392
|
-
omh uninstall --dry-run
|
|
393
|
-
omh uninstall -y
|
|
394
|
-
omh uninstall -y --purge
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
Safety behavior:
|
|
398
|
-
|
|
399
|
-
- Prints the same uninstall plan for dry-run and real execution.
|
|
400
|
-
- Recommends backing up before execution; use `--skip-backup-warning` only for
|
|
401
|
-
automation that already handles backups.
|
|
402
|
-
- Keeps `harness.yaml` by default; `--purge` removes it.
|
|
403
|
-
- Preserves user content in `CLAUDE.md`, `AGENTS.md`, `.claude/settings.json`,
|
|
404
|
-
`.codex/hooks.json`, `.codex/config.toml`, and user Pi extensions.
|
|
405
|
-
- Removes only OMH-owned hook commands that point at this project's
|
|
406
|
-
`.omh/hooks` directory.
|
|
407
|
-
- Warns when `.codex/config.toml` feature flags (`hooks`/`goals`) are removed,
|
|
408
|
-
because manually-owned feature settings cannot be distinguished from OMH
|
|
409
|
-
generated settings.
|
|
410
|
-
- Warns that `.codex/config.toml` comments may be lost when TOML is rewritten.
|
|
411
|
-
- Uses backups for modified files and restores them on stop-on-error failures;
|
|
412
|
-
`--continue-on-error` records failures and keeps applying independent
|
|
413
|
-
operations.
|
|
414
|
-
|
|
415
|
-
### π©Ί `omh doctor`
|
|
416
|
-
|
|
417
|
-
```text
|
|
418
|
-
oh-my-harness: running health checks...
|
|
419
|
-
β .claude/oh-my-harness.json found
|
|
420
|
-
β CLAUDE.md exists with intact markers
|
|
421
|
-
β .claude/settings.json is valid
|
|
422
|
-
β All hook scripts are executable
|
|
423
|
-
oh-my-harness: all checks passed
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
### π§ͺ `omh test` β Dry-Run Verification
|
|
427
|
-
|
|
428
|
-
Simulates hook inputs to verify block/allow behavior without entering Claude Code:
|
|
429
|
-
|
|
430
|
-
```text
|
|
431
|
-
β omh test Harness dry-run verification
|
|
432
|
-
β
|
|
433
|
-
β Branch guard
|
|
434
|
-
β β git commit on feat/my-feature β ALLOWED
|
|
435
|
-
β
|
|
436
|
-
β TDD Guard
|
|
437
|
-
β β src/foo.ts without test β BLOCKED
|
|
438
|
-
β β tests/unit/foo.test.ts β ALLOWED
|
|
439
|
-
β β README.md β ALLOWED
|
|
440
|
-
β
|
|
441
|
-
β File guards
|
|
442
|
-
β β node_modules/test-file.js β BLOCKED
|
|
443
|
-
β β dist/test-file.js β BLOCKED
|
|
444
|
-
β β src/index.ts β ALLOWED
|
|
445
|
-
β
|
|
446
|
-
β Command guards
|
|
447
|
-
β β "rm -rf /" β BLOCKED
|
|
448
|
-
β β "npm test" β ALLOWED
|
|
449
|
-
β
|
|
450
|
-
β 14/14 checks passed β
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
### π `omh stats` β TUI Analytics Dashboard
|
|
454
|
-
|
|
455
|
-
Interactive dashboard powered by [ink](https://github.com/vadimdemedes/ink) with 3 views:
|
|
456
|
-
|
|
457
|
-
```text
|
|
458
|
-
[1] Overview [2] Timeline [3] Blocks d:filter r:reload q:quit
|
|
459
|
-
|
|
460
|
-
Active: 8 Events: 1213 Block rate: 2%
|
|
461
|
-
|
|
462
|
-
branch-guard ββββββββββββββββββββ 0b/202a
|
|
463
|
-
tdd-guard βββββββββββββββββββββ 14b/68a
|
|
464
|
-
commit-test-gate ββββββββββββββββββββ 0b/199a
|
|
465
|
-
path-guard ββββββββββββββββββββ 4b/76a
|
|
466
|
-
command-guard ββββββββββββββββββββ 4b/202a
|
|
467
|
-
|
|
468
|
-
Dormant (0 hits):
|
|
469
|
-
β lockfile-guard
|
|
470
|
-
β secret-file-guard
|
|
471
|
-
```
|
|
472
|
-
|
|
473
|
-
- **Overview** β Active blocks with hit bar charts + dormant block detection
|
|
474
|
-
- **Timeline** β 24-hour heatmap + block rate + peak hour
|
|
475
|
-
- **Blocks** β Scrollable detail view with params, hits, last block reason
|
|
476
|
-
|
|
477
|
-
Keyboard: `1/2/3` views, `β/β` scroll, `d` date filter, `r` reload, `q` quit
|
|
478
|
-
|
|
479
|
-
---
|
|
480
|
-
|
|
481
|
-
## π Stateful Hook Logging
|
|
482
|
-
|
|
483
|
-
Every hook invocation is recorded in `.omh/state/events.jsonl`:
|
|
484
|
-
|
|
485
|
-
```jsonl
|
|
486
|
-
{"ts":"2026-03-18T08:00:00Z","event":"PreToolUse","hook":"catalog-tdd-guard.sh","decision":"block","reason":"TDD β foo.test.* ν
μ€νΈ νμΌμ λ¨Όμ μμ νμΈμ"}
|
|
487
|
-
{"ts":"2026-03-18T08:00:05Z","event":"PreToolUse","hook":"catalog-command-guard.sh","decision":"allow","reason":""}
|
|
488
|
-
```
|
|
489
|
-
|
|
490
|
-
This powers `omh test` live verification and `omh stats` analytics.
|
|
491
|
-
|
|
492
|
-
---
|
|
493
|
-
|
|
494
|
-
## ποΈ Architecture
|
|
495
|
-
|
|
496
|
-
```text
|
|
497
|
-
oh-my-harness/
|
|
498
|
-
βββ bin/ # CLI entry point
|
|
499
|
-
βββ src/
|
|
500
|
-
β βββ catalog/
|
|
501
|
-
β β βββ blocks/ # 18 building block definitions
|
|
502
|
-
β β βββ types.ts # BuildingBlock, HookEntry schemas
|
|
503
|
-
β β βββ registry.ts # Block discovery & search
|
|
504
|
-
β β βββ template-engine.ts # Handlebars rendering + applyDefaults
|
|
505
|
-
β β βββ converter.ts # HookEntry[] β rendered scripts
|
|
506
|
-
β βββ cli/
|
|
507
|
-
β β βββ commands/ # init, doctor, catalog, hook, sync, test, loop (start/run/stop/status/clean)
|
|
508
|
-
β β βββ stats/ # TUI dashboard (ink/React)
|
|
509
|
-
β β β βββ App.tsx # App shell (tab bar, keyboard nav)
|
|
510
|
-
β β β βββ data.ts # Data aggregation layer
|
|
511
|
-
β β β βββ components/ # Overview, Timeline, Blocks views
|
|
512
|
-
β β βββ harness-tester.ts # Hook simulation engine
|
|
513
|
-
β β βββ event-logger.ts # events.jsonl read/write/stats
|
|
514
|
-
β β βββ event-verifier.ts # Event-based verification
|
|
515
|
-
β β βββ tui/ # Interactive provider & model selection
|
|
516
|
-
β β βββ provider-setup.ts # Provider configuration UI
|
|
517
|
-
β β βββ tool-checker.ts # Command executable checks
|
|
518
|
-
β βββ loop/ # autonomous loop supervisor (TypeScript, no generated shell)
|
|
519
|
-
β β βββ supervisor.ts # one run: lock β worktree/seed β turns β release
|
|
520
|
-
β β βββ state.ts # run.json link-lock, events, atomic writes
|
|
521
|
-
β β βββ classify.ts # turn verdict from ledger diff + HEAD; wait policy
|
|
522
|
-
β β βββ runtime.ts # argv per runtime, turn execution in its own process group
|
|
523
|
-
β β βββ worktree.ts # git worktree lifecycle, asset sync, ledger seeding
|
|
524
|
-
β β βββ protocol.ts # the rules, rendered into prompt / CLAUDE.md / skill
|
|
525
|
-
β β βββ stop.ts # identity-checked group stop, group sweep
|
|
526
|
-
β βββ core/
|
|
527
|
-
β β βββ harness-schema.ts # harness.yaml Zod schema
|
|
528
|
-
β β βββ merged-config.ts # MergedConfig + HooksConfig interfaces
|
|
529
|
-
β β βββ harness-converter-v2.ts # harness.yaml β MergedConfig (catalog pipeline)
|
|
530
|
-
β β βββ generator.ts # Orchestrates all generators
|
|
531
|
-
β βββ generators/
|
|
532
|
-
β β βββ claude-md.ts # CLAUDE.md with idempotent markers
|
|
533
|
-
β β βββ hooks.ts # Hook scripts + event logger injection
|
|
534
|
-
β β βββ settings.ts # .claude/settings.json
|
|
535
|
-
β β βββ gitignore.ts # .gitignore updater
|
|
536
|
-
β βββ detector/
|
|
537
|
-
β β βββ project-detector.ts # Deterministic project detection
|
|
538
|
-
β β βββ types.ts # ProjectFacts, Detector interface
|
|
539
|
-
β β βββ detectors/ # 14 language detectors
|
|
540
|
-
β βββ cli/
|
|
541
|
-
β β βββ tui/ # Interactive provider & model selection
|
|
542
|
-
β β βββ provider-setup.ts # Provider configuration UI
|
|
543
|
-
β βββ nl/
|
|
544
|
-
β βββ provider-registry.ts # Multi-provider definitions
|
|
545
|
-
β βββ config-store.ts # ~/.omh/config.json persistence
|
|
546
|
-
β βββ providers/ # Provider implementations
|
|
547
|
-
β β βββ claude-cli.ts
|
|
548
|
-
β β βββ claude-api.ts
|
|
549
|
-
β β βββ openai-api.ts
|
|
550
|
-
β β βββ gemini-api.ts
|
|
551
|
-
β βββ parse-intent.ts # LLM prompt integration
|
|
552
|
-
β βββ prompt-templates.ts # NL prompt construction
|
|
553
|
-
βββ tests/ # 900+ tests (unit + integration)
|
|
554
|
-
```
|
|
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).
|
|
555
169
|
|
|
556
170
|
---
|
|
557
171
|
|
|
558
172
|
## π¦ Requirements
|
|
559
173
|
|
|
560
174
|
- **Node.js** >= 20
|
|
561
|
-
- **
|
|
562
|
-
- **API Keys** (optional, for Claude/OpenAI/Gemini API modes) β set `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or `GOOGLE_API_KEY`
|
|
563
|
-
- **Codex CLI OAuth** (optional, for `codex` CLI-wrapper mode) β install `codex` and run `codex login`
|
|
564
|
-
- **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`
|
|
565
|
-
|
|
566
|
-
---
|
|
567
|
-
|
|
568
|
-
## πΊοΈ Roadmap
|
|
569
|
-
|
|
570
|
-
- [x] `npx oh-my-harness` β zero-install usage
|
|
571
|
-
- [x] `omh sync` β regenerate from harness.yaml
|
|
572
|
-
- [x] Building block catalog β 18 verified hook templates
|
|
573
|
-
- [x] Project detector β 14 language auto-detection
|
|
574
|
-
- [x] `omh test` β dry-run hook verification
|
|
575
|
-
- [x] `omh stats` β TUI analytics dashboard (ink)
|
|
576
|
-
- [x] Stateful hook logging β events.jsonl
|
|
577
|
-
- [x] TDD Guard β enforce test-first workflow
|
|
578
|
-
- [x] Multi-provider AI support β Claude API, OpenAI, Gemini, Codex OAuth
|
|
579
|
-
- [x] Interactive model selection per provider
|
|
580
|
-
- [x] GitHub star prompt β first-time only
|
|
581
|
-
- [x] Codex emitter β `AGENTS.md` + `.codex/hooks.json` + `.codex/config.toml`
|
|
582
|
-
- [x] Unified `.omh/` layout β single source of truth for hooks & state across runtimes
|
|
583
|
-
- [x] Pi ([pi.dev](https://pi.dev)) emitter β bridge extension (`.pi/extensions/omh-harness.ts`) reusing the same `.omh/hooks/*.sh`
|
|
584
|
-
- [x] `ask` mode β request approval before executing risky tools (Claude native prompt / Pi `ctx.ui.select`; Codex falls back to block)
|
|
585
|
-
- [x] `omh uninstall` β remove generated artifacts while preserving user content
|
|
586
|
-
- [x] `omh config` β view, reconfigure, or reset the saved AI provider (rotate expired keys, switch provider/model)
|
|
587
|
-
- [x] Autonomous loop engine β `omh-loop` skill, worktree-isolated runner, loop-guard, JSONL monitoring
|
|
588
|
-
- [ ] Community harness.yaml registry β share and reuse configs
|
|
589
|
-
- [ ] `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).
|
|
590
176
|
|
|
591
177
|
---
|
|
592
178
|
|
|
@@ -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
|
-
|
|
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
|
|
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,19 @@
|
|
|
1
|
+
import type { EventEmitter } from "node:events";
|
|
2
|
+
type SpawnLike = (cmd: string, args: string[], opts: {
|
|
3
|
+
detached: boolean;
|
|
4
|
+
stdio: "ignore";
|
|
5
|
+
}) => EventEmitter & {
|
|
6
|
+
unref(): void;
|
|
7
|
+
};
|
|
8
|
+
export declare function browserCommandFor(platform: NodeJS.Platform, url: string): [string, string[]];
|
|
9
|
+
/**
|
|
10
|
+
* Best-effort: open the URL in the OS default browser. Never throws; returns
|
|
11
|
+
* false so callers can tell the user to open it by hand (SSH, containers).
|
|
12
|
+
*/
|
|
13
|
+
export declare function openInBrowser(url: string, deps?: {
|
|
14
|
+
spawn?: SpawnLike;
|
|
15
|
+
platform?: NodeJS.Platform;
|
|
16
|
+
}): Promise<boolean>;
|
|
17
|
+
/** OSC 8 terminal hyperlink: stays one clickable link even when soft-wrapped. */
|
|
18
|
+
export declare function hyperlink(url: string): string;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
export function browserCommandFor(platform, url) {
|
|
3
|
+
if (platform === "darwin")
|
|
4
|
+
return ["open", [url]];
|
|
5
|
+
if (platform === "win32")
|
|
6
|
+
return ["cmd", ["/c", "start", "", url]];
|
|
7
|
+
return ["xdg-open", [url]];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Best-effort: open the URL in the OS default browser. Never throws; returns
|
|
11
|
+
* false so callers can tell the user to open it by hand (SSH, containers).
|
|
12
|
+
*/
|
|
13
|
+
export function openInBrowser(url, deps = {}) {
|
|
14
|
+
if (!/^https?:\/\//.test(url))
|
|
15
|
+
return Promise.resolve(false);
|
|
16
|
+
const spawn = deps.spawn ?? nodeSpawn;
|
|
17
|
+
const [cmd, args] = browserCommandFor(deps.platform ?? process.platform, url);
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
try {
|
|
20
|
+
const child = spawn(cmd, args, { detached: true, stdio: "ignore" });
|
|
21
|
+
child.once("error", () => resolve(false));
|
|
22
|
+
child.once("spawn", () => resolve(true));
|
|
23
|
+
child.unref();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
resolve(false);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/** OSC 8 terminal hyperlink: stays one clickable link even when soft-wrapped. */
|
|
31
|
+
export function hyperlink(url) {
|
|
32
|
+
const ESC = String.fromCharCode(27);
|
|
33
|
+
return `${ESC}]8;;${url}${ESC}\\${url}${ESC}]8;;${ESC}\\`;
|
|
34
|
+
}
|