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 +36 -11
- package/dist/cli/avouch.js +35 -10
- package/dist/cli/init.d.ts +3 -3
- package/dist/cli/init.js +55 -44
- package/docs/adapter-guide.md +11 -0
- package/docs/markdown-convention.md +1 -1
- package/package.json +13 -1
- package/templates/agents-block.md +1 -1
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
336
|
-
- in an empty directory, `npx -y avouch@<version>` (no arguments) prints the
|
|
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
|
|
package/dist/cli/avouch.js
CHANGED
|
@@ -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 {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { expectPositionals, parseOptions,
|
|
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
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
59
|
-
console.error(
|
|
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 {
|
package/dist/cli/init.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
11
|
-
|
|
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) =>
|
|
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
|
|
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:
|
|
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 `
|
|
69
|
-
function
|
|
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
|
|
74
|
-
const path = join(dir,
|
|
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
|
|
134
|
-
async function
|
|
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
|
|
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 (!
|
|
146
|
-
printCommands(
|
|
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
|
|
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(
|
|
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
|
|
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" &&
|
|
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")
|
|
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
|
|
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;
|
package/docs/adapter-guide.md
CHANGED
|
@@ -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.
|
|
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.
|