claude-turing 4.4.0 → 4.6.0

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 (108) hide show
  1. package/.claude-plugin/marketplace.json +18 -0
  2. package/.claude-plugin/plugin.json +4 -4
  3. package/LICENSE +1 -1
  4. package/README.md +78 -555
  5. package/bin/cli.js +23 -4
  6. package/commands/doctor.md +1 -0
  7. package/commands/init.md +21 -3
  8. package/commands/turing.md +85 -77
  9. package/config/commands.yaml +928 -0
  10. package/config/defaults.yaml +2 -0
  11. package/package.json +7 -6
  12. package/src/command-registry.js +151 -0
  13. package/src/install.js +24 -35
  14. package/src/verify.js +45 -88
  15. package/templates/README.md +1 -1
  16. package/templates/__pycache__/evaluate.cpython-312.pyc +0 -0
  17. package/templates/__pycache__/prepare.cpython-312.pyc +0 -0
  18. package/templates/config.yaml +1 -1
  19. package/templates/features/__pycache__/__init__.cpython-312.pyc +0 -0
  20. package/templates/features/__pycache__/featurizers.cpython-312.pyc +0 -0
  21. package/templates/program.md +1 -1
  22. package/templates/scripts/__pycache__/__init__.cpython-312.pyc +0 -0
  23. package/templates/scripts/__pycache__/ablation_study.cpython-312.pyc +0 -0
  24. package/templates/scripts/__pycache__/architecture_surgery.cpython-312.pyc +0 -0
  25. package/templates/scripts/__pycache__/budget_manager.cpython-312.pyc +0 -0
  26. package/templates/scripts/__pycache__/build_ensemble.cpython-312.pyc +0 -0
  27. package/templates/scripts/__pycache__/calibration.cpython-312.pyc +0 -0
  28. package/templates/scripts/__pycache__/check_convergence.cpython-312.pyc +0 -0
  29. package/templates/scripts/__pycache__/checkpoint_manager.cpython-312.pyc +0 -0
  30. package/templates/scripts/__pycache__/citation_manager.cpython-312.pyc +0 -0
  31. package/templates/scripts/__pycache__/cost_frontier.cpython-312.pyc +0 -0
  32. package/templates/scripts/__pycache__/counterfactual_explanation.cpython-312.pyc +0 -0
  33. package/templates/scripts/__pycache__/critique_hypothesis.cpython-312.pyc +0 -0
  34. package/templates/scripts/__pycache__/curriculum_optimizer.cpython-312.pyc +0 -0
  35. package/templates/scripts/__pycache__/diagnose_errors.cpython-312.pyc +0 -0
  36. package/templates/scripts/__pycache__/draft_paper_sections.cpython-312.pyc +0 -0
  37. package/templates/scripts/__pycache__/equivalence_checker.cpython-312.pyc +0 -0
  38. package/templates/scripts/__pycache__/experiment_annotations.cpython-312.pyc +0 -0
  39. package/templates/scripts/__pycache__/experiment_archive.cpython-312.pyc +0 -0
  40. package/templates/scripts/__pycache__/experiment_diff.cpython-312.pyc +0 -0
  41. package/templates/scripts/__pycache__/experiment_index.cpython-312.pyc +0 -0
  42. package/templates/scripts/__pycache__/experiment_queue.cpython-312.pyc +0 -0
  43. package/templates/scripts/__pycache__/experiment_replay.cpython-312.pyc +0 -0
  44. package/templates/scripts/__pycache__/experiment_search.cpython-312.pyc +0 -0
  45. package/templates/scripts/__pycache__/experiment_simulator.cpython-312.pyc +0 -0
  46. package/templates/scripts/__pycache__/experiment_templates.cpython-312.pyc +0 -0
  47. package/templates/scripts/__pycache__/export_card.cpython-312.pyc +0 -0
  48. package/templates/scripts/__pycache__/export_formats.cpython-312.pyc +0 -0
  49. package/templates/scripts/__pycache__/failure_postmortem.cpython-312.pyc +0 -0
  50. package/templates/scripts/__pycache__/feature_intelligence.cpython-312.pyc +0 -0
  51. package/templates/scripts/__pycache__/fork_experiment.cpython-312.pyc +0 -0
  52. package/templates/scripts/__pycache__/generate_baselines.cpython-312.pyc +0 -0
  53. package/templates/scripts/__pycache__/generate_brief.cpython-312.pyc +0 -0
  54. package/templates/scripts/__pycache__/generate_changelog.cpython-312.pyc +0 -0
  55. package/templates/scripts/__pycache__/generate_figures.cpython-312.pyc +0 -0
  56. package/templates/scripts/__pycache__/generate_logbook.cpython-312.pyc +0 -0
  57. package/templates/scripts/__pycache__/generate_model_card.cpython-312.pyc +0 -0
  58. package/templates/scripts/__pycache__/generate_onboarding.cpython-312.pyc +0 -0
  59. package/templates/scripts/__pycache__/harness_doctor.cpython-312.pyc +0 -0
  60. package/templates/scripts/__pycache__/harness_doctor.cpython-314.pyc +0 -0
  61. package/templates/scripts/__pycache__/incremental_update.cpython-312.pyc +0 -0
  62. package/templates/scripts/__pycache__/knowledge_transfer.cpython-312.pyc +0 -0
  63. package/templates/scripts/__pycache__/latency_benchmark.cpython-312.pyc +0 -0
  64. package/templates/scripts/__pycache__/leakage_detector.cpython-312.pyc +0 -0
  65. package/templates/scripts/__pycache__/literature_search.cpython-312.pyc +0 -0
  66. package/templates/scripts/__pycache__/log_experiment.cpython-312.pyc +0 -0
  67. package/templates/scripts/__pycache__/manage_hypotheses.cpython-312.pyc +0 -0
  68. package/templates/scripts/__pycache__/methodology_audit.cpython-312.pyc +0 -0
  69. package/templates/scripts/__pycache__/model_distiller.cpython-312.pyc +0 -0
  70. package/templates/scripts/__pycache__/model_lifecycle.cpython-312.pyc +0 -0
  71. package/templates/scripts/__pycache__/model_merger.cpython-312.pyc +0 -0
  72. package/templates/scripts/__pycache__/model_pruning.cpython-312.pyc +0 -0
  73. package/templates/scripts/__pycache__/model_quantization.cpython-312.pyc +0 -0
  74. package/templates/scripts/__pycache__/model_xray.cpython-312.pyc +0 -0
  75. package/templates/scripts/__pycache__/novelty_guard.cpython-312.pyc +0 -0
  76. package/templates/scripts/__pycache__/package_experiments.cpython-312.pyc +0 -0
  77. package/templates/scripts/__pycache__/pareto_frontier.cpython-312.pyc +0 -0
  78. package/templates/scripts/__pycache__/parse_metrics.cpython-312.pyc +0 -0
  79. package/templates/scripts/__pycache__/pipeline_manager.cpython-312.pyc +0 -0
  80. package/templates/scripts/__pycache__/profile_training.cpython-312.pyc +0 -0
  81. package/templates/scripts/__pycache__/regression_gate.cpython-312.pyc +0 -0
  82. package/templates/scripts/__pycache__/reproduce_experiment.cpython-312.pyc +0 -0
  83. package/templates/scripts/__pycache__/research_planner.cpython-312.pyc +0 -0
  84. package/templates/scripts/__pycache__/sanity_checks.cpython-312.pyc +0 -0
  85. package/templates/scripts/__pycache__/scaffold.cpython-312.pyc +0 -0
  86. package/templates/scripts/__pycache__/scaffold.cpython-314.pyc +0 -0
  87. package/templates/scripts/__pycache__/scaling_estimator.cpython-312.pyc +0 -0
  88. package/templates/scripts/__pycache__/seed_runner.cpython-312.pyc +0 -0
  89. package/templates/scripts/__pycache__/sensitivity_analysis.cpython-312.pyc +0 -0
  90. package/templates/scripts/__pycache__/session_flashback.cpython-312.pyc +0 -0
  91. package/templates/scripts/__pycache__/show_experiment_tree.cpython-312.pyc +0 -0
  92. package/templates/scripts/__pycache__/show_families.cpython-312.pyc +0 -0
  93. package/templates/scripts/__pycache__/simulate_review.cpython-312.pyc +0 -0
  94. package/templates/scripts/__pycache__/smart_retry.cpython-312.pyc +0 -0
  95. package/templates/scripts/__pycache__/statistical_compare.cpython-312.pyc +0 -0
  96. package/templates/scripts/__pycache__/suggest_next.cpython-312.pyc +0 -0
  97. package/templates/scripts/__pycache__/sweep.cpython-312.pyc +0 -0
  98. package/templates/scripts/__pycache__/synthesize_decision.cpython-312.pyc +0 -0
  99. package/templates/scripts/__pycache__/training_monitor.cpython-312.pyc +0 -0
  100. package/templates/scripts/__pycache__/treequest_suggest.cpython-312.pyc +0 -0
  101. package/templates/scripts/__pycache__/trend_analysis.cpython-312.pyc +0 -0
  102. package/templates/scripts/__pycache__/turing_io.cpython-312.pyc +0 -0
  103. package/templates/scripts/__pycache__/update_state.cpython-312.pyc +0 -0
  104. package/templates/scripts/__pycache__/verify_placeholders.cpython-312.pyc +0 -0
  105. package/templates/scripts/__pycache__/warm_start.cpython-312.pyc +0 -0
  106. package/templates/scripts/__pycache__/whatif_engine.cpython-312.pyc +0 -0
  107. package/templates/scripts/harness_doctor.py +145 -1
  108. package/templates/scripts/scaffold.py +50 -28
