@orchestrator-claude/definitions 3.37.1 → 3.37.2
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/agents/ciclo-engineer.md +35 -19
- package/agents/presentation-engineer.md +24 -11
- package/hooks/lib/capability-scanner.ts +80 -9
- package/hooks/lib/security-gate-core.ts +271 -0
- package/hooks/security-gate-honest.mjs +302 -0
- package/hooks/security-gate-honest.ts +148 -0
- package/hooks/session-start.mjs +53 -26
- package/hooks/session-start.ts +97 -32
- package/hooks/subagent-start.mjs +16 -8
- package/manifest.json +24 -8
- package/package.json +1 -1
- package/skills/ciclo-lane-dispatch/SKILL.md +93 -8
- package/skills/fase-1b-projecao/SKILL.md +4 -0
- package/skills/lane-1-entrevista/SKILL.md +173 -0
|
@@ -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
|
+
});
|
package/hooks/session-start.mjs
CHANGED
|
@@ -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
|
-
|
|
7874
|
-
const
|
|
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] ${
|
|
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 <
|
|
7885
|
-
const entry =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
|
|
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
|
-
|
|
8264
|
+
}
|
|
8265
|
+
function buildMemoryContext() {
|
|
8266
|
+
const paths = resolveVaultPaths("buildMemoryContext");
|
|
8267
|
+
if (!paths) return "";
|
|
8249
8268
|
try {
|
|
8250
|
-
|
|
8269
|
+
return buildMemoryLine(paths.memoryIndexPath);
|
|
8251
8270
|
} catch (err) {
|
|
8252
8271
|
log(HOOK, `Memory context failed: ${err}`);
|
|
8272
|
+
return "";
|
|
8253
8273
|
}
|
|
8254
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|