@mmerterden/multi-agent-pipeline 20.21.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.
- package/CHANGELOG.md +62 -0
- package/docs/adr/0010-own-code-graph.md +3 -2
- package/docs/adr/0017-change-impact-from-the-graph.md +69 -0
- package/docs/adr/README.md +1 -0
- package/docs/ecosystem.md +6 -5
- package/docs/facts.json +5 -4
- package/docs/features.md +2 -2
- package/install/_usage-lifecycle.mjs +44 -0
- package/install/catalog-history.json +1 -1
- package/install/index.mjs +2 -0
- package/install/templates/copilot-instructions.md +1 -0
- package/manifest.json +72 -65
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/create-jira/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/review/SKILL.md +2 -1
- package/pipeline/commands/multi-agent/setup/SKILL.md +9 -18
- package/pipeline/commands/multi-agent/update/SKILL.md +3 -3
- package/pipeline/lib/json-file-lock.mjs +24 -2
- package/pipeline/multi-agent-refs/_dev-context.md +15 -0
- package/pipeline/multi-agent-refs/analysis/evidence.md +6 -4
- package/pipeline/multi-agent-refs/analysis/locked.md +1 -1
- package/pipeline/multi-agent-refs/cross-cli-contract.md +2 -0
- package/pipeline/multi-agent-refs/features/analysis-jira.md +5 -0
- package/pipeline/multi-agent-refs/features/code-graph.md +66 -2
- package/pipeline/multi-agent-refs/features/product-context.md +238 -0
- package/pipeline/multi-agent-refs/features/usage-reporting.md +40 -23
- package/pipeline/multi-agent-refs/generate-issue.md +11 -1
- package/pipeline/multi-agent-refs/knowledge.md +20 -0
- package/pipeline/multi-agent-refs/phases/phase-1-plan.md +9 -14
- package/pipeline/multi-agent-refs/phases/phase-2-dev.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-3-review.md +7 -6
- package/pipeline/multi-agent-refs/platform-parity.md +23 -9
- package/pipeline/multi-agent-refs/setup/token-save-flow.md +24 -0
- package/pipeline/schemas/agent-state.schema.json +31 -0
- package/pipeline/schemas/analysis-spec.schema.json +14 -0
- package/pipeline/schemas/code-graph.schema.json +13 -2
- package/pipeline/schemas/diff-risk.schema.json +2 -1
- package/pipeline/schemas/prefs.schema.json +84 -4
- package/pipeline/scripts/_code-graph.mjs +188 -6
- package/pipeline/scripts/_diff-impact.mjs +319 -0
- package/pipeline/scripts/_supertypes.mjs +133 -0
- package/pipeline/scripts/analysis-story-body.mjs +24 -8
- package/pipeline/scripts/capture-flush.sh +62 -4
- package/pipeline/scripts/code-graph-rules/android.json +1 -0
- package/pipeline/scripts/code-graph-rules/ios.json +5 -0
- package/pipeline/scripts/code-graph-rules/node.json +1 -0
- package/pipeline/scripts/code-graph-rules/python.json +1 -0
- package/pipeline/scripts/diff-risk-score.mjs +80 -9
- package/pipeline/scripts/git-path.mjs +21 -0
- package/pipeline/scripts/graph-affected.mjs +168 -36
- package/pipeline/scripts/graph-confirm.mjs +140 -0
- package/pipeline/scripts/graph-query.mjs +16 -4
- package/pipeline/scripts/learnings-ledger.mjs +3 -2
- package/pipeline/scripts/product-context.mjs +550 -0
- package/pipeline/scripts/smoke-cross-cli-behavior.sh +39 -1
- package/pipeline/scripts/symbol-existence-gate.mjs +21 -9
- package/pipeline/scripts/test-gap-scan.mjs +4 -1
- package/pipeline/scripts/test-strength.mjs +7 -8
- package/pipeline/scripts/usage-identity.mjs +1 -1
- package/pipeline/scripts/usage-register.mjs +181 -15
- package/pipeline/scripts/validate-code-graph.mjs +3 -2
- package/pipeline/scripts/validate-diff-risk.mjs +1 -0
- package/pipeline/scripts/valven-gate.mjs +9 -1
- package/pipeline/scripts/valven-port.mjs +16 -5
- package/pipeline/scripts/valven-split.mjs +6 -4
- package/pipeline/skills/.skill-manifest.json +4 -4
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +5 -1
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +11 -9
- package/pipeline/skills/shared/core/multi-agent-update/SKILL.md +3 -3
|
@@ -236,15 +236,14 @@ function prepare(opts) {
|
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
function changedFiles(root, base, head) {
|
|
239
|
-
const r = git(root, ["diff", "--no-renames", "--name-status", base, head]);
|
|
239
|
+
const r = git(root, ["diff", "--no-renames", "--name-status", "-z", base, head]);
|
|
240
240
|
if (!r.ok) throw new UsageError(`git diff failed: ${r.err}`);
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
});
|
|
241
|
+
const fields = r.out.split("\0");
|
|
242
|
+
const out = [];
|
|
243
|
+
for (let i = 0; i + 1 < fields.length; i += 2) {
|
|
244
|
+
if (fields[i]) out.push({ status: fields[i][0], path: fields[i + 1] });
|
|
245
|
+
}
|
|
246
|
+
return out;
|
|
248
247
|
}
|
|
249
248
|
|
|
250
249
|
function testCommandFor(opts, adapter, worktree, file) {
|
|
@@ -67,7 +67,7 @@ const HOST = "github.com";
|
|
|
67
67
|
const API_DEFAULT = "https://api.github.com";
|
|
68
68
|
const DEFAULT_KEYS = ["id_ed25519", "id_ecdsa", "id_rsa"];
|
|
69
69
|
export const LOGIN_METHODS = ["gh", "credential", "ssh"];
|
|
70
|
-
const LOGIN_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,38}$/;
|
|
70
|
+
export const LOGIN_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,38}$/;
|
|
71
71
|
const REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
72
72
|
|
|
73
73
|
export function defaultPrefsPath(env = process.env) {
|
|
@@ -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
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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(
|
|
86
|
-
|
|
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 (
|
|
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)
|
|
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
|
-
|
|
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 === "
|
|
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,7 +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 (
|
|
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"`);
|
|
141
142
|
if (!ALLOWED_STACK.has(obj.stack))
|
|
142
143
|
errors.push(`stack must be one of ${[...ALLOWED_STACK].join(",")}`);
|
|
143
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, [
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
153
|
-
git(repo, [
|
|
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])
|
|
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
|
|
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-
|
|
3
|
+
"generatedAt": "2026-10-13T08:10:09Z",
|
|
4
4
|
"skillCount": 228,
|
|
5
5
|
"entries": [
|
|
6
6
|
{
|
|
@@ -165,7 +165,7 @@
|
|
|
165
165
|
},
|
|
166
166
|
{
|
|
167
167
|
"path": "shared/core/multi-agent-review/SKILL.md",
|
|
168
|
-
"sha256": "
|
|
168
|
+
"sha256": "1a22618e6d3d9d07b267bd61fc591bb33726e1d7ef8567285401f61bc7be0ad5"
|
|
169
169
|
},
|
|
170
170
|
{
|
|
171
171
|
"path": "shared/core/multi-agent-route-off/SKILL.md",
|
|
@@ -213,7 +213,7 @@
|
|
|
213
213
|
},
|
|
214
214
|
{
|
|
215
215
|
"path": "shared/core/multi-agent-setup/SKILL.md",
|
|
216
|
-
"sha256": "
|
|
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": "
|
|
268
|
+
"sha256": "2f9e224e240e1c9053a59066678f869ae36f18c088482488f3bda8fd207148ea"
|
|
269
269
|
},
|
|
270
270
|
{
|
|
271
271
|
"path": "shared/core/multi-agent/SKILL.md",
|
|
@@ -76,7 +76,11 @@ Skip Phase 0-3 and review a diff only. Input shapes: a PR (`#N`, `repo#N`, GitHu
|
|
|
76
76
|
resolves whose `stack` is the other mobile platform and the diff touches a screen, a
|
|
77
77
|
service, a request model or a localization file, compare the change against that repo
|
|
78
78
|
on four axes: endpoints called, parameters sent, business rules around the call,
|
|
79
|
-
localization keys used.
|
|
79
|
+
localization keys used. When the project belongs to a product
|
|
80
|
+
(`prefs.global.products`), the counterparts are the product's other repositories,
|
|
81
|
+
resolved with `product-context.mjs refs` and compared at their latest integration
|
|
82
|
+
branch through the toolkit's `repo_*` tools, pinned to a sha
|
|
83
|
+
(`features/product-context.md`). Otherwise the counterpart is resolved automatically and in both
|
|
80
84
|
directions (ios ↔ android) from four sources in this order, first hit wins:
|
|
81
85
|
`--with <path|owner/repo>` (a one-off override, remembered like any other resolution),
|
|
82
86
|
then a remembered `prefs.projects[<slug>].counterpartRoots[]`, then the primary
|
|
@@ -24,6 +24,10 @@ node "$HOME/.copilot/scripts/doctor.mjs"
|
|
|
24
24
|
|
|
25
25
|
Run it before the first question and again at the end. The first run makes setup targeted: never ask for a token that is already mapped and answering. The second run is how setup ends - "setup complete" is a claim, and the doctor's exit code is the evidence. Report both in `outputLanguage`; if the second run still shows a BLOCK, say so plainly rather than closing on the word "complete".
|
|
26
26
|
|
|
27
|
+
## Sections
|
|
28
|
+
|
|
29
|
+
`/multi-agent-setup hosts`: the Step 3.5 host prompt per mapped hosted service, then Token Save Flow Step 3.7 per mapped GitHub/Bitbucket token. `accounts`: Step 3.6. `jira-keys`: the Jira key mapping. Tokens are left alone.
|
|
30
|
+
|
|
27
31
|
## Setup (Credential Store + Git Identity Onboarding)
|
|
28
32
|
|
|
29
33
|
Uses `~/.copilot/lib/credential-store.sh` to read/write secrets in the macOS Keychain (`security`, built in); the pipeline is macOS only (ADR-0012). Run `~/.copilot/lib/credential-store.sh doctor` first - it prints the platform and whether the backend is usable. The shell driver auto-delegates `get`/`set`/`delete` to the deterministic Python helper at `$HOME/.copilot/scripts/keychain.py` (Python 3 stdlib only, no external deps). The Python path writes both `-l` (label) and `-s` (service) attributes so items are findable regardless of how callers look them up. Set `KEYCHAIN_DELEGATE=0` to force the raw shell path.
|
|
@@ -136,9 +140,9 @@ Then, **whether they pasted or skipped**, run:
|
|
|
136
140
|
node "$HOME/.copilot/scripts/usage-register.mjs" --json
|
|
137
141
|
```
|
|
138
142
|
|
|
139
|
-
If its JSON line says `"needsPersonalAccount":true`,
|
|
140
|
-
|
|
141
|
-
|
|
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`.
|
|
142
146
|
|
|
143
147
|
It maps whatever token exists and switches reporting on, and requests a
|
|
144
148
|
per-machine write-only one when none resolves. Setup is a registration entry point
|
|
@@ -194,7 +198,7 @@ The Step 2 discovery summary is never the end of setup. Immediately after showin
|
|
|
194
198
|
Loop rules:
|
|
195
199
|
|
|
196
200
|
- **One prompt per service.** Never collapse the list into a single "add missing tokens now?" yes/no, and never offer one multi-select covering all services: each service has its own source URL, key name, identity binding and host prompt, so each gets its own Token Save Flow pass.
|
|
197
|
-
- **Each service pass asks, in order: token → author → host.** Step A/B collect the secret, Step C binds the git identity (author) - Bitbucket and GitHub included; every saved token gets an author - and Step 3.5 asks the host domain inline for hosted services (jira, confluence, bitbucket, fortify, graylog) - plus the Fortify project versions and the optional Graylog test instance. GitHub and Figma skip the host question (fixed domains); `bitbucket_user` skips author binding (username, not a PAT). Never end a service pass with the token saved but the author or a required host unasked.
|
|
201
|
+
- **Each service pass asks, in order: token → author → host → read binding (GitHub, Bitbucket; Token Save Flow Step 3.7).** Step A/B collect the secret, Step C binds the git identity (author) - Bitbucket and GitHub included; every saved token gets an author - and Step 3.5 asks the host domain inline for hosted services (jira, confluence, bitbucket, fortify, graylog) - plus the Fortify project versions and the optional Graylog test instance. GitHub and Figma skip the host question (fixed domains); `bitbucket_user` skips author binding (username, not a PAT). Never end a service pass with the token saved but the author or a required host unasked.
|
|
198
202
|
- **Skip is per-service.** A Skip answer leaves that service's `keychainMapping` at `null` and the loop continues with the next service. Skips never abort the loop.
|
|
199
203
|
- **`figma` and `figma_mcp` are distinct credentials for distinct tiers - ask both.** Skipping the PAT does not imply skipping MCP, and vice versa. The `figma_mcp` pass opens with a Remote / Local mode question instead of a clipboard paste (see "Figma MCP Setup" below): Remote generates a `figu_` OAuth token for the current user, Local wires the PAT-based local server.
|
|
200
204
|
- The loop ends only when every missing service has been asked exactly once; then continue to Step 4 (Verify all).
|
|
@@ -219,17 +223,15 @@ Runs inside Step 3, only when the target repo is Android. One entry, `google_pla
|
|
|
219
223
|
|
|
220
224
|
### Step 3.5 - Host Prompt (embedded in Token Save Flow)
|
|
221
225
|
|
|
222
|
-
|
|
226
|
+
Part of the Token Save Flow reference above.
|
|
223
227
|
|
|
224
228
|
### Step 3.6 - Per-account host overrides (optional)
|
|
225
229
|
|
|
226
230
|
**Runs only when** `~/.copilot/lib/account-resolver.sh` returns **more than one** account AND at least one account has Jira or Bitbucket tokens.
|
|
227
231
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
For each non-default account, ask once: "Account {label} has Jira/Bitbucket tokens but no host override yet. Use the global default ({hosts.jira}) or set a per-account host?" with options **1) Use global default** / **2) Set per-account host**. On `2`, prompt for `jiraHost` / `bitbucketHost` / `confluenceHost` (each optional) and save to `prefs.global.accounts[]`, e.g. `{ "id": "client-x", "jiraHost": "jira.client-x.example.com", "bitbucketHost": "bitbucket.client-x.example.com" }`.
|
|
232
|
+
For each non-default account (two identities, each with its own Jira host), ask once: "Account {label} has Jira/Bitbucket tokens but no host override yet. Use the global default ({hosts.jira}) or set a per-account host?" with options **1) Use global default** / **2) Set per-account host**. On `2`, prompt for `jiraHost` / `bitbucketHost` / `confluenceHost` (each optional) and save to `prefs.global.accounts[]`, e.g. `{ "id": "client-x", "jiraHost": "jira.client-x.example.com", "bitbucketHost": "bitbucket.client-x.example.com" }`.
|
|
231
233
|
|
|
232
|
-
`account-resolver.sh` reads this on every invocation; per-account override wins over `global.hosts.*`. Skip this step on autopilot
|
|
234
|
+
`account-resolver.sh` reads this on every invocation; per-account override wins over `global.hosts.*`. Skip this step on autopilot; `/multi-agent-setup accounts` opens it later.
|
|
233
235
|
|
|
234
236
|
---
|
|
235
237
|
|
|
@@ -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`,
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|