up-cc 2.0.2 → 2.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "up-cc",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "description": "Spec-driven development for Claude Code, Gemini, OpenCode and OpenAI Codex. Brainstorm-first, GitHub-native, TDD por tipo.",
5
5
  "bin": {
6
6
  "up-cc": "up/bin/install.js"
package/up/CHANGELOG.md CHANGED
@@ -4,6 +4,55 @@ Todas as mudancas relevantes do `up-cc` ficam documentadas aqui. O formato segue
4
4
  o espirito de [Keep a Changelog](https://keepachangelog.com/) e o versionamento
5
5
  e [SemVer](https://semver.org/). v2.0.0 e um **major** (breaking change).
6
6
 
7
+ ## 2.1.0
8
+
9
+ > GitHub e a interacao humana viram **eixos separados**. Antes, `--solo` desligava o
10
+ > GitHub (zero worktree/issue/PR) E pulava a cerimonia. Num run autonomo/loop isso fazia
11
+ > o projeto inteiro ir pro `main` sem nenhum artefato GitHub, mesmo com remote conectado.
12
+ > Agora `--solo` NAO desliga o GitHub: e so autonomia. Pra pular o GitHub use `--local`.
13
+
14
+ ### Mudado (atencao: semantica de `--solo`)
15
+
16
+ - **`--solo` agora MANTEM o GitHub-nativo.** Solo virou "autonomo total": cria
17
+ worktree + branch + issue + PR e auto-mergeia, SEM menu e SEM gate visual (pra
18
+ loop/headless onde ninguem aprova). Nao desliga mais o GitHub.
19
+ - **`--local` e o novo escape hatch sem GitHub** (o comportamento que `--solo` tinha
20
+ ate a 2.0.x): commit atomico na branch atual, zero worktree/issue/PR/rede. `/up:rapido`
21
+ segue como o atalho nomeado pro mesmo modo.
22
+ - **`--auto`** continua igual: pula so o menu de fim de fase, mantem GitHub, e o gate
23
+ visual ainda roda se `require_visual_test=true`.
24
+
25
+ ### Adicionado
26
+
27
+ - **Deteccao de GitHub por `gh` CLI OU MCP do GitHub.** O eixo GitHub liga sempre que
28
+ ha remote + (`gh` autenticado OU MCP do GitHub conectado). Transporte de issue/PR:
29
+ `gh` (CLI cria direto), `mcp` (o workflow cria via `mcp__...github__*` e grava de
30
+ volta), `none` (sem remote -> git local). Worktree/branch/push sao git local e
31
+ acontecem sempre, offline-ok.
32
+ - **Subcomandos `github record-issue` / `github record-pr`** no `up-tools.cjs` pra
33
+ gravar no `git-map.json` artefatos criados fora do `.cjs` (ex: via MCP). `record-pr
34
+ --merged` fecha a fase e limpa worktree+branch.
35
+ - **Flag `--local`** no `/up:build` e `start-phase`/`finish-phase` (`--mode local`).
36
+ - **Config builder grava `github_native: true` explicito** (up-arquiteto, workflows/up),
37
+ deixando a intencao visivel em run autonomo (antes dependia do default implicito).
38
+ - **Testes red-green** do `github.cjs` em `up/bin/lib/github.test.cjs` (10 casos; repo
39
+ git temp + remote bare local + seam `UP_FORCE_NO_GH`).
40
+
41
+ ### Corrigido
42
+
43
+ - **Bug de doc em `workflows/up.md`** que dizia `--solo (default, commit na branch atual)`
44
+ e descrevia `--pr` como o fluxo GitHub (vocabulario v1). Era a unica fonte que podia
45
+ induzir um agente autonomo a escolher `--solo` e pular o GitHub. Reescrito pro v2.
46
+ - **`--local` numa fase nao polui mais o `github_native` global** do `git-map.json`
47
+ (poderia degradar fases nao-local seguintes). `finish-phase auto` passou a consultar
48
+ o config, nao o flag global do mapa.
49
+
50
+ ### Migracao
51
+
52
+ - Quem usava `--solo` pra **commit local sem GitHub** deve trocar para `--local`.
53
+ - Quem quer **autonomo mantendo GitHub** (loop/headless): use `--solo` (sem gate visual)
54
+ ou `--auto` (com gate visual se `require_visual_test=true`).
55
+
7
56
  ## 2.0.0
8
57
 
9
58
  > Redesign completo do UP. **Breaking change.** Os comandos antigos somem (nao ha
@@ -305,11 +305,14 @@ Modo builder ativo. Proxima acao: planejar fase 1.
305
305
  "granularity": "standard",
306
306
  "parallelization": true,
307
307
  "commit_docs": true,
308
- "builder_mode": true
308
+ "builder_mode": true,
309
+ "github_native": true
309
310
  }
310
311
  ```
311
312
 
312
- Note: `builder_mode: true` sinaliza que o projeto foi criado em modo autonomo.
313
+ Note: `builder_mode: true` sinaliza que o projeto foi criado em modo autonomo. `github_native: true`
314
+ explicito deixa a intencao gravada no arquivo (o build usa true por default; gravar evita ambiguidade
315
+ em run autonomo/loop). So mude pra `false` se o projeto e local sem GitHub.
313
316
  </state_and_config>
314
317
 
315
318
  <brownfield_mode>
@@ -114,8 +114,9 @@ function loadConfig(cwd) {
114
114
  auto_advance: false,
115
115
  instrumentation: { enabled: true },
116
116
  budget_ceiling: null,
117
- // Fase 4: GitHub-native e o PADRAO. --solo no comando forca github_native=false
118
- // so na execucao (o build.md le a flag e roteia para o modo solo, sem persistir).
117
+ // GitHub-native e o PADRAO. So --local (ou este flag = false) desliga. --solo NAO
118
+ // desliga: solo = autonomia (sem menu/gate visual), GitHub segue ligado. Transporte
119
+ // de issue/PR: gh CLI OU MCP do GitHub (worktree/branch sao git local, sempre rodam).
119
120
  github_native: true,
120
121
  merge_strategy: 'squash',
121
122
  // Fim de fase: se a fase tem UI, sobe dev server e exige aprovacao visual ANTES do merge.
@@ -34,8 +34,13 @@ function getRemoteUrl(cwd) {
34
34
  return res.exitCode === 0 ? res.stdout.trim() : null;
35
35
  }
36
36
 
37
- /** `gh` instalado E autenticado? */
37
+ /**
38
+ * `gh` instalado E autenticado?
39
+ * Seam de teste: UP_FORCE_NO_GH=1 forca indisponivel; UP_FORCE_GH=1 forca disponivel.
40
+ */
38
41
  function ghAvailable(cwd) {
42
+ if (process.env.UP_FORCE_NO_GH === '1') return false;
43
+ if (process.env.UP_FORCE_GH === '1') return true;
39
44
  try {
40
45
  execSync('gh auth status', { cwd, stdio: 'pipe' });
41
46
  return true;
@@ -69,12 +74,34 @@ function execGh(cwd, args) {
69
74
  }
70
75
  }
71
76
 
72
- /** github-native ativo? (config.github_native default true) AND gh+remote disponiveis. */
73
- function githubMode(cwd, solo) {
74
- if (solo) return false;
77
+ /**
78
+ * EIXO A — queremos artefatos GitHub (worktree/branch sempre; issue/PR se remote)?
79
+ * Ligado a menos que: `--local`, config.github_native=false, ou sem remote origin.
80
+ * NAO depende de `gh`: gh-vs-MCP e o transporte (eixo separado), nao o liga/desliga.
81
+ */
82
+ function gitWantsRemote(cwd, local) {
83
+ if (local) return false;
75
84
  const config = loadConfig(cwd);
76
85
  if (config.github_native === false) return false;
77
- return ghAvailable(cwd) && gitHasRemote(cwd);
86
+ return gitHasRemote(cwd);
87
+ }
88
+
89
+ /**
90
+ * Transporte pra criar issue/PR remoto: 'gh' | 'mcp' | 'none'.
91
+ * - 'gh': gh CLI disponivel -> github.cjs cria direto (deterministico).
92
+ * - 'mcp': remote existe mas sem gh -> o WORKFLOW (LLM) cria via MCP do GitHub,
93
+ * porque um subprocesso Node nao chama tools MCP. github.cjs sinaliza e
94
+ * grava de volta via recordIssue/recordPr.
95
+ * - 'none': sem remote (ou --local / config off) -> git local puro, sem issue/PR.
96
+ */
97
+ function issueTransport(cwd, local) {
98
+ if (!gitWantsRemote(cwd, local)) return 'none';
99
+ return ghAvailable(cwd) ? 'gh' : 'mcp';
100
+ }
101
+
102
+ /** Compat: github-native "classico" = transporte gh disponivel. */
103
+ function githubMode(cwd, local) {
104
+ return issueTransport(cwd, local) === 'gh';
78
105
  }
79
106
 
80
107
  // =====================================================================
@@ -189,24 +216,27 @@ function phaseKey(phase) {
189
216
  // =====================================================================
190
217
 
191
218
  /**
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}.
219
+ * startPhase({cwd, phase, slug, local, solo})
220
+ * EIXO A (GitHub) e EIXO B (interacao) sao SEPARADOS:
221
+ * - `local=true` (flag --local / config off / /up:rapido): SEM worktree/issue, commit
222
+ * na branch atual. E o unico jeito de desligar o GitHub aqui.
223
+ * - `solo` NAO desliga o GitHub: solo so afeta o gate visual e o merge (no build.md).
224
+ * Por isso `solo` e aceito mas ignorado neste passo (compat).
225
+ * - Caso contrario: cria worktree+branch SEMPRE (git local, offline-ok). Issue conforme
226
+ * o transporte: 'gh' cria direto; 'mcp' deixa pro workflow criar via MCP (retorna
227
+ * `pending.issue` e mode 'github-mcp'); 'none' (sem remote) so git local.
228
+ * - Escreve .plano/git-map.json. Retorna {branch, worktree, issue, issue_url, mode, transport, warnings, pending?}.
198
229
  */
199
- function startPhase({ cwd, phase, slug, solo = false }) {
230
+ function startPhase({ cwd, phase, slug, local = false, solo = false }) {
231
+ void solo; // solo nao desliga GitHub (eixo de interacao, tratado no build.md)
200
232
  const warnings = [];
201
233
  const key = phaseKey(phase);
202
234
  const branch = branchName(phase, slug);
203
- const ghOn = githubMode(cwd, solo);
204
235
 
205
236
  const map = readGitMap(cwd);
206
- if (solo) map.github_native = false;
207
237
 
208
- // --- modo SOLO: nada de worktree/issue, trabalha na branch atual ---
209
- if (solo) {
238
+ // --- LOCAL: nada de worktree/issue, trabalha na branch atual ---
239
+ if (local) {
210
240
  const cur = execGit(cwd, ['rev-parse', '--abbrev-ref', 'HEAD']);
211
241
  const currentBranch = cur.exitCode === 0 ? cur.stdout.trim() : null;
212
242
  map.phases[key] = {
@@ -225,22 +255,21 @@ function startPhase({ cwd, phase, slug, solo = false }) {
225
255
  worktree: null,
226
256
  issue: null,
227
257
  issue_url: null,
228
- mode: 'solo',
258
+ mode: 'local',
259
+ transport: 'none',
229
260
  warnings,
230
261
  };
231
262
  }
232
263
 
233
- // --- modo GitHub-native ou git-local: criar worktree+branch ---
264
+ // --- worktree+branch SEMPRE (git local, independe de gh/MCP) ---
234
265
  const wt = worktreePath(cwd, phase, slug);
235
266
  shieldWorktreeDir(cwd);
236
267
 
237
- // Worktree e branch existem? (idempotente / recovery)
238
268
  const existing = (map.phases[key] && map.phases[key].worktree) || null;
239
269
  let worktreeCreated = false;
240
270
 
241
271
  if (!fs.existsSync(wt)) {
242
272
  fs.mkdirSync(path.dirname(wt), { recursive: true });
243
- // Branch ja existe? Anexa sem -b. Senao cria com -b.
244
273
  const branchExists = execGit(cwd, ['rev-parse', '--verify', '--quiet', branch]).exitCode === 0;
245
274
  const wtArgs = branchExists
246
275
  ? ['worktree', 'add', wt, branch]
@@ -248,7 +277,6 @@ function startPhase({ cwd, phase, slug, solo = false }) {
248
277
  const wtRes = execGit(cwd, wtArgs);
249
278
  if (wtRes.exitCode !== 0) {
250
279
  warnings.push('worktree_failed: ' + (wtRes.stderr || wtRes.stdout));
251
- // FAIL-OPEN: cai pra branch local sem worktree
252
280
  const co = execGit(cwd, branchExists ? ['checkout', branch] : ['checkout', '-b', branch]);
253
281
  if (co.exitCode !== 0) warnings.push('branch_checkout_failed: ' + (co.stderr || co.stdout));
254
282
  } else {
@@ -259,13 +287,16 @@ function startPhase({ cwd, phase, slug, solo = false }) {
259
287
  }
260
288
  const worktree = worktreeCreated || existing ? wt : null;
261
289
 
262
- // --- issue (so em github-native) ---
290
+ // --- issue: depende do transporte (gh | mcp | none) ---
291
+ const transport = issueTransport(cwd, local);
263
292
  let issue = null;
264
293
  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]);
294
+ let pending = null;
295
+ const issueTitle = `[fase ${padPhase(phase)}] ${slug || branch}`;
296
+ const issueBody = buildIssueBody(cwd, phase, slug);
297
+
298
+ if (transport === 'gh') {
299
+ const res = execGh(cwd, ['issue', 'create', '--title', issueTitle, '--body', issueBody]);
269
300
  if (res.exitCode === 0) {
270
301
  issueUrl = res.stdout.trim().split('\n').pop().trim() || null;
271
302
  const m = issueUrl && issueUrl.match(/\/issues\/(\d+)/);
@@ -273,9 +304,13 @@ function startPhase({ cwd, phase, slug, solo = false }) {
273
304
  } else {
274
305
  warnings.push('issue_create_failed: ' + (res.stderr || res.stdout));
275
306
  }
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)');
307
+ } else if (transport === 'mcp') {
308
+ // Node nao chama MCP: o workflow cria a issue via mcp__...github__issue_write
309
+ // e grava de volta com `up-tools github record-issue`. Worktree/branch ja existem.
310
+ pending = { issue: { title: issueTitle, body: issueBody } };
311
+ warnings.push('gh indisponivel: criar issue via MCP (github-mcp) e gravar com record-issue. worktree/branch ja prontos.');
312
+ } else {
313
+ warnings.push('sem remote origin: git local (sem issue/PR).');
279
314
  }
280
315
 
281
316
  map.phases[key] = {
@@ -290,14 +325,10 @@ function startPhase({ cwd, phase, slug, solo = false }) {
290
325
  };
291
326
  writeGitMap(cwd, map);
292
327
 
293
- return {
294
- branch,
295
- worktree,
296
- issue,
297
- issue_url: issueUrl,
298
- mode: ghOn ? 'github' : 'git-local',
299
- warnings,
300
- };
328
+ const mode = transport === 'gh' ? 'github' : transport === 'mcp' ? 'github-mcp' : 'git-local';
329
+ const ret = { branch, worktree, issue, issue_url: issueUrl, mode, transport, warnings };
330
+ if (pending) ret.pending = pending;
331
+ return ret;
301
332
  }
302
333
 
303
334
  /** Body da issue: goal + criterios do ROADMAP, se disponiveis. */
@@ -317,12 +348,34 @@ function buildIssueBody(cwd, phase, slug) {
317
348
  // finishPhase — solo | menu | auto/pr
318
349
  // =====================================================================
319
350
 
351
+ /** Remove worktree + branch local (best-effort). Usado por finishPhase e recordPr. */
352
+ function cleanupWorktreeBranch(cwd, worktree, branch, warnings) {
353
+ if (worktree && fs.existsSync(worktree)) {
354
+ const rm = execGit(cwd, ['worktree', 'remove', worktree]);
355
+ if (rm.exitCode !== 0) {
356
+ const rmF = execGit(cwd, ['worktree', 'remove', '--force', worktree]);
357
+ if (rmF.exitCode !== 0) warnings.push('worktree_remove_failed: ' + (rmF.stderr || rmF.stdout));
358
+ }
359
+ try { if (fs.existsSync(worktree)) fs.rmdirSync(worktree); } catch (e) { /* nao-vazio: ignora */ }
360
+ try {
361
+ const parent = path.dirname(worktree);
362
+ if (fs.existsSync(parent) && fs.readdirSync(parent).length === 0) fs.rmdirSync(parent);
363
+ } catch (e) { /* ignora */ }
364
+ }
365
+ if (branch) {
366
+ execGit(cwd, ['branch', '-D', branch]); // silencioso: gh/merge ja pode ter removido
367
+ }
368
+ }
369
+
320
370
  /**
321
371
  * finishPhase({cwd, phase, mode, strategy})
322
- * - solo: nao faz nada (ja committado na branch atual). Marca status=done.
372
+ * - local (alias 'solo'): nao faz nada (ja committado na branch atual). status=done.
323
373
  * - 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.
374
+ * - auto (a.k.a. pr): conforme o transporte ('gh' | 'mcp' | 'none'):
375
+ * 'gh' -> push + gh pr create (Closes #issue) + merge + cleanup.
376
+ * 'mcp' -> push e PARA: retorna action 'needs-mcp-pr' + pr_payload pro workflow
377
+ * criar/mergear o PR via MCP e fechar com `record-pr --merged` (que limpa).
378
+ * 'none' -> merge LOCAL fail-open + cleanup.
326
379
  * Atualiza git-map.json. Retorna estado da operacao.
327
380
  */
328
381
  function finishPhase({ cwd, phase, mode = 'menu', strategy }) {
@@ -332,12 +385,12 @@ function finishPhase({ cwd, phase, mode = 'menu', strategy }) {
332
385
  const entry = map.phases[key] || {};
333
386
  const mergeStrategy = strategy || map.merge_strategy || loadConfig(cwd).merge_strategy || 'squash';
334
387
 
335
- // --- SOLO: nada a fazer, ja committado ---
336
- if (mode === 'solo') {
388
+ // --- LOCAL / SOLO: nada a fazer, ja committado ---
389
+ if (mode === 'local' || mode === 'solo') {
337
390
  entry.status = 'done';
338
391
  map.phases[key] = entry;
339
392
  writeGitMap(cwd, map);
340
- return { mode: 'solo', action: 'none', status: 'done', warnings };
393
+ return { mode, action: 'none', status: 'done', warnings };
341
394
  }
342
395
 
343
396
  // --- MENU: imprime as 4 opcoes, nao age ---
@@ -361,7 +414,10 @@ function finishPhase({ cwd, phase, mode = 'menu', strategy }) {
361
414
  if (mode === 'auto' || mode === 'pr') {
362
415
  const branch = entry.branch || branchName(phase, '');
363
416
  const worktree = entry.worktree || null;
364
- const ghOn = ghAvailable(cwd) && gitHasRemote(cwd) && map.github_native !== false;
417
+ // mode 'auto' ja significa "quero GitHub": consulta o config (intencao do projeto),
418
+ // nao o flag global do git-map (que uma fase --local anterior poderia ter mexido).
419
+ const wants = gitHasRemote(cwd) && loadConfig(cwd).github_native !== false;
420
+ const transport = wants ? (ghAvailable(cwd) ? 'gh' : 'mcp') : 'none';
365
421
 
366
422
  // Base de merge (default: branch ativa na main do repo)
367
423
  const baseRes = execGit(cwd, ['symbolic-ref', '--short', 'HEAD']);
@@ -370,16 +426,41 @@ function finishPhase({ cwd, phase, mode = 'menu', strategy }) {
370
426
  let pr = entry.pr || null;
371
427
  let prUrl = entry.pr_url || null;
372
428
 
373
- if (ghOn) {
374
- // push da branch (necessario pro PR). Worktree ja tem a branch checada.
429
+ const issueRef = entry.issue ? `\n\nCloses #${entry.issue}` : '';
430
+ const prTitle = `Fase ${padPhase(phase)}` + (entry.branch ? `: ${entry.branch.replace(/^up\/fase-\d+-?/, '')}` : '');
431
+ const prBody = `Entrega da fase ${padPhase(phase)} (UP github-native).${issueRef}`;
432
+
433
+ // push da branch (necessario pro PR, gh OU MCP). Worktree tem a branch checada.
434
+ if (transport === 'gh' || transport === 'mcp') {
375
435
  const pushCwd = worktree && fs.existsSync(worktree) ? worktree : cwd;
376
436
  const push = execGit(pushCwd, ['push', '-u', 'origin', branch]);
377
437
  if (push.exitCode !== 0) warnings.push('push_failed: ' + (push.stderr || push.stdout));
438
+ }
378
439
 
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}`;
440
+ // --- transporte MCP: push feito, PR fica pro workflow (LLM) via MCP ---
441
+ if (transport === 'mcp') {
442
+ map.phases[key] = entry; // status segue in_progress ate record-pr
443
+ writeGitMap(cwd, map);
444
+ return {
445
+ mode,
446
+ action: 'needs-mcp-pr',
447
+ transport: 'mcp',
448
+ pr_payload: {
449
+ base,
450
+ head: branch,
451
+ title: prTitle,
452
+ body: prBody,
453
+ issue: entry.issue || null,
454
+ strategy: mergeStrategy,
455
+ worktree,
456
+ },
457
+ branch,
458
+ status: entry.status || 'in_progress',
459
+ warnings,
460
+ };
461
+ }
462
+
463
+ if (transport === 'gh') {
383
464
  const create = execGh(cwd, ['pr', 'create', '--base', base, '--head', branch, '--title', prTitle, '--body', prBody]);
384
465
  if (create.exitCode === 0) {
385
466
  prUrl = create.stdout.trim().split('\n').pop().trim() || null;
@@ -389,7 +470,6 @@ function finishPhase({ cwd, phase, mode = 'menu', strategy }) {
389
470
  warnings.push('pr_create_failed: ' + (create.stderr || create.stdout));
390
471
  }
391
472
 
392
- // merge via gh
393
473
  const stratFlag = mergeStrategy === 'merge' ? '--merge' : mergeStrategy === 'rebase' ? '--rebase' : '--squash';
394
474
  const mergeTarget = pr ? String(pr) : branch;
395
475
  const merge = execGh(cwd, ['pr', 'merge', mergeTarget, stratFlag, '--delete-branch']);
@@ -401,7 +481,6 @@ function finishPhase({ cwd, phase, mode = 'menu', strategy }) {
401
481
  } else {
402
482
  // FAIL-OPEN: merge LOCAL (sem remote/gh)
403
483
  warnings.push('sem gh/remote: merge local da branch ' + branch + ' em ' + base);
404
- // Garante base checada no repo principal
405
484
  const co = execGit(cwd, ['checkout', base]);
406
485
  if (co.exitCode !== 0) warnings.push('checkout_base_failed: ' + (co.stderr || co.stdout));
407
486
  const stratArgs = mergeStrategy === 'squash'
@@ -421,35 +500,19 @@ function finishPhase({ cwd, phase, mode = 'menu', strategy }) {
421
500
  }
422
501
  }
423
502
 
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
- }
503
+ cleanupWorktreeBranch(cwd, worktree, branch, warnings);
443
504
 
444
505
  entry.pr = pr;
445
506
  entry.pr_url = prUrl;
446
- if (!entry.status || entry.status === 'in_progress') entry.status = ghOn ? 'merged' : entry.status;
507
+ entry.worktree = null;
508
+ if (!entry.status || entry.status === 'in_progress') entry.status = transport === 'gh' ? 'merged' : entry.status;
447
509
  map.phases[key] = entry;
448
510
  writeGitMap(cwd, map);
449
511
 
450
512
  return {
451
- mode: mode,
513
+ mode,
452
514
  action: 'merged',
515
+ transport,
453
516
  pr,
454
517
  pr_url: prUrl,
455
518
  branch,
@@ -459,10 +522,47 @@ function finishPhase({ cwd, phase, mode = 'menu', strategy }) {
459
522
  };
460
523
  }
461
524
 
462
- warnings.push('modo desconhecido: ' + mode + ' (use solo|menu|auto)');
525
+ warnings.push('modo desconhecido: ' + mode + ' (use local|menu|auto)');
463
526
  return { mode, action: 'noop', warnings };
464
527
  }
465
528
 
529
+ // =====================================================================
530
+ // recordIssue / recordPr — gravam artefatos criados FORA (via MCP no workflow)
531
+ // =====================================================================
532
+
533
+ /** Grava no git-map a issue criada externamente (ex: via MCP). */
534
+ function recordIssue({ cwd, phase, issue, url }) {
535
+ const key = phaseKey(phase);
536
+ const map = readGitMap(cwd);
537
+ const entry = map.phases[key] || {};
538
+ if (issue != null && issue !== '') entry.issue = parseInt(issue, 10);
539
+ if (url) entry.issue_url = url;
540
+ map.phases[key] = entry;
541
+ writeGitMap(cwd, map);
542
+ return { recorded: true, phase: key, issue: entry.issue ?? null, issue_url: entry.issue_url || null };
543
+ }
544
+
545
+ /**
546
+ * Grava no git-map o PR criado externamente (ex: via MCP).
547
+ * Com `merged=true`: marca status=merged e limpa worktree+branch local.
548
+ */
549
+ function recordPr({ cwd, phase, pr, url, merged = false }) {
550
+ const warnings = [];
551
+ const key = phaseKey(phase);
552
+ const map = readGitMap(cwd);
553
+ const entry = map.phases[key] || {};
554
+ if (pr != null && pr !== '') entry.pr = parseInt(pr, 10);
555
+ if (url) entry.pr_url = url;
556
+ if (merged) {
557
+ entry.status = 'merged';
558
+ cleanupWorktreeBranch(cwd, entry.worktree, entry.branch, warnings);
559
+ entry.worktree = null;
560
+ }
561
+ map.phases[key] = entry;
562
+ writeGitMap(cwd, map);
563
+ return { recorded: true, phase: key, pr: entry.pr ?? null, pr_url: entry.pr_url || null, status: entry.status || null, merged: !!merged, warnings };
564
+ }
565
+
466
566
  // =====================================================================
467
567
  // status — mostra git-map.json
468
568
  // =====================================================================
@@ -470,12 +570,16 @@ function finishPhase({ cwd, phase, mode = 'menu', strategy }) {
470
570
  /** status({cwd}) — retorna o git-map.json atual (ou defaults se nao existir). Nunca crasha. */
471
571
  function status({ cwd }) {
472
572
  const map = readGitMap(cwd);
573
+ const hasRemote = gitHasRemote(cwd);
574
+ const ghOk = ghAvailable(cwd);
473
575
  return {
474
576
  github_native: map.github_native,
475
577
  merge_strategy: map.merge_strategy,
476
- gh_available: ghAvailable(cwd),
477
- has_remote: gitHasRemote(cwd),
578
+ gh_available: ghOk,
579
+ has_remote: hasRemote,
478
580
  remote: getRemoteUrl(cwd),
581
+ // transporte efetivo pra issue/PR: 'gh' | 'mcp' | 'none' (assumindo nao-local)
582
+ transport: hasRemote && map.github_native !== false ? (ghOk ? 'gh' : 'mcp') : 'none',
479
583
  phases: map.phases,
480
584
  };
481
585
  }
@@ -483,13 +587,18 @@ function status({ cwd }) {
483
587
  module.exports = {
484
588
  startPhase,
485
589
  finishPhase,
590
+ recordIssue,
591
+ recordPr,
486
592
  status,
487
593
  // helpers expostos para teste/reuso
488
594
  branchName,
489
595
  worktreePath,
490
596
  gitHasRemote,
491
597
  ghAvailable,
598
+ gitWantsRemote,
599
+ issueTransport,
492
600
  githubMode,
601
+ cleanupWorktreeBranch,
493
602
  readGitMap,
494
603
  writeGitMap,
495
604
  };
@@ -0,0 +1,161 @@
1
+ /**
2
+ * github.test.cjs — testes red-green do github.cjs (eixo GitHub vs interacao).
3
+ * Roda: node up/bin/lib/github.test.cjs
4
+ * Sem framework. Cria repos git temporarios. Usa remote bare local (push sem rede)
5
+ * e o seam UP_FORCE_NO_GH=1 pra simular "sem gh CLI, so MCP".
6
+ */
7
+ const assert = require('assert');
8
+ const fs = require('fs');
9
+ const os = require('os');
10
+ const path = require('path');
11
+ const { execSync } = require('child_process');
12
+ const gh = require('./github.cjs');
13
+
14
+ process.env.GIT_TERMINAL_PROMPT = '0';
15
+
16
+ function sh(cmd, cwd) { execSync(cmd, { cwd, stdio: 'pipe' }); }
17
+
18
+ /** Cria repo git temp. withRemote=true adiciona origin = bare repo local. */
19
+ function mkRepo(withRemote) {
20
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'up-gh-'));
21
+ sh('git init -q', dir);
22
+ sh('git config user.email t@t.co', dir);
23
+ sh('git config user.name t', dir);
24
+ sh('git config commit.gpgsign false', dir);
25
+ fs.writeFileSync(path.join(dir, 'README.md'), '# t\n');
26
+ sh('git add -A', dir);
27
+ sh('git commit -qm init', dir);
28
+ sh('git branch -M main', dir);
29
+ fs.mkdirSync(path.join(dir, '.plano'), { recursive: true });
30
+ let bare = null;
31
+ if (withRemote) {
32
+ bare = fs.mkdtempSync(path.join(os.tmpdir(), 'up-gh-bare-'));
33
+ sh('git init -q --bare', bare);
34
+ sh(`git remote add origin ${bare}`, dir);
35
+ }
36
+ return { dir, bare };
37
+ }
38
+
39
+ let pass = 0, fail = 0;
40
+ function t(name, fn) {
41
+ try { fn(); console.log(' ok -', name); pass++; }
42
+ catch (e) { console.error(' FAIL -', name, '\n ', e.message); fail++; }
43
+ }
44
+
45
+ // 1. local=true -> sem worktree/issue, branch atual, mode local
46
+ t('local: sem worktree/issue, branch atual', () => {
47
+ const { dir } = mkRepo(false);
48
+ const r = gh.startPhase({ cwd: dir, phase: 1, slug: 'x', local: true });
49
+ assert.strictEqual(r.worktree, null);
50
+ assert.strictEqual(r.issue, null);
51
+ assert.strictEqual(r.mode, 'local');
52
+ assert.strictEqual(r.transport, 'none');
53
+ assert.strictEqual(r.branch, 'main');
54
+ });
55
+
56
+ // 2. sem remote, nao-local -> worktree criado, git-local, sem issue
57
+ t('sem remote: worktree criado, git-local', () => {
58
+ const { dir } = mkRepo(false);
59
+ const r = gh.startPhase({ cwd: dir, phase: 2, slug: 'feat' });
60
+ assert.ok(r.worktree && fs.existsSync(r.worktree), 'worktree deve existir');
61
+ assert.strictEqual(r.mode, 'git-local');
62
+ assert.strictEqual(r.transport, 'none');
63
+ assert.strictEqual(r.issue, null);
64
+ assert.strictEqual(r.branch, 'up/fase-02-feat');
65
+ });
66
+
67
+ // 3. remote + sem gh -> github-mcp, worktree criado, pending.issue, transport mcp
68
+ t('remote sem gh: github-mcp pendente', () => {
69
+ const { dir } = mkRepo(true);
70
+ process.env.UP_FORCE_NO_GH = '1';
71
+ try {
72
+ const r = gh.startPhase({ cwd: dir, phase: 3, slug: 'mcp' });
73
+ assert.strictEqual(r.transport, 'mcp');
74
+ assert.strictEqual(r.mode, 'github-mcp');
75
+ assert.ok(r.worktree && fs.existsSync(r.worktree), 'worktree deve existir mesmo sem gh');
76
+ assert.strictEqual(r.issue, null);
77
+ assert.ok(r.pending && r.pending.issue && r.pending.issue.title, 'deve trazer pending.issue.title');
78
+ } finally { delete process.env.UP_FORCE_NO_GH; }
79
+ });
80
+
81
+ // 4. REGRESSAO do bug: solo NAO desliga github
82
+ t('solo nao desliga github (vira github-mcp, nao local)', () => {
83
+ const { dir } = mkRepo(true);
84
+ process.env.UP_FORCE_NO_GH = '1';
85
+ try {
86
+ const r = gh.startPhase({ cwd: dir, phase: 4, slug: 's', solo: true });
87
+ assert.notStrictEqual(r.mode, 'local');
88
+ assert.ok(r.worktree && fs.existsSync(r.worktree), 'solo deve criar worktree');
89
+ assert.strictEqual(r.transport, 'mcp');
90
+ } finally { delete process.env.UP_FORCE_NO_GH; }
91
+ });
92
+
93
+ // 5. recordIssue grava no git-map
94
+ t('recordIssue grava issue no git-map', () => {
95
+ const { dir } = mkRepo(true);
96
+ process.env.UP_FORCE_NO_GH = '1';
97
+ try {
98
+ gh.startPhase({ cwd: dir, phase: 5, slug: 'r' });
99
+ const r = gh.recordIssue({ cwd: dir, phase: 5, issue: 42, url: 'https://github.com/x/y/issues/42' });
100
+ assert.strictEqual(r.issue, 42);
101
+ const map = gh.readGitMap(dir);
102
+ assert.strictEqual(map.phases['5'].issue, 42);
103
+ assert.strictEqual(map.phases['5'].issue_url, 'https://github.com/x/y/issues/42');
104
+ } finally { delete process.env.UP_FORCE_NO_GH; }
105
+ });
106
+
107
+ // 6. issueTransport puro
108
+ t('issueTransport: local->none; sem remote->none', () => {
109
+ const { dir } = mkRepo(false);
110
+ assert.strictEqual(gh.issueTransport(dir, true), 'none');
111
+ assert.strictEqual(gh.issueTransport(dir, false), 'none');
112
+ });
113
+
114
+ // 7. finish mode local -> noop, done
115
+ t('finish local: noop status done', () => {
116
+ const { dir } = mkRepo(false);
117
+ gh.startPhase({ cwd: dir, phase: 7, slug: 'l', local: true });
118
+ const r = gh.finishPhase({ cwd: dir, phase: 7, mode: 'local' });
119
+ assert.strictEqual(r.status, 'done');
120
+ });
121
+
122
+ // 8. finish auto sem remote -> merge local fail-open
123
+ t('finish auto sem remote: merge local', () => {
124
+ const { dir } = mkRepo(false);
125
+ const s = gh.startPhase({ cwd: dir, phase: 8, slug: 'm' });
126
+ fs.writeFileSync(path.join(s.worktree, 'f.txt'), 'x');
127
+ sh('git add -A', s.worktree);
128
+ sh('git commit -qm w', s.worktree);
129
+ const r = gh.finishPhase({ cwd: dir, phase: 8, mode: 'auto' });
130
+ assert.strictEqual(r.status, 'merged');
131
+ });
132
+
133
+ // 9. finish auto, remote sem gh -> needs-mcp-pr + payload (push pro bare local funciona)
134
+ t('finish auto remote sem gh: sinaliza needs-mcp-pr', () => {
135
+ const { dir } = mkRepo(true);
136
+ const s = gh.startPhase({ cwd: dir, phase: 9, slug: 'p' });
137
+ fs.writeFileSync(path.join(s.worktree, 'f.txt'), 'x');
138
+ sh('git add -A', s.worktree);
139
+ sh('git commit -qm w', s.worktree);
140
+ process.env.UP_FORCE_NO_GH = '1';
141
+ try {
142
+ const r = gh.finishPhase({ cwd: dir, phase: 9, mode: 'auto' });
143
+ assert.strictEqual(r.action, 'needs-mcp-pr');
144
+ assert.ok(r.pr_payload && r.pr_payload.head, 'deve trazer pr_payload.head');
145
+ assert.strictEqual(r.pr_payload.head, 'up/fase-09-p');
146
+ } finally { delete process.env.UP_FORCE_NO_GH; }
147
+ });
148
+
149
+ // 10. recordPr --merged grava pr e limpa worktree
150
+ t('recordPr merged: grava pr, status merged, limpa worktree', () => {
151
+ const { dir } = mkRepo(true);
152
+ const s = gh.startPhase({ cwd: dir, phase: 10, slug: 'pr' });
153
+ const wt = s.worktree;
154
+ const r = gh.recordPr({ cwd: dir, phase: 10, pr: 7, url: 'https://github.com/x/y/pull/7', merged: true });
155
+ assert.strictEqual(r.pr, 7);
156
+ assert.strictEqual(r.status, 'merged');
157
+ assert.ok(!fs.existsSync(wt), 'worktree deve ter sido removida');
158
+ });
159
+
160
+ console.log(`\n${pass} passed, ${fail} failed`);
161
+ process.exit(fail ? 1 : 0);
@@ -352,11 +352,14 @@ function main() {
352
352
  if (sub === 'start-phase') {
353
353
  const phase = getFlag('--phase');
354
354
  const slug = getFlag('--slug');
355
- if (!phase) error('Usage: github start-phase --phase N --slug S [--solo]');
355
+ if (!phase) error('Usage: github start-phase --phase N --slug S [--local]');
356
+ // --local desliga GitHub (commit na branch atual). --solo NAO desliga mais
357
+ // (so afeta gate visual/merge no build.md); aceito por compat, ignorado aqui.
356
358
  const result = github.startPhase({
357
359
  cwd,
358
360
  phase,
359
361
  slug: slug || '',
362
+ local: hasFlag('--local'),
360
363
  solo: hasFlag('--solo'),
361
364
  });
362
365
  output(result, raw, JSON.stringify(result));
@@ -364,14 +367,28 @@ function main() {
364
367
  const phase = getFlag('--phase');
365
368
  const mode = getFlag('--mode') || 'menu';
366
369
  const strategy = getFlag('--strategy');
367
- if (!phase) error('Usage: github finish-phase --phase N --mode menu|auto|solo [--strategy squash|merge|rebase]');
370
+ if (!phase) error('Usage: github finish-phase --phase N --mode menu|auto|local [--strategy squash|merge|rebase]');
368
371
  const result = github.finishPhase({ cwd, phase, mode, strategy });
369
372
  output(result, raw, JSON.stringify(result));
373
+ } else if (sub === 'record-issue') {
374
+ const phase = getFlag('--phase');
375
+ const issue = getFlag('--issue');
376
+ const url = getFlag('--url');
377
+ if (!phase || !issue) error('Usage: github record-issue --phase N --issue <num> [--url <url>]');
378
+ const result = github.recordIssue({ cwd, phase, issue, url });
379
+ output(result, raw, JSON.stringify(result));
380
+ } else if (sub === 'record-pr') {
381
+ const phase = getFlag('--phase');
382
+ const pr = getFlag('--pr');
383
+ const url = getFlag('--url');
384
+ if (!phase || !pr) error('Usage: github record-pr --phase N --pr <num> [--url <url>] [--merged]');
385
+ const result = github.recordPr({ cwd, phase, pr, url, merged: hasFlag('--merged') });
386
+ output(result, raw, JSON.stringify(result));
370
387
  } else if (sub === 'status') {
371
388
  const result = github.status({ cwd });
372
389
  output(result, raw, JSON.stringify(result));
373
390
  } else {
374
- error('Unknown github subcommand. Available: start-phase, finish-phase, status');
391
+ error('Unknown github subcommand. Available: start-phase, finish-phase, record-issue, record-pr, status');
375
392
  }
376
393
  break;
377
394
  }
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: up:build
3
- description: Use quando o usuario quer EXECUTAR um projeto ja planejado (existe .plano/PLAN-READY.md). Default GitHub-nativo por fase (worktree, issue, PR, merge). Flag --solo pula a cerimonia (commit atomico na branch atual); --board espelha no Multica; --auto pula o menu de fim de fase.
4
- argument-hint: "[--solo] [--board] [--auto]"
3
+ description: Use quando o usuario quer EXECUTAR um projeto ja planejado (existe .plano/PLAN-READY.md). Default GitHub-nativo por fase (worktree, issue, PR, merge) via gh OU MCP. Flag --solo = autonomo total mantendo GitHub (sem menu, sem gate visual); --auto pula so o menu; --local pula o GitHub (commit atomico na branch atual); --board espelha no Multica.
4
+ argument-hint: "[--solo] [--auto] [--local] [--board]"
5
5
  allowed-tools:
6
6
  - Read
7
7
  - Write
@@ -40,11 +40,12 @@ Exemplo: planejou em Claude Code (`/up:plan "X"`), agora roda em OpenCode (`/up-
40
40
  <context>
41
41
  $ARGUMENTS
42
42
 
43
- **Flags:**
44
- - (sem flag) = **GitHub-nativo, o DEFAULT** (`config.github_native=true`): cada fase abre worktree + branch `up/fase-NN-slug` + issue, executa, sobe o dev server pra teste visual (se houver UI) e oferece o menu de fim de fase.
45
- - `--solo` - **ESCAPE HATCH**. Forca `github_native=false` so nesta execucao: commit atomico na branch ATUAL, zero worktree/issue/PR/board.
43
+ **Flags** (GitHub e a interacao humana sao eixos SEPARADOS; as flags so mexem na interacao):
44
+ - (sem flag) = **GitHub-nativo, o DEFAULT** (`config.github_native=true`): cada fase abre worktree + branch `up/fase-NN-slug` + issue (via `gh` OU MCP do GitHub), executa, sobe o dev server pra teste visual (se houver UI) e oferece o menu de fim de fase.
45
+ - `--solo` - **autonomo total**. NAO desliga o GitHub: cria worktree/branch/issue/PR e auto-mergeia, SEM menu e SEM gate visual. Pra loop/headless.
46
+ - `--auto` - pula so o menu de fim de fase (merge automatico). Mantem GitHub. O teste visual ainda roda se `require_visual_test=true`.
47
+ - `--local` - **ESCAPE HATCH sem GitHub**. Commit atomico na branch ATUAL, zero worktree/issue/PR/board. (Mesmo que `/up:rapido`.) Unico jeito de pular o GitHub.
46
48
  - `--board` - espelha o progresso das fases no Multica (board opt-in, batched, fail-open).
47
- - `--auto` - pula o menu de fim de fase (merge automatico). O teste visual ainda roda se `require_visual_test=true`.
48
49
 
49
50
  O comando le o resto de `.plano/PLAN-READY.md`.
50
51
  </context>
@@ -75,23 +76,23 @@ Se algo falta: alertar e oferecer planejamento local OU abortar.
75
76
 
76
77
  **Sem model routing:** O runtime decide o modelo.
77
78
 
78
- **Parsear flags primeiro:** extrair `--solo`/`--board`/`--auto`. Sem flag = GitHub-nativo (default).
79
+ **Parsear flags primeiro:** extrair `--solo`/`--auto`/`--local`/`--board`. Sem flag = GitHub-nativo (default).
79
80
 
80
81
  **Modo de orquestracao (resolver antes de comecar):**
81
- - DEFAULT (sem `--solo`): GitHub-nativo. Por fase: `up-tools.cjs github start-phase` (worktree + branch + issue, fail-open) -> executa as waves -> gate -> teste visual pre-merge (se UI) -> `github finish-phase` (PR -> merge squash -> cleanup) conforme o menu. `--board` adiciona `multica init/sync` (batched). `--auto` pula o menu.
82
- - `--solo`: executa local, commits atomicos na branch atual. Sem worktree/issue/PR/board.
82
+ - DEFAULT (sem `--local`): GitHub-nativo. Por fase: `up-tools.cjs github start-phase` (worktree + branch + issue via gh OU MCP, fail-open) -> executa as waves -> gate -> teste visual pre-merge (se UI) -> `github finish-phase` (PR -> merge squash -> cleanup) conforme o menu. `--auto` pula o menu (mantem GitHub). `--solo` pula menu E gate visual (autonomo total, mantem GitHub). `--board` adiciona `multica init/sync` (batched).
83
+ - `--local`: executa local, commits atomicos na branch atual. Sem worktree/issue/PR/board.
83
84
 
84
85
  **Execute the build workflow from @~/.claude/up/workflows/build.md end-to-end.**
85
86
 
86
87
  Pipeline por fase:
87
88
  1. Validacao light
88
- 2. `github start-phase` (worktree + issue; pulado em `--solo`)
89
+ 2. `github start-phase` (worktree + issue via gh OU MCP; pulado so em `--local`)
89
90
  3. Execucao em WAVES PARALELAS: os planos da mesma wave rodam ao mesmo tempo (varios `up-executor`); waves em sequencia. Re-plan local se algum plano ficar inviavel.
90
91
  4. up-verificador emite VERIFICATION.md com evidencia fresca por tipo (TDD-por-tipo: red-green / visual / smoke)
91
92
  5. **GATE deterministico** `approvals.log` (governance.md, gate-only; exige `evidence=`)
92
93
  6. up-revisor (two-stage: spec-compliance cetico + code-quality) alimenta o gate
93
94
  7. **Teste visual pre-merge** (fase de UI e `require_visual_test=true`): sobe o dev server na worktree, "testar primeiro ou pode mergear?", loop de ajuste ate aprovar
94
- 8. **Menu de fim de fase** (GitHub-nativo, fora `--auto`): merge local / abrir PR / deixa a branch / descarta. Em `--solo`, ja committou na branch atual
95
+ 8. **Menu de fim de fase** (GitHub-nativo, fora `--auto`/`--solo`): merge local / abrir PR / deixa a branch / descarta. Autonomo (`--solo`/`--auto`) auto-mergeia sem menu. Em `--local`, ja committou na branch atual
95
96
  9. Reassessment do roadmap antes da proxima fase
96
97
 
97
98
  **Re-plan local permitido (max 1 round por fase):**
@@ -102,9 +103,9 @@ Se ficar inviavel, o up-planejador LOCAL refaz a fase. Registrar em `.plano/gove
102
103
 
103
104
  <success_criteria>
104
105
  - [ ] PLAN-READY.md validado
105
- - [ ] Flags parseadas (default = GitHub-nativo; --solo = escape)
106
+ - [ ] Flags parseadas (default = GitHub-nativo; --solo/--auto = autonomia mantendo GitHub; --local = escape sem GitHub)
106
107
  - [ ] Build rodou todas as fases; planos da mesma wave em paralelo
107
108
  - [ ] GATE approvals.log respeitado (deterministico, com evidence= por tipo)
108
- - [ ] Teste visual pre-merge em fase de UI (require_visual_test)
109
- - [ ] Fechamento por fase: menu (GitHub-nativo) ou commit na branch (--solo)
109
+ - [ ] Teste visual pre-merge em fase de UI (require_visual_test; pulado em --solo)
110
+ - [ ] Fechamento por fase: menu (GitHub-nativo), auto-merge (--solo/--auto) ou commit na branch (--local)
110
111
  </success_criteria>
@@ -38,6 +38,47 @@ Classifique a tarefa com o `classify-task` do `up-tools.cjs` (tiers: `simple` /
38
38
  | **Pequena** (1 subsistema, 1 escolha de design) | 1 pergunta via AskUserQuestion (a decisao-chave) + design em 3 frases. Aprova e segue. |
39
39
  | **Media / Grande** (multi-subsistema, toca schema/API/auth) | Brainstorm full. |
40
40
 
41
+ O `classify-task` define o PISO (minimo garantido). O usuario sempre pode SUBIR ou DESCER manualmente (override abaixo). Nunca diminua a profundidade por conta propria; so o usuario rebaixa.
42
+
43
+ ## Override de profundidade (controle do usuario)
44
+
45
+ O tier automatico e so o default. O usuario manda na profundidade:
46
+
47
+ | Sinal do usuario | Efeito |
48
+ |------------------|--------|
49
+ | Palavras "a fundo", "detalhado", "explorar", "pensar junto", "completo", "caprichado" OU flag `--deep` | Sobe pra **full** (ou exploracao, se for ideia crua), ignora o score baixo. |
50
+ | Palavras "rapido", "simples", "so faz", "sem perguntas" OU flag `--quick` | Desce pra **trivial** (0 perguntas), mesmo que o score ache complexo. O HARD-GATE continua: anuncia antes de agir. |
51
+ | Nada declarado | Usa o tier do `classify-task` (piso automatico). |
52
+
53
+ Pressa nunca remove o gate; muda so quantas perguntas.
54
+
55
+ ## Modo exploracao (ideia crua, acima do full)
56
+
57
+ Quando o usuario tem so uma SEMENTE e quer DESCOBRIR o que e (nao validar um design ja pronto). Gatilhos: "tenho uma ideia", "to pensando em", "e se", "me ajuda a pensar", "queria explorar", `--deep` numa ideia vaga.
58
+
59
+ Diferenca do full: o full valida um design que o usuario ja tem na cabeca; a exploracao ABRE o espaco antes de fechar.
60
+
61
+ 1. **Nao pule pra solucao.** Primeiro entenda o PORQUE: que problema/desejo move a ideia, pra quem, por que agora.
62
+ 2. **Abra alternativas radicais.** Ofereca 3-5 direcoes bem diferentes (nao variacoes da mesma), incluindo uma obvia, uma ousada e uma "e se fizesse o oposto".
63
+ 3. **Provoque com "e se".** Tensione premissas: "e se nao precisasse de X?", "e se o publico fosse outro?", "qual a versao 10x menor que ja entrega valor?".
64
+ 4. **Uma pergunta por vez**, multipla escolha quando der. Vai estreitando do amplo pro especifico.
65
+ 5. **Destile** a ideia num paragrafo claro: o que e, pra quem, por que, o diferencial. Confirme com o usuario.
66
+ 6. So ENTAO transicione pro design (full) ou direto pro `BRIEFING.md`, conforme o tamanho do que emergiu.
67
+
68
+ A exploracao termina numa ideia destilada e aprovada, que vira BRIEFING. Continua valendo o estado terminal: codigo so depois de `/up:plan`.
69
+
70
+ ## Trilha NAO-codigo (documento, relatorio, analise, conteudo, plano de negocio)
71
+
72
+ Nem todo trabalho e software. Se a tarefa e produzir um ARTEFATO que nao e codigo (documento, proposta, relatorio, analise, roteiro, estrategia, pesquisa), o fluxo muda:
73
+
74
+ - **Brainstorm igual** (escala por tier + override + modo exploracao valem). A diferenca e o que vem depois.
75
+ - **NAO ha `/up:plan` -> `/up:build` -> worktree/PR.** Sem fases de software, sem TDD-por-tipo, sem GitHub-nativo. Isso e cerimonia de codigo.
76
+ - **Apos o design/escopo aprovado:** produza o artefato direto (escreva o documento/analise). Para conteudo do Jonathan (carrossel, aula, post), use as skills dedicadas (`carrossel-*`, `aula-generator`, etc) quando aplicaveis.
77
+ - **Verificacao por adequacao, nao por teste:** a prova e "o artefato existe, cobre o que foi pedido, sem placeholder/TBD, e bate com o briefing". Aplica a Lei de Ferro adaptada: nao diga "pronto" sem reler o artefato e conferir contra o escopo combinado.
78
+ - **Persistencia leve:** salve o artefato no local certo (vault, pasta do projeto) e registre 1 linha no STATE.md se houver `.plano/`. Sem roadmap.
79
+
80
+ Como detectar: pedido fala em "documento, proposta, relatorio, analise, texto, roteiro, estrategia, plano, pesquisa" e NAO em codigo/app/feature/sistema. Na duvida, pergunte: "isso e pra virar codigo ou e um documento/analise?".
81
+
41
82
  ## Brainstorm full (media/grande)
42
83
 
43
84
  1. **Explore o contexto** (arquivos, docs, commits recentes).
@@ -15,13 +15,17 @@ O UP ativa por contexto. Nao precisa decorar comando: a skill certa dispara pelo
15
15
 
16
16
  **Passo ZERO de todo trabalho:** invocar `up-brainstorm`. Profundidade escala por tamanho (trivial = 0 perguntas; grande = design por secao). Nada de implementar antes de design aprovado.
17
17
 
18
- **Fluxo obrigatorio para PROJETO ou FEATURE (nao-trivial):** brainstorm -> `/up:plan` (gera `.plano/PLAN-READY.md`) -> `/up:build` (executa). Depois do design aprovado voce NAO comeca a codar nem a criar fundacao/scaffold direto: registra os artefatos (BRIEFING/PROJECT) e PARA, entregando o handoff para `/up:plan`. Planejar e um passo separado e obrigatorio, nao opcional. Pular o plan so e permitido em tarefa pontual declarada via `/up:rapido`. Se nao tem `.plano/PLAN-READY.md`, voce ainda nao pode buildar.
18
+ **Fluxo obrigatorio para PROJETO ou FEATURE de codigo (nao-trivial):** brainstorm -> `/up:plan` (gera `.plano/PLAN-READY.md`) -> `/up:build` (executa). Depois do design aprovado voce NAO comeca a codar nem a criar fundacao/scaffold direto: registra os artefatos (BRIEFING/PROJECT) e PARA, entregando o handoff para `/up:plan`. Planejar e um passo separado e obrigatorio, nao opcional. Pular o plan so e permitido em tarefa pontual declarada via `/up:rapido`. Se nao tem `.plano/PLAN-READY.md`, voce ainda nao pode buildar.
19
+
20
+ **Tarefa NAO-codigo** (documento, relatorio, analise, conteudo, estrategia): brainstorma igual, mas NAO passa por `/up:plan`/`/up:build`/worktree. Apos o escopo aprovado, produz o artefato direto e verifica por adequacao (cobre o pedido, sem TBD), nao por teste. Detalhe na skill `up-brainstorm`.
21
+
22
+ **Profundidade sob controle do usuario:** o tier automatico e so o piso. "A fundo/detalhado/explorar" ou `--deep` sobe pra brainstorm completo (ou modo exploracao pra ideia crua); "rapido/simples" ou `--quick` desce pra 0 perguntas. O usuario manda na profundidade; voce nunca a reduz sozinho.
19
23
 
20
24
  **Lei de Ferro:** evidencia fresca antes de afirmar pronto. Nunca diga "Pronto" ou "Perfeito" sem o comando de prova rodado NESTA mensagem. Detalhe em `up-verificar-antes-de-concluir`.
21
25
 
22
26
  **Prova por tipo:** logica/bugfix = teste red-green; UI/CSS = captura visual; glue/integracao = smoke-test. Veja `up-tdd`.
23
27
 
24
- **GitHub-nativo e o padrao em repo colaborativo** (worktree -> issue -> PR -> merge), menu de 4 opcoes no fim. Solo usa `--solo` (commit local) ou `/up:rapido` como escape, sem cerimonia.
28
+ **GitHub-nativo e o padrao** (worktree -> issue -> PR -> merge), via `gh` OU MCP do GitHub, menu de 4 opcoes no fim. `--auto` pula o menu; `--solo` e autonomo total (mantem GitHub, sem menu nem gate visual). Pra pular o GitHub de propósito (commit local puro): `--local` no build ou `/up:rapido`. Atencao: `--solo` NAO desliga mais o GitHub.
25
29
 
26
30
  **Persistencia:** tudo vive em `.plano/` e sobrevive a `/clear`. Leia `.plano/STATE.md` antes de assumir contexto perdido.
27
31
 
@@ -52,27 +52,44 @@ O LLM tende a colapsar passos (mesmo agente executa + verifica). Isso e PROIBIDO
52
52
  e um `Agent()` SEPARADO. O enforcement e o GATE deterministico do `approvals.log`
53
53
  (ver `@~/.claude/up/workflows/governance.md`), nao uma piramide de supervisores.
54
54
 
55
- **GitHub-nativo e o DEFAULT (v2):**
56
- - **PADRAO (sem flag):** cada fase abre uma worktree + branch `up/fase-NN-slug` e (se houver `gh` + remote)
57
- uma issue do GitHub; no fim da fase, se a fase tem UI, **sobe o dev server e PEDE aprovacao visual antes de
58
- mergear** (3.8.0), depois fecha (merge local / PR / deixa / descarta). Isto e o caminho quente.
59
- Controlado por `config.github_native=true` (default).
60
- - `--solo`: ESCAPE HATCH sem cerimonia. Forca `github_native=false` SO nesta execucao. Commit atomico na
61
- branch ATUAL. Zero worktree, zero issue, zero PR, zero rede. (Mesmo comportamento de `/up:rapido`.)
55
+ **GitHub-nativo e o DEFAULT (v2): DOIS EIXOS SEPARADOS:**
56
+
57
+ O GitHub (worktree/branch/issue/PR) e a INTERACAO humana (menu/gate visual/merge) sao eixos
58
+ independentes. As flags mexem so na interacao; o GitHub fica ligado sempre que da.
59
+
60
+ - **Eixo GitHub:** LIGADO sempre que ha remote E (`gh` autenticado OU MCP do GitHub conectado).
61
+ Worktree+branch sao git local e SEMPRE acontecem (offline-ok). Issue/PR usam o transporte:
62
+ `gh` (CLI cria direto) ou `mcp` (o workflow cria via `mcp__...github__*` e grava com
63
+ `record-issue`/`record-pr`). DESLIGA so com `--local` ou `config.github_native=false`.
64
+ - `--local`: ESCAPE HATCH sem GitHub. Commit atomico na branch ATUAL. Zero worktree/issue/PR/rede.
65
+ (Mesmo comportamento de `/up:rapido`.) **Este e o unico jeito de pular o GitHub no build.**
66
+ - `--solo`: NAO desliga o GitHub. Solo = autonomo total: GitHub completo (branch/worktree/issue/PR
67
+ + auto-merge), SEM menu e SEM gate visual. Pra loop/headless onde ninguem aprova nada.
68
+ - `--auto`: pula o MENU de fechamento (auto-merge), MAS o gate visual (3.8.0) ainda roda se
69
+ `require_visual_test=true`. Mantem GitHub ligado.
62
70
  - **Teste visual antes do merge (`require_visual_test`, default true):** fase de UI sobe dev server e exige
63
- o dono aprovar na tela ANTES do merge (3.8.0). Projeto em PRODUCAO: deixe ligado (nada sobe sem voce ver).
64
- - `--auto`: pula o MENU de fechamento. Apos o GATE aprovar, `finish-phase --mode auto` (PR -> merge squash ->
65
- cleanup). MAS o teste visual (3.8.0) ainda roda se `require_visual_test=true` (so `false` deixa o --auto
66
- mergear UI sem aprovacao). So faz sentido com github_native ligado.
71
+ o dono aprovar na tela ANTES do merge (3.8.0). `--solo` pula sempre; `--auto` so pula com `require_visual_test=false`.
72
+ Projeto em PRODUCAO: deixe ligado e nao use `--solo` (nada sobe sem voce ver).
67
73
  - `--board`: espelha status no Multica (espelho de board OPT-IN, BATCHED no fim da onda/fase). NAO ha
68
74
  stream ao vivo no fluxo local: o board mostra so o status (`todo -> in_progress -> in_review -> done /
69
75
  blocked`), nunca cada tool_use. Chamadas via `up-tools.cjs multica {init|sync|board}` (que usa
70
76
  `multica.cjs`, deteccao `uname -s` Mac->`ssh server-ecoup`, FAIL-OPEN: se `multica` indisponivel, avisa
71
77
  e segue sem board, nunca crasha). So roda quando `--board` ligado.
72
78
 
73
- **FAIL-OPEN universal:** `start-phase`/`finish-phase` detectam `gh` + remote. Faltando qualquer um, degradam
74
- para git local (worktree local + merge local; issue/PR = null) com aviso, NUNCA crasham. `git worktree` e
75
- sempre local e funciona offline. Com `--solo` nao ha nem worktree (commit direto na branch atual).
79
+ **Resumo das flags** (GitHub = artefatos; demais = interacao):
80
+
81
+ | Flag | GitHub | Menu fim | Gate visual | Merge |
82
+ |------|:---:|:---:|:---:|---|
83
+ | (nenhum) | SIM | SIM (4 opcoes) | SIM | conforme menu |
84
+ | `--auto` | SIM | NAO | SIM (a menos require_visual_test=false) | auto squash |
85
+ | `--solo` | SIM | NAO | NAO (pula sempre) | auto squash |
86
+ | `--local` | NAO | NAO | NAO | commit na branch atual |
87
+
88
+ **FAIL-OPEN universal:** `start-phase`/`finish-phase` detectam remote + transporte (`gh`/`mcp`/`none`).
89
+ Sem remote, degradam para git local (worktree local + merge local; issue/PR = null) com aviso, NUNCA
90
+ crasham. `git worktree` e sempre local e funciona offline. Sem `gh` mas com MCP, o transporte e `mcp`:
91
+ worktree/branch/push acontecem no `.cjs` e o workflow cria issue/PR via MCP. Com `--local` nao ha nem
92
+ worktree (commit direto na branch atual).
76
93
 
77
94
  **Onde o estado vive:** `git-map.json` e canonico no working dir PRINCIPAL (`.plano/git-map.json`). O `.plano/`
78
95
  de cada fase viaja na branch da fase (worktree) e volta pra main no merge. STATE.md permanece a fonte humana
@@ -175,25 +192,31 @@ Projeto planejado em {runtime}.
175
192
  Resumo: {N} fases, {M} planos. Planning confidence: {X}/100.
176
193
  Pendencias conhecidas: {de PENDING.md}.
177
194
 
178
- Modo git: {GITHUB_MODE} (GitHub-nativo e o default; --solo desliga; --auto pula o menu)
195
+ Modo git: {GITHUB_MODE} (GitHub-nativo e o default; --local desliga; --solo/--auto sao autonomia, nao desligam)
179
196
  ```
180
197
 
181
- Resolver `GITHUB_MODE` antes do banner:
198
+ Resolver `GITHUB_MODE` antes do banner. `$LOCAL`/`$SOLO`/`$AUTO`/`$BOARD_FLAG` vem das flags da
199
+ invocacao. **`--solo` NAO desliga o GitHub** (so `--local` desliga):
182
200
 
183
201
  ```bash
184
- # --solo forca github_native=false SO nesta execucao
185
- if [ "$SOLO" = "true" ]; then
202
+ # So --local (ou config github_native=false) desliga o GitHub.
203
+ if [ "$LOCAL" = "true" ]; then
186
204
  GITHUB_NATIVE=false
187
205
  else
188
206
  GITHUB_NATIVE=$(node "$HOME/.claude/up/bin/up-tools.cjs" config get github_native --raw 2>/dev/null)
189
207
  [ -z "$GITHUB_NATIVE" ] && GITHUB_NATIVE=true # default TRUE
190
208
  fi
191
209
 
210
+ # --solo e --auto sao AUTONOMIA (sem menu). --solo tambem pula o gate visual.
211
+ AUTONOMO=false
212
+ { [ "$SOLO" = "true" ] || [ "$AUTO" = "true" ]; } && AUTONOMO=true
213
+
192
214
  if [ "$GITHUB_NATIVE" = "true" ]; then
193
215
  GITHUB_MODE="GitHub-nativo (worktree + issue + PR/menu por fase)"
194
- [ "$AUTO" = "true" ] && GITHUB_MODE="GitHub-nativo --auto (PR + merge squash automatico)"
216
+ [ "$AUTO" = "true" ] && GITHUB_MODE="GitHub-nativo --auto (PR + merge squash; gate visual ainda roda)"
217
+ [ "$SOLO" = "true" ] && GITHUB_MODE="GitHub-nativo --solo (autonomo total: PR + merge, SEM gate visual)"
195
218
  else
196
- GITHUB_MODE="--solo (commit atomico na branch atual, sem worktree/issue/PR)"
219
+ GITHUB_MODE="--local (commit atomico na branch atual, sem worktree/issue/PR)"
197
220
  fi
198
221
 
199
222
  # --board liga o espelho Multica (OPT-IN). So tem efeito se passado explicitamente.
@@ -231,27 +254,37 @@ Para cada fase em ROADMAP.md (em ordem):
231
254
 
232
255
  ### 3.0 Abrir a fase (GitHub-nativo - DEFAULT)
233
256
 
234
- A menos que `--solo` (ou `github_native=false`), abrir worktree + branch + issue ANTES de executar a fase.
257
+ A menos que `--local` (ou `github_native=false`), abrir worktree + branch + issue ANTES de executar a fase.
235
258
 
236
259
  ```bash
237
260
  PHASE_SLUG=$(node "$HOME/.claude/up/bin/up-tools.cjs" slug "{phase_name}" --raw)
238
261
 
239
262
  if [ "$GITHUB_NATIVE" = "true" ]; then
240
- # Cria worktree + branch up/fase-NN-slug; se gh+remote, cria issue. Escreve .plano/git-map.json.
241
- # Fail-open: sem gh/remote -> worktree local, issue=null, aviso (nunca crasha).
263
+ # Cria worktree + branch up/fase-NN-slug SEMPRE (git local). Issue por transporte:
264
+ # gh -> issue criada aqui; mcp -> retorna pending.issue (workflow cria via MCP); none -> sem remote.
242
265
  START=$(node "$HOME/.claude/up/bin/up-tools.cjs" github start-phase \
243
266
  --phase {phase_number} --slug "$PHASE_SLUG" --raw)
244
267
  if [[ "$START" == @file:* ]]; then START=$(cat "${START#@file:}"); fi
245
- WORKTREE=$(echo "$START" | grep -oE '"worktree"[^,}]*' | sed 's/.*: *"//;s/"//')
246
- BRANCH=$(echo "$START" | grep -oE '"branch"[^,}]*' | sed 's/.*: *"//;s/"//')
247
- ISSUE=$(echo "$START" | grep -oE '"issue"[^,}]*' | sed 's/.*: *//')
248
- echo "Fase {phase_number}: branch=$BRANCH worktree=$WORKTREE issue=${ISSUE:-null}"
268
+ WORKTREE=$(echo "$START" | grep -oE '"worktree"[^,}]*' | sed 's/.*: *"//;s/"//')
269
+ BRANCH=$(echo "$START" | grep -oE '"branch"[^,}]*' | sed 's/.*: *"//;s/"//')
270
+ ISSUE=$(echo "$START" | grep -oE '"issue"[^,}]*' | sed 's/.*: *//')
271
+ TRANSPORT=$(echo "$START" | grep -oE '"transport"[^,}]*' | sed 's/.*: *"//;s/"//')
272
+ echo "Fase {phase_number}: branch=$BRANCH worktree=$WORKTREE issue=${ISSUE:-null} transport=$TRANSPORT"
249
273
  else
250
- # --solo: sem worktree/issue. Trabalho acontece na branch atual.
251
- WORKTREE="$(pwd)"; BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)"; ISSUE=""
274
+ # --local: sem worktree/issue. Trabalho acontece na branch atual.
275
+ WORKTREE="$(pwd)"; BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)"; ISSUE=""; TRANSPORT=none
252
276
  fi
