@mmerterden/multi-agent-pipeline 20.17.0 → 20.17.2

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 (83) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/docs/facts.json +6 -6
  3. package/docs/features.md +1 -1
  4. package/install/catalog-history.json +1 -1
  5. package/manifest.json +85 -47
  6. package/package.json +1 -1
  7. package/pipeline/commands/multi-agent/setup/SKILL.md +2 -2
  8. package/pipeline/commands/multi-agent/update/SKILL.md +2 -2
  9. package/pipeline/multi-agent-refs/features/usage-reporting.md +106 -67
  10. package/pipeline/schemas/prefs.schema.json +23 -2
  11. package/pipeline/scripts/usage-identity.mjs +312 -53
  12. package/pipeline/scripts/usage-register.mjs +204 -85
  13. package/pipeline/scripts/usage-report.mjs +7 -5
  14. package/pipeline/skills/.skill-manifest.json +35 -15
  15. package/pipeline/skills/.skills-index.json +66 -11
  16. package/pipeline/skills/shared/README.md +18 -13
  17. package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +2 -2
  18. package/pipeline/skills/shared/core/multi-agent-update/SKILL.md +2 -2
  19. package/pipeline/skills/shared/external/NOTICE-avdlee-swiftui-agent-skill.md +3 -1
  20. package/pipeline/skills/shared/external/NOTICE-dimillian-skills.md +2 -1
  21. package/pipeline/skills/shared/external/hig-app-icons/SKILL.md +159 -0
  22. package/pipeline/skills/shared/external/hig-app-icons/evals/evals.json +48 -0
  23. package/pipeline/skills/shared/external/hig-app-icons/evals/trigger-eval.json +22 -0
  24. package/pipeline/skills/shared/external/hig-app-icons/references/icon-workflow.md +115 -0
  25. package/pipeline/skills/shared/external/hig-components-layout/SKILL.md +32 -4
  26. package/pipeline/skills/shared/external/hig-components-layout/evals/evals.json +47 -0
  27. package/pipeline/skills/shared/external/hig-components-layout/evals/trigger-eval.json +22 -0
  28. package/pipeline/skills/shared/external/hig-components-layout/references/bars-and-tabs.md +231 -0
  29. package/pipeline/skills/shared/external/hig-components-layout/references/containers.md +5 -30
  30. package/pipeline/skills/shared/external/hig-components-status/SKILL.md +1 -1
  31. package/pipeline/skills/shared/external/hig-foundations/SKILL.md +23 -4
  32. package/pipeline/skills/shared/external/hig-foundations/evals/evals.json +47 -0
  33. package/pipeline/skills/shared/external/hig-foundations/evals/trigger-eval.json +21 -0
  34. package/pipeline/skills/shared/external/hig-foundations/references/foundations-detail.md +34 -26
  35. package/pipeline/skills/shared/external/hig-foundations/references/type-color-spacing.md +219 -0
  36. package/pipeline/skills/shared/external/hig-inputs/SKILL.md +5 -4
  37. package/pipeline/skills/shared/external/hig-patterns/SKILL.md +19 -3
  38. package/pipeline/skills/shared/external/hig-patterns/references/onboarding-and-states.md +188 -0
  39. package/pipeline/skills/shared/external/hig-patterns/references/patterns.md +5 -11
  40. package/pipeline/skills/shared/external/hig-platforms/SKILL.md +17 -5
  41. package/pipeline/skills/shared/external/hig-platforms/references/ipad-layout.md +218 -0
  42. package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +4 -1
  43. package/pipeline/skills/shared/external/ios-accessibility/references/large-content-and-appearance.md +135 -0
  44. package/pipeline/skills/shared/external/ios-design-review/SKILL.md +146 -0
  45. package/pipeline/skills/shared/external/ios-design-review/evals/evals.json +48 -0
  46. package/pipeline/skills/shared/external/ios-design-review/evals/trigger-eval.json +22 -0
  47. package/pipeline/skills/shared/external/ios-design-review/references/appearance-matrix.md +135 -0
  48. package/pipeline/skills/shared/external/ios-design-review/references/checklist.md +193 -0
  49. package/pipeline/skills/shared/external/ios-haptics/SKILL.md +191 -0
  50. package/pipeline/skills/shared/external/ios-haptics/evals/evals.json +49 -0
  51. package/pipeline/skills/shared/external/ios-haptics/evals/trigger-eval.json +22 -0
  52. package/pipeline/skills/shared/external/ios-haptics/references/core-haptics.md +215 -0
  53. package/pipeline/skills/shared/external/ios-iphone-duo/NOTICE.md +53 -0
  54. package/pipeline/skills/shared/external/ios-iphone-duo/SKILL.md +170 -0
  55. package/pipeline/skills/shared/external/ios-iphone-duo/evals/evals.json +50 -0
  56. package/pipeline/skills/shared/external/ios-iphone-duo/evals/trigger-eval.json +22 -0
  57. package/pipeline/skills/shared/external/ios-iphone-duo/references/api-map.md +212 -0
  58. package/pipeline/skills/shared/external/ios-iphone-duo/references/fixed-assumptions.md +145 -0
  59. package/pipeline/skills/shared/external/ios-iphone-duo/references/poses-and-panes.md +122 -0
  60. package/pipeline/skills/shared/external/ios-sf-symbols/SKILL.md +177 -0
  61. package/pipeline/skills/shared/external/ios-sf-symbols/evals/evals.json +49 -0
  62. package/pipeline/skills/shared/external/ios-sf-symbols/evals/trigger-eval.json +22 -0
  63. package/pipeline/skills/shared/external/ios-sf-symbols/references/effects-and-custom-symbols.md +135 -0
  64. package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +38 -50
  65. package/pipeline/skills/shared/external/swiftui-animation/evals/evals.json +47 -0
  66. package/pipeline/skills/shared/external/swiftui-animation/evals/trigger-eval.json +22 -0
  67. package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +7 -73
  68. package/pipeline/skills/shared/external/swiftui-animation/references/scroll-effects.md +185 -0
  69. package/pipeline/skills/shared/external/swiftui-expert-skill/SKILL.md +6 -6
  70. package/pipeline/skills/shared/external/swiftui-expert-skill/references/animation-advanced.md +3 -253
  71. package/pipeline/skills/shared/external/swiftui-expert-skill/references/animation-basics.md +4 -47
  72. package/pipeline/skills/shared/external/swiftui-expert-skill/references/animation-transitions.md +3 -111
  73. package/pipeline/skills/shared/external/swiftui-expert-skill/references/liquid-glass.md +11 -381
  74. package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +184 -224
  75. package/pipeline/skills/shared/external/swiftui-liquid-glass/evals/evals.json +47 -0
  76. package/pipeline/skills/shared/external/swiftui-liquid-glass/evals/trigger-eval.json +22 -0
  77. package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +246 -33
  78. package/pipeline/skills/shared/external/swiftui-liquid-glass/references/uikit-glass.md +227 -0
  79. package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +15 -18
  80. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/components-index.md +1 -1
  81. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/haptics.md +6 -68
  82. package/pipeline/skills/skills-index.md +16 -11
  83. package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +0 -786
@@ -18,15 +18,19 @@
18
18
  * - It never registers when `usageLog.optOut` is true. That is permanent and
19
19
  * checked before anything else, including any credential read (the
20
20
  * credential store and gh).
21
- * - It never registers a name it has not checked: the login sent is the
22
- * logged-in GitHub account that can read the pipeline repository
23
- * (usage-identity.mjs). No such account, no registration. The request
24
- * carries that account's gh token as `Authorization: Bearer` so the server
25
- * can confirm the caller owns the login; the server pins the login to the
26
- * token it issues.
27
- * - It never sends the gh token over plain http to anything but loopback, never
28
- * follows a redirect with it, never prints it, and never puts it on an argv:
29
- * gh prints it on its own stdout and it goes straight into the header.
21
+ * - It never registers a name it has not checked: the login sent is the GitHub
22
+ * account on this machine that can read the pipeline repository
23
+ * (usage-identity.mjs: gh, the git credential helper, or an SSH key). No such
24
+ * account, no registration. The request proves the caller owns the login:
25
+ * a gh or credential-helper token as `Authorization: Bearer`, or for an SSH
26
+ * key a signature over a one-time server challenge
27
+ * (`ssh-keygen -Y sign`), sent with no Authorization header at all. The
28
+ * server pins the login to the token it issues.
29
+ * - It never sends a GitHub token over plain http to anything but loopback,
30
+ * never follows a redirect with it, never prints it, never stores it, and
31
+ * never puts it on an argv: gh or git prints it on its own stdout and it goes
32
+ * straight into the header. The credential-helper token is read again at
33
+ * registration rather than kept from the identity check.
30
34
  * - It never fails a caller. Offline, endpoint down, ingest disabled by the
31
35
  * admin, no credential store: one status line, exit 0.
32
36
  * - It never reports run CONTENT. That is usage-report.mjs's contract, and it
@@ -35,22 +39,29 @@
35
39
  * Usage:
36
40
  * node usage-register.mjs [--prefs <path>] [--json] [--quiet] [--dry-run] [--feedback] [--refresh]
37
41
  *
38
- * `--refresh` re-checks which logged-in GitHub account can read the pipeline
39
- * repository instead of using the cached `usageLog.login`.
42
+ * `--refresh` re-checks which GitHub account on this machine can read the
43
+ * pipeline repository instead of using the cached `usageLog.login`.
40
44
  *
41
45
  * Exit: always 0. `--json` says what happened; the shell does not need to care.
42
46
  *
43
47
  * @module pipeline/scripts/usage-register
44
48
  */
45
49
 
46
- import { existsSync, readFileSync } from "node:fs";
50
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
47
51
  import { spawn, spawnSync } from "node:child_process";
48
52
  import { join } from "node:path";
49
- import { homedir, hostname } from "node:os";
53
+ import { homedir, hostname, tmpdir } from "node:os";
50
54
  import { pathToFileURL } from "node:url";
51
55
  import { hostCommand } from "../lib/user-facing.mjs";
52
56
  import { canonicalEndpoint } from "../lib/usage-endpoint.mjs";
53
- import { ghTokenFor, resolveReportingLogin } from "./usage-identity.mjs";
57
+ import {
58
+ SSH_TIMEOUT_MS,
59
+ credentialToken,
60
+ displayPath,
61
+ ghTokenFor,
62
+ resolveReportingLogin,
63
+ sshKeygenBin,
64
+ } from "./usage-identity.mjs";
54
65
  import { lockTimeoutFrom, updateJsonFileSync } from "../lib/json-file-lock.mjs";
55
66
 
56
67
  const DEFAULT_ENDPOINT = "https://mmerterden.com/api/usage/ingest";
@@ -66,10 +77,10 @@ function readJson(file) {
66
77
  }
67
78
 
68
79
  /**
69
- * The name this machine registers under: the logged-in GitHub account that can
70
- * read the pipeline repository (usage-identity.mjs), or null when none can.
71
- * Never the git identity, never the OS user, never the account gh merely has
72
- * active.
80
+ * The name this machine registers under: the GitHub account on this machine
81
+ * that can read the pipeline repository (usage-identity.mjs), with the method
82
+ * it was found by, or login null when none can. Never the git identity, never
83
+ * the OS user, never the account gh merely has active.
73
84
  */
74
85
  export function reportingUser(prefsPath, { env = process.env, refresh = false } = {}) {
75
86
  return resolveReportingLogin({ prefsPath, env, refresh });
@@ -82,6 +93,12 @@ export function reportingUser(prefsPath, { env = process.env, refresh = false }
82
93
  * them, and "you opted out" and "we could not reach the endpoint" are different
83
94
  * facts about the same silence.
84
95
  */
96
+ function pinned(u) {
97
+ return (
98
+ typeof u.registeredAs === "string" && u.registeredAs !== "" && u.registeredFallback !== true
99
+ );
100
+ }
101
+
85
102
  export function decideRegistration(
86
103
  prefs,
87
104
  {
@@ -90,7 +107,6 @@ export function decideRegistration(
90
107
  forFeedback = false,
91
108
  login = undefined,
92
109
  loginReason = null,
93
- fallback = false,
94
110
  } = {},
95
111
  ) {
96
112
  const g = prefs?.global || {};
@@ -103,14 +119,18 @@ export function decideRegistration(
103
119
  if (env.MULTI_AGENT_USAGE_TOKEN) return { act: false, reason: "token in environment" };
104
120
  if (typeof u.token === "string" && u.token.trim())
105
121
  return { act: false, reason: "token in prefs" };
122
+ // A registered token stays pinned to its login whatever this machine holds
123
+ // later, so an account verified once keeps naming the runs after it is logged
124
+ // out. A registration marked `registeredFallback` (20.17.0) was made with an
125
+ // account that cannot read the repository, so it is not pinned.
126
+ if (hasStoredToken && login === null && pinned(u)) {
127
+ return { act: false, reason: "token already onboarded" };
128
+ }
106
129
  if (login === null) {
107
- return { act: false, reason: loginReason || "no logged-in GitHub account has access" };
130
+ return { act: false, reason: loginReason || "no GitHub account on this machine has access" };
108
131
  }
109
- // A registered token stays pinned to its login whatever gh holds later: a
110
- // fallback account never replaces it, so a personal account verified once
111
- // keeps naming the runs after it is logged out.
112
- if (hasStoredToken && fallback && u.registeredAs) {
113
- return { act: false, reason: "token already onboarded" };
132
+ if (hasStoredToken && login && u.registeredFallback === true) {
133
+ return { act: true, reason: `replacing a fallback registration with ${login}` };
114
134
  }
115
135
  // The server pins a token to the login it was issued for. A token issued to a
116
136
  // different login, or before the login was verified, reports under the wrong
@@ -157,61 +177,148 @@ export function registerUrlAllowed(url, trustedHosts = []) {
157
177
  return trusted.has(u.hostname.toLowerCase());
158
178
  }
159
179
 
160
- async function requestToken(url, body, proof) {
180
+ async function postJson(url, body, bearer = null) {
161
181
  const ctrl = new AbortController();
162
182
  const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
183
+ const headers = { "Content-Type": "application/json" };
184
+ if (bearer) headers.Authorization = `Bearer ${bearer}`;
163
185
  try {
164
186
  const res = await fetch(url, {
165
187
  method: "POST",
166
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${proof}` },
188
+ headers,
167
189
  body: JSON.stringify(body),
168
190
  redirect: "error",
169
191
  signal: ctrl.signal,
170
192
  });
171
193
  const json = await res.json().catch(() => null);
172
- if (!res.ok) {
173
- const detail = typeof json?.error === "string" ? `: ${json.error.slice(0, 120)}` : "";
174
- if (json?.code === "login_mismatch") {
175
- return {
176
- token: null,
177
- why: `the gh token does not belong to ${body.u}; run \`gh auth status\` and retry with --refresh`,
178
- };
179
- }
180
- if (res.status === 401) {
181
- return {
182
- token: null,
183
- why:
184
- `endpoint answered ${res.status}${detail} - GitHub did not accept the token for ${body.u}; ` +
185
- `check \`gh auth status\` lists that account, then retry with --refresh`,
186
- };
187
- }
188
- if (res.status === 403) {
189
- return {
190
- token: null,
191
- why:
192
- `endpoint answered 403${detail} - ${body.u} needs collaborator access to the pipeline repository; ` +
193
- `add the account that has it with \`gh auth login\`, then rerun with --refresh`,
194
- };
195
- }
196
- return { token: null, why: `endpoint answered ${res.status}${detail}` };
197
- }
198
- if (json?.accepted === false)
199
- return { token: null, declined: true, why: "the endpoint declined this login" };
200
- const token = typeof json?.token === "string" ? json.token.trim() : "";
201
- const pinned = typeof json?.u === "string" && json.u.trim() ? json.u.trim() : null;
202
- return token
203
- ? { token, why: null, login: pinned }
204
- : { token: null, why: "endpoint returned no token" };
194
+ return { res, json, failure: null };
205
195
  } catch (e) {
206
196
  return {
207
- token: null,
208
- why: e?.name === "AbortError" ? "endpoint timed out" : "endpoint unreachable",
197
+ res: null,
198
+ json: null,
199
+ failure: e?.name === "AbortError" ? "endpoint timed out" : "endpoint unreachable",
209
200
  };
210
201
  } finally {
211
202
  clearTimeout(timer);
212
203
  }
213
204
  }
214
205
 
206
+ const SSH_REFUSALS = {
207
+ ssh_signature_invalid: (u) =>
208
+ `the server could not verify the SSH signature for ${u}; retry with --refresh`,
209
+ ssh_key_not_on_account: (u) =>
210
+ `the SSH key is not on ${u}'s GitHub account; add its .pub under GitHub Settings > SSH keys, then retry with --refresh`,
211
+ ssh_key_unsupported: (u) =>
212
+ `the server does not accept this SSH key type for ${u}; put an ed25519, rsa or ecdsa-p256 key on that account, then retry with --refresh`,
213
+ };
214
+
215
+ function refusal(status, json, body, method) {
216
+ const detail = typeof json?.error === "string" ? `: ${json.error.slice(0, 120)}` : "";
217
+ const code = typeof json?.code === "string" ? json.code : "";
218
+ if (SSH_REFUSALS[code]) return `endpoint answered ${status} - ${SSH_REFUSALS[code](body.u)}`;
219
+ if (code === "login_mismatch") {
220
+ return method === "gh"
221
+ ? `the gh token does not belong to ${body.u}; run \`gh auth status\` and retry with --refresh`
222
+ : `the proof does not belong to ${body.u}; retry with --refresh`;
223
+ }
224
+ if (status === 401) {
225
+ const fix =
226
+ method === "gh"
227
+ ? "check `gh auth status` lists that account, then retry with --refresh"
228
+ : "retry with --refresh";
229
+ return `endpoint answered ${status}${detail} - GitHub did not accept the proof for ${body.u}; ${fix}`;
230
+ }
231
+ if (status === 403) {
232
+ return (
233
+ `endpoint answered 403${detail} - ${body.u} needs collaborator access to the pipeline repository; ` +
234
+ "log in the account that has it (`gh auth login`, or add its SSH key), then rerun with --refresh"
235
+ );
236
+ }
237
+ return `endpoint answered ${status}${detail}`;
238
+ }
239
+
240
+ async function requestToken(url, body, proof, method) {
241
+ const { res, json, failure } = await postJson(url, body, proof);
242
+ if (failure) return { token: null, why: failure };
243
+ if (!res.ok) return { token: null, why: refusal(res.status, json, body, method) };
244
+ if (json?.accepted === false)
245
+ return { token: null, declined: true, why: "the endpoint declined this login" };
246
+ const token = typeof json?.token === "string" ? json.token.trim() : "";
247
+ const pinnedLogin = typeof json?.u === "string" && json.u.trim() ? json.u.trim() : null;
248
+ return token
249
+ ? { token, why: null, login: pinnedLogin }
250
+ : { token: null, why: "endpoint returned no token" };
251
+ }
252
+
253
+ /**
254
+ * Sign `challenge` with the SSH key at `key` under `namespace`. The exact
255
+ * challenge bytes go into a 0600 file in a fresh temp dir that is removed
256
+ * afterwards. stdin is closed and askpass is disabled, so a key that needs a
257
+ * passphrase fails at once; the `.pub` is then tried, which signs through
258
+ * ssh-agent when the key is loaded there. Returns the armored signature or null.
259
+ */
260
+ export function sshSign(key, namespace, challenge, env = process.env) {
261
+ let dir = null;
262
+ try {
263
+ dir = mkdtempSync(join(tmpdir(), "ma-usage-sign-"));
264
+ const file = join(dir, "challenge");
265
+ writeFileSync(file, challenge, { mode: 0o600 });
266
+ const tries = key.endsWith(".pub") ? [key] : [key, `${key}.pub`];
267
+ for (const f of tries) {
268
+ if (!existsSync(f)) continue;
269
+ rmSync(`${file}.sig`, { force: true });
270
+ const r = spawnSync(sshKeygenBin(env), ["-Y", "sign", "-f", f, "-n", namespace, file], {
271
+ encoding: "utf8",
272
+ timeout: SSH_TIMEOUT_MS,
273
+ stdio: ["ignore", "ignore", "ignore"],
274
+ env: { ...env, SSH_ASKPASS_REQUIRE: "never" },
275
+ });
276
+ if (r.status === 0 && existsSync(`${file}.sig`)) {
277
+ const sig = readFileSync(`${file}.sig`, "utf8");
278
+ if (sig.includes("BEGIN SSH SIGNATURE")) return sig;
279
+ }
280
+ }
281
+ return null;
282
+ } catch {
283
+ return null;
284
+ } finally {
285
+ if (dir) rmSync(dir, { recursive: true, force: true });
286
+ }
287
+ }
288
+
289
+ /** The challenge URL next to the register URL. */
290
+ export function challengeUrl(url) {
291
+ return `${url.replace(/\/+$/, "")}/challenge`;
292
+ }
293
+
294
+ async function requestTokenWithSsh(url, body, key) {
295
+ const ch = await postJson(challengeUrl(url), { u: body.u });
296
+ if (ch.failure) return { token: null, why: ch.failure };
297
+ if (!ch.res.ok) {
298
+ if (ch.res.status === 404 || ch.res.status === 405) {
299
+ return {
300
+ token: null,
301
+ why: `the endpoint does not accept an SSH key as proof yet (challenge answered ${ch.res.status})`,
302
+ };
303
+ }
304
+ return { token: null, why: refusal(ch.res.status, ch.json, body, "ssh") };
305
+ }
306
+ const challenge = typeof ch.json?.challenge === "string" ? ch.json.challenge : "";
307
+ const namespace = typeof ch.json?.namespace === "string" ? ch.json.namespace : "";
308
+ if (!challenge || !/^[A-Za-z0-9@._-]{1,128}$/.test(namespace)) {
309
+ return { token: null, why: "the endpoint returned no usable SSH challenge" };
310
+ }
311
+ const signature = sshSign(key, namespace, challenge);
312
+ if (!signature) {
313
+ const shown = displayPath(key);
314
+ return {
315
+ token: null,
316
+ why: `could not sign with the SSH key ${shown} (it needs a passphrase and is not in ssh-agent); run \`ssh-add ${shown}\`, then retry with --refresh`,
317
+ };
318
+ }
319
+ return requestToken(url, { ...body, ssh: { challenge, signature } }, null, "ssh");
320
+ }
321
+
215
322
  /**
216
323
  * Write the switch, and the credential-store ENTRY NAME when there is one.
217
324
  *
@@ -225,7 +332,7 @@ async function requestToken(url, body, proof) {
225
332
  export function persistPrefs(
226
333
  prefsPath,
227
334
  entryName = null,
228
- { enableTelemetry = true, registeredAs = null, registeredFallback = false } = {},
335
+ { enableTelemetry = true, registeredAs = null } = {},
229
336
  ) {
230
337
  try {
231
338
  const written = updateJsonFileSync(
@@ -241,7 +348,7 @@ export function persistPrefs(
241
348
  if (enableTelemetry) j.global.usageLog.enabled = true;
242
349
  if (registeredAs) {
243
350
  j.global.usageLog.registeredAs = registeredAs;
244
- j.global.usageLog.registeredFallback = registeredFallback === true;
351
+ delete j.global.usageLog.registeredFallback;
245
352
  j.global.usageLog.registeredAt = new Date().toISOString();
246
353
  }
247
354
  return j;
@@ -255,16 +362,25 @@ export function persistPrefs(
255
362
  }
256
363
 
257
364
  /**
258
- * True while this machine's runs are not pinned to an account that can read the
259
- * pipeline repository: no registration, or one made with a fallback account.
260
- * Setup and update read it from `--json` and offer to add that account once.
365
+ * True when this machine's runs are not pinned to a verified account AND no
366
+ * GitHub account on it can read the pipeline repository (no gh account, no
367
+ * credential-helper token and no SSH key with access). `login` is the identity
368
+ * check's answer when it ran; otherwise the cached `usageLog.login` stands in.
369
+ * Setup and update read it from `--json` and offer the fix once.
261
370
  */
262
- export function personalAccountMissing(prefs) {
371
+ export function personalAccountMissing(prefs, login = undefined, env = process.env) {
263
372
  const u = prefs?.global?.usageLog || {};
264
373
  if (u.optOut === true) return false;
265
- if (process.env.MULTI_AGENT_USAGE_TOKEN) return false;
374
+ if (env.MULTI_AGENT_USAGE_TOKEN) return false;
266
375
  if (typeof u.token === "string" && u.token.trim()) return false;
267
- return !(typeof u.registeredAs === "string" && u.registeredAs && u.registeredFallback !== true);
376
+ if (pinned(u)) return false;
377
+ const found =
378
+ login !== undefined
379
+ ? login
380
+ : u.loginFallback === true || typeof u.login !== "string"
381
+ ? null
382
+ : u.login;
383
+ return !found;
268
384
  }
269
385
 
270
386
  /**
@@ -326,9 +442,10 @@ async function main(argv) {
326
442
  const say = (line) => {
327
443
  if (!opts.quiet) process.stdout.write(`${line}\n`);
328
444
  };
445
+ const identity = { login: undefined };
329
446
  const done = (status, detail) => {
330
447
  if (opts.json) {
331
- const needsPersonalAccount = personalAccountMissing(readJson(prefsPath));
448
+ const needsPersonalAccount = personalAccountMissing(readJson(prefsPath), identity.login);
332
449
  process.stdout.write(`${JSON.stringify({ status, detail, needsPersonalAccount })}\n`);
333
450
  }
334
451
  if (opts.quiet && reportingStaysOff(status, detail)) {
@@ -358,13 +475,13 @@ async function main(argv) {
358
475
  const who = identityMatters
359
476
  ? reportingUser(prefsPath, { refresh: opts.refresh })
360
477
  : { login: undefined, reason: null };
478
+ identity.login = who.login;
361
479
  const current = readJson(prefsPath) || prefs;
362
480
  const decision = decideRegistration(current, {
363
481
  hasStoredToken: has,
364
482
  forFeedback: opts.feedback,
365
483
  login: who.login,
366
484
  loginReason: who.reason,
367
- fallback: who.fallback === true,
368
485
  });
369
486
  if (!decision.act) {
370
487
  const repairable = ["token in environment", "token in prefs", "token already onboarded"];
@@ -413,19 +530,22 @@ async function main(argv) {
413
530
  return done("dry-run", url);
414
531
  }
415
532
 
416
- const proof = ghTokenFor(user);
417
- if (!proof) {
418
- const why = `gh has no token for ${user}; run \`gh auth login\` for that account`;
419
- say(` -> operational reporting left off (${why})`);
420
- return done("unavailable", why);
533
+ let result;
534
+ if (who.method === "ssh") {
535
+ result = await requestTokenWithSsh(url, { u: user, c: host }, who.key);
536
+ } else {
537
+ const proof = who.method === "credential" ? credentialToken() : ghTokenFor(user);
538
+ if (!proof) {
539
+ const why =
540
+ who.method === "credential"
541
+ ? `the git credential helper no longer returns a GitHub token for ${user}; retry with --refresh`
542
+ : `gh has no token for ${user}; run \`gh auth login\` for that account`;
543
+ say(` -> operational reporting left off (${why})`);
544
+ return done("unavailable", why);
545
+ }
546
+ result = await requestToken(url, { u: user, c: host }, proof, who.method || "gh");
421
547
  }
422
-
423
- const {
424
- token,
425
- why,
426
- declined,
427
- login: pinnedLogin,
428
- } = await requestToken(url, { u: user, c: host }, proof);
548
+ const { token, why, declined, login: pinnedLogin } = result;
429
549
  if (declined) {
430
550
  say(` -> operational reporting left off (${why})`);
431
551
  return done("declined", why);
@@ -449,7 +569,6 @@ async function main(argv) {
449
569
  persistPrefs(prefsPath, entry, {
450
570
  enableTelemetry: !optedOut,
451
571
  registeredAs: pinnedLogin || user,
452
- registeredFallback: who.fallback === true,
453
572
  });
454
573
  if (optedOut) {
455
574
  say(" -> feedback token registered; telemetry stays off (usageLog.optOut is set)");
@@ -2,8 +2,9 @@
2
2
  * usage-report.mjs - optional, opt-in per-run operational reporting.
3
3
  *
4
4
  * Off by default; a no-op unless prefs.global.usageLog.enabled is true, a
5
- * token resolves, and a logged-in GitHub account can read the pipeline
6
- * repository (usage-identity.mjs). Emits one compact run record keyed by run
5
+ * token resolves, and a GitHub account on this machine (gh, the git
6
+ * credential helper or an SSH key) can read the pipeline repository
7
+ * (usage-identity.mjs). Emits one compact run record keyed by run
7
8
  * id (`taskId`), upserted at every phase transition and at completion, halt
8
9
  * and park, fire-and-forget - never throws, never delays a run.
9
10
  * Only coarse run metadata is included; never prompts, code, diffs, token
@@ -664,9 +665,9 @@ function appendErrorLedger(event) {
664
665
  }
665
666
  }
666
667
 
667
- // Telemetry identity is the GitHub account that can read the pipeline
668
- // repository (usage-identity.mjs), never the git identity.name and never the
669
- // account gh merely has active. The server replaces it with the login pinned to
668
+ // Telemetry identity is the GitHub account on this machine that can read the
669
+ // pipeline repository (usage-identity.mjs), never the git identity.name and
670
+ // never the account gh merely has active. The server replaces it with the login pinned to
670
671
  // the ingest token, so this value is informational.
671
672
  function resolveUser(opts = {}) {
672
673
  return resolveReportingLogin(opts).login;
@@ -843,6 +844,7 @@ async function reportInstalledVersion(endpoint, token, login, usageLog) {
843
844
  * no gh call to name its runs, and keeps the name after that account logs out.
844
845
  */
845
846
  function registeredLogin(prefs) {
847
+ if (prefs?.registeredFallback === true) return null;
846
848
  const login = typeof prefs?.registeredAs === "string" ? prefs.registeredAs.trim() : "";
847
849
  return /^[A-Za-z0-9][A-Za-z0-9_-]{0,38}$/.test(login) ? { login, reason: "registered" } : null;
848
850
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-10-07T15:02:16Z",
4
- "skillCount": 223,
3
+ "generatedAt": "2026-10-08T12:53:56Z",
4
+ "skillCount": 228,
5
5
  "entries": [
6
6
  {
7
7
  "path": "shared/core/apple-archive-compliance/SKILL.md",
@@ -213,7 +213,7 @@
213
213
  },
214
214
  {
215
215
  "path": "shared/core/multi-agent-setup/SKILL.md",
216
- "sha256": "28c00a0a32b5d636240ced308983b4aaee919016693a686a18ccb0e1e9103ec3"
216
+ "sha256": "427df811e5130da1ab6e23ff78202eb9134fb76bcd601924fba5668cbd630158"
217
217
  },
218
218
  {
219
219
  "path": "shared/core/multi-agent-stack/SKILL.md",
@@ -265,7 +265,7 @@
265
265
  },
266
266
  {
267
267
  "path": "shared/core/multi-agent-update/SKILL.md",
268
- "sha256": "37f5950dca1a09b1ca0c2e2b1b2d20de418f6398a8aed37b062917cf6d95b4a5"
268
+ "sha256": "013317432263490a01d081b657928b977012afb78f75a64d278d94d2daa8e894"
269
269
  },
270
270
  {
271
271
  "path": "shared/core/multi-agent/SKILL.md",
@@ -495,17 +495,21 @@
495
495
  "path": "shared/external/healthkit/SKILL.md",
496
496
  "sha256": "fece1986bc35925e34fed8d8cb13aa59a3577b8cedefdc02c736c0d4a5d38aff"
497
497
  },
498
+ {
499
+ "path": "shared/external/hig-app-icons/SKILL.md",
500
+ "sha256": "70168a30347b1c51ff72ed30b54443add2e1ad63a6aae0aa79bb32bd87d00c4d"
501
+ },
498
502
  {
499
503
  "path": "shared/external/hig-components-content/SKILL.md",
500
504
  "sha256": "910e4e24c47a3e103b3d398a1c49684f0990966423e2ea2d871111d6251d8a6c"
501
505
  },
502
506
  {
503
507
  "path": "shared/external/hig-components-layout/SKILL.md",
504
- "sha256": "6b2fbc80d37a12f869278b9b281d28f42f5b1dfaa38bdf271fa1d5823320a179"
508
+ "sha256": "dde0a923736255aa2e5de791700fc51945d3c63f41fc7ec15e252953bd7cf168"
505
509
  },
506
510
  {
507
511
  "path": "shared/external/hig-components-status/SKILL.md",
508
- "sha256": "52d4fb97f8709b64ec9bfbd165eaaa6fdf37a1413a8d518031eac1931d10255a"
512
+ "sha256": "8458f7de5e4f3407e162ffe5c4831ce7fb3e93ee4922bfb4d45444cfa31f59fb"
509
513
  },
510
514
  {
511
515
  "path": "shared/external/hig-components-system/SKILL.md",
@@ -513,19 +517,19 @@
513
517
  },
514
518
  {
515
519
  "path": "shared/external/hig-foundations/SKILL.md",
516
- "sha256": "1ef63dd95e6ac65297b5f7b05f687bbf46464f6bde9dd59b545e10f256885b89"
520
+ "sha256": "e0a345ef1e25b9741dc848d9aac9c5bb045a69034b22348036d769adb8441f06"
517
521
  },
518
522
  {
519
523
  "path": "shared/external/hig-inputs/SKILL.md",
520
- "sha256": "8d12e3ea11963f038cfc7a3f77a4469e5511cb22c2564caa68ca3ce1d11d9ba0"
524
+ "sha256": "ec8ec26f5a4469baa72aaa59def64a44891b1655d1ef6a1936bd4d29d926b290"
521
525
  },
522
526
  {
523
527
  "path": "shared/external/hig-patterns/SKILL.md",
524
- "sha256": "9028a1f659f5f96f5fc8f16ae9715e8058b5b049bda8a4eafc096c30c68b66f3"
528
+ "sha256": "771a08a0ba8d6268ea4139fc2cde4a99750cb2c31a78b755a41fae78efd742c9"
525
529
  },
526
530
  {
527
531
  "path": "shared/external/hig-platforms/SKILL.md",
528
- "sha256": "14a4c122dce4ce56f70e9f6c5da8bed30585a22f54ec2c586185c85e9cbfb1a5"
532
+ "sha256": "647da7ec54103b5b93198506e254d7468150fc24b880128a1bab7856390eaf25"
529
533
  },
530
534
  {
531
535
  "path": "shared/external/hig-technologies/SKILL.md",
@@ -545,7 +549,7 @@
545
549
  },
546
550
  {
547
551
  "path": "shared/external/ios-accessibility/SKILL.md",
548
- "sha256": "5331d7031d6e8ba54008e5239a7d1209a2046538717c1f86ea270d5d40057368"
552
+ "sha256": "8dc07ddb1e18d0d0572967ee77b39ffe36342ffc781ffb9a3910c4a2d8ee41fe"
549
553
  },
550
554
  {
551
555
  "path": "shared/external/ios-coding-standard/SKILL.md",
@@ -555,6 +559,18 @@
555
559
  "path": "shared/external/ios-debugger-agent/SKILL.md",
556
560
  "sha256": "2126e7211042209922a848d5e24589b893c4d3f5da658d81c166e5c1347f4b3f"
557
561
  },
562
+ {
563
+ "path": "shared/external/ios-design-review/SKILL.md",
564
+ "sha256": "cdef9893d29fded21c87d39cc30075d7b21c79d07b6cca5386e6e1fca35deb2a"
565
+ },
566
+ {
567
+ "path": "shared/external/ios-haptics/SKILL.md",
568
+ "sha256": "5edfc54f3d7733b6c48d1c9d30a3bbd30a0d5437be1dd6427e817b6e81f18150"
569
+ },
570
+ {
571
+ "path": "shared/external/ios-iphone-duo/SKILL.md",
572
+ "sha256": "2da65582a8d86957ded84345e60095584a26942f9ded8f61e901f3d04fb66c37"
573
+ },
558
574
  {
559
575
  "path": "shared/external/ios-localization/SKILL.md",
560
576
  "sha256": "3ee1a34bbf6c0b77e1e9f16cb7d8bc3d10f2ba7bf6d0b48e64126afcf9dbe2b6"
@@ -571,6 +587,10 @@
571
587
  "path": "shared/external/ios-security/SKILL.md",
572
588
  "sha256": "48924d4faea93fdf435851cbc77ba26cb33f3f8256954787af539f410f64d5c0"
573
589
  },
590
+ {
591
+ "path": "shared/external/ios-sf-symbols/SKILL.md",
592
+ "sha256": "5fa9333a5d9699efba6389a9529aca88a38e2d8de873a129d4fead809143f1ed"
593
+ },
574
594
  {
575
595
  "path": "shared/external/ios-simulator/SKILL.md",
576
596
  "sha256": "f7cb08384d448e3e73b0373e19f3ee1eca90b43eb7a3e3ac5d59a74035c0fc14"
@@ -773,11 +793,11 @@
773
793
  },
774
794
  {
775
795
  "path": "shared/external/swiftui-animation/SKILL.md",
776
- "sha256": "49f49c590f4304ef0905a890b2215d520ff187f8e1b9e68535cdd6099f51892b"
796
+ "sha256": "d67e447c1b2cc002f007b46f68a15a1181a13d5ed16078b79333ebf48d31b329"
777
797
  },
778
798
  {
779
799
  "path": "shared/external/swiftui-expert-skill/SKILL.md",
780
- "sha256": "1fc036e37dd3708fbe333a231e26b5075017b50860403f095399b3b5a7ee4d11"
800
+ "sha256": "c6d5ee097036edf12c6c1b5aed64b01bdafb2eb240169c82de2e951b33997cf6"
781
801
  },
782
802
  {
783
803
  "path": "shared/external/swiftui-gestures/SKILL.md",
@@ -789,7 +809,7 @@
789
809
  },
790
810
  {
791
811
  "path": "shared/external/swiftui-liquid-glass/SKILL.md",
792
- "sha256": "bbcaeda80b6c052a521a63880f0f2f6ddabc589d4c65455b4286a26b4cce4808"
812
+ "sha256": "555aa9c8178f4f14484ff8adee57ea80621be48b6b0628fe9852277e37349048"
793
813
  },
794
814
  {
795
815
  "path": "shared/external/swiftui-navigation/SKILL.md",
@@ -797,7 +817,7 @@
797
817
  },
798
818
  {
799
819
  "path": "shared/external/swiftui-patterns/SKILL.md",
800
- "sha256": "35c710faa4ff459f6349fcbc422bc53d4cad4c485b2b592e4fb96414778777f6"
820
+ "sha256": "0cea41552443a7ee86928b7666ca99c76447a56d1e8f54cca1292e063d966730"
801
821
  },
802
822
  {
803
823
  "path": "shared/external/swiftui-performance-audit/SKILL.md",