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
|
# Capability Bootstrap System
|
||||||
|
|
||||||
**How coding agents discover and integrate the issue-facade capability.**
|
**How coding agents discover and integrate the issue-core capability.**
|
||||||
|
|
||||||
## Design Philosophy
|
## Design Philosophy
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
### 1. Self-Description (Machine-Readable)
|
### 1. Self-Description (Machine-Readable)
|
||||||
|
|
||||||
**File:** `CAPABILITY.yaml`
|
**File:** `CAPABILITY-issue-tracking.yaml`
|
||||||
|
|
||||||
Contains machine-readable metadata that agents and tooling can parse:
|
Contains machine-readable metadata that agents and tooling can parse:
|
||||||
- What the capability does
|
- What the capability does
|
||||||
@@ -29,7 +29,7 @@ Contains machine-readable metadata that agents and tooling can parse:
|
|||||||
**Usage:**
|
**Usage:**
|
||||||
```bash
|
```bash
|
||||||
# Tools can parse this to understand capabilities
|
# 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"
|
# Output: "Agent coordination via issue tracking"
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ Comprehensive guide for coding agents:
|
|||||||
- Error handling
|
- Error handling
|
||||||
- Examples
|
- Examples
|
||||||
|
|
||||||
**Injected into:** `.claude/capabilities/issue-facade.md` in main project
|
**Injected into:** `.claude/capabilities/issue-core.md` in main project
|
||||||
|
|
||||||
### 3. Integration Automation
|
### 3. Integration Automation
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ Interactive script that:
|
|||||||
```bash
|
```bash
|
||||||
make integrate
|
make integrate
|
||||||
# or
|
# or
|
||||||
cd capabilities/issue-facade && ./.capability/integrate.sh
|
cd capabilities/issue-core && ./.capability/integrate.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Integration Checklist
|
### 4. Integration Checklist
|
||||||
@@ -81,7 +81,7 @@ Step-by-step checklist for humans integrating the capability:
|
|||||||
|
|
||||||
```
|
```
|
||||||
Main Project Setup
|
Main Project Setup
|
||||||
├── 1. Human runs: cd capabilities/issue-facade && make integrate
|
├── 1. Human runs: cd capabilities/issue-core && make integrate
|
||||||
├── 2. Script installs capability
|
├── 2. Script installs capability
|
||||||
├── 3. Script configures backend (prompts for credentials)
|
├── 3. Script configures backend (prompts for credentials)
|
||||||
├── 4. Script copies agent-context.md → .claude/capabilities/
|
├── 4. Script copies agent-context.md → .claude/capabilities/
|
||||||
@@ -101,13 +101,13 @@ Main Project Setup
|
|||||||
Agent Workflow
|
Agent Workflow
|
||||||
├── 1. Agent receives task involving issues
|
├── 1. Agent receives task involving issues
|
||||||
├── 2. Agent checks .claude/capabilities/ for relevant docs
|
├── 2. Agent checks .claude/capabilities/ for relevant docs
|
||||||
├── 3. Agent finds issue-facade.md with comprehensive guide
|
├── 3. Agent finds issue-core.md with comprehensive guide
|
||||||
├── 4. Agent uses Python API or CLI as documented
|
├── 4. Agent uses Python API or CLI as documented
|
||||||
└── 5. Agent avoids direct API calls (warned in docs)
|
└── 5. Agent avoids direct API calls (warned in docs)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Key Files Agent Reads:**
|
**Key Files Agent Reads:**
|
||||||
- `.claude/capabilities/issue-facade.md` - Complete usage guide
|
- `.claude/capabilities/issue-core.md` - Complete usage guide
|
||||||
- `.claude/context/capabilities.md` - High-level capability list
|
- `.claude/context/capabilities.md` - High-level capability list
|
||||||
- `.claude/commands/use-issues.md` - Slash command for context injection
|
- `.claude/commands/use-issues.md` - Slash command for context injection
|
||||||
|
|
||||||
@@ -116,18 +116,18 @@ Agent Workflow
|
|||||||
```
|
```
|
||||||
project-root/
|
project-root/
|
||||||
├── capabilities/
|
├── capabilities/
|
||||||
│ └── issue-facade/ # Capability code
|
│ └── issue-core/ # Capability code
|
||||||
│ ├── CAPABILITY.yaml # Machine-readable metadata
|
│ ├── CAPABILITY-issue-tracking.yaml # Machine-readable metadata
|
||||||
│ ├── .capability/
|
│ ├── .capability/
|
||||||
│ │ ├── agent-context.md # Agent guide (source)
|
│ │ ├── agent-context.md # Agent guide (source)
|
||||||
│ │ ├── integrate.sh # Integration script
|
│ │ ├── integrate.sh # Integration script
|
||||||
│ │ └── README.md # This file
|
│ │ └── README.md # This file
|
||||||
│ ├── issue_tracker/ # Python package
|
│ ├── issue_core/ # Python package
|
||||||
│ └── ...
|
│ └── ...
|
||||||
│
|
│
|
||||||
├── .claude/ # Claude Code configuration
|
├── .claude/ # Claude Code configuration
|
||||||
│ ├── capabilities/ # Capability docs for agents
|
│ ├── capabilities/ # Capability docs for agents
|
||||||
│ │ └── issue-facade.md # Agent guide (copy)
|
│ │ └── issue-core.md # Agent guide (copy)
|
||||||
│ │
|
│ │
|
||||||
│ ├── commands/ # Slash commands
|
│ ├── commands/ # Slash commands
|
||||||
│ │ └── use-issues.md # /use-issues command
|
│ │ └── use-issues.md # /use-issues command
|
||||||
@@ -135,7 +135,7 @@ project-root/
|
|||||||
│ └── context/ # Always-available context
|
│ └── context/ # Always-available context
|
||||||
│ └── capabilities.md # List of all capabilities
|
│ └── capabilities.md # List of all capabilities
|
||||||
│
|
│
|
||||||
└── .issue-facade/ # Capability config (gitignored)
|
└── .issue-core/ # Capability config (gitignored)
|
||||||
├── config.json # Backend configuration
|
├── config.json # Backend configuration
|
||||||
└── issues.db # Local cache/backup
|
└── issues.db # Local cache/backup
|
||||||
```
|
```
|
||||||
@@ -150,13 +150,13 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
def has_issue_capability(project_root: Path) -> bool:
|
def has_issue_capability(project_root: Path) -> bool:
|
||||||
"""Check if issue-facade capability is available."""
|
"""Check if issue-core capability is available."""
|
||||||
capability_guide = project_root / ".claude/capabilities/issue-facade.md"
|
capability_guide = project_root / ".claude/capabilities/issue-core.md"
|
||||||
return capability_guide.exists()
|
return capability_guide.exists()
|
||||||
|
|
||||||
if has_issue_capability(Path.cwd()):
|
if has_issue_capability(Path.cwd()):
|
||||||
# Use capability
|
# Use capability
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
backend = GiteaBackend()
|
backend = GiteaBackend()
|
||||||
else:
|
else:
|
||||||
# Fall back or prompt human
|
# Fall back or prompt human
|
||||||
@@ -173,15 +173,15 @@ def get_capability_docs(capability_name: str) -> str:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# Agent can read and understand the guide
|
# Agent can read and understand the guide
|
||||||
docs = get_capability_docs("issue-facade")
|
docs = get_capability_docs("issue-core")
|
||||||
# Parse docs for API usage patterns...
|
# Parse docs for API usage patterns...
|
||||||
```
|
```
|
||||||
|
|
||||||
**3. Use the API as documented:**
|
**3. Use the API as documented:**
|
||||||
```python
|
```python
|
||||||
# Example from agent-context.md
|
# Example from agent-context.md
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
from issue_tracker.core.interfaces import IssueFilter
|
from issue_core.core.interfaces import IssueFilter
|
||||||
|
|
||||||
backend = GiteaBackend()
|
backend = GiteaBackend()
|
||||||
backend.connect(config)
|
backend.connect(config)
|
||||||
@@ -204,8 +204,8 @@ response = requests.post(
|
|||||||
**Good (Uses capability):**
|
**Good (Uses capability):**
|
||||||
```python
|
```python
|
||||||
# ✅ Uses capability
|
# ✅ Uses capability
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
from issue_tracker.core.models import Issue, IssueState
|
from issue_core.core.models import Issue, IssueState
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
backend = GiteaBackend()
|
backend = GiteaBackend()
|
||||||
@@ -228,7 +228,7 @@ backend.create_issue(issue)
|
|||||||
**1. Create capability structure:**
|
**1. Create capability structure:**
|
||||||
```
|
```
|
||||||
capabilities/your-capability/
|
capabilities/your-capability/
|
||||||
├── CAPABILITY.yaml # Metadata
|
├── CAPABILITY-issue-tracking.yaml # Metadata
|
||||||
├── .capability/
|
├── .capability/
|
||||||
│ ├── agent-context.md # Agent guide
|
│ ├── agent-context.md # Agent guide
|
||||||
│ ├── integrate.sh # Integration script
|
│ ├── integrate.sh # Integration script
|
||||||
@@ -237,7 +237,7 @@ capabilities/your-capability/
|
|||||||
└── your_package/ # Implementation
|
└── your_package/ # Implementation
|
||||||
```
|
```
|
||||||
|
|
||||||
**2. Write CAPABILITY.yaml:**
|
**2. Write CAPABILITY-issue-tracking.yaml:**
|
||||||
```yaml
|
```yaml
|
||||||
metadata:
|
metadata:
|
||||||
name: your-capability
|
name: your-capability
|
||||||
@@ -287,7 +287,7 @@ make integrate
|
|||||||
|
|
||||||
### Manual Discovery (Human)
|
### Manual Discovery (Human)
|
||||||
1. Human sees `capabilities/` directory
|
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
|
3. Runs `make integrate` to set up for agents
|
||||||
|
|
||||||
### Automatic Discovery (Agent)
|
### Automatic Discovery (Agent)
|
||||||
@@ -302,7 +302,7 @@ make integrate
|
|||||||
make discover-capabilities
|
make discover-capabilities
|
||||||
# Output:
|
# Output:
|
||||||
# Found capabilities:
|
# Found capabilities:
|
||||||
# - issue-facade (v1.0.0) - Issue tracking coordination
|
# - issue-core (v1.0.0) - Issue tracking coordination
|
||||||
# - ... (other capabilities)
|
# - ... (other capabilities)
|
||||||
|
|
||||||
# Auto-integrate all
|
# Auto-integrate all
|
||||||
@@ -318,7 +318,7 @@ Capabilities have priority scores (0-100) indicating importance:
|
|||||||
- **50-69 (Medium):** Use when available
|
- **50-69 (Medium):** Use when available
|
||||||
- **Below 50 (Low):** Optional convenience
|
- **Below 50 (Low):** Optional convenience
|
||||||
|
|
||||||
**issue-facade priority: 95 (Critical)**
|
**issue-core priority: 95 (Critical)**
|
||||||
|
|
||||||
Agents should check priority when deciding whether to use a capability or fall back to alternatives.
|
Agents should check priority when deciding whether to use a capability or fall back to alternatives.
|
||||||
|
|
||||||
@@ -367,7 +367,7 @@ Agents should check priority when deciding whether to use a capability or fall b
|
|||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
**Q: Why not just document "use issue-facade" in README?**
|
**Q: Why not just document "use issue-core" in README?**
|
||||||
A: Agents often skip general docs. Putting it in `.claude/capabilities/` makes it part of their working context.
|
A: Agents often skip general docs. Putting it in `.claude/capabilities/` makes it part of their working context.
|
||||||
|
|
||||||
**Q: What if agent bypasses capability anyway?**
|
**Q: What if agent bypasses capability anyway?**
|
||||||
@@ -386,7 +386,7 @@ A: Document rollback in integration checklist. Keep backup configs. Have fallbac
|
|||||||
|
|
||||||
**The capability bootstrap system works by:**
|
**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/`
|
2. **Context injection** - Integration copies docs to `.claude/capabilities/`
|
||||||
3. **Agent discovery** - Agents check context before implementing
|
3. **Agent discovery** - Agents check context before implementing
|
||||||
4. **Natural preference** - Good docs + warnings make capability easier than alternatives
|
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):**
|
**With capability (properly integrated):**
|
||||||
```python
|
```python
|
||||||
# Agent checks .claude/capabilities/issue-facade.md
|
# Agent checks .claude/capabilities/issue-core.md
|
||||||
# Reads: "Use this API, don't use direct requests"
|
# Reads: "Use this API, don't use direct requests"
|
||||||
# Agent follows documented pattern:
|
# Agent follows documented pattern:
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
backend = GiteaBackend()
|
backend = GiteaBackend()
|
||||||
backend.connect(config)
|
backend.connect(config)
|
||||||
backend.create_issue(issue)
|
backend.create_issue(issue)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Issue Facade - Agent Integration Context
|
# Issue Core - Agent Integration Context
|
||||||
|
|
||||||
**🤖 For Coding Agents: Read this to understand how to use issue tracking in this project.**
|
**🤖 For Coding Agents: Read this to understand how to use issue tracking in this project.**
|
||||||
|
|
||||||
@@ -25,15 +25,15 @@
|
|||||||
# Verify installation
|
# Verify installation
|
||||||
issue --version
|
issue --version
|
||||||
# or
|
# or
|
||||||
python -c "from issue_tracker.backends.gitea import GiteaBackend; print('OK')"
|
python -c "from issue_core.backends.gitea import GiteaBackend; print('OK')"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Basic Usage (Python)
|
### Basic Usage (Python)
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
from issue_tracker.core.models import Issue, Label, IssueState, User, Comment
|
from issue_core.core.models import Issue, Label, IssueState, User, Comment
|
||||||
from issue_tracker.core.interfaces import IssueFilter
|
from issue_core.core.interfaces import IssueFilter
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@@ -228,7 +228,7 @@ if not verify_issue_backend():
|
|||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from issue_tracker.backends.gitea.backend import GiteaAPIError
|
from issue_core.backends.gitea.backend import GiteaAPIError
|
||||||
|
|
||||||
try:
|
try:
|
||||||
issue = backend.get_issue_by_number(42)
|
issue = backend.get_issue_by_number(42)
|
||||||
@@ -317,7 +317,7 @@ If you're unsure whether to use this capability for something:
|
|||||||
- **NO** → You can use other methods
|
- **NO** → You can use other methods
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
- "Create an issue for the bug I found" → **Use issue-facade**
|
- "Create an issue for the bug I found" → **Use issue-core**
|
||||||
- "Read the project README" → Don't need issue-facade
|
- "Read the project README" → Don't need issue-core
|
||||||
- "Check if issue #42 exists" → **Use issue-facade**
|
- "Check if issue #42 exists" → **Use issue-core**
|
||||||
- "Clone the repository" → Don't need issue-facade
|
- "Clone the repository" → Don't need issue-core
|
||||||
|
|||||||
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
|
#!/bin/bash
|
||||||
# Integration script for issue-facade capability
|
# Integration script for issue-core capability
|
||||||
# This script helps the main project discover and integrate the capability
|
# This script helps the main project discover and integrate the capability
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
CAPABILITY_NAME="issue-facade"
|
CAPABILITY_NAME="issue-core"
|
||||||
CAPABILITY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
CAPABILITY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$CAPABILITY_DIR/../.." && pwd)}"
|
PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$CAPABILITY_DIR/../.." && pwd)}"
|
||||||
|
|
||||||
echo "🔧 Issue Facade Capability Integration"
|
echo "🔧 Issue Core Capability Integration"
|
||||||
echo " Capability: $CAPABILITY_DIR"
|
echo " Capability: $CAPABILITY_DIR"
|
||||||
echo " Project: $PROJECT_ROOT"
|
echo " Project: $PROJECT_ROOT"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -90,7 +90,7 @@ case $choice in
|
|||||||
echo "📝 Adding to Claude Code context..."
|
echo "📝 Adding to Claude Code context..."
|
||||||
mkdir -p "$PROJECT_ROOT/.claude/capabilities"
|
mkdir -p "$PROJECT_ROOT/.claude/capabilities"
|
||||||
cp "$CAPABILITY_DIR/.capability/agent-context.md" \
|
cp "$CAPABILITY_DIR/.capability/agent-context.md" \
|
||||||
"$PROJECT_ROOT/.claude/capabilities/issue-facade.md"
|
"$PROJECT_ROOT/.claude/capabilities/issue-core.md"
|
||||||
|
|
||||||
# Create or update context file
|
# Create or update context file
|
||||||
CONTEXT_FILE="$PROJECT_ROOT/.claude/context/capabilities.md"
|
CONTEXT_FILE="$PROJECT_ROOT/.claude/context/capabilities.md"
|
||||||
@@ -102,10 +102,10 @@ case $choice in
|
|||||||
|
|
||||||
This project uses specialized capabilities. Always check for existing capabilities before implementing similar functionality.
|
This project uses specialized capabilities. Always check for existing capabilities before implementing similar functionality.
|
||||||
|
|
||||||
## Issue Tracking: issue-facade
|
## Issue Tracking: issue-core
|
||||||
|
|
||||||
**Location:** `capabilities/issue-facade/`
|
**Location:** `capabilities/issue-core/`
|
||||||
**Documentation:** `.claude/capabilities/issue-facade.md`
|
**Documentation:** `.claude/capabilities/issue-core.md`
|
||||||
**Priority:** CRITICAL (always use for issue operations)
|
**Priority:** CRITICAL (always use for issue operations)
|
||||||
|
|
||||||
**MUST USE FOR:**
|
**MUST USE FOR:**
|
||||||
@@ -120,13 +120,13 @@ This project uses specialized capabilities. Always check for existing capabiliti
|
|||||||
|
|
||||||
**Quick Start:**
|
**Quick Start:**
|
||||||
```python
|
```python
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
backend = GiteaBackend()
|
backend = GiteaBackend()
|
||||||
backend.connect(config)
|
backend.connect(config)
|
||||||
issues = backend.list_issues()
|
issues = backend.list_issues()
|
||||||
```
|
```
|
||||||
|
|
||||||
**Full Documentation:** See `.claude/capabilities/issue-facade.md`
|
**Full Documentation:** See `.claude/capabilities/issue-core.md`
|
||||||
EOF
|
EOF
|
||||||
echo "✓ Created $CONTEXT_FILE"
|
echo "✓ Created $CONTEXT_FILE"
|
||||||
else
|
else
|
||||||
@@ -138,7 +138,7 @@ EOF
|
|||||||
echo "✓ Added to Claude Code context"
|
echo "✓ Added to Claude Code context"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Files created:"
|
echo "Files created:"
|
||||||
echo " - $PROJECT_ROOT/.claude/capabilities/issue-facade.md"
|
echo " - $PROJECT_ROOT/.claude/capabilities/issue-core.md"
|
||||||
echo " - $CONTEXT_FILE"
|
echo " - $CONTEXT_FILE"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
@@ -148,15 +148,15 @@ EOF
|
|||||||
mkdir -p "$PROJECT_ROOT/.claude/commands"
|
mkdir -p "$PROJECT_ROOT/.claude/commands"
|
||||||
|
|
||||||
cat > "$PROJECT_ROOT/.claude/commands/use-issues.md" << 'EOF'
|
cat > "$PROJECT_ROOT/.claude/commands/use-issues.md" << 'EOF'
|
||||||
You are working with issue tracking. Use the **issue-facade capability**:
|
You are working with issue tracking. Use the **issue-core capability**:
|
||||||
|
|
||||||
## Available API
|
## Available API
|
||||||
|
|
||||||
**Python (Recommended):**
|
**Python (Recommended):**
|
||||||
```python
|
```python
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
from issue_tracker.core.models import Issue, Label, IssueState
|
from issue_core.core.models import Issue, Label, IssueState
|
||||||
from issue_tracker.core.interfaces import IssueFilter
|
from issue_core.core.interfaces import IssueFilter
|
||||||
|
|
||||||
backend = GiteaBackend()
|
backend = GiteaBackend()
|
||||||
backend.connect(config)
|
backend.connect(config)
|
||||||
@@ -193,7 +193,7 @@ issue close 42 --comment="Fixed"
|
|||||||
|
|
||||||
## Full Documentation
|
## Full Documentation
|
||||||
|
|
||||||
See `capabilities/issue-facade/AGENT_INTEGRATION.md` for:
|
See `capabilities/issue-core/AGENT_INTEGRATION.md` for:
|
||||||
- Complete API reference
|
- Complete API reference
|
||||||
- Coordination patterns
|
- Coordination patterns
|
||||||
- Error handling
|
- Error handling
|
||||||
@@ -206,7 +206,7 @@ EOF
|
|||||||
echo "Usage in Claude Code:"
|
echo "Usage in Claude Code:"
|
||||||
echo " /use-issues"
|
echo " /use-issues"
|
||||||
echo ""
|
echo ""
|
||||||
echo "This will inject issue-facade context into the conversation."
|
echo "This will inject issue-core context into the conversation."
|
||||||
;;
|
;;
|
||||||
|
|
||||||
5)
|
5)
|
||||||
@@ -250,7 +250,7 @@ EOF
|
|||||||
mkdir -p "$PROJECT_ROOT/.claude/context"
|
mkdir -p "$PROJECT_ROOT/.claude/context"
|
||||||
|
|
||||||
cp "$CAPABILITY_DIR/.capability/agent-context.md" \
|
cp "$CAPABILITY_DIR/.capability/agent-context.md" \
|
||||||
"$PROJECT_ROOT/.claude/capabilities/issue-facade.md"
|
"$PROJECT_ROOT/.claude/capabilities/issue-core.md"
|
||||||
|
|
||||||
# Create context file if not exists
|
# Create context file if not exists
|
||||||
CONTEXT_FILE="$PROJECT_ROOT/.claude/context/capabilities.md"
|
CONTEXT_FILE="$PROJECT_ROOT/.claude/context/capabilities.md"
|
||||||
@@ -258,21 +258,21 @@ EOF
|
|||||||
cat > "$CONTEXT_FILE" << 'EOF'
|
cat > "$CONTEXT_FILE" << 'EOF'
|
||||||
# Available Capabilities
|
# Available Capabilities
|
||||||
|
|
||||||
## Issue Tracking: issue-facade
|
## Issue Tracking: issue-core
|
||||||
|
|
||||||
**CRITICAL:** Always use this for issue operations. Never bypass with direct API calls.
|
**CRITICAL:** Always use this for issue operations. Never bypass with direct API calls.
|
||||||
|
|
||||||
**Docs:** `.claude/capabilities/issue-facade.md`
|
**Docs:** `.claude/capabilities/issue-core.md`
|
||||||
**Usage:** `/use-issues`
|
**Usage:** `/use-issues`
|
||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create slash command
|
# Create slash command
|
||||||
cat > "$PROJECT_ROOT/.claude/commands/use-issues.md" << 'EOF'
|
cat > "$PROJECT_ROOT/.claude/commands/use-issues.md" << 'EOF'
|
||||||
Use the issue-facade capability for all issue tracking operations.
|
Use the issue-core capability for all issue tracking operations.
|
||||||
|
|
||||||
**Quick reference:** See `.claude/capabilities/issue-facade.md`
|
**Quick reference:** See `.claude/capabilities/issue-core.md`
|
||||||
**Examples:** See `capabilities/issue-facade/examples/agents/`
|
**Examples:** See `capabilities/issue-core/examples/agents/`
|
||||||
|
|
||||||
**DO NOT use direct API calls or platform CLIs!**
|
**DO NOT use direct API calls or platform CLIs!**
|
||||||
EOF
|
EOF
|
||||||
@@ -285,7 +285,7 @@ EOF
|
|||||||
echo ""
|
echo ""
|
||||||
issue --version && echo "✓ CLI works" || echo "❌ CLI not working"
|
issue --version && echo "✓ CLI works" || echo "❌ CLI not working"
|
||||||
issue backend list | grep -q "default" && echo "✓ Backend configured" || echo "⚠️ Backend not configured"
|
issue backend list | grep -q "default" && echo "✓ Backend configured" || echo "⚠️ Backend not configured"
|
||||||
[ -f "$PROJECT_ROOT/.claude/capabilities/issue-facade.md" ] && echo "✓ Claude context exists" || echo "❌ Claude context missing"
|
[ -f "$PROJECT_ROOT/.claude/capabilities/issue-core.md" ] && echo "✓ Claude context exists" || echo "❌ Claude context missing"
|
||||||
[ -f "$PROJECT_ROOT/.claude/commands/use-issues.md" ] && echo "✓ Slash command exists" || echo "❌ Slash command missing"
|
[ -f "$PROJECT_ROOT/.claude/commands/use-issues.md" ] && echo "✓ Slash command exists" || echo "❌ Slash command missing"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
@@ -294,7 +294,7 @@ EOF
|
|||||||
echo "Next steps:"
|
echo "Next steps:"
|
||||||
echo " 1. Test: issue list --limit=5"
|
echo " 1. Test: issue list --limit=5"
|
||||||
echo " 2. In Claude Code: /use-issues"
|
echo " 2. In Claude Code: /use-issues"
|
||||||
echo " 3. See examples: capabilities/issue-facade/examples/agents/"
|
echo " 3. See examples: capabilities/issue-core/examples/agents/"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
0)
|
0)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Issue Facade Integration Checklist
|
# Issue Core Integration Checklist
|
||||||
|
|
||||||
**For project maintainers integrating this capability into their codebase.**
|
**For project maintainers integrating this capability into their codebase.**
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
- [ ] **Install capability:**
|
- [ ] **Install capability:**
|
||||||
```bash
|
```bash
|
||||||
pip install -e capabilities/issue-facade/
|
pip install -e capabilities/issue-core/
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Verify installation:**
|
- [ ] **Verify installation:**
|
||||||
@@ -42,8 +42,8 @@
|
|||||||
- [ ] **Copy agent context to project:**
|
- [ ] **Copy agent context to project:**
|
||||||
```bash
|
```bash
|
||||||
mkdir -p .claude/capabilities/
|
mkdir -p .claude/capabilities/
|
||||||
cp capabilities/issue-facade/.capability/agent-context.md \
|
cp capabilities/issue-core/.capability/agent-context.md \
|
||||||
.claude/capabilities/issue-facade.md
|
.claude/capabilities/issue-core.md
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Add to Claude Code context:**
|
- [ ] **Add to Claude Code context:**
|
||||||
@@ -53,17 +53,17 @@
|
|||||||
|
|
||||||
This project uses specialized capabilities. Always check these before implementing similar functionality.
|
This project uses specialized capabilities. Always check these before implementing similar functionality.
|
||||||
|
|
||||||
## Issue Tracking: issue-facade
|
## Issue Tracking: issue-core
|
||||||
|
|
||||||
**Location:** `capabilities/issue-facade/`
|
**Location:** `capabilities/issue-core/`
|
||||||
**Documentation:** `.claude/capabilities/issue-facade.md`
|
**Documentation:** `.claude/capabilities/issue-core.md`
|
||||||
|
|
||||||
**CRITICAL:** Always use this capability for issue operations. Never use:
|
**CRITICAL:** Always use this capability for issue operations. Never use:
|
||||||
- Direct API calls (requests to /api/v1/repos/...)
|
- Direct API calls (requests to /api/v1/repos/...)
|
||||||
- Platform CLIs (gh, glab)
|
- Platform CLIs (gh, glab)
|
||||||
- Platform libraries (PyGithub, python-gitlab)
|
- Platform libraries (PyGithub, python-gitlab)
|
||||||
|
|
||||||
See `.claude/capabilities/issue-facade.md` for usage patterns.
|
See `.claude/capabilities/issue-core.md` for usage patterns.
|
||||||
```
|
```
|
||||||
|
|
||||||
### Option 2: Slash Command
|
### Option 2: Slash Command
|
||||||
@@ -71,11 +71,11 @@
|
|||||||
- [ ] **Create slash command:**
|
- [ ] **Create slash command:**
|
||||||
Create `.claude/commands/use-issues.md`:
|
Create `.claude/commands/use-issues.md`:
|
||||||
```markdown
|
```markdown
|
||||||
You are working with issue tracking. Use the issue-facade capability:
|
You are working with issue tracking. Use the issue-core capability:
|
||||||
|
|
||||||
**Python API:**
|
**Python API:**
|
||||||
```python
|
```python
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
backend = GiteaBackend()
|
backend = GiteaBackend()
|
||||||
backend.connect(config)
|
backend.connect(config)
|
||||||
```
|
```
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
issue create "Title" --label=bug
|
issue create "Title" --label=bug
|
||||||
```
|
```
|
||||||
|
|
||||||
**Full docs:** See `capabilities/issue-facade/AGENT_INTEGRATION.md`
|
**Full docs:** See `capabilities/issue-core/AGENT_INTEGRATION.md`
|
||||||
|
|
||||||
**DO NOT use direct API calls or platform CLIs!**
|
**DO NOT use direct API calls or platform CLIs!**
|
||||||
```
|
```
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
## Agent Configuration
|
## Agent Configuration
|
||||||
|
|
||||||
- [ ] **Set agent identity:**
|
- [ ] **Set agent identity:**
|
||||||
Add to `.issue-facade/config.json`:
|
Add to `.issue-core/config.json`:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"agent": {
|
"agent": {
|
||||||
@@ -126,8 +126,8 @@
|
|||||||
|
|
||||||
- [ ] **Test basic operations:**
|
- [ ] **Test basic operations:**
|
||||||
```python
|
```python
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
from issue_tracker.core.interfaces import IssueFilter
|
from issue_core.core.interfaces import IssueFilter
|
||||||
|
|
||||||
backend = GiteaBackend()
|
backend = GiteaBackend()
|
||||||
backend.connect({'base_url': '...', 'token': '...', 'owner': '...', 'repo': '...'})
|
backend.connect({'base_url': '...', 'token': '...', 'owner': '...', 'repo': '...'})
|
||||||
@@ -153,26 +153,26 @@
|
|||||||
```markdown
|
```markdown
|
||||||
## Issue Tracking
|
## Issue Tracking
|
||||||
|
|
||||||
This project uses the issue-facade capability for unified issue tracking.
|
This project uses the issue-core capability for unified issue tracking.
|
||||||
|
|
||||||
**Setup:**
|
**Setup:**
|
||||||
```bash
|
```bash
|
||||||
pip install -e capabilities/issue-facade/
|
pip install -e capabilities/issue-core/
|
||||||
export GITEA_API_TOKEN="your-token"
|
export GITEA_API_TOKEN="your-token"
|
||||||
issue backend add myproject gitea
|
issue backend add myproject gitea
|
||||||
```
|
```
|
||||||
|
|
||||||
**Usage:** See `capabilities/issue-facade/AGENT_INTEGRATION.md`
|
**Usage:** See `capabilities/issue-core/AGENT_INTEGRATION.md`
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Add to CONTRIBUTING.md:**
|
- [ ] **Add to CONTRIBUTING.md:**
|
||||||
```markdown
|
```markdown
|
||||||
### Issue Tracking
|
### Issue Tracking
|
||||||
|
|
||||||
Always use the `issue` command or Python API from `issue_tracker` package.
|
Always use the `issue` command or Python API from `issue_core` package.
|
||||||
Never make direct API calls to Gitea/GitHub/GitLab.
|
Never make direct API calls to Gitea/GitHub/GitLab.
|
||||||
|
|
||||||
Examples: `capabilities/issue-facade/examples/agents/`
|
Examples: `capabilities/issue-core/examples/agents/`
|
||||||
```
|
```
|
||||||
|
|
||||||
## Security Review
|
## Security Review
|
||||||
@@ -180,9 +180,9 @@
|
|||||||
- [ ] **Verify tokens are not in code:** `git grep GITEA_TOKEN` (should be empty)
|
- [ ] **Verify tokens are not in code:** `git grep GITEA_TOKEN` (should be empty)
|
||||||
- [ ] **Check .gitignore includes:**
|
- [ ] **Check .gitignore includes:**
|
||||||
```
|
```
|
||||||
.issue-facade/config.json
|
.issue-core/config.json
|
||||||
.issue-facade/issues.db
|
.issue-core/issues.db
|
||||||
.issue-facade/credentials.json
|
.issue-core/credentials.json
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Audit token permissions:** Read-only for bots, write for implementation
|
- [ ] **Audit token permissions:** Read-only for bots, write for implementation
|
||||||
@@ -192,7 +192,7 @@
|
|||||||
|
|
||||||
- [ ] **Run capability tests:**
|
- [ ] **Run capability tests:**
|
||||||
```bash
|
```bash
|
||||||
cd capabilities/issue-facade/
|
cd capabilities/issue-core/
|
||||||
make test
|
make test
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -210,7 +210,7 @@
|
|||||||
|
|
||||||
- [ ] **Schedule regular updates:**
|
- [ ] **Schedule regular updates:**
|
||||||
```bash
|
```bash
|
||||||
cd capabilities/issue-facade/
|
cd capabilities/issue-core/
|
||||||
git pull origin main
|
git pull origin main
|
||||||
pip install -e . --upgrade
|
pip install -e . --upgrade
|
||||||
```
|
```
|
||||||
@@ -232,7 +232,7 @@ If capability causes issues:
|
|||||||
|
|
||||||
- [ ] **Keep backup config:**
|
- [ ] **Keep backup config:**
|
||||||
```bash
|
```bash
|
||||||
cp ~/.config/issue-facade/backends.json ~/.config/issue-facade/backends.json.backup
|
cp ~/.config/issue-core/backends.json ~/.config/issue-core/backends.json.backup
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Document rollback steps in project wiki/docs**
|
- [ ] **Document rollback steps in project wiki/docs**
|
||||||
|
|||||||
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
|
# Agent Integration Guide
|
||||||
|
|
||||||
**Issue Facade for Autonomous Coding Agent Coordination**
|
**Issue Core for Autonomous Coding Agent Coordination**
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
The **Issue Facade** capability provides a standardized interface for autonomous coding agents to coordinate project implementation through issue tracking. Instead of agents directly interfacing with platform-specific APIs (GitHub, GitLab, Gitea), they use a unified abstraction that works consistently across backends.
|
The **Issue Core** capability provides a standardized interface for autonomous coding agents to coordinate project implementation through issue tracking. Instead of agents directly interfacing with platform-specific APIs (GitHub, GitLab, Gitea), they use a unified abstraction that works consistently across backends.
|
||||||
|
|
||||||
### Why Issue Tracking for Agent Coordination?
|
### Why Issue Tracking for Agent Coordination?
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ Issue tracking provides a natural coordination mechanism for multi-agent softwar
|
|||||||
### 1. Installation
|
### 1. Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd capabilities/issue-facade
|
cd capabilities/issue-core
|
||||||
pip install -e .
|
pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ issue backend set-default my-project
|
|||||||
# Configure local SQLite backend
|
# Configure local SQLite backend
|
||||||
issue backend add local-work local
|
issue backend add local-work local
|
||||||
# Prompts for:
|
# Prompts for:
|
||||||
# - Database path: .issue-facade/issues.db
|
# - Database path: .issue-core/issues.db
|
||||||
|
|
||||||
issue backend set-default local-work
|
issue backend set-default local-work
|
||||||
```
|
```
|
||||||
@@ -178,8 +178,8 @@ issue list --label=reviewed --state=closed --format=json | \
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
# Agent creates implementation issues from requirements
|
# Agent creates implementation issues from requirements
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
from issue_tracker.core.models import Issue, Label, IssueState
|
from issue_core.core.models import Issue, Label, IssueState
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
backend = GiteaBackend()
|
backend = GiteaBackend()
|
||||||
@@ -224,9 +224,9 @@ for task in subtasks:
|
|||||||
### Python Integration
|
### Python Integration
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
from issue_tracker.core.models import Issue, Label, IssueState, User
|
from issue_core.core.models import Issue, Label, IssueState, User
|
||||||
from issue_tracker.core.interfaces import IssueFilter
|
from issue_core.core.interfaces import IssueFilter
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@@ -271,7 +271,7 @@ created.state = IssueState.IN_PROGRESS
|
|||||||
backend.update_issue(created)
|
backend.update_issue(created)
|
||||||
|
|
||||||
# Add comment
|
# Add comment
|
||||||
from issue_tracker.core.models import Comment
|
from issue_core.core.models import Comment
|
||||||
comment = Comment(
|
comment = Comment(
|
||||||
id=None,
|
id=None,
|
||||||
body="Analysis complete. Root cause: unclosed file handles in line 234",
|
body="Analysis complete. Root cause: unclosed file handles in line 234",
|
||||||
@@ -432,7 +432,7 @@ issue sync push backup gitea-remote
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
# Check for conflicts before sync
|
# Check for conflicts before sync
|
||||||
from issue_tracker.cli.sync_commands import sync_pull
|
from issue_core.cli.sync_commands import sync_pull
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sync_pull(source='remote', target='local', dry_run=True)
|
sync_pull(source='remote', target='local', dry_run=True)
|
||||||
@@ -505,7 +505,7 @@ Create a setup script for each project:
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# setup-issue-tracking.sh
|
# setup-issue-tracking.sh
|
||||||
|
|
||||||
cat > .issue-facade-config << EOF
|
cat > .issue-core-config << EOF
|
||||||
GITEA_URL=https://gitea.example.com
|
GITEA_URL=https://gitea.example.com
|
||||||
GITEA_OWNER=myorg
|
GITEA_OWNER=myorg
|
||||||
GITEA_REPO=myproject
|
GITEA_REPO=myproject
|
||||||
@@ -513,7 +513,7 @@ GITEA_TOKEN_FILE=~/.secrets/gitea-token
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Load config and configure backend
|
# Load config and configure backend
|
||||||
source .issue-facade-config
|
source .issue-core-config
|
||||||
export GITEA_API_TOKEN=$(cat $GITEA_TOKEN_FILE)
|
export GITEA_API_TOKEN=$(cat $GITEA_TOKEN_FILE)
|
||||||
|
|
||||||
issue backend add $(basename $(pwd)) gitea <<INPUT
|
issue backend add $(basename $(pwd)) gitea <<INPUT
|
||||||
@@ -551,7 +551,7 @@ for issue_number in [1, 2, 3, 4, 5]:
|
|||||||
backend.update_issue(issue)
|
backend.update_issue(issue)
|
||||||
|
|
||||||
# GOOD: Use local backend for bulk operations
|
# GOOD: Use local backend for bulk operations
|
||||||
from issue_tracker.backends.local import LocalSQLiteBackend
|
from issue_core.backends.local import LocalSQLiteBackend
|
||||||
|
|
||||||
local = LocalSQLiteBackend()
|
local = LocalSQLiteBackend()
|
||||||
local.connect({'db_path': '/tmp/batch.db'})
|
local.connect({'db_path': '/tmp/batch.db'})
|
||||||
@@ -595,7 +595,7 @@ if time.time() - last_fetch > 60:
|
|||||||
### Phase 1: Auto-Configuration (v1.1)
|
### Phase 1: Auto-Configuration (v1.1)
|
||||||
- Automatic git remote detection
|
- Automatic git remote detection
|
||||||
- Environment-variable-only setup
|
- Environment-variable-only setup
|
||||||
- Per-repository `.issue-facade/config.json` support
|
- Per-repository `.issue-core/config.json` support
|
||||||
- `issue config detect` command
|
- `issue config detect` command
|
||||||
|
|
||||||
### Phase 2: Agent Features (v1.2)
|
### Phase 2: Agent Features (v1.2)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Issue Facade Capability Manifest
|
# Issue Core Capability Manifest
|
||||||
# This file describes the capability to coding agents and integration systems
|
# This file describes the capability to coding agents and integration systems
|
||||||
|
|
||||||
metadata:
|
metadata:
|
||||||
name: issue-facade
|
name: issue-core
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
type: coordination-tool
|
type: coordination-tool
|
||||||
description: >
|
description: >
|
||||||
@@ -44,7 +44,7 @@ integration:
|
|||||||
methods:
|
methods:
|
||||||
python_api:
|
python_api:
|
||||||
available: true
|
available: true
|
||||||
import: "from issue_tracker.backends.gitea import GiteaBackend"
|
import: "from issue_core.backends.gitea import GiteaBackend"
|
||||||
docs: "AGENT_INTEGRATION.md"
|
docs: "AGENT_INTEGRATION.md"
|
||||||
|
|
||||||
cli:
|
cli:
|
||||||
@@ -59,7 +59,7 @@ integration:
|
|||||||
|
|
||||||
installation:
|
installation:
|
||||||
method: pip
|
method: pip
|
||||||
command: "pip install -e capabilities/issue-facade/"
|
command: "pip install -e capabilities/issue-core/"
|
||||||
verify: "issue --version"
|
verify: "issue --version"
|
||||||
|
|
||||||
configuration:
|
configuration:
|
||||||
@@ -119,8 +119,8 @@ credentials:
|
|||||||
|
|
||||||
security:
|
security:
|
||||||
- "Tokens never in code or logs"
|
- "Tokens never in code or logs"
|
||||||
- "Config stored in ~/.config/issue-facade/"
|
- "Config stored in ~/.config/issue-core/"
|
||||||
- "Per-repo config in .issue-facade/ (gitignored)"
|
- "Per-repo config in .issue-core/ (gitignored)"
|
||||||
|
|
||||||
best_practices:
|
best_practices:
|
||||||
- "Use read-only tokens for monitoring agents"
|
- "Use read-only tokens for monitoring agents"
|
||||||
@@ -131,8 +131,8 @@ credentials:
|
|||||||
agent_guidance:
|
agent_guidance:
|
||||||
quick_start: |
|
quick_start: |
|
||||||
# For Python agents:
|
# For Python agents:
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
from issue_tracker.core.interfaces import IssueFilter
|
from issue_core.core.interfaces import IssueFilter
|
||||||
|
|
||||||
backend = GiteaBackend()
|
backend = GiteaBackend()
|
||||||
backend.connect(config)
|
backend.connect(config)
|
||||||
@@ -158,12 +158,52 @@ agent_guidance:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
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 references
|
||||||
documentation:
|
documentation:
|
||||||
integration: "AGENT_INTEGRATION.md"
|
integration: "AGENT_INTEGRATION.md"
|
||||||
development: "CLAUDE.md"
|
development: "CLAUDE.md"
|
||||||
roadmap: "ROADMAP.md"
|
roadmap: "ROADMAP.md"
|
||||||
examples: "examples/agents/"
|
examples: "examples/agents/"
|
||||||
|
feedback: "feedback/README.md"
|
||||||
|
|
||||||
# Dependencies and requirements
|
# Dependencies and requirements
|
||||||
requirements:
|
requirements:
|
||||||
@@ -221,7 +261,7 @@ support:
|
|||||||
solution: "Check GITEA_API_TOKEN is set and valid"
|
solution: "Check GITEA_API_TOKEN is set and valid"
|
||||||
|
|
||||||
- problem: "Command not found: issue"
|
- problem: "Command not found: issue"
|
||||||
solution: "Run: pip install -e capabilities/issue-facade/"
|
solution: "Run: pip install -e capabilities/issue-core/"
|
||||||
|
|
||||||
# Integration priority score (higher = more important for agent to use)
|
# Integration priority score (higher = more important for agent to use)
|
||||||
priority:
|
priority:
|
||||||
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
|
# issue-core — Claude Code Instructions
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
@SCOPE.md
|
||||||
|
@.claude/rules/repo-identity.md
|
||||||
## Project Overview
|
@.claude/rules/session-protocol.md
|
||||||
|
@.claude/rules/first-session.md
|
||||||
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.
|
@.claude/rules/workplan-convention.md
|
||||||
|
@.claude/rules/stack-and-commands.md
|
||||||
## Development Commands
|
@.claude/rules/architecture.md
|
||||||
|
@.claude/rules/repo-boundary.md
|
||||||
### Installation & Setup
|
@.claude/rules/credential-routing.md
|
||||||
- Install for development: `pip install -e ".[dev]"`
|
@.claude/rules/agents.md
|
||||||
- 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
|
|
||||||
|
|||||||
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
|
# Universal CLI for issue tracking across multiple backends
|
||||||
|
|
||||||
# Capability metadata
|
# Capability metadata
|
||||||
CAPABILITY_NAME := issue-facade
|
CAPABILITY_NAME := issue-core
|
||||||
CAPABILITY_DESCRIPTION := Universal CLI for issue tracking across multiple backends
|
CAPABILITY_DESCRIPTION := Universal CLI for issue tracking across multiple backends
|
||||||
|
|
||||||
# Default target
|
# Default target
|
||||||
.PHONY: help
|
.PHONY: help
|
||||||
help: ## Show issue facade capability help
|
help: ## Show issue core capability help
|
||||||
@echo "🎯 Issue Facade - Universal Issue Tracking CLI"
|
@echo "🎯 Issue Core - Universal Issue Tracking CLI"
|
||||||
@echo "==============================================="
|
@echo "==============================================="
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Core Issue Operations:"
|
@echo "Core Issue Operations:"
|
||||||
@@ -30,35 +30,49 @@ help: ## Show issue facade capability help
|
|||||||
@echo " issue-sync-push Push local issues to remote"
|
@echo " issue-sync-push Push local issues to remote"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Development & Setup (local):"
|
@echo "Development & Setup (local):"
|
||||||
@echo " install Install issue facade for local development"
|
@echo " install Install issue core for local development"
|
||||||
@echo " install-dev Install with development dependencies"
|
@echo " install-dev Install with development dependencies"
|
||||||
@echo " test Run all tests"
|
@echo " test Run all tests"
|
||||||
@echo " test-unit Run unit tests only"
|
@echo " test-unit Run unit tests only"
|
||||||
@echo " test-integration Run integration tests only"
|
@echo " test-integration Run integration tests only"
|
||||||
@echo " test-cov Run tests with coverage report"
|
@echo " test-cov Run tests with coverage report"
|
||||||
@echo " test-verbose Run tests with verbose output"
|
@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 ""
|
||||||
@echo "Development & Setup (from parent):"
|
@echo "Development & Setup (from parent):"
|
||||||
@echo " issue-facade-install Install issue facade capability"
|
@echo " issue-core-install Install issue core capability"
|
||||||
@echo " issue-facade-install-dev Install with development dependencies"
|
@echo " issue-core-install-dev Install with development dependencies"
|
||||||
@echo " issue-facade-test Run issue facade tests"
|
@echo " issue-core-test Run issue core tests"
|
||||||
@echo " issue-facade-test-cov Run tests with coverage report"
|
@echo " issue-core-test-cov Run tests with coverage report"
|
||||||
@echo " issue-facade-lint Run code quality checks"
|
@echo " issue-core-lint Run code quality checks"
|
||||||
@echo " issue-facade-clean Clean build artifacts"
|
@echo " issue-core-clean Clean build artifacts"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "CLI Functionality:"
|
@echo "CLI Functionality:"
|
||||||
@echo " issue-facade-help Show CLI help documentation"
|
@echo " issue-core-help Show CLI help documentation"
|
||||||
@echo " issue-facade-demo Demonstrate facade functionality"
|
@echo " issue-core-demo Demonstrate facade functionality"
|
||||||
|
|
||||||
# Check if issue command is available
|
# Check if issue command is available
|
||||||
ISSUE_CLI := $(shell command -v issue 2> /dev/null)
|
ISSUE_CLI := $(shell command -v issue 2> /dev/null)
|
||||||
|
|
||||||
|
PYTHON ?= python3
|
||||||
|
GITEA_PACKAGE_OWNER ?= coulomb
|
||||||
|
GITEA_PYPI_REPOSITORY_URL ?= https://gitea.coulomb.social/api/packages/$(GITEA_PACKAGE_OWNER)/pypi
|
||||||
|
|
||||||
# Core Issue Operations
|
# Core Issue Operations
|
||||||
.PHONY: issue-list
|
.PHONY: issue-list
|
||||||
issue-list: ## List all issues from configured backend
|
issue-list: ## List all issues from configured backend
|
||||||
ifndef ISSUE_CLI
|
ifndef ISSUE_CLI
|
||||||
@echo "❌ Issue facade not installed"
|
@echo "❌ Issue facade not installed"
|
||||||
@echo " Install with: make issue-facade-install"
|
@echo " Install with: make issue-core-install"
|
||||||
@exit 1
|
@exit 1
|
||||||
endif
|
endif
|
||||||
issue list
|
issue list
|
||||||
@@ -175,7 +189,7 @@ integrate: ## Integrate capability into main project (interactive)
|
|||||||
@./.capability/integrate.sh
|
@./.capability/integrate.sh
|
||||||
|
|
||||||
.PHONY: install
|
.PHONY: install
|
||||||
install: ## Install issue facade (local development)
|
install: ## Install issue core (local development)
|
||||||
pip install -e .
|
pip install -e .
|
||||||
|
|
||||||
.PHONY: install-dev
|
.PHONY: install-dev
|
||||||
@@ -196,56 +210,103 @@ test-integration: ## Run integration tests only (local development)
|
|||||||
|
|
||||||
.PHONY: test-cov
|
.PHONY: test-cov
|
||||||
test-cov: ## Run tests with coverage report (local development)
|
test-cov: ## Run tests with coverage report (local development)
|
||||||
pytest tests/ --cov=issue_tracker --cov-report=html --cov-report=term
|
pytest tests/ --cov=issue_core --cov-report=html --cov-report=term
|
||||||
|
|
||||||
.PHONY: test-verbose
|
.PHONY: test-verbose
|
||||||
test-verbose: ## Run tests with verbose output (local development)
|
test-verbose: ## Run tests with verbose output (local development)
|
||||||
pytest tests/ -v
|
pytest tests/ -v
|
||||||
|
|
||||||
.PHONY: issue-facade-install
|
.PHONY: clean-dist
|
||||||
issue-facade-install: ## Install issue facade capability
|
clean-dist: ## Remove local Python package build artifacts
|
||||||
pip install -e capabilities/issue-facade/
|
rm -rf build/ dist/ *.egg-info/
|
||||||
|
|
||||||
.PHONY: issue-facade-install-dev
|
.PHONY: package
|
||||||
issue-facade-install-dev: ## Install issue facade capability with development dependencies
|
package: clean-dist ## Build source and wheel distributions
|
||||||
pip install -e "capabilities/issue-facade/[dev]"
|
$(PYTHON) -m build
|
||||||
|
|
||||||
.PHONY: issue-facade-test
|
.PHONY: package-check
|
||||||
issue-facade-test: ## Run issue facade tests
|
package-check: package ## Build and validate source/wheel distributions
|
||||||
cd capabilities/issue-facade && pytest tests/
|
$(PYTHON) -m twine check dist/*
|
||||||
|
|
||||||
.PHONY: issue-facade-test-cov
|
.PHONY: publish-gitea
|
||||||
issue-facade-test-cov: ## Run tests with coverage report
|
publish-gitea: package-check ## Publish distributions to the Coulomb Gitea PyPI registry
|
||||||
cd capabilities/issue-facade && pytest tests/ --cov=issue_tracker --cov-report=html --cov-report=term
|
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
|
# Feedback and Continuous Improvement
|
||||||
issue-facade-lint: ## Run code quality checks
|
.PHONY: feedback
|
||||||
@echo "🔍 Running code quality checks for issue-facade..."
|
feedback: ## Submit feedback (Usage: make feedback MSG="your feedback")
|
||||||
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"
|
@./.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"
|
@echo "✅ Code quality checks completed"
|
||||||
|
|
||||||
.PHONY: issue-facade-clean
|
.PHONY: issue-core-clean
|
||||||
issue-facade-clean: ## Clean build artifacts
|
issue-core-clean: ## Clean build artifacts
|
||||||
cd capabilities/issue-facade && rm -rf build/ dist/ *.egg-info/ __pycache__/ .pytest_cache/ htmlcov/ .coverage
|
cd capabilities/issue-core && rm -rf build/ dist/ *.egg-info/ __pycache__/ .pytest_cache/ htmlcov/ .coverage
|
||||||
find capabilities/issue-facade -name "*.pyc" -delete
|
find capabilities/issue-core -name "*.pyc" -delete
|
||||||
find capabilities/issue-facade -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
|
find capabilities/issue-core -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
|
||||||
# CLI Functionality
|
# CLI Functionality
|
||||||
.PHONY: issue-facade-help
|
.PHONY: issue-core-help
|
||||||
issue-facade-help: ## Show CLI help documentation
|
issue-core-help: ## Show CLI help documentation
|
||||||
ifndef ISSUE_CLI
|
ifndef ISSUE_CLI
|
||||||
@echo "❌ Issue facade not installed"
|
@echo "❌ Issue facade not installed"
|
||||||
@echo " Install with: make issue-facade-install"
|
@echo " Install with: make issue-core-install"
|
||||||
@exit 1
|
@exit 1
|
||||||
endif
|
endif
|
||||||
issue --help
|
issue --help
|
||||||
|
|
||||||
.PHONY: issue-facade-demo
|
.PHONY: issue-core-demo
|
||||||
issue-facade-demo: ## Demonstrate facade functionality
|
issue-core-demo: ## Demonstrate facade functionality
|
||||||
@echo "🎬 Issue Facade Demonstration"
|
@echo "🎬 Issue Core Demonstration"
|
||||||
@echo "============================="
|
@echo "============================="
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "The Issue Facade provides a unified CLI for issue tracking across:"
|
@echo "The Issue Core provides a unified CLI for issue tracking across:"
|
||||||
@echo " • GitHub Issues"
|
@echo " • GitHub Issues"
|
||||||
@echo " • GitLab Issues"
|
@echo " • GitLab Issues"
|
||||||
@echo " • Gitea Issues"
|
@echo " • Gitea Issues"
|
||||||
@@ -258,7 +319,7 @@ issue-facade-demo: ## Demonstrate facade functionality
|
|||||||
@echo ""
|
@echo ""
|
||||||
ifndef ISSUE_CLI
|
ifndef ISSUE_CLI
|
||||||
@echo "To try it out:"
|
@echo "To try it out:"
|
||||||
@echo " 1. make issue-facade-install"
|
@echo " 1. make issue-core-install"
|
||||||
@echo " 2. make issue-backend-detect"
|
@echo " 2. make issue-backend-detect"
|
||||||
@echo " 3. make issue-list"
|
@echo " 3. make issue-list"
|
||||||
else
|
else
|
||||||
@@ -326,4 +387,4 @@ capability-info: ## Show capability information
|
|||||||
@echo "Supported backends: Local SQLite, GitHub, GitLab, Gitea"
|
@echo "Supported backends: Local SQLite, GitHub, GitLab, Gitea"
|
||||||
@echo "Key features: Repository-aware, offline-capable, unified interface"
|
@echo "Key features: Repository-aware, offline-capable, unified interface"
|
||||||
@echo "Targets:"
|
@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.**
|
**A unified interface for autonomous coding agents to coordinate project implementation through issue tracking systems.**
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
The **Issue Facade** provides a standardized abstraction layer for coding agents to interact with issue tracking backends (Gitea, GitHub, GitLab, local SQLite). Instead of each agent implementing platform-specific API integrations, they use one consistent interface that works across all backends.
|
The **Issue Core** provides a standardized abstraction layer for coding agents to interact with issue tracking backends (Gitea, GitHub, GitLab, local SQLite). Instead of each agent implementing platform-specific API integrations, they use one consistent interface that works across all backends.
|
||||||
|
|
||||||
### Why Issue Tracking for Agent Coordination?
|
### Why Issue Tracking for Agent Coordination?
|
||||||
|
|
||||||
@@ -40,10 +40,25 @@ Issue tracking provides natural coordination primitives for multi-agent software
|
|||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd capabilities/issue-facade
|
cd capabilities/issue-core
|
||||||
pip install -e .
|
pip install -e . # CLI only
|
||||||
|
pip install -e ".[api]" # CLI + REST ingestion server
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### REST Ingestion Server
|
||||||
|
|
||||||
|
issue-core exposes `POST /issues/` for upstream emitters (primarily
|
||||||
|
activity-core's `IssueSink`). Launch with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export ISSUE_CORE_API_KEY="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')"
|
||||||
|
issue serve --host 0.0.0.0 --port 8765
|
||||||
|
```
|
||||||
|
|
||||||
|
Clients authenticate with `Authorization: Bearer <key>` or `X-API-Key: <key>`.
|
||||||
|
See `SCOPE.md` "TaskSpec payload" for the request schema, or visit
|
||||||
|
`http://<host>:<port>/docs` once the server is running for live OpenAPI docs.
|
||||||
|
|
||||||
### Configuration (One-Time Setup)
|
### Configuration (One-Time Setup)
|
||||||
|
|
||||||
**For Gitea-backed projects:**
|
**For Gitea-backed projects:**
|
||||||
@@ -67,7 +82,7 @@ issue backend test myproject
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
issue backend add local-work local
|
issue backend add local-work local
|
||||||
# Prompts for: database path (.issue-facade/issues.db)
|
# Prompts for: database path (.issue-core/issues.db)
|
||||||
|
|
||||||
issue backend set-default local-work
|
issue backend set-default local-work
|
||||||
```
|
```
|
||||||
@@ -108,8 +123,8 @@ issue close 42 --comment="Ready for review"
|
|||||||
Quick example:
|
Quick example:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
from issue_tracker.core.interfaces import IssueFilter
|
from issue_core.core.interfaces import IssueFilter
|
||||||
|
|
||||||
# Initialize
|
# Initialize
|
||||||
backend = GiteaBackend()
|
backend = GiteaBackend()
|
||||||
@@ -281,20 +296,20 @@ make test-unit
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run linter
|
# Run linter
|
||||||
make issue-facade-lint
|
make issue-core-lint
|
||||||
|
|
||||||
# Format code
|
# Format code
|
||||||
black issue_tracker/ tests/
|
black issue_core/ tests/
|
||||||
|
|
||||||
# Type check
|
# Type check
|
||||||
mypy issue_tracker/
|
mypy issue_core/
|
||||||
```
|
```
|
||||||
|
|
||||||
### Project Structure
|
### Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
issue-facade/
|
issue-core/
|
||||||
├── issue_tracker/
|
├── issue_core/
|
||||||
│ ├── core/ # Domain models and interfaces
|
│ ├── core/ # Domain models and interfaces
|
||||||
│ │ ├── models.py # Issue, Label, User, etc.
|
│ │ ├── models.py # Issue, Label, User, etc.
|
||||||
│ │ └── interfaces.py # IssueBackend, SyncableBackend
|
│ │ └── interfaces.py # IssueBackend, SyncableBackend
|
||||||
@@ -351,7 +366,7 @@ issue-facade/
|
|||||||
|
|
||||||
## Comparison with Platform CLIs
|
## Comparison with Platform CLIs
|
||||||
|
|
||||||
| Feature | Issue Facade | gh (GitHub) | glab (GitLab) |
|
| Feature | Issue Core | gh (GitHub) | glab (GitLab) |
|
||||||
|---------|--------------|-------------|---------------|
|
|---------|--------------|-------------|---------------|
|
||||||
| Multi-backend support | ✅ Yes | ❌ GitHub only | ❌ GitLab only |
|
| Multi-backend support | ✅ Yes | ❌ GitHub only | ❌ GitLab only |
|
||||||
| Offline capability | ✅ Local SQLite | ❌ No | ❌ No |
|
| Offline capability | ✅ Local SQLite | ❌ No | ❌ No |
|
||||||
@@ -362,7 +377,7 @@ issue-facade/
|
|||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
The Issue Facade is designed to be extensible:
|
The Issue Core is designed to be extensible:
|
||||||
|
|
||||||
**To add a new backend:**
|
**To add a new backend:**
|
||||||
1. Implement the `IssueBackend` interface (see `core/interfaces.py`)
|
1. Implement the `IssueBackend` interface (see `core/interfaces.py`)
|
||||||
|
|||||||
36
ROADMAP.md
36
ROADMAP.md
@@ -1,4 +1,4 @@
|
|||||||
# Issue Facade Roadmap
|
# Issue Core Roadmap
|
||||||
|
|
||||||
**Long-term vision and implementation plan for agent-driven software development coordination.**
|
**Long-term vision and implementation plan for agent-driven software development coordination.**
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
**Implementation:**
|
**Implementation:**
|
||||||
```python
|
```python
|
||||||
# issue_tracker/core/detection.py
|
# issue_core/core/detection.py
|
||||||
|
|
||||||
def detect_git_remote() -> Optional[Dict[str, str]]:
|
def detect_git_remote() -> Optional[Dict[str, str]]:
|
||||||
"""
|
"""
|
||||||
@@ -64,7 +64,7 @@ def parse_remote_url(url: str) -> Optional[Dict[str, str]]:
|
|||||||
|
|
||||||
**Implementation:**
|
**Implementation:**
|
||||||
```python
|
```python
|
||||||
# issue_tracker/core/env_config.py
|
# issue_core/core/env_config.py
|
||||||
|
|
||||||
def load_backend_from_env() -> Optional[Dict[str, Any]]:
|
def load_backend_from_env() -> Optional[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
@@ -95,7 +95,7 @@ issue config auto
|
|||||||
|
|
||||||
**Implementation:**
|
**Implementation:**
|
||||||
```
|
```
|
||||||
.issue-facade/
|
.issue-core/
|
||||||
├── config.json # Repository-specific settings
|
├── config.json # Repository-specific settings
|
||||||
├── issues.db # Local cache/backup
|
├── issues.db # Local cache/backup
|
||||||
└── credentials.json # Optional encrypted credentials
|
└── credentials.json # Optional encrypted credentials
|
||||||
@@ -126,10 +126,10 @@ issue config auto
|
|||||||
**Functions:**
|
**Functions:**
|
||||||
```python
|
```python
|
||||||
def load_repo_config(path: Path = Path.cwd()) -> Optional[Dict]:
|
def load_repo_config(path: Path = Path.cwd()) -> Optional[Dict]:
|
||||||
"""Load .issue-facade/config.json from repo root."""
|
"""Load .issue-core/config.json from repo root."""
|
||||||
|
|
||||||
def save_repo_config(config: Dict, path: Path = Path.cwd()):
|
def save_repo_config(config: Dict, path: Path = Path.cwd()):
|
||||||
"""Save config to .issue-facade/config.json."""
|
"""Save config to .issue-core/config.json."""
|
||||||
|
|
||||||
def find_repo_root() -> Optional[Path]:
|
def find_repo_root() -> Optional[Path]:
|
||||||
"""Walk up directory tree to find git root."""
|
"""Walk up directory tree to find git root."""
|
||||||
@@ -141,14 +141,14 @@ def find_repo_root() -> Optional[Path]:
|
|||||||
|
|
||||||
**Implementation:**
|
**Implementation:**
|
||||||
```python
|
```python
|
||||||
# issue_tracker/core/auto_config.py
|
# issue_core/core/auto_config.py
|
||||||
|
|
||||||
def auto_configure_backend() -> IssueBackend:
|
def auto_configure_backend() -> IssueBackend:
|
||||||
"""
|
"""
|
||||||
Auto-configure backend with fallback priority:
|
Auto-configure backend with fallback priority:
|
||||||
1. Check .issue-facade/config.json
|
1. Check .issue-core/config.json
|
||||||
2. Detect from git remote + environment token
|
2. Detect from git remote + environment token
|
||||||
3. Check global config (~/.config/issue-facade/)
|
3. Check global config (~/.config/issue-core/)
|
||||||
4. Prompt user for manual configuration
|
4. Prompt user for manual configuration
|
||||||
"""
|
"""
|
||||||
```
|
```
|
||||||
@@ -196,7 +196,7 @@ if issue backend show "$backend_name" &>/dev/null; then
|
|||||||
if [ "$replace" = "y" ] || [ "$replace" = "Y" ]; then
|
if [ "$replace" = "y" ] || [ "$replace" = "Y" ]; then
|
||||||
# Create timestamped backup
|
# Create timestamped backup
|
||||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||||
CONFIG_FILE="$HOME/.config/issue-facade/backends.json"
|
CONFIG_FILE="$HOME/.config/issue-core/backends.json"
|
||||||
if [ -f "$CONFIG_FILE" ]; then
|
if [ -f "$CONFIG_FILE" ]; then
|
||||||
BACKUP_FILE="$CONFIG_FILE.backup.$TIMESTAMP"
|
BACKUP_FILE="$CONFIG_FILE.backup.$TIMESTAMP"
|
||||||
cp "$CONFIG_FILE" "$BACKUP_FILE"
|
cp "$CONFIG_FILE" "$BACKUP_FILE"
|
||||||
@@ -255,7 +255,7 @@ fi
|
|||||||
|
|
||||||
**Implementation:**
|
**Implementation:**
|
||||||
```python
|
```python
|
||||||
# issue_tracker/core/agent.py
|
# issue_core/core/agent.py
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AgentContext:
|
class AgentContext:
|
||||||
@@ -269,7 +269,7 @@ def get_agent_context() -> AgentContext:
|
|||||||
"""
|
"""
|
||||||
Get agent context from:
|
Get agent context from:
|
||||||
1. Environment (ISSUE_AGENT_ID, ISSUE_AGENT_TYPE)
|
1. Environment (ISSUE_AGENT_ID, ISSUE_AGENT_TYPE)
|
||||||
2. Config file (.issue-facade/config.json)
|
2. Config file (.issue-core/config.json)
|
||||||
3. Default to system username
|
3. Default to system username
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -310,7 +310,7 @@ issue config agent show
|
|||||||
|
|
||||||
**Implementation:**
|
**Implementation:**
|
||||||
```python
|
```python
|
||||||
# issue_tracker/core/locking.py
|
# issue_core/core/locking.py
|
||||||
|
|
||||||
class IssueClaim:
|
class IssueClaim:
|
||||||
issue_id: str
|
issue_id: str
|
||||||
@@ -423,7 +423,7 @@ def get_agent_state(issue: Issue) -> Dict[str, Any]:
|
|||||||
|
|
||||||
**Implementation:**
|
**Implementation:**
|
||||||
```python
|
```python
|
||||||
# issue_tracker/core/webhooks.py
|
# issue_core/core/webhooks.py
|
||||||
|
|
||||||
class WebhookManager:
|
class WebhookManager:
|
||||||
"""Manage webhooks for real-time notifications."""
|
"""Manage webhooks for real-time notifications."""
|
||||||
@@ -525,7 +525,7 @@ issue depends ready # List issues ready to start
|
|||||||
|
|
||||||
**Implementation:**
|
**Implementation:**
|
||||||
```python
|
```python
|
||||||
# issue_tracker/core/query_dsl.py
|
# issue_core/core/query_dsl.py
|
||||||
|
|
||||||
class QueryParser:
|
class QueryParser:
|
||||||
"""
|
"""
|
||||||
@@ -559,7 +559,7 @@ issue list --query="is:in-progress created:>7d"
|
|||||||
|
|
||||||
**Implementation:**
|
**Implementation:**
|
||||||
```python
|
```python
|
||||||
# issue_tracker/core/activity.py
|
# issue_core/core/activity.py
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ActivityEvent:
|
class ActivityEvent:
|
||||||
@@ -596,7 +596,7 @@ class ActivityStream:
|
|||||||
|
|
||||||
**Implementation:**
|
**Implementation:**
|
||||||
```python
|
```python
|
||||||
# issue_tracker/core/distributed_lock.py
|
# issue_core/core/distributed_lock.py
|
||||||
|
|
||||||
class DistributedLockManager:
|
class DistributedLockManager:
|
||||||
"""Distributed locking using Redis/database."""
|
"""Distributed locking using Redis/database."""
|
||||||
@@ -638,7 +638,7 @@ with distributed_lock(f"issue:{issue_id}", agent_id):
|
|||||||
|
|
||||||
**Implementation:**
|
**Implementation:**
|
||||||
```python
|
```python
|
||||||
# issue_tracker/core/sync_strategies.py
|
# issue_core/core/sync_strategies.py
|
||||||
|
|
||||||
class ConflictResolutionStrategy(ABC):
|
class ConflictResolutionStrategy(ABC):
|
||||||
def resolve(self, local: Issue, remote: Issue) -> Issue:
|
def resolve(self, local: Issue, remote: Issue) -> Issue:
|
||||||
|
|||||||
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
|
# Agent Examples
|
||||||
|
|
||||||
This directory contains working examples of autonomous agents using the Issue Facade for coordination.
|
This directory contains working examples of autonomous agents using the Issue Core for coordination.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
1. **Install issue-facade**:
|
1. **Install issue-core**:
|
||||||
```bash
|
```bash
|
||||||
cd ../..
|
cd ../..
|
||||||
pip install -e .
|
pip install -e .
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ from typing import Optional, List
|
|||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||||
|
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
from issue_tracker.core.models import Issue, Label, User, Comment, IssueState
|
from issue_core.core.models import Issue, Label, User, Comment, IssueState
|
||||||
from issue_tracker.core.interfaces import IssueFilter
|
from issue_core.core.interfaces import IssueFilter
|
||||||
|
|
||||||
|
|
||||||
class HumanInLoopAgent:
|
class HumanInLoopAgent:
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ from typing import List, Dict
|
|||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||||
|
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
from issue_tracker.core.models import Issue, Label, User, Comment, IssueState
|
from issue_core.core.models import Issue, Label, User, Comment, IssueState
|
||||||
from issue_tracker.core.interfaces import IssueFilter
|
from issue_core.core.interfaces import IssueFilter
|
||||||
|
|
||||||
|
|
||||||
class MonitoringAgent:
|
class MonitoringAgent:
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ from typing import List
|
|||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||||
|
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
from issue_tracker.core.models import Issue, Label, User, Comment, IssueState
|
from issue_core.core.models import Issue, Label, User, Comment, IssueState
|
||||||
from issue_tracker.core.interfaces import IssueFilter
|
from issue_core.core.interfaces import IssueFilter
|
||||||
|
|
||||||
|
|
||||||
class BaseAgent:
|
class BaseAgent:
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ from pathlib import Path
|
|||||||
# Add parent directory to path for imports
|
# Add parent directory to path for imports
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||||
|
|
||||||
from issue_tracker.backends.gitea import GiteaBackend
|
from issue_core.backends.gitea import GiteaBackend
|
||||||
from issue_tracker.core.models import Issue, Label, User, Comment, IssueState
|
from issue_core.core.models import Issue, Label, User, Comment, IssueState
|
||||||
from issue_tracker.core.interfaces import IssueFilter
|
from issue_core.core.interfaces import IssueFilter
|
||||||
|
|
||||||
|
|
||||||
class SimpleTaskExecutor:
|
class SimpleTaskExecutor:
|
||||||
|
|||||||
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:
|
if issue.milestone:
|
||||||
data['milestone'] = int(issue.milestone.backend_id) if issue.milestone.backend_id else None
|
data['milestone'] = int(issue.milestone.backend_id) if issue.milestone.backend_id else None
|
||||||
|
|
||||||
# Convert labels
|
# Gitea expects numeric label IDs on issue create/update. Name-only
|
||||||
if issue.labels:
|
# labels are preserved in issue-core metadata but omitted from the API
|
||||||
data['labels'] = [label.name for label in issue.labels]
|
# 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
|
return data
|
||||||
|
|
||||||
# Issue CRUD Operations
|
# 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 .commands import issue_group
|
||||||
from .backend_commands import backend_group
|
from .backend_commands import backend_group
|
||||||
from .sync_commands import sync_group
|
from .sync_commands import sync_group
|
||||||
|
from .serve_command import serve_command
|
||||||
from .. import __version__
|
from .. import __version__
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
@click.version_option(version=__version__, package_name='issue-tracker')
|
@click.version_option(version=__version__, package_name='issue-core')
|
||||||
@click.option('--config', type=click.Path(), help='Configuration file path')
|
@click.option('--config', type=click.Path(), help='Configuration file path')
|
||||||
@click.option('--backend', help='Backend to use (local, gitea)')
|
@click.option('--backend', help='Backend to use (local, gitea)')
|
||||||
@click.option('--verbose', '-v', is_flag=True, help='Verbose output')
|
@click.option('--verbose', '-v', is_flag=True, help='Verbose output')
|
||||||
@@ -52,6 +53,7 @@ def cli(ctx, config, backend, verbose):
|
|||||||
cli.add_command(issue_group, name='issue')
|
cli.add_command(issue_group, name='issue')
|
||||||
cli.add_command(backend_group, name='backend')
|
cli.add_command(backend_group, name='backend')
|
||||||
cli.add_command(sync_group, name='sync')
|
cli.add_command(sync_group, name='sync')
|
||||||
|
cli.add_command(serve_command)
|
||||||
|
|
||||||
|
|
||||||
# Convenience aliases - direct issue commands
|
# Convenience aliases - direct issue commands
|
||||||
43
issue_core/cli/serve_command.py
Normal file
43
issue_core/cli/serve_command.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
"""
|
||||||
|
`issue serve` — launch the issue-core REST API.
|
||||||
|
|
||||||
|
Requires the [api] extra: `pip install issue-core[api]`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
|
||||||
|
@click.command("serve")
|
||||||
|
@click.option("--host", default="127.0.0.1", show_default=True, help="Bind address.")
|
||||||
|
@click.option("--port", default=8765, show_default=True, type=int, help="Bind port.")
|
||||||
|
@click.option("--reload", is_flag=True, default=False, help="Auto-reload on code change (dev only).")
|
||||||
|
@click.option("--log-level", default="info", show_default=True, help="Uvicorn log level.")
|
||||||
|
def serve_command(host: str, port: int, reload: bool, log_level: str) -> None:
|
||||||
|
"""Launch the issue-core REST API (POST /issues/ ingestion endpoint).
|
||||||
|
|
||||||
|
Requires ISSUE_CORE_API_KEY to be set in the environment.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import uvicorn # noqa: F401
|
||||||
|
except ImportError:
|
||||||
|
raise click.ClickException(
|
||||||
|
"The 'api' extra is not installed. Run: pip install 'issue-core[api]'"
|
||||||
|
)
|
||||||
|
|
||||||
|
from ..api.auth import AuthConfigError, get_configured_api_key
|
||||||
|
|
||||||
|
try:
|
||||||
|
get_configured_api_key()
|
||||||
|
except AuthConfigError as exc:
|
||||||
|
raise click.ClickException(str(exc))
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
uvicorn.run(
|
||||||
|
"issue_core.api.app:create_app",
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
reload=reload,
|
||||||
|
log_level=log_level,
|
||||||
|
factory=True,
|
||||||
|
)
|
||||||
@@ -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]
|
[build-system]
|
||||||
requires = ["setuptools>=45", "wheel", "setuptools-scm[toml]>=6.2"]
|
requires = ["setuptools>=61,<77", "wheel"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "universal-issue-tracker"
|
name = "issue-core"
|
||||||
description = "Backend-agnostic issue tracking system with plugin architecture"
|
description = "Authoritative task lifecycle manager for the Coulomb org — backend-agnostic with plugin architecture"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
license = {text = "MIT"}
|
license = {text = "MIT"}
|
||||||
@@ -37,15 +37,20 @@ dynamic = ["version"]
|
|||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
|
"build>=1.3",
|
||||||
"pytest>=6.0",
|
"pytest>=6.0",
|
||||||
"pytest-cov>=2.0",
|
"pytest-cov>=2.0",
|
||||||
"pytest-mock>=3.0",
|
"pytest-mock>=3.0",
|
||||||
|
"twine>=6.0",
|
||||||
"black>=22.0",
|
"black>=22.0",
|
||||||
"isort>=5.0",
|
"isort>=5.0",
|
||||||
"flake8>=4.0",
|
"flake8>=4.0",
|
||||||
"mypy>=0.900",
|
"mypy>=0.900",
|
||||||
"pre-commit>=2.0",
|
"pre-commit>=2.0",
|
||||||
]
|
"httpx>=0.27",
|
||||||
|
"fastapi>=0.110,<1.0",
|
||||||
|
"pydantic>=2.0,<3.0",
|
||||||
|
]
|
||||||
docs = [
|
docs = [
|
||||||
"sphinx>=4.0",
|
"sphinx>=4.0",
|
||||||
"sphinx-rtd-theme>=1.0",
|
"sphinx-rtd-theme>=1.0",
|
||||||
@@ -60,25 +65,31 @@ github = [
|
|||||||
jira = [
|
jira = [
|
||||||
"jira>=3.0",
|
"jira>=3.0",
|
||||||
]
|
]
|
||||||
|
api = [
|
||||||
|
"fastapi>=0.110,<1.0",
|
||||||
|
"uvicorn[standard]>=0.27,<1.0",
|
||||||
|
"pydantic>=2.0,<3.0",
|
||||||
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "https://github.com/markitect/universal-issue-tracker"
|
Homepage = "https://github.com/coulomb/issue-core"
|
||||||
Documentation = "https://universal-issue-tracker.readthedocs.io/"
|
Documentation = "https://issue-core.readthedocs.io/"
|
||||||
Repository = "https://github.com/markitect/universal-issue-tracker.git"
|
Repository = "https://github.com/coulomb/issue-core.git"
|
||||||
"Bug Tracker" = "https://github.com/markitect/universal-issue-tracker/issues"
|
"Bug Tracker" = "https://github.com/coulomb/issue-core/issues"
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
issue = "issue_tracker.cli.main:main"
|
issue = "issue_core.cli.main:main"
|
||||||
issue-tracker = "issue_tracker.cli.main:main"
|
issue-core = "issue_core.cli.main:main"
|
||||||
|
issue-tracker = "issue_core.cli.legacy:issue_tracker_hint"
|
||||||
|
|
||||||
[tool.setuptools]
|
[tool.setuptools.packages.find]
|
||||||
packages = ["issue_tracker"]
|
include = ["issue_core*"]
|
||||||
|
|
||||||
[tool.setuptools.dynamic]
|
[tool.setuptools.dynamic]
|
||||||
version = {attr = "issue_tracker.__version__"}
|
version = {attr = "issue_core.__version__"}
|
||||||
|
|
||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
issue_tracker = ["backends/local/schema.sql"]
|
issue_core = ["backends/local/schema.sql"]
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
line-length = 100
|
line-length = 100
|
||||||
@@ -101,7 +112,7 @@ extend-exclude = '''
|
|||||||
[tool.isort]
|
[tool.isort]
|
||||||
profile = "black"
|
profile = "black"
|
||||||
line_length = 100
|
line_length = 100
|
||||||
known_first_party = ["issue_tracker"]
|
known_first_party = ["issue_core"]
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
python_version = "3.8"
|
python_version = "3.8"
|
||||||
@@ -142,7 +153,7 @@ markers = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[tool.coverage.run]
|
[tool.coverage.run]
|
||||||
source = ["issue_tracker"]
|
source = ["issue_core"]
|
||||||
omit = [
|
omit = [
|
||||||
"*/tests/*",
|
"*/tests/*",
|
||||||
"*/test_*",
|
"*/test_*",
|
||||||
@@ -161,4 +172,4 @@ exclude_lines = [
|
|||||||
"if __name__ == .__main__.:",
|
"if __name__ == .__main__.:",
|
||||||
"class .*\\bProtocol\\):",
|
"class .*\\bProtocol\\):",
|
||||||
"@(abc\\.)?abstractmethod",
|
"@(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 click.testing import CliRunner
|
||||||
from unittest.mock import Mock, patch, MagicMock
|
from unittest.mock import Mock, patch, MagicMock
|
||||||
|
|
||||||
from issue_tracker.cli.main import cli
|
from issue_core.cli.main import cli
|
||||||
from issue_tracker.cli.utils import load_backend_configs, save_backend_configs
|
from issue_core.cli.utils import load_backend_configs, save_backend_configs
|
||||||
|
|
||||||
|
|
||||||
class TestCLICommands:
|
class TestCLICommands:
|
||||||
@@ -37,9 +37,9 @@ class TestCLICommands:
|
|||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
# Should show either configured backends or "No backends configured"
|
# Should show either configured backends or "No backends configured"
|
||||||
|
|
||||||
@patch('issue_tracker.cli.backend_commands.load_backend_configs')
|
@patch('issue_core.cli.backend_commands.load_backend_configs')
|
||||||
@patch('issue_tracker.cli.backend_commands.save_backend_configs')
|
@patch('issue_core.cli.backend_commands.save_backend_configs')
|
||||||
@patch('issue_tracker.cli.backend_commands.test_backend_connection')
|
@patch('issue_core.cli.backend_commands.test_backend_connection')
|
||||||
def test_backend_add_gitea_with_env_token(self, mock_test_conn, mock_save, mock_load):
|
def test_backend_add_gitea_with_env_token(self, mock_test_conn, mock_save, mock_load):
|
||||||
"""Test adding Gitea backend with environment token."""
|
"""Test adding Gitea backend with environment token."""
|
||||||
# Mock empty initial config
|
# Mock empty initial config
|
||||||
@@ -63,9 +63,9 @@ class TestCLICommands:
|
|||||||
assert saved_config['test-gitea']['type'] == 'gitea'
|
assert saved_config['test-gitea']['type'] == 'gitea'
|
||||||
assert saved_config['test-gitea']['token'] == 'test-token'
|
assert saved_config['test-gitea']['token'] == 'test-token'
|
||||||
|
|
||||||
@patch('issue_tracker.cli.backend_commands.load_backend_configs')
|
@patch('issue_core.cli.backend_commands.load_backend_configs')
|
||||||
@patch('issue_tracker.cli.backend_commands.save_backend_configs')
|
@patch('issue_core.cli.backend_commands.save_backend_configs')
|
||||||
@patch('issue_tracker.cli.backend_commands.test_backend_connection')
|
@patch('issue_core.cli.backend_commands.test_backend_connection')
|
||||||
def test_backend_add_local(self, mock_test_conn, mock_save, mock_load):
|
def test_backend_add_local(self, mock_test_conn, mock_save, mock_load):
|
||||||
"""Test adding local backend."""
|
"""Test adding local backend."""
|
||||||
mock_load.return_value = {}
|
mock_load.return_value = {}
|
||||||
@@ -78,7 +78,7 @@ class TestCLICommands:
|
|||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert 'Backend \'test-local\' added successfully' in result.output
|
assert 'Backend \'test-local\' added successfully' in result.output
|
||||||
|
|
||||||
@patch('issue_tracker.cli.commands.get_backend')
|
@patch('issue_core.cli.commands.get_backend')
|
||||||
def test_show_command(self, mock_get_backend):
|
def test_show_command(self, mock_get_backend):
|
||||||
"""Test issue show command."""
|
"""Test issue show command."""
|
||||||
# Mock backend and issue
|
# Mock backend and issue
|
||||||
@@ -104,7 +104,7 @@ class TestCLICommands:
|
|||||||
assert 'Test description' in result.output
|
assert 'Test description' in result.output
|
||||||
assert 'State: open' in result.output
|
assert 'State: open' in result.output
|
||||||
|
|
||||||
@patch('issue_tracker.cli.utils.get_backend')
|
@patch('issue_core.cli.utils.get_backend')
|
||||||
def test_show_command_issue_not_found(self, mock_get_backend):
|
def test_show_command_issue_not_found(self, mock_get_backend):
|
||||||
"""Test issue show command when issue doesn't exist."""
|
"""Test issue show command when issue doesn't exist."""
|
||||||
mock_backend = Mock()
|
mock_backend = Mock()
|
||||||
@@ -121,7 +121,7 @@ class TestCLICommands:
|
|||||||
result = self.runner.invoke(cli, ['--version'])
|
result = self.runner.invoke(cli, ['--version'])
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
|
|
||||||
@patch('issue_tracker.cli.utils.get_backend')
|
@patch('issue_core.cli.utils.get_backend')
|
||||||
def test_list_command_basic(self, mock_get_backend):
|
def test_list_command_basic(self, mock_get_backend):
|
||||||
"""Test basic list command functionality."""
|
"""Test basic list command functionality."""
|
||||||
# This test will help us identify the existing bug
|
# This test will help us identify the existing bug
|
||||||
@@ -157,7 +157,7 @@ class TestBackendConfiguration:
|
|||||||
|
|
||||||
def test_config_directory_creation(self):
|
def test_config_directory_creation(self):
|
||||||
"""Test configuration directory is created properly."""
|
"""Test configuration directory is created properly."""
|
||||||
from issue_tracker.cli.utils import get_config_dir
|
from issue_core.cli.utils import get_config_dir
|
||||||
|
|
||||||
config_dir = get_config_dir()
|
config_dir = get_config_dir()
|
||||||
assert config_dir.exists()
|
assert config_dir.exists()
|
||||||
@@ -177,7 +177,7 @@ class TestBackendConfiguration:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Test saving
|
# Test saving
|
||||||
with patch('issue_tracker.cli.utils.get_backend_config_path', return_value=config_file):
|
with patch('issue_core.cli.utils.get_backend_config_path', return_value=config_file):
|
||||||
save_backend_configs(test_config)
|
save_backend_configs(test_config)
|
||||||
|
|
||||||
# Test loading
|
# Test loading
|
||||||
@@ -190,7 +190,7 @@ class TestBackendConfiguration:
|
|||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
non_existent_file = Path(temp_dir) / 'nonexistent.json'
|
non_existent_file = Path(temp_dir) / 'nonexistent.json'
|
||||||
|
|
||||||
with patch('issue_tracker.cli.utils.get_backend_config_path', return_value=non_existent_file):
|
with patch('issue_core.cli.utils.get_backend_config_path', return_value=non_existent_file):
|
||||||
config = load_backend_configs()
|
config = load_backend_configs()
|
||||||
|
|
||||||
assert config == {}
|
assert config == {}
|
||||||
@@ -204,13 +204,13 @@ class TestEnvironmentTokenDetection:
|
|||||||
"""Test GITEA_API_TOKEN environment variable detection."""
|
"""Test GITEA_API_TOKEN environment variable detection."""
|
||||||
mock_getenv.return_value = 'test-env-token'
|
mock_getenv.return_value = 'test-env-token'
|
||||||
|
|
||||||
from issue_tracker.cli.backend_commands import add_backend
|
from issue_core.cli.backend_commands import add_backend
|
||||||
|
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
|
|
||||||
with patch('issue_tracker.cli.backend_commands.load_backend_configs', return_value={}):
|
with patch('issue_core.cli.backend_commands.load_backend_configs', return_value={}):
|
||||||
with patch('issue_tracker.cli.backend_commands.save_backend_configs'):
|
with patch('issue_core.cli.backend_commands.save_backend_configs'):
|
||||||
with patch('issue_tracker.cli.backend_commands.test_backend_connection', return_value=True):
|
with patch('issue_core.cli.backend_commands.test_backend_connection', return_value=True):
|
||||||
result = runner.invoke(add_backend, [
|
result = runner.invoke(add_backend, [
|
||||||
'test-gitea', 'gitea'
|
'test-gitea', 'gitea'
|
||||||
], input='https://git.example.com\ntestorg\ntestrepo\n')
|
], input='https://git.example.com\ntestorg\ntestrepo\n')
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ including state management, validation, and business logic.
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from issue_tracker.core.models import (
|
from issue_core.core.models import (
|
||||||
Issue, Label, User, Milestone, Comment,
|
Issue, Label, User, Milestone, Comment,
|
||||||
IssueState, Priority, IssueType, LabelCategories
|
IssueState, Priority, IssueType, LabelCategories
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ These tests ensure the Gitea backend works correctly with the API.
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import json
|
import json
|
||||||
|
from datetime import datetime, timezone
|
||||||
from unittest.mock import Mock, patch, MagicMock
|
from unittest.mock import Mock, patch, MagicMock
|
||||||
from issue_tracker.backends.gitea.backend import GiteaBackend, GiteaAPIError
|
from issue_core.backends.gitea.backend import GiteaBackend, GiteaAPIError
|
||||||
|
from issue_core.core.models import Issue, IssueState, Label
|
||||||
|
|
||||||
|
|
||||||
class TestGiteaBackend:
|
class TestGiteaBackend:
|
||||||
@@ -32,7 +34,7 @@ class TestGiteaBackend:
|
|||||||
assert self.backend.repo is None
|
assert self.backend.repo is None
|
||||||
assert self.backend.session is not None
|
assert self.backend.session is not None
|
||||||
|
|
||||||
@patch('issue_tracker.backends.gitea.backend.requests.Session')
|
@patch('issue_core.backends.gitea.backend.requests.Session')
|
||||||
def test_connect_success(self, mock_session_class):
|
def test_connect_success(self, mock_session_class):
|
||||||
"""Test successful connection to Gitea API."""
|
"""Test successful connection to Gitea API."""
|
||||||
mock_session = MagicMock()
|
mock_session = MagicMock()
|
||||||
@@ -61,7 +63,7 @@ class TestGiteaBackend:
|
|||||||
'Accept': 'application/json'
|
'Accept': 'application/json'
|
||||||
})
|
})
|
||||||
|
|
||||||
@patch('issue_tracker.backends.gitea.backend.requests.Session')
|
@patch('issue_core.backends.gitea.backend.requests.Session')
|
||||||
def test_connect_failure(self, mock_session_class):
|
def test_connect_failure(self, mock_session_class):
|
||||||
"""Test failed connection raises appropriate error."""
|
"""Test failed connection raises appropriate error."""
|
||||||
mock_session = MagicMock()
|
mock_session = MagicMock()
|
||||||
@@ -96,7 +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]
|
called_url = mock_request.call_args[1]['url'] if 'url' in mock_request.call_args[1] else mock_request.call_args[0][1]
|
||||||
assert called_url == 'https://git.example.com/api/v1/repos/owner/repo'
|
assert called_url == 'https://git.example.com/api/v1/repos/owner/repo'
|
||||||
|
|
||||||
@patch('issue_tracker.backends.gitea.backend.requests.Session')
|
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):
|
def test_test_connection_success(self, mock_session_class):
|
||||||
"""Test test_connection method works correctly."""
|
"""Test test_connection method works correctly."""
|
||||||
mock_session = MagicMock()
|
mock_session = MagicMock()
|
||||||
@@ -117,7 +142,7 @@ class TestGiteaBackend:
|
|||||||
result = backend.test_connection()
|
result = backend.test_connection()
|
||||||
assert result is True
|
assert result is True
|
||||||
|
|
||||||
@patch('issue_tracker.backends.gitea.backend.requests.Session')
|
@patch('issue_core.backends.gitea.backend.requests.Session')
|
||||||
def test_test_connection_failure(self, mock_session_class):
|
def test_test_connection_failure(self, mock_session_class):
|
||||||
"""Test test_connection handles failures gracefully."""
|
"""Test test_connection handles failures gracefully."""
|
||||||
mock_session = MagicMock()
|
mock_session = MagicMock()
|
||||||
@@ -139,7 +164,7 @@ class TestGiteaBackend:
|
|||||||
result = backend.test_connection()
|
result = backend.test_connection()
|
||||||
assert result is False
|
assert result is False
|
||||||
|
|
||||||
@patch('issue_tracker.backends.gitea.backend.requests.Session')
|
@patch('issue_core.backends.gitea.backend.requests.Session')
|
||||||
def test_get_issue_success(self, mock_session_class):
|
def test_get_issue_success(self, mock_session_class):
|
||||||
"""Test successful issue retrieval."""
|
"""Test successful issue retrieval."""
|
||||||
mock_session = MagicMock()
|
mock_session = MagicMock()
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import tempfile
|
|||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from issue_tracker.backends.local.backend import LocalSQLiteBackend
|
from issue_core.backends.local.backend import LocalSQLiteBackend
|
||||||
from issue_tracker.core.models import Issue, Label, User, Milestone, Comment, IssueState
|
from issue_core.core.models import Issue, Label, User, Milestone, Comment, IssueState
|
||||||
from issue_tracker.core.interfaces import IssueFilter
|
from issue_core.core.interfaces import IssueFilter
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
|
|||||||
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