@pushary/agent-hooks 0.50.0 → 0.52.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.
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleSessionEnd
4
- } from "../chunk-SCXPHMAS.js";
4
+ } from "../chunk-5U3YC3BZ.js";
5
5
  import "../chunk-RN3NOEJF.js";
6
6
  import "../chunk-DWED7BS3.js";
7
- import "../chunk-WI3F6XNZ.js";
7
+ import "../chunk-F4DGHFIW.js";
8
8
  import "../chunk-NKXSILEW.js";
9
9
 
10
10
  // bin/pushary-session-end-hook.ts
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleSessionStart
4
- } from "../chunk-SCXPHMAS.js";
4
+ } from "../chunk-5U3YC3BZ.js";
5
5
  import "../chunk-RN3NOEJF.js";
6
6
  import "../chunk-DWED7BS3.js";
7
- import "../chunk-WI3F6XNZ.js";
7
+ import "../chunk-F4DGHFIW.js";
8
8
  import "../chunk-NKXSILEW.js";
9
9
 
10
10
  // bin/pushary-session-start-hook.ts
@@ -13,7 +13,7 @@ import {
13
13
  renderAgentInstructions,
14
14
  renderProjectAgentInstructions,
15
15
  writeInstructionBlock
16
- } from "../chunk-FDRSMTJS.js";
16
+ } from "../chunk-G3Y5HB4P.js";
17
17
  import {
18
18
  execNpm,
19
19
  npmErrorMessage,
@@ -25,15 +25,15 @@ import {
25
25
  addClaudeMcpServer,
26
26
  addPusharyHooks,
27
27
  addPusharyToolPermissions
28
- } from "../chunk-PUT2N5VA.js";
28
+ } from "../chunk-YTW7BZUC.js";
29
29
  import {
30
30
  reportEvent
31
- } from "../chunk-SCXPHMAS.js";
31
+ } from "../chunk-5U3YC3BZ.js";
32
32
  import "../chunk-RN3NOEJF.js";
33
33
  import "../chunk-DWED7BS3.js";
34
34
  import {
35
35
  isValidApiKey
36
- } from "../chunk-WI3F6XNZ.js";
36
+ } from "../chunk-F4DGHFIW.js";
37
37
  import "../chunk-NKXSILEW.js";
38
38
 
39
39
  // bin/pushary-setup.ts
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  readLedgerSummary
4
- } from "../chunk-V42K3XQI.js";
5
- import "../chunk-WI3F6XNZ.js";
4
+ } from "../chunk-CANZPNGD.js";
5
+ import "../chunk-F4DGHFIW.js";
6
6
  import {
7
7
  getApiKey
8
8
  } from "../chunk-NKXSILEW.js";
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleStop
4
- } from "../chunk-SCXPHMAS.js";
4
+ } from "../chunk-5U3YC3BZ.js";
5
5
  import "../chunk-RN3NOEJF.js";
6
6
  import "../chunk-DWED7BS3.js";
7
- import "../chunk-WI3F6XNZ.js";
7
+ import "../chunk-F4DGHFIW.js";
8
8
  import "../chunk-NKXSILEW.js";
9
9
 
10
10
  // bin/pushary-stop-hook.ts
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleStopFailure
4
- } from "../chunk-SCXPHMAS.js";
4
+ } from "../chunk-5U3YC3BZ.js";
5
5
  import "../chunk-RN3NOEJF.js";
6
6
  import "../chunk-DWED7BS3.js";
7
- import "../chunk-WI3F6XNZ.js";
7
+ import "../chunk-F4DGHFIW.js";
8
8
  import "../chunk-NKXSILEW.js";
9
9
 
10
10
  // bin/pushary-stopfailure-hook.ts
@@ -8,8 +8,8 @@ import {
8
8
  addClaudeMcpServer,
9
9
  addPusharyHooks,
10
10
  addPusharyToolPermissions
11
- } from "../chunk-PUT2N5VA.js";
12
- import "../chunk-WI3F6XNZ.js";
11
+ } from "../chunk-YTW7BZUC.js";
12
+ import "../chunk-F4DGHFIW.js";
13
13
  import {
14
14
  getApiKey
15
15
  } from "../chunk-NKXSILEW.js";
