context-guard-cli 2.4.0__tar.gz → 2.5.0__tar.gz

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.
Files changed (87) hide show
  1. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/.gitignore +2 -0
  2. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/CHANGELOG.md +22 -0
  3. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/PKG-INFO +4 -4
  4. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/README.es.md +2 -2
  5. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/README.md +2 -2
  6. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/TUTORIAL.es.md +2 -2
  7. context_guard_cli-2.5.0/context_guard/_data/hosts/cursor/mcp.snippet.json +7 -0
  8. context_guard_cli-2.5.0/context_guard/_data/hosts/cursor/rules/context-guard.mdc +20 -0
  9. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/context_guard/guard/assets.py +3 -3
  10. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/context_guard/guard/cli.py +2 -2
  11. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/context_guard/guard/commands.py +4 -0
  12. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/context_guard/guard/setup.py +78 -6
  13. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/docs/adapters/VERIFY.md +2 -1
  14. context_guard_cli-2.5.0/docs/adapters/cursor/PERMISSIONS.md +42 -0
  15. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/pyproject.toml +1 -1
  16. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_adapters.py +23 -2
  17. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_assets.py +7 -0
  18. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_packaging.py +2 -2
  19. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_release.py +5 -8
  20. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_setup.py +61 -3
  21. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/.agents/rules/context-guard.md +0 -0
  22. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/.githooks/pre-commit +0 -0
  23. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/.github/workflows/ci.yml +0 -0
  24. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/.github/workflows/publish.yml +0 -0
  25. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/AGENTS.md +0 -0
  26. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/LICENSE +0 -0
  27. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/context_guard/__init__.py +0 -0
  28. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/context_guard/_data/hosts/antigravity/hooks.snippet.json +0 -0
  29. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/context_guard/_data/hosts/antigravity/rules/context-guard.md +0 -0
  30. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/context_guard/_data/hosts/antigravity/skills/context-guard/SKILL.md +0 -0
  31. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/context_guard/_data/hosts/claude-code/commands/cg-continue.md +0 -0
  32. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/context_guard/_data/hosts/claude-code/commands/cg-new.md +0 -0
  33. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/context_guard/_data/hosts/claude-code/mcp.snippet.json +0 -0
  34. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/context_guard/_data/hosts/claude-code/settings.snippet.json +0 -0
  35. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/context_guard/_data/hosts/opencode/agent.snippet.json +0 -0
  36. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/context_guard/_data/hosts/opencode/commands/cg-continue.md +0 -0
  37. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/context_guard/_data/hosts/opencode/commands/cg-new.md +0 -0
  38. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/context_guard/_data/hosts/opencode/mcp.snippet.json +0 -0
  39. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/context_guard/_data/hosts/opencode/permissions.snippet.json +0 -0
  40. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/context_guard/_data/phases/execute.md +0 -0
  41. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/context_guard/_data/phases/plan.md +0 -0
  42. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/context_guard/_data/phases/verify.md +0 -0
  43. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/context_guard/guard/__init__.py +0 -0
  44. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/context_guard/guard/errors.py +0 -0
  45. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/context_guard/guard/locking.py +0 -0
  46. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/context_guard/guard/manifest.py +0 -0
  47. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/context_guard/guard/migrate.py +0 -0
  48. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/context_guard/guard/paths.py +0 -0
  49. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/context_guard/guard/plan_import.py +0 -0
  50. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/context_guard/guard/transaction.py +0 -0
  51. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/context_guard/mcp_server.py +0 -0
  52. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/docs/adapters/antigravity/PERMISSIONS.md +0 -0
  53. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/docs/adapters/claude-code/PERMISSIONS.md +0 -0
  54. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/docs/adapters/opencode/PERMISSIONS.md +0 -0
  55. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/__init__.py +0 -0
  56. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/fixtures/plan_en.md +0 -0
  57. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/fixtures/plan_es.md +0 -0
  58. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/fixtures/plan_no_phases.md +0 -0
  59. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/fixtures/plan_sentinel.md +0 -0
  60. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/fixtures/plan_sparse.md +0 -0
  61. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_adversarial.py +0 -0
  62. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_adversarial_approve.py +0 -0
  63. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_adversarial_migrate.py +0 -0
  64. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_adversarial_multichange.py +0 -0
  65. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_agents_md.py +0 -0
  66. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_archive.py +0 -0
  67. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_check_completion.py +0 -0
  68. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_commands.py +0 -0
  69. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_doctor.py +0 -0
  70. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_ergonomics.py +0 -0
  71. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_from_plan.py +0 -0
  72. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_json_format.py +0 -0
  73. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_license.py +0 -0
  74. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_locking.py +0 -0
  75. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_manifest.py +0 -0
  76. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_mcp_server.py +0 -0
  77. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_next_task_status.py +0 -0
  78. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_phase_docs.py +0 -0
  79. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_phases.py +0 -0
  80. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_plan_import.py +0 -0
  81. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_pre_commit_hook.py +0 -0
  82. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_publish.py +0 -0
  83. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_pyproject_pin.py +0 -0
  84. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_readme.py +0 -0
  85. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_readme_es.py +0 -0
  86. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_transaction.py +0 -0
  87. {context_guard_cli-2.4.0 → context_guard_cli-2.5.0}/tests/test_validate.py +0 -0
