enigma-cli 1.15.1 → 1.15.3

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 (45) hide show
  1. package/README.md +37 -0
  2. package/assets/commands/improve.md +81 -0
  3. package/assets/memory/AGENTS.md +1 -1
  4. package/assets/memory/CLAUDE.md +1 -1
  5. package/assets/skills/anti-overengineering-policy/skill.json +1 -1
  6. package/assets/skills/anti-overengineering-review/skill.json +1 -1
  7. package/assets/skills/backend-policy/skill.json +1 -1
  8. package/assets/skills/ciphera-style-policy/skill.json +1 -1
  9. package/assets/skills/code-review-policy/skill.json +1 -1
  10. package/assets/skills/core-engineering-policy/SKILL.md +2 -4
  11. package/assets/skills/core-engineering-policy/skill.json +3 -3
  12. package/assets/skills/database-expert/skill.json +1 -1
  13. package/assets/skills/debugging-policy/skill.json +1 -1
  14. package/assets/skills/dependency-policy/skill.json +1 -1
  15. package/assets/skills/frontend-design/SKILL.md +54 -0
  16. package/assets/skills/frontend-design/skill.json +8 -0
  17. package/assets/skills/frontend-policy/skill.json +1 -1
  18. package/assets/skills/git-policy/skill.json +1 -1
  19. package/assets/skills/security-policy/skill.json +1 -1
  20. package/assets/skills/skill-creator/SKILL.md +485 -0
  21. package/assets/skills/skill-creator/agents/analyzer.md +274 -0
  22. package/assets/skills/skill-creator/agents/comparator.md +202 -0
  23. package/assets/skills/skill-creator/agents/grader.md +223 -0
  24. package/assets/skills/skill-creator/assets/eval_review.html +146 -0
  25. package/assets/skills/skill-creator/eval-viewer/generate_review.py +471 -0
  26. package/assets/skills/skill-creator/eval-viewer/viewer.html +1325 -0
  27. package/assets/skills/skill-creator/references/schemas.md +430 -0
  28. package/assets/skills/skill-creator/scripts/__init__.py +0 -0
  29. package/assets/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  30. package/assets/skills/skill-creator/scripts/generate_report.py +326 -0
  31. package/assets/skills/skill-creator/scripts/improve_description.py +247 -0
  32. package/assets/skills/skill-creator/scripts/package_skill.py +136 -0
  33. package/assets/skills/skill-creator/scripts/quick_validate.py +103 -0
  34. package/assets/skills/skill-creator/scripts/run_eval.py +310 -0
  35. package/assets/skills/skill-creator/scripts/run_loop.py +328 -0
  36. package/assets/skills/skill-creator/scripts/utils.py +47 -0
  37. package/assets/skills/skill-creator/skill.json +8 -0
  38. package/assets/skills/task-completion-policy/skill.json +1 -1
  39. package/assets/skills/testing-policy/SKILL.md +81 -4
  40. package/assets/skills/testing-policy/skill.json +4 -4
  41. package/assets/skills/validation-policy/skill.json +1 -1
  42. package/bin/checksums.json +4 -4
  43. package/package.json +1 -1
  44. package/assets/skills/test-organization-policy/SKILL.md +0 -88
  45. package/assets/skills/test-organization-policy/skill.json +0 -8
