@orkastery/cli 0.4.2 → 0.5.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 (128) hide show
  1. package/adapters/README.md +11 -0
  2. package/adapters/claude-code/.claude-plugin/plugin.json +3 -1
  3. package/adapters/claude-code/agents/ork-check.md +1 -1
  4. package/adapters/claude-code/agents/ork-go.md +2 -2
  5. package/adapters/claude-code/agents/ork-goal.md +1 -1
  6. package/adapters/claude-code/agents/ork-master.md +1 -1
  7. package/adapters/claude-code/agents/ork-plan.md +1 -1
  8. package/adapters/claude-code/agents/ork-ship.md +1 -1
  9. package/adapters/claude-code/commands/master.md +11 -8
  10. package/adapters/claude-code/commands/ork.md +8 -0
  11. package/adapters/claude-code/commands/ship.md +2 -2
  12. package/adapters/codex/skills/ork/SKILL.md +12 -1
  13. package/adapters/hermes/README.md +3 -0
  14. package/adapters/hermes/bin/ork-abrir-thread.sh +17 -2
  15. package/adapters/hermes/bin/ork-brain.sh +3 -0
  16. package/adapters/hermes/bin/ork-maestro.sh +4 -1
  17. package/adapters/hermes/bin/ork-network-roadmap.sh +11 -0
  18. package/adapters/hermes/bin/ork-roadmap-status.sh +9 -0
  19. package/adapters/hermes/hermes.plugin.json +2 -0
  20. package/adapters/hermes/hitl-ingress/__init__.py +36 -3
  21. package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +19 -20
  22. package/adapters/openclaw/README.md +34 -3
  23. package/adapters/openclaw/dist/hitl-ingress.js +37 -3
  24. package/adapters/openclaw/dist/index.js +92 -39
  25. package/adapters/openclaw/openclaw.plugin.json +10 -3
  26. package/adapters/openclaw/package.json +2 -2
  27. package/adapters/openclaw/src/hitl-ingress.ts +32 -3
  28. package/adapters/openclaw/src/index.ts +108 -41
  29. package/assets/docs/padroes/documentacao-de-produto.md +2 -2
  30. package/assets/docs/padroes/roadmap-de-produto.md +1 -1
  31. package/assets/docs/produto/_modelo-feature.md +2 -0
  32. package/assets/orkmind_bridge.py +214 -1
  33. package/dist/adapters/claude-bg.js +88 -9
  34. package/dist/adapters/codex-events.js +3 -1
  35. package/dist/board.js +74 -0
  36. package/dist/branch-de-estado.js +5 -4
  37. package/dist/busca-semantica.js +161 -0
  38. package/dist/ci.js +141 -10
  39. package/dist/company-brain-cli.js +27 -2
  40. package/dist/company-brain-context.js +132 -0
  41. package/dist/company-brain-dossie.js +376 -0
  42. package/dist/company-brain-mcp.js +4 -1
  43. package/dist/conducao.js +117 -31
  44. package/dist/decisao-autonoma.js +42 -0
  45. package/dist/docs.js +64 -22
  46. package/dist/doctor.js +33 -0
  47. package/dist/entrega-pr.js +109 -5
  48. package/dist/experiencia-instalacao.js +256 -0
  49. package/dist/experiencia.js +72 -0
  50. package/dist/fabrica-estado.js +43 -10
  51. package/dist/fechamento.js +105 -0
  52. package/dist/fix.js +1 -1
  53. package/dist/forja.js +435 -0
  54. package/dist/gates.js +10 -1
  55. package/dist/hitl-classificacao.js +11 -1
  56. package/dist/hitl-contract.js +50 -7
  57. package/dist/hitl-curto.js +158 -0
  58. package/dist/hitl-gates.js +67 -1
  59. package/dist/hitl-lote.js +30 -15
  60. package/dist/hitl-presentation.js +10 -7
  61. package/dist/hitl-resumo.js +33 -12
  62. package/dist/hitl-texto-livre.js +150 -0
  63. package/dist/hosts.js +86 -1
  64. package/dist/index.js +605 -41
  65. package/dist/indice-vetorial.js +317 -0
  66. package/dist/init.js +13 -0
  67. package/dist/integracoes-locais.js +1 -1
  68. package/dist/intelligence-benchmark-contract.js +570 -0
  69. package/dist/intelligence-graph-cli.js +430 -0
  70. package/dist/intelligence-graph-contract.js +577 -0
  71. package/dist/intelligence-graph-extract-md.js +469 -0
  72. package/dist/intelligence-graph-extract-ts.js +810 -0
  73. package/dist/intelligence-graph-extract.js +369 -0
  74. package/dist/intelligence-graph-index.js +512 -0
  75. package/dist/intelligence-graph-parsers.js +327 -0
  76. package/dist/intelligence-graph-query.js +344 -0
  77. package/dist/intelligence-graph-repo.js +187 -0
  78. package/dist/leases.js +84 -0
  79. package/dist/ledger.js +14 -0
  80. package/dist/maestro-cli.js +10 -1
  81. package/dist/maestro-contract.js +5 -1
  82. package/dist/maestro-discovery.js +16 -2
  83. package/dist/maestro-snapshot.js +6 -2
  84. package/dist/manifest.js +142 -5
  85. package/dist/master-batch.js +3 -2
  86. package/dist/master-digest.js +2 -2
  87. package/dist/master-nota.js +203 -0
  88. package/dist/master.js +4 -0
  89. package/dist/mcp-experiencia.js +164 -0
  90. package/dist/mcp-git.js +36 -10
  91. package/dist/mcp-install.js +3 -1
  92. package/dist/mcp-maestro.js +3 -2
  93. package/dist/mcp-server.js +84 -5
  94. package/dist/memoria.js +147 -1
  95. package/dist/network-roadmap.js +907 -0
  96. package/dist/ocupacao.js +38 -2
  97. package/dist/onboarding.js +113 -7
  98. package/dist/orkmind.js +165 -9
  99. package/dist/phase.js +166 -13
  100. package/dist/policies.js +2 -0
  101. package/dist/projeto-alvo.js +551 -0
  102. package/dist/prompts.js +1 -1
  103. package/dist/pulse-consentimento.js +4 -2
  104. package/dist/pulse-delivery.js +5 -0
  105. package/dist/pulse-resposta.js +674 -49
  106. package/dist/pulse.js +21 -0
  107. package/dist/ratelimit.js +3 -2
  108. package/dist/retry.js +230 -9
  109. package/dist/roadmap-reservas.js +262 -20
  110. package/dist/roadmap-status.js +213 -0
  111. package/dist/runtime-ambiente.js +12 -1
  112. package/dist/session-watcher-claude.js +26 -0
  113. package/dist/sessoes.js +49 -15
  114. package/dist/thread-close.js +3 -0
  115. package/dist/verify.js +5 -4
  116. package/dist/worktree.js +77 -0
  117. package/eval/casos/orchestration-experience-pt-br.json +48 -0
  118. package/eval/casos/orchestration-experience.json +48 -0
  119. package/package.json +1 -1
  120. package/schemas/code-artifact-graph.v1.schema.json +581 -0
  121. package/schemas/graph-benchmark.v1.schema.json +2240 -0
  122. package/schemas/maestro-snapshot.schema.json +23 -0
  123. package/skills/README.md +7 -5
  124. package/skills/core/onboarding/SKILL.md +7 -0
  125. package/skills/core/orchestration-experience/SKILL.md +113 -0
  126. package/skills/core/orchestration-experience-pt-br/SKILL.md +112 -0
  127. package/skills/core/orkastery-bootstrap/SKILL.md +2 -2
  128. package/skills/phases/go-implementation/SKILL.md +2 -0
