@mmerterden/multi-agent-pipeline 20.22.0 → 20.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/docs/adr/0010-own-code-graph.md +3 -2
  3. package/docs/facts.json +3 -3
  4. package/docs/features.md +2 -2
  5. package/install/_usage-lifecycle.mjs +44 -0
  6. package/install/catalog-history.json +1 -1
  7. package/install/index.mjs +2 -0
  8. package/manifest.json +52 -49
  9. package/package.json +1 -1
  10. package/pipeline/commands/multi-agent/create-jira/SKILL.md +1 -1
  11. package/pipeline/commands/multi-agent/setup/SKILL.md +3 -3
  12. package/pipeline/commands/multi-agent/update/SKILL.md +3 -3
  13. package/pipeline/lib/json-file-lock.mjs +24 -2
  14. package/pipeline/multi-agent-refs/analysis/evidence.md +1 -1
  15. package/pipeline/multi-agent-refs/cross-cli-contract.md +1 -0
  16. package/pipeline/multi-agent-refs/features/analysis-jira.md +5 -0
  17. package/pipeline/multi-agent-refs/features/code-graph.md +40 -3
  18. package/pipeline/multi-agent-refs/features/usage-reporting.md +40 -23
  19. package/pipeline/multi-agent-refs/generate-issue.md +11 -1
  20. package/pipeline/multi-agent-refs/knowledge.md +18 -0
  21. package/pipeline/multi-agent-refs/phases/phase-1-plan.md +2 -2
  22. package/pipeline/schemas/code-graph.schema.json +3 -3
  23. package/pipeline/schemas/prefs.schema.json +26 -4
  24. package/pipeline/scripts/_code-graph.mjs +121 -12
  25. package/pipeline/scripts/_diff-impact.mjs +12 -5
  26. package/pipeline/scripts/_supertypes.mjs +133 -0
  27. package/pipeline/scripts/analysis-story-body.mjs +24 -8
  28. package/pipeline/scripts/capture-flush.sh +4 -1
  29. package/pipeline/scripts/code-graph-rules/android.json +1 -0
  30. package/pipeline/scripts/code-graph-rules/ios.json +5 -0
  31. package/pipeline/scripts/code-graph-rules/node.json +1 -0
  32. package/pipeline/scripts/code-graph-rules/python.json +1 -0
  33. package/pipeline/scripts/git-path.mjs +21 -0
  34. package/pipeline/scripts/graph-affected.mjs +2 -1
  35. package/pipeline/scripts/graph-confirm.mjs +140 -0
  36. package/pipeline/scripts/learnings-ledger.mjs +3 -2
  37. package/pipeline/scripts/product-context.mjs +30 -19
  38. package/pipeline/scripts/symbol-existence-gate.mjs +21 -9
  39. package/pipeline/scripts/test-gap-scan.mjs +4 -1
  40. package/pipeline/scripts/test-strength.mjs +7 -8
  41. package/pipeline/scripts/usage-identity.mjs +1 -1
  42. package/pipeline/scripts/usage-register.mjs +181 -15
  43. package/pipeline/scripts/validate-code-graph.mjs +3 -3
  44. package/pipeline/scripts/valven-gate.mjs +9 -1
  45. package/pipeline/scripts/valven-port.mjs +16 -5
  46. package/pipeline/scripts/valven-split.mjs +6 -4
  47. package/pipeline/skills/.skill-manifest.json +3 -3
  48. package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +3 -3
  49. package/pipeline/skills/shared/core/multi-agent-update/SKILL.md +3 -3
@@ -18,10 +18,12 @@
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 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:
21
+ * - It registers the GitHub account on this machine that can read the
22
+ * pipeline repository (usage-identity.mjs: gh, the git credential helper, or
23
+ * an SSH key). With no such account it registers only a name the person
24
+ * typed (`--login`, kept as `usageLog.declaredLogin`), as a declared claim
25
+ * the server accepts for a collaborator and marks `claimed`. Otherwise the
26
+ * request proves the caller owns the login:
25
27
  * a gh or credential-helper token as `Authorization: Bearer`, or for an SSH
26
28
  * key a signature over a one-time server challenge
27
29
  * (`ssh-keygen -Y sign`), sent with no Authorization header at all. The
@@ -37,11 +39,14 @@
37
39
  * is coarse metadata only.
