@orchestrator-claude/definitions 3.37.1 → 3.37.3

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.
@@ -0,0 +1,302 @@
1
+ import { createRequire as __createRequireShim } from 'node:module'; const require = __createRequireShim(import.meta.url);
2
+
3
+ // hooks/lib/api-client.ts
4
+ import { readFileSync, writeFileSync, mkdirSync, statSync, unlinkSync, readdirSync } from "node:fs";
5
+ import { join as join2 } from "node:path";
6
+ import { tmpdir } from "node:os";
7
+ import { createHash } from "node:crypto";
8
+
9
+ // ../project-root-resolver/dist/index.js
10
+ import { existsSync } from "node:fs";
11
+ import { dirname, join } from "node:path";
12
+ function hasProjectRootMarker(candidate, sentinel) {
13
+ return existsSync(join(candidate, ".git")) || existsSync(join(candidate, "package.json")) || existsSync(join(candidate, sentinel, ".root"));
14
+ }
15
+ function resolveProjectRoot(cwd, sentinel = ".orchestrator") {
16
+ const candidates = [];
17
+ let currentDir = cwd;
18
+ for (; ; ) {
19
+ if (existsSync(join(currentDir, sentinel))) {
20
+ candidates.push(currentDir);
21
+ }
22
+ const parentDir = dirname(currentDir);
23
+ if (parentDir === currentDir) {
24
+ break;
25
+ }
26
+ currentDir = parentDir;
27
+ }
28
+ const [innermost] = candidates;
29
+ if (innermost === void 0) {
30
+ return null;
31
+ }
32
+ for (const candidate of candidates) {
33
+ if (hasProjectRootMarker(candidate, sentinel)) {
34
+ return candidate;
35
+ }
36
+ }
37
+ return innermost;
38
+ }
39
+
40
+ // hooks/lib/api-client.ts
41
+ function resolveFallbackStateDir(cwd) {
42
+ const cwdHash = createHash("sha256").update(cwd).digest("hex").slice(0, 16);
43
+ return join2(tmpdir(), "orchestrator-hook-fallback", cwdHash);
44
+ }
45
+ var PROJECT_ROOT = (() => {
46
+ const resolved = resolveHookProjectRoot(process.cwd());
47
+ if (resolved !== null && hasProjectRootMarker(resolved, ".orchestrator")) {
48
+ return resolved;
49
+ }
50
+ return null;
51
+ })();
52
+ var STATE_DIR_OVERRIDE = process.env.ORCHESTRATOR_STATE_DIR;
53
+ var STATE_DIR = STATE_DIR_OVERRIDE !== void 0 && STATE_DIR_OVERRIDE !== "" ? STATE_DIR_OVERRIDE : PROJECT_ROOT !== null ? join2(PROJECT_ROOT, ".orchestrator", ".state") : resolveFallbackStateDir(process.cwd());
54
+ var JWT_CACHE = join2(STATE_DIR, "jwt-token");
55
+ var DEBUG_LOG = join2(STATE_DIR, "hook-debug.log");
56
+ mkdirSync(STATE_DIR, { recursive: true });
57
+ if ((STATE_DIR_OVERRIDE === void 0 || STATE_DIR_OVERRIDE === "") && PROJECT_ROOT === null) {
58
+ log(
59
+ "API-CLIENT",
60
+ `PROJECT_ROOT: no marker-verified ".orchestrator" project root found above ${process.cwd()} \u2014 falling back to isolated state dir ${STATE_DIR} (RF-003)`
61
+ );
62
+ }
63
+ var API_URL = process.env.ORCHESTRATOR_API_URL || "http://localhost:3001";
64
+ var AUTH_EMAIL = process.env.ORCHESTRATOR_ADMIN_EMAIL || process.env.ORCHESTRATOR_AUTH_EMAIL || "system@orchestrator.local";
65
+ var AUTH_PASSWORD = process.env.ORCHESTRATOR_ADMIN_PASSWORD || process.env.ORCHESTRATOR_AUTH_PASSWORD || "system123";
66
+ var PROJECT_ID = process.env.ORCHESTRATOR_PROJECT_ID || "";
67
+ function resolveHookProjectRoot(cwd) {
68
+ return resolveProjectRoot(cwd);
69
+ }
70
+ function log(prefix, msg) {
71
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
72
+ try {
73
+ const line = `[${ts}] ${prefix}: ${msg}
74
+ `;
75
+ writeFileSync(DEBUG_LOG, line, { flag: "a" });
76
+ } catch {
77
+ }
78
+ }
79
+ var LAST_COMPLETED_FILE = join2(STATE_DIR, "last-completed-workflow.json");
80
+ var LAST_COMPLETED_TTL_MS = 60 * 60 * 1e3;
81
+ var INVOCATIONS_DIR = join2(STATE_DIR, "invocations");
82
+ var INVOCATION_STATE_TTL_MS = 4 * 60 * 60 * 1e3;
83
+ var DISPATCH_INTENT_DIR = join2(STATE_DIR, "dispatch-intent");
84
+ async function readStdin() {
85
+ const chunks = [];
86
+ for await (const chunk of process.stdin) {
87
+ chunks.push(chunk);
88
+ }
89
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
90
+ if (!raw) return {};
91
+ try {
92
+ return JSON.parse(raw);
93
+ } catch {
94
+ return {};
95
+ }
96
+ }
97
+ function getField(obj, path) {
98
+ const parts = path.split(".");
99
+ let current = obj;
100
+ for (const part of parts) {
101
+ if (current == null || typeof current !== "object") return "";
102
+ current = current[part];
103
+ }
104
+ return current != null ? String(current) : "";
105
+ }
106
+
107
+ // hooks/security-gate-honest.ts
108
+ import { execFileSync } from "node:child_process";
109
+
110
+ // hooks/lib/security-gate-core.ts
111
+ var L2_LATENCY_FLOOR_MS = 400;
112
+ var L1_COVERAGE = "literais de credencial no diff (chaves de provedor, tokens, blocos PEM, URLs com senha)";
113
+ var SECRET_RULES = [
114
+ { rule: "aws-access-key-id", re: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/ },
115
+ { rule: "anthropic-api-key", re: /\bsk-ant-[A-Za-z0-9_-]{20,}/ },
116
+ { rule: "openai-api-key", re: /\bsk-(?:proj-)?[A-Za-z0-9]{32,}\b/ },
117
+ { rule: "github-token", re: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/ },
118
+ { rule: "slack-token", re: /\bxox[abprs]-[A-Za-z0-9-]{10,}/ },
119
+ { rule: "google-api-key", re: /\bAIza[0-9A-Za-z_-]{35}\b/ },
120
+ { rule: "npm-token", re: /\bnpm_[A-Za-z0-9]{36}\b/ },
121
+ { rule: "private-key-block", re: /-----BEGIN (?:RSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/ },
122
+ { rule: "jwt", re: /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/ },
123
+ { rule: "url-with-password", re: /\b[a-z][a-z0-9+.-]*:\/\/[^\s:@/]+:[^\s:@/]{6,}@/i }
124
+ ];
125
+ var PLACEHOLDER = /(?:x{6,}|\.{3,}|<[^>]{2,}>|\$\{|EXAMPLE|PLACEHOLDER|YOUR_|CHANGEME|REDACTED|FAKE|DUMMY|\bTODO\b)/i;
126
+ function maskEvidence(s) {
127
+ const t = s.trim().slice(0, 80);
128
+ return t.length <= 12 ? "***" : `${t.slice(0, 6)}\u2026${t.slice(-4)} (${String(t.length)} ch)`;
129
+ }
130
+ function scanDiff(diff) {
131
+ const out = [];
132
+ let file = "(desconhecido)";
133
+ let lineNo = 0;
134
+ for (const raw of diff.split("\n")) {
135
+ if (raw.startsWith("+++ b/")) {
136
+ file = raw.slice(6);
137
+ lineNo = 0;
138
+ continue;
139
+ }
140
+ if (raw.startsWith("@@")) {
141
+ const m = /\+(\d+)/.exec(raw);
142
+ lineNo = m ? parseInt(m[1], 10) - 1 : 0;
143
+ continue;
144
+ }
145
+ if (!raw.startsWith("+") || raw.startsWith("+++")) continue;
146
+ lineNo += 1;
147
+ const content = raw.slice(1);
148
+ if (PLACEHOLDER.test(content)) continue;
149
+ for (const { rule, re } of SECRET_RULES) {
150
+ const hit = re.exec(content);
151
+ if (hit) out.push({ rule, file, line: lineNo, evidence: maskEvidence(hit[0]) });
152
+ }
153
+ }
154
+ return out;
155
+ }
156
+ function isCommitCommand(command) {
157
+ return /git\s+(?:-\S+(?:\s+\S+)?\s+)*commit\b/.test(command) && !/--dry-run\b/.test(command);
158
+ }
159
+ function evaluate(deps) {
160
+ const verdicts = [];
161
+ let src;
162
+ try {
163
+ src = deps.getDiff();
164
+ } catch (e) {
165
+ const reason = `n\xE3o consegui ler o diff do commit (${e.message.slice(0, 80)})`;
166
+ return [
167
+ { state: "not-analyzed", layer: "L1", reason },
168
+ { state: "not-analyzed", layer: "L2", reason }
169
+ ];
170
+ }
171
+ const diff = src.text;
172
+ if (!diff.trim()) {
173
+ const reason = `o diff veio vazio via ${src.how} \u2014 nada para analisar. Isto N\xC3O \xE9 o mesmo que limpo`;
174
+ return [
175
+ { state: "not-analyzed", layer: "L1", reason },
176
+ { state: "not-analyzed", layer: "L2", reason }
177
+ ];
178
+ }
179
+ const hits = scanDiff(diff);
180
+ const cover = `${L1_COVERAGE} \xB7 via ${src.how}`;
181
+ verdicts.push(
182
+ hits.length ? { state: "findings", layer: "L1", detail: cover, hits } : { state: "clean", layer: "L1", detail: cover }
183
+ );
184
+ const sem = deps.runSemantic(diff);
185
+ if (sem === null) {
186
+ verdicts.push({ state: "not-analyzed", layer: "L2", reason: deps.l2UnavailableReason() });
187
+ } else if (sem.elapsedMs < L2_LATENCY_FLOOR_MS) {
188
+ verdicts.push({
189
+ state: "not-analyzed",
190
+ layer: "L2",
191
+ reason: `devolveu veredito em ${String(sem.elapsedMs)} ms, abaixo do piso de ${String(L2_LATENCY_FLOOR_MS)} ms \u2014 tratado como fallback, n\xE3o como an\xE1lise`
192
+ });
193
+ } else {
194
+ verdicts.push(
195
+ sem.hits.length ? { state: "findings", layer: "L2", detail: "an\xE1lise sem\xE2ntica", hits: sem.hits } : { state: "clean", layer: "L2", detail: "an\xE1lise sem\xE2ntica" }
196
+ );
197
+ }
198
+ return verdicts;
199
+ }
200
+ function renderShort(verdicts) {
201
+ const findings = verdicts.filter((v) => v.state === "findings");
202
+ if (findings.length) {
203
+ const n = findings.reduce((a, v) => a + (v.state === "findings" ? v.hits.length : 0), 0);
204
+ const layers = findings.map((v) => v.layer).join("+");
205
+ return `SECURITY-GATE: ${String(n)} achado(s) em ${layers} \u2014 confira antes de publicar`;
206
+ }
207
+ const analyzed = verdicts.filter((v) => v.state !== "not-analyzed").map((v) => v.layer);
208
+ const skipped = verdicts.filter((v) => v.state === "not-analyzed").map((v) => v.layer);
209
+ if (!analyzed.length) {
210
+ return 'SECURITY-GATE: NENHUMA camada analisou o commit \u2014 isto N\xC3O \xE9 "limpo", \xE9 aus\xEAncia de an\xE1lise';
211
+ }
212
+ if (skipped.length) {
213
+ return `SECURITY-GATE: cobertura PARCIAL \u2014 ${analyzed.join("+")} analisou, ${skipped.join("+")} n\xE3o`;
214
+ }
215
+ return `SECURITY-GATE: ${analyzed.join("+")} analisou, sem achados`;
216
+ }
217
+ function render(verdicts) {
218
+ const lines = ["[SECURITY-GATE] Revis\xE3o do commit \u2014 veredito por camada:"];
219
+ for (const v of verdicts) {
220
+ if (v.state === "clean") {
221
+ lines.push(` ${v.layer} ANALISADO \u2014 LIMPO \xB7 cobertura: ${v.detail}`);
222
+ } else if (v.state === "findings") {
223
+ lines.push(
224
+ ` ${v.layer} ANALISADO \u2014 ${String(v.hits.length)} ACHADO(S) \xB7 cobertura: ${v.detail}`
225
+ );
226
+ for (const h of v.hits.slice(0, 10)) {
227
+ lines.push(` ${h.rule} \xB7 ${h.file}:${String(h.line)} \xB7 ${h.evidence}`);
228
+ }
229
+ if (v.hits.length > 10) lines.push(` \u2026 e mais ${String(v.hits.length - 10)}`);
230
+ } else {
231
+ lines.push(` ${v.layer} N\xC3O ANALISEI \u2014 ${v.reason}`);
232
+ }
233
+ }
234
+ const analyzed = verdicts.filter((v) => v.state !== "not-analyzed").map((v) => v.layer);
235
+ const skipped = verdicts.filter((v) => v.state === "not-analyzed").map((v) => v.layer);
236
+ if (!analyzed.length) {
237
+ lines.push(
238
+ ' \u26A0 NENHUMA camada analisou este commit. Isto N\xC3O \xE9 "limpo" \u2014 \xE9 aus\xEAncia de informa\xE7\xE3o.'
239
+ );
240
+ } else if (skipped.length) {
241
+ lines.push(
242
+ ` \u26A0 Cobertura PARCIAL: ${analyzed.join("+")} rodou, ${skipped.join("+")} n\xE3o. O que n\xE3o foi analisado n\xE3o est\xE1 limpo \u2014 est\xE1 por analisar.`
243
+ );
244
+ }
245
+ if (verdicts.some((v) => v.state === "findings")) {
246
+ lines.push(" \u2192 Confira antes de publicar. O commit local j\xE1 existe; nada foi bloqueado.");
247
+ }
248
+ return lines.join("\n");
249
+ }
250
+
251
+ // hooks/security-gate-honest.ts
252
+ var HOOK = "SECURITY-GATE";
253
+ var defaultDeps = {
254
+ getDiff: () => {
255
+ const git = (args2) => execFileSync("git", args2, { encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: 1e4 });
256
+ const parents = git(["rev-list", "--parents", "-n", "1", "HEAD"]).trim().split(/\s+/).length - 1;
257
+ const args = parents > 1 ? ["show", "--format=", "--unified=0", "-m", "--first-parent", "HEAD"] : ["show", "--format=", "--unified=0", "HEAD"];
258
+ const how = parents > 1 ? `git show -m --first-parent HEAD (merge de ${parents} pais)` : "git show HEAD";
259
+ return { text: git(args), how };
260
+ },
261
+ // Ficha 3 item (d): o gate é honesto ANTES de voltar a funcionar. Enquanto a
262
+ // credencial/SDK não forem decididos pelo PM, L2 declara ausência — nunca
263
+ // finge limpeza.
264
+ runSemantic: () => null,
265
+ l2UnavailableReason: () => "camada sem\xE2ntica sem credencial decidida (ficha 3 da S505, item d) \u2014 nenhuma chamada foi tentada, nada foi analisado nesta camada"
266
+ };
267
+ async function mainWithDeps(deps, stdinFn = readStdin) {
268
+ const stdin = await stdinFn();
269
+ const command = getField(stdin, "tool_input.command") || getField(stdin, "command") || "";
270
+ if (!isCommitCommand(command)) return;
271
+ const verdicts = evaluate(deps);
272
+ const text = render(verdicts);
273
+ log(HOOK, text.replace(/\n/g, " | "));
274
+ console.log(
275
+ JSON.stringify({
276
+ systemMessage: renderShort(verdicts),
277
+ hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: text }
278
+ })
279
+ );
280
+ }
281
+ async function main() {
282
+ await mainWithDeps(defaultDeps);
283
+ }
284
+ main().catch((e) => {
285
+ try {
286
+ log(HOOK, `N\xC3O ANALISEI \u2014 o pr\xF3prio gate falhou: ${e.message.slice(0, 120)}`);
287
+ } catch {
288
+ }
289
+ process.exit(0);
290
+ });
291
+ export {
292
+ L1_COVERAGE,
293
+ L2_LATENCY_FLOOR_MS,
294
+ SECRET_RULES,
295
+ evaluate,
296
+ isCommitCommand,
297
+ mainWithDeps,
298
+ maskEvidence,
299
+ render,
300
+ renderShort,
301
+ scanDiff
302
+ };
@@ -0,0 +1,148 @@
1
+ /**
2
+ * security-gate-honest.ts — PostToolUse[Bash] hook
3
+ *
4
+ * Substituto honesto do `security-guidance@claude-plugins-official` para a
5
+ * revisão de commits (ficha 3 da S505; decisão do PM: trocar por algo que
6
+ * FALHE ALTO).
7
+ *
8
+ * ─── O DEFEITO QUE ESTE HOOK EXISTE PARA NÃO COMETER ──────────────────────
9
+ * Medido na S505, `security_reminder_hook.py:1418`: quando a chamada de API
10
+ * falha, o Commit review emite `no security issues found` e sai com 0 — três
11
+ * MILISSEGUNDOS depois do erro. A informação de que nada foi analisado está
12
+ * disponível ali (`llm._last_call_claude_http_error`) e é enviada para a
13
+ * telemetria como `api_error`, mas NÃO muda o veredito mostrado.
14
+ *
15
+ * O mesmo arquivo faz a coisa certa 667 linhas abaixo: o Stop hook (:2085)
16
+ * testa `_last_call_claude_http_error` ANTES do veredito e, quando há erro,
17
+ * chama `restore_unreviewed_stop_state` em vez de declarar limpo. Duas
18
+ * rotinas, a mesma variável, duas condutas — e só uma é honesta.
19
+ *
20
+ * ─── A REGRA, E É A ÚNICA QUE IMPORTA ─────────────────────────────────────
21
+ * AUSÊNCIA DE ANÁLISE NUNCA VIRA VEREDITO POSITIVO. Um gate que degrada para
22
+ * "limpo" é pior que gate nenhum: gate nenhum é uma ausência que se percebe;
23
+ * "no security issues found" é uma afirmação que se cita depois.
24
+ *
25
+ * Três estados, sempre explícitos, nunca implícitos:
26
+ * ANALISADO — LIMPO ......... a camada rodou e não achou nada
27
+ * ANALISADO — ACHADOS ....... a camada rodou e achou
28
+ * NÃO ANALISEI — <motivo> ... a camada não rodou, e o motivo vai junto
29
+ *
30
+ * E a cobertura é declarada em toda saída: dizer "limpo" sem dizer "limpo
31
+ * DE QUÊ" é a mesma família de defeito, um degrau mais sutil.
32
+ *
33
+ * ─── CAMADAS ──────────────────────────────────────────────────────────────
34
+ * L1 · varredura local de segredos (regex sobre o diff) — SEM API, sempre
35
+ * disponível, cobertura estreita e declarada.
36
+ * L2 · análise semântica por LLM — hoje NÃO DISPONÍVEL (sem credencial,
37
+ * ver §credencial na ficha). O gancho e o piso de latência ficam
38
+ * escritos para quando a credencial for decidida; enquanto isso L2
39
+ * reporta NÃO ANALISEI, que é o comportamento correto.
40
+ *
41
+ * ─── PISO DE LATÊNCIA (segunda guarda, achado da S505) ────────────────────
42
+ * Análise semântica de um diff não fecha em milissegundos. Quando L2 existir
43
+ * e devolver veredito em menos de L2_LATENCY_FLOOR_MS, o resultado é tratado
44
+ * como fallback, não como análise — vira NÃO ANALISEI. É guarda secundária:
45
+ * a primária é testar o erro explicitamente, como o Stop hook do plugin faz.
46
+ *
47
+ * NUNCA BLOQUEIA. Emite contexto. O objetivo é informar com honestidade, não
48
+ * impedir o commit — bloquear seria decisão de outra ficha.
49
+ */
50
+
51
+ import { log, readStdin, getField } from './lib/api-client.js';
52
+ import { execFileSync } from 'node:child_process';
53
+ import {
54
+ evaluate,
55
+ render,
56
+ renderShort,
57
+ isCommitCommand,
58
+ type GateDeps,
59
+ } from './lib/security-gate-core.js';
60
+
61
+ export {
62
+ L1_COVERAGE,
63
+ L2_LATENCY_FLOOR_MS,
64
+ SECRET_RULES,
65
+ maskEvidence,
66
+ scanDiff,
67
+ isCommitCommand,
68
+ evaluate,
69
+ render,
70
+ renderShort,
71
+ type Finding,
72
+ type Verdict,
73
+ type GateDeps,
74
+ } from './lib/security-gate-core.js';
75
+
76
+ const HOOK = 'SECURITY-GATE';
77
+
78
+ // ─────────────────────────────────────────────────────────────
79
+ // Produção
80
+ // ─────────────────────────────────────────────────────────────
81
+
82
+ const defaultDeps: GateDeps = {
83
+ getDiff: () => {
84
+ const git = (args: string[]) =>
85
+ execFileSync('git', args, { encoding: 'utf8', maxBuffer: 8 * 1024 * 1024, timeout: 10_000 });
86
+ // Um merge commit devolve diff VAZIO em `git show` sem `-m` (medido S505: o
87
+ // HEAD de merge dava 0 linhas e um commit normal, 159). Sem esta distinção o
88
+ // gate diria "não analisei" para todo merge, misturando-o com falha real.
89
+ const parents =
90
+ git(['rev-list', '--parents', '-n', '1', 'HEAD']).trim().split(/\s+/).length - 1;
91
+ const args =
92
+ parents > 1
93
+ ? ['show', '--format=', '--unified=0', '-m', '--first-parent', 'HEAD']
94
+ : ['show', '--format=', '--unified=0', 'HEAD'];
95
+ const how =
96
+ parents > 1 ? `git show -m --first-parent HEAD (merge de ${parents} pais)` : 'git show HEAD';
97
+ return { text: git(args), how };
98
+ },
99
+ // Ficha 3 item (d): o gate é honesto ANTES de voltar a funcionar. Enquanto a
100
+ // credencial/SDK não forem decididos pelo PM, L2 declara ausência — nunca
101
+ // finge limpeza.
102
+ runSemantic: () => null,
103
+ l2UnavailableReason: () =>
104
+ 'camada semântica sem credencial decidida (ficha 3 da S505, item d) — ' +
105
+ 'nenhuma chamada foi tentada, nada foi analisado nesta camada',
106
+ };
107
+
108
+ export async function mainWithDeps(deps: GateDeps, stdinFn = readStdin): Promise<void> {
109
+ const stdin = await stdinFn();
110
+ const command =
111
+ (getField(stdin, 'tool_input.command') as string | undefined) ||
112
+ (getField(stdin, 'command') as string | undefined) ||
113
+ '';
114
+
115
+ if (!isCommitCommand(command)) return;
116
+
117
+ const verdicts = evaluate(deps);
118
+ const text = render(verdicts);
119
+ log(HOOK, text.replace(/\n/g, ' | '));
120
+
121
+ // DOIS canais, e a distinção é a ficha inteira (achado do Manager, S505):
122
+ // additionalContext → só o MODELO lê;
123
+ // systemMessage → a PESSOA vê, sem bloquear.
124
+ // Um gate que "falha alto" apenas para o modelo não falha alto para ninguém —
125
+ // é o mesmo efeito prático do plugin que este substitui. `outputContext` da
126
+ // casa não emite systemMessage, então o JSON sai montado aqui.
127
+ console.log(
128
+ JSON.stringify({
129
+ systemMessage: renderShort(verdicts),
130
+ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: text },
131
+ })
132
+ );
133
+ }
134
+
135
+ async function main(): Promise<void> {
136
+ await mainWithDeps(defaultDeps);
137
+ }
138
+
139
+ // Falha do próprio gate também não pode virar silêncio-que-parece-limpo:
140
+ // o motivo vai para o log, e a saída 0 apenas não bloqueia o fluxo.
141
+ main().catch((e: unknown) => {
142
+ try {
143
+ log(HOOK, `NÃO ANALISEI — o próprio gate falhou: ${(e as Error).message.slice(0, 120)}`);
144
+ } catch {
145
+ /* nada a fazer; nunca derrubar a sessão por causa do gate */
146
+ }
147
+ process.exit(0);
148
+ });
@@ -7862,6 +7862,10 @@ var MEMORY_TYPES = ["feedback", "project", "reference", "user"];
7862
7862
  function truncateSummary(summary, cap) {
7863
7863
  return summary.length > cap ? `${summary.slice(0, cap)}\u2026` : summary;
7864
7864
  }
