invar-tools 1.0.0__py3-none-any.whl → 1.2.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 (57) hide show
  1. invar/core/contracts.py +75 -5
  2. invar/core/entry_points.py +294 -0
  3. invar/core/format_specs.py +196 -0
  4. invar/core/format_strategies.py +197 -0
  5. invar/core/formatter.py +27 -4
  6. invar/core/hypothesis_strategies.py +47 -5
  7. invar/core/lambda_helpers.py +1 -0
  8. invar/core/models.py +23 -17
  9. invar/core/parser.py +6 -2
  10. invar/core/property_gen.py +81 -40
  11. invar/core/purity.py +10 -4
  12. invar/core/review_trigger.py +298 -0
  13. invar/core/rule_meta.py +61 -2
  14. invar/core/rules.py +83 -19
  15. invar/core/shell_analysis.py +252 -0
  16. invar/core/shell_architecture.py +171 -0
  17. invar/core/suggestions.py +6 -0
  18. invar/core/tautology.py +1 -0
  19. invar/core/utils.py +51 -4
  20. invar/core/verification_routing.py +158 -0
  21. invar/invariant.py +1 -0
  22. invar/mcp/server.py +20 -3
  23. invar/shell/cli.py +59 -31
  24. invar/shell/config.py +259 -10
  25. invar/shell/fs.py +5 -2
  26. invar/shell/git.py +2 -0
  27. invar/shell/guard_helpers.py +78 -3
  28. invar/shell/guard_output.py +100 -24
  29. invar/shell/init_cmd.py +27 -7
  30. invar/shell/mcp_config.py +3 -0
  31. invar/shell/mutate_cmd.py +184 -0
  32. invar/shell/mutation.py +314 -0
  33. invar/shell/perception.py +2 -0
  34. invar/shell/property_tests.py +17 -2
  35. invar/shell/prove.py +35 -3
  36. invar/shell/prove_accept.py +113 -0
  37. invar/shell/prove_fallback.py +148 -46
  38. invar/shell/templates.py +34 -0
  39. invar/shell/test_cmd.py +3 -1
  40. invar/shell/testing.py +6 -17
  41. invar/shell/update_cmd.py +2 -0
  42. invar/templates/CLAUDE.md.template +65 -9
  43. invar/templates/INVAR.md +96 -23
  44. invar/templates/aider.conf.yml.template +16 -14
  45. invar/templates/commands/review.md +200 -0
  46. invar/templates/cursorrules.template +22 -13
  47. invar/templates/examples/contracts.py +3 -1
  48. invar/templates/examples/core_shell.py +3 -1
  49. {invar_tools-1.0.0.dist-info → invar_tools-1.2.0.dist-info}/METADATA +81 -15
  50. invar_tools-1.2.0.dist-info/RECORD +77 -0
  51. invar_tools-1.2.0.dist-info/licenses/LICENSE +190 -0
  52. invar_tools-1.2.0.dist-info/licenses/LICENSE-GPL +674 -0
  53. invar_tools-1.2.0.dist-info/licenses/NOTICE +63 -0
  54. invar_tools-1.0.0.dist-info/RECORD +0 -64
  55. invar_tools-1.0.0.dist-info/licenses/LICENSE +0 -21
  56. {invar_tools-1.0.0.dist-info → invar_tools-1.2.0.dist-info}/WHEEL +0 -0
  57. {invar_tools-1.0.0.dist-info → invar_tools-1.2.0.dist-info}/entry_points.txt +0 -0
@@ -3,17 +3,70 @@ Hypothesis fallback for proof verification.
3
3
 
4
4
  DX-12: Provides Hypothesis as automatic fallback when CrossHair
5
5
  is unavailable, times out, or skips files.
