r/placeAn organizational fruit fly

The tools, one way across the tiers

Authoritative source, reproduced unchanged. Exact Markdown ยท Agent copy with resolved links. These instructions govern research runs, not checkpoint interviews. They keep the run's own words: a room is a community, a mark an artwork, the analyst the aggregator, the case lead the community coder and the global model the population model.

Three agents read this study's material through tools: the brief writer (the seven days before the canvas), the coder (one community's week, or a slice of it), and the aggregator (the coders' cases, codes and notebooks across three hundred communities). Each has a reading tool, a citing tool and a search; a fourth, emit, records what the coder decides. This page says what every one of them does the same way, so that an agent that has used one is not surprised by another, and it lists the differences that remain and why. It binds read.py, read_brief.py, cite.py and emit.py now; the aggregator's cite_case.py and mem.py are to be brought to it at v37, and their present differences are listed at the end.

The shell grammar

python3 <tool>.py <CORPUS> <verb> [arg] [--flag ...]

CORPUS is the file the agent was told is its corpus; the verb comes second; one positional argument at most. --help prints the docstring, which lists every verb. Nothing else is positional. A refusal prints one line, <Error>: <why>, to stderr and exits 2: ReadError: no thread 'x' in this corpus, CiteError: not found in dfnrqjq. .... A refusal is the tool saying the call was wrong, never the tool breaking; a traceback is a defect.

The reading tools: read.py (coder), read_brief.py (brief writer)

Both serve the rendered corpus verbatim and log every service. Shared verbs, same meaning:

verb serves logs
header the block of # lines: every count, exact kind: header, chars
thread ID one thread, whole kind: thread, thread_id, items, chars
item ID one item, verbatim, for what find returned kind: item, item_id, chars
state where you are: tool, corpus, threads_in_corpus, threads_served, chars_served, then the tool's own fields not logged

Own verbs, because the two agents ask different questions of the text: the coder asks does this thread touch the event, so read.py has windows [n] (the next n threads as triage windows, or whole when shorter than a window) and audit (served against declared); the brief writer asks what is ordinary here, so read_brief.py has titles [n] [--day d] and sample [n] (whole threads drawn to fill the thinnest days first). read.py has a cursor that advances on serving; read_brief.py has a reference budget and a hard ceiling. Those are the differences, and each has its reason in the tool's docstring.

The log is out/_read.jsonl beside the corpus for both, one JSON record per service: {seq, t, kind, tool, chars, ...} with the verb's own keys as in the table. The log is the record of what the agent saw; it is replayed on construction, so every call is a fresh process and nothing lives in memory. The environment variable READ_LOG_DIR moves the log and is the scheduler's, never the agent's: the shell form has no --out.

The citing tool: cite.py, the same file in both tiers

from cite import Corpus
C = Corpus(RECORD); cite, find = C.cite, C.find
cite(item_id, "the words you believe are there")   -> {item_id, start, end, text}
find("regex", limit=200)                            -> FindResult

cite searches the item's body (a post: title + "\n\n" + selftext), returns the exact span cut from the body with its offsets in Unicode code points, and refuses a quote that is not there, that occurs twice (extend it), or that matches only after normalising quotes and dashes when that match is ambiguous. What the agent typed is discarded; what comes back is the record's. The agent never computes an offset. The shell form is cite.py RECORD ITEM_ID '<quote>' and cite.py RECORD --find '<regex>' [limit].

find returns a FindResult: a list of {item_id, thread_id, start, end, text, snippet}, one hit per item, first match only, that also carries truncated, limit, scanned and not_scanned; the flag, not the hit count, says whether the scan was complete. Every call is appended to <record>.queries.jsonl as {t, kind: find, tool, pattern, n_items_matched, limit, items_scanned, items_not_scanned, truncated}. find grounds a claim; it does not choose what the agent reads (coder Rule 5; brief writer rule 3 allows it freely because the brief describes rather than samples).

The recording tool: emit.py, coder only

emit.code, emit.pointer and emit.thread(excluded_quote=...) take cite's return value unchanged and verify it against the record at the call; a code or a pointer is refused on an item read.py never served. Everything is appended to out/_journal.jsonl as {seq, t, stage, kind, ...}; nothing is edited. The brief writer records nothing through a tool: its brief is prose, checked afterwards by the scheduler (verify_room.py --brief, which resolves every quoted string in the brief through cite against the pre-week record).

What stays different, and why

The aggregator's tools, as they stand (to align at v37)

cite_case.py reads the coders' outputs, not a corpus, so its address is (case, section) rather than item_id:

The aggregator's v36 run is built and stage 00 has run; these changes wait for v37 so the run's tools stay what its manifest recorded.