@agentrhq/webcmd 0.3.3 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -4
- package/dist/src/browser/ax-snapshot.js +15 -5
- package/dist/src/browser/ax-snapshot.test.js +49 -0
- package/hosted-contract.json +1 -1
- package/package.json +3 -2
- package/skills/smart-search/SKILL.md +20 -6
- package/skills/webcmd-browser/SKILL.md +16 -2
- package/skills/webcmd-usage/SKILL.md +21 -6
package/README.md
CHANGED
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
<img alt="NPM downloads" src="https://img.shields.io/npm/dt/@agentrhq/webcmd.svg?style=for-the-badge&color=1E88E5&labelColor=000000">
|
|
10
10
|
</a>
|
|
11
11
|
<a href="https://docs.webcmd.dev">
|
|
12
|
-
<img alt="Documentation" src="https://img.shields.io/badge/docs-webcmd.dev-
|
|
12
|
+
<img alt="Documentation" src="https://img.shields.io/badge/docs-webcmd.dev-7C3AED.svg?style=for-the-badge&labelColor=000000">
|
|
13
13
|
</a>
|
|
14
14
|
<a href="https://github.com/agentrhq/webcmd/blob/main/LICENSE">
|
|
15
15
|
<img alt="License" src="https://img.shields.io/badge/license-Apache%202.0-1E88E5.svg?style=for-the-badge&labelColor=000000">
|
|
16
16
|
</a>
|
|
17
17
|
<a href="https://discord.gg/9YP2C9tvMp">
|
|
18
|
-
<img alt="Join the community on Discord" src="https://img.shields.io/badge/Join%20the%20community-
|
|
18
|
+
<img alt="Join the community on Discord" src="https://img.shields.io/badge/Join%20the%20community-5865F2.svg?style=for-the-badge&logo=discord&logoColor=white&labelColor=000000&logoWidth=20">
|
|
19
19
|
</a>
|
|
20
20
|
<a href="https://x.com/agentrhq">
|
|
21
21
|
<img alt="Follow AgentR on X" src="https://img.shields.io/badge/Built%20by%20%40agentrhq-000000.svg?style=for-the-badge&logo=x&logoColor=white&labelColor=000000&logoWidth=20">
|
|
@@ -28,8 +28,6 @@
|
|
|
28
28
|
|
|
29
29
|
WebCMD learns the navigational context of websites as agents use them, then compiles that knowledge into deterministic commands for faster, cheaper, more reliable browser automation. The goal is simple: stop making agents rediscover the same sites on every run and cut browser-agent token spend by up to 90%.
|
|
30
30
|
|
|
31
|
-
📖 **Full documentation: [docs.webcmd.dev](https://docs.webcmd.dev)**
|
|
32
|
-
|
|
33
31
|
On top of live browser control, WebCMD adds 3 layers of learnings. Each layer collapses cost and variance for the layer above it.
|
|
34
32
|
|
|
35
33
|
| Layer | Scenario | What Webcmd Helps With |
|
|
@@ -71,7 +71,7 @@ function renderAxTree(axTree, lines, refs, nextRef, opts) {
|
|
|
71
71
|
const rawNodes = Array.isArray(axTree?.nodes)
|
|
72
72
|
? axTree.nodes
|
|
73
73
|
: [];
|
|
74
|
-
const nodes = rawNodes.filter((node) => node
|
|
74
|
+
const nodes = rawNodes.filter((node) => node);
|
|
75
75
|
const byId = new Map();
|
|
76
76
|
const parentIds = new Set();
|
|
77
77
|
for (const node of nodes) {
|
|
@@ -88,11 +88,21 @@ function renderAxTree(axTree, lines, refs, nextRef, opts) {
|
|
|
88
88
|
});
|
|
89
89
|
const root = roots[0] ?? nodes[0];
|
|
90
90
|
const maxDepth = Math.max(1, Math.min(Number(opts.maxDepth) || 50, 200));
|
|
91
|
+
const maxTraversalHops = 200;
|
|
91
92
|
const roleNameCounts = countRoleNames(nodes);
|
|
92
93
|
const roleNameSeen = new Map();
|
|
93
|
-
function render(node, depth) {
|
|
94
|
-
if (!node || depth > maxDepth)
|
|
94
|
+
function render(node, depth, hops, path) {
|
|
95
|
+
if (!node || depth > maxDepth || hops > maxTraversalHops || path.has(node))
|
|
95
96
|
return false;
|
|
97
|
+
const nextPath = new Set(path).add(node);
|
|
98
|
+
if (node.ignored) {
|
|
99
|
+
let hasVisibleChild = false;
|
|
100
|
+
for (const childId of node.childIds ?? []) {
|
|
101
|
+
if (render(byId.get(childId), depth, hops + 1, nextPath))
|
|
102
|
+
hasVisibleChild = true;
|
|
103
|
+
}
|
|
104
|
+
return hasVisibleChild;
|
|
105
|
+
}
|
|
96
106
|
const role = axString(node.role) || 'generic';
|
|
97
107
|
const name = cleanText(axString(node.name));
|
|
98
108
|
const value = cleanText(axString(node.value) || propertyValue(node, 'value'));
|
|
@@ -109,7 +119,7 @@ function renderAxTree(axTree, lines, refs, nextRef, opts) {
|
|
|
109
119
|
const childStart = lines.length;
|
|
110
120
|
let hasVisibleChild = false;
|
|
111
121
|
for (const childId of node.childIds ?? []) {
|
|
112
|
-
if (render(byId.get(childId), depth + 1))
|
|
122
|
+
if (render(byId.get(childId), depth + 1, hops + 1, nextPath))
|
|
113
123
|
hasVisibleChild = true;
|
|
114
124
|
}
|
|
115
125
|
if (!shouldShowSelf && !hasVisibleChild) {
|
|
@@ -151,7 +161,7 @@ function renderAxTree(axTree, lines, refs, nextRef, opts) {
|
|
|
151
161
|
}
|
|
152
162
|
return true;
|
|
153
163
|
}
|
|
154
|
-
render(root, opts.baseDepth);
|
|
164
|
+
render(root, opts.baseDepth, 0, new Set());
|
|
155
165
|
return nextRef;
|
|
156
166
|
}
|
|
157
167
|
export function findAxRefReplacement(axTree, ref) {
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
1
2
|
import { describe, expect, it } from 'vitest';
|
|
2
3
|
import { buildAxSnapshot, buildAxSnapshotFromTrees, findAxRefReplacement } from './ax-snapshot.js';
|
|
3
4
|
describe('AX snapshot prototype', () => {
|
|
5
|
+
it('publishes the AX snapshot builder as a package subpath', () => {
|
|
6
|
+
const packageJson = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
7
|
+
expect(packageJson.exports?.['./browser/ax-snapshot'])
|
|
8
|
+
.toBe('./dist/src/browser/ax-snapshot.js');
|
|
9
|
+
});
|
|
4
10
|
it('builds compact refs from Accessibility.getFullAXTree output', () => {
|
|
5
11
|
const result = buildAxSnapshot({
|
|
6
12
|
nodes: [
|
|
@@ -25,6 +31,49 @@ describe('AX snapshot prototype', () => {
|
|
|
25
31
|
name: 'Category',
|
|
26
32
|
});
|
|
27
33
|
});
|
|
34
|
+
it('keeps actionable descendants of ignored AX nodes', () => {
|
|
35
|
+
const result = buildAxSnapshot({
|
|
36
|
+
nodes: [
|
|
37
|
+
{ nodeId: '1', role: { value: 'RootWebArea' }, childIds: ['2'] },
|
|
38
|
+
{ nodeId: '2', ignored: true, childIds: ['3', '4'] },
|
|
39
|
+
{ nodeId: '3', role: { value: 'button' }, name: { value: 'First' }, backendDOMNodeId: 30 },
|
|
40
|
+
{ nodeId: '4', role: { value: 'button' }, name: { value: 'Second' }, backendDOMNodeId: 40 },
|
|
41
|
+
],
|
|
42
|
+
});
|
|
43
|
+
expect(result.text).toContain(' [1]button "First"\n [2]button "Second"');
|
|
44
|
+
expect(result.text).not.toContain('ignored');
|
|
45
|
+
expect([...result.refs.values()]).toEqual([
|
|
46
|
+
{ ref: '1', backendNodeId: 30, role: 'button', name: 'First' },
|
|
47
|
+
{ ref: '2', backendNodeId: 40, role: 'button', name: 'Second' },
|
|
48
|
+
]);
|
|
49
|
+
});
|
|
50
|
+
it('stops ignored-node cycles without changing promoted indentation', () => {
|
|
51
|
+
const result = buildAxSnapshot({
|
|
52
|
+
nodes: [
|
|
53
|
+
{ nodeId: '1', role: { value: 'RootWebArea' }, childIds: ['2'] },
|
|
54
|
+
{ nodeId: '2', ignored: true, childIds: ['2', '3'] },
|
|
55
|
+
{ nodeId: '3', role: { value: 'button' }, name: { value: 'Save' }, backendDOMNodeId: 30 },
|
|
56
|
+
],
|
|
57
|
+
});
|
|
58
|
+
expect(result.text).toContain('\n [1]button "Save"\n');
|
|
59
|
+
expect(result.refs).toHaveLength(1);
|
|
60
|
+
});
|
|
61
|
+
it('bounds traversal through long ignored-node chains', () => {
|
|
62
|
+
const ignored = Array.from({ length: 201 }, (_, index) => ({
|
|
63
|
+
nodeId: String(index + 2),
|
|
64
|
+
ignored: true,
|
|
65
|
+
childIds: [String(index + 3)],
|
|
66
|
+
}));
|
|
67
|
+
const result = buildAxSnapshot({
|
|
68
|
+
nodes: [
|
|
69
|
+
{ nodeId: '1', role: { value: 'RootWebArea' }, childIds: ['2'] },
|
|
70
|
+
...ignored,
|
|
71
|
+
{ nodeId: '203', role: { value: 'button' }, name: { value: 'Too deep' }, backendDOMNodeId: 203 },
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
expect(result.text).not.toContain('Too deep');
|
|
75
|
+
expect(result.refs).toHaveLength(0);
|
|
76
|
+
});
|
|
28
77
|
it('tracks nth only for duplicate role/name pairs', () => {
|
|
29
78
|
const result = buildAxSnapshot({
|
|
30
79
|
nodes: [
|
package/hosted-contract.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentrhq/webcmd",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Turn websites, browser sessions, desktop apps, and local tools into deterministic CLI surfaces for humans and AI agents.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.0.0"
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"./utils": "./dist/src/utils.js",
|
|
19
19
|
"./logger": "./dist/src/logger.js",
|
|
20
20
|
"./launcher": "./dist/src/launcher.js",
|
|
21
|
+
"./browser/ax-snapshot": "./dist/src/browser/ax-snapshot.js",
|
|
21
22
|
"./browser/cdp": "./dist/src/browser/cdp.js",
|
|
22
23
|
"./browser/page": "./dist/src/browser/page.js",
|
|
23
24
|
"./browser/utils": "./dist/src/browser/utils.js",
|
|
@@ -113,4 +114,4 @@
|
|
|
113
114
|
"overrides": {
|
|
114
115
|
"postcss": "^8.5.10"
|
|
115
116
|
}
|
|
116
|
-
}
|
|
117
|
+
}
|
|
@@ -11,10 +11,24 @@ Route a query to the best webcmd search source based on the topic and context. T
|
|
|
11
11
|
|
|
12
12
|
Before every use, do both of these steps:
|
|
13
13
|
|
|
14
|
-
-
|
|
15
|
-
- Use the live registry to confirm that candidate sites exist, and inspect `strategy`, `browser`, and `domain`
|
|
14
|
+
- Derive literal workflow terms from the requested action, entity, output fields, and any explicitly named site, then filter the live registry before it reaches bounded agent output:
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
```bash
|
|
17
|
+
WORKFLOW_TERMS='["requested action", "output field", "named site"]'
|
|
18
|
+
webcmd list -f json | jq --argjson terms "$WORKFLOW_TERMS" '
|
|
19
|
+
[.[] | select(
|
|
20
|
+
([.site, .name, .description, ((.columns // []) | join(" "))]
|
|
21
|
+
| map(. // "") | join(" ") | ascii_downcase) as $text
|
|
22
|
+
| any($terms[]; . as $term | $text | contains($term | ascii_downcase))
|
|
23
|
+
)]
|
|
24
|
+
'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- Use the complete, non-truncated filtered registry to confirm that candidate sites exist, and inspect `strategy`, `browser`, and `domain`
|
|
28
|
+
|
|
29
|
+
Any truncation warning from registry or plugin output means the inspection is incomplete. Narrow the filter or query and inspect again; absence from truncated output never proves a site, command, or plugin is unavailable. Do not search plugins until a complete filtered registry result for the missing capability is exactly `[]`, and do not use raw browser fallback until plugin output is also complete and non-truncated.
|
|
30
|
+
|
|
31
|
+
If the user explicitly names a site/source, or no installed command covers the query, refine the filter for that missing site or capability. Only when the complete filtered result is `[]`, check installable plugins before marking it unavailable:
|
|
18
32
|
|
|
19
33
|
```bash
|
|
20
34
|
webcmd plugin search <site-or-source-or-capability> -f json
|
|
@@ -22,7 +36,7 @@ webcmd plugin search <site-or-source-or-capability> -f json
|
|
|
22
36
|
|
|
23
37
|
Derive a short plugin query from the missing site or capability. Preserve the user's term when practical: `find flights` becomes `flight`.
|
|
24
38
|
|
|
25
|
-
If plugin search finds a match, tell the user it is available as a plugin and offer `webcmd plugin install <source>`. If plugin search fails because catalog sources cannot be fetched, report that catalog/search error separately from no plugin match.
|
|
39
|
+
If a complete, non-truncated plugin search finds a match, tell the user it is available as a plugin and offer `webcmd plugin install <source>`. If plugin search is truncated, refine it before fallback. If it fails because catalog sources cannot be fetched, report that catalog/search error separately from no plugin match.
|
|
26
40
|
|
|
27
41
|
After choosing a site, do both of these steps:
|
|
28
42
|
|
|
@@ -144,12 +158,12 @@ Keep a typical query to 1 AI source plus 1-2 specialized sources to avoid result
|
|
|
144
158
|
When a site is unavailable:
|
|
145
159
|
|
|
146
160
|
- Do not stop the whole search because one source failed
|
|
147
|
-
- For a missing site or capability, run `webcmd plugin search <query> -f json` before recording unavailable
|
|
161
|
+
- For a missing site or capability, first require a complete filtered registry result of `[]`, then run `webcmd plugin search <query> -f json` before recording unavailable
|
|
148
162
|
- Record: `Skipped: <site> unavailable`
|
|
149
163
|
- Fall back to another site of the same type, or to one AI source
|
|
150
164
|
- Always trust the actual output of `webcmd list -f json`, `webcmd plugin search -f json`, and `webcmd <site> -h`
|
|
151
165
|
|
|
152
|
-
Do not assume any site is always available. Even for public sites, trust live help and execution results in the current environment.
|
|
166
|
+
Do not assume any site is always available. Even for public sites, trust complete, non-truncated live registry and plugin output, live help, and execution results in the current environment. Raw browser fallback requires both the complete `[]` registry result and a complete, non-truncated plugin search that returned no match and no error.
|
|
153
167
|
|
|
154
168
|
## Reference Files
|
|
155
169
|
|
|
@@ -12,13 +12,26 @@ This skill is for **driving a live browser** to accomplish an agent task. If you
|
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
+
## Adapter fallback gate
|
|
16
|
+
|
|
17
|
+
Before starting a raw browser session, filter `webcmd list -f json` at the source using request-derived terms across `site`, `name`, `description`, and `columns`; follow `webcmd-usage` for the command shape. Any truncation warning means adapter discovery is incomplete: narrow the filter and inspect again. Absence from truncated output never proves that no adapter exists.
|
|
18
|
+
|
|
19
|
+
Raw `webcmd browser` use is allowed only after both conditions hold:
|
|
20
|
+
|
|
21
|
+
1. The complete, non-truncated filtered registry result for the missing capability is exactly `[]`.
|
|
22
|
+
2. A complete, non-truncated `webcmd plugin search <capability> -f json` result returns no match and no error.
|
|
23
|
+
|
|
24
|
+
If plugin search returns a match, offer installation. If its output is truncated, refine the query or output and inspect again. If it errors, report the error and stop instead of opening the browser.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
15
28
|
## Prerequisites
|
|
16
29
|
|
|
17
30
|
```bash
|
|
18
31
|
webcmd doctor
|
|
19
32
|
```
|
|
20
33
|
|
|
21
|
-
Until `doctor` is green,
|
|
34
|
+
Until `doctor` is green, browser commands will not work. Registry and plugin discovery do not require `doctor`. Typical failures: Chrome not running, extension not installed, debug port blocked by 1Password / other extensions. The doctor output tells you which.
|
|
22
35
|
|
|
23
36
|
---
|
|
24
37
|
|
|
@@ -61,7 +74,7 @@ Bound sessions use the normal Webcmd session lifecycle; `unbind` releases the Cl
|
|
|
61
74
|
## Critical rules
|
|
62
75
|
|
|
63
76
|
1. **Always inspect before you act.** Run `state` or `find` first. Never hard-code a ref or selector from memory across sessions — indices are per-snapshot.
|
|
64
|
-
2. **Prefer site adapters before raw browser driving.** If `webcmd <site> <command>` already covers the task, use that adapter command first (`webcmd facebook notifications`, `webcmd reddit read`, `webcmd chatgpt model <level>`, etc.). Use `webcmd browser ...` only for gaps, debugging, or one-off UI flows the adapter does not expose.
|
|
77
|
+
2. **Prefer site adapters before raw browser driving.** Complete the adapter fallback gate above. If `webcmd <site> <command>` already covers the task, use that adapter command first (`webcmd facebook notifications`, `webcmd reddit read`, `webcmd chatgpt model <level>`, etc.). Use `webcmd browser ...` only for gaps, debugging, or one-off UI flows the adapter does not expose.
|
|
65
78
|
3. **Prefer numeric ref over CSS once you have it.** Numeric refs survive mild DOM shifts because the CLI fingerprints each tagged element. A CSS selector written by hand will break the first time the site re-renders.
|
|
66
79
|
4. **Read `match_level` after every write.** `exact` = all good. `stable` = the element is the same but some soft attrs drifted — your action still applied. `reidentified` = the original ref was gone and the CLI found a unique replacement; double-check you hit the right element.
|
|
67
80
|
5. **Use the `compound` field for form controls.** Do not regex-guess a date format, do not `state` twice to get the full `<select>` options list. The compound envelope has the format string, full option list up to 50, `options_total` for overflow, and `accept`/`multiple` for `<input type=file>`.
|
|
@@ -433,6 +446,7 @@ normal DOM `state`, or navigate/bind directly to the iframe URL when possible.
|
|
|
433
446
|
| `stale_ref` across every command | You are reusing refs from a prior page. Re-`state`. |
|
|
434
447
|
| `click` succeeds but nothing happens | The element is probably a decorative wrapper stealing clicks from the real target. `find --css "..."` with a narrower selector and retry on the inner element. |
|
|
435
448
|
| `type` appears to finish but value is wrong | Autocomplete, masked input, or React controlled re-render. Verify with `get value`. Add `keys Enter` or re-type. |
|
|
449
|
+
| `webcmd list -f json` output is truncated | Adapter discovery is incomplete. Filter at the source with request-derived terms and narrow until the complete result is `[]`; do not start browser fallback yet. |
|
|
436
450
|
| Giant `get html` output | Pass `--selector` + `--as json --depth 3 --children-max 20 --text-max 200`. |
|
|
437
451
|
| Network cache seems stale | Bump `--ttl` down, or let it expire. The cache lives at `~/.webcmd/cache/browser-network/`. |
|
|
438
452
|
|
|
@@ -50,19 +50,33 @@ Run commands instead of reading static docs:
|
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
52
|
webcmd
|
|
53
|
-
webcmd list -f json
|
|
54
53
|
webcmd <site> --help
|
|
55
54
|
webcmd <site> <command> --help
|
|
56
55
|
```
|
|
57
56
|
|
|
58
57
|
Run `webcmd` with no arguments to see all available functions and installed site adapters. Do not hard-code adapter lists: `webcmd list -f json` is the source of truth for installed commands and emits one entry per command with fields such as `{site, name, aliases, description, strategy, browser, args, columns}`.
|
|
59
58
|
|
|
59
|
+
Large registries can exceed an agent or tool output budget. Filter the JSON stream before it is emitted, using broad literal terms derived from the whole requested workflow:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
WORKFLOW_TERMS='["requested action", "output field", "named site"]'
|
|
63
|
+
webcmd list -f json | jq --argjson terms "$WORKFLOW_TERMS" '
|
|
64
|
+
[.[] | select(
|
|
65
|
+
([.site, .name, .description, ((.columns // []) | join(" "))]
|
|
66
|
+
| map(. // "") | join(" ") | ascii_downcase) as $text
|
|
67
|
+
| any($terms[]; . as $term | $text | contains($term | ascii_downcase))
|
|
68
|
+
)]
|
|
69
|
+
'
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Replace `WORKFLOW_TERMS` with terms from the current request: the requested action, entity, output fields, and any explicitly named site. Literal matching avoids regex errors from terms such as `C++` or `[foo]`. Do not maintain a site or category allowlist. Match across `site`, `name`, `description`, and `columns`. If any layer reports truncated output, the inspection is incomplete. Narrow the filter and inspect again. Never treat absence from truncated output as proof that an adapter or plugin is missing, and do not proceed to the next fallback stage from that evidence.
|
|
73
|
+
|
|
60
74
|
Use this fallback order:
|
|
61
75
|
|
|
62
|
-
1. Run `webcmd list -f json`
|
|
63
|
-
2. Check
|
|
64
|
-
3.
|
|
65
|
-
4. If plugin search returns a match, offer `webcmd plugin install <installSource>`.
|
|
76
|
+
1. Run `webcmd list -f json` through a workflow-derived filter before returning its output to the agent.
|
|
77
|
+
2. Check the complete, non-truncated filtered result against the whole requested workflow. If one installed command covers it, use that command and stop discovery. If candidates do not cover the missing capability, refine the capability filter until its complete result is exactly `[]`.
|
|
78
|
+
3. Only after that complete filtered result is `[]`, derive a short plugin query from the missing site or capability and run `webcmd plugin search <query> -f json`. Preserve the user's term when practical: `find flights` becomes `flight`.
|
|
79
|
+
4. If the complete, non-truncated plugin search returns a match, offer `webcmd plugin install <installSource>`. Only if that complete result returns no match and no error is raw `webcmd browser` allowed. Both plugin search and raw browser fallback require the prior complete filtered registry result to be `[]`. A truncated plugin result is incomplete evidence: refine the query or output before fallback. If plugin search errors, report plugin discovery as unavailable and stop. If `fetch failed` appears in `errors[].message`, report plugin discovery as unavailable due to network/reachability and ask the user whether to rerun with network/escalated permissions. Do not retry unless they approve.
|
|
66
80
|
|
|
67
81
|
## Universal Flags
|
|
68
82
|
|
|
@@ -194,6 +208,7 @@ Do not invoke these removed commands:
|
|
|
194
208
|
|
|
195
209
|
## Do Not
|
|
196
210
|
|
|
197
|
-
- Do not paste static command lists into plans;
|
|
211
|
+
- Do not paste static command lists into plans; query `webcmd list -f json` through a workflow-derived filter.
|
|
212
|
+
- Do not emit a large unfiltered registry into a bounded output or infer absence from a truncation warning; filter at the source and narrow until the result is complete.
|
|
198
213
|
- Do not assume every adapter needs a browser; check `strategy`.
|
|
199
214
|
- Do not silently fall back from a failing adapter to hand-rolled `fetch`; use `--trace retain-on-failure` first.
|