7865
+ function hasRenderableSummary(entry) {
7866
+ return typeof entry.summary === "string";
7867
+ }
7868
+ var MEMORY_SKIPPED_WARNING_PREFIX = "- [AVISO] ";
7865
7869
  function countByType(list) {
7866
7870
  const counts = {};
7867
7871
  for (const e of list) {
@@ -7870,31 +7874,35 @@ function countByType(list) {
7870
7874
  return counts;
7871
7875
  }
7872
7876
  function formatMemoryContext(selected, allEntries) {
7873
- if (selected.length === 0) return "";
7874
- const selectedCounts = countByType(selected);
7877
+ const renderable = selected.filter(hasRenderableSummary);
7878
+ const skipped = selected.filter((e) => !hasRenderableSummary(e));
7879
+ const warning = skipped.length > 0 ? `${MEMORY_SKIPPED_WARNING_PREFIX}${String(skipped.length)} entrada(s) sem 'summary:' pulada(s) \u2014 conserte o \xEDndice: ${skipped.map((e) => e.file).join(", ")}` : "";
7880
+ if (renderable.length === 0) return warning ? `[MEMORY-CONTEXT] 0 mem\xF3rias
7881
+ ${warning}` : "";
7882
+ const selectedCounts = countByType(renderable);
7875
7883
  const totalCounts = countByType(allEntries);
7876
7884
  const perTypeSummary = MEMORY_TYPES.map(
7877
7885
  (t) => `${t}: ${selectedCounts[t] ?? 0}/${totalCounts[t] ?? 0}`
7878
7886
  ).join(", ");
7879
- const headerBase = `[MEMORY-CONTEXT] ${selected.length} de ${allEntries.length} mem\xF3rias (${perTypeSummary}); crit\xE9rio: cota m\xEDnima de 2 por tipo n\xE3o-feedback + prioridade feedback\u2192rec\xEAncia no restante`;
7880
- const availableForLines = AGGREGATE_CHAR_BUDGET - headerBase.length - SUFFIX_RESERVE;
7887
+ const headerBase = `[MEMORY-CONTEXT] ${renderable.length} de ${allEntries.length} mem\xF3rias (${perTypeSummary}); crit\xE9rio: cota m\xEDnima de 2 por tipo n\xE3o-feedback + prioridade feedback\u2192rec\xEAncia no restante`;
7888
+ const availableForLines = AGGREGATE_CHAR_BUDGET - headerBase.length - SUFFIX_RESERVE - (warning ? warning.length + 1 : 0);
7881
7889
  const lines = [];
7882
7890
  let used = 0;
7883
7891
  let droppedByBudget = 0;
7884
- for (let i = 0; i < selected.length; i++) {
7885
- const entry = selected[i];
7892
+ for (let i = 0; i < renderable.length; i++) {
7893
+ const entry = renderable[i];
7886
7894
  const name = entry.file.replace(/\.md$/, "");
7887
7895
  const truncated = truncateSummary(entry.summary, PER_ENTRY_SUMMARY_CHAR_CAP);
7888
7896
  const line = `- [${entry.type}] ${name}: ${truncated}`;
7889
7897
  if (used + line.length + 1 > availableForLines) {
7890
- droppedByBudget = selected.length - i;
7898
+ droppedByBudget = renderable.length - i;
7891
7899
  break;
7892
7900
  }
7893
7901
  lines.push(line);
7894
7902
  used += line.length + 1;
7895
7903
  }
7896
7904
  const header = droppedByBudget > 0 ? `${headerBase}; +${droppedByBudget} truncadas por or\xE7amento de caracteres` : headerBase;
7897
- return [header, ...lines].join("\n");
7905
+ return [header, ...lines, warning].filter(Boolean).join("\n");
7898
7906
  }
7899
7907
  function isWellFormedMemoryEntry(value) {
7900
7908
  return typeof value === "object" && value !== null && typeof value.file === "string";
@@ -8226,32 +8234,46 @@ function buildMemoryLine(memoryIndexPath) {
8226
8234
  const filtered = filterRelevantMemories(memoryEntries);
8227
8235
  return formatMemoryContext(filtered, memoryEntries);
8228
8236
  }
8229
- function buildCapabilityMatrixContext() {
8237
+ function resolveVaultPaths(caller) {
8230
8238
  if (VAULT_CONFIG_PATH === null || AGENTS_DIR === null || PROFILES_PATH === null || MEMORY_INDEX_PATH === null || CAPABILITY_CACHE_PATH === null) {
8231
- log(
8232
- HOOK,
8233
- "buildCapabilityMatrixContext: PROJECT_ROOT unresolved (unregistered cwd) \u2014 skipping"
8234
- );
8235
- return "";
8239
+ log(HOOK, `${caller}: PROJECT_ROOT unresolved (unregistered cwd) \u2014 skipping`);
8240
+ return null;
8236
8241
  }
8237
- let matrixLine = "";
8242
+ return {
8243
+ vaultConfigPath: VAULT_CONFIG_PATH,
8244
+ agentsDir: AGENTS_DIR,
8245
+ profilesPath: PROFILES_PATH,
8246
+ memoryIndexPath: MEMORY_INDEX_PATH,
8247
+ capabilityCachePath: CAPABILITY_CACHE_PATH
8248
+ };
8249
+ }
8250
+ function buildMatrixContext() {
8251
+ const paths = resolveVaultPaths("buildMatrixContext");
8252
+ if (!paths) return "";
8238
8253
  try {
8239
- matrixLine = buildMatrixLine(
8240
- VAULT_CONFIG_PATH,
8241
- AGENTS_DIR,
8242
- PROFILES_PATH,
8243
- CAPABILITY_CACHE_PATH
8254
+ return buildMatrixLine(
8255
+ paths.vaultConfigPath,
8256
+ paths.agentsDir,
8257
+ paths.profilesPath,
8258
+ paths.capabilityCachePath
8244
8259
  );
8245
8260
  } catch (err) {
8246
8261
  log(HOOK, `Capability matrix failed: ${err}`);
8262
+ return "";
8247
8263
  }
8248
- let memoryLine = "";
8264
+ }
8265
+ function buildMemoryContext() {
8266
+ const paths = resolveVaultPaths("buildMemoryContext");
8267
+ if (!paths) return "";
8249
8268
  try {
8250
- memoryLine = buildMemoryLine(MEMORY_INDEX_PATH);
8269
+ return buildMemoryLine(paths.memoryIndexPath);
8251
8270
  } catch (err) {
8252
8271
  log(HOOK, `Memory context failed: ${err}`);
8272
+ return "";
8253
8273
  }
8254
- return [matrixLine, memoryLine].filter(Boolean).join("\n");
8274
+ }
8275
+ function buildCapabilityMatrixContext() {
8276
+ return [buildMatrixContext(), buildMemoryContext()].filter(Boolean).join("\n");
8255
8277
  }
8256
8278
  function checkSprintInbox() {
8257
8279
  const sprintId = process.env.SPRINT_ID;
@@ -8307,7 +8329,8 @@ async function main() {
8307
8329
  const backlog = getBacklogSummary();
8308
8330
  const tierInfo = await checkTier();
8309
8331
  const tierSegment = formatTierSegment(tierInfo);
8310
- const capabilityContext = buildCapabilityMatrixContext();
8332
+ const memoryContext = buildMemoryContext();
8333
+ const matrixContext = buildMatrixContext();
8311
8334
  const sprintInboxContext = checkSprintInbox();
8312
8335
  await getToken();
8313
8336
  const selection = PROJECT_SELECTION_ENABLED && sessionId ? readSessionSelection(SESSION_SELECTION_STATE_DIR, sessionId) : void 0;
@@ -8329,6 +8352,7 @@ async function main() {
8329
8352
  "SessionStart",
8330
8353
  [
8331
8354
  `${isRecovery ? "[ORCHESTRATOR-RECOVERY]" : "[ORCHESTRATOR-DATA]"} ${stateLine2}`,
8355
+ memoryContext,
8332
8356
  ...isRecovery ? RECOVERY_DIRECTIVE : [
8333
8357
  backlog.lastSession ? `[LAST-SESSION] ${backlog.lastSession}` : "",
8334
8358
  backlog.status ? `[PENDENTE] ${backlog.status}` : "",
@@ -8336,7 +8360,7 @@ async function main() {
8336
8360
  "Do NOT invoke /orchestrator skill for the greeting \u2014 use AskUserQuestion directly.",
8337
8361
  "Invoke /orchestrator skill only when the user chooses to start a workflow."
8338
8362
  ],
8339
- capabilityContext,
8363
+ matrixContext,
8340
8364
  sprintInboxContext,
8341
8365
  projectSelectionPrompt
8342
8366
  ].filter(Boolean).join("\n")
@@ -8354,12 +8378,13 @@ async function main() {
8354
8378
  "SessionStart",
8355
8379
  [
8356
8380
  `${isRecovery ? "[ORCHESTRATOR-RECOVERY]" : "[ORCHESTRATOR-DATA]"} ${stateLine}`,
8381
+ memoryContext,
8357
8382
  ...isRecovery ? RECOVERY_DIRECTIVE : [
8358
8383
  `[ORCHESTRATOR] Active workflow detected. Greet the user via AskUserQuestion showing workflow state (orchestrator identity, MUST rule #1).`,
8359
8384
  "Do NOT invoke /orchestrator skill for the greeting \u2014 use AskUserQuestion directly.",
8360
8385
  "Invoke /orchestrator skill when the user chooses to continue the workflow."
8361
8386
  ],
8362
- capabilityContext,
8387
+ matrixContext,
8363
8388
  projectSelectionPrompt
8364
8389
  ].filter(Boolean).join("\n")
8365
8390
  );
@@ -8367,6 +8392,8 @@ async function main() {
8367
8392
  main().catch(() => process.exit(0));
8368
8393
  export {
8369
8394
  buildCapabilityMatrixContext,
8395
+ buildMatrixContext,
8396
+ buildMemoryContext,
8370
8397
  buildProjectSelectionPrompt,
8371
8398
  formatProjectBindingSegment,
8372
8399
  formatSessionNumberSegment,