invar-tools 1.7.1__py3-none-any.whl → 1.10.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.
Files changed (113) hide show
  1. invar/__init__.py +8 -0
  2. invar/core/language.py +88 -0
  3. invar/core/models.py +106 -0
  4. invar/core/patterns/detector.py +6 -1
  5. invar/core/patterns/p0_exhaustive.py +15 -3
  6. invar/core/patterns/p0_literal.py +15 -3
  7. invar/core/patterns/p0_newtype.py +15 -3
  8. invar/core/patterns/p0_nonempty.py +15 -3
  9. invar/core/patterns/p0_validation.py +15 -3
  10. invar/core/patterns/registry.py +5 -1
  11. invar/core/patterns/types.py +5 -1
  12. invar/core/property_gen.py +4 -0
  13. invar/core/rules.py +84 -18
  14. invar/core/sync_helpers.py +27 -1
  15. invar/core/template_helpers.py +32 -0
  16. invar/core/ts_parsers.py +286 -0
  17. invar/core/ts_sig_parser.py +307 -0
  18. invar/node_tools/MANIFEST +7 -0
  19. invar/node_tools/__init__.py +51 -0
  20. invar/node_tools/fc-runner/cli.js +77 -0
  21. invar/node_tools/quick-check/cli.js +28 -0
  22. invar/node_tools/ts-analyzer/cli.js +480 -0
  23. invar/shell/claude_hooks.py +35 -12
  24. invar/shell/commands/guard.py +36 -1
  25. invar/shell/commands/init.py +133 -7
  26. invar/shell/commands/perception.py +157 -33
  27. invar/shell/commands/skill.py +187 -0
  28. invar/shell/commands/template_sync.py +65 -13
  29. invar/shell/commands/uninstall.py +77 -12
  30. invar/shell/commands/update.py +6 -14
  31. invar/shell/contract_coverage.py +1 -0
  32. invar/shell/fs.py +66 -13
  33. invar/shell/pi_hooks.py +213 -0
  34. invar/shell/prove/guard_ts.py +899 -0
  35. invar/shell/skill_manager.py +353 -0
  36. invar/shell/template_engine.py +28 -4
  37. invar/shell/templates.py +4 -4
  38. invar/templates/claude-md/python/critical-rules.md +33 -0
  39. invar/templates/claude-md/python/quick-reference.md +24 -0
  40. invar/templates/claude-md/typescript/critical-rules.md +40 -0
  41. invar/templates/claude-md/typescript/quick-reference.md +24 -0
  42. invar/templates/claude-md/universal/check-in.md +25 -0
  43. invar/templates/claude-md/universal/skills.md +73 -0
  44. invar/templates/claude-md/universal/workflow.md +55 -0
  45. invar/templates/commands/{audit.md → audit.md.jinja} +18 -1
  46. invar/templates/config/AGENT.md.jinja +256 -0
  47. invar/templates/config/CLAUDE.md.jinja +16 -209
  48. invar/templates/config/context.md.jinja +19 -0
  49. invar/templates/examples/{README.md → python/README.md} +2 -0
  50. invar/templates/examples/{conftest.py → python/conftest.py} +1 -1
  51. invar/templates/examples/{contracts.py → python/contracts.py} +81 -4
  52. invar/templates/examples/python/core_shell.py +227 -0
  53. invar/templates/examples/python/functional.py +613 -0
  54. invar/templates/examples/typescript/README.md +31 -0
  55. invar/templates/examples/typescript/contracts.ts +163 -0
  56. invar/templates/examples/typescript/core_shell.ts +374 -0
  57. invar/templates/examples/typescript/functional.ts +601 -0
  58. invar/templates/examples/typescript/workflow.md +95 -0
  59. invar/templates/hooks/PostToolUse.sh.jinja +10 -1
  60. invar/templates/hooks/PreToolUse.sh.jinja +38 -0
  61. invar/templates/hooks/Stop.sh.jinja +1 -1
  62. invar/templates/hooks/UserPromptSubmit.sh.jinja +7 -0
  63. invar/templates/hooks/pi/invar.ts.jinja +82 -0
  64. invar/templates/manifest.toml +8 -6
  65. invar/templates/onboard/assessment.md.jinja +214 -0
  66. invar/templates/onboard/patterns/python.md +347 -0
  67. invar/templates/onboard/patterns/typescript.md +452 -0
  68. invar/templates/onboard/roadmap.md.jinja +168 -0
  69. invar/templates/protocol/INVAR.md.jinja +51 -0
  70. invar/templates/protocol/python/architecture-examples.md +41 -0
  71. invar/templates/protocol/python/contracts-syntax.md +56 -0
  72. invar/templates/protocol/python/markers.md +44 -0
  73. invar/templates/protocol/python/tools.md +24 -0
  74. invar/templates/protocol/python/troubleshooting.md +38 -0
  75. invar/templates/protocol/typescript/architecture-examples.md +52 -0
  76. invar/templates/protocol/typescript/contracts-syntax.md +73 -0
  77. invar/templates/protocol/typescript/markers.md +48 -0
  78. invar/templates/protocol/typescript/tools.md +65 -0
  79. invar/templates/protocol/typescript/troubleshooting.md +104 -0
  80. invar/templates/protocol/universal/architecture.md +36 -0
  81. invar/templates/protocol/universal/completion.md +14 -0
  82. invar/templates/protocol/universal/contracts-concept.md +37 -0
  83. invar/templates/protocol/universal/header.md +17 -0
  84. invar/templates/protocol/universal/session.md +17 -0
  85. invar/templates/protocol/universal/six-laws.md +10 -0
  86. invar/templates/protocol/universal/usbv.md +14 -0
  87. invar/templates/protocol/universal/visible-workflow.md +25 -0
  88. invar/templates/skills/develop/SKILL.md.jinja +98 -3
  89. invar/templates/skills/extensions/_registry.yaml +93 -0
  90. invar/templates/skills/extensions/acceptance/SKILL.md +383 -0
  91. invar/templates/skills/extensions/invar-onboard/SKILL.md +448 -0
  92. invar/templates/skills/extensions/invar-onboard/patterns/python.md +347 -0
  93. invar/templates/skills/extensions/invar-onboard/patterns/typescript.md +452 -0
  94. invar/templates/skills/extensions/invar-onboard/templates/assessment.md.jinja +214 -0
  95. invar/templates/skills/extensions/invar-onboard/templates/roadmap.md.jinja +168 -0
  96. invar/templates/skills/extensions/security/SKILL.md +382 -0
  97. invar/templates/skills/extensions/security/patterns/_common.yaml +126 -0
  98. invar/templates/skills/extensions/security/patterns/python.yaml +155 -0
  99. invar/templates/skills/extensions/security/patterns/typescript.yaml +194 -0
  100. invar/templates/skills/investigate/SKILL.md.jinja +15 -0
  101. invar/templates/skills/propose/SKILL.md.jinja +33 -0
  102. invar/templates/skills/review/SKILL.md.jinja +346 -71
  103. {invar_tools-1.7.1.dist-info → invar_tools-1.10.0.dist-info}/METADATA +326 -19
  104. invar_tools-1.10.0.dist-info/RECORD +173 -0
  105. invar/templates/examples/core_shell.py +0 -127
  106. invar/templates/protocol/INVAR.md +0 -310
  107. invar_tools-1.7.1.dist-info/RECORD +0 -112
  108. /invar/templates/examples/{workflow.md → python/workflow.md} +0 -0
  109. {invar_tools-1.7.1.dist-info → invar_tools-1.10.0.dist-info}/WHEEL +0 -0
  110. {invar_tools-1.7.1.dist-info → invar_tools-1.10.0.dist-info}/entry_points.txt +0 -0
  111. {invar_tools-1.7.1.dist-info → invar_tools-1.10.0.dist-info}/licenses/LICENSE +0 -0
  112. {invar_tools-1.7.1.dist-info → invar_tools-1.10.0.dist-info}/licenses/LICENSE-GPL +0 -0
  113. {invar_tools-1.7.1.dist-info → invar_tools-1.10.0.dist-info}/licenses/NOTICE +0 -0