@@ -0,0 +1,328 @@
1
+ #!/usr/bin/env python3
2
+ """Run the eval + improve loop until all pass or max iterations reached.
3
+
4
+ Combines run_eval.py and improve_description.py in a loop, tracking history
5
+ and returning the best description found. Supports train/test split to prevent
6
+ overfitting.
7
+ """
8
+
9
+ import argparse
10
+ import json
11
+ import random
12
+ import sys
13
+ import tempfile
14
+ import time
15
+ import webbrowser
16
+ from pathlib import Path
17
+
18
+ from scripts.generate_report import generate_html
19
+ from scripts.improve_description import improve_description
20
+ from scripts.run_eval import find_project_root, run_eval
21
+ from scripts.utils import parse_skill_md
22
+
23
+
24
+ def split_eval_set(eval_set: list[dict], holdout: float, seed: int = 42) -> tuple[list[dict], list[dict]]:
25
+ """Split eval set into train and test sets, stratified by should_trigger."""
26
+ random.seed(seed)
27
+
28
+ # Separate by should_trigger
29
+ trigger = [e for e in eval_set if e["should_trigger"]]
30
+ no_trigger = [e for e in eval_set if not e["should_trigger"]]
31
+
32
+ # Shuffle each group
33
+ random.shuffle(trigger)
34
+ random.shuffle(no_trigger)
35
+
36
+ # Calculate split points
37
+ n_trigger_test = max(1, int(len(trigger) * holdout))
38
+ n_no_trigger_test = max(1, int(len(no_trigger) * holdout))
39
+
40
+ # Split
41
+ test_set = trigger[:n_trigger_test] + no_trigger[:n_no_trigger_test]
42
+ train_set = trigger[n_trigger_test:] + no_trigger[n_no_trigger_test:]
43
+
44
+ return train_set, test_set
45
+
46
+
47
+ def run_loop(
48
+ eval_set: list[dict],
49
+ skill_path: Path,
50
+ description_override: str | None,
51
+ num_workers: int,
52
+ timeout: int,
53
+ max_iterations: int,
54
+ runs_per_query: int,
55
+ trigger_threshold: float,
56
+ holdout: float,
57
+ model: str,
58
+ verbose: bool,
59
+ live_report_path: Path | None = None,
60
+ log_dir: Path | None = None,
61
+ ) -> dict:
62
+ """Run the eval + improvement loop."""
63
+ project_root = find_project_root()
64
+ name, original_description, content = parse_skill_md(skill_path)
65
+ current_description = description_override or original_description
66
+
67
+ # Split into train/test if holdout > 0
68
+ if holdout > 0:
69
+ train_set, test_set = split_eval_set(eval_set, holdout)
70
+ if verbose:
71
+ print(f"Split: {len(train_set)} train, {len(test_set)} test (holdout={holdout})", file=sys.stderr)
72
+ else:
73
+ train_set = eval_set
74
+ test_set = []
75
+
76
+ history = []
77
+ exit_reason = "unknown"
78
+
79
+ for iteration in range(1, max_iterations + 1):
80
+ if verbose:
81
+ print(f"\n{'='*60}", file=sys.stderr)
82
+ print(f"Iteration {iteration}/{max_iterations}", file=sys.stderr)
83
+ print(f"Description: {current_description}", file=sys.stderr)
84
+ print(f"{'='*60}", file=sys.stderr)
85
+
86
+ # Evaluate train + test together in one batch for parallelism
87
+ all_queries = train_set + test_set
88
+ t0 = time.time()
89
+ all_results = run_eval(
90
+ eval_set=all_queries,
91
+ skill_name=name,
92
+ description=current_description,
93
+ num_workers=num_workers,
94
+ timeout=timeout,
95
+ project_root=project_root,
96
+ runs_per_query=runs_per_query,
97
+ trigger_threshold=trigger_threshold,
98
+ model=model,
99
+ )
100
+ eval_elapsed = time.time() - t0
101
+
102
+ # Split results back into train/test by matching queries
103
+ train_queries_set = {q["query"] for q in train_set}
104
+ train_result_list = [r for r in all_results["results"] if r["query"] in train_queries_set]
105
+ test_result_list = [r for r in all_results["results"] if r["query"] not in train_queries_set]
106
+
107
+ train_passed = sum(1 for r in train_result_list if r["pass"])
108
+ train_total = len(train_result_list)
109
+ train_summary = {"passed": train_passed, "failed": train_total - train_passed, "total": train_total}
110
+ train_results = {"results": train_result_list, "summary": train_summary}
111
+
112
+ if test_set:
113
+ test_passed = sum(1 for r in test_result_list if r["pass"])
114
+ test_total = len(test_result_list)
115
+ test_summary = {"passed": test_passed, "failed": test_total - test_passed, "total": test_total}
116
+ test_results = {"results": test_result_list, "summary": test_summary}
117
+ else:
118
+ test_results = None
119
+ test_summary = None
120
+
121
+ history.append({
122
+ "iteration": iteration,
123
+ "description": current_description,
124
+ "train_passed": train_summary["passed"],
125
+ "train_failed": train_summary["failed"],
126
+ "train_total": train_summary["total"],
127
+ "train_results": train_results["results"],
128
+ "test_passed": test_summary["passed"] if test_summary else None,
129
+ "test_failed": test_summary["failed"] if test_summary else None,
130
+ "test_total": test_summary["total"] if test_summary else None,
131
+ "test_results": test_results["results"] if test_results else None,
132
+ # For backward compat with report generator
133
+ "passed": train_summary["passed"],
134
+ "failed": train_summary["failed"],
135
+ "total": train_summary["total"],
136
+ "results": train_results["results"],
137
+ })
138
+
139
+ # Write live report if path provided
140
+ if live_report_path:
141
+ partial_output = {
142
+ "original_description": original_description,
143
+ "best_description": current_description,
144
+ "best_score": "in progress",
145
+ "iterations_run": len(history),
146
+ "holdout": holdout,
147
+ "train_size": len(train_set),
148
+ "test_size": len(test_set),
149
+ "history": history,
150
+ }
151
+ live_report_path.write_text(generate_html(partial_output, auto_refresh=True, skill_name=name))
152
+
153
+ if verbose:
154
+ def print_eval_stats(label, results, elapsed):
155
+ pos = [r for r in results if r["should_trigger"]]
156
+ neg = [r for r in results if not r["should_trigger"]]
157
+ tp = sum(r["triggers"] for r in pos)
158
+ pos_runs = sum(r["runs"] for r in pos)
159
+ fn = pos_runs - tp
160
+ fp = sum(r["triggers"] for r in neg)
161
+ neg_runs = sum(r["runs"] for r in neg)
162
+ tn = neg_runs - fp
163
+ total = tp + tn + fp + fn
164
+ precision = tp / (tp + fp) if (tp + fp) > 0 else 1.0
165
+ recall = tp / (tp + fn) if (tp + fn) > 0 else 1.0
166
+ accuracy = (tp + tn) / total if total > 0 else 0.0
167
+ print(f"{label}: {tp+tn}/{total} correct, precision={precision:.0%} recall={recall:.0%} accuracy={accuracy:.0%} ({elapsed:.1f}s)", file=sys.stderr)
168
+ for r in results:
169
+ status = "PASS" if r["pass"] else "FAIL"
170
+ rate_str = f"{r['triggers']}/{r['runs']}"
171
+ print(f" [{status}] rate={rate_str} expected={r['should_trigger']}: {r['query'][:60]}", file=sys.stderr)
172
+
173
+ print_eval_stats("Train", train_results["results"], eval_elapsed)
174
+ if test_summary:
175
+ print_eval_stats("Test ", test_results["results"], 0)
176
+
177
+ if train_summary["failed"] == 0:
178
+ exit_reason = f"all_passed (iteration {iteration})"
179
+ if verbose:
180
+ print(f"\nAll train queries passed on iteration {iteration}!", file=sys.stderr)
181
+ break
182
+
183
+ if iteration == max_iterations:
184
+ exit_reason = f"max_iterations ({max_iterations})"
185
+ if verbose:
186
+ print(f"\nMax iterations reached ({max_iterations}).", file=sys.stderr)
187
+ break
188
+
189
+ # Improve the description based on train results
190
+ if verbose:
191
+ print(f"\nImproving description...", file=sys.stderr)
192
+
193
+ t0 = time.time()
194
+ # Strip test scores from history so improvement model can't see them
195
+ blinded_history = [
196
+ {k: v for k, v in h.items() if not k.startswith("test_")}
197
+ for h in history
198
+ ]
199
+ new_description = improve_description(
200
+ skill_name=name,
201
+ skill_content=content,
202
+ current_description=current_description,
203
+ eval_results=train_results,
204
+ history=blinded_history,
205
+ model=model,
206
+ log_dir=log_dir,
207
+ iteration=iteration,
208
+ )
209
+ improve_elapsed = time.time() - t0
210
+
211
+ if verbose:
212
+ print(f"Proposed ({improve_elapsed:.1f}s): {new_description}", file=sys.stderr)
213
+
214
+ current_description = new_description
215
+
216
+ # Find the best iteration by TEST score (or train if no test set)
217
+ if test_set:
218
+ best = max(history, key=lambda h: h["test_passed"] or 0)
219
+ best_score = f"{best['test_passed']}/{best['test_total']}"
220
+ else:
221
+ best = max(history, key=lambda h: h["train_passed"])
222
+ best_score = f"{best['train_passed']}/{best['train_total']}"
223
+
224
+ if verbose:
225
+ print(f"\nExit reason: {exit_reason}", file=sys.stderr)
226
+ print(f"Best score: {best_score} (iteration {best['iteration']})", file=sys.stderr)
227
+
228
+ return {
229
+ "exit_reason": exit_reason,
230
+ "original_description": original_description,
231
+ "best_description": best["description"],
232
+ "best_score": best_score,
233
+ "best_train_score": f"{best['train_passed']}/{best['train_total']}",
234
+ "best_test_score": f"{best['test_passed']}/{best['test_total']}" if test_set else None,
235
+ "final_description": current_description,
236
+ "iterations_run": len(history),
237
+ "holdout": holdout,
238
+ "train_size": len(train_set),
239
+ "test_size": len(test_set),
240
+ "history": history,
241
+ }
242
+
243
+
244
+ def main():
245
+ parser = argparse.ArgumentParser(description="Run eval + improve loop")
246
+ parser.add_argument("--eval-set", required=True, help="Path to eval set JSON file")
247
+ parser.add_argument("--skill-path", required=True, help="Path to skill directory")
248
+ parser.add_argument("--description", default=None, help="Override starting description")
249
+ parser.add_argument("--num-workers", type=int, default=10, help="Number of parallel workers")
250
+ parser.add_argument("--timeout", type=int, default=30, help="Timeout per query in seconds")
251
+ parser.add_argument("--max-iterations", type=int, default=5, help="Max improvement iterations")
252
+ parser.add_argument("--runs-per-query", type=int, default=3, help="Number of runs per query")
253
+ parser.add_argument("--trigger-threshold", type=float, default=0.5, help="Trigger rate threshold")
254
+ parser.add_argument("--holdout", type=float, default=0.4, help="Fraction of eval set to hold out for testing (0 to disable)")
255
+ parser.add_argument("--model", required=True, help="Model for improvement")
256
+ parser.add_argument("--verbose", action="store_true", help="Print progress to stderr")
257
+ parser.add_argument("--report", default="auto", help="Generate HTML report at this path (default: 'auto' for temp file, 'none' to disable)")
258
+ parser.add_argument("--results-dir", default=None, help="Save all outputs (results.json, report.html, log.txt) to a timestamped subdirectory here")
259
+ args = parser.parse_args()
260
+
261
+ eval_set = json.loads(Path(args.eval_set).read_text())
262
+ skill_path = Path(args.skill_path)
263
+
264
+ if not (skill_path / "SKILL.md").exists():
265
+ print(f"Error: No SKILL.md found at {skill_path}", file=sys.stderr)
266
+ sys.exit(1)
267
+
268
+ name, _, _ = parse_skill_md(skill_path)
269
+
270
+ # Set up live report path
271
+ if args.report != "none":
272
+ if args.report == "auto":
273
+ timestamp = time.strftime("%Y%m%d_%H%M%S")
274
+ live_report_path = Path(tempfile.gettempdir()) / f"skill_description_report_{skill_path.name}_{timestamp}.html"
275
+ else:
276
+ live_report_path = Path(args.report)
277
+ # Open the report immediately so the user can watch
278
+ live_report_path.write_text("<html><body><h1>Starting optimization loop...</h1><meta http-equiv='refresh' content='5'></body></html>")
279
+ webbrowser.open(str(live_report_path))
280
+ else:
281
+ live_report_path = None
282
+
283
+ # Determine output directory (create before run_loop so logs can be written)
284
+ if args.results_dir:
285
+ timestamp = time.strftime("%Y-%m-%d_%H%M%S")
286
+ results_dir = Path(args.results_dir) / timestamp
287
+ results_dir.mkdir(parents=True, exist_ok=True)
288
+ else:
289
+ results_dir = None
290
+
291
+ log_dir = results_dir / "logs" if results_dir else None
292
+
293
+ output = run_loop(
294
+ eval_set=eval_set,
295
+ skill_path=skill_path,
296
+ description_override=args.description,
297
+ num_workers=args.num_workers,
298
+ timeout=args.timeout,
299
+ max_iterations=args.max_iterations,
300
+ runs_per_query=args.runs_per_query,
301
+ trigger_threshold=args.trigger_threshold,
302
+ holdout=args.holdout,
303
+ model=args.model,
304
+ verbose=args.verbose,
305
+ live_report_path=live_report_path,
306
+ log_dir=log_dir,
307
+ )
308
+
309
+ # Save JSON output
310
+ json_output = json.dumps(output, indent=2)
311
+ print(json_output)
312
+ if results_dir:
313
+ (results_dir / "results.json").write_text(json_output)
314
+
315
+ # Write final HTML report (without auto-refresh)
316
+ if live_report_path:
317
+ live_report_path.write_text(generate_html(output, auto_refresh=False, skill_name=name))
318
+ print(f"\nReport: {live_report_path}", file=sys.stderr)
319
+
320
+ if results_dir and live_report_path:
321
+ (results_dir / "report.html").write_text(generate_html(output, auto_refresh=False, skill_name=name))
322
+
323
+ if results_dir:
324
+ print(f"Results saved to: {results_dir}", file=sys.stderr)
325
+
326
+
327
+ if __name__ == "__main__":
328
+ main()
@@ -0,0 +1,47 @@
1
+ """Shared utilities for skill-creator scripts."""
2
+
3
+ from pathlib import Path
4
+
5
+
6
+
7
+ def parse_skill_md(skill_path: Path) -> tuple[str, str, str]:
8
+ """Parse a SKILL.md file, returning (name, description, full_content)."""
9
+ content = (skill_path / "SKILL.md").read_text()
10
+ lines = content.split("\n")
11
+
12
+ if lines[0].strip() != "---":
13
+ raise ValueError("SKILL.md missing frontmatter (no opening ---)")
14
+
15
+ end_idx = None
16
+ for i, line in enumerate(lines[1:], start=1):
17
+ if line.strip() == "---":
18
+ end_idx = i
19
+ break
20
+
21
+ if end_idx is None:
22
+ raise ValueError("SKILL.md missing frontmatter (no closing ---)")
23
+
24
+ name = ""
25
+ description = ""
26
+ frontmatter_lines = lines[1:end_idx]
27
+ i = 0
28
+ while i < len(frontmatter_lines):
29
+ line = frontmatter_lines[i]
30
+ if line.startswith("name:"):
31
+ name = line[len("name:"):].strip().strip('"').strip("'")
32
+ elif line.startswith("description:"):
33
+ value = line[len("description:"):].strip()
34
+ # Handle YAML multiline indicators (>, |, >-, |-)
35
+ if value in (">", "|", ">-", "|-"):
36
+ continuation_lines: list[str] = []
37
+ i += 1
38
+ while i < len(frontmatter_lines) and (frontmatter_lines[i].startswith(" ") or frontmatter_lines[i].startswith("\t")):
39
+ continuation_lines.append(frontmatter_lines[i].strip())
40
+ i += 1
41
+ description = " ".join(continuation_lines)
42
+ continue
43
+ else:
44
+ description = value.strip('"').strip("'")
45
+ i += 1
46
+
47
+ return name, description, content
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "skill-creator",
3
+ "version": "1.0.0",
4
+ "provider": "FJRG2007/enigma",
5
+ "description": "Create new skills, modify and improve existing skills, and measure skill performance with evals and benchmarks.",
6
+ "cliVersion": "1.15.3",
7
+ "sha": "699586cce82ec0a5458288b598ee7e5ebdddb3dfcf19db354d8bc5e85e47c1c7"
8
+ }
@@ -3,6 +3,6 @@
3
3
  "version": "1.1.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Exhaustive completion discipline for long/multi-item tasks - inventory, coverage ledger, verified done.",
