@davidorex/pi-behavior-monitors 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,42 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## v0.1.4
6
+
7
+ [compare changes](https://github.com/davidorex/pi-behavior-monitors/compare/v0.1.3...v0.1.4)
8
+
9
+ ### 🚀 Enhancements
10
+
11
+ - Add ui.select menus to /monitors command for TUI discoverability ([4391ca3](https://github.com/davidorex/pi-behavior-monitors/commit/4391ca3))
12
+
13
+ ### 📖 Documentation
14
+
15
+ - Update SKILL.md with buffered steer delivery and TUI autocomplete ([94aee6e](https://github.com/davidorex/pi-behavior-monitors/commit/94aee6e))
16
+
17
+ ### ❤️ Contributors
18
+
19
+ - David Ryan <davidryan@gmail.com>
20
+
21
+ ## v0.1.3
22
+
23
+ [compare changes](https://github.com/davidorex/pi-behavior-monitors/compare/v0.1.2...v0.1.3)
24
+
25
+ ### 🩹 Fixes
26
+
27
+ - Buffer steer delivery at agent_end to work around pi async event queue ([c899fa5](https://github.com/davidorex/pi-behavior-monitors/commit/c899fa5))
28
+
29
+ ### 📖 Documentation
30
+
31
+ - Add npm publish commands to CLAUDE.md ([91dbe87](https://github.com/davidorex/pi-behavior-monitors/commit/91dbe87))
32
+
33
+ ### 🏡 Chore
34
+
35
+ - Scope package name to @davidorex/pi-behavior-monitors ([e9a9882](https://github.com/davidorex/pi-behavior-monitors/commit/e9a9882))
36
+
37
+ ### ❤️ Contributors
38
+
39
+ - David Ryan <davidryan@gmail.com>
40
+
5
41
  ## v0.1.2
6
42
 
7
43
  [compare changes](https://github.com/davidorex/pi-behavior-monitors/compare/v0.1.1...v0.1.2)
package/index.ts CHANGED
@@ -121,6 +121,12 @@ export interface MonitorMessageDetails {
121
121
  ceiling: number;
122
122
  }
123
123
 
124
+ interface BufferedSteer {
125
+ monitor: Monitor;
126
+ details: MonitorMessageDetails;
127
+ content: string;
128
+ }
129
+
124
130
  type MonitorEvent = "message_end" | "turn_end" | "agent_end" | "command";
125
131
 
126
132
  const VALID_EVENTS = new Set<string>(["message_end", "turn_end", "agent_end", "command"]);
@@ -896,15 +902,21 @@ async function activate(
896
902
  whileCount: monitor.whileCount + 1,
897
903
  ceiling: monitor.ceiling,
898
904
  };
899
- pi.sendMessage<MonitorMessageDetails>(
900
- {
901
- customType: "monitor-steer",
902
- content: `[${monitor.name}] ${description}${annotation}. ${action.steer}`,
903
- display: true,
904
- details,
905
- },
906
- { deliverAs: "steer", triggerTurn: true },
907
- );
905
+ const content = `[${monitor.name}] ${description}${annotation}. ${action.steer}`;
906
+
907
+ if (monitor.event === "agent_end" || monitor.event === "command") {
908
+ // Already post-loop or command context: deliver immediately
909
+ pi.sendMessage<MonitorMessageDetails>(
910
+ { customType: "monitor-steer", content, display: true, details },
911
+ { deliverAs: "steer", triggerTurn: true },
912
+ );
913
+ } else {
914
+ // message_end / turn_end: buffer for drain at agent_end
915
+ // (pi's async event queue means these handlers run after the agent loop
916
+ // has already checked getSteeringMessages — direct sendMessage misses
917
+ // the window and the steer arrives one response late)
918
+ pendingAgentEndSteers.push({ monitor, details, content });
919
+ }
908
920
  }
909
921
 
910
922
  monitor.whileCount++;
@@ -1002,6 +1014,7 @@ export default function (pi: ExtensionAPI) {
1002
1014
  m.activationCount = 0;
1003
1015
  }
1004
1016
  monitorsEnabled = true;
1017
+ pendingAgentEndSteers = [];
1005
1018
  updateStatus();
1006
1019
  });
1007
1020
 
@@ -1036,11 +1049,31 @@ export default function (pi: ExtensionAPI) {
1036
1049
  return box;
1037
1050
  });
1038
1051
 
1039
- // --- abort support ---
1052
+ // --- abort support + buffered steer drain ---
1040
1053
  pi.on("agent_end", async () => {
1041
1054
  pi.events.emit("monitors:abort", undefined);
1055
+
1056
+ // Drain buffered steers from message_end/turn_end monitors.
1057
+ // The _agentEventQueue guarantees this runs AFTER all turn_end/message_end
1058
+ // handlers complete (sequential promise chain), so the buffer is populated.
1059
+ // Deliver only the first — the corrected response will re-trigger monitors
1060
+ // if additional issues remain.
1061
+ if (pendingAgentEndSteers.length > 0) {
1062
+ const first = pendingAgentEndSteers[0];
1063
+ pendingAgentEndSteers = [];
1064
+ pi.sendMessage<MonitorMessageDetails>(
1065
+ { customType: "monitor-steer", content: first.content, display: true, details: first.details },
1066
+ { deliverAs: "steer", triggerTurn: true },
1067
+ );
1068
+ }
1042
1069
  });
1043
1070
 
1071
+ // --- buffered steers for message_end/turn_end monitors ---
1072
+ // These monitors classify during the agent loop but can't inject steers in time
1073
+ // (pi's async event queue means extension handlers run after the agent loop checks
1074
+ // getSteeringMessages). Buffer steers here, drain at agent_end.
1075
+ let pendingAgentEndSteers: BufferedSteer[] = [];
1076
+
1044
1077
  // --- per-turn exclusion tracking ---
1045
1078
  let steeredThisTurn = new Set<string>();
1046
1079
  pi.on("turn_start", () => { steeredThisTurn = new Set(); });
@@ -1094,8 +1127,43 @@ export default function (pi: ExtensionAPI) {
1094
1127
  const monitorNames = new Set(monitors.map((m) => m.name));
1095
1128
  const monitorsByName = new Map(monitors.map((m) => [m.name, m]));
1096
1129
 
1130
+ const monitorVerbs = ["rules", "patterns", "dismiss", "reset"];
1131
+ const rulesActions = ["add", "remove", "replace"];
1132
+
1097
1133
  pi.registerCommand("monitors", {
1098
1134
  description: "Manage behavior monitors",
1135
+ getArgumentCompletions(argumentPrefix: string) {
1136
+ const tokens = argumentPrefix.split(/\s+/);
1137
+ const last = tokens[tokens.length - 1];
1138
+
1139
+ // Level 0: no complete token yet — show global commands + monitor names
1140
+ if (tokens.length <= 1) {
1141
+ const items = [
1142
+ { value: "on", label: "on", description: "Enable all monitoring" },
1143
+ { value: "off", label: "off", description: "Pause all monitoring" },
1144
+ ...Array.from(monitorNames).map((n) => ({ value: n, label: n, description: `${monitorsByName.get(n)?.description ?? ""} → rules|patterns|dismiss|reset` })),
1145
+ ];
1146
+ return items.filter((i) => i.value.startsWith(last));
1147
+ }
1148
+
1149
+ const name = tokens[0];
1150
+
1151
+ // Level 1: monitor name entered — show verbs
1152
+ if (monitorNames.has(name) && tokens.length === 2) {
1153
+ return monitorVerbs
1154
+ .map((v) => ({ value: `${name} ${v}`, label: v, description: "" }))
1155
+ .filter((i) => i.label.startsWith(last));
1156
+ }
1157
+
1158
+ // Level 2: monitor name + "rules" — show actions
1159
+ if (monitorNames.has(name) && tokens[1] === "rules" && tokens.length === 3) {
1160
+ return rulesActions
1161
+ .map((a) => ({ value: `${name} rules ${a}`, label: a, description: "" }))
1162
+ .filter((i) => i.label.startsWith(last));
1163
+ }
1164
+
1165
+ return null;
1166
+ },
1099
1167
  handler: async (args: string, ctx: ExtensionContext) => {
1100
1168
  const cmd = parseMonitorsArgs(args, monitorNames);
1101
1169
 
@@ -1105,7 +1173,57 @@ export default function (pi: ExtensionAPI) {
1105
1173
  }
1106
1174
 
1107
1175
  if (cmd.type === "list") {
1108
- handleList(monitors, ctx, monitorsEnabled);
1176
+ if (!ctx.hasUI) {
1177
+ handleList(monitors, ctx, monitorsEnabled);
1178
+ return;
1179
+ }
1180
+ const options = [
1181
+ `on — Enable all monitoring`,
1182
+ `off — Pause all monitoring`,
1183
+ ...monitors.map((m) => {
1184
+ const state = m.dismissed ? "dismissed" : m.whileCount > 0 ? `engaged (${m.whileCount}/${m.ceiling})` : "idle";
1185
+ return `${m.name} — ${m.description} [${state}]`;
1186
+ }),
1187
+ ];
1188
+ const selected = await ctx.ui.select("Monitors", options);
1189
+ if (!selected) return;
1190
+ const selectedName = selected.split(" ")[0];
1191
+ if (selectedName === "on") {
1192
+ monitorsEnabled = true;
1193
+ updateStatus();
1194
+ ctx.ui.notify("Monitors enabled", "info");
1195
+ } else if (selectedName === "off") {
1196
+ monitorsEnabled = false;
1197
+ updateStatus();
1198
+ ctx.ui.notify("All monitors paused for this session", "info");
1199
+ } else {
1200
+ const monitor = monitorsByName.get(selectedName);
1201
+ if (!monitor) return;
1202
+ const verbOptions = [
1203
+ `inspect — Show monitor state and config`,
1204
+ `rules — List and manage rules`,
1205
+ `patterns — List known patterns`,
1206
+ `dismiss — Silence for this session`,
1207
+ `reset — Reset state and un-dismiss`,
1208
+ ];
1209
+ const verb = await ctx.ui.select(`[${monitor.name}]`, verbOptions);
1210
+ if (!verb) return;
1211
+ const verbName = verb.split(" ")[0];
1212
+ if (verbName === "inspect") handleInspect(monitor, ctx);
1213
+ else if (verbName === "rules") handleRulesList(monitor, ctx);
1214
+ else if (verbName === "patterns") handlePatternsList(monitor, ctx);
1215
+ else if (verbName === "dismiss") {
1216
+ monitor.dismissed = true;
1217
+ monitor.whileCount = 0;
1218
+ updateStatus();
1219
+ ctx.ui.notify(`[${monitor.name}] Dismissed for this session`, "info");
1220
+ } else if (verbName === "reset") {
1221
+ monitor.dismissed = false;
1222
+ monitor.whileCount = 0;
1223
+ updateStatus();
1224
+ ctx.ui.notify(`[${monitor.name}] Reset`, "info");
1225
+ }
1226
+ }
1109
1227
  return;
1110
1228
  }
1111
1229
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davidorex/pi-behavior-monitors",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Behavior monitors for pi that watch agent activity and steer corrections",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -270,6 +270,13 @@ the session. A `CLEAN` verdict resets the consecutive steer counter.
270
270
  a turn, and monitor B has `"excludes": ["A"]`, monitor B skips that turn. Exclusion tracking
271
271
  resets at `turn_start`.
272
272
 
273
+ **Buffered steer delivery**: Monitors on `message_end` or `turn_end` buffer their steer
274
+ messages and deliver them at `agent_end`. This is because pi's async event queue processes
275
+ extension handlers after the agent loop has already checked for steering messages. The
276
+ buffer is drained at `agent_end` — only the first buffered steer fires per agent run; the
277
+ corrected response re-triggers monitors naturally for any remaining issues. Monitors on
278
+ `agent_end` or `command` events deliver steers immediately (they already run post-loop).
279
+
273
280
  **Abort**: Classification calls are aborted when the agent ends (via `agent_end` event).
274
281
  Aborted classifications produce no verdict and no action.
275
282
 
@@ -280,7 +287,9 @@ the `id` field of array entries.
280
287
  </runtime_behavior>
281
288
 
282
289
  <commands>
283
- All monitor management is through the `/monitors` command:
290
+ All monitor management is through the `/monitors` command. Subcommands are
291
+ discoverable via pi's TUI autocomplete — typing `/monitors ` shows available
292
+ monitor names and global commands; selecting a monitor shows its verbs.
284
293
 
285
294
  | Command | Description |
286
295
  |---------|-------------|