@pushary/agent-hooks 0.68.0 → 0.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +3 -2
  3. package/data/cursor-plugin/scripts/pushary-gate.mjs +70 -15
  4. package/data/vscode-plugin/scripts/pushary-gate.mjs +59 -11
  5. package/dist/bin/pushary-claude.js +21 -11
  6. package/dist/bin/pushary-clean.js +60 -22
  7. package/dist/bin/pushary-codex-hook.js +38 -23
  8. package/dist/bin/pushary-codex.js +1 -1
  9. package/dist/bin/pushary-connect.js +6 -4
  10. package/dist/bin/pushary-daemon.js +3 -3
  11. package/dist/bin/pushary-doctor.js +120 -28
  12. package/dist/bin/pushary-gemini-hook.js +32 -16
  13. package/dist/bin/pushary-hook.js +5 -5
  14. package/dist/bin/pushary-login.js +2 -2
  15. package/dist/bin/pushary-logout.js +2 -2
  16. package/dist/bin/pushary-mode.js +2 -2
  17. package/dist/bin/pushary-notification-hook.js +1 -1
  18. package/dist/bin/pushary-permission-denied-hook.js +3 -3
  19. package/dist/bin/pushary-permission-hook.js +3 -3
  20. package/dist/bin/pushary-post-hook.js +1 -1
  21. package/dist/bin/pushary-prompt-hook.js +1 -1
  22. package/dist/bin/pushary-session-end-hook.js +1 -1
  23. package/dist/bin/pushary-session-start-hook.js +1 -1
  24. package/dist/bin/pushary-setup.js +90 -13
  25. package/dist/bin/pushary-stats.js +2 -2
  26. package/dist/bin/pushary-status.js +4 -4
  27. package/dist/bin/pushary-stop-hook.js +1 -1
  28. package/dist/bin/pushary-stopfailure-hook.js +1 -1
  29. package/dist/bin/pushary-suggestions.js +2 -2
  30. package/dist/bin/pushary-upgrade.js +2 -2
  31. package/dist/bin/pushary-wait.js +2 -2
  32. package/dist/bin/pushary.js +1 -1
  33. package/dist/{chunk-JGH37QRB.js → chunk-7OYGFJYZ.js} +1 -1
  34. package/dist/chunk-B7R4YSAB.js +38 -0
  35. package/dist/{chunk-ATP6UQPH.js → chunk-BBK3TTU2.js} +1 -1
  36. package/dist/{chunk-XOUYM27W.js → chunk-FZIQSDTQ.js} +207 -113
  37. package/dist/chunk-GD275GEX.js +66 -0
  38. package/dist/{chunk-SONXGCZY.js → chunk-HU43EZ5Q.js} +45 -27
  39. package/dist/{chunk-KB4ODLGB.js → chunk-I4BYZIX4.js} +2 -0
  40. package/dist/{chunk-YMFZUXL5.js → chunk-MGFZXUUR.js} +57 -4
  41. package/dist/{chunk-YZT2CBC3.js → chunk-NQTRA7H5.js} +1 -1
  42. package/dist/src/index.d.ts +1 -0
  43. package/dist/src/index.js +3 -3
  44. package/package.json +1 -1
  45. package/dist/chunk-YHG74UFF.js +0 -12
