From b605d970e34e9e9b0ce13ef8cd63ecc54d86da03 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 17 May 2026 05:16:27 +0200 Subject: [PATCH] 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 --- .capability/README.md | 46 ++--- .capability/agent-context.md | 20 +- .capability/integrate.sh | 48 ++--- .capability/integration-checklist.md | 50 ++--- AGENT_INTEGRATION.md | 30 +-- CAPABILITY-issue-tracking.yaml | 20 +- CHANGELOG.md | 8 +- CLAUDE.md | 46 ++--- INTENT.md | 116 ++++++++++++ Makefile | 92 ++++----- README.md | 43 +++-- ROADMAP.md | 36 ++-- ReusableCapabilitiesArchitecture.md | 96 +++++----- SCOPE.md | 164 ++++++++++++++++ docs/nats-task-ingestion.md | 143 ++++++++++++++ examples/agents/README.md | 4 +- examples/agents/human_in_loop.py | 6 +- examples/agents/monitoring_agent.py | 6 +- examples/agents/multi_agent_pipeline.py | 6 +- examples/agents/simple_task_executor.py | 6 +- examples/feedback-example.md | 16 +- feedback/README.md | 6 +- issue_core/__init__.py | 48 ++--- issue_core/api/__init__.py | 14 ++ issue_core/api/app.py | 26 +++ issue_core/api/auth.py | 66 +++++++ issue_core/api/ingest.py | 139 ++++++++++++++ issue_core/api/schemas.py | 50 +++++ issue_core/cli/main.py | 4 +- issue_core/cli/serve_command.py | 43 +++++ pyproject.toml | 36 ++-- tests/__init__.py | 2 +- tests/test_api_ingest.py | 177 ++++++++++++++++++ tests/test_cli_commands.py | 36 ++-- tests/test_core_models.py | 2 +- tests/test_gitea_backend.py | 12 +- tests/test_local_backend.py | 6 +- .../ISSC-WP-0001-rename-and-task-ingestion.md | 16 +- 38 files changed, 1324 insertions(+), 361 deletions(-) create mode 100644 INTENT.md create mode 100644 SCOPE.md create mode 100644 docs/nats-task-ingestion.md create mode 100644 issue_core/api/__init__.py create mode 100644 issue_core/api/app.py create mode 100644 issue_core/api/auth.py create mode 100644 issue_core/api/ingest.py create mode 100644 issue_core/api/schemas.py create mode 100644 issue_core/cli/serve_command.py create mode 100644 tests/test_api_ingest.py diff --git a/.capability/README.md b/.capability/README.md index 0185665..c7d1543 100644 --- a/.capability/README.md +++ b/.capability/README.md @@ -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) diff --git a/.capability/agent-context.md b/.capability/agent-context.md index 5f9a9f6..a22f70c 100644 --- a/.capability/agent-context.md +++ b/.capability/agent-context.md @@ -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 diff --git a/.capability/integrate.sh b/.capability/integrate.sh index b28d556..3e3e75b 100755 --- a/.capability/integrate.sh +++ b/.capability/integrate.sh @@ -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) diff --git a/.capability/integration-checklist.md b/.capability/integration-checklist.md index 601e2fa..d0ba8e8 100644 --- a/.capability/integration-checklist.md +++ b/.capability/integration-checklist.md @@ -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** diff --git a/AGENT_INTEGRATION.md b/AGENT_INTEGRATION.md index a6c1640..3839ffd 100644 --- a/AGENT_INTEGRATION.md +++ b/AGENT_INTEGRATION.md @@ -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 < 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) diff --git a/CAPABILITY-issue-tracking.yaml b/CAPABILITY-issue-tracking.yaml index b1344dc..0093c29 100644 --- a/CAPABILITY-issue-tracking.yaml +++ b/CAPABILITY-issue-tracking.yaml @@ -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: diff --git a/CHANGELOG.md b/CHANGELOG.md index 1602365..72e6252 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/CLAUDE.md b/CLAUDE.md index 759bcce..75b9e16 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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//` +1. Create backend package in `issue_core/backends//` 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 diff --git a/INTENT.md b/INTENT.md new file mode 100644 index 0000000..5d44e86 --- /dev/null +++ b/INTENT.md @@ -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/`. diff --git a/Makefile b/Makefile index a2bbf2f..ab44021 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index 7b7b8a6..e3a9a58 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# Issue Facade - Agent Coordination via Issue Tracking +# Issue Core - Agent Coordination via Issue Tracking **A unified interface for autonomous coding agents to coordinate project implementation through issue tracking systems.** ## 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 ` or `X-API-Key: `. +See `SCOPE.md` "TaskSpec payload" for the request schema, or visit +`http://:/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`) diff --git a/ROADMAP.md b/ROADMAP.md index 6b42727..46d8c5a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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: diff --git a/ReusableCapabilitiesArchitecture.md b/ReusableCapabilitiesArchitecture.md index 1b83ae9..3b1d2d2 100644 --- a/ReusableCapabilitiesArchitecture.md +++ b/ReusableCapabilitiesArchitecture.md @@ -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 _` 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 ``` diff --git a/SCOPE.md b/SCOPE.md new file mode 100644 index 0000000..d793d29 --- /dev/null +++ b/SCOPE.md @@ -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. diff --git a/docs/nats-task-ingestion.md b/docs/nats-task-ingestion.md new file mode 100644 index 0000000..279fc3c --- /dev/null +++ b/docs/nats-task-ingestion.md @@ -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. diff --git a/examples/agents/README.md b/examples/agents/README.md index 85ee962..6798ade 100644 --- a/examples/agents/README.md +++ b/examples/agents/README.md @@ -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 . diff --git a/examples/agents/human_in_loop.py b/examples/agents/human_in_loop.py index b848899..dddd0de 100755 --- a/examples/agents/human_in_loop.py +++ b/examples/agents/human_in_loop.py @@ -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: diff --git a/examples/agents/monitoring_agent.py b/examples/agents/monitoring_agent.py index b5c6fc6..719f6ea 100755 --- a/examples/agents/monitoring_agent.py +++ b/examples/agents/monitoring_agent.py @@ -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: diff --git a/examples/agents/multi_agent_pipeline.py b/examples/agents/multi_agent_pipeline.py index 25ef8e8..ecdebda 100755 --- a/examples/agents/multi_agent_pipeline.py +++ b/examples/agents/multi_agent_pipeline.py @@ -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: diff --git a/examples/agents/simple_task_executor.py b/examples/agents/simple_task_executor.py index 30f99a5..121db62 100755 --- a/examples/agents/simple_task_executor.py +++ b/examples/agents/simple_task_executor.py @@ -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: diff --git a/examples/feedback-example.md b/examples/feedback-example.md index ed9efb2..1b4dd62 100644 --- a/examples/feedback-example.md +++ b/examples/feedback-example.md @@ -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!** diff --git a/feedback/README.md b/feedback/README.md index f2075b4..ab5b2fb 100644 --- a/feedback/README.md +++ b/feedback/README.md @@ -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 ``` diff --git a/issue_core/__init__.py b/issue_core/__init__.py index cab4613..e9a0eee 100644 --- a/issue_core/__init__.py +++ b/issue_core/__init__.py @@ -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" \ No newline at end of file +""" +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" diff --git a/issue_core/api/__init__.py b/issue_core/api/__init__.py new file mode 100644 index 0000000..af0fb6b --- /dev/null +++ b/issue_core/api/__init__.py @@ -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"] diff --git a/issue_core/api/app.py b/issue_core/api/app.py new file mode 100644 index 0000000..c21aff4 --- /dev/null +++ b/issue_core/api/app.py @@ -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 diff --git a/issue_core/api/auth.py b/issue_core/api/auth.py new file mode 100644 index 0000000..548232d --- /dev/null +++ b/issue_core/api/auth.py @@ -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 ` or as `X-API-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"}, + ) diff --git a/issue_core/api/ingest.py b/issue_core/api/ingest.py new file mode 100644 index 0000000..c2faaf7 --- /dev/null +++ b/issue_core/api/ingest.py @@ -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, + ) diff --git a/issue_core/api/schemas.py b/issue_core/api/schemas.py new file mode 100644 index 0000000..bcd9fed --- /dev/null +++ b/issue_core/api/schemas.py @@ -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 diff --git a/issue_core/cli/main.py b/issue_core/cli/main.py index a6e8cb9..dafab62 100644 --- a/issue_core/cli/main.py +++ b/issue_core/cli/main.py @@ -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 diff --git a/issue_core/cli/serve_command.py b/issue_core/cli/serve_command.py new file mode 100644 index 0000000..d8651bd --- /dev/null +++ b/issue_core/cli/serve_command.py @@ -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, + ) diff --git a/pyproject.toml b/pyproject.toml index 72166b2..df79bb6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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_*", diff --git a/tests/__init__.py b/tests/__init__.py index c3ae855..23cebb9 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1 @@ -"""Test suite for issue-facade capability.""" \ No newline at end of file +"""Test suite for issue-core capability.""" diff --git a/tests/test_api_ingest.py b/tests/test_api_ingest.py new file mode 100644 index 0000000..de9d1d7 --- /dev/null +++ b/tests/test_api_ingest.py @@ -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"] diff --git a/tests/test_cli_commands.py b/tests/test_cli_commands.py index f05d481..b2d6af8 100644 --- a/tests/test_cli_commands.py +++ b/tests/test_cli_commands.py @@ -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') diff --git a/tests/test_core_models.py b/tests/test_core_models.py index 9cb3403..4fdf578 100644 --- a/tests/test_core_models.py +++ b/tests/test_core_models.py @@ -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 ) diff --git a/tests/test_gitea_backend.py b/tests/test_gitea_backend.py index 0a7338c..73df749 100644 --- a/tests/test_gitea_backend.py +++ b/tests/test_gitea_backend.py @@ -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() diff --git a/tests/test_local_backend.py b/tests/test_local_backend.py index 64d829f..4dc155e 100644 --- a/tests/test_local_backend.py +++ b/tests/test_local_backend.py @@ -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 diff --git a/workplans/ISSC-WP-0001-rename-and-task-ingestion.md b/workplans/ISSC-WP-0001-rename-and-task-ingestion.md index 0c0cdb1..b9642ff 100644 --- a/workplans/ISSC-WP-0001-rename-and-task-ingestion.md +++ b/workplans/ISSC-WP-0001-rename-and-task-ingestion.md @@ -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" ---