explorbot 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/bin/explorbot-cli.ts +18 -13
  2. package/boat/doc-collector/src/cli.ts +3 -0
  3. package/boat/doc-collector/src/docbot.ts +3 -1
  4. package/boat/prima/src/cli.ts +21 -8
  5. package/boat/prima/src/envelope.ts +35 -9
  6. package/boat/prima/src/prima.ts +23 -10
  7. package/dist/bin/explorbot-cli.js +19 -13
  8. package/dist/boat/doc-collector/src/cli.js +3 -0
  9. package/dist/boat/doc-collector/src/docbot.js +3 -1
  10. package/dist/boat/prima/src/cli.js +19 -8
  11. package/dist/boat/prima/src/envelope.js +24 -6
  12. package/dist/boat/prima/src/prima.js +23 -11
  13. package/dist/package.json +2 -2
  14. package/dist/src/action-result.d.ts +9 -1
  15. package/dist/src/action-result.js +57 -18
  16. package/dist/src/action.d.ts +1 -1
  17. package/dist/src/action.js +87 -12
  18. package/dist/src/ai/driller.d.ts +0 -1
  19. package/dist/src/ai/driller.js +8 -20
  20. package/dist/src/ai/fisherman-tools.d.ts +9 -0
  21. package/dist/src/ai/fisherman-tools.js +52 -6
  22. package/dist/src/ai/fisherman.d.ts +4 -2
  23. package/dist/src/ai/fisherman.js +48 -27
  24. package/dist/src/ai/historian/codeceptjs.js +1 -1
  25. package/dist/src/ai/historian/playwright.js +1 -1
  26. package/dist/src/ai/pilot.d.ts +1 -0
  27. package/dist/src/ai/pilot.js +15 -1
  28. package/dist/src/ai/planner.js +1 -1
  29. package/dist/src/ai/provider.js +47 -4
  30. package/dist/src/ai/researcher/deep-analysis.js +1 -3
  31. package/dist/src/ai/researcher.js +3 -3
  32. package/dist/src/ai/tester.d.ts +3 -0
  33. package/dist/src/ai/tester.js +40 -3
  34. package/dist/src/ai/tools.d.ts +1 -0
  35. package/dist/src/ai/tools.js +13 -6
  36. package/dist/src/api/request-result.d.ts +2 -0
  37. package/dist/src/api/request-result.js +8 -2
  38. package/dist/src/api/request-store.d.ts +3 -2
  39. package/dist/src/api/request-store.js +66 -14
  40. package/dist/src/commands/explore-command.d.ts +6 -0
  41. package/dist/src/commands/explore-command.js +27 -2
  42. package/dist/src/commands/freesail-command.js +10 -1
  43. package/dist/src/commands/plans-command.js +6 -6
  44. package/dist/src/config.js +1 -0
  45. package/dist/src/experience-tracker.js +5 -0
  46. package/dist/src/explorbot.d.ts +0 -1
  47. package/dist/src/explorbot.js +23 -36
  48. package/dist/src/state-manager.d.ts +5 -1
  49. package/dist/src/state-manager.js +10 -7
  50. package/dist/src/test-plan.d.ts +3 -0
  51. package/dist/src/test-plan.js +27 -0
  52. package/dist/src/utils/aria.d.ts +1 -1
  53. package/dist/src/utils/aria.js +6 -42
  54. package/dist/src/utils/html-diff.d.ts +4 -0
  55. package/dist/src/utils/html-diff.js +62 -7
  56. package/dist/src/utils/html.d.ts +5 -15
  57. package/dist/src/utils/html.js +14 -85
  58. package/dist/src/utils/overlay.d.ts +56 -11
  59. package/dist/src/utils/overlay.js +191 -21
  60. package/dist/src/utils/request-map.d.ts +7 -0
  61. package/dist/src/utils/request-map.js +16 -0
  62. package/dist/src/utils/url-matcher.js +4 -2
  63. package/docs/reference/commands.md +8 -1
  64. package/docs/reference/websocket.md +1 -0
  65. package/docs/superpowers/plans/2026-08-29-fisherman-reliability.md +953 -0
  66. package/docs/superpowers/plans/2026-08-29-region-states.md +1292 -0
  67. package/docs/superpowers/plans/2026-08-30-fisherman-live-session-auth.md +457 -0
  68. package/docs/superpowers/specs/2026-08-29-fisherman-reliability-design.md +45 -0
  69. package/docs/superpowers/specs/2026-08-29-region-states-design.md +262 -0
  70. package/docs/superpowers/specs/2026-08-29-region-states-fixes-design.md +269 -0
  71. package/docs/superpowers/specs/2026-08-30-fisherman-live-session-auth-design.md +37 -0
  72. package/docs/workflow/agentic-usage.md +1 -0
  73. package/docs/workflow/ci.md +1 -0
  74. package/package.json +2 -2
  75. package/src/action-result.ts +61 -22
  76. package/src/action.ts +87 -14
  77. package/src/ai/driller.ts +7 -39
  78. package/src/ai/fisherman-tools.ts +56 -7
  79. package/src/ai/fisherman.ts +48 -28
  80. package/src/ai/historian/codeceptjs.ts +1 -1
  81. package/src/ai/historian/playwright.ts +1 -1
  82. package/src/ai/pilot.ts +11 -1
  83. package/src/ai/planner.ts +1 -1
  84. package/src/ai/provider.ts +48 -4
  85. package/src/ai/researcher/deep-analysis.ts +1 -2
  86. package/src/ai/researcher.ts +3 -3
  87. package/src/ai/tester.ts +40 -3
  88. package/src/ai/tools.ts +17 -9
  89. package/src/api/request-result.ts +10 -2
  90. package/src/api/request-store.ts +60 -13
  91. package/src/commands/explore-command.ts +25 -2
  92. package/src/commands/freesail-command.ts +7 -1
  93. package/src/commands/plans-command.ts +6 -6
  94. package/src/config.ts +1 -0
  95. package/src/experience-tracker.ts +5 -1
  96. package/src/explorbot.ts +20 -36
  97. package/src/state-manager.ts +13 -7
  98. package/src/test-plan.ts +29 -0
  99. package/src/utils/aria.ts +7 -44
  100. package/src/utils/html-diff.ts +62 -7
  101. package/src/utils/html.ts +14 -91
  102. package/src/utils/overlay.ts +226 -23
  103. package/src/utils/request-map.ts +19 -0
  104. package/src/utils/url-matcher.ts +3 -2
