@orkastery/cli 0.4.3 → 0.5.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.
- package/adapters/README.md +11 -0
- package/adapters/claude-code/.claude-plugin/plugin.json +3 -1
- package/adapters/claude-code/README.md +11 -2
- package/adapters/claude-code/agents/ork-check.md +1 -1
- package/adapters/claude-code/agents/ork-go.md +2 -2
- package/adapters/claude-code/agents/ork-goal.md +1 -1
- package/adapters/claude-code/agents/ork-master.md +1 -1
- package/adapters/claude-code/agents/ork-plan.md +1 -1
- package/adapters/claude-code/agents/ork-ship.md +1 -1
- package/adapters/claude-code/commands/master.md +11 -8
- package/adapters/claude-code/commands/onboarding.md +3 -2
- package/adapters/claude-code/commands/ork.md +8 -0
- package/adapters/claude-code/commands/ship.md +2 -2
- package/adapters/codex/skills/ork/SKILL.md +12 -1
- package/adapters/hermes/README.md +3 -0
- package/adapters/hermes/bin/ork-abrir-thread.sh +17 -2
- package/adapters/hermes/bin/ork-brain.sh +3 -0
- package/adapters/hermes/bin/ork-maestro.sh +4 -1
- package/adapters/hermes/bin/ork-network-roadmap.sh +11 -0
- package/adapters/hermes/bin/ork-roadmap-status.sh +9 -0
- package/adapters/hermes/hermes.plugin.json +2 -0
- package/adapters/hermes/hitl-ingress/__init__.py +36 -3
- package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +19 -20
- package/adapters/openclaw/README.md +45 -2
- package/adapters/openclaw/dist/hitl-ingress.js +37 -3
- package/adapters/openclaw/dist/index.js +93 -21
- package/adapters/openclaw/openclaw.plugin.json +10 -1
- package/adapters/openclaw/package.json +2 -2
- package/adapters/openclaw/src/hitl-ingress.ts +32 -3
- package/adapters/openclaw/src/index.ts +109 -21
- package/assets/docs/padroes/documentacao-de-produto.md +2 -2
- package/assets/docs/padroes/roadmap-de-produto.md +1 -1
- package/assets/docs/produto/_modelo-feature.md +2 -0
- package/assets/orkmind_bridge.py +214 -1
- package/dist/adapters/claude-bg.js +24 -7
- package/dist/auditrun.js +1 -1
- package/dist/board.js +74 -0
- package/dist/branch-de-estado.js +5 -4
- package/dist/busca-semantica.js +161 -0
- package/dist/ci.js +141 -10
- package/dist/claim-lint.js +3 -1
- package/dist/company-brain-cli.js +27 -2
- package/dist/company-brain-context.js +132 -0
- package/dist/company-brain-dossie.js +376 -0
- package/dist/company-brain-mcp.js +4 -1
- package/dist/conducao.js +71 -18
- package/dist/decisao-autonoma.js +42 -0
- package/dist/docs.js +135 -25
- package/dist/doctor.js +242 -32
- package/dist/entrega-pr.js +116 -5
- package/dist/experiencia-instalacao.js +256 -0
- package/dist/experiencia.js +72 -0
- package/dist/fabrica-estado.js +63 -12
- package/dist/fechamento.js +105 -0
- package/dist/fix.js +3 -3
- package/dist/forja.js +435 -0
- package/dist/gates.js +9 -1
- package/dist/hitl-classificacao.js +11 -1
- package/dist/hitl-contract.js +50 -7
- package/dist/hitl-curto.js +158 -0
- package/dist/hitl-gates.js +67 -1
- package/dist/hitl-lote.js +30 -15
- package/dist/hitl-presentation.js +10 -7
- package/dist/hitl-resumo.js +57 -13
- package/dist/hitl-texto-livre.js +150 -0
- package/dist/hosts.js +92 -3
- package/dist/index.js +637 -45
- package/dist/indice-vetorial.js +317 -0
- package/dist/init.js +24 -5
- package/dist/integracoes-locais.js +3 -1
- package/dist/intelligence-benchmark-contract.js +570 -0
- package/dist/intelligence-graph-cli.js +584 -0
- package/dist/intelligence-graph-contract.js +636 -0
- package/dist/intelligence-graph-extract-md.js +487 -0
- package/dist/intelligence-graph-extract-ts.js +980 -0
- package/dist/intelligence-graph-extract.js +546 -0
- package/dist/intelligence-graph-index.js +693 -0
- package/dist/intelligence-graph-parsers.js +327 -0
- package/dist/intelligence-graph-query.js +344 -0
- package/dist/intelligence-graph-repo.js +197 -0
- package/dist/leases.js +100 -0
- package/dist/ledger.js +23 -0
- package/dist/licoes.js +5 -4
- package/dist/liveness.js +2 -1
- package/dist/maestro-cli.js +10 -1
- package/dist/maestro-contract.js +5 -1
- package/dist/maestro-discovery.js +16 -2
- package/dist/maestro-runtime.js +3 -0
- package/dist/maestro-snapshot.js +6 -2
- package/dist/manifest.js +166 -5
- package/dist/master-batch.js +3 -2
- package/dist/master-digest.js +2 -2
- package/dist/master-nota.js +203 -0
- package/dist/master.js +4 -0
- package/dist/mcp-artifacts.js +2 -0
- package/dist/mcp-experiencia.js +164 -0
- package/dist/mcp-git.js +64 -12
- package/dist/mcp-grafo-worker.js +91 -0
- package/dist/mcp-grafo.js +233 -0
- package/dist/mcp-install.js +5 -2
- package/dist/mcp-maestro.js +3 -2
- package/dist/mcp-server.js +93 -5
- package/dist/mcp-ship.js +2 -0
- package/dist/mcp-verify.js +3 -1
- package/dist/memoria.js +147 -1
- package/dist/network-roadmap.js +907 -0
- package/dist/ocupacao.js +43 -3
- package/dist/onboarding.js +122 -11
- package/dist/orkmind.js +165 -9
- package/dist/orquestracao.js +2 -1
- package/dist/parado-no-condutor.js +933 -0
- package/dist/phase.js +166 -13
- package/dist/policies.js +29 -2
- package/dist/projeto-alvo.js +551 -0
- package/dist/prompts.js +1 -1
- package/dist/prova-ativacao.js +345 -0
- package/dist/pulse-consentimento.js +4 -2
- package/dist/pulse-delivery.js +32 -6
- package/dist/pulse-resposta.js +674 -49
- package/dist/pulse.js +74 -3
- package/dist/retry.js +51 -8
- package/dist/roadmap-reservas.js +262 -20
- package/dist/roadmap-status.js +348 -0
- package/dist/runtime-context.js +5 -1
- package/dist/sessoes-inventario.js +7 -1
- package/dist/sessoes.js +4 -1
- package/dist/ship.js +25 -1
- package/dist/thread-close.js +3 -0
- package/dist/thread.js +21 -2
- package/dist/util.js +71 -4
- package/dist/verify.js +6 -5
- package/dist/worktree.js +29 -6
- package/eval/casos/onboarding.json +1 -1
- package/eval/casos/orchestration-experience-pt-br.json +48 -0
- package/eval/casos/orchestration-experience.json +48 -0
- package/package.json +1 -1
- package/schemas/code-artifact-graph.v1.schema.json +581 -0
- package/schemas/graph-benchmark.v1.schema.json +2240 -0
- package/schemas/maestro-snapshot.schema.json +23 -0
- package/skills/README.md +7 -5
- package/skills/core/onboarding/SKILL.md +10 -2
- package/skills/core/orchestration-experience/SKILL.md +113 -0
- package/skills/core/orchestration-experience-pt-br/SKILL.md +112 -0
- package/skills/core/orkastery-bootstrap/SKILL.md +2 -2
- package/skills/phases/go-implementation/SKILL.md +2 -0
- package/skills/phases/ship-release/SKILL.md +6 -3
package/dist/entrega-pr.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.mergeDaEntrega = mergeDaEntrega;
|
|
4
4
|
exports.registrarEntregaPorPr = registrarEntregaPorPr;
|
|
5
|
+
exports.registrarEntregaExternaPorPr = registrarEntregaExternaPorPr;
|
|
5
6
|
exports.registrarEntregasPorPr = registrarEntregasPorPr;
|
|
6
7
|
/**
|
|
7
8
|
* I-57 (RM-008, fatia 2): a entrega feita por PR tambem prova entrega para o MASTER.
|
|
@@ -35,6 +36,18 @@ function mergeDaEntrega(raiz, thread, remoto, base) {
|
|
|
35
36
|
}
|
|
36
37
|
return null;
|
|
37
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* A thread chega ao SHIP quando o ciclo dela tem SHIP, e so AVANCA: o segundo PR da mesma entrega (os
|
|
41
|
+
* dois sites da ork-siteshomesco, por exemplo) com a thread ja no MASTER nao a devolve ao SHIP (achado S9).
|
|
42
|
+
*/
|
|
43
|
+
function avancarParaShip(raiz, threadId) {
|
|
44
|
+
const atualizada = (0, thread_1.lerThread)(raiz, threadId);
|
|
45
|
+
const fases = atualizada.fases ?? [];
|
|
46
|
+
if (!fases.includes('SHIP') || fases.indexOf(atualizada.faseAtual) >= fases.indexOf('SHIP'))
|
|
47
|
+
return;
|
|
48
|
+
atualizada.faseAtual = 'SHIP';
|
|
49
|
+
(0, thread_1.gravarThread)(raiz, atualizada);
|
|
50
|
+
}
|
|
38
51
|
function resultado(thread, acao, motivo, extra = {}) {
|
|
39
52
|
return { thread, acao, motivo, mergeSha: null, headSha: null, ci: null, ...extra };
|
|
40
53
|
}
|
|
@@ -68,6 +81,10 @@ function registrarEntregaPorPr(carregado, threadId, opcoes = {}) {
|
|
|
68
81
|
if (ci.required && !ci.ok) {
|
|
69
82
|
return resultado(threadId, 'recusada', `sem CI verde no head do PR: ${ci.detail}`, { ...merge, ci });
|
|
70
83
|
}
|
|
84
|
+
// RM-037 (fatia 3, defeito 6): o ensaio confere tudo e para aqui, sem ledger, fase nem fabrica.
|
|
85
|
+
if (opcoes.dryRun) {
|
|
86
|
+
return resultado(threadId, 'registraria', `ensaio: registraria ship_done pelo merge ${merge.mergeSha.slice(0, 7)} do PR (nada gravado)`, { ...merge, ci });
|
|
87
|
+
}
|
|
71
88
|
(0, ledger_1.registrar)(dir, threadId, ledger_1.TIPOS_DE_EVENTO.shipConcluido, {
|
|
72
89
|
de: thread.base?.branch ?? null,
|
|
73
90
|
para: base,
|
|
@@ -85,15 +102,109 @@ function registrarEntregaPorPr(carregado, threadId, opcoes = {}) {
|
|
|
85
102
|
evidencia: { viaPr: true, ci: { state: ci.state, context: ci.context, url: ci.url, sha: ci.sha } },
|
|
86
103
|
});
|
|
87
104
|
// Como no `ork ship`: a thread chega ao SHIP quando o ciclo dela tem SHIP.
|
|
88
|
-
|
|
89
|
-
if (atualizada.fases?.includes('SHIP')) {
|
|
90
|
-
atualizada.faseAtual = 'SHIP';
|
|
91
|
-
(0, thread_1.gravarThread)(raiz, atualizada);
|
|
92
|
-
}
|
|
105
|
+
avancarParaShip(raiz, threadId);
|
|
93
106
|
if (opcoes.publicar !== false)
|
|
94
107
|
(0, fabrica_publicar_1.publicarEmSegundoPlano)(raiz);
|
|
95
108
|
return resultado(threadId, 'registrou', `ship_done pelo merge ${merge.mergeSha.slice(0, 7)} do PR`, { ...merge, ci });
|
|
96
109
|
}
|
|
110
|
+
const ghApi = (caminho) => (0, util_1.exec)('gh', ['api', caminho, '-H', 'Accept: application/vnd.github+json'], process.cwd(), 120000);
|
|
111
|
+
function lerJson(r) {
|
|
112
|
+
if (!r.ok)
|
|
113
|
+
return null;
|
|
114
|
+
try {
|
|
115
|
+
return JSON.parse(r.stdout);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* `ork ship registrar-pr <thread> --repo <dono/nome> --pr <n>`: a thread que entrega por PR em outro
|
|
123
|
+
* repositorio (os sites, por exemplo) nao tinha como virar `ship_done`, e a entrega virava decisao. A
|
|
124
|
+
* exigencia e a mesma do caminho local: o merge dentro da ponta da base (pela API do GitHub, porque o
|
|
125
|
+
* repositorio nao e remoto deste checkout) e o check declarado verde no head do PR. O repositorio precisa
|
|
126
|
+
* estar declarado no manifesto; e o dono que diz onde, e com que check, a entrega vale. Idempotente.
|
|
127
|
+
*/
|
|
128
|
+
function registrarEntregaExternaPorPr(carregado, threadId, opcoes) {
|
|
129
|
+
const raiz = carregado.raiz, repo = opcoes.repositorio, n = opcoes.pr;
|
|
130
|
+
const gh = opcoes.executorGitHub ?? ghApi;
|
|
131
|
+
const declarados = carregado.manifesto.ci.external_repositories;
|
|
132
|
+
if (!Object.hasOwn(declarados, repo)) {
|
|
133
|
+
return resultado(threadId, 'recusada', `o repositorio ${repo} nao esta declarado em ci.external_repositories do orkastery.yaml`);
|
|
134
|
+
}
|
|
135
|
+
if (!Number.isSafeInteger(n) || n < 1)
|
|
136
|
+
return resultado(threadId, 'recusada', `--pr precisa ser o numero do PR, recebido ${String(n)}`);
|
|
137
|
+
const thread = (0, thread_1.lerThread)(raiz, threadId);
|
|
138
|
+
if (thread.status === 'fechada')
|
|
139
|
+
return resultado(threadId, 'ja-registrada', 'thread ja fechada');
|
|
140
|
+
const dir = (0, thread_1.dirThread)(raiz, threadId);
|
|
141
|
+
if ((0, ledger_1.lerLedger)(dir).some((e) => e.tipo === ledger_1.TIPOS_DE_EVENTO.shipConcluido && e.repositorio === repo && e.pr === n)) {
|
|
142
|
+
return resultado(threadId, 'ja-registrada', `o PR #${n} de ${repo} ja tem ship_done`);
|
|
143
|
+
}
|
|
144
|
+
const pr = lerJson(gh(`repos/${repo}/pulls/${n}`));
|
|
145
|
+
if (!pr)
|
|
146
|
+
return resultado(threadId, 'recusada', `a consulta do PR #${n} de ${repo} ao GitHub falhou`);
|
|
147
|
+
const mergeSha = typeof pr.merge_commit_sha === 'string' && /^[0-9a-f]{40}$/.test(pr.merge_commit_sha) ? pr.merge_commit_sha : null;
|
|
148
|
+
const headSha = typeof pr.head?.sha === 'string' && /^[0-9a-f]{40}$/.test(pr.head.sha) ? pr.head.sha : null;
|
|
149
|
+
const base = typeof pr.base?.ref === 'string' && /^[A-Za-z0-9._/-]{1,200}$/.test(pr.base.ref) ? pr.base.ref : null;
|
|
150
|
+
if (pr.merged !== true || !mergeSha || !headSha || !base) {
|
|
151
|
+
return resultado(threadId, 'sem-merge', `o PR #${n} de ${repo} nao esta mesclado`);
|
|
152
|
+
}
|
|
153
|
+
// Achado A4 do CHECK: o PR precisa ser DESTA thread e ter entrado na branch padrao do repositorio. Sem o
|
|
154
|
+
// vinculo, qualquer PR mesclado do repositorio viraria entrega de qualquer thread; sem a base, um PR
|
|
155
|
+
// empilhado numa branch de feature contaria como entrega.
|
|
156
|
+
const vinculo = [pr.title, pr.body, pr.head?.ref].filter((x) => typeof x === 'string')
|
|
157
|
+
// N4 do CHECK 2: a branch do proprio ork e `ork/<thread>-<variante>`, entao o hifen depois do id vale;
|
|
158
|
+
// letra ou digito colado nao (outro id). Sem diferenciar maiusculas.
|
|
159
|
+
.some((x) => new RegExp(`(^|[^a-z0-9-])${threadId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![a-z0-9])`, 'i').test(x));
|
|
160
|
+
if (!vinculo) {
|
|
161
|
+
return resultado(threadId, 'recusada', `o PR #${n} de ${repo} nao cita a thread ${threadId} no titulo, no corpo nem na branch`, { mergeSha, headSha });
|
|
162
|
+
}
|
|
163
|
+
const repositorio = lerJson(gh(`repos/${repo}`));
|
|
164
|
+
if (!repositorio)
|
|
165
|
+
return resultado(threadId, 'recusada', `a consulta de ${repo} ao GitHub falhou`, { mergeSha, headSha });
|
|
166
|
+
if (repositorio.default_branch !== base) {
|
|
167
|
+
return resultado(threadId, 'recusada', `o PR #${n} entrou em ${base}, e a branch padrao de ${repo} e ${repositorio.default_branch ?? 'desconhecida'}`, { mergeSha, headSha });
|
|
168
|
+
}
|
|
169
|
+
// A ponta da base AGORA, e o merge dentro dela: o push provado, pela mesma pergunta do ls-remote.
|
|
170
|
+
const ramo = lerJson(gh(`repos/${repo}/branches/${encodeURIComponent(base)}`));
|
|
171
|
+
const ponta = typeof ramo?.commit?.sha === 'string' && /^[0-9a-f]{40}$/.test(ramo.commit.sha) ? ramo.commit.sha : '';
|
|
172
|
+
const comparacao = ponta ? lerJson(gh(`repos/${repo}/compare/${mergeSha}...${ponta}`)) : null;
|
|
173
|
+
if (!ponta || !comparacao || !['ahead', 'identical'].includes(String(comparacao.status))) {
|
|
174
|
+
return resultado(threadId, 'recusada', `o merge ${mergeSha.slice(0, 7)} nao esta na ponta de ${repo}:${base}`, { mergeSha, headSha });
|
|
175
|
+
}
|
|
176
|
+
const check = declarados[repo];
|
|
177
|
+
const ci = check ? (0, ci_1.consultarCiDoRepositorio)(repo, headSha, check, opcoes.executorCi) : null;
|
|
178
|
+
if (ci && !ci.ok) {
|
|
179
|
+
return resultado(threadId, 'recusada', `sem CI verde no head do PR: ${ci.detail}`, { mergeSha, headSha, ci });
|
|
180
|
+
}
|
|
181
|
+
if (opcoes.dryRun) {
|
|
182
|
+
return resultado(threadId, 'registraria', `ensaio: registraria ship_done pelo merge ${mergeSha.slice(0, 7)} do PR #${n} de ${repo} (nada gravado)`, { mergeSha, headSha, ci });
|
|
183
|
+
}
|
|
184
|
+
(0, ledger_1.registrar)(dir, threadId, ledger_1.TIPOS_DE_EVENTO.shipConcluido, {
|
|
185
|
+
de: thread.base?.branch ?? null,
|
|
186
|
+
para: `${repo}:${base}`,
|
|
187
|
+
repositorio: repo,
|
|
188
|
+
pr: n,
|
|
189
|
+
url: pr.html_url ?? `https://github.com/${repo}/pull/${n}`,
|
|
190
|
+
shaDe: headSha,
|
|
191
|
+
mergeSha,
|
|
192
|
+
jaIncorporado: true,
|
|
193
|
+
remoto: `github:${repo}`,
|
|
194
|
+
shaRemoto: ponta,
|
|
195
|
+
pushVerificado: true,
|
|
196
|
+
fonteDaProva: `gh api repos/${repo}/compare/${mergeSha}...${ponta} (status ${String(comparacao.status)}; ponta de ${base} por gh api repos/${repo}/branches/${base})`,
|
|
197
|
+
autorizadoPor: ci ? 'merge de PR com o CI independente verde no SHA exato' : 'merge de PR em repositorio externo declarado sem CI no manifesto',
|
|
198
|
+
tipoDeAutorizacao: 'pr-externo',
|
|
199
|
+
modo: thread.modo,
|
|
200
|
+
tag: (0, modos_1.tagDoModo)(thread.modo),
|
|
201
|
+
evidencia: { viaPr: true, externo: true, ciExigido: !!ci, ci: ci ? { state: ci.state, context: ci.context, url: ci.url, sha: ci.sha } : null },
|
|
202
|
+
});
|
|
203
|
+
avancarParaShip(raiz, threadId);
|
|
204
|
+
if (opcoes.publicar !== false)
|
|
205
|
+
(0, fabrica_publicar_1.publicarEmSegundoPlano)(raiz);
|
|
206
|
+
return resultado(threadId, 'registrou', `ship_done pelo merge ${mergeSha.slice(0, 7)} do PR #${n} de ${repo}`, { mergeSha, headSha, ci });
|
|
207
|
+
}
|
|
97
208
|
/** `ork ship registrar-pr --todas`: toda thread aberta que ja tem merge `ship(<thread>)` na base. */
|
|
98
209
|
function registrarEntregasPorPr(carregado, opcoes = {}) {
|
|
99
210
|
const remoto = opcoes.remoto ?? 'origin';
|
|
@@ -0,0 +1,256 @@
|
|
|
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.FIM_EXPERIENCIA = exports.INICIO_EXPERIENCIA = void 0;
|
|
37
|
+
exports.caminhoSeguro = caminhoSeguro;
|
|
38
|
+
exports.planejarExperiencia = planejarExperiencia;
|
|
39
|
+
exports.aplicarExperiencia = aplicarExperiencia;
|
|
40
|
+
/** Bloco próprio nos arquivos lidos pelo host. Nunca altera o bloco de ork init. */
|
|
41
|
+
const fs = __importStar(require("node:fs"));
|
|
42
|
+
const path = __importStar(require("node:path"));
|
|
43
|
+
const node_crypto_1 = require("node:crypto");
|
|
44
|
+
exports.INICIO_EXPERIENCIA = '<!-- orkastery:experiencia:begin -->';
|
|
45
|
+
exports.FIM_EXPERIENCIA = '<!-- orkastery:experiencia:end -->';
|
|
46
|
+
const CONTRATO = 'ork.experiencia-install/v1';
|
|
47
|
+
const LINHA_DO_DIRETORIO = /^Se ativo, leia a skill selecionada pelo campo skill no diretório (".*")\.$/;
|
|
48
|
+
const conflito = () => { throw Error('experiencia.bloco.conflict: bloco ou recibo alterado/duplicado; preserve os arquivos e revise a instalação'); };
|
|
49
|
+
/** Recusa symlinks e hardlinks em qualquer componente antes da leitura/escrita. */
|
|
50
|
+
function caminhoSeguro(projeto, arquivo) {
|
|
51
|
+
if (fs.realpathSync(projeto) !== projeto || !arquivo.startsWith(projeto + path.sep))
|
|
52
|
+
throw Error('experiencia.path.unsafe');
|
|
53
|
+
let atual = projeto;
|
|
54
|
+
const partes = path.relative(projeto, arquivo).split(path.sep);
|
|
55
|
+
for (let i = 0; i < partes.length; i++) {
|
|
56
|
+
atual = path.join(atual, partes[i]);
|
|
57
|
+
const s = fs.lstatSync(atual, { throwIfNoEntry: false });
|
|
58
|
+
if (!s)
|
|
59
|
+
continue;
|
|
60
|
+
if (s.isSymbolicLink() || (i < partes.length - 1 ? !s.isDirectory() : !s.isFile() || s.nlink !== 1))
|
|
61
|
+
throw Error('experiencia.path.unsafe');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function ler(projeto, arquivo) {
|
|
65
|
+
caminhoSeguro(projeto, arquivo);
|
|
66
|
+
return fs.existsSync(arquivo) ? fs.readFileSync(arquivo) : null;
|
|
67
|
+
}
|
|
68
|
+
function iguais(a, b) { return a === null ? b === null : b !== null && a.equals(b); }
|
|
69
|
+
function contar(b, texto) {
|
|
70
|
+
let n = 0, i = 0;
|
|
71
|
+
while ((i = b.indexOf(texto, i)) >= 0) {
|
|
72
|
+
n++;
|
|
73
|
+
i += Buffer.byteLength(texto);
|
|
74
|
+
}
|
|
75
|
+
return n;
|
|
76
|
+
}
|
|
77
|
+
function decodificar(s) {
|
|
78
|
+
if (typeof s !== 'string')
|
|
79
|
+
return conflito();
|
|
80
|
+
const b = Buffer.from(s, 'base64');
|
|
81
|
+
if (b.toString('base64') !== s)
|
|
82
|
+
return conflito();
|
|
83
|
+
return b;
|
|
84
|
+
}
|
|
85
|
+
const eolDe = (b) => b.includes('\r\n') ? '\r\n' : '\n';
|
|
86
|
+
/** O caminho vai relativo ao projeto: o arquivo de instruções costuma ser versionado. */
|
|
87
|
+
function nucleoDoBloco(skill, eol) {
|
|
88
|
+
return Buffer.from([exports.INICIO_EXPERIENCIA,
|
|
89
|
+
'## Experiência de orquestração',
|
|
90
|
+
'Consulte `ork experiencia show --json` neste projeto. Se experience for false, não ative o pacote.',
|
|
91
|
+
`Se ativo, leia a skill selecionada pelo campo skill no diretório ${JSON.stringify(skill)}.`,
|
|
92
|
+
'As preferências são dados do núcleo; preserve gates, proveniência humana e permissões do host.',
|
|
93
|
+
exports.FIM_EXPERIENCIA, ''].join(eol));
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Bloco sem recibo (clone novo, `.orkastery/` apagado) só é adotado quando é exatamente o texto
|
|
97
|
+
* que esta versão ou a anterior geram, com qualquer diretório; qualquer outra edição é conflito.
|
|
98
|
+
*/
|
|
99
|
+
function reconhecerBloco(antes) {
|
|
100
|
+
if (contar(antes, exports.INICIO_EXPERIENCIA) !== 1 || contar(antes, exports.FIM_EXPERIENCIA) !== 1)
|
|
101
|
+
return null;
|
|
102
|
+
const inicio = antes.indexOf(exports.INICIO_EXPERIENCIA), marcaFim = antes.indexOf(exports.FIM_EXPERIENCIA);
|
|
103
|
+
if (marcaFim < inicio || (inicio > 0 && antes[inicio - 1] !== 10))
|
|
104
|
+
return null;
|
|
105
|
+
let fim = marcaFim + Buffer.byteLength(exports.FIM_EXPERIENCIA);
|
|
106
|
+
const eol = eolDe(antes.subarray(inicio, fim));
|
|
107
|
+
if (antes.subarray(fim, fim + eol.length).toString() === eol)
|
|
108
|
+
fim += eol.length;
|
|
109
|
+
const texto = antes.subarray(inicio, fim);
|
|
110
|
+
const linha = texto.toString('utf8').split(eol)[3] ?? '';
|
|
111
|
+
const m = LINHA_DO_DIRETORIO.exec(linha);
|
|
112
|
+
let skill;
|
|
113
|
+
try {
|
|
114
|
+
skill = m ? JSON.parse(m[1]) : undefined;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
if (typeof skill !== 'string' || /[\r\n`]/.test(skill))
|
|
120
|
+
return null;
|
|
121
|
+
const esperado = nucleoDoBloco(skill, eol);
|
|
122
|
+
if (!texto.equals(esperado) && !texto.equals(esperado.subarray(0, esperado.length - eol.length)))
|
|
123
|
+
return null;
|
|
124
|
+
// Uma linha em branco antes do bloco é o separador que a instalação põe depois de uma linha.
|
|
125
|
+
const sep = Buffer.from(eol);
|
|
126
|
+
const separador = inicio >= 2 * sep.length && antes.subarray(inicio - sep.length, inicio).equals(sep) &&
|
|
127
|
+
antes[inicio - sep.length - 1] === 10 ? sep : Buffer.alloc(0);
|
|
128
|
+
return { inicio, fim, separador, eol, doRecibo: false };
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Remove o bloco e, se ainda intacto, o separador. O separador de uma quebra só sai quando a linha
|
|
132
|
+
* anterior continua terminada; o de duas, quando a última linha do conteúdo original continua sem
|
|
133
|
+
* quebra. Conteúdo externo depois do bloco não se funde à última linha do prefixo.
|
|
134
|
+
*/
|
|
135
|
+
function removerBloco(antes, bloco) {
|
|
136
|
+
let inicio = bloco.inicio;
|
|
137
|
+
const sep = bloco.separador, antesDoSep = bloco.inicio - sep.length;
|
|
138
|
+
if (sep.length && antesDoSep > 0 && antes.subarray(antesDoSep, bloco.inicio).equals(sep)) {
|
|
139
|
+
const terminaEmLinha = antes[antesDoSep - 1] === 10;
|
|
140
|
+
if ((contar(sep, '\n') === 1) === terminaEmLinha)
|
|
141
|
+
inicio = antesDoSep;
|
|
142
|
+
}
|
|
143
|
+
const prefixo = antes.subarray(0, inicio), sufixo = antes.subarray(bloco.fim);
|
|
144
|
+
const juncao = prefixo.length && prefixo.at(-1) !== 10 && sufixo.length ? Buffer.from(bloco.eol) : Buffer.alloc(0);
|
|
145
|
+
return Buffer.concat([prefixo, juncao, sufixo]);
|
|
146
|
+
}
|
|
147
|
+
/** Planejamento puro em relação ao disco: dry-run e preflight usam o mesmo contrato. */
|
|
148
|
+
function planejarExperiencia(projeto, host, skill) {
|
|
149
|
+
projeto = fs.realpathSync(path.resolve(projeto));
|
|
150
|
+
if (!['claude-code', 'codex'].includes(host))
|
|
151
|
+
throw Error('experiencia.host.unsupported');
|
|
152
|
+
if (skill !== null && /[\r\n`]/.test(skill))
|
|
153
|
+
throw Error('experiencia.skill.invalid');
|
|
154
|
+
const arquivo = path.join(projeto, host === 'codex' ? 'AGENTS.md' : 'CLAUDE.md');
|
|
155
|
+
const reciboPath = path.join(projeto, '.orkastery', 'experiencia', host + '.json');
|
|
156
|
+
const antes = ler(projeto, arquivo), bytesRecibo = ler(projeto, reciboPath);
|
|
157
|
+
let recibo = null;
|
|
158
|
+
if (bytesRecibo) {
|
|
159
|
+
try {
|
|
160
|
+
recibo = JSON.parse(bytesRecibo.toString('utf8'));
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
conflito();
|
|
164
|
+
}
|
|
165
|
+
if (!recibo || recibo.contrato !== CONTRATO || recibo.host !== host)
|
|
166
|
+
return conflito();
|
|
167
|
+
if (recibo.anterior !== null)
|
|
168
|
+
decodificar(recibo.anterior);
|
|
169
|
+
}
|
|
170
|
+
const marcas = antes ? contar(antes, exports.INICIO_EXPERIENCIA) + contar(antes, exports.FIM_EXPERIENCIA) : 0;
|
|
171
|
+
// Sem marcas, um recibo que sobrou (arquivo apagado ou bloco tirado à mão) não descreve nada.
|
|
172
|
+
let bloco = null;
|
|
173
|
+
if (antes && marcas) {
|
|
174
|
+
if (recibo) {
|
|
175
|
+
const adicao = decodificar(recibo.adicao), corte = adicao.indexOf(exports.INICIO_EXPERIENCIA);
|
|
176
|
+
if (contar(adicao, exports.INICIO_EXPERIENCIA) !== 1 || contar(adicao, exports.FIM_EXPERIENCIA) !== 1 || corte < 0)
|
|
177
|
+
return conflito();
|
|
178
|
+
const nucleo = adicao.subarray(corte), inicio = antes.indexOf(nucleo);
|
|
179
|
+
if (marcas === 2 && inicio >= 0) {
|
|
180
|
+
bloco = { inicio, fim: inicio + nucleo.length, separador: adicao.subarray(0, corte), eol: eolDe(nucleo), doRecibo: true };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
bloco ??= reconhecerBloco(antes);
|
|
184
|
+
if (!bloco)
|
|
185
|
+
return conflito();
|
|
186
|
+
}
|
|
187
|
+
let depois = antes, proximoRecibo = null;
|
|
188
|
+
if (skill !== null) {
|
|
189
|
+
let separador, nucleo, anterior;
|
|
190
|
+
if (antes && bloco) {
|
|
191
|
+
// Atualização no lugar: o que está antes e depois do bloco, inclusive edição externa, fica.
|
|
192
|
+
nucleo = nucleoDoBloco(skill, bloco.eol);
|
|
193
|
+
separador = bloco.separador;
|
|
194
|
+
depois = Buffer.concat([antes.subarray(0, bloco.inicio), nucleo, antes.subarray(bloco.fim)]);
|
|
195
|
+
// Adotado sem recibo: se só havia o bloco, o arquivo conta como criado pelo pacote.
|
|
196
|
+
const semBloco = removerBloco(antes, bloco);
|
|
197
|
+
anterior = bloco.doRecibo ? recibo.anterior : semBloco.length ? semBloco.toString('base64') : null;
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
const base = antes ?? Buffer.alloc(0), eol = eolDe(base);
|
|
201
|
+
nucleo = nucleoDoBloco(skill, eol);
|
|
202
|
+
separador = Buffer.from(base.length ? (base.at(-1) === 10 ? eol : eol + eol) : '');
|
|
203
|
+
depois = Buffer.concat([base, separador, nucleo]);
|
|
204
|
+
anterior = antes?.toString('base64') ?? null;
|
|
205
|
+
}
|
|
206
|
+
proximoRecibo = Buffer.from(JSON.stringify({ contrato: CONTRATO, host, anterior,
|
|
207
|
+
adicao: Buffer.concat([separador, nucleo]).toString('base64'),
|
|
208
|
+
}, null, 2) + '\n');
|
|
209
|
+
}
|
|
210
|
+
else if (antes && bloco) {
|
|
211
|
+
depois = removerBloco(antes, bloco);
|
|
212
|
+
// Vazio só some quando o arquivo não existia (recibo) ou quando, sem recibo, só havia o bloco.
|
|
213
|
+
if (depois.length === 0 && (recibo ? recibo.anterior === null : true))
|
|
214
|
+
depois = null;
|
|
215
|
+
}
|
|
216
|
+
const mudancas = [{ arquivo, antes, depois }, { arquivo: reciboPath, antes: bytesRecibo, depois: proximoRecibo }]
|
|
217
|
+
.filter(m => !iguais(m.antes, m.depois));
|
|
218
|
+
return { projeto, host, acao: skill === null ? 'remover' : 'instalar', mudancas };
|
|
219
|
+
}
|
|
220
|
+
/** Revalida o plano inteiro; falha de gravação restaura as mudanças já aplicadas. */
|
|
221
|
+
function aplicarExperiencia(plano) {
|
|
222
|
+
for (const m of plano.mudancas)
|
|
223
|
+
if (!iguais(ler(plano.projeto, m.arquivo), m.antes))
|
|
224
|
+
throw Error('experiencia.plan.stale');
|
|
225
|
+
const aplicadas = [];
|
|
226
|
+
const gravar = (arquivo, conteudo) => {
|
|
227
|
+
caminhoSeguro(plano.projeto, arquivo);
|
|
228
|
+
if (conteudo === null) {
|
|
229
|
+
fs.unlinkSync(arquivo);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
fs.mkdirSync(path.dirname(arquivo), { recursive: true });
|
|
233
|
+
const tmp = arquivo + '.' + (0, node_crypto_1.randomUUID)() + '.tmp';
|
|
234
|
+
try {
|
|
235
|
+
// Arquivo novo segue o umask, como um CLAUDE.md criado à mão.
|
|
236
|
+
const modo = fs.statSync(arquivo, { throwIfNoEntry: false })?.mode ?? 0o666;
|
|
237
|
+
fs.writeFileSync(tmp, conteudo, { flag: 'wx', mode: modo });
|
|
238
|
+
fs.renameSync(tmp, arquivo);
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
if (fs.existsSync(tmp))
|
|
242
|
+
fs.unlinkSync(tmp);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
try {
|
|
246
|
+
for (const m of plano.mudancas) {
|
|
247
|
+
gravar(m.arquivo, m.depois);
|
|
248
|
+
aplicadas.push(m);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch (e) {
|
|
252
|
+
for (const m of aplicadas.reverse())
|
|
253
|
+
gravar(m.arquivo, m.antes);
|
|
254
|
+
throw e;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizarIdioma = normalizarIdioma;
|
|
4
|
+
exports.validarPreferencias = validarPreferencias;
|
|
5
|
+
exports.idiomaDoSistema = idiomaDoSistema;
|
|
6
|
+
exports.resolverExperiencia = resolverExperiencia;
|
|
7
|
+
/** Preferências públicas da experiência. Consulta não grava respostas nem autoria. */
|
|
8
|
+
const horario_1 = require("./horario");
|
|
9
|
+
function normalizarIdioma(valor) {
|
|
10
|
+
if (typeof valor !== 'string' || !valor.trim())
|
|
11
|
+
return undefined;
|
|
12
|
+
try {
|
|
13
|
+
return Intl.getCanonicalLocales(valor.trim())[0];
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function validarPreferencias(valor) {
|
|
20
|
+
if (!valor || typeof valor !== 'object' || Array.isArray(valor))
|
|
21
|
+
throw Error('experiencia.config.invalid: owner deve ser um objeto');
|
|
22
|
+
const v = valor, resultado = {};
|
|
23
|
+
if (v.language !== undefined) {
|
|
24
|
+
const idioma = normalizarIdioma(v.language);
|
|
25
|
+
if (!idioma)
|
|
26
|
+
throw Error('experiencia.config.invalid: owner.language exige locale BCP-47');
|
|
27
|
+
resultado.language = idioma;
|
|
28
|
+
}
|
|
29
|
+
if (v.timezone !== undefined) {
|
|
30
|
+
const fuso = (0, horario_1.normalizarFuso)(v.timezone);
|
|
31
|
+
if (!fuso)
|
|
32
|
+
throw Error('experiencia.config.invalid: owner.timezone exige fuso IANA');
|
|
33
|
+
resultado.timezone = fuso;
|
|
34
|
+
}
|
|
35
|
+
if (v.depth !== undefined) {
|
|
36
|
+
if (v.depth !== 'curta' && v.depth !== 'detalhada')
|
|
37
|
+
throw Error('experiencia.config.invalid: owner.depth exige curta ou detalhada');
|
|
38
|
+
resultado.depth = v.depth;
|
|
39
|
+
}
|
|
40
|
+
if (v.experience !== undefined) {
|
|
41
|
+
if (typeof v.experience !== 'boolean')
|
|
42
|
+
throw Error('experiencia.config.invalid: owner.experience exige booleano');
|
|
43
|
+
resultado.experience = v.experience;
|
|
44
|
+
}
|
|
45
|
+
return resultado;
|
|
46
|
+
}
|
|
47
|
+
/** C, POSIX e und (raiz do ICU) dizem que o sistema não escolheu idioma: vale o padrão. */
|
|
48
|
+
const SEM_IDIOMA = new Set(['c', 'posix', 'und']);
|
|
49
|
+
function idiomaEscolhido(valor) {
|
|
50
|
+
const idioma = normalizarIdioma(valor);
|
|
51
|
+
return idioma && !SEM_IDIOMA.has(idioma.toLowerCase().split('-')[0]) ? idioma : undefined;
|
|
52
|
+
}
|
|
53
|
+
function idiomaDoSistema(env = process.env, doIntl = () => Intl.DateTimeFormat().resolvedOptions().locale) {
|
|
54
|
+
const locale = env.LC_ALL || env.LC_MESSAGES || env.LANG;
|
|
55
|
+
return idiomaEscolhido(locale?.split(/[.@]/)[0].replace(/_/g, '-')) ?? idiomaEscolhido(doIntl()) ?? 'en-US';
|
|
56
|
+
}
|
|
57
|
+
function resolverExperiencia(owner = {}, sistema = { language: idiomaDoSistema(), timezone: (0, horario_1.fusoDoSistema)() }) {
|
|
58
|
+
const p = validarPreferencias(owner);
|
|
59
|
+
const language = p.language ?? normalizarIdioma(sistema.language) ?? 'en-US';
|
|
60
|
+
return {
|
|
61
|
+
language, timezone: p.timezone ?? (0, horario_1.normalizarFuso)(sistema.timezone) ?? 'UTC',
|
|
62
|
+
depth: p.depth ?? 'curta', experience: p.experience ?? true,
|
|
63
|
+
skill: language.toLowerCase().startsWith('pt-') || language === 'pt'
|
|
64
|
+
? 'orchestration-experience-pt-br' : 'orchestration-experience',
|
|
65
|
+
origem: {
|
|
66
|
+
language: p.language === undefined ? 'sistema' : 'manifesto',
|
|
67
|
+
timezone: p.timezone === undefined ? 'sistema' : 'manifesto',
|
|
68
|
+
depth: p.depth === undefined ? 'padrao' : 'manifesto',
|
|
69
|
+
experience: p.experience === undefined ? 'padrao' : 'manifesto',
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
package/dist/fabrica-estado.js
CHANGED
|
@@ -33,11 +33,14 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.PULSACAO_MS = exports.BRANCH_DA_FABRICA = exports.CONTRATO_MAQUINA = void 0;
|
|
36
|
+
exports.LIMIAR_SEM_BATIDA_MS = exports.PULSACAO_MS = exports.DIR_DA_FABRICA = exports.BRANCH_DA_FABRICA = exports.CONTRATO_MAQUINA = void 0;
|
|
37
|
+
exports.despachoDaThread = despachoDaThread;
|
|
38
|
+
exports.runtimeDaThread = runtimeDaThread;
|
|
37
39
|
exports.arquivoDaMaquina = arquivoDaMaquina;
|
|
38
40
|
exports.entregasNaBase = entregasNaBase;
|
|
39
41
|
exports.retratoDaMaquina = retratoDaMaquina;
|
|
40
42
|
exports.assinaturaDoRetrato = assinaturaDoRetrato;
|
|
43
|
+
exports.estadoValido = estadoValido;
|
|
41
44
|
exports.lerFabrica = lerFabrica;
|
|
42
45
|
exports.publicarMaquina = publicarMaquina;
|
|
43
46
|
exports.removerMaquina = removerMaquina;
|
|
@@ -70,21 +73,50 @@ const monitor_lock_1 = require("./monitor-lock");
|
|
|
70
73
|
const board_1 = require("./board");
|
|
71
74
|
const estado_thread_1 = require("./estado-thread");
|
|
72
75
|
const horario_1 = require("./horario");
|
|
76
|
+
const ledger_1 = require("./ledger");
|
|
73
77
|
const modos_1 = require("./modos");
|
|
74
78
|
const orquestracao_1 = require("./orquestracao");
|
|
79
|
+
const parado_no_condutor_1 = require("./parado-no-condutor");
|
|
75
80
|
const roadmap_reservas_1 = require("./roadmap-reservas");
|
|
81
|
+
const thread_1 = require("./thread");
|
|
76
82
|
const util_1 = require("./util");
|
|
77
83
|
const versao_1 = require("./versao");
|
|
78
84
|
exports.CONTRATO_MAQUINA = 'ork.fabrica-maquina/v1';
|
|
79
85
|
exports.BRANCH_DA_FABRICA = 'ork/fabrica-estado';
|
|
80
|
-
|
|
86
|
+
/** O diretorio dos retratos na branch; a leitura sem clone (RM-054) le o mesmo. */
|
|
87
|
+
exports.DIR_DA_FABRICA = 'maquinas';
|
|
88
|
+
const DIR = exports.DIR_DA_FABRICA;
|
|
81
89
|
const PAINEL = 'FABRICA.md';
|
|
82
90
|
const PREFIXO = 'fabrica';
|
|
83
91
|
const TENTATIVAS = 5;
|
|
84
92
|
/** Retrato igual ao ultimo publicado so volta ao remoto depois disto: e o sinal de vida da maquina. */
|
|
85
93
|
exports.PULSACAO_MS = 60 * 60 * 1000;
|
|
94
|
+
/**
|
|
95
|
+
* Maquina sem retrato novo ha mais disto esta sem batida: o mesmo limiar da rede (RM-053, `SEM_BATIDA_MS`).
|
|
96
|
+
* RM-037 (fatia 4): mora aqui, junto da pulsacao, porque o status do roadmap tambem o le; o panorama da
|
|
97
|
+
* rede o reexporta.
|
|
98
|
+
*/
|
|
99
|
+
exports.LIMIAR_SEM_BATIDA_MS = 3 * 60 * 60 * 1000;
|
|
86
100
|
const ARQUIVO_DA_MARCA = 'fabrica-publicada.json';
|
|
87
101
|
const ARQUIVO_DO_LOG = 'fabrica.log';
|
|
102
|
+
/** O trio do ultimo `phase_dispatch` do ledger da thread; tudo `null` quando nao ha despacho legivel. */
|
|
103
|
+
function despachoDaThread(dir) {
|
|
104
|
+
let ultimo;
|
|
105
|
+
try {
|
|
106
|
+
ultimo = [...(0, ledger_1.lerLedger)(dir)].reverse().find((e) => e.tipo === ledger_1.TIPOS_DE_EVENTO.faseDespachada);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
ultimo = undefined;
|
|
110
|
+
}
|
|
111
|
+
const campo = (v) => (typeof v === 'string' && v.trim() ? curto(v, 40) : null);
|
|
112
|
+
return { runtime: campo(ultimo?.runtime), modelo: campo(ultimo?.model), esforco: campo(ultimo?.effort) };
|
|
113
|
+
}
|
|
114
|
+
/** `claude-bg opus/xhigh`: o runtime com o modelo e o esforco, para as colunas do texto. */
|
|
115
|
+
function runtimeDaThread(t) {
|
|
116
|
+
if (!t.runtime && !t.modelo)
|
|
117
|
+
return '-';
|
|
118
|
+
return [t.runtime ?? '?', [t.modelo, t.esforco].filter(Boolean).join('/')].filter(Boolean).join(' ');
|
|
119
|
+
}
|
|
88
120
|
const curto = (texto, teto = 120) => {
|
|
89
121
|
const limpo = String(texto ?? '').replace(/\s+/g, ' ').trim();
|
|
90
122
|
return limpo.length > teto ? `${limpo.slice(0, teto - 3)}...` : limpo;
|
|
@@ -101,7 +133,11 @@ function arquivoDaMaquina(maquina) {
|
|
|
101
133
|
* entrega usa, o mesmo fato que o `ork docs sincronizar` le. Um `git log` so, para todas.
|
|
102
134
|
*/
|
|
103
135
|
function entregasNaBase(raiz, base, remoto = 'origin') {
|
|
104
|
-
|
|
136
|
+
// A base vem do manifesto: a ref vai sempre qualificada (`refs/...`), e um valor como
|
|
137
|
+
// `--output=<arquivo>` nunca vira opcao do `git log`, em qualquer versao do git (RM-054, CHECK).
|
|
138
|
+
// Na ordem: a copia remota da base, a branch local, e a base escrita como remota (`origin/main`).
|
|
139
|
+
const candidatas = [`refs/remotes/${remoto}/${base}`, `refs/heads/${base}`, `refs/remotes/${base}`];
|
|
140
|
+
const ref = candidatas.find((c) => (0, branch_de_estado_1.git)(raiz, ['rev-parse', '--verify', '--quiet', c]).ok) ?? candidatas[1];
|
|
105
141
|
// Regex basica do git: o `(` e literal.
|
|
106
142
|
const r = (0, branch_de_estado_1.git)(raiz, ['log', ref, '--format=%h%x09%s', '--grep=^ship(']);
|
|
107
143
|
const entregas = new Map();
|
|
@@ -125,16 +161,28 @@ function retratoDaMaquina(carregado, opcoes = {}) {
|
|
|
125
161
|
const linhas = new Map(monitor.linhas.map((l) => [l.thread, l]));
|
|
126
162
|
const entregas = entregasNaBase(raiz, carregado.manifesto.worktree.base_branch, remoto);
|
|
127
163
|
const itemDaThread = new Map((0, roadmap_reservas_1.reservasLocais)(raiz, remoto).filter((r) => r.thread).map((r) => [r.thread, r.item]));
|
|
128
|
-
const threads = itens.map(({ thread: t }) => {
|
|
129
|
-
const pausa = linhas.get(t.id)?.pausas[0];
|
|
164
|
+
const threads = itens.map(({ thread: t, raiz: raizDoPerfil }) => {
|
|
130
165
|
const entregue = entregas.get(t.id) ?? null;
|
|
131
|
-
|
|
166
|
+
// RM-037 (fatia 4): o fim de turno sem pergunta e do condutor, e as outras maquinas nao o leem como espera
|
|
167
|
+
// do dono. Sai so a pausa que o observador abriu nele; outra pausa da thread continua contando.
|
|
168
|
+
let espera = null;
|
|
169
|
+
try {
|
|
170
|
+
espera = (0, parado_no_condutor_1.esperaDoCondutor)(t, (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raizDoPerfil, t.id)), quando);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
espera = null;
|
|
174
|
+
}
|
|
175
|
+
const pausas = (linhas.get(t.id)?.pausas ?? []).filter((p) => !(espera && p.motivo === 'human.pending' && p.fonte === 'ledger' &&
|
|
176
|
+
p.fase === (espera.fase ?? t.faseAtual)));
|
|
177
|
+
const pausa = pausas[0];
|
|
178
|
+
const esperaVoce = !entregue && pausas.some((p) => p.sessaoViva !== true);
|
|
132
179
|
return {
|
|
133
180
|
id: t.id, nome: curto(t.nome, 100), modo: (0, modos_1.tagDoModo)(t.modo), fase: t.faseAtual, status: t.status,
|
|
134
181
|
roadmap: t.roadmap ?? itemDaThread.get(t.id) ?? null, branch: t.base?.branch ?? null,
|
|
135
182
|
atualizadaEm: t.atualizadaEm ?? null, entregue,
|
|
136
183
|
esperaVoce, pergunta: esperaVoce && pausa ? curto(pausa.pausaSobre || pausa.detalhe) : null,
|
|
137
184
|
paradaDesde: esperaVoce && pausa ? pausa.desdeEm : null,
|
|
185
|
+
...despachoDaThread((0, thread_1.dirThread)(raizDoPerfil, t.id)),
|
|
138
186
|
};
|
|
139
187
|
}).sort((a, b) => a.id.localeCompare(b.id));
|
|
140
188
|
const eu = (0, roadmap_reservas_1.quemSouEu)(raiz, { por: opcoes.por, maquina: opcoes.maquina });
|
|
@@ -146,6 +194,7 @@ function assinaturaDoRetrato(e) {
|
|
|
146
194
|
const { publicadoEm: _publicadoEm, ...resto } = e;
|
|
147
195
|
return (0, node_crypto_1.createHash)('sha256').update(JSON.stringify(resto)).digest('hex');
|
|
148
196
|
}
|
|
197
|
+
/** O retrato so vale inteiro: e o filtro da leitura pelo clone e pela forja (RM-054). */
|
|
149
198
|
function estadoValido(bruto) {
|
|
150
199
|
const e = bruto;
|
|
151
200
|
return !!e && e.contrato === exports.CONTRATO_MAQUINA && typeof e.maquina === 'string' && !!e.maquina.trim() &&
|
|
@@ -278,8 +327,8 @@ function blocoDaMaquina(m, eu) {
|
|
|
278
327
|
`${vivas.length} thread(s) ativa(s)` + (entregues ? `, ${entregues} entregue(s) sem MASTER` : '');
|
|
279
328
|
if (vivas.length === 0)
|
|
280
329
|
return [cabeca];
|
|
281
|
-
return [cabeca, ...tabela(['THREAD', 'MODO', 'FASE', 'ITEM', 'ESPERA VOCE'], vivas.map((t) => [
|
|
282
|
-
t.id, t.modo, t.fase, t.roadmap ?? '-', t.esperaVoce ? `sim: ${curto(t.pergunta ?? 'veredito', 40)}` : '-',
|
|
330
|
+
return [cabeca, ...tabela(['THREAD', 'MODO', 'FASE', 'RUNTIME', 'ITEM', 'ESPERA VOCE'], vivas.map((t) => [
|
|
331
|
+
t.id, t.modo, t.fase, runtimeDaThread(t), t.roadmap ?? '-', t.esperaVoce ? `sim: ${curto(t.pergunta ?? 'veredito', 40)}` : '-',
|
|
283
332
|
]))];
|
|
284
333
|
}
|
|
285
334
|
/** `ork fabrica`: todas as maquinas. */
|
|
@@ -315,15 +364,17 @@ function painelDaFabricaEmMarkdown(maquinas) {
|
|
|
315
364
|
'O que cada máquina está conduzindo agora. Gerado pelo `ork fabrica publicar`; não edite à mão.',
|
|
316
365
|
'Antes de pegar um item do roadmap: `ork roadmap reservas`.',
|
|
317
366
|
'',
|
|
318
|
-
'| Máquina | Thread | Modo | Fase | Item | Espera você | Publicado |',
|
|
319
|
-
'| --- | --- | --- | --- | --- | --- | --- |',
|
|
367
|
+
'| Máquina | Thread | Modo | Fase | Runtime | Item | Espera você | Publicado |',
|
|
368
|
+
'| --- | --- | --- | --- | --- | --- | --- | --- |',
|
|
320
369
|
];
|
|
321
370
|
const vivas = maquinas.flatMap((m) => ativas(m).map((t) => ({ m, t })));
|
|
322
371
|
if (vivas.length === 0)
|
|
323
|
-
linhas.push('| — | nenhuma thread ativa | — | — | — | — | — |');
|
|
372
|
+
linhas.push('| — | nenhuma thread ativa | — | — | | — | — | — |');
|
|
324
373
|
for (const { m, t } of vivas) {
|
|
325
374
|
const pergunta = t.esperaVoce ? `sim: ${curto(t.pergunta ?? 'veredito', 60).replace(/\|/g, '/')}` : '—';
|
|
326
|
-
|
|
375
|
+
// RM-037 (sugestao 6 do CHECK 1): a celula sem despacho diz isso, sem travessao novo.
|
|
376
|
+
const runtime = t.runtime || t.modelo ? runtimeDaThread(t).replace(/\|/g, '/') : 'sem despacho';
|
|
377
|
+
linhas.push(`| ${m.maquina} | ${t.id} | ${t.modo} | ${t.fase} | ${runtime} | ${t.roadmap ?? '—'} | ${pergunta} | ${(0, horario_1.formatarDataHora)(m.publicadoEm)} |`);
|
|
327
378
|
}
|
|
328
379
|
return [...linhas, '', (0, horario_1.legendaDoFuso)(), ''].join('\n');
|
|
329
380
|
}
|