@@ -32,7 +32,7 @@ REQUIRED_SCRIPTS = ["train.py", "prepare.py", "evaluate.py"]
32
32
  REQUIRED_CONFIG_FIELDS = ["evaluation"]
33
33
 
34
34
  CHECK_CATEGORIES = ["environment", "dependencies", "config", "experiment_log",
35
- "scripts", "disk_space", "git_state"]
35
+ "scripts", "disk_space", "git_state", "claude_hooks"]
36
36
 
37
37
 
38
38
  # --- Individual Checks ---
@@ -288,9 +288,136 @@ def check_git_state(project_dir: str = ".") -> dict:
288
288
  }
289
289
 
290
290
 
291
+ def check_claude_hooks(settings_path: str = ".claude/settings.local.json") -> dict:
292
+ """Check Claude Code project hook schema."""
293
+ path = Path(settings_path)
294
+ if not path.exists():
295
+ return {
296
+ "name": "Claude hooks",
297
+ "status": "WARN",
298
+ "detail": f"{settings_path} not found",
299
+ "issues": [],
300
+ }
301
+
302
+ try:
303
+ settings = json.loads(path.read_text(encoding="utf-8"))
304
+ except json.JSONDecodeError as e:
305
+ return {
306
+ "name": "Claude hooks",
307
+ "status": "FAIL",
308
+ "detail": f"{settings_path} has JSON parse error",
309
+ "issues": [str(e)],
310
+ "fixable": False,
311
+ }
312
+
313
+ hooks = settings.get("hooks", {})
314
+ if not isinstance(hooks, dict):
315
+ return {
316
+ "name": "Claude hooks",
317
+ "status": "FAIL",
318
+ "detail": "hooks must be a JSON object",
319
+ "issues": ["hooks must map event names to hook group arrays"],
320
+ "fixable": False,
321
+ }
322
+
323
+ issues = []
324
+ fixable = False
325
+ group_count = 0
326
+
327
+ for event, entries in hooks.items():
328
+ if not isinstance(entries, list):
329
+ issues.append(f"hooks.{event} must be an array")
330
+ continue
331
+
332
+ for index, entry in enumerate(entries):
333
+ if not isinstance(entry, dict):
334
+ issues.append(f"hooks.{event}[{index}] must be an object")
335
+ continue
336
+
337
+ if (
338
+ set(entry.keys()) == {"type", "command"}
339
+ and entry["type"] == "command"
340
+ and isinstance(entry["command"], str)
341
+ ):
342
+ issues.append(f"hooks.{event}[{index}] uses legacy bare command hook shape")
343
+ fixable = True
344
+ continue
345
+
346
+ nested_hooks = entry.get("hooks")
347
+ if not isinstance(nested_hooks, list):
348
+ issues.append(f"hooks.{event}[{index}].hooks must be an array")
349
+ continue
350
+
351
+ group_count += 1
352
+ for hook_index, hook in enumerate(nested_hooks):
353
+ if not isinstance(hook, dict):
354
+ issues.append(f"hooks.{event}[{index}].hooks[{hook_index}] must be an object")
355
+ continue
356
+ if hook.get("type") == "command" and not hook.get("command"):
357
+ issues.append(f"hooks.{event}[{index}].hooks[{hook_index}] command hook missing command")
358
+
359
+ status = "FAIL" if issues else "PASS"
360
+ result = {
361
+ "name": "Claude hooks",
362
+ "status": status,
363
+ "detail": f"{group_count} hook groups valid" if not issues else f"{len(issues)} hook schema issue(s)",
364
+ "issues": issues,
365
+ "fixable": fixable,
366
+ }
367
+ if fixable:
368
+ result["fix"] = "Run /turing:doctor --fix to migrate legacy bare command hooks"
369
+ return result
370
+
371
+
291
372
  # --- Fix Operations ---
