Compare commits

...

36 Commits

Author SHA1 Message Date
54a9749616 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-07-02:
  - update .custodian-brief.md for issue-core
2026-07-02 14:50:19 +02:00
a691f93f16 ISSUE-WP-0003 finished: live REST emission proven (Gitea issue 176), topology corrected
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 14:49:57 +02:00
8d34c6d468 docs: update issue-core deployment closeout 2026-07-01 20:04:37 +02:00
e5172611ec chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-07-01:
  - update .custodian-brief.md for issue-core
2026-07-01 19:39:41 +02:00
9e46004961 Update ISSUE-WP-0003 deployment progress 2026-06-25 19:59:53 +02:00
11a0a69870 Deploy issue-core 0.2.1 image 2026-06-25 19:47:58 +02:00
3c66148205 Fix Gitea issue label payloads
Some checks failed
Publish Python package / publish (push) Has been cancelled
2026-06-25 19:40:15 +02:00
2f40dea6a1 Fix railiance issue-core GitOps runtime config 2026-06-25 19:23:34 +02:00
8c01f07c2d feat(integration): add open-reuse Integration Definition for Gitea backend
Register the Gitea API adapter boundary for issue-core maintenance tracking.
2026-06-24 18:25:13 +02:00
7693ef8680 Accept non-UUID triggering_event_id and add GitOps runbook
Broaden POST /issues/ so triggering_event_id is any non-empty traceability
string, enabling cron/scheduled activity-core emissions with stable keys like
"scheduled" while event-driven paths still send UUIDs. Document the railiance01
ArgoCD deployment path in docs/argocd-gitops.md and update ISSUE-WP-0003 task
status to reflect repo-side progress.
2026-06-24 14:52:47 +02:00
4854bda118 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-23:
  - update .custodian-brief.md for issue-core
2026-06-23 14:26:56 +02:00
19d4bc96df Normalize agent instructions and workplan frontmatter (STATE-WP-0067)
- Align agent files with on-disk workplan prefixes (infer from workplan ids)
- Set workplan domain to registered domain_slug; add topic_slug where applicable
- Repair frontmatter delimiter formatting; migrate legacy task status literals
- Regenerate AGENTS.md, CLAUDE.md, and .claude/rules from State Hub templates
2026-06-22 23:16:25 +02:00
d50ab96a8a Mark .repo-classification.yaml human-reviewed (CUST-WP-0050 T02)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 11:40:43 +02:00
3a60623730 Reclassify as tooling (CUST-WP-0050 T02)
Apply the new 'tooling' category (reusable internal tooling/infrastructure)
from the Repo Classification Standard. First-pass agent classification.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 03:06:01 +02:00
23a233c57e Add repo classification (CUST-WP-0050 T02)
First-pass agent classification per the Repo Classification Standard v1.0
(canon-repo-classification); pending human review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 02:44:46 +02:00
358464b4d4 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-19:
  - update .custodian-brief.md for issue-core
2026-06-19 21:09:30 +02:00
12b356d94a Complete ISSUE-WP-0003-T01: push container image to Gitea registry
Mark T01 done after pushing gitea.coulomb.social/coulomb/issue-core:0.2.0.
Update workplan state and note platform bootstrap dependency for T02/T04.
2026-06-19 21:09:18 +02:00
bed83be0ec chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-19:
  - update .custodian-brief.md for issue-core
