@trac3er/oh-my-god 2.0.8 → 2.0.9

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 (118) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.gemini/settings.json +11 -0
  4. package/.kimi/mcp.json +11 -0
  5. package/CHANGELOG.md +10 -0
  6. package/OMG-setup.sh +1 -1
  7. package/OMG_COMPAT_CONTRACT.md +10 -4
  8. package/README.md +1 -0
  9. package/build/lib/commands/OMG:forge.md +92 -0
  10. package/build/lib/commands/OMG:mode.md +13 -13
  11. package/build/lib/commands/OMG:session-branch.md +17 -1
  12. package/build/lib/commands/OMG:session-fork.md +5 -1
  13. package/build/lib/commands/OMG:session-merge.md +5 -1
  14. package/build/lib/control_plane/server.py +4 -0
  15. package/build/lib/control_plane/service.py +55 -0
  16. package/build/lib/hooks/setup_wizard.py +21 -1
  17. package/build/lib/hooks/shadow_manager.py +25 -2
  18. package/build/lib/hooks/state_migration.py +3 -0
  19. package/build/lib/plugins/dephealth/cve_scanner.py +91 -0
  20. package/build/lib/plugins/dephealth/vuln_analyzer.py +7 -0
  21. package/build/lib/registry/omg-capability.schema.json +83 -1
  22. package/build/lib/runtime/adoption.py +12 -4
  23. package/build/lib/runtime/artifact_parsers.py +161 -0
  24. package/build/lib/runtime/background_verification.py +48 -0
  25. package/build/lib/runtime/claim_judge.py +184 -7
  26. package/build/lib/runtime/contract_compiler.py +118 -9
  27. package/build/lib/runtime/evidence_query.py +203 -0
  28. package/build/lib/runtime/omg_mcp_server.py +19 -0
  29. package/build/lib/runtime/playwright_adapter.py +39 -0
  30. package/build/lib/runtime/proof_chain.py +136 -8
  31. package/build/lib/runtime/proof_gate.py +102 -0
  32. package/build/lib/runtime/providers/gemini_provider.py +7 -0
  33. package/build/lib/runtime/providers/kimi_provider.py +7 -0
  34. package/build/lib/runtime/repro_pack.py +292 -0
  35. package/build/lib/runtime/runtime_profile.py +87 -15
  36. package/build/lib/runtime/security_check.py +86 -3
  37. package/build/lib/runtime/test_intent_lock.py +47 -0
  38. package/build/lib/runtime/tracebank.py +33 -3
  39. package/build/lib/runtime/verification_loop.py +73 -0
  40. package/commands/OMG:forge.md +92 -0
  41. package/commands/OMG:mode.md +13 -13
  42. package/commands/OMG:session-branch.md +17 -1
  43. package/commands/OMG:session-fork.md +5 -1
  44. package/commands/OMG:session-merge.md +5 -1
  45. package/control_plane/server.py +4 -0
  46. package/control_plane/service.py +55 -0
  47. package/dist/enterprise/bundle/.gemini/settings.json +11 -0
  48. package/dist/enterprise/bundle/.kimi/mcp.json +11 -0
  49. package/dist/enterprise/bundle/OMG_COMPAT_CONTRACT.md +9 -3
  50. package/dist/enterprise/bundle/registry/omg-capability.schema.json +83 -1
  51. package/dist/enterprise/bundle/settings.json +1 -0
  52. package/dist/enterprise/manifest.json +17 -3
  53. package/dist/public/bundle/.agents/skills/omg/incident-replay/SKILL.md +1 -1
  54. package/dist/public/bundle/.agents/skills/omg/incident-replay/openai.yaml +1 -1
  55. package/dist/public/bundle/.agents/skills/omg/lsp-pack/SKILL.md +1 -1
  56. package/dist/public/bundle/.agents/skills/omg/lsp-pack/openai.yaml +1 -1
  57. package/dist/public/bundle/.agents/skills/omg/mcp-fabric/SKILL.md +1 -1
  58. package/dist/public/bundle/.agents/skills/omg/mcp-fabric/openai.yaml +1 -1
  59. package/dist/public/bundle/.agents/skills/omg/plan-council/SKILL.md +1 -1
  60. package/dist/public/bundle/.agents/skills/omg/plan-council/openai.yaml +1 -1
  61. package/dist/public/bundle/.agents/skills/omg/preflight/SKILL.md +1 -1
  62. package/dist/public/bundle/.agents/skills/omg/preflight/openai.yaml +1 -1
  63. package/dist/public/bundle/.agents/skills/omg/proof-gate/SKILL.md +1 -1
  64. package/dist/public/bundle/.agents/skills/omg/proof-gate/openai.yaml +1 -1
  65. package/dist/public/bundle/.agents/skills/omg/remote-supervisor/SKILL.md +1 -1
  66. package/dist/public/bundle/.agents/skills/omg/remote-supervisor/openai.yaml +1 -1
  67. package/dist/public/bundle/.agents/skills/omg/robotics/SKILL.md +1 -1
  68. package/dist/public/bundle/.agents/skills/omg/robotics/openai.yaml +1 -1
  69. package/dist/public/bundle/.agents/skills/omg/secure-worktree-pipeline/SKILL.md +1 -1
  70. package/dist/public/bundle/.agents/skills/omg/secure-worktree-pipeline/openai.yaml +1 -1
  71. package/dist/public/bundle/.agents/skills/omg/security-check/SKILL.md +1 -1
  72. package/dist/public/bundle/.agents/skills/omg/security-check/openai.yaml +1 -1
  73. package/dist/public/bundle/.agents/skills/omg/test-intent-lock/SKILL.md +1 -1
  74. package/dist/public/bundle/.agents/skills/omg/test-intent-lock/openai.yaml +1 -1
  75. package/dist/public/bundle/.agents/skills/omg/tracebank/SKILL.md +1 -1
  76. package/dist/public/bundle/.agents/skills/omg/tracebank/openai.yaml +1 -1
  77. package/dist/public/bundle/.agents/skills/omg/vision/SKILL.md +1 -1
  78. package/dist/public/bundle/.agents/skills/omg/vision/openai.yaml +1 -1
  79. package/dist/public/bundle/.gemini/settings.json +11 -0
  80. package/dist/public/bundle/.kimi/mcp.json +11 -0
  81. package/dist/public/bundle/OMG_COMPAT_CONTRACT.md +9 -3
  82. package/dist/public/bundle/registry/omg-capability.schema.json +83 -1
  83. package/dist/public/bundle/settings.json +2 -1
  84. package/dist/public/manifest.json +43 -29
  85. package/docs/proof.md +1 -0
  86. package/hooks/setup_wizard.py +21 -1
  87. package/hooks/shadow_manager.py +25 -2
  88. package/hooks/state_migration.py +3 -0
  89. package/hud/omg-hud.mjs +66 -3
  90. package/package.json +1 -1
  91. package/plugins/advanced/plugin.json +1 -1
  92. package/plugins/core/plugin.json +1 -1
  93. package/plugins/dephealth/cve_scanner.py +91 -0
  94. package/plugins/dephealth/vuln_analyzer.py +7 -0
  95. package/pyproject.toml +1 -1
  96. package/registry/omg-capability.schema.json +83 -1
  97. package/runtime/adoption.py +13 -5
  98. package/runtime/artifact_parsers.py +161 -0
  99. package/runtime/background_verification.py +48 -0
  100. package/runtime/claim_judge.py +184 -7
  101. package/runtime/contract_compiler.py +118 -9
  102. package/runtime/evidence_query.py +203 -0
  103. package/runtime/omg_mcp_server.py +19 -0
  104. package/runtime/playwright_adapter.py +39 -0
  105. package/runtime/proof_chain.py +136 -8
  106. package/runtime/proof_gate.py +102 -0
  107. package/runtime/providers/gemini_provider.py +7 -0
  108. package/runtime/providers/kimi_provider.py +7 -0
  109. package/runtime/repro_pack.py +292 -0
  110. package/runtime/runtime_profile.py +87 -15
  111. package/runtime/security_check.py +86 -3
  112. package/runtime/test_intent_lock.py +47 -0
  113. package/runtime/tracebank.py +33 -3
  114. package/runtime/verification_loop.py +73 -0
  115. package/scripts/omg.py +30 -3
  116. package/settings.json +4 -3
  117. package/tools/python_sandbox.py +9 -6
  118. package/tools/session_snapshot.py +146 -40
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from typing import cast
5
+
6
+
7
+ def _as_int(value: object, default: int) -> int:
8
+ if isinstance(value, bool):
9
+ return int(value)
10
+ if isinstance(value, int):
11
+ return value
12
+ if isinstance(value, float):
13
+ return int(value)
14
+ if isinstance(value, str):
15
+ try:
16
+ return int(value)
17
+ except ValueError:
18
+ return default
19
+ return default
20
+
21
+
22
+ def _as_string_list(value: object) -> list[str]:
23
+ if not isinstance(value, list):
24
+ return []
25
+ items = cast(list[object], value)
26
+ return [str(item) for item in items]
27
+
28
+
29
+ def build_loop_policy(
30
+ host: str,
31
+ max_iterations: int,
32
+ timeout_minutes: int,
33
+ read_only_default: bool = True,
34
+ ) -> dict[str, object]:
35
+ return {
36
+ "host": host,
37
+ "max_iterations": max_iterations,
38
+ "timeout_minutes": timeout_minutes,
39
+ "read_only_default": read_only_default,
40
+ }
41
+
42
+
43
+ def should_continue_loop(state: Mapping[str, object]) -> dict[str, object]:
44
+ iteration = _as_int(state.get("iteration", 0), 0)
45
+ max_iterations = _as_int(state.get("max_iterations", 0), 0)
46
+ status = str(state.get("status", ""))
47
+
48
+ if iteration >= max_iterations:
49
+ return {"continue": False, "reason": "max_iterations_reached"}
50
+ if status == "ok":
51
+ return {"continue": False, "reason": "status_ok"}
52
+ return {"continue": True, "reason": "within_budget"}
53
+
54
+
55
+ def summarize_next_step(state: Mapping[str, object]) -> dict[str, object]:
56
+ status = str(state.get("status", ""))
57
+ blockers = _as_string_list(state.get("blockers"))
58
+ evidence_links = _as_string_list(state.get("evidence_links"))
59
+
60
+ if blockers:
61
+ next_action = f"resolve blockers: {', '.join(blockers)}"
62
+ elif status in {"error", "blocked"}:
63
+ next_action = "verify evidence links and remediate verification errors"
64
+ elif evidence_links:
65
+ next_action = "verify evidence links"
66
+ else:
67
+ next_action = "collect verification evidence links"
68
+
69
+ return {
70
+ "next_action": next_action,
71
+ "evidence_links": evidence_links,
72
+ "blockers": blockers,
73
+ }
package/scripts/omg.py CHANGED
@@ -10,6 +10,7 @@ Implements practical command-line flows for:
10
10
  - omg trust review
