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.
@@ -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
@@ -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: use defaults
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 tip for Claude users
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():
@@ -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
+ }
@@ -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.7.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 for Claude Code (skip prompts)
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` → select Claude Code |
373
- | **Pi / Cursor** | 🚧 In progress | `invar init` → select Other, include `AGENT.md` in prompt |
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 / Cursor (In Progress)
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
- Currently available:
387
- - Protocol document (INVAR.md)
388
- - CLI verification (`invar guard`)
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
- - MCP server (manual configuration)
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=wAPPIDpoJyNOhrD09LldRg8w76mWv0r5qTcqlPWc920,13411
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=u1fqE2jCod2KxFMEYbtC4UFK1zuxzoaQKQzZdAjQcDE,17368
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=cEe7yEOOeaLmOF-PrwZXxiPGjHhsSJYkWBKRHDmSbac,4268
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=3coPSZGh1-YKN9Xc_xcEkfEP3S0XiFMMGF0hJZEaAx8,10562
103
- invar/templates/skills/investigate/SKILL.md.jinja,sha256=bOLdLMH5WUVBYOo4NpsfyvI6xx7I1lCNr_X-8bMe_kg,2744
104
- invar/templates/skills/propose/SKILL.md.jinja,sha256=_iDLYN6-cfzA8n0_8sv-Dnpm1xq9IIpcDyM10mU2WUA,2420
105
- invar/templates/skills/review/SKILL.md.jinja,sha256=e7HULz1jjLOlk2LYejQMk2F-cu7dDIwvh6lWNjx3j-Q,14123
106
- invar_tools-1.7.1.dist-info/METADATA,sha256=dBk4N2qRISfYrc6Rr66eY08_me9Rqfyq34RDBCaEj1s,17600
107
- invar_tools-1.7.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
108
- invar_tools-1.7.1.dist-info/entry_points.txt,sha256=RwH_EhqgtFPsnO6RcrwrAb70Zyfb8Mh6uUtztWnUxGk,102
109
- invar_tools-1.7.1.dist-info/licenses/LICENSE,sha256=qeFksp4H4kfTgQxPCIu3OdagXyiZcgBlVfsQ6M5oFyk,10767
110
- invar_tools-1.7.1.dist-info/licenses/LICENSE-GPL,sha256=IvZfC6ZbP7CLjytoHVzvpDZpD-Z3R_qa1GdMdWlWQ6Q,35157
111
- invar_tools-1.7.1.dist-info/licenses/NOTICE,sha256=joEyMyFhFY8Vd8tTJ-a3SirI0m2Sd0WjzqYt3sdcglc,2561
112
- invar_tools-1.7.1.dist-info/RECORD,,
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,,