2026-06-19 21:07:30 +02:00
3e29bc964d Add railiance01 deployment artifacts and fix container image build
Introduce Dockerfile, entrypoint, and k8s/railiance manifests for the
ArgoCD GitOps pilot (ISSUE-WP-0003). Rename the Gitea PyPI build arg to
GITEA_PYPI_INDEX_URL so pip still resolves dependencies from PyPI.
2026-06-19 21:05:18 +02:00
352a4d7969 Add credential routing instructions for all agent runtimes
Propagate shared credential-routing section (Codex, Claude, Grok, llm-connect)
from state-hub template via scripts/propagate_credential_routing.py.
2026-06-18 22:48:38 +02:00
026982ca4f Document REST ingestion API key pairing in AGENTS.md
Clarify ISSUE_CORE_API_KEY startup requirement, local dev flow, and that
agents must not request the key from ops-warden.
2026-06-18 22:34:59 +02:00
89c1c8cc1e Add capability registry scaffold (REUSE-WP-0014-T05 B03) 2026-06-16 01:53:53 +02:00
4635b0eb36 Close Gitea PyPI publication workplan 2026-06-05 20:42:32 +02:00
d703463f16 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-05:
  - update .custodian-brief.md for issue-core
2026-06-05 20:37:28 +02:00
041afc6311 pypi publication complicatoins 2026-05-23 06:45:36 +02:00
ed14952b17 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-05-23:
  - update .custodian-brief.md for issue-core
2026-05-23 05:20:18 +02:00
2fd7de08d2 Refresh agent instruction files 2026-05-18 16:55:43 +02:00
b20eb11583 feat: restore issue-tracker console script as migration hint
Callers using the pre-rename `issue-tracker` command now get a clear
stderr message pointing them at `issue-core` (or the short `issue`
alias) and exit code 2, instead of `command not found`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 05:27:34 +02:00
f96ca46965 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-05-17:
  - update .custodian-brief.md for issue-core
2026-05-17 05:22:17 +02:00
b605d970e3 feat: rename to issue-core and add task ingestion endpoint
Renames the package, distribution, CLI alias, Makefile targets, and
working directory from issue-facade to issue-core, signalling its
role as the authoritative task lifecycle manager for the Coulomb org
(peer to activity-core, rules-core, project-core).

Adds POST /issues/ ingestion endpoint for activity-core's IssueSink,
under a new optional [api] extra. The endpoint is served by `issue
serve`, authenticates via the ISSUE_CORE_API_KEY env var (Bearer or
X-API-Key header), and routes the TaskSpec payload to the configured
default backend with full traceability metadata embedded in
sync_metadata.

