@codyswann/lisa 2.11.0 → 2.12.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/README.md CHANGED
@@ -12,9 +12,9 @@ A request to fix a bug routes to a different flow than a request to build a feat
12
12
 
13
13
  ### Flows and Agents
14
14
 
15
- A flow is a pipeline. Each step in the pipeline is an **agent** — a scoped AI with specific tools and skills. One agent investigates git history, another reproduces bugs, another writes code, another verifies the result.
15
+ A flow is a pipeline. Each step in the pipeline is an **agent** — a scoped AI with specific tools and instructions. One agent investigates git history, another reproduces bugs, another writes code, another verifies the result.
16
16
 
17
- Agents delegate domain-specific work to **skills** — reusable instruction sets that can be invoked by agents, by slash commands, or by CI workflows. The same skill that triages a JIRA ticket interactively is the same skill invoked by the nightly triage workflow.
17
+ Behind the scenes, agents delegate domain-specific work to reusable instruction sets that are loaded automatically when a command runs. The same logic that triages a JIRA ticket interactively is the same logic invoked by the nightly triage workflow — you don't need to know which one is running.
18
18
 
19
19
  Flows can nest. A build flow includes a verification sub-flow, which includes a ship sub-flow. This composition keeps each flow focused while enabling complex end-to-end workflows.
20
20
 
@@ -28,13 +28,13 @@ Lisa enforces quality through layered gates:
28
28
 
29
29
  ### Location Agnostic
30
30
 
31
- The same rules, skills, and quality gates apply everywhere:
31
+ The same rules, workflows, and quality gates apply everywhere:
32
32
 
33
33
  - On a developer's workstation running Claude Code interactively
34
34
  - In a GitHub Action running a nightly improvement job
35
35
  - In a CI workflow responding to a PR review comment
36
36
 
37
- The analytical logic lives in skills. The enforcement lives in hooks and rules. The orchestration adapts to context — using MCP integrations locally and REST APIs in CI — but the standards don't change.
37
+ The orchestration adapts to context — using MCP integrations locally and REST APIs in CI — but the standards don't change.
38
38
 
39
39
  ### Template Governance
40
40
 
@@ -44,7 +44,7 @@ Lisa distributes its standards to downstream projects as templates. When a proje
44
44
  - Test and coverage infrastructure
45
45
  - CI/CD workflows
46
46
  - Git hooks
47
- - AI agent definitions, skills, and rules
47
+ - AI agent definitions and project rules
48
48
 
49
49
  Templates follow governance rules: some files are overwritten on every update (enforced standards), some are created once and left alone (project customization), and some are merged (shared defaults with project additions).
50
50
 
@@ -58,15 +58,66 @@ curl -fsSL https://claude.ai/install.sh | bash
58
58
 
59
59
  ## Working With Lisa
60
60
 
61
- > Ask Claude: "I have JIRA ticket [TICKET-ID]. Research, plan, and implement it."
61
+ Lisa exposes a small set of top-level commands that map to the work lifecycle. Run them in Claude Code; everything underneath — agents, sub-flows, and the supporting libraries that power each step — happens automatically.
62
62
 
63
- Or use slash commands directly:
63
+ ### The Lifecycle
64
64
 
65
- - `/fix` route through the bug fix flow
66
- - `/build` — route through the feature build flow
67
- - `/improve` — route through the improvement flow
68
- - `/investigate` — route through the investigation flow
69
- - `/jira:triage <TICKET-ID>` — analytical triage gate: detect ambiguities, edge cases, and verification methodology
70
- - `/plan:improve-tests <target>` — improve test quality by analyzing and strengthening weak or brittle tests
65
+ A piece of work moves through five stages. Each stage has one command.
71
66
 
