invar-tools 1.7.1__py3-none-any.whl → 1.8.0__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/core/template_helpers.py +32 -0
- invar/shell/commands/init.py +51 -4
- invar/shell/commands/uninstall.py +17 -0
- invar/shell/pi_hooks.py +207 -0
- invar/templates/config/AGENT.md.jinja +198 -0
- invar/templates/hooks/pi/invar.ts.jinja +73 -0
- invar/templates/manifest.toml +1 -0
- invar/templates/skills/develop/SKILL.md.jinja +59 -0
- invar/templates/skills/investigate/SKILL.md.jinja +15 -0
- invar/templates/skills/propose/SKILL.md.jinja +33 -0
- invar/templates/skills/review/SKILL.md.jinja +15 -0
- {invar_tools-1.7.1.dist-info → invar_tools-1.8.0.dist-info}/METADATA +25 -10
- {invar_tools-1.7.1.dist-info → invar_tools-1.8.0.dist-info}/RECORD +18 -14
- {invar_tools-1.7.1.dist-info → invar_tools-1.8.0.dist-info}/WHEEL +0 -0
- {invar_tools-1.7.1.dist-info → invar_tools-1.8.0.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.7.1.dist-info → invar_tools-1.8.0.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.7.1.dist-info → invar_tools-1.8.0.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.7.1.dist-info → invar_tools-1.8.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Template transformation helpers.
|
|
3
|
+
|
|
4
|
+
Core module: pure logic for template content transformations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from deal import post
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@post(lambda result: "`" not in result or "\\`" in result)
|
|
13
|
+
@post(lambda result: "${" not in result or "\\${" in result)
|
|
14
|
+
def escape_for_js_template(content: str) -> str:
|
|
15
|
+
"""
|
|
16
|
+
Escape content for JavaScript template literal.
|
|
17
|
+
|
|
18
|
+
Escapes backticks and ${} sequences that would be interpreted
|
|
19
|
+
by JavaScript template literals.
|
|
20
|
+
|
|
21
|
+
>>> escape_for_js_template("Hello `world`")
|
|
22
|
+
'Hello \\\\`world\\\\`'
|
|
23
|
+
>>> escape_for_js_template("Value: ${x}")
|
|
24
|
+
'Value: \\\\${x}'
|
|
25
|
+
>>> escape_for_js_template("Normal text")
|
|
26
|
+
'Normal text'
|
|
27
|
+
"""
|
|
28
|
+
# Escape backticks
|
|
29
|
+
content = content.replace("`", "\\`")
|
|
30
|
+
# Escape ${} template expressions
|
|
31
|
+
content = content.replace("${", "\\${")
|
|
32
|
+
return content
|
invar/shell/commands/init.py
CHANGED
|
@@ -22,6 +22,7 @@ from invar.shell.mcp_config import (
|
|
|
22
22
|
generate_mcp_json,
|
|
23
23
|
get_recommended_method,
|
|
24
24
|
)
|
|
25
|
+
from invar.shell.pi_hooks import install_pi_hooks
|
|
25
26
|
from invar.shell.templates import (
|
|
26
27
|
add_config,
|
|
27
28
|
create_directories,
|
|
@@ -55,12 +56,17 @@ FILE_CATEGORIES: dict[str, list[tuple[str, str]]] = {
|
|
|
55
56
|
"generic": [
|
|
56
57
|
("AGENT.md", "Universal agent instructions"),
|
|
57
58
|
],
|
|
59
|
+
"pi": [
|
|
60
|
+
("CLAUDE.md", "Agent instructions (Pi compatible)"),
|
|
61
|
+
(".claude/skills/", "Workflow automation (Pi compatible)"),
|
|
62
|
+
(".pi/hooks/", "Pi-specific hooks"),
|
|
63
|
+
],
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
AGENT_CONFIGS: dict[str, dict[str, str]] = {
|
|
61
67
|
"claude": {"name": "Claude Code", "category": "claude"},
|
|
68
|
+
"pi": {"name": "Pi Coding Agent", "category": "pi"},
|
|
62
69
|
"generic": {"name": "Other (AGENT.md)", "category": "generic"},
|
|
63
|
-
# Future: "cursor", "windsurf", etc.
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
|
|
@@ -103,6 +109,7 @@ def _prompt_agent_selection() -> list[str]:
|
|
|
103
109
|
|
|
104
110
|
choices = [
|
|
105
111
|
questionary.Choice("Claude Code (recommended)", value="claude"),
|
|
112
|
+
questionary.Choice("Pi Coding Agent", value="pi"),
|
|
106
113
|
questionary.Choice("Other (AGENT.md)", value="generic"),
|
|
107
114
|
]
|
|
108
115
|
|
|
@@ -156,6 +163,8 @@ def _prompt_file_selection(agents: list[str]) -> dict[str, bool]:
|
|
|
156
163
|
category_name = category.capitalize()
|
|
157
164
|
if category == "claude":
|
|
158
165
|
category_name = "Claude Code"
|
|
166
|
+
elif category == "pi":
|
|
167
|
+
category_name = "Pi Coding Agent"
|
|
159
168
|
choices.append(questionary.Separator(f"── {category_name} ──"))
|
|
160
169
|
for file, desc in files:
|
|
161
170
|
choices.append(
|
|
@@ -241,6 +250,11 @@ def init(
|
|
|
241
250
|
"--claude",
|
|
242
251
|
help="Auto-select Claude Code, skip all prompts",
|
|
243
252
|
),
|
|
253
|
+
pi: bool = typer.Option(
|
|
254
|
+
False,
|
|
255
|
+
"--pi",
|
|
256
|
+
help="Auto-select Pi Coding Agent, skip all prompts",
|
|
257
|
+
),
|
|
244
258
|
preview: bool = typer.Option(
|
|
245
259
|
False,
|
|
246
260
|
"--preview",
|
|
@@ -252,6 +266,11 @@ def init(
|
|
|
252
266
|
|
|
253
267
|
DX-70: Simplified init with interactive selection and safe merge.
|
|
254
268
|
|
|
269
|
+
\b
|
|
270
|
+
Quick setup options:
|
|
271
|
+
- --claude Auto-select Claude Code (MCP + hooks + skills)
|
|
272
|
+
- --pi Auto-select Pi (shares CLAUDE.md + skills, adds Pi hooks)
|
|
273
|
+
|
|
255
274
|
\b
|
|
256
275
|
This command is safe - it always MERGES with existing files:
|
|
257
276
|
- File doesn't exist → Create
|
|
@@ -264,6 +283,11 @@ def init(
|
|
|
264
283
|
"""
|
|
265
284
|
from invar import __version__
|
|
266
285
|
|
|
286
|
+
# Mutual exclusivity check
|
|
287
|
+
if claude and pi:
|
|
288
|
+
console.print("[red]Error:[/red] Cannot use --claude and --pi together.")
|
|
289
|
+
raise typer.Exit(1)
|
|
290
|
+
|
|
267
291
|
# Resolve path
|
|
268
292
|
if path == Path():
|
|
269
293
|
path = Path.cwd()
|
|
@@ -272,6 +296,8 @@ def init(
|
|
|
272
296
|
# Header
|
|
273
297
|
if claude:
|
|
274
298
|
console.print(f"\n[bold]Invar v{__version__} - Quick Setup (Claude Code)[/bold]")
|
|
299
|
+
elif pi:
|
|
300
|
+
console.print(f"\n[bold]Invar v{__version__} - Quick Setup (Pi)[/bold]")
|
|
275
301
|
else:
|
|
276
302
|
console.print(f"\n[bold]Invar v{__version__} - Project Setup[/bold]")
|
|
277
303
|
console.print("=" * 45)
|
|
@@ -279,16 +305,23 @@ def init(
|
|
|
279
305
|
|
|
280
306
|
# Determine agents and files
|
|
281
307
|
if claude:
|
|
282
|
-
# Quick mode:
|
|
308
|
+
# Quick mode: Claude Code defaults
|
|
283
309
|
agents = ["claude"]
|
|
284
310
|
selected_files: dict[str, bool] = {}
|
|
285
311
|
for category in ["optional", "claude"]:
|
|
286
312
|
for file, _ in FILE_CATEGORIES.get(category, []):
|
|
287
313
|
selected_files[file] = True
|
|
314
|
+
elif pi:
|
|
315
|
+
# Quick mode: Pi defaults
|
|
316
|
+
agents = ["pi"]
|
|
317
|
+
selected_files = {}
|
|
318
|
+
for category in ["optional", "pi"]:
|
|
319
|
+
for file, _ in FILE_CATEGORIES.get(category, []):
|
|
320
|
+
selected_files[file] = True
|
|
288
321
|
else:
|
|
289
322
|
# Interactive mode
|
|
290
323
|
if not _is_interactive():
|
|
291
|
-
console.print("[yellow]Non-interactive terminal detected. Use --claude for quick setup.[/yellow]")
|
|
324
|
+
console.print("[yellow]Non-interactive terminal detected. Use --claude or --pi for quick setup.[/yellow]")
|
|
292
325
|
raise typer.Exit(1)
|
|
293
326
|
|
|
294
327
|
agents = _prompt_agent_selection()
|
|
@@ -379,6 +412,10 @@ def init(
|
|
|
379
412
|
if "claude" in agents and selected_files.get(".claude/hooks/", True):
|
|
380
413
|
install_claude_hooks(path, console)
|
|
381
414
|
|
|
415
|
+
# Install Pi hooks if selected
|
|
416
|
+
if "pi" in agents and selected_files.get(".pi/hooks/", True):
|
|
417
|
+
install_pi_hooks(path, console)
|
|
418
|
+
|
|
382
419
|
# Create MCP setup guide
|
|
383
420
|
mcp_setup = invar_dir / "mcp-setup.md"
|
|
384
421
|
if not mcp_setup.exists():
|
|
@@ -397,7 +434,7 @@ def init(
|
|
|
397
434
|
# Completion message
|
|
398
435
|
console.print(f"\n[bold green]✓ Initialized Invar v{__version__}[/bold green]")
|
|
399
436
|
|
|
400
|
-
# Show
|
|
437
|
+
# Show agent-specific tips
|
|
401
438
|
if "claude" in agents:
|
|
402
439
|
console.print()
|
|
403
440
|
console.print(
|
|
@@ -408,3 +445,13 @@ def init(
|
|
|
408
445
|
border_style="dim",
|
|
409
446
|
)
|
|
410
447
|
)
|
|
448
|
+
elif "pi" in agents:
|
|
449
|
+
console.print()
|
|
450
|
+
console.print(
|
|
451
|
+
Panel(
|
|
452
|
+
"[dim]Pi reads CLAUDE.md and .claude/skills/ directly.\n"
|
|
453
|
+
"Run [bold]pi[/bold] to start — USBV workflow is auto-enabled.[/dim]",
|
|
454
|
+
title="📌 Tip",
|
|
455
|
+
border_style="dim",
|
|
456
|
+
)
|
|
457
|
+
)
|
|
@@ -260,6 +260,23 @@ def collect_removal_targets(path: Path) -> dict:
|
|
|
260
260
|
(f".claude/hooks/{hook_file.name}", "hook, has invar marker")
|
|
261
261
|
)
|
|
262
262
|
|
|
263
|
+
# Pi hooks (LX-04)
|
|
264
|
+
pi_hooks_dir = path / ".pi" / "hooks"
|
|
265
|
+
if pi_hooks_dir.exists():
|
|
266
|
+
invar_ts = pi_hooks_dir / "invar.ts"
|
|
267
|
+
if invar_ts.exists():
|
|
268
|
+
targets["delete_files"].append((".pi/hooks/invar.ts", "Pi hook"))
|
|
269
|
+
# Check if .pi/hooks is empty after removal
|
|
270
|
+
if not any(f for f in pi_hooks_dir.iterdir() if f.name != "invar.ts"):
|
|
271
|
+
targets["delete_dirs"].append((".pi/hooks/", "empty after removal"))
|
|
272
|
+
# Check if .pi is empty
|
|
273
|
+
pi_dir = path / ".pi"
|
|
274
|
+
hooks_only = all(
|
|
275
|
+
child.name == "hooks" for child in pi_dir.iterdir() if child.is_dir()
|
|
276
|
+
)
|
|
277
|
+
if hooks_only:
|
|
278
|
+
targets["delete_dirs"].append((".pi/", "only had hooks"))
|
|
279
|
+
|
|
263
280
|
# CLAUDE.md - delete if empty user region, otherwise modify
|
|
264
281
|
claude_md = path / "CLAUDE.md"
|
|
265
282
|
if claude_md.exists():
|
invar/shell/pi_hooks.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pi Coding Agent hooks for Invar.
|
|
3
|
+
|
|
4
|
+
LX-04: Full feature parity with Claude Code hooks.
|
|
5
|
+
- pytest/crosshair blocking via tool_call
|
|
6
|
+
- Protocol injection via pi.send() for long conversations
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
from jinja2 import Environment, FileSystemLoader
|
|
17
|
+
from returns.result import Failure, Result, Success
|
|
18
|
+
|
|
19
|
+
from invar.core.template_helpers import escape_for_js_template
|
|
20
|
+
from invar.shell.claude_hooks import detect_syntax, get_invar_md_content
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from rich.console import Console
|
|
24
|
+
|
|
25
|
+
# Pi hooks directory
|
|
26
|
+
PI_HOOKS_DIR = ".pi/hooks"
|
|
27
|
+
PROTOCOL_VERSION = "5.0"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_pi_templates_path() -> Path:
|
|
31
|
+
"""Get the path to Pi hook templates."""
|
|
32
|
+
return Path(__file__).parent.parent / "templates" / "hooks" / "pi"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# @shell_complexity: Template rendering with protocol escaping
|
|
36
|
+
def generate_pi_hook_content(project_path: Path) -> Result[str, str]:
|
|
37
|
+
"""Generate Pi hook content from template."""
|
|
38
|
+
templates_path = get_pi_templates_path()
|
|
39
|
+
template_file = "invar.ts.jinja"
|
|
40
|
+
|
|
41
|
+
if not (templates_path / template_file).exists():
|
|
42
|
+
return Failure(f"Template not found: {template_file}")
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
env = Environment(
|
|
46
|
+
loader=FileSystemLoader(str(templates_path)),
|
|
47
|
+
keep_trailing_newline=True,
|
|
48
|
+
)
|
|
49
|
+
template = env.get_template(template_file)
|
|
50
|
+
|
|
51
|
+
# Determine guard command based on syntax
|
|
52
|
+
syntax = detect_syntax(project_path)
|
|
53
|
+
guard_cmd = "invar_guard" if syntax == "mcp" else "invar guard"
|
|
54
|
+
|
|
55
|
+
# Get and escape protocol content for JS template literal
|
|
56
|
+
protocol_content = get_invar_md_content(project_path)
|
|
57
|
+
protocol_escaped = escape_for_js_template(protocol_content)
|
|
58
|
+
|
|
59
|
+
# Build context for template
|
|
60
|
+
context = {
|
|
61
|
+
"protocol_version": PROTOCOL_VERSION,
|
|
62
|
+
"generated_date": datetime.now().strftime("%Y-%m-%d"),
|
|
63
|
+
"guard_cmd": guard_cmd,
|
|
64
|
+
"invar_protocol_escaped": protocol_escaped,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
content = template.render(**context)
|
|
68
|
+
return Success(content)
|
|
69
|
+
except Exception as e:
|
|
70
|
+
return Failure(f"Failed to generate Pi hook: {e}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def install_pi_hooks(
|
|
74
|
+
project_path: Path,
|
|
75
|
+
console: Console,
|
|
76
|
+
) -> Result[list[str], str]:
|
|
77
|
+
"""
|
|
78
|
+
Install Pi hooks for Invar.
|
|
79
|
+
|
|
80
|
+
Creates .pi/hooks/invar.ts with:
|
|
81
|
+
- pytest/crosshair blocking
|
|
82
|
+
- Protocol injection for long conversations
|
|
83
|
+
"""
|
|
84
|
+
hooks_dir = project_path / PI_HOOKS_DIR
|
|
85
|
+
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
|
|
87
|
+
console.print("\n[bold]Installing Pi hooks (LX-04)...[/bold]")
|
|
88
|
+
console.print(" Hooks will:")
|
|
89
|
+
console.print(" ✓ Block pytest/crosshair → redirect to invar guard")
|
|
90
|
+
console.print(" ✓ Refresh protocol in long conversations")
|
|
91
|
+
console.print("")
|
|
92
|
+
|
|
93
|
+
result = generate_pi_hook_content(project_path)
|
|
94
|
+
if isinstance(result, Failure):
|
|
95
|
+
console.print(f" [red]Failed:[/red] {result.failure()}")
|
|
96
|
+
return Failure(result.failure())
|
|
97
|
+
|
|
98
|
+
content = result.unwrap()
|
|
99
|
+
hook_file = hooks_dir / "invar.ts"
|
|
100
|
+
hook_file.write_text(content)
|
|
101
|
+
|
|
102
|
+
console.print(f" [green]Created[/green] {PI_HOOKS_DIR}/invar.ts")
|
|
103
|
+
console.print("\n [bold green]✓ Pi hooks installed[/bold green]")
|
|
104
|
+
console.print(" [dim]Requires: Pi coding agent with hooks support[/dim]")
|
|
105
|
+
console.print(" [yellow]⚠ Restart Pi session for hooks to take effect[/yellow]")
|
|
106
|
+
|
|
107
|
+
return Success(["invar.ts"])
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# @shell_complexity: Version detection and conditional update logic
|
|
111
|
+
def sync_pi_hooks(
|
|
112
|
+
project_path: Path,
|
|
113
|
+
console: Console,
|
|
114
|
+
) -> Result[list[str], str]:
|
|
115
|
+
"""
|
|
116
|
+
Update Pi hooks with current INVAR.md content.
|
|
117
|
+
|
|
118
|
+
Called during `invar init` to ensure hooks stay in sync with protocol.
|
|
119
|
+
Only updates if Pi hooks are already installed.
|
|
120
|
+
"""
|
|
121
|
+
hooks_dir = project_path / PI_HOOKS_DIR
|
|
122
|
+
hook_file = hooks_dir / "invar.ts"
|
|
123
|
+
|
|
124
|
+
if not hook_file.exists():
|
|
125
|
+
return Success([]) # No hooks installed, nothing to sync
|
|
126
|
+
|
|
127
|
+
# Check version in existing hook
|
|
128
|
+
try:
|
|
129
|
+
existing_content = hook_file.read_text()
|
|
130
|
+
version_match = re.search(r"Protocol: v([\d.]+)", existing_content)
|
|
131
|
+
old_version = version_match.group(1) if version_match else "unknown"
|
|
132
|
+
|
|
133
|
+
if old_version != PROTOCOL_VERSION:
|
|
134
|
+
console.print(f"[cyan]Updating Pi hooks: v{old_version} → v{PROTOCOL_VERSION}[/cyan]")
|
|
135
|
+
else:
|
|
136
|
+
console.print("[dim]Refreshing Pi hooks...[/dim]")
|
|
137
|
+
except OSError:
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
result = generate_pi_hook_content(project_path)
|
|
141
|
+
if isinstance(result, Failure):
|
|
142
|
+
console.print(f" [yellow]Warning:[/yellow] Failed to generate Pi hook: {result.failure()}")
|
|
143
|
+
return Failure(result.failure())
|
|
144
|
+
|
|
145
|
+
content = result.unwrap()
|
|
146
|
+
hook_file.write_text(content)
|
|
147
|
+
console.print("[green]✓[/green] Pi hooks synced")
|
|
148
|
+
|
|
149
|
+
return Success(["invar.ts"])
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def remove_pi_hooks(
|
|
153
|
+
project_path: Path,
|
|
154
|
+
console: Console,
|
|
155
|
+
) -> Result[None, str]:
|
|
156
|
+
"""Remove Pi hooks."""
|
|
157
|
+
hooks_dir = project_path / PI_HOOKS_DIR
|
|
158
|
+
hook_file = hooks_dir / "invar.ts"
|
|
159
|
+
|
|
160
|
+
if hook_file.exists():
|
|
161
|
+
hook_file.unlink()
|
|
162
|
+
console.print(f" [red]Removed[/red] {PI_HOOKS_DIR}/invar.ts")
|
|
163
|
+
|
|
164
|
+
# Remove directory if empty
|
|
165
|
+
try:
|
|
166
|
+
hooks_dir.rmdir()
|
|
167
|
+
console.print(f" [red]Removed[/red] {PI_HOOKS_DIR}/")
|
|
168
|
+
except OSError:
|
|
169
|
+
pass # Directory not empty, keep it
|
|
170
|
+
|
|
171
|
+
console.print("[bold green]✓ Pi hooks removed[/bold green]")
|
|
172
|
+
else:
|
|
173
|
+
console.print("[dim]No Pi hooks installed[/dim]")
|
|
174
|
+
|
|
175
|
+
return Success(None)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def pi_hooks_status(
|
|
179
|
+
project_path: Path,
|
|
180
|
+
console: Console,
|
|
181
|
+
) -> Result[dict[str, str], str]:
|
|
182
|
+
"""Check status of Pi hooks."""
|
|
183
|
+
hooks_dir = project_path / PI_HOOKS_DIR
|
|
184
|
+
hook_file = hooks_dir / "invar.ts"
|
|
185
|
+
|
|
186
|
+
status: dict[str, str] = {}
|
|
187
|
+
|
|
188
|
+
if not hook_file.exists():
|
|
189
|
+
console.print("[dim]No Pi hooks installed[/dim]")
|
|
190
|
+
return Success({"status": "not_installed"})
|
|
191
|
+
|
|
192
|
+
status["status"] = "installed"
|
|
193
|
+
|
|
194
|
+
# Try to get version
|
|
195
|
+
try:
|
|
196
|
+
content = hook_file.read_text()
|
|
197
|
+
match = re.search(r"Protocol: v([\d.]+)", content)
|
|
198
|
+
if match:
|
|
199
|
+
version = match.group(1)
|
|
200
|
+
status["version"] = version
|
|
201
|
+
console.print(f"[green]✓ Pi hooks installed (v{version})[/green]")
|
|
202
|
+
else:
|
|
203
|
+
console.print("[green]✓ Pi hooks installed[/green]")
|
|
204
|
+
except OSError:
|
|
205
|
+
console.print("[green]✓ Pi hooks installed[/green]")
|
|
206
|
+
|
|
207
|
+
return Success(status)
|
|
@@ -0,0 +1,198 @@
|
|
|
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
|
+
### Contract Rules (CRITICAL)
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
# ❌ WRONG: Lambda must include ALL parameters
|
|
19
|
+
@pre(lambda x: x >= 0)
|
|
20
|
+
def calc(x: int, y: int = 0): ...
|
|
21
|
+
|
|
22
|
+
# ✅ CORRECT: Include defaults too
|
|
23
|
+
@pre(lambda x, y=0: x >= 0)
|
|
24
|
+
def calc(x: int, y: int = 0): ...
|
|
25
|
+
|
|
26
|
+
# ❌ WRONG: @post cannot access parameters
|
|
27
|
+
@post(lambda result: result > x) # 'x' not available!
|
|
28
|
+
|
|
29
|
+
# ✅ CORRECT: @post only sees 'result'
|
|
30
|
+
@post(lambda result: result >= 0)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
<!--/invar:critical-->
|
|
34
|
+
|
|
35
|
+
<!--invar:managed version="{{ version }}"-->
|
|
36
|
+
# Project Development Guide
|
|
37
|
+
|
|
38
|
+
> **Protocol:** Follow [INVAR.md](./INVAR.md) — includes Check-In, USBV workflow, and Task Completion requirements.
|
|
39
|
+
|
|
40
|
+
## Check-In
|
|
41
|
+
|
|
42
|
+
> See [INVAR.md#check-in](./INVAR.md#check-in-required) for full protocol.
|
|
43
|
+
|
|
44
|
+
**Your first message MUST display:** `✓ Check-In: [project] | [branch] | [clean/dirty]`
|
|
45
|
+
|
|
46
|
+
**Actions:** Read `.invar/context.md`, then show status. Do NOT run guard at Check-In.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Final
|
|
51
|
+
|
|
52
|
+
Your last message for an implementation task MUST display:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
✓ Final: guard PASS | 0 errors, 2 warnings
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
{% if syntax == "mcp" -%}
|
|
59
|
+
Execute `invar_guard()` and show this one-line summary.
|
|
60
|
+
{% else -%}
|
|
61
|
+
Execute `invar guard` and show this one-line summary.
|
|
62
|
+
{% endif %}
|
|
63
|
+
|
|
64
|
+
This is your sign-out. Completes the Check-In/Final pair.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Project Structure
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
src/{project}/
|
|
72
|
+
├── core/ # Pure logic (@pre/@post, doctests, no I/O)
|
|
73
|
+
└── shell/ # I/O operations (Result[T, E] return type)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Key insight:** Core receives data (strings), Shell handles I/O (paths, files).
|
|
77
|
+
|
|
78
|
+
## Quick Reference
|
|
79
|
+
|
|
80
|
+
| Zone | Requirements |
|
|
81
|
+
|------|-------------|
|
|
82
|
+
| Core | `@pre`/`@post` + doctests, pure (no I/O) |
|
|
83
|
+
| Shell | Returns `Result[T, E]` from `returns` library |
|
|
84
|
+
|
|
85
|
+
### Core vs Shell (Edge Cases)
|
|
86
|
+
|
|
87
|
+
- File/network/env vars → **Shell**
|
|
88
|
+
- `datetime.now()`, `random` → **Inject param** OR Shell
|
|
89
|
+
- Pure logic → **Core**
|
|
90
|
+
|
|
91
|
+
> Full decision tree: [INVAR.md#core-shell](./INVAR.md#decision-tree-core-vs-shell)
|
|
92
|
+
|
|
93
|
+
## Documentation Structure
|
|
94
|
+
|
|
95
|
+
| File | Owner | Edit? | Purpose |
|
|
96
|
+
|------|-------|-------|---------|
|
|
97
|
+
| INVAR.md | Invar | No | Protocol (`invar update` to sync) |
|
|
98
|
+
| AGENT.md | User | Yes | Project customization (this file) |
|
|
99
|
+
| .invar/context.md | User | Yes | Project state, lessons learned |
|
|
100
|
+
| .invar/examples/ | Invar | No | **Must read:** Core/Shell patterns, workflow |
|
|
101
|
+
|
|
102
|
+
> **Before writing code:** Check Task Router in `.invar/context.md`
|
|
103
|
+
|
|
104
|
+
## USBV Workflow
|
|
105
|
+
|
|
106
|
+
For complex tasks (3+ functions), follow these phases:
|
|
107
|
+
|
|
108
|
+
### 1. UNDERSTAND
|
|
109
|
+
|
|
110
|
+
- **Intent:** What exactly needs to be done?
|
|
111
|
+
{% if syntax == "mcp" -%}
|
|
112
|
+
- **Inspect:** Use `invar_sig` to see existing contracts
|
|
113
|
+
{% else -%}
|
|
114
|
+
- **Inspect:** Use `invar sig` to see existing contracts
|
|
115
|
+
{% endif -%}
|
|
116
|
+
- **Context:** Read relevant code, understand patterns
|
|
117
|
+
- **Constraints:** What must NOT change?
|
|
118
|
+
|
|
119
|
+
### 2. SPECIFY
|
|
120
|
+
|
|
121
|
+
- **Contracts FIRST:** Write `@pre`/`@post` before implementation
|
|
122
|
+
- **Doctests:** Add examples for expected behavior
|
|
123
|
+
- **Design:** Decompose complex tasks into sub-functions
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
# SPECIFY before BUILD:
|
|
127
|
+
@pre(lambda x: x > 0)
|
|
128
|
+
@post(lambda result: result >= 0)
|
|
129
|
+
def calculate(x: int) -> int:
|
|
130
|
+
"""
|
|
131
|
+
>>> calculate(10)
|
|
132
|
+
100
|
|
133
|
+
"""
|
|
134
|
+
... # Implementation comes in BUILD
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 3. BUILD
|
|
138
|
+
|
|
139
|
+
- Follow the contracts written in SPECIFY
|
|
140
|
+
{% if syntax == "mcp" -%}
|
|
141
|
+
- Run `invar_guard(changed=true)` frequently
|
|
142
|
+
{% else -%}
|
|
143
|
+
- Run `invar guard --changed` frequently
|
|
144
|
+
{% endif -%}
|
|
145
|
+
- Commit after each logical unit
|
|
146
|
+
|
|
147
|
+
### 4. VALIDATE
|
|
148
|
+
|
|
149
|
+
{% if syntax == "mcp" -%}
|
|
150
|
+
- Run `invar_guard()` (full verification)
|
|
151
|
+
{% else -%}
|
|
152
|
+
- Run `invar guard` (full verification)
|
|
153
|
+
{% endif -%}
|
|
154
|
+
- Integration works (if applicable)
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Tool Selection
|
|
159
|
+
|
|
160
|
+
| I want to... | Use |
|
|
161
|
+
|--------------|-----|
|
|
162
|
+
{% if syntax == "mcp" -%}
|
|
163
|
+
| See contracts | `invar_sig(target="<file>")` |
|
|
164
|
+
| Find entry points | `invar_map(top=10)` |
|
|
165
|
+
| Verify code | `invar_guard()` |
|
|
166
|
+
{% else -%}
|
|
167
|
+
| See contracts | `invar sig <file>` |
|
|
168
|
+
| Find entry points | `invar map --top 10` |
|
|
169
|
+
| Verify code | `invar guard` |
|
|
170
|
+
{% endif -%}
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Task Completion
|
|
175
|
+
|
|
176
|
+
A task is complete only when ALL conditions are met:
|
|
177
|
+
- Check-In displayed: `✓ Check-In: [project] | [branch] | [clean/dirty]`
|
|
178
|
+
- Intent explicitly stated
|
|
179
|
+
- Contract written before implementation
|
|
180
|
+
- Final displayed: `✓ Final: guard PASS | <errors>, <warnings>`
|
|
181
|
+
- User requirement satisfied
|
|
182
|
+
|
|
183
|
+
**Missing any = Task incomplete.**
|
|
184
|
+
|
|
185
|
+
<!--/invar:managed-->
|
|
186
|
+
<!--invar:project-->
|
|
187
|
+
<!--/invar:project-->
|
|
188
|
+
<!--invar:user-->
|
|
189
|
+
<!-- ========================================================================
|
|
190
|
+
USER REGION - EDITABLE
|
|
191
|
+
Add your team conventions and project-specific rules below.
|
|
192
|
+
This section is preserved across `invar update` and `invar dev sync`.
|
|
193
|
+
======================================================================== -->
|
|
194
|
+
<!--/invar:user-->
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
*Generated by `invar init` v{{ version }}. Customize the user section freely.*
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invar Pi Hook
|
|
3
|
+
* Protocol: v{{ protocol_version }} | Generated: {{ generated_date }}
|
|
4
|
+
* LX-04: Full feature parity with Claude Code hooks
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - pytest/crosshair blocking via tool_call
|
|
8
|
+
* - Protocol injection via pi.send() for long conversations
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
|
|
12
|
+
|
|
13
|
+
// Blocked commands (same as Claude Code)
|
|
14
|
+
const BLOCKED_CMDS = [/^pytest\b/, /^python\s+-m\s+pytest/, /^crosshair\b/];
|
|
15
|
+
const ALLOWED_FLAGS = [/--pdb/, /--cov/, /--debug/];
|
|
16
|
+
|
|
17
|
+
// Protocol content for injection (escaped for JS)
|
|
18
|
+
const INVAR_PROTOCOL = `{{ invar_protocol_escaped }}`;
|
|
19
|
+
|
|
20
|
+
export default function (pi: HookAPI) {
|
|
21
|
+
let msgCount = 0;
|
|
22
|
+
|
|
23
|
+
// ============================================
|
|
24
|
+
// Session Management
|
|
25
|
+
// ============================================
|
|
26
|
+
pi.on("session", async (event) => {
|
|
27
|
+
// Reset count on session start/restore
|
|
28
|
+
if (event.reason === "start" || event.reason === "branch") {
|
|
29
|
+
msgCount = 0;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// ============================================
|
|
34
|
+
// Long Conversation Protocol Refresh
|
|
35
|
+
// ============================================
|
|
36
|
+
pi.on("agent_start", async () => {
|
|
37
|
+
msgCount++;
|
|
38
|
+
|
|
39
|
+
// Message 15: Lightweight checkpoint
|
|
40
|
+
if (msgCount === 15) {
|
|
41
|
+
pi.send(
|
|
42
|
+
"<system-reminder>Checkpoint: guard=verify, sig=contracts, USBV workflow.</system-reminder>"
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Message 25+: Full protocol injection every 10 messages
|
|
47
|
+
if (msgCount >= 25 && msgCount % 10 === 0) {
|
|
48
|
+
pi.send(`<system-reminder>
|
|
49
|
+
=== Protocol Refresh (message ${msgCount}) ===
|
|
50
|
+
${INVAR_PROTOCOL}
|
|
51
|
+
</system-reminder>`);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ============================================
|
|
56
|
+
// pytest/crosshair Blocking
|
|
57
|
+
// ============================================
|
|
58
|
+
pi.on("tool_call", async (event) => {
|
|
59
|
+
if (event.toolName !== "bash") return;
|
|
60
|
+
const cmd = ((event.input as Record<string, unknown>).command as string || "").trim();
|
|
61
|
+
|
|
62
|
+
// Skip if not a blocked command
|
|
63
|
+
if (!BLOCKED_CMDS.some((p) => p.test(cmd))) return;
|
|
64
|
+
|
|
65
|
+
// Allow if has debug/test flags
|
|
66
|
+
if (ALLOWED_FLAGS.some((p) => p.test(cmd))) return;
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
block: true,
|
|
70
|
+
reason: "Use `{{ guard_cmd }}` instead of pytest/crosshair.",
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
}
|
invar/templates/manifest.toml
CHANGED
|
@@ -54,6 +54,7 @@ extensions = { action = "preserve" }
|
|
|
54
54
|
|
|
55
55
|
# Config files (Jinja2 templates)
|
|
56
56
|
"CLAUDE.md" = { src = "config/CLAUDE.md.jinja", type = "jinja" }
|
|
57
|
+
"AGENT.md" = { src = "config/AGENT.md.jinja", type = "jinja" }
|
|
57
58
|
".invar/context.md" = { src = "config/context.md.jinja", type = "jinja" }
|
|
58
59
|
".pre-commit-config.yaml" = { src = "config/pre-commit.yaml.jinja", type = "jinja" }
|
|
59
60
|
|
|
@@ -10,9 +10,49 @@ _invar:
|
|
|
10
10
|
# Development Mode
|
|
11
11
|
|
|
12
12
|
> **Purpose:** Implement solution following USBV workflow with verification.
|
|
13
|
+
> **Mindset:** CONTRACTS before code — no exceptions.
|
|
14
|
+
|
|
15
|
+
## Scope Boundaries
|
|
16
|
+
|
|
17
|
+
**This skill IS for:**
|
|
18
|
+
- Implementing features ("add", "create", "build")
|
|
19
|
+
- Fixing bugs ("fix", "resolve")
|
|
20
|
+
- Modifying existing code ("update", "change")
|
|
21
|
+
- Writing tests and contracts
|
|
22
|
+
|
|
23
|
+
**This skill is NOT for:**
|
|
24
|
+
- Exploring unclear requirements → switch to `/investigate`
|
|
25
|
+
- Choosing between approaches → switch to `/propose`
|
|
26
|
+
- Reviewing completed work → switch to `/review`
|
|
27
|
+
|
|
28
|
+
**Drift detection:** If requirements are unclear → STOP, exit to `/investigate` first.
|
|
13
29
|
|
|
14
30
|
## Entry Actions (REQUIRED)
|
|
15
31
|
|
|
32
|
+
### Session Restore (if continuing from summary)
|
|
33
|
+
|
|
34
|
+
When conversation begins with a previous session summary:
|
|
35
|
+
|
|
36
|
+
1. **ALWAYS display Check-In first** — even when continuing
|
|
37
|
+
2. **Determine current phase** from todo items:
|
|
38
|
+
| Todo keywords | Phase |
|
|
39
|
+
|---------------|-------|
|
|
40
|
+
| "research", "understand", "analyze" | UNDERSTAND |
|
|
41
|
+
| "contract", "design", "specify" | SPECIFY |
|
|
42
|
+
| "implement", "code", "build" | BUILD |
|
|
43
|
+
| "verify", "test", "guard" | VALIDATE |
|
|
44
|
+
3. **Display phase header** before resuming work
|
|
45
|
+
4. **Re-read context.md** for project state
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
# Example session restore:
|
|
49
|
+
✓ Check-In: Invar | Main | dirty
|
|
50
|
+
|
|
51
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
52
|
+
📍 /develop → BUILD (3/4) [resumed]
|
|
53
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
54
|
+
```
|
|
55
|
+
|
|
16
56
|
### Context Refresh (DX-54)
|
|
17
57
|
|
|
18
58
|
Before any workflow action:
|
|
@@ -123,6 +163,25 @@ If any NO → Stop. Write contract first.
|
|
|
123
163
|
|
|
124
164
|
### 3. BUILD
|
|
125
165
|
|
|
166
|
+
#### New Function Gate (MANDATORY)
|
|
167
|
+
|
|
168
|
+
**Before writing ANY new Core function, STOP and verify:**
|
|
169
|
+
|
|
170
|
+
| Check | If NO → Action |
|
|
171
|
+
|-------|----------------|
|
|
172
|
+
| Contract shown in SPECIFY phase? | ⛔ STOP. Return to SPECIFY. |
|
|
173
|
+
| Doctest written? | ⛔ STOP. Write doctest first. |
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
⛔ GATE VIOLATION: Writing new function without prior contract.
|
|
177
|
+
→ Return to SPECIFY phase. Show contract first.
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Exempt from gate:**
|
|
181
|
+
- Shell functions (no @pre/@post required)
|
|
182
|
+
- Editing existing functions (contract already exists)
|
|
183
|
+
- Non-Python files
|
|
184
|
+
|
|
126
185
|
**For complex tasks:** Enter Plan Mode first, get user approval.
|
|
127
186
|
|
|
128
187
|
**Implementation rules:**
|
|
@@ -11,6 +11,21 @@ _invar:
|
|
|
11
11
|
|
|
12
12
|
> **Purpose:** Understand before acting. Gather information, analyze code, report findings.
|
|
13
13
|
|
|
14
|
+
## Scope Boundaries
|
|
15
|
+
|
|
16
|
+
**This skill IS for:**
|
|
17
|
+
- Understanding vague or unclear tasks
|
|
18
|
+
- Analyzing existing code and architecture
|
|
19
|
+
- Researching before implementation
|
|
20
|
+
- Answering "why", "what", "how does" questions
|
|
21
|
+
|
|
22
|
+
**This skill is NOT for:**
|
|
23
|
+
- Writing or modifying code → switch to `/develop`
|
|
24
|
+
- Making design decisions → switch to `/propose`
|
|
25
|
+
- Reviewing code quality → switch to `/review`
|
|
26
|
+
|
|
27
|
+
**Drift detection:** If you find yourself wanting to edit files → STOP, exit to `/develop`.
|
|
28
|
+
|
|
14
29
|
## Constraints
|
|
15
30
|
|
|
16
31
|
**FORBIDDEN in this phase:**
|
|
@@ -10,6 +10,39 @@ _invar:
|
|
|
10
10
|
# Proposal Mode
|
|
11
11
|
|
|
12
12
|
> **Purpose:** Facilitate human decision-making with clear options and trade-offs.
|
|
13
|
+
> **Mindset:** OPTIONS, not decisions — human chooses.
|
|
14
|
+
|
|
15
|
+
## Scope Boundaries
|
|
16
|
+
|
|
17
|
+
**This skill IS for:**
|
|
18
|
+
- Presenting design choices with trade-offs
|
|
19
|
+
- Facilitating architectural decisions
|
|
20
|
+
- Comparing approaches (A vs B)
|
|
21
|
+
- Creating formal proposals for complex decisions
|
|
22
|
+
|
|
23
|
+
**This skill is NOT for:**
|
|
24
|
+
- Implementing the chosen option → switch to `/develop`
|
|
25
|
+
- Researching to understand the problem → switch to `/investigate`
|
|
26
|
+
- Reviewing existing code → switch to `/review`
|
|
27
|
+
|
|
28
|
+
**Drift detection:** If you find yourself writing implementation code → STOP, wait for user choice, then exit to `/develop`.
|
|
29
|
+
|
|
30
|
+
## Constraints
|
|
31
|
+
|
|
32
|
+
**FORBIDDEN in this phase:**
|
|
33
|
+
- Writing implementation code (beyond examples)
|
|
34
|
+
- Making decisions for the user
|
|
35
|
+
- Creating files other than proposals
|
|
36
|
+
- Committing changes
|
|
37
|
+
|
|
38
|
+
**ALLOWED:**
|
|
39
|
+
- Read, Glob, Grep (research for options)
|
|
40
|
+
{% if syntax == "mcp" -%}
|
|
41
|
+
- invar_sig, invar_map (understand current state)
|
|
42
|
+
{% else -%}
|
|
43
|
+
- invar sig, invar map (understand current state)
|
|
44
|
+
{% endif -%}
|
|
45
|
+
- Creating proposal documents in `docs/proposals/`
|
|
13
46
|
|
|
14
47
|
## Entry Actions
|
|
15
48
|
|
|
@@ -14,6 +14,21 @@ _invar:
|
|
|
14
14
|
> **Success Metric:** Issues FOUND, not code approved. Zero issues = you failed to look hard enough.
|
|
15
15
|
> **Workflow:** AUTOMATIC Reviewer↔Fixer loop until quality_met or max_rounds (no human confirmation).
|
|
16
16
|
|
|
17
|
+
## Scope Boundaries
|
|
18
|
+
|
|
19
|
+
**This skill IS for:**
|
|
20
|
+
- Finding bugs and logic errors in existing code
|
|
21
|
+
- Verifying contract semantic value
|
|
22
|
+
- Auditing escape hatches
|
|
23
|
+
- Security review
|
|
24
|
+
|
|
25
|
+
**This skill is NOT for:**
|
|
26
|
+
- Implementing new features → switch to `/develop`
|
|
27
|
+
- Understanding how code works → switch to `/investigate`
|
|
28
|
+
- Deciding on architecture → switch to `/propose`
|
|
29
|
+
|
|
30
|
+
**Drift detection:** If you're writing significant new code (not fixes) → STOP, you're in wrong skill.
|
|
31
|
+
|
|
17
32
|
## Auto-Loop Configuration
|
|
18
33
|
|
|
19
34
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: invar-tools
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.0
|
|
4
4
|
Summary: AI-native software engineering tools with design-by-contract verification
|
|
5
5
|
Project-URL: Homepage, https://github.com/tefx/invar
|
|
6
6
|
Project-URL: Documentation, https://github.com/tefx/invar#readme
|
|
@@ -138,8 +138,9 @@ cd your-project
|
|
|
138
138
|
# Interactive mode - choose what to install
|
|
139
139
|
uvx invar-tools init
|
|
140
140
|
|
|
141
|
-
# Or quick setup
|
|
142
|
-
uvx invar-tools init --claude
|
|
141
|
+
# Or quick setup (skip prompts)
|
|
142
|
+
uvx invar-tools init --claude # Claude Code
|
|
143
|
+
uvx invar-tools init --pi # Pi Coding Agent
|
|
143
144
|
|
|
144
145
|
# Add runtime contracts to your project
|
|
145
146
|
pip install invar-runtime
|
|
@@ -369,10 +370,13 @@ AlphaCodium · Parsel · Reflexion · Clover
|
|
|
369
370
|
|
|
370
371
|
| Agent | Status | Setup |
|
|
371
372
|
|-------|--------|-------|
|
|
372
|
-
| **Claude Code** | ✅ Full | `invar init`
|
|
373
|
-
| **Pi
|
|
373
|
+
| **Claude Code** | ✅ Full | `invar init --claude` |
|
|
374
|
+
| **Pi** | ✅ Full | `invar init --pi` |
|
|
375
|
+
| **Cursor** | ✅ MCP | `invar init` → select Other, add MCP config |
|
|
374
376
|
| **Other** | 📝 Manual | `invar init` → select Other, include `AGENT.md` in prompt |
|
|
375
377
|
|
|
378
|
+
> **See also:** [Multi-Agent Guide](./docs/guides/multi-agent.md) for detailed integration instructions.
|
|
379
|
+
|
|
376
380
|
### Claude Code (Full Experience)
|
|
377
381
|
|
|
378
382
|
All features auto-configured:
|
|
@@ -381,13 +385,24 @@ All features auto-configured:
|
|
|
381
385
|
- Claude Code hooks (tool guidance, verification reminders)
|
|
382
386
|
- Pre-commit hooks
|
|
383
387
|
|
|
384
|
-
### Pi
|
|
388
|
+
### Pi (Full Support)
|
|
389
|
+
|
|
390
|
+
Pi reads CLAUDE.md and .claude/skills/ directly, sharing configuration with Claude Code:
|
|
391
|
+
- **Same instruction file** — CLAUDE.md (no separate AGENT.md needed)
|
|
392
|
+
- **Same workflow skills** — .claude/skills/ work in Pi
|
|
393
|
+
- **Pi-specific hooks** — .pi/hooks/invar.ts for pytest blocking and protocol refresh
|
|
394
|
+
- **Protocol injection** — Long conversation support via `pi.send()`
|
|
395
|
+
- Pre-commit hooks
|
|
396
|
+
|
|
397
|
+
### Cursor (MCP + Rules)
|
|
385
398
|
|
|
386
|
-
|
|
387
|
-
-
|
|
388
|
-
-
|
|
399
|
+
Cursor users get full verification via MCP:
|
|
400
|
+
- MCP tools (`invar_guard`, `invar_sig`, `invar_map`)
|
|
401
|
+
- .cursor/rules/ for USBV workflow guidance
|
|
402
|
+
- Hooks (beta) for pytest blocking
|
|
389
403
|
- Pre-commit hooks
|
|
390
|
-
|
|
404
|
+
|
|
405
|
+
> See [Cursor Guide](./docs/guides/cursor.md) for detailed setup.
|
|
391
406
|
|
|
392
407
|
### Other Editors (Manual)
|
|
393
408
|
|
|
@@ -27,6 +27,7 @@ invar/core/strategies.py,sha256=2DPl0z2p_CBNd4RlSbZzTeAy6Dq6cpCiBCB2p5qHHkk,8798
|
|
|
27
27
|
invar/core/suggestions.py,sha256=LCg2Dy9EHh_n1t9jATRZ0gTkgJkAEZk3vp2nuuCyr-s,15129
|
|
28
28
|
invar/core/sync_helpers.py,sha256=kd6VyFAcpKfkVcbDk3GaBi2n0EWOGICz4VmdxwbshfI,7523
|
|
29
29
|
invar/core/tautology.py,sha256=Pmn__a0Bt55W0lAQo1G5q8Ory9KuE23dRknKw45xxbs,9221
|
|
30
|
+
invar/core/template_helpers.py,sha256=E1UT7ct0DaUFlfHr9oTBvW4xfxAiS81rbmZHSucPw4c,881
|
|
30
31
|
invar/core/template_parser.py,sha256=vH3H8OX55scZ1hWh3xoA8oJMhgleKufCOhkTvsSuu_4,14730
|
|
31
32
|
invar/core/timeout_inference.py,sha256=BS2fJGmwOrLpYZUku4qrizgNDSIXVLFBslW-6sRAvpc,3451
|
|
32
33
|
invar/core/trivial_detection.py,sha256=KYP8jJb7QDeusAxFdX5NAML_H0NL5wLgMeBWDQmNqfU,6086
|
|
@@ -56,6 +57,7 @@ invar/shell/guard_output.py,sha256=v3gG5P-_47nIFo8eAMKwdA_hLf2KZ0cQ-45Z6JjKp4w,1
|
|
|
56
57
|
invar/shell/mcp_config.py,sha256=-hC7Y5BGuVs285b6gBARk7ZyzVxHwPgXSyt_GoN0jfs,4580
|
|
57
58
|
invar/shell/mutation.py,sha256=Lfyk2b8j8-hxAq-iwAgQeOhr7Ci6c5tRF1TXe3CxQCs,8914
|
|
58
59
|
invar/shell/pattern_integration.py,sha256=pRcjfq3NvMW_tvQCnaXZnD1k5AVEWK8CYOE2jN6VTro,7842
|
|
60
|
+
invar/shell/pi_hooks.py,sha256=ln71f6SkjQ_7pMt6_udzeqgRaRIk3Y11XobLURBP2v0,6597
|
|
59
61
|
invar/shell/property_tests.py,sha256=N9JreyH5PqR89oF5yLcX7ZAV-Koyg5BKo-J05-GUPsA,9109
|
|
60
62
|
invar/shell/subprocess_env.py,sha256=9oXl3eMEbzLsFEgMHqobEw6oW_wV0qMEP7pklwm58Pw,11453
|
|
61
63
|
invar/shell/template_engine.py,sha256=IzOiGsKVFo0lDUdtg27wMzIJJKToclv151RDZuDnHHo,11027
|
|
@@ -64,14 +66,14 @@ invar/shell/testing.py,sha256=rTNBH0Okh2qtG9ohSXOz487baQ2gXrWT3s_WECW3HJs,11143
|
|
|
64
66
|
invar/shell/commands/__init__.py,sha256=MEkKwVyjI9DmkvBpJcuumXo2Pg_FFkfEr-Rr3nrAt7A,284
|
|
65
67
|
invar/shell/commands/guard.py,sha256=vDBGOFb9mQ1D8eXrMvQB505GpjO1XLeCLrv2ig9-6dU,21718
|
|
66
68
|
invar/shell/commands/hooks.py,sha256=W-SOnT4VQyUvXwipozkJwgEYfiOJGz7wksrbcdWegUg,2356
|
|
67
|
-
invar/shell/commands/init.py,sha256=
|
|
69
|
+
invar/shell/commands/init.py,sha256=ASl01hIYW3Dt_d5XhYPwDfdRujbKvBGfK8B9Sf4eXq0,15117
|
|
68
70
|
invar/shell/commands/merge.py,sha256=nuvKo8m32-OL-SCQlS4SLKmOZxQ3qj-1nGCx1Pgzifw,8183
|
|
69
71
|
invar/shell/commands/mutate.py,sha256=GwemiO6LlbGCBEQsBFnzZuKhF-wIMEl79GAMnKUWc8U,5765
|
|
70
72
|
invar/shell/commands/perception.py,sha256=TyH_HpqyKkmE3-zcU4YyBG8ghwJaSFeRC-OQMVBDTbQ,3837
|
|
71
73
|
invar/shell/commands/sync_self.py,sha256=nmqBry7V2_enKwy2zzHg8UoedZNicLe3yKDhjmBeZ68,3880
|
|
72
74
|
invar/shell/commands/template_sync.py,sha256=wVZ-UvJ1wpN2UBcWMfbei0n46XHYx-zRbMA2oX6FSi4,13723
|
|
73
75
|
invar/shell/commands/test.py,sha256=goMf-ovvzEyWQMheq4YlJ-mwK5-w3lDj0cq0IA_1-_c,4205
|
|
74
|
-
invar/shell/commands/uninstall.py,sha256=
|
|
76
|
+
invar/shell/commands/uninstall.py,sha256=Q2tDbGLUf0PC2xxWqmuBYwfxX7IuSQ_SmyxvwYcDyPo,18102
|
|
75
77
|
invar/shell/commands/update.py,sha256=0V5F8vxQ6PHPHPVYDmxdRD7xXeQEFypiJMYpY5ryiek,1349
|
|
76
78
|
invar/shell/prove/__init__.py,sha256=ZqlbmyMFJf6yAle8634jFuPRv8wNvHps8loMlOJyf8A,240
|
|
77
79
|
invar/shell/prove/accept.py,sha256=cnY_6jzU1EBnpLF8-zWUWcXiSXtCwxPsXEYXsSVPG38,3717
|
|
@@ -81,10 +83,11 @@ invar/shell/prove/hypothesis.py,sha256=QUclOOUg_VB6wbmHw8O2EPiL5qBOeBRqQeM04AVuL
|
|
|
81
83
|
invar/templates/CLAUDE.md.template,sha256=eaGU3SyRO_NEifw5b26k3srgQH4jyeujjCJ-HbM36_w,4913
|
|
82
84
|
invar/templates/__init__.py,sha256=cb3ht8KPK5oBn5oG6HsTznujmo9WriJ_P--fVxJwycc,45
|
|
83
85
|
invar/templates/context.md.template,sha256=FKyI1ghpqcf4wftyv9-auIFHor8Nm8lETN45Ja-L8Og,2386
|
|
84
|
-
invar/templates/manifest.toml,sha256=
|
|
86
|
+
invar/templates/manifest.toml,sha256=afovCokbqEh0nyDGdIp1LqTUslJdC8T2HY6MV-NvegY,4331
|
|
85
87
|
invar/templates/proposal.md.template,sha256=UP7SpQ7gk8jVlHGLQCSQ5c-kCj1DBQEz8M-vEStK77I,1573
|
|
86
88
|
invar/templates/commands/audit.md,sha256=OrotO8420zTKnlNyAyL1Eos0VIaihzEU4AHdfDv68Oc,4162
|
|
87
89
|
invar/templates/commands/guard.md,sha256=N_C_AXd9kI85W1B0aTEycjiDp_jdaP8eeq8O0FQ_WQ8,1227
|
|
90
|
+
invar/templates/config/AGENT.md.jinja,sha256=tadNeX5G_XLLdbLiG1JSQm_Xjmt1kAJ3IrTaw_sDU9g,5275
|
|
88
91
|
invar/templates/config/CLAUDE.md.jinja,sha256=VbtDWxn3H8qiE9-DV1hlG3DJ-GcBQU4ZiUHbFh6Bxxk,7814
|
|
89
92
|
invar/templates/config/context.md.jinja,sha256=_kJ8erEQNJMLDCKrv4BXWkO6OaGzE-zW9biCf7144aY,3103
|
|
90
93
|
invar/templates/config/pre-commit.yaml.jinja,sha256=nUPxLxkTHAgZwhFAuOMDbZ8v0NQV9FlQPbr2MDEOsoA,1778
|
|
@@ -98,15 +101,16 @@ invar/templates/hooks/PreToolUse.sh.jinja,sha256=D39PaT1eFSjz_Av16xK1atoBZbhLI8t
|
|
|
98
101
|
invar/templates/hooks/Stop.sh.jinja,sha256=3S6lLeAGIu5aPQVRz4jjFS9AfjCD9DdS_jagmkw-x8Q,960
|
|
99
102
|
invar/templates/hooks/UserPromptSubmit.sh.jinja,sha256=eAQqQ-XdOCyhLpF5_1r1z7C-Ej9GQ5Isqbu_2LAtsno,2302
|
|
100
103
|
invar/templates/hooks/__init__.py,sha256=RnnMoQA-8eqbr8Y_1Vu9B8h5vAz4C-vmo8wgdcGYrz0,43
|
|
104
|
+
invar/templates/hooks/pi/invar.ts.jinja,sha256=D1TRxHuNkmjhICPOxjcyoRUZryyz7MpfIvBLjm-krjA,2234
|
|
101
105
|
invar/templates/protocol/INVAR.md,sha256=ppQhb_-R5YaXAqW1WDMOcXptx-CrAQI_xYxld7YljK8,9998
|
|
102
|
-
invar/templates/skills/develop/SKILL.md.jinja,sha256=
|
|
103
|
-
invar/templates/skills/investigate/SKILL.md.jinja,sha256=
|
|
104
|
-
invar/templates/skills/propose/SKILL.md.jinja,sha256=
|
|
105
|
-
invar/templates/skills/review/SKILL.md.jinja,sha256=
|
|
106
|
-
invar_tools-1.
|
|
107
|
-
invar_tools-1.
|
|
108
|
-
invar_tools-1.
|
|
109
|
-
invar_tools-1.
|
|
110
|
-
invar_tools-1.
|
|
111
|
-
invar_tools-1.
|
|
112
|
-
invar_tools-1.
|
|
106
|
+
invar/templates/skills/develop/SKILL.md.jinja,sha256=kCKXTgHtfqED__Udn3dM6OxG0FQZx1NsjFhHtDNExJA,12558
|
|
107
|
+
invar/templates/skills/investigate/SKILL.md.jinja,sha256=cp6TBEixBYh1rLeeHOR1yqEnFqv1NZYePORMnavLkQI,3231
|
|
108
|
+
invar/templates/skills/propose/SKILL.md.jinja,sha256=6BuKiCqO1AEu3VtzMHy1QWGqr_xqG9eJlhbsKT4jev4,3463
|
|
109
|
+
invar/templates/skills/review/SKILL.md.jinja,sha256=OvKoomS4MJHYbTuWSZjmeS_q_Wh6uNHzjXaewj6ELEg,14596
|
|
110
|
+
invar_tools-1.8.0.dist-info/METADATA,sha256=51-iej0SZ6ramV9uMvwlxBUZYbXTQAUc1Mi5GHJGvDw,18295
|
|
111
|
+
invar_tools-1.8.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
112
|
+
invar_tools-1.8.0.dist-info/entry_points.txt,sha256=RwH_EhqgtFPsnO6RcrwrAb70Zyfb8Mh6uUtztWnUxGk,102
|
|
113
|
+
invar_tools-1.8.0.dist-info/licenses/LICENSE,sha256=qeFksp4H4kfTgQxPCIu3OdagXyiZcgBlVfsQ6M5oFyk,10767
|
|
114
|
+
invar_tools-1.8.0.dist-info/licenses/LICENSE-GPL,sha256=IvZfC6ZbP7CLjytoHVzvpDZpD-Z3R_qa1GdMdWlWQ6Q,35157
|
|
115
|
+
invar_tools-1.8.0.dist-info/licenses/NOTICE,sha256=joEyMyFhFY8Vd8tTJ-a3SirI0m2Sd0WjzqYt3sdcglc,2561
|
|
116
|
+
invar_tools-1.8.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|