253
277
  ```
254
278
 
279
+ **Issue via MCP (quando `transport=mcp`):** sem `gh` mas com remote, `start-phase` ja criou worktree+branch
280
+ e devolveu `pending.issue {title, body}` no JSON. Se voce (orquestrador) TEM as tools `mcp__...github__*`,
281
+ crie a issue agora e grave o numero de volta:
282
+
283
+ > Ao detectar `transport=mcp` no `$START`: extraia `pending.issue.title`/`pending.issue.body`, chame
284
+ > `mcp__plugin_github_github__issue_write` (create) no repo do remote, pegue o numero retornado e rode
285
+ > `node "$HOME/.claude/up/bin/up-tools.cjs" github record-issue --phase {phase_number} --issue <num> --url <url>`.
286
+ > Se voce NAO tem MCP do GitHub: siga sem issue (worktree/branch ja existem; PR pode degradar pra merge local no fim).
287
+
255
288
  **Entrar na worktree (so se github_native):** o trabalho da fase (executor, commits) acontece
256
289
  DENTRO de `$WORKTREE`. Preferir a tool nativa do harness **EnterWorktree** apontando para `$WORKTREE`; se
257
290
  indisponivel, a worktree ja foi criada por `start-phase` (basta usar `--cwd "$WORKTREE"` nos comandos
@@ -259,7 +292,7 @@ indisponivel, a worktree ja foi criada por `start-phase` (basta usar `--cwd "$WO
259
292
  `git-map.json` permanece canonico no working dir principal. Ao terminar a fase usar **ExitWorktree** (ou
260
293
  voltar `cd` para o repo principal) antes de atualizar `git-map.json` na main.
