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.
- context_guard/__init__.py +1 -0
- context_guard/_data/hosts/antigravity/hooks.snippet.json +16 -0
- context_guard/_data/hosts/antigravity/rules/context-guard.md +15 -0
- context_guard/_data/hosts/claude-code/commands/cg-continue.md +13 -0
- context_guard/_data/hosts/claude-code/commands/cg-new.md +11 -0
- context_guard/_data/hosts/claude-code/mcp.snippet.json +7 -0
- context_guard/_data/hosts/claude-code/settings.snippet.json +12 -0
- context_guard/_data/hosts/opencode/agent.snippet.json +7 -0
- context_guard/_data/hosts/opencode/commands/cg-continue.md +16 -0
- context_guard/_data/hosts/opencode/commands/cg-new.md +12 -0
- context_guard/_data/hosts/opencode/mcp.snippet.json +9 -0
- context_guard/_data/hosts/opencode/permissions.snippet.json +12 -0
- context_guard/_data/phases/execute.md +99 -0
- context_guard/_data/phases/plan.md +136 -0
- context_guard/_data/phases/verify.md +129 -0
- context_guard/guard/__init__.py +1 -0
- context_guard/guard/assets.py +94 -0
- context_guard/guard/cli.py +307 -0
- context_guard/guard/commands.py +811 -0
- context_guard/guard/errors.py +71 -0
- context_guard/guard/locking.py +181 -0
- context_guard/guard/manifest.py +69 -0
- context_guard/guard/migrate.py +288 -0
- context_guard/guard/paths.py +199 -0
- context_guard/guard/setup.py +476 -0
- context_guard/guard/transaction.py +403 -0
- context_guard/mcp_server.py +280 -0
- context_guard_cli-2.1.0.dist-info/METADATA +296 -0
- context_guard_cli-2.1.0.dist-info/RECORD +32 -0
- context_guard_cli-2.1.0.dist-info/WHEEL +4 -0
- context_guard_cli-2.1.0.dist-info/entry_points.txt +4 -0
- 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()
|