- T01: Python package issue_tracker -> issue_core, dir rename
- T02: registered in state hub under custodian domain
- T03: INTENT.md (what it is, what it isn't, how it fits)
- T04: SCOPE.md (in/out-of-scope, integration boundaries)
- T05: POST /issues/ via FastAPI + Uvicorn, 9 unit tests
- T06: docs/nats-task-ingestion.md design stub

Closes ISSC-WP-0001.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 05:16:27 +02:00
99ea1fbc45 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-05-17:
  - update .custodian-brief.md for issue-core
2026-05-17 05:06:10 +02:00
663d1961cf chore(workplan): stub ISSC-WP-0001 rename to issue-core and task ingestion
Workplan for renaming issue-facade → issue-core and implementing the
POST /issues/ task ingestion endpoint required by activity-core's IssueSink
adapter. Covers rename, state hub registration, INTENT.md, SCOPE.md, REST
endpoint, and NATS design stub.

Hub workstream: 1135fc1d-1f46-4e35-886d-04cc3b8050b6

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 18:12:38 +02:00
70d7ec0cdc docs: add CHANGELOG.md documenting v1.0.0 release
Add comprehensive CHANGELOG following Keep a Changelog format to document
the architecture refactoring and feedback capability implementation.

Documented in v1.0.0:
- ReusableCapabilitiesArchitecture specification
- Feedback capability with visible directory structure
- Detachment facility for capability removal
- Enhanced documentation (CLAUDE.md, README.md, examples)
- Directory visibility changes (.feedback → feedback)
- Explicit family declaration (CAPABILITY-issue-tracking.yaml)

Also includes version history from v0.1.0 through v0.9.0 and
upgrade notes for migrating to v1.0.0.

Closes the documentation gap referenced in TODO.md.
2025-12-17 22:54:27 +01:00
f89772ac79 fix: update remaining .feedback references to feedback
Update directory structure diagrams and copy examples to use
the new visible feedback/ directory instead of hidden .feedback/

This ensures all documentation is consistent with the
ReusableCapabilitiesArchitecture v0.1 specification.
2025-12-17 22:40:06 +01:00
35daa514e5 refactor: align with ReusableCapabilitiesArchitecture v0.1
Refactor issue-facade to conform to the new ReusableCapabilitiesArchitecture
specification, improving discoverability and establishing consistent patterns
for capability integration.

Architecture Changes:
- Rename .feedback/ → feedback/ (visible user interface)
- Rename CAPABILITY.yaml → CAPABILITY-issue-tracking.yaml (explicit family)
- Keep .capability/ hidden (evolving implementation infrastructure)

File Updates:
- Updated all documentation references (.feedback → feedback)
- Updated .capability/feedback script paths
- Updated Makefile, README.md, CLAUDE.md, examples
- Fixed CAPABILITY.yaml → CAPABILITY-issue-tracking.yaml references

New Tools:
- Created .capability/detach script for clean capability removal
- Supports git submodule and directory-based integrations
- Generates detachment manifest for re-integration guidance

Rationale:
- feedback/ is visible: encourages user participation, shows capability identity
- .capability/ is hidden: implementation details that will evolve
- CAPABILITY-<family>.yaml: explicit family declaration, supports multiple capabilities per repo
- Underscore prefix pattern: flatter hierarchy, clear signal of integration

This aligns with the principle that capabilities are conceptual units
designed for natural language integration by devhumans and devagents,
not just technical libraries.

See ReusableCapabilitiesArchitecture.md for complete specification.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-17 22:22:47 +01:00
1627fd9673 feat: implement reusable feedback capability for continuous improvement
Add comprehensive feedback system that enables lightweight, unstructured feedback
collection from users of the issue-facade capability, establishing a continuous
improvement loop grounded in real-world usage.

Core Components:
- .feedback/ directory structure (inbound, reviewed, archived)
- Standalone CLI tool (.capability/feedback) for submission and management
- Comprehensive documentation (.feedback/README.md)
- Integration examples and usage guides

Key Features:
- Multiple submission methods (CLI, Makefile, direct file drop)
- No structure imposement - accepts any text/markdown format
- Automatic metadata capture (timestamp, git context, version)
- Maintainer workflow (list, review, archive, create issues)
- Colored terminal output for better UX
- Future-ready for API endpoint evolution

Integration:
- Updated CAPABILITY.yaml with feedback section
- Enhanced CLAUDE.md with comprehensive integration guide
- Added Makefile commands (feedback, feedback-list, feedback-stats, etc.)
- Created detailed usage examples (examples/feedback-example.md)

Design Philosophy:
- Capability-agnostic pattern (reusable across all markitect capabilities)
- Decentralized (each capability owns its feedback)
- Flexible (no required formats or fields)
- Durable (plain markdown files, git-tracked)
- Actionable (feedback lives where maintainers work)
- Scalable (works for 1 user or 1000 users)

Feedback Submission Examples:
  ./.capability/feedback submit "Your feedback"
  make feedback MSG="Your feedback"
  echo "Feedback" > .feedback/inbound/$(date +%Y%m%d)-feedback.md

Maintainer Workflow:
  make feedback-list                    # List pending
  make feedback-stats                   # Show statistics
  make feedback-review-issue FILE=xxx   # Review and create issue

This establishes a robust continuous improvement loop:
User Experience → Feedback → Review → Action → Improved Capability

The pattern is designed to be copied to any capability in the markitect
project, providing consistent feedback collection across all capabilities.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-17 21:09:36 +01:00
84 changed files with 5841 additions and 525 deletions

View File

@@ -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)

View File

@@ -1,4 +1,4 @@
# Issue Facade - Agent Integration Context # Issue Core - Agent Integration Context
**🤖 For Coding Agents: Read this to understand how to use issue tracking in this project.** **🤖 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
View 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
View 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 "$@"

