@lannguyensi/harness 0.6.0 → 0.8.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 +255 -0
- package/README.md +189 -148
- package/dist/cli/apply/apply.d.ts +13 -0
- package/dist/cli/apply/apply.js +59 -3
- package/dist/cli/apply/apply.js.map +1 -1
- package/dist/cli/apply/generate-codex-config.d.ts +6 -0
- package/dist/cli/apply/generate-codex-config.js +149 -0
- package/dist/cli/apply/generate-codex-config.js.map +1 -0
- package/dist/cli/apply/generate-settings.d.ts +15 -1
- package/dist/cli/apply/generate-settings.js +16 -1
- package/dist/cli/apply/generate-settings.js.map +1 -1
- package/dist/cli/apply/index.d.ts +2 -1
- package/dist/cli/apply/index.js +2 -1
- package/dist/cli/apply/index.js.map +1 -1
- package/dist/cli/approve/understanding.d.ts +39 -0
- package/dist/cli/approve/understanding.js +122 -0
- package/dist/cli/approve/understanding.js.map +1 -0
- package/dist/cli/describe.d.ts +1 -1
- package/dist/cli/describe.js +2 -0
- package/dist/cli/describe.js.map +1 -1
- package/dist/cli/doctor/codex.d.ts +34 -0
- package/dist/cli/doctor/codex.js +331 -0
- package/dist/cli/doctor/codex.js.map +1 -0
- package/dist/cli/doctor/format.js +29 -1
- package/dist/cli/doctor/format.js.map +1 -1
- package/dist/cli/doctor/index.d.ts +13 -1
- package/dist/cli/doctor/index.js +49 -1
- package/dist/cli/doctor/index.js.map +1 -1
- package/dist/cli/doctor/types.d.ts +35 -1
- package/dist/cli/doctor/types.js +12 -1
- package/dist/cli/doctor/types.js.map +1 -1
- package/dist/cli/explain.d.ts +10 -1
- package/dist/cli/explain.js +44 -18
- package/dist/cli/explain.js.map +1 -1
- package/dist/cli/index.js +315 -8
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/list.d.ts +1 -1
- package/dist/cli/list.js +17 -0
- package/dist/cli/list.js.map +1 -1
- package/dist/cli/pack/add.d.ts +13 -0
- package/dist/cli/pack/add.js +71 -0
- package/dist/cli/pack/add.js.map +1 -0
- package/dist/cli/pack/hook-codex-pre-tool-use.d.ts +30 -0
- package/dist/cli/pack/hook-codex-pre-tool-use.js +149 -0
- package/dist/cli/pack/hook-codex-pre-tool-use.js.map +1 -0
- package/dist/cli/pack/hook-codex-stop.d.ts +31 -0
- package/dist/cli/pack/hook-codex-stop.js +332 -0
- package/dist/cli/pack/hook-codex-stop.js.map +1 -0
- package/dist/cli/pack/hook-codex-user-prompt-submit.d.ts +18 -0
- package/dist/cli/pack/hook-codex-user-prompt-submit.js +92 -0
- package/dist/cli/pack/hook-codex-user-prompt-submit.js.map +1 -0
- package/dist/cli/pack/hook-pre-tool-use.d.ts +32 -0
- package/dist/cli/pack/hook-pre-tool-use.js +181 -0
- package/dist/cli/pack/hook-pre-tool-use.js.map +1 -0
- package/dist/cli/pack/index.d.ts +4 -0
- package/dist/cli/pack/index.js +5 -0
- package/dist/cli/pack/index.js.map +1 -0
- package/dist/cli/pack/list.d.ts +10 -0
- package/dist/cli/pack/list.js +43 -0
- package/dist/cli/pack/list.js.map +1 -0
- package/dist/cli/pack/mutate.d.ts +14 -0
- package/dist/cli/pack/mutate.js +76 -0
- package/dist/cli/pack/mutate.js.map +1 -0
- package/dist/cli/pack/remove.d.ts +15 -0
- package/dist/cli/pack/remove.js +153 -0
- package/dist/cli/pack/remove.js.map +1 -0
- package/dist/cli/session-export/index.d.ts +46 -0
- package/dist/cli/session-export/index.js +169 -0
- package/dist/cli/session-export/index.js.map +1 -0
- package/dist/cli/session-export/redact.d.ts +22 -0
- package/dist/cli/session-export/redact.js +47 -0
- package/dist/cli/session-export/redact.js.map +1 -0
- package/dist/cli/session-export/transcript.d.ts +24 -0
- package/dist/cli/session-export/transcript.js +162 -0
- package/dist/cli/session-export/transcript.js.map +1 -0
- package/dist/cli/validate/checks.js +32 -0
- package/dist/cli/validate/checks.js.map +1 -1
- package/dist/policies/ledger-client.js +2 -1
- package/dist/policies/ledger-client.js.map +1 -1
- package/dist/policy-packs/builtin/permission-profiles.d.ts +11 -0
- package/dist/policy-packs/builtin/permission-profiles.js +74 -0
- package/dist/policy-packs/builtin/permission-profiles.js.map +1 -0
- package/dist/policy-packs/builtin/understanding-before-execution-runtime.d.ts +56 -0
- package/dist/policy-packs/builtin/understanding-before-execution-runtime.js +186 -0
- package/dist/policy-packs/builtin/understanding-before-execution-runtime.js.map +1 -0
- package/dist/policy-packs/builtin/understanding-before-execution.d.ts +15 -0
- package/dist/policy-packs/builtin/understanding-before-execution.js +254 -0
- package/dist/policy-packs/builtin/understanding-before-execution.js.map +1 -0
- package/dist/policy-packs/expand.d.ts +4 -0
- package/dist/policy-packs/expand.js +90 -0
- package/dist/policy-packs/expand.js.map +1 -0
- package/dist/policy-packs/index.d.ts +5 -0
- package/dist/policy-packs/index.js +5 -0
- package/dist/policy-packs/index.js.map +1 -0
- package/dist/policy-packs/permission-translator.d.ts +9 -0
- package/dist/policy-packs/permission-translator.js +76 -0
- package/dist/policy-packs/permission-translator.js.map +1 -0
- package/dist/policy-packs/registry.d.ts +11 -0
- package/dist/policy-packs/registry.js +20 -0
- package/dist/policy-packs/registry.js.map +1 -0
- package/dist/policy-packs/runtime.d.ts +8 -0
- package/dist/policy-packs/runtime.js +30 -0
- package/dist/policy-packs/runtime.js.map +1 -0
- package/dist/policy-packs/source.d.ts +6 -0
- package/dist/policy-packs/source.js +10 -0
- package/dist/policy-packs/source.js.map +1 -0
- package/dist/policy-packs/types.d.ts +41 -0
- package/dist/policy-packs/types.js +11 -0
- package/dist/policy-packs/types.js.map +1 -0
- package/dist/probes/mcp.js +2 -1
- package/dist/probes/mcp.js.map +1 -1
- 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/ledger-add.d.ts +16 -0
- package/dist/runtime/ledger-add.js +139 -0
- package/dist/runtime/ledger-add.js.map +1 -0
- package/dist/runtime/ledger-record.js +2 -1
- package/dist/runtime/ledger-record.js.map +1 -1
- package/dist/schema/audit.d.ts +71 -0
- package/dist/schema/audit.js +32 -0
- package/dist/schema/audit.js.map +1 -0
- package/dist/schema/index.d.ts +1893 -10
- package/dist/schema/index.js +27 -0
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/permission-profiles.d.ts +2161 -0
- package/dist/schema/permission-profiles.js +60 -0
- package/dist/schema/permission-profiles.js.map +1 -0
- package/dist/schema/policy-packs.d.ts +52 -0
- package/dist/schema/policy-packs.js +35 -0
- package/dist/schema/policy-packs.js.map +1 -0
- package/dist/schema/tools.d.ts +8 -8
- package/dist/schema/workflows.d.ts +519 -0
- package/dist/schema/workflows.js +81 -0
- package/dist/schema/workflows.js.map +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type PackAddEntry } from "./mutate.js";
|
|
2
|
+
export interface PackAddOptions {
|
|
3
|
+
configPath?: string;
|
|
4
|
+
homeDir?: string;
|
|
5
|
+
dryRun?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface PackAddResult {
|
|
8
|
+
path: string;
|
|
9
|
+
name: string;
|
|
10
|
+
diff: string;
|
|
11
|
+
applied: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function packAdd(entry: PackAddEntry, opts?: PackAddOptions): Promise<PackAddResult>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// `harness pack add <name>` — managed insert into policy_packs[].
|
|
2
|
+
//
|
|
3
|
+
// Mirrors src/cli/add/index.ts: schema-validate-before-write under a
|
|
4
|
+
// flock; surface dup-name + bad-mode errors at the point the user runs
|
|
5
|
+
// the command, not at the next `harness apply`.
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as os from "node:os";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import { parse as parseYaml } from "yaml";
|
|
10
|
+
import { atomicWriteFile } from "../../io/atomic-write.js";
|
|
11
|
+
import { withFileLock } from "../../io/lock.js";
|
|
12
|
+
import { unifiedDiff } from "../../io/patch.js";
|
|
13
|
+
import { formatValidationErrors, validateBeforeWrite, } from "../../io/validate-before-write.js";
|
|
14
|
+
import { isBuiltinPackName } from "../../policy-packs/index.js";
|
|
15
|
+
import { parsePackSource } from "../../policy-packs/source.js";
|
|
16
|
+
import { EX_FAIL, EX_NOINPUT, HarnessExitError } from "../exit-codes.js";
|
|
17
|
+
import { applyPackAdd } from "./mutate.js";
|
|
18
|
+
const DEFAULT_BASENAME = "harness.yaml";
|
|
19
|
+
const LOCK_BASENAME = ".harness.lock";
|
|
20
|
+
function resolveTargetPath(opts) {
|
|
21
|
+
if (opts.configPath)
|
|
22
|
+
return path.resolve(opts.configPath);
|
|
23
|
+
return path.join(opts.homeDir ?? path.join(os.homedir(), ".claude"), DEFAULT_BASENAME);
|
|
24
|
+
}
|
|
25
|
+
export async function packAdd(entry, opts = {}) {
|
|
26
|
+
const target = resolveTargetPath(opts);
|
|
27
|
+
if (!fs.existsSync(target)) {
|
|
28
|
+
throw new HarnessExitError(`harness manifest not found at ${target}; run \`harness init\` first`, EX_NOINPUT);
|
|
29
|
+
}
|
|
30
|
+
// Pre-flight: catch obviously-broken inputs before we touch the file lock.
|
|
31
|
+
// The schema validate-before-write below catches the same conditions, but
|
|
32
|
+
// surfacing a typed message here gives the user a one-liner hint instead
|
|
33
|
+
// of a zod issue tree for the common cases.
|
|
34
|
+
const sourceParsed = parsePackSource(entry.source ?? "builtin");
|
|
35
|
+
if (sourceParsed.kind === "unknown") {
|
|
36
|
+
throw new HarnessExitError(`policy_packs source ${JSON.stringify(entry.source)} is not recognised in v1 (only "builtin" resolves). See docs/policy-packs/.`, EX_FAIL);
|
|
37
|
+
}
|
|
38
|
+
if (sourceParsed.kind === "builtin" && !isBuiltinPackName(entry.name)) {
|
|
39
|
+
throw new HarnessExitError(`policy_packs name ${JSON.stringify(entry.name)} is not a known builtin pack. See docs/policy-packs/ for supported names.`, EX_FAIL);
|
|
40
|
+
}
|
|
41
|
+
const original = fs.readFileSync(target, "utf8");
|
|
42
|
+
const proposed = applyPackAdd(original, entry);
|
|
43
|
+
const diff = unifiedDiff({
|
|
44
|
+
fileName: path.basename(target),
|
|
45
|
+
oldText: original,
|
|
46
|
+
newText: proposed,
|
|
47
|
+
oldHeader: "current",
|
|
48
|
+
newHeader: "proposed",
|
|
49
|
+
});
|
|
50
|
+
// Schema gate: this is what catches duplicate-name across packs (the
|
|
51
|
+
// PolicyPacksSchema superRefine fires here).
|
|
52
|
+
const schemaResult = validateBeforeWrite(parseYaml(proposed));
|
|
53
|
+
if (!schemaResult.ok) {
|
|
54
|
+
throw new HarnessExitError(`proposed manifest fails schema validation:\n${formatValidationErrors(schemaResult.errors)}`, EX_FAIL);
|
|
55
|
+
}
|
|
56
|
+
if (opts.dryRun) {
|
|
57
|
+
return { path: target, name: entry.name, diff, applied: false };
|
|
58
|
+
}
|
|
59
|
+
const lockPath = path.join(path.dirname(target), LOCK_BASENAME);
|
|
60
|
+
await withFileLock(lockPath, () => {
|
|
61
|
+
const current = fs.readFileSync(target, "utf8");
|
|
62
|
+
const next = applyPackAdd(current, entry);
|
|
63
|
+
const recheck = validateBeforeWrite(parseYaml(next));
|
|
64
|
+
if (!recheck.ok) {
|
|
65
|
+
throw new HarnessExitError(`proposed manifest fails schema validation after lock acquisition:\n${formatValidationErrors(recheck.errors)}`, EX_FAIL);
|
|
66
|
+
}
|
|
67
|
+
atomicWriteFile(target, next);
|
|
68
|
+
});
|
|
69
|
+
return { path: target, name: entry.name, diff, applied: true };
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=add.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"add.js","sourceRoot":"","sources":["../../../src/cli/pack/add.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type LedgerEntry } from "../../policies/index.js";
|
|
2
|
+
import { type ApprovalCheckResult } from "../../policy-packs/builtin/understanding-before-execution-runtime.js";
|
|
3
|
+
import type { Manifest } from "../../schema/index.js";
|
|
4
|
+
import { type LoaderOptions } from "../loader.js";
|
|
5
|
+
export interface PackHookCodexPreToolUseOptions extends LoaderOptions {
|
|
6
|
+
/** Pack name to evaluate. Defaults to understanding-before-execution. */
|
|
7
|
+
pack?: string;
|
|
8
|
+
/** Override report directory (test injection). */
|
|
9
|
+
reportsDir?: string;
|
|
10
|
+
/** Override timeout per ledger call. */
|
|
11
|
+
ledgerTimeoutMs?: number;
|
|
12
|
+
/** Defaults to process.stdin. */
|
|
13
|
+
stdin?: NodeJS.ReadableStream;
|
|
14
|
+
/** Defaults to process.stderr. */
|
|
15
|
+
stderr?: NodeJS.WritableStream;
|
|
16
|
+
/** Inject an alternate manifest (test). */
|
|
17
|
+
manifest?: Manifest;
|
|
18
|
+
/** Inject a fake ledger query (test). */
|
|
19
|
+
ledgerQuery?: (sessionId: string) => Promise<LedgerEntry[] | {
|
|
20
|
+
degraded: string;
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
23
|
+
export interface PackHookCodexPreToolUseResult {
|
|
24
|
+
exitCode: number;
|
|
25
|
+
blocked: boolean;
|
|
26
|
+
approvalCheck: ApprovalCheckResult;
|
|
27
|
+
/** Diagnostic line emitted to stderr (always; even on allow). */
|
|
28
|
+
diagnostic: string;
|
|
29
|
+
}
|
|
30
|
+
export declare function runPackHookCodexPreToolUseCli(opts?: PackHookCodexPreToolUseOptions): Promise<PackHookCodexPreToolUseResult>;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Phase 6 #6 — `harness pack hook codex-pre-tool-use` runtime verb.
|
|
2
|
+
//
|
|
3
|
+
// Codex variant of the Claude Code blocker (`hook-pre-tool-use.ts`).
|
|
4
|
+
// Same approval logic (ledger + persisted-report, either approves), but
|
|
5
|
+
// a different I/O contract:
|
|
6
|
+
//
|
|
7
|
+
// stdin : JSON envelope shaped as `{ session_id, tool_name,
|
|
8
|
+
// raw_input, event }` — harness's published wire format that
|
|
9
|
+
// the Codex CLI integration is wrapped to emit. The
|
|
10
|
+
// integration is documented in
|
|
11
|
+
// `docs/policy-packs/understanding-before-execution.md`
|
|
12
|
+
// "Adapter notes / Codex".
|
|
13
|
+
// stdout : block reason on stderr; allow path is silent on stdout.
|
|
14
|
+
// exit : 0 on allow, 2 on block. Codex's blocking convention is
|
|
15
|
+
// non-zero exit (the JSON-decision shape Claude Code reads
|
|
16
|
+
// is not part of Codex's hook contract today).
|
|
17
|
+
//
|
|
18
|
+
// Failure mode mirrors the Claude blocker: any error in load / parse /
|
|
19
|
+
// ledger / report scan resolves to ALLOW (exit 0, silent diagnostic on
|
|
20
|
+
// stderr). The package's optional standalone blocker remains a safety
|
|
21
|
+
// net for solo users; the harness blocker is strictly more powerful.
|
|
22
|
+
import { queryLedgerByTag } from "../../policies/index.js";
|
|
23
|
+
import { checkPersistedReport, defaultReportsDir, matchLedgerEntries, } from "../../policy-packs/builtin/understanding-before-execution-runtime.js";
|
|
24
|
+
import { loadManifest } from "../loader.js";
|
|
25
|
+
const PACK_NAME = "understanding-before-execution";
|
|
26
|
+
const EXIT_BLOCK = 2;
|
|
27
|
+
async function readStdin(stream) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
let data = "";
|
|
30
|
+
stream.setEncoding("utf8");
|
|
31
|
+
stream.on("data", (chunk) => {
|
|
32
|
+
data += chunk;
|
|
33
|
+
});
|
|
34
|
+
stream.on("end", () => resolve(data));
|
|
35
|
+
stream.on("error", (err) => reject(err));
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function pickString(...candidates) {
|
|
39
|
+
for (const c of candidates) {
|
|
40
|
+
if (typeof c === "string" && c.length > 0)
|
|
41
|
+
return c;
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
function findGroundingMcp(manifest) {
|
|
46
|
+
return manifest.tools.mcp.find((m) => m.name === "grounding-mcp") ?? null;
|
|
47
|
+
}
|
|
48
|
+
async function checkLedger(manifest, sessionId, opts) {
|
|
49
|
+
if (opts.ledgerQuery) {
|
|
50
|
+
const result = await opts.ledgerQuery(sessionId);
|
|
51
|
+
if ("degraded" in result) {
|
|
52
|
+
return { matched: false, detail: `ledger degraded (${result.degraded})` };
|
|
53
|
+
}
|
|
54
|
+
return matchLedgerEntries(result, sessionId);
|
|
55
|
+
}
|
|
56
|
+
const server = findGroundingMcp(manifest);
|
|
57
|
+
if (!server) {
|
|
58
|
+
return { matched: false, detail: "grounding-mcp not declared in manifest" };
|
|
59
|
+
}
|
|
60
|
+
const command = Array.isArray(server.command)
|
|
61
|
+
? server.command
|
|
62
|
+
: server.command.trim().split(/\s+/);
|
|
63
|
+
const env = server.env ?? undefined;
|
|
64
|
+
const timeoutMs = opts.ledgerTimeoutMs ?? server.health?.timeout_ms ?? 5_000;
|
|
65
|
+
const result = await queryLedgerByTag({
|
|
66
|
+
mcpCommand: command,
|
|
67
|
+
...(env && { mcpEnv: env }),
|
|
68
|
+
sessionId,
|
|
69
|
+
timeoutMs,
|
|
70
|
+
});
|
|
71
|
+
if (result.kind === "degraded") {
|
|
72
|
+
return { matched: false, detail: `ledger degraded (${result.reason})` };
|
|
73
|
+
}
|
|
74
|
+
return matchLedgerEntries(result.entries, sessionId);
|
|
75
|
+
}
|
|
76
|
+
function allowResult(detail, source, stderr) {
|
|
77
|
+
const diagnostic = `harness pack hook codex: ${detail}, allowing.`;
|
|
78
|
+
stderr.write(`${diagnostic}\n`);
|
|
79
|
+
return {
|
|
80
|
+
exitCode: 0,
|
|
81
|
+
blocked: false,
|
|
82
|
+
approvalCheck: { approved: true, source, detail },
|
|
83
|
+
diagnostic,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export async function runPackHookCodexPreToolUseCli(opts = {}) {
|
|
87
|
+
const stdin = opts.stdin ?? process.stdin;
|
|
88
|
+
const stderr = opts.stderr ?? process.stderr;
|
|
89
|
+
const packName = opts.pack ?? PACK_NAME;
|
|
90
|
+
// Read stdin defensively. Bad JSON falls through to allow (matches
|
|
91
|
+
// Claude blocker's failure mode).
|
|
92
|
+
const raw = await readStdin(stdin);
|
|
93
|
+
let event = {};
|
|
94
|
+
try {
|
|
95
|
+
event = JSON.parse(raw.trim() || "{}");
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
/* allow on malformed input */
|
|
99
|
+
}
|
|
100
|
+
const sessionId = pickString(event.session_id) ??
|
|
101
|
+
process.env["CODEX_SESSION_ID"] ??
|
|
102
|
+
process.env["CLAUDE_SESSION_ID"] ??
|
|
103
|
+
"";
|
|
104
|
+
const toolName = pickString(event.tool_name, event.tool) ?? "(unknown)";
|
|
105
|
+
// Load manifest (or use injection). Bail to allow on any failure so a
|
|
106
|
+
// missing harness install never bricks the session.
|
|
107
|
+
let manifest;
|
|
108
|
+
try {
|
|
109
|
+
manifest = opts.manifest ?? loadManifest(opts).manifest;
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
return allowResult(`manifest load failed (${err.message})`, "none", stderr);
|
|
113
|
+
}
|
|
114
|
+
// Confirm the pack is enabled.
|
|
115
|
+
const declared = manifest.policy_packs.find((p) => p.name === packName);
|
|
116
|
+
if (!declared) {
|
|
117
|
+
return allowResult(`pack "${packName}" not declared in manifest`, "none", stderr);
|
|
118
|
+
}
|
|
119
|
+
if (!declared.enabled) {
|
|
120
|
+
return allowResult(`pack "${packName}" is enabled:false`, "none", stderr);
|
|
121
|
+
}
|
|
122
|
+
if (sessionId === "") {
|
|
123
|
+
return allowResult("no session_id resolvable from input or $CODEX_SESSION_ID/$CLAUDE_SESSION_ID", "none", stderr);
|
|
124
|
+
}
|
|
125
|
+
// Source 1: ledger.
|
|
126
|
+
const ledger = await checkLedger(manifest, sessionId, opts);
|
|
127
|
+
if (ledger.matched) {
|
|
128
|
+
return allowResult(ledger.detail, "ledger", stderr);
|
|
129
|
+
}
|
|
130
|
+
// Source 2: persisted report.
|
|
131
|
+
const reportsDir = opts.reportsDir ?? defaultReportsDir();
|
|
132
|
+
const report = checkPersistedReport(reportsDir, sessionId);
|
|
133
|
+
if (report.approved) {
|
|
134
|
+
return allowResult(report.detail, "persisted-report", stderr);
|
|
135
|
+
}
|
|
136
|
+
// Neither source approved. Codex blocks via non-zero exit + stderr
|
|
137
|
+
// reason; there is no JSON-decision wire to write to stdout.
|
|
138
|
+
const reason = `${ledger.detail}; ${report.detail}`;
|
|
139
|
+
const diagnostic = `harness pack hook codex: BLOCK: ${reason}. Tool: ${toolName}. ` +
|
|
140
|
+
"Run `harness approve understanding` once you have produced and confirmed an Understanding Report.";
|
|
141
|
+
stderr.write(`${diagnostic}\n`);
|
|
142
|
+
return {
|
|
143
|
+
exitCode: EXIT_BLOCK,
|
|
144
|
+
blocked: true,
|
|
145
|
+
approvalCheck: { approved: false, source: "none", detail: reason },
|
|
146
|
+
diagnostic,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=hook-codex-pre-tool-use.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hook-codex-pre-tool-use.js","sourceRoot":"","sources":["../../../src/cli/pack/hook-codex-pre-tool-use.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Manifest } from "../../schema/index.js";
|
|
2
|
+
import { type LoaderOptions } from "../loader.js";
|
|
3
|
+
export interface PackHookCodexStopOptions extends LoaderOptions {
|
|
4
|
+
pack?: string;
|
|
5
|
+
reportsDir?: string;
|
|
6
|
+
stdin?: NodeJS.ReadableStream;
|
|
7
|
+
stdout?: NodeJS.WritableStream;
|
|
8
|
+
stderr?: NodeJS.WritableStream;
|
|
9
|
+
manifest?: Manifest;
|
|
10
|
+
/** Test-injectable clock; defaults to new Date(). */
|
|
11
|
+
now?: Date;
|
|
12
|
+
}
|
|
13
|
+
export interface ParsedReport {
|
|
14
|
+
interpretation: string;
|
|
15
|
+
assumptions: string[];
|
|
16
|
+
openQuestions: string[];
|
|
17
|
+
outOfScope: string[];
|
|
18
|
+
risks: string[];
|
|
19
|
+
verificationPlan: string;
|
|
20
|
+
}
|
|
21
|
+
export interface PackHookCodexStopResult {
|
|
22
|
+
exitCode: number;
|
|
23
|
+
/** Path of the persisted report; null when no file was written. */
|
|
24
|
+
reportPath: string | null;
|
|
25
|
+
/** True when at least one Understanding Report field was extracted. */
|
|
26
|
+
parsed: boolean;
|
|
27
|
+
diagnostic: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function parseUnderstandingReport(text: string): ParsedReport;
|
|
30
|
+
export declare function reportHasContent(r: ParsedReport): boolean;
|
|
31
|
+
export declare function runPackHookCodexStopCli(opts?: PackHookCodexStopOptions): Promise<PackHookCodexStopResult>;
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
// Phase 6 #6 follow-up — `harness pack hook codex-stop` runtime verb.
|
|
2
|
+
//
|
|
3
|
+
// Codex Stop-equivalent for the `understanding-before-execution` pack.
|
|
4
|
+
// Mirrors the `@lannguyensi/understanding-gate` claude-code stop bin's
|
|
5
|
+
// contract, scoped to v1: read the agent's stop event on stdin,
|
|
6
|
+
// extract the last assistant message, parse Understanding Report
|
|
7
|
+
// fields, persist as JSON under `.understanding-gate/reports/`. The
|
|
8
|
+
// resulting file lands with `approvalStatus: "pending"` so a later
|
|
9
|
+
// `harness approve understanding` flips it to approved.
|
|
10
|
+
//
|
|
11
|
+
// Wire format on stdin (envelope harness publishes; Codex CLI
|
|
12
|
+
// integration wraps its native event into this shape):
|
|
13
|
+
//
|
|
14
|
+
// { session_id?: string, last_assistant_message?: string,
|
|
15
|
+
// messages?: Array<{ role: string, content: string }> }
|
|
16
|
+
//
|
|
17
|
+
// Either `last_assistant_message` is provided directly, OR the last
|
|
18
|
+
// entry in `messages[]` with role === "assistant" is used.
|
|
19
|
+
//
|
|
20
|
+
// Failure mode: any error (malformed input, missing session id,
|
|
21
|
+
// unwritable reports dir, parser yielded zero recognisable fields)
|
|
22
|
+
// falls through to exit 0 + a stderr diagnostic. The Stop event must
|
|
23
|
+
// never block the agent's response path; capture is best-effort.
|
|
24
|
+
//
|
|
25
|
+
// Out of scope for v1 (filed separately when needed):
|
|
26
|
+
// - Backfill of older transcripts.
|
|
27
|
+
// - A schema-validating parser; the v1 parser is heading-driven and
|
|
28
|
+
// intentionally lenient.
|
|
29
|
+
import { createHash } from "node:crypto";
|
|
30
|
+
import * as fs from "node:fs";
|
|
31
|
+
import * as path from "node:path";
|
|
32
|
+
import { atomicWriteFile } from "../../io/atomic-write.js";
|
|
33
|
+
import { defaultReportsDir } from "../../policy-packs/builtin/understanding-before-execution-runtime.js";
|
|
34
|
+
import { loadManifest } from "../loader.js";
|
|
35
|
+
const PACK_NAME = "understanding-before-execution";
|
|
36
|
+
const RUNTIME_TAG = "codex";
|
|
37
|
+
async function readStdin(stream) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
let data = "";
|
|
40
|
+
stream.setEncoding("utf8");
|
|
41
|
+
stream.on("data", (chunk) => {
|
|
42
|
+
data += chunk;
|
|
43
|
+
});
|
|
44
|
+
stream.on("end", () => resolve(data));
|
|
45
|
+
stream.on("error", (err) => reject(err));
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function pickString(...candidates) {
|
|
49
|
+
for (const c of candidates) {
|
|
50
|
+
if (typeof c === "string" && c.length > 0)
|
|
51
|
+
return c;
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
function extractLastAssistantText(env) {
|
|
56
|
+
const direct = pickString(env.last_assistant_message);
|
|
57
|
+
if (direct !== undefined)
|
|
58
|
+
return direct;
|
|
59
|
+
if (!Array.isArray(env.messages))
|
|
60
|
+
return null;
|
|
61
|
+
for (let i = env.messages.length - 1; i >= 0; i--) {
|
|
62
|
+
const row = env.messages[i];
|
|
63
|
+
if (row && typeof row === "object" && row.role === "assistant") {
|
|
64
|
+
const content = pickString(row.content);
|
|
65
|
+
if (content !== undefined)
|
|
66
|
+
return content;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
// Field names recognised by the parser. Lower-case canonical forms;
|
|
72
|
+
// the matcher is case-insensitive and accepts CamelCase / snake_case
|
|
73
|
+
// / spaces.
|
|
74
|
+
const FIELDS = [
|
|
75
|
+
"interpretation",
|
|
76
|
+
"assumptions",
|
|
77
|
+
"openquestions",
|
|
78
|
+
"outofscope",
|
|
79
|
+
"risks",
|
|
80
|
+
"verificationplan",
|
|
81
|
+
];
|
|
82
|
+
const SCALAR_FIELDS = new Set(["interpretation", "verificationplan"]);
|
|
83
|
+
function normalizeFieldKey(raw) {
|
|
84
|
+
// Strip trailing punctuation that operators commonly leave inside
|
|
85
|
+
// bold labels (e.g. `**Interpretation:**` → field name is just
|
|
86
|
+
// "Interpretation"). Then compact whitespace/separators.
|
|
87
|
+
const compact = raw
|
|
88
|
+
.trim()
|
|
89
|
+
.replace(/[:.\s]+$/g, "")
|
|
90
|
+
.toLowerCase()
|
|
91
|
+
.replace(/[\s_-]+/g, "");
|
|
92
|
+
// Accept synonyms / British/American variants.
|
|
93
|
+
switch (compact) {
|
|
94
|
+
case "openquestions":
|
|
95
|
+
case "questions":
|
|
96
|
+
return "openquestions";
|
|
97
|
+
case "outofscope":
|
|
98
|
+
case "scopeexclusions":
|
|
99
|
+
case "exclusions":
|
|
100
|
+
return "outofscope";
|
|
101
|
+
case "verificationplan":
|
|
102
|
+
case "validation":
|
|
103
|
+
case "verification":
|
|
104
|
+
return "verificationplan";
|
|
105
|
+
default:
|
|
106
|
+
if (FIELDS.includes(compact)) {
|
|
107
|
+
return compact;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// A heading/label line opens a section. Recognised:
|
|
113
|
+
// `## Interpretation` markdown heading
|
|
114
|
+
// `**Interpretation:**` bold label
|
|
115
|
+
// `Interpretation:` (line) plain colon-prefixed label
|
|
116
|
+
// Returns the FieldKey on hit, plus any inline content trailing on
|
|
117
|
+
// the same line (e.g. `Interpretation: short paragraph`).
|
|
118
|
+
function matchSectionHeader(line) {
|
|
119
|
+
const trimmed = line.trim();
|
|
120
|
+
if (trimmed === "")
|
|
121
|
+
return null;
|
|
122
|
+
// Markdown heading: `## Interpretation` or `### Open Questions`.
|
|
123
|
+
const heading = trimmed.match(/^#{1,6}\s+(.+?)\s*$/);
|
|
124
|
+
if (heading) {
|
|
125
|
+
const field = normalizeFieldKey(heading[1] ?? "");
|
|
126
|
+
if (field)
|
|
127
|
+
return { field, inlineRest: "" };
|
|
128
|
+
}
|
|
129
|
+
// Bold label: `**Interpretation:**` or `**Interpretation**:`.
|
|
130
|
+
const bold = trimmed.match(/^\*\*([^*]+?)\*\*\s*:?\s*(.*)$/);
|
|
131
|
+
if (bold) {
|
|
132
|
+
const field = normalizeFieldKey(bold[1] ?? "");
|
|
133
|
+
if (field)
|
|
134
|
+
return { field, inlineRest: bold[2]?.trim() ?? "" };
|
|
135
|
+
}
|
|
136
|
+
// Plain label `Interpretation: rest of line` (avoid matching arbitrary
|
|
137
|
+
// sentence colons by requiring the prefix to be a known field name).
|
|
138
|
+
const plain = trimmed.match(/^([A-Za-z][A-Za-z _-]*)\s*:\s*(.*)$/);
|
|
139
|
+
if (plain) {
|
|
140
|
+
const field = normalizeFieldKey(plain[1] ?? "");
|
|
141
|
+
if (field)
|
|
142
|
+
return { field, inlineRest: plain[2]?.trim() ?? "" };
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
function extractBulletText(line) {
|
|
147
|
+
const trimmed = line.trim();
|
|
148
|
+
if (trimmed === "")
|
|
149
|
+
return null;
|
|
150
|
+
const m = trimmed.match(/^[-*•]\s+(.+)$/);
|
|
151
|
+
return m ? m[1].trim() : null;
|
|
152
|
+
}
|
|
153
|
+
export function parseUnderstandingReport(text) {
|
|
154
|
+
const out = {
|
|
155
|
+
interpretation: "",
|
|
156
|
+
assumptions: [],
|
|
157
|
+
openQuestions: [],
|
|
158
|
+
outOfScope: [],
|
|
159
|
+
risks: [],
|
|
160
|
+
verificationPlan: "",
|
|
161
|
+
};
|
|
162
|
+
if (typeof text !== "string" || text.trim() === "")
|
|
163
|
+
return out;
|
|
164
|
+
const lines = text.split(/\r?\n/);
|
|
165
|
+
let active = null;
|
|
166
|
+
let scalarBuffer = [];
|
|
167
|
+
const flushScalar = () => {
|
|
168
|
+
if (active && SCALAR_FIELDS.has(active) && scalarBuffer.length > 0) {
|
|
169
|
+
const joined = scalarBuffer.join(" ").replace(/\s+/g, " ").trim();
|
|
170
|
+
writeScalar(out, active, joined);
|
|
171
|
+
}
|
|
172
|
+
scalarBuffer = [];
|
|
173
|
+
};
|
|
174
|
+
for (const line of lines) {
|
|
175
|
+
const header = matchSectionHeader(line);
|
|
176
|
+
if (header) {
|
|
177
|
+
flushScalar();
|
|
178
|
+
active = header.field;
|
|
179
|
+
if (header.inlineRest !== "") {
|
|
180
|
+
if (SCALAR_FIELDS.has(active)) {
|
|
181
|
+
scalarBuffer.push(header.inlineRest);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
// Inline content on a list-field header counts as the first item.
|
|
185
|
+
appendList(out, active, header.inlineRest);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (!active)
|
|
191
|
+
continue;
|
|
192
|
+
if (SCALAR_FIELDS.has(active)) {
|
|
193
|
+
const trimmed = line.trim();
|
|
194
|
+
if (trimmed === "") {
|
|
195
|
+
// Blank line within a scalar paragraph terminates accumulation.
|
|
196
|
+
if (scalarBuffer.length > 0)
|
|
197
|
+
flushScalar();
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
scalarBuffer.push(trimmed);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
const bullet = extractBulletText(line);
|
|
204
|
+
if (bullet !== null) {
|
|
205
|
+
appendList(out, active, bullet);
|
|
206
|
+
}
|
|
207
|
+
// Non-bullet lines under a list field are dropped (the upstream
|
|
208
|
+
// package's contract is "use bullets"; lenient drop avoids
|
|
209
|
+
// accidentally appending the next paragraph).
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
flushScalar();
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
function writeScalar(out, field, value) {
|
|
216
|
+
if (field === "interpretation")
|
|
217
|
+
out.interpretation = value;
|
|
218
|
+
else if (field === "verificationplan")
|
|
219
|
+
out.verificationPlan = value;
|
|
220
|
+
}
|
|
221
|
+
function appendList(out, field, value) {
|
|
222
|
+
if (field === "assumptions")
|
|
223
|
+
out.assumptions.push(value);
|
|
224
|
+
else if (field === "openquestions")
|
|
225
|
+
out.openQuestions.push(value);
|
|
226
|
+
else if (field === "outofscope")
|
|
227
|
+
out.outOfScope.push(value);
|
|
228
|
+
else if (field === "risks")
|
|
229
|
+
out.risks.push(value);
|
|
230
|
+
}
|
|
231
|
+
export function reportHasContent(r) {
|
|
232
|
+
return (r.interpretation !== "" ||
|
|
233
|
+
r.verificationPlan !== "" ||
|
|
234
|
+
r.assumptions.length > 0 ||
|
|
235
|
+
r.openQuestions.length > 0 ||
|
|
236
|
+
r.outOfScope.length > 0 ||
|
|
237
|
+
r.risks.length > 0);
|
|
238
|
+
}
|
|
239
|
+
function sessionShortHash(sessionId) {
|
|
240
|
+
return createHash("sha256").update(sessionId).digest("hex").slice(0, 8);
|
|
241
|
+
}
|
|
242
|
+
function isoFilenameStamp(now) {
|
|
243
|
+
// 2026-05-10T17:25:30.123Z -> 2026-05-10T17-25-30
|
|
244
|
+
const iso = now.toISOString();
|
|
245
|
+
const head = iso.slice(0, iso.indexOf(".") === -1 ? iso.length - 1 : iso.indexOf("."));
|
|
246
|
+
return head.replace(/:/g, "-");
|
|
247
|
+
}
|
|
248
|
+
function buildReportFilename(sessionId, now) {
|
|
249
|
+
const stamp = isoFilenameStamp(now);
|
|
250
|
+
const hash = sessionShortHash(sessionId);
|
|
251
|
+
return `${stamp}-${RUNTIME_TAG}-${hash}.json`;
|
|
252
|
+
}
|
|
253
|
+
function writeReportFile(reportsDir, filename, body) {
|
|
254
|
+
fs.mkdirSync(reportsDir, { recursive: true });
|
|
255
|
+
const target = path.join(reportsDir, filename);
|
|
256
|
+
atomicWriteFile(target, `${JSON.stringify(body, null, 2)}\n`);
|
|
257
|
+
return target;
|
|
258
|
+
}
|
|
259
|
+
function allowResult(diagnostic, stderr) {
|
|
260
|
+
stderr.write(`${diagnostic}\n`);
|
|
261
|
+
return { exitCode: 0, reportPath: null, parsed: false, diagnostic };
|
|
262
|
+
}
|
|
263
|
+
export async function runPackHookCodexStopCli(opts = {}) {
|
|
264
|
+
const stdin = opts.stdin ?? process.stdin;
|
|
265
|
+
const stderr = opts.stderr ?? process.stderr;
|
|
266
|
+
const packName = opts.pack ?? PACK_NAME;
|
|
267
|
+
const now = opts.now ?? new Date();
|
|
268
|
+
// Fail-open on stdin read errors (e.g. EPIPE on a closed pipe). The
|
|
269
|
+
// Stop hook must never crash the agent's response path; a missed
|
|
270
|
+
// capture is acceptable, an uncaught reject is not.
|
|
271
|
+
let raw;
|
|
272
|
+
try {
|
|
273
|
+
raw = await readStdin(stdin);
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
return allowResult(`harness pack hook codex-stop: stdin read failed (${err.message}), skipping capture.`, stderr);
|
|
277
|
+
}
|
|
278
|
+
let envelope = {};
|
|
279
|
+
try {
|
|
280
|
+
envelope = JSON.parse(raw.trim() || "{}");
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return allowResult("harness pack hook codex-stop: malformed JSON on stdin, skipping capture.", stderr);
|
|
284
|
+
}
|
|
285
|
+
let manifest;
|
|
286
|
+
try {
|
|
287
|
+
manifest = opts.manifest ?? loadManifest(opts).manifest;
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
return allowResult(`harness pack hook codex-stop: manifest load failed (${err.message}), skipping capture.`, stderr);
|
|
291
|
+
}
|
|
292
|
+
const declared = manifest.policy_packs.find((p) => p.name === packName);
|
|
293
|
+
if (!declared || !declared.enabled) {
|
|
294
|
+
return allowResult(`harness pack hook codex-stop: pack "${packName}" not enabled, skipping capture.`, stderr);
|
|
295
|
+
}
|
|
296
|
+
const sessionId = pickString(envelope.session_id) ??
|
|
297
|
+
process.env["CODEX_SESSION_ID"] ??
|
|
298
|
+
process.env["CLAUDE_SESSION_ID"] ??
|
|
299
|
+
"";
|
|
300
|
+
if (sessionId === "") {
|
|
301
|
+
return allowResult("harness pack hook codex-stop: no session_id resolvable, skipping capture.", stderr);
|
|
302
|
+
}
|
|
303
|
+
const lastMessage = extractLastAssistantText(envelope);
|
|
304
|
+
if (lastMessage === null || lastMessage.trim() === "") {
|
|
305
|
+
return allowResult("harness pack hook codex-stop: no assistant message in stop event, skipping capture.", stderr);
|
|
306
|
+
}
|
|
307
|
+
const report = parseUnderstandingReport(lastMessage);
|
|
308
|
+
if (!reportHasContent(report)) {
|
|
309
|
+
return allowResult("harness pack hook codex-stop: assistant message did not contain a recognisable Understanding Report (no labelled fields found), skipping capture.", stderr);
|
|
310
|
+
}
|
|
311
|
+
const reportsDir = opts.reportsDir ?? defaultReportsDir();
|
|
312
|
+
const filename = buildReportFilename(sessionId, now);
|
|
313
|
+
const body = {
|
|
314
|
+
sessionId,
|
|
315
|
+
runtime: RUNTIME_TAG,
|
|
316
|
+
createdAt: now.toISOString(),
|
|
317
|
+
approvalStatus: "pending",
|
|
318
|
+
report,
|
|
319
|
+
rawMessage: lastMessage,
|
|
320
|
+
};
|
|
321
|
+
let target;
|
|
322
|
+
try {
|
|
323
|
+
target = writeReportFile(reportsDir, filename, body);
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
return allowResult(`harness pack hook codex-stop: failed to write report (${err.message}), skipping capture.`, stderr);
|
|
327
|
+
}
|
|
328
|
+
const diagnostic = `harness pack hook codex-stop: captured Understanding Report at ${target} (approvalStatus: pending).`;
|
|
329
|
+
stderr.write(`${diagnostic}\n`);
|
|
330
|
+
return { exitCode: 0, reportPath: target, parsed: true, diagnostic };
|
|
331
|
+
}
|
|
332
|
+
//# sourceMappingURL=hook-codex-stop.js.map
|