ll-skills 1.0.0 → 2.0.2

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.
Files changed (87) hide show
  1. package/CHANGELOG.md +90 -2
  2. package/README.md +134 -69
  3. package/agents/ll-executor.md +96 -0
  4. package/agents/ll-reviewer.md +76 -0
  5. package/agents/ll-scout.md +83 -0
  6. package/agents/ll-verifier.md +127 -0
  7. package/assets/preamble.md +70 -0
  8. package/assets/settings.suggested.json +19 -0
  9. package/bin/install.js +311 -54
  10. package/hooks/ll-precompact.js +49 -0
  11. package/hooks/ll-skills-check-update.js +1 -1
  12. package/hooks/ll-state.js +158 -0
  13. package/package.json +4 -2
  14. package/scripts/fixtures/empty/.gitkeep +0 -0
  15. package/scripts/fixtures/git-history.sh +49 -0
  16. package/scripts/fixtures/project/BACKLOG.md +11 -0
  17. package/scripts/fixtures/project/PLAN.md +66 -0
  18. package/scripts/fixtures/project/PROGRESS.md +57 -0
  19. package/scripts/fixtures/project/ROADMAP.md +28 -0
  20. package/scripts/fixtures/project/VERIFICATION.md +17 -0
  21. package/scripts/fixtures/project/decisions/DEC-0041-cents.md +16 -0
  22. package/scripts/fixtures/project/phases/07/PLAN.md +145 -0
  23. package/scripts/fixtures/project/src/a.ts +3 -0
  24. package/scripts/fixtures/project/src/pay.ts +5 -0
  25. package/scripts/fixtures/project/test/a.test.ts +5 -0
  26. package/scripts/ll-tools.js +664 -0
  27. package/scripts/smoke-test.sh +340 -0
  28. package/skills/ll-brainstorm/SKILL.md +180 -0
  29. package/skills/ll-brainstorm/references/decision-policy.md +118 -0
  30. package/skills/ll-brainstorm/references/techniques.md +75 -0
  31. package/skills/ll-close/SKILL.md +69 -0
  32. package/skills/ll-close/references/delivery.md +75 -0
  33. package/skills/ll-close/references/retrospective.md +48 -0
  34. package/skills/ll-decide/SKILL.md +135 -0
  35. package/skills/ll-decide/references/decision-policy.md +118 -0
  36. package/skills/ll-decide/references/decision-room.md +62 -0
  37. package/skills/ll-decide/references/disarm.md +79 -0
  38. package/skills/ll-decide/references/feedback-ingestion.md +90 -0
  39. package/skills/ll-decide/references/interview.md +102 -0
  40. package/skills/ll-decide/references/plan-skeleton.md +150 -0
  41. package/skills/ll-decide/references/premise-gate.md +83 -0
  42. package/skills/ll-decide/references/premortem.md +90 -0
  43. package/skills/ll-decide/references/review-spec.md +91 -0
  44. package/skills/ll-goal/SKILL.md +64 -0
  45. package/skills/ll-goal/references/goal-template.md +90 -0
  46. package/skills/ll-implement/SKILL.md +118 -0
  47. package/skills/ll-implement/references/briefs.md +123 -0
  48. package/skills/ll-implement/references/decision-policy.md +118 -0
  49. package/skills/ll-implement/references/phase-conversation.md +76 -0
  50. package/skills/ll-implement/references/phase-plan.md +111 -0
  51. package/skills/ll-oncall/SKILL.md +123 -0
  52. package/skills/ll-oncall/references/deploy-preflight.md +57 -0
  53. package/skills/ll-oncall/references/federation.md +110 -0
  54. package/skills/ll-oncall/references/watch-brief.md +39 -0
  55. package/skills/ll-refine/SKILL.md +116 -0
  56. package/skills/ll-refine/references/production-access.md +48 -0
  57. package/skills/ll-refine/references/visual-gate.md +100 -0
  58. package/skills/ll-research/SKILL.md +87 -0
  59. package/skills/ll-research/references/citation-check.md +38 -0
  60. package/skills/ll-research/references/front-brief.md +41 -0
  61. package/skills/ll-research/references/market-mode.md +83 -0
  62. package/skills/ll-resume/SKILL.md +75 -0
  63. package/skills/ll-update/SKILL.md +75 -0
  64. package/skills/ll-verify/SKILL.md +75 -0
  65. package/skills/ll-verify/references/verifier-briefs.md +116 -0
  66. package/agents/ll-implementador.md +0 -23
  67. package/skills/ll-atualizar/SKILL.md +0 -68
  68. package/skills/ll-decidir-antes/SKILL.md +0 -81
  69. package/skills/ll-decidir-antes/referencias/protocolo-entrevista.md +0 -112
  70. package/skills/ll-decidir-antes/referencias/template-spec.md +0 -238
  71. package/skills/ll-desarmar/SKILL.md +0 -254
  72. package/skills/ll-desarmar/referencias/execucao-adversarial.md +0 -217
  73. package/skills/ll-desarmar/referencias/humanos-e-substitutos.md +0 -116
  74. package/skills/ll-desarmar/referencias/placar-e-realimentacao.md +0 -140
  75. package/skills/ll-orquestrar/SKILL.md +0 -100
  76. package/skills/ll-pesquisar/SKILL.md +0 -159
  77. package/skills/ll-pesquisar/referencias/frente-de-pesquisa.md +0 -147
  78. package/skills/ll-pesquisar/referencias/sintese-e-fontes.md +0 -148
  79. package/skills/ll-pesquisar-mercado/SKILL.md +0 -112
  80. package/skills/ll-pesquisar-mercado/referencias/dossie.md +0 -375
  81. package/skills/ll-pesquisar-mercado/referencias/indice-e-fechamento.md +0 -122
  82. package/skills/ll-pesquisar-mercado/referencias/padroes-de-pesquisa.md +0 -149
  83. package/skills/ll-verificar-entrega/SKILL.md +0 -73
  84. package/skills/ll-verificar-entrega/referencias/briefs-auditoria.md +0 -291
  85. package/skills/ll-voltar-do-futuro/SKILL.md +0 -239
  86. package/skills/ll-voltar-do-futuro/referencias/anti-padroes-e-fundamentos.md +0 -201
  87. package/skills/ll-voltar-do-futuro/referencias/vetores-e-testes.md +0 -228