11
11
  - omg runtime dispatch
12
12
  - omg lab train / omg lab eval
13
+ - omg forge run
13
14
  """
14
15
  from __future__ import annotations
15
16
 
@@ -57,7 +58,7 @@ from runtime.compat import (
57
58
  list_compat_skills,
58
59
  run_doctor,
59
60
  )
60
- from runtime.adoption import CANONICAL_VERSION
61
+ from runtime.adoption import CANONICAL_VERSION, VALID_PRESETS
61
62
  from runtime.ecosystem import ecosystem_status, list_ecosystem_repos, sync_ecosystem_repos
62
63
  from runtime.team_router import TeamDispatchRequest, dispatch_team, execute_ccg_mode, execute_crazy_mode
63
64
 
@@ -409,6 +410,22 @@ def cmd_lab_eval(args: argparse.Namespace) -> int:
409
410
  return 0 if out.get("status") == "published" else 2
410
411
 
411
412
 
413
+ def cmd_forge_run(args: argparse.Namespace) -> int:
414
+ preset = args.preset
415
+ if preset != "labs":
416
+ print(
417
+ json.dumps(
418
+ {"status": "error", "message": f"forge requires labs preset, got: {preset}"},
419
+ indent=2,
420
+ )
421
+ )
422
+ return 2
423
+ job = json.loads(args.job_json) if args.job_json else _load_json(args.job)
424
+ result = run_pipeline(job)
425
+ print(json.dumps(result, indent=2))
426
+ return 0 if result.get("status") in {"ready", "failed_evaluation"} else 2
427
+
428
+
412
429
  def cmd_teams(args: argparse.Namespace) -> int:
413
430
  files = [f.strip() for f in args.files.split(",") if f.strip()] if args.files else []
414
431
  req = TeamDispatchRequest(
@@ -633,12 +650,14 @@ def _add_contract_subcommands(parent: argparse.ArgumentParser, *, dest: str) ->
633
650
  contract_validate = contract_sub.add_parser("validate", help="Validate contract doc, schema, and bundle registry")
634
651
  contract_validate.set_defaults(func=cmd_contract_validate)
635
652
 
636
- contract_compile = contract_sub.add_parser("compile", help="Compile Claude/Codex artifacts from the canonical contract")
653
+ contract_compile = contract_sub.add_parser(
654
+ "compile", help="Compile host artifacts from the canonical contract"
655
+ )
637
656
  contract_compile.add_argument(
638
657
  "--host",
639
658
  dest="hosts",
640
659
  action="append",
641
- choices=["claude", "codex"],
660
+ choices=["claude", "codex", "gemini", "kimi"],
642
661
  required=True,
643
662
  help="Host to compile (repeat for multiple hosts)",
644
663
  )
@@ -816,6 +835,14 @@ def build_parser() -> argparse.ArgumentParser:
816
835
  lab_eval.add_argument("--result-json", default="", help="Inline result json")
817
836
  lab_eval.set_defaults(func=cmd_lab_eval)
818
837
 
838
+ forge = sub.add_parser("forge", help="Labs-only domain-model prototyping and evaluation")
839
+ forge_sub = forge.add_subparsers(dest="forge_command", required=True)
840
+ forge_run = forge_sub.add_parser("run", help="Run a forge job through the lab pipeline")
841
+ forge_run.add_argument("--job", default="", help="Path to job json")
842
+ forge_run.add_argument("--job-json", default="", help="Inline job json")
843
+ forge_run.add_argument("--preset", default="labs", choices=list(VALID_PRESETS), help="Adoption preset (must be labs)")
844
+ forge_run.set_defaults(func=cmd_forge_run)
845
+
819
846
  teams = sub.add_parser("teams", help="Internal OMG team routing")
820
847
  teams.add_argument("--target", default="auto", choices=["auto", "codex", "gemini", "ccg"])
821
848
  teams.add_argument("--problem", required=True)
package/settings.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
3
- "_comment": "OMG 2.0.8 - project-level config with hook registrations, presets, and feature flags.",
3
+ "_comment": "OMG 2.0.9 - project-level config with hook registrations, presets, and feature flags.",
4
4
  "permissions": {
5
5
  "allow": [
6
6
  "Agent",
@@ -306,8 +306,9 @@
306
306
  ]
307
307
  },
308
308
  "_omg": {
309
- "_version": "2.0.8",
309
+ "_version": "2.0.9",
310
310
  "preset": "safe",
311
+ "omgMode": "focused",
311
312
  "default_mode": "ulw+ralph",
312
313
  "vision_auto": true,
313
314
  "false_fix_detection": true,
@@ -359,7 +360,7 @@
359
360
  "CONTEXT_MANAGER": false
360
361
  },
361
362
  "generated": {
362
- "contract_version": "2.0.8",
363
+ "contract_version": "2.0.9",
363
364
  "channel": "enterprise",
364
365
  "required_bundles": [
365
366
  "control-plane",
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """
3
- Security Sandbox for OMG Python REPL
2
+ """Security Sandbox for OMG Python REPL (REPL-only).
4
3
 
5
4
  Provides a restricted execution environment that blocks dangerous operations:
6
5
  - Dangerous imports (subprocess, socket, ctypes, etc.)
@@ -11,6 +10,10 @@ Provides a restricted execution environment that blocks dangerous operations:
11
10
 
12
11
  Feature flag: OMG_REPL_SANDBOX_ENABLED (default: False)
13
12
 
13
+ This module is the concrete REPL-only sandbox implementation. Broader sandbox
14
+ policy is mediated by hook-level controls in hooks/firewall.py and
15
+ hooks/secret-guard.py.
16
+
14
17
  Usage:
15
18
  from tools.python_sandbox import execute_sandboxed, is_safe_code, create_sandbox
16
19
 
@@ -67,7 +70,7 @@ def _is_sandbox_enabled() -> bool:
67
70
 
68
71
  # --- Blocked imports configuration ---
69
72
 
70
- _DEFAULT_BLOCKED_IMPORTS: frozenset = frozenset({
73
+ _DEFAULT_BLOCKED_IMPORTS: frozenset[str] = frozenset({
71
74
  "subprocess",
72
75
  "socket",
73
76
  "ctypes",
@@ -91,13 +94,13 @@ def _get_blocked_imports() -> Set[str]:
91
94
  env_val = os.environ.get("OMG_SANDBOX_BLOCKED_IMPORTS", "").strip()
92
95
  if env_val:
93
96
  custom = frozenset(name.strip() for name in env_val.split(",") if name.strip())
94
- return _DEFAULT_BLOCKED_IMPORTS | custom
97
+ return set(_DEFAULT_BLOCKED_IMPORTS | custom)
95
98
  return set(_DEFAULT_BLOCKED_IMPORTS)
96
99
 
97
100
 
98
101
  # --- Blocked builtins ---
99
102
 
100
- _DANGEROUS_BUILTINS: frozenset = frozenset({
103
+ _DANGEROUS_BUILTINS: frozenset[str] = frozenset({
101
104
  "__import__",
102
105
  "eval",
103
106
  "exec",
@@ -278,7 +281,7 @@ def _check_string_escapes(code: str) -> Optional[str]:
278
281
 
279
282
  # --- Restricted open() ---
280
283
 
281
- _ALLOWED_READ_MODES: frozenset = frozenset({
284
+ _ALLOWED_READ_MODES: frozenset[str] = frozenset({
282
285
  "r", "rb", "rt",
283
286
  "", # default mode is 'r'
284
287
  })
@@ -498,6 +498,17 @@ def detect_merge_conflicts(
498
498
  return conflicts
499
499
 
500
500
 
501
+ _BRANCH_META_KEYS = frozenset({
502
+ "name", "created_at", "snapshot_id", "parent_branch",
503
+ "status", "switched_at", "merged_at", "merged_into",
504
+ })
505
+
506
+
507
+ def _strip_branch_meta(state: Dict[str, Any]) -> Dict[str, Any]:
508
+ """Return *state* without branch-level metadata keys."""
509
+ return {k: v for k, v in state.items() if k not in _BRANCH_META_KEYS}
510
+
511
+
501
512
  def preview_merge(
502
513
  source_branch: str,
503
514
  target_branch: str = "main",
@@ -506,7 +517,8 @@ def preview_merge(
506
517
  """Preview a merge without applying changes.
