@mmerterden/multi-agent-pipeline 15.16.0 → 16.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.
Files changed (90) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/README.md +2 -2
  3. package/README.tr.md +1 -2
  4. package/docs/architecture.md +2 -2
  5. package/docs/ecosystem.md +9 -6
  6. package/docs/features.md +2 -3
  7. package/install/_plugin-skills.mjs +28 -2
  8. package/install/templates/copilot-instructions.md +8 -7
  9. package/package.json +1 -1
  10. package/pipeline/commands/multi-agent/SKILL.md +5 -6
  11. package/pipeline/commands/multi-agent/analysis/SKILL.md +77 -588
  12. package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +5 -69
  13. package/pipeline/commands/multi-agent/channels/SKILL.md +1 -1
  14. package/pipeline/commands/multi-agent/complaint-analysis/SKILL.md +5 -4
  15. package/pipeline/commands/multi-agent/dev/SKILL.md +8 -280
  16. package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +12 -124
  17. package/pipeline/commands/multi-agent/dev-local/SKILL.md +8 -111
  18. package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +11 -113
  19. package/pipeline/commands/multi-agent/help/SKILL.md +61 -56
  20. package/pipeline/commands/multi-agent/ios-coding-standard/SKILL.md +4 -2
  21. package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +2 -4
  22. package/pipeline/commands/multi-agent/refactor/SKILL.md +1 -1
  23. package/pipeline/commands/multi-agent/resume-local/SKILL.md +3 -3
  24. package/pipeline/commands/multi-agent/setup/SKILL.md +2 -2
  25. package/pipeline/commands/multi-agent/stack/SKILL.md +10 -9
  26. package/pipeline/commands/multi-agent/store-ready/SKILL.md +1 -1
  27. package/pipeline/commands/multi-agent/sync/SKILL.md +2 -2
  28. package/pipeline/commands/multi-agent/update/SKILL.md +1 -1
  29. package/pipeline/lib/context-link-extractor.sh +38 -0
  30. package/pipeline/lib/fetch-document.sh +190 -0
  31. package/pipeline/multi-agent-refs/_dev-context.md +4 -0
  32. package/pipeline/multi-agent-refs/analysis/evidence.md +213 -0
  33. package/pipeline/multi-agent-refs/analysis/intake.md +167 -0
  34. package/pipeline/multi-agent-refs/analysis/locked.md +53 -0
  35. package/pipeline/multi-agent-refs/analysis/render.md +133 -0
  36. package/pipeline/multi-agent-refs/analysis/resolve.md +76 -0
  37. package/pipeline/multi-agent-refs/analysis/synthesis.md +98 -0
  38. package/pipeline/multi-agent-refs/analysis-template.md +58 -11
  39. package/pipeline/multi-agent-refs/complaint-analysis-template.md +1 -1
  40. package/pipeline/multi-agent-refs/component-dispatch.md +5 -5
  41. package/pipeline/multi-agent-refs/cross-cli-contract.md +9 -7
  42. package/pipeline/multi-agent-refs/features/skill-conformance.md +1 -1
  43. package/pipeline/multi-agent-refs/features/url-enrichment.md +13 -3
  44. package/pipeline/multi-agent-refs/knowledge.md +2 -2
  45. package/pipeline/multi-agent-refs/payload-contracts.md +1 -1
  46. package/pipeline/multi-agent-refs/phases/modes.md +73 -53
  47. package/pipeline/multi-agent-refs/phases/phase-0-init.md +21 -2
  48. package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +26 -0
  49. package/pipeline/multi-agent-refs/phases/phase-2-planning.md +26 -12
  50. package/pipeline/multi-agent-refs/phases/phase-3-dev.md +17 -18
  51. package/pipeline/multi-agent-refs/phases/phase-4-review.md +29 -7
  52. package/pipeline/multi-agent-refs/phases/phase-5-test.md +1 -1
  53. package/pipeline/multi-agent-refs/phases/phase-6-commit.md +8 -0
  54. package/pipeline/multi-agent-refs/phases/phase-7-report.md +1 -1
  55. package/pipeline/multi-agent-refs/phases.md +9 -7
  56. package/pipeline/multi-agent-refs/progress-contract.md +1 -1
  57. package/pipeline/multi-agent-refs/readiness-review.md +2 -0
  58. package/pipeline/multi-agent-refs/rules.md +2 -2
  59. package/pipeline/multi-agent-refs/tracker-contract.md +32 -13
  60. package/pipeline/multi-agent-refs/wiki-capture.md +2 -2
  61. package/pipeline/schemas/agent-state.schema.json +3 -3
  62. package/pipeline/schemas/analysis-output.schema.json +17 -0
  63. package/pipeline/schemas/analysis-spec.schema.json +69 -1
  64. package/pipeline/schemas/complaint-analysis-spec.schema.json +1 -1
  65. package/pipeline/schemas/prefs.schema.json +47 -0
  66. package/pipeline/schemas/token-budget.json +2 -2
  67. package/pipeline/scripts/_stack-routing.mjs +17 -12
  68. package/pipeline/scripts/build-stack-plugins.mjs +16 -6
  69. package/pipeline/scripts/cost-table.json +1 -1
  70. package/pipeline/scripts/gen-mode-dispatch.mjs +20 -30
  71. package/pipeline/scripts/run-aggregator.mjs +1 -1
  72. package/pipeline/scripts/validate-analysis-doc.mjs +36 -3
  73. package/pipeline/skills/.skills-index.json +40 -7
  74. package/pipeline/skills/shared/README.md +12 -9
  75. package/pipeline/skills/shared/core/multi-agent/SKILL.md +9 -5
  76. package/pipeline/skills/shared/core/multi-agent-dev/SKILL.md +7 -61
  77. package/pipeline/skills/shared/core/multi-agent-dev-autopilot/SKILL.md +11 -51
  78. package/pipeline/skills/shared/core/multi-agent-dev-local/SKILL.md +7 -33
  79. package/pipeline/skills/shared/core/multi-agent-dev-local-autopilot/SKILL.md +10 -38
  80. package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +28 -18
  81. package/pipeline/skills/shared/core/multi-agent-ios-coding-standard/SKILL.md +4 -2
  82. package/pipeline/skills/shared/core/multi-agent-local-autopilot/SKILL.md +2 -4
  83. package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +1 -1
  84. package/pipeline/skills/shared/core/multi-agent-resume-local/SKILL.md +2 -2
  85. package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +10 -9
  86. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +2 -2
  87. package/pipeline/skills/shared/external/evidence-github/SKILL.md +45 -0
  88. package/pipeline/skills/shared/external/evidence-registry/SKILL.md +33 -0
  89. package/pipeline/skills/shared/external/signal-community/SKILL.md +44 -0
  90. package/pipeline/skills/skills-index.md +10 -7
