@orkastery/cli 0.2.0 → 0.4.1

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 (257) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +86 -65
  3. package/adapters/README.md +30 -15
  4. package/adapters/claude-code/.claude-plugin/plugin.json +11 -5
  5. package/adapters/claude-code/README.md +110 -8
  6. package/adapters/claude-code/commands/check.md +4 -3
  7. package/adapters/claude-code/commands/goal.md +3 -2
  8. package/adapters/claude-code/commands/master.md +3 -2
  9. package/adapters/claude-code/commands/onboarding.md +21 -0
  10. package/adapters/claude-code/commands/ork.md +99 -27
  11. package/adapters/claude-code/hooks/hooks.json +6 -1
  12. package/adapters/claude-code/hooks/ork-guard.js +1 -1
  13. package/adapters/claude-code/hooks/ork-sensor.js +66 -0
  14. package/adapters/codex/skills/ork/SKILL.md +57 -0
  15. package/adapters/hermes/README.md +161 -0
  16. package/adapters/hermes/bin/ork-abrir-thread.sh +2 -0
  17. package/adapters/hermes/bin/ork-brain.sh +4 -0
  18. package/adapters/hermes/bin/ork-hitl-answer.py +82 -0
  19. package/adapters/hermes/bin/ork-maestro.sh +6 -0
  20. package/adapters/hermes/bin/ork-master-enviar.py +84 -0
  21. package/adapters/hermes/bin/ork-pulse-enviar.py +47 -0
  22. package/adapters/hermes/hermes.plugin.json +24 -4
  23. package/adapters/hermes/hitl-ingress/__init__.py +356 -0
  24. package/adapters/hermes/hitl-ingress/plugin.yaml +4 -0
  25. package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +75 -22
  26. package/adapters/openclaw/README.md +136 -27
  27. package/adapters/openclaw/bin/ork-brain.sh +4 -0
  28. package/adapters/openclaw/construir.sh +19 -0
  29. package/adapters/openclaw/dist/hitl-ingress.js +199 -0
  30. package/adapters/openclaw/dist/index.js +437 -0
  31. package/adapters/openclaw/openclaw.plugin.json +39 -134
  32. package/adapters/openclaw/package.json +26 -0
  33. package/adapters/openclaw/src/hitl-ingress.ts +180 -0
  34. package/adapters/openclaw/src/index.ts +460 -0
  35. package/adapters/openclaw/src/tipos-openclaw.d.ts +58 -0
  36. package/adapters/openclaw/tsconfig.json +15 -0
  37. package/assets/docs/markdownlint-cli2.jsonc +22 -0
  38. package/assets/docs/padroes/documentacao-de-produto.md +133 -0
  39. package/assets/docs/padroes/roadmap-de-produto.md +98 -0
  40. package/assets/docs/produto/README.md +14 -0
  41. package/assets/docs/produto/_modelo-feature.md +59 -0
  42. package/assets/docs/roadmap/README.md +14 -0
  43. package/assets/docs/roadmap/_modelo-item.md +73 -0
  44. package/assets/orkmind-native-schema.json +32 -0
  45. package/assets/orkmind_bridge.py +357 -0
  46. package/assets/orkmind_fixture.py +67 -0
  47. package/assets/orkmind_prospective.py +138 -0
  48. package/assets/reference-tariffs-i07.json +23 -0
  49. package/dist/adapters/claude-bg.js +488 -39
  50. package/dist/adapters/codex-controller-sensor.js +449 -0
  51. package/dist/adapters/codex-controller-worker.js +361 -0
  52. package/dist/adapters/codex-controller.js +252 -0
  53. package/dist/adapters/codex-events.js +205 -0
  54. package/dist/adapters/codex-question.js +26 -0
  55. package/dist/adapters/codex-runner.js +133 -0
  56. package/dist/adapters/codex.js +394 -0
  57. package/dist/agents-md.js +84 -0
  58. package/dist/auditoria.js +12 -9
  59. package/dist/auditrun.js +5 -4
  60. package/dist/board.js +174 -23
  61. package/dist/branch-de-estado.js +136 -0
  62. package/dist/canarios-hitl.js +177 -0
  63. package/dist/canarios-i43.js +543 -0
  64. package/dist/canarios-pulse.js +147 -0
  65. package/dist/canarios-sensores.js +129 -0
  66. package/dist/canarios.js +111 -2
  67. package/dist/catalogo.js +9 -0
  68. package/dist/ci.js +223 -0
  69. package/dist/ciclos.js +2 -1
  70. package/dist/claim-lint.js +64 -0
  71. package/dist/claims.js +60 -0
  72. package/dist/company-brain-capture.js +195 -0
  73. package/dist/company-brain-cli.js +123 -0
  74. package/dist/company-brain-client.js +61 -0
  75. package/dist/company-brain-contract.js +166 -0
  76. package/dist/company-brain-journal.js +192 -0
  77. package/dist/company-brain-mcp.js +33 -0
  78. package/dist/company-brain-migration.js +60 -0
  79. package/dist/company-brain-source.js +177 -0
  80. package/dist/company-brain-worker.js +18 -0
  81. package/dist/conducao-texto.js +61 -0
  82. package/dist/conducao.js +876 -0
  83. package/dist/contrato-publico.js +39 -0
  84. package/dist/creation-operation-store.js +251 -0
  85. package/dist/creation-operation.js +148 -0
  86. package/dist/decisao-autonoma.js +183 -0
  87. package/dist/delegation.js +79 -0
  88. package/dist/demo.js +120 -0
  89. package/dist/docs.js +828 -0
  90. package/dist/doctor.js +184 -25
  91. package/dist/entrega-pr.js +117 -0
  92. package/dist/escopo-escrita.js +71 -0
  93. package/dist/estado-thread.js +222 -0
  94. package/dist/evalrunner.js +12 -0
  95. package/dist/fabrica-estado.js +329 -0
  96. package/dist/fabrica-publicar.js +76 -0
  97. package/dist/fix.js +22 -1
  98. package/dist/gates.js +59 -11
  99. package/dist/handoff.js +39 -23
  100. package/dist/hitl-canais.js +333 -0
  101. package/dist/hitl-classificacao.js +131 -0
  102. package/dist/hitl-contract.js +465 -0
  103. package/dist/hitl-estado.js +136 -0
  104. package/dist/hitl-gates.js +554 -0
  105. package/dist/hitl-ingress-receipt.js +382 -0
  106. package/dist/hitl-local-atestado.js +57 -0
  107. package/dist/hitl-local-receipt.js +328 -0
  108. package/dist/hitl-local.js +143 -0
  109. package/dist/hitl-lock.js +139 -0
  110. package/dist/hitl-lote.js +223 -0
  111. package/dist/hitl-native-offer.js +97 -0
  112. package/dist/hitl-native.js +65 -0
  113. package/dist/hitl-presentation.js +209 -0
  114. package/dist/hitl-public-receipt.js +176 -0
  115. package/dist/hitl-resumo.js +209 -0
  116. package/dist/hitl-sessions.js +475 -0
  117. package/dist/hitl.js +711 -0
  118. package/dist/horario.js +269 -0
  119. package/dist/hosts.js +198 -22
  120. package/dist/index.js +1741 -94
  121. package/dist/indice.js +99 -0
  122. package/dist/init.js +28 -3
  123. package/dist/integracoes-locais.js +17 -0
  124. package/dist/leases.js +65 -21
  125. package/dist/ledger-stats.js +272 -0
  126. package/dist/ledger.js +109 -2
  127. package/dist/licoes.js +221 -0
  128. package/dist/liveness.js +218 -0
  129. package/dist/maestro-actions.js +51 -0
  130. package/dist/maestro-authority.js +152 -0
  131. package/dist/maestro-cli.js +97 -0
  132. package/dist/maestro-contract.js +55 -0
  133. package/dist/maestro-discovery.js +132 -0
  134. package/dist/maestro-runtime.js +268 -0
  135. package/dist/maestro-snapshot.js +86 -0
  136. package/dist/maestro-sources.js +224 -0
  137. package/dist/manifest.js +165 -6
  138. package/dist/maquina.js +102 -0
  139. package/dist/master-audit.js +101 -0
  140. package/dist/master-batch.js +43 -0
  141. package/dist/master-digest.js +149 -0
  142. package/dist/master-migracao.js +155 -0
  143. package/dist/master.js +361 -77
  144. package/dist/mcp-artifacts.js +234 -0
  145. package/dist/mcp-git.js +433 -0
  146. package/dist/mcp-install.js +310 -0
  147. package/dist/mcp-maestro.js +25 -0
  148. package/dist/mcp-server.js +469 -0
  149. package/dist/mcp-ship.js +436 -0
  150. package/dist/mcp-verify.js +173 -0
  151. package/dist/memoria-humana.js +207 -0
  152. package/dist/memoria.js +314 -78
  153. package/dist/memory-migration.js +279 -0
  154. package/dist/memory-prospective.js +148 -0
  155. package/dist/modos-migracao.js +191 -0
  156. package/dist/modos.js +151 -15
  157. package/dist/monitor-lock.js +110 -0
  158. package/dist/objective.js +449 -0
  159. package/dist/ocupacao.js +267 -0
  160. package/dist/onboarding.js +303 -0
  161. package/dist/orkmind.js +389 -90
  162. package/dist/orquestracao.js +96 -69
  163. package/dist/phase.js +612 -151
  164. package/dist/playbook-capabilities.js +150 -0
  165. package/dist/playbook-contracts.js +188 -0
  166. package/dist/policies.js +72 -0
  167. package/dist/portfolio-context.js +61 -0
  168. package/dist/portfolio.js +180 -0
  169. package/dist/preflight.js +207 -0
  170. package/dist/process-audit.js +112 -0
  171. package/dist/project-state.js +93 -0
  172. package/dist/prompts.js +25 -5
  173. package/dist/prova-minima.js +112 -0
  174. package/dist/pulse-cadencia.js +163 -0
  175. package/dist/pulse-consentimento.js +260 -0
  176. package/dist/pulse-delivery.js +323 -0
  177. package/dist/pulse-resposta.js +542 -0
  178. package/dist/pulse.js +262 -0
  179. package/dist/ratelimit.js +35 -2
  180. package/dist/recall.js +80 -3
  181. package/dist/redacao-saida.js +45 -0
  182. package/dist/redacao-url.js +66 -0
  183. package/dist/retry.js +640 -118
  184. package/dist/roadmap-reservas.js +243 -0
  185. package/dist/runtime-ambiente.js +38 -0
  186. package/dist/runtime-context.js +215 -0
  187. package/dist/runtime-profiles.js +869 -0
  188. package/dist/runtimes.js +103 -0
  189. package/dist/sandbox.js +9 -0
  190. package/dist/session-events.js +203 -0
  191. package/dist/session-watcher-claude.js +637 -0
  192. package/dist/session-watcher.js +700 -0
  193. package/dist/sessoes-adopt.js +154 -0
  194. package/dist/sessoes-inventario.js +159 -0
  195. package/dist/sessoes.js +16 -35
  196. package/dist/setup.js +691 -0
  197. package/dist/ship.js +168 -19
  198. package/dist/slug.js +1 -1
  199. package/dist/thread-close.js +115 -0
  200. package/dist/thread.js +142 -28
  201. package/dist/tokens.js +1 -1
  202. package/dist/util.js +4 -2
  203. package/dist/verify-sandbox.js +265 -0
  204. package/dist/verify.js +270 -25
  205. package/dist/versao.js +60 -0
  206. package/dist/worktree.js +31 -2
  207. package/dist/write-activation.js +291 -0
  208. package/dist/yaml.js +5 -1
  209. package/eval/casos/master-metrics.json +1 -1
  210. package/eval/casos/onboarding.json +102 -0
  211. package/eval/casos/orkastery-bootstrap.json +37 -3
  212. package/eval/casos/scope-check-capability-map.json +45 -7
  213. package/eval/casos/ship-release.json +3 -3
  214. package/eval/fixtures/b0-slug-e-modos/caso.json +35 -22
  215. package/eval/fixtures/b2-master-log/caso.json +1 -1
  216. package/eval/fixtures/fx-adapter-editado-detectado/caso.json +19 -0
  217. package/eval/fixtures/fx-auto-quiet/caso.json +11 -0
  218. package/eval/fixtures/fx-blanket-approve/caso.json +10 -0
  219. package/eval/fixtures/fx-check-runtime-cruzado/caso.json +20 -0
  220. package/eval/fixtures/fx-codex-dry/caso.json +18 -0
  221. package/eval/fixtures/fx-donewhen-executavel/caso.json +15 -0
  222. package/eval/fixtures/fx-estado-dividido/caso.json +15 -0
  223. package/eval/fixtures/fx-fase-orfa/caso.json +116 -0
  224. package/eval/fixtures/fx-happy/caso.json +6 -4
  225. package/eval/fixtures/fx-hitl-latency/caso.json +20 -0
  226. package/eval/fixtures/fx-indice-reversao/caso.json +25 -0
  227. package/eval/fixtures/fx-listagem-abertas/caso.json +16 -0
  228. package/eval/fixtures/fx-maestro-bootstrap/caso.json +21 -0
  229. package/eval/fixtures/fx-modo-aposentado-escritor/caso.json +21 -0
  230. package/eval/fixtures/fx-modo-aposentado-leitor/caso.json +16 -0
  231. package/eval/fixtures/fx-objective-oscillation/caso.json +14 -0
  232. package/eval/fixtures/fx-omnicanal/caso.json +17 -0
  233. package/eval/fixtures/fx-sensores-runtime/caso.json +17 -0
  234. package/monitor/company-brain.cjs +17 -0
  235. package/monitor/pulse-scope.cjs +45 -0
  236. package/monitor/pulse.cron +18 -0
  237. package/monitor/varredura-pulse.sh +27 -0
  238. package/package.json +28 -7
  239. package/references/definition-of-done.md +2 -2
  240. package/schemas/claims.schema.json +25 -0
  241. package/schemas/company-brain.schema.json +1158 -0
  242. package/schemas/creation-operation.schema.json +365 -0
  243. package/schemas/maestro-snapshot.schema.json +2716 -0
  244. package/skills/README.md +3 -3
  245. package/skills/core/onboarding/SKILL.md +50 -0
  246. package/skills/core/orkastery-bootstrap/SKILL.md +95 -65
  247. package/skills/core/thread-state/SKILL.md +3 -1
  248. package/skills/governance/decision-triage/SKILL.md +4 -4
  249. package/skills/governance/narrative-guardian/SKILL.md +1 -1
  250. package/skills/governance/roadmap-keeper/SKILL.md +1 -1
  251. package/skills/governance/scope-check-capability-map/SKILL.md +17 -7
  252. package/skills/observability/thread-tracing/SKILL.md +1 -1
  253. package/skills/phases/check-quality/SKILL.md +2 -2
  254. package/skills/phases/goal-definition/SKILL.md +1 -1
  255. package/skills/phases/master-metrics/SKILL.md +9 -7
  256. package/skills/phases/plan-specification/SKILL.md +1 -1
  257. package/skills/phases/ship-release/SKILL.md +1 -1
