@orkastery/cli 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) 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/onboarding.md +21 -0
  7. package/adapters/claude-code/commands/ork.md +99 -27
  8. package/adapters/claude-code/hooks/hooks.json +6 -1
  9. package/adapters/claude-code/hooks/ork-sensor.js +66 -0
  10. package/adapters/codex/skills/ork/SKILL.md +57 -0
  11. package/adapters/hermes/README.md +163 -0
  12. package/adapters/hermes/bin/ork-abrir-thread.sh +2 -0
  13. package/adapters/hermes/bin/ork-brain.sh +4 -0
  14. package/adapters/hermes/bin/ork-hitl-answer.py +82 -0
  15. package/adapters/hermes/bin/ork-maestro.sh +6 -0
  16. package/adapters/hermes/bin/ork-master-enviar.py +84 -0
  17. package/adapters/hermes/bin/ork-objective-message.sh +17 -0
  18. package/adapters/hermes/bin/ork-objective-status.sh +12 -0
  19. package/adapters/hermes/bin/ork-pulse-enviar.py +47 -0
  20. package/adapters/hermes/hermes.plugin.json +26 -4
  21. package/adapters/hermes/hitl-ingress/__init__.py +356 -0
  22. package/adapters/hermes/hitl-ingress/plugin.yaml +4 -0
  23. package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +75 -22
  24. package/adapters/openclaw/README.md +135 -26
  25. package/adapters/openclaw/bin/ork-brain.sh +4 -0
  26. package/adapters/openclaw/construir.sh +19 -0
  27. package/adapters/openclaw/dist/hitl-ingress.js +199 -0
  28. package/adapters/openclaw/dist/index.js +437 -0
  29. package/adapters/openclaw/openclaw.plugin.json +39 -134
  30. package/adapters/openclaw/package.json +26 -0
  31. package/adapters/openclaw/src/hitl-ingress.ts +180 -0
  32. package/adapters/openclaw/src/index.ts +458 -0
  33. package/adapters/openclaw/src/tipos-openclaw.d.ts +58 -0
  34. package/adapters/openclaw/tsconfig.json +15 -0
  35. package/assets/docs/markdownlint-cli2.jsonc +22 -0
  36. package/assets/docs/padroes/documentacao-de-produto.md +133 -0
  37. package/assets/docs/padroes/roadmap-de-produto.md +98 -0
  38. package/assets/docs/produto/README.md +14 -0
  39. package/assets/docs/produto/_modelo-feature.md +59 -0
  40. package/assets/docs/roadmap/README.md +14 -0
  41. package/assets/docs/roadmap/_modelo-item.md +73 -0
  42. package/assets/orkmind-native-schema.json +32 -0
  43. package/assets/orkmind_bridge.py +357 -0
  44. package/assets/orkmind_fixture.py +67 -0
  45. package/assets/orkmind_prospective.py +138 -0
  46. package/assets/reference-tariffs-i07.json +23 -0
  47. package/dist/adapters/claude-bg.js +488 -39
  48. package/dist/adapters/codex-controller-sensor.js +449 -0
  49. package/dist/adapters/codex-controller-worker.js +361 -0
  50. package/dist/adapters/codex-controller.js +252 -0
  51. package/dist/adapters/codex-events.js +205 -0
  52. package/dist/adapters/codex-question.js +26 -0
  53. package/dist/adapters/codex-runner.js +133 -0
  54. package/dist/adapters/codex.js +394 -0
  55. package/dist/agents-md.js +84 -0
  56. package/dist/auditoria.js +11 -8
  57. package/dist/auditrun.js +5 -4
  58. package/dist/board.js +173 -22
  59. package/dist/branch-de-estado.js +136 -0
  60. package/dist/canarios-hitl.js +177 -0
  61. package/dist/canarios-i43.js +543 -0
  62. package/dist/canarios-pulse.js +147 -0
  63. package/dist/canarios-sensores.js +129 -0
  64. package/dist/canarios.js +111 -2
  65. package/dist/catalogo.js +9 -0
  66. package/dist/ci.js +216 -0
  67. package/dist/ciclos.js +2 -1
  68. package/dist/claim-lint.js +64 -0
  69. package/dist/claims.js +60 -0
  70. package/dist/company-brain-capture.js +195 -0
  71. package/dist/company-brain-cli.js +123 -0
  72. package/dist/company-brain-client.js +61 -0
  73. package/dist/company-brain-contract.js +166 -0
  74. package/dist/company-brain-journal.js +192 -0
  75. package/dist/company-brain-mcp.js +33 -0
  76. package/dist/company-brain-migration.js +60 -0
  77. package/dist/company-brain-source.js +177 -0
  78. package/dist/company-brain-worker.js +18 -0
  79. package/dist/conducao-texto.js +61 -0
  80. package/dist/conducao.js +876 -0
  81. package/dist/contrato-publico.js +39 -0
  82. package/dist/creation-operation-store.js +251 -0
  83. package/dist/creation-operation.js +148 -0
  84. package/dist/decisao-autonoma.js +183 -0
  85. package/dist/delegation.js +79 -0
  86. package/dist/demo.js +120 -0
  87. package/dist/docs.js +828 -0
  88. package/dist/doctor.js +183 -24
  89. package/dist/entrega-pr.js +117 -0
  90. package/dist/escopo-escrita.js +71 -0
  91. package/dist/estado-thread.js +222 -0
  92. package/dist/evalrunner.js +12 -0
  93. package/dist/fabrica-estado.js +329 -0
  94. package/dist/fabrica-publicar.js +76 -0
  95. package/dist/fix.js +21 -0
  96. package/dist/gates.js +55 -9
  97. package/dist/handoff.js +39 -23
  98. package/dist/hitl-canais.js +333 -0
  99. package/dist/hitl-classificacao.js +131 -0
  100. package/dist/hitl-contract.js +465 -0
  101. package/dist/hitl-estado.js +136 -0
  102. package/dist/hitl-gates.js +554 -0
  103. package/dist/hitl-ingress-receipt.js +382 -0
  104. package/dist/hitl-local-atestado.js +57 -0
  105. package/dist/hitl-local-receipt.js +328 -0
  106. package/dist/hitl-local.js +143 -0
  107. package/dist/hitl-lock.js +139 -0
  108. package/dist/hitl-lote.js +223 -0
  109. package/dist/hitl-native-offer.js +97 -0
  110. package/dist/hitl-native.js +65 -0
  111. package/dist/hitl-presentation.js +209 -0
  112. package/dist/hitl-public-receipt.js +176 -0
  113. package/dist/hitl-resumo.js +209 -0
  114. package/dist/hitl-sessions.js +475 -0
  115. package/dist/hitl.js +711 -0
  116. package/dist/horario.js +269 -0
  117. package/dist/hosts.js +198 -22
  118. package/dist/index.js +1736 -93
  119. package/dist/indice.js +99 -0
  120. package/dist/init.js +28 -3
  121. package/dist/integracoes-locais.js +17 -0
  122. package/dist/leases.js +65 -21
  123. package/dist/ledger-stats.js +272 -0
  124. package/dist/ledger.js +107 -2
  125. package/dist/licoes.js +215 -0
  126. package/dist/liveness.js +218 -0
  127. package/dist/maestro-actions.js +51 -0
  128. package/dist/maestro-authority.js +152 -0
  129. package/dist/maestro-cli.js +97 -0
  130. package/dist/maestro-contract.js +55 -0
  131. package/dist/maestro-discovery.js +132 -0
  132. package/dist/maestro-runtime.js +268 -0
  133. package/dist/maestro-snapshot.js +86 -0
  134. package/dist/maestro-sources.js +224 -0
  135. package/dist/manifest.js +165 -6
  136. package/dist/maquina.js +102 -0
  137. package/dist/master-audit.js +101 -0
  138. package/dist/master-batch.js +43 -0
  139. package/dist/master-digest.js +149 -0
  140. package/dist/master-migracao.js +155 -0
  141. package/dist/master.js +361 -77
  142. package/dist/mcp-artifacts.js +234 -0
  143. package/dist/mcp-git.js +433 -0
  144. package/dist/mcp-install.js +310 -0
  145. package/dist/mcp-maestro.js +25 -0
  146. package/dist/mcp-server.js +469 -0
  147. package/dist/mcp-ship.js +436 -0
  148. package/dist/mcp-verify.js +173 -0
  149. package/dist/memoria-humana.js +207 -0
  150. package/dist/memoria.js +314 -78
  151. package/dist/memory-migration.js +279 -0
  152. package/dist/memory-prospective.js +148 -0
  153. package/dist/modos-migracao.js +191 -0
  154. package/dist/modos.js +151 -15
  155. package/dist/monitor-lock.js +110 -0
  156. package/dist/objective.js +449 -0
  157. package/dist/ocupacao.js +267 -0
  158. package/dist/onboarding.js +303 -0
  159. package/dist/orkmind.js +389 -90
  160. package/dist/orquestracao.js +96 -69
  161. package/dist/phase.js +596 -150
  162. package/dist/playbook-capabilities.js +150 -0
  163. package/dist/playbook-contracts.js +188 -0
  164. package/dist/portfolio-context.js +61 -0
  165. package/dist/portfolio.js +180 -0
  166. package/dist/preflight.js +207 -0
  167. package/dist/process-audit.js +112 -0
  168. package/dist/project-state.js +93 -0
  169. package/dist/prompts.js +25 -5
  170. package/dist/prova-minima.js +112 -0
  171. package/dist/pulse-cadencia.js +163 -0
  172. package/dist/pulse-consentimento.js +260 -0
  173. package/dist/pulse-delivery.js +323 -0
  174. package/dist/pulse-resposta.js +542 -0
  175. package/dist/pulse.js +262 -0
  176. package/dist/ratelimit.js +35 -2
  177. package/dist/recall.js +80 -3
  178. package/dist/redacao-saida.js +45 -0
  179. package/dist/redacao-url.js +66 -0
  180. package/dist/retry.js +637 -115
  181. package/dist/roadmap-reservas.js +243 -0
  182. package/dist/runtime-ambiente.js +38 -0
  183. package/dist/runtime-context.js +215 -0
  184. package/dist/runtime-profiles.js +869 -0
  185. package/dist/runtimes.js +103 -0
  186. package/dist/sandbox.js +9 -0
  187. package/dist/session-events.js +203 -0
  188. package/dist/session-watcher-claude.js +637 -0
  189. package/dist/session-watcher.js +683 -0
  190. package/dist/sessoes-adopt.js +154 -0
  191. package/dist/sessoes-inventario.js +159 -0
  192. package/dist/sessoes.js +16 -35
  193. package/dist/setup.js +691 -0
  194. package/dist/ship.js +150 -15
  195. package/dist/slug.js +1 -1
  196. package/dist/thread-close.js +115 -0
  197. package/dist/thread.js +142 -28
  198. package/dist/tokens.js +1 -1
  199. package/dist/util.js +4 -2
  200. package/dist/verify-sandbox.js +265 -0
  201. package/dist/verify.js +270 -25
  202. package/dist/versao.js +60 -0
  203. package/dist/worktree.js +31 -2
  204. package/dist/write-activation.js +291 -0
  205. package/dist/yaml.js +2 -0
  206. package/eval/casos/onboarding.json +102 -0
  207. package/eval/casos/orkastery-bootstrap.json +35 -1
  208. package/eval/casos/scope-check-capability-map.json +45 -7
  209. package/eval/casos/ship-release.json +2 -2
  210. package/eval/fixtures/b0-slug-e-modos/caso.json +35 -22
  211. package/eval/fixtures/b2-master-log/caso.json +1 -1
  212. package/eval/fixtures/fx-adapter-editado-detectado/caso.json +19 -0
  213. package/eval/fixtures/fx-auto-quiet/caso.json +11 -0
  214. package/eval/fixtures/fx-blanket-approve/caso.json +10 -0
  215. package/eval/fixtures/fx-check-runtime-cruzado/caso.json +20 -0
  216. package/eval/fixtures/fx-codex-dry/caso.json +18 -0
  217. package/eval/fixtures/fx-donewhen-executavel/caso.json +15 -0
  218. package/eval/fixtures/fx-estado-dividido/caso.json +15 -0
  219. package/eval/fixtures/fx-fase-orfa/caso.json +116 -0
  220. package/eval/fixtures/fx-happy/caso.json +6 -4
  221. package/eval/fixtures/fx-hitl-latency/caso.json +20 -0
  222. package/eval/fixtures/fx-indice-reversao/caso.json +25 -0
  223. package/eval/fixtures/fx-listagem-abertas/caso.json +16 -0
  224. package/eval/fixtures/fx-maestro-bootstrap/caso.json +21 -0
  225. package/eval/fixtures/fx-modo-aposentado-escritor/caso.json +21 -0
  226. package/eval/fixtures/fx-modo-aposentado-leitor/caso.json +16 -0
  227. package/eval/fixtures/fx-objective-oscillation/caso.json +14 -0
  228. package/eval/fixtures/fx-omnicanal/caso.json +17 -0
  229. package/eval/fixtures/fx-sensores-runtime/caso.json +17 -0
  230. package/monitor/company-brain.cjs +17 -0
  231. package/monitor/pulse-scope.cjs +45 -0
  232. package/monitor/pulse.cron +18 -0
  233. package/monitor/varredura-pulse.sh +27 -0
  234. package/package.json +25 -4
  235. package/schemas/claims.schema.json +25 -0
  236. package/schemas/company-brain.schema.json +1158 -0
  237. package/schemas/creation-operation.schema.json +365 -0
  238. package/schemas/maestro-snapshot.schema.json +2716 -0
  239. package/skills/core/onboarding/SKILL.md +50 -0
  240. package/skills/core/orkastery-bootstrap/SKILL.md +95 -65
  241. package/skills/core/thread-state/SKILL.md +3 -1
  242. package/skills/governance/scope-check-capability-map/SKILL.md +17 -7
