feat: add markdown-validator agent and scoped-labels skill

- Add markdown-validator agent for validating Gitea issue descriptions
- Add scoped-labels skill for managing exclusive labels (status::, priority::, type::)
- Add init-scoped-labels.sh script to create standard label sets
- Add GiteaClient methods: createLabel, updateLabel, setScopedLabel, setScopedStatus, setScopedPriority
- Support exclusive labels (scoped labels) in Gitea API 1.21+

Created scoped labels:
- status::new, status::planned, status::in-progress, status::review, status::testing, status::done, status::blocked, status::cancelled
- priority::critical, priority::high, priority::medium, priority::low
- type::bug, type::feature, type::enhancement, type::documentation, type::refactor, type::test, type::chore
- size::xs, size::s, size::m, size::l, size::xl

Scoped labels are mutually exclusive within their scope - applying status::in-progress automatically removes status::new
This commit is contained in:
swp
2026-04-04 01:42:12 +01:00
parent 2519079e6e
commit e58a5b6380
4 changed files with 669 additions and 3 deletions

View File

@@ -0,0 +1,226 @@
---
description: Validates and corrects Markdown descriptions for Gitea issues
mode: subagent
model: qwen/qwen3.6-plus:free
color: "#F97316"
---
# Markdown Validator Agent
Validates and fixes Markdown descriptions for Gitea issues, ensuring proper formatting and structure.
## Role
You are a technical writer specializing in Markdown validation. You ensure all issue descriptions follow Gitea's Markdown specification and best practices.
## Input
- Issue title
- Issue body/description
- Context (what the issue is about)
## Validation Rules
### 1. Checklist Format
✅ Correct:
```markdown
## Checklist
- [x] Completed task
- [ ] Pending task
- [ ] Another pending task
```
❌ Incorrect:
```markdown
## Checklist
[x] Completed task (missing dash)
- [x] Completed task (missing space after bracket)
```
### 2. Headers
✅ Correct:
```markdown
## Description
Content here
## Technical Details
### Backend
Content
### Frontend
Content
```
❌ Incorrect:
```markdown
##Description (missing space)
## Description (leading spaces)
```
### 3. Code Blocks
✅ Correct:
```markdown
```typescript
const x = 1
```
```
❌ Incorrect:
```markdown
``typescript (missing backticks)
```typescript
(no closing backticks)
```
### 4. Links
✅ Correct:
```markdown
[Link text](https://example.com)
Related to #123
```
❌ Incorrect:
```markdown
[Link text] (https://example.com) (space in URL)
Related to Issue #123 (use shorthand #123)
```
### 5. Tables
✅ Correct:
```markdown
| Column 1 | Column 2 |
|----------|----------|
| Value 1 | Value 2 |
```
❌ Incorrect:
```markdown
|Column 1|Column 2| (missing spaces)
|----------| (missing second column)
```
### 6. Lists
✅ Correct:
```markdown
- Item 1
- Nested item
- Item 2
1. Numbered
2. Nested
```
❌ Incorrect:
```markdown
- Item 1
- Nested item (should be indented)
```
### 7. Escaping
- Escape `#` in non-header contexts: `\#123`
- Escape `*` in non-bold contexts: `\*literal\*`
- Escape backticks: `\`literal backticks\``
## Output Format
Return the corrected Markdown:
```markdown
## Description
[Brief description of what needs to be done]
## Checklist
- [ ] Task 1
- [ ] Task 2
- [ ] Task 3
## Technical Details
[Implementation notes]
## Related
- Related to #123
- Depends on #456
## Acceptance Criteria
- [ ] Criterion 1
- [ ] Criterion 2
```
## Common Fixes
| Issue | Fix |
|-------|-----|
| Missing newline before header | Add `\n\n` before `#` |
| Incorrect checkbox syntax | Fix to `- [ ]` or `- [x]` |
| Missing language in code block | Add language identifier |
| Broken links | Fix URL format |
| Improper nesting | Add proper indentation |
## Example
**Input:**
```
Title: Add authentication
Body:
Add auth system
[x] Design API
- Implement
[ ] Test
```
**Output:**
```markdown
## Description
Implement authentication system for the application.
## Checklist
- [x] Design API
- [ ] Implement authentication logic
- [ ] Write unit tests
- [ ] Write integration tests
- [ ] Update documentation
## Technical Details
- Use JWT for session management
- Implement OAuth2 providers (Google, GitHub)
- Add rate limiting for auth endpoints
## Related
- Related to #1
- Depends on #2 (database setup)
## Acceptance Criteria
- [ ] Users can log in with email/password
- [ ] Users can log in via OAuth2
- [ ] Sessions expire after 24 hours
- [ ] Rate limiting prevents brute force
```
## Usage
```
@markdown-validator <issue-content>
```
The agent will:
1. Parse the input Markdown
2. Validate against Gitea specification
3. Fix common issues automatically
4. Return properly formatted Markdown

View File

@@ -0,0 +1,198 @@
---
name: scoped-labels
description: Work with Gitea scoped labels (exclusive labels) for categorizing issues. Create, manage, and apply scoped labels like priority::high, status::new, type::bug etc.
---
# Scoped Labels Skill
## Purpose
Manage Gitea scoped labels (exclusive labels) for better issue organization and pipeline workflow.
## What are Scoped Labels?
Scoped labels are labels with `::` separator that are mutually exclusive within the same scope. When you apply a scoped label, other labels in the same scope are automatically removed.
### Examples
| Scope | Labels | Behavior |
|-------|--------|----------|
| `status` | `status::new`, `status::in-progress`, `status::done` | Only one status at a time |
| `priority` | `priority::critical`, `priority::high`, `priority::medium`, `priority::low` | Only one priority at a time |
| `type` | `type::bug`, `type::feature`, `type::enhancement` | Only one type at a time |
| `size` | `size::xs`, `size::s`, `size::m`, `size::l`, `size::xl` | Only one size at a time |
## API for Scoped Labels
### Create Scoped Label
```typescript
// Create exclusive label (scoped)
await client.createLabel({
name: 'status::new',
color: '0052cc',
description: 'New issue, not started',
exclusive: true // This makes it a scoped label
})
```
### Apply Scoped Label
```typescript
// Apply scoped label (automatically removes other status:: labels)
await client.addLabels(issueNumber, ['status::in-progress'])
// This removes status::new if it was applied
```
## Standard Label Sets
### Status Labels
```json
[
{ "name": "status::new", "color": "0052cc", "description": "New issue", "exclusive": true },
{ "name": "status::planned", "color": "1d76db", "description": "Planned for sprint", "exclusive": true },
{ "name": "status::in-progress", "color": "fbca04", "description": "Work in progress", "exclusive": true },
{ "name": "status::review", "color": "d93f0b", "description": "Under review", "exclusive": true },
{ "name": "status::testing", "color": "d4c5f9", "description": "In testing", "exclusive": true },
{ "name": "status::done", "color": "0e8a16", "description": "Completed", "exclusive": true },
{ "name": "status::blocked", "color": "b60205", "description": "Blocked", "exclusive": true }
]
```
### Priority Labels
```json
[
{ "name": "priority::critical", "color": "b60205", "description": "Critical priority", "exclusive": true },
{ "name": "priority::high", "color": "d93f0b", "description": "High priority", "exclusive": true },
{ "name": "priority::medium", "color": "fbca04", "description": "Medium priority", "exclusive": true },
{ "name": "priority::low", "color": "0e8a16", "description": "Low priority", "exclusive": true }
]
```
### Type Labels
```json
[
{ "name": "type::bug", "color": "d73a4a", "description": "Something is broken", "exclusive": true },
{ "name": "type::feature", "color": "0e8a16", "description": "New feature", "exclusive": true },
{ "name": "type::enhancement", "color": "a2eeef", "description": "Improvement", "exclusive": true },
{ "name": "type::documentation", "color": "0075ca", "description": "Documentation", "exclusive": true },
{ "name": "type::refactor", "color": "7057ff", "description": "Code refactoring", "exclusive": true },
{ "name": "type::test", "color": "d4c5f9", "description": "Testing", "exclusive": true }
]
```
### Size Labels
```json
[
{ "name": "size::xs", "color": "cfd3d7", "description": "Extra small (<1 hour)", "exclusive": true },
{ "name": "size::s", "color": "c2e0c6", "description": "Small (1-2 hours)", "exclusive": true },
{ "name": "size::m", "color": "fbca04", "description": "Medium (2-4 hours)", "exclusive": true },
{ "name": "size::l", "color": "d93f0b", "description": "Large (4-8 hours)", "exclusive": true },
{ "name": "size::xl", "color": "b60205", "description": "Extra large (>8 hours)", "exclusive": true }
]
```
### Component Labels (Non-exclusive)
```json
[
{ "name": "component::api", "color": "1d76db", "description": "API related", "exclusive": false },
{ "name": "component::ui", "color": "bfdadc", "description": "UI related", "exclusive": false },
{ "name": "component::database", "color": "c5def5", "description": "Database related", "exclusive": false },
{ "name": "component::auth", "color": "d4c5f9", "description": "Authentication related", "exclusive": false }
]
```
## Workflow Integration
### Pipeline Status Flow
```
status::new → status::planned → status::in-progress → status::review → status::testing → status::done
status::blocked
```
### Usage in Pipeline
```typescript
// Set initial status
await client.addLabels(issueNumber, ['status::new', 'priority::high', 'type::feature'])
// Later, update status (automatic removal of old status)
await client.addLabels(issueNumber, ['status::in-progress']) // Removes status::new
// Block issue
await client.addLabels(issueNumber, ['status::blocked']) // Removes status::in-progress
// Complete
await client.addLabels(issueNumber, ['status::done']) // Removes any previous status
```
## Setup Script
```bash
#!/bin/bash
# Create standard scoped labels
API_URL="https://git.softuniq.eu/api/v1"
OWNER="UniqueSoft"
REPO="APAW"
TOKEN=$GITEA_TOKEN
# Status labels
for LABEL in 'status::new:0052cc' 'status::planned:1d76db' 'status::in-progress:fbca04' 'status::review:d93f0b' 'status::testing:d4c5f9' 'status::done:0e8a16' 'status::blocked:b60205'; do
NAME=$(echo $LABEL | cut -d: -f1,2)
COLOR=$(echo $LABEL | cut -d: -f3)
curl -X POST \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"name\":\"$NAME\",\"color\":\"$COLOR\",\"exclusive\":true}" \
"$API_URL/repos/$OWNER/$REPO/labels"
done
# Priority labels
for LABEL in 'priority::critical:b60205' 'priority::high:d93f0b' 'priority::medium:fbca04' 'priority::low:0e8a16'; do
NAME=$(echo $LABEL | cut -d: -f1,2)
COLOR=$(echo $LABEL | cut -d: -f3)
curl -X POST \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"name\":\"$NAME\",\"color\":\"$COLOR\",\"exclusive\":true}" \
"$API_URL/repos/$OWNER/$REPO/labels"
done
```
## Label Naming Convention
1. **Scope first**: `scope::value` (e.g., `status::new`)
2. **Lowercase**: Use lowercase letters and hyphens
3. **Descriptive**: Make the purpose clear
4. **Color coding**: Use semantic colors
- Red: Critical/Blocked/Bug
- Yellow: In progress/Warning
- Green: Done/Success
- Blue: Information
## Query by Label
```bash
# Find all issues with status::in-progress
curl -s "$API_URL/repos/$OWNER/$REPO/issues?labels=status::in-progress"
# Find all critical bugs
curl -s "$API_URL/repos/$OWNER/$REPO/issues?labels=priority::critical,type::bug"
# Find blocked issues in any status
curl -s "$API_URL/repos/$OWNER/$REPO/issues?labels=status::blocked"
```
## Benefits
1. **Automatic exclusivity**: Applying `status::in-progress` removes `status::new`
2. **Clear categorization**: Each scope represents one dimension
3. **Better filtering**: Combine scopes for precise queries
4. **Visual clarity**: One label per column in board view

148
scripts/init-scoped-labels.sh Executable file
View File

@@ -0,0 +1,148 @@
#!/bin/bash
# Initialize standard scoped labels for Gitea
# Usage: ./scripts/init-scoped-labels.sh
echo "=== Gitea Scoped Labels Initialization ==="
echo ""
# Check for token
if [ -z "$GITEA_TOKEN" ]; then
echo "❌ GITEA_TOKEN not set!"
echo ""
echo "Run: ./scripts/create-gitea-token.sh <username> <password>"
echo "Or: export GITEA_TOKEN=your_token"
exit 1
fi
API_URL="https://git.softuniq.eu/api/v1"
# Detect repository
REMOTE=$(git remote get-url origin 2>/dev/null | head -1)
OWNER=$(echo "$REMOTE" | sed 's/.*[:/]\([^/]*\)\/.*/\1/')
REPO=$(echo "$REMOTE" | sed 's/.*[:/][^/]*\/\([^/.]*\).*/\1/')
if [ -z "$OWNER" ] || [ -z "$REPO" ]; then
echo "❌ Could not detect repository"
echo "Set OWNER and REPO manually:"
echo " export OWNER=UniqueSoft"
echo " export REPO=APAW"
exit 1
fi
echo "📦 Repository: $OWNER/$REPO"
echo ""
# Function to create label
create_label() {
local NAME="$1"
local COLOR="$2"
local DESC="$3"
local EXCLUSIVE="$4"
# Check if label exists
EXISTING=$(curl -s -H "Authorization: token $GITEA_TOKEN" \
"$API_URL/repos/$OWNER/$REPO/labels?name=$NAME" | grep -o "\"name\":\"$NAME\"")
if [ -n "$EXISTING" ]; then
echo " ⏭️ $NAME (already exists)"
return
fi
# Create label
RESPONSE=$(curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"name\":\"$NAME\",\"color\":\"$COLOR\",\"description\":\"$DESC\",\"exclusive\":$EXCLUSIVE}" \
"$API_URL/repos/$OWNER/$REPO/labels")
if echo "$RESPONSE" | grep -q '"id"'; then
echo "$NAME"
else
echo "$NAME - $(echo $RESPONSE | grep -o '"message":"[^"]*"')"
fi
}
# Status labels
echo "🏷️ Creating status labels..."
create_label "status::new" "0052cc" "New issue, not started" true
create_label "status::planned" "1d76db" "Planned for sprint" true
create_label "status::in-progress" "fbca04" "Work in progress" true
create_label "status::review" "d93f0b" "Under review" true
create_label "status::testing" "d4c5f9" "In testing" true
create_label "status::done" "0e8a16" "Completed" true
create_label "status::blocked" "b60205" "Blocked" true
create_label "status::cancelled" "5319e7" "Cancelled" true
echo ""
# Priority labels
echo "🏷️ Creating priority labels..."
create_label "priority::critical" "b60205" "Critical priority" true
create_label "priority::high" "d93f0b" "High priority" true
create_label "priority::medium" "fbca04" "Medium priority" true
create_label "priority::low" "0e8a16" "Low priority" true
echo ""
# Type labels
echo "🏷️ Creating type labels..."
create_label "type::bug" "d73a4a" "Something is broken" true
create_label "type::feature" "0e8a16" "New feature" true
create_label "type::enhancement" "a2eeef" "Improvement" true
create_label "type::documentation" "0075ca" "Documentation" true
create_label "type::refactor" "7057ff" "Code refactoring" true
create_label "type::test" "d4c5f9" "Testing" true
create_label "type::chore" "cfd3d7" "Maintenance task" true
echo ""
# Size labels
echo "🏷️ Creating size labels..."
create_label "size::xs" "cfd3d7" "Extra small (<1 hour)" true
create_label "size::s" "c2e0c6" "Small (1-2 hours)" true
create_label "size::m" "fbca04" "Medium (2-4 hours)" true
create_label "size::l" "d93f0b" "Large (4-8 hours)" true
create_label "size::xl" "b60205" "Extra large (>8 hours)" true
echo ""
# Component labels (non-exclusive)
echo "🏷️ Creating component labels..."
create_label "component::api" "1d76db" "API related" false
create_label "component::ui" "bfdadc" "UI related" false
create_label "component::database" "c5def5" "Database related" false
create_label "component::auth" "d4c5f9" "Authentication related" false
create_label "component::pipeline" "7057ff" "Pipeline related" false
create_label "component::agent" "5319e7" "Agent related" false
echo ""
# Pipeline status labels (alternative status format)
echo "🏷️ Creating pipeline status labels..."
create_label "pipeline::new" "0052cc" "New pipeline task" true
create_label "pipeline::researching" "1d76db" "Research phase" true
create_label "pipeline::designed" "bfd4f2" "Design complete" true
create_label "pipeline::testing" "fbca04" "Tests in progress" true
create_label "pipeline::implementing" "d93f0b" "Implementation" true
create_label "pipeline::reviewing" "d4c5f9" "Code review" true
create_label "pipeline::fixing" "e99695" "Fixing issues" true
create_label "pipeline::releasing" "c2e0c6" "Release preparation" true
create_label "pipeline::evaluated" "fef2c0" "Evaluation complete" true
create_label "pipeline::completed" "0e8a16" "Pipeline complete" true
echo ""
echo "=========================================="
echo "✅ Scoped labels initialized!"
echo ""
echo "📋 Usage examples:"
echo ""
echo "Set status (exclusive - removes other status:: labels):"
echo " client.addLabels(issueNumber, ['status::in-progress', 'priority::high'])"
echo ""
echo "Set priority:"
echo " client.addLabels(issueNumber, ['priority::critical'])"
echo ""
echo "Set type and size:"
echo " client.addLabels(issueNumber, ['type::feature', 'size::m'])"
echo ""
echo "Add component (multiple allowed):"
echo " client.addLabels(issueNumber, ['component::api', 'component::auth'])"
echo ""
echo "🔗 View all labels:"
echo " https://git.softuniq.eu/$OWNER/$REPO/labels"
echo ""

View File

@@ -312,21 +312,115 @@ export class GiteaClient {
)
}
async getLabel(labelId: number): Promise<Label> {
async getLabel(labelId: number | string): Promise<Label> {
return this.request(
"GET",
`/repos/${this.owner}/${this.repo}/labels/${labelId}`
)
}
async createLabel(label: { name: string; color: string; description?: string }): Promise<Label> {
async createLabel(label: {
name: string
color: string
description?: string
exclusive?: boolean
}): Promise<Label> {
// Ensure color doesn't have # prefix
const color = label.color.startsWith('#') ? label.color.slice(1) : label.color
return this.request(
"POST",
`/repos/${this.owner}/${this.repo}/labels`,
label
{
name: label.name,
color,
description: label.description,
exclusive: label.exclusive ?? false
}
)
}
async updateLabel(labelId: number | string, label: {
name?: string
color?: string
description?: string
exclusive?: boolean
}): Promise<Label> {
const body: Record<string, unknown> = {}
if (label.name) body.name = label.name
if (label.color) {
body.color = label.color.startsWith('#') ? label.color.slice(1) : label.color
}
if (label.description !== undefined) body.description = label.description
if (label.exclusive !== undefined) body.exclusive = label.exclusive
return this.request(
"PATCH",
`/repos/${this.owner}/${this.repo}/labels/${labelId}`,
body
)
}
// ==================== Scoped Labels ====================
/**
* Set status using scoped labels (exclusive within scope)
* Applies status::<status> label and removes other status:: labels
*/
async setScopedStatus(issueNumber: number, status: string): Promise<Label[]> {
const statusLabel = `status::${status}`
return this.setScopedLabel(issueNumber, 'status', statusLabel)
}
/**
* Set priority using scoped labels
* Applies priority::<priority> label and removes other priority:: labels
*/
async setScopedPriority(issueNumber: number, priority: string): Promise<Label[]> {
const priorityLabel = `priority::${priority}`
return this.setScopedLabel(issueNumber, 'priority', priorityLabel)
}
/**
* Generic method to set a scoped label (exclusive within scope)
* Removes all labels with the same scope prefix before adding the new one
*/
async setScopedLabel(issueNumber: number, scope: string, labelName: string): Promise<Label[]> {
// Get current labels
const currentLabels = await this.getIssueLabels(issueNumber)
// Find and remove labels with the same scope
const scopePrefix = `${scope}::`
const labelsToRemove = currentLabels.filter(l => l.name.startsWith(scopePrefix))
for (const label of labelsToRemove) {
await this.removeLabel(issueNumber, label.id)
}
// Find or create the new label
const allLabels = await this.getRepoLabels()
let targetLabel = allLabels.find(l => l.name === labelName)
if (!targetLabel) {
// Create the label if it doesn't exist
const colors: Record<string, string> = {
'status': '0052cc',
'priority': 'd93f0b',
'type': '0e8a16',
'size': 'fbca04',
'component': '1d76db'
}
targetLabel = await this.createLabel({
name: labelName,
color: colors[scope] || '0052cc',
description: `${scope} label: ${labelName.split('::')[1]}`,
exclusive: true
})
}
return this.addLabels(issueNumber, [targetLabel.id])
}
// ==================== Status Management ====================
async getStatusLabels(issueNumber: number): Promise<Label[]> {