| name | init |
| description | Scaffold a new mobile-spine workspace (Claude Code subagent setup for Android + iOS + Backend across separate repos). Runs a short interview, replaces placeholders with the user's org / app / branch / Figma-MCP / license values, and writes the customized files into a chosen target directory. Trigger when the user asks to "set up mobile-spine", "scaffold the mobile multi-agent setup", "initialize a spine for mobile", or invokes /mobile-spine:init. |
| allowed-tools | ["Read","Write","Bash","AskUserQuestion"] |
mobile-spine:init
Bootstrap a new mobile-spine workspace by interviewing the user, then writing the user-customizable files into the chosen install location and a .claude/mobile-spine.config.yaml capturing the interview answers.
The static templates this skill consumes live next to this SKILL.md under ./templates/. They are workspace-data only — CLAUDE.md, SETUP.md, README.md, LICENSE, .gitignore, .claude/settings.json, a thin .claude/commands/feat.md stub, _context/operations.md, plus .gitkeep files for empty directories.
Not scaffolded — the four subagents (api-agent / pm-agent / android-agent / ios-agent) and the full /feat command logic are plugin primitives (live in plugins/mobile-spine/agents/ and plugins/mobile-spine/commands/, served globally by the plugin). The workspace's .claude/commands/feat.md is a thin delegation stub. The agents read .claude/mobile-spine.config.yaml at invocation to pick up the workspace's org / app / baseBranch / figma / copyright values. This means /plugin marketplace update claude-code-mobile-spine propagates agent + /feat improvements to every workspace automatically — without modifying any workspace-owned file.
Step 1 — Locate the templates directory
Resolve in three tiers. The harness should set $CLAUDE_PLUGIN_ROOT for
plugin-installed runs, but in practice this is not guaranteed (e.g. for
directory-typed marketplaces or older Claude Code versions). The cache and
working-tree fallbacks make the skill robust to either case.
TEMPLATES_DIR=""
if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
cand="$CLAUDE_PLUGIN_ROOT/skills/init/templates"
if [ -f "$cand/CLAUDE.md" ] && [ -f "$cand/SETUP.md" ]; then
TEMPLATES_DIR="$cand"
fi
fi
if [ -z "$TEMPLATES_DIR" ]; then
for cand in "$HOME/.claude/plugins/cache/claude-code-mobile-spine/mobile-spine"/*/skills/init/templates; do
if [ -f "$cand/CLAUDE.md" ] && [ -f "$cand/SETUP.md" ]; then
TEMPLATES_DIR="$cand"
break
fi
done
fi
if [ -z "$TEMPLATES_DIR" ]; then
for cand in \
"$(pwd)/plugins/mobile-spine/skills/init/templates" \
"$(pwd)/skills/init/templates" \
"$(pwd)/templates"; do
if [ -f "$cand/CLAUDE.md" ] && [ -f "$cand/SETUP.md" ]; then
TEMPLATES_DIR="$cand"
break
fi
done
fi
echo "TEMPLATES_DIR=$TEMPLATES_DIR"
If TEMPLATES_DIR is empty, abort with a clear message:
"[mobile-spine:init] templates/ directory not found. Install the mobile-spine
plugin via /plugin install mobile-spine@claude-code-mobile-spine, or run this
skill from the plugin's source repo working tree."
Step 2 — Interview
Collect answers in this order. Use plain-text questions for free-form
answers (one question per turn). Use AskUserQuestion only for the choice
questions (base branch, Figma MCP).
Q1. GitHub org or username (free text)
What GitHub org / username should I use for the platform repos?
Example: acme (resulting issue paths will look like acme/<app>-android).
Q2. App prefix (free text)
What is the app prefix? It will be applied to repo names and paths.
Example: cool-app → cool-app-android, cool-app-ios, cool-app-backend.
Q3. Base branch (AskUserQuestion)
- Question: "Which base branch convention does your team use?"
- Header: "Base branch"
- Options:
develop — integration branch; releases cut from main/master (Recommended for app-store release flows)
main — single trunk
master — legacy single trunk
other — type the branch name
Q4. Figma MCP namespace (AskUserQuestion)
Q4 has four substeps. Do them in order; substep 4a is mandatory and gates the rest.
4a. Detect installed Figma MCP servers (REQUIRED — invoke the Bash tool with this exact block before constructing AskUserQuestion options; do NOT skip)
As of Claude Code v2.x, claude mcp list prints one line per configured server in the shape <name>: <url-or-cmd> - <status>. The name portion is always followed by : (colon-space), even when the name itself contains colons (e.g. plugin:supabase:supabase). Filter for lines containing "figma" (case-insensitive), then strip the : <url>... tail to recover the verbatim server name. The namespace is mcp__<server-name>__*.
DETECTED_FIGMA=$(claude mcp list 2>/dev/null \
| grep -i figma \
| sed -E 's/: .*$//')
DETECTED_FIGMA_COUNT=$(printf '%s' "$DETECTED_FIGMA" | grep -c .)
echo "DETECTED_FIGMA_COUNT=$DETECTED_FIGMA_COUNT"
[ "$DETECTED_FIGMA_COUNT" -gt 0 ] && printf '%s\n' "$DETECTED_FIGMA" | sed 's/^/DETECTED_FIGMA_NAME=/'
If a future Claude Code release changes this CLI format, detection degrades silently: $DETECTED_FIGMA_COUNT becomes 0 and substep 4b takes the fallback branch.
4b. Read the Bash output and pick a branch for 4c
Parse the output from 4a, then proceed to 4c and use the branch matching DETECTED_FIGMA_COUNT:
DETECTED_FIGMA_COUNT > 0 → dynamic branch
DETECTED_FIGMA_COUNT == 0 (no figma servers installed, or claude mcp list unavailable) → fallback branch
4c. Construct AskUserQuestion options for the chosen branch
Dynamic branch (when DETECTED_FIGMA_COUNT > 0):
- One option per detected server, up to 3 —
AskUserQuestion accepts at most 4 options total, and we reserve the 4th slot for none. If more than 3 figma servers are detected, keep the first 3 and append a single-sentence note in the question text: "Showing first 3 of N detected — others are still usable; edit .claude/mobile-spine.config.yaml after scaffolding."
- Each per-server option label:
mcp__<server-name>__* — detected. The — detected suffix is required so the user can tell at a glance that this option came from claude mcp list (a bare mcp__<server-name>__* label with no suffix only appears in the fallback branch — see below). Description: "detected via claude mcp list".
- Recommended preference: if
figma-desktop is among the detected names, append (Recommended) after the — detected suffix, yielding the full label mcp__figma-desktop__* — detected (Recommended). Otherwise leave none marked — first-detected order is just whatever claude mcp list emitted, not an editorial signal.
- Final option: label
none, description "skip Figma integration for now (UI sections will always be deferred)". The — detected suffix is not applied to none — only to the per-server options above.
The — detected suffix is a UI-only disambiguator. It must NOT be persisted into .claude/mobile-spine.config.yaml — see §4-3 below, which strips this suffix (and any trailing (Recommended) marker) before writing figmaMcpNamespace.
Fallback branch (when DETECTED_FIGMA_COUNT == 0):
mcp__figma__* — generic placeholder (replace after MCP setup via /mcp)
mcp__figma-desktop__* — official Figma Dev Mode MCP (selection-based; requires Figma paid plan + Desktop app) (Recommended)
none — skip Figma integration for now (UI sections will always be deferred)
4d. Invoke AskUserQuestion with the constructed options
Question parameters:
- Question: "Which Figma MCP server are you using? (this affects pm-agent's
tools whitelist)"
- Header: "Figma MCP"
After the user answers, check the pm-agent compatibility window. The plugin-shipped pm-agent.md has a fixed tools whitelist of mcp__figma__*, mcp__figma-desktop__* (see SETUP.md §3-4). If the selected namespace is neither of those (e.g. a detected custom server), MCP calls from pm-agent will be blocked at runtime. Print a one-line note to the user:
Note: the selected namespace is outside pm-agent's default tool whitelist. After scaffolding, see SETUP.md §3-4 to override pm-agent at workspace level (create .claude/agents/pm-agent.md with your namespace added to tools).
Skip the note when the selection is mcp__figma__*, mcp__figma-desktop__*, or none.
Q5. License copyright holder (free text)
Who is the copyright holder for the LICENSE file? (your name, org, or "n/a" to leave the placeholder.)
Q6. Install location (free text, default suggested)
The spine must be a sibling of the platform repos (<app>-android, <app>-ios, <app>-backend). Agents reach them via relative paths like ../<app>-android/ from the spine's cwd — if the spine is not in the same parent directory, every agent run will fail to find the repos (or need /add-dir each time).
Detect existing sibling repos to pick a smart default:
APP="<Q2>"
CWD_PARENT=$(dirname "$(pwd)")
[ "$CWD_PARENT" = "/" ] && CWD_PARENT="$(pwd)"
SIBLINGS_AS_CHILDREN=0
for p in android ios backend; do
[ -d "$(pwd)/$APP-$p" ] && SIBLINGS_AS_CHILDREN=$((SIBLINGS_AS_CHILDREN+1))
done
SIBLINGS_AS_NEIGHBORS=0
for p in android ios backend; do
[ -d "$CWD_PARENT/$APP-$p" ] && SIBLINGS_AS_NEIGHBORS=$((SIBLINGS_AS_NEIGHBORS+1))
done
if [ "$SIBLINGS_AS_CHILDREN" -gt 0 ]; then
PLATFORM_PARENT="$(pwd)"
SIBLINGS_FOUND=$SIBLINGS_AS_CHILDREN
elif [ "$SIBLINGS_AS_NEIGHBORS" -gt 0 ]; then
PLATFORM_PARENT="$CWD_PARENT"
SIBLINGS_FOUND=$SIBLINGS_AS_NEIGHBORS
else
SIBLINGS_FOUND=0
fi
if [ "$SIBLINGS_FOUND" -gt 0 ]; then
DEFAULT_TARGET="$PLATFORM_PARENT/$APP-spine"
echo "Detected $SIBLINGS_FOUND existing platform repo(s) under $PLATFORM_PARENT/ — defaulting spine to sit alongside them."
else
DEFAULT_TARGET="$(pwd)/$APP-spine"
echo "No sibling platform repos detected — defaulting spine under $(pwd)."
fi
echo "DEFAULT_TARGET=$DEFAULT_TARGET"
Then ask the user (plain text, not AskUserQuestion):
Where should the new spine be created?
Important: the spine must live in the same parent directory as your platform repos, so agents can reach them via ../<app>-android/ etc. Target layout:
<parent>/
├── <app>-spine/ ← this scaffold
├── <app>-android/
├── <app>-ios/
└── <app>-backend/
Suggested default: <DEFAULT_TARGET>
Reply y (or yes / ok / default) to accept the default, or paste an absolute path.
Trim leading/trailing whitespace and lowercase the user's response before matching. Recognize any of y, yes, ok, default, or an empty / whitespace-only response as accepting <DEFAULT_TARGET> — substitute it for <user-answer> in the normalize block below and do NOT re-prompt. (The harness doesn't auto-substitute the default for the model, so the prompt asks for an explicit acceptance word. The lowercase-after-trim rule covers natural variants like Y / Yes / OK that [Y/n]-style CLIs accept. The empty-input case is kept as a defensive fallback in case the user does press Enter and the harness delivers it.)
After the user answers, normalize and validate the sibling layout:
RAW_TARGET="<user-answer-or-DEFAULT_TARGET>"
case "$RAW_TARGET" in
"~"|"~/"*) RAW_TARGET="${HOME}${RAW_TARGET#\~}" ;;
esac
case "$RAW_TARGET" in
/*) TARGET="$RAW_TARGET" ;;
*) TARGET="$(pwd)/$RAW_TARGET" ;;
esac
TARGET_PARENT=$(dirname "$TARGET")
[ "$TARGET_PARENT" = "/" ] && TARGET_PARENT="$TARGET"
SIBLINGS_AT_TARGET=0
for p in android ios backend; do
if [ -d "$TARGET_PARENT/$APP-$p" ]; then
SIBLINGS_AT_TARGET=$((SIBLINGS_AT_TARGET+1))
fi
done
echo "TARGET=$TARGET"
echo "SIBLINGS_AT_TARGET=$SIBLINGS_AT_TARGET (out of 3) at $TARGET_PARENT/"
If SIBLINGS_AT_TARGET is 0, warn the user once (plain text):
Warning: none of <app>-android / <app>-ios / <app>-backend exist under <TARGET_PARENT>/. The spine will still be created, but you must clone the platform repos there before any agent run can succeed (see SETUP.md §3-2 for a setup.sh snippet that clones all three). Continue? (yes / no)
If the user declines, re-ask Q6.
Step 3 — Confirm and substitute
The 5 substituted Qs (org / app / baseBranch / figmaMcpNamespace / copyrightHolder) flow into two outputs:
.claude/mobile-spine.config.yaml (single source of truth — agents read at runtime)
- Inline substitution in workspace-owned doc files (CLAUDE.md / SETUP.md / README.md / LICENSE /
_context/operations.md / .claude/settings.json) so docs read naturally with the user's specific values.
Build a substitution map for the doc-file inlining:
| Placeholder | Replacement | Substitution scope |
|---|
myorg | Q1 | All processed template files |
myapp | Q2 | All processed template files |
develop (whole-word base-branch refs only) | Q3 | SETUP.md and _context/operations.md only — see precise rule in §4-2 |
<your name> | Q5 | LICENSE only (skip if "n/a") |
| Install location | Q6 | Used as the write target, not substituted as text |
Q4 (figmaMcpNamespace) is not substituted into any file — the agents read it from .claude/mobile-spine.config.yaml at invocation and adapt their figma tool calls accordingly. (v1.x substituted mcp__figma__* into agent files at init time; v2.0 ships agents as plugin primitives so that approach no longer applies.)
Print a summary back to the user (plain text, not AskUserQuestion):
[mobile-spine:init] Ready to scaffold (no GitHub calls — these values just get substituted into docs and saved to .claude/mobile-spine.config.yaml):
Target: <Q6>
Platform repos: <Q1>/<Q2>-android / <Q1>/<Q2>-ios / <Q1>/<Q2>-backend
Branch: <Q3>
Figma MCP: <Q4>
License: <Q5>
Proceed? (yes / no)
If the user says no, stop without writing anything.
Step 4 — Scaffold
Create the target directory, write the templated doc files, and write the runtime config.
4-1. Create root and subdirectories
mkdir -p "<TARGET>/.claude/commands" \
"<TARGET>/_context/api" "<TARGET>/_context/design" "<TARGET>/_tasks"
.claude/agents/ is not created — the four subagents are plugin primitives, served from plugins/mobile-spine/agents/. If the user later wants to override an agent at workspace level, they can create .claude/agents/ themselves and add a file; project-level agents take precedence over plugin-level.
4-2. Process every file in $TEMPLATES_DIR
For each source file (preserve relative path):
- Read with the Read tool.
- Apply substitutions:
- Global text:
myorg → Q1, myapp → Q2.
- In LICENSE only:
<your name> → Q5 (skip if "n/a").
- In
SETUP.md and _context/operations.md only: replace the whole word develop when it refers to the base branch (i.e. as a literal Git branch name) with Q3. Concretely, substitute every occurrence matching the patterns below:
Base branch: develop (table cells / inline)
--base develop (gh / git CLI)
`develop` (backticked branch reference, e.g. "From develop" / "off develop")
checkout -b develop / branch -a | grep develop / standalone develop after git commands
feat/{n}-{feature}-{platform} off develop and similar branch-name shapes ending in off develop (suffix in flow diagrams)
develop branch present (e.g. in operations.md week-0 result rows)
Do NOT substitute: develop inside figma-developer-mcp, the words "development" / "developer", or any prose meaning "to develop". When unsure on a given line, leave it as-is — false positives break the file, false negatives only need a manual touch-up.
- Write to the corresponding path under
<TARGET>.
For .gitkeep files: copy as-is (no substitution).
For the workspace's thin .claude/commands/feat.md stub: copy as-is — the stub has no placeholders (it just delegates to /mobile-spine:feat).
The full list of files to process:
CLAUDE.md
SETUP.md
README.md
LICENSE
.gitignore
.claude/settings.json
.claude/commands/feat.md (thin stub, copy as-is)
_context/operations.md
_context/api/.gitkeep (copy as-is)
_context/design/.gitkeep (copy as-is)
_tasks/.gitkeep (copy as-is)
4-3. Write the runtime config
After the file-processing loop, write .claude/mobile-spine.config.yaml. The agents (pm-agent / api-agent / android-agent / ios-agent) read this file at every invocation — it's the source of truth for workspace-specific values.
mobileSpineSchemaVersion is written as the current schema number (1). What happens when a workspace's schema and the plugin's disagree, and what a release owes when it raises the number, is defined once in §Config schema versioning below — do not restate it here.
Normalize Q4 and Q5 first:
-
figmaMcpNamespace: pass through the selected namespace after stripping any trailing UI markers. AskUserQuestion labels can carry up to two markers:
(Recommended) — added when an option is the recommended choice (both dynamic and fallback branches can attach this).
— detected — added in §Q4 4c's dynamic branch on per-server options.
Strip in this order (outermost first): drop a trailing (Recommended), then drop a trailing — detected. The persisted value must equal the bare glob mcp__<server-name>__* exactly — pm-agent reads this field and uses it for tools whitelist glob matching, and any leftover marker will fail to match. Concrete cases:
- Dynamic non-Recommended (e.g.
mcp__custom-figma__* — detected) → mcp__custom-figma__*
- Dynamic Recommended (
mcp__figma-desktop__* — detected (Recommended)) → strip (Recommended) → strip — detected → mcp__figma-desktop__*
- Fallback Recommended (
mcp__figma-desktop__* (Recommended)) → strip (Recommended) → mcp__figma-desktop__*
- Fallback non-Recommended (
mcp__figma__*) → no-op → mcp__figma__*
If Q4 was none, set to YAML null (null, not the string "none").
-
copyrightHolder: pass through Q5 verbatim. If Q5 was n/a, set to YAML null.
Then write (note the single-quoted heredoc terminator 'EOF' — prevents shell expansion of any $ in Q values):
cat > "<TARGET>/.claude/mobile-spine.config.yaml" <<'EOF'
mobileSpineSchemaVersion: 1
org: <Q1>
app: <Q2>
baseBranch: <Q3>
figmaMcpNamespace: <Q4-normalized>
copyrightHolder: <Q5-normalized>
EOF
4-4. Sanity verify
test -f "<TARGET>/CLAUDE.md" && \
test -f "<TARGET>/SETUP.md" && \
test -f "<TARGET>/.claude/settings.json" && \
test -f "<TARGET>/.claude/mobile-spine.config.yaml" && \
test -f "<TARGET>/.claude/commands/feat.md" && \
echo "OK: core files written"
grep -l "myorg\|myapp" \
"<TARGET>/.claude/settings.json" \
"<TARGET>/.claude/commands"/*.md \
"<TARGET>/CLAUDE.md" \
"<TARGET>/SETUP.md" \
"<TARGET>/README.md" \
"<TARGET>/_context/operations.md" \
2>/dev/null | head
The thin feat.md stub may legitimately match myorg/myapp if its text mentions placeholder examples — exclude it from the grep above if needed. Currently the v2.0 stub has no such references, so the simple grep above works.
Step 5 — Final report
Print to the user:
[mobile-spine:init] Scaffold complete.
Location: <TARGET>
Next steps:
1. cd <TARGET>
2. git init # (optional — version control)
3. Make sure these sibling repos exist:
<TARGET_PARENT>/<Q2>-android
<TARGET_PARENT>/<Q2>-ios
<TARGET_PARENT>/<Q2>-backend
(clone manually, or copy the setup.sh starter snippet from SETUP.md §3-2)
4. cd <TARGET> && claude
5. Inside the session, run /mcp to confirm the Figma MCP namespace if you set one.
6. Walk through SETUP.md §9 Week 0 verification before relying on isolation.
Config schema versioning
mobileSpineSchemaVersion is the contract between a workspace-owned file and a plugin that updates independently of it. /plugin marketplace update replaces the agent definitions in every workspace at once; nothing rewrites .claude/mobile-spine.config.yaml, because the user owns it. The two drift by construction, and the version field is how an agent notices.
Current schema: 1. Keys: mobileSpineSchemaVersion, org, app, baseBranch, figmaMcpNamespace, copyrightHolder.
Policy — asymmetric, because the two directions are not the same risk
| Situation | Behavior |
|---|
| Config schema higher than the plugin's | Proceed. Ignore keys you don't recognize, and say so once. The user is ahead of the plugin; that is not an error, and blocking them would punish an upgrade. |
| Config schema lower than the plugin's | Abort. The plugin expects keys the file does not carry, so proceeding means running on values that are absent or have been redefined — silently wrong, which is worse than stopped. |
| Version key missing, or not an integer | Abort. An unversioned config cannot be reasoned about. |
| Config file missing entirely | Existing behavior — abort and point at /mobile-spine:init. |
Never rewrite the config from an agent. It is workspace-owned, and .claude/ is outside every agent's allowed paths. Migration is a main-session action.
Release obligation
A release that raises mobileSpineSchemaVersion must ship the migration path in the same release. Not the release after. When the plugin starts expecting schema N, every existing workspace is instantly at N-1 and hits the abort above — so the recovery must already exist when the abort first becomes reachable.
Two consequences worth stating before anyone writes schema 2:
- The recovery pointer must travel with the plugin, not the workspace. A workspace stuck at an old schema also has an old
SETUP.md — the same drift that caused the problem cannot be its own cure. The abort message names this section; it does not name a workspace file.
- Migration belongs in its own command, not in the init skill. By the time a schema mismatch can happen,
/mobile-spine:init has already run — it creates workspaces, and re-running it on an existing one is not what the user wants. A migration reads an existing config, applies the mapping for each schema step, preserves user-set values, defaults new keys, and writes back. That is a different program; it shares only the key list above.
Schema history
| Schema | Introduced in | Migration from previous |
|---|
1 | v2.0 | n/a — first schema. A v1.x workspace has no config file at all; see the scaffolded SETUP.md §0. |
No migration command ships today because no schema transition exists. The first release to add one ships it alongside.
Notes
- No git init / no GitHub repo creation. This skill stops at file creation, by design (per the user's preference). Version control and remote setup are deliberate user actions.
- Figma MCP namespace: stored in
.claude/mobile-spine.config.yaml. Agents read it at invocation; null means "skip all Figma steps". The plugin's pm-agent frontmatter lists both mcp__figma__* and mcp__figma-desktop__* in its tools array so either namespace works without per-workspace customization. If your namespace is something else entirely, you can override pm-agent at workspace level by creating .claude/agents/pm-agent.md with your tools list (project-level agents take precedence over plugin-level).
- Stack-specific tweaks (Spring / Nest / FastAPI / Express):
api-agent.md keeps all stacks listed. Since the agent is plugin-managed in v2.0, you can't prune unused entries at init time — but you also don't need to. The agent only acts on stacks it finds in ../<app>-backend/.
- License copyright placeholder: if Q5 is "n/a",
<your name> remains in the LICENSE and copyrightHolder is null in the config — the user fills it in later.