oh-my-harness 0.10.2 → 0.11.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 +25 -17
- package/dist/bin/oh-my-harness.js +5 -0
- package/dist/catalog/blocks/branch-guard.js +6 -4
- package/dist/catalog/blocks/command-guard.js +3 -2
- package/dist/catalog/blocks/commit-test-gate.js +3 -2
- package/dist/catalog/blocks/commit-typecheck-gate.js +3 -2
- package/dist/catalog/blocks/config-audit.js +5 -14
- package/dist/catalog/blocks/lint-on-save.js +15 -2
- package/dist/catalog/blocks/lockfile-guard.js +3 -2
- package/dist/catalog/blocks/path-guard.js +15 -10
- package/dist/catalog/blocks/secret-file-guard.js +3 -2
- package/dist/catalog/blocks/sql-guard.js +3 -1
- package/dist/catalog/blocks/tdd-guard.js +11 -10
- package/dist/catalog/converter.js +2 -1
- package/dist/catalog/types.d.ts +2 -2
- package/dist/cli/command-checker.js +1 -1
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.js +37 -2
- package/dist/cli/commands/update.d.ts +18 -0
- package/dist/cli/commands/update.js +78 -0
- package/dist/cli/event-logger.d.ts +1 -0
- package/dist/cli/event-logger.js +4 -5
- package/dist/cli/harness-tester.js +5 -4
- package/dist/cli/index.js +12 -0
- package/dist/cli/installer-detect.d.ts +8 -0
- package/dist/cli/installer-detect.js +72 -0
- package/dist/cli/version-checker.d.ts +5 -0
- package/dist/cli/version-checker.js +27 -0
- package/dist/cli/version-notifier.d.ts +7 -0
- package/dist/cli/version-notifier.js +26 -0
- package/dist/core/generator.js +23 -9
- package/dist/core/harness-converter-v2.js +1 -1
- package/dist/generators/agents-md.d.ts +6 -0
- package/dist/generators/agents-md.js +5 -0
- package/dist/generators/claude-md.js +2 -35
- package/dist/generators/codex-config.d.ts +35 -0
- package/dist/generators/codex-config.js +102 -0
- package/dist/generators/hooks.js +80 -30
- package/dist/generators/managed-md.d.ts +2 -0
- package/dist/generators/managed-md.js +23 -0
- package/dist/utils/fs-helpers.d.ts +3 -0
- package/dist/utils/fs-helpers.js +38 -0
- package/dist/utils/paths.d.ts +11 -0
- package/dist/utils/paths.js +11 -0
- package/dist/utils/state-migration.d.ts +5 -0
- package/dist/utils/state-migration.js +131 -0
- package/package.json +4 -1
- package/presets/android/preset.yaml +150 -0
package/README.md
CHANGED
|
@@ -57,6 +57,7 @@ oh-my-harness init --preset nextjs fastapi
|
|
|
57
57
|
|
|
58
58
|
# Short alias works too
|
|
59
59
|
omh init "React app with TDD"
|
|
60
|
+
omh init --preset android # Android/Kotlin with Hilt, JUnit, Gradle
|
|
60
61
|
omh catalog list
|
|
61
62
|
omh test # Dry-run verify your harness
|
|
62
63
|
omh stats # TUI analytics dashboard
|
|
@@ -69,22 +70,28 @@ omh stats # TUI analytics dashboard
|
|
|
69
70
|
└── config.json # AI provider config (global, not per-project)
|
|
70
71
|
|
|
71
72
|
your-project/
|
|
72
|
-
├── CLAUDE.md # TDD rules,
|
|
73
|
+
├── CLAUDE.md # Claude Code instructions (TDD rules, standards)
|
|
74
|
+
├── AGENTS.md # Codex CLI instructions (same managed sections)
|
|
73
75
|
├── harness.yaml # Your harness config (source of truth)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
76
|
+
├── .omh/ # Single source of truth — hooks + state
|
|
77
|
+
│ ├── hooks/
|
|
78
|
+
│ │ ├── catalog-branch-guard.sh # Blocks commits on merged branches
|
|
79
|
+
│ │ ├── catalog-tdd-guard.sh # Enforces test-first workflow
|
|
80
|
+
│ │ ├── catalog-commit-test-gate.sh # Tests must pass before commit
|
|
81
|
+
│ │ ├── catalog-path-guard.sh # Protects build outputs
|
|
82
|
+
│ │ ├── catalog-command-guard.sh # Blocks dangerous commands
|
|
83
|
+
│ │ ├── catalog-lint-on-save.sh # Auto-lint on save
|
|
84
|
+
│ │ └── catalog-auto-pr.sh # Auto-create PR after push
|
|
85
|
+
│ ├── state/ # gitignored — log/runtime data
|
|
86
|
+
│ │ ├── events.jsonl # Unified hook event log (powers omh stats)
|
|
87
|
+
│ │ └── tdd-edits.json # TDD guard working state
|
|
88
|
+
│ └── manifest.json # Generated-files manifest
|
|
89
|
+
├── .claude/
|
|
90
|
+
│ ├── settings.json # Claude permissions + hooks → .omh/hooks/*.sh
|
|
91
|
+
│ └── oh-my-harness.json # Active preset tracking
|
|
92
|
+
└── .codex/
|
|
93
|
+
├── config.toml # [features] codex_hooks = true
|
|
94
|
+
└── hooks.json # Codex hooks → .omh/hooks/*.sh (same scripts)
|
|
88
95
|
```
|
|
89
96
|
|
|
90
97
|
---
|
|
@@ -176,7 +183,7 @@ All enforcement is powered by **catalog blocks** — reusable, parameterized hoo
|
|
|
176
183
|
| 🎨 `format-on-save` | auto-fix | Auto-format on file save |
|
|
177
184
|
| 🧪 `test-on-save` | auto-fix | Auto-run tests on file save |
|
|
178
185
|
| 🔀 `auto-pr` | automation | Auto-create PR after push |
|
|
179
|
-
| 🧪 `tdd-guard` | quality | Blocks source edits unless test modified first (JS/TS/Python) |
|
|
186
|
+
| 🧪 `tdd-guard` | quality | Blocks source edits unless test modified first (JS/TS/Python/Kotlin/Java) |
|
|
180
187
|
| 🔒 `sql-guard` | security | Blocks dangerous SQL operations |
|
|
181
188
|
| 🌳 `worktree-setup` | monorepo | Supports monorepo worktree patterns |
|
|
182
189
|
| 🗜️ `compact-context` | maintenance | Re-injects context on session start |
|
|
@@ -396,8 +403,9 @@ oh-my-harness/
|
|
|
396
403
|
- [x] Multi-provider AI support — Claude API, OpenAI, Gemini
|
|
397
404
|
- [x] Interactive model selection per provider
|
|
398
405
|
- [x] GitHub star prompt — first-time only
|
|
406
|
+
- [x] Codex emitter — `AGENTS.md` + `.codex/hooks.json` + `.codex/config.toml`
|
|
407
|
+
- [x] Unified `.omh/` layout — single source of truth for hooks & state across runtimes
|
|
399
408
|
- [ ] Cursor (`.cursor/rules/`) emitter
|
|
400
|
-
- [ ] Codex (`AGENTS.md`) emitter
|
|
401
409
|
- [ ] GitHub Copilot emitter
|
|
402
410
|
- [ ] Community preset registry
|
|
403
411
|
- [ ] `omh modify "change X"` — NL config editing
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import { createCli } from "../cli/index.js";
|
|
4
|
+
import { notifyIfUpdateAvailable } from "../cli/version-notifier.js";
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
const pkg = require("../../package.json");
|
|
7
|
+
notifyIfUpdateAvailable({ name: pkg.name, version: pkg.version });
|
|
3
8
|
const cli = createCli();
|
|
4
9
|
cli.parse(process.argv);
|
|
@@ -19,8 +19,9 @@ if echo "$COMMAND" | grep -qE "git commit|git push"; then
|
|
|
19
19
|
[[ -z "$BRANCH" ]] && exit 0
|
|
20
20
|
MAIN='{{mainBranch}}'
|
|
21
21
|
if [[ "$BRANCH" == "$MAIN" ]] || [[ "$BRANCH" == "master" && "$MAIN" == "main" ]]; then
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
REASON="oh-my-harness: direct commits to $BRANCH are blocked. Create a feature branch."
|
|
23
|
+
_log_event "block" "$REASON"
|
|
24
|
+
_emit_decision "block" "$REASON"
|
|
24
25
|
exit 0
|
|
25
26
|
fi
|
|
26
27
|
MERGED=0
|
|
@@ -37,8 +38,9 @@ if echo "$COMMAND" | grep -qE "git commit|git push"; then
|
|
|
37
38
|
fi
|
|
38
39
|
fi
|
|
39
40
|
if [[ "$MERGED" -eq 1 ]]; then
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
REASON="oh-my-harness: branch $BRANCH has already been merged to $MAIN. Create a new branch."
|
|
42
|
+
_log_event "block" "$REASON"
|
|
43
|
+
_emit_decision "block" "$REASON"
|
|
42
44
|
exit 0
|
|
43
45
|
fi
|
|
44
46
|
fi
|
|
@@ -25,8 +25,9 @@ NORMALIZED_CMD=$(printf '%s' "$COMMAND" | tr '[:space:]' ' ' | tr -s ' ')
|
|
|
25
25
|
PATTERNS=({{#each patterns}}"{{{this}}}" {{/each}})
|
|
26
26
|
for PATTERN in "\${PATTERNS[@]}"; do
|
|
27
27
|
if echo "$NORMALIZED_CMD" | grep -qF -- "$PATTERN"; then
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
REASON="oh-my-harness: command matches blocked pattern: $PATTERN"
|
|
29
|
+
_log_event "block" "$REASON"
|
|
30
|
+
_emit_decision "block" "$REASON"
|
|
30
31
|
exit 0
|
|
31
32
|
fi
|
|
32
33
|
done
|
|
@@ -17,8 +17,9 @@ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
|
17
17
|
if echo "$COMMAND" | grep -qE "git commit"; then
|
|
18
18
|
echo "oh-my-harness: Running {{{testCommand}}} before commit..." >&2
|
|
19
19
|
if ! {{{testCommand}}} >&2 2>&1; then
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
REASON="oh-my-harness: pre-commit check failed"
|
|
21
|
+
_log_event "block" "$REASON"
|
|
22
|
+
_emit_decision "block" "$REASON"
|
|
22
23
|
exit 0
|
|
23
24
|
fi
|
|
24
25
|
fi
|
|
@@ -17,8 +17,9 @@ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
|
17
17
|
if echo "$COMMAND" | grep -qE "git commit"; then
|
|
18
18
|
echo "oh-my-harness: Running {{{typecheckCommand}}} before commit..." >&2
|
|
19
19
|
if ! {{{typecheckCommand}}} >&2 2>&1; then
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
REASON="oh-my-harness: pre-commit check failed"
|
|
21
|
+
_log_event "block" "$REASON"
|
|
22
|
+
_emit_decision "block" "$REASON"
|
|
22
23
|
exit 0
|
|
23
24
|
fi
|
|
24
25
|
fi
|
|
@@ -1,29 +1,20 @@
|
|
|
1
1
|
export const configAudit = {
|
|
2
2
|
id: "config-audit",
|
|
3
3
|
name: "Config Audit",
|
|
4
|
-
description: "Logs configuration changes
|
|
4
|
+
description: "Logs configuration changes into the unified events.jsonl audit trail",
|
|
5
5
|
category: "audit",
|
|
6
6
|
event: "ConfigChange",
|
|
7
7
|
matcher: "",
|
|
8
8
|
canBlock: false,
|
|
9
|
-
params: [
|
|
10
|
-
{
|
|
11
|
-
name: "logFile",
|
|
12
|
-
type: "string",
|
|
13
|
-
description: "Path to the audit log file",
|
|
14
|
-
default: ".claude/hooks/.state/config-audit.log",
|
|
15
|
-
required: false,
|
|
16
|
-
},
|
|
17
|
-
],
|
|
9
|
+
params: [],
|
|
18
10
|
tags: ["audit", "config", "logging"],
|
|
19
11
|
template: `#!/bin/bash
|
|
20
12
|
set -euo pipefail
|
|
21
13
|
INPUT=$(cat)
|
|
22
|
-
LOG_FILE="{{{logFile}}}"
|
|
23
|
-
mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true
|
|
24
14
|
SOURCE=$(echo "$INPUT" | jq -r '.source // "unknown"' 2>/dev/null)
|
|
25
15
|
FILE=$(echo "$INPUT" | jq -r '.file_path // "unknown"' 2>/dev/null)
|
|
26
|
-
|
|
27
|
-
|
|
16
|
+
META=$(jq -nc --arg s "$SOURCE" --arg f "$FILE" '{source:$s,file:$f}' 2>/dev/null || true)
|
|
17
|
+
[ -z "$META" ] && META='{"source":"unknown","file":"unknown"}'
|
|
18
|
+
_log_event "allow" "" "$META"
|
|
28
19
|
exit 0`,
|
|
29
20
|
};
|
|
@@ -19,6 +19,13 @@ export const lintOnSave = {
|
|
|
19
19
|
description: "Lint command to run (e.g. eslint --fix)",
|
|
20
20
|
required: true,
|
|
21
21
|
},
|
|
22
|
+
{
|
|
23
|
+
name: "scope",
|
|
24
|
+
type: "string",
|
|
25
|
+
description: "Lint scope: 'file' passes $FILE_PATH to command, 'module' runs command without file arg",
|
|
26
|
+
required: false,
|
|
27
|
+
default: "file",
|
|
28
|
+
},
|
|
22
29
|
],
|
|
23
30
|
tags: ["lint", "auto-fix", "quality", "save"],
|
|
24
31
|
template: `#!/bin/bash
|
|
@@ -29,8 +36,14 @@ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path //
|
|
|
29
36
|
PATTERN='{{{filePattern}}}'
|
|
30
37
|
BASENAME=$(basename "$FILE_PATH")
|
|
31
38
|
if [[ "$BASENAME" == $PATTERN ]]; then
|
|
32
|
-
|
|
33
|
-
{
|
|
39
|
+
SCOPE='{{{scope}}}'
|
|
40
|
+
if [[ "\${SCOPE:-file}" == "module" ]]; then
|
|
41
|
+
echo "oh-my-harness: Running {{{command}}} ..." >&2
|
|
42
|
+
{{{command}}} >&2 || true
|
|
43
|
+
else
|
|
44
|
+
echo "oh-my-harness: Running {{{command}}} on $FILE_PATH..." >&2
|
|
45
|
+
{{{command}}} "$FILE_PATH" >&2 || true
|
|
46
|
+
fi
|
|
34
47
|
fi
|
|
35
48
|
exit 0`,
|
|
36
49
|
};
|
|
@@ -25,8 +25,9 @@ BASENAME=$(basename "$FILE_PATH")
|
|
|
25
25
|
LOCKFILES=({{#each lockfiles}}"{{{this}}}" {{/each}})
|
|
26
26
|
for LOCKFILE in "\${LOCKFILES[@]}"; do
|
|
27
27
|
if [[ "$BASENAME" == "$LOCKFILE" ]]; then
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
REASON="oh-my-harness: direct edits to lockfile $BASENAME are blocked. Use the package manager instead."
|
|
29
|
+
_log_event "block" "$REASON"
|
|
30
|
+
_emit_decision "block" "$REASON"
|
|
30
31
|
exit 0
|
|
31
32
|
fi
|
|
32
33
|
done
|
|
@@ -23,15 +23,17 @@ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path //
|
|
|
23
23
|
# Normalize path to prevent directory traversal attacks (e.g., ./foo/../dist/secret.js -> dist/secret.js)
|
|
24
24
|
if command -v python3 >/dev/null 2>&1; then
|
|
25
25
|
if ! NORMALIZED=$(python3 -c "import os,sys; print(os.path.normpath(sys.argv[1]))" "$FILE_PATH" 2>/dev/null); then
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
REASON="oh-my-harness: path normalization unavailable for non-canonical path"
|
|
27
|
+
_log_event "block" "$REASON"
|
|
28
|
+
_emit_decision "block" "$REASON"
|
|
28
29
|
exit 0
|
|
29
30
|
fi
|
|
30
31
|
else
|
|
31
32
|
case "$FILE_PATH" in
|
|
32
33
|
/*|../*|*/../*|*/..|./*|*/./*|*/.)
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
REASON="oh-my-harness: path normalization unavailable for non-canonical path"
|
|
35
|
+
_log_event "block" "$REASON"
|
|
36
|
+
_emit_decision "block" "$REASON"
|
|
35
37
|
exit 0
|
|
36
38
|
;;
|
|
37
39
|
esac
|
|
@@ -41,21 +43,24 @@ BLOCKED_PATHS=({{#each blockedPaths}}"{{{this}}}" {{/each}})
|
|
|
41
43
|
for BLOCKED in "\${BLOCKED_PATHS[@]}"; do
|
|
42
44
|
if [[ "$BLOCKED" == */ ]]; then
|
|
43
45
|
if [[ "$NORMALIZED" == "$BLOCKED"* || "$NORMALIZED" == *"/$BLOCKED"* ]]; then
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
REASON="oh-my-harness: file path matches blocked directory: $BLOCKED"
|
|
47
|
+
_log_event "block" "$REASON"
|
|
48
|
+
_emit_decision "block" "$REASON"
|
|
46
49
|
exit 0
|
|
47
50
|
fi
|
|
48
51
|
elif [[ "$BLOCKED" == \\** ]]; then
|
|
49
52
|
PATTERN="\${BLOCKED#\\*}"
|
|
50
53
|
if [[ "$NORMALIZED" == *"$PATTERN" ]]; then
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
REASON="oh-my-harness: file path matches blocked pattern: $BLOCKED"
|
|
55
|
+
_log_event "block" "$REASON"
|
|
56
|
+
_emit_decision "block" "$REASON"
|
|
53
57
|
exit 0
|
|
54
58
|
fi
|
|
55
59
|
else
|
|
56
60
|
if [[ "$NORMALIZED" == "$BLOCKED" || "$NORMALIZED" == *"/$BLOCKED" ]]; then
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
REASON="oh-my-harness: file path matches blocked path: $BLOCKED"
|
|
62
|
+
_log_event "block" "$REASON"
|
|
63
|
+
_emit_decision "block" "$REASON"
|
|
59
64
|
exit 0
|
|
60
65
|
fi
|
|
61
66
|
fi
|
|
@@ -25,8 +25,9 @@ BASENAME=$(basename "$FILE_PATH")
|
|
|
25
25
|
PATTERNS=({{#each patterns}}"{{{this}}}" {{/each}})
|
|
26
26
|
for PATTERN in "\${PATTERNS[@]}"; do
|
|
27
27
|
if [[ "$BASENAME" == $PATTERN ]]; then
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
REASON="oh-my-harness: file $BASENAME matches secret file pattern: $PATTERN"
|
|
29
|
+
_log_event "block" "$REASON"
|
|
30
|
+
_emit_decision "block" "$REASON"
|
|
30
31
|
exit 0
|
|
31
32
|
fi
|
|
32
33
|
done
|
|
@@ -26,7 +26,9 @@ PATTERNS=({{#each patterns}}"{{{this}}}" {{/each}})
|
|
|
26
26
|
for PATTERN in "\${PATTERNS[@]}"; do
|
|
27
27
|
PATTERN_LOWER=$(echo "$PATTERN" | tr '[:upper:]' '[:lower:]')
|
|
28
28
|
if echo "$COMMAND_LOWER" | grep -qF -- "$PATTERN_LOWER"; then
|
|
29
|
-
|
|
29
|
+
REASON="oh-my-harness: SQL command matches blocked pattern: $PATTERN"
|
|
30
|
+
_log_event "block" "$REASON"
|
|
31
|
+
_emit_decision "block" "$REASON"
|
|
30
32
|
exit 0
|
|
31
33
|
fi
|
|
32
34
|
done
|
|
@@ -33,9 +33,9 @@ case "\$FILE_PATH" in
|
|
|
33
33
|
*.json|*.yaml|*.yml|*.md|*.sh|*.css|*.html|*.svg|*.png|*.jpg) exit 0 ;;
|
|
34
34
|
esac
|
|
35
35
|
|
|
36
|
-
# edit-history 상태 파일
|
|
37
|
-
STATE_DIR="
|
|
38
|
-
HISTORY_FILE="\$STATE_DIR/
|
|
36
|
+
# edit-history 상태 파일 (logger wrapper가 _OMH_STATE_DIR 를 export)
|
|
37
|
+
STATE_DIR="\${_OMH_STATE_DIR:-.omh/state}"
|
|
38
|
+
HISTORY_FILE="\$STATE_DIR/tdd-edits.json"
|
|
39
39
|
mkdir -p "\$STATE_DIR" 2>/dev/null || true
|
|
40
40
|
|
|
41
41
|
TEST_RE='{{{testPattern}}}'
|
|
@@ -62,19 +62,20 @@ fi
|
|
|
62
62
|
|
|
63
63
|
# 대응 테스트 파일 확인 — 확장자 제거
|
|
64
64
|
BASENAME=$(basename "\$FILE_PATH" | sed -E 's/\\.[^.]+$//')
|
|
65
|
-
TEST_SUFFIX=".test."
|
|
66
65
|
|
|
66
|
+
REASON="oh-my-harness: TDD — \${BASENAME} 에 대응하는 테스트 파일을 먼저 수정하세요"
|
|
67
67
|
if [[ ! -f "\$HISTORY_FILE" ]]; then
|
|
68
|
-
_log_event "block" "
|
|
69
|
-
|
|
68
|
+
_log_event "block" "\$REASON"
|
|
69
|
+
_emit_decision "block" "\$REASON"
|
|
70
70
|
exit 0
|
|
71
71
|
fi
|
|
72
72
|
|
|
73
73
|
# edit-history에서 테스트 파일 검색 (tmp+mv로 원자적 쓰기, 크로스 플랫폼)
|
|
74
|
-
|
|
74
|
+
# Supports JS/TS (.test. / .spec. / test_) and JVM (Test suffix, e.g. IoViewTest.kt) conventions
|
|
75
|
+
if jq -e --arg b "\$BASENAME" '.edits[] | select(contains($b) and (contains(".test.") or contains(".spec.") or contains("test_") or endswith("Test.kt") or endswith("Test.java")))' "\$HISTORY_FILE" >/dev/null 2>&1; then
|
|
75
76
|
# 테스트 먼저 수정됨 → 매칭 테스트 기록 소비(제거) + 소스 기록 + 통과
|
|
76
77
|
UPDATED=$(jq --arg b "\$BASENAME" --arg f "\$FILE_PATH" '
|
|
77
|
-
.edits |= [.[] | select((contains($b) and (contains(".test.") or contains(".spec.") or contains("test_"))) | not)]
|
|
78
|
+
.edits |= [.[] | select((contains($b) and (contains(".test.") or contains(".spec.") or contains("test_") or endswith("Test.kt") or endswith("Test.java"))) | not)]
|
|
78
79
|
| .edits += [$f] | .edits |= unique
|
|
79
80
|
' "\$HISTORY_FILE" 2>/dev/null) || true
|
|
80
81
|
if [[ -n "\$UPDATED" ]]; then
|
|
@@ -89,8 +90,8 @@ if [[ "\$DECISION" == "allow" ]]; then
|
|
|
89
90
|
exit 0
|
|
90
91
|
fi
|
|
91
92
|
|
|
92
|
-
_log_event "block" "
|
|
93
|
-
|
|
93
|
+
_log_event "block" "\$REASON"
|
|
94
|
+
_emit_decision "block" "\$REASON"
|
|
94
95
|
exit 0`,
|
|
95
96
|
tags: ["tdd", "workflow", "quality"],
|
|
96
97
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { renderTemplate, validateParams, applyDefaults } from "./template-engine.js";
|
|
2
|
+
import { OMH_HOOKS_DIR } from "../utils/paths.js";
|
|
2
3
|
export async function convertHookEntries(entries, registry, _projectDir) {
|
|
3
4
|
const hooksConfig = {};
|
|
4
5
|
const scripts = new Map();
|
|
@@ -28,7 +29,7 @@ export async function convertHookEntries(entries, registry, _projectDir) {
|
|
|
28
29
|
const count = blockInstanceCount.get(entry.block) ?? 0;
|
|
29
30
|
blockInstanceCount.set(entry.block, count + 1);
|
|
30
31
|
const scriptName = count === 0 ? `${entry.block}.sh` : `${entry.block}-${count}.sh`;
|
|
31
|
-
const scriptPath =
|
|
32
|
+
const scriptPath = `${OMH_HOOKS_DIR}/${scriptName}`;
|
|
32
33
|
scripts.set(scriptPath, scriptContent);
|
|
33
34
|
const hookEntry = {
|
|
34
35
|
type: "command",
|
package/dist/catalog/types.d.ts
CHANGED
|
@@ -85,7 +85,7 @@ export declare const BuildingBlockSchema: z.ZodObject<{
|
|
|
85
85
|
template: string;
|
|
86
86
|
name: string;
|
|
87
87
|
tags: string[];
|
|
88
|
-
event: "PreToolUse" | "PostToolUse" | "SessionStart" | "Notification" | "ConfigChange" | "WorktreeCreate" | "
|
|
88
|
+
event: "PreToolUse" | "PostToolUse" | "SessionStart" | "Notification" | "ConfigChange" | "WorktreeCreate" | "UserPromptSubmit" | "Stop" | "PreCompact" | "PostCompact" | "SubagentStop" | "PreBash" | "PostBash" | "PreEdit" | "PostEdit" | "PreRead" | "PostRead" | "PreWrite" | "PostWrite" | "SessionEnd" | "PreToolResult" | "PostToolResult" | "WorktreeRemove";
|
|
89
89
|
category: "notification" | "custom" | "git" | "quality" | "security" | "formatting" | "auto-fix" | "automation" | "file-protection" | "audit";
|
|
90
90
|
canBlock: boolean;
|
|
91
91
|
matcher?: string | undefined;
|
|
@@ -102,7 +102,7 @@ export declare const BuildingBlockSchema: z.ZodObject<{
|
|
|
102
102
|
template: string;
|
|
103
103
|
name: string;
|
|
104
104
|
tags: string[];
|
|
105
|
-
event: "PreToolUse" | "PostToolUse" | "SessionStart" | "Notification" | "ConfigChange" | "WorktreeCreate" | "
|
|
105
|
+
event: "PreToolUse" | "PostToolUse" | "SessionStart" | "Notification" | "ConfigChange" | "WorktreeCreate" | "UserPromptSubmit" | "Stop" | "PreCompact" | "PostCompact" | "SubagentStop" | "PreBash" | "PostBash" | "PreEdit" | "PostEdit" | "PreRead" | "PostRead" | "PreWrite" | "PostWrite" | "SessionEnd" | "PreToolResult" | "PostToolResult" | "WorktreeRemove";
|
|
106
106
|
category: "notification" | "custom" | "git" | "quality" | "security" | "formatting" | "auto-fix" | "automation" | "file-protection" | "audit";
|
|
107
107
|
canBlock: boolean;
|
|
108
108
|
matcher?: string | undefined;
|
|
@@ -61,7 +61,7 @@ export async function checkHarnessCommands(hooks, projectDir) {
|
|
|
61
61
|
const fs = await import("node:fs/promises");
|
|
62
62
|
const path = await import("node:path");
|
|
63
63
|
for (const hook of hooks) {
|
|
64
|
-
const scriptPath = hook.command.replace(/^bash\s+/, "").replace(/^"|"$/g, "");
|
|
64
|
+
const scriptPath = hook.command.replace(/^bash\s+/, "").replace(/^['"]|['"]$/g, "");
|
|
65
65
|
const fullPath = path.join(projectDir, scriptPath);
|
|
66
66
|
let content;
|
|
67
67
|
try {
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { parse } from "smol-toml";
|
|
4
|
+
import { OMH_HOOKS_DIR } from "../../utils/paths.js";
|
|
3
5
|
export async function doctorCommand(options = {}) {
|
|
4
6
|
const projectDir = options.projectDir ?? process.cwd();
|
|
5
7
|
const messages = [];
|
|
6
8
|
const checks = {
|
|
7
9
|
stateFile: false,
|
|
8
10
|
claudeMd: false,
|
|
11
|
+
agentsMd: false,
|
|
9
12
|
settingsJson: false,
|
|
13
|
+
codexConfig: false,
|
|
10
14
|
hooksExecutable: false,
|
|
11
15
|
};
|
|
12
16
|
// 1. Check .claude/oh-my-harness.json exists
|
|
@@ -48,8 +52,39 @@ export async function doctorCommand(options = {}) {
|
|
|
48
52
|
messages.push("FAIL: .claude/settings.json is not valid JSON.");
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
|
-
// 4.
|
|
52
|
-
const
|
|
55
|
+
// 4. AGENTS.md (Codex)
|
|
56
|
+
const agentsMdPath = path.join(projectDir, "AGENTS.md");
|
|
57
|
+
try {
|
|
58
|
+
await fs.access(agentsMdPath);
|
|
59
|
+
checks.agentsMd = true;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
messages.push("FAIL: AGENTS.md not found.");
|
|
63
|
+
}
|
|
64
|
+
// 5. .codex/hooks.json + .codex/config.toml — both must exist AND parse.
|
|
65
|
+
const codexHooksPath = path.join(projectDir, ".codex", "hooks.json");
|
|
66
|
+
const codexTomlPath = path.join(projectDir, ".codex", "config.toml");
|
|
67
|
+
try {
|
|
68
|
+
JSON.parse(await fs.readFile(codexHooksPath, "utf-8"));
|
|
69
|
+
const tomlRaw = await fs.readFile(codexTomlPath, "utf-8");
|
|
70
|
+
const parsed = parse(tomlRaw);
|
|
71
|
+
if (parsed.features?.codex_hooks !== true) {
|
|
72
|
+
messages.push("FAIL: .codex/config.toml missing [features] codex_hooks = true.");
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
checks.codexConfig = true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
if (err.code === "ENOENT") {
|
|
80
|
+
messages.push("FAIL: .codex/hooks.json or .codex/config.toml not found.");
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
messages.push("FAIL: .codex/hooks.json or .codex/config.toml is invalid or unreadable.");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// 6. Check hook scripts exist and are executable
|
|
87
|
+
const hooksDir = path.join(projectDir, OMH_HOOKS_DIR);
|
|
53
88
|
try {
|
|
54
89
|
const files = await fs.readdir(hooksDir);
|
|
55
90
|
const scripts = files.filter((f) => f.endsWith(".sh"));
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface UpdateOptions {
|
|
2
|
+
yes?: boolean;
|
|
3
|
+
dryRun?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface UpdateDeps {
|
|
6
|
+
fetchLatest?: (name: string) => Promise<string | null>;
|
|
7
|
+
spawn?: (cmd: string, args: string[]) => {
|
|
8
|
+
status: number;
|
|
9
|
+
};
|
|
10
|
+
env?: NodeJS.ProcessEnv;
|
|
11
|
+
}
|
|
12
|
+
export interface UpdateResult {
|
|
13
|
+
exitCode: number;
|
|
14
|
+
ran: boolean;
|
|
15
|
+
command?: string;
|
|
16
|
+
latestVersion?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function updateCommand(currentVersion: string, options?: UpdateOptions, deps?: UpdateDeps): Promise<UpdateResult>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { detectInstaller } from "../installer-detect.js";
|
|
3
|
+
import { fetchLatestVersion } from "../version-checker.js";
|
|
4
|
+
const PKG_NAME = "oh-my-harness";
|
|
5
|
+
function defaultSpawn(cmd, args) {
|
|
6
|
+
const result = spawnSync(cmd, args, { stdio: "inherit" });
|
|
7
|
+
return { status: result.status ?? 1 };
|
|
8
|
+
}
|
|
9
|
+
export async function updateCommand(currentVersion, options = {}, deps = {}) {
|
|
10
|
+
const fetchLatest = deps.fetchLatest ?? fetchLatestVersion;
|
|
11
|
+
const spawn = deps.spawn ?? defaultSpawn;
|
|
12
|
+
const env = deps.env ?? process.env;
|
|
13
|
+
console.log(`Current version: ${currentVersion}`);
|
|
14
|
+
console.log("Checking for updates…");
|
|
15
|
+
const latest = await fetchLatest(PKG_NAME);
|
|
16
|
+
if (!latest) {
|
|
17
|
+
console.log("Could not reach npm registry. Check your network and try again.");
|
|
18
|
+
return { exitCode: 1, ran: false };
|
|
19
|
+
}
|
|
20
|
+
if (latest === currentVersion) {
|
|
21
|
+
console.log(`Already up to date (${currentVersion}).`);
|
|
22
|
+
return { exitCode: 0, ran: false, latestVersion: latest };
|
|
23
|
+
}
|
|
24
|
+
console.log(`Update available: ${currentVersion} → ${latest}`);
|
|
25
|
+
const info = detectInstaller(env);
|
|
26
|
+
console.log(`Detected installer: ${info.installer}`);
|
|
27
|
+
if (info.notes) {
|
|
28
|
+
console.log(info.notes);
|
|
29
|
+
}
|
|
30
|
+
console.log(`Update command: ${info.updateCommand}`);
|
|
31
|
+
if (info.isEphemeral) {
|
|
32
|
+
console.log("Run the command above to install globally.");
|
|
33
|
+
return {
|
|
34
|
+
exitCode: 0,
|
|
35
|
+
ran: false,
|
|
36
|
+
command: info.updateCommand,
|
|
37
|
+
latestVersion: latest,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (options.dryRun) {
|
|
41
|
+
return {
|
|
42
|
+
exitCode: 0,
|
|
43
|
+
ran: false,
|
|
44
|
+
command: info.updateCommand,
|
|
45
|
+
latestVersion: latest,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (!options.yes && process.stdout.isTTY && process.stdin.isTTY) {
|
|
49
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
50
|
+
const proceed = await confirm({
|
|
51
|
+
message: `Run "${info.updateCommand}"?`,
|
|
52
|
+
default: true,
|
|
53
|
+
});
|
|
54
|
+
if (!proceed) {
|
|
55
|
+
console.log("Cancelled.");
|
|
56
|
+
return {
|
|
57
|
+
exitCode: 0,
|
|
58
|
+
ran: false,
|
|
59
|
+
command: info.updateCommand,
|
|
60
|
+
latestVersion: latest,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const [cmd, ...args] = info.updateCommand.split(" ");
|
|
65
|
+
const { status } = spawn(cmd, args);
|
|
66
|
+
if (status === 0) {
|
|
67
|
+
console.log(`Updated to ${latest}.`);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
console.log(`Update failed with exit code ${status}. Try running the command manually.`);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
exitCode: status,
|
|
74
|
+
ran: true,
|
|
75
|
+
command: info.updateCommand,
|
|
76
|
+
latestVersion: latest,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -5,6 +5,7 @@ export interface HookEvent {
|
|
|
5
5
|
decision: "block" | "allow" | "error";
|
|
6
6
|
reason?: string;
|
|
7
7
|
tool?: string;
|
|
8
|
+
meta?: Record<string, unknown>;
|
|
8
9
|
}
|
|
9
10
|
export declare function appendEvent(projectDir: string, hookEvent: HookEvent): Promise<void>;
|
|
10
11
|
export declare function readEvents(projectDir: string): Promise<HookEvent[]>;
|
package/dist/cli/event-logger.js
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
const EVENTS_FILE = "events.jsonl";
|
|
3
|
+
import { OMH_STATE_DIR, OMH_EVENTS_FILE } from "../utils/paths.js";
|
|
5
4
|
export async function appendEvent(projectDir, hookEvent) {
|
|
6
|
-
const stateDir = path.join(projectDir,
|
|
5
|
+
const stateDir = path.join(projectDir, OMH_STATE_DIR);
|
|
7
6
|
await fs.mkdir(stateDir, { recursive: true });
|
|
8
|
-
const filePath = path.join(stateDir,
|
|
7
|
+
const filePath = path.join(stateDir, OMH_EVENTS_FILE);
|
|
9
8
|
await fs.appendFile(filePath, JSON.stringify(hookEvent) + "\n", "utf-8");
|
|
10
9
|
}
|
|
11
10
|
export async function readEvents(projectDir) {
|
|
12
|
-
const filePath = path.join(projectDir,
|
|
11
|
+
const filePath = path.join(projectDir, OMH_STATE_DIR, OMH_EVENTS_FILE);
|
|
13
12
|
let content;
|
|
14
13
|
try {
|
|
15
14
|
content = await fs.readFile(filePath, "utf-8");
|