oh-my-harness 0.3.2 → 0.4.1

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.
@@ -19,6 +19,7 @@ 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
+ _log_event "block" "oh-my-harness: direct commits to $BRANCH are blocked. Create a feature branch."
22
23
  echo "{\\"decision\\": \\"block\\", \\"reason\\": \\"oh-my-harness: direct commits to $BRANCH are blocked. Create a feature branch.\\"}"
23
24
  exit 0
24
25
  fi
@@ -36,6 +37,7 @@ if echo "$COMMAND" | grep -qE "git commit|git push"; then
36
37
  fi
37
38
  fi
38
39
  if [[ "$MERGED" -eq 1 ]]; then
40
+ _log_event "block" "oh-my-harness: branch $BRANCH has already been merged to $MAIN. Create a new branch."
39
41
  echo "{\\"decision\\": \\"block\\", \\"reason\\": \\"oh-my-harness: branch $BRANCH has already been merged to $MAIN. Create a new branch.\\"}"
40
42
  exit 0
41
43
  fi
@@ -24,6 +24,7 @@ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
24
24
  PATTERNS=({{#each patterns}}"{{this}}" {{/each}})
25
25
  for PATTERN in "\${PATTERNS[@]}"; do
26
26
  if echo "$COMMAND" | grep -qF "$PATTERN"; then
27
+ _log_event "block" "oh-my-harness: command matches blocked pattern: $PATTERN"
27
28
  echo "{\\"decision\\": \\"block\\", \\"reason\\": \\"oh-my-harness: command matches blocked pattern: $PATTERN\\"}"
28
29
  exit 0
29
30
  fi
@@ -17,6 +17,7 @@ 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
+ _log_event "block" "oh-my-harness: pre-commit check failed"
20
21
  echo "{\\"decision\\": \\"block\\", \\"reason\\": \\"oh-my-harness: pre-commit check failed\\"}"
21
22
  exit 0
22
23
  fi
@@ -17,6 +17,7 @@ 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
+ _log_event "block" "oh-my-harness: pre-commit check failed"
20
21
  echo "{\\"decision\\": \\"block\\", \\"reason\\": \\"oh-my-harness: pre-commit check failed\\"}"
21
22
  exit 0
22
23
  fi
@@ -8,6 +8,7 @@ import { lintOnSave } from "./lint-on-save.js";
8
8
  import { formatOnSave } from "./format-on-save.js";
9
9
  import { autoPr } from "./auto-pr.js";
10
10
  import { secretFileGuard } from "./secret-file-guard.js";
11
+ import { tddGuard } from "./tdd-guard.js";
11
12
  export const builtinBlocks = [
12
13
  branchGuard,
13
14
  commitTestGate,
@@ -19,4 +20,5 @@ export const builtinBlocks = [
19
20
  formatOnSave,
20
21
  autoPr,
21
22
  secretFileGuard,
23
+ tddGuard,
22
24
  ];
@@ -25,6 +25,7 @@ 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
+ _log_event "block" "oh-my-harness: direct edits to lockfile $BASENAME are blocked. Use the package manager instead."
28
29
  echo "{\\"decision\\": \\"block\\", \\"reason\\": \\"oh-my-harness: direct edits to lockfile $BASENAME are blocked. Use the package manager instead.\\"}"
29
30
  exit 0
30
31
  fi
@@ -24,17 +24,20 @@ BLOCKED_PATHS=({{#each blockedPaths}}"{{this}}" {{/each}})
24
24
  for BLOCKED in "\${BLOCKED_PATHS[@]}"; do
25
25
  if [[ "$BLOCKED" == */ ]]; then
26
26
  if [[ "$FILE_PATH" == "$BLOCKED"* || "$FILE_PATH" == *"/$BLOCKED"* ]]; then
27
+ _log_event "block" "oh-my-harness: file path matches blocked directory: $BLOCKED"
27
28
  echo "{\\"decision\\": \\"block\\", \\"reason\\": \\"oh-my-harness: file path matches blocked directory: $BLOCKED\\"}"
28
29
  exit 0
29
30
  fi
30
31
  elif [[ "$BLOCKED" == \\** ]]; then
31
32
  PATTERN="\${BLOCKED#\\*}"
32
33
  if [[ "$FILE_PATH" == *"$PATTERN" ]]; then
34
+ _log_event "block" "oh-my-harness: file path matches blocked pattern: $BLOCKED"
33
35
  echo "{\\"decision\\": \\"block\\", \\"reason\\": \\"oh-my-harness: file path matches blocked pattern: $BLOCKED\\"}"
34
36
  exit 0
35
37
  fi
36
38
  else
37
39
  if [[ "$FILE_PATH" == "$BLOCKED" || "$FILE_PATH" == *"/$BLOCKED" ]]; then
40
+ _log_event "block" "oh-my-harness: file path matches blocked path: $BLOCKED"
38
41
  echo "{\\"decision\\": \\"block\\", \\"reason\\": \\"oh-my-harness: file path matches blocked path: $BLOCKED\\"}"
39
42
  exit 0
40
43
  fi
@@ -25,6 +25,7 @@ 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
+ _log_event "block" "oh-my-harness: file $BASENAME matches secret file pattern: $PATTERN"
28
29
  echo "{\\"decision\\": \\"block\\", \\"reason\\": \\"oh-my-harness: file $BASENAME matches secret file pattern: $PATTERN\\"}"
29
30
  exit 0
30
31
  fi
@@ -0,0 +1,2 @@
1
+ import type { BuildingBlock } from "../types.js";
2
+ export declare const tddGuard: BuildingBlock;
@@ -0,0 +1,82 @@
1
+ export const tddGuard = {
2
+ id: "tdd-guard",
3
+ name: "TDD Guard",
4
+ description: "Blocks source file edits unless corresponding test file was modified first",
5
+ category: "quality",
6
+ event: "PreToolUse",
7
+ matcher: "Edit|Write",
8
+ canBlock: true,
9
+ params: [
10
+ {
11
+ name: "srcPattern",
12
+ type: "string",
13
+ description: "Regex pattern for source files to guard (default: .ts/.tsx/.js/.jsx)",
14
+ required: false,
15
+ default: "\\.(ts|tsx|js|jsx)$",
16
+ },
17
+ {
18
+ name: "testPattern",
19
+ type: "string",
20
+ description: "Regex pattern for test files (default: .test.ts/.spec.ts etc.)",
21
+ required: false,
22
+ default: "\\.(test|spec)\\.(ts|tsx|js|jsx)$",
23
+ },
24
+ ],
25
+ template: `#!/bin/bash
26
+ set -euo pipefail
27
+ INPUT=$(cat)
28
+ FILE_PATH=$(echo "\$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty' 2>/dev/null)
29
+ [[ -z "\$FILE_PATH" ]] && exit 0
30
+
31
+ # 비코드 파일은 통과
32
+ case "\$FILE_PATH" in
33
+ *.json|*.yaml|*.yml|*.md|*.sh|*.css|*.html|*.svg|*.png|*.jpg) exit 0 ;;
34
+ esac
35
+
36
+ # edit-history 상태 파일
37
+ STATE_DIR=".claude/hooks/.state"
38
+ HISTORY_FILE="\$STATE_DIR/edit-history.json"
39
+ mkdir -p "\$STATE_DIR" 2>/dev/null || true
40
+
41
+ if echo "\$FILE_PATH" | grep -qE '{{testPattern}}'; then
42
+ # 테스트 파일 수정 → 기록 + 통과
43
+ if [[ ! -f "\$HISTORY_FILE" ]]; then
44
+ echo '{"edits":[]}' > "\$HISTORY_FILE"
45
+ fi
46
+ UPDATED=$(jq --arg f "\$FILE_PATH" '.edits += [$f] | .edits |= unique' "\$HISTORY_FILE" 2>/dev/null) || true
47
+ if [[ -n "\$UPDATED" ]]; then
48
+ echo "\$UPDATED" > "\$HISTORY_FILE"
49
+ fi
50
+ exit 0
51
+ fi
52
+
53
+ # 소스 파일 (.ts/.tsx/.js/.jsx) 이 아니면 통과
54
+ if ! echo "\$FILE_PATH" | grep -qE '{{srcPattern}}'; then
55
+ exit 0
56
+ fi
57
+
58
+ # 대응 테스트 파일 확인
59
+ BASENAME=$(basename "\$FILE_PATH" | sed -E 's/\\.(ts|tsx|js|jsx)$//')
60
+ TEST_SUFFIX=".test."
61
+
62
+ if [[ ! -f "\$HISTORY_FILE" ]]; then
63
+ _log_event "block" "oh-my-harness: TDD — \${BASENAME}\${TEST_SUFFIX}* 테스트 파일을 먼저 수정하세요"
64
+ echo "{\\"decision\\": \\"block\\", \\"reason\\": \\"oh-my-harness: TDD — \${BASENAME}\${TEST_SUFFIX}* 테스트 파일을 먼저 수정하세요\\"}"
65
+ exit 0
66
+ fi
67
+
68
+ # edit-history에서 테스트 파일 검색
69
+ if jq -e --arg b "\$BASENAME" '.edits[] | select(contains($b + ".test.") or contains($b + ".spec."))' "\$HISTORY_FILE" >/dev/null 2>&1; then
70
+ # 테스트 먼저 수정됨 → 소스 기록 + 통과
71
+ UPDATED=$(jq --arg f "\$FILE_PATH" '.edits += [$f] | .edits |= unique' "\$HISTORY_FILE" 2>/dev/null) || true
72
+ if [[ -n "\$UPDATED" ]]; then
73
+ echo "\$UPDATED" > "\$HISTORY_FILE"
74
+ fi
75
+ exit 0
76
+ fi
77
+
78
+ _log_event "block" "oh-my-harness: TDD — \${BASENAME}\${TEST_SUFFIX}* 테스트 파일을 먼저 수정하세요"
79
+ echo "{\\"decision\\": \\"block\\", \\"reason\\": \\"oh-my-harness: TDD — \${BASENAME}\${TEST_SUFFIX}* 테스트 파일을 먼저 수정하세요\\"}"
80
+ exit 0`,
81
+ tags: ["tdd", "workflow", "quality"],
82
+ };
@@ -0,0 +1,21 @@
1
+ export interface HookEvent {
2
+ ts: string;
3
+ event: string;
4
+ hook: string;
5
+ decision: "block" | "allow";
6
+ reason?: string;
7
+ tool?: string;
8
+ }
9
+ export declare function appendEvent(projectDir: string, hookEvent: HookEvent): Promise<void>;
10
+ export declare function readEvents(projectDir: string): Promise<HookEvent[]>;
11
+ export declare function getSessionEvents(projectDir: string, since?: Date): Promise<HookEvent[]>;
12
+ export interface EventStats {
13
+ totalEvents: number;
14
+ blockCount: number;
15
+ allowCount: number;
16
+ byHook: Record<string, {
17
+ block: number;
18
+ allow: number;
19
+ }>;
20
+ }
21
+ export declare function aggregateStats(events: HookEvent[]): EventStats;
@@ -0,0 +1,64 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ const STATE_DIR = ".claude/hooks/.state";
4
+ const EVENTS_FILE = "events.jsonl";
5
+ export async function appendEvent(projectDir, hookEvent) {
6
+ const stateDir = path.join(projectDir, STATE_DIR);
7
+ await fs.mkdir(stateDir, { recursive: true });
8
+ const filePath = path.join(stateDir, EVENTS_FILE);
9
+ await fs.appendFile(filePath, JSON.stringify(hookEvent) + "\n", "utf-8");
10
+ }
11
+ export async function readEvents(projectDir) {
12
+ const filePath = path.join(projectDir, STATE_DIR, EVENTS_FILE);
13
+ let content;
14
+ try {
15
+ content = await fs.readFile(filePath, "utf-8");
16
+ }
17
+ catch {
18
+ return [];
19
+ }
20
+ const events = [];
21
+ for (const line of content.split("\n")) {
22
+ const trimmed = line.trim();
23
+ if (!trimmed)
24
+ continue;
25
+ try {
26
+ const parsed = JSON.parse(trimmed);
27
+ if (typeof parsed.ts === "string" &&
28
+ typeof parsed.event === "string" &&
29
+ typeof parsed.hook === "string" &&
30
+ (parsed.decision === "block" || parsed.decision === "allow")) {
31
+ events.push(parsed);
32
+ }
33
+ }
34
+ catch {
35
+ // 잘못된 줄 무시
36
+ }
37
+ }
38
+ return events;
39
+ }
40
+ export async function getSessionEvents(projectDir, since) {
41
+ const events = await readEvents(projectDir);
42
+ if (!since)
43
+ return events;
44
+ return events.filter((e) => new Date(e.ts) >= since);
45
+ }
46
+ export function aggregateStats(events) {
47
+ const stats = {
48
+ totalEvents: events.length,
49
+ blockCount: 0,
50
+ allowCount: 0,
51
+ byHook: {},
52
+ };
53
+ for (const e of events) {
54
+ if (e.decision === "block")
55
+ stats.blockCount++;
56
+ else
57
+ stats.allowCount++;
58
+ if (!stats.byHook[e.hook]) {
59
+ stats.byHook[e.hook] = { block: 0, allow: 0 };
60
+ }
61
+ stats.byHook[e.hook][e.decision]++;
62
+ }
63
+ return stats;
64
+ }
@@ -15,7 +15,7 @@ export async function generate(options) {
15
15
  await generateSettings({ projectDir, config, hooksOutput });
16
16
  files.push(`${projectDir}/.claude/settings.json`);
17
17
  // Update .gitignore
18
- await updateGitignore(projectDir, [".claude/hooks/"]);
18
+ await updateGitignore(projectDir, [".claude/hooks/", ".claude/hooks/.state/"]);
19
19
  files.push(`${projectDir}/.gitignore`);
20
20
  return { files };
21
21
  }
@@ -18,6 +18,24 @@ async function readJsonFile(filePath) {
18
18
  return null;
19
19
  }
20
20
  }
21
+ /**
22
+ * Resolve scripts.test content into a direct executable command.
23
+ * - Prevents watch mode hang (vitest → vitest run)
24
+ * - Adds npx prefix for direct runner invocation
25
+ */
26
+ function resolveTestCommand(testScript) {
27
+ const trimmed = testScript.trim();
28
+ // vitest without "run" → add --run to prevent watch mode
29
+ if (/^vitest$/.test(trimmed)) {
30
+ return "npx vitest run";
31
+ }
32
+ // Known runners: prefix with npx if not already
33
+ if (/^(vitest|jest|mocha)\b/.test(trimmed)) {
34
+ return trimmed.startsWith("npx ") ? trimmed : `npx ${trimmed}`;
35
+ }
36
+ // Unknown script content: use as-is (e.g. custom shell commands)
37
+ return trimmed;
38
+ }
21
39
  export const nodeDetector = {
22
40
  name: "node",
23
41
  detect: async (projectDir) => {
@@ -53,19 +71,18 @@ export const nodeDetector = {
53
71
  packageManagers.push("npm");
54
72
  detectedFiles.push("package-lock.json");
55
73
  }
56
- // Determine lint command prefix and test command based on package manager
74
+ // Determine lint command prefix based on package manager
57
75
  const lintCmd = pm === "npm" ? "npm run lint" : `${pm} lint`;
58
- const testCmdDefault = pm === "npm" ? "npm test" : `${pm} test`;
59
76
  // Read package.json to inspect scripts
60
77
  const pkg = await readJsonFile(packageJsonPath);
61
78
  const scripts = pkg && typeof pkg.scripts === "object" && pkg.scripts !== null
62
79
  ? pkg.scripts
63
80
  : {};
64
- // Detect test runner from scripts.test
65
- const testScript = scripts["test"];
66
- testCommands.push(testCmdDefault);
67
- if (testScript && /vitest|jest|mocha/.test(testScript) && testScript !== testCmdDefault) {
68
- testCommands.push(testScript);
81
+ // Detect test runner from scripts.test — use direct command instead of npm test wrapper
82
+ const rawTestScript = scripts["test"];
83
+ if (typeof rawTestScript === "string" && rawTestScript.trim().length > 0) {
84
+ const resolved = resolveTestCommand(rawTestScript);
85
+ testCommands.push(resolved);
69
86
  }
70
87
  lintCommands.push(lintCmd);
71
88
  // Detect Next.js
@@ -1,4 +1,5 @@
1
1
  import type { MergedConfig } from "../core/preset-types.js";
2
+ export declare function wrapWithLogger(script: string, event?: string): string;
2
3
  export interface GenerateHooksOptions {
3
4
  projectDir: string;
4
5
  config: MergedConfig;
@@ -1,5 +1,37 @@
1
1
  import { mkdir, writeFile, chmod } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
+ function buildLoggerSnippet(event) {
4
+ return `# --- oh-my-harness event logger ---
5
+ _OMH_STATE_DIR=".claude/hooks/.state"
6
+ mkdir -p "$_OMH_STATE_DIR" 2>/dev/null || true
7
+ _OMH_HOOK_NAME="$(basename "$0")"
8
+ _OMH_EVENT="${event}"
9
+ _OMH_LOGGED=0
10
+ _log_event() {
11
+ _OMH_LOGGED=1
12
+ local decision="\${1:-allow}" reason="\${2:-}"
13
+ printf '{"ts":"%s","event":"%s","hook":"%s","decision":"%s","reason":"%s"}\\n' \\
14
+ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$_OMH_EVENT" "$_OMH_HOOK_NAME" "$decision" "$reason" \\
15
+ >> "$_OMH_STATE_DIR/events.jsonl"
16
+ }
17
+ trap '[ "$_OMH_LOGGED" -eq 0 ] && _log_event "allow"' EXIT
18
+ # --- end logger ---`;
19
+ }
20
+ export function wrapWithLogger(script, event = "unknown") {
21
+ const snippet = buildLoggerSnippet(event);
22
+ if (script.includes("INPUT=$(cat)")) {
23
+ return script.replace("INPUT=$(cat)", `INPUT=$(cat)\n\n${snippet}`);
24
+ }
25
+ if (script.includes("set -euo pipefail")) {
26
+ return script.replace("set -euo pipefail", `set -euo pipefail\n\n${snippet}`);
27
+ }
28
+ // shebang 패턴: #!/bin/bash, #!/usr/bin/env bash, #!/bin/sh 등
29
+ const shebangMatch = script.match(/^#!.+$/m);
30
+ if (shebangMatch) {
31
+ return script.replace(shebangMatch[0], `${shebangMatch[0]}\n\n${snippet}`);
32
+ }
33
+ return `${snippet}\n${script}`;
34
+ }
3
35
  export async function generateHooks(options) {
4
36
  const { projectDir, config } = options;
5
37
  const hooksDir = join(projectDir, ".claude/hooks");
@@ -20,7 +52,8 @@ export async function generateHooks(options) {
20
52
  const safeId = hook.id.replace(/[^a-zA-Z0-9_-]/g, "");
21
53
  const scriptName = `${safeId}.sh`;
22
54
  const scriptPath = join(hooksDir, scriptName);
23
- await writeFile(scriptPath, hook.inline, "utf8");
55
+ const wrappedScript = wrapWithLogger(hook.inline, hook.event);
56
+ await writeFile(scriptPath, wrappedScript, "utf8");
24
57
  await chmod(scriptPath, 0o755);
25
58
  generatedFiles.push(scriptPath);
26
59
  const entry = {
@@ -40,15 +40,18 @@ Do NOT guess commands — use the detected values above.\n`;
40
40
  export function buildHarnessGenerationPrompt(description, catalogBlocks, projectFacts) {
41
41
  const factsSection = projectFacts ? buildProjectFactsSection(projectFacts) : "";
42
42
  const catalogSection = catalogBlocks && catalogBlocks.length > 0
43
- ? `\nAvailable building blocks (use in the hooks field):
43
+ ? `\nAvailable building blocks (MUST use in the hooks field — prefer hooks over enforcement):
44
44
  ${catalogBlocks
45
45
  .map((b) => {
46
46
  const paramDesc = b.params.length > 0
47
- ? ` Params: ${b.params.map((p) => `${p.name}${p.required ? " (required)" : p.default !== undefined ? ` (default: ${String(p.default)})` : ""}`).join(", ")}`
47
+ ? ` params: ${b.params.map((p) => `${p.name}${p.required ? " (required)" : p.default !== undefined ? ` (default: ${String(p.default)})` : ""}`).join(", ")}`
48
48
  : "";
49
- return `- ${b.id}: ${b.description}.${paramDesc}`;
49
+ return `- block: ${b.id} ${b.description}.${paramDesc}`;
50
50
  })
51
- .join("\n")}\n`
51
+ .join("\n")}
52
+
53
+ IMPORTANT: Match the user's description to the most relevant blocks above. Use hooks for ALL enforcement that has a matching block. Only use enforcement as a fallback for commands with no matching block.
54
+ `
52
55
  : "";
53
56
  return `You are a configuration generator for oh-my-harness, an AI code agent harness tool. Given a project description, generate a complete harness.yaml configuration in YAML format. Return ONLY the YAML content with no markdown formatting.
54
57
 
@@ -57,7 +60,8 @@ The harness.yaml schema has these fields:
57
60
  - project: object with name (optional string), description (optional string), stacks (array of {name, framework, language, packageManager?, testRunner?, linter?})
58
61
  - rules: array of {id, title, content (markdown), priority (number, lower = higher in file)}
59
62
  - enforcement: object with preCommit (array of full executable shell commands like "pnpm test", "npx eslint", "npx tsc --noEmit"), blockedPaths (array of glob patterns), blockedCommands (array of dangerous commands), postSave (array of {pattern, command})
60
- - hooks: array of {block, params} — use catalog building blocks instead of manual enforcement where possible (preferred for new configs)
63
+ - hooks: array of {block, params} — MUST use catalog building blocks here. This is the primary mechanism for enforcement. Match user requirements to available blocks.
64
+ - enforcement: fallback for commands with no matching block. Only use enforcement.preCommit when no catalog block covers the use case.
61
65
  - permissions: object with allow (array of permission strings like "Bash(npm test*)") and deny (array)
62
66
  ${catalogSection}${factsSection}
63
67
  Example 1 - Next.js app:
@@ -87,15 +91,24 @@ rules:
87
91
  - Use vitest + @testing-library/react for component tests
88
92
  - Every component MUST have a corresponding .test.tsx file
89
93
  priority: 21
90
- enforcement:
91
- preCommit: ["pnpm test", "npx eslint .", "npx tsc --noEmit"]
92
- blockedPaths: [".next/", "node_modules/", "*.min.js"]
93
- blockedCommands: ["rm -rf /", "sudo rm"]
94
- postSave:
95
- - pattern: "*.ts"
96
- command: "eslint --fix"
97
- - pattern: "*.tsx"
98
- command: "eslint --fix"
94
+ hooks:
95
+ - block: branch-guard
96
+ - block: commit-test-gate
97
+ params:
98
+ testCommand: "pnpm test"
99
+ - block: commit-typecheck-gate
100
+ params:
101
+ typecheckCommand: "npx tsc --noEmit"
102
+ - block: lint-on-save
103
+ params:
104
+ lintCommand: "npx eslint --fix"
105
+ pattern: "*.{ts,tsx}"
106
+ - block: path-guard
107
+ params:
108
+ blockedPaths: ".next/,node_modules/,*.min.js"
109
+ - block: command-guard
110
+ params:
111
+ blockedCommands: "rm -rf /,sudo rm"
99
112
  permissions:
100
113
  allow:
101
114
  - "Bash(pnpm install*)"
@@ -125,13 +138,21 @@ rules:
125
138
  - Use Pydantic v2 models for all request/response schemas
126
139
  - Use dependency injection for database sessions, auth, and shared services
127
140
  priority: 20
128
- enforcement:
129
- preCommit: ["uv run pytest", "uv run ruff check"]
130
- blockedPaths: ["__pycache__/", ".venv/", "*.pyc"]
131
- blockedCommands: ["rm -rf /", "sudo rm", "pip install"]
132
- postSave:
133
- - pattern: "*.py"
134
- command: "ruff check --fix"
141
+ hooks:
142
+ - block: branch-guard
143
+ - block: commit-test-gate
144
+ params:
145
+ testCommand: "uv run pytest"
146
+ - block: lint-on-save
147
+ params:
148
+ lintCommand: "ruff check --fix"
149
+ pattern: "*.py"
150
+ - block: path-guard
151
+ params:
152
+ blockedPaths: "__pycache__/,.venv/,*.pyc"
153
+ - block: command-guard
154
+ params:
155
+ blockedCommands: "rm -rf /,sudo rm,pip install"
135
156
  permissions:
136
157
  allow:
137
158
  - "Bash(pytest*)"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-harness",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "description": "Tame your AI coding agents with natural language",
5
5
  "type": "module",
6
6
  "bin": {