@@ -0,0 +1,637 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.THREAD_REMOVIDA = exports.ARTEFATO_DA_FASE = exports.INTERVALO_WATCH_CLAUDE_MS = void 0;
37
+ exports.normalizarNativo = normalizarNativo;
38
+ exports.stopCorrelacionado = stopCorrelacionado;
39
+ exports.bloqueioPendente = bloqueioPendente;
40
+ exports.classificarSessaoClaude = classificarSessaoClaude;
41
+ exports.falhaDeContaDaTranscricao = falhaDeContaDaTranscricao;
42
+ exports.comFalhaDeConta = comFalhaDeConta;
43
+ exports.processoNativo = processoNativo;
44
+ exports.estadoDoArtefato = estadoDoArtefato;
45
+ exports.saidaDoArtefato = saidaDoArtefato;
46
+ exports.vereditoDoParecer = vereditoDoParecer;
47
+ exports.fimDoIntervalo = fimDoIntervalo;
48
+ exports.eventosDoIntervalo = eventosDoIntervalo;
49
+ exports.headDaWorktree = headDaWorktree;
50
+ exports.commitConferido = commitConferido;
51
+ exports.provaDoOrk = provaDoOrk;
52
+ exports.fonteRegistrada = fonteRegistrada;
53
+ exports.fonteClaudeDoDespacho = fonteClaudeDoDespacho;
54
+ exports.validarFonteClaude = validarFonteClaude;
55
+ exports.garantirFonteClaude = garantirFonteClaude;
56
+ exports.observarClaudeSobLock = observarClaudeSobLock;
57
+ /**
58
+ * I-34: conclusão de sessões claude-bg. O turno termina pelo Stop ingerido dos hooks do plugin,
59
+ * correlacionado ao despacho; o `done` do `claude agents --json --all` é a leitura que o Claude
60
+ * Code faz do texto final do agente (D12), então é condição necessária e nunca suficiente: a
61
+ * fase só conclui com prova gravada pelo próprio `ork` no intervalo do despacho. Silêncio, Stop
62
+ * isolado e encerramento externo não provam conclusão; lacuna vira motivo tipado já existente.
63
+ * O lock, a pasta de sensores e o processo destacado continuam no `session-watcher`; este
64
+ * módulo não o importa, para não criar ciclo.
65
+ */
66
+ const fs = __importStar(require("node:fs"));
67
+ const path = __importStar(require("node:path"));
68
+ const node_crypto_1 = require("node:crypto");
69
+ const claude_bg_1 = require("./adapters/claude-bg");
70
+ const ratelimit_1 = require("./ratelimit");
71
+ const runtime_profiles_1 = require("./runtime-profiles");
72
+ const codex_runner_1 = require("./adapters/codex-runner");
73
+ const session_events_1 = require("./session-events");
74
+ const ledger_1 = require("./ledger");
75
+ const thread_1 = require("./thread");
76
+ const util_1 = require("./util");
77
+ const hitl_1 = require("./hitl");
78
+ /** Uma consulta nativa custa cerca de 0,3 s; cinco segundos mantém o custo abaixo de 10%. */
79
+ exports.INTERVALO_WATCH_CLAUDE_MS = 5000;
80
+ const LIMITE_EVIDENCIA_BYTES = 4096;
81
+ /** D15 (5): teto dos textos nativos (`state`, `status`, `id`, `kind`) e do erro do diagnóstico. */
82
+ const LIMITE_TEXTO_NATIVO = 64;
83
+ const LIMITE_ERRO_DIAGNOSTICO = 512;
84
+ const LIMITE_PLANO_BYTES = 1024 * 1024;
85
+ const ARQUIVO_PLANO = 'docs/plan.md';
86
+ /** D12: artefato de cada fase que entrega documento; sem ele o PLAN mantém `artifact.missing` (D2). */
87
+ exports.ARTEFATO_DA_FASE = { GOAL: 'docs/goal.md', PLAN: ARQUIVO_PLANO, CHECK: 'docs/check.md' };
88
+ const SHA_COMMIT = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
89
+ /** Motivo de encerramento quando a thread some do disco durante a observação (achado 1). */
90
+ exports.THREAD_REMOVIDA = 'thread removida do disco; observação encerrada sem recriar a pasta';
91
+ const SHA256 = /^[a-f0-9]{64}$/;
92
+ /**
93
+ * D15 (5): texto vindo do `claude agents` ou derivado dele entra no ledger sem caracteres de
94
+ * controle e com teto em caracteres, sem partir par substituto. Não string ou vazio vira `null`.
95
+ * A classificação continua comparando o valor exato; isto vale só para o que é gravado.
96
+ */
97
+ function normalizarNativo(v, limite = LIMITE_TEXTO_NATIVO) {
98
+ if (typeof v !== 'string')
99
+ return null;
100
+ const limpo = Array.from(v.replace(/\p{Cc}/gu, '').trim()).slice(0, limite).join('');
101
+ return limpo === '' ? null : limpo;
102
+ }
103
+ /**
104
+ * Atividade que supera um Stop. A notificação de ociosidade não é atividade, e o SubagentStop
105
+ * também não: ele marca o fim de um subagente (medido na sessão b8fa6f86 da i32, 2 s depois do
106
+ * Stop). Se o agente principal retomar, vêm heartbeats e o estado nativo deixa de ser `done`.
107
+ */
108
+ function atividade(e) {
109
+ if (e.tipo === 'runtime_event')
110
+ return !(e.sensor === 'notification' && e.notificationType === 'idle_prompt');
111
+ return ['commit', 'sessao_bloqueada', 'runtime_stop'].includes(e.tipo);
112
+ }
113
+ /** Último Stop do despacho, emitido pelos hooks, sem atividade posterior. */
114
+ function stopCorrelacionado(eventos, sessao) {
115
+ const inicio = Date.parse(sessao.despachadaEm);
116
+ if (!Number.isFinite(inicio))
117
+ return null;
118
+ const correlacionados = eventos.filter(e => e.sessionId === sessao.sessionId && e.despachoEm === sessao.despachadaEm &&
119
+ e.fase === sessao.fase && e.runtime === 'claude-bg' && Number.isFinite(Date.parse(e.ts)) && Date.parse(e.ts) >= inicio);
120
+ let ultimo = -1;
121
+ correlacionados.forEach((e, i) => {
122
+ if (e.tipo === 'runtime_stop' && e.sensor === 'stop' && e.fonte === 'ork sessions event' &&
123
+ typeof e.sensorEventId === 'string' && SHA256.test(e.sensorEventId))
124
+ ultimo = i;
125
+ });
126
+ if (ultimo < 0 || correlacionados.slice(ultimo + 1).some(atividade))
127
+ return null;
128
+ return correlacionados[ultimo];
129
+ }
130
+ /**
131
+ * O último entre bloqueio, destravamento e Stop da sessão, depois do despacho, é bloqueio?
132
+ * O Stop resolve o bloqueio anterior: o turno só termina depois que o prompt de permissão
133
+ * foi respondido (aprovado ou negado), e o estado nativo `done` confirma que não há espera.
134
+ */
135
+ function bloqueioPendente(eventos, sessao) {
136
+ const inicio = Date.parse(sessao.despachadaEm);
137
+ const ultimo = eventos.filter(e => e.sessionId === sessao.sessionId &&
138
+ ['sessao_bloqueada', 'sessao_destravada', 'runtime_stop'].includes(e.tipo) && !(Date.parse(e.ts) < inicio)).at(-1);
139
+ return ultimo?.tipo === 'sessao_bloqueada';
140
+ }
141
+ /**
142
+ * D2: tabela de classificação. Pura sobre as entradas, exceto o cursor, que carrega os
143
+ * instantes de primeira observação entre chamadas. Sem cursor persistido (leitura única do
144
+ * MCP), os prazos nunca vencem e só as classificações imediatas aparecem.
145
+ */
146
+ function classificarSessaoClaude(e) {
147
+ const { cursor, agoraMs: agora } = e;
148
+ const stop = stopCorrelacionado(e.eventos, e.sessao);
149
+ const espera = (motivo) => ({ terminal: false, espera: motivo, stop });
150
+ const bloqueio = (fonte, motivo = 'runtime.unavailable', conclusaoNativa = false) => ({ terminal: true, classificacao: 'gate_blocked', motivo, fonte, conclusaoNativa, stop });
151
+ // D12: o `done` classifica o texto final do agente; a fase só conclui com a prova do `ork`.
152
+ const concluir = (sinal, conclusaoNativa) => {
153
+ const prova = e.prova();
154
+ const c = !prova.ok
155
+ ? { ...bloqueio(`${sinal}; sem prova do ork: ${prova.fonte}`, prova.motivo ?? 'human.pending', conclusaoNativa),
156
+ diagnostico: `sem prova do ork: ${prova.fonte}` }
157
+ : e.pausaAoFim ? bloqueio(`${sinal}; ${prova.fonte}; pausa prevista ao fim do bloco`, 'human.pending', conclusaoNativa)
158
+ : { terminal: true, classificacao: 'fase_concluida', motivo: null, fonte: `${sinal}; ${prova.fonte}`, conclusaoNativa, stop };
159
+ return { ...c, dependeDoStop: true };
160
+ };
161
+ const venceu = (desde) => agora - desde >= e.limiteMs;
162
+ // Sem prova para concluir nem para bloquear: `runtime.unavailable` reexecutaria o mesmo prompt
163
+ // sobre uma fase que pode ter terminado. O observador encerra sem gate e o radar de liveness
164
+ // decide com o humano (`done` não é terminal de retry automático).
165
+ const inconclusivo = (motivo, desde) => venceu(desde)
166
+ ? { terminal: false, espera: `${motivo} além do limite de estabilidade; conclusão não inferida`, stop, inconclusivo: true }
167
+ : espera(motivo);
168
+ if (!e.consulta.ok) {
169
+ cursor.consultaFalhaDesde ??= agora;
170
+ return inconclusivo(`consulta nativa indisponível: ${e.consulta.detalhe}`, cursor.consultaFalhaDesde);
171
+ }
172
+ delete cursor.consultaFalhaDesde;
173
+ const r = e.registro;
174
+ if (!r) {
175
+ cursor.ausenteDesde ??= agora;
176
+ return venceu(cursor.ausenteDesde) ? bloqueio('sessão ausente do claude agents além do limite de estabilidade')
177
+ : espera('sessão ausente da consulta nativa');
178
+ }
179
+ delete cursor.ausenteDesde;
180
+ // Sinônimos que o `ork_observe` já tratava como trabalho.
181
+ const estado = r.state === 'running' || r.state === 'busy' ? 'working' : r.state;
182
+ if (estado !== 'done')
183
+ delete cursor.doneDesde;
184
+ if (estado !== 'working' && estado !== 'blocked')
185
+ delete cursor.mortoDesde;
186
+ if (['done', 'failed', 'stopped', 'working', 'blocked'].includes(estado ?? ''))
187
+ delete cursor.desconhecidoDesde;
188
+ switch (estado) {
189
+ case 'done': {
190
+ const pendente = bloqueioPendente(e.eventos, e.sessao);
191
+ if (stop && !pendente)
192
+ return concluir('terminal nativo done com Stop correlacionado', true);
193
+ const falta = stop ? 'terminal nativo done com sessao_bloqueada pendente' : 'terminal nativo done sem Stop correlacionado';
194
+ cursor.doneDesde ??= agora;
195
+ return inconclusivo(falta, cursor.doneDesde);
196
+ }
197
+ // D17: depois de um Stop correlacionado, nem `failed` nem `stopped` reexecutam a fase, que pode
198
+ // ter terminado sobre a worktree já alterada. `stopped` segue a prova, como o processo morto
199
+ // (D13); `failed` não conclui nem reexecuta: o humano decide. Sem Stop, falha de execução.
200
+ case 'failed':
201
+ if (stop)
202
+ return { ...bloqueio('terminal nativo failed depois de Stop correlacionado; a sessão falhou depois de ' +
203
+ 'encerrar o turno e o humano decide', 'human.pending'), dependeDoStop: true,
204
+ diagnostico: 'a sessão falhou (failed) depois de encerrar o turno; conclusão não provada' };
205
+ return bloqueio('terminal nativo failed sem Stop correlacionado');
206
+ case 'stopped':
207
+ if (stop)
208
+ return concluir('Stop correlacionado e sessão encerrada externamente (stopped) sem done', false);
209
+ return bloqueio('sessão encerrada externamente (stopped) sem terminal done nem Stop correlacionado');
210
+ case 'working':
211
+ case 'blocked': {
212
+ if (e.processo(r.pid, cursor) === 'ausente') {
213
+ cursor.mortoDesde ??= agora;
214
+ if (agora - cursor.mortoDesde < exports.INTERVALO_WATCH_CLAUDE_MS)
215
+ return espera('processo sem prova de vida; aguardando a segunda observação');
216
+ // D13: turno encerrado pelo Stop e processo morto não é falha de infraestrutura. Reexecutar
217
+ // redespacharia uma fase que pode ter terminado sobre a worktree já alterada: vale a prova (D12).
218
+ if (stop)
219
+ return concluir(`Stop correlacionado e processo encerrado sem done (estado ${estado})`, false);
220
+ return bloqueio(`processo da sessão morreu sem terminal nativo nem Stop correlacionado (estado ${estado})`);
221
+ }
222
+ delete cursor.mortoDesde;
223
+ return espera(estado === 'blocked' ? 'sessão bloqueada à espera humana' : 'sessão trabalhando');
224
+ }
225
+ default:
226
+ cursor.desconhecidoDesde ??= agora;
227
+ return inconclusivo(`estado nativo desconhecido (${normalizarNativo(estado) ?? 'ausente'})`, cursor.desconhecidoDesde);
228
+ }
229
+ }
230
+ const LIMITE_CAUDA_TRANSCRICAO = 65536;
231
+ /**
232
+ * I-33 (N4): o texto de um erro de API da transcricao e o codigo do erro e o texto que o Claude Code
233
+ * mostrou (`message.content[].text`), com segredos redigidos. Os metadados do `message` (id, modelo,
234
+ * `stop_*`, uso) ficam fora: sem eles, o trecho gravado no ledger e a frase que classificou.
235
+ */
236
+ function textoDoErroDeApi(e) {
237
+ const m = e.message;
238
+ const conteudo = typeof m === 'string' ? m : m && typeof m === 'object' ? m.content : undefined;
239
+ const textos = typeof conteudo === 'string' ? [conteudo] : Array.isArray(conteudo)
240
+ ? conteudo.map(c => (c && typeof c === 'object' && typeof c.text === 'string' ? c.text : ''))
241
+ : [];
242
+ const codigo = typeof e.error === 'string' ? e.error : '';
243
+ return (0, hitl_1.redigirSegredos)([codigo, textos.filter(Boolean).join(' ')].filter(Boolean).join(': ')).slice(0, 4096);
244
+ }
245
+ /**
246
+ * I-33 (D11b): falha de conta registrada pelo proprio Claude Code na transcricao da sessao, no
247
+ * diretorio do perfil (ou no implicito, sem perfil). So entram linhas marcadas como erro de API
248
+ * (`isApiErrorMessage` ou campo `error`): texto do agente nunca vira motivo. Leitura limitada
249
+ * da cauda, sem seguir link; ausencia de transcricao e ausencia de evidencia. Vale o ULTIMO erro de API
250
+ * (N3), o que encerrou a sessao. O codigo `rate_limit`
251
+ * do Claude Code cobre tanto o limite do plano quanto o 429 transitorio ("Request rejected (429)",
252
+ * "not your usage limit"): quem separa e o criterio unico da D16 (`naturezaDoLimite`), pela frase.
253
+ */
254
+ function falhaDeContaDaTranscricao(perfil, cwd, sessionId, agoraMs = Date.now()) {
255
+ if (!/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(sessionId))
256
+ return null;
257
+ const arquivo = path.join((0, runtime_profiles_1.diretorioEfetivo)('claude-bg', perfil), 'projects', path.resolve(cwd).replace(/[^a-zA-Z0-9-]/g, '-'), `${sessionId}.jsonl`);
258
+ let fd;
259
+ try {
260
+ fd = fs.openSync(arquivo, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
261
+ }
262
+ catch {
263
+ return null;
264
+ }
265
+ let cauda;
266
+ try {
267
+ const st = fs.fstatSync(fd);
268
+ if (!st.isFile())
269
+ return null;
270
+ const n = Math.min(st.size, LIMITE_CAUDA_TRANSCRICAO), buf = Buffer.alloc(n);
271
+ fs.readSync(fd, buf, 0, n, st.size - n);
272
+ cauda = buf.toString('utf8');
273
+ }
274
+ finally {
275
+ fs.closeSync(fd);
276
+ }
277
+ const erros = [];
278
+ for (const linha of cauda.split('\n')) {
279
+ let e;
280
+ try {
281
+ e = JSON.parse(linha);
282
+ }
283
+ catch {
284
+ continue;
285
+ }
286
+ if (!e || typeof e !== 'object' || (e.isApiErrorMessage !== true && e.error === undefined))
287
+ continue;
288
+ erros.push(textoDoErroDeApi(e));
289
+ }
290
+ if (!erros.length)
291
+ return null;
292
+ // I-33 (N3): o criterio roda sobre o erro que encerrou a sessao (o ultimo erro de API), nunca sobre os
293
+ // anteriores juntos: um 429 transitorio do meio nao esconde o limite que matou a sessao, e um
294
+ // esgotamento ja superado nao tira o perfil do rodizio por causa de um 429 transitorio no fim.
295
+ return (0, claude_bg_1.parseFalhaDeConta)(erros[erros.length - 1], agoraMs);
296
+ }
297
+ /**
298
+ * I-33 (D11): precedencia entre a prova da I-34 e a falha de conta. Com Stop correlacionado vale
299
+ * a prova (a); sem Stop, `runtime.unavailable` cede ao motivo mais especifico quando a
300
+ * transcricao traz a evidencia (b). Nenhum outro motivo e reclassificado.
301
+ */
302
+ function comFalhaDeConta(c, evidencia) {
303
+ if (!c.terminal || c.stop || c.dependeDoStop || c.classificacao !== 'gate_blocked' || c.motivo !== 'runtime.unavailable')
304
+ return { c, falha: null };
305
+ const falha = evidencia();
306
+ if (!falha)
307
+ return { c, falha: null };
308
+ return { c: { ...c, motivo: falha.motivo, fonte: `${c.fonte}; ${falha.motivo} na transcricao do perfil: ${falha.trecho}` }, falha };
309
+ }
310
+ /** Prova de vida pela identidade fixada na primeira observação do pid. */
311
+ function processoNativo(pid, cursor) {
312
+ if (pid === undefined)
313
+ return 'ausente';
314
+ if (cursor.pid !== pid || !cursor.identidade) {
315
+ cursor.pid = pid;
316
+ cursor.identidade = (0, codex_runner_1.identidadeProcesso)(pid);
317
+ }
318
+ if (cursor.identidade)
319
+ return (0, codex_runner_1.estadoProcesso)(cursor.identidade) === 'ausente' ? 'ausente' : 'vivo';
320
+ try {
321
+ process.kill(pid, 0);
322
+ return 'vivo';
323
+ }
324
+ catch (erro) {
325
+ return erro.code === 'ESRCH' ? 'ausente' : 'vivo';
326
+ }
327
+ }
328
+ /**
329
+ * sha, mtime e texto de um artefato da thread, lido sem seguir link simbólico. Link, diretório,
330
+ * arquivo vazio ou grande demais não são saída de fase: devolvem `invalido` em vez de lançar,
331
+ * para o observador e o `ork_observe` classificarem a falta de prova sem morrer.
332
+ */
333
+ function estadoDoArtefato(raiz, threadId, arquivo) {
334
+ const file = path.join((0, thread_1.dirThread)(raiz, threadId), arquivo);
335
+ const vazio = { sha256: null, mtimeMs: null, texto: null };
336
+ let fd;
337
+ try {
338
+ fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
339
+ }
340
+ catch (erro) {
341
+ return erro.code === 'ENOENT' ? vazio : { ...vazio, invalido: true };
342
+ }
343
+ try {
344
+ const stat = fs.fstatSync(fd);
345
+ if (!stat.isFile() || stat.size === 0 || stat.size > LIMITE_PLANO_BYTES)
346
+ return { ...vazio, invalido: true };
347
+ const bytes = Buffer.alloc(stat.size);
348
+ fs.readSync(fd, bytes, 0, stat.size, 0);
349
+ return { sha256: (0, node_crypto_1.createHash)('sha256').update(bytes).digest('hex'), mtimeMs: stat.mtimeMs, texto: bytes.toString('utf8') };
350
+ }
351
+ finally {
352
+ fs.closeSync(fd);
353
+ }
354
+ }
355
+ /**
356
+ * Saída do artefato da fase (GOAL, PLAN, CHECK): sha diferente da base registrada no despacho;
357
+ * sem base (fonte retroativa), arquivo com mtime não anterior ao despacho. D16 (nota P3): o
358
+ * intervalo tem fim, e arquivo com mtime no próximo despacho ou depois é da sessão seguinte.
359
+ * Outras fases: `null`.
360
+ */
361
+ function saidaDoArtefato(raiz, threadId, sessao, fonte, eventos) {
362
+ const arquivo = exports.ARTEFATO_DA_FASE[sessao.fase];
363
+ if (!arquivo)
364
+ return null;
365
+ const atual = estadoDoArtefato(raiz, threadId, arquivo);
366
+ const base = fonte?.artefato?.arquivo === arquivo ? fonte.artefato : undefined;
367
+ const noIntervalo = atual.mtimeMs !== null && atual.mtimeMs < fimDoIntervalo(eventos, sessao);
368
+ const produzido = atual.sha256 === null || !noIntervalo ? false : base ? atual.sha256 !== base.sha256
369
+ : atual.mtimeMs !== null && atual.mtimeMs >= Date.parse(sessao.despachadaEm);
370
+ return { arquivo, produzido, sha256: atual.sha256, sha256Base: base?.sha256 ?? null, texto: atual.texto };
371
+ }
372
+ /** Exatamente um veredito legível numa linha `Veredito: ...` (PASSOU, PRECISA DE MUDANCA ou BLOQUEADO). */
373
+ function vereditoDoParecer(texto) {
374
+ const achados = new Set();
375
+ for (const linha of texto.split('\n')) {
376
+ const m = /^[#>*_`\s-]*veredito[*_`\s]*:[*_`\s]*(passou|precisa de mudan[cç]a|bloqueado)(?![\p{L}\p{N}])/iu.exec(linha);
377
+ if (m)
378
+ achados.add(m[1].toUpperCase().replace('Ç', 'C'));
379
+ }
380
+ return achados.size === 1 ? [...achados][0] : null;
381
+ }
382
+ /** Fim do intervalo do despacho: o próximo `phase_dispatch` de outra sessão da thread, ou `Infinity`. */
383
+ function fimDoIntervalo(eventos, sessao) {
384
+ const inicio = Date.parse(sessao.despachadaEm);
385
+ const proximo = eventos.find(e => e.tipo === 'phase_dispatch' && e.sessionId !== sessao.sessionId && Date.parse(e.ts) > inicio);
386
+ return proximo ? Date.parse(proximo.ts) : Infinity;
387
+ }
388
+ /** Eventos do despacho: do `despachoEm` até o próximo `phase_dispatch` de outra sessão da thread. */
389
+ function eventosDoIntervalo(eventos, sessao) {
390
+ const inicio = Date.parse(sessao.despachadaEm);
391
+ if (!Number.isFinite(inicio))
392
+ return [];
393
+ const fim = fimDoIntervalo(eventos, sessao);
394
+ return eventos.filter(e => Date.parse(e.ts) >= inicio && Date.parse(e.ts) < fim);
395
+ }
396
+ /** D16: HEAD da worktree lido pelo `ork`; `null` quando o cwd não tem git legível. */
397
+ function headDaWorktree(cwd) {
398
+ const r = (0, util_1.exec)('git', ['rev-parse', '--verify', 'HEAD^{commit}'], cwd, 30000);
399
+ const sha = r.stdout.trim();
400
+ return r.ok && SHA_COMMIT.test(sha) ? sha : null;
401
+ }
402
+ /**
403
+ * D16: um SHA registrado só prova o GO se existir na worktree do despacho, estiver no HEAD atual
404
+ * (ancestral ou igual) e não estiver no HEAD que a worktree tinha no despacho. O
405
+ * `--is-ancestor` sai 0 (é ancestral ou igual), 1 (não é) ou outro código (erro): só 1 serve.
406
+ */
407
+ function commitConferido(cwd, sha, headNoDespacho) {
408
+ const git = (...args) => (0, util_1.exec)('git', args, cwd, 30000);
409
+ if (!git('cat-file', '-e', `${sha}^{commit}`).ok)
410
+ return false;
411
+ if (!git('merge-base', '--is-ancestor', sha, 'HEAD').ok)
412
+ return false;
413
+ return git('merge-base', '--is-ancestor', sha, headNoDespacho).code === 1;
414
+ }
415
+ /**
416
+ * D12: prova do lado do `ork` no intervalo do despacho, por fase. GOAL e PLAN: artefato gravado;
417
+ * GO: commit conferido no git da worktree (D16) e worktree limpa; CHECK: parecer gravado com
418
+ * exatamente um veredito legível (guarda também o último `verify_run`); MASTER:
419
+ * `score_proposto`; SHIP: `ship_done`. Leitura de disco e do git, sem gravar nada.
420
+ */
421
+ function provaDoOrk(raiz, threadId, sessao, eventos, fonte) {
422
+ const intervalo = eventosDoIntervalo(eventos, sessao);
423
+ const falta = (texto, extra = {}) => ({ ok: false, fonte: texto, motivo: sessao.fase === 'PLAN' ? 'artifact.missing' : 'human.pending', ...extra });
424
+ const saida = saidaDoArtefato(raiz, threadId, sessao, fonte, eventos);
425
+ if (saida) {
426
+ if (!saida.produzido)
427
+ return falta(`${saida.arquivo} não foi gravado no intervalo do despacho`);
428
+ if (sessao.fase !== 'CHECK')
429
+ return { ok: true, fonte: `${saida.arquivo} gravado no intervalo do despacho` };
430
+ const v = intervalo.filter(e => e.tipo === 'verify_run' && typeof e.commit === 'string').at(-1);
431
+ const verify = v ? { eventId: String(v.eventId), commit: String(v.commit), ts: v.ts } : null;
432
+ const veredito = saida.texto === null ? null : vereditoDoParecer(saida.texto);
433
+ if (!veredito)
434
+ return falta(`${saida.arquivo} gravado sem exatamente um veredito legível`, { veredito: null, verify });
435
+ return { ok: true, fonte: `${saida.arquivo} gravado com veredito ${veredito}`, veredito, verify };
436
+ }
437
+ if (sessao.fase === 'GO') {
438
+ // D16: SHA registrado não é prova até ser conferido no git; o sensor `commit` só vale da sessão.
439
+ const registrados = [...new Set(intervalo.filter(e => (e.tipo === 'mcp_git_committed' ||
440
+ (e.tipo === 'commit' && e.sessionId === sessao.sessionId)) &&
441
+ typeof e.commit === 'string' && SHA_COMMIT.test(e.commit)).map(e => String(e.commit)))].slice(-20);
442
+ if (!registrados.length)
443
+ return falta('nenhum commit registrado pelo núcleo no intervalo do despacho', { commits: [] });
444
+ const cwd = fonte?.cwd, headNoDespacho = fonte?.head;
445
+ if (!cwd || typeof headNoDespacho !== 'string')
446
+ return falta('HEAD da worktree no despacho não registrado; commit não conferível', { commits: [], recusados: registrados });
447
+ const commits = registrados.filter(sha => commitConferido(cwd, sha, headNoDespacho));
448
+ const recusados = registrados.filter(sha => !commits.includes(sha));
449
+ if (!commits.length)
450
+ return falta(`nenhum commit conferido no git da worktree: ${registrados.length} SHA(s) registrado(s) ` +
451
+ 'inexistente(s), fora do HEAD atual ou já no HEAD do despacho', { commits, recusados });
452
+ const status = (0, util_1.exec)('git', ['status', '--porcelain'], cwd, 30000);
453
+ if (!status.ok)
454
+ return falta('git status da worktree do despacho falhou', { commits, recusados, worktreeLimpa: false });
455
+ if (status.stdout.trim() !== '')
456
+ return falta('worktree com alterações não commitadas', { commits, recusados, worktreeLimpa: false });
457
+ return { ok: true, fonte: `${commits.length} commit(s) conferido(s) no git da worktree depois do HEAD do despacho e worktree limpa`,
458
+ commits, recusados, worktreeLimpa: true };
459
+ }
460
+ const exigido = sessao.fase === 'MASTER' ? 'score_proposto' : sessao.fase === 'SHIP' ? 'ship_done' : null;
461
+ if (!exigido)
462
+ return falta(`fase ${sessao.fase} sem prova do ork definida`);
463
+ const evento = intervalo.filter(e => e.tipo === exigido).at(-1);
464
+ if (!evento)
465
+ return falta(`nenhum ${exigido} registrado no intervalo do despacho`);
466
+ return { ok: true, fonte: `${exigido} registrado no intervalo do despacho`,
467
+ evento: { tipo: exigido, eventId: String(evento.eventId), ts: evento.ts } };
468
+ }
469
+ /** Último registro de fonte nativa do despacho, se houver. Leitura pura do ledger. */
470
+ function fonteRegistrada(eventos, sessao) {
471
+ const r = eventos.filter(e => e.tipo === 'session_sensor_registered' && e.sessionId === sessao.sessionId &&
472
+ e.despachoEm === sessao.despachadaEm).at(-1);
473
+ return r?.nativo === 'claude-agents' ? r : undefined;
474
+ }
475
+ /**
476
+ * D4: a fonte registrada no despacho. GOAL, PLAN e CHECK guardam o sha de base do artefato (D12);
477
+ * o GO guarda o HEAD da worktree (D16), lido no mesmo instante do `despachoEm`.
478
+ */
479
+ function fonteClaudeDoDespacho(raiz, threadId, fase, cwd) {
480
+ if (!path.isAbsolute(cwd))
481
+ throw new Error('despacho claude-bg sem cwd absoluto');
482
+ const arquivo = exports.ARTEFATO_DA_FASE[fase];
483
+ return { nativo: 'claude-agents', cwd,
484
+ ...(arquivo ? { artefato: { arquivo, sha256: estadoDoArtefato(raiz, threadId, arquivo).sha256 } } : {}),
485
+ ...(fase === 'GO' ? { head: headDaWorktree(cwd) } : {}) };
486
+ }
487
+ function ultimoRegistro(eventos, sessao) {
488
+ return eventos.filter(e => e.tipo === 'session_sensor_registered' && e.sessionId === sessao.sessionId &&
489
+ e.despachoEm === sessao.despachadaEm).at(-1);
490
+ }
491
+ /** Confere a variante nativa contra o despacho; usada também pela validação do watcher. */
492
+ function validarFonteClaude(eventos, sessionId, registro) {
493
+ if (registro.nativo !== 'claude-agents' || typeof registro.cwd !== 'string' || !path.isAbsolute(registro.cwd))
494
+ throw new Error('registro de sensor nativo claude-agents inválido');
495
+ const despacho = eventos.filter(e => e.tipo === 'phase_dispatch' && e.sessionId === sessionId).at(-1);
496
+ if (registro.artefato !== undefined && (!Object.values(exports.ARTEFATO_DA_FASE).includes(registro.artefato.arquivo) ||
497
+ (typeof despacho?.fase === 'string' && exports.ARTEFATO_DA_FASE[despacho.fase] !== registro.artefato.arquivo) ||
498
+ (registro.artefato.sha256 !== null && !SHA256.test(String(registro.artefato.sha256)))))
499
+ throw new Error('base de artefato do registro nativo inválida');
500
+ if (registro.head !== undefined && ((registro.head !== null && !SHA_COMMIT.test(String(registro.head))) ||
501
+ (typeof despacho?.fase === 'string' && despacho.fase !== 'GO')))
502
+ throw new Error('HEAD de despacho do registro nativo inválido');
503
+ if (despacho?.cwd !== undefined && despacho.cwd !== registro.cwd)
504
+ throw new Error('cwd registrado divergente do despacho');
505
+ }
506
+ /**
507
+ * D4 retroativa: sessão despachada antes da I-34 não tem registro. A fonte é derivável do
508
+ * `phase_dispatch` e não é segredo; sem base de artefato, a prova do PLAN passa a ser o mtime.
509
+ */
510
+ function garantirFonteClaude(raiz, thread, sessao) {
511
+ const dir = (0, thread_1.dirThread)(raiz, thread.id);
512
+ const existente = ultimoRegistro((0, ledger_1.lerLedger)(dir), sessao);
513
+ if (existente)
514
+ return existente;
515
+ // Ledger apagado não ganha registro retroativo: o watcher destacado não recria a thread.
516
+ if (!(0, ledger_1.threadPresente)(dir))
517
+ throw new Error(exports.THREAD_REMOVIDA);
518
+ return (0, session_events_1.comLockDeSessao)(dir, () => {
519
+ const eventos = (0, ledger_1.lerLedger)(dir), atual = ultimoRegistro(eventos, sessao);
520
+ if (atual)
521
+ return atual;
522
+ const despacho = eventos.filter(e => e.tipo === 'phase_dispatch' && e.sessionId === sessao.sessionId).at(-1);
523
+ const cwd = typeof despacho?.cwd === 'string' ? despacho.cwd : thread.worktree ?? raiz;
524
+ const fonte = { nativo: 'claude-agents', cwd, origem: 'sessions.watch' };
525
+ validarFonteClaude(eventos, sessao.sessionId, fonte);
526
+ if (!(0, ledger_1.registrarSeExiste)(dir, thread.id, 'session_sensor_registered', { fase: sessao.fase, sessionId: sessao.sessionId,
527
+ despachoEm: sessao.despachadaEm, ...fonte }))
528
+ throw new Error(exports.THREAD_REMOVIDA);
529
+ return fonte;
530
+ });
531
+ }
532
+ /**
533
+ * Evidência limitada: só campos do schema. O `name` vira hash, porque sessão despachada sem
534
+ * `--name` recebe o começo do prompt como nome (medido em 19/09/2026 com 1438 caracteres).
535
+ */
536
+ function evidenciaDoRegistro(r) {
537
+ if (!r)
538
+ return null;
539
+ const { sessionId, cwd, pid, startedAt } = r;
540
+ const [id, kind, state, status] = [r.id, r.kind, r.state, r.status].map(v => normalizarNativo(v) ?? undefined);
541
+ const base = { id, sessionId, cwd, kind, state, status, pid, startedAt,
542
+ nameSha256: r.name === undefined ? undefined : (0, node_crypto_1.createHash)('sha256').update(r.name).digest('hex') };
543
+ return Buffer.byteLength(JSON.stringify(base)) > LIMITE_EVIDENCIA_BYTES ? { sessionId, state, status, pid } : base;
544
+ }
545
+ /** Corpo da observação claude-bg, chamado pelo `session-watcher` já dentro do lock do watcher. */
546
+ function observarClaudeSobLock(ctx) {
547
+ const { raiz, thread, sessao, dir } = ctx;
548
+ const base = { sessionId: sessao.sessionId, thread: thread.id, concluido: false, bytesLidos: 0 };
549
+ const resultadoId = `claude-bg:${ctx.chave}`;
550
+ const pronto = (0, ledger_1.lerLedger)(dir).find(e => e.tipo === 'phase_result' && e.sensorResultId === resultadoId);
551
+ if (pronto)
552
+ return { ...base, concluido: true, classificacao: String(pronto.classificacao) };
553
+ const fonte = garantirFonteClaude(raiz, thread, sessao);
554
+ // Todas as gravações do observador anexam só a ledger existente (achado 1).
555
+ const gravar = (tipo, dados) => {
556
+ if (!(0, ledger_1.registrarSeExiste)(dir, thread.id, tipo, dados))
557
+ throw new Error(exports.THREAD_REMOVIDA);
558
+ };
559
+ const eventos = (0, ledger_1.lerLedger)(dir);
560
+ validarFonteClaude(eventos, sessao.sessionId, fonte);
561
+ const arquivoCursor = path.join(ctx.pasta, 'cursor-claude.json');
562
+ let cursor = { schema: 'ork.session-cursor-claude/v1', sessionId: sessao.sessionId, despachoEm: sessao.despachadaEm };
563
+ if (fs.existsSync(arquivoCursor)) {
564
+ if (fs.statSync(arquivoCursor).size > 65536)
565
+ throw new Error('metadado de sensor excessivo');
566
+ const lido = JSON.parse(fs.readFileSync(arquivoCursor, 'utf8'));
567
+ if (lido.schema !== cursor.schema || lido.sessionId !== sessao.sessionId || lido.despachoEm !== sessao.despachadaEm)
568
+ throw new Error('cursor de outra sessão/despacho');
569
+ cursor = lido;
570
+ }
571
+ // I-33 (D4): o registro nativo e da conta que despachou; perfil invalido lanca, nunca cai para o env do processo.
572
+ const perfil = (0, runtime_profiles_1.perfilDoRegistro)(eventos, sessao);
573
+ const consulta = (ctx.consulta ?? (() => (0, claude_bg_1.consultarAgentesNativos)(ctx.agoraMs, (0, claude_bg_1.ambienteDoPerfil)(perfil))))();
574
+ const registro = consulta.ok ? consulta.registros.find(s => s.sessionId === sessao.sessionId && s.cwd === fonte.cwd) ?? null : null;
575
+ const artefato = saidaDoArtefato(raiz, thread.id, sessao, fonte, eventos);
576
+ let prova;
577
+ const { c, falha } = comFalhaDeConta(classificarSessaoClaude({ sessao, eventos, consulta, registro, cursor, agoraMs: ctx.agoraMs,
578
+ limiteMs: ctx.limiteMs, pausaAoFim: (0, thread_1.pausaNaThread)(thread, sessao.fase),
579
+ prova: () => prova ??= provaDoOrk(raiz, thread.id, sessao, eventos, fonte), processo: processoNativo }), () => falhaDeContaDaTranscricao(perfil, fonte.cwd, sessao.sessionId, ctx.agoraMs));
580
+ (0, codex_runner_1.gravarAtomico)(arquivoCursor, cursor);
581
+ // GO-FIX 3 (achado 1): cursor gravado numa thread que sumiu durante a consulta é desfeito aqui;
582
+ // as pastas criadas pela observação, pelo `session-watcher`.
583
+ if (!(0, ledger_1.threadPresente)(dir)) {
584
+ fs.rmSync(arquivoCursor, { force: true });
585
+ throw new Error(exports.THREAD_REMOVIDA);
586
+ }
587
+ if (!c.terminal && c.inconclusivo)
588
+ return (0, session_events_1.comLockDeSessao)(dir, () => {
589
+ const diagnosticoId = `${resultadoId}:inconclusivo`;
590
+ if (!(0, ledger_1.lerLedger)(dir).some(e => e.sensorEventId === diagnosticoId))
591
+ gravar('session_watcher_error', {
592
+ fase: sessao.fase, sessionId: sessao.sessionId, despachoEm: sessao.despachadaEm, sensorEventId: diagnosticoId,
593
+ motivo: 'runtime.unavailable', erro: normalizarNativo(c.espera, LIMITE_ERRO_DIAGNOSTICO), origem: 'sessions.watch',
594
+ runtime: 'claude-bg', estadoNativo: normalizarNativo(registro?.state), evidencia: { fonte: 'claude agents --json --all', consultadoEm: consulta.consultadoEm,
595
+ registro: evidenciaDoRegistro(registro) }
596
+ });
597
+ return { ...base, espera: c.espera, encerrado: true };
598
+ });
599
+ if (!c.terminal)
600
+ return { ...base, espera: c.espera };
601
+ return (0, session_events_1.comLockDeSessao)(dir, () => {
602
+ const atuais = (0, ledger_1.lerLedger)(dir);
603
+ // A decisão que depende do Stop é reconferida sobre o ledger atual, sob o lock de ingestão.
604
+ // Heartbeat ou bloqueio ingerido depois da primeira leitura adia a decisão para a próxima volta.
605
+ if (c.dependeDoStop && (!stopCorrelacionado(atuais, sessao) || bloqueioPendente(atuais, sessao)))
606
+ return { ...base, espera: 'atividade ingerida durante a observação; reavaliar na próxima volta' };
607
+ const observadoEm = new Date(ctx.agoraMs).toISOString();
608
+ const dados = { fase: sessao.fase, sessionId: sessao.sessionId, despachoEm: sessao.despachadaEm, sensorResultId: resultadoId,
609
+ classificacao: c.classificacao, motivo: c.motivo, runtime: 'claude-bg', fonte: c.fonte,
610
+ estadoNativo: normalizarNativo(registro?.state), statusNativo: normalizarNativo(registro?.status), pidNativo: registro?.pid ?? null,
611
+ exitCode: null, exitCodeFonte: 'unavailable', signal: null, duracaoMs: null, duracaoFonte: 'unavailable',
612
+ ok: c.classificacao === 'fase_concluida', estado: c.classificacao === 'fase_concluida' ? 'concluida' : 'bloqueada',
613
+ stop: c.stop ? { ts: c.stop.ts, sensorEventId: c.stop.sensorEventId } : null,
614
+ evidencia: { fonte: 'claude agents --json --all', consultadoEm: consulta.consultadoEm, registro: evidenciaDoRegistro(registro) },
615
+ ...(artefato ? { artefato: { arquivo: artefato.arquivo, sha256Base: artefato.sha256Base, sha256Atual: artefato.sha256 } } : {}),
616
+ ...(prova ? { provaOrk: prova } : {}),
617
+ conclusaoNativa: c.conclusaoNativa, conclusaoNativaAusente: c.conclusaoNativa ? null : c.fonte,
618
+ ...(perfil ? { perfil } : {}),
619
+ ...(falha ? { falhaDeConta: { motivo: falha.motivo, resetEm: falha.resetEm, fonte: falha.fonte, trecho: falha.trecho } } : {}),
620
+ // D18: o diagnóstico vai ao pedido HITL (`ork gate request`) e, como `detalhe` do
621
+ // `gate_blocked`, ao `ork monitor` e ao `ork pulse`.
622
+ diagnostico: c.diagnostico ?? null, ...(c.diagnostico ? { detalhe: c.diagnostico } : {}),
623
+ observadoEm, gate: 'phase.dispatch', origem: 'sessions.watch' };
624
+ if (!atuais.some(e => e.tipo === c.classificacao && e.sensorResultId === resultadoId))
625
+ gravar(c.classificacao, dados);
626
+ // I-33 (D12): o perfil sai do rodizio na primeira gravacao do resultado, onde a evidencia esta.
627
+ if (falha && perfil && ctx.carregado && !atuais.some(e => e.tipo === 'phase_result' && e.sensorResultId === resultadoId)) {
628
+ try {
629
+ (0, ratelimit_1.marcarContaDaFalha)(ctx.carregado, perfil, falha, ctx.agoraMs);
630
+ }
631
+ catch { /* store ocupado ou invalido: o retry marca de novo pelo gate tipado */ }
632
+ }
633
+ if (!atuais.some(e => e.tipo === 'phase_result' && e.sensorResultId === resultadoId))
634
+ gravar('phase_result', dados);
635
+ return { ...base, concluido: true, classificacao: c.classificacao };
636
+ });
637
+ }