@lannguyensi/harness 0.10.1 → 0.11.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 +78 -7
- package/README.md +1 -1
- package/dist/cli/approve/understanding.d.ts +8 -0
- package/dist/cli/approve/understanding.js +56 -7
- 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/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/index.js +40 -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 +52 -0
- package/dist/cli/session-start/index.js +195 -0
- package/dist/cli/session-start/index.js.map +1 -0
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type LedgerEntry } from "../../policies/index.js";
|
|
2
2
|
import { type LedgerClient, type PolicyDecision } from "../../runtime/index.js";
|
|
3
|
-
import type { Manifest } from "../../schema/index.js";
|
|
3
|
+
import type { Manifest, McpServer } from "../../schema/index.js";
|
|
4
4
|
import { type LoaderOptions } from "../loader.js";
|
|
5
5
|
export interface InterceptCliOptions extends LoaderOptions {
|
|
6
6
|
/** Defaults to process.stdin. */
|
|
@@ -30,5 +30,11 @@ export interface InterceptCliResult {
|
|
|
30
30
|
decisions: PolicyDecision[];
|
|
31
31
|
blocked: boolean;
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Real grounding-mcp-backed ledger client. Exported for testing: the
|
|
35
|
+
* `record` adapter's failure-surfacing path is exercised against a
|
|
36
|
+
* bogus mcpCommand in tests/runtime/intercept-cli.test.ts.
|
|
37
|
+
*/
|
|
38
|
+
export declare function realLedgerClient(server: McpServer, opts: InterceptCliOptions): LedgerClient;
|
|
33
39
|
export declare function runInterceptCli(opts?: InterceptCliOptions): Promise<InterceptCliResult>;
|
|
34
40
|
export type { LedgerEntry };
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// the PreToolUse hook. Reads the event JSON from stdin, runs the runtime
|
|
5
5
|
// interceptor, writes Claude Code's deny JSON to stdout on block.
|
|
6
6
|
import { queryLedgerByTag, } from "../../policies/index.js";
|
|
7
|
-
import { intercept, recordPolicyDecision, } from "../../runtime/index.js";
|
|
7
|
+
import { intercept, recordPolicyDecision, resolveGitContext, } from "../../runtime/index.js";
|
|
8
8
|
import { loadManifest } from "../loader.js";
|
|
9
9
|
async function readStdin(stream) {
|
|
10
10
|
return new Promise((resolve, reject) => {
|
|
@@ -55,12 +55,18 @@ function isVerboseEnabled(opts) {
|
|
|
55
55
|
// Accept anything truthy except literal disable-words (case-insensitive).
|
|
56
56
|
return !/^(0|false|no|off)$/i.test(env.trim());
|
|
57
57
|
}
|
|
58
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Real grounding-mcp-backed ledger client. Exported for testing: the
|
|
60
|
+
* `record` adapter's failure-surfacing path is exercised against a
|
|
61
|
+
* bogus mcpCommand in tests/runtime/intercept-cli.test.ts.
|
|
62
|
+
*/
|
|
63
|
+
export function realLedgerClient(server, opts) {
|
|
59
64
|
const command = Array.isArray(server.command)
|
|
60
65
|
? server.command
|
|
61
66
|
: server.command.trim().split(/\s+/);
|
|
62
67
|
const env = server.env ?? undefined;
|
|
63
68
|
const timeoutMs = opts.ledgerTimeoutMs ?? server.health?.timeout_ms ?? 5_000;
|
|
69
|
+
const stderr = opts.stderr ?? process.stderr;
|
|
64
70
|
return {
|
|
65
71
|
async query(_tag, sessionId) {
|
|
66
72
|
return queryLedgerByTag({
|
|
@@ -71,11 +77,19 @@ function realLedgerClient(server, opts) {
|
|
|
71
77
|
});
|
|
72
78
|
},
|
|
73
79
|
async record(decision, sessionId) {
|
|
74
|
-
await recordPolicyDecision(decision, sessionId, {
|
|
80
|
+
const result = await recordPolicyDecision(decision, sessionId, {
|
|
75
81
|
mcpCommand: command,
|
|
76
82
|
...(env && { mcpEnv: env }),
|
|
77
83
|
timeoutMs,
|
|
78
84
|
});
|
|
85
|
+
// A failed audit-write must not block the tool — the decision is
|
|
86
|
+
// still applied — but it must not be silent either: an unrecorded
|
|
87
|
+
// decision is invisible to `harness audit` / `explain --trace`.
|
|
88
|
+
// Goes to stderr so Claude Code's stdout deny-JSON contract holds.
|
|
89
|
+
if (!result.ok) {
|
|
90
|
+
stderr.write(`harness policy intercept: audit-write failed for ` +
|
|
91
|
+
`${decision.policyName}: ${result.reason ?? "unknown error"}\n`);
|
|
92
|
+
}
|
|
79
93
|
},
|
|
80
94
|
};
|
|
81
95
|
}
|
|
@@ -138,12 +152,20 @@ export async function runInterceptCli(opts = {}) {
|
|
|
138
152
|
// a stripped event can pick up the active session.
|
|
139
153
|
const eventSessionId = typeof event.session_id === "string" ? event.session_id : undefined;
|
|
140
154
|
const builtinSessionId = eventSessionId ?? process.env.CLAUDE_SESSION_ID ?? "";
|
|
155
|
+
// REPO / BRANCH are derived from the tool event's cwd so that per-repo
|
|
156
|
+
// and per-branch ledger tags (`preflight:${REPO}`, `preflight:${BRANCH}`)
|
|
157
|
+
// actually namespace — they were previously read from HARNESS_REPO /
|
|
158
|
+
// HARNESS_BRANCH env vars that nothing sets, collapsing every tag to the
|
|
159
|
+
// literal `preflight:`. An explicit env var still wins: it is the
|
|
160
|
+
// operator's deliberate override of the derived value.
|
|
161
|
+
const cwd = typeof event.cwd === "string" ? event.cwd : process.cwd();
|
|
162
|
+
const gitContext = resolveGitContext(cwd);
|
|
141
163
|
const builtins = {
|
|
142
164
|
SESSION_ID: builtinSessionId,
|
|
143
|
-
REPO: process.env.HARNESS_REPO ??
|
|
144
|
-
BRANCH: process.env.HARNESS_BRANCH ??
|
|
165
|
+
REPO: process.env.HARNESS_REPO ?? gitContext.repo,
|
|
166
|
+
BRANCH: process.env.HARNESS_BRANCH ?? gitContext.branch,
|
|
145
167
|
TOOL_NAME: typeof event.tool_name === "string" ? event.tool_name : "",
|
|
146
|
-
CWD:
|
|
168
|
+
CWD: cwd,
|
|
147
169
|
};
|
|
148
170
|
const result = await intercept({
|
|
149
171
|
manifest,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"intercept.js","sourceRoot":"","sources":["../../../src/cli/policy/intercept.ts"],"names":[],"mappings":"AAAA,0DAA0D;AAC1D,EAAE;AACF,2EAA2E;AAC3E,yEAAyE;AACzE,kEAAkE;AAElE,OAAO,EACL,gBAAgB,GAGjB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,SAAS,EACT,oBAAoB,
|
|
1
|
+
{"version":3,"file":"intercept.js","sourceRoot":"","sources":["../../../src/cli/policy/intercept.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type LoaderOptions } from "../loader.js";
|
|
2
|
+
/** The slice of `preflight run --json` output this producer reads. */
|
|
3
|
+
export interface PreflightJson {
|
|
4
|
+
ready?: boolean;
|
|
5
|
+
confidence?: number;
|
|
6
|
+
checks?: Array<{
|
|
7
|
+
name?: string;
|
|
8
|
+
status?: string;
|
|
9
|
+
message?: string;
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
export type RunPreflightResult = {
|
|
13
|
+
ok: true;
|
|
14
|
+
json: PreflightJson;
|
|
15
|
+
} | {
|
|
16
|
+
ok: false;
|
|
17
|
+
reason: string;
|
|
18
|
+
};
|
|
19
|
+
export interface SessionStartPreflightOptions extends LoaderOptions {
|
|
20
|
+
/** Defaults to process.stdin. */
|
|
21
|
+
stdin?: NodeJS.ReadableStream;
|
|
22
|
+
/** Defaults to process.stderr. stdout is never written (SessionStart). */
|
|
23
|
+
stderr?: NodeJS.WritableStream;
|
|
24
|
+
/** `preflight` subprocess timeout in ms. */
|
|
25
|
+
preflightTimeoutMs?: number;
|
|
26
|
+
/** Per-call ledger timeout in ms. */
|
|
27
|
+
ledgerTimeoutMs?: number;
|
|
28
|
+
/** Inject the preflight runner (tests). */
|
|
29
|
+
runPreflight?: (cwd: string, timeoutMs: number) => Promise<RunPreflightResult>;
|
|
30
|
+
/** Inject the ledger writer (tests). */
|
|
31
|
+
writeLedger?: (args: {
|
|
32
|
+
sessionId: string;
|
|
33
|
+
content: string;
|
|
34
|
+
source: string;
|
|
35
|
+
}) => Promise<{
|
|
36
|
+
ok: boolean;
|
|
37
|
+
reason?: string;
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
40
|
+
export interface SessionStartPreflightResult {
|
|
41
|
+
/** Always 0 — a SessionStart hook must never break the session loop. */
|
|
42
|
+
exitCode: number;
|
|
43
|
+
/** Whether the `preflight:` ledger fact was written. */
|
|
44
|
+
wrote: boolean;
|
|
45
|
+
/** Resolved repo name (the `${REPO}` a tag is namespaced by). */
|
|
46
|
+
repo: string;
|
|
47
|
+
/** Resolved branch (the `${BRANCH}` a tag is namespaced by; "" if detached). */
|
|
48
|
+
branch: string;
|
|
49
|
+
/** Human-readable explanation of a non-write outcome, for diagnostics. */
|
|
50
|
+
reason?: string;
|
|
51
|
+
}
|
|
52
|
+
export declare function runSessionStartPreflight(opts?: SessionStartPreflightOptions): Promise<SessionStartPreflightResult>;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// `harness session-start preflight` — SessionStart hook entrypoint.
|
|
2
|
+
//
|
|
3
|
+
// Wired by the Full template's `git-preflight` SessionStart hook. Reads
|
|
4
|
+
// the SessionStart event JSON from stdin, runs `agent-preflight`
|
|
5
|
+
// (`preflight run --json <cwd>`), and on a `ready:true` result writes a
|
|
6
|
+
// `preflight:${REPO}` fact to the evidence ledger so the
|
|
7
|
+
// `preflight-before-investigation` / `preflight-before-push` policies
|
|
8
|
+
// have a fresh tag to match within their `within` windows.
|
|
9
|
+
//
|
|
10
|
+
// SessionStart hooks are `blocking:false`: this command MUST NOT break
|
|
11
|
+
// the session loop. Every failure path — `preflight` not on PATH, a
|
|
12
|
+
// timeout, a non-`ready` result, an unreachable ledger — logs one line
|
|
13
|
+
// to stderr and exits 0. The only observable effect of a failure is
|
|
14
|
+
// that the preflight policies stay closed (which is the safe default).
|
|
15
|
+
//
|
|
16
|
+
// `ready:false` deliberately does NOT write the tag: the policy intent
|
|
17
|
+
// is "block investigative git reads until agent-preflight ran cleanly",
|
|
18
|
+
// so a failing preflight must leave the gate shut, not satisfy it.
|
|
19
|
+
import { execFile } from "node:child_process";
|
|
20
|
+
import { addLedgerFact, resolveGitContext, resolveSessionId, } from "../../runtime/index.js";
|
|
21
|
+
import { loadManifest } from "../loader.js";
|
|
22
|
+
const PREFLIGHT_BIN = "preflight";
|
|
23
|
+
const DEFAULT_PREFLIGHT_TIMEOUT_MS = 25_000;
|
|
24
|
+
const LEDGER_SOURCE = "harness-session-start-preflight";
|
|
25
|
+
async function readStdin(stream) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
let data = "";
|
|
28
|
+
stream.setEncoding("utf8");
|
|
29
|
+
stream.on("data", (chunk) => {
|
|
30
|
+
data += chunk;
|
|
31
|
+
});
|
|
32
|
+
stream.on("end", () => resolve(data));
|
|
33
|
+
stream.on("error", reject);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function findGroundingMcp(manifest) {
|
|
37
|
+
return manifest.tools.mcp.find((m) => m.name === "grounding-mcp") ?? null;
|
|
38
|
+
}
|
|
39
|
+
function mcpCommandList(server) {
|
|
40
|
+
return Array.isArray(server.command)
|
|
41
|
+
? server.command
|
|
42
|
+
: server.command.trim().split(/\s+/);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Default `preflight` runner: spawn `preflight run --json <cwd>` and
|
|
46
|
+
* parse its stdout. Resolves `{ ok: false }` (never throws) for the
|
|
47
|
+
* not-installed / timeout / unparseable cases so the caller can degrade.
|
|
48
|
+
*/
|
|
49
|
+
function spawnPreflight(cwd, timeoutMs) {
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
execFile(PREFLIGHT_BIN, ["run", "--json", cwd], { timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024, encoding: "utf8" }, (err, stdout) => {
|
|
52
|
+
// `preflight` may exit non-zero on a not-ready result while still
|
|
53
|
+
// emitting valid JSON, so a parseable stdout wins over the exit
|
|
54
|
+
// code. Only a missing binary / timeout / unparseable output is a
|
|
55
|
+
// genuine "could not run".
|
|
56
|
+
const text = (stdout ?? "").trim();
|
|
57
|
+
if (text.length > 0) {
|
|
58
|
+
try {
|
|
59
|
+
return resolve({ ok: true, json: JSON.parse(text) });
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
/* fall through to the error path */
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (err) {
|
|
66
|
+
const e = err;
|
|
67
|
+
if (e.code === "ENOENT") {
|
|
68
|
+
return resolve({
|
|
69
|
+
ok: false,
|
|
70
|
+
reason: `\`${PREFLIGHT_BIN}\` not on PATH (npm i -g @lannguyensi/agent-preflight)`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// maxBuffer overflow also sets `killed:true`; check it first so
|
|
74
|
+
// an over-budget output is not mis-reported as a timeout.
|
|
75
|
+
if (e.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") {
|
|
76
|
+
return resolve({
|
|
77
|
+
ok: false,
|
|
78
|
+
reason: `\`${PREFLIGHT_BIN} run --json\` output exceeded the read buffer`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (e.killed) {
|
|
82
|
+
return resolve({
|
|
83
|
+
ok: false,
|
|
84
|
+
reason: `\`${PREFLIGHT_BIN} run\` timed out after ${timeoutMs}ms`,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return resolve({ ok: false, reason: `\`${PREFLIGHT_BIN} run\` failed: ${e.message}` });
|
|
88
|
+
}
|
|
89
|
+
return resolve({
|
|
90
|
+
ok: false,
|
|
91
|
+
reason: `\`${PREFLIGHT_BIN} run --json\` produced no parseable JSON`,
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function describeNotReady(json) {
|
|
97
|
+
const failing = (json.checks ?? [])
|
|
98
|
+
.filter((c) => c.status === "fail" || c.status === "error")
|
|
99
|
+
.map((c) => c.name ?? "(unnamed)");
|
|
100
|
+
const confidence = typeof json.confidence === "number" ? json.confidence.toFixed(2) : "?";
|
|
101
|
+
const failSuffix = failing.length > 0 ? `; failing: ${failing.join(", ")}` : "";
|
|
102
|
+
return `preflight not ready (confidence ${confidence})${failSuffix}`;
|
|
103
|
+
}
|
|
104
|
+
export async function runSessionStartPreflight(opts = {}) {
|
|
105
|
+
const stdin = opts.stdin ?? process.stdin;
|
|
106
|
+
const stderr = opts.stderr ?? process.stderr;
|
|
107
|
+
const preflightTimeoutMs = opts.preflightTimeoutMs ?? DEFAULT_PREFLIGHT_TIMEOUT_MS;
|
|
108
|
+
const note = (msg) => {
|
|
109
|
+
stderr.write(`harness session-start preflight: ${msg}\n`);
|
|
110
|
+
};
|
|
111
|
+
const done = (wrote, repo, branch, reason) => ({
|
|
112
|
+
exitCode: 0,
|
|
113
|
+
wrote,
|
|
114
|
+
repo,
|
|
115
|
+
branch,
|
|
116
|
+
...(reason !== undefined && { reason }),
|
|
117
|
+
});
|
|
118
|
+
let event;
|
|
119
|
+
try {
|
|
120
|
+
event = JSON.parse((await readStdin(stdin)).trim() || "{}");
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
const reason = `malformed event JSON: ${err.message}`;
|
|
124
|
+
note(reason);
|
|
125
|
+
return done(false, "", "", reason);
|
|
126
|
+
}
|
|
127
|
+
const cwd = typeof event.cwd === "string" && event.cwd.length > 0 ? event.cwd : process.cwd();
|
|
128
|
+
const { repo, branch } = resolveGitContext(cwd);
|
|
129
|
+
if (repo === "") {
|
|
130
|
+
const reason = `cwd is not inside a git work tree (${cwd}); nothing to preflight`;
|
|
131
|
+
note(reason);
|
|
132
|
+
return done(false, "", "", reason);
|
|
133
|
+
}
|
|
134
|
+
const sessionId = resolveSessionId(typeof event.session_id === "string" ? event.session_id : undefined);
|
|
135
|
+
const runPreflight = opts.runPreflight ?? spawnPreflight;
|
|
136
|
+
const preflight = await runPreflight(cwd, preflightTimeoutMs);
|
|
137
|
+
if (!preflight.ok) {
|
|
138
|
+
note(preflight.reason);
|
|
139
|
+
return done(false, repo, branch, preflight.reason);
|
|
140
|
+
}
|
|
141
|
+
if (preflight.json.ready !== true) {
|
|
142
|
+
const reason = describeNotReady(preflight.json);
|
|
143
|
+
note(`${reason} — leaving the preflight tag unwritten so the gate stays closed`);
|
|
144
|
+
return done(false, repo, branch, reason);
|
|
145
|
+
}
|
|
146
|
+
const confidence = typeof preflight.json.confidence === "number"
|
|
147
|
+
? preflight.json.confidence.toFixed(2)
|
|
148
|
+
: "?";
|
|
149
|
+
// Emit BOTH per-repo and per-branch tags in one fact: the requires
|
|
150
|
+
// evaluator substring-matches, so a single entry containing
|
|
151
|
+
// `preflight:${REPO}` and `preflight:${BRANCH}` satisfies both
|
|
152
|
+
// `preflight-before-investigation` (REPO, within 1h) and
|
|
153
|
+
// `preflight-before-push` (BRANCH, within 10m). Caveat: a SessionStart
|
|
154
|
+
// producer cannot keep the 10m push window fresh through a long
|
|
155
|
+
// session — a push-time refresh is a separate concern (see task notes).
|
|
156
|
+
// On a detached HEAD `branch` is "" — only the REPO tag is written.
|
|
157
|
+
const tags = branch.length > 0 ? `preflight:${repo} preflight:${branch}` : `preflight:${repo}`;
|
|
158
|
+
const content = `${tags} ready:true confidence:${confidence}`;
|
|
159
|
+
let writeLedger = opts.writeLedger;
|
|
160
|
+
if (!writeLedger) {
|
|
161
|
+
let manifest;
|
|
162
|
+
try {
|
|
163
|
+
manifest = loadManifest(opts).manifest;
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
const reason = `manifest load failed: ${err.message}`;
|
|
167
|
+
note(reason);
|
|
168
|
+
return done(false, repo, branch, reason);
|
|
169
|
+
}
|
|
170
|
+
const server = findGroundingMcp(manifest);
|
|
171
|
+
if (!server) {
|
|
172
|
+
const reason = "grounding-mcp not declared in manifest; cannot record preflight tag";
|
|
173
|
+
note(reason);
|
|
174
|
+
return done(false, repo, branch, reason);
|
|
175
|
+
}
|
|
176
|
+
const command = mcpCommandList(server);
|
|
177
|
+
const env = server.env ?? undefined;
|
|
178
|
+
const timeoutMs = opts.ledgerTimeoutMs ?? server.health?.timeout_ms ?? 5_000;
|
|
179
|
+
writeLedger = (args) => addLedgerFact({
|
|
180
|
+
mcpCommand: command,
|
|
181
|
+
...(env && { mcpEnv: env }),
|
|
182
|
+
timeoutMs,
|
|
183
|
+
...args,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
const result = await writeLedger({ sessionId, content, source: LEDGER_SOURCE });
|
|
187
|
+
if (!result.ok) {
|
|
188
|
+
const reason = `ledger write failed: ${result.reason ?? "unknown error"}`;
|
|
189
|
+
note(reason);
|
|
190
|
+
return done(false, repo, branch, reason);
|
|
191
|
+
}
|
|
192
|
+
note(`recorded ${content} for session ${sessionId}`);
|
|
193
|
+
return done(true, repo, branch);
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/cli/session-start/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface GitRepoContext {
|
|
2
|
+
/** Basename of the work-tree root, or "" when `cwd` is not in a repo. */
|
|
3
|
+
repo: string;
|
|
4
|
+
/**
|
|
5
|
+
* Current branch name, or "" when not in a repo or HEAD is detached
|
|
6
|
+
* (a raw SHA — there is no branch to name).
|
|
7
|
+
*/
|
|
8
|
+
branch: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Resolve `{ repo, branch }` for a working directory. Returns empty
|
|
12
|
+
* strings (never throws) when `cwd` is not inside a git work tree, or
|
|
13
|
+
* when any individual lookup fails — callers treat "" as "unknown" and
|
|
14
|
+
* fall through to their own behaviour.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveGitContext(cwd: string): GitRepoContext;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Resolves the `REPO` and `BRANCH` policy builtins from a working
|
|
2
|
+
// directory.
|
|
3
|
+
//
|
|
4
|
+
// The intercept engine exposes `${REPO}` / `${BRANCH}` as `ledger_tag`
|
|
5
|
+
// template builtins, but they were only ever populated from the
|
|
6
|
+
// `HARNESS_REPO` / `HARNESS_BRANCH` env vars — which nothing sets — so
|
|
7
|
+
// every `preflight:${REPO}` tag collapsed to the literal `preflight:`.
|
|
8
|
+
// That silently degraded the founding-incident policies to one global
|
|
9
|
+
// tag: a preflight done in repo A satisfied the gate in repo B.
|
|
10
|
+
//
|
|
11
|
+
// This module derives both values from the filesystem, not a `git`
|
|
12
|
+
// subprocess: the intercept hook runs on every Bash / Edit / Write
|
|
13
|
+
// tool call, so the resolution must stay cheap. A bounded walk up the
|
|
14
|
+
// directory tree to find `.git`, plus one small `HEAD` read, is
|
|
15
|
+
// microseconds and spawns no process.
|
|
16
|
+
//
|
|
17
|
+
// It is a deliberate approximation of `git rev-parse`: it reads the
|
|
18
|
+
// work tree's basename and `.git/HEAD` directly and does NOT consult
|
|
19
|
+
// `GIT_DIR` / `GIT_WORK_TREE` / `core.worktree`. For namespacing a
|
|
20
|
+
// ledger tag, the on-disk layout is the right (and more stable)
|
|
21
|
+
// signal; those exotic overrides are out of scope.
|
|
22
|
+
import * as fs from "node:fs";
|
|
23
|
+
import * as path from "node:path";
|
|
24
|
+
const EMPTY = { repo: "", branch: "" };
|
|
25
|
+
// A `.git` *file* (linked worktree / submodule) points at the real git
|
|
26
|
+
// dir: `gitdir: <path>`.
|
|
27
|
+
const GITDIR_RE = /^gitdir:\s*(.+)$/;
|
|
28
|
+
// `.git/HEAD` on a branch: `ref: refs/heads/<branch>`. A detached HEAD
|
|
29
|
+
// holds a raw SHA instead and matches nothing here.
|
|
30
|
+
const HEAD_REF_RE = /^ref:\s*refs\/heads\/(.+)$/;
|
|
31
|
+
/**
|
|
32
|
+
* Walk up from `startDir` looking for a `.git` entry. Handles both the
|
|
33
|
+
* common `.git` directory and the `.git` *file* form used by linked
|
|
34
|
+
* worktrees and submodules. The walk is bounded so a pathologically
|
|
35
|
+
* deep cwd cannot spin.
|
|
36
|
+
*/
|
|
37
|
+
function findGitEntry(startDir) {
|
|
38
|
+
let dir = path.resolve(startDir);
|
|
39
|
+
for (let depth = 0; depth < 128; depth++) {
|
|
40
|
+
const dotGit = path.join(dir, ".git");
|
|
41
|
+
let stat;
|
|
42
|
+
try {
|
|
43
|
+
stat = fs.statSync(dotGit);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
stat = undefined;
|
|
47
|
+
}
|
|
48
|
+
if (stat?.isDirectory()) {
|
|
49
|
+
return { worktreeRoot: dir, gitDir: dotGit };
|
|
50
|
+
}
|
|
51
|
+
if (stat?.isFile()) {
|
|
52
|
+
let gitDir = "";
|
|
53
|
+
try {
|
|
54
|
+
const match = GITDIR_RE.exec(fs.readFileSync(dotGit, "utf8").trim());
|
|
55
|
+
if (match)
|
|
56
|
+
gitDir = path.resolve(dir, match[1].trim());
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
/* unreadable `.git` file — leave gitDir empty, repo still resolves */
|
|
60
|
+
}
|
|
61
|
+
return { worktreeRoot: dir, gitDir };
|
|
62
|
+
}
|
|
63
|
+
const parent = path.dirname(dir);
|
|
64
|
+
if (parent === dir)
|
|
65
|
+
return null; // hit the filesystem root
|
|
66
|
+
dir = parent;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Resolve `{ repo, branch }` for a working directory. Returns empty
|
|
72
|
+
* strings (never throws) when `cwd` is not inside a git work tree, or
|
|
73
|
+
* when any individual lookup fails — callers treat "" as "unknown" and
|
|
74
|
+
* fall through to their own behaviour.
|
|
75
|
+
*/
|
|
76
|
+
export function resolveGitContext(cwd) {
|
|
77
|
+
if (typeof cwd !== "string" || cwd.length === 0)
|
|
78
|
+
return EMPTY;
|
|
79
|
+
const entry = findGitEntry(cwd);
|
|
80
|
+
if (!entry)
|
|
81
|
+
return EMPTY;
|
|
82
|
+
const repo = path.basename(entry.worktreeRoot);
|
|
83
|
+
let branch = "";
|
|
84
|
+
if (entry.gitDir) {
|
|
85
|
+
try {
|
|
86
|
+
const head = fs.readFileSync(path.join(entry.gitDir, "HEAD"), "utf8").trim();
|
|
87
|
+
const match = HEAD_REF_RE.exec(head);
|
|
88
|
+
if (match)
|
|
89
|
+
branch = match[1].trim();
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
/* unreadable HEAD — branch stays "" */
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { repo, branch };
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=git-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-context.js","sourceRoot":"","sources":["../../src/runtime/git-context.ts"],"names":[],"mappings":"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"}
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { intercept, type ClaudeDenyJson, type InterceptOptions, type InterceptResult, type LedgerClient, type PolicyDecision, type PolicyOutcome, type ToolEvent, } from "./intercept.js";
|
|
2
2
|
export { recordPolicyDecision, payloadFromDecision, encodeLedgerContent, decodeLedgerContent, decisionSortKey, type LedgerRecordOptions, type PolicyDecisionPayload, } from "./ledger-record.js";
|
|
3
3
|
export { resolveSessionId } from "./session-id.js";
|
|
4
|
+
export { resolveGitContext, type GitRepoContext } from "./git-context.js";
|
|
4
5
|
export { addLedgerFact, type AddLedgerFactOptions, type AddLedgerFactResult, } from "./ledger-add.js";
|
package/dist/runtime/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { intercept, } from "./intercept.js";
|
|
2
2
|
export { recordPolicyDecision, payloadFromDecision, encodeLedgerContent, decodeLedgerContent, decisionSortKey, } from "./ledger-record.js";
|
|
3
3
|
export { resolveSessionId } from "./session-id.js";
|
|
4
|
+
export { resolveGitContext } from "./git-context.js";
|
|
4
5
|
export { addLedgerFact, } from "./ledger-add.js";
|
|
5
6
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,GAQV,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,oBAAoB,EACpB,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,eAAe,GAGhB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EACL,aAAa,GAGd,MAAM,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,GAQV,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,oBAAoB,EACpB,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,eAAe,GAGhB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAuB,MAAM,kBAAkB,CAAC;AAC1E,OAAO,EACL,aAAa,GAGd,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export declare const GENERATED_DIRNAME = "harness.generated";
|
|
2
|
+
export declare const PENDING_APPROVAL_BASENAME = ".pending-approval";
|
|
3
|
+
/**
|
|
4
|
+
* Resolve `harness.generated/` the way `harness apply` does: a sibling of
|
|
5
|
+
* the manifest, unless an explicit `homeDir` override is in play (tests,
|
|
6
|
+
* non-default home).
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveGeneratedDir(opts: {
|
|
9
|
+
homeDir?: string;
|
|
10
|
+
manifestPath: string;
|
|
11
|
+
}): string;
|
|
12
|
+
export declare function pendingApprovalPath(generatedDir: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Producer: stage `sessionId` for a later `harness approve`. `atomicWriteFile`
|
|
15
|
+
* creates `generatedDir` if missing, so a hand-wired hook with no prior
|
|
16
|
+
* apply still benefits. Callers treat this as best-effort — a write
|
|
17
|
+
* failure must never escalate a gate block into a thrown hook error.
|
|
18
|
+
*/
|
|
19
|
+
export declare function writePendingApproval(generatedDir: string, sessionId: string): void;
|
|
20
|
+
/**
|
|
21
|
+
* Consumer: read the staged session id, or null when the file is absent,
|
|
22
|
+
* empty, whitespace-only, or unreadable. Trims the trailing newline the
|
|
23
|
+
* producer writes plus any surrounding whitespace.
|
|
24
|
+
*/
|
|
25
|
+
export declare function readPendingApproval(generatedDir: string): string | null;
|
|
26
|
+
/**
|
|
27
|
+
* Consumer: drop the staging file once its id has been consumed, so a
|
|
28
|
+
* later arg-less `harness approve` cannot revive a stale session id.
|
|
29
|
+
* Best-effort — a missing file counts as success.
|
|
30
|
+
*/
|
|
31
|
+
export declare function clearPendingApproval(generatedDir: string): void;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Task 33abc147 — `.pending-approval` session-id staging file.
|
|
2
|
+
//
|
|
3
|
+
// The understanding-gate PreToolUse hook knows the running session's
|
|
4
|
+
// exact `session_id` (it arrives on the hook event's stdin). `harness
|
|
5
|
+
// approve`, run from the operator's `!`-shell, does NOT: $CLAUDE_SESSION_ID
|
|
6
|
+
// is unset in that shell, and guessing the id from the newest project
|
|
7
|
+
// transcript is a heuristic that breaks on subagent / parallel-session
|
|
8
|
+
// transcripts (the approve error message warns about exactly that).
|
|
9
|
+
//
|
|
10
|
+
// So the producer hands the id off instead of making the consumer guess:
|
|
11
|
+
// on every block / ask the gate hook writes the `session_id` to
|
|
12
|
+
// `<generatedDir>/.pending-approval`, and `harness approve` reads it when
|
|
13
|
+
// no `--session` flag and no `$CLAUDE_SESSION_ID` are given. Deterministic,
|
|
14
|
+
// not a guess.
|
|
15
|
+
//
|
|
16
|
+
// `harness apply` only writes its own known files into harness.generated/
|
|
17
|
+
// (it never wipes the directory), so the staging file survives applies.
|
|
18
|
+
// `harness approve` deletes it after a successful resolve so a later
|
|
19
|
+
// arg-less invocation cannot revive a stale session id.
|
|
20
|
+
import * as fs from "node:fs";
|
|
21
|
+
import * as path from "node:path";
|
|
22
|
+
import { atomicWriteFile } from "../io/atomic-write.js";
|
|
23
|
+
// Mirrors `GENERATED_DIRNAME` in cli/apply/apply.ts — also independently
|
|
24
|
+
// duplicated in cli/pack/remove.ts and cli/diff/since-apply.ts. The
|
|
25
|
+
// constant has no single home yet; consolidating the four copies is a
|
|
26
|
+
// separate cleanup.
|
|
27
|
+
export const GENERATED_DIRNAME = "harness.generated";
|
|
28
|
+
export const PENDING_APPROVAL_BASENAME = ".pending-approval";
|
|
29
|
+
/**
|
|
30
|
+
* Resolve `harness.generated/` the way `harness apply` does: a sibling of
|
|
31
|
+
* the manifest, unless an explicit `homeDir` override is in play (tests,
|
|
32
|
+
* non-default home).
|
|
33
|
+
*/
|
|
34
|
+
export function resolveGeneratedDir(opts) {
|
|
35
|
+
if (opts.homeDir !== undefined)
|
|
36
|
+
return path.join(opts.homeDir, GENERATED_DIRNAME);
|
|
37
|
+
return path.join(path.dirname(opts.manifestPath), GENERATED_DIRNAME);
|
|
38
|
+
}
|
|
39
|
+
export function pendingApprovalPath(generatedDir) {
|
|
40
|
+
return path.join(generatedDir, PENDING_APPROVAL_BASENAME);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Producer: stage `sessionId` for a later `harness approve`. `atomicWriteFile`
|
|
44
|
+
* creates `generatedDir` if missing, so a hand-wired hook with no prior
|
|
45
|
+
* apply still benefits. Callers treat this as best-effort — a write
|
|
46
|
+
* failure must never escalate a gate block into a thrown hook error.
|
|
47
|
+
*/
|
|
48
|
+
export function writePendingApproval(generatedDir, sessionId) {
|
|
49
|
+
atomicWriteFile(pendingApprovalPath(generatedDir), `${sessionId}\n`);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Consumer: read the staged session id, or null when the file is absent,
|
|
53
|
+
* empty, whitespace-only, or unreadable. Trims the trailing newline the
|
|
54
|
+
* producer writes plus any surrounding whitespace.
|
|
55
|
+
*/
|
|
56
|
+
export function readPendingApproval(generatedDir) {
|
|
57
|
+
let raw;
|
|
58
|
+
try {
|
|
59
|
+
raw = fs.readFileSync(pendingApprovalPath(generatedDir), "utf8");
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const trimmed = raw.trim();
|
|
65
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Consumer: drop the staging file once its id has been consumed, so a
|
|
69
|
+
* later arg-less `harness approve` cannot revive a stale session id.
|
|
70
|
+
* Best-effort — a missing file counts as success.
|
|
71
|
+
*/
|
|
72
|
+
export function clearPendingApproval(generatedDir) {
|
|
73
|
+
try {
|
|
74
|
+
fs.rmSync(pendingApprovalPath(generatedDir));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
/* already gone (or never written) — nothing to clean up */
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=pending-approval.js.map
|