Compare commits

...

13 Commits

Author SHA1 Message Date
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
21 changed files with 509 additions and 133 deletions

View File

@@ -1,11 +1,11 @@
## First Session Protocol
Triggered when `get_domain_summary("custodian")` shows **no workstreams**.
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/custodian/project_charter_v0.1.md` — purpose, scope
- `~/the-custodian/canon/projects/custodian/roadmap_v0.1.md` — planned phases
- `~/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**
@@ -17,7 +17,7 @@ roadmap phase. **Wait for approval before creating.**
**Step 4 — Create workplan file first, then DB record (ADR-001)**
```
workplans/issue-core-WP-NNNN-<slug>.md ← write this first
workplans/ISSUE-WP-NNNN-<slug>.md ← write this first
```
Then register in the hub:
```
@@ -28,7 +28,7 @@ create_task(workstream_id="<id>", title="...", priority="high|medium|low")
**Step 5 — Record the setup**
```
add_progress_event(
summary="First session: structured custodian into N workstreams, M tasks",
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}

View File

@@ -1,5 +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:** custodian
**Domain:** infotech
**Repo slug:** issue-core
**Topic ID:** cee7bedf-2b48-46ef-8601-006474f2ad7a

View File

@@ -1,6 +1,7 @@
## Session Protocol
State Hub: http://127.0.0.1:8000
Dev Hub (State Hub API): http://127.0.0.1:8000
MCP server name in `~/.claude.json`: `dev-hub`
**Step 1 — Orient**
@@ -10,7 +11,7 @@ cat .custodian-brief.md
```
Then call the MCP tool for richer cross-domain context when MCP tools are exposed:
```
get_domain_summary("custodian")
get_domain_summary("infotech")
```
If MCP tools are unavailable in the current agent session, use the REST API:
```bash
@@ -39,11 +40,11 @@ curl -s -X PATCH "http://127.0.0.1:8000/messages/<id>/read" \
ls workplans/
```
For each file with `status: ready`, `active`, or `blocked`, note pending
`todo`/`in_progress` tasks.
`wait`/`todo`/`progress` tasks.
**Step 4 — Present brief**
1. **Active workstreams** for `custodian` — title, task counts, blocking decisions
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"*

View File

@@ -1,7 +1,7 @@
## Workplan Convention (ADR-001)
File location: `workplans/issue-core-WP-NNNN-<slug>.md`
ID prefix: `ISSUE-WP`
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.
@@ -12,7 +12,7 @@ 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-core-WP-NNNN-<slug>.md`. The frontmatter id remains
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**:
@@ -25,4 +25,16 @@ 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 -->

View File

@@ -1,27 +1,23 @@
<!-- custodian-brief: generated by fix-consistency — do not edit manually -->
# Custodian Brief — issue-core
**Domain:** custodian
**Last synced:** 2026-06-19 19:09 UTC
**Domain:** infotech
**Last synced:** 2026-07-01 17:39 UTC
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
## Active Workstreams
### Deploy issue-core as a service on railiance01 (ArgoCD GitOps pilot)
Progress: 1/7 done | workstream_id: `896ace77-21b3-450b-8fb7-254aefc8c570`
Progress: 5/7 done | workstream_id: `896ace77-21b3-450b-8fb7-254aefc8c570`
**Open tasks:**
- ! ArgoCD bootstrap (railiance-platform dependency) + issue-core Application `9b199b1d`
- ► Kubernetes manifests (namespace, Deployment, Service) in GitOps source `38887dd6`
- ► In-cluster backend config (cluster Gitea / markitect) `10923f1e`
- · OpenBao secret: ISSUE_CORE_API_KEY `ad52527f`
- · Wire activity-core to the live service `96b14cdb`
- · End-to-end verification + GitOps runbook `8d853b8e`
- ! Wire activity-core to the live service `96b14cdb`
- ! End-to-end verification + GitOps runbook `8d853b8e`
---
## MCP Orientation (when available)
If the state-hub MCP server is reachable, call:
`get_domain_summary("custodian")`
`get_domain_summary("infotech")`
This provides richer cross-domain context.
If the MCP call fails, use this file as your orientation source.

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.

