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.
@@ -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>
@@ -53,20 +53,59 @@ function readBootstrap() {
53
53
  return FALLBACK_BOOTSTRAP;
54
54
  }
55
55
 
56
- function buildContext(bootstrap) {
57
- return '<EXTREMELY_IMPORTANT>\n' +
56
+ // Teto do trecho de STATE.md injetado. As secoes uteis (Posicao Atual,
57
+ // Contexto Acumulado, Continuidade de Sessao) vivem no topo do arquivo, entao
58
+ // um head capado retoma a posicao sem estourar o contexto.
59
+ const MAX_STATE_CHARS = 2800;
60
+
61
+ /**
62
+ * Le um resumo do estado do projeto em <cwd>/.plano/STATE.md, se existir.
63
+ * Retorna o trecho capado (string) ou null. PUSH deterministico da posicao:
64
+ * o agente sempre volta sabendo onde parou, sem depender de escolher ler.
65
+ * Independe do formato exato (PT/EN) -- e um head bruto, nunca quebra.
66
+ */
67
+ function readState(cwd) {
68
+ if (!cwd || typeof cwd !== 'string') return null;
69
+ try {
70
+ const statePath = path.join(cwd, '.plano', 'STATE.md');
71
+ if (!fs.existsSync(statePath)) return null;
72
+ let content = fs.readFileSync(statePath, 'utf8');
73
+ if (!content || !content.trim()) return null;
74
+ content = content.trim();
75
+ if (content.length > MAX_STATE_CHARS) {
76
+ content = content.slice(0, MAX_STATE_CHARS).trimEnd() +
77
+ '\n\n[...truncado -- leia .plano/STATE.md para o resto]';
78
+ }
79
+ return content;
80
+ } catch (e) {
81
+ return null;
82
+ }
83
+ }
84
+
85
+ function buildContext(bootstrap, state) {
86
+ let ctx = '<EXTREMELY_IMPORTANT>\n' +
58
87
  'Voce tem o UP.\n\n' +
59
88
  'Abaixo esta o conteudo completo da sua skill \'usando-up\' -- sua introducao a usar ' +
60
89
  'as skills do UP. Para todas as outras skills, use a tool Skill:\n\n' +
61
90
  bootstrap.trim() + '\n' +
62
91
  '</EXTREMELY_IMPORTANT>';
92
+
93
+ if (state) {
94
+ ctx += '\n\n<UP-ESTADO-ATUAL>\n' +
95
+ 'Voce esta retomando um projeto UP em andamento (inicio de sessao ou apos /clear). ' +
96
+ 'NAO comece do zero nem assuma contexto perdido: esta e a posicao atual, lida de ' +
97
+ '.plano/STATE.md. Continue daqui. Para rotear o proximo passo, use /up sem argumento.\n\n' +
98
+ state + '\n' +
99
+ '</UP-ESTADO-ATUAL>';
100
+ }
101
+ return ctx;
63
102
  }
64
103
 
65
- function emit(bootstrap) {
104
+ function emit(bootstrap, state) {
66
105
  const output = {
67
106
  hookSpecificOutput: {
68
107
  hookEventName: 'SessionStart',
69
- additionalContext: buildContext(bootstrap),
108
+ additionalContext: buildContext(bootstrap, state || null),
70
109
  },
71
110
  };
72
111
  process.stdout.write(JSON.stringify(output));
@@ -74,8 +113,9 @@ function emit(bootstrap) {
74
113
 
75
114
  let input = '';
76
115
  const stdinTimeout = setTimeout(() => {
77
- // No stdin within 3s (Windows/Git Bash guard): still inject the bootstrap.
78
- try { emit(readBootstrap()); } catch (e) {}
116
+ // No stdin within 3s (Windows/Git Bash guard): inject the bootstrap.
117
+ // Sem payload nao ha cwd, entao sem bloco de estado (so a doutrina).
118
+ try { emit(readBootstrap(), null); } catch (e) {}
79
119
  process.exit(0);
80
120
  }, 3000);
81
121
 
@@ -85,10 +125,13 @@ process.stdin.on('end', () => {
85
125
  clearTimeout(stdinTimeout);
86
126
  try {
87
127
  let source = null;
128
+ let cwd = null;
88
129
  try {
89
130
  const data = JSON.parse(input);
90
- // SessionStart payload carries the trigger in `source` (startup|clear|compact|resume).
131
+ // SessionStart payload carries the trigger in `source` (startup|clear|compact|resume)
132
+ // e o diretorio do projeto em `cwd`.
91
133
  source = data && (data.source || data.hook_event_source || null);
134
+ cwd = (data && data.cwd) || null;
92
135
  } catch (e) {
93
136
  // No/invalid payload: with no matcher available, inject anyway.
94
137
  source = null;
@@ -99,7 +142,9 @@ process.stdin.on('end', () => {
99
142
  process.exit(0);
100
143
  }
101
144
 
102
- emit(readBootstrap());
145
+ // Sem cwd no payload, cai pro diretorio do processo (o harness roda o hook no cwd da sessao).
146
+ const projectCwd = cwd || process.cwd();
147
+ emit(readBootstrap(), readState(projectCwd));
103
148
  } catch (e) {
104
149
  // Fail-open: never block the session.
105
150
  }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * up-session-start.test.cjs - testa a injecao de estado no SessionStart.
3
+ * Roda: node up/hooks/up-session-start.test.cjs
4
+ * Invoca o hook como subprocesso (stdin JSON -> stdout JSON), igual o harness faz.
5
+ */
6
+ const assert = require('assert');
7
+ const fs = require('fs');
8
+ const os = require('os');
9
+ const path = require('path');
10
+ const { execFileSync } = require('child_process');
11
+
12
+ const HOOK = path.resolve(__dirname, 'up-session-start.js');
13
+
14
+ /** Roda o hook com um payload e devolve {additionalContext} ou null. */
15
+ function runHook(payload) {
16
+ const out = execFileSync('node', [HOOK], {
17
+ input: JSON.stringify(payload),
18
+ encoding: 'utf-8',
19
+ timeout: 8000,
20
+ });
21
+ if (!out || !out.trim()) return null;
22
+ const parsed = JSON.parse(out);
23
+ return parsed.hookSpecificOutput ? parsed.hookSpecificOutput.additionalContext : null;
24
+ }
25
+
26
+ function mkProjectWithState(stateBody) {
27
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'up-ss-'));
28
+ fs.mkdirSync(path.join(dir, '.plano'), { recursive: true });
29
+ fs.writeFileSync(path.join(dir, '.plano', 'STATE.md'), stateBody, 'utf-8');
30
+ return dir;
31
+ }
32
+ function mkProjectNoPlano() {
33
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'up-ss-noplano-'));
34
+ }
35
+
36
+ let pass = 0, fail = 0;
37
+ function t(name, fn) {
38
+ try { fn(); console.log(' ok -', name); pass++; }
39
+ catch (e) { console.error(' FAIL -', name, '\n ', e.message); fail++; }
40
+ }
41
+
42
+ const SAMPLE_STATE = `# Estado do Projeto
43
+
44
+ ## Posicao Atual
45
+
46
+ Fase: 7 de 10 (Comando /up:melhorias)
47
+ Status: In progress
48
+ Ultima atividade: 2026-06-19 -- T3 do plano 02 concluido
49
+
50
+ ## Continuidade de Sessao
51
+
52
+ Parou em: implementando o gate de aprovacao visual
53
+ `;
54
+
55
+ // 1. clear + .plano/STATE.md presente -> injeta bootstrap E o estado
56
+ t('clear com .plano: injeta bootstrap + estado', () => {
57
+ const dir = mkProjectWithState(SAMPLE_STATE);
58
+ const ctx = runHook({ source: 'clear', cwd: dir, hook_event_name: 'SessionStart' });
59
+ assert.ok(ctx, 'deve haver additionalContext');
60
+ assert.ok(/Voce tem o UP/.test(ctx), 'deve conter a doutrina (bootstrap)');
61
+ assert.ok(/Fase: 7 de 10/.test(ctx), 'deve conter a posicao do STATE.md');
62
+ assert.ok(/Parou em: implementando o gate/.test(ctx), 'deve conter onde parou');
63
+ });
64
+
65
+ // 2. clear SEM .plano -> so bootstrap, sem bloco de estado
66
+ t('clear sem .plano: so bootstrap', () => {
67
+ const dir = mkProjectNoPlano();
68
+ const ctx = runHook({ source: 'clear', cwd: dir, hook_event_name: 'SessionStart' });
69
+ assert.ok(ctx, 'deve haver additionalContext');
70
+ assert.ok(/Voce tem o UP/.test(ctx), 'deve conter a doutrina');
71
+ assert.ok(!/UP-ESTADO-ATUAL/.test(ctx), 'NAO deve ter bloco de estado sem .plano');
72
+ });
73
+
74
+ // 3. startup tambem injeta estado (retomar projeto ao abrir)
75
+ t('startup com .plano: injeta estado', () => {
76
+ const dir = mkProjectWithState(SAMPLE_STATE);
77
+ const ctx = runHook({ source: 'startup', cwd: dir, hook_event_name: 'SessionStart' });
78
+ assert.ok(/Fase: 7 de 10/.test(ctx), 'startup tambem deve trazer o estado');
79
+ });
80
+
81
+ // 4. source fora dos triggers (resume) -> nada
82
+ t('resume: nenhum output', () => {
83
+ const dir = mkProjectWithState(SAMPLE_STATE);
84
+ const ctx = runHook({ source: 'resume', cwd: dir, hook_event_name: 'SessionStart' });
85
+ assert.strictEqual(ctx, null, 'resume nao deve injetar nada');
86
+ });
87
+
88
+ // 5. STATE.md gigante -> capado (bloco de estado nao explode o contexto)
89
+ t('STATE.md gigante: capado', () => {
90
+ const huge = SAMPLE_STATE + '\n' + 'x'.repeat(20000);
91
+ const dir = mkProjectWithState(huge);
92
+ const ctx = runHook({ source: 'clear', cwd: dir, hook_event_name: 'SessionStart' });
93
+ // o bloco de estado deve estar truncado bem abaixo do tamanho do STATE gigante
94
+ assert.ok(ctx.length < 8000, 'contexto total deve ficar capado (<8000 chars), veio ' + ctx.length);
95
+ assert.ok(/Fase: 7 de 10/.test(ctx), 'mesmo capado, a posicao do topo deve aparecer');
96
+ });
97
+
98
+ console.log(`\n${pass} passed, ${fail} failed`);
99
+ process.exit(fail ? 1 : 0);
@@ -25,7 +25,7 @@ O UP ativa por contexto. Nao precisa decorar comando: a skill certa dispara pelo
25
25
 
26
26
  **Prova por tipo:** logica/bugfix = teste red-green; UI/CSS = captura visual; glue/integracao = smoke-test. Veja `up-tdd`.
27
27
 
28
- **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.
29
29
 
30
30
  **Persistencia:** tudo vive em `.plano/` e sobrevive a `/clear`. Leia `.plano/STATE.md` antes de assumir contexto perdido.
31
31