72
- > Ask Claude: "What commands are available?"
67
+ | Stage | Command | What it does |
68
+ | --- | --- | --- |
69
+ | Research | `/lisa:research <problem>` | Investigates the codebase and problem space, then produces a PRD ready for planning. |
70
+ | Plan | `/lisa:plan <PRD>` | Decomposes a PRD into ordered work items in your tracker (JIRA, GitHub Issues, or Linear). |
71
+ | Implement | `/lisa:implement <ticket>` | Takes one work item from spec to shipped: assembles an agent team, runs the build, opens a PR, handles review, merges. |
72
+ | Verify | `/lisa:verify` | Commits, pushes, opens a PR, monitors deploy, and verifies behavior in the target environment. Folded into `/lisa:implement` but available standalone. |
73
+ | Debrief | `/lisa:debrief <epic>` | After shipping, mines tickets and PRs to surface edge cases, gotchas, and friction. Produces a triage doc; `/lisa:debrief:apply` persists accepted learnings. |
74
+
75
+ Most users only ever call `/lisa:research`, `/lisa:plan`, and `/lisa:implement`. The rest run automatically as sub-flows.
76
+
77
+ ### Batch and Scheduled Work
78
+
79
+ | Command | What it does |
80
+ | --- | --- |
81
+ | `/lisa:intake <queue-url>` | Scans a Ready queue (Notion PRD database, JIRA project, GitHub repo, Linear team, Confluence space) and dispatches each item through the right lifecycle command. Designed as the cron target for unattended runs. |
82
+
83
+ ### Maintenance and Operations
84
+
85
+ | Command | What it does |
86
+ | --- | --- |
87
+ | `/lisa:monitor [environment]` | Checks application health, logs, error rates, and performance for the named environment. |
88
+ | `/lisa:product-walkthrough <route>` | Walks the live product through a real browser to ground PRD or ticket reasoning in current behavior. |
89
+ | `/lisa:codify-verification <type> <what>` | Converts a passing manual verification into a regression test in the appropriate framework (Playwright, integration test, benchmark). Runs automatically after `/lisa:verify`. |
90
+ | `/lisa:review:local` | Reviews local branch changes against `main`. |
91
+ | `/lisa:pull-request:review <pr-url>` | Pulls down review comments on a PR and implements the valid ones. |
92
+ | `/lisa:security:zap-scan` | Runs an OWASP ZAP baseline scan against the local app. |
93
+
94
+ ### Targeted Improvements
95
+
96
+ These commands tighten a specific quality threshold and fix every violation in one pass — useful for incremental hardening or nightly jobs.
97
+
98
+ | Command | What it does |
99
+ | --- | --- |
100
+ | `/lisa:improve:test-coverage <pct>` | Raises coverage to the target percentage by adding tests for uncovered code. |
101
+ | `/lisa:improve:tests <target>` | Strengthens weak, brittle, or poorly-written tests. |
102
+ | `/lisa:improve:code-complexity` | Lowers the cognitive-complexity threshold by 2 and fixes resulting violations. |
103
+ | `/lisa:improve:max-lines <n>` | Reduces the max-file-lines threshold and fixes violations. |
104
+ | `/lisa:improve:max-lines-per-function <n>` | Reduces the max-lines-per-function threshold and fixes violations. |
105
+ | `/lisa:fix:linter-error <rule> [...]` | Fixes every violation of one or more ESLint rules across the codebase. |
106
+
107
+ ### Git Helpers
108
+
109
+ | Command | What it does |
110
+ | --- | --- |
111
+ | `/lisa:git:commit [hint]` | Creates conventional commits from the current changes. |
112
+ | `/lisa:git:submit-pr [hint]` | Pushes and opens or updates a PR. |
113
+ | `/lisa:git:prune` | Prunes local branches whose remotes have been deleted. |
114
+
115
+ ### Talking to Lisa in Plain English
116
+
117
+ You don't have to remember any of this. Tell Claude what you want and the right command will run:
118
+
119
+ > "I have JIRA ticket PROJ-1234. Research, plan, and implement it."
120
+ > "Walk through the checkout flow and tell me what's broken."
121
+ > "Get test coverage to 90%."
122
+
123
+ > Ask Claude: "What commands are available?" for the full list at any time.
package/package.json CHANGED
@@ -79,7 +79,7 @@
79
79
  "lodash": ">=4.18.1"
80
80
  },
81
81
  "name": "@codyswann/lisa",
82
- "version": "2.11.0",
82
+ "version": "2.12.0",
83
83
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
84
84
  "main": "dist/index.js",
85
85
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -34,7 +34,23 @@ Call `mcp__atlassian__getJiraIssue` for the target ticket. Extract and preserve:
34
34
  - Full description (preserve formatting)