@@ -0,0 +1,353 @@
1
+ """
2
+ Skill management for Invar extension skills.
3
+
4
+ LX-07: Extension Skills Architecture
5
+ - List available extension skills from registry
6
+ - Add/remove skills to/from project
7
+ - Update installed skills from templates
8
+
9
+ DX-71: Simplified to idempotent `add` command with region merge.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import shutil
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+ from typing import TYPE_CHECKING
18
+
19
+ import yaml
20
+ from returns.result import Failure, Result, Success
21
+
22
+ from invar.core.template_parser import parse_invar_regions, reconstruct_file
23
+
24
+ if TYPE_CHECKING:
25
+ from rich.console import Console
26
+
27
+
28
+ SKILLS_REGISTRY = "extensions/_registry.yaml"
29
+ SKILLS_DIR = "extensions"
30
+ PROJECT_SKILLS_DIR = ".claude/skills"
31
+
32
+ # Core skills managed by Invar (shared with uninstall.py)
33
+ CORE_SKILLS = {"develop", "review", "investigate", "propose", "guard", "audit"}
34
+
35
+
36
+ # @shell_orchestration: Validation helper used only by shell add_skill/remove_skill
37
+ def _is_valid_skill_name(name: str) -> bool:
38
+ """Validate skill name to prevent path traversal attacks."""
39
+ # Block path traversal characters
40
+ if ".." in name or "/" in name or "\\" in name:
41
+ return False
42
+ # Must be non-empty and not start with dot or underscore
43
+ return bool(name) and not name.startswith(".") and not name.startswith("_")
44
+
45
+
46
+ def _merge_md_file(src: Path, dst: Path) -> tuple[bool, str]:
47
+ """
48
+ Merge .md file preserving user's extensions region.
49
+
50
+ DX-71: Only updates <!--invar:skill--> region, preserves <!--invar:extensions-->.
51
+
52
+ Returns:
53
+ (merged, message) - True if merged, False if copied fresh
54
+ """
55
+ if not dst.exists():
56
+ shutil.copy2(src, dst)
57
+ return False, "Copied"
58
+
59
+ try:
60
+ new_content = src.read_text()
61
+ old_content = dst.read_text()
62
+
63
+ parsed_new = parse_invar_regions(new_content)
64
+ parsed_old = parse_invar_regions(old_content)
65
+
66
+ # If old file has regions and new file has skill region, merge
67
+ if parsed_old.has_regions and "skill" in parsed_new.regions:
68
+ updates = {"skill": parsed_new.regions["skill"].content}
69
+ merged = reconstruct_file(parsed_old, updates)
70
+ dst.write_text(merged)
71
+ return True, "Merged (extensions preserved)"
72
+
73
+ # No regions to merge - overwrite file
74
+ shutil.copy2(src, dst)
75
+ return False, "Updated"
76
+
77
+ except Exception:
78
+ # On parse error, preserve existing file - don't silently lose user data
79
+ # Return warning message so caller can inform user
80
+ return False, "Skipped (merge failed, existing file preserved)"
81
+
82
+
83
+ @dataclass
84
+ class SkillInfo:
85
+ """Information about an extension skill."""
86
+
87
+ name: str
88
+ description: str
89
+ tier: str
90
+ isolation: bool
91
+ status: str # "available", "pending_discussion", "installed"
92
+ files: list[str]
93
+
94
+
95
+ def get_templates_path() -> Path:
96
+ """Get the path to Invar templates directory."""
97
+ # Navigate from this file to templates/skills/
98
+ return Path(__file__).parent.parent / "templates" / "skills"
99
+
100
+
101
+ def load_registry() -> Result[dict, str]:
102
+ """Load the extension skills registry."""
103
+ registry_path = get_templates_path() / SKILLS_REGISTRY
104
+
105
+ if not registry_path.exists():
106
+ return Failure(f"Registry not found: {registry_path}")
107
+
108
+ try:
109
+ content = registry_path.read_text()
110
+ data = yaml.safe_load(content)
111
+ return Success(data)
112
+ except Exception as e:
113
+ return Failure(f"Failed to parse registry: {e}")
114
+
115
+
116
+ # @shell_complexity: Iterates registry entries and checks installed status
117
+ def list_skills(
118
+ project_path: Path, console: Console
119
+ ) -> Result[list[SkillInfo], str]:
120
+ """
121
+ List all available extension skills.
122
+
123
+ Returns both available and installed skills with their status.
124
+ """
125
+ registry_result = load_registry()
126
+ if isinstance(registry_result, Failure):
127
+ return registry_result
128
+
129
+ registry = registry_result.unwrap()
130
+ extensions = registry.get("extensions", {})
131
+
132
+ # Check which skills are installed
133
+ installed_dir = project_path / PROJECT_SKILLS_DIR
134
+ installed_skills = set()
135
+ if installed_dir.exists():
136
+ for skill_dir in installed_dir.iterdir():
137
+ if skill_dir.is_dir() and not skill_dir.name.startswith("_"):
138
+ # Check if it's an extension (not a core skill)
139
+ if (skill_dir / "SKILL.md").exists():
140
+ installed_skills.add(skill_dir.name)
141
+
142
+ skills = []
143
+ for name, info in extensions.items():
144
+ status = info.get("status", "available")
145
+ if name in installed_skills:
146
+ status = "installed"
147
+
148
+ skills.append(
149
+ SkillInfo(
150
+ name=name,
151
+ description=info.get("description", ""),
152
+ tier=info.get("tier", "T0"),
153
+ isolation=info.get("isolation", False),
154
+ status=status,
155
+ files=info.get("files", ["SKILL.md"]),
156
+ )
157
+ )
158
+
159
+ return Success(skills)
160
+
161
+
162
+ # @shell_complexity: Validates skill, copies files/directories with error recovery
163
+ def add_skill(
164
+ skill_name: str, project_path: Path, console: Console
165
+ ) -> Result[str, str]:
166
+ """
167
+ Add or update an extension skill to the project.
168
+
169
+ DX-71: Idempotent - installs if missing, updates if exists.
170
+ For .md files, preserves <!--invar:extensions--> region.
171
+
172
+ Copies skill files from templates to .claude/skills/<name>/
173
+ """
174
+ # Validate skill name (defense in depth against path traversal)
175
+ if not _is_valid_skill_name(skill_name):
176
+ return Failure(
177
+ f"Invalid skill name: {skill_name}. "
178
+ "Names cannot contain '.', '/', '\\' or start with '_'"
179
+ )
180
+
181
+ # Load registry to validate skill exists
182
+ registry_result = load_registry()
183
+ if isinstance(registry_result, Failure):
184
+ return registry_result
185
+
186
+ registry = registry_result.unwrap()
187
+ extensions = registry.get("extensions", {})
188
+
189
+ if skill_name not in extensions:
190
+ available = ", ".join(extensions.keys())
191
+ return Failure(f"Unknown skill: {skill_name}. Available: {available}")
192
+
193
+ skill_info = extensions[skill_name]
194
+
195
+ # Check status
196
+ if skill_info.get("status") == "pending_discussion":
197
+ return Failure(
198
+ f"Skill '{skill_name}' is pending discussion (T1). "
199
+ "It will be available in a future release."
200
+ )
201
+
202
+ # Source and destination paths
203
+ source_dir = get_templates_path() / SKILLS_DIR / skill_name
204
+ dest_dir = project_path / PROJECT_SKILLS_DIR / skill_name
205
+
206
+ if not source_dir.exists():
207
+ return Failure(f"Skill template not found: {source_dir}")
208
+
209
+ # DX-71: Determine if this is install or update
210
+ is_update = dest_dir.exists()
211
+ action = "Updating" if is_update else "Adding"
212
+ console.print(f"{action} skill: {skill_name}")
213
+
214
+ # Copy/merge skill files
215
+ try:
216
+ dest_dir.mkdir(parents=True, exist_ok=True)
217
+
218
+ for file_path in skill_info.get("files", ["SKILL.md"]):
219
+ src = source_dir / file_path
220
+ dst = dest_dir / file_path
221
+
222
+ if src.is_file():
223
+ dst.parent.mkdir(parents=True, exist_ok=True)
224
+
225
+ # DX-71: Use merge for .md files when updating
226
+ if is_update and file_path.endswith(".md"):
227
+ _merged, msg = _merge_md_file(src, dst)
228
+ # DX-71 review: Show warning for merge failures
229
+ if msg.startswith("Skipped"):
230
+ console.print(f" [yellow]Warning: {msg}: {file_path}[/yellow]")
231
+ else:
232
+ console.print(f" [dim]{msg}: {file_path}[/dim]")
233
+ else:
234
+ shutil.copy2(src, dst)
235
+ action_msg = "Updated" if is_update else "Copied"
236
+ console.print(f" [dim]{action_msg}: {file_path}[/dim]")
237
+
238
+ elif src.is_dir():
239
+ # Handle directory (e.g., patterns/)
240
+ # DX-71 review: Use dirs_exist_ok=True for atomic update (no rmtree race)
241
+ shutil.copytree(src, dst, dirs_exist_ok=True)
242
+ action_msg = "Updated" if is_update else "Copied"
243
+ console.print(f" [dim]{action_msg}: {file_path}/[/dim]")
244
+
245
+ result_msg = "updated" if is_update else "installed"
246
+ return Success(f"Skill '{skill_name}' {result_msg} successfully")
247
+
248
+ except Exception as e:
249
+ # Clean up on failure (only for fresh install)
250
+ # M3 note: Updates that fail mid-way may leave directory in partial state.
251
+ # This is acceptable because: (1) user extensions are preserved via merge,
252
+ # (2) re-running add will complete the update. Full atomicity would require
253
+ # temp directory + rename, adding complexity for rare failure cases.
254
+ if not is_update and dest_dir.exists():
255
+ shutil.rmtree(dest_dir)
256
+ return Failure(f"Failed to {'update' if is_update else 'install'} skill: {e}")
257
+
258
+
259
+ def has_user_extensions(skill_dir: Path) -> bool:
260
+ """Check if SKILL.md has user content in extensions region."""
261
+ import re
262
+
263
+ skill_md = skill_dir / "SKILL.md"
264
+ if not skill_md.exists():
265
+ return False
266
+
267
+ # M1 fix: Narrow exception scope for better error handling
268
+ try:
269
+ content = skill_md.read_text()
270
+ except (OSError, UnicodeDecodeError):
271
+ # Cannot read file - assume extensions exist (safe default)
272
+ return True
273
+
274
+ try:
275
+ parsed = parse_invar_regions(content)
276
+
277
+ if "extensions" in parsed.regions:
278
+ ext_content = parsed.regions["extensions"].content
279
+
280
+ # Remove HTML comment blocks (the template content is inside comments)
281
+ # This preserves user content like markdown lists (- item)
282
+ cleaned = re.sub(r"<!--.*?-->", "", ext_content, flags=re.DOTALL)
283
+
284
+ # Check if any non-whitespace content remains
285
+ return bool(cleaned.strip())
286
+ except Exception:
287
+ # Parse error - assume extensions exist (safe default)
288
+ return True
289
+
290
+ return False
291
+
292
+
293
+ # @shell_complexity: Validates core skill protection + user extensions check
294
+ def remove_skill(
295
+ skill_name: str, project_path: Path, console: Console, force: bool = False
296
+ ) -> Result[str, str]:
297
+ """
298
+ Remove an extension skill from the project.
299
+
300
+ DX-71: Warns if user has custom extensions content.
301
+ """
302
+ # Validate skill name (defense in depth against path traversal)
303
+ if not _is_valid_skill_name(skill_name):
304
+ return Failure(
305
+ f"Invalid skill name: {skill_name}. "
306
+ "Names cannot contain '.', '/', '\\' or start with '_'"
307
+ )
308
+
309
+ dest_dir = project_path / PROJECT_SKILLS_DIR / skill_name
310
+
311
+ if not dest_dir.exists():
312
+ return Failure(f"Skill not installed: {skill_name}")
313
+
314
+ # Protect core skills
315
+ if skill_name in CORE_SKILLS:
316
+ return Failure(
317
+ f"Cannot remove core skill: {skill_name}. "
318
+ "Only extension skills can be removed."
319
+ )
320
+
321
+ # DX-71: Check for user extensions
322
+ # Note: CLI also checks this for UX ordering (warn before confirm dialog).
323
+ # This check remains for programmatic API callers.
324
+ if not force and has_user_extensions(dest_dir):
325
+ console.print(
326
+ "[yellow]Warning:[/yellow] This skill has custom extensions content "
327
+ "that will be lost."
328
+ )
329
+ # M2 fix: API-appropriate message (not CLI --force)
330
+ return Failure(
331
+ "Skill has user extensions. Pass force=True to confirm removal."
332
+ )
333
+
334
+ try:
335
+ shutil.rmtree(dest_dir)
336
+ return Success(f"Skill '{skill_name}' removed successfully")
337
+ except Exception as e:
338
+ return Failure(f"Failed to remove skill: {e}")
339
+
340
+
341
+ def update_skill(
342
+ skill_name: str, project_path: Path, console: Console
343
+ ) -> Result[str, str]:
344
+ """
345
+ Update an installed extension skill from templates.
346
+
347
+ DX-71: Deprecated - use `add_skill` instead (idempotent).
348
+ This function now delegates to add_skill with a deprecation notice.
349
+ """
350
+ console.print(
351
+ "[dim]Note: 'skill update' is deprecated, use 'skill add' instead[/dim]"
352
+ )
353
+ return add_skill(skill_name, project_path, console)
@@ -162,7 +162,9 @@ def render_template_file(
162
162
  template_path: Path,
163
163
  variables: dict[str, str],
164
164
  ) -> Result[str, str]:
165
- """Render a Jinja2 template file.
165
+ """Render a Jinja2 template file with {% include %} support.
166
+
167
+ Uses FileSystemLoader to resolve includes relative to templates directory.
166
168
 
167
169
  Examples:
168
170
  >>> from pathlib import Path
@@ -176,11 +178,33 @@ def render_template_file(
176
178
  >>> path.unlink()
177
179
  """