38
40
  *
39
41
  * Usage:
40
- * node usage-register.mjs [--prefs <path>] [--json] [--quiet] [--dry-run] [--feedback] [--refresh]
42
+ * node usage-register.mjs [--prefs <path>] [--json] [--quiet] [--dry-run] [--feedback] [--refresh] [--login <name>]
41
43
  *
42
44
  * `--refresh` re-checks which GitHub account on this machine can read the
43
45
  * pipeline repository instead of using the cached `usageLog.login`.
44
46
  *
47
+ * `--login <name>` records the person's GitHub username, for a machine that
48
+ * proves no account (a work machine), and implies `--refresh`.
49
+ *
45
50
  * Exit: always 0. `--json` says what happened; the shell does not need to care.
46
51
  *
47
52
  * @module pipeline/scripts/usage-register
@@ -49,12 +54,14 @@
49
54
 
50
55
  import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
51
56
  import { spawn, spawnSync } from "node:child_process";
57
+ import { randomBytes } from "node:crypto";
52
58
  import { join } from "node:path";
53
59
  import { homedir, hostname, tmpdir } from "node:os";
54
60
  import { pathToFileURL } from "node:url";
55
61
  import { hostCommand } from "../lib/user-facing.mjs";
56
62
  import { canonicalEndpoint } from "../lib/usage-endpoint.mjs";
