up-cc 0.16.0 → 2.0.0

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 (134) 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 +190 -21
  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 +39 -0
  32. package/up/skills/up-tdd/SKILL.md +39 -0
  33. package/up/skills/up-verificar-antes-de-concluir/SKILL.md +49 -0
  34. package/up/skills/usando-up/SKILL.md +26 -0
  35. package/up/templates/audit-plan.md +3 -3
  36. package/up/templates/audit-report.md +2 -2
  37. package/up/templates/design-tokens.md +2 -2
  38. package/up/workflows/auditar.md +255 -0
  39. package/up/workflows/build.md +600 -386
  40. package/up/workflows/dcrv.md +183 -99
  41. package/up/workflows/governance.md +112 -220
  42. package/up/workflows/plan.md +169 -399
  43. package/up/workflows/rapido.md +7 -1
  44. package/up/workflows/up.md +447 -0
  45. package/up/agents/up-analista-codigo.md +0 -446
  46. package/up/agents/up-api-tester.md +0 -405
  47. package/up/agents/up-architecture-supervisor.md +0 -126
  48. package/up/agents/up-audit-supervisor.md +0 -83
  49. package/up/agents/up-auditor-modernidade.md +0 -378
  50. package/up/agents/up-auditor-performance.md +0 -426
  51. package/up/agents/up-auditor-ux.md +0 -396
  52. package/up/agents/up-backend-specialist.md +0 -175
  53. package/up/agents/up-blind-validator.md +0 -259
  54. package/up/agents/up-chief-architect.md +0 -184
  55. package/up/agents/up-chief-engineer.md +0 -202
  56. package/up/agents/up-chief-operations.md +0 -123
  57. package/up/agents/up-chief-product.md +0 -103
  58. package/up/agents/up-chief-quality.md +0 -211
  59. package/up/agents/up-clone-crawler.md +0 -234
  60. package/up/agents/up-clone-design-extractor.md +0 -227
  61. package/up/agents/up-clone-feature-mapper.md +0 -225
  62. package/up/agents/up-clone-prd-writer.md +0 -169
  63. package/up/agents/up-clone-verifier.md +0 -227
  64. package/up/agents/up-code-reviewer.md +0 -229
  65. package/up/agents/up-consolidador-ideias.md +0 -493
  66. package/up/agents/up-database-specialist.md +0 -169
  67. package/up/agents/up-delivery-auditor.md +0 -247
  68. package/up/agents/up-devops-agent.md +0 -203
  69. package/up/agents/up-execution-supervisor.md +0 -315
  70. package/up/agents/up-exhaustive-tester.md +0 -348
  71. package/up/agents/up-frontend-specialist.md +0 -152
  72. package/up/agents/up-operations-supervisor.md +0 -94
  73. package/up/agents/up-pesquisador-mercado.md +0 -350
  74. package/up/agents/up-pesquisador-projeto.md +0 -358
  75. package/up/agents/up-planning-auditor.md +0 -284
  76. package/up/agents/up-planning-supervisor.md +0 -260
  77. package/up/agents/up-product-analyst.md +0 -192
  78. package/up/agents/up-product-supervisor.md +0 -83
  79. package/up/agents/up-project-ceo.md +0 -352
  80. package/up/agents/up-qa-agent.md +0 -171
  81. package/up/agents/up-quality-supervisor.md +0 -178
  82. package/up/agents/up-requirements-validator.md +0 -230
  83. package/up/agents/up-security-reviewer.md +0 -137
  84. package/up/agents/up-sintetizador-melhorias.md +0 -407
  85. package/up/agents/up-system-designer.md +0 -332
  86. package/up/agents/up-technical-writer.md +0 -188
  87. package/up/agents/up-verification-supervisor.md +0 -111
  88. package/up/agents/up-visual-critic.md +0 -358
  89. package/up/commands/adicionar-fase.md +0 -47
  90. package/up/commands/adicionar-testes.md +0 -145
  91. package/up/commands/ajuda.md +0 -176
  92. package/up/commands/atualizar.md +0 -103
  93. package/up/commands/clone-builder.md +0 -67
  94. package/up/commands/configurar.md +0 -219
  95. package/up/commands/custos.md +0 -67
  96. package/up/commands/dashboard.md +0 -48
  97. package/up/commands/discutir-fase.md +0 -35
  98. package/up/commands/executar-fase.md +0 -40
  99. package/up/commands/ideias.md +0 -49
  100. package/up/commands/iniciar.md +0 -31
  101. package/up/commands/mapear-codigo.md +0 -63
  102. package/up/commands/melhorias.md +0 -45
  103. package/up/commands/mobile-first.md +0 -71
  104. package/up/commands/modo-builder.md +0 -186
  105. package/up/commands/novo-projeto.md +0 -40
  106. package/up/commands/onboard.md +0 -69
  107. package/up/commands/pausar.md +0 -33
  108. package/up/commands/planejar-fase.md +0 -45
  109. package/up/commands/progresso.md +0 -33
  110. package/up/commands/remover-fase.md +0 -34
  111. package/up/commands/resetar.md +0 -27
  112. package/up/commands/retomar.md +0 -35
  113. package/up/commands/saude.md +0 -103
  114. package/up/commands/ux-tester.md +0 -63
  115. package/up/commands/verificar-trabalho.md +0 -35
  116. package/up/workflows/adicionar-fase.md +0 -112
  117. package/up/workflows/builder-e2e.md +0 -501
  118. package/up/workflows/builder.md +0 -3419
  119. package/up/workflows/ceo-intake.md +0 -305
  120. package/up/workflows/ceo-updates.md +0 -183
  121. package/up/workflows/clone-builder.md +0 -320
  122. package/up/workflows/discutir-fase.md +0 -336
  123. package/up/workflows/executar-fase.md +0 -358
  124. package/up/workflows/executar-plano.md +0 -659
  125. package/up/workflows/ideias.md +0 -381
  126. package/up/workflows/iniciar.md +0 -235
  127. package/up/workflows/melhorias.md +0 -409
  128. package/up/workflows/mobile-first.md +0 -692
  129. package/up/workflows/novo-projeto.md +0 -778
  130. package/up/workflows/planejar-fase.md +0 -293
  131. package/up/workflows/progresso.md +0 -226
  132. package/up/workflows/retomar.md +0 -231
  133. package/up/workflows/ux-tester.md +0 -526
  134. 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
+ };