35
35
  - Acceptance criteria section if separately structured
36
36
  - **Validation Journey** section if present (pass verbatim to downstream)
37
- - Attachments list (names + URLs, do not download unless needed)
37
+ - Attachments list capture `id`, `filename`, `mimeType`, `size`, and `content` URL for each. Do not download unless a downstream task needs the bytes (see "Downloading attachments" below).
38
+
39
+ #### Downloading attachments (opt-in)
40
+
41
+ The Atlassian MCP exposes attachment metadata but no binary-fetch tool ([JRACLOUD-97830](https://jira.atlassian.com/browse/JRACLOUD-97830), [ECO-1265](https://jira.atlassian.com/browse/ECO-1265)). Fetch attachment bytes only when a downstream task explicitly needs them — e.g., a design-fidelity check on an image, log-file analysis, PDF text extraction. For everything else, keep the URL reference and move on.
42
+
43
+ ```bash
44
+ bash .claude/skills/jira-read-ticket/scripts/download-attachment.sh <id-or-content-url> <output-path>
45
+ ```
46
+
47
+ Requires `JIRA_SERVER`, `JIRA_LOGIN`, and `JIRA_API_TOKEN` in the environment (same contract as `jira-evidence`). If those are not set the helper exits with code 2 and a clear remediation message — record the URL only and continue.
48
+
49
+ After download, branch on `mimeType`:
50
+ - `image/*` — pass the local path to image-aware downstream tools
51
+ - `text/*`, `application/json`, `application/xml`, `application/x-yaml` — read inline as text
52
+ - `application/pdf` — extract text via downstream tooling if needed
53
+ - everything else — record path only; do not attempt to inline binary content
38
54
 
39
55
  ### Comments
40
56
 
@@ -121,7 +137,7 @@ Produce a single structured output that the caller can pass verbatim to downstre
121
137
  <chronological comments, flagged items called out>
122
138
 
123
139
  ### Attachments
124
- <list>
140
+ <list with id, filename, mimeType, size, content URL — note any that were downloaded and their local paths>
125
141
 
126
142
  ## Remote Links
127
143
  ### Pull Requests (<count>)
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env bash
2
+ # download-attachment.sh — Download a JIRA attachment to a local file.
3
+ #
4
+ # Usage:
5
+ # bash download-attachment.sh <ATTACHMENT_ID_OR_URL> <OUTPUT_PATH>
6
+ #
7
+ # Why this helper exists:
8
+ # The Atlassian MCP server (mcp__atlassian__*) returns attachment metadata
9
+ # (id, filename, mimeType, size, content URL) on getJiraIssue but provides
10
+ # no tool to fetch the binary content. This script closes the gap by
11
+ # hitting the Jira REST API directly with Basic auth, mirroring the
12
+ # env-var contract already used by jira-evidence/scripts/post-evidence.sh.
13
+ #
14
+ # See https://jira.atlassian.com/browse/JRACLOUD-97830 for the upstream
15
+ # gap; remove this helper once Atlassian ships a download tool in the MCP.
16
+ #
17
+ # Required env vars:
18
+ # JIRA_SERVER - https://<your-tenant>.atlassian.net
19
+ # JIRA_LOGIN - login email
20
+ # JIRA_API_TOKEN - API token (https://id.atlassian.com/manage-profile/security/api-tokens)
21
+ #
22
+ # Exit codes:
23
+ # 0 success
24
+ # 1 download failed (HTTP error)
25
+ # 2 missing required env var
26
+ # 3 invalid arguments
27
+
28
+ set -euo pipefail
29
+
30
+ if [[ $# -lt 2 ]]; then
31
+ echo "Usage: download-attachment.sh <ATTACHMENT_ID_OR_URL> <OUTPUT_PATH>" >&2
32
+ exit 3
33
+ fi
34
+ ID_OR_URL="$1"
35
+ OUTPUT_PATH="$2"
36
+
37
+ # Resolve credentials: prefer env, fall back to jira-cli config for server/login.
38
+ JIRA_CONFIG="${HOME}/.config/.jira/.config.yml"
39
+ if [[ -z "${JIRA_SERVER:-}" && -f "$JIRA_CONFIG" ]]; then
40
+ JIRA_SERVER=$(grep '^server:' "$JIRA_CONFIG" | awk '{print $2}')
41
+ fi
42
+ if [[ -z "${JIRA_LOGIN:-}" && -f "$JIRA_CONFIG" ]]; then
43
+ JIRA_LOGIN=$(grep '^login:' "$JIRA_CONFIG" | awk '{print $2}')
44
+ fi
45
+
46
+ for VAR in JIRA_SERVER JIRA_LOGIN JIRA_API_TOKEN; do
47
+ if [[ -z "${!VAR:-}" ]]; then
48
+ echo "ERROR: $VAR is not set." >&2
49
+ echo "Required env vars: JIRA_SERVER, JIRA_LOGIN, JIRA_API_TOKEN." >&2
50
+ echo "Generate an API token: https://id.atlassian.com/manage-profile/security/api-tokens" >&2
51
+ exit 2
52
+ fi
53
+ done
54
+
55
+ OUTPUT_DIR=$(dirname "$OUTPUT_PATH")
56
+ if [[ ! -d "$OUTPUT_DIR" ]]; then
57
+ echo "ERROR: Output directory does not exist: $OUTPUT_DIR" >&2
58
+ exit 3
59
+ fi
60
+
61
+ if [[ "$ID_OR_URL" == http*://* ]]; then
62
+ ATTACHMENT_URL="$ID_OR_URL"
63
+ else
64
+ ATTACHMENT_URL="${JIRA_SERVER%/}/rest/api/3/attachment/content/$ID_OR_URL"
65
+ fi
66
+
67
+ JIRA_AUTH=$(printf '%s' "$JIRA_LOGIN:$JIRA_API_TOKEN" | base64 | tr -d '\n')
68
+
69
+ # Atlassian responds 302 to a signed URL on media.atlassian.com that has its
70
+ # own auth and rejects Basic. Two-step: capture Location, then GET unauthed.
71
+ HEADERS_FILE=$(mktemp)
72
+ trap 'rm -f "$HEADERS_FILE"' EXIT
73
+
74
+ HTTP_CODE=$(curl -sS -o /dev/null -w '%{http_code}' \
75
+ --max-redirs 0 \
76
+ -D "$HEADERS_FILE" \
77
+ -H "Authorization: Basic $JIRA_AUTH" \
78
+ -H "Accept: */*" \
79
+ "$ATTACHMENT_URL" || true)
80
+
81
+ case "$HTTP_CODE" in
82
+ 302|303|307)
83
+ SIGNED_URL=$(awk '/^[Ll]ocation:/{sub(/^[Ll]ocation:[ \t]*/,""); sub(/\r$/,""); print; exit}' "$HEADERS_FILE")
84
+ if [[ -z "$SIGNED_URL" ]]; then
85
+ echo "ERROR: Got HTTP $HTTP_CODE but no Location header in response." >&2
86
+ exit 1
87
+ fi
88
+ curl -sSf -o "$OUTPUT_PATH" "$SIGNED_URL" || { echo "ERROR: Download from signed URL failed." >&2; exit 1; }
89
+ ;;
90
+ 200)
91
+ curl -sSf -o "$OUTPUT_PATH" \
92
+ -H "Authorization: Basic $JIRA_AUTH" \
93
+ -H "Accept: */*" \
94
+ "$ATTACHMENT_URL" || { echo "ERROR: Direct download from $ATTACHMENT_URL failed." >&2; exit 1; }
95
+ ;;
96
+ 401|403)
97
+ echo "ERROR: Authentication failed (HTTP $HTTP_CODE). Verify JIRA_LOGIN and JIRA_API_TOKEN." >&2
98
+ exit 1
99
+ ;;
100
+ 404)
101
+ echo "ERROR: Attachment not found at $ATTACHMENT_URL (HTTP 404). Verify the ID and your access." >&2
102
+ exit 1
103
+ ;;
104
+ *)
105
+ echo "ERROR: Unexpected HTTP $HTTP_CODE from $ATTACHMENT_URL" >&2
106
+ exit 1
107
+ ;;
108
+ esac
109
+
110
+ echo " ✓ Downloaded -> $OUTPUT_PATH"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -34,7 +34,23 @@ Call `mcp__atlassian__getJiraIssue` for the target ticket. Extract and preserve:
34
34
  - Full description (preserve formatting)
35
35
  - Acceptance criteria section if separately structured
36
36
  - **Validation Journey** section if present (pass verbatim to downstream)
37
- - Attachments list (names + URLs, do not download unless needed)
37
+ - Attachments list capture `id`, `filename`, `mimeType`, `size`, and `content` URL for each. Do not download unless a downstream task needs the bytes (see "Downloading attachments" below).
38
+
39
+ #### Downloading attachments (opt-in)
40
+
41
+ The Atlassian MCP exposes attachment metadata but no binary-fetch tool ([JRACLOUD-97830](https://jira.atlassian.com/browse/JRACLOUD-97830), [ECO-1265](https://jira.atlassian.com/browse/ECO-1265)). Fetch attachment bytes only when a downstream task explicitly needs them — e.g., a design-fidelity check on an image, log-file analysis, PDF text extraction. For everything else, keep the URL reference and move on.
42
+
43
+ ```bash
44
+ bash .claude/skills/jira-read-ticket/scripts/download-attachment.sh <id-or-content-url> <output-path>
45
+ ```
46
+
47
+ Requires `JIRA_SERVER`, `JIRA_LOGIN`, and `JIRA_API_TOKEN` in the environment (same contract as `jira-evidence`). If those are not set the helper exits with code 2 and a clear remediation message — record the URL only and continue.
48
+
49
+ After download, branch on `mimeType`:
50
+ - `image/*` — pass the local path to image-aware downstream tools
51
+ - `text/*`, `application/json`, `application/xml`, `application/x-yaml` — read inline as text
52
+ - `application/pdf` — extract text via downstream tooling if needed
53
+ - everything else — record path only; do not attempt to inline binary content
38
54
 
39
55
  ### Comments
40
56
 
@@ -121,7 +137,7 @@ Produce a single structured output that the caller can pass verbatim to downstre
121
137
  <chronological comments, flagged items called out>
122
138
 
123
139
  ### Attachments
124
- <list>
140
+ <list with id, filename, mimeType, size, content URL — note any that were downloaded and their local paths>
125
141
 
126
142
  ## Remote Links
127
143
  ### Pull Requests (<count>)
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env bash
2
+ # download-attachment.sh — Download a JIRA attachment to a local file.
3
+ #
4
+ # Usage:
5
+ # bash download-attachment.sh <ATTACHMENT_ID_OR_URL> <OUTPUT_PATH>
6
+ #
7
+ # Why this helper exists:
8
+ # The Atlassian MCP server (mcp__atlassian__*) returns attachment metadata
9
+ # (id, filename, mimeType, size, content URL) on getJiraIssue but provides
10
+ # no tool to fetch the binary content. This script closes the gap by
11
+ # hitting the Jira REST API directly with Basic auth, mirroring the
12
+ # env-var contract already used by jira-evidence/scripts/post-evidence.sh.
13
+ #
14
+ # See https://jira.atlassian.com/browse/JRACLOUD-97830 for the upstream
15
+ # gap; remove this helper once Atlassian ships a download tool in the MCP.
16
+ #
17
+ # Required env vars:
18
+ # JIRA_SERVER - https://<your-tenant>.atlassian.net
19
+ # JIRA_LOGIN - login email
20
+ # JIRA_API_TOKEN - API token (https://id.atlassian.com/manage-profile/security/api-tokens)
21
+ #
22
+ # Exit codes:
23
+ # 0 success
24
+ # 1 download failed (HTTP error)
25
+ # 2 missing required env var
26
+ # 3 invalid arguments
27
+
28
+ set -euo pipefail
29
+
30
+ if [[ $# -lt 2 ]]; then
31
+ echo "Usage: download-attachment.sh <ATTACHMENT_ID_OR_URL> <OUTPUT_PATH>" >&2
32
+ exit 3
33
+ fi
34
+ ID_OR_URL="$1"
35
+ OUTPUT_PATH="$2"
36
+
37
+ # Resolve credentials: prefer env, fall back to jira-cli config for server/login.
38
+ JIRA_CONFIG="${HOME}/.config/.jira/.config.yml"
39
+ if [[ -z "${JIRA_SERVER:-}" && -f "$JIRA_CONFIG" ]]; then
40
+ JIRA_SERVER=$(grep '^server:' "$JIRA_CONFIG" | awk '{print $2}')
41
+ fi
42
+ if [[ -z "${JIRA_LOGIN:-}" && -f "$JIRA_CONFIG" ]]; then
43
+ JIRA_LOGIN=$(grep '^login:' "$JIRA_CONFIG" | awk '{print $2}')
44
+ fi
45
+
46
+ for VAR in JIRA_SERVER JIRA_LOGIN JIRA_API_TOKEN; do
47
+ if [[ -z "${!VAR:-}" ]]; then
48
+ echo "ERROR: $VAR is not set." >&2
49
+ echo "Required env vars: JIRA_SERVER, JIRA_LOGIN, JIRA_API_TOKEN." >&2
50
+ echo "Generate an API token: https://id.atlassian.com/manage-profile/security/api-tokens" >&2
51
+ exit 2
52
+ fi
53
+ done
54
+
55
+ OUTPUT_DIR=$(dirname "$OUTPUT_PATH")
56
+ if [[ ! -d "$OUTPUT_DIR" ]]; then
57
+ echo "ERROR: Output directory does not exist: $OUTPUT_DIR" >&2
58
+ exit 3
59
+ fi
60
+
61
+ if [[ "$ID_OR_URL" == http*://* ]]; then
62
+ ATTACHMENT_URL="$ID_OR_URL"
63
+ else
64
+ ATTACHMENT_URL="${JIRA_SERVER%/}/rest/api/3/attachment/content/$ID_OR_URL"
65
+ fi
66
+
67
+ JIRA_AUTH=$(printf '%s' "$JIRA_LOGIN:$JIRA_API_TOKEN" | base64 | tr -d '\n')
68
+
69
+ # Atlassian responds 302 to a signed URL on media.atlassian.com that has its
70
+ # own auth and rejects Basic. Two-step: capture Location, then GET unauthed.
71
+ HEADERS_FILE=$(mktemp)
72
+ trap 'rm -f "$HEADERS_FILE"' EXIT
73
+
74
+ HTTP_CODE=$(curl -sS -o /dev/null -w '%{http_code}' \
75
+ --max-redirs 0 \
76
+ -D "$HEADERS_FILE" \
77
+ -H "Authorization: Basic $JIRA_AUTH" \
78
+ -H "Accept: */*" \
79
+ "$ATTACHMENT_URL" || true)
80
+
81
+ case "$HTTP_CODE" in
82
+ 302|303|307)
83
+ SIGNED_URL=$(awk '/^[Ll]ocation:/{sub(/^[Ll]ocation:[ \t]*/,""); sub(/\r$/,""); print; exit}' "$HEADERS_FILE")
84
+ if [[ -z "$SIGNED_URL" ]]; then
85
+ echo "ERROR: Got HTTP $HTTP_CODE but no Location header in response." >&2
86
+ exit 1
87
+ fi
88
+ curl -sSf -o "$OUTPUT_PATH" "$SIGNED_URL" || { echo "ERROR: Download from signed URL failed." >&2; exit 1; }
89
+ ;;
90
+ 200)
91
+ curl -sSf -o "$OUTPUT_PATH" \
92
+ -H "Authorization: Basic $JIRA_AUTH" \
93
+ -H "Accept: */*" \
94
+ "$ATTACHMENT_URL" || { echo "ERROR: Direct download from $ATTACHMENT_URL failed." >&2; exit 1; }
95
+ ;;
96
+ 401|403)
97
+ echo "ERROR: Authentication failed (HTTP $HTTP_CODE). Verify JIRA_LOGIN and JIRA_API_TOKEN." >&2
98
+ exit 1
99
+ ;;
100
+ 404)
101
+ echo "ERROR: Attachment not found at $ATTACHMENT_URL (HTTP 404). Verify the ID and your access." >&2
102
+ exit 1
103
+ ;;
104
+ *)
105
+ echo "ERROR: Unexpected HTTP $HTTP_CODE from $ATTACHMENT_URL" >&2
106
+ exit 1
107
+ ;;
108
+ esac
109
+
110
+ echo " ✓ Downloaded -> $OUTPUT_PATH"