@@ -7,7 +7,7 @@ allowed-tools: Bash, Read, Edit, Write, AskUserQuestion
7
7
 
8
8
  # multi-agent stack - Select Stack(s) via Plugin Enablement
9
9
 
10
- Stack skills ship as plugins in the `{owner}/multi-agent-plugins` marketplace. Selecting a stack = **enabling the matching plugin(s)** in the current repo's `.claude/settings.json` `enabledPlugins`. The `ai-common-toolkit` (accessibility audit, humanizer, Firebase) is always enabled alongside the stack plugin(s).
10
+ Stack skills ship as plugins in the `{owner}/multi-agent-plugins` marketplace. Selecting a stack = **enabling the matching plugin(s)** in the current repo's `.claude/settings.json` `enabledPlugins`. Two toolkits are stack-independent and always enabled alongside the stack plugin(s): `ai-common-toolkit` (accessibility audit, humanizer, Firebase) and `ai-analyst-toolkit` (GitHub and package-registry evidence, community signal for analysis work).
11
11
 
12
12
  On Claude Code the marketplace plugins are the ONLY source of stack skills - nothing is copied into `~/.claude/skills` anymore. A stack that is not enabled here is simply absent from the session. Copilot CLI and Codex CLI have no plugin loader; they receive a local copy filtered to the enabled stacks at install time, which is why step 5 below offers to refresh those copies after a change.
13
13
 
@@ -31,10 +31,10 @@ This replaces the old `stack-swap.sh` mechanic that physically moved skill direc
31
31
 
32
32
  | Stack | Plugins enabled (all `@multi-agent-plugins`) |
33
33
  |---|---|
34
- | `ios` | `ai-common-toolkit`, `ai-ios-toolkit` |
35
- | `android` | `ai-common-toolkit`, `ai-android-toolkit` |
36
- | `frontend` / `web` | `ai-common-toolkit`, `ai-frontend-toolkit` |
37
- | `backend` | `ai-common-toolkit`, `ai-backend-toolkit` |
34
+ | `ios` | `ai-common-toolkit`, `ai-analyst-toolkit`, `ai-ios-toolkit` |
35
+ | `android` | `ai-common-toolkit`, `ai-analyst-toolkit`, `ai-android-toolkit` |
36
+ | `frontend` / `web` | `ai-common-toolkit`, `ai-analyst-toolkit`, `ai-frontend-toolkit` |
37
+ | `backend` | `ai-common-toolkit`, `ai-analyst-toolkit`, `ai-backend-toolkit` |
38
38
  | `mobile` | common + `ai-ios-toolkit` + `ai-android-toolkit` |
39
39
  | `fullstack` | common + `ai-frontend-toolkit` + `ai-backend-toolkit` |
40
40
  | `all` | common + all four stack toolkits |
@@ -53,7 +53,7 @@ Multiple args union their plugin sets: `ios backend` → common + iOS + backend.
53
53
  3. **Write rules:**
54
54
  - every plugin in the union is set to `true`
55
55
  - every stack toolkit **not** in the union is set to `false` (leave non-`@multi-agent-plugins` entries untouched)
56
- - `ai-common-toolkit@multi-agent-plugins` is always `true`
56
+ - `ai-common-toolkit@multi-agent-plugins` and `ai-analyst-toolkit@multi-agent-plugins` are always `true`
57
57
  - **legacy-key cleanup**: delete any `ai-*-engineering-toolkit@multi-agent-plugins` keys - those plugin names were retired by the `ai-<stack>-toolkit` rename and a stale `true` there enables a plugin that no longer exists in the marketplace
58
58
  4. **Unknown arg** → show the table above; do not guess.
59
59
  5. **Copilot/Codex refresh offer.** Their local skill copies are filtered to the enabled stacks at install time, so after a change ask (single `AskUserQuestion`, not silent): "Refresh Copilot/Codex local copies now?" - Yes runs `node <pipelineRepo>/install.js --copilot --codex`, No leaves them stale with a one-line warning naming the command to run later. Skip this question entirely when neither `~/.copilot` nor `~/.codex` exists.
@@ -71,13 +71,14 @@ if ! claude marketplace list 2>/dev/null | grep -q "$MP"; then
71
71
  fi
72
72
 
73
73
  COMMON="ai-common-toolkit@${MP}"
74
+ ANALYST="ai-analyst-toolkit@${MP}"
74
75
  IOS="ai-ios-toolkit@${MP}"
75
76
  ANDROID="ai-android-toolkit@${MP}"
76
77
  FRONTEND="ai-frontend-toolkit@${MP}"
77
78
  BACKEND="ai-backend-toolkit@${MP}"
78
79
 
79
80
  # Enable-mode only: with zero args the write rules below would set every stack
80
- # toolkit to false (nothing but $COMMON is in $ON), silently wiping the repo's
81
+ # toolkit to false (nothing but the always-on pair is in $ON), silently wiping the repo's
81
82
  # selection. No args belongs to the picker / status path (Behaviour step 1).
82
83
  if [ "$#" -eq 0 ]; then
83
84
  echo "No stack given - nothing changed. Pass stacks (ios backend ...) or use the picker."
@@ -85,7 +86,7 @@ if [ "$#" -eq 0 ]; then
85
86
  fi
86
87
 
87
88
  # resolve every arg through the alias table, union the ON set
88
- ON="$COMMON"
89
+ ON="$COMMON $ANALYST"
89
90
  for ARG in "$@"; do
90
91
  case "$ARG" in
91
92
  ios) ON="$ON $IOS" ;;
@@ -104,7 +105,7 @@ done
104
105
  After resolving `$ON`, edit `.claude/settings.json` (create `{ "enabledPlugins": {} }` if absent) so that:
105
106
  - every plugin in `$ON` is set to `true`,
106
107
  - every stack toolkit **not** in `$ON` is set to `false` (leave non-`@multi-agent-plugins` entries untouched),
107
- - `ai-common-toolkit@multi-agent-plugins` is always `true`,
108
+ - `ai-common-toolkit@multi-agent-plugins` and `ai-analyst-toolkit@multi-agent-plugins` are always `true`,
108
109
  - every `ai-*-engineering-toolkit@multi-agent-plugins` key is **deleted** (retired names).
109
110
 
110
111
  Use the Read + Edit/Write tools (JSON must stay valid). Then print the resulting `enabledPlugins` block and run step 5 (Copilot/Codex refresh offer).
@@ -85,7 +85,7 @@ English, per `$HOME/.claude/multi-agent-refs/picker-contract.md`. Print the
85
85
  2. **Branch** - the branch to validate. Resolution order:
86
86
  - `git fetch --prune` first, capturing **stderr**. **If the fetch fails, do not
87
87
  silently fall back to a cached ref**, and **classify before naming a cause** -
88
- the same rule as the `/multi-agent:dev` remote gate:
88
+ the same rule as the pipeline's own remote gate:
89
89
 
90
90
  | stderr contains | Cause | Remedy |
91
91
  |---|---|---|
@@ -82,11 +82,11 @@ If nothing is stale → report "All targets up to date" and stop.
82
82
  - `copilot-instructions.md`: general development instructions + pipeline summary section
83
83
  - `multi-agent-pipeline/pipeline/`: generic open-source version (NO personal data)
84
84
  3. **Sync the shared sections** (Claude ↔ Copilot):
85
- - Pipeline modes table (normal / autopilot / --dev / --dev autopilot)
85
+ - Pipeline entries table (base / :local / :autopilot / :local-autopilot) + the Full-or-Short depth question
86
86
  - Project detection (URL-based + cwd-based)
87
87
  - Figma pipeline flow
88
88
  - Git conventions (author, branch, commit format)
89
- - --dev mode model info (Opus)
89
+ - Short-run model info (Opus)
90
90
  - Keychain management (registry, naming convention, multi-agent pattern)
91
91
  4. **Do not duplicate** - summary in Copilot, full spec in Claude. Keep this distinction.
92
92
  5. **Report** - show a short diff summary after sync
@@ -165,7 +165,7 @@ A git clone of the pipeline repo is a maintainer workspace, kept in sync by `/mu
165
165
  ```
166
166
  Current: v15.6.0 Latest: v15.6.1
167
167
  -> npm pack @{npm-scope}/multi-agent-pipeline@15.6.1
168
- -> node install.js --all (51 commands, 228 scripts, 205 skills)
168
+ -> node install.js --all (51 commands, 245 scripts, 208 skills)
169
169
  -> migrate-prefs.mjs (0 changes - already v2.6.0)
170
170
 
171
171
  ✓ Updated: v15.6.0 → v15.6.1
@@ -19,6 +19,9 @@
19
19
  # its entries carry "url": null. Advisory diagnostic context;
20
20
  # pattern-based + tolerant, a project may refine the labels.
21
21
  # - figma : Figma design / make / FigJam / slides URLs
22
+ # - document : a local document path or a document URL - .docx, .pdf, .md,
23
+ # .txt. Distinct from generic-doc, which is an HTML page: this
24
+ # one is a file that must be converted before it can be read.
22
25
  # - generic-doc : http(s) URLs that didn't match a more specific type but
23
26
  # look like documentation (Notion, Google Docs, GitHub
24
27
  # markdown blob/wiki, gist) - passed through as advisory
@@ -33,6 +36,7 @@
33
36
  # { "type": "swagger", "url": "...", "metadata": { "format": "json|yaml|unknown" } },
34
37
  # { "type": "crashlytics","url": "...", "metadata": { "projectId": "...", "platform": "ios|android", "issueId": "...", "sessionId": "...|null" } },
35
38
  # { "type": "fortify", "url": "...|null", "metadata": { "host": "...|null", "versionId": "...|null", "instanceId": "...|null", "source": "url|label" } },
39
+ # { "type": "document", "url": "...|null", "metadata": { "path": "...", "format": "docx|pdf|md|txt", "source": "path|url" } },
36
40
  # { "type": "graylog", "url": null, "metadata": { "idType": "trx|conversation", "id": "...", "label": "..." } },
37
41
  # { "type": "figma", "url": "...", "metadata": { "kind": "design|make|board|slides", "fileKey": "...", "nodeId": "...|null" } },
38
42
  # ...
@@ -169,6 +173,13 @@ def classify(url: str):
169
173
  "source": "url",
170
174
  })
171
175
 
176
+ # --- Document files (a URL that points at a file, not a page) -----------
177
+ m_doc = re.search(r"\.(docx|pdf|md|markdown|txt)$", path, re.IGNORECASE)
178
+ if m_doc:
179
+ fmt = m_doc.group(1).lower()
180
+ return ("document", {"path": url, "format": "md" if fmt == "markdown" else fmt,
181
+ "source": "url"})
182
+
172
183
  # --- Generic doc fallbacks ---------------------------------------------
173
184
  # Notion, Google Docs, GitHub markdown blob/wiki, gists. Useful as
174
185
  # advisory context - extractor flags them so the orchestrator can decide
@@ -276,5 +287,32 @@ for m in FORTIFY_ID_RE.finditer(text):
276
287
  },
277
288
  })
278
289
 
