@webpieces/ai-hook-rules 0.4.637 → 0.4.639
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/ai-hook-rules",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.639",
|
|
4
4
|
"description": "Pluggable write-time validation framework for AI coding agents (@webpieces/ai-hook-rules). Claude Code PreToolUse + openclaw before_tool_call adapters share one rule engine.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"directory": "packages/tooling/ai-hook-rules"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@webpieces/rules-config": "0.4.
|
|
28
|
+
"@webpieces/rules-config": "0.4.639"
|
|
29
29
|
},
|
|
30
30
|
"publishConfig": {
|
|
31
31
|
"access": "public"
|
|
@@ -62,13 +62,28 @@ export declare class WebpiecesVersions {
|
|
|
62
62
|
/**
|
|
63
63
|
* The catalog pin, scraped from `pnpm-workspace.yaml`.
|
|
64
64
|
*
|
|
65
|
-
* Deliberately a narrow
|
|
66
|
-
* loads on the hook path, where a broken tree is exactly the case that matters)
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
65
|
+
* Deliberately a narrow scraper rather than a YAML parser: this module must stay dependency-free (it
|
|
66
|
+
* loads on the hook path, where a broken tree is exactly the case that matters). A RANGE (`^`, `~`,
|
|
67
|
+
* `workspace:*`) is returned as null, not as a version: a range cannot be compared for equality, and
|
|
68
|
+
* treating it as skew would block every consumer who pins loosely.
|
|
69
|
+
*
|
|
70
|
+
* ANCHORS AND ALIASES ARE NOT OPTIONAL TO SUPPORT. The scraper originally assumed one shape —
|
|
71
|
+
* `'@webpieces/nx-webpieces-rules': 0.4.616` — and a consumer repo that keeps the whole `@webpieces`
|
|
72
|
+
* family in lockstep the obvious way writes the version ONCE and aliases it:
|
|
73
|
+
*
|
|
74
|
+
* catalog:
|
|
75
|
+
* '@webpieces/core-context': &wp 0.4.634
|
|
76
|
+
* '@webpieces/nx-webpieces-rules': *wp
|
|
77
|
+
*
|
|
78
|
+
* There the umbrella's own value is `*wp`, which does not start with a digit, so the pin read as
|
|
79
|
+
* null and the whole TRINARY compare silently degraded to installed-vs-installed — the guard's third
|
|
80
|
+
* leg gone with no error, on exactly the repos that pin most carefully. So both halves are resolved
|
|
81
|
+
* here: a `&name` anchor DEFINED on the umbrella's line is stepped over, and a `*name` alias is
|
|
82
|
+
* looked up against the anchor definition anywhere in the file.
|
|
70
83
|
*/
|
|
71
84
|
private readPin;
|
|
85
|
+
/** One catalog value — literal, `&anchor literal`, or `*alias` — reduced to a plain version or null. */
|
|
86
|
+
private resolveValue;
|
|
72
87
|
/** The version under this tree's OWN node_modules, or null when it has none (the normal worktree). */
|
|
73
88
|
private readInstalled;
|
|
74
89
|
private readText;
|
|
@@ -100,11 +100,24 @@ class WebpiecesVersions {
|
|
|
100
100
|
/**
|
|
101
101
|
* The catalog pin, scraped from `pnpm-workspace.yaml`.
|
|
102
102
|
*
|
|
103
|
-
* Deliberately a narrow
|
|
104
|
-
* loads on the hook path, where a broken tree is exactly the case that matters)
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
103
|
+
* Deliberately a narrow scraper rather than a YAML parser: this module must stay dependency-free (it
|
|
104
|
+
* loads on the hook path, where a broken tree is exactly the case that matters). A RANGE (`^`, `~`,
|
|
105
|
+
* `workspace:*`) is returned as null, not as a version: a range cannot be compared for equality, and
|
|
106
|
+
* treating it as skew would block every consumer who pins loosely.
|
|
107
|
+
*
|
|
108
|
+
* ANCHORS AND ALIASES ARE NOT OPTIONAL TO SUPPORT. The scraper originally assumed one shape —
|
|
109
|
+
* `'@webpieces/nx-webpieces-rules': 0.4.616` — and a consumer repo that keeps the whole `@webpieces`
|
|
110
|
+
* family in lockstep the obvious way writes the version ONCE and aliases it:
|
|
111
|
+
*
|
|
112
|
+
* catalog:
|
|
113
|
+
* '@webpieces/core-context': &wp 0.4.634
|
|
114
|
+
* '@webpieces/nx-webpieces-rules': *wp
|
|
115
|
+
*
|
|
116
|
+
* There the umbrella's own value is `*wp`, which does not start with a digit, so the pin read as
|
|
117
|
+
* null and the whole TRINARY compare silently degraded to installed-vs-installed — the guard's third
|
|
118
|
+
* leg gone with no error, on exactly the repos that pin most carefully. So both halves are resolved
|
|
119
|
+
* here: a `&name` anchor DEFINED on the umbrella's line is stepped over, and a `*name` alias is
|
|
120
|
+
* looked up against the anchor definition anywhere in the file.
|
|
108
121
|
*/
|
|
109
122
|
readPin(root) {
|
|
110
123
|
const file = path.join(root, WORKSPACE_FILE);
|
|
@@ -112,11 +125,35 @@ class WebpiecesVersions {
|
|
|
112
125
|
if (text === null)
|
|
113
126
|
return null;
|
|
114
127
|
const escaped = exports.UMBRELLA_PACKAGE.replace('/', '\\/');
|
|
115
|
-
|
|
128
|
+
// `[ \t]*`, never `\s*`: under the `m` flag `\s` matches newlines, so a leading `\s*` would let
|
|
129
|
+
// the "indent" run across blank lines. Harmless for this grammar, and not a habit worth keeping
|
|
130
|
+
// in the reader whose whole bug history is a regex that matched more than it meant to.
|
|
131
|
+
const match = new RegExp(`^[ \\t]*['"]?${escaped}['"]?[ \\t]*:[ \\t]*(.*)$`, 'm').exec(text);
|
|
116
132
|
if (match === null)
|
|
117
133
|
return null;
|
|
118
|
-
|
|
119
|
-
|
|
134
|
+
return this.resolveValue(match[1], text);
|
|
135
|
+
}
|
|
136
|
+
/** One catalog value — literal, `&anchor literal`, or `*alias` — reduced to a plain version or null. */
|
|
137
|
+
resolveValue(rawValue, text) {
|
|
138
|
+
// `&wp 0.4.634` on the umbrella's own line: the anchor NAMES the value, it is not the value.
|
|
139
|
+
const value = rawValue.replace(/^&\S+[ \t]+/, '').trim();
|
|
140
|
+
const token = /^['"]?([^'"\s#]+)/.exec(value)?.[1] ?? '';
|
|
141
|
+
if (token.startsWith('*')) {
|
|
142
|
+
const anchor = token.slice(1);
|
|
143
|
+
// A YAML anchor name is a plain identifier. Refusing anything else keeps the name out of a
|
|
144
|
+
// RegExp it could otherwise inject into — and an unreadable pin is already a safe answer.
|
|
145
|
+
if (!/^[A-Za-z0-9_-]+$/.test(anchor))
|
|
146
|
+
return null;
|
|
147
|
+
// Resolve against the DEFINITION site, which is another key's value elsewhere in the catalog.
|
|
148
|
+
// The match is pinned to `<key>: &anchor <value>` on a NON-COMMENT line, because the repos
|
|
149
|
+
// that use an anchor also EXPLAIN it right above the catalog — "defined ONCE via the &wp YAML
|
|
150
|
+
// anchor below" — and a bare `&wp` search happily reads the word "YAML" out of that prose.
|
|
151
|
+
const defined = new RegExp(`^[ \\t]*['"]?[^#'"\\s:][^:\\n]*['"]?[ \\t]*:[ \\t]*&${anchor}[ \\t]+['"]?([^'"\\s#]+)`, 'm').exec(text);
|
|
152
|
+
if (defined === null)
|
|
153
|
+
return null;
|
|
154
|
+
return /^[0-9]/.test(defined[1]) ? defined[1] : null;
|
|
155
|
+
}
|
|
156
|
+
return /^[0-9]/.test(token) ? token : null;
|
|
120
157
|
}
|
|
121
158
|
/** The version under this tree's OWN node_modules, or null when it has none (the normal worktree). */
|
|
122
159
|
readInstalled(root) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webpieces-versions.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/webpieces-versions.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\nimport { toError } from './to-error';\n\n/** The umbrella package every consumer pins. One `catalog:` entry pins its children in lockstep. */\nexport const UMBRELLA_PACKAGE = '@webpieces/nx-webpieces-rules';\n\n/** `pnpm-workspace.yaml`'s catalog is the ONE place a webpieces version is declared. */\nconst WORKSPACE_FILE = 'pnpm-workspace.yaml';\n\n/**\n * ONE tree's answer to \"which @webpieces does this tree declare, and which does it have installed\".\n * Data-only (per CLAUDE.md, classes for data). `null` means \"could not be determined\" — which is NOT\n * the same as disagreeing, and every caller must treat it as \"no opinion\", never as skew.\n */\nexport class TreeVersions {\n constructor(\n readonly root: string,\n /** The catalog pin in this tree's `pnpm-workspace.yaml`. Tracked in git, so it is per-branch. */\n readonly pinned: string | null,\n /** The version actually installed under this tree's own `node_modules`, when it has one. */\n readonly installed: string | null,\n ) {}\n}\n\n/**\n * The 3-or-4 webpieces versions in play when a worktree is involved, and whether they agree.\n *\n * THREE always — main pin, worktree pin, main install — and a FOURTH when the worktree has its own\n * `node_modules`, which happens the moment anyone runs `pnpm add <anything>` in it. That fourth is the\n * uncommon one, and it is NOT optional to check: nx, vitest and the eslint plugin all run IN that tree\n * and load THAT copy. (It does not decide who *judges* the tree — with the guard hooks registered\n * absolutely, the judging binary is always the main tree's — but it decides who *builds, lints and\n * tests* it, and nothing else looks at it.)\n */\nexport class VersionQuartet {\n constructor(\n readonly main: TreeVersions,\n readonly worktree: TreeVersions,\n ) {}\n\n /** Every version that was actually readable, deduped — the set that must have exactly one member. */\n get distinct(): readonly string[] {\n const all = [this.main.pinned, this.main.installed, this.worktree.pinned, this.worktree.installed];\n const seen: string[] = [];\n for (const v of all) {\n if (v !== null && !seen.includes(v)) seen.push(v);\n }\n return seen;\n }\n\n /**\n * True when every version we could read agrees.\n *\n * FAILS OPEN on purpose: if nothing could be read (`distinct` is empty) this is `true`. A guard that\n * cannot measure must not block — the repo's worst incidents are guards that fired on a state they\n * could not diagnose, leaving an agent with no reachable cure.\n */\n get inSync(): boolean {\n return this.distinct.length <= 1;\n }\n}\n\n/**\n * Reads the webpieces versions a tree declares and installs.\n *\n * WHY A DEDICATED READER rather than reusing the sh drift guard's scraping: that one compares a pin to\n * an install WITHIN ONE TREE, which is a different question. This is the CROSS-TREE generalisation, and\n * it is the only thing that can catch the case the absolute-registration design deliberately accepts —\n * a worktree being judged by the main tree's release while its own manifest asks for another.\n *\n * Every read is best-effort and returns `null` rather than throwing: this runs on the hook's BLOCKING\n * path, so an unreadable file must degrade to \"no opinion\", never to a fault.\n */\nexport class WebpiecesVersions {\n // root -> answer. The hook resolves the same two roots many times per invocation; git and fs are\n // both far too expensive to repeat on a blocking path.\n private readonly byRoot = new Map<string, TreeVersions>();\n\n /** Both trees' versions, ready to compare. */\n quartet(mainRoot: string, worktreeRoot: string): VersionQuartet {\n return new VersionQuartet(this.forTree(mainRoot), this.forTree(worktreeRoot));\n }\n\n /** One tree's declared + installed versions, memoized. */\n forTree(root: string): TreeVersions {\n const cached = this.byRoot.get(root);\n if (cached !== undefined) return cached;\n const answer = new TreeVersions(root, this.readPin(root), this.readInstalled(root));\n this.byRoot.set(root, answer);\n return answer;\n }\n\n /**\n * The catalog pin, scraped from `pnpm-workspace.yaml`.\n *\n * Deliberately a narrow regex rather than a YAML parser: this module must stay dependency-free (it\n * loads on the hook path, where a broken tree is exactly the case that matters), and the catalog\n * entry has one shape — `'@webpieces/nx-webpieces-rules': 0.4.616`, optionally unquoted. A RANGE\n * (`^`, `~`, `workspace:*`) is returned as null, not as a version: a range cannot be compared for\n * equality, and treating it as skew would block every consumer who pins loosely.\n */\n private readPin(root: string): string | null {\n const file = path.join(root, WORKSPACE_FILE);\n const text = this.readText(file);\n if (text === null) return null;\n const escaped = UMBRELLA_PACKAGE.replace('/', '\\\\/');\n const match = new RegExp(`['\"]?${escaped}['\"]?\\\\s*:\\\\s*['\"]?([^'\"\\\\s#]+)`).exec(text);\n if (match === null) return null;\n const value = match[1];\n return /^[0-9]/.test(value) ? value : null;\n }\n\n /** The version under this tree's OWN node_modules, or null when it has none (the normal worktree). */\n private readInstalled(root: string): string | null {\n const manifest = path.join(root, 'node_modules', UMBRELLA_PACKAGE, 'package.json');\n const text = this.readText(manifest);\n if (text === null) return null;\n const match = /\"version\"\\s*:\\s*\"([^\"]+)\"/.exec(text);\n return match === null ? null : match[1];\n }\n\n // Best-effort by design: this runs on the hook's BLOCKING path, so an unreadable or half-written\n // manifest must degrade to \"no opinion\" rather than fault. A null here can only ever make the guard\n // quieter, never noisier — VersionQuartet.inSync fails open on an empty read.\n private readText(file: string): string | null {\n // webpieces-disable no-unmanaged-exceptions -- a manifest read on a PreToolUse blocking path has no chokepoint above it; letting it throw would fail every tool call over an unrelated fs error\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n /**\n * Every OTHER linked worktree of this repo, so a block can name the ones that are ALSO skewed.\n *\n * A skew is never a two-tree problem: if worktree A is aligned and B is not, the agents working in B\n * are already mis-governed and nothing has told them. Best-effort — an empty answer means \"could not\n * enumerate\", and callers must never read that as \"there are no other worktrees\".\n */\n otherWorktrees(mainRoot: string, exclude: string): readonly string[] {\n const result = spawnSync('git', ['-C', mainRoot, 'worktree', 'list', '--porcelain'], { encoding: 'utf8' });\n if (result.status !== 0) return [];\n const roots: string[] = [];\n for (const line of (result.stdout ?? '').split('\\n')) {\n if (!line.startsWith('worktree ')) continue;\n const dir = line.slice('worktree '.length).trim();\n if (dir !== '' && path.resolve(dir) !== path.resolve(mainRoot) && path.resolve(dir) !== path.resolve(exclude)) {\n roots.push(dir);\n }\n }\n return roots;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"webpieces-versions.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/webpieces-versions.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\nimport { toError } from './to-error';\n\n/** The umbrella package every consumer pins. One `catalog:` entry pins its children in lockstep. */\nexport const UMBRELLA_PACKAGE = '@webpieces/nx-webpieces-rules';\n\n/** `pnpm-workspace.yaml`'s catalog is the ONE place a webpieces version is declared. */\nconst WORKSPACE_FILE = 'pnpm-workspace.yaml';\n\n/**\n * ONE tree's answer to \"which @webpieces does this tree declare, and which does it have installed\".\n * Data-only (per CLAUDE.md, classes for data). `null` means \"could not be determined\" — which is NOT\n * the same as disagreeing, and every caller must treat it as \"no opinion\", never as skew.\n */\nexport class TreeVersions {\n constructor(\n readonly root: string,\n /** The catalog pin in this tree's `pnpm-workspace.yaml`. Tracked in git, so it is per-branch. */\n readonly pinned: string | null,\n /** The version actually installed under this tree's own `node_modules`, when it has one. */\n readonly installed: string | null,\n ) {}\n}\n\n/**\n * The 3-or-4 webpieces versions in play when a worktree is involved, and whether they agree.\n *\n * THREE always — main pin, worktree pin, main install — and a FOURTH when the worktree has its own\n * `node_modules`, which happens the moment anyone runs `pnpm add <anything>` in it. That fourth is the\n * uncommon one, and it is NOT optional to check: nx, vitest and the eslint plugin all run IN that tree\n * and load THAT copy. (It does not decide who *judges* the tree — with the guard hooks registered\n * absolutely, the judging binary is always the main tree's — but it decides who *builds, lints and\n * tests* it, and nothing else looks at it.)\n */\nexport class VersionQuartet {\n constructor(\n readonly main: TreeVersions,\n readonly worktree: TreeVersions,\n ) {}\n\n /** Every version that was actually readable, deduped — the set that must have exactly one member. */\n get distinct(): readonly string[] {\n const all = [this.main.pinned, this.main.installed, this.worktree.pinned, this.worktree.installed];\n const seen: string[] = [];\n for (const v of all) {\n if (v !== null && !seen.includes(v)) seen.push(v);\n }\n return seen;\n }\n\n /**\n * True when every version we could read agrees.\n *\n * FAILS OPEN on purpose: if nothing could be read (`distinct` is empty) this is `true`. A guard that\n * cannot measure must not block — the repo's worst incidents are guards that fired on a state they\n * could not diagnose, leaving an agent with no reachable cure.\n */\n get inSync(): boolean {\n return this.distinct.length <= 1;\n }\n}\n\n/**\n * Reads the webpieces versions a tree declares and installs.\n *\n * WHY A DEDICATED READER rather than reusing the sh drift guard's scraping: that one compares a pin to\n * an install WITHIN ONE TREE, which is a different question. This is the CROSS-TREE generalisation, and\n * it is the only thing that can catch the case the absolute-registration design deliberately accepts —\n * a worktree being judged by the main tree's release while its own manifest asks for another.\n *\n * Every read is best-effort and returns `null` rather than throwing: this runs on the hook's BLOCKING\n * path, so an unreadable file must degrade to \"no opinion\", never to a fault.\n */\nexport class WebpiecesVersions {\n // root -> answer. The hook resolves the same two roots many times per invocation; git and fs are\n // both far too expensive to repeat on a blocking path.\n private readonly byRoot = new Map<string, TreeVersions>();\n\n /** Both trees' versions, ready to compare. */\n quartet(mainRoot: string, worktreeRoot: string): VersionQuartet {\n return new VersionQuartet(this.forTree(mainRoot), this.forTree(worktreeRoot));\n }\n\n /** One tree's declared + installed versions, memoized. */\n forTree(root: string): TreeVersions {\n const cached = this.byRoot.get(root);\n if (cached !== undefined) return cached;\n const answer = new TreeVersions(root, this.readPin(root), this.readInstalled(root));\n this.byRoot.set(root, answer);\n return answer;\n }\n\n /**\n * The catalog pin, scraped from `pnpm-workspace.yaml`.\n *\n * Deliberately a narrow scraper rather than a YAML parser: this module must stay dependency-free (it\n * loads on the hook path, where a broken tree is exactly the case that matters). A RANGE (`^`, `~`,\n * `workspace:*`) is returned as null, not as a version: a range cannot be compared for equality, and\n * treating it as skew would block every consumer who pins loosely.\n *\n * ANCHORS AND ALIASES ARE NOT OPTIONAL TO SUPPORT. The scraper originally assumed one shape —\n * `'@webpieces/nx-webpieces-rules': 0.4.616` — and a consumer repo that keeps the whole `@webpieces`\n * family in lockstep the obvious way writes the version ONCE and aliases it:\n *\n * catalog:\n * '@webpieces/core-context': &wp 0.4.634\n * '@webpieces/nx-webpieces-rules': *wp\n *\n * There the umbrella's own value is `*wp`, which does not start with a digit, so the pin read as\n * null and the whole TRINARY compare silently degraded to installed-vs-installed — the guard's third\n * leg gone with no error, on exactly the repos that pin most carefully. So both halves are resolved\n * here: a `&name` anchor DEFINED on the umbrella's line is stepped over, and a `*name` alias is\n * looked up against the anchor definition anywhere in the file.\n */\n private readPin(root: string): string | null {\n const file = path.join(root, WORKSPACE_FILE);\n const text = this.readText(file);\n if (text === null) return null;\n const escaped = UMBRELLA_PACKAGE.replace('/', '\\\\/');\n // `[ \\t]*`, never `\\s*`: under the `m` flag `\\s` matches newlines, so a leading `\\s*` would let\n // the \"indent\" run across blank lines. Harmless for this grammar, and not a habit worth keeping\n // in the reader whose whole bug history is a regex that matched more than it meant to.\n const match = new RegExp(`^[ \\\\t]*['\"]?${escaped}['\"]?[ \\\\t]*:[ \\\\t]*(.*)$`, 'm').exec(text);\n if (match === null) return null;\n return this.resolveValue(match[1], text);\n }\n\n /** One catalog value — literal, `&anchor literal`, or `*alias` — reduced to a plain version or null. */\n private resolveValue(rawValue: string, text: string): string | null {\n // `&wp 0.4.634` on the umbrella's own line: the anchor NAMES the value, it is not the value.\n const value = rawValue.replace(/^&\\S+[ \\t]+/, '').trim();\n const token = /^['\"]?([^'\"\\s#]+)/.exec(value)?.[1] ?? '';\n if (token.startsWith('*')) {\n const anchor = token.slice(1);\n // A YAML anchor name is a plain identifier. Refusing anything else keeps the name out of a\n // RegExp it could otherwise inject into — and an unreadable pin is already a safe answer.\n if (!/^[A-Za-z0-9_-]+$/.test(anchor)) return null;\n // Resolve against the DEFINITION site, which is another key's value elsewhere in the catalog.\n // The match is pinned to `<key>: &anchor <value>` on a NON-COMMENT line, because the repos\n // that use an anchor also EXPLAIN it right above the catalog — \"defined ONCE via the &wp YAML\n // anchor below\" — and a bare `&wp` search happily reads the word \"YAML\" out of that prose.\n const defined = new RegExp(`^[ \\\\t]*['\"]?[^#'\"\\\\s:][^:\\\\n]*['\"]?[ \\\\t]*:[ \\\\t]*&${anchor}[ \\\\t]+['\"]?([^'\"\\\\s#]+)`, 'm').exec(text);\n if (defined === null) return null;\n return /^[0-9]/.test(defined[1]) ? defined[1] : null;\n }\n return /^[0-9]/.test(token) ? token : null;\n }\n\n /** The version under this tree's OWN node_modules, or null when it has none (the normal worktree). */\n private readInstalled(root: string): string | null {\n const manifest = path.join(root, 'node_modules', UMBRELLA_PACKAGE, 'package.json');\n const text = this.readText(manifest);\n if (text === null) return null;\n const match = /\"version\"\\s*:\\s*\"([^\"]+)\"/.exec(text);\n return match === null ? null : match[1];\n }\n\n // Best-effort by design: this runs on the hook's BLOCKING path, so an unreadable or half-written\n // manifest must degrade to \"no opinion\" rather than fault. A null here can only ever make the guard\n // quieter, never noisier — VersionQuartet.inSync fails open on an empty read.\n private readText(file: string): string | null {\n // webpieces-disable no-unmanaged-exceptions -- a manifest read on a PreToolUse blocking path has no chokepoint above it; letting it throw would fail every tool call over an unrelated fs error\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n /**\n * Every OTHER linked worktree of this repo, so a block can name the ones that are ALSO skewed.\n *\n * A skew is never a two-tree problem: if worktree A is aligned and B is not, the agents working in B\n * are already mis-governed and nothing has told them. Best-effort — an empty answer means \"could not\n * enumerate\", and callers must never read that as \"there are no other worktrees\".\n */\n otherWorktrees(mainRoot: string, exclude: string): readonly string[] {\n const result = spawnSync('git', ['-C', mainRoot, 'worktree', 'list', '--porcelain'], { encoding: 'utf8' });\n if (result.status !== 0) return [];\n const roots: string[] = [];\n for (const line of (result.stdout ?? '').split('\\n')) {\n if (!line.startsWith('worktree ')) continue;\n const dir = line.slice('worktree '.length).trim();\n if (dir !== '' && path.resolve(dir) !== path.resolve(mainRoot) && path.resolve(dir) !== path.resolve(exclude)) {\n roots.push(dir);\n }\n }\n return roots;\n }\n}\n"]}
|