Skip to content

Working with an AI Assistant

Many REX contributors work with an AI coding assistant. AGENTS.md gives the assistant REX’s conventions and the placement process from Getting Placement Right, so it explores the right modules before writing code.

Download AGENTS.md and put it at the root of the folder where you cloned the REX modules. Contributing to REX explains how to set up that folder and what to name the file for your assistant.

The full file is below.


AGENTS.md: REX Operating Instructions for AI Assistants

This file is your operating instructions for working on a public REX module. Read it fully before starting.

The contributor working with you may not be an experienced JavaScript or TypeScript developer. Guide them step by step. Explain things clearly. Do not assume familiarity with npm, webpack, browser extensions, Chrome APIs, or GitHub.

A human-facing companion document, Getting Placement Right, covers the most important conceptual decision (where the feature belongs in the module hierarchy) for contributors who want to read about it themselves. You do not need to read it, since the same material is in this file, expanded with agent-specific behavior. It is the canonical reference if the contributor asks you to point them at one.


The Contributor’s Outcome May Not Be a PR

Not every session ends in a pull request, and you should not assume one. A contributor might be:

  • Trying something out on a branch to see if it works, with no immediate plan to upstream it.
  • Patching their own extension via a private fork (Path A in Contributing to REX): pinning their study extension’s package.json at their fork and shipping. This is a fully supported way to use REX; BRIC just cannot support the fork itself.
  • Building a feature they intend to PR (Path B). The contribution discipline matters most here.

Ask the contributor early what their goal is if it is not obvious. Calibrate accordingly: experimentation gets latitude; a planned PR gets the full rigor of the PR checklist. Do not herd a contributor toward a PR if that is not what they want. “Let me try this and see” is a legitimate outcome, and pushing them harder than they asked for is unhelpful.

That said: the placement discipline applies to all paths. Even an experimental branch benefits from being placed at the right level of generality, because most experiments become real PRs eventually.


Starting a Session

Before writing any code, run an exploratory pass across the repositories in this working directory.

The contributor has cloned a standard set of REX module repos into a single folder. Your first task is to understand where the feature belongs, at the right level of abstraction in the module hierarchy. REX has a generality hierarchy: rex-core at the root, shared infrastructure modules like rex-spider in the middle, and child modules like rex-spider-chatgpt at the most specific level. A change that benefits all spider modules belongs in rex-spider, not in a child module. A change that only makes sense for one site stays in the child module.

The #1 reason contributions get rejected or rewritten: wrong level of generality

Placing the feature at the correct level of generality is the single most challenging part of contributing to REX, and getting it wrong is by far the most common reason a PR is sent back for rework. A feature put in the wrong place is rarely “broken”: it works, the tests pass, the build is clean. It is wrong in a more subtle way. It ossifies an assumption that does not belong at that layer, or it duplicates logic that should have lived one level up, or it pollutes a generic module with something specific to one study or site. These are exactly the kinds of mistakes that look fine in isolation and only reveal themselves when the next researcher tries to reuse the module.

Because the design target is “what serves REX as a framework,” not “what makes this study work,” the bar for placement is higher than in a typical single-product codebase. Treat the placement decision as the most important architectural call of the session. Get it right before any code is written. If you are not confident about the right level, say so and ask the contributor. Do not guess.

Step 1: Get a real description of the feature from the contributor

Before exploring anything, make sure you actually understand what the contributor wants to build. A one-line feature title is not enough. Ask the contributor to describe, in some detail:

  • What the feature does: the observable behavior, end to end
  • Who it is for: only their study, or potentially any researcher using REX
  • What triggers it: a user action, a page event, a config flag, a schedule
  • What data it touches or produces: what is read, what is written, what is sent where
  • Which sites or contexts it applies to: one site, a category of sites, all sites
  • Why it cannot be done with existing modules: what is missing today

If the contributor’s description is vague (“add tracking for X”, “support Y”), ask follow-up questions until you have a concrete picture. Do not move on to exploration with a fuzzy mental model; you will explore the wrong things.

Step 2: Explore the repos to find the right level of generality

Once you have a real description, explore the repositories in depth before proposing where the feature belongs. Read the actual source files (.mts files in each module’s src/ directory), not just READMEs. READMEs are often minimal. The code is the authoritative record of what each module does.

