avouch 0.1.22 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,18 +30,31 @@ Read in this order:
30
30
 
31
31
  ## Adopting Avouch
32
32
 
33
- Start with step 0, then do the two steps, in this order.
33
+ Start with step 0, then do the three steps, in this order.
34
34
 
35
- 0. At the top level of the git repository, run `npx -y avouch init`. It installs the Claude Code plugin (see "Claude Code plugin") and adds the Avouch instructions for coding agents. The instructions tell an agent to read the rules with `avouch query`, not with a text tool, and to run `avouch check` and `avouch gate docs` after a change. There are two modes: `--append` adds the instructions to `AGENTS.md` and `CLAUDE.md`, if they exist; `--print` writes them to the terminal, and you add them yourself. Without a flag, `init` asks in a terminal and prints otherwise. `--dry-run` shows what would change and writes nothing. Then run `/avouch:scan` in Claude Code, which drafts `avouch.config.json`, a first ontology and the document review ledger. See "init" in "Command line".
35
+ 0. At the top level of the git repository, run `npx -y avouch init`. It installs the plugin for Claude Code and for Codex (see "Claude Code and Codex plugin") and adds the Avouch instructions for coding agents. The instructions tell an agent to read the rules with `avouch query`, not with a text tool, and to run `avouch check` and `avouch gate docs` after a change. There are two modes: `--append` adds the instructions to `AGENTS.md` (it creates the file when it is missing) and to `CLAUDE.md` if it exists; `--print` writes them to the terminal, and you add them yourself. Without a flag, `init` asks in a terminal and prints otherwise. `--dry-run` shows what would change and writes nothing. Then run `/avouch:scan` in Claude Code (`$avouch:scan` in Codex), which drafts `avouch.config.json`, a first ontology and the document review ledger. See "init" in "Command line".
36
36
  1. Every project: keep a document review ledger, and run `avouch gate docs` in CI. See "Document review ledger (gate docs)". The gate works with any Markdown style. It guarantees that each changed section of a design document has a recorded review decision. It does not guarantee that the decision is complete: it does not check that a person read the section, that `claims` lists every claim, or that the claimed places say what the section says.
37
37
  2. For the structured checks, choose one path. The checks are: the field lists and catalogs of R2, and R3, R5, R6, R8, R9 and R10 (SPEC section 5). Each needs a fact from the project's documents (SPEC section 6). R7 needs execution evidence, which the built-in adapter does not read.
38
38
  - (a) Write the documents to the Markdown convention, `docs/markdown-convention.md`, and use the built-in adapter. `avouch facts` checks the documents without an ontology.
39
- - (b) Write your own adapter: `docs/adapter-guide.md` sections 1 to 6. The grammar of the documents is then free. An AI agent writes it in a new session, not in the session that writes the ontology, with the prompt in "For AI agents" of that guide.
39
+ - (b) Write your own adapter: `docs/adapter-guide.md` sections 1 to 6 and 8. The grammar of the documents is then free. An AI agent writes it in a new session, not in the session that writes the ontology, with the prompt in "For AI agents" of that guide.
40
+ 3. Runtime consistency: compare the runtime with the documents. See "Runtime consistency on the command line".
41
+ - The project's own test harness records one `RuntimeCase` (SPEC section 9) for each command test: the action, the inputs (`bindings`, `actor`, `scope`, `now`), the `snapshot` that the decision reads, the result that the runtime reported (`reported`), and the expected result (`expected`).
42
+ - Take the expected result from the text of the documents. Never take it from what the code does: then the test compares the code with itself.
43
+ - Run `avouch reconcile`, then `avouch gate verify` with a baseline. Make the baseline with a script, from the output of `avouch gate verify --json`; do not edit it by hand. People write only the `reason` of each `exempt` entry.
44
+ - Do not put a known difference between the code and the documents in the baseline. Keep it as a separate test that asserts the full current behavior, and remove the test when the difference is fixed.
45
+ - Any language can do this: write the cases as JSONL and run the two commands. A TypeScript harness can instead call `reconcile(ontology, cases)` and `verifyCoverage(ontology, rows, baseline)` from the package root in the same process. Both paths give the same result (without `--check`). You do not need TypeScript.
40
46
 
41
47
  Avouch does not widen the built-in grammar for one project.
42
48
 
43
49
  A rule whose fact is not available is switched off in the profile, with a reason: `"rules": {"R8": {"disabled": "<reason>"}}` (in `profile` of the Markdown configuration; `docs/adapter-guide.md` section 5). The checker prints the reason. Without the switch, the rule reports `fact_missing` for the missing fact. The built-in adapter switches R3 and R9 off by itself when their facts are not set, and R7 always (`docs/adapter-guide.md` section 7).
