context-guard-cli 2.1.0__py3-none-any.whl

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 (32) hide show
  1. context_guard/__init__.py +1 -0
  2. context_guard/_data/hosts/antigravity/hooks.snippet.json +16 -0
  3. context_guard/_data/hosts/antigravity/rules/context-guard.md +15 -0
  4. context_guard/_data/hosts/claude-code/commands/cg-continue.md +13 -0
  5. context_guard/_data/hosts/claude-code/commands/cg-new.md +11 -0
  6. context_guard/_data/hosts/claude-code/mcp.snippet.json +7 -0
  7. context_guard/_data/hosts/claude-code/settings.snippet.json +12 -0
  8. context_guard/_data/hosts/opencode/agent.snippet.json +7 -0
  9. context_guard/_data/hosts/opencode/commands/cg-continue.md +16 -0
  10. context_guard/_data/hosts/opencode/commands/cg-new.md +12 -0
  11. context_guard/_data/hosts/opencode/mcp.snippet.json +9 -0
  12. context_guard/_data/hosts/opencode/permissions.snippet.json +12 -0
  13. context_guard/_data/phases/execute.md +99 -0
  14. context_guard/_data/phases/plan.md +136 -0
  15. context_guard/_data/phases/verify.md +129 -0
  16. context_guard/guard/__init__.py +1 -0
  17. context_guard/guard/assets.py +94 -0
  18. context_guard/guard/cli.py +307 -0
  19. context_guard/guard/commands.py +811 -0
  20. context_guard/guard/errors.py +71 -0
  21. context_guard/guard/locking.py +181 -0
  22. context_guard/guard/manifest.py +69 -0
  23. context_guard/guard/migrate.py +288 -0
  24. context_guard/guard/paths.py +199 -0
  25. context_guard/guard/setup.py +476 -0
  26. context_guard/guard/transaction.py +403 -0
  27. context_guard/mcp_server.py +280 -0
  28. context_guard_cli-2.1.0.dist-info/METADATA +296 -0
  29. context_guard_cli-2.1.0.dist-info/RECORD +32 -0
  30. context_guard_cli-2.1.0.dist-info/WHEEL +4 -0
  31. context_guard_cli-2.1.0.dist-info/entry_points.txt +4 -0
  32. context_guard_cli-2.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,307 @@
