claude-ide-bridge 2.25.10 → 2.25.14

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.
@@ -18,6 +18,10 @@ export interface PromptSource {
18
18
  promptName?: string;
19
19
  promptArgs?: Record<string, string>;
20
20
  condition?: string;
21
+ /** Per-hook model override. Falls back to policy.defaultModel (Haiku). */
22
+ model?: string;
23
+ /** Per-hook effort override. Falls back to policy.defaultEffort ("low"). */
24
+ effort?: "low" | "medium" | "high" | "max";
21
25
  }
22
26
  export interface OnDiagnosticsErrorPolicy extends PromptSource {
23
27
  enabled: boolean;
@@ -238,6 +242,30 @@ export interface TaskSuccessResult {
238
242
  output: string;
239
243
  }
240
244
  export interface AutomationPolicy {
245
+ /**
246
+ * Default model for all automation tasks.
247
+ * Defaults to "claude-haiku-4-5-20251001" to minimise cost.
248
+ * Override with "claude-sonnet-4-6" etc. for hooks that need more reasoning.
249
+ */
250
+ defaultModel?: string;
251
+ /**
252
+ * Hard cap on automation tasks spawned per hour (rolling 60-min window).
253
+ * Defaults to 20. Set to 0 to disable.
254
+ */
255
+ maxTasksPerHour?: number;
256
+ /**
257
+ * Custom system prompt passed via --system-prompt to every automation subprocess.
258
+ * Replaces the default Claude Code system prompt, preventing CLAUDE.md from being
259
+ * loaded as workspace instructions. Keep it short — this is the biggest token lever.
260
+ * Default: a lean "be brief" prompt. Max 4096 chars.
261
+ */
262
+ automationSystemPrompt?: string;
263
+ /**
264
+ * Default effort level for all automation tasks (low/medium/high/max).
265
+ * Defaults to "low" — automation tasks rarely need deep reasoning.
266
+ * Override per-hook via the hook's own effort field.
267
+ */
268
+ defaultEffort?: "low" | "medium" | "high" | "max";
241
269
  onDiagnosticsError?: OnDiagnosticsErrorPolicy;
242
270
  onFileSave?: OnFileSavePolicy;
243
271
  /** Fired by Claude Code 2.1.83+ FileChanged hook — reacts to any file edit, not just explicit saves. */
@@ -330,7 +358,19 @@ export declare class AutomationHooks {
330
358
  private prevDiagnosticErrors;
331
359
  /** Active task ID for the task-success handler (workspace-global). */
332
360
  private activeTaskSuccessTaskId;
361
+ /**
362
+ * Rolling window of task enqueue timestamps for maxTasksPerHour enforcement.
363
+ * Entries older than 60 minutes are pruned on each enqueue.
364
+ */
365
+ private taskTimestamps;
333
366
  constructor(policy: AutomationPolicy, orchestrator: ClaudeOrchestrator, log: (msg: string) => void, extensionClient?: ExtensionClient | undefined, workspace?: string | undefined);
367
+ /**
368
+ * Central enqueue for all automation-triggered tasks.
369
+ * Applies defaultModel (Haiku by default) and enforces maxTasksPerHour.
370
+ * Throws with the same "Task queue is full" message on rate-limit breach so
371
+ * callers can handle it identically.
372
+ */
373
+ private _enqueueAutomationTask;
334
374
  /**
335
375
  * Resolve a named prompt, substituting any `{{placeholder}}` tokens in
336
376
  * `promptArgs` values with sanitized event data before calling `getPrompt()`.
@@ -472,5 +512,10 @@ export declare class AutomationHooks {
472
512
  cooldownMs: number;
473
513
  } | null;
474
514
  unwiredEnabledHooks: string[];
515
+ defaultModel: string;
516
+ maxTasksPerHour: number;
517
+ tasksThisHour: number;
518
+ defaultEffort: string;
519
+ automationSystemPrompt: string;
475
520
  };
476
521
  }
@@ -50,6 +50,10 @@ function truncatePrompt(prompt) {
50
50
  }
51
51
  /** Prune lastTrigger entries older than this to prevent unbounded Map growth */
52
52
  const LAST_TRIGGER_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 1 week
53
+ /** Default system prompt for automation subprocesses when none is set in policy. */
54
+ const DEFAULT_AUTOMATION_SYSTEM_PROMPT = "You are a concise automation assistant. " +
55
+ "Respond in \u22645 lines. No preamble. No markdown headers. " +
56
+ "Call the tools listed in the task prompt, then report results only.";
53
57
  /**
54
58
  * Deterministic signature over a diagnostic list — used for content-aware
55
59
  * dedupe in onDiagnosticsError. Sorting by a stable key makes the signature
@@ -104,6 +108,16 @@ function validatePromptSource(hookName, cfg) {
104
108
  throw new Error(`"${hookName}.condition" must be a string ≤ 1024 characters`);
105
109
  }
106
110
  }
111
+ if (cfg.model !== undefined) {
112
+ if (typeof cfg.model !== "string" || cfg.model.trim() === "") {
113
+ throw new Error(`"${hookName}.model" must be a non-empty string`);
114
+ }
115
+ }
116
+ if (cfg.effort !== undefined) {
117
+ if (!["low", "medium", "high", "max"].includes(cfg.effort)) {
118
+ throw new Error(`"${hookName}.effort" must be one of "low", "medium", "high", "max"`);
119
+ }
120
+ }
107
121
  }
108
122
  /** Load and validate a JSON automation policy file. Throws on any failure. */
109
123
  export function loadPolicy(filePath) {
@@ -125,6 +139,31 @@ export function loadPolicy(filePath) {
125
139
  throw new Error(`Automation policy must be a JSON object in "${filePath}"`);
126
140
  }
127
141
  const policy = parsed;
142
+ // Validate top-level fields
143
+ if (policy.defaultModel !== undefined &&
144
+ typeof policy.defaultModel !== "string") {
145
+ throw new Error(`"defaultModel" must be a string`);
146
+ }
147
+ if (policy.maxTasksPerHour !== undefined) {
148
+ if (typeof policy.maxTasksPerHour !== "number" ||
149
+ !Number.isInteger(policy.maxTasksPerHour) ||
150
+ policy.maxTasksPerHour < 0) {
151
+ throw new Error(`"maxTasksPerHour" must be a non-negative integer`);
152
+ }
153
+ }
154
+ if (policy.automationSystemPrompt !== undefined) {
155
+ if (typeof policy.automationSystemPrompt !== "string") {
156
+ throw new Error(`"automationSystemPrompt" must be a string`);
157
+ }
158
+ if (policy.automationSystemPrompt.length > 4096) {
159
+ throw new Error(`"automationSystemPrompt" must be ≤ 4096 characters`);
160
+ }
161
+ }
162
+ if (policy.defaultEffort !== undefined) {
163
+ if (!["low", "medium", "high", "max"].includes(policy.defaultEffort)) {
164
+ throw new Error(`"defaultEffort" must be one of "low", "medium", "high", "max"`);
165
+ }
166
+ }
128
167
  // Validate onDiagnosticsError
129
168
  if (policy.onDiagnosticsError !== undefined) {
130
169
  const d = policy.onDiagnosticsError;
@@ -538,6 +577,11 @@ export class AutomationHooks {
538
577
  prevDiagnosticErrors = new Map();
539
578
  /** Active task ID for the task-success handler (workspace-global). */
540
579
  activeTaskSuccessTaskId = null;
580
+ /**
581
+ * Rolling window of task enqueue timestamps for maxTasksPerHour enforcement.
582
+ * Entries older than 60 minutes are pruned on each enqueue.
583
+ */
584
+ taskTimestamps = [];
541
585
  constructor(policy, orchestrator, log, extensionClient, workspace) {
542
586
  this.policy = policy;
543
587
  this.orchestrator = orchestrator;
@@ -545,6 +589,43 @@ export class AutomationHooks {
545
589
  this.extensionClient = extensionClient;
546
590
  this.workspace = workspace;
547
591
  }
592
+ /**
593
+ * Central enqueue for all automation-triggered tasks.
594
+ * Applies defaultModel (Haiku by default) and enforces maxTasksPerHour.
595
+ * Throws with the same "Task queue is full" message on rate-limit breach so
596
+ * callers can handle it identically.
597
+ */
598
+ _enqueueAutomationTask(opts) {
599
+ const maxPerHour = this.policy.maxTasksPerHour ?? 20;
600
+ if (maxPerHour > 0) {
601
+ const now = Date.now();
602
+ const cutoff = now - 60 * 60 * 1_000;
603
+ // Prune old timestamps
604
+ let i = 0;
605
+ while (i < this.taskTimestamps.length && this.taskTimestamps[i] < cutoff)
606
+ i++;
607
+ if (i > 0)
608
+ this.taskTimestamps.splice(0, i);
609
+ if (this.taskTimestamps.length >= maxPerHour) {
610
+ throw new Error(`Automation rate limit reached (max ${maxPerHour} tasks/hour)`);
611
+ }
612
+ this.taskTimestamps.push(now);
613
+ }
614
+ const model = opts.hookCfg?.model ??
615
+ this.policy.defaultModel ??
616
+ "claude-haiku-4-5-20251001";
617
+ const effort = opts.hookCfg?.effort ?? this.policy.defaultEffort ?? "low";
618
+ const systemPrompt = this.policy.automationSystemPrompt ?? DEFAULT_AUTOMATION_SYSTEM_PROMPT;
619
+ return this.orchestrator.enqueue({
620
+ prompt: opts.prompt,
621
+ sessionId: "",
622
+ isAutomationTask: true,
623
+ triggerSource: opts.triggerSource,
624
+ model,
625
+ effort,
626
+ systemPrompt,
627
+ });
628
+ }
548
629
  /**
549
630
  * Resolve a named prompt, substituting any `{{placeholder}}` tokens in
550
631
  * `promptArgs` values with sanitized event data before calling `getPrompt()`.
@@ -694,10 +775,10 @@ export class AutomationHooks {
694
775
  }
695
776
  prompt = truncatePrompt(buildHookMetadata("onDiagnosticsError", normalizedFile) + prompt);
696
777
  try {
697
- const taskId = this.orchestrator.enqueue({
778
+ const taskId = this._enqueueAutomationTask({
698
779
  prompt,
699
- sessionId: "",
700
- isAutomationTask: true,
780
+ triggerSource: "onDiagnosticsError",
781
+ hookCfg: cfg,
701
782
  });
702
783
  this.lastTrigger.set(key, now);
703
784
  this.activeDiagnosticsTasks.set(normalizedFile, taskId);
@@ -752,10 +833,10 @@ export class AutomationHooks {
752
833
  }
753
834
  prompt = truncatePrompt(buildHookMetadata("onCwdChanged") + prompt);
754
835
  try {
755
- const taskId = this.orchestrator.enqueue({
836
+ const taskId = this._enqueueAutomationTask({
756
837
  prompt,
757
- sessionId: "",
758
- isAutomationTask: true,
838
+ triggerSource: "onCwdChanged",
839
+ hookCfg: cfg,
759
840
  });
760
841
  this.lastTrigger.set(key, now);
761
842
  this.log(`[automation] triggered cwd-changed task ${taskId.slice(0, 8)} for ${newCwd}`);
@@ -792,10 +873,10 @@ export class AutomationHooks {
792
873
  }
793
874
  postCompactPrompt = truncatePrompt(buildHookMetadata("onPostCompact") + postCompactPrompt);
794
875
  try {
795
- const taskId = this.orchestrator.enqueue({
876
+ const taskId = this._enqueueAutomationTask({
796
877
  prompt: postCompactPrompt,
797
- sessionId: "",
798
- isAutomationTask: true,
878
+ triggerSource: "onPostCompact",
879
+ hookCfg: cfg,
799
880
  });
800
881
  // Set lastTrigger AFTER successful enqueue so a failed enqueue does not
801
882
  // impose a spurious cooldown on the next trigger attempt.
@@ -835,10 +916,10 @@ export class AutomationHooks {
835
916
  }
836
917
  instrPrompt = truncatePrompt(buildHookMetadata("onInstructionsLoaded") + instrPrompt);
837
918
  try {
838
- const taskId = this.orchestrator.enqueue({
919
+ const taskId = this._enqueueAutomationTask({
839
920
  prompt: instrPrompt,
840
- sessionId: "",
841
- isAutomationTask: true,
921
+ triggerSource: "onInstructionsLoaded",
922
+ hookCfg: cfg,
842
923
  });
843
924
  this.lastTrigger.set(key, now);
844
925
  this.log(`[automation] triggered InstructionsLoaded task ${taskId.slice(0, 8)}`);
@@ -904,10 +985,10 @@ export class AutomationHooks {
904
985
  }
905
986
  prompt = truncatePrompt(buildHookMetadata("onFileSave", normalizedFile) + prompt);
906
987
  try {
907
- const taskId = this.orchestrator.enqueue({
988
+ const taskId = this._enqueueAutomationTask({
908
989
  prompt,
909
- sessionId: "",
910
- isAutomationTask: true,
990
+ triggerSource: "onFileSave",
991
+ hookCfg: cfg,
911
992
  });
912
993
  this.lastTrigger.set(key, now);
913
994
  this.activeSaveTasks.set(normalizedFile, taskId);
@@ -977,10 +1058,10 @@ export class AutomationHooks {
977
1058
  }
978
1059
  prompt = truncatePrompt(buildHookMetadata("onFileChanged", normalizedFile) + prompt);
979
1060
  try {
980
- const taskId = this.orchestrator.enqueue({
1061
+ const taskId = this._enqueueAutomationTask({
981
1062
  prompt,
982
- sessionId: "",
983
- isAutomationTask: true,
1063
+ triggerSource: "onFileChanged",
1064
+ hookCfg: cfg,
984
1065
  });
985
1066
  this.lastTrigger.set(key, now);
986
1067
  this.activeFileChangedTasks.set(normalizedFile, taskId);
@@ -1069,10 +1150,10 @@ export class AutomationHooks {
1069
1150
  }
1070
1151
  prompt = truncatePrompt(buildHookMetadata("onTestRun") + prompt);
1071
1152
  try {
1072
- const taskId = this.orchestrator.enqueue({
1153
+ const taskId = this._enqueueAutomationTask({
1073
1154
  prompt,
1074
- sessionId: "",
1075
- isAutomationTask: true,
1155
+ triggerSource: "onTestRun",
1156
+ hookCfg: cfg,
1076
1157
  });
1077
1158
  this.lastTrigger.set(key, now);
1078
1159
  this.activeTestRunTaskId = taskId;
@@ -1168,10 +1249,10 @@ export class AutomationHooks {
1168
1249
  }
1169
1250
  prompt = truncatePrompt(buildHookMetadata("onGitCommit") + prompt);
1170
1251
  try {
1171
- const taskId = this.orchestrator.enqueue({
1252
+ const taskId = this._enqueueAutomationTask({
1172
1253
  prompt,
1173
- sessionId: "",
1174
- isAutomationTask: true,
1254
+ triggerSource: "onGitCommit",
1255
+ hookCfg: cfg,
1175
1256
  });
1176
1257
  this.lastTrigger.set(key, now);
1177
1258
  this.activeGitCommitTaskId = taskId;
@@ -1230,10 +1311,10 @@ export class AutomationHooks {
1230
1311
  }
1231
1312
  prompt = truncatePrompt(buildHookMetadata("onGitPush") + prompt);
1232
1313
  try {
1233
- const taskId = this.orchestrator.enqueue({
1314
+ const taskId = this._enqueueAutomationTask({
1234
1315
  prompt,
1235
- sessionId: "",
1236
- isAutomationTask: true,
1316
+ triggerSource: "onGitPush",
1317
+ hookCfg: cfg,
1237
1318
  });
1238
1319
  this.lastTrigger.set(key, now);
1239
1320
  this.activeGitPushTaskId = taskId;
@@ -1289,10 +1370,10 @@ export class AutomationHooks {
1289
1370
  }
1290
1371
  prompt = truncatePrompt(buildHookMetadata("onGitPull") + prompt);
1291
1372
  try {
1292
- const taskId = this.orchestrator.enqueue({
1373
+ const taskId = this._enqueueAutomationTask({
1293
1374
  prompt,
1294
- sessionId: "",
1295
- isAutomationTask: true,
1375
+ triggerSource: "onGitPull",
1376
+ hookCfg: cfg,
1296
1377
  });
1297
1378
  this.lastTrigger.set(key, now);
1298
1379
  this.activeGitPullTaskId = taskId;
@@ -1354,10 +1435,10 @@ export class AutomationHooks {
1354
1435
  }
1355
1436
  prompt = truncatePrompt(buildHookMetadata("onBranchCheckout") + prompt);
1356
1437
  try {
1357
- const taskId = this.orchestrator.enqueue({
1438
+ const taskId = this._enqueueAutomationTask({
1358
1439
  prompt,
1359
- sessionId: "",
1360
- isAutomationTask: true,
1440
+ triggerSource: "onBranchCheckout",
1441
+ hookCfg: cfg,
1361
1442
  });
1362
1443
  this.lastTrigger.set(key, now);
1363
1444
  this.activeBranchCheckoutTaskId = taskId;
@@ -1421,10 +1502,10 @@ export class AutomationHooks {
1421
1502
  }
1422
1503
  prompt = truncatePrompt(buildHookMetadata("onPullRequest") + prompt);
1423
1504
  try {
1424
- const taskId = this.orchestrator.enqueue({
1505
+ const taskId = this._enqueueAutomationTask({
1425
1506
  prompt,
1426
- sessionId: "",
1427
- isAutomationTask: true,
1507
+ triggerSource: "onPullRequest",
1508
+ hookCfg: cfg,
1428
1509
  });
1429
1510
  this.lastTrigger.set(key, now);
1430
1511
  this.activePullRequestTaskId = taskId;
@@ -1474,7 +1555,11 @@ export class AutomationHooks {
1474
1555
  }
1475
1556
  prompt = truncatePrompt(buildHookMetadata("onTaskCreated") + prompt);
1476
1557
  try {
1477
- const taskId = this.orchestrator.enqueue({ prompt, sessionId: "" });
1558
+ const taskId = this._enqueueAutomationTask({
1559
+ prompt,
1560
+ triggerSource: "onTaskCreated",
1561
+ hookCfg: cfg,
1562
+ });
1478
1563
  this.lastTrigger.set(key, now);
1479
1564
  this.activeTaskCreatedTaskId = taskId;
1480
1565
  this.log(`[automation] triggered task-created task ${taskId.slice(0, 8)} (spawned task: ${result.taskId.slice(0, 8)})`);
@@ -1524,7 +1609,11 @@ export class AutomationHooks {
1524
1609
  }
1525
1610
  prompt = truncatePrompt(buildHookMetadata("onPermissionDenied") + prompt);
1526
1611
  try {
1527
- const taskId = this.orchestrator.enqueue({ prompt, sessionId: "" });
1612
+ const taskId = this._enqueueAutomationTask({
1613
+ prompt,
1614
+ triggerSource: "onPermissionDenied",
1615
+ hookCfg: cfg,
1616
+ });
1528
1617
  this.lastTrigger.set(key, now);
1529
1618
  this.activePermissionDeniedTaskId = taskId;
1530
1619
  this.log(`[automation] triggered permission-denied task ${taskId.slice(0, 8)} (tool: ${result.tool})`);
@@ -1577,7 +1666,11 @@ export class AutomationHooks {
1577
1666
  }
1578
1667
  prompt = truncatePrompt(buildHookMetadata("onDiagnosticsCleared", normalizedFile) + prompt);
1579
1668
  try {
1580
- const taskId = this.orchestrator.enqueue({ prompt, sessionId: "" });
1669
+ const taskId = this._enqueueAutomationTask({
1670
+ prompt,
1671
+ triggerSource: "onDiagnosticsCleared",
1672
+ hookCfg: cfg,
1673
+ });
1581
1674
  this.lastTrigger.set(key, now);
1582
1675
  this.activeDiagnosticsClearedTasks.set(normalizedFile, taskId);
1583
1676
  this.log(`[automation] triggered diagnostics-cleared task ${taskId.slice(0, 8)} for ${normalizedFile}`);
@@ -1631,10 +1724,10 @@ export class AutomationHooks {
1631
1724
  }
1632
1725
  prompt = truncatePrompt(buildHookMetadata("onTaskSuccess") + prompt);
1633
1726
  try {
1634
- const taskId = this.orchestrator.enqueue({
1727
+ const taskId = this._enqueueAutomationTask({
1635
1728
  prompt,
1636
- sessionId: "",
1637
- isAutomationTask: true,
1729
+ triggerSource: "onTaskSuccess",
1730
+ hookCfg: cfg,
1638
1731
  });
1639
1732
  this.lastTrigger.set(key, now);
1640
1733
  this.activeTaskSuccessTaskId = taskId;
@@ -1750,6 +1843,11 @@ export class AutomationHooks {
1750
1843
  }
1751
1844
  : null,
1752
1845
  unwiredEnabledHooks,
1846
+ defaultModel: p.defaultModel ?? "claude-haiku-4-5-20251001",
1847
+ maxTasksPerHour: p.maxTasksPerHour ?? 20,
1848
+ tasksThisHour: this.taskTimestamps.filter((t) => t >= Date.now() - 60 * 60 * 1_000).length,
1849
+ defaultEffort: p.defaultEffort ?? "low",
1850
+ automationSystemPrompt: (p.automationSystemPrompt ?? DEFAULT_AUTOMATION_SYSTEM_PROMPT).slice(0, 80),
1753
1851
  };
1754
1852
  }
1755
1853
  }