Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@

## Unreleased

### Added

- Cursor `hook-gate-doctrine.mdc` rule template (`alwaysApply`) with hook gate triage:
invalid JSON vs plan binding vs deterministic path errors.
- `scripts/test-mutation-hook-stdout.sh` to assert the mutation gate emits one JSON object.

### Changed

- Mutation hook shell adapter: emit codex/claude allow JSON; capture
`verify-mutation-hook` stdout/stderr off the hook pipe so Cursor receives valid JSON.
- Cursor `agent-governance.mdc` template: hooks are operational directions, not
obstacles to bypass.
- `docs/agent-environment-matrix.md` Cursor row documents hook operational doctrine.

### Fixed

- `source-limit check` skips generated `keysymdef.go` and vendored `xorg-deps/` trees
in consumer repositories.

## 2.1.0 - 2026-06-01

### Added
Expand Down
5 changes: 5 additions & 0 deletions MANIFEST.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ config_key = "mutation_gate.hook_script_path"
template = "templates/.cursor/rules/agent-governance.mdc.template"
destination = ".cursor/rules/agent-governance.mdc"

[[render]]
template = "templates/.cursor/rules/hook-gate-doctrine.mdc.template"
destination = ".cursor/rules/hook-gate-doctrine.mdc"

[[render]]
template = "templates/.claude/settings.json.template"
destination = ".claude/settings.json"
Expand Down Expand Up @@ -196,6 +200,7 @@ tracked_for_ci = [
".agents/skills/task-registry-flow",
".agents/skills/task-registry-flow.md",
".cursor/rules/agent-governance.mdc",
".cursor/rules/hook-gate-doctrine.mdc",
".cursor/hooks.json",
".cursor/hooks/gap-closure-gate.sh",
"AGENTS.md",
Expand Down
1 change: 1 addition & 0 deletions REQUIREMENTS.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ required = [
".agents/skills/task-registry-flow",
".agents/skills/task-registry-flow.md",
".cursor/rules/agent-governance.mdc",
".cursor/rules/hook-gate-doctrine.mdc",
".cursor/hooks.json",
".cursor/hooks/gap-closure-gate.sh",
"AGENTS.md",
Expand Down
2 changes: 1 addition & 1 deletion docs/agent-environment-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ are guardrails.
|-------------|--------------|--------------|
| Codex | `AGENTS.md`, `.codex/config.toml`, `.codex/hooks.json`, `.agents/skills/<skill>/SKILL.md` | `plugins/agent-governance/scripts/status.sh --env codex`; Codex hooks require a trusted project |
| Antigravity CLI | `GEMINI.md`, `.agents/hooks.json`, `.agents/skills/*.md`, `.agents/plugins/agent-governance` | `agy --version` must be 1.0.3 or newer; `agy plugin validate plugins/agent-governance` must process hooks |
| Cursor | `.cursor/rules/agent-governance.mdc`, `.cursor/skills/<skill>/SKILL.md`, `.cursor/hooks.json` | `plugins/agent-governance/scripts/status.sh --env cursor`; `cursor-agent --plugin-dir plugins/agent-governance` can load local plugin code |
| Cursor | `.cursor/rules/agent-governance.mdc`, `.cursor/rules/hook-gate-doctrine.mdc` (always-on gate triage), `.cursor/skills/<skill>/SKILL.md`, `.cursor/hooks.json` | `plugins/agent-governance/scripts/status.sh --env cursor`; hooks are **operational directions** at mutation time (deny = missing governance step; invalid JSON = gate repair on active hook target, not a new plan); optional user-level governed subagents complement repo-local skills/hooks |
| Claude Code | `CLAUDE.md`, `.claude/settings.json`, `.claude/skills/<skill>/SKILL.md` | `plugins/agent-governance/scripts/status.sh --strict`; `.claude/settings.json` must delegate PreToolUse to the canonical mutation gate |

Do not add compatibility shims for old workspace `.gemini/settings.json`, stale `.codex/settings.toml`, or `.codex/hooks/user-plan-approval.toml`. Current install removes those generated paths.
Expand Down
2 changes: 2 additions & 0 deletions rust/task-registry-flow-cli/src/source_limit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,7 @@ fn skip_dir(path: &str) -> bool {
| "out"
| "venv"
| "vendor"
| "xorg-deps"
)
})
}
Expand All @@ -586,6 +587,7 @@ fn skip_file(path: &str) -> bool {
| "deno.lock"
| "flake.lock"
| "go.sum"
| "keysymdef.go"
) || path == "docs/task-registry/events.jsonl"
|| path.starts_with("docs/task-registry/archive/")
}
Expand Down
20 changes: 20 additions & 0 deletions scripts/test-mutation-hook-stdout.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# Assert the canonical mutation gate emits exactly one JSON object on stdout.
set -euo pipefail

root="$(git rev-parse --show-toplevel)"
cd "$root"

hook="${MUTATION_HOOK_SCRIPT:-tools/agent-governance/pre-tool-use-gap-closure.sh}"
if [[ ! -f "$hook" ]]; then
echo "FAIL: mutation hook not found: $hook" >&2
exit 1
fi

out="$(printf '{}' | GOVERNANCE_HOOK_FORMAT=cursor bash "$hook" --format cursor)"
if printf '%s' "$out" | grep -q '^TASK_VERIFY'; then
echo "FAIL: verify-mutation-hook leaked to stdout: $out" >&2
exit 1
fi
printf '%s' "$out" | python3 -c 'import json,sys; json.load(sys.stdin)'
echo "ok: single valid JSON on hook stdout"
2 changes: 1 addition & 1 deletion templates/.cursor/rules/agent-governance.mdc.template
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Use the plugin-owned registry only: `{{TASK_REGISTRY_CLI}} validate`, `status`,