@@ -8,3 +8,5 @@ dist/
8
8
  PLAN.md
9
9
  PLAN-2.1.md
10
10
  RUNBOOK_HUMANO.md
11
+ .claude/*.local.json
12
+ uv.lock
@@ -6,6 +6,28 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
 
7
7
  ---
8
8
 
9
+ ## [2.5.0] - 2026-09-03
10
+
11
+ ### Added
12
+
13
+ - Support for Cursor as a host adapter: `cg setup --host cursor` installs
14
+ `.cursor/rules/context-guard.mdc` (both globally in `~/.cursor/rules` and
15
+ per-project) and optionally registers `context-guard-mcp` in `.cursor/mcp.json`
16
+ via `--with-mcp`.
17
+ - `cg new` automatically detects Cursor and materialises `.cursor/rules/context-guard.mdc`
18
+ in newly created changes.
19
+ - Documentation for Cursor's cooperative approval gate model in
20
+ `docs/adapters/cursor/PERMISSIONS.md`.
21
+
22
+ ### Changed
23
+
24
+ - Unified Gemini/Antigravity global skill installation path to
25
+ `~/.gemini/config/skills/context-guard/SKILL.md` (the canonical machine-local
26
+ configuration root shared across Gemini CLI, IDE, Desktop, and ACP).
27
+ This fixes broken symlinks when `~/.gemini/config/skills/` was not yet created.
28
+ Any legacy owned skill file at `~/.gemini/antigravity-cli/skills/context-guard/SKILL.md`
29
+ is cleaned up automatically on setup.
30
+
9
31
  ## [2.4.0] - 2026-08-06
10
32
 
11
33
  The bridge between a plan and a change was copy-paste. A phased `PLAN-N.md`
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: context-guard-cli
3
- Version: 2.4.0
3
+ Version: 2.5.0
4
4
  Summary: The transactional memory layer for AI coding agents — your context survives crashes, compaction, and session loss
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -95,7 +95,7 @@ context-guard-cli`**: that installs into whichever venv is currently active —
95
95
  a single project's, if you happen to be inside one — not onto the machine,
96
96
  which silently defeats the point of a global `cg setup`.
97
97
 
98
- `cg setup` detects Claude Code, OpenCode and Antigravity, installs the slash
98
+ `cg setup` detects Claude Code, OpenCode, Antigravity and Cursor, installs the slash
99
99
  commands, and puts `cg approve` behind each one's permission prompt — see
100
100
  [Adapters](#adapters-and-permission-configuration). It prints every file it
101
101
  touched, and running it again changes nothing.
@@ -307,7 +307,7 @@ an open transaction). Activate it once per clone with
307
307
  ## Adapters and permission configuration
308
308
 
309
309
  Thin, per-harness wrappers ship inside the package under
310
- `context_guard/_data/hosts/{claude-code,opencode,antigravity}/` — each points
310
+ `context_guard/_data/hosts/{claude-code,opencode,antigravity,cursor}/` — each points
311
311
  at `phases/{plan,execute,verify}.md` rather than duplicating them. How to put
312
312
  `cg approve` behind each harness's permission prompt is documented in
313
313
  [docs/adapters/](docs/adapters/), one `PERMISSIONS.md` per host, alongside the
@@ -80,7 +80,7 @@ en ese momento — el de un proyecto puntual, si estás parado adentro de
80
80
  uno — no en la máquina, lo que anula en silencio el sentido de un `cg setup`
81
81
  global.
82
82
 
83
- `cg setup` detecta Claude Code, OpenCode y Antigravity, instala los slash
83
+ `cg setup` detecta Claude Code, OpenCode, Antigravity y Cursor, instala los slash
84
84
  commands, y deja `cg approve` detrás del permission prompt de cada uno — ver
85
85
  [Adapters](#adapters-y-configuración-de-permisos). Imprime cada archivo que
86
86
  tocó, y correrlo de nuevo no cambia nada.
@@ -297,7 +297,7 @@ fase completada o una transacción abierta). Activalo una vez por clon con
297
297
  ## Adapters y configuración de permisos
298
298
 
299
299
  Dentro del paquete, en `context_guard/_data/hosts/{claude-code,opencode,
300
- antigravity}/`, viven wrappers finos por harness — cada uno apunta a
300
+ antigravity,cursor}/`, viven wrappers finos por harness — cada uno apunta a
301
301
  `phases/{plan,execute,verify}.md` en vez de duplicarlos. Cómo poner
302
302
  `cg approve` detrás del permission prompt de cada harness está documentado en
303
303
  [docs/adapters/](docs/adapters/), un `PERMISSIONS.md` por host, junto con el
@@ -82,7 +82,7 @@ context-guard-cli`**: that installs into whichever venv is currently active —
82
82
  a single project's, if you happen to be inside one — not onto the machine,
83
83
  which silently defeats the point of a global `cg setup`.
84
84
 
85
- `cg setup` detects Claude Code, OpenCode and Antigravity, installs the slash
85
+ `cg setup` detects Claude Code, OpenCode, Antigravity and Cursor, installs the slash
86
86
  commands, and puts `cg approve` behind each one's permission prompt — see
87
87
  [Adapters](#adapters-and-permission-configuration). It prints every file it
88
88
  touched, and running it again changes nothing.
@@ -294,7 +294,7 @@ an open transaction). Activate it once per clone with
294
294
  ## Adapters and permission configuration
295
295
 
296
296
  Thin, per-harness wrappers ship inside the package under
297
- `context_guard/_data/hosts/{claude-code,opencode,antigravity}/` — each points
297
+ `context_guard/_data/hosts/{claude-code,opencode,antigravity,cursor}/` — each points
298
298
  at `phases/{plan,execute,verify}.md` rather than duplicating them. How to put
299
299
  `cg approve` behind each harness's permission prompt is documented in
300
300
  [docs/adapters/](docs/adapters/), one `PERMISSIONS.md` per host, alongside the
@@ -1,6 +1,6 @@
1
1
  # Tutorial: memoria persistente para tu asistente de programación con context-guard
2
2
 
3
- *Para personas que usan asistentes de IA (Claude Code, OpenCode, Antigravity) y quieren que no se olviden de lo que estaban haciendo. No hace falta ser experto: si sabés abrir una terminal y copiar comandos, podés seguir esta guía.*
3
+ *Para personas que usan asistentes de IA (Claude Code, OpenCode, Antigravity, Cursor) y quieren que no se olviden de lo que estaban haciendo. No hace falta ser experto: si sabés abrir una terminal y copiar comandos, podés seguir esta guía.*
4
4
 
5
5
  ---
6
6
 
@@ -27,7 +27,7 @@ Una aclaración honesta, porque este proyecto se toma en serio la honestidad: co
27
27
  - **Una computadora con terminal.** macOS, Linux, o Windows con WSL.
28
28
  - **Python 3.10 o más nuevo.** Verificalo con `python3 --version`. Si no lo tenés, instalalo desde [python.org](https://www.python.org/downloads/).
29
29
  - **git.** Verificalo con `git --version`.
30
- - **Un asistente de programación instalado**: Claude Code, OpenCode o Antigravity. Cualquiera de los tres funciona; los comandos del asistente son los mismos.
30
+ - **Un asistente de programación instalado**: Claude Code, OpenCode, Antigravity o Cursor. Cualquiera funciona; los comandos del asistente son los mismos.
31
31
  - **Un proyecto** donde trabajar. Puede ser uno existente o una carpeta nueva; solo necesita ser un repositorio git (si no lo es, entrá a la carpeta y corré `git init`).
32
32
 
33
33
  ---
@@ -0,0 +1,7 @@
1
+ {
2
+ "mcpServers": {
3
+ "context-guard": {
4
+ "command": "context-guard-mcp"
5
+ }
6
+ }
7
+ }
@@ -0,0 +1,20 @@
1
+ ---
2
+ description: Active persistence contract and phase discipline for context-guard
3
+ globs:
4
+ alwaysApply: true
5
+ ---
6
+ <!-- context-guard:begin -->
7
+ ## ACTIVE PERSISTENCE CONTRACT: context-guard
8
+ MANDATORY BOOTSTRAP — read before responding to anything in a project that
9
+ has a `.context-guard/` directory:
10
+ 1. Read `AGENTS.md` at the project root and follow it as your state contract.
11
+ 2. State manager binary: `cg` (or `context-guard`). Operative subcommands:
12
+ begin | commit | rollback | checkpoint | status | next-task | validate
13
+ Human-only subcommand (never run by the agent): `cg approve`.
14
+ If `commit` returns EXIT_APPROVAL_REQUIRED (6): stop and ask the user to
15
+ run `cg approve --change <name>` themselves.
16
+ 3. Check `.context-guard/changes/*/manifest.json` for an active change and
17
+ act accordingly (cold start, resume via `cg status`, or report a stuck
18
+ lock — never fix a stuck lock by editing the manifest by hand).
19
+ 4. Phase instructions live in `.context-guard/phases/{plan,execute,verify}.md`.
20
+ <!-- context-guard:end -->
@@ -17,7 +17,7 @@ from .errors import EXIT_GENERIC, GuardError
17
17
 
18
18
  PHASES = ("plan", "execute", "verify")
19
19
 
20
- HOSTS = ("claude-code", "opencode", "antigravity")
20
+ HOSTS = ("claude-code", "opencode", "antigravity", "cursor")
21
21
 
22
22
  # Which snippets each host ships, keyed by the short name callers pass. The
23
23
  # file is always "<name>.snippet.json"; the mapping exists to be an allowlist,
@@ -26,6 +26,7 @@ SNIPPETS = {
26
26
  "claude-code": ("settings", "mcp"),
27
27
  "opencode": ("agent", "permissions", "mcp"),
28
28
  "antigravity": ("hooks",),
29
+ "cursor": ("mcp",),
29
30
  }
30
31
 
31
32
 
@@ -62,8 +63,7 @@ def iter_host_files(host):
62
63
  `relpath` is relative to the host's directory and always uses forward
63
64
  slashes, because callers join it onto a target directory to write the file
64
65
  out. Raises for an unknown host instead of yielding nothing: a `cg setup`
65
- that installs zero files and exits 0 looks exactly like success.
66
- """
66
+ that installs zero files and exits 0 looks exactly like success."""
67
67
  if host not in HOSTS:
68
68
  raise AssetNotFoundError(f"unknown host '{host}' (expected one of {', '.join(HOSTS)})")
69
69
 
@@ -131,7 +131,7 @@ def parse_args(argv=None):
131
131
  p_new = subparsers.add_parser("new")
132
132
  p_new.add_argument("--context", default=".")
133
133
  p_new.add_argument("name")
134
- p_new.add_argument("--host", choices=["claude", "opencode", "antigravity"],
134
+ p_new.add_argument("--host", choices=["claude", "opencode", "antigravity", "cursor"],
135
135
  default=None,
136
136
  help="Also materialise this host's workspace files "
137
137
  "(Antigravity's rule file). Detected automatically "
@@ -148,7 +148,7 @@ def parse_args(argv=None):
148
148
  # setup takes no --context: it configures hosts, not a change. --project
149
149
  # opts back into 2.0's per-project install for teams committing the config.
150
150
  p_setup = subparsers.add_parser("setup")
151
- p_setup.add_argument("--host", choices=["claude", "opencode", "antigravity", "all"],
151
+ p_setup.add_argument("--host", choices=["claude", "opencode", "antigravity", "cursor", "all"],
152
152
  default="all")
153
153
  p_setup.add_argument("--with-mcp", action="store_true",
154
154
  help="Also register the context-guard-mcp server. "
@@ -37,8 +37,10 @@ from .plan_import import (
37
37
  )
38
38
  from .setup import (
39
39
  antigravity_detected,
40
+ cursor_detected,
40
41
  diverged_phases,
41
42
  materialise_antigravity_rule,
43
+ materialise_cursor_rule,
42
44
  materialise_phases,
43
45
  run_setup,
44
46
  )
@@ -143,6 +145,8 @@ def cmd_new(context, change, host=None):
143
145
  materialise_phases(context)
144
146
  if host == "antigravity" or (host is None and antigravity_detected()):
145
147
  materialise_antigravity_rule(context)
148
+ if host == "cursor" or (host is None and cursor_detected()):
149
+ materialise_cursor_rule(context)
146
150
 
147
151
  return CommandResult(f"SUCCESS|CHANGE_CREATED|{name}|phase=PLAN", EXIT_OK)
148
152
 
@@ -27,6 +27,7 @@ HOST_DIRS = {
27
27
  "claude": "claude-code",
28
28
  "opencode": "opencode",
29
29
  "antigravity": "antigravity",
30
+ "cursor": "cursor",
30
31
  }
31
32
  VALID_HOSTS = tuple(HOST_DIRS) + ("all",)
32
33
 
@@ -214,12 +215,12 @@ def _install_opencode(root, with_mcp, global_scope):
214
215
  return touched
215
216
 
216
217
 
217
- # The path was confirmed against Antigravity CLI 1.1.9 via the /skills panel,
218
- # which lists ~/.gemini/antigravity-cli/skills/ as the user's global location;
219
- # builtin/ is the subtree with checksum, a sibling of this one, not its parent.
220
- # ANTIGRAVITY_GLOBAL_ROOT remains as is: hooks.json does reside in .gemini/config/.
218
+ # ~/.gemini/config/ is the unified configuration root for all Gemini tools
219
+ # (CLI, IDE, ACP, Desktop). Hooks live at ~/.gemini/config/hooks.json and
220
+ # global skills at ~/.gemini/config/skills/<name>/SKILL.md.
221
+ # (~/.gemini/antigravity-cli/skills is a symlink pointing to ~/.gemini/config/skills).
221
222
  ANTIGRAVITY_GLOBAL_ROOT = (".gemini", "config")
222
- ANTIGRAVITY_SKILL_REL = (".gemini", "antigravity-cli", "skills", "context-guard", "SKILL.md")
223
+ ANTIGRAVITY_SKILL_REL = (".gemini", "config", "skills", "context-guard", "SKILL.md")
223
224
 
224
225
  # Both artifacts we write into a user's tree as whole files carry this marker,
225
226
  # so a later run can tell its own output from a file of the same name the user
@@ -320,12 +321,25 @@ def _install_antigravity(root, global_scope, no_hooks=False):
320
321
  skill_text = _embedded_antigravity_file("skills/context-guard/SKILL.md")
321
322
  if skill_text is not None:
322
323
  skill_rel = os.path.join(*ANTIGRAVITY_SKILL_REL)
323
- written, skip = _write_owned(os.path.join(root, skill_rel), skill_text)
324
+ skill_dest = os.path.join(root, skill_rel)
325
+ written, skip = _write_owned(skill_dest, skill_text)
324
326
  if written:
325
327
  touched.append(skill_rel.replace(os.sep, "/"))
326
328
  if skip:
327
329
  skips.append(skip)
328
330
 
331
+ # Legacy skill path cleanup if it was installed as a real file previously
332
+ legacy_rel = os.path.join(".gemini", "antigravity-cli", "skills", "context-guard", "SKILL.md")
333
+ legacy_path = os.path.join(root, legacy_rel)
334
+ if (os.path.exists(legacy_path) and
335
+ os.path.realpath(legacy_path) != os.path.realpath(skill_dest)):
336
+ try:
337
+ with open(legacy_path, "r", encoding="utf-8") as f:
338
+ if OWNERSHIP_MARKER in f.read():
339
+ os.remove(legacy_path)
340
+ except OSError:
341
+ pass
342
+
329
343
  # `--no-hooks` declines the enforcement, not the discovery. A user who
330
344
  # does not want the deny hook still wants their agent to know the tool
331
345
  # is there.
@@ -368,6 +382,36 @@ def _install_antigravity(root, global_scope, no_hooks=False):
368
382
  return touched, None, skips
369
383
 
370
384
 
385
+ def _install_cursor(root, with_mcp, global_scope):
386
+ """Cursor adapter installs .cursor/rules/context-guard.mdc (in both global
387
+ and project scopes) and optionally registers context-guard-mcp in .cursor/mcp.json.
388
+ """
389
+ touched = []
390
+ skips = []
391
+
392
+ for relpath, text in iter_host_files("cursor"):
393
+ if relpath == "rules/context-guard.mdc":
394
+ rule_rel = os.path.join(".cursor", "rules", "context-guard.mdc")
395
+ written, skip = _write_owned(os.path.join(root, rule_rel), text)
396
+ if written:
397
+ touched.append(rule_rel.replace(os.sep, "/"))
398
+ if skip:
399
+ skips.append(skip)
400
+ break
401
+
402
+ if with_mcp:
403
+ mcp_rel = os.path.join(".cursor", "mcp.json")
404
+ mcp_path = os.path.join(root, mcp_rel)
405
+ snippet = json.loads(read_snippet("cursor", "mcp"))
406
+ cfg = _read_json(mcp_path) or {}
407
+ cfg.setdefault("mcpServers", {}).setdefault(
408
+ "context-guard", snippet["mcpServers"]["context-guard"])
409
+ _write_json(mcp_path, cfg)
410
+ touched.append(mcp_rel.replace(os.sep, "/"))
411
+
412
+ return touched, skips
413
+
414
+
371
415
  # ---------------------------------------------------------------------------
372
416
  # Detection
373
417
  # ---------------------------------------------------------------------------
@@ -398,6 +442,9 @@ def _detected(host, home):
398
442
  or os.path.isdir(os.path.join(home, ".gemini", "antigravity-cli"))
399
443
  or _on_path("antigravity")
400
444
  or _on_path("agy"))
445
+ if host == "cursor":
446
+ return (os.path.isdir(os.path.join(home, ".cursor"))
447
+ or _on_path("cursor"))
401
448
  return False
402
449
 
403
450
 
@@ -465,6 +512,15 @@ def run_setup(host="all", with_mcp=False, project=None, no_hooks=False):
465
512
  else:
466
513
  lines.append(" -> Antigravity: skill installed, deny hook merged "
467
514
  "into ~/.gemini/config/hooks.json")
515
+ elif name == "cursor":
516
+ cursor_touched, cursor_skips = _install_cursor(
517
+ root, with_mcp, global_scope)
518
+ touched += cursor_touched
519
+ skips += cursor_skips
520
+ if with_mcp:
521
+ lines.append(" -> Cursor: rule installed in .cursor/rules, MCP registered in .cursor/mcp.json")
522
+ else:
523
+ lines.append(" -> Cursor: rule installed in .cursor/rules (cooperative approval gate)")
468
524
 
469
525
  for name in HOST_DIRS:
470
526
  if name in selected:
@@ -549,6 +605,22 @@ def antigravity_detected():
549
605
  return _detected("antigravity", _home())
550
606
 
551
607
 
608
+ def materialise_cursor_rule(context):
609
+ """Write Cursor rule file, unless the project already has one."""
610
+ path = os.path.join(context, ".cursor", "rules", "context-guard.mdc")
611
+ if os.path.exists(path):
612
+ return []
613
+ for relpath, text in iter_host_files("cursor"):
614
+ if relpath == "rules/context-guard.mdc":
615
+ _write_text(path, text)
616
+ return [path]
617
+ return []
618
+
619
+
620
+ def cursor_detected():
621
+ return _detected("cursor", _home())
622
+
623
+
552
624
  def diverged_phases(context):
553
625
  """Phase files that exist but differ from the embedded copy."""
554
626
  from .assets import PHASES, get_phase
@@ -17,7 +17,7 @@ that has to work first for 4-5 to mean anything.
17
17
 
18
18
  ```bash
