Skip to content

Add agent hooks and knowledge code execution#9

Merged
DavertMik merged 6 commits intomainfrom
feature/agent-hooks
Feb 7, 2026
Merged

Add agent hooks and knowledge code execution#9
DavertMik merged 6 commits intomainfrom
feature/agent-hooks

Conversation

@DavertMik
Copy link
Contributor

@DavertMik DavertMik commented Feb 6, 2026

User description

Summary

  • Add beforeHook and afterHook support for Navigator, Researcher, Tester, and Captain agents
  • Hooks can execute either Playwright or CodeceptJS code before/after agent execution
  • Support URL pattern matching (exact, wildcard, glob, regex) for conditional hook execution
  • Add code property to knowledge files for executing CodeceptJS code on page navigation
  • Add tryTo and retryTo effects support in knowledge code for error handling and retries

Features

Agent Hooks

Configure hooks per-agent in explorbot.config.js:

agents: {
  navigator: {
    beforeHook: {
      type: 'playwright',
      hook: async ({ page }) => await page.waitForLoadState('networkidle')
    }
  },
  researcher: {
    beforeHook: {
      '/login': { type: 'codeceptjs', hook: async ({ I }) => await I.waitForElement('#form') },
      '/admin/*': { type: 'playwright', hook: async ({ page }) => await page.locator('.loaded').waitFor() }
    }
  }
}

Knowledge Code Execution

Execute CodeceptJS code when navigating to matching pages:

---
url: /app/*
code: |
  I.waitForElement('.app-ready');
  I.click('.cookie-accept');
---

CodeceptJS Effects

Use tryTo and retryTo for robust page automation:

---
url: /dashboard
code: |
  await tryTo(() => I.click('.cookie-dismiss'));
  await retryTo(() => {
    I.click('Reload Data');
    I.waitForElement('.data-loaded');
  }, 5, 500);
---
Effect Purpose
tryTo(fn) Execute without failing - returns true/false
retryTo(fn, maxTries, interval) Retry on failure with polling
within(context, fn) Execute within a specific element context

Files Changed

  • src/config.ts - Hook type definitions
  • src/utils/hooks-runner.ts - New utility for hook execution
  • src/action.ts - CodeceptJS effects support
  • src/ai/navigator.ts - Hook integration
  • src/ai/researcher.ts - Hook integration
  • src/ai/tester.ts - Hook integration
  • src/ai/captain.ts - Hook integration
  • src/explorer.ts - Knowledge code execution
  • docs/hooks.md - New documentation
  • docs/knowledge.md - Page automation and effects docs
  • docs/page-interaction.md - HTML filtering docs
  • docs/configuration.md - Hook options reference

Test plan

  • Configure beforeHook for navigator agent, verify it runs after navigation
  • Configure afterHook for tester agent, verify it runs after test completion
  • Test URL pattern matching with wildcards and regex
  • Test knowledge file with code property, verify execution on page visit
  • Test tryTo effect - verify it catches errors and returns false
  • Test retryTo effect - verify it retries failed actions
  • Verify Playwright hooks receive { page, url } context
  • Verify CodeceptJS hooks receive { I, url } context

🤖 Generated with Claude Code


CodeAnt-AI Description

Run per-agent before/after hooks and execute page-level CodeceptJS code on navigation

What Changed

  • Agents (navigator, researcher, tester, captain) now run configurable beforeHook and afterHook code around their main work; hooks can be Playwright or CodeceptJS and can be scoped to URL patterns (exact, wildcard, glob, regex)
  • Knowledge files may include a new code field that runs CodeceptJS commands when navigating to matching pages; those knowledge scripts can use CodeceptJS effects (tryTo, retryTo, within, hopeThat) for safe retries and non-fatal attempts
  • Hooks and knowledge code errors are logged but do not stop agent execution; hooks match the most specific URL pattern first
  • Documentation added and updated: configuration reference, hook examples, knowledge file usage, and pattern matching behavior

Impact

✅ Fewer flaky navigations due to agent-specific preconditions
✅ Shorter test setup by automating page preparation (e.g., dismiss banners)
✅ More robust page automation with retryable knowledge scripts

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

- Add beforeHook/afterHook support for Navigator, Researcher, Tester, Captain agents
- Hooks can execute Playwright or CodeceptJS code before/after agent runs
- Support URL pattern matching for conditional hook execution
- Add HooksRunner utility class for hook execution
- Add `code` property to knowledge files for CodeceptJS execution on page visit
- Document hooks system, knowledge automation, and HTML filtering

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@codeant-ai
Copy link

codeant-ai bot commented Feb 6, 2026

CodeAnt AI is reviewing your PR.

@codeant-ai codeant-ai bot added the size:XL This PR changes 500-999 lines, ignoring generated files label Feb 6, 2026
@codeant-ai
Copy link

codeant-ai bot commented Feb 6, 2026

Nitpicks 🔍

🔒 No security issues identified
⚡ Recommended areas for review

  • Runtime Code Execution
    The code now executes free-form code pulled from knowledge files via action.execute(code). This can execute arbitrary CodeceptJS commands and may cause unintended side effects, test instability, or malicious actions if the knowledge content is untrusted. Validate and sandbox or restrict what can run.

  • Hook error handling
    The new calls to runBeforeHook / runAfterHook are invoked without protective error handling. If a hook throws or rejects, it can interrupt the test flow or leave the agent in a half-finished state. Ensure hook failures are caught, logged and do not break the normal test lifecycle.

  • Uncaught Hook Errors
    The new calls to runBeforeHook/runAfterHook are awaited directly and not guarded. If a hook throws or the hooks runner fails, the entire research flow could be interrupted. Hooks are external code (playwright/codeceptjs) and can be slow or error-prone — they must be isolated to avoid breaking core agent behavior.

  • Unguarded Hook Execution
    The new calls to runBeforeHook/runAfterHook execute external hook code before and after the agent run. If that hook throws or rejects, it will bubble up and break the captain's flow (no try/catch or fallback). Failures in hook code should not stop the agent; they should be logged and degraded gracefully.

  • Error Handling
    The newly added await action.execute(code) is not wrapped in try/catch; if the knowledge code throws, it will bubble up and may interrupt navigation or higher-level flows. Consider catching errors and recording/logging them.

Comment on lines 362 to 365
const pageContext = await this.getPageContext();
const planContext = this.planSummary();

// Clean up old page context from previous inputs when continuing conversation
await this.getHooksRunner().runBeforeHook('captain', startUrl);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The before-hook for this agent runs after the initial page context and plan summary are captured, so if the hook mutates the page (e.g., dismisses a modal, waits for extra content, or navigates), the AI will reason on stale page information; move the hook call before collecting context so the prompt reflects the post-hook state, matching how other agents use hooks. [logic error]

Severity Level: Major ⚠️
- ⚠️ Captain prompt uses stale page snapshot.
- ⚠️ Reasoning based on pre-hook DOM state.
- ❌ Actions may fail after hook mutations.
- ⚠️ Affects agent decisions and tool usage.
Suggested change
const pageContext = await this.getPageContext();
const planContext = this.planSummary();
// Clean up old page context from previous inputs when continuing conversation
await this.getHooksRunner().runBeforeHook('captain', startUrl);
await this.getHooksRunner().runBeforeHook('captain', startUrl);
const pageContext = await this.getPageContext();
const planContext = this.planSummary();
Steps of Reproduction ✅
1. Configure a beforeHook for the captain agent in explorbot.config.js that mutates the
page (for example, dismisses a modal or waits for content). This is the new hook mechanism
added in the PR (hooks runner invoked at src/ai/captain.ts:365).

2. Trigger the Captain flow by calling Captain.handle(...) (src/ai/captain.ts:334) with a
valid page already loaded. The code path reaches the section starting at
src/ai/captain.ts:361 where tools and page context are prepared.

3. Observe ordering: at src/ai/captain.ts:361 the code builds tools (this.tools), then at
src/ai/captain.ts:362 it calls getPageContext() to snapshot the page, then at
src/ai/captain.ts:363 it calls planSummary(), and only afterwards at src/ai/captain.ts:365
it executes getHooksRunner().runBeforeHook('captain', startUrl).

4. Because the pageContext (src/ai/captain.ts:362) is captured before the beforeHook runs
(src/ai/captain.ts:365), any DOM/state changes the hook performs (e.g., navigation,
dismiss modal, wait for networkidle) are not reflected in the prompt sent to the provider.
Reproduce by logging or asserting differences: add a beforeHook that clicks a cookie
banner then checks for element presence; compare the page HTML/ARIA in pageContext (line
362 -> ActionResult.fromState) vs the actual page after the hook runs and observe
mismatch.

5. Expected behavior per other agents (and the feature intent) is to run beforeHook before
capturing context so the LLM reasons about the post-hook page. The current ordering
(pageContext then runBeforeHook) produces stale context. This is verifiable by reading the
code flow at src/ai/captain.ts lines 361-365.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/ai/captain.ts
**Line:** 362:365
**Comment:**
	*Logic Error: The before-hook for this agent runs after the initial page context and plan summary are captured, so if the hook mutates the page (e.g., dismisses a modal, waits for extra content, or navigates), the AI will reason on stale page information; move the hook call before collecting context so the prompt reflects the post-hook state, matching how other agents use hooks.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

const isOnCurrentState = this.actionResult!.getStateHash() === this.stateManager.getCurrentState()?.hash;
await this.ensureNavigated(state.url, screenshot && this.provider.hasVision());
await this.hooksRunner.runBeforeHook('researcher', state.url);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: After running the configurable before-hook, the stored page state (actionResult) is not refreshed, so the researcher builds its HTML/ARIA-based prompt from a stale snapshot that does not include any changes the hook may have made (like dismissing modals or navigating), leading to incorrect or misleading research results; recapturing the page state immediately after the hook fixes this. [logic error]

Severity Level: Major ⚠️
- ❌ Research reports use stale DOM snapshots.
- ⚠️ UI map generation may include removed modals.
- ⚠️ Knowledge/context integration may be misleading.
- ⚠️ Investigations relying on researcher agent produce incorrect results.
Suggested change
this.actionResult = await this.explorer
.createAction()
.capturePageState({ includeScreenshot: screenshot && this.provider.hasVision() });
Steps of Reproduction ✅
1. Call Researcher.research() for a page state: open code path in src/ai/researcher.ts
where research() is executed. At research() the sequence around lines 111-116 is:

   - 111: const isOnCurrentState = ...

   - 112: await this.ensureNavigated(state.url, screenshot && this.provider.hasVision());

   - 113: await this.hooksRunner.runBeforeHook('researcher', state.url);

   - 115: debugLog('Researching web page:', this.actionResult!.url);

2. Configure a researcher beforeHook in explorbot.config.js that modifies the page DOM
(realistic example from PR description): e.g. a Playwright hook that clicks a cookie
banner or closes a modal on '/app/*'.

3. Trigger researcher.research(...) (e.g., via any code path that calls
Researcher.research with a WebPageState). The code runs ensureNavigated (line 112) then
runs the beforeHook (line 113). The hook executes and alters the page (dismiss modal,
navigate fragment, etc.).

4. After the hook returns, research() continues using this.actionResult (captured before
the hook at ensureNavigated). Because there is no re-capture, subsequent code builds
prompts and analyzes HTML/ARIA from the stale this.actionResult (see debugLog at line ~115
and prompt construction later), producing research that doesn't reflect the hook-made
changes.

5. Expected observable failure: research output (files under output/research or returned
text) still includes modal content, wrong UI map, or misses elements revealed by the hook.
Reproduced by configuring a beforeHook that modifies DOM and comparing research output
with and without adding the proposed re-capture after the hook.

Explanation: The repository diff shows the hook call was added at line 113 but no
re-capture follows; thus the suggestion to recapture the page state after hooks is
actionable and directly addresses the stale-snapshot problem.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/ai/researcher.ts
**Line:** 114:114
**Comment:**
	*Logic Error: After running the configurable before-hook, the stored page state (`actionResult`) is not refreshed, so the researcher builds its HTML/ARIA-based prompt from a stale snapshot that does not include any changes the hook may have made (like dismissing modals or navigating), leading to incorrect or misleading research results; recapturing the page state immediately after the hook fixes this.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

src/explorer.ts Outdated
const actionResult = currentState ? ActionResult.fromState(currentState) : null;

const { statePush = false, wait, waitForElement } = this.knowledgeTracker.getStateParameters(actionResult!, ['statePush', 'wait', 'waitForElement']);
const { statePush = false, wait, waitForElement, code } = this.knowledgeTracker.getStateParameters(actionResult!, ['statePush', 'wait', 'waitForElement', 'code']);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The call to retrieve knowledge state parameters uses a non-null assertion on a potentially null actionResult, so if visit is called before any state exists (e.g., the very first navigation), getStateParameters will receive null and state.isMatchedBy(...) will throw at runtime; instead, derive a safe fallback ActionResult (for example, from the target url) when no current state is available and pass that into getStateParameters without using a non-null assertion. [null pointer]

Severity Level: Critical 🚨
- ❌ First navigation may throw runtime exception.
- ⚠️ Knowledge matching for URL patterns fails at startup.
- ⚠️ Explorer's page automation becomes unreliable on cold start.
Suggested change
const { statePush = false, wait, waitForElement, code } = this.knowledgeTracker.getStateParameters(actionResult!, ['statePush', 'wait', 'waitForElement', 'code']);
const knowledgeState = actionResult ?? new ActionResult({ url });
const { statePush = false, wait, waitForElement, code } = this.knowledgeTracker.getStateParameters(knowledgeState, ['statePush', 'wait', 'waitForElement', 'code']);
Steps of Reproduction ✅
1. Trigger the very first navigation using Explorer.visit(url) (visit implementation in
src/explorer.ts; relevant lines around 204-207). At startup there is no current state so
actionResult is null (lines ~204-206).

2. The existing line at src/explorer.ts:207 force-unwraps actionResult and passes it to
this.knowledgeTracker.getStateParameters(...). If knowledge lookups call methods like
state.isMatchedBy(...) on the ActionResult, they will throw because actionResult is null.

3. Reproduce locally: instantiate Explorer, call start(), then call visit('/some/path') as
the first navigation. Observe an exception thrown from getStateParameters or inside its
match logic, preventing visit from completing and blocking subsequent action.execute(...)
calls (I.amOnPage or history push).

4. The proposed change constructs a minimal ActionResult from the target url when
actionResult is missing, ensuring getStateParameters receives a valid ActionResult
instance and avoids null dereferences while still allowing URL-based knowledge matching.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/explorer.ts
**Line:** 207:207
**Comment:**
	*Null Pointer: The call to retrieve knowledge state parameters uses a non-null assertion on a potentially null `actionResult`, so if `visit` is called before any state exists (e.g., the very first navigation), `getStateParameters` will receive `null` and `state.isMatchedBy(...)` will throw at runtime; instead, derive a safe fallback `ActionResult` (for example, from the target `url`) when no current state is available and pass that into `getStateParameters` without using a non-null assertion.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

import micromatch from 'micromatch';
import type { ExplorbotConfig, Hook, HookConfig } from '../config.ts';
import type Explorer from '../explorer.ts';
import { createDebug } from './logger.ts';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The logger import uses a .ts extension, but all runtime imports in this codebase target the compiled .js files; this will cause a runtime module resolution error (Cannot find module './logger.ts') once the code is transpiled and executed under Node. [possible bug]

Severity Level: Critical 🚨
- ❌ Application startup fails due to module load error.
- ⚠️ Hooks subsystem unavailable until fixed.
- ⚠️ CI/runtime tests may break on Node environment.
Suggested change
import { createDebug } from './logger.ts';
import { createDebug } from './logger.js';
Steps of Reproduction ✅
1. Inspect src/utils/hooks-runner.ts import lines (Read output shows line 4: import {
createDebug } from './logger.ts';).

2. Build/compile project to JS (typical ts->js pipeline). At runtime Node will load
compiled files; most build setups expect runtime imports to target .js or be
extensionless. With the current import './logger.ts', Node's resolver (or bundler) can try
to load a non-existent './logger.ts' file from the compiled output and throw "Cannot find
module './logger.ts'".

3. Trigger any code path that constructs HooksRunner (e.g., explorer initialization that
imports HooksRunner). The import failure occurs immediately when requiring/initializing
src/utils/hooks-runner.ts at startup, causing the process to crash before hooks run.

4. Replacing the import with './logger.js' or using an extensionless import that build
tooling rewrites prevents the runtime resolution error.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/utils/hooks-runner.ts
**Line:** 4:4
**Comment:**
	*Possible Bug: The logger import uses a `.ts` extension, but all runtime imports in this codebase target the compiled `.js` files; this will cause a runtime module resolution error (`Cannot find module './logger.ts'`) once the code is transpiled and executed under Node.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

if (this.isSingleHook(config)) return config as Hook;

const urlPath = this.extractPath(url);
for (const [pattern, hook] of Object.entries(config)) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: When multiple hook patterns are configured in a map, iteration over Object.entries means a catch-all pattern like * can overshadow more specific patterns if it appears first, so a generic hook may run instead of a route-specific one, which is counter to typical routing semantics. [logic error]

Severity Level: Major ⚠️
- ⚠️ Route-specific hooks bypassed by generic patterns.
- ⚠️ Unexpected hook behavior in Navigator/Researcher agents.
- ⚠️ Tests relying on specific hooks may fail.
Suggested change
for (const [pattern, hook] of Object.entries(config)) {
const entries = Object.entries(config);
// Ensure more specific patterns are evaluated before generic ones like '*'
entries.sort(([patternA], [patternB]) => {
if (patternA === '*' && patternB !== '*') return 1;
if (patternB === '*' && patternA !== '*') return -1;
const aWildcard = patternA.endsWith('/*');
const bWildcard = patternB.endsWith('/*');
if (aWildcard !== bWildcard) return aWildcard ? 1 : -1;
return 0;
});
for (const [pattern, hook] of entries) {
Steps of Reproduction ✅
1. Read src/utils/hooks-runner.ts to find findMatchingHook implementation at lines 36-44
(verified via Read/Grep). It iterates Object.entries(config) in insertion order and
returns the first matching pattern.

2. Create an agent config where a generic pattern '*' (or broad glob) is inserted before a
more specific pattern in the same object, for example in explorbot.config.js:

   beforeHook: { '*': { ... }, '/admin/*': { ... } }

   (Objects in JS preserve insertion order; if '*' is defined first, it will be checked
   first.)

3. Call HooksRunner.runBeforeHook for a path like '/admin/users'. findMatchingHook will
iterate entries and match the '*' pattern first (lines 39-42), returning the generic hook
and never reaching the specific '/admin/*' rule.

4. Observe that a generic hook runs instead of the intended route-specific hook. The
improved code sorts entries so specific patterns take precedence, preventing this
shadowing.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/utils/hooks-runner.ts
**Line:** 40:40
**Comment:**
	*Logic Error: When multiple hook patterns are configured in a map, iteration over `Object.entries` means a catch-all pattern like `*` can overshadow more specific patterns if it appears first, so a generic hook may run instead of a route-specific one, which is counter to typical routing semantics.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

Comment on lines +60 to +61
private isSingleHook(config: HookConfig): boolean {
return 'type' in config && 'hook' in config;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The isSingleHook check currently only tests for the presence of type and hook keys, so a pattern map that happens to use "type" or "hook" as URL patterns will be misclassified as a single hook, causing all other hooks in the map to be ignored and the URL pattern matching logic to never run for that agent; tightening the check to verify the value's actual shape (hook is a function and type is 'playwright' or 'codeceptjs') avoids this misclassification. [logic error]

Severity Level: Major ⚠️
- ❌ URL-specific hooks ignored for misclassified configs.
- ⚠️ Researcher/Navigator agent hooks misbehave unpredictably.
- ⚠️ Hook debugging time increases for maintainers.
Suggested change
private isSingleHook(config: HookConfig): boolean {
return 'type' in config && 'hook' in config;
private isSingleHook(config: HookConfig): config is Hook {
const candidate = config as any;
return (
candidate &&
typeof candidate === 'object' &&
typeof candidate.hook === 'function' &&
(candidate.type === 'playwright' || candidate.type === 'codeceptjs')
);
Steps of Reproduction ✅
1. Open file src/utils/hooks-runner.ts and locate the isSingleHook implementation at lines
60-62 (verified via Read output). The function returns true when the object has keys
'type' and 'hook' regardless of their values.

2. Configure an agent in explorbot.config.js with an agent-specific hook map that uses a
URL string key literally named "type" or "hook", for example:

   agents: { researcher: { beforeHook: { type: { type: 'playwright', hook: async () => {}
   } } } }

   (This configuration structure matches the shape of a map where a pattern key is the
   literal 'type'.)

3. Run code path that triggers hook resolution: HooksRunner.runBeforeHook('researcher',
'/some/url') calls runHook -> findMatchingHook (see src/utils/hooks-runner.ts lines 22-34
and 36-44).

4. findMatchingHook calls isSingleHook(config) (line 36). Because the top-level object has
keys 'type' and 'hook' (even though it was intended as a map), the current isSingleHook
returns true and the entire config is treated as a single Hook object, bypassing pattern
iteration. As a result, pattern-specific hooks in the map are ignored and the intended
URL-based hook matching never runs (observed at lines 36-43).

5. Expected: only true single Hook values (with callable hook and valid type) should be
classified as a single hook; current loose check causes misclassification. The improved
check (type guard verifying hook is function and type is valid) prevents this
misclassification.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/utils/hooks-runner.ts
**Line:** 60:61
**Comment:**
	*Logic Error: The `isSingleHook` check currently only tests for the presence of `type` and `hook` keys, so a pattern map that happens to use `"type"` or `"hook"` as URL patterns will be misclassified as a single hook, causing all other hooks in the map to be ignored and the URL pattern matching logic to never run for that agent; tightening the check to verify the value's actual shape (hook is a function and type is `'playwright'` or `'codeceptjs'`) avoids this misclassification.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

@codeant-ai
Copy link

codeant-ai bot commented Feb 6, 2026

CodeAnt AI finished reviewing your PR.

DavertMik and others added 4 commits February 6, 2026 05:27
- Import tryTo, retryTo, within, hopeThat from codeceptjs/lib/effects.js
- Pass effects to code execution in action.execute() and action.expect()
- Document effects usage in knowledge.md

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@codeant-ai
Copy link

codeant-ai bot commented Feb 7, 2026

CodeAnt AI is running Incremental review

@codeant-ai codeant-ai bot added size:XL This PR changes 500-999 lines, ignoring generated files and removed size:XL This PR changes 500-999 lines, ignoring generated files labels Feb 7, 2026
src/action.ts Outdated
Comment on lines 192 to 193
const codeFunction = new Function('I', 'tryTo', 'retryTo', 'within', 'hopeThat', codeString);
codeFunction(this.actor, tryTo, retryTo, within, hopeThat);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: In execute, the dynamically created function is a regular (non-async) function that is immediately invoked without await, so any code block using await tryTo(...), await retryTo(...), or other async effects will either fail with a syntax error (await only valid in async functions) or not be properly awaited before the recorder delay and page-state capture, breaking the new effects-based knowledge code. [logic error]

Severity Level: Critical 🚨
- ❌ Knowledge code with await tryTo/retryTo throws SyntaxError on execute.
- ❌ Agent actions using documented async effects cannot run successfully.
- ⚠️ Async snippets returning promises are never awaited, risking races.
- ⚠️ Page state capture at src/action.ts:198 may occur before effects complete.
Suggested change
const codeFunction = new Function('I', 'tryTo', 'retryTo', 'within', 'hopeThat', codeString);
codeFunction(this.actor, tryTo, retryTo, within, hopeThat);
const codeFunction = new Function(
'I',
'tryTo',
'retryTo',
'within',
'hopeThat',
`return (async () => { ${codeString} })();`
);
await codeFunction(this.actor, tryTo, retryTo, within, hopeThat);
Steps of Reproduction ✅
1. In the repository documentation at `docs/knowledge.md:139-140`, knowledge code is shown
using async effects, e.g.:

   `await tryTo(() => I.click('.cookie-dismiss'));` and `await retryTo(() => { ... }, 5,
   500);`.

2. In runtime or a test, construct an `Action` instance from `src/action.ts:28-47` and
call its `execute` method at `src/action.ts:176-221` with a code string that matches the
documented pattern, for example:

   `await action.execute("await tryTo(() => I.click('.cookie-dismiss'));");`.

3. Inside `execute`, at `src/action.ts:192`, the code builds a dynamic function via

   `new Function('I', 'tryTo', 'retryTo', 'within', 'hopeThat', codeString)` using the
   provided `codeString` as the function body.

4. Because the generated function is a regular (non-async) function and `codeString`
contains a top-level `await`, the JavaScript engine throws a `SyntaxError` ("await is only
valid in async functions or at the top level") when evaluating the `new Function` call at
line 192, so the action fails before `recorder.add`/`recorder.promise` and
`capturePageState` at lines 195-198 can run; async effects like `tryTo`/`retryTo` never
execute.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/action.ts
**Line:** 192:193
**Comment:**
	*Logic Error: In `execute`, the dynamically created function is a regular (non-async) function that is immediately invoked without `await`, so any code block using `await tryTo(...)`, `await retryTo(...)`, or other async effects will either fail with a syntax error (`await` only valid in async functions) or not be properly awaited before the recorder delay and page-state capture, breaking the new effects-based knowledge code.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

src/action.ts Outdated
Comment on lines 235 to 237
codeFunction = new Function('I', 'tryTo', 'retryTo', 'within', 'hopeThat', codeString);
}
codeFunction(this.actor);
codeFunction(this.actor, tryTo, retryTo, within, hopeThat);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: In expect, string-based expectations are compiled into a non-async function and invoked without await, so expectation code that uses await tryTo(...), await retryTo(...), or other async effects will either be syntactically invalid or not be awaited before recorder.promise(), meaning assertions and retries may not complete in time and can behave unpredictably. [logic error]

Severity Level: Major ⚠️
- ❌ String-based expectations with await tryTo/retryTo crash on parse.
- ⚠️ Agent expectation checks cannot reliably use documented effects.
- ⚠️ Async expectation snippets returning promises are never awaited.
- ⚠️ Recorder at src/action.ts:238 may resolve before async assertions finish.
Suggested change
codeFunction = new Function('I', 'tryTo', 'retryTo', 'within', 'hopeThat', codeString);
}
codeFunction(this.actor);
codeFunction(this.actor, tryTo, retryTo, within, hopeThat);
codeFunction = new Function(
'I',
'tryTo',
'retryTo',
'within',
'hopeThat',
`return (async () => { ${codeString} })();`
);
}
await codeFunction(this.actor, tryTo, retryTo, within, hopeThat);
Steps of Reproduction ✅
1. The documentation at `docs/knowledge.md:139-140` demonstrates async effects usage like

   `await tryTo(() => I.click('.cookie-dismiss'));`, which is a realistic pattern to reuse
   in expectation snippets.

2. In code that uses the `Action` class from `src/action.ts:28-47`, call the `expect`
method defined at `src/action.ts:224-265` with an async string snippet, for example:

   `await action.expect("await tryTo(() => I.click('.cookie-dismiss'));");`.

3. Inside `expect`, the string branch at `src/action.ts:231-236` executes, constructing a
dynamic function with

   `new Function('I', 'tryTo', 'retryTo', 'within', 'hopeThat', codeString)` where
   `codeString` contains the top-level `await`.

4. Because this dynamically created function is not declared as `async`, the top-level
`await` in `codeString` is invalid, so the engine throws a `SyntaxError` before
`codeFunction(this.actor, tryTo, retryTo, within, hopeThat);` at line 237 and before
`await recorder.promise();` at line 238, preventing async expectations and retries from
running.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/action.ts
**Line:** 235:237
**Comment:**
	*Logic Error: In `expect`, string-based expectations are compiled into a non-async function and invoked without `await`, so expectation code that uses `await tryTo(...)`, `await retryTo(...)`, or other async effects will either be syntactically invalid or not be awaited before `recorder.promise()`, meaning assertions and retries may not complete in time and can behave unpredictably.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

const serializedUrl = JSON.stringify(url);
const currentState = this.stateManager.getCurrentState();
const actionResult = currentState ? ActionResult.fromState(currentState) : null;
const actionResult = currentState ? ActionResult.fromState(currentState) : new ActionResult({ url });
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Knowledge parameters for navigation (statePush, wait, waitForElement, code) are derived from the current page state instead of the target URL, so after the first navigation the knowledge rules for the destination page will not be applied and code like waits or hooks defined for that URL will silently never run; always constructing the lookup state from the target URL fixes this. [logic error]

Severity Level: Major ⚠️
- ⚠️ Explorer.visit ignores knowledge for destination URL during navigation.
- ⚠️ Knowledge `wait` and `waitForElement` often never applied.
- ⚠️ Knowledge `code` blocks not executed for target pages.
Suggested change
const actionResult = currentState ? ActionResult.fromState(currentState) : new ActionResult({ url });
const actionResult = new ActionResult({ url });
Steps of Reproduction ✅
1. Note that Explorer navigation uses knowledge parameters in `src/explorer.ts:200-215`
within `async visit(url: string)`. It creates `currentState` from
`this.stateManager.getCurrentState()` at line 204 and then builds `actionResult` from that
state at line 205: `const actionResult = currentState ?
ActionResult.fromState(currentState) : new ActionResult({ url });`.

2. Observe how knowledge is selected in `src/knowledge-tracker.ts:200-213`.
`getStateParameters(state, keys)` calls `getRelevantKnowledge(state)` (line 201), which
filters `this.knowledgeFiles` with `state.isMatchedBy(knowledge)` (lines 72-77) and logs
matches using `state.url` (line 80). This means the URL used for matching comes from the
passed `ActionResult`.

3. Configure a knowledge file for a destination URL only. For example, using the same
setup pattern as `tests/unit/explorer.test.ts:207-238` (which constructs an `Explorer`
with a base URL), create a markdown file in the knowledge directory determined in
`KnowledgeTracker`'s constructor (`src/knowledge-tracker.ts:22-37`) with frontmatter:

   `url: mock://explorer.test/second` and `wait: 5`. Do not create any knowledge for
   `mock://explorer.test/first`.

4. In a test similar to `tests/unit/explorer.test.ts:252-258`, first put the explorer into
a known "current" state for `/first` (e.g., call
`stateManager.updateStateFromBasic('mock://explorer.test/first', 'First', 'manual')` as in
`beforeEach` at lines 241-244), then call `await
explorer.visit('mock://explorer.test/second')`. At this moment, `currentState` (line 204
in `src/explorer.ts`) still describes `/first`, so `actionResult` is built from the
`/first` state. `KnowledgeTracker.getStateParameters` (line 200 in
`src/knowledge-tracker.ts`) therefore matches knowledge against `/first`, not `/second`,
finds no `wait`/`code` for `/second`, and the subsequent blocks in `Explorer.visit` (lines
217-227) never execute the configured `wait` or `code` for `/second`. The destination-page
knowledge silently never runs, demonstrating the bug.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/explorer.ts
**Line:** 205:205
**Comment:**
	*Logic Error: Knowledge parameters for navigation (statePush, wait, waitForElement, code) are derived from the current page state instead of the target URL, so after the first navigation the knowledge rules for the destination page will not be applied and code like waits or hooks defined for that URL will silently never run; always constructing the lookup state from the target URL fixes this.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎


if (pattern.startsWith('^')) {
try {
return new RegExp(pattern.slice(1)).test(path);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Regex URL patterns are handled incorrectly because the leading caret (^) is stripped before building the RegExp, which changes the intended anchoring semantics (e.g., a pattern like ^/admin$ will also match /foo/admin), causing hooks to run on URLs they shouldn't match. [logic error]

Severity Level: Major ⚠️
- ⚠️ Navigator beforeHook may run on unintended nested URLs.
- ⚠️ Researcher hooks can execute on more pages than configured.
- ⚠️ Tester and captain hooks may misfire, affecting automation flow.
- ⚠️ Regex hook semantics differ from standard anchored regex expectations.
Suggested change
return new RegExp(pattern.slice(1)).test(path);
return new RegExp(pattern).test(path);
Steps of Reproduction ✅
1. Configure a regex-based hook pattern in the user config file loaded by `ConfigParser`
(see `src/config.ts:161-213` and `findConfigFile` at `src/config.ts:293-304`). For
example, in `explorbot.config.js`, set `ai.agents.navigator.beforeHook` to an object like
`{ '^/admin$': { type: 'playwright', hook: async ({ page }) => { /* admin-only setup */ }
} }`.

2. Start Explorbot so that `ConfigParser` loads this config and an `Explorer` is created
with it (Explorer stores the config in its `config` field, see `src/explorer.ts:45-53`).
Then create a `Navigator` instance, which constructs a `HooksRunner` with `new
HooksRunner(explorer, explorer.getConfig())` (see `src/ai/navigator.ts:54-63`).

3. Call `Navigator.visit('/foo/admin')` (method defined at `src/ai/navigator.ts:65-91`).
Inside this method, after navigating, it calls
`this.hooksRunner.runBeforeHook('navigator', url)` with `url` equal to `'/foo/admin'`
(lines `69-71`).

4. In `HooksRunner.runBeforeHook` (`src/utils/hooks-runner.ts:14-20`), `runHook` is
invoked, which fetches `agentConfig.beforeHook` (line `26`), then calls `findMatchingHook`
(line `29`). `findMatchingHook` delegates to `matchesPattern`
(`src/utils/hooks-runner.ts:80-98`), where the pattern `'^/admin$'` enters the
`pattern.startsWith('^')` branch at lines `89-95`. The current implementation constructs
`new RegExp(pattern.slice(1))`, i.e. `/admin$/`, which returns `true` for the path
`'/foo/admin'` because it only checks the end of the string. As a result, the admin-only
hook configured for `'^/admin$'` is executed for `'/foo/admin'` as well, even though
`'^/admin$'` as a regex should only match the exact path `'/admin'`.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/utils/hooks-runner.ts
**Line:** 91:91
**Comment:**
	*Logic Error: Regex URL patterns are handled incorrectly because the leading caret (`^`) is stripped before building the `RegExp`, which changes the intended anchoring semantics (e.g., a pattern like `^/admin$` will also match `/foo/admin`), causing hooks to run on URLs they shouldn't match.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

@codeant-ai
Copy link

codeant-ai bot commented Feb 7, 2026

CodeAnt AI Incremental review completed.

- Combined effects support (tryTo, retryTo, within, hopeThat) with code sanitization
- Kept hooks runner in navigator while removing redundant URL expectation
- Added isErrorPage import to researcher

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@DavertMik DavertMik merged commit a64466f into main Feb 7, 2026
0 of 4 checks passed
@DavertMik DavertMik deleted the feature/agent-hooks branch February 7, 2026 23:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL This PR changes 500-999 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant