ganbatte-os 0.2.38 → 0.2.42

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 (82) hide show
  1. package/.gos/README.md +2 -6
  2. package/.gos/agents/profiles/ganbatte-os-master.md +91 -58
  3. package/.gos/agents/profiles/index.json +3 -1
  4. package/.gos/agents/profiles/perf-optimizer.md +10 -0
  5. package/.gos/agents/profiles/po.md +1 -1
  6. package/.gos/agents/profiles/security-auditor.md +11 -0
  7. package/.gos/agents/profiles/sm.md +4 -4
  8. package/.gos/agents/profiles/squad-creator.md +2 -2
  9. package/.gos/config.json +23 -2
  10. package/.gos/docs/curation.md +8 -6
  11. package/.gos/docs/gos_installation_guide.md +1 -1
  12. package/.gos/docs/plan-distribuicao-publica.md +2 -3
  13. package/.gos/docs/toolchain-map.md +2 -2
  14. package/.gos/libraries/component-reuse-gate.md +75 -0
  15. package/.gos/libraries/doc-sync-policy.md +31 -0
  16. package/.gos/libraries/lazy-dev-policy.md +38 -0
  17. package/.gos/libraries/lucide-icons-policy.md +1 -1
  18. package/.gos/libraries/performance-audit-playbook.md +54 -0
  19. package/.gos/libraries/security-audit-playbook.md +63 -0
  20. package/.gos/manifests/g-os-runtime-manifest.json +29 -8
  21. package/.gos/playbooks/audit-streaming-playbook.md +86 -0
  22. package/.gos/playbooks/plan-creation-playbook.md +3 -5
  23. package/.gos/prompts/01-search.md +2 -2
  24. package/.gos/prompts/03-tasks.md +4 -4
  25. package/.gos/prompts/05-reviews.md +1 -1
  26. package/.gos/scripts/cli/gos-cli.js +16 -0
  27. package/.gos/scripts/hooks/claude-stop-summary.js +0 -16
  28. package/.gos/scripts/integrations/check-plan.js +15 -5
  29. package/.gos/scripts/integrations/setup-ide-adapters.js +72 -75
  30. package/.gos/scripts/tools/model-router.js +122 -0
  31. package/.gos/skills/audit-screenshots/SKILL.md +195 -155
  32. package/.gos/skills/execute-plan/SKILL.md +37 -23
  33. package/.gos/skills/figma-print-diff/SKILL.md +5 -0
  34. package/.gos/skills/perf-review/SKILL.md +58 -0
  35. package/.gos/skills/plan-blueprint/SKILL.md +34 -26
  36. package/.gos/skills/plan-to-tasks/SKILL.md +13 -2
  37. package/.gos/skills/progress-tracker/SKILL.md +3 -3
  38. package/.gos/skills/registry.json +3 -4
  39. package/.gos/skills/security-review/SKILL.md +59 -0
  40. package/.gos/skills/simplify-review/SKILL.md +59 -0
  41. package/.gos/skills/validate-plan/SKILL.md +29 -30
  42. package/.gos/squads/code-quality/README.md +20 -0
  43. package/.gos/squads/code-quality/squad.yaml +32 -0
  44. package/.gos/squads/code-quality/workflows/wf-code-quality.yaml +19 -0
  45. package/.gos/squads/design-delivery/README.md +1 -1
  46. package/.gos/squads/design-delivery/squad.yaml +4 -3
  47. package/.gos/squads/design-delivery/workflows/wf-design-delivery.yaml +6 -6
  48. package/.gos/templates/planTemplate.md +35 -5
  49. package/.gos/templates/specTemplate.md +68 -0
  50. package/.gos/templates/taskTemplate.md +37 -10
  51. package/AGENTS.md +0 -2
  52. package/CLAUDE.md +25 -9
  53. package/GEMINI.md +3 -3
  54. package/LICENSE +1 -1
  55. package/README.md +6 -8
  56. package/package.json +2 -3
  57. package/.gos/docs/slack-notifications.md +0 -219
  58. package/.gos/playbooks/sprint-planner-playbook.md +0 -127
  59. package/.gos/scripts/hooks/post-commit-notify.js +0 -175
  60. package/.gos/scripts/tools/clickup-preprocess.js +0 -218
  61. package/.gos/scripts/tools/clickup.js +0 -1058
  62. package/.gos/scripts/tools/slack-notify.js +0 -271
  63. package/.gos/skills/clickup/SKILL.md +0 -151
  64. package/.gos/skills/slack-review/SKILL.md +0 -91
  65. package/.gos/skills/sprint-planner/SKILL.md +0 -434
  66. package/.gos/skills/weekly-update/CHANGELOG.md +0 -165
  67. package/.gos/skills/weekly-update/SKILL.md +0 -361
  68. package/.gos/squads/sprint-planning/agents/sprint-chief.md +0 -47
  69. package/.gos/squads/sprint-planning/agents/sprint-planner-agent.md +0 -43
  70. package/.gos/squads/sprint-planning/agents/sprint-tracker.md +0 -43
  71. package/.gos/squads/sprint-planning/agents/task-importer.md +0 -44
  72. package/.gos/squads/sprint-planning/checklists/sprint-readiness.md +0 -27
  73. package/.gos/squads/sprint-planning/config/config.yaml +0 -65
  74. package/.gos/squads/sprint-planning/data/clickup-field-mapping.yaml +0 -94
  75. package/.gos/squads/sprint-planning/squad.yaml +0 -52
  76. package/.gos/squads/sprint-planning/tasks/close-sprint.md +0 -43
  77. package/.gos/squads/sprint-planning/tasks/create-sprint.md +0 -42
  78. package/.gos/squads/sprint-planning/tasks/import-tasks.md +0 -39
  79. package/.gos/squads/sprint-planning/tasks/sync-status.md +0 -31
  80. package/.gos/squads/sprint-planning/workflows/wf-sprint-creation.yaml +0 -59
  81. package/.gos/squads/sprint-planning/workflows/wf-sprint-sync.yaml +0 -35
  82. package/.gos/templates/sprint-clickup.template.md +0 -80
