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
|
@@ -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);
|
package/up/bin/up-tools.cjs
CHANGED
|
@@ -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 [--
|
|
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|
|
|
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
|
}
|
package/up/commands/build.md
CHANGED
|
@@ -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
|
|
4
|
-
argument-hint: "[--solo] [--
|
|
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` - **
|
|
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
|
|
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 `--
|
|
82
|
-
- `--
|
|
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 `--
|
|
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 `--
|
|
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 (--
|
|
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
|
-
|
|
57
|
-
|
|
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):
|
|
78
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|