@orkastery/cli 0.5.0 → 0.5.3

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 (131) hide show
  1. package/adapters/claude-code/README.md +11 -2
  2. package/adapters/claude-code/agents/ork-master.md +2 -0
  3. package/adapters/claude-code/commands/master.md +8 -0
  4. package/adapters/claude-code/commands/onboarding.md +3 -2
  5. package/adapters/claude-code/commands/ork.md +9 -0
  6. package/adapters/codex/skills/ork/SKILL.md +18 -3
  7. package/adapters/hermes/README.md +1 -0
  8. package/adapters/hermes/bin/ork-network-status.sh +10 -0
  9. package/adapters/hermes/hermes.plugin.json +1 -0
  10. package/adapters/hermes/hitl-ingress/__init__.py +2 -2
  11. package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +6 -5
  12. package/adapters/openclaw/README.md +35 -4
  13. package/adapters/openclaw/dist/hitl-ingress.js +2 -2
  14. package/adapters/openclaw/dist/index.js +47 -12
  15. package/adapters/openclaw/openclaw.plugin.json +5 -1
  16. package/adapters/openclaw/package.json +2 -2
  17. package/adapters/openclaw/src/hitl-ingress.ts +2 -2
  18. package/adapters/openclaw/src/index.ts +50 -12
  19. package/assets/orkmind_bridge.py +117 -9
  20. package/dist/adapters/claude-bg.js +59 -11
  21. package/dist/adapters/codex.js +4 -3
  22. package/dist/agents-md.js +1 -0
  23. package/dist/auditrun.js +6 -2
  24. package/dist/board.js +16 -14
  25. package/dist/branch-de-estado.js +66 -10
  26. package/dist/busca-semantica.js +52 -12
  27. package/dist/canarios-rm057.js +149 -0
  28. package/dist/canarios.js +2 -0
  29. package/dist/ci.js +8 -1
  30. package/dist/claim-lint.js +3 -1
  31. package/dist/claims.js +39 -1
  32. package/dist/company-brain-context.js +158 -22
  33. package/dist/docs.js +110 -3
  34. package/dist/doctor-pulse-cron.js +194 -0
  35. package/dist/doctor-rede.js +149 -0
  36. package/dist/doctor.js +294 -37
  37. package/dist/entrega-pr.js +27 -8
  38. package/dist/esgotamentos.js +181 -0
  39. package/dist/experiencia.js +12 -2
  40. package/dist/fabrica-estado.js +35 -9
  41. package/dist/fabrica-publicar.js +6 -0
  42. package/dist/fechamento.js +20 -66
  43. package/dist/fix.js +3 -3
  44. package/dist/forja.js +31 -7
  45. package/dist/gates.js +5 -1
  46. package/dist/hitl-contract.js +59 -4
  47. package/dist/hitl-curto.js +11 -6
  48. package/dist/hitl-gates.js +5 -0
  49. package/dist/hitl-lote.js +5 -2
  50. package/dist/hitl-resumo.js +33 -1
  51. package/dist/hitl-tempo-parado.js +201 -0
  52. package/dist/hitl-texto-livre.js +5 -5
  53. package/dist/horario.js +27 -0
  54. package/dist/hosts.js +59 -13
  55. package/dist/impedimento.js +205 -0
  56. package/dist/index.js +433 -88
  57. package/dist/indice-vetorial.js +87 -10
  58. package/dist/indice.js +3 -1
  59. package/dist/init.js +28 -5
  60. package/dist/integracoes-locais.js +4 -0
  61. package/dist/intelligence-graph-cli.js +267 -30
  62. package/dist/intelligence-graph-contexto.js +215 -0
  63. package/dist/intelligence-graph-contract.js +71 -12
  64. package/dist/intelligence-graph-extract-md.js +41 -23
  65. package/dist/intelligence-graph-extract-ts.js +181 -11
  66. package/dist/intelligence-graph-extract.js +186 -9
  67. package/dist/intelligence-graph-index.js +205 -24
  68. package/dist/intelligence-graph-parsers.js +6 -4
  69. package/dist/intelligence-graph-repo.js +10 -0
  70. package/dist/leases.js +680 -87
  71. package/dist/ledger-stats.js +15 -1
  72. package/dist/ledger.js +12 -0
  73. package/dist/licoes.js +6 -4
  74. package/dist/liveness.js +2 -1
  75. package/dist/maestro-runtime.js +7 -1
  76. package/dist/manifest.js +44 -1
  77. package/dist/master-nota.js +14 -5
  78. package/dist/master.js +129 -12
  79. package/dist/mcp-artifacts.js +36 -3
  80. package/dist/mcp-git.js +80 -15
  81. package/dist/mcp-grafo-worker.js +93 -0
  82. package/dist/mcp-grafo.js +279 -0
  83. package/dist/mcp-install.js +2 -1
  84. package/dist/mcp-server.js +23 -1
  85. package/dist/mcp-ship.js +2 -0
  86. package/dist/mcp-verify.js +3 -1
  87. package/dist/memoria.js +48 -6
  88. package/dist/network-roadmap.js +195 -20
  89. package/dist/ocupacao.js +17 -5
  90. package/dist/onboarding.js +9 -4
  91. package/dist/orkmind.js +90 -5
  92. package/dist/orquestracao.js +14 -11
  93. package/dist/parado-no-condutor.js +1089 -0
  94. package/dist/phase.js +68 -15
  95. package/dist/policies.js +79 -3
  96. package/dist/portfolio.js +4 -1
  97. package/dist/projeto-alvo.js +5 -1
  98. package/dist/prompts.js +36 -1
  99. package/dist/prova-ativacao.js +414 -0
  100. package/dist/pulse-delivery.js +60 -7
  101. package/dist/pulse.js +99 -3
  102. package/dist/ratelimit.js +20 -4
  103. package/dist/recall.js +3 -1
  104. package/dist/rede-adesao.js +399 -0
  105. package/dist/rede-forja.js +289 -0
  106. package/dist/rede-projetos.js +239 -0
  107. package/dist/rede-status.js +336 -0
  108. package/dist/rede.js +980 -0
  109. package/dist/retry.js +69 -2
  110. package/dist/roadmap-status.js +162 -7
  111. package/dist/runtime-context.js +5 -1
  112. package/dist/runtime-profiles.js +22 -1
  113. package/dist/saida-segura.js +58 -0
  114. package/dist/session-watcher-claude.js +113 -23
  115. package/dist/sessoes-contas.js +187 -0
  116. package/dist/sessoes-inventario.js +6 -12
  117. package/dist/sessoes.js +87 -3
  118. package/dist/ship.js +110 -20
  119. package/dist/thread.js +212 -14
  120. package/dist/util.js +71 -4
  121. package/dist/verify.js +2 -2
  122. package/dist/worktree.js +34 -8
  123. package/eval/casos/onboarding.json +1 -1
  124. package/eval/fixtures/fx-pedido-colado/caso.json +20 -0
  125. package/package.json +7 -3
  126. package/references/definition-of-done.md +1 -1
  127. package/skills/README.md +1 -1
  128. package/skills/core/onboarding/SKILL.md +3 -2
  129. package/skills/core/orkastery-bootstrap/SKILL.md +2 -2
  130. package/skills/phases/master-metrics/SKILL.md +8 -3
  131. package/skills/phases/ship-release/SKILL.md +6 -3
