@mmerterden/multi-agent-pipeline 11.4.0 → 12.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/CHANGELOG.md +172 -0
- package/README.md +1 -0
- package/docs/adr/0007-multi-tool-adapter-framework.md +2 -2
- package/index.js +9 -2
- package/install/_common.mjs +107 -5
- package/install/_telemetry.mjs +5 -23
- package/install/claude.mjs +95 -11
- package/install/copilot.mjs +75 -6
- package/install/index.mjs +34 -1
- package/package.json +4 -10
- package/pipeline/commands/multi-agent/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/analysis/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/autopilot/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/build-optimize/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/channels/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/create-jira/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/dev/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/dev-local/SKILL.md +2 -1
- package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/diff-explain/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/finish/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +29 -3
- package/pipeline/commands/multi-agent/help/SKILL.md +3 -2
- package/pipeline/commands/multi-agent/issue/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/jira/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/kill/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/language/SKILL.md +20 -1
- package/pipeline/commands/multi-agent/local/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/log/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/manual-test/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/prune-logs/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/purge/SKILL.md +27 -24
- package/pipeline/commands/multi-agent/refactor/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/resume/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/review/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/review-issue/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/review-jira/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/scan/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/search/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/setup/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/stack/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/status/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/sync/SKILL.md +20 -2
- package/pipeline/commands/multi-agent/test/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/uninstall/SKILL.md +87 -0
- package/pipeline/commands/multi-agent/update/SKILL.md +1 -0
- package/pipeline/lib/account-resolver.sh +61 -23
- package/pipeline/lib/channels-multi-repo.sh +7 -2
- package/pipeline/lib/count-lib.sh +27 -0
- package/pipeline/lib/credential-store.sh +23 -6
- package/pipeline/lib/extract-conventions.sh +27 -21
- package/pipeline/lib/fetch-confluence.sh +25 -13
- package/pipeline/lib/fetch-crashlytics.sh +30 -8
- package/pipeline/lib/fetch-fortify.sh +11 -2
- package/pipeline/lib/fetch-swagger.sh +18 -7
- package/pipeline/lib/figma-mcp-refresh.sh +26 -11
- package/pipeline/lib/figma-screenshot.sh +11 -2
- package/pipeline/lib/issue-fetcher.sh +31 -6
- package/pipeline/lib/multi-repo-pipeline.sh +84 -20
- package/pipeline/lib/plan-todos.sh +12 -3
- package/pipeline/lib/post-pr-review.sh +5 -3
- package/pipeline/lib/repo-cache.sh +53 -9
- package/pipeline/lib/review-watch.sh +26 -6
- package/pipeline/lib/shadow-git.sh +45 -4
- package/pipeline/lib/vercel-deploy.sh +10 -1
- package/pipeline/multi-agent-refs/cross-cli-contract.md +5 -5
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +12 -1
- package/pipeline/scripts/audit-log-rotate.sh +38 -10
- package/pipeline/scripts/check-md-links.mjs +34 -9
- package/pipeline/scripts/diff-risk-score.mjs +4 -1
- package/pipeline/scripts/evidence-gate.mjs +10 -1
- package/pipeline/scripts/fixtures/install-layout.tsv +6 -4
- package/pipeline/scripts/fixtures/pack-expected-count.txt +1 -0
- package/pipeline/scripts/gc-tmp.sh +67 -8
- package/pipeline/scripts/gc-worktrees.sh +140 -0
- package/pipeline/scripts/keychain-save.sh +13 -9
- package/pipeline/scripts/lint-skills.mjs +40 -2
- package/pipeline/scripts/localize-commands.mjs +130 -0
- package/pipeline/scripts/migrate-prefs.mjs +26 -7
- package/pipeline/scripts/phase-tracker.sh +71 -0
- package/pipeline/scripts/pre-commit-check.sh +39 -0
- package/pipeline/scripts/prune-logs.sh +100 -15
- package/pipeline/scripts/purge.sh +235 -0
- package/pipeline/scripts/scan-skills.sh +217 -127
- package/pipeline/scripts/smoke-bitbucket-contract.sh +21 -5
- package/pipeline/scripts/smoke-description-tr.sh +82 -0
- package/pipeline/scripts/smoke-fetchers-offline.sh +382 -0
- package/pipeline/scripts/smoke-gc-tmp.sh +50 -4
- package/pipeline/scripts/smoke-gc-worktrees.sh +125 -0
- package/pipeline/scripts/smoke-install-layout.sh +11 -3
- package/pipeline/scripts/smoke-lib-scripts.sh +54 -1
- package/pipeline/scripts/smoke-pack-contents.sh +140 -0
- package/pipeline/scripts/smoke-plugin-validate.sh +64 -0
- package/pipeline/scripts/smoke-prune-logs.sh +57 -7
- package/pipeline/scripts/smoke-purge.sh +138 -0
- package/pipeline/scripts/smoke-shadow-git.sh +48 -1
- package/pipeline/scripts/smoke-skill-authoring.sh +1 -1
- package/pipeline/scripts/smoke-update-check.sh +13 -0
- package/pipeline/scripts/smoke-workflow-audit.sh +69 -0
- package/pipeline/scripts/test-gap-scan.mjs +12 -1
- package/pipeline/scripts/triage-memory.mjs +10 -1
- package/pipeline/scripts/uninstall.mjs +146 -42
- package/pipeline/scripts/update-check.sh +9 -0
- package/pipeline/scripts/update-issue-progress.sh +60 -41
- package/pipeline/scripts/write-state.mjs +14 -4
- package/pipeline/skills/.skill-manifest.json +7 -7
- package/pipeline/skills/.skills-index.json +9 -9
- package/pipeline/skills/shared/README.md +19 -10
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +1 -0
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-uninstall/SKILL.md +74 -0
- package/pipeline/skills/skills-index.md +1 -1
- package/pipeline/commands/multi-agent/delete/SKILL.md +0 -67
- package/pipeline/skills/shared/core/multi-agent-delete/SKILL.md +0 -66
|
@@ -67,15 +67,17 @@ if [ -z "$URL" ] && [ -z "$PAGE_ID" ]; then
|
|
|
67
67
|
exit 4
|
|
68
68
|
fi
|
|
69
69
|
|
|
70
|
-
# Resolve host + pageId from URL when not given explicitly.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
# Resolve host + pageId from URL when not given explicitly. Inputs are passed
|
|
71
|
+
# as argv: an env prefix on a plain assignment (VAR=x PARSED=$(...)) never
|
|
72
|
+
# reaches the child process, so os.environ would always come back empty.
|
|
73
|
+
PARSED=$(python3 - "$URL" "$HOST_OVERRIDE" "$PAGE_ID" <<'PY'
|
|
74
|
+
import sys, re
|
|
74
75
|
import urllib.parse as up
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
argv = sys.argv[1:] + ["", "", ""]
|
|
78
|
+
url = argv[0]
|
|
79
|
+
host_override = argv[1]
|
|
80
|
+
explicit_id = argv[2]
|
|
79
81
|
|
|
80
82
|
host = host_override
|
|
81
83
|
page_id = explicit_id
|
|
@@ -124,12 +126,22 @@ except Exception:
|
|
|
124
126
|
fi
|
|
125
127
|
[ -z "$TOKEN_KEY" ] && TOKEN_KEY="${USER}_Confluence_Access_Token"
|
|
126
128
|
|
|
127
|
-
|
|
129
|
+
# Locate the credential helper via the resolver so Copilot-only installs work.
|
|
130
|
+
# shellcheck disable=SC1090,SC1091
|
|
131
|
+
. "$HOME/.claude/lib/credential-store-resolver.sh" 2>/dev/null \
|
|
132
|
+
|| . "$HOME/.copilot/lib/credential-store-resolver.sh" 2>/dev/null \
|
|
133
|
+
|| { printf '%s\n' '{"status":"blocked","reason":"missing-credential-helper","service":"confluence","expected_key":"'"$TOKEN_KEY"'"}' >&2; exit 2; }
|
|
134
|
+
|
|
135
|
+
TOKEN=$("$CRED_STORE" get "$TOKEN_KEY" 2>/dev/null || true)
|
|
128
136
|
if [ -z "$TOKEN" ]; then
|
|
129
137
|
printf '%s\n' '{"status":"blocked","reason":"missing-token","service":"confluence","expected_key":"'"$TOKEN_KEY"'"}' >&2
|
|
130
138
|
exit 2
|
|
131
139
|
fi
|
|
132
140
|
|
|
141
|
+
# Bearer auth via a curl config fed through process substitution so the token
|
|
142
|
+
# never appears in argv (argv is visible to ps).
|
|
143
|
+
confluence_auth_cfg() { printf 'header = "Authorization: Bearer %s"\n' "$TOKEN"; }
|
|
144
|
+
|
|
133
145
|
# Endpoint resolution: Confluence Cloud (<tenant>.atlassian.net/wiki/rest/api)
|
|
134
146
|
# vs Server/Data Center (<host>/rest/api). The same paths exist on both.
|
|
135
147
|
case "$HOST" in
|
|
@@ -141,14 +153,14 @@ esac
|
|
|
141
153
|
if [ -z "$PAGE_ID_RESOLVED" ] && [ -n "$SPACE_KEY" ] && [ -n "$URL" ]; then
|
|
142
154
|
TITLE_RAW=$(python3 -c "
|
|
143
155
|
import urllib.parse as up
|
|
144
|
-
import
|
|
145
|
-
p = up.urlparse(
|
|
156
|
+
import sys, re
|
|
157
|
+
p = up.urlparse(sys.argv[1])
|
|
146
158
|
m = re.match(r'/display/[^/]+/(.+)', p.path)
|
|
147
159
|
print(up.unquote(m.group(1)) if m else '')
|
|
148
|
-
"
|
|
160
|
+
" "$URL")
|
|
149
161
|
if [ -n "$TITLE_RAW" ]; then
|
|
150
162
|
LOOKUP=$(curl -sS --fail --max-time "$TIMEOUT" --connect-timeout 5 \
|
|
151
|
-
-
|
|
163
|
+
-K <(confluence_auth_cfg) -H "Accept: application/json" \
|
|
152
164
|
--data-urlencode "spaceKey=$SPACE_KEY" \
|
|
153
165
|
--data-urlencode "title=$TITLE_RAW" \
|
|
154
166
|
--data-urlencode "expand=" \
|
|
@@ -172,7 +184,7 @@ fi
|
|
|
172
184
|
|
|
173
185
|
# Fetch the page body in storage format.
|
|
174
186
|
PAGE_JSON=$(curl -sS --fail --max-time "$TIMEOUT" --connect-timeout 5 \
|
|
175
|
-
-
|
|
187
|
+
-K <(confluence_auth_cfg) -H "Accept: application/json" \
|
|
176
188
|
"$API_BASE/content/$PAGE_ID_RESOLVED?expand=body.storage,space" 2>/dev/null || true)
|
|
177
189
|
|
|
178
190
|
if [ -z "$PAGE_JSON" ]; then
|
|
@@ -124,22 +124,40 @@ except Exception:
|
|
|
124
124
|
fi
|
|
125
125
|
[ -z "$TOKEN_KEY" ] && TOKEN_KEY="${USER}_Firebase_Access_Json"
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
# Locate the credential helper via the resolver so Copilot-only installs work.
|
|
128
|
+
# shellcheck disable=SC1090,SC1091
|
|
129
|
+
. "$HOME/.claude/lib/credential-store-resolver.sh" 2>/dev/null \
|
|
130
|
+
|| . "$HOME/.copilot/lib/credential-store-resolver.sh" 2>/dev/null \
|
|
131
|
+
|| { printf '%s\n' '{"status":"blocked","reason":"missing-credential-helper","service":"firebase","expected_key":"'"$TOKEN_KEY"'"}' >&2; exit 2; }
|
|
132
|
+
|
|
133
|
+
SA_JSON_B64=$("$CRED_STORE" get "$TOKEN_KEY" 2>/dev/null || true)
|
|
128
134
|
if [ -z "$SA_JSON_B64" ]; then
|
|
129
135
|
printf '%s\n' '{"status":"blocked","reason":"missing-token","service":"firebase","expected_key":"'"$TOKEN_KEY"'"}' >&2
|
|
130
136
|
exit 2
|
|
131
137
|
fi
|
|
132
138
|
|
|
133
139
|
# The credential store may return raw JSON or base64-encoded JSON (the user's
|
|
134
|
-
# setup convention). Try base64-decode first, fall back to raw.
|
|
140
|
+
# setup convention). Try base64-decode first, fall back to raw. The decoded
|
|
141
|
+
# payload contains a GCP private key, so it goes into a 0600 mktemp file with
|
|
142
|
+
# cleanup traps registered BEFORE the secret is written.
|
|
143
|
+
SA_TMP=""
|
|
144
|
+
cleanup_sa_tmp() {
|
|
145
|
+
if [ -n "$SA_TMP" ]; then rm -f "$SA_TMP"; fi
|
|
146
|
+
}
|
|
147
|
+
trap cleanup_sa_tmp EXIT
|
|
148
|
+
trap 'cleanup_sa_tmp; exit 130' INT
|
|
149
|
+
trap 'cleanup_sa_tmp; exit 143' TERM
|
|
150
|
+
SA_TMP=$(umask 077; mktemp "${TMPDIR:-/tmp}/fc-sa.XXXXXX")
|
|
151
|
+
|
|
135
152
|
SA_JSON=""
|
|
136
|
-
if printf '%s' "$SA_JSON_B64" | base64 -d >
|
|
137
|
-
&& python3 -c "import json; json.load(open(
|
|
138
|
-
SA_JSON=$(cat
|
|
153
|
+
if printf '%s' "$SA_JSON_B64" | base64 -d > "$SA_TMP" 2>/dev/null \
|
|
154
|
+
&& python3 -c "import json, sys; json.load(open(sys.argv[1]))" "$SA_TMP" 2>/dev/null; then
|
|
155
|
+
SA_JSON=$(cat "$SA_TMP")
|
|
139
156
|
else
|
|
140
157
|
SA_JSON="$SA_JSON_B64"
|
|
141
158
|
fi
|
|
142
|
-
rm -f
|
|
159
|
+
rm -f "$SA_TMP"
|
|
160
|
+
SA_TMP=""
|
|
143
161
|
|
|
144
162
|
# Verify project_id from SA JSON matches the URL - protects against pasting a
|
|
145
163
|
# crash URL from another GCP project than the keychain's SA covers.
|
|
@@ -235,6 +253,10 @@ if [ -z "$ACCESS_TOKEN" ]; then
|
|
|
235
253
|
exit 3
|
|
236
254
|
fi
|
|
237
255
|
|
|
256
|
+
# Bearer auth via a curl config fed through process substitution so the token
|
|
257
|
+
# never appears in argv (argv is visible to ps).
|
|
258
|
+
crashlytics_auth_cfg() { printf 'header = "Authorization: Bearer %s"\n' "$ACCESS_TOKEN"; }
|
|
259
|
+
|
|
238
260
|
# Construct the appId in the form Firebase expects:
|
|
239
261
|
# ios: ios:<bundleId> → api: /projects/<n>/apps/<bundleId> (looked up via list)
|
|
240
262
|
# android: android:<package>
|
|
@@ -245,7 +267,7 @@ APP_REF="$PLATFORM:$BUNDLE"
|
|
|
245
267
|
ISSUE_URL_API="https://firebasecrashlytics.googleapis.com/v1alpha/projects/$PROJECT_ID/apps/$APP_REF/issues/$ISSUE_ID"
|
|
246
268
|
|
|
247
269
|
ISSUE_JSON=$(curl -sS --fail --max-time "$TIMEOUT" --connect-timeout 5 \
|
|
248
|
-
-
|
|
270
|
+
-K <(crashlytics_auth_cfg) -H "Accept: application/json" \
|
|
249
271
|
"$ISSUE_URL_API" 2>/dev/null || true)
|
|
250
272
|
|
|
251
273
|
if [ -z "$ISSUE_JSON" ]; then
|
|
@@ -257,7 +279,7 @@ SESSION_JSON=""
|
|
|
257
279
|
if [ -n "$SESSION_ID" ]; then
|
|
258
280
|
SESSION_URL_API="$ISSUE_URL_API/sessions/$SESSION_ID"
|
|
259
281
|
SESSION_JSON=$(curl -sS --fail --max-time "$TIMEOUT" --connect-timeout 5 \
|
|
260
|
-
-
|
|
282
|
+
-K <(crashlytics_auth_cfg) -H "Accept: application/json" \
|
|
261
283
|
"$SESSION_URL_API" 2>/dev/null || true)
|
|
262
284
|
fi
|
|
263
285
|
|
|
@@ -128,7 +128,13 @@ except Exception:
|
|
|
128
128
|
fi
|
|
129
129
|
[ -z "$TOKEN_KEY" ] && TOKEN_KEY="${USER}_Fortify_Access_Token"
|
|
130
130
|
|
|
131
|
-
|
|
131
|
+
# Locate the credential helper via the resolver so Copilot-only installs work.
|
|
132
|
+
# shellcheck disable=SC1090,SC1091
|
|
133
|
+
. "$HOME/.claude/lib/credential-store-resolver.sh" 2>/dev/null \
|
|
134
|
+
|| . "$HOME/.copilot/lib/credential-store-resolver.sh" 2>/dev/null \
|
|
135
|
+
|| { printf '%s\n' '{"status":"blocked","reason":"missing-credential-helper","service":"fortify","expected_key":"'"$TOKEN_KEY"'"}' >&2; exit 2; }
|
|
136
|
+
|
|
137
|
+
TOKEN=$("$CRED_STORE" get "$TOKEN_KEY" 2>/dev/null || true)
|
|
132
138
|
if [ -z "$TOKEN" ]; then
|
|
133
139
|
printf '%s\n' '{"status":"blocked","reason":"missing-token","service":"fortify","expected_key":"'"$TOKEN_KEY"'"}' >&2
|
|
134
140
|
exit 2
|
|
@@ -153,8 +159,11 @@ fortify_get() {
|
|
|
153
159
|
else
|
|
154
160
|
hdr="Authorization: FortifyToken $(printf '%s' "$TOKEN" | base64 | tr -d '\n')"
|
|
155
161
|
fi
|
|
162
|
+
# Auth header goes through a curl config via process substitution so the
|
|
163
|
+
# token never appears in argv (argv is visible to ps).
|
|
156
164
|
resp=$(curl -sS --max-time "$TIMEOUT" --connect-timeout 5 -w "\n%{http_code}" \
|
|
157
|
-
-
|
|
165
|
+
-K <(printf 'header = "%s"\n' "$hdr") \
|
|
166
|
+
-H "Accept: application/json" "$url" 2>/dev/null || true)
|
|
158
167
|
http=$(printf '%s' "$resp" | tail -n1)
|
|
159
168
|
if [ "$http" = "200" ]; then
|
|
160
169
|
printf '%s' "$resp" | sed '$d'
|
|
@@ -57,13 +57,21 @@ trap 'rm -f "$TMP_RAW"' EXIT
|
|
|
57
57
|
# platform; we let curl run with --output then check the size).
|
|
58
58
|
CURL_ARGS=(-sS --fail --max-time "$TIMEOUT" --connect-timeout 5 --location --output "$TMP_RAW" \
|
|
59
59
|
--write-out "%{http_code}\n%{content_type}\n%{size_download}\n")
|
|
60
|
-
if [ -n "${SWAGGER_AUTH_HEADER:-}" ]; then
|
|
61
|
-
CURL_ARGS+=(-H "$SWAGGER_AUTH_HEADER")
|
|
62
|
-
fi
|
|
63
60
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
# The optional auth header may carry a token, so it is fed through a curl
|
|
62
|
+
# config via process substitution instead of argv (argv is visible to ps).
|
|
63
|
+
swagger_auth_cfg() { printf 'header = "%s"\n' "$SWAGGER_AUTH_HEADER"; }
|
|
64
|
+
|
|
65
|
+
if [ -n "${SWAGGER_AUTH_HEADER:-}" ]; then
|
|
66
|
+
META=$(curl "${CURL_ARGS[@]}" -K <(swagger_auth_cfg) "$URL" 2>/dev/null) || {
|
|
67
|
+
echo "ERR: swagger fetch failed for $URL" >&2
|
|
68
|
+
exit 2
|
|
69
|
+
}
|
|
70
|
+
else
|
|
71
|
+
META=$(curl "${CURL_ARGS[@]}" "$URL" 2>/dev/null) || {
|
|
72
|
+
echo "ERR: swagger fetch failed for $URL" >&2
|
|
73
|
+
exit 2
|
|
74
|
+
}
|
|
67
75
|
fi
|
|
68
76
|
|
|
69
77
|
HTTP_CODE=$(printf '%s\n' "$META" | sed -n '1p')
|
|
@@ -88,7 +96,10 @@ case "$LOWER_CT" in
|
|
|
88
96
|
*yaml*|*yml*) FORMAT="yaml" ;;
|
|
89
97
|
*json*) FORMAT="json" ;;
|
|
90
98
|
*)
|
|
91
|
-
case
|
|
99
|
+
# Lowercase via tr: the bash-4 case-conversion expansion is not
|
|
100
|
+
# available on macOS /bin/bash 3.2.
|
|
101
|
+
LOWER_URL=$(printf '%s' "$URL" | tr '[:upper:]' '[:lower:]')
|
|
102
|
+
case "$LOWER_URL" in
|
|
92
103
|
*.yaml|*.yml) FORMAT="yaml" ;;
|
|
93
104
|
*) FORMAT="json" ;;
|
|
94
105
|
esac
|
|
@@ -44,16 +44,20 @@ CLIENT_ID=$(jq -r '.client_id // empty' "$OAUTH_CONFIG")
|
|
|
44
44
|
CLIENT_SECRET=$(jq -r '.client_secret // empty' "$OAUTH_CONFIG")
|
|
45
45
|
[ -n "$CLIENT_ID" ] || { say "figma-mcp-refresh: client_id missing in .figma-oauth.json"; exit 2; }
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
# Refresh token + client secret go through a curl config fed via process
|
|
48
|
+
# substitution so they never appear in argv (argv is visible to ps).
|
|
49
|
+
refresh_grant_cfg() {
|
|
50
|
+
printf 'data-urlencode = "grant_type=refresh_token"\n'
|
|
51
|
+
printf 'data-urlencode = "refresh_token=%s"\n' "$REFRESH_TOKEN"
|
|
52
|
+
printf 'data-urlencode = "client_id=%s"\n' "$CLIENT_ID"
|
|
53
|
+
if [ -n "$CLIENT_SECRET" ]; then
|
|
54
|
+
printf 'data-urlencode = "client_secret=%s"\n' "$CLIENT_SECRET"
|
|
55
|
+
fi
|
|
56
|
+
}
|
|
53
57
|
|
|
54
58
|
RESPONSE=$(curl -s --max-time 20 -X POST "$TOKEN_ENDPOINT" \
|
|
55
59
|
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
56
|
-
|
|
60
|
+
-K <(refresh_grant_cfg))
|
|
57
61
|
|
|
58
62
|
NEW_ACCESS=$(printf '%s' "$RESPONSE" | jq -r '.access_token // empty' 2>/dev/null || true)
|
|
59
63
|
NEW_REFRESH=$(printf '%s' "$RESPONSE" | jq -r '.refresh_token // empty' 2>/dev/null || true)
|
|
@@ -65,14 +69,25 @@ if [ -z "$NEW_ACCESS" ]; then
|
|
|
65
69
|
exit 1
|
|
66
70
|
fi
|
|
67
71
|
|
|
72
|
+
# Keychain write: the add-generic-password command is fed to `security -i`
|
|
73
|
+
# over stdin instead of the direct `security add-generic-password -w <secret>`
|
|
74
|
+
# argv form, so the secret never shows up in process listings. printf is a
|
|
75
|
+
# bash builtin, so no argv exposure there either. Attributes match the
|
|
76
|
+
# previous direct call (-T "" -U). Figma tokens are URL-safe, so embedding in
|
|
77
|
+
# the quoted command string is safe.
|
|
78
|
+
save_keychain_secret() {
|
|
79
|
+
local service="$1" secret="$2"
|
|
80
|
+
security delete-generic-password -s "$service" -a "$ACCOUNT" >/dev/null 2>&1 || true
|
|
81
|
+
printf 'add-generic-password -U -s "%s" -a "%s" -T "" -w "%s"\n' \
|
|
82
|
+
"$service" "$ACCOUNT" "$secret" | security -i >/dev/null
|
|
83
|
+
}
|
|
84
|
+
|
|
68
85
|
# Save new access token first, then the rotated refresh token - order matters so a
|
|
69
86
|
# mid-write failure never leaves us with a lost refresh token AND a stale access token.
|
|
70
|
-
|
|
71
|
-
security add-generic-password -s "$KEY_NAME" -a "$ACCOUNT" -w "$NEW_ACCESS" -T "" -U
|
|
87
|
+
save_keychain_secret "$KEY_NAME" "$NEW_ACCESS"
|
|
72
88
|
|
|
73
89
|
if [ -n "$NEW_REFRESH" ]; then
|
|
74
|
-
|
|
75
|
-
security add-generic-password -s "$REFRESH_KEY" -a "$ACCOUNT" -w "$NEW_REFRESH" -T "" -U
|
|
90
|
+
save_keychain_secret "$REFRESH_KEY" "$NEW_REFRESH"
|
|
76
91
|
fi
|
|
77
92
|
|
|
78
93
|
say "figma-mcp-refresh: renewed '$KEY_NAME' (expires_in=${EXPIRES_IN}s, refresh $( [ -n "$NEW_REFRESH" ] && echo rotated || echo unchanged ))"
|
|
@@ -42,7 +42,14 @@ set -euo pipefail
|
|
|
42
42
|
# --- Globals ----------------------------------------------------------------
|
|
43
43
|
|
|
44
44
|
PREFS="$HOME/.claude/multi-agent-preferences.json"
|
|
45
|
-
|
|
45
|
+
# Locate the credential helper via the resolver so Copilot-only installs work.
|
|
46
|
+
# The helper is optional here: the FIGMA_PAT env fallback still applies when
|
|
47
|
+
# no helper is installed, so a resolver miss must not abort the script.
|
|
48
|
+
# shellcheck disable=SC1090,SC1091
|
|
49
|
+
. "$HOME/.claude/lib/credential-store-resolver.sh" 2>/dev/null \
|
|
50
|
+
|| . "$HOME/.copilot/lib/credential-store-resolver.sh" 2>/dev/null \
|
|
51
|
+
|| true
|
|
52
|
+
CRED_STORE="${CRED_STORE:-}"
|
|
46
53
|
FIGMA_API="https://api.figma.com/v1"
|
|
47
54
|
PARALLEL_DL=4
|
|
48
55
|
HTTP_TIMEOUT=60
|
|
@@ -170,9 +177,11 @@ curl_figma() {
|
|
|
170
177
|
local out="$1" url="$2"
|
|
171
178
|
local code attempt sleep_s
|
|
172
179
|
for attempt in 1 2 3; do
|
|
180
|
+
# Token header goes through a curl config via process substitution so it
|
|
181
|
+
# never appears in argv (argv is visible to ps).
|
|
173
182
|
code=$(curl -sS -o "$out" -w '%{http_code}' \
|
|
174
183
|
--max-time "$HTTP_TIMEOUT" --connect-timeout 5 \
|
|
175
|
-
-
|
|
184
|
+
-K <(printf 'header = "X-Figma-Token: %s"\n' "$FIGMA_TOKEN") \
|
|
176
185
|
"$url" || echo "000")
|
|
177
186
|
case "$code" in
|
|
178
187
|
2*) printf '%s' "$code"; return 0 ;;
|
|
@@ -49,11 +49,21 @@ PROMPT_LANG="${PROMPT_LANG:-tr}"
|
|
|
49
49
|
detect_kind() {
|
|
50
50
|
local in="$1"
|
|
51
51
|
case "$in" in
|
|
52
|
-
*atlassian.net/browse/*|*jira*/browse/*|http*://*/browse/*) echo "jira-url" ;;
|
|
53
|
-
*github.com/*/issues/*) echo "gh-url" ;;
|
|
54
|
-
[A-Z]*-[0-9]*) echo "jira-id" ;;
|
|
52
|
+
*atlassian.net/browse/*|*jira*/browse/*|http*://*/browse/*) echo "jira-url"; return ;;
|
|
53
|
+
*github.com/*/issues/*) echo "gh-url"; return ;;
|
|
54
|
+
[A-Z]*-[0-9]*) echo "jira-id"; return ;;
|
|
55
|
+
esac
|
|
56
|
+
# Bare issue number ("316" or "#316") must resolve BEFORE the repo#N glob:
|
|
57
|
+
# `*\#[0-9]*` also matches "#316" with an empty repo prefix, which used to
|
|
58
|
+
# misroute it to gh-short and build https://github.com/<owner>//issues/316.
|
|
59
|
+
local num="${in#\#}"
|
|
60
|
+
case "$num" in
|
|
61
|
+
"") ;;
|
|
62
|
+
*[!0-9]*) ;;
|
|
63
|
+
*) echo "gh-num"; return ;;
|
|
64
|
+
esac
|
|
65
|
+
case "$in" in
|
|
55
66
|
*\#[0-9]*) echo "gh-short" ;;
|
|
56
|
-
[0-9]*) echo "gh-num" ;;
|
|
57
67
|
*) echo "freetext" ;;
|
|
58
68
|
esac
|
|
59
69
|
}
|
|
@@ -87,19 +97,34 @@ branch_for() {
|
|
|
87
97
|
}
|
|
88
98
|
|
|
89
99
|
# --- Provider fetchers (return raw JSON or empty) -----------------------------
|
|
100
|
+
|
|
101
|
+
# Bearer-auth via a curl config fed through process substitution so the
|
|
102
|
+
# token never appears in argv (argv is visible to `ps` / process audit).
|
|
103
|
+
jira_auth_cfg() { printf 'header = "Authorization: Bearer %s"\n' "$1"; }
|
|
104
|
+
|
|
90
105
|
fetch_jira() {
|
|
91
106
|
local key="$1"
|
|
92
107
|
if [ -z "$JIRA_HOST" ] || [ -z "$JIRA_TOKEN_KEY" ]; then
|
|
93
108
|
echo "ERR: ACCOUNT_JIRA_HOST and ACCOUNT_JIRA_TOKEN_KEY must be set" >&2
|
|
94
109
|
return 1
|
|
95
110
|
fi
|
|
111
|
+
# Resolve the credential helper via the shared resolver (works from the
|
|
112
|
+
# repo checkout and from both install trees) - never hardcode the path.
|
|
113
|
+
# A pre-set CRED_STORE (tests, custom installs) is honored as-is.
|
|
114
|
+
if [ -z "${CRED_STORE:-}" ]; then
|
|
115
|
+
# shellcheck disable=SC1090,SC1091
|
|
116
|
+
. "$(cd "$(dirname "$0")" && pwd)/credential-store-resolver.sh" 2>/dev/null \
|
|
117
|
+
|| . "$HOME/.claude/lib/credential-store-resolver.sh" 2>/dev/null \
|
|
118
|
+
|| . "$HOME/.copilot/lib/credential-store-resolver.sh" 2>/dev/null \
|
|
119
|
+
|| { echo "ERR: credential helper not found" >&2; return 1; }
|
|
120
|
+
fi
|
|
96
121
|
local token
|
|
97
|
-
token=$("$
|
|
122
|
+
token=$("$CRED_STORE" get "$JIRA_TOKEN_KEY" 2>/dev/null || true)
|
|
98
123
|
if [ -z "$token" ]; then
|
|
99
124
|
echo "ERR: Jira token not found in credential store ($JIRA_TOKEN_KEY)" >&2
|
|
100
125
|
return 1
|
|
101
126
|
fi
|
|
102
|
-
curl -sf -
|
|
127
|
+
curl -sf -K <(jira_auth_cfg "$token") \
|
|
103
128
|
"https://$JIRA_HOST/rest/api/2/issue/$key?fields=summary,status,issuetype,description,priority,resolution,fixVersions" \
|
|
104
129
|
|| { echo "ERR: Jira fetch failed for $key" >&2; return 1; }
|
|
105
130
|
}
|
|
@@ -81,11 +81,15 @@ cmd_prepare() {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
cmd_commit() {
|
|
84
|
-
|
|
84
|
+
# Process substitution (not a pipe) so the `failed` counter survives the
|
|
85
|
+
# loop - a piped `while` runs in a subshell and the summary exit would
|
|
86
|
+
# always be 0 regardless of per-repo failures.
|
|
87
|
+
local failed=0
|
|
88
|
+
while IFS=$'\t' read -r name url provider; do
|
|
85
89
|
local target="$WORKTREE_ROOT/$name"
|
|
86
90
|
[ ! -d "$target" ] && continue
|
|
87
91
|
cd "$target"
|
|
88
|
-
# Use status --porcelain so untracked files count as "changes" too -
|
|
92
|
+
# Use status --porcelain so untracked files count as "changes" too -
|
|
89
93
|
# dev sessions usually add brand-new files alongside edits, and `git diff`
|
|
90
94
|
# alone misses them.
|
|
91
95
|
if [ -z "$(git status --porcelain 2>/dev/null)" ]; then
|
|
@@ -98,30 +102,45 @@ cmd_commit() {
|
|
|
98
102
|
[ -n "$ISSUE_KEY" ] && subject="$subject [$ISSUE_KEY]"
|
|
99
103
|
# Trim subject to 72 chars
|
|
100
104
|
subject=$(printf '%s' "$subject" | cut -c1-72)
|
|
101
|
-
git commit --quiet -m "$subject"
|
|
105
|
+
if git commit --quiet -m "$subject"; then
|
|
106
|
+
echo "[$name] committed: $subject"
|
|
107
|
+
else
|
|
108
|
+
echo "ERR: [$name] commit failed" >&2
|
|
109
|
+
failed=$((failed + 1))
|
|
110
|
+
fi
|
|
102
111
|
cd - >/dev/null
|
|
103
|
-
|
|
104
|
-
|
|
112
|
+
done < <(list_repos_jq)
|
|
113
|
+
if [ "$failed" -gt 0 ]; then
|
|
114
|
+
echo "ERR: commit failed in $failed repo(s)" >&2
|
|
115
|
+
return 3
|
|
116
|
+
fi
|
|
105
117
|
}
|
|
106
118
|
|
|
107
119
|
cmd_push() {
|
|
108
|
-
|
|
120
|
+
local failed=0
|
|
121
|
+
while IFS=$'\t' read -r name url provider; do
|
|
109
122
|
local target="$WORKTREE_ROOT/$name"
|
|
110
123
|
[ ! -d "$target" ] && continue
|
|
111
124
|
cd "$target"
|
|
112
|
-
git push --quiet -u origin "$BRANCH"
|
|
125
|
+
if git push --quiet -u origin "$BRANCH"; then
|
|
126
|
+
echo "[$name] pushed $BRANCH"
|
|
127
|
+
else
|
|
128
|
+
echo "ERR: [$name] push failed" >&2
|
|
129
|
+
failed=$((failed + 1))
|
|
130
|
+
fi
|
|
113
131
|
cd - >/dev/null
|
|
114
|
-
|
|
115
|
-
|
|
132
|
+
done < <(list_repos_jq)
|
|
133
|
+
if [ "$failed" -gt 0 ]; then
|
|
134
|
+
echo "ERR: push failed in $failed repo(s)" >&2
|
|
135
|
+
return 3
|
|
136
|
+
fi
|
|
116
137
|
}
|
|
117
138
|
|
|
118
139
|
cmd_pr() {
|
|
119
140
|
# Open PR for extras first, then primary with cross-links.
|
|
120
|
-
local primary_name
|
|
141
|
+
local primary_name failed=0
|
|
121
142
|
primary_name=$(python3 -c "import json; d=json.load(open('$STATE')); print(d['primary']['name'])")
|
|
122
|
-
# Extra-repo PR URLs are passed to the primary PR via the _prs.log file below
|
|
123
|
-
# not a shell var - the loop runs in a pipe subshell, so any accumulated var
|
|
124
|
-
# would be lost. The file-based handoff is intentional.
|
|
143
|
+
# Extra-repo PR URLs are passed to the primary PR via the _prs.log file below.
|
|
125
144
|
while IFS=$'\t' read -r name url provider; do
|
|
126
145
|
if [ "$name" = "$primary_name" ]; then continue; fi
|
|
127
146
|
local target="$WORKTREE_ROOT/$name"
|
|
@@ -130,8 +149,16 @@ cmd_pr() {
|
|
|
130
149
|
if [ "$provider" = "github" ] && command -v gh >/dev/null 2>&1; then
|
|
131
150
|
local body="Ref: $ISSUE_KEY
|
|
132
151
|
Part of multi-repo task $TASK_ID."
|
|
133
|
-
gh
|
|
134
|
-
|
|
152
|
+
# Capture output first: in `gh ... | tee ... || echo failed` the `||`
|
|
153
|
+
# binds to tee, so a gh failure was silently reported as success.
|
|
154
|
+
local pr_out
|
|
155
|
+
if pr_out=$(gh pr create --title "$COMMIT_TYPE($name): $ISSUE_TITLE" --body "$body" --fill 2>/dev/null); then
|
|
156
|
+
printf '%s\n' "$pr_out" | tee -a "$WORKTREE_ROOT/_prs.log"
|
|
157
|
+
echo "[$name] PR created"
|
|
158
|
+
else
|
|
159
|
+
echo "ERR: [$name] gh pr create failed" >&2
|
|
160
|
+
failed=$((failed + 1))
|
|
161
|
+
fi
|
|
135
162
|
else
|
|
136
163
|
echo "[$name] PR creation skipped (provider=$provider)"
|
|
137
164
|
fi
|
|
@@ -147,17 +174,54 @@ Part of multi-repo task $TASK_ID."
|
|
|
147
174
|
local body="Ref: $ISSUE_KEY
|
|
148
175
|
$extras_links"
|
|
149
176
|
if command -v gh >/dev/null 2>&1; then
|
|
150
|
-
gh pr create --title "$COMMIT_TYPE($primary_name): $ISSUE_TITLE" --body "$body" --fill 2>/dev/null
|
|
151
|
-
|
|
177
|
+
if gh pr create --title "$COMMIT_TYPE($primary_name): $ISSUE_TITLE" --body "$body" --fill 2>/dev/null; then
|
|
178
|
+
echo "[$primary_name] PR created"
|
|
179
|
+
else
|
|
180
|
+
echo "ERR: [$primary_name] gh pr create failed" >&2
|
|
181
|
+
failed=$((failed + 1))
|
|
182
|
+
fi
|
|
152
183
|
fi
|
|
153
184
|
cd - >/dev/null
|
|
185
|
+
if [ "$failed" -gt 0 ]; then
|
|
186
|
+
echo "ERR: PR creation failed for $failed repo(s)" >&2
|
|
187
|
+
return 3
|
|
188
|
+
fi
|
|
154
189
|
}
|
|
155
190
|
|
|
191
|
+
# Resolve a directory to its physical path ("" when it does not resolve).
|
|
192
|
+
resolve_dir() { (cd "$1" 2>/dev/null && pwd -P); }
|
|
193
|
+
|
|
156
194
|
cmd_teardown() {
|
|
157
|
-
if [ -d "$WORKTREE_ROOT" ]; then
|
|
158
|
-
echo "[teardown]
|
|
159
|
-
|
|
195
|
+
if [ ! -d "$WORKTREE_ROOT" ]; then
|
|
196
|
+
echo "[teardown] nothing to remove ($WORKTREE_ROOT not a directory)"
|
|
197
|
+
return 0
|
|
198
|
+
fi
|
|
199
|
+
# Guard the rm -rf: worktreeRoot comes from state.json, so a corrupted or
|
|
200
|
+
# malicious value ("." / ".." / "~") must never delete the project, the
|
|
201
|
+
# home directory, or the filesystem root. Resolve the physical path and
|
|
202
|
+
# refuse when it equals or contains the current project root or $HOME.
|
|
203
|
+
local rp cwd home_rp
|
|
204
|
+
rp=$(resolve_dir "$WORKTREE_ROOT") || rp=""
|
|
205
|
+
cwd=$(pwd -P)
|
|
206
|
+
home_rp=$(resolve_dir "$HOME") || home_rp="$HOME"
|
|
207
|
+
if [ -z "$rp" ] || [ "$rp" = "/" ] || [ "$rp" = "$home_rp" ] || [ "$rp" = "$cwd" ]; then
|
|
208
|
+
echo "ERR: refusing teardown - unsafe worktreeRoot '$WORKTREE_ROOT' (resolved: '${rp:-unresolvable}')" >&2
|
|
209
|
+
return 4
|
|
160
210
|
fi
|
|
211
|
+
case "$cwd/" in
|
|
212
|
+
"$rp"/*)
|
|
213
|
+
echo "ERR: refusing teardown - '$rp' contains the current project root" >&2
|
|
214
|
+
return 4
|
|
215
|
+
;;
|
|
216
|
+
esac
|
|
217
|
+
case "$home_rp/" in
|
|
218
|
+
"$rp"/*)
|
|
219
|
+
echo "ERR: refusing teardown - '$rp' contains the home directory" >&2
|
|
220
|
+
return 4
|
|
221
|
+
;;
|
|
222
|
+
esac
|
|
223
|
+
echo "[teardown] removing $rp (logs preserved at ~/.claude/logs/multi-agent/$TASK_ID/)"
|
|
224
|
+
rm -rf "$rp"
|
|
161
225
|
}
|
|
162
226
|
|
|
163
227
|
# Emit agent-state.json v2.1.0 from picker state.json so Phase 0 can consume it directly.
|
|
@@ -61,7 +61,10 @@ resolve_state() {
|
|
|
61
61
|
|
|
62
62
|
write_state() {
|
|
63
63
|
local path="$1" content="$2"
|
|
64
|
-
|
|
64
|
+
# mktemp NEXT TO the target: a $TMPDIR temp file can live on a different
|
|
65
|
+
# filesystem than $HOME, where mv degrades to copy+unlink and loses the
|
|
66
|
+
# atomic-rename guarantee.
|
|
67
|
+
local tmp; tmp=$(mktemp "${path}.XXXXXX")
|
|
65
68
|
printf '%s\n' "$content" > "$tmp"
|
|
66
69
|
mv "$tmp" "$path"
|
|
67
70
|
}
|
|
@@ -93,6 +96,12 @@ do_set() {
|
|
|
93
96
|
echo "plan-todos: input must be an object with .title (string) and .todos (array)" >&2
|
|
94
97
|
exit 2
|
|
95
98
|
fi
|
|
99
|
+
# Per-item validation: every todo needs a string .id and .task, otherwise
|
|
100
|
+
# downstream jq like `.id + "\t" + .task` crashes on (null + string).
|
|
101
|
+
if ! jq -e '.todos | all(type == "object" and (.id | type == "string") and (.task | type == "string"))' <<<"$plan_blob" >/dev/null 2>&1; then
|
|
102
|
+
echo "plan-todos: every todo must be an object with string .id and .task fields" >&2
|
|
103
|
+
exit 2
|
|
104
|
+
fi
|
|
96
105
|
local state_path; state_path=$(resolve_state "$task_id")
|
|
97
106
|
local now; now=$(iso_now)
|
|
98
107
|
local new
|
|
@@ -209,7 +218,7 @@ do_next() {
|
|
|
209
218
|
)
|
|
210
219
|
)
|
|
211
220
|
| (.[0] // empty)
|
|
212
|
-
| if . == null or . == "" then "" else .id + "\t" + .task end
|
|
221
|
+
| if . == null or . == "" then "" else (.id // "") + "\t" + (.task // "") end
|
|
213
222
|
' "$state_path"
|
|
214
223
|
}
|
|
215
224
|
|
|
@@ -229,7 +238,7 @@ do_list() {
|
|
|
229
238
|
skipped: "- [/]",
|
|
230
239
|
failed: "- [!]"
|
|
231
240
|
} as $marks
|
|
232
|
-
| ($marks[.status] // "- [ ]") + " **" + .id + "** " + .task
|
|
241
|
+
| ($marks[.status] // "- [ ]") + " **" + (.id // "") + "** " + (.task // "")
|
|
233
242
|
+ (if (.notes // "") != "" then " _(" + .notes + ")_" else "" end)
|
|
234
243
|
+ (if (.failureReason // "") != "" then " _**failed:** " + .failureReason + "_" else "" end)
|
|
235
244
|
+ (if (.skipReason // "") != "" then " _**skipped:** " + .skipReason + "_" else "" end))
|
|
@@ -75,9 +75,11 @@ ITERATION=$(jq -r '.review.iterationNumber // 1' "$AGENT_STATE")
|
|
|
75
75
|
# --- Decision ---------------------------------------------------------------
|
|
76
76
|
|
|
77
77
|
# Single jq pass - group accepted findings by severity into tab-separated counts.
|
|
78
|
+
# `.review.findings // []` so a null/absent findings array (clean review)
|
|
79
|
+
# cannot raise "Cannot iterate over null" under set -e.
|
|
78
80
|
read -r ACCEPTED_BLOCKING ACCEPTED_IMPORTANT < <(
|
|
79
81
|
jq -r '
|
|
80
|
-
[.review.findings[] | select(.status == "accepted")] as $a
|
|
82
|
+
[(.review.findings // [])[] | select(.status == "accepted")] as $a
|
|
81
83
|
| [($a | map(select(.severity == "blocking")) | length),
|
|
82
84
|
($a | map(select(.severity == "important")) | length)]
|
|
83
85
|
| @tsv
|
|
@@ -242,7 +244,7 @@ post_github() {
|
|
|
242
244
|
fi
|
|
243
245
|
|
|
244
246
|
if [ "$DECISION" = "needs_work" ]; then
|
|
245
|
-
jq -c '.review.findings[] | select(.status == "accepted" and (.severity == "blocking" or .severity == "important"))' "$AGENT_STATE" \
|
|
247
|
+
jq -c '(.review.findings // [])[] | select(.status == "accepted" and (.severity == "blocking" or .severity == "important"))' "$AGENT_STATE" \
|
|
246
248
|
| while read -r finding; do
|
|
247
249
|
local sev path line issue_b64 fix_b64 rule_id_b64 issue fix rule_id body fingerprint
|
|
248
250
|
IFS=$'\t' read -r sev path line issue_b64 fix_b64 rule_id_b64 < <(
|
|
@@ -333,7 +335,7 @@ post_bitbucket_server() {
|
|
|
333
335
|
fi
|
|
334
336
|
|
|
335
337
|
if [ "$DECISION" = "needs_work" ]; then
|
|
336
|
-
jq -c '.review.findings[] | select(.status == "accepted" and (.severity == "blocking" or .severity == "important"))' "$AGENT_STATE" \
|
|
338
|
+
jq -c '(.review.findings // [])[] | select(.status == "accepted" and (.severity == "blocking" or .severity == "important"))' "$AGENT_STATE" \
|
|
337
339
|
| while read -r finding; do
|
|
338
340
|
local sev path line issue_b64 fix_b64 rule_id_b64 issue fix rule_id body payload http fingerprint
|
|
339
341
|
IFS=$'\t' read -r sev path line issue_b64 fix_b64 rule_id_b64 < <(
|