-
Notifications
You must be signed in to change notification settings - Fork 0
Add Guard readiness checks and accurate upgrade guidance #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| # Readiness and upgrades | ||
|
|
||
| Guard's runtime and manifest remain **0.2.0**. The readiness tooling does not | ||
| change the shipped rules, severity mapping, hook installation, or enforcement | ||
| behavior. The implementation uses plain Node ESM without runtime dependencies. | ||
|
|
||
| ## Build and validation | ||
|
|
||
| Run `npm run doctor`, then `npm run validate` before linking the plugin. | ||
| Doctor checks Node, the platform lock utility, Python's TOML parser, and the | ||
| selected Herdr binary. It does not connect to a session or inspect private | ||
| configuration. A successful doctor check proves prerequisites only, not live | ||
| pane or harness operation. | ||
|
|
||
| Minimums are Node 20.10, Python 3.11, and Herdr 0.7.5. macOS needs `lockf`; | ||
| Linux needs `flock`. Bash is used by plugin entrypoints and syntax validation. | ||
| If several Herdr binaries are installed, | ||
| `HERDR_BIN_PATH=/absolute/path/to/herdr npm run doctor` checks the selected executable. | ||
| Start Herdr and link the plugin using that same executable. | ||
|
|
||
| The full validation checks every JavaScript and shell file, manifest commands, | ||
| release consistency, and tests using isolated temporary files and local Unix | ||
| sockets. The reporter tests run the actual Claude Code hook process against a | ||
| local reporter server. They verify decision JSON; they do not prove that a | ||
| particular installed Claude Code version honors the decision. Dangerous test | ||
| commands are strings and are never executed. | ||
|
|
||
| Validation recorded on 2026-09-13: 132 tests passed on Node 20.20.2, 24.18.0, | ||
| and 26.4.0 on macOS. Build checked 27 JavaScript/shell files and 10 manifest | ||
| entrypoints. Doctor accepted the installed Herdr 0.8.2 and rejected 0.7.1 | ||
| against the declared 0.7.5 minimum. These checks do not establish live Herdr | ||
| 0.8.2 integration coverage. | ||
|
|
||
| ## Upgrading from 0.1.1 | ||
|
|
||
| 0.2.0 adds the harness reporter and expands the default policy from 26 to 52 | ||
| rules. Existing `rules.json` files are preserved. Upgrading the plugin does | ||
| not automatically add the new defaults to a customized policy. Compare your | ||
| configuration with `src/rules-default.json` before choosing which new rules | ||
| to adopt. The `structupath.guard.reset-rules` action replaces the current rules | ||
| with defaults and creates a timestamped backup; use it only when that reset | ||
| is intended. The reset audit records old and new rule counts. | ||
|
|
||
| The reporter rendezvous is `$XDG_STATE_HOME/herdr-guard/reporter.sock`, falling | ||
| back to `~/.local/state/herdr-guard/reporter.sock`. It is separate from | ||
| `HERDR_PLUGIN_STATE_DIR`. To override it, give the Guard process and the harness | ||
| hook the same nonempty `HERDR_GUARD_REPORTER_SOCKET`. Only one Guard process | ||
| serves a given reporter socket; another session leaves a live owner alone. | ||
|
|
||
| The bundled hook must be configured explicitly in the harness. Its responses | ||
| are advisory verdicts: interrupt-tier becomes `deny`, alert-tier becomes | ||
| `warn`, audit or unmatched becomes `allow`. The Claude Code adapter emits | ||
| `permissionDecision: "deny"` or `"ask"`; an unavailable or malformed reporter | ||
| response fails open. Paused enforcement still audits and returns `allow`. | ||
| Neither this protocol nor pane interrupts prove prevention. | ||
|
|
||
| ## Next improvements | ||
|
|
||
| 1. Repeat isolated live-session smoke tests on each supported Herdr release, | ||
| including pane restart, duplicate startup, reconnect, and named sessions. | ||
| 2. Verify the opt-in Claude Code hook against the installed harness version | ||
| using harmless fixture commands before relying on its decisions. | ||
| 3. Add a policy migration preview that compares existing rules with new | ||
| defaults while preserving user edits; keep applying changes explicit. | ||
| 4. Add additional harness adapters only with protocol and lifecycle tests. | ||
| Popup visibility and per-plugin socket permissions require upstream Herdr | ||
| support and remain limitations. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| #!/usr/bin/env node | ||
| import fs from "node:fs"; | ||
| import path from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
| import { spawnSync } from "node:child_process"; | ||
|
|
||
| const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); | ||
| let checked = 0; | ||
| for (const directory of ["src", "scripts", "hooks", "tests"]) { | ||
| for (const name of fs.readdirSync(path.join(root, directory)).sort()) { | ||
| const file = path.join(root, directory, name); | ||
| if (!fs.statSync(file).isFile()) continue; | ||
| const command = name.endsWith(".mjs") | ||
| ? [process.execPath, ["--check", file]] | ||
| : name.endsWith(".sh") ? ["bash", ["-n", file]] : null; | ||
| if (!command) continue; | ||
| const result = spawnSync(command[0], command[1], { stdio: "inherit" }); | ||
| if (result.error || result.status !== 0) { | ||
| console.error(`Syntax check failed: ${directory}/${name}${result.error ? `: ${result.error.message}` : ""}`); | ||
| process.exit(1); | ||
| } | ||
| checked += 1; | ||
| } | ||
| } | ||
| console.log(`Syntax valid: ${checked} JavaScript and shell files.`); |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,65 @@ | ||||||||||
| #!/usr/bin/env node | ||||||||||
| import fs from "node:fs"; | ||||||||||
| import path from "node:path"; | ||||||||||
| import { fileURLToPath } from "node:url"; | ||||||||||
| import { spawnSync } from "node:child_process"; | ||||||||||
| import { parseManifest } from "./check-manifest.mjs"; | ||||||||||
|
|
||||||||||
| export function versionAtLeast(actual, minimum) { | ||||||||||
| const parse = (value) => | ||||||||||
| /^(?:v|herdr\s+)?(\d+)\.(\d+)(?:\.(\d+))?(?:\s.*)?$/.exec(value.trim()); | ||||||||||
| const have = parse(actual); | ||||||||||
| const need = parse(minimum); | ||||||||||
| if (!have || !need) return false; | ||||||||||
| for (let i = 1; i <= 3; i += 1) { | ||||||||||
| const difference = Number(have[i] ?? 0) - Number(need[i] ?? 0); | ||||||||||
| if (difference !== 0) return difference > 0; | ||||||||||
| } | ||||||||||
| return true; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| function main() { | ||||||||||
| const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); | ||||||||||
| const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")); | ||||||||||
| let failed = false; | ||||||||||
| const report = (ok, message) => { | ||||||||||
| console.log(`${ok ? "PASS" : "FAIL"} ${message}`); | ||||||||||
| if (!ok) failed = true; | ||||||||||
| }; | ||||||||||
| report( | ||||||||||
| versionAtLeast(process.version, pkg.engines.node.replace(/^>=/, "")), | ||||||||||
| `Node ${process.version}; requires ${pkg.engines.node}`, | ||||||||||
| ); | ||||||||||
| report( | ||||||||||
| ["darwin", "linux"].includes(process.platform), | ||||||||||
| `Platform ${process.platform}; supports macOS and Linux`, | ||||||||||
| ); | ||||||||||
| const lock = process.platform === "darwin" ? "/usr/bin/lockf" : "flock"; | ||||||||||
| const lockCheck = spawnSync("/bin/sh", [ | ||||||||||
| "-c", 'command -v "$1" >/dev/null', "doctor", lock, | ||||||||||
| ]); | ||||||||||
| report(!lockCheck.error && lockCheck.status === 0, `Lock utility ${lock}`); | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Check Bash in
Proposed fix report(!lockCheck.error && lockCheck.status === 0, `Lock utility ${lock}`);
+ const bashCheck = spawnSync("/bin/sh", ["-c", "command -v bash >/dev/null"]);
+ report(!bashCheck.error && bashCheck.status === 0, "Bash available");
try {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| try { | ||||||||||
| const manifest = parseManifest(path.join(root, "herdr-plugin.toml")); | ||||||||||
| report(true, "Python 3.11+ TOML parser available"); | ||||||||||
| const herdr = process.env.HERDR_BIN_PATH || "herdr"; | ||||||||||
| const result = spawnSync(herdr, ["--version"], { | ||||||||||
| encoding: "utf8", | ||||||||||
| timeout: 5000, | ||||||||||
| }); | ||||||||||
| const version = result.stdout?.trim() || "unavailable"; | ||||||||||
| report( | ||||||||||
| !result.error && result.status === 0 && versionAtLeast(version, manifest.min_herdr_version), | ||||||||||
| `${version}; requires Herdr >=${manifest.min_herdr_version} (HERDR_BIN_PATH or PATH)`, | ||||||||||
| ); | ||||||||||
| } catch (error) { | ||||||||||
| report(false, error.message); | ||||||||||
| } | ||||||||||
| console.log( | ||||||||||
| "Read-only checks only; run npm run validate for source checks and tests. No panes, hooks, or policy files changed.", | ||||||||||
| ); | ||||||||||
| process.exitCode = failed ? 1 : 0; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) | ||||||||||
| main(); | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import test from "node:test"; | ||
| import assert from "node:assert/strict"; | ||
| import { versionAtLeast } from "../scripts/doctor.mjs"; | ||
|
|
||
| test("doctor compares stable Node and Herdr versions numerically", () => { | ||
| assert.equal(versionAtLeast("herdr 0.7.1", "0.7.5"), false); | ||
| assert.equal(versionAtLeast("herdr 0.7.5", "0.7.5"), true); | ||
| assert.equal(versionAtLeast("herdr 0.10.0", "0.7.5"), true); | ||
| assert.equal(versionAtLeast("v20.9.0", "20.10"), false); | ||
| assert.equal(versionAtLeast("v24.13.0", "20.10"), true); | ||
| assert.equal(versionAtLeast("v20.10.0", "20.10"), true); | ||
| assert.equal(versionAtLeast("0.7.5-rc.1", "0.7.5"), false); | ||
| assert.equal(versionAtLeast("unavailable", "0.7.5"), false); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Point the Guard guide link to release-aligned documentation.
The linked guide currently identifies the plugin release as
0.1.1, while this README identifies release0.2.0. Readers can receive outdated validation and support claims. Publish a0.2.0guide or replace this link before presenting it as canonical documentation. (herdr.structupath.ai)🤖 Prompt for AI Agents