package/dist/ship.js CHANGED
@@ -50,6 +50,7 @@ var __importStar = (this && this.__importStar) || (function () {
50
50
  };
51
51
  })();
52
52
  Object.defineProperty(exports, "__esModule", { value: true });
53
+ exports.commitQueIncorporou = commitQueIncorporou;
53
54
  exports.autorizacaoDePush = autorizacaoDePush;
54
55
  exports.arvoreComBranch = arvoreComBranch;
55
56
  exports.prepararArvore = prepararArvore;
@@ -62,6 +63,7 @@ exports.ship = ship;
62
63
  exports.textoDoShip = textoDoShip;
63
64
  const fs = __importStar(require("node:fs"));
64
65
  const path = __importStar(require("node:path"));
66
+ const node_child_process_1 = require("node:child_process");
65
67
  const gates_1 = require("./gates");
66
68
  const leases_1 = require("./leases");
67
69
  const ledger_1 = require("./ledger");
@@ -76,6 +78,45 @@ const contrato_publico_1 = require("./contrato-publico");
76
78
  const prova_minima_1 = require("./prova-minima");
77
79
  const horario_1 = require("./horario");
78
80
  const fabrica_publicar_1 = require("./fabrica-publicar");
81
+ const branch_de_estado_1 = require("./branch-de-estado");
82
+ /**
83
+ * RM-037 (fatia 5, A1): o commit de primeiro pai da base que incorporou o `shaDe`: o merge que trouxe a branch, ou
84
+ * o proprio `shaDe` quando a base avancou por fast-forward ate ele. Com a branch ja incorporada, a ponta da base e
85
+ * outra coisa: no `ship_done` da ork-docsusuarios (27/09/2026), o unico do `ork ship` com `jaIncorporado` nos ledgers
86
+ * deste projeto, ela era o merge de outra thread. O que trouxe a branch e o commit mais velho do primeiro pai da base
87
+ * que descende do `shaDe`. GO-FIX (rodada 1 do CHECK): `--first-parent` junto com `--ancestry-path` so propaga a
88
+ * descendencia pelos commits de primeiro pai, e o merge do PR que recebeu "Update branch" no GitHub (o pai dele e um
89
+ * merge que descende do `shaDe`, nao o `shaDe`) sumia da lista; agora sao duas listas e a intersecao. `null` quando o
90
+ * git nao responde.
91
+ */
92
+ function commitQueIncorporou(raiz, shaDe, ponta, passos = []) {
93
+ if (shaDe === ponta)
94
+ return shaDe;
95
+ // `passos` recebe os comandos que de fato rodaram, na ordem: e a evidencia do ship (sugestao da conferencia da rodada 2).
96
+ const curto = (sha) => sha.slice(0, 8);
97
+ // As listas crescem com o historico entre a branch e a ponta: o buffer vai alem do 1 MiB padrao do spawnSync.
98
+ const listar = (args) => {
99
+ passos.push(`git ${args.map((a) => a.replace(/^([0-9a-f]{40,64})\.\.([0-9a-f]{40,64})$/, (_, x, y) => `${curto(x)}..${curto(y)}`)).join(' ')}`);
100
+ const r = (0, node_child_process_1.spawnSync)('git', args, { cwd: raiz, encoding: 'utf8', timeout: 120000, maxBuffer: 256 * 1024 * 1024 });
101
+ return r.status === 0 && !r.error ? r.stdout.split('\n').filter((l) => /^[0-9a-f]{40,64}$/.test(l)) : null;
102
+ };
103
+ const primeiroPai = listar(['rev-list', '--first-parent', `${shaDe}..${ponta}`]);
104
+ const descendentes = listar(['rev-list', '--ancestry-path', `${shaDe}..${ponta}`]);
105
+ if (!primeiroPai || !descendentes)
106
+ return null;
107
+ const doShaDe = new Set(descendentes);
108
+ // Do mais velho para o mais novo: o primeiro de primeiro pai que descende do `shaDe`.
109
+ const maisVelho = [...primeiroPai].reverse().find((c) => doShaDe.has(c));
110
+ if (!maisVelho)
111
+ return null;
112
+ // O primeiro pai dele e o proprio `shaDe`: a branch ja estava no primeiro pai da base (fast-forward). Sem a resposta
113
+ // do git, nada e gravado: devolver o mais velho poria no `mergeSha` o filho do `shaDe` (sugestao da rodada 2).
114
+ passos.push(`git rev-parse ${curto(maisVelho)}^1`);
115
+ const pai = (0, util_1.exec)('git', ['rev-parse', `${maisVelho}^1`], raiz);
116
+ if (!pai.ok)
117
+ return null;
118
+ return pai.stdout.trim() === shaDe ? shaDe : maisVelho;
119
+ }
79
120
  /** Os caminhos de contrato publico que a entrega traz sobre a base (diff desde o merge-base). */