44
50
 
51
+ Dependency boundary:
52
+
53
+ - Pin an exact version of `avouch` in a test or development package, and run its local bin (for example `npm exec avouch` in that package). Step 0 runs `npx` once; the gate runs the pinned version. In a repository without a root `package.json`, `npx avouch` gets the newest version, not the pinned one.
54
+ - Production code does not import `avouch`. When production code needs data from the ontology (for example a registry of command permissions), generate files from the ontology, and add a test that fails when a generated file is stale.
55
+ - Upgrade `avouch` in its own pull request, and run the full gate in it.
56
+ - `avouch check` with an adapter checks the ontology against the documents only. It is one part of the project's gate, not the whole gate. The `check` stamp of `avouch query` is `not_run` unless you give `--check` or `--adapter` (see "Query").
57
+
45
58
  ## Layout
46
59
 
47
60
  | Path | Content |
@@ -86,7 +99,7 @@ A project supplies an adapter and a profile. The adapter reads the project's sou
86
99
 
87
100
  ## Command line
88
101
 
89
- Build with `npm run build`, then run it. `avouch --help` (or `-h`) prints the usage lines of all commands; `avouch <command> --help` prints the usage of one command. Both exit with 0 and need no files, configuration or git. For example:
102
+ Build with `npm run build`, then run it. `avouch --help` (or `-h`) prints an overview: the commands, each with a short description, and the options `-h` and `-v`. `avouch <command> --help` prints the usage of one command. `avouch --version` (or `-v`) prints the version. All three exit with 0 and need no files, configuration or git. `avouch` with no arguments prints the overview on stderr and exits with 2; an unknown command prints an error and exits with 2. For example:
90
103
 
91
104
  ```sh
92
105
  avouch check [-f <ontology.yaml>] [--config <avouch.config.json>] [--adapter <module|config.json>]
@@ -107,7 +120,7 @@ An `--adapter` path that ends in `.json` is not loaded as a module: it is the co
107
120
  avouch init [--append | --print] [--plugin | --no-plugin] [--dry-run]
108
121
  ```
109
122
 
110
- `avouch init` prepares a project for Avouch. Run it inside a git repository; it works on the git top level. Step 1 is the Claude Code plugin: with `--plugin`, it runs `claude plugin marketplace add githonllc/avouch` and `claude plugin install avouch@avouch` (it does not check if the plugin is already installed, and runs both commands again on each run). Without a flag, it asks in a terminal and only prints the two commands otherwise. When `claude` is not on `PATH`, it prints the two commands. `--no-plugin` skips the step. Step 2 is the instructions for coding agents (`templates/agents-block.md`). `--append` adds them, between the lines `<!-- BEGIN avouch -->` and `<!-- END avouch -->`, to `AGENTS.md` and `CLAUDE.md` at the top level. It never creates a file: when neither file exists, it prints the instructions. It replaces an earlier block in place, keeps all other bytes and the line endings (CRLF or LF) of the file, and a second run changes nothing. A file that does not have exactly one BEGIN line and then one END line is an error, and the file is not changed. It skips `CLAUDE.md` when the file has the line `@AGENTS.md` and `AGENTS.md` exists. `--print` writes the instructions to stdout. Without a flag, it asks in a terminal and prints otherwise. `--dry-run` runs no command and writes no file; it prints the commands and, for each file, `append`, `replace` or `unchanged`. Step 3 tells you to run `/avouch:scan` in Claude Code. Exit code 0 means done (also a print and a dry run); 1 means a plugin command failed or a file could not be changed; 2 means invalid arguments or no git repository.
123
+ `avouch init` prepares a project for Avouch. Run it inside a git repository; it works on the git top level. Step 1 is the plugin, for Claude Code and then for Codex. With `--plugin`, it runs `claude plugin marketplace add githonllc/avouch` and `claude plugin install avouch@avouch`, then `codex plugin marketplace add githonllc/avouch` and `codex plugin add avouch@avouch` (it does not check if the plugin is already installed, and runs the commands again on each run). Without a flag, it asks in a terminal for each host and only prints the commands otherwise. When `claude` or `codex` is not on `PATH`, it prints the two commands of that host. Codex runs the hook only after you approve it. `--no-plugin` skips the step. Step 2 is the instructions for coding agents (`templates/agents-block.md`). `--append` adds them, between the lines `<!-- BEGIN avouch -->` and `<!-- END avouch -->`, to `AGENTS.md` and `CLAUDE.md` at the top level. It creates `AGENTS.md` when it is missing, but it never creates `CLAUDE.md`. The reason: Claude Code reads `CLAUDE.md`, and reads `AGENTS.md` only when there is no `CLAUDE.md` or through the line `@AGENTS.md` in `CLAUDE.md`; Codex reads only `AGENTS.md`. It replaces an earlier block in place, keeps all other bytes and the line endings (CRLF or LF) of the file, and a second run changes nothing. A file that does not have exactly one BEGIN line and then one END line is an error, and the file is not changed. It skips `CLAUDE.md` when the file has the line `@AGENTS.md`, because `CLAUDE.md` then imports the instructions from `AGENTS.md`. `--print` writes the instructions to stdout. Without a flag, it asks in a terminal and prints otherwise. `--dry-run` runs no command and writes no file; it prints the commands and, for each file, `create`, `append`, `replace` or `unchanged`. Step 3 tells you to run `/avouch:scan` in Claude Code. Exit code 0 means done (also a print and a dry run); 1 means a plugin command failed or a file could not be changed; 2 means invalid arguments or no git repository.
111
124
 