package/bin/install.js CHANGED
@@ -3,17 +3,20 @@
3
3
 
4
4
  // Instalador do ll-skills.
5
5
  //
6
- // npx ll-skills@latest instala/atualiza em ~/.claude (ou $CLAUDE_CONFIG_DIR)
7
- // npx ll-skills@latest --local instala em ./.claude do diretório atual
6
+ // npx ll-skills@latest instala/atualiza em ~/.claude (ou $CLAUDE_CONFIG_DIR)
7
+ // npx ll-skills@latest --local instala em ./.claude do diretório atual
8
+ // npx ll-skills@latest --no-settings só imprime os hooks, não toca em settings.json
9
+ // npx ll-skills@latest --no-preamble não escreve o bloco do preâmbulo no CLAUDE.md
10
+ // npx ll-skills@latest --yes aprova o preâmbulo sem perguntar
8
11
  // npx ll-skills@latest --uninstall
9
12
  //
10
- // Copia skills/ll-*, agents/ll-* e o hook de aviso de atualização para o diretório
11
- // de configuração do Claude Code, registra o hook em settings.json, grava
13
+ // Copia skills/ll-*, agents/ll-*, os 3 hooks e o helper ll-tools.js para o diretório
14
+ // de configuração do Claude Code, registra os hooks em settings.json (SessionStart +
15
+ // PreCompact), escreve o bloco do preâmbulo no CLAUDE.md entre marcadores, grava
12
16
  // VERSION + manifesto com sha256 por arquivo e limpa o que ficou de instalações
13
- // anteriores (plugin legado por marketplace, arquivos órfãos, cache antigo).
17
+ // anteriores (plugin legado por marketplace, skills 1.x, arquivos órfãos, cache antigo).
14
18
  //
15
- // Sem dependências, sem prompts. Tudo relativo ao próprio pacote (__dirname),
16
- // nunca ao cwd.
19
+ // Sem dependências. Tudo relativo ao próprio pacote (__dirname), nunca ao cwd.
17
20
 
18
21
  const fs = require('fs');
19
22
  const path = require('path');
@@ -27,9 +30,42 @@ const PKG_NAME = PKG.name;
27
30
  const REPO_URL = 'https://github.com/allangdy/ll-skills.git';
28
31
  const LEGACY_PLUGIN_ID = 'll-skills@ll-skills';
29
32
  const LEGACY_MARKETPLACE = 'll-skills';