292
373
 
293
374
 
375
+ def fix_claude_hooks(settings_path: str = ".claude/settings.local.json") -> dict:
376
+ """Migrate legacy bare command hooks into Claude Code hook groups."""
377
+ path = Path(settings_path)
378
+ if not path.exists():
379
+ return {"fixed": False, "reason": "Settings file not found"}
380
+
381
+ try:
382
+ settings = json.loads(path.read_text(encoding="utf-8"))
383
+ except json.JSONDecodeError:
384
+ return {"fixed": False, "reason": "Settings file has invalid JSON"}
385
+
386
+ hooks = settings.get("hooks")
387
+ if not isinstance(hooks, dict):
388
+ return {"fixed": False, "reason": "hooks is not a JSON object"}
389
+
390
+ migrated = 0
391
+ for event, entries in hooks.items():
392
+ if not isinstance(entries, list):
393
+ continue
394
+
395
+ fixed_entries = []
396
+ for entry in entries:
397
+ if (
398
+ isinstance(entry, dict)
399
+ and set(entry.keys()) == {"type", "command"}
400
+ and entry["type"] == "command"
401
+ and isinstance(entry["command"], str)
402
+ ):
403
+ fixed_entries.append({
404
+ "matcher": "",
405
+ "hooks": [{"type": "command", "command": entry["command"]}],
406
+ })
407
+ migrated += 1
408
+ else:
409
+ fixed_entries.append(entry)
410
+ hooks[event] = fixed_entries
411
+
412
+ if not migrated:
413
+ return {"fixed": False, "reason": "No legacy bare command hooks found"}
414
+
415
+ backup = path.with_suffix(path.suffix + ".bak")
416
+ shutil.copy2(path, backup)
417
+ path.write_text(json.dumps(settings, indent=2) + "\n", encoding="utf-8")
418
+ return {"fixed": True, "migrated": migrated, "backup": str(backup)}
419
+
420
+
294
421
  def fix_corrupt_log(log_path: str = DEFAULT_LOG_PATH) -> dict:
295
422
  """Remove corrupt lines from experiment log."""
296
423
  path = Path(log_path)
@@ -328,6 +455,7 @@ def fix_corrupt_log(log_path: str = DEFAULT_LOG_PATH) -> dict:
328
455
  def run_doctor(
329
456
  config_path: str = "config.yaml",
330
457
  log_path: str = DEFAULT_LOG_PATH,
458
+ hooks_path: str = ".claude/settings.local.json",
331
459
  fix: bool = False,
332
460
  verbose: bool = False,
333
461
  ) -> dict:
@@ -336,6 +464,7 @@ def run_doctor(
336
464
  Args:
337
465
  config_path: Path to config.yaml.
338
466
  log_path: Path to experiment log.
467
+ hooks_path: Path to Claude Code project settings.
339
468
  fix: If True, auto-fix safe issues.
340
469
  verbose: Include detailed info.
341
470
 
@@ -350,6 +479,7 @@ def run_doctor(
350
479
  check_scripts(),
351
480
  check_disk_space(),
352
481
  check_git_state(),
482
+ check_claude_hooks(hooks_path),
353
483
  ]
354
484
 
355
485
  # Apply fixes if requested
@@ -366,6 +496,18 @@ def run_doctor(
366
496
  checks[i] = check_experiment_log(log_path)
367
497
  break
368
498
 
499
+ hooks_check = next((c for c in checks if c["name"] == "Claude hooks"), None)
500
+ if hooks_check and hooks_check.get("fixable"):
501
+ fix_result = fix_claude_hooks(hooks_path)
502
+ if fix_result.get("fixed"):
503
+ fixes_applied.append(
504
+ f"Migrated {fix_result['migrated']} legacy Claude hook entries (backup: {fix_result['backup']})"
505
+ )
506
+ for i, c in enumerate(checks):
507
+ if c["name"] == "Claude hooks":
508
+ checks[i] = check_claude_hooks(hooks_path)
509
+ break
510
+
369
511
  # Compute score
370
512
  passed = sum(1 for c in checks if c["status"] == "PASS")
371
513
  warned = sum(1 for c in checks if c["status"] == "WARN")
@@ -441,6 +583,7 @@ def main():
441
583
  parser.add_argument("--verbose", action="store_true", help="Show detailed info")
442
584
  parser.add_argument("--config", default="config.yaml", help="Path to config.yaml")
443
585
  parser.add_argument("--log", default=DEFAULT_LOG_PATH, help="Path to experiment log")
586
+ parser.add_argument("--hooks", default=".claude/settings.local.json", help="Path to Claude Code settings")
444
587
  parser.add_argument("--json", action="store_true", help="Output raw JSON")
445
588
 
446
589
  args = parser.parse_args()
@@ -448,6 +591,7 @@ def main():
448
591
  report = run_doctor(
449
592
  config_path=args.config,
450
593
  log_path=args.log,
594
+ hooks_path=args.hooks,
451
595
  fix=args.fix,
452
596
  verbose=args.verbose,
453
597
  )
@@ -34,6 +34,8 @@ PLACEHOLDER_MAP = {
34
34
  "ML_DIR": "ml_dir",
35
35
  "DATA_SOURCE": "data_source",
36
36
  "METRIC_DIRECTION": "metric_direction",
37
+ "LOWER_IS_BETTER": "lower_is_better",
38
+ "MEMORY_DIR_NAME": "memory_dir_name",
37
39
  }
38
40
 
39
41
  # Files to copy from templates/ to the ML directory
@@ -223,32 +225,49 @@ SHELL_SCRIPTS = [
223
225
 
224
226
  def find_templates_dir() -> Path | None:
225
227
  """Locate the templates directory relative to this script or plugin root."""
226
- # When running from a scaffolded project, templates are local
228
+ env_templates_dir = os.environ.get("TURING_TEMPLATES_DIR")
229
+ if env_templates_dir:
230
+ candidate = Path(env_templates_dir).expanduser()
231
+ if (candidate / "prepare.py").exists():
232
+ return candidate
233
+
227
234
  script_dir = Path(__file__).parent
228
235
 
229
- # Check: are we inside the plugin's templates/scripts/ ?
230
- candidate = script_dir.parent # templates/
231
- if (candidate / "prepare.py").exists():
232
- return candidate
233
-
234
- # Check: plugin root (two levels up from scripts/)
235
- plugin_root = script_dir.parent.parent
236
- candidate = plugin_root / "templates"
237
- if candidate.exists() and (candidate / "prepare.py").exists():
238
- return candidate
239
-
240
- # Search common plugin locations
241
- home = Path.home()
242
- for pattern in [
243
- home / ".claude" / "plugins" / "*" / "templates",
236
+ project_command_templates = [
237
+ path / ".claude" / "commands" / "turing" / "templates"
238
+ for path in [Path.cwd(), *Path.cwd().parents]
239
+ ]
240
+
241
+ for candidate in [
242
+ script_dir.parent,
243
+ script_dir.parent.parent / "templates",
244
+ *project_command_templates,
245
+ Path.home() / ".claude" / "commands" / "turing" / "templates",
246
+ Path.cwd() / "node_modules" / "claude-turing" / "templates",
244
247
  ]:
245
- for match in sorted(pattern.parent.glob(pattern.name)):
246
- if (match / "prepare.py").exists():
247
- return match
248
+ if (candidate / "prepare.py").exists():
249
+ return candidate
250
+
251
+ plugins_dir = Path.home() / ".claude" / "plugins"
252
+ for match in sorted(plugins_dir.glob("*/templates")):
253
+ if (match / "prepare.py").exists():
254
+ return match
248
255
 
249
256
  return None
250
257
 
251
258
 
259
+ def derive_values(values: dict[str, str]) -> dict[str, str]:
260
+ """Add scaffold values derived from user-provided fields."""
261
+ derived = dict(values)
262
+ derived["lower_is_better"] = (
263
+ "true" if derived.get("metric_direction", "").lower() == "lower" else "false"
264
+ )
265
+ derived["memory_dir_name"] = re.sub(
266
+ r"[^a-zA-Z0-9_.-]+", "-", derived["project_name"]
267
+ ).strip("-")
268
+ return derived
269
+
270
+
252
271
  def replace_placeholders(text: str, values: dict[str, str]) -> str:
253
272
  """Replace all {{PLACEHOLDER}} markers in text with values."""
254
273
  for placeholder, arg_name in PLACEHOLDER_MAP.items():
@@ -276,6 +295,7 @@ def scaffold_project(
276
295
  Returns:
277
296
  Dict with counts: files_copied, placeholders_replaced, dirs_created.
278
297
  """
298
+ values = derive_values(values)
279
299
  target = Path(ml_dir)
280
300
  target.mkdir(parents=True, exist_ok=True)
281
301
 
@@ -329,7 +349,7 @@ def scaffold_project(
329
349
  continue
330
350
 
331
351
  # Setup agent memory
332
- memory_dir = Path(".claude") / "agent-memory" / "ml-researcher"
352
+ memory_dir = Path(".claude") / "agent-memory" / f"ml-researcher-{values['memory_dir_name']}"
333
353
  memory_dir.mkdir(parents=True, exist_ok=True)
334
354
  memory_src = templates_dir / "MEMORY.md"
335
355
  if memory_src.exists():
@@ -348,6 +368,14 @@ def scaffold_project(
348
368
  return stats
349
369
 
350
370
 
371
+ def make_command_hook_group(command: str, matcher: str = "") -> dict:
372
+ """Build a Claude Code command hook group."""
373
+ return {
374
+ "matcher": matcher,
375
+ "hooks": [{"type": "command", "command": command}],
376
+ }
377
+
378
+
351
379
  def _setup_hooks(ml_dir: str) -> None:
352
380
  """Configure Claude Code hooks in .claude/settings.local.json."""
353
381
  settings_path = Path(".claude") / "settings.local.json"
@@ -366,20 +394,14 @@ def _setup_hooks(ml_dir: str) -> None:
366
394
  post_hooks = hooks.get("PostToolUse", [])
367
395
  post_hook_cmd = f"bash {ml_dir}/scripts/post-train-hook.sh"
368
396
  if not any(post_hook_cmd in str(h) for h in post_hooks):
369
- post_hooks.append({
370
- "matcher": "Bash",
371
- "hooks": [{"type": "command", "command": post_hook_cmd}],
372
- })
397
+ post_hooks.append(make_command_hook_group(post_hook_cmd, matcher="Bash"))
373
398
  hooks["PostToolUse"] = post_hooks
374
399
 
375
400
  # Stop hook for convergence
376
401
  stop_hooks = hooks.get("Stop", [])
377
402
  stop_hook_cmd = f"bash {ml_dir}/scripts/stop-hook.sh"
378
403
  if not any(stop_hook_cmd in str(h) for h in stop_hooks):
379
- stop_hooks.append({
380
- "type": "command",
381
- "command": stop_hook_cmd,
382
- })
404
+ stop_hooks.append(make_command_hook_group(stop_hook_cmd))
383
405
  hooks["Stop"] = stop_hooks
384
406
 
385
407
  settings["hooks"] = hooks