6
- "cliVersion": "1.15.1",
6
+ "cliVersion": "1.15.3",
7
7
  "sha": "6e3facba307eb2b55cefbab2e4b2a346a2b82f93c3ef47e11ebeb78c3c9453a8"
8
8
  }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: testing-policy
3
- description: Test strategy (test pyramid), coverage gates, deterministic tests, mocking discipline, and regression-first bug fixing. Use when writing or changing code that needs tests, when asked to add or fix tests, or after fixing a bug to add a regression test.
3
+ description: Test strategy (test pyramid), coverage gates, deterministic tests, mocking discipline, regression-first bug fixing, and test-suite organization (directory structure by test type and domain, mirrored source paths, file naming, fixture/helper/factory placement). Use when writing or changing code that needs tests, when asked to add or fix tests, after fixing a bug to add a regression test, or when creating, moving, renaming, or structuring test files - never dump tests flat into a single tests/ folder.
4
4
  ---
5
5
 
6
6
  # Testing Policy (Senior Engineering Standards)
@@ -8,8 +8,9 @@ description: Test strategy (test pyramid), coverage gates, deterministic tests,
8
8
  ## Activation Scope
9
9
 
10
10
  - Apply whenever code is written, changed, or fixed, and whenever the user asks for tests.
11
- - Owns test strategy, coverage expectations, determinism, and test-first discipline.
12
- - Test file placement, suite layout, naming of test files, and fixture/helper organization live in test-organization-policy; apply it alongside this skill when creating or moving test files.
11
+ - Apply whenever a test file is created, moved, or renamed, and whenever a test suite is scaffolded or restructured.
12
+ - Owns test strategy, coverage expectations, determinism, mocking discipline, and test-first discipline.
13
+ - Owns test-suite layout: directory structure, subfolders, file naming, and placement of fixtures, helpers, factories, and mocks.
13
14
 
