loki-mode 5.58.2 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +34 -9
- package/autonomy/issue-providers.sh +423 -0
- package/autonomy/loki +1080 -31
- package/autonomy/run.sh +320 -3
- package/dashboard/__init__.py +1 -1
- package/dashboard/migration_engine.py +74 -64
- package/docs/INSTALLATION.md +1 -1
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/providers/claude.sh +52 -2
- package/providers/codex.sh +39 -4
- package/providers/gemini.sh +44 -3
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes PRD to deployed product with minimal human intervention. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode
|
|
6
|
+
# Loki Mode v6.0.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -263,4 +263,4 @@ The following features are documented in skill modules but not yet fully automat
|
|
|
263
263
|
| Quality gates 3-reviewer system | Implemented (v5.35.0) | 5 specialist reviewers in `skills/quality-gates.md`; execution in run.sh |
|
|
264
264
|
| Benchmarks (HumanEval, SWE-bench) | Infrastructure only | Runner scripts and datasets exist in `benchmarks/`; no published results |
|
|
265
265
|
|
|
266
|
-
**
|
|
266
|
+
**v6.0.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
6.0.0
|
|
@@ -259,7 +259,14 @@ council_vote() {
|
|
|
259
259
|
verdict=$(council_member_review "$member" "$role" "$evidence_file" "$vote_dir")
|
|
260
260
|
|
|
261
261
|
local vote_result
|
|
262
|
-
vote_result=$(echo "$verdict" | grep -oE "VOTE:\s*(APPROVE|REJECT)" | grep -oE "APPROVE|REJECT" | head -1)
|
|
262
|
+
vote_result=$(echo "$verdict" | grep -oE "VOTE:\s*(APPROVE|REJECT|CANNOT_VALIDATE)" | grep -oE "APPROVE|REJECT|CANNOT_VALIDATE" | head -1)
|
|
263
|
+
|
|
264
|
+
# v6.0.0: Handle CANNOT_VALIDATE - validator lacks enough context to decide
|
|
265
|
+
if [ "$vote_result" = "CANNOT_VALIDATE" ]; then
|
|
266
|
+
log_warn " Member $member ($role): CANNOT_VALIDATE - insufficient evidence"
|
|
267
|
+
# CANNOT_VALIDATE counts as REJECT (conservative default)
|
|
268
|
+
vote_result="REJECT"
|
|
269
|
+
fi
|
|
263
270
|
|
|
264
271
|
# Extract severity-categorized issues (v5.49.0 error budget)
|
|
265
272
|
local member_issues=""
|
|
@@ -320,9 +327,9 @@ council_vote() {
|
|
|
320
327
|
local contrarian_verdict
|
|
321
328
|
contrarian_verdict=$(council_devils_advocate "$evidence_file" "$vote_dir")
|
|
322
329
|
local contrarian_vote
|
|
323
|
-
contrarian_vote=$(echo "$contrarian_verdict" | grep -oE "VOTE:\s*(APPROVE|REJECT)" | grep -oE "APPROVE|REJECT" | head -1)
|
|
330
|
+
contrarian_vote=$(echo "$contrarian_verdict" | grep -oE "VOTE:\s*(APPROVE|REJECT|CANNOT_VALIDATE)" | grep -oE "APPROVE|REJECT|CANNOT_VALIDATE" | head -1)
|
|
324
331
|
|
|
325
|
-
if [ "$contrarian_vote" = "REJECT" ]; then
|
|
332
|
+
if [ "$contrarian_vote" = "REJECT" ] || [ "$contrarian_vote" = "CANNOT_VALIDATE" ]; then
|
|
326
333
|
log_warn "Anti-sycophancy: Devil's advocate REJECTED unanimous approval"
|
|
327
334
|
log_warn "Overriding to require one more iteration for verification"
|
|
328
335
|
approve_count=$((approve_count - 1))
|
|
@@ -661,6 +668,21 @@ council_member_review() {
|
|
|
661
668
|
local evidence
|
|
662
669
|
evidence=$(cat "$evidence_file" 2>/dev/null || echo "No evidence available")
|
|
663
670
|
|
|
671
|
+
# v6.0.0: Blind validation (default ON) - strip worker iteration context
|
|
672
|
+
# Validators see only: PRD, git state, test results, build artifacts
|
|
673
|
+
# They do NOT see: iteration count, convergence signals, agent done signals
|
|
674
|
+
local blind_mode="${LOKI_BLIND_VALIDATION:-true}"
|
|
675
|
+
if [ "$blind_mode" = "true" ]; then
|
|
676
|
+
# Strip convergence/iteration context that could bias validators
|
|
677
|
+
# Uses awk for macOS/BSD compatibility (sed range syntax differs between GNU/BSD)
|
|
678
|
+
evidence=$(echo "$evidence" | awk '
|
|
679
|
+
/^## Convergence Data/ { skip=1; next }
|
|
680
|
+
/^## / && skip { skip=0 }
|
|
681
|
+
!skip { print }
|
|
682
|
+
')
|
|
683
|
+
log_debug "Blind validation: stripped convergence context for member $member_id"
|
|
684
|
+
fi
|
|
685
|
+
|
|
664
686
|
local verdict=""
|
|
665
687
|
local role_instruction=""
|
|
666
688
|
case "$role" in
|
|
@@ -700,13 +722,14 @@ ${severity_instruction}
|
|
|
700
722
|
INSTRUCTIONS:
|
|
701
723
|
1. Review the evidence carefully
|
|
702
724
|
2. Determine if the project meets completion criteria
|
|
703
|
-
3. Output EXACTLY one line starting with VOTE:APPROVE or VOTE:
|
|
725
|
+
3. Output EXACTLY one line starting with VOTE:APPROVE, VOTE:REJECT, or VOTE:CANNOT_VALIDATE
|
|
704
726
|
4. Output EXACTLY one line starting with REASON: explaining your decision
|
|
705
727
|
5. If issues found, output lines starting with ISSUES: SEVERITY:description
|
|
706
728
|
6. Be honest - do not approve incomplete work
|
|
729
|
+
7. If you lack sufficient evidence to make a determination, vote CANNOT_VALIDATE
|
|
707
730
|
|
|
708
731
|
Output format:
|
|
709
|
-
VOTE:APPROVE or VOTE:REJECT
|
|
732
|
+
VOTE:APPROVE or VOTE:REJECT or VOTE:CANNOT_VALIDATE
|
|
710
733
|
REASON: your reasoning here
|
|
711
734
|
ISSUES: CRITICAL:description (optional, one per line per issue)"
|
|
712
735
|
|
|
@@ -716,12 +739,13 @@ ISSUES: CRITICAL:description (optional, one per line per issue)"
|
|
|
716
739
|
case "${PROVIDER_NAME:-claude}" in
|
|
717
740
|
claude)
|
|
718
741
|
if command -v claude &>/dev/null; then
|
|
719
|
-
|
|
742
|
+
local council_model="${PROVIDER_MODEL_FAST:-haiku}"
|
|
743
|
+
verdict=$(echo "$prompt" | claude --model "$council_model" -p 2>/dev/null | tail -5)
|
|
720
744
|
fi
|
|
721
745
|
;;
|
|
722
746
|
codex)
|
|
723
747
|
if command -v codex &>/dev/null; then
|
|
724
|
-
verdict=$(codex exec -
|
|
748
|
+
verdict=$(codex exec --full-auto "$prompt" 2>/dev/null | tail -5)
|
|
725
749
|
fi
|
|
726
750
|
;;
|
|
727
751
|
gemini)
|
|
@@ -792,12 +816,13 @@ REASON: your reasoning"
|
|
|
792
816
|
case "${PROVIDER_NAME:-claude}" in
|
|
793
817
|
claude)
|
|
794
818
|
if command -v claude &>/dev/null; then
|
|
795
|
-
|
|
819
|
+
local council_model="${PROVIDER_MODEL_FAST:-haiku}"
|
|
820
|
+
verdict=$(echo "$prompt" | claude --model "$council_model" -p 2>/dev/null | tail -5)
|
|
796
821
|
fi
|
|
797
822
|
;;
|
|
798
823
|
codex)
|
|
799
824
|
if command -v codex &>/dev/null; then
|
|
800
|
-
verdict=$(codex exec -
|
|
825
|
+
verdict=$(codex exec --full-auto "$prompt" 2>/dev/null | tail -5)
|
|
801
826
|
fi
|
|
802
827
|
;;
|
|
803
828
|
gemini)
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#===============================================================================
|
|
3
|
+
# Loki Mode - Issue Provider Abstraction (v6.0.0)
|
|
4
|
+
# Multi-provider issue fetching: GitHub, GitLab, Jira, Azure DevOps
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# source autonomy/issue-providers.sh
|
|
8
|
+
# detect_issue_provider "https://github.com/org/repo/issues/123"
|
|
9
|
+
# fetch_issue "https://github.com/org/repo/issues/123"
|
|
10
|
+
# fetch_issue "https://gitlab.com/org/repo/-/issues/42"
|
|
11
|
+
# fetch_issue "https://org.atlassian.net/browse/PROJ-123"
|
|
12
|
+
# fetch_issue "https://dev.azure.com/org/project/_workitems/edit/456"
|
|
13
|
+
#
|
|
14
|
+
# Output:
|
|
15
|
+
# JSON with normalized fields: provider, number, title, body, labels, author, url, created_at
|
|
16
|
+
#===============================================================================
|
|
17
|
+
|
|
18
|
+
# Colors (safe to re-source)
|
|
19
|
+
RED='\033[0;31m'
|
|
20
|
+
GREEN='\033[0;32m'
|
|
21
|
+
YELLOW='\033[1;33m'
|
|
22
|
+
CYAN='\033[0;36m'
|
|
23
|
+
NC='\033[0m'
|
|
24
|
+
|
|
25
|
+
# Supported issue providers
|
|
26
|
+
ISSUE_PROVIDERS=("github" "gitlab" "jira" "azure_devops")
|
|
27
|
+
|
|
28
|
+
# Detect issue provider from a URL or reference
|
|
29
|
+
# Returns: github, gitlab, jira, azure_devops, or "unknown"
|
|
30
|
+
detect_issue_provider() {
|
|
31
|
+
local ref="$1"
|
|
32
|
+
|
|
33
|
+
if [[ "$ref" =~ github\.com ]]; then
|
|
34
|
+
echo "github"
|
|
35
|
+
elif [[ "$ref" =~ gitlab\.com ]] || [[ "$ref" =~ gitlab\. ]]; then
|
|
36
|
+
echo "gitlab"
|
|
37
|
+
elif [[ "$ref" =~ \.atlassian\.net ]] || [[ "$ref" =~ jira\. ]]; then
|
|
38
|
+
echo "jira"
|
|
39
|
+
elif [[ "$ref" =~ dev\.azure\.com ]] || [[ "$ref" =~ visualstudio\.com ]]; then
|
|
40
|
+
echo "azure_devops"
|
|
41
|
+
elif [[ "$ref" =~ ^[0-9]+$ ]] || [[ "$ref" =~ ^#[0-9]+$ ]]; then
|
|
42
|
+
# Bare number - default to GitHub (most common)
|
|
43
|
+
echo "github"
|
|
44
|
+
elif [[ "$ref" =~ ^[^/]+/[^#]+#[0-9]+$ ]]; then
|
|
45
|
+
# owner/repo#N format - GitHub
|
|
46
|
+
echo "github"
|
|
47
|
+
elif [[ "$ref" =~ ^[A-Z]+-[0-9]+$ ]]; then
|
|
48
|
+
# PROJ-123 format - Jira
|
|
49
|
+
echo "jira"
|
|
50
|
+
else
|
|
51
|
+
echo "unknown"
|
|
52
|
+
fi
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Check if required CLI tools are available for a provider
|
|
56
|
+
check_issue_provider_cli() {
|
|
57
|
+
local provider="$1"
|
|
58
|
+
|
|
59
|
+
case "$provider" in
|
|
60
|
+
github)
|
|
61
|
+
if ! command -v gh &>/dev/null; then
|
|
62
|
+
echo -e "${RED}Error: gh CLI not found. Install with: brew install gh${NC}" >&2
|
|
63
|
+
return 1
|
|
64
|
+
fi
|
|
65
|
+
if ! gh auth status &>/dev/null 2>&1; then
|
|
66
|
+
echo -e "${RED}Error: gh CLI not authenticated. Run: gh auth login${NC}" >&2
|
|
67
|
+
return 1
|
|
68
|
+
fi
|
|
69
|
+
;;
|
|
70
|
+
gitlab)
|
|
71
|
+
if ! command -v glab &>/dev/null; then
|
|
72
|
+
echo -e "${RED}Error: glab CLI not found. Install with: brew install glab${NC}" >&2
|
|
73
|
+
return 1
|
|
74
|
+
fi
|
|
75
|
+
;;
|
|
76
|
+
jira)
|
|
77
|
+
# Jira uses REST API via curl - check for JIRA_API_TOKEN
|
|
78
|
+
if [ -z "${JIRA_API_TOKEN:-}" ] && [ -z "${JIRA_TOKEN:-}" ]; then
|
|
79
|
+
echo -e "${RED}Error: JIRA_API_TOKEN or JIRA_TOKEN not set${NC}" >&2
|
|
80
|
+
echo "Set with: export JIRA_API_TOKEN=your-token" >&2
|
|
81
|
+
return 1
|
|
82
|
+
fi
|
|
83
|
+
if [ -z "${JIRA_URL:-}" ] && [ -z "${JIRA_BASE_URL:-}" ]; then
|
|
84
|
+
echo -e "${RED}Error: JIRA_URL or JIRA_BASE_URL not set${NC}" >&2
|
|
85
|
+
echo "Set with: export JIRA_URL=https://your-org.atlassian.net" >&2
|
|
86
|
+
return 1
|
|
87
|
+
fi
|
|
88
|
+
;;
|
|
89
|
+
azure_devops)
|
|
90
|
+
if ! command -v az &>/dev/null; then
|
|
91
|
+
echo -e "${RED}Error: az CLI not found. Install with: brew install azure-cli${NC}" >&2
|
|
92
|
+
return 1
|
|
93
|
+
fi
|
|
94
|
+
;;
|
|
95
|
+
*)
|
|
96
|
+
echo -e "${RED}Error: Unknown issue provider: $provider${NC}" >&2
|
|
97
|
+
return 1
|
|
98
|
+
;;
|
|
99
|
+
esac
|
|
100
|
+
return 0
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# Parse issue reference and extract provider-specific identifiers
|
|
104
|
+
# Sets: ISSUE_PROVIDER, ISSUE_OWNER, ISSUE_REPO, ISSUE_NUMBER, ISSUE_PROJECT, ISSUE_ORG
|
|
105
|
+
parse_issue_reference() {
|
|
106
|
+
local ref="$1"
|
|
107
|
+
|
|
108
|
+
ISSUE_PROVIDER=$(detect_issue_provider "$ref")
|
|
109
|
+
ISSUE_OWNER=""
|
|
110
|
+
ISSUE_REPO=""
|
|
111
|
+
ISSUE_NUMBER=""
|
|
112
|
+
ISSUE_PROJECT=""
|
|
113
|
+
ISSUE_ORG=""
|
|
114
|
+
|
|
115
|
+
case "$ISSUE_PROVIDER" in
|
|
116
|
+
github)
|
|
117
|
+
if [[ "$ref" =~ ^https?://github\.com/([^/]+)/([^/]+)/issues/([0-9]+) ]]; then
|
|
118
|
+
ISSUE_OWNER="${BASH_REMATCH[1]}"
|
|
119
|
+
ISSUE_REPO="${BASH_REMATCH[2]}"
|
|
120
|
+
ISSUE_NUMBER="${BASH_REMATCH[3]}"
|
|
121
|
+
elif [[ "$ref" =~ ^([^/]+)/([^#]+)#([0-9]+)$ ]]; then
|
|
122
|
+
ISSUE_OWNER="${BASH_REMATCH[1]}"
|
|
123
|
+
ISSUE_REPO="${BASH_REMATCH[2]}"
|
|
124
|
+
ISSUE_NUMBER="${BASH_REMATCH[3]}"
|
|
125
|
+
elif [[ "$ref" =~ ^#?([0-9]+)$ ]]; then
|
|
126
|
+
ISSUE_NUMBER="${BASH_REMATCH[1]}"
|
|
127
|
+
# Auto-detect repo from git remote
|
|
128
|
+
local remote_repo
|
|
129
|
+
remote_repo=$(gh repo view --json nameWithOwner -q '.nameWithOwner' 2>/dev/null) || true
|
|
130
|
+
if [ -n "$remote_repo" ]; then
|
|
131
|
+
ISSUE_OWNER="${remote_repo%%/*}"
|
|
132
|
+
ISSUE_REPO="${remote_repo##*/}"
|
|
133
|
+
fi
|
|
134
|
+
fi
|
|
135
|
+
;;
|
|
136
|
+
gitlab)
|
|
137
|
+
if [[ "$ref" =~ ^https?://[^/]+/(.+)/([^/]+)/-/issues/([0-9]+) ]]; then
|
|
138
|
+
ISSUE_OWNER="${BASH_REMATCH[1]}"
|
|
139
|
+
ISSUE_REPO="${BASH_REMATCH[2]}"
|
|
140
|
+
ISSUE_NUMBER="${BASH_REMATCH[3]}"
|
|
141
|
+
elif [[ "$ref" =~ ^#?([0-9]+)$ ]]; then
|
|
142
|
+
ISSUE_NUMBER="${BASH_REMATCH[1]}"
|
|
143
|
+
fi
|
|
144
|
+
;;
|
|
145
|
+
jira)
|
|
146
|
+
if [[ "$ref" =~ /browse/([A-Z]+-[0-9]+) ]]; then
|
|
147
|
+
ISSUE_NUMBER="${BASH_REMATCH[1]}"
|
|
148
|
+
elif [[ "$ref" =~ ^([A-Z]+-[0-9]+)$ ]]; then
|
|
149
|
+
ISSUE_NUMBER="$ref"
|
|
150
|
+
fi
|
|
151
|
+
ISSUE_PROJECT="${ISSUE_NUMBER%%-*}"
|
|
152
|
+
;;
|
|
153
|
+
azure_devops)
|
|
154
|
+
if [[ "$ref" =~ dev\.azure\.com/([^/]+)/([^/]+)/_workitems/edit/([0-9]+) ]]; then
|
|
155
|
+
ISSUE_ORG="${BASH_REMATCH[1]}"
|
|
156
|
+
ISSUE_PROJECT="${BASH_REMATCH[2]}"
|
|
157
|
+
ISSUE_NUMBER="${BASH_REMATCH[3]}"
|
|
158
|
+
fi
|
|
159
|
+
;;
|
|
160
|
+
esac
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# Fetch issue from GitHub using gh CLI
|
|
164
|
+
# Output: normalized JSON
|
|
165
|
+
fetch_github_issue() {
|
|
166
|
+
local owner="$ISSUE_OWNER"
|
|
167
|
+
local repo="$ISSUE_REPO"
|
|
168
|
+
local number="$ISSUE_NUMBER"
|
|
169
|
+
|
|
170
|
+
local repo_ref=""
|
|
171
|
+
if [ -n "$owner" ] && [ -n "$repo" ]; then
|
|
172
|
+
repo_ref="$owner/$repo"
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
local issue_json
|
|
176
|
+
if [ -n "$repo_ref" ]; then
|
|
177
|
+
issue_json=$(gh issue view "$number" --repo "$repo_ref" --json number,title,body,labels,author,createdAt,url 2>&1) || {
|
|
178
|
+
echo -e "${RED}Error fetching GitHub issue: $issue_json${NC}" >&2
|
|
179
|
+
return 1
|
|
180
|
+
}
|
|
181
|
+
else
|
|
182
|
+
issue_json=$(gh issue view "$number" --json number,title,body,labels,author,createdAt,url 2>&1) || {
|
|
183
|
+
echo -e "${RED}Error fetching GitHub issue: $issue_json${NC}" >&2
|
|
184
|
+
return 1
|
|
185
|
+
}
|
|
186
|
+
fi
|
|
187
|
+
|
|
188
|
+
# Normalize to common format (pass repo via env to prevent injection)
|
|
189
|
+
_LOKI_REPO_REF="$repo_ref" python3 -c "
|
|
190
|
+
import json, sys, os
|
|
191
|
+
data = json.loads(sys.stdin.read())
|
|
192
|
+
print(json.dumps({
|
|
193
|
+
'provider': 'github',
|
|
194
|
+
'number': data.get('number', 0),
|
|
195
|
+
'title': data.get('title', ''),
|
|
196
|
+
'body': data.get('body', '') or '',
|
|
197
|
+
'labels': [l.get('name','') for l in data.get('labels', [])],
|
|
198
|
+
'author': data.get('author', {}).get('login', ''),
|
|
199
|
+
'url': data.get('url', ''),
|
|
200
|
+
'created_at': data.get('createdAt', ''),
|
|
201
|
+
'repo': os.environ.get('_LOKI_REPO_REF', '')
|
|
202
|
+
}))
|
|
203
|
+
" <<< "$issue_json"
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
# Fetch issue from GitLab using glab CLI
|
|
207
|
+
fetch_gitlab_issue() {
|
|
208
|
+
local number="$ISSUE_NUMBER"
|
|
209
|
+
local repo_ref=""
|
|
210
|
+
if [ -n "$ISSUE_OWNER" ] && [ -n "$ISSUE_REPO" ]; then
|
|
211
|
+
repo_ref="$ISSUE_OWNER/$ISSUE_REPO"
|
|
212
|
+
fi
|
|
213
|
+
|
|
214
|
+
local issue_json
|
|
215
|
+
if [ -n "$repo_ref" ]; then
|
|
216
|
+
issue_json=$(glab issue view "$number" --repo "$repo_ref" --output json 2>&1) || {
|
|
217
|
+
echo -e "${RED}Error fetching GitLab issue: $issue_json${NC}" >&2
|
|
218
|
+
return 1
|
|
219
|
+
}
|
|
220
|
+
else
|
|
221
|
+
issue_json=$(glab issue view "$number" --output json 2>&1) || {
|
|
222
|
+
echo -e "${RED}Error fetching GitLab issue: $issue_json${NC}" >&2
|
|
223
|
+
return 1
|
|
224
|
+
}
|
|
225
|
+
fi
|
|
226
|
+
|
|
227
|
+
_LOKI_REPO_REF="$repo_ref" python3 -c "
|
|
228
|
+
import json, sys, os
|
|
229
|
+
data = json.loads(sys.stdin.read())
|
|
230
|
+
print(json.dumps({
|
|
231
|
+
'provider': 'gitlab',
|
|
232
|
+
'number': data.get('iid', 0),
|
|
233
|
+
'title': data.get('title', ''),
|
|
234
|
+
'body': data.get('description', '') or '',
|
|
235
|
+
'labels': data.get('labels', []),
|
|
236
|
+
'author': (data.get('author') or {}).get('username', ''),
|
|
237
|
+
'url': data.get('web_url', ''),
|
|
238
|
+
'created_at': data.get('created_at', ''),
|
|
239
|
+
'repo': os.environ.get('_LOKI_REPO_REF', '')
|
|
240
|
+
}))
|
|
241
|
+
" <<< "$issue_json"
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
# Fetch issue from Jira using REST API
|
|
245
|
+
fetch_jira_issue() {
|
|
246
|
+
local issue_key="$ISSUE_NUMBER"
|
|
247
|
+
local base_url="${JIRA_URL:-${JIRA_BASE_URL:-}}"
|
|
248
|
+
local token="${JIRA_API_TOKEN:-${JIRA_TOKEN:-}}"
|
|
249
|
+
local email="${JIRA_EMAIL:-${JIRA_USER:-}}"
|
|
250
|
+
|
|
251
|
+
local auth_header=""
|
|
252
|
+
if [ -n "$email" ]; then
|
|
253
|
+
# Jira Cloud: Basic auth with email:token
|
|
254
|
+
local encoded
|
|
255
|
+
encoded=$(printf '%s:%s' "$email" "$token" | base64 | tr -d '\n')
|
|
256
|
+
auth_header="Authorization: Basic $encoded"
|
|
257
|
+
else
|
|
258
|
+
# Bearer token
|
|
259
|
+
auth_header="Authorization: Bearer $token"
|
|
260
|
+
fi
|
|
261
|
+
|
|
262
|
+
local issue_json
|
|
263
|
+
issue_json=$(curl -sf -H "$auth_header" -H "Content-Type: application/json" \
|
|
264
|
+
"${base_url}/rest/api/2/issue/${issue_key}?fields=summary,description,labels,reporter,created" 2>&1) || {
|
|
265
|
+
echo -e "${RED}Error fetching Jira issue: $issue_json${NC}" >&2
|
|
266
|
+
return 1
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
_LOKI_BASE_URL="$base_url" _LOKI_ISSUE_KEY="$issue_key" _LOKI_PROJECT="$ISSUE_PROJECT" python3 -c "
|
|
270
|
+
import json, sys, os
|
|
271
|
+
data = json.loads(sys.stdin.read())
|
|
272
|
+
fields = data.get('fields', {})
|
|
273
|
+
base_url = os.environ.get('_LOKI_BASE_URL', '')
|
|
274
|
+
issue_key = os.environ.get('_LOKI_ISSUE_KEY', '')
|
|
275
|
+
project = os.environ.get('_LOKI_PROJECT', '')
|
|
276
|
+
reporter = fields.get('reporter') or {}
|
|
277
|
+
print(json.dumps({
|
|
278
|
+
'provider': 'jira',
|
|
279
|
+
'number': data.get('key', ''),
|
|
280
|
+
'title': fields.get('summary', ''),
|
|
281
|
+
'body': fields.get('description', '') or '',
|
|
282
|
+
'labels': fields.get('labels', []),
|
|
283
|
+
'author': reporter.get('displayName', '') if isinstance(reporter, dict) else '',
|
|
284
|
+
'url': f'{base_url}/browse/{issue_key}',
|
|
285
|
+
'created_at': fields.get('created', ''),
|
|
286
|
+
'repo': project
|
|
287
|
+
}))
|
|
288
|
+
" <<< "$issue_json"
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
# Fetch issue from Azure DevOps using az CLI
|
|
292
|
+
fetch_azure_devops_issue() {
|
|
293
|
+
local org="$ISSUE_ORG"
|
|
294
|
+
local project="$ISSUE_PROJECT"
|
|
295
|
+
local id="$ISSUE_NUMBER"
|
|
296
|
+
|
|
297
|
+
local issue_json
|
|
298
|
+
issue_json=$(az boards work-item show --id "$id" --org "https://dev.azure.com/$org" --output json 2>&1) || {
|
|
299
|
+
echo -e "${RED}Error fetching Azure DevOps work item: $issue_json${NC}" >&2
|
|
300
|
+
return 1
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
_LOKI_PROJECT="$project" python3 -c "
|
|
304
|
+
import json, sys, os
|
|
305
|
+
data = json.loads(sys.stdin.read())
|
|
306
|
+
fields = data.get('fields', {})
|
|
307
|
+
project = os.environ.get('_LOKI_PROJECT', '')
|
|
308
|
+
created_by = fields.get('System.CreatedBy', '')
|
|
309
|
+
author = created_by.get('displayName', '') if isinstance(created_by, dict) else str(created_by)
|
|
310
|
+
tags = fields.get('System.Tags', '')
|
|
311
|
+
print(json.dumps({
|
|
312
|
+
'provider': 'azure_devops',
|
|
313
|
+
'number': data.get('id', 0),
|
|
314
|
+
'title': fields.get('System.Title', ''),
|
|
315
|
+
'body': fields.get('System.Description', '') or '',
|
|
316
|
+
'labels': [t.strip() for t in tags.split(';') if t.strip()] if tags else [],
|
|
317
|
+
'author': author,
|
|
318
|
+
'url': data.get('_links', {}).get('html', {}).get('href', ''),
|
|
319
|
+
'created_at': fields.get('System.CreatedDate', ''),
|
|
320
|
+
'repo': project
|
|
321
|
+
}))
|
|
322
|
+
" <<< "$issue_json"
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
# Main entry point: fetch issue from any supported provider
|
|
326
|
+
# Usage: fetch_issue "reference-or-url"
|
|
327
|
+
# Output: normalized JSON on stdout
|
|
328
|
+
fetch_issue() {
|
|
329
|
+
local ref="$1"
|
|
330
|
+
|
|
331
|
+
parse_issue_reference "$ref"
|
|
332
|
+
|
|
333
|
+
if [ "$ISSUE_PROVIDER" = "unknown" ]; then
|
|
334
|
+
echo -e "${RED}Error: Could not detect issue provider from: $ref${NC}" >&2
|
|
335
|
+
echo "Supported formats:" >&2
|
|
336
|
+
echo " GitHub: https://github.com/owner/repo/issues/123 or owner/repo#123 or #123" >&2
|
|
337
|
+
echo " GitLab: https://gitlab.com/owner/repo/-/issues/42" >&2
|
|
338
|
+
echo " Jira: https://org.atlassian.net/browse/PROJ-123 or PROJ-123" >&2
|
|
339
|
+
echo " Azure DevOps: https://dev.azure.com/org/project/_workitems/edit/456" >&2
|
|
340
|
+
return 1
|
|
341
|
+
fi
|
|
342
|
+
|
|
343
|
+
if [ -z "$ISSUE_NUMBER" ]; then
|
|
344
|
+
echo -e "${RED}Error: Could not parse issue number from: $ref${NC}" >&2
|
|
345
|
+
return 1
|
|
346
|
+
fi
|
|
347
|
+
|
|
348
|
+
check_issue_provider_cli "$ISSUE_PROVIDER" || return 1
|
|
349
|
+
|
|
350
|
+
case "$ISSUE_PROVIDER" in
|
|
351
|
+
github) fetch_github_issue ;;
|
|
352
|
+
gitlab) fetch_gitlab_issue ;;
|
|
353
|
+
jira) fetch_jira_issue ;;
|
|
354
|
+
azure_devops) fetch_azure_devops_issue ;;
|
|
355
|
+
esac
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
# Generate PRD from normalized issue JSON
|
|
359
|
+
# Input: normalized issue JSON on stdin
|
|
360
|
+
# Output: PRD markdown on stdout
|
|
361
|
+
generate_prd_from_issue() {
|
|
362
|
+
python3 -c "
|
|
363
|
+
import json, sys
|
|
364
|
+
|
|
365
|
+
data = json.loads(sys.stdin.read())
|
|
366
|
+
provider = data.get('provider', 'unknown')
|
|
367
|
+
number = data.get('number', '')
|
|
368
|
+
title = data.get('title', '')
|
|
369
|
+
body = data.get('body', '')
|
|
370
|
+
labels = data.get('labels', [])
|
|
371
|
+
author = data.get('author', '')
|
|
372
|
+
url = data.get('url', '')
|
|
373
|
+
created_at = data.get('created_at', '')
|
|
374
|
+
repo = data.get('repo', '')
|
|
375
|
+
|
|
376
|
+
labels_str = ', '.join(labels) if labels else ''
|
|
377
|
+
|
|
378
|
+
prd = f'''# PRD: {title}
|
|
379
|
+
|
|
380
|
+
**Source:** {provider.replace('_', ' ').title()} Issue [{number}]({url})
|
|
381
|
+
**Author:** {author}
|
|
382
|
+
**Created:** {created_at}
|
|
383
|
+
'''
|
|
384
|
+
if labels_str:
|
|
385
|
+
prd += f'**Labels:** {labels_str}\n'
|
|
386
|
+
if repo:
|
|
387
|
+
prd += f'**Repository:** {repo}\n'
|
|
388
|
+
|
|
389
|
+
prd += f'''
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## Overview
|
|
393
|
+
|
|
394
|
+
{body}
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## Acceptance Criteria
|
|
399
|
+
|
|
400
|
+
Based on the issue description, implement the following:
|
|
401
|
+
|
|
402
|
+
1. Address all requirements specified in the issue body above
|
|
403
|
+
2. Ensure backward compatibility (unless explicitly breaking changes are requested)
|
|
404
|
+
3. Add appropriate tests for new functionality
|
|
405
|
+
4. Update documentation as needed
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## Technical Notes
|
|
410
|
+
|
|
411
|
+
- Source: {provider.replace('_', ' ').title()} Issue {number}
|
|
412
|
+
- Repository: {repo}
|
|
413
|
+
- Generated by: Loki Mode CLI v6.0.0
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## References
|
|
418
|
+
|
|
419
|
+
- Original Issue: {url}
|
|
420
|
+
'''
|
|
421
|
+
print(prd)
|
|
422
|
+
"
|
|
423
|
+
}
|