@lannguyensi/harness 0.48.0 → 0.49.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.
@@ -1,5 +1,38 @@
1
1
  export declare const MINIMAL_TEMPLATE = "# ~/.harness/harness.yaml (legacy: ~/.claude/harness.yaml)\n#\n# Bootstrapped by `harness init --template minimal`.\n#\n# This is the empty-but-valid manifest. Run `harness validate` to confirm it\n# parses, then add entries under the five top-level keys:\n#\n# grounding: evidence-ledger + claim-gate config (see docs/ARCHITECTURE.md \u00A72)\n# tools: mcp / cli / skills / builtin inventory (\u00A73)\n# memory: directories, retention, scopes (\u00A74)\n# hooks: event-bound shell commands (\u00A75)\n# policies: named rules that bind hooks to triggers (\u00A76)\n#\n# Phase 2 verbs to add entries safely: `harness add mcp <name> ...`,\n# `harness add cli`, `harness add hook`, `harness add skill`.\n# Per-machine overrides live at ~/.harness/machines/<discriminator>.harness.overrides.yaml\n# (ARCHITECTURE.md \u00A78) for paths that vary per host.\n#\n# Docs: https://github.com/LanNguyenSi/harness\n\nversion: 1\n";
2
- export declare const FULL_TEMPLATE = "# ~/.harness/harness.yaml (legacy: ~/.claude/harness.yaml)\n#\n# Bootstrapped by `harness init --template full`. The reference manifest:\n# every example policy from docs/examples/full-manifest.yaml wired through\n# the generic `harness policy intercept` engine, so no external shell\n# scripts under ~/.claude/hooks/ are required.\n#\n# Canonical source for the policy + policy_packs sections is\n# docs/examples/full-manifest.yaml. A parity vitest\n# (tests/cli/init-full-template-parity.test.ts) fails the build if the\n# two diverge on policy names or load-bearing fields.\n#\n# What you still need on PATH (the wizard offers to `npm i -g` these on\n# init): agent-tasks-mcp-bridge, grounding-mcp, memory-router-*,\n# understanding-gate-claude-*.\n\nversion: 1\n\ngrounding:\n session:\n auto_start: true\n id_format: \"gs-{repo}-{rand:8}\"\n evidence_ledger:\n path: ~/.evidence-ledger/ledger.db\n retention_days: 90\n policies_source: ~/.claude/harness.d/policies/claim-gate.yaml\n\ntools:\n mcp:\n # codebase-oracle (the Pandora RAG MCP server) is intentionally NOT\n # in the Full default. It is published as\n # `@lannguyensi/codebase-oracle` and works fine standalone, but it\n # is an opinionated workflow add-on (multi-repo semantic search)\n # rather than infrastructure harness itself assumes. Operators who\n # want it wire it explicitly:\n # npm i -g @lannguyensi/codebase-oracle\n # harness add mcp codebase-oracle --command codebase-oracle,mcp\n # Set ORACLE_SCAN_ROOT (absolute path; tilde is not expanded by the\n # MCP env block) and OPENAI_API_KEY (or switch providers via\n # ORACLE_LLM_PROVIDER) before the first call.\n - name: agent-tasks\n # Zero-setup entry: `@agent-tasks/mcp-bridge` exposes the\n # `agent-tasks-mcp-bridge` binary on PATH. The bridge owns token\n # storage and defaults to the hosted backend; override with\n # `AGENT_TASKS_BASE_URL` / `AGENT_TASKS_TOKEN` for self-hosted.\n # `min_version` floor: 0.6.0 added the `--version` short-circuit\n # the doctor probe needs (PR agent-tasks/240, release-cut PR 241).\n # Bump the floor whenever a fix you depend on lands; loose floors\n # are fine, the point is the drift signal not pinning a specific cut.\n command: [agent-tasks-mcp-bridge]\n min_version: \"0.6.0\"\n health:\n verb: projects_list\n timeout_ms: 5000\n enabled: true\n - name: grounding-mcp\n # Published bin from `@lannguyensi/grounding-mcp`. No env is set:\n # the bundled default resolves to `~/.evidence-ledger/ledger.db`\n # via os.homedir() at startup. Passing a literal tilde in env\n # bypasses shell expansion and creates rogue cwd-relative DB files\n # (see agent-tasks/42d224a6 incident). `min_version` floor: 0.2.0\n # added the `--version` short-circuit the doctor probe needs (PR\n # agent-grounding/76, release-cut PR 77).\n command: [grounding-mcp]\n min_version: \"0.2.0\"\n health:\n verb: ledger_status\n timeout_ms: 5000\n enabled: true\n\n cli:\n - name: gh\n binary: gh\n required: true\n\n skills:\n enabled:\n - simplify\n - init\n - review\n - security-review\n source_dirs:\n - ~/.claude/skills\n\n builtin:\n known: [Read, Edit, Write, Bash, Agent, Skill, TaskCreate, Glob, Grep]\n\nmemory:\n directories:\n - path: ~/.claude/projects/{project}/memory\n scope: project\n router:\n # Published bin from `@lannguyensi/memory-router`.\n # `min_version` floor: 0.3.0 added the `--version` short-circuit\n # the doctor probe needs (PR agent-memory/40, release-cut PR 41).\n command: [memory-router-user-prompt-submit]\n min_version: \"0.3.0\"\n enabled: true\n retention:\n staleness_days: 180\n broken_refs: warn\n scopes:\n default: project\n allowed: [project, user]\n\n# All PreToolUse hooks share the generic `harness policy intercept` CLI\n# entrypoint. The engine reads the tool event on stdin, evaluates whichever\n# policy below has a matching trigger (`match` + optional `bash_match`),\n# and emits Claude Code's deny envelope when the required ledger tag is\n# absent. No external shell scripts are required.\n#\n# The `git-preflight` SessionStart hook is the producer side of the\n# `preflight-before-*` policies: `harness session-start preflight` runs\n# agent-preflight against the session cwd and, on a ready:true result,\n# records `preflight:${REPO}` to the evidence ledger. It needs the\n# `preflight` binary on PATH (`npm i -g @lannguyensi/agent-preflight`); when\n# that is absent the hook logs to stderr and exits 0, so the session is\n# never broken \u2014 the preflight gates just stay closed until a tag is\n# produced some other way.\nhooks:\n - name: git-preflight\n event: SessionStart\n command: harness session-start preflight\n blocking: false\n # 70s budget gives the wrapped preflight (default 60s) headroom plus\n # ledger-write time. Was 30s through v0.17.4, but a healthy preflight\n # on a medium-size repo takes ~28s and the old 25s wrapper ceiling\n # blew through it. Bumped together with DEFAULT_PREFLIGHT_TIMEOUT_MS\n # (agent-tasks/7265599e).\n budget_ms: 70000\n # Floor at agent-preflight 0.2.0, the release that makes secret\n # detection git-aware and diff-scoped: a gitignored+untracked .env,\n # a .md doc, a non-git dir, or a secret in a tracked file the branch\n # never touched is a non-blocking warn, not a hard fail. Pre-0.2.0\n # installs hard-fail preflight on the normal correct state (a\n # gitignored .env holding real credentials), so this SessionStart\n # producer never writes a preflight: tag and the preflight-before-*\n # policies stay closed forever on any repo with a local .env. (0.1.1\n # had already fixed the wrapper-script \"tool not installed\" false\n # positive.) version_command points at the source-of-truth preflight\n # binary, not at the `harness session-start preflight` wrapper.\n min_version: \"0.2.0\"\n version_command: [\"preflight\", \"--version\"]\n\n # toolchain-parity (PATH-shim incident 2026-07-22 follow-up): writes THIS\n # machine's toolchain snapshot (node version, npm globals, OW-Kit\n # version, MCP server names) to `toolchain_parity.machine_state_dir`\n # and advisorily compares it against every peer machine's snapshot\n # already there, warning on drift (version mismatches, missing\n # packages, node/OW-Kit drift, MCP-name differences). Purely advisory \u2014\n # no policy consumes the `toolchain-parity:` ledger fact this writes,\n # it exists for `harness audit`/operator visibility only. If a policy\n # ever does consume it, match with the `:unparseable-peer:<n>` suffix\n # in mind: requires-matching is substring-based, so a bare\n # `toolchain-parity:ok` tag would also match `ok:unparseable-peer:2`\n # (a partial comparison). DISABLED by\n # default (no `toolchain_parity:` block above): opt in with\n # `toolchain_parity: { enabled: true }` (machine_state_dir/profile/\n # workspace_root all have sane defaults \u2014 see docs/CLI.md). No external\n # binary to floor-check (node/npm are assumed present already), so\n # unlike git-preflight this hook carries no min_version/version_command.\n - name: toolchain-parity\n event: SessionStart\n command: harness session-start toolchain-parity\n blocking: false\n # node --version + npm ls -g run in PARALLEL (bounded ~2s/~4s each, so\n # max(2s,4s)=4s worst case for that pair), plus two near-instant file\n # reads; the ledger write runs SEQUENTIALLY after those and carries its\n # own timeout floor (resolveManifestLedgerWriter's default 5s absent an\n # explicit grounding-mcp health.timeout_ms or a bound\n # SessionStartToolchainParityOptions.ledgerTimeoutMs). Worst case is\n # therefore additive, ~4s + 5s = 9s, not the sub-5s the collectors alone\n # would suggest \u2014 10s still clears it, but with only ~1s of margin, not\n # the \"comfortable\" headroom an earlier version of this comment claimed\n # (task c1b5ade5). Nowhere near git-preflight's 70s (which wraps a full\n # external test suite, a fundamentally heavier operation this hook\n # never performs).\n budget_ms: 10000\n\n # Budget note (task 7bf47554, follow-up to the ms/seconds unit fix\n # f2d2a29): every `harness policy intercept` hook below down through\n # `risk-gate` carries `budget_ms: 15000`, i.e. a Claude Code outer\n # kill-timeout of `ceil(15000/1000) = 15` seconds (generate-settings.ts's\n # `hookTimeoutSeconds`). This is deliberately UNIFORM across all eleven of\n # them, for two independent reasons:\n #\n # 1. FAIL-CLOSED MARGIN. `harness policy intercept` evaluates its FULL\n # `policies:` list against the incoming event (src/runtime/intercept.ts,\n # `intercept()`), not just the one named policy this hook happens to\n # also register. Every policy in that evaluation loop that reaches a\n # verdict has its decision written to the evidence ledger via\n # `options.ledger.record()` BEFORE `intercept()` returns and stdout is\n # flushed (intercept.ts ~L1362-1377) \u2014 this includes `operator_only`\n # policies (the three kill-switch denies below), whose VERDICT needs no\n # ledger read but whose AUDIT WRITE is still a live grounding-mcp\n # round-trip on the critical path. A `requires:`-based policy (the\n # require-*-evidence / risk-gate policies) additionally QUERIES the\n # ledger for its verdict (intercept.ts's `evaluateOnePolicy`,\n # ~L752-777) and, on a `deny-degraded` outcome, may retry the audit\n # write once more on a fresh session (`realLedgerClient`,\n # src/cli/policy/intercept.ts ~L276-306). Measured worst case with the\n # grounding-mcp health.timeout_ms=5000 default: query (~5s) + one\n # fresh-session deny-degraded retry (initialize + ledger_add, each\n # bounded by `auditRetryTimeoutMs(5000)` ~=1.25s) is ~10.8-13.75s. If\n # Claude Code's outer timeout fires FIRST, it kills the subprocess\n # before the deny JSON reaches stdout \u2014 a hook that Claude Code cannot\n # read in time is treated as ALLOW, silently turning a computed,\n # fail-closed `deny`/`deny-degraded` verdict into an unintended\n # fail-open one. 15000ms clears the measured worst case with margin,\n # for BOTH the evidence-requiring policies (their own query+retry) AND\n # the pure pattern-deny policies (bounded lower, ~1x\n # health.timeout_ms=~5s via the mandatory record() alone, but still\n # above the pre-fix 1-2s floor).\n # 2. DEDUP-SAFETY. `generate-settings.ts`'s `buildGroups` collapses\n # multiple manifest hooks that share the same `match` (settings.json's\n # tool-name matcher \u2014 `bash_match` is NOT projected there) AND the same\n # `(command, timeout)` fingerprint into ONE settings.json hook entry,\n # specifically so Claude Code spawns `harness policy intercept` ONCE\n # per matching tool call instead of once per manifest hook name (the\n # comment on `buildGroups` names this explicitly: avoiding \"redundant\n # Node bootstraps and ledger queries per tool call\"). All nine\n # `match: \"Bash\"` hooks below share one settings.json matcher group;\n # giving them a NON-uniform budget_ms would make their computed\n # `timeout` values diverge, split that one group into several entries,\n # and reintroduce exactly the redundant-invocation cost the dedup\n # exists to avoid \u2014 on top of leaving whichever entry keeps a low\n # timeout still exposed to the fail-open risk above. Keeping all\n # eleven at the identical 15000ms budget_ms preserves the existing\n # one-invocation-per-matcher-group collapse (previously they all\n # collapsed onto the shared 2s floor; now they collapse onto 15s).\n - name: require-review-evidence\n event: PreToolUse\n match: \"mcp__agent-tasks__pull_requests_merge\"\n command: harness policy intercept\n blocking: hard\n budget_ms: 15000\n\n # Tool-agnostic parallel of require-review-evidence for operators on the\n # gh-cli workflow (`gh pr merge`) instead of agent-tasks MCP. Same generic\n # `harness policy intercept` entrypoint; the matching review-before-merge-bash\n # policy below picks up the trigger. A PolicyTrigger can only AND-match one\n # surface (MCP tool-name OR Bash command), so two parallel definitions are\n # the minimum-scope way to cover both PR surfaces without bumping the schema.\n - name: require-review-evidence-bash\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S+\\s+)*gh pr merge\\b'\n command: harness policy intercept\n blocking: hard\n budget_ms: 15000\n\n - name: require-dogfood-evidence\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S+\\s+)*(npm publish\\b|git( -C \\S+)* tag v)'\n command: harness policy intercept\n blocking: hard\n budget_ms: 15000\n\n - name: require-preflight-evidence\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S+\\s+)*git( -C \\S+)* (status|log|diff|branch)\\b'\n command: harness policy intercept\n blocking: hard\n budget_ms: 15000\n\n - name: require-review-subagent-evidence\n event: PreToolUse\n match: \"mcp__agent-tasks__pull_requests_create\"\n command: harness policy intercept\n blocking: hard\n budget_ms: 15000\n\n # Bash-surface parallel of require-review-subagent-evidence for operators\n # who open PRs with `gh pr create` instead of agent-tasks MCP. The matching\n # review-subagent-before-pr-create-bash policy below tags by branch\n # (`review-subagent:${BRANCH}`) because no task UUID is in `gh pr create`\n # arguments; the working branch is the closest stable handle for \"the\n # PR-in-progress\" at this point in the cycle.\n - name: require-review-subagent-evidence-bash\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S+\\s+)*gh pr create\\b'\n command: harness policy intercept\n blocking: hard\n budget_ms: 15000\n\n - name: require-preflight-push-evidence\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S+\\s+)*git( -C \\S+)* push\\b'\n command: harness policy intercept\n blocking: hard\n budget_ms: 15000\n\n # deny-kill-switch-bash / deny-session-env-strip-bash / deny-sentinel-write-bash\n # (task cf1fde6d): `harness pause` / `harness resume` refuse to run inside an\n # agent shell in CODE, but that CLI check is a speed bump, not a boundary: a\n # Claude Code `! `-prefixed shell inherits this session's env AND its non-TTY\n # stdin, so it is indistinguishable from an agent Bash call and trips the same\n # check the agent would trip (verified live; see\n # docs/okf/pause-vs-gate-kill-switch.md). The actual enforcement point that\n # runs BEFORE the agent's Bash executes is this PreToolUse deny-policy layer.\n # These three hooks/policies below deny, from Bash: the kill-switch verbs\n # themselves, the env-stripping wrappers that would otherwise defeat\n # `refuseIfAgentShell`, and direct writes to the unsigned pause sentinel file.\n - name: deny-kill-switch-bash\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S*\\s+)*(?:npx\\s+|\\S*/)?harness\\s+([\"\\x27]?)(?:pause|resume|gate\\s+([\"\\x27]?)(?:disable|enable)\\4)\\3(?![\\w-])'\n command: harness policy intercept\n blocking: hard\n # 15000, not a smaller pattern-only budget: see the budget note above\n # require-review-evidence. This policy's own verdict (operator_only)\n # needs no ledger read, but intercept()'s audit-record call for that\n # verdict is still a live grounding-mcp round-trip on the critical\n # path, AND this hook shares its settings.json matcher group with the\n # evidence-requiring Bash hooks above (dedup-safety).\n budget_ms: 15000\n\n - name: deny-session-env-strip-bash\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S*\\s+)*(env\\b[^;\\n|&]*-u\\s*(CLAUDE_CODE_SESSION_ID|CLAUDE_SESSION_ID|CODEX_SESSION_ID)\\b|env\\b[^;\\n|&]*--unset(?:=|\\s+)(CLAUDE_CODE_SESSION_ID|CLAUDE_SESSION_ID|CODEX_SESSION_ID)\\b|unset\\s+(\\S+\\s+)*(CLAUDE_CODE_SESSION_ID|CLAUDE_SESSION_ID|CODEX_SESSION_ID)\\b|(CLAUDE_CODE_SESSION_ID|CLAUDE_SESSION_ID|CODEX_SESSION_ID)=(?=\\s))'\n command: harness policy intercept\n blocking: hard\n # 15000: same rationale as deny-kill-switch-bash immediately above.\n budget_ms: 15000\n\n # Known gap, deliberately not faked as coverage: this only catches the\n # obvious shell shapes (`> .harness-paused`, `tee .harness-paused`,\n # `cp ... .harness-paused`). A regex over the raw command string cannot\n # see through a heredoc, `sh -c '...'`, `bash -lc`, `eval`,\n # `python -c`, base64-decoded payloads, an fs.write call inside a script\n # file the agent creates and then executes, a symlink swap, or the\n # sentinel-write equivalents of `cp`/`>` done via `mv`, `ln`,\n # `install`, or `dd`. These are NOT covered on purpose (regex whack-a-\n # mole against every file-write-capable tool does not meaningfully close\n # this class); closing it for real needs either signing the sentinel\n # (HMAC) or a filesystem-level write guard, neither of which is in scope\n # here; both are noted as follow-ups.\n - name: deny-sentinel-write-bash\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S*\\s+)*(tee|cp)\\b[^;\\n|&]*\\.harness-paused\\b|>{1,2}\\s*\\S*\\.harness-paused\\b'\n command: harness policy intercept\n blocking: hard\n # 15000: same rationale as deny-kill-switch-bash above.\n budget_ms: 15000\n\n # risk-gate (Phase 7 #6): the Risk Gate enforcement hook. The\n # gate-prod-destructive policies below reference it. Same generic\n # `harness policy intercept` entrypoint as every other policy hook;\n # the interceptor builds the Action Envelope, classifies risk against\n # `risk.classifiers[]`, resolves the environment against\n # `environments.resolvers[]`, and evaluates the policies' `when:`.\n - name: risk-gate\n event: PreToolUse\n match: \"Bash\"\n command: harness policy intercept\n blocking: hard\n # 15000: same rationale as the budget note above require-review-evidence.\n budget_ms: 15000\n\n # Optional: runtime-reality drift gate (NOT enabled by default).\n # Blocks destructive runtime commands (compose down/restart, systemctl,\n # kill/pkill, ./deploy-*) when the live process state has drifted from what\n # your expectations file says should be running. Left COMMENTED on purpose:\n # the hook is host-coupled and, without RUNTIME_REALITY_KEYWORD + an\n # expectations file + RUNTIME_REALITY_PROBE_CMD, degrades silently to allow,\n # a no-op that looks like protection. To arm it, uncomment the entry and\n # fill in the three env values. The expectations-file format and how to\n # install the probe are documented in docs/runtime-reality-hook.md.\n #\n # - name: runtime-reality\n # event: PreToolUse\n # command: >-\n # RUNTIME_REALITY_KEYWORD=<your-stack>\n # RUNTIME_REALITY_EXPECTATIONS_DIR=$HOME/.runtime-reality/expectations\n # RUNTIME_REALITY_PROBE_CMD=\"node $HOME/.runtime-reality/probes/runtime-reality-docker-probe.mjs\"\n # harness pack hook runtime-reality\n # blocking: hard\n # description: Block destructive runtime commands on critical process drift\n\npolicies:\n - name: review-before-merge\n description: Block PR merges unless a ledger entry tagged review:<pr-number> exists for this session.\n trigger:\n event: PreToolUse\n match: \"mcp__agent-tasks__pull_requests_merge\"\n extract:\n PR_NUMBER: \"toolArgs.prNumber\"\n requires:\n ledger_tag: \"review:${PR_NUMBER}\"\n hook: require-review-evidence\n enforcement: block\n producers:\n - kind: mcp\n verb: mcp__grounding-mcp__ledger_add\n example: '{sessionId:\"${SESSION_ID}\", type:\"fact\", content:\"review:${PR_NUMBER} \u2014 <verdict + key findings + nits>\", source:\"Agent(general-purpose) review\"}'\n description: Spawn a review subagent against the PR diff, capture its verdict, then persist a ledger entry tagged with the PR number. The content should be self-contained enough for an auditor to read without re-opening the chat.\n ux:\n cannot: \"You cannot merge PR #${PR_NUMBER} yet.\"\n required:\n - \"a recorded review of PR #${PR_NUMBER}\"\n run:\n - 'harness record review --pr ${PR_NUMBER} \"<summary>\"'\n\n # Bash-surface parallel of review-before-merge for operators on the gh-cli\n # workflow. Two scope notes:\n # 1. Tag shape: `review:${BRANCH}` instead of `review:${PR_NUMBER}`. The\n # `gh pr merge` invocation can target the PR by number, by URL, or by\n # the current branch (default), and PR_NUMBER is not extractable from\n # `tool_input.command` with today's JSONPath-only extract DSL. BRANCH\n # is the stable identifier the producer can record at review time.\n # 2. This sits ALONGSIDE review-before-merge \u2014 not as a replacement. An\n # operator using both surfaces (e.g. agent-tasks MCP for most repos\n # + gh-cli for a quick hotfix) will have both gates active, each with\n # its own tag shape, which is semantically honest.\n - name: review-before-merge-bash\n description: Block `gh pr merge` unless a ledger entry tagged review:<branch> exists for this session.\n trigger:\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S+\\s+)*gh pr merge\\b'\n requires:\n ledger_tag: \"review:${BRANCH}\"\n hook: require-review-evidence-bash\n enforcement: block\n producers:\n - kind: mcp\n verb: mcp__grounding-mcp__ledger_add\n example: '{sessionId:\"${SESSION_ID}\", type:\"fact\", content:\"review:${BRANCH} \u2014 <verdict + key findings + nits>\", source:\"Agent(general-purpose) review\"}'\n description: Spawn a review subagent against the branch diff, capture its verdict, then persist a ledger entry tagged with the branch name. Mirror of the review-before-merge producer for the gh-cli surface.\n ux:\n cannot: \"You cannot merge the PR for branch ${BRANCH} via `gh pr merge` yet.\"\n required:\n - \"a recorded review of the PR for branch ${BRANCH}\"\n run:\n - 'harness record review --pr <pr> \"<summary>\"'\n\n - name: dogfood-before-release\n description: Block npm publish / git tag v* without a recent dogfood ledger entry.\n trigger:\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S+\\s+)*(npm publish\\b|git( -C \\S+)* tag v)'\n requires:\n ledger_tag: \"dogfood:${SESSION_ID}\"\n within: 24h\n hook: require-dogfood-evidence\n enforcement: block\n producers:\n - kind: mcp\n verb: mcp__grounding-mcp__ledger_add\n example: '{sessionId:\"${SESSION_ID}\", type:\"fact\", content:\"dogfood:${SESSION_ID} \u2014 <end-to-end smoke summary against the live system>\", source:\"manual smoke test\"}'\n description: Before tagging or publishing, run the release path end-to-end against the live system (not just unit tests) and persist the result as a session-tagged ledger entry. Document what you exercised (install, CLI happy path, MCP handshake, etc.) so a future auditor can tell whether the smoke covered the change.\n ux:\n cannot: \"You cannot publish a release yet.\"\n required:\n - \"an end-to-end dogfood run in this session\"\n run:\n - 'harness record dogfood \"<was wurde real ausprobiert>\"'\n\n - name: two-reviewers-required\n description: At least two distinct reviewer ledger entries must exist for the PR.\n trigger:\n event: PreToolUse\n match: \"mcp__agent-tasks__pull_requests_merge\"\n extract:\n PR_NUMBER: \"toolArgs.prNumber\"\n requires:\n ledger_tag: \"review:${PR_NUMBER}\"\n count:\n min: 2\n hook: require-review-evidence\n enforcement: warn\n producers:\n - kind: mcp\n verb: mcp__grounding-mcp__ledger_add\n example: '{sessionId:\"${SESSION_ID}\", type:\"fact\", content:\"review:${PR_NUMBER} \u2014 <verdict + key findings + nits>\", source:\"Agent(general-purpose) review (reviewer 2)\"}'\n description: Same shape as review-before-merge but TWO DISTINCT reviewer entries must exist before the gate is satisfied (count.min 2). Distinguish reviewers by source so the count is honest. Warn-level enforcement, so the agent CAN merge with one reviewer but should consider spawning a second for load-bearing changes.\n\n - name: preflight-before-investigation\n description: Block investigative git reads (status/log/diff/branch) when agent-preflight has not run recently with ready:true for the current repo.\n trigger:\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S+\\s+)*git( -C \\S+)* (status|log|diff|branch)\\b'\n requires:\n ledger_tag: \"preflight:${REPO}\"\n within: 1h\n hook: require-preflight-evidence\n enforcement: block\n producers:\n - kind: bash\n command: harness session-start preflight\n description: Runs agent-preflight against the current cwd; on ready:true, records preflight:${REPO} to the ledger. Standard producer.\n - kind: mcp\n verb: mcp__grounding-mcp__ledger_add\n example: '{sessionId:\"${SESSION_ID}\", type:\"fact\", content:\"preflight:${REPO}\", source:\"manual\"}'\n description: Direct ledger write. Use when the Bash hook is locked down (e.g. understanding-gate active) or when the standard producer is unavailable.\n ux:\n cannot: \"You cannot investigate this repository yet.\"\n required:\n - \"verified repository preflight\"\n - \"an approved Understanding Report, if the Understanding Gate is still active (it blocks `harness preflight` itself)\"\n run:\n - \"harness preflight\"\n\n - name: review-subagent-before-pr-create\n description: Block agent-tasks PR creation unless a review-subagent ledger entry tagged for this task already exists. Forces the rigorous review BEFORE the PR opens, not after.\n trigger:\n event: PreToolUse\n match: \"mcp__agent-tasks__pull_requests_create\"\n extract:\n TASK_ID: \"toolArgs.taskId\"\n requires:\n ledger_tag: \"review-subagent:${TASK_ID}\"\n hook: require-review-subagent-evidence\n enforcement: block\n producers:\n - kind: mcp\n verb: mcp__grounding-mcp__ledger_add\n example: '{sessionId:\"${SESSION_ID}\", type:\"fact\", content:\"review-subagent:${TASK_ID} \u2014 <verdict + key findings + nits>\", source:\"Agent(general-purpose) review\"}'\n description: After running a review subagent against the staged diff, persist its verdict + load-bearing findings as a ledger entry tagged with the task UUID. The content should be self-contained enough to audit later without re-reading the chat.\n ux:\n cannot: \"You cannot open a pull request for task ${TASK_ID} yet.\"\n required:\n - \"a completed review-subagent pass on this task\"\n run:\n - 'harness record review-subagent --task ${TASK_ID} --verdict <verdict>'\n\n # Bash-surface parallel of review-subagent-before-pr-create. Tag shape is\n # `review-subagent:${BRANCH}` because TASK_ID is an agent-tasks-only\n # concept; for the gh-cli workflow the working branch is the closest stable\n # handle for \"the PR-in-progress\" at this point. Same rationale as\n # review-before-merge-bash: sits alongside the MCP variant, not as a\n # replacement.\n - name: review-subagent-before-pr-create-bash\n description: Block `gh pr create` unless a review-subagent ledger entry tagged review-subagent:<branch> exists for this session. Forces the rigorous review BEFORE the PR opens.\n trigger:\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S+\\s+)*gh pr create\\b'\n requires:\n ledger_tag: \"review-subagent:${BRANCH}\"\n hook: require-review-subagent-evidence-bash\n enforcement: block\n producers:\n - kind: mcp\n verb: mcp__grounding-mcp__ledger_add\n example: '{sessionId:\"${SESSION_ID}\", type:\"fact\", content:\"review-subagent:${BRANCH} \u2014 <verdict + key findings + nits>\", source:\"Agent(general-purpose) review\"}'\n description: After running a review subagent against the staged diff for the working branch, persist its verdict + load-bearing findings as a ledger entry tagged with the branch name. Mirror of the review-subagent-before-pr-create producer for the gh-cli surface.\n ux:\n cannot: \"You cannot open a pull request for branch ${BRANCH} via `gh pr create` yet.\"\n required:\n - \"a completed review-subagent pass on branch ${BRANCH}\"\n run:\n - 'harness record review-subagent --task <task-id> --verdict <verdict>'\n\n - name: preflight-before-push\n description: Block git push unless a fresh preflight ledger entry exists for the current branch. Catches the stale-checkout class of incident at the last reversible step.\n trigger:\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S+\\s+)*git( -C \\S+)* push\\b'\n requires:\n ledger_tag: \"preflight:${BRANCH}\"\n within: 10m\n # at_head:true lets a preflight at the current HEAD satisfy the\n # gate at any age (the standard producer writes head:<sha> into\n # the tag content). The 10m window remains the freshness ceiling\n # for the head-mismatch case (operator switched branch, preflight\n # predates HEAD shift, runtime couldn't resolve a sha).\n at_head: true\n hook: require-preflight-push-evidence\n enforcement: block\n producers:\n - kind: bash\n command: harness session-start preflight\n description: Runs agent-preflight against the current cwd; on ready:true, records preflight:${BRANCH} ready:true confidence:<n> head:<sha> to the ledger. Standard producer.\n - kind: mcp\n verb: mcp__grounding-mcp__ledger_add\n example: '{sessionId:\"${SESSION_ID}\", type:\"fact\", content:\"preflight:${BRANCH} head:<full-sha> \u2014 <summary of what is on the branch + smoke results>\", source:\"manual\"}'\n description: Direct ledger write. Include head:<full-sha> if you want the entry to count under at_head; the branch is the WIP review surface and the content should summarise what is staged + the smoke evidence so a reviewer can audit later without re-reading the chat.\n ux:\n cannot: \"You cannot push branch ${BRANCH} yet.\"\n required:\n - \"a preflight for ${BRANCH} at the current HEAD (any age) OR any preflight within the last 10 minutes. Re-run `harness preflight` if you committed since the last preflight AND it has been more than 10 minutes.\"\n - \"if solution-acceptance is enabled, a ready HEAD-pinned verdict at the SAME commit too (run `solution_evaluate`). `git push` trips both gates, so commit first if the tree is dirty, then satisfy both at one HEAD.\"\n - \"an approved Understanding Report, if the Understanding Gate is still active (it blocks `harness preflight` itself)\"\n run:\n - \"harness preflight\"\n\n # Phase 7 Risk Gate \u2014 the canonical built-in worked example. These two\n # policies, with the dangerous-shell classifier and production-signals\n # resolver below, are the Risk Gate's default stance: a destructive\n # shell action whose target environment resolves to production is\n # gated before the runtime fires it. Both fire ONLY when the\n # environment resolves to production (a main / release branch, a\n # prod-looking DATABASE_URL, or a prod kube context); on an ordinary\n # feature branch the environment is unknown and neither fires. Ordered\n # deny-first so a critical action (which also matches the high\n # threshold) gets the hard-deny envelope. See docs/risk-gate.md.\n - name: gate-prod-destructive\n description: Deny critical-severity destructive shell actions against a production target.\n trigger:\n event: PreToolUse\n match: \"Bash\"\n when:\n risk.severity_at_least: critical\n environment.name: production\n requires:\n ledger_tag: \"risk-override:${SESSION_ID}\"\n hook: risk-gate\n enforcement: block\n # Operator-in-the-loop gate: the override tag is written by the\n # operator verb (ask semantics), not by the agent. See\n # writing-custom-policies.md, tripwire 4 (the trust model).\n producers:\n - kind: ask\n command: harness approve risk --force <reason>\n description: Deliberate operator override for a critical production mutation; run from the operator shell.\n - kind: mcp\n verb: mcp__grounding-mcp__ledger_add\n example: '{sessionId:\"${SESSION_ID}\", type:\"fact\", content:\"risk-override:${SESSION_ID} \u2014 operator-authorized <reason>\", source:\"operator\"}'\n description: Recovery path if the approve verb is unavailable; only meaningful when the OPERATOR authorizes the content.\n ux:\n cannot: \"You cannot run this critical destructive action against production.\"\n required:\n - \"a deliberate operator override: a critical production mutation has no benign reading\"\n run:\n - \"Choose a non-destructive alternative, or ask the OPERATOR to run the command themselves, outside the agent.\"\n - \"Operator override (deliberate): the OPERATOR runs `harness approve risk --force <reason>` from their own shell (`! ` prefix in Claude Code, with --i-am-the-operator to acknowledge a non-TTY invocation).\"\n - name: gate-prod-destructive-approval\n description: Require operator approval for high-severity destructive shell actions against a production target.\n trigger:\n event: PreToolUse\n match: \"Bash\"\n when:\n risk.severity_at_least: high\n environment.name: production\n requires:\n ledger_tag: \"risk-approved:${SESSION_ID}\"\n hook: risk-gate\n enforcement: require_approval\n producers:\n - kind: ask\n command: harness approve risk\n description: Operator approves this Risk Gate decision from their own shell.\n - kind: mcp\n verb: mcp__grounding-mcp__ledger_add\n example: '{sessionId:\"${SESSION_ID}\", type:\"fact\", content:\"risk-approved:${SESSION_ID} \u2014 operator-authorized\", source:\"operator\"}'\n description: Recovery path if the approve verb is unavailable; only meaningful when the OPERATOR authorizes the content.\n ux:\n cannot: \"You cannot run this destructive production action yet.\"\n required:\n - \"operator approval of this Risk Gate decision\"\n run:\n - \"harness approve risk\"\n\n # deny-kill-switch-bypass / deny-session-env-strip / deny-pause-sentinel-forgery\n # (task cf1fde6d): `harness pause`/`harness resume` refuse to run inside an\n # agent shell in CODE, but that CLI check (refuseIfAgentShell / refuseIfNonTTY\n # in src/cli/pause/index.ts) is a speed bump, not a boundary: a Claude Code\n # `! `-prefixed shell inherits this session's env AND its non-TTY stdin, so\n # it is indistinguishable from an agent Bash call and trips the same checks\n # (verified live; docs/okf/pause-vs-gate-kill-switch.md). This PreToolUse\n # layer is the enforcement point that actually runs BEFORE the agent's Bash\n # executes.\n #\n # UPDATE (task 2cc73f55, closes the gap the HONESTY NOTE below used to\n # document): these three now declare `operator_only: true` instead of a\n # requires.ledger_tag. Every requires:-based policy in this engine is, by\n # construction, satisfiable by the SAME agent it gates: nothing stops it\n # from calling mcp__grounding-mcp__ledger_add with content matching the\n # tag for its own session before running the denied command, which used to\n # flip deny to allow (src/runtime/intercept.ts, the satisfied-requires\n # branch). `operator_only: true` closes that specific hole: the policy\n # declares NO requires: at all, and intercept() short-circuits to an\n # unconditional deny before the requires pipeline runs \u2014 no ledger query,\n # no template substitution, so no ledger write, marker file, or flag can\n # ever flip it to allow. See docs/writing-custom-policies.md tripwire 4 and\n # the \"Operator-only unconditional deny\" section for the schema contract,\n # and tests/cli/init-full-template-kill-switch-deny.test.ts for a test that\n # forges every ledger-evidence shape available to intercept() and asserts\n # the block holds.\n #\n # RESIDUAL GAP (still open, out of scope for task 2cc73f55): bash_match is\n # still a regex over the raw command string, matched BEFORE evaluation ever\n # reaches operator_only:, so it still cannot see through a heredoc,\n # `sh -c '...'`, `eval`, a base64-decoded payload, or a script file the\n # agent writes and then executes \u2014 an agent that defeats the TRIGGER match\n # never reaches this deny at all. That is a bash_match coverage gap, not a\n # requires-satisfaction gap, and is unrelated to the fix here (see PR #341\n # for the known uncoverable shapes). `harness validate` no longer warns\n # \"declares no producers\" on these three (checkPolicySelfAttestation now\n # recognises operator_only: true as correct-by-construction).\n #\n # UPDATE (task 432db3d3, 2026-07-28, follow-up to ea8becf5): PARTIALLY\n # closed \u2014 for THIS policy only. `src/runtime/command-normalize.ts` now\n # peels wrapper prefixes (`env`, `nice`, `command`, `sudo`, `doas`,\n # `time`, `timeout`, `stdbuf`, `setsid`, leading VAR=value) and\n # collapses whitespace runs for the closed head-token set\n # `git`/`gh`/`npm`/`harness`, so `env harness pause`, `nice harness\n # gate disable` and the like now reach this deny via the raw-OR-normalised\n # match \u2014 measured 2026-07-28 against the real trigger regex through the\n # real hook entry point, pinned in tests/runtime/intercept-cli.test.ts.\n # The OTHER TWO kill switches are NOT covered by that closed set:\n # deny-session-env-strip keys on `env`/`unset` head tokens, and `env`\n # is STRUCTURALLY unreachable for the normaliser (its peeling loop\n # consumes `env -u <VAR>` while hunting for a gated command behind it,\n # erasing the very text this trigger keys on); deny-pause-sentinel-forgery\n # keys on `tee`/`cp`. Both remain wrapper-bypassable (`nice env -u\n # CLAUDE_CODE_SESSION_ID ...`, `nice tee <sentinel>`), measured\n # 2026-07-28 and pinned as explicit not-covered tests plus a head-token\n # drift guard (tests/runtime/bash-match-head-token-drift.test.ts) that\n # goes red when a future bash_match policy gates a new head token.\n - name: deny-kill-switch-bypass\n description: Deny harness pause, harness resume, harness gate disable, and harness gate enable from the agent's Bash tool. These are operator-only kill switches.\n trigger:\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S*\\s+)*(?:npx\\s+|\\S*/)?harness\\s+([\"\\x27]?)(?:pause|resume|gate\\s+([\"\\x27]?)(?:disable|enable)\\4)\\3(?![\\w-])'\n operator_only: true\n hook: deny-kill-switch-bash\n enforcement: block\n ux:\n cannot: \"You cannot run `harness pause`, `harness resume`, `harness gate disable`, or `harness gate enable`.\"\n required:\n - \"the OPERATOR to run this from a terminal genuinely outside your agent session, not a `! `-prefixed Claude Code shell, which inherits this session's env and stdin and does not count\"\n run:\n - \"Stop. Explain to the operator what you were trying to do and why, and let THEM decide whether to run that command from their own terminal. This is the only way to unblock this gate: it is an unconditional deny (operator_only: true) that no in-session evidence \u2014 a ledger write, a marker file, a flag \u2014 can ever satisfy; the only remaining gap is bash_match's regex coverage of exotic shell shapes, not this evaluation (see docs/okf/pause-vs-gate-kill-switch.md).\"\n\n - name: deny-session-env-strip\n description: Deny Bash attempts to strip or unset the agent-session env vars that harness pause/resume check ($CLAUDE_CODE_SESSION_ID, $CLAUDE_SESSION_ID, $CODEX_SESSION_ID). Closes the one-liner that otherwise defeats refuseIfAgentShell.\n trigger:\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S*\\s+)*(env\\b[^;\\n|&]*-u\\s*(CLAUDE_CODE_SESSION_ID|CLAUDE_SESSION_ID|CODEX_SESSION_ID)\\b|env\\b[^;\\n|&]*--unset(?:=|\\s+)(CLAUDE_CODE_SESSION_ID|CLAUDE_SESSION_ID|CODEX_SESSION_ID)\\b|unset\\s+(\\S+\\s+)*(CLAUDE_CODE_SESSION_ID|CLAUDE_SESSION_ID|CODEX_SESSION_ID)\\b|(CLAUDE_CODE_SESSION_ID|CLAUDE_SESSION_ID|CODEX_SESSION_ID)=(?=\\s))'\n operator_only: true\n hook: deny-session-env-strip-bash\n enforcement: block\n ux:\n cannot: \"You cannot strip or unset your own agent-session id env vars from Bash.\"\n required:\n - \"no legitimate reason from inside this session: stripping $CLAUDE_CODE_SESSION_ID / $CLAUDE_SESSION_ID / $CODEX_SESSION_ID is recognised as an attempt to defeat the harness pause/resume agent-shell check\"\n run:\n - \"Stop. If you believe you have a legitimate reason to run a command without one of these env vars set, ask the OPERATOR to do it from their own terminal instead. This gate is an unconditional deny (operator_only: true); see docs/okf/pause-vs-gate-kill-switch.md for the honest trust model and the residual bash_match coverage gap.\"\n\n - name: deny-pause-sentinel-forgery\n description: Deny Bash writes (redirect, tee, cp) to the harness pause sentinel file (.harness-paused). The sentinel is unsigned JSON; a forged file silences every gate exactly like a real operator pause. Reads (cat, stat) are unaffected.\n trigger:\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S*\\s+)*(tee|cp)\\b[^;\\n|&]*\\.harness-paused\\b|>{1,2}\\s*\\S*\\.harness-paused\\b'\n operator_only: true\n hook: deny-sentinel-write-bash\n enforcement: block\n ux:\n cannot: \"You cannot write to the harness pause sentinel file (.harness-paused).\"\n required:\n - \"the OPERATOR to run `harness pause`/`harness resume` themselves, from a terminal outside your agent session, never a direct write to the sentinel file\"\n run:\n - \"Stop. Do not write, redirect, tee, or copy anything to .harness-paused. Ask the OPERATOR to silence the gates themselves, from their own terminal, using the operator-only command named under Required, if the session genuinely needs it. This gate is an unconditional deny (operator_only: true); see docs/okf/pause-vs-gate-kill-switch.md for the honest trust model and the residual bash_match coverage gap.\"\n\n# Full inherits the Solo/Team understanding-gate stack: the Stop hook\n# persists each Understanding Report and the PreToolUse pre-tool-use\n# blocker refuses Edit/Write/Bash until the report is approved. Drop\n# this block if you want the reference policies above without the\n# baseline gate.\npolicy_packs:\n - name: understanding-before-execution\n source: builtin\n enabled: true\n description: Force agents to expose their task interpretation and wait for explicit human approval before any write-capable tool fires.\n config:\n mode: grill_me\n # Producers (agent-tasks/25bced52): rendered into the gate's deny\n # envelope by the same engine as policy producers. Constraint at\n # this layer: at-least-one `ask`. Post-v0.14.0 the gate signal\n # is a filesystem marker and the mcp ledger_add path no longer\n # satisfies the gate; the canonical unblock surface is the\n # operator-approval prompt.\n #\n # KEEP IN SYNC (task 68b9ad9c): this text must match\n # defaultProducers() in\n # src/policy-packs/builtin/understanding-before-execution.ts \u2014\n # that function is what `harness pack reseed` and `harness doctor`'s\n # divergence warning treat as \"the shipped template\". A wording fix\n # landed here without updating defaultProducers() would make reseed\n # silently pull operators BACK to the stale wording. Pinned by\n # tests/cli/init-templates-ux-parity.test.ts.\n producers:\n - kind: ask\n command: harness approve understanding\n description: \"Bare command, no pipes or chaining. The hook recognises it via isEscapeCommand and emits permissionDecision:ask; the operator's go on that prompt IS the gate approval. Golden path.\"\n - kind: bash\n command: harness approve understanding\n description: Same command from any un-hooked terminal (operator only, not reachable from inside the gated session). Writes the canonical marker at harness.generated/.approvals/${SESSION_ID}.\n # ux (agent-tasks/e48e3b45): replaces the legacy engine-vocabulary\n # deny envelope with the plain-language { cannot, required, run }\n # shape. Engine details (the BLOCK reason naming session id /\n # marker / report state) still land in stderr for operator audit;\n # the agent only sees this.\n #\n # KEEP IN SYNC (task 68b9ad9c): see the producers: comment above \u2014\n # same rationale applies here, against defaultUx(\"grill_me\") in the\n # same builtin module.\n ux:\n cannot: \"You cannot use write-capable tools yet.\"\n required:\n - \"an approved Understanding Report for this session\"\n run:\n - \"Write an Understanding Report covering: Current Understanding, Intended Outcome, Derived Todos, Acceptance Criteria, Assumptions, Open Questions, Out Of Scope, Risks, Verification Plan, Prior Art (state what you searched for an existing solution and what you found, with an explicit adopt-or-build judgment)\"\n - \"Run `harness approve understanding` with the report attached as a quoted heredoc (harness approve understanding <<'UNDERSTANDING_REPORT' ...report... UNDERSTANDING_REPORT) so it is persisted for audit, then approve the prompt; the heredoc is the only extra shell shape the gate allows (no pipes, chaining, or other redirection)\"\n # approval_lifecycle (agent-tasks/d8ee60ca + harness/f54e0ecb,\n # v0.18.0+): expire the approval marker on task-completion\n # boundaries so a multi-task session re-prompts for an\n # Understanding Report between tasks. Without this the legacy\n # \"one approval per session\" contract lets a stale interpretation\n # drive the next task's edits.\n #\n # Full ships both boundary kinds: the agent-tasks MCP verbs for\n # operators on that workflow, plus a Bash regex list for hybrid\n # operators who also use gh-cli for PR mechanics. `max_age` is\n # the safety net. Operators who prefer the legacy per-session\n # behaviour opt out with `approval_lifecycle: { mode: session }`.\n # Operators on other task systems override the matchers.\n approval_lifecycle:\n expire_on_tool_match:\n - mcp__agent-tasks__task_finish\n - mcp__agent-tasks__task_abandon\n - mcp__agent-tasks__pull_requests_merge\n - mcp__agent-tasks__tasks_transition\n expire_on_bash_match:\n - '^gh pr (merge|close)\\b'\n - '^git push origin (master|main)\\b'\n max_age: 4h\n\n # branch-protection (agent-tasks/2fdc5bbe, default-enabled since v0.17.2):\n # blocks Write/Edit (claude-code) or apply_patch (codex) on protected\n # branches (default: master, main, develop). Complements\n # preflight-before-push, which fires at the LAST reversible step;\n # branch-protection fires at the FIRST source mutation, catching the\n # \"forgot to branch off master\" pattern earlier in the cycle.\n #\n # Two satisfying signals: a fresh `branch:non-protected:<branch>` tag\n # from the SessionStart producer (`harness session-start branch-check`),\n # or the operator-only override marker written by\n # `harness approve branch-protection --session <id>` for deliberate\n # protected-branch edits (version bumps, CI workflow patches, hotfixes).\n # A branch-protection-ack ledger tag is no longer a sufficient override\n # on its own (it is agent-writable); the marker file is the trusted signal.\n #\n # Fails closed (any load / parse / ledger error refuses). Disable by\n # setting `enabled: false` or removing this entry if your workflow\n # routinely edits master directly. Override the protected list via\n # `config.protected_branches`. Full reference:\n # docs/policy-packs/branch-protection.md.\n - name: branch-protection\n source: builtin\n enabled: true\n description: Block Write/Edit on protected branches (master, main, develop) at the first source mutation.\n config:\n # ux (agent-tasks/9806d4f8): replaces the legacy\n # \"branch-protection: refusing ...\" envelope with the\n # plain-language { cannot, required, run } shape. Engine details\n # (the BLOCK reason naming session id / freshness window) stay\n # on stderr for operator audit.\n #\n # KEEP IN SYNC (task 68b9ad9c): this text must match defaultUx() in\n # src/policy-packs/builtin/branch-protection.ts \u2014 see the identical\n # note on the understanding-before-execution pack above for why.\n # Pinned by tests/cli/init-templates-ux-parity.test.ts.\n ux:\n cannot: \"You cannot edit files on protected branch ${BRANCH} yet.\"\n required:\n - \"a checkout of a non-protected branch (current `${BRANCH}` is protected)\"\n run:\n - \"git checkout -b feat/<your-task>\"\n - \"harness session-start branch-check\"\n\n # solution-acceptance (harness cc43c7a4): Verifier-gated Done. Gates the\n # task-finishing tools (agent-tasks completion verbs + git push / gh pr\n # merge) on a ready, HEAD-pinned verdict that the grounding-mcp producer\n # (solution_evaluate) derives from a real preflight run, plus an\n # anti-forgery write-guard on the verdict marker. DISABLED by default: it\n # is a hard completion-gate and needs grounding-mcp (>= 0.3.2) under\n # tools.mcp plus the preflight binary on PATH. Flip enabled: true once the\n # producer is wired and you want completion earned, not claimed. The pack\n # emits its own instructions.md on apply; harness validate warns if you\n # enable it without the producer.\n - name: solution-acceptance\n source: builtin\n enabled: false\n description: Gate task completion on a ready, HEAD-pinned solution-acceptance verdict earned from a real preflight run.\n\n # post-merge-gate (harness T-001, post-merge-gate): catches \"kept working\n # on a branch after it was already merged\" \u2014 a PostToolUse producer\n # records a merged-tip fact on a successful `gh pr merge`; a PreToolUse\n # blocker denies curated history-mutating commands (git commit/add/push/\n # merge/rebase/cherry-pick/revert/reset/stash pop|apply, gh pr create/\n # merge) while the current branch tip still exactly matches that\n # recorded tip. The recovery vocabulary (git switch/checkout/pull/fetch,\n # git branch -d/-D, git stash list/show, any `harness ...` command)\n # always passes as its OWN command, classified before any manifest or\n # ledger access \u2014 the recovery path this gate itself recommends can never\n # be starved. Chaining one of those with a curated mutation does NOT\n # exempt the mutation (task 19356be7).\n # Fails OPEN (unlike branch-protection) when the ledger is unreachable.\n # DISABLED by default: a fresh gate, opt in once you've reviewed the\n # curated command list for your workflow. Full reference:\n # docs/policy-packs/post-merge-gate.md.\n - name: post-merge-gate\n source: builtin\n enabled: false\n description: Deny curated history-mutating git/gh commands on a branch whose current tip was already merged.\n config:\n # ux: replaces the legacy \"post-merge-gate: refusing ...\" envelope\n # with the plain-language { cannot, required, run } shape.\n #\n # KEEP IN SYNC: this text must match defaultUx() in\n # src/policy-packs/builtin/post-merge-gate.ts \u2014 see the identical\n # note on the branch-protection pack above for why. Pinned by\n # tests/cli/init-templates-ux-parity.test.ts.\n ux:\n cannot: \"You cannot run ${TOOL_NAME} on branch ${BRANCH} yet \u2014 its current tip was already merged.\"\n required:\n - \"a branch tip that is not sitting at an already-merged commit (switch off `${BRANCH}`, or move its tip with a new commit)\"\n run:\n - \"git switch ${DEFAULT_BRANCH}\"\n - \"git pull --ff-only\"\n - \"git branch -d ${BRANCH} # optional cleanup\"\n\n# Phase 7 Risk Gate vocabulary. The dangerous-shell classifier and\n# production-signals resolver feed the gate-prod-destructive policies\n# above: `harness policy intercept` builds the Action Envelope,\n# classifies the action against `risk.classifiers[]`, resolves the\n# target environment against `environments.resolvers[]`, and evaluates\n# each policy's `when:` clauses against the result. Full design and the\n# decision model: docs/risk-gate.md.\nrisk:\n # Fail posture when a policy's evidence cannot be READ (ledger timeout,\n # spawn failure, unresolved template): with `preserve_enforcement`\n # (the default) block/require_approval policies fail CLOSED\n # (`deny-degraded`) while warn policies stay non-blocking. Set\n # `fail_open` to restore the pre-0.45 availability-first behaviour\n # where EVERY degraded evaluation was a non-blocking `warn-degraded`.\n # Kept COMMENTED OUT on purpose: the schema is strict, so a manifest\n # carrying this key fails to parse on a pre-0.45 binary, and a manifest\n # load failure is ALLOW at the hook layer \u2014 on a mixed-version fleet an\n # emitted default would turn a downgrade into a silent full fail-open\n # (review 2026-08-08). See docs/okf/gate-fail-posture-matrix.md.\n # degraded_fail_posture: preserve_enforcement\n classifiers:\n - name: dangerous-shell\n tool: Bash\n patterns:\n - pattern: 'rm\\s+-rf\\s+(/|/var|/data|/mnt|~)'\n categories: [destructive, data_loss]\n severity: critical\n - pattern: 'DROP\\s+TABLE|TRUNCATE\\s+TABLE|DELETE\\s+FROM'\n categories: [destructive, data_loss]\n severity: high\n - pattern: 'kubectl\\s+delete\\s+(namespace|deployment|statefulset|pvc)'\n categories: [destructive, infrastructure_change]\n severity: high\n - pattern: 'terraform\\s+destroy'\n categories: [destructive, infrastructure_change]\n severity: critical\n\nenvironments:\n resolvers:\n - name: production-signals\n environment: production\n signals:\n branch_patterns: [main, \"release/*\"]\n env_var_patterns:\n - var: DATABASE_URL\n patterns: [prod, production]\n kube_context_patterns: [\".*prod.*\"]\n kube_namespace_patterns: [prod, production]\n";
2
+ export declare const FULL_TEMPLATE = "# ~/.harness/harness.yaml (legacy: ~/.claude/harness.yaml)\n#\n# Bootstrapped by `harness init --template full`. The reference manifest:\n# every example policy from docs/examples/full-manifest.yaml wired through\n# the generic `harness policy intercept` engine, so no external shell\n# scripts under ~/.claude/hooks/ are required.\n#\n# Canonical source for the policy + policy_packs sections is\n# docs/examples/full-manifest.yaml. A parity vitest\n# (tests/cli/init-full-template-parity.test.ts) fails the build if the\n# two diverge on policy names or load-bearing fields.\n#\n# What you still need on PATH (the wizard offers to `npm i -g` these on\n# init): agent-tasks-mcp-bridge, grounding-mcp, memory-router-*,\n# understanding-gate-claude-*.\n\nversion: 1\n\ngrounding:\n session:\n auto_start: true\n id_format: \"gs-{repo}-{rand:8}\"\n evidence_ledger:\n path: ~/.evidence-ledger/ledger.db\n retention_days: 90\n policies_source: ~/.claude/harness.d/policies/claim-gate.yaml\n\ntools:\n mcp:\n # codebase-oracle (the Pandora RAG MCP server) is intentionally NOT\n # in the Full default. It is published as\n # `@lannguyensi/codebase-oracle` and works fine standalone, but it\n # is an opinionated workflow add-on (multi-repo semantic search)\n # rather than infrastructure harness itself assumes. Operators who\n # want it wire it explicitly:\n # npm i -g @lannguyensi/codebase-oracle\n # harness add mcp codebase-oracle --command codebase-oracle,mcp\n # Set ORACLE_SCAN_ROOT (absolute path; tilde is not expanded by the\n # MCP env block) and OPENAI_API_KEY (or switch providers via\n # ORACLE_LLM_PROVIDER) before the first call.\n - name: agent-tasks\n # Zero-setup entry: `@agent-tasks/mcp-bridge` exposes the\n # `agent-tasks-mcp-bridge` binary on PATH. The bridge owns token\n # storage and defaults to the hosted backend; override with\n # `AGENT_TASKS_BASE_URL` / `AGENT_TASKS_TOKEN` for self-hosted.\n # `min_version` floor: 0.6.0 added the `--version` short-circuit\n # the doctor probe needs (PR agent-tasks/240, release-cut PR 241).\n # Bump the floor whenever a fix you depend on lands; loose floors\n # are fine, the point is the drift signal not pinning a specific cut.\n command: [agent-tasks-mcp-bridge]\n min_version: \"0.6.0\"\n health:\n verb: projects_list\n timeout_ms: 5000\n enabled: true\n - name: grounding-mcp\n # Published bin from `@lannguyensi/grounding-mcp`. No env is set:\n # the bundled default resolves to `~/.evidence-ledger/ledger.db`\n # via os.homedir() at startup. Passing a literal tilde in env\n # bypasses shell expansion and creates rogue cwd-relative DB files\n # (see agent-tasks/42d224a6 incident). `min_version` floor: 0.2.0\n # added the `--version` short-circuit the doctor probe needs (PR\n # agent-grounding/76, release-cut PR 77).\n command: [grounding-mcp]\n min_version: \"0.2.0\"\n health:\n verb: ledger_status\n timeout_ms: 5000\n enabled: true\n\n cli:\n - name: gh\n binary: gh\n required: true\n\n skills:\n enabled:\n - simplify\n - init\n - review\n - security-review\n source_dirs:\n - ~/.claude/skills\n\n builtin:\n known: [Read, Edit, Write, Bash, Agent, Skill, TaskCreate, Glob, Grep]\n\nmemory:\n directories:\n - path: ~/.claude/projects/{project}/memory\n scope: project\n router:\n # Published bin from `@lannguyensi/memory-router`.\n # `min_version` floor: 0.3.0 added the `--version` short-circuit\n # the doctor probe needs (PR agent-memory/40, release-cut PR 41).\n command: [memory-router-user-prompt-submit]\n min_version: \"0.3.0\"\n enabled: true\n retention:\n staleness_days: 180\n broken_refs: warn\n scopes:\n default: project\n allowed: [project, user]\n\n# All PreToolUse hooks share the generic `harness policy intercept` CLI\n# entrypoint. The engine reads the tool event on stdin, evaluates whichever\n# policy below has a matching trigger (`match` + optional `bash_match`),\n# and emits Claude Code's deny envelope when the required ledger tag is\n# absent. No external shell scripts are required.\n#\n# The `git-preflight` SessionStart hook is the producer side of the\n# `preflight-before-*` policies: `harness session-start preflight` runs\n# agent-preflight against the session cwd and, on a ready:true result,\n# records `preflight:${REPO}` to the evidence ledger. It needs the\n# `preflight` binary on PATH (`npm i -g @lannguyensi/agent-preflight`); when\n# that is absent the hook logs to stderr and exits 0, so the session is\n# never broken \u2014 the preflight gates just stay closed until a tag is\n# produced some other way.\nhooks:\n - name: git-preflight\n event: SessionStart\n command: harness session-start preflight\n blocking: false\n # 70s budget gives the wrapped preflight (default 60s) headroom plus\n # ledger-write time. Was 30s through v0.17.4, but a healthy preflight\n # on a medium-size repo takes ~28s and the old 25s wrapper ceiling\n # blew through it. Bumped together with DEFAULT_PREFLIGHT_TIMEOUT_MS\n # (agent-tasks/7265599e).\n budget_ms: 70000\n # Floor at agent-preflight 0.2.0, the release that makes secret\n # detection git-aware and diff-scoped: a gitignored+untracked .env,\n # a .md doc, a non-git dir, or a secret in a tracked file the branch\n # never touched is a non-blocking warn, not a hard fail. Pre-0.2.0\n # installs hard-fail preflight on the normal correct state (a\n # gitignored .env holding real credentials), so this SessionStart\n # producer never writes a preflight: tag and the preflight-before-*\n # policies stay closed forever on any repo with a local .env. (0.1.1\n # had already fixed the wrapper-script \"tool not installed\" false\n # positive.) version_command points at the source-of-truth preflight\n # binary, not at the `harness session-start preflight` wrapper.\n min_version: \"0.2.0\"\n version_command: [\"preflight\", \"--version\"]\n\n # toolchain-parity (PATH-shim incident 2026-07-22 follow-up): writes THIS\n # machine's toolchain snapshot (node version, npm globals, OW-Kit\n # version, MCP server names) to `toolchain_parity.machine_state_dir`\n # and advisorily compares it against every peer machine's snapshot\n # already there, warning on drift (version mismatches, missing\n # packages, node/OW-Kit drift, MCP-name differences). Purely advisory \u2014\n # no policy consumes the `toolchain-parity:` ledger fact this writes,\n # it exists for `harness audit`/operator visibility only. If a policy\n # ever does consume it, match with the `:unparseable-peer:<n>` suffix\n # in mind: requires-matching is substring-based, so a bare\n # `toolchain-parity:ok` tag would also match `ok:unparseable-peer:2`\n # (a partial comparison). DISABLED by\n # default (no `toolchain_parity:` block above): opt in with\n # `toolchain_parity: { enabled: true }` (machine_state_dir/profile/\n # workspace_root all have sane defaults \u2014 see docs/CLI.md). No external\n # binary to floor-check (node/npm are assumed present already), so\n # unlike git-preflight this hook carries no min_version/version_command.\n - name: toolchain-parity\n event: SessionStart\n command: harness session-start toolchain-parity\n blocking: false\n # node --version + npm ls -g run in PARALLEL (bounded ~2s/~4s each, so\n # max(2s,4s)=4s worst case for that pair), plus two near-instant file\n # reads; the ledger write runs SEQUENTIALLY after those and carries its\n # own timeout floor (resolveManifestLedgerWriter's default 5s absent an\n # explicit grounding-mcp health.timeout_ms or a bound\n # SessionStartToolchainParityOptions.ledgerTimeoutMs). Worst case is\n # therefore additive, ~4s + 5s = 9s, not the sub-5s the collectors alone\n # would suggest \u2014 10s still clears it, but with only ~1s of margin, not\n # the \"comfortable\" headroom an earlier version of this comment claimed\n # (task c1b5ade5). Nowhere near git-preflight's 70s (which wraps a full\n # external test suite, a fundamentally heavier operation this hook\n # never performs).\n budget_ms: 10000\n\n # Budget note (task 7bf47554, follow-up to the ms/seconds unit fix\n # f2d2a29): every `harness policy intercept` hook below down through\n # `risk-gate` carries `budget_ms: 15000`, i.e. a Claude Code outer\n # kill-timeout of `ceil(15000/1000) = 15` seconds (generate-settings.ts's\n # `hookTimeoutSeconds`). This is deliberately UNIFORM across all eleven of\n # them, for two independent reasons:\n #\n # 1. FAIL-CLOSED MARGIN. `harness policy intercept` evaluates its FULL\n # `policies:` list against the incoming event (src/runtime/intercept.ts,\n # `intercept()`), not just the one named policy this hook happens to\n # also register. Every policy in that evaluation loop that reaches a\n # verdict has its decision written to the evidence ledger via\n # `options.ledger.record()` BEFORE `intercept()` returns and stdout is\n # flushed (intercept.ts ~L1362-1377) \u2014 this includes `operator_only`\n # policies (the three kill-switch denies below), whose VERDICT needs no\n # ledger read but whose AUDIT WRITE is still a live grounding-mcp\n # round-trip on the critical path. A `requires:`-based policy (the\n # require-*-evidence / risk-gate policies) additionally QUERIES the\n # ledger for its verdict (intercept.ts's `evaluateOnePolicy`,\n # ~L752-777) and, on a `deny-degraded` outcome, may retry the audit\n # write once more on a fresh session (`realLedgerClient`,\n # src/cli/policy/intercept.ts ~L276-306). Measured worst case with the\n # grounding-mcp health.timeout_ms=5000 default: query (~5s) + one\n # fresh-session deny-degraded retry (initialize + ledger_add, each\n # bounded by `auditRetryTimeoutMs(5000)` ~=1.25s) is ~10.8-13.75s. If\n # Claude Code's outer timeout fires FIRST, it kills the subprocess\n # before the deny JSON reaches stdout \u2014 a hook that Claude Code cannot\n # read in time is treated as ALLOW, silently turning a computed,\n # fail-closed `deny`/`deny-degraded` verdict into an unintended\n # fail-open one. 15000ms clears the measured worst case with margin,\n # for BOTH the evidence-requiring policies (their own query+retry) AND\n # the pure pattern-deny policies (bounded lower, ~1x\n # health.timeout_ms=~5s via the mandatory record() alone, but still\n # above the pre-fix 1-2s floor).\n # 2. DEDUP-SAFETY. `generate-settings.ts`'s `buildGroups` collapses\n # multiple manifest hooks that share the same `match` (settings.json's\n # tool-name matcher \u2014 `bash_match` is NOT projected there) AND the same\n # `(command, timeout)` fingerprint into ONE settings.json hook entry,\n # specifically so Claude Code spawns `harness policy intercept` ONCE\n # per matching tool call instead of once per manifest hook name (the\n # comment on `buildGroups` names this explicitly: avoiding \"redundant\n # Node bootstraps and ledger queries per tool call\"). All nine\n # `match: \"Bash\"` hooks below share one settings.json matcher group;\n # giving them a NON-uniform budget_ms would make their computed\n # `timeout` values diverge, split that one group into several entries,\n # and reintroduce exactly the redundant-invocation cost the dedup\n # exists to avoid \u2014 on top of leaving whichever entry keeps a low\n # timeout still exposed to the fail-open risk above. Keeping all\n # eleven at the identical 15000ms budget_ms preserves the existing\n # one-invocation-per-matcher-group collapse (previously they all\n # collapsed onto the shared 2s floor; now they collapse onto 15s).\n - name: require-review-evidence\n event: PreToolUse\n match: \"mcp__agent-tasks__pull_requests_merge\"\n command: harness policy intercept\n blocking: hard\n budget_ms: 15000\n\n # Tool-agnostic parallel of require-review-evidence for operators on the\n # gh-cli workflow (`gh pr merge`) instead of agent-tasks MCP. Same generic\n # `harness policy intercept` entrypoint; the matching review-before-merge-bash\n # policy below picks up the trigger. A PolicyTrigger can only AND-match one\n # surface (MCP tool-name OR Bash command), so two parallel definitions are\n # the minimum-scope way to cover both PR surfaces without bumping the schema.\n - name: require-review-evidence-bash\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S+\\s+)*gh pr merge\\b'\n command: harness policy intercept\n blocking: hard\n budget_ms: 15000\n\n - name: require-dogfood-evidence\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S+\\s+)*(npm publish\\b|git( -C \\S+)* tag v)'\n command: harness policy intercept\n blocking: hard\n budget_ms: 15000\n\n - name: require-preflight-evidence\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S+\\s+)*git( -C \\S+)* (status|log|diff|branch)\\b'\n command: harness policy intercept\n blocking: hard\n budget_ms: 15000\n\n - name: require-review-subagent-evidence\n event: PreToolUse\n match: \"mcp__agent-tasks__pull_requests_create\"\n command: harness policy intercept\n blocking: hard\n budget_ms: 15000\n\n # Bash-surface parallel of require-review-subagent-evidence for operators\n # who open PRs with `gh pr create` instead of agent-tasks MCP. The matching\n # review-subagent-before-pr-create-bash policy below tags by branch\n # (`review-subagent:${BRANCH}`) because no task UUID is in `gh pr create`\n # arguments; the working branch is the closest stable handle for \"the\n # PR-in-progress\" at this point in the cycle.\n - name: require-review-subagent-evidence-bash\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S+\\s+)*gh pr create\\b'\n command: harness policy intercept\n blocking: hard\n budget_ms: 15000\n\n - name: require-preflight-push-evidence\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S+\\s+)*git( -C \\S+)* push\\b'\n command: harness policy intercept\n blocking: hard\n budget_ms: 15000\n\n # deny-kill-switch-bash / deny-session-env-strip-bash / deny-sentinel-write-bash\n # (task cf1fde6d): `harness pause` / `harness resume` refuse to run inside an\n # agent shell in CODE, but that CLI check is a speed bump, not a boundary: a\n # Claude Code `! `-prefixed shell inherits this session's env AND its non-TTY\n # stdin, so it is indistinguishable from an agent Bash call and trips the same\n # check the agent would trip (verified live; see\n # docs/okf/pause-vs-gate-kill-switch.md). The actual enforcement point that\n # runs BEFORE the agent's Bash executes is this PreToolUse deny-policy layer.\n # These three hooks/policies below deny, from Bash: the kill-switch verbs\n # themselves, the env-stripping wrappers that would otherwise defeat\n # `refuseIfAgentShell`, and direct writes to the unsigned pause sentinel file.\n - name: deny-kill-switch-bash\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S*\\s+)*(?:npx\\s+|\\S*/)?harness\\s+([\"\\x27]?)(?:pause|resume|gate\\s+([\"\\x27]?)(?:disable|enable)\\4)\\3(?![\\w-])'\n command: harness policy intercept\n blocking: hard\n # 15000, not a smaller pattern-only budget: see the budget note above\n # require-review-evidence. This policy's own verdict (operator_only)\n # needs no ledger read, but intercept()'s audit-record call for that\n # verdict is still a live grounding-mcp round-trip on the critical\n # path, AND this hook shares its settings.json matcher group with the\n # evidence-requiring Bash hooks above (dedup-safety).\n budget_ms: 15000\n\n - name: deny-session-env-strip-bash\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S*\\s+)*(env\\b[^;\\n|&]*-u\\s*(CLAUDE_CODE_SESSION_ID|CLAUDE_SESSION_ID|CODEX_SESSION_ID)\\b|env\\b[^;\\n|&]*--unset(?:=|\\s+)(CLAUDE_CODE_SESSION_ID|CLAUDE_SESSION_ID|CODEX_SESSION_ID)\\b|unset\\s+(\\S+\\s+)*(CLAUDE_CODE_SESSION_ID|CLAUDE_SESSION_ID|CODEX_SESSION_ID)\\b|(CLAUDE_CODE_SESSION_ID|CLAUDE_SESSION_ID|CODEX_SESSION_ID)=(?=\\s))'\n command: harness policy intercept\n blocking: hard\n # 15000: same rationale as deny-kill-switch-bash immediately above.\n budget_ms: 15000\n\n # Known gap, deliberately not faked as coverage: this only catches the\n # obvious shell shapes (`> .harness-paused`, `tee .harness-paused`,\n # `cp ... .harness-paused`). A regex over the raw command string cannot\n # see through a heredoc, `sh -c '...'`, `bash -lc`, `eval`,\n # `python -c`, base64-decoded payloads, an fs.write call inside a script\n # file the agent creates and then executes, a symlink swap, or the\n # sentinel-write equivalents of `cp`/`>` done via `mv`, `ln`,\n # `install`, or `dd`. These are NOT covered on purpose (regex whack-a-\n # mole against every file-write-capable tool does not meaningfully close\n # this class); closing it for real needs either signing the sentinel\n # (HMAC) or a filesystem-level write guard, neither of which is in scope\n # here; both are noted as follow-ups.\n - name: deny-sentinel-write-bash\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S*\\s+)*(tee|cp)\\b[^;\\n|&]*\\.harness-paused\\b|>{1,2}\\s*\\S*\\.harness-paused\\b'\n command: harness policy intercept\n blocking: hard\n # 15000: same rationale as deny-kill-switch-bash above.\n budget_ms: 15000\n\n # risk-gate (Phase 7 #6): the Risk Gate enforcement hook. The\n # gate-prod-destructive policies below reference it. Same generic\n # `harness policy intercept` entrypoint as every other policy hook;\n # the interceptor builds the Action Envelope, classifies risk against\n # `risk.classifiers[]`, resolves the environment against\n # `environments.resolvers[]`, and evaluates the policies' `when:`.\n - name: risk-gate\n event: PreToolUse\n match: \"Bash\"\n command: harness policy intercept\n blocking: hard\n # 15000: same rationale as the budget note above require-review-evidence.\n budget_ms: 15000\n\n # Optional: runtime-reality drift gate (NOT enabled by default).\n # Blocks destructive runtime commands (compose down/restart, systemctl,\n # kill/pkill, ./deploy-*) when the live process state has drifted from what\n # your expectations file says should be running. Left COMMENTED on purpose:\n # the hook is host-coupled and, without RUNTIME_REALITY_KEYWORD + an\n # expectations file + RUNTIME_REALITY_PROBE_CMD, degrades silently to allow,\n # a no-op that looks like protection. To arm it, uncomment the entry and\n # fill in the three env values. The expectations-file format and how to\n # install the probe are documented in docs/runtime-reality-hook.md.\n #\n # - name: runtime-reality\n # event: PreToolUse\n # command: >-\n # RUNTIME_REALITY_KEYWORD=<your-stack>\n # RUNTIME_REALITY_EXPECTATIONS_DIR=$HOME/.runtime-reality/expectations\n # RUNTIME_REALITY_PROBE_CMD=\"node $HOME/.runtime-reality/probes/runtime-reality-docker-probe.mjs\"\n # harness pack hook runtime-reality\n # blocking: hard\n # description: Block destructive runtime commands on critical process drift\n\npolicies:\n - name: review-before-merge\n description: Block PR merges unless a ledger entry tagged review:<pr-number> exists for this session.\n trigger:\n event: PreToolUse\n match: \"mcp__agent-tasks__pull_requests_merge\"\n extract:\n PR_NUMBER: \"toolArgs.prNumber\"\n requires:\n ledger_tag: \"review:${PR_NUMBER}\"\n hook: require-review-evidence\n enforcement: block\n producers:\n - kind: mcp\n verb: mcp__grounding-mcp__ledger_add\n example: '{sessionId:\"${SESSION_ID}\", type:\"fact\", content:\"review:${PR_NUMBER} \u2014 <verdict + key findings + nits>\", source:\"Agent(general-purpose) review\"}'\n description: Spawn a review subagent against the PR diff, capture its verdict, then persist a ledger entry tagged with the PR number. The content should be self-contained enough for an auditor to read without re-opening the chat.\n ux:\n cannot: \"You cannot merge PR #${PR_NUMBER} yet.\"\n required:\n - \"a recorded review of PR #${PR_NUMBER}\"\n run:\n - 'harness record review --pr ${PR_NUMBER} \"<summary>\"'\n\n # Bash-surface parallel of review-before-merge for operators on the gh-cli\n # workflow. Two scope notes:\n # 1. Tag shape: `review:${BRANCH}` instead of `review:${PR_NUMBER}`. The\n # `gh pr merge` invocation can target the PR by number, by URL, or by\n # the current branch (default), and PR_NUMBER is not extractable from\n # `tool_input.command` with today's JSONPath-only extract DSL. BRANCH\n # is the stable identifier the producer can record at review time.\n # 2. This sits ALONGSIDE review-before-merge \u2014 not as a replacement. An\n # operator using both surfaces (e.g. agent-tasks MCP for most repos\n # + gh-cli for a quick hotfix) will have both gates active, each with\n # its own tag shape, which is semantically honest.\n - name: review-before-merge-bash\n description: Block `gh pr merge` unless a ledger entry tagged review:<branch> exists for this session.\n trigger:\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S+\\s+)*gh pr merge\\b'\n requires:\n ledger_tag: \"review:${BRANCH}\"\n hook: require-review-evidence-bash\n enforcement: block\n producers:\n - kind: mcp\n verb: mcp__grounding-mcp__ledger_add\n example: '{sessionId:\"${SESSION_ID}\", type:\"fact\", content:\"review:${BRANCH} \u2014 <verdict + key findings + nits>\", source:\"Agent(general-purpose) review\"}'\n description: Spawn a review subagent against the branch diff, capture its verdict, then persist a ledger entry tagged with the branch name. Mirror of the review-before-merge producer for the gh-cli surface.\n ux:\n cannot: \"You cannot merge the PR for branch ${BRANCH} via `gh pr merge` yet.\"\n required:\n - \"a recorded review of the PR for branch ${BRANCH}\"\n run:\n - 'harness record review --pr <pr> \"<summary>\"'\n\n - name: dogfood-before-release\n description: Block npm publish / git tag v* without a recent dogfood ledger entry.\n trigger:\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S+\\s+)*(npm publish\\b|git( -C \\S+)* tag v)'\n requires:\n ledger_tag: \"dogfood:${SESSION_ID}\"\n within: 24h\n hook: require-dogfood-evidence\n enforcement: block\n producers:\n - kind: mcp\n verb: mcp__grounding-mcp__ledger_add\n example: '{sessionId:\"${SESSION_ID}\", type:\"fact\", content:\"dogfood:${SESSION_ID} \u2014 <end-to-end smoke summary against the live system>\", source:\"manual smoke test\"}'\n description: Before tagging or publishing, run the release path end-to-end against the live system (not just unit tests) and persist the result as a session-tagged ledger entry. Document what you exercised (install, CLI happy path, MCP handshake, etc.) so a future auditor can tell whether the smoke covered the change.\n ux:\n cannot: \"You cannot publish a release yet.\"\n required:\n - \"an end-to-end dogfood run in this session\"\n run:\n - 'harness record dogfood \"<was wurde real ausprobiert>\"'\n\n - name: two-reviewers-required\n description: At least two distinct reviewer ledger entries must exist for the PR.\n trigger:\n event: PreToolUse\n match: \"mcp__agent-tasks__pull_requests_merge\"\n extract:\n PR_NUMBER: \"toolArgs.prNumber\"\n requires:\n ledger_tag: \"review:${PR_NUMBER}\"\n count:\n min: 2\n hook: require-review-evidence\n enforcement: warn\n producers:\n - kind: mcp\n verb: mcp__grounding-mcp__ledger_add\n example: '{sessionId:\"${SESSION_ID}\", type:\"fact\", content:\"review:${PR_NUMBER} \u2014 <verdict + key findings + nits>\", source:\"Agent(general-purpose) review (reviewer 2)\"}'\n description: Same shape as review-before-merge but TWO DISTINCT reviewer entries must exist before the gate is satisfied (count.min 2). Distinguish reviewers by source so the count is honest. Warn-level enforcement, so the agent CAN merge with one reviewer but should consider spawning a second for load-bearing changes.\n\n - name: preflight-before-investigation\n description: Block investigative git reads (status/log/diff/branch) when agent-preflight has not run recently with ready:true for the current repo.\n trigger:\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S+\\s+)*git( -C \\S+)* (status|log|diff|branch)\\b'\n requires:\n ledger_tag: \"preflight:${REPO}\"\n within: 1h\n hook: require-preflight-evidence\n enforcement: block\n producers:\n - kind: bash\n command: harness session-start preflight\n description: Runs agent-preflight against the current cwd; on ready:true, records preflight:${REPO} to the ledger. Standard producer.\n - kind: mcp\n verb: mcp__grounding-mcp__ledger_add\n example: '{sessionId:\"${SESSION_ID}\", type:\"fact\", content:\"preflight:${REPO}\", source:\"manual\"}'\n description: Direct ledger write. Use when the Bash hook is locked down (e.g. understanding-gate active) or when the standard producer is unavailable.\n ux:\n cannot: \"You cannot investigate this repository yet.\"\n required:\n - \"verified repository preflight\"\n - \"an approved Understanding Report, if the Understanding Gate is still active (it blocks `harness preflight` itself)\"\n run:\n - \"harness preflight\"\n\n - name: review-subagent-before-pr-create\n description: Block agent-tasks PR creation unless a review-subagent ledger entry tagged for this task already exists. Forces the rigorous review BEFORE the PR opens, not after.\n trigger:\n event: PreToolUse\n match: \"mcp__agent-tasks__pull_requests_create\"\n extract:\n TASK_ID: \"toolArgs.taskId\"\n requires:\n ledger_tag: \"review-subagent:${TASK_ID}\"\n hook: require-review-subagent-evidence\n enforcement: block\n producers:\n - kind: mcp\n verb: mcp__grounding-mcp__ledger_add\n example: '{sessionId:\"${SESSION_ID}\", type:\"fact\", content:\"review-subagent:${TASK_ID} \u2014 <verdict + key findings + nits>\", source:\"Agent(general-purpose) review\"}'\n description: After running a review subagent against the staged diff, persist its verdict + load-bearing findings as a ledger entry tagged with the task UUID. The content should be self-contained enough to audit later without re-reading the chat.\n ux:\n cannot: \"You cannot open a pull request for task ${TASK_ID} yet.\"\n required:\n - \"a completed review-subagent pass on this task\"\n run:\n - 'harness record review-subagent --task ${TASK_ID} --verdict <verdict>'\n\n # Bash-surface parallel of review-subagent-before-pr-create. Tag shape is\n # `review-subagent:${BRANCH}` because TASK_ID is an agent-tasks-only\n # concept; for the gh-cli workflow the working branch is the closest stable\n # handle for \"the PR-in-progress\" at this point. Same rationale as\n # review-before-merge-bash: sits alongside the MCP variant, not as a\n # replacement.\n - name: review-subagent-before-pr-create-bash\n description: Block `gh pr create` unless a review-subagent ledger entry tagged review-subagent:<branch> exists for this session. Forces the rigorous review BEFORE the PR opens.\n trigger:\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S+\\s+)*gh pr create\\b'\n requires:\n ledger_tag: \"review-subagent:${BRANCH}\"\n hook: require-review-subagent-evidence-bash\n enforcement: block\n producers:\n - kind: mcp\n verb: mcp__grounding-mcp__ledger_add\n example: '{sessionId:\"${SESSION_ID}\", type:\"fact\", content:\"review-subagent:${BRANCH} \u2014 <verdict + key findings + nits>\", source:\"Agent(general-purpose) review\"}'\n description: After running a review subagent against the staged diff for the working branch, persist its verdict + load-bearing findings as a ledger entry tagged with the branch name. Mirror of the review-subagent-before-pr-create producer for the gh-cli surface.\n ux:\n cannot: \"You cannot open a pull request for branch ${BRANCH} via `gh pr create` yet.\"\n required:\n - \"a completed review-subagent pass on branch ${BRANCH}\"\n run:\n - 'harness record review-subagent --task <task-id> --verdict <verdict>'\n\n - name: preflight-before-push\n description: Block git push unless a fresh preflight ledger entry exists for the current branch. Catches the stale-checkout class of incident at the last reversible step.\n trigger:\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S+\\s+)*git( -C \\S+)* push\\b'\n requires:\n ledger_tag: \"preflight:${BRANCH}\"\n within: 10m\n # at_head:true lets a preflight at the current HEAD satisfy the\n # gate at any age (the standard producer writes head:<sha> into\n # the tag content). The 10m window remains the freshness ceiling\n # for the head-mismatch case (operator switched branch, preflight\n # predates HEAD shift, runtime couldn't resolve a sha).\n at_head: true\n hook: require-preflight-push-evidence\n enforcement: block\n producers:\n - kind: bash\n command: harness session-start preflight\n description: Runs agent-preflight against the current cwd; on ready:true, records preflight:${BRANCH} ready:true confidence:<n> head:<sha> to the ledger. Standard producer.\n - kind: mcp\n verb: mcp__grounding-mcp__ledger_add\n example: '{sessionId:\"${SESSION_ID}\", type:\"fact\", content:\"preflight:${BRANCH} head:<full-sha> \u2014 <summary of what is on the branch + smoke results>\", source:\"manual\"}'\n description: Direct ledger write. Include head:<full-sha> if you want the entry to count under at_head; the branch is the WIP review surface and the content should summarise what is staged + the smoke evidence so a reviewer can audit later without re-reading the chat.\n ux:\n cannot: \"You cannot push branch ${BRANCH} yet.\"\n required:\n - \"a preflight for ${BRANCH} at the current HEAD (any age) OR any preflight within the last 10 minutes. Re-run `harness preflight` if you committed since the last preflight AND it has been more than 10 minutes.\"\n - \"if solution-acceptance is enabled, a ready HEAD-pinned verdict at the SAME commit too (run `solution_evaluate`). `git push` trips both gates, so commit first if the tree is dirty, then satisfy both at one HEAD.\"\n - \"an approved Understanding Report, if the Understanding Gate is still active (it blocks `harness preflight` itself)\"\n run:\n - \"harness preflight\"\n\n # Phase 7 Risk Gate \u2014 the canonical built-in worked example. These two\n # policies, with the dangerous-shell classifier and production-signals\n # resolver below, are the Risk Gate's default stance: a destructive\n # shell action whose target environment resolves to production is\n # gated before the runtime fires it. Both fire ONLY when the\n # environment resolves to production (a main / release branch, a\n # prod-looking DATABASE_URL, or a prod kube context); on an ordinary\n # feature branch the environment is unknown and neither fires. Ordered\n # deny-first so a critical action (which also matches the high\n # threshold) gets the hard-deny envelope. See docs/risk-gate.md.\n - name: gate-prod-destructive\n description: Deny critical-severity destructive shell actions against a production target.\n trigger:\n event: PreToolUse\n match: \"Bash\"\n when:\n risk.severity_at_least: critical\n environment.name: production\n requires:\n ledger_tag: \"risk-override:${SESSION_ID}\"\n hook: risk-gate\n enforcement: block\n # Operator-in-the-loop gate: the override tag is written by the\n # operator verb (ask semantics), not by the agent. See\n # writing-custom-policies.md, tripwire 4 (the trust model).\n producers:\n - kind: ask\n command: harness approve risk --force <reason>\n description: Deliberate operator override for a critical production mutation; run from the operator shell.\n - kind: mcp\n verb: mcp__grounding-mcp__ledger_add\n example: '{sessionId:\"${SESSION_ID}\", type:\"fact\", content:\"risk-override:${SESSION_ID} \u2014 operator-authorized <reason>\", source:\"operator\"}'\n description: Recovery path if the approve verb is unavailable; only meaningful when the OPERATOR authorizes the content.\n ux:\n cannot: \"You cannot run this critical destructive action against production.\"\n required:\n - \"a deliberate operator override: a critical production mutation has no benign reading\"\n run:\n - \"Choose a non-destructive alternative, or ask the OPERATOR to run the command themselves, outside the agent.\"\n - \"Operator override (deliberate): the OPERATOR runs `harness approve risk --force <reason>` from their own shell (`! ` prefix in Claude Code, with --i-am-the-operator to acknowledge a non-TTY invocation).\"\n - name: gate-prod-destructive-approval\n description: Require operator approval for high-severity destructive shell actions against a production target.\n trigger:\n event: PreToolUse\n match: \"Bash\"\n when:\n risk.severity_at_least: high\n environment.name: production\n requires:\n ledger_tag: \"risk-approved:${SESSION_ID}\"\n hook: risk-gate\n enforcement: require_approval\n producers:\n - kind: ask\n command: harness approve risk\n description: Operator approves this Risk Gate decision from their own shell.\n - kind: mcp\n verb: mcp__grounding-mcp__ledger_add\n example: '{sessionId:\"${SESSION_ID}\", type:\"fact\", content:\"risk-approved:${SESSION_ID} \u2014 operator-authorized\", source:\"operator\"}'\n description: Recovery path if the approve verb is unavailable; only meaningful when the OPERATOR authorizes the content.\n ux:\n cannot: \"You cannot run this destructive production action yet.\"\n required:\n - \"operator approval of this Risk Gate decision\"\n run:\n - \"harness approve risk\"\n\n # deny-kill-switch-bypass / deny-session-env-strip / deny-pause-sentinel-forgery\n # (task cf1fde6d): `harness pause`/`harness resume` refuse to run inside an\n # agent shell in CODE, but that CLI check (refuseIfAgentShell / refuseIfNonTTY\n # in src/cli/pause/index.ts) is a speed bump, not a boundary: a Claude Code\n # `! `-prefixed shell inherits this session's env AND its non-TTY stdin, so\n # it is indistinguishable from an agent Bash call and trips the same checks\n # (verified live; docs/okf/pause-vs-gate-kill-switch.md). This PreToolUse\n # layer is the enforcement point that actually runs BEFORE the agent's Bash\n # executes.\n #\n # UPDATE (task 2cc73f55, closes the gap the HONESTY NOTE below used to\n # document): these three now declare `operator_only: true` instead of a\n # requires.ledger_tag. Every requires:-based policy in this engine is, by\n # construction, satisfiable by the SAME agent it gates: nothing stops it\n # from calling mcp__grounding-mcp__ledger_add with content matching the\n # tag for its own session before running the denied command, which used to\n # flip deny to allow (src/runtime/intercept.ts, the satisfied-requires\n # branch). `operator_only: true` closes that specific hole: the policy\n # declares NO requires: at all, and intercept() short-circuits to an\n # unconditional deny before the requires pipeline runs \u2014 no ledger query,\n # no template substitution, so no ledger write, marker file, or flag can\n # ever flip it to allow. See docs/writing-custom-policies.md tripwire 4 and\n # the \"Operator-only unconditional deny\" section for the schema contract,\n # and tests/cli/init-full-template-kill-switch-deny.test.ts for a test that\n # forges every ledger-evidence shape available to intercept() and asserts\n # the block holds.\n #\n # RESIDUAL GAP (still open, out of scope for task 2cc73f55): bash_match is\n # still a regex over the raw command string, matched BEFORE evaluation ever\n # reaches operator_only:, so it still cannot see through a heredoc,\n # `sh -c '...'`, `eval`, a base64-decoded payload, or a script file the\n # agent writes and then executes \u2014 an agent that defeats the TRIGGER match\n # never reaches this deny at all. That is a bash_match coverage gap, not a\n # requires-satisfaction gap, and is unrelated to the fix here (see PR #341\n # for the known uncoverable shapes). `harness validate` no longer warns\n # \"declares no producers\" on these three (checkPolicySelfAttestation now\n # recognises operator_only: true as correct-by-construction).\n #\n # UPDATE (task 432db3d3, 2026-07-28, follow-up to ea8becf5): PARTIALLY\n # closed \u2014 for THIS policy only. `src/runtime/command-normalize.ts` now\n # peels wrapper prefixes (`env`, `nice`, `command`, `sudo`, `doas`,\n # `time`, `timeout`, `stdbuf`, `setsid`, leading VAR=value) and\n # collapses whitespace runs for the closed head-token set\n # `git`/`gh`/`npm`/`harness`, so `env harness pause`, `nice harness\n # gate disable` and the like now reach this deny via the raw-OR-normalised\n # match \u2014 measured 2026-07-28 against the real trigger regex through the\n # real hook entry point, pinned in tests/runtime/intercept-cli.test.ts.\n # The OTHER TWO kill switches are NOT covered by that closed set:\n # deny-session-env-strip keys on `env`/`unset` head tokens, and `env`\n # is STRUCTURALLY unreachable for the normaliser (its peeling loop\n # consumes `env -u <VAR>` while hunting for a gated command behind it,\n # erasing the very text this trigger keys on); deny-pause-sentinel-forgery\n # keys on `tee`/`cp`. Both remain wrapper-bypassable (`nice env -u\n # CLAUDE_CODE_SESSION_ID ...`, `nice tee <sentinel>`), measured\n # 2026-07-28 and pinned as explicit not-covered tests plus a head-token\n # drift guard (tests/runtime/bash-match-head-token-drift.test.ts) that\n # goes red when a future bash_match policy gates a new head token.\n - name: deny-kill-switch-bypass\n description: Deny harness pause, harness resume, harness gate disable, and harness gate enable from the agent's Bash tool. These are operator-only kill switches.\n trigger:\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S*\\s+)*(?:npx\\s+|\\S*/)?harness\\s+([\"\\x27]?)(?:pause|resume|gate\\s+([\"\\x27]?)(?:disable|enable)\\4)\\3(?![\\w-])'\n operator_only: true\n hook: deny-kill-switch-bash\n enforcement: block\n ux:\n cannot: \"You cannot run `harness pause`, `harness resume`, `harness gate disable`, or `harness gate enable`.\"\n required:\n - \"the OPERATOR to run this from a terminal genuinely outside your agent session, not a `! `-prefixed Claude Code shell, which inherits this session's env and stdin and does not count\"\n run:\n - \"Stop. Explain to the operator what you were trying to do and why, and let THEM decide whether to run that command from their own terminal. This is the only way to unblock this gate: it is an unconditional deny (operator_only: true) that no in-session evidence \u2014 a ledger write, a marker file, a flag \u2014 can ever satisfy; the only remaining gap is bash_match's regex coverage of exotic shell shapes, not this evaluation (see docs/okf/pause-vs-gate-kill-switch.md).\"\n\n - name: deny-session-env-strip\n description: Deny Bash attempts to strip or unset the agent-session env vars that harness pause/resume check ($CLAUDE_CODE_SESSION_ID, $CLAUDE_SESSION_ID, $CODEX_SESSION_ID). Closes the one-liner that otherwise defeats refuseIfAgentShell.\n trigger:\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S*\\s+)*(env\\b[^;\\n|&]*-u\\s*(CLAUDE_CODE_SESSION_ID|CLAUDE_SESSION_ID|CODEX_SESSION_ID)\\b|env\\b[^;\\n|&]*--unset(?:=|\\s+)(CLAUDE_CODE_SESSION_ID|CLAUDE_SESSION_ID|CODEX_SESSION_ID)\\b|unset\\s+(\\S+\\s+)*(CLAUDE_CODE_SESSION_ID|CLAUDE_SESSION_ID|CODEX_SESSION_ID)\\b|(CLAUDE_CODE_SESSION_ID|CLAUDE_SESSION_ID|CODEX_SESSION_ID)=(?=\\s))'\n operator_only: true\n hook: deny-session-env-strip-bash\n enforcement: block\n ux:\n cannot: \"You cannot strip or unset your own agent-session id env vars from Bash.\"\n required:\n - \"no legitimate reason from inside this session: stripping $CLAUDE_CODE_SESSION_ID / $CLAUDE_SESSION_ID / $CODEX_SESSION_ID is recognised as an attempt to defeat the harness pause/resume agent-shell check\"\n run:\n - \"Stop. If you believe you have a legitimate reason to run a command without one of these env vars set, ask the OPERATOR to do it from their own terminal instead. This gate is an unconditional deny (operator_only: true); see docs/okf/pause-vs-gate-kill-switch.md for the honest trust model and the residual bash_match coverage gap.\"\n\n - name: deny-pause-sentinel-forgery\n description: Deny Bash writes (redirect, tee, cp) to the harness pause sentinel file (.harness-paused). The sentinel is unsigned JSON; a forged file silences every gate exactly like a real operator pause. Reads (cat, stat) are unaffected.\n trigger:\n event: PreToolUse\n match: \"Bash\"\n bash_match: '(^|\\n|;|\\||&|\\()\\s*(\\w+=\\S*\\s+)*(tee|cp)\\b[^;\\n|&]*\\.harness-paused\\b|>{1,2}\\s*\\S*\\.harness-paused\\b'\n operator_only: true\n hook: deny-sentinel-write-bash\n enforcement: block\n ux:\n cannot: \"You cannot write to the harness pause sentinel file (.harness-paused).\"\n required:\n - \"the OPERATOR to run `harness pause`/`harness resume` themselves, from a terminal outside your agent session, never a direct write to the sentinel file\"\n run:\n - \"Stop. Do not write, redirect, tee, or copy anything to .harness-paused. Ask the OPERATOR to silence the gates themselves, from their own terminal, using the operator-only command named under Required, if the session genuinely needs it. This gate is an unconditional deny (operator_only: true); see docs/okf/pause-vs-gate-kill-switch.md for the honest trust model and the residual bash_match coverage gap.\"\n\n# Full inherits the Solo/Team understanding-gate stack: the Stop hook\n# persists each Understanding Report and the PreToolUse pre-tool-use\n# blocker refuses Edit/Write/Bash until the report is approved. Drop\n# this block if you want the reference policies above without the\n# baseline gate.\npolicy_packs:\n - name: understanding-before-execution\n source: builtin\n enabled: true\n description: Force agents to expose their task interpretation and wait for explicit human approval before any write-capable tool fires.\n config:\n mode: grill_me\n # Producers (agent-tasks/25bced52): rendered into the gate's deny\n # envelope by the same engine as policy producers. Constraint at\n # this layer: at-least-one `ask`. Post-v0.14.0 the gate signal\n # is a filesystem marker and the mcp ledger_add path no longer\n # satisfies the gate; the canonical unblock surface is the\n # operator-approval prompt.\n #\n # KEEP IN SYNC (task 68b9ad9c): this text must match\n # defaultProducers() in\n # src/policy-packs/builtin/understanding-before-execution.ts \u2014\n # that function is what `harness pack reseed` and `harness doctor`'s\n # divergence warning treat as \"the shipped template\". A wording fix\n # landed here without updating defaultProducers() would make reseed\n # silently pull operators BACK to the stale wording. Pinned by\n # tests/cli/init-templates-ux-parity.test.ts.\n producers:\n - kind: ask\n command: harness approve understanding\n description: \"Bare command, no pipes or chaining. The hook recognises it via isEscapeCommand and emits permissionDecision:ask; the operator's go on that prompt IS the gate approval. Golden path.\"\n - kind: bash\n command: harness approve understanding\n description: Same command from any un-hooked terminal (operator only, not reachable from inside the gated session). Writes the canonical marker at harness.generated/.approvals/${SESSION_ID}.\n # ux (agent-tasks/e48e3b45): replaces the legacy engine-vocabulary\n # deny envelope with the plain-language { cannot, required, run }\n # shape. Engine details (the BLOCK reason naming session id /\n # marker / report state) still land in stderr for operator audit;\n # the agent only sees this.\n #\n # KEEP IN SYNC (task 68b9ad9c): see the producers: comment above \u2014\n # same rationale applies here, against defaultUx(\"grill_me\") in the\n # same builtin module.\n ux:\n cannot: \"You cannot use write-capable tools yet.\"\n required:\n - \"an approved Understanding Report for this session\"\n run:\n - \"Write an Understanding Report covering: Current Understanding, Intended Outcome, Derived Todos, Acceptance Criteria, Assumptions, Open Questions, Out Of Scope, Risks, Verification Plan, Prior Art (state what you searched for an existing solution and what you found, with an explicit adopt-or-build judgment)\"\n - \"Run `harness approve understanding` with the report attached as a quoted heredoc (harness approve understanding <<'UNDERSTANDING_REPORT' ...report... UNDERSTANDING_REPORT) so it is persisted for audit, then approve the prompt; the heredoc is the only extra shell shape the gate allows (no pipes, chaining, or other redirection)\"\n # approval_lifecycle (agent-tasks/d8ee60ca + harness/f54e0ecb,\n # v0.18.0+): expire the approval marker on task-completion\n # boundaries so a multi-task session re-prompts for an\n # Understanding Report between tasks. Without this the legacy\n # \"one approval per session\" contract lets a stale interpretation\n # drive the next task's edits.\n #\n # Full ships both boundary kinds: the agent-tasks MCP verbs for\n # operators on that workflow, plus a Bash regex list for hybrid\n # operators who also use gh-cli for PR mechanics. `max_age` is\n # the safety net. Operators who prefer the legacy per-session\n # behaviour opt out with `approval_lifecycle: { mode: session }`.\n # Operators on other task systems override the matchers.\n approval_lifecycle:\n expire_on_tool_match:\n - mcp__agent-tasks__task_finish\n - mcp__agent-tasks__task_abandon\n - mcp__agent-tasks__pull_requests_merge\n - mcp__agent-tasks__tasks_transition\n expire_on_bash_match:\n - '^gh pr (merge|close)\\b'\n - '^git push origin (master|main)\\b'\n max_age: 4h\n\n # branch-protection (agent-tasks/2fdc5bbe, default-enabled since v0.17.2):\n # blocks Write/Edit (claude-code) or apply_patch (codex) on protected\n # branches (default: master, main, develop). Complements\n # preflight-before-push, which fires at the LAST reversible step;\n # branch-protection fires at the FIRST source mutation, catching the\n # \"forgot to branch off master\" pattern earlier in the cycle.\n #\n # Two satisfying signals: a fresh `branch:non-protected:<branch>` tag\n # from the SessionStart producer (`harness session-start branch-check`),\n # or the operator-only override marker written by\n # `harness approve branch-protection --session <id>` for deliberate\n # protected-branch edits (version bumps, CI workflow patches, hotfixes).\n # A branch-protection-ack ledger tag is no longer a sufficient override\n # on its own (it is agent-writable); the marker file is the trusted signal.\n #\n # Fails closed (any load / parse / ledger error refuses). Disable by\n # setting `enabled: false` or removing this entry if your workflow\n # routinely edits master directly. Override the protected list via\n # `config.protected_branches`. Full reference:\n # docs/policy-packs/branch-protection.md.\n - name: branch-protection\n source: builtin\n enabled: true\n description: Block Write/Edit on protected branches (master, main, develop) at the first source mutation.\n config:\n # ux (agent-tasks/9806d4f8): replaces the legacy\n # \"branch-protection: refusing ...\" envelope with the\n # plain-language { cannot, required, run } shape. Engine details\n # (the BLOCK reason naming session id / freshness window) stay\n # on stderr for operator audit.\n #\n # KEEP IN SYNC (task 68b9ad9c): this text must match defaultUx() in\n # src/policy-packs/builtin/branch-protection.ts \u2014 see the identical\n # note on the understanding-before-execution pack above for why.\n # Pinned by tests/cli/init-templates-ux-parity.test.ts.\n ux:\n cannot: \"You cannot edit files on protected branch ${BRANCH} yet.\"\n required:\n - \"a checkout of a non-protected branch (current `${BRANCH}` is protected)\"\n run:\n - \"git checkout -b feat/<your-task>\"\n - \"harness session-start branch-check\"\n\n # solution-acceptance (harness cc43c7a4): Verifier-gated Done. Gates the\n # task-finishing tools (agent-tasks completion verbs + git push / gh pr\n # merge) on a ready, HEAD-pinned verdict that the grounding-mcp producer\n # (solution_evaluate) derives from a real preflight run, plus an\n # anti-forgery write-guard on the verdict marker. DISABLED by default: it\n # is a hard completion-gate and needs grounding-mcp (>= 0.3.2) under\n # tools.mcp plus the preflight binary on PATH. Flip enabled: true once the\n # producer is wired and you want completion earned, not claimed. The pack\n # emits its own instructions.md on apply; harness validate warns if you\n # enable it without the producer.\n - name: solution-acceptance\n source: builtin\n enabled: false\n description: Gate task completion on a ready, HEAD-pinned solution-acceptance verdict earned from a real preflight run.\n\n # post-merge-gate (harness T-001, post-merge-gate): catches \"kept working\n # on a branch after it was already merged\" \u2014 a PostToolUse producer\n # records a merged-tip fact on a successful `gh pr merge`; a PreToolUse\n # blocker denies curated history-mutating commands (git commit/add/push/\n # merge/rebase/cherry-pick/revert/reset/stash pop|apply, gh pr create/\n # merge) while the current branch tip still exactly matches that\n # recorded tip. The recovery vocabulary (git switch/checkout/pull/fetch,\n # git branch -d/-D, git stash list/show, any `harness ...` command)\n # always passes as its OWN command, classified before any manifest or\n # ledger access \u2014 the recovery path this gate itself recommends can never\n # be starved. Chaining one of those with a curated mutation does NOT\n # exempt the mutation (task 19356be7).\n # Fails OPEN (unlike branch-protection) when the ledger is unreachable.\n # DISABLED by default: a fresh gate, opt in once you've reviewed the\n # curated command list for your workflow. Full reference:\n # docs/policy-packs/post-merge-gate.md.\n - name: post-merge-gate\n source: builtin\n enabled: false\n description: Deny curated history-mutating git/gh commands on a branch whose current tip was already merged.\n config:\n # ux: replaces the legacy \"post-merge-gate: refusing ...\" envelope\n # with the plain-language { cannot, required, run } shape.\n #\n # KEEP IN SYNC: this text must match defaultUx() in\n # src/policy-packs/builtin/post-merge-gate.ts \u2014 see the identical\n # note on the branch-protection pack above for why. Pinned by\n # tests/cli/init-templates-ux-parity.test.ts.\n ux:\n cannot: \"You cannot run ${TOOL_NAME} on branch ${BRANCH} yet \u2014 its current tip was already merged.\"\n required:\n - \"a branch tip that is not sitting at an already-merged commit (switch off `${BRANCH}`, or move its tip with a new commit)\"\n run:\n - \"git switch ${DEFAULT_BRANCH}\"\n - \"git pull --ff-only\"\n - \"git branch -d ${BRANCH} # optional cleanup\"\n\n# Phase 7 Risk Gate vocabulary. The dangerous-shell classifier and\n# production-signals resolver feed the gate-prod-destructive policies\n# above: `harness policy intercept` builds the Action Envelope,\n# classifies the action against `risk.classifiers[]`, resolves the\n# target environment against `environments.resolvers[]`, and evaluates\n# each policy's `when:` clauses against the result. Full design and the\n# decision model: docs/risk-gate.md.\nrisk:\n # Fail posture when a policy's evidence cannot be READ (ledger timeout,\n # spawn failure, unresolved template): with `preserve_enforcement`\n # (the default) block/require_approval policies fail CLOSED\n # (`deny-degraded`) while warn policies stay non-blocking. Set\n # `fail_open` to restore the pre-0.45 availability-first behaviour\n # where EVERY degraded evaluation was a non-blocking `warn-degraded`.\n # Kept COMMENTED OUT on purpose: the schema is strict, so a manifest\n # carrying this key fails to parse on a pre-0.45 binary, and a manifest\n # load failure is ALLOW at the hook layer \u2014 on a mixed-version fleet an\n # emitted default would turn a downgrade into a silent full fail-open\n # (review 2026-08-08). See docs/okf/gate-fail-posture-matrix.md.\n # degraded_fail_posture: preserve_enforcement\n classifiers:\n - name: dangerous-shell\n tool: Bash\n patterns:\n - pattern: 'rm\\s+-rf\\s+(/|/var|/data|/mnt|~)'\n categories: [destructive, data_loss]\n severity: critical\n - pattern: 'DROP\\s+TABLE|TRUNCATE\\s+TABLE|DELETE\\s+FROM'\n categories: [destructive, data_loss]\n severity: high\n # Token-based, flag-tolerant: a flag between `kubectl` and\n # `delete` (e.g. `kubectl --context=x delete namespace payments`)\n # must not defeat the match, without matching `kubectl\n # get`/`describe` and without exponential-backtracking on a long\n # flag run. `(?:\\s+-\\S+(?:\\s+(?!delete\\b)(?!-)\\S+)?)*` consumes\n # zero or more `-`/`--` flag tokens (each optionally taking one\n # following, non-flag, non-\"delete\" value token), linear in\n # command length. See docs/risk-gate.md for the full rationale\n # and the earlier quadratic-alternation form this replaced.\n - pattern: 'kubectl(?:\\s+-\\S+(?:\\s+(?!delete\\b)(?!-)\\S+)?)*\\s+delete\\s+(namespace|deployment|statefulset|pvc)'\n categories: [destructive, infrastructure_change]\n severity: high\n # Same flag-tolerance treatment for terraform's own `-chdir=DIR`\n # global flag, which sits between the tool name and the\n # subcommand (`terraform -chdir=infra destroy`).\n - pattern: 'terraform(?:\\s+-\\S+(?:\\s+(?!destroy\\b)(?!-)\\S+)?)*\\s+destroy'\n categories: [destructive, infrastructure_change]\n severity: critical\n\nenvironments:\n resolvers:\n - name: production-signals\n environment: production\n signals:\n branch_patterns: [main, \"release/*\"]\n env_var_patterns:\n - var: DATABASE_URL\n patterns: [prod, production]\n kube_context_patterns: [\".*prod.*\"]\n kube_namespace_patterns: [prod, production]\n";
3
3
  export type TemplateName = "minimal" | "full" | "solo" | "team";