1
+ """CLI entrypoint for guard middleware.
2
+
3
+ This is the ONLY module that calls sys.exit() and print().
4
+ All business logic is in commands.py.
5
+ """
6
+
7
+ import argparse
8
+ import json
9
+ import sys
10
+
11
+ from .commands import (
12
+ cmd_approve,
13
+ cmd_check_lock,
14
+ cmd_new,
15
+ cmd_setup,
16
+ cmd_list,
17
+ cmd_migrate,
18
+ cmd_claim,
19
+ cmd_release,
20
+ cmd_claim_task,
21
+ cmd_release_task,
22
+ cmd_check_completion,
23
+ cmd_validate,
24
+ cmd_next_task,
25
+ cmd_status,
26
+ cmd_doctor,
27
+ cmd_archive,
28
+ cmd_begin,
29
+ cmd_commit,
30
+ cmd_rollback,
31
+ cmd_checkpoint,
32
+ )
33
+ from .errors import GuardError
34
+
35
+
36
+ def parse_args(argv=None):
37
+ """Parse CLI arguments."""
38
+ parser = argparse.ArgumentParser(description="Context Guard State Manager")
39
+ parser.add_argument("--format", choices=["text", "json"], default="text",
40
+ help="Output format (default: text)")
41
+ subparsers = parser.add_subparsers(dest="command", required=True)
42
+
43
+ # -- Transacciones y Checkpoints --
44
+ p_begin = subparsers.add_parser("begin")
45
+ p_begin.add_argument("--context", default=".")
46
+ p_begin.add_argument("--phase", required=True)
47
+ p_begin.add_argument("--ttl", type=int, default=1800)
48
+
49
+ p_commit = subparsers.add_parser("commit")
50
+ p_commit.add_argument("--context", default=".")
51
+ p_commit.add_argument("--next-phase", required=True)
52
+
53
+ # Human-only: the agent must ask for this to be run, not run it. Pair it
54
+ # with your harness's permission prompt (see adapters/*/PERMISSIONS.md).
55
+ p_approve = subparsers.add_parser("approve")
56
+ p_approve.add_argument("--context", default=".")
57
+ p_approve.add_argument("--by", default=None,
58
+ help="Who is approving (required)")
59
+ p_approve.add_argument("--hotfix", action="store_true",
60
+ help="Skip PLAN and open EXECUTE directly; requires --reason")
61
+ p_approve.add_argument("--reason", default=None,
62
+ help="Why the pipeline is being skipped; persisted in the manifest")
63
+
64
+ p_rollback = subparsers.add_parser("rollback")
65
+ p_rollback.add_argument("--context", default=".")
66
+
67
+ p_checkpoint = subparsers.add_parser("checkpoint")
68
+ p_checkpoint.add_argument("--context", default=".")
69
+ p_checkpoint.add_argument("--summary", required=True)
70
+
71
+ # -- Sesión --
72
+ p_check = subparsers.add_parser("check-lock")
73
+ p_check.add_argument("--context", default=".")
74
+
75
+ p_claim = subparsers.add_parser("claim")
76
+ p_claim.add_argument("--context", default=".")
77
+ p_claim.add_argument("--ttl", type=int, default=1800)
78
+
79
+ p_acq = subparsers.add_parser("acquire")
80
+ p_acq.add_argument("--context", default=".")
81
+ p_acq.add_argument("--ttl", type=int, default=1800)
82
+
83
+ p_release = subparsers.add_parser("release")
84
+ p_release.add_argument("--context", default=".")
85
+ p_release.add_argument("--agent-id", default=None,
86
+ help="Identity of the lock owner (required unless --force)")
87
+ p_release.add_argument("--force", action="store_true",
88
+ help="Release regardless of ownership; recorded in the manifest")
89
+
90
+ # -- Tareas --
91
+ p_claim_task = subparsers.add_parser("claim-task")
92
+ p_claim_task.add_argument("--context", default=".")
93
+ p_claim_task.add_argument("--task-id", required=True)
94
+ p_claim_task.add_argument("--agent-id", default=None)
95
+
96
+ p_release_task = subparsers.add_parser("release-task")
97
+ p_release_task.add_argument("--context", default=".")
98
+ p_release_task.add_argument("--task-id", required=True)
99
+ p_release_task.add_argument("--agent-id", default=None,
100
+ help="Validate ownership before release")
101
+ p_release_task.add_argument("--force", action="store_true",
102
+ help="Skip ownership validation")
103
+
104
+ # -- Utilidades --
105
+ p_completion = subparsers.add_parser("check-completion")
106
+ p_completion.add_argument("--context", default=".")
107
+
108
+ p_validate = subparsers.add_parser("validate")
109
+ p_validate.add_argument("--context", default=".")
110
+ p_validate.add_argument("--max-length", type=int, default=None,
111
+ help="Override max artifact size")
112
+
113
+ p_next = subparsers.add_parser("next-task")
114
+ p_next.add_argument("--context", default=".")
115
+ p_next.add_argument("--agent-id", default=None)
116
+
117
+ p_status = subparsers.add_parser("status")
118
+ p_status.add_argument("--context", default=".")
119
+
120
+ p_doctor = subparsers.add_parser("doctor")
121
+ p_doctor.add_argument("--context", default=".")
122
+ p_doctor.add_argument("--fix", action="store_true",
123
+ help="Release task claims whose owning PID is gone")
124
+
125
+ # -- Archive --
126
+ p_archive = subparsers.add_parser("archive")
127
+ p_archive.add_argument("--context", default=".")
128
+
129
+ # -- Changes --
130
+ p_new = subparsers.add_parser("new")
131
+ p_new.add_argument("--context", default=".")
132
+ p_new.add_argument("name")
133
+ p_new.add_argument("--host", choices=["claude", "opencode", "antigravity"],
134
+ default=None,
135
+ help="Also materialise this host's workspace files "
136
+ "(Antigravity's rule file). Detected automatically "
137
+ "when omitted.")
138
+
139
+ # setup takes no --context: it configures hosts, not a change. --project
140
+ # opts back into 2.0's per-project install for teams committing the config.
141
+ p_setup = subparsers.add_parser("setup")
142
+ p_setup.add_argument("--host", choices=["claude", "opencode", "antigravity", "all"],
143
+ default="all")
144
+ p_setup.add_argument("--with-mcp", action="store_true",
145
+ help="Also register the context-guard-mcp server. "
146
+ "Optional: every adapter works without it — MCP "
147
+ "is an alternative transport, not a requirement.")
148
+ p_setup.add_argument("--project", default=None,
149
+ help="Install into this project instead of the user's "
150
+ "home directory.")
151
+ p_setup.add_argument("--no-hooks", action="store_true",
152
+ help="Skip Antigravity's PreToolUse deny hook. That "
153
+ "hook is what stops the agent from running "
154
+ "cg approve itself; without it Antigravity has "
155
+ "no enforcement of the approval gate, only the "
156
+ "workspace rule asking it not to.")
157
+
158
+ p_list = subparsers.add_parser("list")
159
+ p_list.add_argument("--context", default=".")
160
+
161
+ p_migrate = subparsers.add_parser("migrate")
162
+ p_migrate.add_argument("--context", default=".")
163
+
164
+ # Every context-scoped command accepts --change. Omitting it is only safe
165
+ # when exactly one change is active; ambiguity is an error, never a guess.
166
+ for sub in subparsers.choices.values():
167
+ if sub in (p_list, p_new, p_migrate, p_setup):
168
+ continue
169
+ sub.add_argument("--change", default=None,
170
+ help="Change to operate on (required if several are active)")
171
+
172
+ return parser.parse_args(argv)
173
+
174
+
175
+ def dispatch(args):
176
+ """Route parsed args to the corresponding command function.
177
+
178
+ Returns:
179
+ CommandResult
180
+ """
181
+ change = getattr(args, "change", None)
182
+ handlers = {
183
+ "new": lambda: cmd_new(args.context, args.name, args.host),
184
+ "setup": lambda: cmd_setup(
185
+ host=args.host, with_mcp=args.with_mcp, project=args.project,
186
+ no_hooks=args.no_hooks),
187
+ "list": lambda: cmd_list(args.context),
188
+ "migrate": lambda: cmd_migrate(args.context),
189
+ "begin": lambda: cmd_begin(args.context, args.phase, args.ttl, change),
190
+ "commit": lambda: cmd_commit(args.context, args.next_phase, change),
191
+ "approve": lambda: cmd_approve(
192
+ args.context, args.by, args.hotfix, args.reason, change),
193
+ "rollback": lambda: cmd_rollback(args.context, change),
194
+ "checkpoint": lambda: cmd_checkpoint(args.context, args.summary, change),
195
+ "check-lock": lambda: cmd_check_lock(args.context, change),
196
+ "claim": lambda: cmd_claim(args.context, args.ttl, change),
197
+ "acquire": lambda: cmd_claim(args.context, args.ttl, change), # alias
198
+ "release": lambda: cmd_release(args.context, args.agent_id, args.force, change),
199
+ "claim-task": lambda: cmd_claim_task(
200
+ args.context, args.task_id, args.agent_id, change=change,
201
+ ),
202
+ "release-task": lambda: cmd_release_task(
203
+ args.context, args.task_id, args.agent_id, args.force, change,
204
+ ),
205
+ "check-completion": lambda: cmd_check_completion(args.context, change),
206
+ "validate": lambda: cmd_validate(
207
+ args.context, getattr(args, "max_length", None), change),
208
+ "next-task": lambda: cmd_next_task(
209
+ args.context, getattr(args, "agent_id", None), change),
210
+ "status": lambda: cmd_status(args.context, change),
211
+ "doctor": lambda: cmd_doctor(args.context, args.fix, change),
212
+ "archive": lambda: cmd_archive(args.context, change),
213
+ }
214
+ return handlers[args.command]()
215
+
216
+
217
+
218
+ def _to_json(message, exit_code, command=None):
219
+ """Convert a command result message to JSON.
220
+
221
+ Handles pipe-delimited (single-line), key=value (multi-line),
222
+ and prose (multi-line) output formats.
223
+ """
224
+ lines = message.strip().split("\n")
225
+
226
+ if len(lines) > 1:
227
+ if any("=" in line.strip() for line in lines if line.strip()):
228
+ return _kv_to_json(lines, exit_code, command)
229
+ result = {"output": message.strip()}
230
+ if command:
231
+ result["command"] = command
232
+ result["exit_code"] = exit_code
233
+ return json.dumps(result)
234
+
235
+ line = lines[0].strip()
236
+ parts = line.split("|")
237
+ result = {"status": parts[0]}
238
+ if command:
239
+ result["command"] = command
240
+ if len(parts) > 1:
241
+ result["action"] = parts[1]
242
+ if len(parts) > 2:
243
+ result["details"] = parts[2:]
244
+ result["exit_code"] = exit_code
245
+ return json.dumps(result)
246
+
247
+
248
+ def _kv_to_json(lines, exit_code, command=None):
249
+ """Convert key=value lines to JSON. Used by check-completion."""
250
+ result = {}
251
+ if command:
252
+ result["command"] = command
253
+ current_source = None
254
+ sources = []
255
+ for line in lines:
256
+ line = line.strip()
257
+ if not line:
258
+ if current_source:
259
+ sources.append(current_source)
260
+ current_source = None
261
+ continue
262
+ if "=" in line:
263
+ key, _, value = line.partition("=")
264
+ if value == "true":
265
+ value = True
266
+ elif value == "false":
267
+ value = False
268
+ else:
269
+ try:
270
+ value = int(value)
271
+ except ValueError:
272
+ pass
273
+ if key == "source":
274
+ current_source = {"source": value}
275
+ elif current_source is not None:
276
+ current_source[key] = value
277
+ else:
278
+ result[key] = value
279
+ if current_source:
280
+ sources.append(current_source)
281
+ if sources:
282
+ result["sources"] = sources
283
+ result["exit_code"] = exit_code
284
+ return json.dumps(result)
285
+
286
+
287
+ def main(argv=None):
288
+ """Main entrypoint. Parses args, dispatches, handles errors."""
289
+ args = parse_args(argv)
290
+ fmt = args.format
291
+ try:
292
+ result = dispatch(args)
293
+ if fmt == "json":
294
+ print(_to_json(result.message, result.exit_code, args.command))
295
+ else:
296
+ print(result.message)
297
+ sys.exit(result.exit_code)
298
+ except GuardError as e:
299
+ if fmt == "json":
300
+ print(_to_json(e.message, e.exit_code, args.command))
301
+ else:
302
+ print(e.message)
303
+ sys.exit(e.exit_code)
304
+
305
+
306
+ if __name__ == "__main__":
307
+ main()