View File

@@ -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)

View File

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

View 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

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

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

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

View 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

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

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

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

View 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
View 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
View 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/`)

View File

@@ -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)

View File

@@ -1,8 +1,8 @@
# Issue Facade Capability Manifest # Issue Core Capability Manifest
# This file describes the capability to coding agents and integration systems # 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
View 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
View File

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

@@ -0,0 +1,116 @@
# INTENT — issue-core
## Why it exists
The Coulomb org needs a **single, observable place where tasks land** — regardless
of whether they were created by a human typing a CLI command, by an automation
like activity-core acting on a rule, or by an agent acting on instructions.
Without a single landing zone, task creation fragments across:
- Per-repo Gitea issue trackers (siloed, no cross-repo view)
- Ad hoc files and TODO comments (invisible, unaudited)
- Agent-local memory and notebooks (lost when the agent ends)
- External SaaS trackers (rate-limited, off-network)
issue-core gives every actor — human or machine — one stable, observable place
to file work, and one stable surface to consume work from.
## What it is
A **task lifecycle manager** with a pluggable-backend architecture.
Responsibilities:
- **Ingestion**: accept new tasks via CLI, REST (`POST /issues/`), and — in the
future — NATS subscriptions.
- **Storage**: route each task to the configured backend (Gitea, SQLite, GitHub).
- **Lifecycle**: create → assign → update → close, with state transitions that
hold regardless of backend.
- **Querying**: list, search, filter across the active backend.
- **Synchronization**: bidirectional sync between local SQLite (source of truth
for offline work) and remote backends.
Backends today: **local SQLite**, **Gitea**. Planned: **GitHub**, **GitLab**, **JIRA**.
The CLI entry points are `issue` (primary) and `issue-core` (explicit alias).
## What it is NOT
issue-core is intentionally narrow. The following live elsewhere:
- **Not a project manager.** Phases, campaigns, milestones spanning multiple tasks,
dependency graphs across tasks, gantt-style scheduling — that is the domain of
`project-core` (planned). issue-core deals in individual tasks, not in plans
composed of tasks.
- **Not a spawn audit trail.** When activity-core fires a rule that creates a task,
the *spawn event* (who fired, what rule, what triggering event) is recorded in
activity-core's `task_spawn_log`. issue-core only stores the resulting task and
its `triggering_event_id` reference back. The audit-of-creation belongs to the
emitter.
- **Not an event bus.** Communication between services flows over NATS (and
state-hub progress events). issue-core consumes events, but does not relay them.
- **Not a notification system.** Surfacing "your task changed" to humans is the
job of the relevant UI / digest / chatbot layer, not issue-core.
- **Not a workflow engine.** State transitions are simple (open → closed, with
a few in-between states). Conditional routing, approvals, multi-step
workflows — out of scope.
## How it fits
```
+-------------------+
| activity-core |
| IssueSink (REST) |
+---------+---------+
|
POST /issues/ (TaskSpec payload)
|
v
+------------+ +-------+--------+ +-----------------+
| Humans +----->| |<-----+ Agents |
| CLI: | | issue-core | | (CLI or REST) |
| $ issue | | | | |
+------------+ +-------+--------+ +-----------------+
|
+---------+----------+
| Backend router |
+---+------+------+--+
| | |
v v v
+------+ +-----+ +------+
|Gitea | |SQLite| |GitHub|
+------+ +-----+ +------+
```
**Upstream of issue-core (emitters):**
- **activity-core** — emits tasks via `IssueSink` when a rule fires or an
instruction declares one. Payload: `TaskSpec` over `POST /issues/`.
- **Humans** — `$ issue create ...` from terminals; future web UI.
- **Agents** — same REST surface or CLI.
**Downstream of issue-core (consumers):**
- **Humans and agents** assigned tasks consume them via `$ issue list`, web UI,
or per-backend native UIs (Gitea web, GitHub PR view, etc.).
- **state-hub** receives progress events as tasks move through their lifecycle.
- **Status updates** flow back to issue-core, not to the original emitter —
activity-core does not track what happened to the task it spawned.
## Success looks like
- Every task in the Coulomb org is discoverable from one query surface.
- activity-core can fire a rule and have the resulting task land in the right
backend with the right metadata, with no human in the loop.
- The CLI experience is identical across SQLite-only laptops and full Gitea-
backed servers.
- Offline work syncs back cleanly when connectivity returns.
## See also
- `SCOPE.md` — concrete in/out-of-scope decisions and integration boundaries.
- `ROADMAP.md` — feature trajectory.
- `workplans/` — active workstreams.
- activity-core `docs/adr/adr-001-event-bridge-architecture.md` — the IssueSink
contract that issue-core honors at `POST /issues/`.

153
Makefile
View File

@@ -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/^ / /'

View File

@@ -1,10 +1,10 @@
# Issue Facade - Agent Coordination via Issue Tracking # Issue Core - Agent Coordination via Issue Tracking
**A unified interface for autonomous coding agents to coordinate project implementation through issue tracking systems.** **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`)

