@rulemetric/hooks 0.7.41 → 0.7.43

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulemetric/hooks",
3
- "version": "0.7.41",
3
+ "version": "0.7.43",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -124,35 +124,124 @@ _rulemetric_suggest_instructions() {
124
124
  # suggestions arrive with the actual rule body. The body is rendered to
125
125
  # stdout, which Claude Code surfaces to the model as session context.
126
126
  # Low-confidence suggestions still render as name+reason only.
127
+ # limit=10 (the API's clamp), not 3. The feed now carries THREE directions
128
+ # ranked by one score, and a trim scores 0.55 — below every add and every
129
+ # removal — so a shallow fetch would sort it off the end and the trim
130
+ # direction would appear to produce nothing. Fetching the maximum and capping
131
+ # per direction below is what keeps the weakest signal reachable. It is still
132
+ # possible for a project with >10 pending adds to crowd the trim out; that is
133
+ # accepted, it is the softest row in the feed.
134
+ # The feed carries BOTH directions ranked by one score,
135
+ # and a measured-harmful retirement can score up to 0.95 — so at limit=3 a bad
136
+ # week of grooming would evict every adoption proposal and the additive feed
137
+ # would silently go to zero. Over-fetch, then cap each direction separately
138
+ # below so neither can starve the other.
127
139
  response="$(curl -s --noproxy '*' --max-time 4 \
128
- "${api_url}/api/instruction-suggestions?projectPath=${encoded_path}&limit=3&includeContent=true" \
140
+ "${api_url}/api/instruction-suggestions?projectPath=${encoded_path}&limit=10&includeContent=true&groupByKind=true" \
129
141
  -H "Authorization: Bearer ${auth_token}" 2>/dev/null)" || return 0
130
142
 
131
143
  local count
132
144
  count="$(printf '%s' "$response" | jq -r '.suggestions | length' 2>/dev/null)" || return 0
133
145
  [ "$count" != "0" ] && [ "$count" != "null" ] && [ -n "$count" ] || return 0
134
146
 
135
- # Count high-confidence rows (those with non-null content) for the header
147
+ # Per-direction index lists, capped independently. `kind` is absent on an older
148
+ # API, so an untagged row is an adoption proposal — the historical behaviour.
149
+ local add_idx rem_idx trim_idx render_idx
150
+ add_idx="$(printf '%s' "$response" | jq -r \
151
+ '[.suggestions | to_entries[] | select((.value.kind // "add") == "add") | .key][0:3] | .[]' 2>/dev/null)" || add_idx=""
152
+ rem_idx="$(printf '%s' "$response" | jq -r \
153
+ '[.suggestions | to_entries[] | select(.value.kind == "remove") | .key][0:2] | .[]' 2>/dev/null)" || rem_idx=""
154
+ trim_idx="$(printf '%s' "$response" | jq -r \
155
+ '[.suggestions | to_entries[] | select(.value.kind == "trim") | .key][0:1] | .[]' 2>/dev/null)" || trim_idx=""
156
+ render_idx="$(printf '%s\n%s\n%s\n' "$add_idx" "$rem_idx" "$trim_idx" | grep -v '^$' || true)"
157
+ [ -n "$render_idx" ] || return 0
158
+
159
+ # Count high-confidence rows among the ADDS WE WILL ACTUALLY RENDER. Counting
160
+ # the whole response over-counts now that the fetch pulls 6 to cap 3+2: the
161
+ # header announced "5 high-confidence instruction(s) included below" with 3
162
+ # below it. A header that miscounts what follows it is how a reader stops
163
+ # trusting the block.
136
164
  local high_count
137
- high_count="$(printf '%s' "$response" | jq -r '[.suggestions[] | select(.content != null)] | length' 2>/dev/null)" || high_count="0"
165
+ high_count="$(printf '%s' "$response" | jq -r \
166
+ '[[.suggestions | to_entries[] | select((.value.kind // "add") == "add")][0:3][] | select(.value.content != null)] | length' 2>/dev/null)" || high_count="0"
167
+ # ONE count for the shared "costing you" header. Counting only removals would
168
+ # under-report the moment a trim renders beneath the same heading — the same
169
+ # header-miscounts-its-own-list bug just fixed above, reintroduced by a third
170
+ # direction.
171
+ local rem_count trim_count cost_count
172
+ rem_count="$(printf '%s\n' "$rem_idx" | grep -c '[0-9]' 2>/dev/null)" || rem_count=0
173
+ trim_count="$(printf '%s\n' "$trim_idx" | grep -c '[0-9]' 2>/dev/null)" || trim_count=0
174
+ cost_count=$((rem_count + trim_count))
175
+
176
+ # The header describes the ADDITIVE rows only. `count` is now a mixed total, so
177
+ # using it here would announce "N instructions may help with this project" and
178
+ # then list retirements underneath it.
179
+ local add_count
180
+ add_count="$(printf '%s\n' "$add_idx" | grep -c '[0-9]' 2>/dev/null)" || add_count=0
138
181
 
139
182
  printf '\n---\n'
140
183
  if [ "$high_count" != "0" ] && [ -n "$high_count" ]; then
141
184
  printf '[rulemetric] %s high-confidence instruction(s) included below as session context.\n' "$high_count"
142
185
  printf 'Accept any of them permanently with the command shown under each one.\n'
143
- else
144
- printf '[rulemetric] %s instruction(s) may help with this project:\n' "$count"
186
+ elif [ "$add_count" != "0" ]; then
187
+ printf '[rulemetric] %s instruction(s) may help with this project:\n' "$add_count"
145
188
  fi
146
189
 
