up-cc 0.16.1 → 2.0.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 (135) hide show
  1. package/README.md +87 -577
  2. package/package.json +5 -3
  3. package/up/CHANGELOG.md +110 -0
  4. package/up/agents/up-arquiteto.md +95 -39
  5. package/up/agents/up-auditor.md +218 -0
  6. package/up/agents/up-executor.md +94 -31
  7. package/up/agents/up-mapeador-codigo.md +63 -10
  8. package/up/agents/up-pesquisador.md +278 -0
  9. package/up/agents/up-revisor.md +249 -0
  10. package/up/agents/up-sintetizador.md +156 -179
  11. package/up/agents/up-tester.md +280 -0
  12. package/up/agents/up-verificador.md +95 -11
  13. package/up/bin/install.js +182 -19
  14. package/up/bin/lib/core.cjs +17 -43
  15. package/up/bin/lib/github.cjs +495 -0
  16. package/up/bin/lib/multica.cjs +424 -0
  17. package/up/bin/up-tools.cjs +167 -46
  18. package/up/commands/auditar.md +66 -0
  19. package/up/commands/build.md +54 -43
  20. package/up/commands/depurar.md +1 -1
  21. package/up/commands/plan.md +52 -38
  22. package/up/commands/rapido.md +15 -9
  23. package/up/commands/testar.md +81 -122
  24. package/up/commands/up.md +106 -0
  25. package/up/hooks/up-session-start.js +107 -0
  26. package/up/references/engineering-principles.md +1 -1
  27. package/up/references/governance-rules.md +5 -5
  28. package/up/references/production-requirements.md +1 -1
  29. package/up/references/severity-levels.md +2 -2
  30. package/up/references/tdd-evidence-types.md +81 -0
  31. package/up/skills/up-brainstorm/SKILL.md +54 -0
  32. package/up/skills/up-brainstorm/visual-companion.md +33 -0
  33. package/up/skills/up-tdd/SKILL.md +39 -0
  34. package/up/skills/up-verificar-antes-de-concluir/SKILL.md +49 -0
  35. package/up/skills/usando-up/SKILL.md +26 -0
  36. package/up/templates/audit-plan.md +3 -3
  37. package/up/templates/audit-report.md +2 -2
  38. package/up/templates/design-tokens.md +2 -2
  39. package/up/workflows/auditar.md +255 -0
  40. package/up/workflows/build.md +600 -386
  41. package/up/workflows/dcrv.md +183 -99
  42. package/up/workflows/governance.md +112 -220
  43. package/up/workflows/plan.md +169 -399
  44. package/up/workflows/rapido.md +7 -1
  45. package/up/workflows/up.md +447 -0
  46. package/up/agents/up-analista-codigo.md +0 -446
  47. package/up/agents/up-api-tester.md +0 -405
  48. package/up/agents/up-architecture-supervisor.md +0 -126
  49. package/up/agents/up-audit-supervisor.md +0 -83
  50. package/up/agents/up-auditor-modernidade.md +0 -378
  51. package/up/agents/up-auditor-performance.md +0 -426
  52. package/up/agents/up-auditor-ux.md +0 -396
  53. package/up/agents/up-backend-specialist.md +0 -175
  54. package/up/agents/up-blind-validator.md +0 -259
  55. package/up/agents/up-chief-architect.md +0 -184
  56. package/up/agents/up-chief-engineer.md +0 -202
  57. package/up/agents/up-chief-operations.md +0 -123
  58. package/up/agents/up-chief-product.md +0 -103
  59. package/up/agents/up-chief-quality.md +0 -211
  60. package/up/agents/up-clone-crawler.md +0 -234
  61. package/up/agents/up-clone-design-extractor.md +0 -227
  62. package/up/agents/up-clone-feature-mapper.md +0 -225
  63. package/up/agents/up-clone-prd-writer.md +0 -169
  64. package/up/agents/up-clone-verifier.md +0 -227
  65. package/up/agents/up-code-reviewer.md +0 -229
  66. package/up/agents/up-consolidador-ideias.md +0 -493
  67. package/up/agents/up-database-specialist.md +0 -169
  68. package/up/agents/up-delivery-auditor.md +0 -247
  69. package/up/agents/up-devops-agent.md +0 -203
  70. package/up/agents/up-execution-supervisor.md +0 -315
  71. package/up/agents/up-exhaustive-tester.md +0 -348
  72. package/up/agents/up-frontend-specialist.md +0 -152
  73. package/up/agents/up-operations-supervisor.md +0 -94
  74. package/up/agents/up-pesquisador-mercado.md +0 -350
  75. package/up/agents/up-pesquisador-projeto.md +0 -358
  76. package/up/agents/up-planning-auditor.md +0 -284
  77. package/up/agents/up-planning-supervisor.md +0 -260
  78. package/up/agents/up-product-analyst.md +0 -192
  79. package/up/agents/up-product-supervisor.md +0 -83
  80. package/up/agents/up-project-ceo.md +0 -352
  81. package/up/agents/up-qa-agent.md +0 -171
  82. package/up/agents/up-quality-supervisor.md +0 -178
  83. package/up/agents/up-requirements-validator.md +0 -230
  84. package/up/agents/up-security-reviewer.md +0 -137
  85. package/up/agents/up-sintetizador-melhorias.md +0 -407
  86. package/up/agents/up-system-designer.md +0 -332
  87. package/up/agents/up-technical-writer.md +0 -188
  88. package/up/agents/up-verification-supervisor.md +0 -111
  89. package/up/agents/up-visual-critic.md +0 -358
  90. package/up/commands/adicionar-fase.md +0 -47
  91. package/up/commands/adicionar-testes.md +0 -145
  92. package/up/commands/ajuda.md +0 -176
  93. package/up/commands/atualizar.md +0 -103
  94. package/up/commands/clone-builder.md +0 -67
  95. package/up/commands/configurar.md +0 -219
  96. package/up/commands/custos.md +0 -67
  97. package/up/commands/dashboard.md +0 -48
  98. package/up/commands/discutir-fase.md +0 -35
  99. package/up/commands/executar-fase.md +0 -40
  100. package/up/commands/ideias.md +0 -49
  101. package/up/commands/iniciar.md +0 -31
  102. package/up/commands/mapear-codigo.md +0 -63
  103. package/up/commands/melhorias.md +0 -45
  104. package/up/commands/mobile-first.md +0 -71
  105. package/up/commands/modo-builder.md +0 -186
  106. package/up/commands/novo-projeto.md +0 -40
  107. package/up/commands/onboard.md +0 -69
  108. package/up/commands/pausar.md +0 -33
  109. package/up/commands/planejar-fase.md +0 -45
  110. package/up/commands/progresso.md +0 -33
  111. package/up/commands/remover-fase.md +0 -34
  112. package/up/commands/resetar.md +0 -27
  113. package/up/commands/retomar.md +0 -35
  114. package/up/commands/saude.md +0 -103
  115. package/up/commands/ux-tester.md +0 -63
  116. package/up/commands/verificar-trabalho.md +0 -35
  117. package/up/workflows/adicionar-fase.md +0 -112
  118. package/up/workflows/builder-e2e.md +0 -501
  119. package/up/workflows/builder.md +0 -3419
  120. package/up/workflows/ceo-intake.md +0 -305
  121. package/up/workflows/ceo-updates.md +0 -183
  122. package/up/workflows/clone-builder.md +0 -320
  123. package/up/workflows/discutir-fase.md +0 -336
  124. package/up/workflows/executar-fase.md +0 -358
  125. package/up/workflows/executar-plano.md +0 -659
  126. package/up/workflows/ideias.md +0 -381
  127. package/up/workflows/iniciar.md +0 -235
  128. package/up/workflows/melhorias.md +0 -409
  129. package/up/workflows/mobile-first.md +0 -692
  130. package/up/workflows/novo-projeto.md +0 -778
  131. package/up/workflows/planejar-fase.md +0 -293
  132. package/up/workflows/progresso.md +0 -226
  133. package/up/workflows/retomar.md +0 -231
  134. package/up/workflows/ux-tester.md +0 -526
  135. package/up/workflows/verificar-trabalho.md +0 -308
