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.
- package/LICENSE +21 -0
- package/README.md +144 -0
- package/SECURITY.md +39 -0
- package/SKILL.md +206 -0
- package/bin/cli.mjs +55 -0
- package/install.sh +143 -0
- package/package.json +54 -0
- package/references/adapter-candidates.md +40 -0
- package/references/browser-mcp-cheatsheet.md +132 -0
- package/references/browser-stats-cheatsheet.md +155 -0
- package/references/chrome-devtools-mcp-cheatsheet.md +232 -0
- package/references/midscene-integration.md +359 -0
- package/references/obscura-cheatsheet.md +103 -0
- package/references/playwright-cli-cheatsheet.md +64 -0
- package/references/playwright-lib-cheatsheet.md +90 -0
- package/references/recipes/add-a-tool-adapter.md +134 -0
- package/references/recipes/agent-workflows/README.md +37 -0
- package/references/recipes/agent-workflows/cache-driven-bulk-operation.md +110 -0
- package/references/recipes/agent-workflows/flow-record-and-replay.md +102 -0
- package/references/recipes/agent-workflows/incremental-pattern-discovery.md +125 -0
- package/references/recipes/agent-workflows/login-then-scrape.md +100 -0
- package/references/recipes/anti-patterns-tool-extension.md +182 -0
- package/references/recipes/body-bytes-not-body.md +139 -0
- package/references/recipes/cache-write-security.md +210 -0
- package/references/recipes/fingerprint-rescue.md +154 -0
- package/references/recipes/model-routing.md +143 -0
- package/references/recipes/path-security.md +138 -0
- package/references/recipes/privacy-canary.md +96 -0
- package/references/recipes/visual-rescue-hook.md +182 -0
- package/references/stats-prices.json +42 -0
- package/references/stats-schema.json +77 -0
- package/references/tool-versions.md +8 -0
- package/scripts/browser-add-site.sh +113 -0
- package/scripts/browser-assert.sh +106 -0
- package/scripts/browser-audit.sh +68 -0
- package/scripts/browser-baseline.sh +135 -0
- package/scripts/browser-click.sh +100 -0
- package/scripts/browser-creds-add.sh +254 -0
- package/scripts/browser-creds-list.sh +67 -0
- package/scripts/browser-creds-migrate.sh +122 -0
- package/scripts/browser-creds-remove.sh +69 -0
- package/scripts/browser-creds-rotate-totp.sh +109 -0
- package/scripts/browser-creds-show.sh +82 -0
- package/scripts/browser-creds-totp.sh +94 -0
- package/scripts/browser-do.sh +630 -0
- package/scripts/browser-doctor.sh +365 -0
- package/scripts/browser-drag.sh +90 -0
- package/scripts/browser-extract.sh +192 -0
- package/scripts/browser-fill.sh +142 -0
- package/scripts/browser-flow.sh +316 -0
- package/scripts/browser-history.sh +187 -0
- package/scripts/browser-hover.sh +92 -0
- package/scripts/browser-inspect.sh +188 -0
- package/scripts/browser-list-sessions.sh +78 -0
- package/scripts/browser-list-sites.sh +42 -0
- package/scripts/browser-login.sh +279 -0
- package/scripts/browser-mcp.sh +65 -0
- package/scripts/browser-migrate.sh +195 -0
- package/scripts/browser-open.sh +134 -0
- package/scripts/browser-press.sh +80 -0
- package/scripts/browser-remove-session.sh +72 -0
- package/scripts/browser-remove-site.sh +68 -0
- package/scripts/browser-replay.sh +206 -0
- package/scripts/browser-route.sh +174 -0
- package/scripts/browser-select.sh +122 -0
- package/scripts/browser-show-session.sh +57 -0
- package/scripts/browser-show-site.sh +37 -0
- package/scripts/browser-snapshot.sh +176 -0
- package/scripts/browser-stats.sh +522 -0
- package/scripts/browser-tab-close.sh +112 -0
- package/scripts/browser-tab-list.sh +70 -0
- package/scripts/browser-tab-switch.sh +111 -0
- package/scripts/browser-upload.sh +132 -0
- package/scripts/browser-use.sh +60 -0
- package/scripts/browser-vlm.sh +707 -0
- package/scripts/browser-wait.sh +97 -0
- package/scripts/install-git-hooks.sh +16 -0
- package/scripts/lib/capture.sh +356 -0
- package/scripts/lib/common.sh +262 -0
- package/scripts/lib/credential.sh +237 -0
- package/scripts/lib/fingerprint-rescue.js +123 -0
- package/scripts/lib/flow.sh +448 -0
- package/scripts/lib/flow_record.sh +210 -0
- package/scripts/lib/mask.sh +49 -0
- package/scripts/lib/memory.sh +427 -0
- package/scripts/lib/migrate.sh +390 -0
- package/scripts/lib/migrators/README.md +23 -0
- package/scripts/lib/migrators/memory/v1_to_v2.sh +15 -0
- package/scripts/lib/migrators/recent_urls/README.md +13 -0
- package/scripts/lib/migrators/stats/README.md +24 -0
- package/scripts/lib/node/chrome-devtools-bridge.mjs +1812 -0
- package/scripts/lib/node/mcp-server.mjs +531 -0
- package/scripts/lib/node/mcp-tools.json +68 -0
- package/scripts/lib/node/playwright-driver.mjs +1104 -0
- package/scripts/lib/node/totp-core.mjs +52 -0
- package/scripts/lib/node/totp.mjs +52 -0
- package/scripts/lib/node/url-pattern-cluster.mjs +102 -0
- package/scripts/lib/node/url-pattern-resolver.mjs +77 -0
- package/scripts/lib/output.sh +79 -0
- package/scripts/lib/router.sh +342 -0
- package/scripts/lib/sanitize.sh +107 -0
- package/scripts/lib/secret/keychain.sh +91 -0
- package/scripts/lib/secret/libsecret.sh +74 -0
- package/scripts/lib/secret/plaintext.sh +75 -0
- package/scripts/lib/secret_backend_select.sh +57 -0
- package/scripts/lib/session.sh +153 -0
- package/scripts/lib/site.sh +126 -0
- package/scripts/lib/stats.sh +419 -0
- package/scripts/lib/tool/.gitkeep +0 -0
- package/scripts/lib/tool/chrome-devtools-mcp.sh +349 -0
- package/scripts/lib/tool/obscura.sh +249 -0
- package/scripts/lib/tool/playwright-cli.sh +155 -0
- package/scripts/lib/tool/playwright-lib.sh +106 -0
- package/scripts/lib/verb_helpers.sh +222 -0
- package/scripts/lib/visual-rescue-default.sh +145 -0
- package/scripts/regenerate-docs.sh +99 -0
- package/uninstall.sh +51 -0
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/browser-do.sh
|
|
3
|
+
# Phase 11 part 1-ii — memory-aware verb. Two sub-modes:
|
|
4
|
+
# browser-do --verb VERB --intent "..." — cache lookup; dispatch on hit; emit cache_miss event on miss
|
|
5
|
+
# browser-do record --intent --selector --url — explicit write-back through lib/memory.sh
|
|
6
|
+
#
|
|
7
|
+
# Skill stays model-agnostic: on miss, parent agent picks ref via its own
|
|
8
|
+
# snapshot+reasoning, then explicitly calls `record`. No LLM call here.
|
|
9
|
+
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
IFS=$'\n\t'
|
|
12
|
+
umask 077
|
|
13
|
+
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
15
|
+
SCRIPTS_DIR="${SCRIPT_DIR}"
|
|
16
|
+
export SCRIPTS_DIR
|
|
17
|
+
|
|
18
|
+
# shellcheck source=lib/common.sh
|
|
19
|
+
# shellcheck disable=SC1091
|
|
20
|
+
source "${SCRIPT_DIR}/lib/common.sh"
|
|
21
|
+
# shellcheck source=lib/site.sh
|
|
22
|
+
# shellcheck disable=SC1091
|
|
23
|
+
source "${SCRIPT_DIR}/lib/site.sh"
|
|
24
|
+
# shellcheck source=lib/memory.sh
|
|
25
|
+
# shellcheck disable=SC1091
|
|
26
|
+
source "${SCRIPT_DIR}/lib/memory.sh"
|
|
27
|
+
# shellcheck source=lib/stats.sh
|
|
28
|
+
# shellcheck disable=SC1091
|
|
29
|
+
source "${SCRIPT_DIR}/lib/stats.sh"
|
|
30
|
+
|
|
31
|
+
init_paths
|
|
32
|
+
|
|
33
|
+
SUMMARY_T0="$(now_ms)"
|
|
34
|
+
|
|
35
|
+
# --- Whitelist: verbs that accept --selector as primary target ---
|
|
36
|
+
# Whitelist grows as adapter ABI gains selector-mode plumbing per verb.
|
|
37
|
+
# fill: PR #99. hover: PR #101. select: PR #103. press: deferred (bridge
|
|
38
|
+
# `case 'press':` is target-less by design — would need new "focus + press"
|
|
39
|
+
# semantics; tracked in HANDOFF as separate decision).
|
|
40
|
+
readonly DO_VERB_WHITELIST=(click fill hover select)
|
|
41
|
+
|
|
42
|
+
_verb_in_whitelist() {
|
|
43
|
+
local needle="$1" v
|
|
44
|
+
for v in "${DO_VERB_WHITELIST[@]}"; do
|
|
45
|
+
[ "${v}" = "${needle}" ] && return 0
|
|
46
|
+
done
|
|
47
|
+
return 1
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# --- Privacy canary ---
|
|
51
|
+
# Refuse cache writes containing the literal sentinel. Backs the recipe-
|
|
52
|
+
# pattern privacy-canary tests; not a real secret-detector. Real entropy
|
|
53
|
+
# scanning is a future hardening pass.
|
|
54
|
+
readonly CANARY_SENTINEL='PASSWORD-CANARY'
|
|
55
|
+
|
|
56
|
+
# --- Phase 11 v2 part 1: observation log (events.jsonl) writer ---
|
|
57
|
+
# Tee per-invocation cache-hit/miss observations into a JSONL log under
|
|
58
|
+
# ${BROWSER_SKILL_HOME}/memory/events.jsonl. Doctor's read side (PR #113)
|
|
59
|
+
# consumes the .cache_hit field to report a real hit-rate.
|
|
60
|
+
#
|
|
61
|
+
# Shape: each line is a JSON object with at least .cache_hit (bool) + .ts
|
|
62
|
+
# (ISO 8601). Optional fields: .site, .archetype_id, .reason, .dispatched_verb,
|
|
63
|
+
# .dispatch_rc. Intent strings are NEVER logged — user input could leak.
|
|
64
|
+
# Doctor only needs .cache_hit; everything else is best-effort context.
|
|
65
|
+
#
|
|
66
|
+
# Best-effort writer. Failure must NOT taint the verb's exit code; emits a
|
|
67
|
+
# warn: line and continues. Same contract as memory_record write-back failures
|
|
68
|
+
# in the existing dispatch path.
|
|
69
|
+
#
|
|
70
|
+
# Append-only. File created mode 0600; parent dir mode 0700.
|
|
71
|
+
#
|
|
72
|
+
# Signature: _record_event JSON_STRING
|
|
73
|
+
# Caller builds the per-event JSON (each call site has different fields);
|
|
74
|
+
# this helper adds {ts, verb, mode} envelope and appends.
|
|
75
|
+
_record_event() {
|
|
76
|
+
local payload="$1"
|
|
77
|
+
local events_dir events_file ts line
|
|
78
|
+
events_dir="${BROWSER_SKILL_HOME}/memory"
|
|
79
|
+
events_file="${events_dir}/events.jsonl"
|
|
80
|
+
ts="$(now_iso)"
|
|
81
|
+
|
|
82
|
+
if ! mkdir -p "${events_dir}" 2>/dev/null; then
|
|
83
|
+
warn "browser-do: could not create memory dir; observation log skipped"
|
|
84
|
+
return 0
|
|
85
|
+
fi
|
|
86
|
+
chmod 700 "${events_dir}" 2>/dev/null || true
|
|
87
|
+
|
|
88
|
+
if ! line="$(printf '%s' "${payload}" | jq -c --arg ts "${ts}" \
|
|
89
|
+
'. + {ts:$ts, verb:"do", mode:"intent"}' 2>/dev/null)"; then
|
|
90
|
+
warn "browser-do: events.jsonl encode failed (best-effort; action exit unchanged)"
|
|
91
|
+
return 0
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
# O_APPEND on POSIX is atomic for writes under PIPE_BUF (4KB); jsonl lines
|
|
95
|
+
# are well below that, so concurrent appenders interleave by line, not by
|
|
96
|
+
# character. Same pattern as standard audit-log writers.
|
|
97
|
+
if ! printf '%s\n' "${line}" >> "${events_file}" 2>/dev/null; then
|
|
98
|
+
warn "browser-do: events.jsonl append failed (best-effort; action exit unchanged)"
|
|
99
|
+
return 0
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
# First write may have raced umask; chmod is idempotent thereafter.
|
|
103
|
+
chmod 600 "${events_file}" 2>/dev/null || true
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_canary_check() {
|
|
107
|
+
local field="$1" value="$2"
|
|
108
|
+
if printf '%s' "${value}" | grep -qF -- "${CANARY_SENTINEL}"; then
|
|
109
|
+
die "${EXIT_BLOCKLIST_REJECTED}" "browser-do: refused — ${field} contains canary sentinel '${CANARY_SENTINEL}' (privacy guard; never put credential bytes in cache args)"
|
|
110
|
+
fi
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# --- URL → pattern derivation ---
|
|
114
|
+
# Replace numeric path segments with `:id`. UUID/slug detection deferred
|
|
115
|
+
# to 11-2-ii. Caller can always pass --pattern to override.
|
|
116
|
+
_derive_pattern_from_url() {
|
|
117
|
+
local url="$1" pathname
|
|
118
|
+
pathname="$(node -e "process.stdout.write(new URL(process.argv[1], 'https://x').pathname)" "${url}" 2>/dev/null)" \
|
|
119
|
+
|| die "${EXIT_USAGE_ERROR}" "browser-do: invalid --url '${url}'"
|
|
120
|
+
# /[digits] → /:id (one or more digit segments anywhere in pathname).
|
|
121
|
+
printf '%s' "${pathname}" | sed -E 's@/[0-9]+@/:id@g'
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# --- Pattern → archetype_id derivation ---
|
|
125
|
+
# Strip leading '/', drop ':' chars, replace '/' with '-', lowercase.
|
|
126
|
+
# Constrained to assert_safe_name's regex so memory_save_archetype accepts it.
|
|
127
|
+
_derive_archetype_id() {
|
|
128
|
+
local pattern="$1"
|
|
129
|
+
printf '%s' "${pattern#/}" \
|
|
130
|
+
| sed -E 's|:||g; s|/|-|g; s|[^A-Za-z0-9_-]|_|g' \
|
|
131
|
+
| tr '[:upper:]' '[:lower:]'
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# --- Site resolution ---
|
|
135
|
+
# --site flag wins; falls back to current_get; empty → die USAGE.
|
|
136
|
+
_resolve_site() {
|
|
137
|
+
local arg="$1"
|
|
138
|
+
if [ -n "${arg}" ]; then
|
|
139
|
+
printf '%s' "${arg}"
|
|
140
|
+
return 0
|
|
141
|
+
fi
|
|
142
|
+
local cur
|
|
143
|
+
cur="$(current_get || true)"
|
|
144
|
+
if [ -z "${cur}" ]; then
|
|
145
|
+
die "${EXIT_USAGE_ERROR}" "browser-do: --site not given and no current site set (use 'browser-use --set NAME' or pass --site)"
|
|
146
|
+
fi
|
|
147
|
+
printf '%s' "${cur}"
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# --- usage ---
|
|
151
|
+
usage() {
|
|
152
|
+
cat <<'USAGE'
|
|
153
|
+
Usage:
|
|
154
|
+
browser-do --verb VERB --intent "..." [--site NAME] [--url URL] [-- VERB_ARG ...]
|
|
155
|
+
browser-do record --intent "..." --selector "..." --url "..." [--site NAME] [--pattern PAT] [--archetype NAME]
|
|
156
|
+
|
|
157
|
+
Verbs (whitelist): click | fill | hover | press | select
|
|
158
|
+
USAGE
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# --- Sub-mode dispatch ---
|
|
162
|
+
sub_mode="${1:-}"
|
|
163
|
+
[ -n "${sub_mode}" ] || { usage >&2; die "${EXIT_USAGE_ERROR}" "browser-do: missing sub-mode or flag"; }
|
|
164
|
+
|
|
165
|
+
# `record` and `propose` are literal sub-modes; otherwise we're in --intent
|
|
166
|
+
# mode (flags handled below).
|
|
167
|
+
case "${sub_mode}" in
|
|
168
|
+
record|propose) shift ;;
|
|
169
|
+
-h|--help) usage; exit 0 ;;
|
|
170
|
+
--*) sub_mode="intent" ;;
|
|
171
|
+
*) die "${EXIT_USAGE_ERROR}" "browser-do: unknown sub-mode '${sub_mode}' (expected 'record', 'propose', or --intent flag)" ;;
|
|
172
|
+
esac
|
|
173
|
+
|
|
174
|
+
# ---------- record sub-mode ----------
|
|
175
|
+
if [ "${sub_mode}" = "record" ]; then
|
|
176
|
+
arg_site="" arg_intent="" arg_selector="" arg_url="" arg_pattern="" arg_archetype=""
|
|
177
|
+
while [ "$#" -gt 0 ]; do
|
|
178
|
+
case "$1" in
|
|
179
|
+
--site) arg_site="$2"; shift 2 ;;
|
|
180
|
+
--intent) arg_intent="$2"; shift 2 ;;
|
|
181
|
+
--selector) arg_selector="$2"; shift 2 ;;
|
|
182
|
+
--url) arg_url="$2"; shift 2 ;;
|
|
183
|
+
--pattern) arg_pattern="$2"; shift 2 ;;
|
|
184
|
+
--archetype) arg_archetype="$2"; shift 2 ;;
|
|
185
|
+
-h|--help) usage; exit 0 ;;
|
|
186
|
+
*) die "${EXIT_USAGE_ERROR}" "browser-do record: unknown flag '$1'" ;;
|
|
187
|
+
esac
|
|
188
|
+
done
|
|
189
|
+
[ -n "${arg_intent}" ] || die "${EXIT_USAGE_ERROR}" "browser-do record: --intent required"
|
|
190
|
+
[ -n "${arg_selector}" ] || die "${EXIT_USAGE_ERROR}" "browser-do record: --selector required"
|
|
191
|
+
[ -n "${arg_url}" ] || die "${EXIT_USAGE_ERROR}" "browser-do record: --url required"
|
|
192
|
+
|
|
193
|
+
# Privacy canary first — refuse before touching disk.
|
|
194
|
+
_canary_check "intent" "${arg_intent}"
|
|
195
|
+
_canary_check "selector" "${arg_selector}"
|
|
196
|
+
|
|
197
|
+
site="$(_resolve_site "${arg_site}")"
|
|
198
|
+
|
|
199
|
+
pattern="${arg_pattern}"
|
|
200
|
+
if [ -z "${pattern}" ]; then
|
|
201
|
+
pattern="$(_derive_pattern_from_url "${arg_url}")"
|
|
202
|
+
fi
|
|
203
|
+
|
|
204
|
+
archetype_id="${arg_archetype}"
|
|
205
|
+
if [ -z "${archetype_id}" ]; then
|
|
206
|
+
archetype_id="$(_derive_archetype_id "${pattern}")"
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
# Ensure archetype JSON exists (lazy-init empty shell so memory_record can
|
|
210
|
+
# upsert into it).
|
|
211
|
+
arch_path="${BROWSER_SKILL_HOME}/memory/${site}/archetypes/${archetype_id}.json"
|
|
212
|
+
if [ ! -f "${arch_path}" ]; then
|
|
213
|
+
init_json="$(jq -nc --arg id "${archetype_id}" --arg p "${pattern}" --arg now "$(now_iso)" \
|
|
214
|
+
'{schema_version:1, archetype_id:$id, url_pattern:$p,
|
|
215
|
+
first_seen:$now, last_seen:$now, use_count:0, interactions:[]}')"
|
|
216
|
+
memory_save_archetype "${site}" "${archetype_id}" "${init_json}"
|
|
217
|
+
fi
|
|
218
|
+
|
|
219
|
+
memory_record_pattern "${site}" "${pattern}" "${archetype_id}"
|
|
220
|
+
memory_record "${site}" "${archetype_id}" "${arg_intent}" "${arg_selector}"
|
|
221
|
+
|
|
222
|
+
printf '%s\n' "$(jq -nc --arg site "${site}" --arg arch "${archetype_id}" \
|
|
223
|
+
--arg pat "${pattern}" --arg int "${arg_intent}" \
|
|
224
|
+
'{_kind:"record_ok", site:$site, archetype_id:$arch, url_pattern:$pat, intent:$int}')"
|
|
225
|
+
duration_ms=$(( $(now_ms) - SUMMARY_T0 ))
|
|
226
|
+
summary_json verb=do mode=record site="${site}" archetype_id="${archetype_id}" \
|
|
227
|
+
url_pattern="${pattern}" duration_ms="${duration_ms}" status=ok
|
|
228
|
+
exit 0
|
|
229
|
+
fi
|
|
230
|
+
|
|
231
|
+
# ---------- propose sub-mode (Phase 11 part 2-ii) ----------
|
|
232
|
+
# Pure-compute. Reads URLs from --url args + stdin; clusters by templated
|
|
233
|
+
# pathname (numeric → :id, UUID → :uuid); emits _kind:proposal events for
|
|
234
|
+
# clusters meeting threshold AND not already in patterns.json.
|
|
235
|
+
if [ "${sub_mode}" = "propose" ]; then
|
|
236
|
+
arg_site="" arg_threshold="3" arg_auto_record="false" arg_from_recent="false"
|
|
237
|
+
cli_urls=()
|
|
238
|
+
while [ "$#" -gt 0 ]; do
|
|
239
|
+
case "$1" in
|
|
240
|
+
--site) arg_site="$2"; shift 2 ;;
|
|
241
|
+
--threshold) arg_threshold="$2"; shift 2 ;;
|
|
242
|
+
--url) cli_urls+=("$2"); shift 2 ;;
|
|
243
|
+
--auto-record) arg_auto_record="true"; shift ;;
|
|
244
|
+
--from-recent) arg_from_recent="true"; shift ;;
|
|
245
|
+
-h|--help) usage; exit 0 ;;
|
|
246
|
+
*) die "${EXIT_USAGE_ERROR}" "browser-do propose: unknown flag '$1'" ;;
|
|
247
|
+
esac
|
|
248
|
+
done
|
|
249
|
+
if [[ ! "${arg_threshold}" =~ ^[0-9]+$ ]]; then
|
|
250
|
+
die "${EXIT_USAGE_ERROR}" "browser-do propose: --threshold must be a positive integer (got: ${arg_threshold})"
|
|
251
|
+
fi
|
|
252
|
+
site="$(_resolve_site "${arg_site}")"
|
|
253
|
+
|
|
254
|
+
# Collect URLs from stdin (one per line; skip blank + ^# comments) into
|
|
255
|
+
# the same list. Stdin is non-blocking — if no pipe is connected, read
|
|
256
|
+
# immediately returns. -t 0 is "is stdin a TTY?"; we read stdin only if
|
|
257
|
+
# it's NOT a TTY (i.e. piped or redirected).
|
|
258
|
+
urls=("${cli_urls[@]+"${cli_urls[@]}"}")
|
|
259
|
+
if [ ! -t 0 ]; then
|
|
260
|
+
while IFS= read -r line; do
|
|
261
|
+
[ -z "${line}" ] && continue
|
|
262
|
+
[[ "${line}" =~ ^[[:space:]]*# ]] && continue
|
|
263
|
+
urls+=("${line}")
|
|
264
|
+
done
|
|
265
|
+
fi
|
|
266
|
+
|
|
267
|
+
# Pick A6: --from-recent appends URLs from the navigation observation log
|
|
268
|
+
# filtered to the current site. Absent log → no-op (not an error).
|
|
269
|
+
if [ "${arg_from_recent}" = "true" ]; then
|
|
270
|
+
recent_file="${BROWSER_SKILL_HOME}/memory/recent_urls.jsonl"
|
|
271
|
+
if [ -f "${recent_file}" ]; then
|
|
272
|
+
while IFS= read -r recent_url; do
|
|
273
|
+
[ -z "${recent_url}" ] && continue
|
|
274
|
+
urls+=("${recent_url}")
|
|
275
|
+
done < <(jq -r --arg s "${site}" 'select(.site == $s) | .url' "${recent_file}" 2>/dev/null || true)
|
|
276
|
+
fi
|
|
277
|
+
fi
|
|
278
|
+
|
|
279
|
+
# Build node-helper input + invoke. Empty urls → empty cluster set.
|
|
280
|
+
cluster_helper="$(dirname "${BASH_SOURCE[0]}")/lib/node/url-pattern-cluster.mjs"
|
|
281
|
+
cluster_input="$(jq -nc --argjson u "$(printf '%s\n' "${urls[@]+"${urls[@]}"}" | jq -R . | jq -sc .)" '{urls: $u}')"
|
|
282
|
+
cluster_output="$(printf '%s' "${cluster_input}" | node "${cluster_helper}")"
|
|
283
|
+
|
|
284
|
+
# Load known patterns from patterns.json (if any) into a sorted unique list.
|
|
285
|
+
patterns_path="${BROWSER_SKILL_HOME}/memory/${site}/patterns.json"
|
|
286
|
+
known_json='[]'
|
|
287
|
+
if [ -f "${patterns_path}" ]; then
|
|
288
|
+
known_json="$(jq -c '[.patterns[].url_pattern] // []' "${patterns_path}")"
|
|
289
|
+
fi
|
|
290
|
+
|
|
291
|
+
# Filter clusters: count >= threshold AND templated NOT in known.
|
|
292
|
+
emit_count=0
|
|
293
|
+
skipped_known=0
|
|
294
|
+
auto_recorded=0
|
|
295
|
+
while IFS= read -r cluster_event; do
|
|
296
|
+
[ -z "${cluster_event}" ] && continue
|
|
297
|
+
printf '%s\n' "${cluster_event}"
|
|
298
|
+
emit_count=$(( emit_count + 1 ))
|
|
299
|
+
# Pick A3: when --auto-record, persist the (url_pattern, archetype_id)
|
|
300
|
+
# pair via memory_record_pattern. The proposal stream is already filtered
|
|
301
|
+
# to "not already in patterns.json", so each emit is a fresh pattern.
|
|
302
|
+
# memory_record_pattern is idempotent (per its docstring); best-effort
|
|
303
|
+
# write — failure emits warn: and continues; never taints exit code.
|
|
304
|
+
if [ "${arg_auto_record}" = "true" ]; then
|
|
305
|
+
_ar_url_pattern="$(printf '%s' "${cluster_event}" | jq -r '.url_pattern')"
|
|
306
|
+
_ar_arch_id="$(printf '%s' "${cluster_event}" | jq -r '.archetype_id')"
|
|
307
|
+
if memory_record_pattern "${site}" "${_ar_url_pattern}" "${_ar_arch_id}" 2>/dev/null; then
|
|
308
|
+
auto_recorded=$(( auto_recorded + 1 ))
|
|
309
|
+
else
|
|
310
|
+
warn "browser-do propose: auto-record failed for pattern '${_ar_url_pattern}' (best-effort)"
|
|
311
|
+
fi
|
|
312
|
+
fi
|
|
313
|
+
done < <(printf '%s' "${cluster_output}" | jq -c \
|
|
314
|
+
--argjson threshold "${arg_threshold}" \
|
|
315
|
+
--argjson known "${known_json}" \
|
|
316
|
+
--arg site "${site}" \
|
|
317
|
+
'# Pick A4: canonicalize both the cluster pattern and each known pattern
|
|
318
|
+
# before compare so /devices/:id matches an already-known /devices/:itemId.
|
|
319
|
+
# Original cluster .templated is preserved on emit (only the compare uses
|
|
320
|
+
# the canonical form).
|
|
321
|
+
def _canonical: gsub(":[A-Za-z_][A-Za-z0-9_]*"; ":_");
|
|
322
|
+
($known | map(_canonical)) as $known_canon
|
|
323
|
+
| .clusters
|
|
324
|
+
| map(select(.count >= $threshold and ([(.templated | _canonical)] | inside($known_canon) | not)))
|
|
325
|
+
| .[] |
|
|
326
|
+
{_kind:"proposal", site:$site,
|
|
327
|
+
url_pattern:.templated,
|
|
328
|
+
archetype_id:(.templated
|
|
329
|
+
| sub("^/"; "")
|
|
330
|
+
| gsub(":"; "")
|
|
331
|
+
| gsub("/"; "-")
|
|
332
|
+
| gsub("[^A-Za-z0-9_-]"; "_")
|
|
333
|
+
| ascii_downcase),
|
|
334
|
+
sample_urls:(.urls[0:3]),
|
|
335
|
+
count:.count}')
|
|
336
|
+
|
|
337
|
+
# Count clusters skipped due to "already in patterns.json" (canonical match).
|
|
338
|
+
skipped_known="$(printf '%s' "${cluster_output}" | jq -r \
|
|
339
|
+
--argjson threshold "${arg_threshold}" \
|
|
340
|
+
--argjson known "${known_json}" \
|
|
341
|
+
'def _canonical: gsub(":[A-Za-z_][A-Za-z0-9_]*"; ":_");
|
|
342
|
+
($known | map(_canonical)) as $known_canon
|
|
343
|
+
| .clusters | map(select(.count >= $threshold and ([(.templated | _canonical)] | inside($known_canon)))) | length')"
|
|
344
|
+
|
|
345
|
+
duration_ms=$(( $(now_ms) - SUMMARY_T0 ))
|
|
346
|
+
summary_json verb=do mode=propose site="${site}" \
|
|
347
|
+
proposals="${emit_count}" skipped_known="${skipped_known}" \
|
|
348
|
+
auto_recorded="${auto_recorded}" \
|
|
349
|
+
threshold="${arg_threshold}" url_count="${#urls[@]}" \
|
|
350
|
+
duration_ms="${duration_ms}" status=ok
|
|
351
|
+
exit 0
|
|
352
|
+
fi
|
|
353
|
+
|
|
354
|
+
# ---------- intent sub-mode ----------
|
|
355
|
+
arg_site="" arg_verb="" arg_intent="" arg_url="" arg_pattern="" arg_archetype=""
|
|
356
|
+
extra_args=()
|
|
357
|
+
while [ "$#" -gt 0 ]; do
|
|
358
|
+
case "$1" in
|
|
359
|
+
--site) arg_site="$2"; shift 2 ;;
|
|
360
|
+
--verb) arg_verb="$2"; shift 2 ;;
|
|
361
|
+
--intent) arg_intent="$2"; shift 2 ;;
|
|
362
|
+
--url) arg_url="$2"; shift 2 ;;
|
|
363
|
+
--pattern) arg_pattern="$2"; shift 2 ;;
|
|
364
|
+
--archetype) arg_archetype="$2"; shift 2 ;;
|
|
365
|
+
--) shift; extra_args=("$@"); break ;;
|
|
366
|
+
-h|--help) usage; exit 0 ;;
|
|
367
|
+
*) die "${EXIT_USAGE_ERROR}" "browser-do --intent: unknown flag '$1'" ;;
|
|
368
|
+
esac
|
|
369
|
+
done
|
|
370
|
+
[ -n "${arg_verb}" ] || die "${EXIT_USAGE_ERROR}" "browser-do: --verb required"
|
|
371
|
+
[ -n "${arg_intent}" ] || die "${EXIT_USAGE_ERROR}" "browser-do: --intent required"
|
|
372
|
+
_verb_in_whitelist "${arg_verb}" \
|
|
373
|
+
|| die "${EXIT_USAGE_ERROR}" "browser-do: --verb '${arg_verb}' not in whitelist (allowed: ${DO_VERB_WHITELIST[*]})"
|
|
374
|
+
|
|
375
|
+
site="$(_resolve_site "${arg_site}")"
|
|
376
|
+
|
|
377
|
+
# Resolve archetype with most-explicit-wins priority (Phase 11 part 2-i R1):
|
|
378
|
+
# 1. --archetype NAME — direct; skip URL lookup + pattern derivation.
|
|
379
|
+
# 2. --pattern PAT — derive archetype-id via _derive_archetype_id.
|
|
380
|
+
# 3. --url URL — memory_resolve_archetype (Phase 11 part 1-ii path).
|
|
381
|
+
# Empty archetype after this block → cache_miss reason:no_pattern_for_url.
|
|
382
|
+
archetype_id=""
|
|
383
|
+
if [ -n "${arg_archetype}" ]; then
|
|
384
|
+
assert_safe_name "${arg_archetype}" "archetype-id"
|
|
385
|
+
archetype_id="${arg_archetype}"
|
|
386
|
+
elif [ -n "${arg_pattern}" ]; then
|
|
387
|
+
archetype_id="$(_derive_archetype_id "${arg_pattern}")"
|
|
388
|
+
elif [ -n "${arg_url}" ]; then
|
|
389
|
+
archetype_id="$(memory_resolve_archetype "${site}" "${arg_url}" 2>/dev/null || true)"
|
|
390
|
+
fi
|
|
391
|
+
|
|
392
|
+
if [ -z "${archetype_id}" ]; then
|
|
393
|
+
printf '%s\n' "$(jq -nc --arg int "${arg_intent}" --arg site "${site}" \
|
|
394
|
+
--arg url "${arg_url}" \
|
|
395
|
+
'{_kind:"cache_miss", intent:$int, site:$site, url:$url,
|
|
396
|
+
archetype_id:null, reason:"no_pattern_for_url",
|
|
397
|
+
suggestion:"snapshot+pick+record"}')"
|
|
398
|
+
duration_ms=$(( $(now_ms) - SUMMARY_T0 ))
|
|
399
|
+
_record_event "$(jq -nc --arg site "${site}" \
|
|
400
|
+
'{cache_hit:false, site:$site, reason:"no_pattern_for_url"}')"
|
|
401
|
+
summary_json verb=do mode=intent cache_hit=false reason=no_pattern_for_url \
|
|
402
|
+
site="${site}" duration_ms="${duration_ms}" status=miss
|
|
403
|
+
exit "${EXIT_EMPTY_RESULT}"
|
|
404
|
+
fi
|
|
405
|
+
|
|
406
|
+
selector="$(memory_lookup "${site}" "${archetype_id}" "${arg_intent}" 2>/dev/null || true)"
|
|
407
|
+
|
|
408
|
+
if [ -z "${selector}" ]; then
|
|
409
|
+
printf '%s\n' "$(jq -nc --arg int "${arg_intent}" --arg site "${site}" \
|
|
410
|
+
--arg url "${arg_url}" --arg arch "${archetype_id}" \
|
|
411
|
+
'{_kind:"cache_miss", intent:$int, site:$site, url:$url,
|
|
412
|
+
archetype_id:$arch, reason:"intent_not_cached",
|
|
413
|
+
suggestion:"snapshot+pick+record"}')"
|
|
414
|
+
duration_ms=$(( $(now_ms) - SUMMARY_T0 ))
|
|
415
|
+
_record_event "$(jq -nc --arg site "${site}" --arg arch "${archetype_id}" \
|
|
416
|
+
'{cache_hit:false, site:$site, archetype_id:$arch, reason:"intent_not_cached"}')"
|
|
417
|
+
summary_json verb=do mode=intent cache_hit=false reason=intent_not_cached \
|
|
418
|
+
site="${site}" archetype_id="${archetype_id}" duration_ms="${duration_ms}" status=miss
|
|
419
|
+
exit "${EXIT_EMPTY_RESULT}"
|
|
420
|
+
fi
|
|
421
|
+
|
|
422
|
+
# Cache hit — dispatch via existing verb script. --selector prepended; extra
|
|
423
|
+
# args forwarded verbatim. Forward stdin/stdout/stderr; the dispatched verb
|
|
424
|
+
# emits its own summary; ours follows.
|
|
425
|
+
#
|
|
426
|
+
# BROWSER_DO_DISPATCH_OVERRIDE (test-only env hook): if set, the value is
|
|
427
|
+
# treated as the dispatch script path instead of scripts/browser-${verb}.sh.
|
|
428
|
+
# Production callers never set this; it lets bats mock the dispatched verb's
|
|
429
|
+
# exit code so we can test the self-heal failure-counting trigger end-to-end.
|
|
430
|
+
verb_script="${BROWSER_DO_DISPATCH_OVERRIDE:-${SCRIPT_DIR}/browser-${arg_verb}.sh}"
|
|
431
|
+
[ -x "${verb_script}" ] || [ -f "${verb_script}" ] \
|
|
432
|
+
|| die "${EXIT_TOOL_MISSING}" "browser-do: dispatch target not found: ${verb_script}"
|
|
433
|
+
|
|
434
|
+
printf '%s\n' "$(jq -nc --arg int "${arg_intent}" --arg sel "${selector}" \
|
|
435
|
+
--arg arch "${archetype_id}" --arg site "${site}" \
|
|
436
|
+
'{_kind:"cache_hit", intent:$int, selector:$sel, archetype_id:$arch, site:$site}')"
|
|
437
|
+
|
|
438
|
+
# Run the verb. Capture its exit; forward unchanged. Best-effort cache update
|
|
439
|
+
# on success only — write-back failure must NOT taint the verb's exit code.
|
|
440
|
+
dispatch_rc=0
|
|
441
|
+
bash "${verb_script}" --selector "${selector}" "${extra_args[@]+"${extra_args[@]}"}" || dispatch_rc=$?
|
|
442
|
+
|
|
443
|
+
self_heal_triggered=false
|
|
444
|
+
|
|
445
|
+
if [ "${dispatch_rc}" -eq 0 ]; then
|
|
446
|
+
if ! memory_record "${site}" "${archetype_id}" "${arg_intent}" "${selector}" 2>/dev/null; then
|
|
447
|
+
warn "browser-do: cache success_count update failed (best-effort; action exit unchanged)"
|
|
448
|
+
fi
|
|
449
|
+
if ! memory_record_pattern "${site}" \
|
|
450
|
+
"$(jq -r '.url_pattern' "${BROWSER_SKILL_HOME}/memory/${site}/archetypes/${archetype_id}.json")" \
|
|
451
|
+
"${archetype_id}" 2>/dev/null; then
|
|
452
|
+
warn "browser-do: pattern hit_count update failed (best-effort)"
|
|
453
|
+
fi
|
|
454
|
+
elif [ "${dispatch_rc}" -eq "${EXIT_EMPTY_RESULT}" ] || [ "${dispatch_rc}" -eq "${EXIT_ASSERTION_FAILED}" ]; then
|
|
455
|
+
# Self-heal trigger (Phase 11 1-iii D1): only canonical "selector miss" /
|
|
456
|
+
# "expected element absent" exit codes drive the failure counter. Network
|
|
457
|
+
# errors (30), tool crashes (42), timeouts (43) are environmental — they
|
|
458
|
+
# would poison the cache if we counted them.
|
|
459
|
+
|
|
460
|
+
# Phase 13: weak-fingerprint rescue tier — try BEFORE incrementing fail_count.
|
|
461
|
+
# Algorithm scores DOM candidates by tag + classes + attrs Jaccard, returns
|
|
462
|
+
# a synthesised selector if any candidate scores >= BROWSER_DO_RESCUE_THRESHOLD
|
|
463
|
+
# (default 0.70). If the rescued selector works on retry, the cache silently
|
|
464
|
+
# heals (selector overwritten, fail_count reset, self_heal_history appended).
|
|
465
|
+
rescued_selector=""
|
|
466
|
+
rescued_selector="$(memory_fingerprint_rescue "${site}" "${archetype_id}" \
|
|
467
|
+
"${arg_intent}" "${selector}" 2>/dev/null || printf '')"
|
|
468
|
+
rescued=false
|
|
469
|
+
if [ -n "${rescued_selector}" ] && [ "${rescued_selector}" != "${selector}" ]; then
|
|
470
|
+
# Retry the verb with the rescued selector. Capture rc separately so the
|
|
471
|
+
# original dispatch_rc only flips to 0 when the retry actually succeeds.
|
|
472
|
+
retry_rc=0
|
|
473
|
+
bash "${verb_script}" --selector "${rescued_selector}" "${extra_args[@]+"${extra_args[@]}"}" \
|
|
474
|
+
|| retry_rc=$?
|
|
475
|
+
if [ "${retry_rc}" -eq 0 ]; then
|
|
476
|
+
if memory_record_heal "${site}" "${archetype_id}" "${arg_intent}" \
|
|
477
|
+
"${selector}" "${rescued_selector}" 2>/dev/null; then
|
|
478
|
+
rescued=true
|
|
479
|
+
self_heal_triggered=true
|
|
480
|
+
dispatch_rc=0 # treat as success for the verb's exit-code contract
|
|
481
|
+
printf '%s\n' "$(jq -nc \
|
|
482
|
+
--arg from "${selector}" --arg to "${rescued_selector}" \
|
|
483
|
+
'{_kind:"fingerprint_rescue", from_selector:$from, to_selector:$to,
|
|
484
|
+
rescued:true}')"
|
|
485
|
+
# Phase 13 + 12 audit hook: emit a dedicated stats.jsonl event so
|
|
486
|
+
# `browser-stats report` can compute heal-rate over time. Best-effort.
|
|
487
|
+
_rescue_span_id="$(stats_random_id 2>/dev/null || printf '')"
|
|
488
|
+
_rescue_ts="$(stats_now_iso_ms 2>/dev/null || printf '')"
|
|
489
|
+
if [ -n "${_rescue_span_id}" ] && [ -n "${_rescue_ts}" ]; then
|
|
490
|
+
_rescue_event="$(jq -nc \
|
|
491
|
+
--argjson schema_version 1 \
|
|
492
|
+
--arg ts "${_rescue_ts}" \
|
|
493
|
+
--arg span_id "${_rescue_span_id}" \
|
|
494
|
+
--arg trace_id "${BROWSER_SKILL_TRACE_ID:-${_rescue_span_id}}" \
|
|
495
|
+
--arg verb "do" \
|
|
496
|
+
--arg site "${site}" \
|
|
497
|
+
--arg from "${selector}" \
|
|
498
|
+
--arg to "${rescued_selector}" '
|
|
499
|
+
{ schema_version: $schema_version,
|
|
500
|
+
ts: $ts, span_id: $span_id, trace_id: $trace_id,
|
|
501
|
+
parent_span_id: null, session_id: null,
|
|
502
|
+
gen_ai_operation_name: "execute_tool",
|
|
503
|
+
gen_ai_tool_name: "browser-do.fingerprint_rescue",
|
|
504
|
+
gen_ai_tool_type: "function",
|
|
505
|
+
verb: $verb,
|
|
506
|
+
adapter_route: "browser-do",
|
|
507
|
+
site: ($site | select(. != "") // null),
|
|
508
|
+
selector_kind: "css", selector_value: $from,
|
|
509
|
+
duration_ms: 0, argv_bytes: 0, stdout_bytes: 0, stderr_bytes: 0,
|
|
510
|
+
rc: 0,
|
|
511
|
+
outcome: "success",
|
|
512
|
+
failure_mode: null,
|
|
513
|
+
rescued: true,
|
|
514
|
+
fingerprint_from_selector: $from,
|
|
515
|
+
fingerprint_to_selector: $to
|
|
516
|
+
}' 2>/dev/null || printf '')"
|
|
517
|
+
[ -n "${_rescue_event}" ] && stats_emit_event "${_rescue_event}" 2>/dev/null || true
|
|
518
|
+
fi
|
|
519
|
+
else
|
|
520
|
+
warn "browser-do: rescue retry succeeded but memory_record_heal failed (best-effort)"
|
|
521
|
+
fi
|
|
522
|
+
fi
|
|
523
|
+
fi
|
|
524
|
+
# Phase 14 Path 3: visual rescue via local VLM (extension hook).
|
|
525
|
+
# Inserts BETWEEN fingerprint-rescue failure and the fail_count++ path.
|
|
526
|
+
# Both env vars must be set:
|
|
527
|
+
# BROWSER_SKILL_VISION_FALLBACK=1 enable the tier
|
|
528
|
+
# BROWSER_SKILL_VISUAL_RESCUE_CMD=PATH executable hook script
|
|
529
|
+
# The hook receives: SITE INTENT CACHED_SELECTOR (positional). It should
|
|
530
|
+
# decide if the cached element is still the right target visually and:
|
|
531
|
+
# exit 0 + stdout "yes" → cache rescued; click/fill proceeds as if cache hit
|
|
532
|
+
# exit 0 + stdout "no" → fall through to fail_count++ → cloud LLM
|
|
533
|
+
# non-zero exit → fall through (treat as "unreachable")
|
|
534
|
+
# See references/recipes/visual-rescue-hook.md for a llama.cpp probe example.
|
|
535
|
+
#
|
|
536
|
+
# The skill is intentionally agnostic about HOW the hook reasons (screenshot
|
|
537
|
+
# crop, full-page snapshot, OCR-only, local-model-of-choice). We ship the
|
|
538
|
+
# seam; users plug their own probe.
|
|
539
|
+
if [ "${rescued}" != "true" ] \
|
|
540
|
+
&& [ "${BROWSER_SKILL_VISION_FALLBACK:-0}" = "1" ] \
|
|
541
|
+
&& [ -n "${BROWSER_SKILL_VISUAL_RESCUE_CMD:-}" ] \
|
|
542
|
+
&& [ -x "${BROWSER_SKILL_VISUAL_RESCUE_CMD}" ]; then
|
|
543
|
+
# Smart-skip: don't fire Path 3 when the archetype has too many recent
|
|
544
|
+
# failures. After N consecutive misses the cache is likely fundamentally
|
|
545
|
+
# broken (selector → wrong-class element, page redesigned, etc.) and
|
|
546
|
+
# asking a local VLM "is it still right?" is wasted ~200ms — cloud LLM
|
|
547
|
+
# is the only thing that can re-derive a working selector. Default
|
|
548
|
+
# threshold 3; override via BROWSER_SKILL_VISUAL_RESCUE_MAX_FAIL_COUNT.
|
|
549
|
+
_vr_max_fc="${BROWSER_SKILL_VISUAL_RESCUE_MAX_FAIL_COUNT:-3}"
|
|
550
|
+
_vr_fc=0
|
|
551
|
+
_vr_arch_file="${BROWSER_SKILL_HOME}/memory/${site}/archetypes/${archetype_id}.json"
|
|
552
|
+
if [ -f "${_vr_arch_file}" ]; then
|
|
553
|
+
_vr_fc="$(jq -r --arg i "${arg_intent}" \
|
|
554
|
+
'(.interactions[]? | select(.intent == $i) | .fail_count) // 0' \
|
|
555
|
+
"${_vr_arch_file}" 2>/dev/null | head -1)"
|
|
556
|
+
_vr_fc="${_vr_fc:-0}"
|
|
557
|
+
fi
|
|
558
|
+
visual_rc=0
|
|
559
|
+
visual_out=""
|
|
560
|
+
if [ "${_vr_fc}" -ge "${_vr_max_fc}" ]; then
|
|
561
|
+
visual_out="skip-too-many-fails"
|
|
562
|
+
else
|
|
563
|
+
visual_out="$("${BROWSER_SKILL_VISUAL_RESCUE_CMD}" \
|
|
564
|
+
"${site}" "${arg_intent}" "${selector}" 2>/dev/null)" \
|
|
565
|
+
|| visual_rc=$?
|
|
566
|
+
fi
|
|
567
|
+
if [ "${visual_rc}" -eq 0 ] && [ "${visual_out}" = "yes" ]; then
|
|
568
|
+
rescued=true
|
|
569
|
+
dispatch_rc=0 # treat as success — element is still semantically present
|
|
570
|
+
self_heal_triggered=true
|
|
571
|
+
printf '%s\n' "$(jq -nc \
|
|
572
|
+
--arg sel "${selector}" \
|
|
573
|
+
'{_kind:"visual_rescue", selector:$sel, rescued:true,
|
|
574
|
+
hook:"BROWSER_SKILL_VISUAL_RESCUE_CMD"}')"
|
|
575
|
+
# Emit stats.jsonl event so browser-stats can compute visual-rescue rate.
|
|
576
|
+
_vr_span_id="$(stats_random_id 2>/dev/null || printf '')"
|
|
577
|
+
_vr_ts="$(stats_now_iso_ms 2>/dev/null || printf '')"
|
|
578
|
+
if [ -n "${_vr_span_id}" ] && [ -n "${_vr_ts}" ]; then
|
|
579
|
+
_vr_event="$(jq -nc \
|
|
580
|
+
--argjson schema_version 1 \
|
|
581
|
+
--arg ts "${_vr_ts}" \
|
|
582
|
+
--arg span_id "${_vr_span_id}" \
|
|
583
|
+
--arg trace_id "${BROWSER_SKILL_TRACE_ID:-${_vr_span_id}}" \
|
|
584
|
+
--arg site "${site}" \
|
|
585
|
+
--arg sel "${selector}" '
|
|
586
|
+
{ schema_version: $schema_version,
|
|
587
|
+
ts: $ts, span_id: $span_id, trace_id: $trace_id,
|
|
588
|
+
parent_span_id: null, session_id: null,
|
|
589
|
+
gen_ai_operation_name: "execute_tool",
|
|
590
|
+
gen_ai_tool_name: "browser-do.visual_rescue",
|
|
591
|
+
gen_ai_tool_type: "function",
|
|
592
|
+
verb: "do",
|
|
593
|
+
adapter_route: "browser-do",
|
|
594
|
+
site: ($site | select(. != "") // null),
|
|
595
|
+
selector_kind: "css", selector_value: $sel,
|
|
596
|
+
duration_ms: 0, argv_bytes: 0, stdout_bytes: 0, stderr_bytes: 0,
|
|
597
|
+
rc: 0,
|
|
598
|
+
outcome: "success",
|
|
599
|
+
failure_mode: null,
|
|
600
|
+
rescued: true,
|
|
601
|
+
fingerprint_from_selector: $sel,
|
|
602
|
+
fingerprint_to_selector: $sel
|
|
603
|
+
}' 2>/dev/null || printf '')"
|
|
604
|
+
[ -n "${_vr_event}" ] && stats_emit_event "${_vr_event}" 2>/dev/null || true
|
|
605
|
+
fi
|
|
606
|
+
fi
|
|
607
|
+
fi
|
|
608
|
+
|
|
609
|
+
if [ "${rescued}" != "true" ]; then
|
|
610
|
+
# Fingerprint rescue didn't apply or didn't succeed — original fail_count
|
|
611
|
+
# path runs (Phase 11 1-iii D1 self-heal still escalates to LLM after 4 fails).
|
|
612
|
+
if ! memory_record_failure "${site}" "${archetype_id}" "${arg_intent}" 2>/dev/null; then
|
|
613
|
+
warn "browser-do: cache fail_count update failed (best-effort; action exit unchanged)"
|
|
614
|
+
else
|
|
615
|
+
self_heal_triggered=true
|
|
616
|
+
fi
|
|
617
|
+
fi
|
|
618
|
+
fi
|
|
619
|
+
|
|
620
|
+
duration_ms=$(( $(now_ms) - SUMMARY_T0 ))
|
|
621
|
+
_record_event "$(jq -nc --arg site "${site}" --arg arch "${archetype_id}" \
|
|
622
|
+
--arg dv "${arg_verb}" --argjson rc "${dispatch_rc}" \
|
|
623
|
+
'{cache_hit:true, site:$site, archetype_id:$arch,
|
|
624
|
+
dispatched_verb:$dv, dispatch_rc:$rc}')"
|
|
625
|
+
summary_json verb=do mode=intent cache_hit=true site="${site}" \
|
|
626
|
+
archetype_id="${archetype_id}" duration_ms="${duration_ms}" \
|
|
627
|
+
dispatched_verb="${arg_verb}" dispatch_rc="${dispatch_rc}" \
|
|
628
|
+
self_heal_triggered="${self_heal_triggered}" status=ok
|
|
629
|
+
|
|
630
|
+
exit "${dispatch_rc}"
|