4
4
  export declare function getTemplate(name: TemplateName): string;
5
5
  export declare function shippedOperatorOnlyPolicyNames(): readonly string[];
6
+ /**
7
+ * One shipped `bash_match` trigger's leading boundary-alternation group
8
+ * (task 037cfb7c). Every FULL_TEMPLATE `bash_match` regex opens with a
9
+ * parenthesized alternation of the shell-token boundaries it treats as
10
+ * "start of a new command", e.g. `^|\n|;|\||&|\(` since v0.43.0 (task
11
+ * d834a065, which narrowed the old `&&`-only boundary to `&` so a
12
+ * backgrounded `sleep 0 & gh pr merge 1` is not missed). `level`
13
+ * distinguishes a hook-level `hooks[].bash_match` trigger from a
14
+ * policy-level `policies[].trigger.bash_match` trigger; the same name
15
+ * can exist on both levels for logically-paired entries with no
16
+ * collision (see the require- / review-...-bash hook/policy pairs in
17
+ * the template).
18
+ */
19
+ export interface BashMatchBoundaryEntry {
20
+ level: "hook" | "policy";
21
+ name: string;
22
+ /** The alternation's inner content, e.g. `^|\n|;|\||&|\(` (no outer parens). */
23
+ boundary: string;
24
+ }
25
+ /**
26
+ * Extracts the leading boundary-alternation group from a `bash_match`
27
+ * regex source string (e.g. `"(^|\\n|;|\\||&|\\()..."` -> `"^|\\n|;|\\||&|\\("`).
28
+ * Returns `undefined` when the string does not open with a parenthesized
29
+ * group; defensive only, every shipped template entry does.
30
+ *
31
+ * Known limitation: the scan is not escape-aware, so an escaped closing
32
+ * paren (`\\)`) inside the group would truncate the match early at that
33
+ * `)` character instead of continuing past it. No shipped FULL_TEMPLATE
34
+ * boundary contains one today; a future boundary that needs a literal
35
+ * `)` inside the alternation would need this scan widened first.
36
+ */
37
+ export declare function extractBashMatchBoundary(bashMatch: string): string | undefined;
38
+ export declare function shippedBashMatchBoundaries(): readonly BashMatchBoundaryEntry[];
@@ -987,10 +987,22 @@ risk:
987
987
  - pattern: 'DROP\\s+TABLE|TRUNCATE\\s+TABLE|DELETE\\s+FROM'