112
125
  ### facts
113
126
 
@@ -299,9 +312,9 @@ Exit code 2, with one line on stderr, means:
299
312
 
300
313
  The gate does not check that a person read a unit, that `claims` lists every claim of the unit, or that the claimed places say what the unit says. Review the ledger diff in the pull request, next to the document diff. Known limit: two changes to adjacent ledger lines (for example two units of one document that follow each other) can still give a merge conflict in git.
301
314
 
302
- ## Claude Code plugin
315
+ ## Claude Code and Codex plugin
303
316
 
304
- This repository is also a Claude Code plugin marketplace. Install the plugin:
317
+ This repository is also a plugin marketplace for Claude Code and for Codex. Install the plugin in Claude Code:
305
318
 
306
319
  ```sh
307
320
  claude plugin marketplace add githonllc/avouch
@@ -310,6 +323,15 @@ claude plugin install avouch@avouch
310
323
 
311
324
  Inside a Claude Code session, the same two steps are `/plugin marketplace add githonllc/avouch` and `/plugin install avouch@avouch`.
312
325
 
326
+ Install the plugin in Codex (the same marketplace file is used):
327
+
328
+ ```sh
329
+ codex plugin marketplace add githonllc/avouch
330
+ codex plugin add avouch@avouch
331
+ ```
332
+
333
+ In Codex, call the skills as `$avouch:scan` and `$avouch:cli`. Codex runs the hook only after you approve it.
334
+
313
335
  The plugin has two skills and one hook. The skills:
314
336
  - `cli` (`/avouch:cli`): checks an ontology and runs `avouch query`.
315
337
  - `scan` (`/avouch:scan [project-dir]`): drafts or updates a project's ontology from its design documents, and records the review decision of each document section in the ledger (`avouch gate docs` must pass). It uses the adapter that the project has configured and never writes an adapter. The result is a candidate. `avouch check` must exit 0, and each source gap is listed with a proposed fix to the document. When most objects have no documented field list, it also writes `avouch/doc-patch.md`: field lists, foreign keys and store names derived from the code, for a human to review and merge into the documents. It never drafts states, permissions or rules from code.
@@ -322,18 +344,21 @@ The skills run the CLI with `npx -y avouch@<version>`, pinned to the version of
322
344
 
323
345
  ## Publishing
324
346
 
325
- The npm package is `avouch`, and its command is `avouch`. Versions 0.1.0 to 0.1.21 were published as `@githonllc/avouch`, which is deprecated. A library user changes imports from `@githonllc/avouch` to `avouch`. Versions 1.0.1 to 1.0.7 of `avouch` are an unrelated earlier package from 2020 by another author. The name was transferred to Githon LLC in 2026. Those versions sort higher than 0.x, so `release.yml` publishes with `--tag latest`.
347
+ The npm package is `avouch`, and its command is `avouch`. Versions 0.1.0 to 0.1.21 were published only as `@githonllc/avouch`. Version 0.1.22 was published only as `avouch`. From 0.1.23, each release publishes the same files as `avouch` and as `@githonllc/avouch`; the docs use `avouch`. The name `avouch` was transferred to Githon LLC in 2026, and the earlier, unrelated versions 1.0.0 to 1.0.7 were unpublished. The two packages declare the same command `avouch`, so a project installs one of them, not both.
348
+
349
+ `avouch-cli` is a separate package in `alias/avouch-cli/`. It only forwards to `avouch`, so that `npx -y avouch-cli` does not run a stranger's package. It is published by hand and depends on `"avouch": "*"`. It needs a new version only when the forwarding code changes.
326
350
 
327
351
  `package.json` has `"private": true` in the repository, so an accidental `npm publish` from a working copy is refused. The release workflow removes that line only in its own checkout.
328
352
 
329
353
  A release needs the explicit approval of the project owner. Releases are published by the GitHub Actions workflow `.github/workflows/release.yml` through npm trusted publishing: no npm token is stored, and npm adds provenance. Do these steps in this sequence:
330
354
 
331
355
  1. Set the new `0.x` version in `package.json`, `.claude-plugin/plugin.json` and the `npx -y avouch@<version>` pins in `skills/*/SKILL.md`, merge it to `main`, and wait for CI to pass. Check the file list that the CI step `npm pack --dry-run` prints.
332
- 2. Tag the merge commit `v<version>` (the same version as `package.json`) and push the tag. The workflow checks that the tag matches the version, runs `npm ci`, the type check and the tests, removes `"private"`, and publishes with `--access public --tag latest`.
356
+ 2. Tag the merge commit `v<version>` (the same version as `package.json`) and push the tag. The workflow checks that the tag matches the version, runs `npm ci`, the type check and the tests, removes `"private"`, and publishes as `avouch` and then as `@githonllc/avouch`, with `--access public --tag latest`. A name that already has the version is skipped, so a re-run of a failed release publishes only the missing name.
333
357
  3. The job waits in the `npm` environment until the owner approves it in GitHub. The owner's approval is the release approval.
334
358
  4. Make sure that the release works:
335
- - `npm view avouch version` shows the new version (a new version can take a few minutes to become available);
336
- - in an empty directory, `npx -y avouch@<version>` (no arguments) prints the usage lines and exits with code 2.
359
+ - `npm view avouch version` and `npm view @githonllc/avouch version` show the new version (a new version can take a few minutes to become available);
360
+ - in an empty directory, `npx -y avouch@<version>` (no arguments) prints the help and exits with code 2;
361
+ - `npx -y avouch@<version> --version` prints the version.
337
362
 
338
363
  Version 0.1.0 was published by hand, because npm accepts a trusted publisher only for a package that exists.
339
364
 
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
2
3
  import { resolveProject, runCheck } from "./check.js";
3
- import { FACTS_USAGE, runFacts } from "./facts.js";
4
- import { INIT_USAGE, runInit } from "./init.js";
5
- import { GATE_DOCS_USAGE, GATE_USAGE, runGate } from "./gate.js";
6
- import { QUERY_USAGE, runQuery } from "./query.js";
7
- import { expectPositionals, parseOptions, RECONCILE_USAGE, runReconcile, wantsHelp } from "./reconcile.js";
4
+ import { runFacts } from "./facts.js";
5
+ import { runInit } from "./init.js";
6
+ import { runGate } from "./gate.js";
7
+ import { runQuery } from "./query.js";
8
+ import { expectPositionals, parseOptions, runReconcile, wantsHelp } from "./reconcile.js";
8
9
  const CHECK_USAGE = "usage: avouch check [-f <ontology.yaml>] [--config <avouch.config.json>] [--adapter <module|config.json>]";
9
10
  async function check(args) {
10
11
  if (wantsHelp(args, ["-f", "--config", "--adapter"])) {
@@ -35,12 +36,34 @@ async function check(args) {
35
36
  console.log(`${ok ? "PASS" : "FAIL"}: ${errors.length} schema error(s), ${report.violations.length} violation(s), ${report.waived.length} waived`);
36
37
  return ok ? 0 : 1;
37
38
  }
38
- const USAGES = [CHECK_USAGE, QUERY_USAGE, RECONCILE_USAGE, GATE_USAGE, GATE_DOCS_USAGE, FACTS_USAGE, INIT_USAGE];
39
+ const HELP = `usage: avouch <command> [options]
40
+
41
+ Check and query an Avouch ontology: a spec whose every claim cites its source.
42
+
43
+ Commands:
44
+ init Set up a repository: agent plugin, agent instructions
45
+ check Check an ontology against the schema, rules and documents
46
+ query Read a rule: an action, object, property or citation
47
+ facts Show what the Markdown adapter reads from design documents
48
+ gate docs Require a recorded review decision for each document change
49
+ gate verify Require a verified case for each decision row
50
+ reconcile Compare runtime results with the evaluator and expectations
51
+
52
+ Options:
53
+ -h, --help Show this help, or the help of a command
54
+ -v, --version Show the version
55
+
56
+ Run "avouch <command> --help" for the options of a command.
57
+ Docs: https://avouch.dev
58
+ `;
39
59
  async function main() {
40
60
  const args = process.argv.slice(2);
41
61
  if (args[0] === "--help" || args[0] === "-h") {
42
- for (const usage of USAGES)
43
- console.log(usage);
62
+ process.stdout.write(HELP);
63
+ return 0;
64
+ }
65
+ if (args[0] === "--version" || args[0] === "-v") {
66
+ console.log(JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version);
44
67
  return 0;
45
68
  }
46
69
  if (args[0] === "check")
@@ -55,8 +78,10 @@ async function main() {
55
78
  return runFacts(args.slice(1));
56
79
  if (args[0] === "init")
57
80
  return runInit(args.slice(1));
58
- for (const usage of USAGES)
59
- console.error(usage);
81
+ if (args.length > 0)
82
+ console.error(`avouch: unknown command "${args[0]}"\nRun "avouch --help" for the commands.`);
83
+ else
84
+ process.stderr.write(HELP);
60
85
  return 2;
61
86
  }
62
87
  try {
@@ -9,10 +9,10 @@ export declare function createAsker(input: NodeJS.ReadableStream, output: NodeJS
9
9
  close: () => void;
10
10
  };
11
11
  /**
12
- * How to run `claude` with fixed arguments. On Windows `claude` is usually `claude.cmd`, which only a shell can run; the
13
- * arguments are constants, so the command line is safe for the shell. Elsewhere no shell.
12
+ * How to run a host (`claude` or `codex`) with fixed arguments. On Windows the host is usually `<host>.cmd`, which only a shell
13
+ * can run; the arguments are constants, so the command line is safe for the shell. Elsewhere no shell.
14
14
  */