30
- const HOOK_FILE = 'll-skills-check-update.js';
31
- const HOOK_MATCHER = 'startup|resume';
32
- const PREFIXOS_PROPRIOS = ['skills/ll-', 'agents/ll-', 'hooks/ll-skills-', 'll-skills/'];
33
+
34
+ // hooks registrados em settings.json
35
+ const SESSION_HOOKS = [
36
+ { file: 'll-skills-check-update.js', timeout: 5 },
37
+ { file: 'll-state.js', timeout: 3 },
38
+ ];
39
+ const PRECOMPACT_HOOK = { file: 'll-precompact.js', timeout: 3 };
40
+ const SESSION_MATCHER = 'startup|resume|compact';
41
+ const HOOK_EVENTS = ['SessionStart', 'PreCompact'];
42
+ const OWN_HOOK_FILES = SESSION_HOOKS.map((h) => h.file).concat([PRECOMPACT_HOOK.file]);
43
+
44
+ // helper copiado para dentro das skills que o usam (caminho relativo com --local quebraria require)
45
+ const HELPER_SKILLS = ['ll-implement', 'll-verify', 'll-close'];
46
+ const HELPER_SRC = path.join(PKG_ROOT, 'scripts', 'll-tools.js');
47
+
48
+ // preâmbulo
49
+ const PREAMBLE_SRC = path.join(PKG_ROOT, 'assets', 'preamble.md');
50
+ const PREAMBLE_OPEN = '<!-- ll-skills:preamble v1 -->';
51
+ const PREAMBLE_CLOSE = '<!-- /ll-skills:preamble -->';
52
+ const POLICY_SRC = path.join(PKG_ROOT, 'assets', 'settings.suggested.json');
53
+
54
+ const PREFIXOS_PROPRIOS = ['skills/ll-', 'agents/ll-', 'hooks/ll-', 'll-skills/'];
55
+
56
+ // removidos mesmo sem manifesto (instalações 1.x e cópias manuais)
57
+ const KNOWN_LEGACY = [
58
+ 'skills/ll-atualizar',
59
+ 'skills/ll-decidir-antes',
60
+ 'skills/ll-desarmar',
61
+ 'skills/ll-orquestrar',
62
+ 'skills/ll-pesquisar',
63
+ 'skills/ll-pesquisar-mercado',
64
+ 'skills/ll-verificar-entrega',
65
+ 'skills/ll-voltar-do-futuro',
66
+ 'agents/ll-implementador.md',
67
+ ];
68
+ const LEGACY_BASENAMES = new Set(KNOWN_LEGACY.map((r) => r.split('/')[1]));
33
69
 
