@mmerterden/multi-agent-pipeline 10.9.0 → 10.12.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 (43) hide show
  1. package/CHANGELOG.md +143 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/pipeline/commands/multi-agent/channels.md +42 -6
  5. package/pipeline/commands/multi-agent/finish.md +15 -6
  6. package/pipeline/commands/multi-agent/generate-bug.md +33 -0
  7. package/pipeline/commands/multi-agent/generate-task.md +33 -0
  8. package/pipeline/commands/multi-agent/help.md +12 -0
  9. package/pipeline/commands/multi-agent/manual-test.md +9 -2
  10. package/pipeline/commands/multi-agent/refs/channels/pr.md +1 -1
  11. package/pipeline/commands/multi-agent/refs/cross-cli-contract.md +7 -6
  12. package/pipeline/commands/multi-agent/refs/generate-issue.md +197 -0
  13. package/pipeline/commands/multi-agent/refs/keychain.md +12 -0
  14. package/pipeline/commands/multi-agent/refs/phases/phase-0-init.md +31 -2
  15. package/pipeline/commands/multi-agent/refs/phases/phase-3-dev.md +2 -0
  16. package/pipeline/commands/multi-agent/refs/phases/phase-4-review.md +2 -0
  17. package/pipeline/commands/multi-agent/refs/phases/phase-5-test.md +6 -1
  18. package/pipeline/commands/multi-agent/refs/phases/phase-7-report.md +5 -1
  19. package/pipeline/commands/multi-agent/refs/progress-contract.md +1 -0
  20. package/pipeline/commands/multi-agent/refs/tracker-contract.md +56 -13
  21. package/pipeline/commands/multi-agent/resume.md +6 -2
  22. package/pipeline/commands/multi-agent/sync.md +5 -5
  23. package/pipeline/commands/multi-agent.md +4 -0
  24. package/pipeline/lib/figma-mcp-refresh.sh +79 -0
  25. package/pipeline/preferences-template.json +2 -1
  26. package/pipeline/schemas/prefs.schema.json +11 -0
  27. package/pipeline/scripts/fixtures/install-layout.tsv +6 -6
  28. package/pipeline/scripts/phase-tracker.sh +134 -22
  29. package/pipeline/scripts/smoke-channels-approval-gate.sh +60 -0
  30. package/pipeline/scripts/smoke-generate-issue.sh +114 -0
  31. package/pipeline/scripts/smoke-phase-tracker.sh +69 -0
  32. package/pipeline/scripts/smoke-progress-contract.sh +9 -0
  33. package/pipeline/scripts/smoke-tasklist-ordering.sh +1 -0
  34. package/pipeline/scripts/smoke-token-preflight.sh +82 -0
  35. package/pipeline/scripts/smoke-tracker-contract.sh +62 -0
  36. package/pipeline/skills/.skills-index.json +22 -4
  37. package/pipeline/skills/shared/core/multi-agent/SKILL.md +2 -1
  38. package/pipeline/skills/shared/core/multi-agent-channels/SKILL.md +4 -2
  39. package/pipeline/skills/shared/core/multi-agent-generate-bug/SKILL.md +39 -0
  40. package/pipeline/skills/shared/core/multi-agent-generate-task/SKILL.md +39 -0
  41. package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +4 -0
  42. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +5 -5
  43. package/pipeline/skills/skills-index.md +5 -3
