oh-my-harness 0.4.0 → 0.4.2

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
@@ -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
@@ -60,6 +60,7 @@ BASENAME=$(basename "\$FILE_PATH" | sed -E 's/\\.(ts|tsx|js|jsx)$//')
60
60
  TEST_SUFFIX=".test."
61
61
 
62
62
  if [[ ! -f "\$HISTORY_FILE" ]]; then
63
+ _log_event "block" "oh-my-harness: TDD — \${BASENAME}\${TEST_SUFFIX}* 테스트 파일을 먼저 수정하세요"
63
64
  echo "{\\"decision\\": \\"block\\", \\"reason\\": \\"oh-my-harness: TDD — \${BASENAME}\${TEST_SUFFIX}* 테스트 파일을 먼저 수정하세요\\"}"
64
65
  exit 0
65
66
  fi
@@ -74,6 +75,7 @@ if jq -e --arg b "\$BASENAME" '.edits[] | select(contains($b + ".test.") or cont
74
75
  exit 0
75
76
  fi
76
77
 
78
+ _log_event "block" "oh-my-harness: TDD — \${BASENAME}\${TEST_SUFFIX}* 테스트 파일을 먼저 수정하세요"
77
79
  echo "{\\"decision\\": \\"block\\", \\"reason\\": \\"oh-my-harness: TDD — \${BASENAME}\${TEST_SUFFIX}* 테스트 파일을 먼저 수정하세요\\"}"
