@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.
- package/CHANGELOG.md +124 -0
- package/README.md +2 -2
- package/README.tr.md +1 -2
- package/docs/architecture.md +2 -2
- package/docs/ecosystem.md +9 -6
- package/docs/features.md +2 -3
- package/install/_plugin-skills.mjs +28 -2
- package/install/templates/copilot-instructions.md +8 -7
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/SKILL.md +5 -6
- package/pipeline/commands/multi-agent/analysis/SKILL.md +77 -588
- package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +5 -69
- package/pipeline/commands/multi-agent/channels/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/complaint-analysis/SKILL.md +5 -4
- package/pipeline/commands/multi-agent/dev/SKILL.md +8 -280
- package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +12 -124
- package/pipeline/commands/multi-agent/dev-local/SKILL.md +8 -111
- package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +11 -113
- package/pipeline/commands/multi-agent/help/SKILL.md +61 -56
- package/pipeline/commands/multi-agent/ios-coding-standard/SKILL.md +4 -2
- package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +2 -4
- package/pipeline/commands/multi-agent/refactor/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/resume-local/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/setup/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/stack/SKILL.md +10 -9
- package/pipeline/commands/multi-agent/store-ready/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/sync/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/update/SKILL.md +1 -1
- package/pipeline/lib/context-link-extractor.sh +38 -0
- package/pipeline/lib/fetch-document.sh +190 -0
- package/pipeline/multi-agent-refs/_dev-context.md +4 -0
- package/pipeline/multi-agent-refs/analysis/evidence.md +213 -0
- package/pipeline/multi-agent-refs/analysis/intake.md +167 -0
- package/pipeline/multi-agent-refs/analysis/locked.md +53 -0
- package/pipeline/multi-agent-refs/analysis/render.md +133 -0
- package/pipeline/multi-agent-refs/analysis/resolve.md +76 -0
- package/pipeline/multi-agent-refs/analysis/synthesis.md +98 -0
- package/pipeline/multi-agent-refs/analysis-template.md +58 -11
- package/pipeline/multi-agent-refs/complaint-analysis-template.md +1 -1
- package/pipeline/multi-agent-refs/component-dispatch.md +5 -5
- package/pipeline/multi-agent-refs/cross-cli-contract.md +9 -7
- package/pipeline/multi-agent-refs/features/skill-conformance.md +1 -1
- package/pipeline/multi-agent-refs/features/url-enrichment.md +13 -3
- package/pipeline/multi-agent-refs/knowledge.md +2 -2
- package/pipeline/multi-agent-refs/payload-contracts.md +1 -1
- package/pipeline/multi-agent-refs/phases/modes.md +73 -53
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +21 -2
- package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +26 -0
- package/pipeline/multi-agent-refs/phases/phase-2-planning.md +26 -12
- package/pipeline/multi-agent-refs/phases/phase-3-dev.md +17 -18
- package/pipeline/multi-agent-refs/phases/phase-4-review.md +29 -7
- package/pipeline/multi-agent-refs/phases/phase-5-test.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-6-commit.md +8 -0
- package/pipeline/multi-agent-refs/phases/phase-7-report.md +1 -1
- package/pipeline/multi-agent-refs/phases.md +9 -7
- package/pipeline/multi-agent-refs/progress-contract.md +1 -1
- package/pipeline/multi-agent-refs/readiness-review.md +2 -0
- package/pipeline/multi-agent-refs/rules.md +2 -2
- package/pipeline/multi-agent-refs/tracker-contract.md +32 -13
- package/pipeline/multi-agent-refs/wiki-capture.md +2 -2
- package/pipeline/schemas/agent-state.schema.json +3 -3
- package/pipeline/schemas/analysis-output.schema.json +17 -0
- package/pipeline/schemas/analysis-spec.schema.json +69 -1
- package/pipeline/schemas/complaint-analysis-spec.schema.json +1 -1
- package/pipeline/schemas/prefs.schema.json +47 -0
- package/pipeline/schemas/token-budget.json +2 -2
- package/pipeline/scripts/_stack-routing.mjs +17 -12
- package/pipeline/scripts/build-stack-plugins.mjs +16 -6
- package/pipeline/scripts/cost-table.json +1 -1
- package/pipeline/scripts/gen-mode-dispatch.mjs +20 -30
- package/pipeline/scripts/run-aggregator.mjs +1 -1
- package/pipeline/scripts/validate-analysis-doc.mjs +36 -3
- package/pipeline/skills/.skills-index.json +40 -7
- package/pipeline/skills/shared/README.md +12 -9
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +9 -5
- package/pipeline/skills/shared/core/multi-agent-dev/SKILL.md +7 -61
- package/pipeline/skills/shared/core/multi-agent-dev-autopilot/SKILL.md +11 -51
- package/pipeline/skills/shared/core/multi-agent-dev-local/SKILL.md +7 -33
- package/pipeline/skills/shared/core/multi-agent-dev-local-autopilot/SKILL.md +10 -38
- package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +28 -18
- package/pipeline/skills/shared/core/multi-agent-ios-coding-standard/SKILL.md +4 -2
- package/pipeline/skills/shared/core/multi-agent-local-autopilot/SKILL.md +2 -4
- package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-resume-local/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +10 -9
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +2 -2
- package/pipeline/skills/shared/external/evidence-github/SKILL.md +45 -0
- package/pipeline/skills/shared/external/evidence-registry/SKILL.md +33 -0
- package/pipeline/skills/shared/external/signal-community/SKILL.md +44 -0
- 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`.
|
|
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`
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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,
|
|
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.
|