loki-mode 6.75.3 → 6.76.1

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 (76) hide show
  1. package/SKILL.md +2 -2
  2. package/VERSION +1 -1
  3. package/autonomy/loki +659 -0
  4. package/autonomy/run.sh +115 -4
  5. package/dashboard/__init__.py +1 -1
  6. package/docs/INSTALLATION.md +1 -1
  7. package/mcp/__init__.py +1 -1
  8. package/mcp/magic_tools.py +471 -0
  9. package/mcp/server.py +13 -0
  10. package/package.json +1 -1
  11. package/references/magic-modules-patterns.md +634 -0
  12. package/references/magic-rarv-integration.md +87 -0
  13. package/skills/00-index.md +2 -0
  14. package/skills/magic-modules.md +205 -0
  15. package/web-app/dist/assets/{AdminPage-D4QSV6Zi.js → AdminPage-DwVUK4v9.js} +1 -1
  16. package/web-app/dist/assets/{Avatar-88MlpLO5.js → Avatar-B7gqhcg3.js} +1 -1
  17. package/web-app/dist/assets/{Badge-DbGjLr4i.js → Badge-DA3xNJAS.js} +1 -1
  18. package/web-app/dist/assets/{Button-sp_FVGZj.js → Button-BPXURLaK.js} +1 -1
  19. package/web-app/dist/assets/{ComparePage-p2ENnfa7.js → ComparePage-B0JQMhKG.js} +1 -1
  20. package/web-app/dist/assets/GitHubIssuesPanel-D38-fy29.js +12 -0
  21. package/web-app/dist/assets/{GitHubPRsPanel-Bi_yrcAE.js → GitHubPRsPanel-DLPcW3N0.js} +2 -2
  22. package/web-app/dist/assets/{HomePage-BB83YPiX.js → HomePage-CzeoS2V_.js} +3 -3
  23. package/web-app/dist/assets/{LoginPage-BXUudCJ9.js → LoginPage-DqCzxsfx.js} +1 -1
  24. package/web-app/dist/assets/MagicPage-CBLqpa55.js +31 -0
  25. package/web-app/dist/assets/{MetricsPage-CX0Ahy-_.js → MetricsPage-CPYQR0zr.js} +1 -1
  26. package/web-app/dist/assets/{NotFoundPage-C4JqatEk.js → NotFoundPage-B62u4iCs.js} +1 -1
  27. package/web-app/dist/assets/{ProjectPage-t5J2XAJT.js → ProjectPage-DNujSl6j.js} +67 -72
  28. package/web-app/dist/assets/{ProjectsPage-Bzpz1clk.js → ProjectsPage-uHG7kxB-.js} +1 -1
  29. package/web-app/dist/assets/{SettingsPage-y_yl8FvH.js → SettingsPage-BaQJbOgL.js} +1 -1
  30. package/web-app/dist/assets/{ShowcasePage-B7d6pzMq.js → ShowcasePage-DQR_e-kg.js} +1 -1
  31. package/web-app/dist/assets/{SystemSettingsPage-C4tR33KU.js → SystemSettingsPage-C_Q_1WK4.js} +1 -1
  32. package/web-app/dist/assets/{TeamsPage-DIOCfZIP.js → TeamsPage-DOFErDqX.js} +1 -1
  33. package/web-app/dist/assets/{TemplatesPage-DlKyapXX.js → TemplatesPage-Ty72hILN.js} +1 -1
  34. package/web-app/dist/assets/{TerminalOutput-Czg-ZC2k.js → TerminalOutput-DqOVnR1p.js} +7 -12
  35. package/web-app/dist/assets/{activity-h1wU9a0L.js → activity-BgBZ4s4c.js} +1 -1
  36. package/web-app/dist/assets/{bell-Bu8lsWOp.js → bell-C-UezVWi.js} +1 -1
  37. package/web-app/dist/assets/{bot-rWO7KjkQ.js → bot-D70fEnm5.js} +1 -1
  38. package/web-app/dist/assets/{check-BWp8L5Cy.js → check-CBohulxQ.js} +1 -1
  39. package/web-app/dist/assets/{chevron-left-Bw4I1yGm.js → chevron-left-C-emzUhB.js} +1 -1
  40. package/web-app/dist/assets/{circle-alert-C37PKXiC.js → circle-alert-8SRY0_GX.js} +1 -1
  41. package/web-app/dist/assets/{clock-DDScLol4.js → clock-mfq4XnPQ.js} +1 -1
  42. package/web-app/dist/assets/{cloud-DaYKPLaM.js → cloud-DpRM7T8t.js} +1 -1
  43. package/web-app/dist/assets/code-xml-1N2Ui-4c.js +6 -0
  44. package/web-app/dist/assets/{copy-DKIRv0VK.js → copy-LXquTgzI.js} +1 -1
  45. package/web-app/dist/assets/{database-CYZBHz51.js → database-S1dyXnuT.js} +1 -1
  46. package/web-app/dist/assets/{dollar-sign-CydJu0kl.js → dollar-sign-CRqk0dW5.js} +1 -1
  47. package/web-app/dist/assets/{file-code-corner-DqZ9gpdv.js → file-code-corner-B99CwY_6.js} +1 -1
  48. package/web-app/dist/assets/{file-plus-CzeFJWp3.js → file-plus-DZ5qnz5b.js} +1 -1
  49. package/web-app/dist/assets/{folder-open-4YWk08dP.js → folder-open-DBCm7yuF.js} +1 -1
  50. package/web-app/dist/assets/{git-commit-horizontal-wbqFPNID.js → git-commit-horizontal-DM1ERuNd.js} +1 -1
  51. package/web-app/dist/assets/{globe-Cby-g5Yb.js → globe-B7xEJSL_.js} +1 -1
  52. package/web-app/dist/assets/{hammer-BNScgGdp.js → hammer-Cgi3LTuS.js} +1 -1
  53. package/web-app/dist/assets/{index-6Z4B0I6r.js → index-BN52-GQT.js} +22 -17
  54. package/web-app/dist/assets/index-BfZSDej1.css +1 -0
  55. package/web-app/dist/assets/{layers-XfssQc5V.js → layers-Bi8RPIBC.js} +1 -1
  56. package/web-app/dist/assets/{lightbulb-EhnzRw7M.js → lightbulb-Doc_n8JX.js} +1 -1
  57. package/web-app/dist/assets/{loader-circle-BA0QIVGA.js → loader-circle-BB932A7A.js} +1 -1
  58. package/web-app/dist/assets/{lock-BABtHe6K.js → lock-Bt6gpMrs.js} +1 -1
  59. package/web-app/dist/assets/{mail-Dokiey5S.js → mail-BuzAu1IP.js} +1 -1
  60. package/web-app/dist/assets/{package-DbJyS1Ft.js → package-BE5FHxQ8.js} +1 -1
  61. package/web-app/dist/assets/{plus-BcAN8Kaj.js → plus-CNqABexN.js} +1 -1
  62. package/web-app/dist/assets/{refresh-cw-B3dG1-Sb.js → refresh-cw-34B13ztx.js} +1 -1
  63. package/web-app/dist/assets/{rotate-ccw-Cs1Phctm.js → rotate-ccw-CrD2QB29.js} +1 -1
  64. package/web-app/dist/assets/{save-DsrNCZrP.js → save-DsJcqdnI.js} +1 -1
  65. package/web-app/dist/assets/{server-CpN2GX4G.js → server-BcgRMArA.js} +1 -1
  66. package/web-app/dist/assets/{shield-alert-CKJ1pzCz.js → shield-alert-DLYLdVJ0.js} +1 -1
  67. package/web-app/dist/assets/{trash-2-C9vZqTqw.js → trash-2-Cc-VTvzt.js} +1 -1
  68. package/web-app/dist/assets/{trending-down-BNLTrF5P.js → trending-down-CrDpO2a_.js} +1 -1
  69. package/web-app/dist/assets/{trending-up-DmFIdVOc.js → trending-up-CNVsmM3G.js} +1 -1
  70. package/web-app/dist/assets/upload-LuDuB7Wc.js +6 -0
  71. package/web-app/dist/assets/{usePolling-vUlY-o6P.js → usePolling-C8rvc-CG.js} +1 -1
  72. package/web-app/dist/assets/{user-Dh00W8De.js → user-BT79cI-o.js} +1 -1
  73. package/web-app/dist/index.html +2 -2
  74. package/web-app/server.py +120 -0
  75. package/web-app/dist/assets/GitHubIssuesPanel-DBbBTG9w.js +0 -17
  76. package/web-app/dist/assets/index-CVM4A1Fw.css +0 -1