78
80
  exit 0`,
79
81
  tags: ["tdd", "workflow", "quality"],
@@ -1,5 +1,5 @@
1
1
  import path from "path";
2
- import { renderTemplate, validateParams } from "./template-engine.js";
2
+ import { renderTemplate, validateParams, applyDefaults } from "./template-engine.js";
3
3
  export async function convertHookEntries(entries, registry, projectDir) {
4
4
  const hooksConfig = {};
5
5
  const scripts = new Map();
@@ -11,7 +11,8 @@ export async function convertHookEntries(entries, registry, projectDir) {
11
11
  errors.push(`Unknown block id: "${entry.block}"`);
12
12
  continue;
13
13
  }
14
- const paramErrors = validateParams(block, entry.params);
14
+ const resolvedParams = applyDefaults(block, entry.params);
15
+ const paramErrors = validateParams(block, resolvedParams);
15
16
  if (paramErrors.length > 0) {
16
17
  errors.push(...paramErrors);
17
18
  continue;
@@ -23,7 +24,7 @@ export async function convertHookEntries(entries, registry, projectDir) {
23
24
  seenBlockIds.add(entry.block);
24
25
  let scriptContent;
25
26
  try {
26
- scriptContent = renderTemplate(block.template, entry.params);
27
+ scriptContent = renderTemplate(block.template, resolvedParams);
27
28
  }
28
29
  catch (err) {
29
30
  errors.push(`Failed to render block "${entry.block}": ${err.message}`);
@@ -1,3 +1,4 @@
1
1
  import type { BuildingBlock } from "./types.js";
2
2
  export declare function renderTemplate(template: string, params: Record<string, unknown>): string;
3
+ export declare function applyDefaults(block: BuildingBlock, params: Record<string, unknown>): Record<string, unknown>;
3
4
  export declare function validateParams(block: BuildingBlock, params: Record<string, unknown>): string[];
@@ -3,6 +3,15 @@ export function renderTemplate(template, params) {
3
3
  const compiled = Handlebars.compile(template);
4
4
  return compiled(params);
5
5
  }
6
+ export function applyDefaults(block, params) {
7
+ const result = { ...params };
8
+ for (const param of block.params) {
9
+ if (result[param.name] === undefined && param.default !== undefined) {
10
+ result[param.name] = param.default;
11
+ }
12
+ }
13
+ return result;
14
+ }
6
15
  export function validateParams(block, params) {
7
16
  const errors = [];
8
17
  for (const param of block.params) {
@@ -109,11 +109,11 @@ export declare const BuildingBlockSchema: z.ZodObject<{
109
109
  }>;
110
110
  export declare const HookEntrySchema: z.ZodObject<{
111
111
  block: z.ZodString;
112
- params: z.ZodRecord<z.ZodString, z.ZodUnknown>;
112
+ params: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
113
113
  }, "strip", z.ZodTypeAny, {
114
114
  params: Record<string, unknown>;
115
115
  block: string;
116
116
  }, {
117
- params: Record<string, unknown>;
118
117
  block: string;
118
+ params?: Record<string, unknown> | undefined;
119
119
  }>;
@@ -20,5 +20,5 @@ export const BuildingBlockSchema = z.object({
20
20
  });
21
21
  export const HookEntrySchema = z.object({
22
22
  block: z.string(),
23
- params: z.record(z.unknown()),
23
+ params: z.record(z.unknown()).default({}),
24
24
  });
@@ -65,7 +65,7 @@ export declare const HarnessConfigSchema: z.ZodObject<{
65
65
  content: string;
66
66
  priority?: number | undefined;
67
67
  }>, "many">;
68
- enforcement: z.ZodObject<{
68
+ enforcement: z.ZodDefault<z.ZodObject<{
69
69
  preCommit: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
70
70
  blockedPaths: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
71
71
  blockedCommands: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
@@ -95,16 +95,16 @@ export declare const HarnessConfigSchema: z.ZodObject<{
95
95
  command: string;
96
96
  pattern: string;
97
97
  }[] | undefined;
98
- }>;
98
+ }>>;
99
99
  hooks: z.ZodDefault<z.ZodArray<z.ZodObject<{
100
100
  block: z.ZodString;
101
- params: z.ZodRecord<z.ZodString, z.ZodUnknown>;
101
+ params: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
102
102
  }, "strip", z.ZodTypeAny, {
103
103
  params: Record<string, unknown>;
104
104
  block: string;
105
105
  }, {
106
- params: Record<string, unknown>;
107
106
  block: string;
107
+ params?: Record<string, unknown> | undefined;
108
108
  }>, "many">>;
109
109
  permissions: z.ZodDefault<z.ZodObject<{
110
110
  allow: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
@@ -172,23 +172,23 @@ export declare const HarnessConfigSchema: z.ZodObject<{
172
172
  content: string;
173
173
  priority?: number | undefined;
174
174
  }[];
175
- enforcement: {
176
- blockedPaths?: string[] | undefined;
177
- preCommit?: string[] | undefined;
178
- blockedCommands?: string[] | undefined;
179
- postSave?: {
180
- command: string;
181
- pattern: string;
182
- }[] | undefined;
183
- };
184
175
  permissions?: {
185
176
  allow?: string[] | undefined;
186
177
  deny?: string[] | undefined;
187
178
  } | undefined;
188
179
  version?: "1.0" | undefined;
189
180
  hooks?: {
190
- params: Record<string, unknown>;
191
181
  block: string;
182
+ params?: Record<string, unknown> | undefined;
192
183
  }[] | undefined;
184
+ enforcement?: {
185
+ blockedPaths?: string[] | undefined;
186
+ preCommit?: string[] | undefined;
187
+ blockedCommands?: string[] | undefined;
188
+ postSave?: {
189
+ command: string;
190
+ pattern: string;
191
+ }[] | undefined;
192
+ } | undefined;
193
193
  }>;
194
194
  export type HarnessConfig = z.infer<typeof HarnessConfigSchema>;
@@ -31,7 +31,7 @@ export const HarnessConfigSchema = z.object({
31
31
  pattern: z.string(),
32
32
  command: z.string(),
33
33
  })).default([]),
34
- }),
34
+ }).default({}),
35
35
  // Catalog-based hooks (v2)
36
36
  hooks: z.array(HookEntrySchema).default([]),
37
37
  // Permissions
@@ -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,5 +1,5 @@
1
1
  import type { MergedConfig } from "../core/preset-types.js";
2
- export declare function wrapWithLogger(script: string): string;
2
+ export declare function wrapWithLogger(script: string, event?: string): string;
3
3
  export interface GenerateHooksOptions {
4
4
  projectDir: string;
5
5
  config: MergedConfig;
@@ -1,31 +1,36 @@
1
1
  import { mkdir, writeFile, chmod } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- const LOGGER_SNIPPET = `# --- oh-my-harness event logger ---
