A small, evidence-first harness for AI-assisted development on Claude Code and Codex.
Overview
js-harness sets up Claude Code and Codex for AI-assisted development in a project, on top of a stack it does not rebuild: gentle-ai owns the workflow, task documents and review, engram owns memory, codegraph owns code structure. js-harness adds the thin layer those leave open: a short AGENTS.md, a pre-commit gate that runs the project's checks, task contracts whose end is a set of commands that exit 0, and a record of everything it writes, so that it can be removed exactly. It is plain Node with no dependencies. It needs Node 22 or newer and git. The repository is private: the install needs a GitHub account with access to it. 1. npm i -g puts the js-harness command on the path. 2. js-harness install writes its skills and hooks for Claude Code and Codex under your home, once per machine. 3. js-harness init, in a git repository, writes harness.json, AGENTS.md, an empty failures register (docs/FAILURES.md) and the pre-commit gate of that project. 4.
README
js-harness
js-harness sets up Claude Code and Codex for AI-assisted development in a project, on top of a
stack it does not rebuild: gentle-ai owns the workflow, task documents and review, engram owns
memory, codegraph owns code structure. js-harness adds the thin layer those leave open: a short
AGENTS.md, a pre-commit gate that runs the project’s checks, task contracts whose end is a set
of commands that exit 0, and a record of everything it writes, so that it can be removed exactly.
It is plain Node with no dependencies. It needs Node 22 or newer and git.
Install
The repository is private: the install needs a GitHub account with access to it.
npm i -g github:jsaristi/js-harness
js-harness install
cd my-project
js-harness init
npm i -gputs thejs-harnesscommand on the path.js-harness installwrites its skills and hooks for Claude Code and Codex under your home, once per machine.js-harness init, in a git repository, writesharness.json,AGENTS.md, an empty failures register (docs/FAILURES.md) and the pre-commit gate of that project.js-harness doctortells you which tools of the stack are missing and the command that sets up each one.
Remove
cd my-project
js-harness uninstall
js-harness uninstall --global
npm rm -g js-harness
uninstall removes from the project what init wrote, and uninstall --global removes from
the home what install wrote. Each one reads the manifest the install recorded, so it removes
only those files, and all of them. Measured on Windows with an empty home and an empty project:
- the project is left as it was:
git statusshows nothing; - the home keeps the empty folders
.claude/and.codex/, and an emptyAppData/Roaming/; - the home keeps no
.codegraph/:initrunscodegraph initwithDO_NOT_TRACK=1, so codegraph writes no telemetry there (measured on codegraph 1.6.0).
Commands
Every example below is the output of a real run. Paths are shortened: ~ is the home and
my-project is the project’s folder. A line … marks output left out; output that grows with
each feature, such as install’s hook lines or check’s rows, is shown in part and cut with it.
js-harness check
Runs the project’s gate: the budget of AGENTS.md, the task contracts, the declared sensors
(tests, lint, typecheck) and whether the pending change is due a review. The pre-commit hook
runs it, so a red check means the commit is not ready yet. With JS_HARNESS_TEST_CONCURRENCY=
set, a tests sensor that is node --test (directly or as the test script) runs n test files
at a time, for a machine where node’s default, one fewer than its processors, is too few: this
repository’s CircleCI run of the merge queue’s candidates sets 2.
When a node --test suite fails, the test files it names as failed run once more, alone. A
file that passes on that rerun does not block while it has a current entry in docs/FLAKY.md,
the quarantine ledger: one ### entry with - Owner:, - Expires: YYYY-MM-DD
and - Failure: , written by hand and committed. A file that passes
on rerun with no entry still blocks, and the rerun row names the entry to add; one that
fails again blocks as before, and so does a failure in a file that cannot be rerun (no test
file, or a path with a character a shell reads): then nothing is rerun. A pass that rests on a
quarantine proves no tree for "sameTree": "skip", so the next gate runs the tests again. The
flaky row fails a malformed ledger, an empty field included, and lists the expired entries,
which no longer count.
In a project that holds .github/workflows/gate.yml, as this repository does, every commit’s gate
runs its tests on GitHub’s machines first, whether or not the machine’s gate lock is free: it pushes
the tree being committed to a gate/ branch of the remote (--remote, origin when not
given), reads that workflow’s run in the GitHub repository the remote names with gh, deletes the
branch, and lets the commit through only on green. The tests row then names the run. A gate that
gets no answer from GitHub (a remote not on GitHub, no push, gh not signed in, no run within 3
minutes, a run cancelled or timed out, or one not done within rule:ci-gate-bound of the push,
which is then cancelled) runs here under the lock, after one check: line on stderr that says why. The gate log keeps both: a run’s red tests by name, read from its log with gh’s prefix taken off, and a ci-fallback line with why and what the wait cost, under the id of the gate that then runs here. JS_HARNESS_GATE_CI=0 keeps every gate here. The runner is Linux, so a test that runs only
on Windows is left to the merge queue’s run on this machine.
The fixed load row says how many tokens a Claude session of the project sends at its first turn,
before any work: the median of the latest five sessions, read from their transcripts under
~/.claude/projects (and, from a worktree, its main checkout’s). It never fails a commit, and warns
over 100,000 tokens.
$ js-harness check
PASS budget 639 estimated tokens (budget 1500)
PASS contracts no contracts under odd/tasks
INFO payload claude 639 estimated tokens per turn from files (tools and hook output not counted)
global CLAUDE.md not found ~\.claude\CLAUDE.md
project AGENTS.md 639 my-project\AGENTS.md
…
INFO fixed load 95570 tokens at the first turn, median of the latest 5 sessions (budget 100000)
…
INFO sensors no sensors declared: add test, lint or typecheck scripts to package.json, or a "sensors" object to harness.json
js-harness check --subagents adds one INFO row, subagents, for today’s sub-agents of this project, read from the Claude Code records: each writer launch is compared with subagent-writer-model, each explorer, reviewer or verifier with subagent-reader-model, and every launch’s effort with subagent-effort. It says how many ran off those rules and fails nothing. It is opt-in: the commit gate runs plain check, which reads no record, and a day of records is about 231 MB.
js-harness gate
Runs a heavy command under the machine’s gate lock, one at a time with the gates of every
project, and records the run in the machine’s gate log. gate log prints the last runs. A
command is stopped after 30 minutes (--max ), and while the branch of the folder it runs
in has an open pull request and commits a push would take now, any command but that push or a merge
is refused, with the push command printed. --ready declares a quick check, such as
git diff --quiet, run before the wait and again as the turn comes after one: when it fails, nothing
runs (exit 75), so a command that would refuse at once does not wait for a turn first.
$ js-harness gate --no-wait -- node -e "console.log('a heavy run, one at a time')"
gate: 8.0 of 8 logical processors busy before the command (100%, 1 s sample)
…
a heavy run, one at a time
$ js-harness gate log --last 2
10-14 04:24:38 0 s exit 0 load 8.0 of 8 (vmmem, MsMpEng, claude) my-project
A ticket puts one session’s next whole check before every gate that waits: the orchestrator issues it
with gate first --session --reason , or its own batch run carries one with check --first . Two are open at most, for an hour at most; js-harness sessions prints them with their
issuer and flags one the orchestrator did not issue, and the gate log keeps each issue and claim.
js-harness init
Enables js-harness in a git repository and records what it wrote in a manifest of the clone.
$ js-harness init
wrote harness.json
wrote AGENTS.md
wrote docs/FAILURES.md
wrote .git/hooks/pre-commit
ran codegraph init
manifest: my-project\.git\js-harness\manifest.json
js-harness install
Writes the skills and the session hooks for Claude Code and Codex under the home, and records a manifest of them.
$ js-harness install
installed js-harness 0.1.0 (6 files) into ~
registered hook entry in ~\.claude\settings.json: js-harness doctor --hook (SessionStart, matcher startup|resume)
registered hook entry in ~\.codex\hooks.json: js-harness doctor --hook (SessionStart, matcher startup|resume)
…
js-harness loop
Runs a session’s long background loops (the orchestrator’s heartbeat, its channel watch, a merge
keeper) under an owner record, so two sessions never stop each other’s loops. run keeps the
command in the foreground of a background task, so its end still wakes the session, and records the
owner session, its own process and the command’s by pid and creation time, the purpose and what
ends it. stop refuses a live loop another session owns unless --take-over, and exits 0 only when
a second look at the process table finds none of its processes left. heartbeat and
channel-watch are bodies for run.
heartbeat ends on a quiet machine (no gate-log or channel write for --quiet minutes) and on a
stalled work session: one the orchestrator handed work to, by a notice to its number or its
opening, that has not given it back with a waiting, closed or relieved notice and has made no
commit of its own, sent no notice and run no gate for --stall minutes (the rule session-stall,
an hour). It prints a STALLED: line naming the session, since when it is quiet and what it holds,
and says it again each time that quiet doubles. js-harness sessions lists the same sessions in
its Stalled section, and its --watch says each one as it crosses.
$ js-harness loop heartbeat
js-harness loop heartbeat: ends when 30 minutes pass with no write to the gate log (…) or the channel (…), or a work session holds handed work with no commit, notice or gate for 60 minutes, or after 10 hours
STALLED: JH 19 · skill del orquestador (T8) — no commit, notice or gate since 11:49, holding a note from JH ORQ · js-harness (350k) of 11:49: 17:00 JH ORQ: first-version T3 moved to JH 25 after 6h without an answer…
$ js-harness loop run heartbeat --purpose "wake the lead on a stall" --until "30 min with no gate or channel write" -- js-harness loop heartbeat
js-harness loop: heartbeat runs (pid 60332, command pid 65396), owned by 053e19d7: wake the lead on a stall; until 30 min with no gate or channel write. Stop it with js-harness loop stop heartbeat
$ js-harness loop status
heartbeat alive 053e19d7 pid 60332 since 05:17 wake the lead on a stall — until 30 min with no gate or channel write
$ js-harness loop stop heartbeat # from another session
js-harness loop: heartbeat is owned by 053e19d7, not by 220663e4 (pid 60332, since 05:17: wake the lead on a stall); nothing was stopped. Give --take-over only when that session is told and the loop is to be yours
$ js-harness loop stop heartbeat # from its owner
js-harness loop: stopped heartbeat: 2 processes, none left (looked at 05:18)
js-harness sync
Inside a project, moves it to the version its harness.json pins. With --global, or outside a
git repository, writes the machine install again.
$ js-harness sync
up to date at 0.1.0
js-harness uninstall
Inside a project, removes what init wrote; with --global, removes what install wrote.
$ js-harness uninstall
removed harness.json
removed AGENTS.md
removed docs/FAILURES.md
removed .git/hooks/pre-commit
removed docs
removed .codegraph/
removed manifest my-project\.git\js-harness\manifest.json
$ js-harness uninstall --global
removed ~\.claude\skills\js-harness\SKILL.md
removed ~\.codex\skills\js-harness\SKILL.md
…
removed hook entry from ~\.claude\settings.json
js-harness status
Inside a project, shows its pinned version, the running version and whether this clone is set
up. With --global, shows the machine’s installed version, its manifest and the latest release.
$ js-harness status
project: my-project
pinned version: 0.1.0
running version: 0.1.0
this clone: set up (manifest: my-project\.git\js-harness\manifest.json)
pin and running version: match
js-harness update
Says whether a newer release exists: the newest vX.Y.Z tag of the private repository, read with
git ls-remote and the git credentials already on the machine (--repo names
another repository). No release is tagged before 1.0.0, so for now it says that and how to get
the newest copy. When the tags cannot be listed it says why and exits 1.
$ js-harness update
no release of js-harness is tagged yet (versions are tagged from 1.0.0); to get the newest copy: npm i -g github:jsaristi/js-harness, or the same with the package manager that installed it (installed: 0.1.0)
js-harness upgrade
Installs the newer release with the package manager, then syncs the home with the copy it
installed: the repository at its newest tag (github:jsaristi/js-harness#vX.Y.Z), or the newest
tarball of --source , a folder of npm pack tarballs, which wins when named and needs no
credential. With no release tagged it installs nothing and exits 1; no package registry is ever
asked.
$ js-harness upgrade
nothing was installed: no release of js-harness is tagged yet (versions are tagged from 1.0.0); to get the newest copy: npm i -g github:jsaristi/js-harness, or the same with the package manager that installed it
js-harness doctor
Reports each tool of the stack: its version, whether Claude Code and Codex are configured for
it, and the command that fixes what is missing. --offline skips the check for newer versions.
Its host-update row watches the Claude Desktop app and the Claude Code CLI bundled for its Code
tab, read from the app’s folder under the home (--home), never from the environment. The first
reading is recorded as reviewed in the doctor cache. After an update, the row stays stale until
the review is done, listing the old and new versions and the review steps: the two changelogs,
the harness hooks, Remote Control, and a report to the user. A restart of the app turns Remote
Control off, and the step is to turn it back on in each work session without asking, his
standing yes of 2026-10-15; a session that has run no turn since the restart refuses it, so it
is sent a message first. js-harness doctor --host-reviewed
records the review; with --json it prints the record it wrote. The row also shows
outdated when the app downloaded an update it has not applied: on Windows that waits until
Claude is quit fully, the tray icon included.
Its gentle-ai-sync row does the same for gentle-ai. A gentle-ai sync rewrites the global
~/.claude/CLAUDE.md, which gentle-ai owns: js-harness never writes that file, and only reads it
to measure the per-turn payload (bin/lib/payload.mjs). So a new gentle-ai version stays stale
until it is reviewed. One review step diffs the global CLAUDE.md against the copy gentle-ai saved
before the first sync since the last review (~/.gentle-ai/backups//snapshot.tar.gz), and
the next checks each change against the project’s AGENTS.md for conflicts.
js-harness doctor --gentle-ai-reviewed records the review, and --json prints the record.
Its stack-update row does the same for the other tools of the stack and for the claude on PATH:
the first reading is recorded as reviewed, and a tool that updated itself since (engram, codex, the
Claude CLI’s native updater) stays stale, named by the session-start hook, until
js-harness doctor --stack-reviewed records its review. A tool installed since joins the record as
it is; one removed is no update.
Its js-harness row is the harness’s own rules: the version column holds the first 8 characters
of the hash of the installed copy’s rules with no scope, and up to date compares it with the same
hash of the default branch’s list. A rule scoped to the harness’s own repository never calls a copy
behind. In the harness’s own checkout that list is read from the main ref with git; anywhere else
an interactive doctor reads it from GitHub on every run, never from the cache, and --offline,
the session-start hook and the TUI’s stack block read nothing there (n/a), as does a project that
pins another version than the copy’s. A copy behind is outdated, and its fix names the newest copy and the ids of the
rules that differ; a list that cannot be read is unknown. --json adds both hashes and the ids.
$ js-harness doctor --offline
tool version claude codex project up to date
gentle-ai 4.1.1 missing missing n/a unknown
engram 3.4.0 missing missing n/a unknown
codegraph 1.6.0 missing missing ok unknown
…
Fix:
gentle-ai (claude): gentle-ai install
codegraph (claude): claude mcp add -s user codegraph -- codegraph serve --mcp
…
js-harness progress
Shows the roadmap’s phases and the features, counted from the checkboxes of docs/ROADMAP.md
and odd/tasks/*.md. A feature that keeps its tasks in a table (a header with an ID and a
Status cell) is counted by its rows: a row whose ID is a task (T2, T3c) is done when its
status starts with done.
$ js-harness progress
Roadmap docs/ROADMAP.md 61/62 98%
[x] F0 Safeguard and baseline 3/3 100%
[x] F1 Skeleton 3/3 100%
…
[ ] F2 Context diet 3/4 75%
- Block exclusion via gentle-ai #4036, if it ships
…
Features 295/349 85%
--pace [--days ] reads the first-parent history of the default branch (origin/HEAD, else
main, else HEAD) and prints the tasks closed, reopened, added and removed on each of the last
14 days, or n, by committer day, with the totals and the mean a day; --json gives the same as
data. A task is matched by its ID (T3); one reworded without an ID, or in a moved file, counts
as removed and added. Without --pace, progress runs no git.
$ js-harness progress --pace --days 2
Pace of origin/main, its first-parent line, by committer day over the last 2 days:
2026-10-15 closed 81 reopened 0 added 67 removed 1
2026-10-16 closed 30 reopened 0 added 22 removed 1 (today, so far)
Total closed 111 reopened 0 added 89 removed 2
A day closed 55.5 reopened 0 added 44.5 removed 1
js-harness throughput
The throughput review’s measure: a fixed table of a window of UTC days, by default yesterday,
where every row names the record it was read from and a fact no record holds yet says which
recorder it waits for, never a zero; a record that could not be read, a git process that failed
included, is named on its row with why. The tasks are each commit of main compared with the one
before, as progress compares them. It is read-only and fetches nothing: main is origin/main
as last fetched (git merge-tree, which names the files of a conflicted merge, writes objects
into the object store). The gates are every project’s on the machine, as the gate lock is.
$ js-harness throughput --since 2026-10-16 --until 2026-10-17
Throughput 2026-10-16 to 2026-10-17 (UTC, until excluded; local UTC-05:00: 2026-10-15 19:00 to 2026-10-16 19:00) D:\js-harness
gates: every project of this machine, as the gate lock is; the other rows: this project
2026-10-16 snapshot
pull requests merged 94 (3.92 an hour); merged twice: #292, #305 [git log of origin/main as last fetched]
ready to merged not recorded (B1, JH 26) [a ready notice that names its pull request]
lock busy 16.8 h of 24 h; by kind: check 125 / 446 min, land 65 / 230 min, copy 49 / 150 min, run 39 / 111 min, commit 25 / 70 min [the gate log (~/.js-harness/gate-log)]
commit gates here: median 200 s (150), lock free median 217 s (52); GitHub: median 144 s (75) [the gate log (~/.js-harness/gate-log)]
lands 65, median 216 s, red 8; 65 gates for 49 lands [the gate log (~/.js-harness/gate-log)]
…
The gate log rotates in about two days, so throughput snapshot [--day ] keeps a day’s
gates under the home (.js-harness/throughput//.json) once the day has settled,
two gate-max after its midnight, and the table reads a kept day from there. Main’s history,
the Codex runs and the channel are append-only and always read live. A kept day is written
again only by a fuller one, the replacement recorded; --gate-log (twice at most) reads
archived copies of the log instead, for a day the live log has lost; a copy made before the day
settled may lack its last gates, so the day is kept as partial until then. A copy is made no
earlier than its modification time; one that kept its source’s time (cp -p) says when it was
made with --copied-at . throughput status lists
the kept days and exits 1 naming each day since the first one kept that has none. Uninstall
leaves the snapshots in place.
$ js-harness throughput status
2026-10-15 kept
2026-10-16 kept
--against .. compares the window with an equal window before a change, row by
row, daily value with daily value. The gate-wait median moved from 216 s to 69 s from one day to
the next with nothing shipped, so under 7 days a side it prints both and how often the median of
a window that did not change falls outside the before range, and judges nothing; from 7 days a
row moved only when the after window’s median lies outside the before range, which an unchanged
week does 7% of the time. The window before must end by the one compared.
$ js-harness throughput --since 2026-10-16 --until 2026-10-17 --against 2026-10-15..2026-10-16
Throughput compared before 2026-10-15 to 2026-10-16, after 2026-10-16 to 2026-10-17 (UTC days, until excluded) D:\js-harness
pull requests merged before 80 after 94 not judged: 1 days a side, under 7; with nothing changed, the after median falls outside the before range 100% of the time
lock busy, h before 14.3 after 16.8 not judged: 1 days a side, under 7; with nothing changed, the after median falls outside the before range 100% of the time
…
js-harness contract
Task contracts: template prints the skeleton a feature document opens with, check
validates every contract, run runs a contract’s done-when commands, and goal prints the
/goal condition that runs them. Each task names its kind of work, (kind: design | build | verify | docs); modes prints the effort and sub-agent models each open task calls for, from
the project’s rules, and goal prints the same lines on stderr.
$ js-harness contract template
## Goal
## Why
## Done when
- `` exits 0 # executable, never prose
…
$ js-harness contract check
PASS odd/tasks/codex-parity.md
PASS odd/tasks/codex-review-cost.md
…
js-harness failures
Reads the register of what went wrong, docs/FAILURES.md: the totals, the count by class and
one line per failure, the most seen first. failures prints one entry; failures check
exits 1 when the register is malformed. It only reads: the register is written by hand.
$ js-harness failures
docs/FAILURES.md 69 failures, 178 sightings, 39 seen more than once, 24 not settled, 0 controls replaced
By class
wrong-information 8 failures 30 sightings
…
By failure, the most seen first
10 stray-folder-untracked machine repeated first seen 2026-10-07
8 gate-at-its-bound machine repeated first seen 2026-10-10
…
js-harness rules
Shows the declared rules of the harness, each with its value or the file that holds it.
rules prints one rule with its source and the files that read it; rules check exits 1
when a declaration, its source, a reader or docs/RULES.md is wrong; rules render writes
docs/RULES.md.
$ js-harness rules codex-review-floor
{
"id": "codex-review-floor",
"subject": "the most input tokens a Codex review may cost however small its commit",
"owner": "sebastian",
"scope": "repository",
"since": "2026-10-11",
"value": 300000,
"unit": "input tokens a review"
}
Read by: bin/lib/codex-review-allowance.mjs
…
$ js-harness rules check
PASS rules 18 rules, hash 7963ae6d5597
rules --hook is the turn-start hook of the rules (letter F of odd/tasks/rule-hierarchy.md). install
registers it for Claude Code three times: on UserPromptSubmit, on SessionStart for the
matcher compact|clear|resume, and on SubagentStart. Where the project declares rules
(bin/lib/rules-data.mjs in the harness’s checkout, docs/rules.json in a project), it
compares the list at the session’s merge base with the list on the default branch
(origin/HEAD, else origin/main, else main, never fetched). It tells the session, once,
each rule that was added, removed, retired or changed, with the new declaration itself. After a
compaction, a clear or a resume it tells it again, and a sub-agent is told at its start. Anywhere
else it prints nothing. A turn with no change prints nothing and costs no token. It never blocks:
a fault goes to standard error and it exits 0. What a session was last told is kept in
~/.js-harness/channel/rules-seen/, one file a session, pruned after 30 days. uninstall removes
that folder with the channel’s; a session still running afterwards may write it again, and the
next uninstall removes it. Codex sessions get nothing until a logging hook confirms its events.
js-harness codex-review
Runs the Codex review of ..HEAD and records it. --plan prints what the review would
read and cost, with how close that estimate came on the last reviews this machine recorded, and
starts nothing; a run records in each part’s run line the figures it was planned at, which that
line reads; --unavailable records a skip.
A merge of the default branch is reviewed by what was written in it, not by what it brought in. At HEAD that is one review. Below HEAD, after code commits that each have a record, the range is cut in two turns where two cost less than one review (a part counted as 100 changed lines): the merge, in a worktree at it that the run removes, then the commits after it. Each turn writes its record; a merge whose review owes a correction stops the run before the commits after it.
$ js-harness codex-review --base b36ab7b^1 --plan --again
Plan of the Codex review of b42a30b..c670cff: gpt-6.1-sol at medium effort, 2 parts, 727 changed lines in 7 files.
part 1 of 2: 4 files, 356 changed lines, about 230,000 input tokens
part 2 of 2: 3 files, 371 changed lines, about 240,000 input tokens
Left out of the review: 5 Markdown documents, 818 changed lines.
…
Allowance of this plan: 872,400 input tokens, the larger of 300,000 a review and 1,200 a changed line, for the 727 changed lines it reads. Its estimate, about 470,000, is inside it. …
js-harness merge-queue
Builds the candidate main would be: origin/main with each ready pull request merged into it in
order of number, and names each one it leaves out and why. Ready means the pull request is open,
not a draft, mergeable, on main and with a gentle-ai line in its body, has a Codex review record for
every code commit, and merges into the candidate so far with no conflict. --dry-run prints it and pushes nothing. --land tests it, the whole suite on
this machine under the gate lock and, at the same time, on CircleCI on a queue/* branch pushed
before the suite starts, and when both are green moves main to it by fast-forward, leased on the main it was built from (odd/tasks/merge-queue.md).
A candidate whose every changed path is a document main’s harness.json declares (documents) runs
no suite on this machine, as a commit of documents alone runs none, and takes no turn of the gate
lock: CircleCI’s run of the whole gate is its test.
No GitHub workflow runs for a pull request: the one workflow, the gate of a commit (js-harness check),
runs for a push to a gate/ branch alone.
A candidate red on this machine is bisected: the pull request that turns it red is left out and
named, and the rest is landed. The land asks js-harness gate first for a ticket of the running
session, so its gate goes first in the line; with no session id or a refused ticket it waits its turn
and says so. A gate that ran nothing (no turn in time, or a waiting push took it) or was stopped
before it ended is no red: the land exits 5, with main not pushed and nothing bisected
(odd/tasks/land-unattended.md). The queue/* branch is removed as soon as its answer no longer
matters: when the candidate is red or untested on this machine, or once CircleCI answered. Starting
CircleCI with the suite saves the minutes the two took in series; its cost is one CircleCI run spent
on each candidate this machine turns red or does not test, since removing the branch does not
cancel its build. A land killed before it ends leaves its queue/* branch on origin, to be removed
by hand. A --land run from a bin/merge-queue.mjs that is not origin/main’s, after its fetch
(line ends aside), says so and stops before it builds anything, exit 1: it would land with code
main no longer has. The pre-push hook refuses every other push of main from a clone; a
merge made on GitHub’s side does not pass through it.
Each --land appends one JSON line to lands.jsonl under the git directory’s js-harness folder:
the pull requests it looked at, each one left out with its reason (not-ready, review, door,
conflict, merge or red, with what the suite said), the start and seconds of each suite run,
of CircleCI (from the push of the queue/* branch, so it spans the local suite) and of the push,
the CircleCI build, what landed and the exit. A --dry-run writes none,
and a record that cannot be written is said and changes no exit.
A land also tells the work session of each pull request it left out why, with the failing tests of a red
as the gate log names them, by a note to that session’s number in the channel, which ends its
js-harness wait. The session is the one whose newest ready notice names the pull request
(js-harness notify --kind ready --pr ); one that sent none is not told, and the lead relays it. The
same reason is told once for one ready notice. Each left-out entry of the record keeps the failing
tests of a red (failing) and the number of the session told (told, or null).
A code commit made after the cut-over needs gentle-ai’s evidence written on its own branch, beside
the line in the body: its hash (seven characters or more), or a range a..b that holds it (a and
what git rev-list a..b lists), on a line the pull request adds to a feature document
(odd/tasks/.md) that says gentle-ai, such as gentle-ai: 6b58f85..3d0cc94 high, approved.
The queue resolves each range itself; a merge of main needs no line, and a pull request of documents
asks for none. A rebase changes the hashes, and the refusal says so. For each ready pull request
the queue prints, commit by commit, the Codex record that covers it and the line that names it.
A landed pull request from a rule/ branch closes what js-harness rules change opened: for each
rule it changed in bin/lib/rules-data.mjs, the newest relief line “rule: proposed on
, not landed” that the landed head holds (the line ends with a hash of the declaration it
wrote, which must be the head’s) and that no later landing confirms gets one line of the queue’s, "rule:
landed on main from : confirms relief line ". A branch with no such line is named;
--dry-run and a land that does not move main write none. A rule’s row is read three times: the
commit refuses a restated value, js-harness check --push reports it as INFO for the lines the push
brings past the remote’s default branch, and the merge door refuses it.
On a machine whose gate runs its sensors in WSL2 (the switch ~/.js-harness/gate-wsl.json and
"sensorsInWsl": true in harness.json), the land’s run on this machine is a run in WSL2 too, so
the code paths of Windows (process trees, PowerShell, cmd.exe quoting) are tested by no commit’s
gate. After each landed batch, the one who landed it runs the whole suite once on Windows, in a lock
turn on the main just landed, with the temporary files inside the drive the antivirus excludes:
$ TEMP='D:\js-harness-tmp' TMP='D:\js-harness-tmp' JS_HARNESS_GATE_WSL=0 node bin/check.mjs
Each sensor’s row then ends “on Windows (JS_HARNESS_GATE_WSL=0), its temporary files under
D:\js-harness-tmp”, so a run that forgot TEMP shows it. A red run there is fixed like any red main
(odd/tasks/gate-wsl.md).
$ js-harness merge-queue --dry-run
PR 144 is left out: PR 144 is not mergeable (CONFLICTING).
Candidate: main eb6abf7 with 2 pull requests merged in order: PR 137 (docs/gate-turn-guards-followups), PR 146 (fix/codegraph-home-files).
Dry run: nothing was tested or pushed, and the candidate was removed.
js-harness functions
Shows each function the project can turn off and on in harness.json under "functions": its
value here and why.
$ js-harness functions
codeMap off auto: no file of the project is a program
languageServer off auto: no file of the project is TypeScript
codexReview off auto: no file of the project is a program
js-harness remote
Keeps Claude Code’s remote-control server running for this git repository, so the Claude app
can start sessions here: start, stop and status. start --at-login also starts it at
every Windows login.
$ js-harness remote status
remote: not running
name: project
project: my-project
log: ~\.js-harness\remote\project-fd15a34ab7bf.log
login launcher: off
watchdog task: off
js-harness sessions
The board of the project’s recent Claude Code sessions: each one’s state, context size and
last reply, the worktrees and their feature plans, the running gates, the open pull
requests and whether a memory hygiene pass is due. It only reads. --watch waits until the board changes in a way that needs a
decision; --address prints the title of the session that holds a role; --quote prints one message the user sent; --measures prints, for each compaction
the host made, whether the session was told the pause and committed its feature document (the
orchestrator may write a line in its relief file instead), and how many compactions miss one of
those facts (nobody announces a compaction: he does not compact by hand); then the reliefs of the record (each relieved notice, judged by the context its relieved session was at: filled at the pause or another reason) and the Log lines that name a reopening, as candidates for the lead. Under each roster row’s
mode line, a line says how its session was opened, where the desktop app keeps it and its effort,
and marks a session outside his sidebar (in Routines, or not in the app) and an effort below high.
$ js-harness sessions --address orchestrator
js-harness sessions: no sessions folder for my-project: …
js-harness unseen
What the desktop app opened for this project out of his sidebar: an enabled scheduled task whose
folder is this project (the app runs it in Routines), and a session a schedule opened that sits in
Routines and is not archived. Each one to clear is named as FAIL, and the command exits 1. A
disabled task, a leftover scheduled-task folder, and a session a command opened (js-harness remote)
are INFO. It only reads; doctor reports the same count as its unseen row. --records-only reads
the app’s session records and no transcript, which is the fast path doctor uses at each session start.
$ js-harness unseen
FAIL enabled scheduled task jh-wake runs in D:\my-project: the app opens its sessions in Routines
INFO disabled scheduled task jh-old runs in D:\my-project: it opens no session
unseen: 1 to clear
js-harness memory
Keeps the project’s Claude Code memory small and true. The index of that memory is read on every
turn of every session, and in this repository it grew from 233 to 10,917 characters in 18 days with
nothing watching it. memory status says whether a hygiene pass is due and why: no verified pass
yet, the index over 4,000 bytes, a line over the host’s 150 characters, a link to no file or a note
with no line, a rule note written since the last pass, or 7 days since it with notes written since.
It reads the main checkout’s memory folder, so a worktree sees the same notes, and exits 1 when a
pass is due. memory plan, memory verify and memory undo make a pass safe: a lock, a snapshot
of the folder and a journal under ~/.js-harness/memory-hygiene/, a check that no quotation of the
user is lost and no file another session wrote is overwritten, and an undo that puts back only what
nobody wrote since and refuses while another pass holds the lock. uninstall --global removes that
folder, except a project’s folder that holds snapshots: they are the only copy of what a pass changed,
so it is kept, with its journal, and its stamps are named. The steps of a pass are in the installed js-harness skill, “Memory hygiene”.
$ js-harness memory status
memory ~/.claude/projects/D--js-harness/memory index 10917 B (budget 4000) 41 lines 42 notes
memory hygiene: due
no-pass no verified pass recorded yet
index-bytes the index is 10917 B, over 4000
line-chars 36 lines over 150 characters: 3, 4, 5, 7, ...
orphan questions-through-the-question-tool.md
js-harness subagents
Counts a day’s sub-agents of the project by role (writer, explorer, reviewer, verifier), model and effort, with their tokens and the share of the day’s tokens they took, so that what to measure next and how many to launch come from a count. It reads the Claude Code records of the main checkout and each worktree and keeps no text of any message; Codex is not counted.
$ js-harness subagents --day 2026-10-14
Sub-agents, local day 2026-10-14 (UTC-05:00), 2 record folders; Claude records only, Codex not counted
role model effort asked/ran launches input cache write cache read output
explorer claude-opus-5-5 high/high 23 932 2197153 37928674 274820
writer claude-haiku-5-5 high/high 11 646 1626090 46702162 397934
…
total 54 2502 6420288 132472395 1050507
Their share of the day's tokens: input 19%, cache write 29%, cache read 8%, output 23%
Read as other: 5; effort or model inherited from the session: 14; call not found: 1; carried over from the day before: 0
Read in 8.2 s
js-harness audit
Counts, per session of a day, the two claims a transcript can check by itself, and prints each
wrong one at its transcript and line: the hour a message to another session stamps in its
opening, against the hour the entry was sent (more than a minute apart is wrong), and the share
of its context a session states of itself, against the share its own reply records (more than 2
points apart is wrong). The share is of the window the session’s own last get_usage read
reports, because sessions run on windows of different sizes. It reads the Claude Code records of
the main checkout and each worktree and prints no text of any message.
$ js-harness audit
Audit, local day 2026-10-15 (UTC-05:00), 4 record folders: the hours messages stamp and the context shares sessions state, each against its transcript
session title messages stamped wrong hour shares wrong share
2ec5f55e JH 15 · auditoría de honestidad 7 1 0 8 4
d3ecd893 JH 8 · turno y corte temprano del gate 11 3 0 10 2
f7f03a7e JH 10 · comprobación en GitHub 29 0 0 4 0
total 367 7 0 84 40
Wrong shares, more than 2 points from the share the reply records (transcript:line):
2ec5f55e-e2da-493b-8819-43b3c57394be:4725 stated 50%, recorded 64%
d3ecd893-952c-4591-a356-73f605c9fc90:15014 stated 36%, recorded 39%
Read in 13.7 s
js-harness notify
Appends one notice to the project’s channel record under the home, with the time, the sender’s session, worktree and branch, then prints the address of the role it is for. It sends no message itself.
$ js-harness notify --to orchestrator --kind note "JH 1: the README's example of a notice"
js-harness notify: recorded note for the orchestrator at 2026-10-14T09:24:29.740Z from session 9265156a in ~\.js-harness\channel\…
address of the orchestrator: no single one now (sessions --address exit 1): …
A pull request ready to land is announced with --kind ready --pr : the record keeps
the number, and the board shows it (“ready for pull request 344”), so the land can tell which
session a pull request belongs to from the record, even when the notice was sent from main.
The record stamps every notice with the clock, so the text needs no hour. A text that opens with
one as its sending (19:48 JH ORQ: …, or after the label, JH 5, 19:48: …) more than a minute
off the clock is refused with exit 1, the right hour, and nothing written. An hour after a colon
is an event (JH 5 · canal: compacted at 18:46), and an answer, which quotes his words, is not
held to it.
js-harness wake
A boot kills every background command of every session, and the transcripts do not say so: a
wait the boot killed still reads as running. After a boot, wake lists the sessions of the
project that were open before it (an entry in the 12 hours before), each with the command that
prints its stored opening and the js-harness wait calls the boot killed. With --notify it
leaves each session whose title names a role a note, which the inbox hook tells it on its next
tool call: its waits are gone, read the opening, re-arm only its declared waits. The
orchestrator gets the list of sessions to wake with send_message, since no command can put a
prompt into an idle session. A second run after the same boot records nothing.
$ js-harness wake --notify
The machine booted at 2026-10-15T04:52:10.000Z. 2 sessions were open before it (an entry in the 12 hours before).
JH 5 · canal entre sesiones (last entry 2026-10-15T04:51:00.000Z, e5376fe9-d1cc-4288-b9e9-cf533ba35a5a)
opening: js-harness sessions --opening 'JH 5 · canal entre sesiones'
waits the boot killed:
js-harness wait --pr 162 --for 60
tell it: The machine booted at 2026-10-15T04:52:10.000Z: the background waits and tasks of this session are gone. Read your opening: js-harness sessions --opening 'JH 5 · canal entre sesiones'. Then re-arm only these declared waits: 1) js-harness wait --pr 162 --for 60
JH ORQ · js-harness (last entry 2026-10-15T04:50:00.000Z, c47efdf7-a307-47ef-8644-cf01d1bc377c)
opening: none stored
waits the boot killed: none
tell it: The machine booted at 2026-10-15T04:52:10.000Z: the background waits and tasks of this session are gone. It has no stored opening: resume from its feature document. No declared wait was running: re-arm none.
3 notes recorded in C:\Users\you\.js-harness\channel\D--js-harness.jsonl.
js-harness wake --at-login writes a file in the Windows Startup folder that runs
wake --notify for this project at each login. It records the file in the machine manifest,
so uninstall removes it; --at-login --remove removes it now. A file at that path that
js-harness did not write, or one changed since, is kept.
$ js-harness wake --at-login
wake at login: on (wrote C:\Users\you\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\js-harness-wake-js-harness-0f3a5c2d9e1b.cmd); it runs at each login, and js-harness wake --at-login --remove removes it.
js-harness wait
Waits in the background for one condition and ends when it holds: a pull request merged, a
commit or a file content on origin/main, or a notice addressed to a session in the channel
record. It says at start what it waits for and what ends it, so a session keeps working instead
of ending its turn to wait.
A work session’s wait, whatever its condition, also ends on a notice to that session’s own
number, so a session that waits on a pull request still wakes for the lead. The number is read
from the title of the transcript the host’s CLAUDE_CODE_SESSION_ID names, as notify reads a
sender. A session with no number keeps its one condition: the orchestrator, a phone session
whose transcript has no title. Such a wait exits 3, prints the notice and the same wait to run
again:
$ js-harness wait --pr 340 --for 2 --every 5
js-harness wait: waits for the merge of pull request 340; ends when it holds, on a notice to 26, or after 2 minutes; stop it with TaskStop when it is no longer needed
js-harness wait: a notice to 26 came before the merge of pull request 340 held (20:14); the wait ends
note from JH 26 · canal y compactación: smoke test of T20b
js-harness wait: to wait again, run: js-harness wait --pr 340 --for 2 --every 5
$ js-harness wait --pr 133 --for 1 --every 5
js-harness wait: waits for the merge of pull request 133; ends when it holds, or after 1 minutes; stop it with TaskStop when it is no longer needed
js-harness wait: the merge of pull request 133 holds (14:12)
js-harness resume
The hook that runs after a compaction, which install registers for Claude Code and Codex.
resume --hook reads the hook’s input and prints the session’s title, its opening, the feature
document it works on and what to read there, so nobody pastes a resume prompt. For one document
it names the lines to read, not the whole file: its resume sections (“Next step”, “State for a
resume”), the open task the first of them names first, that task’s specs and its last log entries,
each range with its size. It writes
nothing and always exits 0. resume --pause is the hook of the context pause, measured against the window the host is configured to compact on when a setting gives one (CLAUDE_CODE_AUTO_COMPACT_WINDOW, else autoCompactWindow of the project’s .claude/settings.local.json, its .claude/settings.json or the user’s settings), and of the channel’s inbox: after a tool call it says once each notice addressed to the session that it was not told yet. After a WebFetch the site refused as AI traffic (402, 403, 429 or 503) it names the next rung, js-harness fetch, to a sub-agent too; so it does after a WebFetch that failed as a tool error ("Claude Code is unable to fetch from "), which reaches no PostToolUse hook: install registers the same command as a PostToolUseFailure entry on WebFetch for that.
$ js-harness resume --hook < hook-input.json
js-harness resume: this session was just compacted. What follows is read from files; the summary of the compaction is not the record.
Session: title not known (the host gave the hook none and no session id a transcript could be found by)
…
Find there the document of the feature this session was working on, read its "Next step", and "State for a resume" if it has one, and trust them over the summary. Then repeat to the user any question that is still open.
js-harness guard
The hook that checks a tool call before it runs. install registers it for Claude Code as a
PreToolUse hook, on the sub-agent launch, on the two ways of sending a message to another
session, on Bash, on Write and Edit, and on the scheduled-task and cloud tools that open a session
(create, update and run of a scheduled task, and RemoteTrigger). guard --hook reads the call on
standard input and refuses seven things: a sub-agent launched on a model the project’s rules do not give;
a scheduled-task or cloud tool that would open a session where he does not see it, in a project that
declares sessions-open-in-sidebar, the reason naming start_session, which opens it as a row in his
sidebar, even when he asked for it; a message whose stamped hour, after its label or first before it, is more than a minute off the clock; a shell command with backticks
inside double quotes, which bash runs as commands; a heavy node --test outside a gate turn while
another gate holds the machine’s lock or an eval batch runs, heavy being several files, or one file
the gate log timed alone over a minute (an eval batch marks itself under ~/.js-harness/gate/batches
while its trials run, and uninstall removes that folder with the lock’s); and a Write or Edit that puts a line
over 150 characters, the host’s own limit, in a project’s memory index
(~/.claude/projects//memory/MEMORY.md, or under CLAUDE_CONFIG_DIR when that is set),
which every session reads on every turn; and a Write or Edit of a Markdown file that brings a
line stamping today’s date with an hour more than a minute ahead of the clock, where the line
opens with the date or the date follows “written” (- 2026-10-14 14:50 — T3 done. written at
14:44). A document also records earlier events, so an hour behind the clock passes; an Edit is
judged by the lines of the file it leaves that were not already there. A
refusal is printed as the hook’s deny, with the reason and the right value; any other call runs
as it was. One more Bash call runs with a hint the agent reads beside its result (the hook’s
additionalContext): a git commit or git merge wrapped in js-harness gate --, which holds
the machine’s lock for the commit’s whole gate, so the gate runs here and never goes to GitHub,
while a plain commit waits for its turn by itself. It reads every command the wrapper runs, a
shell’s -c included, from the folder the gate’s --cwd names. A commit with --no-verify or -n, a merge
that makes no commit, a commit after the wrapper (js-harness gate -- npm test && git commit)
and a wrapped test run get none: a test run of your own stays wrapped. A denial on the same call
wins over the hint. A Write or Edit of any other file loads nothing but the memory index check, and of a
Markdown file the hour check besides. An Edit is
judged by the file it leaves, read from disk, so a short clause that makes a line long counts; a
line that was already long there is left to the memory hygiene pass rather than blocking the edit.
It always exits 0 and writes nothing. A check that cannot read what it checks lets the call run.
The model rules are the project’s subagent-writer-model and subagent-reader-model. A project
that installs the harness has them off by default, so the model check applies only where the
project sets them.
$ js-harness guard --hook < send.json
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"js-harness guard: this message stamps 03:10, and the clock says 15:01. Stamp 15:01, or no hour: an hour in a message is read from the clock in the same step, or left out."}}
install also registers guard --stop for Claude Code, as a Stop hook: the share check. A
reply that states the session’s own context share (“Contexto: 40 %”) more than 2 points from the
share its transcript records is turned back with the hooks contract’s decision: "block", so the
session corrects it in one line from get_usage. The share is of the window the session’s last
get_usage read reports, else the default 500,000 tokens; which clauses count as the session’s
own share is js-harness audit’s reading. A reply is already shown when a Stop hook runs, so
the check cannot hold it back, only make the session correct it before the turn ends. It never
turns back a stop it already turned back (stop_hook_active), and always exits 0. When the host
sends the reply’s text (last_assistant_message) and it names no share, the transcript is not
read; a host that sends no text has the transcript’s last reply read instead.
$ js-harness guard --stop < stop.json
{"decision":"block","reason":"js-harness guard: this reply states your context at 40%, and your transcript records 60% of a 500k window (the window of your last get_usage read, else the default). The reply is already sent: correct it in one line with the share mcp__ccd_session_mgmt__get_usage reads now, and say no share you have not just read."}
js-harness reply
The reply check. install registers reply --hook as a Stop hook for Claude Code and Codex;
it acts only in a project whose nearest harness.json holds a reply key, and prints {}
everywhere else. There it reads the reply that ends the turn and turns the stop back, with the
hooks contract’s decision: "block" and the shape to follow, when the reply does not close with
exactly one state line: working (with what wakes the session on that line), decision (never while
a background task runs), cut (one code block of what the user sends, under the three labels what
it is, where it goes and what sending it does, each once) or finished (with the session to return
to on that line). A block under those labels in any other state is turned back too: a decision is
asked with the question tool. A state line is a line that starts with a state’s label once its
markdown is taken off, followed by the end of the line or a mark such as a dash or a colon
(**Working** — the gate wakes me); a label inside a sentence, or a sentence that only begins
with its word (Working on the docs), is none.
"reply": true takes the English labels (Working, Decision, Cut, Finished; What it is,
Where it goes, What sending it does). An object names the project’s own, each over the
English one: {"states": {"working": "Sigo solo", "decision": "Necesito tu decisión", "cut": "Corte", "finished": "Terminado"}, "labels": {"what": "Qué es", "where": "Dónde", "effect": "Qué pasa"}}. A key it cannot read, or a
harness.json that is not JSON, is said on standard error and checks nothing. A reply is
already shown when a Stop hook runs, so the session writes its close again in its next words.
It never turns back a stop the hooks already turned back (stop_hook_active), does not run for
sub-agents, writes nothing and always exits 0.
In the same projects, on Claude Code, it also turns back a work session (a title such as JH 7 · …)
that ends its turn holding work the orchestrator handed it, by a notice to its number or by its
opening, with nothing running in its background and no cron of its own: nothing would wake it when
a notice or a land comes, so it is told to arm js-harness wait --notice-to in the
background, or to give the work back with --kind closed. A waiting notice does not give it
back: a session that waits still arms a wait. It says so once a handing, as the transcript records
its earlier block, and again after a background task launched since has ended. A close with the
decision state is not told, a handing older than the session’s transcript belongs to another
session of that number (except the opening the session read, always stored before it began), a
call the host moved to the background when it outlived its timeout counts as a launch, and Codex, which sends no background tasks, is not checked. When the close
is refused too, both reasons go in one block.
$ js-harness reply --hook < stop.json
{"decision":"block","reason":"js-harness reply: the reply has no state line. End the reply with one line that starts with one of: Sigo solo (say what wakes you), Necesito tu decisión (never while background work runs; the question itself goes through the question tool), Corte (one code block of only what the user sends, under the lines Qué es:, Dónde: and Qué pasa:), Terminado (name the session to return to). Your reply is already shown: write the close again in your next words."}
js-harness fetch
The second rung for a page that refuses WebFetch as AI traffic (a 403, a 402, a challenge
page). It asks r.jina.ai for the page, which needs no install and no key, and reads the answer
for a block: r.jina.ai answers HTTP 200 with the site’s challenge page inside, so its status
alone would take a block for a page. A page is printed whole with exit 0. A block at this rung
exits 3 and says why and what the next rung is: the built-in browser’s get_page_text. The
resume --pause hook names this command after a WebFetch answered 402, 403, 429 or 503, and after one
that failed as a tool error.
It sends no cookie and no key. The URL goes to a third party, so a host that is not public (no dot, loopback, a private address), a URL with a user or a password, or a query parameter named like a secret is refused with exit 1 before anything is sent.
$ js-harness fetch https://www.npmjs.com/package/react
js-harness fetch: blocked at r.jina.ai: the site answered it HTTP 403.
Next rung: open https://www.npmjs.com/package/react in the built-in browser and read it with get_page_text; if the text comes back empty, wait a few seconds and read it again.
Recommended Tools
Try a different keyword or remove a filter.
Install
npx skillfish add jsaristi/js-harness