@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
package/dist/doctor.js CHANGED
@@ -39,21 +39,72 @@ var __importStar = (this && this.__importStar) || (function () {
39
39
  };
40
40
  })();
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.checarContas = checarContas;
43
+ exports.checarOnboarding = checarOnboarding;
42
44
  exports.checar = checar;
43
45
  exports.relatorio = relatorio;
44
46
  exports.doctor = doctor;
45
47
  const fs = __importStar(require("node:fs"));
46
48
  const path = __importStar(require("node:path"));
47
49
  const adapter = __importStar(require("./adapters/claude-bg"));
50
+ const codexAdapter = __importStar(require("./adapters/codex"));
48
51
  const manifest_1 = require("./manifest");
49
52
  const modos_1 = require("./modos");
50
53
  const orkmind_1 = require("./orkmind");
54
+ const onboarding_1 = require("./onboarding");
51
55
  const ratelimit_1 = require("./ratelimit");
52
56
  const slug_1 = require("./slug");
57
+ const horario_1 = require("./horario");
53
58
  const util_1 = require("./util");
59
+ const sessoes_inventario_1 = require("./sessoes-inventario");
60
+ const estado_thread_1 = require("./estado-thread");
61
+ const project_state_1 = require("./project-state");
62
+ const runtime_ambiente_1 = require("./runtime-ambiente");
63
+ const runtime_profiles_1 = require("./runtime-profiles");
64
+ const preflight_1 = require("./preflight");
65
+ /**
66
+ * I-33 (D7): check "contas por runtime". Cada perfil ativo tem o login conferido pelo proprio
67
+ * CLI com o env dele (`claude auth status`, `codex login status`); o doctor so le e relata, sem
68
+ * marcar o store. Runtime com perfis e nenhum pronto reprova: o despacho dele nao sairia.
69
+ */
70
+ function checarContas(raiz, auth = (p) => p.runtime === 'claude-bg' ? adapter.conferirAuth(p) : codexAdapter.conferirAuth(p)) {
71
+ let perfis;
72
+ try {
73
+ perfis = (0, runtime_profiles_1.lerPerfisComContas)(raiz).perfis.filter(p => p.estado !== 'desativado');
74
+ }
75
+ catch (e) {
76
+ return { nome: 'contas por runtime', nivel: 'fail', detalhe: `store de perfis invalido: ${e.message}`,
77
+ correcao: 'o store fica em .orkastery/private/runtime-profiles.json (arquivo 0600, pasta 0700)' };
78
+ }
79
+ if (perfis.length === 0)
80
+ return { nome: 'contas por runtime', nivel: 'ok',
81
+ detalhe: 'nenhum perfil configurado: cada runtime despacha pelo ambiente do processo' };
82
+ const partes = [];
83
+ let nivel = 'ok';
84
+ for (const runtime of runtime_profiles_1.RUNTIMES_COM_PERFIL) {
85
+ const doRuntime = perfis.filter(p => p.runtime === runtime);
86
+ if (doRuntime.length === 0)
87
+ continue;
88
+ const estados = doRuntime.map(p => {
89
+ const a = auth((0, runtime_profiles_1.perfilDeDespacho)(p));
90
+ return { id: p.id, pronto: a.ok && (0, runtime_profiles_1.perfilDisponivel)(p),
91
+ texto: !a.ok ? (a.pago ? 'provider pago (nunca despacha)' : a.transitorio ? 'conferencia de login inconclusiva' : 'sem login')
92
+ : (0, runtime_profiles_1.perfilDisponivel)(p) ? 'pronto' : `${p.estado}${p.esgotadoAte ? ` ate ${p.esgotadoAte}` : ''}` };
93
+ });
94
+ const prontos = estados.filter(e => e.pronto).length;
95
+ if (prontos === 0)
96
+ nivel = 'fail';
97
+ else if (prontos < estados.length && nivel === 'ok')
98
+ nivel = 'warn';
99
+ partes.push(`${runtime}: ${estados.map(e => `${e.id} ${e.texto}`).join(', ')}`);
100
+ }
101
+ return { nome: 'contas por runtime', nivel, detalhe: partes.join('; '),
102
+ ...(nivel !== 'ok' ? { correcao: 'ork accounts check marca os perfis (sem-auth, provider-pago); ork accounts add refaz o login de assinatura pelo proprio CLI, nunca copiando credencial' } : {}) };
103
+ }
54
104
  /**
55
105
  * Variaveis que REDIRECIONAM o despacho do `claude` para um provider pago.
56
- * Presenca delas com politica `subscription-only` e bloqueante (a licao dos US$ 16).
106
+ * Presenca delas com politica `subscription-only` e bloqueante: o despacho passaria a cobrar
107
+ * por token, fora da assinatura, sem ninguem pedir.
57
108
  */