@@ -76,6 +76,9 @@ async function rodarOrk(args, signal, entrada) {
76
76
  env.ORK_RECEIPT_VERIFIERS = verifiers;
77
77
  // I-36 (D6): o adaptador declara o proprio canal de conducao. Descreve a porta, nao concede autoridade.
78
78
  env.ORK_CANAL = 'openclaw';
79
+ // RM-052 (D3): o cwd do gateway nao e projeto de ninguem. Sem `projeto` pedido, o nucleo usa o unico
80
+ // projeto conhecido da maquina ou devolve a escolha; nunca o manifesto que estiver no cwd do gateway.
81
+ env.ORK_PROJETO_EXPLICITO = '1';
79
82
  return new Promise((resolve) => {
80
83
  const filho = execFile(bin, args, { maxBuffer: MAX_BUFFER, signal, env }, (erro, stdout, stderr) => {
81
84
  const partes = [stdout, stderr].map((t) => (t ?? '').trim()).filter((t) => t !== '');
@@ -156,10 +159,36 @@ function argvDaResposta(p, alvo) {
156
159
  return [alvo, 'answer', thread, pedido, '--resposta-stdin', '--por', `telegram:${usuario}`,
157
160
  '--mensagem', `telegram:${chat}:${referencia}`, '--origem', 'telegram'];
158
161
  }
162
+ /**
163
+ * RM-052 (D4): o projeto pedido chega como NOME de projeto registrado (`ork projetos`), nunca como
164
+ * caminho. O modelo escolhe entre os projetos da maquina; nao aponta o nucleo para um diretorio.
165
+ */
166
+ const PADRAO_DO_PROJETO = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/;
167
+ const PARAMETRO_PROJETO = {
168
+ type: 'string',
169
+ pattern: PADRAO_DO_PROJETO.source,
170
+ description: 'Nome do projeto que o dono pediu (ex.: orkastery), como em `ork projetos`. Sem ele e com mais de um projeto na máquina, a resposta é a escolha; nunca use o diretório do gateway.',
171
+ };
172
+ const PROJETO_DA_RM052 = {
173
+ padrao: PADRAO_DO_PROJETO,
174
+ descricao: PARAMETRO_PROJETO.description,
175
+ recusa: 'informe o NOME de um projeto de `ork projetos` (ex.: orkastery), nunca um caminho',
176
+ };
177
+ /**
178
+ * RM-054 (fatia 2, D-G4): no roadmap da rede, `projeto` tambem aceita a forja (`github:dono/repo`,
179
+ * `gitlab:grupo/repo`): numa maquina sem clone, e o unico caminho ate o projeto enquanto a rede por
180
+ * pessoa (RM-053) nao chega. Caminho e URL continuam fora (a URL pode levar credencial), e o nucleo
181
+ * confere de novo, porque o adaptador declara ORK_PROJETO_EXPLICITO=1.
182
+ */
183
+ const PROJETO_DA_REDE = {
184
+ padrao: /^(?:[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}|(?:github|gitlab):[A-Za-z0-9][A-Za-z0-9._-]{0,99}(?:\/[A-Za-z0-9][A-Za-z0-9._-]{0,99}){1,8})$/,
185
+ descricao: 'Projeto que o dono pediu: o nome, como em `ork projetos` (ex.: orkastery), ou a forja (github:dono/repo, gitlab:grupo/repo) quando não há clone nesta máquina. Sem ele, vêm todos os projetos conhecidos. Nunca caminho nem URL.',
186
+ recusa: 'informe o NOME de um projeto de `ork projetos` (ex.: orkastery) ou github:dono/repo, nunca caminho nem URL',
187
+ };
159
188
  const FERRAMENTAS = [
160
189
  {
161
190
  name: 'ork_maestro',
162
- description: 'Ao receber a frase exata orkastery maestro, consulte o panorama somente leitura. Não cria thread. Apresente fontes, lacunas e HITL com recomendação e opções claras; horários para o dono vêm dos campos *Local (fuso do dono), nunca do ISO. Ação posterior exige operação autorizada e readback.',
191
+ description: 'Ao receber a frase exata orkastery maestro: sem projeto nomeado, chame ork_network_roadmap sem projeto (o panorama da rede, com fontes, frescor e lacunas) e apresente-o como vem; com projeto nomeado, consulte este panorama somente leitura com projeto. Não cria thread. Apresente fontes, lacunas e HITL com recomendação e opções claras; horários para o dono vêm dos campos *Local (fuso do dono), nunca do ISO. O panorama NÃO lê o roadmap, as reservas nem as outras máquinas (veja notConsulted): zero threads nunca é roadmap vazio; para o roadmap use ork_network_roadmap. Ação posterior exige operação autorizada e readback.',
163
192
  parameters: { type: 'object', additionalProperties: false, properties: {
164
193
  thread: { type: 'string' }, section: { type: 'string', enum: ['portfolio', 'demands', 'threads', 'sessions', 'blockers', 'leases', 'retries', 'hitl', 'ship', 'master', 'nextActions'] },
165
194
  offset: { type: 'integer', minimum: 0, maximum: 100000 },
@@ -254,25 +283,6 @@ const FERRAMENTAS = [
254
283
  parameters: schema({ thread: { type: 'string' } }),
255
284
  argv: (p) => ['phase', 'list', texto(p, 'thread')],
256
285
  },
257
- {
258
- name: 'ork_objective_status',
259
- description: 'Aposentado na I-43 junto com ork objective: devolve a recusa tipada objective.aposentado, com saida != 0. Estado de uma entrega: ork_thread_status. Ciclos ligados a um produto, projeto ou iniciativa: ork portfolio inspect <id> --json.',
260
- parameters: schema({ objective: { type: 'string', description: 'ID obj-* do ticket' } }),
261
- argv: (p) => ['objective', 'status', texto(p, 'objective'), '--json'],
262
- },
263
- {
264
- name: 'ork_objective_message',
265
- description: 'Aposentado na I-43 junto com ork objective: devolve a recusa tipada objective.aposentado, com saida != 0, e nao grava nada. Resposta do dono a uma pausa vai por ork_gate_answer ou ork_session_answer.',
266
- parameters: schema({
267
- objective: { type: 'string', description: 'ID obj-* do ticket' },
268
- author: { type: 'string', description: 'Identidade fornecida pelo gateway autenticado' },
269
- message: { type: 'string', description: 'Texto integral recebido do dono' },
270
- }),
271
- argv: (p) => [
272
- 'objective', 'message', texto(p, 'objective'),
273
- '--text', texto(p, 'message'), '--por', texto(p, 'author'), '--role', 'human',
274
- ],
275
- },
276
286
  {
277
287
  name: 'ork_portfolio_list',
278
288
  description: 'Lista produtos, projetos e iniciativas do catálogo canônico usado pelo Kanban.',
@@ -300,6 +310,27 @@ const FERRAMENTAS = [
300
310
  parameters: schema({ thread: { type: 'string' }, id: { type: 'string' } }),
301
311
  argv: (p) => ['brain', 'get', texto(p, 'id'), '--thread', texto(p, 'thread')],
302
312
  },
313
+ {
314
+ name: 'ork_brain_context',
315
+ description: 'Pacote de contexto citável do Company Brain: entidades pedidas e seus pais, cada uma com a citação da fonte, o frescor contra o portfólio e as lacunas. Só leitura.',
316
+ parameters: schema({
317
+ thread: { type: 'string' },
318
+ ids: { type: 'string', description: 'ids do portfólio separados por vírgula (prod-, proj-, init-)' },
319
+ }),
320
+ argv: (p) => ['brain', 'context', '--thread', texto(p, 'thread'), '--ids', texto(p, 'ids')],
321
+ },
322
+ {
323
+ name: 'ork_brain_dossie',
324
+ description: 'Dossiê de decisão da thread: vínculo com objetivo e projeto, contexto citável, alternativas, decisão, quem decidiu e evidência, com os ids do Company Brain e as lacunas. Resposta do dono só com recibo conferido. Só leitura.',
325
+ parameters: {
326
+ type: 'object', additionalProperties: false, required: ['thread'],
327
+ properties: {
328
+ thread: { type: 'string' },
329
+ decisao: { type: 'string', description: 'id do pedido ou fact- da decisão; omita para a thread inteira' },
330
+ },
331
+ },
332
+ argv: (p) => ['brain', 'dossie', '--thread', texto(p, 'thread'), ...(p.decisao === undefined ? [] : ['--decisao', texto(p, 'decisao')])],
333
+ },
303
334
  {
304
335
  name: 'ork_claims_add',
305
336
  description: 'Registra uma alegacao verificavel com o comando que a comprova. Toda citacao de arquivo ou teste vira claim.',
@@ -377,30 +408,32 @@ const FERRAMENTAS = [
377
408
  },
378
409
  {
379
410
  name: 'ork_master',
380
- description: 'Fecha a thread: POSTMORTEM tipado, MASTER log no contrato congelado e o score HUMANO de 0 a 5 com justificativa. Nunca invente a nota.',
411
+ description: 'Pede ao dono a nota do MASTER (0 a 5, com o porque) de uma entrega: devolve a linha com o codigo curto para mostrar a ele. A nota volta pelo Telegram, com a prova do ingresso. Esta tool nunca grava nota e nao recebe nome de pessoa (RM-048).',
381
412
  parameters: schema({
382
413
  thread: { type: 'string' },
383
- score: { type: 'string', description: 'inteiro de 0 a 5, dado pelo humano' },
384
- justificativa: { type: 'string' },
385
- quem: { type: 'string' },
386
414
  }),
387
- argv: (p) => [
388
- 'master',
389
- texto(p, 'thread'),
390
- '--score',
391
- texto(p, 'score'),
392
- '--justificativa',
393
- texto(p, 'justificativa'),
394
- '--por',
395
- texto(p, 'quem'),
396
- ],
415
+ argv: (p) => ['master', 'pedir', texto(p, 'thread'), '--formato', 'telegram'],
397
416
  },
398
417
  {
399
418
  name: 'ork_board',
400
- description: 'Todas as threads em uma visao, e o escalonador dizendo quem avanca agora e quem espera.',
419
+ description: 'As threads DESTE projeto nesta maquina e o escalonador dizendo quem avanca agora e quem espera. NAO le o roadmap: nunca conclua sobre o roadmap a partir do board (zero threads nao e roadmap vazio); para o roadmap use ork_network_roadmap. O cabecalho diz o projeto consultado e o que nao foi lido.',
401
420
  parameters: schema({}),
402
421
  argv: () => ['board', 'plan'],
403
422
  },
423
+ {
424
+ name: 'ork_roadmap_status',
425
+ description: 'Status report do roadmap SO desta maquina (o checkout local), no formato aprovado pelo dono (grupos com icones, #HITL e o fecho): nao le as reservas nem as outras maquinas, e o cabecalho diz isso. Para o status do roadmap com as threads de todas as maquinas, as reservas, as fontes e as lacunas, use ork_network_roadmap. Somente leitura: transporte o texto como vem, sem reescrever. Passe projeto com o nome que o dono pediu (ex.: orkastery); sem ele e com mais de um projeto na maquina, a resposta e a escolha. Nunca deduza o roadmap de ork_board ou ork_maestro.',
426
+ parameters: schema({}),
427
+ argv: () => ['roadmap', 'status'],
428
+ },
429
+ {
430
+ name: 'ork_network_roadmap',
431
+ description: 'Roadmap da rede, somente leitura: para cada projeto, o status report do roadmap (RM-048) com as threads de TODAS as máquinas, as reservas, as threads por máquina com a idade da batida, a fonte e a hora de cada parte e as lacunas. É a fonte para qualquer pergunta sobre o roadmap ou o status report: transporte o texto como vem, sem reescrever nem resumir. Passe projeto com o nome que o dono pediu (ex.: orkastery) ou github:dono/repo; sem projeto, vêm todos os projetos conhecidos, e esse é o panorama da frase orkastery maestro sem projeto. Lacuna, "não lido" e "Não consultado" são fontes que ficaram sem leitura: nunca conclua "roadmap vazio" nem "nenhuma máquina publicou" a partir delas.',
432
+ parameters: schema({}),
433
+ projeto: PROJETO_DA_REDE,
434
+ // RM-054 (fatia 2): uma chamada de CLI; o `--projeto` vai no inicio e o nucleo o devolve ao `network`.
435
+ argv: () => ['network', 'roadmap'],
436
+ },
404
437
  {
405
438
  name: 'ork_master_batch',
406
439
  description: 'Todas as entregas em JSON, com o indice derivado do ledger, as ja pontuadas e as aceitas por omissao. Roda ork master --todas; o nome e da antiga fila de score (ork master --batch), aposentada na I-43.',
@@ -408,21 +441,41 @@ const FERRAMENTAS = [
408
441
  argv: () => ['master', '--todas', '--json'],
409
442
  },
410
443
  ];
444
+ /** RM-052: toda tool aceita `projeto` opcional, sem mudar o que ela ja exigia. */
445
+ function comProjeto(f) {
446
+ const propriedades = (f.parameters.properties ?? {});
447
+ const p = f.projeto;
448
+ const projeto = p ? { type: 'string', pattern: p.padrao.source, description: p.descricao } : PARAMETRO_PROJETO;
449
+ return { ...f.parameters, properties: { ...propriedades, projeto } };
450
+ }
451
+ /** `--projeto <nome>` vai no inicio do argv; o resto dos parametros segue para a tool como antes. */
452
+ function argvComProjeto(f, params) {
453
+ const { projeto, ...resto } = params;
454
+ if (projeto !== undefined && (typeof projeto !== 'string' || !(f.projeto ?? PROJETO_DA_RM052).padrao.test(projeto))) {
455
+ throw new Error('projeto.invalido');
456
+ }
457
+ return { argv: [...(projeto === undefined ? [] : ['--projeto', projeto]), ...f.argv(resto)], resto };
458
+ }
411
459
  const plugin = defineToolPlugin({
412
460
  id: 'orkastery',
413
461
  name: 'Orkastery',
414
- description: 'Conducao de looping threads em 6 fases pelo nucleo `ork`, exposta ao OpenClaw como tools `ork_*`. Zero regra de negocio no host: cada tool e uma chamada de CLI.',
462
+ description: 'Conducao de looping threads em 6 fases pelo nucleo `ork`, exposta ao OpenClaw como tools `ork_*`. Zero regra de negocio no host: cada tool e uma chamada de CLI. Toda tool aceita projeto (nome do projeto pedido); o cwd do gateway nunca escolhe o projeto. O status do roadmap vem de ork_network_roadmap.',
415
463
  tools: (tool) => FERRAMENTAS.map((f) => tool({
416
464
  name: f.name,
417
465
  description: f.description,
418
- parameters: f.parameters,
466
+ parameters: comProjeto(f),
419
467
  execute: (params, _config, contexto) => {
468
+ let chamada;
420
469
  try {
421
- return rodarOrk(f.argv(params), contexto.signal, f.entrada?.(params));
470
+ chamada = argvComProjeto(f, params);
422
471
  }
423
- catch {
472
+ catch (e) {
473
+ if (e.message === 'projeto.invalido') {
474
+ return `[ork recusou] projeto.invalido: ${(f.projeto ?? PROJETO_DA_RM052).recusa}`;
475
+ }
424
476
  return '[ork recusou] resposta humana não confirmada; confira origem e correlação do pedido';
425
477
  }
478
+ return rodarOrk(chamada.argv, contexto.signal, f.entrada?.(chamada.resto));
426
479
  },
427
480
  })),
428
481
  });
@@ -2,7 +2,7 @@
2
2
  "id": "orkastery",
3
3
  "name": "Orkastery",
4
4
  "description": "Conducao de looping threads em 6 fases pelo nucleo `ork`, exposta ao OpenClaw como tools `ork_*`. Zero regra de negocio no host: cada tool e uma chamada de CLI.",
5
- "version": "0.4.2",
5
+ "version": "0.5.0",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "properties": {},
@@ -21,12 +21,12 @@
21
21
  "ork_thread_status",
22
22
  "ork_phase_run",
23
23
  "ork_phase_list",
24
- "ork_objective_status",
25
- "ork_objective_message",
26
24
  "ork_portfolio_list",
27
25
  "ork_brain_status",
28
26
  "ork_brain_query",
29
27
  "ork_brain_get",
28
+ "ork_brain_context",
29
+ "ork_brain_dossie",
30
30
  "ork_claims_add",
31
31
  "ork_verify",
32
32
  "ork_verify_baseline",
@@ -37,7 +37,14 @@
37
37
  "ork_ship",
38
38
  "ork_master",
39
39
  "ork_board",
40
+ "ork_roadmap_status",
41
+ "ork_network_roadmap",
40
42
  "ork_master_batch"
41
43
  ]
44
+ },
45
+ "toolMetadata": {
46
+ "ork_network_roadmap": {
47
+ "profiles": ["coding", "messaging"]
48
+ }
42
49
  }
43
50
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@orkastery/openclaw-plugin",
3
- "version": "0.4.2",
4
- "description": "Plugin Orkastery para OpenClaw: 25 tools ork_* que traduzem intencao em chamada de CLI do nucleo `ork`. Zero regra de negocio no host.",
3
+ "version": "0.5.0",
4
+ "description": "Plugin Orkastery para OpenClaw: 27 tools ork_* que traduzem intencao em chamada de CLI do nucleo `ork`. Zero regra de negocio no host.",
5
5
  "type": "module",
6
6
  "private": true,
7
7
  "license": "MIT",
@@ -2,6 +2,8 @@
2
2
  /** D12: this host is the `openclaw` channel, and the channel is signed, not labelled. */
3
3
  import { createHash, createHmac } from 'node:crypto';
4
4
  import { execFile } from 'node:child_process';
5
+ import { readFileSync } from 'node:fs';
6
+ import { isAbsolute, join } from 'node:path';
5
7
 
6
8
  interface NativeEvent {
7
9
  content: string; channel: string; commandAuthorized?: boolean; accountId?: string;
@@ -18,14 +20,41 @@ interface NativeApi { on: (name: 'inbound_claim', handler: (event: NativeEvent,
18
20
  * stay equal. The code starts with a letter and has a digit: ordinary chat never has this shape,
19
21
  * and it never reads as an answer to the batch, which starts with the question number.
20
22
  */
21
- const PULSE_CONSENT = new RegExp("^[ \\t]*(?=[A-HJKMNP-TV-Z][2-9A-HJKMNP-TV-Z]{0,2}[2-9])[A-HJKMNP-TV-Z][2-9A-HJKMNP-TV-Z]{3}[ \\t]+[^\\r\\n]{1,40}$", 'i');
23
+ const PULSE_CONSENT = new RegExp("^[ \\t]*(?=[A-HJKMNP-TV-Z][2-9A-HJKMNP-TV-Z]{0,2}[2-9])[A-HJKMNP-TV-Z][2-9A-HJKMNP-TV-Z]{3}[ \\t]+[^\\r\\n]{1,200}$", 'i');
22
24
  const PULSE_LOTE = new RegExp("^[ \\t]*[0-9]{1,2}[ \\t]*[a-zA-Z](?:[ \\t,;]*[0-9]{1,2}[ \\t]*[a-zA-Z])*[ \\t]*[.!]?[ \\t]*$");
23
25
  /** I-50 (RM-039): the pulse cadence tag, alone in the message. It starts with '#', so it never reads as the other two. */
24
26
  const PULSE_CADENCIA = new RegExp("^[ \\t]*#OrkPulse(?:On(?:-(?:15|30|60)m)?|Off)[ \\t]*[.!]?[ \\t]*$", 'i');
25
27
  /** The pulse address inside the signed body: neither a thread nor a request. The core translates. */
26
28
  const PULSE_ALVO = 'pulse', PULSE_ENDERECO = 'resposta';
27
- export function isPulseAnswer(text: string): boolean {
28
- return !text.includes('\0') && (PULSE_CONSENT.test(text) || PULSE_LOTE.test(text) || PULSE_CADENCIA.test(text));
29
+ /**
30
+ * RM-048 (D2 and D3): the free-text shapes, generated in the core from the closed vocabulary. `lista`
31
+ * ("1. B, 2. aprovo") is always claimed; `livre` ("sim", "aprovo", "a") only while the core's
32
+ * listening window is open: outside it, the word goes to the assistant, as it always did. The
33
+ * window is a routing hint, not proof: the message still goes through the signature and the core rule.
34
+ */
35
+ const PULSE_LISTA = new RegExp("^[ \\t]*[0-9]{1,2}(?:[ \\t]*[.):-])?[ \\t]*(?:[a-d]|pode seguir|pode ir|pode|sim|s|ok|aprovo|aprovado|aprovada|aprovar|aprova|segue|siga|seguir|manda|confirmo|confirmado|de acordo|agora n[aã]o|n[aã]o|n|recuso|recusado|recusar|reprovo|reprovado|revisar|revis[aã]o|refazer|volta|voltar|esperar|espera|aguardar|aguarda|aguarde|depois|mais tarde|detalhes?|evid[eê]ncias?)(?:[ \\t\\r\\n,;]*[0-9]{1,2}(?:[ \\t]*[.):-])?[ \\t]*(?:[a-d]|pode seguir|pode ir|pode|sim|s|ok|aprovo|aprovado|aprovada|aprovar|aprova|segue|siga|seguir|manda|confirmo|confirmado|de acordo|agora n[aã]o|n[aã]o|n|recuso|recusado|recusar|reprovo|reprovado|revisar|revis[aã]o|refazer|volta|voltar|esperar|espera|aguardar|aguarda|aguarde|depois|mais tarde|detalhes?|evid[eê]ncias?))*[ \\t]*(?:[.!][ \\t]*)?$", 'i');
36
+ const PULSE_LIVRE = new RegExp("^[ \\t]*(?:[1-4]|(?:[a-d]|pode seguir|pode ir|pode|sim|s|ok|aprovo|aprovado|aprovada|aprovar|aprova|segue|siga|seguir|manda|confirmo|confirmado|de acordo|agora n[aã]o|n[aã]o|n|recuso|recusado|recusar|reprovo|reprovado|revisar|revis[aã]o|refazer|volta|voltar|esperar|espera|aguardar|aguarda|aguarde|depois|mais tarde|detalhes?|evid[eê]ncias?))[ \\t]*(?:[.!]+[ \\t]*)?$", 'i');
37
+ /** RM-048 (item 8): the digest keyboard line goes to the core at the same signed address. */
38
+ const PULSE_RATIFICACAO = new RegExp("^[ \\t]*ratificar(?:-lote)?(?:[ \\t]+[A-Za-z0-9._-]{1,80}){2,3}[ \\t]*$", 'i');
39
+ /** Text above this is never tested against the shapes: they run before the allowlist. */
40
+ const PULSE_TETO = 300, ESCUTA_TETO = 4096;
41
+
42
+ /** The bare-word window (ork.pulse-escuta/v1), read with a byte cap; any error closes it. */
43
+ export function listeningOpen(root: string | undefined, now = Date.now()): boolean {
44
+ try {
45
+ if (!root || !isAbsolute(root)) return false;
46
+ const raw = readFileSync(join(root, '.orkastery', 'monitor', 'pulse-escuta.json'));
47
+ if (raw.length > ESCUTA_TETO) return false;
48
+ const value = JSON.parse(raw.toString('utf8'));
49
+ return value?.contrato === 'ork.pulse-escuta/v1' && typeof value.livreAte === 'string' && Date.parse(value.livreAte) > now;
50
+ } catch { return false; }
51
+ }
52
+
53
+ export function isPulseAnswer(text: string, root: string | undefined = process.env.ORK_HITL_ROOT): boolean {
54
+ if (text.includes('\0') || text.length > PULSE_TETO) return false;
55
+ if (PULSE_CONSENT.test(text) || PULSE_LOTE.test(text) || PULSE_CADENCIA.test(text) || PULSE_LISTA.test(text) ||
56
+ PULSE_RATIFICACAO.test(text)) return true;
57
+ return PULSE_LIVRE.test(text) && listeningOpen(root);
29
58
  }
30
59
 
31
60
  export function registerHitlIngress(api: NativeApi, bin: string): void {
@@ -71,6 +71,9 @@ async function rodarOrk(args: string[], signal?: AbortSignal, entrada?: string):
71
71
  if (verifiers) env.ORK_RECEIPT_VERIFIERS = verifiers;
72
72
  // I-36 (D6): o adaptador declara o proprio canal de conducao. Descreve a porta, nao concede autoridade.
73
73
  env.ORK_CANAL = 'openclaw';
74
+ // RM-052 (D3): o cwd do gateway nao e projeto de ninguem. Sem `projeto` pedido, o nucleo usa o unico
75
+ // projeto conhecido da maquina ou devolve a escolha; nunca o manifesto que estiver no cwd do gateway.
76
+ env.ORK_PROJETO_EXPLICITO = '1';
74
77
  return new Promise((resolve) => {
75
78
  const filho = execFile(bin, args, { maxBuffer: MAX_BUFFER, signal, env }, (erro, stdout, stderr) => {
76
79
  const partes = [stdout, stderr].map((t) => (t ?? '').trim()).filter((t) => t !== '');
@@ -154,6 +157,38 @@ function argvDaResposta(p: Record<string, unknown>, alvo: 'gate' | 'sessions'):
154
157
  '--mensagem', `telegram:${chat}:${referencia}`, '--origem', 'telegram'];
155
158
  }
156
159
 
160
+ /**
161
+ * RM-052 (D4): o projeto pedido chega como NOME de projeto registrado (`ork projetos`), nunca como
162
+ * caminho. O modelo escolhe entre os projetos da maquina; nao aponta o nucleo para um diretorio.
163
+ */
164
+ const PADRAO_DO_PROJETO = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/;
165
+ const PARAMETRO_PROJETO = {
166
+ type: 'string',
167
+ pattern: PADRAO_DO_PROJETO.source,
168
+ description: 'Nome do projeto que o dono pediu (ex.: orkastery), como em `ork projetos`. Sem ele e com mais de um projeto na máquina, a resposta é a escolha; nunca use o diretório do gateway.',
169
+ };
170
+
171
+ /** O `projeto` de uma tool: o padrao que o host confere, a descricao para o modelo e a recusa. */
172
+ interface ProjetoDaTool { padrao: RegExp; descricao: string; recusa: string }
173
+
174
+ const PROJETO_DA_RM052: ProjetoDaTool = {
175
+ padrao: PADRAO_DO_PROJETO,
176
+ descricao: PARAMETRO_PROJETO.description,
177
+ recusa: 'informe o NOME de um projeto de `ork projetos` (ex.: orkastery), nunca um caminho',
178
+ };
179
+
180
+ /**
181
+ * RM-054 (fatia 2, D-G4): no roadmap da rede, `projeto` tambem aceita a forja (`github:dono/repo`,
182
+ * `gitlab:grupo/repo`): numa maquina sem clone, e o unico caminho ate o projeto enquanto a rede por
183
+ * pessoa (RM-053) nao chega. Caminho e URL continuam fora (a URL pode levar credencial), e o nucleo
184
+ * confere de novo, porque o adaptador declara ORK_PROJETO_EXPLICITO=1.
185
+ */
186
+ const PROJETO_DA_REDE: ProjetoDaTool = {
187
+ padrao: /^(?:[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}|(?:github|gitlab):[A-Za-z0-9][A-Za-z0-9._-]{0,99}(?:\/[A-Za-z0-9][A-Za-z0-9._-]{0,99}){1,8})$/,
188
+ descricao: 'Projeto que o dono pediu: o nome, como em `ork projetos` (ex.: orkastery), ou a forja (github:dono/repo, gitlab:grupo/repo) quando não há clone nesta máquina. Sem ele, vêm todos os projetos conhecidos. Nunca caminho nem URL.',
189
+ recusa: 'informe o NOME de um projeto de `ork projetos` (ex.: orkastery) ou github:dono/repo, nunca caminho nem URL',
190
+ };
191
+
157
192
  /** Catálogo em paridade nome a nome com o manifesto; aprovação genérica legada aposentada. */
158
193
  interface FerramentaOrk {
159
194
  name: string;
@@ -161,12 +196,14 @@ interface FerramentaOrk {
161
196
  parameters: Record<string, unknown>;
162
197
  argv: (params: Record<string, unknown>) => string[];
163
198
  entrada?: (params: Record<string, unknown>) => string;
199
+ /** O `projeto` desta tool, quando nao e o da RM-052 (so nome). */
200
+ projeto?: ProjetoDaTool;
164
201
  }
165
202
 
166
203
  const FERRAMENTAS: FerramentaOrk[] = [
167
204
  {
168
205
  name: 'ork_maestro',
169
- description: 'Ao receber a frase exata orkastery maestro, consulte o panorama somente leitura. Não cria thread. Apresente fontes, lacunas e HITL com recomendação e opções claras; horários para o dono vêm dos campos *Local (fuso do dono), nunca do ISO. Ação posterior exige operação autorizada e readback.',
206
+ description: 'Ao receber a frase exata orkastery maestro: sem projeto nomeado, chame ork_network_roadmap sem projeto (o panorama da rede, com fontes, frescor e lacunas) e apresente-o como vem; com projeto nomeado, consulte este panorama somente leitura com projeto. Não cria thread. Apresente fontes, lacunas e HITL com recomendação e opções claras; horários para o dono vêm dos campos *Local (fuso do dono), nunca do ISO. O panorama NÃO lê o roadmap, as reservas nem as outras máquinas (veja notConsulted): zero threads nunca é roadmap vazio; para o roadmap use ork_network_roadmap. Ação posterior exige operação autorizada e readback.',
170
207
  parameters: { type: 'object', additionalProperties: false, properties: {
171
208
  thread: { type: 'string' }, section: { type: 'string', enum: ['portfolio','demands','threads','sessions','blockers','leases','retries','hitl','ship','master','nextActions'] },
172
209
  offset: { type: 'integer', minimum: 0, maximum: 100000 },
@@ -265,27 +302,6 @@ const FERRAMENTAS: FerramentaOrk[] = [
265
302
  parameters: schema({ thread: { type: 'string' } }),
266
303
  argv: (p) => ['phase', 'list', texto(p, 'thread')],
267
304
  },
268
- {
269
- name: 'ork_objective_status',
270
- description:
271
- 'Aposentado na I-43 junto com ork objective: devolve a recusa tipada objective.aposentado, com saida != 0. Estado de uma entrega: ork_thread_status. Ciclos ligados a um produto, projeto ou iniciativa: ork portfolio inspect <id> --json.',
272
- parameters: schema({ objective: { type: 'string', description: 'ID obj-* do ticket' } }),
273
- argv: (p) => ['objective', 'status', texto(p, 'objective'), '--json'],
274
- },
275
- {
276
- name: 'ork_objective_message',
277
- description:
278
- 'Aposentado na I-43 junto com ork objective: devolve a recusa tipada objective.aposentado, com saida != 0, e nao grava nada. Resposta do dono a uma pausa vai por ork_gate_answer ou ork_session_answer.',
279
- parameters: schema({
280
- objective: { type: 'string', description: 'ID obj-* do ticket' },
281
- author: { type: 'string', description: 'Identidade fornecida pelo gateway autenticado' },
282
- message: { type: 'string', description: 'Texto integral recebido do dono' },
283
- }),
284
- argv: (p) => [
285
- 'objective', 'message', texto(p, 'objective'),
286
- '--text', texto(p, 'message'), '--por', texto(p, 'author'), '--role', 'human',
287
- ],
288
- },
289
305
  {
290
306
  name: 'ork_portfolio_list',
291
307
  description: 'Lista produtos, projetos e iniciativas do catálogo canônico usado pelo Kanban.',
@@ -313,6 +329,29 @@ const FERRAMENTAS: FerramentaOrk[] = [
313
329
  parameters: schema({ thread: { type: 'string' }, id: { type: 'string' } }),
314
330
  argv: (p) => ['brain', 'get', texto(p, 'id'), '--thread', texto(p, 'thread')],
315
331
  },
332
+ {
333
+ name: 'ork_brain_context',
334
+ description:
335
+ 'Pacote de contexto citável do Company Brain: entidades pedidas e seus pais, cada uma com a citação da fonte, o frescor contra o portfólio e as lacunas. Só leitura.',
336
+ parameters: schema({
337
+ thread: { type: 'string' },
338
+ ids: { type: 'string', description: 'ids do portfólio separados por vírgula (prod-, proj-, init-)' },
339
+ }),
340
+ argv: (p) => ['brain', 'context', '--thread', texto(p, 'thread'), '--ids', texto(p, 'ids')],
341
+ },
342
+ {
343
+ name: 'ork_brain_dossie',
344
+ description:
345
+ 'Dossiê de decisão da thread: vínculo com objetivo e projeto, contexto citável, alternativas, decisão, quem decidiu e evidência, com os ids do Company Brain e as lacunas. Resposta do dono só com recibo conferido. Só leitura.',
346
+ parameters: {
347
+ type: 'object', additionalProperties: false, required: ['thread'],
348
+ properties: {
349
+ thread: { type: 'string' },
350
+ decisao: { type: 'string', description: 'id do pedido ou fact- da decisão; omita para a thread inteira' },
351
+ },
352
+ },
353
+ argv: (p) => ['brain', 'dossie', '--thread', texto(p, 'thread'), ...(p.decisao === undefined ? [] : ['--decisao', texto(p, 'decisao')])],
354
+ },
316
355
  {
317
356
  name: 'ork_claims_add',
318
357
  description:
@@ -396,31 +435,35 @@ const FERRAMENTAS: FerramentaOrk[] = [
396
435
  {
397
436
  name: 'ork_master',
398
437
  description:
399
- 'Fecha a thread: POSTMORTEM tipado, MASTER log no contrato congelado e o score HUMANO de 0 a 5 com justificativa. Nunca invente a nota.',
438
+ 'Pede ao dono a nota do MASTER (0 a 5, com o porque) de uma entrega: devolve a linha com o codigo curto para mostrar a ele. A nota volta pelo Telegram, com a prova do ingresso. Esta tool nunca grava nota e nao recebe nome de pessoa (RM-048).',
400
439
  parameters: schema({
401
440
  thread: { type: 'string' },
402
- score: { type: 'string', description: 'inteiro de 0 a 5, dado pelo humano' },
403
- justificativa: { type: 'string' },
404
- quem: { type: 'string' },
405
441
  }),
406
- argv: (p) => [
407
- 'master',
408
- texto(p, 'thread'),
409
- '--score',
410
- texto(p, 'score'),
411
- '--justificativa',
412
- texto(p, 'justificativa'),
413
- '--por',
414
- texto(p, 'quem'),
415
- ],
442
+ argv: (p) => ['master', 'pedir', texto(p, 'thread'), '--formato', 'telegram'],
416
443
  },
417
444
  {
418
445
  name: 'ork_board',
419
446
  description:
420
- 'Todas as threads em uma visao, e o escalonador dizendo quem avanca agora e quem espera.',
447
+ 'As threads DESTE projeto nesta maquina e o escalonador dizendo quem avanca agora e quem espera. NAO le o roadmap: nunca conclua sobre o roadmap a partir do board (zero threads nao e roadmap vazio); para o roadmap use ork_network_roadmap. O cabecalho diz o projeto consultado e o que nao foi lido.',
421
448
  parameters: schema({}),
422
449
  argv: () => ['board', 'plan'],
423
450
  },
451
+ {
452
+ name: 'ork_roadmap_status',
453
+ description:
454
+ 'Status report do roadmap SO desta maquina (o checkout local), no formato aprovado pelo dono (grupos com icones, #HITL e o fecho): nao le as reservas nem as outras maquinas, e o cabecalho diz isso. Para o status do roadmap com as threads de todas as maquinas, as reservas, as fontes e as lacunas, use ork_network_roadmap. Somente leitura: transporte o texto como vem, sem reescrever. Passe projeto com o nome que o dono pediu (ex.: orkastery); sem ele e com mais de um projeto na maquina, a resposta e a escolha. Nunca deduza o roadmap de ork_board ou ork_maestro.',
455
+ parameters: schema({}),
456
+ argv: () => ['roadmap', 'status'],
457
+ },
458
+ {
459
+ name: 'ork_network_roadmap',
460
+ description:
461
+ 'Roadmap da rede, somente leitura: para cada projeto, o status report do roadmap (RM-048) com as threads de TODAS as máquinas, as reservas, as threads por máquina com a idade da batida, a fonte e a hora de cada parte e as lacunas. É a fonte para qualquer pergunta sobre o roadmap ou o status report: transporte o texto como vem, sem reescrever nem resumir. Passe projeto com o nome que o dono pediu (ex.: orkastery) ou github:dono/repo; sem projeto, vêm todos os projetos conhecidos, e esse é o panorama da frase orkastery maestro sem projeto. Lacuna, "não lido" e "Não consultado" são fontes que ficaram sem leitura: nunca conclua "roadmap vazio" nem "nenhuma máquina publicou" a partir delas.',
462
+ parameters: schema({}),
463
+ projeto: PROJETO_DA_REDE,
464
+ // RM-054 (fatia 2): uma chamada de CLI; o `--projeto` vai no inicio e o nucleo o devolve ao `network`.
465
+ argv: () => ['network', 'roadmap'],
466
+ },
424
467
  {
425
468
  name: 'ork_master_batch',
426
469
  description:
@@ -430,20 +473,44 @@ const FERRAMENTAS: FerramentaOrk[] = [
430
473
  },
431
474
  ];
432
475
 
476
+ /** RM-052: toda tool aceita `projeto` opcional, sem mudar o que ela ja exigia. */
477
+ function comProjeto(f: FerramentaOrk): Record<string, unknown> {
478
+ const propriedades = (f.parameters.properties ?? {}) as Record<string, unknown>;
479
+ const p = f.projeto;
480
+ const projeto = p ? { type: 'string', pattern: p.padrao.source, description: p.descricao } : PARAMETRO_PROJETO;
481
+ return { ...f.parameters, properties: { ...propriedades, projeto } };
482
+ }
483
+
484
+ /** `--projeto <nome>` vai no inicio do argv; o resto dos parametros segue para a tool como antes. */
485
+ function argvComProjeto(f: FerramentaOrk, params: Record<string, unknown>): { argv: string[]; resto: Record<string, unknown> } {
486
+ const { projeto, ...resto } = params;
487
+ if (projeto !== undefined && (typeof projeto !== 'string' || !(f.projeto ?? PROJETO_DA_RM052).padrao.test(projeto))) {
488
+ throw new Error('projeto.invalido');
489
+ }
490
+ return { argv: [...(projeto === undefined ? [] : ['--projeto', projeto]), ...f.argv(resto)], resto };
491
+ }
492
+
433
493
  const plugin = defineToolPlugin({
434
494
  id: 'orkastery',
435
495
  name: 'Orkastery',
436
496
  description:
437
- 'Conducao de looping threads em 6 fases pelo nucleo `ork`, exposta ao OpenClaw como tools `ork_*`. Zero regra de negocio no host: cada tool e uma chamada de CLI.',
497
+ 'Conducao de looping threads em 6 fases pelo nucleo `ork`, exposta ao OpenClaw como tools `ork_*`. Zero regra de negocio no host: cada tool e uma chamada de CLI. Toda tool aceita projeto (nome do projeto pedido); o cwd do gateway nunca escolhe o projeto. O status do roadmap vem de ork_network_roadmap.',
438
498
  tools: (tool) =>
439
499
  FERRAMENTAS.map((f) =>
440
500
  tool({
441
501
  name: f.name,
442
502
  description: f.description,
443
- parameters: f.parameters,
503
+ parameters: comProjeto(f),
444
504
  execute: (params, _config, contexto) => {
445
- try { return rodarOrk(f.argv(params), contexto.signal, f.entrada?.(params)); }
446
- catch { return '[ork recusou] resposta humana não confirmada; confira origem e correlação do pedido'; }
505
+ let chamada: ReturnType<typeof argvComProjeto>;
506
+ try { chamada = argvComProjeto(f, params); }
507
+ catch (e) {
508
+ if ((e as Error).message === 'projeto.invalido') {
509
+ return `[ork recusou] projeto.invalido: ${(f.projeto ?? PROJETO_DA_RM052).recusa}`;
510
+ }
511
+ return '[ork recusou] resposta humana não confirmada; confira origem e correlação do pedido';
512
+ }
513
+ return rodarOrk(chamada.argv, contexto.signal, f.entrada?.(chamada.resto));
447
514
  },
448
515
  })
449
516
  ),
@@ -16,7 +16,7 @@ O **roadmap** registra problemas, hipóteses, escolhas, prioridades e andamento
16
16
 
17
17
  | Regra | Aplicação |
18
18
  | --- | --- |
19
- | Identidade | Atribua IDs estáveis, por exemplo `PLAT-01`, `SYS-01`, `MOD-01`, `FEAT-042`, `UC-042-01`, `BR-042-01`, `API-01`, `EVT-01`, `JOB-01` e `RM-042`. Renomear não muda o ID. |
19
+ | Identidade | Atribua IDs estáveis, por exemplo `PLAT-01`, `SYS-01`, `MOD-01`, `FEAT-042`, `UC-042-01`, `BR-042-01`, `API-01`, `EVT-01`, `JOB-01` e `RM-042`. Renomear não muda o ID. No Orkastery, o número da FEAT nova sai de `ork roadmap feat`, reservado entre máquinas; nunca do maior número da sua branch. |
20
20
  | Escopo e estado | Informe se a página descreve `vigente`, `em desenvolvimento`, `proposto` ou `descontinuado`, com ambiente, versão e data de verificação. |
21
21
  | Rastreabilidade | Ligue feature ↔ item de roadmap ↔ casos de uso/regras ↔ contratos/esquemas ↔ código/PR ↔ testes/evidências ↔ release. Use `Não aplicável — motivo` quando uma seção não fizer sentido. |
22
22
  | Fonte | Diferencie `confirmado` (código, contrato, teste ou decisão aprovada), `planejado` (item de roadmap) e `a validar`. Nunca complete uma lacuna por inferência silenciosa. |
@@ -75,7 +75,7 @@ Registre risco, perfis e matriz de permissões (RBAC/ABAC), autenticação/sess
75
75
 
76
76
  ## 4. Modelo de página de feature
77
77
 
78
- Copie [o modelo](../produto/_modelo-feature.md) para cada feature; substitua colchetes por valores ou por `Não aplicável — motivo`. O frontmatter é a camada que agentes e o verificador leem; o corpo é a camada que pessoas leem. Acrescente tabelas de campos e contratos quando houver vários elementos.
78
+ Reserve o número com `ork roadmap feat --thread <thread>` e copie [o modelo](../produto/_modelo-feature.md) para cada feature; substitua colchetes por valores ou por `Não aplicável — motivo`. O frontmatter é a camada que agentes e o verificador leem; o corpo é a camada que pessoas leem. Acrescente tabelas de campos e contratos quando houver vários elementos.
79
79
 
80
80
  Chaves do frontmatter de uma feature:
81
81
 
@@ -80,7 +80,7 @@ Copie [o modelo](../roadmap/_modelo-item.md) para cada item. O frontmatter guard
80
80
 
81
81
  Revisite itens ativos na cadência do time e sempre após decisão, PR relevante, teste, deploy, alteração de flag ou medição de resultado. A cada revisão, compare roadmap, documentação da feature, código, contratos e evidências de produção; anote divergências com owner e prazo. Agentes podem sugerir atualização de status a partir de eventos do repositório, mas um PR mesclado atualiza somente a dimensão **código** até que deploy e exposição sejam comprovados. Registre a data de atualização e mantenha histórico das decisões e mudanças de escopo.
82
82
 
83
- No Orkastery, `ork docs sincronizar` é esse agente: ele atualiza `estado.codigo` e `evidencias.codigo.commit` a partir do ledger (`ship_done`) e do git, e a seção `sdlc` a partir da thread. **Ele nunca mexe em ciclo, documentação, deploy, exposição ou habilitação** — essas dimensões são decisão de pessoa, e o verificador só cobra que estejam coerentes com o código.
83
+ No Orkastery, `ork docs sincronizar` é esse agente: ele atualiza `estado.codigo` e `evidencias.codigo.commit` a partir do ledger (`ship_done`) e do git, e a seção `sdlc` a partir da thread. **Ele nunca mexe em ciclo, documentação, deploy, exposição ou habilitação** — essas dimensões são decisão de pessoa, e o verificador só cobra que estejam coerentes com o código. Na worktree de uma thread, ele só toca o item dela e os índices, para que o PR de uma thread não mude o item de outra; `--so RM-NNN` escolhe os itens, e `--todos` (o padrão na raiz do projeto, onde o condutor sincroniza depois do merge) volta a todos.
84
84
 
85
85
  ## 6. Três leitores e fatos de SDLC (adaptação Orkastery)
86
86
 
@@ -17,6 +17,8 @@ fontes:
17
17
  comandos: []
18
18
  ---
19
19
 
20
+ <!-- O número da feature sai de `ork roadmap feat --thread <thread>`, reservado entre máquinas; nunca do maior número da branch. -->
21
+
20
22
  # FEAT-000 — Nome da feature
21
23
 
22
24
  > **Em uma frase:** [o que esta feature faz, para quem, em até 240 caracteres]