maestro-skills 0.1.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 (56) hide show
  1. package/.github/workflows/ci.yml +26 -0
  2. package/.github/workflows/publish-npm.yml +30 -0
  3. package/CONTRIBUTING.md +31 -0
  4. package/LICENSE +21 -0
  5. package/README.md +300 -0
  6. package/SECURITY.md +33 -0
  7. package/docs/github-workflow.md +96 -0
  8. package/docs/maestro-skills-cli.md +113 -0
  9. package/package.json +35 -0
  10. package/packages/maestro-skills/README.md +37 -0
  11. package/packages/maestro-skills/agents.json +36 -0
  12. package/packages/maestro-skills/bin/cli.js +37 -0
  13. package/packages/maestro-skills/lib/detect-agents.js +28 -0
  14. package/packages/maestro-skills/lib/install.js +58 -0
  15. package/packages/maestro-skills/lib/paths.js +42 -0
  16. package/packages/maestro-skills/lib/remove.js +71 -0
  17. package/packages/maestro-skills/lib/run-manifest.js +92 -0
  18. package/packages/maestro-skills/lib/setup.js +115 -0
  19. package/packages/maestro-skills/package.json +47 -0
  20. package/packages/maestro-skills/test/agents.test.js +17 -0
  21. package/packages/rodovalhofs-maestro/agents.json +36 -0
  22. package/packages/rodovalhofs-maestro/bin/cli.js +10 -0
  23. package/packages/rodovalhofs-maestro/lib/detect-agents.js +28 -0
  24. package/packages/rodovalhofs-maestro/lib/install.js +58 -0
  25. package/packages/rodovalhofs-maestro/lib/paths.js +42 -0
  26. package/packages/rodovalhofs-maestro/lib/remove.js +71 -0
  27. package/packages/rodovalhofs-maestro/lib/run-manifest.js +92 -0
  28. package/packages/rodovalhofs-maestro/lib/setup.js +115 -0
  29. package/packages/rodovalhofs-maestro/package.json +33 -0
  30. package/scripts/sync-skill-to-cli.mjs +75 -0
  31. package/scripts/sync-templates.ps1 +22 -0
  32. package/skills/maestro/SKILL.md +272 -0
  33. package/skills/maestro/maestro-exclude.example.txt +6 -0
  34. package/skills/maestro/scripts/bm25.py +70 -0
  35. package/skills/maestro/scripts/build_manifest.py +183 -0
  36. package/skills/maestro/scripts/concept_gaps.py +196 -0
  37. package/skills/maestro/scripts/domains.py +148 -0
  38. package/skills/maestro/scripts/intents.py +167 -0
  39. package/skills/maestro/scripts/maestro_paths.py +41 -0
  40. package/skills/maestro/scripts/route_tasks.py +101 -0
  41. package/skills/maestro/scripts/routing.py +106 -0
  42. package/skills/maestro/scripts/search_skills.py +287 -0
  43. package/skills/maestro/scripts/synonyms.py +47 -0
  44. package/templates/.github/ISSUE_TEMPLATE/bug_report.yml +34 -0
  45. package/templates/.github/ISSUE_TEMPLATE/chore.yml +17 -0
  46. package/templates/.github/ISSUE_TEMPLATE/config.yml +1 -0
  47. package/templates/.github/ISSUE_TEMPLATE/feature_request.yml +27 -0
  48. package/templates/.github/workflows/ci-failure-to-issue.yml +47 -0
  49. package/templates/.github/workflows/ci.yml +27 -0
  50. package/templates/CONTRIBUTING.md +22 -0
  51. package/templates/labels.json +12 -0
  52. package/templates/pull_request_template.md +18 -0
  53. package/tests/fixtures/sample-manifest.json +76 -0
  54. package/tests/test_concept_gaps.py +63 -0
  55. package/tests/test_maestro_paths.py +29 -0
  56. package/tests/test_search_routing.py +161 -0