14
15
  ---
15
16
 
@@ -70,8 +71,84 @@ description: Test strategy (test pyramid), coverage gates, deterministic tests,
70
71
 
71
72
  ---
72
73
 
74
+ ## Suite Organization: Core Principle
75
+
76
+ - A test suite is navigable code: anyone must find the tests for a module in seconds from the module's path alone, and find the module from its test's path.
77
+ - Never dump test files flat into a single tests/ folder. Flat suites hide coverage gaps, cause name collisions, and stop scaling past a handful of files.
78
+ - Organize from the first test file. Structure is cheapest at file creation and most expensive after the folder is a mess.
79
+
80
+ ---
81
+
82
+ ## Framework Convention First
83
+
84
+ - Every ecosystem has an established convention; it beats any custom layout:
85
+ - JS/TS (Vitest, Jest, Bun): colocated `*.test.ts` next to the source file, a `__tests__/` folder per directory, or a mirrored `tests/` tree - follow whichever the repo already uses.
86
+ - Python (pytest): a `tests/` package mirroring the source package; shared fixtures in `conftest.py` at the narrowest directory that covers their users.
87
+ - Go: `_test.go` colocated in the same package (mandated by the toolchain); black-box tests use the `_test` package suffix.
88
+ - Rust: unit tests in `#[cfg(test)] mod tests` inside the module; integration tests as separate files under the crate-root `tests/`.
89
+ - Java/Kotlin: `src/test/<lang>/` mirroring the `src/main/<lang>/` package path exactly.
90
+ - In an existing repo, detect the established layout and extend it; never introduce a second competing layout. Migrations to a better layout are proposed explicitly, not done by stealth.
91
+
92
+ ---
93
+
94
+ ## Structure by Test Type, Then by Domain
95
+
96
+ - When more than one test type exists, separate types at the top level - they differ in speed, dependencies, and CI stage:
97
+
98
+ ```text
99
+ tests/
100
+ unit/ fast, isolated, no I/O
101
+ integration/ module boundaries, DB, contracts
102
+ e2e/ critical user flows only
103
+ fixtures/ shared static data
104
+ helpers/ shared builders, factories, fakes
105
+ ```
106
+
107
+ - Inside each type, mirror the source tree: tests for `src/<domain>/<module>` live at `tests/<type>/<domain>/<module>.<suffix>`.
108
+ - Group e2e tests by user flow (e.g. `e2e/checkout/`), not by source module - flows cross modules.
109
+ - Default to one test file per module under test; split a large file by scenario, never by arbitrary size cuts.
110
+ - Keep the test runner's discovery config (`testMatch`, `testpaths`, includes) in sync with the layout; a test the runner cannot find is dead code.
111
+
112
+ ---
113
+
114
+ ## Naming Conventions
115
+
116
+ - Test file name = module under test + the framework's suffix: `parser.test.ts`, `test_parser.py`, `parser_test.go`.
117
+ - When splitting by scenario, encode the scenario in the name: `parser.errors.test.ts`, `auth.session-expiry.test.ts`.
118
+ - Suite and case names describe behavior, not implementation - name tests by the behavior they verify, consistent with the Test Quality Rules above.
119
+ - Forbidden names: `test1`, `misc`, `temp`, `new`, `utils-tests`, or any name that does not identify what is verified.
120
+
121
+ ---
122
+
123
+ ## Shared Test Code Placement
124
+
125
+ - Fixtures (static data), factories/builders (object construction), helpers (setup/assertion logic), and fakes each get their own folder; do not mix them in one grab-bag file.
126
+ - Place shared test code at the narrowest scope that covers its users; promote it upward only when a second consumer appears (same reuse rule as production code).
127
+ - Test helpers are production code: deduplicate, name well, and review them like any other module.
128
+ - Never import from another test file; extract the shared piece into a helper module instead.
129
+ - Large fixtures live as data files under `fixtures/`, named after the scenario they encode, not inlined into test bodies.
130
+
131
+ ---
132
+
133
+ ## Scaling & Maintenance
134
+
135
+ - When adding a test to an existing flat or misplaced suite: place the new test correctly and surface the layout debt; do not extend the mess to match it.
136
+ - Layout migrations are pure moves: never change test logic in the same commit as a file move (atomic-commit rule in git-policy).
137
+ - After any move, run the affected suite to prove discovery still works, and delete emptied folders.
138
+ - If two layouts coexist after a partial migration, finish the migration or document the boundary; a half-migrated suite is worse than either layout.
139
+
140
+ ---
141
+
142
+ ## Decision Rule: Colocated vs Centralized
143
+
144
+ - Colocated tests (next to source) fit unit tests in ecosystems that idiomatically support it (JS/TS, Go, Rust) - shortest navigation distance, moves with the code.
145
+ - A centralized `tests/` tree fits integration/e2e tests, packages that must exclude tests from the published artifact, and ecosystems whose tooling expects it (Python, Java).
146
+ - Mixing is fine when each side follows its rule (e.g. colocated unit + centralized integration); mixing within the same test type is not.
147
+
148
+ ---
149
+
73
150
  ## Reporting
