generated from coulomb/repo-seed
Transform Issue Facade from a universal CLI tool into an agent coordination platform with comprehensive documentation and enhanced capabilities for autonomous coding agents. Major Changes: - Complete README rewrite focusing on agent-driven coordination - New comprehensive documentation (AGENT_INTEGRATION.md, CLAUDE.md, ROADMAP.md) - Capability integration setup with CAPABILITY.yaml and integration scripts - Enhanced Makefile with local development targets for easier workflows Bug Fixes: - Fix schema initialization using executescript() for multi-line SQL support - Disable FTS5 triggers due to compatibility issues (documented for future re-enablement) Features: - Enhanced CLI list command with full parameter passthrough - New examples directory with agent integration patterns - New comprehensive test suite (test_core_models.py, test_local_backend.py) Code Quality: - Remove @cached_property decorators for Label properties (simplification) - Clean up test organization (removed old test_gitea_integration.py) This milestone establishes Issue Facade as a production-ready coordination layer for multi-agent software development, with clear integration paths and comprehensive developer documentation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
676 lines
18 KiB
Markdown
676 lines
18 KiB
Markdown
# Agent Integration Guide
|
|
|
|
**Issue Facade for Autonomous Coding Agent Coordination**
|
|
|
|
## Purpose
|
|
|
|
The **Issue Facade** capability provides a standardized interface for autonomous coding agents to coordinate project implementation through issue tracking. Instead of agents directly interfacing with platform-specific APIs (GitHub, GitLab, Gitea), they use a unified abstraction that works consistently across backends.
|
|
|
|
### Why Issue Tracking for Agent Coordination?
|
|
|
|
Issue tracking provides a natural coordination mechanism for multi-agent software development:
|
|
|
|
- **Task Distribution**: Issues represent discrete units of work that agents can claim and execute
|
|
- **State Management**: Issue states (open, in_progress, closed) track progress across the team
|
|
- **Communication Channel**: Comments enable inter-agent communication and human oversight
|
|
- **Progress Visibility**: Labels, assignees, and milestones provide real-time project status
|
|
- **Audit Trail**: Complete history of who did what and when
|
|
- **Human Integration**: Human developers can seamlessly participate in agent-driven projects
|
|
|
|
## Current Status: Production-Ready with Manual Setup
|
|
|
|
### What Works Now (v1.0)
|
|
|
|
✅ **Complete CRUD Operations**
|
|
- Create, read, update, delete issues
|
|
- Full label management
|
|
- User and assignee handling
|
|
- Milestone operations
|
|
- Comment threads
|
|
|
|
✅ **Gitea Backend** (Production-Ready)
|
|
- Complete API integration
|
|
- Rate limiting and error handling
|
|
- State mapping (open/in_progress/blocked → open/closed)
|
|
- Sync support with local backup
|
|
|
|
✅ **Local SQLite Backend** (Fully Functional)
|
|
- Offline operation
|
|
- Complete data model
|
|
- Sync with remote backends
|
|
- Fast queries and filtering
|
|
|
|
✅ **Agent-Friendly Features**
|
|
- JSON output mode for machine parsing
|
|
- Programmatic Python API
|
|
- Comprehensive filtering
|
|
- Batch operations
|
|
- Type-safe models
|
|
|
|
### Current Limitations
|
|
|
|
⚠️ **Manual Configuration Required**
|
|
- No auto-detection from git remotes (yet)
|
|
- Backend configuration is manual one-time setup
|
|
- No environment-variable-only mode (yet)
|
|
|
|
⚠️ **Hardcoded User Context**
|
|
- CLI operations use "cli-user" placeholder
|
|
- Agent identity needs to be managed externally
|
|
|
|
⚠️ **Basic Conflict Resolution**
|
|
- Sync detects conflicts but doesn't auto-resolve
|
|
- Manual intervention required for complex merges
|
|
|
|
## Quick Start for Agents
|
|
|
|
### 1. Installation
|
|
|
|
```bash
|
|
cd capabilities/issue-facade
|
|
pip install -e .
|
|
```
|
|
|
|
### 2. Backend Configuration (One-Time Setup)
|
|
|
|
**For Gitea Projects:**
|
|
|
|
```bash
|
|
# Configure Gitea backend
|
|
export GITEA_API_TOKEN="your-token-here"
|
|
|
|
issue backend add my-project gitea
|
|
# Prompts for:
|
|
# - Gitea URL: https://gitea.example.com
|
|
# - Owner: your-org
|
|
# - Repo: your-project
|
|
# - Token: (reads from GITEA_API_TOKEN)
|
|
|
|
# Verify connection
|
|
issue backend test my-project
|
|
|
|
# Set as default
|
|
issue backend set-default my-project
|
|
```
|
|
|
|
**For Local/Offline Work:**
|
|
|
|
```bash
|
|
# Configure local SQLite backend
|
|
issue backend add local-work local
|
|
# Prompts for:
|
|
# - Database path: .issue-facade/issues.db
|
|
|
|
issue backend set-default local-work
|
|
```
|
|
|
|
### 3. Basic Agent Operations
|
|
|
|
```bash
|
|
# List open issues assigned to an agent
|
|
issue list --state=open --assignee=agent-coder --format=json
|
|
|
|
# Create a new issue
|
|
issue create "Implement user authentication" \
|
|
--label=feature --label=priority:high \
|
|
--assignee=agent-coder
|
|
|
|
# Update issue state
|
|
issue edit 42 --state=in_progress
|
|
|
|
# Add progress comment
|
|
issue comment 42 "Completed database schema migration"
|
|
|
|
# Close when done
|
|
issue close 42 --comment="Implementation complete, tests passing"
|
|
```
|
|
|
|
## Agent Workflow Patterns
|
|
|
|
### Pattern 1: Single Agent, Task Execution
|
|
|
|
**Scenario**: One agent implements features from an issue backlog.
|
|
|
|
```bash
|
|
#!/bin/bash
|
|
# Agent workflow script
|
|
|
|
# 1. Get next available task
|
|
ISSUE=$(issue list --state=open --label=ready \
|
|
--format=json --limit=1 | jq -r '.[0].number')
|
|
|
|
# 2. Claim the issue
|
|
issue edit $ISSUE --assignee=agent-coder --state=in_progress
|
|
issue comment $ISSUE "Starting implementation"
|
|
|
|
# 3. Execute work
|
|
# ... agent implements the feature ...
|
|
|
|
# 4. Report completion
|
|
issue comment $ISSUE "Implementation complete. Files changed: src/auth.py, tests/test_auth.py"
|
|
issue close $ISSUE --comment="Ready for review"
|
|
```
|
|
|
|
### Pattern 2: Multi-Agent Coordination
|
|
|
|
**Scenario**: Multiple specialized agents work on different aspects.
|
|
|
|
```bash
|
|
# Agent 1 (Coder) - Claims and implements
|
|
issue list --label=needs-implementation --state=open --format=json | \
|
|
jq -r '.[0].number' | \
|
|
xargs -I {} issue edit {} --assignee=agent-coder --state=in_progress
|
|
|
|
# Agent 2 (Reviewer) - Reviews completed work
|
|
issue list --label=needs-review --state=closed --format=json | \
|
|
jq -r '.[0].number' | \
|
|
xargs -I {} sh -c 'issue comment {} "Code review complete. Approved."'
|
|
|
|
# Agent 3 (Tester) - Runs tests on reviewed code
|
|
issue list --label=reviewed --state=closed --format=json | \
|
|
jq -r '.[0].number' | \
|
|
xargs -I {} sh -c 'issue comment {} "All tests passing. Deploying to staging."'
|
|
```
|
|
|
|
### Pattern 3: Agent-Human Collaboration
|
|
|
|
**Scenario**: Agents implement, humans review and approve.
|
|
|
|
```python
|
|
# Agent creates implementation issues from requirements
|
|
from issue_tracker.backends.gitea import GiteaBackend
|
|
from issue_tracker.core.models import Issue, Label, IssueState
|
|
from datetime import datetime, timezone
|
|
|
|
backend = GiteaBackend()
|
|
backend.connect({
|
|
'base_url': 'https://gitea.example.com',
|
|
'token': os.environ['GITEA_API_TOKEN'],
|
|
'owner': 'myorg',
|
|
'repo': 'myproject'
|
|
})
|
|
|
|
# Agent breaks down feature into tasks
|
|
feature_issue = backend.get_issue_by_number(100)
|
|
subtasks = [
|
|
"Implement database schema",
|
|
"Create API endpoints",
|
|
"Add frontend components",
|
|
"Write integration tests"
|
|
]
|
|
|
|
for task in subtasks:
|
|
issue = Issue(
|
|
id=None, number=0,
|
|
title=f"{feature_issue.title}: {task}",
|
|
description=f"Subtask of #{feature_issue.number}\n\n{task}",
|
|
state=IssueState.OPEN,
|
|
created_at=datetime.now(timezone.utc),
|
|
updated_at=datetime.now(timezone.utc),
|
|
labels=[
|
|
Label(name="agent-generated"),
|
|
Label(name="needs-implementation"),
|
|
Label(name="parent:100")
|
|
]
|
|
)
|
|
backend.create_issue(issue)
|
|
|
|
# Human reviews and approves/rejects via comments
|
|
# Agent monitors for approval comments and proceeds
|
|
```
|
|
|
|
## Programmatic API for Agents
|
|
|
|
### Python Integration
|
|
|
|
```python
|
|
from issue_tracker.backends.gitea import GiteaBackend
|
|
from issue_tracker.core.models import Issue, Label, IssueState, User
|
|
from issue_tracker.core.interfaces import IssueFilter
|
|
from datetime import datetime, timezone
|
|
import os
|
|
|
|
# Initialize backend
|
|
backend = GiteaBackend()
|
|
backend.connect({
|
|
'base_url': os.environ['GITEA_URL'],
|
|
'token': os.environ['GITEA_API_TOKEN'],
|
|
'owner': os.environ['GITEA_OWNER'],
|
|
'repo': os.environ['GITEA_REPO']
|
|
})
|
|
|
|
# Query issues
|
|
filter_criteria = IssueFilter(
|
|
state='open',
|
|
labels=['bug', 'priority:high'],
|
|
assignee='agent-coder',
|
|
limit=10
|
|
)
|
|
issues = backend.list_issues(filter_criteria)
|
|
|
|
# Create issue
|
|
new_issue = Issue(
|
|
id=None,
|
|
number=0,
|
|
title="Fix memory leak in parser",
|
|
description="Detected memory leak in parse_document() function",
|
|
state=IssueState.OPEN,
|
|
created_at=datetime.now(timezone.utc),
|
|
updated_at=datetime.now(timezone.utc),
|
|
labels=[
|
|
Label(name="bug"),
|
|
Label(name="priority:critical"),
|
|
Label(name="agent-detected")
|
|
],
|
|
assignees=[User(id="agent-coder", username="agent-coder")]
|
|
)
|
|
created = backend.create_issue(new_issue)
|
|
|
|
# Update issue
|
|
created.state = IssueState.IN_PROGRESS
|
|
backend.update_issue(created)
|
|
|
|
# Add comment
|
|
from issue_tracker.core.models import Comment
|
|
comment = Comment(
|
|
id=None,
|
|
body="Analysis complete. Root cause: unclosed file handles in line 234",
|
|
author=User(id="agent-coder", username="agent-coder"),
|
|
created_at=datetime.now(timezone.utc)
|
|
)
|
|
backend.add_comment(created.id, comment)
|
|
|
|
# Close issue
|
|
created.state = IssueState.CLOSED
|
|
created.closed_at = datetime.now(timezone.utc)
|
|
backend.update_issue(created)
|
|
```
|
|
|
|
### Advanced Filtering
|
|
|
|
```python
|
|
# Get all high-priority bugs not assigned
|
|
critical_bugs = backend.list_issues(IssueFilter(
|
|
state='open',
|
|
labels=['bug', 'priority:critical']
|
|
))
|
|
unassigned = [i for i in critical_bugs if not i.assignees]
|
|
|
|
# Get stale issues (not updated in 7 days)
|
|
from datetime import timedelta
|
|
stale_threshold = datetime.now(timezone.utc) - timedelta(days=7)
|
|
stale_issues = backend.list_issues(IssueFilter(
|
|
state='open',
|
|
updated_before=stale_threshold
|
|
))
|
|
|
|
# Search by text
|
|
search_results = backend.search_issues("authentication", limit=20)
|
|
```
|
|
|
|
## Agent Coordination Strategies
|
|
|
|
### Strategy 1: Label-Based Role Assignment
|
|
|
|
Use labels to indicate agent specialization:
|
|
|
|
```python
|
|
# Agent types
|
|
AGENT_ROLES = {
|
|
'agent:coder': ['feature', 'bug', 'refactor'],
|
|
'agent:tester': ['needs-testing', 'test-failure'],
|
|
'agent:reviewer': ['needs-review', 'code-quality'],
|
|
'agent:documenter': ['documentation', 'api-docs']
|
|
}
|
|
|
|
# Each agent filters by their role
|
|
def get_agent_tasks(agent_type):
|
|
role_labels = AGENT_ROLES[agent_type]
|
|
all_tasks = []
|
|
for label in role_labels:
|
|
tasks = backend.list_issues(IssueFilter(
|
|
state='open',
|
|
labels=[label, agent_type]
|
|
))
|
|
all_tasks.extend(tasks)
|
|
return all_tasks
|
|
```
|
|
|
|
### Strategy 2: State Machine Workflow
|
|
|
|
Use issue states to track progress through pipeline:
|
|
|
|
```
|
|
open → in_progress → needs_review → closed
|
|
↓ ↓ ↓
|
|
blocked blocked blocked
|
|
```
|
|
|
|
```python
|
|
def advance_issue_state(issue_number):
|
|
issue = backend.get_issue_by_number(issue_number)
|
|
|
|
state_transitions = {
|
|
IssueState.OPEN: IssueState.IN_PROGRESS,
|
|
IssueState.IN_PROGRESS: IssueState.CLOSED # or needs_review
|
|
}
|
|
|
|
if issue.state in state_transitions:
|
|
issue.state = state_transitions[issue.state]
|
|
backend.update_issue(issue)
|
|
return True
|
|
return False
|
|
```
|
|
|
|
### Strategy 3: Comment-Based Communication
|
|
|
|
Use structured comments for agent-to-agent messages:
|
|
|
|
```python
|
|
import json
|
|
|
|
def post_agent_message(issue_id, message_type, data):
|
|
"""Post structured message for other agents"""
|
|
message = {
|
|
'type': message_type,
|
|
'agent': 'agent-coder',
|
|
'timestamp': datetime.now(timezone.utc).isoformat(),
|
|
'data': data
|
|
}
|
|
comment = Comment(
|
|
id=None,
|
|
body=f"```agent-message\n{json.dumps(message, indent=2)}\n```",
|
|
author=User(id="agent-coder", username="agent-coder"),
|
|
created_at=datetime.now(timezone.utc)
|
|
)
|
|
backend.add_comment(issue_id, comment)
|
|
|
|
def read_agent_messages(issue_id, message_type=None):
|
|
"""Read structured messages from other agents"""
|
|
comments = backend.get_comments(issue_id)
|
|
messages = []
|
|
for comment in comments:
|
|
if '```agent-message' in comment.body:
|
|
try:
|
|
json_str = comment.body.split('```agent-message\n')[1].split('\n```')[0]
|
|
msg = json.loads(json_str)
|
|
if message_type is None or msg['type'] == message_type:
|
|
messages.append(msg)
|
|
except (IndexError, json.JSONDecodeError):
|
|
continue
|
|
return messages
|
|
|
|
# Usage:
|
|
post_agent_message(42, 'implementation_complete', {
|
|
'files_changed': ['src/auth.py', 'tests/test_auth.py'],
|
|
'tests_passing': True,
|
|
'coverage': 95.2
|
|
})
|
|
|
|
# Later, reviewer agent reads:
|
|
results = read_agent_messages(42, 'implementation_complete')
|
|
```
|
|
|
|
## Synchronization and Backup
|
|
|
|
### Sync Local and Remote
|
|
|
|
```bash
|
|
# Pull all issues to local backup
|
|
issue backend add backup local
|
|
issue sync pull gitea-remote backup
|
|
|
|
# Work offline with local backend
|
|
issue backend set-default backup
|
|
issue create "Offline work item" --label=offline
|
|
|
|
# Sync back when online
|
|
issue sync push backup gitea-remote
|
|
```
|
|
|
|
### Conflict Handling
|
|
|
|
```python
|
|
# Check for conflicts before sync
|
|
from issue_tracker.cli.sync_commands import sync_pull
|
|
|
|
try:
|
|
sync_pull(source='remote', target='local', dry_run=True)
|
|
except ConflictError as e:
|
|
# Conflicts detected
|
|
for conflict in e.conflicts:
|
|
print(f"Conflict on issue {conflict['issue_number']}")
|
|
print(f" Local updated: {conflict['local_updated']}")
|
|
print(f" Remote updated: {conflict['remote_updated']}")
|
|
|
|
# Resolve by choosing newer timestamp
|
|
sync_pull(source='remote', target='local', force=True)
|
|
```
|
|
|
|
## Current Workarounds for Limitations
|
|
|
|
### Workaround 1: Agent Identity
|
|
|
|
Since "cli-user" is hardcoded, use labels or comments to indicate agent:
|
|
|
|
```python
|
|
# Add agent identifier to all operations
|
|
agent_id = "agent-coder-v1"
|
|
|
|
# In issue creation
|
|
labels.append(Label(name=f"created-by:{agent_id}"))
|
|
|
|
# In comments
|
|
comment.body = f"[{agent_id}] {actual_message}"
|
|
```
|
|
|
|
### Workaround 2: Issue Claiming
|
|
|
|
No built-in locking, so use assignee + comment:
|
|
|
|
```python
|
|
def claim_issue(issue_number, agent_id, timeout_minutes=30):
|
|
issue = backend.get_issue_by_number(issue_number)
|
|
|
|
# Check if already claimed
|
|
if issue.assignees:
|
|
# Check claim age from comments
|
|
comments = backend.get_comments(issue.id)
|
|
claim_comments = [c for c in comments if 'CLAIMED' in c.body]
|
|
if claim_comments:
|
|
last_claim = claim_comments[-1].created_at
|
|
age = datetime.now(timezone.utc) - last_claim
|
|
if age.total_seconds() < timeout_minutes * 60:
|
|
return False # Still claimed
|
|
|
|
# Claim it
|
|
issue.assignees = [User(id=agent_id, username=agent_id)]
|
|
issue.state = IssueState.IN_PROGRESS
|
|
backend.update_issue(issue)
|
|
|
|
backend.add_comment(issue.id, Comment(
|
|
id=None,
|
|
body=f"CLAIMED by {agent_id} at {datetime.now(timezone.utc).isoformat()}",
|
|
author=User(id=agent_id, username=agent_id),
|
|
created_at=datetime.now(timezone.utc)
|
|
))
|
|
return True
|
|
```
|
|
|
|
### Workaround 3: Manual Repository Configuration
|
|
|
|
Create a setup script for each project:
|
|
|
|
```bash
|
|
#!/bin/bash
|
|
# setup-issue-tracking.sh
|
|
|
|
cat > .issue-facade-config << EOF
|
|
GITEA_URL=https://gitea.example.com
|
|
GITEA_OWNER=myorg
|
|
GITEA_REPO=myproject
|
|
GITEA_TOKEN_FILE=~/.secrets/gitea-token
|
|
EOF
|
|
|
|
# Load config and configure backend
|
|
source .issue-facade-config
|
|
export GITEA_API_TOKEN=$(cat $GITEA_TOKEN_FILE)
|
|
|
|
issue backend add $(basename $(pwd)) gitea <<INPUT
|
|
$GITEA_URL
|
|
$GITEA_OWNER
|
|
$GITEA_REPO
|
|
INPUT
|
|
|
|
issue backend set-default $(basename $(pwd))
|
|
```
|
|
|
|
## Performance Considerations
|
|
|
|
### Efficient Querying
|
|
|
|
```python
|
|
# BAD: Get all issues then filter in Python
|
|
all_issues = backend.list_issues()
|
|
my_issues = [i for i in all_issues if i.assignees and i.assignees[0].username == 'agent-coder']
|
|
|
|
# GOOD: Use backend filtering
|
|
my_issues = backend.list_issues(IssueFilter(
|
|
assignee='agent-coder',
|
|
state='open'
|
|
))
|
|
```
|
|
|
|
### Batch Operations
|
|
|
|
```python
|
|
# BAD: Update issues one by one
|
|
for issue_number in [1, 2, 3, 4, 5]:
|
|
issue = backend.get_issue_by_number(issue_number)
|
|
issue.labels.append(Label(name="batch-processed"))
|
|
backend.update_issue(issue)
|
|
|
|
# GOOD: Use local backend for bulk operations
|
|
from issue_tracker.backends.local import LocalSQLiteBackend
|
|
|
|
local = LocalSQLiteBackend()
|
|
local.connect({'db_path': '/tmp/batch.db'})
|
|
|
|
# Pull from remote
|
|
for issue_number in [1, 2, 3, 4, 5]:
|
|
issue = backend.get_issue_by_number(issue_number)
|
|
local.create_issue(issue)
|
|
|
|
# Bulk update locally
|
|
issues = local.list_issues()
|
|
for issue in issues:
|
|
issue.labels.append(Label(name="batch-processed"))
|
|
local.update_issue(issue)
|
|
|
|
# Push back to remote
|
|
for issue in local.list_issues():
|
|
backend.update_issue(issue)
|
|
```
|
|
|
|
### Caching
|
|
|
|
```python
|
|
# Cache issue list for short-lived operations
|
|
import functools
|
|
import time
|
|
|
|
@functools.lru_cache(maxsize=1)
|
|
def get_open_issues_cached():
|
|
return backend.list_issues(IssueFilter(state='open'))
|
|
|
|
# Invalidate cache after 60 seconds
|
|
last_fetch = time.time()
|
|
if time.time() - last_fetch > 60:
|
|
get_open_issues_cached.cache_clear()
|
|
last_fetch = time.time()
|
|
```
|
|
|
|
## Roadmap: Future Enhancements
|
|
|
|
### Phase 1: Auto-Configuration (v1.1)
|
|
- Automatic git remote detection
|
|
- Environment-variable-only setup
|
|
- Per-repository `.issue-facade/config.json` support
|
|
- `issue config detect` command
|
|
|
|
### Phase 2: Agent Features (v1.2)
|
|
- Agent identity management
|
|
- Issue claiming/locking API
|
|
- Structured metadata fields for agent state
|
|
- Webhook support for reactive agents
|
|
|
|
### Phase 3: Advanced Coordination (v2.0)
|
|
- Issue dependency tracking
|
|
- Query DSL: `is:open assignee:me label:bug,critical`
|
|
- Activity streams and event logs
|
|
- Multi-agent conflict resolution strategies
|
|
- Distributed locking for concurrent operations
|
|
|
|
## Examples Repository
|
|
|
|
See `examples/agents/` for complete working examples:
|
|
|
|
- `simple_task_executor.py` - Single agent claiming and executing tasks
|
|
- `multi_agent_pipeline.py` - Multiple agents in CI/CD-like workflow
|
|
- `human_in_loop.py` - Agents with human approval gates
|
|
- `monitoring_agent.py` - Agent that monitors issue health and sends alerts
|
|
|
|
## Troubleshooting
|
|
|
|
### "Backend not configured"
|
|
|
|
```bash
|
|
# List configured backends
|
|
issue backend list
|
|
|
|
# If empty, configure one
|
|
issue backend add myproject gitea
|
|
```
|
|
|
|
### "Authentication failed"
|
|
|
|
```bash
|
|
# Check token is valid
|
|
issue backend test myproject
|
|
|
|
# Reconfigure with correct token
|
|
export GITEA_API_TOKEN="new-token"
|
|
issue backend remove myproject
|
|
issue backend add myproject gitea
|
|
```
|
|
|
|
### "Issue not found"
|
|
|
|
```python
|
|
# Gitea uses backend_id, not number
|
|
issue = backend.get_issue_by_number(42) # Correct
|
|
# issue = backend.get_issue("42") # Wrong - needs backend_id
|
|
```
|
|
|
|
### "Sync conflicts"
|
|
|
|
```bash
|
|
# Force sync (overwrites target)
|
|
issue sync pull source target --force
|
|
|
|
# Or manually resolve
|
|
issue list --backend=source --format=json > source.json
|
|
issue list --backend=target --format=json > target.json
|
|
# Compare and decide which to keep
|
|
```
|
|
|
|
## Support
|
|
|
|
- **Documentation**: See `CLAUDE.md` for development guide
|
|
- **Tests**: Run `make test` to verify installation
|
|
- **Issues**: Report issues in the main markitect repository
|
|
|
|
## License
|
|
|
|
MIT License - See LICENSE file
|