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,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 -->