invar-tools 1.12.0__py3-none-any.whl → 1.15.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/feedback.py +110 -0
- invar/shell/claude_hooks.py +49 -0
- invar/shell/commands/feedback.py +258 -0
- invar/shell/commands/guard.py +9 -1
- invar/shell/commands/init.py +118 -38
- invar/shell/pi_tools.py +120 -0
- invar/templates/claude-md/universal/tool-selection.md +110 -0
- invar/templates/config/CLAUDE.md.jinja +2 -0
- invar/templates/hooks/UserPromptSubmit.sh.jinja +31 -0
- invar/templates/hooks/pi/invar.ts.jinja +35 -1
- invar/templates/manifest.toml +8 -0
- invar/templates/pi-tools/invar/index.ts +207 -0
- invar/templates/protocol/python/tools.md +5 -2
- invar/templates/protocol/typescript/tools.md +5 -2
- invar/templates/skills/invar-reflect/CONFIG.md +480 -0
- invar/templates/skills/invar-reflect/SKILL.md +466 -0
- invar/templates/skills/invar-reflect/template.md +343 -0
- {invar_tools-1.12.0.dist-info → invar_tools-1.15.0.dist-info}/METADATA +6 -3
- {invar_tools-1.12.0.dist-info → invar_tools-1.15.0.dist-info}/RECORD +24 -16
- {invar_tools-1.12.0.dist-info → invar_tools-1.15.0.dist-info}/WHEEL +0 -0
- {invar_tools-1.12.0.dist-info → invar_tools-1.15.0.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.12.0.dist-info → invar_tools-1.15.0.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.12.0.dist-info → invar_tools-1.15.0.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.12.0.dist-info → invar_tools-1.15.0.dist-info}/licenses/NOTICE +0 -0
invar/shell/commands/init.py
CHANGED
|
@@ -16,13 +16,14 @@ from rich.console import Console
|
|
|
16
16
|
from rich.panel import Panel
|
|
17
17
|
|
|
18
18
|
from invar.core.sync_helpers import VALID_LANGUAGES, SyncConfig
|
|
19
|
-
from invar.shell.claude_hooks import install_claude_hooks
|
|
19
|
+
from invar.shell.claude_hooks import add_feedback_config, install_claude_hooks
|
|
20
20
|
from invar.shell.commands.template_sync import sync_templates
|
|
21
21
|
from invar.shell.mcp_config import (
|
|
22
22
|
generate_mcp_json,
|
|
23
23
|
get_recommended_method,
|
|
24
24
|
)
|
|
25
25
|
from invar.shell.pi_hooks import install_pi_hooks
|
|
26
|
+
from invar.shell.pi_tools import install_pi_tools
|
|
26
27
|
from invar.shell.templates import (
|
|
27
28
|
add_config,
|
|
28
29
|
create_directories,
|
|
@@ -60,6 +61,7 @@ FILE_CATEGORIES: dict[str, list[tuple[str, str]]] = {
|
|
|
60
61
|
("CLAUDE.md", "Agent instructions (Pi compatible)"),
|
|
61
62
|
(".claude/skills/", "Workflow automation (Pi compatible)"),
|
|
62
63
|
(".pi/hooks/", "Pi-specific hooks"),
|
|
64
|
+
(".pi/tools/", "Pi custom tools (invar_guard, invar_sig, invar_map)"),
|
|
63
65
|
],
|
|
64
66
|
}
|
|
65
67
|
|
|
@@ -153,29 +155,43 @@ def _get_prompt_style():
|
|
|
153
155
|
|
|
154
156
|
# @shell_complexity: Interactive prompt with cursor selection
|
|
155
157
|
def _prompt_agent_selection() -> list[str]:
|
|
156
|
-
"""Prompt user to select
|
|
158
|
+
"""Prompt user to select agent(s) using checkbox (DX-81: multi-agent support)."""
|
|
157
159
|
import questionary
|
|
158
160
|
|
|
159
|
-
console.print("\n[bold]Select
|
|
160
|
-
console.print("[dim]
|
|
161
|
+
console.print("\n[bold]Select agent(s) to configure:[/bold]")
|
|
162
|
+
console.print("[dim]Space to toggle, Enter to confirm (can select multiple)[/dim]\n")
|
|
161
163
|
|
|
162
164
|
choices = [
|
|
163
|
-
questionary.Choice(
|
|
164
|
-
|
|
165
|
-
|
|
165
|
+
questionary.Choice(
|
|
166
|
+
"Claude Code (recommended)",
|
|
167
|
+
value="claude",
|
|
168
|
+
checked=True # Default selection
|
|
169
|
+
),
|
|
170
|
+
questionary.Choice(
|
|
171
|
+
"Pi Coding Agent",
|
|
172
|
+
value="pi",
|
|
173
|
+
checked=False
|
|
174
|
+
),
|
|
175
|
+
questionary.Choice(
|
|
176
|
+
"Other (AGENT.md)",
|
|
177
|
+
value="generic",
|
|
178
|
+
checked=False
|
|
179
|
+
),
|
|
166
180
|
]
|
|
167
181
|
|
|
168
|
-
selected = questionary.
|
|
182
|
+
selected = questionary.checkbox(
|
|
169
183
|
"",
|
|
170
184
|
choices=choices,
|
|
171
185
|
instruction="",
|
|
172
186
|
style=_get_prompt_style(),
|
|
173
187
|
).ask()
|
|
174
188
|
|
|
175
|
-
# Handle Ctrl+C
|
|
189
|
+
# Handle Ctrl+C or empty selection
|
|
176
190
|
if not selected:
|
|
177
|
-
|
|
178
|
-
|
|
191
|
+
console.print("[yellow]No agents selected, using Claude Code as default.[/yellow]")
|
|
192
|
+
return ["claude"]
|
|
193
|
+
|
|
194
|
+
return selected
|
|
179
195
|
|
|
180
196
|
|
|
181
197
|
# @shell_complexity: Interactive file selection with cursor navigation
|
|
@@ -205,9 +221,10 @@ def _prompt_file_selection(agents: list[str]) -> dict[str, bool]:
|
|
|
205
221
|
console.print()
|
|
206
222
|
console.print("[dim]Use arrow keys to move, space to toggle, enter to confirm[/dim]\n")
|
|
207
223
|
|
|
208
|
-
# Build choices with categories as separators
|
|
224
|
+
# Build choices with categories as separators (DX-81: deduplicate shared files)
|
|
209
225
|
choices: list[questionary.Choice | questionary.Separator] = []
|
|
210
226
|
file_list: list[str] = []
|
|
227
|
+
seen_files: set[str] = set()
|
|
211
228
|
|
|
212
229
|
for category, files in available.items():
|
|
213
230
|
if category == "required":
|
|
@@ -217,12 +234,19 @@ def _prompt_file_selection(agents: list[str]) -> dict[str, bool]:
|
|
|
217
234
|
category_name = "Claude Code"
|
|
218
235
|
elif category == "pi":
|
|
219
236
|
category_name = "Pi Coding Agent"
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
237
|
+
|
|
238
|
+
# Filter out files already seen (shared between categories)
|
|
239
|
+
unique_files = [(f, d) for f, d in files if f not in seen_files]
|
|
240
|
+
|
|
241
|
+
# Only add separator if there are unique files to show
|
|
242
|
+
if unique_files:
|
|
243
|
+
choices.append(questionary.Separator(f"── {category_name} ──"))
|
|
244
|
+
for file, desc in unique_files:
|
|
245
|
+
choices.append(
|
|
246
|
+
questionary.Choice(f"{file:28} {desc}", value=file, checked=True)
|
|
247
|
+
)
|
|
248
|
+
file_list.append(file)
|
|
249
|
+
seen_files.add(file)
|
|
226
250
|
|
|
227
251
|
selected = questionary.checkbox(
|
|
228
252
|
"Select files to install:",
|
|
@@ -239,6 +263,40 @@ def _prompt_file_selection(agents: list[str]) -> dict[str, bool]:
|
|
|
239
263
|
return {f: f in selected for f in file_list}
|
|
240
264
|
|
|
241
265
|
|
|
266
|
+
# @shell_complexity: Interactive consent prompt for feedback collection
|
|
267
|
+
def _prompt_feedback_consent() -> bool:
|
|
268
|
+
"""
|
|
269
|
+
Prompt user for consent to enable automatic feedback collection.
|
|
270
|
+
|
|
271
|
+
DX-79 Phase C: Opt-out consent flow (default: enabled).
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
True if user consents (or accepts default), False otherwise
|
|
275
|
+
"""
|
|
276
|
+
from rich import print as rprint
|
|
277
|
+
from rich.prompt import Confirm
|
|
278
|
+
|
|
279
|
+
rprint()
|
|
280
|
+
rprint("[bold]━" * 40)
|
|
281
|
+
rprint("[bold]📊 Usage Feedback (Optional)")
|
|
282
|
+
rprint("[bold]━" * 40)
|
|
283
|
+
rprint()
|
|
284
|
+
rprint("Invar can automatically reflect on tool usage to help improve")
|
|
285
|
+
rprint("the framework. Feedback is:")
|
|
286
|
+
rprint(" • Stored locally in [cyan].invar/feedback/[/cyan]")
|
|
287
|
+
rprint(" • Never sent automatically")
|
|
288
|
+
rprint(" • You decide what (if anything) to share")
|
|
289
|
+
rprint()
|
|
290
|
+
|
|
291
|
+
# Opt-out: default is True (Y)
|
|
292
|
+
consent = Confirm.ask(
|
|
293
|
+
"Enable automatic feedback collection?",
|
|
294
|
+
default=True,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
return consent
|
|
298
|
+
|
|
299
|
+
|
|
242
300
|
def _show_execution_output(
|
|
243
301
|
created: list[str],
|
|
244
302
|
merged: list[str],
|
|
@@ -356,10 +414,7 @@ def init(
|
|
|
356
414
|
"""
|
|
357
415
|
from invar import __version__
|
|
358
416
|
|
|
359
|
-
#
|
|
360
|
-
if claude and pi:
|
|
361
|
-
console.print("[red]Error:[/red] Cannot use --claude and --pi together.")
|
|
362
|
-
raise typer.Exit(1)
|
|
417
|
+
# DX-81: Multi-agent support - removed mutual exclusivity check
|
|
363
418
|
|
|
364
419
|
if mcp_only and (claude or pi):
|
|
365
420
|
console.print("[red]Error:[/red] --mcp-only cannot be combined with --claude or --pi.")
|
|
@@ -424,8 +479,10 @@ def init(
|
|
|
424
479
|
console.print(f"[red]Error:[/red] Invalid language '{language}'. Must be one of: {valid}")
|
|
425
480
|
raise typer.Exit(1)
|
|
426
481
|
|
|
427
|
-
# Header
|
|
428
|
-
if claude:
|
|
482
|
+
# Header (DX-81: Support multi-agent display)
|
|
483
|
+
if claude and pi:
|
|
484
|
+
console.print(f"\n[bold]Invar v{__version__} - Quick Setup (Claude Code + Pi)[/bold]")
|
|
485
|
+
elif claude:
|
|
429
486
|
console.print(f"\n[bold]Invar v{__version__} - Quick Setup (Claude Code)[/bold]")
|
|
430
487
|
elif pi:
|
|
431
488
|
console.print(f"\n[bold]Invar v{__version__} - Quick Setup (Pi)[/bold]")
|
|
@@ -434,21 +491,32 @@ def init(
|
|
|
434
491
|
console.print("=" * 45)
|
|
435
492
|
console.print(f"[dim]Language: {language} | Existing files will be MERGED.[/dim]")
|
|
436
493
|
|
|
437
|
-
# Determine agents and files
|
|
438
|
-
if claude:
|
|
439
|
-
# Quick mode:
|
|
440
|
-
agents = [
|
|
494
|
+
# DX-81: Determine agents and files (multi-agent support)
|
|
495
|
+
if claude or pi:
|
|
496
|
+
# Quick mode: Build agent list from flags
|
|
497
|
+
agents = []
|
|
498
|
+
if claude:
|
|
499
|
+
agents.append("claude")
|
|
500
|
+
if pi:
|
|
501
|
+
agents.append("pi")
|
|
502
|
+
|
|
503
|
+
# Build selected_files from all agents' categories
|
|
441
504
|
selected_files: dict[str, bool] = {}
|
|
442
|
-
for
|
|
443
|
-
|
|
444
|
-
selected_files[file] = True
|
|
445
|
-
elif pi:
|
|
446
|
-
# Quick mode: Pi defaults
|
|
447
|
-
agents = ["pi"]
|
|
448
|
-
selected_files = {}
|
|
449
|
-
for category in ["optional", "pi"]:
|
|
505
|
+
for agent in agents:
|
|
506
|
+
category = AGENT_CONFIGS[agent]["category"]
|
|
450
507
|
for file, _ in FILE_CATEGORIES.get(category, []):
|
|
451
508
|
selected_files[file] = True
|
|
509
|
+
|
|
510
|
+
# Add optional files
|
|
511
|
+
for file, _ in FILE_CATEGORIES["optional"]:
|
|
512
|
+
selected_files[file] = True
|
|
513
|
+
|
|
514
|
+
# DX-79: Default feedback enabled for quick mode
|
|
515
|
+
feedback_enabled = True
|
|
516
|
+
if len(agents) > 1:
|
|
517
|
+
console.print(f"\n[dim]📊 Configuring for {len(agents)} agents: {', '.join(agents)}[/dim]")
|
|
518
|
+
console.print("\n[dim]📊 Feedback collection enabled by default (stored locally in .invar/feedback/)[/dim]")
|
|
519
|
+
console.print("[dim] To disable: Set feedback.enabled=false in .claude/settings.local.json[/dim]")
|
|
452
520
|
else:
|
|
453
521
|
# Interactive mode
|
|
454
522
|
if not _is_interactive():
|
|
@@ -457,6 +525,8 @@ def init(
|
|
|
457
525
|
|
|
458
526
|
agents = _prompt_agent_selection()
|
|
459
527
|
selected_files = _prompt_file_selection(agents)
|
|
528
|
+
# DX-79: Prompt for feedback consent (opt-out, default: enabled)
|
|
529
|
+
feedback_enabled = _prompt_feedback_consent()
|
|
460
530
|
|
|
461
531
|
# Preview mode
|
|
462
532
|
if preview:
|
|
@@ -554,6 +624,16 @@ def init(
|
|
|
554
624
|
if "pi" in agents and selected_files.get(".pi/hooks/", True):
|
|
555
625
|
install_pi_hooks(path, console)
|
|
556
626
|
|
|
627
|
+
# Install Pi custom tools if selected
|
|
628
|
+
if "pi" in agents and selected_files.get(".pi/tools/", True):
|
|
629
|
+
install_pi_tools(path, console)
|
|
630
|
+
|
|
631
|
+
# Add feedback configuration (DX-79 Phase C)
|
|
632
|
+
if "claude" in agents or "pi" in agents:
|
|
633
|
+
feedback_result = add_feedback_config(path, feedback_enabled, console)
|
|
634
|
+
if isinstance(feedback_result, Failure):
|
|
635
|
+
console.print(f"[yellow]Warning:[/yellow] {feedback_result.failure()}")
|
|
636
|
+
|
|
557
637
|
# Create MCP setup guide
|
|
558
638
|
mcp_setup = invar_dir / "mcp-setup.md"
|
|
559
639
|
if not mcp_setup.exists():
|
|
@@ -572,7 +652,7 @@ def init(
|
|
|
572
652
|
# Completion message
|
|
573
653
|
console.print(f"\n[bold green]✓ Initialized Invar v{__version__}[/bold green]")
|
|
574
654
|
|
|
575
|
-
# Show agent-specific tips
|
|
655
|
+
# Show agent-specific tips (DX-81: show all relevant tips)
|
|
576
656
|
if "claude" in agents:
|
|
577
657
|
console.print()
|
|
578
658
|
console.print(
|
|
@@ -583,7 +663,7 @@ def init(
|
|
|
583
663
|
border_style="dim",
|
|
584
664
|
)
|
|
585
665
|
)
|
|
586
|
-
|
|
666
|
+
if "pi" in agents:
|
|
587
667
|
console.print()
|
|
588
668
|
console.print(
|
|
589
669
|
Panel(
|
invar/shell/pi_tools.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pi Coding Agent custom tools for Invar.
|
|
3
|
+
|
|
4
|
+
Provides Invar CLI commands as Pi custom tools for better LLM integration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import shutil
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from returns.result import Failure, Result, Success
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
|
|
18
|
+
# Pi tools directory
|
|
19
|
+
PI_TOOLS_DIR = ".pi/tools/invar"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_pi_tools_template_path() -> Path:
|
|
23
|
+
"""Get the path to Pi tools template."""
|
|
24
|
+
return Path(__file__).parent.parent / "templates" / "pi-tools" / "invar"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def install_pi_tools(
|
|
28
|
+
project_path: Path,
|
|
29
|
+
console: Console,
|
|
30
|
+
) -> Result[list[str], str]:
|
|
31
|
+
"""
|
|
32
|
+
Install Pi custom tools for Invar.
|
|
33
|
+
|
|
34
|
+
Creates .pi/tools/invar/index.ts with:
|
|
35
|
+
- invar_guard: Wrapper for invar guard command
|
|
36
|
+
- invar_sig: Wrapper for invar sig command
|
|
37
|
+
- invar_map: Wrapper for invar map command
|
|
38
|
+
"""
|
|
39
|
+
tools_dir = project_path / PI_TOOLS_DIR
|
|
40
|
+
tools_dir.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
|
|
42
|
+
console.print("\n[bold]Installing Pi custom tools...[/bold]")
|
|
43
|
+
console.print(" Tools provide:")
|
|
44
|
+
console.print(" ✓ invar_guard - Smart verification (static + doctests + symbolic)")
|
|
45
|
+
console.print(" ✓ invar_sig - Show function signatures and contracts")
|
|
46
|
+
console.print(" ✓ invar_map - Symbol map with reference counts")
|
|
47
|
+
console.print("")
|
|
48
|
+
|
|
49
|
+
template_path = get_pi_tools_template_path()
|
|
50
|
+
tool_file = template_path / "index.ts"
|
|
51
|
+
|
|
52
|
+
if not tool_file.exists():
|
|
53
|
+
return Failure(f"Template not found: {tool_file}")
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
# Copy the template file
|
|
57
|
+
dest_file = tools_dir / "index.ts"
|
|
58
|
+
shutil.copy2(tool_file, dest_file)
|
|
59
|
+
|
|
60
|
+
console.print(f" [green]Created[/green] {PI_TOOLS_DIR}/index.ts")
|
|
61
|
+
console.print("\n [bold green]✓ Pi custom tools installed[/bold green]")
|
|
62
|
+
console.print(" [dim]Pi will auto-discover tools in .pi/tools/[/dim]")
|
|
63
|
+
console.print(" [yellow]⚠ Restart Pi session for tools to take effect[/yellow]")
|
|
64
|
+
|
|
65
|
+
return Success(["index.ts"])
|
|
66
|
+
except Exception as e:
|
|
67
|
+
return Failure(f"Failed to install Pi tools: {e}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def remove_pi_tools(
|
|
71
|
+
project_path: Path,
|
|
72
|
+
console: Console,
|
|
73
|
+
) -> Result[None, str]:
|
|
74
|
+
"""Remove Pi custom tools."""
|
|
75
|
+
tools_dir = project_path / PI_TOOLS_DIR
|
|
76
|
+
tool_file = tools_dir / "index.ts"
|
|
77
|
+
|
|
78
|
+
if tool_file.exists():
|
|
79
|
+
tool_file.unlink()
|
|
80
|
+
console.print(f" [red]Removed[/red] {PI_TOOLS_DIR}/index.ts")
|
|
81
|
+
|
|
82
|
+
# Remove directory if empty
|
|
83
|
+
try:
|
|
84
|
+
tools_dir.rmdir()
|
|
85
|
+
console.print(f" [red]Removed[/red] {PI_TOOLS_DIR}/")
|
|
86
|
+
except OSError:
|
|
87
|
+
pass # Directory not empty, keep it
|
|
88
|
+
|
|
89
|
+
console.print("[bold green]✓ Pi custom tools removed[/bold green]")
|
|
90
|
+
else:
|
|
91
|
+
console.print("[dim]No Pi custom tools installed[/dim]")
|
|
92
|
+
|
|
93
|
+
return Success(None)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def pi_tools_status(
|
|
97
|
+
project_path: Path,
|
|
98
|
+
console: Console,
|
|
99
|
+
) -> Result[dict[str, str], str]:
|
|
100
|
+
"""Check status of Pi custom tools."""
|
|
101
|
+
tools_dir = project_path / PI_TOOLS_DIR
|
|
102
|
+
tool_file = tools_dir / "index.ts"
|
|
103
|
+
|
|
104
|
+
status: dict[str, str] = {}
|
|
105
|
+
|
|
106
|
+
if not tool_file.exists():
|
|
107
|
+
console.print("[dim]No Pi custom tools installed[/dim]")
|
|
108
|
+
return Success({"status": "not_installed"})
|
|
109
|
+
|
|
110
|
+
status["status"] = "installed"
|
|
111
|
+
|
|
112
|
+
# Try to check file size (basic validation)
|
|
113
|
+
try:
|
|
114
|
+
size = tool_file.stat().st_size
|
|
115
|
+
status["size"] = f"{size} bytes"
|
|
116
|
+
console.print(f"[green]✓ Pi custom tools installed[/green] ({size} bytes)")
|
|
117
|
+
except OSError:
|
|
118
|
+
console.print("[green]✓ Pi custom tools installed[/green]")
|
|
119
|
+
|
|
120
|
+
return Success(status)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
### Document Tools (DX-76)
|
|
2
|
+
|
|
3
|
+
| I want to... | Use |
|
|
4
|
+
|--------------|-----|
|
|
5
|
+
| View document structure | `{% if syntax == "mcp" %}invar_doc_toc(file="<file>"){% else %}invar doc toc <file> [--format text]{% endif %}` |
|
|
6
|
+
| Read specific section | `{% if syntax == "mcp" %}invar_doc_read(file="<file>", section="<section>"){% else %}invar doc read <file> <section>{% endif %}` |
|
|
7
|
+
| Search sections by title | `{% if syntax == "mcp" %}invar_doc_find(file="<file>", pattern="<pattern>"){% else %}invar doc find <pattern> <files...>{% endif %}` |
|
|
8
|
+
| Replace section content | `{% if syntax == "mcp" %}invar_doc_replace(file="<file>", section="<section>"){% else %}invar doc replace <file> <section>{% endif %}` |
|
|
9
|
+
| Insert new section | `{% if syntax == "mcp" %}invar_doc_insert(file="<file>", anchor="<anchor>"){% else %}invar doc insert <file> <anchor>{% endif %}` |
|
|
10
|
+
| Delete section | `{% if syntax == "mcp" %}invar_doc_delete(file="<file>", section="<section>"){% else %}invar doc delete <file> <section>{% endif %}` |
|
|
11
|
+
|
|
12
|
+
**Section addressing:** slug path (`requirements/auth`), fuzzy (`auth`), index (`#0/#1`), line (`@48`)
|
|
13
|
+
|
|
14
|
+
## Tool Selection
|
|
15
|
+
|
|
16
|
+
### Calling Methods (Priority Order)
|
|
17
|
+
|
|
18
|
+
Invar tools can be called in 3 ways. **Try in order:**
|
|
19
|
+
|
|
20
|
+
1. **MCP tools** (Claude Code with MCP enabled)
|
|
21
|
+
- Direct function calls: `invar_guard()`, `invar_sig()`, etc.
|
|
22
|
+
- No Bash wrapper needed
|
|
23
|
+
|
|
24
|
+
2. **CLI command** (if `invar` installed in PATH)
|
|
25
|
+
- Via Bash: `invar guard`, `invar sig`, etc.
|
|
26
|
+
- Install: `pip install invar-tools`
|
|
27
|
+
|
|
28
|
+
3. **uvx fallback** (always available, no install needed)
|
|
29
|
+
- Via Bash: `uvx invar-tools guard`, `uvx invar-tools sig`, etc.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
### Parameter Reference
|
|
34
|
+
|
|
35
|
+
**guard** - Verify code quality
|
|
36
|
+
```{% if syntax == "mcp" %}python
|
|
37
|
+
# MCP
|
|
38
|
+
invar_guard() # Check changed files (default)
|
|
39
|
+
invar_guard(changed=False) # Check all files{% else %}bash
|
|
40
|
+
# CLI
|
|
41
|
+
invar guard # Check changed files (default)
|
|
42
|
+
invar guard --all # Check all files{% endif %}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**sig** - Show function signatures and contracts
|
|
46
|
+
```{% if syntax == "mcp" %}python
|
|
47
|
+
# MCP
|
|
48
|
+
invar_sig(target="src/foo.py"){% else %}bash
|
|
49
|
+
# CLI
|
|
50
|
+
invar sig src/foo.py
|
|
51
|
+
invar sig src/foo.py::function_name{% endif %}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**map** - Find entry points
|
|
55
|
+
```{% if syntax == "mcp" %}python
|
|
56
|
+
# MCP
|
|
57
|
+
invar_map(path=".", top=10){% else %}bash
|
|
58
|
+
# CLI
|
|
59
|
+
invar map [path] --top 10{% endif %}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**refs** - Find all references to a symbol
|
|
63
|
+
```{% if syntax == "mcp" %}python
|
|
64
|
+
# MCP
|
|
65
|
+
invar_refs(target="src/foo.py::MyClass"){% else %}bash
|
|
66
|
+
# CLI
|
|
67
|
+
invar refs src/foo.py::MyClass{% endif %}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**doc*** - Document tools
|
|
71
|
+
```{% if syntax == "mcp" %}python
|
|
72
|
+
# MCP
|
|
73
|
+
invar_doc_toc(file="docs/spec.md")
|
|
74
|
+
invar_doc_read(file="docs/spec.md", section="intro"){% else %}bash
|
|
75
|
+
# CLI
|
|
76
|
+
invar doc toc docs/spec.md
|
|
77
|
+
invar doc read docs/spec.md intro{% endif %}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
### Quick Examples
|
|
83
|
+
|
|
84
|
+
```{% if syntax == "mcp" %}python
|
|
85
|
+
# Verify after changes (all three methods identical)
|
|
86
|
+
invar_guard() # MCP
|
|
87
|
+
bash("invar guard") # CLI
|
|
88
|
+
bash("uvx invar-tools guard") # uvx
|
|
89
|
+
|
|
90
|
+
# Full project check
|
|
91
|
+
invar_guard(changed=False) # MCP
|
|
92
|
+
bash("invar guard --all") # CLI
|
|
93
|
+
|
|
94
|
+
# See function contracts
|
|
95
|
+
invar_sig(target="src/core/parser.py")
|
|
96
|
+
bash("invar sig src/core/parser.py"){% else %}bash
|
|
97
|
+
# Verify after changes (all three methods identical)
|
|
98
|
+
invar guard # CLI
|
|
99
|
+
uvx invar-tools guard # uvx
|
|
100
|
+
|
|
101
|
+
# Full project check
|
|
102
|
+
invar guard --all # CLI
|
|
103
|
+
uvx invar-tools guard --all # uvx
|
|
104
|
+
|
|
105
|
+
# See function contracts
|
|
106
|
+
invar sig src/core/parser.py
|
|
107
|
+
uvx invar-tools sig src/core/parser.py{% endif %}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Note**: All three methods now have identical default behavior.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# Invar UserPromptSubmit Hook
|
|
3
3
|
# Protocol: v{{ protocol_version }} | Generated: {{ generated_date }}
|
|
4
4
|
# DX-57: Protocol refresh with full INVAR.md injection
|
|
5
|
+
# DX-79: Automatic feedback trigger via message count
|
|
5
6
|
|
|
6
7
|
USER_MESSAGE="$1"
|
|
7
8
|
|
|
@@ -82,3 +83,33 @@ if [[ $COUNT -ge 25 && $((COUNT % 10)) -eq 0 ]]; then
|
|
|
82
83
|
INVAR_EOF
|
|
83
84
|
echo "</system-reminder>"
|
|
84
85
|
fi
|
|
86
|
+
|
|
87
|
+
# ============================================
|
|
88
|
+
# DX-79: Feedback trigger at threshold
|
|
89
|
+
# ============================================
|
|
90
|
+
|
|
91
|
+
# Read feedback configuration
|
|
92
|
+
FEEDBACK_ENABLED=true
|
|
93
|
+
MIN_MESSAGES=30
|
|
94
|
+
|
|
95
|
+
if [[ -f ".claude/settings.local.json" ]]; then
|
|
96
|
+
# Try to parse with jq if available, otherwise use defaults
|
|
97
|
+
if command -v jq &> /dev/null; then
|
|
98
|
+
FEEDBACK_ENABLED=$(jq -r '.feedback.enabled // true' .claude/settings.local.json 2>/dev/null)
|
|
99
|
+
MIN_MESSAGES=$(jq -r '.feedback.min_messages // 30' .claude/settings.local.json 2>/dev/null)
|
|
100
|
+
fi
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# Trigger feedback prompt at threshold
|
|
104
|
+
if [[ "$FEEDBACK_ENABLED" == "true" && $COUNT -eq $MIN_MESSAGES ]]; then
|
|
105
|
+
echo "<system-reminder>"
|
|
106
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
107
|
+
echo "📊 Invar: Auto-triggering usage feedback ($COUNT messages)"
|
|
108
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
109
|
+
echo ""
|
|
110
|
+
echo "Session has reached $COUNT messages. Consider running /invar-reflect"
|
|
111
|
+
echo "to generate usage feedback."
|
|
112
|
+
echo ""
|
|
113
|
+
echo "To disable: Set feedback.enabled=false in .claude/settings.local.json"
|
|
114
|
+
echo "</system-reminder>"
|
|
115
|
+
fi
|
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
* Invar Pi Hook
|
|
3
3
|
* Protocol: v{{ protocol_version }} | Generated: {{ generated_date }}
|
|
4
4
|
* LX-04: Full feature parity with Claude Code hooks
|
|
5
|
+
* DX-79: Automatic feedback trigger via message count
|
|
5
6
|
*
|
|
6
7
|
* Features:
|
|
7
8
|
* - pytest/crosshair blocking via tool_call
|
|
8
9
|
* - Protocol injection via pi.send() for long conversations
|
|
10
|
+
* - Automatic /invar-reflect trigger at message threshold
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
|
|
14
|
+
import * as fs from "fs";
|
|
12
15
|
|
|
13
16
|
// Blocked commands (same as Claude Code)
|
|
14
17
|
{% if language == "python" -%}
|
|
@@ -22,6 +25,21 @@ const ALLOWED_FLAGS = [/--inspect/, /--coverage/, /--debug/];
|
|
|
22
25
|
// Protocol content for injection (escaped for JS)
|
|
23
26
|
const INVAR_PROTOCOL = `{{ invar_protocol_escaped }}`;
|
|
24
27
|
|
|
28
|
+
// DX-79: Helper to read feedback configuration
|
|
29
|
+
function readFeedbackConfig() {
|
|
30
|
+
try {
|
|
31
|
+
const settingsPath = ".claude/settings.local.json";
|
|
32
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
33
|
+
return {
|
|
34
|
+
enabled: settings.feedback?.enabled ?? true,
|
|
35
|
+
min_messages: settings.feedback?.min_messages ?? 30,
|
|
36
|
+
};
|
|
37
|
+
} catch {
|
|
38
|
+
// File doesn't exist or other error, use defaults
|
|
39
|
+
}
|
|
40
|
+
return { enabled: true, min_messages: 30 };
|
|
41
|
+
}
|
|
42
|
+
|
|
25
43
|
export default function (pi: HookAPI) {
|
|
26
44
|
let msgCount = 0;
|
|
27
45
|
|
|
@@ -53,6 +71,21 @@ export default function (pi: HookAPI) {
|
|
|
53
71
|
pi.send(`<system-reminder>
|
|
54
72
|
=== Protocol Refresh (message ${msgCount}) ===
|
|
55
73
|
${INVAR_PROTOCOL}
|
|
74
|
+
</system-reminder>`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// DX-79: Feedback trigger at threshold
|
|
78
|
+
const feedbackConfig = readFeedbackConfig();
|
|
79
|
+
if (msgCount === feedbackConfig.min_messages && feedbackConfig.enabled) {
|
|
80
|
+
pi.send(`<system-reminder>
|
|
81
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
82
|
+
📊 Invar: Auto-triggering usage feedback (${msgCount} messages)
|
|
83
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
84
|
+
|
|
85
|
+
Session has reached ${msgCount} messages. Consider running /invar-reflect
|
|
86
|
+
to generate usage feedback.
|
|
87
|
+
|
|
88
|
+
To disable: Set feedback.enabled=false in .claude/settings.local.json
|
|
56
89
|
</system-reminder>`);
|
|
57
90
|
}
|
|
58
91
|
});
|
|
@@ -62,7 +95,8 @@ ${INVAR_PROTOCOL}
|
|
|
62
95
|
// ============================================
|
|
63
96
|
pi.on("tool_call", async (event) => {
|
|
64
97
|
if (event.toolName !== "bash") return;
|
|
65
|
-
const
|
|
98
|
+
const input = event.input as Record<string, unknown>;
|
|
99
|
+
const cmd = (typeof input?.command === "string" ? input.command : "").trim();
|
|
66
100
|
|
|
67
101
|
// Skip if not a blocked command
|
|
68
102
|
if (!BLOCKED_CMDS.some((p) => p.test(cmd))) return;
|
invar/templates/manifest.toml
CHANGED
|
@@ -64,6 +64,11 @@ extensions = { action = "preserve" }
|
|
|
64
64
|
".claude/skills/propose/SKILL.md" = { src = "skills/propose/SKILL.md.jinja", type = "jinja" }
|
|
65
65
|
".claude/skills/review/SKILL.md" = { src = "skills/review/SKILL.md.jinja", type = "jinja" }
|
|
66
66
|
|
|
67
|
+
# DX-79: Invar usage feedback skill
|
|
68
|
+
".claude/skills/invar-reflect/SKILL.md" = { src = "skills/invar-reflect/SKILL.md", type = "copy" }
|
|
69
|
+
".claude/skills/invar-reflect/template.md" = { src = "skills/invar-reflect/template.md", type = "copy" }
|
|
70
|
+
".claude/skills/invar-reflect/CONFIG.md" = { src = "skills/invar-reflect/CONFIG.md", type = "copy" }
|
|
71
|
+
|
|
67
72
|
# Commands (Jinja2 templates for language-specific content)
|
|
68
73
|
".claude/commands/audit.md" = { src = "commands/audit.md.jinja", type = "jinja" }
|
|
69
74
|
".claude/commands/guard.md" = { src = "commands/guard.md", type = "copy" }
|
|
@@ -136,4 +141,7 @@ create_only = [
|
|
|
136
141
|
".pre-commit-config.yaml",
|
|
137
142
|
".claude/commands/audit.md",
|
|
138
143
|
".claude/commands/guard.md",
|
|
144
|
+
".claude/skills/invar-reflect/SKILL.md",
|
|
145
|
+
".claude/skills/invar-reflect/template.md",
|
|
146
|
+
".claude/skills/invar-reflect/CONFIG.md",
|
|
139
147
|
]
|