@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.
Files changed (30) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/package.json +1 -1
  3. package/pipeline/commands/multi-agent/analysis/SKILL.md +68 -32
  4. package/pipeline/commands/multi-agent/forget/SKILL.md +29 -0
  5. package/pipeline/commands/multi-agent/help/SKILL.md +18 -0
  6. package/pipeline/commands/multi-agent/routines/SKILL.md +30 -0
  7. package/pipeline/commands/multi-agent/save/SKILL.md +46 -0
  8. package/pipeline/commands/multi-agent/sync/SKILL.md +4 -4
  9. package/pipeline/lib/fetch-figma-annotations.sh +237 -0
  10. package/pipeline/multi-agent-refs/analysis-template.md +233 -123
  11. package/pipeline/multi-agent-refs/cross-cli-contract.md +4 -3
  12. package/pipeline/schemas/analysis-spec.schema.json +78 -0
  13. package/pipeline/schemas/figma-project-config.schema.json +49 -0
  14. package/pipeline/schemas/migrations/prefs-2.3.0-to-2.4.0.mjs +32 -0
  15. package/pipeline/schemas/prefs.schema.json +58 -1
  16. package/pipeline/scripts/fixtures/install-layout.tsv +8 -8
  17. package/pipeline/scripts/migrate-prefs.mjs +8 -1
  18. package/pipeline/scripts/routine-registry.mjs +226 -0
  19. package/pipeline/scripts/smoke-fetchers-offline.sh +41 -0
  20. package/pipeline/scripts/smoke-generate-issue.sh +3 -3
  21. package/pipeline/scripts/smoke-migrate-state.sh +3 -3
  22. package/pipeline/scripts/smoke-pref-migration.sh +8 -6
  23. package/pipeline/scripts/smoke-review-readiness.sh +1 -1
  24. package/pipeline/scripts/smoke-routines.sh +84 -0
  25. package/pipeline/scripts/smoke-validate-analysis-doc.sh +161 -0
  26. package/pipeline/scripts/validate-analysis-doc.mjs +196 -0
  27. package/pipeline/skills/shared/core/multi-agent-forget/SKILL.md +22 -0
  28. package/pipeline/skills/shared/core/multi-agent-routines/SKILL.md +20 -0
  29. package/pipeline/skills/shared/core/multi-agent-save/SKILL.md +27 -0
  30. 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