@mmerterden/multi-agent-pipeline 12.1.1 → 12.2.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 +8 -0
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/setup/SKILL.md +9 -3
- package/pipeline/lib/context-link-extractor.sh +40 -0
- package/pipeline/lib/fetch-graylog.sh +233 -0
- package/pipeline/multi-agent-refs/features/external-context-injection.md +3 -0
- package/pipeline/multi-agent-refs/keychain.md +3 -1
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +21 -8
- package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +1 -1
- package/pipeline/preferences-template.json +1 -0
- package/pipeline/schemas/prefs.schema.json +10 -0
- package/pipeline/scripts/fixtures/install-layout.tsv +2 -2
- package/pipeline/scripts/smoke-fetchers-offline.sh +25 -0
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,14 @@ Internal file-layout changes that don't affect the slash-command surface are sti
|
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
+
## [12.2.0] - 2026-07-21
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- **Graylog log-fetch integration** — when a task's ticket/issue carries a transaction id (`trx`/`trxId`/`transaction id`) and/or a conversation id (`conversationId`/`convId`/`X-conversationId`), the pipeline fetches the matching Graylog log messages and injects them as advisory diagnostic context into Phase 1 analysis. Mirrors the Crashlytics analysis-context adapter (advisory only, no Phase-4 gate).
|
|
21
|
+
- New `lib/fetch-graylog.sh` adapter (mirrors the Fortify hosted-service shape): host from `prefs.global.hosts.graylog` (+ `GRAYLOG_HOST_OVERRIDE`), token via `prefs.global.keychainMapping.graylog` → `${USER}_Graylog_Access_Token` through the credential-store resolver. Graylog PAT auth is HTTP Basic `<token>:token` delivered only via a `curl -K` process-substitution config (never on argv). Universal/relative search by full-text OR of the ids, range + limit configurable via env.
|
|
22
|
+
- **Non-blocking by design:** any network/VPN failure degrades to an empty normalized result and exit 0 — a log fetch never blocks a run. Exit codes: 0 ok/degraded, 2 missing-token, 3 genuine auth rejection, 4 usage, 6 host-not-configured.
|
|
23
|
+
- Config surface: `graylog` added to `keychainMapping` + `hosts` in `schemas/prefs.schema.json` and `preferences-template.json`; keychain doc row (non-critical service). Extraction branch added to `lib/context-link-extractor.sh`. Phase-0 deep-fetch step (`state.graylogContext`) + Phase-1 dispatch row + external-context-injection dispatch row. `multi-agent:setup` prompts for the Graylog host (like the Jira host) and discovers the token key. Offline smoke coverage in `scripts/smoke-fetchers-offline.sh`.
|
|
24
|
+
|
|
17
25
|
## [12.1.1] - 2026-07-21
|
|
18
26
|
|
|
19
27
|
### Changed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mmerterden/multi-agent-pipeline",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.2.0",
|
|
4
4
|
"description": "8-phase AI development pipeline with full orchestration on Claude Code and Copilot CLI. Analysis, planning, TDD, CLI-aware parallel review with consensus surfacing + Fable triage, default-FAIL evidence gates, secret + intent guards, per-phase cost ledger, persistent learnings memory, wiki generation, commit automation. Token-preserving uninstall.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -99,6 +99,7 @@ These are the RECOMMENDED key names. When creating NEW keys, use these. But exis
|
|
|
99
99
|
|
|
100
100
|
> Multi-agent setup onboards **both** Figma tokens because the pipeline's 3-tier Figma access chain (see `pipeline/rules/figma-pipeline.md`) uses them in priority order: Tier 1 = MCP (`figma_mcp`), Tier 2 = REST PAT (`figma_pat`), Tier 3 = user screenshot fallback. Missing either token forces a downshift; missing both leaves Tier 3 as the only path.
|
|
101
101
|
| `fortify` | `${USER}_Fortify_Access_Token` | `fortify` |
|
|
102
|
+
| `graylog` | `${USER}_Graylog_Access_Token` | `graylog` |
|
|
102
103
|
| `firebase` | `${USER}_Firebase_Access_Json` | `firebase` (any variant: `sa`, `service`, `account`, `access`, `json`) |
|
|
103
104
|
| `jenkins` | `${USER}_Jenkins_Access_Token` | `jenkins` |
|
|
104
105
|
|
|
@@ -180,6 +181,7 @@ Save the resolved mapping to preferences:
|
|
|
180
181
|
"figma_pat": "${USER}_Figma_Access_Token",
|
|
181
182
|
"figma_mcp": "${USER}_Figma_Mcp_Access_Token",
|
|
182
183
|
"fortify": null,
|
|
184
|
+
"graylog": null,
|
|
183
185
|
"firebase": "${USER}_Firebase_Access_Json",
|
|
184
186
|
"jenkins": null
|
|
185
187
|
}
|
|
@@ -331,7 +333,7 @@ Update `prefs.global.keychainMapping.{service}` with the key name.
|
|
|
331
333
|
Update `identity.servicePatMap.{service}` with the key name.
|
|
332
334
|
Update `serviceStatus.{service} = { ok: true, checkedAt: <now> }`.
|
|
333
335
|
|
|
334
|
-
**→ Next: if `{service}` is `jira` / `confluence` / `bitbucket` / `fortify` AND the corresponding
|
|
336
|
+
**→ Next: if `{service}` is `jira` / `confluence` / `bitbucket` / `fortify` / `graylog` AND the corresponding
|
|
335
337
|
`prefs.global.hosts.{service}` is unset, run Step 3.5 (Host Prompt) inline before
|
|
336
338
|
the Auto-routing rule below. required - the pipeline cannot build API URLs without
|
|
337
339
|
the host. Firebase is skipped here (host is always `console.firebase.google.com` /
|
|
@@ -352,7 +354,7 @@ This builds `platformIdentityRouting` incrementally - no separate Step 7 neede
|
|
|
352
354
|
|
|
353
355
|
### Step 3.5 - Host Prompt (embedded in Token Save Flow)
|
|
354
356
|
|
|
355
|
-
**Not a standalone step** - runs inline at the end of the Token Save Flow whenever the saved token belongs to a **hosted service** (Jira, Confluence, Bitbucket, Fortify) AND the host is not yet in `prefs.global.hosts`. Firebase tokens skip this step - Crashlytics is always on Google's fixed domains and `project_id` is embedded in the SA JSON.
|
|
357
|
+
**Not a standalone step** - runs inline at the end of the Token Save Flow whenever the saved token belongs to a **hosted service** (Jira, Confluence, Bitbucket, Fortify, Graylog) AND the host is not yet in `prefs.global.hosts`. Firebase tokens skip this step - Crashlytics is always on Google's fixed domains and `project_id` is embedded in the SA JSON.
|
|
356
358
|
|
|
357
359
|
Right after Step D (identity mapping confirmed), ask:
|
|
358
360
|
|
|
@@ -380,6 +382,7 @@ Resulting shape:
|
|
|
380
382
|
"confluence": "confluence.example.com",
|
|
381
383
|
"bitbucket": "bitbucket.example.com",
|
|
382
384
|
"fortify": "ssc.example.com",
|
|
385
|
+
"graylog": "graylog.example.com",
|
|
383
386
|
"corpDomain": "example.com"
|
|
384
387
|
},
|
|
385
388
|
"defaultJiraKey": "PROJ"
|
|
@@ -387,7 +390,9 @@ Resulting shape:
|
|
|
387
390
|
}
|
|
388
391
|
```
|
|
389
392
|
|
|
390
|
-
Placeholders `{JIRA_HOST}`, `{CONFLUENCE_HOST}`, `{BITBUCKET_HOST}`, `{FORTIFY_HOST}`, `{CORP_DOMAIN}`, `{JIRA_KEY}` in skills/commands are resolved from this block at runtime. If a host is ever missing when a phase needs it, Phase 0 prompts the same Host Prompt inline.
|
|
393
|
+
Placeholders `{JIRA_HOST}`, `{CONFLUENCE_HOST}`, `{BITBUCKET_HOST}`, `{FORTIFY_HOST}`, `{GRAYLOG_HOST}`, `{CORP_DOMAIN}`, `{JIRA_KEY}` in skills/commands are resolved from this block at runtime. If a host is ever missing when a phase needs it, Phase 0 prompts the same Host Prompt inline.
|
|
394
|
+
|
|
395
|
+
Graylog uses a **PAT (access token)** whose API auth is HTTP Basic with the token as the username and the literal `token` as the password (`<token>:token`) - no separate username entry. Its host prompt behaves exactly like the Jira host prompt (saved to `prefs.global.hosts.graylog`, resolved as `{GRAYLOG_HOST}`). Graylog is **automatically optional / non-blocking** via the null-mapping convention: leaving `keychainMapping.graylog` (and/or `hosts.graylog`) `null` simply means the log-fetch adapter degrades to empty and never blocks a run - no extra setup code.
|
|
391
396
|
|
|
392
397
|
**Re-run / update**: `/multi-agent:setup hosts` re-opens the prompt to edit values without touching tokens.
|
|
393
398
|
|
|
@@ -706,6 +711,7 @@ Standard key names (used when creating NEW tokens). Discovery may find existing
|
|
|
706
711
|
| `figma_pat` | `${USER}_Figma_Access_Token` | PAT (Tier 2, REST API) | Figma Developer Settings (max 90 days). Token Save Flow prompt copy: "Figma Personal Access Token (for Tier 2)". |
|
|
707
712
|
| `figma_mcp` | `${USER}_Figma_Mcp_Access_Token` | OAuth (Tier 1, MCP) | Automatic via Claude Code Figma MCP remote auth. Token Save Flow prompt copy: "Figma MCP token (for Tier 1)". |
|
|
708
713
|
| `fortify` | `${USER}_Fortify_Access_Token` | API Token | Fortify SSC -> Token Management (VPN required) |
|
|
714
|
+
| `graylog` | `${USER}_Graylog_Access_Token` | PAT (API Basic `<token>:token`) | Graylog -> System -> Users and Teams -> Edit Tokens (VPN required). Auth is HTTP Basic with the token as username and literal `token` as password. |
|
|
709
715
|
| `firebase` | `${USER}_Firebase_Access_Json` | Firebase JSON (base64) | Firebase Console -> Project settings -> Service accounts -> Generate new private key. Project id is parsed from the decoded JSON - no separate entry. |
|
|
710
716
|
| `jenkins` | `${USER}_Jenkins_Access_Token` | API Token | Jenkins -> User -> Configure -> API Token |
|
|
711
717
|
| - | Git Identities | JSON (preferences) | Stored in `$HOME/.claude/multi-agent-preferences.json` - not Keychain |
|
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
# - confluence : Confluence pages (host pattern + display/spaces/pageId)
|
|
12
12
|
# - crashlytics : Firebase Crashlytics issue/session URLs
|
|
13
13
|
# - fortify : Fortify SSC findings URLs
|
|
14
|
+
# - graylog : Graylog trx-id / conversation-id references. Unlike the
|
|
15
|
+
# others this is matched from free text (labels like "trx",
|
|
16
|
+
# "trxid", "transaction id", "conversationId", "convId",
|
|
17
|
+
# "X-conversationId" followed by an id) rather than a URL, so
|
|
18
|
+
# its entries carry "url": null. Advisory diagnostic context;
|
|
19
|
+
# pattern-based + tolerant, a project may refine the labels.
|
|
14
20
|
# - figma : Figma design / make / FigJam / slides URLs
|
|
15
21
|
# - generic-doc : http(s) URLs that didn't match a more specific type but
|
|
16
22
|
# look like documentation (Notion, Google Docs, GitHub
|
|
@@ -26,6 +32,7 @@
|
|
|
26
32
|
# { "type": "swagger", "url": "...", "metadata": { "format": "json|yaml|unknown" } },
|
|
27
33
|
# { "type": "crashlytics","url": "...", "metadata": { "projectId": "...", "platform": "ios|android", "issueId": "...", "sessionId": "...|null" } },
|
|
28
34
|
# { "type": "fortify", "url": "...", "metadata": { "host": "...", "projectId": "...|null" } },
|
|
35
|
+
# { "type": "graylog", "url": null, "metadata": { "idType": "trx|conversation", "id": "...", "label": "..." } },
|
|
29
36
|
# { "type": "figma", "url": "...", "metadata": { "kind": "design|make|board|slides", "fileKey": "...", "nodeId": "...|null" } },
|
|
30
37
|
# ...
|
|
31
38
|
# ]
|
|
@@ -188,5 +195,38 @@ for url in RAW_URLS:
|
|
|
188
195
|
seen.add(key)
|
|
189
196
|
out.append({"type": typ, "url": url, "metadata": meta})
|
|
190
197
|
|
|
198
|
+
# --- Graylog trx / conversation ids (label-based, not URL) -----------------
|
|
199
|
+
# Recognise free-text references like "trxId: ABC123", "transaction id = ...",
|
|
200
|
+
# "conversationId ...", "X-conversationId: ...". Pattern-based + tolerant - a
|
|
201
|
+
# project can refine the label set. Longer/more-specific labels lead the
|
|
202
|
+
# alternation so "trxid" is not shadowed by "trx". The id is alphanumeric plus
|
|
203
|
+
# ._- and long enough (>= 6 chars) to avoid catching stray words.
|
|
204
|
+
GRAYLOG_RE = re.compile(
|
|
205
|
+
r"(?P<label>"
|
|
206
|
+
r"x[\s_-]*conversation[\s_-]*id"
|
|
207
|
+
r"|conversation[\s_-]*id"
|
|
208
|
+
r"|conv[\s_-]*id"
|
|
209
|
+
r"|transaction[\s_-]*id"
|
|
210
|
+
r"|trx[\s_-]*id"
|
|
211
|
+
r"|trx"
|
|
212
|
+
r")"
|
|
213
|
+
r"\s*[:=#]?\s*"
|
|
214
|
+
r"[\"']?(?P<id>[A-Za-z0-9][A-Za-z0-9._-]{5,})",
|
|
215
|
+
re.IGNORECASE,
|
|
216
|
+
)
|
|
217
|
+
for m in GRAYLOG_RE.finditer(text):
|
|
218
|
+
gid = m.group("id").rstrip(".,;:!?)")
|
|
219
|
+
label = m.group("label")
|
|
220
|
+
id_type = "conversation" if "conv" in label.lower() else "trx"
|
|
221
|
+
key = ("graylog", gid)
|
|
222
|
+
if not gid or key in seen:
|
|
223
|
+
continue
|
|
224
|
+
seen.add(key)
|
|
225
|
+
out.append({
|
|
226
|
+
"type": "graylog",
|
|
227
|
+
"url": None,
|
|
228
|
+
"metadata": {"idType": id_type, "id": gid, "label": label},
|
|
229
|
+
})
|
|
230
|
+
|
|
191
231
|
print(json.dumps(out))
|
|
192
232
|
PY
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# fetch-graylog.sh
|
|
4
|
+
# Query a Graylog instance for the log messages tied to a transaction or
|
|
5
|
+
# conversation id and emit a normalized JSON view the pipeline can attach as
|
|
6
|
+
# analysis context. A log fetch must never block a run: on any network failure
|
|
7
|
+
# (VPN down, host unreachable) the fetcher degrades to an empty result and
|
|
8
|
+
# exits 0.
|
|
9
|
+
#
|
|
10
|
+
# Input forms (at least one id is required):
|
|
11
|
+
# ./fetch-graylog.sh --trx <transaction-id>
|
|
12
|
+
# ./fetch-graylog.sh --conv <conversation-id>
|
|
13
|
+
# ./fetch-graylog.sh --trx <id> --conv <id>
|
|
14
|
+
#
|
|
15
|
+
# Required configuration:
|
|
16
|
+
# prefs.global.hosts.graylog - Graylog host (e.g. logs.example.com)
|
|
17
|
+
# prefs.global.keychainMapping.graylog - keychain key holding the API token
|
|
18
|
+
#
|
|
19
|
+
# Optional env:
|
|
20
|
+
# GRAYLOG_TIMEOUT_SECONDS default 25
|
|
21
|
+
# GRAYLOG_HOST_OVERRIDE forces the host (used by tests and CI dry-runs)
|
|
22
|
+
# GRAYLOG_RANGE_SECONDS relative search window, default 86400 (24h)
|
|
23
|
+
# GRAYLOG_LIMIT max messages to return, default 200
|
|
24
|
+
#
|
|
25
|
+
# Auth: Graylog personal access tokens authenticate as HTTP Basic with the
|
|
26
|
+
# token as the username and the literal string "token" as the password. The
|
|
27
|
+
# credential travels through a curl config via process substitution so it
|
|
28
|
+
# never appears in argv (argv is visible to ps).
|
|
29
|
+
#
|
|
30
|
+
# Output (stdout, single JSON object):
|
|
31
|
+
# {
|
|
32
|
+
# "fetchedAt": "<ISO8601>",
|
|
33
|
+
# "source": { "host": "<host>", "transactionId": "..."|null,
|
|
34
|
+
# "conversationId": "..."|null, "query": "..." },
|
|
35
|
+
# "totalResults": <n>,
|
|
36
|
+
# "messages": [
|
|
37
|
+
# { "timestamp": "...", "source": "...", "level": <n>|null,
|
|
38
|
+
# "message": "...", "fields": { ... } }
|
|
39
|
+
# ],
|
|
40
|
+
# "degraded": <bool>, "degradeReason": "..."|null
|
|
41
|
+
# }
|
|
42
|
+
#
|
|
43
|
+
# Exit codes:
|
|
44
|
+
# 0 success, or degraded-empty on a network failure (never blocks a run)
|
|
45
|
+
# 2 missing/expired credential (orchestrator handles the Save Flow)
|
|
46
|
+
# 3 auth rejected on a reachable host (4xx)
|
|
47
|
+
# 4 bad usage
|
|
48
|
+
# 6 host not configured (prefs.global.hosts.graylog is empty)
|
|
49
|
+
|
|
50
|
+
set -eu
|
|
51
|
+
|
|
52
|
+
TRX_ID=""
|
|
53
|
+
CONV_ID=""
|
|
54
|
+
TIMEOUT="${GRAYLOG_TIMEOUT_SECONDS:-25}"
|
|
55
|
+
RANGE="${GRAYLOG_RANGE_SECONDS:-86400}"
|
|
56
|
+
LIMIT="${GRAYLOG_LIMIT:-200}"
|
|
57
|
+
|
|
58
|
+
while [ $# -gt 0 ]; do
|
|
59
|
+
case "$1" in
|
|
60
|
+
--trx) TRX_ID="$2"; shift 2 ;;
|
|
61
|
+
--conv) CONV_ID="$2"; shift 2 ;;
|
|
62
|
+
-h|--help)
|
|
63
|
+
echo "usage: $0 --trx <transaction-id> | --conv <conversation-id>" >&2
|
|
64
|
+
exit 4 ;;
|
|
65
|
+
*)
|
|
66
|
+
echo "ERR: unexpected arg $1" >&2; exit 4 ;;
|
|
67
|
+
esac
|
|
68
|
+
done
|
|
69
|
+
|
|
70
|
+
# Host comes from prefs (private config); skill templates only show {GRAYLOG_HOST}.
|
|
71
|
+
PREFS="$HOME/.claude/multi-agent-preferences.json"
|
|
72
|
+
HOST="${GRAYLOG_HOST_OVERRIDE:-}"
|
|
73
|
+
if [ -z "$HOST" ] && [ -f "$PREFS" ]; then
|
|
74
|
+
HOST=$(python3 -c "
|
|
75
|
+
import json
|
|
76
|
+
try:
|
|
77
|
+
p = json.load(open('$PREFS'))
|
|
78
|
+
print(p.get('global', {}).get('hosts', {}).get('graylog') or '')
|
|
79
|
+
except Exception:
|
|
80
|
+
print('')
|
|
81
|
+
")
|
|
82
|
+
fi
|
|
83
|
+
if [ -z "$HOST" ]; then
|
|
84
|
+
printf '%s\n' '{"status":"blocked","reason":"host-not-configured","service":"graylog","expected_pref":"global.hosts.graylog"}' >&2
|
|
85
|
+
exit 6
|
|
86
|
+
fi
|
|
87
|
+
HOST=${HOST%/} # strip trailing slash if any
|
|
88
|
+
|
|
89
|
+
if [ -z "$TRX_ID" ] && [ -z "$CONV_ID" ]; then
|
|
90
|
+
echo "ERR: no id (pass --trx <transaction-id> and/or --conv <conversation-id>)" >&2
|
|
91
|
+
exit 4
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
# Build the Graylog query from whichever ids were supplied.
|
|
95
|
+
QUERY=""
|
|
96
|
+
if [ -n "$TRX_ID" ]; then
|
|
97
|
+
QUERY="transactionId:\"$TRX_ID\""
|
|
98
|
+
fi
|
|
99
|
+
if [ -n "$CONV_ID" ]; then
|
|
100
|
+
if [ -n "$QUERY" ]; then
|
|
101
|
+
QUERY="$QUERY OR conversationId:\"$CONV_ID\""
|
|
102
|
+
else
|
|
103
|
+
QUERY="conversationId:\"$CONV_ID\""
|
|
104
|
+
fi
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
# Token via keychain mapping.
|
|
108
|
+
TOKEN_KEY=""
|
|
109
|
+
if [ -f "$PREFS" ]; then
|
|
110
|
+
TOKEN_KEY=$(python3 -c "
|
|
111
|
+
import json
|
|
112
|
+
try:
|
|
113
|
+
p = json.load(open('$PREFS'))
|
|
114
|
+
print(p.get('global', {}).get('keychainMapping', {}).get('graylog') or '')
|
|
115
|
+
except Exception:
|
|
116
|
+
print('')
|
|
117
|
+
")
|
|
118
|
+
fi
|
|
119
|
+
[ -z "$TOKEN_KEY" ] && TOKEN_KEY="${USER}_Graylog_Access_Token"
|
|
120
|
+
|
|
121
|
+
# Locate the credential helper via the resolver so Copilot-only installs work.
|
|
122
|
+
# shellcheck disable=SC1090,SC1091
|
|
123
|
+
. "$HOME/.claude/lib/credential-store-resolver.sh" 2>/dev/null \
|
|
124
|
+
|| . "$HOME/.copilot/lib/credential-store-resolver.sh" 2>/dev/null \
|
|
125
|
+
|| { printf '%s\n' '{"status":"blocked","reason":"missing-credential-helper","service":"graylog","expected_key":"'"$TOKEN_KEY"'"}' >&2; exit 2; }
|
|
126
|
+
|
|
127
|
+
TOKEN=$("$CRED_STORE" get "$TOKEN_KEY" 2>/dev/null || true)
|
|
128
|
+
if [ -z "$TOKEN" ]; then
|
|
129
|
+
printf '%s\n' '{"status":"blocked","reason":"missing-token","service":"graylog","expected_key":"'"$TOKEN_KEY"'"}' >&2
|
|
130
|
+
exit 2
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
API_BASE="https://$HOST/api"
|
|
134
|
+
|
|
135
|
+
graylog_get() {
|
|
136
|
+
# Usage: graylog_get <path-and-query-relative-to-API_BASE>
|
|
137
|
+
# Prints "<body>\n<http_code>"; the caller splits the last line off.
|
|
138
|
+
local path="$1"
|
|
139
|
+
local url="$API_BASE/$path"
|
|
140
|
+
# PAT basic auth: username=<token>, password=literal "token". The credential
|
|
141
|
+
# goes through a curl config via process substitution so it never lands in
|
|
142
|
+
# argv (argv is visible to ps).
|
|
143
|
+
curl -sS --max-time "$TIMEOUT" --connect-timeout 5 -w "\n%{http_code}" \
|
|
144
|
+
-K <(printf 'user = "%s:token"\n' "$TOKEN") \
|
|
145
|
+
-H "Accept: application/json" \
|
|
146
|
+
-H "X-Requested-By: multi-agent-pipeline" \
|
|
147
|
+
"$url" 2>/dev/null || true
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# Graylog relative universal search.
|
|
151
|
+
ENC_QUERY=$(QUERY_IN="$QUERY" python3 -c "
|
|
152
|
+
import os, urllib.parse as up
|
|
153
|
+
print(up.quote(os.environ['QUERY_IN']))
|
|
154
|
+
")
|
|
155
|
+
SEARCH_PATH="search/universal/relative?query=$ENC_QUERY&range=$RANGE&limit=$LIMIT&sort=timestamp:desc"
|
|
156
|
+
|
|
157
|
+
RESP=$(graylog_get "$SEARCH_PATH")
|
|
158
|
+
HTTP=$(printf '%s' "$RESP" | tail -n1)
|
|
159
|
+
BODY=$(printf '%s' "$RESP" | sed '$d')
|
|
160
|
+
|
|
161
|
+
# A genuine auth rejection on a reachable host is the only hard failure.
|
|
162
|
+
case "$HTTP" in
|
|
163
|
+
401|403)
|
|
164
|
+
echo "ERR: Graylog auth rejected (HTTP $HTTP)" >&2
|
|
165
|
+
exit 3 ;;
|
|
166
|
+
esac
|
|
167
|
+
|
|
168
|
+
# Any other non-200 (empty code on connection failure, 5xx, timeout) degrades
|
|
169
|
+
# to an empty result so a log fetch never blocks a run.
|
|
170
|
+
DEGRADED="false"
|
|
171
|
+
DEGRADE_REASON=""
|
|
172
|
+
if [ "$HTTP" != "200" ]; then
|
|
173
|
+
DEGRADED="true"
|
|
174
|
+
DEGRADE_REASON="graylog-unreachable"
|
|
175
|
+
BODY=""
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
SRC_HOST="$HOST" SRC_TRX="$TRX_ID" SRC_CONV="$CONV_ID" SRC_QUERY="$QUERY" \
|
|
179
|
+
DEGRADED_IN="$DEGRADED" DEGRADE_REASON_IN="$DEGRADE_REASON" \
|
|
180
|
+
BODY_IN="$BODY" LIMIT_IN="$LIMIT" \
|
|
181
|
+
python3 - <<'PY'
|
|
182
|
+
import json, os, datetime
|
|
183
|
+
|
|
184
|
+
def safe_load(s):
|
|
185
|
+
try:
|
|
186
|
+
return json.loads(s) if s else None
|
|
187
|
+
except Exception:
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
body = safe_load(os.environ.get("BODY_IN") or "") or {}
|
|
191
|
+
raw = body.get("messages") or []
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
limit = int(os.environ.get("LIMIT_IN") or "200")
|
|
195
|
+
except Exception:
|
|
196
|
+
limit = 200
|
|
197
|
+
|
|
198
|
+
messages = []
|
|
199
|
+
for item in raw[:limit]:
|
|
200
|
+
m = item.get("message") if isinstance(item, dict) else None
|
|
201
|
+
if not isinstance(m, dict):
|
|
202
|
+
continue
|
|
203
|
+
reserved = ("timestamp", "source", "level", "message")
|
|
204
|
+
fields = {k: v for k, v in m.items() if k not in reserved}
|
|
205
|
+
messages.append({
|
|
206
|
+
"timestamp": m.get("timestamp") or "",
|
|
207
|
+
"source": m.get("source") or "",
|
|
208
|
+
"level": m.get("level") if isinstance(m.get("level"), (int, float)) else None,
|
|
209
|
+
"message": (m.get("message") or "")[:2000],
|
|
210
|
+
"fields": fields,
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
trx = os.environ.get("SRC_TRX") or None
|
|
214
|
+
conv = os.environ.get("SRC_CONV") or None
|
|
215
|
+
total = body.get("total_results")
|
|
216
|
+
if not isinstance(total, int):
|
|
217
|
+
total = len(messages)
|
|
218
|
+
|
|
219
|
+
result = {
|
|
220
|
+
"fetchedAt": datetime.datetime.utcnow().isoformat() + "Z",
|
|
221
|
+
"source": {
|
|
222
|
+
"host": os.environ["SRC_HOST"],
|
|
223
|
+
"transactionId": trx,
|
|
224
|
+
"conversationId": conv,
|
|
225
|
+
"query": os.environ.get("SRC_QUERY") or "",
|
|
226
|
+
},
|
|
227
|
+
"totalResults": total,
|
|
228
|
+
"messages": messages,
|
|
229
|
+
"degraded": os.environ.get("DEGRADED_IN") == "true",
|
|
230
|
+
"degradeReason": (os.environ.get("DEGRADE_REASON_IN") or None),
|
|
231
|
+
}
|
|
232
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
233
|
+
PY
|
|
@@ -22,6 +22,7 @@ One fetcher per type. Each fetcher emits a normalized JSON view that the analysi
|
|
|
22
22
|
|---|---|---|
|
|
23
23
|
| `crashlytics` | `~/.claude/lib/fetch-crashlytics.sh <url>` (already invoked in Phase 0 Step 1b.1 for the legacy `state.crashContext` field; Phase 1 reads that field directly) | `state.crashContext` |
|
|
24
24
|
| `fortify` | `~/.claude/lib/fetch-fortify.sh <url>` (already invoked in Phase 0 Step 1b.2 for the legacy `state.fortifyFinding` field; Phase 1 reads that field, Phase 4 reads the full payload for the security gate) | `state.fortifyFinding` |
|
|
25
|
+
| `graylog` | `~/.claude/lib/fetch-graylog.sh --trx <id>` / `--conv <id>` (already invoked in Phase 0 Step 1b.3 for the `state.graylogContext` field; Phase 1 reads that field directly). Diagnostic logs, **advisory only** - no Phase 4 gate, no generated task | `state.graylogContext` |
|
|
25
26
|
| `swagger` | `~/.claude/lib/fetch-swagger.sh <url>` → endpoints[], request/response examples | `state.fetchedContext.swagger[]` |
|
|
26
27
|
| `confluence` | `~/.claude/lib/fetch-confluence.sh <url>` → page body, code blocks, extracted API contracts | `state.fetchedContext.confluence[]` |
|
|
27
28
|
| `figma` | no standalone fetcher - resolve via the Figma 3-tier chain (Tier 1 MCP tools, Tier 2 `~/.claude/lib/figma-screenshot.sh` + REST) when the task is a component; otherwise advisory only | `state.fetchedContext.figma[]` |
|
|
@@ -36,6 +37,8 @@ One fetcher per type. Each fetcher emits a normalized JSON view that the analysi
|
|
|
36
37
|
|
|
37
38
|
Failures are never fatal at Phase 1 - the agent still runs analysis, just without the referenced source.
|
|
38
39
|
|
|
40
|
+
**Graylog specifics.** `fetch-graylog.sh` degrades to empty on any network/VPN failure: it emits a normalized empty object and exits `0`, so an unreachable host reads as `{status:"skipped", reason:"vpn-unreachable"}` and never blocks. Only a genuine auth rejection on a reachable host exits `3` (marked `failed`, still non-fatal here). `state.graylogContext` is prepended to the analysis prompt inside the **Referenced External Sources** section as diagnostic context that is **advisory only** - the agent may use it to orient on a reported error, but code remains ground truth, and there is no Phase 4 review gate for logs.
|
|
41
|
+
|
|
39
42
|
## Prompt injection shape
|
|
40
43
|
|
|
41
44
|
Added immediately after the knowledge-injection block, before codebase exploration:
|
|
@@ -17,7 +17,7 @@ When a token resolves but the service rejects it (401 / 403), do not silently sk
|
|
|
17
17
|
|
|
18
18
|
- **Regenerate** - the existing token is dead; open Save Flow Step B to replace `<KEY_NAME>` in place (same key name, clipboard path), then retry the operation.
|
|
19
19
|
- **Use a different token** - map a new or existing Keychain entry for this service (Save Flow Step B + identity binding), then retry.
|
|
20
|
-
- **Skip and continue** - drop this token's contribution. Non-critical services (Confluence enrichment, Fortify scan, Firebase events, Figma fallback tiers) continue with reduced evidence and a one-line warning. A token that is structurally required for the input (e.g. the Jira PAT for a Jira-ID input) halts instead, since the run cannot proceed without it.
|
|
20
|
+
- **Skip and continue** - drop this token's contribution. Non-critical services (Confluence enrichment, Fortify scan, Graylog log fetch, Firebase events, Figma fallback tiers) continue with reduced evidence and a one-line warning. A token that is structurally required for the input (e.g. the Jira PAT for a Jira-ID input) halts instead, since the run cannot proceed without it.
|
|
21
21
|
|
|
22
22
|
The question asks a *choice*; the replacement value still flows through the clipboard, never chat. `smoke-no-token-prompt.sh` greps for value-prompts (`enter token`, `paste token`, `API key:`) - decision labels like `Regenerate` / `Skip and continue` do not trip it.
|
|
23
23
|
|
|
@@ -64,6 +64,7 @@ The shell driver auto-delegates to `~/.claude/scripts/keychain.py` on macOS / Li
|
|
|
64
64
|
| `figma` | Figma | `${USER}_Figma_Access_Token` | PAT |
|
|
65
65
|
| `figma_mcp` | Figma MCP | `${USER}_Figma_Mcp_Access_Token` | OAuth |
|
|
66
66
|
| `fortify` | Fortify | `${USER}_Fortify_Access_Token` | API Token |
|
|
67
|
+
| `graylog` | Graylog | `${USER}_Graylog_Access_Token` | API Token |
|
|
67
68
|
| `firebase` | Firebase | `${USER}_Firebase_Access_Json` | JSON (base64) |
|
|
68
69
|
| `jenkins` | Jenkins CI | `${USER}_Jenkins_Access_Token` | API Token |
|
|
69
70
|
| - | Git Identity | Stored in preferences JSON | Not Keychain |
|
|
@@ -82,6 +83,7 @@ The shell driver auto-delegates to `~/.claude/scripts/keychain.py` on macOS / Li
|
|
|
82
83
|
"figma": "${USER}_Figma_Access_Token",
|
|
83
84
|
"figma_mcp": "${USER}_Figma_Mcp_Access_Token",
|
|
84
85
|
"fortify": "${USER}_Fortify_Access_Token",
|
|
86
|
+
"graylog": "${USER}_Graylog_Access_Token",
|
|
85
87
|
"firebase": "${USER}_Firebase_Access_Json",
|
|
86
88
|
"jenkins": "${USER}_Jenkins_Access_Token"
|
|
87
89
|
}
|
|
@@ -53,7 +53,7 @@ Then STOP Phase 0 - do not proceed with a broken token state. This prevents th
|
|
|
53
53
|
|
|
54
54
|
**Key design**: Every field is an **array of all historical values** (most recent first) → "recently used" list.
|
|
55
55
|
|
|
56
|
-
**Token resolution rule**: ALWAYS check `prefs.global.keychainMapping.<service_id>` first for the key name - this holds the user's actual Keychain key name (which may differ from the standard convention). Fall back to standard key name ONLY when setup has never run (mapping absent). The standard fallback exists as a safety net, not as the primary path. Service IDs: `jira`, `bitbucket_token`, `bitbucket_user`, `github`, `confluence`, `figma`, `figma_mcp`, `fortify`, `firebase`, `jenkins`.
|
|
56
|
+
**Token resolution rule**: ALWAYS check `prefs.global.keychainMapping.<service_id>` first for the key name - this holds the user's actual Keychain key name (which may differ from the standard convention). Fall back to standard key name ONLY when setup has never run (mapping absent). The standard fallback exists as a safety net, not as the primary path. Service IDs: `jira`, `bitbucket_token`, `bitbucket_user`, `github`, `confluence`, `figma`, `figma_mcp`, `fortify`, `firebase`, `jenkins`, `graylog`.
|
|
57
57
|
|
|
58
58
|
**Jira project key resolution** (first match wins):
|
|
59
59
|
|
|
@@ -196,7 +196,7 @@ Sequential prompts (standard UX pattern with Recent suggestion): Project Key →
|
|
|
196
196
|
|
|
197
197
|
**Token pre-check** (after parsing): Jira input → resolve key via `prefs.global.keychainMapping.jira`, verify token with a lightweight API call (e.g. `GET /myself`). GitHub input → verify `gh auth status`. On failure (missing key, 401, 403) → run the **Token Save Flow** from `setup.md` inline. This is the same clipboard-based flow used during setup - token never appears in terminal. If user skips and the token is critical for the input type (e.g. Jira token for Jira input), halt Phase 0.
|
|
198
198
|
|
|
199
|
-
**VPN connectivity check**: Test VPN-dependent services (Jira, Bitbucket, Confluence, Fortify) with `curl --connect-timeout 3`. If unreachable, warn and offer to continue. Fallback: Jira → manual input, Bitbucket → `git push` only, Confluence → skip Phase 7, Fortify → skip scan. Cache in `agent-state.json` → `"vpnServices": {"jira": true, ...}`.
|
|
199
|
+
**VPN connectivity check**: Test VPN-dependent services (Jira, Bitbucket, Confluence, Fortify, Graylog) with `curl --connect-timeout 3`. If unreachable, warn and offer to continue. Fallback: Jira → manual input, Bitbucket → `git push` only, Confluence → skip Phase 7, Fortify → skip scan, Graylog → skip log fetch (advisory only, never blocks). Cache in `agent-state.json` → `"vpnServices": {"jira": true, ...}`.
|
|
200
200
|
|
|
201
201
|
#### Step 1b - URL Enrichment (catalogue + targeted deep fetches)
|
|
202
202
|
|
|
@@ -225,6 +225,7 @@ Output schema (one array entry per URL - full contract in `~/.claude/lib/conte
|
|
|
225
225
|
{ "type": "confluence", "url": "<url>", "metadata": { "host": "<host>", "pageId": "<id|null>", "spaceKey": "<key|null>" } },
|
|
226
226
|
{ "type": "crashlytics", "url": "<url>", "metadata": { "projectId": "...", "platform": "ios|android", "bundle": "...", "issueId": "...", "sessionId": "<id|null>" } },
|
|
227
227
|
{ "type": "fortify", "url": "<url>", "metadata": { "host": "<host>", "projectId": "<id|null>" } },
|
|
228
|
+
{ "type": "graylog", "url": null, "metadata": { "idType": "trx|conversation", "id": "...", "label": "..." } },
|
|
228
229
|
{ "type": "figma", "url": "<url>", "metadata": { "kind": "design|make|board|slides", "fileKey": "...", "nodeId": "<id|null>" } },
|
|
229
230
|
{ "type": "generic-doc", "url": "<url>", "metadata": { "host": "<host>", "kind": "notion|google-docs|gist|github-doc" } }
|
|
230
231
|
]
|
|
@@ -258,7 +259,18 @@ URL pattern (caught by the extractor): `https://<prefs.global.hosts.fortify>/ssc
|
|
|
258
259
|
- Phase 2 Planning injects `state.fortifyFinding` into the architectural-review prompt under a **Known Security Finding** section, and the generated task breakdown includes a task addressing the finding (naming convention: `sec(fortify-<issueId>): <issueName> at <file>:<line>`).
|
|
259
260
|
- Soft-fail: 401 → re-run Token Save Flow for `fortify`; 403 → warn and skip; network timeout → respect VPN fallback (skip enrichment, mark `state.fortifyFinding = { skipped: "vpn-unreachable" }`).
|
|
260
261
|
|
|
261
|
-
##### Step 1b.3 -
|
|
262
|
+
##### Step 1b.3 - Graylog deep fetch (runs when `type == "graylog"` present in `state.contextLinks[]`)
|
|
263
|
+
|
|
264
|
+
The extractor emits `graylog` entries from free-text trx / conversation ids (labels like `trx`, `trxId`, `transaction id`, `conversationId`, `convId`, `X-conversationId`), not from a URL - so these entries carry `"url": null`. When at least one is present, pull the matching diagnostic logs.
|
|
265
|
+
|
|
266
|
+
- Host comes from `prefs.global.hosts.graylog` (`GRAYLOG_HOST_OVERRIDE` env forces it for tests/CI); no literal host lives in any source file. Missing host → the fetcher exits `6`; log a setup hint and skip.
|
|
267
|
+
- Token resolution: `prefs.global.keychainMapping.graylog` → `~/.claude/lib/credential-store.sh get "<key>"` → Graylog PAT (fallback key `${USER}_Graylog_Access_Token`). Auth is HTTP Basic with the token as username and the literal `token` as password; the token is fed to `curl` only via a `-K` config file (process substitution), never on argv.
|
|
268
|
+
- Fetch logs (VPN required; respect `vpnServices.graylog`): call `~/.claude/lib/fetch-graylog.sh --trx <id>` (or `--conv <id>` for conversation ids) once per extracted id. The fetcher emits a normalized JSON object of matching log messages.
|
|
269
|
+
- Store as `state.graylogContext = { idType, id, messages[], fetchedAt }` (or an array keyed per id when several were extracted).
|
|
270
|
+
- Phase 1 Analysis prepends `state.graylogContext` to the agent prompt under the **Referenced External Sources** section as **diagnostic context, advisory only** (never contradicts code; no task is generated from it, no Phase 4 gate).
|
|
271
|
+
- Soft-fail (non-blocking, mirrors Crashlytics): unreachable host / VPN down / network timeout → the fetcher emits a normalized empty object and exits `0`; mark `state.graylogContext = { skipped: "vpn-unreachable" }` and continue. Only a genuine auth rejection (4xx on a reachable host, fetcher exit `3`) is logged as `skipped` with the auth reason. A log fetch never blocks the run.
|
|
272
|
+
|
|
273
|
+
##### Step 1b.4 - Other link types (catalogued only at Phase 0; fetched at Phase 1)
|
|
262
274
|
|
|
263
275
|
For `swagger`, `confluence`, `figma`, `generic-doc` entries in `state.contextLinks[]`, no Phase 0 fetch runs. The entries pass through to Phase 1, which dispatches the matching fetcher (Block B work: `fetch-swagger.sh`, `fetch-confluence.sh`, etc.). Until those fetchers land, the entries are surfaced in Phase 1 context as "referenced but not fetched - agent should treat the URL as an authoritative external source and read it directly when relevant."
|
|
264
276
|
|
|
@@ -267,11 +279,11 @@ For `swagger`, `confluence`, `figma`, `generic-doc` entries in `state.contextLin
|
|
|
267
279
|
**Log line shape** (progress contract):
|
|
268
280
|
|
|
269
281
|
```
|
|
270
|
-
→ context links: total=<N>, by-type={swagger:<n>, confluence:<n>, crashlytics:<n>, fortify:<n>, figma:<n>, generic-doc:<n>}
|
|
271
|
-
→ URL deep fetch: crashlytics=<hit|miss|skipped>, fortify=<hit|miss|skipped>
|
|
282
|
+
→ context links: total=<N>, by-type={swagger:<n>, confluence:<n>, crashlytics:<n>, fortify:<n>, graylog:<n>, figma:<n>, generic-doc:<n>}
|
|
283
|
+
→ URL deep fetch: crashlytics=<hit|miss|skipped>, fortify=<hit|miss|skipped>, graylog=<hit|miss|skipped>
|
|
272
284
|
```
|
|
273
285
|
|
|
274
|
-
Two lines emit at the end of Step 1b. The first reports the catalogue from 1b.0 (always runs). The second reports the deep-fetch outcomes from 1b.1 / 1b.2 - `miss` =
|
|
286
|
+
Two lines emit at the end of Step 1b. The first reports the catalogue from 1b.0 (always runs). The second reports the deep-fetch outcomes from 1b.1 / 1b.2 / 1b.3 - `miss` = type not present in catalogue (normal), `skipped` = present but auth/VPN/mismatch skipped the fetch (for graylog this includes the non-blocking VPN-unreachable degrade), `hit` = context stored in state.
|
|
275
287
|
|
|
276
288
|
#### Step 2 - Project Selection
|
|
277
289
|
|
|
@@ -484,11 +496,12 @@ Single-repo mode (`projects.length === 1` or scalar-only) uses the legacy single
|
|
|
484
496
|
"inputType": "github-issue-url|github-issue-number|jira-url|jira-id|free-text",
|
|
485
497
|
"jiraId": "PROJ-XXXXX|null", "figmaUrl": "...|null",
|
|
486
498
|
"contextLinks": [
|
|
487
|
-
{ "type": "swagger|confluence|crashlytics|fortify|figma|generic-doc",
|
|
488
|
-
"url": "<full-url>", "metadata": { } }
|
|
499
|
+
{ "type": "swagger|confluence|crashlytics|fortify|graylog|figma|generic-doc",
|
|
500
|
+
"url": "<full-url|null>", "metadata": { } }
|
|
489
501
|
],
|
|
490
502
|
"crashContext": null,
|
|
491
503
|
"fortifyFinding": null,
|
|
504
|
+
"graylogContext": null,
|
|
492
505
|
"localMode": false, "instructionDriven": true|false, "instructionFiles": {},
|
|
493
506
|
"identity": {"name": "...", "email": "...", "keychainKey": "..."},
|
|
494
507
|
"currentPhase": 0, "status": "in_progress", "startedAt": "{ISO}", "shortId": "#N",
|
|
@@ -75,7 +75,7 @@ Progress lines:
|
|
|
75
75
|
|
|
76
76
|
#### Step 1.5 - External Context Injection (`state.contextLinks[]`)
|
|
77
77
|
|
|
78
|
-
Phase 0 Step 1b catalogued every typed external link from the task description into `state.contextLinks[]`. Phase 1 dispatches each entry to its matching fetcher (crashlytics, fortify, swagger, confluence, figma, generic-doc) and prepends results under a **Referenced External Sources** section in the analysis prompt - so the agent doesn't re-discover what the ticket already pointed at. Failures never fatal; pending refs are advisories. Full dispatch table, exit-code handling, prompt injection shape, log line shape: `$HOME/.claude/multi-agent-refs/features/external-context-injection.md`.
|
|
78
|
+
Phase 0 Step 1b catalogued every typed external link from the task description into `state.contextLinks[]`. Phase 1 dispatches each entry to its matching fetcher (crashlytics, fortify, graylog, swagger, confluence, figma, generic-doc) and prepends results under a **Referenced External Sources** section in the analysis prompt - so the agent doesn't re-discover what the ticket already pointed at. `state.graylogContext` is injected there too, as diagnostic context (advisory only). Failures never fatal (a non-zero fetcher exit is marked skipped and the analysis still runs, exactly as for crashlytics); pending refs are advisories. Full dispatch table, exit-code handling, prompt injection shape, log line shape: `$HOME/.claude/multi-agent-refs/features/external-context-injection.md`.
|
|
79
79
|
|
|
80
80
|
**Log line shape** (progress contract):
|
|
81
81
|
|
|
@@ -157,6 +157,12 @@
|
|
|
157
157
|
"null"
|
|
158
158
|
]
|
|
159
159
|
},
|
|
160
|
+
"graylog": {
|
|
161
|
+
"type": [
|
|
162
|
+
"string",
|
|
163
|
+
"null"
|
|
164
|
+
]
|
|
165
|
+
},
|
|
160
166
|
"firebase": {
|
|
161
167
|
"type": [
|
|
162
168
|
"string",
|
|
@@ -482,6 +488,10 @@
|
|
|
482
488
|
"type": "string",
|
|
483
489
|
"description": "Fortify SSC host without scheme, e.g. ssc.example.com. Used by Phase 0 Step 1b URL enrichment to validate pasted Fortify URLs and for direct SSC REST API calls. Resolves {FORTIFY_HOST}."
|
|
484
490
|
},
|
|
491
|
+
"graylog": {
|
|
492
|
+
"type": "string",
|
|
493
|
+
"description": "Graylog host without scheme, e.g. logs.example.com. Used by the Graylog log-fetch adapter for direct REST API calls. Resolves {GRAYLOG_HOST}."
|
|
494
|
+
},
|
|
485
495
|
"corpDomain": {
|
|
486
496
|
"type": "string",
|
|
487
497
|
"description": "Corporate email / cookie domain, e.g. example.com. Resolves {CORP_DOMAIN}."
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
.claude/CLAUDE.md 1
|
|
3
3
|
.claude/agents 8
|
|
4
4
|
.claude/commands 44
|
|
5
|
-
.claude/lib
|
|
5
|
+
.claude/lib 26
|
|
6
6
|
.claude/multi-agent-preferences.json 1
|
|
7
7
|
.claude/multi-agent-refs 51
|
|
8
8
|
.claude/rules 12
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
.copilot/.pipeline-version 1
|
|
14
14
|
.copilot/agents 8
|
|
15
15
|
.copilot/copilot-instructions.md 1
|
|
16
|
-
.copilot/lib
|
|
16
|
+
.copilot/lib 26
|
|
17
17
|
.copilot/schemas 23
|
|
18
18
|
.copilot/scripts 193
|
|
19
19
|
.copilot/skills 467
|
|
@@ -261,6 +261,30 @@ expect_rc 0 "stub token + offline curl degrades to empty result (exit 0)" \
|
|
|
261
261
|
"$SH" "$LIB/fetch-fortify.sh" "$FORTIFY_URL"
|
|
262
262
|
assert_in_file '"gateOutcome"' "$OUT" "normalized JSON still emitted"
|
|
263
263
|
|
|
264
|
+
# -----------------------------------------------------------------------------
|
|
265
|
+
note "4b. fetch-graylog.sh"
|
|
266
|
+
# -----------------------------------------------------------------------------
|
|
267
|
+
expect_rc 6 "no host configured exits 6" \
|
|
268
|
+
env HOME="$FAKE_HOME" USER=smoke \
|
|
269
|
+
"$SH" "$LIB/fetch-graylog.sh" --trx T123
|
|
270
|
+
expect_rc 4 "host override + no ids exits 4 (usage)" \
|
|
271
|
+
env HOME="$FAKE_HOME" USER=smoke GRAYLOG_HOST_OVERRIDE=logs.example.com \
|
|
272
|
+
"$SH" "$LIB/fetch-graylog.sh"
|
|
273
|
+
|
|
274
|
+
expect_rc 2 "host override + id reaches credential stage" \
|
|
275
|
+
env HOME="$FAKE_HOME" USER=smoke GRAYLOG_HOST_OVERRIDE=logs.example.com \
|
|
276
|
+
"$SH" "$LIB/fetch-graylog.sh" --trx T123 --conv C456
|
|
277
|
+
assert_in_file "missing-token" "$ERR" "blocked JSON names missing-token"
|
|
278
|
+
|
|
279
|
+
# Non-blocking contract: with a stub token and a failing curl, the fetch must
|
|
280
|
+
# degrade to an empty normalized result (messages present) and exit 0 so a log
|
|
281
|
+
# fetch never blocks a run.
|
|
282
|
+
expect_rc 0 "stub token + offline curl degrades to empty result (exit 0)" \
|
|
283
|
+
env HOME="$FAKE_HOME" USER=smoke GRAYLOG_HOST_OVERRIDE=logs.example.com \
|
|
284
|
+
FAKE_CRED_VALUE=fake-token PATH="$BIN_CURL_FAIL:$PATH" \
|
|
285
|
+
"$SH" "$LIB/fetch-graylog.sh" --trx T123
|
|
286
|
+
assert_in_file '"messages"' "$OUT" "normalized JSON still emitted"
|
|
287
|
+
|
|
264
288
|
# -----------------------------------------------------------------------------
|
|
265
289
|
note "5. figma-screenshot.sh"
|
|
266
290
|
# -----------------------------------------------------------------------------
|
|
@@ -338,6 +362,7 @@ OWNED_FILES=(
|
|
|
338
362
|
"$LIB/fetch-crashlytics.sh"
|
|
339
363
|
"$LIB/fetch-swagger.sh"
|
|
340
364
|
"$LIB/fetch-fortify.sh"
|
|
365
|
+
"$LIB/fetch-graylog.sh"
|
|
341
366
|
"$LIB/figma-mcp-refresh.sh"
|
|
342
367
|
"$LIB/figma-screenshot.sh"
|
|
343
368
|
"$LIB/credential-store.sh"
|