261
294
 
262
- > Em `--solo`, IGNORAR EnterWorktree/ExitWorktree: tudo na branch atual.
295
+ > Em `--local`, IGNORAR EnterWorktree/ExitWorktree: tudo na branch atual. Em `--solo` HA worktree (solo nao desliga GitHub).
263
296
 
264
297
  **Multica: marcar a fase em execucao (so se `--board`, 1 chamada na ENTRADA da fase).**
265
298
  Uma transicao por fase (nao por microtransicao): status `in_progress` + metadata `gh_issue`/`branch`.
@@ -281,7 +314,7 @@ NAO existe mais "1 plano por fase" (era a regressao do `head -1`): descobrimos T
281
314
  agrupamento por wave do disco.
282
315
 
283
316
  Os planos vivem DENTRO de `$WORKTREE` (a `.plano/` da fase viaja na branch da fase). Por isso TODA chamada
284
- de descoberta usa `--cwd "$WORKTREE"` (em `--solo`, `$WORKTREE` = repo principal, entao funciona igual).
317
+ de descoberta usa `--cwd "$WORKTREE"` (em `--local`, `$WORKTREE` = repo principal, entao funciona igual).
285
318
 
286
319
  ```bash
287
320
  # (a) Metadados da fase + flag de paralelizacao (config). Trata @file: (saida > 50KB).
@@ -635,7 +668,7 @@ fi
635
668
 
636
669
  So apos o GATE aprovar (APPROVE ou forced approval registrado).
637
670
 
638
- **Caso `--solo` (github_native=false):** nada a fazer aqui. Tudo ja foi committado atomicamente na branch
671
+ **Caso `--local` (github_native=false):** nada a fazer aqui. Tudo ja foi committado atomicamente na branch
639
672
  atual. Seguir direto para 3.9. (Sem worktree, sem teste-visual-gate, sem merge.)
640
673
 
641
674
  #### 3.8.0 Checkpoint de teste visual (PRE-MERGE) - so GitHub-nativo
@@ -649,9 +682,10 @@ REQUIRE_VISUAL=$(node "$HOME/.claude/up/bin/up-tools.cjs" config get require_vis
649
682
  HAS_DEV=$(node -e "try{const s=require('./package.json').scripts||{};process.stdout.write((s.dev||s.start||s.serve)?'1':'')}catch(e){}" 2>/dev/null)
650
683
  ```
