What this plugin is for, how a project is laid out, what each part of that layout is responsible for, how it all reaches a cluster, how a person and the agent share the work across interruptions, and how those responsibilities are enforced. The README covers installing and using it; this file covers why it has the shape it has.
The goal is: every numerical fact — a number the code produced — is placeable at the moment it is produced, by the agent that produced it, with no extra human step. Placeable means four things resolve from the record alone: the code (a commit), the knobs (a recipe), the machine (an environment), and the description (the entry it sits on — its motivation and its finding).
A second goal follows from letting an agent run for days: an autonomous run must be legible from a page, not from a transcript. The user reads the objective page and knows where the campaign stands. Sessions end, get compacted and are thrown away; the page is what survives them.
A third follows from the first two: the source code and the understanding are distilled as the work proceeds, not after it. A project that only accumulates entries ends with a thousand true facts and nothing reusable. Condensation is therefore an explicit pump with its own trigger (§7).
A project is one monorepo. The layout below is the default; a project declares
its own in .research-note/project.json (below), and every path it does not
declare is the project's own, which the framework neither creates nor
validates.
src/ §3 canonical, tested interfaces
tests/ §3 one test file per src file, mirrored
experiments/ §4 <node>/impl/ the frozen implementation
<node>/<group>/ how it is invoked, and its logs
tests/ experiment-level tests
project_memory/ §5 nodes/ the facts
§6 objectives/ the intent channel
§7 memory/shared/ what we learned, one fact per file
§7 note/ interpretation, prose, per node
wiki/ §7 knowledge/ the standalone knowledge base
.agent/skills/ §7 where `memory-lift` lands a lifted method (§12)
.research-note/ §2 project.json the declared layout and integrations · state/ core
runtime state (old rules/ files are read-only migration input)
.claude/ §10 this project's bindings — contracts, hpc profile, hooks
.agents/ .codex/ the same bindings under the names the other agent reads (§12)
Layers are told apart by path AND by commit prefix, which is what makes the separation mechanical rather than aspirational:
| prefix | may touch |
|---|---|
feat: fix: refactor: |
src/ + tests/ |
exp(<node>): |
experiments/ |
memory: |
project_memory/ |
wiki: |
wiki/ |
agent: |
.research-note/ .claude/ .agents/ .agent/ .codex/ — one prefix for every binding directory (§12) |
chore: |
nothing but root-level files — .gitignore, pyproject.toml, uv.lock, a root doc. The bucket for a commit that belongs to no layer |
The table is the default, not a requirement. .research-note/project.json
declares roles (which directories are the library, the tests, the
experiments, the knowledge base) and layers (which top-level paths each
commit prefix may touch), every managed path owned by exactly one layer, and
project_config.py is the single reader every consumer goes through. An
existing project therefore keeps code/ and trials/ where they are and
declares them, rather than moving them to earn the checks. Because the rules
live in the declaration, the hook that enforces them is framework text again
and an update can replace it — the earlier design put the table INSIDE
commit-msg, which made every project's hook a hand-edited fork that updates
had to diff and port around. A project with no declaration reads as the
standard layout.
Responsibility: hold the capabilities that outlive any one experiment, behind a stable interface, with a test.
Code arrives here one way: a lift (research-note:lift, §7), and what a lift
moves is a function someone designed to be called — never a file. The version
already running inside an experiment is the correctness reference: the src/
function is written afresh for reuse and its signature may look nothing like the
one it came from. Three rules keep it that way.
- Discovery before writing — search
src/INDEX.md, thenexperiments/for prior art. A capability a second experiment needs is lifted, never copied.INDEX.mdis a regenerated view — one row per capability: name and where it lives — and never edited by hand. tests/mirrorssrc/, one file per file, written first and updated in the same change.src/changes ask the user first — it is the one surface several agents work against at once, so the ask is the serializer.
Responsibility: hold the code a result came from, frozen hard enough that the result stays re-derivable.
Under each node, impl/ holds the callable functions a <group>/ invokes, plus
the declaration of which knobs exist — never their values. <group>/ holds the
invocation: run scripts, submit files, outputs, logs.
Keep that surface thin: a mechanism of any size splits into modules behind it. A thin interface is what keeps a behaviour-preserving diff reviewable against its validation entry, and a later lift a designed-API move rather than an untangling job.
A node is frozen in behaviour, not in bytes:
If some knob setting on the new code reproduces the old recorded result, it is the same node. If none does, it is a new node.
An optional knob whose default reproduces every earlier cell stays in-node — that default bound to the structure it came from, never restated as a literal, which drifts and changes what every old recipe meant. A refactor or a result-preserving optimisation stays in-node. A bug fix that moves a recorded number is a new node, and the old one keeps standing under the entries that closed on it — what makes an old result re-derivable rather than archived.
The claim covers everything the node executes — impl/, the src/ it
imports, the pinned deps — not the impl/ folder. A shared module is inside every
node importing it, and a folder rule watches none of them. Its evidence is a
validation entry naming the closed entry it replays, the recipe, and the number
it reproduced. Nothing here says how knobs are expressed, so the criterion survives
the next language.
A recipe is one knob assignment, supplied per run and recorded in the entry that used it (§5) — text in the record, never executed, so any language's knobs fit. Declaring them makes a changed key set a signature diff rather than a silent divergence, and that diff is the prompt to ask the reproduce question. Pre-declare the axes you mean to explore: skip it and a variant that is one weight moved from 0 to 1 spawns a node, owing everything a node owes — a gate and its pages.
One folder per problem instance the node's algorithm is run on, holding what invokes it and the outputs that came back — four kinds, split by what each is for:
<group>/ run + plot scripts committed
figures/ what a person looks at committed
data/ what you read not committed, never deleted
checkpoint/ what you recompute from not committed, stays on this machine
logs/ what the run says not committed — each headed with
code SHA, dirty diff and machine
(config/ for a Hydra pipeline and submit/ for Slurm join them only where
those exist.)
A node opens with one group from the start, so its outputs sit at their final
path before the record cites them. A plot that spans groups sits at the node
root beside impl/, like the node-level page that reports it.
data/ is what you load to READ a result; checkpoint/ is what you load to
PRODUCE one. A table of energies goes in data/; the wavefunction it came from
is a checkpoint, however small. Role decides it, not size — the size difference is the
usual consequence. The split is what lets a result travel off the machine it was
computed on while the state behind it stays put, and it is the only handle any
transfer has: a filter that can only measure bytes cannot tell the two apart.
Scripts and figures are committed, so they reach any clone; results, state and
logs stay on the machine that made them. That is what forces the record to carry
the number itself rather than a pointer to it — a page quoting a value survives,
a link into logs/ does not.
The record, not the file, preserves a result. A page quotes the number (§5) and scripts are in git, so a result file may be rewritten — a rerun regenerates it, and what the old run established stays on its entry. A checkpoint is overwritten only by its own later state: step 500 supersedes step 400 of the same run.
Responsibility: per computation, hold why it was run, what it produced, and what that means. The first is fixed, the second moves only toward what happened, and only the third is rewritten freely.
The record mirrors the code, node for node, and the name is the join:
experiments/<node>/impl/ the implementation
experiments/<node>/<group>/ its invocation and outputs
project_memory/nodes/<node>/ node.md, and node-level task pages
project_memory/nodes/<node>/<group>/ the task pages of one instance
_index.md is the roster, a regenerated view, never edited by hand — one row
per node, per entry and per objective (the section the next free O<nn> is
read from) — so read-first starts from one file and sees the goals, not just
the runs.
A node has two kinds of page:
| page | holds | written when |
|---|---|---|
node.md |
the code's own record — the gate, then every in-place edit's validation | the node is created, then whenever the impl changes and stays the same node (§4) |
| task page | one question's probes | a question starts that no existing page's title covers |
node.md sits at the node's root; a task page sits under the group of the
instance it probes, and a cross-group comparison sits at node level — the
framework reads only that grouping's existence, to find the node. A code change
lands as a validation entry on node.md or as a new node, and a change to shared
src/ owes one on every node whose numbers it could move —
which is why those ask first (§3).
A code entry is an entry. Same fields, same rules, same portability; only the filing differs. Its source is a code path at a commit, an absence, a directive, or the previous validation — and taking the previous validation keeps the validations a chain from the gate forward.
One page for the node, not one per edit. Every validation replays the same reference cell — chosen at the gate — so its claim is "unchanged to within that history's spread", and the weakest link bounds what the chain can claim.
node.md is that page, and it carries the same frontmatter as any page. Its first entry is the
gate: the node reproducing a reference its Motivation names — the parent cell it
forks from, or whatever independent number exists — so whether the node may be believed
is visible before any of its numbers are read. The same Motivation names the node it
supersedes, when there is one: the new node states it, the old is never edited.
Where a new run goes: if the page's title still describes it, another entry there; if the title would have to be rewritten, a new page.
Every page is a list of entries. An entry is a set of computations answering one question with one code at one recipe — a parameter scan is a single entry, one job per point. A probe that exists only because another's result exists is a different entry.
The requirement has two halves. What each field must HOLD — its shape, its
spelling, the form of its sentence — is the contract's, stated field by field in
project_memory-CLAUDE.md and enforced by check_node_page.py; it is not
restated here, because the copy an agent acts on should have no rival. What
belongs here is the other half, which no linter can decide: when a field may
change, and what the change drags with it. Rows are in the order they appear
on the page. "Never" is about the fact, not about the line describing it:
what a run did is fixed, the record of it is always correctable, and only the
second may cascade.
| field | when it changes |
|---|---|
| Id | Never |
| Status Date Jobs |
Status advances, with two moves backward: a correction returns closed to analyzed (below), and resuming an interrupted procedure before it closed returns the entry to running with one more id on Jobs: — an interruption is not a state. Jobs only grows. Date never |
| Code | The commit never; a wrongly recorded SHA is corrected and cascades nowhere. Different code is a different entry, different behaviour a new node (§4) |
| Motivation | The question never — another question is another entry, including a control decided after seeing this result. The quoted fact moves whenever the RECORD of that fact is wrong: this entry misquoted its source, or the source was itself corrected later. The fact was always right; only the record of it can be wrong, and a description that misstates it is corrected wherever it sits |
| Recipe | The assignment never. A missing knob is backfilled and cascades nowhere; a wrong value is restated and cascades |
| Facts | May, toward more complete or more accurate. Backfilling cascades nowhere; correcting a number cascades along every data ref, transitively |
| Conclusion | May, freely; git holds the previous version. A changed datum reaches every conclusion whose because cited it (below) |
| Refs | Grows only |
The record describes what happened. When the two disagree, the description moves. Everything else follows from that one line:
- Numbers never change. They are what happened. A later discovery changes what they mean, never that they were produced.
- Descriptions change — this entry's, and every other whose own description leaned on the same mistake. Nothing else moves.
- An entry is never withdrawn and never deleted. Nothing that happened un-happens, so there is no state for it; a probe that established nothing says so in its verdict.
- A node's identity changes only when its behaviour changes (§4). A free variable becoming a declared knob is neither a new node nor a correction: the old node's identity always included that freedom, and writing it down does not make the old numbers wrong.
The question at every correction is never "is this entry still valid" but which part of the description is now known to be wrong — move that part, and nothing else.
A correction that reaches a closed entry returns it to analyzed. That alone blocks any
probe whose Refs name it and raises the objective's awaiting-review count. A correction
is its own commit, trailered Correct: <id> — the entry whose description moved.
Responsibility: say what we are trying to find out, and where the route stands.
Principle: the page is the interface, not the session. It shows the route as it stands now and nothing else — every route it used to show is in git. It carries no fact of its own: facts live on nodes, and an entry never links back here (§5), which is what lets everything below the Goal be rewritten freely.
What each part must HOLD is the contract's (project_memory-CLAUDE.md,
check_objective_page.py); what follows is the half that is this design's —
which parts the loop may rewrite, and which are the user's alone.
| part | when it changes |
|---|---|
| id | Never |
| status created |
status moves to closed by the user; created never |
| Goal | Only on explicit user instruction, recorded with hashes of the old and new Goal; an independent goal gets a new objective |
| budget wake-me-if attention |
The user sets the first two, freely; the loop sets and clears attention |
| Plan | Freely — appended, rewritten, renumbered, dropped |
| Verdict | Freely, rewritten at milestones |
- [ ] **P<n> — <the question this phase asks>** (<cost>[ · needs: P<i>])
- [entry name](page.md#entry-slug) `<id>` — <the role it plays in answering it> (<date>)
- ▶ <what this settles for the Goal> one sentence
An entry's date is the day it concluded — not the entry's Date field, which is creation
(§5). While it runs it has none, which is how the phase shows where it stands. No figure
and no table: both belong to the entry that produced them.
A phase is ONE question and as many entries as that question needed — the measurement
plus whatever controls and diagnostics it turned out to want, including ones decided only
after seeing the first result. One entry is the common case, not the rule.
needs: makes the Plan a DAG, and every phase whose dependencies are met launches at once,
one agent each; absent, needs: means the phase above.
An agent that finishes a phase does not take the next one from memory. It writes its phase back and re-reads the page: what runs next is whatever the Plan says after that write, and its own result may have invalidated the phase it was about to take. With several agents running, that write is the only thing the others see.
Neither line carries a number or a mechanism. Those are the entry's, one click away, and
repeating them here is how an objective starts owning facts. Both lines are route-relative —
an entry's role, a phase's bearing on the Goal — and both die with the goal, as the phase
does. <cost> is not an exception: spend is the route's own arithmetic over its runs, and no
entry states it.
A route the work backed out of says so in its reading — a phase that closed a direction settled something. If it is no longer part of the route at all, the phase goes and git holds it.
The record bus maintains O8's review count before saving an objective and after entry changes. The launcher does the same when a job changes an entry's status. Count-only updates to clean objectives use separate Objective commits under the record lock. They skip record lint only for this derived text, preserving other Git hooks: O7/O13 may still require an authored phase review, and the count update must not fabricate one. Pending edits are left to their owner; their count refreshes on the next bus save. Failures are reported and the derived edit is restored. O8 still checks saved counts independently.
An O7 review can leave the phase reading unchanged. objective_state.py review-phase records a note and a fingerprint of the phase plus its full linked
conclusions, so a subsequent evidence change requires a new review. O4's
approve-goal command records the user's instruction and the exact Goal change;
it provides an audit trail, not independent proof of permission.
"Have we computed this" is never a question for this page. It is answered on the node page, where read-first (§9) sends you anyway, and where entries are never deleted.
Two things follow, and neither is optional:
- A route change is its own commit. The decision is what a route is made of; landed in the same commit as an entry, it cannot be told apart from the data that prompted it.
- A sample is a snapshot, never the head. The current page pairs today's conclusions with the route walked under yesterday's. Each commit is internally consistent; the head is a reading surface. This holds whether or not conclusions ever settle.
Responsibility: turn accumulated work into something reusable, on purpose.
Two soft channels, split by mechanism rather than subject:
memory/shared/— the agent's own memory, the harness's auto-memory pointed inside the repo so it is versioned with the project. Loaded into context at session start, so it stays one small fact per file: does this change how the next run is set up?memory/claude/andmemory/codex/are symlinks onto it (§12): what one agent learns, the other has.note/— prose the people on the project read, written by user and agent together: what results mean, why a direction was taken or dropped, engineering experience. Content unconstrained, as long as the thinking needs.
wiki/ is the published end, self-contained by contract — a page that needs
the record to make sense is not publishable. It lags on purpose: material arrives
only through a pump, and only once it has settled.
Its shape is DECLARED rather than emergent. wiki/knowledge/SCHEMA.md fixes
which pages exist and what each covers, derived once from the whole record and
edited deliberately afterwards; a source is routed into a declared page and a
source that fits nowhere is a proposal to change that file, which is the user's
to decide. The alternative was tried and withdrawn twice: a structure
re-derived at every compile takes the shape of whatever arrived last, which is
the record's shape and not the subject's. Pages are one of five types —
concept, method, comparison, open, overview — and there is no
finding type, because a result is evidence cited ON a page rather than a page
of its own. What enters must be reusable across sources: a one-off stays in the
record and is cited by tag, while what the one-off taught may enter.
Condensation is always an explicit pump, never implicit.
| pump | from → to | trigger | gate |
|---|---|---|---|
lift |
experiments/ → src/ |
a SECOND consumer appears | what lifts is a designed API, never the literal code. Lands as a feat: plus an exp(<node>): |
wiki-compile |
note/ and closed entries → wiki/ |
the user asks | the interpretation has settled; an entry must be closed; what compiles is its Conclusion; and it must route into a page SCHEMA.md already declares |
memory-lift |
memory/ → the project's .agent/skills/, which both agents' skill directories are views of (§12) |
the user asks | would it apply verbatim in a DIFFERENT project? |
Two of the three wait for the user, because an agent that generalises its own
habits into the framework compounds its own mistakes. lift is mechanical instead: the moment
a second experiment needs the same capability, copying is the mistake.
Responsibility: let a job run wherever the compute is, and keep the record on this side in step with it.
The remote is a compute service. Code goes out, logs and data come back, and the analysis happens here. No agent works there and no record lives there.
What follows is what must be true, not how to make it true. Clusters differ in scheduler, filesystem, module system and tooling vintage, and a rule written against one of them breaks on the next — so the mechanism is left to whoever meets the machine. Where something is worth suggesting rather than requiring, it says so.
A job runs on one of four combinations — this machine or another, with a scheduler or without:
| with a scheduler | without | |
|---|---|---|
| local | hand it to the queue | launch it in the background |
| remote | hand it to the queue there | launch it in the background there |
Which of these a project will use is a question to answer while setting it up, by looking at the machines it is actually going to run on — not one to find out when a launch fails. Whichever apply return the same three things: an id that names the job, a log that describes its own run, and something that can be asked later whether it is still going. What is shared is the RECORDING contract, not the call — three scripts serve the four venues with three argument shapes, and remote-with-a- scheduler is reachable through two of them. That is a cost, and this section states it rather than hiding it: a fix to queue placement or to log derivation in one does not reach the other, which has happened.
| what | must match | why it is not covered by the others |
|---|---|---|
| the project's code | the local commit | the run has to be the code you have here |
| the environment spec | the file in the repo | the code alone does not say what it needs |
| a library you are developing | its commit | a lockfile pins a release; an edited library has only a commit |
What each machine installed from the spec is never compared against another machine — one spec resolves to different bytes on different hardware, so equality would fail on every run and mean nothing. Each machine is held to the spec, and whatever still differs is recorded rather than aligned. A second spec is allowed where a dependency exists on one platform only, provided it contains the first: every package of the base at the same version, extras permitted.
Suggested: bring the far side up before checking it, so a host that is merely behind is repaired rather than reported; and let a check that cannot establish its premise say so, rather than report a failure it did not observe.
A job that exists and is on no entry is lost work, so the id lands on its entry the same turn it is submitted. From then on the entry and the job stay in step — that alignment is the whole requirement, and everything below is what it takes to hold it.
Watching is a timer, not a single wait, and finishing is only one of the things it asks:
- is it still running?
- did it die — out of memory, node failure, walltime — and should it be resubmitted?
- is it producing anything, or has it stalled with the process still alive?
- does the entry's status still describe what is actually happening?
The session that launched it owns this, since nothing on the far side can write the record. It keeps at most one watcher for all objectives whose background jobs it currently owns, repeating the objective scope in that watcher; a foreground-only run needs none. The watcher changes only when that session's objective set changes, never once per job. Behind it, the harvest hook reports any J3 left unharvested for 15+ minutes into every session's next turn, unarmed.
When a job ends its logs and data are pulled back without anyone asking for them. The state a run resumes from — checkpoints, large intermediates — stays where it was made (§4); no transfer can tell that apart from a result by looking, so the split is made when the file is named.
What we want: no number of interruptions costs the campaign its place. A person asks a question, changes their mind, edits a file by hand, walks away for a day. None of that should end the run. The agent's place is on a page (§1, §6), so after any interruption the move is the same one: re-read the page, and go on from where it says the work stands.
-
The next move comes from the objective page, never from memory (§6). Between actions the agent re-reads the Plan, and the next phase is decided from the concluded readings — the Plan is appended, rewritten or dropped to match before anything runs.
-
Read-first, at every entrance. Which entrances, which sources, and what each asks are the root contract's, stated there once: a second copy lived here and had already fallen a source behind the original. What the principle buys: a recorded answer finishes the request or shrinks it to the part that is new, and what remains is an entry on an open page, a new page (§5), or a new node (§4) — and when §4 does not settle node-or-not, ask. No linter can run this check: a duplicate probe reaches a page only after its hours are spent.
-
Jobs are watched until settled (§8), and the record follows the same turn. A job that ends lands on its entry — status, facts, conclusion — and on the phase that owns it, and the entry's follow-ups run without being asked. The skills that open and write records are the agent's to invoke unprompted; the asks of §3, §5 and §7 are the exceptions.
-
wake-me-iffires → the loop stops, setsattention:and waits (§6). -
A correction is applied at once (§5): move the wrong description, walk the cascade the same turn, then continue under the rewritten page. A question does not interrupt: it is answered from the record and the open phase resumes — the answer is not the turn's deliverable.
-
A handover's first move is read-first. Whatever the predecessor knew and did not write down is gone.
-
A file the person changed is re-read before the agent edits it, and a change the agent did not make is asked about before it is built on or reverted.
Be strict about the finished record and permissive about the route to it. The framework checks claims that can be read from an artifact: required fields, resolvable links, ownership of running work and internal consistency. It leaves scientific judgment to the agent and reviewer. A gate exists where an otherwise silent failure would make the record unreliable.
Four editable contracts cover the project root, experiments, project memory and
wiki. Each contains a managed framework section followed by
## Project-specific rules. Claude reads the contract through CLAUDE.md and
Codex through AGENTS.md; both names point to the same file. Updates replace the
framework section and preserve project rules. An inseparable local edit stops the
update as a conflict instead of being overwritten.
The contracts state stable policy. Skills carry procedures for a particular moment: planning, objective work, background submission, analysis, correction, code lift, wiki compilation, memory lift, reorganization, setup and update.
Lifecycle hooks route new computational work into the record, attach submitted jobs to their entry, lint a page after it is written and make newly created worktrees sparse. Session start checks the current worktree and starts the optional fixer only when the project enables it. The harvest reminder reports a running entry only after all of its jobs have been terminal long enough to have been missed.
Read-only commands take the cheap path. A reminder fires when its obligation is created; normal operation does not depend on a periodic prompt.
The node/page linter protects the evidence record. Its core checks are:
- stable IDs and valid status transitions;
- required code, motivation, recipe, facts and conclusion fields;
- evidence links, figure links and referenced commits that remain reachable;
- one computation script per entry and one owner for recorded jobs;
- rosters, validation references and project language when enabled.
A numerical table is required when measurements exist. A failed start or another run with no measurement may instead record the reason and its evidence. The conclusion must answer the question and identify its basis and boundary, without requiring a fixed English sentence. Dates do not determine entry numbering; IDs only need to remain unique and stable.
The objective linter protects the control loop. It checks the Goal, Plan and Verdict shape; phase and entry links; the plan DAG; globally unique objective IDs; explicit authorization for a changed goal; and reviews of conclusions that changed beneath an existing judgment. The awaiting-review count is maintained by tooling and checked for consistency. Long verdicts receive a non-blocking reminder.
These checks do not decide whether a conclusion is true, whether a probe was the best probe, or whether two results agree scientifically. Those judgments belong in the record. Exact diagnostic codes and command options live beside the linters and their tests, which are the executable specification.
The record bus lints a page under a shared lock before it lands. Git hooks keep a
record commit on the default branch, enforce the project's declared commit
layers and keep src/INDEX.md aligned with reusable code. Existing project hooks
still run through a dispatcher; uninstall restores the prior hook path.
Recorded launchers require an entry and either an objective phase or an explicit
NO_OBJECTIVE=1. Formal runs launch from main, after code has landed. Setup does
not report success until the core wiring and both record views pass. Every gate
has a named emergency escape so a false positive can be handled explicitly
rather than encouraging users to remove the framework.
worktree_retire.sh is an explicit cleanup command. It refuses removal while a
process still has its current directory inside the worktree. Plain
git worktree remove is not wrapped, and worktrees are not deleted merely when
an agent session ends.
The fixer is a background scanner that can surface missed work and reconcile job state. It is disabled by default and runs only when the project enables it. The HTML dashboard and editor extension are also optional. Linters, contracts, Git hooks and the record itself remain the default core.
The record is serial; code can be parallel. project_memory/ and wiki/
live once in the main checkout. Record writes go through the shared bus and
lock. A framework-created worktree omits those directories, so an agent cannot
accidentally edit a stale copy.
One agent may edit a clean main checkout directly. Separate worktrees are used when agents may edit code concurrently, main contains unrelated work, or a change needs an isolated branch. Three agents can therefore work in three worktrees while another session owns the record on main. Worktree branches land by fast-forward after rebasing when needed, and a formal run is repeated from the merged main state.
A session can complete one objective and then open another, or own several active objectives when the work calls for it. Its monitor follows the session's set of objective IDs and their running entries; the framework does not create one daemon per objective.
bootstrap_project.py performs one idempotent installation. It creates the
record layout, contracts and agent views, project configuration, Git-hook
dispatcher, indexes and ignore rules, then runs a read-only acceptance check.
Existing source layouts and hooks are preserved. Dashboard, fixer, HPC and
editor support are persisted opt-ins; Claude and Codex project bindings are
core.
The setup skill handles decisions that a script cannot infer: directory roles,
the project's purpose, environment choices and optional integrations. Project
purpose belongs in project_memory/note/ or an objective Goal, while durable
project-specific agent rules belong in the contracts' project section.
update_projects.sh updates the current project. It stages a shared release,
checks contract conflicts, switches both host bindings and Git hooks as one
transaction, verifies native host installation, then removes legacy project runtime copies.
A failure restores the previous managed files and pin. Framework-owned files may
be refreshed, project-owned files are untouched, and mixed files preserve their
project section. Setup reuses an existing pin; update is the operation that
changes it.
doctor.sh checks core wiring for every project and checks an optional
integration only when enabled or explicitly requested. It diagnoses; normal
setup does not run every diagnostic twice. Dry run remains available when a user
asks to preview setup or update.
The compatibility suite exercises empty and existing repositories, custom layouts, inherited hooks, old-project migration, missing host CLIs, optional services, rollback, worktrees and repeat runs. CI runs fast parser and policy checks at the supported Python and shell endpoints, with real bootstrap, update and rollback paths in one integration job.
The record format is v1. Updating a pre-release project renames its old v3
marker to v1 without changing records. Older schemas still need migration.
The record, linters and Git gates are host-neutral. The plugin package has one core and two manifests because Claude and Codex keep separate registries and caches. A test holds their versions equal.
Claude reads CLAUDE.md; Codex reads AGENTS.md. Each pair points to the same
contract, and their memory views point to the same shared memory. This avoids two
copies of project policy or learned context drifting apart.
.research-note/plugin-lock.json records the selected version, content digest
and source commit, branch and plugin dirty status when available. Immutable plugin content lives once per machine
at ~/.local/share/research-note/releases/<digest>/, respecting XDG data paths.
Both project configurations select a content-specific marketplace; identical
content shares the same source and host cache across projects. No full plugin
copy lives in a new project's .research-note/.
Claude's native registry records project scope and its cache path. For local marketplaces it may read directly from the immutable shared source. Codex's project configuration selects the marketplace and plugin; native discovery loads its own cache only in trusted projects. Setup/update verify available hosts and report missing or untrusted hosts as pending. User-level enablement is unchanged. The lock and configuration select a release, never the highest cache version.
An explicit update changes the current project's lock and switches Claude, Codex, hooks and scripts together. Other projects and user-level installations stay unchanged. Claude can reload plugins in place; Codex uses the new pin in a new session. Existing sessions may keep the code they loaded at start.
A new clone must restore the exact locked release from matching plugin source;
run python3 /path/to/plugin/scripts/project_runtime.py restore --source /path/to/matching/plugin PROJECT_DIR. It does not silently select a newer
release. A moved checkout with its snapshot present only needs restore PROJECT_DIR to refresh its local paths. --latest fetches main,
--version VERSION fetches a release tag, and --source installs local working
files without GitHub. Installed files do not follow subsequent checkout edits.
Versions identify releases; content digests distinguish local builds with the
same version. A development cycle declares a prerelease version once (for
example 0.1.1-dev.1); each local update includes current plugin edits without
rewriting that declaration. The maintainer merges dev and sets the formal
version before publishing main and its tag. Both sources use the same installer.
Shared releases and native caches are retained independently of
project updates because other projects may still select them.