@mmerterden/multi-agent-pipeline 12.2.0 → 12.4.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 +84 -0
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/analysis/SKILL.md +68 -32
- package/pipeline/commands/multi-agent/forget/SKILL.md +29 -0
- package/pipeline/commands/multi-agent/help/SKILL.md +18 -0
- package/pipeline/commands/multi-agent/routines/SKILL.md +30 -0
- package/pipeline/commands/multi-agent/save/SKILL.md +46 -0
- package/pipeline/commands/multi-agent/sync/SKILL.md +4 -4
- package/pipeline/lib/fetch-figma-annotations.sh +237 -0
- package/pipeline/multi-agent-refs/analysis-template.md +233 -123
- package/pipeline/multi-agent-refs/cross-cli-contract.md +4 -3
- package/pipeline/schemas/analysis-spec.schema.json +78 -0
- package/pipeline/schemas/figma-project-config.schema.json +49 -0
- package/pipeline/schemas/migrations/prefs-2.3.0-to-2.4.0.mjs +32 -0
- package/pipeline/schemas/prefs.schema.json +58 -1
- package/pipeline/scripts/fixtures/install-layout.tsv +8 -8
- package/pipeline/scripts/migrate-prefs.mjs +8 -1
- package/pipeline/scripts/routine-registry.mjs +226 -0
- package/pipeline/scripts/smoke-fetchers-offline.sh +41 -0
- package/pipeline/scripts/smoke-generate-issue.sh +3 -3
- package/pipeline/scripts/smoke-migrate-state.sh +3 -3
- package/pipeline/scripts/smoke-pref-migration.sh +8 -6
- package/pipeline/scripts/smoke-review-readiness.sh +1 -1
- package/pipeline/scripts/smoke-routines.sh +84 -0
- package/pipeline/scripts/smoke-validate-analysis-doc.sh +161 -0
- package/pipeline/scripts/validate-analysis-doc.mjs +196 -0
- package/pipeline/skills/shared/core/multi-agent-forget/SKILL.md +22 -0
- package/pipeline/skills/shared/core/multi-agent-routines/SKILL.md +20 -0
- package/pipeline/skills/shared/core/multi-agent-save/SKILL.md +27 -0
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +4 -4
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# fetch-figma-annotations.sh
|
|
4
|
+
#
|
|
5
|
+
# Tier-2 (Figma REST) fallback for /multi-agent:analysis annotation ingestion.
|
|
6
|
+
# Fetches Dev Mode annotations for one or more nodes and emits them as the
|
|
7
|
+
# authoritative copy for each node (the visible text layer is a placeholder).
|
|
8
|
+
#
|
|
9
|
+
# Tier 1 (Figma MCP get_design_context) is preferred and handled in the skill;
|
|
10
|
+
# this script is the no-MCP fallback. Generic: no project file keys, no
|
|
11
|
+
# language codes, and no corporate transport are baked in - the caller passes
|
|
12
|
+
# the file key, node ids, and the recognized language prefixes.
|
|
13
|
+
#
|
|
14
|
+
# Usage:
|
|
15
|
+
# fetch-figma-annotations.sh --file-key <key> --node-id <id>[,<id>...] \
|
|
16
|
+
# [--lang-prefixes TR,EN] [--batch 4]
|
|
17
|
+
# fetch-figma-annotations.sh --url <figma-url> [--lang-prefixes TR,EN]
|
|
18
|
+
#
|
|
19
|
+
# Token source: credential-store.sh get <logical-key>, where <logical-key> is
|
|
20
|
+
# read from prefs.global.keychainMapping.figma_pat. Falls back to the FIGMA_PAT
|
|
21
|
+
# environment variable. No literal keychain service name is embedded here.
|
|
22
|
+
#
|
|
23
|
+
# Label parsing (config-driven via --lang-prefixes, ordered):
|
|
24
|
+
# prefixed : lines like "TR: ...", "EN: ..." win regardless of order
|
|
25
|
+
# two-line : line 1 -> prefixes[0], line 2 -> prefixes[1]
|
|
26
|
+
# single : one line -> prefixes[0]
|
|
27
|
+
# empty : no label text
|
|
28
|
+
#
|
|
29
|
+
# Output (stdout, single JSON object):
|
|
30
|
+
# { "ok": true, "source": "figma-rest", "fileKey": "...", "fetchedAt": "...",
|
|
31
|
+
# "count": N, "withBase": M,
|
|
32
|
+
# "annotations": [ {nodeId, name, designText, raw, parsed:{<lang>:val}, mode} ] }
|
|
33
|
+
# On missing/rejected token: { "ok": false, "reason": "missing-token" } and exit 2.
|
|
34
|
+
#
|
|
35
|
+
# Exit codes: 0 success (incl. zero annotations), 1 usage, 2 auth,
|
|
36
|
+
# 4 node not found, 5 rate limited, 6 network/other fatal.
|
|
37
|
+
|
|
38
|
+
set -uo pipefail
|
|
39
|
+
|
|
40
|
+
PREFS="$HOME/.claude/multi-agent-preferences.json"
|
|
41
|
+
# shellcheck disable=SC1090,SC1091
|
|
42
|
+
. "$HOME/.claude/lib/credential-store-resolver.sh" 2>/dev/null \
|
|
43
|
+
|| . "$HOME/.copilot/lib/credential-store-resolver.sh" 2>/dev/null \
|
|
44
|
+
|| true
|
|
45
|
+
CRED_STORE="${CRED_STORE:-}"
|
|
46
|
+
FIGMA_API="https://api.figma.com/v1"
|
|
47
|
+
HTTP_TIMEOUT=60
|
|
48
|
+
|
|
49
|
+
FILE_KEY=""
|
|
50
|
+
NODE_IDS=""
|
|
51
|
+
URL=""
|
|
52
|
+
LANG_PREFIXES="TR,EN"
|
|
53
|
+
BATCH=4
|
|
54
|
+
|
|
55
|
+
log() { printf '%s\n' "$*" >&2; }
|
|
56
|
+
die() { local code="$1"; shift; log "fetch-figma-annotations: $*"; exit "$code"; }
|
|
57
|
+
|
|
58
|
+
while [ $# -gt 0 ]; do
|
|
59
|
+
case "$1" in
|
|
60
|
+
--file-key) FILE_KEY="${2:-}"; shift 2 ;;
|
|
61
|
+
--node-id) NODE_IDS="${2:-}"; shift 2 ;;
|
|
62
|
+
--url) URL="${2:-}"; shift 2 ;;
|
|
63
|
+
--lang-prefixes) LANG_PREFIXES="${2:-TR,EN}"; shift 2 ;;
|
|
64
|
+
--batch) BATCH="${2:-4}"; shift 2 ;;
|
|
65
|
+
-h|--help) grep -E '^#( |$)' "$0" | sed -E 's/^# ?//'; exit 0 ;;
|
|
66
|
+
*) die 1 "unknown argument: $1" ;;
|
|
67
|
+
esac
|
|
68
|
+
done
|
|
69
|
+
|
|
70
|
+
# Parse fileKey + nodeId from a Figma URL when given.
|
|
71
|
+
if [ -n "$URL" ]; then
|
|
72
|
+
parsed_key=$(printf '%s' "$URL" | sed -nE 's#.*figma\.com/(design|file)/([A-Za-z0-9]+).*#\2#p')
|
|
73
|
+
parsed_node=$(printf '%s' "$URL" | sed -nE 's#.*[?&]node-id=([^&]+).*#\1#p' | sed 's/-/:/')
|
|
74
|
+
[ -z "$FILE_KEY" ] && FILE_KEY="$parsed_key"
|
|
75
|
+
[ -z "$NODE_IDS" ] && NODE_IDS="$parsed_node"
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
[ -z "$FILE_KEY" ] && die 1 "missing --file-key (or a --url to parse it from)"
|
|
79
|
+
[ -z "$NODE_IDS" ] && die 1 "missing --node-id (or a --url to parse it from)"
|
|
80
|
+
|
|
81
|
+
# Resolve the Figma PAT (keychain via prefs mapping, then FIGMA_PAT env).
|
|
82
|
+
resolve_token() {
|
|
83
|
+
local token_key=""
|
|
84
|
+
if [ -f "$PREFS" ] && command -v python3 >/dev/null 2>&1; then
|
|
85
|
+
token_key=$(python3 -c "
|
|
86
|
+
import json
|
|
87
|
+
try:
|
|
88
|
+
p = json.load(open('$PREFS'))
|
|
89
|
+
print(p.get('global', {}).get('keychainMapping', {}).get('figma_pat') or '')
|
|
90
|
+
except Exception:
|
|
91
|
+
print('')
|
|
92
|
+
" 2>/dev/null || true)
|
|
93
|
+
fi
|
|
94
|
+
if [ -n "$token_key" ] && [ -n "$CRED_STORE" ] && [ -x "$CRED_STORE" ]; then
|
|
95
|
+
local tok
|
|
96
|
+
tok=$("$CRED_STORE" get "$token_key" 2>/dev/null || true)
|
|
97
|
+
if [ -n "$tok" ]; then printf '%s' "$tok"; return 0; fi
|
|
98
|
+
fi
|
|
99
|
+
if [ -n "${FIGMA_PAT:-}" ]; then printf '%s' "$FIGMA_PAT"; return 0; fi
|
|
100
|
+
return 1
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
FIGMA_TOKEN=$(resolve_token || true)
|
|
104
|
+
if [ -z "$FIGMA_TOKEN" ]; then
|
|
105
|
+
printf '{"ok":false,"reason":"missing-token","source":"figma-rest","fileKey":"%s"}\n' "$FILE_KEY"
|
|
106
|
+
die 2 "no Figma PAT (map prefs.global.keychainMapping.figma_pat or set FIGMA_PAT)"
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
TMPDIR_A=$(mktemp -d "${TMPDIR:-/tmp}/figma-annot.XXXXXX")
|
|
110
|
+
trap 'rm -rf "$TMPDIR_A"' EXIT INT TERM
|
|
111
|
+
|
|
112
|
+
# curl one /nodes batch (token via -K config so it never lands in argv).
|
|
113
|
+
curl_nodes() {
|
|
114
|
+
local out="$1" ids="$2" code
|
|
115
|
+
code=$(curl -sS -o "$out" -w '%{http_code}' \
|
|
116
|
+
--max-time "$HTTP_TIMEOUT" --connect-timeout 5 \
|
|
117
|
+
-K <(printf 'header = "X-Figma-Token: %s"\n' "$FIGMA_TOKEN") \
|
|
118
|
+
"$FIGMA_API/files/$FILE_KEY/nodes?ids=$ids" || echo "000")
|
|
119
|
+
printf '%s' "$code"
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# Split comma ids into batches of $BATCH, fetch each into TMPDIR_A/part-N.json.
|
|
123
|
+
OLD_IFS="$IFS"; IFS=','; set -- $NODE_IDS; IFS="$OLD_IFS"
|
|
124
|
+
part=0; batch_ids=""; n=0
|
|
125
|
+
flush_batch() {
|
|
126
|
+
[ -z "$batch_ids" ] && return 0
|
|
127
|
+
part=$((part + 1))
|
|
128
|
+
local code
|
|
129
|
+
code=$(curl_nodes "$TMPDIR_A/part-$part.json" "$batch_ids")
|
|
130
|
+
case "$code" in
|
|
131
|
+
2*) : ;;
|
|
132
|
+
401|403) die 2 "auth rejected (HTTP $code)" ;;
|
|
133
|
+
404) die 4 "node(s) not found (HTTP 404)" ;;
|
|
134
|
+
429) die 5 "rate limited (HTTP 429)" ;;
|
|
135
|
+
*) die 6 "figma call failed (HTTP $code)" ;;
|
|
136
|
+
esac
|
|
137
|
+
batch_ids=""; n=0
|
|
138
|
+
}
|
|
139
|
+
for id in "$@"; do
|
|
140
|
+
[ -z "$id" ] && continue
|
|
141
|
+
if [ -z "$batch_ids" ]; then batch_ids="$id"; else batch_ids="$batch_ids,$id"; fi
|
|
142
|
+
n=$((n + 1))
|
|
143
|
+
[ "$n" -ge "$BATCH" ] && flush_batch
|
|
144
|
+
done
|
|
145
|
+
flush_batch
|
|
146
|
+
|
|
147
|
+
# Walk all node docs, collect + parse annotations. Python only parses local
|
|
148
|
+
# response files; the token is never passed to it.
|
|
149
|
+
LANG_PREFIXES="$LANG_PREFIXES" FILE_KEY="$FILE_KEY" python3 - "$TMPDIR_A" <<'PY'
|
|
150
|
+
import json, os, sys, glob, datetime
|
|
151
|
+
|
|
152
|
+
parts_dir = sys.argv[1]
|
|
153
|
+
prefixes = [p.strip() for p in os.environ.get("LANG_PREFIXES", "TR,EN").split(",") if p.strip()]
|
|
154
|
+
base = (prefixes[0].lower() if prefixes else "tr")
|
|
155
|
+
|
|
156
|
+
def parse_label(raw):
|
|
157
|
+
if not raw or not raw.strip():
|
|
158
|
+
return {}, "empty"
|
|
159
|
+
lines = [ln.strip() for ln in raw.splitlines() if ln.strip()]
|
|
160
|
+
parsed = {}
|
|
161
|
+
# prefixed: "TR: ...", "EN: ..."
|
|
162
|
+
prefixed = False
|
|
163
|
+
for ln in lines:
|
|
164
|
+
for p in prefixes:
|
|
165
|
+
lp = p.lower()
|
|
166
|
+
low = ln.lower()
|
|
167
|
+
if low.startswith(lp + ":") or low.startswith(lp + " :"):
|
|
168
|
+
parsed[lp] = ln.split(":", 1)[1].strip()
|
|
169
|
+
prefixed = True
|
|
170
|
+
if prefixed:
|
|
171
|
+
return parsed, "prefixed"
|
|
172
|
+
if len(lines) >= 2 and len(prefixes) >= 2:
|
|
173
|
+
return {prefixes[0].lower(): lines[0], prefixes[1].lower(): lines[1]}, "two-line"
|
|
174
|
+
if len(lines) == 1 and prefixes:
|
|
175
|
+
return {prefixes[0].lower(): lines[0]}, "single"
|
|
176
|
+
# fallback: join everything under the base language
|
|
177
|
+
return {base: " ".join(lines)}, "single"
|
|
178
|
+
|
|
179
|
+
annotations = []
|
|
180
|
+
|
|
181
|
+
def walk(node):
|
|
182
|
+
if not isinstance(node, dict):
|
|
183
|
+
return
|
|
184
|
+
anns = node.get("annotations")
|
|
185
|
+
if isinstance(anns, list) and anns:
|
|
186
|
+
raw_parts = []
|
|
187
|
+
for a in anns:
|
|
188
|
+
if not isinstance(a, dict):
|
|
189
|
+
continue
|
|
190
|
+
lbl = a.get("labelMarkdown") or a.get("label") or ""
|
|
191
|
+
if lbl:
|
|
192
|
+
raw_parts.append(lbl)
|
|
193
|
+
raw = "\n".join(raw_parts)
|
|
194
|
+
parsed, mode = parse_label(raw)
|
|
195
|
+
design_text = node.get("characters")
|
|
196
|
+
annotations.append({
|
|
197
|
+
"nodeId": node.get("id"),
|
|
198
|
+
"name": node.get("name"),
|
|
199
|
+
"designText": design_text if isinstance(design_text, str) else None,
|
|
200
|
+
"raw": raw or None,
|
|
201
|
+
"parsed": parsed,
|
|
202
|
+
"mode": mode,
|
|
203
|
+
})
|
|
204
|
+
for child in node.get("children", []) or []:
|
|
205
|
+
walk(child)
|
|
206
|
+
|
|
207
|
+
for pf in sorted(glob.glob(os.path.join(parts_dir, "part-*.json"))):
|
|
208
|
+
try:
|
|
209
|
+
doc = json.load(open(pf))
|
|
210
|
+
except Exception:
|
|
211
|
+
continue
|
|
212
|
+
nodes = doc.get("nodes", {})
|
|
213
|
+
if isinstance(nodes, dict):
|
|
214
|
+
for _, entry in nodes.items():
|
|
215
|
+
if isinstance(entry, dict) and isinstance(entry.get("document"), dict):
|
|
216
|
+
walk(entry["document"])
|
|
217
|
+
|
|
218
|
+
with_base = sum(1 for a in annotations if a["parsed"].get(base))
|
|
219
|
+
# Flag base-present-but-a-target-missing to stderr (kick back to content team).
|
|
220
|
+
for a in annotations:
|
|
221
|
+
if a["parsed"].get(base):
|
|
222
|
+
missing = [p.lower() for p in prefixes if not a["parsed"].get(p.lower())]
|
|
223
|
+
if missing:
|
|
224
|
+
sys.stderr.write("WARN: node %s has %s but missing %s\n" % (
|
|
225
|
+
a.get("nodeId"), base, ",".join(missing)))
|
|
226
|
+
|
|
227
|
+
out = {
|
|
228
|
+
"ok": True,
|
|
229
|
+
"source": "figma-rest",
|
|
230
|
+
"fileKey": os.environ.get("FILE_KEY"),
|
|
231
|
+
"fetchedAt": datetime.datetime.utcnow().replace(microsecond=0).isoformat() + "Z",
|
|
232
|
+
"count": len(annotations),
|
|
233
|
+
"withBase": with_base,
|
|
234
|
+
"annotations": annotations,
|
|
235
|
+
}
|
|
236
|
+
print(json.dumps(out, ensure_ascii=False, indent=2))
|
|
237
|
+
PY
|