@@ -0,0 +1,272 @@
1
+ ---
2
+ name: maestro
3
+ description: >-
4
+ Meta-orchestrator that analyzes the user prompt, searches local skills via
5
+ hybrid BM25 routing (intents, tags, synonyms, P0-P3), builds an editable
6
+ dependency graph, and spawns focused subagents
7
+ after user confirmation. Use when the user invokes $maestro, asks which skills
8
+ to use, wants optimal skill routing, or has a complex task spanning multiple
9
+ domains and does not know which skill to call.
10
+ disable-model-invocation: true
11
+ ---
12
+
13
+ # Maestro
14
+
15
+ Orchestrate local Cursor skills. Maestro does **not** implement work itself — it discovers skills, plans a dependency graph, waits for user edits/approval, then spawns subagents.
16
+
17
+ ## Hard rules
18
+
19
+ 1. **Never spawn subagents before the user confirms the graph.**
20
+ 2. **Max 10 graph nodes.** Fuse redundant skills that share the same role into one node.
21
+ 3. **Pass explicit `SKILL.md` paths** to every subagent prompt.
22
+ 4. **Do not invoke `$maestro` recursively** from subagents.
23
+ 5. Routers like `index`, `data-visualization`, `grill-me`, `conselho` stay directly invocable — maestro may include them as hub nodes.
24
+ 6. **Repos versionados:** aplicar o fluxo GitHub generico (Issues + PR + CI). Ver secao [GitHub workflow](#github-workflow) e `docs/github-workflow.md` neste repositorio.
25
+
26
+ ## Artifacts
27
+
28
+ | File | Purpose |
29
+ |------|---------|
30
+ | `~/.cursor/skills-manifest.json` | Searchable catalog (regenerate with build_manifest) |
31
+ | `~/.cursor/maestro-exclude.txt` | Skills banned from search |
32
+ | `scripts/build_manifest.py` | Regenerate manifest (tags + `~/.agents/skills`) |
33
+ | `scripts/search_skills.py` | Hybrid BM25 search + routing P0-P3 + `discover` |
34
+ | `scripts/route_tasks.py` | Batch route decomposed sub-tasks |
35
+ | `scripts/concept_gaps.py` | Detect concepts in prompt without local skill coverage |
36
+
37
+ ## Workflow
38
+
39
+ ### 1. Refresh manifest (if stale or missing)
40
+
41
+ ```bash
42
+ py -3 "%USERPROFILE%\.cursor\skills\maestro\scripts\build_manifest.py" --project-root "<workspace-root>"
43
+ ```
44
+
45
+ Fallback: `python` instead of `py -3` if needed.
46
+
47
+ On Unix: `python3 ~/.cursor/skills/maestro/scripts/build_manifest.py --project-root "<workspace-root>"`
48
+
49
+ ### 2. Search skills
50
+
51
+ ```bash
52
+ py -3 "%USERPROFILE%\.cursor\skills\maestro\scripts\search_skills.py" "<user prompt>" --json
53
+ ```
54
+
55
+ Optional: `--domain web|data-viz|analytics|design|creative|devops-git|video-media|integrations|security|meta|general`
56
+
57
+ JSON includes `routing` (P0-P3), `confidence`, `mode`, `discover`, `intent_boosts`.
58
+
59
+ Read `discover` before building the graph:
60
+
61
+ | `discover.triggered` | Meaning |
62
+ |----------------------|---------|
63
+ | `true` | Open a **Discover** branch via `find-skills` (see below) |
64
+ | `false` | Proceed with installed skills only |
65
+
66
+ **Discover reasons:** `force_discover` (explicit “find skill”), `weak_match`, `single_local_skill`, `concept_gap` (e.g. prompt mentions `skeleton-loader` with no local skill).
67
+
68
+ Follow `routing.priority` and `routing.decision`; use `results` as evidence.
69
+
70
+ ### 2a. Discover branch (`discover.triggered: true`)
71
+
72
+ When the JSON signals discover, run **before presenting Graph 1**:
73
+
74
+ ```bash
75
+ npx skills find "<query from discover.queries[0]>"
76
+ ```
77
+
78
+ Repeat for each entry in `discover.queries` (max 2 concept gaps; extra gaps appear in `discover.gap_notes` as graph notes).
79
+
80
+ **Graph 1 — pré-discover** (wait for user **ok**):
81
+
82
+ | # | Nó | Skills | Depende de | Subagente |
83
+ |---|-----|--------|------------|-----------|
84
+ | 1 | Discover `<gap>` | `find-skills` | — | generalPurpose |
85
+ | | → candidata: `owner/repo@skill` (installs) | | | |
86
+ | 2 | Fallback local | `<discover.local_fallback>` | 1 (se discover falhar) | generalPurpose |
87
+ | 3 | Executar tarefa | `<instalada ou #2>` | 1 ou 2 | generalPurpose |
88
+
89
+ After user confirms Graph 1, spawn `find-skills` subagent to install:
90
+
91
+ ```bash
92
+ npx skills add <owner/repo@skill> -g -a cursor -y
93
+ py -3 "%USERPROFILE%\.cursor\skills\maestro\scripts\build_manifest.py" --project-root "<workspace-root>"
94
+ ```
95
+
96
+ Re-run search with the original prompt. Present **Graph 2 — pós-discover** and wait for a **second ok** before execution subagents.
97
+
98
+ **If `npx skills find` returns nothing:** Graph 2 uses `discover.local_fallback` only + note about `npx skills init`. If no fallback either, stop and ask the user how to proceed.
99
+
100
+ **If match is strong and `discover.triggered: false`:** skip Discover; single graph as usual.
101
+
102
+ ### 2b. Refine graph nodes (after draft decomposition)
103
+
104
+ ```bash
105
+ printf '%s\n' "<task 1>" "<task 2>" | py -3 "%USERPROFILE%\.cursor\skills\maestro\scripts\route_tasks.py" --json
106
+ ```
107
+
108
+ Merge router output into the graph: prefer installed `path` when `mode` is `auto-load`; honor `discover` flags from each task.
109
+
110
+ Para tarefas com codigo versionado, inclua no grafo nos de implementacao, git e sintese:
111
+
112
+ ```bash
113
+ py -3 "%USERPROFILE%\.cursor\skills\maestro\scripts\search_skills.py" "github issues PR yeet branch feat CI" --domain devops-git --json
114
+ ```
115
+
116
+ Leia `docs/github-workflow.md` (neste repo ou copiado para o projeto) quando a tarefa tocar codigo versionado.
117
+
118
+ ### 3. Handle weak matches
119
+
120
+ If `weak_match: true` and `discover.triggered: true`, prefer the **Discover branch** (step 2a) instead of only asking domain.
121
+
122
+ If `weak_match: true` and `discover.triggered: false`:
123
+
124
+ - **`low_top_score` / `tight_spread` / `no_results`** → ask domain in one line:
125
+
126
+ > Domínio não ficou claro. Qual se aplica?
127
+ > A) Web/apps B) Data viz C) Analytics D) Design E) Creative F) Git/CI G) Integrations H) Security I) Outro
128
+
129
+ Re-run search with `--domain <choice>`.
130
+
131
+ - If prompt involves **codebase/repo** and match is still weak → run one `explore` subagent (`readonly: true`, `model: fast`) to gather context, append findings to query, search again.
132
+
133
+ ### 4. Build dependency graph
134
+
135
+ From top search results (3–5 skills), design a **DAG**:
136
+
137
+ - **Single dominant skill** (score clearly ahead) → 1 node graph.
138
+ - **Pipeline** → order by dependency (research before build, build before QA).
139
+ - **Parallel** → only independent branches (e.g. `research` + `audit`), then merge.
140
+ - **Hub routers** → use `index` or `data-visualization` as one node instead of many leaf skills when the task is broad within that plugin.
141
+
142
+ **Fusion:** combine skills with the same role into one node:
143
+
144
+ ```text
145
+ Node 2 — Implement UI [react-best-practices + shadcn-best-practices]
146
+ skills: react-best-practices, shadcn-best-practices
147
+ path: .../react-best-practices/SKILL.md (read all listed skills)
148
+ ```
149
+
150
+ ### 5. Present editable graph (mandatory)
151
+
152
+ Show this template and **wait for user edits or confirmation**:
153
+
154
+ ```markdown
155
+ ## Maestro — grafo proposto
156
+
157
+ **Prompt:** <one line>
158
+ **Domínio:** <domain_label>
159
+
160
+ | # | Nó | Skills | Depende de | Subagente |
161
+ |---|-----|--------|------------|-----------|
162
+ | 1 | <role> | `skill-a` | — | explore / generalPurpose |
163
+ | 2 | <role> | `skill-b`, `skill-c` | 1 | generalPurpose |
164
+
165
+ **Paths:**
166
+ - `skill-a`: C:/Users/.../.cursor/skills/skill-a/SKILL.md
167
+
168
+ Edite ordem, skills ou nós. Responda **ok** para executar ou descreva mudanças.
169
+ ```
170
+
171
+ If graph would exceed 10 nodes: fuse, drop lowest-score leaves, or ask user which to cut.
172
+
173
+ ### 6. Spawn subagents (after user OK)
174
+
175
+ Execute graph in dependency order. Parallelize independent nodes in one message.
176
+
177
+ **Subagent type map:**
178
+
179
+ | Task | `subagent_type` | Notes |
180
+ |------|-----------------|-------|
181
+ | Codebase discovery | `explore` | `readonly: true` |
182
+ | Implementation / analysis with skill | `generalPurpose` | Include full skill path + user task |
183
+ | Git / CI / shell ops | `shell` | Only when skill is git/ci focused |
184
+ | Read-only code review | `generalPurpose` | `readonly: true` |
185
+
186
+ **Subagent prompt template:**
187
+
188
+ ```text
189
+ Read and follow this skill before acting:
190
+ <absolute-path-to-SKILL.md>
191
+
192
+ If multiple skills listed, read all paths and synthesize guidance.
193
+
194
+ User task:
195
+ <original user prompt + node-specific slice>
196
+
197
+ Prior node outputs:
198
+ <summary if any>
199
+
200
+ Return: concise result for maestro synthesis.
201
+ ```
202
+
203
+ ### 7. Synthesize
204
+
205
+ After all nodes complete, maestro (parent) delivers:
206
+
207
+ - What ran (nodes + skills)
208
+ - Key outcomes per node
209
+ - Recommended next step for the user
210
+ - **GitHub (se aplicavel):** Issue `#N`, branch `feat/N-slug`, PR URL, status CI
211
+
212
+ ## GitHub workflow
213
+
214
+ Fluxo padrao para qualquer repositorio versionado:
215
+
216
+ | Regra | Valor |
217
+ |-------|--------|
218
+ | Branch | `feat/<N>-slug` ou `fix/<N>-slug` — nunca push direto na branch principal |
219
+ | Entrega | Issue → branch → testes locais → build → PR → CI verde → merge |
220
+ | CI | Job **test** antes de **build**; falha abre Issue com label `ci:falha` |
221
+ | Skills | `github`, `yeet`, `gh-fix-ci` nos nos git/publicacao/CI |
222
+ | Docs | `docs/github-workflow.md` e `templates/` neste repositorio |
223
+ | Agentes | Commits com `Refs #N` / `Closes #N`; informar PR URL ao encerrar |
224
+
225
+ **Fusion no grafo:** tarefas com implementacao + git viram pipeline `implementacao` → `yeet` (branch + PR draft), salvo tarefa so de leitura.
226
+
227
+ **Aplicar templates em um projeto:**
228
+
229
+ ```bash
230
+ # Copie manualmente ou adapte o script sync-templates.ps1
231
+ cp -r templates/.github <seu-projeto>/
232
+ cp templates/CONTRIBUTING.md <seu-projeto>/
233
+ ```
234
+
235
+ ## Domain buckets
236
+
237
+ `web`, `data-viz`, `analytics`, `design`, `creative`, `devops-git`, `video-media`, `integrations`, `security`, `meta`, `general`
238
+
239
+ ## Hub skills (prefer as single node when broad)
240
+
241
+ - `index` — Product Design plugin router
242
+ - `data-visualization` / `build-web-data-visualization-data-visualization` — viz router
243
+
244
+ ## Examples
245
+
246
+ **User:** `$maestro vamos colocar skeleton-loader na UI`
247
+
248
+ 1. Search → `discover.triggered` (`concept_gap: skeleton-loader`); local UI skills as fallback
249
+ 2. `npx skills find "skeleton-loader ui web"` → candidata no Grafo 1
250
+ 3. User ok → find-skills installs → rebuild manifest → Grafo 2 → user ok → implement
251
+
252
+ **User:** `$maestro corrigir CI quebrado no PR`
253
+
254
+ 1. Search → `gh-fix-ci`, `github`
255
+ 2. Graph: 1 node → `gh-fix-ci` (+ regra GitHub: branch `feat/N`, PR, nao main)
256
+ 3. User confirms → spawn `shell` or `generalPurpose` subagent with skill path
257
+
258
+ **User:** `$maestro criar dashboard de vendas com React`
259
+
260
+ 1. Search domain `web` + `data-viz` → may need domain question
261
+ 2. Graph: `data-visualization` → `react-and-nextjs-data-visualization` → `dashboards-and-real-time-visualization`
262
+ 3. User edits → confirm → sequential subagents
263
+
264
+ ## Maintenance
265
+
266
+ After syncing Codex skills, regenerate manifest:
267
+
268
+ ```bash
269
+ py -3 ~/.cursor/skills/maestro/scripts/build_manifest.py
270
+ ```
271
+
272
+ Or run the full sync script which rebuilds manifest automatically.
@@ -0,0 +1,6 @@
1
+ # One skill name per line. Lines starting with # are ignored.
2
+ # Rebuild manifest after edits: python ~/.cursor/skills/maestro/scripts/build_manifest.py
3
+
4
+ # Examples (uncomment to use):
5
+ # zoom-apps-sdk
6
+ # twilio-voice-twiml
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env python3
2
+ """Minimal BM25 for maestro skill search."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import re
7
+ from collections import defaultdict
8
+ from math import log
9
+
10
+
11
+ class BM25:
12
+ def __init__(self, k1: float = 1.5, b: float = 0.75) -> None:
13
+ self.k1 = k1
14
+ self.b = b
15
+ self.corpus: list[list[str]] = []
16
+ self.doc_lengths: list[int] = []
17
+ self.avgdl = 0.0
18
+ self.idf: dict[str, float] = {}
19
+ self.doc_freqs: dict[str, int] = defaultdict(int)
20
+ self.n = 0
21
+
22
+ def tokenize(self, text: str) -> list[str]:
23
+ text = re.sub(r"[^\w\s]", " ", str(text).lower())
24
+ return [w for w in text.split() if len(w) > 2]
25
+
26
+ def fit(self, documents: list[str]) -> None:
27
+ self.corpus = [self.tokenize(doc) for doc in documents]
28
+ self.n = len(self.corpus)
29
+ if self.n == 0:
30
+ return
31
+ self.doc_lengths = [len(doc) for doc in self.corpus]
32
+ self.avgdl = sum(self.doc_lengths) / self.n
33
+ self.doc_freqs = defaultdict(int)
34
+ self.idf = {}
35
+
36
+ for doc in self.corpus:
37
+ seen: set[str] = set()
38
+ for word in doc:
39
+ if word not in seen:
40
+ self.doc_freqs[word] += 1
41
+ seen.add(word)
42
+
43
+ for word, freq in self.doc_freqs.items():
44
+ self.idf[word] = log((self.n - freq + 0.5) / (freq + 0.5) + 1)
45
+
46
+ def score(self, query: str) -> list[tuple[int, float]]:
47
+ if self.n == 0:
48
+ return []
49
+ query_tokens = self.tokenize(query)
50
+ scores: list[tuple[int, float]] = []
51
+
52
+ for idx, doc in enumerate(self.corpus):
53
+ total = 0.0
54
+ doc_len = self.doc_lengths[idx]
55
+ term_freqs: dict[str, int] = defaultdict(int)
56
+ for word in doc:
57
+ term_freqs[word] += 1
58
+
59
+ for token in query_tokens:
60
+ if token not in self.idf:
61
+ continue
62
+ tf = term_freqs[token]
63
+ idf = self.idf[token]
64
+ numerator = tf * (self.k1 + 1)
65
+ denominator = tf + self.k1 * (1 - self.b + self.b * doc_len / self.avgdl)
66
+ total += idf * numerator / denominator
67
+
68
+ scores.append((idx, total))
69
+
70
+ return sorted(scores, key=lambda x: x[1], reverse=True)
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env python3
2
+ """Build skills-manifest.json for maestro from personal and project skills."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import re
9
+ import sys
10
+ from datetime import datetime, timezone
11
+ from pathlib import Path
12
+
13
+ SCRIPT_DIR = Path(__file__).resolve().parent
14
+ sys.path.insert(0, str(SCRIPT_DIR))
15
+
16
+ from domains import classify_skill # noqa: E402
17
+ from maestro_paths import ( # noqa: E402
18
+ EXCLUDE_PATH,
19
+ MANIFEST_PATH,
20
+ all_skill_roots,
21
+ )
22
+
23
+ MAESTRO_NAMES = {"maestro"}
24
+
25
+
26
+ def parse_frontmatter(text: str) -> dict[str, str]:
27
+ match = re.match(r"^---\s*\n(.*?)\n---", text, re.DOTALL)
28
+ if not match:
29
+ return {}
30
+ block = match.group(1)
31
+ result: dict[str, str] = {}
32
+ current_key: str | None = None
33
+ current_lines: list[str] = []
34
+
35
+ def flush() -> None:
36
+ nonlocal current_key, current_lines
37
+ if current_key is not None:
38
+ result[current_key] = "\n".join(current_lines).strip().strip('"').strip("'")
39
+ current_key = None
40
+ current_lines = []
41
+
42
+ for line in block.splitlines():
43
+ if re.match(r"^[A-Za-z0-9_-]+:\s*", line):
44
+ flush()
45
+ key, _, value = line.partition(":")
46
+ current_key = key.strip()
47
+ current_lines = [value.strip()]
48
+ elif current_key is not None and (line.startswith(" ") or line.startswith("\t")):
49
+ current_lines.append(line.strip())
50
+ elif current_key is not None and line.strip():
51
+ current_lines.append(line.strip())
52
+ flush()
53
+ return result
54
+
55
+
56
+ def parse_tags_from_text(text: str) -> list[str]:
57
+ match = re.match(r"^---\s*\n(.*?)\n---", text, re.DOTALL)
58
+ if not match:
59
+ return []
60
+ block = match.group(1)
61
+ tags: list[str] = []
62
+ in_tags = False
63
+ for line in block.splitlines():
64
+ stripped = line.strip()
65
+ if re.match(r"^tags:\s*$", stripped):
66
+ in_tags = True
67
+ continue
68
+ if re.match(r"^tags:\s*\[", stripped):
69
+ inner = stripped.split("[", 1)[1].rstrip("]")
70
+ tags.extend(
71
+ item.strip().strip("'\"") for item in inner.split(",") if item.strip()
72
+ )
73
+ return tags
74
+ if in_tags:
75
+ if line.startswith(" - "):
76
+ tags.append(line.strip()[2:].strip().strip('"').strip("'"))
77
+ elif stripped and not line.startswith(" "):
78
+ in_tags = False
79
+ return tags
80
+
81
+
82
+ def load_exclude_list() -> set[str]:
83
+ if not EXCLUDE_PATH.exists():
84
+ return set()
85
+ names: set[str] = set()
86
+ for line in EXCLUDE_PATH.read_text(encoding="utf-8").splitlines():
87
+ line = line.strip()
88
+ if not line or line.startswith("#"):
89
+ continue
90
+ names.add(line.lower())
91
+ return names
92
+
93
+
94
+ def iter_skill_dirs(root: Path, scope: str) -> list[dict]:
95
+ if not root.is_dir():
96
+ return []
97
+ excluded = load_exclude_list()
98
+ entries: list[dict] = []
99
+
100
+ for child in sorted(root.iterdir()):
101
+ if not child.is_dir():
102
+ continue
103
+ skill_md = child / "SKILL.md"
104
+ if not skill_md.is_file():
105
+ continue
106
+
107
+ folder_name = child.name
108
+ if folder_name.lower() in MAESTRO_NAMES or folder_name.lower() in excluded:
109
+ continue
110
+
111
+ text = skill_md.read_text(encoding="utf-8", errors="replace")
112
+ meta = parse_frontmatter(text)
113
+ name = meta.get("name", folder_name)
114
+ description = meta.get("description", "")
115
+ if not description:
116
+ description = text[:400].replace("\n", " ")
117
+
118
+ raw_domain = meta.get("domain", "")
119
+ if raw_domain.lower() in {"cybersecurity", "security"}:
120
+ skill_domain = "security"
121
+ else:
122
+ skill_domain = classify_skill(name, description)
123
+
124
+ tags = parse_tags_from_text(text)
125
+
126
+ entries.append(
127
+ {
128
+ "name": name,
129
+ "folder": folder_name,
130
+ "description": description[:1024],
131
+ "tags": tags,
132
+ "domain": skill_domain,
133
+ "path": str(skill_md).replace("\\", "/"),
134
+ "scope": scope,
135
+ "installed": True,
136
+ }
137
+ )
138
+
139
+ return entries
140
+
141
+
142
+ def build_manifest(project_root: Path | None) -> dict:
143
+ skills: list[dict] = []
144
+ scanned_roots: list[dict[str, str]] = []
145
+
146
+ for root, scope in all_skill_roots(project_root):
147
+ scanned_roots.append(
148
+ {"path": str(root).replace("\\", "/"), "scope": scope}
149
+ )
150
+ skills.extend(iter_skill_dirs(root, scope))
151
+
152
+ dedup: dict[str, dict] = {}
153
+ for skill in skills:
154
+ key = f"{skill['scope']}:{skill['folder']}"
155
+ dedup[key] = skill
156
+
157
+ return {
158
+ "version": 3,
159
+ "generated_at": datetime.now(timezone.utc).isoformat(),
160
+ "maestro_home": str(MANIFEST_PATH.parent).replace("\\", "/"),
161
+ "scanned_roots": scanned_roots,
162
+ "skill_count": len(dedup),
163
+ "skills": sorted(dedup.values(), key=lambda s: (s["domain"], s["name"])),
164
+ }
165
+
166
+
167
+ def main() -> int:
168
+ parser = argparse.ArgumentParser(description="Build maestro skills manifest")
169
+ parser.add_argument("--project-root", default=None, help="Project root with agent skills dirs")
170
+ parser.add_argument("--output", default=str(MANIFEST_PATH), help="Manifest output path")
171
+ args = parser.parse_args()
172
+
173
+ project_root = Path(args.project_root).resolve() if args.project_root else None
174
+ manifest = build_manifest(project_root)
175
+ output = Path(args.output)
176
+ output.parent.mkdir(parents=True, exist_ok=True)
177
+ output.write_text(json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8")
178
+ print(f"Wrote {manifest['skill_count']} skills to {output}")
179
+ return 0
180
+
181
+
182
+ if __name__ == "__main__":
183
+ raise SystemExit(main())