178
180
  try:
179
- content = template_path.read_text()
181
+ from jinja2 import Environment, FileSystemLoader, StrictUndefined
182
+
183
+ # Use FileSystemLoader for {% include %} support (LX-05)
184
+ templates_dir = get_templates_dir()
185
+ env = Environment(
186
+ loader=FileSystemLoader(str(templates_dir)),
187
+ undefined=StrictUndefined,
188
+ keep_trailing_newline=True,
189
+ )
190
+
191
+ # Get template path relative to templates directory
192
+ try:
193
+ rel_path = template_path.relative_to(templates_dir)
194
+ template = env.get_template(str(rel_path))
195
+ except ValueError:
196
+ # Template is not in templates_dir, fall back to direct rendering
197
+ content = template_path.read_text()
198
+ template = env.from_string(content)
199
+
200
+ rendered = template.render(**variables)
201
+ return Success(rendered)
180
202
  except OSError as e:
181
203
  return Failure(f"Failed to read template {template_path}: {e}")
182
-
183
- return render_template(content, variables)
204
+ except ImportError:
205
+ return Failure("Jinja2 not installed. Run: pip install jinja2")
206
+ except Exception as e:
207
+ return Failure(f"Template rendering failed: {e}")
184
208
 
185
209
 
186
210
  # =============================================================================
