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,106 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""P0-P3 routing decisions for maestro skill matches."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
AUTO_LOAD_CONFIDENCE = 0.22
|
|
10
|
+
OPTIONAL_LOAD_CONFIDENCE = 0.12
|
|
11
|
+
REFERENCE_BM25_SCORE = 8.0
|
|
12
|
+
|
|
13
|
+
HIGH_RISK_PATTERNS = [
|
|
14
|
+
r"config\b", r"\.env", r"auth", r"delete", r"remove", r"rm\s",
|
|
15
|
+
r"push\s+--force", r"deploy", r"secret", r"password", r"token",
|
|
16
|
+
r"excluir", r"remover", r"deletar", r"deploy", r"producao", r"produção",
|
|
17
|
+
r"\bPOST\b", r"\bPUT\b", r"\bDELETE\b",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
VALID_MODES = {"auto-load", "recommend", "bypass"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def bm25_to_confidence(score: float) -> float:
|
|
24
|
+
"""Map BM25 score to 0-1 confidence without external calibration."""
|
|
25
|
+
if score <= 0:
|
|
26
|
+
return 0.0
|
|
27
|
+
return min(1.0, score / REFERENCE_BM25_SCORE)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def is_high_risk(task: str) -> bool:
|
|
31
|
+
task_lower = task.lower()
|
|
32
|
+
return any(re.search(pat, task_lower, re.IGNORECASE) for pat in HIGH_RISK_PATTERNS)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def select_mode(
|
|
36
|
+
confidence: float,
|
|
37
|
+
high_risk: bool,
|
|
38
|
+
suggested_mode: str = "",
|
|
39
|
+
bypass: bool = False,
|
|
40
|
+
) -> str:
|
|
41
|
+
if bypass:
|
|
42
|
+
return "bypass"
|
|
43
|
+
if high_risk:
|
|
44
|
+
return "recommend"
|
|
45
|
+
if suggested_mode in VALID_MODES:
|
|
46
|
+
return suggested_mode
|
|
47
|
+
if confidence >= AUTO_LOAD_CONFIDENCE:
|
|
48
|
+
return "auto-load"
|
|
49
|
+
if confidence >= OPTIONAL_LOAD_CONFIDENCE:
|
|
50
|
+
return "recommend"
|
|
51
|
+
return "recommend"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def build_routing(
|
|
55
|
+
task: str,
|
|
56
|
+
matches: list[dict[str, Any]],
|
|
57
|
+
high_risk: bool,
|
|
58
|
+
bypass: bool = False,
|
|
59
|
+
) -> dict[str, Any]:
|
|
60
|
+
if bypass or not matches:
|
|
61
|
+
return {
|
|
62
|
+
"priority": "P3",
|
|
63
|
+
"decision": "bypass",
|
|
64
|
+
"reason": "Simple or answer-only task; proceed without skill routing.",
|
|
65
|
+
"load_limit": 0,
|
|
66
|
+
"report_policy": "silent",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if high_risk:
|
|
70
|
+
return {
|
|
71
|
+
"priority": "P0",
|
|
72
|
+
"decision": "recommend",
|
|
73
|
+
"reason": "High-risk task; confirm before side effects.",
|
|
74
|
+
"load_limit": 1,
|
|
75
|
+
"report_policy": "report",
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
top = matches[0]
|
|
79
|
+
confidence = float(top.get("confidence", 0))
|
|
80
|
+
mode = str(top.get("mode", "recommend"))
|
|
81
|
+
|
|
82
|
+
if mode == "auto-load" and confidence >= AUTO_LOAD_CONFIDENCE:
|
|
83
|
+
return {
|
|
84
|
+
"priority": "P1",
|
|
85
|
+
"decision": "auto-load",
|
|
86
|
+
"reason": "Strong workflow match.",
|
|
87
|
+
"load_limit": 3,
|
|
88
|
+
"report_policy": "silent",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if confidence >= OPTIONAL_LOAD_CONFIDENCE:
|
|
92
|
+
return {
|
|
93
|
+
"priority": "P2",
|
|
94
|
+
"decision": "optional-load",
|
|
95
|
+
"reason": "Medium confidence; use skill only if it changes execution.",
|
|
96
|
+
"load_limit": 2,
|
|
97
|
+
"report_policy": "silent",
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
"priority": "P3",
|
|
102
|
+
"decision": "bypass",
|
|
103
|
+
"reason": "No strong installed skill match.",
|
|
104
|
+
"load_limit": 0,
|
|
105
|
+
"report_policy": "silent",
|
|
106
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Hybrid BM25 + intent routing search for maestro (manifest-only, fast)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
13
|
+
sys.path.insert(0, str(SCRIPT_DIR))
|
|
14
|
+
|
|
15
|
+
from bm25 import BM25 # noqa: E402
|
|
16
|
+
from concept_gaps import build_discover_queries, find_concept_gaps # noqa: E402
|
|
17
|
+
from domains import DOMAINS, HUB_SKILLS, classify_query, domain_label # noqa: E402
|
|
18
|
+
from intents import apply_intent_boost, is_bypass_task, is_force_discover, task_intents # noqa: E402
|
|
19
|
+
from routing import ( # noqa: E402
|
|
20
|
+
bm25_to_confidence,
|
|
21
|
+
build_routing,
|
|
22
|
+
is_high_risk,
|
|
23
|
+
select_mode,
|
|
24
|
+
)
|
|
25
|
+
from maestro_paths import LEGACY_MANIFEST_PATH, MANIFEST_PATH # noqa: E402
|
|
26
|
+
from synonyms import expand_query # noqa: E402
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def default_manifest_path() -> Path:
|
|
30
|
+
if MANIFEST_PATH.is_file():
|
|
31
|
+
return MANIFEST_PATH
|
|
32
|
+
if LEGACY_MANIFEST_PATH.is_file():
|
|
33
|
+
return LEGACY_MANIFEST_PATH
|
|
34
|
+
return MANIFEST_PATH
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
DEFAULT_MANIFEST = default_manifest_path()
|
|
38
|
+
WEAK_SCORE_THRESHOLD = 1.5
|
|
39
|
+
WEAK_SPREAD_RATIO = 0.10
|
|
40
|
+
DEFAULT_MAX_RESULTS = 5
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def load_manifest(path: Path) -> dict:
|
|
44
|
+
if not path.is_file():
|
|
45
|
+
raise FileNotFoundError(
|
|
46
|
+
f"Manifest not found: {path}. Run: python build_manifest.py"
|
|
47
|
+
)
|
|
48
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def skill_document(skill: dict[str, Any]) -> str:
|
|
52
|
+
tags = skill.get("tags") or []
|
|
53
|
+
tag_text = " ".join(str(t) for t in tags) if isinstance(tags, list) else str(tags)
|
|
54
|
+
return (
|
|
55
|
+
f"{skill['name']} {skill.get('folder', '')} "
|
|
56
|
+
f"{skill.get('description', '')} {tag_text} {skill.get('domain', '')}"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def build_discover(
|
|
61
|
+
query: str,
|
|
62
|
+
results: list[dict[str, Any]],
|
|
63
|
+
pool: list[dict[str, Any]],
|
|
64
|
+
*,
|
|
65
|
+
weak: bool,
|
|
66
|
+
force_discover: bool,
|
|
67
|
+
bypass: bool,
|
|
68
|
+
domain: str,
|
|
69
|
+
) -> dict[str, Any]:
|
|
70
|
+
if bypass:
|
|
71
|
+
return {
|
|
72
|
+
"triggered": False,
|
|
73
|
+
"reasons": [],
|
|
74
|
+
"force_discover": False,
|
|
75
|
+
"gaps": [],
|
|
76
|
+
"gap_notes": [],
|
|
77
|
+
"queries": [],
|
|
78
|
+
"local_fallback": None,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
gaps, gap_notes = find_concept_gaps(
|
|
82
|
+
query, results, pool, skill_document, expand_query
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
reasons: list[str] = []
|
|
86
|
+
if force_discover:
|
|
87
|
+
reasons.append("force_discover")
|
|
88
|
+
if weak:
|
|
89
|
+
reasons.append("weak_match")
|
|
90
|
+
if len(results) == 1 and (
|
|
91
|
+
weak or results[0]["score"] < WEAK_SCORE_THRESHOLD
|
|
92
|
+
):
|
|
93
|
+
reasons.append("single_local_skill")
|
|
94
|
+
if gaps:
|
|
95
|
+
reasons.append("concept_gap")
|
|
96
|
+
|
|
97
|
+
triggered = bool(reasons)
|
|
98
|
+
queries: list[str] = []
|
|
99
|
+
if gaps:
|
|
100
|
+
queries = build_discover_queries(gaps, query, domain)
|
|
101
|
+
elif triggered:
|
|
102
|
+
queries = [query]
|
|
103
|
+
|
|
104
|
+
local_fallback: dict[str, str] | None = None
|
|
105
|
+
if results:
|
|
106
|
+
local_fallback = {
|
|
107
|
+
"name": str(results[0]["name"]),
|
|
108
|
+
"path": str(results[0].get("path", "")),
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
"triggered": triggered,
|
|
113
|
+
"reasons": reasons,
|
|
114
|
+
"force_discover": force_discover,
|
|
115
|
+
"gaps": gaps,
|
|
116
|
+
"gap_notes": gap_notes,
|
|
117
|
+
"queries": queries,
|
|
118
|
+
"local_fallback": local_fallback,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def search_skills(
|
|
123
|
+
query: str,
|
|
124
|
+
manifest: dict,
|
|
125
|
+
domain: str | None = None,
|
|
126
|
+
max_results: int = DEFAULT_MAX_RESULTS,
|
|
127
|
+
include_hubs: bool = True,
|
|
128
|
+
) -> dict:
|
|
129
|
+
skills = manifest.get("skills", [])
|
|
130
|
+
if not skills:
|
|
131
|
+
return {"error": "empty_manifest", "query": query}
|
|
132
|
+
|
|
133
|
+
bypass = is_bypass_task(query)
|
|
134
|
+
high_risk = is_high_risk(query)
|
|
135
|
+
force_discover = is_force_discover(query)
|
|
136
|
+
intents = task_intents(query)
|
|
137
|
+
expanded_query = expand_query(query)
|
|
138
|
+
|
|
139
|
+
detected_domain, domain_scores = classify_query(query)
|
|
140
|
+
active_domain = domain or detected_domain
|
|
141
|
+
|
|
142
|
+
pool = skills
|
|
143
|
+
if active_domain != "general":
|
|
144
|
+
domain_pool = [s for s in skills if s.get("domain") == active_domain]
|
|
145
|
+
if len(domain_pool) >= 3:
|
|
146
|
+
pool = domain_pool
|
|
147
|
+
|
|
148
|
+
documents = [skill_document(s) for s in pool]
|
|
149
|
+
bm25 = BM25()
|
|
150
|
+
bm25.fit(documents)
|
|
151
|
+
ranked = bm25.score(expanded_query)
|
|
152
|
+
|
|
153
|
+
results: list[dict[str, Any]] = []
|
|
154
|
+
for idx, score in ranked:
|
|
155
|
+
if score <= 0:
|
|
156
|
+
continue
|
|
157
|
+
skill = pool[idx]
|
|
158
|
+
if not include_hubs and skill["name"] in HUB_SKILLS:
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
skill_text = skill_document(skill)
|
|
162
|
+
adjusted, intent_boosts, suggested_mode = apply_intent_boost(
|
|
163
|
+
score, skill["name"], skill_text, intents
|
|
164
|
+
)
|
|
165
|
+
confidence = bm25_to_confidence(adjusted)
|
|
166
|
+
mode = select_mode(confidence, high_risk, suggested_mode, bypass=bypass)
|
|
167
|
+
|
|
168
|
+
entry: dict[str, Any] = {
|
|
169
|
+
**skill,
|
|
170
|
+
"score": round(adjusted, 4),
|
|
171
|
+
"bm25_score": round(score, 4),
|
|
172
|
+
"confidence": round(confidence, 3),
|
|
173
|
+
"mode": mode,
|
|
174
|
+
"installed": True,
|
|
175
|
+
}
|
|
176
|
+
if intent_boosts:
|
|
177
|
+
entry["intent_boosts"] = intent_boosts
|
|
178
|
+
results.append(entry)
|
|
179
|
+
|
|
180
|
+
results.sort(key=lambda item: item["score"], reverse=True)
|
|
181
|
+
results = results[:max_results]
|
|
182
|
+
|
|
183
|
+
routing = build_routing(query, results, high_risk, bypass=bypass)
|
|
184
|
+
|
|
185
|
+
weak = False
|
|
186
|
+
weak_reasons: list[str] = []
|
|
187
|
+
if not results and not bypass:
|
|
188
|
+
weak = True
|
|
189
|
+
weak_reasons.append("no_results")
|
|
190
|
+
elif results:
|
|
191
|
+
top = results[0]["score"]
|
|
192
|
+
if top < WEAK_SCORE_THRESHOLD:
|
|
193
|
+
weak = True
|
|
194
|
+
weak_reasons.append("low_top_score")
|
|
195
|
+
if len(results) >= 3:
|
|
196
|
+
third = results[2]["score"]
|
|
197
|
+
if top > 0 and (top - third) / top < WEAK_SPREAD_RATIO:
|
|
198
|
+
weak = True
|
|
199
|
+
weak_reasons.append("tight_spread")
|
|
200
|
+
|
|
201
|
+
discover = build_discover(
|
|
202
|
+
query,
|
|
203
|
+
results,
|
|
204
|
+
pool,
|
|
205
|
+
weak=weak,
|
|
206
|
+
force_discover=force_discover,
|
|
207
|
+
bypass=bypass,
|
|
208
|
+
domain=active_domain,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
"query": query,
|
|
213
|
+
"expanded_query": expanded_query,
|
|
214
|
+
"domain": active_domain,
|
|
215
|
+
"domain_label": domain_label(active_domain),
|
|
216
|
+
"detected_domain": detected_domain,
|
|
217
|
+
"domain_scores": domain_scores,
|
|
218
|
+
"available_domains": DOMAINS,
|
|
219
|
+
"weak_match": weak,
|
|
220
|
+
"weak_reasons": weak_reasons,
|
|
221
|
+
"high_risk": high_risk,
|
|
222
|
+
"routing": routing,
|
|
223
|
+
"count": len(results),
|
|
224
|
+
"results": results,
|
|
225
|
+
"discover": discover,
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def format_text(payload: dict) -> str:
|
|
230
|
+
if "error" in payload:
|
|
231
|
+
return f"Error: {payload['error']}"
|
|
232
|
+
|
|
233
|
+
routing = payload.get("routing", {})
|
|
234
|
+
lines = [
|
|
235
|
+
f"Domain: {payload['domain_label']} ({payload['domain']})",
|
|
236
|
+
f"Query: {payload['query']}",
|
|
237
|
+
f"Routing: {routing.get('priority')} / {routing.get('decision')}",
|
|
238
|
+
f"Weak match: {payload['weak_match']}",
|
|
239
|
+
]
|
|
240
|
+
if payload.get("weak_reasons"):
|
|
241
|
+
lines.append(f"Weak reasons: {', '.join(payload['weak_reasons'])}")
|
|
242
|
+
|
|
243
|
+
discover = payload.get("discover", {})
|
|
244
|
+
if discover.get("triggered"):
|
|
245
|
+
lines.append(f"Discover: {', '.join(discover.get('reasons', []))}")
|
|
246
|
+
if discover.get("gaps"):
|
|
247
|
+
lines.append(f"Concept gaps: {', '.join(discover['gaps'])}")
|
|
248
|
+
if discover.get("queries"):
|
|
249
|
+
lines.append(f"Find queries: {', '.join(discover['queries'])}")
|
|
250
|
+
|
|
251
|
+
lines.append("")
|
|
252
|
+
for i, skill in enumerate(payload.get("results", []), 1):
|
|
253
|
+
lines.append(
|
|
254
|
+
f"{i}. {skill['name']} (score={skill['score']}, "
|
|
255
|
+
f"confidence={skill.get('confidence')}, mode={skill.get('mode')})"
|
|
256
|
+
)
|
|
257
|
+
lines.append(f" path: {skill['path']}")
|
|
258
|
+
|
|
259
|
+
return "\n".join(lines)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def main() -> int:
|
|
263
|
+
parser = argparse.ArgumentParser(description="Search skills for maestro")
|
|
264
|
+
parser.add_argument("query", help="User prompt to match against skills")
|
|
265
|
+
parser.add_argument("--domain", default=None, choices=DOMAINS)
|
|
266
|
+
parser.add_argument("--max-results", type=int, default=DEFAULT_MAX_RESULTS)
|
|
267
|
+
parser.add_argument("--manifest", default=str(DEFAULT_MANIFEST))
|
|
268
|
+
parser.add_argument("--json", action="store_true")
|
|
269
|
+
args = parser.parse_args()
|
|
270
|
+
|
|
271
|
+
manifest = load_manifest(Path(args.manifest))
|
|
272
|
+
payload = search_skills(
|
|
273
|
+
args.query,
|
|
274
|
+
manifest,
|
|
275
|
+
domain=args.domain,
|
|
276
|
+
max_results=args.max_results,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
if args.json:
|
|
280
|
+
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
|
281
|
+
else:
|
|
282
|
+
print(format_text(payload))
|
|
283
|
+
return 0
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
if __name__ == "__main__":
|
|
287
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Query expansion synonyms for maestro skill search (EN + PT partial)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
DEFAULT_SYNONYMS: dict[str, list[str]] = {
|
|
9
|
+
"test": ["teste", "testes", "unitario", "unittest", "falhando", "failing"],
|
|
10
|
+
"debug": ["depurar", "depuracao", "bug", "erro", "root cause", "raiz", "falha"],
|
|
11
|
+
"build": ["compilar", "build", "construir"],
|
|
12
|
+
"review": ["revisar", "revisao", "code review", "auditoria"],
|
|
13
|
+
"audit": ["auditoria", "auditar"],
|
|
14
|
+
"design": ["design", "ui", "ux", "interface", "layout", "visual"],
|
|
15
|
+
"dashboard": ["painel", "dashboard", "grafico"],
|
|
16
|
+
"security": ["seguranca", "cybersecurity", "infosec"],
|
|
17
|
+
"forensics": ["forense", "forensics", "volatility", "memoria"],
|
|
18
|
+
"deploy": ["deploy", "publicar", "implantar"],
|
|
19
|
+
"github": ["git", "pull request", "pr", "ci", "actions"],
|
|
20
|
+
"docs": ["documentacao", "readme", "documentar"],
|
|
21
|
+
"auth": ["autenticacao", "login", "oauth"],
|
|
22
|
+
"skill": ["skill", "habilidade"],
|
|
23
|
+
"superdesign": ["superdesign", "design system"],
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def expand_query(text: str, extra: dict[str, list[str]] | None = None) -> str:
|
|
28
|
+
"""Append canonical terms when synonym phrases appear in the query."""
|
|
29
|
+
synonyms = {**DEFAULT_SYNONYMS, **(extra or {})}
|
|
30
|
+
lowered = text.lower()
|
|
31
|
+
tokens = set(re.findall(r"[a-z0-9]{3,}", lowered))
|
|
32
|
+
extra_terms: list[str] = []
|
|
33
|
+
|
|
34
|
+
for canonical, values in synonyms.items():
|
|
35
|
+
canon = canonical.lower()
|
|
36
|
+
if canon in tokens or canon in lowered:
|
|
37
|
+
extra_terms.append(canon)
|
|
38
|
+
continue
|
|
39
|
+
for value in values:
|
|
40
|
+
value_text = value.lower().strip()
|
|
41
|
+
if value_text and value_text in lowered:
|
|
42
|
+
extra_terms.append(canon)
|
|
43
|
+
break
|
|
44
|
+
|
|
45
|
+
if not extra_terms:
|
|
46
|
+
return text
|
|
47
|
+
return f"{text} {' '.join(sorted(set(extra_terms)))}"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: Bug
|
|
2
|
+
description: Algo quebrou ou comportamento incorreto
|
|
3
|
+
title: "fix(escopo): "
|
|
4
|
+
labels: ["tipo: bug"]
|
|
5
|
+
body:
|
|
6
|
+
- type: markdown
|
|
7
|
+
attributes:
|
|
8
|
+
value: Descreva o problema com passos para reproduzir.
|
|
9
|
+
- type: textarea
|
|
10
|
+
id: descricao
|
|
11
|
+
attributes:
|
|
12
|
+
label: Descricao
|
|
13
|
+
description: O que acontece vs o esperado?
|
|
14
|
+
validations:
|
|
15
|
+
required: true
|
|
16
|
+
- type: textarea
|
|
17
|
+
id: reproduzir
|
|
18
|
+
attributes:
|
|
19
|
+
label: Passos para reproduzir
|
|
20
|
+
placeholder: |
|
|
21
|
+
1. Passo um
|
|
22
|
+
2. Passo dois
|
|
23
|
+
validations:
|
|
24
|
+
required: true
|
|
25
|
+
- type: dropdown
|
|
26
|
+
id: prioridade
|
|
27
|
+
attributes:
|
|
28
|
+
label: Prioridade
|
|
29
|
+
options:
|
|
30
|
+
- alta
|
|
31
|
+
- media
|
|
32
|
+
- baixa
|
|
33
|
+
validations:
|
|
34
|
+
required: true
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
name: Chore
|
|
2
|
+
description: Docs, CI, refactor, dependencias
|
|
3
|
+
title: "chore(escopo): "
|
|
4
|
+
labels: ["tipo: chore"]
|
|
5
|
+
body:
|
|
6
|
+
- type: textarea
|
|
7
|
+
id: tarefa
|
|
8
|
+
attributes:
|
|
9
|
+
label: O que fazer
|
|
10
|
+
validations:
|
|
11
|
+
required: true
|
|
12
|
+
- type: textarea
|
|
13
|
+
id: motivo
|
|
14
|
+
attributes:
|
|
15
|
+
label: Por que
|
|
16
|
+
validations:
|
|
17
|
+
required: true
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
blank_issues_enabled: false
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: Feature
|
|
2
|
+
description: Nova funcionalidade ou melhoria
|
|
3
|
+
title: "feat(escopo): "
|
|
4
|
+
labels: ["tipo: feature"]
|
|
5
|
+
body:
|
|
6
|
+
- type: markdown
|
|
7
|
+
attributes:
|
|
8
|
+
value: Defina objetivo, escopo e o que fica fora.
|
|
9
|
+
- type: textarea
|
|
10
|
+
id: objetivo
|
|
11
|
+
attributes:
|
|
12
|
+
label: Objetivo
|
|
13
|
+
validations:
|
|
14
|
+
required: true
|
|
15
|
+
- type: textarea
|
|
16
|
+
id: escopo
|
|
17
|
+
attributes:
|
|
18
|
+
label: Escopo (checklist)
|
|
19
|
+
placeholder: |
|
|
20
|
+
- [ ] Item 1
|
|
21
|
+
- [ ] Item 2
|
|
22
|
+
validations:
|
|
23
|
+
required: true
|
|
24
|
+
- type: textarea
|
|
25
|
+
id: fora
|
|
26
|
+
attributes:
|
|
27
|
+
label: Fora de escopo
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
name: CI falha → Issue
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_run:
|
|
5
|
+
workflows: [CI]
|
|
6
|
+
types: [completed]
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
issues: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
abrir-issue:
|
|
13
|
+
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/github-script@v7
|
|
17
|
+
with:
|
|
18
|
+
script: |
|
|
19
|
+
const run = context.payload.workflow_run;
|
|
20
|
+
const title = `ci: falha no workflow ${run.name} (${run.head_branch})`;
|
|
21
|
+
const body = [
|
|
22
|
+
'Pipeline CI falhou. Corrija e reabra o PR ate ficar verde.',
|
|
23
|
+
'',
|
|
24
|
+
`- **Workflow:** ${run.name}`,
|
|
25
|
+
`- **Branch:** ${run.head_branch}`,
|
|
26
|
+
`- **Commit:** ${run.head_sha}`,
|
|
27
|
+
`- **Run:** ${run.html_url}`,
|
|
28
|
+
'',
|
|
29
|
+
'Label sugerida: `ci:falha`'
|
|
30
|
+
].join('\n');
|
|
31
|
+
|
|
32
|
+
const { data: existing } = await github.rest.search.issuesAndPullRequests({
|
|
33
|
+
q: `repo:${context.repo.owner}/${context.repo.repo} is:issue is:open label:ci:falha "${run.head_branch}" in:title`
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (existing.total_count > 0) {
|
|
37
|
+
core.info('Issue aberta ja existe para esta branch.');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
await github.rest.issues.create({
|
|
42
|
+
owner: context.repo.owner,
|
|
43
|
+
repo: context.repo.repo,
|
|
44
|
+
title,
|
|
45
|
+
body,
|
|
46
|
+
labels: ['ci:falha', 'tipo: chore']
|
|
47
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, master]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main, master]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- name: Rodar testes
|
|
15
|
+
run: |
|
|
16
|
+
echo "Substitua por: npm test, pytest, go test ./..., etc."
|
|
17
|
+
exit 0
|
|
18
|
+
|
|
19
|
+
build:
|
|
20
|
+
needs: test
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
- name: Build
|
|
25
|
+
run: |
|
|
26
|
+
echo "Substitua por: npm run build, docker build, etc."
|
|
27
|
+
exit 0
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Contribuicao — fluxo Issues + PR
|
|
2
|
+
|
|
3
|
+
## Regra geral
|
|
4
|
+
|
|
5
|
+
1. Abra ou referencie uma **Issue** descrevendo objetivo e escopo.
|
|
6
|
+
2. Crie branch `feat/<numero>-<slug>` ou `fix/<numero>-<slug>`.
|
|
7
|
+
3. Commits descritivos: `tipo(escopo): descricao` + `Refs #N` ou `Closes #N`.
|
|
8
|
+
4. Abra **PR draft**; quando CI e testes locais passarem, marque ready for review.
|
|
9
|
+
5. Merge na branch principal fecha a Issue (`Closes #N` no PR).
|
|
10
|
+
|
|
11
|
+
## CI — ordem obrigatoria
|
|
12
|
+
|
|
13
|
+
O pipeline roda **testes primeiro**, depois **build**. Se falhar:
|
|
14
|
+
|
|
15
|
+
- O PR nao deve ser mergeado.
|
|
16
|
+
- Abra ou atualize Issue com label `ci:falha` ate ficar verde.
|
|
17
|
+
|
|
18
|
+
## Project (opcional)
|
|
19
|
+
|
|
20
|
+
Se o time usar GitHub Projects, vincule Issues e PRs ao board do projeto.
|
|
21
|
+
|
|
22
|
+
Colunas sugeridas: Backlog → Em progresso → Em review → Concluido.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
[
|
|
2
|
+
{ "name": "tipo: bug", "color": "E84C3D", "description": "Correcao de erro" },
|
|
3
|
+
{ "name": "tipo: feature", "color": "3D8BF7", "description": "Nova funcionalidade" },
|
|
4
|
+
{ "name": "tipo: docs", "color": "737373", "description": "Documentacao" },
|
|
5
|
+
{ "name": "tipo: chore", "color": "163475", "description": "Manutencao, deps, CI" },
|
|
6
|
+
{ "name": "prioridade: alta", "color": "F4B30F", "description": "Bloqueia operacao" },
|
|
7
|
+
{ "name": "prioridade: media", "color": "66C94B", "description": "Importante, nao urgente" },
|
|
8
|
+
{ "name": "prioridade: baixa", "color": "EDF2F8", "description": "Pode esperar" },
|
|
9
|
+
{ "name": "ci:falha", "color": "E84C3D", "description": "Pipeline CI falhou — corrigir antes do merge" },
|
|
10
|
+
{ "name": "status: em-progresso", "color": "3D8BF7", "description": "Em desenvolvimento" },
|
|
11
|
+
{ "name": "status: em-review", "color": "F4B30F", "description": "PR aberto aguardando merge" }
|
|
12
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
## Resumo
|
|
2
|
+
|
|
3
|
+
<!-- O que mudou e por que (1-3 frases) -->
|
|
4
|
+
|
|
5
|
+
## Issue
|
|
6
|
+
|
|
7
|
+
Closes #
|
|
8
|
+
|
|
9
|
+
## Checklist
|
|
10
|
+
|
|
11
|
+
- [ ] Branch `feat/<numero>-<slug>` ou `fix/<numero>-<slug>` (nunca commit direto na branch principal)
|
|
12
|
+
- [ ] Testes locais passaram **antes** do build
|
|
13
|
+
- [ ] CI verde (job `test` antes do `build`)
|
|
14
|
+
- [ ] Docs atualizadas se mudou API, comando ou regra de negocio
|
|
15
|
+
|
|
16
|
+
## Como testar
|
|
17
|
+
|
|
18
|
+
<!-- Comandos ou passos manuais -->
|