34
70
  const CACHE_DIR = path.join(process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache'), 'll-skills');
35
71
 
@@ -47,10 +83,13 @@ function fail(msg, code = 1) {
47
83
  }
48
84
 
49
85
  function parseArgs(argv) {
50
- const args = { uninstall: false, local: false, help: false };
86
+ const args = { uninstall: false, local: false, help: false, settings: true, preamble: true, yes: false };
51
87
  for (const a of argv) {
52
88
  if (a === '--uninstall') args.uninstall = true;
53
89
  else if (a === '--local') args.local = true;
90
+ else if (a === '--no-settings') args.settings = false;
91
+ else if (a === '--no-preamble') args.preamble = false;
92
+ else if (a === '--yes' || a === '-y') args.yes = true;
54
93
  else if (a === '--help' || a === '-h') args.help = true;
55
94
  else fail(`argumento desconhecido: ${a} (use --help)`);
56
95
  }
@@ -61,9 +100,12 @@ function usage() {
61
100
  log(`ll-skills ${PKG.version}
62
101
 
63
102
  Uso:
64
- npx ll-skills@latest instala ou atualiza em $CLAUDE_CONFIG_DIR ou ~/.claude
65
- npx ll-skills@latest --local instala em ./.claude (só este projeto)
66
- npx ll-skills@latest --uninstall remove tudo que este pacote instalou
103
+ npx ll-skills@latest instala ou atualiza em $CLAUDE_CONFIG_DIR ou ~/.claude
104
+ npx ll-skills@latest --local instala em ./.claude (só este projeto)
105
+ npx ll-skills@latest --no-settings não toca em settings.json; imprime os hooks
106
+ npx ll-skills@latest --no-preamble não escreve o bloco do preâmbulo no CLAUDE.md
107
+ npx ll-skills@latest --yes aprova o preâmbulo sem perguntar (-y)
108
+ npx ll-skills@latest --uninstall remove tudo que este pacote instalou
67
109
  npx ll-skills@latest --help`);
68
110
  }
69
111
 
@@ -93,6 +135,14 @@ function readJson(file, fallback) {
93
135
  }
94
136
  }
95
137
 
138
+ function readText(file) {
139
+ try {
140
+ return fs.readFileSync(file, 'utf8');
141
+ } catch {
142
+ return null;
143
+ }
144
+ }
145
+
96
146
  function writeJsonAtomic(file, obj) {
97
147
  fs.mkdirSync(path.dirname(file), { recursive: true });
98
148
  const tmp = `${file}.tmp.${process.pid}`;
@@ -100,6 +150,13 @@ function writeJsonAtomic(file, obj) {
100
150
  fs.renameSync(tmp, file);
101
151
  }
102
152
 
153
+ function writeTextAtomic(file, text) {
154
+ fs.mkdirSync(path.dirname(file), { recursive: true });
155
+ const tmp = `${file}.tmp.${process.pid}`;
156
+ fs.writeFileSync(tmp, text);
157
+ fs.renameSync(tmp, file);
158
+ }
159
+
103
160
  function insideDir(base, target) {
104
161
  const rel = path.relative(base, target);
105
162
  return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
@@ -118,12 +175,12 @@ function walk(dir, base = dir, out = []) {
118
175
  return out;
119
176
  }
120
177
 
121
- // Lista de arquivos do pacote a instalar: [{ src, rel }], rel relativo ao configDir.
178
+ // Lista de arquivos do pacote a instalar: [{ src, rel, mode }], rel relativo ao configDir.
122
179
  function planFiles(pkgRoot) {
123
180
  const plan = [];
124
181
  const skillsDir = path.join(pkgRoot, 'skills');
125
182
  for (const name of fs.readdirSync(skillsDir)) {
126
- if (!name.startsWith('ll-')) continue;
183
+ if (!name.startsWith('ll-') || LEGACY_BASENAMES.has(name)) continue;
127
184
  const dir = path.join(skillsDir, name);
128
185
  if (!fs.statSync(dir).isDirectory()) continue;
129
186
  for (const rel of walk(dir)) {
@@ -133,7 +190,7 @@ function planFiles(pkgRoot) {
133
190
  const agentsDir = path.join(pkgRoot, 'agents');
134
191
  if (fs.existsSync(agentsDir)) {
135
192
  for (const name of fs.readdirSync(agentsDir)) {
136
- if (name.startsWith('ll-') && name.endsWith('.md')) {
193
+ if (name.startsWith('ll-') && name.endsWith('.md') && !LEGACY_BASENAMES.has(name)) {
137
194
  plan.push({ src: path.join(agentsDir, name), rel: `agents/${name}` });
138
195
  }
139
196
  }
@@ -141,11 +198,20 @@ function planFiles(pkgRoot) {
141
198
  const hooksDir = path.join(pkgRoot, 'hooks');
142
199
  if (fs.existsSync(hooksDir)) {
143
200
  for (const name of fs.readdirSync(hooksDir)) {
144
- if (name.startsWith('ll-skills-') && name.endsWith('.js')) {
201
+ if (name.startsWith('ll-') && name.endsWith('.js')) {
145
202
  plan.push({ src: path.join(hooksDir, name), rel: `hooks/${name}`, mode: 0o755 });
146
203
  }
147
204
  }
148
205
  }
206
+ if (fs.existsSync(HELPER_SRC)) {
207
+ const installed = new Set(plan.map((p) => p.rel.split('/')[1]));
208
+ for (const name of HELPER_SKILLS) {
209
+ if (!installed.has(name)) continue;
210
+ plan.push({ src: HELPER_SRC, rel: `skills/${name}/scripts/ll-tools.js`, mode: 0o755 });
211
+ }
212
+ } else {
213
+ log(`• AVISO: ${HELPER_SRC} não existe no pacote; as skills ficam sem o helper ll-tools.js`);
214
+ }
149
215
  return plan;
150
216
  }
151
217
 
@@ -260,6 +326,21 @@ function removeManagedFile(configDir, rel) {
260
326
  return true;
261
327
  }
262
328
 
329
+ // Remove um caminho gerenciado que pode ser diretório (skills 1.x) ou arquivo.
330
+ function removeManagedPath(configDir, rel) {
331
+ if (!isOwnRel(rel)) return false;
332
+ const full = path.resolve(configDir, rel);
333
+ if (!insideDir(configDir, full)) return false;
334
+ if (!fs.existsSync(full)) return false;
335
+ try {
336
+ fs.rmSync(full, { recursive: true, force: true });
337
+ } catch {
338
+ return false;
339
+ }
340
+ removeEmptyDirsUpTo(path.dirname(full), configDir);
341
+ return true;
342
+ }
343
+
263
344
  function pruneStale(configDir, oldManifest, newFiles) {
264
345
  const removed = [];
265
346
  const old = (oldManifest && oldManifest.files) || {};
@@ -267,6 +348,10 @@ function pruneStale(configDir, oldManifest, newFiles) {
267
348
  if (rel in newFiles) continue;
268
349
  if (removeManagedFile(configDir, rel)) removed.push(rel);
269
350
  }
351
+ for (const rel of KNOWN_LEGACY) {
352
+ if (rel in newFiles) continue;
353
+ if (removeManagedPath(configDir, rel)) removed.push(rel);
354
+ }
270
355
  return removed;
271
356
  }
272
357
 
@@ -303,29 +388,37 @@ function clearOldCache() {
303
388
  // settings.json
304
389
  // ---------------------------------------------------------------------------
305
390
 
306
- function hookCommand(configDir) {
307
- const hookPath = path.join(configDir, 'hooks', HOOK_FILE);
391
+ function hookCommand(configDir, file) {
392
+ const hookPath = path.join(configDir, 'hooks', file);
308
393
  // `node` bare: com nvm, um caminho absoluto para o binário quebra no próximo `nvm install`.
309
394
  return `command -v node >/dev/null 2>&1 && node "${hookPath}" || true`;
310
395
  }
311
396
 
312
- function buildHookEntry(configDir) {
397
+ function buildHookEntries(configDir) {
313
398
  return {
314
- matcher: HOOK_MATCHER,
315
- hooks: [{ type: 'command', command: hookCommand(configDir), timeout: 5 }],
399
+ SessionStart: {
400
+ matcher: SESSION_MATCHER,
401
+ hooks: SESSION_HOOKS.map((h) => ({ type: 'command', command: hookCommand(configDir, h.file), timeout: h.timeout })),
402
+ },
403
+ PreCompact: {
404
+ hooks: [{ type: 'command', command: hookCommand(configDir, PRECOMPACT_HOOK.file), timeout: PRECOMPACT_HOOK.timeout }],
405
+ },
316
406
  };
317
407
  }
318
408
 
319
409
  function isOwnHook(h) {
320
- return Boolean(h && typeof h.command === 'string' && h.command.includes(HOOK_FILE));
410
+ return Boolean(h && typeof h.command === 'string' && OWN_HOOK_FILES.some((f) => h.command.includes(f)));
321
411
  }
322
412
 
323
413
  function stripOwnHooks(settings) {
324
- const ss = settings.hooks && settings.hooks.SessionStart;
325
- if (!Array.isArray(ss)) return;
326
- settings.hooks.SessionStart = ss
327
- .map((e) => (e && Array.isArray(e.hooks) ? { ...e, hooks: e.hooks.filter((h) => !isOwnHook(h)) } : e))
328
- .filter((e) => !(e && Array.isArray(e.hooks) && e.hooks.length === 0));
414
+ if (!settings.hooks || typeof settings.hooks !== 'object') return;
415
+ for (const ev of HOOK_EVENTS) {
416
+ const list = settings.hooks[ev];
417
+ if (!Array.isArray(list)) continue;
418
+ settings.hooks[ev] = list
419
+ .map((e) => (e && Array.isArray(e.hooks) ? { ...e, hooks: e.hooks.filter((h) => !isOwnHook(h)) } : e))
420
+ .filter((e) => !(e && Array.isArray(e.hooks) && e.hooks.length === 0));
421
+ }
329
422
  }
330
423
 
331
424
  function stripLegacyPluginKeys(settings) {
@@ -340,10 +433,11 @@ function stripLegacyPluginKeys(settings) {
340
433
  }
341
434
 
342
435
  function pruneEmptyHooks(settings) {
343
- if (settings.hooks && Array.isArray(settings.hooks.SessionStart) && settings.hooks.SessionStart.length === 0) {
344
- delete settings.hooks.SessionStart;
436
+ if (!settings.hooks || typeof settings.hooks !== 'object') return;
437
+ for (const ev of HOOK_EVENTS) {
438
+ if (Array.isArray(settings.hooks[ev]) && settings.hooks[ev].length === 0) delete settings.hooks[ev];
345
439
  }
346
- if (settings.hooks && Object.keys(settings.hooks).length === 0) delete settings.hooks;
440
+ if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
347
441
  }
348
442
 
349
443
  // Lê, aplica `mutate(settings)`, grava só se mudou. Retorna 'unchanged' | 'written' | 'invalid'.
@@ -359,32 +453,170 @@ function updateSettings(configDir, mutate) {
359
453
  return 'written';
360
454
  }
361
455
 
362
- function registerHook(configDir) {
456
+ // Registra os 3 hooks (SessionStart + PreCompact) numa única mutação.
457
+ function registerHooks(configDir) {
458
+ const entries = buildHookEntries(configDir);
363
459
  return updateSettings(configDir, (s) => {
364
460
  if (!s.hooks || typeof s.hooks !== 'object') s.hooks = {};
365
- if (!Array.isArray(s.hooks.SessionStart)) s.hooks.SessionStart = [];
366
461
  stripOwnHooks(s);
367
- s.hooks.SessionStart.push(buildHookEntry(configDir));
462
+ for (const ev of HOOK_EVENTS) {
463
+ if (!Array.isArray(s.hooks[ev])) s.hooks[ev] = [];
464
+ s.hooks[ev].push(entries[ev]);
465
+ }
368
466
  stripLegacyPluginKeys(s);
369
467
  });
370
468
  }
371
469
 
372
- function unregisterHook(configDir) {
470
+ function unregisterHooks(configDir) {
373
471
  return updateSettings(configDir, (s) => {
374
472
  stripOwnHooks(s);
375
473
  pruneEmptyHooks(s);
376
474
  });
377
475
  }
378
476
 
379
- function printManualHookSnippet(configDir) {
477
+ function printHookSnippet(configDir) {
478
+ const entries = buildHookEntries(configDir);
479
+ log('');
480
+ log('Adicione manualmente em "hooks" de ' + path.join(configDir, 'settings.json') + ':');
481
+ log('');
482
+ log(JSON.stringify({ hooks: { SessionStart: [entries.SessionStart], PreCompact: [entries.PreCompact] } }, null, 2));
380
483
  log('');
381
- log(`AVISO: ${path.join(configDir, 'settings.json')} não é um JSON válido; não foi alterado.`);
382
- log('Corrija o arquivo e adicione manualmente em "hooks" > "SessionStart":');
484
+ }
485
+
486
+ // Política sugerida da máquina: sempre impressa, nunca escrita.
487
+ function printPolicy() {
488
+ const raw = readText(POLICY_SRC);
489
+ if (raw === null) return;
383
490
  log('');
384
- log(JSON.stringify(buildHookEntry(configDir), null, 2));
491
+ log('Política sugerida (NÃO foi escrita; aplique você mesmo em settings.json):');
492
+ log(raw.trimEnd());
385
493
  log('');
386
494
  }
387
495
 
496
+ // Diagnóstico de limpeza da máquina — só imprime, nunca executa.
497
+ function diagnoseCleanup(configDir) {
498
+ const items = [];
499
+ const cache = path.join(os.homedir(), '.claude', 'plugins', 'cache', 'll-skills');
500
+ if (fs.existsSync(cache)) items.push(`rm -rf ${cache} # cache do plugin legado`);
501
+
502
+ if (!items.length) return;
503
+ log('');
504
+ log('Limpeza sugerida (diagnóstico; nada foi executado):');
505
+ for (const i of items) log(` • ${i}`);
506
+ }
507
+
508
+ // ---------------------------------------------------------------------------
509
+ // preâmbulo no CLAUDE.md
510
+ // ---------------------------------------------------------------------------
511
+
512
+ function preambleFile(configDir) {
513
+ return path.join(configDir, 'CLAUDE.md');
514
+ }
515
+
516
+ // Texto canônico do bloco, com marcadores, sem newline final.
517
+ function preambleText() {
518
+ const raw = readText(PREAMBLE_SRC);
519
+ if (raw === null) return null;
520
+ const lines = raw.replace(/\n+$/, '').split('\n');
521
+ if (lines[0].trim() !== PREAMBLE_OPEN || lines[lines.length - 1].trim() !== PREAMBLE_CLOSE) return null;
522
+ return lines.join('\n');
523
+ }
524
+
525
+ // Localiza o bloco: null (ausente), {start,end} ou {half:true}.
526
+ function readPreambleBlock(text) {
527
+ const all = text.split('\n');
528
+ let start = -1, end = -1;
529
+ for (let i = 0; i < all.length; i++) {
530
+ if (start < 0 && all[i].trim() === PREAMBLE_OPEN) start = i;
531
+ else if (start >= 0 && all[i].trim() === PREAMBLE_CLOSE) { end = i; break; }
532
+ }
533
+ if (start < 0 && end < 0) return null;
534
+ if (start < 0 || end < 0) return { half: true };
535
+ return { start, end, all };
536
+ }
537
+
538
+ function diffLines(oldText, newText) {
539
+ const a = oldText === null ? [] : oldText.split('\n');
540
+ const b = newText.split('\n');
541
+ const setB = new Set(b), setA = new Set(a);
542
+ const out = [];
543
+ for (const l of a) if (!setB.has(l)) out.push('- ' + l);
544
+ for (const l of b) if (!setA.has(l)) out.push('+ ' + l);
545
+ return out;
546
+ }
547
+
548
+ function askTty(question) {
549
+ let fd = null;
550
+ try {
551
+ fd = fs.openSync('/dev/tty', 'r+');
552
+ fs.writeSync(fd, question);
553
+ const buf = Buffer.alloc(64);
554
+ const n = fs.readSync(fd, buf, 0, buf.length, null);
555
+ return buf.slice(0, n).toString('utf8').trim().toLowerCase();
556
+ } catch {
557
+ return null;
558
+ } finally {
559
+ if (fd !== null) try { fs.closeSync(fd); } catch { /* nada */ }
560
+ }
561
+ }
562
+
563
+ // 'skipped' | 'unchanged' | 'written' | 'declined' | 'invalid'
564
+ function writePreamble(configDir, opts) {
565
+ if (opts.skip) return 'skipped';
566
+ const block = preambleText();
567
+ if (block === null) return 'invalid';
568
+
569
+ const file = preambleFile(configDir);
570
+ const cur = readText(file);
571
+ let next;
572
+ if (cur === null) {
573
+ next = block + '\n';
574
+ } else {
575
+ const loc = readPreambleBlock(cur);
576
+ if (loc && loc.half) return 'invalid';
577
+ if (loc) {
578
+ const all = loc.all.slice();
579
+ all.splice(loc.start, loc.end - loc.start + 1, ...block.split('\n'));
580
+ next = all.join('\n');
581
+ } else {
582
+ next = cur.replace(/\n*$/, '\n\n') + block + '\n';
583
+ }
584
+ }
585
+ if (cur !== null && next === cur) return 'unchanged';
586
+
587
+ if (!opts.yes) {
588
+ const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
589
+ const answer = interactive ? askTty(`\nEscrever o bloco do preâmbulo em ${file}? [s/N] `) : null;
590
+ if (!answer || !/^(s|sim|y|yes)$/.test(answer)) {
591
+ log('');
592
+ log(`Preâmbulo NÃO escrito em ${file}. Diff proposto:`);
593
+ for (const l of diffLines(cur, next).slice(0, 80)) log(' ' + l);
594
+ log(' (rode de novo com --yes para aplicar, ou --no-preamble para nunca perguntar)');
595
+ return 'declined';
596
+ }
597
+ }
598
+ if (cur !== null) fs.copyFileSync(file, `${file}.ll-skills.bak`);
599
+ writeTextAtomic(file, next);
600
+ return 'written';
601
+ }
602
+
603
+ // 'missing' | 'unchanged' | 'written' | 'invalid'
604
+ function stripPreamble(configDir) {
605
+ const file = preambleFile(configDir);
606
+ const cur = readText(file);
607
+ if (cur === null) return 'missing';
608
+ const loc = readPreambleBlock(cur);
609
+ if (!loc) return 'unchanged';
610
+ if (loc.half) return 'invalid';
611
+ const all = loc.all.slice();
612
+ all.splice(loc.start, loc.end - loc.start + 1);
613
+ while (all.length && all[0].trim() === '') all.shift();
614
+ const next = all.join('\n').replace(/\n{3,}/g, '\n\n');
615
+ fs.copyFileSync(file, `${file}.ll-skills.bak`);
616
+ writeTextAtomic(file, next);
617
+ return 'written';
618
+ }
619
+
388
620
  // ---------------------------------------------------------------------------
389
621
  // fluxos
390
622
  // ---------------------------------------------------------------------------
@@ -407,25 +639,44 @@ function install(args) {
407
639
  const plan = planFiles(PKG_ROOT);
408
640
  const files = copyFiles(configDir, plan);
409
641
  const skillNames = [...new Set(Object.keys(files).filter((r) => r.startsWith('skills/')).map((r) => r.split('/')[1]))].sort();
410
- const agentNames = Object.keys(files).filter((r) => r.startsWith('agents/')).map((r) => path.basename(r, '.md'));
642
+ const agentNames = Object.keys(files).filter((r) => r.startsWith('agents/')).map((r) => path.basename(r, '.md')).sort();
643
+ const hookNames = Object.keys(files).filter((r) => r.startsWith('hooks/')).map((r) => path.basename(r));
411
644
  log(`• ${skillNames.length} skills: ${skillNames.join(', ')}`);
412
- if (agentNames.length) log(`• ${agentNames.length} agente: ${agentNames.join(', ')}`);
645
+ if (agentNames.length) log(`• ${agentNames.length} agentes: ${agentNames.join(', ')}`);
646
+ if (hookNames.length) log(`• ${hookNames.length} hooks: ${hookNames.join(', ')}`);
413
647
 
414
- // 4. poda
648
+ // 4. poda (manifesto antigo + skills 1.x conhecidas)
415
649
  const pruned = pruneStale(configDir, oldManifest, files);
416
- if (pruned.length) log(`• ${pruned.length} arquivo(s) órfão(s) removido(s) da instalação anterior`);
650
+ if (pruned.length) log(`• ${pruned.length} caminho(s) de instalação anterior removido(s)`);
417
651
 
418
652
  // 5. estado
419
653
  writeState(configDir, files, detectInstallSource(PKG_ROOT));
420
654
 
421
- // 6. hook
422
- const hookResult = registerHook(configDir);
423
- if (hookResult === 'written') log('• hook de aviso de atualização registrado em settings.json');
424
- else if (hookResult === 'unchanged') log('• hook de aviso de atualização já registrado');
655
+ // 6. hooks
656
+ let hookResult = 'skipped';
657
+ if (!args.settings) {
658
+ log('• --no-settings: settings.json não foi alterado');
659
+ printHookSnippet(configDir);
660
+ } else {
661
+ hookResult = registerHooks(configDir);
662
+ if (hookResult === 'written') log('• 3 hooks registrados em settings.json (SessionStart, PreCompact)');
663
+ else if (hookResult === 'unchanged') log('• hooks já registrados em settings.json');
664
+ }
665
+
666
+ // 7. política sugerida (impressa, nunca escrita) + diagnóstico de limpeza
667
+ printPolicy();
668
+ diagnoseCleanup(configDir);
425
669
 
426
- // 7. cache antigo
670
+ // 8. cache antigo
427
671
  clearOldCache();
428
672
 
673
+ // 9. preâmbulo
674
+ const pre = writePreamble(configDir, { yes: args.yes, skip: !args.preamble });
675
+ if (pre === 'written') log(`• preâmbulo escrito em ${preambleFile(configDir)}`);
676
+ else if (pre === 'unchanged') log('• preâmbulo já atualizado');
677
+ else if (pre === 'skipped') log('• --no-preamble: CLAUDE.md não foi tocado');
678
+ else if (pre === 'invalid') log(`• AVISO: bloco do preâmbulo pela metade em ${preambleFile(configDir)}; corrija os marcadores à mão`);
679
+
429
680
  log('');
430
681
  if (legacy.manual.length) {
431
682
  log('Para concluir a remoção do plugin legado, rode dentro do Claude Code ou no terminal:');
@@ -433,11 +684,12 @@ function install(args) {
433
684
  log('');
434
685
  }
435
686
  if (hookResult === 'invalid') {
436
- printManualHookSnippet(configDir);
687
+ log(`AVISO: ${path.join(configDir, 'settings.json')} não é um JSON válido; não foi alterado.`);
688
+ printHookSnippet(configDir);
437
689
  log('Reinicie o Claude Code para que as skills ll-* apareçam.');
438
690
  process.exit(1);
439
691
  }
440
- log('Pronto. Reinicie o Claude Code para que as skills ll-* apareçam (ex.: /ll-decidir-antes).');
692
+ log('Pronto. Reinicie o Claude Code para que as skills ll-* apareçam (ex.: /ll-implement).');
441
693
  }
442
694
 
443
695
  function uninstall(args) {
@@ -448,6 +700,7 @@ function uninstall(args) {
448
700
  const rels = manifest && manifest.files ? Object.keys(manifest.files) : planFiles(PKG_ROOT).map((p) => p.rel);
449
701
  let n = 0;
450
702
  for (const rel of rels) if (removeManagedFile(configDir, rel)) n++;
703
+ for (const rel of KNOWN_LEGACY) if (removeManagedPath(configDir, rel)) n++;
451
704
  log(`• ${n} arquivo(s) removido(s)`);
452
705
 
453
706
  try {
@@ -456,9 +709,13 @@ function uninstall(args) {
456
709
  /* melhor esforço */
457
710
  }
458
711
 
459
- const hookResult = unregisterHook(configDir);
460
- if (hookResult === 'written') log('• hook removido de settings.json');
461
- else if (hookResult === 'invalid') log(`• AVISO: settings.json inválido; remova o hook "${HOOK_FILE}" manualmente`);
712
+ const hookResult = unregisterHooks(configDir);
713
+ if (hookResult === 'written') log('• hooks removidos de settings.json');
714
+ else if (hookResult === 'invalid') log('• AVISO: settings.json inválido; remova os hooks ll-* manualmente');
715
+
716
+ const pre = stripPreamble(configDir);
717
+ if (pre === 'written') log('• bloco do preâmbulo removido do CLAUDE.md');
718
+ else if (pre === 'invalid') log('• AVISO: bloco do preâmbulo pela metade no CLAUDE.md; remova à mão');
462
719
 
463
720
  clearOldCache();
464
721
  log('');
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // ll-precompact.js — ll-skills PreCompact hook. Stamps PROGRESS.md so the
4
+ // post-compaction session knows the disk is the truth. Reads stdin {cwd, trigger}.
5
+ // Silent without PROGRESS.md; any failure ends in silence with exit 0.
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { execFileSync } = require('child_process');
10
+
11
+ function main() {
12
+ let input = {};
13
+ try { input = JSON.parse(fs.readFileSync(0, 'utf8')); } catch { input = {}; }
14
+ const cwd = typeof input.cwd === 'string' && input.cwd ? input.cwd : process.cwd();
15
+ const trigger = typeof input.trigger === 'string' && input.trigger ? input.trigger : 'auto';
16
+
17
+ const file = path.join(cwd, 'PROGRESS.md');
18
+ let body;
19
+ try { body = fs.readFileSync(file, 'utf8'); } catch { return; }
20
+
21
+ let phase = 'NN';
22
+ const m = /^<!--\s*ll-state\s*-->\s*$[\s\S]*?^phase:\s*(\S+)\s*$/m.exec(body);
23
+ if (m) phase = m[1].replace(/^["']|["']$/g, '');
24
+
25
+ let head = '';
26
+ try {
27
+ head = execFileSync('git', ['rev-parse', '--short=7', 'HEAD'],
28
+ { cwd, timeout: 3000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
29
+ } catch { head = ''; }
30
+
31
+ const stamp = new Date().toISOString().replace(/\.\d+Z$/, 'Z');
32
+ const line = '- [compaction ' + stamp + ' · ' + trigger + ' · HEAD ' + (head || 'unknown') + '] '
33
+ + 're-read phases/' + phase + '/PLAN.md and the milestone board before continuing.';
34
+
35
+ const out = body.split('\n');
36
+ const at = out.findIndex((l) => /^## Epilogue/.test(l));
37
+ if (at >= 0) out.splice(at, 0, line, '');
38
+ else {
39
+ while (out.length && out[out.length - 1].trim() === '') out.pop();
40
+ out.push(line, '');
41
+ }
42
+ const tmp = file + '.ll-tmp-' + process.pid;
43
+ fs.writeFileSync(tmp, out.join('\n'));
44
+ fs.renameSync(tmp, file);
45
+ process.stdout.write(JSON.stringify({ systemMessage: '[ll-skills] compaction marked in PROGRESS.md' }) + '\n');
46
+ }
47
+
48
+ try { main(); } catch { /* silence */ }
49
+ process.exit(0);
@@ -129,7 +129,7 @@ function reader(installed) {
129
129
  if (cache && cache.update_available && cache.installed === installed && cache.latest) {
130
130
  process.stdout.write(
131
131
  JSON.stringify({
132
- systemMessage: `ll-skills desatualizado (instalado ${installed}, disponível ${cache.latest}) — rode /ll-atualizar`,
132
+ systemMessage: `ll-skills desatualizado (instalado ${installed}, disponível ${cache.latest}) — rode /ll-update`,
133
133
  }) + '\n'
134
134
  );
135
135
  }