@lannguyensi/harness 0.28.1 → 0.30.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 CHANGED
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.30.0] - 2026-05-25
11
+
12
+ **Headline: `harness approve risk` grows a `--force <reason>` flag so the operator can clear a deny-tier Risk Gate block without ever touching the ledger directly.** Closes the operator-UX leak surfaced during the v0.29.0 release-cut: the built-in `gate-prod-destructive` policy is `deny`-tier and requires the `risk-override:${SESSION_ID}` ledger tag, but no CLI verb wrote that tag. Its `ux.run` instruction told operators to "record a risk-override entry in the evidence ledger", leaking the ledger as an implementation detail. The new flag mirrors the `approve understanding --force` pattern PR #253 shipped: a one-line operator command with an audit-trail suffix. The built-in policy template's `ux.run` now names the verb, not the tag. **Operator action**: none required; back-compat. The default `harness approve risk` (no flag) is byte-for-byte unchanged. Re-run `npm i -g @lannguyensi/harness` to upgrade.
13
+
14
+ ### Added
15
+
16
+ - **`harness approve risk --force <reason>`** (#258, task a48ccd55). Writes `risk-override:${SESSION_ID}:forced:<sanitised-reason>` to the evidence ledger so the deny-tier `gate-prod-destructive` policy's requires clears. The `:forced:<reason>` suffix is additive metadata so `harness audit` can grep `:forced:` to surface every operator-deliberate override. The TTY guard mirrors `harness pause`: non-TTY stdin refuses with `EX_USAGE` unless `--i-am-the-operator` is passed. Agent-shell (`$CLAUDE_SESSION_ID` set) is intentionally NOT refused; the verb is designed to be runnable from `!`-prefixed Claude Code shells and reads `$CLAUDE_CODE_SESSION_ID` as a session-id resolution tier. Reason sanitisation: lowercase, keeps `[a-z0-9._-]`, collapses other runs to `-`, trims edges, caps at 64 chars; an empty-after-sanitisation result falls back to `operator-override` so the tag is never malformed. The built-in `gate-prod-destructive.ux.run` instruction (and the worked example in `docs/examples/full-manifest.yaml` plus its byte-equivalent golden) is rewritten: no more "record a risk-override entry in the evidence ledger", it now names `harness approve risk --force <reason>` for the per-block override and `harness pause --for <duration>` for the session-wide kill switch. `harness approve risk` without `--force` is byte-for-byte unchanged. Covered by 10 new tests in `tests/cli/approve-risk.test.ts`.
17
+
18
+ ## [0.29.0] - 2026-05-25
19
+
20
+ **Headline: Codex hook diagnostics get sharp, `harness approve understanding` actually enforces the Prior Art rule, and read-only Codex shell commands stop tripping the Understanding Gate.** Three feats + three fixes tighten the operator surface around the Codex adapter and the approve verb. The Codex `harness policy intercept` projection now floors at a 2s timeout (PR #255), self-identifies via `--hook <name>` in the projected command + `[hook=<name>]`-prefixed stderr (PR #256), and resolves the per-call workdir for git-context builtins (PR #254). The Understanding Gate now lets read-only Codex shell commands through (PR #252) and `harness approve understanding` validates the persisted report before writing the marker, refusing reports with missing / empty / literal-`- None` `Prior Art` sections (PR #253). The `harness approve` env-var resolution chain is also fixed to read `$CLAUDE_CODE_SESSION_ID` (PR #251). **Operator action**: none required; all changes are back-compat. After upgrade, regenerate Codex config via `harness apply --runtime codex` to pick up the new `--hook` flags and 2s timeout floor in the projected `~/.codex/config.toml`. Re-run `npm i -g @lannguyensi/harness` to upgrade.
21
+
22
+ ### Added
23
+
24
+ - **`harness policy intercept` self-identifies in Codex via `--hook <name>`** (#256, task 16683705). The Codex generator (`generate-codex-config.ts`) now appends ` --hook <name>` to every projected `harness policy intercept` command so each spawned process is identifiable from `ps`, audit logs, and any error string that echoes the command line. The intercept entrypoint (`policy/intercept.ts`) accepts the new flag via a commander option and prefixes every stderr emission with `[hook=<name>]`: no-match hint, verbose decision diagnostic, malformed-event JSON error, manifest-load failure, audit-write failure. Unsafe hook names (whitespace, shell metachars) silently skip the flag (`SAFE_HOOK_NAME_RE` is `[A-Za-z0-9._:-]+`, which excludes every POSIX shell active token) and fall back to the un-tagged emission. The Claude Code projection (`generate-settings.ts`) is deliberately NOT changed: that generator dedupes by `(command, timeout)` within each matcher group, so per-hook flag injection would diverge the dedupe key and N-multiply ledger queries plus audit writes per tool event. Codex has no such dedupe so the change is safe there. Back-compat: invocations without `--hook` (operator probes, smoke scripts, pre-0.29.0 installed configs) keep the un-tagged stderr format. Covered by 3 new tests in `tests/cli/apply/generate-codex-config.test.ts` (positive injection, non-policy bypass, unsafe-name skip) and 6 new tests in `tests/runtime/intercept-cli.test.ts` (tag in no-match / verbose / malformed-JSON / manifest-load / audit-write paths, plus back-compat untagged path).
25
+
26
+ - **`harness approve understanding` validates the persisted Understanding Report before writing the marker** (#253, task 2947c2a9). A `grill_me` report with missing, empty, or literal-`- None` `priorArt` is now refused before the canonical `.approvals/<sessionId>` marker is written, the ledger entry is appended, or the report is flipped from `pending` to `approved`. `fast_confirm` reports skip the check (the relaxed schema variant intentionally drops `priorArt` from `required`); legacy reports without a `mode` field pass through unchanged so the v0.4.0 schema bump doesn't retroactively invalidate historical reports. A new `--force` flag bypasses the check for emergency unblock; the ledger tag is stamped `understanding-approved:<session>:forced:<field>` so audit can distinguish forced approvals from clean ones, and the CLI prints a `validation:` line on stdout so a forced approval is visible at the call site. Closes a gap found via dogfood on 2026-05-24: the prompt declared Section 10 (Prior Art) required since `@lannguyensi/understanding-gate@0.4.0` BREAKING, but no operator-side path enforced it, so an agent could skip the section and still get the gate open. Covered by 11 new tests in `tests/cli/approve-understanding.test.ts`.
27
+
28
+ - **Read-only Codex shell commands skip the Understanding Gate pre-tool-use blocker** (#252, task c0e67c14). The Codex variant of the `understanding-before-execution` pack pre-tool-use hook (`src/cli/pack/hook-codex-pre-tool-use.ts`) now reuses the existing read-only Bash classifier (introduced for the Claude side in PR #242) to allow read-only shell commands (`git status`, `git log`, `git diff`, `ls`, `pwd`, `cat`, `grep`, `find`, etc.) through without an approved report. Mutating commands, shell-chained commands, and `apply_patch` still hard-block. Codex shell extraction reads `raw_input.command`, `raw_input.cmd`, and raw string payloads, failing closed on conflicting aliases. Closes the Codex-side gap left by PR #242, which only treated the Claude pre-tool-use blocker. Covered by `tests/cli/pack-hook-codex-pre-tool-use.test.ts` and `tests/cli/pack-read-only-bash.test.ts`.
29
+
30
+ ### Fixed
31
+
32
+ - **Codex `harness policy intercept` hooks float at a 2s timeout floor** (#255, task 25dec529). The Codex generator (`generate-codex-config.ts`) now projects every `harness policy intercept` hook with `Math.max(2, ceil(budget_ms / 1000))` instead of `Math.max(1, ceil(...))`, so the Full-template policy-intercept hooks with `budget_ms: 1000` (`require-preflight-evidence`, `require-preflight-push-evidence`) get `timeout = 2` instead of `timeout = 1` in the emitted `~/.codex/config.toml`. The 1s floor was too tight under Codex's cold-start path (manifest load, git-context resolution, ledger query, risk/env evaluation, process startup), so the gates surfaced spurious `PreToolUse hook (failed) error: hook timed out after 1s` errors on routine Bash. Non-policy hooks with `budget_ms: 1000` still floor at `timeout = 1` (the bump is scoped via exact-match `h.command.trim() === "harness policy intercept"`). Each generated `[[hooks.*]]` block now also carries a `# harness hook: <name> (budget_ms=N)` comment so an operator opening `~/.codex/config.toml` can identify the source hook. Operators must re-run `harness apply --runtime codex --install` to pick up the floor for an existing install. Covered by regressions in `tests/cli/apply/generate-codex-config.test.ts`.
33
+
34
+ - **Codex `harness policy intercept` resolves the per-call shell workdir for git-context builtins** (#254). When a Codex `exec_command` / `shell` event arrives without a top-level `event.cwd` (Codex's default for non-Bash-aliased shell tools), `harness policy intercept` now resolves the policy cwd from `event.raw_input.workdir`, then `event.tool_input.workdir`, then `event.input.workdir`, then the Codex sandbox `--command-cwd` from `/proc/1/cmdline`, before falling back to `process.cwd()`. This gives the `REPO` / `BRANCH` builtins a meaningful work tree to derive from, so per-repo and per-branch ledger tags (`preflight:${REPO}`, `preflight:${BRANCH}`) actually namespace under Codex rather than collapsing to the harness process's cwd. The git-context resolver also now rejects directory-form `.git` entries without a readable `HEAD`, so an empty parent `.git/` directory no longer becomes a fake repo with a blank `BRANCH`. Covered by new Codex workdir-extraction tests in `tests/runtime/intercept-cli.test.ts` and a directory-`.git`-fallback test in `tests/runtime/git-context.test.ts`.
35
+
36
+ - **`harness approve` reads `$CLAUDE_CODE_SESSION_ID` before legacy env vars** (#251, task 058b31a3). The env-var fallback chain on both `harness approve risk` and `harness approve understanding` previously only checked `$CLAUDE_SESSION_ID` and `$CODEX_SESSION_ID`. Claude Code exports its session id as `$CLAUDE_CODE_SESSION_ID` (not `$CLAUDE_SESSION_ID`), so an arg-less `harness approve` invoked from inside a Claude Code session never resolved via the env tier: it only worked via `--session` or the hook-staged `.pending-approval` marker. The resolver now reads `$CLAUDE_CODE_SESSION_ID` first (canonical, runtime-exported), then `$CLAUDE_SESSION_ID` (legacy / docs-name peer, back-compat), then `$CODEX_SESSION_ID`, before falling through to `.pending-approval` and (for understanding) the newest pending Understanding Report. The no-session-id error message and the `--session` option help text are rewritten to name the full chain. A new `sessionSource: "env-claude-code"` variant lets the CLI annotate `(from $CLAUDE_CODE_SESSION_ID)` in stdout so a wrong env pick is visible before it lands. Tests in `tests/cli/approve-risk.test.ts` and `tests/cli/approve-understanding.test.ts` cover each env-var path independently and assert documented precedence; hermetic env hygiene additions clear all three vars in beforeEach so an external shell's exports cannot leak into a test run.
37
+
10
38
  ## [0.28.1] - 2026-05-24
11
39
 
12
40
  **Hotfix: arg-less `harness approve risk` now actually works after a Risk Gate block, and `harness doctor` warns on the misconfiguration that caused the original lockout.** Surfaced during the 0.28.0 release-cut session: a Risk Gate block fired against a routine read-only Bash probe (`--version`), and recovery via `harness approve risk` failed arg-less because the session id was never staged where the operator could read it. Two complementary fixes ship together.
@@ -111,19 +111,39 @@ function eventKey(event) {
111
111
  return event;
112
112
  }
113
113
  }
114
- function codexTimeoutSeconds(budgetMs) {
115
- return Math.max(1, Math.ceil(budgetMs / 1000));
114
+ function codexTimeoutSeconds(h) {
115
+ const minimumSeconds = h.command.trim() === "harness policy intercept" ? 2 : 1;
116
+ return Math.max(minimumSeconds, Math.ceil(h.budget_ms / 1000));
117
+ }
118
+ // Hook names safe to splice into the emitted command literal as
119
+ // `--hook <name>`. Restricted to a quote/space/metachar-free charset so
120
+ // the projection never produces a shell-broken command, even though the
121
+ // underlying `Hook.name` schema (z.string().min(1)) accepts anything.
122
+ // All policy-pack hook names ship within this charset; an exotic name
123
+ // silently skips the flag and falls back to the un-tagged emission, with
124
+ // the preceding `# harness hook: <name>` TOML comment still naming it.
125
+ const SAFE_HOOK_NAME_RE = /^[A-Za-z0-9._:-]+$/;
126
+ function commandWithHookTag(h) {
127
+ if (h.command.trim() !== "harness policy intercept")
128
+ return h.command;
129
+ if (!SAFE_HOOK_NAME_RE.test(h.name))
130
+ return h.command;
131
+ return `${h.command} --hook ${h.name}`;
116
132
  }
117
133
  function emitCommandHook(h) {
118
134
  const fields = [
119
135
  `type = "command"`,
120
- `command = ${tomlString(h.command)}`,
121
- `timeout = ${codexTimeoutSeconds(h.budget_ms)}`,
136
+ `command = ${tomlString(commandWithHookTag(h))}`,
137
+ `timeout = ${codexTimeoutSeconds(h)}`,
122
138
  ];
123
139
  return `{ ${fields.join(", ")} }`;
124
140
  }
141
+ function tomlCommentText(s) {
142
+ return s.replace(CONTROL_CHAR_RE, " ").replace(/\s+/g, " ").trim();
143
+ }
125
144
  function emitHook(h) {
126
145
  const lines = [];
146
+ lines.push(`# harness hook: ${tomlCommentText(h.name)} (budget_ms=${h.budget_ms})`);
127
147
  lines.push(`[[hooks.${eventKey(h.event)}]]`);
128
148
  if (h.match !== undefined) {
129
149
  lines.push(`matcher = ${tomlString(expandCodexHookMatchPattern(h.match))}`);
@@ -1 +1 @@
1
- {"version":3,"file":"generate-codex-config.js","sourceRoot":"","sources":["../../../src/cli/apply/generate-codex-config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"generate-codex-config.js","sourceRoot":"","sources":["../../../src/cli/apply/generate-codex-config.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,32 @@
1
1
  import type { Manifest } from "../../schema/index.js";
2
2
  import { type LoaderOptions } from "../loader.js";
3
3
  export interface ApproveRiskOptions extends LoaderOptions {
4
- /** Explicit session id (overrides $CLAUDE_SESSION_ID / $CODEX_SESSION_ID). */
4
+ /** Explicit session id (overrides $CLAUDE_CODE_SESSION_ID / $CLAUDE_SESSION_ID / $CODEX_SESSION_ID). */
5
5
  session?: string;
6
+ /**
7
+ * Operator-deliberate override of a Risk Gate `deny` decision. Writes
8
+ * `risk-override:${SESSION_ID}:forced:<reason-slug>` instead of the
9
+ * default `risk-approved:${SESSION_ID}` tag, so the built-in
10
+ * `gate-prod-destructive` policy (which gates on `risk-override:`,
11
+ * see `src/cli/init/templates.ts`) clears. `deny` is by design not
12
+ * approvable, so this path is hard-gated behind operator-only checks:
13
+ * a TTY stdin OR explicit `iAmTheOperator` acknowledgement is
14
+ * required, mirroring `harness pause`. The reason becomes part of the
15
+ * audit trail and is sanitised to a tag-safe slug.
16
+ */
17
+ force?: {
18
+ reason: string;
19
+ };
20
+ /**
21
+ * Acknowledge a non-TTY / scripted invocation of `--force`. Without
22
+ * this flag a non-TTY `--force` refuses with a usage error.
23
+ */
24
+ iAmTheOperator?: boolean;
25
+ /**
26
+ * Override `process.stdin.isTTY` for tests; mirrors the pause/resume
27
+ * seam so the non-TTY refusal can be exercised hermetically.
28
+ */
29
+ stdinIsTTY?: boolean;
6
30
  /** Override the harness.generated/ directory (test injection). */
7
31
  generatedDir?: string;
8
32
  /** Inject a manifest (test); bypasses `loadManifest`. */
@@ -18,7 +42,14 @@ export interface ApproveRiskOptions extends LoaderOptions {
18
42
  export interface ApproveRiskResult {
19
43
  sessionId: string;
20
44
  /** Where `sessionId` came from — surfaced so the operator can sanity-check it. */
21
- sessionSource: "flag" | "env-claude" | "env-codex" | "pending-approval";
45
+ sessionSource: "flag" | "env-claude-code" | "env-claude" | "env-codex" | "pending-approval";
46
+ /**
47
+ * True when the verb wrote a `risk-override:` tag via `--force`
48
+ * instead of the default `risk-approved:`. The CLI surfaces this so
49
+ * the operator can see at a glance that they exercised the deny-tier
50
+ * override, not a clean require_approval approval.
51
+ */
52
+ forced: boolean;
22
53
  ledger: {
23
54
  ok: boolean;
24
55
  tag: string;
@@ -27,13 +58,25 @@ export interface ApproveRiskResult {
27
58
  }
28
59
  /** The evidence-ledger tag a Risk Gate `require_approval` policy consults. */
29
60
  export declare function riskApprovedTagFor(sessionId: string): string;
61
+ /**
62
+ * The evidence-ledger tag a Risk Gate `deny`-tier policy with
63
+ * `requires.ledger_tag: risk-override:${SESSION_ID}` consults. The
64
+ * `:forced:<reason>` suffix is additive metadata: the built-in
65
+ * policy's matcher pins the `risk-override:<session>` substring, so the
66
+ * suffix never affects whether the gate clears. It exists for the
67
+ * audit trail (`harness audit` can grep `:forced:` to surface every
68
+ * operator-deliberate override).
69
+ */
70
+ export declare function riskOverrideTagFor(sessionId: string, reason: string): string;
30
71
  /**
31
72
  * Resolve the target session id and write its `risk-approved:` ledger
32
73
  * tag. Session id precedence mirrors `harness approve understanding`
33
- * tiers 1-4: explicit `--session`, then `$CLAUDE_SESSION_ID`, then
34
- * `$CODEX_SESSION_ID`, then the `.pending-approval` file the gate hook
35
- * staged on its last block. There is no persisted-report tier-5 guess:
36
- * the Risk Gate produces no persisted reports.
74
+ * tiers 1-4: explicit `--session`, then `$CLAUDE_CODE_SESSION_ID`
75
+ * (the var Claude Code itself sets), then `$CLAUDE_SESSION_ID` (legacy
76
+ * / docs name), then `$CODEX_SESSION_ID`, then the `.pending-approval`
77
+ * file the gate hook staged on its last block. There is no
78
+ * persisted-report tier-5 guess: the Risk Gate produces no persisted
79
+ * reports.
37
80
  *
38
81
  * Throws `HarnessExitError(EX_FAIL)` when no session id can be resolved.
39
82
  * A degraded ledger (grounding-mcp absent / unreachable) is surfaced in
@@ -15,13 +15,64 @@
15
15
  // needs no change: it already stores arbitrary tags through `ledger_add`.
16
16
  import { addLedgerFact } from "../../runtime/ledger-add.js";
17
17
  import { readPendingApproval, resolveGeneratedDir, } from "../../runtime/pending-approval.js";
18
- import { EX_FAIL, HarnessExitError } from "../exit-codes.js";
18
+ import { EX_FAIL, EX_USAGE, HarnessExitError } from "../exit-codes.js";
19
19
  import { loadManifest, resolvePaths } from "../loader.js";
20
20
  const RISK_APPROVED_PREFIX = "risk-approved";
21
+ const RISK_OVERRIDE_PREFIX = "risk-override";
21
22
  /** The evidence-ledger tag a Risk Gate `require_approval` policy consults. */
22
23
  export function riskApprovedTagFor(sessionId) {
23
24
  return `${RISK_APPROVED_PREFIX}:${sessionId}`;
24
25
  }
26
+ /**
27
+ * The evidence-ledger tag a Risk Gate `deny`-tier policy with
28
+ * `requires.ledger_tag: risk-override:${SESSION_ID}` consults. The
29
+ * `:forced:<reason>` suffix is additive metadata: the built-in
30
+ * policy's matcher pins the `risk-override:<session>` substring, so the
31
+ * suffix never affects whether the gate clears. It exists for the
32
+ * audit trail (`harness audit` can grep `:forced:` to surface every
33
+ * operator-deliberate override).
34
+ */
35
+ export function riskOverrideTagFor(sessionId, reason) {
36
+ return `${RISK_OVERRIDE_PREFIX}:${sessionId}:forced:${sanitiseReasonSlug(reason)}`;
37
+ }
38
+ /**
39
+ * Reduce an operator-supplied free-form reason to a tag-safe slug. Keeps
40
+ * `[A-Za-z0-9._-]`, collapses any other run to a single `-`, trims
41
+ * leading / trailing `-`, lowercases, caps at 64 chars. An empty result
42
+ * (e.g. the operator passed only punctuation) is rejected upstream.
43
+ */
44
+ function sanitiseReasonSlug(reason) {
45
+ const slug = reason
46
+ .toLowerCase()
47
+ .replace(/[^a-z0-9._-]+/g, "-")
48
+ .replace(/^-+|-+$/g, "")
49
+ .slice(0, 64);
50
+ return slug.length > 0 ? slug : "operator-override";
51
+ }
52
+ function refuseForcedIfNonTTY(opts) {
53
+ if (!opts.force)
54
+ return;
55
+ if (opts.iAmTheOperator === true)
56
+ return;
57
+ // Deliberate divergence from `harness pause`'s twin guard
58
+ // (`src/cli/pause/index.ts`'s `refuseIfAgentShell`): this verb is
59
+ // designed to be runnable from inside `!`-prefixed Claude Code shells
60
+ // where `$CLAUDE_SESSION_ID` is set (the session-id resolver below
61
+ // reads it as a fallback tier). Refusing on that env var would gut
62
+ // the `! ` UX. Operator intent is gated by the non-empty `<reason>`
63
+ // and the TTY / `--i-am-the-operator` check; that is sufficient.
64
+ const tty = opts.stdinIsTTY !== undefined ? opts.stdinIsTTY : process.stdin.isTTY === true;
65
+ if (tty)
66
+ return;
67
+ throw new HarnessExitError([
68
+ "harness approve risk --force refuses to run with non-TTY stdin (looks scripted).",
69
+ "",
70
+ "This is the load-bearing guardrail against `--force` becoming an agent-driven",
71
+ "bypass of a deny-tier Risk Gate decision. Run the verb from your own operator",
72
+ "shell (in Claude Code: prefix the command with `! `), or pass --i-am-the-operator",
73
+ "to acknowledge a one-off recovery script.",
74
+ ].join("\n"), EX_USAGE);
75
+ }
25
76
  function findGroundingMcp(manifest) {
26
77
  return manifest.tools.mcp.find((m) => m.name === "grounding-mcp") ?? null;
27
78
  }
@@ -49,10 +100,12 @@ async function writeLedgerTag(manifest, sessionId, content, opts) {
49
100
  /**
50
101
  * Resolve the target session id and write its `risk-approved:` ledger
51
102
  * tag. Session id precedence mirrors `harness approve understanding`
52
- * tiers 1-4: explicit `--session`, then `$CLAUDE_SESSION_ID`, then
53
- * `$CODEX_SESSION_ID`, then the `.pending-approval` file the gate hook
54
- * staged on its last block. There is no persisted-report tier-5 guess:
55
- * the Risk Gate produces no persisted reports.
103
+ * tiers 1-4: explicit `--session`, then `$CLAUDE_CODE_SESSION_ID`
104
+ * (the var Claude Code itself sets), then `$CLAUDE_SESSION_ID` (legacy
105
+ * / docs name), then `$CODEX_SESSION_ID`, then the `.pending-approval`
106
+ * file the gate hook staged on its last block. There is no
107
+ * persisted-report tier-5 guess: the Risk Gate produces no persisted
108
+ * reports.
56
109
  *
57
110
  * Throws `HarnessExitError(EX_FAIL)` when no session id can be resolved.
58
111
  * A degraded ledger (grounding-mcp absent / unreachable) is surfaced in
@@ -60,6 +113,12 @@ async function writeLedgerTag(manifest, sessionId, content, opts) {
60
113
  * understanding`'s ledger write.
61
114
  */
62
115
  export async function approveRisk(opts = {}) {
116
+ // --force is operator-deliberate; guard before any side effect. The
117
+ // session-id resolution below intentionally still runs the agent-shell
118
+ // env tier (CLAUDE_CODE_SESSION_ID etc.), since the operator may be
119
+ // running from `!` with --i-am-the-operator and the session id has to
120
+ // come from somewhere.
121
+ refuseForcedIfNonTTY(opts);
63
122
  const generatedDir = opts.generatedDir ??
64
123
  resolveGeneratedDir({
65
124
  ...(opts.homeDir !== undefined ? { homeDir: opts.homeDir } : {}),
@@ -71,6 +130,15 @@ export async function approveRisk(opts = {}) {
71
130
  sessionId = opts.session;
72
131
  sessionSource = "flag";
73
132
  }
133
+ else if (typeof process.env.CLAUDE_CODE_SESSION_ID === "string" &&
134
+ process.env.CLAUDE_CODE_SESSION_ID.length > 0) {
135
+ // Canonical: the var Claude Code actually exports into the agent
136
+ // shell. Read before the legacy $CLAUDE_SESSION_ID so an operator
137
+ // who has both set (e.g. a manual export plus the runtime export)
138
+ // gets the runtime's id, not whatever they typed by hand.
139
+ sessionId = process.env.CLAUDE_CODE_SESSION_ID;
140
+ sessionSource = "env-claude-code";
141
+ }
74
142
  else if (typeof process.env.CLAUDE_SESSION_ID === "string" &&
75
143
  process.env.CLAUDE_SESSION_ID.length > 0) {
76
144
  sessionId = process.env.CLAUDE_SESSION_ID;
@@ -90,7 +158,9 @@ export async function approveRisk(opts = {}) {
90
158
  }
91
159
  if (sessionId === "") {
92
160
  throw new HarnessExitError([
93
- "no session id available. Pass --session <id>, or set $CLAUDE_SESSION_ID / $CODEX_SESSION_ID.",
161
+ "no session id available. Pass --session <id>, or set one of",
162
+ "$CLAUDE_CODE_SESSION_ID (Claude Code) / $CLAUDE_SESSION_ID (legacy) /",
163
+ "$CODEX_SESSION_ID (Codex).",
94
164
  "",
95
165
  "The understanding-gate PreToolUse hook and `harness preflight` both stage the",
96
166
  `session id in ${generatedDir}/.pending-approval, so an arg-less`,
@@ -98,7 +168,7 @@ export async function approveRisk(opts = {}) {
98
168
  "neither has run for the current session yet.",
99
169
  "",
100
170
  "From inside the running agent you can also read the id directly:",
101
- "Claude Code exposes $CLAUDE_SESSION_ID; Codex exposes $CODEX_SESSION_ID.",
171
+ "Claude Code exposes $CLAUDE_CODE_SESSION_ID; Codex exposes $CODEX_SESSION_ID.",
102
172
  ].join("\n"), EX_FAIL);
103
173
  }
104
174
  // The manifest is needed only for the grounding-mcp command. If it
@@ -111,13 +181,22 @@ export async function approveRisk(opts = {}) {
111
181
  catch {
112
182
  /* swallow; ledger write becomes a degraded-ok */
113
183
  }
114
- const tag = riskApprovedTagFor(sessionId);
184
+ // Forced override writes a different tag prefix (`risk-override:`)
185
+ // that the deny-tier `gate-prod-destructive` policy template consults.
186
+ // The audit-only `:forced:<reason>` suffix lets `harness audit` and
187
+ // `harness explain --trace` distinguish a clean require_approval
188
+ // approval from a deliberate deny override.
189
+ const forced = opts.force !== undefined;
190
+ const tag = forced
191
+ ? riskOverrideTagFor(sessionId, opts.force.reason)
192
+ : riskApprovedTagFor(sessionId);
115
193
  const ledgerResult = manifest
116
194
  ? await writeLedgerTag(manifest, sessionId, tag, opts)
117
195
  : { ok: false, reason: "manifest unreadable; skipped ledger write" };
118
196
  return {
119
197
  sessionId,
120
198
  sessionSource,
199
+ forced,
121
200
  ledger: ledgerResult.ok
122
201
  ? { ok: true, tag }
123
202
  : { ok: false, tag, reason: ledgerResult.reason },
@@ -1 +1 @@
1
- {"version":3,"file":"risk.js","sourceRoot":"","sources":["../../../src/cli/approve/risk.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"risk.js","sourceRoot":"","sources":["../../../src/cli/approve/risk.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,17 @@
1
1
  import type { Manifest } from "../../schema/index.js";
2
2
  import { type LoaderOptions } from "../loader.js";
3
3
  export interface ApproveUnderstandingOptions extends LoaderOptions {
4
- /** Explicit session id (overrides $CLAUDE_SESSION_ID). */
4
+ /** Explicit session id (overrides $CLAUDE_CODE_SESSION_ID / $CLAUDE_SESSION_ID / $CODEX_SESSION_ID). */
5
5
  session?: string;
6
+ /**
7
+ * Override the approve-time report validation (priorArt enforcement for
8
+ * `grill_me` reports). Writes the marker / ledger / report-flip anyway
9
+ * and stamps the ledger tag content with a `:forced:<field>-<reason>`
10
+ * suffix so the audit trail records the bypass. Emergency-unblock path:
11
+ * the default refuses the marker when validation fails so an agent
12
+ * cannot ship a hollow Understanding Report and still get the gate open.
13
+ */
14
+ force?: boolean;
6
15
  /**
7
16
  * Optional agent-tasks task id (harness/1ee26e77). When set, an
8
17
  * additional task-scoped marker file is written at
@@ -73,7 +82,7 @@ export interface ApproveUnderstandingResult {
73
82
  * operator when the id was not explicit (`pending-approval` / `env`),
74
83
  * which is the moment to sanity-check it against the live session.
75
84
  */
76
- sessionSource: "flag" | "env-claude" | "env-codex" | "pending-approval" | "newest-report";
85
+ sessionSource: "flag" | "env-claude-code" | "env-claude" | "env-codex" | "pending-approval" | "newest-report";
77
86
  /**
78
87
  * When `sessionSource` is `"newest-report"`, the absolute path of the
79
88
  * persisted report the session id was guessed from. Undefined for
@@ -125,6 +134,33 @@ export interface ApproveUnderstandingResult {
125
134
  ok: false;
126
135
  reason: string;
127
136
  };
137
+ /**
138
+ * Approve-time content validation of the persisted report. `mode` is
139
+ * the report's stamped mode field (the schema variant the report is
140
+ * judged against). `ok: false` means a structural rule the prompt
141
+ * already declared was violated (today: `grill_me` reports must have
142
+ * a non-empty priorArt list with no literal `- None`).
143
+ *
144
+ * `enforced: false` means the failure was bypassed via `--force`; the
145
+ * marker / ledger / report-flip still ran, and `ledger.tag` carries a
146
+ * `:forced:<field>-<reason>` suffix so audit can distinguish forced
147
+ * approvals from clean ones.
148
+ *
149
+ * `skipped: true` means no report was loaded to validate (no `latest`
150
+ * matched the session, ledger-only path); validation is silently
151
+ * waived because there is nothing to enforce.
152
+ */
153
+ validation: {
154
+ ok: true;
155
+ mode: string | null;
156
+ } | {
157
+ ok: false;
158
+ field: string;
159
+ reason: string;
160
+ enforced: boolean;
161
+ } | {
162
+ skipped: true;
163
+ };
128
164
  }
129
165
  /**
130
166
  * Normalise a list of task ids supplied via `opts.tasks` (or the CLI's