invar-tools 1.11.0__py3-none-any.whl → 1.14.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/mcp/handlers.py +28 -0
- invar/mcp/server.py +64 -14
- invar/node_tools/ts-query.js +396 -0
- invar/shell/claude_hooks.py +49 -0
- invar/shell/commands/feedback.py +258 -0
- invar/shell/commands/guard.py +33 -1
- invar/shell/commands/init.py +51 -1
- invar/shell/commands/perception.py +302 -6
- invar/shell/py_refs.py +156 -0
- invar/shell/ts_compiler.py +238 -0
- invar/templates/claude-md/universal/tool-selection.md +110 -0
- invar/templates/config/CLAUDE.md.jinja +2 -0
- invar/templates/examples/typescript/patterns.md +193 -0
- invar/templates/manifest.toml +8 -0
- invar/templates/protocol/python/tools.md +5 -2
- invar/templates/protocol/typescript/tools.md +5 -2
- invar/templates/skills/invar-reflect/CONFIG.md +355 -0
- invar/templates/skills/invar-reflect/SKILL.md +466 -0
- invar/templates/skills/invar-reflect/template.md +343 -0
- {invar_tools-1.11.0.dist-info → invar_tools-1.14.0.dist-info}/METADATA +25 -7
- {invar_tools-1.11.0.dist-info → invar_tools-1.14.0.dist-info}/RECORD +27 -17
- {invar_tools-1.11.0.dist-info → invar_tools-1.14.0.dist-info}/WHEEL +0 -0
- {invar_tools-1.11.0.dist-info → invar_tools-1.14.0.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.11.0.dist-info → invar_tools-1.14.0.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.11.0.dist-info → invar_tools-1.14.0.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.11.0.dist-info → invar_tools-1.14.0.dist-info}/licenses/NOTICE +0 -0
invar/shell/claude_hooks.py
CHANGED
|
@@ -193,6 +193,55 @@ def _register_hooks_in_settings(project_path: Path) -> Result[bool, str]:
|
|
|
193
193
|
return Failure(f"Failed to update settings: {e}")
|
|
194
194
|
|
|
195
195
|
|
|
196
|
+
# @shell_complexity: Feedback config management in settings.local.json
|
|
197
|
+
def add_feedback_config(
|
|
198
|
+
project_path: Path,
|
|
199
|
+
enabled: bool = True,
|
|
200
|
+
console: Console | None = None,
|
|
201
|
+
) -> Result[bool, str]:
|
|
202
|
+
"""
|
|
203
|
+
Add feedback configuration to .claude/settings.local.json.
|
|
204
|
+
|
|
205
|
+
DX-79 Phase C: Init Integration for /invar-reflect skill.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
project_path: Path to project root
|
|
209
|
+
enabled: Whether to enable feedback collection (default: True)
|
|
210
|
+
console: Optional Rich console for output
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Success(True) if config added/updated, Failure with error message otherwise
|
|
214
|
+
"""
|
|
215
|
+
import json
|
|
216
|
+
|
|
217
|
+
settings_path = project_path / ".claude" / "settings.local.json"
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
existing = json.loads(settings_path.read_text()) if settings_path.exists() else {}
|
|
221
|
+
|
|
222
|
+
# Add feedback configuration
|
|
223
|
+
existing["feedback"] = {
|
|
224
|
+
"enabled": enabled,
|
|
225
|
+
"auto_trigger": enabled, # Same as enabled
|
|
226
|
+
"retention_days": 90,
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
# Ensure .claude directory exists
|
|
230
|
+
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
231
|
+
|
|
232
|
+
# Write with indentation for readability
|
|
233
|
+
settings_path.write_text(json.dumps(existing, indent=2) + "\n")
|
|
234
|
+
|
|
235
|
+
if console:
|
|
236
|
+
status = "enabled" if enabled else "disabled"
|
|
237
|
+
console.print(f" [green]✓[/green] Feedback collection {status}")
|
|
238
|
+
|
|
239
|
+
return Success(True)
|
|
240
|
+
|
|
241
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
242
|
+
return Failure(f"Failed to update settings: {e}")
|
|
243
|
+
|
|
244
|
+
|
|
196
245
|
# @shell_complexity: Multi-file installation with backup/merge logic for user hooks
|
|
197
246
|
def install_claude_hooks(
|
|
198
247
|
project_path: Path,
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI commands for feedback management.
|
|
3
|
+
|
|
4
|
+
DX-79 Phase D: Analysis Tools for /invar-reflect feedback.
|
|
5
|
+
Shell module: handles feedback file operations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from datetime import datetime, timedelta
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Annotated
|
|
13
|
+
|
|
14
|
+
import typer
|
|
15
|
+
from returns.result import Failure, Result, Success
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
|
|
19
|
+
from invar.core.feedback import anonymize_feedback_content
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
# Create feedback subcommand app
|
|
24
|
+
feedback_app = typer.Typer(
|
|
25
|
+
name="feedback",
|
|
26
|
+
help="Manage Invar usage feedback files.",
|
|
27
|
+
no_args_is_help=True,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# @shell_complexity: File discovery with filtering
|
|
32
|
+
def _get_feedback_files(
|
|
33
|
+
project_path: Path,
|
|
34
|
+
older_than_days: int | None = None,
|
|
35
|
+
) -> Result[list[tuple[Path, datetime]], str]:
|
|
36
|
+
"""
|
|
37
|
+
Get feedback files with optional age filtering.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
project_path: Path to project root
|
|
41
|
+
older_than_days: Only return files older than N days (None = all)
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Success with list of (file_path, modified_time) tuples, or Failure
|
|
45
|
+
"""
|
|
46
|
+
feedback_dir = project_path / ".invar" / "feedback"
|
|
47
|
+
|
|
48
|
+
if not feedback_dir.exists():
|
|
49
|
+
return Success([])
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
files: list[tuple[Path, datetime]] = []
|
|
53
|
+
cutoff = datetime.now() - timedelta(days=older_than_days) if older_than_days else None
|
|
54
|
+
|
|
55
|
+
for file in feedback_dir.glob("feedback-*.md"):
|
|
56
|
+
if not file.is_file():
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
mtime = datetime.fromtimestamp(file.stat().st_mtime)
|
|
60
|
+
|
|
61
|
+
if cutoff is None or mtime < cutoff:
|
|
62
|
+
files.append((file, mtime))
|
|
63
|
+
|
|
64
|
+
# Sort by modification time (newest first)
|
|
65
|
+
files.sort(key=lambda x: x[1], reverse=True)
|
|
66
|
+
return Success(files)
|
|
67
|
+
|
|
68
|
+
except OSError as e:
|
|
69
|
+
return Failure(f"Failed to read feedback directory: {e}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# @invar:allow entry_point_too_thick: CLI display with table formatting and help text
|
|
73
|
+
@feedback_app.command(name="list")
|
|
74
|
+
def list_feedback(
|
|
75
|
+
path: Annotated[
|
|
76
|
+
Path,
|
|
77
|
+
typer.Argument(help="Project root directory (default: current directory)"),
|
|
78
|
+
] = Path(),
|
|
79
|
+
) -> None:
|
|
80
|
+
"""
|
|
81
|
+
List all feedback files in the project.
|
|
82
|
+
|
|
83
|
+
Shows:
|
|
84
|
+
- File name
|
|
85
|
+
- Last modified time
|
|
86
|
+
- File size
|
|
87
|
+
|
|
88
|
+
Example:
|
|
89
|
+
invar feedback list
|
|
90
|
+
"""
|
|
91
|
+
path = path.resolve()
|
|
92
|
+
result = _get_feedback_files(path)
|
|
93
|
+
|
|
94
|
+
if isinstance(result, Failure):
|
|
95
|
+
console.print(f"[red]Error:[/red] {result.failure()}")
|
|
96
|
+
raise typer.Exit(1)
|
|
97
|
+
|
|
98
|
+
files = result.unwrap()
|
|
99
|
+
|
|
100
|
+
if not files:
|
|
101
|
+
console.print("[dim]No feedback files found in .invar/feedback/[/dim]")
|
|
102
|
+
console.print("\n[dim]Tip: Use /invar-reflect to generate feedback[/dim]")
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
# Display table
|
|
106
|
+
table = Table(title="Invar Feedback Files")
|
|
107
|
+
table.add_column("File", style="cyan")
|
|
108
|
+
table.add_column("Last Modified", style="yellow")
|
|
109
|
+
table.add_column("Size", justify="right", style="dim")
|
|
110
|
+
|
|
111
|
+
for file, mtime in files:
|
|
112
|
+
size_kb = file.stat().st_size / 1024
|
|
113
|
+
table.add_row(
|
|
114
|
+
file.name,
|
|
115
|
+
mtime.strftime("%Y-%m-%d %H:%M"),
|
|
116
|
+
f"{size_kb:.1f} KB",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
console.print(table)
|
|
120
|
+
console.print(f"\n[bold]{len(files)}[/bold] feedback file(s) found")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# @invar:allow entry_point_too_thick: CLI workflow with confirmation, deletion, and error handling
|
|
124
|
+
@feedback_app.command(name="cleanup")
|
|
125
|
+
def cleanup_feedback(
|
|
126
|
+
older_than: Annotated[
|
|
127
|
+
int,
|
|
128
|
+
typer.Option("--older-than", help="Delete files older than N days"),
|
|
129
|
+
] = 90,
|
|
130
|
+
path: Annotated[
|
|
131
|
+
Path,
|
|
132
|
+
typer.Argument(help="Project root directory (default: current directory)"),
|
|
133
|
+
] = Path(),
|
|
134
|
+
dry_run: Annotated[
|
|
135
|
+
bool,
|
|
136
|
+
typer.Option("--dry-run", help="Show what would be deleted without deleting"),
|
|
137
|
+
] = False,
|
|
138
|
+
) -> None:
|
|
139
|
+
"""
|
|
140
|
+
Clean up old feedback files.
|
|
141
|
+
|
|
142
|
+
Default: Delete files older than 90 days.
|
|
143
|
+
|
|
144
|
+
Examples:
|
|
145
|
+
invar feedback cleanup # Delete files older than 90 days
|
|
146
|
+
invar feedback cleanup --older-than 30 # Delete files older than 30 days
|
|
147
|
+
invar feedback cleanup --dry-run # Preview what would be deleted
|
|
148
|
+
"""
|
|
149
|
+
path = path.resolve()
|
|
150
|
+
result = _get_feedback_files(path, older_than_days=older_than)
|
|
151
|
+
|
|
152
|
+
if isinstance(result, Failure):
|
|
153
|
+
console.print(f"[red]Error:[/red] {result.failure()}")
|
|
154
|
+
raise typer.Exit(1)
|
|
155
|
+
|
|
156
|
+
files = result.unwrap()
|
|
157
|
+
|
|
158
|
+
if not files:
|
|
159
|
+
console.print(f"[green]✓[/green] No files older than {older_than} days")
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
# Display what will be deleted
|
|
163
|
+
console.print(f"[bold]Files older than {older_than} days:[/bold]\n")
|
|
164
|
+
for file, mtime in files:
|
|
165
|
+
age_days = (datetime.now() - mtime).days
|
|
166
|
+
console.print(f" [red]✗[/red] {file.name} ({age_days} days old)")
|
|
167
|
+
|
|
168
|
+
if dry_run:
|
|
169
|
+
console.print(f"\n[dim]Dry run: Would delete {len(files)} file(s)[/dim]")
|
|
170
|
+
console.print("[dim]Run without --dry-run to apply[/dim]")
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
# Confirm deletion
|
|
174
|
+
from rich.prompt import Confirm
|
|
175
|
+
|
|
176
|
+
if not Confirm.ask(f"\nDelete {len(files)} file(s)?", default=False):
|
|
177
|
+
console.print("[yellow]Cancelled[/yellow]")
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
# Delete files
|
|
181
|
+
deleted = 0
|
|
182
|
+
failed = 0
|
|
183
|
+
for file, _ in files:
|
|
184
|
+
try:
|
|
185
|
+
file.unlink()
|
|
186
|
+
deleted += 1
|
|
187
|
+
except OSError as e:
|
|
188
|
+
console.print(f" [red]Failed to delete {file.name}:[/red] {e}")
|
|
189
|
+
failed += 1
|
|
190
|
+
|
|
191
|
+
# Summary
|
|
192
|
+
console.print(f"\n[green]✓[/green] Deleted {deleted} file(s)")
|
|
193
|
+
if failed > 0:
|
|
194
|
+
console.print(f"[red]✗[/red] Failed to delete {failed} file(s)")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# @invar:allow entry_point_too_thick: CLI with file handling, error display, and multiple output modes
|
|
198
|
+
@feedback_app.command(name="anonymize")
|
|
199
|
+
def anonymize_feedback(
|
|
200
|
+
file: Annotated[
|
|
201
|
+
str,
|
|
202
|
+
typer.Argument(help="Feedback file name (e.g., feedback-2026-01-03.md)"),
|
|
203
|
+
],
|
|
204
|
+
path: Annotated[
|
|
205
|
+
Path,
|
|
206
|
+
typer.Option("--path", help="Project root directory"),
|
|
207
|
+
] = Path(),
|
|
208
|
+
output: Annotated[
|
|
209
|
+
Path | None,
|
|
210
|
+
typer.Option("--output", "-o", help="Output file (default: stdout)"),
|
|
211
|
+
] = None,
|
|
212
|
+
) -> None:
|
|
213
|
+
"""
|
|
214
|
+
Anonymize feedback for sharing.
|
|
215
|
+
|
|
216
|
+
Removes:
|
|
217
|
+
- Project names
|
|
218
|
+
- File paths
|
|
219
|
+
- Function/symbol names
|
|
220
|
+
- Error messages
|
|
221
|
+
|
|
222
|
+
Examples:
|
|
223
|
+
invar feedback anonymize feedback-2026-01-03.md
|
|
224
|
+
invar feedback anonymize feedback-2026-01-03.md -o safe.md
|
|
225
|
+
invar feedback anonymize feedback-2026-01-03.md > share.md
|
|
226
|
+
"""
|
|
227
|
+
path = path.resolve()
|
|
228
|
+
feedback_dir = path / ".invar" / "feedback"
|
|
229
|
+
input_file = feedback_dir / file
|
|
230
|
+
|
|
231
|
+
if not input_file.exists():
|
|
232
|
+
console.print(f"[red]Error:[/red] File not found: {input_file}")
|
|
233
|
+
console.print("\n[dim]Available files:[/dim]")
|
|
234
|
+
|
|
235
|
+
# Show available files
|
|
236
|
+
result = _get_feedback_files(path)
|
|
237
|
+
if isinstance(result, Success):
|
|
238
|
+
for f, _ in result.unwrap():
|
|
239
|
+
console.print(f" {f.name}")
|
|
240
|
+
|
|
241
|
+
raise typer.Exit(1)
|
|
242
|
+
|
|
243
|
+
# Read and anonymize
|
|
244
|
+
try:
|
|
245
|
+
content = input_file.read_text(encoding="utf-8")
|
|
246
|
+
anonymized = anonymize_feedback_content(content)
|
|
247
|
+
|
|
248
|
+
# Output
|
|
249
|
+
if output:
|
|
250
|
+
output.write_text(anonymized, encoding="utf-8")
|
|
251
|
+
console.print(f"[green]✓[/green] Anonymized feedback saved to: {output}")
|
|
252
|
+
else:
|
|
253
|
+
# Print to stdout
|
|
254
|
+
console.print(anonymized)
|
|
255
|
+
|
|
256
|
+
except OSError as e:
|
|
257
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
258
|
+
raise typer.Exit(1)
|
invar/shell/commands/guard.py
CHANGED
|
@@ -41,6 +41,11 @@ from invar.shell.commands.doc import doc_app
|
|
|
41
41
|
|
|
42
42
|
app.add_typer(doc_app, name="doc")
|
|
43
43
|
|
|
44
|
+
# DX-79: Register feedback subcommand
|
|
45
|
+
from invar.shell.commands.feedback import feedback_app
|
|
46
|
+
|
|
47
|
+
app.add_typer(feedback_app, name="feedback")
|
|
48
|
+
|
|
44
49
|
|
|
45
50
|
# @shell_orchestration: Statistics helper for CLI guard output
|
|
46
51
|
# @shell_complexity: Iterates symbols checking kind and contracts (4 branches minimal)
|
|
@@ -122,7 +127,7 @@ def guard(
|
|
|
122
127
|
),
|
|
123
128
|
strict: bool = typer.Option(False, "--strict", help="Treat warnings as errors"),
|
|
124
129
|
changed: bool = typer.Option(
|
|
125
|
-
|
|
130
|
+
True, "--changed/--all", help="Check git-modified files only (use --all for full check)"
|
|
126
131
|
),
|
|
127
132
|
static: bool = typer.Option(
|
|
128
133
|
False, "--static", help="Static analysis only, skip all runtime tests"
|
|
@@ -159,6 +164,9 @@ def guard(
|
|
|
159
164
|
"""Check project against Invar architecture rules.
|
|
160
165
|
|
|
161
166
|
Smart Guard: Runs static analysis + doctests + CrossHair + Hypothesis by default.
|
|
167
|
+
|
|
168
|
+
By default, checks only git-modified files for fast feedback during development.
|
|
169
|
+
Use --all to check the entire project (useful for CI/release).
|
|
162
170
|
Use --static for quick static-only checks (~0.5s vs ~5s full).
|
|
163
171
|
Use --suggest to get functional pattern suggestions (NewType, Validation, etc.).
|
|
164
172
|
Use --contracts-only (-c) to check contract coverage without running tests (DX-63).
|
|
@@ -480,6 +488,30 @@ def sig_command(
|
|
|
480
488
|
raise typer.Exit(1)
|
|
481
489
|
|
|
482
490
|
|
|
491
|
+
# @invar:allow entry_point_too_thick: Multi-language ref finding with examples
|
|
492
|
+
@app.command("refs")
|
|
493
|
+
def refs_command(
|
|
494
|
+
target: str = typer.Argument(..., help="file.py::symbol or file.ts::symbol"),
|
|
495
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
496
|
+
) -> None:
|
|
497
|
+
"""Find all references to a symbol.
|
|
498
|
+
|
|
499
|
+
DX-78: Supports Python (via jedi) and TypeScript (via TS Compiler API).
|
|
500
|
+
|
|
501
|
+
Examples:
|
|
502
|
+
invar refs src/auth.py::AuthService
|
|
503
|
+
invar refs src/auth.ts::validateToken
|
|
504
|
+
"""
|
|
505
|
+
from invar.shell.commands.perception import run_refs
|
|
506
|
+
|
|
507
|
+
# Auto-detect agent mode
|
|
508
|
+
use_json = json_output or _detect_agent_mode()
|
|
509
|
+
result = run_refs(target, use_json)
|
|
510
|
+
if isinstance(result, Failure):
|
|
511
|
+
console.print(f"[red]Error:[/red] {result.failure()}")
|
|
512
|
+
raise typer.Exit(1)
|
|
513
|
+
|
|
514
|
+
|
|
483
515
|
# @invar:allow entry_point_too_thick: Rules display with filtering and dual output modes
|
|
484
516
|
@app.command()
|
|
485
517
|
def rules(
|
invar/shell/commands/init.py
CHANGED
|
@@ -16,7 +16,7 @@ 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,
|
|
@@ -239,6 +239,40 @@ def _prompt_file_selection(agents: list[str]) -> dict[str, bool]:
|
|
|
239
239
|
return {f: f in selected for f in file_list}
|
|
240
240
|
|
|
241
241
|
|
|
242
|
+
# @shell_complexity: Interactive consent prompt for feedback collection
|
|
243
|
+
def _prompt_feedback_consent() -> bool:
|
|
244
|
+
"""
|
|
245
|
+
Prompt user for consent to enable automatic feedback collection.
|
|
246
|
+
|
|
247
|
+
DX-79 Phase C: Opt-out consent flow (default: enabled).
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
True if user consents (or accepts default), False otherwise
|
|
251
|
+
"""
|
|
252
|
+
from rich import print as rprint
|
|
253
|
+
from rich.prompt import Confirm
|
|
254
|
+
|
|
255
|
+
rprint()
|
|
256
|
+
rprint("[bold]━" * 40)
|
|
257
|
+
rprint("[bold]📊 Usage Feedback (Optional)")
|
|
258
|
+
rprint("[bold]━" * 40)
|
|
259
|
+
rprint()
|
|
260
|
+
rprint("Invar can automatically reflect on tool usage to help improve")
|
|
261
|
+
rprint("the framework. Feedback is:")
|
|
262
|
+
rprint(" • Stored locally in [cyan].invar/feedback/[/cyan]")
|
|
263
|
+
rprint(" • Never sent automatically")
|
|
264
|
+
rprint(" • You decide what (if anything) to share")
|
|
265
|
+
rprint()
|
|
266
|
+
|
|
267
|
+
# Opt-out: default is True (Y)
|
|
268
|
+
consent = Confirm.ask(
|
|
269
|
+
"Enable automatic feedback collection?",
|
|
270
|
+
default=True,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
return consent
|
|
274
|
+
|
|
275
|
+
|
|
242
276
|
def _show_execution_output(
|
|
243
277
|
created: list[str],
|
|
244
278
|
merged: list[str],
|
|
@@ -442,6 +476,10 @@ def init(
|
|
|
442
476
|
for category in ["optional", "claude"]:
|
|
443
477
|
for file, _ in FILE_CATEGORIES.get(category, []):
|
|
444
478
|
selected_files[file] = True
|
|
479
|
+
# DX-79: Default feedback enabled for quick mode
|
|
480
|
+
feedback_enabled = True
|
|
481
|
+
console.print("\n[dim]📊 Feedback collection enabled by default (stored locally in .invar/feedback/)[/dim]")
|
|
482
|
+
console.print("[dim] To disable: Set feedback.enabled=false in .claude/settings.local.json[/dim]")
|
|
445
483
|
elif pi:
|
|
446
484
|
# Quick mode: Pi defaults
|
|
447
485
|
agents = ["pi"]
|
|
@@ -449,6 +487,10 @@ def init(
|
|
|
449
487
|
for category in ["optional", "pi"]:
|
|
450
488
|
for file, _ in FILE_CATEGORIES.get(category, []):
|
|
451
489
|
selected_files[file] = True
|
|
490
|
+
# DX-79: Default feedback enabled for quick mode
|
|
491
|
+
feedback_enabled = True
|
|
492
|
+
console.print("\n[dim]📊 Feedback collection enabled by default (stored locally in .invar/feedback/)[/dim]")
|
|
493
|
+
console.print("[dim] To disable: Set feedback.enabled=false in .claude/settings.local.json[/dim]")
|
|
452
494
|
else:
|
|
453
495
|
# Interactive mode
|
|
454
496
|
if not _is_interactive():
|
|
@@ -457,6 +499,8 @@ def init(
|
|
|
457
499
|
|
|
458
500
|
agents = _prompt_agent_selection()
|
|
459
501
|
selected_files = _prompt_file_selection(agents)
|
|
502
|
+
# DX-79: Prompt for feedback consent (opt-out, default: enabled)
|
|
503
|
+
feedback_enabled = _prompt_feedback_consent()
|
|
460
504
|
|
|
461
505
|
# Preview mode
|
|
462
506
|
if preview:
|
|
@@ -554,6 +598,12 @@ def init(
|
|
|
554
598
|
if "pi" in agents and selected_files.get(".pi/hooks/", True):
|
|
555
599
|
install_pi_hooks(path, console)
|
|
556
600
|
|
|
601
|
+
# Add feedback configuration (DX-79 Phase C)
|
|
602
|
+
if "claude" in agents or "pi" in agents:
|
|
603
|
+
feedback_result = add_feedback_config(path, feedback_enabled, console)
|
|
604
|
+
if isinstance(feedback_result, Failure):
|
|
605
|
+
console.print(f"[yellow]Warning:[/yellow] {feedback_result.failure()}")
|
|
606
|
+
|
|
557
607
|
# Create MCP setup guide
|
|
558
608
|
mcp_setup = invar_dir / "mcp-setup.md"
|
|
559
609
|
if not mcp_setup.exists():
|