@mmerterden/multi-agent-pipeline 10.0.6 → 10.2.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 (50) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/README.md +1 -0
  3. package/package.json +1 -1
  4. package/pipeline/agents/android-architect.md +3 -3
  5. package/pipeline/agents/backend-architect.md +3 -3
  6. package/pipeline/agents/code-reviewer.md +4 -4
  7. package/pipeline/agents/ios-architect.md +3 -3
  8. package/pipeline/agents/security-auditor.md +3 -3
  9. package/pipeline/commands/multi-agent/dev-autopilot.md +3 -3
  10. package/pipeline/commands/multi-agent/dev-local-autopilot.md +2 -2
  11. package/pipeline/commands/multi-agent/dev-local.md +3 -3
  12. package/pipeline/commands/multi-agent/dev.md +9 -9
  13. package/pipeline/commands/multi-agent/help.md +10 -10
  14. package/pipeline/commands/multi-agent/refs/channels/jira.md +1 -0
  15. package/pipeline/commands/multi-agent/refs/cross-cli-contract.md +7 -3
  16. package/pipeline/commands/multi-agent/refs/features/model-fallback.md +36 -18
  17. package/pipeline/commands/multi-agent/refs/phases/operations.md +15 -5
  18. package/pipeline/commands/multi-agent/refs/phases/phase-0-init.md +16 -2
  19. package/pipeline/commands/multi-agent/refs/phases/phase-1-analysis.md +8 -3
  20. package/pipeline/commands/multi-agent/refs/phases/phase-2-planning.md +1 -1
  21. package/pipeline/commands/multi-agent/refs/phases/phase-4-review.md +30 -8
  22. package/pipeline/commands/multi-agent/resume.md +4 -1
  23. package/pipeline/commands/multi-agent.md +5 -5
  24. package/pipeline/lib/fetch-confluence.sh +2 -2
  25. package/pipeline/lib/fetch-crashlytics.sh +2 -2
  26. package/pipeline/lib/fetch-fortify.sh +1 -1
  27. package/pipeline/lib/fetch-swagger.sh +1 -1
  28. package/pipeline/lib/figma-screenshot.sh +2 -2
  29. package/pipeline/preferences-template.json +8 -1
  30. package/pipeline/schemas/agent-state.schema.json +8 -0
  31. package/pipeline/schemas/figma-project-config.schema.json +39 -0
  32. package/pipeline/schemas/prefs.schema.json +3 -3
  33. package/pipeline/scripts/cost-table.json +0 -6
  34. package/pipeline/scripts/fixtures/install-layout.tsv +2 -2
  35. package/pipeline/scripts/phase-tracker.sh +7 -0
  36. package/pipeline/scripts/smoke-model-fallback.sh +20 -12
  37. package/pipeline/scripts/validate-state.mjs +108 -0
  38. package/pipeline/scripts/write-state.mjs +15 -4
  39. package/pipeline/skills/figma-common/README.md +8 -1
  40. package/pipeline/skills/figma-common/figma-bottom-sheets/SKILL.md +137 -0
  41. package/pipeline/skills/figma-common/figma-evolve-component/SKILL.md +61 -0
  42. package/pipeline/skills/figma-common/figma-navigation/SKILL.md +142 -0
  43. package/pipeline/skills/figma-common/figma-overlays/SKILL.md +128 -0
  44. package/pipeline/skills/figma-common/figma-ui-patterns/SKILL.md +1 -0
  45. package/pipeline/skills/figma-common/figma-ui-patterns/patterns/animated-gradient-border.md +112 -0
  46. package/pipeline/skills/figma-ios/figma-to-component/SKILL.md +15 -0
  47. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-3d-patterns.md +31 -0
  48. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-4b-view.md +10 -0
  49. package/pipeline/skills/figma-ios/figma-to-component/reference/accessibility.md +55 -1
  50. package/pipeline/skills/figma-ios/figma-to-component/reference/orchestrator-discipline.md +1 -1
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+ // validate-state.mjs - resume-safety check for agent-state.json.
3
+ //
4
+ // agent-state.json is the canonical per-run state read by every phase. A
5
+ // half-written or corrupt state (crash mid-write, truncated JSON, missing
6
+ // currentPhase) makes resume silently re-enter the wrong phase. Phase 0 and
7
+ // resume run this before trusting the file.
8
+ //
9
+ // This is deliberately a RESUME-SAFETY check, not strict conformance to
10
+ // pipeline/schemas/agent-state.schema.json. Real state files written by older
11
+ // pipeline versions omit schemaVersion, use a string shortId ("#2"), and write
12
+ // status "completed" - they are still perfectly resumable, so rejecting them
13
+ // here would break every legacy resume. We fail only on the conditions that
14
+ // actually make a resume unsafe:
15
+ // - the file does not parse (truncated / corrupt)
16
+ // - root is not an object
17
+ // - currentPhase is missing or out of the 0..7 range (resume keys on it)
18
+ // - phases is present but not an object, or an entry is not an object
19
+ // Phase status strings are NOT enum-checked: real writers use "completed"
20
+ // alongside the schema's "done", and an odd status does not make a resume
21
+ // unsafe (resume keys on currentPhase, not phase status). Zero deps, same
22
+ // style as the other validators.
23
+ //
24
+ // Usage:
25
+ // node validate-state.mjs path/to/agent-state.json
26
+ // cat agent-state.json | node validate-state.mjs -
27
+ //
28
+ // Exit codes:
29
+ // 0 - safe to resume
30
+ // 1 - unsafe: unparseable, or missing/invalid currentPhase, or malformed phases
31
+ // 64 - usage error
32
+
33
+ import { readFileSync } from "node:fs";
34
+
35
+ function readInput() {
36
+ const arg = process.argv[2];
37
+ if (!arg) {
38
+ console.error("usage: validate-state.mjs <path|->");
39
+ process.exit(64);
40
+ }
41
+ if (arg === "-") {
42
+ const chunks = [];
43
+ process.stdin.setEncoding("utf-8");
44
+ return new Promise((resolve) => {
45
+ process.stdin.on("data", (c) => chunks.push(c));
46
+ process.stdin.on("end", () => resolve(chunks.join("")));
47
+ });
48
+ }
49
+ return Promise.resolve(readFileSync(arg, "utf-8"));
50
+ }
51
+
52
+ async function main() {
53
+ const raw = await readInput();
54
+ let s;
55
+ try {
56
+ s = JSON.parse(raw);
57
+ } catch (err) {
58
+ emit({ ok: false, code: 1, errors: [`invalid JSON: ${err.message}`] });
59
+ process.exit(1);
60
+ }
61
+
62
+ const errors = [];
63
+
64
+ if (typeof s !== "object" || s === null || Array.isArray(s)) {
65
+ emit({ ok: false, code: 1, errors: ["root must be an object"] });
66
+ process.exit(1);
67
+ }
68
+
69
+ // currentPhase is the field resume keys on - it must be present and in range.
70
+ if (!Number.isInteger(s.currentPhase) || s.currentPhase < 0 || s.currentPhase > 7) {
71
+ errors.push(`currentPhase must be integer 0..7, got ${JSON.stringify(s.currentPhase)}`);
72
+ }
73
+
74
+ // phases is optional (some early/aborted states omit it), but if present it
75
+ // must be a well-formed map so sub-step resume can read it.
76
+ if (s.phases !== undefined) {
77
+ if (typeof s.phases !== "object" || s.phases === null || Array.isArray(s.phases)) {
78
+ errors.push("phases, when present, must be an object keyed by phase number");
79
+ } else {
80
+ for (const [k, v] of Object.entries(s.phases)) {
81
+ if (!/^[0-7]$/.test(k)) {
82
+ errors.push(`phases key invalid: "${k}" (expected "0".."7")`);
83
+ continue;
84
+ }
85
+ if (typeof v !== "object" || v === null) {
86
+ errors.push(`phases["${k}"] must be an object`);
87
+ }
88
+ }
89
+ }
90
+ }
91
+
92
+ if (errors.length > 0) {
93
+ emit({ ok: false, code: 1, errors });
94
+ process.exit(1);
95
+ }
96
+
97
+ emit({ ok: true, code: 0, errors: [] });
98
+ process.exit(0);
99
+ }
100
+
101
+ function emit(result) {
102
+ process.stdout.write(JSON.stringify(result) + "\n");
103
+ }
104
+
105
+ main().catch((err) => {
106
+ emit({ ok: false, code: 1, errors: [`unhandled: ${err.message}`] });
107
+ process.exit(1);
108
+ });
@@ -20,8 +20,13 @@
20
20
  * Exit codes:
