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:
2026-05-17 05:16:27 +02:00
parent 99ea1fbc45
commit b605d970e3
38 changed files with 1324 additions and 361 deletions

View File

@@ -1,6 +1,6 @@
# 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
@@ -44,7 +44,7 @@ Comprehensive guide for coding agents:
- Error handling
- Examples
**Injected into:** `.claude/capabilities/issue-facade.md` in main project
**Injected into:** `.claude/capabilities/issue-core.md` in main project
### 3. Integration Automation
@@ -61,7 +61,7 @@ Interactive script that:
```bash
make integrate
# or
cd capabilities/issue-facade && ./.capability/integrate.sh
cd capabilities/issue-core && ./.capability/integrate.sh
```
### 4. Integration Checklist
@@ -81,7 +81,7 @@ Step-by-step checklist for humans integrating the capability:
```
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
├── 3. Script configures backend (prompts for credentials)
├── 4. Script copies agent-context.md → .claude/capabilities/
@@ -101,13 +101,13 @@ Main Project Setup
Agent Workflow
├── 1. Agent receives task involving issues
├── 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
└── 5. Agent avoids direct API calls (warned in docs)
```
**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/commands/use-issues.md` - Slash command for context injection
@@ -116,18 +116,18 @@ Agent Workflow
```
project-root/
├── capabilities/
│ └── issue-facade/ # Capability code
│ └── issue-core/ # Capability code
│ ├── CAPABILITY-issue-tracking.yaml # Machine-readable metadata
│ ├── .capability/
│ │ ├── agent-context.md # Agent guide (source)
│ │ ├── integrate.sh # Integration script
│ │ └── README.md # This file
│ ├── issue_tracker/ # Python package
│ ├── issue_core/ # Python package
│ └── ...
├── .claude/ # Claude Code configuration
│ ├── capabilities/ # Capability docs for agents
│ │ └── issue-facade.md # Agent guide (copy)
│ │ └── issue-core.md # Agent guide (copy)
│ │
│ ├── commands/ # Slash commands
│ │ └── use-issues.md # /use-issues command
@@ -135,7 +135,7 @@ project-root/
│ └── context/ # Always-available context
│ └── capabilities.md # List of all capabilities
└── .issue-facade/ # Capability config (gitignored)
└── .issue-core/ # Capability config (gitignored)
├── config.json # Backend configuration
└── issues.db # Local cache/backup
```
@@ -150,13 +150,13 @@ import os
from pathlib import Path
def has_issue_capability(project_root: Path) -> bool:
"""Check if issue-facade capability is available."""
capability_guide = project_root / ".claude/capabilities/issue-facade.md"
"""Check if issue-core capability is available."""
capability_guide = project_root / ".claude/capabilities/issue-core.md"
return capability_guide.exists()
if has_issue_capability(Path.cwd()):
# Use capability
from issue_tracker.backends.gitea import GiteaBackend
from issue_core.backends.gitea import GiteaBackend
backend = GiteaBackend()
else:
# Fall back or prompt human
@@ -173,15 +173,15 @@ def get_capability_docs(capability_name: str) -> str:
return None
# 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...
```
**3. Use the API as documented:**
```python
# Example from agent-context.md
from issue_tracker.backends.gitea import GiteaBackend
from issue_tracker.core.interfaces import IssueFilter
from issue_core.backends.gitea import GiteaBackend
from issue_core.core.interfaces import IssueFilter
backend = GiteaBackend()
backend.connect(config)
@@ -204,8 +204,8 @@ response = requests.post(
**Good (Uses capability):**
```python
# ✅ Uses capability
from issue_tracker.backends.gitea import GiteaBackend
from issue_tracker.core.models import Issue, IssueState
from issue_core.backends.gitea import GiteaBackend
from issue_core.core.models import Issue, IssueState
from datetime import datetime, timezone
backend = GiteaBackend()
@@ -302,7 +302,7 @@ make integrate
make discover-capabilities
# Output:
# Found capabilities:
# - issue-facade (v1.0.0) - Issue tracking coordination
# - issue-core (v1.0.0) - Issue tracking coordination
# - ... (other capabilities)
# Auto-integrate all
@@ -318,7 +318,7 @@ Capabilities have priority scores (0-100) indicating importance:
- **50-69 (Medium):** Use when available
- **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.
@@ -367,7 +367,7 @@ Agents should check priority when deciding whether to use a capability or fall b
## 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.
**Q: What if agent bypasses capability anyway?**
@@ -405,10 +405,10 @@ response = requests.post(gitea_url + "/issues", ...)
**With capability (properly integrated):**
```python
# Agent checks .claude/capabilities/issue-facade.md
# Agent checks .claude/capabilities/issue-core.md
# Reads: "Use this API, don't use direct requests"
# Agent follows documented pattern:
from issue_tracker.backends.gitea import GiteaBackend
from issue_core.backends.gitea import GiteaBackend
backend = GiteaBackend()
backend.connect(config)
backend.create_issue(issue)

View File

@@ -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.**
@@ -25,15 +25,15 @@
# Verify installation
issue --version
# 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)
```python
from issue_tracker.backends.gitea import GiteaBackend
from issue_tracker.core.models import Issue, Label, IssueState, User, Comment
from issue_tracker.core.interfaces import IssueFilter
from issue_core.backends.gitea import GiteaBackend
from issue_core.core.models import Issue, Label, IssueState, User, Comment
from issue_core.core.interfaces import IssueFilter
from datetime import datetime, timezone
import os
@@ -228,7 +228,7 @@ if not verify_issue_backend():
## Error Handling
```python
from issue_tracker.backends.gitea.backend import GiteaAPIError
from issue_core.backends.gitea.backend import GiteaAPIError
try:
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
**Example:**
- "Create an issue for the bug I found" → **Use issue-facade**
- "Read the project README" → Don't need issue-facade
- "Check if issue #42 exists" → **Use issue-facade**
- "Clone the repository" → Don't need issue-facade
- "Create an issue for the bug I found" → **Use issue-core**
- "Read the project README" → Don't need issue-core
- "Check if issue #42 exists" → **Use issue-core**
- "Clone the repository" → Don't need issue-core

View File

@@ -1,14 +1,14 @@
#!/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
set -e
CAPABILITY_NAME="issue-facade"
CAPABILITY_NAME="issue-core"
CAPABILITY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$CAPABILITY_DIR/../.." && pwd)}"
echo "🔧 Issue Facade Capability Integration"
echo "🔧 Issue Core Capability Integration"
echo " Capability: $CAPABILITY_DIR"
echo " Project: $PROJECT_ROOT"
echo ""
@@ -90,7 +90,7 @@ case $choice in
echo "📝 Adding to Claude Code context..."
mkdir -p "$PROJECT_ROOT/.claude/capabilities"
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
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.
## Issue Tracking: issue-facade
## Issue Tracking: issue-core
**Location:** `capabilities/issue-facade/`
**Documentation:** `.claude/capabilities/issue-facade.md`
**Location:** `capabilities/issue-core/`
**Documentation:** `.claude/capabilities/issue-core.md`
**Priority:** CRITICAL (always use for issue operations)
**MUST USE FOR:**
@@ -120,13 +120,13 @@ This project uses specialized capabilities. Always check for existing capabiliti
**Quick Start:**
```python
from issue_tracker.backends.gitea import GiteaBackend
from issue_core.backends.gitea import GiteaBackend
backend = GiteaBackend()
backend.connect(config)
issues = backend.list_issues()
```
**Full Documentation:** See `.claude/capabilities/issue-facade.md`
**Full Documentation:** See `.claude/capabilities/issue-core.md`
EOF
echo "✓ Created $CONTEXT_FILE"
else
@@ -138,7 +138,7 @@ EOF
echo "✓ Added to Claude Code context"
echo ""
echo "Files created:"
echo " - $PROJECT_ROOT/.claude/capabilities/issue-facade.md"
echo " - $PROJECT_ROOT/.claude/capabilities/issue-core.md"
echo " - $CONTEXT_FILE"
;;
@@ -148,15 +148,15 @@ EOF
mkdir -p "$PROJECT_ROOT/.claude/commands"
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
**Python (Recommended):**
```python
from issue_tracker.backends.gitea import GiteaBackend
from issue_tracker.core.models import Issue, Label, IssueState
from issue_tracker.core.interfaces import IssueFilter
from issue_core.backends.gitea import GiteaBackend
from issue_core.core.models import Issue, Label, IssueState
from issue_core.core.interfaces import IssueFilter
backend = GiteaBackend()
backend.connect(config)
@@ -193,7 +193,7 @@ issue close 42 --comment="Fixed"
## Full Documentation
See `capabilities/issue-facade/AGENT_INTEGRATION.md` for:
See `capabilities/issue-core/AGENT_INTEGRATION.md` for:
- Complete API reference
- Coordination patterns
- Error handling
@@ -206,7 +206,7 @@ EOF
echo "Usage in Claude Code:"
echo " /use-issues"
echo ""
echo "This will inject issue-facade context into the conversation."
echo "This will inject issue-core context into the conversation."
;;
5)
@@ -250,7 +250,7 @@ EOF
mkdir -p "$PROJECT_ROOT/.claude/context"
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
CONTEXT_FILE="$PROJECT_ROOT/.claude/context/capabilities.md"
@@ -258,21 +258,21 @@ EOF
cat > "$CONTEXT_FILE" << 'EOF'
# Available Capabilities
## Issue Tracking: issue-facade
## Issue Tracking: issue-core
**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`
EOF
fi
# Create slash command
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`
**Examples:** See `capabilities/issue-facade/examples/agents/`
**Quick reference:** See `.claude/capabilities/issue-core.md`
**Examples:** See `capabilities/issue-core/examples/agents/`
**DO NOT use direct API calls or platform CLIs!**
EOF
@@ -285,7 +285,7 @@ EOF
echo ""
issue --version && echo "✓ CLI works" || echo "❌ CLI not working"
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"
echo ""
@@ -294,7 +294,7 @@ EOF
echo "Next steps:"
echo " 1. Test: issue list --limit=5"
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)

