The Monday Morning Agent

How query-plays.mjs works

This is the one program the agent is allowed to run in episode 2. It answers two shapes of question: how many play descriptions contain some words, and how often a defined play converts against every other short-yardage rush. For the second, it will not measure a play until the play is written down in the dictionary. It prints the definition it used and every filter, and it was written before the agent existed.

What it is written in

JavaScriptOne file, 392 lines, rewritten for this episode. The .mjs ending means it is a JavaScript module: a file that brings in other code with import.
Run by Node.jsNode.js is the program that runs JavaScript outside a web browser. You start the tool by typing node ./query-plays.mjs and then the filters.
Nothing to installIt imports three things, and all three come with Node itself: node:fs to read a file, and node:path and node:url to find the data and the dictionary next to the script. There is no package.json and no npm install.
Not written by the agentThe agent does not write or change this code. It chooses the filters, and it writes dictionary entries, which are data the tool reads, not code.

Two shapes of question

A search--search "tush push" --seasons 2021-2026. It counts the play descriptions that contain the words, in any case, season by season, with up to five examples. When nothing matches it says so: NO MATCH. It takes only the seasons, the season type, and the weeks.
A defined play against the field--play tush-push --seasons 2021-2025. It reads the dictionary entry of that name, keeps the rushes with the entry's yards to go or fewer, and splits them in two: the defined play, and every other short-yardage rush. Then it counts how often each converted. Flags such as --team, --not-team, --downs, and --by season narrow both groups or split the result.

What happens when it runs

Six stages, top to bottom, the same every time. A search skips the dictionary and stops after the first three filters. The left column is what the code does. The right column is what that looked like for one real command, node ./query-plays.mjs --play tush-push --seasons 2021-2025, on October 12, 2026, with the reference definition on file as a rehearsal entry.

1

Read the command

It reads the filters typed after its name and checks each one. A flag it does not know, or a value that makes no sense, stops it right there with a message and exit code 64. It never guesses what you meant. A search with a team or a downs flag stops too.

Lines 105 to 152

In the example

--play tush-push      the dictionary entry to apply
--seasons 2021-2025   five seasons
everything else       left at its default
If every flag is valid, it goes on.
2

Read one dictionary entry

It opens dictionary/tush-push.json, and only that file, and checks every key against the entry's fixed shape. With no entry of that name it stops with exit code 65 and prints the shape an entry takes. An entry with a missing key, an unknown key, or a value outside the choices stops it with exit code 64 and the reason.

It reads one entry and prints it before any filter. There is no flag for a definition made up on the spot, so whatever it measures has a written, named, dated definition, and the printout quotes it. A search skips this stage.

Lines 54 to 103, called at 153 to 159; printed at 363 to 370

In the example

DEFINITION, FROM THE DICTIONARY
  dictionary/tush-push.json, written 2026-10-12
    by a rehearsal, not the analyst
  "A quarterback run with two or fewer yards to go,
   not from shotgun."
  rules: the runner is the team's primary passer
    that game (the player with its most pass
    attempts); not from shotgun; any down;
    2 or fewer yards to go
  compared with: every other rush with the same
    yards to go
  cannot see: who pushed the runner, or any
    formation detail beyond shotgun or not
The definition is fixed before a single play is read.
3

Load the plays

It reads data/plays.csv into memory, one row per play. The play description is the last column, because it is the only one that can hold a comma. This is the only data file it opens, and it only reads it.

Lines 161 to 193

In the example

260,927 plays
31 columns each, of the 372 in the raw data
no betting column among them
Every play is now in memory. Nothing has been removed yet.
4

Apply the filters, in order, and write each one down

The seasons, the season type, and the weeks come first, for both shapes of question. For a defined play, five more follow: the team with the ball, the downs, a rush, the yards to go the entry sets, and goal to go. After each one it records the filter and the plays left, even when the filter removed nothing.

Before the rush filter removes the passes, it finds each team's primary passer in each game, the player with the most pass attempts, so the passes it counts from are still there.

Lines 195 to 230, then 273 to 297; the primary passer at 278 to 290

In the example, plays left after each

  1. 0. The file260,927
  2. 1. Seasons 2021 to 2025247,284
  3. 2. Regular season and playoffs247,284
  4. 3. All weeks247,284
  5. 4. Team with the ball: any247,284
  6. 5. Downs: any247,284
  7. 6. A rush by the team with the ball74,608
  8. 7. Yards to go: 2 or fewer, the entry's distance11,331
  9. 8. Goal to go: any11,331
Only the plays that passed every filter go on. Pink is what a filter took out.
or

A search ends here: read every description left

After the first three filters, a search reads each play's description and counts the ones that contain the words, season by season. It keeps up to five examples. With no match it prints NO MATCH and the count it searched.

Lines 232 to 271

For --search "tush push" --seasons 2021-2026

