ganbatte-os 0.2.31 → 0.2.33

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.
@@ -30,9 +30,74 @@ function readPackageVersion() {
30
30
 
31
31
  const VERSION = readPackageVersion();
32
32
  const UPSTREAM_REMOTE = 'upstream';
33
- const UPSTREAM_BRANCH = 'main';
33
+ const DEFAULT_UPSTREAM_BRANCH = 'main';
34
34
  const LOCAL_DIR = '.gos-local';
35
35
  const MANIFEST_PATH = '.gos/manifests/gos-install-manifest.json';
36
+ const CONFIG_PATH = '.gos/config.json';
37
+ const CORRECT_UPSTREAM_URL = 'git@github-adriano:adrianomorais-ganbatte/g-os.git';
38
+ const CORRECT_UPSTREAM_URL_HTTPS = 'https://github.com/adrianomorais-ganbatte/g-os.git';
39
+ const STASH_LABEL = 'gos-update-auto-stash';
40
+
41
+ /**
42
+ * Resolve a development branch a partir de .gos/config.json.
43
+ * Fallback: 'main'. Permite override via env GOS_UPSTREAM_BRANCH.
44
+ */
45
+ function resolveUpstreamBranch(root) {
46
+ if (process.env.GOS_UPSTREAM_BRANCH) return process.env.GOS_UPSTREAM_BRANCH;
47
+ const cfg = readJson(path.join(root, CONFIG_PATH));
48
+ return cfg?.defaultBranches?.development || cfg?.defaultBranches?.production || DEFAULT_UPSTREAM_BRANCH;
49
+ }
50
+
51
+ /**
52
+ * Detecta se este workspace é o framework G-OS em si (fork/clone) ou um projeto
53
+ * consumidor que apenas instalou o framework via `gos install`.
54
+ * - 'framework': package.json#name === 'ganbatte-os' AND .git no root
55
+ * - 'consumer': qualquer outro caso
56
+ */
57
+ function detectMode(root) {
58
+ const pkg = readJson(path.join(root, 'package.json'), {});
59
+ const isGosPackage = pkg.name === 'ganbatte-os';
60
+ const hasGit = pathExists(path.join(root, '.git'));
61
+ return isGosPackage && hasGit ? 'framework' : 'consumer';
62
+ }
63
+
64
+ /**
65
+ * Valida que o remote upstream existe E responde. Não modifica nada.
66
+ * Retorna { ok, remoteUrl, error }.
67
+ */
68
+ function validateUpstream(root) {
69
+ const remotes = gitCapture(['remote'], { cwd: root });
70
+ if (!remotes.split(/\r?\n/).includes(UPSTREAM_REMOTE)) {
71
+ return { ok: false, error: `remote "${UPSTREAM_REMOTE}" não configurado` };
72
+ }
73
+ const url = gitCapture(['remote', 'get-url', UPSTREAM_REMOTE], { cwd: root });
74
+ // Detecta URL antiga incorreta (ganbatte-os.git em vez de g-os.git)
75
+ if (/ganbatte-os\.git/.test(url) && !/g-os\.git/.test(url)) {
76
+ return { ok: false, remoteUrl: url, error: 'URL do upstream parece estar com nome antigo (ganbatte-os.git)' };
77
+ }
78
+ // Ping no remoto sem stashar nada
79
+ try {
80
+ execFileSync('git', ['ls-remote', '--quiet', '--exit-code', UPSTREAM_REMOTE, 'HEAD'], {
81
+ cwd: root, stdio: 'pipe', encoding: 'utf8',
82
+ });
83
+ return { ok: true, remoteUrl: url };
84
+ } catch (e) {
85
+ return { ok: false, remoteUrl: url, error: `não foi possível alcançar ${url}` };
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Lista todos stashes auto-criados pelo gos-update.
91
+ * Retorna [{ ref, branch, label, dateIso }].
92
+ */
93
+ function listGosStashes(root) {
94
+ const out = gitCapture(['stash', 'list', '--format=%gd|%gs|%ci'], { cwd: root });
95
+ if (!out) return [];
96
+ return out.split(/\r?\n/).filter(Boolean).map(line => {
97
+ const [ref, subject, dateIso] = line.split('|');
98
+ return { ref, subject, dateIso };
99
+ }).filter(s => s.subject && s.subject.includes(STASH_LABEL));
100
+ }
36
101
 
37
102
  // ---------------------------------------------------------------------------
38
103
  // Utilitarios
@@ -347,57 +412,219 @@ function cmdInstall(args) {
347
412
 
348
413
  function cmdUpdate(root, args) {
349
414
  const skipStash = args.includes('--no-stash');
415
+ const branch = resolveUpstreamBranch(root);
416
+ const mode = detectMode(root);
350
417
 
351
418
  log('Atualizando workspace ganbatte-os...');
352
419
 
353
- // 1. Verificar remote upstream
354
- const remotes = gitCapture(['remote'], { cwd: root });
355
- if (!remotes.includes(UPSTREAM_REMOTE)) {
356
- fail(`Remote "${UPSTREAM_REMOTE}" nao encontrado.`);
357
- info(`Adicione com: git remote add ${UPSTREAM_REMOTE} https://github.com/adrianomorais-ganbatte/ganbatte-os.git`);
420
+ // 0. Modo: bloquear se for projeto consumidor (não fork do framework).
421
+ if (mode === 'consumer') {
422
+ fail('Este workspace é um projeto consumidor, não o repositório do framework G-OS.');
423
+ info('Para atualizar o framework dentro do seu projeto, use:');
424
+ info(' gos install --force');
425
+ info('Ou (se você instalou via npm install):');
426
+ info(' npm install ganbatte-os@latest');
427
+ info('');
428
+ info('`gos update` é apenas para forks/clones do repositório g-os.');
358
429
  process.exit(1);
359
430
  }
360
431
 
361
- // 2. Checar se ha mudancas locais e fazer stash
362
- const status = gitCapture(['status', '--porcelain'], { cwd: root });
363
- let didStash = false;
364
- if (status && !skipStash) {
365
- log('Mudancas locais detectadas. Fazendo stash...');
366
- git(['stash', 'push', '-m', 'gos-update-auto-stash'], { cwd: root });
367
- didStash = true;
368
- ok('Stash criado.');
432
+ // 1. Validar upstream ANTES de qualquer modificação local (stash, etc).
433
+ const upstream = validateUpstream(root);
434
+ if (!upstream.ok) {
435
+ fail(`Upstream inválido: ${upstream.error}`);
436
+ if (upstream.remoteUrl) info(`URL atual: ${upstream.remoteUrl}`);
437
+ info('Corrija com:');
438
+ info(` git remote set-url ${UPSTREAM_REMOTE} ${CORRECT_UPSTREAM_URL}`);
439
+ info(` (ou via HTTPS: ${CORRECT_UPSTREAM_URL_HTTPS})`);
440
+ info('');
441
+ info(`Depois rode: npm run gos:update`);
442
+ process.exit(1);
369
443
  }
370
444
 
371
- // 3. Fetch upstream
372
- log(`Buscando atualizacoes de ${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH}...`);
373
- git(['fetch', UPSTREAM_REMOTE, UPSTREAM_BRANCH], { cwd: root });
445
+ // 2. Dry-run fetch (sem stashar): se nada mudou, sai cedo.
446
+ log(`Verificando ${UPSTREAM_REMOTE}/${branch}...`);
447
+ try {
448
+ git(['fetch', UPSTREAM_REMOTE, branch], { cwd: root, quiet: true });
449
+ } catch (e) {
450
+ fail(`Fetch de ${UPSTREAM_REMOTE}/${branch} falhou.`);
451
+ info('Verifique sua conexão e credenciais SSH/HTTPS. Nada foi modificado localmente.');
452
+ process.exit(1);
453
+ }
374
454
 
375
- // 4. Checar se ha commits novos
376
455
  const behind = gitCapture(
377
- ['rev-list', '--count', `HEAD..${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH}`],
456
+ ['rev-list', '--count', `HEAD..${UPSTREAM_REMOTE}/${branch}`],
378
457
  { cwd: root }
379
458
  );
380
- if (behind === '0') {
381
- ok('Workspace ja esta atualizado.');
382
- if (didStash) git(['stash', 'pop'], { cwd: root });
459
+ if (behind === '0' || behind === '') {
460
+ ok(`Workspace ja esta atualizado com ${UPSTREAM_REMOTE}/${branch}.`);
383
461
  return;
384
462
  }
385
463
 
464
+ // 3. AGORA é seguro stashar (upstream validado + commits novos confirmados).
465
+ // Importante: stash deve EXCLUIR paths do framework — eles serão sobrescritos
466
+ // pelo merge de qualquer forma, e stashar a si próprio (gos-cli.js) cria um
467
+ // loop em que o stash reverte a versão atualizada do CLI no working tree.
468
+ const status = gitCapture(['status', '--porcelain'], { cwd: root });
469
+ // Detectar se há mudanças além dos paths framework
470
+ const userModifiedLines = status
471
+ .split(/\r?\n/)
472
+ .filter(Boolean)
473
+ .filter(line => {
474
+ const file = line.slice(3); // 2-char status flag + space
475
+ // Exclui paths gerados/managed pelo framework
476
+ const FRAMEWORK_PREFIXES = [
477
+ '.gos/', '.claude/', '.qwen/', '.gemini/', '.cursor/', '.agents/',
478
+ '.kilocode/', '.antigravity/', '.opencode/', '.codex/',
479
+ ];
480
+ return !FRAMEWORK_PREFIXES.some(p => file.startsWith(p));
481
+ });
482
+ let didStash = false;
483
+ if (userModifiedLines.length > 0 && !skipStash) {
484
+ log(`Mudancas locais do usuario detectadas (${userModifiedLines.length}). Fazendo stash...`);
485
+ const stashLabel = `${STASH_LABEL} ${new Date().toISOString()}`;
486
+ // Stash com pathspec excluindo paths do framework — assim o stash captura
487
+ // apenas mudanças do usuário, e o working tree mantém a versão atualizada
488
+ // dos arquivos do framework (incluindo este próprio CLI).
489
+ const stashArgs = [
490
+ 'stash', 'push', '-m', stashLabel, '--',
491
+ ':(exclude,top).gos',
492
+ ':(exclude,top).claude',
493
+ ':(exclude,top).qwen',
494
+ ':(exclude,top).gemini',
495
+ ':(exclude,top).cursor',
496
+ ':(exclude,top).agents',
497
+ ':(exclude,top).kilocode',
498
+ ':(exclude,top).antigravity',
499
+ ':(exclude,top).opencode',
500
+ ':(exclude,top).codex',
501
+ '.',
502
+ ];
503
+ git(stashArgs, { cwd: root });
504
+ didStash = true;
505
+ ok(`Stash criado (apenas mudancas do usuario): ${stashLabel}`);
506
+ } else if (status) {
507
+ info(`Mudancas locais detectadas apenas em paths do framework — serao sobrescritas pelo merge.`);
508
+ }
509
+
386
510
  const commitBefore = gitCapture(['rev-parse', '--short', 'HEAD'], { cwd: root });
387
511
 
388
512
  // 5. Merge
389
513
  log(`${behind} commit(s) novo(s). Fazendo merge...`);
390
514
  const manifest = getManifest(root);
391
515
  const frameworkPaths = manifest.frameworkManaged || [];
516
+ const allowUnrelated = args.includes('--allow-unrelated');
517
+ const clobberUntracked = args.includes('--clobber-untracked');
518
+
519
+ // Paths que podemos clobberar com segurança:
520
+ // - IDE adapters: regenerados por sync:ides
521
+ // - .gos/: framework directory, sempre vem do upstream
522
+ const FRAMEWORK_GENERATED_PREFIXES = [
523
+ '.claude/', '.qwen/', '.gemini/', '.cursor/', '.agents/',
524
+ '.kilocode/', '.antigravity/', '.opencode/', '.codex/',
525
+ '.gos/',
526
+ ];
527
+ const isGenerated = (p) => FRAMEWORK_GENERATED_PREFIXES.some(prefix => p.startsWith(prefix));
528
+
529
+ const mergeArgs = ['merge', `${UPSTREAM_REMOTE}/${branch}`, '--no-edit'];
530
+ if (allowUnrelated) mergeArgs.push('--allow-unrelated-histories');
531
+
532
+ // Função tentando o merge, capturando stderr — chamada mais de uma vez se houver retry
533
+ function tryMerge() {
534
+ try {
535
+ execFileSync('git', mergeArgs, { cwd: root, stdio: 'pipe', encoding: 'utf8' });
536
+ return { ok: true };
537
+ } catch (e) {
538
+ return { ok: false, err: ((e.stderr || '') + (e.stdout || '')).toString() };
539
+ }
540
+ }
392
541
 
393
- try {
394
- git(['merge', `${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH}`, '--no-edit'], { cwd: root, quiet: true });
542
+ let mergeErr = '';
543
+ let attempt = tryMerge();
544
+ if (attempt.ok) {
395
545
  ok('Merge concluido sem conflitos.');
396
- } catch {
397
- // Checar conflitos
546
+ } else {
547
+ mergeErr = attempt.err;
548
+
549
+ // Auto-resolução: untracked files que upstream quer escrever, mas só se
550
+ // todos forem paths gerados pelo framework (sync:ides regenera).
551
+ const untrackedMatch = mergeErr.match(/untracked working tree files would be overwritten by merge:\s*([\s\S]+?)(?:Please move or remove|$)/i);
552
+ if (untrackedMatch) {
553
+ const conflictingPaths = untrackedMatch[1]
554
+ .split(/\r?\n/)
555
+ .map(s => s.trim())
556
+ .filter(Boolean)
557
+ // git termina com "Aborting", "Merge with strategy ort failed.", "Please move or remove..."
558
+ // Caminhos reais sempre contêm "." (extensão ou dotfile) OU "/" (subdir).
559
+ .filter(s => (s.includes('.') || s.includes('/')) && !/^(Aborting|Merge with|Please move)/i.test(s));
560
+ const generated = conflictingPaths.filter(isGenerated);
561
+ const userOwned = conflictingPaths.filter(p => !isGenerated(p));
562
+
563
+ if (clobberUntracked && userOwned.length === 0) {
564
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
565
+ log(`${generated.length} arquivo(s) untracked em paths regenerados — movendo para .bak.${ts}/`);
566
+ for (const file of generated) {
567
+ const src = path.join(root, file);
568
+ const dst = path.join(root, `${file}.bak.${ts}`);
569
+ if (pathExists(src)) {
570
+ ensureDir(path.dirname(dst));
571
+ fs.renameSync(src, dst);
572
+ }
573
+ }
574
+ ok(`Movidos ${generated.length} arquivo(s). Retentando merge...`);
575
+ attempt = tryMerge();
576
+ if (attempt.ok) {
577
+ ok('Merge concluido apos clobber.');
578
+ mergeErr = '';
579
+ } else {
580
+ mergeErr = attempt.err;
581
+ }
582
+ } else if (userOwned.length === 0 && !clobberUntracked) {
583
+ fail(`Merge abortou: ${conflictingPaths.length} arquivo(s) untracked seriam sobrescritos.`);
584
+ info('Todos os arquivos afetados estão em paths gerados pelo framework (regenerados por sync:ides):');
585
+ for (const p of generated.slice(0, 10)) info(` - ${p}`);
586
+ if (generated.length > 10) info(` ... mais ${generated.length - 10}`);
587
+ info('');
588
+ info('Para auto-mover esses arquivos para .bak.<timestamp> antes do merge:');
589
+ info(` npm run gos:update -- --clobber-untracked${allowUnrelated ? ' --allow-unrelated' : ''}`);
590
+ if (didStash) info('Stash preservado. Rode: git stash pop quando terminar.');
591
+ process.exit(1);
592
+ } else {
593
+ // Misto — tem arquivos do usuário também → não clobberamos nada
594
+ fail(`Merge abortou: arquivos untracked seriam sobrescritos.`);
595
+ info(`${userOwned.length} arquivo(s) NÃO gerados pelo framework (revisar antes):`);
596
+ for (const p of userOwned.slice(0, 10)) info(` - ${p}`);
597
+ info('Mova ou versione esses arquivos manualmente; depois retente.');
598
+ if (didStash) info('Stash preservado.');
599
+ process.exit(1);
600
+ }
601
+ }
602
+ }
603
+
604
+ if (!attempt.ok) {
605
+
606
+ // Detecta histórias não relacionadas (comum em workspaces criados via `gos install`)
607
+ if (/unrelated histories/i.test(mergeErr) && !allowUnrelated) {
608
+ fail('Merge abortou: histórias não relacionadas entre HEAD e upstream.');
609
+ info('Isto é comum quando o workspace foi bootstrappado via `gos install`');
610
+ info('e nunca compartilhou commits com o repositório do framework.');
611
+ info('');
612
+ info('Para forçar o merge unindo as histórias (recomendado neste caso):');
613
+ info(` npm run gos:update -- --allow-unrelated`);
614
+ info('');
615
+ info('Alternativa segura (sobrescreve apenas .gos/ preservando seus arquivos):');
616
+ info(' gos install --force');
617
+ if (didStash) info('Stash preservado. Rode: git stash pop quando resolver.');
618
+ process.exit(1);
619
+ }
620
+
621
+ // Checar conflitos de arquivo
398
622
  const conflictFiles = gitCapture(['diff', '--name-only', '--diff-filter=U'], { cwd: root });
399
623
  if (!conflictFiles) {
400
- fail('Merge falhou por motivo desconhecido.');
624
+ fail('Merge falhou. Erro do git:');
625
+ for (const line of mergeErr.split(/\r?\n/).filter(Boolean).slice(0, 6)) {
626
+ console.log(` ${line}`);
627
+ }
401
628
  if (didStash) info('Stash preservado. Rode: git stash pop');
402
629
  process.exit(1);
403
630
  }
@@ -600,12 +827,46 @@ function cmdDoctor(root) {
600
827
  check(`Diretorio ${LOCAL_DIR}/ existe`, hasLocalDir);
601
828
  }
602
829
 
603
- // 10. Report
830
+ // 10. Mode + upstream sanity (warnings, não falhas)
831
+ const mode = detectMode(root);
832
+ const branch = resolveUpstreamBranch(root);
833
+ const isCi = Boolean(process.env.CI || process.env.GITHUB_ACTIONS);
834
+ if (mode === 'framework') {
835
+ if (isCi) {
836
+ info('Upstream check ignorado em CI (sem credenciais para git ls-remote)');
837
+ } else {
838
+ const upstream = validateUpstream(root);
839
+ if (upstream.ok) {
840
+ ok(`Upstream alcançável (branch: ${branch})`);
841
+ } else {
842
+ warn(`Upstream inválido: ${upstream.error}`);
843
+ if (upstream.remoteUrl) info(`URL atual: ${upstream.remoteUrl}`);
844
+ info(`Corrija com: git remote set-url ${UPSTREAM_REMOTE} ${CORRECT_UPSTREAM_URL}`);
845
+ issues++;
846
+ }
847
+ }
848
+ checks++;
849
+ }
850
+
851
+ // 11. Stashes acumulados de updates falhos
852
+ const goshStashes = listGosStashes(root);
853
+ if (goshStashes.length > 1) {
854
+ warn(`${goshStashes.length} stashes do gos-update acumulados (sintoma de updates falhos).`);
855
+ info('Resgate com: npm run gos:rescue');
856
+ issues++;
857
+ } else if (goshStashes.length === 1) {
858
+ info(`1 stash do gos-update presente: ${goshStashes[0].ref}`);
859
+ }
860
+ checks++;
861
+
862
+ // 12. Report
604
863
  const updateLog = readJson(path.join(root, LOCAL_DIR, 'update-log.json'));
605
864
  const installLogData = readJson(path.join(root, LOCAL_DIR, 'install-log.json'));
606
865
 
607
866
  console.log('');
608
867
  console.log(` Versao: ${pkg ? pkg.version || VERSION : VERSION}`);
868
+ console.log(` Modo: ${mode === 'framework' ? 'framework workspace' : 'projeto consumidor'}`);
869
+ console.log(` Branch dev: ${branch}`);
609
870
  console.log(` Inicializado: ${installLogData ? installLogData.initializedAt : 'N/A'}`);
610
871
  console.log(` Ultimo update: ${updateLog ? updateLog.lastUpdate : 'N/A'}`);
611
872
  console.log(` Node.js: ${process.version}`);
@@ -620,6 +881,60 @@ function cmdDoctor(root) {
620
881
  process.exit(issues > 0 ? 1 : 0);
621
882
  }
622
883
 
884
+ // ---------------------------------------------------------------------------
885
+ // gos rescue — recuperar stashes acumulados de updates falhos
886
+ // ---------------------------------------------------------------------------
887
+
888
+ function cmdRescue(root, args) {
889
+ log('Buscando stashes do gos-update...');
890
+ const stashes = listGosStashes(root);
891
+ if (stashes.length === 0) {
892
+ ok('Nenhum stash do gos-update encontrado.');
893
+ return;
894
+ }
895
+
896
+ console.log('');
897
+ console.log(` ${stashes.length} stash(es) auto-criado(s) pelo gos-update:`);
898
+ console.log('');
899
+ for (const s of stashes) {
900
+ console.log(` ${s.ref} (${s.dateIso})`);
901
+ const stat = gitCapture(['stash', 'show', '--stat', s.ref], { cwd: root });
902
+ if (stat) {
903
+ for (const line of stat.split(/\r?\n/).slice(0, 5)) {
904
+ if (line.trim()) console.log(` ${line}`);
905
+ }
906
+ }
907
+ console.log('');
908
+ }
909
+
910
+ if (args.includes('--drop-all')) {
911
+ warn(`Removendo todos os ${stashes.length} stashes...`);
912
+ // Remove em ordem reversa porque os índices mudam
913
+ for (let i = stashes.length - 1; i >= 0; i--) {
914
+ git(['stash', 'drop', stashes[i].ref], { cwd: root, quiet: true });
915
+ }
916
+ ok('Todos os stashes do gos-update foram removidos.');
917
+ return;
918
+ }
919
+
920
+ if (args.includes('--pop-latest')) {
921
+ log(`Aplicando ${stashes[0].ref}...`);
922
+ try {
923
+ git(['stash', 'pop', stashes[0].ref], { cwd: root });
924
+ ok('Stash aplicado.');
925
+ } catch {
926
+ warn('Conflito ao aplicar. Resolva manualmente e rode `git stash drop` quando terminar.');
927
+ }
928
+ return;
929
+ }
930
+
931
+ info('Comandos disponíveis:');
932
+ info(' npm run gos:rescue -- --pop-latest # aplica o stash mais recente');
933
+ info(' npm run gos:rescue -- --drop-all # remove todos (após revisão)');
934
+ info(' git stash pop <ref> # aplica stash específico');
935
+ info(' git stash drop <ref> # remove stash específico');
936
+ }
937
+
623
938
  // ---------------------------------------------------------------------------
624
939
  // gos version
625
940
  // ---------------------------------------------------------------------------
@@ -627,30 +942,43 @@ function cmdDoctor(root) {
627
942
  function cmdVersion(root) {
628
943
  const pkg = readJson(path.join(root, 'package.json'), {});
629
944
  const localVersion = pkg.version || VERSION;
945
+ const mode = detectMode(root);
946
+ const branch = resolveUpstreamBranch(root);
630
947
 
631
948
  console.log(`ganbatte-os v${localVersion}`);
949
+ console.log(` modo: ${mode === 'framework' ? 'framework workspace (fork/clone do g-os)' : 'projeto consumidor'}`);
632
950
 
633
- // Checar se ha commits novos no upstream
634
- const remotes = gitCapture(['remote'], { cwd: root });
635
- if (!remotes.includes(UPSTREAM_REMOTE)) {
636
- info(`Remote "${UPSTREAM_REMOTE}" nao configurado. Nao foi possivel checar atualizacoes.`);
951
+ if (mode === 'consumer') {
952
+ info('Para checar a última versão publicada do framework:');
953
+ info(' npm view ganbatte-os version');
954
+ info('Para atualizar o framework dentro do seu projeto:');
955
+ info(' gos install --force');
956
+ return;
957
+ }
958
+
959
+ // Modo framework: checar commits novos no upstream
960
+ const upstream = validateUpstream(root);
961
+ if (!upstream.ok) {
962
+ warn(`Upstream inválido: ${upstream.error}`);
963
+ if (upstream.remoteUrl) info(`URL atual: ${upstream.remoteUrl}`);
964
+ info(`Corrija com: git remote set-url ${UPSTREAM_REMOTE} ${CORRECT_UPSTREAM_URL}`);
637
965
  return;
638
966
  }
639
967
 
640
968
  try {
641
- execFileSync('git', ['fetch', UPSTREAM_REMOTE, UPSTREAM_BRANCH], {
969
+ execFileSync('git', ['fetch', UPSTREAM_REMOTE, branch], {
642
970
  encoding: 'utf8', stdio: 'pipe', cwd: root
643
971
  });
644
972
  const behind = gitCapture(
645
- ['rev-list', '--count', `HEAD..${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH}`],
973
+ ['rev-list', '--count', `HEAD..${UPSTREAM_REMOTE}/${branch}`],
646
974
  { cwd: root }
647
975
  );
648
976
 
649
977
  if (behind && behind !== '0') {
650
- console.log(`\n ${behind} commit(s) novo(s) disponivel(is).`);
978
+ console.log(`\n ${behind} commit(s) novo(s) em ${UPSTREAM_REMOTE}/${branch}.`);
651
979
  console.log(' Atualize com: npm run gos:update\n');
652
980
  } else {
653
- ok('Voce esta na versao mais recente.');
981
+ ok(`Voce esta na versao mais recente de ${UPSTREAM_REMOTE}/${branch}.`);
654
982
  }
655
983
  } catch {
656
984
  warn('Nao foi possivel checar atualizacoes (sem conexao?).');
@@ -683,6 +1011,10 @@ function main() {
683
1011
  cmdDoctor(root);
684
1012
  break;
685
1013
 
1014
+ case 'rescue':
1015
+ cmdRescue(root, args.slice(1));
1016
+ break;
1017
+
686
1018
  case 'version':
687
1019
  case '-v':
688
1020
  case '--version':
@@ -698,14 +1030,24 @@ ganbatte-os CLI v${VERSION}
698
1030
  Comandos:
699
1031
  gos install Instalar G-OS no diretorio atual (via npx ou global)
700
1032
  gos init Setup pos-clone (remote, dirs, IDEs)
701
- gos update Atualizar do upstream/main
1033
+ gos update Atualizar do upstream (apenas em fork/clone do g-os)
1034
+ gos rescue Listar/recuperar stashes acumulados de updates falhos
702
1035
  gos doctor Validar integridade do workspace
703
- gos version Mostrar versao e checar atualizacoes
1036
+ gos version Mostrar versao, modo (framework/consumer) e atualizacoes
704
1037
  gos help Exibir esta ajuda
705
1038
 
706
1039
  Flags:
707
- --force Sobrescrever arquivos existentes (install/init)
708
- --no-stash Nao fazer stash automatico (update)
1040
+ --force Sobrescrever arquivos existentes (install/init)
1041
+ --no-stash Nao fazer stash automatico (update)
1042
+ --allow-unrelated Permitir merge de histórias não relacionadas (update)
1043
+ --clobber-untracked Mover untracked files (em paths regenerados) para
1044
+ .bak.<timestamp> antes do merge (update)
1045
+ --pop-latest Aplicar stash mais recente (rescue)
1046
+ --drop-all Remover todos os stashes do gos-update (rescue)
1047
+
1048
+ Env:
1049
+ GOS_UPSTREAM_BRANCH Override da branch a ser pulled (default lê de
1050
+ .gos/config.json#defaultBranches.development)
709
1051
 
710
1052
  Exemplos:
711
1053
  npx -p ganbatte-os gos install
package/README.md CHANGED
@@ -43,30 +43,149 @@ Após rodar o install, o framework criará uma estrutura limpa na sua raiz:
43
43
 
44
44
  ### 3. Comandos do Workspace
45
45
 
46
- A partir da raiz do seu projeto, você pode gerenciar o framework:
47
-
48
- | Comando | O que faz |
49
- |---------|-----------|
50
- | `npm run gos:init` | Setup pos-clone (remote, dirs, IDEs, sync framework pai) |
51
- | `npm run gos:update` | Fetch upstream + merge + re-sync IDEs + sync framework pai |
52
- | `npm run gos:doctor` | Valida integridade do workspace e IDEs |
53
- | `npm run gos:version` | Mostra versao e checa atualizacoes |
54
- | `npm run sync:ides` | Regenera adapters para Claude, Gemini, Cursor e outras |
55
- | `npm run check:ides` | Valida compatibilidade dos IDE adapters |
56
- | `npm run clickup` | CLI ClickUp (tarefas, sprints, status) |
57
- | `npm run doctor` | Alias direto para gos:doctor |
58
-
59
- ### Via CLI global (`gos`)
60
-
61
- Apos `npm install -g ganbatte-os`:
62
-
63
- | Comando | O que faz |
64
- |---------|-----------|
65
- | `gos install` | Instala framework em diretorio novo |
66
- | `gos init` | Inicializa workspace existente |
67
- | `gos update` | Atualiza framework |
68
- | `gos doctor` | Health-check |
69
- | `gos version` | Versao instalada |
46
+ | Comando | Equivalente CLI | O que faz |
47
+ |---------|-----------------|-----------|
48
+ | `npm run gos:init` | `gos init` | Setup pos-clone: configura `upstream`, cria `.gos-local/`, gera IDE adapters |
49
+ | `npm run gos:update` | `gos update` | Fetch + merge do upstream (**apenas em fork/clone do g-os**) |
50
+ | `npm run gos:doctor` | `gos doctor` | Health-check (42+ checks: skills, agents, IDE adapters, upstream alcançável, stashes acumulados, modo workspace) |
51
+ | `npm run gos:version` | `gos version` | Versão instalada, modo do workspace e atualizações pendentes |
52
+ | `npm run gos:rescue` | `gos rescue` | Lista/recupera stashes acumulados de updates falhos |
53
+ | `npm run sync:ides` | | Regenera apenas os IDE adapters (`.claude/`, `.qwen/`, `.gemini/`, `.cursor/`, `.agents/`) |
54
+ | `npm run check:ides` | | Valida que adapters estão consistentes com `.gos/skills/registry.json` |
55
+ | `npm run clickup` | | CLI ClickUp |
56
+
57
+ ## Atualizar o ganbatte-os
58
+
59
+ Existem **três níveis distintos** de versão. Saiba qual atualizar antes de rodar qualquer comando.
60
+
61
+ ### Descobrir em que cenário você está
62
+
63
+ ```bash
64
+ npm run gos:version
65
+ # imprime: versão local + modo (framework workspace | projeto consumidor)
66
+ ```
67
+
68
+ | Modo detectado | Significa | Como atualizar |
69
+ |----------------|-----------|----------------|
70
+ | `framework workspace` | Você clonou/forkou o repo `g-os` para contribuir | `npm run gos:update` |
71
+ | `projeto consumidor` | Seu projeto usa o G-OS (rodou `gos install` aqui) | `gos install --force` |
72
+ | (CLI global) | O comando `gos` instalado via `npm install -g` | `npm install -g ganbatte-os@latest` |
73
+
74
+ ### Nível 1 — CLI global (`gos`)
75
+
76
+ ```bash
77
+ # Versão atual instalada vs publicada no registry:
78
+ npm list -g ganbatte-os
79
+ npm view ganbatte-os version
80
+
81
+ # Atualizar:
82
+ npm install -g ganbatte-os@latest
83
+ ```
84
+
85
+ ### Nível 2 — Projeto consumidor
86
+
87
+ Seu projeto NÃO é fork do G-OS, mas usa o framework via `gos install`. O `.gos/` mora dentro do seu repo.
88
+
89
+ ```bash
90
+ # Pré-checagem (uma vez):
91
+ git remote -v
92
+ # Se houver "upstream" apontando para g-os.git, REMOVA:
93
+ # git remote remove upstream
94
+ # (essa configuração só faz sentido em fork do framework)
95
+
96
+ # Atualizar o framework dentro do seu projeto:
97
+ gos install --force
98
+ # Sobrescreve .gos/ com a última versão do pacote ganbatte-os global,
99
+ # preservando seus arquivos (packages/, docs/, .gos-local/).
100
+ ```
101
+
102
+ > [!WARNING]
103
+ > NÃO use `npm run gos:update` em projeto consumidor. O CLI agora detecta esse cenário e aborta com instruções, mas em versões antigas ele tentava `git fetch upstream main` e quebrava de forma confusa.
104
+
105
+ ### Nível 3 — Framework workspace (fork/clone do g-os)
106
+
107
+ Você está contribuindo com o próprio framework.
108
+
109
+ ```bash
110
+ # Pré-checagem (sempre antes de update):
111
+ npm run gos:doctor
112
+ # Valida 42+ pontos. Aborta se upstream estiver com URL quebrada
113
+ # ou se houver stashes acumulados de updates anteriores falhos.
114
+
115
+ # Ver quantos commits faltam:
116
+ npm run gos:version
117
+
118
+ # Aplicar (lê dev branch do .gos/config.json#defaultBranches.development):
119
+ npm run gos:update
120
+ ```
121
+
122
+ `gos:update` agora é **fail-safe**:
123
+
124
+ 1. Bloqueia se modo for `consumer`
125
+ 2. Valida `upstream` reachable (`git ls-remote`) ANTES de stashar
126
+ 3. Lê branch de desenvolvimento do `.gos/config.json` (não hardcoded)
127
+ 4. Faz fetch primeiro; se nada mudou, sai sem stash
128
+ 5. Stash recebe label timestamped único
129
+ 6. Auto-resolve conflitos em arquivos do framework (`frameworkManaged` no manifest); aborta em conflitos do usuário
130
+
131
+ Override de branch (debug):
132
+
133
+ ```bash
134
+ GOS_UPSTREAM_BRANCH=beta npm run gos:update
135
+ ```
136
+
137
+ #### Caso especial: "histórias não relacionadas"
138
+
139
+ Workspaces criados via `gos install` no passado podem ter `.git` próprio sem ancestor comum com o repo do framework. Nesse caso o merge falha com `fatal: refusing to merge unrelated histories`. O CLI detecta e instrui:
140
+
141
+ ```bash
142
+ npm run gos:update -- --allow-unrelated
143
+ ```
144
+
145
+ Isso une as duas histórias num commit de merge único. Faça uma vez; depois disso updates normais funcionam.
146
+
147
+ #### Caso especial: untracked files que conflitam com o merge
148
+
149
+ Se você tem arquivos não-versionados em paths que o framework gera (`.claude/`, `.qwen/`, `.gemini/`, `.cursor/`, `.agents/`, etc), o git recusa o merge para evitar perda. O CLI detecta e oferece:
150
+
151
+ ```bash
152
+ npm run gos:update -- --clobber-untracked
153
+ ```
154
+
155
+ Isso renomeia os arquivos conflitantes para `.bak.<timestamp>` antes do merge. Como esses paths são **sempre regenerados** por `npm run sync:ides`, o backup é só por garantia — você pode deletar depois.
156
+
157
+ Combinar com `--allow-unrelated` quando ambos os casos ocorrerem (típico de workspaces antigos):
158
+
159
+ ```bash
160
+ npm run gos:update -- --allow-unrelated --clobber-untracked
161
+ ```
162
+
163
+ Alternativa segura (não toca seu git, apenas atualiza `.gos/`):
164
+
165
+ ```bash
166
+ gos install --force
167
+ ```
168
+
169
+ ### Resgate de stashes presos
170
+
171
+ Se você teve falhas anteriores que deixaram stashes:
172
+
173
+ ```bash
174
+ npm run gos:rescue # lista todos com stat
175
+ npm run gos:rescue -- --pop-latest # aplica o mais recente
176
+ npm run gos:rescue -- --drop-all # remove todos (após revisão)
177
+ ```
178
+
179
+ ### Resumo (qual comando para qual cenário)
180
+
181
+ | Cenário | Comando |
182
+ |---------|---------|
183
+ | Atualizar CLI `gos` global | `npm install -g ganbatte-os@latest` |
184
+ | Atualizar G-OS num projeto consumidor | `gos install --force` |
185
+ | Atualizar G-OS num fork/clone do repo | `npm run gos:update` |
186
+ | Stashes presos de updates falhos | `npm run gos:rescue` |
187
+ | Saber em qual cenário você está | `npm run gos:version` |
188
+ | Validar tudo de uma vez | `npm run gos:doctor` |
70
189
 
71
190
  > [!NOTE]
72
191
  > A pasta `.agent/` que pode aparecer na raiz do workspace e criada pela IDE Google Antigravity / Gemini Code Assist — nao faz parte do setup padrao do ganbatte-os.
@@ -136,7 +255,7 @@ O `ganbatte-os` utiliza uma estrutura **encapsulada** para manter seu projeto li
136
255
 
137
256
  ## Plan Pipeline (stack-aware)
138
257
 
139
- Pipeline padronizado para criacao de planos por tela. Toda tela vira um plano + tasks + contexto, com status auditavel e contrato de stack.
258
+ Pipeline padronizado para criacao de planos por tela. **Toda tela = 1 plano**. Toda execucao gera plano + tasks + contexto, com status auditavel e contrato de stack.
140
259
 
141
260
  ### Fluxo
142
261
 
@@ -161,31 +280,206 @@ Pipeline padronizado para criacao de planos por tela. Toda tela vira um plano +
161
280
 
162
281
  ### Configuracao por workspace
163
282
 
164
- `.gos-local/plan-paths.json` define onde cada projeto guarda seus artefatos. Cada projeto/dev pode organizar diferente:
283
+ `.gos-local/plan-paths.json` define onde cada projeto guarda seus artefatos. Cada projeto/dev pode organizar diferente.
284
+
285
+ **Inicializar com defaults** (helper):
286
+
287
+ ```bash
288
+ node .gos/scripts/tools/plan-paths.js init # cria .gos-local/plan-paths.json se nao existir
289
+ node .gos/scripts/tools/plan-paths.js show # imprime config atual
290
+ node .gos/scripts/tools/plan-paths.js get planos # imprime path resolvido para "planos"
291
+ ```
292
+
293
+ **Exemplo de config** (caso real do `packages/fractus`):
165
294
 
166
295
  ```json
167
296
  {
168
297
  "schema": "gos.plan-paths.v1",
169
298
  "dirs": {
170
- "planos": "docs/plans/",
171
- "tasks": "docs/plans/{plan}/tasks/",
172
- "contexto": "docs/plans/{plan}/context.md",
173
- "progress": "progress.txt",
174
- "stack": "docs/stack.md",
175
- "postman": "docs/postman/",
176
- "regras_negocio": "docs/regras-de-negocio/"
299
+ "projeto": "src/",
300
+ "storybook": ".referencia-storybook/",
301
+ "design_system_doc": ".referencia-storybook/docs/DESIGN_SYSTEM_REFERENCE.md",
302
+ "components": ".referencia-storybook/components/",
303
+ "stories": ".referencia-storybook/stories/",
304
+ "planos": "docs/plans/",
305
+ "tasks": "docs/plans/{plan}/tasks/",
306
+ "contexto": "docs/plans/{plan}/context.md",
307
+ "progress": "progress.txt",
308
+ "stack": "docs/stack.md",
309
+ "adr": "docs/adr/",
310
+ "postman": "docs/postman/",
311
+ "regras_negocio": "docs/regras-de-negocio/"
177
312
  },
178
313
  "knowledge_sources": [
179
314
  { "kind": "postman", "path": "docs/postman/", "required": false },
180
315
  { "kind": "business-rules", "path": "docs/regras-de-negocio/", "required": false },
316
+ { "kind": "adr", "path": "docs/adr/", "required": false },
181
317
  { "kind": "design-system", "path": ".referencia-storybook/docs/DESIGN_SYSTEM_REFERENCE.md", "required": true }
182
- ]
318
+ ],
319
+ "naming": { "plan_prefix": "PLAN", "task_prefix": "T", "seq_padding": 3 },
320
+ "figma": { "mcp_enabled": true, "default_file_url": null }
183
321
  }
184
322
  ```
185
323
 
186
- Helpers: `scripts/tools/plan-paths.js`, `scripts/tools/plan-status.js`, `scripts/tools/stack-scan.js`.
324
+ Helpers em `.gos/scripts/tools/`:
325
+ - `plan-paths.js` — resolve caminhos do projeto-cliente
326
+ - `plan-status.js` — valida transicoes de status (state machine)
327
+ - `stack-scan.js` — infere stack lendo `package.json`, configs e `knowledge_sources`
328
+
329
+ ### Exemplos de uso (end-to-end)
330
+
331
+ Todos os comandos abaixo sao invocados via slash command no `gos-master` (Claude Code, Gemini, Cursor, etc).
332
+
333
+ #### 1. Bootstrap do workspace (uma vez por projeto)
334
+
335
+ ```bash
336
+ /gos:agents:gos-master
337
+ ```
338
+
339
+ Em seguida, no chat:
340
+
341
+ ```
342
+ *stack refresh
343
+ ```
344
+
345
+ Saida esperada:
346
+
347
+ ```
348
+ ## Stack profilada — packages/fractus
349
+
350
+ - Framework: Next.js 15 (App Router)
351
+ - DB: Supabase (read-only)
352
+ - Design System: .referencia-storybook
353
+ - Knowledge sources: 4 (postman, business-rules, adr, design-system)
354
+
355
+ stack.md: docs/stack.md
356
+ lock: .gos-local/stack.lock.json
357
+ hashes: 12 arquivos
358
+ ```
359
+
360
+ Verifique sempre que algo na stack mudar (lib, framework, ORM):
361
+
362
+ ```
363
+ *stack drift
364
+ ```
365
+
366
+ #### 2. Criar plano para uma tela
367
+
368
+ A partir de URL Figma (autodetecta e usa Figma MCP):
369
+
370
+ ```
371
+ *plan https://www.figma.com/design/kXd8uP6dgeSuQypFnPmuQP/FRACTUS?node-id=9140-25387
372
+ ```
373
+
374
+ A partir de descricao livre:
375
+
376
+ ```
377
+ *plan tela de checkout com formulario de pagamento e resumo do pedido
378
+ ```
379
+
380
+ Saida:
381
+
382
+ ```
383
+ ## Plano criado: PLAN-042-checkout
384
+
385
+ - plan.md: docs/plans/PLAN-042-checkout/plan.md
386
+ - context.md: docs/plans/PLAN-042-checkout/context.md
387
+ - tasks/: docs/plans/PLAN-042-checkout/tasks/ (5 tasks: T-042-001 ... T-042-005)
388
+ - progress: atualizado (status=pendente)
389
+ - stack_ref: docs/stack.md@a1b2c3d
390
+
391
+ Proximos passos:
392
+ 1. Revisar plan.md e checklist de aceite
393
+ 2. *progress status T-042-001 em-andamento
394
+ 3. Executar
395
+ ```
396
+
397
+ Telas complexas (modal + drawer + sub-rotas) sao **subdivididas automaticamente** em planos filhos:
398
+
399
+ ```
400
+ PLAN-042-checkout (pai, checklist consolidado)
401
+ ├── PLAN-042.1-payment-modal (filho)
402
+ ├── PLAN-042.2-summary-drawer
403
+ └── PLAN-042.3-confirm-page
404
+ ```
405
+
406
+ #### 3. Executar tasks
407
+
408
+ ```
409
+ *progress show # mostra progress.txt atual
410
+ *progress status T-042-001 em-andamento # iniciar task
411
+ # ... dev implementa ...
412
+ *progress status T-042-001 validacao # task pronta para revisao (commit preparado, nao pushado)
413
+ ```
414
+
415
+ Tentar pular validacao falha:
416
+
417
+ ```
418
+ *progress status T-042-001 concluido
419
+ > erro: transicao invalida: em-andamento → concluido
420
+ > use --rollback para voltar para pendente
421
+ ```
422
+
423
+ #### 4. Fechar o plano
424
+
425
+ Quando todas as tasks estao em `validacao` e o checklist do plano esta completo:
426
+
427
+ ```
428
+ *progress status PLAN-042-checkout validacao # validacao humana + QA
429
+ *progress status PLAN-042-checkout concluido # SOMENTE apos aprovacao
430
+ ```
431
+
432
+ `progress-tracker compact` periodicamente reescreve `progress.txt` removendo tasks `concluido` antigas para manter o arquivo < 4kB (otimizado para LLM).
433
+
434
+ #### 5. Mudanca de arquitetura (excecao)
435
+
436
+ Se a tela exigir alteracao da stack (nova lib, novo padrao de fetching, schema novo):
437
+
438
+ ```
439
+ *plan tela-relatorios --allow-arch-change
440
+ ```
441
+
442
+ Isso forca a Fase 2 propositiva e gera um ADR em `docs/adr/ADR-NNNN-<slug>.md` antes de prosseguir. O plano resultante referencia o ADR no frontmatter (`arch_change: true`).
443
+
444
+ ### Estrutura de saida
445
+
446
+ Apos `*plan tela-checkout`:
447
+
448
+ ```
449
+ docs/plans/PLAN-042-checkout/
450
+ ├── plan.md # frontmatter (id, tela, figma_url, status, stack_ref) + secoes fixas
451
+ ├── context.md # denso, indice de arquivos, decisoes, riscos
452
+ └── tasks/
453
+ ├── T-042-001-criar-rota-checkout.md
454
+ ├── T-042-002-fetching-supabase.md
455
+ ├── T-042-003-form-pagamento.md
456
+ ├── T-042-004-resumo-pedido.md
457
+ └── T-042-005-estados-erro-loading.md
458
+ ```
459
+
460
+ `progress.txt` na raiz do projeto fica:
461
+
462
+ ```
463
+ # progress.l1
464
+ ts=2026-05-01T18:22:00-03:00
465
+ project=fractus
466
+ plan_active=docs/plans/PLAN-042-checkout/plan.md
467
+ tasks_dir=docs/plans/PLAN-042-checkout/tasks/
468
+ context=docs/plans/PLAN-042-checkout/context.md
469
+ stack_ref=docs/stack.md@a1b2c3d
470
+ status=em-andamento
471
+
472
+ last_done=T-042-002 fetching-supabase
473
+ current=T-042-003 form-pagamento
474
+ next=T-042-004 resumo-pedido
475
+
476
+ blockers=
477
+ notes=fetch usa server component em app/checkout/page.tsx
478
+ ```
479
+
480
+ ### Playbook completo
187
481
 
188
- Playbook completo: [`.gos/playbooks/plan-creation-playbook.md`](./.gos/playbooks/plan-creation-playbook.md).
482
+ [`.gos/playbooks/plan-creation-playbook.md`](./.gos/playbooks/plan-creation-playbook.md) — documenta o fluxo end-to-end com troubleshooting.
189
483
 
190
484
  ## Documentacao
191
485
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ganbatte-os",
3
- "version": "0.2.31",
3
+ "version": "0.2.33",
4
4
  "description": "Framework operacional para design-to-code, squads de entrega e sprint sync com ClickUp.",
5
5
  "bin": {
6
6
  "gos": ".gos/scripts/cli/gos-cli.js"
@@ -10,6 +10,7 @@
10
10
  "gos:update": "node .gos/scripts/cli/gos-cli.js update",
11
11
  "gos:doctor": "node .gos/scripts/cli/gos-cli.js doctor",
12
12
  "gos:version": "node .gos/scripts/cli/gos-cli.js version",
13
+ "gos:rescue": "node .gos/scripts/cli/gos-cli.js rescue",
13
14
  "clickup": "node .gos/scripts/tools/clickup.js",
14
15
  "sync:ides": "node .gos/scripts/integrations/setup-ide-adapters.js",
15
16
  "check:ides": "node .gos/scripts/integrations/check-ide-compat.js",