@@ -0,0 +1,457 @@
1
+ # Fisherman Live-Session Auth Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** In replicate mode, Fisherman API requests carry the current browser session's credentials — cookies filtered to the API origin and a live CSRF token — and never a credential scraped from a previous session's captures. Achieve mode keeps authenticating solely through `api.headers` config.
6
+
7
+ **Architecture:** Three layers change. `RequestStore.extractAuthHeaders` gains a session gate (only captures made during this run, newest first) and stops scraping cookies. `Fisherman.refreshAuth` reorders precedence to captured < live browser < config. The DI-glue provider in `explorbot.ts` filters the cookie jar by the API origin and additionally reads the page's `meta[name="csrf-token"]`.
8
+
9
+ **Tech Stack:** Bun, TypeScript, Playwright (via Explorer.withPage), bun:test, @copilotkit/aimock for the integration test.
10
+
11
+ **Spec:** `docs/superpowers/specs/2026-08-30-fisherman-live-session-auth-design.md`
12
+
13
+ ## Global Constraints
14
+
15
+ - Bun only — never Node.js. Run tests with `bun test`.
16
+ - Work in the worktree `/home/davert/projects/explorbot-fisherman-reliability`, branch `fisherman-reliability` (continues PR #160 — do NOT open a new PR).
17
+ - No code comments. No ternary operators. Premature exit over if/else.
18
+ - Run `bun run format` after each code change, before each commit.
19
+ - NEVER trigger the regression CI workflow (no `regression` label, no `gh workflow run regression.yml`). Only the user does that.
20
+ - Task 4 is severable: if the user cuts it, Tasks 1–3 stand alone and the provider keeps its `cookieProvider` name.
21
+
22
+ ---
23
+
24
+ ### Task 1: Session-gated auth header extraction (RequestStore)
25
+
26
+ **Files:**
27
+ - Modify: `src/api/request-store.ts:6` (AUTH_HEADERS), `src/api/request-store.ts:98-112` (extractAuthHeaders), field near `src/api/request-store.ts:9-16`
28
+ - Modify: `src/api/request-result.ts:176` (timestamp fallback in `RequestResult.load`)
29
+ - Test: `tests/unit/request-store.test.ts`
30
+
31
+ **Interfaces:**
32
+ - Consumes: existing `RequestResult` (`timestamp: Date`, `requestHeaders: Record<string, string>`), `RequestStore.addCapturedRequest`, `RequestStore.loadFromDisk`.
33
+ - Produces: `extractAuthHeaders(): Record<string, string>` — same signature, but returns only `authorization` / `x-api-key` / `x-csrf-token` headers from captures made during this session, newest value first. Task 2 relies on this being safe to apply before live browser headers.
34
+
35
+ - [ ] **Step 1: Write the failing tests**
36
+
37
+ Extend the `makeRequest` helper in `tests/unit/request-store.test.ts` with an optional headers argument:
38
+
39
+ ```ts
40
+ let counter = 0;
41
+ function makeRequest(method: string, path: string, status: number, id?: string, headers: Record<string, string> = {}): RequestResult {
42
+ counter++;
43
+ return new RequestResult({
44
+ id: id || `req_${counter}`,
45
+ method,
46
+ path,
47
+ fullUrl: path,
48
+ requestHeaders: headers,
49
+ status,
50
+ statusText: String(status),
51
+ responseHeaders: {},
52
+ timing: 0,
53
+ timestamp: new Date(),
54
+ });
55
+ }
56
+ ```
57
+
58
+ Add a new describe block at the end of the file (before the final `loadFromDisk` block is fine too — position does not matter):
59
+
60
+ ```ts
61
+ describe('extractAuthHeaders session gating', () => {
62
+ let outputDir: string;
63
+
64
+ beforeEach(() => {
65
+ outputDir = mkdtempSync(join(tmpdir(), 'reqstore-'));
66
+ });
67
+
68
+ afterEach(() => {
69
+ if (existsSync(outputDir)) rmSync(outputDir, { recursive: true, force: true });
70
+ });
71
+
72
+ it('ignores auth headers from captures of previous sessions', () => {
73
+ const stale = makeRequest('DELETE', '/api/old-project/suites', 200, 'xhr_033_DELETE_api_old', { 'x-csrf-token': 'stale-token' });
74
+ stale.timestamp = new Date('2026-07-07');
75
+ stale.save(outputDir);
76
+
77
+ const store = new RequestStore(outputDir);
78
+ store.loadFromDisk();
79
+
80
+ expect(store.extractAuthHeaders()).toEqual({});
81
+ });
82
+
83
+ it('returns auth headers from captures made during this session', () => {
84
+ const store = new RequestStore(outputDir);
85
+ store.addCapturedRequest(makeRequest('POST', '/api/suites', 201, undefined, { authorization: 'Bearer live', 'x-csrf-token': 'live-token' }));
86
+
87
+ expect(store.extractAuthHeaders()).toEqual({ authorization: 'Bearer live', 'x-csrf-token': 'live-token' });
88
+ });
89
+
90
+ it('never returns cookie headers from captures', () => {
91
+ const store = new RequestStore(outputDir);
92
+ store.addCapturedRequest(makeRequest('POST', '/api/suites', 201, undefined, { cookie: 'session=captured', 'x-csrf-token': 'live-token' }));
93
+
94
+ expect(store.extractAuthHeaders()).toEqual({ 'x-csrf-token': 'live-token' });
95
+ });
96
+
97
+ it('prefers the newest session capture when values differ', () => {
98
+ const store = new RequestStore(outputDir);
99
+ const older = makeRequest('POST', '/api/suites', 201, undefined, { 'x-csrf-token': 'first' });
100
+ older.timestamp = new Date(Date.now() + 1000);
101
+ const newer = makeRequest('POST', '/api/tests', 201, undefined, { 'x-csrf-token': 'second' });
102
+ newer.timestamp = new Date(Date.now() + 2000);
103
+ store.addCapturedRequest(older);
104
+ store.addCapturedRequest(newer);
105
+
106
+ expect(store.extractAuthHeaders()).toEqual({ 'x-csrf-token': 'second' });
107
+ });
108
+
109
+ it('resolves a same-id collision between a stale disk file and a live capture', () => {
110
+ const stale = makeRequest('POST', '/api/suites', 201, 'xhr_001_POST_api_suites', { 'x-csrf-token': 'stale-token' });
111
+ stale.timestamp = new Date('2026-07-07');
112
+ stale.save(outputDir);
113
+
114
+ const store = new RequestStore(outputDir);
115
+ store.loadFromDisk();
116
+ store.addCapturedRequest(makeRequest('POST', '/api/suites', 201, 'xhr_001_POST_api_suites', { 'x-csrf-token': 'live-token' }));
117
+
118
+ expect(store.extractAuthHeaders()).toEqual({ 'x-csrf-token': 'live-token' });
119
+ });
120
+
121
+ it('treats a capture file without a timestamp as stale', () => {
122
+ const requestsDir = join(outputDir, 'requests');
123
+ mkdirSync(requestsDir, { recursive: true });
124
+ writeFileSync(join(requestsDir, 'xhr_002_POST_api_x.request.yaml'), '---\nmethod: POST\nurl: /api/x\nfullUrl: /api/x\nheaders:\n x-csrf-token: orphan\nstatus: 200\nstatusText: OK\nresponseHeaders:\n---\n', 'utf8');
125
+
126
+ const store = new RequestStore(outputDir);
127
+ store.loadFromDisk();
128
+
129
+ expect(store.extractAuthHeaders()).toEqual({});
130
+ });
131
+ });
132
+ ```
133
+
134
+ Update the file's imports: add `mkdirSync, writeFileSync` to the `node:fs` import.
135
+
136
+ - [ ] **Step 2: Run the tests to verify they fail**
137
+
138
+ Run: `cd /home/davert/projects/explorbot-fisherman-reliability && bun test tests/unit/request-store.test.ts`
139
+ Expected: the first, third, and sixth new tests FAIL (the stale, cookie, and no-timestamp values are currently returned). The second, fourth, and fifth pass by accident under the current code — they stay in as pins on the new sort-based behavior.
140
+
141
+ - [ ] **Step 3: Implement**
142
+
143
+ In `src/api/request-store.ts`, change the constant:
144
+
145
+ ```ts
146
+ const AUTH_HEADERS = ['authorization', 'x-api-key', 'x-csrf-token'];
147
+ ```
148
+
149
+ Add a private field to `RequestStore` (private fields live after public methods is a rule for methods; fields stay at the top with the others):
150
+
151
+ ```ts
152
+ private sessionStartedAt = new Date();
153
+ ```
154
+
155
+ Replace `extractAuthHeaders`:
156
+
157
+ ```ts
158
+ extractAuthHeaders(): Record<string, string> {
159
+ const headers: Record<string, string> = {};
160
+ const sessionCaptures = this.capturedRequests.filter((r) => r.timestamp >= this.sessionStartedAt).sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
161
+
162
+ for (const req of sessionCaptures) {
163
+ for (const [key, value] of Object.entries(req.requestHeaders)) {
164
+ if (AUTH_HEADERS.includes(key.toLowerCase()) && !headers[key]) {
165
+ headers[key] = value;
166
+ }
167
+ }
168
+ }
169
+
170
+ return headers;
171
+ }
172
+ ```
173
+
174
+ In `src/api/request-result.ts` line 176, change the load fallback so an absent timestamp reads as stale:
175
+
176
+ ```ts
177
+ timestamp: new Date(meta.timestamp || 0),
178
+ ```
179
+
180
+ - [ ] **Step 4: Run the tests to verify they pass**
181
+
182
+ Run: `bun test tests/unit/request-store.test.ts && bun test tests/unit/`
183
+ Expected: all PASS (the second command catches any other unit test that relied on the old fallback).
184
+
185
+ - [ ] **Step 5: Format and commit**
186
+
187
+ ```bash
188
+ cd /home/davert/projects/explorbot-fisherman-reliability
189
+ bun run format
190
+ git add src/api/request-store.ts src/api/request-result.ts tests/unit/request-store.test.ts
191
+ git commit -m "Extract auth headers only from current-session captures"
192
+ ```
193
+
194
+ ---
195
+
196
+ ### Task 2: Live browser headers replace captured ones, replicate mode only (Fisherman.refreshAuth)
197
+
198
+ **Files:**
199
+ - Modify: `src/ai/fisherman.ts:156-170` (refreshAuth)
200
+ - Test: `tests/integration/fisherman.test.ts`
201
+
202
+ **Interfaces:**
203
+ - Consumes: `extractAuthHeaders()` from Task 1; the Fisherman constructor's 6th argument `cookieProvider: () => Promise<Record<string, string>>` (renamed in Task 4, unchanged here); `ApiClient.setHeaders(headers)` which Object.assigns into defaults; `this.mode`, set by `ensureReady()` before `refreshAuth()` runs (`prepareData` calls them in that order).
204
+ - Produces: `refreshAuth` application order captured → browser → config, with the captured and browser layers applied only when `this.mode === 'replicate'`. Task 4's provider relies on its returned headers overriding same-named captured headers.
205
+
206
+ - [ ] **Step 1: Write the failing integration test**
207
+
208
+ In `tests/integration/fisherman.test.ts`:
209
+
210
+ 1. Add a module-scope variable next to `apiResponses` and reset it in `beforeEach`:
211
+
212
+ ```ts
213
+ let apiHeaders: Record<string, string>;
214
+ ```
215
+
216
+ ```ts
217
+ apiHeaders = {};
218
+ ```
219
+
220
+ 2. Change `createFisherman` to record headers, accept the browser-provided ones, and allow achieve-mode construction (existing call sites stay `createFisherman()`):
221
+
222
+ ```ts
223
+ function createFisherman(browserHeaders: Record<string, string> = {}, configHeaders: Record<string, string> = {}, hasApiConfig = false): Fisherman {
224
+ const apiClient = {
225
+ request: async () => apiResponses.shift(),
226
+ setHeaders: (h: Record<string, string>) => Object.assign(apiHeaders, h),
227
+ getHeaders: () => ({ ...apiHeaders }),
228
+ };
229
+ return new Fisherman(
230
+ provider,
231
+ apiClient as any,
232
+ requestStore,
233
+ async () => null,
234
+ 'https://example.test/api',
235
+ async () => browserHeaders,
236
+ configHeaders,
237
+ hasApiConfig
238
+ );
239
+ }
240
+ ```
241
+
242
+ 3. Add the tests:
243
+
244
+ ```ts
245
+ it('sends the current browser session credentials, replacing captured ones', async () => {
246
+ const captured = requestResult('xhr_010_POST_api_alpha-shop_tests', 'POST', '/api/alpha-shop/tests', 201);
247
+ captured.requestHeaders = { 'x-csrf-token': 'captured-token', cookie: 'session=captured' };
248
+ requestStore.addCapturedRequest(captured);
249
+
250
+ mock.on({ sequenceIndex: 0 }, { toolCalls: [toolCall('c1', 'stop', { reason: 'nothing to do' })] });
251
+ mock.on({}, { content: 'done' });
252
+
253
+ await createFisherman({ Cookie: 'session=live', 'x-csrf-token': 'live-token' }).prepareData('1 suite', '/projects/alpha-shop/suites');
254
+
255
+ expect(apiHeaders.Cookie).toBe('session=live');
256
+ expect(apiHeaders['x-csrf-token']).toBe('live-token');
257
+ expect(apiHeaders.cookie).toBeUndefined();
258
+ });
259
+
260
+ it('achieve mode authenticates only through config headers, never the browser session', async () => {
261
+ mock.on({ sequenceIndex: 0 }, { toolCalls: [toolCall('c1', 'stop', { reason: 'nothing to do' })] });
262
+ mock.on({}, { content: 'done' });
263
+
264
+ await createFisherman({ Cookie: 'session=live' }, { 'x-api-key': 'from-config' }, true).prepareData('1 suite', '/projects/alpha-shop/suites');
265
+
266
+ expect(apiHeaders['x-api-key']).toBe('from-config');
267
+ expect(apiHeaders.Cookie).toBeUndefined();
268
+ });
269
+ ```
270
+
271
+ - [ ] **Step 2: Run the tests to verify they fail**
272
+
273
+ Run: `bun test tests/integration/fisherman.test.ts`
274
+ Expected: both new tests FAIL — in the first, the current order applies browser headers before captured ones, so the captured `x-csrf-token` (`captured-token`, a live-session capture that passes Task 1's gate) overwrites `live-token`; in the second, the current unconditional `refreshAuth` sends the browser Cookie in achieve mode.
275
+
276
+ - [ ] **Step 3: Reorder refreshAuth and gate it to replicate mode**
277
+
278
+ In `src/ai/fisherman.ts`, replace `refreshAuth`:
279
+
280
+ ```ts
281
+ private async refreshAuth(): Promise<void> {
282
+ if (this.mode === 'replicate') {
283
+ const xhrHeaders = this.requestStore.extractAuthHeaders();
284
+ if (Object.keys(xhrHeaders).length > 0) {
285
+ this.apiClient.setHeaders(xhrHeaders);
286
+ }
287
+
288
+ const cookies = await this.cookieProvider();
289
+ if (Object.keys(cookies).length > 0) {
290
+ this.apiClient.setHeaders(cookies);
291
+ }
292
+ }
293
+
294
+ if (Object.keys(this.configHeaders).length > 0) {
295
+ this.apiClient.setHeaders(this.configHeaders);
296
+ }
297
+ }
298
+ ```
299
+
300
+ - [ ] **Step 4: Run the tests to verify they pass**
301
+
302
+ Run: `bun test tests/integration/fisherman.test.ts && bun test tests/unit/fisherman-tools.test.ts`
303
+ Expected: all PASS, including the two pre-existing integration tests.
304
+
305
+ - [ ] **Step 5: Format and commit**
306
+
307
+ ```bash
308
+ bun run format
309
+ git add src/ai/fisherman.ts tests/integration/fisherman.test.ts
310
+ git commit -m "Apply live browser session headers over captured ones"
311
+ ```
312
+
313
+ ---
314
+
315
+ ### Task 3: Filter the cookie jar by the API origin
316
+
317
+ **Files:**
318
+ - Modify: `src/explorbot.ts:349-353` (cookieProvider inside agentFisherman)
319
+
320
+ **Interfaces:**
321
+ - Consumes: `baseEndpoint` local (`apiConfig?.baseEndpoint || this.config.playwright.url`), `Explorer.withPage`.
322
+ - Produces: unchanged provider signature; the returned `Cookie` header now contains only cookies Playwright would send to `baseEndpoint`.
323
+
324
+ - [ ] **Step 1: Pass the target URL to the jar**
325
+
326
+ This is DI glue over a Playwright API (`BrowserContext.cookies(urls)` filters by domain/path the way a real browser does) — there is no unit seam to test without mocking Playwright itself, so this task is verified by types and the full suite. Change line 350:
327
+
328
+ ```ts
329
+ const cookies = await this.explorer.withPage((page) => page.context().cookies(baseEndpoint)).catch(() => []);
330
+ ```
331
+
332
+ - [ ] **Step 2: Run the suite**
333
+
334
+ Run: `bun test tests/unit/ && bun test tests/integration/`
335
+ Expected: all PASS (no behavior change reachable from tests).
336
+
337
+ - [ ] **Step 3: Format and commit**
338
+
339
+ ```bash
340
+ bun run format
341
+ git add src/explorbot.ts
342
+ git commit -m "Send only cookies scoped to the API origin"
343
+ ```
344
+
345
+ ---
346
+
347
+ ### Task 4 (severable): Live CSRF token from the page, provider renamed
348
+
349
+ If the user cuts this task, stop after Task 3 — nothing below is required by Tasks 1–3.
350
+
351
+ **Files:**
352
+ - Modify: `src/explorbot.ts:349-356` (provider + Fisherman construction)
353
+ - Modify: `src/ai/fisherman.ts:24,33,39` (field/param rename) and the `refreshAuth` body from Task 2
354
+ - Modify: `tests/integration/fisherman.test.ts` (argument name only)
355
+
356
+ **Interfaces:**
357
+ - Consumes: Task 2's refreshAuth ordering; Task 3's URL-filtered jar.
358
+ - Produces: `browserHeaderProvider: () => Promise<Record<string, string>>` as the Fisherman constructor's 6th argument — same type, new name — returning `{ Cookie?, 'x-csrf-token'? }`.
359
+
360
+ - [ ] **Step 1: Extend and rename the provider in explorbot.ts**
361
+
362
+ Replace the `cookieProvider` block (which after Task 3 reads `cookies(baseEndpoint)`) with:
363
+
364
+ ```ts
365
+ const browserHeaderProvider = async (): Promise<Record<string, string>> => {
366
+ const session = await this.explorer
367
+ .withPage(async (page) => ({
368
+ cookies: await page.context().cookies(baseEndpoint),
369
+ csrf: await page.evaluate(() => document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''),
370
+ }))
371
+ .catch(() => ({ cookies: [] as any[], csrf: '' }));
372
+
373
+ const headers: Record<string, string> = {};
374
+ if (session.cookies.length) headers.Cookie = session.cookies.map((c: any) => `${c.name}=${c.value}`).join('; ');
375
+ if (session.csrf) headers['x-csrf-token'] = session.csrf;
376
+ return headers;
377
+ };
378
+ ```
379
+
380
+ Update the construction call on the line that reads `new Fisherman(ai, apiClient, requestStore, specLoader, baseEndpoint, cookieProvider, configHeaders, hasApiConfig)` to pass `browserHeaderProvider`.
381
+
382
+ `meta[name="csrf-token"]` is a cross-framework convention (Rails, Laravel) — structural knowledge like ARIA roles, not a site-specific locator (see spec D5).
383
+
384
+ - [ ] **Step 2: Rename inside Fisherman**
385
+
386
+ In `src/ai/fisherman.ts`, rename the private field `cookieProvider` to `browserHeaderProvider` (declaration line 24, constructor parameter and assignment lines 33/39) and update the browser block inside `refreshAuth`'s replicate-mode gate accordingly:
387
+
388
+ ```ts
389
+ const browserHeaders = await this.browserHeaderProvider();
390
+ if (Object.keys(browserHeaders).length > 0) {
391
+ this.apiClient.setHeaders(browserHeaders);
392
+ }
393
+ ```
394
+
395
+ - [ ] **Step 3: Rename the test argument**
396
+
397
+ In `tests/integration/fisherman.test.ts`, `createFisherman(browserHeaders …)` already uses the right name from Task 2 — verify no remaining `cookieProvider` identifier exists in the repo:
398
+
399
+ Run: `grep -rn cookieProvider src/ tests/`
400
+ Expected: no matches.
401
+
402
+ - [ ] **Step 4: Run the suite**
403
+
404
+ Run: `bun test tests/unit/ && bun test tests/integration/`
405
+ Expected: all PASS — the Task 2 test already proves a provider-supplied `x-csrf-token` reaches the client and overrides the captured one.
406
+
407
+ - [ ] **Step 5: Format and commit**
408
+
409
+ ```bash
410
+ bun run format
411
+ git add src/explorbot.ts src/ai/fisherman.ts tests/integration/fisherman.test.ts
412
+ git commit -m "Read the live CSRF token from the page meta tag"
413
+ ```
414
+
415
+ ---
416
+
417
+ ### Task 5: Housekeeping and PR update
418
+
419
+ **Files:**
420
+ - Modify: `CHANGELOG.md` (extend the existing 2026-08-29 `[Fisherman]` entry area with a 2026-08-30 entry)
421
+
422
+ **Interfaces:**
423
+ - Consumes: all previous tasks committed.
424
+ - Produces: branch pushed to PR #160 with an updated description.
425
+
426
+ - [ ] **Step 1: Merge main and verify**
427
+
428
+ ```bash
429
+ cd /home/davert/projects/explorbot-fisherman-reliability
430
+ git fetch origin && git merge origin/main
431
+ bun test tests/unit/ && bun test tests/integration/
432
+ ```
433
+
434
+ Expected: clean merge (resolve conflicts if any, rerun tests), all tests PASS.
435
+
436
+ - [ ] **Step 2: Update CHANGELOG**
437
+
438
+ Add under a `## 2026-08-30` heading, following the existing entry style:
439
+
440
+ ```markdown
441
+ - [Fisherman] In replicate mode, API requests now authenticate with the current browser session: cookies are taken from the live jar filtered to the API origin, the CSRF token is read from the page, and auth headers are never reused from previous sessions' captured requests. Achieve mode authenticates solely through `api.headers` config.
442
+ ```
443
+
444
+ - [ ] **Step 3: Format, commit, push**
445
+
446
+ ```bash
447
+ bun run format
448
+ git add CHANGELOG.md
449
+ git commit -m "Changelog for live-session auth"
450
+ git push origin fisherman-reliability
451
+ ```
452
+
453
+ - [ ] **Step 4: Update the PR #160 description**
454
+
455
+ Append a section to the PR body via `gh pr edit 160 --body-file` (fetch the current body with `gh pr view 160 --json body -q .body` first, never overwrite blindly): one paragraph stating that trace `de95bd1cffce09169599d99d1bee56cd` exposed stale-credential assembly (unfiltered cookie jar + auth headers scraped from months-old captures) and that Fisherman now authenticates with the live browser session per `docs/superpowers/specs/2026-08-30-fisherman-live-session-auth-design.md`.
456
+
457
+ Do NOT touch the `regression` label or workflow.
@@ -0,0 +1,45 @@
1
+ # Fisherman Reliability — Design
2
+
3
+ Fisherman is the API test-data preparation agent. Pilot calls it through the `precondition()` tool before a test runs; in **replicate mode** (no `api` config block) it learns endpoints from browser XHR traffic captured into `output/requests/`, in **achieve mode** it reads an OpenAPI spec. This design fixes replicate mode, which has never worked reliably.
4
+
5
+ ## Evidence
6
+
7
+ Langfuse traces contain only two real Fisherman episodes (2026-04-30 and 2026-06-01); every August export has zero Fisherman spans, so the August fixes (#133) have no behavioral data behind them.
8
+
9
+ - **Jun 01 01:28 — the one clean success.** 12 tool calls: a 400 on a missing field, retried correctly, parent suite created, test created inside it, `finish` with both real ids. Error-driven recovery worked.
10
+ - **Jun 01 01:30–01:32 — poisoned by the success.** The 01:28 run's own *rejected* bodies were handed back to the next runs as "the captured request example". They spent 20–49 requests guessing body shapes. One "succeeded" on the third invocation after ~155 model calls, with weak evidence: the reported created id was one an earlier run had *read* out of a GET list.
11
+ - **Apr 30 — false success.** Asked for a milestone; no milestone endpoint existed, so it POSTed to `/tests` and reported a created milestone. Pilot passed that to the Tester as a satisfied precondition. The prompt's endpoint list also contained a write endpoint from a different project. `stop` was never called in any episode; runs that hit max iterations reported nothing at all.
12
+
13
+ ## Root causes (verified in current code)
14
+
15
+ 1. **Self-poisoning store.** `addMadeRequest()` saves every request Fisherman itself makes — 400s included — into `output/requests/`. `loadFromDisk()` reads the whole directory back as `capturedRequests`, indistinguishable from browser XHR. Replicate-only: achieve mode never calls `loadFromDisk`.
16
+ 2. **First-match spec lookup.** `findCapturedRequest()` is `find(method && path.startsWith(prefix))` — a stale 400 displaces a good 200, and `/suites` matches `/suites/123/move`. #133 only labels rejected captures `usable: false`; it does not rank.
17
+ 3. **Scope filter never matches.** `getWriteRequestsForScope()` prefix-matches a page URL (`/projects/…`) against API paths (`/api/…`) — always falls through to `'/'`, silently, giving every captured write from every project.
18
+ 4. **`finish` is unconditional success.** It writes the model's `created` array through verbatim; nothing checks it against what HTTP actually returned.
19
+ 5. **Silent exhaustion & clobbered status.** No `finish` → `summary: ''` → Pilot logs nothing and the vision fallback is told the reason is "unknown". In the `request` tool, `...extractKeyFields()` spreads after the `status` key, so a body field named `status` overwrites the HTTP status, and the depth-5 first-id scan surfaces ids the run never created.
20
+
21
+ ## Design decisions
22
+
23
+ 1. **Provenance by id prefix, not a new envelope key.** File ids already encode the writer: `xhr_*` from `xhr-capture.ts`, unprefixed from `api-client.ts`, and `fail_*` records never reach disk. `loadFromDisk()` admits only `xhr_*` files. Poisoned directories migrate for free; a directory with only Fisherman-made files now correctly yields replicate mode disabled.
24
+ 2. **Ranked lookup.** `findCapturedRequest` ranks candidates: exact segment-count match > deeper sub-path, then `status < 400` > rejection, then newest timestamp. An exact-path rejection deliberately beats a sub-path success — the `usable: false` branch explains it to the model.
25
+ 3. **Shared path normalization.** Id-shaped segments are detected by the existing `isDynamicSegment()` (`src/utils/url-matcher.ts`) and printed as `{id}` in endpoint lists; lookup normalizes both sides, so patterns and concrete ids both match. Over-generalization (e.g. `/api/v2/…` → `/api/{id}/…`) is accepted: `getEndpointSpec` returns the concrete stored path, and the workflow mandates a spec lookup before first use.
26
+ 4. **Scope = most selective shared segment.** Score each page-URL path segment by how many captured writes contain it; the scope key is the non-zero segment with the fewest matches, leftmost on ties. The project slug beats generic literals like `projects` structurally, with no site-specific knowledge. When nothing matches, the list degrades to global — and the system prompt says so.
27
+ 5. **`finish` gated by the request ledger, not replaced.** `createFishermanTools` snapshots the made-request count; "this run" is everything after it. `finish` with zero successful writes is rejected back to the model (it can keep working or `stop`). Claimed ids are verified against actual 2xx write responses; verified items carry `via: "POST /api/…"` so Pilot sees what ran; unverifiable ids are dropped. When the loop ends without `finish`, the result is synthesized from the ledger — honest summary, ledger-derived created items — never an empty string.
28
+ 6. **Deterministic loop guard.** Four consecutive failures (HTTP ≥ 400 or network error) against one endpoint end the run — the API-side analog of StateManager's dead-loop detection.
29
+ 7. **One general prompt rule** against substituting resource types: if no endpoint creates a requested type, `stop` — never create a different type. (Ledger id-verification alone cannot catch the milestone→test case, since that POST genuinely succeeded.)
30
+
31
+ All checks are deterministic-tier (structural matching, closed vocabularies); the model keeps judgment over what to create and how to describe it — the generate-then-verify ladder from CLAUDE.md.
32
+
33
+ ## Out of scope
34
+
35
+ - Achieve mode (OpenAPI-driven) behavior.
36
+ - Cross-session reuse of captures beyond what the `xhr_` filter admits.
37
+ - New agents, tools, or envelope keys.
38
+
39
+ ## Acceptance criterion
40
+
41
+ One regression trace where `precondition()` returns created ids and those ids are visible on the page the Tester then acts on. The regression run is triggered only by the user via the `regression` label.
42
+
43
+ ## Implementation
44
+
45
+ `docs/superpowers/plans/2026-08-29-fisherman-reliability.md`