74
151
 
75
152
  - State plainly what was tested and the actual result.
76
153
  - If tests fail, report the failure with output; do not claim success.
77
- - If testing was skipped or partial, say so explicitly and why.
154
+ - If testing was skipped or partial, say so explicitly and why.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "testing-policy",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "provider": "FJRG2007/enigma",
5
- "description": "Test strategy, coverage gates, deterministic tests, mocking discipline, and regression-first bug fixing.",
6
- "cliVersion": "1.15.1",
7
- "sha": "dd5c0bb67aba7f31b2f520e626a9df512b5ad2d2e7baf5567fe64d9e28a437db"
5
+ "description": "Test strategy, coverage gates, deterministic tests, mocking discipline, regression-first bug fixing, and test-suite organization (layout by type/domain, mirrored paths, file naming, fixture/helper placement).",
6
+ "cliVersion": "1.15.3",
7
+ "sha": "3bdf591057b760f674fb2b1425f63acb426cda2c4f042e1a74c5a5d3807df664"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.0.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Strict frontend + backend schema validation, schema consistency, and safe client-facing error handling.",
6
- "cliVersion": "1.15.1",
6
+ "cliVersion": "1.15.3",
7
7
  "sha": "a33622a2f810ee4cea39824cb1a7ca34b355a917d4224025df50d77dd74f0b3a"