View File

@@ -1,4 +1,4 @@
# Issue Facade Integration Checklist
# Issue Core Integration Checklist
**For project maintainers integrating this capability into their codebase.**
@@ -13,7 +13,7 @@
- [ ] **Install capability:**
```bash
pip install -e capabilities/issue-facade/
pip install -e capabilities/issue-core/
```
- [ ] **Verify installation:**
@@ -42,8 +42,8 @@
- [ ] **Copy agent context to project:**
```bash
mkdir -p .claude/capabilities/
cp capabilities/issue-facade/.capability/agent-context.md \
.claude/capabilities/issue-facade.md
cp capabilities/issue-core/.capability/agent-context.md \
.claude/capabilities/issue-core.md
```
- [ ] **Add to Claude Code context:**
@@ -53,17 +53,17 @@
This project uses specialized capabilities. Always check these before implementing similar functionality.
## Issue Tracking: issue-facade
## Issue Tracking: issue-core
**Location:** `capabilities/issue-facade/`
**Documentation:** `.claude/capabilities/issue-facade.md`
**Location:** `capabilities/issue-core/`
**Documentation:** `.claude/capabilities/issue-core.md`
**CRITICAL:** Always use this capability for issue operations. Never use:
- Direct API calls (requests to /api/v1/repos/...)
- Platform CLIs (gh, glab)
- 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
@@ -71,11 +71,11 @@
- [ ] **Create slash command:**
Create `.claude/commands/use-issues.md`:
```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
from issue_tracker.backends.gitea import GiteaBackend
from issue_core.backends.gitea import GiteaBackend
backend = GiteaBackend()
backend.connect(config)
```
@@ -86,7 +86,7 @@
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!**
```
@@ -106,7 +106,7 @@
## Agent Configuration
- [ ] **Set agent identity:**
Add to `.issue-facade/config.json`:
Add to `.issue-core/config.json`:
```json
{
"agent": {
@@ -126,8 +126,8 @@
- [ ] **Test basic operations:**
```python
from issue_tracker.backends.gitea import GiteaBackend
from issue_tracker.core.interfaces import IssueFilter
from issue_core.backends.gitea import GiteaBackend
from issue_core.core.interfaces import IssueFilter
backend = GiteaBackend()
backend.connect({'base_url': '...', 'token': '...', 'owner': '...', 'repo': '...'})
@@ -153,26 +153,26 @@
```markdown
## 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:**
```bash
pip install -e capabilities/issue-facade/
pip install -e capabilities/issue-core/
export GITEA_API_TOKEN="your-token"
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:**
```markdown
### 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.
Examples: `capabilities/issue-facade/examples/agents/`
Examples: `capabilities/issue-core/examples/agents/`
```
## Security Review
@@ -180,9 +180,9 @@
- [ ] **Verify tokens are not in code:** `git grep GITEA_TOKEN` (should be empty)
- [ ] **Check .gitignore includes:**
```
.issue-facade/config.json
.issue-facade/issues.db
.issue-facade/credentials.json
.issue-core/config.json
.issue-core/issues.db
.issue-core/credentials.json
```
- [ ] **Audit token permissions:** Read-only for bots, write for implementation
@@ -192,7 +192,7 @@
- [ ] **Run capability tests:**
```bash
cd capabilities/issue-facade/
cd capabilities/issue-core/
make test
```
@@ -210,7 +210,7 @@
- [ ] **Schedule regular updates:**
```bash
cd capabilities/issue-facade/
cd capabilities/issue-core/
git pull origin main
pip install -e . --upgrade
```
@@ -232,7 +232,7 @@ If capability causes issues:
- [ ] **Keep backup config:**
```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**

View File

@@ -1,10 +1,10 @@
# Agent Integration Guide
**Issue Facade for Autonomous Coding Agent Coordination**
**Issue Core for Autonomous Coding Agent Coordination**
## Purpose
The **Issue Facade** capability provides a standardized interface for autonomous coding agents to coordinate project implementation through issue tracking. Instead of agents directly interfacing with platform-specific APIs (GitHub, GitLab, Gitea), they use a unified abstraction that works consistently across backends.
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?
@@ -67,7 +67,7 @@ Issue tracking provides a natural coordination mechanism for multi-agent softwar
### 1. Installation
```bash
cd capabilities/issue-facade
cd capabilities/issue-core
pip install -e .
```
@@ -99,7 +99,7 @@ issue backend set-default my-project
# Configure local SQLite backend
issue backend add local-work local
# Prompts for:
# - Database path: .issue-facade/issues.db
# - Database path: .issue-core/issues.db
issue backend set-default local-work
```
@@ -178,8 +178,8 @@ issue list --label=reviewed --state=closed --format=json | \
```python
# Agent creates implementation issues from requirements
from issue_tracker.backends.gitea import GiteaBackend
from issue_tracker.core.models import Issue, Label, IssueState
from issue_core.backends.gitea import GiteaBackend
from issue_core.core.models import Issue, Label, IssueState
from datetime import datetime, timezone
backend = GiteaBackend()
@@ -224,9 +224,9 @@ for task in subtasks:
### Python Integration
```python
from issue_tracker.backends.gitea import GiteaBackend
from issue_tracker.core.models import Issue, Label, IssueState, User
from issue_tracker.core.interfaces import IssueFilter
from issue_core.backends.gitea import GiteaBackend
from issue_core.core.models import Issue, Label, IssueState, User
from issue_core.core.interfaces import IssueFilter
from datetime import datetime, timezone
import os
@@ -271,7 +271,7 @@ created.state = IssueState.IN_PROGRESS
backend.update_issue(created)
# Add comment
from issue_tracker.core.models import Comment
from issue_core.core.models import Comment
comment = Comment(
id=None,
body="Analysis complete. Root cause: unclosed file handles in line 234",
@@ -432,7 +432,7 @@ issue sync push backup gitea-remote
```python
# Check for conflicts before sync
from issue_tracker.cli.sync_commands import sync_pull
from issue_core.cli.sync_commands import sync_pull
try:
sync_pull(source='remote', target='local', dry_run=True)
@@ -505,7 +505,7 @@ Create a setup script for each project:
#!/bin/bash
# setup-issue-tracking.sh
cat > .issue-facade-config << EOF
cat > .issue-core-config << EOF
GITEA_URL=https://gitea.example.com
GITEA_OWNER=myorg
GITEA_REPO=myproject
@@ -513,7 +513,7 @@ GITEA_TOKEN_FILE=~/.secrets/gitea-token
EOF
# Load config and configure backend
source .issue-facade-config
source .issue-core-config
export GITEA_API_TOKEN=$(cat $GITEA_TOKEN_FILE)
issue backend add $(basename $(pwd)) gitea <<INPUT
@@ -551,7 +551,7 @@ for issue_number in [1, 2, 3, 4, 5]:
backend.update_issue(issue)
# GOOD: Use local backend for bulk operations
from issue_tracker.backends.local import LocalSQLiteBackend
from issue_core.backends.local import LocalSQLiteBackend
local = LocalSQLiteBackend()
local.connect({'db_path': '/tmp/batch.db'})
@@ -595,7 +595,7 @@ if time.time() - last_fetch > 60:
### Phase 1: Auto-Configuration (v1.1)
- Automatic git remote detection
- Environment-variable-only setup
- Per-repository `.issue-facade/config.json` support
- Per-repository `.issue-core/config.json` support
- `issue config detect` command
### Phase 2: Agent Features (v1.2)

View File

@@ -1,8 +1,8 @@
# Issue Facade Capability Manifest
# Issue Core Capability Manifest
# This file describes the capability to coding agents and integration systems
metadata:
name: issue-facade
name: issue-core
version: 1.0.0
type: coordination-tool
description: >
@@ -44,7 +44,7 @@ integration:
methods:
python_api:
available: true
import: "from issue_tracker.backends.gitea import GiteaBackend"
import: "from issue_core.backends.gitea import GiteaBackend"
docs: "AGENT_INTEGRATION.md"
cli:
@@ -59,7 +59,7 @@ integration:
installation:
method: pip
command: "pip install -e capabilities/issue-facade/"
command: "pip install -e capabilities/issue-core/"
verify: "issue --version"
configuration:
@@ -119,8 +119,8 @@ credentials:
security:
- "Tokens never in code or logs"
- "Config stored in ~/.config/issue-facade/"
- "Per-repo config in .issue-facade/ (gitignored)"
- "Config stored in ~/.config/issue-core/"
- "Per-repo config in .issue-core/ (gitignored)"
best_practices:
- "Use read-only tokens for monitoring agents"
@@ -131,8 +131,8 @@ credentials:
agent_guidance:
quick_start: |
# For Python agents:
from issue_tracker.backends.gitea import GiteaBackend
from issue_tracker.core.interfaces import IssueFilter
from issue_core.backends.gitea import GiteaBackend
from issue_core.core.interfaces import IssueFilter
backend = GiteaBackend()
backend.connect(config)
@@ -177,7 +177,7 @@ feedback:
archived: "Resolved or outdated feedback"
for_users: |
Submit feedback about issue-facade:
Submit feedback about issue-core:
./.capability/feedback submit "Feedback text"
./.capability/feedback submit detailed-feedback.md
@@ -261,7 +261,7 @@ support:
solution: "Check GITEA_API_TOKEN is set and valid"
- 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)
priority:

View File

@@ -85,7 +85,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Enhanced agent integration documentation
### Fixed
- ID mapping bugs in issue-facade
- ID mapping bugs in issue-core
- Sync metadata handling
- 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
### 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)
- Gitea backend implementation (production-ready)
- Local SQLite backend (offline capability)
@@ -173,6 +173,6 @@ To submit feedback, see [feedback/README.md](feedback/README.md).
## Links
- [Repository](http://92.205.130.254:32166/coulomb/issue-facade)
- [Issues](http://92.205.130.254:32166/coulomb/issue-facade/issues)
- [Repository](http://92.205.130.254:32166/coulomb/issue-core)
- [Issues](http://92.205.130.254:32166/coulomb/issue-core/issues)
- [MarkiTect Project](https://github.com/markitect)

View File

@@ -4,28 +4,28 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## 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
### Installation & Setup
- Install for development: `pip install -e ".[dev]"`
- Install production: `pip install -e .`
- Clean build artifacts: `make issue-facade-clean`
- Clean build artifacts: `make issue-core-clean`
### Testing
- Run all tests: `pytest tests/`
- 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`
### Code Quality
- Run linter: `make issue-facade-lint`
- Format code: `black issue_tracker/ tests/` (line length: 100)
- Sort imports: `isort issue_tracker/ tests/`
- Run linter: `make issue-core-lint`
- Format code: `black issue_core/ tests/` (line length: 100)
- Sort imports: `isort issue_core/ tests/`
### 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:
- `issue list` - List issues
@@ -44,18 +44,18 @@ The codebase implements a **plugin-based facade pattern** with clear separation
```
┌─────────────────────────────────────────┐
│ CLI Layer (Click) │
│ issue_tracker/cli/*.py │
│ issue_core/cli/*.py │
└───────────────┬─────────────────────────┘
┌───────────────▼─────────────────────────┐
│ Core Domain Models │
│ issue_tracker/core/models.py │
│ issue_core/core/models.py │
│ (Issue, Label, User, etc.) │
└───────────────┬─────────────────────────┘
┌───────────────▼─────────────────────────┐
│ Backend Interface (ABC) │
│ issue_tracker/core/interfaces.py │
│ issue_core/core/interfaces.py │
│ IssueBackend, LocalBackend, │
│ RemoteBackend, SyncableBackend │
└───────────────┬─────────────────────────┘
@@ -70,7 +70,7 @@ The codebase implements a **plugin-based facade pattern** with clear separation
### 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
- **Label**: Supports categorization (priority/type/status/other) with cached properties
- **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.
#### 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
- **LocalBackend, RemoteBackend**: Marker interfaces for backend categorization
- **SyncableBackend**: Interface for backends supporting synchronization
@@ -94,19 +94,19 @@ The Issue model uses `@cached_property` for performance optimization and include
#### 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`
- Full offline functionality
- Serves as synchronization source of truth
- 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
- Rate limiting and error handling
- ID mapping between local and remote issues
- 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
- **commands.py**: Core issue operations (list, show, create, close)
- **backend_commands.py**: Backend management (add, list, switch)
@@ -150,7 +150,7 @@ Run only integration tests: `pytest -m integration`
### 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`)
3. Implement all abstract methods from the interface
4. Define `BackendCapabilities` to specify supported features
@@ -161,7 +161,7 @@ Run only integration tests: `pytest -m integration`
### Modifying the Issue Model
When changing `issue_tracker/core/models.py`:
When changing `issue_core/core/models.py`:
1. Update the `Issue` dataclass definition
2. Update `to_dict()` serialization method
3. Invalidate caches if adding/modifying label-dependent properties
@@ -181,7 +181,7 @@ When changing `issue_tracker/core/models.py`:
## Configuration
### Project Configuration (`pyproject.toml`)
- Entry points: `issue` and `issue-tracker` commands
- Entry points: `issue` and `issue-core` commands
- Dependencies: click, requests, python-dateutil
- Optional dependencies: dev, docs, gitea, github, jira
- Code style: Black (line-length=100), isort (profile="black")
@@ -189,7 +189,7 @@ When changing `issue_tracker/core/models.py`:
### Makefile Integration
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
- Uses `pip install -e` for editable installation
@@ -239,7 +239,7 @@ When implementing sync:
## 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 .`
- Integrates with parent project via Makefile targets
- Follows markitect capability conventions for structure and naming
@@ -268,7 +268,7 @@ 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**
```bash
@@ -294,8 +294,8 @@ EOF
**Option 3: From master project**
```bash
cd my-master-project
echo "Feedback about issue-facade..." > feedback.md
cp feedback.md capabilities/issue-facade/feedback/inbound/$(date +%Y%m%d)-feedback.md
echo "Feedback about issue-core..." > feedback.md
cp feedback.md capabilities/issue-core/feedback/inbound/$(date +%Y%m%d)-feedback.md
```
### For Maintainers: Processing Feedback

116
INTENT.md Normal file
View 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/`.

View File

@@ -1,14 +1,14 @@
# Issue Facade Capability Makefile
# Issue Core Capability Makefile
# Universal CLI for issue tracking across multiple backends
# Capability metadata
CAPABILITY_NAME := issue-facade
CAPABILITY_NAME := issue-core
CAPABILITY_DESCRIPTION := Universal CLI for issue tracking across multiple backends
# Default target
.PHONY: help
help: ## Show issue facade capability help
@echo "🎯 Issue Facade - Universal Issue Tracking CLI"
help: ## Show issue core capability help
@echo "🎯 Issue Core - Universal Issue Tracking CLI"
@echo "==============================================="
@echo ""
@echo "Core Issue Operations:"
@@ -30,7 +30,7 @@ help: ## Show issue facade capability help
@echo " issue-sync-push Push local issues to remote"
@echo ""
@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 " test Run all tests"
@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 ""
@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-stats Show feedback statistics"
@echo " feedback-show FILE=\"...\" Show specific feedback"
@echo " feedback-review FILE=\"...\" Review feedback (maintainers)"
@echo ""
@echo "Development & Setup (from parent):"
@echo " issue-facade-install Install issue facade capability"
@echo " issue-facade-install-dev Install with development dependencies"
@echo " issue-facade-test Run issue facade tests"
@echo " issue-facade-test-cov Run tests with coverage report"
@echo " issue-facade-lint Run code quality checks"
@echo " issue-facade-clean Clean build artifacts"
@echo " issue-core-install Install issue core capability"
@echo " issue-core-install-dev Install with development dependencies"
@echo " issue-core-test Run issue core tests"
@echo " issue-core-test-cov Run tests with coverage report"
@echo " issue-core-lint Run code quality checks"
@echo " issue-core-clean Clean build artifacts"
@echo ""
@echo "CLI Functionality:"
@echo " issue-facade-help Show CLI help documentation"
@echo " issue-facade-demo Demonstrate facade functionality"
@echo " issue-core-help Show CLI help documentation"
@echo " issue-core-demo Demonstrate facade functionality"
# Check if issue command is available
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
ifndef ISSUE_CLI
@echo "❌ Issue facade not installed"
@echo " Install with: make issue-facade-install"
@echo " Install with: make issue-core-install"
@exit 1
endif
issue list
@@ -182,7 +182,7 @@ integrate: ## Integrate capability into main project (interactive)
@./.capability/integrate.sh
.PHONY: install
install: ## Install issue facade (local development)
install: ## Install issue core (local development)
pip install -e .
.PHONY: install-dev
@@ -203,7 +203,7 @@ test-integration: ## Run integration tests only (local development)
.PHONY: test-cov
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
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)
@./.capability/feedback review "$(FILE)" --create-issue
.PHONY: issue-facade-install
issue-facade-install: ## Install issue facade capability
pip install -e capabilities/issue-facade/
.PHONY: issue-core-install
issue-core-install: ## Install issue core capability
pip install -e capabilities/issue-core/
.PHONY: issue-facade-install-dev
issue-facade-install-dev: ## Install issue facade capability with development dependencies
pip install -e "capabilities/issue-facade/[dev]"
.PHONY: issue-core-install-dev
issue-core-install-dev: ## Install issue core capability with development dependencies
pip install -e "capabilities/issue-core/[dev]"
.PHONY: issue-facade-test
issue-facade-test: ## Run issue facade tests
cd capabilities/issue-facade && pytest tests/
.PHONY: issue-core-test
issue-core-test: ## Run issue core tests
cd capabilities/issue-core && pytest tests/
.PHONY: issue-facade-test-cov
issue-facade-test-cov: ## Run tests with coverage report
cd capabilities/issue-facade && pytest tests/ --cov=issue_tracker --cov-report=html --cov-report=term
.PHONY: issue-core-test-cov
issue-core-test-cov: ## Run tests with coverage report
cd capabilities/issue-core && pytest tests/ --cov=issue_core --cov-report=html --cov-report=term
.PHONY: issue-facade-lint
issue-facade-lint: ## Run code quality checks
@echo "🔍 Running code quality checks for issue-facade..."
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"
.PHONY: issue-core-lint
issue-core-lint: ## Run code quality checks
@echo "🔍 Running code quality checks for issue-core..."
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"
.PHONY: issue-facade-clean
issue-facade-clean: ## Clean build artifacts
cd capabilities/issue-facade && rm -rf build/ dist/ *.egg-info/ __pycache__/ .pytest_cache/ htmlcov/ .coverage
find capabilities/issue-facade -name "*.pyc" -delete
find capabilities/issue-facade -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
.PHONY: issue-core-clean
issue-core-clean: ## Clean build artifacts
cd capabilities/issue-core && rm -rf build/ dist/ *.egg-info/ __pycache__/ .pytest_cache/ htmlcov/ .coverage
find capabilities/issue-core -name "*.pyc" -delete
find capabilities/issue-core -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
# CLI Functionality
.PHONY: issue-facade-help
issue-facade-help: ## Show CLI help documentation
.PHONY: issue-core-help
issue-core-help: ## Show CLI help documentation
ifndef ISSUE_CLI
@echo "❌ Issue facade not installed"
@echo " Install with: make issue-facade-install"
@echo " Install with: make issue-core-install"
@exit 1
endif
issue --help
.PHONY: issue-facade-demo
issue-facade-demo: ## Demonstrate facade functionality
@echo "🎬 Issue Facade Demonstration"
.PHONY: issue-core-demo
issue-core-demo: ## Demonstrate facade functionality
@echo "🎬 Issue Core Demonstration"
@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 " • GitLab Issues"
@echo " • Gitea Issues"
@@ -290,7 +290,7 @@ issue-facade-demo: ## Demonstrate facade functionality
@echo ""
ifndef ISSUE_CLI
@echo "To try it out:"
@echo " 1. make issue-facade-install"
@echo " 1. make issue-core-install"
@echo " 2. make issue-backend-detect"
@echo " 3. make issue-list"
else

View File

@@ -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.**
## 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?
@@ -40,10 +40,25 @@ Issue tracking provides natural coordination primitives for multi-agent software
### Installation
```bash
cd capabilities/issue-facade
pip install -e .
cd capabilities/issue-core
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)
**For Gitea-backed projects:**
@@ -67,7 +82,7 @@ issue backend test myproject
```bash
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
```
@@ -108,8 +123,8 @@ issue close 42 --comment="Ready for review"
Quick example:
```python
from issue_tracker.backends.gitea import GiteaBackend
from issue_tracker.core.interfaces import IssueFilter
from issue_core.backends.gitea import GiteaBackend
from issue_core.core.interfaces import IssueFilter
# Initialize
backend = GiteaBackend()
@@ -281,20 +296,20 @@ make test-unit
```bash
# Run linter
make issue-facade-lint
make issue-core-lint
# Format code
black issue_tracker/ tests/
black issue_core/ tests/
# Type check
mypy issue_tracker/
mypy issue_core/
```
### Project Structure
```
issue-facade/
├── issue_tracker/
issue-core/
├── issue_core/
│ ├── core/ # Domain models and interfaces
│ │ ├── models.py # Issue, Label, User, etc.
│ │ └── interfaces.py # IssueBackend, SyncableBackend
@@ -351,7 +366,7 @@ issue-facade/
## 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 |
| Offline capability | ✅ Local SQLite | ❌ No | ❌ No |
@@ -362,7 +377,7 @@ issue-facade/
## Contributing
The Issue Facade is designed to be extensible:
The Issue Core is designed to be extensible:
**To add a new backend:**
1. Implement the `IssueBackend` interface (see `core/interfaces.py`)

View File

@@ -1,4 +1,4 @@
# Issue Facade Roadmap
# Issue Core Roadmap
**Long-term vision and implementation plan for agent-driven software development coordination.**
@@ -29,7 +29,7 @@
**Implementation:**
```python
# issue_tracker/core/detection.py
# issue_core/core/detection.py
def detect_git_remote() -> Optional[Dict[str, str]]:
"""
@@ -64,7 +64,7 @@ def parse_remote_url(url: str) -> Optional[Dict[str, str]]:
**Implementation:**
```python
# issue_tracker/core/env_config.py
# issue_core/core/env_config.py
def load_backend_from_env() -> Optional[Dict[str, Any]]:
"""
@@ -95,7 +95,7 @@ issue config auto
**Implementation:**
```
.issue-facade/
.issue-core/
├── config.json # Repository-specific settings
├── issues.db # Local cache/backup
└── credentials.json # Optional encrypted credentials
@@ -126,10 +126,10 @@ issue config auto
**Functions:**
```python
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()):
"""Save config to .issue-facade/config.json."""
"""Save config to .issue-core/config.json."""
def find_repo_root() -> Optional[Path]:
"""Walk up directory tree to find git root."""
@@ -141,14 +141,14 @@ def find_repo_root() -> Optional[Path]:
**Implementation:**
```python
# issue_tracker/core/auto_config.py
# issue_core/core/auto_config.py
def auto_configure_backend() -> IssueBackend:
"""
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
3. Check global config (~/.config/issue-facade/)
3. Check global config (~/.config/issue-core/)
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
# Create timestamped backup
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
BACKUP_FILE="$CONFIG_FILE.backup.$TIMESTAMP"
cp "$CONFIG_FILE" "$BACKUP_FILE"
@@ -255,7 +255,7 @@ fi
**Implementation:**
```python
# issue_tracker/core/agent.py
# issue_core/core/agent.py
@dataclass
class AgentContext:
@@ -269,7 +269,7 @@ def get_agent_context() -> AgentContext:
"""
Get agent context from:
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
"""
@@ -310,7 +310,7 @@ issue config agent show
**Implementation:**
```python
# issue_tracker/core/locking.py
# issue_core/core/locking.py
class IssueClaim:
issue_id: str
@@ -423,7 +423,7 @@ def get_agent_state(issue: Issue) -> Dict[str, Any]:
**Implementation:**
```python
# issue_tracker/core/webhooks.py
# issue_core/core/webhooks.py
class WebhookManager:
"""Manage webhooks for real-time notifications."""
@@ -525,7 +525,7 @@ issue depends ready # List issues ready to start
**Implementation:**
```python
# issue_tracker/core/query_dsl.py
# issue_core/core/query_dsl.py
class QueryParser:
"""
@@ -559,7 +559,7 @@ issue list --query="is:in-progress created:>7d"
**Implementation:**
```python
# issue_tracker/core/activity.py
# issue_core/core/activity.py
@dataclass
class ActivityEvent:
@@ -596,7 +596,7 @@ class ActivityStream:
**Implementation:**
```python
# issue_tracker/core/distributed_lock.py
# issue_core/core/distributed_lock.py
class DistributedLockManager:
"""Distributed locking using Redis/database."""
@@ -638,7 +638,7 @@ with distributed_lock(f"issue:{issue_id}", agent_id):
**Implementation:**
```python
# issue_tracker/core/sync_strategies.py
# issue_core/core/sync_strategies.py
class ConflictResolutionStrategy(ABC):
def resolve(self, local: Issue, remote: Issue) -> Issue:

View File

@@ -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.
**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
**Characteristics:**
@@ -94,7 +94,7 @@ CAPABILITY-authentication.yaml # Declares authentication capability
### Single Capability Repository Structure
```
issue-facade/ # Repository name (implementation)
issue-core/ # Repository name (implementation)
├── CAPABILITY-issue-tracking.yaml # Declares: provides "issue-tracking"
├── README.md # Human-readable documentation
├── feedback/ # Visible: user interface for feedback
@@ -105,7 +105,7 @@ issue-facade/ # Repository name (implementation)
│ ├── feedback # Feedback CLI tool
│ ├── integrate.sh # Integration script
│ └── ...
├── issue_tracker/ # Core implementation code
├── issue_core/ # Core implementation code
│ ├── core/
│ ├── backends/
│ └── cli/
@@ -141,7 +141,7 @@ unified-devtools/ # Repository providing multiple capab
│ ├── feedback-collection/
│ └── common/
├── src/
│ ├── issue_tracker/
│ ├── issue_core/
│ ├── feedback_tool/
│ └── doc_generator/
└── tests/
@@ -162,8 +162,8 @@ Traditional approaches create deep directory trees:
```
my-project/
└── capabilities/
└── issue-facade/
└── issue_tracker/
└── issue-core/
└── issue_core/
└── backends/
└── gitea/
└── 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)**
```
my-project/
├── _issue-facade/ # Integrated capability (flat!)
│ └── issue_tracker/ # Only 2-3 levels deep
├── _issue-core/ # Integrated capability (flat!)
│ └── issue_core/ # Only 2-3 levels deep
│ └── backends/
├── _feedback-tool/ # Another integrated capability
├── src/ # Core project code
@@ -190,7 +190,7 @@ my-project/
```
my-project/
├── _issue-tracking/ # Capability family
│ └── issue-facade/ # Implementation (if multiple needed)
│ └── issue-core/ # Implementation (if multiple needed)
├── _feedback-collection/
│ └── feedback-tool/
└── src/
@@ -200,7 +200,7 @@ my-project/
```
my-project/
├── c/ # "c" for capabilities
│ ├── issue-facade/
│ ├── issue-core/
│ └── feedback-tool/
└── src/
```
@@ -208,7 +208,7 @@ my-project/
**Recommendation: Option A (Implementation-Based)**
**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"
- **Discoverable** - `ls _*/` shows all integrated capabilities
- **No ambiguity** - implementation name is explicit
@@ -217,7 +217,7 @@ my-project/
**Example:**
```
my-project/
├── _issue-facade/ # Issue tracking via issue-facade
├── _issue-core/ # Issue tracking via issue-core
├── _auth-service/ # Authentication via auth-service
├── _postgres-tools/ # Database tools
├── src/ # Core project code
@@ -236,11 +236,11 @@ my-project/
ls -d _*/
# Read capability specs
cat _issue-facade/CAPABILITY-*.yaml
cat _issue-core/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
metadata:
family: issue-tracking
implementation: issue-facade
implementation: issue-core
version: 1.0.0
description: >
Unified interface for issue tracking across Gitea, GitHub, GitLab.
@@ -276,7 +276,7 @@ integration:
mcp_server: false # planned
installation:
command: "pip install -e _issue-facade/"
command: "pip install -e _issue-core/"
verify: "issue --version"
documentation:
@@ -295,7 +295,7 @@ feedback:
# CAPABILITY-issue-tracking.yaml
metadata:
family: issue-tracking
implementation: issue-facade
implementation: issue-core
version: 1.0.0
maturity: production # experimental, beta, production
@@ -338,7 +338,7 @@ integration:
methods:
python_api:
available: true
import: "from issue_tracker.backends.gitea import GiteaBackend"
import: "from issue_core.backends.gitea import GiteaBackend"
docs: "AGENT_INTEGRATION.md"
cli:
@@ -354,7 +354,7 @@ integration:
installation:
method: pip
command: "pip install -e _issue-facade/"
command: "pip install -e _issue-core/"
verify: "issue --version"
configuration:
@@ -421,8 +421,8 @@ credentials:
security:
- "Tokens never in code or logs"
- "Config stored in ~/.config/issue-facade/"
- "Per-repo config in .issue-facade/ (gitignored)"
- "Config stored in ~/.config/issue-core/"
- "Per-repo config in .issue-core/ (gitignored)"
# Documentation references
documentation:
@@ -504,13 +504,13 @@ cat feedback/README.md
**Integrate a capability:**
```bash
# 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
git submodule add https://github.com/markitect/issue-facade _issue-facade
git submodule add https://github.com/markitect/issue-core _issue-core
# Install
pip install -e _issue-facade/
pip install -e _issue-core/
# Verify
issue --version
@@ -546,18 +546,18 @@ def find_capability_family(repo_path, family_name):
return None
# Usage
capabilities = discover_capabilities("_issue-facade")
capabilities = discover_capabilities("_issue-core")
# 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']}")
# Output: Found: issue-facade
# Output: Found: issue-core
```
**Use capability via natural language understanding:**
```python
# Agent reads capability spec
spec = find_capability_family("_issue-facade", "issue-tracking")
spec = find_capability_family("_issue-core", "issue-tracking")
# Agent understands use-cases
use_cases = spec['purpose']['use_cases']
@@ -566,7 +566,7 @@ use_cases = spec['purpose']['use_cases']
# Agent discovers integration methods
if spec['integration']['methods']['python_api']['available']:
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
exec(import_statement)
@@ -616,7 +616,7 @@ metadata:
# This capability integrates other capabilities
integrates:
- family: issue-tracking
implementation: issue-facade
implementation: issue-core
version: ">=1.0.0"
reason: "Uses issue tracking for task management"
@@ -635,12 +635,12 @@ integrates:
```
pm-tool/
├── CAPABILITY-project-management.yaml
├── _issue-facade/ # Integrated capability
├── _issue-core/ # Integrated capability
├── _feedback-tool/ # Integrated capability
├── _doc-gen/ # Integrated capability
├── src/
│ └── pm/
│ ├── tasks.py # Uses issue-facade
│ ├── tasks.py # Uses issue-core
│ ├── feedback.py # Uses feedback-tool
│ └── docs.py # Uses doc-gen
└── README.md
@@ -660,7 +660,7 @@ integrates:
**Multiple valid integrations:**
```
pm-tool/
├── _issue-facade/ # Option 1: issue-facade implementation
├── _issue-core/ # Option 1: issue-core implementation
└── ...
pm-tool/
@@ -740,7 +740,7 @@ Project C needs Gitea issues
→ Pattern: "We need 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 3: jira-bridge (JIRA connector)
@@ -757,7 +757,7 @@ All implement "issue-tracking" family with different trade-offs
**Implementation Versions:**
- Implementations evolve independently
- 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:**
```yaml
@@ -765,7 +765,7 @@ All implement "issue-tracking" family with different trade-offs
metadata:
family: issue-tracking
family_version: "1.x" # Compatible with v1 family spec
implementation: issue-facade
implementation: issue-core
version: 1.2.3 # Implementation version
```
@@ -785,9 +785,9 @@ metadata:
### 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
3. **Use Underscore Prefix** - `_issue-facade/` signals "integrated"
3. **Use Underscore Prefix** - `_issue-core/` signals "integrated"
4. **Provide Feedback** - Use `feedback/` to guide improvement
5. **Read the Spec** - `CAPABILITY-*.yaml` is the contract
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"
**Benefits:**
- Visual distinction: `_issue-facade/` vs `src/`
- Visual distinction: `_issue-core/` vs `src/`
- Flatter hierarchy: 2-3 levels vs 5-6 levels
- Easy discovery: `ls _*/` lists all integrations
- 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-v2.yaml # Experimental, next-gen backend
├── src/
│ ├── v1/ # issue-facade-classic
│ └── v2/ # issue-facade-next
│ ├── v1/ # issue-core-classic
│ └── v2/ # issue-core-next
└── README.md
```
@@ -896,10 +896,10 @@ Agents consider:
```python
# Agent logic
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
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
best = max(production_ready, key=lambda c: c['metadata']['version'])
@@ -986,13 +986,13 @@ devtools-suite/
```
my-saas-app/
├── _issue-facade/ # Issue tracking capability
├── _issue-core/ # Issue tracking capability
├── _auth-service/ # Authentication capability
├── _feedback-tool/ # Feedback collection capability
├── src/
│ └── myapp/
│ ├── api/
│ │ ├── issues.py # Uses _issue-facade
│ │ ├── issues.py # Uses _issue-core
│ │ └── auth.py # Uses _auth-service
│ ├── models/
│ └── main.py
@@ -1004,15 +1004,15 @@ my-saas-app/
**Installation:**
```bash
# 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/feedback-tool _feedback-tool
# 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
from issue_tracker.backends.gitea import GiteaBackend
from issue_core.backends.gitea import GiteaBackend
from auth_service import AuthManager
```

164
SCOPE.md Normal file
View 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
View 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.

View File

@@ -1,10 +1,10 @@
# 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
1. **Install issue-facade**:
1. **Install issue-core**:
```bash
cd ../..
pip install -e .

View File

@@ -27,9 +27,9 @@ from typing import Optional, List
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from issue_tracker.backends.gitea import GiteaBackend
from issue_tracker.core.models import Issue, Label, User, Comment, IssueState
from issue_tracker.core.interfaces import IssueFilter
from issue_core.backends.gitea import GiteaBackend
from issue_core.core.models import Issue, Label, User, Comment, IssueState
from issue_core.core.interfaces import IssueFilter
class HumanInLoopAgent:

View File

@@ -28,9 +28,9 @@ from typing import List, Dict
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from issue_tracker.backends.gitea import GiteaBackend
from issue_tracker.core.models import Issue, Label, User, Comment, IssueState
from issue_tracker.core.interfaces import IssueFilter
from issue_core.backends.gitea import GiteaBackend
from issue_core.core.models import Issue, Label, User, Comment, IssueState
from issue_core.core.interfaces import IssueFilter
class MonitoringAgent:

View File

@@ -36,9 +36,9 @@ from typing import List
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from issue_tracker.backends.gitea import GiteaBackend
from issue_tracker.core.models import Issue, Label, User, Comment, IssueState
from issue_tracker.core.interfaces import IssueFilter
from issue_core.backends.gitea import GiteaBackend
from issue_core.core.models import Issue, Label, User, Comment, IssueState
from issue_core.core.interfaces import IssueFilter
class BaseAgent:

View File

@@ -26,9 +26,9 @@ from pathlib import Path
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from issue_tracker.backends.gitea import GiteaBackend
from issue_tracker.core.models import Issue, Label, User, Comment, IssueState
from issue_tracker.core.interfaces import IssueFilter
from issue_core.backends.gitea import GiteaBackend
from issue_core.core.models import Issue, Label, User, Comment, IssueState
from issue_core.core.interfaces import IssueFilter
class SimpleTaskExecutor:

View File

@@ -1,6 +1,6 @@
# 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
@@ -8,7 +8,7 @@ This example demonstrates how to submit feedback about the issue-facade capabili
```bash
# Navigate to the capability directory
cd capabilities/issue-facade
cd capabilities/issue-core
# 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."
@@ -68,17 +68,17 @@ EOF
### 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
# From your master project root
cd ~/my-master-project
# Write feedback
cat > feedback-for-issue-facade.md << 'EOF'
cat > feedback-for-issue-core.md << 'EOF'
## 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
can use the same CLI for all our issue tracking.
@@ -91,8 +91,8 @@ We'd be happy to contribute or test!
EOF
# Copy to capability's feedback directory
cp feedback-for-issue-facade.md \
capabilities/issue-facade/feedback/inbound/$(date +%Y%m%d)-github-backend.md
cp feedback-for-issue-core.md \
capabilities/issue-core/feedback/inbound/$(date +%Y%m%d)-github-backend.md
```
## 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!**

View File

@@ -65,10 +65,10 @@ If you're integrating this capability into a master project:
```bash
cd my-master-project
echo "Feedback about issue-facade..." > feedback.md
echo "Feedback about issue-core..." > feedback.md
# 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
@@ -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.
```bash
cd capabilities/issue-facade
cd capabilities/issue-core
echo "Feedback..." > feedback/inbound/$(date +%Y%m%d)-feedback.md
```

View File

@@ -1,24 +1,24 @@
"""
Universal Issue Tracking System
A backend-agnostic issue tracking system that supports multiple backends
through a plugin architecture. Designed to be extracted into a standalone
repository for use across multiple projects.
Features:
- Unified issue model across all backends
- Plugin-based backend architecture
- Local SQLite backend for offline work
- Bidirectional synchronization
- CLI-first interface
- Support for GitHub-style and other issue tracking systems
Supported Backends:
- Local SQLite (for offline/standalone use)
- Gitea (GitHub-compatible API)
- Future: GitHub, GitLab, JIRA, Redmine, etc.
"""
__version__ = "0.1.0"
__author__ = "MarkiTect Project"
__description__ = "Universal Issue Tracking System with Plugin Architecture"
"""
issue-core — Authoritative Task Lifecycle Manager
The single observable place in the Coulomb org where tasks land —
regardless of whether they were created by a human, by activity-core,
or by an agent. Backend-agnostic via a plugin architecture.
Features:
- Unified issue model across all backends
- Plugin-based backend architecture
- Local SQLite backend for offline work
- Bidirectional synchronization
- CLI-first interface
- REST ingestion endpoint for activity-core's IssueSink
Supported Backends:
- Local SQLite (offline/standalone)
- Gitea (GitHub-compatible API)
- Future: GitHub, GitLab, JIRA, Redmine
"""
__version__ = "0.2.0"
__author__ = "Coulomb / MarkiTect Project"
__description__ = "Authoritative task lifecycle manager with plugin architecture"

View 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
View 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
View 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
View 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
View 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

View File

@@ -11,11 +11,12 @@ from pathlib import Path
from .commands import issue_group
from .backend_commands import backend_group
from .sync_commands import sync_group
from .serve_command import serve_command
from .. import __version__
@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('--backend', help='Backend to use (local, gitea)')
@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(backend_group, name='backend')
cli.add_command(sync_group, name='sync')
cli.add_command(serve_command)
# Convenience aliases - direct issue commands

View 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,
)

View File

@@ -3,8 +3,8 @@ requires = ["setuptools>=45", "wheel", "setuptools-scm[toml]>=6.2"]
build-backend = "setuptools.build_meta"
[project]
name = "universal-issue-tracker"
description = "Backend-agnostic issue tracking system with plugin architecture"
name = "issue-core"
description = "Authoritative task lifecycle manager for the Coulomb org — backend-agnostic with plugin architecture"
readme = "README.md"
requires-python = ">=3.8"
license = {text = "MIT"}
@@ -45,6 +45,9 @@ dev = [
"flake8>=4.0",
"mypy>=0.900",
"pre-commit>=2.0",
"httpx>=0.27",
"fastapi>=0.110,<1.0",
"pydantic>=2.0,<3.0",
]
docs = [
"sphinx>=4.0",
@@ -60,25 +63,30 @@ github = [
jira = [
"jira>=3.0",
]
api = [
"fastapi>=0.110,<1.0",
"uvicorn[standard]>=0.27,<1.0",
"pydantic>=2.0,<3.0",
]
[project.urls]
Homepage = "https://github.com/markitect/universal-issue-tracker"
Documentation = "https://universal-issue-tracker.readthedocs.io/"
Repository = "https://github.com/markitect/universal-issue-tracker.git"
"Bug Tracker" = "https://github.com/markitect/universal-issue-tracker/issues"
Homepage = "https://github.com/coulomb/issue-core"
Documentation = "https://issue-core.readthedocs.io/"
Repository = "https://github.com/coulomb/issue-core.git"
"Bug Tracker" = "https://github.com/coulomb/issue-core/issues"
[project.scripts]
issue = "issue_tracker.cli.main:main"
issue-tracker = "issue_tracker.cli.main:main"
issue = "issue_core.cli.main:main"
issue-core = "issue_core.cli.main:main"
[tool.setuptools]
packages = ["issue_tracker"]
[tool.setuptools.packages.find]
include = ["issue_core*"]
[tool.setuptools.dynamic]
version = {attr = "issue_tracker.__version__"}
version = {attr = "issue_core.__version__"}
[tool.setuptools.package-data]
issue_tracker = ["backends/local/schema.sql"]
issue_core = ["backends/local/schema.sql"]
[tool.black]
line-length = 100
@@ -101,7 +109,7 @@ extend-exclude = '''
[tool.isort]
profile = "black"
line_length = 100
known_first_party = ["issue_tracker"]
known_first_party = ["issue_core"]
[tool.mypy]
python_version = "3.8"
@@ -142,7 +150,7 @@ markers = [
]
[tool.coverage.run]
source = ["issue_tracker"]
source = ["issue_core"]
omit = [
"*/tests/*",
"*/test_*",

View File

@@ -1 +1 @@
"""Test suite for issue-facade capability."""
"""Test suite for issue-core capability."""

177
tests/test_api_ingest.py Normal file
View 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"]

View File

@@ -12,8 +12,8 @@ from pathlib import Path
from click.testing import CliRunner
from unittest.mock import Mock, patch, MagicMock
from issue_tracker.cli.main import cli
from issue_tracker.cli.utils import load_backend_configs, save_backend_configs
from issue_core.cli.main import cli
from issue_core.cli.utils import load_backend_configs, save_backend_configs
class TestCLICommands:
@@ -37,9 +37,9 @@ class TestCLICommands:
assert result.exit_code == 0
# Should show either configured backends or "No backends configured"
@patch('issue_tracker.cli.backend_commands.load_backend_configs')
@patch('issue_tracker.cli.backend_commands.save_backend_configs')
@patch('issue_tracker.cli.backend_commands.test_backend_connection')
@patch('issue_core.cli.backend_commands.load_backend_configs')
@patch('issue_core.cli.backend_commands.save_backend_configs')
@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):
"""Test adding Gitea backend with environment token."""
# Mock empty initial config
@@ -63,9 +63,9 @@ class TestCLICommands:
assert saved_config['test-gitea']['type'] == 'gitea'
assert saved_config['test-gitea']['token'] == 'test-token'
@patch('issue_tracker.cli.backend_commands.load_backend_configs')
@patch('issue_tracker.cli.backend_commands.save_backend_configs')
@patch('issue_tracker.cli.backend_commands.test_backend_connection')
@patch('issue_core.cli.backend_commands.load_backend_configs')
@patch('issue_core.cli.backend_commands.save_backend_configs')
@patch('issue_core.cli.backend_commands.test_backend_connection')
def test_backend_add_local(self, mock_test_conn, mock_save, mock_load):
"""Test adding local backend."""
mock_load.return_value = {}
@@ -78,7 +78,7 @@ class TestCLICommands:
assert result.exit_code == 0
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):
"""Test issue show command."""
# Mock backend and issue
@@ -104,7 +104,7 @@ class TestCLICommands:
assert 'Test description' 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):
"""Test issue show command when issue doesn't exist."""
mock_backend = Mock()
@@ -121,7 +121,7 @@ class TestCLICommands:
result = self.runner.invoke(cli, ['--version'])
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):
"""Test basic list command functionality."""
# This test will help us identify the existing bug
@@ -157,7 +157,7 @@ class TestBackendConfiguration:
def test_config_directory_creation(self):
"""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()
assert config_dir.exists()
@@ -177,7 +177,7 @@ class TestBackendConfiguration:
}
# 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)
# Test loading
@@ -190,7 +190,7 @@ class TestBackendConfiguration:
with tempfile.TemporaryDirectory() as temp_dir:
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()
assert config == {}
@@ -204,13 +204,13 @@ class TestEnvironmentTokenDetection:
"""Test GITEA_API_TOKEN environment variable detection."""
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()
with patch('issue_tracker.cli.backend_commands.load_backend_configs', return_value={}):
with patch('issue_tracker.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.load_backend_configs', return_value={}):
with patch('issue_core.cli.backend_commands.save_backend_configs'):
with patch('issue_core.cli.backend_commands.test_backend_connection', return_value=True):
result = runner.invoke(add_backend, [
'test-gitea', 'gitea'
], input='https://git.example.com\ntestorg\ntestrepo\n')

View File

@@ -7,7 +7,7 @@ including state management, validation, and business logic.
import pytest
from datetime import datetime, timezone
from issue_tracker.core.models import (
from issue_core.core.models import (
Issue, Label, User, Milestone, Comment,
IssueState, Priority, IssueType, LabelCategories
)

View File

@@ -7,7 +7,7 @@ These tests ensure the Gitea backend works correctly with the API.
import pytest
import json
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:
@@ -32,7 +32,7 @@ class TestGiteaBackend:
assert self.backend.repo is 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):
"""Test successful connection to Gitea API."""
mock_session = MagicMock()
@@ -61,7 +61,7 @@ class TestGiteaBackend:
'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):
"""Test failed connection raises appropriate error."""
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]
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):
"""Test test_connection method works correctly."""
mock_session = MagicMock()
@@ -117,7 +117,7 @@ class TestGiteaBackend:
result = backend.test_connection()
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):
"""Test test_connection handles failures gracefully."""
mock_session = MagicMock()
@@ -139,7 +139,7 @@ class TestGiteaBackend:
result = backend.test_connection()
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):
"""Test successful issue retrieval."""
mock_session = MagicMock()

View File

@@ -10,9 +10,9 @@ import tempfile
from datetime import datetime, timezone, timedelta
from pathlib import Path
from issue_tracker.backends.local.backend import LocalSQLiteBackend
from issue_tracker.core.models import Issue, Label, User, Milestone, Comment, IssueState
from issue_tracker.core.interfaces import IssueFilter
from issue_core.backends.local.backend import LocalSQLiteBackend
from issue_core.core.models import Issue, Label, User, Milestone, Comment, IssueState
from issue_core.core.interfaces import IssueFilter
@pytest.mark.unit

View File

@@ -2,34 +2,34 @@
id: ISSC-WP-0001
type: workplan
domain: custodian
repo: issue-facade
status: active
repo: issue-core
status: done
state_hub_workstream_id: 1135fc1d-1f46-4e35-886d-04cc3b8050b6
tasks:
- id: T01
title: Rename package issue-facade → issue-core throughout
state_hub_task_id: b7054428-82a9-4d81-bfa8-5b5ee2eaf69f
status: todo
status: done
- id: T02
title: Register issue-core in state hub under capabilities domain
state_hub_task_id: b1d36996-44ff-48b9-b208-709d6874453c
status: todo
status: done
- id: T03
title: Write INTENT.md
state_hub_task_id: 265c6338-0310-409d-a081-6446042f6274
status: todo
status: done
- id: T04
title: Update or write SCOPE.md
state_hub_task_id: f95ac730-7ba0-4eae-bcae-de1e7d24b164
status: todo
status: done
- id: T05
title: Implement task ingestion REST endpoint POST /issues/
state_hub_task_id: 26af07e4-c072-42ad-bb5c-facb196156c9
status: todo
status: done
- id: T06
title: Document NATS subscriber interface (design stub)
state_hub_task_id: dff61fed-1e8c-4eb3-bbd6-1e3742329945
status: todo
status: done
created: "2026-05-14"
---