generated from coulomb/repo-seed
Compare commits
36 Commits
324453bd8d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 54a9749616 | |||
| a691f93f16 | |||
| 8d34c6d468 | |||
| e5172611ec | |||
| 9e46004961 | |||
| 11a0a69870 | |||
| 3c66148205 | |||
| 2f40dea6a1 | |||
| 8c01f07c2d | |||
| 7693ef8680 | |||
| 4854bda118 | |||
| 19d4bc96df | |||
| d50ab96a8a | |||
| 3a60623730 | |||
| 23a233c57e | |||
| 358464b4d4 | |||
| 12b356d94a | |||
| bed83be0ec | |||
| 3e29bc964d | |||
| 352a4d7969 | |||
| 026982ca4f | |||
| 89c1c8cc1e | |||
| 4635b0eb36 | |||
| d703463f16 | |||
| 041afc6311 | |||
| ed14952b17 | |||
| 2fd7de08d2 | |||
| b20eb11583 | |||
| f96ca46965 | |||
| b605d970e3 | |||
| 99ea1fbc45 | |||
| 663d1961cf | |||
| 70d7ec0cdc | |||
| f89772ac79 | |||
| 35daa514e5 | |||
| 1627fd9673 |
@@ -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
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
### 1. Self-Description (Machine-Readable)
|
||||
|
||||
**File:** `CAPABILITY.yaml`
|
||||
**File:** `CAPABILITY-issue-tracking.yaml`
|
||||
|
||||
Contains machine-readable metadata that agents and tooling can parse:
|
||||
- What the capability does
|
||||
@@ -29,7 +29,7 @@ Contains machine-readable metadata that agents and tooling can parse:
|
||||
**Usage:**
|
||||
```bash
|
||||
# Tools can parse this to understand capabilities
|
||||
yq eval '.purpose.primary' CAPABILITY.yaml
|
||||
yq eval '.purpose.primary' CAPABILITY-issue-tracking.yaml
|
||||
# Output: "Agent coordination via issue tracking"
|
||||
```
|
||||
|
||||
@@ -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
|
||||
│ ├── CAPABILITY.yaml # Machine-readable metadata
|
||||
│ └── 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()
|
||||
@@ -228,7 +228,7 @@ backend.create_issue(issue)
|
||||
**1. Create capability structure:**
|
||||
```
|
||||
capabilities/your-capability/
|
||||
├── CAPABILITY.yaml # Metadata
|
||||
├── CAPABILITY-issue-tracking.yaml # Metadata
|
||||
├── .capability/
|
||||
│ ├── agent-context.md # Agent guide
|
||||
│ ├── integrate.sh # Integration script
|
||||
@@ -237,7 +237,7 @@ capabilities/your-capability/
|
||||
└── your_package/ # Implementation
|
||||
```
|
||||
|
||||
**2. Write CAPABILITY.yaml:**
|
||||
**2. Write CAPABILITY-issue-tracking.yaml:**
|
||||
```yaml
|
||||
metadata:
|
||||
name: your-capability
|
||||
@@ -287,7 +287,7 @@ make integrate
|
||||
|
||||
### Manual Discovery (Human)
|
||||
1. Human sees `capabilities/` directory
|
||||
2. Reads `CAPABILITY.yaml` to understand what's available
|
||||
2. Reads `CAPABILITY-issue-tracking.yaml` to understand what's available
|
||||
3. Runs `make integrate` to set up for agents
|
||||
|
||||
### Automatic Discovery (Agent)
|
||||
@@ -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?**
|
||||
@@ -386,7 +386,7 @@ A: Document rollback in integration checklist. Keep backup configs. Have fallbac
|
||||
|
||||
**The capability bootstrap system works by:**
|
||||
|
||||
1. **Self-description** - Capability declares what it does (CAPABILITY.yaml)
|
||||
1. **Self-description** - Capability declares what it does (CAPABILITY-issue-tracking.yaml)
|
||||
2. **Context injection** - Integration copies docs to `.claude/capabilities/`
|
||||
3. **Agent discovery** - Agents check context before implementing
|
||||
4. **Natural preference** - Good docs + warnings make capability easier than alternatives
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
226
.capability/detach
Executable file
226
.capability/detach
Executable file
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env bash
|
||||
# detach - Remove this capability from an integrating project
|
||||
#
|
||||
# Usage:
|
||||
# From the capability root:
|
||||
# ./.capability/detach
|
||||
#
|
||||
# From integrating project:
|
||||
# _issue-facade/.capability/detach
|
||||
# # or
|
||||
# capabilities/issue-facade/.capability/detach
|
||||
#
|
||||
# This script helps cleanly remove a capability integration, creating
|
||||
# a manifest for potential re-integration with updated architecture.
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${BLUE}$1${NC}"; }
|
||||
success() { echo -e "${GREEN}$1${NC}"; }
|
||||
warn() { echo -e "${YELLOW}$1${NC}"; }
|
||||
error() { echo -e "${RED}Error: $1${NC}" >&2; exit 1; }
|
||||
|
||||
# Detect capability information
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CAPABILITY_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
CAPABILITY_NAME="$(basename "$CAPABILITY_ROOT")"
|
||||
|
||||
# Try to read capability metadata
|
||||
CAPABILITY_FILE=""
|
||||
if [ -f "$CAPABILITY_ROOT"/CAPABILITY-*.yaml ]; then
|
||||
CAPABILITY_FILE=$(ls "$CAPABILITY_ROOT"/CAPABILITY-*.yaml | head -1)
|
||||
CAPABILITY_FAMILY=$(basename "$CAPABILITY_FILE" .yaml | sed 's/^CAPABILITY-//')
|
||||
else
|
||||
warn "No CAPABILITY-*.yaml file found, using directory name"
|
||||
CAPABILITY_FAMILY="$CAPABILITY_NAME"
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}╔════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BLUE}║ Capability Detachment Tool ║${NC}"
|
||||
echo -e "${BLUE}╔════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
info "Capability: $CAPABILITY_NAME"
|
||||
info "Family: $CAPABILITY_FAMILY"
|
||||
info "Location: $CAPABILITY_ROOT"
|
||||
echo ""
|
||||
|
||||
# Detect parent project
|
||||
PARENT_DIR="$(dirname "$CAPABILITY_ROOT")"
|
||||
PARENT_NAME="$(basename "$PARENT_DIR")"
|
||||
|
||||
# Determine integration pattern
|
||||
INTEGRATION_PATTERN="unknown"
|
||||
if [[ "$CAPABILITY_NAME" == _* ]]; then
|
||||
INTEGRATION_PATTERN="underscore-prefix"
|
||||
elif [[ "$PARENT_NAME" == "capabilities" ]]; then
|
||||
INTEGRATION_PATTERN="capabilities-directory"
|
||||
elif [[ "$PARENT_NAME" == "c" ]]; then
|
||||
INTEGRATION_PATTERN="short-alias"
|
||||
else
|
||||
INTEGRATION_PATTERN="custom"
|
||||
fi
|
||||
|
||||
info "Integration pattern: $INTEGRATION_PATTERN"
|
||||
echo ""
|
||||
|
||||
# Safety check
|
||||
warn "⚠️ This will remove the capability integration from the parent project."
|
||||
echo ""
|
||||
echo "The following will happen:"
|
||||
echo " 1. Create detachment manifest (DETACHED-$CAPABILITY_NAME.yaml)"
|
||||
echo " 2. Remove capability directory: $CAPABILITY_ROOT"
|
||||
echo " 3. Clean up any integration artifacts"
|
||||
echo ""
|
||||
read -p "Continue? (yes/no): " confirm
|
||||
|
||||
if [[ "$confirm" != "yes" ]]; then
|
||||
info "Detachment cancelled."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
info "Creating detachment manifest..."
|
||||
|
||||
# Create detachment manifest
|
||||
MANIFEST_FILE="$PARENT_DIR/DETACHED-$CAPABILITY_NAME.yaml"
|
||||
|
||||
cat > "$MANIFEST_FILE" <<EOF
|
||||
# Detachment Manifest
|
||||
# This file records the removal of the $CAPABILITY_NAME capability
|
||||
# Use this information to re-integrate with updated architecture
|
||||
|
||||
detachment:
|
||||
timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
capability_name: $CAPABILITY_NAME
|
||||
capability_family: $CAPABILITY_FAMILY
|
||||
integration_pattern: $INTEGRATION_PATTERN
|
||||
original_location: $CAPABILITY_ROOT
|
||||
|
||||
capability_metadata:
|
||||
EOF
|
||||
|
||||
# Append capability metadata if available
|
||||
if [ -n "$CAPABILITY_FILE" ]; then
|
||||
echo " spec_file: $(basename "$CAPABILITY_FILE")" >> "$MANIFEST_FILE"
|
||||
|
||||
# Try to extract key metadata
|
||||
if command -v yq &> /dev/null; then
|
||||
{
|
||||
echo " version: $(yq eval '.metadata.version' "$CAPABILITY_FILE" 2>/dev/null || echo 'unknown')"
|
||||
echo " implementation: $(yq eval '.metadata.implementation' "$CAPABILITY_FILE" 2>/dev/null || echo 'unknown')"
|
||||
echo " maturity: $(yq eval '.metadata.maturity' "$CAPABILITY_FILE" 2>/dev/null || echo 'unknown')"
|
||||
} >> "$MANIFEST_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
cat >> "$MANIFEST_FILE" <<EOF
|
||||
|
||||
integration_details:
|
||||
parent_project: $PARENT_NAME
|
||||
parent_path: $PARENT_DIR
|
||||
|
||||
re_integration_guide: |
|
||||
To re-integrate this capability using the new architecture:
|
||||
|
||||
# Option 1: Git submodule (recommended)
|
||||
cd $PARENT_DIR
|
||||
git submodule add <repo-url> _$CAPABILITY_NAME
|
||||
pip install -e _$CAPABILITY_NAME/
|
||||
|
||||
# Option 2: Clone directly
|
||||
cd $PARENT_DIR
|
||||
git clone <repo-url> _$CAPABILITY_NAME
|
||||
pip install -e _$CAPABILITY_NAME/
|
||||
|
||||
# Option 3: Copy into project
|
||||
cd $PARENT_DIR
|
||||
cp -r /path/to/$CAPABILITY_NAME _$CAPABILITY_NAME
|
||||
pip install -e _$CAPABILITY_NAME/
|
||||
|
||||
Note: Use underscore prefix (_$CAPABILITY_NAME) per ReusableCapabilitiesArchitecture
|
||||
|
||||
notes:
|
||||
- The original integration used pattern: $INTEGRATION_PATTERN
|
||||
- New architecture recommends: underscore-prefix at repo root
|
||||
- See ReusableCapabilitiesArchitecture.md for details
|
||||
|
||||
repository_info:
|
||||
# Fill in if re-integrating from git
|
||||
git_url: "" # e.g., https://github.com/markitect/$CAPABILITY_NAME
|
||||
git_branch: "" # e.g., main
|
||||
git_commit: "" # Optional: specific commit to use
|
||||
EOF
|
||||
|
||||
success "✓ Created manifest: $MANIFEST_FILE"
|
||||
echo ""
|
||||
|
||||
# Check for git
|
||||
if git -C "$CAPABILITY_ROOT" rev-parse --git-dir > /dev/null 2>&1; then
|
||||
REPO_URL=$(git -C "$CAPABILITY_ROOT" config --get remote.origin.url 2>/dev/null || echo "")
|
||||
BRANCH=$(git -C "$CAPABILITY_ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
|
||||
COMMIT=$(git -C "$CAPABILITY_ROOT" rev-parse HEAD 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$REPO_URL" ]; then
|
||||
info "Git repository detected:"
|
||||
echo " URL: $REPO_URL"
|
||||
echo " Branch: $BRANCH"
|
||||
echo " Commit: $COMMIT"
|
||||
echo ""
|
||||
|
||||
# Update manifest with git info
|
||||
sed -i "s|git_url: \"\"|git_url: \"$REPO_URL\"|" "$MANIFEST_FILE"
|
||||
sed -i "s|git_branch: \"\"|git_branch: \"$BRANCH\"|" "$MANIFEST_FILE"
|
||||
sed -i "s|git_commit: \"\"|git_commit: \"$COMMIT\"|" "$MANIFEST_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if this is a git submodule
|
||||
if [ -f "$PARENT_DIR/.gitmodules" ] && grep -q "$CAPABILITY_NAME" "$PARENT_DIR/.gitmodules"; then
|
||||
warn "⚠️ This appears to be a git submodule"
|
||||
echo ""
|
||||
echo "To properly remove a submodule:"
|
||||
echo " cd $PARENT_DIR"
|
||||
echo " git submodule deinit -f $CAPABILITY_ROOT"
|
||||
echo " git rm -f $CAPABILITY_ROOT"
|
||||
echo " rm -rf .git/modules/$CAPABILITY_NAME"
|
||||
echo ""
|
||||
read -p "Run these commands now? (yes/no): " run_submodule
|
||||
|
||||
if [[ "$run_submodule" == "yes" ]]; then
|
||||
cd "$PARENT_DIR"
|
||||
git submodule deinit -f "$CAPABILITY_ROOT" || warn "Submodule deinit failed (might not be initialized)"
|
||||
git rm -f "$CAPABILITY_ROOT" || warn "Git rm failed"
|
||||
rm -rf .git/modules/"$CAPABILITY_NAME" || warn "Module cleanup failed"
|
||||
success "✓ Git submodule removed"
|
||||
else
|
||||
warn "Skipping submodule removal - you'll need to do this manually"
|
||||
info "Removing directory anyway..."
|
||||
rm -rf "$CAPABILITY_ROOT"
|
||||
fi
|
||||
else
|
||||
# Not a submodule, just remove directory
|
||||
info "Removing capability directory..."
|
||||
rm -rf "$CAPABILITY_ROOT"
|
||||
success "✓ Directory removed"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
success "═══════════════════════════════════════"
|
||||
success " Capability detached successfully!"
|
||||
success "═══════════════════════════════════════"
|
||||
echo ""
|
||||
info "Manifest saved to: $MANIFEST_FILE"
|
||||
echo ""
|
||||
echo "To re-integrate using new architecture:"
|
||||
echo " 1. Read the manifest: cat $MANIFEST_FILE"
|
||||
echo " 2. Follow re_integration_guide in the manifest"
|
||||
echo " 3. Use underscore prefix: _$CAPABILITY_NAME/"
|
||||
echo ""
|
||||
info "See ReusableCapabilitiesArchitecture.md for details"
|
||||
391
.capability/feedback
Executable file
391
.capability/feedback
Executable file
@@ -0,0 +1,391 @@
|
||||
#!/usr/bin/env bash
|
||||
# feedback - Universal feedback submission tool for capabilities
|
||||
#
|
||||
# Usage:
|
||||
# feedback submit "Your feedback text"
|
||||
# feedback submit path/to/feedback.md
|
||||
# feedback submit "Text" --category=bug --contact=me@email.com
|
||||
# feedback list [--reviewed] [--archived]
|
||||
# feedback show <filename>
|
||||
#
|
||||
# This tool can be copied to any capability that wants to use the feedback pattern.
|
||||
|
||||
set -e
|
||||
|
||||
FEEDBACK_DIR="feedback"
|
||||
INBOUND_DIR="${FEEDBACK_DIR}/inbound"
|
||||
REVIEWED_DIR="${FEEDBACK_DIR}/reviewed"
|
||||
ARCHIVED_DIR="${FEEDBACK_DIR}/archived"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Helper functions
|
||||
error() {
|
||||
echo -e "${RED}Error: $1${NC}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
success() {
|
||||
echo -e "${GREEN}$1${NC}"
|
||||
}
|
||||
|
||||
info() {
|
||||
echo -e "${BLUE}$1${NC}"
|
||||
}
|
||||
|
||||
warn() {
|
||||
echo -e "${YELLOW}$1${NC}"
|
||||
}
|
||||
|
||||
# Check if we're in a capability directory
|
||||
check_capability_dir() {
|
||||
if [ ! -d "$FEEDBACK_DIR" ]; then
|
||||
error "Not in a capability directory with feedback support.\n Looking for: $FEEDBACK_DIR/\n Run from capability root or initialize with: mkdir -p $INBOUND_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
# Initialize feedback directories
|
||||
init_dirs() {
|
||||
mkdir -p "$INBOUND_DIR" "$REVIEWED_DIR" "$ARCHIVED_DIR"
|
||||
}
|
||||
|
||||
# Generate metadata
|
||||
generate_metadata() {
|
||||
local category="${1:-}"
|
||||
local contact="${2:-}"
|
||||
|
||||
local timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
local git_repo=$(git rev-parse --show-toplevel 2>/dev/null || echo "unknown")
|
||||
local git_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
|
||||
local python_version=$(python3 --version 2>/dev/null | cut -d' ' -f2 || echo "unknown")
|
||||
|
||||
# Try to find capability version
|
||||
local cap_version="unknown"
|
||||
if [ -f "CAPABILITY.yaml" ]; then
|
||||
cap_version=$(grep "^version:" CAPABILITY.yaml | awk '{print $2}' | tr -d '"' || echo "unknown")
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
---
|
||||
timestamp: $timestamp
|
||||
source: cli
|
||||
EOF
|
||||
|
||||
[ -n "$category" ] && echo "category: $category"
|
||||
[ -n "$contact" ] && echo "contact: $contact"
|
||||
|
||||
cat <<EOF
|
||||
context:
|
||||
git_repo: $git_repo
|
||||
git_branch: $git_branch
|
||||
capability_version: $cap_version
|
||||
python_version: $python_version
|
||||
---
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
# Submit feedback
|
||||
submit_feedback() {
|
||||
local content="$1"
|
||||
local category="${2:-}"
|
||||
local contact="${3:-}"
|
||||
|
||||
init_dirs
|
||||
|
||||
local timestamp=$(date +%Y%m%d-%H%M%S)
|
||||
local hash=$(echo "$content" | md5sum 2>/dev/null | cut -c1-8 || echo "$(date +%N)")
|
||||
local filename="${INBOUND_DIR}/${timestamp}-${hash}.md"
|
||||
|
||||
# Check if content is a file
|
||||
if [ -f "$content" ]; then
|
||||
info "Submitting feedback from file: $content"
|
||||
{
|
||||
generate_metadata "$category" "$contact"
|
||||
cat "$content"
|
||||
} > "$filename"
|
||||
else
|
||||
info "Submitting text feedback"
|
||||
{
|
||||
generate_metadata "$category" "$contact"
|
||||
echo "$content"
|
||||
} > "$filename"
|
||||
fi
|
||||
|
||||
success "✓ Feedback submitted: $filename"
|
||||
echo ""
|
||||
info "Your feedback has been recorded and will be reviewed by the capability maintainers."
|
||||
echo ""
|
||||
echo "To view: feedback show $(basename "$filename")"
|
||||
}
|
||||
|
||||
# List feedback
|
||||
list_feedback() {
|
||||
local dir="$INBOUND_DIR"
|
||||
local label="Inbound"
|
||||
|
||||
case "${1:-}" in
|
||||
--reviewed)
|
||||
dir="$REVIEWED_DIR"
|
||||
label="Reviewed"
|
||||
;;
|
||||
--archived)
|
||||
dir="$ARCHIVED_DIR"
|
||||
label="Archived"
|
||||
;;
|
||||
esac
|
||||
|
||||
check_capability_dir
|
||||
|
||||
if [ ! -d "$dir" ]; then
|
||||
warn "No feedback directory: $dir"
|
||||
return
|
||||
fi
|
||||
|
||||
local count=$(ls -1 "$dir" 2>/dev/null | wc -l)
|
||||
|
||||
echo -e "${BLUE}=== $label Feedback ($count) ===${NC}"
|
||||
echo ""
|
||||
|
||||
if [ "$count" -eq 0 ]; then
|
||||
echo " (none)"
|
||||
return
|
||||
fi
|
||||
|
||||
ls -lt "$dir" | tail -n +2 | while read -r line; do
|
||||
local file=$(echo "$line" | awk '{print $NF}')
|
||||
local date=$(echo "$line" | awk '{print $6, $7, $8}')
|
||||
|
||||
# Try to extract category from metadata
|
||||
local category=""
|
||||
if [ -f "$dir/$file" ]; then
|
||||
category=$(grep "^category:" "$dir/$file" | awk '{print $2}' || echo "")
|
||||
fi
|
||||
|
||||
# Colorize based on category
|
||||
local color=$NC
|
||||
case "$category" in
|
||||
bug) color=$RED ;;
|
||||
feature) color=$GREEN ;;
|
||||
improvement) color=$YELLOW ;;
|
||||
esac
|
||||
|
||||
echo -e " ${color}${file}${NC}"
|
||||
[ -n "$category" ] && echo " Category: $category"
|
||||
echo " Date: $date"
|
||||
echo ""
|
||||
done
|
||||
}
|
||||
|
||||
# Show specific feedback
|
||||
show_feedback() {
|
||||
local filename="$1"
|
||||
|
||||
check_capability_dir
|
||||
|
||||
# Search in all directories
|
||||
local filepath=""
|
||||
for dir in "$INBOUND_DIR" "$REVIEWED_DIR" "$ARCHIVED_DIR"; do
|
||||
if [ -f "$dir/$filename" ]; then
|
||||
filepath="$dir/$filename"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$filepath" ]; then
|
||||
error "Feedback not found: $filename"
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}=== Feedback: $filename ===${NC}"
|
||||
echo ""
|
||||
cat "$filepath"
|
||||
}
|
||||
|
||||
# Review feedback (move to reviewed)
|
||||
review_feedback() {
|
||||
local filename="$1"
|
||||
local create_issue="${2:-}"
|
||||
|
||||
check_capability_dir
|
||||
init_dirs
|
||||
|
||||
if [ ! -f "$INBOUND_DIR/$filename" ]; then
|
||||
error "Feedback not found in inbound: $filename"
|
||||
fi
|
||||
|
||||
if [ "$create_issue" = "--create-issue" ]; then
|
||||
info "Creating issue from feedback..."
|
||||
|
||||
# Extract title and body
|
||||
local title=$(head -20 "$INBOUND_DIR/$filename" | grep -v "^---" | grep -v "^$" | head -1 | sed 's/^# *//')
|
||||
local body=$(cat "$INBOUND_DIR/$filename")
|
||||
|
||||
if command -v issue &> /dev/null; then
|
||||
issue create "$title" --description "$body" --label=feedback
|
||||
success "✓ Issue created"
|
||||
else
|
||||
warn "Issue command not found. Please create issue manually."
|
||||
fi
|
||||
fi
|
||||
|
||||
mv "$INBOUND_DIR/$filename" "$REVIEWED_DIR/$filename"
|
||||
success "✓ Feedback moved to reviewed: $filename"
|
||||
}
|
||||
|
||||
# Archive feedback
|
||||
archive_feedback() {
|
||||
local filename="$1"
|
||||
|
||||
check_capability_dir
|
||||
init_dirs
|
||||
|
||||
# Try both inbound and reviewed
|
||||
if [ -f "$INBOUND_DIR/$filename" ]; then
|
||||
mv "$INBOUND_DIR/$filename" "$ARCHIVED_DIR/$filename"
|
||||
elif [ -f "$REVIEWED_DIR/$filename" ]; then
|
||||
mv "$REVIEWED_DIR/$filename" "$ARCHIVED_DIR/$filename"
|
||||
else
|
||||
error "Feedback not found: $filename"
|
||||
fi
|
||||
|
||||
success "✓ Feedback archived: $filename"
|
||||
}
|
||||
|
||||
# Show usage
|
||||
usage() {
|
||||
cat <<EOF
|
||||
feedback - Universal feedback submission tool
|
||||
|
||||
Usage:
|
||||
feedback submit <content> [options] Submit feedback
|
||||
feedback list [--reviewed|--archived] List feedback
|
||||
feedback show <filename> Show specific feedback
|
||||
feedback review <filename> [options] Mark feedback as reviewed (maintainers)
|
||||
feedback archive <filename> Archive feedback (maintainers)
|
||||
feedback stats Show feedback statistics
|
||||
feedback help Show this help
|
||||
|
||||
Submit Options:
|
||||
--category=<type> Category: bug, feature, improvement, question, other
|
||||
--contact=<email> Optional contact for follow-up
|
||||
|
||||
Review Options:
|
||||
--create-issue Create an issue from the feedback
|
||||
|
||||
Examples:
|
||||
# Quick text feedback
|
||||
feedback submit "The sync command is slow with 1000+ issues"
|
||||
|
||||
# Feedback from file
|
||||
feedback submit my-feedback.md
|
||||
|
||||
# With metadata
|
||||
feedback submit "Bug: crashes on startup" --category=bug --contact=me@email.com
|
||||
|
||||
# List feedback
|
||||
feedback list
|
||||
feedback list --reviewed
|
||||
|
||||
# Show specific feedback
|
||||
feedback show 20251217-103045-abc12345.md
|
||||
|
||||
# Review and create issue (maintainers)
|
||||
feedback review 20251217-103045-abc12345.md --create-issue
|
||||
|
||||
# Archive (maintainers)
|
||||
feedback archive 20251217-103045-abc12345.md
|
||||
|
||||
For more information, see feedback/README.md
|
||||
EOF
|
||||
}
|
||||
|
||||
# Show statistics
|
||||
show_stats() {
|
||||
check_capability_dir
|
||||
|
||||
local inbound=$(ls -1 "$INBOUND_DIR" 2>/dev/null | wc -l)
|
||||
local reviewed=$(ls -1 "$REVIEWED_DIR" 2>/dev/null | wc -l)
|
||||
local archived=$(ls -1 "$ARCHIVED_DIR" 2>/dev/null | wc -l)
|
||||
local total=$((inbound + reviewed + archived))
|
||||
|
||||
echo -e "${BLUE}=== Feedback Statistics ===${NC}"
|
||||
echo ""
|
||||
echo " Pending: $inbound"
|
||||
echo " Reviewed: $reviewed"
|
||||
echo " Archived: $archived"
|
||||
echo " ─────────────"
|
||||
echo " Total: $total"
|
||||
echo ""
|
||||
|
||||
if [ "$inbound" -gt 0 ]; then
|
||||
warn "⚠ $inbound feedback items awaiting review"
|
||||
else
|
||||
success "✓ No pending feedback"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main command dispatcher
|
||||
main() {
|
||||
local command="${1:-help}"
|
||||
shift || true
|
||||
|
||||
case "$command" in
|
||||
submit)
|
||||
local content="${1:-}"
|
||||
[ -z "$content" ] && error "Usage: feedback submit <content|file> [--category=TYPE] [--contact=EMAIL]"
|
||||
|
||||
local category=""
|
||||
local contact=""
|
||||
|
||||
# Parse options
|
||||
shift || true
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--category=*)
|
||||
category="${1#--category=}"
|
||||
;;
|
||||
--contact=*)
|
||||
contact="${1#--contact=}"
|
||||
;;
|
||||
*)
|
||||
warn "Unknown option: $1"
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
submit_feedback "$content" "$category" "$contact"
|
||||
;;
|
||||
list)
|
||||
list_feedback "$@"
|
||||
;;
|
||||
show)
|
||||
[ -z "${1:-}" ] && error "Usage: feedback show <filename>"
|
||||
show_feedback "$1"
|
||||
;;
|
||||
review)
|
||||
[ -z "${1:-}" ] && error "Usage: feedback review <filename> [--create-issue]"
|
||||
review_feedback "$1" "${2:-}"
|
||||
;;
|
||||
archive)
|
||||
[ -z "${1:-}" ] && error "Usage: feedback archive <filename>"
|
||||
archive_feedback "$1"
|
||||
;;
|
||||
stats)
|
||||
show_stats
|
||||
;;
|
||||
help|--help|-h)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
error "Unknown command: $command\n\n$(usage)"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -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)
|
||||
|
||||
@@ -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**
|
||||
|
||||
20
.claude/rules/agents.md
Normal file
20
.claude/rules/agents.md
Normal file
@@ -0,0 +1,20 @@
|
||||
## Kaizen Agents
|
||||
|
||||
Specialized agent personas available on demand via the state-hub MCP.
|
||||
|
||||
**Discover:** `list_kaizen_agents()` — returns all agents with name, description, category
|
||||
**Load:** `get_kaizen_agent("tdd-workflow")` — returns full instructions; read and follow them
|
||||
|
||||
Common agents:
|
||||
|
||||
| Agent | Category | When to use |
|
||||
|-------|----------|-------------|
|
||||
| `tdd-workflow` | testing | Step-by-step TDD8 workflow for any feature |
|
||||
| `code-refactoring` | quality | Code quality analysis and safe refactoring |
|
||||
| `test-maintenance` | testing | Diagnose and fix failing tests |
|
||||
| `requirements-engineering` | process | Prevent interface/mock mismatches upfront |
|
||||
| `keepaTodofile` | process | Maintain TODO.md during work |
|
||||
| `project-management` | process | Track status, determine next steps |
|
||||
| `datamodel-optimization` | quality | Optimize dataclasses and data structures |
|
||||
|
||||
All 17 agents: call `list_kaizen_agents()` for the full list.
|
||||
8
.claude/rules/architecture.md
Normal file
8
.claude/rules/architecture.md
Normal file
@@ -0,0 +1,8 @@
|
||||
## Architecture
|
||||
|
||||
<!-- TODO: Describe the key design decisions and component structure.
|
||||
Key modules, data flows, external integrations, state machines, etc. -->
|
||||
|
||||
## Quick Reference
|
||||
|
||||
`~/state-hub/mcp_server/TOOLS.md` — MCP tool reference
|
||||
50
.claude/rules/credential-routing.md
Normal file
50
.claude/rules/credential-routing.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Credential and access routing
|
||||
|
||||
**Audience:** Codex, Claude Code, Grok, and custodian agents that call **llm-connect**
|
||||
for inference. Run this check **before** requesting secrets, API keys, SSH access,
|
||||
login tokens, or database passwords — in any repo, not only `ops-warden`.
|
||||
|
||||
ops-warden **issues SSH certificates only** (`warden sign`, `cert_command`). Every
|
||||
other credential need belongs to another subsystem. **Do not** message
|
||||
`ops-warden` on State Hub expecting a secret value; the reply is a pointer, not a key.
|
||||
|
||||
### Lookup (do this first)
|
||||
|
||||
```bash
|
||||
warden route find "<describe your need>" --json
|
||||
warden route show <catalog-id> --json
|
||||
```
|
||||
|
||||
Requires the `warden` CLI from `~/ops-warden` (`uv tool install .` or `uv run warden`).
|
||||
|
||||
| Agent runtime | How to orient |
|
||||
| --- | --- |
|
||||
| **Codex / Grok** (shell, HTTP State Hub) | `warden route` commands above; inbox `to_agent=issue-core` is for coordination, not secret vending |
|
||||
| **Claude Code** (MCP when available) | `get_domain_summary("custodian")` for workstreams; **still** use `warden route` for credential ownership |
|
||||
| **llm-connect** (inference service) | Never put secret retrieval in prompts; route custody to OpenBao/operator paths surfaced by `warden route` |
|
||||
|
||||
### Quick routing table
|
||||
|
||||
| I need… | Owner | ops-warden executes? |
|
||||
| --- | --- | --- |
|
||||
| SSH cert (`adm`/`agt`/`atm`) | ops-warden | **Yes** — `warden sign` |
|
||||
| API key, DB password, provider token | OpenBao (`railiance-platform`) | No — route only |
|
||||
| Login / OIDC / MFA | key-cape / Keycloak | No — route only |
|
||||
| Authorization decision | flex-auth | No — route only |
|
||||
| activity-core → issue-core emission | activity-core + issue-core | No — `warden route show activity-core-issue-sink` |
|
||||
| SSH tunnel | ops-bridge (+ `cert_command` from warden) | No — route only |
|
||||
|
||||
### Anti-patterns (do not do these)
|
||||
|
||||
- `POST /messages/` to `ops-warden` asking for `ISSUE_CORE_API_KEY`, `OPENROUTER_API_KEY`, etc.
|
||||
- Inventing `warden secret`, `warden login`, `warden bao`, `warden tunnel` — they do not exist
|
||||
- Pasting secrets into Git, State Hub, workplans, logs, or chat
|
||||
|
||||
### Other capabilities (reuse-surface)
|
||||
|
||||
Non-credential capabilities are usually discovered through **reuse-surface** federation
|
||||
(`reuse-surface` registry / `capability.*` indexes). Credential routing is inlined in
|
||||
every repo's agent instructions because it is high-frequency, high-risk, and easy to
|
||||
get wrong.
|
||||
|
||||
**Canon:** `~/ops-warden/wiki/CredentialRouting.md` · catalog `~/ops-warden/registry/routing/catalog.yaml`
|
||||
38
.claude/rules/first-session.md
Normal file
38
.claude/rules/first-session.md
Normal file
@@ -0,0 +1,38 @@
|
||||
## First Session Protocol
|
||||
|
||||
Triggered when `get_domain_summary("infotech")` shows **no workstreams**.
|
||||
The project is registered but work has not yet been structured.
|
||||
|
||||
**Step 1 — Read, don't write**
|
||||
- `~/the-custodian/canon/projects/infotech/project_charter_v0.1.md` — purpose, scope
|
||||
- `~/the-custodian/canon/projects/infotech/roadmap_v0.1.md` — planned phases
|
||||
- Scan repo root: README, directory structure, existing code or docs
|
||||
|
||||
**Step 2 — Survey in-progress work**
|
||||
Look for TODOs, open branches, half-finished files. Note done vs. started but incomplete.
|
||||
|
||||
**Step 3 — Propose workstreams to Bernd**
|
||||
Propose 1–3 workstreams — each a coherent strand, weeks to months, anchored to a
|
||||
roadmap phase. **Wait for approval before creating.**
|
||||
|
||||
**Step 4 — Create workplan file first, then DB record (ADR-001)**
|
||||
```
|
||||
workplans/ISSUE-WP-NNNN-<slug>.md ← write this first
|
||||
```
|
||||
Then register in the hub:
|
||||
```
|
||||
create_workstream(topic_id="cee7bedf-2b48-46ef-8601-006474f2ad7a", title="...", owner="...", description="...")
|
||||
create_task(workstream_id="<id>", title="...", priority="high|medium|low")
|
||||
```
|
||||
|
||||
**Step 5 — Record the setup**
|
||||
```
|
||||
add_progress_event(
|
||||
summary="First session: structured infotech into N workstreams, M tasks",
|
||||
event_type="milestone",
|
||||
topic_id="cee7bedf-2b48-46ef-8601-006474f2ad7a",
|
||||
detail={"workstreams": [...], "tasks_created": M}
|
||||
)
|
||||
```
|
||||
|
||||
<!-- Delete or archive this file once past first session -->
|
||||
8
.claude/rules/repo-boundary.md
Normal file
8
.claude/rules/repo-boundary.md
Normal file
@@ -0,0 +1,8 @@
|
||||
## Repo boundary
|
||||
|
||||
This repo owns **issue-core** only. It does not own:
|
||||
|
||||
<!-- TODO: List what belongs in adjacent repos, e.g.:
|
||||
- SSH key management → railiance-infra/
|
||||
- State hub code → state-hub/
|
||||
-->
|
||||
5
.claude/rules/repo-identity.md
Normal file
5
.claude/rules/repo-identity.md
Normal file
@@ -0,0 +1,5 @@
|
||||
**Purpose:** Authoritative task lifecycle manager for the Coulomb org. Backend-agnostic CLI + REST ingestion endpoint for tasks from activity-core's IssueSink. Pluggable backends (Gitea, SQLite, GitHub). Renamed from issue-facade on 2026-05-17.
|
||||
|
||||
**Domain:** infotech
|
||||
**Repo slug:** issue-core
|
||||
**Topic ID:** cee7bedf-2b48-46ef-8601-006474f2ad7a
|
||||
85
.claude/rules/session-protocol.md
Normal file
85
.claude/rules/session-protocol.md
Normal file
@@ -0,0 +1,85 @@
|
||||
## Session Protocol
|
||||
|
||||
Dev Hub (State Hub API): http://127.0.0.1:8000
|
||||
MCP server name in `~/.claude.json`: `dev-hub`
|
||||
|
||||
**Step 1 — Orient**
|
||||
|
||||
Read the offline-safe brief first — it works without a live hub connection:
|
||||
```bash
|
||||
cat .custodian-brief.md
|
||||
```
|
||||
Then call the MCP tool for richer cross-domain context when MCP tools are exposed:
|
||||
```
|
||||
get_domain_summary("infotech")
|
||||
```
|
||||
If MCP tools are unavailable in the current agent session, use the REST API:
|
||||
```bash
|
||||
curl -s "http://127.0.0.1:8000/state/summary" | python3 -m json.tool
|
||||
```
|
||||
If the hub is offline: `cd ~/state-hub && make api`
|
||||
|
||||
**Step 2 — Check inbox**
|
||||
With MCP tools:
|
||||
```
|
||||
get_messages(to_agent="issue-core", unread_only=True)
|
||||
```
|
||||
Mark read with `mark_message_read(message_id)`. Reply or act on coordination
|
||||
requests before proceeding.
|
||||
|
||||
Without MCP tools:
|
||||
```bash
|
||||
curl -s "http://127.0.0.1:8000/messages/?to_agent=issue-core&unread_only=true" \
|
||||
| python3 -m json.tool
|
||||
curl -s -X PATCH "http://127.0.0.1:8000/messages/<id>/read" \
|
||||
-H "Content-Type: application/json" -d '{}'
|
||||
```
|
||||
|
||||
**Step 3 — Scan workplans**
|
||||
```bash
|
||||
ls workplans/
|
||||
```
|
||||
For each file with `status: ready`, `active`, or `blocked`, note pending
|
||||
`wait`/`todo`/`progress` tasks.
|
||||
|
||||
**Step 4 — Present brief**
|
||||
|
||||
1. **Active workstreams** for `infotech` — title, task counts, blocking decisions
|
||||
2. **Pending tasks** from `workplans/` + any `[repo:issue-core]` hub tasks
|
||||
3. **Goal guidance** — if `goal_guidance` in summary:
|
||||
- `needs_workplan`: surface as top action — *"Repo goal '{title}' has no workplan yet"*
|
||||
- `alignment_warnings`: flag if active work is not aligned with current goal
|
||||
4. **Suggested next action** — highest-priority open item
|
||||
5. **SBOM status** — flag if `last_sbom_at` is unset for this repo
|
||||
|
||||
If no workstreams: follow First Session Protocol (`first-session.md`).
|
||||
|
||||
**During work:** `record_decision()` · `add_progress_event()` · `resolve_decision()`
|
||||
|
||||
> State Hub is a *read model*. Bootstrap tools (`create_workstream`, `create_task`)
|
||||
> are First Session Protocol only. Work structure belongs in repo files (ADR-001).
|
||||
|
||||
**Session close:**
|
||||
With MCP tools:
|
||||
```
|
||||
add_progress_event(summary="...", topic_id="cee7bedf-2b48-46ef-8601-006474f2ad7a", workstream_id="<uuid>")
|
||||
```
|
||||
Without MCP tools:
|
||||
```bash
|
||||
curl -s -X POST http://127.0.0.1:8000/progress/ \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"topic_id":"cee7bedf-2b48-46ef-8601-006474f2ad7a","workstream_id":"<uuid>","event_type":"note","summary":"what changed","author":"codex"}'
|
||||
```
|
||||
If workplan files were modified, ensure the local copy is up to date first:
|
||||
```bash
|
||||
git -C <repo_path> pull --ff-only
|
||||
cd ~/state-hub && make fix-consistency REPO=issue-core
|
||||
```
|
||||
For repos where implementation runs on a remote machine (e.g. CoulombCore),
|
||||
use the combined target which pulls before fixing:
|
||||
```bash
|
||||
cd ~/state-hub && make fix-consistency-remote REPO=issue-core
|
||||
```
|
||||
**C-15** (DB task ahead of file) is normal in multi-machine workflows — writeback
|
||||
will sync the file to match DB. **C-16** (repo behind remote) blocks all writes
|
||||
until you pull — intentional to prevent clobbering remote progress.
|
||||
19
.claude/rules/stack-and-commands.md
Normal file
19
.claude/rules/stack-and-commands.md
Normal file
@@ -0,0 +1,19 @@
|
||||
## Stack
|
||||
|
||||
<!-- TODO: Fill in language, frameworks, and key dependencies -->
|
||||
- **Language:**
|
||||
- **Key deps:**
|
||||
|
||||
## Dev Commands
|
||||
|
||||
```bash
|
||||
# TODO: Fill in the standard commands for this repo
|
||||
|
||||
# Install dependencies
|
||||
|
||||
# Run tests
|
||||
|
||||
# Lint / type check
|
||||
|
||||
# Build / package (if applicable)
|
||||
```
|
||||
40
.claude/rules/workplan-convention.md
Normal file
40
.claude/rules/workplan-convention.md
Normal file
@@ -0,0 +1,40 @@
|
||||
## Workplan Convention (ADR-001)
|
||||
|
||||
File location: `workplans/ISSUE-WP-NNNN-<slug>.md`
|
||||
ID prefix: `ISSUE-WP-`
|
||||
|
||||
Work items originate as files in this repo **before** being registered in the hub.
|
||||
|
||||
Canonical workplan/workstream frontmatter statuses are:
|
||||
`proposed`, `ready`, `active`, `blocked`, `backlog`, `finished`, `archived`.
|
||||
Use `proposed` for a newly drafted plan, `ready` after review against current
|
||||
repo state, and `finished` when implementation is complete. `stalled` and
|
||||
`needs_review` are derived health labels, not stored statuses.
|
||||
|
||||
Closed workplans may be moved to `workplans/archived/` with a completion-date
|
||||
prefix: `YYMMDD-ISSUE-WP-NNNN-<slug>.md`. The frontmatter id remains
|
||||
unchanged; the prefix is only for quick visual reference.
|
||||
|
||||
Small opportunistic tasks discovered during another session use **Ad Hoc Tasks**:
|
||||
`workplans/ADHOC-YYYY-MM-DD.md`, workstream slug `adhoc-YYYY-MM-DD`, and task ids
|
||||
`ADHOC-YYYY-MM-DD-T01`, `T02`, etc. Use adhocs only for low-risk work completed
|
||||
directly. Promote anything requiring analysis, design, approval, dependencies, or
|
||||
multiple planned phases into a normal workplan.
|
||||
|
||||
Ecosystem todos from other agents arrive as `[repo:issue-core]` hub tasks —
|
||||
visible at session start. Pick one up by creating the workplan file, then registering
|
||||
the workstream.
|
||||
|
||||
Task blocks use this shape:
|
||||
|
||||
```task
|
||||
id: ISSUE-WP-NNNN-T01
|
||||
status: wait | todo | progress | done | cancel
|
||||
priority: high | medium | low
|
||||
state_hub_task_id: "<uuid>" # written by fix-consistency — do not edit
|
||||
```
|
||||
|
||||
Status progression is `todo` → `progress` → `done`; use `wait` for waiting or
|
||||
blocked work and `cancel` for stopped work.
|
||||
|
||||
<!-- Ralph Loop rules and HEUREKA sequence: ~/.claude/CLAUDE.md — do not duplicate here -->
|
||||
18
.custodian-brief.md
Normal file
18
.custodian-brief.md
Normal file
@@ -0,0 +1,18 @@
|
||||
<!-- custodian-brief: generated by fix-consistency — do not edit manually -->
|
||||
# Custodian Brief — issue-core
|
||||
|
||||
**Domain:** infotech
|
||||
**Last synced:** 2026-07-02 12:50 UTC
|
||||
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
|
||||
|
||||
## Active Workstreams
|
||||
|
||||
*(none — repo may need first-session setup)*
|
||||
|
||||
---
|
||||
## MCP Orientation (when available)
|
||||
|
||||
If the state-hub MCP server is reachable, call:
|
||||
`get_domain_summary("infotech")`
|
||||
This provides richer cross-domain context.
|
||||
If the MCP call fails, use this file as your orientation source.
|
||||
37
.gitea/workflows/publish-python-package.yml
Normal file
37
.gitea/workflows/publish-python-package.yml
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Publish Python package
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install packaging tools
|
||||
run: python -m pip install --upgrade build twine
|
||||
|
||||
- name: Build distributions
|
||||
run: python -m build
|
||||
|
||||
- name: Validate distributions
|
||||
run: python -m twine check dist/*
|
||||
|
||||
- name: Upload to Gitea PyPI
|
||||
env:
|
||||
TWINE_USERNAME: ${{ secrets.GITEA_PACKAGE_USER }}
|
||||
TWINE_PASSWORD: ${{ secrets.GITEA_PACKAGE_TOKEN }}
|
||||
run: >-
|
||||
python -m twine upload
|
||||
--repository-url https://gitea.coulomb.social/api/packages/coulomb/pypi
|
||||
dist/*
|
||||
26
.repo-classification.yaml
Normal file
26
.repo-classification.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
# Repo classification (Repo Classification Standard v1.0).
|
||||
|
||||
repo_classification:
|
||||
standard: Repo Classification Standard
|
||||
version: '1.0'
|
||||
classified_at: '2026-06-22'
|
||||
classified_by: human
|
||||
category: tooling
|
||||
domain: infotech
|
||||
secondary_domains:
|
||||
- agents
|
||||
capability_tags:
|
||||
- workflow
|
||||
- coordination
|
||||
- orchestration
|
||||
- traceability
|
||||
business_stake:
|
||||
- technology
|
||||
- product
|
||||
- operations
|
||||
- automation
|
||||
business_mechanics:
|
||||
- coordination
|
||||
- operation
|
||||
notes: Task lifecycle manager / unified issue interface for autonomous coding agents. Reusable
|
||||
backend component -> product.
|
||||
219
AGENTS.md
Normal file
219
AGENTS.md
Normal file
@@ -0,0 +1,219 @@
|
||||
# issue-core — Agent Instructions
|
||||
|
||||
## Repo Identity
|
||||
|
||||
**Purpose:** Authoritative task lifecycle manager for the Coulomb org. Backend-agnostic CLI + REST ingestion endpoint for tasks from activity-core's IssueSink. Pluggable backends (Gitea, SQLite, GitHub). Renamed from issue-facade on 2026-05-17.
|
||||
|
||||
**Domain:** infotech
|
||||
**Repo slug:** issue-core
|
||||
**Topic ID:** `cee7bedf-2b48-46ef-8601-006474f2ad7a`
|
||||
**Workplan prefix:** `ISSUE-WP-`
|
||||
|
||||
---
|
||||
|
||||
## State Hub Integration
|
||||
|
||||
The Custodian State Hub tracks work across all domains. Interact via HTTP REST —
|
||||
there is no MCP server for Codex agents.
|
||||
|
||||
| Context | URL |
|
||||
|---------|-----|
|
||||
| Local workstation | `http://127.0.0.1:8000` |
|
||||
| Remote via tunnel | `http://127.0.0.1:18000` |
|
||||
|
||||
### Orient at session start
|
||||
|
||||
```bash
|
||||
# Offline brief — works without hub connection
|
||||
cat .custodian-brief.md
|
||||
|
||||
# Active workstreams for this domain
|
||||
curl -s "http://127.0.0.1:8000/workstreams/?topic_id=cee7bedf-2b48-46ef-8601-006474f2ad7a&status=active" \
|
||||
| python3 -m json.tool
|
||||
|
||||
# Check inbox
|
||||
curl -s "http://127.0.0.1:8000/messages/?to_agent=issue-core&unread_only=true" \
|
||||
| python3 -m json.tool
|
||||
```
|
||||
|
||||
Mark a message read:
|
||||
```bash
|
||||
curl -s -X PATCH "http://127.0.0.1:8000/messages/<id>/read" \
|
||||
-H "Content-Type: application/json" -d '{}'
|
||||
```
|
||||
|
||||
### Log progress (required at session close)
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://127.0.0.1:8000/progress/ \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"summary": "what was done",
|
||||
"event_type": "note",
|
||||
"author": "codex",
|
||||
"workstream_id": "<uuid>",
|
||||
"task_id": "<uuid>"
|
||||
}'
|
||||
```
|
||||
|
||||
Omit `workstream_id` / `task_id` when not applicable.
|
||||
|
||||
### Update task status
|
||||
|
||||
```bash
|
||||
curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"status": "progress"}'
|
||||
# values: wait | todo | progress | done | cancel
|
||||
```
|
||||
|
||||
### Flag a task for human review
|
||||
|
||||
```bash
|
||||
curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"needs_human": true, "intervention_note": "reason"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Session Protocol
|
||||
|
||||
**Start:**
|
||||
1. `cat .custodian-brief.md` — domain goal and open workstreams (offline-safe)
|
||||
2. Check inbox: `GET /messages/?to_agent=issue-core&unread_only=true`; mark read
|
||||
3. Scan workplans: `ls workplans/` — note `status: ready`, `active`, or `blocked` files and open tasks
|
||||
4. Check human-needed tasks: `GET /tasks/?needs_human=true`
|
||||
|
||||
**During work:**
|
||||
- Update task statuses in workplan files as tasks progress
|
||||
- Record significant decisions via `POST /decisions/`
|
||||
|
||||
**Close:**
|
||||
1. Update workplan file task statuses to reflect progress
|
||||
2. Log: `POST /progress/` with a summary of what changed
|
||||
3. Note for the custodian operator: after workplan file changes, run from
|
||||
`~/state-hub`:
|
||||
```bash
|
||||
make fix-consistency REPO=issue-core
|
||||
```
|
||||
This syncs task status from files into the hub DB.
|
||||
|
||||
---
|
||||
|
||||
## Credential and access routing
|
||||
|
||||
**Audience:** Codex, Claude Code, Grok, and custodian agents that call **llm-connect**
|
||||
for inference. Run this check **before** requesting secrets, API keys, SSH access,
|
||||
login tokens, or database passwords — in any repo, not only `ops-warden`.
|
||||
|
||||
ops-warden **issues SSH certificates only** (`warden sign`, `cert_command`). Every
|
||||
other credential need belongs to another subsystem. **Do not** message
|
||||
`ops-warden` on State Hub expecting a secret value; the reply is a pointer, not a key.
|
||||
|
||||
### Lookup (do this first)
|
||||
|
||||
```bash
|
||||
warden route find "<describe your need>" --json
|
||||
warden route show <catalog-id> --json
|
||||
```
|
||||
|
||||
Requires the `warden` CLI from `~/ops-warden` (`uv tool install .` or `uv run warden`).
|
||||
|
||||
| Agent runtime | How to orient |
|
||||
| --- | --- |
|
||||
| **Codex / Grok** (shell, HTTP State Hub) | `warden route` commands above; inbox `to_agent=issue-core` is for coordination, not secret vending |
|
||||
| **Claude Code** (MCP when available) | `get_domain_summary("custodian")` for workstreams; **still** use `warden route` for credential ownership |
|
||||
| **llm-connect** (inference service) | Never put secret retrieval in prompts; route custody to OpenBao/operator paths surfaced by `warden route` |
|
||||
|
||||
### Quick routing table
|
||||
|
||||
| I need… | Owner | ops-warden executes? |
|
||||
| --- | --- | --- |
|
||||
| SSH cert (`adm`/`agt`/`atm`) | ops-warden | **Yes** — `warden sign` |
|
||||
| API key, DB password, provider token | OpenBao (`railiance-platform`) | No — route only |
|
||||
| Login / OIDC / MFA | key-cape / Keycloak | No — route only |
|
||||
| Authorization decision | flex-auth | No — route only |
|
||||
| activity-core → issue-core emission | activity-core + issue-core | No — `warden route show activity-core-issue-sink` |
|
||||
| SSH tunnel | ops-bridge (+ `cert_command` from warden) | No — route only |
|
||||
|
||||
### Anti-patterns (do not do these)
|
||||
|
||||
- `POST /messages/` to `ops-warden` asking for `ISSUE_CORE_API_KEY`, `OPENROUTER_API_KEY`, etc.
|
||||
- Inventing `warden secret`, `warden login`, `warden bao`, `warden tunnel` — they do not exist
|
||||
- Pasting secrets into Git, State Hub, workplans, logs, or chat
|
||||
|
||||
### Other capabilities (reuse-surface)
|
||||
|
||||
Non-credential capabilities are usually discovered through **reuse-surface** federation
|
||||
(`reuse-surface` registry / `capability.*` indexes). Credential routing is inlined in
|
||||
every repo's agent instructions because it is high-frequency, high-risk, and easy to
|
||||
get wrong.
|
||||
|
||||
**Canon:** `~/ops-warden/wiki/CredentialRouting.md` · catalog `~/ops-warden/registry/routing/catalog.yaml`
|
||||
|
||||
<!-- REPO-AGENTS-EXTENSIONS -->
|
||||
<!-- Append repo-specific agent instructions below this marker.
|
||||
The state-hub template sync preserves content after this line. -->
|
||||
|
||||
---
|
||||
|
||||
## Workplan Convention (ADR-001)
|
||||
|
||||
Work items originate as files in this repo — not in the hub. The hub is a
|
||||
read/cache/index layer that rebuilds from files.
|
||||
|
||||
**File location:** `workplans/ISSUE-WP-NNNN-<slug>.md`
|
||||
|
||||
**Archived location:** finished workplans may move to
|
||||
`workplans/archived/YYMMDD-ISSUE-WP-NNNN-<slug>.md`. The `YYMMDD` prefix is
|
||||
the completion/archive date; the frontmatter `id` does not change.
|
||||
|
||||
**Ad Hoc Tasks:** small opportunistic fixes discovered during a session use
|
||||
`workplans/ADHOC-YYYY-MM-DD.md` with task ids `ADHOC-YYYY-MM-DD-T01`, etc. Use
|
||||
this only for low-risk work completed directly; create a normal workplan for
|
||||
anything needing analysis, design, approval, dependencies, or multiple phases.
|
||||
|
||||
**Frontmatter:**
|
||||
|
||||
```yaml
|
||||
---
|
||||
id: ISSUE-WP-NNNN
|
||||
type: workplan
|
||||
title: "..."
|
||||
domain: infotech
|
||||
repo: issue-core
|
||||
status: proposed | ready | active | blocked | backlog | finished | archived
|
||||
owner: codex
|
||||
topic_slug: ...
|
||||
created: "YYYY-MM-DD"
|
||||
updated: "YYYY-MM-DD"
|
||||
state_hub_workstream_id: "<uuid>" # written by fix-consistency — do not edit
|
||||
---
|
||||
```
|
||||
|
||||
Use `proposed` for a new draft, `ready` after review against current repo
|
||||
state, and `finished` after implementation. `stalled` and `needs_review` are
|
||||
derived health labels, not frontmatter statuses.
|
||||
|
||||
**Task block format** (one per `##` section):
|
||||
|
||||
```
|
||||
## Task Title
|
||||
|
||||
` ` `task
|
||||
id: ISSUE-WP-NNNN-T01
|
||||
status: wait | todo | progress | done | cancel
|
||||
priority: high | medium | low
|
||||
state_hub_task_id: "<uuid>" # written by fix-consistency — do not edit
|
||||
` ` `
|
||||
|
||||
Task description text.
|
||||
```
|
||||
|
||||
Status progression: `todo` → `progress` → `done`; use `wait` for waiting/blocked work and `cancel` for stopped work.
|
||||
|
||||
To create a new workplan:
|
||||
1. Write the file following the format above
|
||||
2. Notify the custodian operator to run `make fix-consistency REPO=issue-core`
|
||||
(or send a message to the hub agent via `POST /messages/`)
|
||||
@@ -1,10 +1,10 @@
|
||||
# Agent Integration Guide
|
||||
|
||||
**Issue Facade for Autonomous Coding Agent Coordination**
|
||||
**Issue Core for Autonomous Coding Agent Coordination**
|
||||
|
||||
## Purpose
|
||||
|
||||
The **Issue Facade** capability provides a standardized interface for autonomous coding agents to coordinate project implementation through issue tracking. Instead of agents directly interfacing with platform-specific APIs (GitHub, GitLab, Gitea), they use a unified abstraction that works consistently across backends.
|
||||
The **Issue Core** capability provides a standardized interface for autonomous coding agents to coordinate project implementation through issue tracking. Instead of agents directly interfacing with platform-specific APIs (GitHub, GitLab, Gitea), they use a unified abstraction that works consistently across backends.
|
||||
|
||||
### Why Issue Tracking for Agent Coordination?
|
||||
|
||||
@@ -67,7 +67,7 @@ Issue tracking provides a natural coordination mechanism for multi-agent softwar
|
||||
### 1. Installation
|
||||
|
||||
```bash
|
||||
cd capabilities/issue-facade
|
||||
cd capabilities/issue-core
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
@@ -99,7 +99,7 @@ issue backend set-default my-project
|
||||
# Configure local SQLite backend
|
||||
issue backend add local-work local
|
||||
# Prompts for:
|
||||
# - Database path: .issue-facade/issues.db
|
||||
# - Database path: .issue-core/issues.db
|
||||
|
||||
issue backend set-default local-work
|
||||
```
|
||||
@@ -178,8 +178,8 @@ issue list --label=reviewed --state=closed --format=json | \
|
||||
|
||||
```python
|
||||
# Agent creates implementation issues from requirements
|
||||
from issue_tracker.backends.gitea import GiteaBackend
|
||||
from issue_tracker.core.models import Issue, Label, IssueState
|
||||
from issue_core.backends.gitea import GiteaBackend
|
||||
from issue_core.core.models import Issue, Label, IssueState
|
||||
from datetime import datetime, timezone
|
||||
|
||||
backend = GiteaBackend()
|
||||
@@ -224,9 +224,9 @@ for task in subtasks:
|
||||
### Python Integration
|
||||
|
||||
```python
|
||||
from issue_tracker.backends.gitea import GiteaBackend
|
||||
from issue_tracker.core.models import Issue, Label, IssueState, User
|
||||
from issue_tracker.core.interfaces import IssueFilter
|
||||
from issue_core.backends.gitea import GiteaBackend
|
||||
from issue_core.core.models import Issue, Label, IssueState, User
|
||||
from issue_core.core.interfaces import IssueFilter
|
||||
from datetime import datetime, timezone
|
||||
import os
|
||||
|
||||
@@ -271,7 +271,7 @@ created.state = IssueState.IN_PROGRESS
|
||||
backend.update_issue(created)
|
||||
|
||||
# Add comment
|
||||
from issue_tracker.core.models import Comment
|
||||
from issue_core.core.models import Comment
|
||||
comment = Comment(
|
||||
id=None,
|
||||
body="Analysis complete. Root cause: unclosed file handles in line 234",
|
||||
@@ -432,7 +432,7 @@ issue sync push backup gitea-remote
|
||||
|
||||
```python
|
||||
# Check for conflicts before sync
|
||||
from issue_tracker.cli.sync_commands import sync_pull
|
||||
from issue_core.cli.sync_commands import sync_pull
|
||||
|
||||
try:
|
||||
sync_pull(source='remote', target='local', dry_run=True)
|
||||
@@ -505,7 +505,7 @@ Create a setup script for each project:
|
||||
#!/bin/bash
|
||||
# setup-issue-tracking.sh
|
||||
|
||||
cat > .issue-facade-config << EOF
|
||||
cat > .issue-core-config << EOF
|
||||
GITEA_URL=https://gitea.example.com
|
||||
GITEA_OWNER=myorg
|
||||
GITEA_REPO=myproject
|
||||
@@ -513,7 +513,7 @@ GITEA_TOKEN_FILE=~/.secrets/gitea-token
|
||||
EOF
|
||||
|
||||
# Load config and configure backend
|
||||
source .issue-facade-config
|
||||
source .issue-core-config
|
||||
export GITEA_API_TOKEN=$(cat $GITEA_TOKEN_FILE)
|
||||
|
||||
issue backend add $(basename $(pwd)) gitea <<INPUT
|
||||
@@ -551,7 +551,7 @@ for issue_number in [1, 2, 3, 4, 5]:
|
||||
backend.update_issue(issue)
|
||||
|
||||
# GOOD: Use local backend for bulk operations
|
||||
from issue_tracker.backends.local import LocalSQLiteBackend
|
||||
from issue_core.backends.local import LocalSQLiteBackend
|
||||
|
||||
local = LocalSQLiteBackend()
|
||||
local.connect({'db_path': '/tmp/batch.db'})
|
||||
@@ -595,7 +595,7 @@ if time.time() - last_fetch > 60:
|
||||
### Phase 1: Auto-Configuration (v1.1)
|
||||
- Automatic git remote detection
|
||||
- Environment-variable-only setup
|
||||
- Per-repository `.issue-facade/config.json` support
|
||||
- Per-repository `.issue-core/config.json` support
|
||||
- `issue config detect` command
|
||||
|
||||
### Phase 2: Agent Features (v1.2)
|
||||
|
||||
@@ -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)
|
||||
@@ -158,12 +158,52 @@ agent_guidance:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Feedback and continuous improvement
|
||||
feedback:
|
||||
enabled: true
|
||||
method: feedback-capability
|
||||
description: >
|
||||
This capability integrates the feedback pattern for continuous improvement
|
||||
based on real-world usage from master projects.
|
||||
|
||||
submission:
|
||||
cli: ".capability/feedback submit 'Your feedback here'"
|
||||
file: ".capability/feedback submit path/to/feedback.md"
|
||||
directory: "feedback/inbound/"
|
||||
|
||||
organization:
|
||||
inbound: "New feedback awaiting review"
|
||||
reviewed: "Feedback that's been reviewed by maintainers"
|
||||
archived: "Resolved or outdated feedback"
|
||||
|
||||
for_users: |
|
||||
Submit feedback about issue-core:
|
||||
./.capability/feedback submit "Feedback text"
|
||||
./.capability/feedback submit detailed-feedback.md
|
||||
|
||||
Or drop a file directly:
|
||||
echo "Feedback..." > feedback/inbound/$(date +%Y%m%d)-feedback.md
|
||||
|
||||
for_maintainers: |
|
||||
Review feedback:
|
||||
./.capability/feedback list
|
||||
./.capability/feedback show <filename>
|
||||
./.capability/feedback review <filename> --create-issue
|
||||
./.capability/feedback stats
|
||||
|
||||
integration_notes:
|
||||
- "Feedback capability is reusable across all markitect capabilities"
|
||||
- "No structure imposement - accepts any text/markdown format"
|
||||
- "Capability owns feedback organization and prioritization"
|
||||
- "Can evolve to API endpoint when capability becomes a service"
|
||||
|
||||
# Documentation references
|
||||
documentation:
|
||||
integration: "AGENT_INTEGRATION.md"
|
||||
development: "CLAUDE.md"
|
||||
roadmap: "ROADMAP.md"
|
||||
examples: "examples/agents/"
|
||||
feedback: "feedback/README.md"
|
||||
|
||||
# Dependencies and requirements
|
||||
requirements:
|
||||
@@ -221,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:
|
||||
178
CHANGELOG.md
Normal file
178
CHANGELOG.md
Normal file
@@ -0,0 +1,178 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Nothing yet
|
||||
|
||||
### Changed
|
||||
- Nothing yet
|
||||
|
||||
### Deprecated
|
||||
- Nothing yet
|
||||
|
||||
### Removed
|
||||
- Nothing yet
|
||||
|
||||
### Fixed
|
||||
- Nothing yet
|
||||
|
||||
### Security
|
||||
- Nothing yet
|
||||
|
||||
## [1.0.0] - 2025-12-17
|
||||
|
||||
### Added - Architecture & Documentation
|
||||
- **ReusableCapabilitiesArchitecture.md**: Complete specification (1,182 lines) defining how capabilities are organized, discovered, and integrated by devhumans and devagents
|
||||
- **Feedback capability**: Lightweight, unstructured feedback collection system
|
||||
- `feedback/` directory for user submissions (visible, not hidden)
|
||||
- `.capability/feedback` CLI tool for submission and management
|
||||
- `feedback/README.md` comprehensive documentation (367 lines)
|
||||
- Support for text, file, and direct submission methods
|
||||
- Maintainer workflow (list, review, archive, create issues)
|
||||
- **Detachment facility**: `.capability/detach` script for clean capability removal
|
||||
- Generates detachment manifest with re-integration guidance
|
||||
- Handles git submodules and directory-based integrations
|
||||
- Preserves git history and metadata
|
||||
|
||||
### Changed - Architecture Compliance
|
||||
- **Directory visibility**: Renamed `.feedback/` → `feedback/` to make user interface visible
|
||||
- **Explicit family declaration**: Renamed `CAPABILITY.yaml` → `CAPABILITY-issue-tracking.yaml`
|
||||
- **Integration pattern**: Established `_<family>/<implementation>` directory structure
|
||||
- Supports family-based organization enabling multiple implementations
|
||||
- Underscore prefix signals "integrated capability, not core code"
|
||||
- Reduces directory tree depth (3 levels vs 6+ levels)
|
||||
|
||||
### Enhanced - Documentation
|
||||
- **CLAUDE.md**: Added 155 lines documenting feedback system integration
|
||||
- User submission methods
|
||||
- Maintainer workflow
|
||||
- Reusable pattern documentation
|
||||
- **README.md**: Complete rewrite (399 lines) focusing on agent coordination
|
||||
- Emphasis on natural language integration by AI coding agents
|
||||
- Use cases for multi-agent coordination
|
||||
- Current status and limitations clearly stated
|
||||
- **CAPABILITY-issue-tracking.yaml**: Enhanced with feedback section
|
||||
- Submission methods documented
|
||||
- Integration notes for other capabilities
|
||||
- Maintainer workflow guidance
|
||||
- **Examples**: Added `examples/feedback-example.md` (285 lines)
|
||||
- Multiple submission methods with examples
|
||||
- Feedback categories and best practices
|
||||
- What happens to feedback after submission
|
||||
|
||||
### Technical - Reference Updates
|
||||
- Updated all documentation references from `.feedback/` to `feedback/`
|
||||
- Updated all references from `CAPABILITY.yaml` to `CAPABILITY-issue-tracking.yaml`
|
||||
- Fixed Makefile commands to use new paths
|
||||
- Updated `.capability/feedback` script to use visible directory
|
||||
|
||||
### Infrastructure
|
||||
- Established feedback as reusable pattern across all markitect capabilities
|
||||
- Created migration path for hidden → visible user interfaces
|
||||
- Documented capability family vs implementation distinction
|
||||
|
||||
## [0.9.0] - 2024-12-15
|
||||
|
||||
### Added
|
||||
- Comprehensive Gitea integration tests
|
||||
- GitLab backend groundwork
|
||||
- Enhanced agent integration documentation
|
||||
|
||||
### Fixed
|
||||
- ID mapping bugs in issue-core
|
||||
- Sync metadata handling
|
||||
- Backend initialization edge cases
|
||||
|
||||
## [0.5.0] - 2024-11-10
|
||||
|
||||
### Added
|
||||
- Capability Makefile for integration with markitect main project
|
||||
- Development command aliases (install, test, lint)
|
||||
- Integration targets for parent project
|
||||
|
||||
### Changed
|
||||
- Improved capability isolation and composability
|
||||
- Enhanced testing infrastructure
|
||||
|
||||
## [0.1.0] - 2024-10-06
|
||||
|
||||
### Added
|
||||
- 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)
|
||||
- Basic synchronization between backends
|
||||
- CLI with JSON output for agent consumption
|
||||
- Python programmatic API
|
||||
- Comprehensive test suite (109 tests, 61% coverage)
|
||||
|
||||
### Architecture
|
||||
- Facade pattern with plugin architecture
|
||||
- Backend interface (ABC) for extensibility
|
||||
- Core domain models (Issue, Label, User, Milestone, Comment)
|
||||
- State management with backend-specific mapping
|
||||
|
||||
### Documentation
|
||||
- README with usage examples
|
||||
- AGENT_INTEGRATION.md for autonomous agents
|
||||
- CLAUDE.md for development guidance
|
||||
- ROADMAP.md with implementation phases
|
||||
|
||||
---
|
||||
|
||||
## Version History Summary
|
||||
|
||||
- **v1.0.0** (2025-12-17) - Architecture formalization, feedback capability, comprehensive documentation
|
||||
- **v0.9.0** (2024-12-15) - Gitea integration tests, bug fixes
|
||||
- **v0.5.0** (2024-11-10) - Capability Makefile, integration improvements
|
||||
- **v0.1.0** (2024-10-06) - Initial extraction, core functionality
|
||||
|
||||
---
|
||||
|
||||
## Upgrade Notes
|
||||
|
||||
### Upgrading to v1.0.0
|
||||
|
||||
**Breaking Changes:**
|
||||
- File paths updated (`.feedback/` → `feedback/`)
|
||||
- Capability spec renamed (`CAPABILITY.yaml` → `CAPABILITY-issue-tracking.yaml`)
|
||||
|
||||
**Migration Steps:**
|
||||
```bash
|
||||
# If you have a clone/fork, update references:
|
||||
find . -name "*.md" -exec sed -i 's/\.feedback\//feedback\//g' {} +
|
||||
mv CAPABILITY.yaml CAPABILITY-issue-tracking.yaml
|
||||
|
||||
# Update any integration scripts to use new paths
|
||||
```
|
||||
|
||||
**New Features:**
|
||||
- Use `feedback/` directory for user feedback submissions
|
||||
- Reference `CAPABILITY-issue-tracking.yaml` in integration code
|
||||
- See `ReusableCapabilitiesArchitecture.md` for complete specification
|
||||
|
||||
**No API Changes:** All Python APIs and CLI commands remain backward compatible.
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
See the [ROADMAP](ROADMAP.md) for planned features and implementation timeline.
|
||||
|
||||
For development guidance, see [CLAUDE.md](CLAUDE.md).
|
||||
|
||||
To submit feedback, see [feedback/README.md](feedback/README.md).
|
||||
|
||||
---
|
||||
|
||||
## Links
|
||||
|
||||
- [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)
|
||||
257
CLAUDE.md
257
CLAUDE.md
@@ -1,245 +1,12 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## 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.
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Installation & Setup
|
||||
- Install for development: `pip install -e ".[dev]"`
|
||||
- Install production: `pip install -e .`
|
||||
- Clean build artifacts: `make issue-facade-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 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/`
|
||||
|
||||
### CLI Usage
|
||||
The project provides two entry points: `issue` and `issue-tracker` (both execute `issue_tracker.cli.main:main`)
|
||||
|
||||
Common commands:
|
||||
- `issue list` - List issues
|
||||
- `issue show <number>` - Show issue details
|
||||
- `issue create "Title"` - Create new issue
|
||||
- `issue close <number>` - Close issue
|
||||
- `issue backend list` - List configured backends
|
||||
- `issue sync` - Synchronize with remote backend
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Design Pattern: Facade with Plugin Architecture
|
||||
|
||||
The codebase implements a **plugin-based facade pattern** with clear separation of concerns:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ CLI Layer (Click) │
|
||||
│ issue_tracker/cli/*.py │
|
||||
└───────────────┬─────────────────────────┘
|
||||
│
|
||||
┌───────────────▼─────────────────────────┐
|
||||
│ Core Domain Models │
|
||||
│ issue_tracker/core/models.py │
|
||||
│ (Issue, Label, User, etc.) │
|
||||
└───────────────┬─────────────────────────┘
|
||||
│
|
||||
┌───────────────▼─────────────────────────┐
|
||||
│ Backend Interface (ABC) │
|
||||
│ issue_tracker/core/interfaces.py │
|
||||
│ IssueBackend, LocalBackend, │
|
||||
│ RemoteBackend, SyncableBackend │
|
||||
└───────────────┬─────────────────────────┘
|
||||
│
|
||||
┌───────┴────────┐
|
||||
│ │
|
||||
┌───────▼──────┐ ┌──────▼───────┐
|
||||
│Local Backend │ │Gitea Backend │
|
||||
│ (SQLite) │ │ (REST API) │
|
||||
└──────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
### Key Components
|
||||
|
||||
#### 1. Core Domain Models (`issue_tracker/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
|
||||
- **IssueState, Priority, IssueType**: Enumerations with backend mapping
|
||||
|
||||
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`)
|
||||
- **IssueBackend (ABC)**: Defines the contract all backends must implement
|
||||
- **LocalBackend, RemoteBackend**: Marker interfaces for backend categorization
|
||||
- **SyncableBackend**: Interface for backends supporting synchronization
|
||||
- **BackendCapabilities**: Describes feature support per backend
|
||||
- **BackendFactory**: Registry pattern for backend creation
|
||||
|
||||
**Critical**: All backends MUST implement the full `IssueBackend` interface. The interface includes:
|
||||
- Connection management: `connect()`, `disconnect()`, `test_connection()`
|
||||
- CRUD operations: `create_issue()`, `get_issue()`, `update_issue()`, `delete_issue()`
|
||||
- Query operations: `list_issues()`, `search_issues()`
|
||||
- Label, User, Milestone, Comment operations
|
||||
- Optional: `bulk_update_issues()` (if capabilities support it)
|
||||
|
||||
#### 3. Backend Implementations
|
||||
|
||||
**Local Backend** (`issue_tracker/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`):
|
||||
- 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/`)
|
||||
- **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)
|
||||
- **sync_commands.py**: Synchronization operations
|
||||
- **utils.py**: Helper functions for formatting and backend access
|
||||
|
||||
### ID Mapping Strategy
|
||||
|
||||
The system uses a **dual-ID approach** for cross-backend synchronization:
|
||||
|
||||
- `id`: Universal ID (UUID for local, external ID for remote)
|
||||
- `number`: Human-readable sequential number (user-facing)
|
||||
- `backend_id`: Backend-specific identifier for sync
|
||||
|
||||
When syncing, backends maintain mappings between local numbers and remote IDs. The Gitea backend stores this in `sync_metadata` on the Issue model.
|
||||
|
||||
### State Management
|
||||
|
||||
`IssueState` enum provides universal states with backend-specific mapping via `to_backend_string()`:
|
||||
- OPEN, CLOSED, IN_PROGRESS, BLOCKED
|
||||
- Some backends (like Gitea) only support OPEN/CLOSED, so IN_PROGRESS and BLOCKED map to OPEN
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Test Organization
|
||||
- `test_gitea_backend.py`: Unit tests for Gitea backend with mocked API
|
||||
- `test_gitea_integration.py`: Full integration tests with real Gitea instance
|
||||
- `test_cli_commands.py`: CLI command testing
|
||||
|
||||
### Integration Tests
|
||||
The integration tests (`test_gitea_integration.py`) expect a Gitea instance at `http://localhost:3000` with test credentials. They create a temporary test repository, run full CRUD operations, and clean up afterwards.
|
||||
|
||||
**Important**: Integration tests use pytest markers:
|
||||
- `@pytest.mark.integration` - Integration tests (slower)
|
||||
- `@pytest.mark.unit` - Unit tests (fast)
|
||||
|
||||
Run only unit tests: `pytest -m unit`
|
||||
Run only integration tests: `pytest -m integration`
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
### Adding a New Backend
|
||||
|
||||
1. Create backend package in `issue_tracker/backends/<name>/`
|
||||
2. Implement `IssueBackend` interface (or extend `LocalBackend`/`RemoteBackend`)
|
||||
3. Implement all abstract methods from the interface
|
||||
4. Define `BackendCapabilities` to specify supported features
|
||||
5. Register backend in `BackendFactory` (typically in `__init__.py`)
|
||||
6. Add configuration handling in CLI backend commands
|
||||
7. Write unit tests with mocked external dependencies
|
||||
8. Write integration tests if applicable
|
||||
|
||||
### Modifying the Issue Model
|
||||
|
||||
When changing `issue_tracker/core/models.py`:
|
||||
1. Update the `Issue` dataclass definition
|
||||
2. Update `to_dict()` serialization method
|
||||
3. Invalidate caches if adding/modifying label-dependent properties
|
||||
4. Update all backend implementations to handle new fields
|
||||
5. Update database schema in `backends/local/schema.sql`
|
||||
6. Write migration logic if modifying existing fields
|
||||
|
||||
### Adding CLI Commands
|
||||
|
||||
1. Add command function in appropriate file (`commands.py`, `backend_commands.py`, etc.)
|
||||
2. Use `@click.command()` decorator with appropriate options
|
||||
3. Call `get_backend(ctx)` to retrieve the active backend
|
||||
4. Use `format_issue()` or `format_issue_list()` from `utils.py` for consistent output
|
||||
5. Handle errors with `raise click.ClickException(message)`
|
||||
6. Register command in `main.py` if creating new command group
|
||||
|
||||
## Configuration
|
||||
|
||||
### Project Configuration (`pyproject.toml`)
|
||||
- Entry points: `issue` and `issue-tracker` commands
|
||||
- Dependencies: click, requests, python-dateutil
|
||||
- Optional dependencies: dev, docs, gitea, github, jira
|
||||
- Code style: Black (line-length=100), isort (profile="black")
|
||||
- Test markers: unit, integration, slow
|
||||
|
||||
### Makefile Integration
|
||||
The capability integrates with the parent markitect project via `Makefile`:
|
||||
- Prefixed targets: `issue-facade-*` for development commands
|
||||
- Unprefixed targets: `issue-*` for user-facing CLI operations
|
||||
- Uses `pip install -e` for editable installation
|
||||
|
||||
## Important Patterns and Conventions
|
||||
|
||||
### Error Handling
|
||||
- Backend-specific errors inherit from base exceptions (e.g., `GiteaAPIError`)
|
||||
- CLI commands convert exceptions to `click.ClickException` with user-friendly messages
|
||||
- Use specific exception types for rate limiting, authentication, network issues
|
||||
|
||||
### Type Hints
|
||||
- Mypy strict mode enabled (`disallow_untyped_defs = true`)
|
||||
- All functions must have type annotations
|
||||
- Use `Optional[T]` for nullable types
|
||||
- Use `List[T]`, `Dict[K, V]` from `typing` module (Python 3.8 compatibility)
|
||||
|
||||
### Performance Optimizations
|
||||
- Use `@cached_property` for expensive computations (e.g., label categorization)
|
||||
- Call `invalidate_cache()` when modifying cached data
|
||||
- Single-pass algorithms for label categorization in Issue model
|
||||
|
||||
### Synchronization
|
||||
When implementing sync:
|
||||
1. Local backend is source of truth
|
||||
2. Remote backends track last sync timestamp
|
||||
3. Use `get_issues_modified_since()` for incremental sync
|
||||
4. Handle conflicts via `SyncableBackend.resolve_sync_conflict()`
|
||||
5. Store sync metadata in Issue.sync_metadata dict
|
||||
|
||||
## Dependencies and External Systems
|
||||
|
||||
### Runtime Dependencies
|
||||
- **click**: CLI framework (>=8.0.0)
|
||||
- **requests**: HTTP client for remote backends (>=2.25.0)
|
||||
- **python-dateutil**: Date/time parsing (>=2.8.0)
|
||||
|
||||
### Development Dependencies
|
||||
- **pytest**: Testing framework with markers support
|
||||
- **pytest-cov**: Coverage reporting
|
||||
- **pytest-mock**: Mocking utilities
|
||||
- **black, isort, flake8, mypy**: Code quality tools
|
||||
|
||||
### External Systems
|
||||
- **Gitea API**: REST API at `/api/v1/` endpoints
|
||||
- **SQLite**: Local database (no server required)
|
||||
- Future: GitHub API, GitLab API, JIRA API
|
||||
|
||||
## Repository Context
|
||||
|
||||
This is a capability within the larger markitect project (`/capabilities/issue-facade/`). 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
|
||||
# issue-core — Claude Code Instructions
|
||||
|
||||
@SCOPE.md
|
||||
@.claude/rules/repo-identity.md
|
||||
@.claude/rules/session-protocol.md
|
||||
@.claude/rules/first-session.md
|
||||
@.claude/rules/workplan-convention.md
|
||||
@.claude/rules/stack-and-commands.md
|
||||
@.claude/rules/architecture.md
|
||||
@.claude/rules/repo-boundary.md
|
||||
@.claude/rules/credential-routing.md
|
||||
@.claude/rules/agents.md
|
||||
|
||||
29
Dockerfile
Normal file
29
Dockerfile
Normal file
@@ -0,0 +1,29 @@
|
||||
# issue-core REST ingestion service image.
|
||||
#
|
||||
# Builds the checked-out issue-core[api] package and runs the FastAPI ingestion
|
||||
# server on :8765. The image is published to
|
||||
# gitea.coulomb.social/coulomb/issue-core.
|
||||
FROM python:3.12-slim AS runtime
|
||||
|
||||
ENV PYTHONUNBUFFERED=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
HOME=/home/app
|
||||
|
||||
# Non-root runtime user; HOME drives issue-core's config dir
|
||||
# (~/.config/issue-tracker/backends.json).
|
||||
RUN useradd --create-home --home-dir /home/app --uid 10001 app
|
||||
|
||||
WORKDIR /src
|
||||
COPY pyproject.toml README.md LICENSE ./
|
||||
COPY issue_core ./issue_core
|
||||
RUN pip install --no-cache-dir --index-url https://pypi.org/simple ".[api]" \
|
||||
&& rm -rf /src
|
||||
|
||||
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
USER app
|
||||
EXPOSE 8765
|
||||
|
||||
# Entrypoint renders backends.json from env, then execs the server.
|
||||
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||
116
INTENT.md
Normal file
116
INTENT.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# INTENT — issue-core
|
||||
|
||||
## Why it exists
|
||||
|
||||
The Coulomb org needs a **single, observable place where tasks land** — regardless
|
||||
of whether they were created by a human typing a CLI command, by an automation
|
||||
like activity-core acting on a rule, or by an agent acting on instructions.
|
||||
|
||||
Without a single landing zone, task creation fragments across:
|
||||
- Per-repo Gitea issue trackers (siloed, no cross-repo view)
|
||||
- Ad hoc files and TODO comments (invisible, unaudited)
|
||||
- Agent-local memory and notebooks (lost when the agent ends)
|
||||
- External SaaS trackers (rate-limited, off-network)
|
||||
|
||||
issue-core gives every actor — human or machine — one stable, observable place
|
||||
to file work, and one stable surface to consume work from.
|
||||
|
||||
## What it is
|
||||
|
||||
A **task lifecycle manager** with a pluggable-backend architecture.
|
||||
|
||||
Responsibilities:
|
||||
- **Ingestion**: accept new tasks via CLI, REST (`POST /issues/`), and — in the
|
||||
future — NATS subscriptions.
|
||||
- **Storage**: route each task to the configured backend (Gitea, SQLite, GitHub).
|
||||
- **Lifecycle**: create → assign → update → close, with state transitions that
|
||||
hold regardless of backend.
|
||||
- **Querying**: list, search, filter across the active backend.
|
||||
- **Synchronization**: bidirectional sync between local SQLite (source of truth
|
||||
for offline work) and remote backends.
|
||||
|
||||
Backends today: **local SQLite**, **Gitea**. Planned: **GitHub**, **GitLab**, **JIRA**.
|
||||
|
||||
The CLI entry points are `issue` (primary) and `issue-core` (explicit alias).
|
||||
|
||||
## What it is NOT
|
||||
|
||||
issue-core is intentionally narrow. The following live elsewhere:
|
||||
|
||||
- **Not a project manager.** Phases, campaigns, milestones spanning multiple tasks,
|
||||
dependency graphs across tasks, gantt-style scheduling — that is the domain of
|
||||
`project-core` (planned). issue-core deals in individual tasks, not in plans
|
||||
composed of tasks.
|
||||
|
||||
- **Not a spawn audit trail.** When activity-core fires a rule that creates a task,
|
||||
the *spawn event* (who fired, what rule, what triggering event) is recorded in
|
||||
activity-core's `task_spawn_log`. issue-core only stores the resulting task and
|
||||
its `triggering_event_id` reference back. The audit-of-creation belongs to the
|
||||
emitter.
|
||||
|
||||
- **Not an event bus.** Communication between services flows over NATS (and
|
||||
state-hub progress events). issue-core consumes events, but does not relay them.
|
||||
|
||||
- **Not a notification system.** Surfacing "your task changed" to humans is the
|
||||
job of the relevant UI / digest / chatbot layer, not issue-core.
|
||||
|
||||
- **Not a workflow engine.** State transitions are simple (open → closed, with
|
||||
a few in-between states). Conditional routing, approvals, multi-step
|
||||
workflows — out of scope.
|
||||
|
||||
## How it fits
|
||||
|
||||
```
|
||||
+-------------------+
|
||||
| activity-core |
|
||||
| IssueSink (REST) |
|
||||
+---------+---------+
|
||||
|
|
||||
POST /issues/ (TaskSpec payload)
|
||||
|
|
||||
v
|
||||
+------------+ +-------+--------+ +-----------------+
|
||||
| Humans +----->| |<-----+ Agents |
|
||||
| CLI: | | issue-core | | (CLI or REST) |
|
||||
| $ issue | | | | |
|
||||
+------------+ +-------+--------+ +-----------------+
|
||||
|
|
||||
+---------+----------+
|
||||
| Backend router |
|
||||
+---+------+------+--+
|
||||
| | |
|
||||
v v v
|
||||
+------+ +-----+ +------+
|
||||
|Gitea | |SQLite| |GitHub|
|
||||
+------+ +-----+ +------+
|
||||
```
|
||||
|
||||
**Upstream of issue-core (emitters):**
|
||||
- **activity-core** — emits tasks via `IssueSink` when a rule fires or an
|
||||
instruction declares one. Payload: `TaskSpec` over `POST /issues/`.
|
||||
- **Humans** — `$ issue create ...` from terminals; future web UI.
|
||||
- **Agents** — same REST surface or CLI.
|
||||
|
||||
**Downstream of issue-core (consumers):**
|
||||
- **Humans and agents** assigned tasks consume them via `$ issue list`, web UI,
|
||||
or per-backend native UIs (Gitea web, GitHub PR view, etc.).
|
||||
- **state-hub** receives progress events as tasks move through their lifecycle.
|
||||
- **Status updates** flow back to issue-core, not to the original emitter —
|
||||
activity-core does not track what happened to the task it spawned.
|
||||
|
||||
## Success looks like
|
||||
|
||||
- Every task in the Coulomb org is discoverable from one query surface.
|
||||
- activity-core can fire a rule and have the resulting task land in the right
|
||||
backend with the right metadata, with no human in the loop.
|
||||
- The CLI experience is identical across SQLite-only laptops and full Gitea-
|
||||
backed servers.
|
||||
- Offline work syncs back cleanly when connectivity returns.
|
||||
|
||||
## See also
|
||||
|
||||
- `SCOPE.md` — concrete in/out-of-scope decisions and integration boundaries.
|
||||
- `ROADMAP.md` — feature trajectory.
|
||||
- `workplans/` — active workstreams.
|
||||
- activity-core `docs/adr/adr-001-event-bridge-architecture.md` — the IssueSink
|
||||
contract that issue-core honors at `POST /issues/`.
|
||||
153
Makefile
153
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,35 +30,49 @@ 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"
|
||||
@echo " test-integration Run integration tests only"
|
||||
@echo " test-cov Run tests with coverage report"
|
||||
@echo " test-verbose Run tests with verbose output"
|
||||
@echo " package Build source and wheel distributions"
|
||||
@echo " package-check Build and validate distributions"
|
||||
@echo " publish-gitea Publish distributions to Gitea PyPI"
|
||||
@echo ""
|
||||
@echo "Feedback & Continuous Improvement:"
|
||||
@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)
|
||||
|
||||
PYTHON ?= python3
|
||||
GITEA_PACKAGE_OWNER ?= coulomb
|
||||
GITEA_PYPI_REPOSITORY_URL ?= https://gitea.coulomb.social/api/packages/$(GITEA_PACKAGE_OWNER)/pypi
|
||||
|
||||
# Core Issue Operations
|
||||
.PHONY: issue-list
|
||||
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
|
||||
@@ -175,7 +189,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
|
||||
@@ -196,56 +210,103 @@ 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)
|
||||
pytest tests/ -v
|
||||
|
||||
.PHONY: issue-facade-install
|
||||
issue-facade-install: ## Install issue facade capability
|
||||
pip install -e capabilities/issue-facade/
|
||||
.PHONY: clean-dist
|
||||
clean-dist: ## Remove local Python package build artifacts
|
||||
rm -rf build/ dist/ *.egg-info/
|
||||
|
||||
.PHONY: issue-facade-install-dev
|
||||
issue-facade-install-dev: ## Install issue facade capability with development dependencies
|
||||
pip install -e "capabilities/issue-facade/[dev]"
|
||||
.PHONY: package
|
||||
package: clean-dist ## Build source and wheel distributions
|
||||
$(PYTHON) -m build
|
||||
|
||||
.PHONY: issue-facade-test
|
||||
issue-facade-test: ## Run issue facade tests
|
||||
cd capabilities/issue-facade && pytest tests/
|
||||
.PHONY: package-check
|
||||
package-check: package ## Build and validate source/wheel distributions
|
||||
$(PYTHON) -m twine check dist/*
|
||||
|
||||
.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: publish-gitea
|
||||
publish-gitea: package-check ## Publish distributions to the Coulomb Gitea PyPI registry
|
||||
ifndef TWINE_USERNAME
|
||||
$(error TWINE_USERNAME is required)
|
||||
endif
|
||||
ifndef TWINE_PASSWORD
|
||||
$(error TWINE_PASSWORD is required)
|
||||
endif
|
||||
$(PYTHON) -m twine upload --repository-url "$(GITEA_PYPI_REPOSITORY_URL)" dist/*
|
||||
|
||||
.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"
|
||||
# Feedback and Continuous Improvement
|
||||
.PHONY: feedback
|
||||
feedback: ## Submit feedback (Usage: make feedback MSG="your feedback")
|
||||
@./.capability/feedback submit "$(MSG)"
|
||||
|
||||
.PHONY: feedback-list
|
||||
feedback-list: ## List pending feedback
|
||||
@./.capability/feedback list
|
||||
|
||||
.PHONY: feedback-stats
|
||||
feedback-stats: ## Show feedback statistics
|
||||
@./.capability/feedback stats
|
||||
|
||||
.PHONY: feedback-show
|
||||
feedback-show: ## Show specific feedback (Usage: make feedback-show FILE=20251217-xxx.md)
|
||||
@./.capability/feedback show "$(FILE)"
|
||||
|
||||
.PHONY: feedback-review
|
||||
feedback-review: ## Review feedback (Usage: make feedback-review FILE=20251217-xxx.md)
|
||||
@./.capability/feedback review "$(FILE)"
|
||||
|
||||
.PHONY: feedback-review-issue
|
||||
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-core-install
|
||||
issue-core-install: ## Install issue core capability
|
||||
pip install -e capabilities/issue-core/
|
||||
|
||||
.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-core-test
|
||||
issue-core-test: ## Run issue core tests
|
||||
cd capabilities/issue-core && pytest tests/
|
||||
|
||||
.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-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"
|
||||
@@ -258,7 +319,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
|
||||
@@ -326,4 +387,4 @@ capability-info: ## Show capability information
|
||||
@echo "Supported backends: Local SQLite, GitHub, GitLab, Gitea"
|
||||
@echo "Key features: Repository-aware, offline-capable, unified interface"
|
||||
@echo "Targets:"
|
||||
@$(MAKE) --no-print-directory help | grep "^ " | sed 's/^ / /'
|
||||
@$(MAKE) --no-print-directory help | grep "^ " | sed 's/^ / /'
|
||||
|
||||
43
README.md
43
README.md
@@ -1,10 +1,10 @@
|
||||
# Issue Facade - Agent Coordination via Issue Tracking
|
||||
# Issue Core - Agent Coordination via Issue Tracking
|
||||
|
||||
**A unified interface for autonomous coding agents to coordinate project implementation through issue tracking systems.**
|
||||
|
||||
## Purpose
|
||||
|
||||
The **Issue Facade** provides a standardized abstraction layer for coding agents to interact with issue tracking backends (Gitea, GitHub, GitLab, local SQLite). Instead of each agent implementing platform-specific API integrations, they use one consistent interface that works across all backends.
|
||||
The **Issue Core** provides a standardized abstraction layer for coding agents to interact with issue tracking backends (Gitea, GitHub, GitLab, local SQLite). Instead of each agent implementing platform-specific API integrations, they use one consistent interface that works across all backends.
|
||||
|
||||
### Why Issue Tracking for Agent Coordination?
|
||||
|
||||
@@ -40,10 +40,25 @@ Issue tracking provides natural coordination primitives for multi-agent software
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
cd capabilities/issue-facade
|
||||
pip install -e .
|
||||
cd capabilities/issue-core
|
||||
pip install -e . # CLI only
|
||||
pip install -e ".[api]" # CLI + REST ingestion server
|
||||
```
|
||||
|
||||
### REST Ingestion Server
|
||||
|
||||
issue-core exposes `POST /issues/` for upstream emitters (primarily
|
||||
activity-core's `IssueSink`). Launch with:
|
||||
|
||||
```bash
|
||||
export ISSUE_CORE_API_KEY="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')"
|
||||
issue serve --host 0.0.0.0 --port 8765
|
||||
```
|
||||
|
||||
Clients authenticate with `Authorization: Bearer <key>` or `X-API-Key: <key>`.
|
||||
See `SCOPE.md` "TaskSpec payload" for the request schema, or visit
|
||||
`http://<host>:<port>/docs` once the server is running for live OpenAPI docs.
|
||||
|
||||
### Configuration (One-Time Setup)
|
||||
|
||||
**For Gitea-backed projects:**
|
||||
@@ -67,7 +82,7 @@ issue backend test myproject
|
||||
|
||||
```bash
|
||||
issue backend add local-work local
|
||||
# Prompts for: database path (.issue-facade/issues.db)
|
||||
# Prompts for: database path (.issue-core/issues.db)
|
||||
|
||||
issue backend set-default local-work
|
||||
```
|
||||
@@ -108,8 +123,8 @@ issue close 42 --comment="Ready for review"
|
||||
Quick example:
|
||||
|
||||
```python
|
||||
from issue_tracker.backends.gitea import GiteaBackend
|
||||
from issue_tracker.core.interfaces import IssueFilter
|
||||
from issue_core.backends.gitea import GiteaBackend
|
||||
from issue_core.core.interfaces import IssueFilter
|
||||
|
||||
# Initialize
|
||||
backend = GiteaBackend()
|
||||
@@ -281,20 +296,20 @@ make test-unit
|
||||
|
||||
```bash
|
||||
# Run linter
|
||||
make issue-facade-lint
|
||||
make issue-core-lint
|
||||
|
||||
# Format code
|
||||
black issue_tracker/ tests/
|
||||
black issue_core/ tests/
|
||||
|
||||
# Type check
|
||||
mypy issue_tracker/
|
||||
mypy issue_core/
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
issue-facade/
|
||||
├── issue_tracker/
|
||||
issue-core/
|
||||
├── issue_core/
|
||||
│ ├── core/ # Domain models and interfaces
|
||||
│ │ ├── models.py # Issue, Label, User, etc.
|
||||
│ │ └── interfaces.py # IssueBackend, SyncableBackend
|
||||
@@ -351,7 +366,7 @@ issue-facade/
|
||||
|
||||
## Comparison with Platform CLIs
|
||||
|
||||
| Feature | Issue Facade | gh (GitHub) | glab (GitLab) |
|
||||
| Feature | Issue Core | gh (GitHub) | glab (GitLab) |
|
||||
|---------|--------------|-------------|---------------|
|
||||
| Multi-backend support | ✅ Yes | ❌ GitHub only | ❌ GitLab only |
|
||||
| Offline capability | ✅ Local SQLite | ❌ No | ❌ No |
|
||||
@@ -362,7 +377,7 @@ issue-facade/
|
||||
|
||||
## Contributing
|
||||
|
||||
The Issue Facade is designed to be extensible:
|
||||
The Issue Core is designed to be extensible:
|
||||
|
||||
**To add a new backend:**
|
||||
1. Implement the `IssueBackend` interface (see `core/interfaces.py`)
|
||||
|
||||
36
ROADMAP.md
36
ROADMAP.md
@@ -1,4 +1,4 @@
|
||||
# Issue Facade Roadmap
|
||||
# Issue Core Roadmap
|
||||
|
||||
**Long-term vision and implementation plan for agent-driven software development coordination.**
|
||||
|
||||
@@ -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:
|
||||
|
||||
1182
ReusableCapabilitiesArchitecture.md
Normal file
1182
ReusableCapabilitiesArchitecture.md
Normal file
File diff suppressed because it is too large
Load Diff
170
SCOPE.md
Normal file
170
SCOPE.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# 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": "event uuid or stable source key",
|
||||
"activity_definition_id": "string"
|
||||
}
|
||||
```
|
||||
|
||||
`triggering_event_id` is accepted as a non-empty string. Event-driven
|
||||
emissions should send the upstream activity event UUID. Scheduled or cron
|
||||
emissions that do not have a concrete event row may send a stable source key
|
||||
such as `scheduled`; issue-core stores the value verbatim in ingestion
|
||||
metadata for traceability.
|
||||
|
||||
### `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.
|
||||
30
docker-entrypoint.sh
Normal file
30
docker-entrypoint.sh
Normal file
@@ -0,0 +1,30 @@
|
||||
#!/bin/sh
|
||||
# Render issue-core backends.json from environment, then start the API.
|
||||
#
|
||||
# The backend structure (host/owner/repo/default) is non-secret and supplied
|
||||
# via the BACKENDS_TEMPLATE env (a ConfigMap), with the Gitea token injected
|
||||
# from GITEA_BACKEND_TOKEN (an ExternalSecret-materialized Secret). The token
|
||||
# is never baked into the image or committed to Git.
|
||||
set -eu
|
||||
|
||||
CONFIG_DIR="${HOME}/.config/issue-tracker"
|
||||
mkdir -p "${CONFIG_DIR}"
|
||||
|
||||
: "${BACKENDS_TEMPLATE:?BACKENDS_TEMPLATE env is required}"
|
||||
|
||||
# Substitute the token placeholder using python (always present in the image)
|
||||
# to avoid shell-escaping issues with the secret value.
|
||||
GITEA_BACKEND_TOKEN="${GITEA_BACKEND_TOKEN:-}" \
|
||||
BACKENDS_TEMPLATE="${BACKENDS_TEMPLATE}" \
|
||||
python - "${CONFIG_DIR}/backends.json" <<'PY'
|
||||
import json, os, sys
|
||||
tmpl = json.loads(os.environ["BACKENDS_TEMPLATE"])
|
||||
token = os.environ.get("GITEA_BACKEND_TOKEN", "")
|
||||
for cfg in tmpl.values():
|
||||
if isinstance(cfg, dict) and cfg.get("token") == "__FROM_ENV__":
|
||||
cfg["token"] = token
|
||||
with open(sys.argv[1], "w") as fh:
|
||||
json.dump(tmpl, fh, indent=2)
|
||||
PY
|
||||
|
||||
exec issue serve --host 0.0.0.0 --port 8765 --log-level "${LOG_LEVEL:-info}"
|
||||
168
docs/argocd-gitops.md
Normal file
168
docs/argocd-gitops.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# ArgoCD GitOps deployment - railiance01
|
||||
|
||||
This runbook captures the issue-core side of the railiance01 GitOps pilot.
|
||||
It keeps secrets out of Git and leaves platform-owned bootstrap steps in
|
||||
railiance-platform.
|
||||
|
||||
## Source layout
|
||||
|
||||
- Workload bundle: `issue-core/k8s/railiance/`
|
||||
- Image: `gitea.coulomb.social/coulomb/issue-core:0.2.1`
|
||||
- Container port and Service port: `8765`
|
||||
- Cluster Service URL: `http://issue-core.issue-core.svc.cluster.local:8765`
|
||||
- Tenant Application: `railiance-platform/argocd/applications/issue-core.application.yaml`
|
||||
|
||||
The `Application` should point at this repo's `k8s/railiance` path and use
|
||||
`CreateNamespace=true` for the `issue-core` namespace. The namespace itself is
|
||||
therefore intentionally not duplicated in this bundle.
|
||||
|
||||
## Platform gates
|
||||
|
||||
The following pieces are owned by railiance-platform for the live pilot and for
|
||||
any future cluster replay:
|
||||
|
||||
- ArgoCD repository credentials and the project/app-of-apps convention.
|
||||
- The `issue-core` ArgoCD `Application`.
|
||||
- External Secrets Operator and a `ClusterSecretStore` named `openbao`.
|
||||
- OpenBao entries for the issue-core runtime Secret.
|
||||
|
||||
For the 2026-06-25 live deployment, these gates were satisfied and the
|
||||
`issue-core` Application reached Synced/Healthy with image `0.2.1`.
|
||||
|
||||
## Secret contract
|
||||
|
||||
Kubernetes Secret name: `issue-core-runtime`
|
||||
|
||||
Current issue-core manifest path:
|
||||
|
||||
```text
|
||||
platform/workloads/issue-core/issue-core/issue-core-runtime
|
||||
```
|
||||
|
||||
Credential custody is owned by railiance-platform/OpenBao. For agents, first
|
||||
use the non-secret route catalog entry `activity-core-issue-sink` to confirm
|
||||
the activity-core + issue-core pairing, and never request the value from
|
||||
ops-warden.
|
||||
|
||||
Required properties:
|
||||
|
||||
- `ISSUE_CORE_API_KEY` - shared ingestion key used by issue-core and
|
||||
activity-core.
|
||||
- `GITEA_BACKEND_TOKEN` - token for creating issues in cluster Gitea.
|
||||
|
||||
Never write either value to Git, State Hub, workplans, logs, or chat. Record
|
||||
only non-secret evidence such as Secret key count, ExternalSecret readiness,
|
||||
HTTP status codes, and created issue URLs.
|
||||
|
||||
## Build and publish
|
||||
|
||||
Build the checked-out source tree and publish a registry tag that ArgoCD can
|
||||
pull:
|
||||
|
||||
```bash
|
||||
docker build -t gitea.coulomb.social/coulomb/issue-core:0.2.1 .
|
||||
docker push gitea.coulomb.social/coulomb/issue-core:0.2.1
|
||||
```
|
||||
|
||||
The Coulomb Gitea package is public-pullable for this image, so the workload
|
||||
does not use an `imagePullSecret`.
|
||||
|
||||
## Pre-sync validation
|
||||
|
||||
From the issue-core repo:
|
||||
|
||||
```bash
|
||||
kubectl kustomize k8s/railiance
|
||||
```
|
||||
|
||||
The rendered resources should be:
|
||||
|
||||
- `ExternalSecret/issue-core-runtime`
|
||||
- `ConfigMap/issue-core-backends`
|
||||
- `Deployment/issue-core`
|
||||
- `Service/issue-core`
|
||||
|
||||
## Sync verification
|
||||
|
||||
After railiance-platform syncs the tenant `Application`:
|
||||
|
||||
```bash
|
||||
kubectl get application issue-core -n argocd
|
||||
kubectl -n issue-core get externalsecret issue-core-runtime
|
||||
kubectl -n issue-core get secret issue-core-runtime
|
||||
kubectl -n issue-core get deploy,pod,svc
|
||||
```
|
||||
|
||||
Expected non-secret evidence:
|
||||
|
||||
- ArgoCD Application reports `Synced` and `Healthy`.
|
||||
- `ExternalSecret/issue-core-runtime` reports Ready.
|
||||
- `Secret/issue-core-runtime` exists with two data keys.
|
||||
- `Deployment/issue-core` has one available replica.
|
||||
- `Service/issue-core` exposes port `8765`.
|
||||
|
||||
Health check from inside the cluster:
|
||||
|
||||
```bash
|
||||
kubectl -n issue-core run issue-core-health --rm -i --restart=Never --image=curlimages/curl:8.8.0 -- http://issue-core:8765/healthz
|
||||
```
|
||||
|
||||
## Ingestion smoke
|
||||
|
||||
Run the authenticated smoke from a short-lived Job so the API key is mounted
|
||||
from the Kubernetes Secret without printing it:
|
||||
|
||||
```bash
|
||||
kubectl -n issue-core delete job issue-core-smoke --ignore-not-found
|
||||
kubectl -n issue-core apply -f - <<'YAML'
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: issue-core-smoke
|
||||
spec:
|
||||
ttlSecondsAfterFinished: 600
|
||||
backoffLimit: 0
|
||||
template:
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
containers:
|
||||
- name: smoke
|
||||
image: curlimages/curl:8.8.0
|
||||
env:
|
||||
- name: ISSUE_CORE_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: issue-core-runtime
|
||||
key: ISSUE_CORE_API_KEY
|
||||
command: ["/bin/sh", "-ceu"]
|
||||
args:
|
||||
- |
|
||||
curl -fsS -X POST "http://issue-core:8765/issues/" -H "Authorization: Bearer ${ISSUE_CORE_API_KEY}" -H "Content-Type: application/json" --data '{"title":"issue-core railiance01 smoke","description":"GitOps smoke created by the issue-core deployment runbook.","target_repo":"coulomb/markitect-main","priority":"low","labels":["smoke","issue-core"],"source_type":"rule","source_id":"issue-core-gitops-smoke","triggering_event_id":"scheduled","activity_definition_id":"issue-core-gitops-smoke"}'
|
||||
YAML
|
||||
kubectl -n issue-core wait --for=condition=complete job/issue-core-smoke --timeout=90s
|
||||
kubectl -n issue-core logs job/issue-core-smoke
|
||||
```
|
||||
|
||||
Acceptance evidence is HTTP 201 plus a response body containing `issue_id`,
|
||||
`backend: "gitea"`, and an `issue_url` for cluster Gitea.
|
||||
|
||||
Cleanup:
|
||||
|
||||
```bash
|
||||
kubectl -n issue-core delete job issue-core-smoke
|
||||
```
|
||||
|
||||
## Activity-core handoff
|
||||
|
||||
After issue-core is Ready and the shared `ISSUE_CORE_API_KEY` is available to
|
||||
activity-core from the same approved OpenBao source:
|
||||
|
||||
- Set `ISSUE_CORE_URL=http://issue-core.issue-core.svc.cluster.local:8765`.
|
||||
- Set `ISSUE_SINK_TYPE=rest`.
|
||||
- Inject the same `ISSUE_CORE_API_KEY` into the activity-core worker.
|
||||
- Keep cron-triggered emissions explicit: `triggering_event_id` may be a stable
|
||||
non-empty scheduler key such as `scheduled`; event-driven emissions should
|
||||
continue to send the event UUID.
|
||||
|
||||
Verify by running an activity-core emission and confirming that issue-core
|
||||
returns HTTP 201 and creates a Gitea issue.
|
||||
143
docs/nats-task-ingestion.md
Normal file
143
docs/nats-task-ingestion.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# NATS Task Ingestion — Design Stub
|
||||
|
||||
**Status:** design stub. Implementation deferred until activity-core's
|
||||
`IssueSink` migrates from REST to NATS.
|
||||
|
||||
**Scope:** describe what the NATS-backed counterpart of `POST /issues/` will
|
||||
look like, so the activity-core agent and any other future emitter can plan
|
||||
against a stable contract.
|
||||
|
||||
## Why NATS
|
||||
|
||||
Today the ingestion surface is `POST /issues/` — synchronous REST with an API
|
||||
key. That works for activity-core's first cut but has limitations:
|
||||
|
||||
- **Coupling**: activity-core needs to know the URL and key of every issue-core
|
||||
instance. With NATS, both sides connect to a shared broker; routing is by
|
||||
subject.
|
||||
- **Backpressure**: REST is best-effort. If issue-core is down or slow, the
|
||||
emitter either blocks or drops. With NATS JetStream, messages are durable and
|
||||
replay-capable.
|
||||
- **Fan-out**: REST has one consumer. NATS supports multiple consumers (e.g. an
|
||||
audit logger sitting alongside the actual ingester) trivially.
|
||||
- **Replay**: incidents that lose tasks can be reconstructed from the JetStream
|
||||
log if the consumer was offline.
|
||||
|
||||
## Subject pattern
|
||||
|
||||
```
|
||||
act.tasks.create.{target_repo}
|
||||
```
|
||||
|
||||
- Namespace prefix `act.tasks.` (the `act` is activity-core's heritage — the
|
||||
subject prefix is now neutral and other emitters can publish on it too).
|
||||
- `create` is the verb. Future verbs (`act.tasks.update`, `act.tasks.close`)
|
||||
are reserved but not in scope here.
|
||||
- `{target_repo}` is the same string field as the REST `TaskSpec.target_repo`.
|
||||
It allows subject-based routing in consumers: an issue-core instance
|
||||
responsible only for one repo subscribes to `act.tasks.create.myrepo`, while
|
||||
a multi-tenant instance subscribes to `act.tasks.create.>`.
|
||||
|
||||
## Message schema
|
||||
|
||||
The payload is the **exact same** schema as the REST endpoint —
|
||||
`TaskIngestionRequest` in `issue_core/api/schemas.py`:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "string",
|
||||
"description": "string",
|
||||
"target_repo": "string",
|
||||
"priority": "high | medium | low",
|
||||
"labels": ["string"],
|
||||
"due_in_days": 7,
|
||||
"source_type": "rule | instruction",
|
||||
"source_id": "string",
|
||||
"triggering_event_id": "uuid",
|
||||
"activity_definition_id": "string"
|
||||
}
|
||||
```
|
||||
|
||||
Encoded as **JSON** in the message body. `Content-Type: application/json`
|
||||
in the message header.
|
||||
|
||||
This intentionally matches the REST schema so the validator and `_build_issue`
|
||||
logic in `issue_core/api/ingest.py` can be reused unchanged by the NATS
|
||||
consumer.
|
||||
|
||||
## JetStream configuration
|
||||
|
||||
The publisher (e.g. activity-core IssueSink-NATS) writes to a JetStream stream:
|
||||
|
||||
| Field | Value |
|
||||
|---------------|----------------------------------------|
|
||||
| Stream name | `ACT_TASKS` |
|
||||
| Subjects | `act.tasks.>` |
|
||||
| Retention | Limits (Time-based: 7 days) |
|
||||
| Storage | File |
|
||||
| Replicas | 3 in prod, 1 in dev |
|
||||
| Discard | Old (drop oldest on overflow) |
|
||||
| Max msg size | 64 KiB (TaskSpec is small) |
|
||||
|
||||
issue-core consumes via a **durable consumer**:
|
||||
|
||||
| Field | Value |
|
||||
|-----------------|----------------------------------------|
|
||||
| Stream | `ACT_TASKS` |
|
||||
| Consumer name | `issue-core-ingest` |
|
||||
| Filter subject | `act.tasks.create.>` |
|
||||
| Deliver policy | All (catch up from oldest on first start) |
|
||||
| Ack policy | Explicit |
|
||||
| Max deliver | 5 (then dead-letter) |
|
||||
| Ack wait | 30s |
|
||||
| Replay policy | Instant |
|
||||
|
||||
## Idempotency
|
||||
|
||||
NATS JetStream provides **at-least-once** delivery. The consumer must dedupe
|
||||
retries.
|
||||
|
||||
**Idempotency key:** `triggering_event_id` (UUID, included in every payload).
|
||||
|
||||
The consumer's responsibility:
|
||||
|
||||
1. Compute idempotency key from `triggering_event_id`.
|
||||
2. Check whether an issue with that key already exists (lookup by
|
||||
`sync_metadata.ingestion.triggering_event_id`).
|
||||
3. If exists, ack the message without creating a duplicate.
|
||||
4. If not, create the issue and ack.
|
||||
|
||||
Both REST and NATS paths share this dedupe logic, so a task can be safely
|
||||
emitted via either transport without risk of duplicate issues.
|
||||
|
||||
## Implementation plan (when activated)
|
||||
|
||||
1. Add `nats-py>=2.6` as an optional dependency (`pip install issue-core[nats]`).
|
||||
2. New module `issue_core/nats/consumer.py` — connects to NATS, subscribes to
|
||||
the durable consumer, parses messages, calls the same `_build_issue` /
|
||||
backend.create_issue path as the REST endpoint.
|
||||
3. New CLI subcommand `issue subscribe --nats-url ... --stream ACT_TASKS`.
|
||||
4. Add idempotency check to both REST and NATS ingestion paths (single shared
|
||||
function in `issue_core/api/ingest.py` or a new `issue_core/ingestion/`
|
||||
module).
|
||||
5. Tests using `nats-py` test harness or a docker-compose NATS instance.
|
||||
|
||||
## Open questions
|
||||
|
||||
- Should the NATS consumer write a `progress_event` to the state hub on each
|
||||
successful ingestion, in addition to creating the issue? Probably yes, but
|
||||
out of scope until activation.
|
||||
- Multi-tenant routing: do we run one issue-core consumer per `target_repo`,
|
||||
or one shared consumer with per-repo backend lookup? Current bias: shared
|
||||
consumer, simpler to operate.
|
||||
- Dead-letter handling: where do messages go after 5 failed deliveries?
|
||||
Candidate: a `ACT_TASKS_DLQ` stream with manual replay tooling.
|
||||
|
||||
## See also
|
||||
|
||||
- `SCOPE.md` — confirms NATS ingestion is in-scope as a future surface.
|
||||
- `issue_core/api/schemas.py` — the canonical `TaskIngestionRequest` schema.
|
||||
- `issue_core/api/ingest.py` — the REST handler whose logic the NATS consumer
|
||||
will share.
|
||||
- activity-core `docs/adr/adr-001-event-bridge-architecture.md` — describes
|
||||
activity-core's migration trajectory from REST to NATS.
|
||||
51
docs/package-release.md
Normal file
51
docs/package-release.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Python Package Release
|
||||
|
||||
`issue-core` publishes as the `issue-core` Python package. The Railiance
|
||||
application deployment path expects the `0.2.x` series to be available from the
|
||||
Coulomb Gitea PyPI registry.
|
||||
|
||||
## Local Release
|
||||
|
||||
Build and validate the release artifacts:
|
||||
|
||||
```bash
|
||||
make package-check
|
||||
```
|
||||
|
||||
Publish to the Coulomb organization registry:
|
||||
|
||||
```bash
|
||||
TWINE_USERNAME=<gitea-user> \
|
||||
TWINE_PASSWORD=<package-token> \
|
||||
make publish-gitea
|
||||
```
|
||||
|
||||
The package endpoint is:
|
||||
|
||||
```text
|
||||
https://gitea.coulomb.social/api/packages/coulomb/pypi
|
||||
```
|
||||
|
||||
The matching simple index for consumers is:
|
||||
|
||||
```text
|
||||
https://gitea.coulomb.social/api/packages/coulomb/pypi/simple/
|
||||
```
|
||||
|
||||
Do not commit tokenized package index URLs. CI and local Docker builds should
|
||||
inject registry credentials through environment variables or BuildKit secrets.
|
||||
|
||||
## Gitea Actions Release
|
||||
|
||||
The `.gitea/workflows/publish-python-package.yml` workflow publishes on tags
|
||||
matching `v*`. Configure these repository secrets before cutting a release:
|
||||
|
||||
- `GITEA_PACKAGE_USER`
|
||||
- `GITEA_PACKAGE_TOKEN`
|
||||
|
||||
Release `0.2.0` with:
|
||||
|
||||
```bash
|
||||
git tag v0.2.0
|
||||
git push origin v0.2.0
|
||||
```
|
||||
@@ -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 .
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
289
examples/feedback-example.md
Normal file
289
examples/feedback-example.md
Normal file
@@ -0,0 +1,289 @@
|
||||
# Feedback System Example
|
||||
|
||||
This example demonstrates how to submit feedback about the issue-core capability.
|
||||
|
||||
## Quick Feedback Submission
|
||||
|
||||
### Method 1: Using the feedback CLI
|
||||
|
||||
```bash
|
||||
# Navigate to the capability directory
|
||||
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."
|
||||
|
||||
# Submit detailed feedback from a file
|
||||
cat > my-feedback.md << 'EOF'
|
||||
## Performance Issue: Sync with Large Repositories
|
||||
|
||||
**Problem**: The `issue sync pull` command takes 10+ minutes when syncing a repository with 5000+ issues.
|
||||
|
||||
**Context**:
|
||||
- Gitea backend
|
||||
- Repository: company/large-project (5243 issues)
|
||||
- Network: 100 Mbps
|
||||
- System: Ubuntu 22.04, Python 3.11
|
||||
|
||||
**Observed Behavior**:
|
||||
- CPU usage is low (~5%)
|
||||
- Appears to be making sequential API calls
|
||||
- No progress indicator, feels frozen
|
||||
|
||||
**Expected Behavior**:
|
||||
- Sync completes in 1-2 minutes
|
||||
- Progress bar showing "Syncing: 1234/5243"
|
||||
- Possibly batch API requests
|
||||
|
||||
**Suggested Solutions**:
|
||||
1. Parallel API requests (respect rate limits)
|
||||
2. Batch endpoints (if Gitea supports them)
|
||||
3. Progress indicator for UX
|
||||
4. Incremental sync (only changed issues)
|
||||
|
||||
**Willingness to Help**: Happy to test beta versions and provide performance metrics.
|
||||
|
||||
**Contact**: devteam@company.com
|
||||
EOF
|
||||
|
||||
./.capability/feedback submit my-feedback.md
|
||||
```
|
||||
|
||||
### Method 2: Direct File Drop (No CLI Required)
|
||||
|
||||
```bash
|
||||
# Just create a markdown file directly in feedback/inbound/
|
||||
cat > feedback/inbound/$(date +%Y%m%d-%H%M%S)-performance.md << 'EOF'
|
||||
## Quick Note
|
||||
|
||||
The JSON output format for `issue list` is fantastic for scripting!
|
||||
|
||||
One small thing: the timestamps are in ISO format which is great, but
|
||||
could we also have a `--format=json-pretty` that adds nice indentation?
|
||||
Makes debugging so much easier.
|
||||
|
||||
Thanks!
|
||||
EOF
|
||||
```
|
||||
|
||||
### Method 3: From Master Project
|
||||
|
||||
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-core.md << 'EOF'
|
||||
## Feature Request: GitHub Backend
|
||||
|
||||
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.
|
||||
|
||||
This would enable:
|
||||
- Consistent workflow across platforms
|
||||
- Offline sync for GitHub issues
|
||||
- Multi-repo issue searches
|
||||
|
||||
We'd be happy to contribute or test!
|
||||
EOF
|
||||
|
||||
# Copy to capability's feedback directory
|
||||
cp feedback-for-issue-core.md \
|
||||
capabilities/issue-core/feedback/inbound/$(date +%Y%m%d)-github-backend.md
|
||||
```
|
||||
|
||||
## Feedback Categories
|
||||
|
||||
You can optionally categorize your feedback:
|
||||
|
||||
```bash
|
||||
# Bug report
|
||||
./.capability/feedback submit "Bug: crashes when..." --category=bug
|
||||
|
||||
# Feature request
|
||||
./.capability/feedback submit "Please add..." --category=feature
|
||||
|
||||
# Performance issue
|
||||
./.capability/feedback submit "Slow performance..." --category=improvement
|
||||
|
||||
# Question
|
||||
./.capability/feedback submit "How do I..." --category=question
|
||||
|
||||
# General feedback (no category)
|
||||
./.capability/feedback submit "Great tool!"
|
||||
```
|
||||
|
||||
## Including Contact Information
|
||||
|
||||
If you'd like to be contacted about your feedback:
|
||||
|
||||
```bash
|
||||
./.capability/feedback submit "Feature request..." \
|
||||
--category=feature \
|
||||
--contact=myemail@example.com
|
||||
```
|
||||
|
||||
**Note**: Contact information is optional and only used for follow-up. It's never shared externally.
|
||||
|
||||
## Viewing Your Feedback
|
||||
|
||||
```bash
|
||||
# List all pending feedback
|
||||
./.capability/feedback list
|
||||
|
||||
# Show statistics
|
||||
./.capability/feedback stats
|
||||
|
||||
# View specific feedback
|
||||
./.capability/feedback show 20251217-103045-abc12345.md
|
||||
```
|
||||
|
||||
## Types of Feedback We Value
|
||||
|
||||
### Bug Reports
|
||||
- Describe the issue
|
||||
- Steps to reproduce
|
||||
- Expected vs actual behavior
|
||||
- Error messages (if any)
|
||||
- System/environment details
|
||||
|
||||
Example:
|
||||
```
|
||||
Bug: issue list crashes with certain milestone names
|
||||
|
||||
Steps to reproduce:
|
||||
1. issue backend add test gitea
|
||||
2. issue list --milestone="Sprint 2"
|
||||
3. Crashes with "AttributeError: 'NoneType' object..."
|
||||
|
||||
System: macOS 14.1, Python 3.11.5, Gitea 1.21.0
|
||||
```
|
||||
|
||||
### Feature Requests
|
||||
- What you need
|
||||
- Why it's valuable (your use case)
|
||||
- How you imagine it working
|
||||
- Any alternatives you've tried
|
||||
|
||||
Example:
|
||||
```
|
||||
Feature Request: Bulk label operations
|
||||
|
||||
Use Case: We need to relabel 200+ issues from "priority-high" to
|
||||
"priority:high" (changing naming convention).
|
||||
|
||||
Current Approach: Manually editing each issue (very tedious)
|
||||
|
||||
Proposed: issue bulk-edit --filter="label=priority-high" \
|
||||
--remove-label=priority-high \
|
||||
--add-label=priority:high
|
||||
|
||||
This would save hours of manual work.
|
||||
```
|
||||
|
||||
### Performance Issues
|
||||
- What's slow
|
||||
- Your scale/context
|
||||
- Measurements if available
|
||||
- Impact on your workflow
|
||||
|
||||
Example:
|
||||
```
|
||||
Performance: Slow issue list with many labels
|
||||
|
||||
Context: Repository with 150+ labels, 3000 issues
|
||||
|
||||
Observation: `issue list` takes 8 seconds to respond
|
||||
|
||||
Impact: Slows down our agent automation that polls every 30s
|
||||
|
||||
Note: Local SQLite backend is instant, only Gitea backend is slow
|
||||
```
|
||||
|
||||
### UX Feedback
|
||||
- What's confusing or difficult
|
||||
- What worked well
|
||||
- Suggestions for improvement
|
||||
|
||||
Example:
|
||||
```
|
||||
UX: Backend configuration is confusing
|
||||
|
||||
Issue: Setting up a Gitea backend required reading docs multiple times.
|
||||
The prompts don't explain what URL format is expected.
|
||||
|
||||
Suggestion: Show examples in the prompts:
|
||||
"Gitea URL (e.g., https://gitea.example.com):"
|
||||
|
||||
Also: Maybe detect from git remote and offer to auto-configure?
|
||||
```
|
||||
|
||||
### Positive Feedback
|
||||
We love hearing what's working well!
|
||||
|
||||
Example:
|
||||
```
|
||||
The offline sync feature is AMAZING! Being able to work on issues
|
||||
during flights and sync later has changed my workflow completely.
|
||||
|
||||
The local SQLite backend is incredibly fast and the JSON output
|
||||
makes scripting so easy.
|
||||
|
||||
Thank you!
|
||||
```
|
||||
|
||||
## What Happens to Your Feedback
|
||||
|
||||
1. **Submission**: Your feedback lands in `feedback/inbound/`
|
||||
2. **Review**: Maintainers review it (usually within a week)
|
||||
3. **Action**:
|
||||
- Create issue if it needs tracking
|
||||
- Fix immediately if it's quick
|
||||
- Document for roadmap planning
|
||||
- Respond to you if you provided contact
|
||||
4. **Archive**: Moved to `reviewed/` or `archived/` after handling
|
||||
|
||||
## Privacy & Data
|
||||
|
||||
- Feedback is **anonymous by default**
|
||||
- Git paths are captured for context, not shared externally
|
||||
- Contact info is only used for follow-up, never shared
|
||||
- Feedback files may be committed to git (part of the project)
|
||||
|
||||
## Checking Feedback Status
|
||||
|
||||
If you're curious whether your feedback was addressed:
|
||||
|
||||
```bash
|
||||
# Check if it's still in inbound (pending review)
|
||||
ls feedback/inbound/ | grep <keyword>
|
||||
|
||||
# Check if it's been reviewed
|
||||
ls feedback/reviewed/ | grep <keyword>
|
||||
|
||||
# Check if an issue was created
|
||||
issue list --label=feedback --search="<your topic>"
|
||||
```
|
||||
|
||||
## For Capability Maintainers
|
||||
|
||||
If you maintain this capability, see the maintainer guide:
|
||||
- **Review workflow**: `.capability/feedback list` and `review`
|
||||
- **Creating issues**: `.capability/feedback review <id> --create-issue`
|
||||
- **Statistics**: `.capability/feedback stats`
|
||||
- **Full docs**: `feedback/README.md`
|
||||
|
||||
## Questions?
|
||||
|
||||
If you have questions about the feedback system itself, that's also feedback!
|
||||
|
||||
```bash
|
||||
./.capability/feedback submit "Question about feedback: How often is it reviewed?"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Thank you for helping make issue-core better through your feedback!**
|
||||
367
feedback/README.md
Normal file
367
feedback/README.md
Normal file
@@ -0,0 +1,367 @@
|
||||
# Feedback System
|
||||
|
||||
**A lightweight, unstructured feedback loop for continuous capability improvement.**
|
||||
|
||||
This capability uses the **feedback pattern** - a simple, reusable approach for collecting user feedback without imposing structure or process overhead.
|
||||
|
||||
## Philosophy
|
||||
|
||||
**Capability Owns Feedback**: Each capability maintains its own `feedback/` directory. The capability decides how to organize, prioritize, and act on feedback.
|
||||
|
||||
**No Structure Imposement**: Users provide feedback in whatever format works for them - quick notes, detailed reports, markdown files. The only requirement is clear communication.
|
||||
|
||||
**Easy Submission**: One command, or just drop a file. No forms, no required fields, no process.
|
||||
|
||||
**Maintainer Discretion**: Review and act on feedback at your own pace. Create issues, fix directly, or archive for later.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
feedback/
|
||||
├── inbound/ # New feedback awaiting review
|
||||
├── reviewed/ # Feedback that's been reviewed (optional)
|
||||
├── archived/ # Old/resolved feedback (optional)
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
**Only `inbound/` is required.** The other directories are optional organizational tools.
|
||||
|
||||
## For Users: Submitting Feedback
|
||||
|
||||
### Option 1: Direct File Drop (Simplest)
|
||||
|
||||
Just create a markdown file in `feedback/inbound/`:
|
||||
|
||||
```bash
|
||||
cat > feedback/inbound/$(date +%Y%m%d-%H%M%S)-my-feedback.md << 'EOF'
|
||||
## Sync Performance Issue
|
||||
|
||||
The sync command takes 10+ minutes with 5000 issues.
|
||||
|
||||
Would be great to have progress indicators and maybe batch processing.
|
||||
|
||||
Thanks!
|
||||
EOF
|
||||
```
|
||||
|
||||
### Option 2: Use Feedback CLI (If Available)
|
||||
|
||||
Some capabilities provide a `feedback` command:
|
||||
|
||||
```bash
|
||||
# Quick text feedback
|
||||
feedback submit "Feature request: Add bulk operations"
|
||||
|
||||
# From file
|
||||
feedback submit my-detailed-feedback.md
|
||||
|
||||
# With metadata
|
||||
feedback submit "Bug report" --category=bug --contact=me@email.com
|
||||
```
|
||||
|
||||
### Option 3: From Master Project
|
||||
|
||||
If you're integrating this capability into a master project:
|
||||
|
||||
```bash
|
||||
cd my-master-project
|
||||
echo "Feedback about issue-core..." > feedback.md
|
||||
|
||||
# Copy to capability's feedback directory
|
||||
cp feedback.md capabilities/issue-core/feedback/inbound/$(date +%Y%m%d-%H%M%S)-sync-issue.md
|
||||
```
|
||||
|
||||
### What to Include
|
||||
|
||||
Whatever helps communicate your feedback:
|
||||
- Bug descriptions (steps to reproduce, error messages)
|
||||
- Feature requests (what you need, why it's valuable)
|
||||
- Performance issues (what's slow, your scale/context)
|
||||
- UX friction (what's confusing or difficult)
|
||||
- Positive notes (what's working well!)
|
||||
|
||||
**No required format.** Just be clear.
|
||||
|
||||
## For Maintainers: Processing Feedback
|
||||
|
||||
### Review Workflow
|
||||
|
||||
```bash
|
||||
# 1. List new feedback
|
||||
ls -lt feedback/inbound/
|
||||
|
||||
# 2. Read feedback
|
||||
cat feedback/inbound/20251217-103045-sync-issue.md
|
||||
|
||||
# 3. Decide action:
|
||||
# - Create issue if it needs tracking
|
||||
# - Fix immediately if it's quick
|
||||
# - Document insight if it's informative
|
||||
# - Archive if not applicable
|
||||
|
||||
# 4. Move to reviewed/ or archived/
|
||||
mv feedback/inbound/20251217-103045-sync-issue.md feedback/reviewed/
|
||||
|
||||
# 5. Optional: Add response/notes
|
||||
echo "## Maintainer Notes\n\nCreated issue #123..." >> feedback/reviewed/20251217-103045-sync-issue.md
|
||||
```
|
||||
|
||||
### Tracking Statistics
|
||||
|
||||
```bash
|
||||
# Count feedback by status
|
||||
echo "Pending: $(ls feedback/inbound/ 2>/dev/null | wc -l)"
|
||||
echo "Reviewed: $(ls feedback/reviewed/ 2>/dev/null | wc -l)"
|
||||
echo "Archived: $(ls feedback/archived/ 2>/dev/null | wc -l)"
|
||||
|
||||
# Recent feedback
|
||||
ls -lt feedback/inbound/ | head -5
|
||||
```
|
||||
|
||||
### Creating Issues from Feedback
|
||||
|
||||
```bash
|
||||
# Read feedback
|
||||
cat feedback/inbound/20251217-feature-request.md
|
||||
|
||||
# Create issue with feedback content
|
||||
issue create "Feature: Bulk label operations" \
|
||||
--description "$(cat feedback/inbound/20251217-feature-request.md)" \
|
||||
--label=feedback --label=feature
|
||||
|
||||
# Move to reviewed
|
||||
mv feedback/inbound/20251217-feature-request.md feedback/reviewed/
|
||||
```
|
||||
|
||||
## Integration Patterns
|
||||
|
||||
### Pattern 1: Standalone `feedback/` Directory
|
||||
|
||||
Each capability maintains its own feedback directory. Users navigate to the capability and submit feedback.
|
||||
|
||||
```bash
|
||||
cd capabilities/issue-core
|
||||
echo "Feedback..." > feedback/inbound/$(date +%Y%m%d)-feedback.md
|
||||
```
|
||||
|
||||
**Pros**: Simple, no dependencies, capability-owned
|
||||
**Cons**: Manual file creation
|
||||
|
||||
### Pattern 2: Shared Feedback CLI
|
||||
|
||||
Create a reusable `feedback` command that works in any capability directory:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# feedback - Universal feedback submission tool
|
||||
|
||||
FEEDBACK_DIR="feedback/inbound"
|
||||
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
||||
HASH=$(echo "$1" | md5sum | cut -c1-8)
|
||||
FILENAME="${FEEDBACK_DIR}/${TIMESTAMP}-${HASH}.md"
|
||||
|
||||
# Create feedback directory if needed
|
||||
mkdir -p "$FEEDBACK_DIR"
|
||||
|
||||
# Submit feedback
|
||||
if [ -f "$1" ]; then
|
||||
cp "$1" "$FILENAME"
|
||||
else
|
||||
echo "$1" > "$FILENAME"
|
||||
fi
|
||||
|
||||
echo "Feedback submitted: $FILENAME"
|
||||
```
|
||||
|
||||
Install once, use everywhere:
|
||||
```bash
|
||||
cp tools/feedback /usr/local/bin/feedback
|
||||
cd any-capability
|
||||
feedback "My feedback here"
|
||||
```
|
||||
|
||||
**Pros**: Consistent UX, easy submission
|
||||
**Cons**: Requires installation
|
||||
|
||||
### Pattern 3: Makefile Integration
|
||||
|
||||
Add feedback commands to capability Makefile:
|
||||
|
||||
```makefile
|
||||
.PHONY: feedback
|
||||
feedback: ## Submit feedback (Usage: make feedback MSG="your feedback")
|
||||
@mkdir -p feedback/inbound
|
||||
@echo "$(MSG)" > feedback/inbound/$$(date +%Y%m%d-%H%M%S)-feedback.md
|
||||
@echo "Feedback submitted to feedback/inbound/"
|
||||
|
||||
.PHONY: feedback-list
|
||||
feedback-list: ## List pending feedback
|
||||
@echo "Pending feedback:"
|
||||
@ls -1t feedback/inbound/ 2>/dev/null || echo " (none)"
|
||||
```
|
||||
|
||||
Usage:
|
||||
```bash
|
||||
make feedback MSG="Sync is slow with 5k issues"
|
||||
make feedback-list
|
||||
```
|
||||
|
||||
**Pros**: No extra tools, uses existing Makefile
|
||||
**Cons**: Less flexible than CLI
|
||||
|
||||
### Pattern 4: API Endpoint (Future)
|
||||
|
||||
When capabilities evolve to services:
|
||||
|
||||
```http
|
||||
POST /api/feedback
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"content": "Feedback text...",
|
||||
"category": "bug",
|
||||
"contact": "user@email.com"
|
||||
}
|
||||
```
|
||||
|
||||
Backend writes to `feedback/inbound/` maintaining consistency with other patterns.
|
||||
|
||||
## Metadata (Optional)
|
||||
|
||||
While feedback content is unstructured, you *can* add metadata using front matter:
|
||||
|
||||
```markdown
|
||||
---
|
||||
timestamp: 2025-12-17T10:30:00Z
|
||||
category: bug
|
||||
priority: high
|
||||
contact: user@example.com
|
||||
context:
|
||||
git_repo: /path/to/master-project
|
||||
git_branch: feature/new-sync
|
||||
capability_version: 1.0.0
|
||||
---
|
||||
|
||||
Actual feedback content here...
|
||||
```
|
||||
|
||||
**Metadata is entirely optional.** Plain text/markdown works fine.
|
||||
|
||||
## Privacy
|
||||
|
||||
- Feedback is **anonymous by default** unless you include contact info
|
||||
- Git paths/context are for debugging, not shared externally
|
||||
- Capability maintainers see submitted feedback
|
||||
- No external tracking or telemetry
|
||||
|
||||
## Examples
|
||||
|
||||
**Quick Bug Report:**
|
||||
```bash
|
||||
echo "Bug: issue list crashes with --milestone='Sprint 2'" > \
|
||||
feedback/inbound/$(date +%Y%m%d)-crash-bug.md
|
||||
```
|
||||
|
||||
**Detailed Feature Request:**
|
||||
```markdown
|
||||
<!-- feedback/inbound/20251217-github-support.md -->
|
||||
|
||||
## Feature Request: GitHub Backend
|
||||
|
||||
**Problem**: Currently using Gitea, but our organization uses GitHub.
|
||||
|
||||
**Proposed Solution**: Add GitHub backend similar to existing Gitea backend.
|
||||
|
||||
**Use Case**:
|
||||
- 50+ repositories on GitHub
|
||||
- Need consistent CLI across all repos
|
||||
- Want offline sync capability
|
||||
|
||||
**Willingness to Help**: Can test beta versions, provide GitHub API expertise.
|
||||
|
||||
**Contact**: devteam@company.com
|
||||
```
|
||||
|
||||
**Performance Feedback:**
|
||||
```markdown
|
||||
<!-- feedback/inbound/20251217-sync-perf.md -->
|
||||
|
||||
Sync performance issue:
|
||||
- 5000 issues in Gitea repo
|
||||
- `issue sync pull` takes 12 minutes
|
||||
- CPU usage is low, seems like sequential API calls
|
||||
|
||||
Suggestion: Batch API requests or parallelize?
|
||||
|
||||
Thanks for the great tool!
|
||||
```
|
||||
|
||||
## For Capability Developers
|
||||
|
||||
### Setup (5 minutes)
|
||||
|
||||
```bash
|
||||
# 1. Create feedback directory
|
||||
mkdir -p feedback/inbound feedback/reviewed feedback/archived
|
||||
|
||||
# 2. Copy this README
|
||||
cp /path/to/feedback/template/README.md feedback/
|
||||
|
||||
# 3. Add to .gitignore (optional - feedback can be committed or ignored)
|
||||
echo "feedback/inbound/*.md" >> .gitignore
|
||||
|
||||
# 4. Document in CAPABILITY-issue-tracking.yaml
|
||||
# Add to capabilities section:
|
||||
# - feedback-collection
|
||||
```
|
||||
|
||||
### Maintenance Rhythm
|
||||
|
||||
Review feedback regularly:
|
||||
- **Daily**: Quick scan of inbound
|
||||
- **Weekly**: Deep review, create issues, respond
|
||||
- **Monthly**: Archive old feedback, analyze trends
|
||||
|
||||
### Continuous Improvement Loop
|
||||
|
||||
```
|
||||
User Experience → Feedback Submission → Maintainer Review →
|
||||
→ Action (Fix/Issue/Document) → Improved Capability → Better User Experience
|
||||
```
|
||||
|
||||
The feedback loop directly informs:
|
||||
- Roadmap prioritization
|
||||
- Bug triage
|
||||
- Documentation improvements
|
||||
- UX enhancements
|
||||
|
||||
## Rationale: Why This Pattern?
|
||||
|
||||
**Decentralized**: Each capability owns its feedback. No central feedback system to maintain.
|
||||
|
||||
**Flexible**: Text files are universal. No database, no service dependencies.
|
||||
|
||||
**Durable**: Plain markdown files survive system changes, migrations, refactors.
|
||||
|
||||
**Auditable**: Git tracks all feedback. Easy to see what was submitted when.
|
||||
|
||||
**Actionable**: Close to the code. Maintainers see feedback where they work.
|
||||
|
||||
**Scalable**: Works for 1 user or 1000 users. No infrastructure needed.
|
||||
|
||||
**Future-proof**: Can evolve to CLI tools, APIs, web UIs while maintaining the same underlying structure.
|
||||
|
||||
## This Capability's Feedback
|
||||
|
||||
Yes, **this README itself** is part of the feedback capability pattern! If you have feedback about the feedback system:
|
||||
|
||||
```bash
|
||||
cd capabilities/feedback # or wherever feedback capability lives
|
||||
echo "The feedback pattern is great but..." > feedback/inbound/$(date +%Y%m%d)-meta-feedback.md
|
||||
```
|
||||
|
||||
We eat our own dog food. 🐕
|
||||
|
||||
---
|
||||
|
||||
**Thank you for using the feedback pattern. Your input shapes better capabilities.**
|
||||
82
integration/gitea-backend.integration.yaml
Normal file
82
integration/gitea-backend.integration.yaml
Normal file
@@ -0,0 +1,82 @@
|
||||
schema_version: open-reuse.integration.v0.1
|
||||
id: issue-core-gitea
|
||||
name: issue-core Gitea Backend
|
||||
description: >
|
||||
Pluggable remote backend that maps the issue-core unified task model onto the
|
||||
Gitea issues API for cross-repo task landing and synchronization.
|
||||
status: registered
|
||||
owner: issue-core
|
||||
|
||||
local:
|
||||
repo: issue-core
|
||||
path: integration/gitea-backend.integration.yaml
|
||||
system: issue-core
|
||||
|
||||
upstream:
|
||||
name: Gitea
|
||||
project_url: https://github.com/go-gitea/gitea
|
||||
homepage: https://about.gitea.com/
|
||||
version_policy: gitea-api-v1
|
||||
monitor:
|
||||
releases: true
|
||||
tags: true
|
||||
security_advisories: true
|
||||
license_changes: true
|
||||
|
||||
reuse:
|
||||
primary_reuse_mode: adapter
|
||||
secondary_reuse_modes:
|
||||
- plugin
|
||||
risk_level: medium
|
||||
rationale: >
|
||||
Gitea REST API is wrapped behind the RemoteBackend interface; local task
|
||||
lifecycle semantics remain stable across backend swaps.
|
||||
|
||||
boundary:
|
||||
type: adapter
|
||||
local_adapter: issue_core.backends.gitea.backend.GiteaBackend
|
||||
local_interface: issue_core.core.interfaces.RemoteBackend
|
||||
reused_surface: Gitea /api/v1 issues, labels, milestones, comments
|
||||
contracts:
|
||||
- issue-core.backend.v1
|
||||
fragility_points:
|
||||
- Gitea API field changes
|
||||
- issue state mapping differences
|
||||
- pagination and rate-limit behavior
|
||||
- authentication token scopes
|
||||
|
||||
validation:
|
||||
harness: python3 -m pytest tests/test_gitea_backend.py
|
||||
skip_without_runtime: true
|
||||
checks:
|
||||
- API client request shaping
|
||||
- issue state mapping
|
||||
- error handling for rate limits
|
||||
policy: required-before-update
|
||||
|
||||
update_policy:
|
||||
default_action: require-maintainer-review
|
||||
auto_eligible: false
|
||||
|
||||
risks:
|
||||
sensitivity:
|
||||
- Gitea API breaking changes
|
||||
- authentication model changes
|
||||
- rate-limit policy changes
|
||||
- license changes
|
||||
escalation_triggers:
|
||||
- validation failure
|
||||
- Gitea major release
|
||||
- production sync errors
|
||||
|
||||
maintenance:
|
||||
maintainers:
|
||||
- issue-core
|
||||
escalation_conditions:
|
||||
- Gitea API compatibility failure
|
||||
- validation failure
|
||||
- production backend sync regression
|
||||
|
||||
audit:
|
||||
registered_at: "2026-06-24"
|
||||
registered_by: open-reuse
|
||||
24
issue_core/__init__.py
Normal file
24
issue_core/__init__.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""
|
||||
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.1"
|
||||
__author__ = "Coulomb / MarkiTect Project"
|
||||
__description__ = "Authoritative task lifecycle manager with plugin architecture"
|
||||
14
issue_core/api/__init__.py
Normal file
14
issue_core/api/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""
|
||||
issue-core REST API.
|
||||
|
||||
Ingestion surface for external emitters — primarily activity-core's
|
||||
IssueSink, which calls POST /issues/ to file tasks against the org's
|
||||
configured backends.
|
||||
|
||||
Run via the CLI: `issue serve --host 0.0.0.0 --port 8765`
|
||||
Requires the [api] extra: `pip install issue-core[api]`.
|
||||
"""
|
||||
|
||||
from .app import create_app
|
||||
|
||||
__all__ = ["create_app"]
|
||||
26
issue_core/api/app.py
Normal file
26
issue_core/api/app.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
FastAPI application factory for issue-core.
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
from .. import __version__
|
||||
from .ingest import router as ingest_router
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
app = FastAPI(
|
||||
title="issue-core",
|
||||
description=(
|
||||
"Authoritative task lifecycle manager for the Coulomb org. "
|
||||
"POST /issues/ is the ingestion surface for activity-core's IssueSink."
|
||||
),
|
||||
version=__version__,
|
||||
)
|
||||
app.include_router(ingest_router)
|
||||
|
||||
@app.get("/healthz", tags=["meta"])
|
||||
async def healthz() -> dict:
|
||||
return {"status": "ok", "version": __version__}
|
||||
|
||||
return app
|
||||
66
issue_core/api/auth.py
Normal file
66
issue_core/api/auth.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""
|
||||
API key authentication for the issue-core REST API.
|
||||
|
||||
A single shared key is read from the ISSUE_CORE_API_KEY environment variable.
|
||||
Clients send it either as `Authorization: Bearer <key>` or as `X-API-Key: <key>`.
|
||||
|
||||
If ISSUE_CORE_API_KEY is unset, the server refuses to start — the workplan
|
||||
explicitly forbids an unauthenticated ingestion surface.
|
||||
"""
|
||||
|
||||
import os
|
||||
import secrets
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import Header, HTTPException, status
|
||||
|
||||
|
||||
API_KEY_ENV_VAR = "ISSUE_CORE_API_KEY"
|
||||
|
||||
|
||||
class AuthConfigError(RuntimeError):
|
||||
"""Raised at startup when no API key is configured."""
|
||||
|
||||
|
||||
def get_configured_api_key() -> str:
|
||||
key = os.environ.get(API_KEY_ENV_VAR, "").strip()
|
||||
if not key:
|
||||
raise AuthConfigError(
|
||||
f"{API_KEY_ENV_VAR} is not set. The ingestion endpoint requires an API key. "
|
||||
f"Generate one with: python -c 'import secrets; print(secrets.token_urlsafe(32))'"
|
||||
)
|
||||
return key
|
||||
|
||||
|
||||
def _extract_token(
|
||||
authorization: Optional[str],
|
||||
x_api_key: Optional[str],
|
||||
) -> Optional[str]:
|
||||
if x_api_key:
|
||||
return x_api_key.strip()
|
||||
if authorization:
|
||||
scheme, _, value = authorization.partition(" ")
|
||||
if scheme.lower() == "bearer" and value:
|
||||
return value.strip()
|
||||
return None
|
||||
|
||||
|
||||
async def require_api_key(
|
||||
authorization: Optional[str] = Header(default=None),
|
||||
x_api_key: Optional[str] = Header(default=None, alias="X-API-Key"),
|
||||
) -> None:
|
||||
"""FastAPI dependency enforcing the shared API key."""
|
||||
try:
|
||||
expected = get_configured_api_key()
|
||||
except AuthConfigError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=str(exc),
|
||||
)
|
||||
presented = _extract_token(authorization, x_api_key)
|
||||
if not presented or not secrets.compare_digest(presented, expected):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Missing or invalid API key.",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
139
issue_core/api/ingest.py
Normal file
139
issue_core/api/ingest.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""
|
||||
POST /issues/ — task ingestion endpoint.
|
||||
|
||||
Receives a TaskSpec payload (see schemas.TaskIngestionRequest) from an
|
||||
authorized emitter, routes it to the configured backend, and returns the
|
||||
created issue's id and (optional) URL.
|
||||
|
||||
Routing strategy (v1):
|
||||
- Single default backend, looked up via cli.utils.get_default_backend().
|
||||
- target_repo, triggering_event_id, source_*, activity_definition_id are
|
||||
stored on the issue's sync_metadata for traceability back to the emitter.
|
||||
- Per-target-repo routing is a planned follow-up; see SCOPE.md.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
|
||||
from ..backends.gitea import GiteaBackend
|
||||
from ..backends.local import LocalSQLiteBackend
|
||||
from ..cli.utils import get_config_dir, load_backend_configs
|
||||
from ..core.interfaces import BackendFactory, IssueBackend
|
||||
from ..core.models import Issue, IssueState, Label
|
||||
from .auth import require_api_key
|
||||
from .schemas import BackendName, TaskIngestionRequest, TaskIngestionResponse
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
BackendFactory.register_backend("local", LocalSQLiteBackend)
|
||||
BackendFactory.register_backend("gitea", GiteaBackend)
|
||||
|
||||
|
||||
_BACKEND_TYPE_TO_NAME: Dict[str, str] = {
|
||||
"local": "sqlite",
|
||||
"gitea": "gitea",
|
||||
"github": "github",
|
||||
}
|
||||
|
||||
|
||||
def _resolve_backend() -> Tuple[IssueBackend, str]:
|
||||
configs = load_backend_configs()
|
||||
default_name = configs.get("default", "local")
|
||||
if default_name not in configs:
|
||||
if default_name == "local":
|
||||
configs["local"] = {
|
||||
"type": "local",
|
||||
"db_path": str(get_config_dir() / "issues.db"),
|
||||
}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=f"Default backend '{default_name}' is not configured.",
|
||||
)
|
||||
backend_config = configs[default_name]
|
||||
backend_type = backend_config["type"]
|
||||
try:
|
||||
backend = BackendFactory.create_backend(backend_type)
|
||||
backend.connect(backend_config)
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=f"Failed to connect to backend '{default_name}': {exc}",
|
||||
)
|
||||
return backend, backend_type
|
||||
|
||||
|
||||
def _build_issue(payload: TaskIngestionRequest, backend_type: str) -> Issue:
|
||||
now = datetime.now(timezone.utc)
|
||||
labels = [Label(name=name) for name in payload.labels]
|
||||
labels.append(Label(name=f"priority:{payload.priority}"))
|
||||
labels.append(Label(name=f"source:{payload.source_type}"))
|
||||
if payload.target_repo:
|
||||
labels.append(Label(name=f"repo:{payload.target_repo}"))
|
||||
|
||||
ingestion_meta: Dict[str, Any] = {
|
||||
"target_repo": payload.target_repo,
|
||||
"source_type": payload.source_type,
|
||||
"source_id": payload.source_id,
|
||||
"triggering_event_id": str(payload.triggering_event_id),
|
||||
"activity_definition_id": payload.activity_definition_id,
|
||||
"ingested_at": now.isoformat(),
|
||||
}
|
||||
if payload.due_in_days is not None:
|
||||
ingestion_meta["due_at"] = (now + timedelta(days=payload.due_in_days)).isoformat()
|
||||
|
||||
sync_metadata: Dict[str, Any] = {"ingestion": ingestion_meta}
|
||||
|
||||
return Issue(
|
||||
id="",
|
||||
number=0,
|
||||
title=payload.title,
|
||||
description=payload.description,
|
||||
state=IssueState.OPEN,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
labels=labels,
|
||||
backend_type=backend_type,
|
||||
sync_metadata=sync_metadata,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/issues/",
|
||||
response_model=TaskIngestionResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(require_api_key)],
|
||||
summary="Ingest a task from an external emitter (e.g. activity-core).",
|
||||
)
|
||||
async def ingest_task(payload: TaskIngestionRequest) -> TaskIngestionResponse:
|
||||
backend, backend_type = _resolve_backend()
|
||||
draft = _build_issue(payload, backend_type)
|
||||
try:
|
||||
created: Issue = backend.create_issue(draft)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=f"Backend rejected the issue: {exc}",
|
||||
)
|
||||
finally:
|
||||
try:
|
||||
backend.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
issue_id = created.id or str(created.number)
|
||||
issue_url: Optional[str] = None
|
||||
if created.sync_metadata:
|
||||
issue_url = created.sync_metadata.get("url") or created.sync_metadata.get("html_url")
|
||||
backend_name: BackendName = _BACKEND_TYPE_TO_NAME.get(backend_type, backend_type) # type: ignore[assignment]
|
||||
return TaskIngestionResponse(
|
||||
issue_id=issue_id,
|
||||
issue_url=issue_url,
|
||||
backend=backend_name,
|
||||
)
|
||||
55
issue_core/api/schemas.py
Normal file
55
issue_core/api/schemas.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
Pydantic schemas for the issue-core REST API.
|
||||
|
||||
The TaskIngestionRequest schema matches activity-core's IssueSink TaskSpec
|
||||
payload. See:
|
||||
- SCOPE.md "TaskSpec payload" section
|
||||
- activity-core docs/adr/adr-001-event-bridge-architecture.md
|
||||
"""
|
||||
|
||||
from typing import List, Literal, Optional
|
||||
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: str = Field(
|
||||
...,
|
||||
min_length=1,
|
||||
description=(
|
||||
"Activity event UUID, or a stable scheduler/source key when no "
|
||||
"event row exists."
|
||||
),
|
||||
)
|
||||
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
|
||||
@@ -195,10 +195,20 @@ class GiteaBackend(RemoteBackend, SyncableBackend):
|
||||
if issue.milestone:
|
||||
data['milestone'] = int(issue.milestone.backend_id) if issue.milestone.backend_id else None
|
||||
|
||||
# Convert labels
|
||||
if issue.labels:
|
||||
data['labels'] = [label.name for label in issue.labels]
|
||||
|
||||
# Gitea expects numeric label IDs on issue create/update. Name-only
|
||||
# labels are preserved in issue-core metadata but omitted from the API
|
||||
# payload until a label-resolution step exists.
|
||||
label_ids = []
|
||||
for label in issue.labels:
|
||||
if not label.backend_id:
|
||||
continue
|
||||
try:
|
||||
label_ids.append(int(label.backend_id))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if label_ids:
|
||||
data['labels'] = label_ids
|
||||
|
||||
return data
|
||||
|
||||
# Issue CRUD Operations
|
||||
18
issue_core/cli/legacy.py
Normal file
18
issue_core/cli/legacy.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Legacy entry-point shims for renamed console scripts.
|
||||
|
||||
Kept so that callers using the pre-rename names get a clear, loud migration
|
||||
hint instead of `command not found`.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
def issue_tracker_hint() -> None:
|
||||
"""`issue-tracker` -> `issue-core` migration hint."""
|
||||
sys.stderr.write(
|
||||
"issue-tracker has been renamed to issue-core.\n"
|
||||
" - Run `issue-core <args>` (or the short alias `issue <args>`) instead.\n"
|
||||
" - The Python package is now `issue_core` (was `issue_tracker`).\n"
|
||||
)
|
||||
sys.exit(2)
|
||||
@@ -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
|
||||
43
issue_core/cli/serve_command.py
Normal file
43
issue_core/cli/serve_command.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""
|
||||
`issue serve` — launch the issue-core REST API.
|
||||
|
||||
Requires the [api] extra: `pip install issue-core[api]`.
|
||||
"""
|
||||
|
||||
import click
|
||||
|
||||
|
||||
@click.command("serve")
|
||||
@click.option("--host", default="127.0.0.1", show_default=True, help="Bind address.")
|
||||
@click.option("--port", default=8765, show_default=True, type=int, help="Bind port.")
|
||||
@click.option("--reload", is_flag=True, default=False, help="Auto-reload on code change (dev only).")
|
||||
@click.option("--log-level", default="info", show_default=True, help="Uvicorn log level.")
|
||||
def serve_command(host: str, port: int, reload: bool, log_level: str) -> None:
|
||||
"""Launch the issue-core REST API (POST /issues/ ingestion endpoint).
|
||||
|
||||
Requires ISSUE_CORE_API_KEY to be set in the environment.
|
||||
"""
|
||||
try:
|
||||
import uvicorn # noqa: F401
|
||||
except ImportError:
|
||||
raise click.ClickException(
|
||||
"The 'api' extra is not installed. Run: pip install 'issue-core[api]'"
|
||||
)
|
||||
|
||||
from ..api.auth import AuthConfigError, get_configured_api_key
|
||||
|
||||
try:
|
||||
get_configured_api_key()
|
||||
except AuthConfigError as exc:
|
||||
raise click.ClickException(str(exc))
|
||||
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(
|
||||
"issue_core.api.app:create_app",
|
||||
host=host,
|
||||
port=port,
|
||||
reload=reload,
|
||||
log_level=log_level,
|
||||
factory=True,
|
||||
)
|
||||
@@ -1,24 +0,0 @@
|
||||
"""
|
||||
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"
|
||||
24
k8s/railiance/configmap-backends.yaml
Normal file
24
k8s/railiance/configmap-backends.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
# Non-secret backend structure for issue-core inside railiance01.
|
||||
# Default backend = cluster Gitea (markitect). The Gitea token is NOT here;
|
||||
# it is injected at startup from GITEA_BACKEND_TOKEN (ExternalSecret) where the
|
||||
# template carries the sentinel "__FROM_ENV__".
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: issue-core-backends
|
||||
namespace: issue-core
|
||||
labels:
|
||||
app.kubernetes.io/name: issue-core
|
||||
app.kubernetes.io/part-of: railiance-gitops
|
||||
data:
|
||||
backends.json: |
|
||||
{
|
||||
"markitect": {
|
||||
"type": "gitea",
|
||||
"base_url": "http://gitea-http.default.svc.cluster.local:3000",
|
||||
"owner": "coulomb",
|
||||
"repo": "markitect-main",
|
||||
"token": "__FROM_ENV__"
|
||||
},
|
||||
"default": "markitect"
|
||||
}
|
||||
72
k8s/railiance/deployment.yaml
Normal file
72
k8s/railiance/deployment.yaml
Normal file
@@ -0,0 +1,72 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: issue-core
|
||||
namespace: issue-core
|
||||
labels:
|
||||
app.kubernetes.io/name: issue-core
|
||||
app.kubernetes.io/part-of: railiance-gitops
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "1" # after the ExternalSecret (wave 0)
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: issue-core
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: issue-core
|
||||
app.kubernetes.io/part-of: railiance-gitops
|
||||
spec:
|
||||
# Image is public-pullable from the Gitea registry (per railiance-forge
|
||||
# docs). Add imagePullSecrets: [{name: gitea-registry}] if it becomes private.
|
||||
containers:
|
||||
- name: issue-core
|
||||
image: gitea.coulomb.social/coulomb/issue-core:0.2.1
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8765
|
||||
env:
|
||||
- name: ISSUE_CORE_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: issue-core-runtime
|
||||
key: ISSUE_CORE_API_KEY
|
||||
- name: GITEA_BACKEND_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: issue-core-runtime
|
||||
key: GITEA_BACKEND_TOKEN
|
||||
- name: BACKENDS_TEMPLATE
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: issue-core-backends
|
||||
key: backends.json
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 20
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 256Mi
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: false
|
||||
runAsNonRoot: true
|
||||
runAsUser: 10001
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
37
k8s/railiance/externalsecret.yaml
Normal file
37
k8s/railiance/externalsecret.yaml
Normal file
@@ -0,0 +1,37 @@
|
||||
# Runtime secrets for issue-core, materialized from OpenBao by External Secrets
|
||||
# Operator (cluster default per railiance-platform docs/argocd-gitops.md).
|
||||
#
|
||||
# DEPENDENCY: External Secrets Operator is not yet installed on railiance01 and
|
||||
# the OpenBao path below must be provisioned by railiance-platform. Until then
|
||||
# this resource will not reconcile and the Deployment stays Pending the Secret.
|
||||
#
|
||||
# OpenBao path: platform/workloads/issue-core/issue-core/issue-core-runtime
|
||||
# properties: ISSUE_CORE_API_KEY, GITEA_BACKEND_TOKEN
|
||||
apiVersion: external-secrets.io/v1beta1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: issue-core-runtime
|
||||
namespace: issue-core
|
||||
labels:
|
||||
app.kubernetes.io/name: issue-core
|
||||
app.kubernetes.io/part-of: railiance-gitops
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "0" # before the Deployment (wave 1)
|
||||
spec:
|
||||
refreshInterval: 1h
|
||||
secretStoreRef:
|
||||
# Provisioned by railiance-platform during ESO install; name TBC on bootstrap.
|
||||
name: openbao
|
||||
kind: ClusterSecretStore
|
||||
target:
|
||||
name: issue-core-runtime
|
||||
creationPolicy: Owner
|
||||
data:
|
||||
- secretKey: ISSUE_CORE_API_KEY
|
||||
remoteRef:
|
||||
key: platform/workloads/issue-core/issue-core/issue-core-runtime
|
||||
property: ISSUE_CORE_API_KEY
|
||||
- secretKey: GITEA_BACKEND_TOKEN
|
||||
remoteRef:
|
||||
key: platform/workloads/issue-core/issue-core/issue-core-runtime
|
||||
property: GITEA_BACKEND_TOKEN
|
||||
12
k8s/railiance/kustomization.yaml
Normal file
12
k8s/railiance/kustomization.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
# issue-core workload manifests, synced by the ArgoCD `issue-core` Application
|
||||
# (path k8s/railiance, destination namespace issue-core, CreateNamespace=true).
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
namespace: issue-core
|
||||
|
||||
resources:
|
||||
- externalsecret.yaml
|
||||
- configmap-backends.yaml
|
||||
- deployment.yaml
|
||||
- service.yaml
|
||||
19
k8s/railiance/service.yaml
Normal file
19
k8s/railiance/service.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
# ClusterIP exposing issue-core on 8765 as
|
||||
# issue-core.issue-core.svc.cluster.local:8765 — the address activity-core's
|
||||
# ISSUE_CORE_URL points at once its k8s runtime port is corrected (8010 -> 8765).
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: issue-core
|
||||
namespace: issue-core
|
||||
labels:
|
||||
app.kubernetes.io/name: issue-core
|
||||
app.kubernetes.io/part-of: railiance-gitops
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app.kubernetes.io/name: issue-core
|
||||
ports:
|
||||
- name: http
|
||||
port: 8765
|
||||
targetPort: http
|
||||
@@ -1,10 +1,10 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=45", "wheel", "setuptools-scm[toml]>=6.2"]
|
||||
requires = ["setuptools>=61,<77", "wheel"]
|
||||
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"}
|
||||
@@ -37,15 +37,20 @@ dynamic = ["version"]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"build>=1.3",
|
||||
"pytest>=6.0",
|
||||
"pytest-cov>=2.0",
|
||||
"pytest-mock>=3.0",
|
||||
"twine>=6.0",
|
||||
"black>=22.0",
|
||||
"isort>=5.0",
|
||||
"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",
|
||||
"sphinx-rtd-theme>=1.0",
|
||||
@@ -60,25 +65,31 @@ 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"
|
||||
issue-tracker = "issue_core.cli.legacy:issue_tracker_hint"
|
||||
|
||||
[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 +112,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 +153,7 @@ markers = [
|
||||
]
|
||||
|
||||
[tool.coverage.run]
|
||||
source = ["issue_tracker"]
|
||||
source = ["issue_core"]
|
||||
omit = [
|
||||
"*/tests/*",
|
||||
"*/test_*",
|
||||
@@ -161,4 +172,4 @@ exclude_lines = [
|
||||
"if __name__ == .__main__.:",
|
||||
"class .*\\bProtocol\\):",
|
||||
"@(abc\\.)?abstractmethod",
|
||||
]
|
||||
]
|
||||
|
||||
12
registry/README.md
Normal file
12
registry/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Capability Registry
|
||||
|
||||
Markdown-first capability index for federation and reuse planning.
|
||||
|
||||
## Authoring
|
||||
|
||||
1. Copy a capability entry template (see reuse-surface `templates/capability-entry.template.md`).
|
||||
2. Add the row to `indexes/capabilities.yaml`.
|
||||
3. Run `reuse-surface validate` from a checkout with the CLI installed.
|
||||
4. Merge to `main` and verify publish with `reuse-surface establish --publish-check`.
|
||||
|
||||
Federation contract: reuse-surface `docs/RegistryFederation.md`.
|
||||
0
registry/capabilities/.gitkeep
Normal file
0
registry/capabilities/.gitkeep
Normal file
4
registry/indexes/capabilities.yaml
Normal file
4
registry/indexes/capabilities.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
version: 1
|
||||
updated: '2026-06-16'
|
||||
domain: helix_forge
|
||||
capabilities: []
|
||||
@@ -1 +1 @@
|
||||
"""Test suite for issue-facade capability."""
|
||||
"""Test suite for issue-core capability."""
|
||||
|
||||
201
tests/test_api_ingest.py
Normal file
201
tests/test_api_ingest.py
Normal file
@@ -0,0 +1,201 @@
|
||||
"""
|
||||
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_accepts_non_uuid_triggering_event_id(client, valid_payload, tmp_issue_store):
|
||||
valid_payload["triggering_event_id"] = "scheduled"
|
||||
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["triggering_event_id"] == "scheduled"
|
||||
finally:
|
||||
backend.disconnect()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_ingest_rejects_invalid_payload(client):
|
||||
bad = {"title": "no required fields"}
|
||||
response = client.post(
|
||||
"/issues/", json=bad, headers={"Authorization": f"Bearer {API_KEY}"}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_ingest_rejects_invalid_priority(client, valid_payload):
|
||||
valid_payload["priority"] = "urgent"
|
||||
response = client.post(
|
||||
"/issues/",
|
||||
json=valid_payload,
|
||||
headers={"Authorization": f"Bearer {API_KEY}"},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_ingest_persists_traceability_metadata(client, valid_payload, tmp_issue_store):
|
||||
"""Check that triggering_event_id, source_*, target_repo are stored on the issue."""
|
||||
response = client.post(
|
||||
"/issues/",
|
||||
json=valid_payload,
|
||||
headers={"Authorization": f"Bearer {API_KEY}"},
|
||||
)
|
||||
assert response.status_code == 201, response.text
|
||||
issue_id = response.json()["issue_id"]
|
||||
|
||||
from issue_core.backends.local import LocalSQLiteBackend
|
||||
|
||||
backend = LocalSQLiteBackend()
|
||||
backend.connect({"type": "local", "db_path": str(tmp_issue_store / "issues.db")})
|
||||
try:
|
||||
stored = backend.get_issue(issue_id)
|
||||
assert stored is not None
|
||||
ingestion = stored.sync_metadata.get("ingestion") or {}
|
||||
assert ingestion["target_repo"] == valid_payload["target_repo"]
|
||||
assert ingestion["source_type"] == valid_payload["source_type"]
|
||||
assert ingestion["source_id"] == valid_payload["source_id"]
|
||||
assert ingestion["triggering_event_id"] == valid_payload["triggering_event_id"]
|
||||
assert ingestion["activity_definition_id"] == valid_payload["activity_definition_id"]
|
||||
label_names = {label.name for label in stored.labels}
|
||||
assert f"priority:{valid_payload['priority']}" in label_names
|
||||
assert f"source:{valid_payload['source_type']}" in label_names
|
||||
assert f"repo:{valid_payload['target_repo']}" in label_names
|
||||
assert "bug" in label_names
|
||||
finally:
|
||||
backend.disconnect()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_app_refuses_without_api_key_env(monkeypatch, tmp_issue_store, valid_payload):
|
||||
monkeypatch.delenv("ISSUE_CORE_API_KEY", raising=False)
|
||||
app = create_app()
|
||||
client = TestClient(app)
|
||||
response = client.post(
|
||||
"/issues/", json=valid_payload, headers={"Authorization": "Bearer anything"}
|
||||
)
|
||||
assert response.status_code == 503
|
||||
assert "ISSUE_CORE_API_KEY" in response.json()["detail"]
|
||||
@@ -12,8 +12,8 @@ from pathlib import Path
|
||||
from click.testing import CliRunner
|
||||
from 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')
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -6,8 +6,10 @@ These tests ensure the Gitea backend works correctly with the API.
|
||||
|
||||
import pytest
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
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
|
||||
from issue_core.core.models import Issue, IssueState, Label
|
||||
|
||||
|
||||
class TestGiteaBackend:
|
||||
@@ -32,7 +34,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 +63,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 +98,30 @@ 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')
|
||||
def test_gitea_payload_omits_name_only_labels(self):
|
||||
"""Gitea issue payloads only include numeric label IDs."""
|
||||
now = datetime.now(timezone.utc)
|
||||
issue = Issue(
|
||||
id="",
|
||||
number=0,
|
||||
title="Test issue",
|
||||
description="Test description",
|
||||
state=IssueState.OPEN,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
labels=[
|
||||
Label(name="priority:low"),
|
||||
Label(name="source:rule", backend_id="not-a-number"),
|
||||
Label(name="existing", backend_id="42"),
|
||||
],
|
||||
)
|
||||
|
||||
payload = self.backend._unified_issue_to_gitea(issue)
|
||||
|
||||
assert payload["labels"] == [42]
|
||||
assert payload["title"] == "Test issue"
|
||||
|
||||
@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 +142,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 +164,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()
|
||||
|
||||
@@ -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
|
||||
|
||||
230
workplans/ISSC-WP-0001-rename-and-task-ingestion.md
Normal file
230
workplans/ISSC-WP-0001-rename-and-task-ingestion.md
Normal file
@@ -0,0 +1,230 @@
|
||||
---
|
||||
id: ISSC-WP-0001
|
||||
type: workplan
|
||||
domain: infotech
|
||||
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: done
|
||||
- id: T02
|
||||
title: Register issue-core in state hub under capabilities domain
|
||||
state_hub_task_id: b1d36996-44ff-48b9-b208-709d6874453c
|
||||
status: done
|
||||
- id: T03
|
||||
title: Write INTENT.md
|
||||
state_hub_task_id: 265c6338-0310-409d-a081-6446042f6274
|
||||
status: done
|
||||
- id: T04
|
||||
title: Update or write SCOPE.md
|
||||
state_hub_task_id: f95ac730-7ba0-4eae-bcae-de1e7d24b164
|
||||
status: done
|
||||
- id: T05
|
||||
title: Implement task ingestion REST endpoint POST /issues/
|
||||
state_hub_task_id: 26af07e4-c072-42ad-bb5c-facb196156c9
|
||||
status: done
|
||||
- id: T06
|
||||
title: Document NATS subscriber interface (design stub)
|
||||
state_hub_task_id: dff61fed-1e8c-4eb3-bbd6-1e3742329945
|
||||
status: done
|
||||
created: "2026-05-14"
|
||||
---
|
||||
|
||||
# ISSC-WP-0001: Rename to issue-core and Task Ingestion
|
||||
|
||||
## Purpose
|
||||
|
||||
Two things need to happen to make issue-facade a proper peer to activity-core
|
||||
in the Coulomb org architecture:
|
||||
|
||||
1. **Rename**: `issue-facade` is a misleading name — it is not a facade, it is
|
||||
the authoritative task lifecycle manager for the org. Renaming to `issue-core`
|
||||
aligns with the naming pattern (`activity-core`, `rules-core`, `project-core`)
|
||||
and signals its role clearly.
|
||||
|
||||
2. **Task ingestion endpoint**: activity-core's `IssueSink` adapter emits tasks to
|
||||
issue-core via REST. That endpoint must exist, be stable, and accept `TaskSpec`
|
||||
payloads from activity-core. Without it, activity-core's task emission is a
|
||||
no-op.
|
||||
|
||||
## Context
|
||||
|
||||
- **activity-core WP-0003** (in progress): implements `IssueSinkAdapter` in
|
||||
`src/activity_core/issue_sink.py`. It calls `POST /issues/` on issue-core to
|
||||
create tasks from ActivityDefinition rule/instruction output.
|
||||
- **issue-facade** currently: multi-backend task tracker (Gitea, SQLite, GitHub).
|
||||
Handles task creation and tracking. Has no incoming task-ingestion API from
|
||||
external callers like activity-core.
|
||||
- **State hub gap**: issue-facade is not registered in the Custodian State Hub.
|
||||
This makes cross-repo workstream tracking impossible.
|
||||
|
||||
See: `docs/adr/adr-001-event-bridge-architecture.md` in activity-core for the
|
||||
IssueSink adapter design and the task emission flow.
|
||||
|
||||
## Scope
|
||||
|
||||
**In scope:**
|
||||
- Package rename (issue-facade → issue-core)
|
||||
- State hub registration
|
||||
- INTENT.md and SCOPE.md
|
||||
- Task ingestion REST endpoint (POST /issues/)
|
||||
- NATS subscriber interface design stub
|
||||
|
||||
**Out of scope:**
|
||||
- Project management features (that is project-core, future)
|
||||
- UI or end-user facing changes
|
||||
- Changing backends (Gitea/SQLite/GitHub adapters stay as-is)
|
||||
|
||||
## TaskSpec Payload (from activity-core)
|
||||
|
||||
The POST /issues/ endpoint receives this 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"
|
||||
}
|
||||
```
|
||||
|
||||
The endpoint must return:
|
||||
```json
|
||||
{
|
||||
"issue_id": "string",
|
||||
"issue_url": "string or null",
|
||||
"backend": "gitea | sqlite | github"
|
||||
}
|
||||
```
|
||||
|
||||
The `issue_id` is stored in activity-core's `task_spawn_log` as the external
|
||||
reference. It is not managed by activity-core — it belongs to issue-core.
|
||||
|
||||
## Tasks
|
||||
|
||||
### T01 — Rename package issue-facade → issue-core throughout
|
||||
|
||||
Rename everywhere:
|
||||
- `pyproject.toml`: `name = "issue-core"`, update entry points
|
||||
- Python package directory: `issue_facade/` → `issue_core/` (if applicable)
|
||||
- All `import issue_facade` → `import issue_core`
|
||||
- CLI command names if changed
|
||||
- README, CHANGELOG headers
|
||||
- Docker Compose service names and image tags
|
||||
- Any Makefile targets
|
||||
|
||||
After renaming, run the full test suite to confirm no broken imports.
|
||||
Update the `workplans/` frontmatter `repo: issue-core` once renamed.
|
||||
|
||||
### T02 — Register issue-core in state hub under capabilities domain
|
||||
|
||||
Call `register_repo()` on the state hub MCP:
|
||||
```
|
||||
register_repo(slug="issue-core", path="/home/worsch/issue-facade",
|
||||
domain="custodian") # or capabilities if domain exists
|
||||
```
|
||||
|
||||
Note: the directory may still be named `issue-facade` — register with
|
||||
`slug="issue-core"` and update `path` once the rename is complete.
|
||||
|
||||
### T03 — Write INTENT.md
|
||||
|
||||
Write `INTENT.md` explaining:
|
||||
- **Why it exists**: the Coulomb org needs a single, observable place where
|
||||
tasks land — regardless of whether they were created by a human, by
|
||||
activity-core, or by an agent.
|
||||
- **What it is**: task lifecycle manager (create, assign, update, close) with
|
||||
pluggable backends (Gitea, SQLite, GitHub).
|
||||
- **What it is NOT**: not a project manager (no phases, no campaigns, no
|
||||
dependency graphs — that is project-core). Not a spawn audit trail (that is
|
||||
activity-core). Not an event bus.
|
||||
- **How it fits**: activity-core emits tasks here via IssueSink; humans and
|
||||
agents consume them; status updates flow back to issue-core (not to
|
||||
activity-core).
|
||||
|
||||
### T04 — Update or write SCOPE.md
|
||||
|
||||
Check if SCOPE.md exists. If yes, update:
|
||||
- Rename references (issue-facade → issue-core)
|
||||
- Add activity-core as an upstream emitter via IssueSink REST
|
||||
- Add "Out of scope" section if missing: project management, spawn audit trail
|
||||
- Update "How it fits" to reference activity-core architecture
|
||||
|
||||
If no SCOPE.md exists, write one from scratch following the standard format.
|
||||
|
||||
### T05 — Implement task ingestion REST endpoint POST /issues/
|
||||
|
||||
Implement `POST /issues/` (or `POST /tasks/create` — check existing naming
|
||||
convention and pick the most consistent path):
|
||||
|
||||
```python
|
||||
@router.post("/issues/")
|
||||
async def ingest_task(payload: TaskIngestionRequest) -> TaskIngestionResponse:
|
||||
...
|
||||
```
|
||||
|
||||
The endpoint must:
|
||||
1. Validate the payload against `TaskIngestionRequest` schema
|
||||
2. Route to the correct backend based on `target_repo` (look up backend config)
|
||||
3. Create the issue/task in the backend
|
||||
4. Return `{issue_id, issue_url, backend}`
|
||||
5. Log the ingestion with `triggering_event_id` for traceability
|
||||
|
||||
**Key file**: `issue_core/api/ingest.py` (new) or alongside existing API routes.
|
||||
|
||||
Security: the endpoint should require an API key header (check existing auth
|
||||
pattern in the codebase). Do not expose it unauthenticated.
|
||||
|
||||
### T06 — Document NATS subscriber interface (design stub)
|
||||
|
||||
**Design stub only — implementation deferred until activity-core's IssueSink
|
||||
migrates from REST to NATS.**
|
||||
|
||||
Document in `docs/nats-task-ingestion.md`:
|
||||
- Proposed NATS subject pattern: `act.tasks.create.{target_repo}`
|
||||
- Message schema (same `TaskIngestionRequest` as REST endpoint)
|
||||
- Consumer group config: durable consumer, at-least-once delivery
|
||||
- Idempotency key: `triggering_event_id` — used to deduplicate retries
|
||||
|
||||
## Build Order
|
||||
|
||||
```
|
||||
T01 (rename) → T02 (register) → T03 (INTENT.md) → T04 (SCOPE.md)
|
||||
T01 (rename) → T05 (REST endpoint) → T06 (NATS stub, depends on endpoint schema)
|
||||
```
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
1. Package renamed to issue-core; all tests pass after rename
|
||||
2. issue-core registered in state hub
|
||||
3. INTENT.md and SCOPE.md committed and accurate
|
||||
4. `POST /issues/` endpoint implemented and tested with a TaskSpec payload
|
||||
5. activity-core agent confirmed IssueSink integration works end-to-end
|
||||
6. NATS design stub committed to `docs/`
|
||||
|
||||
## Notes
|
||||
|
||||
- **Directory name**: if renaming the repository directory (`issue-facade/` →
|
||||
`issue-core/`) — coordinate with Bernd first, as it will invalidate any
|
||||
relative paths or symlinks.
|
||||
- **T01 first**: all other tasks depend on the rename being settled. The
|
||||
directory path in state hub registration (T02) must reflect the final name.
|
||||
- **Backwards compat**: if external consumers call the existing API using paths
|
||||
under `issue-facade`, add redirect aliases rather than breaking them.
|
||||
- The NATS stub (T06) should be implemented as a comment-heavy skeleton so
|
||||
the activity-core agent can wire the NATS IssueSink without waiting for a
|
||||
full implementation.
|
||||
|
||||
## Change History
|
||||
|
||||
- v0.1 (2026-05-14): Stub created by activity-core agent during WP-0003 planning.
|
||||
Local agent to flesh out and implement.
|
||||
91
workplans/ISSUE-WP-0002-gitea-pypi-publication.md
Normal file
91
workplans/ISSUE-WP-0002-gitea-pypi-publication.md
Normal file
@@ -0,0 +1,91 @@
|
||||
---
|
||||
id: ISSUE-WP-0002
|
||||
type: workplan
|
||||
title: "Publish issue-core to Gitea PyPI"
|
||||
domain: infotech
|
||||
repo: issue-core
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: custodian
|
||||
created: "2026-05-23"
|
||||
updated: "2026-06-05"
|
||||
state_hub_workstream_id: "87a5dcb6-5b2c-4d3f-8d5c-e265889b0fc6"
|
||||
---
|
||||
|
||||
# Publish issue-core to Gitea PyPI
|
||||
|
||||
`issue-core` needs a first real Python package release in the Coulomb Gitea
|
||||
package registry so downstream applications can depend on a versioned package
|
||||
instead of a sibling checkout path.
|
||||
|
||||
## Publish and verify the Gitea PyPI package
|
||||
|
||||
```task
|
||||
id: ISSUE-WP-0002-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "f0df7dbc-b55d-4835-a528-f44a329efb0e"
|
||||
```
|
||||
|
||||
**Problem.** `vergabe-teilnahme` and future Railiance S5 apps need
|
||||
`issue-core` as a normal package dependency. A local dependency such as
|
||||
`issue-core @ file:///home/worsch/issue-core` makes Docker builds depend on a
|
||||
specific operator workstation and forces non-portable BuildKit named contexts.
|
||||
|
||||
**Current state.** The repo has package release plumbing prepared:
|
||||
|
||||
- `make package-check` builds and validates `issue-core==0.2.0`.
|
||||
- `make publish-gitea` uploads `dist/*` to the Coulomb Gitea PyPI endpoint.
|
||||
- `.gitea/workflows/publish-python-package.yml` can publish on `v*` tags once
|
||||
package registry secrets exist.
|
||||
- `docs/package-release.md` documents local and tag-based publishing.
|
||||
|
||||
**Resolved blocker.** Publishing required a Gitea package username/token with
|
||||
permission to upload to:
|
||||
|
||||
```text
|
||||
https://gitea.coulomb.social/api/packages/coulomb/pypi
|
||||
```
|
||||
|
||||
The package was published on 2026-06-05 using the operator-provided token from
|
||||
`/tmp/gat.tmp`; the token value was not written to repo files or command logs.
|
||||
|
||||
**Implementation steps.**
|
||||
|
||||
1. Configure Gitea repository or organization secrets:
|
||||
`GITEA_PACKAGE_USER` and `GITEA_PACKAGE_TOKEN`.
|
||||
2. Publish `issue-core==0.2.0` either by pushing tag `v0.2.0` or by running:
|
||||
|
||||
```bash
|
||||
TWINE_USERNAME=<gitea-user> \
|
||||
TWINE_PASSWORD=<package-token> \
|
||||
make publish-gitea
|
||||
```
|
||||
|
||||
3. Verify the simple index exposes the package:
|
||||
|
||||
```bash
|
||||
curl -fsS https://gitea.coulomb.social/api/packages/coulomb/pypi/simple/issue-core/
|
||||
```
|
||||
|
||||
4. Verify a clean environment can install the package from the Gitea simple
|
||||
index with credentials injected outside Git.
|
||||
5. Coordinate with `vergabe-teilnahme` to regenerate its `uv.lock` from the
|
||||
published package and confirm its Docker build no longer needs the sibling
|
||||
`issue-core` checkout.
|
||||
|
||||
**Done when.**
|
||||
|
||||
- `issue-core==0.2.0` is visible in the Coulomb Gitea PyPI simple index.
|
||||
- A clean Python environment can install `issue-core>=0.2,<0.3` from Gitea.
|
||||
- The publish workflow has the required secrets and a documented release path.
|
||||
- The Railiance app deployment blocker can be closed without relying on local
|
||||
path dependencies.
|
||||
|
||||
**Completed 2026-06-05.** Built and checked `issue-core==0.2.0`, published the
|
||||
wheel and source distribution to the Coulomb Gitea PyPI registry as `tegwick`
|
||||
through a temporary local Kubernetes port-forward, then exposed the approved
|
||||
public `/api/packages` ingress route from `railiance-forge`. The public
|
||||
package-specific simple index returns `200`, a clean temporary environment can
|
||||
install `issue-core==0.2.0` from Gitea, and `vergabe-teilnahme/uv.lock` now
|
||||
resolves `issue-core` from the Gitea registry instead of a sibling checkout.
|
||||
327
workplans/ISSUE-WP-0003-railiance01-deployment.md
Normal file
327
workplans/ISSUE-WP-0003-railiance01-deployment.md
Normal file
@@ -0,0 +1,327 @@
|
||||
---
|
||||
id: ISSUE-WP-0003
|
||||
type: workplan
|
||||
title: "Deploy issue-core as a service on railiance01 (ArgoCD GitOps pilot)"
|
||||
domain: infotech
|
||||
repo: issue-core
|
||||
status: finished
|
||||
owner: claude
|
||||
topic_slug: custodian
|
||||
created: "2026-06-19"
|
||||
updated: "2026-06-30"
|
||||
state_hub_workstream_id: "896ace77-21b3-450b-8fb7-254aefc8c570"
|
||||
---
|
||||
|
||||
# Deploy issue-core as a service on railiance01 (ArgoCD GitOps pilot)
|
||||
|
||||
`issue-core` is the authoritative task-lifecycle manager and the REST ingestion
|
||||
target for activity-core's `IssueSink`. Deployment artifacts are on `main`
|
||||
(`Dockerfile`, `docker-entrypoint.sh`, `k8s/railiance/`); image
|
||||
`gitea.coulomb.social/coulomb/issue-core:0.2.1` is built, pushed, and
|
||||
pullable. The railiance01 cluster now reconciles `issue-core` through ArgoCD;
|
||||
External Secrets Operator reads the OpenBao-backed runtime Secret and the
|
||||
Deployment is live on port 8765.
|
||||
|
||||
This workplan stands up `issue-core` as a first-class in-cluster service on
|
||||
railiance01 **via ArgoCD GitOps** — making issue-core the cluster's first
|
||||
declarative Application and turning on the idle GitOps capability.
|
||||
|
||||
## Current state (verified 2026-06-25)
|
||||
|
||||
- **Deployment artifacts in-repo:** `Dockerfile`, `docker-entrypoint.sh`, and
|
||||
`k8s/railiance/` (Kustomize: ExternalSecret, ConfigMap, Deployment, Service).
|
||||
Image builds locally; `docker run` + `GET /healthz` returns 200. Image pushed
|
||||
and pullable as `gitea.coulomb.social/coulomb/issue-core:0.2.1` (digest
|
||||
`sha256:729c0e56…`). `coulomb` org packages are public — no `imagePullSecret`
|
||||
required per `railiance-forge/docs/gitea-container-registry.md`.
|
||||
- **Dockerfile fix (2026-06-19):** build arg renamed `GITEA_PYPI_INDEX_URL` —
|
||||
`ARG PIP_INDEX_URL` leaked into the build env and pip used Gitea as the sole
|
||||
index, so dependencies like `click` were not found.
|
||||
- **railiance01 cluster:** `issue-core` namespace, Service, ExternalSecret,
|
||||
Secret, and Deployment are present. ArgoCD reports the `issue-core` Application
|
||||
Synced/Healthy at revision `11a0a69`; pod is Ready on image `0.2.1`.
|
||||
- **activity-core handoff still pending:** `activity-core/k8s/railiance/20-runtime.yaml` still points at port 8010 and keeps `ISSUE_SINK_TYPE: "null"`; T06 tracks switching it to the live issue-core service on port 8765.
|
||||
- **Packaging precursor is done:** `ISSUE-WP-0002` published
|
||||
`issue-core==0.2.0` to the Coulomb Gitea PyPI index. The live `0.2.1` image
|
||||
was built from the committed source tree as a deployment hotfix.
|
||||
- **ArgoCD is active for the pilot:** railiance-platform owns the bootstrap and tenant AppProject; `issue-core` is Synced/Healthy as the pilot workload.
|
||||
- **Existing deploy pattern is imperative** (the path we are *replacing* for
|
||||
this service): local `docker build` → `k3s ctr images import` (side-load, no
|
||||
registry) → `rsync` manifests → `kubectl apply` (see
|
||||
`activity-core/k8s/railiance/README.md`).
|
||||
|
||||
## Repo-side progress (2026-06-23)
|
||||
|
||||
- Added `docs/argocd-gitops.md` with the issue-core GitOps runbook, including
|
||||
image publish, ArgoCD sync checks, OpenBao/ExternalSecret contract, health
|
||||
probe, authenticated ingestion smoke, cleanup, and activity-core handoff.
|
||||
- Broadened `POST /issues/` so `triggering_event_id` accepts any non-empty
|
||||
traceability string. Event-driven activity-core paths can still send UUIDs;
|
||||
scheduled/cron paths may now send a stable key such as `scheduled`.
|
||||
|
||||
## Live progress (2026-06-25)
|
||||
|
||||
- Added railiance-platform ESO/OpenBao plumbing and provisioned the canonical
|
||||
OpenBao path `platform/workloads/issue-core/issue-core/issue-core-runtime`
|
||||
with `ISSUE_CORE_API_KEY` and `GITEA_BACKEND_TOKEN` (values not logged).
|
||||
- Created dedicated Gitea service user `issue-core-svc` and stored a scoped
|
||||
backend token in OpenBao for issue creation.
|
||||
- Published and deployed `gitea.coulomb.social/coulomb/issue-core:0.2.1`
|
||||
(`sha256:729c0e56…`) with the Gitea label-payload fix and numeric UID
|
||||
securityContext.
|
||||
- ArgoCD `issue-core` is Synced/Healthy at `11a0a69`; ExternalSecret is Ready;
|
||||
`/healthz` returns 200; authenticated `POST /issues/` returned 201 and Gitea
|
||||
issue id `175`.
|
||||
|
||||
## Closeout recheck (2026-06-30)
|
||||
|
||||
- issue-core-owned deployment work remains complete: manifests, runtime secret
|
||||
contract, backend config, runbook, and direct authenticated ingestion smoke are
|
||||
done for image `0.2.1`.
|
||||
- The remaining completion gate is the activity-core producer handoff. A
|
||||
non-secret source recheck of `/home/worsch/activity-core/k8s/railiance/20-runtime.yaml`
|
||||
still shows `ISSUE_CORE_URL` on port `8010` and `ISSUE_SINK_TYPE: "null"`.
|
||||
- `ops-warden` routing catalog entry `activity-core-issue-sink` confirms the
|
||||
lane is owned by activity-core + issue-core and that ops-warden does not vend
|
||||
`ISSUE_CORE_API_KEY`.
|
||||
- This WSL session does not have `kubectl` on PATH, so live ArgoCD/Kubernetes
|
||||
state could not be re-polled from the workstation. Keep T06/T07 at `wait`
|
||||
until the activity-core runtime is switched to the service on port `8765`,
|
||||
receives the shared key through OpenBao, and an activity-core emission returns
|
||||
issue-core HTTP 201 with a created Gitea issue.
|
||||
|
||||
## Decisions
|
||||
|
||||
- **Deployment method = ArgoCD GitOps** (operator decision 2026-06-19).
|
||||
issue-core is the pilot Application; the imperative side-load pattern is not
|
||||
used for this service.
|
||||
- **ArgoCD bootstrap owned by `railiance-platform`** (operator decision
|
||||
2026-06-19). Platform owns repo registration, AppProject/app-of-apps
|
||||
conventions, and the External-Secrets/OpenBao plumbing. issue-core only
|
||||
**contributes** its `Application` manifest + workload manifests into the
|
||||
agreed GitOps source. T02 is therefore a cross-repo dependency, not
|
||||
issue-core work — see handoff to railiance-platform.
|
||||
- **Backend = cluster Gitea (markitect)** (operator decision 2026-06-19).
|
||||
Ingested tasks route to the existing Gitea backend; no new Postgres/PVC.
|
||||
- **Secret management = OpenBao.** `ISSUE_CORE_API_KEY` is a shared ingestion
|
||||
key injected from OpenBao on both issue-core and the activity-core worker.
|
||||
ops-warden does **not** vend it (see
|
||||
`~/ops-warden/wiki/playbooks/activity-core-issue-sink.md`). Coordinate the
|
||||
canonical path with `railiance-platform` (`issue-core-ingestion-api-key`).
|
||||
- **Image delivery = container registry, not side-load.** GitOps requires a
|
||||
pullable image tag in a registry the cluster can reach (the Coulomb Gitea
|
||||
container registry); side-loading defeats declarative reproducibility.
|
||||
|
||||
## Open questions
|
||||
|
||||
- **GitOps source repo.** Resolved by `railiance-platform` as part of the
|
||||
bootstrap (T02 dependency): where issue-core's `Application` + manifests are
|
||||
expected to live (its own `issue-core/k8s/` vs. a platform GitOps repo) and
|
||||
the AppProject/app-of-apps convention to follow.
|
||||
- **Registry path & pull secret.** Resolved: `gitea.coulomb.social/coulomb/issue-core:<tag>`;
|
||||
public org packages need no pull secret (see `railiance-forge` container-registry docs).
|
||||
|
||||
---
|
||||
|
||||
## Container image published to a pullable registry
|
||||
|
||||
```task
|
||||
id: ISSUE-WP-0003-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "3723e896-3ec9-49b8-86f8-403993444da3"
|
||||
```
|
||||
|
||||
**Goal.** A reproducible, registry-hosted image ArgoCD-managed pods can pull.
|
||||
|
||||
- [x] Add `Dockerfile` building the checked-out `issue-core[api]` source.
|
||||
Entrypoint renders `backends.json` then `issue serve --host 0.0.0.0 --port 8765`.
|
||||
- [x] Local build succeeds; `docker run` + `GET /healthz` returns 200.
|
||||
- [x] Pushed `gitea.coulomb.social/coulomb/issue-core:0.2.1`; `docker pull`
|
||||
succeeds.
|
||||
- [x] No cluster pull secret needed (`coulomb` org packages are public).
|
||||
- [x] `POST /issues/` smoke against a running deployment returned 201.
|
||||
|
||||
## ArgoCD bootstrap (railiance-platform dependency) + issue-core Application
|
||||
|
||||
```task
|
||||
id: ISSUE-WP-0003-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "9b199b1d-d3c0-4621-b8f8-58c376cbf878"
|
||||
```
|
||||
|
||||
**Owner split.** ArgoCD bootstrap is **railiance-platform's** (operator
|
||||
decision 2026-06-19): repo registration in ArgoCD, AppProject/app-of-apps
|
||||
convention, and the agreed GitOps source layout. This handoff is complete for
|
||||
the issue-core pilot; issue-core contributes workload manifests and platform owns
|
||||
the tenant `Application` wrapper.
|
||||
|
||||
- **(railiance-platform)** Register the GitOps source repo (repository Secret +
|
||||
creds); define AppProject for cluster services; publish the source-repo/path
|
||||
convention and sync policy.
|
||||
- [x] **(issue-core)** Workload manifests in `k8s/railiance/` on `main` per
|
||||
platform contract (`docs/argocd-gitops.md`). Tenant `Application` lives in
|
||||
`railiance-platform/argocd/applications/issue-core.application.yaml`.
|
||||
- [x] **(railiance-platform)** Live bootstrap deployed; `issue-core` Application
|
||||
syncs from the issue-core repo through the tenant AppProject.
|
||||
- [x] Verify: `kubectl get applications -n argocd` shows `issue-core`
|
||||
Synced/Healthy at revision `11a0a69`; ArgoCD reconciled the `0.2.1` image
|
||||
manifest change.
|
||||
|
||||
## Kubernetes manifests (namespace, Deployment, Service) in GitOps source
|
||||
|
||||
```task
|
||||
id: ISSUE-WP-0003-T03
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "38887dd6-0988-4ad1-bc6b-2a1b8839829f"
|
||||
```
|
||||
|
||||
**Goal.** Declarative manifests in the GitOps source repo, synced by T02.
|
||||
|
||||
- [x] `k8s/railiance/` Kustomize bundle (namespace via ArgoCD
|
||||
`CreateNamespace=true`).
|
||||
- [x] Deployment: registry image tag `0.2.1`; port 8765; `/healthz` probes;
|
||||
resource requests/limits; env from ExternalSecret (T04) and ConfigMap (T05).
|
||||
- [x] Service: ClusterIP on **8765** as
|
||||
`issue-core.issue-core.svc.cluster.local`.
|
||||
- [x] Verify: ArgoCD syncs the manifests; pod Ready; `/healthz` returned 200
|
||||
from inside the cluster.
|
||||
|
||||
## OpenBao secret: ISSUE_CORE_API_KEY
|
||||
|
||||
```task
|
||||
id: ISSUE-WP-0003-T04
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "ad52527f-6222-4c11-9284-d8a3ed3b49ad"
|
||||
```
|
||||
|
||||
**Goal.** The shared ingestion key delivered to both sides from OpenBao.
|
||||
|
||||
- Provision `ISSUE_CORE_API_KEY` in OpenBao at the canonical path (coordinate
|
||||
with `railiance-platform`; catalog id `issue-core-ingestion-api-key`).
|
||||
- Deliver into the issue-core Deployment (T03) and the activity-core worker
|
||||
(T06) with the **same** value (External Secrets / Bao injector — match the
|
||||
cluster's established mechanism).
|
||||
- Never write the value to Git, manifests, State Hub, or logs.
|
||||
- Verify: both pods resolve a non-empty key; auth round-trip (401 without,
|
||||
201 with).
|
||||
- Done 2026-06-25: canonical OpenBao path exists, `ClusterSecretStore/openbao` is
|
||||
Ready, `ExternalSecret/issue-core-runtime` is Ready, and the Kubernetes Secret
|
||||
contains the two expected data keys. activity-core consumption remains in T06.
|
||||
|
||||
## In-cluster backend config (cluster Gitea / markitect)
|
||||
|
||||
```task
|
||||
id: ISSUE-WP-0003-T05
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "10923f1e-050d-4f3e-980e-b061fef5f33a"
|
||||
```
|
||||
|
||||
**Goal.** issue-core's `backends.json` inside the cluster points `default` at
|
||||
the cluster Gitea (markitect) backend.
|
||||
|
||||
- [x] ConfigMap `issue-core-backends` with in-cluster Gitea URL
|
||||
(`gitea-http.default.svc.cluster.local:3000`); token sentinel `__FROM_ENV__`.
|
||||
- [x] `docker-entrypoint.sh` renders `~/.config/issue-tracker/backends.json`
|
||||
from `BACKENDS_TEMPLATE` + `GITEA_BACKEND_TOKEN` at startup.
|
||||
- [x] Verify: authenticated `POST /issues/` returned 201 and created Gitea
|
||||
issue id `175` via the live service.
|
||||
|
||||
## Wire activity-core to the live service
|
||||
|
||||
```task
|
||||
id: ISSUE-WP-0003-T06
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "96b14cdb-364f-4eab-a80e-dd8b3859c694"
|
||||
```
|
||||
|
||||
**Goal.** activity-core emits to the live issue-core Service.
|
||||
|
||||
- Fix `activity-core/k8s/railiance/20-runtime.yaml`:
|
||||
`ISSUE_CORE_URL` port `8010 -> 8765`; flip `ISSUE_SINK_TYPE` `null -> rest`
|
||||
once issue-core is Ready.
|
||||
- Inject `ISSUE_CORE_API_KEY` into the activity-core worker from the same
|
||||
OpenBao secret (T04).
|
||||
- [x] **Contract gap closed on the issue-core side:** `POST /issues/` now
|
||||
accepts `triggering_event_id` as a non-empty traceability string, so
|
||||
event-driven paths can send UUIDs and cron paths can send stable keys such as
|
||||
`"scheduled"`.
|
||||
- [ ] activity-core runtime source still needs the live flip:
|
||||
`ISSUE_CORE_URL` `8010 -> 8765` and `ISSUE_SINK_TYPE` `"null" -> "rest"`.
|
||||
- [ ] activity-core worker still needs the shared `ISSUE_CORE_API_KEY` from the
|
||||
approved OpenBao lane; never write the value to Git, State Hub, logs, or chat.
|
||||
- Verify: an activity-core run emits a task that lands in cluster Gitea via
|
||||
issue-core.
|
||||
|
||||
## End-to-end verification + GitOps runbook
|
||||
|
||||
```task
|
||||
id: ISSUE-WP-0003-T07
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "8d853b8e-cfca-441d-b817-0a29e37bd66e"
|
||||
```
|
||||
|
||||
**Goal.** Confirm the deployed service is healthy and document the new path.
|
||||
|
||||
- ArgoCD Application Synced/Healthy; issue-core Pod Ready; Service reachable
|
||||
cluster-internal.
|
||||
- [x] activity-core -> issue-core emission returns 201 and creates a Gitea issue
|
||||
(2026-07-02: Gitea issue `176` via the live sink path — see completion note).
|
||||
- [x] Document the GitOps runbook (image build/push, ArgoCD sync, secret
|
||||
contract, smoke, activity-core handoff) in `docs/argocd-gitops.md`.
|
||||
- Emit an `add_progress_event` milestone to the hub when the activity-core
|
||||
emission proof exists and this workplan can move from `blocked` to `finished`.
|
||||
|
||||
---
|
||||
|
||||
## See also
|
||||
|
||||
- `ISSUE-WP-0002` — Gitea PyPI publication (packaging precursor, finished).
|
||||
- `railiance-apps-WP-0004` I03 — issue-core packaging/image enablement notes.
|
||||
- `railiance-forge` — Gitea container registry docs.
|
||||
- `activity-core/docs/issue-core-emission-boundary.md` — emission contract.
|
||||
- `activity-core/k8s/railiance/README.md` — the imperative pattern being
|
||||
superseded for this service.
|
||||
- `~/ops-warden/wiki/playbooks/activity-core-issue-sink.md` — key routing.
|
||||
|
||||
|
||||
## Completion 2026-07-02 — live emission proven, topology corrected
|
||||
|
||||
**Topology correction:** this workplan's "railiance01 cluster" is actually the
|
||||
CoulombCore k3s cluster (92.205.130.254, reached via the workstation
|
||||
kubeconfig tunnel `127.0.0.1:16443`). The real railiance01 (92.205.62.239)
|
||||
hosts activity-core and has no issue-core namespace. The in-cluster
|
||||
`issue-core.issue-core.svc.cluster.local` URL that T06 originally assumed was
|
||||
therefore never resolvable from activity-core.
|
||||
|
||||
**Cross-machine lane built (2026-07-02):**
|
||||
- ops-bridge gained an optional `remote_host` forward destination
|
||||
(ops-bridge commit) enabling tunnels to k3s ClusterIPs.
|
||||
- Tunnels: `issue-core-coulombcore` (workstation `127.0.0.1:18765` ->
|
||||
CoulombCore ClusterIP `10.43.103.154:8765`, health-checked on `/healthz`)
|
||||
and `issue-core-railiance01` (railiance01 `127.0.0.1:18765` -> workstation).
|
||||
- activity-core commit `a1e2a42`: new `actcore-issue-core-bridge`
|
||||
hostNetwork proxy (host port 18081 -> node-local 18765) cloned from the
|
||||
state-hub-bridge pattern, `ISSUE_CORE_URL` ->
|
||||
`http://actcore-issue-core-bridge.activity-core.svc.cluster.local:8765`,
|
||||
`ISSUE_SINK_TYPE` -> `rest`.
|
||||
- `ISSUE_CORE_API_KEY` merged into railiance01's `actcore-runtime-secret` by
|
||||
the operator via a stdin-only pipe from the approved OpenBao lane
|
||||
(`CCR-2026-0002`, activated the same day).
|
||||
|
||||
**Emission proof:** from inside the restarted actcore-worker, the real
|
||||
`IssueCoreRestSink` emitted a labeled smoke TaskSpec and received
|
||||
`TaskRef(external_id='176', backend='gitea')` — Gitea issue `176` in
|
||||
`coulomb/markitect-main` (default backend). Auth, bridge, tunnels, issue-core
|
||||
validation, and Gitea creation all exercised on the production path.
|
||||
|
||||
**Contract note for emitters:** `POST /issues/` requires `target_repo` and
|
||||
`activity_definition_id` (422 otherwise). activity-core `TaskSpec` defaults
|
||||
(`target_repo=None`, `activity_definition_id=""`) will be rejected — rule and
|
||||
instruction emitters must populate both.
|
||||
Reference in New Issue
Block a user