@nurix/etna 0.15.6 → 0.15.8
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/bin/etna.js +64 -13
- package/data/harness-index.json +7 -5
- package/data/harnesses/code/CLAUDE.md +3 -7
- package/data/harnesses/code/agents/harness-manager.md +4 -4
- package/data/harnesses/code/agents/maintenance.md +1 -1
- package/data/harnesses/code/harness.json +4 -3
- package/data/harnesses/code/hooks/hooks.json +3 -17
- package/data/harnesses/code/hooks/prompt-recall.js +157 -0
- package/data/harnesses/code/hooks/session-reflect.js +283 -0
- package/data/harnesses/code/skills/distiller/SKILL.md +62 -60
- package/data/harnesses/code/skills/distiller/references/docs-mode.md +21 -21
- package/data/harnesses/code/skills/distiller/references/method.md +41 -41
- package/data/harnesses/code/skills/documentation/references/templates.md +20 -27
- package/data/harnesses/code-fable/CLAUDE.md +138 -0
- package/data/harnesses/code-fable/agents/brainstorm.md +35 -0
- package/data/harnesses/code-fable/agents/change-review.md +249 -0
- package/data/harnesses/code-fable/agents/harness-manager.md +193 -0
- package/data/harnesses/code-fable/agents/maintenance.md +123 -0
- package/data/harnesses/code-fable/agents/performance-review.md +218 -0
- package/data/harnesses/code-fable/agents/spec-mode.md +56 -0
- package/data/harnesses/code-fable/harness.json +7 -0
- package/data/harnesses/code-fable/hooks/hooks.json +27 -0
- package/data/harnesses/code-fable/hooks/lib/redact.js +37 -0
- package/data/harnesses/code-fable/hooks/prompt-recall.js +157 -0
- package/data/harnesses/code-fable/hooks/session-reflect.js +283 -0
- package/data/harnesses/code-fable/rules/general.md +96 -0
- package/data/harnesses/code-fable/rules/nomenclature.md +34 -0
- package/data/harnesses/code-fable/skills/boundary/SKILL.md +262 -0
- package/data/harnesses/code-fable/skills/boundary/references/boundary-taxonomy.md +734 -0
- package/data/harnesses/code-fable/skills/brainstorm/README.md +136 -0
- package/data/harnesses/code-fable/skills/brainstorm/SKILL.md +135 -0
- package/data/harnesses/code-fable/skills/brainstorm/harvest.md +26 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/backlog.md +39 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/build.md +37 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/decisions.md +29 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/doctrine.md +31 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/domain-model.md +25 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/glossary.md +19 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/milestones.md +43 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/non-goals.md +21 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/plan-doc.md +61 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/questions.md +35 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/references.md +28 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/rejected.md +29 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/state.md +8 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/tech.md +23 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/thesis.md +21 -0
- package/data/harnesses/code-fable/skills/distiller/SKILL.md +219 -0
- package/data/harnesses/code-fable/skills/distiller/references/docs-mode.md +526 -0
- package/data/harnesses/code-fable/skills/distiller/references/method.md +166 -0
- package/data/harnesses/code-fable/skills/documentation/SKILL.md +127 -0
- package/data/harnesses/code-fable/skills/documentation/references/examples.md +193 -0
- package/data/harnesses/code-fable/skills/documentation/references/style-guide.md +288 -0
- package/data/harnesses/code-fable/skills/documentation/references/templates.md +942 -0
- package/data/harnesses/code-fable/skills/workflow-mapper/SKILL.md +430 -0
- package/data/harnesses/code-fable/skills/workflow-mapper/references/patterns.md +214 -0
- package/data/harnesses/discover/CLAUDE.md +180 -0
- package/data/harnesses/{re → discover}/agents/api.md +3 -3
- package/data/harnesses/{re → discover}/agents/codebase.md +3 -3
- package/data/harnesses/{re → discover}/agents/community.md +3 -3
- package/data/harnesses/{re → discover}/agents/deployed-client-bundle.md +21 -15
- package/data/harnesses/{re → discover}/agents/distribution-artifacts.md +13 -12
- package/data/harnesses/{re → discover}/agents/docs.md +3 -3
- package/data/harnesses/{re → discover}/agents/infra-backend-fingerprint.md +3 -3
- package/data/harnesses/{re → discover}/agents/packages.md +3 -3
- package/data/harnesses/{re → discover}/agents/session.md +29 -11
- package/data/harnesses/{re → discover}/agents/website.md +3 -3
- package/data/harnesses/{re → discover}/agents/wire-capture.md +14 -10
- package/data/harnesses/discover/harness.json +7 -0
- package/data/harnesses/{re/rules/re-discovery.md → discover/rules/discovery.md} +86 -17
- package/data/harnesses/{re/rules/re-evaluation.md → discover/rules/evaluation.md} +40 -4
- package/data/harnesses/discover/rules/ingestion.md +551 -0
- package/data/harnesses/discover/rules/self-correction.md +177 -0
- package/data/harnesses/discover/rules/tradecraft.md +532 -0
- package/data/harnesses/hr/CLAUDE.md +91 -0
- package/data/harnesses/hr/harness.json +7 -0
- package/data/harnesses/hr/skills/interview-eval/SKILL.md +118 -0
- package/data/harnesses/hr/skills/interview-eval/TEMPLATE.md +124 -0
- package/data/harnesses/hr/skills/interview-eval/extract_session.py +418 -0
- package/data/harnesses/hr/skills/interview-eval/rubric.md +219 -0
- package/data/harnesses/kb-agi/harness.json +1 -0
- package/package.json +1 -1
- package/data/harnesses/code/hooks/post-commit.js +0 -136
- package/data/harnesses/code/hooks/post-tool-failure.js +0 -136
- package/data/harnesses/code/hooks/session-stop.js +0 -207
- package/data/harnesses/re/CLAUDE.md +0 -146
- package/data/harnesses/re/harness.json +0 -6
- package/data/harnesses/re/rules/re-ingestion.md +0 -327
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
# Discover Harness — Session & Wire Tradecraft (keyboard-level reference)
|
|
2
|
+
|
|
3
|
+
The executable field layer the browser-driving dimensions (`session`, `wire-capture`) reach for at
|
|
4
|
+
the keyboard. Everything here is **runnable verbatim** — taps, scrubbers, escape snippets — not prose
|
|
5
|
+
to re-derive. It is the distilled tradecraft from real teardowns; the `§1.x`/`§2.x` anchors are the
|
|
6
|
+
origin tags from those runs.
|
|
7
|
+
|
|
8
|
+
**How the dimensions use this file:**
|
|
9
|
+
- `session` / `wire-capture` install the **Tap toolkit** taps first, then traverse via in-app nav.
|
|
10
|
+
- Every captured value passes through the **runnable redaction layer** — which lives in the ethics
|
|
11
|
+
contract (`ingestion.md` §7, "Redaction — runnable") because redaction is non-negotiable —
|
|
12
|
+
before it is returned, logged, or written to `raw/`.
|
|
13
|
+
- Read the **Harness gotchas** before every run; reach for **Dead-end recovery** when a surface walls.
|
|
14
|
+
- `discovery` (fingerprinting) and `evaluation` (classifying a finding) also cite
|
|
15
|
+
the **fingerprint tables** here.
|
|
16
|
+
|
|
17
|
+
> Provenance: ported verbatim from the prior-generation playbook's `general-tricks.md` /
|
|
18
|
+
> `general-saas.md`. Keep it living — fold every new gotcha / tap / fingerprint back in (the same
|
|
19
|
+
> maintenance loop the redaction table carries).
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Tap toolkit (copy-paste in-page interceptors)
|
|
24
|
+
|
|
25
|
+
The runnable in-page instrumentation `session`/`wire-capture` install to mine the authenticated runtime. Each snippet is **idempotent** (early-exits if already installed via a `window.__xTap` sentinel) so it is safe to re-run after any DOM-changing action.
|
|
26
|
+
|
|
27
|
+
**Two operating rules bind every snippet here:**
|
|
28
|
+
|
|
29
|
+
- **Re-install after every full navigation.** The JS context is wiped on full page loads, `location.reload()`, and any anchor click that triggers a full nav. SPA-internal `pushState`-routed nav preserves it (the in-page wrapper survives client-side nav — which is exactly why client-side nav is how you capture a fresh on-load socket; see ingestion §9.4). Full navs do not.
|
|
30
|
+
- **`SCRUB` is defined in the ingestion redaction section** (`ingestion.md` §7 / the source toolkit §2.1, §4). Every value a tap returns to the harness — and every value before it reaches `raw/` — passes through `SCRUB`. The taps below capture **shapes, not secrets**; pair them with `SCRUB`/`redactSecrets` before anything is returned, logged, or written.
|
|
31
|
+
|
|
32
|
+
> **Sentinel gotcha (ingestion §9.4 / source §2.9):** never wrap `fetch` twice. A second body interceptor breaks `response.clone()` from the inner wrapper. Install **one** tap of each kind — the sentinel guard below is what enforces that.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
### 1. Fetch + XHR interceptor (URL / method / body shapes)
|
|
37
|
+
|
|
38
|
+
**When to use:** the primary capture for any REST/JSON/SSE-over-fetch app — install it FIRST, before navigating, then traverse via in-app nav. Logs request URL/method/body and a clamped response preview; CDP `read_network_requests` is an unreliable supplement that misses in-flight + cached requests.
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
(() => {
|
|
42
|
+
if (window.__fetchTap) return 'already';
|
|
43
|
+
window.__fetchTap = [];
|
|
44
|
+
const orig = window.fetch.bind(window);
|
|
45
|
+
window.fetch = function(input, init) {
|
|
46
|
+
const url = typeof input === 'string' ? input : input.url;
|
|
47
|
+
const method = (init && init.method) || (input && input.method) || 'GET';
|
|
48
|
+
const entry = { url, method, at: Date.now() };
|
|
49
|
+
if (init && typeof init.body === 'string') entry.body = init.body.slice(0, 8000);
|
|
50
|
+
window.__fetchTap.push(entry);
|
|
51
|
+
return orig(input, init).then(resp => {
|
|
52
|
+
try {
|
|
53
|
+
entry.status = resp.status;
|
|
54
|
+
entry.respCt = resp.headers.get('content-type') || '';
|
|
55
|
+
const c = resp.clone();
|
|
56
|
+
if (/json|text\/plain|event-stream/.test(entry.respCt)) {
|
|
57
|
+
c.text().then(t => {
|
|
58
|
+
entry.respLen = t.length;
|
|
59
|
+
entry.resp = t.slice(0, 12000);
|
|
60
|
+
}).catch(() => {});
|
|
61
|
+
}
|
|
62
|
+
} catch(_){}
|
|
63
|
+
return resp;
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
if (!window.__xhrTapped) {
|
|
67
|
+
window.__xhrTapped = true;
|
|
68
|
+
const origOpen = XMLHttpRequest.prototype.open;
|
|
69
|
+
const origSend = XMLHttpRequest.prototype.send;
|
|
70
|
+
XMLHttpRequest.prototype.open = function(method, url) {
|
|
71
|
+
this.__url = url; this.__method = method;
|
|
72
|
+
return origOpen.apply(this, arguments);
|
|
73
|
+
};
|
|
74
|
+
XMLHttpRequest.prototype.send = function(body) {
|
|
75
|
+
window.__fetchTap.push({
|
|
76
|
+
kind: 'xhr', url: this.__url, method: this.__method,
|
|
77
|
+
body: typeof body === 'string' ? body.slice(0, 600) : '[non-string]',
|
|
78
|
+
at: Date.now(),
|
|
79
|
+
});
|
|
80
|
+
return origSend.apply(this, arguments);
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return 'tapped';
|
|
84
|
+
})()
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
> Apply `SCRUB` (redaction section) to `entry.body` / `entry.resp` before returning or writing — body capture is clamped to 8000–12000 chars; larger bodies go to a `window.__RAW_*` global and are paged (ingestion §5, size discipline).
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
### 2. WebSocket constructor wrapper (with binary-frame signature handling)
|
|
92
|
+
|
|
93
|
+
**When to use:** any realtime/collab/streaming layer carried over WebSocket. Catches only sockets created **after** install — a library already initialised at page-boot (Pusher/Ably) is invisible here; use tap 4 instead. For an on-load socket, client-side-nav to a fresh room so a new socket is constructed through your wrapper (ingestion §9.4).
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
(() => {
|
|
97
|
+
if (window.__wsTap) return 'already-tapped';
|
|
98
|
+
window.__wsTap = [];
|
|
99
|
+
const OriginalWS = window.WebSocket;
|
|
100
|
+
window.WebSocket = function (...args) {
|
|
101
|
+
const ws = new OriginalWS(...args);
|
|
102
|
+
window.__wsTap.push({ kind: 'open', url: args[0], at: Date.now() });
|
|
103
|
+
ws.addEventListener('message', (e) => {
|
|
104
|
+
window.__wsTap.push({
|
|
105
|
+
kind: 'recv', url: args[0],
|
|
106
|
+
data: typeof e.data === 'string' ? e.data.slice(0, 4000) : '[binary]',
|
|
107
|
+
at: Date.now(),
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
ws.addEventListener('close', () => window.__wsTap.push({ kind: 'close', url: args[0], at: Date.now() }));
|
|
111
|
+
const origSend = ws.send.bind(ws);
|
|
112
|
+
ws.send = (data) => {
|
|
113
|
+
window.__wsTap.push({
|
|
114
|
+
kind: 'send', url: args[0],
|
|
115
|
+
data: typeof data === 'string' ? data.slice(0, 4000) : '[binary]',
|
|
116
|
+
at: Date.now(),
|
|
117
|
+
});
|
|
118
|
+
return origSend(data);
|
|
119
|
+
};
|
|
120
|
+
return ws;
|
|
121
|
+
};
|
|
122
|
+
Object.assign(window.WebSocket, OriginalWS);
|
|
123
|
+
return 'tapped';
|
|
124
|
+
})()
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
> **Never log a binary frame as opaque `[binary]`** (ingestion §9.4). The snippet above stubs binary as `[binary]` for the string-clamp path; for any `ArrayBuffer`/`Blob` frame replace that branch with a **signature** capture — byte-length + first ~16 bytes as hex AND decoded as ASCII (a legible prefix names STOMP/Phoenix/Centrifugo/`+topic` in one step before guessing protobuf/MessagePack/CBOR/Yjs). Drop this into the `message`/`send` handlers in place of the `'[binary]'` literal:
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
const frameSig = (d) => {
|
|
131
|
+
if (typeof d === 'string') return { kind: 'text', len: d.length, data: d.slice(0, 4000) };
|
|
132
|
+
const buf = d instanceof ArrayBuffer ? d : (d && d.buffer) ? d.buffer : null;
|
|
133
|
+
if (!buf) return { kind: 'unknown' };
|
|
134
|
+
const head = new Uint8Array(buf.slice(0, 16));
|
|
135
|
+
const hex = [...head].map(b => b.toString(16).padStart(2, '0')).join(' ');
|
|
136
|
+
const ascii = [...head].map(b => (b >= 32 && b < 127) ? String.fromCharCode(b) : '.').join('');
|
|
137
|
+
return { kind: 'binary', byteLen: buf.byteLength, head16Hex: hex, head16Ascii: ascii };
|
|
138
|
+
};
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### 3. EventSource wrapper
|
|
144
|
+
|
|
145
|
+
**When to use:** apps that stream via the `EventSource` API. Always run alongside tap 1 — many products read `text/event-stream` over `fetch().body` instead of `EventSource`, where this tap returns empty (source §2.3); the fetch tap catches those.
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
(() => {
|
|
149
|
+
if (window.__esTapped) return 'already';
|
|
150
|
+
window.__esTapped = true;
|
|
151
|
+
window.__esTap = [];
|
|
152
|
+
const OrigES = window.EventSource;
|
|
153
|
+
window.EventSource = function(url, init) {
|
|
154
|
+
const es = new OrigES(url, init);
|
|
155
|
+
window.__esTap.push({ kind: 'es-open', url, at: Date.now() });
|
|
156
|
+
es.addEventListener('message', (e) => {
|
|
157
|
+
window.__esTap.push({
|
|
158
|
+
kind: 'es-msg', url,
|
|
159
|
+
data: typeof e.data === 'string' ? e.data.slice(0, 4000) : '[non-string]',
|
|
160
|
+
at: Date.now(),
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
es.addEventListener('error', () => window.__esTap.push({ kind: 'es-err', url, at: Date.now() }));
|
|
164
|
+
return es;
|
|
165
|
+
};
|
|
166
|
+
Object.assign(window.EventSource, OrigES);
|
|
167
|
+
return 'tapped';
|
|
168
|
+
})()
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
### 4. Realtime-SDK global catalog listener (Pusher / Ably `bind_global`)
|
|
174
|
+
|
|
175
|
+
**When to use:** a realtime library (Pusher, Ably, Socket.IO, Centrifugo, Phoenix) was initialised as a global **before** your WebSocket wrap installed — its socket used the unwrapped constructor and is invisible to tap 2. Hook the library's own event-bus instead; `bind_global` yields decoded event names + channels + payloads (more robust than walking `p.connection.connection.transport.socket`).
|
|
176
|
+
|
|
177
|
+
**For Pusher:**
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
(() => {
|
|
181
|
+
if (!window.Pusher || !window.Pusher.instances || !window.Pusher.instances[0]) return 'no-pusher';
|
|
182
|
+
if (window.__pusherTapped) return 'already';
|
|
183
|
+
window.__pusherTapped = true;
|
|
184
|
+
window.__pusherLog = [];
|
|
185
|
+
const p = window.Pusher.instances[0];
|
|
186
|
+
p.bind_global((eventName, data) => {
|
|
187
|
+
window.__pusherLog.push({
|
|
188
|
+
ts: Date.now(), evt: eventName,
|
|
189
|
+
channels: Object.keys(p.channels.channels),
|
|
190
|
+
data: typeof data === 'string' ? data.slice(0, 800) : JSON.stringify(data || {}).slice(0, 800)
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
return JSON.stringify({
|
|
194
|
+
state: p.connection.state,
|
|
195
|
+
cluster: p.config.cluster,
|
|
196
|
+
key: p.key,
|
|
197
|
+
channels: Object.keys(p.channels.channels)
|
|
198
|
+
});
|
|
199
|
+
})()
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
> **For Ably:** `window.Ably.Realtime.instances` exposes the same pattern. The returned `{ state, cluster, key, channels }` is the realtime-handshake summary; the accumulating `window.__pusherLog` is the event/channel catalog. Pass the `key` field through `SCRUB` before returning.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
### 5. Idempotent re-install guard
|
|
207
|
+
|
|
208
|
+
**When to use:** every tap above — and any `window.__*` capture global — must early-exit if already installed, so re-running a snippet after a DOM-changing action does not double-wrap (`fetch` wrapped twice breaks `response.clone()`; source §2.9). The guard is the first line of each tap; for capture registries that should *survive and accumulate* within a navigation rather than early-exit, use the `||=` init form.
|
|
209
|
+
|
|
210
|
+
```js
|
|
211
|
+
// Guard form — early-exit if a wrapper is already installed (use atop every tap):
|
|
212
|
+
(() => {
|
|
213
|
+
if (window.__xTap) return 'already'; // sentinel: unique per tap (__wsTap, __esTap, __fetchTap, __pusherTapped, ...)
|
|
214
|
+
window.__xTap = [];
|
|
215
|
+
// ... install wrapper here ...
|
|
216
|
+
return 'tapped';
|
|
217
|
+
})()
|
|
218
|
+
|
|
219
|
+
// Init form — idempotent registry that survives & accumulates within one navigation
|
|
220
|
+
// (re-init after every full nav; the global is wiped on reload — source §2.11):
|
|
221
|
+
window.__probe = window.__probe || {};
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
| Sentinel | Guards tap |
|
|
225
|
+
|----------|-----------|
|
|
226
|
+
| `window.__fetchTap` / `window.__xhrTapped` | Fetch + XHR (item 1) |
|
|
227
|
+
| `window.__wsTap` | WebSocket (item 2) |
|
|
228
|
+
| `window.__esTapped` / `window.__esTap` | EventSource (item 3) |
|
|
229
|
+
| `window.__pusherTapped` / `window.__pusherLog` | Pusher/Ably global (item 4) |
|
|
230
|
+
| `window.__probe` (init-form `||=`) | per-navigation probe registry |
|
|
231
|
+
|
|
232
|
+
> The guard is a **per-navigation** sentinel: a full `navigate()` / `location.reload()` wipes both the wrapper and its sentinel, so re-running the snippet after a full nav correctly re-installs. Within a single SPA session, the sentinel prevents the double-wrap.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Harness gotchas (Chrome MCP-specific) — read before every run
|
|
237
|
+
|
|
238
|
+
The Chrome-MCP browser-driving layer has ~27 recurring failure modes; every one bit a real run. Each entry below is **failure mode → concrete workaround**. They bind the `session` and `wire-capture` dimensions (the only browser-driving dimensions); the runnable taps/escapes are EXECUTABLE, not prose — paste them as-is.
|
|
239
|
+
|
|
240
|
+
1. **Output that *looks* like cookie/query data is blocked** (`[BLOCKED: Cookie/query string data]`) — a `javascript_tool` result with JWT-shaped strings, long base64, long hex, many UUIDs, or query-string substrings is blocked on the *returned string*, not on what the code does → run the returned value through a `SCRUB()` (and, when that's not enough, the `ULTRA()` regex) before returning. *(§2.1)*
|
|
241
|
+
2. **`read_network_requests` only tracks AFTER its first call** — initial page-load requests are invisible (it starts capturing on first invocation) → install the fetch/XHR taps **before** navigation, or reload after the first call. *(§2.2)*
|
|
242
|
+
3. **`read_network_requests` misses cross-origin XHRs + cache-served loads (GraphQL SPAs)** — it may not surface cross-origin API XHRs (e.g. `i.jasper.ai/gw/graphql` from `app.jasper.ai`), and on reload data is served from the Apollo/SWR cache so no request fires at all → don't conclude "no API calls"; pivot to the client cache and force a refetch to capture verbatim bodies. *(§2.2 corollary)*
|
|
243
|
+
4. **`EventSource` taps miss `text/event-stream` over `fetch`** — many products read SSE-formatted bodies via `fetch().body`, not `EventSource`, so the ES tap returns empty even while streaming → always run **both** the EventSource tap and the fetch/streaming-body tap. *(§2.3)*
|
|
244
|
+
5. **Long-poll buffers rotate fast on heartbeats** — heartbeats fly every few seconds and evict interesting frames from bounded buffers → skip frames smaller than ~350 bytes when storing, and dedupe events by `id` so retried polls don't re-add. *(§2.4)*
|
|
245
|
+
6. **`find` returns ghost duplicates in dynamic UIs** — modern apps render a textarea+button pair twice (visible+hidden, or in a portal); `find` returns both refs but only one is wired → default to coordinate clicks for action-bearing buttons; use refs only for stable structural elements. *(§2.5)*
|
|
246
|
+
7. **`Enter` in textareas doesn't submit; contentEditable divs aren't textareas** — the submit handler is bound to a button, not the keypress, and many apps use `contentEditable` divs as fake textareas (`form_input` won't work) → click the submit button by coordinate after typing; on contentEditable use coordinate-click + `computer.type`. *(§2.6)*
|
|
247
|
+
8. **`wait` caps at 10s** — a single `wait` won't hold longer → batch multiple `wait` calls, or use `browser_batch` with several `wait` actions; inside JS, `await new Promise(r => setTimeout(r, 4000))` works for ≤45s waits. *(§2.7)*
|
|
248
|
+
9. **Don't dump raw tokens** — the harness (correctly) blocks raw `document.cookie` / `Authorization` dumps even when the user is OK seeing structure → inspect **shape** only (lengths, names, decoded JWT payload with PII redacted), never raw values. *(§2.8)*
|
|
249
|
+
10. **`response.clone()` chains break if you wrap fetch twice** — a `clone()` from an inner wrapper may not work in the outer → install **one** tap and do everything in it; don't layer multiple body interceptors. *(§2.9)*
|
|
250
|
+
11. **Sleep/wait is never your loop** — if you're doing `wait → check → wait → check`, server-side state is more reliable than babysitting client polling → switch to: trigger action → wait once → query the trajectory/history endpoint directly. *(§2.10)*
|
|
251
|
+
12. **JS context is wiped on every full navigation** — `window.__TOK/__TAP/__RAW/__probe/__DUMP/...` are all gone after `navigate()`, `location.reload()`, or any anchor click triggering a full load → re-install the tap after every navigation, re-grab the token from localStorage, and use idempotent init (`window.__X = window.__X || {...}`). SPA-internal `pushState`-routed nav preserves context (keep your taps), **but** `history.pushState` + `dispatchEvent(new PopStateEvent('popstate'))` may trigger a *full* nav in Next.js App Router — don't rely on programmatic SPA-internal nav; use real link clicks or just navigate. *(§2.11)*
|
|
252
|
+
13. **WebSocket tap doesn't catch sockets created before installation** — a library (Pusher, Ably) that inits its WebSocket at app-boot via `window.WebSocket` is invisible to a wrap installed after that point → use the library-specific event-bus tap (e.g. Pusher `bind_global`) instead. *(§2.12)*
|
|
253
|
+
14. **Page renderer freezes during agent generation** — `find`, `screenshot`, `read_page` wait for `document_idle` and hang while the page constantly mutates (streaming output) → for liveness checks during generation, use `javascript_tool` (no idle wait). *(§2.13)*
|
|
254
|
+
15. **`cloned.text()` never resolves on long-poll bodies** — the body tap reads via `response.clone().text()`, which only resolves on response *close*; a held-open long-poll never closes, so the promise never resolves → capturing chunks-as-they-arrive needs `getReader()`-based decoding; for most RE, skip streaming-chunk capture and hit the `/history` endpoint instead. *(§2.14)*
|
|
255
|
+
16. **Be aggressive with body-capture limits** — when wrapping fetch, slice the response-body capture to 8000–12000 chars; the tap entry's `resp` field is for previewing only → anything bigger goes into a `window.__RAW_*` global and is paged through (the global is for the full body). *(§2.15)*
|
|
256
|
+
17. **Output-truncation tells** — the harness truncates very long outputs at ~3–5KB; watch for `[TRUNCATED]` markers → either page-through via `slice()` or write the data to a file via the Write tool (skip the JS round-trip). *(§2.16)*
|
|
257
|
+
18. **Slices get truncated even at 3000 chars** — empirically the harness truncates each slice output to ~2.5–3KB *regardless of slice size* → use **3000-char slices** as a defensive default and scrub aggressively before returning; for 100KB+ bodies switch to the aggregate-summary-from-window pattern (write decoded summaries + samples, not verbatim bodies). *(§2.17)*
|
|
258
|
+
19. **`tabs_context_mcp` is required at the start of any session** — tab IDs from previous conversations are stale and won't work → always call `mcp__claude-in-chrome__tabs_context_mcp` first to confirm tab IDs and that the tab group is alive. *(§2.18)*
|
|
259
|
+
20. **`read_network_requests` filters URL *substring*, not regex** — pass `urlPattern: "/api/"` for a literal substring match; no regex, no globs → don't try `urlPattern: "/api/*"` (it's treated as the literal substring `/api/*`, rarely what you want). *(§2.19)*
|
|
260
|
+
21. **`read_network_requests` is cumulative within a domain** — the panel accumulates across the session and only clears on cross-domain nav OR when you pass `clear: true` → for per-route mining, clear the panel before navigating to each new route (`{tabId, urlPattern: "...", clear: true}`) so the request set is scoped to that page. *(§2.20)*
|
|
261
|
+
22. **`browser_batch` outputs are individually capped, not jointly** — each item's output is independently capped (~3–5KB) → fit ~10 `slice()` calls in one batch for ~30KB total across separate output blocks (each item still truncates individually if too long). *(§2.21)*
|
|
262
|
+
23. **A SyntaxError inside a batch fails the rest of the batch** — one bad `javascript_tool` item in a `browser_batch` can stop the rest from executing → test JS expressions in isolation before batching them. *(§2.22)*
|
|
263
|
+
24. **CORS-walled serverless RPC is a hard read-only stop** — cross-origin fetch from the SPA origin to a Cloud Functions / Cloud-Run RPC host fails preflight (`TypeError: Failed to fetch`); POST-only RPC bodies/headers aren't probeable read-only → mine the **Firestore IndexedDB cache** (`collectionParents` census, `remoteDocuments` cached bodies+schemas, `targets` active subscriptions) + `redux-persist` slices; a Firestore REST `GET documents/users/{uid}` (200) confirms the Bearer model, `listCollectionIds` (403) is the negative control proving security rules are active. *(§2.23 — Plai)*
|
|
264
|
+
25. **`403 "Token is not valid."` (not 401) = wrong-auth-scheme tripwire, not broken auth** — on single-host dual-auth-surface products, the app Bearer JWT hitting a *public-developer-API* path returns a 403 with a token message → conclude it's the wrong scheme for that path, not that the token is bad. *(§2.24 — Creatify)*
|
|
265
|
+
26. **Onboarding gating can blank the whole live surface — an un-code-split bundle is the rescue** — when every route redirects to `/onboarding` for an incomplete tenant, no live traffic is harvestable → string-mine a single un-split SPA bundle (Omneky's 5.74 MB `index-*.js`) for the full API-path catalog + route table, and fill in confirmed schemas from the pre-onboarding-reachable endpoints (brand-setup/reference/account). Treat an un-split bundle as a gift, not a nuisance. *(§2.25 — Omneky)*
|
|
266
|
+
27. **Probe Bearer vs cookie vs raw-token explicitly** — a JS-readable token cookie does NOT imply cookie-auth; on Creatify and Omneky the API rejects the raw cookie (`credentials:'include'` → `Failed to fetch` under `ACAO:*`) and accepts only an explicit `Authorization: Bearer` → probe all three (Bearer header / cookie / raw-token) to pin the real header model before mining. *(§2.26)*
|
|
267
|
+
28. **Dynamic/runtime-constructed realtime hub URLs can't be recovered by bundle regex** — AdCreative's SignalR hub path is built at runtime in `initializeHub` with no static literal → record realtime *presence* + transport as confirmed, the URL + message contract as open (needs Pass 2). Don't report "no realtime" just because grep found no socket URL. *(§2.27 — AdCreative)*
|
|
268
|
+
|
|
269
|
+
### The escape snippets (paste verbatim)
|
|
270
|
+
|
|
271
|
+
**Fire-and-forget poll — CDP `Runtime.evaluate` times out at ~45s; a fetch that holds the connection open longer (long-poll, slow stream) must be fired, then polled (§1.10):**
|
|
272
|
+
|
|
273
|
+
```js
|
|
274
|
+
// Pass 1: fire
|
|
275
|
+
(() => {
|
|
276
|
+
window.__done = false;
|
|
277
|
+
fetch(url, { headers }).then(r => r.text()).then(t => {
|
|
278
|
+
window.__raw = t;
|
|
279
|
+
window.__done = true;
|
|
280
|
+
});
|
|
281
|
+
return 'fired';
|
|
282
|
+
})()
|
|
283
|
+
|
|
284
|
+
// Pass 2..N: poll
|
|
285
|
+
(() => 'done=' + window.__done + ' len=' + (window.__raw||'').length)()
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Window-storage + pagination — when a fetched body is larger than the harness output budget (~3–5KB), store it in a global and page through it; use 3000-char slices to leave headroom for the truncation tell, and scrub each slice before returning (§1.9):**
|
|
289
|
+
|
|
290
|
+
```js
|
|
291
|
+
// Pass 1: fetch + store
|
|
292
|
+
(async () => {
|
|
293
|
+
const r = await fetch(url, { headers });
|
|
294
|
+
window.__RAW = await r.text();
|
|
295
|
+
return 'len=' + window.__RAW.length;
|
|
296
|
+
})()
|
|
297
|
+
|
|
298
|
+
// Pass 2..N: slice
|
|
299
|
+
(() => window.__RAW.slice(0, 3000))()
|
|
300
|
+
(() => window.__RAW.slice(3000, 6000))()
|
|
301
|
+
// etc.
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
**SCRUB — neutralise blocked patterns on any string returned from `javascript_tool` (§2.1):**
|
|
305
|
+
|
|
306
|
+
```js
|
|
307
|
+
const SCRUB = (s) => String(s||'')
|
|
308
|
+
.replace(/eyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+/g, '<jwt>')
|
|
309
|
+
.replace(/[A-Za-z0-9+/]{40,}={0,2}/g, '<b64>')
|
|
310
|
+
.replace(/data:image\/[^"]+/g, '<data-image>')
|
|
311
|
+
.replace(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/g, '<uuid>')
|
|
312
|
+
.replace(/[a-f0-9]{32,}/g, '<hex>')
|
|
313
|
+
.replace(/(user|org|ins|sess|cli)_[A-Za-z0-9]{20,}/g, '.$1id.') // Clerk-style IDs
|
|
314
|
+
.replace(/\+91\d{10}/g, '<phone-in>') // Indian phone numbers
|
|
315
|
+
.replace(/\+1\d{10}/g, '<phone-us>') // US phone numbers
|
|
316
|
+
.replace(/\b\d{10,15}\b/g, '<phone>') // generic numeric phone
|
|
317
|
+
.replace(/[?&]([a-z_]+)=([^&\s"]+)/gi, (_,k,v) => '?'+k+'=<v>');
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**ULTRA scrub — last resort when SCRUB still isn't enough (agent-generated content matching the blocklist); loses JSON braces but keeps structural information (§2.1, §4.2):**
|
|
321
|
+
|
|
322
|
+
```js
|
|
323
|
+
const ULTRA = (s) => String(s||'').replace(/[^a-zA-Z0-9 _.,:!?\-/]/g,'.');
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## Dead-end recovery — when X is walled, try Y
|
|
329
|
+
|
|
330
|
+
When a surface is genuinely closed read-only, the failure mode is a recovery cue, not a stop. Each row pairs the wall with the pivot; the runnable ones carry the exact escape.
|
|
331
|
+
|
|
332
|
+
| When X is walled | …try Y | Anchor |
|
|
333
|
+
|---|---|---|
|
|
334
|
+
| **CORS-walled serverless RPC** (Cloud Functions / Cloud-Run, POST-only, preflight `TypeError: Failed to fetch`) | mine the **Firestore IndexedDB cache** — `collectionParents` (census), `remoteDocuments` (cached bodies+schemas), `targets` (active listeners) + `redux-persist` slices. `GET documents/users/{uid}` (200) confirms Bearer; `listCollectionIds` (403) is the negative control proving rules are active | §2.23 — Plai |
|
|
335
|
+
| **Onboarding gate blanks the surface** (every route → `/onboarding`, no live traffic) | string-mine the **un-code-split bundle** for the full API-path catalog + route table; fill confirmed schemas from pre-onboarding-reachable endpoints (brand-setup / reference / account) | §2.25 — Omneky |
|
|
336
|
+
| **`403 "Token is not valid."`** on a path | NOT broken auth — it's a **wrong-auth-scheme tripwire**: the app Bearer JWT is hitting a *public-developer-API* path on a dual-auth host. Switch scheme for that path, don't discard the token | §2.24 — Creatify |
|
|
337
|
+
| **Sleep/wait loop** (`wait → check → wait → check`) | trigger the action, **wait once, then query the history/trajectory endpoint** directly — server-side state beats babysitting client-side polling | §2.10 |
|
|
338
|
+
| **`cloned.text()` never resolves** (held-open long-poll) | don't chase streaming chunks — hit the `/history` (or `/state`, `/snapshot`, `/page-metadata`) endpoint instead | §2.14 / §1.8 |
|
|
339
|
+
| **JS-readable token cookie** tempts `credentials:'include'` | a readable cookie ≠ cookie-auth — probe **Bearer header / cookie / raw-token** all three to pin the real header model; the API often rejects the raw cookie under `ACAO:*` and requires explicit `Authorization: Bearer` | §2.26 |
|
|
340
|
+
| **`read_network_requests` shows only telemetry** (cross-origin XHRs / cache-served reload, GraphQL SPA) | pivot to the **Apollo/client cache** (`window.__APOLLO_CLIENT__.cache.extract()`) and **force a refetch** to capture verbatim wire bodies — don't conclude "no API calls" | §2.2 corollary / §1.13 |
|
|
341
|
+
| **Realtime hub URL absent from bundle grep** (runtime-constructed, e.g. SignalR `initializeHub`) | record realtime **presence + transport as confirmed**, URL + message contract as **open (needs Pass 2)** — never report "no realtime" on a grep miss | §2.27 — AdCreative |
|
|
342
|
+
| **Authoring/builder surface paywalled** (input/form schema hidden) | open the **run flow** instead (it fetches the richer record incl. the input form), capture the typed-input union, optionally run **one** cheap action to capture the write mutation + output record — deep server-side bits (system prompt, model) stay open, don't fabricate | §1.14 — Jasper |
|
|
343
|
+
| **GraphQL introspection disabled** (`{ __schema }` → 400 "introspection is not allowed") | fall back to **cache census + wire-body capture** — the POST body still carries the full query text; force a re-send via `refetchQueries` and the fetch tap captures it verbatim | §1.13 — Jasper |
|
|
344
|
+
|
|
345
|
+
**The "skip the wait" anchor (§1.8) — the engine of read-only Pass 1: once you have an entity ID, hit its `/history`/`/state`/`/snapshot`/`/page-metadata` endpoint with the page's existing token; one call returns an entire log / column catalog / state, equivalent to hours of streaming observation. Always check for it first.**
|
|
346
|
+
|
|
347
|
+
```js
|
|
348
|
+
(async () => {
|
|
349
|
+
// For Bearer-token products:
|
|
350
|
+
const tok = JSON.parse(localStorage.getItem('sb-auth-auth-token')).access_token;
|
|
351
|
+
const id = '<entity-uuid>';
|
|
352
|
+
const r = await fetch('https://api.example.com/.../' + id + '/history', {
|
|
353
|
+
headers: { 'authorization': 'Bearer ' + tok }
|
|
354
|
+
});
|
|
355
|
+
return await r.json();
|
|
356
|
+
})()
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
For same-origin cookie-auth products (Clerk, Next.js axios proxies):
|
|
360
|
+
|
|
361
|
+
```js
|
|
362
|
+
(async () => {
|
|
363
|
+
const r = await fetch('/api/conversations/page-metadata?agentIds=<uuid>', { credentials: 'include' });
|
|
364
|
+
return await r.json();
|
|
365
|
+
})()
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## GraphQL read-side mining + classification fingerprint tables
|
|
371
|
+
|
|
372
|
+
Two reusable assets for the `session` dimension (ingestion §9.4 runtime taps). Asset 1 is an **executable read-side recipe** for any Apollo/GraphQL SPA; Asset 2 is a set of **classification tables** that turn a raw observation (a storage key, a host, an envelope shape) into a *named* stack finding. Every code block below is runnable verbatim in the page console / in-page tap — do not paraphrase it into prose.
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
### 1. The Apollo-cache read-side recipe (the richest Pass-1 GraphQL source)
|
|
377
|
+
|
|
378
|
+
> **Why this is the richest Pass-1 GraphQL source.** For Apollo-backed SPAs (look for `window.__APOLLO_CLIENT__`), the **normalized client cache is the single richest read-only source** — often better than network capture, which may miss cross-origin GraphQL XHRs and won't fire at all when data is served from cache on reload (a real run on Jasper saw the network panel show only telemetry because the reload was served entirely from the Apollo cache). The cache holds a normalized entity census, every entity's field schema, and enum distributions across all entities — the whole read-side data model — with **zero network cost and no state change**. Introspection is almost always disabled in prod (`{ __schema { ... } }` → `400 "introspection is not allowed"`), so the cache census + verbatim wire-body capture *is* the schema-recovery path, not a fallback.
|
|
379
|
+
|
|
380
|
+
### 1a. Entity census, field schema, and enum distributions
|
|
381
|
+
|
|
382
|
+
Dump the normalized cache, count entities per type, decode one entity of a type to get its field schema, and compute enum distributions across all entities of that type:
|
|
383
|
+
|
|
384
|
+
```js
|
|
385
|
+
// 1) entity census — what types exist and how many
|
|
386
|
+
const ex = window.__APOLLO_CLIENT__.cache.extract();
|
|
387
|
+
const counts = {}; Object.keys(ex).forEach(k => { const t = k.split(':')[0]; counts[t] = (counts[t]||0)+1; });
|
|
388
|
+
// 2) decode one entity of a type = its field schema
|
|
389
|
+
const sample = ex[Object.keys(ex).find(k => k.startsWith('Task:'))];
|
|
390
|
+
// 3) enum distributions across all entities of a type
|
|
391
|
+
const tasks = Object.keys(ex).filter(k=>k.startsWith('Task:')).map(k=>ex[k]);
|
|
392
|
+
const dist = f => tasks.reduce((c,t)=>(c[t[f]]=(c[t[f]]||0)+1,c),{});
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### 1b. List observable queries (operation names + variables + AST)
|
|
396
|
+
|
|
397
|
+
Recover every live operation's name, variable keys, and selection-set AST — even when the production build has stripped the query source text:
|
|
398
|
+
|
|
399
|
+
```js
|
|
400
|
+
const arr = [...window.__APOLLO_CLIENT__.getObservableQueries('all').values()];
|
|
401
|
+
arr.map(q => { const d = q.query || q.options.query;
|
|
402
|
+
return { name: d.definitions.find(z=>z.operation)?.name?.value,
|
|
403
|
+
vars: Object.keys(q.variables||{}),
|
|
404
|
+
sel: d.definitions[0].selectionSet /* walk for fields */ }; });
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### 1c. Force a refetch to capture verbatim query/mutation text off the wire
|
|
408
|
+
|
|
409
|
+
Production strips `DocumentNode.loc.source.body`, but the **POST body still contains the full query**. Install a fetch tap that keeps `init.body`, then **force a re-send** so the tap captures the exact query string + fragments + variables:
|
|
410
|
+
|
|
411
|
+
```js
|
|
412
|
+
await window.__APOLLO_CLIENT__.refetchQueries({ include: ['Web_Library'] });
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
The fetch tap then holds the verbatim query/mutation text + fragments + variables for that operation. (This is how the Jasper `Web_Library` / `Web_CreateAppSnapshot` queries and the 11-type `ContextItem` union were recovered.)
|
|
416
|
+
|
|
417
|
+
> **Other GraphQL clients:** `urql` exposes `__URQL_CLIENT__`; Relay stores an environment with a `Store` — apply the same census → operations → force-refetch pattern against those handles.
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
### 2. Cross-cutting inference / fingerprint tables (classify what a finding *is*)
|
|
422
|
+
|
|
423
|
+
These tables let a raw observation be **classified** — a storage key prefix names the stack, a key+host pair names the auth provider, an envelope shape names the backend framework, and the realtime-trap table stops a run concluding after one channel.
|
|
424
|
+
|
|
425
|
+
### 2.1 Storage-key prefix → stack inference
|
|
426
|
+
|
|
427
|
+
Inferences from `localStorage` / `sessionStorage` / cookie / IndexedDB **key names alone** (no values):
|
|
428
|
+
|
|
429
|
+
| Prefix | Stack signal |
|
|
430
|
+
|--------|--------------|
|
|
431
|
+
| `sb-*-auth-*` | Supabase Auth |
|
|
432
|
+
| `firebase:*`, `firebaseLocalStorage` (in IndexedDB) | Firebase Auth + Firestore |
|
|
433
|
+
| `clerk-*`, `__clerk*` | **Clerk** |
|
|
434
|
+
| `__client_uat*`, `__session*` (cookies) | Clerk session cookies |
|
|
435
|
+
| `auth0.*` | Auth0 |
|
|
436
|
+
| `persist:*` | Redux Persist |
|
|
437
|
+
| `_pinia_*` | Pinia (Vue state) |
|
|
438
|
+
| `apollo-cache-persist` | Apollo Client |
|
|
439
|
+
| `i18next*` | i18next |
|
|
440
|
+
| `ph_phc_*` | PostHog |
|
|
441
|
+
| `_ga*`, `_gid` | Google Analytics |
|
|
442
|
+
| `__mpq_*`, `mp_*_mixpanel` | **Mixpanel** |
|
|
443
|
+
| `_fbp` | Facebook Pixel |
|
|
444
|
+
| `intercom-*` | Intercom |
|
|
445
|
+
| `pusher*` | Pusher |
|
|
446
|
+
| `ably-*` | Ably |
|
|
447
|
+
| `rzp_*` | Razorpay |
|
|
448
|
+
| `stripe.*` | Stripe |
|
|
449
|
+
| `<something>-prod-*.a.run.app` (in network) | Google Cloud Run |
|
|
450
|
+
| `*.appspot.com` | Google App Engine |
|
|
451
|
+
| `*.lambda-url.*.amazonaws.com` | AWS Lambda Function URL |
|
|
452
|
+
| `ov-*.ecs.us-west-2.on.aws` | AWS ECS Fargate |
|
|
453
|
+
| `accounts.<root>.com`, `clerk.<root>.com` | Clerk on a Satellite Domain (enterprise; cookies are first-party) |
|
|
454
|
+
| `static.prod-images.*` | Public CDN for screenshots/assets |
|
|
455
|
+
|
|
456
|
+
> **Don't forget IndexedDB.** Firebase Modular SDK v9+ stores tokens in IndexedDB (`firebaseLocalStorageDb` / `firebaseLocalStorage` object store), **not** localStorage. A "nothing auth-related in localStorage" result is a finding pointing you at IndexedDB next, not a dead end.
|
|
457
|
+
|
|
458
|
+
### 2.2 Auth-provider fingerprint (storage keys + hosts → provider)
|
|
459
|
+
|
|
460
|
+
| Provider pattern | Tells (storage keys + hosts + token shape) |
|
|
461
|
+
|------------------|--------------------------------------------|
|
|
462
|
+
| **Supabase Auth, gateway-validated** | `sb-*-auth-token` in localStorage, HS256, **no `apikey` header** on calls (= a gateway sits in front) |
|
|
463
|
+
| **Firebase Auth, IndexedDB-stored** | `firebaseLocalStorageDb` IndexedDB object store, RS256, ~1-hour token, auto-refreshed |
|
|
464
|
+
| **Clerk** (standard) | `__clerk_db_jwt`, `__session` cookie, `clerk-*` localStorage |
|
|
465
|
+
| **Clerk Enterprise** (Satellite/Proxy Domain) | Clerk JS hosted on `clerk.<root>` (not `clerk.com`); JWT issuer is the proxy domain; cookies are first-party |
|
|
466
|
+
| **Auth0 / OIDC** | `auth0.*` localStorage, RS256, often hybrid (cookie + Bearer) |
|
|
467
|
+
| **WorkOS / Stytch / Ory** | Passwordless flows, often opaque tokens, magic-link or SSO |
|
|
468
|
+
| **Cognito / AWS Amplify** | `CognitoIdentityServiceProvider.*` localStorage, RS256 |
|
|
469
|
+
| **Self-hosted IdentityServer (Duende/IdentityServer4) OIDC** | `at+jwt` RS256 carrying short claims + classic WS-Federation claim URIs; very long TTL (~180 d); token is a **field inside a localStorage JSON blob** (`oidc-client-ts` idiom), not a bare key |
|
|
470
|
+
| **Self-hosted JWT** | Custom claims, in-house signing, often HS256 (worst posture: multi-year exp, single shared secret across a host fleet, no refresh) |
|
|
471
|
+
|
|
472
|
+
**Auth-token-location taxonomy** (*where* the token lives is a distinct axis from *who* issued it — it drives the XSS-reachability read):
|
|
473
|
+
|
|
474
|
+
| Token location | Shape / retrieval tell | XSS-reachable? |
|
|
475
|
+
|----------------|------------------------|----------------|
|
|
476
|
+
| **Bearer-from-cookie** (JS-readable cookie → explicit `Authorization: Bearer`) | Non-HttpOnly cookie the SPA reads via `document.cookie` and re-attaches as a Bearer header; `credentials:'include'` with the raw cookie is **rejected** (forced by `ACAO:*`). Probe Bearer vs cookie vs raw-token to confirm. | Yes |
|
|
477
|
+
| **Token-in-localStorage-blob** | Token is a *field inside a JSON object* in localStorage (`JSON.parse` then read `.access_token`), not a bare key — OIDC `oidc-client-ts` idiom. | Yes |
|
|
478
|
+
| **Opaque session cookie** (HttpOnly, same-origin, auto-attached) | No `Authorization` header anywhere on the SPA; the cookie auto-attaches because all API is same-origin. **The negative finding is the tell.** | No |
|
|
479
|
+
| **Firebase ID token in IndexedDB** | `firebaseLocalStorageDb` → `stsTokenManager.accessToken`; Firebase Modular SDK v9+ store; short-lived (3600 s) + auto-refreshed. Confirm the Bearer model with one Firestore REST `GET documents/users/{uid}`. | Partially (IDB is JS-reachable, but token is short-lived + refreshed) |
|
|
480
|
+
|
|
481
|
+
**Key claims to look for in the JWT payload** (decode *structure* only — redact `sub`/`email`/`session_id`/`provider_id`, never log the signature):
|
|
482
|
+
|
|
483
|
+
| Claim | What it tells you |
|
|
484
|
+
|-------|-------------------|
|
|
485
|
+
| `iss` | Issuer URL — names the auth provider (a custom domain → enterprise tier) |
|
|
486
|
+
| `org_id`, `org_slug`, `org_role`, `org_permissions` | **Tenant context inside the token** — backend does RBAC from the verified JWT alone, no separate org lookup (standard with Clerk Orgs) |
|
|
487
|
+
| `azp` | Authorised party — typically the SPA origin |
|
|
488
|
+
| `fva` (Clerk-specific) | Factor verification age — gates "re-verify your password to do X" actions |
|
|
489
|
+
| Lifetime (`exp - iat`) | Short (≤5 min) → tight refresh, enterprise-leaning. Long (≥1 day) → loose; cookie-only or large-hop systems |
|
|
490
|
+
|
|
491
|
+
### 2.3 API-envelope / style fingerprint (response shape → backend framework)
|
|
492
|
+
|
|
493
|
+
| Style | Tells (envelope / endpoint shape) |
|
|
494
|
+
|-------|-----------------------------------|
|
|
495
|
+
| **JSON-Schema-driven (Huma / similar)** | Every response carries inline `"$schema": "https://api.x.com/<TypeName>OutputBody.json"`; RFC 7807 error envelope |
|
|
496
|
+
| **Hand-rolled FastAPI / Express** | No `$schema` field; minimal error shape (`{detail: "..."}`) |
|
|
497
|
+
| **GraphQL** | Single `/graphql` endpoint, query/variables in body, often `apollo-cache-persist` storage |
|
|
498
|
+
| **gRPC-Web / Connect / tRPC** | `application/grpc-web*` content-type, or `/trpc/<procedure>` endpoints |
|
|
499
|
+
| **OData** | `$filter`, `$expand`, `$select` query params |
|
|
500
|
+
| **JSON-API** | `data.attributes` / `data.relationships` envelope |
|
|
501
|
+
| **Next.js API-route axios proxy** | Same-origin `/api/*` route handlers fronting upstream microservices. Tell: 500s carry verbatim axios string `"Request failed with status code 404"` |
|
|
502
|
+
| **Spring Data Pageable** (Java/Spring) | `{content, pageable, totalPages, totalElements, last, numberOfElements, size, number, sort, first, empty}` envelope |
|
|
503
|
+
| **ASP.NET Core CQRS/MediatR** | `{response, correlationId, isSuccess, failures[]}` envelope; may coexist with legacy raw-JSON (in-progress migration); PascalCase resource paths off root (`/User`, `/Brand`) with no `/api` prefix |
|
|
504
|
+
| **DRF (Django REST Framework)** | `{count, next, previous, results[]}` pagination; `@action` verb routes |
|
|
505
|
+
| **Firestore-as-the-API (serverless, read/write split)** | No read REST for app data — SPA subscribes to Firestore docs (WebChannel) for reads; writes are POST-only Cloud Function / RPC (often CORS-walled from the SPA origin); entitlement in a Firestore field, not token claims. Mine the Firestore IndexedDB cache (`collectionParents`/`remoteDocuments`/`targets`) instead of the network. |
|
|
506
|
+
|
|
507
|
+
> **Multi-stack envelope detection:** if one product returns multiple distinct envelope shapes across endpoints, that signals multiple upstream services with different stacks (a real run saw **6 distinct envelopes = 4–6 microservices**). Naming conventions corroborate — snake_case (Python/FastAPI) vs camelCase (Java/Spring).
|
|
508
|
+
|
|
509
|
+
### 2.4 The multi-channel-realtime trap (don't conclude after finding one)
|
|
510
|
+
|
|
511
|
+
A single product often uses **multiple** realtime channels with different roles — install **all** taps simultaneously *before* triggering, and let each make its own claim. Two architectural patterns to name first: **content-streaming** (frames carry the actual deltas — text, edits) vs **cache-coherence-ping** (frames carry only metadata like `{job_id, step_num, function_name}`; the SPA re-fetches a REST endpoint after each ping).
|
|
512
|
+
|
|
513
|
+
**Channel-role split (the trap):** a realtime product commonly runs (1) a content/op stream **plus** (2) a REST refetch for snapshots **plus** (3) a separate presence channel — three channels, three roles. **A dashboard that is REST-only (no WS, no SSE, no long-poll) is a real architectural signal, not a miss** — record the negative.
|
|
514
|
+
|
|
515
|
+
**Streaming-protocol detection table:**
|
|
516
|
+
|
|
517
|
+
| Pattern | Tells (host / content-type / frame shape) |
|
|
518
|
+
|---------|-------------------------------------------|
|
|
519
|
+
| **WebSocket** | `wss://` URLs in the network panel — wrap the `WebSocket` constructor |
|
|
520
|
+
| **Server-Sent Events via `EventSource`** | `text/event-stream` content-type **and** `EventSource` constructor used — wrap `EventSource` |
|
|
521
|
+
| **HTTP long-poll, SSE-formatted body via `fetch`** | Repeated GETs with `?last_*=` / `?cursor=`; `text/event-stream` content-type but **no** `EventSource` constructor — fetch response-body tap |
|
|
522
|
+
| **HTTP long-poll, JSON body** | Repeated identical-URL GETs, JSON responses — fetch response-body tap |
|
|
523
|
+
| **WebSocket, content-streaming** | `wss://`, frames carry actual deltas |
|
|
524
|
+
| **WebSocket, cache-coherence-ping** | Tiny WS frames `{job_id, step_num, function_name}`; SPA refetches REST `/history` after each |
|
|
525
|
+
| **SSE-via-fetch, content-streaming** | `GET .../latest-message` with `text/event-stream`; held open for one message |
|
|
526
|
+
| **Pusher / Ably / Pubnub** | `wss://ws-*.pusher.com/...`, `realtime.ably.io/...` — WebSocket constructor wrap + library global tap; events namespaced per channel (`user-{uuid}`, `workspace-{id}`, `presence-doc-{id}`) |
|
|
527
|
+
| **Firestore WebChannel** | `firestore.googleapis.com/.../Listen/channel` GET-with-streamed-body, `text/plain` |
|
|
528
|
+
| **Firebase RTDB WebSocket** | `wss://*.firebaseio.com/.ws?ns=<project>` (often presence only) |
|
|
529
|
+
| **GraphQL Subscriptions** | `wss://...` with `connection_init` / `subscribe` / `next` / `complete` frames (Apollo/Hasura) |
|
|
530
|
+
| **gRPC-Web / Connect** | `application/grpc-web*` content-type — fetch response-body tap (binary) |
|
|
531
|
+
| **WS-OT/CRDT (Yjs, Automerge)** | Binary-framed WS, small + bursty frame sizes |
|
|
532
|
+
| **No streaming on dashboard** (per-call only) | Voice/per-session platforms: dashboard is REST-only; realtime lives only at the per-call layer (WebRTC + data channel), never visible from admin pages |
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# hr — AI-Steering Evaluation harness
|
|
2
|
+
|
|
3
|
+
This repo evaluates **how a person steered a Claude Code session** — for interview and hiring
|
|
4
|
+
assessment. You read session transcripts (`.jsonl`) and produce a calibrated, evidence-anchored read
|
|
5
|
+
on the candidate's ability to drive an AI coding agent.
|
|
6
|
+
|
|
7
|
+
You are the **evaluator**. The work is the `interview-eval` skill (its `rubric.md` holds the band
|
|
8
|
+
anchors, gates, and session-type matrix; `extract_session.py` is the deterministic extractor;
|
|
9
|
+
`TEMPLATE.md` holds the output shapes). This `CLAUDE.md` is the **policy** over that skill: the
|
|
10
|
+
pipeline, the discipline that keeps scores honest, and the things you must never do.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## The one rule above all others
|
|
15
|
+
|
|
16
|
+
**You score the human, not the model.** Claude writing clean code earns the candidate nothing;
|
|
17
|
+
Claude writing buggy code costs them nothing — *unless* the candidate's prompt caused it or they
|
|
18
|
+
failed to catch it. Reward the prompt that set the agent up, the verification that caught its
|
|
19
|
+
mistake, the redirect that pulled it off a bad path. Turn count, prompt length, and verbosity are
|
|
20
|
+
**not** quality — precision, judgment, and verification are. Re-read this before every scoring pass.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Step 0 — first run
|
|
25
|
+
|
|
26
|
+
If the `interview-eval` skill isn't resolvable (no `.claude/skills/interview-eval/`), this project isn't set
|
|
27
|
+
up — tell the user to run `npx @nurix/etna hr` (or point you at the skill) before continuing. Confirm
|
|
28
|
+
`python3` is on PATH; the extractor is stdlib-only and needs nothing else.
|
|
29
|
+
|
|
30
|
+
## Step 1 — intake interview
|
|
31
|
+
|
|
32
|
+
Before scoring anything, establish scope with a short `AskUserQuestion` round (skip a question if the
|
|
33
|
+
user already answered it):
|
|
34
|
+
|
|
35
|
+
1. **What am I evaluating?** → one candidate (one or many sessions) · several candidates · a cohort.
|
|
36
|
+
2. **What context governs expectations?** → the candidate's target role/seniority, the task brief
|
|
37
|
+
they were given, and any time box. This adjusts *expectations* (a time-boxed take-home weights
|
|
38
|
+
efficiency up, polish down) — it never changes the rubric's absolute band anchors.
|
|
39
|
+
3. **Where do the transcripts live, and where should output go?** → input `.jsonl` paths / a folder,
|
|
40
|
+
and an `out_dir` (default `./interview-eval-out/`).
|
|
41
|
+
|
|
42
|
+
Keep it to those. Carry the answers forward; don't re-interview per session.
|
|
43
|
+
|
|
44
|
+
## Step 2 — run the pipeline (delegate to `interview-eval`)
|
|
45
|
+
|
|
46
|
+
For each candidate, invoke the `interview-eval` skill. It owns the mechanics; your job is to drive it and
|
|
47
|
+
hold the discipline below. The flow it runs:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
Extract → python3 extract_session.py SESSION.jsonl --out <out_dir>/<slug>/ --json
|
|
51
|
+
│ → read the FULL evidence pack before scoring; open raw .jsonl around any turn you quote
|
|
52
|
+
Classify → dominant session type (greenfield / feature / debugging / refactor / ops / research / review)
|
|
53
|
+
│ → sets which dimensions are primary / secondary / N/A
|
|
54
|
+
Score → 9 dimensions × 0–5 absolute bands, ≥2 quoted turn cites each; gates; green flags
|
|
55
|
+
│ → <out_dir>/<slug>/{report.md, scorecard.yaml}
|
|
56
|
+
Roll up → if >1 session: dimension trends + consistency + one calibrated level
|
|
57
|
+
→ <out_dir>/candidate-rollup.md
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
After the rollup, **always produce a one-page candidate brief** (verdict + level, the standout
|
|
61
|
+
evidence, the single biggest risk, and 3–4 interview follow-ups that would confirm or kill that risk).
|
|
62
|
+
That brief is the deliverable a hiring manager reads first.
|
|
63
|
+
|
|
64
|
+
## Step 3 — calibration discipline (non-negotiable)
|
|
65
|
+
|
|
66
|
+
- **Evidence or it didn't happen.** Every band cites real `U<n>` turns with short verbatim quotes.
|
|
67
|
+
"Seems thorough" is not a finding.
|
|
68
|
+
- **Absolute bands, not a curve.** Score each session against `rubric.md`'s anchors. With multiple
|
|
69
|
+
candidates you may *report* relative ranking, but each band is assigned absolutely.
|
|
70
|
+
- **Exclude harness noise.** API 529s / connection drops are Anthropic-side — never penalize them.
|
|
71
|
+
Tool-permission prompts and model slips the human then caught are not candidate failures.
|
|
72
|
+
Distinguish a sharp early interrupt (good) from thrash (bad) by reading the surrounding turns.
|
|
73
|
+
- **Gates require human direction.** A gate fires only on confirmed *unsafe steering* the human
|
|
74
|
+
directed or ratified (pasted a live secret, approved a destructive op unchecked, knowingly shipped
|
|
75
|
+
broken output) — not on a model mistake. Verify the auto-detected red-flag leads against the
|
|
76
|
+
transcript before firing one.
|
|
77
|
+
- **Charity with a spine.** Read terse prompts and typos in the best plausible light, but never
|
|
78
|
+
invent verification or judgment the transcript doesn't show.
|
|
79
|
+
|
|
80
|
+
## What you do *not* do
|
|
81
|
+
|
|
82
|
+
- **You don't decide the hire.** You produce a level and a brief; the human decides. Frame the level
|
|
83
|
+
as a calibrated lens, never a verdict.
|
|
84
|
+
- **You don't mutate the source transcripts.** The skill is read-only over `.jsonl`; you only write
|
|
85
|
+
into `out_dir`.
|
|
86
|
+
- **You don't score Claude's code quality as if it were the candidate's.** If the agent proposed the
|
|
87
|
+
good design and the human only assented, say so — that's weaker judgment than originating it.
|
|
88
|
+
- **You don't fabricate a score from thin evidence.** A one-turn fragment or an empty/corrupt
|
|
89
|
+
transcript gets a stub that says so and defers to the rollup — not an invented band.
|
|
90
|
+
|
|
91
|
+
Every output lands under `out_dir` and is plain markdown/YAML — reviewable, quotable, and reversible.
|