80
121
  function contratosDaEntrega(raiz, shaPara, shaDe) {
81
122
  const base = (0, util_1.exec)('git', ['merge-base', shaPara, shaDe], raiz);
@@ -185,7 +226,7 @@ function prepararArvore(raiz, para) {
185
226
  }
186
227
  const dir = dirTemporarioDeShip(raiz, para);
187
228
  fs.mkdirSync(path.dirname(dir), { recursive: true });
188
- const r = (0, util_1.exec)('git', ['worktree', 'add', dir, para], raiz);
229
+ const r = (0, util_1.exec)('git', ['worktree', 'add', '--', dir, para], raiz);
189
230
  if (!r.ok) {
190
231
  throw new Error(`nao foi possivel montar a arvore de merge de "${para}": ${(r.stderr || r.stdout).trim()}`);
191
232
  }
@@ -211,7 +252,8 @@ function shaDaRef(dir, ref) {
211
252
  * codigo de saida do `git push` nao prova que o remoto ficou com o commit certo.
212
253
  */
213
254
  function shaNoRemoto(dir, remoto, branch) {
214
- const r = (0, util_1.exec)('git', ['ls-remote', remoto, `refs/heads/${branch}`], dir, 120000);
255
+ // RM-047: so nome de remoto chega ao git, e depois do `--` (nunca vira opcao nem transporte).
256
+ const r = (0, util_1.exec)('git', ['ls-remote', '--', (0, branch_de_estado_1.exigirRemoto)(remoto, 'ship'), `refs/heads/${branch}`], dir, 120000);
215
257
  if (!r.ok)
216
258
  return null;
217
259
  const linha = r.stdout.split('\n').find((l) => l.trim() !== '');
@@ -222,7 +264,7 @@ function shaNoRemoto(dir, remoto, branch) {
222
264
  }
223
265
  /** O remoto esta configurado neste repositorio? */
224
266
  function remotoConfigurado(dir, remoto) {
225
- return (0, util_1.exec)('git', ['remote', 'get-url', remoto], dir).ok;
267
+ return (0, util_1.exec)('git', ['remote', 'get-url', '--', (0, branch_de_estado_1.exigirRemoto)(remoto, 'ship')], dir).ok;
226
268
  }
227
269
  function baseDoResultado(thread, de, opcoes) {
228
270
  return {
@@ -237,6 +279,7 @@ function baseDoResultado(thread, de, opcoes) {
237
279
  shaDe: '',
238
280
  shaParaAntes: '',
239
281
  mergeSha: null,
282
+ pontaDaBase: null,
240
283
  jaIncorporado: false,
241
284
  remoto: opcoes.remoto ?? 'origin',
242
285
  shaRemoto: null,
@@ -329,7 +372,10 @@ function ship(carregado, threadId, opcoes) {
329
372
  const dir = (0, thread_1.dirThread)(raiz, threadId);
330
373
  const de = opcoes.de ?? thread.base.branch;
331
374
  const para = opcoes.para;
332
- const remoto = opcoes.remoto ?? 'origin';
375
+ // RM-047: o `--remoto` vem da linha de comando. Antes de qualquer git (inclusive a leitura da reversao),
376
+ // so nome de remoto passa; o resto recusa com `ship.remoto-invalido`, e o ship nao entrega sem push provado
377
+ // por um remoto que nunca existiu.
378
+ const remoto = (0, branch_de_estado_1.exigirRemoto)(opcoes.remoto ?? 'origin', 'ship');
333
379
  const r = baseDoResultado(thread, de, opcoes);
334
380
  // I-43 (T7): antes de qualquer coisa, se a entrega ANTERIOR desta thread foi
335
381
  // revertida na base, isso vira `rollback_done`. E o insumo de maior peso do indice
@@ -348,6 +394,8 @@ function ship(carregado, threadId, opcoes) {
348
394
  correcao,
349
395
  de,
350
396
  para,
397
+ // Fatia 2 do ensaio da 0.5.0 (P2): o gate do ensaio e marcado, e os leitores de estado o ignoram.
398
+ ...(r.dryRun ? { dryRun: true } : {}),
351
399
  });
352
400
  (0, ledger_1.registrar)(dir, threadId, ledger_1.TIPOS_DE_EVENTO.shipBloqueado, {
353
401
  de,
@@ -373,6 +421,26 @@ function ship(carregado, threadId, opcoes) {
373
421
  const branchAtrasDaBase = pontaDe && pontaPara && de !== para
374
422
  ? !(0, util_1.exec)('git', ['merge-base', '--is-ancestor', pontaPara, pontaDe], raiz).ok
375
423
  : undefined;
424
+ // Fatia 2 do ensaio da 0.5.0 (P9): sem delta (a branch da thread ja esta contida na base) e com a base
425
+ // local a frente da ref de rastreio do remoto, o push publicaria a base sem merge de thread. A medida e
426
+ // local, sem rede; sem remoto, sem ref de rastreio ou com --sem-push, o fato nao vem e nada muda.
427
+ // CHECK, rodada 1 (B1): o merge que o proprio ship ja fez e nao chegou ao remoto (push recusado,
428
+ // --sem-push, retomada do MCP) tambem deixa a base a frente sem delta, mas leva a ponta da thread como
429
+ // pai que nao e o primeiro: e entrega de thread, e o retry empurra como antes. Rodada 2 (S-R2-1): so
430
+ // quando a linha principal da base, alem do remoto, e toda de merge; commit direto nela segue barrado.
431
+ const rastreio = pontaDe && pontaPara && de !== para && !opcoes.semPush && remotoConfigurado(raiz, remoto)
432
+ ? shaDaRef(raiz, `refs/remotes/${remoto}/${para}`) : null;
433
+ const mergeDaThreadNaBase = (desde) => {
434
+ const diretos = (0, util_1.exec)('git', ['rev-list', '--first-parent', '--no-merges', `${desde}..${pontaPara}`], raiz);
435
+ return diretos.ok && diretos.stdout.trim() === '' &&
436
+ (0, util_1.exec)('git', ['rev-list', '--merges', '--parents', `${desde}..${pontaPara}`], raiz).stdout.split('\n')
437
+ .some((linha) => linha.trim().split(' ').slice(2).includes(pontaDe));
438
+ };
439
+ const semDeltaComBaseAFrente = rastreio
440
+ ? (0, util_1.exec)('git', ['merge-base', '--is-ancestor', pontaDe, pontaPara], raiz).ok &&
441
+ !(0, util_1.exec)('git', ['merge-base', '--is-ancestor', pontaPara, rastreio], raiz).ok &&
442
+ !mergeDaThreadNaBase(rastreio)
443
+ : undefined;
376
444
  const violacoes = (0, policies_1.avaliarPolicies)(manifesto, {
377
445
  gate: 'ship',
378
446
  de,
@@ -380,6 +448,8 @@ function ship(carregado, threadId, opcoes) {
380
448
  baseBranch: manifesto.worktree.base_branch,
381
449
  threadId,
382
450
  branchAtrasDaBase,
451
+ semDeltaComBaseAFrente,
452
+ remoto,
383
453
  });
384
454
  r.violacoes = violacoes;
385
455
  if (!r.dryRun) {
@@ -425,7 +495,7 @@ function ship(carregado, threadId, opcoes) {
425
495
  }
426
496
  // 4. Lease `main-tree`: so uma thread mergeia por vez.
427
497
  if (r.dryRun) {
428
- const ocupado = (0, leases_1.lerLease)(raiz, leases_1.LEASE_MAIN_TREE);
498
+ const ocupado = (0, leases_1.leasesColidentes)(raiz, leases_1.LEASE_MAIN_TREE)[0] ?? null;
429
499
  r.leaseOcupadoPor = ocupado;
430
500
  // O ensaio tambem responde ONDE o merge aconteceria e se aquela arvore esta limpa:
431
501
  // descobrir isso so na hora do merge real seria descobrir tarde demais.
@@ -434,11 +504,17 @@ function ship(carregado, threadId, opcoes) {
434
504
  ? `${dona} (arvore que tem ${para} em check-out; ${temAlteracaoNaoCommitada(dona) ? 'SUJA, o ship reprovaria com tree.blocked' : 'limpa'})`
435
505
  : `worktree temporaria em ${path.join((0, manifest_1.dirEstado)(raiz), 'tmp')} (ninguem tem ${para} em check-out)`;
436
506
  r.detalhe = `arvore de merge: ${arvoreDoMerge}`;
507
+ // Ensaio de 03/10 (R5): sem remoto (ou com --sem-push), o ensaio diz o mesmo que o ship real, e nao
508
+ // promete um push e uma prova que nao vao acontecer.
509
+ const semPushNoReal = opcoes.semPush ? 'push nao executado por --sem-push (merge local concluido)'
510
+ : !remotoConfigurado(raiz, remoto) ? `remoto "${remoto}" nao configurado: merge local concluido, sem push a provar` : null;
437
511
  r.passos = [
438
512
  `arvore de merge: ${arvoreDoMerge}`,
439
513
  `git merge --no-ff ${de} -m "<mensagem de ship>"`,
440
- `git push ${remoto} ${para}`,
441
- `git ls-remote ${remoto} refs/heads/${para} (prova do push)`,
514
+ ...(semPushNoReal ? [`sem push: ${semPushNoReal}`] : [
515
+ `git push ${remoto} ${para}`,
516
+ `git ls-remote ${remoto} refs/heads/${para} (prova do push)`,
517
+ ]),
442
518
  ];
443
519
  (0, ledger_1.registrar)(dir, threadId, ledger_1.TIPOS_DE_EVENTO.shipIniciado, {
444
520
  de,
@@ -469,11 +545,11 @@ function ship(carregado, threadId, opcoes) {
469
545
  posicao: aquisicao.posicaoNaFila,
470
546
  naFrente: (0, leases_1.esperandoPor)(raiz, leases_1.LEASE_MAIN_TREE).length,
471
547
  bloqueadaPor: dono?.thread ?? '(desconhecida)',
472
- motivo: 'lease.busy',
548
+ motivo: aquisicao.motivo ?? 'lease.busy',
473
549
  });
474
- return bloquear('lease.busy', `lease ${leases_1.LEASE_MAIN_TREE} esta com a thread ${dono?.thread ?? '(desconhecida)'} ` +
550
+ return bloquear(aquisicao.motivo ?? 'lease.busy', aquisicao.falhaRetomada ? aquisicao.detalhe : `lease ${leases_1.LEASE_MAIN_TREE} esta com a thread ${dono?.thread ?? '(desconhecida)'} ` +
475
551
  `desde ${dono?.adquiridoEm ?? '?'} (${dono?.motivo ?? 'sem motivo'}); ` +
476
- `esta thread entrou na merge queue na posicao ${aquisicao.posicaoNaFila}`, `espere a vez na fila (ork lease list), ou libere com: ork lease release ${leases_1.LEASE_MAIN_TREE} --forcar`);
552
+ `esta thread entrou na merge queue na posicao ${aquisicao.posicaoNaFila}`, aquisicao.correcao);
477
553
  }
478
554
  r.lease = aquisicao.lease;
479
555
  (0, ledger_1.registrar)(dir, threadId, ledger_1.TIPOS_DE_EVENTO.leaseAdquirido, {
@@ -508,6 +584,9 @@ function ship(carregado, threadId, opcoes) {
508
584
  const jaIncorporado = (0, util_1.exec)('git', ['merge-base', '--is-ancestor', shaDe, shaPara], raiz).ok;
509
585
  r.jaIncorporado = jaIncorporado;
510
586
  let mergeSha = shaPara;
587
+ // RM-037 (fatia 5, A1): a ponta que o push prova. Com o merge feito aqui, e ele; com a branch ja incorporada, a
588
+ // ponta de antes, que pode trazer outros merges depois do que entregou esta thread.
589
+ let pontaDaBase = shaPara;
511
590
  if (!jaIncorporado) {
512
591
  const mensagem = opcoes.mensagem ??
513
592
  `ship(${threadId}): merge de ${de} em ${para}\n\n` +
@@ -515,7 +594,8 @@ function ship(carregado, threadId, opcoes) {
515
594
  `modo: ${(0, modos_1.tagDoModo)(thread.modo)}\n` +
516
595
  `autorizado por: ${autorizacao.por} (${autorizacao.tipo})\n` +
517
596
  `verificado em: HEAD ${verificacao.commit}\n`;
518
- const merge = (0, util_1.exec)('git', ['merge', '--no-ff', de, '-m', mensagem], arvore.dir, 300000);
597
+ // RM-047: o merge e do sha verificado, e nao do nome que veio de `--de` (nome nunca vira opcao do git).
598
+ const merge = (0, util_1.exec)('git', ['merge', '--no-ff', '-m', mensagem, shaDe], arvore.dir, 300000);
519
599
  passos.push(`git merge --no-ff ${de}`);
520
600
  if (!merge.ok) {
521
601
  (0, util_1.exec)('git', ['merge', '--abort'], arvore.dir);
@@ -527,6 +607,7 @@ function ship(carregado, threadId, opcoes) {
527
607
  return bloquear('artifact.missing', 'nao foi possivel ler o HEAD apos o merge', 'confira o repositorio');
528
608
  }
529
609
  mergeSha = novo;
610
+ pontaDaBase = novo;
530
611
  // Verificacao independente do merge: a origem precisa ser ancestral do resultado.
531
612
  const incorporou = (0, util_1.exec)('git', ['merge-base', '--is-ancestor', shaDe, mergeSha], arvore.dir).ok;
532
613
  passos.push(`git merge-base --is-ancestor ${shaDe.slice(0, 8)} ${mergeSha.slice(0, 8)}`);
@@ -535,9 +616,16 @@ function ship(carregado, threadId, opcoes) {
535
616
  }
536
617
  }
537
618
  else {
538
- passos.push(`(nada a mergear: ${de} ja e ancestral de ${para})`);
619
+ // RM-037 (fatia 5, A1): o merge que trouxe a branch, e nao a ponta da base.
620
+ const incorporadoEm = commitQueIncorporou(raiz, shaDe, shaPara, passos);
621
+ if (!incorporadoEm) {
622
+ return bloquear('artifact.missing', `${de} ja e ancestral de ${para}, mas o git nao achou o commit de primeiro pai que o incorporou`, 'confira o repositorio: o ship_done precisa do merge que entregou a thread');
623
+ }
624
+ mergeSha = incorporadoEm;
625
+ passos.push(`(nada a mergear: ${de} ja e ancestral de ${para}, incorporado em ${mergeSha.slice(0, 8)})`);
539
626
  }
540
627
  r.mergeSha = mergeSha;
628
+ r.pontaDaBase = pontaDaBase;
541
629
  opcoes.revalidarGit?.('push', arvore.dir);
542
630
  // 6. Push PROVADO por ls-remote.
543
631
  const temRemoto = remotoConfigurado(arvore.dir, remoto);
@@ -551,8 +639,8 @@ function ship(carregado, threadId, opcoes) {
551
639
  else {
552
640
  const existeNoRemoto = shaNoRemoto(arvore.dir, remoto, para) !== null;
553
641
  const argsPush = existeNoRemoto
554
- ? ['push', remoto, `${para}:refs/heads/${para}`]
555
- : ['push', '-u', remoto, `${para}:refs/heads/${para}`];
642
+ ? ['push', '--', remoto, `refs/heads/${para}:refs/heads/${para}`]
643
+ : ['push', '-u', '--', remoto, `refs/heads/${para}:refs/heads/${para}`];
556
644
  const push = (0, util_1.exec)('git', argsPush, arvore.dir, 300000);
557
645
  passos.push(`git ${argsPush.join(' ')}`);
558
646
  if (!push.ok) {
@@ -563,10 +651,10 @@ function ship(carregado, threadId, opcoes) {
563
651
  const shaRemoto = shaNoRemoto(arvore.dir, remoto, para);
564
652
  passos.push(`git ls-remote ${remoto} refs/heads/${para}`);
565
653
  r.shaRemoto = shaRemoto;
566
- r.pushVerificado = shaRemoto !== null && shaRemoto === mergeSha;
654
+ r.pushVerificado = shaRemoto !== null && shaRemoto === pontaDaBase;
567
655
  if (!r.pushVerificado) {
568
656
  r.passos = passos;
569
- return bloquear('runtime.unavailable', `ls-remote devolveu ${shaRemoto ?? '(nada)'} e o destino local esta em ${mergeSha}: ` +
657
+ return bloquear('runtime.unavailable', `ls-remote devolveu ${shaRemoto ?? '(nada)'} e o destino local esta em ${pontaDaBase}: ` +
570
658
  'o push nao ficou provado', 'rode o ship de novo e confira o remoto');
571
659
  }
572
660
  }
@@ -585,6 +673,8 @@ function ship(carregado, threadId, opcoes) {
585
673
  shaDe,
586
674
  shaParaAntes: shaPara,
587
675
  mergeSha,
676
+ // RM-037 (fatia 5, A1): a ponta da base vai em campo proprio; o `mergeSha` e o merge que entregou a thread.
677
+ pontaDaBase,
588
678
  jaIncorporado,
589
679
  remoto: r.remoto,
590
680
  shaRemoto: r.shaRemoto,
@@ -647,12 +737,12 @@ function textoDoShip(r) {
647
737
  linhas.push(` ${local(r.autorizacao.razao)}`);
648
738
  if (r.verificacao) {
649
739
  const v = r.verificacao;
650
- linhas.push(` verificacao ${v.ok ? 'passou' : 'REPROVOU'} no HEAD ${v.commit.slice(0, 8)} ` +
740
+ linhas.push(` verificacao ${v.ok ? 'passou' : 'REPROVOU'} no HEAD ${(0, util_1.shaCurto)(v.commit)} ` +
651
741
  `(${v.claims.filter((c) => c.verificado).length}/${v.claims.length} claims, ` +
652
742
  `${v.comandos.filter((c) => c.ok).length}/${v.comandos.length} comandos)`);
653
743
  }
654
744
  if (r.ci) {
655
- linhas.push(` ci ${r.ci.required ? `${r.ci.state.toUpperCase()} ${r.ci.context} em ${r.ci.sha.slice(0, 8)}` : 'não exigido'}`);
745
+ linhas.push(` ci ${r.ci.required ? `${r.ci.state.toUpperCase()} ${r.ci.context} em ${r.ci.sha.slice(0, 8)}` : 'não exigido'}`);
656
746
  }
657
747
  linhas.push(...(0, policies_1.linhasDeAviso)(r.violacoes ?? []));
658
748
  if (r.leaseOcupadoPor) {
@@ -681,11 +771,11 @@ function textoDoShip(r) {
681
771
  }
682
772
  linhas.push('');
683
773
  linhas.push(r.jaIncorporado
684
- ? `Nada a mergear: ${r.de} ja estava em ${r.para}.`
774
+ ? `Nada a mergear: ${r.de} ja estava em ${r.para}, incorporado em ${r.mergeSha}.`
685
775
  : `Merge --no-ff concluido: ${r.mergeSha}`);
686
776
  if (r.pushVerificado) {
687
777
  linhas.push(`Push PROVADO: ls-remote de ${r.remoto}/${r.para} devolveu ${r.shaRemoto}`);
688
- linhas.push(` e o destino local esta no mesmo sha: ${r.mergeSha}`);
778
+ linhas.push(` e o destino local esta no mesmo sha: ${r.pontaDaBase ?? r.mergeSha}`);
689
779
  }
690
780
  else {
691
781
  linhas.push(`Push nao provado: ${local(r.detalhe) || 'sem remoto configurado'}`);
package/dist/thread.js CHANGED
@@ -45,8 +45,18 @@ exports.caminhoThread = caminhoThread;
45
45
  exports.listarIds = listarIds;
46
46
  exports.lerThread = lerThread;
47
47
  exports.gravarThread = gravarThread;
48
+ exports.avisoDeThreadSemBase = avisoDeThreadSemBase;
48
49
  exports.idDisponivel = idDisponivel;
50
+ exports.pastaDaWorktree = pastaDaWorktree;
51
+ exports.alvoDaWorktree = alvoDaWorktree;
52
+ exports.worktreePrevista = worktreePrevista;
49
53
  exports.criarWorktree = criarWorktree;
54
+ exports.pedidoDeWorktree = pedidoDeWorktree;
55
+ exports.linhaDaWorktree = linhaDaWorktree;
56
+ exports.avisoDeThreadSemWorktree = avisoDeThreadSemWorktree;
57
+ exports.avisoDeWorktreeQueFalharia = avisoDeWorktreeQueFalharia;
58
+ exports.avisoDeChaveSemCommit = avisoDeChaveSemCommit;
59
+ exports.avisoDaWorktree = avisoDaWorktree;
50
60
  exports.novaThread = novaThread;
51
61
  exports.exigirVarianteCompativel = exigirVarianteCompativel;
52
62
  exports.blocoDaThread = blocoDaThread;
@@ -113,9 +123,22 @@ function carimbarBase(raiz) {
113
123
  const commit = (0, util_1.exec)('git', ['rev-parse', 'HEAD'], raiz);
114
124
  return {
115
125
  branch: branch.ok ? branch.stdout.trim() : 'desconhecida',
116
- commit: commit.ok ? commit.stdout.trim() : 'desconhecido',
126
+ commit: commit.ok ? commit.stdout.trim() : util_1.COMMIT_DESCONHECIDO,
117
127
  };
118
128
  }
129
+ /**
130
+ * Ensaio da 0.5.0: a thread aberta antes do primeiro commit nascia sem base, e nada avisava; o
131
+ * ship depois nao tem de onde partir. O aviso vai ao stderr e nao muda a saida do comando.
132
+ */
133
+ function avisoDeThreadSemBase(thread, gravada) {
134
+ if (thread.base.commit !== util_1.COMMIT_DESCONHECIDO)
135
+ return null;
136
+ return gravada
137
+ ? `Aviso: o repositório ainda não tem commit, e a thread ${thread.id} nasceu sem base: o ship não tem de onde partir. ` +
138
+ `Faça o primeiro commit, feche esta com ork thread close ${thread.id} --motivo engano --por <quem> ` +
139
+ '--justificativa "aberta antes do primeiro commit" e abra outra.'
140
+ : 'Aviso: o repositório ainda não tem commit, e a thread nasceria sem base: faça o primeiro commit antes de abrir a primeira thread.';
141
+ }
119
142
  /** Resolve um id livre a partir do assunto normalizado (`<abbrev>-<assunto>`). */
120
143
  function idDisponivel(raiz, abbrev, assunto) {
121
144
  let candidato = assunto;
@@ -127,6 +150,43 @@ function idDisponivel(raiz, abbrev, assunto) {
127
150
  }
128
151
  return { id: `${abbrev}-${candidato}`, assunto: candidato };
129
152
  }
153
+ /**
154
+ * Pasta da worktree de uma thread: `worktree.dir` resolvido na arvore principal, nunca na worktree de onde o comando
155
+ * roda. P4 do ensaio da 0.5.0 (CHECK): com a chave valendo, o `ork thread new` rodado de dentro da worktree de outra
156
+ * thread aninhava a nova na pasta da mae, e o release da mae levava a da filha junto.
157
+ */
158
+ function pastaDaWorktree(carregado, id) {
159
+ return path.resolve((0, estado_thread_1.raizDoEstado)(carregado.raiz), carregado.manifesto.worktree.dir, id);
160
+ }
161
+ /** Pasta e branch da worktree de uma thread: o que `criarWorktree` cria e o `--dry-run` preve. */
162
+ function alvoDaWorktree(carregado, id, slug, opcoes = {}) {
163
+ return { dir: pastaDaWorktree(carregado, id), branch: opcoes.branchExistente ?? `ork/${slug}` };
164
+ }
165
+ /**
166
+ * P4 do ensaio da 0.5.0: a worktree que `criarWorktree` criaria para este id, sem criar nada. Mesma pasta, mesma
167
+ * branch e o commit de onde ela partiria: a branch existente (ciclo `merge-branch`), senao a base do manifesto,
168
+ * senao o HEAD. Diz tambem o que faria a criacao recusar: a pasta que ja existe, o repositorio sem commit ou a
169
+ * branch nova que ja existe.
170
+ */
171
+ function worktreePrevista(carregado, id, slug, opcoes = {}) {
172
+ const alvo = alvoDaWorktree(carregado, id, slug, opcoes);
173
+ const refs = opcoes.branchExistente
174
+ ? [opcoes.branchExistente]
175
+ : [opcoes.base ?? carregado.manifesto.worktree.base_branch, 'HEAD'];
176
+ let commit = null;
177
+ for (const ref of refs) {
178
+ const r = (0, util_1.exec)('git', ['rev-parse', '--verify', ref], carregado.raiz);
179
+ if (r.ok) {
180
+ commit = r.stdout.trim();
181
+ break;
182
+ }
183
+ }
184
+ const branchJaExiste = !opcoes.branchExistente &&
185
+ (0, util_1.exec)('git', ['rev-parse', '--verify', '--quiet', `refs/heads/${alvo.branch}`], carregado.raiz).ok;
186
+ const falha = fs.existsSync(alvo.dir) ? 'pasta-existe'
187
+ : commit === null ? 'sem-commit' : branchJaExiste ? 'branch-existe' : null;
188
+ return { ...alvo, commit, falha };
189
+ }
130
190
  /**
131
191
  * Cria a worktree isolada da thread (paralelismo sem colisao entre threads).
132
192
  *
@@ -136,14 +196,16 @@ function idDisponivel(raiz, abbrev, assunto) {
136
196
  */
137
197
  function criarWorktree(carregado, id, slug, opcoes = {}) {
138
198
  const { raiz, manifesto } = carregado;
139
- const dir = path.resolve(raiz, manifesto.worktree.dir, id);
140
- const branch = opcoes.branchExistente ?? `ork/${slug}`;
199
+ const { dir, branch } = alvoDaWorktree(carregado, id, slug, opcoes);
141
200
  if (fs.existsSync(dir)) {
142
201
  throw new Error(`worktree ja existe em ${dir}`);
143
202
  }
144
203
  const base = opcoes.base ?? manifesto.worktree.base_branch;
145
204
  const commitBase = (0, util_1.exec)('git', ['rev-parse', '--verify', base], raiz);
146
205
  const ref = commitBase.ok ? commitBase.stdout.trim() : 'HEAD';
206
+ // Fatia 2 do ensaio da 0.5.0 (P3): a pasta das worktrees que o ork cria agora nasce fora do git.
207
+ const pastaDasWorktrees = path.dirname(dir);
208
+ const pastaNova = !fs.existsSync(pastaDasWorktrees);
147
209
  // Branch nova (`-b`) no caso comum; branch que ja existe no ciclo `merge-branch`.
148
210
  const argumentos = opcoes.branchExistente
149
211
  ? ['worktree', 'add', dir, opcoes.branchExistente]
@@ -152,6 +214,8 @@ function criarWorktree(carregado, id, slug, opcoes = {}) {
152
214
  if (!r.ok) {
153
215
  throw new Error(`git worktree add falhou: ${(r.stderr || r.stdout).trim()}`);
154
216
  }
217
+ if (pastaNova)
218
+ (0, util_1.ignorarPastaNoGit)(pastaDasWorktrees, (0, estado_thread_1.raizDoEstado)(raiz));
155
219
  const lista = (0, util_1.exec)('git', ['worktree', 'list', '--porcelain'], raiz);
156
220
  const criada = lista.ok && lista.stdout.split('\n').some((l) => l.trim() === `worktree ${dir}`);
157
221
  if (!criada) {
@@ -160,6 +224,105 @@ function criarWorktree(carregado, id, slug, opcoes = {}) {
160
224
  (0, estado_thread_1.vincularEstado)(raiz, id, dir);
161
225
  return { dir, branch };
162
226
  }
227
+ /**
228
+ * Resolve `--worktree`, `--sem-worktree` e `worktree.por_thread`. As flags vencem a chave; as duas juntas, ou
229
+ * `--sem-worktree` com valor (o parser leva o argumento seguinte), sao erro de uso. `--worktree` sozinho ou `auto`
230
+ * cria a worktree; outro texto e um diretorio que ja existe, como antes.
231
+ */
232
+ function pedidoDeWorktree(flags, porThread) {
233
+ const worktree = flags.worktree === false ? undefined : flags.worktree;
234
+ if (typeof worktree === 'string' && !worktree.trim())
235
+ throw new Error('uso: --worktree pede auto ou um diretório que já existe');
236
+ if (typeof flags.semWorktree === 'string') {
237
+ throw new Error(`uso: --sem-worktree não leva valor (recebeu "${flags.semWorktree}")`);
238
+ }
239
+ const semWorktree = flags.semWorktree === true;
240
+ if (semWorktree && worktree !== undefined)
241
+ throw new Error('uso: --worktree e --sem-worktree se excluem; use um dos dois');
242
+ if (semWorktree)
243
+ return { criar: false, dir: null, origem: 'sem-worktree' };
244
+ if (worktree === true || worktree === 'auto')
245
+ return { criar: true, dir: null, origem: 'flag' };
246
+ if (typeof worktree === 'string')
247
+ return { criar: false, dir: worktree, origem: 'diretorio' };
248
+ return porThread ? { criar: true, dir: null, origem: 'chave' } : { criar: false, dir: null, origem: 'nenhuma' };
249
+ }
250
+ /**
251
+ * P4 do ensaio da 0.5.0: a linha que diz de onde veio a worktree da thread nova. Na criacao, so quando ela veio da
252
+ * chave (quem passou `--worktree auto` sabe de onde ela veio); no `--dry-run`, para qualquer origem.
253
+ */
254
+ function linhaDaWorktree(por, gravada, variante) {
255
+ if (!por || (gravada && por !== 'chave'))
256
+ return null;
257
+ const verbo = gravada ? 'criada' : 'seria criada';
258
+ if (por === 'chave') {
259
+ return ` worktree: ${verbo} pela chave worktree.por_thread do orkastery.yaml; para criar sem ela, use --sem-worktree`;
260
+ }
261
+ if (por === 'flag')
262
+ return ` worktree: ${verbo} pelo --worktree auto`;
263
+ return ` worktree: ${verbo} pelo ciclo ${variante ?? 'da thread'}, que exige worktree isolada`;
264
+ }
265
+ /**
266
+ * P4 do ensaio da 0.5.0: o que acontece no SHIP com a thread criada por `--sem-worktree`. O `ork ship` entrega a
267
+ * branch em que a raiz estava na criacao (`thread.base.branch`), e a policy `push_direto_na_base` do manifesto barra
268
+ * (`block`), avisa (outro valor) ou deixa passar (sem ela, ou `off`) a entrega de uma branch para ela mesma ou da
269
+ * base. Raiz fora de branch (HEAD destacado ou repositorio sem commit) nao tem branch de origem.
270
+ */
271
+ function avisoDeThreadSemWorktree(thread, manifesto, gravada = true) {
272
+ const base = manifesto.worktree.base_branch;
273
+ const branch = thread.base.branch;
274
+ const inicio = gravada
275
+ ? `Aviso: thread ${thread.id} sem worktree (--sem-worktree): ela trabalha na raiz do projeto`
276
+ : `Aviso: sem worktree (--sem-worktree), a thread ${thread.id} trabalharia na raiz do projeto`;
277
+ const ensure = `Antes do GO, ork worktree ensure ${thread.id} cria a worktree e a branch da thread`;
278
+ if (thread.base.commit === util_1.COMMIT_DESCONHECIDO) {
279
+ // Sem commit, nem o `ork worktree ensure` tem de onde partir: o caminho e o do aviso de thread sem base.
280
+ return `${inicio}, num repositório ainda sem commit, fora de qualquer branch, e o ork ship não tem branch de origem ` +
281
+ 'para entregar. Faça o primeiro commit e siga o aviso de thread sem base.';
282
+ }
283
+ if (branch === 'HEAD') {
284
+ return `${inicio}, com o HEAD destacado, fora de qualquer branch, e o ork ship não tem branch de origem para entregar. ${ensure}.`;
285
+ }
286
+ if (branch !== base) {
287
+ return `${inicio}, na branch ${branch}, e o ork ship entrega essa branch como estiver, com o que mais entrar nela; ` +
288
+ `com --para ${branch}, a entrega vira push direto, que a policy push_direto_na_base confere. ${ensure}.`;
289
+ }
290
+ const policy = manifesto.policies?.push_direto_na_base;
291
+ const efeito = policy === 'block'
292
+ ? 'o ork ship barra a entrega por push_direto_na_base (block): não há branch de thread para mergear'
293
+ : policy === undefined || policy === 'off'
294
+ ? 'o ork ship empurra a base direto, sem branch de thread (push_direto_na_base desligada)'
295
+ : `o ork ship avisa push_direto_na_base (${policy}) e empurra a base direto, sem branch de thread`;
296
+ return `${inicio}, na branch base ${base}, e ${efeito}. ${ensure}; depois do GO, os commits já estão na base e o ship não os separa.`;
297
+ }
298
+ /**
299
+ * P4 do ensaio da 0.5.0 (CHECK): no `--dry-run`, por que a criacao de verdade recusaria a worktree, em vez de preve-la.
300
+ * O ciclo que exige worktree nao aceita `--sem-worktree`, entao a saida so e sugerida para a chave e a flag.
301
+ */
302
+ function avisoDeWorktreeQueFalharia(f) {
303
+ const semWorktree = f.por === 'ciclo' ? '' : ' ou crie com --sem-worktree';
304
+ if (f.motivo === 'pasta-existe') {
305
+ return `Aviso: a pasta ${f.dir} já existe, e a criação de verdade recusaria a worktree da thread: tire a pasta${semWorktree}.`;
306
+ }
307
+ if (f.motivo === 'branch-existe') {
308
+ return `Aviso: a branch ${f.branch} já existe, e a criação de verdade recusaria a worktree da thread: tire a branch${semWorktree}.`;
309
+ }
310
+ return 'Aviso: o repositório ainda não tem commit, e a criação de verdade recusaria a worktree da thread no git worktree add: ' +
311
+ 'faça o primeiro commit antes.';
312
+ }
313
+ /** P4 do ensaio da 0.5.0: a worktree que a chave pede nao tem de onde partir num repositorio sem commit. */
314
+ function avisoDeChaveSemCommit(gravada) {
315
+ return 'Aviso: worktree.por_thread pede a worktree da thread, mas o repositório ainda não tem commit: ' +
316
+ `a thread ${gravada ? 'nasceu' : 'nasceria'} na raiz do projeto, sem worktree.`;
317
+ }
318
+ /** P4: o aviso de worktree do `ork thread new`, para o stderr, ou `null` quando nao ha o que avisar. */
319
+ function avisoDaWorktree(origem, resultado, manifesto) {
320
+ if (origem === 'sem-worktree')
321
+ return avisoDeThreadSemWorktree(resultado.thread, manifesto, resultado.gravada);
322
+ if (origem === 'chave' && !resultado.worktreePor)
323
+ return avisoDeChaveSemCommit(resultado.gravada);
324
+ return null;
325
+ }
163
326
  /** Cria a thread: valida modo contra o manifesto, gera o slug e carimba a base. */
164
327
  function novaThread(carregado, opcoes) {
165
328
  if (opcoes.dryRun)
@@ -217,6 +380,13 @@ function criarThreadSerializada(carregado, opcoes) {
217
380
  else if (opcoes.branch) {
218
381
  throw new Error('--branch so vale com --ciclo merge-branch');
219
382
  }
383
+ // `greenfield`, `merge-branch` e `feature-xl-faseada` exigem worktree isolada: sao os
384
+ // ciclos que escrevem codigo em paralelo com outras threads.
385
+ const exigeWorktree = variante ? (0, ciclos_1.definicaoDaVariante)(variante).exigeWorktree : false;
386
+ // P4 do ensaio da 0.5.0 (D4): o ciclo que exige worktree nao nasce sem ela, e a recusa sai antes de gravar.
387
+ if (exigeWorktree && opcoes.origemDaWorktree === 'sem-worktree') {
388
+ throw new Error(`o ciclo ${variante} exige worktree isolada: crie a thread sem --sem-worktree ou escolha outro ciclo`);
389
+ }
220
390
  const reservation = opcoes.reservation;
221
391
  const operation = reservation ? (0, creation_operation_store_1.readCreationOperation)(raiz, reservation.operationId, reservation.principal) : null;
222
392
  if (operation && (!operation.reserved.threadIds.includes(reservation.threadId)
@@ -241,11 +411,11 @@ function criarThreadSerializada(carregado, opcoes) {
241
411
  && fs.readdirSync(dirThread(raiz, reservation.threadId)).some((name) => !/^\.thread\.json\..+\.tmp$/.test(name))) {
242
412
  throw new Error('creation.conflict: diretório reservado sem origem confirmada');
243
413
  }
414
+ // P4 do ensaio da 0.5.0 (CHECK): o ensaio usa o mesmo id livre da criacao (o `idDisponivel` so le o disco), para a
415
+ // worktree que ele mostra ser a que a criacao usaria.
244
416
  const { id, assunto } = reservation
245
417
  ? { id: reservation.threadId, assunto: reservation.threadId.slice(abbrev.length + 1) }
246
- : opcoes.dryRun
247
- ? { id: `${abbrev}-${assuntoBruto}`, assunto: assuntoBruto }
248
- : idDisponivel(raiz, abbrev, assuntoBruto);
418
+ : idDisponivel(raiz, abbrev, assuntoBruto);
249
419
  const slug = opcoes.slug ?? (0, slug_1.montarSlug)(abbrev, assunto, blocos[0].slugFases);
250
420
  // O override `--slug` do builder tambem passa pela regex canonica.
251
421
  if (!(0, slug_1.slugValido)(slug)) {
@@ -281,13 +451,38 @@ function criarThreadSerializada(carregado, opcoes) {
281
451
  };
282
452
  if (variante === 'feature-xl-faseada')
283
453
  thread.fatias = opcoes.fatias ?? 3;
284
- if (opcoes.dryRun)
285
- return { thread, gravada: false };
454
+ // P4 do ensaio da 0.5.0: por que a thread ganha a worktree. O ensaio e a chave preveem a worktree antes; a pedida
455
+ // por flag ou por ciclo segue direto ao git, como era. A pedida so pela chave espera o primeiro commit (D3): sem
456
+ // commit, a thread fica na raiz, como antes. O ciclo que exige worktree vence a chave e a flag na origem: a worktree
457
+ // e dele, e o `--sem-worktree` que a linha da chave sugeriria ele recusa (CHECK, rodada 2).
458
+ const pedeWorktree = opcoes.criarWorktree === true || exigeWorktree;
459
+ const pelaChave = opcoes.criarWorktree === true && opcoes.origemDaWorktree === 'chave';
460
+ const prevista = pedeWorktree && (opcoes.dryRun || pelaChave)
461
+ ? worktreePrevista(carregado, id, slug, { branchExistente: opcoes.branch }) : null;
462
+ const worktreePor = !pedeWorktree || (pelaChave && !exigeWorktree && prevista?.falha === 'sem-commit') ? null
463
+ : exigeWorktree ? 'ciclo' : pelaChave ? 'chave' : 'flag';
464
+ if (opcoes.dryRun) {
465
+ // P4 (D8): o ensaio mostra a worktree que a criacao usaria; quando ela recusaria, diz por que, em vez de preve-la.
466
+ if (worktreePor && prevista?.falha) {
467
+ return { thread, gravada: false, worktreePor: null,
468
+ worktreeFalharia: { motivo: prevista.falha, dir: prevista.dir, branch: prevista.branch, por: worktreePor } };
469
+ }
470
+ if (worktreePor && prevista?.commit) {
471
+ thread.worktree = prevista.dir;
472
+ thread.base = { branch: prevista.branch, commit: prevista.commit };
473
+ }
474
+ return { thread, gravada: false, worktreePor };
475
+ }
476
+ // P4 (CHECK): a worktree pedida so pela chave, com a pasta ou a branch ja ocupadas, recusa dizendo de onde veio o
477
+ // pedido e qual e a saida.
478
+ const ocupadaPelaChave = pelaChave && !exigeWorktree && prevista &&
479
+ (prevista.falha === 'pasta-existe' || prevista.falha === 'branch-existe') ? prevista : null;
480
+ if (ocupadaPelaChave) {
481
+ const qual = ocupadaPelaChave.falha === 'pasta-existe' ? `a pasta ${ocupadaPelaChave.dir}` : `a branch ${ocupadaPelaChave.branch}`;
482
+ throw new Error(`${qual} já existe, e a chave worktree.por_thread pede a worktree da thread: tire-a ou crie com --sem-worktree`);
483
+ }
286
484
  let worktreeCriada = null;
287
- // `greenfield`, `merge-branch` e `feature-xl-faseada` exigem worktree isolada: sao os
288
- // ciclos que escrevem codigo em paralelo com outras threads.
289
- const exigeWorktree = variante ? (0, ciclos_1.definicaoDaVariante)(variante).exigeWorktree : false;
290
- if (opcoes.criarWorktree || exigeWorktree) {
485
+ if (worktreePor) {
291
486
  worktreeCriada = criarWorktree(carregado, id, slug, {
292
487
  branchExistente: opcoes.branch,
293
488
  });
@@ -309,6 +504,8 @@ function criarThreadSerializada(carregado, opcoes) {
309
504
  base: thread.base,
310
505
  projeto: manifesto.project.name,
311
506
  worktree: thread.worktree,
507
+ // P4 do ensaio da 0.5.0 (D9): quem abriu a thread sem worktree de proposito fica no rastro.
508
+ ...(opcoes.origemDaWorktree === 'sem-worktree' ? { semWorktree: true } : {}),
312
509
  variante,
313
510
  branchDeOrigem: opcoes.branch ?? null,
314
511
  maquina: thread.maquina,
@@ -319,10 +516,11 @@ function criarThreadSerializada(carregado, opcoes) {
319
516
  dir: worktreeCriada.dir,
320
517
  branch: worktreeCriada.branch,
321
518
  base: manifesto.worktree.base_branch,
519
+ origem: worktreePor,
322
520
  fonte: 'git worktree list --porcelain',
323
521
  });
324
522
  }
325
- return { thread, gravada: true };
523
+ return { thread, gravada: true, worktreePor };
326
524
  }
327
525
  /**
328
526
  * O bloco de loop que conduz a fase DESTA thread.
@@ -362,7 +560,7 @@ function resumoDaThread(thread, conducao) {
362
560
  linhas.push(` ciclo ${thread.variante ?? 'padrao do modo'}`);
363
561
  linhas.push(` fase ${thread.faseAtual}`);
364
562
  linhas.push(` status ${thread.status}`);
365
- linhas.push(` base ${thread.base.branch} @ ${thread.base.commit.slice(0, 8)}`);
563
+ linhas.push(` base ${thread.base.branch} @ ${(0, util_1.shaCurto)(thread.base.commit)}`);
366
564
  linhas.push(` worktree ${thread.worktree ?? '(raiz do projeto)'}`);
367
565
  // I-36 (T17): quem passa a leitura da conducao ve a linha unica; sem ela o resumo fica como era.
368
566
  if (conducao !== undefined)