651
684
 
652
- **Aplica o gate quando:** (HAS_UI ou HAS_DEV) E NAO (`--auto` E REQUIRE_VISUAL=false).
653
- Ou seja: por padrao SEMPRE pede aprovacao visual antes do merge em fase de UI. Para PULAR (CI/yolo):
654
- `--auto` + setar `require_visual_test=false` no config. Para projeto em PRODUCAO: deixe o default (true).
685
+ **Aplica o gate quando:** (HAS_UI ou HAS_DEV) E NAO `--solo` E NAO (`--auto` E REQUIRE_VISUAL=false).
686
+ Ou seja: por padrao SEMPRE pede aprovacao visual antes do merge em fase de UI. `--solo` pula SEMPRE
687
+ (autonomo total). Pra pular sem solo (CI/yolo): `--auto` + `require_visual_test=false`. Projeto em
688
+ PRODUCAO: deixe o default (true) e nao use `--solo`.
655
689
 
656
690
  Se aplica:
657
691
 
@@ -706,32 +740,44 @@ pkill -f "npm run dev" 2>/dev/null || true; pkill -f "npm start" 2>/dev/null ||
706
740
  A escolha do checkpoint define a acao de fechamento (3.8.1): "Pode mergear"/"Aprovado" -> MERGE;
