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.
- package/README.md +30 -340
- package/dist/catalog/blocks/index.js +2 -0
- package/dist/catalog/blocks/loop-guard.d.ts +10 -0
- package/dist/catalog/blocks/loop-guard.js +120 -0
- package/dist/catalog/blocks/tdd-guard.js +1 -1
- package/dist/cli/commands/config.js +11 -2
- package/dist/cli/commands/loop.d.ts +54 -0
- package/dist/cli/commands/loop.js +221 -0
- package/dist/cli/index.js +56 -0
- 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/core/generator.js +64 -3
- package/dist/core/harness-converter-v2.js +24 -0
- package/dist/core/harness-schema.d.ts +118 -1
- package/dist/core/harness-schema.js +45 -0
- package/dist/core/merged-config.d.ts +33 -0
- package/dist/core/uninstall.js +15 -0
- package/dist/generators/codex-config.js +2 -1
- package/dist/generators/loop-assets.d.ts +14 -0
- package/dist/generators/loop-assets.js +15 -0
- package/dist/loop/classify.d.ts +36 -0
- package/dist/loop/classify.js +46 -0
- package/dist/loop/ledger.d.ts +19 -0
- package/dist/loop/ledger.js +33 -0
- package/dist/loop/protocol.d.ts +15 -0
- package/dist/loop/protocol.js +81 -0
- package/dist/loop/runtime.d.ts +35 -0
- package/dist/loop/runtime.js +112 -0
- package/dist/loop/state.d.ts +79 -0
- package/dist/loop/state.js +256 -0
- package/dist/loop/stop.d.ts +36 -0
- package/dist/loop/stop.js +127 -0
- package/dist/loop/supervisor.d.ts +27 -0
- package/dist/loop/supervisor.js +188 -0
- package/dist/loop/worktree.d.ts +38 -0
- package/dist/loop/worktree.js +166 -0
- 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,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
|
-
|
|
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
|
-
|
|
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
|
-
- **
|
|
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
|
-
|
|
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,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>;
|