invar/shell/templates.py CHANGED
@@ -15,12 +15,12 @@ _DEFAULT_PYPROJECT_CONFIG = """\n# Invar Configuration
15
15
  [tool.invar.guard]
16
16
  core_paths = ["src/core"]
17
17
  shell_paths = ["src/shell"]
18
- max_file_lines = 300
18
+ max_file_lines = 500
19
19
  max_function_lines = 50
20
20
  require_contracts = true
21
21
  require_doctests = true
22
22
  forbidden_imports = ["os", "sys", "socket", "requests", "urllib", "subprocess", "shutil", "io", "pathlib"]
23
- exclude_paths = ["tests", "scripts", ".venv"]
23
+ exclude_paths = ["tests", "test", "scripts", ".venv", "venv", "__pycache__", ".pytest_cache", "node_modules", "dist", "build"]
24
24
  """
25
25
 
26
26
  _DEFAULT_INVAR_TOML = """# Invar Configuration
@@ -29,12 +29,12 @@ _DEFAULT_INVAR_TOML = """# Invar Configuration
29
29
  [guard]
30
30
  core_paths = ["src/core"]
31
31
  shell_paths = ["src/shell"]
32
- max_file_lines = 300
32
+ max_file_lines = 500
33
33
  max_function_lines = 50