6
+
7
+ DX-22: Smart routing - detects C extension imports and routes
8
+ directly to Hypothesis without wasting time on CrossHair.
6
9
  """
7
10
 
8
11
  from __future__ import annotations
9
12
 
10
13
  import subprocess
11
14
  import sys
15
+ from dataclasses import dataclass, field
12
16
  from pathlib import Path
13
17
 
14
18
  from returns.result import Failure, Result, Success
15
19
 
20
+ from invar.core.verification_routing import get_incompatible_imports
21
+
22
+
23
+ @dataclass
24
+ class FileRouting:
25
+ """DX-22: Classification of files for smart verification routing."""
26
+
27
+ crosshair_files: list[Path] = field(default_factory=list)
28
+ hypothesis_files: list[Path] = field(default_factory=list)
29
+ skip_files: list[Path] = field(default_factory=list)
30
+ incompatible_reasons: dict[str, set[str]] = field(default_factory=dict)
31
+
32
+
33
+ # @shell_complexity: File I/O with error handling for import detection
34
+ def classify_files_for_verification(files: list[Path]) -> FileRouting:
35
+ """
36
+ Classify files for smart verification routing.
37
+
38
+ DX-22: Detects C extension imports and routes files appropriately:
39
+ - Pure Python with contracts -> CrossHair (can prove)
40
+ - C extensions (numpy, pandas, etc.) -> Hypothesis (cannot prove)
41
+ - No contracts -> Skip
42
+
43
+ Returns FileRouting with classified files.
44
+ """
45
+ routing = FileRouting()
46
+
47
+ for file_path in files:
48
+ if not file_path.exists() or file_path.suffix != ".py":
49
+ routing.skip_files.append(file_path)
50
+ continue
51
+
52
+ try:
53
+ source = file_path.read_text()
54
+ except Exception:
55
+ routing.skip_files.append(file_path)
56
+ continue
57
+
58
+ # Check for incompatible imports
59
+ incompatible = get_incompatible_imports(source)
60
+ if incompatible:
61
+ routing.hypothesis_files.append(file_path)
62
+ routing.incompatible_reasons[str(file_path)] = incompatible
63
+ else:
64
+ routing.crosshair_files.append(file_path)
65
+
66
+ return routing
67
+
16
68
 
69
+ # @shell_complexity: Fallback verification with hypothesis availability check
17
70
  def run_hypothesis_fallback(
18
71
  files: list[Path],
19
72
  max_examples: int = 100,
@@ -101,6 +154,8 @@ def run_hypothesis_fallback(
101
154
  return Failure(f"Hypothesis error: {e}")
102
155
 
103
156
 
157
+ # @shell_orchestration: DX-22 smart routing + DX-12/13 fallback chain
158
+ # @shell_complexity: Multiple verification phases with error handling paths
104
159
  def run_prove_with_fallback(
105
160
  files: list[Path],
106
161
  crosshair_timeout: int = 10,
@@ -109,9 +164,16 @@ def run_prove_with_fallback(
109
164
  cache_dir: Path | None = None,
110
165
  ) -> Result[dict, str]:
111
166
  """
112
- Run proof verification with automatic Hypothesis fallback.
167
+ Run proof verification with smart routing and automatic fallback.
113
168
 
114
- DX-12 + DX-13: Tries CrossHair first with optimizations, falls back to Hypothesis.
169
+ DX-22: Smart routing - routes C extension code directly to Hypothesis.
170
+ DX-12 + DX-13: CrossHair with caching, falls back to Hypothesis on failure.
171
+
172
+ Flow:
173
+ 1. Classify files (CrossHair-compatible vs C-extension)
174
+ 2. Run CrossHair on compatible files only
175
+ 3. Run Hypothesis on incompatible files (no wasted CrossHair attempt)
176
+ 4. Merge results with de-duplicated statistics
115
177
 
116
178
  Args:
117
179
  files: List of Python file paths to verify