@@ -26,37 +26,36 @@ function agentSlug(id) {
26
26
  return id.startsWith('gos-') ? id : `gos-${id}`;
27
27
  }
28
28
 
29
- function cleanupStaleAdapters() {
30
- // Remove arquivos .md soltos no root de skills/ (formato legado) em todas as IDEs.
31
- // Codex/Qwen/Gemini/Opencode/Antigravity esperam <slug>/SKILL.md (diretorio), nao .md solto.
32
- // .agent e o diretorio canonico do Antigravity (workspace scope: .agent/skills/, .agent/workflows/).
33
- const allIdes = ['.codex', '.qwen', '.gemini', '.opencode', '.agent'];
34
- for (const ide of allIdes) {
35
- const skillsDir = path.join(root, ide, 'skills');
36
- if (fs.existsSync(skillsDir)) {
37
- for (const entry of fs.readdirSync(skillsDir)) {
38
- const full = path.join(skillsDir, entry);
39
- if (fs.statSync(full).isFile() && entry.endsWith('.md')) {
40
- fs.unlinkSync(full);
41
- }
42
- }
43
- }
44
- const agentsDir = path.join(root, ide, 'agents');
45
- if (fs.existsSync(agentsDir)) {
46
- for (const entry of fs.readdirSync(agentsDir)) {
47
- if (/^gos-gos-/.test(entry)) {
48
- fs.unlinkSync(path.join(agentsDir, entry));
49
- }
50
- }
29
+ // IDEs alvo do G-OS (adapters gerados para estas).
30
+ // Claude Code, Codex, Qwen, Opencode, Gemini CLI, Antigravity (.agent).
31
+ const TARGET_IDES = ['.claude', '.codex', '.qwen', '.opencode', '.gemini', '.agent'];
32
+ // Entrypoints user-facing (slash/workflow). O master roteia o resto internamente (item 10).
33
+ const CMD_WHITELIST = new Set(['gos-master', 'ux-design-expert']);
34
+
35
+ function removeGosPrefixed(dir) {
36
+ // Remove apenas entradas G-OS (prefixo gos-), preservando skills/agents/workflows
37
+ // proprios do usuario que coexistem no mesmo diretorio (contrato do prefixo gos-).
38
+ if (!fs.existsSync(dir)) return;
39
+ for (const entry of fs.readdirSync(dir)) {
40
+ if (/^gos-/.test(entry)) {
41
+ fs.rmSync(path.join(dir, entry), { recursive: true, force: true });
51
42
  }
52
43
  }
44
+ }
53
45
 
54
- // Legado: .antigravity/ era o diretorio errado (Antigravity nunca leu isso — usa .agent/).
55
- // Remove silenciosamente para evitar drift entre IDEs.
56
- const legacyAntigravity = path.join(root, '.antigravity');
57
- if (fs.existsSync(legacyAntigravity)) {
58
- fs.rmSync(legacyAntigravity, { recursive: true, force: true });
46
+ function cleanupStaleAdapters() {
47
+ for (const ide of TARGET_IDES) {
48
+ removeGosPrefixed(path.join(root, ide, 'skills')); // gos-<slug>/
49
+ removeGosPrefixed(path.join(root, ide, 'agents')); // gos-<id>.md
50
+ // Commands sao namespaced sob gos/ — seguro remover o namespace inteiro.
51
+ const cmdGos = path.join(root, ide, 'commands', 'gos');
52
+ if (fs.existsSync(cmdGos)) fs.rmSync(cmdGos, { recursive: true, force: true });
59
53
  }
54
+ // Antigravity: workflows dos entrypoints (gos-*) — limpar apenas os do G-OS.
55
+ removeGosPrefixed(path.join(root, '.agent', 'workflows'));
56
+ // .antigravity/ e o diretorio errado (Antigravity usa .agent/) — remover legado.
57
+ const legacyAntigravity = path.join(root, '.antigravity');
58
+ if (fs.existsSync(legacyAntigravity)) fs.rmSync(legacyAntigravity, { recursive: true, force: true });
60
59
  }
61
60
 
62
61
  function agentWrapper(agentId, target) {
@@ -131,71 +130,57 @@ function main() {
131
130
  const agentProfilePath = path.join(root, '.gos', 'agents', 'profiles', agent.path);
132
131
  const agentDesc = extractAgentDescription(agentProfilePath) || `${agent.id} agent`;
133
132
  const aSlug = agentSlug(agent.id);
133
+ const safeDesc = agentDesc.replace(/"/g, '\\"').replace(/\n/g, ' ').slice(0, 200);
134
+ const isEntrypoint = CMD_WHITELIST.has(agent.id);
135
+
136
+ // Subagents (delegacao interna do master) — para TODOS os agents.
137
+ const qwenAgent = path.join(root, '.qwen', 'agents', `${aSlug}.md`);
138
+ const qwenAgentTarget = relativeTarget(qwenAgent, agentProfilePath);
139
+ writeFile(qwenAgent, `---\nname: "${aSlug}"\ndescription: "${safeDesc}"\nmodel: inherit\ntools:\n - Read\n - Glob\n - Grep\n - Bash\n - Edit\n - Write\n---\n\nFonte canonica: \`${qwenAgentTarget}\`\nLeia e siga o perfil em \`${qwenAgentTarget}\`.`);
140
+
141
+ const codexAgent = path.join(root, '.codex', 'agents', `${aSlug}.md`);
142
+ const codexAgentTarget = relativeTarget(codexAgent, agentProfilePath);
143
+ writeFile(codexAgent, `---\nname: "${aSlug}"\ndescription: "${safeDesc}"\nmodel: inherit\ntools:\n - Read\n - Glob\n - Grep\n - Bash\n - Edit\n - Write\n---\n\nFonte canonica: \`${codexAgentTarget}\`\nLeia e siga o perfil em \`${codexAgentTarget}\`.`);
144
+
145
+ // Slash commands / picker (user-facing) — APENAS entrypoints (item 10).
146
+ if (!isEntrypoint) continue;
134
147
 
135
- // Claude commands (requires YAML frontmatter with description)
136
148
  const claudeFile = path.join(root, '.claude', 'commands', 'gos', 'agents', `${agent.id}.md`);
137
149
  writeFile(claudeFile, claudeCommandWrapper(aSlug, agentDesc, relativeTarget(claudeFile, agentProfilePath), '', 'Claude'));
138
150
 
139
- // Qwen commands (requires YAML frontmatter with description)
140
151
  const qwenCmd = path.join(root, '.qwen', 'commands', 'gos', 'agents', `${agent.id}.md`);
141
152
  writeFile(qwenCmd, qwenCommandWrapper(aSlug, agentDesc, relativeTarget(qwenCmd, agentProfilePath)));
142
153
 
143
- // Qwen sub-agents
144
- const qwenAgent = path.join(root, '.qwen', 'agents', `${aSlug}.md`);
145
- const agentTarget = relativeTarget(qwenAgent, agentProfilePath);
146
- const safeDesc = agentDesc.replace(/"/g, '\\"').replace(/\n/g, ' ').slice(0, 200);
147
- writeFile(qwenAgent, `---\nname: "${aSlug}"\ndescription: "${safeDesc}"\nmodel: inherit\ntools:\n - Read\n - Glob\n - Grep\n - Bash\n - Edit\n - Write\n---\n\nFonte canonica: \`${agentTarget}\`\nLeia e siga o perfil em \`${agentTarget}\`.`);
148
-
149
- // Codex commands (slash commands no Codex IDE Extension)
150
154
  const codexCmd = path.join(root, '.codex', 'commands', 'gos', 'agents', `${agent.id}.md`);
151
155
  writeFile(codexCmd, claudeCommandWrapper(aSlug, agentDesc, relativeTarget(codexCmd, agentProfilePath), '', 'Codex'));
152
156
 
153
- // Codex sub-agents (Codex IDE espera .codex/agents/<id>.md)
154
- const codexAgent = path.join(root, '.codex', 'agents', `${aSlug}.md`);
155
- const codexAgentTarget = relativeTarget(codexAgent, agentProfilePath);
156
- writeFile(codexAgent, `---\nname: "${aSlug}"\ndescription: "${safeDesc}"\nmodel: inherit\ntools:\n - Read\n - Glob\n - Grep\n - Bash\n - Edit\n - Write\n---\n\nFonte canonica: \`${codexAgentTarget}\`\nLeia e siga o perfil em \`${codexAgentTarget}\`.`);
157
-
158
- // Codex / Antigravity (.agent/) / Gemini / Opencode usam namespace plano gos-<slug> em skills/.
159
- // Agents aparecem no mesmo picker se emitidos como wrapper SKILL.md em skills/.
160
- for (const ide of ['.codex', '.agent', '.gemini', '.opencode']) {
157
+ // Codex/Opencode/Gemini/Antigravity populam o picker a partir de <ide>/skills/<slug>/SKILL.md.
158
+ // Emitir o entrypoint tambem como skill garante que aparece no picker de cada uma.
159
+ for (const ide of ['.codex', '.opencode', '.gemini', '.agent']) {
161
160
  const ideAgentSkill = path.join(root, ide, 'skills', aSlug, 'SKILL.md');
162
161
  writeFile(ideAgentSkill, skillWrapper(agent.id, relativeTarget(ideAgentSkill, agentProfilePath), agentDesc));
163
162
  }
164
163
 
165
- // Antigravity workflows (.agent/workflows/<id>.md) — slash command invocation no picker.
166
- const antigravityWorkflow = path.join(root, '.agent', 'workflows', `${agent.id}.md`);
164
+ // Antigravity: workflow (slash command no picker) — nome prefixado gos- p/ coexistencia.
165
+ const antigravityWorkflow = path.join(root, '.agent', 'workflows', `${aSlug}.md`);
167
166
  const workflowTarget = relativeTarget(antigravityWorkflow, agentProfilePath);
168
- const safeAgentDesc = agentDesc.replace(/"/g, '\\"').replace(/\n/g, ' ').slice(0, 200);
169
167
  writeFile(
170
168
  antigravityWorkflow,
171
- `---\ndescription: "${safeAgentDesc}"\n---\n\n# /${agent.id} (Antigravity Workflow)\n\nFonte canonica: \`${workflowTarget}\`\n\nLeia o arquivo canonico apontado em CANONICAL-SOURCE e execute as instrucoes como fonte primaria.\n\nCANONICAL-SOURCE: ${workflowTarget}\n\nArgumentos do usuario seguem o prompt do agente.`
169
+ `---\ndescription: "${safeDesc}"\n---\n\n# /${aSlug} (Antigravity Workflow)\n\nFonte canonica: \`${workflowTarget}\`\n\nLeia o arquivo canonico apontado em CANONICAL-SOURCE e execute as instrucoes como fonte primaria.\n\nCANONICAL-SOURCE: ${workflowTarget}\n\nArgumentos do usuario seguem o prompt do agente.`
172
170
  );
173
171
  }
174
172
 
173
+ // Skills: auto-discoveraveis (o master as invoca internamente), NAO slash commands.
175
174
  for (const skill of skills) {
176
175
  const skillTargetPath = skill.skillFile || skill.path;
177
176
  const canonicalPath = path.join(root, '.gos', skillTargetPath);
178
- const claudeSkill = path.join(root, '.claude', 'commands', 'gos', 'skills', `${skill.slug}.md`);
179
- const codexSkill = path.join(root, '.codex', 'skills', `gos-${skill.slug}`, 'SKILL.md');
180
- const antigravitySkill = path.join(root, '.agent', 'skills', `gos-${skill.slug}`, 'SKILL.md');
181
- const geminiSkill = path.join(root, '.gemini', 'skills', `gos-${skill.slug}`, 'SKILL.md');
182
- const opencodeSkill = path.join(root, '.opencode', 'skills', `gos-${skill.slug}`, 'SKILL.md');
183
- const qwenSkill = path.join(root, '.qwen', 'skills', `gos-${skill.slug}`, 'SKILL.md');
184
-
185
- const qwenCmd = path.join(root, '.qwen', 'commands', 'gos', 'skills', `${skill.slug}.md`);
186
-
187
177
  const skillFm = parseFrontmatter(canonicalPath);
188
178
  const skillDesc = skillFm.description || skill.description || skill.name || skill.slug;
189
- const skillArgHint = skillFm['argument-hint'] || '';
190
-
191
- writeFile(codexSkill, skillWrapper(skill.slug, relativeTarget(codexSkill, canonicalPath), skillDesc));
192
- writeFile(antigravitySkill, skillWrapper(skill.slug, relativeTarget(antigravitySkill, canonicalPath), skillDesc));
193
- writeFile(geminiSkill, skillWrapper(skill.slug, relativeTarget(geminiSkill, canonicalPath), skillDesc));
194
- writeFile(opencodeSkill, skillWrapper(skill.slug, relativeTarget(opencodeSkill, canonicalPath), skillDesc));
195
- writeFile(qwenSkill, skillWrapper(skill.slug, relativeTarget(qwenSkill, canonicalPath), skillDesc));
196
179
 
197
- writeFile(qwenCmd, qwenCommandWrapper(`gos-${skill.slug}`, skillDesc, relativeTarget(qwenCmd, canonicalPath)));
198
- writeFile(claudeSkill, claudeCommandWrapper(`gos-${skill.slug}`, skillDesc, relativeTarget(claudeSkill, canonicalPath), skillArgHint, 'Claude'));
180
+ for (const ide of TARGET_IDES) {
181
+ const t = path.join(root, ide, 'skills', `gos-${skill.slug}`, 'SKILL.md');
182
+ writeFile(t, skillWrapper(skill.slug, relativeTarget(t, canonicalPath), skillDesc));
183
+ }
199
184
  }
200
185
 
201
186
  // Antigravity le AGENTS.md no root do workspace (ja existe no projeto).
@@ -228,11 +213,17 @@ function main() {
228
213
  'Ciclo: pre-flight visual -> loop por task com state machine -> visual gate -> validacao -> humano marca concluido.',
229
214
  'Detalhes: `../.gos/skills/execute-plan/SKILL.md`.',
230
215
  '',
231
- '## Agents disponiveis',
216
+ '## Entrypoints (slash commands user-facing)',
217
+ '',
218
+ 'Apenas 2 comandos: o master roteia o resto internamente (skills, subagents, squads).',
219
+ '',
220
+ ...agents.filter((a) => CMD_WHITELIST.has(a.id)).map((agent) => `- \`/gos:agents:${agent.id}\` -> \`../.gos/agents/profiles/${agent.path}\``),
221
+ '',
222
+ '## Subagents (delegacao interna do master)',
232
223
  '',
233
224
  ...agents.map((agent) => `- \`gos-${agent.id}\` -> \`../.gos/agents/profiles/${agent.path}\``),
234
225
  '',
235
- '## Skills curadas',
226
+ '## Skills curadas (auto-discoveraveis, invocadas pelo master)',
236
227
  '',
237
228
  '| Slug | Arquivo canonico |',
238
229
  '|------|------------------|',
@@ -240,9 +231,10 @@ function main() {
240
231
  '',
241
232
  '## Como o Codex consome',
242
233
  '',
243
- '- Slash commands em `.codex/commands/gos/{agents,skills}/<id>.md` -> unica superficie de skills/agents. Codex carrega o canonico apontado em CANONICAL-SOURCE e executa.',
244
- '- Subagents em `.codex/agents/gos-<id>.md` -> declaracao de subagent (acessivel via Task tool e delegacao interna).',
245
- '- Para invocar o orquestrador master, digite `/gos:agents:gos-master` no picker.',
234
+ '- Slash commands (`.codex/commands/gos/agents/`) apenas para os 2 entrypoints. O `gos-master` analisa o input e decide quais skills, agents, subagents e squads acionar.',
235
+ '- Subagents em `.codex/agents/gos-<id>.md` -> delegacao interna (Task tool).',
236
+ '- Skills em `.codex/skills/gos-<slug>/SKILL.md` -> auto-discoveraveis; o master as invoca, o usuario nao digita.',
237
+ '- Para comecar, digite `/gos:agents:gos-master` no picker.',
246
238
  ''
247
239
  ].join('\n');
248
240
  writeFile(path.join(root, '.codex', 'AGENTS.md'), codexAgentsMd);
@@ -267,10 +259,14 @@ function main() {
267
259
  // Evita regressoes silenciosas que ja quebraram a IDE no passado.
268
260
  const codexFailures = [];
269
261
  for (const agent of agents) {
262
+ // Subagent (delegacao) para TODOS os agents.
270
263
  const expectedAgent = path.join(root, '.codex', 'agents', `${agentSlug(agent.id)}.md`);
271
- const expectedCmd = path.join(root, '.codex', 'commands', 'gos', 'agents', `${agent.id}.md`);
272
264
  if (!fs.existsSync(expectedAgent)) codexFailures.push(expectedAgent);
273
- if (!fs.existsSync(expectedCmd)) codexFailures.push(expectedCmd);
265
+ // Slash command apenas para entrypoints.
266
+ if (CMD_WHITELIST.has(agent.id)) {
267
+ const expectedCmd = path.join(root, '.codex', 'commands', 'gos', 'agents', `${agent.id}.md`);
268
+ if (!fs.existsSync(expectedCmd)) codexFailures.push(expectedCmd);
269
+ }
274
270
  }
275
271
  for (const skill of skills) {
276
272
  const expectedSkill = path.join(root, '.codex', 'skills', `gos-${skill.slug}`, 'SKILL.md');
@@ -284,8 +280,9 @@ function main() {
284
280
  process.exit(1);
285
281
  }
286
282
 
287
- console.log(`Adapters generated for ${agents.length} agents and ${skills.length} skills.`);
288
- console.log(`Codex IDE: ${agents.length} agents + ${skills.length} skills + AGENTS.md + config.toml.`);
283
+ const entrypoints = agents.filter((a) => CMD_WHITELIST.has(a.id)).length;
284
+ console.log(`Adapters gerados: ${entrypoints} entrypoints (slash), ${agents.length} subagents, ${skills.length} skills.`);
285
+ console.log(`IDEs alvo: ${TARGET_IDES.join(', ')}.`);
289
286
  }
290
287
 
291
288
  main();
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * model-router — resolve modelo/provider por etapa do pipeline.
4
+ *
5
+ * Regra do framework: Junior executa, Senior audita.
6
+ * - plan -> Senior cria plano + tasks + spec
7
+ * - execute -> Junior (modelo mais barato adequado) implementa
8
+ * - validate -> Senior audita e corrige gaps
9
+ *
10
+ * Precedência: .gos-local/models.json (override local, por dev/projeto)
11
+ * -> .gos/config.json "stageModels" (default versionado).
12
+ *
13
+ * Uso (CLI):
14
+ * node model-router.js # imprime o mapa resolvido das 3 etapas
15
+ * node model-router.js get <stage> # imprime JSON da etapa (plan|execute|validate)
16
+ * node model-router.js model <stage> # imprime só o model da etapa
17
+ * node model-router.js init # cria .gos-local/models.json com os defaults
18
+ *
19
+ * Uso (módulo):
20
+ * const { resolveStage, resolveAll } = require('./model-router');
21
+ * const { provider, model, role } = resolveStage('plan');
22
+ */
23
+
24
+ 'use strict';
25
+
26
+ const fs = require('fs');
27
+ const path = require('path');
28
+
29
+ const STAGES = ['plan', 'execute', 'validate'];
30
+ const LOCAL_FILE = '.gos-local/models.json';
31
+
32
+ // Fallback embutido — usado se config.json não tiver stageModels (ex.: instalação antiga).
33
+ const HARD_DEFAULTS = {
34
+ plan: { provider: 'anthropic', model: 'claude-opus-4-8', role: 'senior' },
35
+ execute: {
36
+ provider: 'any',
37
+ model: 'claude-sonnet-5',
38
+ role: 'junior',
39
+ allow: ['claude-sonnet-5', 'codex', 'claude-haiku-4-5', 'cheapest-capable'],
40
+ },
41
+ validate: { provider: 'anthropic', model: 'claude-opus-4-8', role: 'senior' },
42
+ };
43
+
44
+ function findRoot(start) {
45
+ let dir = start || process.cwd();
46
+ for (let i = 0; i < 8; i++) {
47
+ if (fs.existsSync(path.join(dir, '.gos', 'config.json'))) return dir;
48
+ const parent = path.dirname(dir);
49
+ if (parent === dir) break;
50
+ dir = parent;
51
+ }
52
+ return process.cwd();
53
+ }
54
+
55
+ function readJson(file) {
56
+ try {
57
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ function defaultsFromConfig(root) {
64
+ const cfg = readJson(path.join(root, '.gos', 'config.json'));
65
+ const sm = cfg && cfg.stageModels;
66
+ if (!sm) return { ...HARD_DEFAULTS };
67
+ const out = {};
68
+ for (const stage of STAGES) out[stage] = sm[stage] || HARD_DEFAULTS[stage];
69
+ return out;
70
+ }
71
+
72
+ function resolveAll(root) {
73
+ root = root || findRoot();
74
+ const base = defaultsFromConfig(root);
75
+ const local = readJson(path.join(root, LOCAL_FILE));
76
+ const overrides = local && local.stageModels ? local.stageModels : {};
77
+ const out = {};
78
+ for (const stage of STAGES) {
79
+ out[stage] = { ...base[stage], ...(overrides[stage] || {}) };
80
+ }
81
+ return out;
82
+ }
83
+
84
+ function resolveStage(stage, root) {
85
+ if (!STAGES.includes(stage)) {
86
+ throw new Error(`Etapa desconhecida: ${stage}. Use: ${STAGES.join(', ')}`);
87
+ }
88
+ return resolveAll(root)[stage];
89
+ }
90
+
91
+ function writeLocalDefaults(root) {
92
+ root = root || findRoot();
93
+ const target = path.join(root, LOCAL_FILE);
94
+ if (fs.existsSync(target)) return { created: false, path: target };
95
+ fs.mkdirSync(path.dirname(target), { recursive: true });
96
+ const payload = {
97
+ schema: 'gos.models.v1',
98
+ stageModels: defaultsFromConfig(root),
99
+ };
100
+ fs.writeFileSync(target, JSON.stringify(payload, null, 2) + '\n');
101
+ return { created: true, path: target };
102
+ }
103
+
104
+ module.exports = { STAGES, resolveStage, resolveAll, writeLocalDefaults };
105
+
106
+ // --- CLI ---------------------------------------------------------------------
107
+ if (require.main === module) {
108
+ const [cmd, arg] = process.argv.slice(2);
109
+ const root = findRoot();
110
+ if (cmd === 'get') {
111
+ process.stdout.write(JSON.stringify(resolveStage(arg, root), null, 2) + '\n');
112
+ } else if (cmd === 'model') {
113
+ process.stdout.write((resolveStage(arg, root).model || '') + '\n');
114
+ } else if (cmd === 'init') {
115
+ const r = writeLocalDefaults(root);
116
+ process.stdout.write(
117
+ (r.created ? 'Criado: ' : 'Ja existe: ') + r.path + '\n'
118
+ );
119
+ } else {
120
+ process.stdout.write(JSON.stringify(resolveAll(root), null, 2) + '\n');
121
+ }
122
+ }