View File

@@ -4,7 +4,7 @@
**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:** custodian
**Domain:** infotech
**Repo slug:** issue-core
**Topic ID:** `cee7bedf-2b48-46ef-8601-006474f2ad7a`
**Workplan prefix:** `ISSUE-WP-`
@@ -63,8 +63,8 @@ Omit `workstream_id` / `task_id` when not applicable.
```bash
curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
-H "Content-Type: application/json" \
-d '{"status": "in_progress"}'
# values: todo | in_progress | done | blocked
-d '{"status": "progress"}'
# values: wait | todo | progress | done | cancel
```
### Flag a task for human review
@@ -83,7 +83,7 @@ curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
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 blocked tasks: `GET /tasks/?needs_human=true`
4. Check human-needed tasks: `GET /tasks/?needs_human=true`
**During work:**
- Update task statuses in workplan files as tasks progress
@@ -151,31 +151,10 @@ every repo's agent instructions because it is high-frequency, high-risk, and eas
get wrong.
**Canon:** `~/ops-warden/wiki/CredentialRouting.md` · catalog `~/ops-warden/registry/routing/catalog.yaml`
---
## REST ingestion API key
`POST /issues/` requires a shared key in `ISSUE_CORE_API_KEY`. The server
refuses to start without it. activity-core's `IssueCoreRestSink` sends the same
value as `Authorization: Bearer <key>` (also accepts `X-API-Key`).
**Do not request this key from ops-warden** — pair env vars locally or via
OpenBao/K8s on both repos. Routing lookup:
`warden route show activity-core-issue-sink --json`.
**Local dev:**
```bash
export ISSUE_CORE_API_KEY="$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')"
issue serve --host 127.0.0.1 --port 8765
```
Set the same `ISSUE_CORE_API_KEY` in activity-core when `ISSUE_SINK_TYPE=rest`.
For local ingest smoke, set `default: local` in `~/.config/issue-tracker/backends.json`
— a remote Gitea default backend will hang on `POST /issues/`.
See `README.md` (REST Ingestion Server) and activity-core
`docs/issue-core-emission-boundary.md`.
<!-- REPO-AGENTS-EXTENSIONS -->
<!-- Append repo-specific agent instructions below this marker.
The state-hub template sync preserves content after this line. -->
---
@@ -202,7 +181,7 @@ anything needing analysis, design, approval, dependencies, or multiple phases.
id: ISSUE-WP-NNNN
type: workplan
title: "..."
domain: custodian
domain: infotech
repo: issue-core
status: proposed | ready | active | blocked | backlog | finished | archived
owner: codex
@@ -224,7 +203,7 @@ derived health labels, not frontmatter statuses.
` ` `task
id: ISSUE-WP-NNNN-T01
status: todo | in_progress | done | blocked
status: wait | todo | progress | done | cancel
priority: high | medium | low
state_hub_task_id: "<uuid>" # written by fix-consistency — do not edit
` ` `
@@ -232,7 +211,7 @@ state_hub_task_id: "<uuid>" # written by fix-consistency — do not edit
Task description text.
```
Status progression: `todo` → `in_progress` → `done` (or `blocked`)
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

View File

@@ -1,15 +1,10 @@
# issue-core REST ingestion service image.
#
# Installs the published issue-core[api] package from the Coulomb Gitea PyPI
# index (no sibling-checkout build context) and runs the FastAPI ingestion
# server on :8765. Built and pushed to gitea.coulomb.social/coulomb/issue-core.
# 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
ARG ISSUE_CORE_VERSION=">=0.2,<0.3"
# Do not name this PIP_INDEX_URL — Docker exposes ARGs as env vars during RUN,
# and pip treats PIP_INDEX_URL as the sole primary index (excluding PyPI).
ARG GITEA_PYPI_INDEX_URL=https://gitea.coulomb.social/api/packages/coulomb/pypi/simple/
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
HOME=/home/app
@@ -18,10 +13,11 @@ ENV PYTHONUNBUFFERED=1 \
# (~/.config/issue-tracker/backends.json).
RUN useradd --create-home --home-dir /home/app --uid 10001 app
RUN pip install --no-cache-dir \
--index-url https://pypi.org/simple \
--extra-index-url "${GITEA_PYPI_INDEX_URL}" \
"issue-core[api]${ISSUE_CORE_VERSION}"
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
@@ -30,4 +26,4 @@ USER app
EXPOSE 8765
# Entrypoint renders backends.json from env, then execs the server.
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]

View File

@@ -135,11 +135,17 @@ user directory.
"due_in_days": 7,
"source_type": "rule | instruction",
"source_id": "string",
"triggering_event_id": "uuid",
"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

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.

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

View File

@@ -19,6 +19,6 @@ Supported Backends:
- Future: GitHub, GitLab, JIRA, Redmine
"""
__version__ = "0.2.0"
__version__ = "0.2.1"
__author__ = "Coulomb / MarkiTect Project"
__description__ = "Authoritative task lifecycle manager with plugin architecture"

View File

@@ -2,14 +2,12 @@
Pydantic schemas for the issue-core REST API.
The TaskIngestionRequest schema matches activity-core's IssueSink TaskSpec
payload exactly. See:
payload. See:
- SCOPE.md "TaskSpec payload" section
- activity-core docs/adr/adr-001-event-bridge-architecture.md
"""
from typing import List, Literal, Optional
from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field
@@ -31,7 +29,14 @@ class TaskIngestionRequest(BaseModel):
due_in_days: Optional[int] = Field(default=None, ge=0)
source_type: SourceType
source_id: str = Field(..., min_length=1)
triggering_event_id: UUID
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)

View File

@@ -195,10 +195,20 @@ class GiteaBackend(RemoteBackend, SyncableBackend):
if issue.milestone:
data['milestone'] = int(issue.milestone.backend_id) if issue.milestone.backend_id else None
# Convert labels
if issue.labels:
data['labels'] = [label.name for label in issue.labels]
# Gitea expects numeric label IDs on issue create/update. Name-only
# labels are preserved in issue-core metadata but omitted from the API
# payload until a label-resolution step exists.
label_ids = []
for label in issue.labels:
if not label.backend_id:
continue
try:
label_ids.append(int(label.backend_id))
except (TypeError, ValueError):
continue
if label_ids:
data['labels'] = label_ids
return data
# Issue CRUD Operations

View File

@@ -17,7 +17,7 @@ data:
"type": "gitea",
"base_url": "http://gitea-http.default.svc.cluster.local:3000",
"owner": "coulomb",
"repo": "markitect_project",
"repo": "markitect-main",
"token": "__FROM_ENV__"
},
"default": "markitect"

View File

@@ -23,7 +23,7 @@ spec:
# docs). Add imagePullSecrets: [{name: gitea-registry}] if it becomes private.
containers:
- name: issue-core
image: gitea.coulomb.social/coulomb/issue-core:0.2.0
image: gitea.coulomb.social/coulomb/issue-core:0.2.1
imagePullPolicy: IfNotPresent
ports:
- name: http
@@ -67,5 +67,6 @@ spec:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
runAsNonRoot: true
runAsUser: 10001
capabilities:
drop: ["ALL"]

View File

@@ -112,6 +112,30 @@ def test_ingest_creates_issue_with_x_api_key(client, valid_payload):
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"}

View File

@@ -6,8 +6,10 @@ These tests ensure the Gitea backend works correctly with the API.
import pytest
import json
from datetime import datetime, timezone
from unittest.mock import Mock, patch, MagicMock
from issue_core.backends.gitea.backend import GiteaBackend, GiteaAPIError
from issue_core.core.models import Issue, IssueState, Label
class TestGiteaBackend:
@@ -96,6 +98,29 @@ class TestGiteaBackend:
called_url = mock_request.call_args[1]['url'] if 'url' in mock_request.call_args[1] else mock_request.call_args[0][1]
assert called_url == 'https://git.example.com/api/v1/repos/owner/repo'
def test_gitea_payload_omits_name_only_labels(self):
"""Gitea issue payloads only include numeric label IDs."""
now = datetime.now(timezone.utc)
issue = Issue(
id="",
number=0,
title="Test issue",
description="Test description",
state=IssueState.OPEN,
created_at=now,
updated_at=now,
labels=[
Label(name="priority:low"),
Label(name="source:rule", backend_id="not-a-number"),
Label(name="existing", backend_id="42"),
],
)
payload = self.backend._unified_issue_to_gitea(issue)
assert payload["labels"] == [42]
assert payload["title"] == "Test issue"
@patch('issue_core.backends.gitea.backend.requests.Session')
def test_test_connection_success(self, mock_session_class):
"""Test test_connection method works correctly."""

View File

@@ -1,7 +1,7 @@
---
id: ISSC-WP-0001
type: workplan
domain: custodian
domain: infotech
repo: issue-core
status: done
state_hub_workstream_id: 1135fc1d-1f46-4e35-886d-04cc3b8050b6

View File

@@ -2,7 +2,7 @@
id: ISSUE-WP-0002
type: workplan
title: "Publish issue-core to Gitea PyPI"
domain: custodian
domain: infotech
repo: issue-core
status: finished
owner: codex

View File

@@ -2,13 +2,13 @@
id: ISSUE-WP-0003
type: workplan
title: "Deploy issue-core as a service on railiance01 (ArgoCD GitOps pilot)"
domain: custodian
domain: infotech
repo: issue-core
status: active
status: blocked
owner: claude
topic_slug: custodian
created: "2026-06-19"
updated: "2026-06-19"
updated: "2026-06-30"
state_hub_workstream_id: "896ace77-21b3-450b-8fb7-254aefc8c570"
---
@@ -17,41 +17,79 @@ state_hub_workstream_id: "896ace77-21b3-450b-8fb7-254aefc8c570"
`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.0` is built, pushed, and
pullable. The railiance01 cluster still has no `issue-core` workload until
T02 live ArgoCD bootstrap (RAILIANCE-WP-0004-T05) and T04 OpenBao secrets land.
`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-19)
## 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.0` (digest
`sha256:153fbe43…`). `coulomb` org packages are public — no `imagePullSecret`
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:** no `issue-core` namespace; no issue-core
Deployment/Service/Pod in any namespace.
- **Dangling reference:** `activity-core/k8s/railiance/20-runtime.yaml` sets
`ISSUE_CORE_URL: http://issue-core.issue-core.svc.cluster.local:8010` — a
service that does not exist, on the **wrong port** (issue-core serves 8765) —
with `ISSUE_SINK_TYPE: "null"` so emission is disabled. It is a placeholder.
- **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.
- **ArgoCD is installed but unused:** all 7 components healthy (~290d), but
**0 Applications, 0 ApplicationSets, 0 registered git repos**, only the stock
`default` AppProject. No `kind: Application` manifests exist in any infra repo.
`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).
@@ -96,30 +134,28 @@ state_hub_task_id: "3723e896-3ec9-49b8-86f8-403993444da3"
**Goal.** A reproducible, registry-hosted image ArgoCD-managed pods can pull.
- [x] Add `Dockerfile` installing `issue-core[api]>=0.2,<0.3` from the Gitea
PyPI index (with explicit PyPI primary index). Entrypoint renders
`backends.json` then `issue serve --host 0.0.0.0 --port 8765`.
- [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.0`; `docker pull`
- [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).
- [ ] `POST /issues/` smoke against a running deployment (deferred to T03/T04
cluster verification).
- [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: wait
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 task is `wait` on that
handoff. issue-core's part is to **contribute** the `Application` manifest +
workload manifests into the layout platform defines.
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
@@ -127,16 +163,17 @@ workload manifests into the layout platform defines.
- [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`.
- [ ] **(railiance-platform)** RAILIANCE-WP-0004-T05 live bootstrap: register
repo creds, deploy bootstrap, sync `issue-core` Application.
- [ ] Verify: `kubectl get applications -n argocd` shows `issue-core`
Synced/Healthy; ArgoCD reconciles a trivial manifest change.
- [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: in_progress
status: done
priority: high
state_hub_task_id: "38887dd6-0988-4ad1-bc6b-2a1b8839829f"
```
@@ -145,18 +182,18 @@ state_hub_task_id: "38887dd6-0988-4ad1-bc6b-2a1b8839829f"
- [x] `k8s/railiance/` Kustomize bundle (namespace via ArgoCD
`CreateNamespace=true`).
- [x] Deployment: registry image tag `0.2.0`; port 8765; `/healthz` probes;
- [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`.
- [ ] Verify: ArgoCD syncs the manifests; Pod Ready; `/healthz` 200 from a debug
pod (blocked on T01 push + T02 bootstrap + T04 secrets).
- [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: todo
status: done
priority: high
state_hub_task_id: "ad52527f-6222-4c11-9284-d8a3ed3b49ad"
```
@@ -171,12 +208,15 @@ state_hub_task_id: "ad52527f-6222-4c11-9284-d8a3ed3b49ad"
- 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: in_progress
status: done
priority: medium
state_hub_task_id: "10923f1e-050d-4f3e-980e-b061fef5f33a"
```
@@ -188,14 +228,14 @@ the cluster Gitea (markitect) backend.
(`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.
- [ ] Verify: a `POST /issues/` creates a real Gitea issue and returns
`issue_url` (blocked on T04 secrets + in-cluster deployment).
- [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: todo
status: wait
priority: high
state_hub_task_id: "96b14cdb-364f-4eab-a80e-dd8b3859c694"
```
@@ -207,11 +247,14 @@ state_hub_task_id: "96b14cdb-364f-4eab-a80e-dd8b3859c694"
once issue-core is Ready.
- Inject `ISSUE_CORE_API_KEY` into the activity-core worker from the same
OpenBao secret (T04).
- **Contract gap:** issue-core requires `triggering_event_id` as a UUID;
activity-core cron paths may send non-UUID keys (e.g. `"scheduled"`).
Event-driven emission with real event UUIDs works (the `str()` guard in
`issue_sink.py`, commit f05c56e, handles UUID objects). Align schemas before
enabling `rest` for cron-triggered rules.
- [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.
@@ -219,7 +262,7 @@ state_hub_task_id: "96b14cdb-364f-4eab-a80e-dd8b3859c694"
```task
id: ISSUE-WP-0003-T07
status: todo
status: wait
priority: medium
state_hub_task_id: "8d853b8e-cfca-441d-b817-0a29e37bd66e"
```
@@ -228,10 +271,12 @@ state_hub_task_id: "8d853b8e-cfca-441d-b817-0a29e37bd66e"
- ArgoCD Application Synced/Healthy; issue-core Pod Ready; Service reachable
cluster-internal.
- activity-core issue-core emission returns 201 and creates a Gitea issue.
- Document the GitOps runbook (image build/push, ArgoCD sync, secret rotation,
rollback) in `docs/`.
- Emit an `add_progress_event` milestone to the hub on completion.
- [ ] activity-core -> issue-core emission returns 201 and creates a Gitea issue
(remaining T06 handoff).
- [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`.
---