@lannguyensi/harness 0.10.1 → 0.12.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/CHANGELOG.md +160 -7
- package/README.md +99 -58
- package/dist/cli/apply/apply.js +19 -3
- package/dist/cli/apply/apply.js.map +1 -1
- package/dist/cli/approve/understanding.d.ts +8 -0
- package/dist/cli/approve/understanding.js +154 -15
- package/dist/cli/approve/understanding.js.map +1 -1
- package/dist/cli/audit.d.ts +8 -0
- package/dist/cli/audit.js +2 -2
- package/dist/cli/audit.js.map +1 -1
- package/dist/cli/doctor/codex.d.ts +6 -1
- package/dist/cli/doctor/codex.js +10 -6
- package/dist/cli/doctor/codex.js.map +1 -1
- package/dist/cli/doctor/format.js +7 -1
- package/dist/cli/doctor/format.js.map +1 -1
- package/dist/cli/doctor/index.js +62 -5
- package/dist/cli/doctor/index.js.map +1 -1
- package/dist/cli/doctor/types.d.ts +15 -0
- package/dist/cli/dry-run.js +9 -3
- package/dist/cli/dry-run.js.map +1 -1
- package/dist/cli/explain.d.ts +8 -0
- package/dist/cli/explain.js +6 -4
- package/dist/cli/explain.js.map +1 -1
- package/dist/cli/gate/disable.d.ts +42 -0
- package/dist/cli/gate/disable.js +199 -0
- package/dist/cli/gate/disable.js.map +1 -0
- package/dist/cli/gate/enable.d.ts +33 -0
- package/dist/cli/gate/enable.js +127 -0
- package/dist/cli/gate/enable.js.map +1 -0
- package/dist/cli/gate/snapshot.d.ts +65 -0
- package/dist/cli/gate/snapshot.js +119 -0
- package/dist/cli/gate/snapshot.js.map +1 -0
- package/dist/cli/index.js +141 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init/dependencies.js +17 -7
- package/dist/cli/init/dependencies.js.map +1 -1
- package/dist/cli/init/templates.d.ts +1 -1
- package/dist/cli/init/templates.js +14 -5
- package/dist/cli/init/templates.js.map +1 -1
- package/dist/cli/pack/hook-pre-tool-use.d.ts +2 -0
- package/dist/cli/pack/hook-pre-tool-use.js +34 -2
- package/dist/cli/pack/hook-pre-tool-use.js.map +1 -1
- package/dist/cli/policy/intercept.d.ts +7 -1
- package/dist/cli/policy/intercept.js +28 -6
- package/dist/cli/policy/intercept.js.map +1 -1
- package/dist/cli/session-start/index.d.ts +75 -0
- package/dist/cli/session-start/index.js +232 -0
- package/dist/cli/session-start/index.js.map +1 -0
- package/dist/policy-packs/builtin/understanding-before-execution-runtime.d.ts +27 -0
- package/dist/policy-packs/builtin/understanding-before-execution-runtime.js +32 -0
- package/dist/policy-packs/builtin/understanding-before-execution-runtime.js.map +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution.d.ts +16 -1
- package/dist/policy-packs/builtin/understanding-before-execution.js +35 -7
- package/dist/policy-packs/builtin/understanding-before-execution.js.map +1 -1
- package/dist/policy-packs/expand.d.ts +3 -1
- package/dist/policy-packs/expand.js +2 -2
- package/dist/policy-packs/expand.js.map +1 -1
- package/dist/policy-packs/index.d.ts +1 -1
- package/dist/policy-packs/index.js.map +1 -1
- package/dist/policy-packs/registry.d.ts +2 -1
- package/dist/policy-packs/registry.js +2 -2
- package/dist/policy-packs/registry.js.map +1 -1
- package/dist/runtime/git-context.d.ts +16 -0
- package/dist/runtime/git-context.js +97 -0
- package/dist/runtime/git-context.js.map +1 -0
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/pending-approval.d.ts +31 -0
- package/dist/runtime/pending-approval.js +80 -0
- package/dist/runtime/pending-approval.js.map +1 -0
- package/dist/runtime/session-id.d.ts +40 -1
- package/dist/runtime/session-id.js +99 -8
- package/dist/runtime/session-id.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// `harness gate enable` — restore the most recent `gate disable` snapshot.
|
|
2
|
+
//
|
|
3
|
+
// Reads the newest `harness.gate-disable.*.json` next to settings.json,
|
|
4
|
+
// merges its `removed[]` groups back into `hooks[event][]` at their
|
|
5
|
+
// original indices, and atomically rewrites settings.json. Refuses if
|
|
6
|
+
// settings.json has been edited since the snapshot was taken (the
|
|
7
|
+
// recorded `settingsAfterSha256` no longer matches the on-disk content)
|
|
8
|
+
// unless `--force` is passed, so a hand-edit between disable and enable
|
|
9
|
+
// is not silently overwritten. Idempotent on an already-restored file
|
|
10
|
+
// (the snapshot's `settingsBeforeSha256` matches the current sha →
|
|
11
|
+
// nothing to do).
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as os from "node:os";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import { atomicWriteFile } from "../../io/atomic-write.js";
|
|
16
|
+
import { listSnapshots, readSnapshot, sha256Hex, } from "./snapshot.js";
|
|
17
|
+
const DEFAULT_SETTINGS_REL = path.join(".claude", "settings.json");
|
|
18
|
+
export class GateEnableError extends Error {
|
|
19
|
+
constructor(message) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "GateEnableError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function resolveSettingsPath(opts) {
|
|
25
|
+
if (typeof opts.settingsPath === "string" && opts.settingsPath.length > 0) {
|
|
26
|
+
return opts.settingsPath;
|
|
27
|
+
}
|
|
28
|
+
const home = opts.homeDir ?? os.homedir();
|
|
29
|
+
return path.join(home, DEFAULT_SETTINGS_REL);
|
|
30
|
+
}
|
|
31
|
+
function readCurrentSettings(settingsPath) {
|
|
32
|
+
let raw;
|
|
33
|
+
try {
|
|
34
|
+
raw = fs.readFileSync(settingsPath, "utf8");
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
if (err.code === "ENOENT") {
|
|
38
|
+
throw new GateEnableError(`settings file not found: ${settingsPath} — cannot restore into a missing target`);
|
|
39
|
+
}
|
|
40
|
+
throw new GateEnableError(`cannot read ${settingsPath}: ${err.message}`);
|
|
41
|
+
}
|
|
42
|
+
let parsed;
|
|
43
|
+
try {
|
|
44
|
+
parsed = JSON.parse(raw);
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
throw new GateEnableError(`${settingsPath} is not valid JSON (${err.message}); refusing to operate.`);
|
|
48
|
+
}
|
|
49
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
50
|
+
throw new GateEnableError(`${settingsPath} is not a JSON object; refusing to operate.`);
|
|
51
|
+
}
|
|
52
|
+
const obj = parsed;
|
|
53
|
+
const rawHooks = obj["hooks"];
|
|
54
|
+
let hooks = {};
|
|
55
|
+
if (rawHooks !== undefined) {
|
|
56
|
+
if (rawHooks === null || typeof rawHooks !== "object" || Array.isArray(rawHooks)) {
|
|
57
|
+
throw new GateEnableError(`${settingsPath} \`hooks\` field is not an object.`);
|
|
58
|
+
}
|
|
59
|
+
for (const [event, groups] of Object.entries(rawHooks)) {
|
|
60
|
+
if (!Array.isArray(groups)) {
|
|
61
|
+
throw new GateEnableError(`${settingsPath} \`hooks.${event}\` is not an array; refusing to operate.`);
|
|
62
|
+
}
|
|
63
|
+
hooks[event] = groups.slice();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { raw, obj, hooks };
|
|
67
|
+
}
|
|
68
|
+
function reinsertRemoved(hooks, snapshot) {
|
|
69
|
+
// Sort removed entries by event, then by ascending index, so we splice
|
|
70
|
+
// them back in growing order — that way an earlier insertion never
|
|
71
|
+
// shifts a later insertion's target index past the array end.
|
|
72
|
+
const sorted = [...snapshot.removed].sort((a, b) => {
|
|
73
|
+
if (a.event !== b.event)
|
|
74
|
+
return a.event < b.event ? -1 : 1;
|
|
75
|
+
return a.index - b.index;
|
|
76
|
+
});
|
|
77
|
+
let inserted = 0;
|
|
78
|
+
for (const entry of sorted) {
|
|
79
|
+
const arr = hooks[entry.event] ?? [];
|
|
80
|
+
const idx = Math.min(entry.index, arr.length);
|
|
81
|
+
arr.splice(idx, 0, entry.group);
|
|
82
|
+
hooks[entry.event] = arr;
|
|
83
|
+
inserted += 1;
|
|
84
|
+
}
|
|
85
|
+
return inserted;
|
|
86
|
+
}
|
|
87
|
+
export function gateEnable(opts = {}) {
|
|
88
|
+
const settingsPath = resolveSettingsPath(opts);
|
|
89
|
+
const settingsDir = path.dirname(settingsPath);
|
|
90
|
+
const snapshotPaths = listSnapshots(settingsDir);
|
|
91
|
+
if (snapshotPaths.length === 0) {
|
|
92
|
+
return { mode: "no-snapshots", settingsPath };
|
|
93
|
+
}
|
|
94
|
+
const latest = snapshotPaths[0];
|
|
95
|
+
const readResult = readSnapshot(latest);
|
|
96
|
+
if (!readResult.ok) {
|
|
97
|
+
throw new GateEnableError(readResult.reason);
|
|
98
|
+
}
|
|
99
|
+
const snapshot = readResult.snapshot;
|
|
100
|
+
const current = readCurrentSettings(settingsPath);
|
|
101
|
+
const currentSha = sha256Hex(current.raw);
|
|
102
|
+
if (currentSha === snapshot.settingsBeforeSha256) {
|
|
103
|
+
// The current file already matches the pre-disable state: nothing
|
|
104
|
+
// to restore. Idempotent return.
|
|
105
|
+
return { mode: "already-restored", settingsPath, snapshotPath: latest };
|
|
106
|
+
}
|
|
107
|
+
if (currentSha !== snapshot.settingsAfterSha256 && !opts.force) {
|
|
108
|
+
throw new GateEnableError(`settings.json has been edited since the snapshot was taken (current sha ${currentSha.slice(0, 12)}…, ` +
|
|
109
|
+
`snapshot's post-disable sha ${snapshot.settingsAfterSha256.slice(0, 12)}…). ` +
|
|
110
|
+
`Re-running would overwrite those edits. Inspect the diff against ` +
|
|
111
|
+
`${snapshot.settingsBackupPath}, then pass --force to restore anyway.`);
|
|
112
|
+
}
|
|
113
|
+
const restoredCount = reinsertRemoved(current.hooks, snapshot);
|
|
114
|
+
const newObj = { ...current.obj };
|
|
115
|
+
if (Object.keys(current.hooks).length === 0) {
|
|
116
|
+
delete newObj["hooks"];
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
newObj["hooks"] = current.hooks;
|
|
120
|
+
}
|
|
121
|
+
const newSettings = `${JSON.stringify(newObj, null, 2)}\n`;
|
|
122
|
+
atomicWriteFile(settingsPath, newSettings);
|
|
123
|
+
return { mode: "restored", settingsPath, snapshotPath: latest, restoredCount };
|
|
124
|
+
}
|
|
125
|
+
/** Test-helper: re-export for the test suite to inspect snapshot pickup. */
|
|
126
|
+
export { listSnapshots };
|
|
127
|
+
//# sourceMappingURL=enable.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enable.js","sourceRoot":"","sources":["../../../src/cli/gate/enable.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export declare const SNAPSHOT_BASENAME_PREFIX = "harness.gate-disable.";
|
|
2
|
+
export declare const SNAPSHOT_BASENAME_SUFFIX = ".json";
|
|
3
|
+
export declare const SNAPSHOT_VERSION: 1;
|
|
4
|
+
export interface RemovedGroup {
|
|
5
|
+
/** Hook event the group lived under, e.g. "PreToolUse". */
|
|
6
|
+
event: string;
|
|
7
|
+
/** Position of the group inside `hooks[event][]` at the time of removal. */
|
|
8
|
+
index: number;
|
|
9
|
+
/** The literal JSON value of the removed group; preserved verbatim. */
|
|
10
|
+
group: unknown;
|
|
11
|
+
}
|
|
12
|
+
export interface GateDisableSnapshot {
|
|
13
|
+
version: typeof SNAPSHOT_VERSION;
|
|
14
|
+
/** ISO-8601 UTC timestamp of the disable call. */
|
|
15
|
+
createdAt: string;
|
|
16
|
+
/** Absolute path to the settings.json that was mutated. */
|
|
17
|
+
settingsPath: string;
|
|
18
|
+
/** Absolute path the pre-mutation copy was written to. */
|
|
19
|
+
settingsBackupPath: string;
|
|
20
|
+
/**
|
|
21
|
+
* SHA-256 of `settings.json` content BEFORE the removal. `gate enable`
|
|
22
|
+
* compares the live settings.json sha against this to detect operator
|
|
23
|
+
* edits since the disable; a mismatch refuses unless `--force`.
|
|
24
|
+
*/
|
|
25
|
+
settingsBeforeSha256: string;
|
|
26
|
+
/** SHA-256 of `settings.json` content AFTER the removal. */
|
|
27
|
+
settingsAfterSha256: string;
|
|
28
|
+
/** The filter the operator applied — recorded for the audit trail. */
|
|
29
|
+
filter: {
|
|
30
|
+
matcher?: string;
|
|
31
|
+
event?: string;
|
|
32
|
+
};
|
|
33
|
+
/** Groups removed, in stable order (sorted by event then ascending index). */
|
|
34
|
+
removed: RemovedGroup[];
|
|
35
|
+
}
|
|
36
|
+
export declare function sha256Hex(input: string | Buffer): string;
|
|
37
|
+
/**
|
|
38
|
+
* Build the snapshot file path for `settingsPath` at `now`. ISO timestamps
|
|
39
|
+
* use `-` in place of `:` so the filename is portable across filesystems
|
|
40
|
+
* (Windows reject `:` in basenames).
|
|
41
|
+
*/
|
|
42
|
+
export declare function snapshotPath(settingsPath: string, now: Date): string;
|
|
43
|
+
/** Build the settings-backup file path for `settingsPath` at `now`. */
|
|
44
|
+
export declare function backupPath(settingsPath: string, now: Date): string;
|
|
45
|
+
/**
|
|
46
|
+
* Return all gate-disable snapshot paths in `settingsDir`, newest-first
|
|
47
|
+
* by mtime. Missing directory returns []. Any file that isn't a valid
|
|
48
|
+
* snapshot is silently skipped — `gate enable` re-parses the candidates
|
|
49
|
+
* and surfaces format errors at that point.
|
|
50
|
+
*/
|
|
51
|
+
export declare function listSnapshots(settingsDir: string): string[];
|
|
52
|
+
export interface SnapshotReadOk {
|
|
53
|
+
ok: true;
|
|
54
|
+
snapshot: GateDisableSnapshot;
|
|
55
|
+
}
|
|
56
|
+
export interface SnapshotReadErr {
|
|
57
|
+
ok: false;
|
|
58
|
+
reason: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Parse a snapshot file. Validates structural shape only — the body's
|
|
62
|
+
* `removed[].group` payload is opaque (preserved verbatim from
|
|
63
|
+
* settings.json) and not re-validated against a hook schema.
|
|
64
|
+
*/
|
|
65
|
+
export declare function readSnapshot(filePath: string): SnapshotReadOk | SnapshotReadErr;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Snapshot file format + I/O for `harness gate disable` / `harness gate enable`.
|
|
2
|
+
//
|
|
3
|
+
// A snapshot is the on-disk record of one `gate disable` invocation. It
|
|
4
|
+
// captures (a) which hook groups were removed and where they came from,
|
|
5
|
+
// (b) the SHA-256 of `settings.json` immediately before and after the
|
|
6
|
+
// removal, and (c) the path the original was backed up to. `gate enable`
|
|
7
|
+
// reads the newest snapshot in the settings directory, refuses to restore
|
|
8
|
+
// over a settings.json the operator has edited since (`--force` overrides),
|
|
9
|
+
// and writes the removed groups back into place.
|
|
10
|
+
//
|
|
11
|
+
// The format is intentionally narrow: no compression, no transform, just
|
|
12
|
+
// the literal JSON groups as they were read out of settings.json.
|
|
13
|
+
import * as crypto from "node:crypto";
|
|
14
|
+
import * as fs from "node:fs";
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
export const SNAPSHOT_BASENAME_PREFIX = "harness.gate-disable.";
|
|
17
|
+
export const SNAPSHOT_BASENAME_SUFFIX = ".json";
|
|
18
|
+
export const SNAPSHOT_VERSION = 1;
|
|
19
|
+
export function sha256Hex(input) {
|
|
20
|
+
return crypto.createHash("sha256").update(input).digest("hex");
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Build the snapshot file path for `settingsPath` at `now`. ISO timestamps
|
|
24
|
+
* use `-` in place of `:` so the filename is portable across filesystems
|
|
25
|
+
* (Windows reject `:` in basenames).
|
|
26
|
+
*/
|
|
27
|
+
export function snapshotPath(settingsPath, now) {
|
|
28
|
+
const stamp = now.toISOString().replace(/:/g, "-");
|
|
29
|
+
return path.join(path.dirname(settingsPath), `${SNAPSHOT_BASENAME_PREFIX}${stamp}${SNAPSHOT_BASENAME_SUFFIX}`);
|
|
30
|
+
}
|
|
31
|
+
/** Build the settings-backup file path for `settingsPath` at `now`. */
|
|
32
|
+
export function backupPath(settingsPath, now) {
|
|
33
|
+
const stamp = now.toISOString().replace(/:/g, "-");
|
|
34
|
+
return `${settingsPath}.bak.${stamp}`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Return all gate-disable snapshot paths in `settingsDir`, newest-first
|
|
38
|
+
* by mtime. Missing directory returns []. Any file that isn't a valid
|
|
39
|
+
* snapshot is silently skipped — `gate enable` re-parses the candidates
|
|
40
|
+
* and surfaces format errors at that point.
|
|
41
|
+
*/
|
|
42
|
+
export function listSnapshots(settingsDir) {
|
|
43
|
+
let names;
|
|
44
|
+
try {
|
|
45
|
+
names = fs.readdirSync(settingsDir);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
const candidates = [];
|
|
51
|
+
for (const name of names) {
|
|
52
|
+
if (!name.startsWith(SNAPSHOT_BASENAME_PREFIX))
|
|
53
|
+
continue;
|
|
54
|
+
if (!name.endsWith(SNAPSHOT_BASENAME_SUFFIX))
|
|
55
|
+
continue;
|
|
56
|
+
const full = path.join(settingsDir, name);
|
|
57
|
+
try {
|
|
58
|
+
const stat = fs.statSync(full);
|
|
59
|
+
if (!stat.isFile())
|
|
60
|
+
continue;
|
|
61
|
+
candidates.push({ filePath: full, mtimeMs: stat.mtimeMs });
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
68
|
+
return candidates.map((c) => c.filePath);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Parse a snapshot file. Validates structural shape only — the body's
|
|
72
|
+
* `removed[].group` payload is opaque (preserved verbatim from
|
|
73
|
+
* settings.json) and not re-validated against a hook schema.
|
|
74
|
+
*/
|
|
75
|
+
export function readSnapshot(filePath) {
|
|
76
|
+
let raw;
|
|
77
|
+
try {
|
|
78
|
+
raw = fs.readFileSync(filePath, "utf8");
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
return { ok: false, reason: `cannot read snapshot ${filePath}: ${err.message}` };
|
|
82
|
+
}
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = JSON.parse(raw);
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
return { ok: false, reason: `snapshot ${filePath} is not valid JSON: ${err.message}` };
|
|
89
|
+
}
|
|
90
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
91
|
+
return { ok: false, reason: `snapshot ${filePath} is not a JSON object` };
|
|
92
|
+
}
|
|
93
|
+
const obj = parsed;
|
|
94
|
+
if (obj["version"] !== SNAPSHOT_VERSION) {
|
|
95
|
+
return {
|
|
96
|
+
ok: false,
|
|
97
|
+
reason: `snapshot ${filePath} has unsupported version ${JSON.stringify(obj["version"])}; expected ${SNAPSHOT_VERSION}`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
for (const field of [
|
|
101
|
+
"createdAt",
|
|
102
|
+
"settingsPath",
|
|
103
|
+
"settingsBackupPath",
|
|
104
|
+
"settingsBeforeSha256",
|
|
105
|
+
"settingsAfterSha256",
|
|
106
|
+
]) {
|
|
107
|
+
if (typeof obj[field] !== "string") {
|
|
108
|
+
return { ok: false, reason: `snapshot ${filePath} is missing string field "${field}"` };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (typeof obj["filter"] !== "object" || obj["filter"] === null) {
|
|
112
|
+
return { ok: false, reason: `snapshot ${filePath} is missing object field "filter"` };
|
|
113
|
+
}
|
|
114
|
+
if (!Array.isArray(obj["removed"])) {
|
|
115
|
+
return { ok: false, reason: `snapshot ${filePath} is missing array field "removed"` };
|
|
116
|
+
}
|
|
117
|
+
return { ok: true, snapshot: obj };
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=snapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../../src/cli/gate/snapshot.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/index.js
CHANGED
|
@@ -27,6 +27,9 @@ import { audit } from "./audit.js";
|
|
|
27
27
|
import { sessionExport } from "./session-export/index.js";
|
|
28
28
|
import { dryRun } from "./dry-run.js";
|
|
29
29
|
import { runInterceptCli } from "./policy/intercept.js";
|
|
30
|
+
import { runSessionStartPreflight } from "./session-start/index.js";
|
|
31
|
+
import { gateDisable, GateDisableError } from "./gate/disable.js";
|
|
32
|
+
import { gateEnable, GateEnableError } from "./gate/enable.js";
|
|
30
33
|
import { formatSmokeReport, runSmoke, splitCommaList, } from "./smoke/index.js";
|
|
31
34
|
import { formatReport, validate } from "./validate/index.js";
|
|
32
35
|
import { VERSION } from "../version.js";
|
|
@@ -783,7 +786,14 @@ export function buildProgram(opts = {}) {
|
|
|
783
786
|
cliOpts.approvedBy = options.approvedBy;
|
|
784
787
|
const result = await approveUnderstanding(cliOpts);
|
|
785
788
|
const lines = [];
|
|
786
|
-
|
|
789
|
+
// Annotate non-explicit session sources so the operator can spot
|
|
790
|
+
// a wrong id before it lands in the ledger.
|
|
791
|
+
const sourceNote = result.sessionSource === "pending-approval"
|
|
792
|
+
? " (resolved from .pending-approval staged by the gate hook)"
|
|
793
|
+
: result.sessionSource === "env"
|
|
794
|
+
? " (from $CLAUDE_SESSION_ID)"
|
|
795
|
+
: "";
|
|
796
|
+
lines.push(`session: ${result.sessionId}${sourceNote}`);
|
|
787
797
|
if (result.ledger.ok) {
|
|
788
798
|
lines.push(`ledger: ✓ wrote ${result.ledger.tag}`);
|
|
789
799
|
}
|
|
@@ -983,6 +993,136 @@ export function buildProgram(opts = {}) {
|
|
|
983
993
|
throw new HarnessExitError("", result.exitCode);
|
|
984
994
|
}
|
|
985
995
|
});
|
|
996
|
+
const sessionStart = program
|
|
997
|
+
.command("session-start")
|
|
998
|
+
.description("SessionStart hook entrypoints (called by Claude Code via settings.json)");
|
|
999
|
+
sessionStart
|
|
1000
|
+
.command("preflight")
|
|
1001
|
+
.description("SessionStart producer: run agent-preflight against the session cwd and, on a ready:true result, " +
|
|
1002
|
+
"record a `preflight:${REPO}` fact to the evidence ledger so the preflight-before-* policies have a " +
|
|
1003
|
+
"fresh tag to match. Reads SessionStart event JSON from stdin ({ session_id, cwd, hook_event_name }). " +
|
|
1004
|
+
"blocking:false — every failure path logs to stderr and exits 0.")
|
|
1005
|
+
.option("--config <path>", "manifest path (default: ~/.claude/harness.yaml)")
|
|
1006
|
+
.option("--project <name>", "apply per-project overrides")
|
|
1007
|
+
.option("--session <id>", "explicit session id (overrides stdin event + env). Use for manual / scripted invocations " +
|
|
1008
|
+
"where no SessionStart event JSON is piped on stdin. Without it the resolver tries " +
|
|
1009
|
+
"stdin event → $CLAUDE_SESSION_ID → newest Claude Code transcript → 'default' (which logs " +
|
|
1010
|
+
"a loud warning since the literal 'default' session never satisfies a preflight-before-* gate).")
|
|
1011
|
+
.option("--timeout <ms>", "agent-preflight subprocess timeout in milliseconds (default 25000)")
|
|
1012
|
+
.option("--ledger-timeout <ms>", "per-call ledger timeout in milliseconds")
|
|
1013
|
+
.action(async (options) => {
|
|
1014
|
+
const cliOpts = {};
|
|
1015
|
+
if (options.config)
|
|
1016
|
+
cliOpts.configPath = options.config;
|
|
1017
|
+
if (options.project)
|
|
1018
|
+
cliOpts.project = options.project;
|
|
1019
|
+
if (options.session)
|
|
1020
|
+
cliOpts.session = options.session;
|
|
1021
|
+
if (options.timeout) {
|
|
1022
|
+
const n = Number.parseInt(options.timeout, 10);
|
|
1023
|
+
if (Number.isFinite(n) && n > 0)
|
|
1024
|
+
cliOpts.preflightTimeoutMs = n;
|
|
1025
|
+
}
|
|
1026
|
+
if (options.ledgerTimeout) {
|
|
1027
|
+
const n = Number.parseInt(options.ledgerTimeout, 10);
|
|
1028
|
+
if (Number.isFinite(n) && n > 0)
|
|
1029
|
+
cliOpts.ledgerTimeoutMs = n;
|
|
1030
|
+
}
|
|
1031
|
+
await runSessionStartPreflight(cliOpts);
|
|
1032
|
+
});
|
|
1033
|
+
// `harness gate` — operator escape hatch for hard-blocking hooks.
|
|
1034
|
+
// Task 8fcddb26: the understanding-before-execution PreToolUse hook can
|
|
1035
|
+
// lock a Claude session out of every Bash call, and the recommended
|
|
1036
|
+
// recovery (`harness approve understanding`) is itself a Bash invocation
|
|
1037
|
+
// and gets caught by the same gate. `gate disable` strips the
|
|
1038
|
+
// offending hook group out of settings.json with a reversible snapshot.
|
|
1039
|
+
const gate = program
|
|
1040
|
+
.command("gate")
|
|
1041
|
+
.description("Operator escape hatch: disable/restore hook groups in ~/.claude/settings.json");
|
|
1042
|
+
gate
|
|
1043
|
+
.command("disable")
|
|
1044
|
+
.description("Remove hook groups from ~/.claude/settings.json whose `matcher` substring-matches " +
|
|
1045
|
+
"`--matcher <pattern>`. Writes a snapshot of the removed groups next to settings.json " +
|
|
1046
|
+
"(`harness.gate-disable.<ts>.json`) and backs up the original to `settings.json.bak.<ts>` " +
|
|
1047
|
+
"so `harness gate enable` can restore them. With no `--matcher` flag, lists the candidate " +
|
|
1048
|
+
"groups without writing.")
|
|
1049
|
+
.option("--matcher <pattern>", "remove groups whose matcher includes this substring")
|
|
1050
|
+
.option("--settings <path>", "override ~/.claude/settings.json")
|
|
1051
|
+
.action(async (options) => {
|
|
1052
|
+
const cliOpts = {};
|
|
1053
|
+
if (options.matcher)
|
|
1054
|
+
cliOpts.matcher = options.matcher;
|
|
1055
|
+
if (options.settings)
|
|
1056
|
+
cliOpts.settingsPath = options.settings;
|
|
1057
|
+
try {
|
|
1058
|
+
const result = gateDisable(cliOpts);
|
|
1059
|
+
if (result.mode === "list") {
|
|
1060
|
+
if (result.candidates.length === 0) {
|
|
1061
|
+
stdout(`no hook groups in ${result.settingsPath}; nothing to disable.\n`);
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
stdout(`candidate hook groups in ${result.settingsPath}:\n`);
|
|
1065
|
+
for (const c of result.candidates) {
|
|
1066
|
+
const matcherLabel = c.matcher === null ? "(no matcher)" : JSON.stringify(c.matcher);
|
|
1067
|
+
stdout(` ${c.event}[${c.index}] matcher=${matcherLabel}: ${c.description}\n`);
|
|
1068
|
+
}
|
|
1069
|
+
stdout(`\nPass --matcher <substring> to remove a group. Removal is reversible via ` +
|
|
1070
|
+
`\`harness gate enable\`.\n`);
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
stdout(`disabled ${result.removed.length} hook group(s) in ${result.settingsPath}:\n`);
|
|
1074
|
+
for (const r of result.removed) {
|
|
1075
|
+
const matcherLabel = r.group !== null && typeof r.group === "object" && !Array.isArray(r.group)
|
|
1076
|
+
? JSON.stringify(r.group["matcher"] ?? null)
|
|
1077
|
+
: "null";
|
|
1078
|
+
stdout(` ${r.event}[${r.index}] matcher=${matcherLabel}\n`);
|
|
1079
|
+
}
|
|
1080
|
+
stdout(`backup: ${result.backupPath}\n`);
|
|
1081
|
+
stdout(`snapshot: ${result.snapshotPath}\n`);
|
|
1082
|
+
stdout(`This is reversible: run \`harness gate enable\` to restore.\n`);
|
|
1083
|
+
}
|
|
1084
|
+
catch (err) {
|
|
1085
|
+
if (err instanceof GateDisableError) {
|
|
1086
|
+
throw new HarnessExitError(err.message, EX_FAIL);
|
|
1087
|
+
}
|
|
1088
|
+
throw err;
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
gate
|
|
1092
|
+
.command("enable")
|
|
1093
|
+
.description("Restore hook groups from the newest `harness.gate-disable.*.json` snapshot next to " +
|
|
1094
|
+
"settings.json. Refuses if settings.json has been edited since the snapshot was taken " +
|
|
1095
|
+
"(use `--force` to restore anyway). Idempotent: if settings.json already matches the " +
|
|
1096
|
+
"pre-disable state, exits 0 without writing.")
|
|
1097
|
+
.option("--settings <path>", "override ~/.claude/settings.json")
|
|
1098
|
+
.option("--force", "restore even when settings.json has been edited since the snapshot")
|
|
1099
|
+
.action(async (options) => {
|
|
1100
|
+
const cliOpts = {};
|
|
1101
|
+
if (options.settings)
|
|
1102
|
+
cliOpts.settingsPath = options.settings;
|
|
1103
|
+
if (options.force)
|
|
1104
|
+
cliOpts.force = true;
|
|
1105
|
+
try {
|
|
1106
|
+
const result = gateEnable(cliOpts);
|
|
1107
|
+
if (result.mode === "no-snapshots") {
|
|
1108
|
+
stdout(`no gate-disable snapshots found next to ${result.settingsPath}; nothing to restore.\n`);
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
if (result.mode === "already-restored") {
|
|
1112
|
+
stdout(`settings.json already matches the pre-disable state; no write needed. ` +
|
|
1113
|
+
`(snapshot: ${result.snapshotPath})\n`);
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
stdout(`restored ${result.restoredCount} hook group(s) into ${result.settingsPath} from ` +
|
|
1117
|
+
`${result.snapshotPath}.\n`);
|
|
1118
|
+
}
|
|
1119
|
+
catch (err) {
|
|
1120
|
+
if (err instanceof GateEnableError) {
|
|
1121
|
+
throw new HarnessExitError(err.message, EX_FAIL);
|
|
1122
|
+
}
|
|
1123
|
+
throw err;
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
986
1126
|
const policy = program.command("policy").description("Policy runtime verbs");
|
|
987
1127
|
policy
|
|
988
1128
|
.command("intercept")
|