@oneie/claude 0.3.0 → 0.3.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/commands/skill-create.md +100 -0
- package/hooks/hooks.json +47 -0
- package/hooks/lib/hook.sh +69 -0
- package/hooks/scripts/compact-hint.sh +36 -0
- package/hooks/scripts/config-protect.sh +56 -0
- package/hooks/scripts/gate-guard.sh +84 -0
- package/hooks/scripts/read-tracker.sh +27 -0
- package/hooks/scripts/stop-reflect.sh +12 -0
- package/package.json +21 -5
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# /skill-create — crystallise a cycle's learnings into a reusable skill
|
|
2
|
+
|
|
3
|
+
Run after a `/do` cycle closes (or any meaningful session). Reads the recent git
|
|
4
|
+
diff and the last `learnings.md` entry, then drafts a `SKILL.md` under
|
|
5
|
+
`.claude/skills/<slug>/` — so the pattern compounds instead of evaporating.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## When to invoke
|
|
10
|
+
|
|
11
|
+
- After `/do` closes a cycle and `learnings.md` has a new entry
|
|
12
|
+
- When you notice a pattern being repeated across sessions
|
|
13
|
+
- Explicitly: `/skill-create [slug]`
|
|
14
|
+
|
|
15
|
+
If `[slug]` is omitted, derive it from the most recent `learnings.md` entry
|
|
16
|
+
(the slug of the last `/do` cycle).
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## What it does
|
|
21
|
+
|
|
22
|
+
**Step 1 — Gather raw material (zero LLM)**
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Last learnings entry
|
|
26
|
+
tail -5 plans/learnings.md
|
|
27
|
+
|
|
28
|
+
# Recent cycle diff (what actually changed)
|
|
29
|
+
git diff HEAD~1 HEAD --stat
|
|
30
|
+
git diff HEAD~1 HEAD -- $(git diff HEAD~1 HEAD --name-only | grep -v 'plans/\|text/' | head -20)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Step 2 — Extract the pattern (Haiku · low)**
|
|
34
|
+
|
|
35
|
+
From the diff + learnings entry, identify:
|
|
36
|
+
- What recurring problem this cycle solved
|
|
37
|
+
- The specific shape of the solution (code pattern, file layout, command sequence)
|
|
38
|
+
- When it applies (trigger condition)
|
|
39
|
+
- When it doesn't (anti-patterns / exclusions)
|
|
40
|
+
|
|
41
|
+
Ignore one-off specifics (feature names, slugs). Keep the reusable skeleton.
|
|
42
|
+
|
|
43
|
+
**Step 3 — Write `.claude/skills/<slug>/SKILL.md`**
|
|
44
|
+
|
|
45
|
+
Use this template:
|
|
46
|
+
|
|
47
|
+
```markdown
|
|
48
|
+
# <title> skill
|
|
49
|
+
|
|
50
|
+
<one-sentence description — what this skill does for the model>
|
|
51
|
+
|
|
52
|
+
## When to use
|
|
53
|
+
<trigger phrase or condition that should invoke this skill>
|
|
54
|
+
|
|
55
|
+
## Pattern
|
|
56
|
+
<the reusable shape — code snippet, command, or process>
|
|
57
|
+
|
|
58
|
+
## Don't
|
|
59
|
+
- <anti-pattern 1>
|
|
60
|
+
- <anti-pattern 2>
|
|
61
|
+
|
|
62
|
+
## Example
|
|
63
|
+
<minimal concrete example>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Step 4 — Wire it (if applicable)**
|
|
67
|
+
|
|
68
|
+
- If the skill has a natural trigger phrase, append a row to the skills table in
|
|
69
|
+
`.claude/CLAUDE.md` so it auto-loads.
|
|
70
|
+
- If it extends an existing skill, note the relation.
|
|
71
|
+
|
|
72
|
+
**Step 5 — Close**
|
|
73
|
+
|
|
74
|
+
Emit `signal("learning:harden", 1, "skill=<slug>")` and append one line to
|
|
75
|
+
`plans/learnings.md`:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
- YYYY-MM-DD · skill:<slug> · crystallised from <source-slug> · source=skill-create
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Rules
|
|
84
|
+
|
|
85
|
+
- **Extract, don't invent.** The pattern must be in the diff or learnings entry — no
|
|
86
|
+
generalising beyond what the cycle actually did.
|
|
87
|
+
- **One skill = one pattern.** If the cycle contains two independent patterns, create
|
|
88
|
+
two skills.
|
|
89
|
+
- **Lean.** A skill that fits in 30 lines is more likely to be read than one that
|
|
90
|
+
doesn't. Cut examples to the minimum that makes the pattern clear.
|
|
91
|
+
- **No cycle-specific names.** Slug and title describe the pattern, not the feature
|
|
92
|
+
that spawned it (`worktree-isolation`, not `do-auto-fix`).
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Close
|
|
97
|
+
|
|
98
|
+
After writing the skill file: `[ ] skill-create:<slug> done` appended to
|
|
99
|
+
`plans/learnings.md`, signal emitted, session stop-reflect will pick it up as
|
|
100
|
+
a new primitive on the next session start.
|
package/hooks/hooks.json
CHANGED
|
@@ -1,9 +1,49 @@
|
|
|
1
1
|
{
|
|
2
|
+
"PreToolUse": [
|
|
3
|
+
{
|
|
4
|
+
"matcher": "Edit|Write|MultiEdit",
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"id": "hook:gate-guard",
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/scripts/gate-guard.sh\"",
|
|
10
|
+
"timeout": 5000,
|
|
11
|
+
"blocking": true
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"id": "hook:config-protect",
|
|
15
|
+
"type": "command",
|
|
16
|
+
"command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/scripts/config-protect.sh\"",
|
|
17
|
+
"timeout": 5000,
|
|
18
|
+
"blocking": true
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "hook:compact-hint",
|
|
22
|
+
"type": "command",
|
|
23
|
+
"command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/scripts/compact-hint.sh\"",
|
|
24
|
+
"timeout": 3000,
|
|
25
|
+
"blocking": false
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
],
|
|
2
30
|
"PostToolUse": [
|
|
31
|
+
{
|
|
32
|
+
"matcher": "Read",
|
|
33
|
+
"hooks": [
|
|
34
|
+
{
|
|
35
|
+
"id": "hook:read-tracker",
|
|
36
|
+
"type": "command",
|
|
37
|
+
"command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/scripts/read-tracker.sh\"",
|
|
38
|
+
"timeout": 3000
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
},
|
|
3
42
|
{
|
|
4
43
|
"matcher": "Write|Edit",
|
|
5
44
|
"hooks": [
|
|
6
45
|
{
|
|
46
|
+
"id": "hook:post-edit",
|
|
7
47
|
"type": "command",
|
|
8
48
|
"command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/scripts/post-edit-check.sh\"",
|
|
9
49
|
"timeout": 15000
|
|
@@ -14,6 +54,7 @@
|
|
|
14
54
|
"matcher": "Write|Edit|MultiEdit",
|
|
15
55
|
"hooks": [
|
|
16
56
|
{
|
|
57
|
+
"id": "hook:sync-todo-docs",
|
|
17
58
|
"type": "command",
|
|
18
59
|
"command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/scripts/sync-todo-docs.sh\"",
|
|
19
60
|
"timeout": 5000
|
|
@@ -24,6 +65,7 @@
|
|
|
24
65
|
"matcher": "Write|Edit|MultiEdit",
|
|
25
66
|
"hooks": [
|
|
26
67
|
{
|
|
68
|
+
"id": "hook:design-check",
|
|
27
69
|
"type": "command",
|
|
28
70
|
"command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/scripts/design-check.sh\"",
|
|
29
71
|
"timeout": 5000,
|
|
@@ -35,6 +77,7 @@
|
|
|
35
77
|
"matcher": "*",
|
|
36
78
|
"hooks": [
|
|
37
79
|
{
|
|
80
|
+
"id": "hook:tool-signal",
|
|
38
81
|
"type": "command",
|
|
39
82
|
"command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/scripts/tool-signal.sh\"",
|
|
40
83
|
"timeout": 3000
|
|
@@ -47,6 +90,7 @@
|
|
|
47
90
|
"matcher": "*",
|
|
48
91
|
"hooks": [
|
|
49
92
|
{
|
|
93
|
+
"id": "hook:task-complete",
|
|
50
94
|
"type": "command",
|
|
51
95
|
"command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/scripts/task-complete-verify.sh\"",
|
|
52
96
|
"timeout": 120000,
|
|
@@ -60,12 +104,14 @@
|
|
|
60
104
|
"matcher": "*",
|
|
61
105
|
"hooks": [
|
|
62
106
|
{
|
|
107
|
+
"id": "hook:session-end",
|
|
63
108
|
"type": "command",
|
|
64
109
|
"command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/scripts/session-end-verify.sh\"",
|
|
65
110
|
"timeout": 30000,
|
|
66
111
|
"blocking": false
|
|
67
112
|
},
|
|
68
113
|
{
|
|
114
|
+
"id": "hook:stop-reflect",
|
|
69
115
|
"type": "command",
|
|
70
116
|
"command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/scripts/stop-reflect.sh\"",
|
|
71
117
|
"timeout": 10000,
|
|
@@ -79,6 +125,7 @@
|
|
|
79
125
|
"matcher": "*",
|
|
80
126
|
"hooks": [
|
|
81
127
|
{
|
|
128
|
+
"id": "hook:session-start",
|
|
82
129
|
"type": "command",
|
|
83
130
|
"command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/scripts/session-start.sh\"",
|
|
84
131
|
"timeout": 5000,
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Shared hook utilities. Source from any hook:
|
|
3
|
+
# source "$CLAUDE_PROJECT_DIR/.claude/hooks/lib/hook.sh"
|
|
4
|
+
#
|
|
5
|
+
# is_hook_disabled <id> — returns 0 (skip) or 1 (run)
|
|
6
|
+
# hook_session_key <json-arg> — stable per-session key for temp files
|
|
7
|
+
|
|
8
|
+
# ── Runtime toggles ──────────────────────────────────────────────────────────
|
|
9
|
+
#
|
|
10
|
+
# ECC_DISABLED_HOOKS=hook:gate-guard,hook:stop-reflect
|
|
11
|
+
# Comma-separated hook IDs to skip. Useful in CI or setup scripts.
|
|
12
|
+
#
|
|
13
|
+
# ECC_HOOK_PROFILE=minimal|standard (default: standard)
|
|
14
|
+
# minimal — skips signal emissions, reflect, session-end, post-edit lint
|
|
15
|
+
# standard — all hooks active
|
|
16
|
+
#
|
|
17
|
+
# Per-hook disable: ECC_GATEGUARD=off (alias for hook:gate-guard)
|
|
18
|
+
|
|
19
|
+
is_hook_disabled() {
|
|
20
|
+
local id="$1"
|
|
21
|
+
local profile="${ECC_HOOK_PROFILE:-standard}"
|
|
22
|
+
|
|
23
|
+
# Explicit disable list (comma-separated)
|
|
24
|
+
if [[ -n "${ECC_DISABLED_HOOKS:-}" ]]; then
|
|
25
|
+
local IFS=','
|
|
26
|
+
for entry in $ECC_DISABLED_HOOKS; do
|
|
27
|
+
# trim whitespace
|
|
28
|
+
entry="${entry#"${entry%%[![:space:]]*}"}"
|
|
29
|
+
entry="${entry%"${entry##*[![:space:]]}"}"
|
|
30
|
+
[[ "$entry" == "$id" ]] && return 0
|
|
31
|
+
done
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Profile-based skips
|
|
35
|
+
case "$profile" in
|
|
36
|
+
minimal)
|
|
37
|
+
case "$id" in
|
|
38
|
+
hook:tool-signal|hook:stop-reflect|hook:session-end|hook:post-edit) return 0 ;;
|
|
39
|
+
esac
|
|
40
|
+
;;
|
|
41
|
+
esac
|
|
42
|
+
|
|
43
|
+
# Per-hook env aliases
|
|
44
|
+
case "$id" in
|
|
45
|
+
hook:gate-guard)
|
|
46
|
+
local v="${ECC_GATEGUARD:-}"
|
|
47
|
+
[[ "$v" =~ ^(0|off|false|disabled|disable)$ ]] && return 0
|
|
48
|
+
;;
|
|
49
|
+
esac
|
|
50
|
+
|
|
51
|
+
return 1 # not disabled — run
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Derive a stable per-session key from the hook JSON payload ($1).
|
|
55
|
+
# Uses session_id from JSON if present; falls back to a cksum of the
|
|
56
|
+
# project dir so the key is consistent across hook invocations in one
|
|
57
|
+
# session even when CLAUDE_SESSION_ID isn't set.
|
|
58
|
+
hook_session_key() {
|
|
59
|
+
local payload="${1:-}"
|
|
60
|
+
local sid
|
|
61
|
+
sid=$(printf '%s' "$payload" | jq -r '.session_id // empty' 2>/dev/null)
|
|
62
|
+
if [[ -n "$sid" ]]; then
|
|
63
|
+
# Sanitise to filesystem-safe chars
|
|
64
|
+
printf '%s' "$sid" | tr -dc 'a-zA-Z0-9_-' | cut -c1-48
|
|
65
|
+
return
|
|
66
|
+
fi
|
|
67
|
+
# Fallback: hash of project dir
|
|
68
|
+
printf '%s' "${CLAUDE_PROJECT_DIR:-$(pwd)}" | cksum | awk '{print $1}'
|
|
69
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# COMPACT-HINT — PreToolUse: suggest /compact when context is large.
|
|
3
|
+
#
|
|
4
|
+
# Non-blocking — writes one line to stderr, fires at most once per session.
|
|
5
|
+
# Threshold: 400KB transcript (~80-120k tokens depending on content density).
|
|
6
|
+
#
|
|
7
|
+
# Disable: ECC_DISABLED_HOOKS=hook:compact-hint
|
|
8
|
+
|
|
9
|
+
# shellcheck source=lib/hook.sh
|
|
10
|
+
_HOOK_LIB="${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/hooks/lib"
|
|
11
|
+
source "$_HOOK_LIB/hook.sh"
|
|
12
|
+
is_hook_disabled "hook:compact-hint" && exit 0
|
|
13
|
+
|
|
14
|
+
PAYLOAD="${1:-}"
|
|
15
|
+
[[ -z "$PAYLOAD" ]] && exit 0
|
|
16
|
+
|
|
17
|
+
# Locate the transcript — from hook JSON or env var (Claude Code sets both)
|
|
18
|
+
TRANSCRIPT=$(printf '%s' "$PAYLOAD" | jq -r '.transcript_path // .transcriptPath // empty' 2>/dev/null)
|
|
19
|
+
[[ -z "$TRANSCRIPT" ]] && TRANSCRIPT="${CLAUDE_TRANSCRIPT_PATH:-}"
|
|
20
|
+
[[ -z "$TRANSCRIPT" || ! -f "$TRANSCRIPT" ]] && exit 0
|
|
21
|
+
|
|
22
|
+
SIZE=$(wc -c < "$TRANSCRIPT" 2>/dev/null | tr -d ' ')
|
|
23
|
+
[[ -z "$SIZE" || "$SIZE" -lt 400000 ]] && exit 0
|
|
24
|
+
|
|
25
|
+
# Fire at most once per session
|
|
26
|
+
SESSION_KEY=$(hook_session_key "$PAYLOAD")
|
|
27
|
+
FLAG="/tmp/oneie-compact-hint-${SESSION_KEY}"
|
|
28
|
+
[[ -f "$FLAG" ]] && exit 0
|
|
29
|
+
touch "$FLAG"
|
|
30
|
+
|
|
31
|
+
SIZE_KB=$(( SIZE / 1024 ))
|
|
32
|
+
echo "" >&2
|
|
33
|
+
echo "⚡ Context is ~${SIZE_KB}KB — run /compact before the next big edit to keep responses fast and cheap." >&2
|
|
34
|
+
echo "" >&2
|
|
35
|
+
|
|
36
|
+
exit 0
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# CONFIG-PROTECT — PreToolUse(Edit|Write): block lint/type config weakening.
|
|
3
|
+
#
|
|
4
|
+
# Prevents introducing "off"/"warn" rules or disabling strict flags in
|
|
5
|
+
# biome.json, tsconfig*.json, or .eslintrc* files.
|
|
6
|
+
#
|
|
7
|
+
# Why: W4 verify uses biome + tsc as hard gates. Weakening the config
|
|
8
|
+
# is equivalent to deleting the gate. Fix the code — not the rules.
|
|
9
|
+
# If a rule genuinely needs changing, explain it in the PR description.
|
|
10
|
+
#
|
|
11
|
+
# Disable: ECC_DISABLED_HOOKS=hook:config-protect
|
|
12
|
+
|
|
13
|
+
# shellcheck source=lib/hook.sh
|
|
14
|
+
_HOOK_LIB="${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/hooks/lib"
|
|
15
|
+
source "$_HOOK_LIB/hook.sh"
|
|
16
|
+
is_hook_disabled "hook:config-protect" && exit 0
|
|
17
|
+
|
|
18
|
+
PAYLOAD="${1:-}"
|
|
19
|
+
[[ -z "$PAYLOAD" ]] && exit 0
|
|
20
|
+
|
|
21
|
+
TOOL=$(printf '%s' "$PAYLOAD" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
22
|
+
case "$TOOL" in Edit|Write) ;; *) exit 0 ;; esac
|
|
23
|
+
|
|
24
|
+
FILE=$(printf '%s' "$PAYLOAD" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
25
|
+
[[ -z "$FILE" ]] && exit 0
|
|
26
|
+
|
|
27
|
+
# Only gate known config files
|
|
28
|
+
case "$(basename "$FILE")" in
|
|
29
|
+
biome.json|biome.*.json|\
|
|
30
|
+
tsconfig.json|tsconfig.*.json|\
|
|
31
|
+
.eslintrc|.eslintrc.json|.eslintrc.js|.eslintrc.cjs|.eslintrc.mjs) ;;
|
|
32
|
+
*) exit 0 ;;
|
|
33
|
+
esac
|
|
34
|
+
|
|
35
|
+
# For Edit: compare old_string vs new_string — block only if weakening is NEW.
|
|
36
|
+
# For Write: check the whole content.
|
|
37
|
+
if [[ "$TOOL" == "Edit" ]]; then
|
|
38
|
+
OLD=$(printf '%s' "$PAYLOAD" | jq -r '.tool_input.old_string // empty' 2>/dev/null)
|
|
39
|
+
NEW=$(printf '%s' "$PAYLOAD" | jq -r '.tool_input.new_string // empty' 2>/dev/null)
|
|
40
|
+
[[ -z "$NEW" ]] && exit 0
|
|
41
|
+
# Only block if the weakening pattern appears in NEW but not in OLD
|
|
42
|
+
_has_weakening() { printf '%s' "$1" | grep -qE '"(off|warn)"|"noErrors":\s*true|"strict":\s*false|"skipLibCheck":\s*true|"noUnusedLocals":\s*false|"noImplicitAny":\s*false|"allowJs":\s*true'; }
|
|
43
|
+
_has_weakening "$OLD" && exit 0 # already present — not introducing it
|
|
44
|
+
_has_weakening "$NEW" || exit 0 # not in new content — safe
|
|
45
|
+
else
|
|
46
|
+
# Write: check full content
|
|
47
|
+
CONTENT=$(printf '%s' "$PAYLOAD" | jq -r '.tool_input.content // empty' 2>/dev/null)
|
|
48
|
+
[[ -z "$CONTENT" ]] && exit 0
|
|
49
|
+
printf '%s' "$CONTENT" | grep -qE '"(off|warn)"|"noErrors":\s*true|"strict":\s*false|"skipLibCheck":\s*true|"noUnusedLocals":\s*false|"noImplicitAny":\s*false|"allowJs":\s*true' || exit 0
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
FNAME=$(basename "$FILE")
|
|
53
|
+
printf '%s' "$(jq -nc --arg f "$FNAME" --arg path "$FILE" \
|
|
54
|
+
'{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":
|
|
55
|
+
("[Config-Guard] Weakening \($f) is not allowed.\n\nW4 verify uses biome + tsc as hard gates — disabling rules deletes the gate.\nFix the code, not the config.\n\nIf this change is genuinely intentional, explain it in the PR description, then\ndisable for this session: ECC_DISABLED_HOOKS=hook:config-protect")}}')"
|
|
56
|
+
exit 0
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# GATE-GUARD — PreToolUse(Edit|Write|MultiEdit): require Read before edit.
|
|
3
|
+
#
|
|
4
|
+
# Blocks the first attempt to Edit/Write an existing file that hasn't been
|
|
5
|
+
# Read in this session. Forces "understand before you change."
|
|
6
|
+
#
|
|
7
|
+
# Rules:
|
|
8
|
+
# - New files (Write creating a file that doesn't exist yet) → always allowed
|
|
9
|
+
# - Settings files (.claude/settings*.json) → always allowed
|
|
10
|
+
# - Subagent calls → allowed (parent session's reads cover the session)
|
|
11
|
+
# - File already in this session's read registry → allowed
|
|
12
|
+
# - Existing file NOT yet read → BLOCK with instructions
|
|
13
|
+
#
|
|
14
|
+
# Disable for the session: ECC_GATEGUARD=off
|
|
15
|
+
# Disable one-off: ECC_DISABLED_HOOKS=hook:gate-guard
|
|
16
|
+
# Skip globally in CI: add hook:gate-guard to ECC_DISABLED_HOOKS in env
|
|
17
|
+
|
|
18
|
+
# shellcheck source=lib/hook.sh
|
|
19
|
+
_HOOK_LIB="${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/hooks/lib"
|
|
20
|
+
source "$_HOOK_LIB/hook.sh"
|
|
21
|
+
is_hook_disabled "hook:gate-guard" && exit 0
|
|
22
|
+
|
|
23
|
+
PAYLOAD="${1:-}"
|
|
24
|
+
[[ -z "$PAYLOAD" ]] && exit 0
|
|
25
|
+
|
|
26
|
+
TOOL=$(printf '%s' "$PAYLOAD" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
27
|
+
case "$TOOL" in Edit|Write|MultiEdit) ;; *) exit 0 ;; esac
|
|
28
|
+
|
|
29
|
+
# Subagents: parent session already passed the gate for these files
|
|
30
|
+
SUBAGENT=$(printf '%s' "$PAYLOAD" | jq -r '.parent_tool_use_id // .parentToolUseId // empty' 2>/dev/null)
|
|
31
|
+
[[ -n "$SUBAGENT" ]] && exit 0
|
|
32
|
+
|
|
33
|
+
SESSION_KEY=$(hook_session_key "$PAYLOAD")
|
|
34
|
+
READS_FILE="/tmp/oneie-gate-reads-${SESSION_KEY}"
|
|
35
|
+
|
|
36
|
+
_deny() {
|
|
37
|
+
# Claude Code PreToolUse block format
|
|
38
|
+
printf '%s' "$(jq -nc --arg msg "$1" \
|
|
39
|
+
'{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":$msg}}')"
|
|
40
|
+
exit 0
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_is_settings() {
|
|
44
|
+
[[ "$1" =~ \.claude/settings[^/]*\.json$ ]]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_was_read() {
|
|
48
|
+
local fp="$1"
|
|
49
|
+
[[ -f "$READS_FILE" ]] && grep -qF "$fp" "$READS_FILE" 2>/dev/null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_block_msg() {
|
|
53
|
+
local fp="$1" verb="$2"
|
|
54
|
+
printf '[Gate-Guard] Read %s before you %s it.\n\nUse the Read tool on this file first to prove you understand it, then retry.\n\nTo disable for this session: set ECC_GATEGUARD=off\nTo disable permanently: add hook:gate-guard to ECC_DISABLED_HOOKS in your environment.' \
|
|
55
|
+
"$fp" "$verb"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
check() {
|
|
59
|
+
local fp="$1" tool="$2"
|
|
60
|
+
[[ -z "$fp" ]] && return 0
|
|
61
|
+
_is_settings "$fp" && return 0
|
|
62
|
+
# New file being created — no prior read needed
|
|
63
|
+
[[ "$tool" == "Write" && ! -f "$fp" ]] && return 0
|
|
64
|
+
# Previously read this session — allowed
|
|
65
|
+
_was_read "$fp" && return 0
|
|
66
|
+
# Existing file not read → block
|
|
67
|
+
[[ -f "$fp" ]] && { _deny "$(_block_msg "$fp" "$(printf '%s' "$tool" | tr '[:upper:]' '[:lower:]')")"; }
|
|
68
|
+
return 0
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
case "$TOOL" in
|
|
72
|
+
Edit|Write)
|
|
73
|
+
FILE=$(printf '%s' "$PAYLOAD" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
74
|
+
check "$FILE" "$TOOL"
|
|
75
|
+
;;
|
|
76
|
+
MultiEdit)
|
|
77
|
+
while IFS= read -r fp; do
|
|
78
|
+
[[ -z "$fp" ]] && continue
|
|
79
|
+
check "$fp" "Edit"
|
|
80
|
+
done < <(printf '%s' "$PAYLOAD" | jq -r '.tool_input.edits[].file_path // empty' 2>/dev/null)
|
|
81
|
+
;;
|
|
82
|
+
esac
|
|
83
|
+
|
|
84
|
+
exit 0
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# READ-TRACKER — PostToolUse(Read): record file_path as read this session.
|
|
3
|
+
#
|
|
4
|
+
# Feeds gate-guard.sh: once a file appears in the reads registry,
|
|
5
|
+
# Edit/Write on that file is allowed. Disable both with ECC_GATEGUARD=off
|
|
6
|
+
# or ECC_DISABLED_HOOKS=hook:gate-guard.
|
|
7
|
+
#
|
|
8
|
+
# Non-blocking, always exits 0. Failure to write the registry is silent
|
|
9
|
+
# (gate-guard falls back to allow on missing registry).
|
|
10
|
+
|
|
11
|
+
# shellcheck source=lib/hook.sh
|
|
12
|
+
_HOOK_LIB="${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/hooks/lib"
|
|
13
|
+
source "$_HOOK_LIB/hook.sh"
|
|
14
|
+
is_hook_disabled "hook:gate-guard" && exit 0
|
|
15
|
+
|
|
16
|
+
PAYLOAD="${1:-}"
|
|
17
|
+
[[ -z "$PAYLOAD" ]] && exit 0
|
|
18
|
+
|
|
19
|
+
FILE=$(printf '%s' "$PAYLOAD" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
20
|
+
[[ -z "$FILE" ]] && exit 0
|
|
21
|
+
|
|
22
|
+
SESSION_KEY=$(hook_session_key "$PAYLOAD")
|
|
23
|
+
READS_FILE="/tmp/oneie-gate-reads-${SESSION_KEY}"
|
|
24
|
+
|
|
25
|
+
# Append absolute path — idempotent (duplicates are fine, grep is fast)
|
|
26
|
+
echo "$FILE" >> "$READS_FILE" 2>/dev/null || true
|
|
27
|
+
exit 0
|
|
@@ -73,6 +73,18 @@ check_drift '^packages/sdk/src/' 'packages/sdk/CLAUDE.md'
|
|
|
73
73
|
check_drift '^one\.ie/web/src/' 'one.ie/web/CLAUDE.md'
|
|
74
74
|
check_drift '^api/src/' 'api/CLAUDE.md'
|
|
75
75
|
|
|
76
|
+
# (4) Completed /do cycle → suggest /skill-create to crystallise the pattern.
|
|
77
|
+
# Heuristic: learnings.md was touched AND W3 edited >=3 non-plan/non-text files.
|
|
78
|
+
if echo "$CHANGED" | grep -qF 'plans/learnings.md'; then
|
|
79
|
+
CYCLE_FILES=$(echo "$CHANGED" | grep -vE '^plans/|^text/|^\.claude/' | wc -l | awk '{print $1}')
|
|
80
|
+
if [ "$CYCLE_FILES" -ge 3 ]; then
|
|
81
|
+
# Extract the cycle slug from the last learnings entry
|
|
82
|
+
CYCLE_SLUG=$(grep -oE 'cycle [0-9]+|· [a-z][a-z0-9-]+' plans/learnings.md 2>/dev/null | tail -1 | sed 's/^· //')
|
|
83
|
+
BUF+="- **skill-create** cycle closed with ${CYCLE_FILES} file(s) changed — run \`/skill-create ${CYCLE_SLUG}\` to crystallise the pattern as a reusable skill.\n"
|
|
84
|
+
PROPOSALS=$((PROPOSALS + 1))
|
|
85
|
+
fi
|
|
86
|
+
fi
|
|
87
|
+
|
|
76
88
|
# Only write the file if we found something worth reviewing
|
|
77
89
|
if [ "$PROPOSALS" -gt 0 ]; then
|
|
78
90
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oneie/claude",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "ONE Claude Code plugin — /do lifecycle, W1-W4 BUILD engine, ONE substrate via MCP, signals, and rules",
|
|
5
5
|
"files": [
|
|
6
6
|
".claude-plugin",
|
|
@@ -19,11 +19,27 @@
|
|
|
19
19
|
"@oneie/cli": ">=0.2.0"
|
|
20
20
|
},
|
|
21
21
|
"peerDependenciesMeta": {
|
|
22
|
-
"@oneie/sdk": {
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
"@oneie/sdk": {
|
|
23
|
+
"optional": true
|
|
24
|
+
},
|
|
25
|
+
"@oneie/mcp": {
|
|
26
|
+
"optional": true
|
|
27
|
+
},
|
|
28
|
+
"@oneie/cli": {
|
|
29
|
+
"optional": true
|
|
30
|
+
}
|
|
25
31
|
},
|
|
26
|
-
"keywords": [
|
|
32
|
+
"keywords": [
|
|
33
|
+
"claude-code",
|
|
34
|
+
"plugin",
|
|
35
|
+
"do",
|
|
36
|
+
"lifecycle",
|
|
37
|
+
"mcp",
|
|
38
|
+
"signals",
|
|
39
|
+
"typedb",
|
|
40
|
+
"agents",
|
|
41
|
+
"one"
|
|
42
|
+
],
|
|
27
43
|
"license": "SEE LICENSE IN https://one.ie/free-license",
|
|
28
44
|
"repository": {
|
|
29
45
|
"type": "git",
|