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.
- package/dist/catalog/blocks/branch-guard.js +2 -0
- package/dist/catalog/blocks/command-guard.js +1 -0
- package/dist/catalog/blocks/commit-test-gate.js +1 -0
- package/dist/catalog/blocks/commit-typecheck-gate.js +1 -0
- package/dist/catalog/blocks/lockfile-guard.js +1 -0
- package/dist/catalog/blocks/path-guard.js +3 -0
- package/dist/catalog/blocks/secret-file-guard.js +1 -0
- package/dist/catalog/blocks/tdd-guard.js +2 -0
- package/dist/catalog/converter.js +4 -3
- package/dist/catalog/template-engine.d.ts +1 -0
- package/dist/catalog/template-engine.js +9 -0
- package/dist/catalog/types.d.ts +2 -2
- package/dist/catalog/types.js +1 -1
- package/dist/core/harness-schema.d.ts +14 -14
- package/dist/core/harness-schema.js +1 -1
- package/dist/detector/detectors/node.js +24 -7
- package/dist/generators/hooks.d.ts +1 -1
- package/dist/generators/hooks.js +14 -9
- package/dist/nl/prompt-templates.js +52 -20
- package/package.json +1 -1
|
@@ -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
|
|
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,
|
|
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) {
|
package/dist/catalog/types.d.ts
CHANGED
|
@@ -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
|
}>;
|
package/dist/catalog/types.js
CHANGED
|
@@ -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>;
|
|
@@ -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
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
testCommands.push(
|
|
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;
|
package/dist/generators/hooks.js
CHANGED
|
@@ -1,31 +1,36 @@
|
|
|
1
1
|
import { mkdir, writeFile, chmod } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
|
|
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="
|
|
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
|
-
|
|
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${
|
|
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${
|
|
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${
|
|
31
|
+
return script.replace(shebangMatch[0], `${shebangMatch[0]}\n\n${snippet}`);
|
|
27
32
|
}
|
|
28
|
-
return `${
|
|
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
|
-
? `
|
|
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}
|
|
49
|
+
return `- block: ${b.id} — ${b.description}.${paramDesc}`;
|
|
50
50
|
})
|
|
51
|
-
.join("\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
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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*)"
|