generated from coulomb/repo-seed
feat: rename to issue-core and add task ingestion endpoint
Renames the package, distribution, CLI alias, Makefile targets, and working directory from issue-facade to issue-core, signalling its role as the authoritative task lifecycle manager for the Coulomb org (peer to activity-core, rules-core, project-core). Adds POST /issues/ ingestion endpoint for activity-core's IssueSink, under a new optional [api] extra. The endpoint is served by `issue serve`, authenticates via the ISSUE_CORE_API_KEY env var (Bearer or X-API-Key header), and routes the TaskSpec payload to the configured default backend with full traceability metadata embedded in sync_metadata. - T01: Python package issue_tracker -> issue_core, dir rename - T02: registered in state hub under custodian domain - T03: INTENT.md (what it is, what it isn't, how it fits) - T04: SCOPE.md (in/out-of-scope, integration boundaries) - T05: POST /issues/ via FastAPI + Uvicorn, 9 unit tests - T06: docs/nats-task-ingestion.md design stub Closes ISSC-WP-0001. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Capability Bootstrap System
|
# Capability Bootstrap System
|
||||||
|
|
||||||
**How coding agents discover and integrate the issue-facade capability.**
|
**How coding agents discover and integrate the issue-core capability.**
|
||||||
|
|
||||||
## Design Philosophy
|
## Design Philosophy
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ Comprehensive guide for coding agents:
|
|||||||
- Error handling
|
- Error handling
|
||||||
- Examples
|
- Examples
|
||||||
|
|
||||||
**Injected into:** `.claude/capabilities/issue-facade.md` in main project
|
**Injected into:** `.claude/capabilities/issue-core.md` in main project
|
||||||
|
|
||||||
### 3. Integration Automation
|
### 3. Integration Automation
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ Interactive script that:
|
|||||||
```bash
|
```bash
|
||||||
make integrate
|
make integrate
|
||||||
# or
|
# or
|
||||||
cd capabilities/issue-facade && ./.capability/integrate.sh
|
cd capabilities/issue-core && ./.capability/integrate.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Integration Checklist
|
### 4. Integration Checklist
|
||||||
@@ -81,7 +81,7 @@ Step-by-step checklist for humans integrating the capability:
|
|||||||
|
|
||||||
```
|
```
|
||||||
Main Project Setup
|
Main Project Setup
|
||||||
├── 1. Human runs: cd capabilities/issue-facade && make integrate
|
├── 1. Human runs: cd capabilities/issue-core && make integrate
|
||||||
├── 2. Script installs capability
|
├── 2. Script installs capability
|
||||||
├── 3. Script configures backend (prompts for credentials)
|
├── 3. Script configures backend (prompts for credentials)
|
||||||
├── 4. Script copies agent-context.md → .claude/capabilities/
|
├── 4. Script copies agent-context.md → .claude/capabilities/
|
||||||
@@ -101,13 +101,13 @@ Main Project Setup
|
|||||||
Agent Workflow
|
Agent Workflow
|
||||||
├── 1. Agent receives task involving issues
|
├── 1. Agent receives task involving issues
|
||||||
├── 2. Agent checks .claude/capabilities/ for relevant docs
|
├── 2. Agent checks .claude/capabilities/ for relevant docs
|
||||||
├── 3. Agent finds issue-facade.md with comprehensive guide
|
├── 3. Agent finds issue-core.md with comprehensive guide
|
||||||
├── 4. Agent uses Python API or CLI as documented
|
├── 4. Agent uses Python API or CLI as documented
|
||||||
└── 5. Agent avoids direct API calls (warned in docs)
|
└── 5. Agent avoids direct API calls (warned in docs)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Key Files Agent Reads:**
|
**Key Files Agent Reads:**
|
||||||
- `.claude/capabilities/issue-facade.md` - Complete usage guide
|
- `.claude/capabilities/issue-core.md` - Complete usage guide
|
||||||
- `.claude/context/capabilities.md` - High-level capability list
|
- `.claude/context/capabilities.md` - High-level capability list
|
||||||
- `.claude/commands/use-issues.md` - Slash command for context injection
|
- `.claude/commands/use-issues.md` - Slash command for context injection
|
||||||
|
|
||||||
@@ -116,18 +116,18 @@ Agent Workflow
|
|||||||
```
|
```
|
||||||
project-root/
|
project-root/
|
||||||
├── capabilities/
|
├── capabilities/
|
||||||
│ └── issue-facade/ # Capability code
|
│ └── issue-core/ # Capability code
|
||||||
│ ├── CAPABILITY-issue-tracking.yaml # Machine-readable metadata
|
│ ├── CAPABILITY-issue-tracking.yaml # Machine-readable metadata
|
||||||
│ ├── .capability/
|
│ ├── .capability/
|
||||||
│ │ ├── agent-context.md # Agent guide (source)
|
│ │ ├── agent-context.md # Agent guide (source)
|
||||||
│ │ ├── integrate.sh # Integration script
|
│ │ ├── integrate.sh # Integration script
|
||||||
│ │ └── README.md # This file
|
│ │ └── README.md # This file
|
||||||
│ ├── issue_tracker/ # Python package
|
│ ├── issue_core/ # Python package
|
||||||
│ └── ...
|
│ └── ...
|
||||||
│
|
│
|
||||||
├── .claude/ # Claude Code configuration
|
├── .claude/ # Claude Code configuration
|
||||||
│ ├── capabilities/ # Capability docs for agents
|
│ ├── capabilities/ # Capability docs for agents
|
||||||
│ │ └── issue-facade.md # Agent guide (copy)
|
│ │ └── issue-core.md # Agent guide (copy)
|
||||||
│ │
|
│ │
|
||||||
│ ├── commands/ # Slash commands
|
│ ├── commands/ # Slash commands
|
||||||
│ │ └── use-issues.md # /use-issues command
|
│ │ └── use-issues.md # /use-issues command
|
||||||
@@ -135,7 +135,7 @@ project-root/
|
|||||||
│ └── context/ # Always-available context
|
│ └── context/ # Always-available context
|
||||||
│ └── capabilities.md # List of all capabilities
|
│ └── capabilities.md # List of all capabilities
|
||||||
│
|
│
|
||||||
└── .issue-facade/ # Capability config (gitignored)
|
└── .issue-core/ # Capability config (gitignored)
|
||||||
├── config.json # Backend configuration
|
├── config.json # Backend configuration
|
||||||
└── issues.db # Local cache/backup
|
└── issues.db # Local cache/backup
|
||||||
```
|
```
|
||||||
@@ -150,13 +150,13 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
def has_issue_capability(project_root: Path) -> bool:
|
def has_issue_capability(project_root: Path) -> bool:
|
||||||
"""Check if issue-facade capability is available."""
|
"""Check if issue-core capability is available."""
|
||||||
capability_guide = project_root / ".claude/capabilities/issue-facade.md"
|
capability_guide = project_root / ".claude/capabilities/issue-core.md"
|
||||||
return capability_guide.exists()
|
return capability_guide.exists()
|
||||||
|
|
||||||
if has_issue_capability(Path.cwd()):
|
if has_issue_capability(Path.cwd()):
|
||||||
# Use capability
|
# Use capability
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
backend = GiteaBackend()
|
backend = GiteaBackend()
|
||||||
else:
|
else:
|
||||||
# Fall back or prompt human
|
# Fall back or prompt human
|
||||||
@@ -173,15 +173,15 @@ def get_capability_docs(capability_name: str) -> str:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# Agent can read and understand the guide
|
# Agent can read and understand the guide
|
||||||
docs = get_capability_docs("issue-facade")
|
docs = get_capability_docs("issue-core")
|
||||||
# Parse docs for API usage patterns...
|
# Parse docs for API usage patterns...
|
||||||
```
|
```
|
||||||
|
|
||||||
**3. Use the API as documented:**
|
**3. Use the API as documented:**
|
||||||
```python
|
```python
|
||||||
# Example from agent-context.md
|
# Example from agent-context.md
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
from issue_tracker.core.interfaces import IssueFilter
|
from issue_core.core.interfaces import IssueFilter
|
||||||
|
|
||||||
backend = GiteaBackend()
|
backend = GiteaBackend()
|
||||||
backend.connect(config)
|
backend.connect(config)
|
||||||
@@ -204,8 +204,8 @@ response = requests.post(
|
|||||||
**Good (Uses capability):**
|
**Good (Uses capability):**
|
||||||
```python
|
```python
|
||||||
# ✅ Uses capability
|
# ✅ Uses capability
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
from issue_tracker.core.models import Issue, IssueState
|
from issue_core.core.models import Issue, IssueState
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
backend = GiteaBackend()
|
backend = GiteaBackend()
|
||||||
@@ -302,7 +302,7 @@ make integrate
|
|||||||
make discover-capabilities
|
make discover-capabilities
|
||||||
# Output:
|
# Output:
|
||||||
# Found capabilities:
|
# Found capabilities:
|
||||||
# - issue-facade (v1.0.0) - Issue tracking coordination
|
# - issue-core (v1.0.0) - Issue tracking coordination
|
||||||
# - ... (other capabilities)
|
# - ... (other capabilities)
|
||||||
|
|
||||||
# Auto-integrate all
|
# Auto-integrate all
|
||||||
@@ -318,7 +318,7 @@ Capabilities have priority scores (0-100) indicating importance:
|
|||||||
- **50-69 (Medium):** Use when available
|
- **50-69 (Medium):** Use when available
|
||||||
- **Below 50 (Low):** Optional convenience
|
- **Below 50 (Low):** Optional convenience
|
||||||
|
|
||||||
**issue-facade priority: 95 (Critical)**
|
**issue-core priority: 95 (Critical)**
|
||||||
|
|
||||||
Agents should check priority when deciding whether to use a capability or fall back to alternatives.
|
Agents should check priority when deciding whether to use a capability or fall back to alternatives.
|
||||||
|
|
||||||
@@ -367,7 +367,7 @@ Agents should check priority when deciding whether to use a capability or fall b
|
|||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
**Q: Why not just document "use issue-facade" in README?**
|
**Q: Why not just document "use issue-core" in README?**
|
||||||
A: Agents often skip general docs. Putting it in `.claude/capabilities/` makes it part of their working context.
|
A: Agents often skip general docs. Putting it in `.claude/capabilities/` makes it part of their working context.
|
||||||
|
|
||||||
**Q: What if agent bypasses capability anyway?**
|
**Q: What if agent bypasses capability anyway?**
|
||||||
@@ -405,10 +405,10 @@ response = requests.post(gitea_url + "/issues", ...)
|
|||||||
|
|
||||||
**With capability (properly integrated):**
|
**With capability (properly integrated):**
|
||||||
```python
|
```python
|
||||||
# Agent checks .claude/capabilities/issue-facade.md
|
# Agent checks .claude/capabilities/issue-core.md
|
||||||
# Reads: "Use this API, don't use direct requests"
|
# Reads: "Use this API, don't use direct requests"
|
||||||
# Agent follows documented pattern:
|
# Agent follows documented pattern:
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
backend = GiteaBackend()
|
backend = GiteaBackend()
|
||||||
backend.connect(config)
|
backend.connect(config)
|
||||||
backend.create_issue(issue)
|
backend.create_issue(issue)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Issue Facade - Agent Integration Context
|
# Issue Core - Agent Integration Context
|
||||||
|
|
||||||
**🤖 For Coding Agents: Read this to understand how to use issue tracking in this project.**
|
**🤖 For Coding Agents: Read this to understand how to use issue tracking in this project.**
|
||||||
|
|
||||||
@@ -25,15 +25,15 @@
|
|||||||
# Verify installation
|
# Verify installation
|
||||||
issue --version
|
issue --version
|
||||||
# or
|
# or
|
||||||
python -c "from issue_tracker.backends.gitea import GiteaBackend; print('OK')"
|
python -c "from issue_core.backends.gitea import GiteaBackend; print('OK')"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Basic Usage (Python)
|
### Basic Usage (Python)
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
from issue_tracker.core.models import Issue, Label, IssueState, User, Comment
|
from issue_core.core.models import Issue, Label, IssueState, User, Comment
|
||||||
from issue_tracker.core.interfaces import IssueFilter
|
from issue_core.core.interfaces import IssueFilter
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@@ -228,7 +228,7 @@ if not verify_issue_backend():
|
|||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from issue_tracker.backends.gitea.backend import GiteaAPIError
|
from issue_core.backends.gitea.backend import GiteaAPIError
|
||||||
|
|
||||||
try:
|
try:
|
||||||
issue = backend.get_issue_by_number(42)
|
issue = backend.get_issue_by_number(42)
|
||||||
@@ -317,7 +317,7 @@ If you're unsure whether to use this capability for something:
|
|||||||
- **NO** → You can use other methods
|
- **NO** → You can use other methods
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
- "Create an issue for the bug I found" → **Use issue-facade**
|
- "Create an issue for the bug I found" → **Use issue-core**
|
||||||
- "Read the project README" → Don't need issue-facade
|
- "Read the project README" → Don't need issue-core
|
||||||
- "Check if issue #42 exists" → **Use issue-facade**
|
- "Check if issue #42 exists" → **Use issue-core**
|
||||||
- "Clone the repository" → Don't need issue-facade
|
- "Clone the repository" → Don't need issue-core
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Integration script for issue-facade capability
|
# Integration script for issue-core capability
|
||||||
# This script helps the main project discover and integrate the capability
|
# This script helps the main project discover and integrate the capability
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
CAPABILITY_NAME="issue-facade"
|
CAPABILITY_NAME="issue-core"
|
||||||
CAPABILITY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
CAPABILITY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$CAPABILITY_DIR/../.." && pwd)}"
|
PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$CAPABILITY_DIR/../.." && pwd)}"
|
||||||
|
|
||||||
echo "🔧 Issue Facade Capability Integration"
|
echo "🔧 Issue Core Capability Integration"
|
||||||
echo " Capability: $CAPABILITY_DIR"
|
echo " Capability: $CAPABILITY_DIR"
|
||||||
echo " Project: $PROJECT_ROOT"
|
echo " Project: $PROJECT_ROOT"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -90,7 +90,7 @@ case $choice in
|
|||||||
echo "📝 Adding to Claude Code context..."
|
echo "📝 Adding to Claude Code context..."
|
||||||
mkdir -p "$PROJECT_ROOT/.claude/capabilities"
|
mkdir -p "$PROJECT_ROOT/.claude/capabilities"
|
||||||
cp "$CAPABILITY_DIR/.capability/agent-context.md" \
|
cp "$CAPABILITY_DIR/.capability/agent-context.md" \
|
||||||
"$PROJECT_ROOT/.claude/capabilities/issue-facade.md"
|
"$PROJECT_ROOT/.claude/capabilities/issue-core.md"
|
||||||
|
|
||||||
# Create or update context file
|
# Create or update context file
|
||||||
CONTEXT_FILE="$PROJECT_ROOT/.claude/context/capabilities.md"
|
CONTEXT_FILE="$PROJECT_ROOT/.claude/context/capabilities.md"
|
||||||
@@ -102,10 +102,10 @@ case $choice in
|
|||||||
|
|
||||||
This project uses specialized capabilities. Always check for existing capabilities before implementing similar functionality.
|
This project uses specialized capabilities. Always check for existing capabilities before implementing similar functionality.
|
||||||
|
|
||||||
## Issue Tracking: issue-facade
|
## Issue Tracking: issue-core
|
||||||
|
|
||||||
**Location:** `capabilities/issue-facade/`
|
**Location:** `capabilities/issue-core/`
|
||||||
**Documentation:** `.claude/capabilities/issue-facade.md`
|
**Documentation:** `.claude/capabilities/issue-core.md`
|
||||||
**Priority:** CRITICAL (always use for issue operations)
|
**Priority:** CRITICAL (always use for issue operations)
|
||||||
|
|
||||||
**MUST USE FOR:**
|
**MUST USE FOR:**
|
||||||
@@ -120,13 +120,13 @@ This project uses specialized capabilities. Always check for existing capabiliti
|
|||||||
|
|
||||||
**Quick Start:**
|
**Quick Start:**
|
||||||
```python
|
```python
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
backend = GiteaBackend()
|
backend = GiteaBackend()
|
||||||
backend.connect(config)
|
backend.connect(config)
|
||||||
issues = backend.list_issues()
|
issues = backend.list_issues()
|
||||||
```
|
```
|
||||||
|
|
||||||
**Full Documentation:** See `.claude/capabilities/issue-facade.md`
|
**Full Documentation:** See `.claude/capabilities/issue-core.md`
|
||||||
EOF
|
EOF
|
||||||
echo "✓ Created $CONTEXT_FILE"
|
echo "✓ Created $CONTEXT_FILE"
|
||||||
else
|
else
|
||||||
@@ -138,7 +138,7 @@ EOF
|
|||||||
echo "✓ Added to Claude Code context"
|
echo "✓ Added to Claude Code context"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Files created:"
|
echo "Files created:"
|
||||||
echo " - $PROJECT_ROOT/.claude/capabilities/issue-facade.md"
|
echo " - $PROJECT_ROOT/.claude/capabilities/issue-core.md"
|
||||||
echo " - $CONTEXT_FILE"
|
echo " - $CONTEXT_FILE"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
@@ -148,15 +148,15 @@ EOF
|
|||||||
mkdir -p "$PROJECT_ROOT/.claude/commands"
|
mkdir -p "$PROJECT_ROOT/.claude/commands"
|
||||||
|
|
||||||
cat > "$PROJECT_ROOT/.claude/commands/use-issues.md" << 'EOF'
|
cat > "$PROJECT_ROOT/.claude/commands/use-issues.md" << 'EOF'
|
||||||
You are working with issue tracking. Use the **issue-facade capability**:
|
You are working with issue tracking. Use the **issue-core capability**:
|
||||||
|
|
||||||
## Available API
|
## Available API
|
||||||
|
|
||||||
**Python (Recommended):**
|
**Python (Recommended):**
|
||||||
```python
|
```python
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
from issue_tracker.core.models import Issue, Label, IssueState
|
from issue_core.core.models import Issue, Label, IssueState
|
||||||
from issue_tracker.core.interfaces import IssueFilter
|
from issue_core.core.interfaces import IssueFilter
|
||||||
|
|
||||||
backend = GiteaBackend()
|
backend = GiteaBackend()
|
||||||
backend.connect(config)
|
backend.connect(config)
|
||||||
@@ -193,7 +193,7 @@ issue close 42 --comment="Fixed"
|
|||||||
|
|
||||||
## Full Documentation
|
## Full Documentation
|
||||||
|
|
||||||
See `capabilities/issue-facade/AGENT_INTEGRATION.md` for:
|
See `capabilities/issue-core/AGENT_INTEGRATION.md` for:
|
||||||
- Complete API reference
|
- Complete API reference
|
||||||
- Coordination patterns
|
- Coordination patterns
|
||||||
- Error handling
|
- Error handling
|
||||||
@@ -206,7 +206,7 @@ EOF
|
|||||||
echo "Usage in Claude Code:"
|
echo "Usage in Claude Code:"
|
||||||
echo " /use-issues"
|
echo " /use-issues"
|
||||||
echo ""
|
echo ""
|
||||||
echo "This will inject issue-facade context into the conversation."
|
echo "This will inject issue-core context into the conversation."
|
||||||
;;
|
;;
|
||||||
|
|
||||||
5)
|
5)
|
||||||
@@ -250,7 +250,7 @@ EOF
|
|||||||
mkdir -p "$PROJECT_ROOT/.claude/context"
|
mkdir -p "$PROJECT_ROOT/.claude/context"
|
||||||
|
|
||||||
cp "$CAPABILITY_DIR/.capability/agent-context.md" \
|
cp "$CAPABILITY_DIR/.capability/agent-context.md" \
|
||||||
"$PROJECT_ROOT/.claude/capabilities/issue-facade.md"
|
"$PROJECT_ROOT/.claude/capabilities/issue-core.md"
|
||||||
|
|
||||||
# Create context file if not exists
|
# Create context file if not exists
|
||||||
CONTEXT_FILE="$PROJECT_ROOT/.claude/context/capabilities.md"
|
CONTEXT_FILE="$PROJECT_ROOT/.claude/context/capabilities.md"
|
||||||
@@ -258,21 +258,21 @@ EOF
|
|||||||
cat > "$CONTEXT_FILE" << 'EOF'
|
cat > "$CONTEXT_FILE" << 'EOF'
|
||||||
# Available Capabilities
|
# Available Capabilities
|
||||||
|
|
||||||
## Issue Tracking: issue-facade
|
## Issue Tracking: issue-core
|
||||||
|
|
||||||
**CRITICAL:** Always use this for issue operations. Never bypass with direct API calls.
|
**CRITICAL:** Always use this for issue operations. Never bypass with direct API calls.
|
||||||
|
|
||||||
**Docs:** `.claude/capabilities/issue-facade.md`
|
**Docs:** `.claude/capabilities/issue-core.md`
|
||||||
**Usage:** `/use-issues`
|
**Usage:** `/use-issues`
|
||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create slash command
|
# Create slash command
|
||||||
cat > "$PROJECT_ROOT/.claude/commands/use-issues.md" << 'EOF'
|
cat > "$PROJECT_ROOT/.claude/commands/use-issues.md" << 'EOF'
|
||||||
Use the issue-facade capability for all issue tracking operations.
|
Use the issue-core capability for all issue tracking operations.
|
||||||
|
|
||||||
**Quick reference:** See `.claude/capabilities/issue-facade.md`
|
**Quick reference:** See `.claude/capabilities/issue-core.md`
|
||||||
**Examples:** See `capabilities/issue-facade/examples/agents/`
|
**Examples:** See `capabilities/issue-core/examples/agents/`
|
||||||
|
|
||||||
**DO NOT use direct API calls or platform CLIs!**
|
**DO NOT use direct API calls or platform CLIs!**
|
||||||
EOF
|
EOF
|
||||||
@@ -285,7 +285,7 @@ EOF
|
|||||||
echo ""
|
echo ""
|
||||||
issue --version && echo "✓ CLI works" || echo "❌ CLI not working"
|
issue --version && echo "✓ CLI works" || echo "❌ CLI not working"
|
||||||
issue backend list | grep -q "default" && echo "✓ Backend configured" || echo "⚠️ Backend not configured"
|
issue backend list | grep -q "default" && echo "✓ Backend configured" || echo "⚠️ Backend not configured"
|
||||||
[ -f "$PROJECT_ROOT/.claude/capabilities/issue-facade.md" ] && echo "✓ Claude context exists" || echo "❌ Claude context missing"
|
[ -f "$PROJECT_ROOT/.claude/capabilities/issue-core.md" ] && echo "✓ Claude context exists" || echo "❌ Claude context missing"
|
||||||
[ -f "$PROJECT_ROOT/.claude/commands/use-issues.md" ] && echo "✓ Slash command exists" || echo "❌ Slash command missing"
|
[ -f "$PROJECT_ROOT/.claude/commands/use-issues.md" ] && echo "✓ Slash command exists" || echo "❌ Slash command missing"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
@@ -294,7 +294,7 @@ EOF
|
|||||||
echo "Next steps:"
|
echo "Next steps:"
|
||||||
echo " 1. Test: issue list --limit=5"
|
echo " 1. Test: issue list --limit=5"
|
||||||
echo " 2. In Claude Code: /use-issues"
|
echo " 2. In Claude Code: /use-issues"
|
||||||
echo " 3. See examples: capabilities/issue-facade/examples/agents/"
|
echo " 3. See examples: capabilities/issue-core/examples/agents/"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
0)
|
0)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Issue Facade Integration Checklist
|
# Issue Core Integration Checklist
|
||||||
|
|
||||||
**For project maintainers integrating this capability into their codebase.**
|
**For project maintainers integrating this capability into their codebase.**
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
- [ ] **Install capability:**
|
- [ ] **Install capability:**
|
||||||
```bash
|
```bash
|
||||||
pip install -e capabilities/issue-facade/
|
pip install -e capabilities/issue-core/
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Verify installation:**
|
- [ ] **Verify installation:**
|
||||||
@@ -42,8 +42,8 @@
|
|||||||
- [ ] **Copy agent context to project:**
|
- [ ] **Copy agent context to project:**
|
||||||
```bash
|
```bash
|
||||||
mkdir -p .claude/capabilities/
|
mkdir -p .claude/capabilities/
|
||||||
cp capabilities/issue-facade/.capability/agent-context.md \
|
cp capabilities/issue-core/.capability/agent-context.md \
|
||||||
.claude/capabilities/issue-facade.md
|
.claude/capabilities/issue-core.md
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Add to Claude Code context:**
|
- [ ] **Add to Claude Code context:**
|
||||||
@@ -53,17 +53,17 @@
|
|||||||
|
|
||||||
This project uses specialized capabilities. Always check these before implementing similar functionality.
|
This project uses specialized capabilities. Always check these before implementing similar functionality.
|
||||||
|
|
||||||
## Issue Tracking: issue-facade
|
## Issue Tracking: issue-core
|
||||||
|
|
||||||
**Location:** `capabilities/issue-facade/`
|
**Location:** `capabilities/issue-core/`
|
||||||
**Documentation:** `.claude/capabilities/issue-facade.md`
|
**Documentation:** `.claude/capabilities/issue-core.md`
|
||||||
|
|
||||||
**CRITICAL:** Always use this capability for issue operations. Never use:
|
**CRITICAL:** Always use this capability for issue operations. Never use:
|
||||||
- Direct API calls (requests to /api/v1/repos/...)
|
- Direct API calls (requests to /api/v1/repos/...)
|
||||||
- Platform CLIs (gh, glab)
|
- Platform CLIs (gh, glab)
|
||||||
- Platform libraries (PyGithub, python-gitlab)
|
- Platform libraries (PyGithub, python-gitlab)
|
||||||
|
|
||||||
See `.claude/capabilities/issue-facade.md` for usage patterns.
|
See `.claude/capabilities/issue-core.md` for usage patterns.
|
||||||
```
|
```
|
||||||
|
|
||||||
### Option 2: Slash Command
|
### Option 2: Slash Command
|
||||||
@@ -71,11 +71,11 @@
|
|||||||
- [ ] **Create slash command:**
|
- [ ] **Create slash command:**
|
||||||
Create `.claude/commands/use-issues.md`:
|
Create `.claude/commands/use-issues.md`:
|
||||||
```markdown
|
```markdown
|
||||||
You are working with issue tracking. Use the issue-facade capability:
|
You are working with issue tracking. Use the issue-core capability:
|
||||||
|
|
||||||
**Python API:**
|
**Python API:**
|
||||||
```python
|
```python
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
backend = GiteaBackend()
|
backend = GiteaBackend()
|
||||||
backend.connect(config)
|
backend.connect(config)
|
||||||
```
|
```
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
issue create "Title" --label=bug
|
issue create "Title" --label=bug
|
||||||
```
|
```
|
||||||
|
|
||||||
**Full docs:** See `capabilities/issue-facade/AGENT_INTEGRATION.md`
|
**Full docs:** See `capabilities/issue-core/AGENT_INTEGRATION.md`
|
||||||
|
|
||||||
**DO NOT use direct API calls or platform CLIs!**
|
**DO NOT use direct API calls or platform CLIs!**
|
||||||
```
|
```
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
## Agent Configuration
|
## Agent Configuration
|
||||||
|
|
||||||
- [ ] **Set agent identity:**
|
- [ ] **Set agent identity:**
|
||||||
Add to `.issue-facade/config.json`:
|
Add to `.issue-core/config.json`:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"agent": {
|
"agent": {
|
||||||
@@ -126,8 +126,8 @@
|
|||||||
|
|
||||||
- [ ] **Test basic operations:**
|
- [ ] **Test basic operations:**
|
||||||
```python
|
```python
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
from issue_tracker.core.interfaces import IssueFilter
|
from issue_core.core.interfaces import IssueFilter
|
||||||
|
|
||||||
backend = GiteaBackend()
|
backend = GiteaBackend()
|
||||||
backend.connect({'base_url': '...', 'token': '...', 'owner': '...', 'repo': '...'})
|
backend.connect({'base_url': '...', 'token': '...', 'owner': '...', 'repo': '...'})
|
||||||
@@ -153,26 +153,26 @@
|
|||||||
```markdown
|
```markdown
|
||||||
## Issue Tracking
|
## Issue Tracking
|
||||||
|
|
||||||
This project uses the issue-facade capability for unified issue tracking.
|
This project uses the issue-core capability for unified issue tracking.
|
||||||
|
|
||||||
**Setup:**
|
**Setup:**
|
||||||
```bash
|
```bash
|
||||||
pip install -e capabilities/issue-facade/
|
pip install -e capabilities/issue-core/
|
||||||
export GITEA_API_TOKEN="your-token"
|
export GITEA_API_TOKEN="your-token"
|
||||||
issue backend add myproject gitea
|
issue backend add myproject gitea
|
||||||
```
|
```
|
||||||
|
|
||||||
**Usage:** See `capabilities/issue-facade/AGENT_INTEGRATION.md`
|
**Usage:** See `capabilities/issue-core/AGENT_INTEGRATION.md`
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Add to CONTRIBUTING.md:**
|
- [ ] **Add to CONTRIBUTING.md:**
|
||||||
```markdown
|
```markdown
|
||||||
### Issue Tracking
|
### Issue Tracking
|
||||||
|
|
||||||
Always use the `issue` command or Python API from `issue_tracker` package.
|
Always use the `issue` command or Python API from `issue_core` package.
|
||||||
Never make direct API calls to Gitea/GitHub/GitLab.
|
Never make direct API calls to Gitea/GitHub/GitLab.
|
||||||
|
|
||||||
Examples: `capabilities/issue-facade/examples/agents/`
|
Examples: `capabilities/issue-core/examples/agents/`
|
||||||
```
|
```
|
||||||
|
|
||||||
## Security Review
|
## Security Review
|
||||||
@@ -180,9 +180,9 @@
|
|||||||
- [ ] **Verify tokens are not in code:** `git grep GITEA_TOKEN` (should be empty)
|
- [ ] **Verify tokens are not in code:** `git grep GITEA_TOKEN` (should be empty)
|
||||||
- [ ] **Check .gitignore includes:**
|
- [ ] **Check .gitignore includes:**
|
||||||
```
|
```
|
||||||
.issue-facade/config.json
|
.issue-core/config.json
|
||||||
.issue-facade/issues.db
|
.issue-core/issues.db
|
||||||
.issue-facade/credentials.json
|
.issue-core/credentials.json
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Audit token permissions:** Read-only for bots, write for implementation
|
- [ ] **Audit token permissions:** Read-only for bots, write for implementation
|
||||||
@@ -192,7 +192,7 @@
|
|||||||
|
|
||||||
- [ ] **Run capability tests:**
|
- [ ] **Run capability tests:**
|
||||||
```bash
|
```bash
|
||||||
cd capabilities/issue-facade/
|
cd capabilities/issue-core/
|
||||||
make test
|
make test
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -210,7 +210,7 @@
|
|||||||
|
|
||||||
- [ ] **Schedule regular updates:**
|
- [ ] **Schedule regular updates:**
|
||||||
```bash
|
```bash
|
||||||
cd capabilities/issue-facade/
|
cd capabilities/issue-core/
|
||||||
git pull origin main
|
git pull origin main
|
||||||
pip install -e . --upgrade
|
pip install -e . --upgrade
|
||||||
```
|
```
|
||||||
@@ -232,7 +232,7 @@ If capability causes issues:
|
|||||||
|
|
||||||
- [ ] **Keep backup config:**
|
- [ ] **Keep backup config:**
|
||||||
```bash
|
```bash
|
||||||
cp ~/.config/issue-facade/backends.json ~/.config/issue-facade/backends.json.backup
|
cp ~/.config/issue-core/backends.json ~/.config/issue-core/backends.json.backup
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Document rollback steps in project wiki/docs**
|
- [ ] **Document rollback steps in project wiki/docs**
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# Agent Integration Guide
|
# Agent Integration Guide
|
||||||
|
|
||||||
**Issue Facade for Autonomous Coding Agent Coordination**
|
**Issue Core for Autonomous Coding Agent Coordination**
|
||||||
|
|
||||||
## Purpose
|
## 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.
|
The **Issue Core** 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?
|
### Why Issue Tracking for Agent Coordination?
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ Issue tracking provides a natural coordination mechanism for multi-agent softwar
|
|||||||
### 1. Installation
|
### 1. Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd capabilities/issue-facade
|
cd capabilities/issue-core
|
||||||
pip install -e .
|
pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ issue backend set-default my-project
|
|||||||
# Configure local SQLite backend
|
# Configure local SQLite backend
|
||||||
issue backend add local-work local
|
issue backend add local-work local
|
||||||
# Prompts for:
|
# Prompts for:
|
||||||
# - Database path: .issue-facade/issues.db
|
# - Database path: .issue-core/issues.db
|
||||||
|
|
||||||
issue backend set-default local-work
|
issue backend set-default local-work
|
||||||
```
|
```
|
||||||
@@ -178,8 +178,8 @@ issue list --label=reviewed --state=closed --format=json | \
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
# Agent creates implementation issues from requirements
|
# Agent creates implementation issues from requirements
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
from issue_tracker.core.models import Issue, Label, IssueState
|
from issue_core.core.models import Issue, Label, IssueState
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
backend = GiteaBackend()
|
backend = GiteaBackend()
|
||||||
@@ -224,9 +224,9 @@ for task in subtasks:
|
|||||||
### Python Integration
|
### Python Integration
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
from issue_tracker.core.models import Issue, Label, IssueState, User
|
from issue_core.core.models import Issue, Label, IssueState, User
|
||||||
from issue_tracker.core.interfaces import IssueFilter
|
from issue_core.core.interfaces import IssueFilter
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@@ -271,7 +271,7 @@ created.state = IssueState.IN_PROGRESS
|
|||||||
backend.update_issue(created)
|
backend.update_issue(created)
|
||||||
|
|
||||||
# Add comment
|
# Add comment
|
||||||
from issue_tracker.core.models import Comment
|
from issue_core.core.models import Comment
|
||||||
comment = Comment(
|
comment = Comment(
|
||||||
id=None,
|
id=None,
|
||||||
body="Analysis complete. Root cause: unclosed file handles in line 234",
|
body="Analysis complete. Root cause: unclosed file handles in line 234",
|
||||||
@@ -432,7 +432,7 @@ issue sync push backup gitea-remote
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
# Check for conflicts before sync
|
# Check for conflicts before sync
|
||||||
from issue_tracker.cli.sync_commands import sync_pull
|
from issue_core.cli.sync_commands import sync_pull
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sync_pull(source='remote', target='local', dry_run=True)
|
sync_pull(source='remote', target='local', dry_run=True)
|
||||||
@@ -505,7 +505,7 @@ Create a setup script for each project:
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# setup-issue-tracking.sh
|
# setup-issue-tracking.sh
|
||||||
|
|
||||||
cat > .issue-facade-config << EOF
|
cat > .issue-core-config << EOF
|
||||||
GITEA_URL=https://gitea.example.com
|
GITEA_URL=https://gitea.example.com
|
||||||
GITEA_OWNER=myorg
|
GITEA_OWNER=myorg
|
||||||
GITEA_REPO=myproject
|
GITEA_REPO=myproject
|
||||||
@@ -513,7 +513,7 @@ GITEA_TOKEN_FILE=~/.secrets/gitea-token
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Load config and configure backend
|
# Load config and configure backend
|
||||||
source .issue-facade-config
|
source .issue-core-config
|
||||||
export GITEA_API_TOKEN=$(cat $GITEA_TOKEN_FILE)
|
export GITEA_API_TOKEN=$(cat $GITEA_TOKEN_FILE)
|
||||||
|
|
||||||
issue backend add $(basename $(pwd)) gitea <<INPUT
|
issue backend add $(basename $(pwd)) gitea <<INPUT
|
||||||
@@ -551,7 +551,7 @@ for issue_number in [1, 2, 3, 4, 5]:
|
|||||||
backend.update_issue(issue)
|
backend.update_issue(issue)
|
||||||
|
|
||||||
# GOOD: Use local backend for bulk operations
|
# GOOD: Use local backend for bulk operations
|
||||||
from issue_tracker.backends.local import LocalSQLiteBackend
|
from issue_core.backends.local import LocalSQLiteBackend
|
||||||
|
|
||||||
local = LocalSQLiteBackend()
|
local = LocalSQLiteBackend()
|
||||||
local.connect({'db_path': '/tmp/batch.db'})
|
local.connect({'db_path': '/tmp/batch.db'})
|
||||||
@@ -595,7 +595,7 @@ if time.time() - last_fetch > 60:
|
|||||||
### Phase 1: Auto-Configuration (v1.1)
|
### Phase 1: Auto-Configuration (v1.1)
|
||||||
- Automatic git remote detection
|
- Automatic git remote detection
|
||||||
- Environment-variable-only setup
|
- Environment-variable-only setup
|
||||||
- Per-repository `.issue-facade/config.json` support
|
- Per-repository `.issue-core/config.json` support
|
||||||
- `issue config detect` command
|
- `issue config detect` command
|
||||||
|
|
||||||
### Phase 2: Agent Features (v1.2)
|
### Phase 2: Agent Features (v1.2)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Issue Facade Capability Manifest
|
# Issue Core Capability Manifest
|
||||||
# This file describes the capability to coding agents and integration systems
|
# This file describes the capability to coding agents and integration systems
|
||||||
|
|
||||||
metadata:
|
metadata:
|
||||||
name: issue-facade
|
name: issue-core
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
type: coordination-tool
|
type: coordination-tool
|
||||||
description: >
|
description: >
|
||||||
@@ -44,7 +44,7 @@ integration:
|
|||||||
methods:
|
methods:
|
||||||
python_api:
|
python_api:
|
||||||
available: true
|
available: true
|
||||||
import: "from issue_tracker.backends.gitea import GiteaBackend"
|
import: "from issue_core.backends.gitea import GiteaBackend"
|
||||||
docs: "AGENT_INTEGRATION.md"
|
docs: "AGENT_INTEGRATION.md"
|
||||||
|
|
||||||
cli:
|
cli:
|
||||||
@@ -59,7 +59,7 @@ integration:
|
|||||||
|
|
||||||
installation:
|
installation:
|
||||||
method: pip
|
method: pip
|
||||||
command: "pip install -e capabilities/issue-facade/"
|
command: "pip install -e capabilities/issue-core/"
|
||||||
verify: "issue --version"
|
verify: "issue --version"
|
||||||
|
|
||||||
configuration:
|
configuration:
|
||||||
@@ -119,8 +119,8 @@ credentials:
|
|||||||
|
|
||||||
security:
|
security:
|
||||||
- "Tokens never in code or logs"
|
- "Tokens never in code or logs"
|
||||||
- "Config stored in ~/.config/issue-facade/"
|
- "Config stored in ~/.config/issue-core/"
|
||||||
- "Per-repo config in .issue-facade/ (gitignored)"
|
- "Per-repo config in .issue-core/ (gitignored)"
|
||||||
|
|
||||||
best_practices:
|
best_practices:
|
||||||
- "Use read-only tokens for monitoring agents"
|
- "Use read-only tokens for monitoring agents"
|
||||||
@@ -131,8 +131,8 @@ credentials:
|
|||||||
agent_guidance:
|
agent_guidance:
|
||||||
quick_start: |
|
quick_start: |
|
||||||
# For Python agents:
|
# For Python agents:
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
from issue_tracker.core.interfaces import IssueFilter
|
from issue_core.core.interfaces import IssueFilter
|
||||||
|
|
||||||
backend = GiteaBackend()
|
backend = GiteaBackend()
|
||||||
backend.connect(config)
|
backend.connect(config)
|
||||||
@@ -177,7 +177,7 @@ feedback:
|
|||||||
archived: "Resolved or outdated feedback"
|
archived: "Resolved or outdated feedback"
|
||||||
|
|
||||||
for_users: |
|
for_users: |
|
||||||
Submit feedback about issue-facade:
|
Submit feedback about issue-core:
|
||||||
./.capability/feedback submit "Feedback text"
|
./.capability/feedback submit "Feedback text"
|
||||||
./.capability/feedback submit detailed-feedback.md
|
./.capability/feedback submit detailed-feedback.md
|
||||||
|
|
||||||
@@ -261,7 +261,7 @@ support:
|
|||||||
solution: "Check GITEA_API_TOKEN is set and valid"
|
solution: "Check GITEA_API_TOKEN is set and valid"
|
||||||
|
|
||||||
- problem: "Command not found: issue"
|
- problem: "Command not found: issue"
|
||||||
solution: "Run: pip install -e capabilities/issue-facade/"
|
solution: "Run: pip install -e capabilities/issue-core/"
|
||||||
|
|
||||||
# Integration priority score (higher = more important for agent to use)
|
# Integration priority score (higher = more important for agent to use)
|
||||||
priority:
|
priority:
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Enhanced agent integration documentation
|
- Enhanced agent integration documentation
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- ID mapping bugs in issue-facade
|
- ID mapping bugs in issue-core
|
||||||
- Sync metadata handling
|
- Sync metadata handling
|
||||||
- Backend initialization edge cases
|
- Backend initialization edge cases
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
## [0.1.0] - 2024-10-06
|
## [0.1.0] - 2024-10-06
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Initial extraction of issue-facade as standalone capability
|
- Initial extraction of issue-core as standalone capability
|
||||||
- Core CRUD operations (create, read, update, delete issues)
|
- Core CRUD operations (create, read, update, delete issues)
|
||||||
- Gitea backend implementation (production-ready)
|
- Gitea backend implementation (production-ready)
|
||||||
- Local SQLite backend (offline capability)
|
- Local SQLite backend (offline capability)
|
||||||
@@ -173,6 +173,6 @@ To submit feedback, see [feedback/README.md](feedback/README.md).
|
|||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
|
||||||
- [Repository](http://92.205.130.254:32166/coulomb/issue-facade)
|
- [Repository](http://92.205.130.254:32166/coulomb/issue-core)
|
||||||
- [Issues](http://92.205.130.254:32166/coulomb/issue-facade/issues)
|
- [Issues](http://92.205.130.254:32166/coulomb/issue-core/issues)
|
||||||
- [MarkiTect Project](https://github.com/markitect)
|
- [MarkiTect Project](https://github.com/markitect)
|
||||||
|
|||||||
46
CLAUDE.md
46
CLAUDE.md
@@ -4,28 +4,28 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
Issue Facade is a universal CLI for issue tracking that provides a unified interface to multiple issue tracking backends (GitHub, GitLab, Gitea, local SQLite). It implements the **Facade Pattern** to abstract away differences between various issue tracking systems, providing developers with a consistent CLI experience regardless of the underlying backend.
|
Issue Core is a universal CLI for issue tracking that provides a unified interface to multiple issue tracking backends (GitHub, GitLab, Gitea, local SQLite). It implements the **Facade Pattern** to abstract away differences between various issue tracking systems, providing developers with a consistent CLI experience regardless of the underlying backend.
|
||||||
|
|
||||||
## Development Commands
|
## Development Commands
|
||||||
|
|
||||||
### Installation & Setup
|
### Installation & Setup
|
||||||
- Install for development: `pip install -e ".[dev]"`
|
- Install for development: `pip install -e ".[dev]"`
|
||||||
- Install production: `pip install -e .`
|
- Install production: `pip install -e .`
|
||||||
- Clean build artifacts: `make issue-facade-clean`
|
- Clean build artifacts: `make issue-core-clean`
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
- Run all tests: `pytest tests/`
|
- Run all tests: `pytest tests/`
|
||||||
- Run specific test file: `pytest tests/test_gitea_backend.py`
|
- Run specific test file: `pytest tests/test_gitea_backend.py`
|
||||||
- Run with coverage: `pytest tests/ --cov=issue_tracker --cov-report=html --cov-report=term`
|
- Run with coverage: `pytest tests/ --cov=issue_core --cov-report=html --cov-report=term`
|
||||||
- Run integration tests: `pytest tests/test_gitea_integration.py -v`
|
- Run integration tests: `pytest tests/test_gitea_integration.py -v`
|
||||||
|
|
||||||
### Code Quality
|
### Code Quality
|
||||||
- Run linter: `make issue-facade-lint`
|
- Run linter: `make issue-core-lint`
|
||||||
- Format code: `black issue_tracker/ tests/` (line length: 100)
|
- Format code: `black issue_core/ tests/` (line length: 100)
|
||||||
- Sort imports: `isort issue_tracker/ tests/`
|
- Sort imports: `isort issue_core/ tests/`
|
||||||
|
|
||||||
### CLI Usage
|
### CLI Usage
|
||||||
The project provides two entry points: `issue` and `issue-tracker` (both execute `issue_tracker.cli.main:main`)
|
The project provides two entry points: `issue` and `issue-core` (both execute `issue_core.cli.main:main`)
|
||||||
|
|
||||||
Common commands:
|
Common commands:
|
||||||
- `issue list` - List issues
|
- `issue list` - List issues
|
||||||
@@ -44,18 +44,18 @@ The codebase implements a **plugin-based facade pattern** with clear separation
|
|||||||
```
|
```
|
||||||
┌─────────────────────────────────────────┐
|
┌─────────────────────────────────────────┐
|
||||||
│ CLI Layer (Click) │
|
│ CLI Layer (Click) │
|
||||||
│ issue_tracker/cli/*.py │
|
│ issue_core/cli/*.py │
|
||||||
└───────────────┬─────────────────────────┘
|
└───────────────┬─────────────────────────┘
|
||||||
│
|
│
|
||||||
┌───────────────▼─────────────────────────┐
|
┌───────────────▼─────────────────────────┐
|
||||||
│ Core Domain Models │
|
│ Core Domain Models │
|
||||||
│ issue_tracker/core/models.py │
|
│ issue_core/core/models.py │
|
||||||
│ (Issue, Label, User, etc.) │
|
│ (Issue, Label, User, etc.) │
|
||||||
└───────────────┬─────────────────────────┘
|
└───────────────┬─────────────────────────┘
|
||||||
│
|
│
|
||||||
┌───────────────▼─────────────────────────┐
|
┌───────────────▼─────────────────────────┐
|
||||||
│ Backend Interface (ABC) │
|
│ Backend Interface (ABC) │
|
||||||
│ issue_tracker/core/interfaces.py │
|
│ issue_core/core/interfaces.py │
|
||||||
│ IssueBackend, LocalBackend, │
|
│ IssueBackend, LocalBackend, │
|
||||||
│ RemoteBackend, SyncableBackend │
|
│ RemoteBackend, SyncableBackend │
|
||||||
└───────────────┬─────────────────────────┘
|
└───────────────┬─────────────────────────┘
|
||||||
@@ -70,7 +70,7 @@ The codebase implements a **plugin-based facade pattern** with clear separation
|
|||||||
|
|
||||||
### Key Components
|
### Key Components
|
||||||
|
|
||||||
#### 1. Core Domain Models (`issue_tracker/core/models.py`)
|
#### 1. Core Domain Models (`issue_core/core/models.py`)
|
||||||
- **Issue**: Universal issue model with state management, label categorization, and domain logic
|
- **Issue**: Universal issue model with state management, label categorization, and domain logic
|
||||||
- **Label**: Supports categorization (priority/type/status/other) with cached properties
|
- **Label**: Supports categorization (priority/type/status/other) with cached properties
|
||||||
- **User, Milestone, Comment**: Supporting models
|
- **User, Milestone, Comment**: Supporting models
|
||||||
@@ -78,7 +78,7 @@ The codebase implements a **plugin-based facade pattern** with clear separation
|
|||||||
|
|
||||||
The Issue model uses `@cached_property` for performance optimization and includes domain logic methods (`close()`, `reopen()`, `add_label()`, etc.) that enforce business rules.
|
The Issue model uses `@cached_property` for performance optimization and includes domain logic methods (`close()`, `reopen()`, `add_label()`, etc.) that enforce business rules.
|
||||||
|
|
||||||
#### 2. Backend Interface (`issue_tracker/core/interfaces.py`)
|
#### 2. Backend Interface (`issue_core/core/interfaces.py`)
|
||||||
- **IssueBackend (ABC)**: Defines the contract all backends must implement
|
- **IssueBackend (ABC)**: Defines the contract all backends must implement
|
||||||
- **LocalBackend, RemoteBackend**: Marker interfaces for backend categorization
|
- **LocalBackend, RemoteBackend**: Marker interfaces for backend categorization
|
||||||
- **SyncableBackend**: Interface for backends supporting synchronization
|
- **SyncableBackend**: Interface for backends supporting synchronization
|
||||||
@@ -94,19 +94,19 @@ The Issue model uses `@cached_property` for performance optimization and include
|
|||||||
|
|
||||||
#### 3. Backend Implementations
|
#### 3. Backend Implementations
|
||||||
|
|
||||||
**Local Backend** (`issue_tracker/backends/local/backend.py`):
|
**Local Backend** (`issue_core/backends/local/backend.py`):
|
||||||
- Uses SQLite with schema defined in `schema.sql`
|
- Uses SQLite with schema defined in `schema.sql`
|
||||||
- Full offline functionality
|
- Full offline functionality
|
||||||
- Serves as synchronization source of truth
|
- Serves as synchronization source of truth
|
||||||
- Implements `LocalBackend` and `SyncableBackend`
|
- Implements `LocalBackend` and `SyncableBackend`
|
||||||
|
|
||||||
**Gitea Backend** (`issue_tracker/backends/gitea/backend.py`):
|
**Gitea Backend** (`issue_core/backends/gitea/backend.py`):
|
||||||
- REST API integration with Gitea instances
|
- REST API integration with Gitea instances
|
||||||
- Rate limiting and error handling
|
- Rate limiting and error handling
|
||||||
- ID mapping between local and remote issues
|
- ID mapping between local and remote issues
|
||||||
- Implements `RemoteBackend` and `SyncableBackend`
|
- Implements `RemoteBackend` and `SyncableBackend`
|
||||||
|
|
||||||
#### 4. CLI Layer (`issue_tracker/cli/`)
|
#### 4. CLI Layer (`issue_core/cli/`)
|
||||||
- **main.py**: Entry point, Click group setup, command registration
|
- **main.py**: Entry point, Click group setup, command registration
|
||||||
- **commands.py**: Core issue operations (list, show, create, close)
|
- **commands.py**: Core issue operations (list, show, create, close)
|
||||||
- **backend_commands.py**: Backend management (add, list, switch)
|
- **backend_commands.py**: Backend management (add, list, switch)
|
||||||
@@ -150,7 +150,7 @@ Run only integration tests: `pytest -m integration`
|
|||||||
|
|
||||||
### Adding a New Backend
|
### Adding a New Backend
|
||||||
|
|
||||||
1. Create backend package in `issue_tracker/backends/<name>/`
|
1. Create backend package in `issue_core/backends/<name>/`
|
||||||
2. Implement `IssueBackend` interface (or extend `LocalBackend`/`RemoteBackend`)
|
2. Implement `IssueBackend` interface (or extend `LocalBackend`/`RemoteBackend`)
|
||||||
3. Implement all abstract methods from the interface
|
3. Implement all abstract methods from the interface
|
||||||
4. Define `BackendCapabilities` to specify supported features
|
4. Define `BackendCapabilities` to specify supported features
|
||||||
@@ -161,7 +161,7 @@ Run only integration tests: `pytest -m integration`
|
|||||||
|
|
||||||
### Modifying the Issue Model
|
### Modifying the Issue Model
|
||||||
|
|
||||||
When changing `issue_tracker/core/models.py`:
|
When changing `issue_core/core/models.py`:
|
||||||
1. Update the `Issue` dataclass definition
|
1. Update the `Issue` dataclass definition
|
||||||
2. Update `to_dict()` serialization method
|
2. Update `to_dict()` serialization method
|
||||||
3. Invalidate caches if adding/modifying label-dependent properties
|
3. Invalidate caches if adding/modifying label-dependent properties
|
||||||
@@ -181,7 +181,7 @@ When changing `issue_tracker/core/models.py`:
|
|||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
### Project Configuration (`pyproject.toml`)
|
### Project Configuration (`pyproject.toml`)
|
||||||
- Entry points: `issue` and `issue-tracker` commands
|
- Entry points: `issue` and `issue-core` commands
|
||||||
- Dependencies: click, requests, python-dateutil
|
- Dependencies: click, requests, python-dateutil
|
||||||
- Optional dependencies: dev, docs, gitea, github, jira
|
- Optional dependencies: dev, docs, gitea, github, jira
|
||||||
- Code style: Black (line-length=100), isort (profile="black")
|
- Code style: Black (line-length=100), isort (profile="black")
|
||||||
@@ -189,7 +189,7 @@ When changing `issue_tracker/core/models.py`:
|
|||||||
|
|
||||||
### Makefile Integration
|
### Makefile Integration
|
||||||
The capability integrates with the parent markitect project via `Makefile`:
|
The capability integrates with the parent markitect project via `Makefile`:
|
||||||
- Prefixed targets: `issue-facade-*` for development commands
|
- Prefixed targets: `issue-core-*` for development commands
|
||||||
- Unprefixed targets: `issue-*` for user-facing CLI operations
|
- Unprefixed targets: `issue-*` for user-facing CLI operations
|
||||||
- Uses `pip install -e` for editable installation
|
- Uses `pip install -e` for editable installation
|
||||||
|
|
||||||
@@ -239,7 +239,7 @@ When implementing sync:
|
|||||||
|
|
||||||
## Repository Context
|
## Repository Context
|
||||||
|
|
||||||
This is a capability within the larger markitect project (`/capabilities/issue-facade/`). The capability:
|
This is a capability within the larger markitect project (`/capabilities/issue-core/`). The capability:
|
||||||
- Can be installed independently via `pip install -e .`
|
- Can be installed independently via `pip install -e .`
|
||||||
- Integrates with parent project via Makefile targets
|
- Integrates with parent project via Makefile targets
|
||||||
- Follows markitect capability conventions for structure and naming
|
- Follows markitect capability conventions for structure and naming
|
||||||
@@ -268,7 +268,7 @@ feedback/
|
|||||||
|
|
||||||
### For Users: Submitting Feedback
|
### For Users: Submitting Feedback
|
||||||
|
|
||||||
Users of issue-facade (master projects integrating it) can submit feedback in multiple ways:
|
Users of issue-core (master projects integrating it) can submit feedback in multiple ways:
|
||||||
|
|
||||||
**Option 1: Using feedback CLI**
|
**Option 1: Using feedback CLI**
|
||||||
```bash
|
```bash
|
||||||
@@ -294,8 +294,8 @@ EOF
|
|||||||
**Option 3: From master project**
|
**Option 3: From master project**
|
||||||
```bash
|
```bash
|
||||||
cd my-master-project
|
cd my-master-project
|
||||||
echo "Feedback about issue-facade..." > feedback.md
|
echo "Feedback about issue-core..." > feedback.md
|
||||||
cp feedback.md capabilities/issue-facade/feedback/inbound/$(date +%Y%m%d)-feedback.md
|
cp feedback.md capabilities/issue-core/feedback/inbound/$(date +%Y%m%d)-feedback.md
|
||||||
```
|
```
|
||||||
|
|
||||||
### For Maintainers: Processing Feedback
|
### For Maintainers: Processing Feedback
|
||||||
|
|||||||
116
INTENT.md
Normal file
116
INTENT.md
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
# INTENT — issue-core
|
||||||
|
|
||||||
|
## Why it exists
|
||||||
|
|
||||||
|
The Coulomb org needs a **single, observable place where tasks land** — regardless
|
||||||
|
of whether they were created by a human typing a CLI command, by an automation
|
||||||
|
like activity-core acting on a rule, or by an agent acting on instructions.
|
||||||
|
|
||||||
|
Without a single landing zone, task creation fragments across:
|
||||||
|
- Per-repo Gitea issue trackers (siloed, no cross-repo view)
|
||||||
|
- Ad hoc files and TODO comments (invisible, unaudited)
|
||||||
|
- Agent-local memory and notebooks (lost when the agent ends)
|
||||||
|
- External SaaS trackers (rate-limited, off-network)
|
||||||
|
|
||||||
|
issue-core gives every actor — human or machine — one stable, observable place
|
||||||
|
to file work, and one stable surface to consume work from.
|
||||||
|
|
||||||
|
## What it is
|
||||||
|
|
||||||
|
A **task lifecycle manager** with a pluggable-backend architecture.
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
- **Ingestion**: accept new tasks via CLI, REST (`POST /issues/`), and — in the
|
||||||
|
future — NATS subscriptions.
|
||||||
|
- **Storage**: route each task to the configured backend (Gitea, SQLite, GitHub).
|
||||||
|
- **Lifecycle**: create → assign → update → close, with state transitions that
|
||||||
|
hold regardless of backend.
|
||||||
|
- **Querying**: list, search, filter across the active backend.
|
||||||
|
- **Synchronization**: bidirectional sync between local SQLite (source of truth
|
||||||
|
for offline work) and remote backends.
|
||||||
|
|
||||||
|
Backends today: **local SQLite**, **Gitea**. Planned: **GitHub**, **GitLab**, **JIRA**.
|
||||||
|
|
||||||
|
The CLI entry points are `issue` (primary) and `issue-core` (explicit alias).
|
||||||
|
|
||||||
|
## What it is NOT
|
||||||
|
|
||||||
|
issue-core is intentionally narrow. The following live elsewhere:
|
||||||
|
|
||||||
|
- **Not a project manager.** Phases, campaigns, milestones spanning multiple tasks,
|
||||||
|
dependency graphs across tasks, gantt-style scheduling — that is the domain of
|
||||||
|
`project-core` (planned). issue-core deals in individual tasks, not in plans
|
||||||
|
composed of tasks.
|
||||||
|
|
||||||
|
- **Not a spawn audit trail.** When activity-core fires a rule that creates a task,
|
||||||
|
the *spawn event* (who fired, what rule, what triggering event) is recorded in
|
||||||
|
activity-core's `task_spawn_log`. issue-core only stores the resulting task and
|
||||||
|
its `triggering_event_id` reference back. The audit-of-creation belongs to the
|
||||||
|
emitter.
|
||||||
|
|
||||||
|
- **Not an event bus.** Communication between services flows over NATS (and
|
||||||
|
state-hub progress events). issue-core consumes events, but does not relay them.
|
||||||
|
|
||||||
|
- **Not a notification system.** Surfacing "your task changed" to humans is the
|
||||||
|
job of the relevant UI / digest / chatbot layer, not issue-core.
|
||||||
|
|
||||||
|
- **Not a workflow engine.** State transitions are simple (open → closed, with
|
||||||
|
a few in-between states). Conditional routing, approvals, multi-step
|
||||||
|
workflows — out of scope.
|
||||||
|
|
||||||
|
## How it fits
|
||||||
|
|
||||||
|
```
|
||||||
|
+-------------------+
|
||||||
|
| activity-core |
|
||||||
|
| IssueSink (REST) |
|
||||||
|
+---------+---------+
|
||||||
|
|
|
||||||
|
POST /issues/ (TaskSpec payload)
|
||||||
|
|
|
||||||
|
v
|
||||||
|
+------------+ +-------+--------+ +-----------------+
|
||||||
|
| Humans +----->| |<-----+ Agents |
|
||||||
|
| CLI: | | issue-core | | (CLI or REST) |
|
||||||
|
| $ issue | | | | |
|
||||||
|
+------------+ +-------+--------+ +-----------------+
|
||||||
|
|
|
||||||
|
+---------+----------+
|
||||||
|
| Backend router |
|
||||||
|
+---+------+------+--+
|
||||||
|
| | |
|
||||||
|
v v v
|
||||||
|
+------+ +-----+ +------+
|
||||||
|
|Gitea | |SQLite| |GitHub|
|
||||||
|
+------+ +-----+ +------+
|
||||||
|
```
|
||||||
|
|
||||||
|
**Upstream of issue-core (emitters):**
|
||||||
|
- **activity-core** — emits tasks via `IssueSink` when a rule fires or an
|
||||||
|
instruction declares one. Payload: `TaskSpec` over `POST /issues/`.
|
||||||
|
- **Humans** — `$ issue create ...` from terminals; future web UI.
|
||||||
|
- **Agents** — same REST surface or CLI.
|
||||||
|
|
||||||
|
**Downstream of issue-core (consumers):**
|
||||||
|
- **Humans and agents** assigned tasks consume them via `$ issue list`, web UI,
|
||||||
|
or per-backend native UIs (Gitea web, GitHub PR view, etc.).
|
||||||
|
- **state-hub** receives progress events as tasks move through their lifecycle.
|
||||||
|
- **Status updates** flow back to issue-core, not to the original emitter —
|
||||||
|
activity-core does not track what happened to the task it spawned.
|
||||||
|
|
||||||
|
## Success looks like
|
||||||
|
|
||||||
|
- Every task in the Coulomb org is discoverable from one query surface.
|
||||||
|
- activity-core can fire a rule and have the resulting task land in the right
|
||||||
|
backend with the right metadata, with no human in the loop.
|
||||||
|
- The CLI experience is identical across SQLite-only laptops and full Gitea-
|
||||||
|
backed servers.
|
||||||
|
- Offline work syncs back cleanly when connectivity returns.
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- `SCOPE.md` — concrete in/out-of-scope decisions and integration boundaries.
|
||||||
|
- `ROADMAP.md` — feature trajectory.
|
||||||
|
- `workplans/` — active workstreams.
|
||||||
|
- activity-core `docs/adr/adr-001-event-bridge-architecture.md` — the IssueSink
|
||||||
|
contract that issue-core honors at `POST /issues/`.
|
||||||
92
Makefile
92
Makefile
@@ -1,14 +1,14 @@
|
|||||||
# Issue Facade Capability Makefile
|
# Issue Core Capability Makefile
|
||||||
# Universal CLI for issue tracking across multiple backends
|
# Universal CLI for issue tracking across multiple backends
|
||||||
|
|
||||||
# Capability metadata
|
# Capability metadata
|
||||||
CAPABILITY_NAME := issue-facade
|
CAPABILITY_NAME := issue-core
|
||||||
CAPABILITY_DESCRIPTION := Universal CLI for issue tracking across multiple backends
|
CAPABILITY_DESCRIPTION := Universal CLI for issue tracking across multiple backends
|
||||||
|
|
||||||
# Default target
|
# Default target
|
||||||
.PHONY: help
|
.PHONY: help
|
||||||
help: ## Show issue facade capability help
|
help: ## Show issue core capability help
|
||||||
@echo "🎯 Issue Facade - Universal Issue Tracking CLI"
|
@echo "🎯 Issue Core - Universal Issue Tracking CLI"
|
||||||
@echo "==============================================="
|
@echo "==============================================="
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Core Issue Operations:"
|
@echo "Core Issue Operations:"
|
||||||
@@ -30,7 +30,7 @@ help: ## Show issue facade capability help
|
|||||||
@echo " issue-sync-push Push local issues to remote"
|
@echo " issue-sync-push Push local issues to remote"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Development & Setup (local):"
|
@echo "Development & Setup (local):"
|
||||||
@echo " install Install issue facade for local development"
|
@echo " install Install issue core for local development"
|
||||||
@echo " install-dev Install with development dependencies"
|
@echo " install-dev Install with development dependencies"
|
||||||
@echo " test Run all tests"
|
@echo " test Run all tests"
|
||||||
@echo " test-unit Run unit tests only"
|
@echo " test-unit Run unit tests only"
|
||||||
@@ -39,23 +39,23 @@ help: ## Show issue facade capability help
|
|||||||
@echo " test-verbose Run tests with verbose output"
|
@echo " test-verbose Run tests with verbose output"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Feedback & Continuous Improvement:"
|
@echo "Feedback & Continuous Improvement:"
|
||||||
@echo " feedback MSG=\"...\" Submit feedback about issue-facade"
|
@echo " feedback MSG=\"...\" Submit feedback about issue-core"
|
||||||
@echo " feedback-list List pending feedback"
|
@echo " feedback-list List pending feedback"
|
||||||
@echo " feedback-stats Show feedback statistics"
|
@echo " feedback-stats Show feedback statistics"
|
||||||
@echo " feedback-show FILE=\"...\" Show specific feedback"
|
@echo " feedback-show FILE=\"...\" Show specific feedback"
|
||||||
@echo " feedback-review FILE=\"...\" Review feedback (maintainers)"
|
@echo " feedback-review FILE=\"...\" Review feedback (maintainers)"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Development & Setup (from parent):"
|
@echo "Development & Setup (from parent):"
|
||||||
@echo " issue-facade-install Install issue facade capability"
|
@echo " issue-core-install Install issue core capability"
|
||||||
@echo " issue-facade-install-dev Install with development dependencies"
|
@echo " issue-core-install-dev Install with development dependencies"
|
||||||
@echo " issue-facade-test Run issue facade tests"
|
@echo " issue-core-test Run issue core tests"
|
||||||
@echo " issue-facade-test-cov Run tests with coverage report"
|
@echo " issue-core-test-cov Run tests with coverage report"
|
||||||
@echo " issue-facade-lint Run code quality checks"
|
@echo " issue-core-lint Run code quality checks"
|
||||||
@echo " issue-facade-clean Clean build artifacts"
|
@echo " issue-core-clean Clean build artifacts"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "CLI Functionality:"
|
@echo "CLI Functionality:"
|
||||||
@echo " issue-facade-help Show CLI help documentation"
|
@echo " issue-core-help Show CLI help documentation"
|
||||||
@echo " issue-facade-demo Demonstrate facade functionality"
|
@echo " issue-core-demo Demonstrate facade functionality"
|
||||||
|
|
||||||
# Check if issue command is available
|
# Check if issue command is available
|
||||||
ISSUE_CLI := $(shell command -v issue 2> /dev/null)
|
ISSUE_CLI := $(shell command -v issue 2> /dev/null)
|
||||||
@@ -65,7 +65,7 @@ ISSUE_CLI := $(shell command -v issue 2> /dev/null)
|
|||||||
issue-list: ## List all issues from configured backend
|
issue-list: ## List all issues from configured backend
|
||||||
ifndef ISSUE_CLI
|
ifndef ISSUE_CLI
|
||||||
@echo "❌ Issue facade not installed"
|
@echo "❌ Issue facade not installed"
|
||||||
@echo " Install with: make issue-facade-install"
|
@echo " Install with: make issue-core-install"
|
||||||
@exit 1
|
@exit 1
|
||||||
endif
|
endif
|
||||||
issue list
|
issue list
|
||||||
@@ -182,7 +182,7 @@ integrate: ## Integrate capability into main project (interactive)
|
|||||||
@./.capability/integrate.sh
|
@./.capability/integrate.sh
|
||||||
|
|
||||||
.PHONY: install
|
.PHONY: install
|
||||||
install: ## Install issue facade (local development)
|
install: ## Install issue core (local development)
|
||||||
pip install -e .
|
pip install -e .
|
||||||
|
|
||||||
.PHONY: install-dev
|
.PHONY: install-dev
|
||||||
@@ -203,7 +203,7 @@ test-integration: ## Run integration tests only (local development)
|
|||||||
|
|
||||||
.PHONY: test-cov
|
.PHONY: test-cov
|
||||||
test-cov: ## Run tests with coverage report (local development)
|
test-cov: ## Run tests with coverage report (local development)
|
||||||
pytest tests/ --cov=issue_tracker --cov-report=html --cov-report=term
|
pytest tests/ --cov=issue_core --cov-report=html --cov-report=term
|
||||||
|
|
||||||
.PHONY: test-verbose
|
.PHONY: test-verbose
|
||||||
test-verbose: ## Run tests with verbose output (local development)
|
test-verbose: ## Run tests with verbose output (local development)
|
||||||
@@ -234,50 +234,50 @@ feedback-review: ## Review feedback (Usage: make feedback-review FILE=20251217-x
|
|||||||
feedback-review-issue: ## Review feedback and create issue (Usage: make feedback-review-issue FILE=20251217-xxx.md)
|
feedback-review-issue: ## Review feedback and create issue (Usage: make feedback-review-issue FILE=20251217-xxx.md)
|
||||||
@./.capability/feedback review "$(FILE)" --create-issue
|
@./.capability/feedback review "$(FILE)" --create-issue
|
||||||
|
|
||||||
.PHONY: issue-facade-install
|
.PHONY: issue-core-install
|
||||||
issue-facade-install: ## Install issue facade capability
|
issue-core-install: ## Install issue core capability
|
||||||
pip install -e capabilities/issue-facade/
|
pip install -e capabilities/issue-core/
|
||||||
|
|
||||||
.PHONY: issue-facade-install-dev
|
.PHONY: issue-core-install-dev
|
||||||
issue-facade-install-dev: ## Install issue facade capability with development dependencies
|
issue-core-install-dev: ## Install issue core capability with development dependencies
|
||||||
pip install -e "capabilities/issue-facade/[dev]"
|
pip install -e "capabilities/issue-core/[dev]"
|
||||||
|
|
||||||
.PHONY: issue-facade-test
|
.PHONY: issue-core-test
|
||||||
issue-facade-test: ## Run issue facade tests
|
issue-core-test: ## Run issue core tests
|
||||||
cd capabilities/issue-facade && pytest tests/
|
cd capabilities/issue-core && pytest tests/
|
||||||
|
|
||||||
.PHONY: issue-facade-test-cov
|
.PHONY: issue-core-test-cov
|
||||||
issue-facade-test-cov: ## Run tests with coverage report
|
issue-core-test-cov: ## Run tests with coverage report
|
||||||
cd capabilities/issue-facade && pytest tests/ --cov=issue_tracker --cov-report=html --cov-report=term
|
cd capabilities/issue-core && pytest tests/ --cov=issue_core --cov-report=html --cov-report=term
|
||||||
|
|
||||||
.PHONY: issue-facade-lint
|
.PHONY: issue-core-lint
|
||||||
issue-facade-lint: ## Run code quality checks
|
issue-core-lint: ## Run code quality checks
|
||||||
@echo "🔍 Running code quality checks for issue-facade..."
|
@echo "🔍 Running code quality checks for issue-core..."
|
||||||
cd capabilities/issue-facade && python -m py_compile cli/*.py core/*.py backends/*/*.py 2>/dev/null || echo "⚠️ Some files may not compile due to missing dependencies"
|
cd capabilities/issue-core && python -m py_compile cli/*.py core/*.py backends/*/*.py 2>/dev/null || echo "⚠️ Some files may not compile due to missing dependencies"
|
||||||
@echo "✅ Code quality checks completed"
|
@echo "✅ Code quality checks completed"
|
||||||
|
|
||||||
.PHONY: issue-facade-clean
|
.PHONY: issue-core-clean
|
||||||
issue-facade-clean: ## Clean build artifacts
|
issue-core-clean: ## Clean build artifacts
|
||||||
cd capabilities/issue-facade && rm -rf build/ dist/ *.egg-info/ __pycache__/ .pytest_cache/ htmlcov/ .coverage
|
cd capabilities/issue-core && rm -rf build/ dist/ *.egg-info/ __pycache__/ .pytest_cache/ htmlcov/ .coverage
|
||||||
find capabilities/issue-facade -name "*.pyc" -delete
|
find capabilities/issue-core -name "*.pyc" -delete
|
||||||
find capabilities/issue-facade -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
|
find capabilities/issue-core -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
|
||||||
# CLI Functionality
|
# CLI Functionality
|
||||||
.PHONY: issue-facade-help
|
.PHONY: issue-core-help
|
||||||
issue-facade-help: ## Show CLI help documentation
|
issue-core-help: ## Show CLI help documentation
|
||||||
ifndef ISSUE_CLI
|
ifndef ISSUE_CLI
|
||||||
@echo "❌ Issue facade not installed"
|
@echo "❌ Issue facade not installed"
|
||||||
@echo " Install with: make issue-facade-install"
|
@echo " Install with: make issue-core-install"
|
||||||
@exit 1
|
@exit 1
|
||||||
endif
|
endif
|
||||||
issue --help
|
issue --help
|
||||||
|
|
||||||
.PHONY: issue-facade-demo
|
.PHONY: issue-core-demo
|
||||||
issue-facade-demo: ## Demonstrate facade functionality
|
issue-core-demo: ## Demonstrate facade functionality
|
||||||
@echo "🎬 Issue Facade Demonstration"
|
@echo "🎬 Issue Core Demonstration"
|
||||||
@echo "============================="
|
@echo "============================="
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "The Issue Facade provides a unified CLI for issue tracking across:"
|
@echo "The Issue Core provides a unified CLI for issue tracking across:"
|
||||||
@echo " • GitHub Issues"
|
@echo " • GitHub Issues"
|
||||||
@echo " • GitLab Issues"
|
@echo " • GitLab Issues"
|
||||||
@echo " • Gitea Issues"
|
@echo " • Gitea Issues"
|
||||||
@@ -290,7 +290,7 @@ issue-facade-demo: ## Demonstrate facade functionality
|
|||||||
@echo ""
|
@echo ""
|
||||||
ifndef ISSUE_CLI
|
ifndef ISSUE_CLI
|
||||||
@echo "To try it out:"
|
@echo "To try it out:"
|
||||||
@echo " 1. make issue-facade-install"
|
@echo " 1. make issue-core-install"
|
||||||
@echo " 2. make issue-backend-detect"
|
@echo " 2. make issue-backend-detect"
|
||||||
@echo " 3. make issue-list"
|
@echo " 3. make issue-list"
|
||||||
else
|
else
|
||||||
|
|||||||
43
README.md
43
README.md
@@ -1,10 +1,10 @@
|
|||||||
# Issue Facade - Agent Coordination via Issue Tracking
|
# Issue Core - Agent Coordination via Issue Tracking
|
||||||
|
|
||||||
**A unified interface for autonomous coding agents to coordinate project implementation through issue tracking systems.**
|
**A unified interface for autonomous coding agents to coordinate project implementation through issue tracking systems.**
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
The **Issue Facade** provides a standardized abstraction layer for coding agents to interact with issue tracking backends (Gitea, GitHub, GitLab, local SQLite). Instead of each agent implementing platform-specific API integrations, they use one consistent interface that works across all backends.
|
The **Issue Core** provides a standardized abstraction layer for coding agents to interact with issue tracking backends (Gitea, GitHub, GitLab, local SQLite). Instead of each agent implementing platform-specific API integrations, they use one consistent interface that works across all backends.
|
||||||
|
|
||||||
### Why Issue Tracking for Agent Coordination?
|
### Why Issue Tracking for Agent Coordination?
|
||||||
|
|
||||||
@@ -40,10 +40,25 @@ Issue tracking provides natural coordination primitives for multi-agent software
|
|||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd capabilities/issue-facade
|
cd capabilities/issue-core
|
||||||
pip install -e .
|
pip install -e . # CLI only
|
||||||
|
pip install -e ".[api]" # CLI + REST ingestion server
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### REST Ingestion Server
|
||||||
|
|
||||||
|
issue-core exposes `POST /issues/` for upstream emitters (primarily
|
||||||
|
activity-core's `IssueSink`). Launch with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export ISSUE_CORE_API_KEY="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')"
|
||||||
|
issue serve --host 0.0.0.0 --port 8765
|
||||||
|
```
|
||||||
|
|
||||||
|
Clients authenticate with `Authorization: Bearer <key>` or `X-API-Key: <key>`.
|
||||||
|
See `SCOPE.md` "TaskSpec payload" for the request schema, or visit
|
||||||
|
`http://<host>:<port>/docs` once the server is running for live OpenAPI docs.
|
||||||
|
|
||||||
### Configuration (One-Time Setup)
|
### Configuration (One-Time Setup)
|
||||||
|
|
||||||
**For Gitea-backed projects:**
|
**For Gitea-backed projects:**
|
||||||
@@ -67,7 +82,7 @@ issue backend test myproject
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
issue backend add local-work local
|
issue backend add local-work local
|
||||||
# Prompts for: database path (.issue-facade/issues.db)
|
# Prompts for: database path (.issue-core/issues.db)
|
||||||
|
|
||||||
issue backend set-default local-work
|
issue backend set-default local-work
|
||||||
```
|
```
|
||||||
@@ -108,8 +123,8 @@ issue close 42 --comment="Ready for review"
|
|||||||
Quick example:
|
Quick example:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
from issue_tracker.core.interfaces import IssueFilter
|
from issue_core.core.interfaces import IssueFilter
|
||||||
|
|
||||||
# Initialize
|
# Initialize
|
||||||
backend = GiteaBackend()
|
backend = GiteaBackend()
|
||||||
@@ -281,20 +296,20 @@ make test-unit
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run linter
|
# Run linter
|
||||||
make issue-facade-lint
|
make issue-core-lint
|
||||||
|
|
||||||
# Format code
|
# Format code
|
||||||
black issue_tracker/ tests/
|
black issue_core/ tests/
|
||||||
|
|
||||||
# Type check
|
# Type check
|
||||||
mypy issue_tracker/
|
mypy issue_core/
|
||||||
```
|
```
|
||||||
|
|
||||||
### Project Structure
|
### Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
issue-facade/
|
issue-core/
|
||||||
├── issue_tracker/
|
├── issue_core/
|
||||||
│ ├── core/ # Domain models and interfaces
|
│ ├── core/ # Domain models and interfaces
|
||||||
│ │ ├── models.py # Issue, Label, User, etc.
|
│ │ ├── models.py # Issue, Label, User, etc.
|
||||||
│ │ └── interfaces.py # IssueBackend, SyncableBackend
|
│ │ └── interfaces.py # IssueBackend, SyncableBackend
|
||||||
@@ -351,7 +366,7 @@ issue-facade/
|
|||||||
|
|
||||||
## Comparison with Platform CLIs
|
## Comparison with Platform CLIs
|
||||||
|
|
||||||
| Feature | Issue Facade | gh (GitHub) | glab (GitLab) |
|
| Feature | Issue Core | gh (GitHub) | glab (GitLab) |
|
||||||
|---------|--------------|-------------|---------------|
|
|---------|--------------|-------------|---------------|
|
||||||
| Multi-backend support | ✅ Yes | ❌ GitHub only | ❌ GitLab only |
|
| Multi-backend support | ✅ Yes | ❌ GitHub only | ❌ GitLab only |
|
||||||
| Offline capability | ✅ Local SQLite | ❌ No | ❌ No |
|
| Offline capability | ✅ Local SQLite | ❌ No | ❌ No |
|
||||||
@@ -362,7 +377,7 @@ issue-facade/
|
|||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
The Issue Facade is designed to be extensible:
|
The Issue Core is designed to be extensible:
|
||||||
|
|
||||||
**To add a new backend:**
|
**To add a new backend:**
|
||||||
1. Implement the `IssueBackend` interface (see `core/interfaces.py`)
|
1. Implement the `IssueBackend` interface (see `core/interfaces.py`)
|
||||||
|
|||||||
36
ROADMAP.md
36
ROADMAP.md
@@ -1,4 +1,4 @@
|
|||||||
# Issue Facade Roadmap
|
# Issue Core Roadmap
|
||||||
|
|
||||||
**Long-term vision and implementation plan for agent-driven software development coordination.**
|
**Long-term vision and implementation plan for agent-driven software development coordination.**
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
**Implementation:**
|
**Implementation:**
|
||||||
```python
|
```python
|
||||||
# issue_tracker/core/detection.py
|
# issue_core/core/detection.py
|
||||||
|
|
||||||
def detect_git_remote() -> Optional[Dict[str, str]]:
|
def detect_git_remote() -> Optional[Dict[str, str]]:
|
||||||
"""
|
"""
|
||||||
@@ -64,7 +64,7 @@ def parse_remote_url(url: str) -> Optional[Dict[str, str]]:
|
|||||||
|
|
||||||
**Implementation:**
|
**Implementation:**
|
||||||
```python
|
```python
|
||||||
# issue_tracker/core/env_config.py
|
# issue_core/core/env_config.py
|
||||||
|
|
||||||
def load_backend_from_env() -> Optional[Dict[str, Any]]:
|
def load_backend_from_env() -> Optional[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
@@ -95,7 +95,7 @@ issue config auto
|
|||||||
|
|
||||||
**Implementation:**
|
**Implementation:**
|
||||||
```
|
```
|
||||||
.issue-facade/
|
.issue-core/
|
||||||
├── config.json # Repository-specific settings
|
├── config.json # Repository-specific settings
|
||||||
├── issues.db # Local cache/backup
|
├── issues.db # Local cache/backup
|
||||||
└── credentials.json # Optional encrypted credentials
|
└── credentials.json # Optional encrypted credentials
|
||||||
@@ -126,10 +126,10 @@ issue config auto
|
|||||||
**Functions:**
|
**Functions:**
|
||||||
```python
|
```python
|
||||||
def load_repo_config(path: Path = Path.cwd()) -> Optional[Dict]:
|
def load_repo_config(path: Path = Path.cwd()) -> Optional[Dict]:
|
||||||
"""Load .issue-facade/config.json from repo root."""
|
"""Load .issue-core/config.json from repo root."""
|
||||||
|
|
||||||
def save_repo_config(config: Dict, path: Path = Path.cwd()):
|
def save_repo_config(config: Dict, path: Path = Path.cwd()):
|
||||||
"""Save config to .issue-facade/config.json."""
|
"""Save config to .issue-core/config.json."""
|
||||||
|
|
||||||
def find_repo_root() -> Optional[Path]:
|
def find_repo_root() -> Optional[Path]:
|
||||||
"""Walk up directory tree to find git root."""
|
"""Walk up directory tree to find git root."""
|
||||||
@@ -141,14 +141,14 @@ def find_repo_root() -> Optional[Path]:
|
|||||||
|
|
||||||
**Implementation:**
|
**Implementation:**
|
||||||
```python
|
```python
|
||||||
# issue_tracker/core/auto_config.py
|
# issue_core/core/auto_config.py
|
||||||
|
|
||||||
def auto_configure_backend() -> IssueBackend:
|
def auto_configure_backend() -> IssueBackend:
|
||||||
"""
|
"""
|
||||||
Auto-configure backend with fallback priority:
|
Auto-configure backend with fallback priority:
|
||||||
1. Check .issue-facade/config.json
|
1. Check .issue-core/config.json
|
||||||
2. Detect from git remote + environment token
|
2. Detect from git remote + environment token
|
||||||
3. Check global config (~/.config/issue-facade/)
|
3. Check global config (~/.config/issue-core/)
|
||||||
4. Prompt user for manual configuration
|
4. Prompt user for manual configuration
|
||||||
"""
|
"""
|
||||||
```
|
```
|
||||||
@@ -196,7 +196,7 @@ if issue backend show "$backend_name" &>/dev/null; then
|
|||||||
if [ "$replace" = "y" ] || [ "$replace" = "Y" ]; then
|
if [ "$replace" = "y" ] || [ "$replace" = "Y" ]; then
|
||||||
# Create timestamped backup
|
# Create timestamped backup
|
||||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||||
CONFIG_FILE="$HOME/.config/issue-facade/backends.json"
|
CONFIG_FILE="$HOME/.config/issue-core/backends.json"
|
||||||
if [ -f "$CONFIG_FILE" ]; then
|
if [ -f "$CONFIG_FILE" ]; then
|
||||||
BACKUP_FILE="$CONFIG_FILE.backup.$TIMESTAMP"
|
BACKUP_FILE="$CONFIG_FILE.backup.$TIMESTAMP"
|
||||||
cp "$CONFIG_FILE" "$BACKUP_FILE"
|
cp "$CONFIG_FILE" "$BACKUP_FILE"
|
||||||
@@ -255,7 +255,7 @@ fi
|
|||||||
|
|
||||||
**Implementation:**
|
**Implementation:**
|
||||||
```python
|
```python
|
||||||
# issue_tracker/core/agent.py
|
# issue_core/core/agent.py
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AgentContext:
|
class AgentContext:
|
||||||
@@ -269,7 +269,7 @@ def get_agent_context() -> AgentContext:
|
|||||||
"""
|
"""
|
||||||
Get agent context from:
|
Get agent context from:
|
||||||
1. Environment (ISSUE_AGENT_ID, ISSUE_AGENT_TYPE)
|
1. Environment (ISSUE_AGENT_ID, ISSUE_AGENT_TYPE)
|
||||||
2. Config file (.issue-facade/config.json)
|
2. Config file (.issue-core/config.json)
|
||||||
3. Default to system username
|
3. Default to system username
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -310,7 +310,7 @@ issue config agent show
|
|||||||
|
|
||||||
**Implementation:**
|
**Implementation:**
|
||||||
```python
|
```python
|
||||||
# issue_tracker/core/locking.py
|
# issue_core/core/locking.py
|
||||||
|
|
||||||
class IssueClaim:
|
class IssueClaim:
|
||||||
issue_id: str
|
issue_id: str
|
||||||
@@ -423,7 +423,7 @@ def get_agent_state(issue: Issue) -> Dict[str, Any]:
|
|||||||
|
|
||||||
**Implementation:**
|
**Implementation:**
|
||||||
```python
|
```python
|
||||||
# issue_tracker/core/webhooks.py
|
# issue_core/core/webhooks.py
|
||||||
|
|
||||||
class WebhookManager:
|
class WebhookManager:
|
||||||
"""Manage webhooks for real-time notifications."""
|
"""Manage webhooks for real-time notifications."""
|
||||||
@@ -525,7 +525,7 @@ issue depends ready # List issues ready to start
|
|||||||
|
|
||||||
**Implementation:**
|
**Implementation:**
|
||||||
```python
|
```python
|
||||||
# issue_tracker/core/query_dsl.py
|
# issue_core/core/query_dsl.py
|
||||||
|
|
||||||
class QueryParser:
|
class QueryParser:
|
||||||
"""
|
"""
|
||||||
@@ -559,7 +559,7 @@ issue list --query="is:in-progress created:>7d"
|
|||||||
|
|
||||||
**Implementation:**
|
**Implementation:**
|
||||||
```python
|
```python
|
||||||
# issue_tracker/core/activity.py
|
# issue_core/core/activity.py
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ActivityEvent:
|
class ActivityEvent:
|
||||||
@@ -596,7 +596,7 @@ class ActivityStream:
|
|||||||
|
|
||||||
**Implementation:**
|
**Implementation:**
|
||||||
```python
|
```python
|
||||||
# issue_tracker/core/distributed_lock.py
|
# issue_core/core/distributed_lock.py
|
||||||
|
|
||||||
class DistributedLockManager:
|
class DistributedLockManager:
|
||||||
"""Distributed locking using Redis/database."""
|
"""Distributed locking using Redis/database."""
|
||||||
@@ -638,7 +638,7 @@ with distributed_lock(f"issue:{issue_id}", agent_id):
|
|||||||
|
|
||||||
**Implementation:**
|
**Implementation:**
|
||||||
```python
|
```python
|
||||||
# issue_tracker/core/sync_strategies.py
|
# issue_core/core/sync_strategies.py
|
||||||
|
|
||||||
class ConflictResolutionStrategy(ABC):
|
class ConflictResolutionStrategy(ABC):
|
||||||
def resolve(self, local: Issue, remote: Issue) -> Issue:
|
def resolve(self, local: Issue, remote: Issue) -> Issue:
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ A **Capability Family** is an abstract, conceptual grouping of related functiona
|
|||||||
A **Capability Implementation** is a concrete realization of a Capability Family - the actual code, tools, and patterns that provide the functionality.
|
A **Capability Implementation** is a concrete realization of a Capability Family - the actual code, tools, and patterns that provide the functionality.
|
||||||
|
|
||||||
**Examples:**
|
**Examples:**
|
||||||
- `issue-facade` - An implementation of the `issue-tracking` family
|
- `issue-core` - An implementation of the `issue-tracking` family
|
||||||
- `feedback-tool` - An implementation of the `feedback-collection` family
|
- `feedback-tool` - An implementation of the `feedback-collection` family
|
||||||
|
|
||||||
**Characteristics:**
|
**Characteristics:**
|
||||||
@@ -94,7 +94,7 @@ CAPABILITY-authentication.yaml # Declares authentication capability
|
|||||||
### Single Capability Repository Structure
|
### Single Capability Repository Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
issue-facade/ # Repository name (implementation)
|
issue-core/ # Repository name (implementation)
|
||||||
├── CAPABILITY-issue-tracking.yaml # Declares: provides "issue-tracking"
|
├── CAPABILITY-issue-tracking.yaml # Declares: provides "issue-tracking"
|
||||||
├── README.md # Human-readable documentation
|
├── README.md # Human-readable documentation
|
||||||
├── feedback/ # Visible: user interface for feedback
|
├── feedback/ # Visible: user interface for feedback
|
||||||
@@ -105,7 +105,7 @@ issue-facade/ # Repository name (implementation)
|
|||||||
│ ├── feedback # Feedback CLI tool
|
│ ├── feedback # Feedback CLI tool
|
||||||
│ ├── integrate.sh # Integration script
|
│ ├── integrate.sh # Integration script
|
||||||
│ └── ...
|
│ └── ...
|
||||||
├── issue_tracker/ # Core implementation code
|
├── issue_core/ # Core implementation code
|
||||||
│ ├── core/
|
│ ├── core/
|
||||||
│ ├── backends/
|
│ ├── backends/
|
||||||
│ └── cli/
|
│ └── cli/
|
||||||
@@ -141,7 +141,7 @@ unified-devtools/ # Repository providing multiple capab
|
|||||||
│ ├── feedback-collection/
|
│ ├── feedback-collection/
|
||||||
│ └── common/
|
│ └── common/
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── issue_tracker/
|
│ ├── issue_core/
|
||||||
│ ├── feedback_tool/
|
│ ├── feedback_tool/
|
||||||
│ └── doc_generator/
|
│ └── doc_generator/
|
||||||
└── tests/
|
└── tests/
|
||||||
@@ -162,8 +162,8 @@ Traditional approaches create deep directory trees:
|
|||||||
```
|
```
|
||||||
my-project/
|
my-project/
|
||||||
└── capabilities/
|
└── capabilities/
|
||||||
└── issue-facade/
|
└── issue-core/
|
||||||
└── issue_tracker/
|
└── issue_core/
|
||||||
└── backends/
|
└── backends/
|
||||||
└── gitea/
|
└── gitea/
|
||||||
└── backend.py # 6 levels deep!
|
└── backend.py # 6 levels deep!
|
||||||
@@ -176,8 +176,8 @@ Use **underscore-prefixed directories** at the repository root to signal "integr
|
|||||||
**Option A: Implementation-Based (Recommended)**
|
**Option A: Implementation-Based (Recommended)**
|
||||||
```
|
```
|
||||||
my-project/
|
my-project/
|
||||||
├── _issue-facade/ # Integrated capability (flat!)
|
├── _issue-core/ # Integrated capability (flat!)
|
||||||
│ └── issue_tracker/ # Only 2-3 levels deep
|
│ └── issue_core/ # Only 2-3 levels deep
|
||||||
│ └── backends/
|
│ └── backends/
|
||||||
├── _feedback-tool/ # Another integrated capability
|
├── _feedback-tool/ # Another integrated capability
|
||||||
├── src/ # Core project code
|
├── src/ # Core project code
|
||||||
@@ -190,7 +190,7 @@ my-project/
|
|||||||
```
|
```
|
||||||
my-project/
|
my-project/
|
||||||
├── _issue-tracking/ # Capability family
|
├── _issue-tracking/ # Capability family
|
||||||
│ └── issue-facade/ # Implementation (if multiple needed)
|
│ └── issue-core/ # Implementation (if multiple needed)
|
||||||
├── _feedback-collection/
|
├── _feedback-collection/
|
||||||
│ └── feedback-tool/
|
│ └── feedback-tool/
|
||||||
└── src/
|
└── src/
|
||||||
@@ -200,7 +200,7 @@ my-project/
|
|||||||
```
|
```
|
||||||
my-project/
|
my-project/
|
||||||
├── c/ # "c" for capabilities
|
├── c/ # "c" for capabilities
|
||||||
│ ├── issue-facade/
|
│ ├── issue-core/
|
||||||
│ └── feedback-tool/
|
│ └── feedback-tool/
|
||||||
└── src/
|
└── src/
|
||||||
```
|
```
|
||||||
@@ -208,7 +208,7 @@ my-project/
|
|||||||
**Recommendation: Option A (Implementation-Based)**
|
**Recommendation: Option A (Implementation-Based)**
|
||||||
|
|
||||||
**Rationale:**
|
**Rationale:**
|
||||||
- **Flatter hierarchy** - `_issue-facade/issue_tracker/...` (3 levels vs 6)
|
- **Flatter hierarchy** - `_issue-core/issue_core/...` (3 levels vs 6)
|
||||||
- **Clear signal** - underscore means "integrated, not core"
|
- **Clear signal** - underscore means "integrated, not core"
|
||||||
- **Discoverable** - `ls _*/` shows all integrated capabilities
|
- **Discoverable** - `ls _*/` shows all integrated capabilities
|
||||||
- **No ambiguity** - implementation name is explicit
|
- **No ambiguity** - implementation name is explicit
|
||||||
@@ -217,7 +217,7 @@ my-project/
|
|||||||
**Example:**
|
**Example:**
|
||||||
```
|
```
|
||||||
my-project/
|
my-project/
|
||||||
├── _issue-facade/ # Issue tracking via issue-facade
|
├── _issue-core/ # Issue tracking via issue-core
|
||||||
├── _auth-service/ # Authentication via auth-service
|
├── _auth-service/ # Authentication via auth-service
|
||||||
├── _postgres-tools/ # Database tools
|
├── _postgres-tools/ # Database tools
|
||||||
├── src/ # Core project code
|
├── src/ # Core project code
|
||||||
@@ -236,11 +236,11 @@ my-project/
|
|||||||
ls -d _*/
|
ls -d _*/
|
||||||
|
|
||||||
# Read capability specs
|
# Read capability specs
|
||||||
cat _issue-facade/CAPABILITY-*.yaml
|
cat _issue-core/CAPABILITY-*.yaml
|
||||||
cat _auth-service/CAPABILITY-*.yaml
|
cat _auth-service/CAPABILITY-*.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
**Important:** The underscore prefix is **local convention** for integrated capabilities, not part of the upstream git repository name. When integrating `github.com/markitect/issue-facade`, it becomes `_issue-facade/` in your project.
|
**Important:** The underscore prefix is **local convention** for integrated capabilities, not part of the upstream git repository name. When integrating `github.com/markitect/issue-core`, it becomes `_issue-core/` in your project.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -252,7 +252,7 @@ cat _auth-service/CAPABILITY-*.yaml
|
|||||||
# CAPABILITY-issue-tracking.yaml
|
# CAPABILITY-issue-tracking.yaml
|
||||||
metadata:
|
metadata:
|
||||||
family: issue-tracking
|
family: issue-tracking
|
||||||
implementation: issue-facade
|
implementation: issue-core
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
description: >
|
description: >
|
||||||
Unified interface for issue tracking across Gitea, GitHub, GitLab.
|
Unified interface for issue tracking across Gitea, GitHub, GitLab.
|
||||||
@@ -276,7 +276,7 @@ integration:
|
|||||||
mcp_server: false # planned
|
mcp_server: false # planned
|
||||||
|
|
||||||
installation:
|
installation:
|
||||||
command: "pip install -e _issue-facade/"
|
command: "pip install -e _issue-core/"
|
||||||
verify: "issue --version"
|
verify: "issue --version"
|
||||||
|
|
||||||
documentation:
|
documentation:
|
||||||
@@ -295,7 +295,7 @@ feedback:
|
|||||||
# CAPABILITY-issue-tracking.yaml
|
# CAPABILITY-issue-tracking.yaml
|
||||||
metadata:
|
metadata:
|
||||||
family: issue-tracking
|
family: issue-tracking
|
||||||
implementation: issue-facade
|
implementation: issue-core
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
maturity: production # experimental, beta, production
|
maturity: production # experimental, beta, production
|
||||||
|
|
||||||
@@ -338,7 +338,7 @@ integration:
|
|||||||
methods:
|
methods:
|
||||||
python_api:
|
python_api:
|
||||||
available: true
|
available: true
|
||||||
import: "from issue_tracker.backends.gitea import GiteaBackend"
|
import: "from issue_core.backends.gitea import GiteaBackend"
|
||||||
docs: "AGENT_INTEGRATION.md"
|
docs: "AGENT_INTEGRATION.md"
|
||||||
|
|
||||||
cli:
|
cli:
|
||||||
@@ -354,7 +354,7 @@ integration:
|
|||||||
|
|
||||||
installation:
|
installation:
|
||||||
method: pip
|
method: pip
|
||||||
command: "pip install -e _issue-facade/"
|
command: "pip install -e _issue-core/"
|
||||||
verify: "issue --version"
|
verify: "issue --version"
|
||||||
|
|
||||||
configuration:
|
configuration:
|
||||||
@@ -421,8 +421,8 @@ credentials:
|
|||||||
|
|
||||||
security:
|
security:
|
||||||
- "Tokens never in code or logs"
|
- "Tokens never in code or logs"
|
||||||
- "Config stored in ~/.config/issue-facade/"
|
- "Config stored in ~/.config/issue-core/"
|
||||||
- "Per-repo config in .issue-facade/ (gitignored)"
|
- "Per-repo config in .issue-core/ (gitignored)"
|
||||||
|
|
||||||
# Documentation references
|
# Documentation references
|
||||||
documentation:
|
documentation:
|
||||||
@@ -504,13 +504,13 @@ cat feedback/README.md
|
|||||||
**Integrate a capability:**
|
**Integrate a capability:**
|
||||||
```bash
|
```bash
|
||||||
# Clone/copy into underscore-prefixed directory
|
# Clone/copy into underscore-prefixed directory
|
||||||
git clone https://github.com/markitect/issue-facade _issue-facade
|
git clone https://github.com/markitect/issue-core _issue-core
|
||||||
|
|
||||||
# Or use git submodule
|
# Or use git submodule
|
||||||
git submodule add https://github.com/markitect/issue-facade _issue-facade
|
git submodule add https://github.com/markitect/issue-core _issue-core
|
||||||
|
|
||||||
# Install
|
# Install
|
||||||
pip install -e _issue-facade/
|
pip install -e _issue-core/
|
||||||
|
|
||||||
# Verify
|
# Verify
|
||||||
issue --version
|
issue --version
|
||||||
@@ -546,18 +546,18 @@ def find_capability_family(repo_path, family_name):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
capabilities = discover_capabilities("_issue-facade")
|
capabilities = discover_capabilities("_issue-core")
|
||||||
# Returns: [{'metadata': {'family': 'issue-tracking', ...}}]
|
# Returns: [{'metadata': {'family': 'issue-tracking', ...}}]
|
||||||
|
|
||||||
issue_cap = find_capability_family("_issue-facade", "issue-tracking")
|
issue_cap = find_capability_family("_issue-core", "issue-tracking")
|
||||||
print(f"Found: {issue_cap['metadata']['implementation']}")
|
print(f"Found: {issue_cap['metadata']['implementation']}")
|
||||||
# Output: Found: issue-facade
|
# Output: Found: issue-core
|
||||||
```
|
```
|
||||||
|
|
||||||
**Use capability via natural language understanding:**
|
**Use capability via natural language understanding:**
|
||||||
```python
|
```python
|
||||||
# Agent reads capability spec
|
# Agent reads capability spec
|
||||||
spec = find_capability_family("_issue-facade", "issue-tracking")
|
spec = find_capability_family("_issue-core", "issue-tracking")
|
||||||
|
|
||||||
# Agent understands use-cases
|
# Agent understands use-cases
|
||||||
use_cases = spec['purpose']['use_cases']
|
use_cases = spec['purpose']['use_cases']
|
||||||
@@ -566,7 +566,7 @@ use_cases = spec['purpose']['use_cases']
|
|||||||
# Agent discovers integration methods
|
# Agent discovers integration methods
|
||||||
if spec['integration']['methods']['python_api']['available']:
|
if spec['integration']['methods']['python_api']['available']:
|
||||||
import_statement = spec['integration']['methods']['python_api']['import']
|
import_statement = spec['integration']['methods']['python_api']['import']
|
||||||
# "from issue_tracker.backends.gitea import GiteaBackend"
|
# "from issue_core.backends.gitea import GiteaBackend"
|
||||||
|
|
||||||
# Agent can now integrate programmatically
|
# Agent can now integrate programmatically
|
||||||
exec(import_statement)
|
exec(import_statement)
|
||||||
@@ -616,7 +616,7 @@ metadata:
|
|||||||
# This capability integrates other capabilities
|
# This capability integrates other capabilities
|
||||||
integrates:
|
integrates:
|
||||||
- family: issue-tracking
|
- family: issue-tracking
|
||||||
implementation: issue-facade
|
implementation: issue-core
|
||||||
version: ">=1.0.0"
|
version: ">=1.0.0"
|
||||||
reason: "Uses issue tracking for task management"
|
reason: "Uses issue tracking for task management"
|
||||||
|
|
||||||
@@ -635,12 +635,12 @@ integrates:
|
|||||||
```
|
```
|
||||||
pm-tool/
|
pm-tool/
|
||||||
├── CAPABILITY-project-management.yaml
|
├── CAPABILITY-project-management.yaml
|
||||||
├── _issue-facade/ # Integrated capability
|
├── _issue-core/ # Integrated capability
|
||||||
├── _feedback-tool/ # Integrated capability
|
├── _feedback-tool/ # Integrated capability
|
||||||
├── _doc-gen/ # Integrated capability
|
├── _doc-gen/ # Integrated capability
|
||||||
├── src/
|
├── src/
|
||||||
│ └── pm/
|
│ └── pm/
|
||||||
│ ├── tasks.py # Uses issue-facade
|
│ ├── tasks.py # Uses issue-core
|
||||||
│ ├── feedback.py # Uses feedback-tool
|
│ ├── feedback.py # Uses feedback-tool
|
||||||
│ └── docs.py # Uses doc-gen
|
│ └── docs.py # Uses doc-gen
|
||||||
└── README.md
|
└── README.md
|
||||||
@@ -660,7 +660,7 @@ integrates:
|
|||||||
**Multiple valid integrations:**
|
**Multiple valid integrations:**
|
||||||
```
|
```
|
||||||
pm-tool/
|
pm-tool/
|
||||||
├── _issue-facade/ # Option 1: issue-facade implementation
|
├── _issue-core/ # Option 1: issue-core implementation
|
||||||
└── ...
|
└── ...
|
||||||
|
|
||||||
pm-tool/
|
pm-tool/
|
||||||
@@ -740,7 +740,7 @@ Project C needs Gitea issues
|
|||||||
|
|
||||||
→ Pattern: "We need issue tracking"
|
→ Pattern: "We need issue tracking"
|
||||||
→ Family: "issue-tracking"
|
→ Family: "issue-tracking"
|
||||||
→ Implementation 1: issue-facade (multi-backend)
|
→ Implementation 1: issue-core (multi-backend)
|
||||||
→ Implementation 2: github-native (GitHub-only, optimized)
|
→ Implementation 2: github-native (GitHub-only, optimized)
|
||||||
→ Implementation 3: jira-bridge (JIRA connector)
|
→ Implementation 3: jira-bridge (JIRA connector)
|
||||||
|
|
||||||
@@ -757,7 +757,7 @@ All implement "issue-tracking" family with different trade-offs
|
|||||||
**Implementation Versions:**
|
**Implementation Versions:**
|
||||||
- Implementations evolve independently
|
- Implementations evolve independently
|
||||||
- Follow semantic versioning (semver)
|
- Follow semantic versioning (semver)
|
||||||
- Example: `issue-facade@1.2.3`, `github-native@0.5.0`
|
- Example: `issue-core@1.2.3`, `github-native@0.5.0`
|
||||||
|
|
||||||
**Compatibility:**
|
**Compatibility:**
|
||||||
```yaml
|
```yaml
|
||||||
@@ -765,7 +765,7 @@ All implement "issue-tracking" family with different trade-offs
|
|||||||
metadata:
|
metadata:
|
||||||
family: issue-tracking
|
family: issue-tracking
|
||||||
family_version: "1.x" # Compatible with v1 family spec
|
family_version: "1.x" # Compatible with v1 family spec
|
||||||
implementation: issue-facade
|
implementation: issue-core
|
||||||
version: 1.2.3 # Implementation version
|
version: 1.2.3 # Implementation version
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -785,9 +785,9 @@ metadata:
|
|||||||
|
|
||||||
### For Capability Users (Integrators)
|
### For Capability Users (Integrators)
|
||||||
|
|
||||||
1. **Prefer Families over Implementations** - Depend on `issue-tracking`, not `issue-facade`
|
1. **Prefer Families over Implementations** - Depend on `issue-tracking`, not `issue-core`
|
||||||
2. **Pin Versions Loosely** - `>=1.0.0, <2.0.0` allows updates
|
2. **Pin Versions Loosely** - `>=1.0.0, <2.0.0` allows updates
|
||||||
3. **Use Underscore Prefix** - `_issue-facade/` signals "integrated"
|
3. **Use Underscore Prefix** - `_issue-core/` signals "integrated"
|
||||||
4. **Provide Feedback** - Use `feedback/` to guide improvement
|
4. **Provide Feedback** - Use `feedback/` to guide improvement
|
||||||
5. **Read the Spec** - `CAPABILITY-*.yaml` is the contract
|
5. **Read the Spec** - `CAPABILITY-*.yaml` is the contract
|
||||||
6. **Check Maturity** - Match capability maturity to your needs
|
6. **Check Maturity** - Match capability maturity to your needs
|
||||||
@@ -846,7 +846,7 @@ git commit -m "refactor: align with ReusableCapabilitiesArchitecture v0.1"
|
|||||||
**Solution:** Underscore signals "used by this repo, not core to it"
|
**Solution:** Underscore signals "used by this repo, not core to it"
|
||||||
|
|
||||||
**Benefits:**
|
**Benefits:**
|
||||||
- Visual distinction: `_issue-facade/` vs `src/`
|
- Visual distinction: `_issue-core/` vs `src/`
|
||||||
- Flatter hierarchy: 2-3 levels vs 5-6 levels
|
- Flatter hierarchy: 2-3 levels vs 5-6 levels
|
||||||
- Easy discovery: `ls _*/` lists all integrations
|
- Easy discovery: `ls _*/` lists all integrations
|
||||||
- Works with tab completion: `cd _<tab>` shows capabilities
|
- Works with tab completion: `cd _<tab>` shows capabilities
|
||||||
@@ -877,8 +877,8 @@ multi-backend-tool/
|
|||||||
├── CAPABILITY-issue-tracking-v1.yaml # Stable, production backend
|
├── CAPABILITY-issue-tracking-v1.yaml # Stable, production backend
|
||||||
├── CAPABILITY-issue-tracking-v2.yaml # Experimental, next-gen backend
|
├── CAPABILITY-issue-tracking-v2.yaml # Experimental, next-gen backend
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── v1/ # issue-facade-classic
|
│ ├── v1/ # issue-core-classic
|
||||||
│ └── v2/ # issue-facade-next
|
│ └── v2/ # issue-core-next
|
||||||
└── README.md
|
└── README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -896,10 +896,10 @@ Agents consider:
|
|||||||
```python
|
```python
|
||||||
# Agent logic
|
# Agent logic
|
||||||
capabilities = discover_capabilities(".")
|
capabilities = discover_capabilities(".")
|
||||||
issue_trackers = [c for c in capabilities if c['metadata']['family'] == 'issue-tracking']
|
issue_cores = [c for c in capabilities if c['metadata']['family'] == 'issue-tracking']
|
||||||
|
|
||||||
# Filter by maturity
|
# Filter by maturity
|
||||||
production_ready = [c for c in issue_trackers if c['metadata']['maturity'] == 'production']
|
production_ready = [c for c in issue_cores if c['metadata']['maturity'] == 'production']
|
||||||
|
|
||||||
# Choose highest version
|
# Choose highest version
|
||||||
best = max(production_ready, key=lambda c: c['metadata']['version'])
|
best = max(production_ready, key=lambda c: c['metadata']['version'])
|
||||||
@@ -986,13 +986,13 @@ devtools-suite/
|
|||||||
|
|
||||||
```
|
```
|
||||||
my-saas-app/
|
my-saas-app/
|
||||||
├── _issue-facade/ # Issue tracking capability
|
├── _issue-core/ # Issue tracking capability
|
||||||
├── _auth-service/ # Authentication capability
|
├── _auth-service/ # Authentication capability
|
||||||
├── _feedback-tool/ # Feedback collection capability
|
├── _feedback-tool/ # Feedback collection capability
|
||||||
├── src/
|
├── src/
|
||||||
│ └── myapp/
|
│ └── myapp/
|
||||||
│ ├── api/
|
│ ├── api/
|
||||||
│ │ ├── issues.py # Uses _issue-facade
|
│ │ ├── issues.py # Uses _issue-core
|
||||||
│ │ └── auth.py # Uses _auth-service
|
│ │ └── auth.py # Uses _auth-service
|
||||||
│ ├── models/
|
│ ├── models/
|
||||||
│ └── main.py
|
│ └── main.py
|
||||||
@@ -1004,15 +1004,15 @@ my-saas-app/
|
|||||||
**Installation:**
|
**Installation:**
|
||||||
```bash
|
```bash
|
||||||
# Clone integrated capabilities
|
# Clone integrated capabilities
|
||||||
git submodule add https://github.com/markitect/issue-facade _issue-facade
|
git submodule add https://github.com/markitect/issue-core _issue-core
|
||||||
git submodule add https://github.com/markitect/auth-service _auth-service
|
git submodule add https://github.com/markitect/auth-service _auth-service
|
||||||
git submodule add https://github.com/markitect/feedback-tool _feedback-tool
|
git submodule add https://github.com/markitect/feedback-tool _feedback-tool
|
||||||
|
|
||||||
# Install
|
# Install
|
||||||
pip install -e _issue-facade/ -e _auth-service/ -e _feedback-tool/
|
pip install -e _issue-core/ -e _auth-service/ -e _feedback-tool/
|
||||||
|
|
||||||
# Use in code
|
# Use in code
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
from auth_service import AuthManager
|
from auth_service import AuthManager
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
164
SCOPE.md
Normal file
164
SCOPE.md
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
# SCOPE — issue-core
|
||||||
|
|
||||||
|
Concrete in-scope / out-of-scope decisions for issue-core. Paired with `INTENT.md`,
|
||||||
|
which explains *why*; this file states *what* and *what not*.
|
||||||
|
|
||||||
|
## In scope
|
||||||
|
|
||||||
|
### Task CRUD across backends
|
||||||
|
|
||||||
|
- **Create** issues with title, description, labels, priority, type, milestone,
|
||||||
|
assignee, due date.
|
||||||
|
- **Read** individual issues and lists with filters (state, labels, priority,
|
||||||
|
assignee, text search).
|
||||||
|
- **Update** any mutable field on existing issues.
|
||||||
|
- **Close / reopen** with state transitions enforced at the domain layer.
|
||||||
|
- **Delete** where the backend allows (local SQLite; soft-archive elsewhere).
|
||||||
|
- **Comment** threads on issues.
|
||||||
|
|
||||||
|
### Backend abstraction
|
||||||
|
|
||||||
|
- A single `IssueBackend` ABC contract that every backend implements.
|
||||||
|
- `BackendCapabilities` declares which optional features a backend supports
|
||||||
|
(bulk update, search, milestones, etc.).
|
||||||
|
- A `BackendFactory` registry that maps config to backend instances.
|
||||||
|
|
||||||
|
### Backends shipped today
|
||||||
|
|
||||||
|
- **Local SQLite** — offline source of truth.
|
||||||
|
- **Gitea** — REST API integration.
|
||||||
|
|
||||||
|
### Backends planned
|
||||||
|
|
||||||
|
- **GitHub** — Issues + PRs.
|
||||||
|
- **GitLab** — Issues.
|
||||||
|
- **JIRA** — issues with story-points.
|
||||||
|
|
||||||
|
### Ingestion surfaces
|
||||||
|
|
||||||
|
- **CLI** (`issue` / `issue-core`) for humans and agents on a shell.
|
||||||
|
- **REST** (`POST /issues/`) for automation — primarily activity-core's
|
||||||
|
`IssueSink`, but open to any well-authenticated client.
|
||||||
|
- **NATS subscriber** (design stub only — implementation deferred until
|
||||||
|
activity-core migrates from REST to NATS, see `docs/nats-task-ingestion.md`).
|
||||||
|
|
||||||
|
### Synchronization
|
||||||
|
|
||||||
|
- Local SQLite ↔ remote backends, bidirectional.
|
||||||
|
- `get_issues_modified_since()` on `SyncableBackend` for incremental sync.
|
||||||
|
- Conflict resolution via `SyncableBackend.resolve_sync_conflict()`.
|
||||||
|
- Sync metadata (last-synced timestamps, remote IDs) stored on `Issue.sync_metadata`.
|
||||||
|
|
||||||
|
### State Hub integration
|
||||||
|
|
||||||
|
- Registered as a custodian-domain repo.
|
||||||
|
- Emits `add_progress_event()` calls on significant task lifecycle moments.
|
||||||
|
- Surfaces blocked tasks via the hub's `list_blocked_tasks()` view.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
### Project management
|
||||||
|
|
||||||
|
Phases, campaigns, milestones-as-plans, dependency graphs between tasks,
|
||||||
|
gantt-style scheduling, OKR linkage. **That is `project-core` (planned).**
|
||||||
|
issue-core operates on individual tasks. A milestone field exists for
|
||||||
|
flat grouping, not for multi-stage plans.
|
||||||
|
|
||||||
|
### Spawn audit trail
|
||||||
|
|
||||||
|
When activity-core's IssueSink files a task here, the *spawn event* (who fired
|
||||||
|
the rule, against which activity definition, on which triggering event) is
|
||||||
|
recorded in **activity-core's `task_spawn_log`**. issue-core stores the resulting
|
||||||
|
task and a back-reference (`triggering_event_id`) — nothing more.
|
||||||
|
|
||||||
|
Symmetrically: issue-core does not push status updates back to activity-core.
|
||||||
|
Lifecycle updates stay here; activity-core does not care what happens to a task
|
||||||
|
it spawned.
|
||||||
|
|
||||||
|
### Event bus / message broker
|
||||||
|
|
||||||
|
Inter-service communication runs on NATS managed elsewhere. issue-core consumes
|
||||||
|
specific subjects (future) and exposes a REST surface; it does not relay events
|
||||||
|
between other services.
|
||||||
|
|
||||||
|
### Notifications
|
||||||
|
|
||||||
|
Telling a human "your task changed" is the job of the relevant UI, digest,
|
||||||
|
chatbot, or notification service — not issue-core. issue-core emits progress
|
||||||
|
events; downstream consumers decide what to do with them.
|
||||||
|
|
||||||
|
### Workflow / approval engine
|
||||||
|
|
||||||
|
State machine is intentionally small (OPEN, CLOSED, IN_PROGRESS, BLOCKED).
|
||||||
|
Conditional routing, approval chains, multi-step workflows, SLA timers — all
|
||||||
|
out of scope.
|
||||||
|
|
||||||
|
### UI
|
||||||
|
|
||||||
|
issue-core is CLI + REST first. Each backend brings its own native UI
|
||||||
|
(Gitea web, GitHub web, etc.) and that is enough. A web UI for issue-core
|
||||||
|
itself is not on the roadmap.
|
||||||
|
|
||||||
|
### Identity / access management
|
||||||
|
|
||||||
|
Authentication relies on backend credentials (Gitea tokens, GitHub tokens) and
|
||||||
|
on a service-level API key for the REST ingestion endpoint. issue-core is not a
|
||||||
|
user directory.
|
||||||
|
|
||||||
|
## Integration boundaries
|
||||||
|
|
||||||
|
### Upstream emitters
|
||||||
|
|
||||||
|
| Emitter | Transport | Payload | Notes |
|
||||||
|
|----------------|----------------------|--------------------------|-------|
|
||||||
|
| Human CLI | local process call | CLI args | The classic path. |
|
||||||
|
| activity-core | REST `POST /issues/` | `TaskSpec` (see below) | Primary integration; planned NATS migration. |
|
||||||
|
| Agents | REST or CLI | `TaskSpec` or CLI args | Same surfaces as humans/automation. |
|
||||||
|
|
||||||
|
### Downstream consumers
|
||||||
|
|
||||||
|
| Consumer | Mechanism | Notes |
|
||||||
|
|----------------------|------------------------------------|-------|
|
||||||
|
| Humans / agents | `issue list`, web UI, backend UI | Standard task pickup. |
|
||||||
|
| state-hub | `add_progress_event()` calls | Lifecycle visibility. |
|
||||||
|
| Backend remote (e.g. Gitea) | direct backend write | Pass-through for the storage layer. |
|
||||||
|
|
||||||
|
### `TaskSpec` payload (from activity-core's IssueSink)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title": "string",
|
||||||
|
"description": "string",
|
||||||
|
"target_repo": "string",
|
||||||
|
"priority": "high | medium | low",
|
||||||
|
"labels": ["string"],
|
||||||
|
"due_in_days": 7,
|
||||||
|
"source_type": "rule | instruction",
|
||||||
|
"source_id": "string",
|
||||||
|
"triggering_event_id": "uuid",
|
||||||
|
"activity_definition_id": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `POST /issues/` response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"issue_id": "string",
|
||||||
|
"issue_url": "string or null",
|
||||||
|
"backend": "gitea | sqlite | github"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `issue_id` is the canonical back-reference activity-core stores in its
|
||||||
|
`task_spawn_log`. It is owned and managed by issue-core; activity-core does not
|
||||||
|
mutate it.
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- `INTENT.md` — why issue-core exists and how it fits in the Coulomb org.
|
||||||
|
- `ROADMAP.md` — feature trajectory.
|
||||||
|
- `workplans/ISSC-WP-0001-rename-and-task-ingestion.md` — current rename +
|
||||||
|
ingestion workstream.
|
||||||
|
- activity-core `docs/adr/adr-001-event-bridge-architecture.md` — the upstream
|
||||||
|
side of the IssueSink contract.
|
||||||
143
docs/nats-task-ingestion.md
Normal file
143
docs/nats-task-ingestion.md
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
# NATS Task Ingestion — Design Stub
|
||||||
|
|
||||||
|
**Status:** design stub. Implementation deferred until activity-core's
|
||||||
|
`IssueSink` migrates from REST to NATS.
|
||||||
|
|
||||||
|
**Scope:** describe what the NATS-backed counterpart of `POST /issues/` will
|
||||||
|
look like, so the activity-core agent and any other future emitter can plan
|
||||||
|
against a stable contract.
|
||||||
|
|
||||||
|
## Why NATS
|
||||||
|
|
||||||
|
Today the ingestion surface is `POST /issues/` — synchronous REST with an API
|
||||||
|
key. That works for activity-core's first cut but has limitations:
|
||||||
|
|
||||||
|
- **Coupling**: activity-core needs to know the URL and key of every issue-core
|
||||||
|
instance. With NATS, both sides connect to a shared broker; routing is by
|
||||||
|
subject.
|
||||||
|
- **Backpressure**: REST is best-effort. If issue-core is down or slow, the
|
||||||
|
emitter either blocks or drops. With NATS JetStream, messages are durable and
|
||||||
|
replay-capable.
|
||||||
|
- **Fan-out**: REST has one consumer. NATS supports multiple consumers (e.g. an
|
||||||
|
audit logger sitting alongside the actual ingester) trivially.
|
||||||
|
- **Replay**: incidents that lose tasks can be reconstructed from the JetStream
|
||||||
|
log if the consumer was offline.
|
||||||
|
|
||||||
|
## Subject pattern
|
||||||
|
|
||||||
|
```
|
||||||
|
act.tasks.create.{target_repo}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Namespace prefix `act.tasks.` (the `act` is activity-core's heritage — the
|
||||||
|
subject prefix is now neutral and other emitters can publish on it too).
|
||||||
|
- `create` is the verb. Future verbs (`act.tasks.update`, `act.tasks.close`)
|
||||||
|
are reserved but not in scope here.
|
||||||
|
- `{target_repo}` is the same string field as the REST `TaskSpec.target_repo`.
|
||||||
|
It allows subject-based routing in consumers: an issue-core instance
|
||||||
|
responsible only for one repo subscribes to `act.tasks.create.myrepo`, while
|
||||||
|
a multi-tenant instance subscribes to `act.tasks.create.>`.
|
||||||
|
|
||||||
|
## Message schema
|
||||||
|
|
||||||
|
The payload is the **exact same** schema as the REST endpoint —
|
||||||
|
`TaskIngestionRequest` in `issue_core/api/schemas.py`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title": "string",
|
||||||
|
"description": "string",
|
||||||
|
"target_repo": "string",
|
||||||
|
"priority": "high | medium | low",
|
||||||
|
"labels": ["string"],
|
||||||
|
"due_in_days": 7,
|
||||||
|
"source_type": "rule | instruction",
|
||||||
|
"source_id": "string",
|
||||||
|
"triggering_event_id": "uuid",
|
||||||
|
"activity_definition_id": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Encoded as **JSON** in the message body. `Content-Type: application/json`
|
||||||
|
in the message header.
|
||||||
|
|
||||||
|
This intentionally matches the REST schema so the validator and `_build_issue`
|
||||||
|
logic in `issue_core/api/ingest.py` can be reused unchanged by the NATS
|
||||||
|
consumer.
|
||||||
|
|
||||||
|
## JetStream configuration
|
||||||
|
|
||||||
|
The publisher (e.g. activity-core IssueSink-NATS) writes to a JetStream stream:
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---------------|----------------------------------------|
|
||||||
|
| Stream name | `ACT_TASKS` |
|
||||||
|
| Subjects | `act.tasks.>` |
|
||||||
|
| Retention | Limits (Time-based: 7 days) |
|
||||||
|
| Storage | File |
|
||||||
|
| Replicas | 3 in prod, 1 in dev |
|
||||||
|
| Discard | Old (drop oldest on overflow) |
|
||||||
|
| Max msg size | 64 KiB (TaskSpec is small) |
|
||||||
|
|
||||||
|
issue-core consumes via a **durable consumer**:
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-----------------|----------------------------------------|
|
||||||
|
| Stream | `ACT_TASKS` |
|
||||||
|
| Consumer name | `issue-core-ingest` |
|
||||||
|
| Filter subject | `act.tasks.create.>` |
|
||||||
|
| Deliver policy | All (catch up from oldest on first start) |
|
||||||
|
| Ack policy | Explicit |
|
||||||
|
| Max deliver | 5 (then dead-letter) |
|
||||||
|
| Ack wait | 30s |
|
||||||
|
| Replay policy | Instant |
|
||||||
|
|
||||||
|
## Idempotency
|
||||||
|
|
||||||
|
NATS JetStream provides **at-least-once** delivery. The consumer must dedupe
|
||||||
|
retries.
|
||||||
|
|
||||||
|
**Idempotency key:** `triggering_event_id` (UUID, included in every payload).
|
||||||
|
|
||||||
|
The consumer's responsibility:
|
||||||
|
|
||||||
|
1. Compute idempotency key from `triggering_event_id`.
|
||||||
|
2. Check whether an issue with that key already exists (lookup by
|
||||||
|
`sync_metadata.ingestion.triggering_event_id`).
|
||||||
|
3. If exists, ack the message without creating a duplicate.
|
||||||
|
4. If not, create the issue and ack.
|
||||||
|
|
||||||
|
Both REST and NATS paths share this dedupe logic, so a task can be safely
|
||||||
|
emitted via either transport without risk of duplicate issues.
|
||||||
|
|
||||||
|
## Implementation plan (when activated)
|
||||||
|
|
||||||
|
1. Add `nats-py>=2.6` as an optional dependency (`pip install issue-core[nats]`).
|
||||||
|
2. New module `issue_core/nats/consumer.py` — connects to NATS, subscribes to
|
||||||
|
the durable consumer, parses messages, calls the same `_build_issue` /
|
||||||
|
backend.create_issue path as the REST endpoint.
|
||||||
|
3. New CLI subcommand `issue subscribe --nats-url ... --stream ACT_TASKS`.
|
||||||
|
4. Add idempotency check to both REST and NATS ingestion paths (single shared
|
||||||
|
function in `issue_core/api/ingest.py` or a new `issue_core/ingestion/`
|
||||||
|
module).
|
||||||
|
5. Tests using `nats-py` test harness or a docker-compose NATS instance.
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
- Should the NATS consumer write a `progress_event` to the state hub on each
|
||||||
|
successful ingestion, in addition to creating the issue? Probably yes, but
|
||||||
|
out of scope until activation.
|
||||||
|
- Multi-tenant routing: do we run one issue-core consumer per `target_repo`,
|
||||||
|
or one shared consumer with per-repo backend lookup? Current bias: shared
|
||||||
|
consumer, simpler to operate.
|
||||||
|
- Dead-letter handling: where do messages go after 5 failed deliveries?
|
||||||
|
Candidate: a `ACT_TASKS_DLQ` stream with manual replay tooling.
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- `SCOPE.md` — confirms NATS ingestion is in-scope as a future surface.
|
||||||
|
- `issue_core/api/schemas.py` — the canonical `TaskIngestionRequest` schema.
|
||||||
|
- `issue_core/api/ingest.py` — the REST handler whose logic the NATS consumer
|
||||||
|
will share.
|
||||||
|
- activity-core `docs/adr/adr-001-event-bridge-architecture.md` — describes
|
||||||
|
activity-core's migration trajectory from REST to NATS.
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
# Agent Examples
|
# Agent Examples
|
||||||
|
|
||||||
This directory contains working examples of autonomous agents using the Issue Facade for coordination.
|
This directory contains working examples of autonomous agents using the Issue Core for coordination.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
1. **Install issue-facade**:
|
1. **Install issue-core**:
|
||||||
```bash
|
```bash
|
||||||
cd ../..
|
cd ../..
|
||||||
pip install -e .
|
pip install -e .
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ from typing import Optional, List
|
|||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||||
|
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
from issue_tracker.core.models import Issue, Label, User, Comment, IssueState
|
from issue_core.core.models import Issue, Label, User, Comment, IssueState
|
||||||
from issue_tracker.core.interfaces import IssueFilter
|
from issue_core.core.interfaces import IssueFilter
|
||||||
|
|
||||||
|
|
||||||
class HumanInLoopAgent:
|
class HumanInLoopAgent:
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ from typing import List, Dict
|
|||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||||
|
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
from issue_tracker.core.models import Issue, Label, User, Comment, IssueState
|
from issue_core.core.models import Issue, Label, User, Comment, IssueState
|
||||||
from issue_tracker.core.interfaces import IssueFilter
|
from issue_core.core.interfaces import IssueFilter
|
||||||
|
|
||||||
|
|
||||||
class MonitoringAgent:
|
class MonitoringAgent:
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ from typing import List
|
|||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||||
|
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
from issue_tracker.core.models import Issue, Label, User, Comment, IssueState
|
from issue_core.core.models import Issue, Label, User, Comment, IssueState
|
||||||
from issue_tracker.core.interfaces import IssueFilter
|
from issue_core.core.interfaces import IssueFilter
|
||||||
|
|
||||||
|
|
||||||
class BaseAgent:
|
class BaseAgent:
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ from pathlib import Path
|
|||||||
# Add parent directory to path for imports
|
# Add parent directory to path for imports
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||||
|
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
from issue_tracker.core.models import Issue, Label, User, Comment, IssueState
|
from issue_core.core.models import Issue, Label, User, Comment, IssueState
|
||||||
from issue_tracker.core.interfaces import IssueFilter
|
from issue_core.core.interfaces import IssueFilter
|
||||||
|
|
||||||
|
|
||||||
class SimpleTaskExecutor:
|
class SimpleTaskExecutor:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Feedback System Example
|
# Feedback System Example
|
||||||
|
|
||||||
This example demonstrates how to submit feedback about the issue-facade capability.
|
This example demonstrates how to submit feedback about the issue-core capability.
|
||||||
|
|
||||||
## Quick Feedback Submission
|
## Quick Feedback Submission
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ This example demonstrates how to submit feedback about the issue-facade capabili
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Navigate to the capability directory
|
# Navigate to the capability directory
|
||||||
cd capabilities/issue-facade
|
cd capabilities/issue-core
|
||||||
|
|
||||||
# Submit quick text feedback
|
# Submit quick text feedback
|
||||||
./.capability/feedback submit "The sync command is very slow when dealing with repositories that have 5000+ issues. Would love to see a progress indicator or batch processing to speed this up."
|
./.capability/feedback submit "The sync command is very slow when dealing with repositories that have 5000+ issues. Would love to see a progress indicator or batch processing to speed this up."
|
||||||
@@ -68,17 +68,17 @@ EOF
|
|||||||
|
|
||||||
### Method 3: From Master Project
|
### Method 3: From Master Project
|
||||||
|
|
||||||
If you're working in a master project that integrates issue-facade:
|
If you're working in a master project that integrates issue-core:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# From your master project root
|
# From your master project root
|
||||||
cd ~/my-master-project
|
cd ~/my-master-project
|
||||||
|
|
||||||
# Write feedback
|
# Write feedback
|
||||||
cat > feedback-for-issue-facade.md << 'EOF'
|
cat > feedback-for-issue-core.md << 'EOF'
|
||||||
## Feature Request: GitHub Backend
|
## Feature Request: GitHub Backend
|
||||||
|
|
||||||
We're using issue-facade for our Gitea repos, but our company also has
|
We're using issue-core for our Gitea repos, but our company also has
|
||||||
50+ repositories on GitHub. Would love to have a GitHub backend so we
|
50+ repositories on GitHub. Would love to have a GitHub backend so we
|
||||||
can use the same CLI for all our issue tracking.
|
can use the same CLI for all our issue tracking.
|
||||||
|
|
||||||
@@ -91,8 +91,8 @@ We'd be happy to contribute or test!
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Copy to capability's feedback directory
|
# Copy to capability's feedback directory
|
||||||
cp feedback-for-issue-facade.md \
|
cp feedback-for-issue-core.md \
|
||||||
capabilities/issue-facade/feedback/inbound/$(date +%Y%m%d)-github-backend.md
|
capabilities/issue-core/feedback/inbound/$(date +%Y%m%d)-github-backend.md
|
||||||
```
|
```
|
||||||
|
|
||||||
## Feedback Categories
|
## Feedback Categories
|
||||||
@@ -286,4 +286,4 @@ If you have questions about the feedback system itself, that's also feedback!
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Thank you for helping make issue-facade better through your feedback!**
|
**Thank you for helping make issue-core better through your feedback!**
|
||||||
|
|||||||
@@ -65,10 +65,10 @@ If you're integrating this capability into a master project:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd my-master-project
|
cd my-master-project
|
||||||
echo "Feedback about issue-facade..." > feedback.md
|
echo "Feedback about issue-core..." > feedback.md
|
||||||
|
|
||||||
# Copy to capability's feedback directory
|
# Copy to capability's feedback directory
|
||||||
cp feedback.md capabilities/issue-facade/feedback/inbound/$(date +%Y%m%d-%H%M%S)-sync-issue.md
|
cp feedback.md capabilities/issue-core/feedback/inbound/$(date +%Y%m%d-%H%M%S)-sync-issue.md
|
||||||
```
|
```
|
||||||
|
|
||||||
### What to Include
|
### What to Include
|
||||||
@@ -140,7 +140,7 @@ mv feedback/inbound/20251217-feature-request.md feedback/reviewed/
|
|||||||
Each capability maintains its own feedback directory. Users navigate to the capability and submit feedback.
|
Each capability maintains its own feedback directory. Users navigate to the capability and submit feedback.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd capabilities/issue-facade
|
cd capabilities/issue-core
|
||||||
echo "Feedback..." > feedback/inbound/$(date +%Y%m%d)-feedback.md
|
echo "Feedback..." > feedback/inbound/$(date +%Y%m%d)-feedback.md
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"""
|
"""
|
||||||
Universal Issue Tracking System
|
issue-core — Authoritative Task Lifecycle Manager
|
||||||
|
|
||||||
A backend-agnostic issue tracking system that supports multiple backends
|
The single observable place in the Coulomb org where tasks land —
|
||||||
through a plugin architecture. Designed to be extracted into a standalone
|
regardless of whether they were created by a human, by activity-core,
|
||||||
repository for use across multiple projects.
|
or by an agent. Backend-agnostic via a plugin architecture.
|
||||||
|
|
||||||
Features:
|
Features:
|
||||||
- Unified issue model across all backends
|
- Unified issue model across all backends
|
||||||
@@ -11,14 +11,14 @@ Features:
|
|||||||
- Local SQLite backend for offline work
|
- Local SQLite backend for offline work
|
||||||
- Bidirectional synchronization
|
- Bidirectional synchronization
|
||||||
- CLI-first interface
|
- CLI-first interface
|
||||||
- Support for GitHub-style and other issue tracking systems
|
- REST ingestion endpoint for activity-core's IssueSink
|
||||||
|
|
||||||
Supported Backends:
|
Supported Backends:
|
||||||
- Local SQLite (for offline/standalone use)
|
- Local SQLite (offline/standalone)
|
||||||
- Gitea (GitHub-compatible API)
|
- Gitea (GitHub-compatible API)
|
||||||
- Future: GitHub, GitLab, JIRA, Redmine, etc.
|
- Future: GitHub, GitLab, JIRA, Redmine
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "0.1.0"
|
__version__ = "0.2.0"
|
||||||
__author__ = "MarkiTect Project"
|
__author__ = "Coulomb / MarkiTect Project"
|
||||||
__description__ = "Universal Issue Tracking System with Plugin Architecture"
|
__description__ = "Authoritative task lifecycle manager with plugin architecture"
|
||||||
|
|||||||
14
issue_core/api/__init__.py
Normal file
14
issue_core/api/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
"""
|
||||||
|
issue-core REST API.
|
||||||
|
|
||||||
|
Ingestion surface for external emitters — primarily activity-core's
|
||||||
|
IssueSink, which calls POST /issues/ to file tasks against the org's
|
||||||
|
configured backends.
|
||||||
|
|
||||||
|
Run via the CLI: `issue serve --host 0.0.0.0 --port 8765`
|
||||||
|
Requires the [api] extra: `pip install issue-core[api]`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .app import create_app
|
||||||
|
|
||||||
|
__all__ = ["create_app"]
|
||||||
26
issue_core/api/app.py
Normal file
26
issue_core/api/app.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
"""
|
||||||
|
FastAPI application factory for issue-core.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
from .. import __version__
|
||||||
|
from .ingest import router as ingest_router
|
||||||
|
|
||||||
|
|
||||||
|
def create_app() -> FastAPI:
|
||||||
|
app = FastAPI(
|
||||||
|
title="issue-core",
|
||||||
|
description=(
|
||||||
|
"Authoritative task lifecycle manager for the Coulomb org. "
|
||||||
|
"POST /issues/ is the ingestion surface for activity-core's IssueSink."
|
||||||
|
),
|
||||||
|
version=__version__,
|
||||||
|
)
|
||||||
|
app.include_router(ingest_router)
|
||||||
|
|
||||||
|
@app.get("/healthz", tags=["meta"])
|
||||||
|
async def healthz() -> dict:
|
||||||
|
return {"status": "ok", "version": __version__}
|
||||||
|
|
||||||
|
return app
|
||||||
66
issue_core/api/auth.py
Normal file
66
issue_core/api/auth.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
"""
|
||||||
|
API key authentication for the issue-core REST API.
|
||||||
|
|
||||||
|
A single shared key is read from the ISSUE_CORE_API_KEY environment variable.
|
||||||
|
Clients send it either as `Authorization: Bearer <key>` or as `X-API-Key: <key>`.
|
||||||
|
|
||||||
|
If ISSUE_CORE_API_KEY is unset, the server refuses to start — the workplan
|
||||||
|
explicitly forbids an unauthenticated ingestion surface.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import Header, HTTPException, status
|
||||||
|
|
||||||
|
|
||||||
|
API_KEY_ENV_VAR = "ISSUE_CORE_API_KEY"
|
||||||
|
|
||||||
|
|
||||||
|
class AuthConfigError(RuntimeError):
|
||||||
|
"""Raised at startup when no API key is configured."""
|
||||||
|
|
||||||
|
|
||||||
|
def get_configured_api_key() -> str:
|
||||||
|
key = os.environ.get(API_KEY_ENV_VAR, "").strip()
|
||||||
|
if not key:
|
||||||
|
raise AuthConfigError(
|
||||||
|
f"{API_KEY_ENV_VAR} is not set. The ingestion endpoint requires an API key. "
|
||||||
|
f"Generate one with: python -c 'import secrets; print(secrets.token_urlsafe(32))'"
|
||||||
|
)
|
||||||
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_token(
|
||||||
|
authorization: Optional[str],
|
||||||
|
x_api_key: Optional[str],
|
||||||
|
) -> Optional[str]:
|
||||||
|
if x_api_key:
|
||||||
|
return x_api_key.strip()
|
||||||
|
if authorization:
|
||||||
|
scheme, _, value = authorization.partition(" ")
|
||||||
|
if scheme.lower() == "bearer" and value:
|
||||||
|
return value.strip()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def require_api_key(
|
||||||
|
authorization: Optional[str] = Header(default=None),
|
||||||
|
x_api_key: Optional[str] = Header(default=None, alias="X-API-Key"),
|
||||||
|
) -> None:
|
||||||
|
"""FastAPI dependency enforcing the shared API key."""
|
||||||
|
try:
|
||||||
|
expected = get_configured_api_key()
|
||||||
|
except AuthConfigError as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail=str(exc),
|
||||||
|
)
|
||||||
|
presented = _extract_token(authorization, x_api_key)
|
||||||
|
if not presented or not secrets.compare_digest(presented, expected):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Missing or invalid API key.",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
139
issue_core/api/ingest.py
Normal file
139
issue_core/api/ingest.py
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
"""
|
||||||
|
POST /issues/ — task ingestion endpoint.
|
||||||
|
|
||||||
|
Receives a TaskSpec payload (see schemas.TaskIngestionRequest) from an
|
||||||
|
authorized emitter, routes it to the configured backend, and returns the
|
||||||
|
created issue's id and (optional) URL.
|
||||||
|
|
||||||
|
Routing strategy (v1):
|
||||||
|
- Single default backend, looked up via cli.utils.get_default_backend().
|
||||||
|
- target_repo, triggering_event_id, source_*, activity_definition_id are
|
||||||
|
stored on the issue's sync_metadata for traceability back to the emitter.
|
||||||
|
- Per-target-repo routing is a planned follow-up; see SCOPE.md.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from typing import Any, Dict, Optional, Tuple
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
|
||||||
|
from ..backends.gitea import GiteaBackend
|
||||||
|
from ..backends.local import LocalSQLiteBackend
|
||||||
|
from ..cli.utils import get_config_dir, load_backend_configs
|
||||||
|
from ..core.interfaces import BackendFactory, IssueBackend
|
||||||
|
from ..core.models import Issue, IssueState, Label
|
||||||
|
from .auth import require_api_key
|
||||||
|
from .schemas import BackendName, TaskIngestionRequest, TaskIngestionResponse
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
BackendFactory.register_backend("local", LocalSQLiteBackend)
|
||||||
|
BackendFactory.register_backend("gitea", GiteaBackend)
|
||||||
|
|
||||||
|
|
||||||
|
_BACKEND_TYPE_TO_NAME: Dict[str, str] = {
|
||||||
|
"local": "sqlite",
|
||||||
|
"gitea": "gitea",
|
||||||
|
"github": "github",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_backend() -> Tuple[IssueBackend, str]:
|
||||||
|
configs = load_backend_configs()
|
||||||
|
default_name = configs.get("default", "local")
|
||||||
|
if default_name not in configs:
|
||||||
|
if default_name == "local":
|
||||||
|
configs["local"] = {
|
||||||
|
"type": "local",
|
||||||
|
"db_path": str(get_config_dir() / "issues.db"),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail=f"Default backend '{default_name}' is not configured.",
|
||||||
|
)
|
||||||
|
backend_config = configs[default_name]
|
||||||
|
backend_type = backend_config["type"]
|
||||||
|
try:
|
||||||
|
backend = BackendFactory.create_backend(backend_type)
|
||||||
|
backend.connect(backend_config)
|
||||||
|
except Exception as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail=f"Failed to connect to backend '{default_name}': {exc}",
|
||||||
|
)
|
||||||
|
return backend, backend_type
|
||||||
|
|
||||||
|
|
||||||
|
def _build_issue(payload: TaskIngestionRequest, backend_type: str) -> Issue:
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
labels = [Label(name=name) for name in payload.labels]
|
||||||
|
labels.append(Label(name=f"priority:{payload.priority}"))
|
||||||
|
labels.append(Label(name=f"source:{payload.source_type}"))
|
||||||
|
if payload.target_repo:
|
||||||
|
labels.append(Label(name=f"repo:{payload.target_repo}"))
|
||||||
|
|
||||||
|
ingestion_meta: Dict[str, Any] = {
|
||||||
|
"target_repo": payload.target_repo,
|
||||||
|
"source_type": payload.source_type,
|
||||||
|
"source_id": payload.source_id,
|
||||||
|
"triggering_event_id": str(payload.triggering_event_id),
|
||||||
|
"activity_definition_id": payload.activity_definition_id,
|
||||||
|
"ingested_at": now.isoformat(),
|
||||||
|
}
|
||||||
|
if payload.due_in_days is not None:
|
||||||
|
ingestion_meta["due_at"] = (now + timedelta(days=payload.due_in_days)).isoformat()
|
||||||
|
|
||||||
|
sync_metadata: Dict[str, Any] = {"ingestion": ingestion_meta}
|
||||||
|
|
||||||
|
return Issue(
|
||||||
|
id="",
|
||||||
|
number=0,
|
||||||
|
title=payload.title,
|
||||||
|
description=payload.description,
|
||||||
|
state=IssueState.OPEN,
|
||||||
|
created_at=now,
|
||||||
|
updated_at=now,
|
||||||
|
labels=labels,
|
||||||
|
backend_type=backend_type,
|
||||||
|
sync_metadata=sync_metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/issues/",
|
||||||
|
response_model=TaskIngestionResponse,
|
||||||
|
status_code=status.HTTP_201_CREATED,
|
||||||
|
dependencies=[Depends(require_api_key)],
|
||||||
|
summary="Ingest a task from an external emitter (e.g. activity-core).",
|
||||||
|
)
|
||||||
|
async def ingest_task(payload: TaskIngestionRequest) -> TaskIngestionResponse:
|
||||||
|
backend, backend_type = _resolve_backend()
|
||||||
|
draft = _build_issue(payload, backend_type)
|
||||||
|
try:
|
||||||
|
created: Issue = backend.create_issue(draft)
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||||
|
detail=f"Backend rejected the issue: {exc}",
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
backend.disconnect()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
issue_id = created.id or str(created.number)
|
||||||
|
issue_url: Optional[str] = None
|
||||||
|
if created.sync_metadata:
|
||||||
|
issue_url = created.sync_metadata.get("url") or created.sync_metadata.get("html_url")
|
||||||
|
backend_name: BackendName = _BACKEND_TYPE_TO_NAME.get(backend_type, backend_type) # type: ignore[assignment]
|
||||||
|
return TaskIngestionResponse(
|
||||||
|
issue_id=issue_id,
|
||||||
|
issue_url=issue_url,
|
||||||
|
backend=backend_name,
|
||||||
|
)
|
||||||
50
issue_core/api/schemas.py
Normal file
50
issue_core/api/schemas.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
"""
|
||||||
|
Pydantic schemas for the issue-core REST API.
|
||||||
|
|
||||||
|
The TaskIngestionRequest schema matches activity-core's IssueSink TaskSpec
|
||||||
|
payload exactly. See:
|
||||||
|
- SCOPE.md "TaskSpec payload" section
|
||||||
|
- activity-core docs/adr/adr-001-event-bridge-architecture.md
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import List, Literal, Optional
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
|
||||||
|
SourceType = Literal["rule", "instruction"]
|
||||||
|
Priority = Literal["high", "medium", "low"]
|
||||||
|
BackendName = Literal["gitea", "sqlite", "github"]
|
||||||
|
|
||||||
|
|
||||||
|
class TaskIngestionRequest(BaseModel):
|
||||||
|
"""TaskSpec payload from activity-core's IssueSink (POST /issues/)."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
title: str = Field(..., min_length=1, max_length=500)
|
||||||
|
description: str = ""
|
||||||
|
target_repo: str = Field(..., min_length=1)
|
||||||
|
priority: Priority = "medium"
|
||||||
|
labels: List[str] = Field(default_factory=list)
|
||||||
|
due_in_days: Optional[int] = Field(default=None, ge=0)
|
||||||
|
source_type: SourceType
|
||||||
|
source_id: str = Field(..., min_length=1)
|
||||||
|
triggering_event_id: UUID
|
||||||
|
activity_definition_id: str = Field(..., min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
class TaskIngestionResponse(BaseModel):
|
||||||
|
"""Response returned to the emitter after a successful ingestion."""
|
||||||
|
|
||||||
|
issue_id: str
|
||||||
|
issue_url: Optional[str] = None
|
||||||
|
backend: BackendName
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorResponse(BaseModel):
|
||||||
|
"""Uniform error envelope."""
|
||||||
|
|
||||||
|
error: str
|
||||||
|
detail: Optional[str] = None
|
||||||
@@ -11,11 +11,12 @@ from pathlib import Path
|
|||||||
from .commands import issue_group
|
from .commands import issue_group
|
||||||
from .backend_commands import backend_group
|
from .backend_commands import backend_group
|
||||||
from .sync_commands import sync_group
|
from .sync_commands import sync_group
|
||||||
|
from .serve_command import serve_command
|
||||||
from .. import __version__
|
from .. import __version__
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
@click.version_option(version=__version__, package_name='issue-tracker')
|
@click.version_option(version=__version__, package_name='issue-core')
|
||||||
@click.option('--config', type=click.Path(), help='Configuration file path')
|
@click.option('--config', type=click.Path(), help='Configuration file path')
|
||||||
@click.option('--backend', help='Backend to use (local, gitea)')
|
@click.option('--backend', help='Backend to use (local, gitea)')
|
||||||
@click.option('--verbose', '-v', is_flag=True, help='Verbose output')
|
@click.option('--verbose', '-v', is_flag=True, help='Verbose output')
|
||||||
@@ -52,6 +53,7 @@ def cli(ctx, config, backend, verbose):
|
|||||||
cli.add_command(issue_group, name='issue')
|
cli.add_command(issue_group, name='issue')
|
||||||
cli.add_command(backend_group, name='backend')
|
cli.add_command(backend_group, name='backend')
|
||||||
cli.add_command(sync_group, name='sync')
|
cli.add_command(sync_group, name='sync')
|
||||||
|
cli.add_command(serve_command)
|
||||||
|
|
||||||
|
|
||||||
# Convenience aliases - direct issue commands
|
# Convenience aliases - direct issue commands
|
||||||
|
|||||||
43
issue_core/cli/serve_command.py
Normal file
43
issue_core/cli/serve_command.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
"""
|
||||||
|
`issue serve` — launch the issue-core REST API.
|
||||||
|
|
||||||
|
Requires the [api] extra: `pip install issue-core[api]`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
|
||||||
|
@click.command("serve")
|
||||||
|
@click.option("--host", default="127.0.0.1", show_default=True, help="Bind address.")
|
||||||
|
@click.option("--port", default=8765, show_default=True, type=int, help="Bind port.")
|
||||||
|
@click.option("--reload", is_flag=True, default=False, help="Auto-reload on code change (dev only).")
|
||||||
|
@click.option("--log-level", default="info", show_default=True, help="Uvicorn log level.")
|
||||||
|
def serve_command(host: str, port: int, reload: bool, log_level: str) -> None:
|
||||||
|
"""Launch the issue-core REST API (POST /issues/ ingestion endpoint).
|
||||||
|
|
||||||
|
Requires ISSUE_CORE_API_KEY to be set in the environment.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import uvicorn # noqa: F401
|
||||||
|
except ImportError:
|
||||||
|
raise click.ClickException(
|
||||||
|
"The 'api' extra is not installed. Run: pip install 'issue-core[api]'"
|
||||||
|
)
|
||||||
|
|
||||||
|
from ..api.auth import AuthConfigError, get_configured_api_key
|
||||||
|
|
||||||
|
try:
|
||||||
|
get_configured_api_key()
|
||||||
|
except AuthConfigError as exc:
|
||||||
|
raise click.ClickException(str(exc))
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
uvicorn.run(
|
||||||
|
"issue_core.api.app:create_app",
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
reload=reload,
|
||||||
|
log_level=log_level,
|
||||||
|
factory=True,
|
||||||
|
)
|
||||||
@@ -3,8 +3,8 @@ requires = ["setuptools>=45", "wheel", "setuptools-scm[toml]>=6.2"]
|
|||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "universal-issue-tracker"
|
name = "issue-core"
|
||||||
description = "Backend-agnostic issue tracking system with plugin architecture"
|
description = "Authoritative task lifecycle manager for the Coulomb org — backend-agnostic with plugin architecture"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
license = {text = "MIT"}
|
license = {text = "MIT"}
|
||||||
@@ -45,6 +45,9 @@ dev = [
|
|||||||
"flake8>=4.0",
|
"flake8>=4.0",
|
||||||
"mypy>=0.900",
|
"mypy>=0.900",
|
||||||
"pre-commit>=2.0",
|
"pre-commit>=2.0",
|
||||||
|
"httpx>=0.27",
|
||||||
|
"fastapi>=0.110,<1.0",
|
||||||
|
"pydantic>=2.0,<3.0",
|
||||||
]
|
]
|
||||||
docs = [
|
docs = [
|
||||||
"sphinx>=4.0",
|
"sphinx>=4.0",
|
||||||
@@ -60,25 +63,30 @@ github = [
|
|||||||
jira = [
|
jira = [
|
||||||
"jira>=3.0",
|
"jira>=3.0",
|
||||||
]
|
]
|
||||||
|
api = [
|
||||||
|
"fastapi>=0.110,<1.0",
|
||||||
|
"uvicorn[standard]>=0.27,<1.0",
|
||||||
|
"pydantic>=2.0,<3.0",
|
||||||
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "https://github.com/markitect/universal-issue-tracker"
|
Homepage = "https://github.com/coulomb/issue-core"
|
||||||
Documentation = "https://universal-issue-tracker.readthedocs.io/"
|
Documentation = "https://issue-core.readthedocs.io/"
|
||||||
Repository = "https://github.com/markitect/universal-issue-tracker.git"
|
Repository = "https://github.com/coulomb/issue-core.git"
|
||||||
"Bug Tracker" = "https://github.com/markitect/universal-issue-tracker/issues"
|
"Bug Tracker" = "https://github.com/coulomb/issue-core/issues"
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
issue = "issue_tracker.cli.main:main"
|
issue = "issue_core.cli.main:main"
|
||||||
issue-tracker = "issue_tracker.cli.main:main"
|
issue-core = "issue_core.cli.main:main"
|
||||||
|
|
||||||
[tool.setuptools]
|
[tool.setuptools.packages.find]
|
||||||
packages = ["issue_tracker"]
|
include = ["issue_core*"]
|
||||||
|
|
||||||
[tool.setuptools.dynamic]
|
[tool.setuptools.dynamic]
|
||||||
version = {attr = "issue_tracker.__version__"}
|
version = {attr = "issue_core.__version__"}
|
||||||
|
|
||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
issue_tracker = ["backends/local/schema.sql"]
|
issue_core = ["backends/local/schema.sql"]
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
line-length = 100
|
line-length = 100
|
||||||
@@ -101,7 +109,7 @@ extend-exclude = '''
|
|||||||
[tool.isort]
|
[tool.isort]
|
||||||
profile = "black"
|
profile = "black"
|
||||||
line_length = 100
|
line_length = 100
|
||||||
known_first_party = ["issue_tracker"]
|
known_first_party = ["issue_core"]
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
python_version = "3.8"
|
python_version = "3.8"
|
||||||
@@ -142,7 +150,7 @@ markers = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[tool.coverage.run]
|
[tool.coverage.run]
|
||||||
source = ["issue_tracker"]
|
source = ["issue_core"]
|
||||||
omit = [
|
omit = [
|
||||||
"*/tests/*",
|
"*/tests/*",
|
||||||
"*/test_*",
|
"*/test_*",
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
"""Test suite for issue-facade capability."""
|
"""Test suite for issue-core capability."""
|
||||||
|
|||||||
177
tests/test_api_ingest.py
Normal file
177
tests/test_api_ingest.py
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
"""
|
||||||
|
Tests for POST /issues/ ingestion endpoint.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import uuid
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
pytest.importorskip("fastapi")
|
||||||
|
pytest.importorskip("httpx")
|
||||||
|
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from issue_core.api.app import create_app
|
||||||
|
|
||||||
|
|
||||||
|
API_KEY = "test-key-not-a-real-secret-only-for-pytest"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def tmp_issue_store(monkeypatch, tmp_path):
|
||||||
|
"""Point cli.utils.get_config_dir at a tmp dir and the default backend at local."""
|
||||||
|
config_dir = tmp_path / "issue-core"
|
||||||
|
config_dir.mkdir()
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"issue_core.cli.utils.get_config_dir", lambda: config_dir, raising=True
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"issue_core.api.ingest.get_config_dir", lambda: config_dir, raising=True
|
||||||
|
)
|
||||||
|
|
||||||
|
db_path = str(config_dir / "issues.db")
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"issue_core.cli.utils.load_backend_configs",
|
||||||
|
lambda: {"default": "local", "local": {"type": "local", "db_path": db_path}},
|
||||||
|
raising=True,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"issue_core.api.ingest.load_backend_configs",
|
||||||
|
lambda: {"default": "local", "local": {"type": "local", "db_path": db_path}},
|
||||||
|
raising=True,
|
||||||
|
)
|
||||||
|
return config_dir
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client(monkeypatch, tmp_issue_store):
|
||||||
|
monkeypatch.setenv("ISSUE_CORE_API_KEY", API_KEY)
|
||||||
|
return TestClient(create_app())
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def valid_payload():
|
||||||
|
return {
|
||||||
|
"title": "Fix the parser",
|
||||||
|
"description": "Parser fails on multi-line input.",
|
||||||
|
"target_repo": "coulomb/parser",
|
||||||
|
"priority": "high",
|
||||||
|
"labels": ["bug"],
|
||||||
|
"due_in_days": 7,
|
||||||
|
"source_type": "rule",
|
||||||
|
"source_id": "rule:parse-failure",
|
||||||
|
"triggering_event_id": str(uuid.uuid4()),
|
||||||
|
"activity_definition_id": "ad:parser-monitor",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_healthz(client):
|
||||||
|
response = client.get("/healthz")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["status"] == "ok"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_ingest_rejects_missing_api_key(client, valid_payload):
|
||||||
|
response = client.post("/issues/", json=valid_payload)
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_ingest_rejects_wrong_api_key(client, valid_payload):
|
||||||
|
response = client.post(
|
||||||
|
"/issues/", json=valid_payload, headers={"Authorization": "Bearer wrong"}
|
||||||
|
)
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_ingest_creates_issue_with_bearer(client, valid_payload):
|
||||||
|
response = client.post(
|
||||||
|
"/issues/",
|
||||||
|
json=valid_payload,
|
||||||
|
headers={"Authorization": f"Bearer {API_KEY}"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 201, response.text
|
||||||
|
body = response.json()
|
||||||
|
assert body["backend"] == "sqlite"
|
||||||
|
assert body["issue_id"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_ingest_creates_issue_with_x_api_key(client, valid_payload):
|
||||||
|
response = client.post(
|
||||||
|
"/issues/",
|
||||||
|
json=valid_payload,
|
||||||
|
headers={"X-API-Key": API_KEY},
|
||||||
|
)
|
||||||
|
assert response.status_code == 201, response.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_ingest_rejects_invalid_payload(client):
|
||||||
|
bad = {"title": "no required fields"}
|
||||||
|
response = client.post(
|
||||||
|
"/issues/", json=bad, headers={"Authorization": f"Bearer {API_KEY}"}
|
||||||
|
)
|
||||||
|
assert response.status_code == 422
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_ingest_rejects_invalid_priority(client, valid_payload):
|
||||||
|
valid_payload["priority"] = "urgent"
|
||||||
|
response = client.post(
|
||||||
|
"/issues/",
|
||||||
|
json=valid_payload,
|
||||||
|
headers={"Authorization": f"Bearer {API_KEY}"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 422
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_ingest_persists_traceability_metadata(client, valid_payload, tmp_issue_store):
|
||||||
|
"""Check that triggering_event_id, source_*, target_repo are stored on the issue."""
|
||||||
|
response = client.post(
|
||||||
|
"/issues/",
|
||||||
|
json=valid_payload,
|
||||||
|
headers={"Authorization": f"Bearer {API_KEY}"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 201, response.text
|
||||||
|
issue_id = response.json()["issue_id"]
|
||||||
|
|
||||||
|
from issue_core.backends.local import LocalSQLiteBackend
|
||||||
|
|
||||||
|
backend = LocalSQLiteBackend()
|
||||||
|
backend.connect({"type": "local", "db_path": str(tmp_issue_store / "issues.db")})
|
||||||
|
try:
|
||||||
|
stored = backend.get_issue(issue_id)
|
||||||
|
assert stored is not None
|
||||||
|
ingestion = stored.sync_metadata.get("ingestion") or {}
|
||||||
|
assert ingestion["target_repo"] == valid_payload["target_repo"]
|
||||||
|
assert ingestion["source_type"] == valid_payload["source_type"]
|
||||||
|
assert ingestion["source_id"] == valid_payload["source_id"]
|
||||||
|
assert ingestion["triggering_event_id"] == valid_payload["triggering_event_id"]
|
||||||
|
assert ingestion["activity_definition_id"] == valid_payload["activity_definition_id"]
|
||||||
|
label_names = {label.name for label in stored.labels}
|
||||||
|
assert f"priority:{valid_payload['priority']}" in label_names
|
||||||
|
assert f"source:{valid_payload['source_type']}" in label_names
|
||||||
|
assert f"repo:{valid_payload['target_repo']}" in label_names
|
||||||
|
assert "bug" in label_names
|
||||||
|
finally:
|
||||||
|
backend.disconnect()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_app_refuses_without_api_key_env(monkeypatch, tmp_issue_store, valid_payload):
|
||||||
|
monkeypatch.delenv("ISSUE_CORE_API_KEY", raising=False)
|
||||||
|
app = create_app()
|
||||||
|
client = TestClient(app)
|
||||||
|
response = client.post(
|
||||||
|
"/issues/", json=valid_payload, headers={"Authorization": "Bearer anything"}
|
||||||
|
)
|
||||||
|
assert response.status_code == 503
|
||||||
|
assert "ISSUE_CORE_API_KEY" in response.json()["detail"]
|
||||||
@@ -12,8 +12,8 @@ from pathlib import Path
|
|||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
from unittest.mock import Mock, patch, MagicMock
|
from unittest.mock import Mock, patch, MagicMock
|
||||||
|
|
||||||
from issue_tracker.cli.main import cli
|
from issue_core.cli.main import cli
|
||||||
from issue_tracker.cli.utils import load_backend_configs, save_backend_configs
|
from issue_core.cli.utils import load_backend_configs, save_backend_configs
|
||||||
|
|
||||||
|
|
||||||
class TestCLICommands:
|
class TestCLICommands:
|
||||||
@@ -37,9 +37,9 @@ class TestCLICommands:
|
|||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
# Should show either configured backends or "No backends configured"
|
# Should show either configured backends or "No backends configured"
|
||||||
|
|
||||||
@patch('issue_tracker.cli.backend_commands.load_backend_configs')
|
@patch('issue_core.cli.backend_commands.load_backend_configs')
|
||||||
@patch('issue_tracker.cli.backend_commands.save_backend_configs')
|
@patch('issue_core.cli.backend_commands.save_backend_configs')
|
||||||
@patch('issue_tracker.cli.backend_commands.test_backend_connection')
|
@patch('issue_core.cli.backend_commands.test_backend_connection')
|
||||||
def test_backend_add_gitea_with_env_token(self, mock_test_conn, mock_save, mock_load):
|
def test_backend_add_gitea_with_env_token(self, mock_test_conn, mock_save, mock_load):
|
||||||
"""Test adding Gitea backend with environment token."""
|
"""Test adding Gitea backend with environment token."""
|
||||||
# Mock empty initial config
|
# Mock empty initial config
|
||||||
@@ -63,9 +63,9 @@ class TestCLICommands:
|
|||||||
assert saved_config['test-gitea']['type'] == 'gitea'
|
assert saved_config['test-gitea']['type'] == 'gitea'
|
||||||
assert saved_config['test-gitea']['token'] == 'test-token'
|
assert saved_config['test-gitea']['token'] == 'test-token'
|
||||||
|
|
||||||
@patch('issue_tracker.cli.backend_commands.load_backend_configs')
|
@patch('issue_core.cli.backend_commands.load_backend_configs')
|
||||||
@patch('issue_tracker.cli.backend_commands.save_backend_configs')
|
@patch('issue_core.cli.backend_commands.save_backend_configs')
|
||||||
@patch('issue_tracker.cli.backend_commands.test_backend_connection')
|
@patch('issue_core.cli.backend_commands.test_backend_connection')
|
||||||
def test_backend_add_local(self, mock_test_conn, mock_save, mock_load):
|
def test_backend_add_local(self, mock_test_conn, mock_save, mock_load):
|
||||||
"""Test adding local backend."""
|
"""Test adding local backend."""
|
||||||
mock_load.return_value = {}
|
mock_load.return_value = {}
|
||||||
@@ -78,7 +78,7 @@ class TestCLICommands:
|
|||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert 'Backend \'test-local\' added successfully' in result.output
|
assert 'Backend \'test-local\' added successfully' in result.output
|
||||||
|
|
||||||
@patch('issue_tracker.cli.commands.get_backend')
|
@patch('issue_core.cli.commands.get_backend')
|
||||||
def test_show_command(self, mock_get_backend):
|
def test_show_command(self, mock_get_backend):
|
||||||
"""Test issue show command."""
|
"""Test issue show command."""
|
||||||
# Mock backend and issue
|
# Mock backend and issue
|
||||||
@@ -104,7 +104,7 @@ class TestCLICommands:
|
|||||||
assert 'Test description' in result.output
|
assert 'Test description' in result.output
|
||||||
assert 'State: open' in result.output
|
assert 'State: open' in result.output
|
||||||
|
|
||||||
@patch('issue_tracker.cli.utils.get_backend')
|
@patch('issue_core.cli.utils.get_backend')
|
||||||
def test_show_command_issue_not_found(self, mock_get_backend):
|
def test_show_command_issue_not_found(self, mock_get_backend):
|
||||||
"""Test issue show command when issue doesn't exist."""
|
"""Test issue show command when issue doesn't exist."""
|
||||||
mock_backend = Mock()
|
mock_backend = Mock()
|
||||||
@@ -121,7 +121,7 @@ class TestCLICommands:
|
|||||||
result = self.runner.invoke(cli, ['--version'])
|
result = self.runner.invoke(cli, ['--version'])
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
|
|
||||||
@patch('issue_tracker.cli.utils.get_backend')
|
@patch('issue_core.cli.utils.get_backend')
|
||||||
def test_list_command_basic(self, mock_get_backend):
|
def test_list_command_basic(self, mock_get_backend):
|
||||||
"""Test basic list command functionality."""
|
"""Test basic list command functionality."""
|
||||||
# This test will help us identify the existing bug
|
# This test will help us identify the existing bug
|
||||||
@@ -157,7 +157,7 @@ class TestBackendConfiguration:
|
|||||||
|
|
||||||
def test_config_directory_creation(self):
|
def test_config_directory_creation(self):
|
||||||
"""Test configuration directory is created properly."""
|
"""Test configuration directory is created properly."""
|
||||||
from issue_tracker.cli.utils import get_config_dir
|
from issue_core.cli.utils import get_config_dir
|
||||||
|
|
||||||
config_dir = get_config_dir()
|
config_dir = get_config_dir()
|
||||||
assert config_dir.exists()
|
assert config_dir.exists()
|
||||||
@@ -177,7 +177,7 @@ class TestBackendConfiguration:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Test saving
|
# Test saving
|
||||||
with patch('issue_tracker.cli.utils.get_backend_config_path', return_value=config_file):
|
with patch('issue_core.cli.utils.get_backend_config_path', return_value=config_file):
|
||||||
save_backend_configs(test_config)
|
save_backend_configs(test_config)
|
||||||
|
|
||||||
# Test loading
|
# Test loading
|
||||||
@@ -190,7 +190,7 @@ class TestBackendConfiguration:
|
|||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
non_existent_file = Path(temp_dir) / 'nonexistent.json'
|
non_existent_file = Path(temp_dir) / 'nonexistent.json'
|
||||||
|
|
||||||
with patch('issue_tracker.cli.utils.get_backend_config_path', return_value=non_existent_file):
|
with patch('issue_core.cli.utils.get_backend_config_path', return_value=non_existent_file):
|
||||||
config = load_backend_configs()
|
config = load_backend_configs()
|
||||||
|
|
||||||
assert config == {}
|
assert config == {}
|
||||||
@@ -204,13 +204,13 @@ class TestEnvironmentTokenDetection:
|
|||||||
"""Test GITEA_API_TOKEN environment variable detection."""
|
"""Test GITEA_API_TOKEN environment variable detection."""
|
||||||
mock_getenv.return_value = 'test-env-token'
|
mock_getenv.return_value = 'test-env-token'
|
||||||
|
|
||||||
from issue_tracker.cli.backend_commands import add_backend
|
from issue_core.cli.backend_commands import add_backend
|
||||||
|
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
|
|
||||||
with patch('issue_tracker.cli.backend_commands.load_backend_configs', return_value={}):
|
with patch('issue_core.cli.backend_commands.load_backend_configs', return_value={}):
|
||||||
with patch('issue_tracker.cli.backend_commands.save_backend_configs'):
|
with patch('issue_core.cli.backend_commands.save_backend_configs'):
|
||||||
with patch('issue_tracker.cli.backend_commands.test_backend_connection', return_value=True):
|
with patch('issue_core.cli.backend_commands.test_backend_connection', return_value=True):
|
||||||
result = runner.invoke(add_backend, [
|
result = runner.invoke(add_backend, [
|
||||||
'test-gitea', 'gitea'
|
'test-gitea', 'gitea'
|
||||||
], input='https://git.example.com\ntestorg\ntestrepo\n')
|
], input='https://git.example.com\ntestorg\ntestrepo\n')
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ including state management, validation, and business logic.
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from issue_tracker.core.models import (
|
from issue_core.core.models import (
|
||||||
Issue, Label, User, Milestone, Comment,
|
Issue, Label, User, Milestone, Comment,
|
||||||
IssueState, Priority, IssueType, LabelCategories
|
IssueState, Priority, IssueType, LabelCategories
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ These tests ensure the Gitea backend works correctly with the API.
|
|||||||
import pytest
|
import pytest
|
||||||
import json
|
import json
|
||||||
from unittest.mock import Mock, patch, MagicMock
|
from unittest.mock import Mock, patch, MagicMock
|
||||||
from issue_tracker.backends.gitea.backend import GiteaBackend, GiteaAPIError
|
from issue_core.backends.gitea.backend import GiteaBackend, GiteaAPIError
|
||||||
|
|
||||||
|
|
||||||
class TestGiteaBackend:
|
class TestGiteaBackend:
|
||||||
@@ -32,7 +32,7 @@ class TestGiteaBackend:
|
|||||||
assert self.backend.repo is None
|
assert self.backend.repo is None
|
||||||
assert self.backend.session is not None
|
assert self.backend.session is not None
|
||||||
|
|
||||||
@patch('issue_tracker.backends.gitea.backend.requests.Session')
|
@patch('issue_core.backends.gitea.backend.requests.Session')
|
||||||
def test_connect_success(self, mock_session_class):
|
def test_connect_success(self, mock_session_class):
|
||||||
"""Test successful connection to Gitea API."""
|
"""Test successful connection to Gitea API."""
|
||||||
mock_session = MagicMock()
|
mock_session = MagicMock()
|
||||||
@@ -61,7 +61,7 @@ class TestGiteaBackend:
|
|||||||
'Accept': 'application/json'
|
'Accept': 'application/json'
|
||||||
})
|
})
|
||||||
|
|
||||||
@patch('issue_tracker.backends.gitea.backend.requests.Session')
|
@patch('issue_core.backends.gitea.backend.requests.Session')
|
||||||
def test_connect_failure(self, mock_session_class):
|
def test_connect_failure(self, mock_session_class):
|
||||||
"""Test failed connection raises appropriate error."""
|
"""Test failed connection raises appropriate error."""
|
||||||
mock_session = MagicMock()
|
mock_session = MagicMock()
|
||||||
@@ -96,7 +96,7 @@ class TestGiteaBackend:
|
|||||||
called_url = mock_request.call_args[1]['url'] if 'url' in mock_request.call_args[1] else mock_request.call_args[0][1]
|
called_url = mock_request.call_args[1]['url'] if 'url' in mock_request.call_args[1] else mock_request.call_args[0][1]
|
||||||
assert called_url == 'https://git.example.com/api/v1/repos/owner/repo'
|
assert called_url == 'https://git.example.com/api/v1/repos/owner/repo'
|
||||||
|
|
||||||
@patch('issue_tracker.backends.gitea.backend.requests.Session')
|
@patch('issue_core.backends.gitea.backend.requests.Session')
|
||||||
def test_test_connection_success(self, mock_session_class):
|
def test_test_connection_success(self, mock_session_class):
|
||||||
"""Test test_connection method works correctly."""
|
"""Test test_connection method works correctly."""
|
||||||
mock_session = MagicMock()
|
mock_session = MagicMock()
|
||||||
@@ -117,7 +117,7 @@ class TestGiteaBackend:
|
|||||||
result = backend.test_connection()
|
result = backend.test_connection()
|
||||||
assert result is True
|
assert result is True
|
||||||
|
|
||||||
@patch('issue_tracker.backends.gitea.backend.requests.Session')
|
@patch('issue_core.backends.gitea.backend.requests.Session')
|
||||||
def test_test_connection_failure(self, mock_session_class):
|
def test_test_connection_failure(self, mock_session_class):
|
||||||
"""Test test_connection handles failures gracefully."""
|
"""Test test_connection handles failures gracefully."""
|
||||||
mock_session = MagicMock()
|
mock_session = MagicMock()
|
||||||
@@ -139,7 +139,7 @@ class TestGiteaBackend:
|
|||||||
result = backend.test_connection()
|
result = backend.test_connection()
|
||||||
assert result is False
|
assert result is False
|
||||||
|
|
||||||
@patch('issue_tracker.backends.gitea.backend.requests.Session')
|
@patch('issue_core.backends.gitea.backend.requests.Session')
|
||||||
def test_get_issue_success(self, mock_session_class):
|
def test_get_issue_success(self, mock_session_class):
|
||||||
"""Test successful issue retrieval."""
|
"""Test successful issue retrieval."""
|
||||||
mock_session = MagicMock()
|
mock_session = MagicMock()
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import tempfile
|
|||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from issue_tracker.backends.local.backend import LocalSQLiteBackend
|
from issue_core.backends.local.backend import LocalSQLiteBackend
|
||||||
from issue_tracker.core.models import Issue, Label, User, Milestone, Comment, IssueState
|
from issue_core.core.models import Issue, Label, User, Milestone, Comment, IssueState
|
||||||
from issue_tracker.core.interfaces import IssueFilter
|
from issue_core.core.interfaces import IssueFilter
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
|
|||||||
@@ -2,34 +2,34 @@
|
|||||||
id: ISSC-WP-0001
|
id: ISSC-WP-0001
|
||||||
type: workplan
|
type: workplan
|
||||||
domain: custodian
|
domain: custodian
|
||||||
repo: issue-facade
|
repo: issue-core
|
||||||
status: active
|
status: done
|
||||||
state_hub_workstream_id: 1135fc1d-1f46-4e35-886d-04cc3b8050b6
|
state_hub_workstream_id: 1135fc1d-1f46-4e35-886d-04cc3b8050b6
|
||||||
tasks:
|
tasks:
|
||||||
- id: T01
|
- id: T01
|
||||||
title: Rename package issue-facade → issue-core throughout
|
title: Rename package issue-facade → issue-core throughout
|
||||||
state_hub_task_id: b7054428-82a9-4d81-bfa8-5b5ee2eaf69f
|
state_hub_task_id: b7054428-82a9-4d81-bfa8-5b5ee2eaf69f
|
||||||
status: todo
|
status: done
|
||||||
- id: T02
|
- id: T02
|
||||||
title: Register issue-core in state hub under capabilities domain
|
title: Register issue-core in state hub under capabilities domain
|
||||||
state_hub_task_id: b1d36996-44ff-48b9-b208-709d6874453c
|
state_hub_task_id: b1d36996-44ff-48b9-b208-709d6874453c
|
||||||
status: todo
|
status: done
|
||||||
- id: T03
|
- id: T03
|
||||||
title: Write INTENT.md
|
title: Write INTENT.md
|
||||||
state_hub_task_id: 265c6338-0310-409d-a081-6446042f6274
|
state_hub_task_id: 265c6338-0310-409d-a081-6446042f6274
|
||||||
status: todo
|
status: done
|
||||||
- id: T04
|
- id: T04
|
||||||
title: Update or write SCOPE.md
|
title: Update or write SCOPE.md
|
||||||
state_hub_task_id: f95ac730-7ba0-4eae-bcae-de1e7d24b164
|
state_hub_task_id: f95ac730-7ba0-4eae-bcae-de1e7d24b164
|
||||||
status: todo
|
status: done
|
||||||
- id: T05
|
- id: T05
|
||||||
title: Implement task ingestion REST endpoint POST /issues/
|
title: Implement task ingestion REST endpoint POST /issues/
|
||||||
state_hub_task_id: 26af07e4-c072-42ad-bb5c-facb196156c9
|
state_hub_task_id: 26af07e4-c072-42ad-bb5c-facb196156c9
|
||||||
status: todo
|
status: done
|
||||||
- id: T06
|
- id: T06
|
||||||
title: Document NATS subscriber interface (design stub)
|
title: Document NATS subscriber interface (design stub)
|
||||||
state_hub_task_id: dff61fed-1e8c-4eb3-bbd6-1e3742329945
|
state_hub_task_id: dff61fed-1e8c-4eb3-bbd6-1e3742329945
|
||||||
status: todo
|
status: done
|
||||||
created: "2026-05-14"
|
created: "2026-05-14"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user