@@ -121,63 +183,103 @@ def run_prove_with_fallback(
121
183
  cache_dir: Cache directory (default: .invar/cache/prove)
122
184
 
123
185
  Returns:
124
- Success with verification results or Failure with error message
186
+ Success with verification results including routing statistics
125
187
  """
126
188
  # Import here to avoid circular import
127
189
  from invar.shell.prove import CrossHairStatus, run_crosshair_parallel
128
190
  from invar.shell.prove_cache import ProveCache
129
191
 
130
- # DX-13: Initialize cache
192
+ # DX-22: Smart routing - classify files before verification
193
+ routing = classify_files_for_verification(files)
194
+
195
+ # Initialize result structure with DX-22 routing stats
196
+ result = {
197
+ "status": "passed",
198
+ "routing": {
199
+ "crosshair_files": len(routing.crosshair_files),
200
+ "hypothesis_files": len(routing.hypothesis_files),
201
+ "skip_files": len(routing.skip_files),
202
+ "incompatible_reasons": {
203
+ k: list(v) for k, v in routing.incompatible_reasons.items()
204
+ },
205
+ },
206
+ "crosshair": None,
207
+ "hypothesis": None,
208
+ "files": [str(f) for f in files],
209
+ }
210
+
211
+ # DX-13: Initialize cache for CrossHair
131
212
  cache = None
132
213
  if use_cache:
133
214
  if cache_dir is None:
134
215
  cache_dir = Path(".invar/cache/prove")
135
216
  cache = ProveCache(cache_dir=cache_dir)
136
217
 
137
- # DX-13: Use parallel CrossHair with caching
138
- crosshair_result = run_crosshair_parallel(
139
- files,
140
- max_iterations=5, # Fast mode
141
- max_workers=None, # Auto-detect
142
- cache=cache,
143
- )
144
-
145
- if isinstance(crosshair_result, Failure):
146
- # CrossHair failed, try Hypothesis
147
- return run_hypothesis_fallback(files, max_examples=hypothesis_max_examples)
148
-
149
- result_data = crosshair_result.unwrap()
150
- status = result_data.get("status", "")
151
-
152
- # Check if we need fallback
153
- needs_fallback = (
154
- status == CrossHairStatus.SKIPPED
155
- or status == CrossHairStatus.TIMEOUT
156
- or "not installed" in result_data.get("reason", "")
157
- )
158
-
159
- if needs_fallback:
160
- # Run Hypothesis as fallback
218
+ # Phase 1: Run CrossHair on compatible files
219
+ if routing.crosshair_files:
220
+ crosshair_result = run_crosshair_parallel(
221
+ routing.crosshair_files,
222
+ max_iterations=5, # Fast mode
223
+ max_workers=None, # Auto-detect
224
+ cache=cache,
225
+ )
226
+
227
+ if isinstance(crosshair_result, Success):
228
+ xh_data = crosshair_result.unwrap()
229
+ result["crosshair"] = xh_data
230
+
231
+ # Check if CrossHair needs fallback for any files
232
+ xh_status = xh_data.get("status", "")
233
+ needs_fallback = (
234
+ xh_status == CrossHairStatus.SKIPPED
235
+ or xh_status == CrossHairStatus.TIMEOUT
236
+ or "not installed" in xh_data.get("reason", "")
237
+ )
238
+
239
+ if needs_fallback:
240
+ # CrossHair failed, add these files to Hypothesis batch
241
+ routing.hypothesis_files.extend(routing.crosshair_files)
242
+ result["crosshair"]["fallback_triggered"] = True
243
+ else:
244
+ # CrossHair error, fallback all to Hypothesis
245
+ routing.hypothesis_files.extend(routing.crosshair_files)
246
+ result["crosshair"] = {
247
+ "status": "error",
248
+ "error": str(crosshair_result.failure()),
249
+ "fallback_triggered": True,
250
+ }
251
+
252
+ # Phase 2: Run Hypothesis on incompatible files + fallback files
253
+ if routing.hypothesis_files:
161
254
  hypothesis_result = run_hypothesis_fallback(
162
- files, max_examples=hypothesis_max_examples
255
+ routing.hypothesis_files, max_examples=hypothesis_max_examples
163
256
  )
164
257
 
165
258
  if isinstance(hypothesis_result, Success):
166
- hyp_data = hypothesis_result.unwrap()
167
- # Merge results
168
- return Success(
169
- {
170
- "status": hyp_data.get("status", "unknown"),
171
- "primary_tool": "hypothesis",
172
- "crosshair_status": status,
173
- "crosshair_reason": result_data.get("reason", ""),
174
- "hypothesis_result": hyp_data,
175
- "files": [str(f) for f in files],
176
- "note": "CrossHair skipped/unavailable, used Hypothesis fallback",
177
- }
178
- )
179
- return hypothesis_result
259
+ result["hypothesis"] = hypothesis_result.unwrap()
260
+ else:
261
+ result["hypothesis"] = {
262
+ "status": "error",
263
+ "error": str(hypothesis_result.failure()),
264
+ }
265
+ result["status"] = "failed"
266
+
267
+ # Determine overall status
268
+ xh_status = result.get("crosshair", {}).get("status", "passed")
269
+ hyp_status = result.get("hypothesis", {}).get("status", "passed")
270
+
271
+ if xh_status == "counterexample_found" or hyp_status == "failed":
272
+ result["status"] = "failed"
273
+ elif xh_status in ("error",) or hyp_status in ("error",):
274
+ result["status"] = "error"
275
+
276
+ # DX-22: Add de-duplicated statistics
277
+ result["stats"] = {
278
+ "crosshair_proven": len(
279
+ result.get("crosshair", {}).get("verified", [])
280
+ ),
281
+ "hypothesis_tested": len(routing.hypothesis_files),
282
+ "total_verified": len(files) - len(routing.skip_files),
283
+ }
180
284
 
181
- # CrossHair succeeded (verified or found counterexample)
182
- result_data["primary_tool"] = "crosshair"
183
- return Success(result_data)
285
+ return Success(result)
invar/shell/templates.py CHANGED
@@ -53,6 +53,7 @@ def get_template_path(name: str) -> Result[Path, str]:
53
53
  return Failure(f"Failed to get template path: {e}")
54
54
 
55
55
 
56
+ # @shell_complexity: Template copy with path resolution
56
57
  def copy_template(
57
58
  template_name: str, dest: Path, dest_name: str | None = None
58
59
  ) -> Result[bool, str]:
@@ -73,6 +74,7 @@ def copy_template(
73
74
  return Failure(f"Failed to copy template: {e}")
74
75
 
75
76
 
77
+ # @shell_complexity: Config addition with existing file detection
76
78
  def add_config(path: Path, console) -> Result[bool, str]:
77
79
  """Add configuration to project. Returns Success(True) if added, Success(False) if skipped."""
78
80
  pyproject = path / "pyproject.toml"
@@ -114,6 +116,7 @@ def create_directories(path: Path, console) -> None:
114
116
  console.print("[green]Created[/green] src/shell/")
115
117
 
116
118
 
119
+ # @shell_complexity: Directory copy with file filtering
117
120
  def copy_examples_directory(dest: Path, console) -> Result[bool, str]:
118
121
  """Copy examples directory to .invar/examples/. Returns Success(True) if copied."""
119
122
  import shutil
@@ -139,6 +142,32 @@ def copy_examples_directory(dest: Path, console) -> Result[bool, str]:
139
142
  return Failure(f"Failed to copy examples: {e}")
140
143
 
141
144
 
145
+ # @shell_complexity: Directory copy for Claude commands (DX-32)
146
+ def copy_commands_directory(dest: Path, console) -> Result[bool, str]:
147
+ """Copy commands directory to .claude/commands/. Returns Success(True) if copied."""
148
+ import shutil
149
+
150
+ commands_dest = dest / ".claude" / "commands"
151
+ if commands_dest.exists():
152
+ return Success(False)
153
+
154
+ try:
155
+ commands_src = Path(str(resources.files("invar.templates").joinpath("commands")))
156
+ if not commands_src.exists():
157
+ return Failure("Commands template directory not found")
158
+
159
+ # Create .claude if needed
160
+ claude_dir = dest / ".claude"
161
+ if not claude_dir.exists():
162
+ claude_dir.mkdir()
163
+
164
+ shutil.copytree(commands_src, commands_dest)
165
+ console.print("[green]Created[/green] .claude/commands/ (Claude Code skills)")
166
+ return Success(True)
167
+ except OSError as e:
168
+ return Failure(f"Failed to copy commands: {e}")
169
+
170
+
142
171
  # Agent configuration for multi-agent support (DX-11, DX-17)
143
172
  AGENT_CONFIGS = {
144
173
  "claude": {
@@ -162,6 +191,7 @@ AGENT_CONFIGS = {
162
191
  }
163
192
 
164
193
 
194
+ # @shell_complexity: Agent config detection across multiple locations
165
195
  def detect_agent_configs(path: Path) -> Result[dict[str, str], str]:
166
196
  """
167
197
  Detect existing agent configuration files.
@@ -195,6 +225,7 @@ def detect_agent_configs(path: Path) -> Result[dict[str, str], str]:
195
225
  return Failure(f"Failed to detect agent configs: {e}")
196
226
 
197
227
 
228
+ # @shell_complexity: Reference addition with existing check
198
229
  def add_invar_reference(path: Path, agent: str, console) -> Result[bool, str]:
199
230
  """Add Invar reference to an existing agent config file."""
200
231
  if agent not in AGENT_CONFIGS:
@@ -220,6 +251,7 @@ def add_invar_reference(path: Path, agent: str, console) -> Result[bool, str]:
220
251
  return Failure(f"Failed to update {config['file']}: {e}")
221
252
 
222
253
 
254
+ # @shell_complexity: Config creation with template selection
223
255
  def create_agent_config(path: Path, agent: str, console) -> Result[bool, str]:
224
256
  """
225
257
  Create agent config from template (DX-17).
@@ -248,6 +280,7 @@ def create_agent_config(path: Path, agent: str, console) -> Result[bool, str]:
248
280
  return Success(False)
249
281
 
250
282
 
283
+ # @shell_complexity: MCP server config with JSON manipulation
251
284
  def configure_mcp_server(path: Path, console) -> Result[list[str], str]:
252
285
  """
253
286
  Configure MCP server for AI agents (DX-16).
@@ -407,6 +440,7 @@ The server communicates via stdio and should be managed by your AI agent.
407
440
  """
408
441
 
409
442
 
443
+ # @shell_complexity: Git hooks installation with backup
410
444
  def install_hooks(path: Path, console) -> Result[bool, str]:
411
445
  """Install pre-commit hooks configuration and activate them."""
412
446
  import subprocess
invar/shell/test_cmd.py CHANGED
@@ -26,6 +26,7 @@ def _detect_agent_mode() -> bool:
26
26
  return os.getenv("INVAR_MODE") == "agent" or not sys.stdout.isatty()
27
27
 
28
28
 
29
+ # @shell_complexity: Test command with file collection and output
29
30
  def test(
30
31
  target: str = typer.Argument(None, help="File to test (optional with --changed)"),
31
32
  verbose: bool = typer.Option(False, "-v", "--verbose", help="Verbose output"),
@@ -33,7 +34,7 @@ def test(
33
34
  changed: bool = typer.Option(False, "--changed", help="Test git-modified files only"),
34
35
  max_examples: int = typer.Option(100, "--max-examples", help="Maximum Hypothesis examples per function"),
35
36
  ) -> None:
36
- """Run property-based tests using Hypothesis on contracted functions (DX-08)."""
37
+ """Run property-based tests using Hypothesis on contracted functions."""
37
38
  from invar.shell.property_tests import (
38
39
  format_property_test_report,
39
40
  run_property_tests_on_files,
@@ -75,6 +76,7 @@ def test(
75
76
  raise typer.Exit(1)
76
77
 
77
78
 
79
+ # @shell_complexity: Verify command with CrossHair integration
78
80
  def verify(
79
81
  target: str = typer.Argument(None, help="File to verify (optional with --changed)"),
80
82
  timeout: int = typer.Option(30, "--timeout", help="Timeout per function (seconds)"),
invar/shell/testing.py CHANGED
@@ -40,7 +40,6 @@ __all__ = [
40
40
  "ProveCache",
41
41
  "VerificationLevel",
42
42
  "VerificationResult",
43
- "detect_verification_context",
44
43
  "get_available_verifiers",
45
44
  "get_files_to_prove",
46
45
  "run_crosshair_on_files",
@@ -80,6 +79,7 @@ class VerificationResult:
80
79
  errors: list[str] = field(default_factory=list)
81
80
 
82
81
 
82
+ # @shell_orchestration: Verifier discovery helper
83
83
  def get_available_verifiers() -> list[str]:
84
84
  """
85
85
  Detect installed verification tools.
@@ -111,21 +111,7 @@ def get_available_verifiers() -> list[str]:
111
111
  return available
112
112
 
113
113
 
114
- def detect_verification_context() -> VerificationLevel:
115
- """
116
- Auto-detect appropriate verification depth based on context.
117
-
118
- DX-19: Simplified to 2 levels. Always returns STANDARD (full verification).
119
- STATIC is only used when explicitly requested via --static flag.
120
-
121
- >>> detect_verification_context() == VerificationLevel.STANDARD
122
- True
123
- """
124
- # DX-19: Always use STANDARD (full verification) by default
125
- # STATIC is only for explicit --static flag
126
- return VerificationLevel.STANDARD
127
-
128
-
114
+ # @shell_complexity: Doctest execution with subprocess and result parsing
129
115
  def run_doctests_on_files(
130
116
  files: list[Path], verbose: bool = False
131
117
  ) -> Result[dict, str]:
@@ -173,6 +159,7 @@ def run_doctests_on_files(
173
159
  return Failure(f"Doctest error: {e}")
174
160
 
175
161
 
162
+ # @shell_complexity: Property test orchestration with subprocess
176
163
  def run_test(
177
164
  target: str, json_output: bool = False, verbose: bool = False
178
165
  ) -> Result[dict, str]:
@@ -230,6 +217,7 @@ def run_test(
230
217
  return Failure(f"Test error: {e}")
231
218
 
232
219
 
220
+ # @shell_complexity: CrossHair verification with subprocess
233
221
  def run_verify(
234
222
  target: str, json_output: bool = False, timeout: int = 30
235
223
  ) -> Result[dict, str]:
@@ -266,9 +254,10 @@ def run_verify(
266
254
  try:
267
255
  result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout * 10)
268
256
 
257
+ # CrossHair format: "file:line: error: Err when calling func(...)"
269
258
  counterexamples = [
270
259
  line.strip() for line in result.stdout.split("\n")
271
- if "error" in line.lower() or "counterexample" in line.lower()
260
+ if ": error:" in line.lower() or "counterexample" in line.lower()
272
261
  ]
273
262
 
274
263
  verify_result = {
invar/shell/update_cmd.py CHANGED
@@ -21,6 +21,7 @@ console = Console()
21
21
  VERSION_PATTERN = re.compile(r"v(\d+)\.(\d+)(?:\.(\d+))?")
22
22
 
23
23
 
24
+ # @shell_orchestration: Version parsing helper for update command
24
25
  def parse_version(text: str) -> tuple[int, int, int] | None:
25
26
  """
26
27
  Parse version string from text.
@@ -113,6 +114,7 @@ def update_examples(path: Path, console: Console) -> Result[bool, str]:
113
114
  return copy_examples_directory(path, console)
114
115
 
115
116
 
117
+ # @shell_complexity: Update command with template comparison
116
118
  def update(
117
119
  path: Path = typer.Argument(Path(), help="Project root directory"),
118
120
  force: bool = typer.Option(
@@ -1,21 +1,40 @@
1
1
  # Project Development Guide
2
2
 
3
- > **Protocol:** Follow [INVAR.md](./INVAR.md) — includes Session Start, ICIDIV workflow, and Task Completion requirements.
3
+ > **Protocol:** Follow [INVAR.md](./INVAR.md) — includes Check-In, USBV workflow, and Task Completion requirements.
4
4
 
5
- ## Claude-Specific: Entry Verification
5
+ ## Check-In
6
6
 
7
- Your **first message** for any implementation task MUST include actual output from:
7
+ Your first message MUST display:
8
8
 
9
9
  ```
10
- invar_guard(changed=true) # or: invar guard --changed
11
- invar_map(top=10) # or: invar map --top 10
10
+ Check-In: guard PASS | top: <entry1>, <entry2>
12
11
  ```
13
12
 
14
- **Use MCP tools if available**, otherwise use CLI commands.
13
+ Execute `invar_guard(changed=true)` and `invar_map(top=10)`, then show this one-line summary.
15
14
 
16
- No output = Session not started correctly. Stop, execute tools, restart.
15
+ Example:
16
+ ```
17
+ ✓ Check-In: guard PASS | top: parse_file, check_rules
18
+ ```
17
19
 
18
- This ensures you've followed the Session Start requirements in INVAR.md.
20
+ This is your sign-in. The user sees it immediately.
21
+ No visible check-in = Session not started.
22
+
23
+ Then read `.invar/context.md` for project state and lessons learned.
24
+
25
+ ---
26
+
27
+ ## Final
28
+
29
+ Your last message for an implementation task MUST display:
30
+
31
+ ```
32
+ ✓ Final: guard PASS | 0 errors, 2 warnings
33
+ ```
34
+
35
+ Execute `invar_guard()` and show this one-line summary.
36
+
37
+ This is your sign-out. Completes the Check-In/Final pair.
19
38
 
20
39
  ---
21
40
 
@@ -43,7 +62,44 @@ src/{project}/
43
62
  | INVAR.md | Invar | No | Protocol (`invar update` to sync) |
44
63
  | CLAUDE.md | User | Yes | Project customization (this file) |
45
64
  | .invar/context.md | User | Yes | Project state, lessons learned |
46
- | .invar/examples/ | Invar | No | **Must read:** Core/Shell patterns |
65
+ | .invar/examples/ | Invar | No | **Must read:** Core/Shell patterns, workflow |
66
+
67
+ ## Visible Workflow (DX-30)
68
+
69
+ For complex tasks (3+ functions), show 3 checkpoints in TodoList:
70
+
71
+ ```
72
+ □ [UNDERSTAND] Task description, codebase context, constraints
73
+ □ [SPECIFY] Contracts (@pre/@post) and design decomposition
74
+ □ [VALIDATE] Guard results, Review Gate status, integration status
75
+ ```
76
+
77
+ **BUILD is internal work** — not shown in TodoList.
78
+
79
+ **Show contracts before code.** See `.invar/examples/workflow.md` for full example.
80
+
81
+ ---
82
+
83
+ ## Agent Roles
84
+
85
+ | Command | Role | Purpose |
86
+ |---------|------|---------|
87
+ | `/review` | Reviewer | Adversarial code review (DX-31) |
88
+
89
+ ### Review Modes (Auto-Selected)
90
+
91
+ `/review` automatically selects mode based on Guard output:
92
+
93
+ | Condition | Mode | Behavior |
94
+ |-----------|------|----------|
95
+ | `review_suggested` triggered | **Isolated** | Task tool sub-agent (fresh context) |
96
+ | No trigger | **Quick** | Same-context adversarial review |
97
+ | `--isolated` flag | **Isolated** | Force isolation |
98
+ | `--quick` flag | **Quick** | Force same-context |
99
+
100
+ Guard triggers `review_suggested` for: security-sensitive files, escape hatches >= 3, contract coverage < 50%.
101
+
102
+ ---
47
103
 
48
104
  ## Project-Specific Rules
49
105