@@ -0,0 +1,683 @@
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.BLOCO_WATCH_BYTES = exports.INTERVALO_WATCH_MS = exports.LIMITE_MORTE_MS = void 0;
37
+ exports.observarSessao = observarSessao;
38
+ exports.validarFonteWatcher = validarFonteWatcher;
39
+ exports.iniciarWatcher = iniciarWatcher;
40
+ /** D6: observador restrito a uma sessão, cursor canônico e resultados idempotentes. */
41
+ const fs = __importStar(require("node:fs"));
42
+ const path = __importStar(require("node:path"));
43
+ const node_crypto_1 = require("node:crypto");
44
+ const node_child_process_1 = require("node:child_process");
45
+ const codex_1 = require("./adapters/codex");
46
+ const runtime_profiles_1 = require("./runtime-profiles");
47
+ const codex_events_1 = require("./adapters/codex-events");
48
+ const codex_runner_1 = require("./adapters/codex-runner");
49
+ const thread_1 = require("./thread");
50
+ const session_events_1 = require("./session-events");
51
+ const ledger_1 = require("./ledger");
52
+ const manifest_1 = require("./manifest");
53
+ const codex_controller_sensor_1 = require("./adapters/codex-controller-sensor");
54
+ const retry_1 = require("./retry");
55
+ const ratelimit_1 = require("./ratelimit");
56
+ const session_watcher_claude_1 = require("./session-watcher-claude");
57
+ /** Falhas operacionais e bloqueios sem retry automático não alegam conclusão do produto. */
58
+ function bloqueioPreservado(terminal) {
59
+ return terminal.classificacao === 'gate_blocked' && terminal.motivo !== null &&
60
+ ((0, retry_1.politicaDoMotivo)(terminal.motivo)?.automatica === false ||
61
+ ['runtime.rate-limited', 'runtime.unavailable', 'runtime.silencio', 'tree.blocked', 'lease.busy'].includes(terminal.motivo) ||
62
+ // I-33 (D12): falha da conta com erro estruturado do runtime tambem nao alega conclusao.
63
+ (!!terminal.falhaDeConta && terminal.motivo === terminal.falhaDeConta.motivo));
64
+ }
65
+ /**
66
+ * I-33 (D12): falha da CONTA no caminho terminal do turno codex. Duas fontes do proprio runtime,
67
+ * nunca texto do agente: o `task_complete.error` do rollout (valido e com o turno fechado) e o erro
68
+ * nativo do `turn/completed` `failed` gravado pelo controller. O incidente de 18/09 tinha as duas.
69
+ */
70
+ function falhaDeContaDoTerminal(terminal, rolloutConfiavel, snapshot, agoraMs) {
71
+ if (rolloutConfiavel && terminal?.falhaDeConta)
72
+ return { falha: terminal.falhaDeConta, fonte: `${terminal.fonte}.error` };
73
+ const nativo = snapshot?.terminalNativo;
74
+ if (nativo?.status !== 'failed' || !nativo.erro)
75
+ return null;
76
+ const falha = (0, codex_events_1.falhaDeContaDoErroCodex)(nativo.erro, agoraMs);
77
+ return falha ? { falha, fonte: 'controller.turn/completed.error' } : null;
78
+ }
79
+ /**
80
+ * Portao I04 do transporte do controller. Enumera o que falta para o sucesso ser prova,
81
+ * nunca o contrario: devolve `null` so quando terminal nativo `completed`, estado sem erro
82
+ * nem limitacao, correlacao de thread, cwd e turno e `close` real zero estao todos
83
+ * demonstrados. Prefixo de texto nao e proveniencia: id de origem gerada nao vincula turno.
84
+ */
85
+ function semProvaNativa(snapshot, terminal, fonteVencedora, fechamento, sessionId, cwd) {
86
+ const nativo = snapshot.terminalNativo;
87
+ if (!nativo)
88
+ return 'terminal nativo ausente na fonte do controller';
89
+ if (nativo.status !== 'completed') {
90
+ const conhecido = nativo.status === 'failed' || nativo.status === 'interrupted';
91
+ return `terminal nativo com status ${conhecido ? nativo.status : 'desconhecido'}`;
92
+ }
93
+ if (nativo.threadId !== sessionId)
94
+ return 'terminal nativo de outra thread';
95
+ if (snapshot.erro || snapshot.limitacao)
96
+ return 'estado do controller com erro ou limitacao tipada';
97
+ if (snapshot.fonte.cwd !== cwd)
98
+ return 'cwd da fonte divergente do despacho';
99
+ if (snapshot.turno !== nativo.turnId)
100
+ return 'turno do estado divergente do terminal nativo';
101
+ if (fonteVencedora !== 'rollout')
102
+ return 'terminal observado fora do rollout fixado';
103
+ if (terminal.turnIdOrigem !== 'explicito')
104
+ return 'id de turno sem proveniencia explicita e sem vinculacao alternativa provada';
105
+ if (terminal.turnId !== nativo.turnId)
106
+ return 'turno observado divergente do terminal nativo';
107
+ if (!fechamento || fechamento.exitCode !== 0 || fechamento.signal || fechamento.erro)
108
+ return 'close real zero do controller ausente';
109
+ return null;
110
+ }
111
+ exports.LIMITE_MORTE_MS = 600000;
112
+ exports.INTERVALO_WATCH_MS = 1000;
113
+ exports.BLOCO_WATCH_BYTES = codex_events_1.LIMITE_LINHA_CODEX + 65536;
114
+ function lerJson(file) {
115
+ if (!fs.existsSync(file))
116
+ return null;
117
+ if (fs.statSync(file).size > 65536)
118
+ throw new Error('metadado de sensor excessivo');
119
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
120
+ }
121
+ /** Recovery serializado: quem perde o mutex não remove lock recém-adquirido. */
122
+ function comLockWatcher(dir, action) {
123
+ const lock = path.join(dir, 'watch.lock'), recover = path.join(dir, 'recover.lock');
124
+ const dono = { identidade: (0, codex_runner_1.identidadeProcesso)(process.pid), token: (0, node_crypto_1.randomUUID)() };
125
+ const owner = path.join(lock, 'owner.json');
126
+ const criar = () => { fs.mkdirSync(lock); (0, codex_runner_1.gravarAtomico)(owner, dono); };
127
+ try {
128
+ criar();
129
+ }
130
+ catch (e) {
131
+ if (e.code !== 'EEXIST')
132
+ throw e;
133
+ try {
134
+ fs.mkdirSync(recover);
135
+ }
136
+ catch {
137
+ return null;
138
+ }
139
+ try {
140
+ const atual = lerJson(owner);
141
+ if (atual ? (0, codex_runner_1.estadoProcesso)(atual.identidade) !== 'ausente' : Date.now() - fs.statSync(lock).mtimeMs < 60000)
142
+ return null;
143
+ if (atual)
144
+ fs.unlinkSync(owner);
145
+ fs.rmdirSync(lock);
146
+ try {
147
+ criar();
148
+ }
149
+ catch (erro) {
150
+ if (erro.code === 'EEXIST')
151
+ return null;
152
+ throw erro;
153
+ }
154
+ }
155
+ finally {
156
+ fs.rmdirSync(recover);
157
+ }
158
+ }
159
+ try {
160
+ return action();
161
+ }
162
+ finally {
163
+ if (lerJson(owner)?.token === dono.token) {
164
+ fs.unlinkSync(owner);
165
+ fs.rmdirSync(lock);
166
+ }
167
+ }
168
+ }
169
+ function lerFonte(file, anterior, agoraMs, sessionId, fixada) {
170
+ const fd = fs.openSync(file, fixada
171
+ ? fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK : 'r');
172
+ try {
173
+ const stat = fs.fstatSync(fd);
174
+ if (fixada) {
175
+ // A validação anterior do sensor não autentica um pathname reaberto depois.
176
+ // Os bytes precisam vir do próprio descritor vinculado à identidade fixada.
177
+ const pai = fs.statSync(path.dirname(file));
178
+ if (file !== fixada.rollout || !stat.isFile() || stat.ino !== fixada.rolloutIno ||
179
+ stat.dev !== fixada.rolloutDev || stat.uid !== fixada.uid || (stat.mode & 0o022) !== 0 ||
180
+ fs.realpathSync(file) !== file || !pai.isDirectory() || pai.uid !== fixada.uid ||
181
+ (pai.mode & 0o022) !== 0) {
182
+ throw new Error('runtime.unavailable: descritor consumidor de rollout divergente da fonte fixada');
183
+ }
184
+ }
185
+ const mesmo = anterior?.file === file && anterior.ino === stat.ino && stat.size >= anterior.offset;
186
+ const parser = new codex_events_1.ParserCodex(mesmo ? anterior.parser : undefined);
187
+ // Corrupção já observada continua relevante depois de truncamento/rotação.
188
+ parser.estado.invalido ||= anterior?.parser.invalido ?? false;
189
+ let offset = mesmo ? anterior.offset : 0;
190
+ const buffer = Buffer.alloc(Math.min(exports.BLOCO_WATCH_BYTES, stat.size - offset));
191
+ const n = fs.readSync(fd, buffer, 0, buffer.length, offset);
192
+ const fim = buffer.subarray(0, n).lastIndexOf(10);
193
+ let terminal = mesmo ? anterior.terminal : null;
194
+ if (fim >= 0) {
195
+ for (const linha of buffer.subarray(0, fim + 1).toString('utf8').split('\n')) {
196
+ let e;
197
+ try {
198
+ e = JSON.parse(linha);
199
+ }
200
+ catch {
201
+ continue;
202
+ }
203
+ if ((e?.type === 'thread.started' && e.thread_id !== sessionId) ||
204
+ (e?.type === 'session_meta' && e.payload?.id !== sessionId))
205
+ throw new Error('log/rollout pertence a outra sessão');
206
+ }
207
+ for (const t of parser.push(buffer.subarray(0, fim + 1), agoraMs))
208
+ terminal = t;
209
+ offset += fim + 1;
210
+ }
211
+ else if (n >= codex_events_1.LIMITE_LINHA_CODEX) {
212
+ // Uma linha sem limite não retém o cursor para sempre nem permite alegar sucesso.
213
+ parser.estado.invalido = true;
214
+ offset += n;
215
+ }
216
+ if (parser.estado.turnoAberto || (terminal?.numeroTurno ?? 0) < parser.estado.turnos)
217
+ terminal = null;
218
+ const parcial = n < exports.BLOCO_WATCH_BYTES && stat.size > offset;
219
+ return { cursor: { file, ino: stat.ino, tamanho: stat.size, offset, parser: parser.estado, terminal, parcial },
220
+ cresceu: parser.eventosValidos > 0,
221
+ quando: Math.min(agoraMs, stat.mtimeMs), bytes: n, temMais: stat.size > offset };
222
+ }
223
+ finally {
224
+ fs.closeSync(fd);
225
+ }
226
+ }
227
+ function conferirRegistro(dir, registro) {
228
+ // A variante nativa não declara arquivo: a fonte é o `claude agents` e o cwd do despacho.
229
+ if (registro.nativo !== undefined) {
230
+ if (registro.logPath || registro.processoPath || registro.reciboPath || registro.controlador)
231
+ throw new Error('registro de sensor mistura fonte nativa e arquivo');
232
+ return;
233
+ }
234
+ const esperado = path.join(dir, 'sessoes') + path.sep;
235
+ for (const file of [registro.logPath, registro.processoPath, registro.reciboPath, registro.controlador].filter(Boolean)) {
236
+ if (!path.resolve(file).startsWith(esperado) ||
237
+ (fs.existsSync(file) && !fs.realpathSync(file).startsWith(esperado)))
238
+ throw new Error('fonte de sensor fora do diretório canônico de sessões');
239
+ }
240
+ // A variante do controller dispensa logPath; a histórica continua exigindo o log e seus sufixos.
241
+ if (registro.controlador) {
242
+ if (registro.logPath || registro.processoPath || registro.reciboPath)
243
+ throw new Error('registro de sensor mistura controller e log supervisionado');
244
+ return;
245
+ }
246
+ if (!registro.logPath)
247
+ throw new Error('registro de sensor sem fonte declarada');
248
+ if ((registro.processoPath && registro.processoPath !== registro.logPath + '.process.json') ||
249
+ (registro.reciboPath && registro.reciboPath !== registro.logPath + '.exit.json'))
250
+ throw new Error('recibo não corresponde ao log registrado');
251
+ }
252
+ /**
253
+ * GO-FIX 3 (achado 1): apaga os cursores da pasta de sensores e as pastas que a observação
254
+ * criou, da mais funda para a raiz. Pasta que já sumiu, ou que ganhou entrada alheia, fica
255
+ * com o dono da remoção.
256
+ */
257
+ function desfazerNaThreadRemovida(pasta, criadas) {
258
+ for (const cursor of ['cursor.json', 'cursor-claude.json']) {
259
+ try {
260
+ fs.rmSync(path.join(pasta, cursor), { force: true });
261
+ }
262
+ catch { /* pasta já removida */ }
263
+ }
264
+ for (const nivel of [...criadas].reverse()) {
265
+ try {
266
+ fs.rmdirSync(nivel);
267
+ }
268
+ catch { /* já removida ou não vazia */ }
269
+ }
270
+ }
271
+ function observarSessao(carregado, sessionId, opcoes = {}) {
272
+ const { thread, sessao: registrada } = (0, session_events_1.resolverSessao)(carregado.raiz, sessionId);
273
+ const sessao = (0, session_events_1.exigirSessaoDespachada)(registrada, sessionId);
274
+ if (sessao.runtime !== 'codex' && sessao.runtime !== 'claude-bg')
275
+ throw new Error('watch requer sessão Codex ou claude-bg registrada');
276
+ const now = opcoes.agoraMs ?? Date.now();
277
+ if (!Number.isFinite(now))
278
+ throw new Error('relógio inválido');
279
+ const dir = (0, thread_1.dirThread)(carregado.raiz, thread.id);
280
+ const chave = (0, node_crypto_1.createHash)('sha256').update(sessionId + '|' + sessao.despachadaEm).digest('hex');
281
+ const base = { sessionId, thread: thread.id, concluido: false, bytesLidos: 0 };
282
+ const pasta = path.join(dir, 'sensores', chave);
283
+ // Processo destacado não recria a thread de um projeto apagado: a pasta de sensores nasce
284
+ // sem `recursive`, só dentro de uma thread presente, e a ausência encerra a observação.
285
+ const removida = { ...base, encerrado: true, espera: session_watcher_claude_1.THREAD_REMOVIDA };
286
+ // GO-FIX 3 (achado 1): a thread pode sumir entre a checagem e a criação. O que esta observação
287
+ // criou ou gravou numa thread que sumiu é desfeito, para um `rm -rf` concorrente não esbarrar
288
+ // em `sensores/` recriado nem deixar cursor órfão.
289
+ const criadas = [];
290
+ const desfazer = () => { desfazerNaThreadRemovida(pasta, criadas); return removida; };
291
+ try {
292
+ if (!(0, ledger_1.threadPresente)(dir))
293
+ return removida;
294
+ for (const nivel of [path.dirname(pasta), pasta]) {
295
+ try {
296
+ fs.mkdirSync(nivel);
297
+ criadas.push(nivel);
298
+ }
299
+ catch (e) {
300
+ if (e.code !== 'EEXIST')
301
+ throw e;
302
+ }
303
+ }
304
+ if (!(0, ledger_1.threadPresente)(dir))
305
+ return desfazer();
306
+ if (sessao.runtime === 'claude-bg') {
307
+ const r = comLockWatcher(pasta, () => (0, session_watcher_claude_1.observarClaudeSobLock)({ raiz: carregado.raiz,
308
+ thread, sessao, dir, pasta, chave, agoraMs: now, limiteMs: exports.LIMITE_MORTE_MS, consulta: opcoes.consultaClaude, carregado })) ?? { ...base, ocupado: true };
309
+ return (0, ledger_1.threadPresente)(dir) ? r : desfazer();
310
+ }
311
+ return comLockWatcher(pasta, () => {
312
+ const eventos = (0, ledger_1.lerLedger)(dir), resultadoId = `codex:${chave}`;
313
+ const pronto = eventos.find(e => e.tipo === 'phase_result' && e.sensorResultId === resultadoId);
314
+ if (pronto)
315
+ return { ...base, concluido: true, classificacao: String(pronto.classificacao) };
316
+ const registro = eventos.filter(e => e.tipo === 'session_sensor_registered' &&
317
+ e.sessionId === sessionId && e.despachoEm === sessao.despachadaEm).at(-1);
318
+ if (registro)
319
+ conferirRegistro(dir, registro);
320
+ // I-33 (D4): o rollout mora no CODEX_HOME do perfil que despachou, nunca no do processo.
321
+ const perfil = (0, runtime_profiles_1.perfilDoRegistro)(eventos, sessao);
322
+ // Fonte do controller validada ANTES de ler rollout ou escrever cursor.
323
+ let fonteControlador = null, snapshot = null;
324
+ let cwdDespacho;
325
+ if (registro?.controlador) {
326
+ const despacho = eventos.filter(e => e.tipo === 'phase_dispatch' && e.sessionId === sessionId).at(-1);
327
+ if (despacho?.controlador !== undefined && despacho.controlador !== registro.controlador)
328
+ throw new Error('controlador registrado divergente do despacho');
329
+ const cwd = (despacho?.cwd ?? registro.cwd);
330
+ if (!cwd)
331
+ throw new Error('despacho do controller sem cwd registrado');
332
+ cwdDespacho = cwd;
333
+ fonteControlador = (0, codex_controller_sensor_1.registrarFonteController)(registro.controlador, {
334
+ dirSessoes: path.join(dir, 'sessoes'), sessionId, cwd, despachoEm: sessao.despachadaEm, agoraMs: now,
335
+ fixacao: path.join(dir, 'sessoes', `watcher-source-${chave}.json`),
336
+ vinculo: { thread: thread.id, fase: sessao.fase, promptSha256: sessao.promptSha256 }
337
+ });
338
+ snapshot = (0, codex_controller_sensor_1.lerSnapshotController)(fonteControlador, { agoraMs: now });
339
+ }
340
+ const file = path.join(pasta, 'cursor.json');
341
+ const cursor = lerJson(file) ?? { schema: 'ork.session-cursor/v1', sessionId,
342
+ despachoEm: sessao.despachadaEm, crescimentoEm: Date.parse(sessao.despachadaEm), heartbeatEm: 0, fontes: {} };
343
+ if (cursor.schema !== 'ork.session-cursor/v1' || cursor.sessionId !== sessionId || cursor.despachoEm !== sessao.despachadaEm)
344
+ throw new Error('cursor de outra sessão/despacho');
345
+ // Cursor anterior media bytes, nao progresso: migracao conservadora desde o despacho.
346
+ cursor.progressoEm ??= Date.parse(sessao.despachadaEm);
347
+ // Transporte do controller não admite descoberta nem override de rollout.
348
+ const fontes = { stream: registro?.logPath, rollout: snapshot ? snapshot.rollout
349
+ : opcoes.rollout === undefined ? (0, codex_1.acharRollout)(sessionId, 3, (0, codex_1.casaDoCodex)(perfil)) : opcoes.rollout };
350
+ let bytesLidos = 0, temMais = false, cresceu = false;
351
+ for (const [nome, fonte] of Object.entries(fontes)) {
352
+ if (!fonte || !fs.existsSync(fonte))
353
+ continue;
354
+ let leitura;
355
+ try {
356
+ leitura = lerFonte(fonte, cursor.fontes[nome], now, sessionId, nome === 'rollout' ? fonteControlador ?? undefined : undefined);
357
+ }
358
+ catch (e) {
359
+ if (nome !== 'rollout' || e.message !== 'log/rollout pertence a outra sessão')
360
+ throw e;
361
+ delete cursor.fontes[nome];
362
+ if (!eventos.some(e => e.tipo === 'session_watcher_source_rejected' && e.sessionId === sessionId)) {
363
+ (0, ledger_1.registrar)(dir, thread.id, 'session_watcher_source_rejected', { sessionId, despachoEm: sessao.despachadaEm,
364
+ fase: sessao.fase, fonte: nome, motivo: 'identidade de sessão divergente' });
365
+ }
366
+ continue;
367
+ }
368
+ cursor.fontes[nome] = leitura.cursor;
369
+ bytesLidos += leitura.bytes;
370
+ temMais ||= leitura.temMais;
371
+ if (leitura.cresceu) {
372
+ cresceu = true;
373
+ cursor.crescimentoEm = Math.max(cursor.crescimentoEm, leitura.quando);
374
+ cursor.progressoEm = Math.max(cursor.progressoEm, leitura.quando);
375
+ }
376
+ }
377
+ // Erro de metadados posterior não obriga a reler o mesmo backlog no restart.
378
+ (0, codex_runner_1.gravarAtomico)(file, cursor);
379
+ const processo = registro?.processoPath ? lerJson(registro.processoPath) : null;
380
+ const recibo = registro?.reciboPath ? lerJson(registro.reciboPath) : null;
381
+ if (recibo && (!processo || recibo.schema !== 'ork.codex-process/v1' || recibo.iniciadoEm !== processo.iniciadoEm ||
382
+ JSON.stringify(recibo.filho) !== JSON.stringify(processo.filho) ||
383
+ !Number.isFinite(Date.parse(recibo.terminadoEm)) || Date.parse(recibo.terminadoEm) > now ||
384
+ !Number.isFinite(recibo.duracaoMs) || recibo.duracaoMs < 0 ||
385
+ (recibo.exitCode !== null && (!Number.isInteger(recibo.exitCode) || recibo.exitCode < 0)) ||
386
+ (recibo.signal !== null && typeof recibo.signal !== 'string')))
387
+ throw new Error('recibo de processo inválido');
388
+ // Fechamento normalizado: recibo do supervisor OU close real do controller, nunca ambos.
389
+ const fechamento = snapshot?.fechamento
390
+ ? { exitCode: snapshot.fechamento.exitCode, signal: snapshot.fechamento.signal,
391
+ duracaoMs: snapshot.fechamento.duracaoMs, erro: null, fonte: 'controller.close' }
392
+ : recibo ? { exitCode: recibo.exitCode, signal: recibo.signal, duracaoMs: recibo.duracaoMs,
393
+ erro: recibo.erro, fonte: 'supervisor.close' } : null;
394
+ const supervisionado = !!registro?.processoPath || !!snapshot;
395
+ // Espera nativa autenticada e viva não é silêncio: não conta prazo nem termina o turno.
396
+ const esperaHumana = snapshot?.esperaHumana ?? false;
397
+ const estado = fechamento ? 'ausente' : snapshot ? snapshot.estadoRuntime : (0, codex_runner_1.estadoProcesso)(processo?.filho ?? null);
398
+ const terminais = Object.entries(cursor.fontes).filter(([, f]) => f.terminal)
399
+ .map(([nome, f]) => ({ nome, terminal: f.terminal }));
400
+ const ordem = { gate_blocked: 0, 'human.pending': 1, fase_concluida: 2 };
401
+ terminais.sort((a, b) => {
402
+ const x = a.terminal, y = b.terminal;
403
+ // Observação de leitura não é timestamp do evento. Contagem só desempata
404
+ // fontes sem timestamps comparáveis; severidade vale no mesmo turno.
405
+ const tempo = x.timestampFonte === 'evento' && y.timestampFonte === 'evento'
406
+ ? Date.parse(y.timestamp) - Date.parse(x.timestamp) : 0;
407
+ return tempo || (y.numeroTurno ?? 0) - (x.numeroTurno ?? 0) ||
408
+ ordem[x.classificacao] - ordem[y.classificacao] || a.nome.localeCompare(b.nome);
409
+ });
410
+ let terminal = terminais[0]?.terminal;
411
+ const fontesAtuais = Object.values(cursor.fontes);
412
+ const aberto = fontesAtuais.some(f => {
413
+ if (!f.parser.turnoAberto)
414
+ return false;
415
+ if (!terminal)
416
+ return true;
417
+ if (f.parser.turnoIniciadoEm && terminal.timestampFonte === 'evento') {
418
+ return Date.parse(f.parser.turnoIniciadoEm) > Date.parse(terminal.timestamp);
419
+ }
420
+ // Um terminal confirmado cobre a fonte atrasada ainda aberta no mesmo turno.
421
+ return f.parser.turnos > (terminal.numeroTurno ?? 0);
422
+ });
423
+ const parcial = fontesAtuais.some(f => f.parcial);
424
+ const backlog = fontesAtuais.some(f => f.tamanho > f.offset && !f.parcial);
425
+ // Suspensao medida entre observacoes autenticadas; persiste no cursor para restart.
426
+ if (esperaHumana)
427
+ cursor.esperaDesde ??= now;
428
+ else if (cursor.esperaDesde !== undefined) {
429
+ cursor.progressoEm += Math.max(0, now - Math.max(cursor.esperaDesde, cursor.progressoEm));
430
+ delete cursor.esperaDesde;
431
+ }
432
+ const estavel = !esperaHumana && now - cursor.progressoEm >= exports.LIMITE_MORTE_MS;
433
+ const semCloseNoLimite = supervisionado && !fechamento && estavel;
434
+ const invalido = fontesAtuais.some(f => f.parser.invalido);
435
+ // Conclusão nativa é medida em todo terminal, independente da política de retry:
436
+ // bloqueio sem retry preserva o motivo, mas não prova que o turno do runtime terminou.
437
+ const provaNativa = snapshot && terminal
438
+ ? semProvaNativa(snapshot, terminal, terminais[0]?.nome, fechamento, sessionId, cwdDespacho)
439
+ : 'terminal nativo do controller ausente';
440
+ // Parecer negativo também exige conclusão nativa inteira: texto claims.failed de
441
+ // turno interrompido não pode virar rejeição autenticada de um objetivo.
442
+ const semProva = snapshot && terminal && terminal.classificacao !== 'human.pending' && !bloqueioPreservado(terminal)
443
+ ? provaNativa : null;
444
+ const descorrelacionado = !!semProva;
445
+ const incompleto = semCloseNoLimite && (!!terminal || estado !== 'ausente' || aberto || parcial || invalido) || !backlog && (((parcial || aberto || invalido || descorrelacionado || !terminal) && !!fechamento) ||
446
+ (estavel && (parcial || (aberto && estado === 'ausente') ||
447
+ (supervisionado && estado === 'desconhecido'))));
448
+ const morte = estado === 'ausente' && estavel && !temMais;
449
+ // Fechamento supervisionado precisa existir antes de associar o código de saída ao turno.
450
+ const podeTerminar = !esperaHumana && (incompleto || (!temMais && !aberto &&
451
+ ((terminal && (fechamento || !supervisionado)) || morte)));
452
+ const conta = falhaDeContaDoTerminal(terminal, !invalido && !aberto, snapshot, now);
453
+ (0, codex_runner_1.gravarAtomico)(file, cursor);
454
+ return (0, session_events_1.comLockDeSessao)(dir, () => {
455
+ const atuais = (0, ledger_1.lerLedger)(dir);
456
+ if (cresceu && cursor.crescimentoEm > cursor.heartbeatEm) {
457
+ const heartbeatId = `${resultadoId}:heartbeat:${cursor.crescimentoEm}`;
458
+ if (!atuais.some(e => e.sensorEventId === heartbeatId))
459
+ (0, ledger_1.registrar)(dir, thread.id, 'runtime_heartbeat', {
460
+ ts: new Date(cursor.crescimentoEm).toISOString(), fase: sessao.fase, sessionId, despachoEm: sessao.despachadaEm,
461
+ sensorEventId: heartbeatId, fonte: 'progresso reconhecido do stream/rollout', bytesLidos,
462
+ });
463
+ cursor.heartbeatEm = cursor.crescimentoEm;
464
+ (0, codex_runner_1.gravarAtomico)(file, cursor);
465
+ }
466
+ if (!podeTerminar)
467
+ return { ...base, bytesLidos };
468
+ const semTerminal = !terminal;
469
+ // Fato registrado para a autoridade de revisão: `null` só com terminal nativo correlacionado.
470
+ const conclusaoNativa = incompleto || invalido || aberto || descorrelacionado
471
+ ? provaNativa ?? 'observação incompleta ou inválida' : provaNativa;
472
+ if (incompleto || invalido || aberto || descorrelacionado)
473
+ terminal = { classificacao: 'gate_blocked', motivo: 'runtime.unavailable',
474
+ timestamp: new Date(now).toISOString(), turnId: null, fonte: semProva ?? 'observação incompleta ou inválida',
475
+ tokens: new codex_events_1.ParserCodex().estado.tokens, duracaoMs: null };
476
+ if (!terminal)
477
+ terminal = { classificacao: 'gate_blocked', motivo: 'runtime.silencio',
478
+ timestamp: new Date(now).toISOString(), turnId: null, fonte: 'processo ausente e arquivos estáveis',
479
+ tokens: new codex_events_1.ParserCodex().estado.tokens, duracaoMs: null };
480
+ if (fechamento && (fechamento.exitCode !== 0 || fechamento.signal || fechamento.erro) &&
481
+ terminal.classificacao !== 'human.pending' && !bloqueioPreservado(terminal)) {
482
+ terminal = { ...terminal, classificacao: 'gate_blocked', motivo: 'runtime.unavailable' };
483
+ }
484
+ // I-33 (D12): o turno terminou pela CONTA (cota, credito ou login), dito pelo proprio runtime.
485
+ // E bloqueio, nunca conclusao; a D11c decide no retry se houve producao antes da morte.
486
+ if (conta)
487
+ terminal = { ...terminal, classificacao: 'gate_blocked', motivo: conta.falha.motivo, falhaDeConta: conta.falha,
488
+ fonte: `${conta.fonte}: ${conta.falha.motivo} (${conta.falha.trecho})` };
489
+ const dados = { fase: sessao.fase, sessionId, despachoEm: sessao.despachadaEm, sensorResultId: resultadoId,
490
+ classificacao: terminal.classificacao, motivo: terminal.motivo, runtime: 'codex', fonte: terminal.fonte,
491
+ fonteVencedora: incompleto || invalido || aberto || descorrelacionado ? null : terminais[0]?.nome ?? null,
492
+ turnId: terminal.turnId, numeroTurno: terminal.numeroTurno ?? null,
493
+ tokens: terminal.tokens, exitCode: fechamento?.exitCode ?? null, exitCodeFonte: fechamento?.fonte ?? 'unavailable',
494
+ signal: fechamento?.signal ?? null, duracaoMs: fechamento?.duracaoMs ?? terminal.duracaoMs,
495
+ duracaoFonte: fechamento?.fonte ?? (terminal.duracaoMs === null ? 'unavailable' : terminal.fonte),
496
+ ok: terminal.classificacao === 'fase_concluida' && fechamento?.exitCode === 0,
497
+ conclusaoNativa: conclusaoNativa === null,
498
+ conclusaoNativaAusente: conclusaoNativa,
499
+ estado: terminal.classificacao === 'fase_concluida' ? 'concluida' : 'bloqueada',
500
+ observadoEm: terminal.timestamp, gate: 'phase.dispatch', origem: 'sessions.watch',
501
+ morteLatenciaMs: morte ? now - cursor.progressoEm : null, ...(perfil ? { perfil } : {}),
502
+ ...(conta ? { falhaDeConta: { motivo: conta.falha.motivo, resetEm: conta.falha.resetEm, fonte: conta.falha.fonte,
503
+ trecho: conta.falha.trecho } } : {}) };
504
+ const tipo = semTerminal && morte && !incompleto && !conta ? 'sessao_morta' : terminal.classificacao;
505
+ // I-33 (D12): o perfil sai do rodizio na primeira gravacao do resultado, onde a evidencia esta.
506
+ if (conta && perfil && !atuais.some(e => e.tipo === 'phase_result' && e.sensorResultId === resultadoId)) {
507
+ try {
508
+ (0, ratelimit_1.marcarContaDaFalha)(carregado, perfil, conta.falha, now);
509
+ }
510
+ catch { /* store ocupado ou invalido: o retry marca de novo pelo gate tipado */ }
511
+ }
512
+ if (!atuais.some(e => e.tipo === tipo && e.sensorResultId === resultadoId))
513
+ (0, ledger_1.registrar)(dir, thread.id, tipo, dados);
514
+ if (terminal.classificacao !== 'fase_concluida' && tipo !== 'gate_blocked' &&
515
+ !atuais.some(e => e.tipo === 'gate_blocked' && e.sensorResultId === resultadoId))
516
+ (0, ledger_1.registrar)(dir, thread.id, 'gate_blocked', dados);
517
+ if (!atuais.some(e => e.tipo === 'phase_result' && e.sensorResultId === resultadoId))
518
+ (0, ledger_1.registrar)(dir, thread.id, 'phase_result', dados);
519
+ return { ...base, bytesLidos, concluido: true, classificacao: terminal.classificacao };
520
+ });
521
+ }) ?? { ...base, ocupado: true };
522
+ }
523
+ catch (e) {
524
+ // Thread apagada no meio da observação: encerra sem erro, sem recriar a pasta e sem resíduo.
525
+ if (!(0, ledger_1.threadPresente)(dir))
526
+ return desfazer();
527
+ if (!/ocupad/.test(e.message) && !(0, ledger_1.registrarSeExiste)(dir, thread.id, 'session_watcher_error', {
528
+ fase: sessao.fase, sessionId, despachoEm: sessao.despachadaEm, motivo: 'runtime.unavailable',
529
+ // Não copiar conteúdo de metadados/logs em mensagens de JSON.parse.
530
+ erro: e instanceof SyntaxError ? 'metadado JSON inválido' : e.message,
531
+ origem: 'sessions.watch',
532
+ }))
533
+ return removida;
534
+ throw e;
535
+ }
536
+ }
537
+ /** Valida a fonte antes de registro, stderr ou spawn; tambem usada na recuperacao. */
538
+ function validarFonteWatcher(raiz, sessionId, fonte) {
539
+ const { thread, sessao: registrada } = (0, session_events_1.resolverSessao)(raiz, sessionId);
540
+ const sessao = (0, session_events_1.exigirSessaoDespachada)(registrada, sessionId);
541
+ const dir = (0, thread_1.dirThread)(raiz, thread.id);
542
+ const eventos = (0, ledger_1.lerLedger)(dir);
543
+ const registro = fonte ?? eventos.filter(e => e.tipo === 'session_sensor_registered' &&
544
+ e.sessionId === sessionId && e.despachoEm === sessao.despachadaEm).at(-1);
545
+ if (!registro)
546
+ throw new Error('fonte de sensor ausente');
547
+ conferirRegistro(dir, registro);
548
+ // I-33 (D4): perfil invalido ou divergente do despacho reprova antes de observar pela conta errada.
549
+ (0, runtime_profiles_1.perfilDoRegistro)([...eventos, ...(fonte ? [{ tipo: 'session_sensor_registered', sessionId,
550
+ despachoEm: sessao.despachadaEm, ...fonte }] : [])], sessao);
551
+ if (registro.nativo !== undefined) {
552
+ if (sessao.runtime !== 'claude-bg')
553
+ throw new Error('fonte nativa claude-agents em sessão de outro runtime');
554
+ (0, session_watcher_claude_1.validarFonteClaude)(eventos, sessionId, registro);
555
+ return;
556
+ }
557
+ if (registro.controlador) {
558
+ const despacho = eventos.filter(e => e.tipo === 'phase_dispatch' && e.sessionId === sessionId).at(-1);
559
+ if (despacho?.controlador !== undefined && despacho.controlador !== registro.controlador)
560
+ throw new Error('controlador registrado divergente do despacho');
561
+ const cwd = (despacho?.cwd ?? registro.cwd);
562
+ if (!cwd)
563
+ throw new Error('despacho do controller sem cwd registrado');
564
+ const chave = (0, node_crypto_1.createHash)('sha256').update(sessionId + '|' + sessao.despachadaEm).digest('hex');
565
+ const fixada = (0, codex_controller_sensor_1.registrarFonteController)(registro.controlador, { dirSessoes: path.join(dir, 'sessoes'),
566
+ fixacao: path.join(dir, 'sessoes', `watcher-source-${chave}.json`),
567
+ sessionId, cwd, despachoEm: sessao.despachadaEm,
568
+ vinculo: { thread: thread.id, fase: sessao.fase, promptSha256: sessao.promptSha256 } });
569
+ (0, codex_controller_sensor_1.lerSnapshotController)(fixada);
570
+ }
571
+ }
572
+ function iniciarWatcher(raiz, sessionId) {
573
+ const { thread, sessao: registrada } = (0, session_events_1.resolverSessao)(raiz, sessionId);
574
+ const sessao = (0, session_events_1.exigirSessaoDespachada)(registrada, sessionId);
575
+ const dir = (0, thread_1.dirThread)(raiz, thread.id);
576
+ // I-34: sessão claude-bg despachada antes do registro nativo ganha a fonte derivada do despacho.
577
+ if (sessao.runtime === 'claude-bg')
578
+ (0, session_watcher_claude_1.garantirFonteClaude)(raiz, thread, sessao);
579
+ validarFonteWatcher(raiz, sessionId);
580
+ const anterior = (0, ledger_1.lerLedger)(dir).filter(e => e.tipo === 'session_watcher_started' &&
581
+ e.sessionId === sessionId && e.despachoEm === sessao.despachadaEm).at(-1);
582
+ const identidadeAnterior = anterior?.identidade;
583
+ if (identidadeAnterior && anterior?.pid === identidadeAnterior.pid && (0, codex_runner_1.estadoProcesso)(identidadeAnterior) === 'vivo')
584
+ return identidadeAnterior.pid;
585
+ const token = (0, node_crypto_1.randomUUID)();
586
+ const readyPath = path.join(dir, `watcher-${sessionId}-${token}.ready.json`);
587
+ const ackPath = readyPath + '.ack';
588
+ const err = fs.openSync(path.join(dir, `watcher-${sessionId}.stderr`), 'a', 0o600);
589
+ let child;
590
+ try {
591
+ child = (0, node_child_process_1.spawn)(process.execPath, [__filename, '--run', raiz, sessionId, readyPath, token], {
592
+ cwd: raiz, detached: true, stdio: ['ignore', 'ignore', err],
593
+ });
594
+ }
595
+ finally {
596
+ fs.closeSync(err);
597
+ }
598
+ let erroRegistrado = false;
599
+ const falha = () => {
600
+ if (erroRegistrado)
601
+ return;
602
+ erroRegistrado = true;
603
+ (0, ledger_1.registrar)(dir, thread.id, 'session_watcher_error', { sessionId, despachoEm: sessao.despachadaEm,
604
+ fase: sessao.fase, motivo: 'runtime.unavailable', erro: 'falha ao confirmar prontidao do watcher',
605
+ origem: 'sessions.watch' });
606
+ };
607
+ child.on('error', falha);
608
+ child.unref();
609
+ const identidade = child.pid ? (0, codex_runner_1.identidadeProcesso)(child.pid) : null;
610
+ try {
611
+ if (!identidade)
612
+ throw new Error('watcher sem identidade de processo');
613
+ const deadline = Date.now() + 5000;
614
+ const pausa = new Int32Array(new SharedArrayBuffer(4));
615
+ while (Date.now() < deadline) {
616
+ const ready = lerJson(readyPath);
617
+ if (ready && ready.token === token && ready.sessionId === sessionId && ready.despachoEm === sessao.despachadaEm &&
618
+ JSON.stringify(ready.identidade) === JSON.stringify(identidade) && (0, codex_runner_1.estadoProcesso)(identidade) === 'vivo') {
619
+ (0, ledger_1.registrar)(dir, thread.id, 'session_watcher_started', { fase: sessao.fase, sessionId,
620
+ despachoEm: sessao.despachadaEm, pid: identidade.pid, identidade, readyPath, token });
621
+ (0, codex_runner_1.gravarAtomico)(ackPath, { token });
622
+ return identidade.pid;
623
+ }
624
+ if ((0, codex_runner_1.estadoProcesso)(identidade) === 'ausente')
625
+ break;
626
+ Atomics.wait(pausa, 0, 0, 10);
627
+ }
628
+ throw new Error('watcher sem prontidao confirmada');
629
+ }
630
+ catch (e) {
631
+ falha();
632
+ // Encerrar apenas o filho cuja identidade continua sendo a capturada no spawn.
633
+ if (identidade && (0, codex_runner_1.estadoProcesso)(identidade) === 'vivo')
634
+ child.kill('SIGTERM');
635
+ throw e;
636
+ }
637
+ }
638
+ async function acompanhar() {
639
+ const carregado = (0, manifest_1.exigirManifesto)(process.argv[3]), sessionId = process.argv[4];
640
+ const { thread } = (0, session_events_1.resolverSessao)(carregado.raiz, sessionId);
641
+ const readyPath = process.argv[5], token = process.argv[6];
642
+ if (readyPath && token) {
643
+ validarFonteWatcher(carregado.raiz, sessionId);
644
+ const sessao = (0, session_events_1.exigirSessaoDespachada)((0, session_events_1.resolverSessao)(carregado.raiz, sessionId).sessao, sessionId);
645
+ const identidade = (0, codex_runner_1.identidadeProcesso)(process.pid);
646
+ if (!identidade)
647
+ throw new Error('watcher sem identidade propria');
648
+ (0, codex_runner_1.gravarAtomico)(readyPath, { token, sessionId, despachoEm: sessao.despachadaEm, identidade });
649
+ const deadline = Date.now() + 5000;
650
+ while (lerJson(readyPath + '.ack')?.token !== token) {
651
+ if (Date.now() >= deadline)
652
+ throw new Error('watcher sem confirmacao do pai');
653
+ await new Promise(r => setTimeout(r, 10));
654
+ }
655
+ }
656
+ const dir = (0, thread_1.dirThread)(carregado.raiz, thread.id);
657
+ for (;;) {
658
+ // Projeto ou thread apagados encerram o laço em silêncio: nada a observar nem a recriar.
659
+ if (!(0, ledger_1.threadPresente)(dir))
660
+ return;
661
+ let atual;
662
+ try {
663
+ atual = (0, thread_1.lerThread)(carregado.raiz, thread.id);
664
+ if (atual.status !== 'aberta' || atual.sessoes.at(-1)?.sessionId !== sessionId)
665
+ return;
666
+ const r = observarSessao(carregado, sessionId);
667
+ if (r.concluido || r.encerrado)
668
+ return;
669
+ }
670
+ catch (e) {
671
+ if (!(0, ledger_1.threadPresente)(dir))
672
+ return;
673
+ if (!/ocupad/.test(e.message))
674
+ throw e;
675
+ }
676
+ await new Promise(r => setTimeout(r, atual?.sessoes.at(-1)?.runtime === 'claude-bg' ? session_watcher_claude_1.INTERVALO_WATCH_CLAUDE_MS : exports.INTERVALO_WATCH_MS));
677
+ }
678
+ }
679
+ if (require.main === module && process.argv[2] === '--run')
680
+ acompanhar().catch(() => {
681
+ process.stderr.write('ork watcher: observação interrompida; confira session_watcher_error e estado canônico.\n');
682
+ process.exitCode = 1;
683
+ });