57
63
  import {
64
+ LOGIN_RE,
58
65
  SSH_TIMEOUT_MS,
59
66
  credentialToken,
60
67
  displayPath,
@@ -82,8 +89,11 @@ function readJson(file) {
82
89
  * it was found by, or login null when none can. Never the git identity, never
83
90
  * the OS user, never the account gh merely has active.
84
91
  */
85
- export function reportingUser(prefsPath, { env = process.env, refresh = false } = {}) {
86
- return resolveReportingLogin({ prefsPath, env, refresh });
92
+ export function reportingUser(
93
+ prefsPath,
94
+ { env = process.env, refresh = false, write = true } = {},
95
+ ) {
96
+ return resolveReportingLogin({ prefsPath, env, refresh, write });
87
97
  }
88
98
 
89
99
  /**
@@ -107,6 +117,7 @@ export function decideRegistration(
107
117
  forFeedback = false,
108
118
  login = undefined,
109
119
  loginReason = null,
120
+ proven = true,
110
121
  } = {},
111
122
  ) {
112
123
  const g = prefs?.global || {};
@@ -129,13 +140,21 @@ export function decideRegistration(
129
140
  if (login === null) {
130
141
  return { act: false, reason: loginReason || "no GitHub account on this machine has access" };
131
142
  }
143
+ if (hasStoredToken && login && proven && u.registeredClaim === true) {
144
+ return { act: true, reason: `replacing a declared registration with a proof for ${login}` };
145
+ }
132
146
  if (hasStoredToken && login && u.registeredFallback === true) {
133
147
  return { act: true, reason: `replacing a fallback registration with ${login}` };
134
148
  }
135
149
  // The server pins a token to the login it was issued for. A token issued to a
136
150
  // different login, or before the login was verified, reports under the wrong
137
151
  // name, so it is replaced.
138
- if (hasStoredToken && login && u.registeredAs && u.registeredAs !== login) {
152
+ if (
153
+ hasStoredToken &&
154
+ login &&
155
+ u.registeredAs &&
156
+ u.registeredAs.toLowerCase() !== login.toLowerCase()
157
+ ) {
139
158
  return { act: true, reason: `reporting account changed to ${login}` };
140
159
  }
141
160
  if (hasStoredToken && login && !u.registeredAs) {
@@ -228,6 +247,14 @@ function refusal(status, json, body, method) {
228
247
  : "retry with --refresh";
229
248
  return `endpoint answered ${status}${detail} - GitHub did not accept the proof for ${body.u}; ${fix}`;
230
249
  }
250
+ if (code === "proof_required") {
251
+ return `${body.u} is the repository owner, which registers with a proof: log in with \`gh auth login\` or add its SSH key, then retry with --refresh`;
252
+ }
253
+ if (code === "not_collaborator") {
254
+ const rerun =
255
+ method === "claim" ? "rerun setup and enter the name again" : "rerun with --refresh";
256
+ return `${body.u} is not a collaborator of the pipeline repository; the repository owner adds it, then ${rerun}`;
257
+ }
231
258
  if (status === 403) {
232
259
  return (
233
260
  `endpoint answered 403${detail} - ${body.u} needs collaborator access to the pipeline repository; ` +
@@ -240,7 +267,10 @@ function refusal(status, json, body, method) {
240
267
  async function requestToken(url, body, proof, method) {
241
268
  const { res, json, failure } = await postJson(url, body, proof);
242
269
  if (failure) return { token: null, why: failure };
243
- if (!res.ok) return { token: null, why: refusal(res.status, json, body, method) };
270
+ if (!res.ok) {
271
+ const code = typeof json?.code === "string" ? json.code : null;
272
+ return { token: null, code, why: refusal(res.status, json, body, method) };
273
+ }
244
274
  if (json?.accepted === false)
245
275
  return { token: null, declined: true, why: "the endpoint declined this login" };
246
276
  const token = typeof json?.token === "string" ? json.token.trim() : "";
@@ -319,6 +349,94 @@ async function requestTokenWithSsh(url, body, key) {
319
349
  return requestToken(url, { ...body, ssh: { challenge, signature } }, null, "ssh");
320
350
  }
321
351
 
352
+ /** The account name the person typed at setup or update, when it is a GitHub login. */
353
+ export function declaredLoginOf(prefs) {
354
+ const d = prefs?.global?.usageLog?.declaredLogin;
355
+ return typeof d === "string" && LOGIN_RE.test(d.trim()) ? d.trim() : null;
356
+ }
357
+
358
+ /**
359
+ * Keep a typed account name and prefer it as the account to report under (the
360
+ * reportAs it replaces is kept as `usageLog.reportAsBefore`), or
361
+ * forget it (name null) once the server says it is not a collaborator, so the
362
+ * next setup asks again instead of retrying a name that cannot register.
363
+ */
364
+ function declareLogin(prefsPath, name) {
365
+ try {
366
+ const written = updateJsonFileSync(
367
+ prefsPath,
368
+ (j) => {
369
+ if (!j || typeof j !== "object" || Array.isArray(j)) return undefined;
370
+ j.global = j.global || {};
371
+ const u = { ...(j.global.usageLog || {}) };
372
+ if (name) {
373
+ if (u.reportAs && u.reportAs !== u.declaredLogin && u.reportAsBefore === undefined)
374
+ u.reportAsBefore = u.reportAs;
375
+ Object.assign(u, { declaredLogin: name, reportAs: name });
376
+ delete u.claimFailedAt;
377
+ } else if (u.declaredLogin) {
378
+ if (u.reportAs === u.declaredLogin) {
379
+ if (u.reportAsBefore) u.reportAs = u.reportAsBefore;
380
+ else delete u.reportAs;
381
+ }
382
+ delete u.reportAsBefore;
383
+ delete u.declaredLogin;
384
+ }
385
+ j.global.usageLog = u;
386
+ return j;
387
+ },
388
+ { mode: 0o600, followSymlink: true, timeoutMs: lockTimeoutFrom(process.env) },
389
+ );
390
+ return written !== undefined;
391
+ } catch {
392
+ return false;
393
+ }
394
+ }
395
+
396
+ /** Merge `fields` into `usageLog` (undefined removes a key). True when written. */
397
+ function patchUsageLog(prefsPath, fields) {
398
+ try {
399
+ const written = updateJsonFileSync(
400
+ prefsPath,
401
+ (j) => {
402
+ if (!j || typeof j !== "object" || Array.isArray(j)) return undefined;
403
+ j.global = j.global || {};
404
+ const u = { ...(j.global.usageLog || {}) };
405
+ for (const [k, v] of Object.entries(fields)) {
406
+ if (v === undefined) delete u[k];
407
+ else u[k] = v;
408
+ }
409
+ j.global.usageLog = u;
410
+ return j;
411
+ },
412
+ { mode: 0o600, followSymlink: true, timeoutMs: lockTimeoutFrom(process.env) },
413
+ );
414
+ return written !== undefined;
415
+ } catch {
416
+ return false;
417
+ }
418
+ }
419
+
420
+ /**
421
+ * A declared claim that failed (`usageLog.claimFailedAt`) is not sent again for
422
+ * a day unless the person types the name again.
423
+ */
424
+ const CLAIM_RETRY_MS = 24 * 3600 * 1000;
425
+
426
+ /**
427
+ * The machine label a declared registration is sent with: the host name and a
428
+ * random suffix kept in prefs (`usageLog.claimNonce`). The server replaces a login's token for the same
429
+ * label, so a label nobody else can guess keeps a stranger from replacing it.
430
+ */
431
+ function claimLabel(prefsPath, usageLog, host) {
432
+ let nonce = usageLog?.claimNonce;
433
+ if (typeof nonce !== "string" || !/^[0-9a-f]{8,32}$/.test(nonce)) {
434
+ nonce = randomBytes(6).toString("hex");
435
+ patchUsageLog(prefsPath, { claimNonce: nonce });
436
+ }
437
+ return `${host.slice(0, 30)}-${nonce}`;
438
+ }
439
+
322
440
  /**
323
441
  * Write the switch, and the credential-store ENTRY NAME when there is one.
324
442
  *
@@ -332,7 +450,7 @@ async function requestTokenWithSsh(url, body, key) {
332
450
  export function persistPrefs(
333
451
  prefsPath,
334
452
  entryName = null,
335
- { enableTelemetry = true, registeredAs = null } = {},
453
+ { enableTelemetry = true, registeredAs = null, claimed = false } = {},
336
454
  ) {
337
455
  try {
338
456
  const written = updateJsonFileSync(
@@ -349,6 +467,8 @@ export function persistPrefs(
349
467
  if (registeredAs) {
350
468
  j.global.usageLog.registeredAs = registeredAs;
351
469
  delete j.global.usageLog.registeredFallback;
470
+ if (claimed) j.global.usageLog.registeredClaim = true;
471
+ else delete j.global.usageLog.registeredClaim;
352
472
  j.global.usageLog.registeredAt = new Date().toISOString();
353
473
  }
354
474
  return j;
@@ -371,6 +491,7 @@ export function persistPrefs(
371
491
  export function personalAccountMissing(prefs, login = undefined, env = process.env) {
372
492
  const u = prefs?.global?.usageLog || {};
373
493
  if (u.optOut === true) return false;
494
+ if (declaredLoginOf(prefs)) return false;
374
495
  if (env.MULTI_AGENT_USAGE_TOKEN) return false;
375
496
  if (typeof u.token === "string" && u.token.trim()) return false;
376
497
  if (pinned(u)) return false;
@@ -425,6 +546,7 @@ async function main(argv) {
425
546
  prefs: null,
426
547
  feedback: false,
427
548
  refresh: false,
549
+ login: null,
428
550
  };
429
551
  for (let i = 0; i < argv.length; i += 1) {
430
552
  const a = argv[i];
@@ -434,6 +556,7 @@ async function main(argv) {
434
556
  else if (a === "--feedback") opts.feedback = true;
435
557
  else if (a === "--refresh") opts.refresh = true;
436
558
  else if (a === "--prefs") opts.prefs = argv[++i];
559
+ else if (a === "--login") opts.login = argv[++i] ?? "";
437
560
  }
438
561
  const HOME = homedir();
439
562
  const prefsPath = opts.prefs || join(HOME, ".claude", "multi-agent-preferences.json");
@@ -464,6 +587,26 @@ async function main(argv) {
464
587
  return done("skipped", "no prefs");
465
588
  }
466
589
 
590
+ // A name the person typed: kept as `declaredLogin` and preferred as the
591
+ // account to report under. A proof for it is still used when this machine
592
+ // has one; without one it is registered as declared (see below).
593
+ if (opts.login !== null) {
594
+ if (prefs.global?.usageLog?.optOut === true && !opts.feedback) {
595
+ say(" -> operational reporting: unchanged (opted out)");
596
+ return done("skipped", "opted out");
597
+ }
598
+ const name = String(opts.login).trim();
599
+ if (!LOGIN_RE.test(name)) {
600
+ say(` -> operational reporting: "${name.slice(0, 40)}" is not a GitHub login`);
601
+ return done("skipped", `"${name.slice(0, 40)}" is not a GitHub login`);
602
+ }
603
+ if (!opts.dryRun && !declareLogin(prefsPath, name)) {
604
+ say(" -> operational reporting: could not write preferences");
605
+ return done("unavailable", "prefs write failed");
606
+ }
607
+ opts.refresh = true;
608
+ }
609
+
467
610
  // Decided before any credential is read: an opted-out machine, or one whose
468
611
  // token is already in the environment or prefs, never touches the credential
469
612
  // store or gh.
@@ -472,16 +615,26 @@ async function main(argv) {
472
615
  pre.reason,
473
616
  );
474
617
  const has = identityMatters ? Boolean(storedToken(prefs, credStore)) : false;
475
- const who = identityMatters
476
- ? reportingUser(prefsPath, { refresh: opts.refresh })
618
+ let who = identityMatters
619
+ ? reportingUser(prefsPath, { refresh: opts.refresh, write: !opts.dryRun })
477
620
  : { login: undefined, reason: null };
478
- identity.login = who.login;
479
621
  const current = readJson(prefsPath) || prefs;
622
+ const declared =
623
+ opts.dryRun && opts.login !== null ? String(opts.login).trim() : declaredLoginOf(current);
624
+ // A typed name is used only when this machine proves no account at all; a
625
+ // proof, for that name or another, is evidence and a declaration is not.
626
+ const cu = current.global?.usageLog || {};
627
+ const provenPin = has && pinned(cu) && cu.registeredClaim !== true;
628
+ if (identityMatters && declared && !who.login && !provenPin && !opts.feedback) {
629
+ who = { login: declared, method: "claim", key: null, reason: "declared by the person" };
630
+ }
631
+ identity.login = who.login;
480
632
  const decision = decideRegistration(current, {
481
633
  hasStoredToken: has,
482
634
  forFeedback: opts.feedback,
483
635
  login: who.login,
484
636
  loginReason: who.reason,
637
+ proven: who.method !== "claim",
485
638
  });
486
639
  if (!decision.act) {
487
640
  const repairable = ["token in environment", "token in prefs", "token already onboarded"];
@@ -531,7 +684,15 @@ async function main(argv) {
531
684
  }
532
685
 
533
686
  let result;
534
- if (who.method === "ssh") {
687
+ if (who.method === "claim") {
688
+ const failedAt = Date.parse(current.global?.usageLog?.claimFailedAt ?? "");
689
+ if (opts.login === null && !Number.isNaN(failedAt) && Date.now() - failedAt < CLAIM_RETRY_MS) {
690
+ say(" -> operational reporting: unchanged (the declared name was refused in the last day)");
691
+ return done("skipped", "declared registration refused recently");
692
+ }
693
+ const label = claimLabel(prefsPath, current.global?.usageLog, host);
694
+ result = await requestToken(url, { u: user, c: label, claim: true }, null, "claim");
695
+ } else if (who.method === "ssh") {
535
696
  result = await requestTokenWithSsh(url, { u: user, c: host }, who.key);
536
697
  } else {
537
698
  const proof = who.method === "credential" ? credentialToken() : ghTokenFor(user);
@@ -545,7 +706,11 @@ async function main(argv) {
545
706
  }
546
707
  result = await requestToken(url, { u: user, c: host }, proof, who.method || "gh");
547
708
  }
548
- const { token, why, declined, login: pinnedLogin } = result;
709
+ const { token, why, declined, code, login: pinnedLogin } = result;
710
+ if (who.method === "claim" && ["not_collaborator", "proof_required"].includes(code))
711
+ declareLogin(prefsPath, null);
712
+ else if (who.method === "claim" && !token && !declined)
713
+ patchUsageLog(prefsPath, { claimFailedAt: new Date().toISOString() });
549
714
  if (declined) {
550
715
  say(` -> operational reporting left off (${why})`);
551
716
  return done("declined", why);
@@ -569,6 +734,7 @@ async function main(argv) {
569
734
  persistPrefs(prefsPath, entry, {
570
735
  enableTelemetry: !optedOut,
571
736
  registeredAs: pinnedLogin || user,
737
+ claimed: who.method === "claim",
572
738
  });
573
739
  if (optedOut) {
574
740
  say(" -> feedback token registered; telemetry stays off (usageLog.optOut is set)");
@@ -17,7 +17,7 @@ import { readFileSync } from "node:fs";
17
17
 
18
18
  const ALLOWED_STACK = new Set(["ios", "android", "node", "python", "go"]);
19
19
  const ALLOWED_NODE_KIND = new Set(["file", "symbol", "module"]);
20
- const ALLOWED_EDGE_KIND = new Set(["defines", "imports", "references"]);
20
+ const ALLOWED_EDGE_KIND = new Set(["defines", "imports", "references", "inherits"]);
21
21
  const MAX_REPORTED_DANGLING = 5;
22
22
 
23
23
  function readInput() {
@@ -137,8 +137,8 @@ export function validate(obj) {
137
137
  if (typeof obj !== "object" || obj === null || Array.isArray(obj))
138
138
  return ["root must be an object"];
139
139
 
140
- if (!["1.0.0", "1.1.0"].includes(obj.schemaVersion))
141
- errors.push(`schemaVersion must be "1.0.0" or "1.1.0"`);
140
+ if (!["1.0.0", "1.1.0", "1.2.0"].includes(obj.schemaVersion))
141
+ errors.push(`schemaVersion must be "1.0.0", "1.1.0" or "1.2.0"`);
142
142
  if (!ALLOWED_STACK.has(obj.stack))
143
143
  errors.push(`stack must be one of ${[...ALLOWED_STACK].join(",")}`);
144
144
  if (typeof obj.root !== "string" || obj.root.length === 0)
@@ -837,7 +837,15 @@ function measureRework(repo, mergeBase, range, human, windowDays, now) {
837
837
  if (f.deleted === 0 || f.binary) continue;
838
838
  const old = f.oldPath ?? f.path;
839
839
  if (f.untracked) continue;
840
- const diff = git(repo, ["diff", "-U0", "-M", ...range, "--", old, f.path]);
840
+ const diff = git(repo, [
841
+ "diff",
842
+ "-U0",
843
+ "-M",
844
+ ...range,
845
+ "--",
846
+ `:(literal)${old}`,
847
+ `:(literal)${f.path}`,
848
+ ]);
841
849
  for (const [start, count] of removedRanges(diff)) {
842
850
  const porcelain = git(repo, [
843
851
  "blame",
@@ -75,6 +75,7 @@ function fail(msg) {
75
75
  }
76
76
 
77
77
  const lines = (out) => out.split("\n").filter(Boolean);
78
+ const paths = (out) => out.split("\0").filter(Boolean);
78
79
 
79
80
  async function main() {
80
81
  let args;
@@ -93,9 +94,11 @@ async function main() {
93
94
  const headSha = git(repo, ["rev-parse", head]).trim();
94
95
  const baseSha = git(repo, ["rev-parse", args.base]).trim();
95
96
  const branch = args.branch ?? git(repo, ["rev-parse", "--abbrev-ref", head]).trim();
96
- const changed = lines(git(repo, ["diff", "--name-only", "-M", mergeBase, headSha]));
97
+ const changed = paths(git(repo, ["diff", "-z", "--name-only", "-M", mergeBase, headSha]));
97
98
  if (!changed.length) return fail(`no change between ${args.base} and ${head}`);
98
- const baseTouched = new Set(lines(git(repo, ["diff", "--name-only", mergeBase, baseSha])));
99
+ const baseTouched = new Set(
100
+ paths(git(repo, ["diff", "-z", "--name-only", mergeBase, baseSha])),
101
+ );
99
102
  const exists = (name) => git(repo, ["branch", "--list", name]).trim() !== "";
100
103
  const to = args.to ?? nextName(branch, exists);
101
104
  const subject =
@@ -139,7 +142,7 @@ async function main() {
139
142
  encoding: "utf-8",
140
143
  });
141
144
  if (r.status !== 0) {
142
- const unmerged = lines(git(repo, ["diff", "--name-only", "--diff-filter=U"]));
145
+ const unmerged = paths(git(repo, ["diff", "-z", "--name-only", "--diff-filter=U"]));
143
146
  plan.conflicts = unmerged.length ? unmerged : lines(r.stderr).slice(0, 20);
144
147
  git(repo, ["reset", "-q", "--hard"]);
145
148
  git(repo, ["checkout", "-q", back]);
@@ -149,8 +152,16 @@ async function main() {
149
152
  git(repo, ["commit", "-q", "-m", plan.subject]);
150
153
  plan.applied = true;
151
154
  plan.portedSha = git(repo, ["rev-parse", "HEAD"]).trim();
152
- plan.differsFromHead = lines(
153
- git(repo, ["diff", "--name-only", plan.head, "HEAD", "--", ...plan.changedPaths]),
155
+ plan.differsFromHead = paths(
156
+ git(repo, [
157
+ "diff",
158
+ "-z",
159
+ "--name-only",
160
+ plan.head,
161
+ "HEAD",
162
+ "--",
163
+ ...plan.changedPaths.map((p) => `:(literal)${p}`),
164
+ ]),
154
165
  );
155
166
  git(repo, ["checkout", "-q", back]);
156
167
  }
@@ -320,12 +320,14 @@ async function main() {
320
320
  git(repo, ["commit", "-q", "--allow-empty", "-m", plan.slices[i].subject]);
321
321
  parent = name;
322
322
  }
323
- const drift = git(repo, ["diff", "--name-only", parent, headSha]).trim();
323
+ const drift = git(repo, ["diff", "-z", "--name-only", parent, headSha])
324
+ .split("\0")
325
+ .filter(Boolean);
324
326
  git(repo, ["checkout", "-q", original === "HEAD" ? headSha : original]);
325
327
  plan.applied = true;
326
- plan.topMatchesBranch = drift === "";
327
- if (drift) {
328
- plan.drift = drift.split("\n");
328
+ plan.topMatchesBranch = drift.length === 0;
329
+ if (drift.length) {
330
+ plan.drift = drift;
329
331
  process.exitCode = 1;
330
332
  }
331
333
  } catch (err) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-10-12T21:38:23Z",
3
+ "generatedAt": "2026-10-13T08:10:09Z",
4
4
  "skillCount": 228,
5
5
  "entries": [
6
6
  {
@@ -213,7 +213,7 @@
213
213
  },
214
214
  {
215
215
  "path": "shared/core/multi-agent-setup/SKILL.md",
216
- "sha256": "79a4afa655df12221a25bc386a32e61c38016d2de660dd618b404ca8bb0b28d3"
216
+ "sha256": "73a1d1d8484d32b6d83107632a84aeced2f230f8581b7d47cad5e2d14e13860a"
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": "013317432263490a01d081b657928b977012afb78f75a64d278d94d2daa8e894"
268
+ "sha256": "2f9e224e240e1c9053a59066678f869ae36f18c088482488f3bda8fd207148ea"
269
269
  },
270
270
  {
271
271
  "path": "shared/core/multi-agent/SKILL.md",
@@ -140,9 +140,9 @@ Then, **whether they pasted or skipped**, run:
140
140
  node "$HOME/.copilot/scripts/usage-register.mjs" --json
141
141
  ```
142
142
 
143
- If its JSON line says `"needsPersonalAccount":true`, offer once: `gh auth login`,
144
- or an SSH key on the personal account (the "Personal account" step in
145
- `$HOME/.claude/multi-agent-refs/features/usage-reporting.md`).
143
+ If its JSON line says `"needsPersonalAccount":true`, ask once for the person's
144
+ GitHub username (free text; Skip leaves reporting off) and run
145
+ `node "$HOME/.copilot/scripts/usage-register.mjs" --login <name> --json`.
146
146
 
147
147
  It maps whatever token exists and switches reporting on, and requests a
148
148
  per-machine write-only one when none resolves. Setup is a registration entry point
@@ -44,9 +44,9 @@ Update the pipeline with a single command. Preferences are preserved; only skill
44
44
  already onboarded, and permanently blocked by `usageLog.optOut: true`. Offline
45
45
  or endpoint down leaves reporting off with one line, never an error. Contract:
46
46
  `$HOME/.claude/multi-agent-refs/features/usage-reporting.md`.
47
- If its JSON line says `"needsPersonalAccount":true`, offer once: `gh auth login`,
48
- or an SSH key on the personal account (the "Personal account" step in
49
- `$HOME/.claude/multi-agent-refs/features/usage-reporting.md`).
47
+ If its JSON line says `"needsPersonalAccount":true`, ask once for the person's
48
+ GitHub username (free text; Skip leaves reporting off) and run
49
+ `node "$HOME/.copilot/scripts/usage-register.mjs" --login <name> --json`.
50
50
  6. Show the new version + changelog
51
51
  7. Smoke test (schema + cross-cli)
52
52