707
741
  "Deixa a branch" -> nao mergeia; "Descarta" -> remove sem merge.
708
742
 
709
- **Fase SEM UI** (infra/schema/backend puro) ou `--auto` com `require_visual_test=false`: pula 3.8.0.
743
+ **Fase SEM UI** (infra/schema/backend puro), `--solo`, ou `--auto` com `require_visual_test=false`: pula 3.8.0.
710
744
  Nesse caso, GitHub-nativo interativo ainda apresenta o mesmo AskUserQuestion de 4 opcoes (sem o passo do
711
- dev server) pra o dono decidir merge/PR/deixa/descarta. `--auto` com fase sem UI fecha direto (sem perguntar).
745
+ dev server) pra o dono decidir merge/PR/deixa/descarta. **Autonomo (`--solo`/`--auto`)** fecha direto sem
746
+ menu: `ESCOLHA=mergear`.
712
747
 
713
748
  #### 3.8.1 Merge e avancar
714
749
 
715
750
  Sair da worktree (**ExitWorktree** ou `cd` de volta ao repo principal) para que `finish-phase` opere e
716
- atualize `git-map.json` na main. Mapear a escolha de 3.8.0 para `finish-phase`
717
- (`--mode menu|auto|solo`):
751
+ atualize `git-map.json` na main. **Autonomo (`--solo`/`--auto`):** sem menu, `ESCOLHA=mergear`.
752
+ Mapear a escolha de 3.8.0 para `finish-phase` (`--mode menu|auto|local`):
718
753
 
