browser-automation-skill 0.71.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 (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +144 -0
  3. package/SECURITY.md +39 -0
  4. package/SKILL.md +206 -0
  5. package/bin/cli.mjs +55 -0
  6. package/install.sh +143 -0
  7. package/package.json +54 -0
  8. package/references/adapter-candidates.md +40 -0
  9. package/references/browser-mcp-cheatsheet.md +132 -0
  10. package/references/browser-stats-cheatsheet.md +155 -0
  11. package/references/chrome-devtools-mcp-cheatsheet.md +232 -0
  12. package/references/midscene-integration.md +359 -0
  13. package/references/obscura-cheatsheet.md +103 -0
  14. package/references/playwright-cli-cheatsheet.md +64 -0
  15. package/references/playwright-lib-cheatsheet.md +90 -0
  16. package/references/recipes/add-a-tool-adapter.md +134 -0
  17. package/references/recipes/agent-workflows/README.md +37 -0
  18. package/references/recipes/agent-workflows/cache-driven-bulk-operation.md +110 -0
  19. package/references/recipes/agent-workflows/flow-record-and-replay.md +102 -0
  20. package/references/recipes/agent-workflows/incremental-pattern-discovery.md +125 -0
  21. package/references/recipes/agent-workflows/login-then-scrape.md +100 -0
  22. package/references/recipes/anti-patterns-tool-extension.md +182 -0
  23. package/references/recipes/body-bytes-not-body.md +139 -0
  24. package/references/recipes/cache-write-security.md +210 -0
  25. package/references/recipes/fingerprint-rescue.md +154 -0
  26. package/references/recipes/model-routing.md +143 -0
  27. package/references/recipes/path-security.md +138 -0
  28. package/references/recipes/privacy-canary.md +96 -0
  29. package/references/recipes/visual-rescue-hook.md +182 -0
  30. package/references/stats-prices.json +42 -0
  31. package/references/stats-schema.json +77 -0
  32. package/references/tool-versions.md +8 -0
  33. package/scripts/browser-add-site.sh +113 -0
  34. package/scripts/browser-assert.sh +106 -0
  35. package/scripts/browser-audit.sh +68 -0
  36. package/scripts/browser-baseline.sh +135 -0
  37. package/scripts/browser-click.sh +100 -0
  38. package/scripts/browser-creds-add.sh +254 -0
  39. package/scripts/browser-creds-list.sh +67 -0
  40. package/scripts/browser-creds-migrate.sh +122 -0
  41. package/scripts/browser-creds-remove.sh +69 -0
  42. package/scripts/browser-creds-rotate-totp.sh +109 -0
  43. package/scripts/browser-creds-show.sh +82 -0
  44. package/scripts/browser-creds-totp.sh +94 -0
  45. package/scripts/browser-do.sh +630 -0
  46. package/scripts/browser-doctor.sh +365 -0
  47. package/scripts/browser-drag.sh +90 -0
  48. package/scripts/browser-extract.sh +192 -0
  49. package/scripts/browser-fill.sh +142 -0
  50. package/scripts/browser-flow.sh +316 -0
  51. package/scripts/browser-history.sh +187 -0
  52. package/scripts/browser-hover.sh +92 -0
  53. package/scripts/browser-inspect.sh +188 -0
  54. package/scripts/browser-list-sessions.sh +78 -0
  55. package/scripts/browser-list-sites.sh +42 -0
  56. package/scripts/browser-login.sh +279 -0
  57. package/scripts/browser-mcp.sh +65 -0
  58. package/scripts/browser-migrate.sh +195 -0
  59. package/scripts/browser-open.sh +134 -0
  60. package/scripts/browser-press.sh +80 -0
  61. package/scripts/browser-remove-session.sh +72 -0
  62. package/scripts/browser-remove-site.sh +68 -0
  63. package/scripts/browser-replay.sh +206 -0
  64. package/scripts/browser-route.sh +174 -0
  65. package/scripts/browser-select.sh +122 -0
  66. package/scripts/browser-show-session.sh +57 -0
  67. package/scripts/browser-show-site.sh +37 -0
  68. package/scripts/browser-snapshot.sh +176 -0
  69. package/scripts/browser-stats.sh +522 -0
  70. package/scripts/browser-tab-close.sh +112 -0
  71. package/scripts/browser-tab-list.sh +70 -0
  72. package/scripts/browser-tab-switch.sh +111 -0
  73. package/scripts/browser-upload.sh +132 -0
  74. package/scripts/browser-use.sh +60 -0
  75. package/scripts/browser-vlm.sh +707 -0
  76. package/scripts/browser-wait.sh +97 -0
  77. package/scripts/install-git-hooks.sh +16 -0
  78. package/scripts/lib/capture.sh +356 -0
  79. package/scripts/lib/common.sh +262 -0
  80. package/scripts/lib/credential.sh +237 -0
  81. package/scripts/lib/fingerprint-rescue.js +123 -0
  82. package/scripts/lib/flow.sh +448 -0
  83. package/scripts/lib/flow_record.sh +210 -0
  84. package/scripts/lib/mask.sh +49 -0
  85. package/scripts/lib/memory.sh +427 -0
  86. package/scripts/lib/migrate.sh +390 -0
  87. package/scripts/lib/migrators/README.md +23 -0
  88. package/scripts/lib/migrators/memory/v1_to_v2.sh +15 -0
  89. package/scripts/lib/migrators/recent_urls/README.md +13 -0
  90. package/scripts/lib/migrators/stats/README.md +24 -0
  91. package/scripts/lib/node/chrome-devtools-bridge.mjs +1812 -0
  92. package/scripts/lib/node/mcp-server.mjs +531 -0
  93. package/scripts/lib/node/mcp-tools.json +68 -0
  94. package/scripts/lib/node/playwright-driver.mjs +1104 -0
  95. package/scripts/lib/node/totp-core.mjs +52 -0
  96. package/scripts/lib/node/totp.mjs +52 -0
  97. package/scripts/lib/node/url-pattern-cluster.mjs +102 -0
  98. package/scripts/lib/node/url-pattern-resolver.mjs +77 -0
  99. package/scripts/lib/output.sh +79 -0
  100. package/scripts/lib/router.sh +342 -0
  101. package/scripts/lib/sanitize.sh +107 -0
  102. package/scripts/lib/secret/keychain.sh +91 -0
  103. package/scripts/lib/secret/libsecret.sh +74 -0
  104. package/scripts/lib/secret/plaintext.sh +75 -0
  105. package/scripts/lib/secret_backend_select.sh +57 -0
  106. package/scripts/lib/session.sh +153 -0
  107. package/scripts/lib/site.sh +126 -0
  108. package/scripts/lib/stats.sh +419 -0
  109. package/scripts/lib/tool/.gitkeep +0 -0
  110. package/scripts/lib/tool/chrome-devtools-mcp.sh +349 -0
  111. package/scripts/lib/tool/obscura.sh +249 -0
  112. package/scripts/lib/tool/playwright-cli.sh +155 -0
  113. package/scripts/lib/tool/playwright-lib.sh +106 -0
  114. package/scripts/lib/verb_helpers.sh +222 -0
  115. package/scripts/lib/visual-rescue-default.sh +145 -0
  116. package/scripts/regenerate-docs.sh +99 -0
  117. package/uninstall.sh +51 -0
@@ -0,0 +1,419 @@
1
+ # shellcheck shell=bash
2
+ # scripts/lib/stats.sh — Phase 12 part 1: per-action telemetry / "balance triangle" audit.
3
+ #
4
+ # Emits one JSONL event per adapter invocation to
5
+ # ${BROWSER_SKILL_HOME}/memory/stats.jsonl (mode 0600, parent 0700).
6
+ # Each event captures: route, verb, selector kind/hit, retries, duration,
7
+ # stdout/stderr byte sizes (token proxies for the bash skill), post-condition
8
+ # result, and a 13-value failure-mode enum.
9
+ #
10
+ # Optional gen_ai.* token fields are populated only when Claude Code injects
11
+ # them via env (CLAUDE_USAGE_INPUT_TOKENS / _OUTPUT_TOKENS / _CACHE_READ_TOKENS
12
+ # / _CACHE_CREATE_TOKENS / _MODEL / _SERVICE_TIER) — left null otherwise.
13
+ # Field naming follows OpenInference + OTel GenAI v1.40 conventions so the log
14
+ # is forward-compatible with Langfuse/Phoenix/Jaeger via an OTLP exporter.
15
+ #
16
+ # Best-effort writer — failure NEVER taints the verb's exit code. Mirrors the
17
+ # contract of scripts/browser-do.sh::_record_event and lib/memory.sh::
18
+ # memory_record_recent_url (Phase 11 v2 prior art).
19
+ #
20
+ # Public API:
21
+ # stats_init_dir — lazy-create memory dir mode 0700
22
+ # stats_random_id — 16-hex-char id (fork-free $RANDOM
23
+ # unless STATS_USE_CRYPTO_ID=1)
24
+ # stats_now_iso_ms — ISO 8601 with ms precision; uses
25
+ # $EPOCHREALTIME (bash 5.0+) — no fork
26
+ # stats_classify_failure RC OUT ERR — echo one failure_mode enum value
27
+ # stats_postcond_check T M EXP OBS — return 0/1; sets STATS_POSTCOND_HIT
28
+ # stats_extract_selector_meta ARGS... — sets STATS_SEL_KIND, STATS_SEL_VALUE
29
+ # stats_emit_event JSON_OBJECT — append one JSONL line (best-effort)
30
+ # stats_run_adapter_emit VERB ROUTE T0 RC STDOUT STDERR -- ARGS...
31
+ # — convenience helper for verb scripts.
32
+ # Post-condition contract comes via env:
33
+ # STATS_EXPECT_TYPE ∈ url|element_path|element_value
34
+ # STATS_EXPECT_MATCH ∈ exact|include|semantic (default: include)
35
+ # STATS_EXPECT_VALUE (string; "" disables check)
36
+ # STATS_OBSERVED (string the verb measured)
37
+ #
38
+ # Performance notes (Phase 12 part 2 — audit improvements):
39
+ # - Bash 5.0+ required. macOS users need Homebrew bash (already the case for
40
+ # this skill's other bash-isms). $EPOCHREALTIME + LC_ALL=C ${#var} + $RANDOM
41
+ # replace ~9 forks per emit; one jq invocation remains.
42
+ # - chmod 600 runs only on file creation, not every emit.
43
+ #
44
+ # Schema: see references/stats-schema.json. Schema version: 1.
45
+
46
+ [ -n "${BROWSER_SKILL_STATS_LOADED:-}" ] && return 0
47
+ readonly BROWSER_SKILL_STATS_LOADED=1
48
+
49
+ readonly STATS_SCHEMA_VERSION=1
50
+
51
+ # Failure-mode enum (synced with WAREX + Agent-E + WebVoyager taxonomies).
52
+ # Update references/stats-schema.json::failure_mode.enum in lockstep.
53
+ # Phase 14 (Bundle #3) added `unknown_failure` as the catch-all so rc!=0 events
54
+ # never silently drop out of the histogram. Real telemetry (35-event sample)
55
+ # had 100% of rc!=0 events with failure_mode=null — invisible to `stats tune`.
56
+ # shellcheck disable=SC2034 # documentation constant; consumers grep this for the canonical list
57
+ readonly STATS_FAILURE_MODES="element_not_found element_ambiguous wrong_element_acted stale_ref action_timeout navigation_mismatch js_not_ready network_error captcha_blocked auth_required popup_intercept extraction_mismatch oblivious_success unknown_failure"
58
+
59
+ # stats_init_dir — idempotent mkdir + chmod for ${BROWSER_SKILL_HOME}/memory/.
60
+ # Same lazy-create pattern as memory_init_dir in lib/memory.sh; duplicated here
61
+ # so callers that don't source memory.sh can still emit (e.g. extract verb).
62
+ stats_init_dir() {
63
+ local dir
64
+ dir="${BROWSER_SKILL_HOME}/memory"
65
+ mkdir -p "${dir}" 2>/dev/null || return 1
66
+ chmod 700 "${dir}" 2>/dev/null || true
67
+ }
68
+
69
+ # stats_random_id — 16 hex chars. Fork-free $RANDOM-based by default (~60 bits
70
+ # effective entropy; adequate for in-session correlation IDs, NOT cryptography).
71
+ # Set STATS_USE_CRYPTO_ID=1 to fall back to `openssl rand -hex 8` when crypto-
72
+ # strength uniqueness is needed (e.g. cross-session log export).
73
+ stats_random_id() {
74
+ if [ "${STATS_USE_CRYPTO_ID:-0}" = "1" ] && command -v openssl >/dev/null 2>&1; then
75
+ openssl rand -hex 8
76
+ else
77
+ # 4 × $RANDOM (15 bits each, %04x pads to 4 hex chars). printf -v keeps
78
+ # the result in a var without forking; piping to stdout is one printf call.
79
+ local _hex
80
+ printf -v _hex '%04x%04x%04x%04x' "${RANDOM}" "${RANDOM}" "${RANDOM}" "${RANDOM}"
81
+ printf '%s\n' "${_hex}"
82
+ fi
83
+ }
84
+
85
+ # stats_now_iso_ms — ISO 8601 with ms precision, UTC. Uses bash 5.0+ $EPOCHREALTIME
86
+ # (no fork) + the bash-4.2+ `printf -v %()T` builtin date formatter (no fork).
87
+ # Fallback for bash <5.0 is a single `date -u` fork at second precision — no
88
+ # python3 dependency, no millisecond gymnastics.
89
+ stats_now_iso_ms() {
90
+ if [ -n "${EPOCHREALTIME:-}" ]; then
91
+ local secs=${EPOCHREALTIME%.*}
92
+ local frac=${EPOCHREALTIME#*.}
93
+ local ms=${frac:0:3}
94
+ local _ts
95
+ # %()T strftime reads TZ at call time; env-prefix sets it for this builtin.
96
+ TZ=UTC printf -v _ts '%(%Y-%m-%dT%H:%M:%S)T.%sZ' "${secs}" "${ms}"
97
+ printf '%s\n' "${_ts}"
98
+ else
99
+ # Legacy fallback (bash <5.0). Second precision only.
100
+ date -u +%Y-%m-%dT%H:%M:%SZ
101
+ fi
102
+ }
103
+
104
+ # stats_classify_failure RC STDOUT STDERR
105
+ # Echo one failure_mode enum value (or empty when outcome is success).
106
+ # Heuristic — looks for adapter-specific markers in stdout/stderr.
107
+ # RC == 0 → empty (no failure).
108
+ # Otherwise pattern-match exit codes + error text. Phase 14 (Bundle #3): when
109
+ # no pattern matches but rc!=0, fall back to `unknown_failure` (was empty).
110
+ # Empty silently dropped events from the histogram so `stats tune` couldn't
111
+ # surface them. The `unknown_failure` bucket is the explicit "we know it failed
112
+ # but couldn't classify" signal — actionable for adding new patterns later.
113
+ stats_classify_failure() {
114
+ local rc="$1" out="$2" err="$3"
115
+ [ "${rc}" = "0" ] && return 0
116
+
117
+ # Exit code first (common.sh constants).
118
+ case "${rc}" in
119
+ 13) printf 'extraction_mismatch\n'; return 0 ;; # EXIT_ASSERTION_FAILED
120
+ 22) printf 'auth_required\n'; return 0 ;; # EXIT_SESSION_EXPIRED
121
+ 25) printf 'auth_required\n'; return 0 ;; # EXIT_AUTH_INTERACTIVE_REQUIRED
122
+ 30) printf 'network_error\n'; return 0 ;; # EXIT_NETWORK_ERROR
123
+ 43) printf 'action_timeout\n'; return 0 ;; # EXIT_TOOL_TIMEOUT
124
+ esac
125
+
126
+ local combined="${out}${err}"
127
+ # Order matters — earlier patterns win on overlap. Each branch lists
128
+ # NON-OVERLAPPING substrings: e.g. *"captcha"* already matches "hcaptcha"
129
+ # and "recaptcha", so listing those separately would be dead code.
130
+ case "${combined}" in
131
+ *"captcha"*|*"Cloudflare"*)
132
+ printf 'captcha_blocked\n'; return 0 ;;
133
+ *"login required"*|*"unauthorized"*|*" 401 "*|*" 403 "*)
134
+ printf 'auth_required\n'; return 0 ;;
135
+ *"strict mode"*|*"ambiguous"*)
136
+ printf 'element_ambiguous\n'; return 0 ;;
137
+ *"not found"*|*"no element"*|*"Target closed"*|*"detached"*)
138
+ printf 'element_not_found\n'; return 0 ;;
139
+ *"stale "*|*"snapshot is outdated"*|*"ref expired"*|*"invalid ref"*)
140
+ printf 'stale_ref\n'; return 0 ;;
141
+ *"timeout"*|*"timed out"*|*"exceeded"*)
142
+ printf 'action_timeout\n'; return 0 ;;
143
+ *"net::ERR"*|*"ECONNREFUSED"*|*"ENOTFOUND"*|*"ETIMEDOUT"*)
144
+ printf 'network_error\n'; return 0 ;;
145
+ *"navigation"*|*"redirect"*|*"URL did not match"*)
146
+ printf 'navigation_mismatch\n'; return 0 ;;
147
+ *"modal"*|*"dialog"*|*"consent"*|*"popup"*)
148
+ printf 'popup_intercept\n'; return 0 ;;
149
+ *"script error"*|*"ReferenceError"*|*"TypeError"*)
150
+ printf 'js_not_ready\n'; return 0 ;;
151
+ esac
152
+ # No pattern matched but rc!=0 — explicit catch-all so the event surfaces in
153
+ # `stats tune` histograms instead of being silently dropped as null.
154
+ printf 'unknown_failure\n'
155
+ return 0
156
+ }
157
+
158
+ # stats_postcond_check TYPE MATCHER EXPECTED OBSERVED
159
+ # Verify a post-condition. TYPE ∈ {url, element_path, element_value};
160
+ # MATCHER ∈ {exact, include, semantic}. Returns 0 on hit, 1 on miss.
161
+ # Sets STATS_POSTCOND_HIT="true"/"false" so callers can serialize.
162
+ # Semantic matcher v1 = case-insensitive substring (placeholder for LLM-judge).
163
+ stats_postcond_check() {
164
+ local type="$1" matcher="$2" expected="$3" observed="$4"
165
+ STATS_POSTCOND_HIT="false"
166
+ [ -z "${type}" ] && return 1
167
+ [ -z "${expected}" ] && return 1
168
+ case "${matcher}" in
169
+ exact)
170
+ [ "${expected}" = "${observed}" ] && STATS_POSTCOND_HIT="true"
171
+ ;;
172
+ include)
173
+ case "${observed}" in
174
+ *"${expected}"*) STATS_POSTCOND_HIT="true" ;;
175
+ esac
176
+ ;;
177
+ semantic)
178
+ # v1 placeholder: case-insensitive substring. Upgrade path = LLM-judge.
179
+ local exp_lc="${expected,,}" obs_lc="${observed,,}"
180
+ case "${obs_lc}" in
181
+ *"${exp_lc}"*) STATS_POSTCOND_HIT="true" ;;
182
+ esac
183
+ ;;
184
+ *)
185
+ return 1
186
+ ;;
187
+ esac
188
+ [ "${STATS_POSTCOND_HIT}" = "true" ]
189
+ }
190
+
191
+ # stats_extract_selector_meta ARGS...
192
+ # Walks an argv looking for --ref / --selector / --src-ref / --dst-ref and
193
+ # sets STATS_SEL_KIND ∈ {a11y_ref, css, role, text, none} + STATS_SEL_VALUE.
194
+ # Read-only on argv — does not modify positional params.
195
+ stats_extract_selector_meta() {
196
+ STATS_SEL_KIND="none"
197
+ STATS_SEL_VALUE=""
198
+ local prev=""
199
+ local a
200
+ for a in "$@"; do
201
+ case "${prev}" in
202
+ --ref|--src-ref|--dst-ref)
203
+ STATS_SEL_KIND="a11y_ref"
204
+ STATS_SEL_VALUE="${a}"
205
+ return 0
206
+ ;;
207
+ --selector)
208
+ STATS_SEL_VALUE="${a}"
209
+ case "${a}" in
210
+ 'role='*|"[role="*) STATS_SEL_KIND="role" ;;
211
+ 'text='*|*':has-text('*) STATS_SEL_KIND="text" ;;
212
+ *) STATS_SEL_KIND="css" ;;
213
+ esac
214
+ return 0
215
+ ;;
216
+ esac
217
+ prev="${a}"
218
+ done
219
+ return 0
220
+ }
221
+
222
+ # stats_emit_event JSON_OBJECT
223
+ # Append one canonicalised JSONL line to memory/stats.jsonl. Best-effort —
224
+ # emits warn: on failure but never taints exit code.
225
+ # Phase 12 part 2: chmod runs only on file creation, not every emit (eliminates
226
+ # one fork per write on the hot path).
227
+ stats_emit_event() {
228
+ local payload="$1"
229
+ if ! stats_init_dir; then
230
+ warn "stats: could not create memory dir; event skipped"
231
+ return 0
232
+ fi
233
+ local file="${BROWSER_SKILL_HOME}/memory/stats.jsonl"
234
+ local needs_chmod=0
235
+ [ -f "${file}" ] || needs_chmod=1
236
+ local line
237
+ if ! line="$(printf '%s' "${payload}" | jq -c . 2>/dev/null)"; then
238
+ warn "stats: jq encode failed (best-effort; action exit unchanged)"
239
+ return 0
240
+ fi
241
+ if ! printf '%s\n' "${line}" >> "${file}" 2>/dev/null; then
242
+ warn "stats: append failed (best-effort; action exit unchanged)"
243
+ return 0
244
+ fi
245
+ [ "${needs_chmod}" = "1" ] && chmod 600 "${file}" 2>/dev/null
246
+ return 0
247
+ }
248
+
249
+ # stats_run_adapter_emit VERB ROUTE T0_MS RC STDOUT STDERR -- ARGS...
250
+ # Convenience wrapper that builds + emits a complete event from raw bash vars.
251
+ # Caller passes already-captured stdout/stderr (as strings); we compute byte
252
+ # sizes from them. ARGS... is the verb's own argv (used by selector extractor).
253
+ #
254
+ # Post-condition contract comes via env (kept out of positional args to keep
255
+ # the call-site readable — see Phase 12 part 2 audit):
256
+ # STATS_EXPECT_TYPE ∈ url | element_path | element_value
257
+ # STATS_EXPECT_MATCH ∈ exact | include | semantic (default: include)
258
+ # STATS_EXPECT_VALUE string ("" disables check)
259
+ # STATS_OBSERVED string the verb measured
260
+ # Verb scripts conventionally export BROWSER_STATS_EXPECT_* / BROWSER_STATS_OBSERVED
261
+ # and this helper reads either prefix (BROWSER_STATS_* preferred, STATS_* legacy).
262
+ stats_run_adapter_emit() {
263
+ local verb="$1" route="$2" t0_ms="$3" rc="$4"
264
+ local stdout="$5" stderr="$6"
265
+ shift 6
266
+ [ "${1:-}" = "--" ] && shift
267
+
268
+ # Resolve post-condition fields from env (BROWSER_STATS_* wins; STATS_* legacy fallback).
269
+ local exp_type="${BROWSER_STATS_EXPECT_TYPE:-${STATS_EXPECT_TYPE:-}}"
270
+ local exp_match="${BROWSER_STATS_EXPECT_MATCH:-${STATS_EXPECT_MATCH:-include}}"
271
+ local exp_value="${BROWSER_STATS_EXPECT_VALUE:-${STATS_EXPECT_VALUE:-}}"
272
+ local observed="${BROWSER_STATS_OBSERVED:-${STATS_OBSERVED:-}}"
273
+
274
+ local t1_ms duration_ms
275
+ t1_ms="$(now_ms)"
276
+ duration_ms=$(( t1_ms - t0_ms ))
277
+
278
+ stats_extract_selector_meta "$@"
279
+
280
+ # Phase 14 (Bundle #3): span_id generated up-front so the unknown_failure
281
+ # self-healing hint can quote the exact event the operator will grep for.
282
+ local span_id trace_id ts
283
+ span_id="$(stats_random_id)"
284
+ trace_id="${BROWSER_SKILL_TRACE_ID:-${span_id}}"
285
+ ts="$(stats_now_iso_ms)"
286
+
287
+ local failure_mode outcome
288
+ if [ "${rc}" = "0" ]; then
289
+ outcome="success"
290
+ failure_mode=""
291
+ else
292
+ outcome="fail"
293
+ failure_mode="$(stats_classify_failure "${rc}" "${stdout}" "${stderr}")"
294
+ # Phase 14 (Bundle #3): self-healing hint per spec §2.5 when classifier
295
+ # bucketed the failure as unknown. Tells operator exactly where to look
296
+ # to extend the pattern table — span_id lets them grep stats.jsonl for
297
+ # the full event including argv/stderr.
298
+ if [ "${failure_mode}" = "unknown_failure" ]; then
299
+ warn "${verb} exited rc=${rc}; no diagnosable signal — span_id=${span_id} in memory/stats.jsonl"
300
+ fi
301
+ fi
302
+
303
+ STATS_POSTCOND_HIT=""
304
+ local postcond_hit_json="null"
305
+ if [ -n "${exp_type}" ] && [ -n "${exp_value}" ]; then
306
+ if stats_postcond_check "${exp_type}" "${exp_match:-include}" "${exp_value}" "${observed}"; then
307
+ postcond_hit_json="true"
308
+ else
309
+ postcond_hit_json="false"
310
+ # Oblivious-success detection: adapter said OK but post-condition fails.
311
+ if [ "${outcome}" = "success" ]; then
312
+ outcome="partial"
313
+ failure_mode="oblivious_success"
314
+ fi
315
+ fi
316
+ fi
317
+
318
+ local site
319
+ # span_id, trace_id, ts already populated above so the unknown_failure hint
320
+ # can reference span_id without forward-declaring vars.
321
+ site="${ARG_SITE:-}"
322
+ if [ -z "${site}" ] && command -v current_get >/dev/null 2>&1; then
323
+ site="$(current_get 2>/dev/null || true)"
324
+ fi
325
+
326
+ # Byte counts via LC_ALL=C ${#var} — bash builtin, fork-free.
327
+ # `${#var}` returns CHARS in UTF-8 locale, BYTES under LC_ALL=C. Save+restore
328
+ # the prior LC_ALL so the rest of the function (jq) sees its original locale.
329
+ local argv_bytes stdout_bytes stderr_bytes _argv_str _saved_lc="${LC_ALL-}"
330
+ LC_ALL=C
331
+ _argv_str="$*"
332
+ argv_bytes=${#_argv_str}
333
+ stdout_bytes=${#stdout}
334
+ stderr_bytes=${#stderr}
335
+ if [ -z "${_saved_lc}" ]; then
336
+ unset LC_ALL
337
+ else
338
+ LC_ALL="${_saved_lc}"
339
+ fi
340
+
341
+ # Token fields from env (Claude Code injects when available; null otherwise).
342
+ local model="${CLAUDE_MODEL:-${ANTHROPIC_MODEL:-}}"
343
+ local input_tokens="${CLAUDE_USAGE_INPUT_TOKENS:-}"
344
+ local output_tokens="${CLAUDE_USAGE_OUTPUT_TOKENS:-}"
345
+ local cache_read="${CLAUDE_USAGE_CACHE_READ_TOKENS:-}"
346
+ local cache_create="${CLAUDE_USAGE_CACHE_CREATE_TOKENS:-}"
347
+ local service_tier="${CLAUDE_SERVICE_TIER:-}"
348
+ local session_id="${CLAUDE_SESSION_ID:-${BROWSER_SKILL_SESSION_ID:-}}"
349
+
350
+ # Build event JSON via jq for safe escaping (never bash-interpolate JSON strings).
351
+ local event
352
+ event=$(jq -nc \
353
+ --argjson schema_version "${STATS_SCHEMA_VERSION}" \
354
+ --arg ts "${ts}" \
355
+ --arg span_id "${span_id}" \
356
+ --arg trace_id "${trace_id}" \
357
+ --arg parent_span_id "${BROWSER_SKILL_PARENT_SPAN_ID:-}" \
358
+ --arg session_id "${session_id}" \
359
+ --arg verb "${verb}" \
360
+ --arg adapter_route "${route}" \
361
+ --arg gen_ai_tool_name "${route}.${verb}" \
362
+ --arg site "${site}" \
363
+ --arg selector_kind "${STATS_SEL_KIND}" \
364
+ --arg selector_value "${STATS_SEL_VALUE}" \
365
+ --argjson duration_ms "${duration_ms}" \
366
+ --argjson argv_bytes "${argv_bytes:-0}" \
367
+ --argjson stdout_bytes "${stdout_bytes:-0}" \
368
+ --argjson stderr_bytes "${stderr_bytes:-0}" \
369
+ --argjson rc "${rc}" \
370
+ --arg outcome "${outcome}" \
371
+ --arg failure_mode "${failure_mode}" \
372
+ --arg model "${model}" \
373
+ --arg service_tier "${service_tier}" \
374
+ --arg input_tokens "${input_tokens}" \
375
+ --arg output_tokens "${output_tokens}" \
376
+ --arg cache_read "${cache_read}" \
377
+ --arg cache_create "${cache_create}" \
378
+ --arg exp_type "${exp_type}" \
379
+ --arg exp_match "${exp_match}" \
380
+ --arg exp_value "${exp_value}" \
381
+ --arg observed "${observed}" \
382
+ --argjson postcond_hit "${postcond_hit_json}" '
383
+ {
384
+ schema_version: $schema_version,
385
+ ts: $ts,
386
+ span_id: $span_id,
387
+ trace_id: $trace_id,
388
+ parent_span_id: ($parent_span_id | select(. != "") // null),
389
+ session_id: ($session_id | select(. != "") // null),
390
+ gen_ai_operation_name: "execute_tool",
391
+ gen_ai_tool_name: $gen_ai_tool_name,
392
+ gen_ai_tool_type: "function",
393
+ verb: $verb,
394
+ adapter_route: $adapter_route,
395
+ site: ($site | select(. != "") // null),
396
+ selector_kind: $selector_kind,
397
+ selector_value: ($selector_value | select(. != "") // null),
398
+ duration_ms: $duration_ms,
399
+ argv_bytes: $argv_bytes,
400
+ stdout_bytes: $stdout_bytes,
401
+ stderr_bytes: $stderr_bytes,
402
+ rc: $rc,
403
+ outcome: $outcome,
404
+ failure_mode: ($failure_mode | select(. != "") // null),
405
+ model: ($model | select(. != "") // null),
406
+ service_tier: ($service_tier | select(. != "") // null),
407
+ gen_ai_usage_input_tokens: (if $input_tokens == "" then null else ($input_tokens | tonumber) end),
408
+ gen_ai_usage_output_tokens: (if $output_tokens == "" then null else ($output_tokens | tonumber) end),
409
+ gen_ai_usage_cache_read_input_tokens: (if $cache_read == "" then null else ($cache_read | tonumber) end),
410
+ gen_ai_usage_cache_creation_input_tokens: (if $cache_create == "" then null else ($cache_create | tonumber) end),
411
+ post_condition_target_type: ($exp_type | select(. != "") // null),
412
+ post_condition_matcher: ($exp_match | select(. != "") // null),
413
+ post_condition_expected: ($exp_value | select(. != "") // null),
414
+ post_condition_observed: ($observed | select(. != "") // null),
415
+ post_condition_hit: $postcond_hit
416
+ }')
417
+
418
+ stats_emit_event "${event}"
419
+ }
File without changes