290
+ # --- Local document paths (not URLs, so the URL pass above never sees them) -
291
+ # A spec arrives as often as a path as it does as a link: "/Users/x/Spec.docx",
292
+ # "~/notes/api.md". Quotes and surrounding prose are stripped; the extension is
293
+ # what makes it a document, so a bare directory or a code path cannot match.
294
+ DOCUMENT_PATH_RE = re.compile(
295
+ r"(?<![\w/])(~?/[^\s\"'<>|]+\.(?:docx|pdf|md|markdown|txt))",
296
+ re.IGNORECASE,
297
+ )
298
+ # A URL contains a slash-path too, so skip any match that falls inside one -
299
+ # otherwise "https://x/api.pdf" also yields the bogus local path "//x/api.pdf".
300
+ url_spans = [m.span() for m in URL_RE.finditer(text)]
301
+ for m in DOCUMENT_PATH_RE.finditer(text):
302
+ if any(a <= m.start() < b for a, b in url_spans):
303
+ continue
304
+ raw = m.group(1).rstrip(".,;:!?)")
305
+ key = ("document", raw)
306
+ if key in seen:
307
+ continue
308
+ seen.add(key)
309
+ fmt = raw.rsplit(".", 1)[1].lower()
310
+ out.append({
311
+ "type": "document",
312
+ "url": None,
313
+ "metadata": {"path": raw, "format": "md" if fmt == "markdown" else fmt,
314
+ "source": "path"},
315
+ })
316
+
279
317
  print(json.dumps(out))
280
318
  PY