@@ -54,6 +54,11 @@ var isWaitForAnswerResponse = (data) => {
54
54
 
55
55
  // src/policy.ts
56
56
  var CACHE_TTL_MS = 60 * 1e3;
57
+ var STALE_CACHE_MAX_MS = 12 * 60 * 60 * 1e3;
58
+ var policyRequestFor = (agentType) => ({
59
+ agent: agentType,
60
+ repoAware: agentType === "claude_code"
61
+ });
57
62
  var policyCacheFile = (apiKey, agent, repoAware = false) => {
58
63
  const parts = [apiKey, agent, repoAware ? "repoAware" : void 0].filter(Boolean);
59
64
  const hash = createHash("sha256").update(parts.join(":")).digest("hex").slice(0, 12);
@@ -88,18 +93,19 @@ var getPolicy = async (apiKey, expectedVersion, agent, repoAware = false) => {
88
93
  const cached = JSON.parse(stat);
89
94
  if (!isPolicyConfig(cached)) throw new Error("Corrupted cache");
90
95
  const versionStale = expectedVersion != null && (cached._policyVersion ?? null) !== expectedVersion;
91
- const ttlFresh = !cached._cachedAt || Date.now() - cached._cachedAt < CACHE_TTL_MS;
96
+ const age = cached._cachedAt ? Date.now() - cached._cachedAt : 0;
97
+ const ttlFresh = !cached._cachedAt || age < CACHE_TTL_MS;
92
98
  if (ttlFresh && !versionStale) {
93
99
  return cached;
94
100
  }
95
- staleCache = cached;
101
+ if (age < STALE_CACHE_MAX_MS) staleCache = cached;
96
102
  } catch {
97
103
  }
98
104
  }
99
105
  try {
100
106
  const policy = await fetchPolicy(apiKey, agent, repoAware);
101
107
  try {
102
- writeFileSync(path, JSON.stringify({ ...policy, _cachedAt: Date.now(), _policyVersion: expectedVersion ?? null }), "utf-8");
108
+ writeFileSync(path, JSON.stringify({ ...policy, _cachedAt: Date.now(), _policyVersion: expectedVersion ?? null }), { encoding: "utf-8", mode: 384 });
103
109
  } catch {
104
110
  }
105
111
  return policy;
@@ -130,6 +136,8 @@ var findBestMatch = (policies, toolName, args, repoKey) => {
130
136
  }
131
137
  return best;
132
138
  };
139
+ var isAutoApprove = (policy) => policy.timeoutSeconds === 0 && policy.timeoutAction === "approve";
140
+ var isAutoDeny = (policy) => policy.timeoutSeconds === 0 && policy.timeoutAction === "deny";
133
141
  var autoApprove = (tool) => ({
134
142
  tool,
135
143
  timeoutSeconds: 0,
@@ -162,7 +170,7 @@ var resolvePolicy = (config, toolName, modeOverride, toolInput, cwd, repoKey) =>
162
170
  if (!governedBySpecificRule && toolName === "Bash" && typeof arg === "string" && isSafeReadOnlyCommand(arg)) {
163
171
  base = autoApprove(base.tool);
164
172
  }
165
- const wouldAutoApprove = base.timeoutSeconds === 0 && base.timeoutAction === "approve";
173
+ const wouldAutoApprove = isAutoApprove(base);
166
174
  if (!governedBySpecificRule && wouldAutoApprove && toolName === "Bash" && typeof arg === "string" && classifyDecisionRisk(toolName, arg) === "destructive") {
167
175
  base = {
168
176
  tool: base.tool,
@@ -178,6 +186,57 @@ var resolvePolicy = (config, toolName, modeOverride, toolInput, cwd, repoKey) =>
178
186
  }
179
187
  return base;
180
188
  };
189
+ var restraintRank = (policy) => {
190
+ if (isAutoDeny(policy)) return 4;
191
+ if (isAutoApprove(policy)) return 0;
192
+ if (policy.mode === "push_only") return 3;
193
+ return policy.mode === "terminal_only" || policy.mode === "notify_only" ? 1 : 2;
194
+ };
195
+ var strictestPolicy = (a, b) => restraintRank(b) > restraintRank(a) ? b : a;
196
+ var resolvePolicyAcross = (config, toolName, modeOverride, toolInputs, cwd, repoKey) => {
197
+ if (toolInputs.length === 0) return resolvePolicy(config, toolName, modeOverride, void 0, cwd, repoKey);
198
+ return toolInputs.map((input) => resolvePolicy(config, toolName, modeOverride, input, cwd, repoKey)).reduce(strictestPolicy);
199
+ };
200
+ var isModeStateDegraded = (state) => state?.degraded === true;
201
+ var MODE_GRACE_MS = 60 * 1e3;
202
+ var modeStateCacheFile = (apiKey) => {
203
+ const hash = createHash("sha256").update(`mode:${apiKey}`).digest("hex").slice(0, 12);
204
+ return join(tmpdir(), `pushary-mode-${hash}.json`);
205
+ };
206
+ var readLastGoodMode = (apiKey) => {
207
+ try {
208
+ const path = modeStateCacheFile(apiKey);
209
+ if (!existsSync(path)) return null;
210
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
211
+ if (!parsed || typeof parsed.at !== "number" || !parsed.state) return null;
212
+ return parsed;
213
+ } catch {
214
+ return null;
215
+ }
216
+ };
217
+ var writeLastGoodMode = (apiKey, state) => {
218
+ try {
219
+ writeFileSync(modeStateCacheFile(apiKey), JSON.stringify({ state, at: Date.now() }), { encoding: "utf-8", mode: 384 });
220
+ } catch {
221
+ }
222
+ };
223
+ var unknownMode = () => ({
224
+ mode: null,
225
+ kill: false,
226
+ policyVersion: null,
227
+ relayUrl: null,
228
+ trainingConsent: false,
229
+ scope: null,
230
+ degraded: true
231
+ });
232
+ var degradedFallback = (apiKey, authoritative = false) => {
233
+ if (authoritative) return unknownMode();
234
+ const last = readLastGoodMode(apiKey);
235
+ if (last && Date.now() - last.at < MODE_GRACE_MS) {
236
+ return { ...last.state, degraded: false };
237
+ }
238
+ return unknownMode();
239
+ };
181
240
  var toPolicyVersion = (value) => typeof value === "string" || typeof value === "number" ? String(value) : null;
182
241
  var fetchModeState = async (apiKey, sessionId) => {
183
242
  try {
@@ -187,10 +246,12 @@ var fetchModeState = async (apiKey, sessionId) => {
187
246
  headers: { "Authorization": `Bearer ${apiKey}` },
188
247
  signal: AbortSignal.timeout(3e3)
189
248
  });
190
- if (!response.ok) return { mode: null, kill: false, policyVersion: null, relayUrl: null, trainingConsent: false, scope: null };
249
+ if (!response.ok) {
250
+ return degradedFallback(apiKey, response.status === 401 || response.status === 403);
251
+ }
191
252
  const data = await response.json();
192
253
  const mode = data.override?.mode;
193
- return {
254
+ const state = {
194
255
  mode: isApprovalMode(mode) ? mode : null,
195
256
  kill: data.kill === true,
196
257
  policyVersion: toPolicyVersion(data.policyVersion),
@@ -198,10 +259,13 @@ var fetchModeState = async (apiKey, sessionId) => {
198
259
  trainingConsent: data.trainingConsent === true,
199
260
  // Validated rather than cast: a malformed contract must read as no contract,
200
261
  // never as one that silently matches nothing.
201
- scope: isScopeContract(data.scope) ? data.scope : null
262
+ scope: isScopeContract(data.scope) ? data.scope : null,
263
+ degraded: false
202
264
  };
265
+ writeLastGoodMode(apiKey, state);
266
+ return state;
203
267
  } catch {
204
- return { mode: null, kill: false, policyVersion: null, relayUrl: null, trainingConsent: false, scope: null };
268
+ return degradedFallback(apiKey);
205
269
  }
206
270
  };
207
271
  var fetchModeOverride = async (apiKey) => (await fetchModeState(apiKey)).mode;
@@ -275,7 +339,7 @@ var TOOL_TARGET_MAX_LENGTH = 80;
275
339
  var deriveCommandHead = (command) => {
276
340
  if (typeof command !== "string") return void 0;
277
341
  const head = command.trim().split(/\s+/).slice(0, 2).join(" ");
278
- return head ? head.slice(0, TOOL_TARGET_MAX_LENGTH) : void 0;
342
+ return head ? redactSecrets(head).slice(0, TOOL_TARGET_MAX_LENGTH) : void 0;
279
343
  };
280
344
  var deriveToolTarget = (toolName, toolInput) => {
281
345
  if (toolName === "Bash" || toolName === "PowerShell" || toolName === "Monitor") {
@@ -300,7 +364,7 @@ var deriveReceiptCommandHead = (command) => {
300
364
  const [first, second] = tokens;
301
365
  const keep = first && second && SCRIPT_RUNNERS.has(first.toLowerCase()) && RUN_SUBCOMMANDS.has(second.toLowerCase()) ? 3 : 2;
302
366
  const head = tokens.slice(0, keep).join(" ");
303
- return head ? head.slice(0, TOOL_TARGET_MAX_LENGTH) : void 0;
367
+ return head ? redactSecrets(head).slice(0, TOOL_TARGET_MAX_LENGTH) : void 0;
304
368
  };
305
369
  var RECEIPT_TARGET_MAX_LENGTH = 256;
306
370
  var EXIT_CODE_FAILURE = /^Error: Exit code \d+/;
@@ -335,7 +399,9 @@ var deriveReceiptMeta = (toolName, toolInput, toolResult, cwd) => {
335
399
  if (typeof filePath !== "string" || !filePath) return void 0;
336
400
  return {
337
401
  kind: toolName === "Write" ? "write" : "edit",
338
- target: relativizeReceiptPath(filePath, cwd).slice(0, RECEIPT_TARGET_MAX_LENGTH),
402
+ // A path is an unlikely place for a credential but not an impossible one
403
+ // (a token in a temp directory name), and this field is persisted.
404
+ target: redactSecrets(relativizeReceiptPath(filePath, cwd)).slice(0, RECEIPT_TARGET_MAX_LENGTH),
339
405
  ok
340
406
  };
341
407
  }
@@ -441,9 +507,93 @@ var removePendingSession = (sessionId) => {
441
507
  }
442
508
  };
443
509
 
510
+ // src/repo.ts
511
+ import { existsSync as existsSync3, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
512
+ import { basename, dirname, isAbsolute as isAbsolute2, join as join3, resolve } from "path";
513
+ var MAX_PARENT_WALK = 64;
514
+ var GIT_FILE_PREFIX = "gitdir:";
515
+ var findGitDir = (startDir) => {
516
+ let current = resolve(startDir);
517
+ for (let depth = 0; depth < MAX_PARENT_WALK; depth += 1) {
518
+ const candidate = join3(current, ".git");
519
+ try {
520
+ if (existsSync3(candidate)) {
521
+ if (statSync2(candidate).isDirectory()) return candidate;
522
+ const pointer = readFileSync2(candidate, "utf-8").trim();
523
+ if (pointer.startsWith(GIT_FILE_PREFIX)) {
524
+ const target = pointer.slice(GIT_FILE_PREFIX.length).trim();
525
+ return isAbsolute2(target) ? target : resolve(current, target);
526
+ }
527
+ }
528
+ } catch {
529
+ }
530
+ const parent = dirname(current);
531
+ if (parent === current) return void 0;
532
+ current = parent;
533
+ }
534
+ return void 0;
535
+ };
536
+ var configPathFor = (gitDir) => {
537
+ const index = gitDir.replace(/\\/g, "/").indexOf("/worktrees/");
538
+ return index === -1 ? join3(gitDir, "config") : join3(gitDir.slice(0, index), "config");
539
+ };
540
+ var ORIGIN_SECTION = /^\[remote "origin"\]/;
541
+ var SECTION_HEADER = /^\[/;
542
+ var URL_LINE = /^url\s*=\s*(.+)$/;
543
+ var readOriginRemote = (gitDir) => {
544
+ try {
545
+ const configPath = configPathFor(gitDir);
546
+ if (!existsSync3(configPath)) return void 0;
547
+ let inOrigin = false;
548
+ for (const rawLine of readFileSync2(configPath, "utf-8").split("\n")) {
549
+ const line = rawLine.trim();
550
+ if (SECTION_HEADER.test(line)) {
551
+ inOrigin = ORIGIN_SECTION.test(line);
552
+ continue;
553
+ }
554
+ if (!inOrigin) continue;
555
+ const url = URL_LINE.exec(line);
556
+ if (url) return url[1]?.trim();
557
+ }
558
+ } catch {
559
+ }
560
+ return void 0;
561
+ };
562
+ var deriveRepoKey = (cwd) => {
563
+ const dir = cwd && cwd.length > 0 ? cwd : process.cwd();
564
+ try {
565
+ const gitDir = findGitDir(dir);
566
+ if (gitDir) {
567
+ const remote = readOriginRemote(gitDir);
568
+ const normalized = remote ? normalizeRepoRemote(remote) : void 0;
569
+ if (normalized) return normalized;
570
+ const root = gitDir.endsWith(".git") ? dirname(gitDir) : dir;
571
+ return localRepoKey(basename(root));
572
+ }
573
+ return localRepoKey(basename(resolve(dir)));
574
+ } catch {
575
+ return void 0;
576
+ }
577
+ };
578
+ var repoKeyCache = /* @__PURE__ */ new Map();
579
+ var REPO_KEY_ENV = "PUSHARY_REPO_KEY";
580
+ var repoKeyFor = (cwd) => {
581
+ const override = process.env[REPO_KEY_ENV];
582
+ if (typeof override === "string") {
583
+ const trimmed = override.trim();
584
+ if (trimmed.toLowerCase() === "off") return void 0;
585
+ if (trimmed.length > 0) return trimmed.toLowerCase();
586
+ }
587
+ const dir = cwd && cwd.length > 0 ? cwd : process.cwd();
588
+ if (repoKeyCache.has(dir)) return repoKeyCache.get(dir);
589
+ const derived = deriveRepoKey(dir);
590
+ repoKeyCache.set(dir, derived);
591
+ return derived;
592
+ };
593
+
444
594
  // src/usage.ts
445
- import { closeSync, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync2, readSync, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
446
- import { join as join3 } from "path";
595
+ import { closeSync, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync3, readSync, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
596
+ import { join as join4 } from "path";
447
597
  import { tmpdir as tmpdir3 } from "os";
448
598
  var DEFAULT_PRICES = [
449
599
  { match: "opus-4-1", in: 15, out: 75 },
@@ -479,12 +629,12 @@ var estimateCostUsd = (usage, model) => {
479
629
  const perTokenOut = price.out / 1e6;
480
630
  return usage.inputTokens * perTokenIn + usage.outputTokens * perTokenOut + usage.cacheCreationTokens * perTokenIn * CACHE_WRITE_MULTIPLIER + usage.cacheReadTokens * perTokenIn * CACHE_READ_MULTIPLIER;
481
631
  };
482
- var stateDir = () => process.env.PUSHARY_USAGE_DIR?.trim() || join3(tmpdir3(), "pushary-usage");
483
- var stateFile = (sessionId) => join3(stateDir(), sessionId.replace(/[^a-zA-Z0-9_-]/g, "_"));
632
+ var stateDir = () => process.env.PUSHARY_USAGE_DIR?.trim() || join4(tmpdir3(), "pushary-usage");
633
+ var stateFile = (sessionId) => join4(stateDir(), sessionId.replace(/[^a-zA-Z0-9_-]/g, "_"));
484
634
  var emptyState = () => ({ offset: 0, tokensIn: 0, tokensOut: 0, costUsd: 0, recentIds: [] });
485
635
  var readState = (path) => {
486
636
  try {
487
- const parsed = JSON.parse(readFileSync2(path, "utf-8"));
637
+ const parsed = JSON.parse(readFileSync3(path, "utf-8"));
488
638
  if (typeof parsed.offset === "number" && parsed.offset >= 0 && typeof parsed.tokensIn === "number" && typeof parsed.tokensOut === "number" && typeof parsed.costUsd === "number" && Array.isArray(parsed.recentIds)) {
489
639
  return {
490
640
  offset: parsed.offset,
@@ -559,7 +709,7 @@ var extractUserText = (content) => {
559
709
  };
560
710
  var readLastUserPrompt = (transcriptPath) => {
561
711
  try {
562
- const size = statSync2(transcriptPath).size;
712
+ const size = statSync3(transcriptPath).size;
563
713
  const start = Math.max(0, size - USER_PROMPT_TAIL_BYTES);
564
714
  const lines = readRange(transcriptPath, start, size).toString("utf-8").split("\n");
565
715
  for (let i = lines.length - 1; i >= 0; i -= 1) {
@@ -582,7 +732,7 @@ var readLastUserPrompt = (transcriptPath) => {
582
732
  };
583
733
  var readNewUsage = (transcriptPath, sessionId) => {
584
734
  try {
585
- const size = statSync2(transcriptPath).size;
735
+ const size = statSync3(transcriptPath).size;
586
736
  const path = stateFile(sessionId);
587
737
  let state = readState(path);
588
738
  if (size < state.offset) state = { ...emptyState(), recentIds: state.recentIds };
@@ -613,7 +763,7 @@ var readNewUsage = (transcriptPath, sessionId) => {
613
763
  };
614
764
 
615
765
  // src/codex-adapter.ts
616
- import { resolve } from "path";
766
+ import { resolve as resolve2 } from "path";
617
767
  var CODEX_AGENT = { type: "codex", label: "Codex" };
618
768
  var codexAllow = () => ({ kind: "allow" });
619
769
  var codexDeny = (reason) => ({ kind: "deny", reason });
@@ -679,10 +829,25 @@ var toPolicyLookup = (toolName, toolInput, cwd) => {
679
829
  const command = toolInput.command;
680
830
  const files = parseApplyPatchFiles(command);
681
831
  if (files.length === 1) {
682
- return { tool: "Edit", input: { file_path: cwd ? resolve(cwd, files[0]) : files[0] } };
832
+ return { tool: "Edit", input: { file_path: cwd ? resolve2(cwd, files[0]) : files[0] } };
683
833
  }
684
834
  return { tool: "Edit", input: typeof command === "string" ? { file_path: command } : {} };
685
835
  };
836
+ var toPolicyLookups = (toolName, toolInput, cwd) => {
837
+ if (toolName !== "apply_patch") {
838
+ const single = toPolicyLookup(toolName, toolInput, cwd);
839
+ return { tool: single.tool, inputs: [single.input] };
840
+ }
841
+ const files = parseApplyPatchFiles(toolInput.command);
842
+ if (files.length === 0) {
843
+ const single = toPolicyLookup(toolName, toolInput, cwd);
844
+ return { tool: single.tool, inputs: [single.input] };
845
+ }
846
+ return {
847
+ tool: "Edit",
848
+ inputs: files.map((file) => ({ file_path: cwd ? resolve2(cwd, file) : file }))
849
+ };
850
+ };
686
851
  var permissionTimeoutDecision = (timeoutAction) => {
687
852
  if (timeoutAction === "approve") return codexAllow();
688
853
  if (timeoutAction === "deny") return codexDeny("No response within timeout");
@@ -690,90 +855,6 @@ var permissionTimeoutDecision = (timeoutAction) => {
690
855
  };
691
856
  var preToolUseTimeoutDecision = (timeoutAction, denyReason = "No response within timeout") => timeoutAction === "deny" ? codexDeny(denyReason) : codexPass();
692
857
 
693
- // src/repo.ts
694
- import { existsSync as existsSync3, readFileSync as readFileSync3, statSync as statSync3 } from "fs";
695
- import { basename, dirname, isAbsolute as isAbsolute2, join as join4, resolve as resolve2 } from "path";
696
- var MAX_PARENT_WALK = 64;
697
- var GIT_FILE_PREFIX = "gitdir:";
698
- var findGitDir = (startDir) => {
699
- let current = resolve2(startDir);
700
- for (let depth = 0; depth < MAX_PARENT_WALK; depth += 1) {
701
- const candidate = join4(current, ".git");
702
- try {
703
- if (existsSync3(candidate)) {
704
- if (statSync3(candidate).isDirectory()) return candidate;
705
- const pointer = readFileSync3(candidate, "utf-8").trim();
706
- if (pointer.startsWith(GIT_FILE_PREFIX)) {
707
- const target = pointer.slice(GIT_FILE_PREFIX.length).trim();
708
- return isAbsolute2(target) ? target : resolve2(current, target);
709
- }
710
- }
711
- } catch {
712
- }
713
- const parent = dirname(current);
714
- if (parent === current) return void 0;
715
- current = parent;
716
- }
717
- return void 0;
718
- };
719
- var configPathFor = (gitDir) => {
720
- const index = gitDir.replace(/\\/g, "/").indexOf("/worktrees/");
721
- return index === -1 ? join4(gitDir, "config") : join4(gitDir.slice(0, index), "config");
722
- };
723
- var ORIGIN_SECTION = /^\[remote "origin"\]/;
724
- var SECTION_HEADER = /^\[/;
725
- var URL_LINE = /^url\s*=\s*(.+)$/;
726
- var readOriginRemote = (gitDir) => {
727
- try {
728
- const configPath = configPathFor(gitDir);
729
- if (!existsSync3(configPath)) return void 0;
730
- let inOrigin = false;
731
- for (const rawLine of readFileSync3(configPath, "utf-8").split("\n")) {
732
- const line = rawLine.trim();
733
- if (SECTION_HEADER.test(line)) {
734
- inOrigin = ORIGIN_SECTION.test(line);
735
- continue;
736
- }
737
- if (!inOrigin) continue;
738
- const url = URL_LINE.exec(line);
739
- if (url) return url[1]?.trim();
740
- }
741
- } catch {
742
- }
743
- return void 0;
744
- };
745
- var deriveRepoKey = (cwd) => {
746
- const dir = cwd && cwd.length > 0 ? cwd : process.cwd();
747
- try {
748
- const gitDir = findGitDir(dir);
749
- if (gitDir) {
750
- const remote = readOriginRemote(gitDir);
751
- const normalized = remote ? normalizeRepoRemote(remote) : void 0;
752
- if (normalized) return normalized;
753
- const root = gitDir.endsWith(".git") ? dirname(gitDir) : dir;
754
- return localRepoKey(basename(root));
755
- }
756
- return localRepoKey(basename(resolve2(dir)));
757
- } catch {
758
- return void 0;
759
- }
760
- };
761
- var repoKeyCache = /* @__PURE__ */ new Map();
762
- var REPO_KEY_ENV = "PUSHARY_REPO_KEY";
763
- var repoKeyFor = (cwd) => {
764
- const override = process.env[REPO_KEY_ENV];
765
- if (typeof override === "string") {
766
- const trimmed = override.trim();
767
- if (trimmed.toLowerCase() === "off") return void 0;
768
- if (trimmed.length > 0) return trimmed.toLowerCase();
769
- }
770
- const dir = cwd && cwd.length > 0 ? cwd : process.cwd();
771
- if (repoKeyCache.has(dir)) return repoKeyCache.get(dir);
772
- const derived = deriveRepoKey(dir);
773
- repoKeyCache.set(dir, derived);
774
- return derived;
775
- };
776
-
777
858
  // src/throttle.ts
778
859
  import { join as join5 } from "path";
779
860
  import { tmpdir as tmpdir4 } from "os";
@@ -870,31 +951,38 @@ var notifyLateAnswers = async (apiKey, late, agentName, sessionId) => {
870
951
  };
871
952
  var CLAUDE_CODE_AGENT = { type: "claude_code", label: "Claude Code" };
872
953
  var POLICY_CACHE_TTL_MS = 5 * 60 * 1e3;
873
- var readFreshCachedPolicy = (apiKey) => {
874
- const path = policyCacheFile(apiKey);
954
+ var readFreshCachedPolicy = (apiKey, agentType) => {
955
+ const request = policyRequestFor(agentType);
956
+ const path = policyCacheFile(apiKey, request.agent, request.repoAware);
875
957
  if (!existsSync5(path)) return null;
876
958
  const cached = JSON.parse(readFileSync4(path, "utf-8"));
877
959
  if (!isPolicyConfig(cached)) return null;
878
960
  if (!cached._cachedAt || Date.now() - cached._cachedAt >= POLICY_CACHE_TTL_MS) return null;
879
961
  return cached;
880
962
  };
881
- var deriveDecisionSource = (toolName, toolInput, liveMode, cwd) => {
963
+ var autoDecisionEnabled = () => {
964
+ const flag = process.env.PUSHARY_AUTO_DECISION_BEACON;
965
+ return flag === "1" || flag === "true";
966
+ };
967
+ var deriveDecisionSource = (toolName, toolInput, liveMode, agentType, cwd) => {
882
968
  try {
883
969
  if (liveMode.kill) return "terminal";
884
- const policy = readFreshCachedPolicy(getApiKey());
970
+ if (!autoDecisionEnabled()) return void 0;
971
+ const policy = readFreshCachedPolicy(getApiKey(), agentType);
885
972
  if (!policy) return void 0;
886
973
  const resolved = resolvePolicy(policy, toolName, liveMode.mode, toolInput, cwd, repoKeyFor(cwd));
887
- if (resolved.timeoutSeconds === 0 && resolved.timeoutAction === "approve") return "policy_auto";
974
+ if (isAutoApprove(resolved)) return "policy_auto";
888
975
  if (resolved.mode === "push_only" || resolved.mode === "push_first") return "human";
889
976
  return "terminal";
890
977
  } catch {
891
978
  return void 0;
892
979
  }
893
980
  };
894
- var deriveAutoDecisionBeacon = (toolName, toolInput, liveMode, cwd) => {
981
+ var deriveAutoDecisionBeacon = (toolName, toolInput, liveMode, agentType, cwd) => {
895
982
  try {
896
983
  if (liveMode.kill) return void 0;
897
- const policy = readFreshCachedPolicy(getApiKey());
984
+ if (!autoDecisionEnabled()) return void 0;
985
+ const policy = readFreshCachedPolicy(getApiKey(), agentType);
898
986
  if (!policy) return void 0;
899
987
  const resolved = resolvePolicy(policy, toolName, liveMode.mode, toolInput, cwd, repoKeyFor(cwd));
900
988
  if (resolved.timeoutSeconds !== 0 || resolved.timeoutAction !== "approve") return void 0;
@@ -980,8 +1068,8 @@ var handlePostToolUse = async (input, agent = CLAUDE_CODE_AGENT) => {
980
1068
  for (const l of late) removePendingQuestion(sessionKey, l.correlationId);
981
1069
  }
982
1070
  const isMirrorOnly = NON_GATED_MIRROR_TOOLS.has(input.tool_name);
983
- const decisionSource = isMirrorOnly ? void 0 : deriveDecisionSource(lookup.tool, lookup.input, liveMode, input.cwd);
984
- const beacon = !isMirrorOnly && decisionSource === "policy_auto" && throttlePass(`autodecision:${sessionKey}:${lookup.tool}`, AUTO_DECISION_WINDOW_MS) ? deriveAutoDecisionBeacon(lookup.tool, lookup.input, liveMode, input.cwd) : void 0;
1071
+ const decisionSource = isMirrorOnly ? void 0 : deriveDecisionSource(lookup.tool, lookup.input, liveMode, agent.type, input.cwd);
1072
+ const beacon = !isMirrorOnly && decisionSource === "policy_auto" && throttlePass(`autodecision:${sessionKey}:${lookup.tool}`, AUTO_DECISION_WINDOW_MS) ? deriveAutoDecisionBeacon(lookup.tool, lookup.input, liveMode, agent.type, input.cwd) : void 0;
985
1073
  const toolReport = reportEvent({
986
1074
  event: isError ? "tool_error" : "tool_complete",
987
1075
  agentType: agent.type,
@@ -1235,7 +1323,12 @@ var handleStopFailure = async (input, agent = CLAUDE_CODE_AGENT) => {
1235
1323
 
1236
1324
  export {
1237
1325
  getPolicy,
1326
+ isAutoApprove,
1327
+ isAutoDeny,
1328
+ resolveAutoResolveOrigin,
1238
1329
  resolvePolicy,
1330
+ resolvePolicyAcross,
1331
+ isModeStateDegraded,
1239
1332
  fetchModeState,
1240
1333
  fetchModeOverride,
1241
1334
  askUser,
@@ -1260,6 +1353,7 @@ export {
1260
1353
  toCodexWire,
1261
1354
  describeApplyPatch,
1262
1355
  toPolicyLookup,
1356
+ toPolicyLookups,
1263
1357
  permissionTimeoutDecision,
1264
1358
  preToolUseTimeoutDecision,
1265
1359
  readLastPrompt,
@@ -0,0 +1,66 @@
1
+ import {
2
+ isAutoApprove,
3
+ isAutoDeny,
4
+ isModeStateDegraded,
5
+ resolveAutoResolveOrigin,
6
+ resolvePolicyAcross
7
+ } from "./chunk-FZIQSDTQ.js";
8
+ import {
9
+ evaluateScope,
10
+ scopeChangeReason,
11
+ scopeRequiresHuman
12
+ } from "./chunk-DQAN3JQP.js";
13
+
14
+ // src/answer.ts
15
+ var denyReasonFrom = (value, note) => {
16
+ const trimmed = note?.trim();
17
+ if (trimmed) return `Denied from your phone: ${trimmed}`;
18
+ return value && value !== "no" && value !== "yes" ? `Denied from your phone: ${value}` : "Denied via push notification";
19
+ };
20
+ var isDeferAnswer = (value) => value === "defer";
21
+
22
+ // src/gate.ts
23
+ var KILL_REASON = "Stopped by user, this agent was halted from Pushary";
24
+ var strictestScopeVerdict = (scope, paths) => {
25
+ if (paths.length === 0) return evaluateScope(scope, void 0);
26
+ let worst = { outcome: "in_scope" };
27
+ for (const path of paths) {
28
+ const verdict = evaluateScope(scope, path);
29
+ if (verdict.outcome === "off_limits") return verdict;
30
+ if (verdict.outcome === "out_of_scope") worst = verdict;
31
+ }
32
+ return worst;
33
+ };
34
+ var survivesDegraded = (config, toolName, toolInputs, cwd, repoKey) => toolInputs.length === 1 && resolveAutoResolveOrigin(config, toolName, toolInputs[0], cwd, repoKey) === "safe_readonly";
35
+ var resolveGate = (input) => {
36
+ const { modeState, config, toolName, toolInputs, cwd, repoKey } = input;
37
+ if (modeState.kill) return { kind: "kill", reason: KILL_REASON };
38
+ const policy = resolvePolicyAcross(config, toolName, modeState.mode, toolInputs, cwd, repoKey);
39
+ const autoApproves = isAutoApprove(policy);
40
+ const autoDenies = isAutoDeny(policy);
41
+ if (autoDenies) return { kind: "deny", policy, reason: `Denied by policy for ${policy.tool}` };
42
+ if (isModeStateDegraded(modeState)) {
43
+ if (autoApproves) {
44
+ return survivesDegraded(config, toolName, toolInputs, cwd, repoKey) ? { kind: "allow", policy } : { kind: "defer", policy };
45
+ }
46
+ return { kind: "gate", policy };
47
+ }
48
+ const scopeVerdict = modeState.scope ? strictestScopeVerdict(modeState.scope, input.scopePaths ?? []) : { outcome: "no_contract" };
49
+ if (autoApproves) {
50
+ if (!scopeRequiresHuman(scopeVerdict)) return { kind: "allow", policy };
51
+ }
52
+ const scopeReason = scopeChangeReason(scopeVerdict);
53
+ return {
54
+ kind: "gate",
55
+ policy,
56
+ scopeReason,
57
+ scopePath: scopeReason && "path" in scopeVerdict ? scopeVerdict.path : void 0
58
+ };
59
+ };
60
+
61
+ export {
62
+ denyReasonFrom,
63
+ isDeferAnswer,
64
+ KILL_REASON,
65
+ resolveGate
66
+ };