147
- local i=0
148
- while [ "$i" -lt "$count" ]; do
149
- local name reason score scope content sid
190
+ local rendered_removal_header=0
191
+ local i
192
+ for i in $render_idx; do
193
+ local name reason score scope content sid kind tokens
150
194
  name="$(printf '%s' "$response" | jq -r ".suggestions[$i].name" 2>/dev/null)" || break
151
195
  reason="$(printf '%s' "$response" | jq -r ".suggestions[$i].reason" 2>/dev/null)" || break
152
196
  score="$(printf '%s' "$response" | jq -r ".suggestions[$i].score" 2>/dev/null)" || break
153
197
  scope="$(printf '%s' "$response" | jq -r ".suggestions[$i].instructionScope" 2>/dev/null)" || break
154
198
  content="$(printf '%s' "$response" | jq -r ".suggestions[$i].content // empty" 2>/dev/null)" || content=""
155
199
  sid="$(printf '%s' "$response" | jq -r ".suggestions[$i].id" 2>/dev/null)" || break
200
+ kind="$(printf '%s' "$response" | jq -r ".suggestions[$i].kind // \"add\"" 2>/dev/null)" || kind="add"
201
+ tokens="$(printf '%s' "$response" | jq -r ".suggestions[$i].estimatedTokens // empty" 2>/dev/null)" || tokens=""
202
+
203
+ if [ "$kind" = "trim" ]; then
204
+ # Shares the cost heading with retirements — both are "this is charging
205
+ # you context" — but never the verb. Neutral means no effect was
206
+ # DETECTED, which is not evidence the rule is worthless, so the ask is
207
+ # "shorten", never "delete".
208
+ if [ "$rendered_removal_header" = "0" ]; then
209
+ printf '\n[rulemetric] %s instruction(s) look like they are costing you more than they return:\n' "$cost_count"
210
+ rendered_removal_header=1
211
+ fi
212
+ if [ -n "$tokens" ] && [ "$tokens" != "null" ]; then
213
+ printf ' - TRIM [%s] %s (~%s tokens/session, measures neutral)\n %s\n' "$scope" "$name" "$tokens" "$reason"
214
+ else
215
+ printf ' - TRIM [%s] %s (measures neutral)\n %s\n' "$scope" "$name" "$reason"
216
+ fi
217
+ if [ -n "$sid" ] && [ "$sid" != "null" ]; then
218
+ printf ' Acknowledge: rulemetric suggestions accept %s (no automatic change; shorten it yourself)\n' "$sid"
219
+ fi
220
+ continue
221
+ fi
222
+
223
+ if [ "$kind" = "remove" ]; then
224
+ # Retirements get their own labelled section and their own verb. Rendered
225
+ # in the additive shape they would read as "adopt this" to both the human
226
+ # and the model — the same text that has always meant "add" would now
227
+ # sometimes mean "delete", which is how someone archives a rule they meant
228
+ # to keep. The body is deliberately NOT here: the API withholds content for
229
+ # this direction, so proposing a retirement never pays the cost of the rule
230
+ # it is proposing to retire.
231
+ if [ "$rendered_removal_header" = "0" ]; then
232
+ printf '\n[rulemetric] %s instruction(s) look like they are costing you more than they return:\n' "$cost_count"
233
+ rendered_removal_header=1
234
+ fi
235
+ if [ -n "$tokens" ] && [ "$tokens" != "null" ]; then
236
+ printf ' - RETIRE [%s] %s (~%s tokens/session)\n %s\n' "$scope" "$name" "$tokens" "$reason"
237
+ else
238
+ printf ' - RETIRE [%s] %s\n %s\n' "$scope" "$name" "$reason"
239
+ fi
240
+ if [ -n "$sid" ] && [ "$sid" != "null" ]; then
241
+ printf ' Retire it: rulemetric suggestions accept %s (archives it; undo: rulemetric instructions restore <instruction-id>)\n' "$sid"
242
+ fi
243
+ continue
244
+ fi
156
245
 
157
246
  if [ -n "$content" ]; then
158
247
  printf '\n## Skill: %s (%s, score: %s)\n%s\n\n%s\n' "$name" "$scope" "$score" "$reason" "$content"
@@ -167,16 +256,18 @@ _rulemetric_suggest_instructions() {
167
256
  if [ -n "$sid" ] && [ "$sid" != "null" ]; then
168
257
  printf ' Accept: rulemetric suggestions accept %s\n' "$sid"
169
258
  fi
170
- i=$((i + 1))
171
259
  done
172
260
 
173
261
  printf '\n---\n'
174
262
 
175
263
  # Log surfaced events fire-and-forget. Event type differs based on whether
176
264
  # the full content was included so we can later measure outcome differences.
177
- i=0
265
+ # Only the rows actually rendered. Iterating 0..count would log a `surfaced`
266
+ # event for suggestions the over-fetch pulled but the per-direction caps never
267
+ # showed — inflating the denominator of the exact acceptance rate this feed
268
+ # exists to measure.
178
269
  local tool="${HOOK_TOOL:-unknown}"
179
- while [ "$i" -lt "$count" ]; do
270
+ for i in $render_idx; do
180
271
  local sid has_content event_type
181
272
  sid="$(printf '%s' "$response" | jq -r ".suggestions[$i].id" 2>/dev/null)" || break
182
273
  has_content="$(printf '%s' "$response" | jq -r ".suggestions[$i].content // \"null\"" 2>/dev/null)" || has_content="null"
@@ -200,7 +291,6 @@ _rulemetric_suggest_instructions() {
200
291
  '{eventType: $eventType, tool: $tool, projectPath: $projectPath, sessionId: $sessionId}')" \
201
292
  >/dev/null 2>&1 &
202
293
  disown 2>/dev/null || true
203
- i=$((i + 1))
204
294
  done
205
295
  }
206
296