Check the module map against what is actually cloned. The contributor may only have cloned the repos they thought were relevant when they started. Based on the feature description, scan the module map for any modules that look like plausible homes, including ones the contributor did not initially consider, and ask them to clone those repos so you can read the source. It is perfectly fine, and often necessary, to say something like: “Based on what you described, rex-page-events and rex-lists could also be the right home for this. They are not in this directory. Could you clone them from github.com/bric-digital/rex-page-events and github.com/bric-digital/rex-lists so I can look at the source before deciding?” Do not make a placement call without having read the source of every plausible candidate.

Suggested opening exploration prompt for the contributor to use, after they have described the feature:

Now that I have described the feature, please explore all the repositories in this directory in depth to determine: (1) which module is the right place for this feature, (2) at what level of the hierarchy it should live (aim for the highest level of generality that is still appropriate, without going so high that the module takes on responsibilities that do not belong to it), (3) whether any similar functionality already exists that we should build on or extend rather than duplicate, (4) whether any part of this feature is study-specific and belongs in my private extension repo instead of a public module. Read the actual source files, not just READMEs. Report your findings and propose an approach before we start implementation.

Once the right module and the right level of generality are clear, confirm both with the contributor before writing any code. If the contributor disagrees with your placement, take the disagreement seriously; they often know the domain better than the exploration can reveal.


What REX Is

REX (Research EXtensions) is a modular browser extension framework for behavioral research. It is made up of independent TypeScript modules that are composed together into Chrome/Edge extensions for specific research studies.

You are helping a contributor modify one of these public modules. The modules live at github.com/bric-digital/<module-name>.

The full module map is at docs.bric.digital/rex/module-map. Read it to understand where functionality belongs before writing any code.


The Public/Private Boundary

This is the most important rule. Public modules must be generic and reusable across any researcher’s study.

Never put any of the following into a public module:

  • Study-specific logic or participant flows
  • Participant identifiers or study IDs
  • Client names or organization names
  • Hardcoded URLs specific to one study
  • Any assumption that only makes sense for one research project

If the contributor’s feature is study-specific, it belongs in their own extension repository, not in the public module. Ask the contributor before proceeding: “Would a researcher at a completely different university find this feature useful?” If no, redirect the changes to their private repo, which should be cloned into a sub-directory of the one you are working in.


The Cardinal Rule: Never Bypass rex-core

Modules must never read from chrome.storage.local directly for keys that rex-core manages. Always use the message-passing API:

What you needCorrect approach
The participant identifierchrome.runtime.sendMessage({ messageType: 'getIdentifier' })
The extension configurationrexCorePlugin.fetchConfiguration()
Set the identifierchrome.runtime.sendMessage({ messageType: 'setIdentifier', identifier: ... })
Refresh config from serverchrome.runtime.sendMessage({ messageType: 'refreshConfiguration' })

If you see code reading chrome.storage.local.get('rexIdentifier') or chrome.storage.local.get('REXConfiguration') directly, that is a bug. Replace it with the appropriate message call above.

rex-core is also a shared library, not only a message broker

rex-core exports utility functions that other modules are expected to call directly: sha256() and related helpers live in rex-core/src/common.mts. Before adding a new npm dependency to any module, check whether rex-core (or another rex-* module already in the dependency graph) provides the capability.

Concretely, before running npm install <something>:

  1. Grep rex-core/src/ for the capability. Common categories that already live there or in sibling modules include crypto and hashing, registrable-domain extraction (psl is used in rex-core, rex-history, and rex-lists), storage access, configuration, and messaging.
  2. If rex-core has the capability, use it. Do not vendor a parallel library into a downstream module.
  3. If rex-core’s version is missing the case you need (for example, a synchronous fallback or a plain-HTTP-safe path), the correct move is to extend rex-core, not to add a parallel implementation in the calling module. Surface the gap to the contributor and propose the rex-core change as part of the work.
  4. Crypto specifically: do not bring crypto libraries into a module. Crypto belongs in rex-core. If rex-core’s current crypto helper does not cover the case, that is a rex-core change. Never roll your own implementation in a downstream module, even for non-security uses like deterministic randomization.

The reason this rule is strict: every parallel implementation is a future maintenance burden, a divergence risk, and one more thing a reviewer has to keep in their head. A change that looks like “use a well-known library instead of hand-rolled code” can still be the wrong answer if the library duplicates a capability rex-core already owns.


Module Conventions

File structure

Each module has up to three TypeScript entry points, listed in the exports section of its package.json:

  • src/browser.mts: injected into web pages as a content script (DOM access, page events)
  • src/extension.mts: the extension’s own pages
  • src/service-worker.mts: the background service worker (Chrome API access, storage, config)

