@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.gemini/settings.json +11 -0
- package/.kimi/mcp.json +11 -0
- package/CHANGELOG.md +10 -0
- package/OMG-setup.sh +1 -1
- package/OMG_COMPAT_CONTRACT.md +10 -4
- package/README.md +1 -0
- package/build/lib/commands/OMG:forge.md +92 -0
- package/build/lib/commands/OMG:mode.md +13 -13
- package/build/lib/commands/OMG:session-branch.md +17 -1
- package/build/lib/commands/OMG:session-fork.md +5 -1
- package/build/lib/commands/OMG:session-merge.md +5 -1
- package/build/lib/control_plane/server.py +4 -0
- package/build/lib/control_plane/service.py +55 -0
- package/build/lib/hooks/setup_wizard.py +21 -1
- package/build/lib/hooks/shadow_manager.py +25 -2
- package/build/lib/hooks/state_migration.py +3 -0
- package/build/lib/plugins/dephealth/cve_scanner.py +91 -0
- package/build/lib/plugins/dephealth/vuln_analyzer.py +7 -0
- package/build/lib/registry/omg-capability.schema.json +83 -1
- package/build/lib/runtime/adoption.py +12 -4
- package/build/lib/runtime/artifact_parsers.py +161 -0
- package/build/lib/runtime/background_verification.py +48 -0
- package/build/lib/runtime/claim_judge.py +184 -7
- package/build/lib/runtime/contract_compiler.py +118 -9
- package/build/lib/runtime/evidence_query.py +203 -0
- package/build/lib/runtime/omg_mcp_server.py +19 -0
- package/build/lib/runtime/playwright_adapter.py +39 -0
- package/build/lib/runtime/proof_chain.py +136 -8
- package/build/lib/runtime/proof_gate.py +102 -0
- package/build/lib/runtime/providers/gemini_provider.py +7 -0
- package/build/lib/runtime/providers/kimi_provider.py +7 -0
- package/build/lib/runtime/repro_pack.py +292 -0
- package/build/lib/runtime/runtime_profile.py +87 -15
- package/build/lib/runtime/security_check.py +86 -3
- package/build/lib/runtime/test_intent_lock.py +47 -0
- package/build/lib/runtime/tracebank.py +33 -3
- package/build/lib/runtime/verification_loop.py +73 -0
- package/commands/OMG:forge.md +92 -0
- package/commands/OMG:mode.md +13 -13
- package/commands/OMG:session-branch.md +17 -1
- package/commands/OMG:session-fork.md +5 -1
- package/commands/OMG:session-merge.md +5 -1
- package/control_plane/server.py +4 -0
- package/control_plane/service.py +55 -0
- package/dist/enterprise/bundle/.gemini/settings.json +11 -0
- package/dist/enterprise/bundle/.kimi/mcp.json +11 -0
- package/dist/enterprise/bundle/OMG_COMPAT_CONTRACT.md +9 -3
- package/dist/enterprise/bundle/registry/omg-capability.schema.json +83 -1
- package/dist/enterprise/bundle/settings.json +1 -0
- package/dist/enterprise/manifest.json +17 -3
- package/dist/public/bundle/.agents/skills/omg/incident-replay/SKILL.md +1 -1
- package/dist/public/bundle/.agents/skills/omg/incident-replay/openai.yaml +1 -1
- package/dist/public/bundle/.agents/skills/omg/lsp-pack/SKILL.md +1 -1
- package/dist/public/bundle/.agents/skills/omg/lsp-pack/openai.yaml +1 -1
- package/dist/public/bundle/.agents/skills/omg/mcp-fabric/SKILL.md +1 -1
- package/dist/public/bundle/.agents/skills/omg/mcp-fabric/openai.yaml +1 -1
- package/dist/public/bundle/.agents/skills/omg/plan-council/SKILL.md +1 -1
- package/dist/public/bundle/.agents/skills/omg/plan-council/openai.yaml +1 -1
- package/dist/public/bundle/.agents/skills/omg/preflight/SKILL.md +1 -1
- package/dist/public/bundle/.agents/skills/omg/preflight/openai.yaml +1 -1
- package/dist/public/bundle/.agents/skills/omg/proof-gate/SKILL.md +1 -1
- package/dist/public/bundle/.agents/skills/omg/proof-gate/openai.yaml +1 -1
- package/dist/public/bundle/.agents/skills/omg/remote-supervisor/SKILL.md +1 -1
- package/dist/public/bundle/.agents/skills/omg/remote-supervisor/openai.yaml +1 -1
- package/dist/public/bundle/.agents/skills/omg/robotics/SKILL.md +1 -1
- package/dist/public/bundle/.agents/skills/omg/robotics/openai.yaml +1 -1
- package/dist/public/bundle/.agents/skills/omg/secure-worktree-pipeline/SKILL.md +1 -1
- package/dist/public/bundle/.agents/skills/omg/secure-worktree-pipeline/openai.yaml +1 -1
- package/dist/public/bundle/.agents/skills/omg/security-check/SKILL.md +1 -1
- package/dist/public/bundle/.agents/skills/omg/security-check/openai.yaml +1 -1
- package/dist/public/bundle/.agents/skills/omg/test-intent-lock/SKILL.md +1 -1
- package/dist/public/bundle/.agents/skills/omg/test-intent-lock/openai.yaml +1 -1
- package/dist/public/bundle/.agents/skills/omg/tracebank/SKILL.md +1 -1
- package/dist/public/bundle/.agents/skills/omg/tracebank/openai.yaml +1 -1
- package/dist/public/bundle/.agents/skills/omg/vision/SKILL.md +1 -1
- package/dist/public/bundle/.agents/skills/omg/vision/openai.yaml +1 -1
- package/dist/public/bundle/.gemini/settings.json +11 -0
- package/dist/public/bundle/.kimi/mcp.json +11 -0
- package/dist/public/bundle/OMG_COMPAT_CONTRACT.md +9 -3
- package/dist/public/bundle/registry/omg-capability.schema.json +83 -1
- package/dist/public/bundle/settings.json +2 -1
- package/dist/public/manifest.json +43 -29
- package/docs/proof.md +1 -0
- package/hooks/setup_wizard.py +21 -1
- package/hooks/shadow_manager.py +25 -2
- package/hooks/state_migration.py +3 -0
- package/hud/omg-hud.mjs +66 -3
- package/package.json +1 -1
- package/plugins/advanced/plugin.json +1 -1
- package/plugins/core/plugin.json +1 -1
- package/plugins/dephealth/cve_scanner.py +91 -0
- package/plugins/dephealth/vuln_analyzer.py +7 -0
- package/pyproject.toml +1 -1
- package/registry/omg-capability.schema.json +83 -1
- package/runtime/adoption.py +13 -5
- package/runtime/artifact_parsers.py +161 -0
- package/runtime/background_verification.py +48 -0
- package/runtime/claim_judge.py +184 -7
- package/runtime/contract_compiler.py +118 -9
- package/runtime/evidence_query.py +203 -0
- package/runtime/omg_mcp_server.py +19 -0
- package/runtime/playwright_adapter.py +39 -0
- package/runtime/proof_chain.py +136 -8
- package/runtime/proof_gate.py +102 -0
- package/runtime/providers/gemini_provider.py +7 -0
- package/runtime/providers/kimi_provider.py +7 -0
- package/runtime/repro_pack.py +292 -0
- package/runtime/runtime_profile.py +87 -15
- package/runtime/security_check.py +86 -3
- package/runtime/test_intent_lock.py +47 -0
- package/runtime/tracebank.py +33 -3
- package/runtime/verification_loop.py +73 -0
- package/scripts/omg.py +30 -3
- package/settings.json +4 -3
- package/tools/python_sandbox.py +9 -6
- 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(
|
|
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.
|
|
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.
|
|
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.
|
|
363
|
+
"contract_version": "2.0.9",
|
|
363
364
|
"channel": "enterprise",
|
|
364
365
|
"required_bundles": [
|
|
365
366
|
"control-plane",
|
package/tools/python_sandbox.py
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
571
|
-
if
|
|
572
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
720
|
+
file=_dest,
|
|
640
721
|
)
|
|
641
|
-
print("Commands:", file=
|
|
642
|
-
print("
|
|
643
|
-
print("
|
|
644
|
-
print("
|
|
645
|
-
print("
|
|
646
|
-
print("
|
|
647
|
-
print("
|
|
648
|
-
print("
|
|
649
|
-
print("
|
|
650
|
-
print("
|
|
651
|
-
|
|
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 == "
|
|
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:
|