719
754
  ```bash
755
+ [ "$AUTONOMO" = "true" ] && [ -z "$ESCOLHA" ] && ESCOLHA=mergear
720
756
  case "$ESCOLHA" in
721
- # Pode mergear / Aprovado: finish-phase --mode auto faz gh pr create (Closes #N) -> merge squash -> cleanup.
722
- # FAIL-OPEN: sem gh/remote, --mode auto degrada para merge LOCAL da branch na base + cleanup (issue/PR=null).
723
- mergear|aprovado) node "$HOME/.claude/up/bin/up-tools.cjs" github finish-phase --phase {phase_number} --mode auto --strategy squash ;;
757
+ # Pode mergear / Aprovado: finish-phase --mode auto. Transporte gh: pr create (Closes #N) -> merge -> cleanup.
758
+ # Transporte mcp (sem gh): faz push e retorna action 'needs-mcp-pr' (ver abaixo). none: merge LOCAL + cleanup.
759
+ mergear|aprovado) FIN=$(node "$HOME/.claude/up/bin/up-tools.cjs" github finish-phase --phase {phase_number} --mode auto --strategy squash --raw) ;;
724
760
  # Deixa a branch: nao mergeia; worktree+branch vivos. menu so atualiza git-map.json (status=in_review).
725
- deixa) node "$HOME/.claude/up/bin/up-tools.cjs" github finish-phase --phase {phase_number} --mode menu ;;
761
+ deixa) FIN=$(node "$HOME/.claude/up/bin/up-tools.cjs" github finish-phase --phase {phase_number} --mode menu --raw) ;;
726
762
  # Descarta: orquestrador remove worktree + branch (sem merge); reflete em git-map.json via status=cancelled.
727
- descarta) echo "Descartando fase {phase_number}: remover worktree + branch (sem merge)." ;;
763
+ descarta) echo "Descartando fase {phase_number}: remover worktree + branch (sem merge)." ; FIN="" ;;
728
764
  esac
729
765
  ```
