up-cc 2.0.3 → 2.2.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 +1 -1
- package/up/CHANGELOG.md +79 -0
- package/up/agents/up-arquiteto.md +5 -2
- package/up/bin/lib/core.cjs +3 -2
- package/up/bin/lib/github.cjs +185 -76
- package/up/bin/lib/github.test.cjs +161 -0
- package/up/bin/up-tools.cjs +20 -3
- package/up/commands/build.md +15 -14
- package/up/hooks/up-session-start.js +53 -8
- package/up/hooks/up-session-start.test.cjs +99 -0
- package/up/skills/usando-up/SKILL.md +1 -1
- package/up/workflows/build.md +98 -51
- package/up/workflows/rapido.md +1 -1
- package/up/workflows/up.md +9 -4
package/package.json
CHANGED
package/up/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,85 @@ 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.2.0
|
|
8
|
+
|
|
9
|
+
> O `.plano/` sempre sobreviveu ao `/clear` no DISCO, mas a RETOMADA era pull: o
|
|
10
|
+
> hook reinjetava so a doutrina e o agente tinha que escolher ler o STATE.md. Agora
|
|
11
|
+
> e push: o SessionStart injeta a posicao atual sozinho.
|
|
12
|
+
|
|
13
|
+
### Corrigido
|
|
14
|
+
|
|
15
|
+
- **SessionStart agora injeta `.plano/STATE.md`, nao so a doutrina.** O hook
|
|
16
|
+
`up-session-start.js` le `<cwd>/.plano/STATE.md` (cwd do payload, fallback
|
|
17
|
+
`process.cwd()`) e injeta um trecho capado (2800 chars) num bloco
|
|
18
|
+
`<UP-ESTADO-ATUAL>` junto do bootstrap. Resultado: a cada `/clear` (e no
|
|
19
|
+
startup) a posicao do projeto volta pro contexto **deterministicamente**, sem
|
|
20
|
+
depender do agente escolher ler. Resolve o "as vezes nao sobrevive ao /clear".
|
|
21
|
+
Fail-open mantido: sem `.plano`, so a doutrina (comportamento anterior). Head
|
|
22
|
+
bruto, independe do formato PT/EN do STATE.md.
|
|
23
|
+
|
|
24
|
+
### Testes
|
|
25
|
+
|
|
26
|
+
- `up/hooks/up-session-start.test.cjs`: 5 casos red-green invocando o hook como
|
|
27
|
+
subprocesso (stdin->stdout), cobrindo clear/startup com e sem `.plano`, skip de
|
|
28
|
+
`resume`, e cap de STATE.md gigante.
|
|
29
|
+
|
|
30
|
+
### Conhecido (nao corrigido aqui)
|
|
31
|
+
|
|
32
|
+
- `up-tools state-snapshot` parseia rotulos em INGLES e os STATE.md reais sao em
|
|
33
|
+
PORTUGUES, entao retorna campos null em projeto real. O hook NAO usa
|
|
34
|
+
state-snapshot (le STATE.md direto), entao a retomada nao e afetada. Fica
|
|
35
|
+
registrado pra um fix futuro do parser.
|
|
36
|
+
|
|
37
|
+
## 2.1.0
|
|
38
|
+
|
|
39
|
+
> GitHub e a interacao humana viram **eixos separados**. Antes, `--solo` desligava o
|
|
40
|
+
> GitHub (zero worktree/issue/PR) E pulava a cerimonia. Num run autonomo/loop isso fazia
|
|
41
|
+
> o projeto inteiro ir pro `main` sem nenhum artefato GitHub, mesmo com remote conectado.
|
|
42
|
+
> Agora `--solo` NAO desliga o GitHub: e so autonomia. Pra pular o GitHub use `--local`.
|
|
43
|
+
|
|
44
|
+
### Mudado (atencao: semantica de `--solo`)
|
|
45
|
+
|
|
46
|
+
- **`--solo` agora MANTEM o GitHub-nativo.** Solo virou "autonomo total": cria
|
|
47
|
+
worktree + branch + issue + PR e auto-mergeia, SEM menu e SEM gate visual (pra
|
|
48
|
+
loop/headless onde ninguem aprova). Nao desliga mais o GitHub.
|
|
49
|
+
- **`--local` e o novo escape hatch sem GitHub** (o comportamento que `--solo` tinha
|
|
50
|
+
ate a 2.0.x): commit atomico na branch atual, zero worktree/issue/PR/rede. `/up:rapido`
|
|
51
|
+
segue como o atalho nomeado pro mesmo modo.
|
|
52
|
+
- **`--auto`** continua igual: pula so o menu de fim de fase, mantem GitHub, e o gate
|
|
53
|
+
visual ainda roda se `require_visual_test=true`.
|
|
54
|
+
|
|
55
|
+
### Adicionado
|
|
56
|
+
|
|
57
|
+
- **Deteccao de GitHub por `gh` CLI OU MCP do GitHub.** O eixo GitHub liga sempre que
|
|
58
|
+
ha remote + (`gh` autenticado OU MCP do GitHub conectado). Transporte de issue/PR:
|
|
59
|
+
`gh` (CLI cria direto), `mcp` (o workflow cria via `mcp__...github__*` e grava de
|
|
60
|
+
volta), `none` (sem remote -> git local). Worktree/branch/push sao git local e
|
|
61
|
+
acontecem sempre, offline-ok.
|
|
62
|
+
- **Subcomandos `github record-issue` / `github record-pr`** no `up-tools.cjs` pra
|
|
63
|
+
gravar no `git-map.json` artefatos criados fora do `.cjs` (ex: via MCP). `record-pr
|
|
64
|
+
--merged` fecha a fase e limpa worktree+branch.
|
|
65
|
+
- **Flag `--local`** no `/up:build` e `start-phase`/`finish-phase` (`--mode local`).
|
|
66
|
+
- **Config builder grava `github_native: true` explicito** (up-arquiteto, workflows/up),
|
|
67
|
+
deixando a intencao visivel em run autonomo (antes dependia do default implicito).
|
|
68
|
+
- **Testes red-green** do `github.cjs` em `up/bin/lib/github.test.cjs` (10 casos; repo
|
|
69
|
+
git temp + remote bare local + seam `UP_FORCE_NO_GH`).
|
|
70
|
+
|
|
71
|
+
### Corrigido
|
|
72
|
+
|
|
73
|
+
- **Bug de doc em `workflows/up.md`** que dizia `--solo (default, commit na branch atual)`
|
|
74
|
+
e descrevia `--pr` como o fluxo GitHub (vocabulario v1). Era a unica fonte que podia
|
|
75
|
+
induzir um agente autonomo a escolher `--solo` e pular o GitHub. Reescrito pro v2.
|
|
76
|
+
- **`--local` numa fase nao polui mais o `github_native` global** do `git-map.json`
|
|
77
|
+
(poderia degradar fases nao-local seguintes). `finish-phase auto` passou a consultar
|
|
78
|
+
o config, nao o flag global do mapa.
|
|
79
|
+
|
|
80
|
+
### Migracao
|
|
81
|
+
|
|
82
|
+
- Quem usava `--solo` pra **commit local sem GitHub** deve trocar para `--local`.
|
|
83
|
+
- Quem quer **autonomo mantendo GitHub** (loop/headless): use `--solo` (sem gate visual)
|
|
84
|
+
ou `--auto` (com gate visual se `require_visual_test=true`).
|
|
85
|
+
|
|
7
86
|
## 2.0.0
|
|
8
87
|
|
|
9
88
|
> 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>
|
package/up/bin/lib/core.cjs
CHANGED
|
@@ -114,8 +114,9 @@ function loadConfig(cwd) {
|
|
|
114
114
|
auto_advance: false,
|
|
115
115
|
instrumentation: { enabled: true },
|
|
116
116
|
budget_ceiling: null,
|
|
117
|
-
//
|
|
118
|
-
//
|
|
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.
|
package/up/bin/lib/github.cjs
CHANGED
|
@@ -34,8 +34,13 @@ function getRemoteUrl(cwd) {
|
|
|
34
34
|
return res.exitCode === 0 ? res.stdout.trim() : null;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
/**
|
|
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
|
-
/**
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
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
|
-
*
|
|
194
|
-
* -
|
|
195
|
-
*
|
|
196
|
-
* -
|
|
197
|
-
*
|
|
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
|
-
// ---
|
|
209
|
-
if (
|
|
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: '
|
|
258
|
+
mode: 'local',
|
|
259
|
+
transport: 'none',
|
|
229
260
|
warnings,
|
|
230
261
|
};
|
|
231
262
|
}
|
|
232
263
|
|
|
233
|
-
// ---
|
|
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 (
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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 (
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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).
|
|
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):
|
|
325
|
-
* ->
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
374
|
-
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
477
|
-
has_remote:
|
|
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
|
};
|