View File

@@ -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:

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,143 @@
# NATS Task Ingestion — Design Stub
**Status:** design stub. Implementation deferred until activity-core's
`IssueSink` migrates from REST to NATS.
**Scope:** describe what the NATS-backed counterpart of `POST /issues/` will
look like, so the activity-core agent and any other future emitter can plan
against a stable contract.
## Why NATS
Today the ingestion surface is `POST /issues/` — synchronous REST with an API
key. That works for activity-core's first cut but has limitations:
- **Coupling**: activity-core needs to know the URL and key of every issue-core
instance. With NATS, both sides connect to a shared broker; routing is by
subject.
- **Backpressure**: REST is best-effort. If issue-core is down or slow, the
emitter either blocks or drops. With NATS JetStream, messages are durable and
replay-capable.
- **Fan-out**: REST has one consumer. NATS supports multiple consumers (e.g. an
audit logger sitting alongside the actual ingester) trivially.
- **Replay**: incidents that lose tasks can be reconstructed from the JetStream
log if the consumer was offline.
## Subject pattern
```
act.tasks.create.{target_repo}
```
- Namespace prefix `act.tasks.` (the `act` is activity-core's heritage — the
subject prefix is now neutral and other emitters can publish on it too).
- `create` is the verb. Future verbs (`act.tasks.update`, `act.tasks.close`)
are reserved but not in scope here.
- `{target_repo}` is the same string field as the REST `TaskSpec.target_repo`.
It allows subject-based routing in consumers: an issue-core instance
responsible only for one repo subscribes to `act.tasks.create.myrepo`, while
a multi-tenant instance subscribes to `act.tasks.create.>`.
## Message schema
The payload is the **exact same** schema as the REST endpoint —
`TaskIngestionRequest` in `issue_core/api/schemas.py`:
```json
{
"title": "string",
"description": "string",
"target_repo": "string",
"priority": "high | medium | low",
"labels": ["string"],
"due_in_days": 7,
"source_type": "rule | instruction",
"source_id": "string",
"triggering_event_id": "uuid",
"activity_definition_id": "string"
}
```
Encoded as **JSON** in the message body. `Content-Type: application/json`
in the message header.
This intentionally matches the REST schema so the validator and `_build_issue`
logic in `issue_core/api/ingest.py` can be reused unchanged by the NATS
consumer.
## JetStream configuration
The publisher (e.g. activity-core IssueSink-NATS) writes to a JetStream stream:
| Field | Value |
|---------------|----------------------------------------|
| Stream name | `ACT_TASKS` |
| Subjects | `act.tasks.>` |
| Retention | Limits (Time-based: 7 days) |
| Storage | File |
| Replicas | 3 in prod, 1 in dev |
| Discard | Old (drop oldest on overflow) |
| Max msg size | 64 KiB (TaskSpec is small) |
issue-core consumes via a **durable consumer**:
| Field | Value |
|-----------------|----------------------------------------|
| Stream | `ACT_TASKS` |
| Consumer name | `issue-core-ingest` |
| Filter subject | `act.tasks.create.>` |
| Deliver policy | All (catch up from oldest on first start) |
| Ack policy | Explicit |
| Max deliver | 5 (then dead-letter) |
| Ack wait | 30s |
| Replay policy | Instant |
## Idempotency
NATS JetStream provides **at-least-once** delivery. The consumer must dedupe
retries.
**Idempotency key:** `triggering_event_id` (UUID, included in every payload).
The consumer's responsibility:
1. Compute idempotency key from `triggering_event_id`.
2. Check whether an issue with that key already exists (lookup by
`sync_metadata.ingestion.triggering_event_id`).
3. If exists, ack the message without creating a duplicate.
4. If not, create the issue and ack.
Both REST and NATS paths share this dedupe logic, so a task can be safely
emitted via either transport without risk of duplicate issues.
## Implementation plan (when activated)
1. Add `nats-py>=2.6` as an optional dependency (`pip install issue-core[nats]`).
2. New module `issue_core/nats/consumer.py` — connects to NATS, subscribes to
the durable consumer, parses messages, calls the same `_build_issue` /
backend.create_issue path as the REST endpoint.
3. New CLI subcommand `issue subscribe --nats-url ... --stream ACT_TASKS`.
4. Add idempotency check to both REST and NATS ingestion paths (single shared
function in `issue_core/api/ingest.py` or a new `issue_core/ingestion/`
module).
5. Tests using `nats-py` test harness or a docker-compose NATS instance.
## Open questions
- Should the NATS consumer write a `progress_event` to the state hub on each
successful ingestion, in addition to creating the issue? Probably yes, but
out of scope until activation.
- Multi-tenant routing: do we run one issue-core consumer per `target_repo`,
or one shared consumer with per-repo backend lookup? Current bias: shared
consumer, simpler to operate.
- Dead-letter handling: where do messages go after 5 failed deliveries?
Candidate: a `ACT_TASKS_DLQ` stream with manual replay tooling.
## See also
- `SCOPE.md` — confirms NATS ingestion is in-scope as a future surface.
- `issue_core/api/schemas.py` — the canonical `TaskIngestionRequest` schema.
- `issue_core/api/ingest.py` — the REST handler whose logic the NATS consumer
will share.
- activity-core `docs/adr/adr-001-event-bridge-architecture.md` — describes
activity-core's migration trajectory from REST to NATS.

