@gatebolt/cli 0.3.1 → 0.5.0
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 +68 -19
- package/dist/agents/claude-code.js +183 -0
- package/dist/commands/configure.js +30 -13
- package/dist/commands/declare.js +54 -12
- package/dist/commands/hook.js +128 -0
- package/dist/commands/init.js +221 -0
- package/dist/commands/link.js +61 -0
- package/dist/commands/login.js +317 -0
- package/dist/commands/observe.js +46 -14
- package/dist/config.js +48 -12
- package/dist/contracts.js +6 -0
- package/dist/diff.js +6 -4
- package/dist/errors.js +35 -0
- package/dist/git.js +26 -1
- package/dist/help.js +161 -0
- package/dist/index.js +42 -43
- package/dist/profiles.js +48 -13
- package/dist/rest.js +49 -0
- package/dist/session.js +2 -1
- package/dist/transport.js +32 -12
- package/package.json +3 -4
- package/recipes/claude-code/README.md +0 -64
- package/recipes/claude-code/hooks/gatebolt-guard.sh +0 -48
- package/recipes/claude-code/hooks/gatebolt-observe.sh +0 -24
- package/recipes/claude-code/hooks/pre-push +0 -23
- package/recipes/claude-code/settings.json +0 -25
package/dist/profiles.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
+
import { isUuid } from "./contracts.js";
|
|
4
5
|
import { isRecord } from "./json.js";
|
|
5
|
-
const
|
|
6
|
+
export const REPO_LINK_FILE = ".gatebolt";
|
|
6
7
|
const CONFIG_FILE_MODE = 0o600;
|
|
7
8
|
function configPath() {
|
|
8
9
|
return (nonEmpty(process.env.GATEBOLT_CONFIG) ??
|
|
@@ -28,14 +29,15 @@ export function parseConfig(raw) {
|
|
|
28
29
|
function parseProfile(value) {
|
|
29
30
|
if (!isRecord(value))
|
|
30
31
|
return null;
|
|
31
|
-
const { url,
|
|
32
|
-
if (typeof
|
|
32
|
+
const { url, key } = value;
|
|
33
|
+
if (typeof key !== "string")
|
|
33
34
|
return null;
|
|
34
|
-
|
|
35
|
-
if (url !== undefined && typeof url !== "string") {
|
|
35
|
+
if (url !== undefined && typeof url !== "string")
|
|
36
36
|
return null;
|
|
37
|
-
}
|
|
38
|
-
|
|
37
|
+
const profile = { key };
|
|
38
|
+
if (url !== undefined)
|
|
39
|
+
profile.url = url;
|
|
40
|
+
return profile;
|
|
39
41
|
}
|
|
40
42
|
export function selectProfileName(config, inputs) {
|
|
41
43
|
const explicit = nonEmpty(inputs.name) ?? nonEmpty(inputs.env) ?? nonEmpty(inputs.pointer);
|
|
@@ -51,21 +53,25 @@ export async function resolveProfile(opts) {
|
|
|
51
53
|
if (!config)
|
|
52
54
|
return null;
|
|
53
55
|
const explicit = nonEmpty(opts.name) ?? nonEmpty(process.env.GATEBOLT_PROFILE);
|
|
54
|
-
const pointer = await
|
|
56
|
+
const pointer = await readLinkedProfile(opts.root);
|
|
57
|
+
const preference = configured(config, nonEmpty(opts.preferred)) ?? pointer;
|
|
55
58
|
const name = selectProfileName(config, {
|
|
56
59
|
name: opts.name,
|
|
57
60
|
env: process.env.GATEBOLT_PROFILE,
|
|
58
|
-
pointer,
|
|
61
|
+
pointer: preference,
|
|
59
62
|
});
|
|
60
63
|
if (!name)
|
|
61
64
|
return null;
|
|
62
65
|
const profile = config.profiles[name];
|
|
63
66
|
if (profile)
|
|
64
|
-
return profile;
|
|
65
|
-
if (!explicit && name ===
|
|
67
|
+
return { name, ...profile };
|
|
68
|
+
if (!explicit && name === preference)
|
|
66
69
|
return null;
|
|
67
70
|
throw new Error(`Profile "${name}" is not in ${configPath()}. Run \`gatebolt configure --profile ${name}\`.`);
|
|
68
71
|
}
|
|
72
|
+
function configured(config, name) {
|
|
73
|
+
return name && config.profiles[name] ? name : undefined;
|
|
74
|
+
}
|
|
69
75
|
export function profileLoader(opts) {
|
|
70
76
|
let cached;
|
|
71
77
|
return () => (cached ??= resolveProfile(opts));
|
|
@@ -102,14 +108,43 @@ async function loadConfig() {
|
|
|
102
108
|
}
|
|
103
109
|
return config;
|
|
104
110
|
}
|
|
105
|
-
async function
|
|
111
|
+
async function readLinkedProfile(root) {
|
|
106
112
|
try {
|
|
107
|
-
|
|
113
|
+
const link = await readRepoLink(root);
|
|
114
|
+
return link?.profile;
|
|
108
115
|
}
|
|
109
116
|
catch {
|
|
110
117
|
return undefined;
|
|
111
118
|
}
|
|
112
119
|
}
|
|
120
|
+
export async function readRepoLink(root) {
|
|
121
|
+
let raw;
|
|
122
|
+
try {
|
|
123
|
+
raw = await readFile(join(root, REPO_LINK_FILE), "utf8");
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (isNotFound(error))
|
|
127
|
+
return null;
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
const data = safeJsonParse(raw);
|
|
131
|
+
if (!isRecord(data) ||
|
|
132
|
+
typeof data.repositoryId !== "string" ||
|
|
133
|
+
!isUuid(data.repositoryId)) {
|
|
134
|
+
throw new Error(`${join(root, REPO_LINK_FILE)} is malformed — it should hold {"repositoryId": "<uuid>"}. ` +
|
|
135
|
+
"Run `gatebolt link` from the repo root to rewrite it, then commit it.");
|
|
136
|
+
}
|
|
137
|
+
const link = { repositoryId: data.repositoryId };
|
|
138
|
+
if (typeof data.profile === "string") {
|
|
139
|
+
link.profile = data.profile;
|
|
140
|
+
}
|
|
141
|
+
return link;
|
|
142
|
+
}
|
|
143
|
+
export async function writeRepoLink(root, link) {
|
|
144
|
+
const path = join(root, REPO_LINK_FILE);
|
|
145
|
+
await writeFile(path, JSON.stringify(link, null, 2) + "\n", "utf8");
|
|
146
|
+
return path;
|
|
147
|
+
}
|
|
113
148
|
function safeJsonParse(raw) {
|
|
114
149
|
try {
|
|
115
150
|
return JSON.parse(raw);
|
package/dist/rest.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const AUTH_BASE_PATH = "/api/auth";
|
|
2
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
3
|
+
export async function authFetch(baseUrl, path, request) {
|
|
4
|
+
let response;
|
|
5
|
+
try {
|
|
6
|
+
response = await fetch(buildUrl(baseUrl, path, request.query), {
|
|
7
|
+
method: request.method,
|
|
8
|
+
headers: buildHeaders(request),
|
|
9
|
+
body: request.body === undefined ? undefined : JSON.stringify(request.body),
|
|
10
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
throw connectionError(baseUrl, error);
|
|
15
|
+
}
|
|
16
|
+
return { status: response.status, json: await readJson(response) };
|
|
17
|
+
}
|
|
18
|
+
function buildUrl(baseUrl, path, query) {
|
|
19
|
+
const url = new URL(`${baseUrl}${AUTH_BASE_PATH}${path}`);
|
|
20
|
+
for (const [name, value] of Object.entries(query ?? {})) {
|
|
21
|
+
url.searchParams.set(name, value);
|
|
22
|
+
}
|
|
23
|
+
return url.toString();
|
|
24
|
+
}
|
|
25
|
+
function buildHeaders(request) {
|
|
26
|
+
const headers = {};
|
|
27
|
+
if (request.body !== undefined)
|
|
28
|
+
headers["content-type"] = "application/json";
|
|
29
|
+
if (request.token)
|
|
30
|
+
headers.authorization = `Bearer ${request.token}`;
|
|
31
|
+
return headers;
|
|
32
|
+
}
|
|
33
|
+
async function readJson(response) {
|
|
34
|
+
const text = await response.text();
|
|
35
|
+
if (!text)
|
|
36
|
+
return null;
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(text);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function connectionError(baseUrl, error) {
|
|
45
|
+
if (error instanceof Error && error.name === "TimeoutError") {
|
|
46
|
+
return new Error(`Timed out waiting for ${baseUrl}.`);
|
|
47
|
+
}
|
|
48
|
+
return new Error(`Could not reach ${baseUrl}.`);
|
|
49
|
+
}
|
package/dist/session.js
CHANGED
|
@@ -26,11 +26,12 @@ export function writeSession(gitDir, session) {
|
|
|
26
26
|
return writeJson(gitDir, SESSION_FILE, session);
|
|
27
27
|
}
|
|
28
28
|
export function readSession(gitDir) {
|
|
29
|
-
return readJson(gitDir, SESSION_FILE, ({ changeId, baseSha, branch }) => typeof changeId === "string" && typeof baseSha === "string"
|
|
29
|
+
return readJson(gitDir, SESSION_FILE, ({ changeId, baseSha, branch, profile }) => typeof changeId === "string" && typeof baseSha === "string"
|
|
30
30
|
? {
|
|
31
31
|
changeId,
|
|
32
32
|
baseSha,
|
|
33
33
|
branch: typeof branch === "string" ? branch : undefined,
|
|
34
|
+
profile: typeof profile === "string" ? profile : undefined,
|
|
34
35
|
}
|
|
35
36
|
: null);
|
|
36
37
|
}
|
package/dist/transport.js
CHANGED
|
@@ -3,11 +3,15 @@ const REQUEST_TIMEOUT_MS = 30_000;
|
|
|
3
3
|
export class RequestError extends Error {
|
|
4
4
|
status;
|
|
5
5
|
code;
|
|
6
|
-
|
|
6
|
+
procedure;
|
|
7
|
+
fieldErrors;
|
|
8
|
+
constructor(message, status, code, details) {
|
|
7
9
|
super(message);
|
|
8
10
|
this.status = status;
|
|
9
11
|
this.code = code;
|
|
10
12
|
this.name = "RequestError";
|
|
13
|
+
this.procedure = details.procedure;
|
|
14
|
+
this.fieldErrors = details.fieldErrors ?? [];
|
|
11
15
|
}
|
|
12
16
|
}
|
|
13
17
|
export async function postChange(connection, procedure, input) {
|
|
@@ -24,7 +28,7 @@ export async function postChange(connection, procedure, input) {
|
|
|
24
28
|
});
|
|
25
29
|
}
|
|
26
30
|
catch (error) {
|
|
27
|
-
throw connectionError(connection.baseUrl, error);
|
|
31
|
+
throw connectionError(connection.baseUrl, error, procedure);
|
|
28
32
|
}
|
|
29
33
|
let body;
|
|
30
34
|
try {
|
|
@@ -32,32 +36,38 @@ export async function postChange(connection, procedure, input) {
|
|
|
32
36
|
}
|
|
33
37
|
catch (error) {
|
|
34
38
|
if (!(error instanceof SyntaxError)) {
|
|
35
|
-
throw connectionError(connection.baseUrl, error);
|
|
39
|
+
throw connectionError(connection.baseUrl, error, procedure);
|
|
36
40
|
}
|
|
37
41
|
body = null;
|
|
38
42
|
}
|
|
39
43
|
if (!response.ok) {
|
|
40
|
-
throw toRequestError(response.status, body);
|
|
44
|
+
throw toRequestError(response.status, body, procedure);
|
|
41
45
|
}
|
|
42
|
-
return extractResult(body);
|
|
46
|
+
return extractResult(body, procedure);
|
|
43
47
|
}
|
|
44
|
-
function connectionError(baseUrl, error) {
|
|
48
|
+
function connectionError(baseUrl, error, procedure) {
|
|
45
49
|
if (error instanceof Error && error.name === "TimeoutError") {
|
|
46
|
-
return new RequestError(`Timed out waiting for ${baseUrl}.`, 0, "TIMEOUT"
|
|
50
|
+
return new RequestError(`Timed out waiting for ${baseUrl}.`, 0, "TIMEOUT", {
|
|
51
|
+
procedure,
|
|
52
|
+
});
|
|
47
53
|
}
|
|
48
|
-
return new RequestError(`Could not reach ${baseUrl}.`, 0, "NETWORK"
|
|
54
|
+
return new RequestError(`Could not reach ${baseUrl}.`, 0, "NETWORK", {
|
|
55
|
+
procedure,
|
|
56
|
+
});
|
|
49
57
|
}
|
|
50
|
-
function toRequestError(status, body) {
|
|
58
|
+
function toRequestError(status, body, procedure) {
|
|
51
59
|
const parsed = parseTrpcError(body);
|
|
52
|
-
return new RequestError(parsed?.message ?? `Request failed with status ${status}.`, status, parsed?.code ?? "UNKNOWN");
|
|
60
|
+
return new RequestError(parsed?.message ?? `Request failed with status ${status}.`, status, parsed?.code ?? "UNKNOWN", { procedure, fieldErrors: parsed?.fieldErrors ?? [] });
|
|
53
61
|
}
|
|
54
|
-
export function extractResult(body) {
|
|
62
|
+
export function extractResult(body, procedure) {
|
|
55
63
|
const result = isRecord(body) ? body.result : undefined;
|
|
56
64
|
const data = isRecord(result) ? result.data : undefined;
|
|
57
65
|
if (isRecord(data) && data.json !== undefined) {
|
|
58
66
|
return data.json;
|
|
59
67
|
}
|
|
60
|
-
throw new RequestError("Unexpected response from server.", 0, "MALFORMED"
|
|
68
|
+
throw new RequestError("Unexpected response from server.", 0, "MALFORMED", {
|
|
69
|
+
procedure,
|
|
70
|
+
});
|
|
61
71
|
}
|
|
62
72
|
function parseTrpcError(body) {
|
|
63
73
|
const error = isRecord(body) ? body.error : undefined;
|
|
@@ -68,5 +78,15 @@ function parseTrpcError(body) {
|
|
|
68
78
|
return {
|
|
69
79
|
message: typeof json.message === "string" ? json.message : undefined,
|
|
70
80
|
code: isRecord(data) && typeof data.code === "string" ? data.code : undefined,
|
|
81
|
+
fieldErrors: parseFieldErrors(data),
|
|
71
82
|
};
|
|
72
83
|
}
|
|
84
|
+
function parseFieldErrors(data) {
|
|
85
|
+
const zodError = isRecord(data) ? data.zodError : undefined;
|
|
86
|
+
const fieldErrors = isRecord(zodError) ? zodError.fieldErrors : undefined;
|
|
87
|
+
if (!isRecord(fieldErrors))
|
|
88
|
+
return [];
|
|
89
|
+
return Object.values(fieldErrors)
|
|
90
|
+
.flat()
|
|
91
|
+
.filter((message) => typeof message === "string");
|
|
92
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gatebolt/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Declare-then-observe agent client for GateBolt — records the files an AI agent intends to touch, then reconciles the actual git diff.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"gatebolt",
|
|
@@ -22,8 +22,7 @@
|
|
|
22
22
|
"gatebolt": "./dist/index.js"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
|
-
"dist"
|
|
26
|
-
"recipes"
|
|
25
|
+
"dist"
|
|
27
26
|
],
|
|
28
27
|
"engines": {
|
|
29
28
|
"node": ">=20"
|
|
@@ -40,6 +39,6 @@
|
|
|
40
39
|
"build": "tsc -p tsconfig.build.json",
|
|
41
40
|
"typecheck": "tsc --noEmit --emitDeclarationOnly false && tsc -p tsconfig.test.json",
|
|
42
41
|
"test": "vitest run",
|
|
43
|
-
"test:it": "vitest run --config vitest.it.config.ts"
|
|
42
|
+
"test:it": "pnpm build && vitest run --config vitest.it.config.ts"
|
|
44
43
|
}
|
|
45
44
|
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
# GateBolt × Claude Code — declare → observe recipe
|
|
2
|
-
|
|
3
|
-
Files you copy into the repo your agent works in so a Claude Code session runs
|
|
4
|
-
the GateBolt loop on its own: the agent **declares** the files it plans to touch
|
|
5
|
-
before editing, and each turn's diff is **observed** and reconciled when the turn
|
|
6
|
-
ends. Drives the [`gatebolt` CLI](../../README.md) — read that for what `declare`
|
|
7
|
-
and `observe` do.
|
|
8
|
-
|
|
9
|
-
## What's in here
|
|
10
|
-
|
|
11
|
-
| File | Copies to | Role |
|
|
12
|
-
| --------------------------- | ------------------------------ | --------------------------------------------------- |
|
|
13
|
-
| `settings.json` | `<repo>/.claude/settings.json` | Wires the `PreToolUse` guard + `Stop` observe hook |
|
|
14
|
-
| `hooks/gatebolt-guard.sh` | `<repo>/.claude/hooks/` | Strict mode: deny `Edit`/`Write` until declared |
|
|
15
|
-
| `hooks/gatebolt-observe.sh` | `<repo>/.claude/hooks/` | `Stop`: run `gatebolt observe` when a turn ends |
|
|
16
|
-
| `hooks/pre-push` | `<repo>/.git/hooks/pre-push` | Backstop: refuse to push an un-observed declaration |
|
|
17
|
-
|
|
18
|
-
Merge the `hooks` block if the repo already has a `.claude/settings.json`.
|
|
19
|
-
|
|
20
|
-
## Setup
|
|
21
|
-
|
|
22
|
-
```sh
|
|
23
|
-
# 1. Copy the recipe in.
|
|
24
|
-
cp hooks/gatebolt-*.sh <repo>/.claude/hooks/
|
|
25
|
-
cp settings.json <repo>/.claude/settings.json
|
|
26
|
-
cp hooks/pre-push <repo>/.git/hooks/pre-push
|
|
27
|
-
chmod +x <repo>/.claude/hooks/gatebolt-*.sh <repo>/.git/hooks/pre-push
|
|
28
|
-
|
|
29
|
-
# 2. Save a GateBolt profile for this repo once — paste the key when prompted.
|
|
30
|
-
# The hooks call `gatebolt` with no flags; the profile is resolved automatically.
|
|
31
|
-
npx @gatebolt/cli configure --profile <repo-slug> \
|
|
32
|
-
--url https://your-gatebolt.app --repo <repo-slug>
|
|
33
|
-
|
|
34
|
-
# 3. Launch Claude Code.
|
|
35
|
-
export GATEBOLT_ENFORCE="strict" # or omit for lenient
|
|
36
|
-
export GATEBOLT_CLI="npx @gatebolt/cli" # optional — omit if `gatebolt` is on PATH
|
|
37
|
-
cd <repo> && claude
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
**Prereqs:** `git`, `jq`, and the [`@gatebolt/cli`](../../README.md) client
|
|
41
|
-
(`npm i -g @gatebolt/cli`). `GATEBOLT_CLI` is a command _prefix_ the hooks append
|
|
42
|
-
to, defaulting to `gatebolt`; set it to `npx @gatebolt/cli` if you didn't install
|
|
43
|
-
globally.
|
|
44
|
-
|
|
45
|
-
## The two modes (`GATEBOLT_ENFORCE`)
|
|
46
|
-
|
|
47
|
-
- **`strict`** — the guard **denies** every `Edit`/`Write` until a declaration
|
|
48
|
-
exists (holds even under `--dangerously-skip-permissions`) and tells the agent
|
|
49
|
-
to run `gatebolt declare` first. That `Bash` call isn't blocked, so the agent
|
|
50
|
-
declares, then edits go through.
|
|
51
|
-
- **`lenient`** (default) — no block; declaring is an instructed step (e.g. a
|
|
52
|
-
line in the repo's `CLAUDE.md`). The pre-push backstop guarantees nothing lands
|
|
53
|
-
undeclared.
|
|
54
|
-
|
|
55
|
-
## Notes and limits
|
|
56
|
-
|
|
57
|
-
- **One turn ≈ one change.** `Stop` fires every turn, so `observe` reconciles and
|
|
58
|
-
clears the session each turn; strict mode re-declares next turn.
|
|
59
|
-
- **Rate limit.** Two API calls per change, and the default key limit is low
|
|
60
|
-
(10/day today, surfacing as `401` when hit). Use a fresh key when iterating.
|
|
61
|
-
- **Backstop is v1.** Catches only an open, un-observed declaration (the
|
|
62
|
-
Esc-interrupt case), not work that was never declared — a follow-up.
|
|
63
|
-
- **Watch over-broad declarations.** Declaring `src/**` to dodge the block kills
|
|
64
|
-
the drift signal, like an empty declaration. The deny asks for specific paths.
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# GateBolt strict-mode PreToolUse guard — blocks Edit/Write until a declaration
|
|
3
|
-
# exists for this change. Opt-in via GATEBOLT_ENFORCE=strict.
|
|
4
|
-
set -u
|
|
5
|
-
|
|
6
|
-
# Drain stdin so the hook's pipe doesn't block; we don't need the payload.
|
|
7
|
-
cat >/dev/null
|
|
8
|
-
|
|
9
|
-
[ "${GATEBOLT_ENFORCE:-lenient}" = "strict" ] || exit 0
|
|
10
|
-
|
|
11
|
-
project_dir="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
12
|
-
git_dir=$(git -C "$project_dir" rev-parse --git-dir 2>/dev/null) || exit 0
|
|
13
|
-
case "$git_dir" in
|
|
14
|
-
/*) ;;
|
|
15
|
-
*) git_dir="$project_dir/$git_dir" ;;
|
|
16
|
-
esac
|
|
17
|
-
|
|
18
|
-
# session.json exists = already declared → allow the edit.
|
|
19
|
-
[ -f "$git_dir/gatebolt/session.json" ] && exit 0
|
|
20
|
-
|
|
21
|
-
cli="${GATEBOLT_CLI:-gatebolt}"
|
|
22
|
-
reason="GateBolt strict mode: declare the files you plan to touch before editing.
|
|
23
|
-
Run this in Bash first, listing EVERY file you plan to create or modify for this
|
|
24
|
-
task — specific repo-relative paths, not broad globs like src/** :
|
|
25
|
-
|
|
26
|
-
$cli declare \\
|
|
27
|
-
--task \"<one line describing the task>\" \\
|
|
28
|
-
--vendor claude_code --name \"Claude Code\" --model <your-model-id> \\
|
|
29
|
-
--file <path> [--file <path> ...]
|
|
30
|
-
|
|
31
|
-
GBOLT_KEY, GBOLT_URL and GBOLT_REPO are already set in your environment. Once it
|
|
32
|
-
prints \"Declared change …\", retry the edit."
|
|
33
|
-
|
|
34
|
-
# JSON deny holds even under --dangerously-skip-permissions; without jq we fall
|
|
35
|
-
# back to exit-2, which re-prompts but isn't guaranteed under bypass.
|
|
36
|
-
if command -v jq >/dev/null 2>&1; then
|
|
37
|
-
jq -n --arg r "$reason" '{
|
|
38
|
-
hookSpecificOutput: {
|
|
39
|
-
hookEventName: "PreToolUse",
|
|
40
|
-
permissionDecision: "deny",
|
|
41
|
-
permissionDecisionReason: $r
|
|
42
|
-
}
|
|
43
|
-
}'
|
|
44
|
-
exit 0
|
|
45
|
-
fi
|
|
46
|
-
|
|
47
|
-
printf '%s\n' "$reason" >&2
|
|
48
|
-
exit 2
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# GateBolt Stop hook — runs `gatebolt observe` to reconcile the diff when a turn
|
|
3
|
-
# ends. Advisory; never blocks the turn.
|
|
4
|
-
set -u
|
|
5
|
-
|
|
6
|
-
# Drain stdin; observe reads its target from the session file.
|
|
7
|
-
cat >/dev/null
|
|
8
|
-
|
|
9
|
-
cd "${CLAUDE_PROJECT_DIR:-$PWD}" || exit 0
|
|
10
|
-
|
|
11
|
-
git_dir=$(git rev-parse --git-dir 2>/dev/null) || exit 0
|
|
12
|
-
case "$git_dir" in
|
|
13
|
-
/*) ;;
|
|
14
|
-
*) git_dir="$PWD/$git_dir" ;;
|
|
15
|
-
esac
|
|
16
|
-
|
|
17
|
-
# Stop fires every turn; only reconcile when a declaration is actually open.
|
|
18
|
-
[ -f "$git_dir/gatebolt/session.json" ] || exit 0
|
|
19
|
-
|
|
20
|
-
cli="${GATEBOLT_CLI:-gatebolt}"
|
|
21
|
-
# GATEBOLT_CLI is a command prefix; word-splitting is intentional.
|
|
22
|
-
# shellcheck disable=SC2086
|
|
23
|
-
$cli observe 2>&1 || true
|
|
24
|
-
exit 0
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# GateBolt pre-push backstop — refuses to push while a declaration is still open
|
|
3
|
-
# but un-observed (the Esc-interrupt case Stop misses). Needs gatebolt's env
|
|
4
|
-
# (GBOLT_KEY / GBOLT_URL / GATEBOLT_CLI) exported in the shell you push from.
|
|
5
|
-
set -u
|
|
6
|
-
|
|
7
|
-
git_dir=$(git rev-parse --git-dir 2>/dev/null) || exit 0
|
|
8
|
-
case "$git_dir" in
|
|
9
|
-
/*) ;;
|
|
10
|
-
*) git_dir="$PWD/$git_dir" ;;
|
|
11
|
-
esac
|
|
12
|
-
|
|
13
|
-
[ -f "$git_dir/gatebolt/session.json" ] || exit 0
|
|
14
|
-
|
|
15
|
-
cli="${GATEBOLT_CLI:-gatebolt}"
|
|
16
|
-
echo "GateBolt: an un-observed declaration is still open; reconciling before push…" >&2
|
|
17
|
-
# shellcheck disable=SC2086
|
|
18
|
-
if ! $cli observe >&2; then
|
|
19
|
-
echo "GateBolt: observe failed — refusing to push unreconciled work." >&2
|
|
20
|
-
echo "Resolve the error above (or run '$cli observe' yourself), then push again." >&2
|
|
21
|
-
exit 1
|
|
22
|
-
fi
|
|
23
|
-
exit 0
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"hooks": {
|
|
3
|
-
"PreToolUse": [
|
|
4
|
-
{
|
|
5
|
-
"matcher": "Edit|Write",
|
|
6
|
-
"hooks": [
|
|
7
|
-
{
|
|
8
|
-
"type": "command",
|
|
9
|
-
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/gatebolt-guard.sh"
|
|
10
|
-
}
|
|
11
|
-
]
|
|
12
|
-
}
|
|
13
|
-
],
|
|
14
|
-
"Stop": [
|
|
15
|
-
{
|
|
16
|
-
"hooks": [
|
|
17
|
-
{
|
|
18
|
-
"type": "command",
|
|
19
|
-
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/gatebolt-observe.sh"
|
|
20
|
-
}
|
|
21
|
-
]
|
|
22
|
-
}
|
|
23
|
-
]
|
|
24
|
-
}
|
|
25
|
-
}
|