package/autonomy/run.sh CHANGED
@@ -5784,6 +5784,53 @@ run_doc_quality_gate() {
5784
5784
  [ "$score" -ge 70 ]
5785
5785
  }
5786
5786
 
5787
+ # ============================================================================
5788
+ # Magic Modules Debate Gate - Gate 12 (v6.77.0)
5789
+ # Runs when any .loki/magic/specs/*.md changed since last iteration.
5790
+ # Blocks iteration completion if debate flags any block severity.
5791
+ # ============================================================================
5792
+
5793
+ run_magic_debate_gate() {
5794
+ local specs_dir="$TARGET_DIR/.loki/magic/specs"
5795
+ if [ ! -d "$specs_dir" ]; then
5796
+ return 0
5797
+ fi
5798
+
5799
+ local has_specs
5800
+ has_specs=$(find "$specs_dir" -maxdepth 1 -name "*.md" 2>/dev/null | head -1)
5801
+ if [ -z "$has_specs" ]; then
5802
+ return 0
5803
+ fi
5804
+
5805
+ # Auto-run update to catch stale generated files
5806
+ log_info "Magic Modules: running incremental update"
5807
+ (cd "$TARGET_DIR" && PYTHONPATH="$PROJECT_DIR" LOKI_PROVIDER="${PROVIDER_NAME:-claude}" \
5808
+ "$PROJECT_DIR/autonomy/loki" magic update 2>&1 | tail -10) || true
5809
+
5810
+ # Run debate on most recently modified component
5811
+ local latest_spec
5812
+ latest_spec=$(find "$specs_dir" -maxdepth 1 -name "*.md" -type f -print0 2>/dev/null | xargs -0 ls -t 2>/dev/null | head -1)
5813
+ if [ -z "$latest_spec" ]; then
5814
+ return 0
5815
+ fi
5816
+ local latest_name
5817
+ latest_name=$(basename "$latest_spec" .md)
5818
+
5819
+ log_info "Magic Modules: running debate on '$latest_name'"
5820
+ local debate_out
5821
+ debate_out=$(cd "$TARGET_DIR" && PYTHONPATH="$PROJECT_DIR" LOKI_PROVIDER="${PROVIDER_NAME:-claude}" \
5822
+ timeout 300 "$PROJECT_DIR/autonomy/loki" magic debate "$latest_name" --rounds 2 2>&1 || true)
5823
+
5824
+ # Parse debate outcome; block if any persona set severity=block
5825
+ if echo "$debate_out" | grep -qi '"severity"[[:space:]]*:[[:space:]]*"block"'; then
5826
+ log_warn "Magic Modules Gate 12: debate returned BLOCK severity for '$latest_name'"
5827
+ return 1
5828
+ fi
5829
+
5830
+ log_info "Magic Modules Gate 12: PASS"
5831
+ return 0
5832
+ }
5833
+
5787
5834
  # ============================================================================