@@ -0,0 +1,190 @@
1
+ #!/bin/bash
2
+ #
3
+ # fetch-document.sh
4
+ # Extract readable text from a local document or a document URL so an analysis
5
+ # run can cite a Word file, a PDF or a plain-text spec the way it cites a
6
+ # Confluence page. Before this existed nothing in the pipeline could read a
7
+ # .docx, which is the format most feature specs actually arrive in.
8
+ #
9
+ # Input forms:
10
+ # ./fetch-document.sh /path/to/spec.docx
11
+ # ./fetch-document.sh ~/notes/spec.md
12
+ # ./fetch-document.sh https://example.com/spec.docx
13
+ #
14
+ # Optional env:
15
+ # DOCUMENT_TIMEOUT_SECONDS download bound, default 25
16
+ # DOCUMENT_MAX_CHARS truncate the extracted body, default 200000
17
+ # DOCUMENT_PDFTOTEXT pdf converter binary, default `pdftotext`. Some
18
+ # distributions ship it under another name or path,
19
+ # and the offline smoke points it at a binary that
20
+ # does not exist to exercise the degrade path.
21
+ #
22
+ # Output (stdout, single JSON object):
23
+ # {
24
+ # "fetchedAt": "<ISO8601>",
25
+ # "source": { "url": "<url|null>", "path": "<local path>",
26
+ # "format": "docx|pdf|md|txt", "bytes": <n> },
27
+ # "title": "<first heading or filename>",
28
+ # "text": "<extracted plain text>",
29
+ # "truncated": <bool>,
30
+ # "degraded": <bool>, "degradeReason": "<reason|null>"
31
+ # }
32
+ #
33
+ # Exit codes:
34
+ # 0 success (may be degraded-empty for a pdf with no converter)
35
+ # 3 download or read failure
36
+ # 4 bad usage / unsupported extension
37
+ # 6 converter not available (pdf only; the orchestrator treats this as
38
+ # "referenced but not fetched" and continues)
39
+ #
40
+ # .docx needs no external tool: the file is a zip and `word/document.xml` is
41
+ # read with the python3 standard library, which the lib layer already depends
42
+ # on. Requiring pandoc or libreoffice would be the wrong dependency for a tool
43
+ # that must work on macOS, Linux and Windows alike. PDF has no stdlib path, so
44
+ # `pdftotext` is used when present and its absence is a soft skip rather than
45
+ # an error - on Windows that absence is the normal case.
46
+
47
+ set -uo pipefail
48
+
49
+ TIMEOUT="${DOCUMENT_TIMEOUT_SECONDS:-25}"
50
+ MAX_CHARS="${DOCUMENT_MAX_CHARS:-200000}"
51
+ SRC="${1:-}"
52
+
53
+ if [ -z "$SRC" ] || [ "$SRC" = "-h" ] || [ "$SRC" = "--help" ]; then
54
+ echo "usage: $0 <path-or-url> # .docx | .pdf | .md | .txt" >&2
55
+ exit 4
56
+ fi
57
+
58
+ URL=""
59
+ LOCAL="$SRC"
60
+ TMPDL=""
61
+ cleanup() { [ -n "$TMPDL" ] && rm -f "$TMPDL"; }
62
+ trap cleanup EXIT
63
+
64
+ case "$SRC" in
65
+ http://*|https://*)
66
+ URL="$SRC"
67
+ TMPDL=$(mktemp "${TMPDIR:-/tmp}/fetch-document.XXXXXX")
68
+ if ! curl -sS -L --fail --max-time "$TIMEOUT" --connect-timeout 5 -o "$TMPDL" "$URL" 2>/dev/null; then
69
+ printf '%s\n' '{"status":"failed","reason":"download-failed","service":"document"}' >&2
70
+ exit 3
71
+ fi
72
+ # Keep the URL's extension so the format probe below still works.
73
+ LOCAL="$TMPDL"
74
+ EXT=$(printf '%s' "${URL%%\?*}" | tr 'A-Z' 'a-z' | sed -n 's/.*\.\([a-z0-9]\{1,5\}\)$/\1/p')
75
+ ;;
76
+ *)
77
+ case "$LOCAL" in "~"/*) LOCAL="$HOME/${LOCAL#\~/}" ;; esac
78
+ if [ ! -f "$LOCAL" ]; then
79
+ printf '%s\n' '{"status":"failed","reason":"file-not-found","service":"document"}' >&2
80
+ exit 3
81
+ fi
82
+ EXT=$(printf '%s' "$LOCAL" | tr 'A-Z' 'a-z' | sed -n 's/.*\.\([a-z0-9]\{1,5\}\)$/\1/p')
83
+ ;;
84
+ esac
85
+
86
+ case "$EXT" in
87
+ docx|pdf|md|markdown|txt) ;;
88
+ *)
89
+ printf '%s\n' '{"status":"failed","reason":"unsupported-format","service":"document"}' >&2
90
+ exit 4 ;;
91
+ esac
92
+
93
+ DEGRADED=false
94
+ DEGRADE_REASON=""
95
+ PDFTOTEXT="${DOCUMENT_PDFTOTEXT:-pdftotext}"
96
+ if [ "$EXT" = "pdf" ] && ! command -v "$PDFTOTEXT" >/dev/null 2>&1; then
97
+ DEGRADED=true
98
+ DEGRADE_REASON="converter-not-available"
99
+ fi
100
+
101
+ SRC_URL="$URL" SRC_PATH="$SRC" LOCAL_IN="$LOCAL" EXT_IN="$EXT" \
102
+ MAX_IN="$MAX_CHARS" DEGRADED_IN="$DEGRADED" REASON_IN="$DEGRADE_REASON" \
103
+ PDFTOTEXT_IN="$PDFTOTEXT" \
104
+ python3 - <<'PY'
105
+ import datetime, json, os, re, subprocess, zipfile
106
+ import xml.etree.ElementTree as ET
107
+
108
+ path = os.environ["LOCAL_IN"]
109
+ ext = os.environ["EXT_IN"]
110
+ degraded = os.environ["DEGRADED_IN"] == "true"
111
+ reason = os.environ["REASON_IN"] or None
112
+ try:
113
+ limit = int(os.environ["MAX_IN"])
114
+ except ValueError:
115
+ limit = 200000
116
+
117
+ W = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}"
118
+
119
+ def from_docx(p):
120
+ # A .docx is a zip; the body is word/document.xml. <w:t> holds the runs,
121
+ # <w:p> is the paragraph boundary. Standard library only, so the same code
122
+ # path runs on every platform.
123
+ with zipfile.ZipFile(p) as z:
124
+ xml = z.read("word/document.xml")
125
+ root = ET.fromstring(xml)
126
+ out = []
127
+ for para in root.iter(W + "p"):
128
+ runs = [t.text or "" for t in para.iter(W + "t")]
129
+ line = "".join(runs).strip()
130
+ if line:
131
+ out.append(line)
132
+ return "\n\n".join(out)
133
+
134
+ def from_pdf(p):
135
+ if degraded:
136
+ return ""
137
+ r = subprocess.run([os.environ.get("PDFTOTEXT_IN", "pdftotext"), "-layout", p, "-"],
138
+ capture_output=True, text=True)
139
+ return r.stdout if r.returncode == 0 else ""
140
+
141
+ text = ""
142
+ err = None
143
+ try:
144
+ if ext == "docx":
145
+ text = from_docx(path)
146
+ elif ext == "pdf":
147
+ text = from_pdf(path)
148
+ else:
149
+ with open(path, "r", encoding="utf-8", errors="replace") as fh:
150
+ text = fh.read()
151
+ except Exception as exc: # unreadable archive, bad xml, ...
152
+ err = type(exc).__name__
153
+ text = ""
154
+
155
+ if err and not degraded:
156
+ degraded, reason = True, f"extract-failed:{err}"
157
+
158
+ truncated = len(text) > limit
159
+ if truncated:
160
+ text = text[:limit]
161
+
162
+ title = ""
163
+ for line in text.split("\n"):
164
+ s = line.strip().lstrip("#").strip()
165
+ if s:
166
+ title = s[:120]
167
+ break
168
+ if not title:
169
+ title = os.path.basename(os.environ["SRC_PATH"])
170
+
171
+ print(json.dumps({
172
+ "fetchedAt": datetime.datetime.now(datetime.timezone.utc)
173
+ .isoformat().replace("+00:00", "Z"),
174
+ "source": {
175
+ "url": os.environ["SRC_URL"] or None,
176
+ "path": os.environ["SRC_PATH"],
177
+ "format": "md" if ext == "markdown" else ext,
178
+ "bytes": os.path.getsize(path) if os.path.exists(path) else 0,
179
+ },
180
+ "title": title,
181
+ "text": text,
182
+ "truncated": truncated,
183
+ "degraded": degraded,
184
+ "degradeReason": reason,
185
+ }, ensure_ascii=False))
186
+ PY
187
+
188
+ if [ "$DEGRADED" = "true" ]; then
189
+ exit 6
190
+ fi
@@ -48,6 +48,10 @@ Selects extra repos the pipeline may touch beyond the primary repo(s) - typica
48
48
 
49
49
  4. **Empty submit** - user picks nothing → `extras=[]`, primary repo(s) only.
50
50
 
51
+ ## Frontend repos - `frontendRepos`
52
+
53
+ Frontend repos are rarely submodules of the app repo, so submodule detection never finds them. `prefs.projects[<key>].frontendRepos[]` is the primary source for them and is merged into the candidate list alongside the detected submodules. Only `/multi-agent:analysis` used to read this key, which meant the pipeline's own dev-context picker could not offer a frontend repo at all.
54
+
51
55
  ## Pref override - `editableRelatedRepos`
52
56
 
53
57
  For repos that aren't on a remote provider (local-only checkout) or to explicitly grant edit rights regardless of provider state, add to `~/.claude/multi-agent-preferences.json`:
@@ -0,0 +1,213 @@
1
+ # Evidence gathering (Phases 1, 1b, 1b.1, 1b.2, 1c, 1d)
2
+
3
+ > Evidence gathering for the analysis engine. Loaded on demand by `/multi-agent:analysis` and by pipeline Phase 1. Produces `state.analysisSpec.evidence.*`; nothing here writes a document.
4
+
5
+ ### Phase 1 - Fetch & extract (parallel)
6
+
7
+ For each entry in `state.analysisSpec.contextLinks[]`, fan out by `type`. Entries tagged `binding: true` (Q5 inputs) land in `evidence.standards[]` regardless of underlying source type so Section 7 can iterate them as one set.
8
+
9
+ | type | Command | Output bucket |
10
+ |------|---------|---------------|
11
+ | swagger | `~/.claude/lib/fetch-swagger.sh <url>` | `state.analysisSpec.evidence.swagger[]` |
12
+ | confluence | `~/.claude/lib/fetch-confluence.sh <url>` | `state.analysisSpec.evidence.confluence[]` |
13
+ | figma | **MUST** establish the Figma access tier per Locked decision #12. Tier 1 (preferred): call `mcp__claude_ai_Figma__get_design_context` (primary), `mcp__claude_ai_Figma__get_screenshot`, `mcp__claude_ai_Figma__get_metadata` for every node ID; auth failure runs `authenticate` + `complete_authentication` then retries. Tier 2 (fallback): `GET https://api.figma.com/v1/files/{fileKey}/nodes?ids={nodeId}` for design context and `GET /v1/images/{fileKey}?ids={nodeId}&format=png&scale=2` for screenshots, with PAT from `~/.claude/lib/credential-store.sh get <logical-key>` (logical key = `prefs.global.keychainMapping.figma`); canonical component resolves via repo `*.figma.swift` / `*.figma.kt` mapping. Tier 3 (last resort): user-attached screenshot, `codeConnectSnippets: []`, forced Open Question. Capture each `CodeConnectSnippet` block verbatim when on Tier 1 (component name + modifier chain). **Annotation capture (when the project `figma-config` has `annotations.enabled`):** on Tier 1, walk the `annotations[]` the `get_design_context` payload returns for each node (read `label` / `labelMarkdown`); on Tier 2, run `~/.claude/lib/fetch-figma-annotations.sh --file-key <fileKey> --node-id <ids> --lang-prefixes <config.annotations.langPrefixes>`. Parse each annotation by the configured language prefixes (prefixed `TR:/EN:` wins, else line1/line2, else single) and persist to `annotations[]`. The annotation is the authoritative copy for its node (Locked 3); the visible text layer is a placeholder. Persist `state.figmaAccess.tier`. See Locked decision #12 and `$HOME/.claude/rules/figma-pipeline.md` "MUST: Figma access - 3-tier fallback chain". | `state.analysisSpec.evidence.figma[]` (records: `nodeId`, `screenshotUrl`, `codeConnectSnippets[]`, `tokens[]`, `textLayers[]`, `annotations[]`, `tier`; see `analysis-spec.schema.json`) |
14
+ | jira | `gh api` or Jira REST API for issue summary | `state.analysisSpec.evidence.jira[]` |
15
+ | document | `~/.claude/lib/fetch-document.sh <path-or-url>` (`.docx` via python3 stdlib, `.md` / `.txt` read directly, `.pdf` only when `pdftotext` exists). Exit `6` is a soft skip, not a failure: record the file with `fetched: false` so Section 21 still cites it as referenced-but-not-fetched. | `state.analysisSpec.evidence.documents[]`, or `evidence.standards[]` when `binding: true` |
16
+ | generic-doc | WebFetch on demand | `state.analysisSpec.evidence.confluence[]` (generic bucket) unless `binding: true` -> `evidence.standards[]` |
17
+ | `local-file` | `Read` (no fetch) on tilde-expanded absolute path | `state.analysisSpec.evidence.standards[]` |
18
+ | `wiki` | `git clone --depth 1 https://github.com/<owner>/<repo>.wiki.git /tmp/<repo>-wiki`, then `Read /tmp/<repo>-wiki/<PageName>.md` (`-` decoded back to space in PageName). Fallback chain: `gh api repos/<owner>/<repo>/contents/<file>.md` for repo-hosted docs if wiki repo is 404. | `state.analysisSpec.evidence.standards[]` |
19
+ | `standards-confluence` | `~/.claude/lib/fetch-confluence.sh <url>` (same fetcher as `confluence`) | `state.analysisSpec.evidence.standards[]` |
20
+ | `firebase-events:names` | Scaffold rows directly from `metadata.names[]`; one row per name with empty params (filled later by repo grep cross-check) | `state.analysisSpec.evidence.firebase[]` |
21
+ | `firebase-events:schema` | `Read` the JSON path; parse `events[]` with `python3 -c json.load`; each entry yields `{name, params: [{name, type, required}]}` | `state.analysisSpec.evidence.firebase[]` |
22
+ | `firebase-events:console` | Reference-only; print INFO warning `INFO: Firebase Console URL captured for reference; provide JSON schema or event-name list for structured ingestion.`; no fetch attempt | `state.analysisSpec.evidence.firebase[]` (URL stored as reference link only) |
23
+
24
+ **Auth / access failure handling** (applies to `confluence`, `standards-confluence`, `generic-doc` when WebFetch returns a login page, 401, 403, or HTML containing `<form action="/login.action"`):
25
+
26
+ 1. Do **not** silently drop the entry. Record `state.analysisSpec.evidence.fetchErrors[]` with `{url, type, reason: "auth_required"}`.
27
+ 2. Surface a one-line warning to the user during Phase 1 progress output: `WARN: Confluence requires browser auth for <url>. Falling back to repo evidence; add the page text manually to /tmp/analysis-paste.md and re-run if needed.`
28
+ 3. Continue Phase 1 with remaining evidence sources rather than aborting.
29
+
30
+ For each repo in `state.analysisSpec.repos[]`, run parallel greps (lightweight surface scan; the deeper 13-bucket extraction happens in Phase 1b):
31
+
32
+ ```bash
33
+ # Localization
34
+ grep -rEl '(Localizable\.strings|strings\.xml|i18n|locale)' "$REPO_PATH"
35
+
36
+ # Deeplink handlers
37
+ grep -rEl '(UniversalLink|DeeplinkRouter|getDeeplink|onNewIntent|intent-filter)' "$REPO_PATH"
38
+
39
+ # Push handlers
40
+ grep -rEl '(UNUserNotificationCenter|didReceiveRemoteNotification|FirebaseMessaging|FCM)' "$REPO_PATH"
41
+
42
+ # Firebase Analytics call sites (Phase 1 auto-detect + Phase 1b cross-check)
43
+ grep -rEln '(Analytics\.logEvent|firebaseAnalytics\.logEvent|logEvent\(analytics,)' "$REPO_PATH"
44
+
45
+ # Code Connect
46
+ find "$REPO_PATH" -name '*.figma.swift' -o -name '*.figma.kt'
47
+ ```
48
+
49
+ Results go to `state.analysisSpec.evidence.repo[]` (presence flags only).
50
+
51
+ **Confluence-embedded API table detection**: if a Confluence page body contains `Request Path` / `Service Name` / `Response Body` table columns, set `embeddedApiTable=true` and parse the endpoints from those columns (a project may supply its own parser for this). Do not also trigger a separate Swagger fetch.
52
+
53
+ ### Phase 1b - Repo-evidence collector (parallel, per repo)
54
+
55
+ After Phase 1 finishes, run a deeper scan per repo to enable Section 7's reuse-first rule (Locked decision 11). Output: `state.analysisSpec.evidence.repoEvidence[<repo>]`.
56
+
57
+ **Whitelist roots per platform** (subset of repo paths scanned to keep the catalogue bounded):
58
+
59
+ | Platform | Roots |
60
+ |---|---|
61
+ | iOS | `Domains/`, `Common/`, `Core/`, `App/` |
62
+ | Android | `app/src/`, `feature/`, `core/`, `common/` |
63
+ | Backend | `src/`, `api/`, `services/` |
64
+ | Frontend | `src/`, `app/`, `components/`, `features/`, `lib/` (override with `prefs.projects[<key>].frontendRoots[]`) |
65
+
66
+ Skip dirs (always): `.build`, `DerivedData`, `Pods`, `node_modules`, `.next`, `build/`, `.gradle`, `vendor/`.
67
+
68
+ **Candidate set**: build a feature-name slug bundle:
69
+
70
+ ```bash
71
+ slugs=$(echo "$featureName" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g')
72
+ # e.g. UserProfile -> "user profile userprofile"
73
+ grep -rilE "$slugs" $WHITELIST --include="*.swift" --include="*.kt" --include="*.ts" --include="*.tsx" --include="*.py" --include="*.go" | head -200 > $CANDIDATES
74
+ ```
75
+
76
+ If `wc -l < $CANDIDATES` exceeds 200, record `fetchWarnings += "feature-name too generic; candidate cap hit"` in `repoEvidence[<repo>]`.
77
+
78
+ **13 buckets** (each scanned in parallel with a 30 s timeout per bucket; timeout marks the bucket as `partial`, never blocks the others):
79
+
80
+ | Bucket | Detection (iOS Swift sketch) | Cross-platform variant |
81
+ |---|---|---|
82
+ | services | `class .*Service\b`, `protocol .*Service\b`, generated `InfoEndpoint<...>` | `class *Service` / `interface *Service` / `def *_service` |
83
+ | dtos | `struct .*(Request|Response|DTO)\b` | `data class *Dto` / `interface *DTO` / Pydantic `BaseModel` |
84
+ | useCases | `protocol .*UseCase\b`, `class .*UseCase\b` | `class *UseCase` / `def *_use_case` |
85
+ | validationRules | `*Rule.swift`, `*Validator.swift`, Validation/ dir | `*Validator.kt` / `*.ts` zod schemas / pydantic validators |
86
+ | domainEntities | `Domain/Entities/*.swift` | `domain/entities/*.kt` / `entities/*.py` |
87
+ | routes | Public `enum *Route` matching `DomainRoute` | `*Route` sealed / NavRoute / `routes.*` modules |
88
+ | coordinators | `*Coordinator.swift` + `CoordinatorProtocol`/`DomainRouter` | Navigator / NavController helpers |
89
+ | diConfigurators | `*DependencyConfigurator.swift`, `Module.kt` (Hilt) | DI container registrations |
90
+ | uiComponents | `Common/UIComponents/.../Components/**/*.swift` triplet (Configuration + View + +Modifiers) | Compose @Composable functions / React components |
91
+ | tokens | `*Token` enums under `UIAssetTokens/Generated/` | `Theme.kt` / `tokens.ts` / `tailwind.config.*` |
92
+ | localizationKeys | `LocalizationStringKeys.swift` public enum + cases | `strings.xml` keys / `i18n/*.json` keys |
93
+ | testingIdentifiers | `ui-testing-identifiers.json` | `testTag` strings / `data-testid` constants |
94
+ | analyticsEvents | Generated `AnalyticsEvents/*.swift` + `Analytics.logEvent\(` call sites | Equivalent Android / Frontend / Backend telemetry |
95
+
96
+ **Tagging rule** for each row:
97
+
98
+ | Tag | Match condition |
99
+ |---|---|
100
+ | `direct-match` | The item name contains a feature-name slug fragment |
101
+ | `same-domain` | Item lives under `Domains/<feature>/` or `feature/<feature>/` |
102
+ | `cross-cutting` | Item lives under `Common/` / `core/` / shared roots (not under any single feature) |
103
+
104
+ **Output JSON shape** (per repo):
105
+
106
+ ```json
107
+ {
108
+ "buckets": {
109
+ "services": [
110
+ {"name": "UserProfileService", "file": "Domains/UserProfile/Services/UserProfileService.swift", "line": 12, "tag": "direct-match"},
111
+ {"name": "LoggingService", "file": "Common/Logging/LoggingService.swift", "line": 8, "tag": "cross-cutting"}
112
+ ]
113
+ },
114
+ "fetchWarnings": []
115
+ }
116
+ ```
117
+
118
+ ### Phase 1b.1 - Code Connect index (after Phase 1b)
119
+
120
+ figma-to-swiftui (and the Android equivalent) already produced the feature's components and bound them to Figma via Code Connect. The analysis treats those bindings as the ground truth for "what already exists" - it does not re-derive components from the design. Build an index from the `*.figma.swift` / `*.figma.kt` files found in Phase 1:
121
+
122
+ ```bash
123
+ # Code Connect figma(...) calls carry the Figma URL (fileKey + node-id)
124
+ grep -rEn 'figma\("https://www\.figma\.com/[^"]+"' "$REPO_PATH" \
125
+ --include='*.figma.swift' --include='*.figma.kt'
126
+ ```
127
+
128
+ For each binding, parse `{ fileKey, nodeId, component, path }` where `component` is the registered component type and `path` is the binding file. Decode the URL `node-id` form (`-` to `:`). Output: `state.analysisSpec.evidence.codeConnect[]`.
129
+
130
+ Phase 2 fills the design->code link deterministically from this index:
131
+ - Section 5.1 `Code Connect` column: the matched component name for that frame's `nodeId` (blank when the frame has no existing binding).
132
+ - Section 6 `Mevcut / Existing`: `yes` + `reuse <component> at <path>` when the design `nodeId` (or its `fileKey`) matches an index entry; `no` + `new component` (added to Section 14) when it does not.
133
+
134
+ When no Code Connect file exists in any selected repo, the index is empty and Section 6 falls back to the Phase 1b `uiComponents` heuristic (the prior behavior).
135
+
136
+ ### Phase 1b.2 - Main component variant matrix (after the Code Connect index)
137
+
138
+ The index answers "does this component exist and is it bound". It does not answer "what can it do". For every bound component, walk from the instance to its definition and read the whole axis:
139
+
140
+ 1. Resolve the instance's `componentId` to its main component / component set.
141
+ 2. Read the full variant axis. Tier 1: `mcp__claude_ai_Figma__get_metadata` on the component set. Tier 2: `GET /v1/files/{fileKey}/nodes?ids=<componentSetId>` and read `componentPropertyDefinitions` (each axis plus every one of its values); `GET /v1/files/{fileKey}/components` when the set id is not known.
142
+ 3. Diff the full axis against the values this screen actually uses.
143
+
144
+ Output: `state.analysisSpec.evidence.variantMatrix[<component>] = { axis, allValues[], usedValues[] }`, which fills Section 6.X.
145
+
146
+ Why it lives here and nowhere later: Locked 30 forbids Figma access from Phase 2 onward, so an axis not captured now cannot be recovered - Section 13.6 Preview and Section 15.2 Snapshot would then be validated against a subset nobody could check.
147
+
148
+ A component with no variant axis (a plain component, not a set) records `axis: null` and is not a finding.
149
+
150
+ ### Phase 1c - Convention extraction (parallel, per repo)
151
+
152
+ Per Locked decision 23. After Phase 1b finishes, run `~/.claude/lib/extract-conventions.sh <repo-path> <platform>` for each selected repo. The script returns a JSON document with seven pattern groups:
153
+
154
+ | Group | Keys |
155
+ |---|---|
156
+ | C1 folder structure | `folderStructure` |
157
+ | C2 class naming | `stateHolderNaming`, `viewNaming`, `navigatorNaming`, `useCaseNaming`, `repositoryNaming`, `dtoNaming` |
158
+ | C3 UI state model | `uiStateModel` |
159
+ | C4 test method naming | `testMethodNaming` |
160
+ | C5 accessibility identifier | `accessibilityIdentifier` |
161
+ | C6 localization key | `localizationKey` |
162
+ | C7 DI registration | `diRegistration` |
163
+
164
+ Each field has shape `{ pattern, example, confidence, evidenceFiles, alternativeCandidates }` where `confidence` is one of `high` (5+ examples, dominant), `medium` (3-4 examples, majority), `low` (2 examples or mixed), `none` (no evidence).
165
+
166
+ **Output**: `state.analysisSpec.evidence.conventions[<repo>]`.
167
+
168
+ **Risk auto-population (Locked 23)**: when any field returns `confidence: "low"` or `"none"`, push a row onto `state.analysisSpec.evidence.conventionRisks[]`:
169
+
170
+ ```json
171
+ {
172
+ "repo": "<repo>",
173
+ "field": "<pattern key>",
174
+ "confidence": "<low | none>",
175
+ "fallback": "conventions-defaults.md:C<n>-<platform>",
176
+ "question": "<convention-key> evidence is insufficient. Apply default <pattern> or override?"
177
+ }
178
+ ```
179
+
180
+ Phase 2 Section 20 Risks reads this list and emits one open question per entry.
181
+
182
+ **Fallback source**: when `confidence == "none"` AND `evidence.standards[]` does not contain an explicit rule for that field, the renderer reads `$HOME/.claude/multi-agent-refs/conventions-defaults.md` and applies the platform default.
183
+
184
+ **Caching (Locked 27)**: compute `evidence_digest = sha256(featureName || sorted(platforms) || hash(evidence.repoEvidence) || hash(evidence.conventions))`. Cache key on disk: `/tmp/multi-agent-analysis-cache/<digest>.json` with mtime <= 24h. Cache hit skips Phase 1b and Phase 1c. `--no-cache` flag forces re-run.
185
+
186
+ Phase 1d is deliberately absent from the digest inputs. Community signal changes by the hour, so folding it in would produce a new digest on every run, invalidate the cache every time, and re-run the two expensive repo phases the cache exists to skip. The consequence is worth stating plainly: a cache hit reuses yesterday's signal rows. That is the correct trade for an advisory tier, and `--no-cache` is the way to refresh them.
187
+
188
+ ### Phase 1d - Outside facts (optional, analyst toolkit)
189
+
190
+ Runs after Phase 1c, only when `ai-analyst-toolkit` is enabled. Not enabled is a recorded no-op, never a halt: this phase adds context the repo cannot supply, and a run without it is smaller, not wrong.
191
+
192
+ Two tiers, and the difference decides where a fact is allowed to appear.
193
+
194
+ | Tier | Sources | Bucket | May appear in |
195
+ |---|---|---|---|
196
+ | Evidence | `evidence-github`, `evidence-registry` | `state.analysisSpec.evidence.outside[]` | any section, cited `GitHub:<owner>/<repo>#<n>` or `Release:<pkg>@<ver>` (Locked 3) |
197
+ | Signal | `signal-community` | `state.analysisSpec.evidence.signals[]` | Section 20 Risks only, as a "reported in the wild" row with its link and date |
198
+
199
+ What to ask for: whether a dependency the feature touches has an open upstream bug, what a pinned version actually changed, whether prior art exists for the pattern being introduced, and whether anyone outside this team has reported the same symptom.
200
+
201
+ The tier boundary is not advisory. A signal row that reaches Section 4 (business rules), Section 9 (API contracts) or Section 13 (architecture) is a Locked 3 violation: those sections carry claims a reviewer can check, and a forum post is one person's experience, not a fact about the system being specified.
202
+
203
+ Per source: reachable and answered, reachable and empty, or unreachable. All three are recorded; only the third produces a `fetchErrors[]` entry, and none of them halts.
204
+
205
+ **Lite mode auto-detection (Locked 25, v9.1.0 scoring)**: at the end of Phase 1c, evaluate three signals and score each:
206
+
207
+ | Signal | True condition | Score |
208
+ |---|---|---|
209
+ | Confluence spec body lines | < 100 | 1 |
210
+ | Figma frames count | <= 1 | 1 |
211
+ | Repo evidence direct-match count | >= 8 | 1 |
212
+
213
+ `liteModeAuto = (totalScore >= 2)`. Two of three signals true is enough; the v8.12.0..v9.0.x AND-threshold (all three) forced too many small features into Full mode when one signal was marginal (e.g. a tiny spec with 2 Figma frames). User overrides via `--full` or `--lite` always win over scoring.