21
21
  * 0 - write succeeded
22
22
  * 1 - invalid JSON on stdin
23
- * 2 - lock timeout (another writer held the lock >5s)
23
+ * 2 - lock timeout (another writer held the lock past the acquire window)
24
24
  * 3 - I/O error (disk full, permission denied, parent dir missing)
25
+ *
26
+ * Tunables (env):
27
+ * WRITE_STATE_LOCK_TIMEOUT_MS acquire window before exit 2 (default 15000)
28
+ * WRITE_STATE_LOCK_STALE_MS age past which a lock is reclaimed (default 30000)
29
+ * A dead holder (PID no longer alive) is reclaimed immediately regardless of age.
25
30
  */
26
31
 
27
32
  import {
@@ -65,9 +70,9 @@ function parseArgs() {
65
70
  * @param {number} timeoutMs
66
71
  * @returns {Promise<void>}
67
72
  */
68
- async function acquireLock(path, timeoutMs = 5000) {
73
+ async function acquireLock(path, timeoutMs = Number(process.env.WRITE_STATE_LOCK_TIMEOUT_MS) || 15_000) {
69
74
  const lockPath = `${path}.lock`;
70
- const staleMs = 60_000;
75
+ const staleMs = Number(process.env.WRITE_STATE_LOCK_STALE_MS) || 30_000;
71
76
  const deadline = Date.now() + timeoutMs;
72
77
  while (Date.now() < deadline) {
73
78
  try {
@@ -89,7 +94,13 @@ async function acquireLock(path, timeoutMs = 5000) {
89
94
  await new Promise((r) => setTimeout(r, 50));
90
95
  }
91
96
  }
92
- throw Object.assign(new Error(`lock timeout on ${lockPath}`), { code: "LOCK_TIMEOUT" });
97
+ throw Object.assign(
98
+ new Error(
99
+ `lock timeout on ${lockPath} after ${timeoutMs}ms. If no other writer is running, ` +
100
+ `the lock is stale - remove it (rm "${lockPath}") and retry, or raise WRITE_STATE_LOCK_TIMEOUT_MS.`,
101
+ ),
102
+ { code: "LOCK_TIMEOUT" },
103
+ );
93
104
  }
94
105
 
95
106
  /**
@@ -2,7 +2,7 @@
2
2
 
3
3
  Shared skills that both `pipeline/skills/figma-ios/` (SwiftUI) and `pipeline/skills/figma-android/` (Jetpack Compose) dispatch. Moved here by WS-7b of the v5.0.0 rebuild so the platform trees stay lean and there's no duplication.
4
4
 
5
- ## What lives here (27 skills)
5
+ ## What lives here (31 skills)
6
6
 
7
7
  **Workflow orchestration:**
8
8
 
@@ -28,6 +28,7 @@ Shared skills that both `pipeline/skills/figma-ios/` (SwiftUI) and `pipeline/ski
28
28
  - `figma-validate` - Pre-implementation validation
29
29
  - `figma-fix` - Targeted bug fix from review findings
30
30
  - `figma-mend` - Re-implement component from scratch
31
+ - `figma-evolve-component` - Reconcile drift + additively extend an existing component (human-gated)
31
32
 
32
33
  **Setup + utility:**
33
34
 
@@ -38,6 +39,12 @@ Shared skills that both `pipeline/skills/figma-ios/` (SwiftUI) and `pipeline/ski
38
39
  - `figma-form-integration` - Form protocol adapter (shared UX)
39
40
  - `figma-price-integration` - Price protocol adapter (shared UX)
40
41
 
42
+ **Cross-cutting interaction adapters (native-SwiftUI-first; project system via `figma-config` `ui.*`):**
43
+
44
+ - `figma-navigation` - Navigation pattern: headless Output vs self-route; native `NavigationStack` or `ui.navigationSystem`
45
+ - `figma-overlays` - Overlay pattern: toast/HUD/alert/data-modal; native `.alert`/`.sheet(item:)` or `ui.overlaySystem`
46
+ - `figma-bottom-sheets` - Bottom-sheet/detent pattern: native `.presentationDetents` or `ui.sheetSystem`
47
+
41
48
  **Performance batch:**
42
49
 
43
50
  - `performance-start`, `performance-swiftui`, `performance-tour`, `performance-review-next`, `performance-iteration-commit-all`
@@ -0,0 +1,137 @@
1
+ ---
2
+ name: figma-bottom-sheets
3
+ description: "Bottom-sheet / detent pattern integration for Figma-to-SwiftUI components — half-sheets, drawers, pinned bottom CTAs, resizable pull-up panels, content-sized sheets, and multi-step in-sheet flows. Native SwiftUI (.sheet + presentationDetents/dragIndicator/backgroundInteraction) by default; an optional project-supplied detent-sheet system when figma-config declares one. Reach for this when a design shows a bottom sheet, half-sheet, bottom drawer, snap points, a sticky footer button, or a wizard-in-a-sheet."
4
+ user-invocable: true
5
+ allowed-tools: Read, Glob, Grep
6
+ status: wip-v5
7
+ sourced-from: upstream/figma-bottom-sheets
8
+ ---
9
+
10
+ # /figma-bottom-sheets - Bottom-Sheet / Detent Pattern Integration
11
+
12
+ ## Purpose
13
+
14
+ Consumed by the figma-to-swiftui pipeline (Phase 3D detection, Phase 4B view) and available ad-hoc, this skill defines how **bottom-anchored, detent-based surfaces** are presented. Sibling of `/figma-overlays` (transient/blocking overlays) and `/figma-navigation` (routing): native-SwiftUI default + a config-driven hook for projects that ship a custom detent-sheet engine.
15
+
16
+ **Generic by design.** Default is stock SwiftUI `.sheet` + `.presentationDetents`. If `figma-config.json` declares `ui.sheetSystem`, route through that engine by name - same rules, project vocabulary. (Some apps replace system sheets because iOS 26 forces a Liquid-Glass floating inset on partial detents with no removal API; that replacement is the custom-system case, not the default.)
17
+
18
+ ---
19
+
20
+ ## Strict Rules (non-negotiable)
21
+
22
+ 1. **The sheet surface is a caller concern; the component is the content.** A reusable component authored from Figma is the sheet's **content**, not the presenter. The caller owns the `isPresented`/`item` binding and the detents. A component that presents itself can't be reused elsewhere.
23
+ 2. **No magic numbers.** Detents, corner radius, grabber size, insets come from tokens / figma-config token patterns, never hard-coded literals.
24
+ 3. **Detents ascend, max 3.** Pass `[detent]` lowest→highest; the sheet opens at the lowest. Prefer semantic detents (`.medium`, `.large`, `.fraction`, content-height) over pixel heights where possible.
25
+ 4. **A bottom sheet is not a modal card and not a navigation push.** Data alert/modal card → `/figma-overlays`; screen routing → `/figma-navigation` (a routed modal may still *render* as a sheet, but navigation owns the routing).
26
+ 5. **Native SwiftUI is the default; a custom sheet engine is opt-in via config.** Only use a project detent-sheet system when `ui.sheetSystem.mode == "custom"`.
27
+
28
+ ---
29
+
30
+ ## When to Invoke This Skill
31
+
32
+ 1. **Phase 3D - sheet affordance/pattern:** the design is (or contains) a bottom sheet / half-sheet / drawer, a **pinned bottom CTA** bar, a resizable pull-up panel, a **content-sized** sheet, or a **multi-step** in-sheet flow (in-sheet back/next that resizes per step).
33
+ 2. **Phase 4B - View:** the view is authored as sheet content, or presents a child as a sheet.
34
+ 3. **Ad-hoc:** "half sheet", "bottom drawer", "pull-up panel", "snap points", "sticky footer button", "multi-step sheet", "sheet sized to its content".
35
+
36
+ **Not a bottom sheet (guards):**
37
+ - Toast / HUD / alert / data modal card → `/figma-overlays`.
38
+ - Full screen navigation / tab / deep link → `/figma-navigation`.
39
+ - An inline expandable section within the page flow (not an anchored surface) → plain component content.
40
+
41
+ ---
42
+
43
+ ## Native-first architecture (default)
44
+
45
+ ```
46
+ Caller
47
+ | owns isPresented / item + detents; presents the component as content
48
+ v
49
+ .sheet(item:/isPresented:) {
50
+ ComponentContent() // the Figma-authored component
51
+ .presentationDetents([.medium, .large])
52
+ .presentationDragIndicator(.visible)
53
+ .presentationBackgroundInteraction(.enabled(upThrough: .medium)) // passthrough-ish
54
+ .presentationCornerRadius(<token>)
55
+ }
56
+ ```
57
+
58
+ - **Half / expandable:** `.presentationDetents([.medium, .large])` or `[.fraction(0.25), .fraction(0.6), .large]`.
59
+ - **Content-sized:** `.presentationDetents([.height(measuredHeight)])` using a height read via a preference key (a `sizeThatFits`/`GeometryReader` measured content height), then scroll past the cap.
60
+ - **Pinned bottom CTA (part of the screen, not a modal):** `.safeAreaInset(edge: .bottom) { CTA() }` - reserves space, screen stays interactive. This is NOT a presented sheet.
61
+ - **Backdrop behavior:** dim+block is the `.sheet` default; passthrough/interactive-behind uses `.presentationBackgroundInteraction(.enabled(...))`.
62
+ - **Multi-step in-sheet flow:** drive a `NavigationStack(path:)` *inside* the sheet content for push/pop, resizing detents per step; each step is a headless view emitting `Output` (see `/figma-navigation`). If the target project's custom engine forbids a nested `NavigationStack`, use its engine (config hook).
63
+ - **Overlays over a sheet:** mount the toast/HUD host on the sheet content (`/figma-overlays`), not the root.
64
+
65
+ Tokens per `reference/ui` + figma-config token patterns.
66
+
67
+ ## Config hook - project-supplied detent-sheet engine
68
+
69
+ When `figma-config.json` declares:
70
+ ```jsonc
71
+ "ui": { "sheetSystem": {
72
+ "mode": "custom",
73
+ "brandedModifier": "bottomSheet", // .bottomSheet(isPresented:title:) branded entry
74
+ "headlessModifiers": ["modalSheet", "dockedSheet", "expandableSheet", "detentNavigationSheet"],
75
+ "detentType": "SheetDetentType", // detent enum
76
+ "backdropType": "Backdrop", // backdrop enum
77
+ "cornerRadiusToken": ".Radius.n12" // branded top-corner token
78
+ }}
79
+ ```
80
+ apply the SAME rules via the project's engine (names from config, do NOT hardcode):
81
+ - Use `.{brandedModifier}` for product sheets needing brand chrome; the headless modifiers for unbranded surfaces / CTAs / resizable panels / multi-step flows.
82
+ - Detents/backdrops use `{detentType}`/`{backdropType}` values; corner radius from `{cornerRadiusToken}`.
83
+ - Multi-step flows use the engine's navigation sheet (e.g. `detentNavigationSheet` with a `Navigator<Route>`) rather than a raw nested `NavigationStack`.
84
+ - One container-owned grabber (pass custom headers grabberless if the engine draws the grabber).
85
+ - Present only from the surface's own host - never walk the scene/window hierarchy for a "top" controller.
86
+
87
+ If `mode` is absent or `native`, use the native-first section.
88
+
89
+ ---
90
+
91
+ ## Integration with Phase 3D
92
+
93
+ Record in `04b_component_architecture.md`:
94
+
95
+ ```markdown
96
+ ## Bottom Sheet
97
+ | Signal | Surface | Detents | Owner |
98
+ |---|---|---|---|
99
+ | Half-sheet picker | presented sheet | [.medium, .large] | caller owns isPresented |
100
+ | Sticky "Devam Et" bar | pinned bottom CTA | n/a (safeAreaInset) | part of screen |
101
+ | 3-step add flow | multi-step in-sheet | per-step (content height) | caller drives path |
102
+ ```
103
+
104
+ Property classification addition:
105
+
106
+ | Classification | When | Swift Pattern |
107
+ |---|---|---|
108
+ | Sheet content | Component is authored to live inside a sheet | plain component; caller adds `.presentationDetents(...)` |
109
+ | Present-child-as-sheet | Component reveals a child surface | caller-owned `isPresented`/`item` + `.sheet` (component emits the trigger intent) |
110
+ | Pinned CTA | Sticky bottom action bar | `.safeAreaInset(edge:.bottom)` at the screen, not a modal |
111
+
112
+ ## Integration with Phase 4B
113
+ - Author the component as sheet **content**; let the caller own presentation + detents.
114
+ - Prefer semantic detents; content-sized sheets measure height via a preference key.
115
+ - Pinned CTA = `.safeAreaInset(edge:.bottom)`, not a presented sheet.
116
+ - Multi-step flow = `NavigationStack` inside the sheet (native) or the engine's nav sheet (custom).
117
+ - If `ui.sheetSystem.mode == custom`, use `{brandedModifier}`/headless modifiers + `{detentType}`/`{backdropType}` per config.
118
+ - Verify on the simulator across detents, backdrop, and drag/dismiss on the project's min + current iOS.
119
+
120
+ ---
121
+
122
+ ## When NOT to Use Bottom-Sheet Integration
123
+ - Toast/HUD/alert/data modal card → `/figma-overlays`.
124
+ - Screen routing/tabs/deep links → `/figma-navigation`.
125
+ - An inline expandable/disclosure within page flow → plain component content.
126
+
127
+ ## Checklist - Before Leaving the Skill
128
+ ```
129
+ [ ] Component is sheet CONTENT; caller owns isPresented/item + detents
130
+ [ ] Detents ascend, max 3; semantic detents preferred; no magic-number heights
131
+ [ ] Pinned CTA uses safeAreaInset(edge:.bottom), not a presented sheet
132
+ [ ] Content-sized sheet measures height via preference key, then scrolls
133
+ [ ] Multi-step flow uses NavigationStack-in-sheet (native) or engine nav sheet (custom)
134
+ [ ] Corner radius / grabber / insets from tokens, not literals
135
+ [ ] Default native SwiftUI; custom engine only when figma-config ui.sheetSystem.mode == custom
136
+ [ ] Overlays over the sheet mount on the sheet content surface
137
+ ```
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: figma-evolve-component
3
+ description: "On-demand reconcile-and-extend for an ALREADY-implemented SwiftUI component. Heals drift from current Figma (stale variants/props/structure, design-token drift, a detached/overridden deviation) AND extends the component non-destructively to cover a need the design can't express — always behind a mandatory human gate. Reach for this when an existing component drifted from its current design or doesn't cover a need. Not for building from scratch (figma-to-swiftui / figma-mend) or fixing one review bug (figma-fix)."
4
+ user-invocable: true
5
+ argument-hint: <component | figma-url> [--report-only]
6
+ allowed-tools: Task, Read, Glob, Grep, Bash, Edit, Write, AskUserQuestion
7
+ status: wip-v5
8
+ sourced-from: upstream/figma-evolve-component
9
+ ---
10
+
11
+ # /figma-evolve-component - reconcile & extend an existing component
12
+
13
+ > **Use when:** an existing UI component drifted from its current Figma design, OR it doesn't cover a need you now have.
14
+ > **Don't use when:** building a brand-new component → `figma-to-swiftui`; re-implementing a discarded component from scratch → `figma-mend`; fixing a single bug filed in review → `figma-fix`.
15
+
16
+ ## Scope
17
+ Reconcile **one** existing component against current Figma and extend it for a real need, then propagate the change to its Code Connect mapping + docs + wiki. **Out:** new components, networking, navigation graphs, batch/all-component sweeps.
18
+
19
+ ## Invariants
20
+ - **Two truths.** Design is truth for **heal** (drift); **the user** is truth for **extend** (a need/divergence design can't supply). Design cannot tell us which gap to close - the user does.
21
+ - **Human gate is mandatory.** Never write before the user's decisions (Procedure step 3). This is prompt-driven, not autonomous. `--report-only` stops after the diff.
22
+ - **Additive / non-destructive.** Preserve the public API, init signatures, and variant cases. Never remove or rename them without explicit confirmation - it breaks callers.
23
+ - **Detached / overridden node = suspect truth.** If the Figma node is a detached instance or has local overrides, surface it and reconcile against the **main** component - don't auto-heal toward a deviated instance.
24
+ - **Recorded divergences are respected.** A `// evolve-keep: <reason>` marker in the code means the audit skips it and apply never reverts it.
25
+ - **Not done until it builds + existing call sites still compile + tests/snapshots pass** (`figma-to-swiftui` Phase 5 / `reference/build-and-test`).
26
+
27
+ ## Conventions & golden paths (point, don't re-implement)
28
+ - **Design truth** → `/figma-utility` (`properties`, `tokens`, `context`, `metadata`, `screenshot`, `code-connect`). Analysis phase only - same MCP/REST rules as the pipeline.
29
+ - **Locate the component + its Figma node** → `FigmaRegistryUtility` lookup / repo grep for the `{ComponentName}` files and its `*.figma.swift`, or a URL the user gives. Paths resolve from `figma-config.json` (`repos.*`, component roots) - never hardcode.
30
+ - **Component structure + tokens** → `reference/ui` (`figma-to-component/reference/`), `figma-to-swiftui-effects.md`, `macros.md`. Cross-cutting patterns → `/figma-ui-patterns`, `/figma-form-integration`, `/figma-price-integration`, `/figma-navigation`, `/figma-overlays`, `/figma-bottom-sheets`.
31
+ - **After any prop/variant change, regenerate the derived artifacts - don't hand-roll:** Code Connect → `figma-to-component` Phase 6; local `.figma.md` doc → Phase 4C / `figma-component-docs`; public wiki → Phase 7 / `figma-component-wiki`.
32
+ - Build/run/test gate → `reference/build-and-test`; ship → `figma-commit` / `figma-iteration-commit`.
33
+
34
+ ## Decision rules - classify each finding heal / extend / keep
35
+ - **stale** — variant/prop/structure differs from current Figma → **heal** toward design.
36
+ - **token drift** — hardcoded or old color/spacing/typography/radius vs the current token → **heal** to the current token (resolve the token name via the registry / `reference/ui`; don't guess).
37
+ - **detached / override** — node deviates from its main → **stop, ask the user** which is truth.
38
+ - **coverage gap** — design doesn't cover the need, or the impl is wrong → **extend**; the user says what to add.
39
+ - Removing/renaming public API or a variant case is **never** automatic → explicit confirmation.
40
+
41
+ ## Procedure (the gate is non-negotiable)
42
+ 1. **Resolve & gather** — locate the component file(s) + its Figma node (registry / Code Connect map / user URL). Pull design truth via `/figma-utility`.
43
+ - If the node is unresolved, or **detached/overridden** → ask the user (which node is truth?).
44
+ 2. **Diff & present** — list findings per drift axis, each tagged **heal / extend / keep**. **No writes.** (`--report-only` stops here.)
45
+ 3. **GATE — user decides** (`AskUserQuestion`): which to heal, which to keep (→ add `// evolve-keep:` marker), what to extend (the need design can't supply). Do not proceed without this.
46
+ 4. **Apply** — only the approved changes, additively / non-destructively. When healing token drift, resolve the current token name via the registry / `reference/ui` - don't guess it.
47
+ 5. **Propagate** — regenerate Code Connect (Phase 6), the local `.figma.md` doc (Phase 4C), and the wiki page (Phase 7) for the changed props/variants. If keys changed (testing/loc/a11y/analytics), update them through the normal utility path.
48
+ 6. **Verify** — build + snapshot/preview; confirm existing call sites still compile (no API break); run affected tests.
49
+
50
+ ## Verification
51
+ Build (project scheme from figma-config) + snapshot/preview render; callers compile; Code Connect + `.figma.md` + wiki reflect the new props/variants.
52
+
53
+ ## Pitfalls
54
+ - Applying without the step-3 gate - this skill is human-gated by design.
55
+ - Healing toward a detached/overridden node (deviated truth).
56
+ - Removing/renaming a variant or init param → breaks callers; stay additive.
57
+ - Skipping Propagate → Code Connect + docs silently go stale after a prop/variant change.
58
+
59
+ ## Escape hatches
60
+ - Build new from scratch → `figma-to-swiftui`. Re-implement a discarded component → `figma-mend`. Fix one review bug → `figma-fix`.
61
+ - Just fetch design data → `/figma-utility`. Build/test → `reference/build-and-test`. Commit/PR → `figma-commit`.
@@ -0,0 +1,142 @@
1
+ ---
2
+ name: figma-navigation
3
+ description: "Navigation pattern integration for Figma-to-SwiftUI components. Guides how a component reaches other screens, tabs, or deep links without coupling to a router — headless screens that emit a typed Output, native NavigationStack/navigationDestination by default, an optional project-supplied navigation system (router + route enum + scene container) when figma-config declares one. Reach for this when a design embeds a tab bar, nav bar / back button, a push/detail affordance, or a deep-link entry point."
4
+ user-invocable: true
5
+ allowed-tools: Read, Glob, Grep
6
+ status: wip-v5
7
+ sourced-from: upstream/figma-navigation
8
+ ---
9
+
10
+ # /figma-navigation - Navigation Pattern Integration
11
+
12
+ ## Purpose
13
+
14
+ Consumed by the figma-to-swiftui pipeline (Phase 3D pattern detection, Phase 4B view implementation) and available for ad-hoc/vibe-coding, this skill defines how a component participates in navigation **without owning routing**. It is the navigation counterpart of `/figma-form-integration` and `/figma-price-integration`: a cross-cutting integration with a **native-SwiftUI default** and a **config-driven hook** for projects that ship their own navigation system.
15
+
16
+ **Generic by design.** Nothing here assumes a specific app. The default is stock SwiftUI (`NavigationStack`, `navigationDestination`, `.toolbar`, `.sheet`). If the target project declares a custom navigation system in its `figma-config.json` (`ui.navigationSystem`), this skill routes to that system's types by name instead — same rules, project-specific vocabulary.
17
+
18
+ ---
19
+
20
+ ## Strict Rules (non-negotiable)
21
+
22
+ 1. **A component never names a route, a sibling screen, or a router.** A component/screen takes its arrival data as init params and emits intent through a typed **`Output`** closure (`onSelect`, `onSubmit`, `output:`). The *caller* (scene/coordinator/parent) maps each output to a navigation edge. This keeps the component reusable and testable in isolation.
23
+ 2. **No navigation side effects inside a component body.** No `NavigationLink(destination:)` hardcoding a concrete next screen, no router calls, no global singletons touched from `body`. Navigation is a caller concern expressed via the emitted output.
24
+ 3. **Selection/tab state is observable data, not an imperative bridge.** A selected tab/section is a bound value (`@Binding`/observable), written by the caller; the component reflects it. A selection set before the container mounts is simply rendered on appear - no cold-start race.
25
+ 4. **Deep links reduce to state and never crash.** A URL parses to a plan/value; malformed input is dropped (logged, no trap). The component is never the deep-link parser - it only exposes the entry state the parser can set.
26
+ 5. **Native SwiftUI is the default; a custom system is opt-in via config.** Do NOT invent a bespoke router for a project that hasn't asked for one. Only use custom navigation types when `ui.navigationSystem.mode == "custom"` is set in `figma-config.json`.
27
+
28
+ ---
29
+
30
+ ## When to Invoke This Skill
31
+
32
+ Invoke when ANY of these are detected in the design or task:
33
+
34
+ 1. **Phase 3D - navigation sub-component / affordance:** a nested instance of a navigation-capable design element:
35
+ - Tab bar / segmented section switcher that changes the top-level view
36
+ - Navigation bar / header with a **back button** or close affordance
37
+ - A row/card whose variant implies a **push/detail** affordance (chevron, "Detaylar", disclosure)
38
+ - A link/button whose label implies leaving the screen ("Devam", "Tümünü gör")
39
+ - A deep-link entry point (the design is a landing surface reached by URL)
40
+ 2. **Phase 4B - View:** the view renders a back button, tab strip, or a control that should navigate.
41
+ 3. **Ad-hoc:** wiring a screen's push/pop/tab/deep-link flow outside the full pipeline.
42
+
43
+ **Not navigation (false-positive guards):**
44
+ - A segmented control that filters in-place (no screen change) → plain `Binding<Enum>`, not navigation.
45
+ - A button that submits a form → that's `/figma-form-integration`; the *result* may navigate, but the control is a form action.
46
+ - A modal card of data / a toast / a loading HUD → `/figma-overlays`. A bottom sheet surface → `/figma-bottom-sheets`.
47
+
48
+ ---
49
+
50
+ ## Native-first architecture (default)
51
+
52
+ ```
53
+ Parent / Scene (caller)
54
+ | owns NavigationStack(path:) or tab Binding, maps Output → edge
55
+ v
56
+ ComponentView
57
+ | init(arrivalData…, output: @escaping (Output) -> Void)
58
+ | emits .selected(id) / .continue / .back - never routes
59
+ v
60
+ (caller) path.append(route) | selectedTab = .x | present(.sheet)
61
+ ```
62
+
63
+ - **Push/pop within a flow:** caller owns `NavigationStack(path:)`; component emits `Output`; caller appends a `Hashable` route; `navigationDestination(for:)` renders it.
64
+ - **Back button:** prefer the system back button. A custom header back affordance emits `.back` (or calls `dismiss`); it renders only when there is something to pop (depth > 0 / `isPresented`).
65
+ - **Tabs:** `TabView(selection:)` bound to an observable selection the caller owns; the component reads/writes the binding, never a global.
66
+ - **Deep links:** caller parses URL → a `Hashable` route / tab selection and applies it to the same path/selection state the UI already uses. Component only exposes the entry state.
67
+ - **Modal presentation of another screen:** `.sheet(item:)` / `.fullScreenCover(item:)` driven by caller state (data-driven, no `AnyView`). A *bottom sheet surface* is `/figma-bottom-sheets`; an *alert/data modal card* is `/figma-overlays`.
68
+
69
+ Design tokens (spacing, colors, typography) follow `reference/ui` + the project's figma-config token patterns - never raw literals.
70
+
71
+ ## Config hook - project-supplied navigation system
72
+
73
+ When `figma-config.json` declares:
74
+ ```jsonc
75
+ "ui": { "navigationSystem": {
76
+ "mode": "custom",
77
+ "router": "AppRouter", // type that dispatches routes
78
+ "routeEnum": "AppRoute", // exhaustive cross-surface route enum
79
+ "sceneType": "SceneContent", // headless screen container (optional)
80
+ "navigatorProtocol": "NavigatorProtocol" // DI'd selection/present API (optional)
81
+ }}
82
+ ```
83
+ then apply the SAME rules, but express them in the project's vocabulary resolved from config (do NOT hardcode the names below - read them from `ui.navigationSystem`):
84
+
85
+ - Wrap screen content in `{sceneType}` (headless container) instead of a bare `NavigationStack` child, when provided.
86
+ - Screens still emit a typed `Output`; the domain coordinator maps outputs to the project's route type (`{routeEnum}`) and the `{router}` dispatches.
87
+ - Cross-surface jumps go through the DI'd `{navigatorProtocol}` (`selectTab`, `present`), never by importing a sibling domain.
88
+ - Deep links build the project's `{routeEnum}` case and hand it to the router's deep-link entry.
89
+
90
+ If `mode` is absent or `native`, use the native-first section above. Resolve types via `tools/resource-utility` / repo grep only when the config points at real files.
91
+
92
+ ---
93
+
94
+ ## Integration with Phase 3D
95
+
96
+ When Phase 3D detects a navigation affordance, record it in `04b_component_architecture.md` and classify it as an **Output**, not a route:
97
+
98
+ ```markdown
99
+ ## Navigation
100
+ | Affordance | Emits | Caller maps to |
101
+ |---|---|---|
102
+ | Back button (header) | `.back` closure / dismiss | pop current stack |
103
+ | Row chevron ("Detaylar") | `.selected(id)` | push detail route |
104
+ | Tab strip | `selection: Binding<Tab>` | caller-owned tab state |
105
+ | Deep-link landing | entry `route`/`tab` param | parser sets caller state |
106
+ ```
107
+
108
+ Property classification addition:
109
+
110
+ | Classification | When | Swift Pattern |
111
+ |---|---|---|
112
+ | Navigation output | Component has a push/select/back affordance | `output: (Output) -> Void` / `onSelect: (ID) -> Void` closure on the View |
113
+ | Tab/selection binding | Component switches top-level content | `selection: Binding<SomeTab>` |
114
+ | Deep-link entry | Component is a URL landing surface | plain entry `route`/`tab` param on the View/Configuration |
115
+
116
+ The affordance's **label/icon copy is static Configuration content** (localized), exactly like other copy - only the *intent* is an Output closure.
117
+
118
+ ## Integration with Phase 4B
119
+
120
+ - Never hardcode `NavigationLink(destination: ConcreteScreen())` inside a reusable component - emit the Output.
121
+ - Prefer the system back button; a custom back affordance calls `@Environment(\.dismiss)` or emits `.back`.
122
+ - Bind tab/selection via the caller-owned binding; never a global.
123
+ - If `ui.navigationSystem.mode == custom`, wrap in `{sceneType}` and route via `{router}`/`{routeEnum}` per config.
124
+ - Verify by building + running on the simulator (`reference/build-and-test` / `figma-to-component` Phase 5): the transition happens, the back button appears only after a push, deep links land on the right surface.
125
+
126
+ ---
127
+
128
+ ## When NOT to Use Navigation Integration
129
+ - In-place filters/segmented state that never changes screen → `Binding<Enum>`.
130
+ - Form submission → `/figma-form-integration` (the *result* may navigate; the control is a form action).
131
+ - Toast / loading / alert / data modal → `/figma-overlays`. Bottom-sheet surface → `/figma-bottom-sheets`.
132
+
133
+ ## Checklist - Before Leaving the Skill
134
+ ```
135
+ [ ] Component emits a typed Output for every navigation affordance - no hardcoded routes
136
+ [ ] Back uses system back / dismiss / .back output; renders only when poppable
137
+ [ ] Tab/selection is a caller-owned binding, not a global
138
+ [ ] Deep-link entry is plain state the caller's parser can set (component never parses URLs)
139
+ [ ] Default is native SwiftUI (NavigationStack/navigationDestination/TabView/.sheet(item:))
140
+ [ ] Custom system used ONLY when figma-config ui.navigationSystem.mode == custom (names from config)
141
+ [ ] Labels/icons are localized Configuration copy; only intent is a closure
142
+ ```