5788
5835
  # 3-Reviewer Parallel Code Review (v5.35.0)
5789
5836
  # Specialist pool from skills/quality-gates.md with blind review
@@ -7905,6 +7952,24 @@ except Exception as e:
7905
7952
  PYEOF
7906
7953
  }
7907
7954
 
7955
+ # Magic Modules COMPOUND: record successful component patterns (v6.77.0)
7956
+ # Called at end of each iteration to capture generated/updated components
7957
+ # as semantic memory patterns via magic.core.memory_bridge.
7958
+ _magic_compound_capture() {
7959
+ local registry="$TARGET_DIR/.loki/magic/registry.json"
7960
+ if [ ! -f "$registry" ]; then
7961
+ return 0
7962
+ fi
7963
+ # Delegate to memory_bridge (built by agent 3)
7964
+ PYTHONPATH="$PROJECT_DIR" python3 -c "
7965
+ try:
7966
+ from magic.core.memory_bridge import capture_iteration_compound
7967
+ capture_iteration_compound('${TARGET_DIR}', iteration=${ITERATION_COUNT:-0})
7968
+ except Exception as exc:
7969
+ pass
7970
+ " 2>/dev/null || true
7971
+ }
7972
+
7908
7973
  # Automatic episode capture with enriched context (v6.15.0)
7909
7974
  # Captures git changes, files modified, and RARV phase automatically
7910
7975
  # after every iteration -- no manual invocation needed.
@@ -8583,6 +8648,21 @@ except Exception:
8583
8648
  " 2>/dev/null || true)
8584
8649
  fi
8585
8650
 
8651
+ # Magic Modules context injection
8652
+ local magic_context=""
8653
+ local magic_specs_dir="$TARGET_DIR/.loki/magic/specs"
8654
+ if [ -d "$magic_specs_dir" ]; then
8655
+ local spec_count
8656
+ spec_count=$(find "$magic_specs_dir" -maxdepth 1 -name "*.md" 2>/dev/null | wc -l | tr -d ' ')
8657
+ if [ "$spec_count" -gt 0 ]; then
8658
+ local spec_list
8659
+ spec_list=$(find "$magic_specs_dir" -maxdepth 1 -name "*.md" -exec basename {} .md \; 2>/dev/null | tr '\n' ',' | sed 's/,$//')
8660
+ magic_context="MAGIC_MODULES: ${spec_count} component specs exist: ${spec_list}. To add or update a component: write markdown to ${magic_specs_dir}/<Name>.md and run 'loki magic update'. The spec becomes source of truth; implementation regenerates automatically. Debate runs in VERIFY phase -- if accessibility or performance blocks, refine the spec and re-run."
8661
+ else
8662
+ magic_context="MAGIC_MODULES: available. To create UI components, write spec at ${magic_specs_dir}/<Name>.md and run 'loki magic update'. Spec-driven generation produces React + Web Component variants with auto-generated tests. Debate gate runs in VERIFY."
8663
+ fi
8664
+ fi
8665
+
8586
8666
  # Degraded providers with small models need simplified prompts
