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.
- relaypy-0.1.0/.github/workflows/ci.yml +44 -0
- relaypy-0.1.0/ADR.md +41 -0
- relaypy-0.1.0/CLAUDE.md +29 -0
- relaypy-0.1.0/FUNCTIONAL_REQUIREMENTS.md +37 -0
- relaypy-0.1.0/PKG-INFO +88 -0
- relaypy-0.1.0/PRD.md +44 -0
- relaypy-0.1.0/README.md +61 -0
- relaypy-0.1.0/pyproject.toml +58 -0
- relaypy-0.1.0/src/thegent_cli_share/__init__.py +87 -0
- relaypy-0.1.0/src/thegent_cli_share/cli.py +112 -0
- relaypy-0.1.0/src/thegent_cli_share/domain/__init__.py +1 -0
- relaypy-0.1.0/src/thegent_cli_share/domain/entities.py +156 -0
- relaypy-0.1.0/src/thegent_cli_share/domain/events.py +69 -0
- relaypy-0.1.0/src/thegent_cli_share/domain/value_objects.py +81 -0
- relaypy-0.1.0/src/thegent_cli_share/ports/driven.py +84 -0
- relaypy-0.1.0/tests/__init__.py +1 -0
- relaypy-0.1.0/tests/test_thegent_cli_share.py +103 -0
|
@@ -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.
|
relaypy-0.1.0/CLAUDE.md
ADDED
|
@@ -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
|
relaypy-0.1.0/README.md
ADDED
|
@@ -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)
|