invar-tools 1.3.0__py3-none-any.whl → 1.3.1__py3-none-any.whl
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.
- invar/shell/claude_hooks.py +387 -0
- invar/shell/commands/guard.py +2 -0
- invar/shell/commands/hooks.py +74 -0
- invar/shell/commands/init.py +30 -0
- invar/shell/commands/template_sync.py +42 -11
- invar/shell/commands/test.py +1 -1
- invar/templates/CLAUDE.md.template +25 -5
- invar/templates/config/CLAUDE.md.jinja +16 -0
- invar/templates/config/context.md.jinja +11 -6
- invar/templates/context.md.template +35 -18
- invar/templates/hooks/PostToolUse.sh.jinja +102 -0
- invar/templates/hooks/PreToolUse.sh.jinja +74 -0
- invar/templates/hooks/Stop.sh.jinja +23 -0
- invar/templates/hooks/UserPromptSubmit.sh.jinja +77 -0
- invar/templates/hooks/__init__.py +1 -0
- invar/templates/manifest.toml +2 -2
- invar/templates/protocol/INVAR.md +105 -6
- invar/templates/skills/develop/SKILL.md.jinja +4 -7
- invar/templates/skills/investigate/SKILL.md.jinja +4 -7
- invar/templates/skills/propose/SKILL.md.jinja +4 -7
- invar/templates/skills/review/SKILL.md.jinja +63 -15
- {invar_tools-1.3.0.dist-info → invar_tools-1.3.1.dist-info}/METADATA +1 -1
- {invar_tools-1.3.0.dist-info → invar_tools-1.3.1.dist-info}/RECORD +28 -21
- {invar_tools-1.3.0.dist-info → invar_tools-1.3.1.dist-info}/WHEEL +0 -0
- {invar_tools-1.3.0.dist-info → invar_tools-1.3.1.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.3.0.dist-info → invar_tools-1.3.1.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.3.0.dist-info → invar_tools-1.3.1.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.3.0.dist-info → invar_tools-1.3.1.dist-info}/licenses/NOTICE +0 -0
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
<!--invar:critical-->
|
|
2
|
+
## ⚡ Critical Rules
|
|
3
|
+
|
|
4
|
+
| Always | Remember |
|
|
5
|
+
|--------|----------|
|
|
6
|
+
| **Verify** | `invar_guard` — NOT pytest, NOT crosshair |
|
|
7
|
+
| **Core** | `@pre/@post` + doctests, NO I/O imports |
|
|
8
|
+
| **Shell** | Returns `Result[T, E]` from `returns` library |
|
|
9
|
+
| **Flow** | USBV: Understand → Specify → Build → Validate |
|
|
10
|
+
|
|
11
|
+
<!--/invar:critical-->
|
|
12
|
+
|
|
13
|
+
<!--invar:managed version="5.0"-->
|
|
1
14
|
# Project Development Guide
|
|
2
15
|
|
|
3
16
|
> **Protocol:** Follow [INVAR.md](./INVAR.md) — includes Check-In, USBV workflow, and Task Completion requirements.
|
|
@@ -120,16 +133,23 @@ When user message contains these triggers, you MUST invoke the corresponding ski
|
|
|
120
133
|
- "Am I in a workflow?"
|
|
121
134
|
- "Did I invoke the correct skill?"
|
|
122
135
|
|
|
123
|
-
|
|
136
|
+
<!--/invar:managed-->
|
|
124
137
|
|
|
138
|
+
<!--invar:project-->
|
|
125
139
|
## Project-Specific Rules
|
|
126
140
|
|
|
127
|
-
<!-- Add your
|
|
141
|
+
<!-- Add your project structure and rules here -->
|
|
128
142
|
|
|
129
|
-
|
|
143
|
+
<!--/invar:project-->
|
|
130
144
|
|
|
131
|
-
<!--
|
|
145
|
+
<!--invar:user-->
|
|
146
|
+
<!-- ========================================================================
|
|
147
|
+
USER REGION - EDITABLE
|
|
148
|
+
Add your team conventions and project-specific rules below.
|
|
149
|
+
This section is preserved across invar update and sync-self.
|
|
150
|
+
======================================================================== -->
|
|
151
|
+
<!--/invar:user-->
|
|
132
152
|
|
|
133
153
|
---
|
|
134
154
|
|
|
135
|
-
*Generated by `invar init
|
|
155
|
+
*Generated by `invar init` v5.0. Customize the user section freely.*
|
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
<!--invar:critical-->
|
|
2
|
+
## ⚡ Critical Rules
|
|
3
|
+
|
|
4
|
+
| Always | Remember |
|
|
5
|
+
|--------|----------|
|
|
6
|
+
{% if syntax == "mcp" -%}
|
|
7
|
+
| **Verify** | `invar_guard` — NOT pytest, NOT crosshair |
|
|
8
|
+
{% else -%}
|
|
9
|
+
| **Verify** | `invar guard` — NOT pytest, NOT crosshair |
|
|
10
|
+
{% endif -%}
|
|
11
|
+
| **Core** | `@pre/@post` + doctests, NO I/O imports |
|
|
12
|
+
| **Shell** | Returns `Result[T, E]` from `returns` library |
|
|
13
|
+
| **Flow** | USBV: Understand → Specify → Build → Validate |
|
|
14
|
+
|
|
15
|
+
<!--/invar:critical-->
|
|
16
|
+
|
|
1
17
|
<!--invar:managed version="{{ version }}"-->
|
|
2
18
|
# Project Development Guide
|
|
3
19
|
|
|
@@ -55,17 +55,22 @@
|
|
|
55
55
|
|
|
56
56
|
## Documentation Structure
|
|
57
57
|
|
|
58
|
-
| File | Owner | Edit? |
|
|
59
|
-
|
|
60
|
-
| INVAR.md | Invar | No — use `invar update` |
|
|
61
|
-
| CLAUDE.md | User | Yes |
|
|
62
|
-
| .invar/context.md | User | Yes (this file) |
|
|
63
|
-
| .invar/examples/ | Invar | No |
|
|
58
|
+
| File | Owner | Edit? | When to Read |
|
|
59
|
+
|------|-------|-------|--------------|
|
|
60
|
+
| INVAR.md | Invar | No — use `invar update` | Protocol details, Six Laws |
|
|
61
|
+
| CLAUDE.md | User | Yes | Project rules |
|
|
62
|
+
| .invar/context.md | User | Yes (this file) | Session start, refresh |
|
|
63
|
+
| .invar/examples/ | Invar | No | Learning Core/Shell patterns |
|
|
64
64
|
|
|
65
65
|
**Decision rule:** Is this Invar protocol or project-specific?
|
|
66
66
|
- Protocol content → Already in INVAR.md, don't duplicate
|
|
67
67
|
- Project-specific → Add to CLAUDE.md or here
|
|
68
68
|
|
|
69
|
+
**When to consult INVAR.md:**
|
|
70
|
+
- Unsure about Core/Shell separation rules
|
|
71
|
+
- Need to understand Six Laws principles
|
|
72
|
+
- Checking USBV workflow details
|
|
73
|
+
|
|
69
74
|
## Technical Debt
|
|
70
75
|
|
|
71
76
|
{% if syntax == "mcp" -%}
|
|
@@ -2,11 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
*Last updated: [DATE]*
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- Phase: [current development phase]
|
|
8
|
-
- Working on: [current task or feature]
|
|
9
|
-
- Blockers: None
|
|
5
|
+
<!-- DX-58: Keep this file under 200 lines for efficient Check-In -->
|
|
10
6
|
|
|
11
7
|
## Key Rules (Quick Reference)
|
|
12
8
|
|
|
@@ -20,7 +16,7 @@
|
|
|
20
16
|
1. Understand → 2. Specify (contracts first) → 3. Build → 4. Validate
|
|
21
17
|
|
|
22
18
|
### Verification
|
|
23
|
-
- `
|
|
19
|
+
- `invar_guard` = static + doctests + CrossHair + Hypothesis
|
|
24
20
|
- Final must show: `✓ Final: guard PASS | ...`
|
|
25
21
|
|
|
26
22
|
## Self-Reminder
|
|
@@ -41,13 +37,39 @@
|
|
|
41
37
|
|
|
42
38
|
---
|
|
43
39
|
|
|
44
|
-
##
|
|
40
|
+
## Current State
|
|
41
|
+
|
|
42
|
+
- **Phase:** [current development phase]
|
|
43
|
+
- **Working on:** [current task or feature]
|
|
44
|
+
- **Blockers:** None
|
|
45
|
+
|
|
46
|
+
## Active Work
|
|
47
|
+
|
|
48
|
+
**Current focus:** [describe current focus]
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
**Completed recently:**
|
|
51
|
+
- [recent completion 1]
|
|
52
|
+
- [recent completion 2]
|
|
53
|
+
|
|
54
|
+
---
|
|
47
55
|
|
|
48
56
|
## Lessons Learned
|
|
49
57
|
|
|
50
|
-
|
|
58
|
+
<!-- DX-58: Keep last 5-10 lessons here; older ones can be moved to .invar/archive/ if needed -->
|
|
59
|
+
|
|
60
|
+
1. [Lesson] - [Brief explanation]
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Tool Priority
|
|
65
|
+
|
|
66
|
+
| Task | Primary | Fallback |
|
|
67
|
+
|------|---------|----------|
|
|
68
|
+
| See contracts | `invar sig` | — |
|
|
69
|
+
| Find entry points | `invar map --top` | — |
|
|
70
|
+
| Verify | `invar guard` | — |
|
|
71
|
+
|
|
72
|
+
---
|
|
51
73
|
|
|
52
74
|
## Documentation Structure
|
|
53
75
|
|
|
@@ -62,6 +84,8 @@
|
|
|
62
84
|
- Protocol content → Already in INVAR.md, don't duplicate
|
|
63
85
|
- Project-specific → Add to CLAUDE.md or here
|
|
64
86
|
|
|
87
|
+
---
|
|
88
|
+
|
|
65
89
|
## Technical Debt
|
|
66
90
|
|
|
67
91
|
*Run `invar guard` to check current status.*
|
|
@@ -70,15 +94,8 @@
|
|
|
70
94
|
|------|---------|----------|
|
|
71
95
|
| (none) | — | — |
|
|
72
96
|
|
|
73
|
-
## Key Files
|
|
74
|
-
|
|
75
|
-
| File | Purpose |
|
|
76
|
-
|------|---------|
|
|
77
|
-
| INVAR.md | Protocol reference (Invar-managed) |
|
|
78
|
-
| CLAUDE.md | Project guide (customize freely) |
|
|
79
|
-
| src/core/ | Pure business logic |
|
|
80
|
-
| src/shell/ | I/O adapters |
|
|
81
|
-
|
|
82
97
|
---
|
|
83
98
|
|
|
99
|
+
<!-- ARCHIVE: For full session history, create .invar/archive/ directory -->
|
|
100
|
+
|
|
84
101
|
*Update this file when: completing major tasks, making design decisions, discovering pitfalls.*
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Invar PostToolUse Hook
|
|
3
|
+
# Protocol: v{{ protocol_version }} | Generated: {{ generated_date }}
|
|
4
|
+
# DX-57: Git-based change detection with fallback
|
|
5
|
+
|
|
6
|
+
TOOL_NAME="$1"
|
|
7
|
+
|
|
8
|
+
# Check if hooks are disabled
|
|
9
|
+
[[ -f ".claude/hooks/.invar_disabled" ]] && exit 0
|
|
10
|
+
|
|
11
|
+
# Use session-specific state directory
|
|
12
|
+
STATE_DIR="${CLAUDE_STATE_DIR:-/tmp/invar_hooks_$(id -u)}"
|
|
13
|
+
mkdir -p "$STATE_DIR" 2>/dev/null
|
|
14
|
+
|
|
15
|
+
CHANGES_FILE="$STATE_DIR/changes"
|
|
16
|
+
LAST_GUARD="$STATE_DIR/last_guard"
|
|
17
|
+
LAST_CHECK_MARKER="$STATE_DIR/last_check"
|
|
18
|
+
|
|
19
|
+
# ============================================
|
|
20
|
+
# Reset state on guard run (MCP or CLI)
|
|
21
|
+
# ============================================
|
|
22
|
+
# MCP: invar_guard tool call
|
|
23
|
+
if [[ "$TOOL_NAME" == "mcp__invar__invar_guard" ]]; then
|
|
24
|
+
date +%s > "$LAST_GUARD"
|
|
25
|
+
rm -f "$CHANGES_FILE"
|
|
26
|
+
touch "$LAST_CHECK_MARKER"
|
|
27
|
+
exit 0
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# CLI: Bash command containing "invar guard"
|
|
31
|
+
if [[ "$TOOL_NAME" == "Bash" ]]; then
|
|
32
|
+
TOOL_INPUT="$2"
|
|
33
|
+
if echo "$TOOL_INPUT" | grep -qE '"command"[^}]*invar\s+guard'; then
|
|
34
|
+
date +%s > "$LAST_GUARD"
|
|
35
|
+
rm -f "$CHANGES_FILE"
|
|
36
|
+
touch "$LAST_CHECK_MARKER"
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# ============================================
|
|
42
|
+
# Detect changes (git with fallback)
|
|
43
|
+
# ============================================
|
|
44
|
+
is_git_repo() {
|
|
45
|
+
git rev-parse --git-dir >/dev/null 2>&1
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
detect_changes() {
|
|
49
|
+
if is_git_repo; then
|
|
50
|
+
# Primary: Git-based detection (includes staged + unstaged)
|
|
51
|
+
{ git diff --name-only -- '*.py' 2>/dev/null; git diff --cached --name-only -- '*.py' 2>/dev/null; } | sort -u
|
|
52
|
+
elif [[ -f "$LAST_CHECK_MARKER" ]]; then
|
|
53
|
+
# Fallback: Timestamp-based detection (approximate)
|
|
54
|
+
find . -name "*.py" -newer "$LAST_CHECK_MARKER" -type f 2>/dev/null | \
|
|
55
|
+
grep -v __pycache__ | grep -v '.venv' | head -20
|
|
56
|
+
fi
|
|
57
|
+
# Update marker for next check
|
|
58
|
+
touch "$LAST_CHECK_MARKER" 2>/dev/null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Track changes
|
|
62
|
+
CHANGED=$(detect_changes)
|
|
63
|
+
if [[ -n "$CHANGED" ]]; then
|
|
64
|
+
echo "$CHANGED" >> "$CHANGES_FILE"
|
|
65
|
+
sort -u "$CHANGES_FILE" -o "$CHANGES_FILE" 2>/dev/null
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# ============================================
|
|
69
|
+
# Smart trigger evaluation
|
|
70
|
+
# ============================================
|
|
71
|
+
CHANGE_COUNT=$(wc -l < "$CHANGES_FILE" 2>/dev/null | tr -d ' ' || echo 0)
|
|
72
|
+
LAST_TIME=$(cat "$LAST_GUARD" 2>/dev/null || echo 0)
|
|
73
|
+
NOW=$(date +%s)
|
|
74
|
+
ELAPSED=$((NOW - LAST_TIME))
|
|
75
|
+
|
|
76
|
+
SHOULD_REMIND=false
|
|
77
|
+
REASON=""
|
|
78
|
+
|
|
79
|
+
# Trigger 1: Accumulated changes (3+ files)
|
|
80
|
+
if [[ $CHANGE_COUNT -ge 3 ]]; then
|
|
81
|
+
SHOULD_REMIND=true
|
|
82
|
+
REASON="$CHANGE_COUNT files changed"
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# Trigger 2: Core file changed (high priority)
|
|
86
|
+
if grep -qE "/core/|/contracts/" "$CHANGES_FILE" 2>/dev/null; then
|
|
87
|
+
SHOULD_REMIND=true
|
|
88
|
+
REASON="core/ files modified"
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# Trigger 3: Time threshold (>5 min with changes)
|
|
92
|
+
if [[ $ELAPSED -gt 300 && $CHANGE_COUNT -gt 0 ]]; then
|
|
93
|
+
SHOULD_REMIND=true
|
|
94
|
+
REASON=">5 min since last guard"
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
# Output reminder if triggered
|
|
98
|
+
if [[ "$SHOULD_REMIND" == "true" ]]; then
|
|
99
|
+
echo ""
|
|
100
|
+
echo "⚠️ Verification suggested: $REASON"
|
|
101
|
+
echo " Run: {{ guard_cmd }} --changed"
|
|
102
|
+
fi
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Invar PreToolUse Hook
|
|
3
|
+
# Protocol: v{{ protocol_version }} | Generated: {{ generated_date }}
|
|
4
|
+
# DX-57: Smart blocking with auto-escape for pytest/crosshair
|
|
5
|
+
|
|
6
|
+
TOOL_NAME="$1"
|
|
7
|
+
TOOL_INPUT="$2"
|
|
8
|
+
|
|
9
|
+
# Check if hooks are disabled
|
|
10
|
+
[[ -f ".claude/hooks/.invar_disabled" ]] && exit 0
|
|
11
|
+
|
|
12
|
+
# Only process Bash commands
|
|
13
|
+
[[ "$TOOL_NAME" != "Bash" ]] && exit 0
|
|
14
|
+
|
|
15
|
+
# Parse command from JSON input
|
|
16
|
+
# Primary: jq (accurate), Fallback: grep/sed (basic)
|
|
17
|
+
if command -v jq &>/dev/null; then
|
|
18
|
+
CMD=$(echo "$TOOL_INPUT" | jq -r '.command // empty' 2>/dev/null)
|
|
19
|
+
else
|
|
20
|
+
# Fallback: Extract command field using grep/sed (handles simple cases)
|
|
21
|
+
CMD=$(echo "$TOOL_INPUT" | grep -o '"command"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*: *"\(.*\)"/\1/')
|
|
22
|
+
fi
|
|
23
|
+
[[ -z "$CMD" ]] && exit 0
|
|
24
|
+
|
|
25
|
+
# ============================================
|
|
26
|
+
# pytest blocking with smart escape
|
|
27
|
+
# ============================================
|
|
28
|
+
if echo "$CMD" | grep -qE '\bpytest\b|python.*-m\s+pytest\b'; then
|
|
29
|
+
|
|
30
|
+
# Auto-escape 1: Debugging mode (--pdb, --debug, --tb=long)
|
|
31
|
+
if echo "$CMD" | grep -qE '\-\-pdb|\-\-debug|\-\-tb='; then
|
|
32
|
+
echo "⚠️ pytest debugging allowed. Run {{ guard_cmd }} after."
|
|
33
|
+
exit 0
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Auto-escape 2: External/vendor tests
|
|
37
|
+
if echo "$CMD" | grep -qE 'vendor/|third_party/|external/|node_modules/'; then
|
|
38
|
+
exit 0
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Auto-escape 3: Explicit coverage collection
|
|
42
|
+
if echo "$CMD" | grep -qE '\-\-cov'; then
|
|
43
|
+
echo "⚠️ pytest coverage allowed. Run {{ guard_cmd }} for contract verification."
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Auto-escape 4: Environment variable override
|
|
48
|
+
if [[ "$INVAR_ALLOW_PYTEST" == "1" ]]; then
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Default: Block with helpful message
|
|
53
|
+
echo "❌ Use {{ guard_cmd }} instead of pytest"
|
|
54
|
+
echo " {{ guard_cmd }} = static + doctests + CrossHair + Hypothesis"
|
|
55
|
+
echo ""
|
|
56
|
+
echo " Auto-allowed: pytest --pdb (debug), pytest --cov (coverage)"
|
|
57
|
+
echo " Manual escape: INVAR_ALLOW_PYTEST=1 pytest ..."
|
|
58
|
+
exit 1
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# ============================================
|
|
62
|
+
# crosshair blocking (always redirect)
|
|
63
|
+
# ============================================
|
|
64
|
+
if echo "$CMD" | grep -qE '\bcrosshair\b'; then
|
|
65
|
+
if [[ "$INVAR_ALLOW_CROSSHAIR" == "1" ]]; then
|
|
66
|
+
exit 0
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
echo "❌ Use {{ guard_cmd }} (includes CrossHair by default)"
|
|
70
|
+
echo " Manual escape: INVAR_ALLOW_CROSSHAIR=1 crosshair ..."
|
|
71
|
+
exit 1
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
exit 0
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Invar Stop Hook
|
|
3
|
+
# Protocol: v{{ protocol_version }} | Generated: {{ generated_date }}
|
|
4
|
+
# DX-57: Session cleanup and unverified changes warning
|
|
5
|
+
|
|
6
|
+
# Use session-specific state
|
|
7
|
+
STATE_DIR="${CLAUDE_STATE_DIR:-/tmp/invar_hooks_$(id -u)}"
|
|
8
|
+
CHANGES_FILE="$STATE_DIR/changes"
|
|
9
|
+
|
|
10
|
+
# Check for unverified changes
|
|
11
|
+
if [[ -f "$CHANGES_FILE" ]]; then
|
|
12
|
+
CHANGE_COUNT=$(wc -l < "$CHANGES_FILE" 2>/dev/null | tr -d ' ' || echo 0)
|
|
13
|
+
if [[ $CHANGE_COUNT -gt 0 ]]; then
|
|
14
|
+
echo ""
|
|
15
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
16
|
+
echo "⚠️ Invar: $CHANGE_COUNT Python files not verified"
|
|
17
|
+
echo " Run before commit: {{ guard_cmd }} --changed"
|
|
18
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
19
|
+
fi
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
# Cleanup session state
|
|
23
|
+
rm -rf "$STATE_DIR" 2>/dev/null
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Invar UserPromptSubmit Hook
|
|
3
|
+
# Protocol: v{{ protocol_version }} | Generated: {{ generated_date }}
|
|
4
|
+
# DX-57: Protocol refresh with full INVAR.md injection
|
|
5
|
+
|
|
6
|
+
USER_MESSAGE="$1"
|
|
7
|
+
|
|
8
|
+
# Check if hooks are disabled
|
|
9
|
+
[[ -f ".claude/hooks/.invar_disabled" ]] && exit 0
|
|
10
|
+
|
|
11
|
+
# Use session-specific state
|
|
12
|
+
STATE_DIR="${CLAUDE_STATE_DIR:-/tmp/invar_hooks_$(id -u)}"
|
|
13
|
+
mkdir -p "$STATE_DIR" 2>/dev/null
|
|
14
|
+
|
|
15
|
+
# Session detection: Reset if state is stale (>4 hours)
|
|
16
|
+
SESSION_MARKER="$STATE_DIR/session_start"
|
|
17
|
+
MAX_AGE_SECONDS=14400 # 4 hours
|
|
18
|
+
|
|
19
|
+
reset_session() {
|
|
20
|
+
rm -f "$STATE_DIR/msg_count" "$STATE_DIR/changes" 2>/dev/null
|
|
21
|
+
date +%s > "$SESSION_MARKER"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if [[ -f "$SESSION_MARKER" ]]; then
|
|
25
|
+
MARKER_TIME=$(cat "$SESSION_MARKER" 2>/dev/null || echo 0)
|
|
26
|
+
NOW=$(date +%s)
|
|
27
|
+
AGE=$((NOW - MARKER_TIME))
|
|
28
|
+
if [[ $AGE -gt $MAX_AGE_SECONDS ]]; then
|
|
29
|
+
reset_session
|
|
30
|
+
fi
|
|
31
|
+
else
|
|
32
|
+
reset_session
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
COUNT_FILE="$STATE_DIR/msg_count"
|
|
36
|
+
COUNT=$(cat "$COUNT_FILE" 2>/dev/null || echo 0)
|
|
37
|
+
COUNT=$((COUNT + 1))
|
|
38
|
+
echo "$COUNT" > "$COUNT_FILE"
|
|
39
|
+
|
|
40
|
+
# ============================================
|
|
41
|
+
# Keyword triggers (independent of count)
|
|
42
|
+
# ============================================
|
|
43
|
+
|
|
44
|
+
# pytest intent → immediate correction
|
|
45
|
+
if echo "$USER_MESSAGE" | grep -qiE "run.*pytest|pytest.*test|用.*pytest"; then
|
|
46
|
+
echo "<system-reminder>Use {{ guard_cmd }}, not pytest.</system-reminder>"
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Implementation intent → workflow reminder (after warmup)
|
|
50
|
+
if [[ $COUNT -gt 3 ]]; then
|
|
51
|
+
if echo "$USER_MESSAGE" | grep -qiE "^implement|^fix|^add|^实现|^修复|^添加"; then
|
|
52
|
+
echo "<system-reminder>USBV: Specify contracts → Build → Validate</system-reminder>"
|
|
53
|
+
fi
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# ============================================
|
|
57
|
+
# Progressive refresh based on message count
|
|
58
|
+
# ============================================
|
|
59
|
+
|
|
60
|
+
# Message 15: Lightweight checkpoint
|
|
61
|
+
if [[ $COUNT -eq 15 ]]; then
|
|
62
|
+
echo "<system-reminder>"
|
|
63
|
+
echo "Checkpoint: guard=verify, sig=contracts, USBV workflow."
|
|
64
|
+
echo "</system-reminder>"
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# Message 25+: Full INVAR.md injection every 10 messages
|
|
68
|
+
# SSOT: Inject entire protocol to ensure no content drift
|
|
69
|
+
if [[ $COUNT -ge 25 && $((COUNT % 10)) -eq 0 ]]; then
|
|
70
|
+
echo "<system-reminder>"
|
|
71
|
+
echo "=== Protocol Refresh (message $COUNT) ==="
|
|
72
|
+
echo ""
|
|
73
|
+
cat << 'INVAR_EOF'
|
|
74
|
+
{{ invar_protocol }}
|
|
75
|
+
INVAR_EOF
|
|
76
|
+
echo "</system-reminder>"
|
|
77
|
+
fi
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Invar Claude Code hook templates (DX-57)
|
invar/templates/manifest.toml
CHANGED
|
@@ -103,8 +103,8 @@ overwrite = ["INVAR.md", ".invar/examples/"]
|
|
|
103
103
|
merge = ["CLAUDE.md", ".claude/skills/"]
|
|
104
104
|
skip = [".invar/context.md"]
|
|
105
105
|
|
|
106
|
-
[commands.
|
|
107
|
-
# For Invar project only
|
|
106
|
+
[commands.dev_sync]
|
|
107
|
+
# For Invar project only (invar dev sync)
|
|
108
108
|
syntax = "mcp"
|
|
109
109
|
inject_project_additions = true
|
|
110
110
|
|
|
@@ -36,6 +36,31 @@
|
|
|
36
36
|
|
|
37
37
|
**Forbidden in Core:** `os`, `sys`, `subprocess`, `pathlib`, `open`, `requests`, `datetime.now`
|
|
38
38
|
|
|
39
|
+
### Decision Tree: Core vs Shell
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
Does this function...
|
|
43
|
+
│
|
|
44
|
+
├─ Read or write files? ──────────────────→ Shell
|
|
45
|
+
├─ Make network requests? ─────────────────→ Shell
|
|
46
|
+
├─ Access current time (datetime.now)? ────→ Shell OR inject as parameter
|
|
47
|
+
├─ Generate random values? ────────────────→ Shell OR inject as parameter
|
|
48
|
+
├─ Print to console? ──────────────────────→ Shell (return data, Shell logs)
|
|
49
|
+
├─ Access environment variables? ──────────→ Shell
|
|
50
|
+
│
|
|
51
|
+
└─ None of the above? ─────────────────────→ Core
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Pattern:** Inject impure values as parameters:
|
|
55
|
+
```python
|
|
56
|
+
# Core: receives 'now' as parameter (pure)
|
|
57
|
+
def is_expired(expiry: datetime, now: datetime) -> bool:
|
|
58
|
+
return now > expiry
|
|
59
|
+
|
|
60
|
+
# Shell calls with actual time
|
|
61
|
+
expired = is_expired(token.expiry, datetime.now())
|
|
62
|
+
```
|
|
63
|
+
|
|
39
64
|
## Core Example (Pure Logic)
|
|
40
65
|
|
|
41
66
|
```python
|
|
@@ -76,6 +101,50 @@ def read_config(path: Path) -> Result[dict, str]:
|
|
|
76
101
|
|
|
77
102
|
More examples: `.invar/examples/`
|
|
78
103
|
|
|
104
|
+
## Contract Rules
|
|
105
|
+
|
|
106
|
+
### Lambda Signature (Critical)
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
# WRONG: Lambda only takes first parameter
|
|
110
|
+
@pre(lambda x: x >= 0)
|
|
111
|
+
def calculate(x: int, y: int = 0): ...
|
|
112
|
+
|
|
113
|
+
# CORRECT: Lambda must include ALL parameters (even defaults)
|
|
114
|
+
@pre(lambda x, y=0: x >= 0)
|
|
115
|
+
def calculate(x: int, y: int = 0): ...
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Guard's `param_mismatch` rule catches this as ERROR.
|
|
119
|
+
|
|
120
|
+
### Meaningful Contracts
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
# Redundant - type hints already check this
|
|
124
|
+
@pre(lambda x: isinstance(x, int))
|
|
125
|
+
def calc(x: int): ...
|
|
126
|
+
|
|
127
|
+
# Meaningful - checks business logic
|
|
128
|
+
@pre(lambda x: x > 0)
|
|
129
|
+
def calc(x: int): ...
|
|
130
|
+
|
|
131
|
+
# Meaningful - checks relationship between params
|
|
132
|
+
@pre(lambda start, end: start < end)
|
|
133
|
+
def process_range(start: int, end: int): ...
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### @post Scope
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
# WRONG: @post cannot access function parameters
|
|
140
|
+
@post(lambda result: result > x) # 'x' not available!
|
|
141
|
+
def calc(x: int) -> int: ...
|
|
142
|
+
|
|
143
|
+
# CORRECT: @post can only use 'result'
|
|
144
|
+
@post(lambda result: result >= 0)
|
|
145
|
+
def calc(x: int) -> int: ...
|
|
146
|
+
```
|
|
147
|
+
|
|
79
148
|
## Check-In (Required)
|
|
80
149
|
|
|
81
150
|
Your first message MUST display:
|
|
@@ -184,27 +253,57 @@ When rule violation has valid architectural justification:
|
|
|
184
253
|
def flask_handler(): ...
|
|
185
254
|
```
|
|
186
255
|
|
|
187
|
-
|
|
256
|
+
**Valid rule names for @invar:allow:**
|
|
257
|
+
- `shell_result` — Shell function without Result return type
|
|
258
|
+
- `entry_point_too_thick` — Entry point exceeds 15 lines
|
|
259
|
+
- `forbidden_import` — I/O import in Core (rare, justify carefully)
|
|
260
|
+
|
|
261
|
+
Run `invar rules` for complete rule catalog with hints.
|
|
188
262
|
|
|
189
263
|
## Commands
|
|
190
264
|
|
|
191
265
|
```bash
|
|
192
|
-
invar guard # Full: static + doctests + CrossHair + Hypothesis
|
|
266
|
+
invar guard # Full: static + doctests + CrossHair + Hypothesis
|
|
193
267
|
invar guard --static # Static only (quick debug, ~0.5s)
|
|
194
268
|
invar guard --changed # Modified files only
|
|
269
|
+
invar guard --coverage # Collect branch coverage
|
|
195
270
|
invar sig <file> # Show contracts + signatures
|
|
196
271
|
invar map --top 10 # Most-referenced symbols
|
|
272
|
+
invar rules # List all rules with detection/hints (JSON)
|
|
197
273
|
```
|
|
198
274
|
|
|
199
275
|
## Configuration
|
|
200
276
|
|
|
201
277
|
```toml
|
|
278
|
+
# pyproject.toml or invar.toml
|
|
202
279
|
[tool.invar.guard]
|
|
203
|
-
core_paths = ["src/myapp/core"]
|
|
204
|
-
shell_paths = ["src/myapp/shell"]
|
|
205
|
-
#
|
|
280
|
+
core_paths = ["src/myapp/core"] # Default: ["src/core", "core"]
|
|
281
|
+
shell_paths = ["src/myapp/shell"] # Default: ["src/shell", "shell"]
|
|
282
|
+
max_file_lines = 500 # Default: 500 (warning at 80%)
|
|
283
|
+
max_function_lines = 50 # Default: 50
|
|
284
|
+
# Doctest lines are excluded from size calculations
|
|
206
285
|
```
|
|
207
286
|
|
|
287
|
+
## Troubleshooting
|
|
288
|
+
|
|
289
|
+
### Size Limits (Agent Quick Reference)
|
|
290
|
+
|
|
291
|
+
| Rule | Limit | Fix |
|
|
292
|
+
|------|-------|-----|
|
|
293
|
+
| `function_too_long` | **50 lines** | Extract helper: `_impl()` + main with docstring |
|
|
294
|
+
| `file_too_long` | **500 lines** | Split by responsibility |
|
|
295
|
+
| `entry_point_too_thick` | **15 lines** | Delegate to Shell functions |
|
|
296
|
+
|
|
297
|
+
*Doctest lines excluded from counts. Limits configurable in `pyproject.toml`.*
|
|
298
|
+
|
|
299
|
+
### Common Errors
|
|
300
|
+
|
|
301
|
+
| Symptom | Cause | Fix |
|
|
302
|
+
|---------|-------|-----|
|
|
303
|
+
| `param_mismatch` error | Lambda missing params | Include ALL params (even defaults) |
|
|
304
|
+
| `shell_result` error | Shell func no Result | Add Result[T,E] or @invar:allow |
|
|
305
|
+
| `is_failure()` not found | Wrong Result check | Use `isinstance(result, Failure)` |
|
|
306
|
+
|
|
208
307
|
---
|
|
209
308
|
|
|
210
|
-
*Protocol v5.0 — USBV workflow (DX-32) | [
|
|
309
|
+
*Protocol v5.0 — USBV workflow (DX-32) | [Examples](.invar/examples/)*
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
<!--invar:skill version="{{ version }}"-->
|
|
2
|
-
<!-- ========================================================================
|
|
3
|
-
SKILL REGION - DO NOT EDIT
|
|
4
|
-
This section is managed by Invar and will be overwritten on update.
|
|
5
|
-
To add project-specific extensions, use the "extensions" region below.
|
|
6
|
-
======================================================================== -->
|
|
7
1
|
---
|
|
8
2
|
name: develop
|
|
9
3
|
description: Implementation phase following USBV workflow. Use when task is clear and actionable - "add", "implement", "create", "fix", "update", "build", "write". Requires Check-In at start and Final at end.
|
|
4
|
+
_invar:
|
|
5
|
+
version: "{{ version }}"
|
|
6
|
+
managed: skill
|
|
10
7
|
---
|
|
8
|
+
<!--invar:skill-->
|
|
11
9
|
|
|
12
10
|
# Development Mode
|
|
13
11
|
|
|
@@ -303,7 +301,6 @@ Agent:
|
|
|
303
301
|
✓ Final: guard PASS | 0 errors, 1 warning
|
|
304
302
|
```
|
|
305
303
|
<!--/invar:skill-->
|
|
306
|
-
|
|
307
304
|
<!--invar:extensions-->
|
|
308
305
|
<!-- ========================================================================
|
|
309
306
|
EXTENSIONS REGION - USER EDITABLE
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
<!--invar:skill version="{{ version }}"-->
|
|
2
|
-
<!-- ========================================================================
|
|
3
|
-
SKILL REGION - DO NOT EDIT
|
|
4
|
-
This section is managed by Invar and will be overwritten on update.
|
|
5
|
-
To add project-specific extensions, use the "extensions" region below.
|
|
6
|
-
======================================================================== -->
|
|
7
1
|
---
|
|
8
2
|
name: investigate
|
|
9
3
|
description: Exploration and understanding phase. Use when task is vague, needs analysis, or requires understanding before action. Triggers on "why", "what is", "how does", "explain", "understand", "analyze", "investigate", "explore". NO CODE CHANGES in this phase.
|
|
4
|
+
_invar:
|
|
5
|
+
version: "{{ version }}"
|
|
6
|
+
managed: skill
|
|
10
7
|
---
|
|
8
|
+
<!--invar:skill-->
|
|
11
9
|
|
|
12
10
|
# Investigation Mode
|
|
13
11
|
|
|
@@ -91,7 +89,6 @@ Before any workflow action:
|
|
|
91
89
|
**Next step?**
|
|
92
90
|
```
|
|
93
91
|
<!--/invar:skill-->
|
|
94
|
-
|
|
95
92
|
<!--invar:extensions-->
|
|
96
93
|
<!-- ========================================================================
|
|
97
94
|
EXTENSIONS REGION - USER EDITABLE
|