@@ -16,7 +16,7 @@ import {
16
16
  matchToolPattern,
17
17
  redactSecrets,
18
18
  redactSecretsDeep
19
- } from "./chunk-WI3F6XNZ.js";
19
+ } from "./chunk-F4DGHFIW.js";
20
20
  import {
21
21
  getApiKey,
22
22
  getBaseUrl
@@ -168,17 +168,18 @@ var fetchModeState = async (apiKey, sessionId) => {
168
168
  headers: { "Authorization": `Bearer ${apiKey}` },
169
169
  signal: AbortSignal.timeout(3e3)
170
170
  });
171
- if (!response.ok) return { mode: null, kill: false, policyVersion: null, relayUrl: null };
171
+ if (!response.ok) return { mode: null, kill: false, policyVersion: null, relayUrl: null, trainingConsent: false };
172
172
  const data = await response.json();
173
173
  const mode = data.override?.mode;
174
174
  return {
175
175
  mode: isApprovalMode(mode) ? mode : null,
176
176
  kill: data.kill === true,
177
177
  policyVersion: toPolicyVersion(data.policyVersion),
178
- relayUrl: typeof data.relayUrl === "string" && data.relayUrl.length > 0 ? data.relayUrl : null
178
+ relayUrl: typeof data.relayUrl === "string" && data.relayUrl.length > 0 ? data.relayUrl : null,
179
+ trainingConsent: data.trainingConsent === true
179
180
  };
180
181
  } catch {
181
- return { mode: null, kill: false, policyVersion: null, relayUrl: null };
182
+ return { mode: null, kill: false, policyVersion: null, relayUrl: null, trainingConsent: false };
182
183
  }
183
184
  };
184
185
  var fetchModeOverride = async (apiKey) => (await fetchModeState(apiKey)).mode;
@@ -807,6 +808,9 @@ var reportEvent = async (event, options = {}) => {
807
808
  ...event,
808
809
  machineId: event.machineId ?? getMachineId(),
809
810
  installMode: detectInstallMode(),
811
+ // Link-back for a phone-spawned session: forward the spawn id the daemon
812
+ // set in the environment so the server can map spawnId -> sessionId.
813
+ ...process.env.PUSHARY_SPAWN_ID ? { spawnId: process.env.PUSHARY_SPAWN_ID } : {},
810
814
  // Advertise that this client reads `pendingCommand` back off the
811
815
  // response, so the server may drain the phone-queued instruction on
812
816
  // activity events (tool_complete/tool_error/user_prompt), not only Stop.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  isSafeReadOnlyCommand
3
- } from "./chunk-WI3F6XNZ.js";
3
+ } from "./chunk-F4DGHFIW.js";
4
4
 
5
5
  // src/ledger.ts
6
6
  import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync } from "fs";
@@ -296,6 +296,19 @@ var APPROVAL_FATIGUE_FLAG_PATTERNS = [
296
296
  /\bterraform\s+(apply|destroy)\b/i,
297
297
  /\bdocker\s+system\s+prune\b/i
298
298
  ];