@@ -0,0 +1,197 @@
1
+ # Generate Issue - Shared Flow (generate-task / generate-bug)
2
+
3
+ > **TLDR** - Shared 11-step flow for `/multi-agent:generate-task` and `/multi-agent:generate-bug`. Mines the target project's existing issues to learn team conventions, detects the active sprint, drafts a standards-compliant issue, asks the user about every genuinely unknown field, renders a full preview, and creates the Jira issue only after explicit approval. Creates exactly one Jira issue per run - no branches, no commits, no worktrees.
4
+
5
+ Consumed by `generate-task.md` and `generate-bug.md` via their profile blocks (`ISSUE_TYPE`, default description sections, JQL sample filter). This ref is never invoked directly.
6
+
7
+ ## Hard rules (must not regress)
8
+
9
+ - **Approval gate is absolute.** Step [8/11] (full preview + explicit approval) runs on every invocation. There is no autopilot variant of these commands and no preference that skips the gate. `Cancel` means nothing was created.
10
+ - **Never invent content.** Anything the user did not supply and mining could not derive stays an open question (step [7/11]) or an explicitly empty section - never fabricated repro steps, environments, or acceptance criteria.
11
+ - **Issue content language follows `prefs.global.outputLanguage`.** Summary and description are written in the user's output language (intentional exception to the "external payloads stay English" default - the issue is authored FOR the user's team). Code identifiers, file paths, and URLs stay verbatim. AskUserQuestion `question`/`description` also follow `outputLanguage`; `label`/`header` stay English.
12
+ - **UTF-8 verbatim POST path** (same as `refs/channels/jira.md`): description body goes to a file, `jq -n --rawfile` builds the payload, `curl --data-binary @file` ships it. Never round-trip through `unicode_escape`/`latin-1`, never hand-roll a re-encoding helper. Jira wiki markup (`h3.` headings, `[text|url]` links), real newlines, no HTML entities.
13
+ - **Humanizer pass** on the description body after composition, before the preview render.
14
+ - **Read-only until approval.** Steps 1-8 perform only GET requests. The first write of any kind is the `POST /rest/api/2/issue` in step [9/11].
15
+
16
+ ## Flow
17
+
18
+ ### [1/11] Pick account (`_account-picker`)
19
+
20
+ ```bash
21
+ ACCOUNTS=$(~/.claude/lib/account-resolver.sh --providers jira)
22
+ TOKEN=$(~/.claude/lib/credential-store.sh get "$ACCOUNT_JIRA_TOKEN_KEY")
23
+ ```
24
+ - Single account → auto-select silently (picker-contract behavior); multiple → AskUserQuestion.
25
+ - Output: `ACCOUNT_JIRA_TOKEN_KEY`, `ACCOUNT_JIRA_HOST`.
26
+ - Token missing/expired → Token Save Flow from `setup.md` inline; user skips → abort, nothing created.
27
+
28
+ ### [2/11] Resolve project key
29
+
30
+ Fallback chain (same as `refs/issue-jira-triad.md` Create path):
31
+ 1. `figmaConfig.jira.projectKey` (per-project config, when running inside a configured repo)
32
+ 2. `prefs.global.defaultJiraKey`
33
+ 3. Fetch and ask:
34
+ ```bash
35
+ curl -s -H "Authorization: Bearer $TOKEN" \
36
+ "https://$ACCOUNT_JIRA_HOST/rest/api/2/project" \
37
+ | python3 -c 'import json,sys; [print(p["key"]+"\t"+p["name"]) for p in json.load(sys.stdin)]'
38
+ ```
39
+ AskUserQuestion (single-select) → cache the choice to `prefs.global.defaultJiraKey`.
40
+ - Output: `PROJECT_KEY`.
41
+
42
+ ### [3/11] Parse input
43
+
44
+ - Any `figma.com` URL in `$ARGUMENTS` → `FIGMA_URL`. Node-id normalization: `-` → `:`. Branch URLs use `branchKey` as effective `fileKey` (see `rules/figma-pipeline.md` URL parsing).
45
+ - Everything else → `FREE_TEXT`.
46
+ - Both empty → AskUserQuestion (free-text, in `outputLanguage`): "Describe the {task|bug} you want to open."
47
+ - `FIGMA_URL` is always optional. Never ask for one; only use it when given.
48
+
49
+ ### [4/11] Convention mining + field discovery + sprint detection (read-only)
50
+
51
+ Three GET groups. Run them before drafting anything.
52
+
53
+ **4a. Sample recent same-type issues:**
54
+ ```bash
55
+ JQL="project = ${PROJECT_KEY} AND issuetype = ${ISSUE_TYPE} ORDER BY created DESC"
56
+ curl -s -H "Authorization: Bearer $TOKEN" \
57
+ "https://$ACCOUNT_JIRA_HOST/rest/api/2/search?jql=$(printf '%s' "$JQL" | python3 -c 'import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read()))')&fields=summary,description,labels,components,priority,fixVersions&maxResults=30"
58
+ ```
59
+ Extract from the sample (thresholds are guidance, not hard gates):
60
+ - **Summary prefix pattern**: leading `[...]` or `AREA:` token frequency. Appears in >= 40% of samples → adopt as suggested prefix; top 2-3 prefixes become AskUserQuestion options in step 7 when ambiguous.
61
+ - **Description heading set**: lines matching `h[1-4]\.` or `*bold*` header style, frequency-ranked. A heading set shared by >= 50% of samples overrides the profile's default sections - the team template wins.
62
+ - **Top labels / components**: top 5 each by frequency → option lists for step 7.
63
+ - **Priority norm**: modal priority for this issuetype → pre-filled default, editable in preview.
64
+ - **Epic usage rate**: share of sampled issues carrying an epic link → decides whether step 7 asks for an epic.
65
+
66
+ **4b. Field discovery (createmeta):**
67
+ ```bash
68
+ curl -s -H "Authorization: Bearer $TOKEN" \
69
+ "https://$ACCOUNT_JIRA_HOST/rest/api/2/issue/createmeta?projectKeys=${PROJECT_KEY}&issuetypeNames=${ISSUE_TYPE}&expand=projects.issuetypes.fields"
70
+ ```
71
+ - 404 or empty `projects[]` (newer Jira DC deprecates the classic form) → fall back to the paged endpoint: `GET /rest/api/2/issue/createmeta/{PROJECT_KEY}/issuetypes` then `GET /rest/api/2/issue/createmeta/{PROJECT_KEY}/issuetypes/{issuetypeId}`.
72
+ - Every field with `required: true` beyond project/issuetype/summary/description/reporter → step 7 question (options from `allowedValues` when present, free-text fallback).
73
+ - Generic custom-field detection by `schema.custom` id - never assume field ids:
74
+ - Sprint: `com.pyxis.greenhopper.jira:gh-sprint`
75
+ - Epic Link: `com.pyxis.greenhopper.jira:gh-epic-link`
76
+ - Team-like fields: case-insensitive name match on `Team`
77
+ - 403 → warn "cannot read project field metadata", continue with profile defaults; required fields surface reactively via the 400 path in step 9.
78
+
79
+ **4c. Board + active sprint (Agile REST):**
80
+ ```bash
81
+ curl -s -H "Authorization: Bearer $TOKEN" \
82
+ "https://$ACCOUNT_JIRA_HOST/rest/agile/1.0/board?projectKeyOrId=${PROJECT_KEY}"
83
+ curl -s -H "Authorization: Bearer $TOKEN" \
84
+ "https://$ACCOUNT_JIRA_HOST/rest/agile/1.0/board/${BOARD_ID}/sprint?state=active"
85
+ ```
86
+ - Multiple boards → AskUserQuestion (board name + type). One board → silent. Zero boards or Agile API 404 → the sprint option is silently omitted; the preview notes "backlog only (no board found)".
87
+ - Active sprint found → remember `SPRINT_ID` + name for the step 7 placement choice. None → backlog only; if a `state=future` sprint exists, mention it in the preview as information.
88
+
89
+ ### [5/11] Figma context (only when `FIGMA_URL` is set)
90
+
91
+ Follow the 3-tier chain from `rules/figma-pipeline.md`: Tier 1 MCP (`get_design_context` / `get_screenshot`, one re-auth retry) → Tier 2 REST (`keychainMapping.figma_pat`) → Tier 3 ask the user for a screenshot or proceed link-only.
92
+
93
+ > This command is standalone (same class as `/multi-agent:analysis`), so Figma MCP use here is allowed - it is not a dev-phase violation under the "No MCP outside analysis phase" rule.
94
+
95
+ - Output: frame name, node id, optional screenshot PNG in `/tmp/generate-issue-$$-figma.png` for the step 10 attachment opt-in.
96
+ - Any tier failure degrades gracefully; Figma never blocks issue creation. Link-only is always acceptable.
97
+
98
+ ### [6/11] Compose draft
99
+
100
+ Write summary + description in `outputLanguage`:
101
+ - **Summary**: `{minedPrefix} {concise title}`, truncated to 255 chars.
102
+ - **Description**: mined heading set from 4a (or the profile's default sections when mining found no dominant template), in Jira wiki markup (`h3.` headings).
103
+ - Task profile fills: Goal, Scope, Acceptance Criteria, Notes from `FREE_TEXT`.
104
+ - Bug profile fills: Steps to Reproduce, Expected Result, Actual Result, Environment, Notes from `FREE_TEXT`.
105
+ - Missing information stays an open question for step 7 - never invented.
106
+ - `FIGMA_URL` set → add `h3. Design Reference` with `[Figma|{FIGMA_URL}]` + frame name + node id.
107
+ - Run the humanizer skill on the description body.
108
+ - Write the final body to `/tmp/generate-issue-$$.txt` (UTF-8, real newlines) for the `--rawfile` POST.
109
+
110
+ ### [7/11] Clarifying questions (only genuinely unknown fields)
111
+
112
+ Batched AskUserQuestion(s), each with a "Skip / leave unset" option where Jira allows it:
113
+ - **Component**: mined top components + "none" - only when the project uses components.
114
+ - **Epic link**: only when 4a epic usage rate is high. Options from `project = {KEY} AND issuetype = Epic AND statusCategory != Done`.
115
+ - **Priority**: only when the mined norm is ambiguous; otherwise pre-fill the norm and let the preview edit change it.
116
+ - **Labels**: mined top labels, multiSelect.
117
+ - **Assignee**: "me / unassigned / someone else".
118
+ - **Sprint placement**: "Active sprint: {name}" vs "Backlog" - only when an active sprint exists.
119
+ - **Required custom fields** from 4b not yet resolved (allowedValues as options).
120
+ - **Bug profile**: Steps to Reproduce or Environment underivable from `FREE_TEXT` → ask here.
121
+
122
+ Rule: never ask about anything already answerable from mining or the input. Regardless of how complete the draft looks, step 8 always runs.
123
+
124
+ ### [8/11] Full preview + approval gate (never skipped)
125
+
126
+ Render the complete issue in chat: project, issuetype, summary, the full description body, priority, labels, components, epic, assignee, sprint/backlog placement, attachment plan (Figma screenshot upload - default off, opt-in).
127
+
128
+ Then AskUserQuestion (`question` in `outputLanguage`, e.g. tr: "Bu sekilde olusturuyorum, onayliyor musun?"):
129
+
130
+ | Option | Behavior |
131
+ |---|---|
132
+ | `Approve` | Proceed to step 9 |
133
+ | `Edit` | Free-text "what should change?" → apply → re-render the FULL preview → re-ask. Loop, no iteration cap |
134
+ | `Cancel` | Stop. Nothing was created; say so explicitly |
135
+
136
+ This gate has no bypass. No flag, mode, or preference suppresses it.
137
+
138
+ ### [9/11] Create
139
+
140
+ ```bash
141
+ jq -n --rawfile desc /tmp/generate-issue-$$.txt \
142
+ --arg key "$PROJECT_KEY" --arg type "$ISSUE_TYPE" --arg summary "$SUMMARY" \
143
+ '{fields: {project: {key: $key}, issuetype: {name: $type}, summary: $summary, description: $desc}}' \
144
+ > /tmp/generate-issue-$$-payload.json
145
+ # merge approved optional fields (priority / labels / components / epic / assignee / required customs) into .fields with jq before POST
146
+ curl -sS -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
147
+ --data-binary @/tmp/generate-issue-$$-payload.json \
148
+ "https://$ACCOUNT_JIRA_HOST/rest/api/2/issue"
149
+ ```
150
+ - Parse `key` from the response → `NEW_KEY`.
151
+ - 400 with field errors → show Jira's per-field error verbatim, return to step 7 for exactly those fields, then re-run step 8 (full preview again).
152
+
153
+ ### [10/11] Post-create steps
154
+
155
+ Each step is failure-isolated: on error, warn + continue - the issue already exists.
156
+
157
+ 1. **Sprint** (when chosen in step 7/8):
158
+ ```bash
159
+ jq -n --arg key "$NEW_KEY" '{issues: [$key]}' > /tmp/generate-issue-$$-sprint.json
160
+ curl -sS -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
161
+ --data-binary @/tmp/generate-issue-$$-sprint.json \
162
+ "https://$ACCOUNT_JIRA_HOST/rest/agile/1.0/sprint/${SPRINT_ID}/issue"
163
+ ```
164
+ Post-create move instead of a Sprint custom field in the create payload: field-id-agnostic, works on every Jira Software instance.
165
+ 2. **Figma remote link** (when `FIGMA_URL` set):
166
+ ```bash
167
+ curl -sS -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
168
+ -d '{"object":{"url":"'"$FIGMA_URL"'","title":"Figma design"}}' \
169
+ "https://$ACCOUNT_JIRA_HOST/rest/api/2/issue/${NEW_KEY}/remotelink"
170
+ ```
171
+ 3. **Screenshot attachment** (only when opted in during preview):
172
+ ```bash
173
+ curl -sS -X POST -H "Authorization: Bearer $TOKEN" -H "X-Atlassian-Token: no-check" \
174
+ -F "file=@/tmp/generate-issue-$$-figma.png" \
175
+ "https://$ACCOUNT_JIRA_HOST/rest/api/2/issue/${NEW_KEY}/attachments"
176
+ ```
177
+
178
+ ### [11/11] Report
179
+
180
+ ```
181
+ Created {NEW_KEY}: https://{ACCOUNT_JIRA_HOST}/browse/{NEW_KEY}
182
+ ```
183
+ Plus one line per post-create step that ran (sprint placement, remote link, attachment) and any that were skipped with reason.
184
+
185
+ ## Error paths
186
+
187
+ | Condition | Behavior |
188
+ |---|---|
189
+ | Token missing/expired (401) | Token Save Flow from `setup.md` inline; user skips → abort with message, nothing created |
190
+ | Search 403 / createmeta denied | Warn "cannot read project conventions/fields"; continue with profile-default template; required fields asked reactively on 400 |
191
+ | Zero sampled issues (new project) | Skip mining, use profile defaults, note in preview "no prior issues to learn from" |
192
+ | Agile API 404 / no boards | Omit sprint choice, backlog implied, say so in preview |
193
+ | No active sprint | Offer backlog only; mention the next `state=future` sprint when one exists |
194
+ | Figma Tier 1 fails | Tier 2; Tier 2 fails → Tier 3 (ask screenshot or link-only). Never block issue creation on Figma |
195
+ | Create 400 (field errors) | Show per-field errors, loop back to step 7 for those fields, re-preview |
196
+ | Create 5xx / network | Show the error, offer one retry, then abort - keep `/tmp/generate-issue-$$.txt` and print its path so the draft survives |
197
+ | Post-create step failure | Warn + continue (issue exists); print a manual-fix hint |
@@ -21,6 +21,18 @@ When a token resolves but the service rejects it (401 / 403), do not silently sk
21
21
 
22
22
  The question asks a *choice*; the replacement value still flows through the clipboard, never chat. `smoke-no-token-prompt.sh` greps for value-prompts (`enter token`, `paste token`, `API key:`) - decision labels like `Regenerate` / `Skip and continue` do not trip it.
23
23
 
24
+ Expiry is also checked **proactively at init**: Phase 0 Step 0.7 (`refs/phases/phase-0-init.md`) probes every token the run will need with one cheap call each (cached via `global.serviceStatus`, TTL 300s) and runs this same decision at init instead of waiting for the first mid-run 401. The mid-run 401 path stays as the safety net.
25
+
26
+ #### Figma MCP token lifecycle (critical)
27
+
28
+ `figma_mcp` is the only OAuth token in the registry (`figu_` prefix, ~90-day expiry); everything else is a long-lived PAT. A dead MCP token silently degrades every Figma consumer to Tier 2/3, so its expiry is handled specially:
29
+
30
+ 1. **Refresh token**: the generation flow stores a refresh token under `<keychainMapping.figma_mcp>_Refresh`. `~/.claude/lib/figma-mcp-refresh.sh` performs a silent renewal (OAuth refresh grant; client credentials read from the `.figma-oauth.json` next to `prefs.global.tokenScripts.figma_mcp`). Exit 0 = renewed in place, no user interaction. This ALWAYS runs before any question is asked.
31
+ 2. **Generation script**: `prefs.global.tokenScripts.figma_mcp` may point to a user-owned script that produces a fresh MCP token via Dynamic Client Registration + browser OAuth and saves it to the Keychain. When silent renewal fails, the expired-token question offers **Regenerate now (script)** which runs it (interactive runs only - it opens a browser).
32
+ 3. **Fallbacks**: Token Save Flow (clipboard) or continue degraded on Tier 2 (REST PAT), per the Phase 0 Step 0.7 decision table.
33
+
34
+ `prefs.global.tokenScripts` maps service ids to user-owned generation scripts (`{"figma_mcp": "/path/to/start_mcp.sh"}`); paths are personal and live only in the local preferences file - never in synced command files. Script output is status lines only - token values never enter chat or logs.
35
+
24
36
  Reasons:
25
37
  - Asking for a token in chat leaks it into the conversation transcript and any audit log.
26
38
  - The user has already configured the keychain; missing/expired is a setup issue, not a per-run issue.
@@ -107,6 +107,35 @@ Run `bash pipeline/scripts/update-check.sh` (cached per `updateCheck.ttlHours`,
107
107
 
108
108
  Advisory - NEVER blocks. Must run BEFORE Step 6 (worktree creation) so an accepted update cannot mutate `~/.claude` under a mid-phase run.
109
109
 
110
+ #### Step 0.7 - Token expiry pre-flight (runs before Step 1 fetch)
111
+
112
+ Validate the tokens THIS run will need before any of them is used mid-phase. Scope is derived, not exhaustive:
113
+
114
+ | Run signal | Tokens to probe | Cheap probe (1 call each) |
115
+ |---|---|---|
116
+ | Input is Jira-id / Jira-url | `jira` | `GET /rest/api/2/myself` |
117
+ | Input is GitHub issue / repo#N | `github` | `gh auth status` (or `GET /user`) |
118
+ | Remote is Bitbucket | `bitbucket_token` | `GET /rest/api/1.0/application-properties` |
119
+ | `reportChannels.confluence` true | `confluence` | `GET /rest/api/user/current` |
120
+ | Task carries a Figma reference | `figma_mcp` (Tier 1), `figma` (Tier 2) | MCP `initialize` POST to `mcp.figma.com/mcp`; `GET api.figma.com/v1/me` |
121
+
122
+ Results are cached in `global.serviceStatus` (existing TTL contract, default 300s) - a probe that ran in the last 5 minutes is not repeated.
123
+
124
+ **Valid** → continue silently (one log line: `→ token pre-flight: N ok`).
125
+
126
+ **Figma MCP expired / rejected - CRITICAL path.** The `figma_mcp` OAuth token (`figu_`) expires on a schedule (90 days), unlike the PATs. Because a dead MCP token silently degrades every Figma consumer to Tier 2/3 and has cost rebuild rounds, an expired `figma_mcp` is never deferred to mid-run:
127
+
128
+ 1. **Silent renewal first**: run `~/.claude/lib/figma-mcp-refresh.sh` (refresh grant via `<key>_Refresh` + the `.figma-oauth.json` client credentials next to `prefs.global.tokenScripts.figma_mcp`). Exit 0 → re-probe, log `→ figma mcp token renewed silently`, continue. No question asked.
129
+ 2. **Renewal impossible/rejected** (exit 1/2) → AskUserQuestion at init (`question`/`description` in `outputLanguage`): "Figma MCP token expired - update it now?"
130
+ - **Regenerate now (script)** - shown only when `prefs.global.tokenScripts.figma_mcp` is set: run that script (browser OAuth flow), then re-probe and continue.
131
+ - **Save a new token** - Token Save Flow from `setup.md` (clipboard path).
132
+ - **Continue degraded** - proceed on Tier 2 (REST PAT) for this run; log the downgrade.
133
+ 3. **Autopilot**: silent renewal only; if it fails, log `→ figma mcp token expired (autopilot: continuing on tier 2)` and continue degraded - never prompt.
134
+
135
+ **Other tokens expired / rejected** → run the Expired-token decision from `refs/keychain.md` Rule 1 HERE at init (Regenerate / Use a different token / Skip and continue) instead of waiting for the first mid-run 401. A token that is structurally required for the input (e.g. the Jira PAT for a Jira-ID input) halts on skip, since Step 1 cannot fetch without it. Autopilot: skip-and-degrade semantics per Rule 1, halt only on structurally required tokens.
136
+
137
+ Probe failures caused by network (timeout, DNS) are NOT treated as expiry - log and continue; the mid-run 401 path still exists as the safety net.
138
+
110
139
  #### Step 1 - Parse Input
111
140
 
112
141
  **Branch from input**: If the user provided a branch name after the issue reference (space-separated), store as `baseBranch` and skip Step 3. Otherwise Step 3 asks interactively.
@@ -526,8 +555,8 @@ Log: `Phase 0 Step 7: taskType = {component|bugfix|feature|refactor|chore}`
526
555
  After each clarifier call:
527
556
 
528
557
  ```bash
529
- pipeline/scripts/log-metric.sh "$TASK_ID" 0 clarify.call \
530
- score=$SCORE questions=$Q stop_and_ask=$STOP autopilot_mode=$AP \
558
+ LOG_METRIC_FORWARD_TO_TRACKER=1 pipeline/scripts/log-metric.sh "$TASK_ID" 0 clarify.call \
559
+ model=haiku score=$SCORE questions=$Q stop_and_ask=$STOP autopilot_mode=$AP \
531
560
  duration_ms=$D tokens_in=$TI tokens_out=$TO
532
561
  ```
533
562
 
@@ -296,6 +296,8 @@ When `agent-state.json.onlyDevelop === true`, Phase 3 runs self-contained with *
296
296
 
297
297
  **Combinable with autopilot**: `--dev autopilot` = zero interaction. Init → Dev → auto-commit → auto-PR → Report.
298
298
 
299
+ **Tracker visibility during Opus dispatch**: on Claude Code the model switch to Opus happens via subagent dispatch, and the parent widget cannot move while an Agent call is in flight. Dispatch per task from the self-generated task list (never one monolithic call for the whole phase), set the pre-dispatch `activeForm` marker, and record tokens between chunks - full rules in `refs/tracker-contract.md` section "Delegated phases".
300
+
299
301
  ---
300
302
 
301
303
  #### v2.1.0+ Multi-Repo Mode
@@ -157,6 +157,8 @@ When `state.evidence.figma[]` is non-empty, the reviewer subagents MUST receive
157
157
 
158
158
  Visual-fidelity mismatches against the captured screenshot are BLOCKING findings, not nits:
159
159
 
160
+ - Canonical component usage: the Code Connect-mapped component is used verbatim - a sound-alike substitute, a forked copy, or ad-hoc inline UI where a mapping exists is blocking (Phase 3 "Design fidelity contract")
161
+ - Inter-component spacing: gaps, paddings, and alignment BETWEEN components match the design's measured values mapped to spacing tokens - invented numeric values are blocking
160
162
  - Avatar icon (presence, glyph, shape, position)
161
163
  - Field grouping (one rounded box vs two; separator vs gap)
162
164
  - Character counter visibility
@@ -95,7 +95,12 @@ Tier 1 / Tier 2 records print `screenshotUrl` from the captured evidence (Tier 2
95
95
  "ok" -> proceeds to Phase 6 (WIP commit will be replaced via git reset HEAD~1 + proper commit)
96
96
  "fix: ..." -> worktree is recreated, returns to Phase 3
97
97
  ```
98
- 5. Wait for user response
98
+ 5. Mark the tracker as waiting, then wait for the user response:
99
+ ```bash
100
+ bash $HOME/.claude/scripts/phase-tracker.sh now 5 "awaiting local test (user)"
101
+ bash $HOME/.claude/scripts/phase-tracker.sh render
102
+ ```
103
+ The waiting state persists in `tracker-state.json` across the handoff; `/multi-agent:finish` and `/multi-agent:manual-test` CONTINUE this state file and never re-init it (`refs/tracker-contract.md` "Continuation runs").
99
104
  6. If fix needed:
100
105
  - Branch already has WIP commit (from step 2) - changes are safe
101
106
  - Recreate worktree from branch: `git -C $PROJECT_ROOT worktree add {worktree-path} {branch}`
@@ -101,6 +101,8 @@ Tracker: `phase-tracker.sh sub 7 1 "Channels dispatch" completed` (or `failed` i
101
101
 
102
102
  Runs only when `state.tracker.kind === "github-issue"`. The Issue adapter is mandatory whenever the run was triggered from a GitHub issue - no exceptions, no "I'll just update the flags" shortcut. Full template + rules: `refs/channels/issue-comment.md`.
103
103
 
104
+ **Approval gate:** interactive runs show the composed comment body (and the Progress-flag diff intent) through the same preview + approval gate as channels Step 6 (`channels.md`) before posting - Approve & post / Edit / Cancel. Autopilot posts directly.
105
+
104
106
  Two paired actions, in order:
105
107
 
106
108
  ```bash
@@ -182,9 +184,11 @@ Skipped sections: when `planTodos.enabled` is false or no `plan.todos[]` was emi
182
184
  (embed `aggregate-metrics.mjs --since=<30 days ago> --json` output as collapsed JSON)
183
185
  ```
184
186
 
185
- **Telemetry emission** (mandatory): emit final event:
187
+ **Telemetry emission** (mandatory): forward the phase's own LLM spend (humanizer + report compose calls) to the tracker, then emit the final event:
186
188
 
187
189
  ```bash
190
+ LOG_METRIC_FORWARD_TO_TRACKER=1 pipeline/scripts/log-metric.sh "$TASK_ID" 7 report.compose \
191
+ model=$REPORT_MODEL tokens_in=$R_IN tokens_out=$R_OUT duration_ms=$R_DUR
188
192
  pipeline/scripts/log-metric.sh "$TASK_ID" 7 task.completed \
189
193
  phases=$PHASE_COUNT review_cycles=$CYCLES lang=$PROMPT_LANG \
190
194
  channels_pr=$PR_STATUS channels_jira=$JIRA_STATUS \
@@ -21,6 +21,7 @@ Today pipeline users see a phase banner (`→ Phase 3: Dev`) and then silence fo
21
21
  - **verb** - lowercase present participle or imperative, one word: `fetching`, `reading`, `running`, `writing`, `posting`, `waiting`, `dispatching`, `retrying`, `building`, `committing`, `checking`.
22
22
  - **object** - what is being acted on: file basename, API endpoint last segment, subagent name, command, smoke name.
23
23
  - **target** - optional, human-readable scope: repo name, ticket ID, URL host.
24
+ - **Live mirror to the phase tracker**: each `normal`-tier emitted line is also mirrored to the active phase per `refs/tracker-contract.md` "Active phase" (Claude Code `TaskUpdate activeForm`, other CLIs `phase-tracker.sh now`) - the authoritative throttling rules live there.
24
25
 
25
26
  ### Examples
26
27
 
@@ -58,8 +58,9 @@ Phases by mode:
58
58
  | Mode | Phases |
59
59
  |---|---|
60
60
  | Full pipeline | 0,1,2,3,4,5,6,7 |
61
- | `--dev` | 0,3,6,7 |
62
- | `--autopilot`, `--dev autopilot` | same as dev, with confirmations skipped |
61
+ | `--dev` | 0,3,5,6,7 |
62
+ | `--dev autopilot`, `--dev local`, `--dev local autopilot` | 0,3,6,7 (Phase 5 interactive test gate dropped) |
63
+ | `--autopilot` | same as full pipeline, with confirmations skipped |
63
64
 
64
65
  Register each phase:
65
66
 
@@ -118,9 +119,10 @@ Mode-specific phase sets:
118
119
  | Mode | TaskCreate set (in order) |
119
120
  |---|---|
120
121
  | Full pipeline (`/multi-agent`, `:autopilot`, `:local`, `:local-autopilot`) | 0 → 1 → 2 → 3 → 4 → 5 → 6 → 7 (all 8) |
121
- | `:dev`, `:dev-autopilot`, `:dev-local`, `:dev-local-autopilot` | 0 → 3 → 5 → 6 → 7 (5 phases - 1/2/4 omitted entirely) |
122
+ | `:dev` | 0 → 3 → 5 → 6 → 7 (5 phases - 1/2/4 omitted entirely) |
123
+ | `:dev-autopilot`, `:dev-local`, `:dev-local-autopilot` | 0 → 3 → 6 → 7 (4 phases - 1/2/4/5 omitted entirely; the autopilot/local variants drop the interactive Phase 5 test gate) |
122
124
 
123
- Note: in `--dev` modes the omitted phases (1, 2, 4) **do not get TaskCreate calls at all** - they're not part of the mode's phase set. The "[SKIPPED]" pattern above only applies if a normally-included phase is conditionally skipped at runtime.
125
+ Note: in `--dev` modes the omitted phases (1, 2, 4 - plus 5 in the autopilot/local variants) **do not get TaskCreate calls at all** - they're not part of the mode's phase set. The "[SKIPPED]" pattern above only applies if a normally-included phase is conditionally skipped at runtime. The authoritative per-mode set is the `for p in ...` init block in each mode's own entry doc; this table mirrors those blocks.
124
126
 
125
127
  **Enforcement**: `smoke-tasklist-ordering.sh` scans every mode entry point doc (`commands/multi-agent.md`, `commands/multi-agent/{autopilot,local,local-autopilot,dev,dev-autopilot,dev-local,dev-local-autopilot}.md` + Copilot mirrors) for the explicit "in phase-number order" rule. Inventory drift fails the smoke.
126
128
 
@@ -172,23 +174,40 @@ TaskUpdate({taskId: <dev_task>, activeForm: "Editing TopBarView.swift"})
172
174
  TaskUpdate({taskId: <dev_task>, activeForm: "Running xcodebuild test"})
173
175
  ```
174
176
 
175
- **Other CLIs** - `meta Now` key plus a render call:
177
+ **Other CLIs** - the dedicated `now` action (quiet: writes `meta.Now`, never renders; the card picks it up at the next boundary render/update):
176
178
  ```bash
177
- bash $HOME/.claude/scripts/phase-tracker.sh meta 3 Now "editing TopBarView.swift"
178
- bash $HOME/.claude/scripts/phase-tracker.sh render
179
+ bash $HOME/.claude/scripts/phase-tracker.sh now 3 "editing TopBarView.swift"
179
180
  ```
180
181
 
181
- The `Now` meta key shows up in the bordered card as `Now: editing TopBarView.swift`.
182
+ The `Now` value shows up in the bordered card as `Now: editing TopBarView.swift` (truncated to 60 chars by the script).
183
+
184
+ **Progress-line mirror (required).** Every `normal`-tier progress line from the progress contract's canonical "When to emit" set is ALSO mirrored to the active phase, so the tracker always answers "what is it doing right now":
185
+
186
+ - Claude Code: `TaskUpdate({activeForm: "<line text, arrow prefix stripped>"})`
187
+ - Other CLIs: `phase-tracker.sh now <N> "<line text, arrow prefix stripped>"`
188
+
189
+ Throttling rules: mirror only canonical-set lines (`verbose`-tier internals are not mirrored); at most one mirror per emitted line; skip the mirror when the text is identical to the last mirrored value; the mirror itself never triggers a render. Text is truncated to 60 chars.
190
+
191
+ ### Delegated phases - mirror limitation + chunked dispatch (required)
192
+
193
+ When a phase's work is delegated to a subagent (Phase 3 Dev on Opus in `--dev` modes, `figma-to-component` dispatch, Phase 1 explorers, Phase 4 reviewers), the visual channel freezes for the duration of the Agent call: the orchestrator is blocked while the call is in flight, so it cannot fire `TaskUpdate` / `now` / `tokens`, and a subagent cannot drive the parent session's TaskList (its own TaskCreate/TaskUpdate calls land on an invisible child list). The progress-line mirror above can therefore only fire while the orchestrator holds control. Rules:
194
+
195
+ 1. **Pre-dispatch marker.** Immediately before every Agent call, set the active-phase line to the delegation itself, so the frozen interval at least states what is running and on which model:
196
+ - Claude Code: `TaskUpdate({activeForm: "Dev subagent (opus): <task subject>"})`
197
+ - Other CLIs: `phase-tracker.sh now <N> "dev subagent (opus): <task subject>"`
198
+ 2. **Chunk long delegations.** A phase whose delegated work spans multiple tasks MUST NOT go out as one monolithic Agent call. Dispatch per task (the Phase 2 task graph, or in `--dev` mode the self-generated task list, is the natural chunk boundary) so the orchestrator regains control at each boundary and refreshes `activeForm`, `tokens`, and `now` between chunks. Single-task phases and inherently atomic dispatches (one reviewer, one explorer) are exempt.
199
+ 3. **Post-chunk accounting.** When each chunk returns, record its token estimate (`phase-tracker.sh tokens <N> <in> <out>`) before dispatching the next chunk - not accumulated once at phase end.
182
200
 
183
201
  ### 5. Token accounting - automatic (manual top-up optional)
184
202
 
185
203
  The native TaskList header automatically prints `↑Nk tokens · thought for Ns` - Claude Code uses session-level metrics. For per-phase token attribution:
186
204
 
187
205
  ```bash
188
- bash $HOME/.claude/scripts/phase-tracker.sh tokens <N> <input_count> <output_count>
206
+ bash $HOME/.claude/scripts/phase-tracker.sh tokens <N> <input_count> <output_count> [cached_count]
207
+ bash $HOME/.claude/scripts/phase-tracker.sh model <N> <model_name>
189
208
  ```
190
209
 
191
- Token counts are additive - multiple calls accumulate. Stored per phase in the state file; surfaced in `:log` reports.
210
+ Token counts are additive - multiple calls accumulate. `input_count` is FRESH input (cache-exclusive); the optional 4th arg is the prompt-cache-read count, priced at the discounted rate. `model` tags the phase so the card and the cost helper can price it. Stored per phase in the state file; surfaced in `:log` reports, on the bash card tile (`<elapsed> · <tok> tok · ~$<usd>`), and in the card footer (total USD + cached tokens).
192
211
 
193
212
  **MANDATORY: per-phase token narration on completion (v9.10.2).** The native
194
213
  TaskList widget cannot display per-phase tokens - it shows name, status, and
@@ -203,9 +222,22 @@ Phase <N> <name> tamamlandi - ~<in> giris / ~<out> cikis token (<model>, ~$<usd>
203
222
  Phase <N> <name> done - ~<in> in / ~<out> out tokens (<model>, ~$<usd>)
204
223
  ```
205
224
 
206
- USD comes from `cost-table.json` rates for the phase's model (same math as
207
- `render-agent-log-cost.sh`: floor to cents). Phases with zero recorded tokens
208
- print `(no LLM calls)` instead of fabricating numbers.
225
+ USD comes from `phase-tracker.sh cost <N>` - the single pricing implementation
226
+ (cost-table.json rates, cached tokens at the discounted rate, floored to
227
+ cents; prints `-` when the model is unknown). Do not re-implement the math
228
+ inline. Phases with zero recorded tokens print `(no LLM calls)` instead of
229
+ fabricating numbers.
230
+
231
+ **Completion tile suffix (Claude Code).** The native widget cannot show
232
+ per-phase tokens, but subject edits after creation are safe (tile order is
233
+ fixed at TaskCreate time). So on phase completion, in addition to the
234
+ narration line, append the spend to the tile subject:
235
+
236
+ ```text
237
+ TaskUpdate({taskId: <phase_task>, subject: "Phase 3: Dev · ~35k tok · ~$0.26"})
238
+ ```
239
+
240
+ Skip the suffix when the phase recorded zero tokens (subject stays clean).
209
241
 
210
242
  **Honesty note:** on Claude Code the orchestrator does not receive its own
211
243
  usage metering, so per-phase counts are content-size estimates (chars/4 for
@@ -259,11 +291,22 @@ When `/multi-agent:resume <task_id>` is called:
259
291
 
260
292
  The `tasklist_id` meta from the previous session is replaced with the new IDs during resume.
261
293
 
294
+ ## Continuation runs (finish / manual-test)
295
+
296
+ A pre-existing `tracker-state.json` for the task is never re-initialized. Rules for any command that continues an earlier run (`/multi-agent:finish`, `/multi-agent:manual-test`, resume):
297
+
298
+ 1. `init` runs ONLY when no state file exists for the task. Otherwise the existing file is kept - phase history (elapsed, tokens, model, meta) survives.
299
+ 2. The continuing command re-declares its phase set with `add` - `add` is idempotent, so existing phases keep their name, status, and token history; only genuinely new phases are appended. The card renders phases sorted by numeric id, so mixed sets stay in order.
300
+ 3. If Phase 5 was left `in_progress` with `Now: awaiting local test (user)`, the continuing command marks it `update 5 completed` + `meta 5 Result "local test done (user)"` before its own work starts (finish may re-open it with `update 5 in_progress` when its build+test gate runs; elapsed keeps the original `started_at`, which is acceptable).
301
+ 4. On Claude Code, rebuild the FULL TaskList from the state file (completed tiles included) in phase order before any `TaskUpdate`, refreshing every `tasklist_id` meta - exactly the Resume behaviour above.
302
+ 5. Print ONE line in `outputLanguage` summarizing the inherited history, e.g. `Continuing PROJ-12345: phases 0-3 finished earlier (12m, 38.4k tok, ~$0.74)` (USD via `cost total`), then `render`.
303
+
262
304
  ## Anti-patterns
263
305
 
264
306
  ❌ **TaskList only** → `:resume` will not work; no run history is preserved.
265
307
  ❌ **`phase-tracker.sh` only** in Claude Code → the user sees no widget, only bash stdout snapshots - the #1 source of "I see no phases" complaints.
266
308
  ❌ **Wiring `phase-tracker.sh render` to `statusLine` config** → it drops below the screen as a placeholder; the user complained, removed in v7.9.0.
309
+ ❌ **One monolithic subagent call for a whole multi-task phase** → the tile freezes for the entire phase (no `activeForm`, no `tokens`, no `now`) - the user sees a dead widget for 20+ minutes. Chunk per task; see "Delegated phases" above.
267
310
 
268
311
  ## Verification
269
312
 
@@ -31,6 +31,10 @@ Resume a paused or failed task from the last successful phase.
31
31
  - Phase 3 code → already in the worktree
32
32
  - Recent `git log --oneline -10` in the worktree grounds what was actually committed vs. claimed.
33
33
 
34
- 4. **Continue the pipeline** - start from the next phase (same pipeline as the main multi-agent command).
34
+ 4. **Rebuild the phase tiles** - load `tracker-state.json` for the task (never re-init it; `refs/tracker-contract.md` "Resume behaviour" + "Continuation runs"):
35
+ - Claude Code: `TaskCreate` every phase from the state file in phase order, then `TaskUpdate` each to its stored status, and replace every `tasklist_id` meta with the new IDs.
36
+ - Other CLIs: a single `bash $HOME/.claude/scripts/phase-tracker.sh render`.
35
37
 
36
- 5. **Log**: `🔄 Resumed {JIRA-KEY}-{id} from Phase {N}`
38
+ 5. **Continue the pipeline** - start from the next phase (same pipeline as the main multi-agent command).
39
+
40
+ 6. **Log**: `🔄 Resumed {JIRA-KEY}-{id} from Phase {N}`
@@ -58,7 +58,7 @@ Run every step automatically:
58
58
  Step 0: FIGMA_SYNC SKIP (deprecated - feedback_figma_source_deprecated)
59
59
  Step 1: PLATFORM Detect macOS / Linux / Windows (Git Bash / WSL); export PLATFORM env
60
60
  Step 1.5: DETECT Compare timestamps, find stale targets
61
- Step 2: COPILOT Claude Code -> Copilot CLI (instructions + 35 sub-command skills)
61
+ Step 2: COPILOT Claude Code -> Copilot CLI (instructions + 37 sub-command skills)
62
62
  Step 3: REPO Claude Code -> pipeline repo (genericized, personal data scrub, bash -n on all sh)
63
63
  Step 3c: PLUGINS pipeline shared/external -> multi-agent-plugins marketplace (rebuild knowledge/,
64
64
  bump changed plugins' patch version, commit + push the plugins repo)
@@ -277,14 +277,14 @@ This runs on the Claude <-> Copilot axis — the two CLIs the pipeline supports
277
277
  |-------------|-------------|
278
278
  | `~/.claude/commands/multi-agent/{cmd}.md` | `~/.copilot/skills/multi-agent-{cmd}/SKILL.md` |
279
279
 
280
- **35 commands are synced** (canonical inventory - must match `cross-cli-contract.md` section 1; drift = contract violation):
280
+ **37 commands are synced** (canonical inventory - must match `cross-cli-contract.md` section 1; drift = contract violation):
281
281
 
282
282
  ```
283
283
  analysis, analysis-resolve, autopilot, build-optimize, channels, delete, dev,
284
284
  dev-autopilot, dev-local, dev-local-autopilot, diff-explain, finish, garbage-collect,
285
- help, issue, jira, kill, language, local, local-autopilot, log, manual-test,
286
- prune-logs, purge, refactor, resume, review, scan, search, setup, stack, status,
287
- sync, test, update
285
+ generate-bug, generate-task, help, issue, jira, kill, language, local,
286
+ local-autopilot, log, manual-test, prune-logs, purge, refactor, resume, review,
287
+ scan, search, setup, stack, status, sync, test, update
288
288
  ```
289
289
 
290
290
  **NOT synced**: `refs/*` - lazy-load references, Claude Code specific
@@ -77,6 +77,8 @@ Lib scripts (`~/.claude/lib/`):
77
77
  | `analysis ["<feature>"]` | Standalone feature-spec analizi: Figma / Swagger / Confluence / Jira / repo girdileri sabit 7-bölümlük şablona dökülür, humanizer'dan geçer, Local/Confluence/Jira hedef(ler)ine post edilir. Worktree veya commit yok. Dev'e zincirleme yok. |
78
78
  | `build-optimize` | iOS-only Xcode build performance wrapper. Vendored `xcode-build-orchestrator`'a dispatch eder; benchmark + compilation / project / SPM analyzer'lar + recommend-first plan `.build-benchmark/optimization-plan.md`. Non-iOS stack'lerde fail-fast. |
79
79
  | `channels [PR-url\|#N\|Jira-url\|Jira-id] [--channels pr,jira,confluence,wiki] [--content normal,test,auto-diff,note] [--message "..."]` | Post task report to multi-select channels (PR description, Jira comment, Confluence page, Wiki pages) with multi-select content sources. Humanizer pass per-channel. Bitbucket PR updates use reviewer-preserving PUT. Phase 7 delegates to this command; also invocable post-hoc for fixes made outside the pipeline. No worktree. |
80
+ | `generate-task ["desc"] [figma-url]` | Create a standards-compliant Jira **Task**: mines the project's recent Tasks for conventions (summary format, template, labels, priority), detects the active sprint, asks about unknown fields, then full draft preview + explicit approval before create. No worktree, no commits. |
81
+ | `generate-bug ["desc"] [figma-url]` | Create a standards-compliant Jira **Bug**: same convention mining + active-sprint detection; free-text logs/devices map to Steps to Reproduce / Environment; full draft preview + explicit approval before create. No worktree, no commits. |
80
82
  | `test` or `test [args]` | UI Bug Hunter - screenshot + tap + analyze on booted simulator via MCP (read `$HOME/.claude/commands/sim-test.md`). `/multi-agent:test` also resolves here via the `commands/multi-agent/test.md` delegate. |
81
83
  | `manual-test [#id]` | Phase 5 standalone Manual Test - checks out the task branch, prints Xcode / SourceTree hints, waits for user verdict (`ok` / `fix: ...`). |
82
84
  | `stack [ios\|android\|backend\|mobile\|all]` | Swap skills for next conversation. No arg = show current stack. |
@@ -116,6 +118,8 @@ This command uses lazy loading for token efficiency. Read the relevant sub-file
116
118
  | `local-autopilot` | `$HOME/.claude/commands/multi-agent/local-autopilot.md` |
117
119
  | `dev-local` | `$HOME/.claude/commands/multi-agent/dev-local.md` |
118
120
  | `dev-local-autopilot` | `$HOME/.claude/commands/multi-agent/dev-local-autopilot.md` |
121
+ | `generate-task` | `$HOME/.claude/commands/multi-agent/generate-task.md` (loads `refs/generate-issue.md`) |
122
+ | `generate-bug` | `$HOME/.claude/commands/multi-agent/generate-bug.md` (loads `refs/generate-issue.md`) |
119
123
  | `stack` | `$HOME/.claude/commands/multi-agent/stack.md` |
120
124
  | `language` | Handled inline - set/show prompt language in preferences |
121
125
  | SwiftUI component task (iOS) | `$HOME/.claude/commands/multi-agent/refs/swiftui-guide.md` |
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env bash
2
+ # figma-mcp-refresh.sh - silent Figma MCP (figu_) token renewal via OAuth refresh grant.
3
+ #
4
+ # Used by the Phase 0 token pre-flight (refs/phases/phase-0-init.md Step 0.7) and the
5
+ # expired-token decision (refs/keychain.md). Never prints token values - status lines only.
6
+ #
7
+ # Flow:
8
+ # 1. Resolve the access-token Keychain name from prefs.global.keychainMapping.figma_mcp.
9
+ # 2. Read the refresh token from "<name>_Refresh" (written by the user's generation script).
10
+ # 3. Read client credentials from .figma-oauth.json next to prefs.global.tokenScripts.figma_mcp.
11
+ # 4. POST grant_type=refresh_token to the Figma token endpoint.
12
+ # 5. On success save the new access token (and rotated refresh token) back to the Keychain.
13
+ #
14
+ # Exit codes:
15
+ # 0 renewed (access token replaced in Keychain)
16
+ # 1 refresh grant rejected by Figma (refresh token dead - run the generation script)
17
+ # 2 prerequisites missing (no mapping / no refresh entry / no oauth config)
18
+
19
+ set -euo pipefail
20
+
21
+ PREFS_FILE="${PREFS_FILE:-$HOME/.claude/multi-agent-preferences.json}"
22
+ TOKEN_ENDPOINT="https://api.figma.com/v1/oauth/token"
23
+ ACCOUNT="${USER:-$(whoami)}"
24
+
25
+ say() { printf '%s\n' "$*"; }
26
+
27
+ [ -f "$PREFS_FILE" ] || { say "figma-mcp-refresh: prefs file missing ($PREFS_FILE)"; exit 2; }
28
+
29
+ KEY_NAME=$(jq -r '.global.keychainMapping.figma_mcp // empty' "$PREFS_FILE")
30
+ [ -n "$KEY_NAME" ] || { say "figma-mcp-refresh: keychainMapping.figma_mcp not set"; exit 2; }
31
+
32
+ REFRESH_KEY="${KEY_NAME}_Refresh"
33
+ REFRESH_TOKEN=$(security find-generic-password -s "$REFRESH_KEY" -w 2>/dev/null || true)
34
+ [ -n "$REFRESH_TOKEN" ] || { say "figma-mcp-refresh: no refresh token at '$REFRESH_KEY'"; exit 2; }
35
+
36
+ SCRIPT_PATH=$(jq -r '.global.tokenScripts.figma_mcp // empty' "$PREFS_FILE")
37
+ OAUTH_CONFIG=""
38
+ if [ -n "$SCRIPT_PATH" ]; then
39
+ OAUTH_CONFIG="$(dirname "$SCRIPT_PATH")/.figma-oauth.json"
40
+ fi
41
+ [ -n "$OAUTH_CONFIG" ] && [ -f "$OAUTH_CONFIG" ] || { say "figma-mcp-refresh: .figma-oauth.json not found (tokenScripts.figma_mcp unset or sibling config missing)"; exit 2; }
42
+
43
+ CLIENT_ID=$(jq -r '.client_id // empty' "$OAUTH_CONFIG")
44
+ CLIENT_SECRET=$(jq -r '.client_secret // empty' "$OAUTH_CONFIG")
45
+ [ -n "$CLIENT_ID" ] || { say "figma-mcp-refresh: client_id missing in .figma-oauth.json"; exit 2; }
46
+
47
+ CURL_ARGS=(
48
+ --data-urlencode "grant_type=refresh_token"
49
+ --data-urlencode "refresh_token=${REFRESH_TOKEN}"
50
+ --data-urlencode "client_id=${CLIENT_ID}"
51
+ )
52
+ [ -n "$CLIENT_SECRET" ] && CURL_ARGS+=( --data-urlencode "client_secret=${CLIENT_SECRET}" )
53
+
54
+ RESPONSE=$(curl -s --max-time 20 -X POST "$TOKEN_ENDPOINT" \
55
+ -H "Content-Type: application/x-www-form-urlencoded" \
56
+ "${CURL_ARGS[@]}")
57
+
58
+ NEW_ACCESS=$(printf '%s' "$RESPONSE" | jq -r '.access_token // empty' 2>/dev/null || true)
59
+ NEW_REFRESH=$(printf '%s' "$RESPONSE" | jq -r '.refresh_token // empty' 2>/dev/null || true)
60
+ EXPIRES_IN=$(printf '%s' "$RESPONSE" | jq -r '.expires_in // "?"' 2>/dev/null || true)
61
+
62
+ if [ -z "$NEW_ACCESS" ]; then
63
+ ERR=$(printf '%s' "$RESPONSE" | jq -r '.error // .message // "unknown"' 2>/dev/null || echo "unknown")
64
+ say "figma-mcp-refresh: refresh grant rejected ($ERR) - regenerate via tokenScripts.figma_mcp"
65
+ exit 1
66
+ fi
67
+
68
+ # Save new access token first, then the rotated refresh token - order matters so a
69
+ # mid-write failure never leaves us with a lost refresh token AND a stale access token.
70
+ security delete-generic-password -s "$KEY_NAME" -a "$ACCOUNT" >/dev/null 2>&1 || true
71
+ security add-generic-password -s "$KEY_NAME" -a "$ACCOUNT" -w "$NEW_ACCESS" -T "" -U
72
+
73
+ if [ -n "$NEW_REFRESH" ]; then
74
+ security delete-generic-password -s "$REFRESH_KEY" -a "$ACCOUNT" >/dev/null 2>&1 || true
75
+ security add-generic-password -s "$REFRESH_KEY" -a "$ACCOUNT" -w "$NEW_REFRESH" -T "" -U
76
+ fi
77
+
78
+ say "figma-mcp-refresh: renewed '$KEY_NAME' (expires_in=${EXPIRES_IN}s, refresh $( [ -n "$NEW_REFRESH" ] && echo rotated || echo unchanged ))"
79
+ exit 0
@@ -14,6 +14,7 @@
14
14
  "firebase": null,
15
15
  "jenkins": null
16
16
  },
17
+ "tokenScripts": {},
17
18
  "platformIdentityRouting": {},
18
19
  "recentProjects": [],
19
20
  "recentBranches": {},
@@ -153,4 +154,4 @@
153
154
  "overrides": {}
154
155
  }
155
156
  }
156
- }
157
+ }