34
34
  require_contracts = true
35
35
  require_doctests = true
36
36
  forbidden_imports = ["os", "sys", "socket", "requests", "urllib", "subprocess", "shutil", "io", "pathlib"]
37
- exclude_paths = ["tests", "scripts", ".venv"]
37
+ exclude_paths = ["tests", "test", "scripts", ".venv", "venv", "__pycache__", ".pytest_cache", "node_modules", "dist", "build"]
38
38
 
39
39
  # Pattern-based classification (optional, takes priority over paths)
40
40
  # core_patterns = ["**/domain/**", "**/models/**"]
@@ -0,0 +1,33 @@
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-->
@@ -0,0 +1,24 @@
1
+ ## Project Structure
2
+
3
+ ```
4
+ src/{project}/
5
+ ├── core/ # Pure logic (@pre/@post, doctests, no I/O)
6
+ └── shell/ # I/O operations (Result[T, E] return type)
7
+ ```
8
+
9
+ **Key insight:** Core receives data (strings), Shell handles I/O (paths, files).
10
+
11
+ ## Quick Reference
12
+
13
+ | Zone | Requirements |
14
+ |------|-------------|
15
+ | Core | `@pre`/`@post` + doctests, pure (no I/O) |
16
+ | Shell | Returns `Result[T, E]` from `returns` library |
17
+
18
+ ### Core vs Shell (Edge Cases)
19
+
20
+ - File/network/env vars → **Shell**
21
+ - `datetime.now()`, `random` → **Inject param** OR Shell
22
+ - Pure logic → **Core**
23
+
24
+ > Full decision tree: [INVAR.md#core-shell](./INVAR.md#decision-tree-core-vs-shell)
@@ -0,0 +1,40 @@
1
+ <!--invar:critical-->
2
+ ## ⚡ Critical Rules
3
+
4
+ | Always | Remember |
5
+ |--------|----------|
6
+ {% if syntax == "mcp" -%}
7
+ | **Verify** | `invar_guard` — NOT just `tsc`, NOT just `vitest` |
8
+ {% else -%}
9
+ | **Verify** | `invar guard` — NOT just `tsc`, NOT just `vitest` |
10
+ {% endif -%}
11
+ | **Core** | Zod schemas + JSDoc examples, NO I/O imports |
12
+ | **Shell** | Returns `Result<T, E>` from `neverthrow` library |
13
+ | **Flow** | USBV: Understand → Specify → Build → Validate |
14
+
15
+ ### Contract Rules (CRITICAL)
16
+
17
+ ```typescript
18
+ import { z } from 'zod';
19
+
20
+ // ❌ WRONG: No validation, just type annotation
21
+ function calc(x: number): number { ... }
22
+
23
+ // ✅ CORRECT: Zod schema validates at runtime
24
+ const CalcInput = z.number().positive();
25
+ const CalcOutput = z.number().nonnegative();
26
+
27
+ function calc(x: number): number {
28
+ const validated = CalcInput.parse(x);
29
+ const result = validated * 2;
30
+ return CalcOutput.parse(result);
31
+ }
32
+
33
+ // ❌ WRONG: Schema only checks type
34
+ const BadSchema = z.number();
35
+
36
+ // ✅ CORRECT: Schema checks domain constraints
37
+ const GoodSchema = z.number().positive().max(100);
38
+ ```
39
+
40
+ <!--/invar:critical-->
@@ -0,0 +1,24 @@
1
+ ## Project Structure
2
+
3
+ ```
4
+ src/{project}/
5
+ ├── core/ # Pure logic (Zod schemas, JSDoc examples, no I/O)
6
+ └── shell/ # I/O operations (Result<T, E> return type)
7
+ ```
8
+
9
+ **Key insight:** Core receives data (validated types), Shell handles I/O (files, network).
10
+
11
+ ## Quick Reference
12
+
13
+ | Zone | Requirements |
14
+ |------|-------------|
15
+ | Core | Zod schemas + JSDoc @example, pure (no I/O) |
16
+ | Shell | Returns `Result<T, E>` from `neverthrow` library |
17
+
18
+ ### Core vs Shell (Edge Cases)
19
+
20
+ - fs/path/http/fetch → **Shell**
21
+ - `Date.now()`, `Math.random()` → **Inject param** OR Shell
22
+ - Pure logic → **Core**
23
+
24
+ > Full decision tree: [INVAR.md#core-shell](./INVAR.md#decision-tree-core-vs-shell)
@@ -0,0 +1,25 @@
1
+ ## Check-In
2
+
3
+ > See [INVAR.md#check-in](./INVAR.md#check-in-required) for full protocol.
4
+
5
+ **Your first message MUST display:** `✓ Check-In: [project] | [branch] | [clean/dirty]`
6
+
7
+ **Actions:** Read `.invar/context.md`, then show status. Do NOT run guard at Check-In.
8
+
9
+ ---
10
+
11
+ ## Final
12
+
13
+ Your last message for an implementation task MUST display:
14
+
15
+ ```
16
+ ✓ Final: guard PASS | 0 errors, 2 warnings
17
+ ```
18
+
19
+ {% if syntax == "mcp" -%}
20
+ Execute `invar_guard()` and show this one-line summary.
21
+ {% else -%}
22
+ Execute `invar guard` and show this one-line summary.
23
+ {% endif %}
24
+
25
+ This is your sign-out. Completes the Check-In/Final pair.
@@ -0,0 +1,73 @@
1
+ ## Commands (User-Invokable)
2
+
3
+ | Command | Purpose |
4
+ |---------|---------|
5
+ | `/audit` | Read-only code review (reports issues, no fixes) |
6
+ | `/guard` | Run Invar verification (reports results) |
7
+
8
+ ## Skills (Agent-Invoked)
9
+
10
+ | Skill | Triggers | Purpose |
11
+ |-------|----------|---------|
12
+ | `/investigate` | "why", "explain", vague tasks | Research mode, no code changes |
13
+ | `/propose` | "should we", "compare" | Decision facilitation |
14
+ | `/develop` | "add", "fix", "implement" | USBV implementation workflow |
15
+ | `/review` | After /develop, `review_suggested` | Adversarial review with fix loop |
16
+
17
+ **Note:** Skills are invoked by agent based on context. Use `/audit` for user-initiated review.
18
+
19
+ Guard triggers `review_suggested` for: security-sensitive files, escape hatches >= 3, contract coverage < 50%.
20
+
21
+ ---
22
+
23
+ ## Workflow Routing (MANDATORY)
24
+
25
+ When user message contains these triggers, you MUST use the **Skill tool** to invoke the skill:
26
+
27
+ | Trigger Words | Skill Tool Call | Notes |
28
+ |---------------|-----------------|-------|
29
+ | "review", "review and fix" | `Skill(skill="review")` | Adversarial review with fix loop |
30
+ | "implement", "add", "fix", "update" | `Skill(skill="develop")` | Unless in review context |
31
+ | "why", "explain", "investigate" | `Skill(skill="investigate")` | Research mode, no code changes |
32
+ | "compare", "should we", "design" | `Skill(skill="propose")` | Decision facilitation |
33
+
34
+ **CRITICAL: You must call the Skill tool, not just follow the workflow mentally.**
35
+
36
+ The Skill tool reads `.claude/skills/<skill>/SKILL.md` which contains:
37
+ - Detailed phase instructions (USBV breakdown)
38
+ - Error handling rules
39
+ - Timeout policies
40
+ - Incremental development patterns (DX-63)
41
+
42
+ **Violation check (before writing ANY code):**
43
+ - "Did I call `Skill(skill="...")`?"
44
+ - "Am I following the SKILL.md instructions?"
45
+
46
+ ---
47
+
48
+ ## Routing Control (DX-42)
49
+
50
+ Agent announces routing decision before entering any workflow:
51
+
52
+ ```
53
+ 📍 Routing: /[skill] — [trigger or reason]
54
+ Task: [summary]
55
+ ```
56
+
57
+ **User can redirect with natural language:**
58
+ - "wait" / "stop" — pause and ask for direction
59
+ - "just do it" — proceed with /develop
60
+ - "let's discuss" — switch to /propose
61
+ - "explain first" — switch to /investigate
62
+
63
+ **Simple task optimization:** For simple tasks (single file, clear target, <50 lines), agent may offer:
64
+
65
+ ```
66
+ 📊 Simple task. Auto-orchestrate? [Y/N]
67
+ ```
68
+
69
+ - Y → Full cycle without intermediate confirmations
70
+ - N → Normal step-by-step workflow
71
+
72
+ **Auto-review (DX-41):** When Guard outputs `review_suggested`, agent automatically
73
+ enters /review. Say "skip" to bypass.