988
988
  categories: [destructive, data_loss]
989
989
  severity: high
990
- - pattern: 'kubectl\\s+delete\\s+(namespace|deployment|statefulset|pvc)'
990
+ # Token-based, flag-tolerant: a flag between \`kubectl\` and
991
+ # \`delete\` (e.g. \`kubectl --context=x delete namespace payments\`)
992
+ # must not defeat the match, without matching \`kubectl
993
+ # get\`/\`describe\` and without exponential-backtracking on a long
994
+ # flag run. \`(?:\\s+-\\S+(?:\\s+(?!delete\\b)(?!-)\\S+)?)*\` consumes
995
+ # zero or more \`-\`/\`--\` flag tokens (each optionally taking one
996
+ # following, non-flag, non-"delete" value token), linear in
997
+ # command length. See docs/risk-gate.md for the full rationale
998
+ # and the earlier quadratic-alternation form this replaced.
999
+ - pattern: 'kubectl(?:\\s+-\\S+(?:\\s+(?!delete\\b)(?!-)\\S+)?)*\\s+delete\\s+(namespace|deployment|statefulset|pvc)'
991
1000
  categories: [destructive, infrastructure_change]
992
1001
  severity: high
993
- - pattern: 'terraform\\s+destroy'
1002
+ # Same flag-tolerance treatment for terraform's own \`-chdir=DIR\`
1003
+ # global flag, which sits between the tool name and the
1004
+ # subcommand (\`terraform -chdir=infra destroy\`).
1005
+ - pattern: 'terraform(?:\\s+-\\S+(?:\\s+(?!destroy\\b)(?!-)\\S+)?)*\\s+destroy'
994
1006
  categories: [destructive, infrastructure_change]