Keep source, scripts, configs, docs, templates, and governance files at or below 1600 lines. Treat this as a design-time rule. Before adding behavior to a violating file, run `{{TASK_REGISTRY_CLI}} source-limit plan --path <file>` and split first.

Cursor hooks live in `.cursor/hooks.json`; they are runtime guardrails, not the source of truth. CI and `{{TASK_REGISTRY_CLI}} source-limit check` are authoritative.
Cursor hooks in `.cursor/hooks.json` enforce repo law at mutation time. Treat hook outcomes as **operational directions** (see `.cursor/rules/hook-gate-doctrine.mdc`): a deny names the missing governance step; **invalid JSON ≠ need another plan** — fix stdout on the existing active hook target. Do not disable hooks or skip the mutation gate. Policy authority remains in `.codex/agent-governance.toml`, `docs/task-registry.toml`, and CI; hooks apply that policy live.

Use exact active or planned task targets. Ambiguous shell redirections, compact redirects, and inline write calls without deterministic paths fail closed. A terminal task is immutable after `completed` or `cancelled`; changed follow-up work needs a new `task_id`.

Expand Down
22 changes: 22 additions & 0 deletions templates/.cursor/rules/hook-gate-doctrine.mdc.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
description: Hook gate triage — invalid JSON vs plan binding vs path errors.
alwaysApply: true
---

# Hook gate triage (operational directions)

Cursor hooks enforce repo law at mutation time. Read `.cursor/rules/agent-governance.mdc` for the full workflow. **Do not disable hooks or use subagents or external terminals to skip registry binding** — except gate self-repair on `{{MUTATION_HOOK_SCRIPT}}` when the hook cannot emit valid JSON.

## Triage table

| Symptom | Meaning | Response |
|---------|---------|----------|
| **invalid JSON** from hook | Gate script stdout is polluted (e.g. `TASK_VERIFY_MUTATION_HOOK ok` before JSON) | **Not** “create another plan.” Fix stdout on the **existing active hook target** (capture `verify-mutation-hook` output off the hook pipe). Then retry inside Cursor. |
| **not bound to an active registry task target** | Path not in `[[tasks.targets]]` for an active task | Write/refresh `docs/plans/<slug>.md`, run `{{TASK_REGISTRY_CLI}} activate`, edit only listed targets. |
| **did not expose a deterministic target path** | Write/Shell lacked an exact repo-relative path | Use `Write`/`StrReplace` with full path; no vague shell redirects. |

## When a plan is already active

Activation unlocks **only** paths in `[[tasks.targets]]`. If those paths still fail with **invalid JSON**, the gate script is broken — repair it first, not the registry.

Policy authority: `.codex/agent-governance.toml`, `docs/task-registry.toml`, CI. Hooks apply that policy live.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ emit_json() {
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":%s}}\n' "$escaped_reason"
;;
codex:allow|claude:allow)
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}\n'
;;
cursor:deny)
printf '{"permission":"deny","user_message":%s,"agent_message":%s}\n' "$escaped_reason" "$escaped_reason"
Expand Down Expand Up @@ -88,9 +89,17 @@ if [[ "$base_verify_cmd" != "$canonical_verify_cmd" ]]; then
exit 0
fi

if output="$(.codex/scripts/task-registry verify-mutation-hook --format "$format" 2>&1)"; then
verify_stderr="$(mktemp)"
verify_stdout="$(mktemp)"
trap 'rm -f "${verify_stderr}" "${verify_stdout}"' EXIT

if .codex/scripts/task-registry verify-mutation-hook --format "$format" >"${verify_stdout}" 2>"${verify_stderr}"; then
emit_json allow
else
output="$(tr '\n' ' ' <"${verify_stderr}")"
if [[ -z "${output// }" ]]; then
output="$(tr '\n' ' ' <"${verify_stdout}")"
fi
emit_deny "mutation gate failed: ${output}"
exit 0
fi
11 changes: 10 additions & 1 deletion tools/agent-governance/pre-tool-use-gap-closure.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ emit_json() {
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":%s}}\n' "$escaped_reason"
;;
codex:allow|claude:allow)
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}\n'
;;
cursor:deny)
printf '{"permission":"deny","user_message":%s,"agent_message":%s}\n' "$escaped_reason" "$escaped_reason"
Expand Down Expand Up @@ -88,9 +89,17 @@ if [[ "$base_verify_cmd" != "$canonical_verify_cmd" ]]; then
exit 0
fi

if output="$(.codex/scripts/task-registry verify-mutation-hook --format "$format" 2>&1)"; then
verify_stderr="$(mktemp)"
verify_stdout="$(mktemp)"
trap 'rm -f "${verify_stderr}" "${verify_stdout}"' EXIT

if .codex/scripts/task-registry verify-mutation-hook --format "$format" >"${verify_stdout}" 2>"${verify_stderr}"; then
emit_json allow
else
output="$(tr '\n' ' ' <"${verify_stderr}")"
if [[ -z "${output// }" ]]; then
output="$(tr '\n' ' ' <"${verify_stdout}")"
fi
emit_deny "mutation gate failed: ${output}"
exit 0
fi
Loading