507
518
 
508
519
  Loads both branch snapshot states (as flat JSON dicts from snapshot
509
- metadata) and detects conflicts.
520
+ metadata), strips branch-level metadata keys that always differ,
521
+ and detects conflicts on the remaining user-state keys.
510
522
 
511
523
  Args:
512
524
  source_branch: Branch to merge from
@@ -529,10 +541,11 @@ def preview_merge(
529
541
  if target_state is None:
530
542
  return {"error": f"Target branch not found: {target_branch}"}
531
543
 
532
- conflicts = detect_merge_conflicts(source_state, target_state)
544
+ source_user = _strip_branch_meta(source_state)
545
+ target_user = _strip_branch_meta(target_state)
546
+ conflicts = detect_merge_conflicts(source_user, target_user)
533
547
 
534
- # Count keys that exist only in source (net new changes)
535
- source_only_keys = set(source_state.keys()) - set(target_state.keys())
548
+ source_only_keys = set(source_user.keys()) - set(target_user.keys())
536
549
  changes = len(source_only_keys) + len(conflicts)
537
550
 
538
551
  return {
@@ -567,11 +580,18 @@ def merge_branch(
567
580
  if not _get_merge_flag_enabled():
568
581
  return {"skipped": True}
569
582
 
570
- preview = preview_merge(source_branch, target_branch, state_dir=state_dir)
571
- if preview.get("error"):
572
- return preview
583
+ source_state = _load_branch_state(source_branch, state_dir=state_dir)
584
+ if source_state is None:
585
+ return {"error": f"Source branch not found: {source_branch}"}
586
+
587
+ target_state = _load_branch_state(target_branch, state_dir=state_dir)
588
+ if target_state is None:
589
+ return {"error": f"Target branch not found: {target_branch}"}
590
+
591
+ source_user = _strip_branch_meta(source_state)
592
+ target_user = _strip_branch_meta(target_state)
593
+ conflicts = detect_merge_conflicts(source_user, target_user)
573
594
 
574
- conflicts = preview.get("conflicts", [])
575
595
  if conflicts:
576
596
  return {
577
597
  "merged": False,
@@ -579,19 +599,11 @@ def merge_branch(
579
599
  "changes_applied": 0,
580
600
  }
581
601
 
582
- # --- Apply merge: last-write-wins (source on top of target) ---
583
- source_state = _load_branch_state(source_branch, state_dir=state_dir)
584
- target_state = _load_branch_state(target_branch, state_dir=state_dir)
585
- if source_state is None or target_state is None:
586
- return {"error": "Branch state became unavailable during merge"}
587
-
588
602
  merged_state = {**target_state, **source_state}
589
- # Preserve target branch name and update status
590
603
  merged_state["name"] = target_branch
591
604
  merged_state["status"] = "active"
592
605
 
593
- # Count actual changes applied
594
- source_only_keys = set(source_state.keys()) - set(target_state.keys())
606
+ source_only_keys = set(source_user.keys()) - set(target_user.keys())
595
607
  changes_applied = len(source_only_keys)
596
608
 
597
609
  # Write merged state to target branch file
@@ -631,36 +643,111 @@ def merge_branch(
631
643
  "changes_applied": changes_applied,
632
644
  }
633
645
 
646
+ def fork_branch(
647
+ from_snapshot_id: str,
648
+ name: str,
649
+ state_dir: str = ".omg/state",
650
+ ) -> Dict[str, Any]:
651
+ """Fork a new branch from a specific snapshot checkpoint.
652
+
653
+ This is a convenience wrapper around ``create_branch`` that always
654
+ requires a source snapshot ID.
655
+
656
+ Args:
657
+ from_snapshot_id: Snapshot ID to fork from (required, non-empty).
658
+ name: Name for the new branch (required, non-empty, no slashes).
659
+ state_dir: Path to the state directory (default: ".omg/state").
660
+
661
+ Returns:
662
+ Branch metadata dict on success, ``{"skipped": True}`` if feature
663
+ flag is disabled, or ``{"error": ...}`` on failure.
664
+ """
665
+ if not _get_branching_flag_enabled():
666
+ return {"skipped": True}
667
+
668
+ if not from_snapshot_id:
669
+ return {"error": "fork_branch requires a non-empty snapshot ID"}
670
+ if not name or "/" in name:
671
+ return {"error": "Invalid branch name: must be non-empty with no slashes"}
672
+
673
+ return create_branch(name, from_snapshot_id=from_snapshot_id, state_dir=state_dir)
674
+
675
+
676
+ # Public alias expected by callers (canonical name is preview_merge)
677
+ merge_preview = preview_merge
678
+
679
+
680
+ def get_status(state_dir: str = ".omg/state") -> Dict[str, Any]:
681
+ """
682
+ Get the current branch name and total snapshot count.
683
+
684
+ Args:
685
+ state_dir: Path to the state directory (default: ".omg/state")
686
+
687
+ Returns:
688
+ Dict with keys: current_branch, snapshot_count
689
+ """
690
+ # Get current branch
691
+ current_branch = None
692
+ current_branch_path = os.path.join(state_dir, "current_branch.json")
693
+ if os.path.exists(current_branch_path):
694
+ try:
695
+ with open(current_branch_path, "r", encoding="utf-8") as f:
696
+ cb = json.load(f)
697
+ current_branch = cb.get("name")
698
+ except (json.JSONDecodeError, OSError):
699
+ pass
700
+
701
+ # Get snapshot count
702
+ snapshots = list_snapshots(state_dir=state_dir)
703
+ snapshot_count = len(snapshots)
704
+
705
+ return {
706
+ "current_branch": current_branch,
707
+ "snapshot_count": snapshot_count,
708
+ }
709
+
710
+
634
711
  def main():
635
712
  """CLI entry point."""
636
- if len(sys.argv) < 2:
713
+ state_dir = os.environ.get("OMG_STATE_DIR", ".omg/state")
714
+
715
+ if len(sys.argv) < 2 or sys.argv[1] in ("--help", "-h"):
716
+ _dest = sys.stdout if (len(sys.argv) > 1 and sys.argv[1] in ("--help", "-h")) else sys.stderr
717
+ _code = 0 if _dest is sys.stdout else 1
637
718
  print(
638
719
  "Usage: python3 session_snapshot.py <command> [options]",
639
- file=sys.stderr,
720
+ file=_dest,
640
721
  )
641
- print("Commands:", file=sys.stderr)
642
- print(" create [--name NAME] Create a snapshot", file=sys.stderr)
643
- print(" list List all snapshots", file=sys.stderr)
644
- print(" restore <snapshot_id> Restore a snapshot", file=sys.stderr)
645
- print(" delete <snapshot_id> Delete a snapshot", file=sys.stderr)
646
- print(" branch <name> Create a branch", file=sys.stderr)
647
- print(" branches List all branches", file=sys.stderr)
648
- print(" switch <name> Switch to a branch", file=sys.stderr)
649
- print(" merge <source> [--into <target>] Merge branches", file=sys.stderr)
650
- print(" merge-preview <source> [--into <target>] Preview merge", file=sys.stderr)
651
- sys.exit(1)
722
+ print("Commands:", file=_dest)
723
+ print(" status Show current branch and snapshot count", file=_dest)
724
+ print(" create [--name NAME] Create a snapshot", file=_dest)
725
+ print(" list List all snapshots", file=_dest)
726
+ print(" restore <snapshot_id> Restore a snapshot", file=_dest)
727
+ print(" delete <snapshot_id> Delete a snapshot", file=_dest)
728
+ print(" branch <name> Create a branch", file=_dest)
729
+ print(" branches List all branches", file=_dest)
730
+ print(" switch <name> Switch to a branch", file=_dest)
731
+ print(" fork --from <snapshot_id> --name <name> Fork from snapshot", file=_dest)
732
+ print(" merge <source> [--into <target>] Merge branches", file=_dest)
733
+ print(" merge-preview <source> [--into <target>] Preview merge", file=_dest)
734
+ sys.exit(_code)
652
735
 
653
736
  command = sys.argv[1]
654
737
 
655
- if command == "create":
738
+ if command == "status":
739
+ result = get_status(state_dir=state_dir)
740
+ print(json.dumps(result, indent=2))
741
+
742
+ elif command == "create":
656
743
  name = None
657
744
  if len(sys.argv) > 3 and sys.argv[2] == "--name":
658
745
  name = sys.argv[3]
659
- result = create_snapshot(name=name)
746
+ result = create_snapshot(name=name, state_dir=state_dir)
660
747
  print(json.dumps(result, indent=2))
661
748
 
662
749
  elif command == "list":
663
- snapshots = list_snapshots()
750
+ snapshots = list_snapshots(state_dir=state_dir)
664
751
  print(json.dumps(snapshots, indent=2))
665
752
 
666
753
  elif command == "restore":
@@ -668,7 +755,7 @@ def main():
668
755
  print("Usage: python3 session_snapshot.py restore <snapshot_id>", file=sys.stderr)
669
756
  sys.exit(1)
670
757
  snapshot_id = sys.argv[2]
671
- success = restore_snapshot(snapshot_id)
758
+ success = restore_snapshot(snapshot_id, state_dir=state_dir)
672
759
  result = {"success": success, "snapshot_id": snapshot_id}
673
760
  print(json.dumps(result, indent=2))
674
761
 
@@ -677,7 +764,7 @@ def main():
677
764
  print("Usage: python3 session_snapshot.py delete <snapshot_id>", file=sys.stderr)
678
765
  sys.exit(1)
679
766
  snapshot_id = sys.argv[2]
680
- success = delete_snapshot(snapshot_id)
767
+ success = delete_snapshot(snapshot_id, state_dir=state_dir)
681
768
  result = {"success": success, "snapshot_id": snapshot_id}
682
769
  print(json.dumps(result, indent=2))
683
770
 
@@ -689,11 +776,11 @@ def main():
689
776
  from_id = None
690
777
  if len(sys.argv) > 4 and sys.argv[3] == "--from":
691
778
  from_id = sys.argv[4]
692
- result = create_branch(branch_name, from_snapshot_id=from_id)
779
+ result = create_branch(branch_name, from_snapshot_id=from_id, state_dir=state_dir)
693
780
  print(json.dumps(result, indent=2))
694
781
 
695
782
  elif command == "branches":
696
- branches = list_branches()
783
+ branches = list_branches(state_dir=state_dir)
697
784
  print(json.dumps(branches, indent=2))
698
785
 
699
786
  elif command == "switch":
@@ -701,7 +788,7 @@ def main():
701
788
  print("Usage: python3 session_snapshot.py switch <name>", file=sys.stderr)
702
789
  sys.exit(1)
703
790
  branch_name = sys.argv[2]
704
- success = switch_branch(branch_name)
791
+ success = switch_branch(branch_name, state_dir=state_dir)
705
792
  result = {"success": success, "branch": branch_name}
706
793
  print(json.dumps(result, indent=2))
707
794
 
@@ -713,7 +800,26 @@ def main():
713
800
  target = "main"
714
801
  if len(sys.argv) > 4 and sys.argv[3] == "--into":
715
802
  target = sys.argv[4]
716
- result = merge_branch(source, target_branch=target)
803
+ result = merge_branch(source, target_branch=target, state_dir=state_dir)
804
+ print(json.dumps(result, indent=2))
805
+
806
+ elif command == "fork":
807
+ from_id = None
808
+ fork_name = None
809
+ i = 2
810
+ while i < len(sys.argv):
811
+ if sys.argv[i] == "--from" and i + 1 < len(sys.argv):
812
+ from_id = sys.argv[i + 1]
813
+ i += 2
814
+ elif sys.argv[i] == "--name" and i + 1 < len(sys.argv):
815
+ fork_name = sys.argv[i + 1]
816
+ i += 2
817
+ else:
818
+ i += 1
819
+ if not from_id or not fork_name:
820
+ print("Usage: python3 session_snapshot.py fork --from <snapshot_id> --name <name>", file=sys.stderr)
821
+ sys.exit(1)
822
+ result = fork_branch(from_snapshot_id=from_id, name=fork_name, state_dir=state_dir)
717
823
  print(json.dumps(result, indent=2))
718
824
 
719
825
  elif command == "merge-preview":
@@ -724,7 +830,7 @@ def main():
724
830
  target = "main"
725
831
  if len(sys.argv) > 4 and sys.argv[3] == "--into":
726
832
  target = sys.argv[4]
727
- result = preview_merge(source, target_branch=target)
833
+ result = preview_merge(source, target_branch=target, state_dir=state_dir)
728
834
  print(json.dumps(result, indent=2))
729
835
 
730
836
  else: