godcode-engine 4.0.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.
godcode/cli.py ADDED
@@ -0,0 +1,698 @@
1
+ """Command-line interface for the God Code engine.
2
+
3
+ Subcommands: run, check, repl, fmt, ledger verify.
4
+ Sibling modules (lexer, parser, ast, interpreter, ledger, errors) are imported
5
+ lazily inside each command so `--help` works even before they land.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import sys
11
+ from pathlib import Path
12
+
13
+
14
+ # ---------------------------------------------------------------------------
15
+ # run
16
+ # ---------------------------------------------------------------------------
17
+ # --- v3: agentics --json ---
18
+ def _make_interpreter(log_path):
19
+ """Build an Interpreter the way `godcode run` always has, shared by the
20
+ plain and --json run paths so they can never drift apart."""
21
+ from godcode.interpreter import Interpreter
22
+
23
+ kwargs: dict = {}
24
+ if log_path:
25
+ kwargs["log_path"] = log_path
26
+ # Bind the Spirit and the covenant ledger by default; degrade
27
+ # gracefully if either cannot be raised in this environment.
28
+ try:
29
+ from godcode.spirit import SpiritEngine
30
+ kwargs["spirit"] = SpiritEngine()
31
+ except Exception:
32
+ pass
33
+ try:
34
+ from godcode.ledger import CovenantLedger
35
+ kwargs["ledger"] = CovenantLedger()
36
+ except Exception:
37
+ pass
38
+ return Interpreter(**kwargs)
39
+ # --- end v3: agentics --json ---
40
+
41
+
42
+ def cmd_run(args: argparse.Namespace) -> int:
43
+ from godcode.errors import GodCodeError
44
+
45
+ # --- v3: sandbox commands ---
46
+ if getattr(args, "sandbox", False):
47
+ return _cmd_run_sandboxed(args)
48
+ # --- end v3: sandbox commands ---
49
+
50
+ # --- v3: agentics --json ---
51
+ if getattr(args, "json", False):
52
+ from godcode import agentics
53
+ return agentics.cmd_run_json(args)
54
+ # --- end v3: agentics --json ---
55
+
56
+ try:
57
+ source = Path(args.file).read_text(encoding="utf-8")
58
+ except OSError as exc:
59
+ print(f"godcode: cannot read '{args.file}': {exc.strerror or exc}",
60
+ file=sys.stderr)
61
+ return 1
62
+
63
+ try:
64
+ _make_interpreter(args.log).run_source(source, source_name=args.file)
65
+ except GodCodeError as err:
66
+ print(str(err), file=sys.stderr)
67
+ return 1
68
+ return 0
69
+
70
+
71
+ # --- v3: sandbox commands ---
72
+ def _cmd_run_sandboxed(args: argparse.Namespace) -> int:
73
+ """Run a scroll under the strict sandbox policy.
74
+
75
+ Spirit, covenant ledger, and the audit log stay unbound: they write
76
+ to the host world, which the sandbox does not permit. The CLI itself
77
+ reads the scroll file before the sandbox is entered — that read is
78
+ the invoker's own act, not the creation's.
79
+ """
80
+ from godcode.errors import GodCodeError
81
+ from godcode.sandbox import SandboxPolicy, run_sandboxed
82
+
83
+ try:
84
+ source = Path(args.file).read_text(encoding="utf-8")
85
+ except OSError as exc:
86
+ if getattr(args, "json", False):
87
+ from godcode import agentics
88
+ agentics.emit(agentics.run_payload(
89
+ args.file, False, [], [],
90
+ agentics._file_error_diagnostic(args.file, exc), 0))
91
+ else:
92
+ print(f"godcode: cannot read '{args.file}': {exc.strerror or exc}",
93
+ file=sys.stderr)
94
+ return 1
95
+
96
+ source_dir = str(Path(args.file).resolve().parent)
97
+ policy = SandboxPolicy.strict(
98
+ source_dir=source_dir,
99
+ timeout_seconds=args.sandbox_timeout,
100
+ )
101
+ if getattr(args, "json", False):
102
+ # Machine-readable report; stdout carries exactly one JSON document.
103
+ import contextlib
104
+ import io
105
+ import time as _time
106
+
107
+ from godcode import agentics
108
+ from godcode.sandbox import run_sandboxed_with_interpreter
109
+
110
+ error = None
111
+ output: list[str] = []
112
+ intents: list[dict] = []
113
+ start = _time.perf_counter()
114
+ with contextlib.redirect_stdout(io.StringIO()):
115
+ try:
116
+ output, interp = run_sandboxed_with_interpreter(
117
+ source, policy, source_name=args.file)
118
+ intents = list(getattr(interp, "intent_checks", []) or [])
119
+ except GodCodeError as err:
120
+ error = agentics.diagnostic(err)
121
+ ms = int((_time.perf_counter() - start) * 1000)
122
+ agentics.emit(agentics.run_payload(
123
+ args.file, error is None, output, [], error, ms,
124
+ intents=intents))
125
+ return 0 if error is None else 1
126
+ try:
127
+ run_sandboxed(source, policy, source_name=args.file)
128
+ except GodCodeError as err:
129
+ print(str(err), file=sys.stderr)
130
+ return 1
131
+ return 0
132
+ # --- end v3: sandbox commands ---
133
+
134
+
135
+ # ---------------------------------------------------------------------------
136
+ # check
137
+ # ---------------------------------------------------------------------------
138
+ def cmd_check(args: argparse.Namespace) -> int:
139
+ from godcode.errors import GodCodeError
140
+ from godcode.lexer import Lexer
141
+ from godcode.parser import Parser
142
+
143
+ # --- v3: agentics --json ---
144
+ if getattr(args, "json", False):
145
+ from godcode import agentics
146
+ return agentics.cmd_check_json(args)
147
+ # --- end v3: agentics --json ---
148
+
149
+ try:
150
+ source = Path(args.file).read_text(encoding="utf-8")
151
+ except OSError as exc:
152
+ print(f"godcode: cannot read '{args.file}': {exc.strerror or exc}",
153
+ file=sys.stderr)
154
+ return 1
155
+ try:
156
+ Parser(Lexer(source).lex()).parse()
157
+ except GodCodeError as err:
158
+ print(str(err), file=sys.stderr)
159
+ return 1
160
+ print(f"✓ {args.file} is pure.")
161
+ return 0
162
+
163
+
164
+ # ---------------------------------------------------------------------------
165
+ # repl
166
+ # ---------------------------------------------------------------------------
167
+ def cmd_repl(args: argparse.Namespace) -> int: # noqa: ARG001
168
+ from godcode.errors import GodCodeError
169
+ from godcode.interpreter import Interpreter
170
+
171
+ try:
172
+ import readline # noqa: F401 (enables history + line editing)
173
+ except ImportError:
174
+ pass
175
+
176
+ print("God Code Live Mode 🕊")
177
+ print("Speak your creation; end each utterance with a blank line. "
178
+ "(:quit to ascend)")
179
+ interp = Interpreter(interactive=True)
180
+ buf: list[str] = []
181
+ while True:
182
+ try:
183
+ line = input("godcode> " if not buf else "...... ")
184
+ except EOFError:
185
+ print()
186
+ break
187
+ except KeyboardInterrupt:
188
+ print()
189
+ break
190
+ if line.strip() in (":quit", ":q"):
191
+ break
192
+ if not line.strip():
193
+ if buf:
194
+ chunk = "\n".join(buf)
195
+ buf = []
196
+ try:
197
+ interp.run_source(chunk, source_name="<repl>")
198
+ except GodCodeError as err:
199
+ print(str(err), file=sys.stderr)
200
+ continue
201
+ buf.append(line)
202
+ print("🕊 The sanctuary rests.")
203
+ return 0
204
+
205
+
206
+ # ---------------------------------------------------------------------------
207
+ # fmt — AST -> canonical God Code
208
+ # ---------------------------------------------------------------------------
209
+ _PRECEDENCE = {
210
+ "or": 1, "and": 2,
211
+ "==": 3, "!=": 3, "<": 3, ">": 3, "<=": 3, ">=": 3,
212
+ "+": 4, "-": 4, "*": 5, "/": 5, "%": 5,
213
+ }
214
+ _UNARY_PREC = 6
215
+ _BLOCK_NODES = {"CreationBlock", "IfStmt", "ForLoop", "WhileLoop", "DefineRite"}
216
+
217
+
218
+ def _escape(text: str) -> str:
219
+ return (text.replace("\\", "\\\\").replace('"', '\\"')
220
+ .replace("\n", "\\n").replace("\t", "\\t"))
221
+
222
+
223
+ class CanonicalFormatter:
224
+ """Re-emit an AST as canonical God Code: keywords UPPER, 2-space indent,
225
+ one statement per line, blank line between top-level blocks."""
226
+
227
+ def __init__(self) -> None:
228
+ self._lines: list[str] = []
229
+ self._depth = 0
230
+
231
+ # -- driver ---------------------------------------------------------
232
+ def format(self, program) -> str:
233
+ prev_was_block = False
234
+ for i, stmt in enumerate(program.statements):
235
+ is_block = type(stmt).__name__ in _BLOCK_NODES
236
+ if i and (is_block or prev_was_block):
237
+ self._lines.append("")
238
+ self._emit_stmt(stmt)
239
+ prev_was_block = is_block
240
+ return "\n".join(self._lines) + "\n"
241
+
242
+ def _emit_stmt(self, node) -> None:
243
+ meth = getattr(self, "_stmt_" + type(node).__name__, None)
244
+ if meth is None:
245
+ raise ValueError(
246
+ f"the formatter knows not this node: {type(node).__name__}")
247
+ meth(node)
248
+
249
+ def _line(self, text: str) -> None:
250
+ self._lines.append(" " * self._depth + text)
251
+
252
+ def _block(self, stmts) -> None:
253
+ self._depth += 1
254
+ for stmt in stmts:
255
+ self._emit_stmt(stmt)
256
+ self._depth -= 1
257
+
258
+ # -- expressions ----------------------------------------------------
259
+ def _expr(self, node, parent_prec: int = 0) -> str:
260
+ kind = type(node).__name__
261
+ if kind == "BinaryOp":
262
+ prec = _PRECEDENCE[node.op]
263
+ op = node.op.upper() if node.op in ("and", "or") else node.op
264
+ text = (f"{self._expr(node.left, prec)} {op} "
265
+ f"{self._expr(node.right, prec + 1)}")
266
+ return f"({text})" if prec < parent_prec else text
267
+ if kind == "UnaryOp":
268
+ inner = self._expr(node.operand, _UNARY_PREC)
269
+ if type(node.operand).__name__ == "UnaryOp":
270
+ inner = f"({inner})"
271
+ return f"NOT {inner}" if node.op == "not" else f"-{inner}"
272
+ if kind == "Literal":
273
+ return self._literal(node.value)
274
+ if kind == "Identifier":
275
+ return node.name
276
+ if kind == "ListLiteral":
277
+ return "[" + ", ".join(self._expr(i) for i in node.items) + "]"
278
+ if kind == "Index":
279
+ return f"{self._expr(node.obj, _UNARY_PREC)}[{self._expr(node.index)}]"
280
+ if kind == "CallExpr":
281
+ return (f"{node.callee}("
282
+ + ", ".join(self._expr(a) for a in node.args) + ")")
283
+ raise ValueError(
284
+ f"the formatter knows not this expression: {kind}")
285
+
286
+ @staticmethod
287
+ def _literal(value) -> str:
288
+ if isinstance(value, str):
289
+ return f'"{_escape(value)}"'
290
+ if value is True:
291
+ return "true"
292
+ if value is False:
293
+ return "false"
294
+ if value is None:
295
+ return "void"
296
+ return repr(value)
297
+
298
+ # -- statements -----------------------------------------------------
299
+ def _stmt_CreationBlock(self, node) -> None:
300
+ self._line("BEGIN CREATION")
301
+ self._block(node.statements)
302
+ self._line("END CREATION")
303
+
304
+ def _stmt_Declare(self, node) -> None:
305
+ value = node.value
306
+ if type(value).__name__ == "ListLiteral":
307
+ rhs = ", ".join(self._expr(i) for i in value.items)
308
+ else:
309
+ rhs = self._expr(value)
310
+ self._line(f"DECLARE {node.name} AS {rhs}")
311
+
312
+ def _stmt_DeclareIntent(self, node) -> None: # v4.0
313
+ self._line(f'DECLARE INTENT "{_escape(node.text)}" ON {node.rite}')
314
+
315
+ def _stmt_Breathe(self, node) -> None:
316
+ self._line(f"BREATHE LIFE INTO {node.name}")
317
+
318
+ def _stmt_Reveal(self, node) -> None:
319
+ self._line(f"REVEAL({self._expr(node.expr)})")
320
+
321
+ def _stmt_Prophesy(self, node) -> None:
322
+ self._line(f"PROPHESY {node.text}".rstrip())
323
+
324
+ def _stmt_Ascend(self, node) -> None:
325
+ self._line("ASCEND")
326
+
327
+ def _stmt_Reflect(self, node) -> None:
328
+ self._line("REFLECT")
329
+
330
+ def _stmt_Bless(self, node) -> None:
331
+ self._line(f"BLESS {node.name}")
332
+
333
+ def _stmt_Anoint(self, node) -> None:
334
+ self._line(f"ANOINT {node.name}")
335
+
336
+ def _stmt_SealStmt(self, node) -> None:
337
+ self._line(f"SEAL {self._expr(node.expr)}")
338
+
339
+ def _stmt_Testify(self, node) -> None:
340
+ self._line(f"TESTIFY {self._expr(node.expr)}")
341
+
342
+ def _stmt_IfStmt(self, node) -> None:
343
+ self._line(f"IF {self._expr(node.cond)} THEN")
344
+ self._block(node.then_body)
345
+ if node.else_body:
346
+ self._line("ELSE")
347
+ self._block(node.else_body)
348
+ self._line("ENDIF")
349
+
350
+ def _stmt_ForLoop(self, node) -> None:
351
+ self._line(f"FOR {node.var} IN {self._expr(node.iterable)}")
352
+ self._block(node.body)
353
+ self._line("ENDFOR")
354
+
355
+ def _stmt_WhileLoop(self, node) -> None:
356
+ self._line(f"WHILE {self._expr(node.cond)} DO")
357
+ self._block(node.body)
358
+ self._line("ENDWHILE")
359
+
360
+ def _stmt_DefineRite(self, node) -> None:
361
+ params = ", ".join(node.params)
362
+ self._line(f"DEFINE RITE {node.name}({params})")
363
+ self._block(node.body)
364
+ self._line("END RITE")
365
+
366
+ def _stmt_Return(self, node) -> None:
367
+ self._line("RETURN" if node.expr is None
368
+ else f"RETURN {self._expr(node.expr)}")
369
+
370
+ def _stmt_Import(self, node) -> None:
371
+ self._line(f'IMPORT "{_escape(node.path)}"')
372
+
373
+ def _stmt_ExprStmt(self, node) -> None:
374
+ expr = node.expr
375
+ if type(expr).__name__ == "CallExpr":
376
+ args = ", ".join(self._expr(a) for a in expr.args)
377
+ self._line(f"INVOKE {expr.callee}({args})")
378
+ else:
379
+ self._line(self._expr(expr))
380
+
381
+
382
+ def cmd_fmt(args: argparse.Namespace) -> int:
383
+ from godcode.errors import GodCodeError
384
+ from godcode.lexer import Lexer
385
+ from godcode.parser import Parser
386
+
387
+ src_path = Path(args.file)
388
+ try:
389
+ source = src_path.read_text(encoding="utf-8")
390
+ except OSError as exc:
391
+ print(f"godcode: cannot read '{args.file}': {exc.strerror or exc}",
392
+ file=sys.stderr)
393
+ return 1
394
+ try:
395
+ program = Parser(Lexer(source).lex()).parse()
396
+ except GodCodeError as err:
397
+ print(str(err), file=sys.stderr)
398
+ return 1
399
+
400
+ canonical = CanonicalFormatter().format(program)
401
+ if args.in_place:
402
+ src_path.write_text(canonical, encoding="utf-8")
403
+ else:
404
+ sys.stdout.write(canonical)
405
+ return 0
406
+
407
+
408
+ # ---------------------------------------------------------------------------
409
+ # ledger verify
410
+ # ---------------------------------------------------------------------------
411
+ def cmd_ledger_verify(args: argparse.Namespace) -> int:
412
+ # v4.0: verifies the covenant chain AND the anchor chain, reporting both.
413
+ from godcode.chain import SimulatedChainAdapter
414
+ from godcode.ledger import CovenantLedger
415
+
416
+ cov_ok, cov_message = CovenantLedger(args.file).verify()
417
+ anc_ok, anc_message = SimulatedChainAdapter(args.anchor_file).verify_chain()
418
+ print(cov_message)
419
+ print(anc_message)
420
+ return 0 if (cov_ok and anc_ok) else 1
421
+
422
+
423
+ # --- v3: scroll commands ---
424
+ # Pillar 2 — Scroll Registry: publish/install/info/list installable scrolls.
425
+
426
+
427
+ def _scroll_registry():
428
+ from godcode.registry import ScrollRegistry
429
+
430
+ return ScrollRegistry()
431
+
432
+
433
+ def cmd_scroll_list(args: argparse.Namespace) -> int: # noqa: ARG001
434
+ from godcode.registry import ScrollError
435
+
436
+ try:
437
+ rows = _scroll_registry().list_installed()
438
+ except ScrollError as exc:
439
+ print(f"godcode: {exc}", file=sys.stderr)
440
+ return 1
441
+ if not rows:
442
+ print("No scrolls installed. "
443
+ "Publish one with `godcode scroll publish <dir>`, "
444
+ "then `godcode scroll install <name>`.")
445
+ return 0
446
+ for row in rows:
447
+ loc = ",".join(row["locations"])
448
+ vers = ", ".join(row["versions"])
449
+ print(f"{row['name']} {vers} [{loc}]")
450
+ return 0
451
+
452
+
453
+ def cmd_scroll_install(args: argparse.Namespace) -> int:
454
+ from godcode.registry import ScrollError
455
+
456
+ try:
457
+ receipt = _scroll_registry().install(
458
+ args.name, version=args.version, project=args.project)
459
+ except ScrollError as exc:
460
+ print(f"godcode: {exc}", file=sys.stderr)
461
+ return 1
462
+ where = "project-local" if args.project else "user-global"
463
+ print(f"Installed {receipt['name']} {receipt['version']} "
464
+ f"({where}: {receipt['manifest']['description'][:60]}...)")
465
+ return 0
466
+
467
+
468
+ def cmd_scroll_publish(args: argparse.Namespace) -> int:
469
+ from godcode.registry import ScrollError
470
+
471
+ try:
472
+ manifest = _scroll_registry().publish(args.dir)
473
+ except ScrollError as exc:
474
+ print(f"godcode: {exc}", file=sys.stderr)
475
+ return 1
476
+ print(f"Published {manifest['name']} {manifest['version']} "
477
+ f"to the local registry.")
478
+ return 0
479
+
480
+
481
+ def cmd_scroll_info(args: argparse.Namespace) -> int:
482
+ from godcode.registry import ScrollError
483
+
484
+ try:
485
+ info = _scroll_registry().info(args.name)
486
+ except ScrollError as exc:
487
+ print(f"godcode: {exc}", file=sys.stderr)
488
+ return 1
489
+ print(f"name: {info['name']}")
490
+ if info["manifest"]:
491
+ manifest = info["manifest"]
492
+ print(f"version: {info['latest']} (latest published)")
493
+ print(f"author: {manifest['author']}")
494
+ print(f"entry: {manifest['entry']}")
495
+ print(f"godcode: {manifest['godcode']}")
496
+ print(f"description: {manifest['description']}")
497
+ else:
498
+ print("published: (not in the local registry)")
499
+ if info["published"]:
500
+ print(f"published: {', '.join(info['published'])}")
501
+ if info["installed"]:
502
+ print(f"installed: {', '.join(info['installed'])} "
503
+ f"(project: {', '.join(info['installed_project']) or '--'}; "
504
+ f"user: {', '.join(info['installed_user']) or '--'})")
505
+ else:
506
+ print(f"installed: (nowhere -- `godcode scroll install "
507
+ f"{info['name']}` to receive it)")
508
+ return 0
509
+
510
+
511
+ def _add_scroll_commands(sub) -> None:
512
+ p_scroll = sub.add_parser("scroll", help="Scroll Registry commands")
513
+ scroll_sub = p_scroll.add_subparsers(dest="scroll_command", required=True)
514
+
515
+ p_list = scroll_sub.add_parser("list", help="List installed scrolls")
516
+ p_list.set_defaults(func=cmd_scroll_list)
517
+
518
+ p_install = scroll_sub.add_parser("install",
519
+ help="Install a scroll from the registry")
520
+ p_install.add_argument("name", help="Scroll name, e.g. json-tools")
521
+ p_install.add_argument("--version", default=None, metavar="X.Y.Z",
522
+ help="Exact version (default: latest published)")
523
+ p_install.add_argument("--project", action="store_true",
524
+ help="Install project-local (.godcode/scrolls/) "
525
+ "instead of user-global (~/.godcode/scrolls/)")
526
+ p_install.set_defaults(func=cmd_scroll_install)
527
+
528
+ p_publish = scroll_sub.add_parser("publish",
529
+ help="Publish a scroll dir to the registry")
530
+ p_publish.add_argument("dir", help="Directory holding scroll.toml")
531
+ p_publish.set_defaults(func=cmd_scroll_publish)
532
+
533
+ p_info = scroll_sub.add_parser("info",
534
+ help="Show a scroll's manifest and state")
535
+ p_info.add_argument("name", help="Scroll name")
536
+ p_info.set_defaults(func=cmd_scroll_info)
537
+
538
+
539
+ # --- end v3: scroll commands ---
540
+
541
+ # --- v3: lsp commands ---
542
+ def cmd_lsp(args: argparse.Namespace) -> int: # noqa: ARG001
543
+ from godcode.lsp import serve
544
+
545
+ return serve()
546
+ # --- end v3: lsp commands ---
547
+
548
+ # --- v4.0: intent command ---
549
+ def cmd_intent(args: argparse.Namespace) -> int:
550
+ """`godcode intent "some words" [--json]`: resolve intent via the Spirit."""
551
+ import json as _json
552
+
553
+ from godcode.spirit import SpiritEngine
554
+
555
+ result = SpiritEngine().resolve_intent(args.text)
556
+ if getattr(args, "json", False):
557
+ print(_json.dumps(
558
+ {"tool": "godcode", "command": "intent",
559
+ "text": args.text, "result": result},
560
+ ensure_ascii=False))
561
+ return 0
562
+ pct = round(result["confidence"] * 100)
563
+ print(f"[INTENT] The Spirit discerns: '{result['intent']}' "
564
+ f"({pct}% certainty).")
565
+ print(f"Spiritual intent: {result['spiritual_intent']}")
566
+ print(f"Counsel: {result['suggestion']}")
567
+ aligned = result.get("aligned_with") or []
568
+ if aligned:
569
+ for entry in aligned:
570
+ print(f"Aligned with: {entry['rite']} (\"{entry['declared']}\")")
571
+ else:
572
+ print("Aligned with no declared intent.")
573
+ return 0
574
+ # --- end v4.0: intent command ---
575
+
576
+
577
+ # --- v4.0: tools + bridge commands (implemented in godcode.tools) ---
578
+ def cmd_tools(args: argparse.Namespace) -> int:
579
+ from godcode.tools import cmd_tools as _cmd_tools
580
+
581
+ return _cmd_tools(args)
582
+
583
+
584
+ def cmd_bridge(args: argparse.Namespace) -> int:
585
+ from godcode.tools import cmd_bridge as _cmd_bridge
586
+
587
+ return _cmd_bridge(args)
588
+ # --- end v4.0 ---
589
+
590
+
591
+ # ---------------------------------------------------------------------------
592
+ # parser assembly
593
+ # ---------------------------------------------------------------------------
594
+ def build_parser() -> argparse.ArgumentParser:
595
+ parser = argparse.ArgumentParser(
596
+ prog="godcode",
597
+ description="God Code — the language of divine computation 🕊")
598
+ sub = parser.add_subparsers(dest="command", required=True)
599
+
600
+ p_run = sub.add_parser("run", help="Execute a God Code scroll")
601
+ p_run.add_argument("file", help="Path to the .god scroll")
602
+ p_run.add_argument("--log", default=None, metavar="PATH",
603
+ help="Audit log path (default: logs/godcode.log)")
604
+ # --- v3: sandbox commands ---
605
+ p_run.add_argument("--sandbox", action="store_true",
606
+ help="Run under the strict sandbox policy "
607
+ "(deny fs writes, network, subprocesses, "
608
+ "stdin; scroll imports limited to the "
609
+ "scroll's own directory and the stdlib "
610
+ "scrolls; time and step budgets enforced)")
611
+ p_run.add_argument("--sandbox-timeout", type=float, default=5.0,
612
+ metavar="SECS",
613
+ help="Wall-clock grant for --sandbox runs "
614
+ "(default: 5.0 seconds)")
615
+ # --- end v3: sandbox commands ---
616
+ # --- v3: agentics --json ---
617
+ p_run.add_argument("--json", action="store_true",
618
+ help="Emit a machine-readable JSON report on stdout")
619
+ # --- end v3: agentics --json ---
620
+ p_run.set_defaults(func=cmd_run)
621
+
622
+ p_check = sub.add_parser("check",
623
+ help="Lex and parse a scroll without running it")
624
+ p_check.add_argument("file", help="Path to the .god scroll")
625
+ # --- v3: agentics --json ---
626
+ p_check.add_argument("--json", action="store_true",
627
+ help="Emit a machine-readable JSON report on stdout")
628
+ # --- end v3: agentics --json ---
629
+ p_check.set_defaults(func=cmd_check)
630
+
631
+ p_repl = sub.add_parser("repl", help="Enter the live sanctuary")
632
+ p_repl.set_defaults(func=cmd_repl)
633
+
634
+ p_fmt = sub.add_parser("fmt", help="Re-emit a scroll in canonical form")
635
+ p_fmt.add_argument("file", help="Path to the .god scroll")
636
+ p_fmt.add_argument("--in-place", "-w", dest="in_place",
637
+ action="store_true",
638
+ help="Rewrite the file instead of printing")
639
+ p_fmt.set_defaults(func=cmd_fmt)
640
+
641
+ p_ledger = sub.add_parser("ledger", help="Covenant ledger commands")
642
+ ledger_sub = p_ledger.add_subparsers(dest="ledger_command", required=True)
643
+ p_verify = ledger_sub.add_parser("verify",
644
+ help="Verify the covenant chain")
645
+ p_verify.add_argument("--file", default="covenant.chain", metavar="PATH",
646
+ help="Chain file (default: covenant.chain)")
647
+ # --- v4.0: the anchor chain is verified alongside the covenant chain ---
648
+ p_verify.add_argument("--anchor-file", default="anchors.chain",
649
+ metavar="PATH",
650
+ help="Anchor chain file (default: anchors.chain)")
651
+ # --- end v4.0 ---
652
+ p_verify.set_defaults(func=cmd_ledger_verify)
653
+
654
+ # --- v4.0: intent, tools, bridge commands ---
655
+ p_intent = sub.add_parser("intent",
656
+ help="Resolve the intent behind words "
657
+ "with the Spirit Engine")
658
+ p_intent.add_argument("text", help="Words to resolve the intent of")
659
+ p_intent.add_argument("--json", action="store_true",
660
+ help="Emit a machine-readable JSON report on stdout")
661
+ p_intent.set_defaults(func=cmd_intent)
662
+
663
+ p_tools = sub.add_parser("tools",
664
+ help="Show the MCP-compatible agent tool schemas")
665
+ p_tools.add_argument("--json", action="store_true",
666
+ help="Emit the schemas as JSON")
667
+ p_tools.set_defaults(func=cmd_tools)
668
+
669
+ p_bridge = sub.add_parser("bridge",
670
+ help="Serve the agent tool bridge "
671
+ "(JSON-RPC 2.0 over stdio)")
672
+ p_bridge.set_defaults(func=cmd_bridge)
673
+ # --- end v4.0 ---
674
+
675
+ # --- v3: scroll commands ---
676
+ _add_scroll_commands(sub)
677
+ # --- end v3: scroll commands ---
678
+
679
+ # --- v3: lsp commands ---
680
+ p_lsp = sub.add_parser("lsp",
681
+ help="Start the language server over stdio")
682
+ p_lsp.set_defaults(func=cmd_lsp)
683
+ # --- end v3: lsp commands ---
684
+
685
+ return parser
686
+
687
+
688
+ def main(argv=None) -> int:
689
+ """Entry point. Returns 0 on success; raises SystemExit(code) on failure."""
690
+ args = build_parser().parse_args(argv)
691
+ code = args.func(args)
692
+ if code:
693
+ raise SystemExit(code)
694
+ return 0
695
+
696
+
697
+ if __name__ == "__main__":
698
+ sys.exit(main())