995
1007
  severity: critical
996
1008
 
@@ -1047,4 +1059,62 @@ export function shippedOperatorOnlyPolicyNames() {
1047
1059
  }
1048
1060
  return shippedOperatorOnlyCache;
1049
1061
  }
1062
+ // Matches the leading `(...)` group of a bash_match regex source string.
1063
+ // Every shipped bash_match opens with the boundary-alternation group and
1064
+ // none of its branches contain a literal `)` (parens inside are always
1065
+ // escaped as `\(`), so a non-greedy scan to the first `)` is exact for
1066
+ // every entry in FULL_TEMPLATE.
1067
+ const BOUNDARY_GROUP_RE = /^\(([^)]*)\)/;
1068
+ /**
1069
+ * Extracts the leading boundary-alternation group from a `bash_match`
1070
+ * regex source string (e.g. `"(^|\\n|;|\\||&|\\()..."` -> `"^|\\n|;|\\||&|\\("`).
1071
+ * Returns `undefined` when the string does not open with a parenthesized
1072
+ * group; defensive only, every shipped template entry does.
1073
+ *
1074
+ * Known limitation: the scan is not escape-aware, so an escaped closing
1075
+ * paren (`\\)`) inside the group would truncate the match early at that
1076
+ * `)` character instead of continuing past it. No shipped FULL_TEMPLATE
1077
+ * boundary contains one today; a future boundary that needs a literal
1078
+ * `)` inside the alternation would need this scan widened first.
1079
+ */
1080
+ export function extractBashMatchBoundary(bashMatch) {
1081
+ return BOUNDARY_GROUP_RE.exec(bashMatch)?.[1];
1082
+ }
1083
+ /**
1084
+ * Every FULL_TEMPLATE `hooks[]` / `policies[].trigger` entry that
1085
+ * declares a `bash_match`, paired with its boundary-alternation group
1086
+ * (task 037cfb7c). Parsed from FULL_TEMPLATE itself, the same
1087
+ * single-source-of-truth pattern as `shippedOperatorOnlyPolicyNames`
1088
+ * immediately above, so this list can never drift from what
1089
+ * `harness init --template full` actually writes; unlike
1090
+ * `shippedOperatorOnlyPolicyNames`, this one is pinned by its own
1091
+ * fixed-point test in tests/cli/doctor-trigger-boundary-drift.test.ts
1092
+ * (entry count and the literal shipped boundary), not by
1093
+ * tests/cli/init-full-template-parity.test.ts, which does not reference
1094
+ * it. Memoized: the parse is pure and FULL_TEMPLATE is a build constant.
1095
+ */
1096
+ let shippedBashMatchBoundariesCache;
1097
+ export function shippedBashMatchBoundaries() {
1098
+ if (shippedBashMatchBoundariesCache === undefined) {
1099
+ const manifest = parseManifest(parseYaml(FULL_TEMPLATE));
1100
+ const entries = [];
1101
+ for (const hook of manifest.hooks) {
1102
+ if (!hook.bash_match)
1103
+ continue;
1104
+ const boundary = extractBashMatchBoundary(hook.bash_match);
1105
+ if (boundary !== undefined)
1106
+ entries.push({ level: "hook", name: hook.name, boundary });
1107
+ }
1108
+ for (const policy of manifest.policies) {
1109
+ const bashMatch = policy.trigger.bash_match;
1110
+ if (!bashMatch)
1111
+ continue;
1112
+ const boundary = extractBashMatchBoundary(bashMatch);
1113
+ if (boundary !== undefined)
1114
+ entries.push({ level: "policy", name: policy.name, boundary });
1115
+ }
1116
+ shippedBashMatchBoundariesCache = entries;
1117
+ }
1118
+ return shippedBashMatchBoundariesCache;
1119
+ }
1050
1120
  //# sourceMappingURL=templates.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"templates.js","sourceRoot":"","sources":["../../../src/cli/init/templates.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,gBAAgB,GAAG;;;;;;;;;;;;;;;;;;;;;CAqB/B,CAAC;AAEF,MAAM,CAAC,MAAM,aAAa,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAy9B5B,CAAC;AAEF,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAI7D,MAAM,UAAU,WAAW,CAAC,IAAkB;IAC5C,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,MAAM;YACT,OAAO,aAAa,CAAC;QACvB,KAAK,MAAM;YACT,OAAO,aAAa,CAAC;QACvB,KAAK,MAAM;YACT,OAAO,aAAa,CAAC;QACvB,KAAK,SAAS;YACZ,OAAO,gBAAgB,CAAC;IAC5B,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,IAAI,wBAAuD,CAAC;AAC5D,MAAM,UAAU,8BAA8B;IAC5C,IAAI,wBAAwB,KAAK,SAAS,EAAE,CAAC;QAC3C,MAAM,QAAQ,GAAG,aAAa,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC;QACzD,wBAAwB,GAAG,QAAQ,CAAC,QAAQ;aACzC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,KAAK,IAAI,CAAC;aACvC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACxB,CAAC;IACD,OAAO,wBAAwB,CAAC;AAClC,CAAC"}
