up-cc 2.1.0 → 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
CHANGED
package/up/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,36 @@ 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
|
+
|
|
7
37
|
## 2.1.0
|
|
8
38
|
|
|
9
39
|
> GitHub e a interacao humana viram **eixos separados**. Antes, `--solo` desligava o
|
|
@@ -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);
|