51
docs/package-release.md Normal file
View 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
```

View File

@@ -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 .

View File

@@ -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:

View File

@@ -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:

View File

@@ -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:

View File

@@ -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:

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

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

View File

@@ -0,0 +1,14 @@
"""
issue-core REST API.
Ingestion surface for external emitters — primarily activity-core's
IssueSink, which calls POST /issues/ to file tasks against the org's
configured backends.
Run via the CLI: `issue serve --host 0.0.0.0 --port 8765`
Requires the [api] extra: `pip install issue-core[api]`.
"""
from .app import create_app
__all__ = ["create_app"]

26
issue_core/api/app.py Normal file
View File

@@ -0,0 +1,26 @@
"""
FastAPI application factory for issue-core.
"""
from fastapi import FastAPI
from .. import __version__
from .ingest import router as ingest_router
def create_app() -> FastAPI:
app = FastAPI(
title="issue-core",
description=(
"Authoritative task lifecycle manager for the Coulomb org. "
"POST /issues/ is the ingestion surface for activity-core's IssueSink."
),
version=__version__,
)
app.include_router(ingest_router)
@app.get("/healthz", tags=["meta"])
async def healthz() -> dict:
return {"status": "ok", "version": __version__}
return app

66
issue_core/api/auth.py Normal file
View File

@@ -0,0 +1,66 @@
"""
API key authentication for the issue-core REST API.
A single shared key is read from the ISSUE_CORE_API_KEY environment variable.
Clients send it either as `Authorization: Bearer <key>` or as `X-API-Key: <key>`.
If ISSUE_CORE_API_KEY is unset, the server refuses to start — the workplan
explicitly forbids an unauthenticated ingestion surface.
"""
import os
import secrets
from typing import Optional
from fastapi import Header, HTTPException, status
API_KEY_ENV_VAR = "ISSUE_CORE_API_KEY"
class AuthConfigError(RuntimeError):
"""Raised at startup when no API key is configured."""
def get_configured_api_key() -> str:
key = os.environ.get(API_KEY_ENV_VAR, "").strip()
if not key:
raise AuthConfigError(
f"{API_KEY_ENV_VAR} is not set. The ingestion endpoint requires an API key. "
f"Generate one with: python -c 'import secrets; print(secrets.token_urlsafe(32))'"
)
return key
def _extract_token(
authorization: Optional[str],
x_api_key: Optional[str],
) -> Optional[str]:
if x_api_key:
return x_api_key.strip()
if authorization:
scheme, _, value = authorization.partition(" ")
if scheme.lower() == "bearer" and value:
return value.strip()
return None
async def require_api_key(
authorization: Optional[str] = Header(default=None),
x_api_key: Optional[str] = Header(default=None, alias="X-API-Key"),
) -> None:
"""FastAPI dependency enforcing the shared API key."""
try:
expected = get_configured_api_key()
except AuthConfigError as exc:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=str(exc),
)
presented = _extract_token(authorization, x_api_key)
if not presented or not secrets.compare_digest(presented, expected):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing or invalid API key.",
headers={"WWW-Authenticate": "Bearer"},
)

139
issue_core/api/ingest.py Normal file
View File

@@ -0,0 +1,139 @@
"""
POST /issues/ — task ingestion endpoint.
Receives a TaskSpec payload (see schemas.TaskIngestionRequest) from an
authorized emitter, routes it to the configured backend, and returns the
created issue's id and (optional) URL.
Routing strategy (v1):
- Single default backend, looked up via cli.utils.get_default_backend().
- target_repo, triggering_event_id, source_*, activity_definition_id are
stored on the issue's sync_metadata for traceability back to the emitter.
- Per-target-repo routing is a planned follow-up; see SCOPE.md.
"""
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, Optional, Tuple
from fastapi import APIRouter, Depends, HTTPException, status
from ..backends.gitea import GiteaBackend
from ..backends.local import LocalSQLiteBackend
from ..cli.utils import get_config_dir, load_backend_configs
from ..core.interfaces import BackendFactory, IssueBackend
from ..core.models import Issue, IssueState, Label
from .auth import require_api_key
from .schemas import BackendName, TaskIngestionRequest, TaskIngestionResponse
router = APIRouter()
BackendFactory.register_backend("local", LocalSQLiteBackend)
BackendFactory.register_backend("gitea", GiteaBackend)
_BACKEND_TYPE_TO_NAME: Dict[str, str] = {
"local": "sqlite",
"gitea": "gitea",
"github": "github",
}
def _resolve_backend() -> Tuple[IssueBackend, str]:
configs = load_backend_configs()
default_name = configs.get("default", "local")
if default_name not in configs:
if default_name == "local":
configs["local"] = {
"type": "local",
"db_path": str(get_config_dir() / "issues.db"),
}
else:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=f"Default backend '{default_name}' is not configured.",
)
backend_config = configs[default_name]
backend_type = backend_config["type"]
try:
backend = BackendFactory.create_backend(backend_type)
backend.connect(backend_config)
except Exception as exc:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=f"Failed to connect to backend '{default_name}': {exc}",
)
return backend, backend_type
def _build_issue(payload: TaskIngestionRequest, backend_type: str) -> Issue:
now = datetime.now(timezone.utc)
labels = [Label(name=name) for name in payload.labels]
labels.append(Label(name=f"priority:{payload.priority}"))
labels.append(Label(name=f"source:{payload.source_type}"))
if payload.target_repo:
labels.append(Label(name=f"repo:{payload.target_repo}"))
ingestion_meta: Dict[str, Any] = {
"target_repo": payload.target_repo,
"source_type": payload.source_type,
"source_id": payload.source_id,
"triggering_event_id": str(payload.triggering_event_id),
"activity_definition_id": payload.activity_definition_id,
"ingested_at": now.isoformat(),
}
if payload.due_in_days is not None:
ingestion_meta["due_at"] = (now + timedelta(days=payload.due_in_days)).isoformat()
sync_metadata: Dict[str, Any] = {"ingestion": ingestion_meta}
return Issue(
id="",
number=0,
title=payload.title,
description=payload.description,
state=IssueState.OPEN,
created_at=now,
updated_at=now,
labels=labels,
backend_type=backend_type,
sync_metadata=sync_metadata,
)
@router.post(
"/issues/",
response_model=TaskIngestionResponse,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(require_api_key)],
summary="Ingest a task from an external emitter (e.g. activity-core).",
)
async def ingest_task(payload: TaskIngestionRequest) -> TaskIngestionResponse:
backend, backend_type = _resolve_backend()
draft = _build_issue(payload, backend_type)
try:
created: Issue = backend.create_issue(draft)
except HTTPException:
raise
except Exception as exc:
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail=f"Backend rejected the issue: {exc}",
)
finally:
try:
backend.disconnect()
except Exception:
pass
issue_id = created.id or str(created.number)
issue_url: Optional[str] = None
if created.sync_metadata:
issue_url = created.sync_metadata.get("url") or created.sync_metadata.get("html_url")
backend_name: BackendName = _BACKEND_TYPE_TO_NAME.get(backend_type, backend_type) # type: ignore[assignment]
return TaskIngestionResponse(
issue_id=issue_id,
issue_url=issue_url,
backend=backend_name,
)

55
issue_core/api/schemas.py Normal file
View 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

View File

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

View File

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

View File

@@ -0,0 +1,43 @@
"""
`issue serve` — launch the issue-core REST API.
Requires the [api] extra: `pip install issue-core[api]`.
"""
import click
@click.command("serve")
@click.option("--host", default="127.0.0.1", show_default=True, help="Bind address.")
@click.option("--port", default=8765, show_default=True, type=int, help="Bind port.")
@click.option("--reload", is_flag=True, default=False, help="Auto-reload on code change (dev only).")
@click.option("--log-level", default="info", show_default=True, help="Uvicorn log level.")
def serve_command(host: str, port: int, reload: bool, log_level: str) -> None:
"""Launch the issue-core REST API (POST /issues/ ingestion endpoint).
Requires ISSUE_CORE_API_KEY to be set in the environment.
"""
try:
import uvicorn # noqa: F401
except ImportError:
raise click.ClickException(
"The 'api' extra is not installed. Run: pip install 'issue-core[api]'"
)
from ..api.auth import AuthConfigError, get_configured_api_key
try:
get_configured_api_key()
except AuthConfigError as exc:
raise click.ClickException(str(exc))
import uvicorn
uvicorn.run(
"issue_core.api.app:create_app",
host=host,
port=port,
reload=reload,
log_level=log_level,
factory=True,
)

View File

@@ -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"

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

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

View 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

View 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

View 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

View File

@@ -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
View 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`.

View File

View File

@@ -0,0 +1,4 @@
version: 1
updated: '2026-06-16'
domain: helix_forge
capabilities: []

View File

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

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

View File

@@ -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')

View File

@@ -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
) )

View File

@@ -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()

View File

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

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

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

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