Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .coherence/runs/pr-report-words.0f6f8353.jsonl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions .coherence/runs/pr-report-words.656f4efb.jsonl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions .coherence/runs/pr-report-words.e0c74a81.jsonl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions .coherence/runs/pr-report-words.e2255a45.jsonl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions .coherence/runs/pr-report-words.fca8b5cf.jsonl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,7 @@ node src/cli.ts hooks uninstall --host claude|codex # removes only Coherence's
node src/cli.ts hooks --check --host claude|codex # exit 1 naming each event missing, stale, or extra against what install would write
node src/cli.ts hooks status # the wiring per agent host, and what each event delivers for this project
node src/cli.ts ci install --host github --mode fail|comment [--write] # the workflow that runs Coherence on every pull request; never over a file already there
node src/cli.ts ci report --session <id> # the pull request report as Markdown, from that session's run
node src/cli.ts ci report --session <id> [--base <commit>] # the pull request report as Markdown, from that session's run; --base marks findings in files changed since that commit (on GitHub Actions, the pull request's base branch by default)
npm run spec:check # the spec check alone
npm test # typecheck, tests, vocabulary check, spec check; any one failing fails the tree
```
Expand Down Expand Up @@ -624,8 +624,9 @@ repository all build. `node-llama-cpp` is an optional peer: a project that
wants the local embedding pass installs it beside Coherence.

To release, merge a change that bumps `version` in package.json, and the same
`version` in `mods/coherence/.claude-plugin/plugin.json` (a test holds them
equal), into main.
`version` in `mods/coherence/.claude-plugin/plugin.json` and in each bundled
plugin that ships at the package's version (`plugins/declared`, `python`,
`rust` and `typescript`; tests hold them equal), into main.
The Release workflow sees a version npm does not have, publishes it through
npm trusted publishing from `.github/workflows/release.yml` in the `Release`
environment, with provenance and no stored token, and tags it as a GitHub
Expand Down
2 changes: 1 addition & 1 deletion mods/coherence/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "coherence",
"version": "1.9.0",
"version": "1.10.0",
"description": "Coherence's hooks through one warm process per session, with its verdicts where the user reads them",
"author": { "name": "PostHog" },
"types": "./types/index.d.ts"
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@posthog/coherence",
"version": "1.9.0",
"version": "1.10.0",
"description": "A prosthetic for proprioception: specs, invariants, and the readings that keep an agent balanced.",
"type": "module",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion plugins/declared/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "declared",
"version": "1.9.0",
"version": "1.10.0",
"pluginApi": 2,
"provides": {
"declaredLanguages": true
Expand Down
2 changes: 1 addition & 1 deletion plugins/python/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "python",
"version": "1.9.0",
"version": "1.10.0",
"pluginApi": 2,
"provides": {
"language": "python"
Expand Down
2 changes: 1 addition & 1 deletion plugins/rust/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "rust",
"version": "1.9.0",
"version": "1.10.0",
"pluginApi": 2,
"provides": {
"language": "rust"
Expand Down
2 changes: 1 addition & 1 deletion plugins/typescript/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "typescript",
"version": "1.9.0",
"version": "1.10.0",
"pluginApi": 2,
"provides": {
"language": "typescript"
Expand Down
8 changes: 8 additions & 0 deletions src/adapters/Adapters.spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ The language adapter seam: how to ask a language's server for definitions, refer
crossing: project-source -> instrument
refuted: left an open document to the file watcher on forget, as the adapter did before -> "the check reads the current disk text after a forget, with the instrument's watcher blind to the file: a loop of edit-then-check yields zero wrong verdicts" went red in enforcement.test.ts with two of six verdicts answered from pre-edit text; restored, green (2026-09-18)
kinds: none
- a broken pipe is a server gone: A write the language server can no longer read fails the client the way the server exiting does, rejecting every pending request and refusing new ones, ends the server process, since no caller kills a client that is no longer alive, and never raises an uncaught error in the process that talks to it.
over: a server that closes its input while it still runs, then notifications and a request sent to it
via: a write to a server that closed its input fails the client and ends the server, never the process
because: the TypeScript adapter's close sends exit after shutdown; when the server had already gone, the write's EPIPE went to a stream with no error listener and crashed a CI test run in code the run was not about
crossing: instrument -> instrument
refuted: removed the stdin error listener in jsonrpc.ts -> the test, then named "a write to a server that closed its input fails the client, never the process", went red in jsonrpc.test.ts with an uncaught EPIPE; restored byte for byte, green (2026-10-25)
refuted: left the server running after its input pipe failed in jsonrpc.ts -> "a write to a server that closed its input fails the client and ends the server, never the process" went red in jsonrpc.test.ts, the process still there; restored byte for byte, green (2026-10-25)
kinds: none
- every site is a reference: Every site the language server reports for the protected thing is a reference, an import or re-export specifier included; the adapter reports the syntactic form it read at the site and never exempts one, and the check alone decides what the form means.
over: every reference site either adapter reports, import and export specifiers included
via: every site the language server reports is a reference: a bypass beneath a semicolon-less bare import is a bypass, and an import outside the chokepoint is one too
Expand Down
26 changes: 26 additions & 0 deletions src/adapters/jsonrpc.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* The JSON-RPC client over a language server's stdio: a server that stops
* reading is a server gone, never an uncaught error in the process that
* talks to it.
*/

import assert from "node:assert/strict";
import { tmpdir } from "node:os";
import { test } from "node:test";
import { JsonRpcClient } from "./jsonrpc.ts";

test("a write to a server that closed its input fails the client and ends the server, never the process", async () => {
const client = JsonRpcClient.spawn(process.execPath, ["-e", "require(\"node:fs\").closeSync(0); setInterval(() => {}, 1000);"], tmpdir());
try {
await new Promise((resolve) => setTimeout(resolve, 300));
for (let i = 0; i < 5; i++) client.notify("exit", null);
await new Promise((resolve) => setTimeout(resolve, 300));
await assert.rejects(client.request("shutdown", null, 1_000), /EPIPE|exited|write after end/);
assert.equal(client.alive, false, "a broken pipe is a server gone");
const pid = client.pid!;
await new Promise((resolve) => setTimeout(resolve, 300));
assert.throws(() => process.kill(pid, 0), /ESRCH/, "the server whose input broke is ended, since no caller will kill a client that is not alive");
} finally {
client.kill();
}
});
Loading
Loading