@mmerterden/multi-agent-pipeline 20.27.1 → 20.28.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 +38 -0
- package/README.md +6 -5
- package/README.tr.md +6 -5
- package/docs/adr/0008-installer-modularization-and-secret-leak-defense.md +0 -1
- package/docs/architecture.md +4 -4
- package/docs/ecosystem.md +5 -5
- package/docs/facts.json +14 -12
- package/docs/features.md +2 -0
- package/install/_dev-only-files.mjs +1 -0
- package/install/catalog-history.json +1 -1
- package/install/index.mjs +0 -2
- package/manifest.json +74 -51
- package/package.json +4 -1
- package/pipeline/agents/code-reviewer.md +1 -0
- package/pipeline/agents/test-gen-analyst.md +98 -0
- package/pipeline/commands/multi-agent/sync/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/test-gen/SKILL.md +124 -0
- package/pipeline/multi-agent-refs/cross-cli-contract.md +11 -5
- package/pipeline/multi-agent-refs/features/doctor.md +13 -0
- package/pipeline/multi-agent-refs/features/test-classification.md +76 -0
- package/pipeline/multi-agent-refs/features/usage-reporting.md +17 -0
- package/pipeline/multi-agent-refs/help/en.md +1 -0
- package/pipeline/multi-agent-refs/help/tr.md +1 -0
- package/pipeline/multi-agent-refs/phases/operations.md +2 -2
- package/pipeline/multi-agent-refs/phases/phase-2-dev.md +6 -7
- package/pipeline/multi-agent-refs/phases/phase-3-review.md +3 -3
- package/pipeline/multi-agent-refs/phases/phase-5-report.md +6 -5
- package/pipeline/multi-agent-refs/store-ready/gate-3-policy.md +6 -2
- package/pipeline/multi-agent-refs/test-gen/stages.md +227 -0
- package/pipeline/preferences-template.json +3 -0
- package/pipeline/rules/app-store-guidelines.md +29 -41
- package/pipeline/schemas/prefs.schema.json +14 -0
- package/pipeline/schemas/test-gen-discovery.schema.json +73 -0
- package/pipeline/schemas/test-gen-gaps.schema.json +122 -0
- package/pipeline/schemas/test-gen-inventory.schema.json +59 -0
- package/pipeline/schemas/test-gen-results.schema.json +44 -0
- package/pipeline/scripts/_handle-patterns.mjs +166 -0
- package/pipeline/scripts/_http-post.mjs +141 -0
- package/pipeline/scripts/_stack-routing.mjs +4 -0
- package/pipeline/scripts/_test-gaming.mjs +227 -0
- package/pipeline/scripts/_unified-diff.mjs +104 -0
- package/pipeline/scripts/_worktree-snapshot.mjs +113 -0
- package/pipeline/scripts/doctor.mjs +51 -0
- package/pipeline/scripts/evidence-gate.mjs +74 -3
- package/pipeline/scripts/handle-diff-audit.mjs +188 -0
- package/pipeline/scripts/routine-registry.mjs +5 -1
- package/pipeline/scripts/run-ui-tests.sh +27 -3
- package/pipeline/scripts/test-classify.mjs +296 -0
- package/pipeline/scripts/test-gen-gate.mjs +409 -0
- package/pipeline/scripts/test-gen-state.mjs +313 -0
- package/pipeline/scripts/test-integrity-gate.mjs +76 -11
- package/pipeline/scripts/uninstall.mjs +1 -0
- package/pipeline/scripts/usage-register-beacon.mjs +239 -0
- package/pipeline/scripts/usage-register.mjs +128 -33
- package/pipeline/scripts/usage-report.mjs +43 -33
- package/pipeline/skills/.skill-manifest.json +25 -5
- package/pipeline/skills/.skills-index.json +57 -2
- package/pipeline/skills/shared/README.md +11 -6
- package/pipeline/skills/shared/core/apple-archive-compliance/SKILL.md +5 -4
- package/pipeline/skills/shared/core/apple-archive-compliance/references/rules.yml +8 -8
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +3 -3
- package/pipeline/skills/shared/core/multi-agent-test-gen/SKILL.md +120 -0
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +69 -8
- package/pipeline/skills/shared/external/app-store-review/evals/evals.json +12 -0
- package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +10 -1
- package/pipeline/skills/shared/external/fix-review-verdicts/SKILL.md +133 -0
- package/pipeline/skills/shared/external/handle-request/SKILL.md +97 -0
- package/pipeline/skills/shared/external/scenario-gap-analysis/SKILL.md +129 -0
- package/pipeline/skills/shared/external/scenario-gap-analysis/references/scenario-schema.md +60 -0
- package/pipeline/skills/shared/external/test-result-triage/SKILL.md +160 -0
- package/pipeline/skills/skills-index.md +7 -2
- package/install/_telemetry.mjs +0 -61
|
@@ -52,7 +52,16 @@
|
|
|
52
52
|
* @module pipeline/scripts/usage-register
|
|
53
53
|
*/
|
|
54
54
|
|
|
55
|
-
import {
|
|
55
|
+
import { postJson as postHttp } from "./_http-post.mjs";
|
|
56
|
+
import {
|
|
57
|
+
existsSync,
|
|
58
|
+
mkdirSync,
|
|
59
|
+
mkdtempSync,
|
|
60
|
+
readFileSync,
|
|
61
|
+
renameSync,
|
|
62
|
+
rmSync,
|
|
63
|
+
writeFileSync,
|
|
64
|
+
} from "node:fs";
|
|
56
65
|
import { spawn, spawnSync } from "node:child_process";
|
|
57
66
|
import { randomBytes } from "node:crypto";
|
|
58
67
|
import { join } from "node:path";
|
|
@@ -70,6 +79,7 @@ import {
|
|
|
70
79
|
sshKeygenBin,
|
|
71
80
|
} from "./usage-identity.mjs";
|
|
72
81
|
import { lockTimeoutFrom, updateJsonFileSync } from "../lib/json-file-lock.mjs";
|
|
82
|
+
import { BEACON_ENV, beaconUrl, httpReason, transportReason } from "./usage-register-beacon.mjs";
|
|
73
83
|
|
|
74
84
|
const DEFAULT_ENDPOINT = "https://mmerterden.com/api/usage/ingest";
|
|
75
85
|
const BUILT_IN_HOSTS = ["mmerterden.com"];
|
|
@@ -196,30 +206,39 @@ export function registerUrlAllowed(url, trustedHosts = []) {
|
|
|
196
206
|
return trusted.has(u.hostname.toLowerCase());
|
|
197
207
|
}
|
|
198
208
|
|
|
209
|
+
// Transport goes through _http-post.mjs, which retries a connection-level
|
|
210
|
+
// failure through curl (proxy variables, system trust store). The failure text
|
|
211
|
+
// keeps both causes so a blocked corporate network is diagnosable.
|
|
199
212
|
async function postJson(url, body, bearer = null) {
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
headers,
|
|
208
|
-
body: JSON.stringify(body),
|
|
209
|
-
redirect: "error",
|
|
210
|
-
signal: ctrl.signal,
|
|
211
|
-
});
|
|
212
|
-
const json = await res.json().catch(() => null);
|
|
213
|
-
return { res, json, failure: null };
|
|
214
|
-
} catch (e) {
|
|
213
|
+
const headers = bearer ? { Authorization: `Bearer ${bearer}` } : {};
|
|
214
|
+
const r = await postHttp({ url, headers, body: JSON.stringify(body), timeoutMs: TIMEOUT_MS });
|
|
215
|
+
if (r.failure) {
|
|
216
|
+
const causes = [r.fetchFailure && `fetch ${r.fetchFailure}`, `curl ${r.failure}`]
|
|
217
|
+
.filter(Boolean)
|
|
218
|
+
.join(", ");
|
|
219
|
+
const what = r.fetchFailure === "timeout" ? "endpoint timed out" : "endpoint unreachable";
|
|
215
220
|
return {
|
|
216
221
|
res: null,
|
|
217
222
|
json: null,
|
|
218
|
-
failure:
|
|
223
|
+
failure: `${what} (${causes})`,
|
|
224
|
+
reason: transportReason(r.fetchFailure, r.failure),
|
|
219
225
|
};
|
|
220
|
-
} finally {
|
|
221
|
-
clearTimeout(timer);
|
|
222
226
|
}
|
|
227
|
+
if (r.status >= 300 && r.status < 400) {
|
|
228
|
+
return {
|
|
229
|
+
res: null,
|
|
230
|
+
json: null,
|
|
231
|
+
failure: `endpoint redirected (${r.status})`,
|
|
232
|
+
reason: "redirected",
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
let json = null;
|
|
236
|
+
try {
|
|
237
|
+
json = JSON.parse(r.body);
|
|
238
|
+
} catch {
|
|
239
|
+
/* non-JSON answer: the status alone decides */
|
|
240
|
+
}
|
|
241
|
+
return { res: { ok: r.status >= 200 && r.status < 300, status: r.status }, json, failure: null };
|
|
223
242
|
}
|
|
224
243
|
|
|
225
244
|
const SSH_REFUSALS = {
|
|
@@ -265,11 +284,16 @@ function refusal(status, json, body, method) {
|
|
|
265
284
|
}
|
|
266
285
|
|
|
267
286
|
async function requestToken(url, body, proof, method) {
|
|
268
|
-
const { res, json, failure } = await postJson(url, body, proof);
|
|
269
|
-
if (failure) return { token: null, why: failure };
|
|
287
|
+
const { res, json, failure, reason } = await postJson(url, body, proof);
|
|
288
|
+
if (failure) return { token: null, why: failure, reason };
|
|
270
289
|
if (!res.ok) {
|
|
271
290
|
const code = typeof json?.code === "string" ? json.code : null;
|
|
272
|
-
return {
|
|
291
|
+
return {
|
|
292
|
+
token: null,
|
|
293
|
+
code,
|
|
294
|
+
why: refusal(res.status, json, body, method),
|
|
295
|
+
reason: httpReason(res.status, code),
|
|
296
|
+
};
|
|
273
297
|
}
|
|
274
298
|
if (json?.accepted === false)
|
|
275
299
|
return { token: null, declined: true, why: "the endpoint declined this login" };
|
|
@@ -277,7 +301,7 @@ async function requestToken(url, body, proof, method) {
|
|
|
277
301
|
const pinnedLogin = typeof json?.u === "string" && json.u.trim() ? json.u.trim() : null;
|
|
278
302
|
return token
|
|
279
303
|
? { token, why: null, login: pinnedLogin }
|
|
280
|
-
: { token: null, why: "endpoint returned no token" };
|
|
304
|
+
: { token: null, why: "endpoint returned no token", reason: "other" };
|
|
281
305
|
}
|
|
282
306
|
|
|
283
307
|
/**
|
|
@@ -323,20 +347,26 @@ export function challengeUrl(url) {
|
|
|
323
347
|
|
|
324
348
|
async function requestTokenWithSsh(url, body, key) {
|
|
325
349
|
const ch = await postJson(challengeUrl(url), { u: body.u });
|
|
326
|
-
if (ch.failure) return { token: null, why: ch.failure };
|
|
350
|
+
if (ch.failure) return { token: null, why: ch.failure, reason: ch.reason };
|
|
327
351
|
if (!ch.res.ok) {
|
|
328
352
|
if (ch.res.status === 404 || ch.res.status === 405) {
|
|
329
353
|
return {
|
|
330
354
|
token: null,
|
|
331
355
|
why: `the endpoint does not accept an SSH key as proof yet (challenge answered ${ch.res.status})`,
|
|
356
|
+
reason: "other",
|
|
332
357
|
};
|
|
333
358
|
}
|
|
334
|
-
|
|
359
|
+
const code = typeof ch.json?.code === "string" ? ch.json.code : null;
|
|
360
|
+
return {
|
|
361
|
+
token: null,
|
|
362
|
+
why: refusal(ch.res.status, ch.json, body, "ssh"),
|
|
363
|
+
reason: httpReason(ch.res.status, code),
|
|
364
|
+
};
|
|
335
365
|
}
|
|
336
366
|
const challenge = typeof ch.json?.challenge === "string" ? ch.json.challenge : "";
|
|
337
367
|
const namespace = typeof ch.json?.namespace === "string" ? ch.json.namespace : "";
|
|
338
368
|
if (!challenge || !/^[A-Za-z0-9@._-]{1,128}$/.test(namespace)) {
|
|
339
|
-
return { token: null, why: "the endpoint returned no usable SSH challenge" };
|
|
369
|
+
return { token: null, why: "the endpoint returned no usable SSH challenge", reason: "other" };
|
|
340
370
|
}
|
|
341
371
|
const signature = sshSign(key, namespace, challenge);
|
|
342
372
|
if (!signature) {
|
|
@@ -344,6 +374,7 @@ async function requestTokenWithSsh(url, body, key) {
|
|
|
344
374
|
return {
|
|
345
375
|
token: null,
|
|
346
376
|
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`,
|
|
377
|
+
reason: "no-proof",
|
|
347
378
|
};
|
|
348
379
|
}
|
|
349
380
|
return requestToken(url, { ...body, ssh: { challenge, signature } }, null, "ssh");
|
|
@@ -538,6 +569,57 @@ function reportingStaysOff(status, detail) {
|
|
|
538
569
|
return status === "skipped" && !QUIET_REASONS.has(detail);
|
|
539
570
|
}
|
|
540
571
|
|
|
572
|
+
export const LAST_REGISTER_FILE = "usage-last-register.json";
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Overwrites the record of the most recent registration attempt. Registration
|
|
576
|
+
* runs quietly from setup, update and Phase 0, so this file is where a machine
|
|
577
|
+
* that never registers shows why; doctor reads it. It holds no token.
|
|
578
|
+
*/
|
|
579
|
+
function recordRegister(home, status, detail) {
|
|
580
|
+
const dir = join(home, ".claude", "logs", "multi-agent");
|
|
581
|
+
const target = join(dir, LAST_REGISTER_FILE);
|
|
582
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
583
|
+
try {
|
|
584
|
+
mkdirSync(dir, { recursive: true });
|
|
585
|
+
const entry = {
|
|
586
|
+
t: new Date().toISOString(),
|
|
587
|
+
status,
|
|
588
|
+
detail: String(detail ?? "").slice(0, 300),
|
|
589
|
+
};
|
|
590
|
+
writeFileSync(tmp, JSON.stringify(entry) + "\n", { mode: 0o600 });
|
|
591
|
+
renameSync(tmp, target);
|
|
592
|
+
} catch {
|
|
593
|
+
/* logs dir unwritable - never break the caller */
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Report a failed registration to the site, detached so it never delays or
|
|
599
|
+
* fails the caller (usage-register-beacon.mjs). Only for a failure the person
|
|
600
|
+
* did not choose, never on a dry run or for an opted-out machine, only under a
|
|
601
|
+
* login this run had, and only to a register URL the proof may go to.
|
|
602
|
+
*/
|
|
603
|
+
function sendFailureBeacon({ home, prefsPath, login, reason, dryRun }) {
|
|
604
|
+
if (dryRun || !reason || typeof login !== "string" || !LOGIN_RE.test(login)) return;
|
|
605
|
+
const usageLog = readJson(prefsPath)?.global?.usageLog || {};
|
|
606
|
+
if (usageLog.optOut === true) return;
|
|
607
|
+
const url = registerUrl(usageLog.endpoint);
|
|
608
|
+
if (!registerUrlAllowed(url, usageLog.trustedHosts || [])) return;
|
|
609
|
+
try {
|
|
610
|
+
spawn(process.execPath, [join(import.meta.dirname, "usage-register-beacon.mjs")], {
|
|
611
|
+
detached: true,
|
|
612
|
+
stdio: "ignore",
|
|
613
|
+
env: {
|
|
614
|
+
...process.env,
|
|
615
|
+
[BEACON_ENV]: JSON.stringify({ home, url: beaconUrl(url), login, reason }),
|
|
616
|
+
},
|
|
617
|
+
}).unref();
|
|
618
|
+
} catch {
|
|
619
|
+
// reporting never fails the caller
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
541
623
|
async function main(argv) {
|
|
542
624
|
const opts = {
|
|
543
625
|
quiet: false,
|
|
@@ -566,7 +648,16 @@ async function main(argv) {
|
|
|
566
648
|
if (!opts.quiet) process.stdout.write(`${line}\n`);
|
|
567
649
|
};
|
|
568
650
|
const identity = { login: undefined };
|
|
569
|
-
const
|
|
651
|
+
const beacon = { login: null };
|
|
652
|
+
const done = (status, detail, reason = null) => {
|
|
653
|
+
if (!opts.dryRun) recordRegister(HOME, status, detail);
|
|
654
|
+
sendFailureBeacon({
|
|
655
|
+
home: HOME,
|
|
656
|
+
prefsPath,
|
|
657
|
+
login: beacon.login,
|
|
658
|
+
reason,
|
|
659
|
+
dryRun: opts.dryRun,
|
|
660
|
+
});
|
|
570
661
|
if (opts.json) {
|
|
571
662
|
const needsPersonalAccount = personalAccountMissing(readJson(prefsPath), identity.login);
|
|
572
663
|
process.stdout.write(`${JSON.stringify({ status, detail, needsPersonalAccount })}\n`);
|
|
@@ -602,7 +693,8 @@ async function main(argv) {
|
|
|
602
693
|
}
|
|
603
694
|
if (!opts.dryRun && !declareLogin(prefsPath, name)) {
|
|
604
695
|
say(" -> operational reporting: could not write preferences");
|
|
605
|
-
|
|
696
|
+
beacon.login = name;
|
|
697
|
+
return done("unavailable", "prefs write failed", "prefs-write");
|
|
606
698
|
}
|
|
607
699
|
opts.refresh = true;
|
|
608
700
|
}
|
|
@@ -629,6 +721,7 @@ async function main(argv) {
|
|
|
629
721
|
who = { login: declared, method: "claim", key: null, reason: "declared by the person" };
|
|
630
722
|
}
|
|
631
723
|
identity.login = who.login;
|
|
724
|
+
beacon.login = who.login || null;
|
|
632
725
|
const decision = decideRegistration(current, {
|
|
633
726
|
hasStoredToken: has,
|
|
634
727
|
forFeedback: opts.feedback,
|
|
@@ -649,7 +742,8 @@ async function main(argv) {
|
|
|
649
742
|
return done("enabled", decision.reason);
|
|
650
743
|
}
|
|
651
744
|
say(" -> operational reporting: could not write preferences");
|
|
652
|
-
|
|
745
|
+
beacon.login = beacon.login || cu.registeredAs || null;
|
|
746
|
+
return done("unavailable", "prefs write failed", "prefs-write");
|
|
653
747
|
}
|
|
654
748
|
}
|
|
655
749
|
if (
|
|
@@ -702,11 +796,11 @@ async function main(argv) {
|
|
|
702
796
|
? `the git credential helper no longer returns a GitHub token for ${user}; retry with --refresh`
|
|
703
797
|
: `gh has no token for ${user}; run \`gh auth login\` for that account`;
|
|
704
798
|
say(` -> operational reporting left off (${why})`);
|
|
705
|
-
return done("unavailable", why);
|
|
799
|
+
return done("unavailable", why, "no-proof");
|
|
706
800
|
}
|
|
707
801
|
result = await requestToken(url, { u: user, c: host }, proof, who.method || "gh");
|
|
708
802
|
}
|
|
709
|
-
const { token, why, declined, code, login: pinnedLogin } = result;
|
|
803
|
+
const { token, why, declined, code, reason, login: pinnedLogin } = result;
|
|
710
804
|
if (who.method === "claim" && ["not_collaborator", "proof_required"].includes(code))
|
|
711
805
|
declareLogin(prefsPath, null);
|
|
712
806
|
else if (who.method === "claim" && !token && !declined)
|
|
@@ -717,7 +811,7 @@ async function main(argv) {
|
|
|
717
811
|
}
|
|
718
812
|
if (!token) {
|
|
719
813
|
say(` -> operational reporting left off (${why})`);
|
|
720
|
-
return done("unavailable", why);
|
|
814
|
+
return done("unavailable", why, reason || "other");
|
|
721
815
|
}
|
|
722
816
|
|
|
723
817
|
const optedOut = prefs.global?.usageLog?.optOut === true;
|
|
@@ -729,7 +823,8 @@ async function main(argv) {
|
|
|
729
823
|
});
|
|
730
824
|
if (set.status !== 0) {
|
|
731
825
|
say(" -> operational reporting left off (the credential store refused the token)");
|
|
732
|
-
|
|
826
|
+
beacon.login = pinnedLogin || user;
|
|
827
|
+
return done("unavailable", "credential store write failed", "credential-store");
|
|
733
828
|
}
|
|
734
829
|
persistPrefs(prefsPath, entry, {
|
|
735
830
|
enableTelemetry: !optedOut,
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* Usage:
|
|
23
23
|
* node usage-report.mjs --state /path/to/agent-state.json [--event <kind>]
|
|
24
24
|
* node usage-report.mjs --task-id PROJ-123 [--phase N] [--event <kind>]
|
|
25
|
+
* node usage-report.mjs --task-id PROJ-123 --print-state (prints the run's agent-state.json path)
|
|
25
26
|
*
|
|
26
27
|
* node usage-report.mjs --lifecycle (report the installed version when it changed)
|
|
27
28
|
* --event phase | completed | halted | parked (default: derived from state)
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
* @module pipeline/scripts/usage-report
|
|
30
31
|
*/
|
|
31
32
|
|
|
33
|
+
import { postJson } from "./_http-post.mjs";
|
|
32
34
|
import {
|
|
33
35
|
readFileSync,
|
|
34
36
|
existsSync,
|
|
@@ -86,6 +88,7 @@ function parseArgs(argv) {
|
|
|
86
88
|
else if (a === "--event") out.event = argv[++i];
|
|
87
89
|
else if (a === "--dry-run") out.dryRun = true;
|
|
88
90
|
else if (a === "--lifecycle") out.lifecycle = true;
|
|
91
|
+
else if (a === "--print-state") out.printState = true;
|
|
89
92
|
}
|
|
90
93
|
return out;
|
|
91
94
|
}
|
|
@@ -737,40 +740,31 @@ function hostOf(url) {
|
|
|
737
740
|
* instead, so a moved endpoint is visible rather than silent.
|
|
738
741
|
*/
|
|
739
742
|
async function post(endpoint, token, event) {
|
|
740
|
-
const
|
|
741
|
-
|
|
743
|
+
const r = await postJson({
|
|
744
|
+
url: endpoint,
|
|
745
|
+
headers: { "X-Usage-Token": token },
|
|
746
|
+
body: JSON.stringify(event),
|
|
747
|
+
timeoutMs: TIMEOUT_MS,
|
|
748
|
+
});
|
|
749
|
+
const via = r.fetchFailure ? { transport: r.transport, fetchFailure: r.fetchFailure } : {};
|
|
750
|
+
if (r.failure) {
|
|
751
|
+
return { outcome: "failed", reason: codeOf(String(r.failure), 40), ...via };
|
|
752
|
+
}
|
|
753
|
+
if (r.status >= 300 && r.status < 400) {
|
|
754
|
+
return { outcome: "redirected", status: r.status, location: hostOf(r.location || ""), ...via };
|
|
755
|
+
}
|
|
756
|
+
if (r.status < 200 || r.status >= 300) return { outcome: "rejected", status: r.status, ...via };
|
|
757
|
+
let body = null;
|
|
742
758
|
try {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
"Content-Type": "application/json",
|
|
747
|
-
"X-Usage-Token": token,
|
|
748
|
-
},
|
|
749
|
-
body: JSON.stringify(event),
|
|
750
|
-
redirect: "manual",
|
|
751
|
-
signal: ctrl.signal,
|
|
752
|
-
});
|
|
753
|
-
if (res.status >= 300 && res.status < 400) {
|
|
754
|
-
return {
|
|
755
|
-
outcome: "redirected",
|
|
756
|
-
status: res.status,
|
|
757
|
-
location: hostOf(res.headers.get("location") || ""),
|
|
758
|
-
};
|
|
759
|
-
}
|
|
760
|
-
if (!res.ok) return { outcome: "rejected", status: res.status };
|
|
761
|
-
let body = null;
|
|
762
|
-
try {
|
|
763
|
-
body = await res.json();
|
|
764
|
-
} catch {
|
|
765
|
-
/* a 2xx without a JSON body is still a delivery */
|
|
766
|
-
}
|
|
767
|
-
return { outcome: body?.accepted === false ? "not-accepted" : "accepted", status: res.status };
|
|
768
|
-
} catch (err) {
|
|
769
|
-
const reason = ctrl.signal.aborted ? "timeout" : (err?.cause?.code ?? err?.name ?? "error");
|
|
770
|
-
return { outcome: "failed", reason: codeOf(String(reason), 40) };
|
|
771
|
-
} finally {
|
|
772
|
-
clearTimeout(timer);
|
|
759
|
+
body = JSON.parse(r.body);
|
|
760
|
+
} catch {
|
|
761
|
+
/* a 2xx without a JSON body is still a delivery */
|
|
773
762
|
}
|
|
763
|
+
return {
|
|
764
|
+
outcome: body?.accepted === false ? "not-accepted" : "accepted",
|
|
765
|
+
status: r.status,
|
|
766
|
+
...via,
|
|
767
|
+
};
|
|
774
768
|
}
|
|
775
769
|
|
|
776
770
|
export const LAST_SEND_FILE = "usage-last-send.json";
|
|
@@ -852,6 +846,13 @@ function registeredLogin(prefs) {
|
|
|
852
846
|
async function main() {
|
|
853
847
|
const args = parseArgs(process.argv.slice(2));
|
|
854
848
|
if (process.env.MULTI_AGENT_SMOKE && !args.dryRun) return;
|
|
849
|
+
// Resolving the state path needs no prefs and sends nothing: a shell block
|
|
850
|
+
// whose $STATE_FILE did not survive from an earlier command gets it back here.
|
|
851
|
+
if (args.printState) {
|
|
852
|
+
const path = resolveStatePath(args);
|
|
853
|
+
if (path) process.stdout.write(`${path}\n`);
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
855
856
|
const prefs = resolvePrefs();
|
|
856
857
|
const endpoint = canonicalEndpoint(prefs.endpoint || ENDPOINT_DEFAULT);
|
|
857
858
|
|
|
@@ -883,7 +884,16 @@ async function main() {
|
|
|
883
884
|
let state = statePath ? readJson(statePath) : null;
|
|
884
885
|
|
|
885
886
|
if (!state) {
|
|
886
|
-
if (!args.taskId)
|
|
887
|
+
if (!args.taskId) {
|
|
888
|
+
// An unset shell variable reaches here as --state "", and a silent return
|
|
889
|
+
// would make a run that never reports look exactly like one that did.
|
|
890
|
+
recordSend({
|
|
891
|
+
ev: EVENT_KINDS.includes(args.event) ? args.event : null,
|
|
892
|
+
outcome: "skipped",
|
|
893
|
+
reason: args.state === "" ? "empty-state-path" : "no-state",
|
|
894
|
+
});
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
887
897
|
state = { taskId: args.taskId };
|
|
888
898
|
}
|
|
889
899
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "1.0.0",
|
|
3
|
-
"generatedAt": "2026-10-
|
|
4
|
-
"skillCount":
|
|
3
|
+
"generatedAt": "2026-10-24T11:39:37Z",
|
|
4
|
+
"skillCount": 234,
|
|
5
5
|
"entries": [
|
|
6
6
|
{
|
|
7
7
|
"path": "shared/core/apple-archive-compliance/SKILL.md",
|
|
8
|
-
"sha256": "
|
|
8
|
+
"sha256": "08d5277e4a6c246c613bd3bea27646f580f1bdc1937cb4acbbd93a156e15b24e"
|
|
9
9
|
},
|
|
10
10
|
{
|
|
11
11
|
"path": "shared/core/google-play-compliance/SKILL.md",
|
|
@@ -237,7 +237,7 @@
|
|
|
237
237
|
},
|
|
238
238
|
{
|
|
239
239
|
"path": "shared/core/multi-agent-sync/SKILL.md",
|
|
240
|
-
"sha256": "
|
|
240
|
+
"sha256": "e96afa9b89a2ab6e2fe13a01158f98272c6828e5cef03c8909240766aaefd44b"
|
|
241
241
|
},
|
|
242
242
|
{
|
|
243
243
|
"path": "shared/core/multi-agent-test-accessibility/SKILL.md",
|
|
@@ -251,6 +251,10 @@
|
|
|
251
251
|
"path": "shared/core/multi-agent-test-dynamic-type/SKILL.md",
|
|
252
252
|
"sha256": "e1cadd5eb8d680b0edfc40a4912733dc6a432493c868f4997b2dec0db6638dd4"
|
|
253
253
|
},
|
|
254
|
+
{
|
|
255
|
+
"path": "shared/core/multi-agent-test-gen/SKILL.md",
|
|
256
|
+
"sha256": "0da450ed0c74ab610d3c8f0cf1b0facd2a4ae179afc832440939c3367de7e762"
|
|
257
|
+
},
|
|
254
258
|
{
|
|
255
259
|
"path": "shared/core/multi-agent-test-screenshots/SKILL.md",
|
|
256
260
|
"sha256": "a41507304e4650079e6359b3ad5854f90f45a90172911f487c4c923ef8839f91"
|
|
@@ -345,7 +349,7 @@
|
|
|
345
349
|
},
|
|
346
350
|
{
|
|
347
351
|
"path": "shared/external/app-store-review/SKILL.md",
|
|
348
|
-
"sha256": "
|
|
352
|
+
"sha256": "073d99485665497229750b5f040c3a196c1e5de2bbcff281b8aec7e53817d3f4"
|
|
349
353
|
},
|
|
350
354
|
{
|
|
351
355
|
"path": "shared/external/apple-on-device-ai/SKILL.md",
|
|
@@ -487,6 +491,10 @@
|
|
|
487
491
|
"path": "shared/external/firebase/SKILL.md",
|
|
488
492
|
"sha256": "d377217a03e7fc63bd7eba285c6e2b450f293b13c6e65680e71b022cd14872a3"
|
|
489
493
|
},
|
|
494
|
+
{
|
|
495
|
+
"path": "shared/external/fix-review-verdicts/SKILL.md",
|
|
496
|
+
"sha256": "a728b7c87fbf175e959e23b655c626294b7a7a2949c62a37e7b3d4a172ce57d5"
|
|
497
|
+
},
|
|
490
498
|
{
|
|
491
499
|
"path": "shared/external/github-actions-templates/SKILL.md",
|
|
492
500
|
"sha256": "13b6fec454c2ebf69b06fcdc00033a6dab98d6111e0346447c8d474db547f801"
|
|
@@ -495,6 +503,10 @@
|
|
|
495
503
|
"path": "shared/external/gradle-kotlin-dsl/SKILL.md",
|
|
496
504
|
"sha256": "af7b1e30e2448f43b5100b2a935bbaadde86ccd490844c901736c9270f8cc3ce"
|
|
497
505
|
},
|
|
506
|
+
{
|
|
507
|
+
"path": "shared/external/handle-request/SKILL.md",
|
|
508
|
+
"sha256": "05ecf72d8e40eeca8dfc0913764b1188b9b1189ea098ad23b4f0c8256f2c671c"
|
|
509
|
+
},
|
|
498
510
|
{
|
|
499
511
|
"path": "shared/external/healthkit/SKILL.md",
|
|
500
512
|
"sha256": "fece1986bc35925e34fed8d8cb13aa59a3577b8cedefdc02c736c0d4a5d38aff"
|
|
@@ -703,6 +715,10 @@
|
|
|
703
715
|
"path": "shared/external/room-database/SKILL.md",
|
|
704
716
|
"sha256": "45fe377469f1fb4036431194d3b525d3caaa79231808625f643468863c6e814a"
|
|
705
717
|
},
|
|
718
|
+
{
|
|
719
|
+
"path": "shared/external/scenario-gap-analysis/SKILL.md",
|
|
720
|
+
"sha256": "fcd9f614dc18ea0d4610469f7edcf9214d3b14d65976fa3fb7c4a524330a392a"
|
|
721
|
+
},
|
|
706
722
|
{
|
|
707
723
|
"path": "shared/external/search-first/SKILL.md",
|
|
708
724
|
"sha256": "c9d5ffb698bbfb76a89da6a35dfc0f1d72a31d2b4a0e9e9f3e027be7a8fddfde"
|
|
@@ -855,6 +871,10 @@
|
|
|
855
871
|
"path": "shared/external/tailwind-css/SKILL.md",
|
|
856
872
|
"sha256": "9eda0a871962f52e381781b95c948128626984a82ddbd47be53ea869cd250b7e"
|
|
857
873
|
},
|
|
874
|
+
{
|
|
875
|
+
"path": "shared/external/test-result-triage/SKILL.md",
|
|
876
|
+
"sha256": "72b0e0049fcf549b61a189900ca8e503b779e93cd26575d7b7328ae785465e38"
|
|
877
|
+
},
|
|
858
878
|
{
|
|
859
879
|
"path": "shared/external/testing-backend/SKILL.md",
|
|
860
880
|
"sha256": "d3eea1c6d1e2a40a0b66bc6460194ee313e17fc2af8f75047030b7840c2db857"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "1.0.0",
|
|
3
|
-
"skillCount":
|
|
3
|
+
"skillCount": 234,
|
|
4
4
|
"entries": [
|
|
5
5
|
{
|
|
6
6
|
"name": "accessibility-compliance-accessibility-audit",
|
|
@@ -191,7 +191,7 @@
|
|
|
191
191
|
},
|
|
192
192
|
{
|
|
193
193
|
"name": "app-store-review",
|
|
194
|
-
"description": "App Store submission readiness and rejection prevention: Review Guidelines 2.1, 2.3, 2.5.1, 3.1.1, 4.2 and 5.1.2, PrivacyInfo.xcprivacy manifests and required-reason codes, IAP and StoreKit rules, ATT consent, EU DMA
|
|
194
|
+
"description": "App Store submission readiness and rejection prevention: Review Guidelines 1.2, 2.1, 2.3, 2.3.14, 2.5.1, 2.5.19, 3.1.1, 4.2 and 5.1.2, upload floors, age rating and medical device declarations, PrivacyInfo.xcprivacy manifests and required-reason codes, IAP and StoreKit rules, ATT consent, EU DMA dis",
|
|
195
195
|
"platform": null,
|
|
196
196
|
"group": "external",
|
|
197
197
|
"plugin": "ai-ios-toolkit",
|
|
@@ -596,6 +596,17 @@
|
|
|
596
596
|
"triggerPaths": [],
|
|
597
597
|
"relativePath": "shared/external/firebase/SKILL.md"
|
|
598
598
|
},
|
|
599
|
+
{
|
|
600
|
+
"name": "fix-review-verdicts",
|
|
601
|
+
"description": "Independent fix review with three verdicts, test-gaming, scope and root-cause, each backed by file:line findings; the result is computed from findings, never from a claim. Use when reviewing a fix made to turn tests green. Not a full review checklist.",
|
|
602
|
+
"platform": null,
|
|
603
|
+
"group": "external",
|
|
604
|
+
"plugin": "ai-common-toolkit",
|
|
605
|
+
"invokeAs": "ai-common-toolkit:fix-review-verdicts",
|
|
606
|
+
"triggerKeywords": [],
|
|
607
|
+
"triggerPaths": [],
|
|
608
|
+
"relativePath": "shared/external/fix-review-verdicts/SKILL.md"
|
|
609
|
+
},
|
|
599
610
|
{
|
|
600
611
|
"name": "github-actions-templates",
|
|
601
612
|
"description": "Authoring GitHub Actions workflows: triggers, matrices, caching, concurrency, least-privilege permissions, SHA pinning, OIDC, reusable workflows, composite actions, Xcode runners. Use when writing or hardening a workflow file.",
|
|
@@ -629,6 +640,17 @@
|
|
|
629
640
|
"triggerPaths": [],
|
|
630
641
|
"relativePath": "shared/external/gradle-kotlin-dsl/SKILL.md"
|
|
631
642
|
},
|
|
643
|
+
{
|
|
644
|
+
"name": "handle-request",
|
|
645
|
+
"description": "Add the test handles a scenario needs and answer each request with ADDED, ALREADY or COULD NOT ADD plus file:line or reason. Identity values only, attribute-only diffs; iOS accessibilityIdentifier, Compose testTag, web data-testid. Use when a test needs an element handle the UI lacks.",
|
|
646
|
+
"platform": null,
|
|
647
|
+
"group": "external",
|
|
648
|
+
"plugin": "ai-common-toolkit",
|
|
649
|
+
"invokeAs": "ai-common-toolkit:handle-request",
|
|
650
|
+
"triggerKeywords": [],
|
|
651
|
+
"triggerPaths": [],
|
|
652
|
+
"relativePath": "shared/external/handle-request/SKILL.md"
|
|
653
|
+
},
|
|
632
654
|
{
|
|
633
655
|
"name": "healthkit",
|
|
634
656
|
"description": "HealthKit on iOS, iPadOS, watchOS and visionOS: HKHealthStore authorization, sample, statistics and chart-ready collection queries, saving HKQuantitySample values, background delivery, live workouts (HKWorkoutSession, HKLiveWorkoutBuilder), HKUnit, HKQuantityTypeIdentifier. Use when reading, writing",
|
|
@@ -1685,6 +1707,17 @@
|
|
|
1685
1707
|
"triggerPaths": [],
|
|
1686
1708
|
"relativePath": "shared/core/multi-agent-test-dynamic-type/SKILL.md"
|
|
1687
1709
|
},
|
|
1710
|
+
{
|
|
1711
|
+
"name": "multi-agent-test-gen",
|
|
1712
|
+
"description": "Write the tests an app lacks, stage by stage, each gated on its artefact: inventory, read-only gap analysis, handles, tests, runs, per-test classification. Use when screens, flows or a change need tests.",
|
|
1713
|
+
"platform": null,
|
|
1714
|
+
"group": "core",
|
|
1715
|
+
"plugin": null,
|
|
1716
|
+
"invokeAs": "multi-agent-test-gen",
|
|
1717
|
+
"triggerKeywords": [],
|
|
1718
|
+
"triggerPaths": [],
|
|
1719
|
+
"relativePath": "shared/core/multi-agent-test-gen/SKILL.md"
|
|
1720
|
+
},
|
|
1688
1721
|
{
|
|
1689
1722
|
"name": "multi-agent-test-screenshots",
|
|
1690
1723
|
"description": "App Store screenshot set from a booted simulator / emulator in a given locale, taken as an argument and defaulting to tr. Alias pinning the screenshot scenario. Use when generating store screenshots or checking a localised build renders.",
|
|
@@ -1927,6 +1960,17 @@
|
|
|
1927
1960
|
"triggerPaths": [],
|
|
1928
1961
|
"relativePath": "shared/external/room-database/SKILL.md"
|
|
1929
1962
|
},
|
|
1963
|
+
{
|
|
1964
|
+
"name": "scenario-gap-analysis",
|
|
1965
|
+
"description": "Set an inventory against existing tests, sort each behaviour into COVERED, WRONGLY COVERED or MISSING, and write gaps as measurable, prioritised scenarios with cross-screen flows. Use when planning tests or auditing coverage. Not for writing tests or judging a run.",
|
|
1966
|
+
"platform": null,
|
|
1967
|
+
"group": "external",
|
|
1968
|
+
"plugin": "ai-common-toolkit",
|
|
1969
|
+
"invokeAs": "ai-common-toolkit:scenario-gap-analysis",
|
|
1970
|
+
"triggerKeywords": [],
|
|
1971
|
+
"triggerPaths": [],
|
|
1972
|
+
"relativePath": "shared/external/scenario-gap-analysis/SKILL.md"
|
|
1973
|
+
},
|
|
1930
1974
|
{
|
|
1931
1975
|
"name": "search-first",
|
|
1932
1976
|
"description": "Research-before-coding workflow. Before writing a custom implementation, check what already exists (repo utilities, installed libraries, packages, internal components) and decide adopt / extend / compose / build on evidence. Use at the start of any non-trivial feature or utility, especially when you",
|
|
@@ -2345,6 +2389,17 @@
|
|
|
2345
2389
|
"triggerPaths": [],
|
|
2346
2390
|
"relativePath": "shared/external/tailwind-css/SKILL.md"
|
|
2347
2391
|
},
|
|
2392
|
+
{
|
|
2393
|
+
"name": "test-result-triage",
|
|
2394
|
+
"description": "Judge each test result on changed code as product-bug, stale-test, valid or ambiguous (the default), and list behaviour no test covers. stale-test needs a diff file:line, product-bug needs evidence. Use when tests ran on a new commit, PR or fix. Not for writing or editing tests.",
|
|
2395
|
+
"platform": null,
|
|
2396
|
+
"group": "external",
|
|
2397
|
+
"plugin": "ai-common-toolkit",
|
|
2398
|
+
"invokeAs": "ai-common-toolkit:test-result-triage",
|
|
2399
|
+
"triggerKeywords": [],
|
|
2400
|
+
"triggerPaths": [],
|
|
2401
|
+
"relativePath": "shared/external/test-result-triage/SKILL.md"
|
|
2402
|
+
},
|
|
2348
2403
|
{
|
|
2349
2404
|
"name": "testing-backend",
|
|
2350
2405
|
"description": "Backend testing: pytest patterns (Python), Jest patterns (Node.js), integration testing, database fixtures/factories, API contract testing. Use when writing or reviewing backend tests: unit, integration, database and API level.",
|