58
109
  const ENVS_QUE_REDIRECIONAM = [
59
110
  'ANTHROPIC_API_KEY',
@@ -62,16 +113,36 @@ const ENVS_QUE_REDIRECIONAM = [
62
113
  'CLAUDE_CODE_USE_BEDROCK',
63
114
  'CLAUDE_CODE_USE_VERTEX',
64
115
  ];
65
- /**
66
- * Variaveis de provider pago que nao mudam o despacho do `claude`, mas indicam
67
- * credencial cobrada por token no ambiente. Viram aviso, nao bloqueio.
68
- */
69
- const ENVS_DE_PROVIDER_PAGO = ['OPENROUTER_API_KEY', 'OPENAI_API_KEY', 'GEMINI_API_KEY'];
70
116
  function versaoNode() {
71
117
  return Number(process.versions.node.split('.')[0]);
72
118
  }
119
+ /** Checks locais; ler a entrevista não abre driver, banco, runtime ou rede. */
120
+ function checarOnboarding(carregado) {
121
+ const estado = (0, onboarding_1.lerOnboarding)(carregado.raiz);
122
+ const pendentes = onboarding_1.ETAPAS_ONBOARDING.filter(e => estado.etapas[e] === null);
123
+ const checks = [{ nome: 'onboarding', nivel: pendentes.length ? 'warn' : 'ok',
124
+ detalhe: pendentes.length ? `${pendentes.length} etapa(s) pendente(s): ${pendentes.join(', ')}` : '9 etapas respondidas',
125
+ ...(pendentes.length ? { correcao: 'ork onboarding' } : {}) }];
126
+ const escolha = estado.etapas.memoria?.conteudo;
127
+ const modo = escolha === 'sim' || escolha === true ? 'orkmind' :
128
+ escolha === 'orkmind' || escolha === 'files' ? escolha :
129
+ escolha && typeof escolha === 'object' && !Array.isArray(escolha) ? escolha.modo : null;
130
+ if ((modo === 'orkmind' || modo === 'files') && modo !== carregado.manifesto.memory.mode) {
131
+ checks.push({ nome: 'onboarding memoria', nivel: 'warn', detalhe: `entrevista escolheu ${modo}; manifesto declara ${carregado.manifesto.memory.mode}`,
132
+ correcao: `revise memory.mode: ${modo} em orkastery.yaml; a entrevista não altera o manifesto` });
133
+ }
134
+ // I-35: o fuso respondido na etapa maestro orienta owner.timezone, sem editar o manifesto.
135
+ const maestro = estado.etapas.maestro?.conteudo;
136
+ const fusoDaEntrevista = maestro && typeof maestro === 'object' && !Array.isArray(maestro) ? (0, horario_1.normalizarFuso)(maestro.fuso) : undefined;
137
+ if (fusoDaEntrevista && fusoDaEntrevista !== carregado.manifesto.owner?.timezone) {
138
+ checks.push({ nome: 'onboarding fuso', nivel: 'warn',
139
+ detalhe: `entrevista informou ${fusoDaEntrevista}; manifesto declara ${carregado.manifesto.owner?.timezone ?? `${horario_1.CHAVE_DO_FUSO} ausente`}`,
140
+ correcao: `revise ${horario_1.CHAVE_DO_FUSO}: "${fusoDaEntrevista}" em orkastery.yaml; a entrevista não altera o manifesto` });
141
+ }
142
+ return checks;
143
+ }
73
144
  /** Roda todos os checks a partir do diretorio informado. */
74
- function checar(dirInicial = process.cwd()) {
145
+ function checar(dirInicial = process.cwd(), nomesHerdados = (0, runtime_ambiente_1.nomesDeProviderAtivos)()) {
75
146
  const checks = [];
76
147
  const major = versaoNode();
77
148
  checks.push({
@@ -104,6 +175,37 @@ function checar(dirInicial = process.cwd()) {
104
175
  detalhe: claude ? `${claude} (${v ?? 'versao desconhecida'})` : 'binario `claude` fora do PATH',
105
176
  correcao: claude ? undefined : 'instale o Claude Code e garanta `claude` no PATH',
106
177
  });
178
+ // Segundo runtime homologado. Ausente e `warn`, nao `fail`: o claude-bg segue sendo o
179
+ // padrao, e um projeto que nunca pediu codex nao pode ficar bloqueado por ele.
180
+ const codex = codexAdapter.disponivel();
181
+ const codexVersao = codex ? codexAdapter.versao() : null;
182
+ checks.push({
183
+ nome: 'runtime codex',
184
+ nivel: codex ? 'ok' : 'warn',
185
+ detalhe: codex
186
+ ? `${codex} (${codexVersao ?? 'versao desconhecida'})`
187
+ : 'binario `codex` fora do PATH (opcional: claude-bg e o runtime padrao)',
188
+ correcao: codex
189
+ ? undefined
190
+ : 'para despachar pelo codex, instale o Codex CLI e autentique com `codex login`',
191
+ });
192
+ // Pitfall MEDIDO em 06/09/2026: com o sandbox quebrado, o `codex exec` sai 0 e o agente
193
+ // NARRA sucesso sem executar nada. A sonda e deterministica (roda `true`, sem LLM).
194
+ let sondaDoCodex = null;
195
+ if (codex) {
196
+ sondaDoCodex = codexAdapter.sondarSandbox();
197
+ checks.push({
198
+ nome: 'sandbox do codex',
199
+ nivel: sondaDoCodex.ok ? 'ok' : 'warn',
200
+ detalhe: sondaDoCodex.ok
201
+ ? '`codex sandbox true` executa nesta maquina'
202
+ : `sonda \`codex sandbox true\` falhou: ${sondaDoCodex.detalhe}`,
203
+ correcao: sondaDoCodex.ok
204
+ ? undefined
205
+ : 'instale o pacote bubblewrap do sistema (o bwrap embutido nao cria user namespace ' +
206
+ 'nesta maquina) ou declare runtime.sandbox: danger-full-access ciente do risco',
207
+ });
208
+ }
107
209
  const carregado = (0, manifest_1.carregarManifesto)(dirInicial);
108
210
  if (!carregado) {
109
211
  checks.push({
@@ -123,6 +225,7 @@ function checar(dirInicial = process.cwd()) {
123
225
  (carregado.avisos.length > 0 ? `; ${carregado.avisos.join('; ')}` : ''),
124
226
  correcao: carregado.erros.length > 0 ? 'corrija o manifesto e rode ork doctor de novo' : undefined,
125
227
  });
228
+ checks.push(...checarOnboarding(carregado));
126
229
  const abbrev = carregado.manifesto.project.abbrev;
127
230
  const checkAbbrev = (0, slug_1.validarAbbrev)(abbrev);
128
231
  checks.push({
@@ -140,13 +243,43 @@ function checar(dirInicial = process.cwd()) {
140
243
  detalhe: `padrao ${modos_1.MODOS[cond.default_mode].tag}; ` +
141
244
  `permitidos ${cond.allowed_modes.map((m) => modos_1.MODOS[m].tag).join(', ')}`,
142
245
  });
246
+ // I-35: o fuso em que todo horario chega ao dono; valor invalido avisa e nao para nada.
247
+ const fuso = (0, horario_1.fusoDoManifesto)(carregado);
248
+ checks.push({
249
+ nome: 'fuso do dono',
250
+ nivel: fuso.aviso ? 'warn' : 'ok',
251
+ detalhe: fuso.aviso ?? (fuso.origem === 'manifesto'
252
+ ? `${fuso.fuso} (${horario_1.CHAVE_DO_FUSO}); horarios para pessoas em ${(0, horario_1.rotuloDoFuso)(fuso.fuso)}`
253
+ : `${fuso.fuso} (fuso do sistema; ${horario_1.CHAVE_DO_FUSO} nao configurado)`),
254
+ correcao: fuso.aviso ? `corrija ${horario_1.CHAVE_DO_FUSO} com um nome IANA` : undefined,
255
+ });
256
+ // Quando o manifesto ELEGE o codex como runtime de despacho, a ausencia dele (ou um
257
+ // sandbox que nao executa) deixa de ser aviso: e exatamente o que bloquearia a fase.
258
+ if (carregado.manifesto.runtime.adapter === 'codex') {
259
+ const sandboxConfigurado = carregado.manifesto.runtime.sandbox;
260
+ const sandboxQuebrado = sondaDoCodex !== null && !sondaDoCodex.ok && sandboxConfigurado !== 'danger-full-access';
261
+ checks.push({
262
+ nome: 'despacho pelo codex',
263
+ nivel: !codex ? 'fail' : sandboxQuebrado ? 'fail' : 'ok',
264
+ detalhe: !codex
265
+ ? 'runtime.adapter: codex, mas o binario `codex` esta fora do PATH'
266
+ : sandboxQuebrado
267
+ ? `runtime.adapter: codex com sandbox "${sandboxConfigurado}", mas o sandbox nao executa nesta maquina (o agente narraria sucesso sem rodar nada)`
268
+ : `runtime.adapter: codex com sandbox "${sandboxConfigurado}"`,
269
+ correcao: !codex
270
+ ? 'instale o Codex CLI e autentique com `codex login` (assinatura, nunca API key)'
271
+ : sandboxQuebrado
272
+ ? 'instale o bubblewrap do sistema, ou declare runtime.sandbox: danger-full-access ciente do risco'
273
+ : undefined,
274
+ });
275
+ }
143
276
  const politica = carregado.manifesto.runtime.provider_policy;
144
277
  const soAssinatura = politica === 'subscription-only';
145
- const redirecionam = ENVS_QUE_REDIRECIONAM.filter((e) => (process.env[e] ?? '').trim() !== '');
146
- const pagas = ENVS_DE_PROVIDER_PAGO.filter((e) => (process.env[e] ?? '').trim() !== '');
278
+ const redirecionam = ENVS_QUE_REDIRECIONAM.filter(e => nomesHerdados.includes(e));
279
+ const pagas = nomesHerdados;
147
280
  const bloqueia = soAssinatura && redirecionam.length > 0;
148
281
  checks.push({
149
- nome: 'custo e provider',
282
+ nome: 'custo e provider herdado',
150
283
  nivel: bloqueia ? 'fail' : soAssinatura && pagas.length > 0 ? 'warn' : 'ok',
151
284
  detalhe: bloqueia
152
285
  ? `politica ${politica} violada: ${redirecionam.join(', ')} redireciona o despacho do claude`
@@ -159,10 +292,19 @@ function checar(dirInicial = process.cwd()) {
159
292
  ? `nenhuma acao obrigatoria; nunca despache fase por ${pagas.join(', ')}`
160
293
  : undefined,
161
294
  });
295
+ const efetivas = (0, runtime_ambiente_1.nomesDeProviderAtivos)();
296
+ checks.push({
297
+ nome: 'provider efetivo da fabrica',
298
+ nivel: soAssinatura && efetivas.length ? 'warn' : 'ok',
299
+ detalhe: efetivas.length ? `nomes ativos: ${efetivas.join(', ')}` : 'nenhum nome da lista de provider ativo',
300
+ });
162
301
  // Bloco B6: o regime de memoria e um CHECK, nao um detalhe do manifesto. Manifesto
163
302
  // que pede `orkmind` e nao consegue liga-lo vira `warn` com a correcao, nunca `fail`:
164
303
  // memoria semantica melhora o produto, e nao e requisito dele.
165
- const { estado } = (0, orkmind_1.resolverRegime)(carregado.manifesto, null);
304
+ const context = (0, project_state_1.memoryState)(carregado);
305
+ const { estado } = (0, orkmind_1.resolverRegime)(context.loaded.manifesto, null);
306
+ checks.push({ nome: 'fonte da memoria', nivel: context.divergent ? 'warn' : 'ok',
307
+ detalhe: `${context.source}${context.divergent ? ' (cópia da worktree divergente preservada)' : ''}` });
166
308
  checks.push({
167
309
  nome: 'regime de memoria',
168
310
  nivel: estado.efetivo === 'orkmind' ? 'ok' : estado.pedido === 'orkmind' ? 'warn' : 'ok',
@@ -185,14 +327,14 @@ function checar(dirInicial = process.cwd()) {
185
327
  detalhe: fila.length === 0
186
328
  ? 'vazia: nenhuma fase morreu por limite de uso neste projeto'
187
329
  : `${esperando.length} aguardando janela, ${escalados.length} escalado(s) para humano` +
188
- (esperando.length > 0 ? `; proxima janela em ${esperando[0].liberaEm}` : ''),
330
+ (esperando.length > 0 ? `; proxima janela em ${(0, horario_1.formatarDataHoraRotulada)(esperando[0].liberaEm)}` : ''),
189
331
  correcao: escalados.length > 0
190
- ? `pedido escalado alem do limite de retry.max_tentativas: ork retry list e ork gate approve`
332
+ ? `pedido escalado alem do limite de retry.max_tentativas: ork retry list e ork gate request <thread>`
191
333
  : esperando.length > 0
192
334
  ? 'ork retry resume retoma o que ja liberou'
193
335
  : undefined,
194
336
  });
195
- const dirEstadoProjeto = path.join(carregado.raiz, manifest_1.DIR_ESTADO);
337
+ const dirEstadoProjeto = path.join((0, estado_thread_1.raizDoEstado)(carregado.raiz), manifest_1.DIR_ESTADO);
196
338
  const threads = path.join(dirEstadoProjeto, 'threads');
197
339
  const qtd = fs.existsSync(threads)
198
340
  ? fs.readdirSync(threads, { withFileTypes: true }).filter((e) => e.isDirectory()).length
@@ -205,14 +347,19 @@ function checar(dirInicial = process.cwd()) {
205
347
  : `${dirEstadoProjeto} ausente`,
206
348
  correcao: fs.existsSync(dirEstadoProjeto) ? undefined : 'ork init cria o diretorio de estado',
207
349
  });
208
- if (claude) {
209
- const sessoes = adapter.listarSessoes(carregado.raiz);
350
+ checks.push(checarContas(carregado.raiz));
351
+ // I-33 (D7): umask e permissoes do estado, na mesma regra do sensor (sem bits 0o022).
352
+ checks.push(...(0, preflight_1.sondasDeAmbiente)(carregado.raiz));
353
+ {
354
+ const inventario = (0, sessoes_inventario_1.inventariarSessoes)(carregado.raiz, { global: true, todas: true });
210
355
  checks.push({
211
- nome: 'sessoes vivas do projeto',
212
- nivel: 'ok',
213
- detalhe: sessoes.length === 0
214
- ? 'nenhuma sessao ativa sob a raiz do projeto'
215
- : sessoes.map((s) => `${s.name ?? s.sessionId.slice(0, 8)} (${s.state ?? s.status ?? '?'})`).join(', '),
356
+ nome: 'governanca de sessoes',
357
+ nivel: inventario.ok && inventario.semThread === 0 ? 'ok' : 'warn',
358
+ detalhe: `${inventario.escopo.usuario}, Claude e Codex, global com histórico: ${inventario.total} sessões; ` +
359
+ `${inventario.semThread} sem thread; ${inventario.ambiguas} ambíguas; ` +
360
+ `inventário ${inventario.ok ? 'válido' : 'INCOMPLETO (zero não comprovado)'}`,
361
+ correcao: inventario.ok && inventario.semThread === 0 ? undefined :
362
+ 'ork sessions --global --all --json; adote identidades sem vínculo com ork sessions adopt <id>',
216
363
  });
217
364
  }
218
365
  }
@@ -224,12 +371,24 @@ function relatorio(checks) {
224
371
  linhas.push('ork doctor: o que vale nesta maquina agora');
225
372
  linhas.push('');
226
373
  const largura = Math.max(...checks.map((c) => c.nome.length));
374
+ // I-35: o Check guarda o ISO (dado de maquina, tambem lido por teste); quem imprime para o dono
375
+ // localiza. Vale para todo check, nao so o de contas: nenhum horario chega cru ao relatorio.
376
+ let localizou = false;
377
+ const local = (t) => {
378
+ const l = (0, horario_1.localizarTexto)(t);
379
+ if (l !== t)
380
+ localizou = true;
381
+ return l;
382
+ };
227
383
  for (const c of checks) {
228
- linhas.push(` ${(0, util_1.simbolo)(c.nivel)} ${c.nome.padEnd(largura)} ${c.detalhe}`);
384
+ linhas.push(` ${(0, util_1.simbolo)(c.nivel)} ${c.nome.padEnd(largura)} ${local(c.detalhe)}`);
229
385
  if (c.nivel !== 'ok' && c.correcao) {
230
- linhas.push(` ${' '.repeat(largura)} correcao: ${c.correcao}`);
386
+ linhas.push(` ${' '.repeat(largura)} correcao: ${local(c.correcao)}`);
231
387
  }
232
388
  }
389
+ // O fuso sai uma vez por mensagem, antes do veredito, que segue sendo a ultima linha.
390
+ if (localizou && !linhas.some((l) => l.includes((0, horario_1.legendaDoFuso)())))
391
+ linhas.push((0, horario_1.legendaDoFuso)());
233
392
  const falhas = checks.filter((c) => c.nivel === 'fail').length;
234
393
  const avisos = checks.filter((c) => c.nivel === 'warn').length;
235
394
  linhas.push('');
@@ -239,8 +398,8 @@ function relatorio(checks) {
239
398
  return linhas.join('\n');
240
399
  }
241
400
  /** Executa o doctor e devolve o codigo de saida (0 = pronto). */
242
- function doctor(dirInicial = process.cwd()) {
243
- const checks = checar(dirInicial);
401
+ function doctor(dirInicial = process.cwd(), nomesHerdados = (0, runtime_ambiente_1.nomesDeProviderAtivos)()) {
402
+ const checks = checar(dirInicial, nomesHerdados);
244
403
  const falhas = checks.filter((c) => c.nivel === 'fail').length;
245
404
  return { texto: relatorio(checks), codigo: falhas > 0 ? 1 : 0 };
246
405
  }
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mergeDaEntrega = mergeDaEntrega;
4
+ exports.registrarEntregaPorPr = registrarEntregaPorPr;
5
+ exports.registrarEntregasPorPr = registrarEntregasPorPr;
6
+ /**
7
+ * I-57 (RM-008, fatia 2): a entrega feita por PR tambem prova entrega para o MASTER.
8
+ *
9
+ * O fluxo de entrega desta fabrica e o PR: CI independente verde no SHA exato, merge com
10
+ * `--match-head-commit` e o assunto `ship(<thread>): ...`. So que o MASTER exigia o `ship_done`
11
+ * do `ork ship`, e a thread entregue por PR ficava aberta para sempre: nao fechava, nao virava
12
+ * licao para o loop de aprendizado, e o board a mostrava como trabalho em curso.
13
+ *
14
+ * Aqui a mesma prova vira `ship_done`, com o que a sustenta de verdade: o merge dentro da base
15
+ * remota (ancestralidade conferida contra a ponta que o `ls-remote` devolve) e o check do CI no
16
+ * head do PR. Sem CI verde, nao registra: merge sem prova independente nao e entrega.
17
+ */
18
+ const ci_1 = require("./ci");
19
+ const fabrica_publicar_1 = require("./fabrica-publicar");
20
+ const ledger_1 = require("./ledger");
21
+ const modos_1 = require("./modos");
22
+ const thread_1 = require("./thread");
23
+ const util_1 = require("./util");
24
+ /** O merge `ship(<thread>)` mais recente na base remota, com o head do PR (segundo pai). */
25
+ function mergeDaEntrega(raiz, thread, remoto, base) {
26
+ const r = (0, util_1.exec)('git', ['log', `refs/remotes/${remoto}/${base}`, '--first-parent', '--format=%H%x09%P%x09%s', `--grep=^ship(${thread})`], raiz);
27
+ if (!r.ok)
28
+ return null;
29
+ for (const linha of r.stdout.split('\n')) {
30
+ const [sha, pais, assunto] = linha.split('\t');
31
+ if (!sha || !assunto || !assunto.startsWith(`ship(${thread})`))
32
+ continue;
33
+ const segundo = (pais ?? '').split(' ')[1] ?? null;
34
+ return { mergeSha: sha, headSha: segundo };
35
+ }
36
+ return null;
37
+ }
38
+ function resultado(thread, acao, motivo, extra = {}) {
39
+ return { thread, acao, motivo, mergeSha: null, headSha: null, ci: null, ...extra };
40
+ }
41
+ /**
42
+ * `ork ship registrar-pr <thread>`: grava o `ship_done` da entrega feita por PR. Idempotente:
43
+ * a mesma entrega nao e registrada duas vezes.
44
+ */
45
+ function registrarEntregaPorPr(carregado, threadId, opcoes = {}) {
46
+ const raiz = carregado.raiz;
47
+ const remoto = opcoes.remoto ?? 'origin';
48
+ const base = carregado.manifesto.worktree.base_branch;
49
+ const thread = (0, thread_1.lerThread)(raiz, threadId);
50
+ if (thread.status === 'fechada')
51
+ return resultado(threadId, 'ja-registrada', 'thread ja fechada');
52
+ if (opcoes.buscar !== false)
53
+ (0, util_1.exec)('git', ['fetch', '-q', remoto, `+refs/heads/${base}:refs/remotes/${remoto}/${base}`], raiz);
54
+ const merge = mergeDaEntrega(raiz, threadId, remoto, base);
55
+ if (!merge)
56
+ return resultado(threadId, 'sem-merge', `nenhum merge ship(${threadId}) em ${remoto}/${base}`);
57
+ const dir = (0, thread_1.dirThread)(raiz, threadId);
58
+ const jaTem = (0, ledger_1.lerLedger)(dir).some((e) => e.tipo === ledger_1.TIPOS_DE_EVENTO.shipConcluido && e.mergeSha === merge.mergeSha);
59
+ if (jaTem)
60
+ return resultado(threadId, 'ja-registrada', 'esta entrega ja tem ship_done', merge);
61
+ // A ponta do remoto AGORA, e o merge dentro dela: e o que torna o push provado, nao presumido.
62
+ const remotoAgora = (0, util_1.exec)('git', ['ls-remote', remoto, `refs/heads/${base}`], raiz);
63
+ const ponta = remotoAgora.ok ? remotoAgora.stdout.split('\t')[0].trim() : '';
64
+ if (!ponta || !(0, util_1.exec)('git', ['merge-base', '--is-ancestor', merge.mergeSha, ponta], raiz).ok) {
65
+ return resultado(threadId, 'recusada', `o merge ${merge.mergeSha.slice(0, 7)} nao esta na ponta de ${remoto}/${base}`, merge);
66
+ }
67
+ const ci = (0, ci_1.consultarCi)(carregado, merge.headSha ?? merge.mergeSha, remoto, opcoes.executorCi);
68
+ if (ci.required && !ci.ok) {
69
+ return resultado(threadId, 'recusada', `sem CI verde no head do PR: ${ci.detail}`, { ...merge, ci });
70
+ }
71
+ (0, ledger_1.registrar)(dir, threadId, ledger_1.TIPOS_DE_EVENTO.shipConcluido, {
72
+ de: thread.base?.branch ?? null,
73
+ para: base,
74
+ shaDe: merge.headSha,
75
+ mergeSha: merge.mergeSha,
76
+ jaIncorporado: true,
77
+ remoto,
78
+ shaRemoto: ponta,
79
+ pushVerificado: true,
80
+ fonteDaProva: `git merge-base --is-ancestor ${merge.mergeSha} ${ponta} (ls-remote ${remoto} refs/heads/${base})`,
81
+ autorizadoPor: 'merge de PR com o CI independente verde no SHA exato',
82
+ tipoDeAutorizacao: 'pr',
83
+ modo: thread.modo,
84
+ tag: (0, modos_1.tagDoModo)(thread.modo),
85
+ evidencia: { viaPr: true, ci: { state: ci.state, context: ci.context, url: ci.url, sha: ci.sha } },
86
+ });
87
+ // Como no `ork ship`: a thread chega ao SHIP quando o ciclo dela tem SHIP.
88
+ const atualizada = (0, thread_1.lerThread)(raiz, threadId);
89
+ if (atualizada.fases?.includes('SHIP')) {
90
+ atualizada.faseAtual = 'SHIP';
91
+ (0, thread_1.gravarThread)(raiz, atualizada);
92
+ }
93
+ if (opcoes.publicar !== false)
94
+ (0, fabrica_publicar_1.publicarEmSegundoPlano)(raiz);
95
+ return resultado(threadId, 'registrou', `ship_done pelo merge ${merge.mergeSha.slice(0, 7)} do PR`, { ...merge, ci });
96
+ }
97
+ /** `ork ship registrar-pr --todas`: toda thread aberta que ja tem merge `ship(<thread>)` na base. */
98
+ function registrarEntregasPorPr(carregado, opcoes = {}) {
99
+ const remoto = opcoes.remoto ?? 'origin';
100
+ const base = carregado.manifesto.worktree.base_branch;
101
+ (0, util_1.exec)('git', ['fetch', '-q', remoto, `+refs/heads/${base}:refs/remotes/${remoto}/${base}`], carregado.raiz);
102
+ const r = (0, thread_1.listarIds)(carregado.raiz)
103
+ .filter((id) => (0, thread_1.lerThread)(carregado.raiz, id).status !== 'fechada')
104
+ .filter((id) => mergeDaEntrega(carregado.raiz, id, remoto, base) !== null)
105
+ .map((id) => {
106
+ try {
107
+ return registrarEntregaPorPr(carregado, id, { ...opcoes, buscar: false, publicar: false });
108
+ }
109
+ catch (e) {
110
+ return resultado(id, 'recusada', e.message);
111
+ }
112
+ });
113
+ // Uma publicacao so para o lote inteiro, nao uma por thread.
114
+ if (r.some((x) => x.acao === 'registrou'))
115
+ (0, fabrica_publicar_1.publicarEmSegundoPlano)(carregado.raiz);
116
+ return r;
117
+ }
@@ -0,0 +1,71 @@
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.THREADS_PROTEGIDAS = void 0;
37
+ exports.validarThreadDeEscrita = validarThreadDeEscrita;
38
+ exports.exigirEscopoDeEscrita = exigirEscopoDeEscrita;
39
+ exports.validarDiretorioDeThread = validarDiretorioDeThread;
40
+ const fs = __importStar(require("node:fs"));
41
+ const path = __importStar(require("node:path"));
42
+ const estado_thread_1 = require("./estado-thread");
43
+ /** Autorizar leitura de um tenant nao autoriza escrever em suas threads. */
44
+ exports.THREADS_PROTEGIDAS = new Set([
45
+ 'ork-grandeevoluc', 'ork-jornadasdpa', 'ork-renarrativac',
46
+ ]);
47
+ function validarThreadDeEscrita(id) {
48
+ if (typeof id !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(id)) {
49
+ throw new Error('scope.thread.invalid');
50
+ }
51
+ if (exports.THREADS_PROTEGIDAS.has(id.toLowerCase()))
52
+ throw new Error('scope.thread.protected');
53
+ }
54
+ function exigirEscopoDeEscrita(threads) {
55
+ if (!Array.isArray(threads) || threads.length === 0)
56
+ throw new Error('scope.write.required');
57
+ threads.forEach(validarThreadDeEscrita);
58
+ return new Set(threads);
59
+ }
60
+ /** Um nome autorizado nao pode ser alias de outra thread ou de estado externo. */
61
+ function validarDiretorioDeThread(raiz, id) {
62
+ validarThreadDeEscrita(id);
63
+ const dir = (0, estado_thread_1.estadoCanonico)(raiz, id);
64
+ for (const candidato of [path.dirname(path.dirname(dir)), path.dirname(dir), dir]) {
65
+ const stat = fs.lstatSync(candidato, { throwIfNoEntry: false });
66
+ if (!stat)
67
+ throw new Error('scope.thread.missing');
68
+ if (stat.isSymbolicLink() || !stat.isDirectory())
69
+ throw new Error('scope.thread.alias');
70
+ }
71
+ }