invar-tools 1.5.0__py3-none-any.whl → 1.7.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/__init__.py +7 -1
- invar/core/entry_points.py +12 -10
- invar/core/formatter.py +15 -0
- invar/core/models.py +85 -0
- invar/shell/commands/guard.py +38 -9
- invar/shell/commands/init.py +8 -79
- invar/shell/commands/uninstall.py +341 -0
- invar/shell/config.py +46 -0
- invar/shell/guard_output.py +10 -0
- invar/shell/templates.py +1 -70
- invar/templates/CLAUDE.md.template +18 -10
- invar/templates/commands/audit.md +6 -0
- invar/templates/commands/guard.md +6 -0
- invar/templates/config/CLAUDE.md.jinja +51 -30
- invar/templates/config/context.md.jinja +14 -0
- invar/templates/pre-commit-config.yaml.template +2 -0
- invar/templates/protocol/INVAR.md +1 -0
- invar/templates/skills/develop/SKILL.md.jinja +2 -1
- {invar_tools-1.5.0.dist-info → invar_tools-1.7.0.dist-info}/METADATA +1 -1
- {invar_tools-1.5.0.dist-info → invar_tools-1.7.0.dist-info}/RECORD +25 -26
- invar/templates/aider.conf.yml.template +0 -31
- invar/templates/cursorrules.template +0 -40
- {invar_tools-1.5.0.dist-info → invar_tools-1.7.0.dist-info}/WHEEL +0 -0
- {invar_tools-1.5.0.dist-info → invar_tools-1.7.0.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.5.0.dist-info → invar_tools-1.7.0.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.5.0.dist-info → invar_tools-1.7.0.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.5.0.dist-info → invar_tools-1.7.0.dist-info}/licenses/NOTICE +0 -0
invar/__init__.py
CHANGED
|
@@ -8,7 +8,13 @@ This package provides development tools (guard, map, sig).
|
|
|
8
8
|
For runtime contracts only, use invar-runtime instead.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
import importlib.metadata
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
__version__ = importlib.metadata.version("invar-tools")
|
|
15
|
+
except importlib.metadata.PackageNotFoundError:
|
|
16
|
+
__version__ = "0.0.0.dev" # Development mode fallback
|
|
17
|
+
|
|
12
18
|
__protocol_version__ = "5.0" # Protocol/spec version (separate from package version)
|
|
13
19
|
|
|
14
20
|
# Re-export from invar-runtime for backwards compatibility
|
invar/core/entry_points.py
CHANGED
|
@@ -100,30 +100,30 @@ def count_escape_hatches(source: str) -> int:
|
|
|
100
100
|
return len(extract_escape_hatches(source))
|
|
101
101
|
|
|
102
102
|
|
|
103
|
-
@post(lambda result: all(len(t) ==
|
|
104
|
-
def extract_escape_hatches(source: str) -> list[tuple[str, str]]:
|
|
103
|
+
@post(lambda result: all(len(t) == 3 for t in result)) # Returns (rule, reason, line) tuples
|
|
104
|
+
def extract_escape_hatches(source: str) -> list[tuple[str, str, int]]:
|
|
105
105
|
"""
|
|
106
|
-
Extract @invar:allow markers with their reasons (DX-33
|
|
106
|
+
Extract @invar:allow markers with their reasons and line numbers (DX-33, DX-66).
|
|
107
107
|
|
|
108
108
|
Uses tokenize to only match real comments, not strings/docstrings.
|
|
109
|
-
Returns list of (rule, reason) tuples for cross-file analysis.
|
|
109
|
+
Returns list of (rule, reason, line) tuples for cross-file analysis.
|
|
110
110
|
|
|
111
111
|
Examples:
|
|
112
112
|
>>> extract_escape_hatches("")
|
|
113
113
|
[]
|
|
114
114
|
>>> extract_escape_hatches("# @invar:allow shell_result: API boundary")
|
|
115
|
-
[('shell_result', 'API boundary')]
|
|
115
|
+
[('shell_result', 'API boundary', 1)]
|
|
116
116
|
>>> source = '''
|
|
117
117
|
... # @invar:allow rule1: same reason
|
|
118
118
|
... # @invar:allow rule2: different reason
|
|
119
119
|
... '''
|
|
120
120
|
>>> extract_escape_hatches(source)
|
|
121
|
-
[('rule1', 'same reason'), ('rule2', 'different reason')]
|
|
121
|
+
[('rule1', 'same reason', 2), ('rule2', 'different reason', 3)]
|
|
122
122
|
>>> # DX-33 Option C: Strings containing the pattern should NOT match
|
|
123
123
|
>>> extract_escape_hatches('suggestion = "# @invar:allow rule: reason"')
|
|
124
124
|
[]
|
|
125
125
|
"""
|
|
126
|
-
results: list[tuple[str, str]] = []
|
|
126
|
+
results: list[tuple[str, str, int]] = []
|
|
127
127
|
try:
|
|
128
128
|
# Use iterator-based readline to avoid io.StringIO (forbidden in Core)
|
|
129
129
|
lines = iter(source.splitlines(keepends=True))
|
|
@@ -132,10 +132,12 @@ def extract_escape_hatches(source: str) -> list[tuple[str, str]]:
|
|
|
132
132
|
if tok.type == tokenize.COMMENT:
|
|
133
133
|
match = INVAR_ALLOW_PATTERN.search(tok.string)
|
|
134
134
|
if match:
|
|
135
|
-
|
|
135
|
+
# DX-66: tok.start[0] is the 1-based line number
|
|
136
|
+
results.append((match.group(1), match.group(2), tok.start[0]))
|
|
136
137
|
except Exception:
|
|
137
|
-
# Fall back to regex if tokenization fails
|
|
138
|
-
|
|
138
|
+
# Fall back to regex if tokenization fails - can't get line numbers
|
|
139
|
+
# Return line 0 to indicate unknown position
|
|
140
|
+
return [(r, reason, 0) for r, reason in INVAR_ALLOW_PATTERN.findall(source)]
|
|
139
141
|
return results
|
|
140
142
|
|
|
141
143
|
|
invar/core/formatter.py
CHANGED
|
@@ -256,6 +256,21 @@ def format_guard_agent(report: GuardReport, combined_status: str | None = None)
|
|
|
256
256
|
if report.suggests > 0:
|
|
257
257
|
result["static"]["suggests"] = report.suggests
|
|
258
258
|
result["summary"]["suggests"] = report.suggests
|
|
259
|
+
# DX-66: Add escape hatch summary if any exist
|
|
260
|
+
if report.escape_hatches.count > 0:
|
|
261
|
+
result["escape_hatches"] = {
|
|
262
|
+
"count": report.escape_hatches.count,
|
|
263
|
+
"by_rule": report.escape_hatches.by_rule,
|
|
264
|
+
"details": [
|
|
265
|
+
{
|
|
266
|
+
"file": d.file,
|
|
267
|
+
"line": d.line,
|
|
268
|
+
"rule": d.rule,
|
|
269
|
+
"reason": d.reason,
|
|
270
|
+
}
|
|
271
|
+
for d in report.escape_hatches.details
|
|
272
|
+
],
|
|
273
|
+
}
|
|
259
274
|
return result
|
|
260
275
|
|
|
261
276
|
|
invar/core/models.py
CHANGED
|
@@ -83,6 +83,89 @@ class Violation(BaseModel):
|
|
|
83
83
|
suggestion: str | None = None
|
|
84
84
|
|
|
85
85
|
|
|
86
|
+
class EscapeHatchDetail(BaseModel):
|
|
87
|
+
"""
|
|
88
|
+
Detail of a single escape hatch (@invar:allow) marker (DX-66).
|
|
89
|
+
|
|
90
|
+
Examples:
|
|
91
|
+
>>> d = EscapeHatchDetail(file="test.py", line=10, rule="shell_result", reason="API")
|
|
92
|
+
>>> d.line
|
|
93
|
+
10
|
|
94
|
+
>>> # line=0 is valid (fallback when line number unknown)
|
|
95
|
+
>>> d0 = EscapeHatchDetail(file="test.py", line=0, rule="test", reason="fallback")
|
|
96
|
+
>>> d0.line
|
|
97
|
+
0
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
file: str
|
|
101
|
+
line: int = Field(ge=0) # 0 = fallback when line number unknown
|
|
102
|
+
rule: str
|
|
103
|
+
reason: str
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class EscapeHatchSummary(BaseModel):
|
|
107
|
+
"""
|
|
108
|
+
Summary of escape hatches in the codebase (DX-66).
|
|
109
|
+
|
|
110
|
+
Provides visibility into @invar:allow usage for tracking technical debt.
|
|
111
|
+
|
|
112
|
+
Examples:
|
|
113
|
+
>>> summary = EscapeHatchSummary()
|
|
114
|
+
>>> summary.count
|
|
115
|
+
0
|
|
116
|
+
>>> summary.by_rule
|
|
117
|
+
{}
|
|
118
|
+
>>> detail = EscapeHatchDetail(file="test.py", line=10, rule="shell_result", reason="API boundary")
|
|
119
|
+
>>> summary.add(detail)
|
|
120
|
+
>>> summary.count
|
|
121
|
+
1
|
|
122
|
+
>>> summary.by_rule
|
|
123
|
+
{'shell_result': 1}
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
details: list[EscapeHatchDetail] = Field(default_factory=list)
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
@post(lambda result: result >= 0)
|
|
130
|
+
def count(self) -> int:
|
|
131
|
+
"""
|
|
132
|
+
Total number of escape hatches.
|
|
133
|
+
|
|
134
|
+
Examples:
|
|
135
|
+
>>> EscapeHatchSummary().count
|
|
136
|
+
0
|
|
137
|
+
"""
|
|
138
|
+
return len(self.details)
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
@post(lambda result: all(v >= 0 for v in result.values()))
|
|
142
|
+
def by_rule(self) -> dict[str, int]:
|
|
143
|
+
"""
|
|
144
|
+
Count of escape hatches grouped by rule.
|
|
145
|
+
|
|
146
|
+
Examples:
|
|
147
|
+
>>> EscapeHatchSummary().by_rule
|
|
148
|
+
{}
|
|
149
|
+
"""
|
|
150
|
+
counts: dict[str, int] = {}
|
|
151
|
+
for detail in self.details:
|
|
152
|
+
counts[detail.rule] = counts.get(detail.rule, 0) + 1
|
|
153
|
+
return counts
|
|
154
|
+
|
|
155
|
+
@pre(lambda self, detail: bool(detail.rule) and bool(detail.file))
|
|
156
|
+
def add(self, detail: EscapeHatchDetail) -> None:
|
|
157
|
+
"""
|
|
158
|
+
Add an escape hatch detail to the summary.
|
|
159
|
+
|
|
160
|
+
Examples:
|
|
161
|
+
>>> s = EscapeHatchSummary()
|
|
162
|
+
>>> s.add(EscapeHatchDetail(file="t.py", line=1, rule="r", reason="x"))
|
|
163
|
+
>>> s.count
|
|
164
|
+
1
|
|
165
|
+
"""
|
|
166
|
+
self.details.append(detail)
|
|
167
|
+
|
|
168
|
+
|
|
86
169
|
class GuardReport(BaseModel):
|
|
87
170
|
"""Complete Guard report for a project."""
|
|
88
171
|
|
|
@@ -95,6 +178,8 @@ class GuardReport(BaseModel):
|
|
|
95
178
|
# P24: Contract coverage statistics (Core files only)
|
|
96
179
|
core_functions_total: int = 0
|
|
97
180
|
core_functions_with_contracts: int = 0
|
|
181
|
+
# DX-66: Escape hatch visibility
|
|
182
|
+
escape_hatches: EscapeHatchSummary = Field(default_factory=EscapeHatchSummary)
|
|
98
183
|
|
|
99
184
|
@pre(lambda self, violation: violation.rule and violation.severity) # Valid violation
|
|
100
185
|
def add_violation(self, violation: Violation) -> None:
|
invar/shell/commands/guard.py
CHANGED
|
@@ -25,7 +25,7 @@ from invar import __version__
|
|
|
25
25
|
from invar.core.models import GuardReport, RuleConfig
|
|
26
26
|
from invar.core.rules import check_all_rules
|
|
27
27
|
from invar.core.utils import get_exit_code
|
|
28
|
-
from invar.shell.config import load_config
|
|
28
|
+
from invar.shell.config import find_project_root, load_config
|
|
29
29
|
from invar.shell.fs import scan_project
|
|
30
30
|
from invar.shell.guard_output import output_agent, output_rich
|
|
31
31
|
|
|
@@ -56,13 +56,13 @@ def _count_core_functions(file_info) -> tuple[int, int]:
|
|
|
56
56
|
return (total, with_contracts)
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
# @shell_complexity: Core orchestration - iterates files, handles failures, aggregates results
|
|
60
59
|
# @shell_complexity: Core orchestration - iterates files, handles failures, aggregates results
|
|
61
60
|
def _scan_and_check(
|
|
62
61
|
path: Path, config: RuleConfig, only_files: set[Path] | None = None
|
|
63
62
|
) -> Result[GuardReport, str]:
|
|
64
63
|
"""Scan project files and check against rules."""
|
|
65
64
|
from invar.core.entry_points import extract_escape_hatches
|
|
65
|
+
from invar.core.models import EscapeHatchDetail
|
|
66
66
|
from invar.core.review_trigger import check_duplicate_escape_reasons
|
|
67
67
|
from invar.core.shell_architecture import check_complexity_debt
|
|
68
68
|
|
|
@@ -80,10 +80,17 @@ def _scan_and_check(
|
|
|
80
80
|
report.update_coverage(total, with_contracts)
|
|
81
81
|
for violation in check_all_rules(file_info, config):
|
|
82
82
|
report.add_violation(violation)
|
|
83
|
-
# DX-33: Collect escape hatches for cross-file analysis
|
|
83
|
+
# DX-33 + DX-66: Collect escape hatches for cross-file analysis and visibility
|
|
84
84
|
if file_info.source:
|
|
85
|
-
for rule, reason in extract_escape_hatches(file_info.source):
|
|
85
|
+
for rule, reason, line in extract_escape_hatches(file_info.source):
|
|
86
86
|
all_escapes.append((file_info.path, rule, reason))
|
|
87
|
+
# DX-66: Add to escape hatch summary
|
|
88
|
+
report.escape_hatches.add(EscapeHatchDetail(
|
|
89
|
+
file=file_info.path,
|
|
90
|
+
line=line,
|
|
91
|
+
rule=rule,
|
|
92
|
+
reason=reason,
|
|
93
|
+
))
|
|
87
94
|
|
|
88
95
|
# DX-22: Check project-level complexity debt (Fix-or-Explain enforcement)
|
|
89
96
|
for debt_violation in check_complexity_debt(
|
|
@@ -102,7 +109,11 @@ def _scan_and_check(
|
|
|
102
109
|
@app.command()
|
|
103
110
|
def guard(
|
|
104
111
|
path: Path = typer.Argument(
|
|
105
|
-
Path(),
|
|
112
|
+
Path(),
|
|
113
|
+
help="Project directory or single Python file",
|
|
114
|
+
exists=True,
|
|
115
|
+
file_okay=True,
|
|
116
|
+
dir_okay=True,
|
|
106
117
|
),
|
|
107
118
|
strict: bool = typer.Option(False, "--strict", help="Treat warnings as errors"),
|
|
108
119
|
changed: bool = typer.Option(
|
|
@@ -157,6 +168,16 @@ def guard(
|
|
|
157
168
|
)
|
|
158
169
|
from invar.shell.testing import VerificationLevel
|
|
159
170
|
|
|
171
|
+
# DX-65: Handle single file mode
|
|
172
|
+
single_file_mode = path.is_file()
|
|
173
|
+
single_file: Path | None = None
|
|
174
|
+
if single_file_mode:
|
|
175
|
+
if path.suffix != ".py":
|
|
176
|
+
console.print(f"[red]Error:[/red] {path} is not a Python file")
|
|
177
|
+
raise typer.Exit(1)
|
|
178
|
+
single_file = path.resolve()
|
|
179
|
+
path = find_project_root(path)
|
|
180
|
+
|
|
160
181
|
# Load and configure
|
|
161
182
|
config_result = load_config(path)
|
|
162
183
|
if isinstance(config_result, Failure):
|
|
@@ -179,7 +200,9 @@ def guard(
|
|
|
179
200
|
format_contract_coverage_report,
|
|
180
201
|
)
|
|
181
202
|
|
|
182
|
-
|
|
203
|
+
# DX-65: Use single file path if in single file mode
|
|
204
|
+
coverage_path = single_file if single_file else path
|
|
205
|
+
coverage_result = calculate_contract_coverage(coverage_path, changed_only=changed)
|
|
183
206
|
if isinstance(coverage_result, Failure):
|
|
184
207
|
console.print(f"[red]Error:[/red] {coverage_result.failure()}")
|
|
185
208
|
raise typer.Exit(1)
|
|
@@ -194,10 +217,14 @@ def guard(
|
|
|
194
217
|
|
|
195
218
|
raise typer.Exit(0 if report_data.ready_for_build else 1)
|
|
196
219
|
|
|
197
|
-
# Handle --changed mode
|
|
220
|
+
# Handle --changed mode or single file mode (DX-65)
|
|
198
221
|
only_files: set[Path] | None = None
|
|
199
222
|
checked_files: list[Path] = []
|
|
200
|
-
if
|
|
223
|
+
if single_file:
|
|
224
|
+
# DX-65: Single file mode - only check the specified file
|
|
225
|
+
only_files = {single_file}
|
|
226
|
+
checked_files = [single_file]
|
|
227
|
+
elif changed:
|
|
201
228
|
changed_result = handle_changed_mode(path)
|
|
202
229
|
if isinstance(changed_result, Failure):
|
|
203
230
|
if changed_result.failure() == "NO_CHANGES":
|
|
@@ -379,7 +406,7 @@ def _show_verification_level(verification_level) -> None:
|
|
|
379
406
|
@app.command()
|
|
380
407
|
def version() -> None:
|
|
381
408
|
"""Show Invar version."""
|
|
382
|
-
console.print(f"invar {__version__}")
|
|
409
|
+
console.print(f"invar-tools {__version__}")
|
|
383
410
|
|
|
384
411
|
|
|
385
412
|
@app.command("map")
|
|
@@ -494,9 +521,11 @@ from invar.shell.commands.init import init
|
|
|
494
521
|
from invar.shell.commands.mutate import mutate # DX-28
|
|
495
522
|
from invar.shell.commands.sync_self import sync_self # DX-49
|
|
496
523
|
from invar.shell.commands.test import test, verify
|
|
524
|
+
from invar.shell.commands.uninstall import uninstall # DX-69
|
|
497
525
|
from invar.shell.commands.update import update
|
|
498
526
|
|
|
499
527
|
app.command()(init)
|
|
528
|
+
app.command()(uninstall) # DX-69: Remove Invar from project
|
|
500
529
|
app.command()(update)
|
|
501
530
|
app.command()(test)
|
|
502
531
|
app.command()(verify)
|
invar/shell/commands/init.py
CHANGED
|
@@ -38,8 +38,6 @@ from invar.shell.mcp_config import (
|
|
|
38
38
|
from invar.shell.template_engine import generate_from_manifest
|
|
39
39
|
from invar.shell.templates import (
|
|
40
40
|
add_config,
|
|
41
|
-
add_invar_reference,
|
|
42
|
-
create_agent_config,
|
|
43
41
|
create_directories,
|
|
44
42
|
detect_agent_configs,
|
|
45
43
|
install_hooks,
|
|
@@ -65,18 +63,17 @@ def run_claude_init(path: Path) -> bool:
|
|
|
65
63
|
|
|
66
64
|
console.print("\n[bold]Running claude /init...[/bold]")
|
|
67
65
|
try:
|
|
66
|
+
# Don't capture output - claude /init is interactive and needs user input
|
|
68
67
|
result = subprocess.run(
|
|
69
68
|
["claude", "/init"],
|
|
70
69
|
cwd=path,
|
|
71
|
-
capture_output=True,
|
|
72
|
-
text=True,
|
|
73
70
|
timeout=120,
|
|
74
71
|
)
|
|
75
72
|
if result.returncode == 0:
|
|
76
73
|
console.print("[green]claude /init completed successfully[/green]")
|
|
77
74
|
return True
|
|
78
75
|
else:
|
|
79
|
-
console.print(
|
|
76
|
+
console.print("[yellow]Warning:[/yellow] claude /init failed")
|
|
80
77
|
return False
|
|
81
78
|
except subprocess.TimeoutExpired:
|
|
82
79
|
console.print("[yellow]Warning:[/yellow] claude /init timed out")
|
|
@@ -86,57 +83,6 @@ def run_claude_init(path: Path) -> bool:
|
|
|
86
83
|
return False
|
|
87
84
|
|
|
88
85
|
|
|
89
|
-
def append_invar_reference_to_claude_md(path: Path) -> bool:
|
|
90
|
-
"""
|
|
91
|
-
Append Invar reference to existing CLAUDE.md.
|
|
92
|
-
|
|
93
|
-
Preserves content generated by 'claude /init'.
|
|
94
|
-
Returns True if modified, False otherwise.
|
|
95
|
-
"""
|
|
96
|
-
claude_md = path / "CLAUDE.md"
|
|
97
|
-
if not claude_md.exists():
|
|
98
|
-
return False
|
|
99
|
-
|
|
100
|
-
content = claude_md.read_text()
|
|
101
|
-
if "INVAR.md" in content:
|
|
102
|
-
console.print("[dim]CLAUDE.md already references INVAR.md[/dim]")
|
|
103
|
-
return False
|
|
104
|
-
|
|
105
|
-
# Append reference at the end
|
|
106
|
-
invar_reference = """
|
|
107
|
-
|
|
108
|
-
---
|
|
109
|
-
|
|
110
|
-
## Invar Protocol
|
|
111
|
-
|
|
112
|
-
> **Protocol:** Follow [INVAR.md](./INVAR.md) — includes Check-In, USBV workflow, and Task Completion.
|
|
113
|
-
|
|
114
|
-
### Check-In
|
|
115
|
-
|
|
116
|
-
Your first message MUST display:
|
|
117
|
-
|
|
118
|
-
```
|
|
119
|
-
✓ Check-In: [project] | [branch] | [clean/dirty]
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
Read `.invar/context.md` first. Do NOT run guard/map at Check-In.
|
|
123
|
-
|
|
124
|
-
### Final
|
|
125
|
-
|
|
126
|
-
Your last message MUST display:
|
|
127
|
-
|
|
128
|
-
```
|
|
129
|
-
✓ Final: guard PASS | 0 errors, 2 warnings
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
Execute `invar guard` and show this one-line summary.
|
|
133
|
-
"""
|
|
134
|
-
|
|
135
|
-
claude_md.write_text(content + invar_reference)
|
|
136
|
-
console.print("[green]Updated[/green] CLAUDE.md (added Invar reference)")
|
|
137
|
-
return True
|
|
138
|
-
|
|
139
|
-
|
|
140
86
|
# @shell_complexity: MCP config with method selection and validation
|
|
141
87
|
def configure_mcp_with_method(
|
|
142
88
|
path: Path, mcp_method: str | None
|
|
@@ -312,11 +258,9 @@ def init(
|
|
|
312
258
|
return
|
|
313
259
|
|
|
314
260
|
# DX-21B: Run claude /init if requested (before sync)
|
|
261
|
+
# DX-69: sync_templates() will merge claude's CLAUDE.md with invar template
|
|
315
262
|
if claude:
|
|
316
|
-
|
|
317
|
-
if claude_success:
|
|
318
|
-
# Append Invar reference to generated CLAUDE.md
|
|
319
|
-
append_invar_reference_to_claude_md(path)
|
|
263
|
+
run_claude_init(path)
|
|
320
264
|
|
|
321
265
|
config_result = add_config(path, console)
|
|
322
266
|
if isinstance(config_result, Failure):
|
|
@@ -371,28 +315,13 @@ def init(
|
|
|
371
315
|
if isinstance(result, Success) and result.unwrap():
|
|
372
316
|
console.print("[green]Created[/green] .invar/proposals/TEMPLATE.md")
|
|
373
317
|
|
|
374
|
-
# Agent detection
|
|
318
|
+
# Agent detection (DX-69: simplified, only Claude Code supported)
|
|
375
319
|
console.print("\n[bold]Checking for agent configurations...[/bold]")
|
|
376
320
|
agent_result = detect_agent_configs(path)
|
|
377
|
-
if isinstance(agent_result,
|
|
378
|
-
console.print(f"[yellow]Warning:[/yellow] {agent_result.failure()}")
|
|
379
|
-
agent_status: dict[str, str] = {}
|
|
380
|
-
else:
|
|
321
|
+
if isinstance(agent_result, Success):
|
|
381
322
|
agent_status = agent_result.unwrap()
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
for agent, status in agent_status.items():
|
|
385
|
-
if status == "configured":
|
|
386
|
-
console.print(f" [green]✓[/green] {agent}: already configured")
|
|
387
|
-
elif status == "found":
|
|
388
|
-
# Existing file without Invar reference - ask before modifying
|
|
389
|
-
if yes or typer.confirm(f" Add Invar reference to {agent} config?", default=True):
|
|
390
|
-
add_invar_reference(path, agent, console)
|
|
391
|
-
else:
|
|
392
|
-
console.print(f" [yellow]○[/yellow] {agent}: skipped")
|
|
393
|
-
elif status == "not_found":
|
|
394
|
-
# Create full template with workflow enforcement (DX-17)
|
|
395
|
-
create_agent_config(path, agent, console)
|
|
323
|
+
if agent_status.get("claude") == "configured":
|
|
324
|
+
console.print(" [green]✓[/green] claude: already configured")
|
|
396
325
|
|
|
397
326
|
# Configure MCP server (DX-16, DX-21B)
|
|
398
327
|
configure_mcp_with_method(path, mcp_method)
|