1
+ {"version":3,"file":"templates.js","sourceRoot":"","sources":["../../../src/cli/init/templates.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,gBAAgB,GAAG;;;;;;;;;;;;;;;;;;;;;CAqB/B,CAAC;AAEF,MAAM,CAAC,MAAM,aAAa,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAq+B5B,CAAC;AAEF,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAI7D,MAAM,UAAU,WAAW,CAAC,IAAkB;IAC5C,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,MAAM;YACT,OAAO,aAAa,CAAC;QACvB,KAAK,MAAM;YACT,OAAO,aAAa,CAAC;QACvB,KAAK,MAAM;YACT,OAAO,aAAa,CAAC;QACvB,KAAK,SAAS;YACZ,OAAO,gBAAgB,CAAC;IAC5B,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,IAAI,wBAAuD,CAAC;AAC5D,MAAM,UAAU,8BAA8B;IAC5C,IAAI,wBAAwB,KAAK,SAAS,EAAE,CAAC;QAC3C,MAAM,QAAQ,GAAG,aAAa,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC;QACzD,wBAAwB,GAAG,QAAQ,CAAC,QAAQ;aACzC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,KAAK,IAAI,CAAC;aACvC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACxB,CAAC;IACD,OAAO,wBAAwB,CAAC;AAClC,CAAC;AAsBD,yEAAyE;AACzE,yEAAyE;AACzE,uEAAuE;AACvE,uEAAuE;AACvE,gCAAgC;AAChC,MAAM,iBAAiB,GAAG,cAAc,CAAC;AAEzC;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,wBAAwB,CAAC,SAAiB;IACxD,OAAO,iBAAiB,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AAChD,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,IAAI,+BAA8E,CAAC;AACnF,MAAM,UAAU,0BAA0B;IACxC,IAAI,+BAA+B,KAAK,SAAS,EAAE,CAAC;QAClD,MAAM,QAAQ,GAAG,aAAa,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC;QACzD,MAAM,OAAO,GAA6B,EAAE,CAAC;QAC7C,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,KAAK,EAAE,CAAC;YAClC,IAAI,CAAC,IAAI,CAAC,UAAU;gBAAE,SAAS;YAC/B,MAAM,QAAQ,GAAG,wBAAwB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC3D,IAAI,QAAQ,KAAK,SAAS;gBAAE,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;QACzF,CAAC;QACD,KAAK,MAAM,MAAM,IAAI,QAAQ,CAAC,QAAQ,EAAE,CAAC;YACvC,MAAM,SAAS,GAAG,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC;YAC5C,IAAI,CAAC,SAAS;gBAAE,SAAS;YACzB,MAAM,QAAQ,GAAG,wBAAwB,CAAC,SAAS,CAAC,CAAC;YACrD,IAAI,QAAQ,KAAK,SAAS;gBAAE,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC7F,CAAC;QACD,+BAA+B,GAAG,OAAO,CAAC;IAC5C,CAAC;IACD,OAAO,+BAA+B,CAAC;AACzC,CAAC"}
@@ -10,6 +10,7 @@ import * as path from "node:path";
10
10
  import { buildActionEnvelope, intercept, isBlockingDecision, recordPolicyDecisionOnSession, resolveEnvironment, resolveGitContext, resolveKubeContext, sanitizeEnvelopeReason, } from "../../runtime/index.js";
11
11
  import { resolveGeneratedDir, writePendingApproval } from "../../runtime/pending-approval.js";
12
12
  import { parseBashPrefix } from "../../runtime/bash-prefix-parse.js";
13
+ import { parseKubectlTarget } from "../../runtime/kubectl-target-parse.js";
13
14
  import { MAX_NORMALIZE_LENGTH, normalizeCommand, normalizeCommandAmpAware, normalizeCommandQuoteAware, segmentViewOf, } from "../../runtime/command-normalize.js";
14
15
  import { extractShellCommand, SHELL_ALIASES } from "../../runtime/tool-name-aliases.js";
15
16
  import { loadManifest } from "../loader.js";
@@ -195,6 +196,35 @@ function applyBranchSwitchUpgrade(event, manifest, cwd, baseGit, branchTarget, i
195
196
  const candidateResolution = resolveEnvironment(buildActionEnvelope(event, { cwd, git: candidateGit, user, host, now: effectiveNow }), manifest.environments.resolvers, inputs);
196
197
  return ENV_RANK[candidateResolution.name] < ENV_RANK[baseResolution.name] ? candidateGit : baseGit;
197
198
  }
199
+ /**
200
+ * Explicit kubectl target, upgrade-only merge (task a7eb1a71): an
201
+ * explicit --context/--namespace/-n on a kubectl command can only push
202
+ * the resolved environment to something MORE dangerous than the
203
+ * ambient kubeconfig alone already gave, never less, mirroring
204
+ * applyBranchSwitchUpgrade exactly (both the ambient-only base and the
205
+ * merged-candidate kube inputs run through the SAME resolveEnvironment
206
+ * call with identical git/env inputs, so ENV_RANK picks the more
207
+ * dangerous of the two). See CHANGELOG.md's `[Unreleased]` entry for
208
+ * the measured downgrade this fixes.
209
+ */
210
+ function applyKubeTargetUpgrade(event, manifest, cwd, git, ambientKube, kubectlTarget, env, user, host, now) {
211
+ if (kubectlTarget === null)
212
+ return ambientKube;
213
+ const candidateKube = {
214
+ context: kubectlTarget.context ?? ambientKube.context,
215
+ namespace: kubectlTarget.namespace ?? ambientKube.namespace,
216
+ };
217
+ if (candidateKube.context === ambientKube.context &&
218
+ candidateKube.namespace === ambientKube.namespace) {
219
+ return ambientKube;
220
+ }
221
+ const effectiveNow = now ?? new Date();
222
+ const baseResolution = resolveEnvironment(buildActionEnvelope(event, { cwd, git, user, host, now: effectiveNow }), manifest.environments.resolvers, { env, kubeContext: ambientKube.context, kubeNamespace: ambientKube.namespace });
223
+ const candidateResolution = resolveEnvironment(buildActionEnvelope(event, { cwd, git, user, host, now: effectiveNow }), manifest.environments.resolvers, { env, kubeContext: candidateKube.context, kubeNamespace: candidateKube.namespace });
224
+ return ENV_RANK[candidateResolution.name] < ENV_RANK[baseResolution.name]
225
+ ? candidateKube
226
+ : ambientKube;
227
+ }
198
228
  /**
199
229
  * Real grounding-mcp-backed ledger client. Exported for testing: the
200
230
  * `record` adapter's failure-surfacing path is exercised against a
@@ -813,12 +843,19 @@ export async function runInterceptCli(opts = {}) {
813
843
  // `resolverGit` itself — a switch away from a production branch can
814
844
  // never downgrade what `resolverGit` (cwd- or cd-based) already
815
845
  // resolved.
816
- const bashPrefix = event.tool_name === "Bash"
817
- ? (() => {
818
- const cmd = readBashCommand(event.tool_input);
819
- return cmd === null ? null : parseBashPrefix(cmd);
820
- })()
821
- : null;
846
+ const riskBashCommand = event.tool_name === "Bash" ? readBashCommand(event.tool_input) : null;
847
+ const bashPrefix = riskBashCommand === null ? null : parseBashPrefix(riskBashCommand);
848
+ // Explicit `--context`/`--namespace`/`-n` on a `kubectl ...`
849
+ // invocation (task a7eb1a71). Fed the REMAINDER after `bashPrefix`
850
+ // consumed a leading `cd <path> &&` / `VAR=value` / `git switch
851
+ // <branch> &&` prefix, not the raw command, so a wrapped invocation
852
+ // like `cd /tmp && kubectl ... --context x` is still covered; see
853
+ // `parseKubectlTarget`'s own doc comment for the narrow,
854
+ // command-head-anchored scope this is otherwise kept to, and for
855
+ // the forms still NOT unwrapped (sudo/time/env wrappers, a kubectl
856
+ // that is not the first chained segment, a piped kubectl).
857
+ const kubectlSubject = riskBashCommand === null ? null : riskBashCommand.slice(bashPrefix?.remainderStart ?? 0);
858
+ const kubectlTarget = kubectlSubject === null ? null : parseKubectlTarget(kubectlSubject);
822
859
  const resolverGit = (() => {
823
860
  if (bashPrefix === null || bashPrefix.cdTarget === null)
824
861
  return cwdGitContext;
@@ -841,18 +878,31 @@ export async function runInterceptCli(opts = {}) {
841
878
  const riskUser = safeOs(() => os.userInfo().username);
842
879
  const riskHost = safeOs(() => os.hostname());
843
880
  // A leading `git switch`/`checkout <branch>` (task 341e024b) is
844
- // merged on top of `resolverGit` above — upgrade-only, see
881
+ // merged on top of `resolverGit` above, upgrade-only, using the
882
+ // AMBIENT kube state (`kube` from `resolveKubeContext()` / the
883
+ // `opts.kubeContext`/`opts.kubeNamespace` test override), unaffected
884
+ // by the kubectl-target merge below. See
845
885
  // `applyBranchSwitchUpgrade`'s own doc comment for why this one is
846
886
  // NOT a straight replacement the way the `cd` merge is.
847
887
  const gitForRisk = applyBranchSwitchUpgrade(event, manifest, cwd, resolverGit, bashPrefix?.branchTarget ?? null, { env: resolverEnv, kubeContext: kube.context, kubeNamespace: kube.namespace }, riskUser, riskHost, opts.now);
888
+ // CONFLICT PRIORITY (task a7eb1a71): an explicit kubectl
889
+ // --context/--namespace/-n is merged on top of the AMBIENT kube
890
+ // state (`kube`), on top of `gitForRisk` above (the git context
891
+ // AFTER the branch-switch upgrade), upgrade-only, exactly the same
892
+ // asymmetric shape the branch-switch merge itself uses. Command
893
+ // text can only raise the resolved environment toward production,
894
+ // never lower an already-resolved production; see
895
+ // `applyKubeTargetUpgrade`'s own doc comment for the measured
896
+ // downgrade this fixes.
897
+ const kubeForRisk = applyKubeTargetUpgrade(event, manifest, cwd, gitForRisk, { context: kube.context, namespace: kube.namespace }, kubectlTarget, resolverEnv, riskUser, riskHost, opts.now);
848
898
  riskContext = {
849
899
  git: gitForRisk,
850
900
  cwd,
851
901
  user: riskUser,
852
902
  host: riskHost,
853
903
  env: resolverEnv,
854
- kubeContext: kube.context,
855
- kubeNamespace: kube.namespace,
904
+ kubeContext: kubeForRisk.context,
905
+ kubeNamespace: kubeForRisk.namespace,
856
906
  };
857
907
  }
858
908
  let result;