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.
- package/.github/workflows/ci.yml +26 -0
- package/.github/workflows/publish-npm.yml +30 -0
- package/CONTRIBUTING.md +31 -0
- package/LICENSE +21 -0
- package/README.md +300 -0
- package/SECURITY.md +33 -0
- package/docs/github-workflow.md +96 -0
- package/docs/maestro-skills-cli.md +113 -0
- package/package.json +35 -0
- package/packages/maestro-skills/README.md +37 -0
- package/packages/maestro-skills/agents.json +36 -0
- package/packages/maestro-skills/bin/cli.js +37 -0
- package/packages/maestro-skills/lib/detect-agents.js +28 -0
- package/packages/maestro-skills/lib/install.js +58 -0
- package/packages/maestro-skills/lib/paths.js +42 -0
- package/packages/maestro-skills/lib/remove.js +71 -0
- package/packages/maestro-skills/lib/run-manifest.js +92 -0
- package/packages/maestro-skills/lib/setup.js +115 -0
- package/packages/maestro-skills/package.json +47 -0
- package/packages/maestro-skills/test/agents.test.js +17 -0
- package/packages/rodovalhofs-maestro/agents.json +36 -0
- package/packages/rodovalhofs-maestro/bin/cli.js +10 -0
- package/packages/rodovalhofs-maestro/lib/detect-agents.js +28 -0
- package/packages/rodovalhofs-maestro/lib/install.js +58 -0
- package/packages/rodovalhofs-maestro/lib/paths.js +42 -0
- package/packages/rodovalhofs-maestro/lib/remove.js +71 -0
- package/packages/rodovalhofs-maestro/lib/run-manifest.js +92 -0
- package/packages/rodovalhofs-maestro/lib/setup.js +115 -0
- package/packages/rodovalhofs-maestro/package.json +33 -0
- package/scripts/sync-skill-to-cli.mjs +75 -0
- package/scripts/sync-templates.ps1 +22 -0
- package/skills/maestro/SKILL.md +272 -0
- package/skills/maestro/maestro-exclude.example.txt +6 -0
- package/skills/maestro/scripts/bm25.py +70 -0
- package/skills/maestro/scripts/build_manifest.py +183 -0
- package/skills/maestro/scripts/concept_gaps.py +196 -0
- package/skills/maestro/scripts/domains.py +148 -0
- package/skills/maestro/scripts/intents.py +167 -0
- package/skills/maestro/scripts/maestro_paths.py +41 -0
- package/skills/maestro/scripts/route_tasks.py +101 -0
- package/skills/maestro/scripts/routing.py +106 -0
- package/skills/maestro/scripts/search_skills.py +287 -0
- package/skills/maestro/scripts/synonyms.py +47 -0
- package/templates/.github/ISSUE_TEMPLATE/bug_report.yml +34 -0
- package/templates/.github/ISSUE_TEMPLATE/chore.yml +17 -0
- package/templates/.github/ISSUE_TEMPLATE/config.yml +1 -0
- package/templates/.github/ISSUE_TEMPLATE/feature_request.yml +27 -0
- package/templates/.github/workflows/ci-failure-to-issue.yml +47 -0
- package/templates/.github/workflows/ci.yml +27 -0
- package/templates/CONTRIBUTING.md +22 -0
- package/templates/labels.json +12 -0
- package/templates/pull_request_template.md +18 -0
- package/tests/fixtures/sample-manifest.json +76 -0
- package/tests/test_concept_gaps.py +63 -0
- package/tests/test_maestro_paths.py +29 -0
- 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,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())
|