season   plays searched   containing "tush push"
2021             49,922            0
2022             49,434            0
2023             49,665            0
2024             49,492            0
2025             48,771            0
2026             13,643            0
all             260,927            0

NO MATCH: not one of the 260,927 descriptions
contains "tush push".
A defined play goes on.
5

Split what is left by the definition, and count

Each rush left either matches the entry's rules or it does not: who carried, shotgun or not, which downs. The matches are the defined play; the rest are the other short-yardage rushes. A play converted when it gained the first down or scored. Fewer than 50 defined plays adds a small-sample warning; none at all, a NO RESULT line.

Lines 299 to 335

In the example

11,331 rushes with 2 or fewer yards to go
 1,554 match the definition
 9,777 are every other rush in the same situation
The numbers are done. All that is left is to print them.
6

Print the definition, then the filters, then the result

It prints the question, the entry from stage 2, quoted, the filter list from stage 4, the split from stage 5, and the result: plays, converted, and rate for each group, and the gap between them. Last, one standing reminder: whether the play should be legal is not in the output. With --json it prints the same content as data, which is how the explorer page (published after the episode airs) gets its answers.

Lines 337 to 392

In the example

RESULT
  group                            plays   converted       rate
  1  the defined play               1,554       1,199      77.2%
  2  other short-yardage rushes     9,777       6,567      67.2%
  gap, the defined play minus the others: 10.0 points

NOT IN THIS OUTPUT
  Whether the play should be legal.

The agent reads that printout and nothing else. It never sees a play.

What it never does

These are properties of the code, not promises in a prompt. You can confirm each one by reading the file.

It never writesNo file is created, changed, or deleted, the dictionary included. The agent writes entries; the tool only reads them.
It never goes onlineNo network call of any kind. It works with Wi-Fi off.
It never sees a betting lineThe file it reads has 31 columns, and none of them is one.
It never guessesGive it the same filters, the same entry, and the same file, and it prints the same numbers, every time.
It never measures a definition that is not written downNo entry, no number. It stops and prints the shape of an entry instead.

The runner: run-analyst.sh, in nine beats

The tool above is what the agent calls. The runner is what you call: a shell script of about 250 lines that hands the agent one claim and makes its work visible. The first six beats are episode 1's. The last three are new: they check the agent's memory in code rather than by asking.

1. Takes the claimFrom the command line, or from claim.txt when you give none.
2. Hands over the policyIt reads analyst.md and passes it to claude -p as the system prompt. The model is unchanged.
3. Sets the allowlistTwo lines: Bash(node ./query-plays.mjs:*), the one script with any arguments, and Edit(//<repo>/dictionary/*.json), a new .json file in the dictionary folder. No file reads, no other writes, no network. The path is absolute because a rule with ../ in it never matches.
4. Streams the runIt asks for the event stream instead of one answer at the end, and pipes it through show-run.mjs, which prints each tool call as it lands and shows each entry written in full.
5. Opens the run as a pageWhen the run ends, render-run.mjs writes the whole run as one page, the ruling and the entry it cites at the top, every call down a clock, and the runner opens it.
6. Holds the word limitUnder 400 words above the commands, up from 350 because the ruling gained a part. Over the limit, the ruling goes back once with "shorten it" and no tools. Still over, the run fails with exit code 68.
7. Appends the dictionaryBefore the run it copies the dictionary aside, then appends every entry to the policy under "The dictionary today," with today's date. The agent has no Read tool. This is how it remembers what it wrote.
8. Requires the citationAfter the run it reads the saved event stream for every --play NAME the agent ran, and checks that the ruling has a "Definition used" line naming that entry. Missing, the ruling goes back once with no tools. Still missing, the run fails with exit code 69.
9. Restores a changed entryIt compares each entry that was on file with the copy made before the run. One that changed is put back, and the run fails with exit code 70. A new entry is checked with the tool; one the tool rejects fails the run with exit code 71.

Nothing in the runner computes a number. It moves the claim, the policy, the dictionary, and the tool's printout between the pieces, and checks what came back.

See it for yourself

# the search on this page
node ./query-plays.mjs --search "tush push" --seasons 2021-2026

# every flag it accepts
node ./query-plays.mjs --help

# a play with no entry: it stops and prints the shape of one
node ./query-plays.mjs --play no-such-play --seasons 2021-2025

# the example on this page, once the episode's entry is in the repo
node ./query-plays.mjs --play tush-push --seasons 2021-2025
Read the codeAll 392 lines, with the line numbers this page refers to. The runner is run-analyst.sh, beside it.
The dictionaryThe README gives the shape of an entry, every key, and the rules for adding one.
Change the definitionThe explorer page: the same output for 54 definitions of the play and seven sets of seasons, with nothing to install. Up after the episode airs on October 26, 2026.
How the agent is set upWhere this tool sits: under a written policy, beside a dictionary, behind a two-line allowlist.