15
- export declare function pluginSpawn(args: string[], platform?: NodeJS.Platform): {
15
+ export declare function pluginSpawn(args: string[], platform?: NodeJS.Platform, host?: string): {
16
16
  command: string;
17
17
  args: string[];
18
18
  options: SpawnSyncOptions;
package/dist/cli/init.js CHANGED
@@ -7,15 +7,17 @@ import { gitTop } from "./check.js";
7
7
  import { expectPositionals, parseOptions, wantsHelp } from "./reconcile.js";
8
8
  export const INIT_USAGE = "usage: avouch init [--append | --print] [--plugin | --no-plugin] [--dry-run]";
9
9
  const FLAGS = ["--append", "--print", "--plugin", "--no-plugin", "--dry-run"];
10
- const PLUGIN_COMMANDS = [
11
- ["plugin", "marketplace", "add", "githonllc/avouch"],
12
- ["plugin", "install", "avouch@avouch"],
10
+ // The hosts of the plugin, in the order that init handles them, and the commands that install it into each.
11
+ const HOSTS = [
12
+ { name: "claude", label: "Claude Code", suffix: "", commands: [["plugin", "marketplace", "add", "githonllc/avouch"], ["plugin", "install", "avouch@avouch"]] },
13
+ { name: "codex", label: "Codex", suffix: " for Codex", commands: [["plugin", "marketplace", "add", "githonllc/avouch"], ["plugin", "add", "avouch@avouch"]] },
13
14
  ];
15
+ const CODEX_HOOK_NOTE = "In Codex, the hook runs only after you approve it: approve the avouch hook when Codex asks.";
14
16
  const BEGIN = "<!-- BEGIN avouch -->";
15
17
  const END = "<!-- END avouch -->";
16
18
  const TARGETS = ["AGENTS.md", "CLAUDE.md"];
17
19
  const isTty = () => process.stdin.isTTY === true && process.stdout.isTTY === true;
18
- const commandLine = (args) => `claude ${args.join(" ")}`;
20
+ const commandLine = (host, args) => `${host} ${args.join(" ")}`;
19
21
  /**
20
22
  * Yes/no questions on one readline interface, made at the first question. Lines that arrive before a question is asked
21
23
  * (typed ahead) are queued and answer the next questions in order; at the end of input a question gets no (an empty answer).
@@ -57,21 +59,21 @@ export function createAsker(input, output) {
57
59
  let asker = createAsker(process.stdin, process.stdout);
58
60
  const ask = (question) => asker.ask(question);
59
61
  /**
60
- * How to run `claude` with fixed arguments. On Windows `claude` is usually `claude.cmd`, which only a shell can run; the
61
- * arguments are constants, so the command line is safe for the shell. Elsewhere no shell.
62
+ * How to run a host (`claude` or `codex`) with fixed arguments. On Windows the host is usually `<host>.cmd`, which only a shell
63
+ * can run; the arguments are constants, so the command line is safe for the shell. Elsewhere no shell.
62
64
  */
63
- export function pluginSpawn(args, platform = process.platform) {
65
+ export function pluginSpawn(args, platform = process.platform, host = "claude") {
64
66
  return platform === "win32"
65
- ? { command: commandLine(args), args: [], options: { stdio: "inherit", shell: true } }
66
- : { command: "claude", args, options: { stdio: "inherit" } };
67
+ ? { command: commandLine(host, args), args: [], options: { stdio: "inherit", shell: true } }
68
+ : { command: host, args, options: { stdio: "inherit" } };
67
69
  }
68
- // True when an executable file named `claude` is in a PATH directory.
69
- function claudeOnPath() {
70
+ // True when an executable file named `name` is in a PATH directory.
71
+ function onPath(name) {
70
72
  for (const dir of (process.env.PATH ?? "").split(delimiter)) {
71
73
  if (dir === "")
72
74
  continue;
73
- for (const name of process.platform === "win32" ? ["claude.exe", "claude.cmd", "claude"] : ["claude"]) {
74
- const path = join(dir, name);
75
+ for (const file of process.platform === "win32" ? [`${name}.exe`, `${name}.cmd`, name] : [name]) {
76
+ const path = join(dir, file);
75
77
  try {
76
78
  if (!statSync(path).isFile())
77
79
  continue;
@@ -130,27 +132,22 @@ function printBlock() {
130
132
  process.stdout.write(wrappedBlock("\n"));
131
133
  console.log("-----8<----- end of the Avouch agent instructions -----8<-----");
132
134
  }
133
- /** Step 1: the Claude Code plugin. Returns false when a plugin command failed. */
134
- async function plugin(opts, dryRun) {
135
- console.log("Step 1: the Avouch plugin for Claude Code (a hook that blocks text reads of an ontology, and the skills /avouch:scan and /avouch:cli).");
136
- if (opts.has("--no-plugin")) {
137
- console.log("Skipped (--no-plugin).");
138
- return true;
139
- }
135
+ /** Step 1, one host: install the plugin or print the commands. Returns false when a plugin command failed. */
136
+ async function pluginFor(host, opts, dryRun) {
140
137
  const printCommands = (why) => {
141
- console.log(`${why} To install the plugin, run:`);
142
- for (const c of PLUGIN_COMMANDS)
143
- console.log(` ${commandLine(c)}`);
138
+ console.log(`${why} To install the plugin${host.suffix}, run:`);
139
+ for (const c of host.commands)
140
+ console.log(` ${commandLine(host.name, c)}`);
144
141
  };
145
- if (!claudeOnPath()) {
146
- printCommands("No `claude` executable is on PATH.");
142
+ if (!onPath(host.name)) {
143
+ printCommands(`No \`${host.name}\` executable is on PATH.`);
147
144
  return true;
148
145
  }
149
146
  if (dryRun) {
150
147
  if (opts.has("--plugin")) {
151
148
  console.log("Dry run. These commands would run:");
152
- for (const c of PLUGIN_COMMANDS)
153
- console.log(` ${commandLine(c)}`);
149
+ for (const c of host.commands)
150
+ console.log(` ${commandLine(host.name, c)}`);
154
151
  }
155
152
  else
156
153
  printCommands(`Dry run. Without --plugin, init would ${isTty() ? "ask before it runs these commands" : "only print these commands"}.`);
@@ -162,7 +159,7 @@ async function plugin(opts, dryRun) {
162
159
  printCommands("No terminal, so init does not run the commands.");
163
160
  return true;
164
161
  }
165
- run = await ask("Install the Avouch plugin for Claude Code (user scope)?");
162
+ run = await ask(`Install the Avouch plugin for ${host.label} (user scope)?`);
166
163
  if (!run) {
167
164
  printCommands("Not installed.");
168
165
  return true;
@@ -170,60 +167,74 @@ async function plugin(opts, dryRun) {
170
167
  }
171
168
  let ok = true;
172
169
  // Both commands run even when the first fails: adding a marketplace that is already added can fail while the install still works.
173
- for (const c of PLUGIN_COMMANDS) {
174
- console.log(`Running: ${commandLine(c)}`);
175
- const s = pluginSpawn(c);
170
+ for (const c of host.commands) {
171
+ console.log(`Running: ${commandLine(host.name, c)}`);
172
+ const s = pluginSpawn(c, process.platform, host.name);
176
173
  const r = spawnSync(s.command, s.args, s.options);
177
174
  if (r.error !== undefined || r.status !== 0) {
178
175
  ok = false;
179
- console.error(`FAIL: ${commandLine(c)}: ${r.error !== undefined ? r.error.message : r.status !== null ? `exit ${r.status}` : `signal ${r.signal}`}`);
176
+ console.error(`FAIL: ${commandLine(host.name, c)}: ${r.error !== undefined ? r.error.message : r.status !== null ? `exit ${r.status}` : `signal ${r.signal}`}`);
180
177
  }
181
178
  }
182
179
  return ok;
183
180
  }
181
+ /** Step 1: the plugin for Claude Code and Codex. Returns false when a plugin command failed. */
182
+ async function plugin(opts, dryRun) {
183
+ console.log("Step 1: the Avouch plugin for Claude Code and Codex (a hook that blocks text reads of an ontology, and the skills avouch:scan and avouch:cli).");
184
+ if (opts.has("--no-plugin")) {
185
+ console.log("Skipped (--no-plugin).");
186
+ return true;
187
+ }
188
+ let ok = true;
189
+ for (const host of HOSTS) {
190
+ const hostOk = await pluginFor(host, opts, dryRun);
191
+ ok = hostOk && ok;
192
+ if (host.name === "codex" && hostOk)
193
+ console.log(CODEX_HOOK_NOTE);
194
+ }
195
+ return ok;
196
+ }
184
197
  /** Step 2: the agent instructions. Returns false when a file could not be written. */
185
198
  async function instructions(top, opts, dryRun) {
186
199
  console.log("Step 2: the Avouch instructions for coding agents (AGENTS.md, CLAUDE.md).");
187
200
  const exists = (name) => existsSync(join(top, name)) && statSync(join(top, name)).isFile();
201
+ // AGENTS.md is always a target and is created when it is missing (Codex reads only AGENTS.md); CLAUDE.md is never created.
188
202
  const files = [];
189
203
  for (const name of TARGETS) {
190
- if (!exists(name))
204
+ if (name === "CLAUDE.md" && !exists(name))
191
205
  continue;
192
- if (name === "CLAUDE.md" && exists("AGENTS.md") && readFileSync(join(top, name), "utf8").split(/\r?\n/).some((l) => l.trim() === "@AGENTS.md")) {
206
+ if (name === "CLAUDE.md" && readFileSync(join(top, name), "utf8").split(/\r?\n/).some((l) => l.trim() === "@AGENTS.md")) {
193
207
  console.log("CLAUDE.md is skipped: it has the line `@AGENTS.md`, so it imports the instructions from AGENTS.md.");
194
208
  continue;
195
209
  }
196
210
  files.push(name);
197
211
  }
212
+ const label = (name) => (exists(name) ? name : `${name} (new file)`);
198
213
  let append = opts.has("--append");
199
- if (!append && !opts.has("--print") && files.length > 0) {
214
+ if (!append && !opts.has("--print")) {
200
215
  if (dryRun) {
201
216
  console.log("Dry run without --append or --print: this is the plan for --append (init asks in a terminal and prints otherwise).");
202
217
  append = true;
203
218
  }
204
219
  else if (isTty())
205
- append = await ask(`Append the Avouch instructions to ${files.join(", ")}?`);
220
+ append = await ask(`Append the Avouch instructions to ${files.map(label).join(", ")}?`);
206
221
  }
207
222
  if (!append) {
208
223
  printBlock();
209
224
  return true;
210
225
  }
211
- if (files.length === 0) {
212
- console.log(`No file to append to: AGENTS.md and CLAUDE.md do not exist at ${top}, and init never creates a file.`);
213
- printBlock();
214
- return true;
215
- }
216
226
  let ok = true;
217
227
  for (const name of files) {
218
228
  const path = join(top, name);
219
- const plan = planFile(name, readFileSync(path));
229
+ const created = !exists(name);
230
+ const plan = planFile(name, created ? Buffer.alloc(0) : readFileSync(path));
220
231
  if ("error" in plan) {
221
232
  ok = false;
222
233
  console.error(`FAIL: ${plan.error}`);
223
234
  continue;
224
235
  }
225
236
  if (dryRun) {
226
- console.log(`Dry run: ${name}: ${plan.action === "unchanged" ? "unchanged" : `would ${plan.action === "append" ? "append the block" : "replace the block"}`}`);
237
+ console.log(`Dry run: ${name}: ${created ? "would create the file with the block" : plan.action === "unchanged" ? "unchanged" : `would ${plan.action === "append" ? "append the block" : "replace the block"}`}`);
227
238
  continue;
228
239
  }
229
240
  if (plan.action === "unchanged") {
@@ -232,7 +243,7 @@ async function instructions(top, opts, dryRun) {
232
243
  }
233
244
  try {
234
245
  writeFileSync(path, plan.bytes);
235
- console.log(`${name}: ${plan.action === "append" ? "appended the block" : "replaced the block"}`);
246
+ console.log(`${name}: ${created ? "created the file with the block" : plan.action === "append" ? "appended the block" : "replaced the block"}`);
236
247
  }
237
248
  catch (error) {
238
249
  ok = false;
@@ -185,3 +185,14 @@ Without `--check` and without `--adapter`, the check stamp of `avouch query` sta
185
185
  - the generic adapter also reports R6 `fact_missing facts.evidence`.
186
186
 
187
187
  The R3 and R9 findings are the same, because the configuration sets `facts.stateMachines`, `facts.transitionTriggers` and `facts.scenarios`. So the generic adapter does not catch three mutations: `lib-r6-none-but-written`, `lib-r7-effect-unwitnessed` and `lib-r7-undeclared-store`. It catches the other 25, `lib-r3-transition-binding`, `lib-r6-link-sweep-missing` and `lib-r9-unknown-scenario` included. With the evidence ids kept, the clean toy fails with three violations: R1 for each of the two evidence ids, and R6 `fact_missing`.
188
+
189
+ ## 8. Test your adapter with mutations
190
+
191
+ A clean PASS can prove nothing. An adapter that returns too few facts, or a rule that never fires, also gives PASS. A mutation test shows that each check can fail.
192
+
193
+ - For each rule that you turn on, keep a few mutated copies of the ontology or of the documents. Each copy changes one thing, and must fail with exactly its expected rule: that rule, and no other rule.
194
+ - Pin the result in a snapshot, so that a change in the adapter or in Avouch that changes a finding shows in the diff.
195
+ - Give each waiver (`knownSourceGaps`: `conflict` and `proposed`) and each disabled rule (`{ disabled: "<reason>" }`) a reason.
196
+ - Where the documents decide, track that no `unknown` or `unspecified` is left in the ontology: for example `permission: unknown` (R10 `permission_unknown`) or a condition without an expression (R12 `condition_unspecified`). Do not waive these when the text gives the answer; write the value. `avouch query list gaps` lists the waivers.
197
+
198
+ The model is the toy library: each file in `examples/library/mutations/` is a JSON-pointer patch of the ontology (`src/patch.ts`) with `expect: R<n>`, and `test/library.test.ts` checks that the clean ontology passes and that each mutation fails with exactly its expected rule. Mutations of the documents follow the same pattern: change one source text, run the adapter, and expect one rule.
@@ -3,7 +3,7 @@
3
3
  This document is the grammar of the built-in Markdown adapter (`src/markdown.ts`). The adapter reads facts only in the form that this document describes. A project has two ways to use Avouch with Markdown documents:
4
4
 
5
5
  1. Write the documents to this convention. Then `avouch check` runs the built-in adapter, with a JSON configuration (`docs/adapter-guide.md` section 7).
6
- 2. Write an own adapter. Then the grammar of the documents is free. See `docs/adapter-guide.md` sections 1 to 6; an AI agent uses the prompt in "For AI agents" of that guide.
6
+ 2. Write an own adapter. Then the grammar of the documents is free. See `docs/adapter-guide.md` sections 1 to 6 and 8; an AI agent uses the prompt in "For AI agents" of that guide.
7
7
 
8
8
  An AI that reformats documents can use this document as the target form. Write each fact as one block, as the templates in sections 3 to 10 show.
9
9
 
package/package.json CHANGED
@@ -1,7 +1,19 @@
1
1
  {
2
2
  "name": "avouch",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
+ "description": "Avouch: an ontology spec whose every claim cites its source, with a checker and a CLI.",
4
5
  "license": "Apache-2.0",
6
+ "author": "Githon LLC",
7
+ "keywords": [
8
+ "avouch",
9
+ "ontology",
10
+ "specification",
11
+ "design-docs",
12
+ "ai-agents",
13
+ "claude-code",
14
+ "checker",
15
+ "cli"
16
+ ],
5
17
  "repository": {
6
18
  "type": "git",
7
19
  "url": "git+https://github.com/githonllc/avouch.git"
@@ -7,6 +7,6 @@ The business rules of this project are in an Avouch ontology (`*.ontology.yaml`,
7
7
  - Do not read the ontology file with grep, cat, sed or a script. Comments and neighbouring entries in the text look like rules, but the checker does not read them.
8
8
  - If the rule is `unspecified` or is in `knownSourceGaps`, the documents do not decide it. Do not invent the behavior: tell your user, and say so in the pull request.
9
9
  - If the ontology, the documents and the code disagree, do not decide in code. Tell your user which texts disagree.
10
- - To change a rule, change the design document first, or ask your user to. Then update the ontology so that each claim quotes the new text: in Claude Code with `/avouch:scan`, otherwise with the writing rules in https://avouch.dev/llms.txt. Do not add a rule that no document states.
10
+ - To change a rule, change the design document first, or ask your user to. Then update the ontology so that each claim quotes the new text: in Claude Code with `/avouch:scan`, in Codex with `$avouch:scan`, otherwise with the writing rules in https://avouch.dev/llms.txt. Do not add a rule that no document states.
11
11
  - After you change the ontology or a cited document section, run `npx -y avouch check`, and `npx -y avouch gate docs` if the project has a ledger (`avouch.ledger.jsonl`). Both must exit 0. `check` covers the format rules only; it does not replace the project's tests.
12
12
  - Do not edit the ledger to make the gate pass without reading the changed section.