relaypy 0.1.0__tar.gz

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.
@@ -0,0 +1,44 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ lint:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: astral-sh/ruff-action@v3
15
+
16
+ type-check:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: actions/setup-python@v5
21
+ with:
22
+ python-version: '3.11'
23
+ - run: pip install mypy
24
+ - run: mypy src/
25
+
26
+ test:
27
+ runs-on: ubuntu-latest
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+ - uses: actions/setup-python@v5
31
+ with:
32
+ python-version: '3.11'
33
+ - run: pip install -e ".[dev]"
34
+ - run: pytest tests/ -v --cov=src
35
+
36
+ security:
37
+ runs-on: ubuntu-latest
38
+ steps:
39
+ - uses: actions/checkout@v4
40
+ - uses: actions/setup-python@v5
41
+ with:
42
+ python-version: '3.11'
43
+ - run: pip install bandit
44
+ - run: bandit -r src/ -x tests/
relaypy-0.1.0/ADR.md ADDED
@@ -0,0 +1,41 @@
1
+ # ADR: thegent-cli-share
2
+
3
+ ## ADR-001: Python over Rust for CLI Coordination Layer
4
+
5
+ **Status**: Accepted
6
+
7
+ **Context**: thegent-cli-share needs to coordinate Python-based agent processes. Implementation language choices: Rust (for performance), Python (for ecosystem fit), Go.
8
+
9
+ **Decision**: Python, implemented as a library that other Python agent processes import directly.
10
+
11
+ **Rationale**: thegent agents are Python processes. A Python library is directly importable without IPC overhead. Rust would require a separate daemon process. Python asyncio is sufficient for coordination at the expected scale (< 100 concurrent agents).
12
+
13
+ **Consequences**: Coordination is in-process for single-host deployments. Multi-host coordination would require a separate service layer (future work).
14
+
15
+ ---
16
+
17
+ ## ADR-002: File-Based State Store for Coordination Primitives
18
+
19
+ **Status**: Accepted
20
+
21
+ **Context**: Distributed locks, task queues, and command caches need a shared state store. Options: Redis, PostgreSQL, filesystem with fcntl locks.
22
+
23
+ **Decision**: Filesystem-based state store using atomic file operations and fcntl advisory locks for single-host deployments.
24
+
25
+ **Rationale**: No external service dependency. thegent is a local developer tool; Redis/PostgreSQL is overkill. fcntl locks are reliable on Linux/macOS. Filesystem state is inspectable for debugging.
26
+
27
+ **Consequences**: Not suitable for multi-host deployments without a storage backend swap. Backend is abstracted via a `StateStore` protocol for future Redis adapter.
28
+
29
+ ---
30
+
31
+ ## ADR-003: CQRS for Command and Query Separation
32
+
33
+ **Status**: Accepted
34
+
35
+ **Context**: Coordination operations are a mix of state-mutating commands (enqueue, acquire_lock) and queries (get_task_status, list_in_flight). Mixing these leads to complex APIs.
36
+
37
+ **Decision**: Separate command and query interfaces following CQRS pattern. `CommandBus` for mutations, `QueryBus` for reads.
38
+
39
+ **Rationale**: Clean separation simplifies testing (queries are read-only and easier to mock). Aligns with thegent hexagonal architecture standard.
40
+
41
+ **Consequences**: More initial API surface. Justified by the complexity of the coordination domain.
@@ -0,0 +1,29 @@
1
+ # CLAUDE.md - Development Guidelines for thegent-cli-share
2
+
3
+ ## Project Overview
4
+
5
+ CLI share system for thegent
6
+
7
+ ## Key Files
8
+
9
+ - - Project overview
10
+ - See project-specific directories
11
+
12
+ ## Development Commands
13
+
14
+ ```bash
15
+ cargo build && cargo test
16
+ ```
17
+
18
+ ## Architecture Principles
19
+
20
+ - **SOLID** - Single Responsibility, Dependency Inversion
21
+ - **DRY** - Shared abstractions
22
+ - **PoLA** - Descriptive error types
23
+
24
+ ## Phenotype Org Rules
25
+
26
+ - UTF-8 encoding only in all text files
27
+ - Worktree discipline: canonical repo stays on `main`
28
+ - CI completeness: fix all CI failures before merging
29
+ - Never commit agent directories (`.claude/`, `.codex/`, `.cursor/`)
@@ -0,0 +1,37 @@
1
+ # Functional Requirements: thegent-cli-share
2
+
3
+ ## FR-CLI-001: Command Identity
4
+ FR-CLI-001a: The library SHALL compute a deterministic command ID by hashing: command string, working directory, and environment variables.
5
+ FR-CLI-001b: Command IDs SHALL be stable across agent processes on the same machine.
6
+
7
+ ## FR-CLI-002: Command Deduplication
8
+ FR-CLI-002a: When an agent submits a command, the library SHALL check if an identical command is already in-flight.
9
+ FR-CLI-002b: If a duplicate in-flight command exists, the submitting agent SHALL wait for the in-flight result instead of executing again.
10
+ FR-CLI-002c: Idempotent commands SHALL have their results cached for a configurable TTL (default: 60 seconds).
11
+ FR-CLI-002d: Non-idempotent commands (marked with `idempotent=False`) SHALL never be deduplicated.
12
+
13
+ ## FR-CLI-003: Task Queue
14
+ FR-CLI-003a: The library SHALL provide `enqueue(task, priority)` and `dequeue() -> Task` operations.
15
+ FR-CLI-003b: Tasks with higher priority SHALL be dequeued before lower-priority tasks.
16
+ FR-CLI-003c: An idle agent SHALL be able to steal tasks from other agents' assigned queues.
17
+ FR-CLI-003d: A failed task SHALL be moved to the dead letter queue after `max_retries` attempts.
18
+
19
+ ## FR-CLI-004: Task Dependencies
20
+ FR-CLI-004a: Tasks SHALL support a `depends_on: list[TaskId]` field.
21
+ FR-CLI-004b: A task SHALL not be dequeued until all its dependencies have completed successfully.
22
+ FR-CLI-004c: Circular dependencies SHALL be detected at enqueue time and rejected with an error.
23
+
24
+ ## FR-CLI-005: Distributed Lock
25
+ FR-CLI-005a: `acquire_lock(resource_id, timeout)` SHALL block until the lock is acquired or timeout expires.
26
+ FR-CLI-005b: Locks SHALL have a mandatory TTL to prevent deadlock from crashed lock holders.
27
+ FR-CLI-005c: Lock acquisition and release SHALL be logged to the audit log.
28
+
29
+ ## FR-CLI-006: Smart Merge
30
+ FR-CLI-006a: When two agents attempt to write the same file, the library SHALL detect the conflict.
31
+ FR-CLI-006b: The library SHALL attempt a three-way merge using the common ancestor as base.
32
+ FR-CLI-006c: If three-way merge fails (conflict), the operation SHALL be flagged for operator review.
33
+
34
+ ## FR-CLI-007: Audit Log
35
+ FR-CLI-007a: Every coordination operation SHALL be logged with: timestamp, agent_id, operation_type, resource_id, outcome.
36
+ FR-CLI-007b: The audit log SHALL be queryable by agent_id and time range.
37
+ FR-CLI-007c: The audit log SHALL be durable (written to disk, not only in-memory).
relaypy-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: relaypy
3
+ Version: 0.1.0
4
+ Summary: CLI share system - command deduplication, task queue, smart merge
5
+ Author-email: Koosha Pari <kooshapari@kooshapari.com>
6
+ License-Expression: MIT
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
+ Requires-Python: >=3.10
15
+ Requires-Dist: pydantic>=2.0.0
16
+ Requires-Dist: rich>=13.0.0
17
+ Requires-Dist: watchfiles>=0.21.0
18
+ Provides-Extra: dev
19
+ Requires-Dist: bandit>=1.7.0; extra == 'dev'
20
+ Requires-Dist: mypy>=1.5.0; extra == 'dev'
21
+ Requires-Dist: pre-commit>=3.0.0; extra == 'dev'
22
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
23
+ Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
24
+ Requires-Dist: pytest>=7.4.0; extra == 'dev'
25
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # thegent-cli-share
29
+
30
+ CLI share system for multi-agent orchestration - command deduplication, task queue, smart merge, and coordination.
31
+
32
+ ## Architecture
33
+
34
+ This crate follows **Hexagonal Architecture** (Ports & Adapters) with **Clean Architecture** layers.
35
+
36
+ ## xDD Methodologies Applied
37
+
38
+ - **TDD**: Tests written first
39
+ - **DDD**: Bounded contexts for command cache, task queue, smart merge
40
+ - **SOLID**: Single responsibility per module
41
+ - **CQRS**: Separate command and query interfaces
42
+ - **EDA**: Domain events for state changes
43
+
44
+ ## Domain Services
45
+
46
+ ### Command Deduplication
47
+ Prevents duplicate command execution across agents using SHM-based locks.
48
+
49
+ ### Task Queue
50
+ Maildir-style queue for distributed task processing.
51
+
52
+ ### Smart Merge
53
+ Mergiraf-style AST-aware merging for conflict resolution.
54
+
55
+ ### Coordination
56
+ HLC-based distributed coordination.
57
+
58
+ ## Installation
59
+
60
+ ```bash
61
+ pip install thegent-cli-share
62
+ ```
63
+
64
+ ## CLI Usage
65
+
66
+ ```bash
67
+ # Command deduplication
68
+ thegent-cli-share lock-acquire <cmd_hash>
69
+ thegent-cli-share lock-release <cmd_hash> --pid <pid>
70
+ thegent-cli-share lock-list
71
+
72
+ # Task queue
73
+ thegent-cli-share queue-enqueue <command> --priority high
74
+ thegent-cli-share queue-list
75
+ ```
76
+
77
+ ## Python API
78
+
79
+ ```python
80
+ from thegent_cli_share.adapters.dedup import InMemoryLockAdapter
81
+
82
+ adapter = InMemoryLockAdapter()
83
+ lock = adapter.acquire("cmd_hash", pid=1234)
84
+ ```
85
+
86
+ ## License
87
+
88
+ MIT
relaypy-0.1.0/PRD.md ADDED
@@ -0,0 +1,44 @@
1
+ # PRD: thegent-cli-share — CLI Coordination for Multi-Agent Orchestration
2
+
3
+ ## Overview
4
+ `thegent-cli-share` is a Python library providing multi-agent CLI coordination primitives: command deduplication, task queue management, smart merge, and agent synchronization for the thegent system.
5
+
6
+ ## Problem Statement
7
+ When multiple thegent agents run concurrently, they may issue duplicate CLI commands, create conflicting file edits, or deadlock on shared resources. `thegent-cli-share` prevents these problems by providing a shared coordination layer.
8
+
9
+ ## Goals
10
+ 1. Prevent duplicate command execution across concurrent agents
11
+ 2. Provide a shared task queue for work distribution
12
+ 3. Smart merge: detect and resolve conflicting file edits
13
+ 4. Agent-to-agent synchronization primitives (locks, semaphores, barriers)
14
+ 5. Audit log for all coordinated operations
15
+
16
+ ## Epics
17
+
18
+ ### E1: Command Deduplication
19
+ - E1.1: Command identity hashing (deterministic ID for equivalent commands)
20
+ - E1.2: In-flight command registry (prevent duplicate execution)
21
+ - E1.3: Result caching (share results of idempotent commands)
22
+ - E1.4: TTL-based cache invalidation
23
+
24
+ ### E2: Task Queue
25
+ - E2.1: Priority queue with agent-assigned work items
26
+ - E2.2: Work stealing for idle agents
27
+ - E2.3: Task dependency graph (DAG) with execution ordering
28
+ - E2.4: Dead letter queue for failed tasks
29
+
30
+ ### E3: Smart Merge
31
+ - E3.1: Detect concurrent edits to the same file
32
+ - E3.2: Three-way merge for text files
33
+ - E3.3: Conflict escalation to human/operator when auto-merge fails
34
+ - E3.4: Merge audit log
35
+
36
+ ### E4: Synchronization Primitives
37
+ - E4.1: Distributed lock (mutex for critical sections)
38
+ - E4.2: Semaphore (rate limiting concurrent operations)
39
+ - E4.3: Barrier (coordinate multiple agents to a sync point)
40
+
41
+ ### E5: Audit Log
42
+ - E5.1: Structured event log for all coordination operations
43
+ - E5.2: Agent identity tracking per operation
44
+ - E5.3: Queryable log for debugging coordination issues
@@ -0,0 +1,61 @@
1
+ # thegent-cli-share
2
+
3
+ CLI share system for multi-agent orchestration - command deduplication, task queue, smart merge, and coordination.
4
+
5
+ ## Architecture
6
+
7
+ This crate follows **Hexagonal Architecture** (Ports & Adapters) with **Clean Architecture** layers.
8
+
9
+ ## xDD Methodologies Applied
10
+
11
+ - **TDD**: Tests written first
12
+ - **DDD**: Bounded contexts for command cache, task queue, smart merge
13
+ - **SOLID**: Single responsibility per module
14
+ - **CQRS**: Separate command and query interfaces
15
+ - **EDA**: Domain events for state changes
16
+
17
+ ## Domain Services
18
+
19
+ ### Command Deduplication
20
+ Prevents duplicate command execution across agents using SHM-based locks.
21
+
22
+ ### Task Queue
23
+ Maildir-style queue for distributed task processing.
24
+
25
+ ### Smart Merge
26
+ Mergiraf-style AST-aware merging for conflict resolution.
27
+
28
+ ### Coordination
29
+ HLC-based distributed coordination.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install thegent-cli-share
35
+ ```
36
+
37
+ ## CLI Usage
38
+
39
+ ```bash
40
+ # Command deduplication
41
+ thegent-cli-share lock-acquire <cmd_hash>
42
+ thegent-cli-share lock-release <cmd_hash> --pid <pid>
43
+ thegent-cli-share lock-list
44
+
45
+ # Task queue
46
+ thegent-cli-share queue-enqueue <command> --priority high
47
+ thegent-cli-share queue-list
48
+ ```
49
+
50
+ ## Python API
51
+
52
+ ```python
53
+ from thegent_cli_share.adapters.dedup import InMemoryLockAdapter
54
+
55
+ adapter = InMemoryLockAdapter()
56
+ lock = adapter.acquire("cmd_hash", pid=1234)
57
+ ```
58
+
59
+ ## License
60
+
61
+ MIT
@@ -0,0 +1,58 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "relaypy"
7
+ version = "0.1.0"
8
+ description = "CLI share system - command deduplication, task queue, smart merge"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "Koosha Pari", email = "kooshapari@kooshapari.com" }
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Topic :: Software Development :: Libraries :: Python Modules",
23
+ ]
24
+
25
+ dependencies = [
26
+ "watchfiles>=0.21.0",
27
+ "pydantic>=2.0.0",
28
+ "rich>=13.0.0",
29
+ ]
30
+
31
+ [project.optional-dependencies]
32
+ dev = [
33
+ "pytest>=7.4.0",
34
+ "pytest-cov>=4.1.0",
35
+ "pytest-asyncio>=0.21.0",
36
+ "ruff>=0.1.0",
37
+ "mypy>=1.5.0",
38
+ "bandit>=1.7.0",
39
+ "pre-commit>=3.0.0",
40
+ ]
41
+
42
+ [project.scripts]
43
+ thegent-share = "thegent_cli_share.cli:main"
44
+
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["src/thegent_cli_share"]
47
+
48
+ [tool.pytest.ini_options]
49
+ testpaths = ["tests"]
50
+ asyncio_mode = "auto"
51
+
52
+ [tool.ruff]
53
+ line-length = 100
54
+ target-version = "py310"
55
+
56
+ [tool.mypy]
57
+ python_version = "3.10"
58
+ strict = true
@@ -0,0 +1,87 @@
1
+ """thegent-cli-share - CLI share system with hexagonal architecture.
2
+
3
+ CLI Share System Architecture:
4
+ - Command deduplication (cmd_share)
5
+ - Task queue (Maildir-style)
6
+ - Smart merge (Mergiraf)
7
+ - Request coalescing (Singleflight)
8
+ - Coordination (HLC, OCC, leases)
9
+
10
+ xDD Methodologies Applied:
11
+ - TDD: Tests with pytest
12
+ - BDD: Behavior-driven with pytest-bdd
13
+ - DDD: Domain-driven design with bounded contexts
14
+ - SOLID: Single responsibility per module
15
+ - CQRS: Separate commands and queries
16
+ - EDA: Event-driven architecture
17
+ - Hexagonal: Ports and adapters pattern
18
+ """
19
+
20
+ __version__ = "0.1.0"
21
+
22
+ from .domain.entities import (
23
+ CommandLock,
24
+ TaskQueueItem,
25
+ MergeCandidate,
26
+ CoordinationState,
27
+ )
28
+ from .domain.value_objects import (
29
+ CommandHash,
30
+ LockStatus,
31
+ QueuePriority,
32
+ MergeStrategy,
33
+ )
34
+ from .domain.events import (
35
+ CliShareEvent,
36
+ TaskEvent,
37
+ MergeEvent,
38
+ CoordinationEvent,
39
+ )
40
+ from .application.commands import (
41
+ AcquireLockCommand,
42
+ ReleaseLockCommand,
43
+ EnqueueTaskCommand,
44
+ MergeCommand,
45
+ )
46
+ from .application.queries import (
47
+ GetLockQuery,
48
+ ListLocksQuery,
49
+ GetQueueDepthQuery,
50
+ GetMergeCandidatesQuery,
51
+ )
52
+ from .ports.driven import (
53
+ LockPort,
54
+ QueuePort,
55
+ MergePort,
56
+ CoordinationPort,
57
+ )
58
+
59
+ __all__ = [
60
+ # Domain
61
+ "CommandLock",
62
+ "TaskQueueItem",
63
+ "MergeCandidate",
64
+ "CoordinationState",
65
+ "CommandHash",
66
+ "LockStatus",
67
+ "QueuePriority",
68
+ "MergeStrategy",
69
+ "CliShareEvent",
70
+ "TaskEvent",
71
+ "MergeEvent",
72
+ "CoordinationEvent",
73
+ # Application
74
+ "AcquireLockCommand",
75
+ "ReleaseLockCommand",
76
+ "EnqueueTaskCommand",
77
+ "MergeCommand",
78
+ "GetLockQuery",
79
+ "ListLocksQuery",
80
+ "GetQueueDepthQuery",
81
+ "GetMergeCandidatesQuery",
82
+ # Ports
83
+ "LockPort",
84
+ "QueuePort",
85
+ "MergePort",
86
+ "CoordinationPort",
87
+ ]
@@ -0,0 +1,112 @@
1
+ """CLI interface for thegent-cli-share."""
2
+
3
+ import sys
4
+ from typing import Optional
5
+
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+
10
+ from thegent_cli_share import __version__
11
+ from thegent_cli_share.adapters.dedup import InMemoryLockAdapter
12
+ from thegent_cli_share.adapters.queue import InMemoryQueueAdapter
13
+
14
+ app = typer.Typer(help="CLI share system - command deduplication and task queue")
15
+ console = Console()
16
+
17
+ # Global adapters (in-memory for CLI)
18
+ _lock_adapter = InMemoryLockAdapter()
19
+ _queue_adapter = InMemoryQueueAdapter()
20
+
21
+
22
+ @app.command()
23
+ def lock_acquire(
24
+ cmd_hash: str = typer.Argument(..., help="Command hash to lock"),
25
+ pid: int = typer.Option(0, help="Process ID"),
26
+ output_path: Optional[str] = typer.Option(None, help="Output file path"),
27
+ ) -> None:
28
+ """Acquire a command lock."""
29
+ try:
30
+ lock = _lock_adapter.acquire(cmd_hash, pid, output_path)
31
+ console.print(f"[green]Lock acquired:[/green] {lock.cmd_hash} (PID: {lock.pid})")
32
+ except ValueError as e:
33
+ console.print(f"[red]Error:[/red] {e}")
34
+ raise typer.Exit(1)
35
+
36
+
37
+ @app.command()
38
+ def lock_release(
39
+ cmd_hash: str = typer.Argument(..., help="Command hash to release"),
40
+ pid: int = typer.Option(..., help="Process ID"),
41
+ ) -> None:
42
+ """Release a command lock."""
43
+ try:
44
+ _lock_adapter.release(cmd_hash, pid)
45
+ console.print(f"[green]Lock released:[/green] {cmd_hash}")
46
+ except ValueError as e:
47
+ console.print(f"[red]Error:[/red] {e}")
48
+ raise typer.Exit(1)
49
+
50
+
51
+ @app.command(name="lock-list")
52
+ def lock_list() -> None:
53
+ """List all active locks."""
54
+ locks = _lock_adapter.list_all()
55
+ if not locks:
56
+ console.print("[yellow]No active locks[/yellow]")
57
+ return
58
+
59
+ table = Table(title="Active Locks")
60
+ table.add_column("Command Hash", style="cyan")
61
+ table.add_column("PID", style="magenta")
62
+ table.add_column("Status", style="green")
63
+
64
+ for lock in locks:
65
+ table.add_row(lock.cmd_hash, str(lock.pid), lock.status.value)
66
+
67
+ console.print(table)
68
+
69
+
70
+ @app.command()
71
+ def queue_enqueue(
72
+ command: str = typer.Argument(..., help="Command to enqueue"),
73
+ priority: str = typer.Option("normal", help="Priority: low, normal, high, critical"),
74
+ ) -> None:
75
+ """Enqueue a task."""
76
+ item = _queue_adapter.enqueue(command, priority)
77
+ console.print(f"[green]Task enqueued:[/green] {item.id} ({priority})")
78
+
79
+
80
+ @app.command(name="queue-list")
81
+ def queue_list() -> None:
82
+ """List all queued tasks."""
83
+ items = _queue_adapter.list_all()
84
+ if not items:
85
+ console.print("[yellow]Queue is empty[/yellow]")
86
+ return
87
+
88
+ table = Table(title=f"Task Queue ({len(items)} items)")
89
+ table.add_column("ID", style="cyan")
90
+ table.add_column("Command", style="white")
91
+ table.add_column("Priority", style="yellow")
92
+ table.add_column("Status", style="green")
93
+
94
+ for item in items:
95
+ table.add_row(str(item.id), item.command[:50], item.priority.value, item.status)
96
+
97
+ console.print(table)
98
+
99
+
100
+ @app.command()
101
+ def version() -> None:
102
+ """Show version information."""
103
+ console.print(f"thegent-cli-share [cyan]{__version__}[/cyan]")
104
+
105
+
106
+ def main() -> None:
107
+ """Main entry point."""
108
+ app()
109
+
110
+
111
+ if __name__ == "__main__":
112
+ main()
@@ -0,0 +1 @@
1
+ """Init file for domain module."""
@@ -0,0 +1,156 @@
1
+ """Domain entities - core business objects with identity.
2
+
3
+ DDD (Domain-Driven Design) Principles:
4
+ - Entities have unique identity
5
+ - Value objects are immutable
6
+ - Domain events are immutable facts
7
+ """
8
+
9
+ from datetime import datetime
10
+ from enum import Enum
11
+ from typing import Optional
12
+ from uuid import UUID, uuid4
13
+
14
+ from pydantic import BaseModel, Field
15
+
16
+
17
+ class LockStatus(str, Enum):
18
+ """Lock status for command deduplication."""
19
+ UNLOCKED = "unlocked"
20
+ LOCKED = "locked"
21
+ COMPLETED = "completed"
22
+ TIMED_OUT = "timed_out"
23
+
24
+
25
+ class QueuePriority(str, Enum):
26
+ """Task queue priority levels."""
27
+ LOW = "low"
28
+ NORMAL = "normal"
29
+ HIGH = "high"
30
+ CRITICAL = "critical"
31
+
32
+
33
+ class MergeStrategy(str, Enum):
34
+ """Merge strategies for smart merge."""
35
+ AUTO = "auto"
36
+ THEIRS = "theirs"
37
+ OURS = "ours"
38
+ MANUAL = "manual"
39
+
40
+
41
+ class CommandLock(BaseModel):
42
+ """Command lock entity for deduplication.
43
+
44
+ Implements the cmd_share functionality - ensures only one
45
+ instance of a command runs at a time.
46
+ """
47
+ cmd_hash: str = Field(description="Unique command hash")
48
+ pid: int = Field(default=0, description="Process ID holding lock")
49
+ status: LockStatus = Field(default=LockStatus.UNLOCKED)
50
+ output_path: Optional[str] = Field(default=None)
51
+ start_time: Optional[datetime] = Field(default=None)
52
+ timeout_seconds: int = Field(default=3600)
53
+
54
+ @property
55
+ def is_locked(self) -> bool:
56
+ return self.pid != 0 and self.status == LockStatus.LOCKED
57
+
58
+ def acquire(self, pid: int, output_path: Optional[str] = None) -> None:
59
+ """Acquire the lock for a process."""
60
+ if self.is_locked and self.pid != pid:
61
+ raise ValueError(f"Lock held by PID {self.pid}")
62
+ self.pid = pid
63
+ self.status = LockStatus.LOCKED
64
+ self.output_path = output_path
65
+ self.start_time = datetime.now()
66
+
67
+ def release(self, pid: int) -> None:
68
+ """Release the lock."""
69
+ if self.pid != pid:
70
+ raise ValueError(f"Lock held by PID {self.pid}, cannot release")
71
+ self.pid = 0
72
+ self.status = LockStatus.UNLOCKED
73
+ self.start_time = None
74
+
75
+ def complete(self, pid: int) -> None:
76
+ """Mark the command as completed."""
77
+ if self.pid != pid:
78
+ raise ValueError("Cannot complete - not lock owner")
79
+ self.status = LockStatus.COMPLETED
80
+
81
+
82
+ class TaskQueueItem(BaseModel):
83
+ """Task queue item for Maildir-style queue."""
84
+ id: UUID = Field(default_factory=uuid4)
85
+ command: str = Field(description="Command to execute")
86
+ priority: QueuePriority = Field(default=QueuePriority.NORMAL)
87
+ created_at: datetime = Field(default_factory=datetime.now)
88
+ started_at: Optional[datetime] = None
89
+ completed_at: Optional[datetime] = None
90
+ status: str = Field(default="pending")
91
+ cwd: Optional[str] = None
92
+ env: dict[str, str] = Field(default_factory=dict)
93
+ result: Optional[dict] = None
94
+
95
+ @property
96
+ def is_pending(self) -> bool:
97
+ return self.status == "pending"
98
+
99
+ @property
100
+ def is_running(self) -> bool:
101
+ return self.status == "running"
102
+
103
+ @property
104
+ def is_completed(self) -> bool:
105
+ return self.status == "completed"
106
+
107
+ def start(self) -> None:
108
+ """Mark task as started."""
109
+ self.status = "running"
110
+ self.started_at = datetime.now()
111
+
112
+ def complete(self, result: dict) -> None:
113
+ """Mark task as completed."""
114
+ self.status = "completed"
115
+ self.completed_at = datetime.now()
116
+ self.result = result
117
+
118
+
119
+ class MergeCandidate(BaseModel):
120
+ """Merge candidate for smart merge."""
121
+ id: UUID = Field(default_factory=uuid4)
122
+ base_commit: str = Field(description="Base commit SHA")
123
+ theirs_commit: str = Field(description="Their branch commit")
124
+ ours_commit: str = Field(description="Our branch commit")
125
+ strategy: MergeStrategy = Field(default=MergeStrategy.AUTO)
126
+ conflict_count: int = Field(default=0)
127
+ auto_mergeable: bool = Field(default=False)
128
+ created_at: datetime = Field(default_factory=datetime.now)
129
+
130
+
131
+ class CoordinationState(BaseModel):
132
+ """Coordination state for distributed locking."""
133
+ resource_id: str = Field(description="Resource being coordinated")
134
+ owner_id: Optional[str] = Field(default=None)
135
+ lease_expires_at: Optional[datetime] = None
136
+ version: int = Field(default=0)
137
+ hlc_timestamp: int = Field(default=0)
138
+
139
+ @property
140
+ def is_owned(self) -> bool:
141
+ return self.owner_id is not None
142
+
143
+ def acquire_lease(self, owner_id: str, duration_seconds: int) -> None:
144
+ """Acquire a lease on the resource."""
145
+ from datetime import timedelta
146
+ self.owner_id = owner_id
147
+ self.lease_expires_at = datetime.now() + timedelta(seconds=duration_seconds)
148
+ self.version += 1
149
+
150
+ def release_lease(self, owner_id: str) -> None:
151
+ """Release the lease."""
152
+ if self.owner_id != owner_id:
153
+ raise ValueError("Not the lease owner")
154
+ self.owner_id = None
155
+ self.lease_expires_at = None
156
+ self.version += 1
@@ -0,0 +1,69 @@
1
+ """Domain events - immutable facts representing state changes.
2
+
3
+ Event Sourcing Principles:
4
+ - Events are immutable
5
+ - Append-only log
6
+ - Reconstruct state by replaying events
7
+ """
8
+
9
+ from datetime import datetime
10
+ from enum import Enum
11
+ from typing import Optional
12
+ from dataclasses import dataclass
13
+
14
+
15
+ class EventType(str, Enum):
16
+ """Event type enumeration."""
17
+ LOCK_ACQUIRED = "lock_acquired"
18
+ LOCK_RELEASED = "lock_released"
19
+ LOCK_COMPLETED = "lock_completed"
20
+ LOCK_TIMEOUT = "lock_timeout"
21
+ TASK_ENQUEUED = "task_enqueued"
22
+ TASK_STARTED = "task_started"
23
+ TASK_COMPLETED = "task_completed"
24
+ TASK_FAILED = "task_failed"
25
+ MERGE_STARTED = "merge_started"
26
+ MERGE_COMPLETED = "merge_completed"
27
+ MERGE_CONFLICT = "merge_conflict"
28
+ COORD_LEASE_ACQUIRED = "coord_lease_acquired"
29
+ COORD_LEASE_RELEASED = "coord_lease_released"
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class CliShareEvent:
34
+ """Base event for CLI share operations."""
35
+ event_type: EventType
36
+ timestamp: datetime
37
+ cmd_hash: str
38
+ pid: Optional[int] = None
39
+ metadata: Optional[dict] = None
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class TaskEvent:
44
+ """Event for task queue operations."""
45
+ event_type: EventType
46
+ timestamp: datetime
47
+ task_id: str
48
+ metadata: Optional[dict] = None
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class MergeEvent:
53
+ """Event for merge operations."""
54
+ event_type: EventType
55
+ timestamp: datetime
56
+ base_commit: str
57
+ branch_name: str
58
+ conflict_count: int = 0
59
+ metadata: Optional[dict] = None
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class CoordinationEvent:
64
+ """Event for coordination operations."""
65
+ event_type: EventType
66
+ timestamp: datetime
67
+ resource_id: str
68
+ owner_id: str
69
+ metadata: Optional[dict] = None
@@ -0,0 +1,81 @@
1
+ """Domain value objects - immutable objects defined by their attributes.
2
+
3
+ Value Object Principles:
4
+ - Immutable (no setters, create new instances)
5
+ - No identity (two VOs with same values are equal)
6
+ - Self-validating
7
+ """
8
+
9
+ from dataclasses import dataclass
10
+ from datetime import datetime, timedelta
11
+ from typing import Optional
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class CommandHash:
16
+ """Immutable command hash for deduplication."""
17
+ value: str
18
+ algorithm: str = "sha256"
19
+
20
+ def __str__(self) -> str:
21
+ return self.value
22
+
23
+ def __len__(self) -> int:
24
+ return len(self.value)
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class TaskMetadata:
29
+ """Immutable task metadata."""
30
+ command: str
31
+ cwd: str
32
+ env: tuple[tuple[str, str], ...]
33
+ timeout_seconds: int = 3600
34
+
35
+ def with_timeout(self, seconds: int) -> "TaskMetadata":
36
+ """Create new TaskMetadata with different timeout."""
37
+ return TaskMetadata(
38
+ command=self.command,
39
+ cwd=self.cwd,
40
+ env=self.env,
41
+ timeout_seconds=seconds,
42
+ )
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class MergeConflict:
47
+ """Immutable merge conflict information."""
48
+ file_path: str
49
+ line_start: int
50
+ line_end: int
51
+ ours_content: str
52
+ theirs_content: str
53
+
54
+ @property
55
+ def conflict_range(self) -> str:
56
+ return f"{self.line_start}-{self.line_end}"
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class HealthScore:
61
+ """Health score for system monitoring."""
62
+ overall: float
63
+ components: tuple[tuple[str, float], ...]
64
+
65
+ @classmethod
66
+ def from_components(cls, **kwargs: float) -> "HealthScore":
67
+ """Create health score from components."""
68
+ components = tuple(kwargs.items())
69
+ if not components:
70
+ return cls(overall=1.0, components=())
71
+ overall = sum(v for _, v in components) / len(components)
72
+ return cls(overall=overall, components=components)
73
+
74
+ def is_healthy(self) -> bool:
75
+ return self.overall >= 0.8
76
+
77
+ def is_degraded(self) -> bool:
78
+ return 0.5 <= self.overall < 0.8
79
+
80
+ def is_unhealthy(self) -> bool:
81
+ return self.overall < 0.5
@@ -0,0 +1,84 @@
1
+ """Driven ports - interfaces for infrastructure adapters.
2
+
3
+ Hexagonal Architecture: Ports define how the domain interacts with external systems.
4
+ """
5
+
6
+ from typing import Protocol, Optional
7
+ from ..domain.entities import CommandLock, TaskQueueItem, MergeCandidate, CoordinationState
8
+ from ..domain.value_objects import CommandHash
9
+
10
+
11
+ class LockPort(Protocol):
12
+ """Port for command lock operations."""
13
+
14
+ def acquire(self, cmd_hash: CommandHash, pid: int, output_path: Optional[str] = None) -> CommandLock:
15
+ """Acquire a command lock."""
16
+ ...
17
+
18
+ def release(self, cmd_hash: CommandHash, pid: int) -> None:
19
+ """Release a command lock."""
20
+ ...
21
+
22
+ def get(self, cmd_hash: CommandHash) -> Optional[CommandLock]:
23
+ """Get lock status."""
24
+ ...
25
+
26
+ def list_all(self) -> list[CommandLock]:
27
+ """List all locks."""
28
+ ...
29
+
30
+
31
+ class QueuePort(Protocol):
32
+ """Port for task queue operations."""
33
+
34
+ def enqueue(self, item: TaskQueueItem) -> TaskQueueItem:
35
+ """Add item to queue."""
36
+ ...
37
+
38
+ def dequeue(self) -> Optional[TaskQueueItem]:
39
+ """Remove and return next item."""
40
+ ...
41
+
42
+ def peek(self) -> Optional[TaskQueueItem]:
43
+ """View next item without removing."""
44
+ ...
45
+
46
+ def length(self) -> int:
47
+ """Get queue length."""
48
+ ...
49
+
50
+ def clear(self) -> None:
51
+ """Clear the queue."""
52
+ ...
53
+
54
+
55
+ class MergePort(Protocol):
56
+ """Port for merge operations."""
57
+
58
+ def find_conflicts(self, base: str, theirs: str, ours: str) -> list[MergeCandidate]:
59
+ """Find merge conflicts between branches."""
60
+ ...
61
+
62
+ def merge(self, base: str, theirs: str, strategy: str = "auto") -> dict:
63
+ """Perform merge with given strategy."""
64
+ ...
65
+
66
+ def apply_resolution(self, candidate_id: str, resolution: dict) -> None:
67
+ """Apply conflict resolution."""
68
+ ...
69
+
70
+
71
+ class CoordinationPort(Protocol):
72
+ """Port for distributed coordination."""
73
+
74
+ def acquire_lease(self, resource_id: str, owner_id: str, duration_seconds: int) -> CoordinationState:
75
+ """Acquire a lease on a resource."""
76
+ ...
77
+
78
+ def release_lease(self, resource_id: str, owner_id: str) -> None:
79
+ """Release a lease."""
80
+ ...
81
+
82
+ def check_lease(self, resource_id: str) -> Optional[CoordinationState]:
83
+ """Check lease status."""
84
+ ...
@@ -0,0 +1 @@
1
+ """Init file for thegent-cli-share."""
@@ -0,0 +1,103 @@
1
+ """Tests for thegent-cli-share."""
2
+
3
+ import pytest
4
+ from thegent_cli_share.adapters.dedup import InMemoryLockAdapter, CommandLock
5
+ from thegent_cli_share.adapters.queue import InMemoryQueueAdapter, QueueItem, Priority
6
+
7
+
8
+ class TestCommandLock:
9
+ """Tests for CommandLock entity."""
10
+
11
+ def test_create_lock(self) -> None:
12
+ """Test creating a new command lock."""
13
+ lock = CommandLock(cmd_hash="test_hash", pid=0)
14
+ assert lock.cmd_hash == "test_hash"
15
+ assert lock.pid == 0
16
+
17
+ def test_acquire_lock(self) -> None:
18
+ """Test acquiring a lock."""
19
+ adapter = InMemoryLockAdapter()
20
+ lock = adapter.acquire("test_hash", 1234)
21
+ assert lock.cmd_hash == "test_hash"
22
+ assert lock.pid == 1234
23
+ assert lock.is_locked()
24
+
25
+ def test_release_lock(self) -> None:
26
+ """Test releasing a lock."""
27
+ adapter = InMemoryLockAdapter()
28
+ adapter.acquire("test_hash", 1234)
29
+ adapter.release("test_hash", 1234)
30
+ assert not adapter.get("test_hash").is_locked()
31
+
32
+ def test_cannot_acquire_locked(self) -> None:
33
+ """Test that we cannot acquire a locked command."""
34
+ adapter = InMemoryLockAdapter()
35
+ adapter.acquire("test_hash", 1234)
36
+ with pytest.raises(ValueError, match="already locked"):
37
+ adapter.acquire("test_hash", 5678)
38
+
39
+
40
+ class TestTaskQueue:
41
+ """Tests for TaskQueue."""
42
+
43
+ def test_enqueue(self) -> None:
44
+ """Test enqueuing a task."""
45
+ adapter = InMemoryQueueAdapter()
46
+ item = adapter.enqueue("echo 'hello'")
47
+ assert item.command == "echo 'hello'"
48
+ assert item.status == "queued"
49
+
50
+ def test_dequeue(self) -> None:
51
+ """Test dequeuing a task."""
52
+ adapter = InMemoryQueueAdapter()
53
+ adapter.enqueue("task1")
54
+ adapter.enqueue("task2")
55
+ item = adapter.dequeue()
56
+ assert item.command == "task1"
57
+ assert item.status == "dequeued"
58
+
59
+ def test_priority_ordering(self) -> None:
60
+ """Test that high priority items are dequeued first."""
61
+ adapter = InMemoryQueueAdapter()
62
+ adapter.enqueue("low", priority=Priority.LOW)
63
+ adapter.enqueue("high", priority=Priority.HIGH)
64
+ adapter.enqueue("normal", priority=Priority.NORMAL)
65
+ item = adapter.dequeue()
66
+ assert item.command == "high"
67
+
68
+
69
+ class TestCoordination:
70
+ """Tests for CoordinationService."""
71
+
72
+ def test_edit_intent(self) -> None:
73
+ """Test EditIntent creation and checking."""
74
+ from thegent_cli_share.domain.entities import EditIntent
75
+
76
+ intent = EditIntent(
77
+ agent_id="agent1",
78
+ file_path="/path/to/file",
79
+ start_line=10,
80
+ end_line=20,
81
+ )
82
+ assert intent.file_path == "/path/to/file"
83
+ assert intent.start_line == 10
84
+ assert intent.end_line == 20
85
+ assert intent.conflicts_with(intent) # Same range conflicts
86
+
87
+ def test_edit_intent_no_conflict(self) -> None:
88
+ """Test that non-overlapping edits don't conflict."""
89
+ from thegent_cli_share.domain.entities import EditIntent
90
+
91
+ intent1 = EditIntent(
92
+ agent_id="agent1",
93
+ file_path="/path/to/file",
94
+ start_line=10,
95
+ end_line=20,
96
+ )
97
+ intent2 = EditIntent(
98
+ agent_id="agent2",
99
+ file_path="/path/to/file",
100
+ start_line=30,
101
+ end_line=40,
102
+ )
103
+ assert not intent1.conflicts_with(intent2)