haac-aikit 0.1.1 → 0.7.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/README.md +169 -69
- package/catalog/agents/tier1/architect.md +86 -0
- package/catalog/agents/tier1/debugger.md +64 -0
- package/catalog/agents/tier1/pr-describer.md +57 -0
- package/catalog/agents/{researcher.md → tier1/researcher.md} +1 -1
- package/catalog/agents/tier2/changelog-curator.md +60 -0
- package/catalog/agents/tier2/dependency-upgrader.md +67 -0
- package/catalog/agents/tier2/evals-author.md +65 -0
- package/catalog/agents/tier2/flake-hunter.md +57 -0
- package/catalog/agents/tier2/prompt-engineer.md +58 -0
- package/catalog/agents/tier2/simplifier.md +57 -0
- package/catalog/ci/aikit-rules.yml +55 -0
- package/catalog/docs/claude-md-reference.md +316 -0
- package/catalog/hooks/block-dangerous-bash.sh +0 -0
- package/catalog/hooks/block-force-push-main.sh +0 -0
- package/catalog/hooks/block-secrets-in-commits.sh +0 -0
- package/catalog/hooks/check-pattern-violations.sh +137 -0
- package/catalog/hooks/compaction-preservation.sh +0 -0
- package/catalog/hooks/file-guard.sh +0 -0
- package/catalog/hooks/format-on-save.sh +0 -0
- package/catalog/hooks/hooks.json +38 -0
- package/catalog/hooks/judge-rule-compliance.sh +197 -0
- package/catalog/hooks/log-rule-event.sh +80 -0
- package/catalog/hooks/session-start-prime.sh +0 -0
- package/catalog/husky/commit-msg +0 -0
- package/catalog/husky/pre-commit +0 -0
- package/catalog/husky/pre-push +0 -0
- package/catalog/rules/AGENTS.md.tmpl +28 -7
- package/catalog/rules/aikit-rules.json +37 -0
- package/catalog/rules/claude-rules/example.md +38 -0
- package/catalog/skills/tier1/software-architect.md +42 -0
- package/dist/cli.mjs +1516 -173
- package/dist/cli.mjs.map +1 -1
- package/package.json +4 -2
- /package/catalog/agents/{devops.md → tier1/devops.md} +0 -0
- /package/catalog/agents/{implementer.md → tier1/implementer.md} +0 -0
- /package/catalog/agents/{orchestrator.md → tier1/orchestrator.md} +0 -0
- /package/catalog/agents/{planner.md → tier1/planner.md} +0 -0
- /package/catalog/agents/{reviewer.md → tier1/reviewer.md} +0 -0
- /package/catalog/agents/{security-auditor.md → tier1/security-auditor.md} +0 -0
- /package/catalog/agents/{tester.md → tier1/tester.md} +0 -0
- /package/catalog/agents/{backend.md → tier2/backend.md} +0 -0
- /package/catalog/agents/{frontend.md → tier2/frontend.md} +0 -0
- /package/catalog/agents/{mobile.md → tier2/mobile.md} +0 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Telemetry hook: pattern-checks just-written files against rules in
|
|
3
|
+
# .claude/aikit-rules.json and logs violations.
|
|
4
|
+
# Fires on PostToolUse(Edit|Write).
|
|
5
|
+
#
|
|
6
|
+
# Output: appends one JSON line per violation to .aikit/events.jsonl.
|
|
7
|
+
# Schema: {"ts":"<iso>","event":"violation","rule_id":"<id>","file":"<path>","severity":"<level>","line":<n>}
|
|
8
|
+
#
|
|
9
|
+
# Read-only and non-blocking. Never fails the tool call.
|
|
10
|
+
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
INPUT="$(cat)"
|
|
14
|
+
LOG_DIR=".aikit"
|
|
15
|
+
LOG_FILE="$LOG_DIR/events.jsonl"
|
|
16
|
+
RULES_FILE=".claude/aikit-rules.json"
|
|
17
|
+
|
|
18
|
+
mkdir -p "$LOG_DIR" 2>/dev/null || exit 0
|
|
19
|
+
|
|
20
|
+
# Bail if there are no rules to check.
|
|
21
|
+
[[ ! -f "$RULES_FILE" ]] && { echo '{"decision":"approve"}'; exit 0; }
|
|
22
|
+
|
|
23
|
+
# Extract the file_path from the tool input.
|
|
24
|
+
FILE_PATH="$(echo "$INPUT" | python3 -c "import sys,json
|
|
25
|
+
try:
|
|
26
|
+
d = json.load(sys.stdin)
|
|
27
|
+
print(d.get('file_path','') or d.get('path',''))
|
|
28
|
+
except Exception:
|
|
29
|
+
pass" 2>/dev/null || echo "")"
|
|
30
|
+
|
|
31
|
+
[[ -z "$FILE_PATH" || ! -f "$FILE_PATH" ]] && { echo '{"decision":"approve"}'; exit 0; }
|
|
32
|
+
|
|
33
|
+
TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
34
|
+
|
|
35
|
+
# Use python to parse JSON rules + apply globs + scan for patterns. Bash glob
|
|
36
|
+
# matching is too limited (no exclude support, no `**`).
|
|
37
|
+
HOOK_FILE_PATH="$FILE_PATH" HOOK_RULES_PATH="$RULES_FILE" HOOK_LOG_PATH="$LOG_FILE" HOOK_TS="$TS" \
|
|
38
|
+
python3 <<'PY' 2>/dev/null || true
|
|
39
|
+
import json, re, os, sys
|
|
40
|
+
file_path = os.environ["HOOK_FILE_PATH"]
|
|
41
|
+
rules_path = os.environ["HOOK_RULES_PATH"]
|
|
42
|
+
log_path = os.environ["HOOK_LOG_PATH"]
|
|
43
|
+
ts = os.environ["HOOK_TS"]
|
|
44
|
+
try:
|
|
45
|
+
with open(rules_path, "r", encoding="utf-8") as f:
|
|
46
|
+
cfg = json.load(f)
|
|
47
|
+
except Exception:
|
|
48
|
+
sys.exit(0)
|
|
49
|
+
|
|
50
|
+
def glob_to_regex(pattern: str) -> str:
|
|
51
|
+
# Custom translator because Python 3.9's pathlib doesn't handle ** recursion
|
|
52
|
+
# and fnmatch treats ** as a single segment.
|
|
53
|
+
out = []
|
|
54
|
+
i = 0
|
|
55
|
+
while i < len(pattern):
|
|
56
|
+
c = pattern[i]
|
|
57
|
+
if c == "*":
|
|
58
|
+
if i + 1 < len(pattern) and pattern[i + 1] == "*":
|
|
59
|
+
if i + 2 < len(pattern) and pattern[i + 2] == "/":
|
|
60
|
+
out.append(r"(?:[^/]+/)*") # zero-or-more directory segments
|
|
61
|
+
i += 3
|
|
62
|
+
continue
|
|
63
|
+
out.append(".*")
|
|
64
|
+
i += 2
|
|
65
|
+
continue
|
|
66
|
+
out.append(r"[^/]*")
|
|
67
|
+
i += 1
|
|
68
|
+
elif c == "?":
|
|
69
|
+
out.append(r"[^/]")
|
|
70
|
+
i += 1
|
|
71
|
+
elif c in ".+()[]{}|^$\\":
|
|
72
|
+
out.append("\\" + c)
|
|
73
|
+
i += 1
|
|
74
|
+
else:
|
|
75
|
+
out.append(c)
|
|
76
|
+
i += 1
|
|
77
|
+
return "^" + "".join(out) + "$"
|
|
78
|
+
|
|
79
|
+
def glob_match(path: str, patterns) -> bool:
|
|
80
|
+
for p in patterns:
|
|
81
|
+
rx = re.compile(glob_to_regex(p))
|
|
82
|
+
if rx.match(path):
|
|
83
|
+
return True
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
|
88
|
+
content = f.read()
|
|
89
|
+
except Exception:
|
|
90
|
+
sys.exit(0)
|
|
91
|
+
|
|
92
|
+
events = []
|
|
93
|
+
for rule in cfg.get("rules", []):
|
|
94
|
+
rid = rule.get("id") or ""
|
|
95
|
+
pat = rule.get("pattern")
|
|
96
|
+
if not rid or not pat:
|
|
97
|
+
continue
|
|
98
|
+
includes = rule.get("fileGlobs") or ["**/*"]
|
|
99
|
+
excludes = rule.get("exclude") or []
|
|
100
|
+
if not glob_match(file_path, includes):
|
|
101
|
+
continue
|
|
102
|
+
if excludes and glob_match(file_path, excludes):
|
|
103
|
+
continue
|
|
104
|
+
try:
|
|
105
|
+
rx = re.compile(pat, re.MULTILINE)
|
|
106
|
+
except re.error as compile_err:
|
|
107
|
+
events.append({
|
|
108
|
+
"ts": ts,
|
|
109
|
+
"event": "rule_compile_error",
|
|
110
|
+
"rule_id": rid,
|
|
111
|
+
"pattern": pat[:200],
|
|
112
|
+
"error": str(compile_err)[:200],
|
|
113
|
+
})
|
|
114
|
+
continue
|
|
115
|
+
for m in rx.finditer(content):
|
|
116
|
+
line = content.count("\n", 0, m.start()) + 1
|
|
117
|
+
events.append({
|
|
118
|
+
"ts": ts,
|
|
119
|
+
"event": "violation",
|
|
120
|
+
"rule_id": rid,
|
|
121
|
+
"file": file_path,
|
|
122
|
+
"severity": rule.get("severity", "warn"),
|
|
123
|
+
"line": line,
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
if events:
|
|
127
|
+
try:
|
|
128
|
+
os.makedirs(os.path.dirname(log_path) or ".", exist_ok=True)
|
|
129
|
+
with open(log_path, "a", encoding="utf-8") as f:
|
|
130
|
+
for e in events:
|
|
131
|
+
f.write(json.dumps(e, separators=(",", ":")) + "\n")
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
134
|
+
PY
|
|
135
|
+
|
|
136
|
+
# Always approve — this hook only observes.
|
|
137
|
+
echo '{"decision":"approve"}'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/catalog/hooks/hooks.json
CHANGED
|
@@ -40,6 +40,44 @@
|
|
|
40
40
|
"type": "command",
|
|
41
41
|
"command": "bash .claude/hooks/format-on-save.sh",
|
|
42
42
|
"description": "Auto-format after file write"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"type": "command",
|
|
46
|
+
"command": "bash .claude/hooks/check-pattern-violations.sh",
|
|
47
|
+
"description": "Telemetry: pattern-check just-written file against .claude/aikit-rules.json"
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"InstructionsLoaded": [
|
|
53
|
+
{
|
|
54
|
+
"hooks": [
|
|
55
|
+
{
|
|
56
|
+
"type": "command",
|
|
57
|
+
"command": "bash .claude/hooks/log-rule-event.sh",
|
|
58
|
+
"description": "Telemetry: record which rule IDs loaded this session"
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
"Stop": [
|
|
64
|
+
{
|
|
65
|
+
"hooks": [
|
|
66
|
+
{
|
|
67
|
+
"type": "command",
|
|
68
|
+
"command": "bash .claude/hooks/judge-rule-compliance.sh",
|
|
69
|
+
"description": "Telemetry: opt-in LLM judge of rule compliance (set AIKIT_JUDGE=1)"
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
],
|
|
74
|
+
"SubagentStop": [
|
|
75
|
+
{
|
|
76
|
+
"hooks": [
|
|
77
|
+
{
|
|
78
|
+
"type": "command",
|
|
79
|
+
"command": "bash .claude/hooks/judge-rule-compliance.sh",
|
|
80
|
+
"description": "Telemetry: opt-in LLM judge of rule compliance (set AIKIT_JUDGE=1)"
|
|
43
81
|
}
|
|
44
82
|
]
|
|
45
83
|
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Telemetry hook: opt-in LLM judge that verdicts whether the assistant's last
|
|
3
|
+
# turn cited / violated / ignored each loaded rule.
|
|
4
|
+
# Fires on Stop and SubagentStop.
|
|
5
|
+
#
|
|
6
|
+
# Output: appends one JSON line per rule verdict to .aikit/events.jsonl.
|
|
7
|
+
# Schema: {"ts":"<iso>","event":"cited"|"judged_violation","rule_id":"<id>","verdict":"<text>"}
|
|
8
|
+
#
|
|
9
|
+
# Activation:
|
|
10
|
+
# - Set AIKIT_JUDGE=1 (otherwise the hook exits immediately, free).
|
|
11
|
+
# - Set ANTHROPIC_API_KEY (otherwise the hook exits silently).
|
|
12
|
+
# Cost: ~$0.001/turn with claude-haiku-4-5. Disabled by default for that reason.
|
|
13
|
+
#
|
|
14
|
+
# Read-only and non-blocking. Never fails the parent tool call.
|
|
15
|
+
|
|
16
|
+
set -uo pipefail
|
|
17
|
+
|
|
18
|
+
INPUT="$(cat)"
|
|
19
|
+
LOG_DIR=".aikit"
|
|
20
|
+
LOG_FILE="$LOG_DIR/events.jsonl"
|
|
21
|
+
MODEL="${AIKIT_JUDGE_MODEL:-claude-haiku-4-5-20251001}"
|
|
22
|
+
|
|
23
|
+
# Always echo approve at the end, even on early exit.
|
|
24
|
+
trap 'echo "{\"decision\":\"approve\"}"' EXIT
|
|
25
|
+
|
|
26
|
+
# Cheap exits before any expensive work.
|
|
27
|
+
[[ "${AIKIT_JUDGE:-0}" != "1" ]] && exit 0
|
|
28
|
+
[[ -z "${ANTHROPIC_API_KEY:-}" ]] && exit 0
|
|
29
|
+
command -v curl >/dev/null 2>&1 || exit 0
|
|
30
|
+
mkdir -p "$LOG_DIR" 2>/dev/null || exit 0
|
|
31
|
+
|
|
32
|
+
TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
33
|
+
|
|
34
|
+
# Build the judge call entirely in python so JSON escaping is correct. The
|
|
35
|
+
# script reads the hook input (assistant transcript) from $HOOK_INPUT, scans
|
|
36
|
+
# the project's rule files for declared IDs, and asks Haiku for a verdict.
|
|
37
|
+
HOOK_INPUT="$INPUT" HOOK_LOG="$LOG_FILE" HOOK_TS="$TS" HOOK_MODEL="$MODEL" \
|
|
38
|
+
python3 <<'PY' 2>/dev/null || true
|
|
39
|
+
import json, os, re, sys, urllib.request, urllib.error
|
|
40
|
+
|
|
41
|
+
api_key = os.environ.get("ANTHROPIC_API_KEY", "")
|
|
42
|
+
if not api_key:
|
|
43
|
+
sys.exit(0)
|
|
44
|
+
|
|
45
|
+
raw = os.environ.get("HOOK_INPUT", "")
|
|
46
|
+
log_path = os.environ["HOOK_LOG"]
|
|
47
|
+
ts = os.environ["HOOK_TS"]
|
|
48
|
+
model = os.environ["HOOK_MODEL"]
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
payload = json.loads(raw) if raw else {}
|
|
52
|
+
except Exception:
|
|
53
|
+
payload = {}
|
|
54
|
+
|
|
55
|
+
# Try to extract the assistant's last response. Hook input shape varies across
|
|
56
|
+
# Claude Code versions; handle a few likely fields.
|
|
57
|
+
transcript = ""
|
|
58
|
+
for key in ("assistant_message", "last_response", "transcript", "messages"):
|
|
59
|
+
val = payload.get(key)
|
|
60
|
+
if isinstance(val, str):
|
|
61
|
+
transcript = val
|
|
62
|
+
break
|
|
63
|
+
if isinstance(val, list) and val:
|
|
64
|
+
last = val[-1]
|
|
65
|
+
if isinstance(last, dict):
|
|
66
|
+
transcript = str(last.get("content") or last.get("text") or "")
|
|
67
|
+
else:
|
|
68
|
+
transcript = str(last)
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
if not transcript or len(transcript) < 20:
|
|
72
|
+
sys.exit(0)
|
|
73
|
+
|
|
74
|
+
# Cap transcript size to keep the judge prompt cheap.
|
|
75
|
+
if len(transcript) > 8000:
|
|
76
|
+
transcript = transcript[:8000] + "\n...[truncated]"
|
|
77
|
+
|
|
78
|
+
# Collect declared rule IDs from project rule files.
|
|
79
|
+
id_rx = re.compile(r"<!--\s*id:\s*([a-zA-Z][a-zA-Z0-9_-]*\.[a-zA-Z0-9._-]+)(?:\s+[a-zA-Z][a-zA-Z0-9_-]*=[^>\s]+)*\s*-->")
|
|
80
|
+
rules = {} # id -> surrounding line for context
|
|
81
|
+
candidates = ["AGENTS.md", "CLAUDE.md", ".claude/CLAUDE.md"]
|
|
82
|
+
rules_dir = ".claude/rules"
|
|
83
|
+
if os.path.isdir(rules_dir):
|
|
84
|
+
for f in os.listdir(rules_dir):
|
|
85
|
+
if f.endswith(".md"):
|
|
86
|
+
candidates.append(os.path.join(rules_dir, f))
|
|
87
|
+
|
|
88
|
+
for path in candidates:
|
|
89
|
+
if not os.path.isfile(path):
|
|
90
|
+
continue
|
|
91
|
+
try:
|
|
92
|
+
with open(path, "r", encoding="utf-8", errors="replace") as fh:
|
|
93
|
+
content = fh.read()
|
|
94
|
+
except Exception:
|
|
95
|
+
continue
|
|
96
|
+
for line in content.splitlines():
|
|
97
|
+
m = id_rx.search(line)
|
|
98
|
+
if not m:
|
|
99
|
+
continue
|
|
100
|
+
rid = m.group(1)
|
|
101
|
+
# Strip the comment to get the rule text in human-readable form.
|
|
102
|
+
text = id_rx.sub("", line).strip(" -*\t")
|
|
103
|
+
if rid and text and rid not in rules:
|
|
104
|
+
rules[rid] = text[:240]
|
|
105
|
+
|
|
106
|
+
if not rules:
|
|
107
|
+
sys.exit(0)
|
|
108
|
+
|
|
109
|
+
# Construct judge prompt. Keep it tight — Haiku rewards specificity.
|
|
110
|
+
rules_block = "\n".join(f"- {rid}: {txt}" for rid, txt in rules.items())
|
|
111
|
+
prompt = (
|
|
112
|
+
"You are a precise auditor. Given the assistant's last response and a list "
|
|
113
|
+
"of project rules, decide for each rule whether the response CITED it "
|
|
114
|
+
"(referenced it explicitly), VIOLATED it (acted against it), or was "
|
|
115
|
+
"IRRELEVANT (the rule didn't apply this turn). Respond with JSON only.\n\n"
|
|
116
|
+
f"RULES:\n{rules_block}\n\n"
|
|
117
|
+
f"ASSISTANT RESPONSE:\n{transcript}\n\n"
|
|
118
|
+
'Output JSON exactly: {"verdicts":[{"rule_id":"...","status":"cited|violated|irrelevant","reason":"<10 words"}]}'
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
req = urllib.request.Request(
|
|
122
|
+
"https://api.anthropic.com/v1/messages",
|
|
123
|
+
data=json.dumps({
|
|
124
|
+
"model": model,
|
|
125
|
+
"max_tokens": 1024,
|
|
126
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
127
|
+
}).encode("utf-8"),
|
|
128
|
+
headers={
|
|
129
|
+
"Content-Type": "application/json",
|
|
130
|
+
"x-api-key": api_key,
|
|
131
|
+
"anthropic-version": "2023-06-01",
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def log_error(reason):
|
|
136
|
+
# Errors land in a sibling log so the user (who explicitly opted in via
|
|
137
|
+
# AIKIT_JUDGE=1 and is paying for verdicts) can see why a turn produced
|
|
138
|
+
# nothing. Stays out of events.jsonl to keep the rule-event stream clean.
|
|
139
|
+
try:
|
|
140
|
+
err_path = os.path.join(os.path.dirname(log_path) or ".", "judge-errors.log")
|
|
141
|
+
os.makedirs(os.path.dirname(err_path) or ".", exist_ok=True)
|
|
142
|
+
with open(err_path, "a", encoding="utf-8") as eh:
|
|
143
|
+
eh.write(f"{ts} {reason}\n")
|
|
144
|
+
except Exception:
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
149
|
+
body = json.loads(resp.read().decode("utf-8"))
|
|
150
|
+
except urllib.error.HTTPError as e:
|
|
151
|
+
log_error(f"http {e.code}: {e.reason}")
|
|
152
|
+
sys.exit(0)
|
|
153
|
+
except urllib.error.URLError as e:
|
|
154
|
+
log_error(f"url-error: {e.reason}")
|
|
155
|
+
sys.exit(0)
|
|
156
|
+
except (TimeoutError, json.JSONDecodeError) as e:
|
|
157
|
+
log_error(f"transport: {type(e).__name__}: {e}")
|
|
158
|
+
sys.exit(0)
|
|
159
|
+
|
|
160
|
+
# Extract the model's text content.
|
|
161
|
+
text_chunks = [c.get("text", "") for c in body.get("content", []) if c.get("type") == "text"]
|
|
162
|
+
verdict_text = "\n".join(text_chunks).strip()
|
|
163
|
+
|
|
164
|
+
# Pull out the first {...} JSON object (Haiku usually emits clean JSON, but be defensive).
|
|
165
|
+
match = re.search(r"\{.*\}", verdict_text, re.DOTALL)
|
|
166
|
+
if not match:
|
|
167
|
+
log_error(f"no JSON object in model response (len={len(verdict_text)})")
|
|
168
|
+
sys.exit(0)
|
|
169
|
+
try:
|
|
170
|
+
verdicts = json.loads(match.group(0)).get("verdicts", [])
|
|
171
|
+
except Exception as e:
|
|
172
|
+
log_error(f"verdicts json parse failed: {e}")
|
|
173
|
+
sys.exit(0)
|
|
174
|
+
|
|
175
|
+
# Append events. cited → "cited", violated → "judged_violation", irrelevant → skip.
|
|
176
|
+
events = []
|
|
177
|
+
for v in verdicts:
|
|
178
|
+
rid = v.get("rule_id")
|
|
179
|
+
status = v.get("status")
|
|
180
|
+
if not rid or status not in {"cited", "violated"}:
|
|
181
|
+
continue
|
|
182
|
+
events.append({
|
|
183
|
+
"ts": ts,
|
|
184
|
+
"event": "cited" if status == "cited" else "judged_violation",
|
|
185
|
+
"rule_id": rid,
|
|
186
|
+
"verdict": (v.get("reason") or "")[:200],
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
if events:
|
|
190
|
+
try:
|
|
191
|
+
os.makedirs(os.path.dirname(log_path) or ".", exist_ok=True)
|
|
192
|
+
with open(log_path, "a", encoding="utf-8") as fh:
|
|
193
|
+
for e in events:
|
|
194
|
+
fh.write(json.dumps(e, separators=(",", ":")) + "\n")
|
|
195
|
+
except Exception:
|
|
196
|
+
pass
|
|
197
|
+
PY
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Telemetry hook: records when instruction files load and which rule IDs they contain.
|
|
3
|
+
# Fires on InstructionsLoaded.
|
|
4
|
+
#
|
|
5
|
+
# Output: appends one JSON line per loaded rule to .aikit/events.jsonl.
|
|
6
|
+
# Schema: {"ts":"<iso>","event":"loaded","rule_id":"<id>","source":"<path>"}
|
|
7
|
+
#
|
|
8
|
+
# This hook is read-only and never blocks. If anything fails it exits silently
|
|
9
|
+
# so it can never break a Claude Code session.
|
|
10
|
+
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
INPUT="$(cat)"
|
|
14
|
+
LOG_DIR=".aikit"
|
|
15
|
+
LOG_FILE="$LOG_DIR/events.jsonl"
|
|
16
|
+
|
|
17
|
+
# Bail early if we can't even create the log dir (read-only fs, etc.).
|
|
18
|
+
mkdir -p "$LOG_DIR" 2>/dev/null || { echo '{"decision":"approve"}'; exit 0; }
|
|
19
|
+
|
|
20
|
+
TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
21
|
+
|
|
22
|
+
# Do all work inside python so file paths with quotes/backslashes can't break
|
|
23
|
+
# the JSONL output (json.dumps escapes them safely).
|
|
24
|
+
HOOK_INPUT="$INPUT" HOOK_LOG="$LOG_FILE" HOOK_TS="$TS" \
|
|
25
|
+
python3 <<'PY' 2>/dev/null || true
|
|
26
|
+
import json, os, re
|
|
27
|
+
try:
|
|
28
|
+
payload = json.loads(os.environ.get("HOOK_INPUT", "") or "{}")
|
|
29
|
+
if not isinstance(payload, dict):
|
|
30
|
+
payload = {}
|
|
31
|
+
except Exception:
|
|
32
|
+
payload = {}
|
|
33
|
+
|
|
34
|
+
candidates = []
|
|
35
|
+
for key in ("files", "paths", "instructionFiles", "loaded", "memory"):
|
|
36
|
+
val = payload.get(key)
|
|
37
|
+
if isinstance(val, list):
|
|
38
|
+
candidates.extend(str(v) for v in val if v)
|
|
39
|
+
elif isinstance(val, str):
|
|
40
|
+
candidates.append(val)
|
|
41
|
+
# Always probe the standard memory file locations even if not echoed back.
|
|
42
|
+
for default in ("AGENTS.md", "CLAUDE.md", ".claude/CLAUDE.md"):
|
|
43
|
+
candidates.append(default)
|
|
44
|
+
|
|
45
|
+
# Dedupe while preserving order.
|
|
46
|
+
seen = set()
|
|
47
|
+
files = [c for c in candidates if not (c in seen or seen.add(c))]
|
|
48
|
+
|
|
49
|
+
# Match a dotted-slug rule ID, starting with a letter so docstring examples
|
|
50
|
+
# like <!-- id: ... --> don't produce phantom events. Allows optional
|
|
51
|
+
# space-separated key=value metadata between the ID and the closing -->.
|
|
52
|
+
id_rx = re.compile(r"<!--\s*id:\s*([a-zA-Z][a-zA-Z0-9_-]*\.[a-zA-Z0-9._-]+)(?:\s+[a-zA-Z][a-zA-Z0-9_-]*=[^>\s]+)*\s*-->")
|
|
53
|
+
ts = os.environ["HOOK_TS"]
|
|
54
|
+
log_path = os.environ["HOOK_LOG"]
|
|
55
|
+
|
|
56
|
+
events = []
|
|
57
|
+
for path in files:
|
|
58
|
+
if not os.path.isfile(path):
|
|
59
|
+
continue
|
|
60
|
+
try:
|
|
61
|
+
with open(path, "r", encoding="utf-8", errors="replace") as fh:
|
|
62
|
+
content = fh.read()
|
|
63
|
+
except Exception:
|
|
64
|
+
continue
|
|
65
|
+
for match in id_rx.finditer(content):
|
|
66
|
+
rule_id = match.group(1)
|
|
67
|
+
if rule_id:
|
|
68
|
+
events.append({"ts": ts, "event": "loaded", "rule_id": rule_id, "source": path})
|
|
69
|
+
|
|
70
|
+
if events:
|
|
71
|
+
try:
|
|
72
|
+
with open(log_path, "a", encoding="utf-8") as fh:
|
|
73
|
+
for e in events:
|
|
74
|
+
fh.write(json.dumps(e, separators=(",", ":")) + "\n")
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
PY
|
|
78
|
+
|
|
79
|
+
# Always approve — this hook only observes.
|
|
80
|
+
echo '{"decision":"approve"}'
|
|
File without changes
|
package/catalog/husky/commit-msg
CHANGED
|
File without changes
|
package/catalog/husky/pre-commit
CHANGED
|
File without changes
|
package/catalog/husky/pre-push
CHANGED
|
File without changes
|
|
@@ -27,20 +27,41 @@ TODO: 3-5 bullets on where things live.
|
|
|
27
27
|
TODO: How to run a single test; framework quirks.
|
|
28
28
|
|
|
29
29
|
## Commit & PR conventions
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
<!-- Rule IDs (the `<!-- id: ... -->` comments below) are used by
|
|
31
|
+
`aikit doctor --rules` to track which rules fire and which are followed.
|
|
32
|
+
They cost zero context tokens (HTML comments are stripped before injection).
|
|
33
|
+
Optional metadata after the ID:
|
|
34
|
+
emphasis=high prefix with **bold** in dialect translators
|
|
35
|
+
paths=glob,glob restrict to matching files (Cursor `globs:`, Claude `paths:`)
|
|
36
|
+
Add IDs to your own rules to opt them into observability + translation. -->
|
|
37
|
+
- <!-- id: commit.conventional-commits --> Conventional commits: `type(scope): description`
|
|
38
|
+
- <!-- id: commit.types --> Types: `feat`, `fix`, `refactor`, `test`, `docs`, `chore`, `perf`
|
|
39
|
+
- <!-- id: pr.title-under-70 --> PR title < 70 characters; body: Summary + Test Plan.
|
|
40
|
+
- <!-- id: git.no-force-push-protected emphasis=high --> Never force-push to `main`/`master`/`develop`.
|
|
34
41
|
|
|
35
42
|
## Security
|
|
36
|
-
Never read, write, or commit:
|
|
43
|
+
<!-- id: security.no-sensitive-files emphasis=high --> Never read, write, or commit:
|
|
37
44
|
- `.env*`, `secrets/`, `.ssh/`, `.aws/`, `*.pem`, `id_rsa*`
|
|
38
45
|
|
|
39
|
-
Never force-push to `main`/`master`/`develop`.
|
|
46
|
+
<!-- id: git.no-force-push-protected emphasis=high --> Never force-push to `main`/`master`/`develop`.
|
|
40
47
|
|
|
41
48
|
TODO: Project-specific security notes.
|
|
42
49
|
|
|
43
50
|
## Gotchas
|
|
44
|
-
<!-- High-signal section. Add things the agent repeatedly gets wrong.
|
|
51
|
+
<!-- High-signal section. Add things the agent repeatedly gets wrong.
|
|
52
|
+
Tip: prefix the most load-bearing rules with `IMPORTANT:` or `YOU MUST` —
|
|
53
|
+
Anthropic guidance says emphasis tokens improve adherence noticeably.
|
|
54
|
+
HTML block comments like this one are stripped before injection, so they
|
|
55
|
+
cost zero context tokens. -->
|
|
45
56
|
TODO
|
|
57
|
+
|
|
58
|
+
## When compacting
|
|
59
|
+
<!-- Tells Claude what to preserve when /compact runs. The project-root
|
|
60
|
+
CLAUDE.md / AGENTS.md is re-injected after compaction; nested files are
|
|
61
|
+
not. Use this block to lock in must-keep context. -->
|
|
62
|
+
Always preserve: the full list of files modified this session, any failing test names + error messages, and the current task being worked on.
|
|
63
|
+
|
|
64
|
+
## Further reading
|
|
65
|
+
- `docs/claude-md-reference.md` — Anthropic 2026 reference for CLAUDE.md / memory / `.claude/rules/` (shipped by `aikit` when `claude` is selected at scope ≥ standard).
|
|
66
|
+
- `.claude/rules/` — drop topic-scoped or path-scoped rule files here (see the example shipped with this kit). Path-scoped rules load only when Claude reads matching files, saving context.
|
|
46
67
|
<!-- END:haac-aikit -->
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/Hamad-Center/haac-aikit/main/schema/aikit-rules.schema.json",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"comment": "Maps rule IDs (in AGENTS.md / .claude/rules/*.md) to machine-checkable patterns. The PostToolUse hook reads this to detect violations in just-written files. Pattern-checkable rules are a SUBSET of all rules — many rules can only be observed (loaded/cited) but not auto-validated. Add your own; remove what you don't want.",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"id": "code-style.no-console-log",
|
|
8
|
+
"title": "No console.log in production code",
|
|
9
|
+
"pattern": "(?<!//\\s)console\\.log\\(",
|
|
10
|
+
"fileGlobs": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.js", "src/**/*.jsx"],
|
|
11
|
+
"exclude": ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts"],
|
|
12
|
+
"severity": "warn"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": "code-style.no-default-export",
|
|
16
|
+
"title": "Use named exports, not default",
|
|
17
|
+
"pattern": "^\\s*export\\s+default\\b",
|
|
18
|
+
"fileGlobs": ["src/**/*.ts", "src/**/*.tsx"],
|
|
19
|
+
"severity": "warn"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": "code-style.no-any",
|
|
23
|
+
"title": "Use unknown + type guards, not any",
|
|
24
|
+
"pattern": ":\\s*any\\b|<any>|as\\s+any\\b",
|
|
25
|
+
"fileGlobs": ["src/**/*.ts", "src/**/*.tsx"],
|
|
26
|
+
"exclude": ["**/*.d.ts"],
|
|
27
|
+
"severity": "warn"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "code-style.no-ts-ignore-without-comment",
|
|
31
|
+
"title": "@ts-ignore must include a reason",
|
|
32
|
+
"pattern": "@ts-ignore(?!\\s*[—:-])",
|
|
33
|
+
"fileGlobs": ["src/**/*.ts", "src/**/*.tsx"],
|
|
34
|
+
"severity": "warn"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "src/**/*.ts"
|
|
4
|
+
- "src/**/*.tsx"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Example path-scoped rule (CUSTOMISE OR DELETE)
|
|
8
|
+
|
|
9
|
+
> This file was shipped by `haac-aikit` as a starter. It demonstrates the 2026
|
|
10
|
+
> `.claude/rules/` mechanism: rules with a `paths:` frontmatter load **only when
|
|
11
|
+
> Claude reads a matching file**, saving context.
|
|
12
|
+
>
|
|
13
|
+
> Edit it to match your project's conventions, change the `paths:` glob, or
|
|
14
|
+
> delete the file entirely. See `docs/claude-md-reference.md` §6 for the full
|
|
15
|
+
> spec.
|
|
16
|
+
|
|
17
|
+
## Conventions for files matching the `paths:` glob
|
|
18
|
+
|
|
19
|
+
<!-- Rule IDs let `aikit doctor --rules` track each rule's fire-rate and
|
|
20
|
+
violation-rate over time. Match the ID exactly in `aikit-rules.json` if
|
|
21
|
+
you want pattern-based violation detection on top of the load-time log. -->
|
|
22
|
+
|
|
23
|
+
- <!-- id: code-style.no-default-export --> Use named exports, not `export default`.
|
|
24
|
+
- <!-- id: code-style.no-any --> Use `unknown` + type guards instead of `any`.
|
|
25
|
+
- <!-- id: code-style.no-console-log --> No `console.log` in production code (remove before commit).
|
|
26
|
+
- Prefix the most load-bearing rules with `IMPORTANT:` or `YOU MUST` —
|
|
27
|
+
Anthropic guidance says emphasis improves adherence.
|
|
28
|
+
|
|
29
|
+
## How to add another rule
|
|
30
|
+
|
|
31
|
+
Create another `.md` file in `.claude/rules/`. Without `paths:` frontmatter, the
|
|
32
|
+
rule loads on every session. With `paths:`, it loads only when Claude reads a
|
|
33
|
+
matching file. Subdirectories like `.claude/rules/frontend/` are supported.
|
|
34
|
+
|
|
35
|
+
## How to delete this file
|
|
36
|
+
|
|
37
|
+
`rm .claude/rules/example.md`. `aikit diff` will then report it as missing on
|
|
38
|
+
the next run; ignore the warning or run `aikit sync --force` to suppress it.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: software-architect
|
|
3
|
+
description: Use when a task introduces new services, cross-module data flow, external integrations, schema changes, or API design — pauses implementation to produce an RFC before planning begins. Also invokable explicitly.
|
|
4
|
+
version: "1.0.0"
|
|
5
|
+
source: haac-aikit
|
|
6
|
+
license: MIT
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## When to use
|
|
10
|
+
|
|
11
|
+
Auto-trigger when ANY of:
|
|
12
|
+
- New service, module, or package being introduced
|
|
13
|
+
- Cross-module data flow or shared state
|
|
14
|
+
- New external integration (API, DB, queue, auth provider)
|
|
15
|
+
- Schema or data model changes
|
|
16
|
+
- API surface being designed (REST, GraphQL, events)
|
|
17
|
+
- Feature touches ≥ 3 files across different domains
|
|
18
|
+
|
|
19
|
+
Explicit trigger: user says "run the architect", "design this first", "RFC for X"
|
|
20
|
+
|
|
21
|
+
## Process
|
|
22
|
+
|
|
23
|
+
1. **Pause** — do not write code or a plan yet.
|
|
24
|
+
|
|
25
|
+
2. **Explore** — read the codebase for existing patterns relevant to the decision; identify files, modules, and conventions already in use. Never design around patterns you haven't verified exist.
|
|
26
|
+
|
|
27
|
+
3. **Identify decisions** — surface the 2-3 key architectural choices this feature requires. Name each one explicitly.
|
|
28
|
+
|
|
29
|
+
4. **Dispatch the `architect` agent** with:
|
|
30
|
+
- Feature description and goals
|
|
31
|
+
- Relevant existing files and patterns found in exploration
|
|
32
|
+
- Known constraints (performance, security, backwards compat, deadline)
|
|
33
|
+
|
|
34
|
+
5. **Wait** — do not invoke `writing-plans` until the RFC artifact exists at `docs/decisions/YYYY-MM-DD-<topic>.md` and is committed.
|
|
35
|
+
|
|
36
|
+
## Anti-patterns to avoid
|
|
37
|
+
|
|
38
|
+
- Skipping to implementation when trigger conditions are met
|
|
39
|
+
- Producing a plan before an RFC exists
|
|
40
|
+
- Making design decisions inline in conversation without a committed artifact
|
|
41
|
+
- Inventing patterns that already exist in the codebase
|
|
42
|
+
- Running the architect agent without first exploring existing code (the agent needs grounded context)
|