uni-harness 0.1.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/.claude/agents/TEMPLATE.md +64 -0
- package/.claude/hooks/guard-pre-bash.sh +46 -0
- package/.claude/hooks/observe-log.sh +88 -0
- package/.claude/hooks/sensor-post-edit.sh +62 -0
- package/.claude/hooks/session-start.sh +86 -0
- package/.claude/hooks/stop-gate.sh +70 -0
- package/.claude/settings.json +76 -0
- package/.claude/skills/checkpoint/SKILL.md +49 -0
- package/.claude/skills/guide-audit/SKILL.md +50 -0
- package/.claude/skills/harness-init/SKILL.md +70 -0
- package/.claude/skills/ratchet/SKILL.md +72 -0
- package/.harness/commands.env +13 -0
- package/CLAUDE.md +68 -0
- package/LICENSE +21 -0
- package/README.md +212 -0
- package/bin/cli.js +371 -0
- package/harness/harness_report.py +90 -0
- package/harness/tests/test_hooks.sh +129 -0
- package/harness/tests/test_installer.sh +112 -0
- package/package.json +35 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
# ── Custom agent template (this file itself is inactive) ────────
|
|
3
|
+
# Claude Code treats a file without a `name` in its frontmatter as
|
|
4
|
+
# documentation and ignores it. To create an agent: copy this file →
|
|
5
|
+
# uncomment and fill the fields below → replace the body with that
|
|
6
|
+
# agent's system prompt.
|
|
7
|
+
#
|
|
8
|
+
# name: lowercase-hyphen identifier (becomes the call name) [required]
|
|
9
|
+
# description: WHEN to delegate to this agent — the main agent
|
|
10
|
+
# reads this sentence to decide auto-delegation [required]
|
|
11
|
+
# tools: allowed tools (omit to inherit all)
|
|
12
|
+
# disallowedTools: explicit denials (take precedence over tools)
|
|
13
|
+
# model: sonnet | opus | haiku (omit to inherit)
|
|
14
|
+
#
|
|
15
|
+
# name: code-reviewer
|
|
16
|
+
# description: Reviews code changes in a fresh context, without seeing the implementation process. Use after finishing a feature or fix, before commit/PR. Judges plan compliance and defects, and reports.
|
|
17
|
+
# tools: Read, Grep, Glob, Bash
|
|
18
|
+
#
|
|
19
|
+
# Note: creating the first active agent in agents/ requires one session
|
|
20
|
+
# restart. The harness hooks and permissions apply to agent tool calls too.
|
|
21
|
+
# ────────────────────────────────────────────────────────────────
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
The body below is the system prompt of the example (fresh-context code
|
|
25
|
+
reviewer). Replace it entirely when creating a new agent.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
You are a code reviewer. You see only the result, not the implementer's
|
|
30
|
+
reasoning — that is your value. Find what the implementer missed, without
|
|
31
|
+
inheriting their assumptions.
|
|
32
|
+
|
|
33
|
+
## Procedure
|
|
34
|
+
|
|
35
|
+
1. Read the full change via `git diff` (or the scope you were given).
|
|
36
|
+
2. If `plan.md` exists, read it and check the change against the plan and
|
|
37
|
+
requirements.
|
|
38
|
+
3. Read enough surrounding code to judge each change in context.
|
|
39
|
+
4. If `.harness/commands.env` has a TEST_CMD, run it and check the result.
|
|
40
|
+
|
|
41
|
+
## Judging Criteria (in priority order)
|
|
42
|
+
|
|
43
|
+
1. **Compliance**: implemented differently from the plan/requirements, or
|
|
44
|
+
requirements missing
|
|
45
|
+
2. **Defects**: code that produces wrong results or crashes on real input —
|
|
46
|
+
report only what you can pair with a concrete failure scenario
|
|
47
|
+
3. **Verification gaps**: changed behavior that no test covers
|
|
48
|
+
4. **Simplification**: duplication replaceable by existing code (only when
|
|
49
|
+
confident)
|
|
50
|
+
|
|
51
|
+
## Rules
|
|
52
|
+
|
|
53
|
+
- Report only. Never modify code.
|
|
54
|
+
- Anchor every finding to `file:line` with evidence. If it's speculation,
|
|
55
|
+
say so.
|
|
56
|
+
- If there is nothing to report, say so. Never invent findings to fill space.
|
|
57
|
+
- No style preferences — that's the linter's job.
|
|
58
|
+
|
|
59
|
+
## Output Format
|
|
60
|
+
|
|
61
|
+
- **Verdict**: approvable / needs changes (one-line rationale)
|
|
62
|
+
- **Findings**: ordered by severity, each with location, problem, evidence,
|
|
63
|
+
failure scenario
|
|
64
|
+
- **Scope checked**: files read, verification commands run and their results
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ════════════════════════════════════════════════════════════════
|
|
3
|
+
# PreToolUse(Bash) guard — blocks destructive/irreversible commands
|
|
4
|
+
# before they run.
|
|
5
|
+
# exit 0 + JSON(deny) = blocked / exit 0 (no output) = normal flow
|
|
6
|
+
# ════════════════════════════════════════════════════════════════
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
INPUT=$(cat)
|
|
10
|
+
|
|
11
|
+
python3 - "$INPUT" <<'PYEOF'
|
|
12
|
+
import json, re, sys
|
|
13
|
+
|
|
14
|
+
data = json.loads(sys.argv[1])
|
|
15
|
+
cmd = (data.get("tool_input") or {}).get("command", "") or ""
|
|
16
|
+
|
|
17
|
+
# DENY: irreversible/destructive patterns (extend per project as needed)
|
|
18
|
+
DENY_PATTERNS = [
|
|
19
|
+
(r"\brm\s+(-[a-zA-Z]*[rf][a-zA-Z]*\s+)+(/|\$HOME|~)(\s|$)", "recursive delete of root/home directory"),
|
|
20
|
+
(r"\bgit\s+push\s+.*--force(?!-with-lease)", "--force push (use --force-with-lease if needed)"),
|
|
21
|
+
(r"\bgit\s+reset\s+--hard\s+origin/", "hard reset to remote (loses all local changes)"),
|
|
22
|
+
(r"\bgit\s+(commit|push)\b[^|;&]*--no-verify\b", "bypassing verification hooks with --no-verify"),
|
|
23
|
+
(r"\bDROP\s+(TABLE|DATABASE)\b", "destructive database command"),
|
|
24
|
+
(r"\bchmod\s+(-[a-zA-Z]+\s+)*777\b", "opening full permissions (chmod 777)"),
|
|
25
|
+
(r"curl[^|;&]*\|\s*(sudo\s+)?(ba)?sh", "piping a remote script into a shell"),
|
|
26
|
+
(r"wget[^|;&]*\|\s*(sudo\s+)?(ba)?sh", "piping a remote script into a shell"),
|
|
27
|
+
(r">\s*/dev/sd[a-z]", "writing directly to a block device"),
|
|
28
|
+
(r"\bmkfs\b", "formatting a filesystem"),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
for pattern, reason in DENY_PATTERNS:
|
|
32
|
+
if re.search(pattern, cmd, re.IGNORECASE):
|
|
33
|
+
print(json.dumps({
|
|
34
|
+
"hookSpecificOutput": {
|
|
35
|
+
"hookEventName": "PreToolUse",
|
|
36
|
+
"permissionDecision": "deny",
|
|
37
|
+
"permissionDecisionReason":
|
|
38
|
+
f"[harness guard] Blocked: {reason}. "
|
|
39
|
+
"If this action is truly needed, ask the user to run it directly."
|
|
40
|
+
}
|
|
41
|
+
}))
|
|
42
|
+
sys.exit(0)
|
|
43
|
+
|
|
44
|
+
# No match -> exit silently (normal permission flow)
|
|
45
|
+
sys.exit(0)
|
|
46
|
+
PYEOF
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ════════════════════════════════════════════════════════════════
|
|
3
|
+
# Observability log — records every tool call as JSONL + tripwires
|
|
4
|
+
# Log: .harness/logs/tool_calls.jsonl
|
|
5
|
+
# Tripwires: (1) warn when per-session tool calls exceed a ceiling
|
|
6
|
+
# (2) same failure 3x in a row -> tell Claude to stop and pivot
|
|
7
|
+
# ════════════════════════════════════════════════════════════════
|
|
8
|
+
set -uo pipefail
|
|
9
|
+
|
|
10
|
+
INPUT=$(cat)
|
|
11
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
12
|
+
LOG_DIR="$PROJECT_DIR/.harness/logs"
|
|
13
|
+
mkdir -p "$LOG_DIR"
|
|
14
|
+
|
|
15
|
+
python3 - "$INPUT" "$LOG_DIR" <<'PYEOF'
|
|
16
|
+
import json, sys, datetime, collections
|
|
17
|
+
|
|
18
|
+
data = json.loads(sys.argv[1])
|
|
19
|
+
log_dir = sys.argv[2]
|
|
20
|
+
log_path = f"{log_dir}/tool_calls.jsonl"
|
|
21
|
+
|
|
22
|
+
event = data.get("hook_event_name", "")
|
|
23
|
+
entry = {
|
|
24
|
+
"ts": datetime.datetime.now().isoformat(timespec="seconds"),
|
|
25
|
+
"session_id": data.get("session_id", ""),
|
|
26
|
+
"event": event,
|
|
27
|
+
"tool": data.get("tool_name", ""),
|
|
28
|
+
}
|
|
29
|
+
# If the call came from a subagent, record which one (for /ratchet diagnosis)
|
|
30
|
+
for k in ("agent_id", "agent_type"):
|
|
31
|
+
if data.get(k):
|
|
32
|
+
entry[k] = data[k]
|
|
33
|
+
ti = data.get("tool_input") or {}
|
|
34
|
+
# Privacy/size: log only a summary of the input
|
|
35
|
+
if "command" in ti:
|
|
36
|
+
entry["summary"] = str(ti["command"])[:200]
|
|
37
|
+
elif "file_path" in ti:
|
|
38
|
+
entry["summary"] = str(ti["file_path"])[:200]
|
|
39
|
+
|
|
40
|
+
if event == "PostToolUseFailure":
|
|
41
|
+
resp = data.get("tool_response") or data.get("error") or {}
|
|
42
|
+
entry["error"] = str(resp)[:300]
|
|
43
|
+
|
|
44
|
+
with open(log_path, "a") as f:
|
|
45
|
+
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
46
|
+
|
|
47
|
+
# ── tripwire checks ──────────────────────────────────────────
|
|
48
|
+
session = data.get("session_id", "")
|
|
49
|
+
lines = []
|
|
50
|
+
try:
|
|
51
|
+
with open(log_path) as f:
|
|
52
|
+
for line in f:
|
|
53
|
+
try:
|
|
54
|
+
e = json.loads(line)
|
|
55
|
+
if e.get("session_id") == session:
|
|
56
|
+
lines.append(e)
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
except Exception:
|
|
60
|
+
lines = []
|
|
61
|
+
|
|
62
|
+
warnings = []
|
|
63
|
+
|
|
64
|
+
# (1) per-session tool-call ceiling (default 300, warn every 100)
|
|
65
|
+
n = len(lines)
|
|
66
|
+
if n > 0 and n % 100 == 0 and n >= 300:
|
|
67
|
+
warnings.append(f"This session has exceeded {n} tool calls. "
|
|
68
|
+
"Re-examine the plan — this may be a runaway loop.")
|
|
69
|
+
|
|
70
|
+
# (2) same failure 3x in a row
|
|
71
|
+
fails = [e for e in lines if e.get("event") == "PostToolUseFailure"][-3:]
|
|
72
|
+
if len(fails) == 3:
|
|
73
|
+
sigs = {(e.get("tool", ""), (e.get("error") or "")[:80]) for e in fails}
|
|
74
|
+
if len(sigs) == 1:
|
|
75
|
+
warnings.append("[tripwire] The same tool failed with the same error "
|
|
76
|
+
"3 times in a row. Stop repeating this approach; write "
|
|
77
|
+
"an escalation packet per the Work Loop section of "
|
|
78
|
+
"CLAUDE.md and report to the user.")
|
|
79
|
+
|
|
80
|
+
if warnings:
|
|
81
|
+
print(json.dumps({
|
|
82
|
+
"hookSpecificOutput": {
|
|
83
|
+
"hookEventName": event,
|
|
84
|
+
"additionalContext": "[harness tripwire] " + " ".join(warnings)
|
|
85
|
+
}
|
|
86
|
+
}, ensure_ascii=False))
|
|
87
|
+
PYEOF
|
|
88
|
+
exit 0
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ════════════════════════════════════════════════════════════════
|
|
3
|
+
# PostToolUse(Edit|Write) verification sensor
|
|
4
|
+
# When a code file is modified: (1) run lint immediately and feed
|
|
5
|
+
# failures back to Claude, (2) record the file in the per-session
|
|
6
|
+
# pending_test marker. Tests run in batch at turn end (stop-gate.sh).
|
|
7
|
+
# Commands come from .harness/commands.env (LINT_CMD/TEST_CMD).
|
|
8
|
+
# ════════════════════════════════════════════════════════════════
|
|
9
|
+
set -uo pipefail
|
|
10
|
+
|
|
11
|
+
INPUT=$(cat)
|
|
12
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
13
|
+
ENV_FILE="$PROJECT_DIR/.harness/commands.env"
|
|
14
|
+
LOG_DIR="$PROJECT_DIR/.harness/logs"
|
|
15
|
+
|
|
16
|
+
# Commands not configured -> pass silently (harness not set up yet)
|
|
17
|
+
[ -f "$ENV_FILE" ] || exit 0
|
|
18
|
+
# shellcheck disable=SC1090
|
|
19
|
+
source "$ENV_FILE"
|
|
20
|
+
|
|
21
|
+
FILE_PATH=$(python3 -c '
|
|
22
|
+
import json,sys
|
|
23
|
+
d=json.loads(sys.argv[1])
|
|
24
|
+
print((d.get("tool_input") or {}).get("file_path",""))' "$INPUT" 2>/dev/null || echo "")
|
|
25
|
+
SESSION_ID=$(python3 -c '
|
|
26
|
+
import json,sys
|
|
27
|
+
print(json.loads(sys.argv[1]).get("session_id",""))' "$INPUT" 2>/dev/null || echo "")
|
|
28
|
+
|
|
29
|
+
# Not a code file -> pass (don't run verification on docs/config edits)
|
|
30
|
+
case "$FILE_PATH" in
|
|
31
|
+
*.py|*.ts|*.tsx|*.js|*.jsx|*.go|*.rs|*.java|*.rb|*.c|*.cpp|*.h|*.cs) ;;
|
|
32
|
+
*) exit 0 ;;
|
|
33
|
+
esac
|
|
34
|
+
|
|
35
|
+
# Record for the turn-end test gate (stop-gate.sh)
|
|
36
|
+
if [ -n "$SESSION_ID" ]; then
|
|
37
|
+
mkdir -p "$LOG_DIR"
|
|
38
|
+
printf '%s\n' "$FILE_PATH" >> "$LOG_DIR/pending_test.$SESSION_ID"
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Lint is fast, so run it immediately (timeout may be absent on macOS)
|
|
42
|
+
TIMEOUT_BIN=$(command -v timeout || command -v gtimeout || true)
|
|
43
|
+
if [ -n "${LINT_CMD:-}" ]; then
|
|
44
|
+
LINT_OUT=$(cd "$PROJECT_DIR" && ${TIMEOUT_BIN:+"$TIMEOUT_BIN" 120} bash -c "$LINT_CMD" 2>&1 | tail -40)
|
|
45
|
+
LINT_RC=$?
|
|
46
|
+
if [ "$LINT_RC" -ne 0 ]; then
|
|
47
|
+
python3 - "[LINT FAILED rc=$LINT_RC]
|
|
48
|
+
$LINT_OUT" <<'PYEOF'
|
|
49
|
+
import json, sys
|
|
50
|
+
ctx = ("[harness sensor] The code you just modified failed lint. "
|
|
51
|
+
"Read the output below and fix it. This work is not done until it passes.\n"
|
|
52
|
+
+ sys.argv[1])
|
|
53
|
+
print(json.dumps({
|
|
54
|
+
"hookSpecificOutput": {
|
|
55
|
+
"hookEventName": "PostToolUse",
|
|
56
|
+
"additionalContext": ctx[:8000]
|
|
57
|
+
}
|
|
58
|
+
}))
|
|
59
|
+
PYEOF
|
|
60
|
+
fi
|
|
61
|
+
fi
|
|
62
|
+
exit 0
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ════════════════════════════════════════════════════════════════
|
|
3
|
+
# SessionStart context injection (startup|resume|clear|compact)
|
|
4
|
+
# (1) If an in-progress checkpoint (progress.json) exists, surface
|
|
5
|
+
# it so completed steps aren't repeated. Re-injected after
|
|
6
|
+
# compaction too.
|
|
7
|
+
# (2) If tool failures have piled up in the last 7 days of logs,
|
|
8
|
+
# suggest running /ratchet.
|
|
9
|
+
# (3) If the harness is installed but unconfigured (commands.env has
|
|
10
|
+
# no commands), have Claude offer to run /harness-init.
|
|
11
|
+
# ════════════════════════════════════════════════════════════════
|
|
12
|
+
set -uo pipefail
|
|
13
|
+
|
|
14
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
15
|
+
|
|
16
|
+
python3 - "$PROJECT_DIR" <<'PYEOF'
|
|
17
|
+
import json, sys, os, datetime
|
|
18
|
+
|
|
19
|
+
proj = sys.argv[1]
|
|
20
|
+
parts = []
|
|
21
|
+
|
|
22
|
+
# (3) harness installed but not configured -> offer /harness-init
|
|
23
|
+
# (checked first so a fresh install greets the user with setup)
|
|
24
|
+
try:
|
|
25
|
+
import re
|
|
26
|
+
with open(os.path.join(proj, ".harness/commands.env")) as f:
|
|
27
|
+
env = f.read()
|
|
28
|
+
if not re.search(r'^(LINT_CMD|TEST_CMD)=".+"', env, re.M):
|
|
29
|
+
parts.append(
|
|
30
|
+
"[harness] The harness is installed but not configured: "
|
|
31
|
+
".harness/commands.env has no LINT_CMD/TEST_CMD, so the "
|
|
32
|
+
"verification sensors are inactive. Offer the user to run "
|
|
33
|
+
"/harness-init — it scans the repo, verifies the build/test/lint "
|
|
34
|
+
"commands by running them, and proposes the config as a diff for "
|
|
35
|
+
"approval. If the sensors are off intentionally, they can ignore this.")
|
|
36
|
+
except Exception:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
# (1) in-progress checkpoint
|
|
40
|
+
try:
|
|
41
|
+
with open(os.path.join(proj, "progress.json")) as f:
|
|
42
|
+
ckpt = json.load(f)
|
|
43
|
+
if ckpt.get("status") not in ("completed", "abandoned"):
|
|
44
|
+
parts.append(
|
|
45
|
+
"[harness memory] An in-progress checkpoint exists. Before starting "
|
|
46
|
+
"work, read progress.json / plan.md / decisions.jsonl, do not repeat "
|
|
47
|
+
"completed steps, and resume from the next step.\n"
|
|
48
|
+
f"Checkpoint summary: {json.dumps(ckpt, ensure_ascii=False)[:1500]}")
|
|
49
|
+
except Exception:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
# (2) accumulated failures in the last 7 days -> nudge toward /ratchet
|
|
53
|
+
try:
|
|
54
|
+
cutoff = datetime.datetime.now() - datetime.timedelta(days=7)
|
|
55
|
+
n = 0
|
|
56
|
+
with open(os.path.join(proj, ".harness/logs/tool_calls.jsonl")) as f:
|
|
57
|
+
for line in f:
|
|
58
|
+
try:
|
|
59
|
+
e = json.loads(line)
|
|
60
|
+
except Exception:
|
|
61
|
+
continue
|
|
62
|
+
if e.get("event") != "PostToolUseFailure":
|
|
63
|
+
continue
|
|
64
|
+
try:
|
|
65
|
+
ts = datetime.datetime.fromisoformat(e.get("ts", ""))
|
|
66
|
+
except Exception:
|
|
67
|
+
continue
|
|
68
|
+
if ts >= cutoff:
|
|
69
|
+
n += 1
|
|
70
|
+
if n >= 3:
|
|
71
|
+
parts.append(
|
|
72
|
+
f"[harness] The logs record {n} tool failures in the last 7 days. "
|
|
73
|
+
"If you see a repeating pattern, consider running /ratchet to turn "
|
|
74
|
+
"it into a rule/sensor. (Ignore if already handled.)")
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
if parts:
|
|
79
|
+
print(json.dumps({
|
|
80
|
+
"hookSpecificOutput": {
|
|
81
|
+
"hookEventName": "SessionStart",
|
|
82
|
+
"additionalContext": "\n".join(parts)
|
|
83
|
+
}
|
|
84
|
+
}, ensure_ascii=False))
|
|
85
|
+
PYEOF
|
|
86
|
+
exit 0
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ════════════════════════════════════════════════════════════════
|
|
3
|
+
# Stop test gate
|
|
4
|
+
# If code was modified this session (pending_test marker), run
|
|
5
|
+
# TEST_CMD in batch before the turn ends. On failure, block the
|
|
6
|
+
# stop and feed the output back. Blocking is capped at 3 per
|
|
7
|
+
# session — after that, instruct an escalation packet and let the
|
|
8
|
+
# stop through (prevents infinite loops).
|
|
9
|
+
# ════════════════════════════════════════════════════════════════
|
|
10
|
+
set -uo pipefail
|
|
11
|
+
|
|
12
|
+
INPUT=$(cat)
|
|
13
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
14
|
+
ENV_FILE="$PROJECT_DIR/.harness/commands.env"
|
|
15
|
+
LOG_DIR="$PROJECT_DIR/.harness/logs"
|
|
16
|
+
|
|
17
|
+
SESSION_ID=$(python3 -c '
|
|
18
|
+
import json,sys
|
|
19
|
+
print(json.loads(sys.argv[1]).get("session_id",""))' "$INPUT" 2>/dev/null || echo "")
|
|
20
|
+
[ -n "$SESSION_ID" ] || exit 0
|
|
21
|
+
|
|
22
|
+
MARKER="$LOG_DIR/pending_test.$SESSION_ID"
|
|
23
|
+
COUNTER="$LOG_DIR/stop_blocks.$SESSION_ID"
|
|
24
|
+
|
|
25
|
+
# No code modified this session -> pass
|
|
26
|
+
[ -s "$MARKER" ] || exit 0
|
|
27
|
+
|
|
28
|
+
# Not configured -> pass silently (the harness must not break the work itself)
|
|
29
|
+
[ -f "$ENV_FILE" ] || exit 0
|
|
30
|
+
# shellcheck disable=SC1090
|
|
31
|
+
source "$ENV_FILE"
|
|
32
|
+
if [ -z "${TEST_CMD:-}" ]; then
|
|
33
|
+
rm -f "$MARKER"
|
|
34
|
+
exit 0
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# timeout may be absent on macOS — use it only if available
|
|
38
|
+
TIMEOUT_BIN=$(command -v timeout || command -v gtimeout || true)
|
|
39
|
+
TEST_OUT=$(cd "$PROJECT_DIR" && ${TIMEOUT_BIN:+"$TIMEOUT_BIN" 600} bash -c "$TEST_CMD" 2>&1 | tail -60)
|
|
40
|
+
TEST_RC=$?
|
|
41
|
+
|
|
42
|
+
if [ "$TEST_RC" -eq 0 ]; then
|
|
43
|
+
rm -f "$MARKER" "$COUNTER"
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
COUNT=0
|
|
48
|
+
[ -f "$COUNTER" ] && COUNT=$(cat "$COUNTER" 2>/dev/null || echo 0)
|
|
49
|
+
COUNT=$((COUNT + 1))
|
|
50
|
+
|
|
51
|
+
if [ "$COUNT" -ge 3 ]; then
|
|
52
|
+
# Retry ceiling reached: let the next stop through, but demand escalation
|
|
53
|
+
rm -f "$MARKER" "$COUNTER"
|
|
54
|
+
REASON="[harness stop-gate] This is the 3rd attempt to end the turn with \
|
|
55
|
+
failing tests. Stop repeating fix attempts. Write the escalation packet from \
|
|
56
|
+
the Work Loop section of CLAUDE.md, report it to the user, then stop.
|
|
57
|
+
$TEST_OUT"
|
|
58
|
+
else
|
|
59
|
+
echo "$COUNT" > "$COUNTER"
|
|
60
|
+
REASON="[harness stop-gate] Code modified in this session does not pass \
|
|
61
|
+
tests (rc=$TEST_RC, block $COUNT/3). Read the output below, fix it, then stop.
|
|
62
|
+
$TEST_OUT"
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
python3 - "$REASON" <<'PYEOF'
|
|
66
|
+
import json, sys
|
|
67
|
+
print(json.dumps({"decision": "block", "reason": sys.argv[1][:8000]},
|
|
68
|
+
ensure_ascii=False))
|
|
69
|
+
PYEOF
|
|
70
|
+
exit 0
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PreToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "Bash",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/guard-pre-bash.sh",
|
|
10
|
+
"timeout": 15,
|
|
11
|
+
"statusMessage": "harness: command safety check"
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"PostToolUse": [
|
|
17
|
+
{
|
|
18
|
+
"matcher": "Edit|Write",
|
|
19
|
+
"hooks": [
|
|
20
|
+
{
|
|
21
|
+
"type": "command",
|
|
22
|
+
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/sensor-post-edit.sh",
|
|
23
|
+
"timeout": 180,
|
|
24
|
+
"statusMessage": "harness: lint sensor"
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"matcher": "*",
|
|
30
|
+
"hooks": [
|
|
31
|
+
{
|
|
32
|
+
"type": "command",
|
|
33
|
+
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/observe-log.sh",
|
|
34
|
+
"timeout": 15
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"PostToolUseFailure": [
|
|
40
|
+
{
|
|
41
|
+
"matcher": "*",
|
|
42
|
+
"hooks": [
|
|
43
|
+
{
|
|
44
|
+
"type": "command",
|
|
45
|
+
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/observe-log.sh",
|
|
46
|
+
"timeout": 15
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"Stop": [
|
|
52
|
+
{
|
|
53
|
+
"hooks": [
|
|
54
|
+
{
|
|
55
|
+
"type": "command",
|
|
56
|
+
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/stop-gate.sh",
|
|
57
|
+
"timeout": 720,
|
|
58
|
+
"statusMessage": "harness: pre-stop test gate"
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
"SessionStart": [
|
|
64
|
+
{
|
|
65
|
+
"matcher": "startup|resume|clear|compact",
|
|
66
|
+
"hooks": [
|
|
67
|
+
{
|
|
68
|
+
"type": "command",
|
|
69
|
+
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/session-start.sh",
|
|
70
|
+
"timeout": 10
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: checkpoint
|
|
3
|
+
description: Save current work state to plan.md / decisions.jsonl / progress.json checkpoints. Use after completing a meaningful step, before pausing a long task, or when the user invokes /checkpoint.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Checkpoint — Saving Work State
|
|
7
|
+
|
|
8
|
+
The filesystem is the memory. Maintain these three files at the project root.
|
|
9
|
+
The single test: **if this session died right now, could the next session
|
|
10
|
+
pick up where it left off?**
|
|
11
|
+
|
|
12
|
+
## Procedure
|
|
13
|
+
|
|
14
|
+
1. **plan.md** — update if the plan changed. Format:
|
|
15
|
+
```markdown
|
|
16
|
+
# Plan: [task title]
|
|
17
|
+
- [x] completed step
|
|
18
|
+
- [ ] next step ← NEXT
|
|
19
|
+
- [ ] later step
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
2. **decisions.jsonl** — append only *settled decisions* from this step,
|
|
23
|
+
one per line (no exploratory reasoning or transient facts):
|
|
24
|
+
```json
|
|
25
|
+
{"ts": "ISO8601", "decision": "what was decided", "why": "one-line rationale", "alternatives_rejected": ["rejected option"]}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
3. **progress.json** — rewrite in full:
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"task_id": "task identifier",
|
|
32
|
+
"status": "in_progress | completed | abandoned",
|
|
33
|
+
"completed_steps": ["list of completed steps"],
|
|
34
|
+
"next_step": "what to do next (be specific)",
|
|
35
|
+
"artifacts": ["files created/modified"],
|
|
36
|
+
"open_issues": ["unresolved items"],
|
|
37
|
+
"last_updated": "ISO8601"
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Rules
|
|
42
|
+
|
|
43
|
+
- When the task is fully done, set `status: "completed"`. The SessionStart
|
|
44
|
+
hook does not inject completed checkpoints — forget this closing step and
|
|
45
|
+
the next session will read stale state.
|
|
46
|
+
- Write checkpoints against the recovery test: the next session must be
|
|
47
|
+
able to resume from `next_step` alone.
|
|
48
|
+
- Suggest deleting completed checkpoints older than 3 weeks (stale state
|
|
49
|
+
causes bad decisions).
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: guide-audit
|
|
3
|
+
description: Audit the RULES/ANTI-PATTERNS in CLAUDE.md and classify each rule as keep / delete candidate / convert-to-sensor candidate. Use for the monthly review or when the user invokes /guide-audit. Proposes only — applying always requires user approval.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Guide Audit — Guide Hygiene
|
|
7
|
+
|
|
8
|
+
Goal: keep CLAUDE.md down to rules that are *still* needed. Rules that only
|
|
9
|
+
accumulate are debt — find the ones a sensor now enforces, the ones that
|
|
10
|
+
lost their grounding, and the ones that contradict each other.
|
|
11
|
+
**Never apply any deletion or edit without user approval.**
|
|
12
|
+
|
|
13
|
+
## Procedure
|
|
14
|
+
|
|
15
|
+
For each entry in RULES and ANTI-PATTERNS, run five checks:
|
|
16
|
+
|
|
17
|
+
1. **Verifiability** — can compliance be judged objectively?
|
|
18
|
+
(Vague rules like "write good code" fail this.)
|
|
19
|
+
2. **Grounding** — did it come from an actually observed failure? Does it
|
|
20
|
+
carry a date and cause?
|
|
21
|
+
3. **Contradiction** — does it conflict with another rule?
|
|
22
|
+
4. **Sensor overlap** — is it already enforced automatically? Cross-check:
|
|
23
|
+
- what LINT_CMD/TEST_CMD in `.harness/commands.env` catches
|
|
24
|
+
- the DENY_PATTERNS in `guard-pre-bash.sh`
|
|
25
|
+
- the permissions (allow/ask/deny) in `settings.json`, if present
|
|
26
|
+
5. **Freshness** — when was it last seen actually working (blocking or
|
|
27
|
+
catching a violation)? Cite `.harness/logs/tool_calls.jsonl` if evidence
|
|
28
|
+
exists.
|
|
29
|
+
|
|
30
|
+
## Output Format
|
|
31
|
+
|
|
32
|
+
| Rule | Verdict | Rationale |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| (rule summary) | keep / delete candidate / convert-to-sensor candidate / rewrite candidate | one sentence |
|
|
35
|
+
|
|
36
|
+
- **Delete candidate**: already enforced by a sensor, or its originating
|
|
37
|
+
failure can no longer be traced.
|
|
38
|
+
- **Convert-to-sensor candidate**: repeatedly violated — include the
|
|
39
|
+
concrete promotion (linter rule, guard pattern, or permissions entry).
|
|
40
|
+
- **Rewrite candidate**: intent is valid but unverifiable as written —
|
|
41
|
+
include replacement wording that is observable.
|
|
42
|
+
|
|
43
|
+
## Approval and Application
|
|
44
|
+
|
|
45
|
+
- Apply only what the user approves. Deletions remove the whole line;
|
|
46
|
+
sensor conversions are applied as a pair — "add sensor + delete rule"
|
|
47
|
+
(never the half-application where the rule is gone but the sensor
|
|
48
|
+
didn't land).
|
|
49
|
+
- If the audit leaves zero rules, an empty RULES section can be the right
|
|
50
|
+
answer. Never invent rules to fill space.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: harness-init
|
|
3
|
+
description: Scan the repository of a new project to fill in the PROJECT section of CLAUDE.md and .harness/commands.env, and propose initial RULES/ANTI-PATTERNS. Run once right after installing the harness, or whenever CLAUDE.md is still in placeholder state. Verifies detected commands by actually running them before applying.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Harness Init — Initialize the Harness by Scanning the Repo
|
|
7
|
+
|
|
8
|
+
Goal: fill CLAUDE.md and `.harness/commands.env` with commands that
|
|
9
|
+
**actually work**. Never write a guessed command — only ones verified by
|
|
10
|
+
running them.
|
|
11
|
+
|
|
12
|
+
## Step 1: Scan
|
|
13
|
+
|
|
14
|
+
- Language/version: from manifests (package.json, pyproject.toml, go.mod,
|
|
15
|
+
Cargo.toml, *.csproj, ...). Prefer versions pinned in lockfiles/config.
|
|
16
|
+
- BUILD / TEST / LINT candidates: collect from manifest scripts, Makefile,
|
|
17
|
+
CI config (.github/workflows etc. — read only), and any existing README.
|
|
18
|
+
- One-line project purpose: from the README or top-level code structure.
|
|
19
|
+
|
|
20
|
+
## Step 2: Verify (the core step)
|
|
21
|
+
|
|
22
|
+
- **Actually run** each collected TEST / LINT / BUILD command (with a
|
|
23
|
+
timeout). Record the results as they are:
|
|
24
|
+
- Passes → adopt the command.
|
|
25
|
+
- Fails but the command itself is valid (tests were already broken, etc.)
|
|
26
|
+
→ adopt it, and report the current failing state to the user.
|
|
27
|
+
- Command missing or not runnable → leave that field blank and report.
|
|
28
|
+
Do not invent one.
|
|
29
|
+
- If dependencies need installing, do not run the install — tell the user
|
|
30
|
+
first.
|
|
31
|
+
|
|
32
|
+
## Step 3: Initial Rule Candidates
|
|
33
|
+
|
|
34
|
+
- Skim past mistakes with `git log --oneline --grep="fix\|revert\|hotfix" -i`;
|
|
35
|
+
if a type repeats, draft ANTI-PATTERNS candidates (date = today, cite the
|
|
36
|
+
commit hashes in parentheses).
|
|
37
|
+
- Propose at most 3 RULES candidates grounded in this codebase. Generic
|
|
38
|
+
truisms ("write good code") are forbidden — only things observable in
|
|
39
|
+
this repository.
|
|
40
|
+
|
|
41
|
+
## Existing Projects (brownfield)
|
|
42
|
+
|
|
43
|
+
- If CLAUDE.md existed before the harness was installed, never replace it.
|
|
44
|
+
Propose **appending** the harness protocol sections it lacks — PROJECT,
|
|
45
|
+
RULES, ANTI-PATTERNS, Work Loop, Checkpoints, "When a Mistake Is Found" —
|
|
46
|
+
while keeping every existing instruction intact. If an existing
|
|
47
|
+
instruction conflicts with a harness rule, surface the conflict and let
|
|
48
|
+
the user decide; never silently override either side.
|
|
49
|
+
- If the project already has its own hooks, skills, or agents under
|
|
50
|
+
`.claude/`, list them and check for overlap before proposing anything
|
|
51
|
+
(e.g. an existing post-edit test hook would double-run tests alongside
|
|
52
|
+
the harness sensor).
|
|
53
|
+
- If tests are currently failing in the existing project, say so and
|
|
54
|
+
recommend starting with LINT_CMD only (TEST_CMD empty) until the suite
|
|
55
|
+
is green — a stop gate that always fails would block every turn.
|
|
56
|
+
|
|
57
|
+
## Step 4: Propose → Approve → Apply
|
|
58
|
+
|
|
59
|
+
- Show everything as a diff and get user approval:
|
|
60
|
+
1. The PROJECT section of CLAUDE.md (PROJECT/LANGUAGE/BUILD/TEST/LINT)
|
|
61
|
+
2. LINT_CMD / TEST_CMD in `.harness/commands.env` (identical to PROJECT)
|
|
62
|
+
3. Initial RULES / ANTI-PATTERNS entries (if any)
|
|
63
|
+
- Write only what was approved. Afterwards, run TEST_CMD once more to
|
|
64
|
+
confirm the sensor configuration points at a command that really runs.
|
|
65
|
+
|
|
66
|
+
## Forbidden
|
|
67
|
+
|
|
68
|
+
- Writing any command into CLAUDE.md or commands.env that was not executed.
|
|
69
|
+
- Installing dependencies or changing the environment without approval.
|
|
70
|
+
- Inventing generic rules to fill space (an empty RULES beats a fabricated one).
|