Skip to content

Commit dfd77a4

Browse files
luiseimanclaude
andcommitted
chore: v3 behaviors pilot rollout + audit 2026-04-14
Compile and wire 4 core v3 behaviors (no-destructive-git, search-first, verify-before-done, respect-todo-state) into dotforge itself. Update registry with full 12-project audit results under v3.0.4. Pilot also applied to cotiza-api-cloud, TRADINGBOT, jira-nbch (separate repos). jira-nbch had wired hooks but was missing scripts/runtime/lib.sh — restored. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fe5a63d commit dfd77a4

14 files changed

Lines changed: 952 additions & 15 deletions
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"hooks": {
3+
"PreToolUse": [
4+
{
5+
"matcher": "Bash",
6+
"hooks": [
7+
{
8+
"type": "command",
9+
"command": ".claude/hooks/generated/no-destructive-git__pretooluse__bash__0.sh"
10+
}
11+
]
12+
}
13+
]
14+
}
15+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
#!/usr/bin/env bash
2+
# GENERATED by scripts/compiler/compile.sh — DO NOT EDIT BY HAND.
3+
# Source: behaviors/no-destructive-git/behavior.yaml
4+
# Behavior: no-destructive-git (core)
5+
# Event: PreToolUse
6+
# Matcher: Bash
7+
# Action: evaluate
8+
# Trigger: index 0
9+
set -u
10+
11+
if [ -n "${FORGE_LIB_PATH:-}" ]; then
12+
# shellcheck source=/dev/null
13+
. "${FORGE_LIB_PATH}"
14+
else
15+
HOOK_SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
16+
REPO_ROOT=$(cd "${HOOK_SCRIPT_DIR}/../../.." && pwd)
17+
# shellcheck source=/dev/null
18+
. "${REPO_ROOT}/scripts/runtime/lib.sh"
19+
fi
20+
21+
BEHAVIOR_ID='no-destructive-git'
22+
BEHAVIOR_NAME='No Destructive Git Operations'
23+
DEFAULT_LEVEL='hard_block'
24+
ESCALATION_JSON='[]'
25+
BLOCK_REASON='Destructive git operation blocked: force push, hard reset, clean -f, and forced branch delete are not allowed.'
26+
RECOVERY_HINT='Destructive git operations are blocked. Use safe alternatives: git revert, git stash, git reset --soft, or create a new branch.'
27+
NUDGE_TEMPLATE=''
28+
WARNING_TEMPLATE=''
29+
EVENT_NAME='PreToolUse'
30+
31+
PAYLOAD=$(cat)
32+
SESSION_ID=$(printf '%s' "$PAYLOAD" | forge_session_id)
33+
TOOL_NAME=$(printf '%s' "$PAYLOAD" | jq -r '.tool_name // empty')
34+
# For PreToolUse/PostToolUse, conditions see tool_input.
35+
# For UserPromptSubmit/Stop, merge top-level payload fields (e.g., .prompt)
36+
# into the condition context so DSL fields like `prompt` resolve.
37+
TOOL_INPUT_JSON=$(printf '%s' "$PAYLOAD" | jq -c '(.tool_input // {}) + (del(.session_id, .tool_name, .tool_input, .transcript_path, .cwd, .hook_event_name))')
38+
TOOL_INPUT_HASH=$(printf '%s' "$TOOL_INPUT_JSON" | forge_tool_input_hash)
39+
TOOL_INPUT_SUMMARY=$(printf '%s' "$TOOL_INPUT_JSON" | cut -c1-100 | tr -d '\n')
40+
41+
# Session-scope disable (set via /forge behavior off --session)
42+
if forge_behavior_session_is_disabled "$SESSION_ID" "$BEHAVIOR_ID"; then
43+
exit 0
44+
fi
45+
46+
# Conditions (logic: all)
47+
CONDITIONS_JSON='[{"field":"command","operator":"regex_match","value":"git\\s+(push\\s+[^|;&]*(--force\\b|--force-with-lease\\b|-f\\b)|reset\\s+--hard\\b|clean\\s+-[a-z]*f[a-z]*\\b|branch\\s+-[a-zA-Z]*D[a-zA-Z]*\\b)"}]'
48+
if ! python3 - "$CONDITIONS_JSON" 'all' "$TOOL_INPUT_JSON" <<'PYCOND'
49+
import sys, json, re
50+
conditions = json.loads(sys.argv[1])
51+
logic = sys.argv[2]
52+
tool_input = json.loads(sys.argv[3] or '{}')
53+
54+
def get_field(f):
55+
return tool_input.get(f, '') or ''
56+
57+
def check(c):
58+
field = c.get('field', '')
59+
op = c.get('operator', '')
60+
val = c.get('value', '')
61+
v = get_field(field)
62+
sv = str(v) if v is not None else ''
63+
if op == 'regex_match':
64+
try:
65+
return bool(re.search(val, sv))
66+
except re.error:
67+
return False
68+
if op == 'contains': return val in sv
69+
if op == 'not_contains': return val not in sv
70+
if op == 'equals': return sv == str(val)
71+
if op == 'starts_with': return sv.startswith(val)
72+
if op == 'ends_with': return sv.endswith(val)
73+
if op == 'exists': return bool(sv)
74+
if op == 'not_exists': return not sv
75+
try:
76+
nv = float(sv) if sv != '' else 0.0
77+
nval = float(val)
78+
except (TypeError, ValueError):
79+
return False
80+
if op == 'gt': return nv > nval
81+
if op == 'lt': return nv < nval
82+
if op == 'gte': return nv >= nval
83+
if op == 'lte': return nv <= nval
84+
return False
85+
86+
results = [check(c) for c in conditions]
87+
if logic == 'any':
88+
ok = any(results) if results else True
89+
else:
90+
ok = all(results) if results else True
91+
sys.exit(0 if ok else 1)
92+
PYCOND
93+
then
94+
exit 0
95+
fi
96+
97+
render_template() {
98+
local tpl="$1" counter="$2" level="$3"
99+
printf '%s' "$tpl" \
100+
| sed "s|{behavior_id}|${BEHAVIOR_ID}|g" \
101+
| sed "s|{behavior_name}|${BEHAVIOR_NAME}|g" \
102+
| sed "s|{counter}|${counter}|g" \
103+
| sed "s|{tool_name}|${TOOL_NAME}|g" \
104+
| sed "s|{level}|${level}|g" \
105+
| sed "s|{threshold}|${counter}|g"
106+
}
107+
108+
emit_output() {
109+
local level="$1" counter="$2"
110+
case "$level" in
111+
silent)
112+
exit 0
113+
;;
114+
nudge)
115+
local msg
116+
msg=$(render_template "$NUDGE_TEMPLATE" "$counter" "$level")
117+
jq -cn --arg m "$msg" '{systemMessage: $m}'
118+
exit 0
119+
;;
120+
warning)
121+
local msg
122+
msg=$(render_template "$WARNING_TEMPLATE" "$counter" "$level")
123+
jq -cn --arg m "$msg" '{systemMessage: $m}'
124+
exit 0
125+
;;
126+
soft_block)
127+
# Write pending_block BEFORE emitting so the next invocation can
128+
# detect reinvocation after user override.
129+
forge_pending_block_set "$SESSION_ID" "$BEHAVIOR_ID" "$TOOL_INPUT_HASH" || true
130+
local msg
131+
msg=$(render_template "$BLOCK_REASON" "$counter" "$level")
132+
jq -cn --arg m "$msg" --arg evt "$EVENT_NAME" \
133+
'{hookSpecificOutput: {hookEventName: $evt, permissionDecision: "deny"}, systemMessage: $m}'
134+
exit 0
135+
;;
136+
hard_block)
137+
local msg
138+
msg=$(render_template "$BLOCK_REASON" "$counter" "$level")
139+
jq -cn --arg m "$msg" --arg evt "$EVENT_NAME" \
140+
'{hookSpecificOutput: {hookEventName: $evt, permissionDecision: "deny", override_allowed: false}, systemMessage: $m}'
141+
exit 0
142+
;;
143+
esac
144+
exit 0
145+
}
146+
147+
run_evaluate() {
148+
# Step 0: try override detection via reinvocation. If the pending_block
149+
# matches this incoming tool_input hash within the window, record the
150+
# override and pass through silently — do NOT increment counter.
151+
if forge_pending_block_try_override "$SESSION_ID" "$BEHAVIOR_ID" \
152+
"$TOOL_NAME" "$TOOL_INPUT_HASH" "$TOOL_INPUT_SUMMARY"; then
153+
exit 0
154+
fi
155+
156+
local counter calculated previous effective
157+
counter=$(forge_counter_increment "$SESSION_ID" "$BEHAVIOR_ID" "$TOOL_NAME")
158+
[ -n "$counter" ] || { _forge_log "counter increment failed"; exit 0; }
159+
calculated=$(forge_resolve_level "$counter" "$DEFAULT_LEVEL" "$ESCALATION_JSON")
160+
previous=$(jq -r --arg sid "$SESSION_ID" --arg bid "$BEHAVIOR_ID" \
161+
'.sessions[$sid].behaviors[$bid].effective_level // "silent"' "$FORGE_STATE_FILE")
162+
effective=$(forge_level_max "$previous" "$calculated")
163+
forge_effective_level_set "$SESSION_ID" "$BEHAVIOR_ID" "$effective"
164+
emit_output "$effective" "$counter"
165+
}
166+
167+
# Action: evaluate
168+
run_evaluate
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"hooks": {
3+
"PreToolUse": [
4+
{
5+
"matcher": "TaskUpdate",
6+
"hooks": [
7+
{
8+
"type": "command",
9+
"command": ".claude/hooks/generated/respect-todo-state__pretooluse__taskupdate__0.sh"
10+
}
11+
]
12+
},
13+
{
14+
"matcher": "TaskCreate",
15+
"hooks": [
16+
{
17+
"type": "command",
18+
"command": ".claude/hooks/generated/respect-todo-state__pretooluse__taskcreate__1.sh"
19+
}
20+
]
21+
}
22+
]
23+
}
24+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
#!/usr/bin/env bash
2+
# GENERATED by scripts/compiler/compile.sh — DO NOT EDIT BY HAND.
3+
# Source: behaviors/respect-todo-state/behavior.yaml
4+
# Behavior: respect-todo-state (core)
5+
# Event: PreToolUse
6+
# Matcher: TaskCreate
7+
# Action: check_flag
8+
# Trigger: index 1
9+
set -u
10+
11+
if [ -n "${FORGE_LIB_PATH:-}" ]; then
12+
# shellcheck source=/dev/null
13+
. "${FORGE_LIB_PATH}"
14+
else
15+
HOOK_SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
16+
REPO_ROOT=$(cd "${HOOK_SCRIPT_DIR}/../../.." && pwd)
17+
# shellcheck source=/dev/null
18+
. "${REPO_ROOT}/scripts/runtime/lib.sh"
19+
fi
20+
21+
BEHAVIOR_ID='respect-todo-state'
22+
BEHAVIOR_NAME='Respect Todo State'
23+
DEFAULT_LEVEL='silent'
24+
ESCALATION_JSON='[{"after":1,"level":"nudge"},{"after":3,"level":"warning"},{"after":5,"level":"soft_block"}]'
25+
BLOCK_REASON='[{behavior_id}] BLOCKED: update existing tasks before creating new ones.'
26+
RECOVERY_HINT='Before creating new todos, mark existing ones with TaskUpdate (completed, in_progress, or deleted). Keeps the task list honest.'
27+
NUDGE_TEMPLATE='{behavior_name}: TaskCreate without recent TaskUpdate ({counter})'
28+
WARNING_TEMPLATE='[{behavior_id}] create/update imbalance: counter={counter}. Run TaskUpdate on existing tasks before adding more.'
29+
EVENT_NAME='PreToolUse'
30+
31+
PAYLOAD=$(cat)
32+
SESSION_ID=$(printf '%s' "$PAYLOAD" | forge_session_id)
33+
TOOL_NAME=$(printf '%s' "$PAYLOAD" | jq -r '.tool_name // empty')
34+
# For PreToolUse/PostToolUse, conditions see tool_input.
35+
# For UserPromptSubmit/Stop, merge top-level payload fields (e.g., .prompt)
36+
# into the condition context so DSL fields like `prompt` resolve.
37+
TOOL_INPUT_JSON=$(printf '%s' "$PAYLOAD" | jq -c '(.tool_input // {}) + (del(.session_id, .tool_name, .tool_input, .transcript_path, .cwd, .hook_event_name))')
38+
TOOL_INPUT_HASH=$(printf '%s' "$TOOL_INPUT_JSON" | forge_tool_input_hash)
39+
TOOL_INPUT_SUMMARY=$(printf '%s' "$TOOL_INPUT_JSON" | cut -c1-100 | tr -d '\n')
40+
41+
# Session-scope disable (set via /forge behavior off --session)
42+
if forge_behavior_session_is_disabled "$SESSION_ID" "$BEHAVIOR_ID"; then
43+
exit 0
44+
fi
45+
46+
render_template() {
47+
local tpl="$1" counter="$2" level="$3"
48+
printf '%s' "$tpl" \
49+
| sed "s|{behavior_id}|${BEHAVIOR_ID}|g" \
50+
| sed "s|{behavior_name}|${BEHAVIOR_NAME}|g" \
51+
| sed "s|{counter}|${counter}|g" \
52+
| sed "s|{tool_name}|${TOOL_NAME}|g" \
53+
| sed "s|{level}|${level}|g" \
54+
| sed "s|{threshold}|${counter}|g"
55+
}
56+
57+
emit_output() {
58+
local level="$1" counter="$2"
59+
case "$level" in
60+
silent)
61+
exit 0
62+
;;
63+
nudge)
64+
local msg
65+
msg=$(render_template "$NUDGE_TEMPLATE" "$counter" "$level")
66+
jq -cn --arg m "$msg" '{systemMessage: $m}'
67+
exit 0
68+
;;
69+
warning)
70+
local msg
71+
msg=$(render_template "$WARNING_TEMPLATE" "$counter" "$level")
72+
jq -cn --arg m "$msg" '{systemMessage: $m}'
73+
exit 0
74+
;;
75+
soft_block)
76+
# Write pending_block BEFORE emitting so the next invocation can
77+
# detect reinvocation after user override.
78+
forge_pending_block_set "$SESSION_ID" "$BEHAVIOR_ID" "$TOOL_INPUT_HASH" || true
79+
local msg
80+
msg=$(render_template "$BLOCK_REASON" "$counter" "$level")
81+
jq -cn --arg m "$msg" --arg evt "$EVENT_NAME" \
82+
'{hookSpecificOutput: {hookEventName: $evt, permissionDecision: "deny"}, systemMessage: $m}'
83+
exit 0
84+
;;
85+
hard_block)
86+
local msg
87+
msg=$(render_template "$BLOCK_REASON" "$counter" "$level")
88+
jq -cn --arg m "$msg" --arg evt "$EVENT_NAME" \
89+
'{hookSpecificOutput: {hookEventName: $evt, permissionDecision: "deny", override_allowed: false}, systemMessage: $m}'
90+
exit 0
91+
;;
92+
esac
93+
exit 0
94+
}
95+
96+
run_evaluate() {
97+
# Step 0: try override detection via reinvocation. If the pending_block
98+
# matches this incoming tool_input hash within the window, record the
99+
# override and pass through silently — do NOT increment counter.
100+
if forge_pending_block_try_override "$SESSION_ID" "$BEHAVIOR_ID" \
101+
"$TOOL_NAME" "$TOOL_INPUT_HASH" "$TOOL_INPUT_SUMMARY"; then
102+
exit 0
103+
fi
104+
105+
local counter calculated previous effective
106+
counter=$(forge_counter_increment "$SESSION_ID" "$BEHAVIOR_ID" "$TOOL_NAME")
107+
[ -n "$counter" ] || { _forge_log "counter increment failed"; exit 0; }
108+
calculated=$(forge_resolve_level "$counter" "$DEFAULT_LEVEL" "$ESCALATION_JSON")
109+
previous=$(jq -r --arg sid "$SESSION_ID" --arg bid "$BEHAVIOR_ID" \
110+
'.sessions[$sid].behaviors[$bid].effective_level // "silent"' "$FORGE_STATE_FILE")
111+
effective=$(forge_level_max "$previous" "$calculated")
112+
forge_effective_level_set "$SESSION_ID" "$BEHAVIOR_ID" "$effective"
113+
emit_output "$effective" "$counter"
114+
}
115+
116+
# Action: check_flag
117+
if forge_flag_consume "$SESSION_ID" 'todo_recently_updated'; then
118+
# Flag was present.
119+
exit 0
120+
else
121+
# Flag absent.
122+
run_evaluate
123+
fi
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#!/usr/bin/env bash
2+
# GENERATED by scripts/compiler/compile.sh — DO NOT EDIT BY HAND.
3+
# Source: behaviors/respect-todo-state/behavior.yaml
4+
# Behavior: respect-todo-state (core)
5+
# Event: PreToolUse
6+
# Matcher: TaskUpdate
7+
# Action: set_flag
8+
# Trigger: index 0
9+
set -u
10+
11+
if [ -n "${FORGE_LIB_PATH:-}" ]; then
12+
# shellcheck source=/dev/null
13+
. "${FORGE_LIB_PATH}"
14+
else
15+
HOOK_SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
16+
REPO_ROOT=$(cd "${HOOK_SCRIPT_DIR}/../../.." && pwd)
17+
# shellcheck source=/dev/null
18+
. "${REPO_ROOT}/scripts/runtime/lib.sh"
19+
fi
20+
21+
BEHAVIOR_ID='respect-todo-state'
22+
BEHAVIOR_NAME='Respect Todo State'
23+
DEFAULT_LEVEL='silent'
24+
ESCALATION_JSON='[{"after":1,"level":"nudge"},{"after":3,"level":"warning"},{"after":5,"level":"soft_block"}]'
25+
BLOCK_REASON='[{behavior_id}] BLOCKED: update existing tasks before creating new ones.'
26+
RECOVERY_HINT='Before creating new todos, mark existing ones with TaskUpdate (completed, in_progress, or deleted). Keeps the task list honest.'
27+
NUDGE_TEMPLATE='{behavior_name}: TaskCreate without recent TaskUpdate ({counter})'
28+
WARNING_TEMPLATE='[{behavior_id}] create/update imbalance: counter={counter}. Run TaskUpdate on existing tasks before adding more.'
29+
EVENT_NAME='PreToolUse'
30+
31+
PAYLOAD=$(cat)
32+
SESSION_ID=$(printf '%s' "$PAYLOAD" | forge_session_id)
33+
TOOL_NAME=$(printf '%s' "$PAYLOAD" | jq -r '.tool_name // empty')
34+
# For PreToolUse/PostToolUse, conditions see tool_input.
35+
# For UserPromptSubmit/Stop, merge top-level payload fields (e.g., .prompt)
36+
# into the condition context so DSL fields like `prompt` resolve.
37+
TOOL_INPUT_JSON=$(printf '%s' "$PAYLOAD" | jq -c '(.tool_input // {}) + (del(.session_id, .tool_name, .tool_input, .transcript_path, .cwd, .hook_event_name))')
38+
TOOL_INPUT_HASH=$(printf '%s' "$TOOL_INPUT_JSON" | forge_tool_input_hash)
39+
TOOL_INPUT_SUMMARY=$(printf '%s' "$TOOL_INPUT_JSON" | cut -c1-100 | tr -d '\n')
40+
41+
# Session-scope disable (set via /forge behavior off --session)
42+
if forge_behavior_session_is_disabled "$SESSION_ID" "$BEHAVIOR_ID"; then
43+
exit 0
44+
fi
45+
46+
# Action: set_flag
47+
forge_flag_set "$SESSION_ID" 'todo_recently_updated' || true
48+
exit 0

0 commit comments

Comments
 (0)