8587
8667
  # Full RARV/SDLC instructions overwhelm models < 30B parameters
8588
8668
  if [ "${PROVIDER_DEGRADED:-false}" = "true" ]; then
@@ -8607,15 +8687,15 @@ except Exception:
8607
8687
  else
8608
8688
  if [ $retry -eq 0 ]; then
8609
8689
  if [ -n "$prd" ]; then
8610
- echo "Loki Mode with PRD at $prd. $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $checklist_status $app_runner_info $playwright_info $memory_context_section $rarv_instruction $memory_instruction $compaction_reminder $completion_instruction $sdlc_instruction $autonomous_suffix"
8690
+ echo "Loki Mode with PRD at $prd. $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $rarv_instruction $memory_instruction $compaction_reminder $completion_instruction $sdlc_instruction $autonomous_suffix"
8611
8691
  else
8612
- echo "Loki Mode. $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $checklist_status $app_runner_info $playwright_info $memory_context_section $analysis_instruction $rarv_instruction $memory_instruction $compaction_reminder $completion_instruction $sdlc_instruction $autonomous_suffix"
8692
+ echo "Loki Mode. $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $analysis_instruction $rarv_instruction $memory_instruction $compaction_reminder $completion_instruction $sdlc_instruction $autonomous_suffix"
8613
8693
  fi
8614
8694
  else
8615
8695
  if [ -n "$prd" ]; then
8616
- echo "Loki Mode - Resume iteration #$iteration (retry #$retry). PRD: $prd. $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $checklist_status $app_runner_info $playwright_info $memory_context_section $rarv_instruction $memory_instruction $compaction_reminder $completion_instruction $sdlc_instruction $autonomous_suffix"
8696
+ echo "Loki Mode - Resume iteration #$iteration (retry #$retry). PRD: $prd. $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $rarv_instruction $memory_instruction $compaction_reminder $completion_instruction $sdlc_instruction $autonomous_suffix"
8617
8697
  else
8618
- echo "Loki Mode - Resume iteration #$iteration (retry #$retry). $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $checklist_status $app_runner_info $playwright_info $memory_context_section Use .loki/generated-prd.md if exists. $rarv_instruction $memory_instruction $compaction_reminder $completion_instruction $sdlc_instruction $autonomous_suffix"
8698
+ echo "Loki Mode - Resume iteration #$iteration (retry #$retry). $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section Use .loki/generated-prd.md if exists. $rarv_instruction $memory_instruction $compaction_reminder $completion_instruction $sdlc_instruction $autonomous_suffix"
8619
8699
  fi
8620
8700
  fi
8621
8701
  fi
