@gaffa-dev/skills 0.1.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/README.md +88 -0
- package/package.json +23 -0
- package/skills/gaffa-authoring/SKILL.md +177 -0
- package/skills/gaffa-authoring/references/actions.md +235 -0
- package/skills/gaffa-authoring/templates/async-poll.md +58 -0
- package/skills/gaffa-authoring/templates/schema-extraction.md +72 -0
- package/skills/gaffa-authoring/templates/sync.md +31 -0
- package/skills/gaffa-debug/SKILL.md +161 -0
- package/skills/gaffa-debug/references/failure-classification.md +29 -0
- package/skills/gaffa-debug/templates/rerun-with-capture.md +31 -0
- package/skills/gaffa-find/SKILL.md +159 -0
- package/skills/gaffa-find/templates/loop.md +105 -0
- package/skills/gaffa-support/SKILL.md +145 -0
- package/skills/gaffa-support/agents/openai.yaml +2 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Template: synchronous request
|
|
2
|
+
|
|
3
|
+
Use only when the developer wants a blocking call and the expected runtime is well under the plan max.
|
|
4
|
+
Set `"async": false` in the request body.
|
|
5
|
+
The response comes back on the same call, so there is no id to poll.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
curl -sS -X POST https://api.gaffa.dev/v1/browser/requests \
|
|
9
|
+
-H "X-API-Key: ${GAFFA_API_KEY}" \
|
|
10
|
+
-H "Content-Type: application/json" \
|
|
11
|
+
-H "User-Agent: gaffa-skill/1.0" \
|
|
12
|
+
-d '{
|
|
13
|
+
"url": "https://example.com",
|
|
14
|
+
"async": false,
|
|
15
|
+
"max_cache_age": 0,
|
|
16
|
+
"settings": {
|
|
17
|
+
"time_limit": 60000,
|
|
18
|
+
"record_request": true,
|
|
19
|
+
"actions": [
|
|
20
|
+
{ "type": "capture_dom" }
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
}'
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Notes
|
|
27
|
+
|
|
28
|
+
- The plan-tiered max runtime (1 / 2 / 5 min) applies to sync calls too.
|
|
29
|
+
If the job can exceed it, prefer the async-poll template instead.
|
|
30
|
+
- Set the HTTP client read timeout above `time_limit` so the client does not abort a request that is still within its allowed runtime.
|
|
31
|
+
- Everything else (auth, `time_limit`, `record_request`, `max_cache_age`) behaves the same as the async pattern.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gaffa-debug
|
|
3
|
+
description: Use when a gaffa request misbehaves, identified by a brq_* request ID or a script that errors, returns null, times out, or gives wrong output. Pulls the request recording, classifies the failure (gaffa API misuse, target-site change, bot detection, flaky timing, wrong action), and proposes a minimal patch.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# gaffa debug
|
|
7
|
+
|
|
8
|
+
Given a failing gaffa script or a `brq_*` request id, pull the recording, classify the failure, and suggest a targeted patch.
|
|
9
|
+
Prefers a minimal patch over a speculative rewrite, and recognizes when the target site (not the script) is the problem.
|
|
10
|
+
|
|
11
|
+
A request id paired with a new extraction goal is ambiguous.
|
|
12
|
+
Ask the developer whether to triage the existing request or start a fresh discovery.
|
|
13
|
+
The only cap that applies is the credits-per-invocation cap on the optional re-run.
|
|
14
|
+
|
|
15
|
+
## Cost guardrail cap
|
|
16
|
+
|
|
17
|
+
- `CREDITS_PER_INVOCATION` default `50`.
|
|
18
|
+
The optional re-run counts against this cap.
|
|
19
|
+
|
|
20
|
+
## Critical gaffa facts (grounding)
|
|
21
|
+
|
|
22
|
+
1. Auth header is `X-API-Key: <key>`.
|
|
23
|
+
Read from `GAFFA_API_KEY` env var.
|
|
24
|
+
Never hard-code.
|
|
25
|
+
2. `POST /v1/browser/requests` is async by default.
|
|
26
|
+
Returns an id.
|
|
27
|
+
Poll `GET /v1/browser/requests/{id}`.
|
|
28
|
+
Opt into sync with `"async": false`.
|
|
29
|
+
3. Max runtime is plan-tiered (1 / 2 / 5 min) for both sync and async.
|
|
30
|
+
Always set `settings.time_limit` explicitly.
|
|
31
|
+
4. `parse_json` is token-priced, so its cost scales with the content parsed rather than being a flat per-call charge.
|
|
32
|
+
Stored `/v1/schemas` extractions run the same `parse_json` action and are priced the same way.
|
|
33
|
+
Check the live docs for the current model and token rates.
|
|
34
|
+
Other actions are deterministically priced.
|
|
35
|
+
5. Request recordings (`settings.record_request: true`) are strongly recommended for `/gaffa-debug`.
|
|
36
|
+
Without one, the skill can only suggest re-running the failing request with recording enabled.
|
|
37
|
+
Plan-tiered retention applies (7 days / 30 days / 3 months).
|
|
38
|
+
|
|
39
|
+
The API base URL is `https://api.gaffa.dev`.
|
|
40
|
+
Every `/v1/...` endpoint is called on that host.
|
|
41
|
+
The documentation and the docs MCP live on `https://gaffa.dev`.
|
|
42
|
+
API responses are wrapped in a top-level `data` object, so read fields as `data.id`, `data.state`, `data.credit_usage`, and `data.actions`.
|
|
43
|
+
A finished request has `data.state` equal to `completed`.
|
|
44
|
+
Each action result is a URL in `data.actions[].output`.
|
|
45
|
+
The gaffa edge rejects some default HTTP-client User-Agents (for example Python `urllib`) with a 403, so emitted code should set an explicit `User-Agent` header.
|
|
46
|
+
curl works with its default User-Agent.
|
|
47
|
+
In the request body, `actions`, `time_limit`, and `record_request` go under `settings`, while `url`, `async`, `max_cache_age`, and `proxy_location` are root-level.
|
|
48
|
+
`time_limit` is in milliseconds.
|
|
49
|
+
The `parse_json` action uses a structured `data_schema` of the form `{name, description, fields: [{type, name, description}]}`, never a flat object and never a `schema` or `prompt` field.
|
|
50
|
+
An optional `instruction` parameter sits beside `data_schema` (not inside it) for extra parsing guidance.
|
|
51
|
+
`/v1/schemas` is an endpoint for reusable stored schemas, not an action type.
|
|
52
|
+
LLM-backed extraction runs through the `parse_json` action, with an inline `data_schema` or a stored `data_schema_id`, and there is no separate schema action type.
|
|
53
|
+
On a large content-rich page, `parse_json` over the full DOM can fail with `action_failed` (verified on Wikipedia), so narrow the input with a `selector` for the region that holds the data, or set `input_token_cap`.
|
|
54
|
+
|
|
55
|
+
## Preferences
|
|
56
|
+
|
|
57
|
+
General preferences for using the Gaffa API.
|
|
58
|
+
They capture guidance beyond the API docs, and we add to them over time.
|
|
59
|
+
Follow them unless the specific task calls for something else.
|
|
60
|
+
|
|
61
|
+
- Prefer an inline `data_schema` over a stored `data_schema_id` for `parse_json`, so the end user can see the shape of what is being extracted.
|
|
62
|
+
Reach for a stored `data_schema_id` only when a shape is registered server-side for reuse across separate scripts or sessions.
|
|
63
|
+
|
|
64
|
+
## Credential hygiene
|
|
65
|
+
|
|
66
|
+
1. Read `GAFFA_API_KEY` from env only.
|
|
67
|
+
Never hard-code in emitted code.
|
|
68
|
+
Reference it as `${GAFFA_API_KEY}`.
|
|
69
|
+
2. Never echo, log, narrate, or persist the value of `GAFFA_API_KEY`.
|
|
70
|
+
Never put it in a URL or query string.
|
|
71
|
+
3. Before showing a gaffa recording, error trace, or request body to the user, to an LLM judge, or to disk, strip the values of any fields whose names match (case-insensitive, including vendor-prefixed variants like `gaffa_api_key`): `Authorization`, `X-API-Key`, `api_key` (and `apiKey`, `api-key`), `cookie`, `set-cookie`.
|
|
72
|
+
Replace the value with `<REDACTED>`.
|
|
73
|
+
4. If the runtime value of `GAFFA_API_KEY` appears as a substring anywhere in a payload you are about to show or persist, replace it with `<REDACTED>`.
|
|
74
|
+
Only enable this substring scrub when the env value is at least 16 characters long and contains both a digit and a letter.
|
|
75
|
+
Otherwise skip and warn the developer on first invocation that the entropy floor was not met (field-name and prose rules still apply).
|
|
76
|
+
5. Persisted writes (the `/gaffa-find` reasoning log `./.gaffa-find-<timestamp>.log`, and any other on-disk artifact) go through redaction first, then to a tempfile, then atomic-rename to the final path.
|
|
77
|
+
A crash mid-write must not leave a plaintext-secrets file on disk.
|
|
78
|
+
6. If unsure whether a string is a secret, redact it.
|
|
79
|
+
|
|
80
|
+
## Doc-fetching strategy
|
|
81
|
+
|
|
82
|
+
Resolve documentation queries in two tiers, in order.
|
|
83
|
+
|
|
84
|
+
1. Preferred: gaffa docs MCP server at `https://gaffa.dev/docs/~gitbook/mcp`.
|
|
85
|
+
`searchDocumentation` (param `query`) for "how do I do X".
|
|
86
|
+
`getPage` (param `url`) to fetch one page by URL.
|
|
87
|
+
2. Fallback: live HTTP fetch.
|
|
88
|
+
`?ask=` against the docs for narrow lookups, `https://gaffa.dev/docs/llms.txt` for breadth.
|
|
89
|
+
Per-call timeout of 5 seconds.
|
|
90
|
+
|
|
91
|
+
MCP availability probe, once per session, cached, total budget 10 seconds: JSON-RPC `initialize`, then the mandatory `notifications/initialized` notification, then `tools/list` confirming `searchDocumentation` and `getPage` (or at least one).
|
|
92
|
+
Per-call MCP timeout is 5 seconds.
|
|
93
|
+
Two consecutive timeouts demote to live HTTP for the rest of the session, announced once.
|
|
94
|
+
|
|
95
|
+
If both tiers fail before any diagnosis work, refuse with a clear message (suggest registering the MCP if none was configured).
|
|
96
|
+
|
|
97
|
+
## First action
|
|
98
|
+
|
|
99
|
+
If a `brq_*` id is provided, call `GET /v1/browser/requests/{id}` first to pull the recording.
|
|
100
|
+
If a script is provided without an id, read the script and, when the developer agrees, re-run it once with recording enabled to produce a recording to inspect.
|
|
101
|
+
|
|
102
|
+
Important limitation of the recording: the `GET` response does not echo the submitted request body or the action configuration.
|
|
103
|
+
Each entry in `data.actions` carries only its `id`, `type`, `timestamp`, an `error` if it failed, and an `output` URL if it produced one.
|
|
104
|
+
So you can see that an action failed and read the top-level `data.error_reason`, but you cannot read the exact parameters that were sent.
|
|
105
|
+
When the misuse is in the request body (for example an invalid `parse_json` `data_schema`), name the most likely cause from the failure signal and ask the developer for the request body or the script, or recommend a re-run with `record_request` plus `capture_dom` and `capture_screenshot` to gather more signal.
|
|
106
|
+
Do not claim to have read a parameter you could not see.
|
|
107
|
+
|
|
108
|
+
## Behavior
|
|
109
|
+
|
|
110
|
+
- If `GET /v1/browser/requests/{id}` returns 404, surface the plan-tiered retention reality (Starter 7 days, Startup 30 days, Growth 3 months).
|
|
111
|
+
The recording has likely expired.
|
|
112
|
+
- If `settings.record_request` was not set on the original request, surface that no recording was captured, so a re-run is needed to inspect what happened.
|
|
113
|
+
- A recommended re-run sets `record_request: true` and adds `capture_dom` and `capture_screenshot` actions.
|
|
114
|
+
- All recording payloads have credential fields scrubbed per the credential-hygiene rules before being shown to the developer, to Claude, or to any LLM judge.
|
|
115
|
+
- If the diagnosis is inconclusive (an expired recording, no recording and no re-run, or a target-side failure with no script-side fix), end the output with one line: *Still stuck? Run `/gaffa-support` to get help or file a report.*
|
|
116
|
+
|
|
117
|
+
### Failure classification from recording fields
|
|
118
|
+
|
|
119
|
+
All of these fields live under the top-level `data` object in the response.
|
|
120
|
+
|
|
121
|
+
- `data.http_status_code` 4xx plus `data.error_reason` indicates gaffa API misuse.
|
|
122
|
+
Emit a minimal patch.
|
|
123
|
+
- A bare 403 `Forbidden` with no recording and no `data.error_reason`, especially from a non-curl client such as Python `urllib`, often means the gaffa edge blocked the request User-Agent.
|
|
124
|
+
Recommend setting an explicit `User-Agent` header before assuming a key or permission problem.
|
|
125
|
+
Confirm by checking whether the same request succeeds from curl.
|
|
126
|
+
- `data.state` is `completed` but an action returned an empty `output` indicates a target-site DOM change versus a wrong selector.
|
|
127
|
+
Diff the actions against a fresh capture to tell which.
|
|
128
|
+
- `data.error` is `action_failed` on a `parse_json` action has two common, verified causes.
|
|
129
|
+
First, an invalid `data_schema`: it must be the structured form `{name, description, fields: [{type, name, description}]}`, and a flat object such as `{"title": "string"}`, or a `schema` or `prompt` field, makes the action fail.
|
|
130
|
+
Second, an un-narrowed large page: `parse_json` over the full DOM of a content-rich page (for example a Wikipedia article) fails even with a valid schema, and the fix is a `selector` for the region that holds the data, or an `input_token_cap`.
|
|
131
|
+
Check the schema shape first, then the input size.
|
|
132
|
+
Where the data sits in a stable, well-structured place, say a table or a known element, the third option is to stop using `parse_json` for it: `parse_table` or `capture_element` cannot fail this way, cost the same every run, and are the better patch.
|
|
133
|
+
Classify that as a wrong action rather than a `parse_json` to be tuned.
|
|
134
|
+
- `data.error_reason` matching bot detection or captcha indicates target-site bot protection.
|
|
135
|
+
Suggest setting `proxy_location` to a residential location (us, ie, sg, fr), not script changes.
|
|
136
|
+
If one location is blocked, try another supported location, unless the goal is geo-specific and switching would return irrelevant results.
|
|
137
|
+
- `data.running_time` much greater than `data.page_load_time` indicates flaky timing.
|
|
138
|
+
Add a `wait` action or raise `time_limit`.
|
|
139
|
+
Both are duration strings (for example `00:00:01.04`), not numbers.
|
|
140
|
+
- `data.from_cache: true` when fresh data was expected indicates a cross-user cache hit.
|
|
141
|
+
Set `max_cache_age: 0` to disable, or change a parameter to bust the cache key.
|
|
142
|
+
|
|
143
|
+
### Patch and re-run
|
|
144
|
+
|
|
145
|
+
- Prefer minimal patches over rewrites.
|
|
146
|
+
- Before emitting any patch or re-run command, read `templates/rerun-with-capture.md` and follow its request shape exactly.
|
|
147
|
+
Do not write a request body from memory.
|
|
148
|
+
Every emitted body must obey these rules, which are the most common mistakes to avoid:
|
|
149
|
+
- `actions`, `time_limit`, and `record_request` go under `settings`.
|
|
150
|
+
Never place `actions` at the top level of the body.
|
|
151
|
+
- `time_limit` is in milliseconds.
|
|
152
|
+
Use a realistic value such as `60000`, never `60`.
|
|
153
|
+
- `parse_json` uses `data_schema` (structured), never a field called `schema` and never `prompt`.
|
|
154
|
+
- Set an explicit `User-Agent` header.
|
|
155
|
+
- Use the `https://api.gaffa.dev` host and read the key from `${GAFFA_API_KEY}`.
|
|
156
|
+
- Optional re-run defaults to OFF.
|
|
157
|
+
It requires explicit developer confirmation.
|
|
158
|
+
The re-run cost counts against `CREDITS_PER_INVOCATION`.
|
|
159
|
+
Show the credit estimate before re-running and proceed only on an explicit yes.
|
|
160
|
+
|
|
161
|
+
See `references/failure-classification.md` for the field-to-cause table and `templates/rerun-with-capture.md` for the recommended re-run shape.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# gaffa failure classification
|
|
2
|
+
|
|
3
|
+
Map recording fields to a likely cause and the smallest fix.
|
|
4
|
+
Pull the recording with `GET /v1/browser/requests/{id}` and read these fields.
|
|
5
|
+
All fields live under the top-level `data` object in the response.
|
|
6
|
+
The field set includes `data.id`, `data.url`, `data.actual_url`, `data.state`, `data.credit_usage`, `data.http_status_code`, `data.from_cache`, `data.started_at`, `data.completed_at`, `data.running_time`, `data.page_load_time`, and `data.actions` (each action carries its result as a URL in `output`).
|
|
7
|
+
When relevant the response also carries `data.error`, `data.error_reason`, and `data.proxy_location`.
|
|
8
|
+
With `record_request: true` it also includes `data.video`.
|
|
9
|
+
A finished request has `data.state` equal to `completed`.
|
|
10
|
+
|
|
11
|
+
| Signal in recording | Likely cause | Smallest fix |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `data.http_status_code` 4xx plus `data.error_reason` | gaffa API misuse (malformed body, bad field) | One-line patch to the offending field |
|
|
14
|
+
| Bare 403 `Forbidden`, no recording, no `data.error_reason`, non-curl client | edge blocked the request User-Agent | Set an explicit `User-Agent` header. Confirm the same request works from curl |
|
|
15
|
+
| `data.state` `completed` but an action `output` is empty | target-site DOM change vs wrong selector | Diff actions against a fresh capture, then correct the selector or accept the DOM moved |
|
|
16
|
+
| `parse_json` action has `error: action_failed`, no `output` | invalid `data_schema`, or `parse_json` run over the full DOM of a large page | Fix the schema to the structured form first. If the schema is valid, narrow the input with a `selector` or `input_token_cap` |
|
|
17
|
+
| `data.error_reason` matches bot detection or captcha | target-site bot protection | Set `proxy_location` to a residential location (us, ie, sg, fr). If blocked, try another, unless the goal is geo-specific. Not a script change |
|
|
18
|
+
| `data.running_time` much greater than `data.page_load_time` | flaky timing | Add a `wait` action or raise `time_limit` |
|
|
19
|
+
| `data.from_cache: true` when fresh data expected | cross-user cache hit | Set `max_cache_age: 0` or change a parameter to bust the cache key |
|
|
20
|
+
| A `loop` action ends `action_timed_out` | its `timeout` covers all iterations together and ran out | Raise the loop `timeout` or lower the iteration count. Completed iterations keep their outputs |
|
|
21
|
+
| Actions after a `loop` end `action_cancelled` | the loop exited early and cancelled the rest | Set `continue_on_fail: true` on the loop so the actions after it still run |
|
|
22
|
+
| `GET` returns 404 | recording expired past retention | Re-run with `record_request: true`. Retention is 7 days / 30 days / 3 months by plan |
|
|
23
|
+
|
|
24
|
+
## Notes
|
|
25
|
+
|
|
26
|
+
- `max_cache_age` and `proxy_location` are root-level body fields, not under `settings`.
|
|
27
|
+
- Always scrub credential-named fields out of the recording before showing it.
|
|
28
|
+
- Prefer the smallest fix.
|
|
29
|
+
A rewrite is rarely the right first move.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Template: re-run with capture for triage
|
|
2
|
+
|
|
3
|
+
When the original request had no recording, or you need fresh page state to compare against, recommend a re-run with recording plus DOM and screenshot captures.
|
|
4
|
+
The video lets a human review what happened.
|
|
5
|
+
The DOM and screenshot let an agent inspect page state directly.
|
|
6
|
+
|
|
7
|
+
The re-run defaults to OFF.
|
|
8
|
+
Get explicit developer confirmation and show the credit estimate first.
|
|
9
|
+
The cost counts against `CREDITS_PER_INVOCATION`.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
curl -sS -X POST https://api.gaffa.dev/v1/browser/requests \
|
|
13
|
+
-H "X-API-Key: ${GAFFA_API_KEY}" \
|
|
14
|
+
-H "Content-Type: application/json" \
|
|
15
|
+
-H "User-Agent: gaffa-skill/1.0" \
|
|
16
|
+
-d '{
|
|
17
|
+
"url": "URL_FROM_FAILING_REQUEST",
|
|
18
|
+
"max_cache_age": 0,
|
|
19
|
+
"settings": {
|
|
20
|
+
"time_limit": 60000,
|
|
21
|
+
"record_request": true,
|
|
22
|
+
"actions": [
|
|
23
|
+
{ "type": "capture_dom" },
|
|
24
|
+
{ "type": "capture_screenshot" }
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
}'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Append the original failing action after the captures when you want to reproduce the failure with full evidence.
|
|
31
|
+
Keep `max_cache_age: 0` so the re-run reflects live page state rather than a cache hit.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gaffa-find
|
|
3
|
+
description: Use when the user wants to find or extract a specific piece of information from a target website given only a URL and a plain-language goal but has no gaffa script yet. Iteratively writes and refines a gaffa script through reconnaissance, extraction, and validation, then stops in a clear terminal state.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# gaffa find
|
|
7
|
+
|
|
8
|
+
Given a target URL and a natural-language information goal, write and refine a gaffa script through reconnaissance, hypothesis, extraction, validation, and refinement, then stop with a clear terminal state.
|
|
9
|
+
Prevents premature commitment to a wrong selector and silent acceptance of empty or wrong results.
|
|
10
|
+
|
|
11
|
+
## Cost guardrail caps
|
|
12
|
+
|
|
13
|
+
The skill enforces the caps below.
|
|
14
|
+
|
|
15
|
+
- `REQUESTS_PER_ITERATION` default `3`.
|
|
16
|
+
Caps requests inside one loop step.
|
|
17
|
+
- `REQUESTS_PER_INVOCATION` default `15`.
|
|
18
|
+
Caps total requests per run.
|
|
19
|
+
- `CREDITS_PER_INVOCATION` default `50`.
|
|
20
|
+
Caps total credits per run.
|
|
21
|
+
- `MAX_ITERATIONS` default `5`.
|
|
22
|
+
Caps loop iterations.
|
|
23
|
+
|
|
24
|
+
## Critical gaffa facts (grounding)
|
|
25
|
+
|
|
26
|
+
1. Auth header is `X-API-Key: <key>`.
|
|
27
|
+
Read from `GAFFA_API_KEY` env var.
|
|
28
|
+
Never hard-code.
|
|
29
|
+
2. `POST /v1/browser/requests` is async by default.
|
|
30
|
+
Returns an id.
|
|
31
|
+
Poll `GET /v1/browser/requests/{id}`.
|
|
32
|
+
Opt into sync with `"async": false`.
|
|
33
|
+
3. Max runtime is plan-tiered (1 / 2 / 5 min) for both sync and async.
|
|
34
|
+
Always set `settings.time_limit` explicitly.
|
|
35
|
+
4. `parse_json` is token-priced, so its cost scales with the content parsed rather than being a flat per-call charge.
|
|
36
|
+
Stored `/v1/schemas` extractions run the same `parse_json` action and are priced the same way.
|
|
37
|
+
Check the live docs for the current model and token rates.
|
|
38
|
+
Other actions are deterministically priced.
|
|
39
|
+
5. Request recordings (`settings.record_request: true`) are strongly recommended for `/gaffa-debug`.
|
|
40
|
+
Without one, the skill can only suggest re-running the failing request with recording enabled.
|
|
41
|
+
Plan-tiered retention applies (7 days / 30 days / 3 months).
|
|
42
|
+
|
|
43
|
+
The API base URL is `https://api.gaffa.dev`.
|
|
44
|
+
Every `/v1/...` endpoint is called on that host.
|
|
45
|
+
The documentation and the docs MCP live on `https://gaffa.dev`.
|
|
46
|
+
API responses are wrapped in a top-level `data` object, so read fields as `data.id`, `data.state`, `data.credit_usage`, and `data.actions`.
|
|
47
|
+
A finished request has `data.state` equal to `completed`.
|
|
48
|
+
Each action result is a URL in `data.actions[].output`.
|
|
49
|
+
The gaffa edge rejects some default HTTP-client User-Agents (for example Python `urllib`) with a 403, so emitted code should set an explicit `User-Agent` header.
|
|
50
|
+
curl works with its default User-Agent.
|
|
51
|
+
In the request body, `actions`, `time_limit`, and `record_request` go under `settings`, while `url`, `async`, `max_cache_age`, and `proxy_location` are root-level.
|
|
52
|
+
`time_limit` is in milliseconds.
|
|
53
|
+
Each entry in `settings.actions` is an object keyed by `type`, for example `{"type": "generate_markdown"}` or `{"type": "capture_element", "selector": "h1"}`.
|
|
54
|
+
The key is `type`, never `action`, and an action object missing it is rejected with `invalid_action`.
|
|
55
|
+
The `parse_json` action uses a structured `data_schema` of the form `{name, description, fields: [{type, name, description}]}`, never a flat object and never a `schema` or `prompt` field.
|
|
56
|
+
An optional `instruction` parameter sits beside `data_schema` (not inside it) for extra parsing guidance.
|
|
57
|
+
`/v1/schemas` is an endpoint for reusable stored schemas, not an action type.
|
|
58
|
+
LLM-backed extraction runs through the `parse_json` action, with an inline `data_schema` or a stored `data_schema_id`, and there is no separate schema action type.
|
|
59
|
+
`parse_json` is not the only way to get data off a page, see step 3 of the loop.
|
|
60
|
+
On a large content-rich page, `parse_json` over the full DOM can fail with `action_failed` (verified on Wikipedia), so narrow the input with a `selector` for the region that holds the data, or set `input_token_cap`.
|
|
61
|
+
|
|
62
|
+
Reconnaissance uses `POST /v1/site/map` (singular `map`, GET the id to read results) or a broad markdown capture (`generate_markdown`).
|
|
63
|
+
|
|
64
|
+
## Preferences
|
|
65
|
+
|
|
66
|
+
General preferences for using the Gaffa API.
|
|
67
|
+
They capture guidance beyond the API docs, and we add to them over time.
|
|
68
|
+
Follow them unless the specific task calls for something else.
|
|
69
|
+
|
|
70
|
+
- Prefer an inline `data_schema` over a stored `data_schema_id` for `parse_json`, so the end user can see the shape of what is being extracted.
|
|
71
|
+
Reach for a stored `data_schema_id` only when a shape is registered server-side for reuse across separate scripts or sessions.
|
|
72
|
+
|
|
73
|
+
## Credential hygiene
|
|
74
|
+
|
|
75
|
+
1. Read `GAFFA_API_KEY` from env only.
|
|
76
|
+
Never hard-code in emitted code.
|
|
77
|
+
Reference it as `${GAFFA_API_KEY}`.
|
|
78
|
+
2. Never echo, log, narrate, or persist the value of `GAFFA_API_KEY`.
|
|
79
|
+
Never put it in a URL or query string.
|
|
80
|
+
3. Before showing a gaffa recording, error trace, or request body to the user, to an LLM judge, or to disk, strip the values of any fields whose names match (case-insensitive, including vendor-prefixed variants like `gaffa_api_key`): `Authorization`, `X-API-Key`, `api_key` (and `apiKey`, `api-key`), `cookie`, `set-cookie`.
|
|
81
|
+
Replace the value with `<REDACTED>`.
|
|
82
|
+
4. If the runtime value of `GAFFA_API_KEY` appears as a substring anywhere in a payload you are about to show or persist, replace it with `<REDACTED>`.
|
|
83
|
+
Only enable this substring scrub when the env value is at least 16 characters long and contains both a digit and a letter.
|
|
84
|
+
Otherwise skip and warn the developer on first invocation that the entropy floor was not met (field-name and prose rules still apply).
|
|
85
|
+
5. Persisted writes (the `/gaffa-find` reasoning log `./.gaffa-find-<timestamp>.log`, and any other on-disk artifact) go through redaction first, then to a tempfile, then atomic-rename to the final path.
|
|
86
|
+
A crash mid-write must not leave a plaintext-secrets file on disk.
|
|
87
|
+
6. If unsure whether a string is a secret, redact it.
|
|
88
|
+
|
|
89
|
+
## Doc-fetching strategy
|
|
90
|
+
|
|
91
|
+
Resolve documentation queries in two tiers, in order.
|
|
92
|
+
|
|
93
|
+
1. Preferred: gaffa docs MCP server at `https://gaffa.dev/docs/~gitbook/mcp`.
|
|
94
|
+
`searchDocumentation` (param `query`) for "how do I do X".
|
|
95
|
+
`getPage` (param `url`) to fetch one page by URL.
|
|
96
|
+
2. Fallback: live HTTP fetch.
|
|
97
|
+
`?ask=` against the docs for narrow lookups, `https://gaffa.dev/docs/llms.txt` for breadth.
|
|
98
|
+
Per-call timeout of 5 seconds.
|
|
99
|
+
|
|
100
|
+
MCP availability probe, once per session, cached, total budget 10 seconds: JSON-RPC `initialize`, then the mandatory `notifications/initialized` notification, then `tools/list` confirming `searchDocumentation` and `getPage` (or at least one).
|
|
101
|
+
Per-call MCP timeout is 5 seconds.
|
|
102
|
+
Two consecutive timeouts demote to live HTTP for the rest of the session, announced once.
|
|
103
|
+
|
|
104
|
+
If both tiers fail at iteration 0 (before any work), refuse with a clear message (suggest registering the MCP if none was configured).
|
|
105
|
+
If both tiers fail mid-flight (at least one iteration done), do not discard progress: return the current best candidate as `needs-human-review`, persist the reasoning log, and surface the doc-fetch failure as the reason.
|
|
106
|
+
|
|
107
|
+
## First action
|
|
108
|
+
|
|
109
|
+
Fire a reconnaissance request, not a doc load.
|
|
110
|
+
The reconnaissance (site map or broad markdown capture) tells you where the answer is likely to live before you commit to a selector.
|
|
111
|
+
|
|
112
|
+
## Terminal states
|
|
113
|
+
|
|
114
|
+
The skill always ends in exactly one of these three states.
|
|
115
|
+
|
|
116
|
+
- success.
|
|
117
|
+
The extracted value clearly fits the goal, and any developer-supplied validator passed.
|
|
118
|
+
Output includes a short evidence line (which page, which selector or extraction step) so the developer can verify without rerunning.
|
|
119
|
+
- needs-human-review.
|
|
120
|
+
The skill is uncertain (judgment ambiguous, or budget exhausted with a partial candidate).
|
|
121
|
+
Output includes the best candidate, the reasoning-log location, and the failing script's request id with a `/gaffa-debug` suggestion.
|
|
122
|
+
- terminal-failure.
|
|
123
|
+
Unrecoverable error: the target blocks every attempt, or the doc fetch fails mid-flight after work began.
|
|
124
|
+
Output explains why and surfaces the request id.
|
|
125
|
+
|
|
126
|
+
On `needs-human-review` or `terminal-failure`, end the output with one line: *Still stuck? Run `/gaffa-support` to get help or file a report.*
|
|
127
|
+
|
|
128
|
+
## Loop
|
|
129
|
+
|
|
130
|
+
1. Reconnaissance (`/v1/site/map` or a broad markdown capture).
|
|
131
|
+
2. Hypothesize the answer's likely location.
|
|
132
|
+
3. Targeted extraction.
|
|
133
|
+
Choose the action before you look up its parameters, and say which you chose and why.
|
|
134
|
+
When the value sits in a stable, well-structured place, take the deterministic path: `parse_table` with a `selector` for a table, `generate_markdown` with a `selector` for a region you then parse in the developer's own language, or `capture_element` with a `selector` for one element.
|
|
135
|
+
Cheaper, repeatable, identical every run, and your `generate_markdown` recon output often already shows whether the shape is stable enough.
|
|
136
|
+
Only when the value is buried in free text or moves from page to page (a salary somewhere in a job description), or the goal is interpretive (summarising, classifying), use the `parse_json` action (inline `data_schema`, or `data_schema_id` for a reused shape), which handles ambiguity a selector cannot but is token-priced and can vary between runs.
|
|
137
|
+
4. Check the stop condition: does the value clearly fit the goal, and does any supplied validator pass?
|
|
138
|
+
5. Refine the hypothesis or finish, returning the answer plus a re-runnable script.
|
|
139
|
+
6. Stop on success, on budget exhaustion (any of the three caps or `MAX_ITERATIONS`), or on an unrecoverable error.
|
|
140
|
+
|
|
141
|
+
## Behavior
|
|
142
|
+
|
|
143
|
+
- Inputs.
|
|
144
|
+
Required: target URL and a plain-language goal.
|
|
145
|
+
Optional: an explicit validator passed as `--validator` (a regex, JSON schema, or expected type) for deterministic termination, and a max-iterations override.
|
|
146
|
+
- Pre-flight credit estimate before each request.
|
|
147
|
+
Pause and confirm when the invocation total would breach `CREDITS_PER_INVOCATION`.
|
|
148
|
+
For token-priced actions (`parse_json`, `/v1/schemas`) gate against the upper bound of the estimate, not the midpoint.
|
|
149
|
+
For `parse_json`, fetch current rates from https://gaffa.dev/docs/credits-and-pricing.md once per invocation, reuse that cached rate for later requests, and add a 50% safety margin on output tokens.
|
|
150
|
+
For `/v1/schemas`, pin a conservative ceiling at the worst current `parse_json` rate applied to the captured page size in tokens, tell the developer the ceiling is a guess, and ask for confirmation regardless of headroom.
|
|
151
|
+
- `--reconnaissance-only` returns the site map or landing capture without entering the extraction loop.
|
|
152
|
+
- Per-iteration reasoning is logged to `./.gaffa-find-<timestamp>.log` in the developer's working directory, written through the redaction-then-tempfile-then-atomic-rename path.
|
|
153
|
+
See `templates/loop.md` for the rationale and shape.
|
|
154
|
+
- Always set `settings.time_limit` explicitly and `settings.record_request: true` so a failing attempt can be triaged with `/gaffa-debug`.
|
|
155
|
+
- Do not invent an answer.
|
|
156
|
+
If the goal is not found within budget, stop with `needs-human-review` and say so plainly.
|
|
157
|
+
- Returns both the answer and the working, re-runnable gaffa script.
|
|
158
|
+
|
|
159
|
+
See `templates/loop.md` for the reconnaissance, extraction, and reasoning-log shapes.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Template: find loop shapes
|
|
2
|
+
|
|
3
|
+
Language-agnostic shapes for the discovery loop.
|
|
4
|
+
Adapt to the developer's language.
|
|
5
|
+
|
|
6
|
+
## Reconnaissance: site map
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
curl -sS -X POST https://api.gaffa.dev/v1/site/map \
|
|
10
|
+
-H "X-API-Key: ${GAFFA_API_KEY}" \
|
|
11
|
+
-H "Content-Type: application/json" \
|
|
12
|
+
-H "User-Agent: gaffa-skill/1.0" \
|
|
13
|
+
-d '{ "url": "https://example.com" }'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The response carries an id.
|
|
17
|
+
Read the result with `GET /v1/site/map/{id}`.
|
|
18
|
+
The path is singular `map`, not `maps`.
|
|
19
|
+
|
|
20
|
+
## Reconnaissance: broad markdown capture
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
curl -sS -X POST https://api.gaffa.dev/v1/browser/requests \
|
|
24
|
+
-H "X-API-Key: ${GAFFA_API_KEY}" \
|
|
25
|
+
-H "Content-Type: application/json" \
|
|
26
|
+
-H "User-Agent: gaffa-skill/1.0" \
|
|
27
|
+
-d '{
|
|
28
|
+
"url": "https://example.com",
|
|
29
|
+
"max_cache_age": 0,
|
|
30
|
+
"settings": {
|
|
31
|
+
"time_limit": 60000,
|
|
32
|
+
"record_request": true,
|
|
33
|
+
"actions": [
|
|
34
|
+
{ "type": "generate_markdown" }
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
}'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Targeted extraction with the parse_json action
|
|
41
|
+
|
|
42
|
+
Only reach for this once you have ruled out a deterministic path, per step 3 of the loop in SKILL.md.
|
|
43
|
+
LLM-backed extraction runs through the `parse_json` action, and there is no `/v1/schemas` action type.
|
|
44
|
+
Pass an inline `data_schema`, or reference a stored schema with `data_schema_id` after creating it via the `/v1/schemas` endpoint.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
curl -sS -X POST https://api.gaffa.dev/v1/browser/requests \
|
|
48
|
+
-H "X-API-Key: ${GAFFA_API_KEY}" \
|
|
49
|
+
-H "Content-Type: application/json" \
|
|
50
|
+
-H "User-Agent: gaffa-skill/1.0" \
|
|
51
|
+
-d '{
|
|
52
|
+
"url": "https://example.com/leadership",
|
|
53
|
+
"max_cache_age": 0,
|
|
54
|
+
"settings": {
|
|
55
|
+
"time_limit": 60000,
|
|
56
|
+
"record_request": true,
|
|
57
|
+
"actions": [
|
|
58
|
+
{
|
|
59
|
+
"type": "parse_json",
|
|
60
|
+
"selector": "main",
|
|
61
|
+
"data_schema": {
|
|
62
|
+
"name": "leadership",
|
|
63
|
+
"description": "Find the chief executive officer",
|
|
64
|
+
"fields": [
|
|
65
|
+
{ "type": "string", "name": "ceo_name", "description": "the full name of the CEO" }
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
}'
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`parse_json` is token-priced.
|
|
75
|
+
Gate it against the upper-bound estimate before firing.
|
|
76
|
+
Give it a `selector` for the region your recon identified, because `parse_json` over the full DOM of a large page can return `action_failed`.
|
|
77
|
+
If you do not yet know a good selector, your `generate_markdown` recon output often already contains the answer, so read that before spending another extraction request.
|
|
78
|
+
|
|
79
|
+
## Reasoning log
|
|
80
|
+
|
|
81
|
+
Append one entry per iteration to `./.gaffa-find-<timestamp>.log`.
|
|
82
|
+
One entry records the iteration number, the hypothesis, the request id, the candidate value, and the stop-condition decision.
|
|
83
|
+
Never write a key value into the log.
|
|
84
|
+
|
|
85
|
+
Write it safely.
|
|
86
|
+
The tempfile must live in the same directory as the final log so the rename is atomic on the same filesystem.
|
|
87
|
+
A tempfile in `/tmp` and a final log in the working directory are usually on different filesystems, which turns the rename into a non-atomic copy and defeats the crash-safety guarantee.
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
FINALLOG="./.gaffa-find-$(date +%Y%m%dT%H%M%S).log"
|
|
91
|
+
TMPLOG="${FINALLOG}.tmp" # same directory as the final log, not /tmp
|
|
92
|
+
|
|
93
|
+
cat > "$TMPLOG" << 'EOF'
|
|
94
|
+
# gaffa-find reasoning log
|
|
95
|
+
... one entry per iteration ...
|
|
96
|
+
EOF
|
|
97
|
+
|
|
98
|
+
# redact only when the env value meets the entropy floor (16+ chars, a digit and a letter)
|
|
99
|
+
if [ "${#GAFFA_API_KEY}" -ge 16 ] && printf '%s' "$GAFFA_API_KEY" | grep -q '[0-9]' && printf '%s' "$GAFFA_API_KEY" | grep -q '[A-Za-z]'
|
|
100
|
+
then
|
|
101
|
+
sed -i "s|${GAFFA_API_KEY}|<REDACTED>|g" "$TMPLOG"
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
mv "$TMPLOG" "$FINALLOG" # atomic rename within the same directory
|
|
105
|
+
```
|