299
+ var DELTA_STOPWORDS = /* @__PURE__ */ new Set([
300
+ "the",
301
+ "a",
302
+ "an",
303
+ "to",
304
+ "of",
305
+ "for",
306
+ "and",
307
+ "is",
308
+ "in",
309
+ "on",
310
+ "with"
311
+ ]);
299
312
  var HIGH_CONSEQUENCE_PATTERNS = [
300
313
  /(^|[\\/])\.env(\.|$|[\\/])/i,
301
314
  /secrets?/i,
@@ -304,6 +317,74 @@ var HIGH_CONSEQUENCE_PATTERNS = [
304
317
  /\.github[\\/]workflows/i,
305
318
  /package\.json/i
306
319
  ];
320
+ var isCommandTool = (toolName) => {
321
+ const name = (toolName ?? "").toLowerCase();
322
+ return name === "bash" || name === "shell" || name === "run_shell_command" || name === "apply_patch" || name.includes("command");
323
+ };
324
+ var normalizeTarget = (toolName, toolTarget, actionBody) => {
325
+ const command = isCommandTool(toolName);
326
+ const target = toolTarget ?? "";
327
+ const body = actionBody ?? "";
328
+ const commandText = command ? body.length > 0 ? body : target : `${target}
329
+ ${body}`;
330
+ const path = command ? "" : target;
331
+ const scanText = command ? commandText : `${path}
332
+ ${body}`;
333
+ const highConsequence = HIGH_CONSEQUENCE_PATTERNS.some((re) => re.test(scanText));
334
+ return { isCommand: command, commandText, path, highConsequence };
335
+ };
336
+ var isDestructive = (toolName, commandText) => {
337
+ const haystack = `${toolName ?? ""}
338
+ ${commandText}`;
339
+ return APPROVAL_FATIGUE_FLAG_PATTERNS.some((re) => re.test(haystack));
340
+ };
341
+ var addAlnumTokens = (source, set) => {
342
+ for (const raw of source.toLowerCase().split(/[^a-z0-9]+/)) {
343
+ if (raw) set.add(raw);
344
+ }
345
+ };
346
+ var tokenize = (text) => {
347
+ const set = /* @__PURE__ */ new Set();
348
+ for (const raw of text.toLowerCase().split(/[^a-z0-9]+/)) {
349
+ if (raw && !DELTA_STOPWORDS.has(raw)) set.add(raw);
350
+ }
351
+ return set;
352
+ };
353
+ var targetTokenSet = (target) => {
354
+ const set = /* @__PURE__ */ new Set();
355
+ if (target.isCommand) {
356
+ const parts = target.commandText.trim().split(/\s+/).filter(Boolean);
357
+ if (parts[0]) addAlnumTokens(basenameOf(parts[0]), set);
358
+ if (parts[1]) addAlnumTokens(basenameOf(parts[1]), set);
359
+ } else {
360
+ addAlnumTokens(target.path, set);
361
+ }
362
+ return set;
363
+ };
364
+ var intersectionSize = (a, b) => {
365
+ const [small, large] = a.size <= b.size ? [a, b] : [b, a];
366
+ let count = 0;
367
+ for (const token of small) if (large.has(token)) count += 1;
368
+ return count;
369
+ };
370
+ var computeDeltaSignal = (input) => {
371
+ const target = normalizeTarget(input.toolName, input.toolTarget, input.actionBody);
372
+ const destructive = isDestructive(input.toolName, target.commandText);
373
+ if (!input.intent || input.intent.trim().length === 0) {
374
+ return { deltaSignal: "unclear", deltaScore: 0, remoteApprove: !destructive };
375
+ }
376
+ const intentTokens = tokenize(input.intent);
377
+ const targetTokens = targetTokenSet(target);
378
+ const overlap = intersectionSize(intentTokens, targetTokens);
379
+ const zeroOverlap = overlap === 0 && targetTokens.size > 0;
380
+ if (zeroOverlap && (destructive || target.highConsequence)) {
381
+ return { deltaSignal: "mismatch", deltaScore: 1, remoteApprove: false };
382
+ }
383
+ if (zeroOverlap) {
384
+ return { deltaSignal: "unclear", deltaScore: 0.5, remoteApprove: true };
385
+ }
386
+ return { deltaSignal: "match", deltaScore: 0, remoteApprove: !destructive };
387
+ };
307
388
  var SECRET_REDACTION_RULES = [
308
389
  { pattern: /-----BEGIN[A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END[A-Z0-9 ]*PRIVATE KEY-----/g, replacement: "[redacted key]" },
309
390
  { pattern: /\bsk-[A-Za-z0-9_-]{16,}\b/g, replacement: "[redacted]" },
@@ -335,6 +416,164 @@ var classifyDecisionRisk = (toolName, toolTarget) => {
335
416
  if (matchesAny(HIGH_CONSEQUENCE_PATTERNS)) return "elevated";
336
417
  return "standard";
337
418
  };
419
+ var POLICY_CAPABILITIES = ["read", "edit", "shell", "network", "destructive"];
420
+ var CAPABILITY_PATTERNS = {
421
+ read: ["Read", "Grep", "Glob", "LS", "NotebookRead"],
422
+ edit: ["Edit", "Write"],
423
+ shell: ["Bash"],
424
+ network: [
425
+ "WebFetch",
426
+ "Bash(curl:*)",
427
+ "Bash(wget:*)",
428
+ "Bash(nc:*)",
429
+ "Bash(ssh:*)",
430
+ "Bash(scp:*)",
431
+ "Bash(npm install:*)",
432
+ "Bash(npm i:*)",
433
+ "Bash(pnpm add:*)",
434
+ "Bash(yarn add:*)",
435
+ "Bash(bun add:*)",
436
+ "Bash(pip install:*)",
437
+ "Bash(brew install:*)",
438
+ "Bash(apt:*)",
439
+ "Bash(apt-get:*)"
440
+ ],
441
+ destructive: [
442
+ "Bash(rm:*)",
443
+ "Bash(rmdir:*)",
444
+ "Bash(sudo:*)",
445
+ "Bash(git push:*)",
446
+ "Bash(git reset --hard:*)",
447
+ "Bash(git clean:*)",
448
+ "Bash(npm publish:*)",
449
+ "Bash(kubectl delete:*)",
450
+ "Bash(terraform apply:*)",
451
+ "Bash(terraform destroy:*)",
452
+ "Bash(docker system prune:*)",
453
+ "Bash(dropdb:*)",
454
+ "Bash(mkfs:*)"
455
+ ]
456
+ };
457
+ var DECISION_EPISODE_SCHEMA_VERSION = 1;
458
+ var DECISION_FEATURE_EXTRACTOR = `pushary-hooks/schema-${DECISION_EPISODE_SCHEMA_VERSION}`;
459
+ var EPISODE_CAPABILITIES = [...POLICY_CAPABILITIES, "other"];
460
+ var NETWORK_COMMAND_PATTERNS = [
461
+ /\bcurl\b/i,
462
+ /\bwget\b/i,
463
+ /\bnc\b/i,
464
+ /\bssh\b/i,
465
+ /\bscp\b/i,
466
+ /\b(?:npm|pnpm|yarn|bun)\s+(?:install|i|add)\b/i,
467
+ /\bpip\s+install\b/i,
468
+ /\bbrew\s+install\b/i,
469
+ /\bapt(?:-get)?\s+(?:install|update|upgrade)\b/i
470
+ ];
471
+ var SECRET_TARGET_PATTERNS = [
472
+ /(^|[\\/])\.env(\.|$|[\\/])/i,
473
+ /secrets?/i,
474
+ /credentials?/i,
475
+ /private[_-]?key/i,
476
+ /\.pem$/i
477
+ ];
478
+ var READ_TOOL_NAMES = new Set(CAPABILITY_PATTERNS.read);
479
+ var EDIT_TOOL_NAMES = new Set(CAPABILITY_PATTERNS.edit);
480
+ var deriveEpisodeCapability = (toolName, commandText) => {
481
+ if (READ_TOOL_NAMES.has(toolName)) return "read";
482
+ if (EDIT_TOOL_NAMES.has(toolName)) return "edit";
483
+ if (toolName === "WebFetch" || toolName === "WebSearch") return "network";
484
+ if (isCommandTool(toolName)) {
485
+ if (isDestructive(toolName, commandText)) return "destructive";
486
+ if (NETWORK_COMMAND_PATTERNS.some((re) => re.test(commandText))) return "network";
487
+ return "shell";
488
+ }
489
+ if (isDestructive(toolName, commandText)) return "destructive";
490
+ return "other";
491
+ };
492
+ var deriveTargetClass = (path) => {
493
+ const p = path.toLowerCase();
494
+ if (!p) return "other";
495
+ if (SECRET_TARGET_PATTERNS.some((re) => re.test(p))) return "secret";
496
+ if (/(^|[\\/])(?:tests?|__tests__|spec)([\\/]|$)|\.(?:test|spec)\.[a-z0-9]+$/i.test(p)) return "test";
497
+ if (/\.(?:md|mdx|txt|rst)$|(^|[\\/])docs?([\\/]|$)/i.test(p)) return "docs";
498
+ if (/(^|[\\/])(?:package\.json|tsconfig[^\\/]*\.json|\.[a-z]+rc)$|\.(?:json|ya?ml|toml|ini|conf|cfg)$/i.test(p)) return "config";
499
+ if (/\.(?:ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|rb|php|c|cc|cpp|h|hpp|cs|swift|kt|scala|sh)$/i.test(p)) return "source";
500
+ return "other";
501
+ };
502
+ var bucketArgCount = (n) => n <= 0 ? "none" : n <= 3 ? "few" : "many";
503
+ var bucketLength = (n) => n < 40 ? "short" : n < 200 ? "medium" : "long";
504
+ var deriveArgShape = (isCommand, commandText, path) => {
505
+ const text = isCommand ? commandText : path;
506
+ const tokenCount = isCommand ? text.trim().split(/\s+/).filter(Boolean).length : 0;
507
+ return {
508
+ hasPipe: isCommand && /(?<!\|)\|(?!\|)/.test(text),
509
+ hasRedirect: isCommand && /(?<![0-9])[<>]/.test(text),
510
+ hasChain: isCommand && /&&|\|\||;/.test(text),
511
+ hasSudo: /\bsudo\b/i.test(text),
512
+ hasGlob: isCommand && /[*?]/.test(text),
513
+ hasSubshell: isCommand && /\$\(|`/.test(text),
514
+ touchesSecretPath: SECRET_TARGET_PATTERNS.some((re) => re.test(text)),
515
+ // The command word itself is not an argument, so subtract one.
516
+ argCountBucket: bucketArgCount(isCommand ? Math.max(0, tokenCount - 1) : 0),
517
+ lengthBucket: bucketLength(text.length)
518
+ };
519
+ };
520
+ var deriveReversibility = (capability, riskClass) => {
521
+ if (capability === "read") return "none";
522
+ if (capability === "destructive" || riskClass === "destructive") return "irreversible";
523
+ return "reversible";
524
+ };
525
+ var deriveBlastRadius = (capability, riskClass, targetClass) => {
526
+ if (capability === "read") return "low";
527
+ if (capability === "destructive" || riskClass === "destructive" || targetClass === "secret") return "high";
528
+ if (capability === "network" || capability === "shell" || riskClass === "elevated" || targetClass === "config") return "medium";
529
+ return "low";
530
+ };
531
+ var deriveLabelTier = (origin, decision) => {
532
+ if (origin === "autopilot") return "none";
533
+ if (origin === "human" && (decision === "approve" || decision === "deny")) return "bronze";
534
+ return "bronze";
535
+ };
536
+ var buildDecisionEpisodeFeatures = (input) => {
537
+ const isCommand = isCommandTool(input.toolName);
538
+ const raw = input.commandOrPath ?? input.toolTarget ?? "";
539
+ const commandText = isCommand ? raw : "";
540
+ const path = isCommand ? "" : raw;
541
+ const capability = deriveEpisodeCapability(input.toolName, commandText);
542
+ const riskClass = classifyDecisionRisk(input.toolName, input.toolTarget ?? raw);
543
+ const targetClass = isCommand ? SECRET_TARGET_PATTERNS.some((re) => re.test(commandText)) ? "secret" : "other" : deriveTargetClass(path);
544
+ const argShape = deriveArgShape(isCommand, commandText, path);
545
+ const { deltaSignal } = computeDeltaSignal({
546
+ intent: input.intent,
547
+ toolName: input.toolName,
548
+ toolTarget: input.toolTarget,
549
+ actionBody: input.commandOrPath
550
+ });
551
+ return {
552
+ schemaVersion: DECISION_EPISODE_SCHEMA_VERSION,
553
+ extractorVersion: DECISION_FEATURE_EXTRACTOR,
554
+ capability,
555
+ toolFamily: input.toolName.trim().slice(0, 40),
556
+ riskClass,
557
+ targetClass,
558
+ argShape,
559
+ reversibility: deriveReversibility(capability, riskClass),
560
+ blastRadius: deriveBlastRadius(capability, riskClass, targetClass),
561
+ deltaSignal,
562
+ preset: input.preset,
563
+ // A policy artifact (usually canonical like `Bash(rm:*)`), but a user could
564
+ // author a custom pattern, so scrub secrets defensively before it is stored.
565
+ matchedPattern: input.matchedPattern ? redactSecretsDeep(input.matchedPattern).slice(0, 120) : void 0,
566
+ policyMode: input.policyMode,
567
+ requestedGate: input.requestedGate,
568
+ timeoutSeconds: input.timeoutSeconds,
569
+ timeoutAction: input.timeoutAction,
570
+ decision: input.decision,
571
+ origin: input.origin,
572
+ latencyMs: input.latencyMs,
573
+ labelTier: deriveLabelTier(input.origin, input.decision),
574
+ agentType: input.agentType
575
+ };
576
+ };
338
577
 
339
578
  export {
340
579
  isApprovalMode,
@@ -351,5 +590,6 @@ export {
351
590
  DECISION_LINE_MAX,
352
591
  redactSecrets,
353
592
  redactSecretsDeep,
354
- classifyDecisionRisk
593
+ classifyDecisionRisk,
594
+ buildDecisionEpisodeFeatures
355
595
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  HOOK_BUDGETS
3
- } from "./chunk-WI3F6XNZ.js";
3
+ } from "./chunk-F4DGHFIW.js";
4
4
 
5
5
  // src/codex-config.ts
6
6
  import { createHash } from "crypto";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  PRETOOLUSE_HANDLED_TOOLS
3
- } from "./chunk-PUT2N5VA.js";
3
+ } from "./chunk-YTW7BZUC.js";
4
4
  import {
5
5
  denyReasonFrom,
6
6
  isDeferAnswer
@@ -23,23 +23,47 @@ import {
23
23
  sendNotification,
24
24
  throttlePass,
25
25
  waitForAnswer
26
- } from "./chunk-SCXPHMAS.js";
26
+ } from "./chunk-5U3YC3BZ.js";
27
27
  import {
28
28
  getMachineId
29
29
  } from "./chunk-RN3NOEJF.js";
30
30
  import {
31
31
  isGatingMoment,
32
32
  recordKeylessMoment
33
- } from "./chunk-V42K3XQI.js";
33
+ } from "./chunk-CANZPNGD.js";
34
34
  import {
35
+ buildDecisionEpisodeFeatures,
35
36
  effectiveWaitSeconds,
36
37
  hookWaitClamped,
37
38
  hookWaitDeadline
38
- } from "./chunk-WI3F6XNZ.js";
39
+ } from "./chunk-F4DGHFIW.js";
39
40
  import {
40
- getApiKey
41
+ getApiKey,
42
+ getBaseUrl
41
43
  } from "./chunk-NKXSILEW.js";
42
44
 
45
+ // src/decision-episode.ts
46
+ var EPISODE_PATH = "/api/agent/decision-episode";
47
+ var EMIT_TIMEOUT_MS = 3e3;
48
+ var buildEpisodeBody = (input) => ({
49
+ sessionId: input.sessionId,
50
+ machineId: input.machineId,
51
+ agentType: input.agentType,
52
+ features: buildDecisionEpisodeFeatures(input)
53
+ });
54
+ var reportDecisionEpisode = async (apiKey, input) => {
55
+ try {
56
+ const baseUrl = getBaseUrl();
57
+ await fetch(`${baseUrl}${EPISODE_PATH}`, {
58
+ method: "POST",
59
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
60
+ body: JSON.stringify(buildEpisodeBody(input)),
61
+ signal: AbortSignal.timeout(EMIT_TIMEOUT_MS)
62
+ });
63
+ } catch {
64
+ }
65
+ };
66
+
43
67
  // src/hook.ts
44
68
  import { basename, join } from "path";
45
69
  import { tmpdir } from "os";
@@ -90,7 +114,7 @@ var pollForAnswer = async (apiKey, correlationId, deadlineMs, pollInterval = 2e3
90
114
  }
91
115
  return { answered: false };
92
116
  };
93
- var handlePushOnly = async (apiKey, description, projectName, timeoutSeconds, timeoutAction, sessionId, machineId, toolName, toolTarget, decision) => {
117
+ var handlePushOnly = async (apiKey, description, projectName, timeoutSeconds, timeoutAction, sessionId, machineId, toolName, toolTarget, decision, signals) => {
94
118
  let result;
95
119
  try {
96
120
  result = await askUser(apiKey, {
@@ -136,6 +160,7 @@ var handlePushOnly = async (apiKey, description, projectName, timeoutSeconds, ti
136
160
  const answer = await pollForAnswer(apiKey, result.correlationId, deadline);
137
161
  if (answer.answered) {
138
162
  if (isDeferAnswer(answer.value)) return ask("Handling on your machine");
163
+ signals.humanDecision = true;
139
164
  return answer.value === "yes" ? allow() : deny(denyReasonFrom(answer.value));
140
165
  }
141
166
  if (timeoutAction === "wait" || clampCut) {
@@ -154,7 +179,7 @@ var handlePushOnly = async (apiKey, description, projectName, timeoutSeconds, ti
154
179
  var handleTerminalOnly = () => {
155
180
  return ask();
156
181
  };
157
- var handlePushFirst = async (apiKey, description, projectName, pushFirstSeconds, sessionId, machineId, toolName, toolTarget, decision) => {
182
+ var handlePushFirst = async (apiKey, description, projectName, pushFirstSeconds, sessionId, machineId, toolName, toolTarget, decision, signals) => {
158
183
  let result;
159
184
  try {
160
185
  result = await askUser(apiKey, {
@@ -183,6 +208,7 @@ var handlePushFirst = async (apiKey, description, projectName, pushFirstSeconds,
183
208
  const answer = await pollForAnswer(apiKey, result.correlationId, deadline, 1500);
184
209
  if (answer.answered) {
185
210
  if (isDeferAnswer(answer.value)) return ask("Handling on your machine");
211
+ signals.humanDecision = true;
186
212
  return answer.value === "yes" ? allow() : deny(denyReasonFrom(answer.value));
187
213
  }
188
214
  savePendingQuestion(sessionId || DEFAULT_SESSION, result.correlationId);
@@ -201,7 +227,36 @@ var handleNotifyOnly = async (apiKey, description, projectName, sessionId, machi
201
227
  }
202
228
  return ask();
203
229
  };
204
- var dispatchModeHandler = async (apiKey, input, toolPolicy, machineId) => {
230
+ var CLAUDE_AGENT_TYPE = "claude_code";
231
+ var emitDecisionEpisode = (apiKey, input, toolPolicy, machineId, toolTarget, decision, result) => {
232
+ const verdict = result?.hookSpecificOutput?.permissionDecision;
233
+ const outcome = verdict === "allow" ? "approve" : verdict === "deny" ? "deny" : null;
234
+ if (!outcome) return;
235
+ const ti = input.tool_input ?? {};
236
+ const commandOrPath = typeof ti.command === "string" ? ti.command : typeof ti.file_path === "string" ? ti.file_path : typeof ti.path === "string" ? ti.path : void 0;
237
+ void reportDecisionEpisode(apiKey, {
238
+ toolName: input.tool_name,
239
+ commandOrPath,
240
+ intent: decision.intent,
241
+ toolTarget,
242
+ agentType: CLAUDE_AGENT_TYPE,
243
+ matchedPattern: toolPolicy.tool,
244
+ policyMode: toolPolicy.mode,
245
+ timeoutSeconds: toolPolicy.timeoutSeconds,
246
+ timeoutAction: toolPolicy.timeoutAction,
247
+ // Reaching a handler means a human gate was requested (auto-allow/deny
248
+ // returned earlier in the caller before dispatch).
249
+ requestedGate: "ask",
250
+ decision: outcome,
251
+ // Truthfully human: the caller only emits when signals.humanDecision is set
252
+ // (a real phone answer). Bronze label until reason + execution-outcome land.
253
+ origin: "human",
254
+ latencyMs: Date.now() - START_MS,
255
+ sessionId: input.session_id,
256
+ machineId
257
+ });
258
+ };
259
+ var dispatchModeHandler = async (apiKey, input, toolPolicy, machineId, trainingConsent = false) => {
205
260
  const description = describeToolCall(input.tool_name, input.tool_input, "hook");
206
261
  const projectName = basename(input.cwd ?? process.cwd());
207
262
  const sessionId = input.session_id;
@@ -213,18 +268,26 @@ var dispatchModeHandler = async (apiKey, input, toolPolicy, machineId) => {
213
268
  blocker: deriveBlocker(toolPolicy.mode),
214
269
  actionBody: deriveActionBody(input.tool_name, input.tool_input)
215
270
  };
216
- switch (toolPolicy.mode) {
217
- case "push_only":
218
- return handlePushOnly(apiKey, description, projectName, toolPolicy.timeoutSeconds, toolPolicy.timeoutAction, sessionId, machineId, input.tool_name, toolTarget, decision);
219
- case "terminal_only":
220
- return handleTerminalOnly();
221
- case "push_first":
222
- return handlePushFirst(apiKey, description, projectName, toolPolicy.pushFirstSeconds, sessionId, machineId, input.tool_name, toolTarget, decision);
223
- case "notify_only":
224
- return handleNotifyOnly(apiKey, description, projectName, sessionId, machineId);
225
- default:
226
- return handlePushFirst(apiKey, description, projectName, toolPolicy.pushFirstSeconds, sessionId, machineId, input.tool_name, toolTarget, decision);
271
+ const signals = {};
272
+ const runHandler = () => {
273
+ switch (toolPolicy.mode) {
274
+ case "push_only":
275
+ return handlePushOnly(apiKey, description, projectName, toolPolicy.timeoutSeconds, toolPolicy.timeoutAction, sessionId, machineId, input.tool_name, toolTarget, decision, signals);
276
+ case "terminal_only":
277
+ return Promise.resolve(handleTerminalOnly());
278
+ case "push_first":
279
+ return handlePushFirst(apiKey, description, projectName, toolPolicy.pushFirstSeconds, sessionId, machineId, input.tool_name, toolTarget, decision, signals);
280
+ case "notify_only":
281
+ return handleNotifyOnly(apiKey, description, projectName, sessionId, machineId);
282
+ default:
283
+ return handlePushFirst(apiKey, description, projectName, toolPolicy.pushFirstSeconds, sessionId, machineId, input.tool_name, toolTarget, decision, signals);
284
+ }
285
+ };
286
+ const result = await runHandler();
287
+ if (trainingConsent && signals.humanDecision) {
288
+ emitDecisionEpisode(apiKey, input, toolPolicy, machineId, toolTarget, decision, result);
227
289
  }
290
+ return result;
228
291
  };
229
292
  var KEYLESS_NOTICE = "[pushary] Not connected (no API key), so your agent runs untouched. Approval moments are counted locally; see them with: npx @pushary/agent-hooks@latest stats. Connect: https://pushary.com/sign-up?utm_source=cli&utm_medium=keyless-hook";
230
293
  var keylessNoticeOnce = (sessionId) => {
@@ -339,7 +402,7 @@ var handlePreToolUse = async (input) => {
339
402
  if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "deny") {
340
403
  return deny(`Denied by policy for ${toolPolicy.tool}`);
341
404
  }
342
- return dispatchModeHandler(apiKey, input, toolPolicy, getMachineId());
405
+ return dispatchModeHandler(apiKey, input, toolPolicy, getMachineId(), modeState.trainingConsent);
343
406
  } catch {
344
407
  return void 0;
345
408
  }
@@ -364,7 +427,10 @@ var toPermissionRequestOutput = (hookOutput) => {
364
427
  return void 0;
365
428
  };
366
429
  var handlePermissionRequest = async (input) => {
367
- if (PRETOOLUSE_HANDLED_TOOLS.has(input.tool_name)) return void 0;
430
+ if (PRETOOLUSE_HANDLED_TOOLS.has(input.tool_name)) {
431
+ const reclaimDeferredExec = input.tool_name !== "AskUserQuestion" && shouldDeferToNativeMode(input.permission_mode, input.tool_name);
432
+ if (!reclaimDeferredExec) return void 0;
433
+ }
368
434
  if (input.tool_name.startsWith("mcp__pushary__")) return void 0;
369
435
  let apiKey;
370
436
  try {
@@ -381,7 +447,7 @@ var handlePermissionRequest = async (input) => {
381
447
  if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "deny") {
382
448
  return permReqDeny(`Denied by policy for ${toolPolicy.tool}`);
383
449
  }
384
- return toPermissionRequestOutput(await dispatchModeHandler(apiKey, input, toolPolicy, getMachineId()));
450
+ return toPermissionRequestOutput(await dispatchModeHandler(apiKey, input, toolPolicy, getMachineId(), modeState.trainingConsent));
385
451
  } catch {
386
452
  return void 0;
387
453
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  HOOK_BUDGETS
3
- } from "./chunk-WI3F6XNZ.js";
3
+ } from "./chunk-F4DGHFIW.js";
4
4
 
5
5
  // src/claude-config.ts
6
6
  import { join } from "path";
@@ -34,6 +34,7 @@ interface ModeState {
34
34
  readonly kill: boolean;
35
35
  readonly policyVersion: string | null;
36
36
  readonly relayUrl: string | null;
37
+ readonly trainingConsent?: boolean;
37
38
  }
38
39
  declare const fetchModeState: (apiKey: string, sessionId?: string) => Promise<ModeState>;
39
40
  declare const fetchModeOverride: (apiKey: string) => Promise<ApprovalMode | null>;
package/dist/src/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  handlePreToolUse
3
- } from "../chunk-M24KUFLW.js";
4
- import "../chunk-PUT2N5VA.js";
3
+ } from "../chunk-JKSSDN2O.js";
4
+ import "../chunk-YTW7BZUC.js";
5
5
  import "../chunk-KQYIHZ5E.js";
6
6
  import {
7
7
  askUser,
@@ -15,11 +15,11 @@ import {
15
15
  reportEvent,
16
16
  resolvePolicy,
17
17
  waitForAnswer
18
- } from "../chunk-SCXPHMAS.js";
18
+ } from "../chunk-5U3YC3BZ.js";
19
19
  import "../chunk-RN3NOEJF.js";
20
- import "../chunk-V42K3XQI.js";
20
+ import "../chunk-CANZPNGD.js";
21
21
  import "../chunk-DWED7BS3.js";
22
- import "../chunk-WI3F6XNZ.js";
22
+ import "../chunk-F4DGHFIW.js";
23
23
  import {
24
24
  getApiKey,
25
25
  getBaseUrl