@@ -9475,6 +9555,22 @@ run_autonomous() {
9475
9555
  # Populate task queue from PRD (if no adapters already populated, runs once)
9476
9556
  populate_prd_queue "$prd_path"
9477
9557
 
9558
+ # Magic Modules BOOTSTRAP: extract design tokens from project so component
9559
+ # generation matches the codebase design language from iteration 1.
9560
+ if [ -x "${PROJECT_DIR}/autonomy/loki" ]; then
9561
+ PYTHONPATH="${PROJECT_DIR}" python3 -c "
9562
+ try:
9563
+ from magic.core.design_tokens import DesignTokens
9564
+ dt = DesignTokens('${TARGET_DIR}')
9565
+ observed = dt.extract_from_codebase(save=True)
9566
+ print(f'[magic] Extracted design tokens: '
9567
+ f'{len(observed.get(\"colors\",{}))} colors, '
9568
+ f'{len(observed.get(\"spacing\",{}))} spacing')
9569
+ except Exception as exc:
9570
+ print(f'[magic] Token extraction skipped: {exc}')
9571
+ " 2>&1 | grep -E '\[magic\]' || true
9572
+ fi
9573
+
9478
9574
  # Check max iterations before starting
9479
9575
  if check_max_iterations; then
9480
9576
  log_error "Max iterations already reached. Reset with: rm .loki/autonomy-state.json"
@@ -10087,6 +10183,18 @@ if __name__ == "__main__":
10087
10183
  log_warn "Documentation coverage gate: Score below threshold ($dc_count consecutive)"
10088
10184
  fi
10089
10185
  fi
10186
+ # Magic Modules debate gate - Gate 12 (v6.77.0)
10187
+ if [ "${LOKI_GATE_MAGIC_DEBATE:-true}" = "true" ] && [ "$ITERATION_COUNT" -gt 0 ]; then
10188
+ log_info "Quality gate: magic modules debate..."
10189
+ if run_magic_debate_gate; then
10190
+ clear_gate_failure "magic_debate"
10191
+ else
10192
+ local md_count
10193
+ md_count=$(track_gate_failure "magic_debate")
10194
+ gate_failures="${gate_failures}magic_debate,"
10195
+ log_warn "Magic Modules debate gate: BLOCK severity detected ($md_count consecutive)"
10196
+ fi
10197
+ fi
10090
10198
  # Store gate failures for prompt injection
10091
10199
  if [ -n "$gate_failures" ]; then
10092
10200
  echo "$gate_failures" > "${TARGET_DIR:-.}/.loki/quality/gate-failures.txt"
@@ -10106,6 +10214,9 @@ if __name__ == "__main__":
10106
10214
  auto_capture_episode "$ITERATION_COUNT" "$exit_code" "${rarv_phase:-iteration}" \
10107
10215
  "${prd_path:-codebase-analysis}" "$duration" "$log_file"
10108
10216
 
10217
+ # Magic Modules COMPOUND capture (v6.77.0): record component patterns
10218
+ _magic_compound_capture
10219
+
10109
10220
  # BUG-QG-008: Track iteration for convergence regardless of exit code
10110
10221
  if type council_track_iteration &>/dev/null; then
10111
10222
  council_track_iteration "$log_file"
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "6.75.3"
10
+ __version__ = "6.76.1"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v6.75.3
5
+ **Version:** v6.76.1
6
6
 
7
7
  ---
8
8
 
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '6.75.3'
60
+ __version__ = '6.76.1'
@@ -0,0 +1,471 @@
1
+ """MCP tools for Magic Modules.
2
+
3
+ Exposes loki magic functionality to AI coding assistants via the MCP
4
+ protocol. Tools delegate to the magic/ package where possible and fall
5
+ back to the `loki magic` CLI when direct imports are unavailable.
6
+
7
+ Registration pattern:
8
+ from mcp.magic_tools import register_magic_tools
9
+ register_magic_tools(mcp_server) # Called from mcp/server.py
10
+
11
+ All tools are resilient: they catch exceptions and return structured
12
+ error dicts rather than raising, so an MCP client always receives a
13
+ well-formed JSON response.
14
+ """
15
+
16
+ import json
17
+ import re
18
+ import subprocess
19
+ from typing import Any, Dict, List, Optional
20
+
21
+
22
+ # Regex used to validate component names. Mirrors the rule stated in the
23
+ # tool docstrings so callers get a consistent, early error.
24
+ _NAME_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9_-]*$")
25
+
26
+ # Default timeout (seconds) for CLI subprocess calls. Generation involves
27
+ # an LLM round-trip so it can legitimately take a while; we still cap it
28
+ # to avoid hanging the MCP server indefinitely.
29
+ _CLI_TIMEOUT = 600
30
+
31
+
32
+ def _error(message: str, **extra: Any) -> Dict[str, Any]:
33
+ """Return a structured error dict."""
34
+ result: Dict[str, Any] = {"ok": False, "error": message}
35
+ result.update(extra)
36
+ return result
37
+
38
+
39
+ def _ok(**fields: Any) -> Dict[str, Any]:
40
+ """Return a structured success dict."""
41
+ result: Dict[str, Any] = {"ok": True}
42
+ result.update(fields)
43
+ return result
44
+
45
+
46
+ def _validate_name(name: str) -> Optional[str]:
47
+ """Return an error message if name is invalid, else None."""
48
+ if not isinstance(name, str) or not name:
49
+ return "name must be a non-empty string"
50
+ if not _NAME_RE.match(name):
51
+ return (
52
+ "name must match ^[a-zA-Z][a-zA-Z0-9_-]*$ "
53
+ "(letters, digits, underscore, hyphen; must start with a letter)"
54
+ )
55
+ return None
56
+
57
+
58
+ def _run_loki(args: List[str], timeout: int = _CLI_TIMEOUT) -> Dict[str, Any]:
59
+ """Invoke the `loki` CLI and return a structured result.
60
+
61
+ The CLI is expected to emit JSON on stdout for machine consumers. If
62
+ stdout is not valid JSON we surface both stdout and stderr in the
63
+ error response so callers can diagnose.
64
+ """
65
+ cmd = ["loki", *args]
66
+ try:
67
+ proc = subprocess.run(
68
+ cmd,
69
+ capture_output=True,
70
+ text=True,
71
+ timeout=timeout,
72
+ check=False,
73
+ )
74
+ except FileNotFoundError:
75
+ return _error("loki CLI not found on PATH", command=cmd)
76
+ except subprocess.TimeoutExpired:
77
+ return _error(
78
+ "loki CLI timed out", command=cmd, timeout_seconds=timeout
79
+ )
80
+ except Exception as exc: # pragma: no cover - defensive
81
+ return _error(f"failed to invoke loki CLI: {exc}", command=cmd)
82
+
83
+ stdout = (proc.stdout or "").strip()
84
+ stderr = (proc.stderr or "").strip()
85
+
86
+ if proc.returncode != 0:
87
+ return _error(
88
+ f"loki CLI exited with code {proc.returncode}",
89
+ command=cmd,
90
+ stdout=stdout,
91
+ stderr=stderr,
92
+ returncode=proc.returncode,
93
+ )
94
+
95
+ # Try to decode JSON first; fall back to raw stdout if not JSON.
96
+ if stdout:
97
+ try:
98
+ parsed = json.loads(stdout)
99
+ if isinstance(parsed, dict):
100
+ parsed.setdefault("ok", True)
101
+ return parsed
102
+ return _ok(result=parsed)
103
+ except json.JSONDecodeError:
104
+ return _ok(stdout=stdout, stderr=stderr)
105
+
106
+ return _ok(stdout=stdout, stderr=stderr)
107
+
108
+
109
+ # ---------------------------------------------------------------------------
110
+ # Tool implementations
111
+ # ---------------------------------------------------------------------------
112
+
113
+
114
+ def loki_magic_generate(
115
+ name: str,
116
+ description: str = "",
117
+ target: str = "react",
118
+ placement: str = "",
119
+ tags: Optional[List[str]] = None,
120
+ ) -> Dict[str, Any]:
121
+ """Generate a new component from a description.
122
+
123
+ Args:
124
+ name: Component name (must match ^[a-zA-Z][a-zA-Z0-9_-]*$).
125
+ description: Natural-language description of what the component does.
126
+ target: 'react' | 'webcomponent' | 'both'.
127
+ placement: Optional file path where the component should be placed.
128
+ tags: List of tags for registry search.
129
+
130
+ Returns:
131
+ A dict with keys:
132
+ ok (bool), name (str), spec_path (str),
133
+ react_path (str, if target includes react),
134
+ webcomponent_path (str, if target includes webcomponent),
135
+ version (str), debate_passed (bool)
136
+ On failure: {"ok": False, "error": "..."}.
137
+ """
138
+ err = _validate_name(name)
139
+ if err:
140
+ return _error(err, name=name)
141
+
142
+ allowed_targets = {"react", "webcomponent", "both"}
143
+ if target not in allowed_targets:
144
+ return _error(
145
+ f"target must be one of {sorted(allowed_targets)}",
146
+ target=target,
147
+ )
148
+
149
+ if tags is not None and not isinstance(tags, list):
150
+ return _error("tags must be a list of strings", tags=tags)
151
+
152
+ args: List[str] = ["magic", "generate", name, "--target", target]
153
+ if description:
154
+ args.extend(["--description", description])
155
+ if placement:
156
+ args.extend(["--placement", placement])
157
+ if tags:
158
+ args.extend(["--tags", ",".join(str(t) for t in tags)])
159
+
160
+ return _run_loki(args)
161
+
162
+
163
+ def loki_magic_list(
164
+ query: str = "",
165
+ tags: Optional[List[str]] = None,
166
+ target: Optional[str] = None,
167
+ ) -> Dict[str, Any]:
168
+ """List / search registered components.
169
+
170
+ Args:
171
+ query: Substring match against component names.
172
+ tags: Filter by tags (AND logic).
173
+ target: Filter by target framework.
174
+
175
+ Returns:
176
+ {"ok": True, "count": int, "components": [...]} on success,
177
+ or a structured error dict on failure.
178
+ """
179
+ if tags is not None and not isinstance(tags, list):
180
+ return _error("tags must be a list of strings", tags=tags)
181
+
182
+ try:
183
+ # Prefer direct Python API when available.
184
+ from magic.core.registry import ComponentRegistry # type: ignore
185
+ except ImportError:
186
+ # Fall back to CLI if the magic package is not importable.
187
+ args: List[str] = ["magic", "list", "--json"]
188
+ if query:
189
+ args.extend(["--query", query])
190
+ if tags:
191
+ args.extend(["--tags", ",".join(str(t) for t in tags)])
192
+ if target:
193
+ args.extend(["--target", target])
194
+ return _run_loki(args)
195
+ except Exception as exc:
196
+ return _error(f"failed to import magic registry: {exc}")
197
+
198
+ try:
199
+ reg = ComponentRegistry(".")
200
+ results = reg.search(query=query, tags=tags, target=target)
201
+ except Exception as exc:
202
+ return _error(f"registry search failed: {exc}")
203
+
204
+ components = list(results) if results is not None else []
205
+ return _ok(count=len(components), components=components)
206
+
207
+
208
+ def loki_magic_get(name: str) -> Dict[str, Any]:
209
+ """Fetch details for a specific component.
210
+
211
+ Args:
212
+ name: Component name (must match ^[a-zA-Z][a-zA-Z0-9_-]*$).
213
+
214
+ Returns:
215
+ {"ok": True, "component": {...}} on success,
216
+ or a structured error dict on failure.
217
+ """
218
+ err = _validate_name(name)
219
+ if err:
220
+ return _error(err, name=name)
221
+
222
+ try:
223
+ from magic.core.registry import ComponentRegistry # type: ignore
224
+ except ImportError:
225
+ return _run_loki(["magic", "get", name, "--json"])
226
+ except Exception as exc:
227
+ return _error(f"failed to import magic registry: {exc}")
228
+
229
+ try:
230
+ reg = ComponentRegistry(".")
231
+ # Prefer a .get() method if the registry exposes one; otherwise
232
+ # fall back to searching by exact name.
233
+ component: Any = None
234
+ if hasattr(reg, "get"):
235
+ component = reg.get(name)
236
+ elif hasattr(reg, "find"):
237
+ component = reg.find(name)
238
+ else:
239
+ results = reg.search(query=name)
240
+ for item in (results or []):
241
+ if isinstance(item, dict) and item.get("name") == name:
242
+ component = item
243
+ break
244
+ except Exception as exc:
245
+ return _error(f"registry lookup failed: {exc}", name=name)
246
+
247
+ if component is None:
248
+ return _error(f"component not found: {name}", name=name)
249
+ return _ok(component=component)
250
+
251
+
252
+ def loki_magic_update(
253
+ name: str,
254
+ spec_update: str = "",
255
+ force: bool = False,
256
+ ) -> Dict[str, Any]:
257
+ """Update a component when its spec has changed.
258
+
259
+ Args:
260
+ name: Component name.
261
+ spec_update: Optional updated description / spec text to apply
262
+ before regeneration.
263
+ force: If False (default), only regenerates when the spec hash
264
+ has diverged. If True, always regenerates.
265
+
266
+ Returns:
267
+ A dict describing the update result on success, or a structured
268
+ error dict on failure.
269
+ """
270
+ err = _validate_name(name)
271
+ if err:
272
+ return _error(err, name=name)
273
+
274
+ args: List[str] = ["magic", "update", name]
275
+ if spec_update:
276
+ args.extend(["--spec-update", spec_update])
277
+ if force:
278
+ args.append("--force")
279
+ args.append("--json")
280
+
281
+ return _run_loki(args)
282
+
283
+
284
+ def loki_magic_debate(
285
+ name: str,
286
+ rounds: int = 3,
287
+ personas: Optional[List[str]] = None,
288
+ ) -> Dict[str, Any]:
289
+ """Run multi-persona debate on an existing component.
290
+
291
+ Args:
292
+ name: Component name.
293
+ rounds: Number of debate rounds (default 3).
294
+ personas: Optional list of persona identifiers. When omitted,
295
+ the debate runner uses its default persona set.
296
+
297
+ Returns:
298
+ Debate result including critiques and (if applicable) refined
299
+ code, or a structured error dict on failure.
300
+ """
301
+ err = _validate_name(name)
302
+ if err:
303
+ return _error(err, name=name)
304
+
305
+ if not isinstance(rounds, int) or rounds < 1:
306
+ return _error("rounds must be a positive integer", rounds=rounds)
307
+
308
+ if personas is not None and not isinstance(personas, list):
309
+ return _error("personas must be a list of strings", personas=personas)
310
+
311
+ try:
312
+ from magic.core.debate import DebateRunner # type: ignore
313
+ except ImportError:
314
+ args: List[str] = ["magic", "debate", name, "--rounds", str(rounds)]
315
+ if personas:
316
+ args.extend(["--personas", ",".join(str(p) for p in personas)])
317
+ args.append("--json")
318
+ return _run_loki(args)
319
+ except Exception as exc:
320
+ return _error(f"failed to import debate runner: {exc}")
321
+
322
+ try:
323
+ runner_kwargs: Dict[str, Any] = {"rounds": rounds}
324
+ if personas:
325
+ runner_kwargs["personas"] = personas
326
+ # DebateRunner's exact constructor signature is defined by the
327
+ # magic package. We pass the workspace root positionally and
328
+ # forward debate params as kwargs, staying tolerant of minor
329
+ # signature differences.
330
+ try:
331
+ runner = DebateRunner(".", **runner_kwargs)
332
+ except TypeError:
333
+ runner = DebateRunner(".")
334
+ for attr, value in runner_kwargs.items():
335
+ try:
336
+ setattr(runner, attr, value)
337
+ except Exception:
338
+ pass
339
+
340
+ if hasattr(runner, "run"):
341
+ result = runner.run(name)
342
+ elif hasattr(runner, "debate"):
343
+ result = runner.debate(name)
344
+ else:
345
+ return _error(
346
+ "DebateRunner exposes neither run() nor debate()",
347
+ name=name,
348
+ )
349
+ except Exception as exc:
350
+ return _error(f"debate failed: {exc}", name=name)
351
+
352
+ if isinstance(result, dict):
353
+ result.setdefault("ok", True)
354
+ return result
355
+ return _ok(result=result)
356
+
357
+
358
+ def loki_magic_tokens_extract() -> Dict[str, Any]:
359
+ """Extract design tokens from the current codebase.
360
+
361
+ Returns observed colors, spacing, typography, etc. Observation is
362
+ non-destructive: the registry is not modified (save=False).
363
+ """
364
+ try:
365
+ from magic.core.design_tokens import DesignTokens # type: ignore
366
+ except ImportError:
367
+ return _run_loki(["magic", "tokens", "extract", "--json"])
368
+ except Exception as exc:
369
+ return _error(f"failed to import design tokens module: {exc}")
370
+
371
+ try:
372
+ dt = DesignTokens(".")
373
+ observed = dt.extract_from_codebase(save=False)
374
+ except Exception as exc:
375
+ return _error(f"token extraction failed: {exc}")
376
+
377
+ if isinstance(observed, dict):
378
+ observed.setdefault("ok", True)
379
+ return observed
380
+ return _ok(tokens=observed)
381
+
382
+
383
+ def loki_magic_stats() -> Dict[str, Any]:
384
+ """Registry stats: total components, per-target counts, debate pass rate.
385
+
386
+ Returns:
387
+ A dict of statistics on success, or a structured error dict on
388
+ failure.
389
+ """
390
+ try:
391
+ from magic.core.registry import ComponentRegistry # type: ignore
392
+ except ImportError:
393
+ return _run_loki(["magic", "stats", "--json"])
394
+ except Exception as exc:
395
+ return _error(f"failed to import magic registry: {exc}")
396
+
397
+ try:
398
+ stats = ComponentRegistry(".").stats()
399
+ except Exception as exc:
400
+ return _error(f"failed to compute stats: {exc}")
401
+
402
+ if isinstance(stats, dict):
403
+ stats.setdefault("ok", True)
404
+ return stats
405
+ return _ok(stats=stats)
406
+
407
+
408
+ # ---------------------------------------------------------------------------
409
+ # Public registration entry point
410
+ # ---------------------------------------------------------------------------
411
+
412
+
413
+ # Tuple of (callable, public-tool-name). Tool names are stable and match
414
+ # the CLI verbs so MCP clients can predict them.
415
+ _TOOLS = (
416
+ (loki_magic_generate, "loki_magic_generate"),
417
+ (loki_magic_list, "loki_magic_list"),
418
+ (loki_magic_get, "loki_magic_get"),
419
+ (loki_magic_update, "loki_magic_update"),
420
+ (loki_magic_debate, "loki_magic_debate"),
421
+ (loki_magic_tokens_extract, "loki_magic_tokens_extract"),
422
+ (loki_magic_stats, "loki_magic_stats"),
423
+ )
424
+
425
+
426
+ def register_magic_tools(mcp_server: Any) -> List[str]:
427
+ """Wire the module's functions into a FastMCP server instance.
428
+
429
+ Usage from mcp/server.py:
430
+ from mcp.magic_tools import register_magic_tools
431
+ register_magic_tools(mcp)
432
+
433
+ Args:
434
+ mcp_server: A FastMCP-compatible server instance exposing a
435
+ `.tool()` decorator factory.
436
+
437
+ Returns:
438
+ The list of tool names that were successfully registered.
439
+ """
440
+ if mcp_server is None:
441
+ raise ValueError("mcp_server must not be None")
442
+
443
+ if not hasattr(mcp_server, "tool"):
444
+ raise TypeError(
445
+ "mcp_server does not expose a .tool() method; "
446
+ "expected a FastMCP-compatible instance"
447
+ )
448
+
449
+ registered: List[str] = []
450
+ for func, tool_name in _TOOLS:
451
+ try:
452
+ mcp_server.tool()(func)
453
+ registered.append(tool_name)
454
+ except Exception:
455
+ # Don't let a single bad registration break the rest. The
456
+ # integration pass can inspect the returned list to confirm
457
+ # which tools made it through.
458
+ continue
459
+ return registered
460
+
461
+
462
+ __all__ = [
463
+ "loki_magic_generate",
464
+ "loki_magic_list",
465
+ "loki_magic_get",
466
+ "loki_magic_update",
467
+ "loki_magic_debate",
468
+ "loki_magic_tokens_extract",
469
+ "loki_magic_stats",
470
+ "register_magic_tools",
471
+ ]
package/mcp/server.py CHANGED
@@ -1954,6 +1954,19 @@ async def loki_phase_report() -> str:
1954
1954
  Use loki_state_get and loki_task_queue_list to gather data."""
1955
1955
 
1956
1956
 
1957
+ # ============================================================
1958
+ # MAGIC MODULES TOOLS (spec-driven component generation)
1959
+ # ============================================================
1960
+
1961
+ try:
1962
+ from mcp.magic_tools import register_magic_tools
1963
+ register_magic_tools(mcp)
1964
+ except Exception as _magic_err:
1965
+ # Magic Modules is optional; log and continue if unavailable
1966
+ import sys as _sys
1967
+ print(f"[warn] magic_tools registration skipped: {_magic_err}", file=_sys.stderr)
1968
+
1969
+
1957
1970
  # ============================================================
1958
1971
  # MAIN
1959
1972
  # ============================================================