A module registers itself when an extension imports it. Source files use the .mts extension (TypeScript ES modules). All modules use @bric/ scoped package names.

Browser-context dependency budget

browser.mts can be injected into every page the participant visits. Anything imported there ships into the user’s browsing context, runs under the page’s CSP, and adds to the size of the injected bundle.

The default budget for new browser.mts dependencies is zero. Adding one requires:

  1. A specific reason it cannot run in service-worker.mts instead. The service worker has full Chrome API access, no CSP constraints, and a single instance per browser rather than one per page. Most “I need to compute X” tasks belong there, with chrome.runtime.sendMessage carrying the request and response across the boundary.
  2. A check against the rule above (“rex-core is also a shared library”). If the capability already exists somewhere in the rex-* graph, use it instead of adding a new dependency.
  3. A note in the PR description naming the dependency, its size, and the reason a service-worker round-trip was not viable.

This applies to new dependencies, not to ones already present in the module.

ES2022+ hazards: stay conservative

REX modules publish raw TypeScript source. Consumers compile that source through their own toolchain, which may not support ES2022+ features. Do not use these in module source:

  • new Error(msg, { cause: err }); use the single-argument form instead
  • Array.prototype.at()
  • Object.hasOwn()
  • structuredClone()
  • Private class fields (#foo)
  • Top-level await

If a linting rule pushes you toward one of these, use the single-arg form and add a comment:

// eslint-disable-next-line preserve-caught-error -- Error cause {} ctor arg is ES2022, not available in consumer toolchains
throw new Error(`Failed: ${error.message}`)

Promise-based validation

All validation logic must return a Promise, not a synchronous boolean. Validation often grows to include async operations (server lookups, storage reads), and a synchronous API forces breaking changes later.

// correct
validateIdentifier(id: string): Promise<ValidationResult>
// wrong
validateIdentifier(id: string): boolean

Timestamps

Never call a timestamp generator more than once for a single logical event. Capture it once and reuse it:

// correct
const now = Date.now()
record.createdAt = now
record.updatedAt = now
// wrong
record.createdAt = Date.now()
record.updatedAt = Date.now()

Async/await

Always handle both success and error cases for async operations. Prefer .catch() and .finally() where the error handling is straightforward; prefer try/catch blocks for complex flows. Never leave a Promise unhandled.

UI conventions (extension pages)

Extensions use Bootstrap 5. Always prefer Bootstrap utility classes over inline styles. Only use inline styles for values Bootstrap has no utility for (e.g. a specific max-width in pixels).


Minimal Changes Policy

Changes must be the absolute minimum necessary. The contributor must be able to justify every changed line in a PR review.

  • Do not refactor, reformat, or rename surrounding code
  • Do not add comments, types, or documentation to code you did not change
  • Do not add error handling for scenarios that cannot happen
  • Do not add backwards-compatibility shims when you can simply change the code
  • If you see a problem in surrounding code that is not part of the task, flag it to the contributor rather than fixing it silently, but do not ignore significant problems either

Before Writing New Code

If you have not already done the exploratory pass described in “Starting a Session” above, do it now before continuing.

  1. Confirm you are in the right module and at the right level of the hierarchy (see “Starting a Session”).
  2. Search the module’s source for existing functionality that might serve the need.
  3. Search other rex-* modules for utilities that already exist. Do not re-implement what is already there.
  4. If you are unsure whether a new function or abstraction is warranted, flag the question to the contributor before writing it.
  5. If the feature might belong in a new module rather than an existing one, check the criteria in Getting Placement Right before proceeding.

Testing and Build

The contributor tests changes by loading a built extension in Chrome or Edge. The extension for testing is either rex-demo (the public demo extension) or the contributor’s own extension repository, whichever they prefer. Ask which one they want to use if it is not obvious from the working directory. Module changes must be pushed to GitHub before they can be tested in an extension; local file paths in package.json are not allowed.

To test a module change:

  1. Commit your changes to a feature branch in the module repo, and ask the contributor to push the branch to GitHub.
  2. In the extension’s package.json (either rex-demo/package.json or the contributor’s own extension), update the module reference to point at that branch:
    "@bric/rex-page-manipulation": "github:bric-digital/rex-page-manipulation#your-branch-name"
  3. In the extension’s directory, run:
    Terminal window
    npm run clean-build:scrub
    npm cache clean --force
    rm -rf node_modules package-lock.json
    npm install
    npm run build
  4. In Chrome or Edge, go to chrome://extensions, enable Developer Mode, click “Load unpacked”, and select the extension’s dist/extension/ folder (e.g. rex-demo/dist/extension/).
  5. Verify the change made it into the built bundle before asking the contributor to test.

Never use local file paths in package.json:

// wrong
"@bric/rex-page-manipulation": "file:../rex-page-manipulation"
// correct
"@bric/rex-page-manipulation": "github:bric-digital/rex-page-manipulation#feature-branch"

PR and Contribution Rules

Skip this section if the contributor is on Path A or just experimenting. It applies when they intend to open a public PR (Path B).

The PR checklist describes what a public PR needs. Your job is to make the contributor’s PR satisfy it, not to re-derive it.

Workflow:

  1. Fork the public module repo on GitHub
  2. Create a branch for the change (fix/... or feat/...)
  3. Make changes following all conventions above
  4. Open a pull request from the fork back to the original repo
  5. CLA Assistant will prompt for signature on the first PR; this is required for external contributors

Commit messages: One line, imperative verb, lowercase, no period. Examples:

add scroll depth to page events
fix url redirect rule not removed on disable

AI use disclosure (mandatory in the PR body): If you (the agent) wrote or rewrote any committed code, the contributor must disclose this in the PR description. Tell the contributor what to say: name the AI tool and the rough scope (e.g. “drafted the scroll depth tracking in src/browser.mts; contributor reviewed and modified”). By making that disclosure, the contributor is also confirming that they have reviewed every line, can explain the design choices in review, and have the right to contribute the code. Do not let them make it without actually having reviewed the code with you.

Preserve comments by audaciouscode. These comments often capture domain knowledge, historical decisions, or non-obvious constraints that are not recoverable from the surrounding code. Do not delete or rewrite them as part of unrelated changes. Check git blame if you are unsure of authorship. The only exception is when such a comment has become inaccurate because the code it describes has changed; in that case, flag it to the contributor rather than silently editing it.

Interpreting review feedback

When a PR comes back with review comments, the failure mode to watch for is acting on the literal reading of a comment while missing the structural reading. Architectural advice (“use X instead of Y”, “this belongs in module Z”, “don’t import A into B”) almost always has both.

Before implementing a response to review feedback:

  1. Read each comment twice. Once for the literal instruction, once for the architectural point underneath it. “Don’t roll your own crypto” has a literal reading (swap to a library) and a structural reading (crypto belongs in rex-core as a layer boundary). These are different changes.
  2. If a comment has more than one plausible reading, write your interpretation back to the reviewer as a one-liner before implementing. A single comment like “To make sure I have this right: you want me to (a) extend rex-core’s sha256() to handle plain-HTTP pages, and (b) call it from browser.mts via the existing import, rather than vendor a new SHA library into this module. Is that what you had in mind?” costs nothing and prevents a rewrite.
  3. Treat reviewer sketches as the target design, not the floor. If the reviewer drew a schema or named specific functions, that is the architecture they want. If your implementation has the same names but a different structure underneath, you have not addressed the comment.
  4. If the reviewer asked for a public API (registration, extension points, hooks), build the public interface, not the private dispatcher. Punting the public API back to the reviewer with “I left it for you” is what the previous comment was already asking you not to do.
  5. Watch the size signal. If your response to a review nudge grows the diff substantially rather than shrinking it, that is usually a sign the rework went the wrong direction. Stop and ask before pushing.

The cost of a clarifying question is one round-trip. The cost of a misread rework is the reviewer giving up and rewriting the PR themselves.

Never commit:

  • Plan files or scratchpad documents (add them to .gitignore)
  • .env files or files containing secrets
  • Changes to package.json that point at local file paths

Never push. Suggest the commit message to the contributor and stop. The contributor decides when to push and when to submit the PR.


What to Never Do

  • git push yourself; the contributor pushes
  • Read chrome.storage.local.get('rexIdentifier') directly; use the message API
  • Read chrome.storage.local.get('REXConfiguration') directly; use rexCorePlugin.fetchConfiguration()
  • Hardcode any domain name, study ID, participant ID, client name, or study-specific URL in module code
  • Use "file:../rex-core" or any local file path in package.json
  • Use ES2022+ features listed in the hazards section above
  • Refactor or reformat code outside the scope of the task
  • Commit plan files or .env files
  • Make any change you cannot explain in one sentence to a PR reviewer