@nforma.ai/nforma 0.2.1

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 (215) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +1024 -0
  3. package/agents/qgsd-codebase-mapper.md +764 -0
  4. package/agents/qgsd-debugger.md +1201 -0
  5. package/agents/qgsd-executor.md +472 -0
  6. package/agents/qgsd-integration-checker.md +443 -0
  7. package/agents/qgsd-phase-researcher.md +502 -0
  8. package/agents/qgsd-plan-checker.md +643 -0
  9. package/agents/qgsd-planner.md +1182 -0
  10. package/agents/qgsd-project-researcher.md +621 -0
  11. package/agents/qgsd-quorum-orchestrator.md +628 -0
  12. package/agents/qgsd-quorum-slot-worker.md +41 -0
  13. package/agents/qgsd-quorum-synthesizer.md +133 -0
  14. package/agents/qgsd-quorum-test-worker.md +37 -0
  15. package/agents/qgsd-quorum-worker.md +161 -0
  16. package/agents/qgsd-research-synthesizer.md +239 -0
  17. package/agents/qgsd-roadmapper.md +660 -0
  18. package/agents/qgsd-verifier.md +628 -0
  19. package/bin/accept-debug-invariant.cjs +165 -0
  20. package/bin/account-manager.cjs +719 -0
  21. package/bin/aggregate-requirements.cjs +466 -0
  22. package/bin/analyze-assumptions.cjs +757 -0
  23. package/bin/analyze-state-space.cjs +921 -0
  24. package/bin/attribute-trace-divergence.cjs +150 -0
  25. package/bin/auth-drivers/gh-cli.cjs +93 -0
  26. package/bin/auth-drivers/index.cjs +46 -0
  27. package/bin/auth-drivers/pool.cjs +67 -0
  28. package/bin/auth-drivers/simple.cjs +95 -0
  29. package/bin/autoClosePtoF.cjs +110 -0
  30. package/bin/blessed-terminal.cjs +350 -0
  31. package/bin/build-phase-index.cjs +472 -0
  32. package/bin/call-quorum-slot.cjs +541 -0
  33. package/bin/ccr-secure-config.cjs +99 -0
  34. package/bin/ccr-secure-start.cjs +83 -0
  35. package/bin/check-bundled-sdks.cjs +177 -0
  36. package/bin/check-coverage-guard.cjs +112 -0
  37. package/bin/check-liveness-fairness.cjs +95 -0
  38. package/bin/check-mcp-health.cjs +123 -0
  39. package/bin/check-provider-health.cjs +395 -0
  40. package/bin/check-results-exit.cjs +24 -0
  41. package/bin/check-spec-sync.cjs +360 -0
  42. package/bin/check-trace-redaction.cjs +271 -0
  43. package/bin/check-trace-schema-drift.cjs +99 -0
  44. package/bin/compareDrift.cjs +21 -0
  45. package/bin/conformance-schema.cjs +12 -0
  46. package/bin/count-scenarios.cjs +420 -0
  47. package/bin/debt-dedup.cjs +144 -0
  48. package/bin/debt-ledger.cjs +61 -0
  49. package/bin/debt-retention.cjs +76 -0
  50. package/bin/debt-state-machine.cjs +80 -0
  51. package/bin/detect-coverage-gaps.cjs +204 -0
  52. package/bin/detect-project-intent.cjs +362 -0
  53. package/bin/export-prism-constants.cjs +164 -0
  54. package/bin/extract-annotations.cjs +633 -0
  55. package/bin/extractFormalExpected.cjs +104 -0
  56. package/bin/fingerprint-drift.cjs +24 -0
  57. package/bin/fingerprint-issue.cjs +46 -0
  58. package/bin/formal-core.cjs +519 -0
  59. package/bin/formal-ref-linker.cjs +141 -0
  60. package/bin/formal-test-sync.cjs +788 -0
  61. package/bin/generate-formal-specs.cjs +588 -0
  62. package/bin/generate-petri-net.cjs +397 -0
  63. package/bin/generate-phase-spec.cjs +249 -0
  64. package/bin/generate-proposed-changes.cjs +194 -0
  65. package/bin/generate-tla-cfg.cjs +122 -0
  66. package/bin/generate-traceability-matrix.cjs +701 -0
  67. package/bin/generate-triage-bundle.cjs +300 -0
  68. package/bin/gh-account-rotate.cjs +34 -0
  69. package/bin/initialize-model-registry.cjs +105 -0
  70. package/bin/install-formal-tools.cjs +382 -0
  71. package/bin/install.js +2424 -0
  72. package/bin/isNumericThreshold.cjs +34 -0
  73. package/bin/issue-classifier.cjs +151 -0
  74. package/bin/levenshtein.cjs +74 -0
  75. package/bin/lint-formal-models.cjs +580 -0
  76. package/bin/load-baseline-requirements.cjs +275 -0
  77. package/bin/manage-agents-core.cjs +815 -0
  78. package/bin/migrate-formal-dir.cjs +172 -0
  79. package/bin/migrate-planning.cjs +206 -0
  80. package/bin/migrate-to-slots.cjs +255 -0
  81. package/bin/nForma.cjs +2726 -0
  82. package/bin/observe-config.cjs +353 -0
  83. package/bin/observe-debt-writer.cjs +140 -0
  84. package/bin/observe-handler-grafana.cjs +128 -0
  85. package/bin/observe-handler-internal.cjs +301 -0
  86. package/bin/observe-handler-logstash.cjs +153 -0
  87. package/bin/observe-handler-prometheus.cjs +185 -0
  88. package/bin/observe-handlers.cjs +436 -0
  89. package/bin/observe-registry.cjs +131 -0
  90. package/bin/observe-render.cjs +168 -0
  91. package/bin/planning-paths.cjs +167 -0
  92. package/bin/polyrepo.cjs +560 -0
  93. package/bin/prism-priority.cjs +153 -0
  94. package/bin/probe-quorum-slots.cjs +167 -0
  95. package/bin/promote-model.cjs +225 -0
  96. package/bin/propose-debug-invariants.cjs +165 -0
  97. package/bin/providers.json +392 -0
  98. package/bin/pty-proxy.py +129 -0
  99. package/bin/qgsd-solve.cjs +2477 -0
  100. package/bin/quorum-consensus-gate.cjs +238 -0
  101. package/bin/quorum-formal-context.cjs +183 -0
  102. package/bin/quorum-slot-dispatch.cjs +934 -0
  103. package/bin/read-policy.cjs +60 -0
  104. package/bin/requirement-map.cjs +63 -0
  105. package/bin/requirements-core.cjs +247 -0
  106. package/bin/resolve-cli.cjs +101 -0
  107. package/bin/review-mcp-logs.cjs +294 -0
  108. package/bin/run-account-manager-tlc.cjs +188 -0
  109. package/bin/run-account-pool-alloy.cjs +158 -0
  110. package/bin/run-alloy.cjs +153 -0
  111. package/bin/run-audit-alloy.cjs +187 -0
  112. package/bin/run-breaker-tlc.cjs +181 -0
  113. package/bin/run-formal-check.cjs +395 -0
  114. package/bin/run-formal-verify.cjs +701 -0
  115. package/bin/run-installer-alloy.cjs +188 -0
  116. package/bin/run-oauth-rotation-prism.cjs +132 -0
  117. package/bin/run-oscillation-tlc.cjs +202 -0
  118. package/bin/run-phase-tlc.cjs +228 -0
  119. package/bin/run-prism.cjs +446 -0
  120. package/bin/run-protocol-tlc.cjs +201 -0
  121. package/bin/run-quorum-composition-alloy.cjs +155 -0
  122. package/bin/run-sensitivity-sweep.cjs +231 -0
  123. package/bin/run-stop-hook-tlc.cjs +188 -0
  124. package/bin/run-tlc.cjs +467 -0
  125. package/bin/run-transcript-alloy.cjs +173 -0
  126. package/bin/run-uppaal.cjs +264 -0
  127. package/bin/secrets.cjs +134 -0
  128. package/bin/sensitivity-report.cjs +219 -0
  129. package/bin/sensitivity-sweep-feedback.cjs +194 -0
  130. package/bin/set-secret.cjs +29 -0
  131. package/bin/setup-telemetry-cron.sh +36 -0
  132. package/bin/sweepPtoF.cjs +63 -0
  133. package/bin/sync-baseline-requirements.cjs +290 -0
  134. package/bin/task-envelope.cjs +360 -0
  135. package/bin/telemetry-collector.cjs +229 -0
  136. package/bin/unified-mcp-server.mjs +735 -0
  137. package/bin/update-agents.cjs +369 -0
  138. package/bin/update-scoreboard.cjs +1134 -0
  139. package/bin/validate-debt-entry.cjs +207 -0
  140. package/bin/validate-invariant.cjs +419 -0
  141. package/bin/validate-memory.cjs +389 -0
  142. package/bin/validate-requirements-haiku.cjs +435 -0
  143. package/bin/validate-traces.cjs +438 -0
  144. package/bin/verify-formal-results.cjs +124 -0
  145. package/bin/verify-quorum-health.cjs +273 -0
  146. package/bin/write-check-result.cjs +106 -0
  147. package/bin/xstate-to-tla.cjs +483 -0
  148. package/bin/xstate-trace-walker.cjs +205 -0
  149. package/commands/qgsd/add-phase.md +43 -0
  150. package/commands/qgsd/add-requirement.md +24 -0
  151. package/commands/qgsd/add-todo.md +47 -0
  152. package/commands/qgsd/audit-milestone.md +37 -0
  153. package/commands/qgsd/check-todos.md +45 -0
  154. package/commands/qgsd/cleanup.md +18 -0
  155. package/commands/qgsd/close-formal-gaps.md +33 -0
  156. package/commands/qgsd/complete-milestone.md +136 -0
  157. package/commands/qgsd/debug.md +166 -0
  158. package/commands/qgsd/discuss-phase.md +83 -0
  159. package/commands/qgsd/execute-phase.md +117 -0
  160. package/commands/qgsd/fix-tests.md +27 -0
  161. package/commands/qgsd/formal-test-sync.md +32 -0
  162. package/commands/qgsd/health.md +22 -0
  163. package/commands/qgsd/help.md +22 -0
  164. package/commands/qgsd/insert-phase.md +32 -0
  165. package/commands/qgsd/join-discord.md +18 -0
  166. package/commands/qgsd/list-phase-assumptions.md +46 -0
  167. package/commands/qgsd/map-codebase.md +71 -0
  168. package/commands/qgsd/map-requirements.md +20 -0
  169. package/commands/qgsd/mcp-restart.md +176 -0
  170. package/commands/qgsd/mcp-set-model.md +134 -0
  171. package/commands/qgsd/mcp-setup.md +1371 -0
  172. package/commands/qgsd/mcp-status.md +274 -0
  173. package/commands/qgsd/mcp-update.md +238 -0
  174. package/commands/qgsd/new-milestone.md +44 -0
  175. package/commands/qgsd/new-project.md +42 -0
  176. package/commands/qgsd/observe.md +260 -0
  177. package/commands/qgsd/pause-work.md +38 -0
  178. package/commands/qgsd/plan-milestone-gaps.md +34 -0
  179. package/commands/qgsd/plan-phase.md +44 -0
  180. package/commands/qgsd/polyrepo.md +50 -0
  181. package/commands/qgsd/progress.md +24 -0
  182. package/commands/qgsd/queue.md +54 -0
  183. package/commands/qgsd/quick.md +133 -0
  184. package/commands/qgsd/quorum-test.md +275 -0
  185. package/commands/qgsd/quorum.md +707 -0
  186. package/commands/qgsd/reapply-patches.md +110 -0
  187. package/commands/qgsd/remove-phase.md +31 -0
  188. package/commands/qgsd/research-phase.md +189 -0
  189. package/commands/qgsd/resume-work.md +40 -0
  190. package/commands/qgsd/set-profile.md +34 -0
  191. package/commands/qgsd/settings.md +39 -0
  192. package/commands/qgsd/solve.md +565 -0
  193. package/commands/qgsd/sync-baselines.md +119 -0
  194. package/commands/qgsd/triage.md +233 -0
  195. package/commands/qgsd/update.md +37 -0
  196. package/commands/qgsd/verify-work.md +38 -0
  197. package/hooks/dist/config-loader.js +297 -0
  198. package/hooks/dist/conformance-schema.cjs +12 -0
  199. package/hooks/dist/gsd-context-monitor.js +64 -0
  200. package/hooks/dist/qgsd-check-update.js +62 -0
  201. package/hooks/dist/qgsd-circuit-breaker.js +682 -0
  202. package/hooks/dist/qgsd-precompact.js +156 -0
  203. package/hooks/dist/qgsd-prompt.js +653 -0
  204. package/hooks/dist/qgsd-session-start.js +122 -0
  205. package/hooks/dist/qgsd-slot-correlator.js +58 -0
  206. package/hooks/dist/qgsd-spec-regen.js +86 -0
  207. package/hooks/dist/qgsd-statusline.js +91 -0
  208. package/hooks/dist/qgsd-stop.js +553 -0
  209. package/hooks/dist/qgsd-token-collector.js +133 -0
  210. package/hooks/dist/unified-mcp-server.mjs +669 -0
  211. package/package.json +95 -0
  212. package/scripts/build-hooks.js +46 -0
  213. package/scripts/postinstall.js +48 -0
  214. package/scripts/secret-audit.sh +45 -0
  215. package/templates/qgsd.json +49 -0
@@ -0,0 +1,1371 @@
1
+ ---
2
+ name: qgsd:mcp-setup
3
+ description: Configure quorum agents — first-run linear onboarding for new installs, live-status agent menu for re-runs
4
+ allowed-tools:
5
+ - Bash
6
+ - Read
7
+ ---
8
+
9
+ <objective>
10
+ Configure QGSD quorum agents in `~/.claude.json`. Detects whether any MCP servers are configured and routes to the appropriate flow:
11
+ - **First-run** (zero configured entries): linear onboarding — select agent templates, collect API keys via keytar, write batch changes with backup, restart agents
12
+ - **Re-run** (existing entries): live-status agent roster menu — view model/provider/key status, select agent, choose action (set key / swap provider / remove)
13
+ </objective>
14
+
15
+ <process>
16
+
17
+ ## Step 1: Detect first-run vs re-run
18
+
19
+ Run this Bash command and store the output as SETUP_INFO:
20
+
21
+ ```bash
22
+ SETUP_INFO=$(node -e "
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+ const os = require('os');
26
+
27
+ const claudeJsonPath = path.join(os.homedir(), '.claude.json');
28
+ let claudeJson = {};
29
+ try {
30
+ claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8'));
31
+ } catch (e) {
32
+ // Missing or corrupt: treat as fresh install
33
+ }
34
+ const servers = claudeJson.mcpServers || {};
35
+ const configured = Object.entries(servers).filter(([k, v]) => v && v.command && v.args);
36
+ const isFirstRun = configured.length === 0;
37
+ const result = { isFirstRun, configuredCount: configured.length, agentKeys: configured.map(([k]) => k) };
38
+ process.stdout.write(JSON.stringify(result) + '\n');
39
+ ")
40
+ ```
41
+
42
+ Parse SETUP_INFO JSON for: `isFirstRun` (boolean), `configuredCount` (int), `agentKeys` (array).
43
+
44
+ **If `isFirstRun` is true:** Continue to Step 2 (first-run flow).
45
+
46
+ **If `isFirstRun` is false:** Skip to the Re-run Agent Menu section below.
47
+
48
+ ---
49
+
50
+ ## Step 2: First-run onboarding flow
51
+
52
+ Display the welcome banner:
53
+
54
+ ```
55
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
56
+ QGSD ► MCP SETUP — FIRST RUN
57
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
58
+
59
+ No quorum agents configured. Let's set up your first agent.
60
+
61
+ Each agent is a claude-mcp-server instance connected to a
62
+ different LLM provider. You need at least one to use quorum.
63
+ ```
64
+
65
+ **Provider template map** (reference throughout the first-run flow):
66
+
67
+ | Agent | Provider | Base URL | Model |
68
+ |---|---|---|---|
69
+ | claude-1 | AkashML | https://api.akashml.com/v1 | deepseek-ai/DeepSeek-V3 |
70
+ | claude-2 | AkashML | https://api.akashml.com/v1 | MiniMaxAI/MiniMax-M2.5 |
71
+ | claude-3 | Together.xyz | https://api.together.xyz/v1 | Qwen/Qwen3-Coder-480B |
72
+ | claude-5 | Together.xyz | https://api.together.xyz/v1 | meta-llama/Llama-4-M |
73
+ | claude-4 | Fireworks | https://api.fireworks.ai/inference/v1 | kimi |
74
+
75
+ ### Step 2a: Select agent template
76
+
77
+ Use AskUserQuestion:
78
+ - header: "Choose an agent to configure"
79
+ - question: "Select an agent template to set up. You can add more after."
80
+ - options (omit agents already configured or skipped in this session):
81
+ - "1 — claude-1 (AkashML, DeepSeek-V3)"
82
+ - "2 — claude-2 (AkashML, MiniMax-M2.5)"
83
+ - "3 — claude-3 (Together.xyz, Qwen3-Coder-480B)"
84
+ - "4 — claude-5 (Together.xyz, Llama-4-M)"
85
+ - "5 — claude-4 (Fireworks, kimi)"
86
+ - "Skip — configure later via /qgsd:mcp-setup"
87
+
88
+ If "Skip" is chosen, display:
89
+
90
+ ```
91
+ ⚠ No agents configured. Run /qgsd:mcp-setup when ready.
92
+ ```
93
+
94
+ Stop.
95
+
96
+ ### Step 2b: Collect API key
97
+
98
+ Resolve agent name, provider name, base URL, and model from the selection using the template map above.
99
+
100
+ Use AskUserQuestion:
101
+ - header: "API Key — {agent-name}"
102
+ - question: "Enter your {provider-name} API key.\n\nThe key will be stored in your system keychain (keytar). It will not appear in any log or plain-text file."
103
+ - options:
104
+ - "Continue (I have my key ready)"
105
+ - "Skip this agent"
106
+
107
+ If "Skip this agent": add agent to skipped list, return to Step 2a.
108
+
109
+ If "Continue": use a second AskUserQuestion to collect the key:
110
+ - header: "Enter API Key"
111
+ - question: "Paste the API key for {agent-name} ({provider-name}):"
112
+ - options: (user types key and selects "Confirm")
113
+ - "Confirm key"
114
+
115
+ Store the key using this Bash command (substitute AGENT_KEY and API_KEY):
116
+
117
+ ```bash
118
+ KEY_RESULT=$(node -e "
119
+ const { set, SERVICE } = require('~/.claude/qgsd-bin/secrets.cjs');
120
+ const agentKey = process.env.AGENT_KEY;
121
+ const apiKey = process.env.API_KEY;
122
+ (async () => {
123
+ try {
124
+ const keyName = 'ANTHROPIC_API_KEY_' + agentKey.toUpperCase().replace(/-/g,'_');
125
+ await set(SERVICE, keyName, apiKey);
126
+ process.stdout.write(JSON.stringify({ stored: true, method: 'keytar', keyName }) + '\n');
127
+ } catch (e) {
128
+ process.stdout.write(JSON.stringify({ stored: false, error: e.message }) + '\n');
129
+ }
130
+ })();
131
+ " AGENT_KEY="{agent-name}" API_KEY="{user-key}")
132
+ ```
133
+
134
+ Parse KEY_RESULT:
135
+ - `stored: true` — mark agent as `method: keytar` in pending batch
136
+ - `stored: false` (keytar unavailable) — handle fallback below
137
+
138
+ **Keytar unavailable fallback:**
139
+
140
+ Use AskUserQuestion:
141
+ - header: "Keychain Unavailable"
142
+ - question: "System keychain unavailable. API key will be stored unencrypted in ~/.claude.json (less secure). Confirm?\n\nLinux users: sudo apt install libsecret-1-dev gnome-keyring"
143
+ - options:
144
+ - "Store unencrypted in ~/.claude.json (less secure)"
145
+ - "Skip this agent"
146
+
147
+ If "Skip this agent": add to skipped list, return to Step 2a.
148
+
149
+ If "Store unencrypted": mark agent as `method: env_block` with the key value in pending batch. Write audit log:
150
+
151
+ ```bash
152
+ mkdir -p ~/.claude/debug
153
+ node -e "
154
+ const fs = require('fs');
155
+ const ts = new Date().toISOString();
156
+ const msg = ts + ' QGSD mcp-setup: keytar unavailable for ' + process.env.AGENT_KEY + ' — API key stored unencrypted in env block\n';
157
+ fs.appendFileSync(require('os').homedir() + '/.claude/debug/mcp-setup-audit.log', msg);
158
+ " AGENT_KEY="{agent-name}"
159
+ ```
160
+
161
+ ### Step 2c: Add another or finish
162
+
163
+ Use AskUserQuestion:
164
+ - header: "Agent Added"
165
+ - question: "Agent {agent-name} configured. Add another or finish?"
166
+ - options:
167
+ - "Add another agent"
168
+ - "Finish setup"
169
+
170
+ If "Add another agent": return to Step 2a (omit already-configured agents).
171
+ If "Finish setup": continue to Step 3.
172
+
173
+ ---
174
+
175
+ ## Step 3: Apply pending changes
176
+
177
+ If pending batch is empty (all skipped):
178
+
179
+ ```
180
+ ⚠ No agents configured. Run /qgsd:mcp-setup when ready.
181
+ ```
182
+
183
+ Stop.
184
+
185
+ Display pending summary:
186
+
187
+ ```
188
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
189
+ QGSD ► REVIEW PENDING CHANGES
190
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
191
+
192
+ Agents to add to ~/.claude.json:
193
+
194
+ ◆ {agent-name} → {provider} ({base-url})
195
+ Key: {stored in system keychain | stored in env block (unencrypted)}
196
+ [repeat for each pending agent]
197
+
198
+ Skipped:
199
+ ○ {agent-name} — run /qgsd:mcp-setup to configure later
200
+ ```
201
+
202
+ Use AskUserQuestion:
203
+ - header: "Apply Changes"
204
+ - question: "Apply changes to ~/.claude.json and restart configured agents?"
205
+ - options:
206
+ - "Apply and restart agents"
207
+ - "Cancel — discard all changes"
208
+
209
+ If "Cancel": display "Changes discarded." Stop.
210
+
211
+ If "Apply and restart agents":
212
+
213
+ ### Step 3a: Backup ~/.claude.json
214
+
215
+ ```bash
216
+ cp ~/.claude.json ~/.claude.json.backup-$(date +%Y-%m-%d-%H%M%S) 2>/dev/null || true
217
+ ```
218
+
219
+ ### Step 3b: Resolve claude-mcp-server path
220
+
221
+ ```bash
222
+ CLAUDE_MCP_PATH=$(node -e "
223
+ const path = require('path');
224
+ const fs = require('fs');
225
+ const os = require('os');
226
+ const { spawnSync } = require('child_process');
227
+
228
+ // Strategy 1: read from existing ~/.claude.json entries
229
+ try {
230
+ const cj = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf8'));
231
+ for (const [, cfg] of Object.entries(cj.mcpServers || {})) {
232
+ if ((cfg.args || []).some(a => String(a).includes('claude-mcp-server'))) {
233
+ process.stdout.write(cfg.args[0]);
234
+ process.exit(0);
235
+ }
236
+ }
237
+ } catch (e) {}
238
+
239
+ // Strategy 2: check global npm root
240
+ try {
241
+ const r = spawnSync('npm', ['root', '-g'], { encoding: 'utf8' });
242
+ const npmRoot = (r.stdout || '').trim();
243
+ const candidate = path.join(npmRoot, 'claude-mcp-server', 'dist', 'index.js');
244
+ if (fs.existsSync(candidate)) { process.stdout.write(candidate); process.exit(0); }
245
+ } catch (e) {}
246
+
247
+ process.stdout.write('');
248
+ " 2>/dev/null)
249
+ ```
250
+
251
+ ### Step 3c: Write entries to ~/.claude.json
252
+
253
+ If `$CLAUDE_MCP_PATH` is empty (Step 3b returned empty string), display a warning and use the fallback:
254
+
255
+ ```bash
256
+ if [ -z "$CLAUDE_MCP_PATH" ]; then
257
+ echo "⚠ Could not resolve claude-mcp-server path automatically. The mcpServers entry will use 'claude-mcp-server' as the args value. You may need to update the path manually after installation."
258
+ CLAUDE_MCP_PATH="claude-mcp-server"
259
+ fi
260
+ ```
261
+
262
+ For each pending agent, add the mcpServers entry. Use this inline node script as a template (adapt for the actual pending agents in the session):
263
+
264
+ ```bash
265
+ node -e "
266
+ const fs = require('fs');
267
+ const path = require('path');
268
+ const os = require('os');
269
+
270
+ const claudeJsonPath = path.join(os.homedir(), '.claude.json');
271
+ let claudeJson = {};
272
+ try { claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8')); } catch (e) {}
273
+ if (!claudeJson.mcpServers) claudeJson.mcpServers = {};
274
+
275
+ // pendingAgents is an array of { name, baseUrl, model, method, apiKey }
276
+ // where method is 'keytar' or 'env_block'
277
+ const pendingAgents = JSON.parse(process.env.PENDING_AGENTS_JSON);
278
+ const mcpPath = process.env.CLAUDE_MCP_PATH || '';
279
+
280
+ for (const agent of pendingAgents) {
281
+ claudeJson.mcpServers[agent.name] = {
282
+ command: 'node',
283
+ args: [mcpPath],
284
+ env: {
285
+ ANTHROPIC_API_KEY: agent.method === 'env_block' ? agent.apiKey : '',
286
+ ANTHROPIC_BASE_URL: agent.baseUrl,
287
+ CLAUDE_DEFAULT_MODEL: agent.model
288
+ }
289
+ };
290
+ }
291
+
292
+ fs.writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2));
293
+ process.stdout.write(JSON.stringify({ written: true, count: pendingAgents.length }) + '\n');
294
+ " PENDING_AGENTS_JSON='...' CLAUDE_MCP_PATH="$CLAUDE_MCP_PATH"
295
+ ```
296
+
297
+ ### Step 3d: Sync keytar secrets to env blocks
298
+
299
+ ```bash
300
+ node -e "
301
+ const { syncToClaudeJson, SERVICE } = require('~/.claude/qgsd-bin/secrets.cjs');
302
+ syncToClaudeJson(SERVICE)
303
+ .then(() => process.stdout.write('synced\n'))
304
+ .catch(e => process.stderr.write('sync warning: ' + e.message + '\n'));
305
+ "
306
+ ```
307
+
308
+ ### Step 3e: Restart each configured agent (sequential — one at a time)
309
+
310
+ For each agent in the pending batch:
311
+
312
+ Invoke `/qgsd:mcp-restart {agent-name}`.
313
+
314
+ If restart fails or times out, leave config in written state and display:
315
+
316
+ ```
317
+ ⚠ {agent-name}: restart failed. Config was written — agent will reload on next Claude Code restart.
318
+ Manual retry: /qgsd:mcp-restart {agent-name}
319
+ ```
320
+
321
+ ### Step 3f: Closing summary
322
+
323
+ ```
324
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
325
+ QGSD ► SETUP COMPLETE ✓
326
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
327
+
328
+ Changes applied and agents restarted.
329
+
330
+ ✓ {agent-name} — restarted
331
+ [repeat for each successfully restarted agent]
332
+
333
+ ○ {agent-name} — skipped (run /qgsd:mcp-setup to configure later)
334
+ [repeat for each skipped agent]
335
+
336
+ Run /qgsd:mcp-status to verify agent health.
337
+ ```
338
+
339
+ ---
340
+
341
+ ## Re-run Agent Menu
342
+
343
+ Read the current agent roster from `~/.claude.json`:
344
+
345
+ ```bash
346
+ ROSTER=$(node -e "
347
+ const fs = require('fs');
348
+ const path = require('path');
349
+ const os = require('os');
350
+
351
+ (async () => {
352
+ let claudeJson = {};
353
+ try {
354
+ claudeJson = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf8'));
355
+ } catch (e) {}
356
+
357
+ const servers = claudeJson.mcpServers || {};
358
+ const agents = [];
359
+
360
+ for (const [name, cfg] of Object.entries(servers)) {
361
+ const env = cfg.env || {};
362
+ const model = env.CLAUDE_DEFAULT_MODEL || '—';
363
+ const provider = env.ANTHROPIC_BASE_URL || '—';
364
+
365
+ let keyStatus = 'no key';
366
+ try {
367
+ const { get, SERVICE } = require('~/.claude/qgsd-bin/secrets.cjs');
368
+ const keyName = 'ANTHROPIC_API_KEY_' + name.toUpperCase().replace(/-/g,'_');
369
+ const stored = await get(SERVICE, keyName);
370
+ keyStatus = stored ? 'key stored' : (env.ANTHROPIC_API_KEY ? 'key in env' : 'no key');
371
+ } catch (e) {
372
+ keyStatus = env.ANTHROPIC_API_KEY ? 'key in env' : 'no key';
373
+ }
374
+
375
+ agents.push({ name, model, provider, keyStatus });
376
+ }
377
+
378
+ process.stdout.write(JSON.stringify({ agents }) + '\n');
379
+ })();
380
+ ")
381
+ ```
382
+
383
+ Parse ROSTER for `agents` array.
384
+
385
+ Display re-run banner:
386
+
387
+ ```
388
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
389
+ QGSD ► MCP SETUP — AGENT ROSTER
390
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
391
+ ```
392
+
393
+ Render a numbered table with columns: #, Agent, Model, Provider, Key:
394
+
395
+ ```
396
+ # Agent Model Provider Key
397
+ ── ─────────────────── ────────────────────────── ───────────────────────────────────── ──────────
398
+ 1 claude-1 deepseek-ai/DeepSeek-V3 https://api.akashml.com/v1 key stored
399
+ 2 claude-2 MiniMaxAI/MiniMax-M2.5 https://api.akashml.com/v1 no key
400
+ ```
401
+
402
+ Use AskUserQuestion:
403
+ - header: "Select Agent"
404
+ - question: "Enter the number of the agent to configure, or choose an option:"
405
+ - options:
406
+ - "1 — {agent-name}" (one per agent)
407
+ - "Add new agent"
408
+ - "Edit Quorum Composition"
409
+ - "Exit"
410
+
411
+ If "Exit": display "No changes made." Stop.
412
+
413
+ If "Edit Quorum Composition": route to **Composition Screen** section below.
414
+
415
+ If "Add new agent":
416
+
417
+ **Step A — Select agent template**
418
+
419
+ First, detect which servers are already configured in `~/.claude.json`:
420
+
421
+ ```bash
422
+ EXISTING_SERVERS=$(node -e "
423
+ const fs = require('fs'), os = require('os');
424
+ try {
425
+ const cj = JSON.parse(fs.readFileSync(os.homedir() + '/.claude.json', 'utf8'));
426
+ console.log(JSON.stringify(Object.keys(cj.mcpServers || {})));
427
+ } catch(e) { console.log('[]'); }
428
+ ")
429
+ ```
430
+
431
+ Parse `EXISTING_SERVERS` as a JSON array. Use this array to filter the options below.
432
+
433
+ Then use AskUserQuestion with two sections of options:
434
+
435
+ - header: "Add Agent — Select Template"
436
+ - question: "Select an agent template to add:\n\n(Agents already configured are excluded)"
437
+ - options — build the list using these filtering rules:
438
+ - **Claude MCP slots** (omit if agent name already in EXISTING_SERVERS):
439
+ - "1 — claude-1 (AkashML, DeepSeek-V3)"
440
+ - "2 — claude-2 (AkashML, MiniMax-M2.5)"
441
+ - "3 — claude-3 (Together.xyz, Qwen3-Coder-480B)"
442
+ - "4 — claude-5 (Together.xyz, Llama-4-M)"
443
+ - "5 — claude-4 (Fireworks, kimi)"
444
+ - **Native CLI second slots** (omit if second slot already in EXISTING_SERVERS OR if first slot NOT in EXISTING_SERVERS):
445
+ - "6 — codex-cli-2 (second Codex slot — copies codex-cli-1 config)" [show only if codex-cli-1 is in EXISTING_SERVERS AND codex-cli-2 is NOT in EXISTING_SERVERS]
446
+ - "7 — gemini-cli-2 (second Gemini slot — copies gemini-cli-1 config)" [show only if gemini-cli-1 is in EXISTING_SERVERS AND gemini-cli-2 is NOT in EXISTING_SERVERS]
447
+ - "8 — opencode-2 (second OpenCode slot — copies opencode-1 config)" [show only if opencode-1 is in EXISTING_SERVERS AND opencode-2 is NOT in EXISTING_SERVERS]
448
+ - "9 — copilot-2 (second Copilot slot — copies copilot-1 config)" [show only if copilot-1 is in EXISTING_SERVERS AND copilot-2 is NOT in EXISTING_SERVERS]
449
+ - "Cancel — back to roster"
450
+
451
+ If "Cancel — back to roster": display "No changes made." Return to roster display.
452
+
453
+ **Resolver — map selection to slot details:**
454
+
455
+ Claude MCP slot resolver (options 1–5):
456
+ - "1 — claude-1…" → agentName=`claude-1`, provider=`AkashML`, baseUrl=`https://api.akashml.com/v1`, model=`deepseek-ai/DeepSeek-V3`
457
+ - "2 — claude-2…" → agentName=`claude-2`, provider=`AkashML`, baseUrl=`https://api.akashml.com/v1`, model=`MiniMaxAI/MiniMax-M2.5`
458
+ - "3 — claude-3…" → agentName=`claude-3`, provider=`Together.xyz`, baseUrl=`https://api.together.xyz/v1`, model=`Qwen/Qwen3-Coder-480B`
459
+ - "4 — claude-5…" → agentName=`claude-5`, provider=`Together.xyz`, baseUrl=`https://api.together.xyz/v1`, model=`meta-llama/Llama-4-M`
460
+ - "5 — claude-4…" → agentName=`claude-4`, provider=`Fireworks`, baseUrl=`https://api.fireworks.ai/inference/v1`, model=`kimi`
461
+
462
+ → When options 1–5 selected: continue to Step B (API key collection) as before.
463
+
464
+ Native CLI second-slot resolver (options 6–9):
465
+ - "6 — codex-cli-2…" → newSlot=`codex-cli-2`, sourceSlot=`codex-cli-1`
466
+ - "7 — gemini-cli-2…" → newSlot=`gemini-cli-2`, sourceSlot=`gemini-cli-1`
467
+ - "8 — opencode-2…" → newSlot=`opencode-2`, sourceSlot=`opencode-1`
468
+ - "9 — copilot-2…" → newSlot=`copilot-2`, sourceSlot=`copilot-1`
469
+
470
+ → When options 6–9 selected: route to **Step B-native** below (skip the API key step).
471
+
472
+ **Step B — Collect API key (claude-mcp-server slots only)**
473
+
474
+ Use AskUserQuestion:
475
+ - header: "API Key — {agent-name}"
476
+ - question: `"Enter your {provider-name} API key.\n\nThe key will be stored in your system keychain (keytar). It will not appear in any log or plain-text file."`
477
+ - options:
478
+ - "Continue (I have my key ready)"
479
+ - "Cancel"
480
+
481
+ If "Cancel": display "No changes made." Return to roster display.
482
+
483
+ If "Continue": second AskUserQuestion:
484
+ - header: "Enter API Key — {agent-name}"
485
+ - question: `"Paste the API key for {agent-name} ({provider-name}):"`
486
+ - options:
487
+ - "Confirm key"
488
+ - "Cancel"
489
+
490
+ If "Cancel": display "No changes made." Return to roster display.
491
+
492
+ Store the key using bin/secrets.cjs (agent name and key passed via env vars — never interpolated):
493
+
494
+ ```bash
495
+ KEY_RESULT=$(node -e "
496
+ const { set, SERVICE } = require('~/.claude/qgsd-bin/secrets.cjs');
497
+ const agentKey = process.env.AGENT_KEY;
498
+ const apiKey = process.env.API_KEY;
499
+ (async () => {
500
+ try {
501
+ const keyName = 'ANTHROPIC_API_KEY_' + agentKey.toUpperCase().replace(/-/g,'_');
502
+ await set(SERVICE, keyName, apiKey);
503
+ process.stdout.write(JSON.stringify({ stored: true, method: 'keytar', keyName }) + '\n');
504
+ } catch (e) {
505
+ process.stdout.write(JSON.stringify({ stored: false, error: e.message }) + '\n');
506
+ }
507
+ })();
508
+ " AGENT_KEY="{agent-name}" API_KEY="{user-key}")
509
+ ```
510
+
511
+ Parse KEY_RESULT:
512
+ - `stored: true` — proceed to Step C
513
+ - `stored: false` — keytar unavailable fallback:
514
+
515
+ Use AskUserQuestion:
516
+ - header: "Keychain Unavailable"
517
+ - question: "System keychain unavailable. API key will be stored unencrypted in ~/.claude.json (less secure). Confirm?\n\nLinux users: sudo apt install libsecret-1-dev gnome-keyring"
518
+ - options:
519
+ - "Store unencrypted in ~/.claude.json (less secure)"
520
+ - "Cancel — back to roster"
521
+
522
+ If "Cancel — back to roster": display "No changes made." Return to roster display.
523
+
524
+ If "Store unencrypted in ~/.claude.json (less secure)": write audit log then mark agent as `method: env_block` in pending changes and proceed to Step C:
525
+ ```bash
526
+ mkdir -p ~/.claude/debug
527
+ node -e "
528
+ const fs = require('fs');
529
+ const ts = new Date().toISOString();
530
+ const msg = ts + ' QGSD mcp-setup: keytar unavailable for ' + process.env.AGENT_KEY + ' — API key stored unencrypted in env block\n';
531
+ fs.appendFileSync(require('os').homedir() + '/.claude/debug/mcp-setup-audit.log', msg);
532
+ " AGENT_KEY="{agent-name}"
533
+ ```
534
+
535
+ **Step C — Confirm + apply**
536
+
537
+ Show pending summary:
538
+
539
+ ```
540
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
541
+ QGSD ► REVIEW PENDING CHANGES
542
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
543
+
544
+ ◆ {agent-name} → {provider-name} ({base-url})
545
+ Key: {stored in system keychain | stored in env block (unencrypted)}
546
+ ```
547
+
548
+ Use AskUserQuestion:
549
+ - header: "Add Agent"
550
+ - question: "Add {agent-name} to ~/.claude.json and start it?"
551
+ - options:
552
+ - "Add and start"
553
+ - "Cancel — discard changes"
554
+
555
+ If "Cancel — discard changes": display "Changes discarded." Return to roster display.
556
+
557
+ If "Add and start":
558
+
559
+ 1. Backup ~/.claude.json:
560
+ ```bash
561
+ cp ~/.claude.json ~/.claude.json.backup-$(date +%Y-%m-%d-%H%M%S) 2>/dev/null || true
562
+ ```
563
+
564
+ 2. Resolve claude-mcp-server path (read from existing entries, then npm root fallback):
565
+ ```bash
566
+ CLAUDE_MCP_PATH=$(node -e "
567
+ const path = require('path');
568
+ const fs = require('fs');
569
+ const os = require('os');
570
+ const { spawnSync } = require('child_process');
571
+
572
+ // Strategy 1: read from existing ~/.claude.json entries
573
+ try {
574
+ const cj = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf8'));
575
+ for (const [, cfg] of Object.entries(cj.mcpServers || {})) {
576
+ if ((cfg.args || []).some(a => String(a).includes('claude-mcp-server'))) {
577
+ process.stdout.write(cfg.args[0]);
578
+ process.exit(0);
579
+ }
580
+ }
581
+ } catch (e) {}
582
+
583
+ // Strategy 2: check global npm root
584
+ try {
585
+ const r = spawnSync('npm', ['root', '-g'], { encoding: 'utf8' });
586
+ const npmRoot = (r.stdout || '').trim();
587
+ const candidate = path.join(npmRoot, 'claude-mcp-server', 'dist', 'index.js');
588
+ if (fs.existsSync(candidate)) { process.stdout.write(candidate); process.exit(0); }
589
+ } catch (e) {}
590
+
591
+ process.stdout.write('');
592
+ " 2>/dev/null)
593
+ ```
594
+
595
+ If `CLAUDE_MCP_PATH` is empty: display warning and use placeholder args `["claude-mcp-server"]` (command: `"node"`).
596
+
597
+ 3. Write new mcpServers entry via inline node (all values passed via env vars — never interpolated):
598
+ ```bash
599
+ node -e "
600
+ const fs = require('fs');
601
+ const path = require('path');
602
+ const os = require('os');
603
+ const claudeJsonPath = path.join(os.homedir(), '.claude.json');
604
+ let claudeJson = {};
605
+ try { claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8')); } catch (e) {}
606
+ if (!claudeJson.mcpServers) claudeJson.mcpServers = {};
607
+ const agentName = process.env.AGENT_NAME;
608
+ const baseUrl = process.env.BASE_URL;
609
+ const model = process.env.MODEL;
610
+ const mcpPath = process.env.MCP_PATH;
611
+ const apiKey = process.env.API_KEY_ENV || '';
612
+ claudeJson.mcpServers[agentName] = {
613
+ command: 'node',
614
+ args: [mcpPath],
615
+ env: {
616
+ ANTHROPIC_API_KEY: apiKey,
617
+ ANTHROPIC_BASE_URL: baseUrl,
618
+ CLAUDE_DEFAULT_MODEL: model
619
+ }
620
+ };
621
+ fs.writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2));
622
+ process.stdout.write(JSON.stringify({ written: true }) + '\n');
623
+ " AGENT_NAME="{agent-name}" BASE_URL="{base-url}" MODEL="{model}" MCP_PATH="$CLAUDE_MCP_PATH" API_KEY_ENV="{api-key-if-env-block-method}"
624
+ ```
625
+
626
+ 4. Sync keytar secrets to ~/.claude.json:
627
+ ```bash
628
+ node -e "
629
+ const { syncToClaudeJson, SERVICE } = require('~/.claude/qgsd-bin/secrets.cjs');
630
+ syncToClaudeJson(SERVICE).then(() => process.stdout.write('synced\n')).catch(e => process.stderr.write(e.message + '\n'));
631
+ "
632
+ ```
633
+
634
+ 5. Invoke `/qgsd:mcp-restart {agent-name}` to start the new agent process.
635
+
636
+ **Step D — Identity ping (AGENT-03)**
637
+
638
+ After restart, display: `"◆ Waiting for {agent-name} to start... calling identity tool"`
639
+
640
+ Invoke the `identity` tool on the newly started agent. Display the result:
641
+
642
+ If identity responds:
643
+ ```
644
+ ✓ Agent added and verified live.
645
+
646
+ ✓ {agent-name} — added, restarted, identity confirmed
647
+ Name: {identity.name}
648
+ Version: {identity.version}
649
+ Model: {identity.model}
650
+
651
+ Run /qgsd:mcp-status to see full agent roster.
652
+ ```
653
+
654
+ If identity times out or errors:
655
+ ```
656
+ ✓ Agent added and restarted.
657
+
658
+ ◆ {agent-name} — added, restarted (identity ping timed out — agent may need a moment to start)
659
+
660
+ Run /qgsd:mcp-status to verify agent health.
661
+ ```
662
+
663
+ **Return path:** If this add-slot flow was entered from the Composition Screen (via "Add new slot"), return to Composition Screen after identity ping. Otherwise, return to roster display (re-read roster to show new agent).
664
+
665
+ ---
666
+
667
+ **Step B-native — Add Native CLI Slot (options 6–9 from Step A)**
668
+
669
+ This branch is entered when the user selected a native CLI second slot (options 6–9) in Step A. At this point `newSlot` and `sourceSlot` are set from the resolver.
670
+
671
+ Show a confirmation screen:
672
+
673
+ ```
674
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
675
+ QGSD ► REVIEW PENDING CHANGES
676
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
677
+
678
+ ◆ {new-slot-name} → copied from {source-slot-name}
679
+ Binary: {path from source slot}
680
+ Auth: uses {source-slot-name}'s existing credentials
681
+ quorum_active: will be appended
682
+ ```
683
+
684
+ Use AskUserQuestion:
685
+ - header: "Add Native CLI Slot"
686
+ - question: "Add {new-slot-name} to ~/.claude.json and quorum_active?"
687
+ - options:
688
+ - "Add and start"
689
+ - "Cancel — discard changes"
690
+
691
+ If "Cancel — discard changes": display "Changes discarded." Return to roster display.
692
+
693
+ If "Add and start":
694
+
695
+ 1. Backup `~/.claude.json`:
696
+ ```bash
697
+ cp ~/.claude.json ~/.claude.json.backup-$(date +%Y-%m-%d-%H%M%S) 2>/dev/null || true
698
+ ```
699
+
700
+ 2. Copy mcpServers entry from source slot to new slot (deep copy — command, args, env all preserved):
701
+ ```bash
702
+ node -e "
703
+ const fs = require('fs');
704
+ const os = require('os');
705
+ const claudeJsonPath = os.homedir() + '/.claude.json';
706
+ let cj = {};
707
+ try { cj = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8')); } catch(e) {}
708
+ const srcSlot = process.env.SOURCE_SLOT;
709
+ const newSlot = process.env.NEW_SLOT;
710
+ const src = (cj.mcpServers || {})[srcSlot];
711
+ if (!src) { process.stdout.write(JSON.stringify({ written: false, error: 'source slot not found' }) + '\n'); process.exit(1); }
712
+ cj.mcpServers = cj.mcpServers || {};
713
+ cj.mcpServers[newSlot] = JSON.parse(JSON.stringify(src)); // deep copy
714
+ fs.writeFileSync(claudeJsonPath, JSON.stringify(cj, null, 2));
715
+ process.stdout.write(JSON.stringify({ written: true, newSlot, sourceSlot: srcSlot }) + '\n');
716
+ " SOURCE_SLOT="{source-slot-name}" NEW_SLOT="{new-slot-name}"
717
+ ```
718
+
719
+ If `written: false`: display error and return to roster.
720
+
721
+ 3. Append new slot to `quorum_active` in `~/.claude/qgsd.json`:
722
+ ```bash
723
+ node -e "
724
+ const fs = require('fs'), os = require('os');
725
+ const qgsdPath = os.homedir() + '/.claude/qgsd.json';
726
+ let cfg = {};
727
+ try { cfg = JSON.parse(fs.readFileSync(qgsdPath, 'utf8')); } catch(e) {}
728
+ const active = Array.isArray(cfg.quorum_active) ? cfg.quorum_active : [];
729
+ const newSlot = process.env.NEW_SLOT;
730
+ if (!active.includes(newSlot)) {
731
+ cfg.quorum_active = [...active, newSlot];
732
+ fs.writeFileSync(qgsdPath, JSON.stringify(cfg, null, 2) + '\n');
733
+ process.stdout.write(JSON.stringify({ added: true, slot: newSlot }) + '\n');
734
+ } else {
735
+ process.stdout.write(JSON.stringify({ added: false, slot: newSlot, reason: 'already present' }) + '\n');
736
+ }
737
+ " NEW_SLOT="{new-slot-name}"
738
+ ```
739
+
740
+ 4. Invoke `/qgsd:mcp-restart {new-slot-name}` to start the new agent process.
741
+
742
+ **Step D — Identity ping** (same as claude-mcp-server path):
743
+
744
+ After restart, display: `"◆ Waiting for {new-slot-name} to start... calling identity tool"`
745
+
746
+ Invoke the `identity` tool on the newly started agent. Display the result:
747
+
748
+ If identity responds:
749
+ ```
750
+ ✓ Agent added and verified live.
751
+
752
+ ✓ {new-slot-name} — added, restarted, identity confirmed
753
+ Name: {identity.name}
754
+ Version: {identity.version}
755
+ Model: {identity.model}
756
+
757
+ Run /qgsd:mcp-status to see full agent roster.
758
+ ```
759
+
760
+ If identity times out or errors:
761
+ ```
762
+ ✓ Agent added and restarted.
763
+
764
+ ◆ {new-slot-name} — added, restarted (identity ping timed out — agent may need a moment to start)
765
+
766
+ Run /qgsd:mcp-status to verify agent health.
767
+ ```
768
+
769
+ **Return path:** If this add-slot flow was entered from the Composition Screen (via "Add new slot"), return to Composition Screen after identity ping. Otherwise, return to roster display (re-read roster to show new agent).
770
+
771
+ ---
772
+
773
+ If agent selected: continue to Agent Sub-Menu.
774
+
775
+ ---
776
+
777
+ ## Composition Screen
778
+
779
+ This screen is entered when the user selects "Edit Quorum Composition" from the re-run menu. It shows all configured slots with their current `quorum_active` status and allows toggling.
780
+
781
+ **Step CS-1: Read slots and quorum_active**
782
+
783
+ ```bash
784
+ COMPOSITION_DATA=$(node -e "
785
+ const fs = require('fs'), os = require('os');
786
+ let slots = [];
787
+ let active = [];
788
+ try {
789
+ const cj = JSON.parse(fs.readFileSync(os.homedir() + '/.claude.json', 'utf8'));
790
+ slots = Object.keys(cj.mcpServers || {});
791
+ } catch(e) {}
792
+ try {
793
+ const qgsd = JSON.parse(fs.readFileSync(os.homedir() + '/.claude/qgsd.json', 'utf8'));
794
+ active = Array.isArray(qgsd.quorum_active) ? qgsd.quorum_active : [];
795
+ } catch(e) {}
796
+ process.stdout.write(JSON.stringify({ slots, active }) + '\n');
797
+ ")
798
+ ```
799
+
800
+ Parse `COMPOSITION_DATA` for `slots` (array of all slot names from `~/.claude.json`) and `active` (current `quorum_active` array from `~/.claude/qgsd.json`).
801
+
802
+ **Step CS-2: Display composition table**
803
+
804
+ Display banner:
805
+
806
+ ```
807
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
808
+ QGSD ► QUORUM COMPOSITION
809
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
810
+ ```
811
+
812
+ Render a numbered table. Status rules:
813
+ - If `active` is **empty**: show `● ON (all)` for every slot — fail-open mode means all slots participate
814
+ - If `active` is **non-empty** AND slot IS in `active`: show `● ON`
815
+ - If `active` is **non-empty** AND slot NOT in `active`: show `○ OFF`
816
+
817
+ ```
818
+ # Slot Status
819
+ ── ──────────────── ──────
820
+ 1 codex-cli-1 ● ON
821
+ 2 gemini-cli-1 ● ON
822
+ 3 opencode-1 ○ OFF
823
+ 4 copilot-1 ● ON
824
+ ```
825
+
826
+ If `active` is empty, display a note below the table:
827
+
828
+ ```
829
+ ℹ quorum_active is empty — all slots participate (fail-open mode)
830
+ ```
831
+
832
+ **Step CS-3: AskUserQuestion for composition actions**
833
+
834
+ Initialize `PENDING_ACTIVE` as a copy of `active` (in-memory working array).
835
+
836
+ Use AskUserQuestion:
837
+ - header: "Quorum Composition"
838
+ - question: "Enter slot number to toggle ON/OFF, or choose an option:"
839
+ - options:
840
+ - "1 — {slot-name} [{current PENDING_ACTIVE status}]" (one per slot — show ● ON or ○ OFF based on current PENDING_ACTIVE state)
841
+ - "Apply — save changes to qgsd.json"
842
+ - "Add new slot — add a slot to ~/.claude.json and quorum_active"
843
+ - "Cancel — discard changes"
844
+
845
+ **Toggle handler:** When user selects slot number N:
846
+
847
+ - If slot IS currently in `PENDING_ACTIVE`:
848
+ - If `PENDING_ACTIVE.length === 1` (removing would empty the array): use a second AskUserQuestion to warn: "Removing this slot will leave quorum_active empty — all slots will participate (fail-open). Continue?" with options "Yes — set fail-open mode" / "Cancel". If confirmed: set `PENDING_ACTIVE = []`. If cancelled: no change.
849
+ - Otherwise: remove slot from `PENDING_ACTIVE` → status becomes `○ OFF`
850
+ - If slot is NOT in `PENDING_ACTIVE` AND `PENDING_ACTIVE` is **non-empty**: add slot to `PENDING_ACTIVE` → status becomes `● ON`
851
+ - If slot is NOT in `PENDING_ACTIVE` AND `PENDING_ACTIVE` is **empty**: `PENDING_ACTIVE = [slot]` (switching from fail-open to explicit single-slot list — slot becomes `● ON`, others become `○ OFF`)
852
+
853
+ Re-display the AskUserQuestion with updated statuses after each toggle.
854
+
855
+ **Apply handler:** When "Apply" selected:
856
+
857
+ ```bash
858
+ node -e "
859
+ const fs = require('fs'), os = require('os');
860
+ const qgsdPath = os.homedir() + '/.claude/qgsd.json';
861
+ let cfg = {};
862
+ try { cfg = JSON.parse(fs.readFileSync(qgsdPath, 'utf8')); } catch(e) {}
863
+ cfg.quorum_active = JSON.parse(process.env.PENDING_ACTIVE);
864
+ fs.writeFileSync(qgsdPath, JSON.stringify(cfg, null, 2) + '\n');
865
+ process.stdout.write(JSON.stringify({ written: true, count: cfg.quorum_active.length }) + '\n');
866
+ " PENDING_ACTIVE="{JSON.stringify(PENDING_ACTIVE)}"
867
+ ```
868
+
869
+ If `written: true`: display:
870
+
871
+ ```
872
+ ✓ quorum_active updated — {N} slot(s) active.
873
+
874
+ Changes take effect on next quorum call (no restart required).
875
+ ```
876
+
877
+ Return to roster display.
878
+
879
+ If error: display error message and return to roster.
880
+
881
+ **Add new slot handler:** When "Add new slot" selected:
882
+
883
+ Route to **Step A** (EXISTING_SERVERS detection + template selection). This is the same flow as "Add new agent" from the roster menu. After Step D (identity ping) completes:
884
+
885
+ - Return to **Composition Screen** (re-read fresh `slots` and `active` data from disk).
886
+ - The newly added slot will appear in the table with ● ON status (it was added to `quorum_active` during the Step B or Step B-native apply flow).
887
+ - Re-display the Composition Screen AskUserQuestion.
888
+
889
+ **Cancel handler:** When "Cancel" selected:
890
+
891
+ Display "No changes made." Return to roster display.
892
+
893
+ ---
894
+
895
+ ## Agent Sub-Menu
896
+
897
+ Display agent detail banner:
898
+
899
+ ```
900
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
901
+ QGSD ► {agent-name}
902
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
903
+
904
+ Model: {model}
905
+ Provider: {provider}
906
+ Key: {keyStatus}
907
+ ```
908
+
909
+ Use AskUserQuestion:
910
+ - header: "Actions — {agent-name}"
911
+ - question: "Choose an action:"
912
+ - options:
913
+ - "1 — Set / update API key"
914
+ - "2 — Swap provider"
915
+ - "3 — Remove agent"
916
+ - "Back — return to agent list"
917
+
918
+ **Option 1 — Set / update API key:**
919
+
920
+ **Step A — Check existing key status**
921
+
922
+ Run an inline node script to check whether a key is already stored in keytar for the selected agent:
923
+
924
+ ```bash
925
+ KEY_CHECK_RESULT=$(node -e "
926
+ const { get, SERVICE } = require('~/.claude/qgsd-bin/secrets.cjs');
927
+ (async () => {
928
+ try {
929
+ const agentName = process.env.AGENT_NAME;
930
+ const keyName = 'ANTHROPIC_API_KEY_' + agentName.toUpperCase().replace(/-/g,'_');
931
+ const stored = await get(SERVICE, keyName);
932
+ if (stored) {
933
+ process.stdout.write(JSON.stringify({ hasKey: true, method: 'keytar' }) + '\n');
934
+ } else {
935
+ // Check env block fallback
936
+ const fs = require('fs'), path = require('path'), os = require('os');
937
+ let envVal = '';
938
+ try {
939
+ const cj = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf8'));
940
+ envVal = (cj.mcpServers && cj.mcpServers[agentName] && cj.mcpServers[agentName].env && cj.mcpServers[agentName].env.ANTHROPIC_API_KEY) || '';
941
+ } catch (e) {}
942
+ if (envVal) {
943
+ process.stdout.write(JSON.stringify({ hasKey: true, method: 'env_block' }) + '\n');
944
+ } else {
945
+ process.stdout.write(JSON.stringify({ hasKey: false, method: 'none' }) + '\n');
946
+ }
947
+ }
948
+ } catch (e) {
949
+ process.stdout.write(JSON.stringify({ hasKey: false, method: 'none', error: e.message }) + '\n');
950
+ }
951
+ })();
952
+ " AGENT_NAME="{agent-name}")
953
+ ```
954
+
955
+ Parse KEY_CHECK_RESULT for: `hasKey` (boolean), `method` ('keytar'|'env_block'|'none').
956
+
957
+ **Step B — Prompt user with key-status hint**
958
+
959
+ Use AskUserQuestion:
960
+ - header: "Set API Key — {agent-name}"
961
+ - question: one of:
962
+ - If `hasKey` is true and `method` is `keytar`: `"API key already stored in system keychain (key stored). Enter a new key to overwrite it, or skip.\n\nThe key will be stored in your system keychain (keytar). It will not appear in any log or plain-text file."`
963
+ - If `method` is `env_block`: `"API key currently stored in ~/.claude.json env block. Enter a new key to move it to the system keychain, or skip.\n\nThe key will be stored in your system keychain (keytar). It will not appear in any log or plain-text file."`
964
+ - If `method` is `none`: `"No API key configured for {agent-name}. Enter a key to store it in your system keychain.\n\nThe key will be stored in your system keychain (keytar). It will not appear in any log or plain-text file."`
965
+ - options:
966
+ - "Continue (I have my key ready)"
967
+ - "Skip — back to agent menu"
968
+
969
+ If "Skip — back to agent menu": display "No changes made." Return to Agent Sub-Menu (re-display sub-menu for the same agent).
970
+
971
+ **Step C — Collect the key value**
972
+
973
+ Use a second AskUserQuestion to receive the actual key:
974
+ - header: "Enter API Key — {agent-name}"
975
+ - question: `"Paste your API key for {agent-name}:"`
976
+ - options:
977
+ - "Confirm key"
978
+ - "Cancel"
979
+
980
+ If "Cancel": display "No changes made." Return to Agent Sub-Menu.
981
+
982
+ **Step D — Store in keytar**
983
+
984
+ Run inline node script using `set()` from `bin/secrets.cjs`. Pass key via environment variable only — never interpolate into the script body:
985
+
986
+ ```bash
987
+ KEY_STORE_RESULT=$(node -e "
988
+ const { set, SERVICE } = require('~/.claude/qgsd-bin/secrets.cjs');
989
+ (async () => {
990
+ try {
991
+ const agentName = process.env.AGENT_NAME;
992
+ const apiKey = process.env.API_KEY;
993
+ const keyName = 'ANTHROPIC_API_KEY_' + agentName.toUpperCase().replace(/-/g,'_');
994
+ await set(SERVICE, keyName, apiKey);
995
+ process.stdout.write(JSON.stringify({ stored: true, method: 'keytar', keyName }) + '\n');
996
+ } catch (e) {
997
+ process.stdout.write(JSON.stringify({ stored: false, error: e.message }) + '\n');
998
+ }
999
+ })();
1000
+ " AGENT_NAME="{agent-name}" API_KEY="{user-entered-key}")
1001
+ ```
1002
+
1003
+ Parse KEY_STORE_RESULT:
1004
+ - `stored: true` — continue to Step E
1005
+ - `stored: false` — handle keytar fallback:
1006
+
1007
+ Use AskUserQuestion:
1008
+ - header: "Keychain Unavailable"
1009
+ - question: "System keychain unavailable. API key will be stored unencrypted in ~/.claude.json (less secure). Confirm?\n\nLinux users: sudo apt install libsecret-1-dev gnome-keyring"
1010
+ - options:
1011
+ - "Store unencrypted in ~/.claude.json (less secure)"
1012
+ - "Skip — back to agent menu"
1013
+
1014
+ If "Skip — back to agent menu": display "No changes made." Return to Agent Sub-Menu.
1015
+
1016
+ If "Store unencrypted in ~/.claude.json (less secure)": write audit log then proceed to Step E with `method: env_block`:
1017
+ ```bash
1018
+ mkdir -p ~/.claude/debug
1019
+ node -e "
1020
+ const fs = require('fs');
1021
+ const ts = new Date().toISOString();
1022
+ const msg = ts + ' QGSD mcp-setup: keytar unavailable for ' + process.env.AGENT_KEY + ' — API key stored unencrypted in env block\n';
1023
+ fs.appendFileSync(require('os').homedir() + '/.claude/debug/mcp-setup-audit.log', msg);
1024
+ " AGENT_KEY="{agent-name}"
1025
+ ```
1026
+
1027
+ **Step E — Confirm + apply**
1028
+
1029
+ Show pending summary using the existing "Confirm + Apply + Restart Flow" pattern:
1030
+
1031
+ ```
1032
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1033
+ QGSD ► REVIEW PENDING CHANGES
1034
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1035
+
1036
+ ◆ {agent-name} — API key updated (stored in system keychain)
1037
+ ```
1038
+
1039
+ Use AskUserQuestion:
1040
+ - header: "Apply Key Change"
1041
+ - question: "Apply key change to ~/.claude.json and restart {agent-name}?"
1042
+ - options:
1043
+ - "Apply and restart"
1044
+ - "Cancel — discard changes"
1045
+
1046
+ If "Cancel — discard changes": display "Changes discarded." Return to Agent Sub-Menu.
1047
+
1048
+ If "Apply and restart":
1049
+
1050
+ 1. Backup ~/.claude.json:
1051
+ ```bash
1052
+ cp ~/.claude.json ~/.claude.json.backup-$(date +%Y-%m-%d-%H%M%S) 2>/dev/null || true
1053
+ ```
1054
+
1055
+ 2. Patch ANTHROPIC_API_KEY in the agent's env block. Pass key via environment variable only — never interpolate the value into the script body:
1056
+ ```bash
1057
+ node -e "
1058
+ const fs = require('fs');
1059
+ const path = require('path');
1060
+ const os = require('os');
1061
+ const claudeJsonPath = path.join(os.homedir(), '.claude.json');
1062
+ let claudeJson = {};
1063
+ try { claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8')); } catch (e) {}
1064
+ const agentName = process.env.AGENT_NAME;
1065
+ const apiKey = process.env.API_KEY;
1066
+ if (claudeJson.mcpServers && claudeJson.mcpServers[agentName]) {
1067
+ if (!claudeJson.mcpServers[agentName].env) claudeJson.mcpServers[agentName].env = {};
1068
+ claudeJson.mcpServers[agentName].env.ANTHROPIC_API_KEY = apiKey;
1069
+ }
1070
+ fs.writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2));
1071
+ process.stdout.write(JSON.stringify({ written: true }) + '\n');
1072
+ " AGENT_NAME="{agent-name}" API_KEY="{user-entered-key}"
1073
+ ```
1074
+
1075
+ 3. Sync all keytar secrets back to ~/.claude.json:
1076
+ ```bash
1077
+ node -e "
1078
+ const { syncToClaudeJson, SERVICE } = require('~/.claude/qgsd-bin/secrets.cjs');
1079
+ syncToClaudeJson(SERVICE).then(() => process.stdout.write('synced\n')).catch(e => process.stderr.write(e.message + '\n'));
1080
+ "
1081
+ ```
1082
+
1083
+ 4. Invoke `/qgsd:mcp-restart {agent-name}` (sequential). If restart fails, leave config written and display:
1084
+ ```
1085
+ ⚠ {agent-name}: restart failed. Config applied — reload on next Claude Code restart.
1086
+ Manual retry: /qgsd:mcp-restart {agent-name}
1087
+ ```
1088
+
1089
+ 5. Display:
1090
+ ```
1091
+ ✓ API key updated and agent restarted.
1092
+
1093
+ ✓ {agent-name} — key updated, restarted
1094
+
1095
+ Run /qgsd:mcp-status to verify agent health.
1096
+ ```
1097
+
1098
+ Return to Agent Sub-Menu (user can make further changes or go Back).
1099
+
1100
+ **Option 2 — Swap provider:**
1101
+
1102
+ **Step A — Show current provider**
1103
+
1104
+ Read the agent's current `ANTHROPIC_BASE_URL` from `~/.claude.json`:
1105
+
1106
+ ```bash
1107
+ CURRENT_PROVIDER=$(node -e "
1108
+ const fs = require('fs'), path = require('path'), os = require('os');
1109
+ let url = '—';
1110
+ try {
1111
+ const cj = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf8'));
1112
+ url = (cj.mcpServers && cj.mcpServers[process.env.AGENT_NAME] && cj.mcpServers[process.env.AGENT_NAME].env && cj.mcpServers[process.env.AGENT_NAME].env.ANTHROPIC_BASE_URL) || '—';
1113
+ } catch (e) {}
1114
+ process.stdout.write(url + '\n');
1115
+ " AGENT_NAME="{agent-name}")
1116
+ ```
1117
+
1118
+ Map the raw URL to a friendly provider name for display:
1119
+ - `https://api.akashml.com/v1` → `AkashML`
1120
+ - `https://api.together.xyz/v1` → `Together.xyz`
1121
+ - `https://api.fireworks.ai/inference/v1` → `Fireworks`
1122
+ - anything else → the raw URL value
1123
+
1124
+ **Step B — Prompt user with provider selection**
1125
+
1126
+ Use AskUserQuestion:
1127
+ - header: "Swap Provider — {agent-name}"
1128
+ - question: `"Current provider: {friendly-provider-name}\n\nSelect a new provider for {agent-name}:"`
1129
+ - options:
1130
+ - "1 — AkashML (https://api.akashml.com/v1)"
1131
+ - "2 — Together.xyz (https://api.together.xyz/v1)"
1132
+ - "3 — Fireworks (https://api.fireworks.ai/inference/v1)"
1133
+ - "4 — Custom URL"
1134
+ - "Skip — back to agent menu"
1135
+
1136
+ If "Skip — back to agent menu": display "No changes made." Return to Agent Sub-Menu.
1137
+
1138
+ **Step C — Resolve new URL**
1139
+
1140
+ For curated selections (1–3): resolve the canonical URL from the selection:
1141
+ - "1 — AkashML…" → `NEW_URL="https://api.akashml.com/v1"`, `NEW_PROVIDER_NAME="AkashML"`
1142
+ - "2 — Together.xyz…" → `NEW_URL="https://api.together.xyz/v1"`, `NEW_PROVIDER_NAME="Together.xyz"`
1143
+ - "3 — Fireworks…" → `NEW_URL="https://api.fireworks.ai/inference/v1"`, `NEW_PROVIDER_NAME="Fireworks"`
1144
+
1145
+ For "4 — Custom URL": use a second AskUserQuestion to collect the URL:
1146
+ - header: "Custom Provider URL — {agent-name}"
1147
+ - question: `"Enter the full base URL for the custom provider (e.g. https://openrouter.ai/api/v1):"`
1148
+ - options:
1149
+ - "Confirm URL"
1150
+ - "Cancel"
1151
+
1152
+ If "Cancel": display "No changes made." Return to Agent Sub-Menu.
1153
+
1154
+ Store the user-entered value as `NEW_URL` and `NEW_PROVIDER_NAME="custom"`.
1155
+
1156
+ **Step D — Confirm + apply**
1157
+
1158
+ Show pending summary:
1159
+
1160
+ ```
1161
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1162
+ QGSD ► REVIEW PENDING CHANGES
1163
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1164
+
1165
+ ◆ {agent-name} — provider changed to {NEW_PROVIDER_NAME} ({NEW_URL})
1166
+ ```
1167
+
1168
+ Use AskUserQuestion:
1169
+ - header: "Apply Provider Change"
1170
+ - question: "Apply provider change to ~/.claude.json and restart {agent-name}?"
1171
+ - options:
1172
+ - "Apply and restart"
1173
+ - "Cancel — discard changes"
1174
+
1175
+ If "Cancel — discard changes": display "Changes discarded." Return to Agent Sub-Menu.
1176
+
1177
+ If "Apply and restart":
1178
+
1179
+ 1. Backup ~/.claude.json:
1180
+ ```bash
1181
+ cp ~/.claude.json ~/.claude.json.backup-$(date +%Y-%m-%d-%H%M%S) 2>/dev/null || true
1182
+ ```
1183
+
1184
+ 2. Patch ANTHROPIC_BASE_URL in the agent's env block. Pass the new URL via environment variable — never interpolate into the script body:
1185
+ ```bash
1186
+ node -e "
1187
+ const fs = require('fs');
1188
+ const path = require('path');
1189
+ const os = require('os');
1190
+ const claudeJsonPath = path.join(os.homedir(), '.claude.json');
1191
+ let claudeJson = {};
1192
+ try { claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8')); } catch (e) {}
1193
+ const agentName = process.env.AGENT_NAME;
1194
+ const newUrl = process.env.NEW_URL;
1195
+ if (claudeJson.mcpServers && claudeJson.mcpServers[agentName]) {
1196
+ if (!claudeJson.mcpServers[agentName].env) claudeJson.mcpServers[agentName].env = {};
1197
+ claudeJson.mcpServers[agentName].env.ANTHROPIC_BASE_URL = newUrl;
1198
+ }
1199
+ fs.writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2));
1200
+ process.stdout.write(JSON.stringify({ written: true }) + '\n');
1201
+ " AGENT_NAME="{agent-name}" NEW_URL="{new-url}"
1202
+ ```
1203
+
1204
+ 3. Sync keytar secrets to ~/.claude.json:
1205
+ ```bash
1206
+ node -e "
1207
+ const { syncToClaudeJson, SERVICE } = require('~/.claude/qgsd-bin/secrets.cjs');
1208
+ syncToClaudeJson(SERVICE).then(() => process.stdout.write('synced\n')).catch(e => process.stderr.write(e.message + '\n'));
1209
+ "
1210
+ ```
1211
+
1212
+ 4. Invoke `/qgsd:mcp-restart {agent-name}` (sequential). If restart fails, leave config written and display:
1213
+ ```
1214
+ ⚠ {agent-name}: restart failed. Config applied — reload on next Claude Code restart.
1215
+ Manual retry: /qgsd:mcp-restart {agent-name}
1216
+ ```
1217
+
1218
+ 5. Display:
1219
+ ```
1220
+ ✓ Provider updated and agent restarted.
1221
+
1222
+ ✓ {agent-name} — provider changed to {NEW_PROVIDER_NAME}, restarted
1223
+
1224
+ Run /qgsd:mcp-status to verify agent health.
1225
+ ```
1226
+
1227
+ Return to Agent Sub-Menu (user can make further changes or go Back).
1228
+
1229
+ **Option 3 — Remove agent:**
1230
+
1231
+ **Step A — Confirm removal**
1232
+
1233
+ Display removal warning:
1234
+
1235
+ ```
1236
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1237
+ QGSD ► REMOVE AGENT
1238
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1239
+
1240
+ ⚠ This will permanently remove {agent-name} from
1241
+ ~/.claude.json. The agent will be deregistered on
1242
+ the next Claude Code restart.
1243
+ ```
1244
+
1245
+ Use AskUserQuestion:
1246
+ - header: "Remove Agent — {agent-name}"
1247
+ - question: `"Remove {agent-name} from ~/.claude.json?\n\nThis deletes the mcpServers entry. The agent process will be deregistered on the next Claude Code restart."`
1248
+ - options:
1249
+ - "Remove agent"
1250
+ - "Cancel — back to agent menu"
1251
+
1252
+ If "Cancel — back to agent menu": display "No changes made." Return to Agent Sub-Menu.
1253
+
1254
+ **Step B — Delete mcpServers entry**
1255
+
1256
+ If "Remove agent":
1257
+
1258
+ 1. Backup ~/.claude.json:
1259
+ ```bash
1260
+ cp ~/.claude.json ~/.claude.json.backup-$(date +%Y-%m-%d-%H%M%S) 2>/dev/null || true
1261
+ ```
1262
+
1263
+ 2. Delete the agent's mcpServers entry via inline node (agent name passed via env var — never interpolated):
1264
+ ```bash
1265
+ node -e "
1266
+ const fs = require('fs');
1267
+ const path = require('path');
1268
+ const os = require('os');
1269
+ const claudeJsonPath = path.join(os.homedir(), '.claude.json');
1270
+ let claudeJson = {};
1271
+ try { claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8')); } catch (e) {}
1272
+ const agentName = process.env.AGENT_NAME;
1273
+ if (claudeJson.mcpServers && claudeJson.mcpServers[agentName]) {
1274
+ delete claudeJson.mcpServers[agentName];
1275
+ }
1276
+ fs.writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2));
1277
+ process.stdout.write(JSON.stringify({ removed: true, agent: agentName }) + '\n');
1278
+ " AGENT_NAME="{agent-name}"
1279
+ ```
1280
+
1281
+ 3. Display:
1282
+ ```
1283
+ ✓ Agent removed.
1284
+
1285
+ ✓ {agent-name} — removed from ~/.claude.json
1286
+ The agent will be deregistered on next Claude Code restart.
1287
+
1288
+ Run /qgsd:mcp-status to verify the updated agent roster.
1289
+ ```
1290
+
1291
+ Return to roster display (re-read roster — removed agent no longer appears).
1292
+
1293
+ **Option "Back":** Return to Re-run Agent Menu.
1294
+
1295
+ ---
1296
+
1297
+ ## Confirm + Apply + Restart Flow
1298
+
1299
+ Used by actions that accumulate pending changes in a session.
1300
+
1301
+ Display pending summary:
1302
+
1303
+ ```
1304
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1305
+ QGSD ► REVIEW PENDING CHANGES
1306
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1307
+
1308
+ ◆ {agent-name} — {description of change}
1309
+ ```
1310
+
1311
+ Use AskUserQuestion:
1312
+ - header: "Apply Changes"
1313
+ - question: "Apply changes to ~/.claude.json and restart affected agents?"
1314
+ - options:
1315
+ - "Apply and restart"
1316
+ - "Cancel — discard changes"
1317
+
1318
+ If "Cancel": display "Changes discarded." Return to roster.
1319
+
1320
+ If "Apply and restart":
1321
+
1322
+ 1. Backup:
1323
+ ```bash
1324
+ cp ~/.claude.json ~/.claude.json.backup-$(date +%Y-%m-%d-%H%M%S) 2>/dev/null || true
1325
+ ```
1326
+
1327
+ 2. Write changes to `~/.claude.json` (inline node — read current, apply patch, write with 2-space indent).
1328
+
1329
+ 3. Sync keytar secrets:
1330
+ ```bash
1331
+ node -e "
1332
+ const { syncToClaudeJson, SERVICE } = require('~/.claude/qgsd-bin/secrets.cjs');
1333
+ syncToClaudeJson(SERVICE).then(() => process.stdout.write('synced\n')).catch(e => process.stderr.write(e.message + '\n'));
1334
+ "
1335
+ ```
1336
+
1337
+ 4. For each affected agent (sequential, one at a time): invoke `/qgsd:mcp-restart {agent-name}`.
1338
+
1339
+ 5. Display:
1340
+ ```
1341
+ ✓ Changes applied and agent(s) restarted.
1342
+
1343
+ ✓ {agent-name} — restarted
1344
+
1345
+ Run /qgsd:mcp-status to verify agent health.
1346
+ ```
1347
+
1348
+ If a restart fails, leave config written and display:
1349
+ ```
1350
+ ⚠ {agent-name}: restart failed. Config applied — reload on next Claude Code restart.
1351
+ Manual retry: /qgsd:mcp-restart {agent-name}
1352
+ ```
1353
+
1354
+ </process>
1355
+
1356
+ <success_criteria>
1357
+ - First-run (no mcpServers): welcome banner + agent template list + key collection (keytar/fallback) + batch-write + backup + restart + summary
1358
+ - Re-run (existing entries): numbered agent roster with model/provider/key-status columns
1359
+ - Sub-menu per agent: full API key set/update flow (Option 1), full provider swap flow (Option 2), full remove-agent flow (Option 3)
1360
+ - Add-agent flow (roster menu): template select (filtered) → key collection → mcpServers write → syncToClaudeJson → restart → identity ping
1361
+ - Remove-agent flow (Option 3): confirm → backup → delete mcpServers[agent] entry → write
1362
+ - Option 1 API key flow: key-status check → "(key stored)" hint → key input → keytar store → confirm → backup → patch ~/.claude.json → syncToClaudeJson → mcp-restart
1363
+ - Option 2 provider swap flow: current provider display → curated list (AkashML/Together.xyz/Fireworks) + Custom URL → confirm → backup → patch ANTHROPIC_BASE_URL → mcp-restart
1364
+ - Confirm+apply+restart: backup then write then sync keytar then mcp-restart per agent then confirmation
1365
+ - No changes applied without explicit user confirmation
1366
+ - Keytar failure: warning + Linux hint + confirmation before env-block fallback + audit log
1367
+ - Key value never appears in displayed text, log output, or shell history (passed via env var only)
1368
+ - Edit Quorum Composition flow (WIZ-08): re-run menu option "Edit Quorum Composition" → routes to Composition Screen
1369
+ - Composition toggle flow (WIZ-09): slot list with ● ON / ○ OFF indicators → toggle updates PENDING_ACTIVE → apply writes quorum_active to ~/.claude/qgsd.json → no restart required
1370
+ - Add slot from composition flow (WIZ-10): "Add new slot" → Step A/B/B-native → identity ping → return to Composition Screen showing new slot ● ON
1371
+ </success_criteria>