8
8
  }
@@ -1,6 +1,6 @@
1
1
  {
2
- "enigma-darwin-arm64": "d6c2b6e9f56a2990eec5ba36839689c1de854bba4ffc67273804c0adbc900d76",
3
- "enigma-linux-arm64": "136e536a42ccf808adfb1baa340cff2c8b87a662a4cd933b73cfc4f95d3ead39",
4
- "enigma-linux-x64": "99b983ae1d1f47f0a678cd81845ee165229535ef18bdd2563404bdd1065d1db8",
5
- "enigma-win32-x64.exe": "ba9ab77cdb0f6331703f8784c3782e98cc5b2f4c3299987498f0520e95404e7a"
2
+ "enigma-darwin-arm64": "88d759633379d158070176c01ddd029f14af5806179c1d96347f4eef701733fe",
3
+ "enigma-linux-arm64": "6cae5efcc4da741d67c184a02f09d3137e182882f4457b0ec99e50cc204799f0",
4
+ "enigma-linux-x64": "9a1546028ff7fd826beaa05b13cf1ccf68dbd6635d5c0471c7f338cdffe7af4b",
5
+ "enigma-win32-x64.exe": "3c5f15b10d2612352a009bf3eecc6bdc114de0aa229ec8fb0ec2cc16b172b0d0"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "enigma-cli",
3
- "version": "1.15.1",
3
+ "version": "1.15.3",
4
4
  "description": "Everything you need to work with a coding agent: install shared policy skills for Claude Code, OpenAI Codex and opencode, and set up portable git security hooks.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,88 +0,0 @@
1
- ---
2
- name: test-organization-policy
3
- description: Expert test-suite organization - directory structure by test type and domain, mirrored source paths, file naming conventions, and fixture/helper/factory placement. Use when creating, moving, or renaming test files, scaffolding or restructuring a test suite, or deciding where a new test lives - never dump tests flat into a single tests/ folder.
4
- ---
5
-
6
- # Test Organization Policy (Senior Engineering Standards)
7
-
8
- ## Activation Scope
9
-
10
- - Apply whenever a test file is created, moved, or renamed, and whenever a test suite is scaffolded or restructured.
11
- - Owns test-suite layout: directory structure, subfolders, file naming, and placement of fixtures, helpers, factories, and mocks.
12
- - Test strategy, coverage, determinism, and mocking discipline live in testing-policy; compose with it, never restate it.
13
-
14
- ---
15
-
16
- ## Core Principle
17
-
18
- - A test suite is navigable code: anyone must find the tests for a module in seconds from the module's path alone, and find the module from its test's path.
19
- - Never dump test files flat into a single tests/ folder. Flat suites hide coverage gaps, cause name collisions, and stop scaling past a handful of files.
20
- - Organize from the first test file. Structure is cheapest at file creation and most expensive after the folder is a mess.
21
-
22
- ---
23
-
24
- ## Framework Convention First
25
-
26
- - Every ecosystem has an established convention; it beats any custom layout:
27
- - JS/TS (Vitest, Jest, Bun): colocated `*.test.ts` next to the source file, a `__tests__/` folder per directory, or a mirrored `tests/` tree - follow whichever the repo already uses.
28
- - Python (pytest): a `tests/` package mirroring the source package; shared fixtures in `conftest.py` at the narrowest directory that covers their users.
29
- - Go: `_test.go` colocated in the same package (mandated by the toolchain); black-box tests use the `_test` package suffix.
30
- - Rust: unit tests in `#[cfg(test)] mod tests` inside the module; integration tests as separate files under the crate-root `tests/`.
31
- - Java/Kotlin: `src/test/<lang>/` mirroring the `src/main/<lang>/` package path exactly.
32
- - In an existing repo, detect the established layout and extend it; never introduce a second competing layout. Migrations to a better layout are proposed explicitly, not done by stealth.
33
-
34
- ---
35
-
36
- ## Structure by Test Type, Then by Domain
37
-
38
- - When more than one test type exists, separate types at the top level - they differ in speed, dependencies, and CI stage:
39
-
40
- ```text
41
- tests/
42
- unit/ fast, isolated, no I/O
43
- integration/ module boundaries, DB, contracts
44
- e2e/ critical user flows only
45
- fixtures/ shared static data
46
- helpers/ shared builders, factories, fakes
47
- ```
48
-
49
- - Inside each type, mirror the source tree: tests for `src/<domain>/<module>` live at `tests/<type>/<domain>/<module>.<suffix>`.
50
- - Group e2e tests by user flow (e.g. `e2e/checkout/`), not by source module - flows cross modules.
51
- - Default to one test file per module under test; split a large file by scenario, never by arbitrary size cuts.
52
- - Keep the test runner's discovery config (`testMatch`, `testpaths`, includes) in sync with the layout; a test the runner cannot find is dead code.
53
-
54
- ---
55
-
56
- ## Naming Conventions
57
-
58
- - Test file name = module under test + the framework's suffix: `parser.test.ts`, `test_parser.py`, `parser_test.go`.
59
- - When splitting by scenario, encode the scenario in the name: `parser.errors.test.ts`, `auth.session-expiry.test.ts`.
60
- - Suite and case names describe behavior, not implementation (naming-by-behavior rules live in testing-policy).
61
- - Forbidden names: `test1`, `misc`, `temp`, `new`, `utils-tests`, or any name that does not identify what is verified.
62
-
63
- ---
64
-
65
- ## Shared Test Code Placement
66
-
67
- - Fixtures (static data), factories/builders (object construction), helpers (setup/assertion logic), and fakes each get their own folder; do not mix them in one grab-bag file.
68
- - Place shared test code at the narrowest scope that covers its users; promote it upward only when a second consumer appears (same reuse rule as production code).
69
- - Test helpers are production code: deduplicate, name well, and review them like any other module.
70
- - Never import from another test file; extract the shared piece into a helper module instead.
71
- - Large fixtures live as data files under `fixtures/`, named after the scenario they encode, not inlined into test bodies.
72
-
73
- ---
74
-
75
- ## Scaling & Maintenance
76
-
77
- - When adding a test to an existing flat or misplaced suite: place the new test correctly and surface the layout debt; do not extend the mess to match it.
78
- - Layout migrations are pure moves: never change test logic in the same commit as a file move (atomic-commit rule in git-policy).
79
- - After any move, run the affected suite to prove discovery still works, and delete emptied folders.
80
- - If two layouts coexist after a partial migration, finish the migration or document the boundary; a half-migrated suite is worse than either layout.
81
-
82
- ---
83
-
84
- ## Decision Rule: Colocated vs Centralized
85
-
86
- - Colocated tests (next to source) fit unit tests in ecosystems that idiomatically support it (JS/TS, Go, Rust) - shortest navigation distance, moves with the code.
87
- - A centralized `tests/` tree fits integration/e2e tests, packages that must exclude tests from the published artifact, and ecosystems whose tooling expects it (Python, Java).
88
- - Mixing is fine when each side follows its rule (e.g. colocated unit + centralized integration); mixing within the same test type is not.
@@ -1,8 +0,0 @@
1
- {
2
- "name": "test-organization-policy",
3
- "version": "1.0.0",
4
- "provider": "FJRG2007/enigma",
5
- "description": "Expert test-suite organization: structure by test type and domain, mirrored source paths, naming conventions, and fixture/helper placement.",
6
- "cliVersion": "1.15.1",
7
- "sha": "09184beb8e423efd26fce0221cd374d41b6ba0bb3d223d7edce250d9d978767e"
8
- }