730
766
 
731
- `finish-phase --mode auto` faz: gh pr create (body com `Closes #<issue>` quando ha issue) -> merge (squash
732
- default, ou `--strategy merge|rebase`) -> cleanup worktree+branch, e atualiza `git-map.json`. FAIL-OPEN: sem
733
- gh/remote, faz merge LOCAL da branch da fase na base e remove a worktree (issue/PR = null). `--mode solo` nao
734
- faz nada (usado quando ja esta tudo committado na branch atual); em `--solo` o fluxo nem chega aqui.
767
+ **PR via MCP (quando `FIN` traz `"action":"needs-mcp-pr"`):** sem `gh`, o `finish-phase` ja deu `git push` da
768
+ branch e devolveu `pr_payload {base, head, title, body, issue, strategy}`. Se voce TEM `mcp__...github__*`:
769
+
770
+ > Chame `mcp__plugin_github_github__create_pull_request` (base/head/title/body do `pr_payload`), depois
771
+ > `mcp__plugin_github_github__merge_pull_request` (merge_method=squash). Pegue o numero do PR e rode:
772
+ > `node "$HOME/.claude/up/bin/up-tools.cjs" github record-pr --phase {phase_number} --pr <num> --url <url> --merged`
773
+ > (`--merged` grava o PR, marca a fase merged e limpa worktree+branch). Se NAO tem MCP: rode
774
+ > `github finish-phase --phase {phase_number} --mode local` como degradacao (a branch ja foi pushada; faca o
775
+ > merge no GitHub manualmente depois) OU avise o dono.
776
+
777
+ `finish-phase --mode auto` (transporte gh) faz: gh pr create (body com `Closes #<issue>`) -> merge (squash
778
+ default, ou `--strategy merge|rebase`) -> cleanup worktree+branch, atualiza `git-map.json`. Sem remote (none),
779
+ faz merge LOCAL da branch na base e remove a worktree (issue/PR=null). `--mode local` nao faz nada (usado em
780
+ `--local`, ja committado na branch atual); em `--local` o fluxo nem chega aqui (tratado em 3.8).
735
781
 
