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.
@@ -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
- INCREMENT_ID=$(basename "$dir")
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": "optional",
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
- # Must be a valid hostname no special chars, no consecutive dots
91
- 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
92
- echo "Error: JIRA_DOMAIN contains invalid characters"
90
+ # Reject IP addresses FIRSTIPv4, 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
- # Cloud JIRA: must match <subdomain>.atlassian.net
97
- if [[ ! "$JIRA_DOMAIN" =~ ^[a-zA-Z0-9-]+\.atlassian\.net$ ]]; then
98
- echo "Error: Domain does not match <subdomain>.atlassian.net pattern"
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
- # Reject IP addresses (SSRF prevention)
105
- if [[ "$JIRA_DOMAIN" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+ ]] || [[ "$JIRA_DOMAIN" =~ ^\[.*\]$ ]] || [[ "$JIRA_DOMAIN" =~ ^0x ]]; then
106
- echo "Error: IP addresses not allowed — use a hostname"
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
- # Reject localhost and private networks
111
- if [[ "$JIRA_DOMAIN" =~ ^(localhost|127\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.) ]]; then
112
- echo "Error: Internal/localhost addresses not allowed"
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
- ```bash
35
- JIRA_API_TOKEN=your_token_here
36
- JIRA_EMAIL=your_email@company.com
37
- JIRA_DOMAIN=yourcompany.atlassian.net
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
- # Must be a valid hostname each label: alphanumeric, hyphens allowed mid-label, no consecutive dots
110
- 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
111
- echo "Error: JIRA_DOMAIN is not a valid hostname"
109
+ # Reject IP addresses FIRSTIPv4, 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
- # Must end with .atlassian.net for cloud JIRA
116
- if [[ ! "$JIRA_DOMAIN" =~ ^[a-zA-Z0-9-]+\.atlassian\.net$ ]]; then
117
- echo "Error: Domain does not match <subdomain>.atlassian.net pattern"
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
- # Reject IP addressesIPv4, IPv6 brackets, hex-encoded (SSRF prevention)
124
- if [[ "$JIRA_DOMAIN" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+ ]] || [[ "$JIRA_DOMAIN" =~ ^\[.*\]$ ]] || [[ "$JIRA_DOMAIN" =~ ^0x ]]; then
125
- echo "Error: IP addresses not allowed use a hostname"
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
- # Reject localhost and internal hostnames
130
- if [[ "$JIRA_DOMAIN" =~ ^(localhost|127\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.) ]]; then
131
- echo "Error: Internal/localhost addresses not allowed"
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
- ```bash
194
- # 1. Get current version
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
- # 2. Update with version + 1
199
- PUT ".../pages/{id}"
200
- { "version": { "number": 6 } }
201
- ```
195
+ If version is not incremented, the API returns `409 Conflict`.
202
196
 
203
- **Error if version not incremented**:
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