@cybedefend/vibedefend 1.4.6 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5764,26 +5764,34 @@ async function resolveProjectIdWithApi(cwd = process.cwd(), deps = {}) {
5764
5764
  const resolveSync = deps.resolveSync ?? resolveProjectId;
5765
5765
  const base = resolveSync(cwd);
5766
5766
  if (base.projectId) return base;
5767
+ let repository;
5768
+ const none = (inactiveReason) => ({
5769
+ ...base,
5770
+ inactiveReason,
5771
+ ...repository ? { repository } : {}
5772
+ });
5767
5773
  try {
5768
5774
  const getOrigin = deps.getOrigin ?? getOriginRemote;
5769
5775
  const originUrl = getOrigin(cwd);
5770
- if (!originUrl) return base;
5776
+ if (!originUrl) return none("no-git-origin");
5771
5777
  const remote = parseRemoteUrl(originUrl);
5772
- if (!remote || remote.provider === null) return base;
5778
+ if (!remote) return none("unsupported-remote");
5779
+ repository = `${remote.host}/${remote.slug}`;
5780
+ if (remote.provider === null) return none("unsupported-remote");
5773
5781
  const key = `${remote.provider}:${remote.host}:${remote.slug}`;
5774
5782
  const cached = getCachedRepoLink(key, deps.cacheDeps);
5775
5783
  if (cached.kind === "positive") {
5776
5784
  return { projectId: cached.projectId, apiBase: base.apiBase };
5777
5785
  }
5778
5786
  if (cached.kind === "negative") {
5779
- return base;
5787
+ return none("no-linked-project");
5780
5788
  }
5781
5789
  const config = (deps.loadConfig ?? loadRuntimeConfig)();
5782
- if (!config) return base;
5790
+ if (!config) return none("lookup-failed");
5783
5791
  const apiBase = base.apiBase ?? config.region.apiBase;
5784
5792
  const resolveTokenFn = deps.resolveTokenFn ?? resolveTokenWithRefresh;
5785
5793
  const token = await resolveTokenFn(config.region.mcpName);
5786
- if (!token) return base;
5794
+ if (!token) return none("not-signed-in");
5787
5795
  const resolveByRepo = deps.resolveByRepo ?? resolveProjectByRepository;
5788
5796
  const matches = await resolveByRepo(
5789
5797
  apiBase,
@@ -5791,7 +5799,7 @@ async function resolveProjectIdWithApi(cwd = process.cwd(), deps = {}) {
5791
5799
  { provider: remote.provider, slug: remote.slug, host: remote.host },
5792
5800
  { timeoutMs: deps.timeoutMs ?? RESOLVE_API_TIMEOUT_MS }
5793
5801
  );
5794
- if (matches === null) return base;
5802
+ if (matches === null) return none("lookup-failed");
5795
5803
  if (matches.length === 1 && matches[0].vibedefendEnabled !== false) {
5796
5804
  const m = matches[0];
5797
5805
  writeCachedRepoLink(
@@ -5802,13 +5810,171 @@ async function resolveProjectIdWithApi(cwd = process.cwd(), deps = {}) {
5802
5810
  return { projectId: m.projectId, apiBase: base.apiBase };
5803
5811
  }
5804
5812
  writeCachedRepoLink(key, { negative: true }, deps.cacheDeps);
5805
- return base;
5813
+ return none("no-linked-project");
5806
5814
  } catch {
5807
- return base;
5815
+ return none("lookup-failed");
5808
5816
  }
5809
5817
  }
5810
5818
 
5819
+ // src/hooks/runtime/usage-block.ts
5820
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
5821
+ import { homedir as homedir6 } from "node:os";
5822
+ import { dirname as dirname3, join as join5 } from "node:path";
5823
+ var BLOCKED_CODE = "VIBEDEFEND_BLOCKED";
5824
+ var USAGE_WARNING_HEADER = "x-vibedefend-usage-warning";
5825
+ var UNITS = ["injection", "judgment", "diff_scan"];
5826
+ var REASONS = [
5827
+ "free_tier_exhausted",
5828
+ "cap_reached",
5829
+ "unpaid",
5830
+ "product_inactive"
5831
+ ];
5832
+ var WARNINGS = [
5833
+ "approaching_limit",
5834
+ "limit_reached"
5835
+ ];
5836
+ function pick(allowed, value) {
5837
+ return allowed.find((v) => v === value);
5838
+ }
5839
+ async function readVibedefendBlock(res) {
5840
+ if (res.status !== 402) return null;
5841
+ let body;
5842
+ try {
5843
+ body = await res.json();
5844
+ } catch {
5845
+ return null;
5846
+ }
5847
+ if (!body || typeof body !== "object") return null;
5848
+ const root = body;
5849
+ const data = root.data && typeof root.data === "object" ? root.data : root;
5850
+ const source = data.code === BLOCKED_CODE ? data : root;
5851
+ if (source.code !== BLOCKED_CODE) return null;
5852
+ return {
5853
+ reason: pick(REASONS, source.reason) ?? "unknown",
5854
+ unit: pick(UNITS, source.unit) ?? "unknown"
5855
+ };
5856
+ }
5857
+ function readUsageWarning(res) {
5858
+ let raw;
5859
+ try {
5860
+ raw = res.headers?.get?.(USAGE_WARNING_HEADER);
5861
+ } catch {
5862
+ return null;
5863
+ }
5864
+ if (typeof raw !== "string") return null;
5865
+ const parts = raw.trim().split(":");
5866
+ if (parts.length !== 2) return null;
5867
+ const unit = pick(UNITS, parts[0]);
5868
+ const warning = pick(WARNINGS, parts[1]);
5869
+ if (!unit || !warning) return null;
5870
+ return { unit, warning };
5871
+ }
5872
+ var UNIT_LABEL = {
5873
+ injection: "Security rules injection",
5874
+ judgment: "Action Guards judgments",
5875
+ diff_scan: "Diff scans",
5876
+ unknown: "Part of VibeDefend"
5877
+ };
5878
+ var UNIT_EFFECT = {
5879
+ injection: "no security or business rules are added to your edits",
5880
+ judgment: "Action Guards are off for this session",
5881
+ diff_scan: "no diff scan can be started",
5882
+ unknown: "part of VibeDefend is paused"
5883
+ };
5884
+ function blockSentence(block) {
5885
+ const label = UNIT_LABEL[block.unit];
5886
+ const effect = UNIT_EFFECT[block.unit];
5887
+ switch (block.reason) {
5888
+ case "free_tier_exhausted":
5889
+ return `VibeDefend paused: ${label} used up this month's free tier, so ${effect}. An administrator of your organisation can add a card in the VibeDefend usage page to continue.`;
5890
+ case "cap_reached":
5891
+ return `VibeDefend paused: ${label} reached the monthly spending cap of your organisation, so ${effect}. An administrator can raise the cap in the VibeDefend usage page.`;
5892
+ case "unpaid":
5893
+ return "VibeDefend paused: your organisation's last VibeDefend invoice is unpaid, so rules, Action Guards and diff scans are off. An administrator can update the card in the VibeDefend usage page.";
5894
+ case "product_inactive":
5895
+ return "VibeDefend is not active for your organisation, so rules, Action Guards and diff scans are off. An administrator can activate it in the CybeDefend dashboard.";
5896
+ default:
5897
+ return `VibeDefend paused: ${label} is blocked for your organisation, so ${effect}. An administrator can check the VibeDefend usage page.`;
5898
+ }
5899
+ }
5900
+ function warningSentence(w) {
5901
+ const label = UNIT_LABEL[w.unit];
5902
+ if (w.warning === "approaching_limit") {
5903
+ return `VibeDefend: ${label} has used 80 % of this month's allowance of your organisation. An administrator can follow it in the VibeDefend usage page.`;
5904
+ }
5905
+ return `VibeDefend: ${label} has reached this month's limit of your organisation; the next requests will be refused. An administrator can add a card or raise the cap in the VibeDefend usage page.`;
5906
+ }
5907
+ var ORG_WIDE_REASONS = /* @__PURE__ */ new Set([
5908
+ "unpaid",
5909
+ "product_inactive"
5910
+ ]);
5911
+ function blockKey(block) {
5912
+ return ORG_WIDE_REASONS.has(block.reason) ? `block:${block.reason}` : `block:${block.reason}:${block.unit}`;
5913
+ }
5914
+ var NOTICES_CAP = 300;
5915
+ function defaultUsageNoticesFile() {
5916
+ const dir = process.env.CYBEDEFEND_BASELINE_DIR ?? join5(homedir6(), ".cybedefend");
5917
+ return join5(dir, "usage-notices-sessions.json");
5918
+ }
5919
+ function makeUsageNotifier(opts = {}) {
5920
+ const file = opts.file ?? defaultUsageNoticesFile();
5921
+ const write = opts.write ?? ((line) => {
5922
+ try {
5923
+ process.stderr.write(line);
5924
+ } catch {
5925
+ }
5926
+ });
5927
+ function load() {
5928
+ try {
5929
+ if (!existsSync7(file)) return [];
5930
+ const data = JSON.parse(readFileSync6(file, "utf8"));
5931
+ return Array.isArray(data.seen) ? data.seen.filter((s) => typeof s === "string") : [];
5932
+ } catch {
5933
+ return [];
5934
+ }
5935
+ }
5936
+ function once(sessionId, key, sentence) {
5937
+ if (sessionId) {
5938
+ const marker = `${sessionId}|${key}`;
5939
+ const seen = load();
5940
+ if (seen.includes(marker)) return null;
5941
+ try {
5942
+ mkdirSync4(dirname3(file), { recursive: true });
5943
+ writeFileSync3(
5944
+ file,
5945
+ JSON.stringify({ seen: [...seen, marker].slice(-NOTICES_CAP) }),
5946
+ "utf8"
5947
+ );
5948
+ } catch {
5949
+ }
5950
+ }
5951
+ write(`[VibeDefend] ${sentence}
5952
+ `);
5953
+ return sentence;
5954
+ }
5955
+ return {
5956
+ notifyBlock: (sessionId, block) => once(sessionId, blockKey(block), blockSentence(block)),
5957
+ notifyWarning: (sessionId, w) => once(sessionId, `warning:${w.unit}:${w.warning}`, warningSentence(w))
5958
+ };
5959
+ }
5960
+
5811
5961
  // src/hooks/runtime/api.ts
5962
+ async function reportUsageSignal(res, listener) {
5963
+ const emit2 = (signal) => {
5964
+ try {
5965
+ listener?.(signal);
5966
+ } catch {
5967
+ }
5968
+ };
5969
+ if (!res.ok) {
5970
+ const block = await readVibedefendBlock(res);
5971
+ if (block) emit2({ kind: "blocked", block });
5972
+ return false;
5973
+ }
5974
+ const warning = readUsageWarning(res);
5975
+ if (warning) emit2({ kind: "warning", warning });
5976
+ return true;
5977
+ }
5812
5978
  var DEFAULT_TIMEOUT_MS = 1e4;
5813
5979
  async function fetchBusinessRules(opts) {
5814
5980
  const f = opts.fetchImpl ?? globalThis.fetch;
@@ -5832,7 +5998,7 @@ async function fetchBusinessRules(opts) {
5832
5998
  } catch {
5833
5999
  return null;
5834
6000
  }
5835
- if (!res.ok) return null;
6001
+ if (!await reportUsageSignal(res, opts.onUsageSignal)) return null;
5836
6002
  let body;
5837
6003
  try {
5838
6004
  body = await res.json();
@@ -5867,7 +6033,7 @@ async function fetchSecurityRules(opts) {
5867
6033
  } catch {
5868
6034
  return null;
5869
6035
  }
5870
- if (!res.ok) return null;
6036
+ if (!await reportUsageSignal(res, opts.onUsageSignal)) return null;
5871
6037
  let body;
5872
6038
  try {
5873
6039
  body = await res.json();
@@ -5906,6 +6072,12 @@ async function fetchProposalsCount(opts) {
5906
6072
  return { total, raw: body };
5907
6073
  }
5908
6074
 
6075
+ // src/hooks/runtime/session-activation.ts
6076
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
6077
+ import { homedir as homedir7 } from "node:os";
6078
+ import { dirname as dirname4, isAbsolute as isAbsolute2, join as join6, resolve as resolvePath2 } from "node:path";
6079
+ import { fileURLToPath } from "node:url";
6080
+
5909
6081
  // src/hooks/runtime/emit.ts
5910
6082
  var ENVELOPE_CLIENTS = {
5911
6083
  SessionStart: /* @__PURE__ */ new Set(["codex"]),
@@ -5926,6 +6098,10 @@ function emit(body, client) {
5926
6098
  }
5927
6099
  function emitContext(body, client, hookEventName) {
5928
6100
  if (!body) return;
6101
+ if (client === "cursor" && hookEventName === "SessionStart") {
6102
+ process.stdout.write(JSON.stringify({ additional_context: body }) + "\n");
6103
+ return;
6104
+ }
5929
6105
  if (ENVELOPE_CLIENTS[hookEventName].has(client)) {
5930
6106
  const payload = JSON.stringify({
5931
6107
  hookSpecificOutput: {
@@ -5938,6 +6114,25 @@ function emitContext(body, client, hookEventName) {
5938
6114
  }
5939
6115
  emit(body, client);
5940
6116
  }
6117
+ var SYSTEM_MESSAGE_CLIENTS = /* @__PURE__ */ new Set([
6118
+ "claude-code",
6119
+ "codex"
6120
+ ]);
6121
+ function emitSessionNotice(notice, client) {
6122
+ if (SYSTEM_MESSAGE_CLIENTS.has(client)) {
6123
+ process.stdout.write(
6124
+ JSON.stringify({
6125
+ systemMessage: notice.userMessage,
6126
+ hookSpecificOutput: {
6127
+ hookEventName: "SessionStart",
6128
+ additionalContext: notice.modelContext
6129
+ }
6130
+ }) + "\n"
6131
+ );
6132
+ return;
6133
+ }
6134
+ emitContext(notice.modelContext, client, "SessionStart");
6135
+ }
5941
6136
  function emitStop(body, client, reason) {
5942
6137
  if (!body) return;
5943
6138
  if (client === "claude-code") {
@@ -5952,6 +6147,143 @@ function emitStop(body, client, reason) {
5952
6147
  emit(body, client);
5953
6148
  }
5954
6149
 
6150
+ // src/hooks/runtime/session-activation.ts
6151
+ var SETTLED_REASONS = /* @__PURE__ */ new Set([
6152
+ "no-git-origin",
6153
+ "unsupported-remote",
6154
+ "no-linked-project"
6155
+ ]);
6156
+ function isSettledReason(reason) {
6157
+ return SETTLED_REASONS.has(reason);
6158
+ }
6159
+ var SESSIONS_CAP = 200;
6160
+ function defaultStoreFile() {
6161
+ const dir = process.env.CYBEDEFEND_BASELINE_DIR ?? join6(homedir7(), ".cybedefend");
6162
+ return join6(dir, "inactive-sessions.json");
6163
+ }
6164
+ function defaultInactiveSessionStore(file = defaultStoreFile(), now = Date.now) {
6165
+ function load() {
6166
+ if (!existsSync8(file)) return {};
6167
+ try {
6168
+ const parsed = JSON.parse(readFileSync7(file, "utf8"));
6169
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
6170
+ } catch {
6171
+ return {};
6172
+ }
6173
+ }
6174
+ function save(map) {
6175
+ try {
6176
+ mkdirSync5(dirname4(file), { recursive: true });
6177
+ writeFileSync4(file, JSON.stringify(map) + "\n", "utf8");
6178
+ } catch {
6179
+ }
6180
+ }
6181
+ return {
6182
+ get(sessionId) {
6183
+ const rec = load()[sessionId];
6184
+ if (!rec || typeof rec !== "object" || typeof rec.cwd !== "string" || typeof rec.reason !== "string") {
6185
+ return null;
6186
+ }
6187
+ return rec;
6188
+ },
6189
+ mark(sessionId, record) {
6190
+ const map = load();
6191
+ map[sessionId] = { ...record, at: now() };
6192
+ const entries = Object.entries(map);
6193
+ if (entries.length > SESSIONS_CAP) {
6194
+ entries.sort(([, a], [, b]) => (a.at ?? 0) - (b.at ?? 0));
6195
+ save(Object.fromEntries(entries.slice(-SESSIONS_CAP)));
6196
+ return;
6197
+ }
6198
+ save(map);
6199
+ },
6200
+ clear(sessionId) {
6201
+ const map = load();
6202
+ if (!(sessionId in map)) return;
6203
+ delete map[sessionId];
6204
+ save(map);
6205
+ }
6206
+ };
6207
+ }
6208
+ function sessionIdOf(stdin) {
6209
+ for (const key of ["session_id", "sessionId", "conversation_id", "trajectory_id"]) {
6210
+ const v = stdin[key];
6211
+ if (typeof v === "string" && v.length > 0) return v;
6212
+ }
6213
+ return null;
6214
+ }
6215
+ function sessionFolderOf(stdin, client, env = process.env) {
6216
+ const candidates = [];
6217
+ if (client === "cursor") {
6218
+ const roots = stdin.workspace_roots;
6219
+ if (Array.isArray(roots)) candidates.push(roots[0]);
6220
+ candidates.push(env.CURSOR_PROJECT_DIR);
6221
+ }
6222
+ candidates.push(stdin.cwd);
6223
+ for (const c of candidates) {
6224
+ const folder = absoluteFolder(c);
6225
+ if (folder) return folder;
6226
+ }
6227
+ return process.cwd();
6228
+ }
6229
+ function absoluteFolder(value) {
6230
+ if (typeof value !== "string" || value.length === 0) return null;
6231
+ let p = value;
6232
+ if (p.startsWith("file://")) {
6233
+ try {
6234
+ p = fileURLToPath(p);
6235
+ } catch {
6236
+ return null;
6237
+ }
6238
+ }
6239
+ return isAbsolute2(p) ? resolvePath2(p) : null;
6240
+ }
6241
+ function isSessionInactive(stdin, deps = {}) {
6242
+ try {
6243
+ const sessionId = sessionIdOf(stdin);
6244
+ if (!sessionId) return false;
6245
+ const record = (deps.store ?? defaultInactiveSessionStore()).get(sessionId);
6246
+ if (!record || !isSettledReason(record.reason)) return false;
6247
+ const cwd = deps.cwd ?? process.cwd();
6248
+ if (record.cwd !== cwd) return false;
6249
+ const resolveSync = deps.resolveSync ?? resolveProjectId;
6250
+ return !resolveSync(cwd).projectId;
6251
+ } catch {
6252
+ return false;
6253
+ }
6254
+ }
6255
+ function whyInactive(reason, repository) {
6256
+ const repo = repository ? `\`${repository}\`` : "this folder";
6257
+ switch (reason) {
6258
+ case "no-git-origin":
6259
+ return "this folder has no `.cybedefend/config.json` and no GitHub or GitLab `origin` remote";
6260
+ case "unsupported-remote":
6261
+ return `this folder has no \`.cybedefend/config.json\` and its git \`origin\`${repository ? ` (${repo})` : ""} is not on GitHub or GitLab`;
6262
+ case "no-linked-project":
6263
+ return `this folder has no \`.cybedefend/config.json\` and no CybeDefend project with VibeDefend enabled matches ${repo}`;
6264
+ case "not-signed-in":
6265
+ return `you are not signed in, so the CybeDefend project for ${repo} could not be looked up`;
6266
+ case "lookup-failed":
6267
+ return `the CybeDefend API did not answer, so the project for ${repo} could not be looked up`;
6268
+ }
6269
+ }
6270
+ function composeInactiveNotice(reason, repository, client) {
6271
+ const why = whyInactive(reason, repository);
6272
+ const settled = isSettledReason(reason);
6273
+ const userMessage = settled ? `\u{1F6E1}\uFE0F CybeDefend is off for this session: ${why}. Run \`vibedefend link\` in this folder to turn it on.` : reason === "not-signed-in" ? `\u{1F6E1}\uFE0F CybeDefend is off for now: ${why}. Run \`vibedefend login\` to turn it on.` : `\u{1F6E1}\uFE0F CybeDefend is off for now: ${why}. It will try again on the next prompt or edit.`;
6274
+ const scope = settled ? "- Its hooks stay off until this session ends: no rules fetch, no proposal inbox, no Action Guards, no end-of-session review." : "- Its hooks stay quiet until the project can be found.";
6275
+ const relay = SYSTEM_MESSAGE_CLIENTS.has(client) ? "- The user has already been shown this notice; do not repeat it." : "- Tell the user once, in one short line, that CybeDefend is off for this session and why.";
6276
+ const modelContext = [
6277
+ "## CybeDefend is not active in this session",
6278
+ `CybeDefend found no project for this folder: ${why}.`,
6279
+ scope,
6280
+ "- Do not call CybeDefend MCP tools, run the CybeDefend workflow or ask for a CybeDefend project id unless the user asks about CybeDefend.",
6281
+ relay,
6282
+ ""
6283
+ ].join("\n");
6284
+ return { userMessage, modelContext };
6285
+ }
6286
+
5955
6287
  // src/hooks/runtime/fetch-rules.ts
5956
6288
  function emitPreToolUseContext(body, client) {
5957
6289
  emitContext(body, client, "PreToolUse");
@@ -5971,13 +6303,16 @@ async function fetchRulesHook(deps = {}) {
5971
6303
  const fetchRules = deps.fetchRulesFn ?? fetchBusinessRules;
5972
6304
  const fetchSecRules = deps.fetchSecurityRulesFn ?? fetchSecurityRules;
5973
6305
  const emitFn = deps.emitFn ?? emitPreToolUseContext;
6306
+ const usageNotifier = deps.usageNotifier ?? makeUsageNotifier();
5974
6307
  const stdin = await readStdin();
5975
6308
  const event = sniff(stdin);
5976
6309
  if (!INTERESTED_TOOLS.has(event.toolName)) return;
5977
6310
  if (!event.filePath) return;
5978
6311
  const config = loadConfig();
5979
6312
  if (!config) return;
5980
- const { projectId, apiBase: configApiBase } = await resolveProject();
6313
+ const folder = sessionFolderOf(stdin, event.client);
6314
+ if (isSessionInactive(stdin, { store: deps.sessionStore, cwd: folder })) return;
6315
+ const { projectId, apiBase: configApiBase } = await resolveProject(folder);
5981
6316
  if (!projectId) return;
5982
6317
  const apiBase = process.env.CYBEDEFEND_API_BASE ?? configApiBase ?? config.region.apiBase;
5983
6318
  const token = resolveTok(config.region.mcpName, {
@@ -5987,13 +6322,20 @@ async function fetchRulesHook(deps = {}) {
5987
6322
  const intent = buildIntent(event);
5988
6323
  const files = event.filePaths.length > 0 ? event.filePaths : [event.filePath];
5989
6324
  const fileLabel = files.join(", ");
6325
+ const sessionId = sessionIdOf2(stdin);
6326
+ const usageNotices = [];
6327
+ const onUsageSignal = (signal) => {
6328
+ const sentence = signal.kind === "blocked" ? usageNotifier.notifyBlock(sessionId, signal.block) : usageNotifier.notifyWarning(sessionId, signal.warning);
6329
+ if (sentence) usageNotices.push(sentence);
6330
+ };
5990
6331
  const fetchOpts = {
5991
6332
  apiBase,
5992
6333
  projectId,
5993
6334
  token,
5994
6335
  files,
5995
6336
  intent,
5996
- topK: 5
6337
+ topK: 5,
6338
+ onUsageSignal
5997
6339
  };
5998
6340
  const [businessResult, securityResult] = await Promise.all([
5999
6341
  fetchRules(fetchOpts),
@@ -6017,8 +6359,18 @@ async function fetchRulesHook(deps = {}) {
6017
6359
  )
6018
6360
  );
6019
6361
  }
6362
+ if (usageNotices.length > 0) {
6363
+ blocks.push(
6364
+ ["## CybeDefend VibeDefend usage", ...usageNotices.map((n) => `- ${n}`), ""].join("\n")
6365
+ );
6366
+ }
6020
6367
  if (blocks.length > 0) emitFn(blocks.join("\n"), event.client);
6021
6368
  }
6369
+ function sessionIdOf2(stdin) {
6370
+ if (typeof stdin.session_id === "string" && stdin.session_id) return stdin.session_id;
6371
+ if (typeof stdin.sessionId === "string" && stdin.sessionId) return stdin.sessionId;
6372
+ return void 0;
6373
+ }
6022
6374
  function buildIntent(event) {
6023
6375
  const limit = parseInt(process.env.CYBEDEFEND_INTENT_BYTES ?? "800", 10);
6024
6376
  const snippet = event.newContent.slice(0, Number.isFinite(limit) ? limit : 800).replace(/\s+/g, " ").trim();
@@ -6099,9 +6451,9 @@ var DIRECT_PROPOSE_DIRECTIVES = {
6099
6451
 
6100
6452
  // src/hooks/runtime/self-update-check.ts
6101
6453
  import { spawn } from "node:child_process";
6102
- import { existsSync as existsSync8, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
6103
- import { homedir as homedir6 } from "node:os";
6104
- import { join as join5 } from "node:path";
6454
+ import { existsSync as existsSync10, readFileSync as readFileSync8, rmSync, writeFileSync as writeFileSync5 } from "node:fs";
6455
+ import { homedir as homedir8 } from "node:os";
6456
+ import { join as join7 } from "node:path";
6105
6457
 
6106
6458
  // src/hooks/runtime/semver.ts
6107
6459
  function parseVersion(input) {
@@ -6128,8 +6480,8 @@ function isNewer(latest, current) {
6128
6480
  }
6129
6481
 
6130
6482
  // src/self-update.ts
6131
- import { fileURLToPath } from "node:url";
6132
- import { existsSync as existsSync7, realpathSync } from "node:fs";
6483
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
6484
+ import { existsSync as existsSync9, realpathSync } from "node:fs";
6133
6485
  var GLOBAL_MODES = /* @__PURE__ */ new Set([
6134
6486
  "npm-global",
6135
6487
  "pnpm-global",
@@ -6157,14 +6509,14 @@ function detectInstallMode(scriptPath) {
6157
6509
  }
6158
6510
  function resolveScriptPath() {
6159
6511
  const fromArgv = process.argv[1];
6160
- if (fromArgv && existsSync7(fromArgv)) {
6512
+ if (fromArgv && existsSync9(fromArgv)) {
6161
6513
  try {
6162
6514
  return realpathSync(fromArgv);
6163
6515
  } catch {
6164
6516
  return fromArgv;
6165
6517
  }
6166
6518
  }
6167
- return fileURLToPath(import.meta.url);
6519
+ return fileURLToPath2(import.meta.url);
6168
6520
  }
6169
6521
 
6170
6522
  // src/hooks/runtime/self-update-check.ts
@@ -6179,9 +6531,9 @@ function isValidRegistryVersion(value) {
6179
6531
  }
6180
6532
  async function selfUpdateCheck(deps) {
6181
6533
  const now = deps.now ?? (() => Date.now());
6182
- const dir = deps.cybeDir ?? join5(process.env.CYBEDEFEND_HOME ?? homedir6(), ".cybedefend");
6183
- const throttlePath = join5(dir, "update-check.json");
6184
- const logPath = join5(dir, "last-update.log");
6534
+ const dir = deps.cybeDir ?? join7(process.env.CYBEDEFEND_HOME ?? homedir8(), ".cybedefend");
6535
+ const throttlePath = join7(dir, "update-check.json");
6536
+ const logPath = join7(dir, "last-update.log");
6185
6537
  const installMode = deps.installMode ?? (deps.runnerSource ? detectInstallMode(deps.runnerSource) : detectInstallMode(resolveScriptPath()));
6186
6538
  const priorFailure = takeUpdateLogNotice(logPath);
6187
6539
  const throttle = readThrottle(throttlePath);
@@ -6214,8 +6566,8 @@ function applyOrNotify(deps, installMode, latest, logPath) {
6214
6566
  }
6215
6567
  function readThrottle(path) {
6216
6568
  try {
6217
- if (!existsSync8(path)) return null;
6218
- const data = JSON.parse(readFileSync6(path, "utf8"));
6569
+ if (!existsSync10(path)) return null;
6570
+ const data = JSON.parse(readFileSync8(path, "utf8"));
6219
6571
  if (typeof data.lastCheckedAt !== "number") return null;
6220
6572
  return { lastCheckedAt: data.lastCheckedAt };
6221
6573
  } catch {
@@ -6225,16 +6577,16 @@ function readThrottle(path) {
6225
6577
  function writeThrottle(path, throttle) {
6226
6578
  try {
6227
6579
  ensureDirFor(path);
6228
- writeFileSync3(path, JSON.stringify(throttle) + "\n");
6580
+ writeFileSync5(path, JSON.stringify(throttle) + "\n");
6229
6581
  } catch {
6230
6582
  }
6231
6583
  }
6232
6584
  function takeUpdateLogNotice(logPath) {
6233
6585
  try {
6234
- if (!existsSync8(logPath)) return null;
6586
+ if (!existsSync10(logPath)) return null;
6235
6587
  let parsed = {};
6236
6588
  try {
6237
- parsed = JSON.parse(readFileSync6(logPath, "utf8"));
6589
+ parsed = JSON.parse(readFileSync8(logPath, "utf8"));
6238
6590
  } catch {
6239
6591
  parsed = {};
6240
6592
  }
@@ -6309,18 +6661,18 @@ function pmCommand(mode, spec) {
6309
6661
 
6310
6662
  // src/hooks/runtime/hook-refresh-trigger.ts
6311
6663
  import { spawn as spawn2 } from "node:child_process";
6312
- import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
6313
- import { homedir as homedir7 } from "node:os";
6314
- import { dirname as dirname3, join as join6 } from "node:path";
6664
+ import { existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "node:fs";
6665
+ import { homedir as homedir9 } from "node:os";
6666
+ import { dirname as dirname5, join as join8 } from "node:path";
6315
6667
  function maybeRefreshStaleWiring(deps) {
6316
6668
  try {
6317
6669
  const { config, runnerVersion } = deps;
6318
6670
  if (!config.runnerSource || config.installedVersion === runnerVersion) {
6319
6671
  return false;
6320
6672
  }
6321
- const cliEntry = join6(dirname3(dirname3(config.runnerSource)), "bin", "vibedefend.js");
6322
- if (!(deps.exists ?? existsSync9)(cliEntry)) return false;
6323
- const markerPath = deps.markerPath ?? join6(process.env.CYBEDEFEND_HOME ?? homedir7(), ".cybedefend", "hook-refresh.json");
6673
+ const cliEntry = join8(dirname5(dirname5(config.runnerSource)), "bin", "vibedefend.js");
6674
+ if (!(deps.exists ?? existsSync11)(cliEntry)) return false;
6675
+ const markerPath = deps.markerPath ?? join8(process.env.CYBEDEFEND_HOME ?? homedir9(), ".cybedefend", "hook-refresh.json");
6324
6676
  if (readMarkerVersion(markerPath) === runnerVersion) return false;
6325
6677
  (deps.spawnRefresh ?? defaultSpawnRefresh)(cliEntry);
6326
6678
  writeMarker(markerPath, runnerVersion);
@@ -6331,8 +6683,8 @@ function maybeRefreshStaleWiring(deps) {
6331
6683
  }
6332
6684
  function readMarkerVersion(path) {
6333
6685
  try {
6334
- if (!existsSync9(path)) return null;
6335
- const data = JSON.parse(readFileSync7(path, "utf8"));
6686
+ if (!existsSync11(path)) return null;
6687
+ const data = JSON.parse(readFileSync9(path, "utf8"));
6336
6688
  return typeof data.version === "string" ? data.version : null;
6337
6689
  } catch {
6338
6690
  return null;
@@ -6341,7 +6693,7 @@ function readMarkerVersion(path) {
6341
6693
  function writeMarker(path, version) {
6342
6694
  try {
6343
6695
  ensureDirFor(path);
6344
- writeFileSync4(path, JSON.stringify({ version, spawnedAt: Date.now() }) + "\n");
6696
+ writeFileSync6(path, JSON.stringify({ version, spawnedAt: Date.now() }) + "\n");
6345
6697
  } catch {
6346
6698
  }
6347
6699
  }
@@ -6355,18 +6707,18 @@ function defaultSpawnRefresh(cliEntry) {
6355
6707
  }
6356
6708
 
6357
6709
  // src/version.ts
6358
- import { readFileSync as readFileSync8 } from "node:fs";
6359
- import { fileURLToPath as fileURLToPath2 } from "node:url";
6360
- import { dirname as dirname4, join as join7 } from "node:path";
6710
+ import { readFileSync as readFileSync10 } from "node:fs";
6711
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
6712
+ import { dirname as dirname6, join as join9 } from "node:path";
6361
6713
  var PKG_NAME = "@cybedefend/vibedefend";
6362
6714
  function defaultPkgPath() {
6363
- return join7(dirname4(fileURLToPath2(import.meta.url)), "..", "package.json");
6715
+ return join9(dirname6(fileURLToPath3(import.meta.url)), "..", "package.json");
6364
6716
  }
6365
6717
  function readPackageMeta(opts = {}) {
6366
6718
  if (typeof opts.bakedVersion === "string" && opts.bakedVersion.length > 0) {
6367
6719
  return { name: PKG_NAME, version: opts.bakedVersion };
6368
6720
  }
6369
- const readFile = opts.readFile ?? ((p) => readFileSync8(p, "utf8"));
6721
+ const readFile = opts.readFile ?? ((p) => readFileSync10(p, "utf8"));
6370
6722
  const pkgPath = opts.pkgPath ?? defaultPkgPath();
6371
6723
  try {
6372
6724
  return JSON.parse(readFile(pkgPath));
@@ -6375,13 +6727,22 @@ function readPackageMeta(opts = {}) {
6375
6727
  }
6376
6728
  }
6377
6729
  var pkg = readPackageMeta({
6378
- bakedVersion: true ? "1.4.6" : void 0
6730
+ bakedVersion: true ? "1.5.0" : void 0
6379
6731
  });
6380
6732
 
6381
6733
  // src/hooks/runtime/session-start.ts
6382
6734
  function emitSessionStartContext(body, client) {
6383
6735
  emitContext(body, client, "SessionStart");
6384
6736
  }
6737
+ function announceInactive(resolution, sessionId, cwd, store, client, emitNoticeFn) {
6738
+ const reason = resolution.inactiveReason ?? "no-linked-project";
6739
+ if (sessionId) {
6740
+ const prior = store.get(sessionId);
6741
+ if (prior && prior.cwd === cwd && prior.reason === reason) return;
6742
+ store.mark(sessionId, { cwd, reason });
6743
+ }
6744
+ emitNoticeFn(composeInactiveNotice(reason, resolution.repository, client), client);
6745
+ }
6385
6746
  async function sessionStartHook(deps = {}) {
6386
6747
  const readStdin = deps.readStdin ?? readStdinJson;
6387
6748
  const loadConfig = deps.loadConfig ?? loadRuntimeConfig;
@@ -6390,6 +6751,7 @@ async function sessionStartHook(deps = {}) {
6390
6751
  const fetchProposals = deps.fetchProposalsFn ?? fetchProposalsCount;
6391
6752
  const selfUpdateCheckFn = deps.selfUpdateCheckFn ?? selfUpdateCheck;
6392
6753
  const emitFn = deps.emitFn ?? emitSessionStartContext;
6754
+ const emitNoticeFn = deps.emitNoticeFn ?? emitSessionNotice;
6393
6755
  const stdin = await readStdin();
6394
6756
  const event = sniff(stdin);
6395
6757
  const config = loadConfig();
@@ -6398,8 +6760,16 @@ async function sessionStartHook(deps = {}) {
6398
6760
  config,
6399
6761
  runnerVersion: pkg.version
6400
6762
  });
6401
- const { projectId, apiBase: configApiBase } = await resolveProject();
6402
- if (!projectId) return;
6763
+ const folder = sessionFolderOf(stdin, event.client);
6764
+ const resolution = await resolveProject(folder);
6765
+ const { projectId, apiBase: configApiBase } = resolution;
6766
+ const sessionStore = deps.sessionStore ?? defaultInactiveSessionStore();
6767
+ const sessionId = sessionIdOf(stdin);
6768
+ if (!projectId) {
6769
+ announceInactive(resolution, sessionId, folder, sessionStore, event.client, emitNoticeFn);
6770
+ return;
6771
+ }
6772
+ if (sessionId) sessionStore.clear(sessionId);
6403
6773
  const apiBase = process.env.CYBEDEFEND_API_BASE ?? configApiBase ?? config.region.apiBase;
6404
6774
  if (process.env.CYBEDEFEND_DRY_RUN === "1") {
6405
6775
  emitFn(
@@ -6491,7 +6861,7 @@ async function sessionStartHook(deps = {}) {
6491
6861
  }
6492
6862
 
6493
6863
  // src/hooks/runtime/session-review.ts
6494
- import { existsSync as existsSync10, readFileSync as readFileSync9 } from "node:fs";
6864
+ import { existsSync as existsSync12, readFileSync as readFileSync11 } from "node:fs";
6495
6865
  var COUNTED_TOOLS = /* @__PURE__ */ new Set([
6496
6866
  "Edit",
6497
6867
  "Write",
@@ -6533,6 +6903,10 @@ async function sessionReviewHook(deps = {}) {
6533
6903
  }
6534
6904
  if (editCount === 0) return;
6535
6905
  if (editCount < resolveReviewThreshold(config)) return;
6906
+ const folder = sessionFolderOf(stdin, event.client);
6907
+ if (isSessionInactive(stdin, { store: deps.sessionStore, cwd: folder })) return;
6908
+ const { projectId } = await (deps.resolveProject ?? resolveProjectIdWithApi)(folder);
6909
+ if (!projectId) return;
6536
6910
  const userPrompt = extractFirstUserPrompt(transcript);
6537
6911
  const proposalMode = resolveProposalMode(config);
6538
6912
  let body = composeReviewBody(
@@ -6543,8 +6917,7 @@ async function sessionReviewHook(deps = {}) {
6543
6917
  );
6544
6918
  const withDiffScan = config.hooks.enableDiffScan !== false;
6545
6919
  if (withDiffScan) {
6546
- const cwd = typeof stdin.cwd === "string" && stdin.cwd ? stdin.cwd : process.cwd();
6547
- body += "\n" + composeDiffScanBody(extractChangedFiles(transcript, void 0, cwd));
6920
+ body += "\n" + composeDiffScanBody(extractChangedFiles(transcript, void 0, folder));
6548
6921
  }
6549
6922
  emitFn(body, event.client, composeStopReason(withDiffScan, proposalMode));
6550
6923
  }
@@ -6575,9 +6948,9 @@ function loadTranscript(stdin, readFile) {
6575
6948
  return out;
6576
6949
  }
6577
6950
  function defaultReadTranscript(path) {
6578
- if (!existsSync10(path)) return null;
6951
+ if (!existsSync12(path)) return null;
6579
6952
  try {
6580
- return readFileSync9(path, "utf8");
6953
+ return readFileSync11(path, "utf8");
6581
6954
  } catch {
6582
6955
  return null;
6583
6956
  }
@@ -6810,6 +7183,10 @@ async function preCompactHook(deps = {}) {
6810
7183
  const config = loadConfig();
6811
7184
  if (!config) return;
6812
7185
  if (!config.hooks.enableSessionReview) return;
7186
+ const folder = sessionFolderOf(stdin, event.client);
7187
+ if (isSessionInactive(stdin, { store: deps.sessionStore, cwd: folder })) return;
7188
+ const { projectId } = await (deps.resolveProject ?? resolveProjectIdWithApi)(folder);
7189
+ if (!projectId) return;
6813
7190
  if (process.env.CYBEDEFEND_DRY_RUN === "1") {
6814
7191
  emitFn(
6815
7192
  `## \u{1F6E1}\uFE0F CybeDefend PreCompact (dry-run, client=${event.client})
@@ -6837,19 +7214,19 @@ async function preCompactHook(deps = {}) {
6837
7214
  }
6838
7215
 
6839
7216
  // src/hooks/runtime/user-prompt-submit.ts
6840
- import { existsSync as existsSync11, mkdirSync as mkdirSync4, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "node:fs";
6841
- import { homedir as homedir8 } from "node:os";
6842
- import { join as join8 } from "node:path";
6843
- var SESSIONS_CAP = 100;
7217
+ import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "node:fs";
7218
+ import { homedir as homedir10 } from "node:os";
7219
+ import { join as join10 } from "node:path";
7220
+ var SESSIONS_CAP2 = 100;
6844
7221
  function defaultSessionsFile() {
6845
- const dir = process.env.CYBEDEFEND_BASELINE_DIR ?? join8(homedir8(), ".cybedefend");
6846
- return join8(dir, "user-prompt-seen-sessions.json");
7222
+ const dir = process.env.CYBEDEFEND_BASELINE_DIR ?? join10(homedir10(), ".cybedefend");
7223
+ return join10(dir, "user-prompt-seen-sessions.json");
6847
7224
  }
6848
7225
  function defaultSessionTracker(file = defaultSessionsFile()) {
6849
7226
  function load() {
6850
- if (!existsSync11(file)) return [];
7227
+ if (!existsSync13(file)) return [];
6851
7228
  try {
6852
- const raw = readFileSync10(file, "utf8");
7229
+ const raw = readFileSync12(file, "utf8");
6853
7230
  const data = JSON.parse(raw);
6854
7231
  if (!Array.isArray(data.sessions)) return [];
6855
7232
  return data.sessions.filter((s) => typeof s === "string");
@@ -6864,10 +7241,10 @@ function defaultSessionTracker(file = defaultSessionsFile()) {
6864
7241
  async markSeen(sessionId) {
6865
7242
  const existing = load();
6866
7243
  if (existing.includes(sessionId)) return;
6867
- const next = [...existing, sessionId].slice(-SESSIONS_CAP);
7244
+ const next = [...existing, sessionId].slice(-SESSIONS_CAP2);
6868
7245
  try {
6869
- mkdirSync4(join8(file, ".."), { recursive: true });
6870
- writeFileSync5(file, JSON.stringify({ sessions: next }), "utf8");
7246
+ mkdirSync6(join10(file, ".."), { recursive: true });
7247
+ writeFileSync7(file, JSON.stringify({ sessions: next }), "utf8");
6871
7248
  } catch {
6872
7249
  }
6873
7250
  }
@@ -6889,7 +7266,9 @@ async function userPromptSubmitHook(deps = {}) {
6889
7266
  }
6890
7267
  const config = loadConfig();
6891
7268
  if (!config) return;
6892
- const { projectId, apiBase: configApiBase } = await resolveProject();
7269
+ const folder = sessionFolderOf(stdin, event.client);
7270
+ if (isSessionInactive(stdin, { store: deps.sessionStore, cwd: folder })) return;
7271
+ const { projectId, apiBase: configApiBase } = await resolveProject(folder);
6893
7272
  if (!projectId) return;
6894
7273
  const apiBase = process.env.CYBEDEFEND_API_BASE ?? configApiBase ?? config.region.apiBase;
6895
7274
  const token = resolveTok(config.region.mcpName, {
@@ -7938,10 +8317,10 @@ function nearMiss(req, rule) {
7938
8317
  }
7939
8318
 
7940
8319
  // src/hooks/runtime/guard-rules-cache.ts
7941
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync6, existsSync as existsSync12, mkdirSync as mkdirSync5 } from "node:fs";
7942
- import { dirname as dirname5 } from "node:path";
7943
- import { homedir as homedir9 } from "node:os";
7944
- import { join as join9 } from "node:path";
8320
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync8, existsSync as existsSync14, mkdirSync as mkdirSync7 } from "node:fs";
8321
+ import { dirname as dirname7 } from "node:path";
8322
+ import { homedir as homedir11 } from "node:os";
8323
+ import { join as join11 } from "node:path";
7945
8324
 
7946
8325
  // node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
7947
8326
  var external_exports = {};
@@ -12053,8 +12432,8 @@ function parseRuleFields(raw) {
12053
12432
  }
12054
12433
 
12055
12434
  // src/hooks/runtime/guard-rules-cache.ts
12056
- var DEFAULT_CACHE_FILE = join9(
12057
- homedir9(),
12435
+ var DEFAULT_CACHE_FILE = join11(
12436
+ homedir11(),
12058
12437
  ".cybedefend",
12059
12438
  "guards-cache.json"
12060
12439
  );
@@ -12134,14 +12513,26 @@ function makeGuardRulesCache(opts) {
12134
12513
  const entry = readCache(ctx.projectId);
12135
12514
  const ageSec = entry ? (Date.now() - entry.fetchedAt) / 1e3 : Infinity;
12136
12515
  if (entry && ageSec < staleSec) {
12516
+ if (entry.blocked) return { kind: "blocked", block: entry.blocked };
12137
12517
  return { kind: "rules", rules: entry.rules, llmJudge: entry.llmJudge };
12138
12518
  }
12139
12519
  try {
12140
12520
  const result = await fetchFn({
12141
12521
  ...ctx,
12142
- etag: entry?.etag ?? null
12522
+ etag: entry?.blocked ? null : entry?.etag ?? null
12143
12523
  });
12144
- if (result.notModified && entry) {
12524
+ if (result.blocked) {
12525
+ writeCache({
12526
+ projectId: ctx.projectId,
12527
+ rules: [],
12528
+ etag: null,
12529
+ fetchedAt: Date.now(),
12530
+ llmJudge: null,
12531
+ blocked: result.blocked
12532
+ });
12533
+ return { kind: "blocked", block: result.blocked };
12534
+ }
12535
+ if (result.notModified && entry && !entry.blocked) {
12145
12536
  writeCache({
12146
12537
  ...entry,
12147
12538
  fetchedAt: Date.now()
@@ -12163,7 +12554,7 @@ function makeGuardRulesCache(opts) {
12163
12554
  return { kind: "rules", rules: fresh.rules, llmJudge: fresh.llmJudge };
12164
12555
  } catch (e) {
12165
12556
  const msg = e instanceof Error ? e.message : String(e);
12166
- if (entry?.rules?.length) {
12557
+ if (entry?.rules?.length && !entry.blocked) {
12167
12558
  const ageSec2 = Math.round((Date.now() - entry.fetchedAt) / 1e3);
12168
12559
  process.stderr.write(
12169
12560
  `[VibeDefend] Action Guards: rule refresh failed (${msg}). Using cached rules (age=${ageSec2}s).
@@ -12178,9 +12569,9 @@ function makeGuardRulesCache(opts) {
12178
12569
  }
12179
12570
  }
12180
12571
  function readCache(projectId) {
12181
- if (!existsSync12(cacheFile)) return null;
12572
+ if (!existsSync14(cacheFile)) return null;
12182
12573
  try {
12183
- const raw = readFileSync11(cacheFile, "utf8");
12574
+ const raw = readFileSync13(cacheFile, "utf8");
12184
12575
  const data = JSON.parse(raw);
12185
12576
  if (data.projectId !== projectId) return null;
12186
12577
  return data;
@@ -12191,16 +12582,16 @@ function makeGuardRulesCache(opts) {
12191
12582
  function writeCache(entry) {
12192
12583
  try {
12193
12584
  ensureDirFor2(cacheFile);
12194
- writeFileSync6(cacheFile, JSON.stringify(entry), "utf8");
12585
+ writeFileSync8(cacheFile, JSON.stringify(entry), "utf8");
12195
12586
  } catch {
12196
12587
  }
12197
12588
  }
12198
12589
  return { refreshIfStale };
12199
12590
  }
12200
12591
  function ensureDirFor2(filePath) {
12201
- const dir = dirname5(filePath);
12202
- if (!existsSync12(dir)) {
12203
- mkdirSync5(dir, { recursive: true });
12592
+ const dir = dirname7(filePath);
12593
+ if (!existsSync14(dir)) {
12594
+ mkdirSync7(dir, { recursive: true });
12204
12595
  }
12205
12596
  }
12206
12597
  var DEFAULT_TIMEOUT_MS2 = 8e3;
@@ -12223,6 +12614,10 @@ async function fetchEffectiveRulesFromGateway(ctx, opts = {}) {
12223
12614
  return { rules: [], etag: ctx.etag ?? null, notModified: true };
12224
12615
  }
12225
12616
  if (!res.ok) {
12617
+ const blocked = await readVibedefendBlock(res);
12618
+ if (blocked) {
12619
+ return { rules: [], etag: null, notModified: false, blocked };
12620
+ }
12226
12621
  throw new Error(`HTTP ${res.status} from effective-rules endpoint`);
12227
12622
  }
12228
12623
  const body = await res.json();
@@ -12243,6 +12638,9 @@ function parseLlmJudgeHint(raw) {
12243
12638
  }
12244
12639
 
12245
12640
  // src/hooks/runtime/judge.ts
12641
+ function isJudgeBlocked(res) {
12642
+ return res !== null && "blocked" in res;
12643
+ }
12246
12644
  var DEFAULT_TIMEOUT_MS3 = 1500;
12247
12645
  async function callJudge(input, opts = {}) {
12248
12646
  const f = opts.fetchImpl ?? globalThis.fetch;
@@ -12266,7 +12664,11 @@ async function callJudge(input, opts = {}) {
12266
12664
  }),
12267
12665
  signal: AbortSignal.timeout(timeoutMs)
12268
12666
  });
12269
- if (!res.ok) return null;
12667
+ if (!res.ok) {
12668
+ const blocked = await readVibedefendBlock(res);
12669
+ return blocked ? { blocked } : null;
12670
+ }
12671
+ const usageWarning = readUsageWarning(res);
12270
12672
  const body = await res.json();
12271
12673
  const data = body.data ?? body;
12272
12674
  const decision = data["decision"];
@@ -12276,7 +12678,8 @@ async function callJudge(input, opts = {}) {
12276
12678
  reason: typeof data["reason"] === "string" ? data["reason"] : "",
12277
12679
  model: typeof data["model"] === "string" ? data["model"] : "",
12278
12680
  quotaRemaining: typeof data["quota_remaining"] === "number" ? data["quota_remaining"] : -1,
12279
- quotaExceeded: Boolean(data["quota_exceeded"])
12681
+ quotaExceeded: Boolean(data["quota_exceeded"]),
12682
+ ...usageWarning ? { usageWarning } : {}
12280
12683
  };
12281
12684
  } catch {
12282
12685
  return null;
@@ -12321,11 +12724,11 @@ function detectPromptInjection(text) {
12321
12724
  }
12322
12725
 
12323
12726
  // src/hooks/runtime/judge-cache.ts
12324
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync7, existsSync as existsSync13, mkdirSync as mkdirSync6 } from "node:fs";
12325
- import { dirname as dirname6, join as join10 } from "node:path";
12326
- import { homedir as homedir10 } from "node:os";
12327
- var DEFAULT_JUDGE_CACHE_FILE = join10(
12328
- homedir10(),
12727
+ import { readFileSync as readFileSync14, writeFileSync as writeFileSync9, existsSync as existsSync15, mkdirSync as mkdirSync8 } from "node:fs";
12728
+ import { dirname as dirname8, join as join12 } from "node:path";
12729
+ import { homedir as homedir12 } from "node:os";
12730
+ var DEFAULT_JUDGE_CACHE_FILE = join12(
12731
+ homedir12(),
12329
12732
  ".cybedefend",
12330
12733
  "guards-judge-cache.json"
12331
12734
  );
@@ -12335,9 +12738,9 @@ function makeJudgeCache(opts = {}) {
12335
12738
  const ttlMs = opts.ttlMs ?? DEFAULT_JUDGE_TTL_MS;
12336
12739
  const now = opts.now ?? (() => Date.now());
12337
12740
  function readAll() {
12338
- if (!existsSync13(cacheFile)) return {};
12741
+ if (!existsSync15(cacheFile)) return {};
12339
12742
  try {
12340
- return JSON.parse(readFileSync12(cacheFile, "utf8"));
12743
+ return JSON.parse(readFileSync14(cacheFile, "utf8"));
12341
12744
  } catch {
12342
12745
  return {};
12343
12746
  }
@@ -12357,36 +12760,36 @@ function makeJudgeCache(opts = {}) {
12357
12760
  }
12358
12761
  all[key] = { res, at: t };
12359
12762
  ensureDirFor3(cacheFile);
12360
- writeFileSync7(cacheFile, JSON.stringify(all), "utf8");
12763
+ writeFileSync9(cacheFile, JSON.stringify(all), "utf8");
12361
12764
  } catch {
12362
12765
  }
12363
12766
  }
12364
12767
  return { get, set };
12365
12768
  }
12366
12769
  function ensureDirFor3(filePath) {
12367
- const dir = dirname6(filePath);
12368
- if (!existsSync13(dir)) {
12369
- mkdirSync6(dir, { recursive: true });
12770
+ const dir = dirname8(filePath);
12771
+ if (!existsSync15(dir)) {
12772
+ mkdirSync8(dir, { recursive: true });
12370
12773
  }
12371
12774
  }
12372
12775
 
12373
12776
  // src/hooks/runtime/guard-check.ts
12374
- import { homedir as homedir12 } from "node:os";
12375
- import { join as join12 } from "node:path";
12777
+ import { homedir as homedir14 } from "node:os";
12778
+ import { join as join14 } from "node:path";
12376
12779
 
12377
12780
  // src/hooks/runtime/guard-violations-buffer.ts
12378
12781
  import {
12379
12782
  appendFileSync,
12380
- existsSync as existsSync14,
12381
- readFileSync as readFileSync13,
12382
- writeFileSync as writeFileSync8,
12383
- mkdirSync as mkdirSync7
12783
+ existsSync as existsSync16,
12784
+ readFileSync as readFileSync15,
12785
+ writeFileSync as writeFileSync10,
12786
+ mkdirSync as mkdirSync9
12384
12787
  } from "node:fs";
12385
- import { dirname as dirname7 } from "node:path";
12386
- import { homedir as homedir11 } from "node:os";
12387
- import { join as join11 } from "node:path";
12388
- var DEFAULT_BUFFER_FILE = join11(
12389
- homedir11(),
12788
+ import { dirname as dirname9 } from "node:path";
12789
+ import { homedir as homedir13 } from "node:os";
12790
+ import { join as join13 } from "node:path";
12791
+ var DEFAULT_BUFFER_FILE = join13(
12792
+ homedir13(),
12390
12793
  ".cybedefend",
12391
12794
  "guards-violations-buffer.jsonl"
12392
12795
  );
@@ -12405,10 +12808,10 @@ function makeViolationsBuffer(opts = {}) {
12405
12808
  }
12406
12809
  }
12407
12810
  async function flushViolations2(ctx, postFn) {
12408
- if (!existsSync14(bufferFile)) return;
12811
+ if (!existsSync16(bufferFile)) return;
12409
12812
  let content;
12410
12813
  try {
12411
- content = readFileSync13(bufferFile, "utf8").trim();
12814
+ content = readFileSync15(bufferFile, "utf8").trim();
12412
12815
  } catch {
12413
12816
  return;
12414
12817
  }
@@ -12427,7 +12830,7 @@ function makeViolationsBuffer(opts = {}) {
12427
12830
  }
12428
12831
  if (records.length === 0) {
12429
12832
  try {
12430
- writeFileSync8(bufferFile, "", "utf8");
12833
+ writeFileSync10(bufferFile, "", "utf8");
12431
12834
  } catch {
12432
12835
  }
12433
12836
  return;
@@ -12457,10 +12860,10 @@ function makeViolationsBuffer(opts = {}) {
12457
12860
  }
12458
12861
  try {
12459
12862
  if (failed.length === 0) {
12460
- writeFileSync8(bufferFile, "", "utf8");
12863
+ writeFileSync10(bufferFile, "", "utf8");
12461
12864
  } else {
12462
12865
  const serialised = failed.map((r) => JSON.stringify(r)).join("\n") + "\n";
12463
- writeFileSync8(bufferFile, serialised, "utf8");
12866
+ writeFileSync10(bufferFile, serialised, "utf8");
12464
12867
  }
12465
12868
  } catch {
12466
12869
  }
@@ -12468,9 +12871,9 @@ function makeViolationsBuffer(opts = {}) {
12468
12871
  return { bufferViolation: bufferViolation2, flushViolations: flushViolations2 };
12469
12872
  }
12470
12873
  function ensureDirFor4(filePath) {
12471
- const dir = dirname7(filePath);
12472
- if (!existsSync14(dir)) {
12473
- mkdirSync7(dir, { recursive: true });
12874
+ const dir = dirname9(filePath);
12875
+ if (!existsSync16(dir)) {
12876
+ mkdirSync9(dir, { recursive: true });
12474
12877
  }
12475
12878
  }
12476
12879
  var _defaultBuffer = makeViolationsBuffer();
@@ -12496,11 +12899,11 @@ function appendDebugLog(entry) {
12496
12899
  try {
12497
12900
  const homeDir = process.env.HOME ?? process.env.USERPROFILE ?? "";
12498
12901
  if (!homeDir) return;
12499
- const { mkdirSync: mkdirSync8, appendFileSync: appendFileSync2 } = __require("node:fs");
12500
- const { join: join13 } = __require("node:path");
12501
- const dir = join13(homeDir, ".cybedefend");
12502
- mkdirSync8(dir, { recursive: true });
12503
- appendFileSync2(join13(dir, "hook-debug.log"), entry + "\n", "utf8");
12902
+ const { mkdirSync: mkdirSync10, appendFileSync: appendFileSync2 } = __require("node:fs");
12903
+ const { join: join15 } = __require("node:path");
12904
+ const dir = join15(homeDir, ".cybedefend");
12905
+ mkdirSync10(dir, { recursive: true });
12906
+ appendFileSync2(join15(dir, "hook-debug.log"), entry + "\n", "utf8");
12504
12907
  } catch {
12505
12908
  }
12506
12909
  }
@@ -12671,6 +13074,28 @@ function stringifyTarget(t) {
12671
13074
  return `git ${t.subcommand}`;
12672
13075
  }
12673
13076
  }
13077
+ async function consultJudge(judge, sessionId, cacheKey, input) {
13078
+ const cached = judge.cacheGet(cacheKey);
13079
+ if (cached) return { kind: "result", res: cached };
13080
+ if (judge.usage?.isBlocked(sessionId)) return { kind: "blocked" };
13081
+ const answer = await judge.callJudge(input);
13082
+ if (isJudgeBlocked(answer)) {
13083
+ if (judge.usage) {
13084
+ judge.usage.markBlocked(sessionId);
13085
+ judge.usage.notifier.notifyBlock(sessionId, answer.blocked);
13086
+ } else {
13087
+ judge.warnOnce(sessionId, blockSentence(answer.blocked));
13088
+ }
13089
+ return { kind: "blocked" };
13090
+ }
13091
+ if (!answer) return { kind: "result", res: null };
13092
+ const { usageWarning, ...res } = answer;
13093
+ if (usageWarning) {
13094
+ judge.usage?.notifier.notifyWarning(sessionId, usageWarning);
13095
+ }
13096
+ judge.cacheSet(cacheKey, res);
13097
+ return { kind: "result", res };
13098
+ }
12674
13099
  async function runJudgeStep(args) {
12675
13100
  const {
12676
13101
  judge,
@@ -12699,30 +13124,30 @@ async function runJudgeStep(args) {
12699
13124
  const actionHash = hashTarget(targetStr);
12700
13125
  const ruleIds = llmRules.map((r) => r.id).sort().join(",");
12701
13126
  const cacheKey = `${actionHash}|${ruleIds}|confirm`;
12702
- let res = judge.cacheGet(cacheKey);
12703
- if (!res) {
12704
- res = await judge.callJudge({
12705
- apiBaseResolved: projectContext.apiBaseResolved,
12706
- token: projectContext.token,
12707
- projectId: request.projectId,
12708
- tool: request.tool,
12709
- agent: request.agent,
12710
- sessionId,
12711
- phase: "confirm",
12712
- // Judge over the FULL (schema-bounded 2048) redacted action — the default
12713
- // redactTarget cap of 256 (right for the DB-stored violation summary) would
12714
- // truncate a long dangerous action and risk an overturn on a partial view.
12715
- actionSummary: redactTarget(targetStr, 2048),
12716
- actionHash,
12717
- rules: llmRules.map((r) => ({
12718
- id: r.id,
12719
- name: r.name ?? r.id,
12720
- description: r.description ?? "",
12721
- severity: r.severity
12722
- }))
12723
- });
12724
- if (res) judge.cacheSet(cacheKey, res);
13127
+ const consultation = await consultJudge(judge, sessionId, cacheKey, {
13128
+ apiBaseResolved: projectContext.apiBaseResolved,
13129
+ token: projectContext.token,
13130
+ projectId: request.projectId,
13131
+ tool: request.tool,
13132
+ agent: request.agent,
13133
+ sessionId,
13134
+ phase: "confirm",
13135
+ // Judge over the FULL (schema-bounded 2048) redacted action — the default
13136
+ // redactTarget cap of 256 (right for the DB-stored violation summary) would
13137
+ // truncate a long dangerous action and risk an overturn on a partial view.
13138
+ actionSummary: redactTarget(targetStr, 2048),
13139
+ actionHash,
13140
+ rules: llmRules.map((r) => ({
13141
+ id: r.id,
13142
+ name: r.name ?? r.id,
13143
+ description: r.description ?? "",
13144
+ severity: r.severity
13145
+ }))
13146
+ });
13147
+ if (consultation.kind === "blocked") {
13148
+ return { engine: "llm", outcome: "quota_exceeded" };
12725
13149
  }
13150
+ const res = consultation.res;
12726
13151
  if (!res) {
12727
13152
  return {
12728
13153
  engine: "llm",
@@ -12785,27 +13210,25 @@ async function runCatchJudgeStep(args) {
12785
13210
  const actionHash = hashTarget(targetStr);
12786
13211
  const ruleIds = near.map((r) => r.id).sort();
12787
13212
  const cacheKey = `${actionHash}|${ruleIds.join(",")}|catch`;
12788
- let res = judge.cacheGet(cacheKey);
12789
- if (!res) {
12790
- res = await judge.callJudge({
12791
- apiBaseResolved: projectContext.apiBaseResolved,
12792
- token: projectContext.token,
12793
- projectId: request.projectId,
12794
- tool: request.tool,
12795
- agent: request.agent,
12796
- sessionId,
12797
- phase: "catch",
12798
- actionSummary: redactTarget(targetStr, 2048),
12799
- actionHash,
12800
- rules: near.map((r) => ({
12801
- id: r.id,
12802
- name: r.name ?? r.id,
12803
- description: r.description ?? "",
12804
- severity: r.severity
12805
- }))
12806
- });
12807
- if (res) judge.cacheSet(cacheKey, res);
12808
- }
13213
+ const consultation = await consultJudge(judge, sessionId, cacheKey, {
13214
+ apiBaseResolved: projectContext.apiBaseResolved,
13215
+ token: projectContext.token,
13216
+ projectId: request.projectId,
13217
+ tool: request.tool,
13218
+ agent: request.agent,
13219
+ sessionId,
13220
+ phase: "catch",
13221
+ actionSummary: redactTarget(targetStr, 2048),
13222
+ actionHash,
13223
+ rules: near.map((r) => ({
13224
+ id: r.id,
13225
+ name: r.name ?? r.id,
13226
+ description: r.description ?? "",
13227
+ severity: r.severity
13228
+ }))
13229
+ });
13230
+ if (consultation.kind === "blocked") return void 0;
13231
+ const res = consultation.res;
12809
13232
  if (!res) return void 0;
12810
13233
  if (res.quotaExceeded) {
12811
13234
  judge.warnOnce(
@@ -12851,6 +13274,9 @@ function emitUnverifiedBanner(reason) {
12851
13274
  }
12852
13275
  }
12853
13276
  }
13277
+ function sessionIdOf3(envelope) {
13278
+ return typeof envelope.session_id === "string" ? envelope.session_id : typeof envelope.sessionId === "string" ? envelope.sessionId : void 0;
13279
+ }
12854
13280
  async function runPreToolUseGuardWith(opts) {
12855
13281
  const {
12856
13282
  stdinEnvelope,
@@ -12866,11 +13292,19 @@ async function runPreToolUseGuardWith(opts) {
12866
13292
  );
12867
13293
  return 0;
12868
13294
  }
13295
+ if (rulesOutcome.kind === "blocked") {
13296
+ const sentence = opts.usageNotifier?.notifyBlock(
13297
+ sessionIdOf3(stdinEnvelope),
13298
+ rulesOutcome.block
13299
+ );
13300
+ if (sentence) emitAgentMessage("warn", sentence);
13301
+ return 0;
13302
+ }
12869
13303
  const rules = rulesOutcome.rules;
12870
13304
  const caller = sniff(stdinEnvelope);
12871
13305
  const partial = mapEnvelopeToGuardRequest(stdinEnvelope, caller.client);
12872
13306
  if (partial === null) return 0;
12873
- const sessionId = typeof stdinEnvelope.session_id === "string" ? stdinEnvelope.session_id : typeof stdinEnvelope.sessionId === "string" ? stdinEnvelope.sessionId : void 0;
13307
+ const sessionId = sessionIdOf3(stdinEnvelope);
12874
13308
  const guardRequest = {
12875
13309
  ...partial,
12876
13310
  projectId: projectContext.projectId,
@@ -13014,11 +13448,21 @@ function emitAgentMessage(verdict, message) {
13014
13448
  }
13015
13449
  function makeProductionJudgeDeps() {
13016
13450
  const cache = makeJudgeCache();
13017
- const warnDir = process.env.CYBEDEFEND_BASELINE_DIR ?? join12(homedir12(), ".cybedefend");
13451
+ const warnDir = process.env.CYBEDEFEND_BASELINE_DIR ?? join14(homedir14(), ".cybedefend");
13018
13452
  const warnTracker = defaultSessionTracker(
13019
- join12(warnDir, "guards-judge-warned-sessions.json")
13453
+ join14(warnDir, "guards-judge-warned-sessions.json")
13454
+ );
13455
+ const blockedTracker = defaultSessionTracker(
13456
+ join14(warnDir, "guards-judge-blocked-sessions.json")
13020
13457
  );
13021
13458
  return {
13459
+ usage: {
13460
+ notifier: makeUsageNotifier(),
13461
+ isBlocked: (sessionId) => sessionId ? blockedTracker.hasSeen(sessionId) : false,
13462
+ markBlocked: (sessionId) => {
13463
+ if (sessionId) void blockedTracker.markSeen(sessionId);
13464
+ }
13465
+ },
13022
13466
  callJudge,
13023
13467
  cacheGet: (k) => cache.get(k),
13024
13468
  cacheSet: (k, r) => cache.set(k, r),
@@ -13038,7 +13482,9 @@ async function runPreToolUseGuard() {
13038
13482
  const envelope = await readStdinJson();
13039
13483
  const config = loadRuntimeConfig();
13040
13484
  if (!config) return 0;
13041
- const { projectId, apiBase: configApiBase } = await resolveProjectIdWithApi();
13485
+ const folder = sessionFolderOf(envelope, sniff(envelope).client);
13486
+ if (isSessionInactive(envelope, { cwd: folder })) return 0;
13487
+ const { projectId, apiBase: configApiBase } = await resolveProjectIdWithApi(folder);
13042
13488
  if (!projectId) return 0;
13043
13489
  const apiBaseResolved = process.env.CYBEDEFEND_API_BASE ?? configApiBase ?? config.region.apiBase;
13044
13490
  const token = await resolveTokenWithRefresh(
@@ -13076,7 +13522,10 @@ async function runPreToolUseGuard() {
13076
13522
  judge: makeProductionJudgeDeps(),
13077
13523
  // v1.1 catch — OFF by default; opt in with CYBEDEFEND_JUDGE_CATCH=1 once
13078
13524
  // the call-rate gate is validated in the field.
13079
- judgeCatch: process.env.CYBEDEFEND_JUDGE_CATCH === "1"
13525
+ judgeCatch: process.env.CYBEDEFEND_JUDGE_CATCH === "1",
13526
+ // The refusal sentence goes out through the agent-message channels
13527
+ // (stderr included), so this notifier itself writes nothing.
13528
+ usageNotifier: makeUsageNotifier({ write: () => void 0 })
13080
13529
  });
13081
13530
  try {
13082
13531
  await buffer.flushViolations(