19
19
  cd /path/to/toy-project
20
- cg setup --host <claude|opencode|antigravity>
20
+ cg setup --host <claude|opencode|antigravity|cursor>
21
21
  ```
22
22
 
23
23
  Confirm the files land where the tests say they should:
@@ -88,3 +88,4 @@ today.
88
88
  - [x] Claude Code — verified by: fdomerlo on: 2026-08-02
89
89
  - [x] OpenCode — verified by: fdomerlo on: 2026-08-02
90
90
  - [x] Antigravity — verified by: fdomerlo on: 2026-08-02
91
+ - [ ] Cursor — unverified
@@ -0,0 +1,42 @@
1
+ # Cursor — permission configuration
2
+
3
+ `cg approve` is the only command in context-guard that a human is supposed to
4
+ run. Everything else the agent drives itself.
5
+
6
+ ## What this buys you
7
+
8
+ The approval gate inside `cg` is **cooperative**: `commit --next-phase EXECUTE`
9
+ refuses without a recorded approval (exit code 6), but an agent with a shell
10
+ can run `cg approve` itself. A confirmation that happens outside the agent's
11
+ process is the only hard control in the model — see PLAN.md 0.6.
12
+
13
+ ## How to configure it
14
+
15
+ Cursor does not currently provide a per-command shell deny hook (like Antigravity's
16
+ `PreToolUse` hook in `hooks.json`) or a fine-grained command permission list (like
17
+ Claude Code's `permissions.ask`).
18
+
19
+ Therefore, enforcement in Cursor is **cooperative**:
20
+
21
+ 1. **Keep terminal confirmation enabled.** Leave terminal command confirmation
22
+ enabled in Cursor settings so terminal commands — `cg approve` included —
23
+ surface for human confirmation before execution.
24
+ 2. **Keep the workspace rule installed.** `.cursor/rules/context-guard.mdc`
25
+ (installed by `cg setup --host cursor`) instructs the agent that `cg approve`
26
+ is human-only and directs it to pause and request human approval when `commit`
27
+ returns exit code 6.
28
+
29
+ If auto-run terminal commands is enabled in Cursor, understand that enforcement
30
+ becomes purely cooperative, and the manifest's `approval_history` records what
31
+ the agent decided rather than what you explicitly gated.
32
+
33
+ ## MCP registration
34
+
35
+ `cg setup --host cursor --with-mcp` automatically registers `context-guard-mcp`
36
+ in `.cursor/mcp.json`. Registration is optional; `cg` works completely via CLI
37
+ without MCP.
38
+
39
+ ## Unverified
40
+
41
+ **This adapter is unverified.** It is covered by static tests and has not been
42
+ tested in an interactive end-to-end Cursor session.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "context-guard-cli"
7
- version = "2.4.0"
7
+ version = "2.5.0"
8
8
  description = "The transactional memory layer for AI coding agents — your context survives crashes, compaction, and session loss"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -95,7 +95,7 @@ class TestPermissionDocsPerHarness(unittest.TestCase):
95
95
  it ships the cooperative half and calls it enforcement.
96
96
  """
97
97
 
98
- HOSTS = ("claude-code", "opencode", "antigravity")
98
+ HOSTS = ("claude-code", "opencode", "antigravity", "cursor")
99
99
 
100
100
  def _permissions_doc(self, host):
101
101
  path = os.path.join(DOCS_DIR, host, "PERMISSIONS.md")
@@ -128,7 +128,7 @@ class TestPermissionDocsPerHarness(unittest.TestCase):
128
128
  """PLAN.md backlog: the OpenCode and Antigravity adapters were never run
129
129
  against a real host. Documenting them as if they were is the kind of
130
130
  claim the audits exist to falsify."""
131
- for host in ("opencode", "antigravity"):
131
+ for host in ("opencode", "antigravity", "cursor"):
132
132
  text = self._permissions_doc(host).lower()
133
133
  self.assertIn("unverified", text)
134
134
 
@@ -416,3 +416,24 @@ class TestVerifyChecklist(unittest.TestCase):
416
416
  at all."""
417
417
  self.assertRegex(self.text, r"- \[[ x]\]")
418
418
 
419
+
420
+
421
+ CURSOR_DIR = os.path.join(HOSTS_DIR, "cursor")
422
+
423
+
424
+ class TestCursorAdapter(unittest.TestCase):
425
+ RULE_PATH = os.path.join(CURSOR_DIR, "rules", "context-guard.mdc")
426
+ MCP_PATH = os.path.join(CURSOR_DIR, "mcp.snippet.json")
427
+
428
+ def test_rule_file_exists(self):
429
+ self.assertTrue(os.path.exists(self.RULE_PATH), "cursor rule file missing")
430
+ with open(self.RULE_PATH, "r", encoding="utf-8") as f:
431
+ content = f.read()
432
+ self.assertIn("cg approve", content)
433
+ self.assertIn("AGENTS.md", content)
434
+
435
+ def test_mcp_snippet_is_valid_json(self):
436
+ self.assertTrue(os.path.exists(self.MCP_PATH), "cursor mcp snippet missing")
437
+ with open(self.MCP_PATH, "r", encoding="utf-8") as f:
438
+ data = json.load(f)
439
+ self.assertEqual(data["mcpServers"]["context-guard"]["command"], "context-guard-mcp")
@@ -41,6 +41,8 @@ EXPECTED_DATA_FILES = {
41
41
  "hosts/antigravity/rules/context-guard.md",
42
42
  "hosts/antigravity/skills/context-guard/SKILL.md",
43
43
  "hosts/antigravity/hooks.snippet.json",
44
+ "hosts/cursor/rules/context-guard.mdc",
45
+ "hosts/cursor/mcp.snippet.json",
44
46
  }
45
47
 
46
48
  EXPECTED_HOST_FILES = {
@@ -62,6 +64,10 @@ EXPECTED_HOST_FILES = {
62
64
  "skills/context-guard/SKILL.md",
63
65
  "hooks.snippet.json",
64
66
  },
67
+ "cursor": {
68
+ "rules/context-guard.mdc",
69
+ "mcp.snippet.json",
70
+ },
65
71
  }
66
72
 
67
73
  SNIPPETS = (
@@ -71,6 +77,7 @@ SNIPPETS = (
71
77
  ("opencode", "permissions"),
72
78
  ("opencode", "mcp"),
73
79
  ("antigravity", "hooks"),
80
+ ("cursor", "mcp"),
74
81
  )
75
82
 
76
83
 
@@ -130,7 +130,7 @@ for name in ("plan", "execute", "verify"):
130
130
  for host, snippet in %(snippets)r:
131
131
  out["snippets"]["%%s/%%s" %% (host, snippet)] = assets.read_snippet(host, snippet)
132
132
  out["hosts"] = {h: sorted(r for r, _ in assets.iter_host_files(h))
133
- for h in ("claude-code", "opencode", "antigravity")}
133
+ for h in ("claude-code", "opencode", "antigravity", "cursor")}
134
134
  json.dump(out, sys.stdout)
135
135
  """
136
136
 
@@ -238,7 +238,7 @@ json.dump(out, sys.stdout)
238
238
  # three hosts must each end up with a *discoverable*
239
239
  # entry point. Antigravity's is this skill; the hook
240
240
  # above is enforcement, which nobody discovers.
241
- ".gemini/antigravity-cli/skills/context-guard/SKILL.md"):
241
+ ".gemini/config/skills/context-guard/SKILL.md"):
242
242
  with self.subTest(path=relpath):
243
243
  self.assertTrue(os.path.exists(os.path.join(home, relpath)),
244
244
  f"{relpath} was not configured by cg setup")
@@ -23,18 +23,15 @@ SPANISH_INDICATORS = ["á", "é", "í", "ó", "ú", "ñ", "¿", "¡"]
23
23
 
24
24
 
25
25
  class TestPyprojectVersion(unittest.TestCase):
26
- def test_version_is_2_4_0(self):
27
- """A minor bump: `cg new --from-plan` is additive, nothing removed
28
- from the public surface. Pinned here so the release tag and the
29
- CHANGELOG cannot drift from what actually ships — which is exactly
30
- what this caught: the first attempt at this release tagged v2.4.0
31
- over a tree still pinned to 2.5.0, and the publish workflow's test
32
- job would have refused to ship it."""
26
+ def test_version_is_2_5_0(self):
27
+ """A minor bump: Cursor adapter and unified Gemini config path.
28
+ Additive, nothing removed from the public surface. Pinned here so
29
+ the release tag and the CHANGELOG cannot drift from what actually ships."""
33
30
  with open(PYPROJECT_PATH, "r", encoding="utf-8") as f:
34
31
  text = f.read()
35
32
  match = re.search(r'^version\s*=\s*"([^"]+)"', text, re.MULTILINE)
36
33
  self.assertIsNotNone(match, "version not declared")
37
- self.assertEqual(match.group(1), "2.4.0")
34
+ self.assertEqual(match.group(1), "2.5.0")
38
35
 
39
36
  def test_description_is_the_one_sentence_pitch(self):
40
37
  """PLAN.md 0.7's pitch doubles as the PyPI project description — the
@@ -543,7 +543,7 @@ class SkillCase(SetupCase):
543
543
  """
544
544
 
545
545
  def skill_path(self):
546
- return self.home_path(".gemini", "antigravity-cli", "skills", "context-guard", "SKILL.md")
546
+ return self.home_path(".gemini", "config", "skills", "context-guard", "SKILL.md")
547
547
 
548
548
  def skill_text(self):
549
549
  with open(self.skill_path(), "r", encoding="utf-8") as f:
@@ -594,7 +594,7 @@ class TestAntigravityGetsADiscoverableEntryPoint(SkillCase):
594
594
 
595
595
  def test_the_skill_is_listed_as_touched(self):
596
596
  res = self.setup(host="antigravity")
597
- self.assertIn(".gemini/antigravity-cli/skills/context-guard/SKILL.md", res.message)
597
+ self.assertIn(".gemini/config/skills/context-guard/SKILL.md", res.message)
598
598
 
599
599
  def test_a_second_run_rewrites_it_byte_identically(self):
600
600
  self.setup(host="antigravity")
@@ -742,7 +742,7 @@ class TestEveryHostGetsADiscoveryEntryPoint(SetupCase):
742
742
  GLOBAL_DISCOVERY = {
743
743
  "claude": ".claude/commands/cg-new.md",
744
744
  "opencode": ".config/opencode/commands/cg-new.md",
745
- "antigravity": ".gemini/antigravity-cli/skills/context-guard/SKILL.md",
745
+ "antigravity": ".gemini/config/skills/context-guard/SKILL.md",
746
746
  }
747
747
 
748
748
  PROJECT_DISCOVERY = {
@@ -1055,3 +1055,61 @@ class TestEmbeddedCommandsAreScopeAgnostic(unittest.TestCase):
1055
1055
 
1056
1056
  if __name__ == "__main__":
1057
1057
  unittest.main()
1058
+
1059
+
1060
+
1061
+ class TestCursorSetup(SetupCase):
1062
+ def rule_path(self, global_scope=True):
1063
+ if global_scope:
1064
+ return self.home_path(".cursor", "rules", "context-guard.mdc")
1065
+ return self.project_path(".cursor", "rules", "context-guard.mdc")
1066
+
1067
+ def mcp_path(self, global_scope=True):
1068
+ if global_scope:
1069
+ return self.home_path(".cursor", "mcp.json")
1070
+ return self.project_path(".cursor", "mcp.json")
1071
+
1072
+ def test_cursor_global_setup_installs_rule(self):
1073
+ res = self.setup(host="cursor")
1074
+ self.assertEqual(res.exit_code, EXIT_OK, res.message)
1075
+ self.assertTrue(os.path.exists(self.rule_path(global_scope=True)))
1076
+ with open(self.rule_path(global_scope=True), "r", encoding="utf-8") as f:
1077
+ content = f.read()
1078
+ self.assertIn("ACTIVE PERSISTENCE CONTRACT: context-guard", content)
1079
+
1080
+ def test_cursor_project_setup_installs_rule(self):
1081
+ res = self.setup(host="cursor", project=self.project)
1082
+ self.assertEqual(res.exit_code, EXIT_OK, res.message)
1083
+ self.assertTrue(os.path.exists(self.rule_path(global_scope=False)))
1084
+
1085
+ def test_cursor_with_mcp_registers_server(self):
1086
+ res = self.setup(host="cursor", with_mcp=True)
1087
+ self.assertEqual(res.exit_code, EXIT_OK, res.message)
1088
+ self.assertTrue(os.path.exists(self.mcp_path(global_scope=True)))
1089
+ cfg = self.read_json(self.mcp_path(global_scope=True))
1090
+ self.assertEqual(cfg["mcpServers"]["context-guard"]["command"], "context-guard-mcp")
1091
+
1092
+ def test_cursor_detection_via_dir(self):
1093
+ os.makedirs(self.home_path(".cursor"), exist_ok=True)
1094
+ self.setup(host="all")
1095
+ self.assertTrue(os.path.exists(self.rule_path(global_scope=True)))
1096
+
1097
+ def test_cursor_detection_via_binary(self):
1098
+ self.fake_binary("cursor")
1099
+ self.setup(host="all")
1100
+ self.assertTrue(os.path.exists(self.rule_path(global_scope=True)))
1101
+
1102
+ def test_cursor_not_detected_is_skipped(self):
1103
+ res = self.setup(host="all")
1104
+ self.assertFalse(os.path.exists(self.rule_path(global_scope=True)))
1105
+ self.assertIn("cursor: not detected, skipped", res.message)
1106
+
1107
+ def test_foreign_cursor_rule_not_clobbered(self):
1108
+ mine = "---\ndescription: custom\n---\n# custom rule\n"
1109
+ os.makedirs(os.path.dirname(self.rule_path()), exist_ok=True)
1110
+ with open(self.rule_path(), "w", encoding="utf-8") as f:
1111
+ f.write(mine)
1112
+ res = self.setup(host="cursor")
1113
+ with open(self.rule_path(), "r", encoding="utf-8") as f:
1114
+ self.assertEqual(f.read(), mine)
1115
+ self.assertIn("SKIP|SKILL_EXISTS", res.message)