3
+ function buildLoggerSnippet(event) {
4
+ return `# --- oh-my-harness event logger ---
4
5
  _OMH_STATE_DIR=".claude/hooks/.state"
5
6
  mkdir -p "$_OMH_STATE_DIR" 2>/dev/null || true
6
7
  _OMH_HOOK_NAME="$(basename "$0")"
7
- _OMH_EVENT="\${_OMH_EVENT:-unknown}"
8
+ _OMH_EVENT="${event}"
9
+ _OMH_LOGGED=0
8
10
  _log_event() {
11
+ _OMH_LOGGED=1
9
12
  local decision="\${1:-allow}" reason="\${2:-}"
10
13
  printf '{"ts":"%s","event":"%s","hook":"%s","decision":"%s","reason":"%s"}\\n' \\
11
14
  "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$_OMH_EVENT" "$_OMH_HOOK_NAME" "$decision" "$reason" \\
12
15
  >> "$_OMH_STATE_DIR/events.jsonl"
13
16
  }
14
- trap '_log_event "allow"' EXIT
17
+ trap '[ "$_OMH_LOGGED" -eq 0 ] && _log_event "allow"' EXIT
15
18
  # --- end logger ---`;
16
- export function wrapWithLogger(script) {
19
+ }
20
+ export function wrapWithLogger(script, event = "unknown") {
21
+ const snippet = buildLoggerSnippet(event);
17
22
  if (script.includes("INPUT=$(cat)")) {
18
- return script.replace("INPUT=$(cat)", `INPUT=$(cat)\n\n${LOGGER_SNIPPET}`);
23
+ return script.replace("INPUT=$(cat)", `INPUT=$(cat)\n\n${snippet}`);
19
24
  }
20
25
  if (script.includes("set -euo pipefail")) {
21
- return script.replace("set -euo pipefail", `set -euo pipefail\n\n${LOGGER_SNIPPET}`);
26
+ return script.replace("set -euo pipefail", `set -euo pipefail\n\n${snippet}`);
22
27
  }
23
28
  // shebang 패턴: #!/bin/bash, #!/usr/bin/env bash, #!/bin/sh 등
24
29
  const shebangMatch = script.match(/^#!.+$/m);
25
30
  if (shebangMatch) {
26
- return script.replace(shebangMatch[0], `${shebangMatch[0]}\n\n${LOGGER_SNIPPET}`);
31
+ return script.replace(shebangMatch[0], `${shebangMatch[0]}\n\n${snippet}`);
27
32
  }
28
- return `${LOGGER_SNIPPET}\n${script}`;
33
+ return `${snippet}\n${script}`;
29
34
  }
30
35
  export async function generateHooks(options) {
31
36
  const { projectDir, config } = options;
@@ -47,7 +52,7 @@ export async function generateHooks(options) {
47
52
  const safeId = hook.id.replace(/[^a-zA-Z0-9_-]/g, "");
48
53
  const scriptName = `${safeId}.sh`;
49
54
  const scriptPath = join(hooksDir, scriptName);
50
- const wrappedScript = wrapWithLogger(hook.inline);
55
+ const wrappedScript = wrapWithLogger(hook.inline, hook.event);
51
56
  await writeFile(scriptPath, wrappedScript, "utf8");
52
57
  await chmod(scriptPath, 0o755);
53
58
  generatedFiles.push(scriptPath);
@@ -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,29 @@ 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
+ filePattern: "*.ts"
105
+ command: "npx eslint --fix"
106
+ - block: path-guard
107
+ params:
108
+ blockedPaths:
109
+ - ".next/"
110
+ - "node_modules/"
111
+ - "*.min.js"
112
+ - block: command-guard
113
+ params:
114
+ patterns:
115
+ - "rm -rf /"
116
+ - "sudo rm"
99
117
  permissions:
100
118
  allow:
101
119
  - "Bash(pnpm install*)"
@@ -125,13 +143,27 @@ rules:
125
143
  - Use Pydantic v2 models for all request/response schemas
126
144
  - Use dependency injection for database sessions, auth, and shared services
127
145
  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"
146
+ hooks:
147
+ - block: branch-guard
148
+ - block: commit-test-gate
149
+ params:
150
+ testCommand: "uv run pytest"
151
+ - block: lint-on-save
152
+ params:
153
+ filePattern: "*.py"
134
154
  command: "ruff check --fix"
155
+ - block: path-guard
156
+ params:
157
+ blockedPaths:
158
+ - "__pycache__/"
159
+ - ".venv/"
160
+ - "*.pyc"
161
+ - block: command-guard
162
+ params:
163
+ patterns:
164
+ - "rm -rf /"
165
+ - "sudo rm"
166
+ - "pip install"
135
167
  permissions:
136
168
  allow:
137
169
  - "Bash(pytest*)"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-harness",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Tame your AI coding agents with natural language",
5
5
  "type": "module",
6
6
  "bin": {