@@ -0,0 +1,495 @@
1
+ /**
2
+ * github.cjs — GitHub-native execution layer for UP (worktree -> issue -> PR -> merge)
3
+ *
4
+ * Construido sobre execGit (core.cjs). FAIL-OPEN em tudo: se faltar `gh` ou
5
+ * remote, degrada para git local (branch + merge local, issue/pr=null) com aviso,
6
+ * nunca crasha. git worktree e sempre local (funciona offline).
7
+ *
8
+ * Modelo de estado: .plano/git-map.json (canonico no working dir principal).
9
+ * { github_native, merge_strategy, phases: { N: {branch, worktree, issue, issue_url, pr, pr_url, status} } }
10
+ *
11
+ * Branch: up/fase-NN-slug (NN zero-pad)
12
+ * Worktree: <repoParent>/.up-worktrees/<repoName>/fase-NN-slug (FORA do repo)
13
+ *
14
+ * Exporta: startPhase, finishPhase, status.
15
+ */
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const { execSync } = require('child_process');
20
+ const { execGit, loadConfig } = require('./core.cjs');
21
+
22
+ // =====================================================================
23
+ // Detecção de ambiente (gh + remote) — FAIL-OPEN
24
+ // =====================================================================
25
+
26
+ /** Existe remote `origin`? Decide local-only vs GitHub. */
27
+ function gitHasRemote(cwd) {
28
+ const res = execGit(cwd, ['remote', 'get-url', 'origin']);
29
+ return res.exitCode === 0 && res.stdout.trim().length > 0;
30
+ }
31
+
32
+ function getRemoteUrl(cwd) {
33
+ const res = execGit(cwd, ['remote', 'get-url', 'origin']);
34
+ return res.exitCode === 0 ? res.stdout.trim() : null;
35
+ }
36
+
37
+ /** `gh` instalado E autenticado? */
38
+ function ghAvailable(cwd) {
39
+ try {
40
+ execSync('gh auth status', { cwd, stdio: 'pipe' });
41
+ return true;
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Executa `gh <args>` no cwd. Retorna {exitCode, stdout, stderr}.
49
+ * Nunca lança: erro de gh ausente/falha vira exitCode != 0.
50
+ */
51
+ function execGh(cwd, args) {
52
+ try {
53
+ const escaped = args.map(a => {
54
+ if (/^[a-zA-Z0-9._\-/=:@,]+$/.test(a)) return a;
55
+ return "'" + String(a).replace(/'/g, "'\\''") + "'";
56
+ });
57
+ const stdout = execSync('gh ' + escaped.join(' '), {
58
+ cwd,
59
+ stdio: 'pipe',
60
+ encoding: 'utf-8',
61
+ });
62
+ return { exitCode: 0, stdout: stdout.trim(), stderr: '' };
63
+ } catch (err) {
64
+ return {
65
+ exitCode: err.status ?? 1,
66
+ stdout: (err.stdout ?? '').toString().trim(),
67
+ stderr: (err.stderr ?? '').toString().trim(),
68
+ };
69
+ }
70
+ }
71
+
72
+ /** github-native ativo? (config.github_native default true) AND gh+remote disponiveis. */
73
+ function githubMode(cwd, solo) {
74
+ if (solo) return false;
75
+ const config = loadConfig(cwd);
76
+ if (config.github_native === false) return false;
77
+ return ghAvailable(cwd) && gitHasRemote(cwd);
78
+ }
79
+
80
+ // =====================================================================
81
+ // Helpers de fase / branch / worktree
82
+ // =====================================================================
83
+
84
+ /** Zero-pad o número da fase para 2 dígitos, preservando letra/decimal. */
85
+ function padPhase(phase) {
86
+ const m = String(phase).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
87
+ if (!m) return String(phase);
88
+ const padded = m[1].padStart(2, '0');
89
+ const letter = m[2] ? m[2].toUpperCase() : '';
90
+ const decimal = m[3] || '';
91
+ return padded + letter + decimal;
92
+ }
93
+
94
+ function slugify(text) {
95
+ if (!text) return '';
96
+ return String(text).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
97
+ }
98
+
99
+ /** up/fase-NN-slug */
100
+ function branchName(phase, slug) {
101
+ const nn = padPhase(phase);
102
+ const s = slugify(slug);
103
+ return s ? `up/fase-${nn}-${s}` : `up/fase-${nn}`;
104
+ }
105
+
106
+ /** Caminho do toplevel do repo principal (resolve worktrees aninhadas). */
107
+ function repoToplevel(cwd) {
108
+ const res = execGit(cwd, ['rev-parse', '--show-toplevel']);
109
+ return res.exitCode === 0 && res.stdout ? res.stdout.trim() : cwd;
110
+ }
111
+
112
+ /**
113
+ * Worktree FORA do repo: <repoParent>/.up-worktrees/<repoName>/fase-NN-slug
114
+ * Espelha a estrutura do nome de branch, mas com `/` -> `-` (1 nivel de pasta).
115
+ */
116
+ function worktreePath(cwd, phase, slug) {
117
+ const top = repoToplevel(cwd);
118
+ const repoName = path.basename(top);
119
+ const repoParent = path.dirname(top);
120
+ const nn = padPhase(phase);
121
+ const s = slugify(slug);
122
+ const dirName = s ? `fase-${nn}-${s}` : `fase-${nn}`;
123
+ return path.join(repoParent, '.up-worktrees', repoName, dirName);
124
+ }
125
+
126
+ /** Garante que .up-worktrees nao polua git status do repo principal (exclude local + gitignore best-effort). */
127
+ function shieldWorktreeDir(cwd) {
128
+ const top = repoToplevel(cwd);
129
+ // 1) .git/info/exclude (nao versionado, sempre seguro)
130
+ try {
131
+ const excludePath = path.join(top, '.git', 'info', 'exclude');
132
+ let content = fs.existsSync(excludePath) ? fs.readFileSync(excludePath, 'utf-8') : '';
133
+ if (!content.split('\n').some(l => l.trim() === '.up-worktrees/' || l.trim() === '.up-worktrees')) {
134
+ if (content.length && !content.endsWith('\n')) content += '\n';
135
+ content += '.up-worktrees/\n';
136
+ fs.writeFileSync(excludePath, content, 'utf-8');
137
+ }
138
+ } catch {
139
+ // best-effort, ignora
140
+ }
141
+ }
142
+
143
+ // =====================================================================
144
+ // git-map.json — leitura/escrita (canonico no working dir principal)
145
+ // =====================================================================
146
+
147
+ function gitMapPath(cwd) {
148
+ return path.join(repoToplevel(cwd), '.plano', 'git-map.json');
149
+ }
150
+
151
+ function readGitMap(cwd) {
152
+ const config = loadConfig(cwd);
153
+ const defaults = {
154
+ github_native: config.github_native !== false,
155
+ merge_strategy: config.merge_strategy || 'squash',
156
+ phases: {},
157
+ };
158
+ try {
159
+ const p = gitMapPath(cwd);
160
+ if (!fs.existsSync(p)) return defaults;
161
+ const parsed = JSON.parse(fs.readFileSync(p, 'utf-8'));
162
+ return {
163
+ github_native: parsed.github_native ?? defaults.github_native,
164
+ merge_strategy: parsed.merge_strategy ?? defaults.merge_strategy,
165
+ phases: parsed.phases ?? {},
166
+ };
167
+ } catch {
168
+ return defaults;
169
+ }
170
+ }
171
+
172
+ function writeGitMap(cwd, map) {
173
+ const p = gitMapPath(cwd);
174
+ try {
175
+ fs.mkdirSync(path.dirname(p), { recursive: true });
176
+ fs.writeFileSync(p, JSON.stringify(map, null, 2), 'utf-8');
177
+ return true;
178
+ } catch {
179
+ return false;
180
+ }
181
+ }
182
+
183
+ function phaseKey(phase) {
184
+ return padPhase(phase).replace(/^0+(\d)/, '$1'); // "03" -> "3", "02.1" -> "2.1"
185
+ }
186
+
187
+ // =====================================================================
188
+ // startPhase — worktree + branch + (opcional) issue
189
+ // =====================================================================
190
+
191
+ /**
192
+ * startPhase({cwd, phase, slug, solo})
193
+ * - Cria branch up/fase-NN-slug e worktree FORA do repo.
194
+ * - Se github_native e gh+remote disponiveis: cria gh issue.
195
+ * - Com --solo: degrada (sem worktree, sem issue) — trabalha na branch atual.
196
+ * - Sem gh/remote: cria worktree+branch local, issue=null (fail-open, nunca crasha).
197
+ * - Escreve .plano/git-map.json. Retorna {branch, worktree, issue, issue_url, mode, warnings}.
198
+ */
199
+ function startPhase({ cwd, phase, slug, solo = false }) {
200
+ const warnings = [];
201
+ const key = phaseKey(phase);
202
+ const branch = branchName(phase, slug);
203
+ const ghOn = githubMode(cwd, solo);
204
+
205
+ const map = readGitMap(cwd);
206
+ if (solo) map.github_native = false;
207
+
208
+ // --- modo SOLO: nada de worktree/issue, trabalha na branch atual ---
209
+ if (solo) {
210
+ const cur = execGit(cwd, ['rev-parse', '--abbrev-ref', 'HEAD']);
211
+ const currentBranch = cur.exitCode === 0 ? cur.stdout.trim() : null;
212
+ map.phases[key] = {
213
+ ...(map.phases[key] || {}),
214
+ branch: currentBranch,
215
+ worktree: null,
216
+ issue: null,
217
+ issue_url: null,
218
+ pr: null,
219
+ pr_url: null,
220
+ status: 'in_progress',
221
+ };
222
+ writeGitMap(cwd, map);
223
+ return {
224
+ branch: currentBranch,
225
+ worktree: null,
226
+ issue: null,
227
+ issue_url: null,
228
+ mode: 'solo',
229
+ warnings,
230
+ };
231
+ }
232
+
233
+ // --- modo GitHub-native ou git-local: criar worktree+branch ---
234
+ const wt = worktreePath(cwd, phase, slug);
235
+ shieldWorktreeDir(cwd);
236
+
237
+ // Worktree e branch existem? (idempotente / recovery)
238
+ const existing = (map.phases[key] && map.phases[key].worktree) || null;
239
+ let worktreeCreated = false;
240
+
241
+ if (!fs.existsSync(wt)) {
242
+ fs.mkdirSync(path.dirname(wt), { recursive: true });
243
+ // Branch ja existe? Anexa sem -b. Senao cria com -b.
244
+ const branchExists = execGit(cwd, ['rev-parse', '--verify', '--quiet', branch]).exitCode === 0;
245
+ const wtArgs = branchExists
246
+ ? ['worktree', 'add', wt, branch]
247
+ : ['worktree', 'add', '-b', branch, wt];
248
+ const wtRes = execGit(cwd, wtArgs);
249
+ if (wtRes.exitCode !== 0) {
250
+ warnings.push('worktree_failed: ' + (wtRes.stderr || wtRes.stdout));
251
+ // FAIL-OPEN: cai pra branch local sem worktree
252
+ const co = execGit(cwd, branchExists ? ['checkout', branch] : ['checkout', '-b', branch]);
253
+ if (co.exitCode !== 0) warnings.push('branch_checkout_failed: ' + (co.stderr || co.stdout));
254
+ } else {
255
+ worktreeCreated = true;
256
+ }
257
+ } else {
258
+ worktreeCreated = true; // ja existia
259
+ }
260
+ const worktree = worktreeCreated || existing ? wt : null;
261
+
262
+ // --- issue (so em github-native) ---
263
+ let issue = null;
264
+ let issueUrl = null;
265
+ if (ghOn) {
266
+ const title = `[fase ${padPhase(phase)}] ${slug || branch}`;
267
+ const body = buildIssueBody(cwd, phase, slug);
268
+ const res = execGh(cwd, ['issue', 'create', '--title', title, '--body', body]);
269
+ if (res.exitCode === 0) {
270
+ issueUrl = res.stdout.trim().split('\n').pop().trim() || null;
271
+ const m = issueUrl && issueUrl.match(/\/issues\/(\d+)/);
272
+ issue = m ? parseInt(m[1], 10) : null;
273
+ } else {
274
+ warnings.push('issue_create_failed: ' + (res.stderr || res.stdout));
275
+ }
276
+ } else if (!solo) {
277
+ if (!ghAvailable(cwd)) warnings.push('gh indisponivel ou nao autenticado: degradando para git local (sem issue/PR)');
278
+ else if (!gitHasRemote(cwd)) warnings.push('sem remote origin: degradando para git local (sem issue/PR)');
279
+ }
280
+
281
+ map.phases[key] = {
282
+ ...(map.phases[key] || {}),
283
+ branch,
284
+ worktree,
285
+ issue,
286
+ issue_url: issueUrl,
287
+ pr: (map.phases[key] && map.phases[key].pr) || null,
288
+ pr_url: (map.phases[key] && map.phases[key].pr_url) || null,
289
+ status: 'in_progress',
290
+ };
291
+ writeGitMap(cwd, map);
292
+
293
+ return {
294
+ branch,
295
+ worktree,
296
+ issue,
297
+ issue_url: issueUrl,
298
+ mode: ghOn ? 'github' : 'git-local',
299
+ warnings,
300
+ };
301
+ }
302
+
303
+ /** Body da issue: goal + criterios do ROADMAP, se disponiveis. */
304
+ function buildIssueBody(cwd, phase, slug) {
305
+ let body = `Fase ${padPhase(phase)}` + (slug ? ` — ${slug}` : '') + '\n\nGerado por UP (github-native).';
306
+ try {
307
+ const { getRoadmapPhaseInternal } = require('./core.cjs');
308
+ const rp = getRoadmapPhaseInternal(repoToplevel(cwd), phase);
309
+ if (rp && rp.goal) body = `**Objetivo:** ${rp.goal}\n\n${body}`;
310
+ } catch {
311
+ // best-effort
312
+ }
313
+ return body;
314
+ }
315
+
316
+ // =====================================================================
317
+ // finishPhase — solo | menu | auto/pr
318
+ // =====================================================================
319
+
320
+ /**
321
+ * finishPhase({cwd, phase, mode, strategy})
322
+ * - solo: nao faz nada (ja committado na branch atual). Marca status=done.
323
+ * - menu: imprime as 4 opcoes pro orquestrador perguntar (nao age).
324
+ * - auto (a.k.a. pr): gh pr create (body "Closes #<issue>") -> merge (squash default)
325
+ * -> cleanup worktree+branch. Sem remote: merge LOCAL da branch na base, cleanup.
326
+ * Atualiza git-map.json. Retorna estado da operacao.
327
+ */
328
+ function finishPhase({ cwd, phase, mode = 'menu', strategy }) {
329
+ const warnings = [];
330
+ const key = phaseKey(phase);
331
+ const map = readGitMap(cwd);
332
+ const entry = map.phases[key] || {};
333
+ const mergeStrategy = strategy || map.merge_strategy || loadConfig(cwd).merge_strategy || 'squash';
334
+
335
+ // --- SOLO: nada a fazer, ja committado ---
336
+ if (mode === 'solo') {
337
+ entry.status = 'done';
338
+ map.phases[key] = entry;
339
+ writeGitMap(cwd, map);
340
+ return { mode: 'solo', action: 'none', status: 'done', warnings };
341
+ }
342
+
343
+ // --- MENU: imprime as 4 opcoes, nao age ---
344
+ if (mode === 'menu') {
345
+ return {
346
+ mode: 'menu',
347
+ action: 'prompt',
348
+ options: [
349
+ { id: 1, label: 'merge local', detail: 'Mescla a branch na base localmente (sem PR).' },
350
+ { id: 2, label: 'abrir PR', detail: 'Cria PR no GitHub (Closes #issue) e para pra revisao.' },
351
+ { id: 3, label: 'deixa a branch', detail: 'Mantem a branch/worktree como esta.' },
352
+ { id: 4, label: 'descarta', detail: 'Remove worktree e branch sem mesclar.' },
353
+ ],
354
+ branch: entry.branch || null,
355
+ issue: entry.issue || null,
356
+ warnings,
357
+ };
358
+ }
359
+
360
+ // --- AUTO / PR ---
361
+ if (mode === 'auto' || mode === 'pr') {
362
+ const branch = entry.branch || branchName(phase, '');
363
+ const worktree = entry.worktree || null;
364
+ const ghOn = ghAvailable(cwd) && gitHasRemote(cwd) && map.github_native !== false;
365
+
366
+ // Base de merge (default: branch ativa na main do repo)
367
+ const baseRes = execGit(cwd, ['symbolic-ref', '--short', 'HEAD']);
368
+ const base = baseRes.exitCode === 0 ? baseRes.stdout.trim() : 'main';
369
+
370
+ let pr = entry.pr || null;
371
+ let prUrl = entry.pr_url || null;
372
+
373
+ if (ghOn) {
374
+ // push da branch (necessario pro PR). Worktree ja tem a branch checada.
375
+ const pushCwd = worktree && fs.existsSync(worktree) ? worktree : cwd;
376
+ const push = execGit(pushCwd, ['push', '-u', 'origin', branch]);
377
+ if (push.exitCode !== 0) warnings.push('push_failed: ' + (push.stderr || push.stdout));
378
+
379
+ // gh pr create (Closes #issue no body)
380
+ const issueRef = entry.issue ? `\n\nCloses #${entry.issue}` : '';
381
+ const prTitle = `Fase ${padPhase(phase)}` + (entry.branch ? `: ${entry.branch.replace(/^up\/fase-\d+-?/, '')}` : '');
382
+ const prBody = `Entrega da fase ${padPhase(phase)} (UP github-native).${issueRef}`;
383
+ const create = execGh(cwd, ['pr', 'create', '--base', base, '--head', branch, '--title', prTitle, '--body', prBody]);
384
+ if (create.exitCode === 0) {
385
+ prUrl = create.stdout.trim().split('\n').pop().trim() || null;
386
+ const m = prUrl && prUrl.match(/\/pull\/(\d+)/);
387
+ pr = m ? parseInt(m[1], 10) : null;
388
+ } else {
389
+ warnings.push('pr_create_failed: ' + (create.stderr || create.stdout));
390
+ }
391
+
392
+ // merge via gh
393
+ const stratFlag = mergeStrategy === 'merge' ? '--merge' : mergeStrategy === 'rebase' ? '--rebase' : '--squash';
394
+ const mergeTarget = pr ? String(pr) : branch;
395
+ const merge = execGh(cwd, ['pr', 'merge', mergeTarget, stratFlag, '--delete-branch']);
396
+ if (merge.exitCode !== 0) {
397
+ warnings.push('pr_merge_failed: ' + (merge.stderr || merge.stdout));
398
+ } else {
399
+ entry.status = 'merged';
400
+ }
401
+ } else {
402
+ // FAIL-OPEN: merge LOCAL (sem remote/gh)
403
+ warnings.push('sem gh/remote: merge local da branch ' + branch + ' em ' + base);
404
+ // Garante base checada no repo principal
405
+ const co = execGit(cwd, ['checkout', base]);
406
+ if (co.exitCode !== 0) warnings.push('checkout_base_failed: ' + (co.stderr || co.stdout));
407
+ const stratArgs = mergeStrategy === 'squash'
408
+ ? ['merge', '--squash', branch]
409
+ : ['merge', '--no-ff', branch];
410
+ const merge = execGit(cwd, stratArgs);
411
+ if (merge.exitCode !== 0) {
412
+ warnings.push('local_merge_failed: ' + (merge.stderr || merge.stdout));
413
+ } else {
414
+ if (mergeStrategy === 'squash') {
415
+ const c = execGit(cwd, ['commit', '-m', `feat(fase-${padPhase(phase)}): merge da fase`]);
416
+ if (c.exitCode !== 0 && !/nothing to commit/.test(c.stderr + c.stdout)) {
417
+ warnings.push('local_squash_commit_failed: ' + (c.stderr || c.stdout));
418
+ }
419
+ }
420
+ entry.status = 'merged';
421
+ }
422
+ }
423
+
424
+ // cleanup: remove worktree e branch local
425
+ if (worktree && fs.existsSync(worktree)) {
426
+ const rm = execGit(cwd, ['worktree', 'remove', worktree]);
427
+ if (rm.exitCode !== 0) {
428
+ const rmF = execGit(cwd, ['worktree', 'remove', '--force', worktree]);
429
+ if (rmF.exitCode !== 0) warnings.push('worktree_remove_failed: ' + (rmF.stderr || rmF.stdout));
430
+ }
431
+ // best-effort: remove a casca vazia que git deixa, e o dir-pai .up-worktrees/<repo> se vazio
432
+ try { if (fs.existsSync(worktree)) fs.rmdirSync(worktree); } catch (e) { /* nao-vazio: ignora */ }
433
+ try {
434
+ const parent = path.dirname(worktree);
435
+ if (fs.existsSync(parent) && fs.readdirSync(parent).length === 0) fs.rmdirSync(parent);
436
+ } catch (e) { /* ignora */ }
437
+ }
438
+ // remove branch local (se ainda existir e nao for a branch ativa)
439
+ const delBranch = execGit(cwd, ['branch', '-D', branch]);
440
+ if (delBranch.exitCode !== 0 && !/not found|não encontrad/i.test(delBranch.stderr)) {
441
+ // silencioso: gh --delete-branch ja pode ter removido
442
+ }
443
+
444
+ entry.pr = pr;
445
+ entry.pr_url = prUrl;
446
+ if (!entry.status || entry.status === 'in_progress') entry.status = ghOn ? 'merged' : entry.status;
447
+ map.phases[key] = entry;
448
+ writeGitMap(cwd, map);
449
+
450
+ return {
451
+ mode: mode,
452
+ action: 'merged',
453
+ pr,
454
+ pr_url: prUrl,
455
+ branch,
456
+ strategy: mergeStrategy,
457
+ status: entry.status,
458
+ warnings,
459
+ };
460
+ }
461
+
462
+ warnings.push('modo desconhecido: ' + mode + ' (use solo|menu|auto)');
463
+ return { mode, action: 'noop', warnings };
464
+ }
465
+
466
+ // =====================================================================
467
+ // status — mostra git-map.json
468
+ // =====================================================================
469
+
470
+ /** status({cwd}) — retorna o git-map.json atual (ou defaults se nao existir). Nunca crasha. */
471
+ function status({ cwd }) {
472
+ const map = readGitMap(cwd);
473
+ return {
474
+ github_native: map.github_native,
475
+ merge_strategy: map.merge_strategy,
476
+ gh_available: ghAvailable(cwd),
477
+ has_remote: gitHasRemote(cwd),
478
+ remote: getRemoteUrl(cwd),
479
+ phases: map.phases,
480
+ };
481
+ }
482
+
483
+ module.exports = {
484
+ startPhase,
485
+ finishPhase,
486
+ status,
487
+ // helpers expostos para teste/reuso
488
+ branchName,
489
+ worktreePath,
490
+ gitHasRemote,
491
+ ghAvailable,
492
+ githubMode,
493
+ readGitMap,
494
+ writeGitMap,
495
+ };