736
782
  **Multica: sync BATCHED no FIM da fase/onda (so se `--board`).**
737
783
  Uma unica chamada que reflete TODAS as transicoes acumuladas da fase de uma vez (nao por microtransicao):
@@ -759,7 +805,7 @@ if [ "$BOARD" = "true" ]; then
759
805
  fi
760
806
  ```
761
807
 
762
- > Em `--solo` o fluxo nem chega aqui (sem `--board`): nenhuma chamada Multica.
808
+ > Em `--local` o fluxo nem chega aqui (sem `--board`): nenhuma chamada Multica.
763
809
 
764
810
  ### 3.9 Reassessment de roadmap (pos-fase, inline, ~30s)
765
811
 
@@ -845,8 +891,9 @@ final_confidence: [do up-revisor de delivery]
845
891
  - [ ] E2E + DCRV rodaram por fase (delegado a dcrv.md)
846
892
  - [ ] up-revisor emitiu veredito por fase e LOGOU em approvals.log COM campo evidence=<tipo>:<resultado>
847
893
  - [ ] GATE de fase deterministico passou (APPROVE + evidence do tipo certo, ou forced approval com debito)
848
- - [ ] GitHub-nativo (default): worktree+branch+issue por fase via `github start-phase`; menu 4 opcoes /
849
- `github finish-phase` no fim. `--solo` degrada para commit na branch atual (sem worktree/issue/PR)
894
+ - [ ] GitHub-nativo (default): worktree+branch+issue por fase via `github start-phase` (transporte gh OU
895
+ MCP); menu 4 opcoes / `github finish-phase` no fim. `--solo`/`--auto` mantem GitHub (autonomia, nao
896
+ desliga). `--local` degrada para commit na branch atual (sem worktree/issue/PR)
850
897
  - [ ] Execucao sempre via up-executor (roteia por contexto: frontend/backend/database/misto); SEM agentes
851
898
  specialist separados (Onda 2 do corte)
852
899
  - [ ] `--board`: Multica init no inicio (project+pai), sync `in_progress` na entrada da fase, sync BATCHED
@@ -4,7 +4,7 @@ Executar tarefas pequenas e ad-hoc com garantias UP (commits atomicos, rastreame
4
4
  **ESCAPE HATCH PURO (sem cerimonia GitHub).** Diferente de `/up:build` (GitHub-nativo por DEFAULT:
5
5
  worktree -> branch `up/fase-NN` -> issue -> PR -> menu), o modo rapido NUNCA cria worktree, NUNCA cria
6
6
  issue do GitHub, NUNCA abre PR e NAO toca em `.plano/git-map.json`. Todo o trabalho e committado
7
- atomicamente na branch ATUAL (mesmo semantica do `--solo` do build). E o caminho quente para
7
+ atomicamente na branch ATUAL (mesma semantica do `--local` do build). E o caminho quente para
8
8
  "so faz e commita". Quem quer worktree/issue/PR usa `/up:build`.
9
9
  </purpose>
10
10
 
@@ -290,9 +290,12 @@ Escrever inline (sem CEO):
290
290
  **`.plano/config.json`** — defaults (perguntar so na complex):
291
291
 
292
292
  ```json
293
- { "mode": "yolo", "granularity": "standard", "parallelization": true, "commit_docs": true }
293
+ { "mode": "yolo", "granularity": "standard", "parallelization": true, "commit_docs": true, "github_native": true }
294
294
  ```
295
295
 
296
+ `github_native: true` explicito torna a intencao visivel no arquivo (o build ja usa true por default, mas
297
+ deixar gravado evita ambiguidade em run autonomo). So vira `false` se o projeto e local sem GitHub.
298
+
296
299
  Commits atomicos:
297
300
 
298
301
  ```bash
@@ -345,9 +348,10 @@ Apos estruturar, o `/up` NAO planeja nem executa fases sozinho. Ele entrega o ha
345
348
  ---
346
349
  ```
347
350
 
348
- > `/up:build` aceita `--solo` (default, commit na branch atual), `--pr` (worktree+issue+PR),
349
- > `--board` (espelho de status no Multica, batched, fail-open) e `--auto` (merge se verde). Para ver o
350
- > board depois: `/up estado board` (abre a URL no Multica). Aqui so roteamos pro /up:plan.
351
+ > `/up:build` e **GitHub-nativo por padrao** (worktree + issue + PR por fase, via `gh` OU MCP do GitHub).
352
+ > Flags: `--auto` (pula o menu, mantem GitHub), `--solo` (autonomo total: GitHub + auto-merge, sem menu
353
+ > nem gate visual), `--local` (escape sem GitHub: commit na branch atual), `--board` (espelho de status no
354
+ > Multica, batched, fail-open). Para ver o board depois: `/up estado board`. Aqui so roteamos pro /up:plan.
351
355
 
352
356
  ## Passo 4: CLONE (URL recebida)
353
357
 
@@ -394,6 +398,7 @@ escreve o PRD pronto pro planejamento.
394
398
 
395
399
  ```json
396
400
  { "mode": "yolo", "granularity": "standard", "parallelization": true, "commit_docs": true,
401
+ "github_native": true,
397
402
  "builder_type": "clone", "clone_source": "{URL}", "clone_mode": "{exact|improve|inspiration}",
398
403
  "clone_data": ".plano/clone/" }
399
404
  ```