specweave 1.0.300 → 1.0.301
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/dist/src/cli/commands/auto.js +1 -0
- package/dist/src/cli/commands/auto.js.map +1 -1
- package/dist/src/core/lazy-loading/llm-plugin-detector.d.ts +8 -27
- package/dist/src/core/lazy-loading/llm-plugin-detector.d.ts.map +1 -1
- package/dist/src/core/lazy-loading/llm-plugin-detector.js +12 -90
- package/dist/src/core/lazy-loading/llm-plugin-detector.js.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave/hooks/lib/score-increment.sh +87 -0
- package/plugins/specweave/hooks/stop-auto-v5.sh +55 -9
- package/plugins/specweave/hooks/tests/test-auto-context-integration.sh +126 -0
- package/plugins/specweave/hooks/tests/test-stop-auto-enriched.sh +128 -0
- package/plugins/specweave/hooks/user-prompt-submit.sh +89 -150
- package/plugins/specweave/scripts/setup-auto.sh +58 -4
- package/plugins/specweave/scripts/tests/test-setup-auto-selection.sh +74 -0
- package/plugins/specweave/scripts/tests/test-setup-auto-usergoal.sh +83 -0
- package/plugins/specweave/skills/auto/SKILL.md +3 -1
- package/plugins/specweave/skills/do/SKILL.md +11 -0
- package/plugins/specweave-jira/skills/jira-mapper/SKILL.md +13 -14
- package/plugins/specweave-jira/skills/jira-resource-validator/SKILL.md +74 -4
- package/plugins/specweave-jira/skills/jira-sync/SKILL.md +18 -27
|
@@ -249,21 +249,61 @@ if [ "$ALL_BACKLOG" = "true" ]; then
|
|
|
249
249
|
fi
|
|
250
250
|
fi
|
|
251
251
|
|
|
252
|
-
# If no increments specified, find current in-progress increment
|
|
252
|
+
# If no increments specified, find current in-progress increment(s) via intent scoring
|
|
253
253
|
if [ ${#INCREMENT_IDS[@]} -eq 0 ]; then
|
|
254
|
+
# Collect ALL active/in-progress increments (no blind first-match break)
|
|
255
|
+
_ACTIVE_DIRS=()
|
|
254
256
|
for dir in "$INCREMENTS_DIR"/[0-9][0-9][0-9][0-9]-*/; do
|
|
255
257
|
if [ -d "$dir" ]; then
|
|
256
258
|
META_FILE="$dir/metadata.json"
|
|
257
259
|
if [ -f "$META_FILE" ]; then
|
|
258
260
|
STATUS=$(jq -r '.status' "$META_FILE" 2>/dev/null || echo "")
|
|
259
261
|
if [ "$STATUS" = "active" ] || [ "$STATUS" = "in-progress" ]; then
|
|
260
|
-
|
|
261
|
-
INCREMENT_IDS+=("$INCREMENT_ID")
|
|
262
|
-
break
|
|
262
|
+
_ACTIVE_DIRS+=("$dir")
|
|
263
263
|
fi
|
|
264
264
|
fi
|
|
265
265
|
fi
|
|
266
266
|
done
|
|
267
|
+
|
|
268
|
+
if [ ${#_ACTIVE_DIRS[@]} -eq 1 ]; then
|
|
269
|
+
# Fast path: single active increment, no scoring needed
|
|
270
|
+
INCREMENT_IDS+=("$(basename "${_ACTIVE_DIRS[0]}")")
|
|
271
|
+
elif [ ${#_ACTIVE_DIRS[@]} -gt 1 ]; then
|
|
272
|
+
select_best_increment() {
|
|
273
|
+
local prompt_text="$1"; shift; local dirs=("$@")
|
|
274
|
+
local _setup_dir; _setup_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
275
|
+
local _score_script="$_setup_dir/../hooks/lib/score-increment.sh"
|
|
276
|
+
|
|
277
|
+
if [ -n "$prompt_text" ] && [ -f "$_score_script" ]; then
|
|
278
|
+
# Score each increment against the prompt, pick best
|
|
279
|
+
local _best_score=-1 _best_dir=""
|
|
280
|
+
for _dir in "${dirs[@]}"; do
|
|
281
|
+
local _score; _score=$(bash "$_score_script" "$_dir" "$prompt_text" 2>/dev/null || echo "0")
|
|
282
|
+
if [ "$_score" -gt "$_best_score" ]; then
|
|
283
|
+
_best_score="$_score"; _best_dir="$_dir"
|
|
284
|
+
fi
|
|
285
|
+
done
|
|
286
|
+
local _sel; _sel=$(basename "$_best_dir")
|
|
287
|
+
echo "🎯 Selected '$_sel' by intent match (score: $_best_score/100)" >&2
|
|
288
|
+
echo "{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"event\":\"increment_selected\",\"increment\":\"$_sel\",\"method\":\"intent_scoring\",\"score\":$_best_score}" >> "$LOGS_DIR/auto-sessions.log"
|
|
289
|
+
echo "$_sel"
|
|
290
|
+
else
|
|
291
|
+
# No prompt: pick most-recently-modified increment
|
|
292
|
+
local _latest_mtime=0 _latest_dir=""
|
|
293
|
+
for _dir in "${dirs[@]}"; do
|
|
294
|
+
local _mtime; _mtime=$(stat -f%m "$_dir/metadata.json" 2>/dev/null || stat -c%Y "$_dir/metadata.json" 2>/dev/null || echo "0")
|
|
295
|
+
if [ "$_mtime" -gt "$_latest_mtime" ]; then
|
|
296
|
+
_latest_mtime="$_mtime"; _latest_dir="$_dir"
|
|
297
|
+
fi
|
|
298
|
+
done
|
|
299
|
+
local _sel; _sel=$(basename "${_latest_dir:-${dirs[0]}}")
|
|
300
|
+
echo "📅 Selected '$_sel' by most recent activity" >&2
|
|
301
|
+
echo "{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"event\":\"increment_selected\",\"increment\":\"$_sel\",\"method\":\"recent_activity\"}" >> "$LOGS_DIR/auto-sessions.log"
|
|
302
|
+
echo "$_sel"
|
|
303
|
+
fi
|
|
304
|
+
}
|
|
305
|
+
INCREMENT_IDS+=("$(select_best_increment "$PROMPT" "${_ACTIVE_DIRS[@]}")")
|
|
306
|
+
fi
|
|
267
307
|
fi
|
|
268
308
|
|
|
269
309
|
if [ ${#INCREMENT_IDS[@]} -eq 0 ]; then
|
|
@@ -472,6 +512,20 @@ echo "$SESSION_JSON" | jq . > "$SESSION_FILE"
|
|
|
472
512
|
# Log session start
|
|
473
513
|
echo "{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"event\":\"session_start\",\"sessionId\":\"$SESSION_ID\",\"increments\":${#INCREMENT_IDS[@]}}" >> "$LOGS_DIR/auto-sessions.log"
|
|
474
514
|
|
|
515
|
+
# Write userGoal to auto-mode.json BEFORE the session start banner
|
|
516
|
+
AUTO_MODE_FILE="$STATE_DIR/auto-mode.json"
|
|
517
|
+
if [ -f "$AUTO_MODE_FILE" ]; then
|
|
518
|
+
if [ -n "$PROMPT" ]; then
|
|
519
|
+
_UPDATED_AM=$(jq --arg g "$PROMPT" '.userGoal = $g' "$AUTO_MODE_FILE" 2>/dev/null)
|
|
520
|
+
else
|
|
521
|
+
_UPDATED_AM=$(jq '.userGoal = null' "$AUTO_MODE_FILE" 2>/dev/null)
|
|
522
|
+
fi
|
|
523
|
+
[ -n "$_UPDATED_AM" ] && echo "$_UPDATED_AM" > "$AUTO_MODE_FILE"
|
|
524
|
+
elif [ -n "$PROMPT" ]; then
|
|
525
|
+
# Create stub so stop hook can read userGoal even before LLM writes full auto-mode.json
|
|
526
|
+
jq -n --arg g "$PROMPT" '{"active":false,"userGoal":$g}' > "$AUTO_MODE_FILE"
|
|
527
|
+
fi
|
|
528
|
+
|
|
475
529
|
# Output - Session Start Banner
|
|
476
530
|
echo ""
|
|
477
531
|
echo "╔══════════════════════════════════════════════════════════════════════════════╗"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for scored increment selection in setup-auto.sh (AC-US1-01, AC-US1-03, AC-US1-04)
|
|
3
|
+
# Uses score-increment.sh directly to verify selection logic.
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
SCORE_SCRIPT="$SCRIPT_DIR/../../hooks/lib/score-increment.sh"
|
|
9
|
+
TMPDIR_ROOT=$(mktemp -d)
|
|
10
|
+
trap 'rm -rf "$TMPDIR_ROOT"' EXIT
|
|
11
|
+
|
|
12
|
+
PASS=0; FAIL=0
|
|
13
|
+
|
|
14
|
+
assert_eq() {
|
|
15
|
+
if [ "$1" = "$2" ]; then
|
|
16
|
+
echo " ✓ $3"; PASS=$((PASS+1))
|
|
17
|
+
else
|
|
18
|
+
echo " ✗ $3 (expected '$2', got '$1')"; FAIL=$((FAIL+1))
|
|
19
|
+
fi
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
assert_gt() {
|
|
23
|
+
if [ "$1" -gt "$2" ]; then
|
|
24
|
+
echo " ✓ $3 ($1 > $2)"; PASS=$((PASS+1))
|
|
25
|
+
else
|
|
26
|
+
echo " ✗ $3 (expected $1 > $2)"; FAIL=$((FAIL+1))
|
|
27
|
+
fi
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
make_increment() {
|
|
31
|
+
local dir="$TMPDIR_ROOT/$1"
|
|
32
|
+
mkdir -p "$dir"
|
|
33
|
+
echo "{\"title\":\"$2\",\"status\":\"active\",\"lastActivity\":\"$5\"}" > "$dir/metadata.json"
|
|
34
|
+
printf "# %s\n\n%s\n" "$2" "$3" > "$dir/spec.md"
|
|
35
|
+
printf "### T-001: %s\n**Status**: [ ] pending\n" "$4" > "$dir/tasks.md"
|
|
36
|
+
echo "$dir"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
echo "scored increment selection tests"
|
|
40
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
41
|
+
|
|
42
|
+
# TC-007: Prompt-based selection picks best match
|
|
43
|
+
d1=$(make_increment "0100-user-auth" "Authentication Login Feature" "Implement user authentication and login" "Add login endpoint" "2026-02-18T10:00:00Z")
|
|
44
|
+
d2=$(make_increment "0101-deploy-pipeline" "CI/CD Deploy Pipeline" "Setup deployment pipeline with Docker" "Configure CI workflow" "2026-02-19T10:00:00Z")
|
|
45
|
+
|
|
46
|
+
s1=$(bash "$SCORE_SCRIPT" "$d1" "authentication login" 2>/dev/null)
|
|
47
|
+
s2=$(bash "$SCORE_SCRIPT" "$d2" "authentication login" 2>/dev/null)
|
|
48
|
+
assert_gt "$s1" "$s2" "TC-007: auth scores higher than deploy for 'authentication login'"
|
|
49
|
+
|
|
50
|
+
# TC-007b: Reverse -- deploy query should score deploy higher
|
|
51
|
+
s3=$(bash "$SCORE_SCRIPT" "$d1" "deploy pipeline docker" 2>/dev/null)
|
|
52
|
+
s4=$(bash "$SCORE_SCRIPT" "$d2" "deploy pipeline docker" 2>/dev/null)
|
|
53
|
+
assert_gt "$s4" "$s3" "TC-007b: deploy scores higher than auth for 'deploy pipeline docker'"
|
|
54
|
+
|
|
55
|
+
# TC-008: Single increment -- score still works (fast path skips scoring in setup-auto.sh)
|
|
56
|
+
d3=$(make_increment "0102-single" "Single Active Feature" "Only one active increment exists" "Implement feature" "2026-02-19T12:00:00Z")
|
|
57
|
+
result=$(bash "$SCORE_SCRIPT" "$d3" "single feature" 2>/dev/null)
|
|
58
|
+
echo "$result" | grep -qE '^[0-9]+$' && \
|
|
59
|
+
echo " ✓ TC-008: single increment scoring returns integer ($result)" && PASS=$((PASS+1)) || \
|
|
60
|
+
{ echo " ✗ TC-008: non-integer output: '$result'"; FAIL=$((FAIL+1)); }
|
|
61
|
+
|
|
62
|
+
# TC-009: Empty query → 0 for all (triggers mtime fallback in setup-auto.sh)
|
|
63
|
+
s5=$(bash "$SCORE_SCRIPT" "$d1" "" 2>/dev/null)
|
|
64
|
+
s6=$(bash "$SCORE_SCRIPT" "$d2" "" 2>/dev/null)
|
|
65
|
+
assert_eq "$s5" "0" "TC-009: empty query → 0 for increment A"
|
|
66
|
+
assert_eq "$s6" "0" "TC-009b: empty query → 0 for increment B"
|
|
67
|
+
|
|
68
|
+
# TC-X: Scoring is deterministic (same inputs → same output)
|
|
69
|
+
s7=$(bash "$SCORE_SCRIPT" "$d1" "authentication login" 2>/dev/null)
|
|
70
|
+
assert_eq "$s7" "$s1" "TC-X: scoring is deterministic"
|
|
71
|
+
|
|
72
|
+
echo ""
|
|
73
|
+
echo "Results: $PASS passed, $FAIL failed"
|
|
74
|
+
[ "$FAIL" -eq 0 ] || exit 1
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for userGoal wiring in setup-auto.sh (AC-US2-01, AC-US2-02, AC-US2-04)
|
|
3
|
+
# Tests the logic directly without running the full setup-auto.sh.
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
TMPDIR_ROOT=$(mktemp -d)
|
|
8
|
+
trap 'rm -rf "$TMPDIR_ROOT"' EXIT
|
|
9
|
+
|
|
10
|
+
PASS=0; FAIL=0
|
|
11
|
+
|
|
12
|
+
assert_eq() {
|
|
13
|
+
if [ "$1" = "$2" ]; then
|
|
14
|
+
echo " ✓ $3"; PASS=$((PASS+1))
|
|
15
|
+
else
|
|
16
|
+
echo " ✗ $3 (expected '$2', got '$1')"; FAIL=$((FAIL+1))
|
|
17
|
+
fi
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
# Helper: run userGoal wiring logic (extracted from setup-auto.sh)
|
|
21
|
+
write_user_goal() {
|
|
22
|
+
local prompt="$1" file="$2"
|
|
23
|
+
if [ -f "$file" ]; then
|
|
24
|
+
if [ -n "$prompt" ]; then
|
|
25
|
+
local updated; updated=$(jq --arg g "$prompt" '.userGoal = $g' "$file" 2>/dev/null)
|
|
26
|
+
else
|
|
27
|
+
local updated; updated=$(jq '.userGoal = null' "$file" 2>/dev/null)
|
|
28
|
+
fi
|
|
29
|
+
[ -n "$updated" ] && echo "$updated" > "$file"
|
|
30
|
+
elif [ -n "$prompt" ]; then
|
|
31
|
+
jq -n --arg g "$prompt" '{"active":false,"userGoal":$g}' > "$file"
|
|
32
|
+
fi
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
echo "setup-auto.sh userGoal wiring tests"
|
|
36
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
37
|
+
|
|
38
|
+
# TC-005: Prompt sets userGoal on existing file
|
|
39
|
+
AM="$TMPDIR_ROOT/auto-mode.json"
|
|
40
|
+
echo '{"active":false}' > "$AM"
|
|
41
|
+
write_user_goal "fix auth bug" "$AM"
|
|
42
|
+
result=$(jq -r '.userGoal' "$AM")
|
|
43
|
+
assert_eq "$result" "fix auth bug" "TC-005: prompt sets userGoal"
|
|
44
|
+
|
|
45
|
+
# TC-006: No prompt sets userGoal to null on existing file
|
|
46
|
+
echo '{"active":false,"userGoal":"old goal"}' > "$AM"
|
|
47
|
+
write_user_goal "" "$AM"
|
|
48
|
+
result=$(jq -r '.userGoal' "$AM")
|
|
49
|
+
assert_eq "$result" "null" "TC-006: no prompt → userGoal null"
|
|
50
|
+
|
|
51
|
+
# TC-X: Prompt creates new file when absent
|
|
52
|
+
rm -f "$AM"
|
|
53
|
+
write_user_goal "add payment feature" "$AM"
|
|
54
|
+
result=$(jq -r '.userGoal' "$AM")
|
|
55
|
+
assert_eq "$result" "add payment feature" "TC-X1: creates file with userGoal"
|
|
56
|
+
|
|
57
|
+
# TC-X: No prompt does NOT create file when absent (userGoal null only on existing files)
|
|
58
|
+
rm -f "$AM"
|
|
59
|
+
write_user_goal "" "$AM"
|
|
60
|
+
if [ -f "$AM" ]; then
|
|
61
|
+
result=$(jq -r '.userGoal' "$AM")
|
|
62
|
+
assert_eq "$result" "null" "TC-X2: no-prompt no-file → file absent or null"
|
|
63
|
+
else
|
|
64
|
+
echo " ✓ TC-X2: no-prompt no-file → no file created"; PASS=$((PASS+1))
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# TC-X: Prompt preserves other fields in existing auto-mode.json
|
|
68
|
+
echo '{"active":true,"incrementIds":["0100-auth"],"tddMode":false}' > "$AM"
|
|
69
|
+
write_user_goal "fix login" "$AM"
|
|
70
|
+
active=$(jq -r '.active' "$AM")
|
|
71
|
+
goal=$(jq -r '.userGoal' "$AM")
|
|
72
|
+
assert_eq "$active" "true" "TC-X3: existing fields preserved after goal write"
|
|
73
|
+
assert_eq "$goal" "fix login" "TC-X3: userGoal set correctly"
|
|
74
|
+
|
|
75
|
+
# TC-X: Special characters in prompt are handled safely
|
|
76
|
+
echo '{"active":false}' > "$AM"
|
|
77
|
+
write_user_goal 'fix "tricky" bug & deploy' "$AM"
|
|
78
|
+
result=$(jq -r '.userGoal' "$AM")
|
|
79
|
+
assert_eq "$result" 'fix "tricky" bug & deploy' "TC-X4: special chars in prompt"
|
|
80
|
+
|
|
81
|
+
echo ""
|
|
82
|
+
echo "Results: $PASS passed, $FAIL failed"
|
|
83
|
+
[ "$FAIL" -eq 0 ] || exit 1
|
|
@@ -75,7 +75,7 @@ Use Read/Write/Edit/Glob tools directly (no CLI needed):
|
|
|
75
75
|
"incrementIds": ["0001-feature"],
|
|
76
76
|
"tddMode": false,
|
|
77
77
|
"requireTests": false,
|
|
78
|
-
"userGoal":
|
|
78
|
+
"userGoal": null,
|
|
79
79
|
"successCriteria": [
|
|
80
80
|
{ "type": "tasks_complete", "description": "All tasks marked complete", "required": true },
|
|
81
81
|
{ "type": "acs_satisfied", "description": "All ACs satisfied", "required": true }
|
|
@@ -96,6 +96,8 @@ Map flags to extra `successCriteria` entries:
|
|
|
96
96
|
|
|
97
97
|
Always include `tasks_complete` and `acs_satisfied` as base criteria. Ensure `.specweave/state/` dir exists.
|
|
98
98
|
|
|
99
|
+
**`userGoal` field**: Set to the user's stated intent from conversation context. If the user said "fix the auth bug", set `userGoal` to `"fix the auth bug"`. If no clear intent is expressed, set to `null`. This field is read by the stop hook to provide context-aware feedback and guide `/sw:do` to the correct increment.
|
|
100
|
+
|
|
99
101
|
### Step 1.5a: MANDATORY - Complexity Check for Team-Lead Routing
|
|
100
102
|
|
|
101
103
|
**Before starting autonomous execution, check if this increment needs team-lead:**
|
|
@@ -49,6 +49,17 @@ When no ID provided, auto-select (NEVER ask user for ID):
|
|
|
49
49
|
3. Select best candidate and auto-promote to in-progress if needed
|
|
50
50
|
4. If no candidates, show status summary and offer: create new, close ready_for_review, resume backlog, or view status
|
|
51
51
|
|
|
52
|
+
### Step 1.5: Auto-Mode Context Override
|
|
53
|
+
|
|
54
|
+
When running inside an active auto session (`.specweave/state/auto-mode.json` has `active: true`):
|
|
55
|
+
|
|
56
|
+
1. **Explicit ID takes priority**: If an explicit increment ID was passed (e.g., `/sw:do 0252`), use it directly — skip this step
|
|
57
|
+
2. **Stop hook guidance**: If the stop hook feedback in the current conversation mentions a specific increment ID (e.g., "Continue: /sw:do 0252"), use that ID
|
|
58
|
+
3. **Read incrementIds**: If no ID from above, read `incrementIds` array from `auto-mode.json` and use the **first entry** — this is the increment prioritized by scoring at session start
|
|
59
|
+
4. **Skip filesystem scanning**: When auto-mode context provides an increment ID via steps 2 or 3, skip Step 1's filesystem scanning entirely — auto-mode context takes priority
|
|
60
|
+
|
|
61
|
+
This ensures the execution loop stays focused on the contextually correct increment rather than re-scanning the filesystem each iteration.
|
|
62
|
+
|
|
52
63
|
### Step 2: Load Context
|
|
53
64
|
|
|
54
65
|
1. **Find increment directory**: Normalize ID to 4-digit format, match `.specweave/increments/NNNN-*/`
|
|
@@ -87,29 +87,28 @@ JIRA_DOMAIN="$(grep '^JIRA_DOMAIN=' .env | head -1 | cut -d '=' -f2-)"
|
|
|
87
87
|
### Domain Validation (before ANY API call)
|
|
88
88
|
|
|
89
89
|
```bash
|
|
90
|
-
#
|
|
91
|
-
if [[
|
|
92
|
-
echo "Error:
|
|
90
|
+
# Reject IP addresses FIRST — IPv4, IPv6 brackets, hex-encoded (SSRF prevention)
|
|
91
|
+
if [[ "$JIRA_DOMAIN" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+ ]] || [[ "$JIRA_DOMAIN" =~ ^\[.*\]$ ]] || [[ "$JIRA_DOMAIN" =~ ^0x ]]; then
|
|
92
|
+
echo "Error: IP addresses not allowed — use a hostname"
|
|
93
93
|
exit 1
|
|
94
94
|
fi
|
|
95
95
|
|
|
96
|
-
#
|
|
97
|
-
if [[
|
|
98
|
-
echo "Error:
|
|
99
|
-
echo "Self-hosted JIRA requires explicit user confirmation"
|
|
96
|
+
# Reject localhost and private networks
|
|
97
|
+
if [[ "$JIRA_DOMAIN" =~ ^(localhost|127\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.) ]]; then
|
|
98
|
+
echo "Error: Internal/localhost addresses not allowed"
|
|
100
99
|
exit 1
|
|
101
|
-
# Agent: use AskUserQuestion to confirm non-standard domain before retrying
|
|
102
100
|
fi
|
|
103
101
|
|
|
104
|
-
#
|
|
105
|
-
if [[ "$JIRA_DOMAIN" =~ ^[
|
|
106
|
-
echo "Error:
|
|
102
|
+
# Must be a valid hostname — no special chars, no consecutive dots
|
|
103
|
+
if [[ ! "$JIRA_DOMAIN" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$ ]]; then
|
|
104
|
+
echo "Error: JIRA_DOMAIN contains invalid characters"
|
|
107
105
|
exit 1
|
|
108
106
|
fi
|
|
109
107
|
|
|
110
|
-
#
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
# Cloud JIRA: must match <subdomain>.atlassian.net
|
|
109
|
+
# Agent: use AskUserQuestion to confirm non-standard domain before retrying
|
|
110
|
+
if [[ ! "$JIRA_DOMAIN" =~ ^[a-zA-Z0-9-]+\.atlassian\.net$ ]]; then
|
|
111
|
+
echo "Error: Domain does not match <subdomain>.atlassian.net pattern"
|
|
113
112
|
exit 1
|
|
114
113
|
fi
|
|
115
114
|
```
|
|
@@ -9,6 +9,76 @@ allowed-tools: Read, Bash, Write, Edit
|
|
|
9
9
|
|
|
10
10
|
**Auto-Activation**: Triggers when Jira setup or validation is needed.
|
|
11
11
|
|
|
12
|
+
## Security Rules (MANDATORY)
|
|
13
|
+
|
|
14
|
+
These rules apply to ALL JIRA API operations in this skill.
|
|
15
|
+
|
|
16
|
+
### Credential Handling
|
|
17
|
+
|
|
18
|
+
1. **Never collect credentials** — this skill reads from `.env` only, never prompts the user
|
|
19
|
+
2. **Never log secrets** — never echo token values, auth headers, or base64 credentials
|
|
20
|
+
3. **Never write credentials** — the user configures `.env` themselves (this skill may update non-secret keys like `JIRA_BOARDS` and `JIRA_PROJECT`)
|
|
21
|
+
|
|
22
|
+
### Credential Loading
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# 1. Validate presence FIRST (before reading any values)
|
|
26
|
+
for KEY in JIRA_API_TOKEN JIRA_EMAIL JIRA_DOMAIN; do
|
|
27
|
+
if ! grep -qE "^${KEY}=.+" .env; then
|
|
28
|
+
echo "Error: ${KEY} missing or empty in .env"
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
done
|
|
32
|
+
|
|
33
|
+
# 2. Load credentials ONLY after validation passes (never display values)
|
|
34
|
+
# head -1 ensures only first match used if .env has duplicate keys
|
|
35
|
+
JIRA_API_TOKEN="$(grep '^JIRA_API_TOKEN=' .env | head -1 | cut -d '=' -f2-)"
|
|
36
|
+
JIRA_EMAIL="$(grep '^JIRA_EMAIL=' .env | head -1 | cut -d '=' -f2-)"
|
|
37
|
+
JIRA_DOMAIN="$(grep '^JIRA_DOMAIN=' .env | head -1 | cut -d '=' -f2-)"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Domain Validation (before ANY API call)
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Reject IP addresses first (SSRF prevention)
|
|
44
|
+
if [[ "$JIRA_DOMAIN" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+ ]] || [[ "$JIRA_DOMAIN" =~ ^\[.*\]$ ]] || [[ "$JIRA_DOMAIN" =~ ^0x ]]; then
|
|
45
|
+
echo "Error: IP addresses not allowed — use a hostname"
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Reject localhost and private networks
|
|
50
|
+
if [[ "$JIRA_DOMAIN" =~ ^(localhost|127\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.) ]]; then
|
|
51
|
+
echo "Error: Internal/localhost addresses not allowed"
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# Must be a valid hostname — no special chars, no consecutive dots
|
|
56
|
+
if [[ ! "$JIRA_DOMAIN" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$ ]]; then
|
|
57
|
+
echo "Error: JIRA_DOMAIN contains invalid characters"
|
|
58
|
+
exit 1
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# Cloud JIRA: must match <subdomain>.atlassian.net
|
|
62
|
+
if [[ ! "$JIRA_DOMAIN" =~ ^[a-zA-Z0-9-]+\.atlassian\.net$ ]]; then
|
|
63
|
+
echo "Error: Domain does not match <subdomain>.atlassian.net pattern"
|
|
64
|
+
exit 1
|
|
65
|
+
fi
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### API Call Pattern (HTTPS only, quoted variables)
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
AUTH="$(printf '%s:%s' "$JIRA_EMAIL" "$JIRA_API_TOKEN" | base64)"
|
|
72
|
+
|
|
73
|
+
# All API calls MUST use https://, double-quote all variables
|
|
74
|
+
curl -s -f \
|
|
75
|
+
-H "Authorization: Basic $AUTH" \
|
|
76
|
+
-H "Content-Type: application/json" \
|
|
77
|
+
"https://${JIRA_DOMAIN}/rest/api/3/..."
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
12
82
|
## What This Skill Does
|
|
13
83
|
|
|
14
84
|
This skill ensures your Jira configuration in `.env` is valid and all resources exist. It's **smart enough** to:
|
|
@@ -31,10 +101,10 @@ This skill ensures your Jira configuration in `.env` is valid and all resources
|
|
|
31
101
|
|
|
32
102
|
### Required .env Variables
|
|
33
103
|
|
|
34
|
-
```
|
|
35
|
-
JIRA_API_TOKEN
|
|
36
|
-
JIRA_EMAIL
|
|
37
|
-
JIRA_DOMAIN
|
|
104
|
+
```
|
|
105
|
+
JIRA_API_TOKEN=<your-token>
|
|
106
|
+
JIRA_EMAIL=<your-email>
|
|
107
|
+
JIRA_DOMAIN=<your-company>.atlassian.net
|
|
38
108
|
JIRA_STRATEGY=board-based
|
|
39
109
|
JIRA_PROJECT=PROJECTKEY
|
|
40
110
|
JIRA_BOARDS=1,2,3 # IDs (if exist) OR names (if creating)
|
|
@@ -106,29 +106,28 @@ if [ -z "$JIRA_DOMAIN" ]; then
|
|
|
106
106
|
exit 1
|
|
107
107
|
fi
|
|
108
108
|
|
|
109
|
-
#
|
|
110
|
-
if [[
|
|
111
|
-
echo "Error:
|
|
109
|
+
# Reject IP addresses FIRST — IPv4, IPv6 brackets, hex-encoded (SSRF prevention)
|
|
110
|
+
if [[ "$JIRA_DOMAIN" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+ ]] || [[ "$JIRA_DOMAIN" =~ ^\[.*\]$ ]] || [[ "$JIRA_DOMAIN" =~ ^0x ]]; then
|
|
111
|
+
echo "Error: IP addresses not allowed — use a hostname"
|
|
112
112
|
exit 1
|
|
113
113
|
fi
|
|
114
114
|
|
|
115
|
-
#
|
|
116
|
-
if [[
|
|
117
|
-
echo "Error:
|
|
118
|
-
echo "Self-hosted JIRA requires explicit user confirmation"
|
|
115
|
+
# Reject localhost and internal hostnames
|
|
116
|
+
if [[ "$JIRA_DOMAIN" =~ ^(localhost|127\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.) ]]; then
|
|
117
|
+
echo "Error: Internal/localhost addresses not allowed"
|
|
119
118
|
exit 1
|
|
120
|
-
# Agent: use AskUserQuestion to confirm non-standard domain before retrying
|
|
121
119
|
fi
|
|
122
120
|
|
|
123
|
-
#
|
|
124
|
-
if [[ "$JIRA_DOMAIN" =~ ^[
|
|
125
|
-
echo "Error:
|
|
121
|
+
# Must be a valid hostname — each label: alphanumeric, hyphens allowed mid-label, no consecutive dots
|
|
122
|
+
if [[ ! "$JIRA_DOMAIN" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$ ]]; then
|
|
123
|
+
echo "Error: JIRA_DOMAIN is not a valid hostname"
|
|
126
124
|
exit 1
|
|
127
125
|
fi
|
|
128
126
|
|
|
129
|
-
#
|
|
130
|
-
|
|
131
|
-
|
|
127
|
+
# Must end with .atlassian.net for cloud JIRA
|
|
128
|
+
# Agent: use AskUserQuestion to confirm non-standard domain before retrying
|
|
129
|
+
if [[ ! "$JIRA_DOMAIN" =~ ^[a-zA-Z0-9-]+\.atlassian\.net$ ]]; then
|
|
130
|
+
echo "Error: Domain does not match <subdomain>.atlassian.net pattern"
|
|
132
131
|
exit 1
|
|
133
132
|
fi
|
|
134
133
|
```
|
|
@@ -168,7 +167,7 @@ JIRA and Confluence are both Atlassian products and often used together. This sk
|
|
|
168
167
|
|
|
169
168
|
### Confluence Credentials
|
|
170
169
|
|
|
171
|
-
Same authentication and security rules as JIRA — user configures `.env`, skill only validates presence. Same domain validation applies (must be `<subdomain>.atlassian.net`, HTTPS only, no IPs).
|
|
170
|
+
Same authentication and security rules as JIRA — user configures `.env`, skill only validates presence. Same domain validation applies (must be `<subdomain>.atlassian.net`, HTTPS only, no IPs). `CONFLUENCE_DOMAIN` MUST pass the same validation as `JIRA_DOMAIN` (Step 4) before any Confluence API call.
|
|
172
171
|
|
|
173
172
|
Required `.env` keys (configured by the user, NOT by this skill):
|
|
174
173
|
```
|
|
@@ -190,20 +189,12 @@ CONFLUENCE_SPACE_KEY=<space-key>
|
|
|
190
189
|
|
|
191
190
|
**Every page update MUST increment the version number**:
|
|
192
191
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
curl -s GET ".../pages/{id}" | jq '.version.number'
|
|
196
|
-
# Returns: 5
|
|
192
|
+
1. GET current page to retrieve `version.number` (e.g., returns 5)
|
|
193
|
+
2. PUT update with `version.number` set to current + 1 (e.g., 6)
|
|
197
194
|
|
|
198
|
-
|
|
199
|
-
PUT ".../pages/{id}"
|
|
200
|
-
{ "version": { "number": 6 } }
|
|
201
|
-
```
|
|
195
|
+
If version is not incremented, the API returns `409 Conflict`.
|
|
202
196
|
|
|
203
|
-
**
|
|
204
|
-
```
|
|
205
|
-
409 Conflict: "Version must be incremented on update. Current version is: 5"
|
|
206
|
-
```
|
|
197
|
+
All Confluence API calls follow the same security rules as JIRA (HTTPS only, domain validation, credential handling). See the `jira-mapper` skill's **Security Rules** section for implementation patterns.
|
|
207
198
|
|
|
208
199
|
### Reference Documentation
|
|
209
200
|
|