@keepsnappy/agent 0.1.0-alpha.24 → 0.1.0-alpha.25
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/README.md +2 -2
- package/dist/appium-driver.js +39 -7
- package/dist/keepsnappy.js +829 -446
- package/package.json +1 -1
package/dist/keepsnappy.js
CHANGED
|
@@ -560,10 +560,144 @@ async function readProjectConfig(read, explicit, cwd = process.cwd()) {
|
|
|
560
560
|
|
|
561
561
|
// apps/agent/src/run-command.ts
|
|
562
562
|
import { readFile } from "node:fs/promises";
|
|
563
|
-
import { join as
|
|
563
|
+
import { join as join10, resolve as resolve3 } from "node:path";
|
|
564
564
|
|
|
565
|
-
// apps/agent/src/
|
|
565
|
+
// apps/agent/src/device-select.ts
|
|
566
|
+
import { basename } from "node:path";
|
|
567
|
+
|
|
568
|
+
// apps/agent/src/device-model.ts
|
|
569
|
+
import { tmpdir } from "node:os";
|
|
566
570
|
import { join as join2 } from "node:path";
|
|
571
|
+
function parseDevicectlList(json) {
|
|
572
|
+
let data;
|
|
573
|
+
try {
|
|
574
|
+
data = JSON.parse(json);
|
|
575
|
+
} catch {
|
|
576
|
+
return [];
|
|
577
|
+
}
|
|
578
|
+
const devices = Array.isArray(data.result?.devices) ? data.result.devices : [];
|
|
579
|
+
return devices.flatMap((d) => {
|
|
580
|
+
const hw = d.hardwareProperties ?? {};
|
|
581
|
+
const conn = d.connectionProperties ?? {};
|
|
582
|
+
if (typeof hw.productType !== "string" || !hw.productType)
|
|
583
|
+
return [];
|
|
584
|
+
const text = (v) => typeof v === "string" ? v : "";
|
|
585
|
+
return [{ productType: hw.productType, udid: text(hw.udid), identifier: text(d.identifier), serial: text(hw.serialNumber), connected: conn.tunnelState === "connected" }];
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
var parseSimulators = (json) => {
|
|
589
|
+
try {
|
|
590
|
+
const data = JSON.parse(json);
|
|
591
|
+
return Object.values(data.devices ?? {}).flat();
|
|
592
|
+
} catch {
|
|
593
|
+
return [];
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
var withSimulatorWord = (name) => /simulator/i.test(name) ? name : `${name} Simulator`;
|
|
597
|
+
var simulatorModel = (s) => withSimulatorWord(s.name);
|
|
598
|
+
async function androidModel(deps, id) {
|
|
599
|
+
const r = await deps.spawner.run("adb", [...id ? ["-s", id] : [], "shell", "getprop", "ro.product.model"]);
|
|
600
|
+
const model = r.code === 0 ? (r.stdout.split(`
|
|
601
|
+
`)[0] ?? "").trim() : "";
|
|
602
|
+
return model || null;
|
|
603
|
+
}
|
|
604
|
+
async function simulators(deps, id) {
|
|
605
|
+
const r = await deps.spawner.run("xcrun", ["simctl", "list", "devices", ...id ? [] : ["booted"], "-j"]);
|
|
606
|
+
if (r.code !== 0)
|
|
607
|
+
return [];
|
|
608
|
+
return parseSimulators(r.stdout).filter((s) => id ? s.udid === id : s.state === "Booted");
|
|
609
|
+
}
|
|
610
|
+
async function phones(deps) {
|
|
611
|
+
const out = join2(tmpdir(), `keepsnappy-devicectl-${deps.clock.now()}.json`);
|
|
612
|
+
try {
|
|
613
|
+
const r = await deps.spawner.run("xcrun", ["devicectl", "list", "devices", "--quiet", "--json-output", out]);
|
|
614
|
+
const text = r.code === 0 ? await deps.fs.readFile(out).catch(() => "") : "";
|
|
615
|
+
return parseDevicectlList(text);
|
|
616
|
+
} finally {
|
|
617
|
+
await deps.spawner.run("rm", ["-f", out]);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
async function iosModel(deps, id) {
|
|
621
|
+
const sims = await simulators(deps, id);
|
|
622
|
+
if (id) {
|
|
623
|
+
if (sims.length === 1)
|
|
624
|
+
return simulatorModel(sims[0]);
|
|
625
|
+
const found = (await phones(deps)).filter((p) => [p.udid, p.identifier, p.serial].some((x) => x.toLowerCase() === id.toLowerCase()));
|
|
626
|
+
return found.length === 1 ? found[0].productType : null;
|
|
627
|
+
}
|
|
628
|
+
const connected = (await phones(deps)).filter((p) => p.connected);
|
|
629
|
+
if (sims.length + connected.length !== 1)
|
|
630
|
+
return null;
|
|
631
|
+
return sims[0] ? simulatorModel(sims[0]) : connected[0].productType;
|
|
632
|
+
}
|
|
633
|
+
async function detectDeviceModel(deps, q) {
|
|
634
|
+
if (q.platform === "android")
|
|
635
|
+
return androidModel(deps, q.deviceId);
|
|
636
|
+
if (q.platform === "ios")
|
|
637
|
+
return iosModel(deps, q.deviceId);
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
async function detectSimulator(deps, udid) {
|
|
641
|
+
const sims = await simulators(deps, udid);
|
|
642
|
+
return sims.length === 1 ? simulatorModel(sims[0]) : null;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// apps/agent/src/device-select.ts
|
|
646
|
+
var CHOOSE = "name one with --device <UDID>, or `device: <UDID>` in keepsnappy.yml";
|
|
647
|
+
var label = (d) => `${d.name} (${d.udid})`;
|
|
648
|
+
var list = (ds) => ds.map(label).join(", ");
|
|
649
|
+
async function simulators2(deps) {
|
|
650
|
+
const r = await deps.spawner.run("xcrun", ["simctl", "list", "devices", "-j"]);
|
|
651
|
+
if (r.code !== 0)
|
|
652
|
+
throw new ConfigError([`cannot list simulators: ${(r.stderr || r.error || `xcrun simctl exited ${r.code}`).trim()}`]);
|
|
653
|
+
return parseSimulators(r.stdout);
|
|
654
|
+
}
|
|
655
|
+
async function resolveRunDevice(deps, platform, named) {
|
|
656
|
+
if (platform === "ios-device") {
|
|
657
|
+
if (!named)
|
|
658
|
+
throw new ConfigError([`no device named: ${CHOOSE}`]);
|
|
659
|
+
const phone = (await phones(deps)).find((p) => [p.udid, p.identifier, p.serial].some((x) => x.toLowerCase() === named.toLowerCase()));
|
|
660
|
+
if (!phone)
|
|
661
|
+
throw new ConfigError([`device ${named} not found: no connected iPhone has that id (xcrun devicectl list devices)`]);
|
|
662
|
+
if (!phone.connected)
|
|
663
|
+
throw new ConfigError([`device ${named} is not connected: plug it in and unlock it`]);
|
|
664
|
+
return named;
|
|
665
|
+
}
|
|
666
|
+
const all = await simulators2(deps);
|
|
667
|
+
const booted = all.filter((s) => s.state === "Booted");
|
|
668
|
+
if (named) {
|
|
669
|
+
const sim = all.find((s) => s.udid.toLowerCase() === named.toLowerCase());
|
|
670
|
+
if (!sim)
|
|
671
|
+
throw new ConfigError([`simulator ${named} not found${booted.length ? ` (booted: ${list(booted)})` : " (none is booted)"}`]);
|
|
672
|
+
if (sim.state !== "Booted")
|
|
673
|
+
throw new ConfigError([`simulator ${label(sim)} is not booted: xcrun simctl boot ${sim.udid}${booted.length ? `, or ${CHOOSE} (booted: ${list(booted)})` : ""}`]);
|
|
674
|
+
return sim.udid;
|
|
675
|
+
}
|
|
676
|
+
if (booted.length === 0)
|
|
677
|
+
throw new ConfigError([`no simulator is booted: boot one (xcrun simctl boot <UDID>) and ${CHOOSE}`]);
|
|
678
|
+
if (booted.length > 1)
|
|
679
|
+
throw new ConfigError([`several simulators are booted and none is named: ${list(booted)}. Maestro would take the first, so ${CHOOSE}`]);
|
|
680
|
+
return booted[0].udid;
|
|
681
|
+
}
|
|
682
|
+
var DEVICE_FLAG = /^--(?:device|udid)(?:=(.*))?$/;
|
|
683
|
+
function withDevice(flow, udid) {
|
|
684
|
+
const [cmd, ...args] = flow.command;
|
|
685
|
+
if (!cmd || basename(cmd) !== "maestro")
|
|
686
|
+
return flow;
|
|
687
|
+
for (const [i, a] of args.entries()) {
|
|
688
|
+
const m = DEVICE_FLAG.exec(a);
|
|
689
|
+
if (!m)
|
|
690
|
+
continue;
|
|
691
|
+
const given = m[1] ?? args[i + 1];
|
|
692
|
+
if (given === udid)
|
|
693
|
+
return flow;
|
|
694
|
+
throw new ConfigError([`the command names device ${given ?? "(none)"} but this run uses ${udid}: drop --device from the command, it is passed for you`]);
|
|
695
|
+
}
|
|
696
|
+
return { ...flow, command: [cmd, "--device", udid, ...args] };
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// apps/agent/src/data-source.ts
|
|
700
|
+
import { join as join3 } from "node:path";
|
|
567
701
|
var KEEPSNAPPY_LOG = "Documents/keepsnappy/log.txt";
|
|
568
702
|
var SAFE_FILE = /^[\w.+:-]+$/;
|
|
569
703
|
var profilePath = (fileName) => {
|
|
@@ -603,10 +737,10 @@ class IosSimulatorDataSource {
|
|
|
603
737
|
return dir;
|
|
604
738
|
}
|
|
605
739
|
async fetchProfile(bundleId, fileName) {
|
|
606
|
-
return this.deps.fs.readFile(
|
|
740
|
+
return this.deps.fs.readFile(join3(await this.dataDir(bundleId), profilePath(fileName)));
|
|
607
741
|
}
|
|
608
742
|
async fetchLog(bundleId) {
|
|
609
|
-
return this.deps.fs.readFile(
|
|
743
|
+
return this.deps.fs.readFile(join3(await this.dataDir(bundleId), KEEPSNAPPY_LOG));
|
|
610
744
|
}
|
|
611
745
|
async describeDevice() {
|
|
612
746
|
const r = await this.deps.spawner.run("xcrun", ["simctl", "list", "devices", this.udid === "booted" ? "booted" : "", "-j"].filter(Boolean));
|
|
@@ -625,7 +759,7 @@ class IosDeviceDataSource {
|
|
|
625
759
|
}
|
|
626
760
|
async fetchLog(bundleId) {
|
|
627
761
|
await this.deps.fs.mkdir(this.scratchDir);
|
|
628
|
-
const dest =
|
|
762
|
+
const dest = join3(this.scratchDir, "keepsnappy.log");
|
|
629
763
|
const r = await this.deps.spawner.run("xcrun", [
|
|
630
764
|
"devicectl",
|
|
631
765
|
"device",
|
|
@@ -670,7 +804,7 @@ class IosDeviceDataSource {
|
|
|
670
804
|
}
|
|
671
805
|
async fetchProfile(bundleId, fileName) {
|
|
672
806
|
await this.deps.fs.mkdir(this.scratchDir);
|
|
673
|
-
const dest =
|
|
807
|
+
const dest = join3(this.scratchDir, fileName);
|
|
674
808
|
const r = await this.deps.spawner.run("xcrun", [
|
|
675
809
|
"devicectl",
|
|
676
810
|
"device",
|
|
@@ -694,10 +828,10 @@ class IosDeviceDataSource {
|
|
|
694
828
|
}
|
|
695
829
|
|
|
696
830
|
// apps/agent/src/flows.ts
|
|
697
|
-
import { basename, dirname, join as
|
|
831
|
+
import { basename as basename2, dirname, join as join4 } from "node:path";
|
|
698
832
|
var FLOW_FILE = /\.ya?ml$/i;
|
|
699
833
|
var NOT_FLOWS = new Set(["config.yaml", "config.yml", "keepsnappy.yml", "keepsnappy.yaml", "keepsnappy.yml", "keepsnappy.yaml"]);
|
|
700
|
-
var flowStem = (path) =>
|
|
834
|
+
var flowStem = (path) => basename2(path).replace(FLOW_FILE, "");
|
|
701
835
|
function flowTags(text) {
|
|
702
836
|
const head = text.split(/^---\s*$/m)[0] ?? "";
|
|
703
837
|
const inline = /^tags:\s*\[([^\]]*)\]/m.exec(head);
|
|
@@ -713,11 +847,11 @@ async function isDirectory(path, listDir) {
|
|
|
713
847
|
if (path === "." || path === ".." || path.endsWith("/"))
|
|
714
848
|
return true;
|
|
715
849
|
const entries = await listDir(dirname(path) || ".").catch(() => []);
|
|
716
|
-
return entries.some((e) => e.name ===
|
|
850
|
+
return entries.some((e) => e.name === basename2(path) && e.isDir);
|
|
717
851
|
}
|
|
718
852
|
function uniqueNames(files) {
|
|
719
853
|
const stems = files.map(flowStem);
|
|
720
|
-
return files.map((f, i) => stems.filter((s) => s === stems[i]).length > 1 ? `${
|
|
854
|
+
return files.map((f, i) => stems.filter((s) => s === stems[i]).length > 1 ? `${basename2(dirname(f))}-${stems[i]}` : stems[i]);
|
|
721
855
|
}
|
|
722
856
|
async function findFlowFiles(paths, info, listDir) {
|
|
723
857
|
const out = new Map;
|
|
@@ -733,7 +867,7 @@ async function findFlowFiles(paths, info, listDir) {
|
|
|
733
867
|
}
|
|
734
868
|
const dir = p.replace(/\/+$/, "") || ".";
|
|
735
869
|
const entries = await listDir(dir).catch(() => []);
|
|
736
|
-
files.push(...entries.filter((e) => !e.isDir && FLOW_FILE.test(e.name) && !NOT_FLOWS.has(e.name)).map((e) => dir === "." ? e.name :
|
|
870
|
+
files.push(...entries.filter((e) => !e.isDir && FLOW_FILE.test(e.name) && !NOT_FLOWS.has(e.name)).map((e) => dir === "." ? e.name : join4(dir, e.name)).sort());
|
|
737
871
|
}
|
|
738
872
|
const meta = info[pat];
|
|
739
873
|
if (meta && (meta.title || meta.description) && files.length !== 1) {
|
|
@@ -2379,7 +2513,7 @@ function withLaunchNames(windows) {
|
|
|
2379
2513
|
}
|
|
2380
2514
|
|
|
2381
2515
|
// packages/derive/dist/src/model.js
|
|
2382
|
-
var DERIVATION_VERSION =
|
|
2516
|
+
var DERIVATION_VERSION = 31;
|
|
2383
2517
|
// packages/stats/dist/src/core/rng.js
|
|
2384
2518
|
function splitmix32(seed) {
|
|
2385
2519
|
let s = seed >>> 0;
|
|
@@ -4722,10 +4856,11 @@ function footprintAdded(windows) {
|
|
|
4722
4856
|
return first && last ? footprintEnd(last) - footprintStart(first) : undefined;
|
|
4723
4857
|
}
|
|
4724
4858
|
var RECORD_EDGE_MS = 1;
|
|
4859
|
+
var isEndGcRead = (s) => typeof s.raw.reason === "string" && s.raw.reason.startsWith("endgc-read:");
|
|
4725
4860
|
var SAME_PROCESS_MS = 1000;
|
|
4726
|
-
function windowEndFootprints(placed, records) {
|
|
4861
|
+
function windowEndFootprints(placed, records, version) {
|
|
4727
4862
|
const out = new Map;
|
|
4728
|
-
const recs = (records ?? []).map((s) => ({ t: num7(s.raw.t), offset: num7(s.raw.t) === undefined ? undefined : s.wall - num7(s.raw.t), mb: num7(obj7(s.raw.mem).footprintMb) })).filter((r) => r.t !== undefined).sort((a, b) => a.t - b.t);
|
|
4863
|
+
const recs = (records ?? []).filter((s) => version < 31 || !isEndGcRead(s)).map((s) => ({ t: num7(s.raw.t), offset: num7(s.raw.t) === undefined ? undefined : s.wall - num7(s.raw.t), mb: num7(obj7(s.raw.mem).footprintMb) })).filter((r) => r.t !== undefined).sort((a, b) => a.t - b.t);
|
|
4729
4864
|
for (const w of placed) {
|
|
4730
4865
|
const own = footprintEnd(w);
|
|
4731
4866
|
if (own !== undefined) {
|
|
@@ -4754,7 +4889,7 @@ function v7Metrics(windows, steps, records, version) {
|
|
|
4754
4889
|
for (const [name, v] of added)
|
|
4755
4890
|
out.set(`step:${name}.sampler_footprint_delta_mb`, median(v));
|
|
4756
4891
|
const placed = onAppClock(windows, version).filter((w) => w.raw.endReason !== "timeout");
|
|
4757
|
-
const ends = windowEndFootprints(placed, records);
|
|
4892
|
+
const ends = windowEndFootprints(placed, records, version);
|
|
4758
4893
|
const withEnd = [...ends.keys()];
|
|
4759
4894
|
const launchWindow = [...withEnd].sort((a, b) => a.startWall - b.startWall || a.endWall - b.endWall)[0];
|
|
4760
4895
|
const lastWindow = [...withEnd].sort((a, b) => b.endWall - a.endWall)[0];
|
|
@@ -5134,7 +5269,7 @@ function keyFirstOpens(windows) {
|
|
|
5134
5269
|
}
|
|
5135
5270
|
|
|
5136
5271
|
// packages/derive/dist/src/derive.js
|
|
5137
|
-
var DERIVATION_VERSIONS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 26, 27, 28, 29, 30];
|
|
5272
|
+
var DERIVATION_VERSIONS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 26, 27, 28, 29, 30, 31];
|
|
5138
5273
|
var windowsForVersion = (windows, version = DERIVATION_VERSION, nav) => version >= 22 ? keyFirstOpens(navKeyed(windows, nav, version)) : windows;
|
|
5139
5274
|
function stepWindowsFor(windows, version = DERIVATION_VERSION, nav) {
|
|
5140
5275
|
return windowsForVersion(version >= 16 ? withLaunchNames(windows).filter((w) => !isBackgroundWindow(w)) : windows, version, nav);
|
|
@@ -5983,23 +6118,18 @@ function tip(headline, rows = [], foot) {
|
|
|
5983
6118
|
return foot ? { headline, rows: made, foot } : { headline, rows: made };
|
|
5984
6119
|
}
|
|
5985
6120
|
|
|
5986
|
-
// packages/present/dist/src/
|
|
5987
|
-
var
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
6121
|
+
// packages/present/dist/src/not-compared.js
|
|
6122
|
+
var NOT_COMPARED_KIND_WORDS = {
|
|
6123
|
+
"new-place": "New place",
|
|
6124
|
+
"no-baseline": "No history",
|
|
6125
|
+
"measured-differently": "Measured differently",
|
|
6126
|
+
"too-few-runs": "Too few valid runs",
|
|
6127
|
+
overheated: "Phone started overheated",
|
|
6128
|
+
"main-unstable": "Not judged",
|
|
6129
|
+
other: "Not compared"
|
|
5995
6130
|
};
|
|
5996
|
-
var
|
|
5997
|
-
|
|
5998
|
-
{ look: "slower", word: "slower" },
|
|
5999
|
-
{ look: "faster", word: "faster" },
|
|
6000
|
-
{ look: "cant", word: stateWordInSentence("cant-tell") }
|
|
6001
|
-
];
|
|
6002
|
-
var PLACES_OF = { slower: `${STATE_WORDS.worse} places`, faster: `${STATE_WORDS.better} places` };
|
|
6131
|
+
var NEW_PLACE_WORDS = { branch: "main has no history for it yet", journey: "no earlier session has it yet" };
|
|
6132
|
+
|
|
6003
6133
|
// packages/present/dist/src/verdict-section.js
|
|
6004
6134
|
var places2 = (n) => `${n.toLocaleString("en-US")} ${n === 1 ? "place" : "places"}`;
|
|
6005
6135
|
var of = (n, total) => `${n.toLocaleString("en-US")} of ${places2(total)}`;
|
|
@@ -6087,10 +6217,15 @@ var MAIN_TIER_RESULT_WORDS = {
|
|
|
6087
6217
|
slight: STATE_WORDS.warning,
|
|
6088
6218
|
worse: STATE_WORDS.worse
|
|
6089
6219
|
};
|
|
6220
|
+
var STACK_AT = { same: 0, slight: 1, worse: 2, better: 3, cant: 4 };
|
|
6221
|
+
var MAIN_TIER_STACK = Object.keys(STACK_AT).sort((p, q) => STACK_AT[p] - STACK_AT[q]);
|
|
6222
|
+
var HAS_VERDICT = { same: false, slight: true, worse: true, better: true, cant: false };
|
|
6223
|
+
var MAIN_VERDICT_TIERS = Object.keys(HAS_VERDICT).filter((t) => HAS_VERDICT[t]);
|
|
6090
6224
|
var MAIN_TIER_RANK = { worse: 0, slight: 1, cant: 2, better: 3, same: 4 };
|
|
6091
6225
|
var earlierWords = (sessions, since, config) => `main has ${sessions}${config.baseline.sessions === undefined ? "" : ` of ${config.baseline.sessions}`} earlier sessions${since ? ` on ${since}` : ""}`;
|
|
6092
6226
|
var UNRELIABLE_WORDS = {
|
|
6093
6227
|
"overheated-start": "the phone started overheated; this session does not count",
|
|
6228
|
+
"overheated-slow": "the phone was overheated; left out of this comparison",
|
|
6094
6229
|
"few-valid-runs": "too few valid runs; this session does not count"
|
|
6095
6230
|
};
|
|
6096
6231
|
function mainCantWords(reason, config) {
|
|
@@ -6160,29 +6295,67 @@ function mainCutWords(v) {
|
|
|
6160
6295
|
return parts.length ? parts.join(" ") : "Measures alike; no baseline cut.";
|
|
6161
6296
|
}
|
|
6162
6297
|
var BY_RANK = Object.keys(MAIN_TIER_RANK).sort((a, b) => MAIN_TIER_RANK[a] - MAIN_TIER_RANK[b]);
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
["
|
|
6169
|
-
["
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6298
|
+
|
|
6299
|
+
// packages/present/dist/src/app-cpu.js
|
|
6300
|
+
var APP_CPU_WORDS = {
|
|
6301
|
+
title: "App CPU · cores",
|
|
6302
|
+
titleTip: tip("App CPU", [
|
|
6303
|
+
["Shows", "the app’s process, not the whole phone"],
|
|
6304
|
+
["Line", "The median of a session’s runs"],
|
|
6305
|
+
["Band", "From its lowest to its highest run"],
|
|
6306
|
+
["1 core", "One CPU core fully busy; above 1 is usual on a multi-core phone"]
|
|
6307
|
+
]),
|
|
6308
|
+
none: "No CPU reading yet",
|
|
6309
|
+
sessionNone: "No CPU reading for this session yet.",
|
|
6310
|
+
runTitle: "App CPU over the run · cores",
|
|
6311
|
+
runTip: tip("App CPU over the run", [
|
|
6312
|
+
["Shows", "the app’s process, not the whole phone"],
|
|
6313
|
+
["Block", "One step of the journey, in run order"],
|
|
6314
|
+
["Width", "How long the step took"],
|
|
6315
|
+
["Height", "The app’s average cores while it ran"],
|
|
6316
|
+
["Time", "Steps laid end to end; launch and time between steps are not shown."]
|
|
6317
|
+
]),
|
|
6318
|
+
noSteps: "No App CPU for this run: it has no declared steps with a CPU reading.",
|
|
6319
|
+
axisEnd: "s of steps",
|
|
6320
|
+
mean: "mean",
|
|
6321
|
+
loading: "Loading the run…",
|
|
6322
|
+
failed: "Couldn’t load the run.",
|
|
6323
|
+
medianRun: (k, n) => `Median run: Run ${k} of the ${n} runs with a CPU reading`,
|
|
6324
|
+
otherRuns: "Other runs",
|
|
6325
|
+
runChip: (run, median) => `Run ${run}${median ? " · median" : ""}`,
|
|
6326
|
+
runAria: (steps, seconds) => `App CPU over the run in cores: ${steps} steps in run order over ${seconds} seconds. Use left and right arrows for each step.`
|
|
6177
6327
|
};
|
|
6328
|
+
var NOT_STORED_WORD = "not stored";
|
|
6329
|
+
|
|
6330
|
+
// packages/present/dist/src/tips.js
|
|
6331
|
+
var LEAD_TIP = tip("A lead, not a confirmed cause", [], "Read from what the app did");
|
|
6332
|
+
|
|
6333
|
+
// packages/present/dist/src/placeStrip.js
|
|
6334
|
+
var PLACE_STRIP_WORDS = {
|
|
6335
|
+
strip: "vs main",
|
|
6336
|
+
value: "This session",
|
|
6337
|
+
change: "Change",
|
|
6338
|
+
noRange: `${NOT_COMPARED_KIND_WORDS["new-place"]}: ${NEW_PLACE_WORDS.journey}`
|
|
6339
|
+
};
|
|
6340
|
+
// packages/present/dist/src/slowerScreens.js
|
|
6341
|
+
var SLOWER_CHART_WORDS = {
|
|
6342
|
+
title: (dir) => dir === "faster" ? "Faster by" : "Slower by",
|
|
6343
|
+
sub: "ms vs main, by screen · biggest first",
|
|
6344
|
+
noChange: stateWordInSentence("no-change"),
|
|
6345
|
+
moved: "moved in some runs",
|
|
6346
|
+
cantTell: stateWordInSentence("cant-tell")
|
|
6347
|
+
};
|
|
6348
|
+
var PLACES_OF = { slower: `${STATE_WORDS.worse} places`, faster: `${STATE_WORDS.better} places` };
|
|
6178
6349
|
// packages/present/dist/src/rules-lines.js
|
|
6179
6350
|
var RULES_WORDS = {
|
|
6180
6351
|
action: "Recompute",
|
|
6181
6352
|
older: "Computed with older rules",
|
|
6182
6353
|
marker: "new rules",
|
|
6354
|
+
markerShort: "rules",
|
|
6183
6355
|
blocksUnderPrevious: "blocks under the previous rules",
|
|
6184
6356
|
bannerText: "New rules: pull requests wait for a recompute",
|
|
6185
6357
|
bannerAction: "Recompute",
|
|
6358
|
+
bannerSeparator: "·",
|
|
6186
6359
|
engine: "engine",
|
|
6187
6360
|
engineUpdate: "engine update"
|
|
6188
6361
|
};
|
|
@@ -6190,7 +6363,7 @@ var sameRulesRef = (a, b) => a.configId === b.configId && a.coreVersion === b.co
|
|
|
6190
6363
|
var rulesChangeText = (computed, live) => sameRulesRef(computed, live) ? null : RULES_WORDS.older;
|
|
6191
6364
|
var staleRulesText = (rules) => rules ? rulesChangeText(rules.computed, rules.live) : null;
|
|
6192
6365
|
var blocksUnderPreviousRulesText = (r) => r.blocking && r.stale ? RULES_WORDS.blocksUnderPrevious : null;
|
|
6193
|
-
var waitingForRecomputeBanner = (r) => r.waiting > 0 ? `${RULES_WORDS.bannerText}
|
|
6366
|
+
var waitingForRecomputeBanner = (r) => r.waiting > 0 ? `${RULES_WORDS.bannerText} ${RULES_WORDS.bannerSeparator} ${RULES_WORDS.bannerAction}` : null;
|
|
6194
6367
|
|
|
6195
6368
|
// packages/present/dist/src/summary-message.js
|
|
6196
6369
|
var SUMMARY_WORDS = {
|
|
@@ -6232,6 +6405,7 @@ var ORDER = [...STATE_ORDER.slice(0, 3), "not-computed", ...STATE_ORDER.slice(3)
|
|
|
6232
6405
|
var rank = (l) => ORDER.indexOf(l.state);
|
|
6233
6406
|
var WORD = { ...STATE_WORDS, "not-computed": SUMMARY_WORDS.notComputed };
|
|
6234
6407
|
var shownLane = (lanes) => [...lanes].sort((a, b) => rank(a) - rank(b) || b.places - a.places)[0];
|
|
6408
|
+
var outcomeOf = (lanes) => new Set(lanes.map((x) => x.phone ?? "-")).size === 1 ? shownLane(lanes)?.outcome ?? null : null;
|
|
6235
6409
|
var clip = (s, n) => s.length > n ? `${s.slice(0, n - 1)}…` : s;
|
|
6236
6410
|
var NAME = { name: 32, button: 30 };
|
|
6237
6411
|
var TOP2 = 3;
|
|
@@ -6323,8 +6497,9 @@ function summaryMessage(input) {
|
|
|
6323
6497
|
const phones = new Set(input.lanes.map((x) => x.phone ?? "-")).size;
|
|
6324
6498
|
const single = input.lanes.length === 1;
|
|
6325
6499
|
const newestSlower = !input.pr && (l.state === "worse" || l.state === "warning");
|
|
6500
|
+
const said = outcomeOf(input.lanes);
|
|
6326
6501
|
const lead = [
|
|
6327
|
-
laneWord(l),
|
|
6502
|
+
said?.word ?? laneWord(l),
|
|
6328
6503
|
...input.app ? [input.app] : [],
|
|
6329
6504
|
journeys === 1 ? l.journey ?? "journey" : SUMMARY_WORDS.journeys(journeys),
|
|
6330
6505
|
input.pr && input.prNumber ? `${input.branch} (#${input.prNumber})` : input.branch,
|
|
@@ -6333,7 +6508,7 @@ function summaryMessage(input) {
|
|
|
6333
6508
|
const label = input.pr ? SUMMARY_WORDS.link.comparison : SUMMARY_WORDS.link.session;
|
|
6334
6509
|
const link = { label, url: (single ? l.url : null) ?? input.url };
|
|
6335
6510
|
if (!input.slack)
|
|
6336
|
-
return { state: l.state, newestSlower, lead, ...withLines(changeLines(l, input.pr, newestSlower)), link };
|
|
6511
|
+
return { state: l.state, newestSlower, lead, ...withLines(changeLines(l, input.pr, newestSlower, said)), link };
|
|
6337
6512
|
const lines = [];
|
|
6338
6513
|
if (l.state === "worse" || l.state === "warning") {
|
|
6339
6514
|
const n = l.places || l.changes.length;
|
|
@@ -6356,10 +6531,12 @@ function summaryMessage(input) {
|
|
|
6356
6531
|
if (context)
|
|
6357
6532
|
lines.push(basis ? context : upper(context));
|
|
6358
6533
|
lines.push(...rulesLines(l));
|
|
6534
|
+
if (said)
|
|
6535
|
+
lines[0] = `${said.line} · ${lines[0]}`;
|
|
6359
6536
|
return { state: l.state, newestSlower, lead, ...withLines(lines), link };
|
|
6360
6537
|
}
|
|
6361
|
-
function changeLines(l, pr, newestSlower) {
|
|
6362
|
-
const lines = [];
|
|
6538
|
+
function changeLines(l, pr, newestSlower, said) {
|
|
6539
|
+
const lines = said ? [said.line] : [];
|
|
6363
6540
|
const basis = compared(l, pr, newestSlower);
|
|
6364
6541
|
const extra = conditions(l, pr).join(", ");
|
|
6365
6542
|
const heat = l.heatedRuns ? upper(SUMMARY_WORDS.heated(l.heatedRuns, l.runs)) : null;
|
|
@@ -6372,7 +6549,7 @@ function changeLines(l, pr, newestSlower) {
|
|
|
6372
6549
|
const key = c.key ?? c.place ?? c.measure;
|
|
6373
6550
|
return seen.has(key) ? false : (seen.add(key), true);
|
|
6374
6551
|
});
|
|
6375
|
-
const room = MAX_LINES - tail.length;
|
|
6552
|
+
const room = MAX_LINES - lines.length - tail.length;
|
|
6376
6553
|
const top = places.length > room ? places.slice(0, Math.min(TOP2, room - 1)) : places.slice(0, TOP2);
|
|
6377
6554
|
lines.push(...top.map((c) => changeLine(c, l.state)));
|
|
6378
6555
|
if (!top.length)
|
|
@@ -6396,6 +6573,43 @@ function noResultMessage(branch, reason, url) {
|
|
|
6396
6573
|
return { state: "not-computed", newestSlower: false, lead: `${SUMMARY_WORDS.noResult} · ${clip(branch, NAME.name)}`, ...withLines([reason.replace(/\.$/, "")]), link: url ? { label: SUMMARY_WORDS.link.runSet, url } : null };
|
|
6397
6574
|
}
|
|
6398
6575
|
|
|
6576
|
+
// packages/present/dist/src/fix-words.js
|
|
6577
|
+
var FIX_VERDICT_WORDS = {
|
|
6578
|
+
fixed: "Fixed",
|
|
6579
|
+
"no-change": STATE_WORDS["no-change"],
|
|
6580
|
+
worse: STATE_WORDS.worse,
|
|
6581
|
+
waiting: "Waiting for the fix branch"
|
|
6582
|
+
};
|
|
6583
|
+
var ORDER2 = Object.keys(FIX_VERDICT_WORDS);
|
|
6584
|
+
var FIX_OUTCOME_WORDS = {
|
|
6585
|
+
fixed: FIX_VERDICT_WORDS.fixed,
|
|
6586
|
+
"partly-fixed": "Partly fixed",
|
|
6587
|
+
"no-change": FIX_VERDICT_WORDS["no-change"],
|
|
6588
|
+
worse: FIX_VERDICT_WORDS.worse,
|
|
6589
|
+
waiting: FIX_VERDICT_WORDS.waiting
|
|
6590
|
+
};
|
|
6591
|
+
var fixCardText = (c) => [`${c.outcome_word}: ${c.headline}`, ...c.lines.map((l) => ` ${l.text}`), ...c.notes.map((n) => ` ${n}`)].join(`
|
|
6592
|
+
`);
|
|
6593
|
+
|
|
6594
|
+
// packages/present/dist/src/branch-outcome.js
|
|
6595
|
+
var BRANCH_OUTCOME_WORDS = {
|
|
6596
|
+
title: "Since the first session",
|
|
6597
|
+
fixed: FIX_VERDICT_WORDS.fixed,
|
|
6598
|
+
"still-worse": "Still worse",
|
|
6599
|
+
"got-worse": "Got worse",
|
|
6600
|
+
reRun: "re-run",
|
|
6601
|
+
first: "First session",
|
|
6602
|
+
judging: "Judging",
|
|
6603
|
+
empty: ["No session of this branch yet.", "Sessions come from your CI or the perf: measure label."]
|
|
6604
|
+
};
|
|
6605
|
+
function saidOutcome(o) {
|
|
6606
|
+
return o && (o.id === "fixed" || o.id === "still-worse" || o.id === "got-worse") && o.word && o.line && o.state ? o : null;
|
|
6607
|
+
}
|
|
6608
|
+
function outcomeLines(o) {
|
|
6609
|
+
const said = saidOutcome(o);
|
|
6610
|
+
return [...said ? [`${said.word}: ${said.line}`] : [], ...o?.pending ? [o.pending] : []];
|
|
6611
|
+
}
|
|
6612
|
+
|
|
6399
6613
|
// packages/present/dist/src/run-set-message.js
|
|
6400
6614
|
var midLine = (label, metric) => !metric.startsWith("component:") && /^[A-Z][a-z]*(\s|$)/.test(label) ? `${label[0].toLowerCase()}${label.slice(1)}` : label;
|
|
6401
6615
|
function movedItems(v, sc) {
|
|
@@ -6491,7 +6705,8 @@ function laneOf(v, sc, set) {
|
|
|
6491
6705
|
fasterByMs: sc.headline?.faster_by_ms ?? null,
|
|
6492
6706
|
toCheckCount: sc.headline?.to_check_count ?? null,
|
|
6493
6707
|
rules: sc.headline?.rules ?? null,
|
|
6494
|
-
waiting: sc.headline?.waiting_for_reprocess ?? 0
|
|
6708
|
+
waiting: sc.headline?.waiting_for_reprocess ?? 0,
|
|
6709
|
+
outcome: saidOutcome(sc.branch_outcome)
|
|
6495
6710
|
};
|
|
6496
6711
|
}
|
|
6497
6712
|
var isPending = (s) => lanesOf(s).every((l) => l.state === "not-computed" && !l.word?.startsWith(`${NOT_JUDGED_WORD}: `));
|
|
@@ -6506,7 +6721,7 @@ function messageTone(s) {
|
|
|
6506
6721
|
return { mark: "\uD83D\uDD34", color: MESSAGE_COLORS.error };
|
|
6507
6722
|
if (lanes.some((l) => l.state === "warning"))
|
|
6508
6723
|
return { mark: "\uD83D\uDFE1", color: MESSAGE_COLORS.warn };
|
|
6509
|
-
switch (shownLane(lanes)?.state) {
|
|
6724
|
+
switch (outcomeOf(lanes)?.state ?? shownLane(lanes)?.state) {
|
|
6510
6725
|
case "better":
|
|
6511
6726
|
return { mark: "\uD83D\uDFE2", color: MESSAGE_COLORS.better };
|
|
6512
6727
|
case "cant-tell":
|
|
@@ -6594,32 +6809,51 @@ function notificationBody(e) {
|
|
|
6594
6809
|
return `${e.word}, ${e.headline.component} ${e.headline.change}`;
|
|
6595
6810
|
return [e.word, e.journey, e.phone ? phoneModelName(e.phone) : e.phone].filter(Boolean).join(" · ");
|
|
6596
6811
|
}
|
|
6812
|
+
var shellWord = (s) => /^[A-Za-z0-9_@%+=:,./-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`;
|
|
6597
6813
|
var FIX_END_WORDS = {
|
|
6598
6814
|
"dry-run": (n) => `Dry run: ${n} ${n === 1 ? "commit" : "commits"} on a local branch; nothing was pushed or opened.`,
|
|
6599
6815
|
opened: (n) => `Draft PR opened with ${n} ${n === 1 ? "commit" : "commits"}.`,
|
|
6600
|
-
none: () => "Nothing to fix: the agent found nothing it could edit safely.",
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
|
|
6610
|
-
|
|
6816
|
+
none: () => "Nothing to fix: the agent found nothing it could edit safely — investigate again, then Propose fixes.",
|
|
6817
|
+
"no-findings": () => "Nothing to fix on this branch now — its findings were resolved or are no longer slower; nothing was started.",
|
|
6818
|
+
refused: () => "The change was refused by the checks; nothing was committed — the refused files are listed below.",
|
|
6819
|
+
main: () => "Main has no branch to fix — propose fixes on a pull request instead.",
|
|
6820
|
+
fork: () => "The pull request comes from a fork — push its branch to the repo itself, then Propose fixes.",
|
|
6821
|
+
closed: () => "The pull request is closed — reopen it, then Propose fixes.",
|
|
6822
|
+
exists: (_n, o = {}) => o.fixBranch && o.repo ? `The fix branch ${o.fixBranch} is left from an earlier run — delete it with \`git -C ${shellWord(o.repo)} branch -D ${shellWord(o.fixBranch)}\`, then Propose fixes.` : "A fix branch for this pull request already exists — review or delete it, then Propose fixes.",
|
|
6823
|
+
"unsafe-checkout": () => "The checkout has a link that points outside it; nothing was started — remove the link from the branch, then Propose fixes.",
|
|
6824
|
+
timeout: () => "It ran out of time; nothing was committed — pick fewer findings, then Propose fixes.",
|
|
6825
|
+
failed: () => "The fix did not complete — the log in KeepSnappy’s state folder says why; try again.",
|
|
6826
|
+
"not-json": () => "The agent’s answer was not the list of changes it made — try again.",
|
|
6827
|
+
schema: () => "The agent’s list of changes did not match what it changed — try again.",
|
|
6611
6828
|
stopped: () => "It was stopped.",
|
|
6612
|
-
push: () => "The branch could not be pushed.",
|
|
6613
|
-
gh: () => "GitHub (gh)
|
|
6614
|
-
|
|
6829
|
+
push: () => "The branch could not be pushed — check that you can push to the repo, then try again.",
|
|
6830
|
+
gh: () => "GitHub (gh) could not read or open the pull request — check `gh pr view` in the repo folder, then try again.",
|
|
6831
|
+
"gh-auth": () => "GitHub CLI (gh) is not signed in — run `gh auth login`, then Propose fixes.",
|
|
6832
|
+
"no-pr": () => "No pull request on GitHub for this branch — open one, then Propose fixes.",
|
|
6833
|
+
"no-repo": () => "No local checkout of this app — set its folder in Settings › Apps & code, then Propose fixes.",
|
|
6834
|
+
api: () => "The KeepSnappy API did not answer — try again in a minute."
|
|
6615
6835
|
};
|
|
6616
|
-
var fixEndText = (status, commits) => Object.hasOwn(FIX_END_WORDS, status) ? FIX_END_WORDS[status](commits) : null;
|
|
6836
|
+
var fixEndText = (status, commits, o = {}) => Object.hasOwn(FIX_END_WORDS, status) ? FIX_END_WORDS[status](commits, o) : null;
|
|
6617
6837
|
var FIX_WHY_NOT = {
|
|
6618
6838
|
main: "Main has no branch to fix.",
|
|
6619
6839
|
notWorse: "Only Worse and Slightly worse findings are fixed.",
|
|
6620
|
-
notMeasured: "Not a measured change."
|
|
6840
|
+
notMeasured: "Not a measured change.",
|
|
6841
|
+
noPr: FIX_END_WORDS["no-pr"](),
|
|
6842
|
+
ghAuth: FIX_END_WORDS["gh-auth"](),
|
|
6843
|
+
gh: "GitHub didn’t answer — check the network, then try again.",
|
|
6844
|
+
fork: FIX_END_WORDS.fork(),
|
|
6845
|
+
closed: FIX_END_WORDS.closed()
|
|
6846
|
+
};
|
|
6847
|
+
var fixNoRepoText = (app) => `No local checkout of ${app} — set its folder in Settings › Apps & code.`;
|
|
6848
|
+
var FIX_REJECT_REASONS = ["too-many", "bad-command", "unknown-app", "duplicate"];
|
|
6849
|
+
var FIX_REJECT_WORDS = {
|
|
6850
|
+
"too-many": (o) => `Up to ${o.max} findings per fix, and this asked for ${o.count} — uncheck some, then Propose fixes.`,
|
|
6851
|
+
"bad-command": () => "The watcher could not read this fix command — update KeepSnappy, then try again.",
|
|
6852
|
+
"unknown-app": () => "The watcher has not loaded this app’s results yet — wait a moment, then Retry.",
|
|
6853
|
+
duplicate: () => "A fix of these findings is already running or queued — its result shows here when it ends."
|
|
6621
6854
|
};
|
|
6622
|
-
var
|
|
6855
|
+
var fixRejectText = (reason, o = { count: 0, max: 0 }) => FIX_REJECT_REASONS.includes(reason) ? FIX_REJECT_WORDS[reason](o) : null;
|
|
6856
|
+
var FIX_NOTIFY_STATUSES = ["opened", "refused", "failed", "timeout", "not-json", "schema", "unsafe-checkout", "push", "gh", "gh-auth", "no-pr", "no-repo", "api"];
|
|
6623
6857
|
// packages/present/dist/src/hypothesis-words.js
|
|
6624
6858
|
var HYPOTHESIS_OUTCOME_WORDS = {
|
|
6625
6859
|
open: "Open",
|
|
@@ -6627,10 +6861,10 @@ var HYPOTHESIS_OUTCOME_WORDS = {
|
|
|
6627
6861
|
rejected: "Rejected",
|
|
6628
6862
|
dismissed: "Dismissed"
|
|
6629
6863
|
};
|
|
6630
|
-
var
|
|
6864
|
+
var ORDER3 = ["open", "confirmed", "rejected", "dismissed"];
|
|
6631
6865
|
var HYPOTHESIS_NOTE = "Predictions from a coding agent: only a later measured session confirms or rejects one.";
|
|
6632
6866
|
function hypothesesSummaryText(counts) {
|
|
6633
|
-
const present =
|
|
6867
|
+
const present = ORDER3.map((id) => [id, counts[id] ?? 0]).filter(([, n]) => n > 0);
|
|
6634
6868
|
const total = present.reduce((sum, [, n]) => sum + n, 0);
|
|
6635
6869
|
if (total === 0)
|
|
6636
6870
|
return "No hypotheses";
|
|
@@ -6687,48 +6921,8 @@ function runDurationText(ms) {
|
|
|
6687
6921
|
return `${m}m ${s % 60}s`;
|
|
6688
6922
|
return `${Math.floor(m / 60)}h ${m % 60}m`;
|
|
6689
6923
|
}
|
|
6690
|
-
// packages/present/dist/src/fix-words.js
|
|
6691
|
-
var FIX_VERDICT_WORDS = {
|
|
6692
|
-
fixed: "Fixed",
|
|
6693
|
-
"no-change": STATE_WORDS["no-change"],
|
|
6694
|
-
worse: STATE_WORDS.worse,
|
|
6695
|
-
waiting: "Waiting for the fix branch"
|
|
6696
|
-
};
|
|
6697
|
-
var ORDER3 = Object.keys(FIX_VERDICT_WORDS);
|
|
6698
|
-
// packages/present/dist/src/app-cpu.js
|
|
6699
|
-
var APP_CPU_WORDS = {
|
|
6700
|
-
title: "App CPU · cores",
|
|
6701
|
-
titleTip: tip("App CPU", [
|
|
6702
|
-
["Shows", "the app’s process, not the whole phone"],
|
|
6703
|
-
["Line", "The median of a session’s runs"],
|
|
6704
|
-
["Band", "From its lowest to its highest run"],
|
|
6705
|
-
["1 core", "One CPU core fully busy; above 1 is usual on a multi-core phone"]
|
|
6706
|
-
]),
|
|
6707
|
-
none: "No CPU reading yet",
|
|
6708
|
-
sessionNone: "No CPU reading for this session yet.",
|
|
6709
|
-
runTitle: "App CPU over the run · cores",
|
|
6710
|
-
runTip: tip("App CPU over the run", [
|
|
6711
|
-
["Shows", "the app’s process, not the whole phone"],
|
|
6712
|
-
["Block", "One step of the journey, in run order"],
|
|
6713
|
-
["Width", "How long the step took"],
|
|
6714
|
-
["Height", "The app’s average cores while it ran"],
|
|
6715
|
-
["Time", "Steps laid end to end; launch and time between steps are not shown."]
|
|
6716
|
-
]),
|
|
6717
|
-
noSteps: "No App CPU for this run: it has no declared steps with a CPU reading.",
|
|
6718
|
-
axisEnd: "s of steps",
|
|
6719
|
-
mean: "mean",
|
|
6720
|
-
loading: "Loading the run…",
|
|
6721
|
-
failed: "Couldn’t load the run.",
|
|
6722
|
-
medianRun: (k, n) => `Median run: Run ${k} of the ${n} runs with a CPU reading`,
|
|
6723
|
-
otherRuns: "Other runs",
|
|
6724
|
-
runChip: (run, median) => `Run ${run}${median ? " · median" : ""}`,
|
|
6725
|
-
runAria: (steps, seconds) => `App CPU over the run in cores: ${steps} steps in run order over ${seconds} seconds. Use left and right arrows for each step.`
|
|
6726
|
-
};
|
|
6727
|
-
|
|
6728
|
-
// packages/present/dist/src/tips.js
|
|
6729
|
-
var LEAD_TIP = tip("A lead, not a confirmed cause", [], "Read from what the app did");
|
|
6730
6924
|
// apps/agent/src/upload.ts
|
|
6731
|
-
import { join as
|
|
6925
|
+
import { join as join5 } from "node:path";
|
|
6732
6926
|
var MAX_RATE_RETRIES = 5;
|
|
6733
6927
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
6734
6928
|
var pause = (ms) => sleep(ms);
|
|
@@ -6777,7 +6971,7 @@ async function uploadRuns(deps, outDir, runs, opts) {
|
|
|
6777
6971
|
skipped.push({ run_id: r.run_id, why: r.invalid_reason || "invalid" });
|
|
6778
6972
|
continue;
|
|
6779
6973
|
}
|
|
6780
|
-
const log = await deps.fs.readFile(
|
|
6974
|
+
const log = await deps.fs.readFile(join5(outDir, r.run_id, "keepsnappy.log"));
|
|
6781
6975
|
const windows = windowsInRun(parseWindows(log), r.t0, r.t1);
|
|
6782
6976
|
if (windows.length === 0) {
|
|
6783
6977
|
skipped.push({ run_id: r.run_id, why: "no-windows" });
|
|
@@ -6817,7 +7011,7 @@ async function uploadRuns(deps, outDir, runs, opts) {
|
|
|
6817
7011
|
}
|
|
6818
7012
|
let markers = [];
|
|
6819
7013
|
try {
|
|
6820
|
-
markers = parseMarkersNdjson(await deps.fs.readFile(
|
|
7014
|
+
markers = parseMarkersNdjson(await deps.fs.readFile(join5(outDir, r.run_id, "markers.ndjson")));
|
|
6821
7015
|
} catch {}
|
|
6822
7016
|
for (const m of markers) {
|
|
6823
7017
|
await call(f, opts.ingestUrl, "/v1/markers", { authorization: `Bearer ${token}` }, { step: m.step, phase: m.phase, t_host: m.t_host });
|
|
@@ -6865,17 +7059,17 @@ function summarize(input) {
|
|
|
6865
7059
|
}
|
|
6866
7060
|
|
|
6867
7061
|
// packages/verdict-core/dist/src/version.generated.js
|
|
6868
|
-
var CORE_FINGERPRINT = "
|
|
7062
|
+
var CORE_FINGERPRINT = "ae0b19d47d41bbfe";
|
|
6869
7063
|
|
|
6870
7064
|
// packages/verdict-core/dist/src/version.js
|
|
6871
7065
|
var CORE_VERSION = parseInt(CORE_FINGERPRINT.slice(0, 7), 16);
|
|
6872
|
-
// packages/verdict-core/dist/src/reliable.js
|
|
6873
|
-
var prRunsNeeded = (config) => config.reliable?.minValidRuns ?? config.minRuns;
|
|
6874
|
-
|
|
6875
7066
|
// packages/verdict-core/dist/src/window-count.js
|
|
6876
7067
|
var WINDOW_DEPENDENT_JOURNEY_MEASURES = ["sampler.cpu_ms", "sampler.frames.longest_gap_ms"];
|
|
6877
7068
|
var OFF = new WeakSet;
|
|
6878
7069
|
|
|
7070
|
+
// packages/verdict-core/dist/src/reliable.js
|
|
7071
|
+
var prRunsNeeded = (config) => config.reliable.minValidRuns;
|
|
7072
|
+
|
|
6879
7073
|
// packages/verdict-core/dist/src/clauses.js
|
|
6880
7074
|
var row = (id, what, unit, cmp, ruling, uncalibrated, value) => ({ id, what, unit, cmp, ruling, uncalibrated, value });
|
|
6881
7075
|
var NO_RULING = "no ruling: a starting value of the deciding config (L2 audit F-13)";
|
|
@@ -6888,7 +7082,7 @@ var CLAUSES = {
|
|
|
6888
7082
|
"S-UNCERTAIN": row("S-UNCERTAIN", "A worse result is uncertain and does not block while main has fewer sessions than this.", "sessions", "≥", 'REQ-VER-38, Arek 2026-10-16 "Nie blokuj przy małej bazie"', false, { belowMainSessions: 3 }),
|
|
6889
7083
|
"G-JUDGE-FROM": row("G-JUDGE-FROM", "A phone or simulator is judged only with at least this many main sessions.", "sessions", "≥", "REQ-VER-41, Arek 2026-10-18", false, { mainSessions: 3 }),
|
|
6890
7084
|
"W-WORKKIND": row("W-WORKKIND", "The share of render time spent mounting from which a place is an in-screen mount, not an update.", "share", "≥", "REQ-MET-25; TraceSift MOUNT_DOMINANT_SHARE", true, { mountShareAtLeast: 0.8 }),
|
|
6891
|
-
"W-WINDOW-COUNT": row("W-WINDOW-COUNT", "A PR run that opened fewer windows on a screen than this share of main's median there leaves that screen (its taps, scrolls and steps) and the journey's window-dependent totals out of its comparisons.", "share", "≥", "plan 51 D2 floor (90 % of the usual window count), Arek 2026-10-22 ~07:20, per screen (plan 79 S8)", false, { below: 0.9 }),
|
|
7085
|
+
"W-WINDOW-COUNT": row("W-WINDOW-COUNT", "A PR run that opened fewer windows on a screen than this share of main's median there leaves that screen (its taps, scrolls and steps) and the journey's window-dependent totals out of its comparisons. Its scroll gestures count as main's median of them ([ctl-arek-gesture-bug]).", "share", "≥", "plan 51 D2 floor (90 % of the usual window count), Arek 2026-10-22 ~07:20, per screen (plan 79 S8); scrolls at main's median (Arek 2026-10-26, [ctl-arek-gesture-bug])", false, { below: 0.9 }),
|
|
6892
7086
|
"T-FRAME": row("T-FRAME", "One frame, the unit of every frame limit, the same on every screen (a 120 Hz phone is not held to 8.3 ms).", "ms", ">", "REQ-MET-23, Arek 2026-10-16", false, { ms: LIMIT_FRAME_MS }),
|
|
6893
7087
|
"C-DETAIL-MS": row("C-DETAIL-MS", "A component below this keeps key and ms only and is not listed as New or Removed.", "ms", "≥", "plan 79 S9 r2, Arek 2026-10-23 13:20; half the lowest component rule floor (judged and evidence)", false, { ms: Math.min(FLOOR.abs, COMPONENT_WARN_MS.update, COMPONENT_WARN_MS.work) / 2 }),
|
|
6894
7088
|
"T-WARN-MS": row("T-WARN-MS", "A judged change counts as Slightly worse past at least this many ms (an update, or an entry / in-screen mount).", "ms", ">", "REQ-MET-26 (Arek 2026-10-16 ~14:30, ~17:30); followups [RULED 2026-10-18 ~09:40]", false, COMPONENT_WARN_MS),
|
|
@@ -7110,6 +7304,13 @@ var SDK_RELEASES = [
|
|
|
7110
7304
|
renamed: { places: ["screen", "press", "drag"] },
|
|
7111
7305
|
uncalibrated: false,
|
|
7112
7306
|
source: "#277 (plan 74 step 4 SDK review): screen windows end at every touch delivered while open; late-mounting screens keep their component key (3-commit cap gone), so screen keys and the press/drag keys on them change; the run commit stream is cut at the flush, not inside a window (window times of long journeys and journey CPU change); a gesture whose touchEnd never reached JS moves press → drag; a late press takes its own open sample (press sampler measures). Flush answers lost data as error (validity only, no measure). Launch and journey memory stay compared."
|
|
7307
|
+
},
|
|
7308
|
+
{
|
|
7309
|
+
version: "0.1.0-alpha.18",
|
|
7310
|
+
previous: "0.1.0-alpha.17",
|
|
7311
|
+
declares: "nothing",
|
|
7312
|
+
uncalibrated: false,
|
|
7313
|
+
source: "packages/react-native since bb71e4ec (alpha.17 as published: its js/, ios/ and android/ equal that commit's). #578: the flush uploads 8 profile files at a time on native upload pools (Android: ThreadPoolExecutor, iOS: OperationQueue, both created at the first upload) and iOS streams the gzip to a temp file. All of it runs after the flush link, which ends every window first (core.ts flushOnce endAll('flush')), and the driver sends that link after the end GC and its memory read have been answered (session.ts endRun); the next run relaunches the app, so no pool, gzip or PUT lands in a measured window, the end GC or its memory read. Same files and lines go up; only the time from the flush link to its ack changes, which no measure reads. #615: EXPO_PUBLIC_KEEPSNAPPY_TAP_TEXT=0 (opt-in, default unchanged) drops visible text from the legacy tap name only (legacyName, touched); press and drag place keys are component keys (fiber.ts placeName) and stay the same. Were a key to change, it would be a new place with its own history (as with alpha.12 and alpha.16 `renamed`), not a measure change; here the only effect is that placeAliases (an information-only join of renamed places) finds fewer one-to-one legacy names. The rest is test-only (resetRunStateForTest, resetCurrentScreenForTest, @keepsnappy/test-support). REQ-FARM-35."
|
|
7113
7314
|
}
|
|
7114
7315
|
];
|
|
7115
7316
|
var DRIVER_RELEASES = [
|
|
@@ -7222,13 +7423,73 @@ var DRIVER_RELEASES = [
|
|
|
7222
7423
|
declares: [
|
|
7223
7424
|
{ affects: MEASURES_AT("drag"), places: ["drag"] },
|
|
7224
7425
|
{ affects: MEASURES_AT("step"), places: ["step"] },
|
|
7225
|
-
{ affects: MEASURES_AT("journey"), places: ["journey"] }
|
|
7426
|
+
{ affects: MEASURES_AT("journey"), places: ["journey"] },
|
|
7427
|
+
{ affects: MEASURES_AT("screen"), places: ["screen"] },
|
|
7428
|
+
{ affects: MEASURES_AT("press"), places: ["press"] }
|
|
7429
|
+
],
|
|
7430
|
+
uncalibrated: false,
|
|
7431
|
+
source: "appium-driver CHANGELOG \"0.1.0-alpha.24\", 2026-10-30 (ctl 09:15, Arek: alpha.24 = the audience helpers with TapIn's 3-page audience, the freeze starts from it; D1 counts per [ctl-arek-p84] and [ctl-arek-nav-scroll]): pagedToEnd and topMeasured give lists a journey pages itself a fixed count of controlled gestures per measured step (per page exactly `gestures`, the top 2, fewer only when the first row already shows after the 1st), the way to the page's marker and back to the top being a navigation pass outside the step; before, TapIn's own audience loops sent a count that depended on where the list rested (Android to-end 10/10/8, iOS to-top 6/6/5). New audience step names are new step places; the audience drag places hold only the controlled gestures; the top step returns right after its gestures and navigation passes read rests faster, so step and journey places move too. Also #596/#606 (CHANGELOG `beginRun`): every iteration launches the app to home, sends the activation link, waits 2 s and relaunches to home, where alpha.23 sent the link into the process the journey had left running and the journey launched the app itself; the process each run measures starts later and from home, so the first screen windows after launch and the taps on them move (A24 gate, same app build c500cef as main: Pixel set a7ae0d0a Worse at screen HomeScreen#1, CommunityFeedScreen#1, JoinRoomScreen#1 and press HeroLiveContent›Touchable#1; iPhone set 16e7dbc5 only at an audience drag place, declared already; ctl 2026-10-31). Drag, step, journey, screen and press places are declared, on both platforms (REQ-FARM-35)."
|
|
7432
|
+
},
|
|
7433
|
+
{
|
|
7434
|
+
version: "0.1.0-alpha.25",
|
|
7435
|
+
previous: "0.1.0-alpha.24",
|
|
7436
|
+
declares: [
|
|
7437
|
+
{ affects: MEASURES_AT("screen"), places: ["screen"] },
|
|
7438
|
+
{ affects: MEASURES_AT("press"), places: ["press"] },
|
|
7439
|
+
{ affects: MEASURES_AT("drag"), places: ["drag"] },
|
|
7440
|
+
{ affects: MEASURES_AT("step"), places: ["step"] },
|
|
7441
|
+
{ affects: MEASURES_AT("journey"), places: ["journey"] },
|
|
7442
|
+
{ affects: MEASURES_AT("nav"), places: ["nav"] }
|
|
7226
7443
|
],
|
|
7227
7444
|
uncalibrated: false,
|
|
7228
|
-
source: "
|
|
7445
|
+
source: "The instrument bundle of 2026-10-31 (ctl 13:30, Arek YES 10:30), both candidates of the scroll lab A/B. (1) #675 (L1a+N1, L2, L3): a tap is one request, with no DELETE /actions after its pointerUp, so the span from a tap to the next touch is one farm round trip shorter (press windows, the screen windows a tap opens, the steps holding taps, the run); a navigation pass reads its rests by cost (Android: the page source on a light tree, one held row on a heavy one, timed on the list's first read) and a held-row read is one command (the frame, no label), so the reads before a pass's first stroke, inside the touch window before it (a drag after a measured step, a press or screen before a pass), and the nav windows between strokes move. (2) L1b, included only if the A/B keeps it: on Android the end/top steps read the coast inside the measured step (drag and step, covered above). (3) #607 beforeLaunch: a per-iteration journey hook that runs after the activation link and before the measured launch, in the process the launch replaces; nothing measured. (4) The default Android device is the farm Pixel 9 Pro (Google Pixel 9 Pro-15.0, was the retired Pixel 7): a default only, a run naming its device sends the same session; nothing measured. Launch and background places are not declared: no change reaches the launch window or a backgrounded app (REQ-FARM-35)."
|
|
7229
7446
|
}
|
|
7230
7447
|
];
|
|
7231
|
-
var JOURNEY_RELEASES = {
|
|
7448
|
+
var JOURNEY_RELEASES = {
|
|
7449
|
+
tapin: [
|
|
7450
|
+
{
|
|
7451
|
+
version: "01c42817b31f",
|
|
7452
|
+
previous: "9dc9b234b811",
|
|
7453
|
+
declares: [
|
|
7454
|
+
{ affects: MEASURES_AT("journey"), places: ["journey"] },
|
|
7455
|
+
{ affects: MEASURES_AT("launch"), places: ["launch"] },
|
|
7456
|
+
{ affects: MEASURES_AT("screen"), places: ["screen"] },
|
|
7457
|
+
{ affects: MEASURES_AT("press"), places: ["press"] },
|
|
7458
|
+
{ affects: MEASURES_AT("drag"), places: ["drag"] },
|
|
7459
|
+
{ affects: MEASURES_AT("step"), places: ["step"] }
|
|
7460
|
+
],
|
|
7461
|
+
uncalibrated: false,
|
|
7462
|
+
source: "TapIn instrument bundle gate test/keepsnappy-bundle-gate @ca3e1fde2 (ctl 2026-10-31 13:30), three journey changes over 9dc9b234b811, both platforms. (1) #3873 (ff729dfd5): the waits between steps in profile, chat and front-row and the home check after the first join's launch are gone; they sat in the touch window before the next touch, so press, drag (the profile close swipe), screen and launch windows (the home check came before the first action) end sooner, and the run is shorter. (2) B5 (2ca5adac3, 499aed4e5): every launch starts in the reset mock world (#607 beforeLaunch, before the measured launch) and the fake room is filled at home before the community link, so the launch and home windows, the community and room screens, the taps and steps on them move. (3) Quick checks (6e0a0f56a, ca3e1fde2): after reactions.close the journey opens and closes 27 new places (live widgets, the viewer summary, community post/events/event/about/feed, home mine/settings) and ends on home; the new places are new keys with no baseline, not declared. Existing places that move: the reactions.close press and the live screen window it leaves open end at the next touch, not at the journey end; reactions.close is no longer the last step, so the end GC's step rows go to home.tab-home; the end GC and memory read now see home with the live left, so the journey's end, peak and kept memory (mem.end_mb, mem.end_after_gc_mb, mem.retained_growth_mb, mem.js_heap_end_mb, all at the journey place) and whole-run CPU change; sdk.journey_ends_home goes 0 -> 1, a setup fact (facts NOT_FACTS) that no verdict compares, so it has nothing to declare. nav and background places are not declared: no change reaches a navigation pass or a backgrounded app. A part the gate drops narrows this entry (REQ-FARM-35)."
|
|
7463
|
+
},
|
|
7464
|
+
{
|
|
7465
|
+
version: "9dc9b234b811",
|
|
7466
|
+
previous: "a02a95ce2c8d",
|
|
7467
|
+
declares: [
|
|
7468
|
+
{ affects: MEASURES_AT("journey"), places: ["journey"] },
|
|
7469
|
+
{ affects: MEASURES_AT("screen"), places: ["screen"] },
|
|
7470
|
+
{ affects: MEASURES_AT("launch"), places: ["launch"] },
|
|
7471
|
+
{ affects: MEASURES_AT("press"), places: ["press"] },
|
|
7472
|
+
{ affects: MEASURES_AT("drag"), places: ["drag"] },
|
|
7473
|
+
{ affects: MEASURES_AT("nav"), places: ["nav"] },
|
|
7474
|
+
{ affects: MEASURES_AT("step"), places: ["step"] },
|
|
7475
|
+
{ affects: MEASURES_AT("background"), places: ["background"] }
|
|
7476
|
+
],
|
|
7477
|
+
uncalibrated: false,
|
|
7478
|
+
source: 'TapIn e2e/keepsnappy/live-ui-appium.mjs (the only file of the hash that changed), gigaverse-mobile bfd33ebb8 (main since #3852, a02a95ce2c8d) -> 0ac3879db "pin KeepSnappy to alpha.24" (TapIn PR #3872, 9dc9b234b811; ctl 2026-10-31). (1) The audience: one `audience.scroll-to-end` step over 5 pages, each a loop of long scrolls until "Show more" shows and its tap inside the step, and a `audience.scroll-to-top` loop capped by the way down, become driver alpha.24 `pagedToEnd` over 3 pages (steps `audience.scroll-to-more-<p>` with a fixed gesture count, the tap in its own step `audience.show-more-<p>`, the way to the marker a navigation pass) and `topMeasured` (`audience.scroll-to-top`, 2 gestures): step places are renamed and reshaped, the audience drag places hold other gestures, "Show more" is tapped 3 times, not 5, after other scrolls, navigation passes are new nav places, and two fewer pages load (step, drag, press, nav, journey). (2) `enterLive` no longer relaunches the app on its first join, as alpha.24 `beginRun` launches it (the setup changes of TapIn #3853, folded into the pin): the process each run measures, its launch and the first screens and taps after it come from `beginRun`, followed by its activation check, not from the journey (launch, screen, press, background, journey). (3) The waits for the newest chat row (30 s) and the event pill (10 s) before the first step are gone, so the first steps start from a less settled room (screen, press, step). The whole run changes, on both platforms: every place kind of a run is declared except `other` (REQ-FARM-35).'
|
|
7479
|
+
},
|
|
7480
|
+
{
|
|
7481
|
+
version: "a02a95ce2c8d",
|
|
7482
|
+
previous: "031769c140fd",
|
|
7483
|
+
declares: [
|
|
7484
|
+
{ affects: MEASURES_AT("drag"), places: ["drag"] },
|
|
7485
|
+
{ affects: MEASURES_AT("step"), places: ["step"] },
|
|
7486
|
+
{ affects: MEASURES_AT("journey"), places: ["journey"] }
|
|
7487
|
+
],
|
|
7488
|
+
uncalibrated: false,
|
|
7489
|
+
source: "TapIn gigaverse-mobile #3852 (6efd02c4bc, 2026-10-30 08:56 UTC), the driver alpha.23 pin: the only edit to the journey module since journey 031769c140fd (#3807, driver alpha.21) deletes the Android-only `scrolls` cap of the History scroll cycles (LONG_SCROLL_CAP + 2), so a History scroll step sends the driver's own count. That moves drag places, the step places holding them and the whole run; screens and taps send the same commands. Declared like the driver release it ships with: drag, step and journey places, on both platforms (REQ-FARM-35, [ctl-arek-p84] D2). Production: every TapIn main session from 5f8fae52 (iPhone 15 Pro) and d79ea5be (Pixel 9 Pro) on read Can't tell at all 46 places for 3 sessions while this change was undeclared."
|
|
7490
|
+
}
|
|
7491
|
+
]
|
|
7492
|
+
};
|
|
7232
7493
|
var CAPTURE_MEASURED_WITH = { aspect: "capture", affects: LOADED_MEMORY, marks: true };
|
|
7233
7494
|
var UNSTABLE_NOISY = {
|
|
7234
7495
|
reason: "noisy",
|
|
@@ -7282,8 +7543,6 @@ var noiseJudged = (id, on) => {
|
|
|
7282
7543
|
var part = (b) => ({ ...b.rel !== undefined ? { rel: b.rel } : {}, ...b.abs !== undefined ? { abs: b.abs } : {} });
|
|
7283
7544
|
var EVIDENCE_BARS = new Map(Object.values(CLAUSES).filter((c) => c.id.startsWith("E-")).flatMap((c) => Object.entries(c.value).map(([rule, bar]) => [rule, { bar, uncalibrated: c.uncalibrated }])));
|
|
7284
7545
|
function withClauseValues(base) {
|
|
7285
|
-
if (!base.better)
|
|
7286
|
-
throw new ConfigError2(["better: missing from base config"]);
|
|
7287
7546
|
const warnMs = CLAUSES["T-WARN-MS"].value;
|
|
7288
7547
|
const errorMs = CLAUSES["TIER-ERROR"].value;
|
|
7289
7548
|
const memory = CLAUSES["J-MEMORY-RETAINED"];
|
|
@@ -7328,7 +7587,7 @@ function withClauseValues(base) {
|
|
|
7328
7587
|
uncertain: { ...base.uncertain, belowMainSessions: c("S-UNCERTAIN").value.belowMainSessions, uncalibrated: c("S-UNCERTAIN").uncalibrated },
|
|
7329
7588
|
judgeFrom: { ...base.judgeFrom, mainSessions: c("G-JUDGE-FROM").value.mainSessions, uncalibrated: c("G-JUDGE-FROM").uncalibrated },
|
|
7330
7589
|
work: { ...base.work, mountShareAtLeast: c("W-WORKKIND").value.mountShareAtLeast, uncalibrated: c("W-WORKKIND").uncalibrated },
|
|
7331
|
-
|
|
7590
|
+
windowCount: { ...base.windowCount, below: c("W-WINDOW-COUNT").value.below, uncalibrated: c("W-WINDOW-COUNT").uncalibrated },
|
|
7332
7591
|
frame: { ...base.frame, ms: c("T-FRAME").value.ms, uncalibrated: c("T-FRAME").uncalibrated },
|
|
7333
7592
|
tiers: {
|
|
7334
7593
|
...base.tiers,
|
|
@@ -7399,7 +7658,7 @@ var RELIABLE = {
|
|
|
7399
7658
|
source: 'Arek via ctl 2026-10-28 ~11:40, corrected ~11:55 ("fair and moderate are not an overheated phone") [ctl-arek-thermal-unreliable]: a run that started at Android SEVERE (3) or above, or iOS serious (2) or above, makes its session unreliable; the SDK maps SEVERE to serious, so both scales read alike. NONE / LIGHT / MODERATE and nominal / fair are normal: listed as started warm, never unreliable. [ctl-arek-three-runs]: a session counts only with 3 valid runs, on main and on the PR (no earlier ruling on partial PR sessions: G-MINRUNS had none). Background: giga2-scratch/noise-p9p-g71/findings.md (P9P false Worse from LIGHT starts stay; the G71 1-run main session no longer counts)'
|
|
7400
7659
|
};
|
|
7401
7660
|
var SHAPE = {
|
|
7402
|
-
id: "simple-4.
|
|
7661
|
+
id: "simple-4.31+noise95-99",
|
|
7403
7662
|
minRuns: 2,
|
|
7404
7663
|
baseline: { sessions: 3, ...GUESS("Q4 / REQ-VER-33: main's newest 3 sessions against one PR session (the quieter setting of the plan 58 simulation)") },
|
|
7405
7664
|
repeats: { kind: "every-pr-beyond-every-main" },
|
|
@@ -7513,6 +7772,7 @@ var SHAPE = {
|
|
|
7513
7772
|
noisy: NOISY_WIDEN,
|
|
7514
7773
|
compConfirm: COMP_CONFIRM,
|
|
7515
7774
|
reliable: RELIABLE,
|
|
7775
|
+
commitGrouping: { uncalibrated: true, source: "plan 81 P3; Arek 2026-10-26 [ctl-arek-p81-p3p4]: two windows that share one proven commit (the same slowest commit in every PR run) may be one finding; never merged without it; deciding since simple-4.31, Arek 2026-10-30 [ctl-arek-1030-per-rec] and [ctl-arek-1030-p4-back]" },
|
|
7516
7776
|
judgeFrom: { mainSessions: 3, uncalibrated: false, source: 'Arek 2026-10-18 (REQ-VER-41): a phone is judged only when main has at least 3 sessions on that phone and journey; with fewer the result is "not enough main yet", not judged, not blocking, values shown. Replay (agents/release/replay, d22): 1 PR vs 2 main on the S23 night pool gave 13 z 60 false worse+warning, 1 vs 3 gave 0' },
|
|
7517
7777
|
measuredWith: [
|
|
7518
7778
|
{ aspect: "renderer", affects: REACT_MEASURES },
|
|
@@ -7549,13 +7809,8 @@ var CONFIG = withClauseValues(SHAPE);
|
|
|
7549
7809
|
var P4_EVEN = {
|
|
7550
7810
|
...CONFIG,
|
|
7551
7811
|
id: `${CONFIG.id}+p4-even`,
|
|
7552
|
-
evenSlowdown: { maxExcess: 0.3, renders: 0.1, commits: 0.25, uncalibrated: true, source: "plan 81 P4 (Opus O2 guesses); Arek 2026-10-23 ~12:45 direction (4); Arek 2026-10-26 ctl-arek-p81-p3p4: render and commit counts may block a downgrade as a guard, never colour a verdict alone" }
|
|
7812
|
+
evenSlowdown: { maxExcess: 0.3, renders: 0.1, commits: 0.25, uncalibrated: true, source: "plan 81 P4 (Opus O2 guesses); Arek 2026-10-23 ~12:45 direction (4); Arek 2026-10-26 ctl-arek-p81-p3p4: render and commit counts may block a downgrade as a guard, never colour a verdict alone; Arek 2026-10-30 [ctl-arek-1030-p4-back]: a candidate until the blind spot is closed" }
|
|
7553
7813
|
};
|
|
7554
|
-
var P3_COMMIT = withClauseValues({
|
|
7555
|
-
...CONFIG,
|
|
7556
|
-
id: `${CONFIG.id}+p3-commit`,
|
|
7557
|
-
commitGrouping: { uncalibrated: true, source: "plan 81 P3; Arek 2026-10-26 [ctl-arek-p81-p3p4]: two windows that share one proven commit (the same slowest commit in every PR run) may be one finding; never merged without it" }
|
|
7558
|
-
});
|
|
7559
7814
|
// apps/agent/src/remote.ts
|
|
7560
7815
|
var isBlocking = (s) => !!s?.verdicts && (s.verdicts.blocking ?? s.verdicts.state === "worse");
|
|
7561
7816
|
function heatNote(sc) {
|
|
@@ -7736,12 +7991,12 @@ Runs uploaded for \`${fallback.branch}\`. The result appears on the dashboard: [
|
|
|
7736
7991
|
}
|
|
7737
7992
|
|
|
7738
7993
|
// apps/agent/src/run.ts
|
|
7739
|
-
import { join as
|
|
7994
|
+
import { join as join9 } from "node:path";
|
|
7740
7995
|
// apps/agent/src/extract.ts
|
|
7741
|
-
import { join as
|
|
7996
|
+
import { join as join7 } from "node:path";
|
|
7742
7997
|
|
|
7743
7998
|
// apps/agent/src/profiles.ts
|
|
7744
|
-
import { join as
|
|
7999
|
+
import { join as join6 } from "node:path";
|
|
7745
8000
|
|
|
7746
8001
|
// packages/profile-analysis/dist/src/tracesift/lib/format.js
|
|
7747
8002
|
function formatMs(ms) {
|
|
@@ -7958,7 +8213,7 @@ var dispatchNames = new Set(["processTicksAndRejections", "runMicrotasks", "proc
|
|
|
7958
8213
|
function identity(frame) {
|
|
7959
8214
|
return JSON.stringify([frame.functionName, frame.scriptId, frame.url, frame.lineNumber, frame.columnNumber]);
|
|
7960
8215
|
}
|
|
7961
|
-
function
|
|
8216
|
+
function label2(frame) {
|
|
7962
8217
|
return `${frame.functionName || "(anonymous)"} (${frame.url || "(anonymous)"}:${frame.lineNumber + 1}:${frame.columnNumber + 1})`;
|
|
7963
8218
|
}
|
|
7964
8219
|
function groupBottlenecks(profile, durationMs) {
|
|
@@ -8025,7 +8280,7 @@ function groupBottlenecks(profile, durationMs) {
|
|
|
8025
8280
|
continue;
|
|
8026
8281
|
let group = groups.get(key);
|
|
8027
8282
|
if (!group) {
|
|
8028
|
-
group = { id: `b${groups.size + 1}`, title: owner.callFrame.functionName || "(anonymous)", combinedTimeMs: 0, percentOfTotal: 0, occurrences: 0, longestRunMs: 0, stack: meaningful.slice(meaningful.indexOf(owner)).map((node) =>
|
|
8283
|
+
group = { id: `b${groups.size + 1}`, title: owner.callFrame.functionName || "(anonymous)", combinedTimeMs: 0, percentOfTotal: 0, occurrences: 0, longestRunMs: 0, stack: meaningful.slice(meaningful.indexOf(owner)).map((node) => label2(node.callFrame)), functions: [], members: new Map };
|
|
8029
8284
|
groups.set(key, group);
|
|
8030
8285
|
}
|
|
8031
8286
|
if (!runGroup) {
|
|
@@ -8037,7 +8292,7 @@ function groupBottlenecks(profile, durationMs) {
|
|
|
8037
8292
|
const leafKey = identity(leaf.callFrame);
|
|
8038
8293
|
let member = group.members.get(leafKey);
|
|
8039
8294
|
if (!member) {
|
|
8040
|
-
member = { id: `${group.id}-f${group.members.size + 1}`, title: leaf.callFrame.functionName || "(anonymous)", selfTimeMs: 0, percentOfGroup: 0, location: frameSourceLocation(leaf.callFrame), stack: meaningful.map((node) =>
|
|
8295
|
+
member = { id: `${group.id}-f${group.members.size + 1}`, title: leaf.callFrame.functionName || "(anonymous)", selfTimeMs: 0, percentOfGroup: 0, location: frameSourceLocation(leaf.callFrame), stack: meaningful.map((node) => label2(node.callFrame)), frameworkInternal: isFrameworkInternalFrame(leaf.callFrame) };
|
|
8041
8296
|
group.members.set(leafKey, member);
|
|
8042
8297
|
}
|
|
8043
8298
|
member.selfTimeMs += sampleMs;
|
|
@@ -11023,20 +11278,20 @@ function deriveLocal(input) {
|
|
|
11023
11278
|
async function writeProfiles(deps, runDir, derived) {
|
|
11024
11279
|
if (!derived.profiles.length)
|
|
11025
11280
|
return;
|
|
11026
|
-
await deps.fs.writeFile(
|
|
11281
|
+
await deps.fs.writeFile(join6(runDir, "profiles.json"), `${JSON.stringify({ derivationVersion: derived.derivationVersion, profiles: derived.profiles }, null, 2)}
|
|
11027
11282
|
`);
|
|
11028
11283
|
}
|
|
11029
11284
|
async function saveCpuProfiles(deps, runDir, texts) {
|
|
11030
11285
|
if (!texts.size)
|
|
11031
11286
|
return;
|
|
11032
|
-
await deps.fs.mkdir(
|
|
11287
|
+
await deps.fs.mkdir(join6(runDir, CPU_DIR));
|
|
11033
11288
|
for (const [name, text] of texts)
|
|
11034
|
-
await deps.fs.writeFile(
|
|
11289
|
+
await deps.fs.writeFile(join6(runDir, CPU_DIR, name), text).catch(() => {});
|
|
11035
11290
|
}
|
|
11036
11291
|
async function readCpuProfiles(deps, runDir, logText) {
|
|
11037
11292
|
const out = new Map;
|
|
11038
11293
|
for (const name of cpuFileNames(logText)) {
|
|
11039
|
-
const text = await deps.fs.readFile(
|
|
11294
|
+
const text = await deps.fs.readFile(join6(runDir, CPU_DIR, name)).catch(() => {
|
|
11040
11295
|
return;
|
|
11041
11296
|
});
|
|
11042
11297
|
if (text !== undefined)
|
|
@@ -11059,7 +11314,7 @@ async function pullCpuProfiles(source, bundleId, logText, known = new Set) {
|
|
|
11059
11314
|
}
|
|
11060
11315
|
// apps/agent/src/extract.ts
|
|
11061
11316
|
async function extract(deps, outDir, ri, memWindow, clockOffsetMs) {
|
|
11062
|
-
const runs = (await deps.fs.readFile(
|
|
11317
|
+
const runs = (await deps.fs.readFile(join7(outDir, "runs.ndjson"))).split(`
|
|
11063
11318
|
`).filter((l) => l.trim()).map((l) => JSON.parse(l));
|
|
11064
11319
|
const samples = ri ? await ri.load() : undefined;
|
|
11065
11320
|
const rows = [];
|
|
@@ -11069,26 +11324,26 @@ async function extract(deps, outDir, ri, memWindow, clockOffsetMs) {
|
|
|
11069
11324
|
let logText = "";
|
|
11070
11325
|
let cpuProfiles = new Map;
|
|
11071
11326
|
try {
|
|
11072
|
-
const log = await deps.fs.readFile(
|
|
11327
|
+
const log = await deps.fs.readFile(join7(outDir, r.run_id, "keepsnappy.log"));
|
|
11073
11328
|
logText = log;
|
|
11074
|
-
cpuProfiles = await readCpuProfiles(deps,
|
|
11329
|
+
cpuProfiles = await readCpuProfiles(deps, join7(outDir, r.run_id), log);
|
|
11075
11330
|
windows = windowsInRun(parseWindows(log), r.t0, r.t1);
|
|
11076
11331
|
runSamples = samplesInRun(parseSamples(log), r.t0, r.t1);
|
|
11077
11332
|
} catch {}
|
|
11078
11333
|
let markers = [];
|
|
11079
11334
|
try {
|
|
11080
|
-
markers = parseMarkersNdjson(await deps.fs.readFile(
|
|
11335
|
+
markers = parseMarkersNdjson(await deps.fs.readFile(join7(outDir, r.run_id, "markers.ndjson")));
|
|
11081
11336
|
} catch {}
|
|
11082
11337
|
const derived = deriveLocal({ run: r, windows, markers, samples: runSamples, ...memWindow ? { memWindow } : {}, ...samples ? { ri: { samples, ...memWindow ? { memWindow } : {} } } : {}, ...clockOffsetMs === undefined ? {} : { clockOffsetMs }, cpuProfiles, logText });
|
|
11083
11338
|
rows.push(...derived.rows);
|
|
11084
|
-
await writeProfiles(deps,
|
|
11339
|
+
await writeProfiles(deps, join7(outDir, r.run_id), derived);
|
|
11085
11340
|
}
|
|
11086
|
-
await deps.fs.writeFile(
|
|
11341
|
+
await deps.fs.writeFile(join7(outDir, "metrics.csv"), toCsv(rows));
|
|
11087
11342
|
return rows;
|
|
11088
11343
|
}
|
|
11089
11344
|
|
|
11090
11345
|
// apps/agent/src/runner.ts
|
|
11091
|
-
import { join as
|
|
11346
|
+
import { join as join8 } from "node:path";
|
|
11092
11347
|
|
|
11093
11348
|
// apps/agent/src/schedule.ts
|
|
11094
11349
|
function schedule(arms, repeat) {
|
|
@@ -11142,9 +11397,9 @@ async function measure(deps, source, installer, opts) {
|
|
|
11142
11397
|
if (flows.some((f) => !f.command[0]))
|
|
11143
11398
|
throw new ConfigError(["no test command given (put it after `--`)"]);
|
|
11144
11399
|
await fs.mkdir(opts.outDir);
|
|
11145
|
-
const runsPath =
|
|
11400
|
+
const runsPath = join8(opts.outDir, "runs.ndjson");
|
|
11146
11401
|
await fs.writeFile(runsPath, "");
|
|
11147
|
-
await fs.writeFile(
|
|
11402
|
+
await fs.writeFile(join8(opts.outDir, "arms.json"), `${JSON.stringify(opts.arms, null, 2)}
|
|
11148
11403
|
`);
|
|
11149
11404
|
const targets = new Map(opts.arms.map((a) => [a.name, a.target]));
|
|
11150
11405
|
const armEnv = new Map(opts.arms.map((a) => [a.name, a.env ?? {}]));
|
|
@@ -11155,7 +11410,7 @@ async function measure(deps, source, installer, opts) {
|
|
|
11155
11410
|
const plan = schedule(opts.arms, opts.repeat).flatMap((p) => flows.map((f) => ({ ...p, runId: (opts.flows?.length ?? 0) > 1 ? `${p.runId}-${f.name}` : p.runId, flow: f })));
|
|
11156
11411
|
for (const p of plan) {
|
|
11157
11412
|
const [cmd, ...args] = p.flow.command;
|
|
11158
|
-
const dir =
|
|
11413
|
+
const dir = join8(opts.outDir, p.runId);
|
|
11159
11414
|
await fs.mkdir(dir);
|
|
11160
11415
|
let reason = "";
|
|
11161
11416
|
const target = targets.get(p.arm);
|
|
@@ -11177,7 +11432,7 @@ async function measure(deps, source, installer, opts) {
|
|
|
11177
11432
|
...opts.testTimeoutMs ? { timeoutMs: opts.testTimeoutMs } : {}
|
|
11178
11433
|
});
|
|
11179
11434
|
exit = r.code;
|
|
11180
|
-
await fs.writeFile(
|
|
11435
|
+
await fs.writeFile(join8(dir, "test.log"), `${r.stdout}${r.stderr ? `
|
|
11181
11436
|
--- stderr ---
|
|
11182
11437
|
${r.stderr}` : ""}`);
|
|
11183
11438
|
if (r.error)
|
|
@@ -11193,7 +11448,7 @@ ${r.stderr}` : ""}`);
|
|
|
11193
11448
|
try {
|
|
11194
11449
|
const text = await source.fetchLog(opts.bundleId);
|
|
11195
11450
|
logText = text;
|
|
11196
|
-
await fs.writeFile(
|
|
11451
|
+
await fs.writeFile(join8(dir, "keepsnappy.log"), text);
|
|
11197
11452
|
windows = windowsInRun(parseWindows(text), t0, t1);
|
|
11198
11453
|
samples = samplesInRun(parseSamples(text), t0, t1);
|
|
11199
11454
|
cpuProfiles = await pullCpuProfiles(source, opts.bundleId, text, pulled);
|
|
@@ -11206,7 +11461,7 @@ ${r.stderr}` : ""}`);
|
|
|
11206
11461
|
}
|
|
11207
11462
|
const markers = opts.markers?.forRun(p.runId) ?? [];
|
|
11208
11463
|
if (markers.length)
|
|
11209
|
-
await fs.writeFile(
|
|
11464
|
+
await fs.writeFile(join8(dir, "markers.ndjson"), markersToNdjson(markers));
|
|
11210
11465
|
let riInput;
|
|
11211
11466
|
if (opts.ri) {
|
|
11212
11467
|
try {
|
|
@@ -11237,7 +11492,7 @@ ${r.stderr}` : ""}`);
|
|
|
11237
11492
|
`);
|
|
11238
11493
|
log(`${p.runId} ${final.valid ? "valid" : `INVALID (${final.invalid_reason})`} windows=${windows.length}${markers.length ? ` markers=${markers.length} steps=${steps.length}` : ""}`);
|
|
11239
11494
|
}
|
|
11240
|
-
await fs.writeFile(
|
|
11495
|
+
await fs.writeFile(join8(opts.outDir, "metrics.csv"), toCsv(rows));
|
|
11241
11496
|
return { runs, rows };
|
|
11242
11497
|
}
|
|
11243
11498
|
|
|
@@ -11272,7 +11527,7 @@ async function keepsnappyRun(deps, source, opts) {
|
|
|
11272
11527
|
...opts.clockOffsetMs === undefined ? {} : { clockOffsetMs: opts.clockOffsetMs }
|
|
11273
11528
|
});
|
|
11274
11529
|
const rows = renameMetrics(raw, opts.config.steps.rename, deps.log);
|
|
11275
|
-
await deps.fs.writeFile(
|
|
11530
|
+
await deps.fs.writeFile(join9(opts.outDir, "metrics.csv"), toCsv(rows));
|
|
11276
11531
|
const summary = summarize({ runs, outDir: opts.outDir });
|
|
11277
11532
|
return { runs, rows, summary };
|
|
11278
11533
|
}
|
|
@@ -11280,7 +11535,7 @@ async function replayRuns(deps, sources, outDir, scenario) {
|
|
|
11280
11535
|
await deps.fs.mkdir(outDir);
|
|
11281
11536
|
const runs = [];
|
|
11282
11537
|
for (const src of sources) {
|
|
11283
|
-
const text = await deps.fs.readFile(
|
|
11538
|
+
const text = await deps.fs.readFile(join9(src.dir, "runs.ndjson")).catch(() => {
|
|
11284
11539
|
throw new ConfigError([`--replay ${src.dir}: no runs.ndjson (not a keepsnappy out dir)`]);
|
|
11285
11540
|
});
|
|
11286
11541
|
for (const line of text.split(`
|
|
@@ -11291,18 +11546,18 @@ async function replayRuns(deps, sources, outDir, scenario) {
|
|
|
11291
11546
|
if (!r.valid || src.arm && r.arm !== src.arm)
|
|
11292
11547
|
continue;
|
|
11293
11548
|
const id = `${String(runs.length + 1).padStart(3, "0")}-A`;
|
|
11294
|
-
await deps.fs.mkdir(
|
|
11549
|
+
await deps.fs.mkdir(join9(outDir, id));
|
|
11295
11550
|
for (const f of ["keepsnappy.log", "markers.ndjson", "test.log"]) {
|
|
11296
|
-
const body = await deps.fs.readFile(
|
|
11551
|
+
const body = await deps.fs.readFile(join9(src.dir, r.run_id, f)).catch(() => null);
|
|
11297
11552
|
if (body !== null)
|
|
11298
|
-
await deps.fs.writeFile(
|
|
11553
|
+
await deps.fs.writeFile(join9(outDir, id, f), body);
|
|
11299
11554
|
}
|
|
11300
11555
|
runs.push({ ...r, run_id: id, arm: "A", block: runs.length, scenario });
|
|
11301
11556
|
}
|
|
11302
11557
|
}
|
|
11303
11558
|
if (runs.length === 0)
|
|
11304
11559
|
throw new ConfigError([`--replay: no valid runs in ${sources.map((x) => x.dir + (x.arm ? `:${x.arm}` : "")).join(", ")}`]);
|
|
11305
|
-
await deps.fs.writeFile(
|
|
11560
|
+
await deps.fs.writeFile(join9(outDir, "runs.ndjson"), runs.map((r) => JSON.stringify(r)).join(`
|
|
11306
11561
|
`) + `
|
|
11307
11562
|
`);
|
|
11308
11563
|
await extract(deps, outDir, undefined);
|
|
@@ -11526,8 +11781,11 @@ async function runCommand(deps, o, positionals, rt = { env: process.env, print:
|
|
|
11526
11781
|
if (o["fail-on-worse"] && !o.wait)
|
|
11527
11782
|
throw new ConfigError(["--fail-on-worse needs --wait"]);
|
|
11528
11783
|
const replay = o.replay?.length ? parseReplay(o.replay) : null;
|
|
11529
|
-
const
|
|
11530
|
-
const
|
|
11784
|
+
const resolved = replay ? [await replayFlow(config, o.scenario)] : await resolveFlows(config, positionals, { ...o.scenario ? { scenario: o.scenario } : {}, readFile: (p) => deps.fs.readFile(p) });
|
|
11785
|
+
const named = o.device ?? (config.device === "booted" ? undefined : config.device);
|
|
11786
|
+
const udid = replay ? undefined : await resolveRunDevice(deps, config.platform, named);
|
|
11787
|
+
const flows = udid ? resolved.map((f) => withDevice(f, udid)) : resolved;
|
|
11788
|
+
const platformSource = () => config.platform === "ios-sim" ? new IosSimulatorDataSource(deps, udid) : new IosDeviceDataSource(deps, udid, resolve3(out, ".scratch"));
|
|
11531
11789
|
const source = rt.source ?? (replay ? null : platformSource());
|
|
11532
11790
|
const described = o["device-model"] ? null : await source?.describeDevice?.().catch(() => null);
|
|
11533
11791
|
const device = {
|
|
@@ -11588,7 +11846,7 @@ async function runCommand(deps, o, positionals, rt = { env: process.env, print:
|
|
|
11588
11846
|
${outcome.summary}`);
|
|
11589
11847
|
}
|
|
11590
11848
|
const info = { device, ...git.branch ? { branch: git.branch } : {}, ...git.commit ? { commit: git.commit } : {} };
|
|
11591
|
-
await deps.fs.writeFile(
|
|
11849
|
+
await deps.fs.writeFile(join10(out, RUN_INFO), `${JSON.stringify({ ...info, ...runSet ? { run_set_id: runSet.run_set_id } : {} }, null, 2)}
|
|
11592
11850
|
`);
|
|
11593
11851
|
if (!api) {
|
|
11594
11852
|
if (!runs.some((run) => run.valid)) {
|
|
@@ -11602,7 +11860,7 @@ ${outcome.summary}`);
|
|
|
11602
11860
|
ingestUrl: api.url,
|
|
11603
11861
|
key: api.key,
|
|
11604
11862
|
externalPrefix: `${git.commit ? git.commit.slice(0, 12) : "local"}-${runSet ? runSet.run_set_id.slice(0, 8) : String(Date.now())}`,
|
|
11605
|
-
device: { ...device, ...
|
|
11863
|
+
device: { ...device, ...named ? { id: named } : {} },
|
|
11606
11864
|
meta: { device_provider: provider, ...git.commit ? { commit_sha: git.commit } : {}, ...git.branch ? { branch: git.branch } : {} },
|
|
11607
11865
|
...runSet ? { runSetId: runSet.run_set_id } : {},
|
|
11608
11866
|
driver: config.flows.runner === "maestro" && positionals.length === 0 ? "maestro" : "keepsnappy-run",
|
|
@@ -11656,7 +11914,7 @@ var infra = (what) => (e) => {
|
|
|
11656
11914
|
};
|
|
11657
11915
|
async function writeResult(deps, out, rt, runSet, summary, branch, blocked = false) {
|
|
11658
11916
|
const url = summary?.dashboard_url ?? runSet.dashboard_url;
|
|
11659
|
-
await deps.fs.writeFile(
|
|
11917
|
+
await deps.fs.writeFile(join10(out, "keepsnappy-result.json"), `${JSON.stringify({ run_set_id: runSet.run_set_id, dashboard_url: url, state: blocked ? "blocked" : summary?.verdicts?.state ?? null, summary }, null, 2)}
|
|
11660
11918
|
`);
|
|
11661
11919
|
const step = rt.env.GITHUB_STEP_SUMMARY;
|
|
11662
11920
|
if (step)
|
|
@@ -11669,20 +11927,20 @@ import { parseArgs as parseArgs6 } from "node:util";
|
|
|
11669
11927
|
// apps/agent/src/user-config.ts
|
|
11670
11928
|
import { mkdir, readFile as readFile2, writeFile } from "node:fs/promises";
|
|
11671
11929
|
import { homedir } from "node:os";
|
|
11672
|
-
import { dirname as dirname2, join as
|
|
11930
|
+
import { dirname as dirname2, join as join12, resolve as resolve5 } from "node:path";
|
|
11673
11931
|
import { parseArgs } from "node:util";
|
|
11674
11932
|
|
|
11675
11933
|
// apps/agent/src/agent-path.ts
|
|
11676
11934
|
import { constants } from "node:fs";
|
|
11677
11935
|
import { access, stat } from "node:fs/promises";
|
|
11678
|
-
import { join as
|
|
11936
|
+
import { join as join11, resolve as resolve4 } from "node:path";
|
|
11679
11937
|
var AGENTS = ["claude", "codex"];
|
|
11680
11938
|
var PATH_KEYS = { claude: "claudePath", codex: "codexPath" };
|
|
11681
11939
|
var DIR_KEYS = { claude: "claudeConfigDir", codex: "codexHome" };
|
|
11682
11940
|
var DIR_FLAGS = { claude: "claude-config-dir", codex: "codex-home" };
|
|
11683
11941
|
var DIR_ENV = { claude: "CLAUDE_CONFIG_DIR", codex: "CODEX_HOME" };
|
|
11684
11942
|
var isAgent = (v) => AGENTS.includes(v);
|
|
11685
|
-
var expandHome = (p, home) => home && (p === "~" || p.startsWith("~/")) ?
|
|
11943
|
+
var expandHome = (p, home) => home && (p === "~" || p.startsWith("~/")) ? join11(home, p.slice(1)) : p;
|
|
11686
11944
|
async function checkAgentFile(given, o) {
|
|
11687
11945
|
const path = resolve4(o.cwd, expandHome(given, o.home));
|
|
11688
11946
|
let info;
|
|
@@ -11711,7 +11969,7 @@ async function checkFolder(given, o) {
|
|
|
11711
11969
|
var agentEnv = (env, agent, folder) => folder === undefined ? env : { ...env, [DIR_ENV[agent]]: folder };
|
|
11712
11970
|
|
|
11713
11971
|
// apps/agent/src/user-config.ts
|
|
11714
|
-
var userConfigPath = (env) =>
|
|
11972
|
+
var userConfigPath = (env) => join12(env.XDG_CONFIG_HOME?.trim() || join12(env.HOME?.trim() || homedir(), ".config"), "keepsnappy", "config.yml");
|
|
11715
11973
|
var isObj2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
11716
11974
|
function parseDoc(text, label) {
|
|
11717
11975
|
try {
|
|
@@ -12086,9 +12344,10 @@ var listFindings = (t, f = {}) => get(t, "v1/findings", { branch: f.branch, main
|
|
|
12086
12344
|
var branchStatus = (t, branch) => get(t, "v1/branches/status", { branch });
|
|
12087
12345
|
var listProblems = (t, f = {}) => get(t, "v1/problems", { branch: f.branch, main: f.main ? "true" : undefined, journey: f.journey, phone: f.phone, limit: f.limit, offset: f.offset || undefined });
|
|
12088
12346
|
var getFinding = (t, id) => get(t, `v1/findings/${encodeURIComponent(id)}`);
|
|
12089
|
-
var getInvestigation = (t, id) => get(t, `v1/findings/${encodeURIComponent(id)}/investigation
|
|
12347
|
+
var getInvestigation = (t, id, q = {}) => get(t, `v1/findings/${encodeURIComponent(id)}/investigation`, q);
|
|
12090
12348
|
var listEvents = (t, q = {}) => get(t, "v1/events", q);
|
|
12091
12349
|
var listBranches = (t, q = {}) => get(t, "v1/branches", q);
|
|
12350
|
+
var listFixProposals = (t, q = {}) => get(t, "v1/fix-proposals", q);
|
|
12092
12351
|
var postFixProposal = (t, submission) => get(t, "v1/fix-proposals", {}, { method: "POST", body: submission });
|
|
12093
12352
|
var submitHypotheses = (t, id, submission) => get(t, `v1/findings/${encodeURIComponent(id)}/hypotheses`, {}, { method: "POST", body: submission });
|
|
12094
12353
|
var postInvestigationRun = (t, id, run, opts = {}) => get(t, `v1/findings/${encodeURIComponent(id)}/investigation-runs`, {}, { method: "POST", body: run, ...opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {} });
|
|
@@ -16333,12 +16592,14 @@ class Rows {
|
|
|
16333
16592
|
var LABEL_W = 14;
|
|
16334
16593
|
var line = (label, text) => ` ${label.padEnd(LABEL_W)}${text}`;
|
|
16335
16594
|
var plural2 = (n, w) => `${n} ${w}${n === 1 ? "" : "s"}`;
|
|
16595
|
+
var notComparedLines = (places, indent) => (places ?? []).map((p) => `${indent}${p.text}`);
|
|
16336
16596
|
var promptText = (f) => f.what_to_do.prompt ?? "No prompt: this finding is an improvement.";
|
|
16337
16597
|
function formatFinding(f, opts = {}) {
|
|
16338
16598
|
const r = new Rows;
|
|
16339
16599
|
r.both(f.answer.headline, line("What changed", f.answer.what_changed), line("Where", f.answer.where), line("How sure", f.answer.how_sure));
|
|
16340
16600
|
if (f.lane)
|
|
16341
16601
|
r.both(...laneContextLines(f.lane.context, f.lane.journey, f.lane.phone_name, opts.verbose, opts.form === "short"));
|
|
16602
|
+
r.both(...notComparedLines(f.not_compared, " "));
|
|
16342
16603
|
r.full(line("Why", f.answer.why));
|
|
16343
16604
|
r.both(line("What to do", f.answer.what_to_do));
|
|
16344
16605
|
r.full(line("Seen", f.answer.lifecycle), ...f.lifecycle.unstable_note ? [line("", f.lifecycle.unstable_note)] : [], line("Layer", `${f.layer.id} · ${f.layer.name} (${f.certificate ? "certified" : "uncertified"})${f.limits.length ? `; limits: ${f.limits.join("; ")}` : ""}`));
|
|
@@ -16354,10 +16615,14 @@ function formatFinding(f, opts = {}) {
|
|
|
16354
16615
|
if (memory.length) {
|
|
16355
16616
|
r.both(` Memory evidence: ${f.where.on === "main" ? "earlier → latest" : "main → PR"} (values are not a verdict)`);
|
|
16356
16617
|
for (const m of memory) {
|
|
16357
|
-
const value = (n) => n === null ?
|
|
16618
|
+
const value = (n) => n === null ? NOT_STORED_WORD : fieldValueText(m.metric, n);
|
|
16358
16619
|
r.add(m.metric === f.what.metric, ` ${m.label}: ${value(m.before)} → ${value(m.after)}${m.word ? ` (${m.word})` : ""}`);
|
|
16359
16620
|
}
|
|
16360
16621
|
}
|
|
16622
|
+
if (f.why.cpu?.place)
|
|
16623
|
+
r.both(` ${f.why.cpu.place.text}`);
|
|
16624
|
+
if (f.why.cpu?.session)
|
|
16625
|
+
r.both(` ${f.why.cpu.session.text}`);
|
|
16361
16626
|
if (opts.prompt)
|
|
16362
16627
|
r.full("", promptText(f));
|
|
16363
16628
|
return r.render(opts.form, [
|
|
@@ -16387,7 +16652,7 @@ function measuredLines(l, r, opts) {
|
|
|
16387
16652
|
};
|
|
16388
16653
|
if (m.lanes.length === 1) {
|
|
16389
16654
|
r.both(`${scope}: newest measured ${f.branch ? "" : `on ${n.branch}, `}${lane(n)}; ${plural2(m.run_sets, "run set")} measured.`);
|
|
16390
|
-
r.both(...(n.comparison_notes ?? []).map((line) => ` ${line}`));
|
|
16655
|
+
r.both(...(n.comparison_notes ?? []).map((line) => ` ${line}`), ...notComparedLines(n.not_compared, " "));
|
|
16391
16656
|
r.full(...(n.session_speed ?? []).map((line) => ` ${line}`));
|
|
16392
16657
|
r.both(...laneContextLines(n.context, n.journey, n.phone_name, opts.verbose, opts.form === "short"));
|
|
16393
16658
|
return [];
|
|
@@ -16395,10 +16660,10 @@ function measuredLines(l, r, opts) {
|
|
|
16395
16660
|
const shown = m.lanes.slice(0, LANES_SHOWN);
|
|
16396
16661
|
r.add(shown.some((x) => !x.judged || !!x.context), `${scope}: ${plural2(m.run_sets, "run set")} measured; the newest of each journey × phone:`);
|
|
16397
16662
|
for (const x of shown) {
|
|
16398
|
-
const keep = !x.judged || !!x.context || !!x.comparison_notes;
|
|
16663
|
+
const keep = !x.judged || !!x.context || !!x.comparison_notes || !!x.not_compared?.length;
|
|
16399
16664
|
r.add(keep, ` ${x.journey} on ${x.phone_name}${f.branch ? "" : ` (${x.branch})`}: ${lane(x)}`);
|
|
16400
16665
|
r.add(keep, ...resultContextLines(x.context, opts.verbose, opts.form === "short").map((line) => ` ${line}`));
|
|
16401
|
-
r.add(keep, ...(x.comparison_notes ?? []).map((line) => ` ${line}`));
|
|
16666
|
+
r.add(keep, ...(x.comparison_notes ?? []).map((line) => ` ${line}`), ...notComparedLines(x.not_compared, " "));
|
|
16402
16667
|
r.full(...(x.session_speed ?? []).map((line) => ` ${line}`));
|
|
16403
16668
|
}
|
|
16404
16669
|
if (m.lanes.length > shown.length)
|
|
@@ -16467,11 +16732,12 @@ function formatBranchStatus(s, opts = {}) {
|
|
|
16467
16732
|
const shown = `${l.result === "blocks" ? `${l.word} (${blocks})` : badgedWord(l.word, l.blocking)}${l.stale && !l.rules ? ` (${STALE_FLAG})` : ""}`;
|
|
16468
16733
|
const sha = l.commit?.sha?.slice(0, 7);
|
|
16469
16734
|
r.both(` ${l.journey_title ?? l.journey} on ${l.phone_name}: ${shown}${feltWords(l)} · ${sha ? `commit ${sha}` : "commit not recorded"} (${when(l.at)})${l.in_newest_run_set ? "" : " · older run set"}${l.waiting_for_reprocess ? `; ${waitingWords(l.waiting_for_reprocess)}` : ""}`);
|
|
16735
|
+
r.both(...outcomeLines(l.branch_outcome).map((line) => ` ${line}`));
|
|
16470
16736
|
r.both(...resultContextLines(l.context, opts.verbose, opts.form === "short"));
|
|
16471
16737
|
const rulesLine = staleRulesText(l.rules);
|
|
16472
16738
|
if (rulesLine)
|
|
16473
16739
|
r.both(` ${rulesLine}`);
|
|
16474
|
-
r.both(...(l.comparison_notes ?? []).map((line) => ` ${line}`));
|
|
16740
|
+
r.both(...(l.comparison_notes ?? []).map((line) => ` ${line}`), ...notComparedLines(l.not_compared, " "));
|
|
16475
16741
|
r.full(...(l.session_speed ?? []).map((line) => ` ${line}`));
|
|
16476
16742
|
for (const f of l.worst)
|
|
16477
16743
|
r.both(` ${f.id} ${f.headline}`);
|
|
@@ -16653,7 +16919,7 @@ function formatCommits(c) {
|
|
|
16653
16919
|
].join(`
|
|
16654
16920
|
`);
|
|
16655
16921
|
}
|
|
16656
|
-
var
|
|
16922
|
+
var list2 = (xs) => xs.join(", ");
|
|
16657
16923
|
function versionText(label, counts) {
|
|
16658
16924
|
const values = Object.keys(counts).sort((a, b) => a === "unknown" ? -1 : b === "unknown" ? 1 : a < b ? -1 : a > b ? 1 : 0);
|
|
16659
16925
|
const name = (k) => k === "unknown" ? "not recorded" : k;
|
|
@@ -16669,7 +16935,7 @@ function formatSessionPlaces(p, opts = {}) {
|
|
|
16669
16935
|
const commit = s.commit ? `${s.commit.title ? `“${s.commit.title}”${s.commit.author ? ` by ${s.commit.author}` : ""}` : sha ?? "commit not recorded"}${s.commit.title && sha ? ` (${sha})` : ""}` : "commit not recorded";
|
|
16670
16936
|
const v = s.versions;
|
|
16671
16937
|
const codes = Object.keys(s.runs.invalid_codes);
|
|
16672
|
-
const r = new Rows().both(`Session ${s.completed_at ? `${when(s.completed_at)} ` : ""}on ${s.branch}: ${commit}${s.pull_request?.number ? `, PR #${s.pull_request.number}` : ""}; ${s.journey.title ?? s.journey.key} on ${s.phone.model}${s.phone.os ? ` · ${s.phone.os}` : ""}${s.status === "processed" ? "" : ` (run set ${s.status})`}.`, `Runs: ${s.runs.valid} valid${s.runs.invalid ? `, ${s.runs.invalid} invalid${codes.length ? ` (${
|
|
16938
|
+
const r = new Rows().both(`Session ${s.completed_at ? `${when(s.completed_at)} ` : ""}on ${s.branch}: ${commit}${s.pull_request?.number ? `, PR #${s.pull_request.number}` : ""}; ${s.journey.title ?? s.journey.key} on ${s.phone.model}${s.phone.os ? ` · ${s.phone.os}` : ""}${s.status === "processed" ? "" : ` (run set ${s.status})`}.`, `Runs: ${s.runs.valid} valid${s.runs.invalid ? `, ${s.runs.invalid} invalid${codes.length ? ` (${list2(codes)})` : ""}` : ""}. ${[
|
|
16673
16939
|
versionText("SDK", v.sdk),
|
|
16674
16940
|
versionText("driver", v.driver),
|
|
16675
16941
|
versionText("driver version", v.driver_version),
|
|
@@ -16723,7 +16989,7 @@ function formatSessionPlaces(p, opts = {}) {
|
|
|
16723
16989
|
}
|
|
16724
16990
|
}
|
|
16725
16991
|
if (comps.length) {
|
|
16726
|
-
r.full(` Components: ${
|
|
16992
|
+
r.full(` Components: ${list2(comps)}`);
|
|
16727
16993
|
componentLines++;
|
|
16728
16994
|
}
|
|
16729
16995
|
});
|
|
@@ -16758,6 +17024,7 @@ function formatMainSessions(h, opts = {}) {
|
|
|
16758
17024
|
r.add(k, ` ${MAIN_SUMMARY_PENDING}${s.summary_reason ? ` (${s.summary_reason})` : ""}`);
|
|
16759
17025
|
else
|
|
16760
17026
|
r.add(k, ` ${MAIN_TIERS_WORST_FIRST.map((tier) => `${MAIN_TIER_WORDS[tier]}: ${s.places[tier]}`).join("; ")}${s.cant_reason ? `; ${mainSessionCantWords(s.places.cant, s.cant_reason, baseline)}` : s.cant_range ? `; ${mainSessionCantRangeWords(s.places.cant, s.cant_range, baseline)}` : ""}`);
|
|
17027
|
+
r.add(k, ...notComparedLines(s.not_compared, " "));
|
|
16761
17028
|
if (s.waiting_sessions)
|
|
16762
17029
|
r.add(k, ` ${waitingSessionsWords(s.waiting_sessions)}`);
|
|
16763
17030
|
if (s.memory)
|
|
@@ -16897,9 +17164,13 @@ function formatInvestigation(p, opts = {}) {
|
|
|
16897
17164
|
const sha = v.lane.commit?.sha?.slice(0, 7);
|
|
16898
17165
|
r.both(`${v.lane.journey_title ?? v.lane.journey} on ${v.lane.phone_name}, branch ${v.lane.branch} (${v.lane.on === "main" ? "main" : "PR"}) · ${sha ? `commit ${sha}` : "commit not recorded"} (${when(v.lane.at)})`);
|
|
16899
17166
|
r.both(`Verdict: ${v.stale ? `${v.word} (${STALE_FLAG})` : badgedWord(v.word, v.blocking)}${feltWords({ felt_cost: v.felt_cost ?? undefined, to_check: v.to_check_count ?? undefined })}${v.reason ? ` · ${v.reason}` : ""}`);
|
|
16900
|
-
r.both(...v.comparison_notes.map((n) => ` ${n}`));
|
|
17167
|
+
r.both(...v.comparison_notes.map((n) => ` ${n}`), ...notComparedLines(v.not_compared, " "));
|
|
16901
17168
|
if (v.waiting_for_reprocess)
|
|
16902
17169
|
r.both(` ${waitingWords(v.waiting_for_reprocess)}`);
|
|
17170
|
+
if (f.why?.cpu?.place)
|
|
17171
|
+
r.both(` ${f.why.cpu.place.text}`);
|
|
17172
|
+
if (f.why?.cpu?.session)
|
|
17173
|
+
r.both(` ${f.why.cpu.session.text}`);
|
|
16903
17174
|
} else
|
|
16904
17175
|
r.both(`No stored verdict for this finding (${f.kind} finding): ${f.answer.headline}`);
|
|
16905
17176
|
r.both(`Places with a worse result: ${heldWords(p.places)}`);
|
|
@@ -16954,6 +17225,14 @@ function formatInvestigation(p, opts = {}) {
|
|
|
16954
17225
|
...c.available && c.commits.items.length ? [plural2(c.commits.items.length, "commit line")] : []
|
|
16955
17226
|
]);
|
|
16956
17227
|
}
|
|
17228
|
+
function formatFixProposals(a) {
|
|
17229
|
+
if (a.proposals.length === 0)
|
|
17230
|
+
return `No fix was proposed${a.branch ? ` for ${a.branch}` : ""} yet: \`keepsnappy fix <pr|branch>\` makes one.`;
|
|
17231
|
+
return a.proposals.map((p) => [`Fix for ${p.source_pr === null ? p.source_branch : `#${p.source_pr} (${p.source_branch})`}: ${p.fix_branch}${p.fix_pr_number === null ? "" : `, draft PR #${p.fix_pr_number}`}${p.fix_pr_url ? ` ${p.fix_pr_url}` : ""}`, fixCardText(p.card)].join(`
|
|
17232
|
+
`)).join(`
|
|
17233
|
+
|
|
17234
|
+
`);
|
|
17235
|
+
}
|
|
16957
17236
|
function runLine(r) {
|
|
16958
17237
|
return [
|
|
16959
17238
|
r.created_at.slice(0, 16).replace("T", " "),
|
|
@@ -16999,6 +17278,7 @@ var FINDINGS_USAGE = `keepsnappy findings [--branch NAME | --main] [--journey KE
|
|
|
16999
17278
|
main --journey KEY --phone MODEL [--sessions N | --session ID] [--vs-previous] [--measure REF] [--limit N] [--offset N] [--short] [--json]
|
|
17000
17279
|
main --journey KEY --phone MODEL --changes [--limit N] [--json] (main's confirmed changes, newest first: commit range from the session before to the first session at the new level, and the PRs merged in it)
|
|
17001
17280
|
hypotheses <finding-id> [--json] (the hypotheses stored about a finding by keepsnappy investigate, with their outcome, and the investigation runs that made them or did not, each with the tokens it used and what that would cost on an API key; predictions and cost, never a verdict.)
|
|
17281
|
+
fixes [--branch NAME | --pr N] [--json] (the fix PRs keepsnappy fix opened: the card of each, what it did to every finding it was for: Fixed, No change, Worse, or Waiting for the fix branch; --branch is the PR branch or the fix branch, --pr the PR or the draft PR)
|
|
17002
17282
|
layers [--json]
|
|
17003
17283
|
trend <journey> <window words...> --phone MODEL [--window PLACE ...] [--ref REF ...] [--sessions N] [--short] [--json]
|
|
17004
17284
|
(the words after the journey are one window name; more windows with repeated --window)
|
|
@@ -17010,7 +17290,7 @@ ${READ_USAGE.detailFlag}
|
|
|
17010
17290
|
var STATES = ["present", "not-judged", "resolved", "all"];
|
|
17011
17291
|
var FINDING_ID = /^f_[0-9a-f]{16}$/;
|
|
17012
17292
|
var SHORT_COMMANDS = new Set(["findings", "status", "problems", "finding", "compare", "history", "places", "main", "trend"]);
|
|
17013
|
-
var FINDINGS_COMMANDS = new Set(["findings", "status", "problems", "finding", "compare", "history", "commits", "places", "layers", "main", "trend", "hypotheses"]);
|
|
17293
|
+
var FINDINGS_COMMANDS = new Set(["findings", "status", "problems", "finding", "compare", "history", "commits", "places", "layers", "main", "trend", "hypotheses", "fixes"]);
|
|
17014
17294
|
function resolveReadTarget(o, rt) {
|
|
17015
17295
|
const keyEnv = o["key-env"] ?? "KEEPSNAPPY_KEY";
|
|
17016
17296
|
const key = projectKey(rt.env, keyEnv);
|
|
@@ -17032,6 +17312,7 @@ async function findingsCommand(cmd, argv, rt) {
|
|
|
17032
17312
|
branch: { type: "string" },
|
|
17033
17313
|
main: { type: "boolean" },
|
|
17034
17314
|
journey: { type: "string" },
|
|
17315
|
+
pr: { type: "string" },
|
|
17035
17316
|
phone: { type: "string" },
|
|
17036
17317
|
state: { type: "string" },
|
|
17037
17318
|
session: { type: "string" },
|
|
@@ -17171,6 +17452,14 @@ async function findingsCommand(cmd, argv, rt) {
|
|
|
17171
17452
|
${h.outcome_word}`), ...formatInvestigationRuns(v), v.note].join(`
|
|
17172
17453
|
`));
|
|
17173
17454
|
}
|
|
17455
|
+
case "fixes": {
|
|
17456
|
+
const pr = o.pr === undefined ? undefined : Number(o.pr);
|
|
17457
|
+
if (pr !== undefined && (!Number.isInteger(pr) || pr < 1))
|
|
17458
|
+
throw new UsageError(["--pr must be a PR number"]);
|
|
17459
|
+
if (o.branch && pr !== undefined)
|
|
17460
|
+
throw new UsageError(["give --branch or --pr, not both"]);
|
|
17461
|
+
return out(await listFixProposals(t, { ...o.branch ? { branch: o.branch } : {}, ...pr !== undefined ? { pr } : {} }), formatFixProposals);
|
|
17462
|
+
}
|
|
17174
17463
|
case "history":
|
|
17175
17464
|
need(2, "<journey> <window>");
|
|
17176
17465
|
return out(await windowHistory(t, { journey: p[0], window: p.slice(1).join(" "), ...o.phone ? { phone: o.phone } : {}, ...limit ? { limit } : {} }), formatHistory);
|
|
@@ -17498,8 +17787,8 @@ import { StringDecoder } from "node:string_decoder";
|
|
|
17498
17787
|
import { randomBytes } from "node:crypto";
|
|
17499
17788
|
import { createRequire } from "node:module";
|
|
17500
17789
|
import { chmod, mkdir as mkdir3, mkdtemp, readFile as readFile4, realpath, rm, writeFile as writeFile3 } from "node:fs/promises";
|
|
17501
|
-
import { tmpdir } from "node:os";
|
|
17502
|
-
import { dirname as dirname3, join as
|
|
17790
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
17791
|
+
import { dirname as dirname3, join as join13, resolve as resolve6 } from "node:path";
|
|
17503
17792
|
import { parseArgs as parseArgs3 } from "node:util";
|
|
17504
17793
|
|
|
17505
17794
|
// apps/agent/src/agent-usage.ts
|
|
@@ -17804,7 +18093,7 @@ async function falseAlarmFacts(pkg, reads) {
|
|
|
17804
18093
|
` Unstable or noisy marks of the lane:
|
|
17805
18094
|
${lines(pkg.verdict?.unstable.items.map((u) => `${u.place_name}: ${u.note}`))}`,
|
|
17806
18095
|
` Comparison notes:
|
|
17807
|
-
${lines(pkg.verdict?.comparison_notes)}`,
|
|
18096
|
+
${lines([...pkg.verdict?.comparison_notes ?? [], ...(pkg.verdict?.not_compared ?? []).map((p) => p.text)])}`,
|
|
17808
18097
|
" A/A results: not stored (no read exposes them)."
|
|
17809
18098
|
];
|
|
17810
18099
|
const read = async (tool, run) => {
|
|
@@ -18078,7 +18367,7 @@ var usageLine = (v) => `${USAGE_PREFIX} ${JSON.stringify(usageJson(v))}`;
|
|
|
18078
18367
|
var USAGE_LINE = /^KEEPSNAPPY-USAGE (\{[^\r\n]{2,12000}\})$/gm;
|
|
18079
18368
|
var count2 = (v, max) => v === null || typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= max;
|
|
18080
18369
|
var cost2 = (v) => v === null || typeof v === "number" && Number.isFinite(v) && v >= 0 && v <= INVESTIGATION_RUN_LIMITS.costUsd;
|
|
18081
|
-
var
|
|
18370
|
+
var label3 = (v, max) => typeof v === "string" && v.length >= 1 && v.length <= max && !/[\u0000-\u001f]/.test(v);
|
|
18082
18371
|
var TOKEN_KEYS = ["input_tokens", "output_tokens", "cache_read_tokens", "cache_creation_tokens", "cache_creation_5m_tokens", "cache_creation_1h_tokens"];
|
|
18083
18372
|
var sameKeys2 = (o, keys) => Object.keys(o).length === keys.length && keys.every((k) => (k in o));
|
|
18084
18373
|
function readUsageLine(log) {
|
|
@@ -18093,14 +18382,15 @@ function readUsageLine(log) {
|
|
|
18093
18382
|
}
|
|
18094
18383
|
if (!isObj3(v) || !sameKeys2(v, KEYS))
|
|
18095
18384
|
return null;
|
|
18096
|
-
const ok = typeof v.run_id === "string" && /^[A-Za-z0-9._:-]{8,64}$/.test(v.run_id) && INVESTIGATION_RUN_OUTCOMES.includes(v.outcome) &&
|
|
18385
|
+
const ok = typeof v.run_id === "string" && /^[A-Za-z0-9._:-]{8,64}$/.test(v.run_id) && INVESTIGATION_RUN_OUTCOMES.includes(v.outcome) && label3(v.agent, INVESTIGATION_RUN_LIMITS.label) && (v.model === null || label3(v.model, INVESTIGATION_RUN_LIMITS.label)) && (v.by_model === null || Array.isArray(v.by_model) && v.by_model.length >= 1 && v.by_model.length <= INVESTIGATION_RUN_LIMITS.models && v.by_model.every((m) => isObj3(m) && sameKeys2(m, ["model", ...TOKEN_KEYS]) && label3(m.model, INVESTIGATION_RUN_LIMITS.label) && TOKEN_KEYS.every((k) => count2(m[k], INVESTIGATION_RUN_LIMITS.tokens)))) && TOKEN_KEYS.every((k) => count2(v[k], INVESTIGATION_RUN_LIMITS.tokens)) && count2(v.total_tokens, 6 * INVESTIGATION_RUN_LIMITS.tokens) && count2(v.turns, INVESTIGATION_RUN_LIMITS.turns) && count2(v.duration_ms, INVESTIGATION_RUN_LIMITS.durationMs) && cost2(v.reported_cost_usd) && cost2(v.estimated_cost_usd) && label3(v.price_table_version, 40) && typeof v.stored === "boolean";
|
|
18097
18386
|
return ok ? v : null;
|
|
18098
18387
|
}
|
|
18099
18388
|
|
|
18100
18389
|
// apps/agent/src/investigate.ts
|
|
18101
|
-
var INVESTIGATE_USAGE = `keepsnappy investigate <finding-id> [--repo PATH] [--agent claude|codex] [--claude-path FILE] [--codex-path FILE] [--claude-config-dir DIR] [--codex-home DIR] [--codex-model NAME] [--max-turns N] [--max-minutes N] [--save] [--dry-run] [--preflight] [--json] [--config keepsnappy.yml]
|
|
18390
|
+
var INVESTIGATE_USAGE = `keepsnappy investigate <finding-id> [--branch NAME] [--repo PATH] [--agent claude|codex] [--claude-path FILE] [--codex-path FILE] [--claude-config-dir DIR] [--codex-home DIR] [--codex-model NAME] [--max-turns N] [--max-minutes N] [--save] [--dry-run] [--preflight] [--json] [--config keepsnappy.yml]
|
|
18102
18391
|
Reads the finding's investigation package, checks out the measured commit in a separate worktree of your app repo, runs your own claude or codex
|
|
18103
18392
|
there read-only, validates its hypotheses against the schema and stores them. --dry-run does everything except the last step.
|
|
18393
|
+
--branch NAME: the package of that PR (its lane, its measured commit): one finding is one place, which can be worse on several PRs; without it, the PR it was seen on last.
|
|
18104
18394
|
--preflight does everything before the agent starts and calls no model: reads the package, checks the commit is in your repo, adds and removes the worktree, runs
|
|
18105
18395
|
the agent with --version. It ends with KEEPSNAPPY-STATUS preflight-ok, or fails the way a real run would.
|
|
18106
18396
|
--repo defaults to investigate.repos.<app> in keepsnappy.yml; --agent, --claude-path, --codex-path, --max-turns and --max-minutes to investigate.agent,
|
|
@@ -18267,6 +18557,10 @@ async function readInvestigateSettings(explicit, env = process.env, cwd = proces
|
|
|
18267
18557
|
const outDir = isObj4(output) && typeof output.dir === "string" && output.dir.trim() !== "" ? { outDir: output.dir } : {};
|
|
18268
18558
|
return { ...user, ...project, ...outDir, repos: { ...user.repos, ...project.repos }, paths: { ...user.paths, ...project.paths }, dirs: { ...user.dirs, ...project.dirs } };
|
|
18269
18559
|
}
|
|
18560
|
+
var realTimer = (ms, fire) => {
|
|
18561
|
+
const t = setTimeout(fire, ms);
|
|
18562
|
+
return () => clearTimeout(t);
|
|
18563
|
+
};
|
|
18270
18564
|
var MAX_STDERR_CHARS = 64 * 1024;
|
|
18271
18565
|
var MAX_STDOUT_HEAD_CHARS = 4096;
|
|
18272
18566
|
function runAgentProcess(o) {
|
|
@@ -18291,7 +18585,7 @@ function runAgentProcess(o) {
|
|
|
18291
18585
|
killTree("SIGTERM");
|
|
18292
18586
|
hard = setTimeout(() => killTree("SIGKILL"), o.killGraceMs);
|
|
18293
18587
|
};
|
|
18294
|
-
const
|
|
18588
|
+
const cancelTimer = (o.timer ?? realTimer)(o.timeoutMs, () => halt("timeout"));
|
|
18295
18589
|
const onAbort = () => halt("aborted");
|
|
18296
18590
|
if (o.signal.aborted)
|
|
18297
18591
|
onAbort();
|
|
@@ -18318,7 +18612,7 @@ function runAgentProcess(o) {
|
|
|
18318
18612
|
});
|
|
18319
18613
|
child.on("exit", () => killTree("SIGKILL"));
|
|
18320
18614
|
child.on("close", (code) => {
|
|
18321
|
-
|
|
18615
|
+
cancelTimer();
|
|
18322
18616
|
clearTimeout(hard);
|
|
18323
18617
|
o.signal.removeEventListener("abort", onAbort);
|
|
18324
18618
|
killTree("SIGKILL");
|
|
@@ -18381,7 +18675,7 @@ var rangeStart = (p) => p.main_change ? p.main_change.commits[0] : p.commits.ava
|
|
|
18381
18675
|
function mcpServerCommand() {
|
|
18382
18676
|
try {
|
|
18383
18677
|
const entry = createRequire(import.meta.url).resolve("@keepsnappy/mcp");
|
|
18384
|
-
return { command: process.execPath, args: [
|
|
18678
|
+
return { command: process.execPath, args: [join13(dirname3(entry), "bin.js")] };
|
|
18385
18679
|
} catch {
|
|
18386
18680
|
return { command: "keepsnappy-mcp", args: [] };
|
|
18387
18681
|
}
|
|
@@ -18427,6 +18721,7 @@ async function investigateRun(argv, rt, end, step) {
|
|
|
18427
18721
|
allowPositionals: true,
|
|
18428
18722
|
options: {
|
|
18429
18723
|
finding: { type: "string" },
|
|
18724
|
+
branch: { type: "string" },
|
|
18430
18725
|
repo: { type: "string" },
|
|
18431
18726
|
agent: { type: "string" },
|
|
18432
18727
|
"claude-path": { type: "string" },
|
|
@@ -18454,6 +18749,8 @@ async function investigateRun(argv, rt, end, step) {
|
|
|
18454
18749
|
throw new UsageError(["keepsnappy investigate needs one <finding-id>", INVESTIGATE_USAGE]);
|
|
18455
18750
|
if (o.preflight && o.save)
|
|
18456
18751
|
throw new UsageError(["--preflight changes nothing: it cannot be combined with --save", INVESTIGATE_USAGE]);
|
|
18752
|
+
if (o.branch !== undefined && o.branch.trim() === "")
|
|
18753
|
+
throw new UsageError(["--branch needs a branch name", INVESTIGATE_USAGE]);
|
|
18457
18754
|
const cwd = rt.cwd ?? process.cwd();
|
|
18458
18755
|
const home = rt.env.HOME;
|
|
18459
18756
|
const saved = await readInvestigateSettings(o.config, rt.env, cwd);
|
|
@@ -18517,7 +18814,7 @@ async function investigateRun(argv, rt, end, step) {
|
|
|
18517
18814
|
}
|
|
18518
18815
|
const target = resolveReadTarget(o, rt);
|
|
18519
18816
|
step(1);
|
|
18520
|
-
const pkg = await getInvestigation(target, id);
|
|
18817
|
+
const pkg = await getInvestigation(target, id, o.branch ? { branch: o.branch } : {});
|
|
18521
18818
|
const sha = measuredCommit(pkg);
|
|
18522
18819
|
const base = rangeStart(pkg);
|
|
18523
18820
|
for (const c of [sha, base])
|
|
@@ -18571,8 +18868,8 @@ async function investigateRun(argv, rt, end, step) {
|
|
|
18571
18868
|
process.once("SIGINT", stopped);
|
|
18572
18869
|
process.once("SIGTERM", stopped);
|
|
18573
18870
|
step(2);
|
|
18574
|
-
scratchDir = await mkdtemp(
|
|
18575
|
-
worktree =
|
|
18871
|
+
scratchDir = await mkdtemp(join13(rt.tmpdir ?? tmpdir2(), "keepsnappy-investigate-"));
|
|
18872
|
+
worktree = join13(scratchDir, "code");
|
|
18576
18873
|
const add = await git(["worktree", "add", "--detach", worktree, sha]);
|
|
18577
18874
|
if (add.code !== 0)
|
|
18578
18875
|
throw new Error(`git worktree add failed in ${root}: ${add.stderr.trim()}`);
|
|
@@ -18590,18 +18887,18 @@ async function investigateRun(argv, rt, end, step) {
|
|
|
18590
18887
|
let lastMessage;
|
|
18591
18888
|
let mcpConfig = "";
|
|
18592
18889
|
if (agent === "claude") {
|
|
18593
|
-
mcpConfig =
|
|
18890
|
+
mcpConfig = join13(scratchDir, "mcp.json");
|
|
18594
18891
|
const server = rt.mcpServer ?? mcpServerCommand();
|
|
18595
18892
|
await writeFile3(mcpConfig, JSON.stringify({ mcpServers: { keepsnappy: { ...server, env: { KEEPSNAPPY_KEY: target.key, KEEPSNAPPY_API: target.url } } } }), { mode: 384 });
|
|
18596
18893
|
await chmod(mcpConfig, 384);
|
|
18597
18894
|
args = claudeArgs({ maxTurns, mcpConfig });
|
|
18598
18895
|
} else {
|
|
18599
|
-
lastMessage =
|
|
18896
|
+
lastMessage = join13(scratchDir, "last-message.txt");
|
|
18600
18897
|
args = codexArgs({ lastMessage, worktree: await realpath(worktree), model: codexModel });
|
|
18601
18898
|
}
|
|
18602
18899
|
step(3);
|
|
18603
18900
|
const startedAt = Date.now();
|
|
18604
|
-
const run = await runAgentProcess({ agent, cmd: command, args, cwd: worktree, env: childEnv, stdin: prompt, timeoutMs: maxMinutes * 60000, killGraceMs: rt.killGraceMs ?? 5000, signal: abort.signal });
|
|
18901
|
+
const run = await runAgentProcess({ agent, cmd: command, args, cwd: worktree, env: childEnv, stdin: prompt, timeoutMs: maxMinutes * 60000, killGraceMs: rt.killGraceMs ?? 5000, signal: abort.signal, ...rt.runTimer ? { timer: rt.runTimer } : {} });
|
|
18605
18902
|
let measuredMs = Date.now() - startedAt;
|
|
18606
18903
|
if (run.stop === "not-found")
|
|
18607
18904
|
throw new Error(given === undefined ? `${agent} not found on PATH: install it and log in once with \`${agent} login\`` : `${command} could not be started (not found)`);
|
|
@@ -18704,9 +19001,9 @@ async function investigateRun(argv, rt, end, step) {
|
|
|
18704
19001
|
}
|
|
18705
19002
|
const keep = async (attempts) => {
|
|
18706
19003
|
try {
|
|
18707
|
-
const dir =
|
|
19004
|
+
const dir = join13(resolve6(cwd, expandHome(saved.outDir ?? DEFAULT_OUT_DIR, home)), "investigate");
|
|
18708
19005
|
await mkdir3(dir, { recursive: true });
|
|
18709
|
-
const file =
|
|
19006
|
+
const file = join13(dir, `${id.replace(/[^A-Za-z0-9._-]/g, "_")}-${Date.now()}.json`);
|
|
18710
19007
|
await writeFile3(file, `${JSON.stringify({ finding_id: id, prompt_version: INVESTIGATE_PROMPT_VERSION, attempts }, null, 2)}
|
|
18711
19008
|
`, { mode: 384 });
|
|
18712
19009
|
log(`The refused answer is kept in ${file} (local only, never uploaded).`);
|
|
@@ -18744,18 +19041,19 @@ async function investigateRun(argv, rt, end, step) {
|
|
|
18744
19041
|
if (agent === "claude")
|
|
18745
19042
|
secondArgs = claudeArgs({ maxTurns: REPAIR_TURNS, mcpConfig });
|
|
18746
19043
|
else {
|
|
18747
|
-
secondLast =
|
|
19044
|
+
secondLast = join13(scratchDir, "last-message-repair.txt");
|
|
18748
19045
|
secondArgs = codexArgs({ lastMessage: secondLast, worktree: await realpath(worktree), model: codexModel });
|
|
18749
19046
|
}
|
|
19047
|
+
const repairMinutes = rt.repairMs === undefined ? Math.min(maxMinutes, REPAIR_MINUTES) : rt.repairMs / 60000;
|
|
18750
19048
|
const secondStart = Date.now();
|
|
18751
|
-
const second = await runAgentProcess({ agent, cmd: command, args: secondArgs, cwd: worktree, env: childEnv, stdin: repairPrompt({ answerText: answer.text, errors: attempts[0].errors }), timeoutMs:
|
|
19049
|
+
const second = await runAgentProcess({ agent, cmd: command, args: secondArgs, cwd: worktree, env: childEnv, stdin: repairPrompt({ answerText: answer.text, errors: attempts[0].errors }), timeoutMs: repairMinutes * 60000, killGraceMs: rt.killGraceMs ?? 5000, signal: abort.signal });
|
|
18752
19050
|
measuredMs += Date.now() - secondStart;
|
|
18753
19051
|
usage = addUsage(usage, second.stream.usage());
|
|
18754
19052
|
if (second.stop === "aborted") {
|
|
18755
19053
|
log("Stopped: the agent and the worktree are removed.");
|
|
18756
19054
|
return endsWith("stopped", "stopped", SIGINT_EXIT);
|
|
18757
19055
|
}
|
|
18758
|
-
const again = second.stop === "timeout" ? { text: "", failed: `no result within ${
|
|
19056
|
+
const again = second.stop === "timeout" ? { text: "", failed: `no result within ${repairMinutes} min` } : second.stop === "oversize" ? { text: "", failed: "it printed too much" } : await answerOf(second, secondLast);
|
|
18759
19057
|
if (again.failed)
|
|
18760
19058
|
notes.push(`The repair turn gave no answer: ${again.failed}.`);
|
|
18761
19059
|
else {
|
|
@@ -18927,7 +19225,7 @@ function jsonNotifier(emit) {
|
|
|
18927
19225
|
else if (a?.kind === "burst")
|
|
18928
19226
|
emit({ type: "burst", app: a.app, count: a.count, body: a.body });
|
|
18929
19227
|
else if (a?.kind === "ended")
|
|
18930
|
-
emit({ type: "investigation", finding_id: a.findingId, state: "ended", status: a.status, count: a.count, text: a.text, ...a.usage ? { usage: a.usage } : {} });
|
|
19228
|
+
emit({ type: "investigation", finding_id: a.findingId, branch: a.branch, state: "ended", status: a.status, count: a.count, text: a.text, ...a.usage ? { usage: a.usage } : {} });
|
|
18931
19229
|
return null;
|
|
18932
19230
|
}
|
|
18933
19231
|
};
|
|
@@ -18955,16 +19253,16 @@ async function detectNotifier(spawner, platform, print, preferred) {
|
|
|
18955
19253
|
|
|
18956
19254
|
// apps/agent/src/setup-detect.ts
|
|
18957
19255
|
import { readdir as readdir2, stat as stat3 } from "node:fs/promises";
|
|
18958
|
-
import { join as
|
|
19256
|
+
import { join as join14 } from "node:path";
|
|
18959
19257
|
function agentSearchDirs(home) {
|
|
18960
|
-
return [...home ? [
|
|
19258
|
+
return [...home ? [join14(home, ".local", "bin")] : [], "/opt/homebrew/bin"];
|
|
18961
19259
|
}
|
|
18962
19260
|
async function detectAgentPath(agent, o) {
|
|
18963
19261
|
const shell = o.env.SHELL?.trim() || "/bin/zsh";
|
|
18964
19262
|
const r = await o.spawner.run(shell, ["-lc", `command -v ${agent}`], { timeoutMs: 5000, env: stringEnv(o.env) });
|
|
18965
19263
|
const fromShell = !r.error && r.code === 0 ? r.stdout.trim().split(`
|
|
18966
19264
|
`).at(-1)?.trim() : undefined;
|
|
18967
|
-
const candidates = [...fromShell && fromShell.startsWith("/") ? [fromShell] : [], ...(o.dirs ?? agentSearchDirs(o.home)).map((d) =>
|
|
19265
|
+
const candidates = [...fromShell && fromShell.startsWith("/") ? [fromShell] : [], ...(o.dirs ?? agentSearchDirs(o.home)).map((d) => join14(d, agent))];
|
|
18968
19266
|
for (const c of candidates) {
|
|
18969
19267
|
const checked = await checkAgentFile(c, { home: o.home, cwd: "/" });
|
|
18970
19268
|
if ("path" in checked)
|
|
@@ -19004,7 +19302,7 @@ async function scanForRepos(o) {
|
|
|
19004
19302
|
if (seen.has(dir) || o.now() - start > budget)
|
|
19005
19303
|
return;
|
|
19006
19304
|
seen.add(dir);
|
|
19007
|
-
if (self && await isDir(
|
|
19305
|
+
if (self && await isDir(join14(dir, ".git"))) {
|
|
19008
19306
|
found.add(dir);
|
|
19009
19307
|
return;
|
|
19010
19308
|
}
|
|
@@ -19019,11 +19317,11 @@ async function scanForRepos(o) {
|
|
|
19019
19317
|
for (const n of names) {
|
|
19020
19318
|
if (!n.isDirectory() || n.name.startsWith(".") || n.name === "node_modules" || skip?.has(n.name))
|
|
19021
19319
|
continue;
|
|
19022
|
-
await walk(
|
|
19320
|
+
await walk(join14(dir, n.name), depth - 1);
|
|
19023
19321
|
}
|
|
19024
19322
|
};
|
|
19025
19323
|
for (const r of ROOTS)
|
|
19026
|
-
await walk(
|
|
19324
|
+
await walk(join14(o.home, r), ROOT_DEPTH);
|
|
19027
19325
|
await walk(o.home, HOME_DEPTH, false, PROTECTED);
|
|
19028
19326
|
return [...found].sort();
|
|
19029
19327
|
}
|
|
@@ -19050,10 +19348,10 @@ async function findRepoCandidates(o) {
|
|
|
19050
19348
|
|
|
19051
19349
|
// apps/agent/src/watch.ts
|
|
19052
19350
|
import { createHash as createHash2 } from "node:crypto";
|
|
19053
|
-
import { closeSync, openSync, realpathSync } from "node:fs";
|
|
19054
|
-
import { mkdir as mkdir6, readFile as readFile8, rename as rename2, writeFile as writeFile6 } from "node:fs/promises";
|
|
19055
|
-
import { homedir as homedir2 } from "node:os";
|
|
19056
|
-
import { join as
|
|
19351
|
+
import { closeSync, openSync, realpathSync, writeSync } from "node:fs";
|
|
19352
|
+
import { mkdir as mkdir6, readdir as readdir5, readFile as readFile8, rename as rename2, writeFile as writeFile6 } from "node:fs/promises";
|
|
19353
|
+
import { homedir as homedir2, uptime } from "node:os";
|
|
19354
|
+
import { basename as basename4, join as join19 } from "node:path";
|
|
19057
19355
|
import { spawn as spawn6 } from "node:child_process";
|
|
19058
19356
|
import { createInterface } from "node:readline";
|
|
19059
19357
|
import { parseArgs as parseArgs5 } from "node:util";
|
|
@@ -19280,7 +19578,7 @@ var NO_WAIT_MS = 0;
|
|
|
19280
19578
|
var NEW_COMMAND_TIMEOUT_S = 300;
|
|
19281
19579
|
var ANDROID = {
|
|
19282
19580
|
name: "android",
|
|
19283
|
-
defaultDevice: "Google Pixel
|
|
19581
|
+
defaultDevice: "Google Pixel 9 Pro-15.0",
|
|
19284
19582
|
caps: (device) => ({
|
|
19285
19583
|
platformName: "Android",
|
|
19286
19584
|
"appium:automationName": "UiAutomator2",
|
|
@@ -19357,15 +19655,15 @@ class WdError extends Error {
|
|
|
19357
19655
|
// packages/appium-driver/dist/src/version.js
|
|
19358
19656
|
import { createHash } from "node:crypto";
|
|
19359
19657
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
19360
|
-
import { dirname as dirname4, join as
|
|
19658
|
+
import { dirname as dirname4, join as join15, relative as relative2, resolve as resolve7 } from "node:path";
|
|
19361
19659
|
import { fileURLToPath } from "node:url";
|
|
19362
19660
|
function driverVersion() {
|
|
19363
|
-
const built = "0.1.0-alpha.
|
|
19661
|
+
const built = "0.1.0-alpha.25";
|
|
19364
19662
|
if (built)
|
|
19365
19663
|
return built;
|
|
19366
19664
|
let dir = dirname4(fileURLToPath(import.meta.url));
|
|
19367
19665
|
for (let i = 0;i < 3; i++, dir = dirname4(dir)) {
|
|
19368
|
-
const file =
|
|
19666
|
+
const file = join15(dir, "package.json");
|
|
19369
19667
|
if (!existsSync(file))
|
|
19370
19668
|
continue;
|
|
19371
19669
|
try {
|
|
@@ -19662,8 +19960,11 @@ function fixableItem(f, lane, hypotheses) {
|
|
|
19662
19960
|
return { finding_id: f.id, title: f.what.window_name, word: f.word, state: f.state, ms: lane?.felt_cost_ms ?? 0, felt_cost: lane?.felt_cost ?? null, hypotheses, fixable: why === undefined, ...why ? { why_not: why } : {} };
|
|
19663
19961
|
}
|
|
19664
19962
|
var byFeltCost = (a, b) => Number(b.fixable) - Number(a.fixable) || b.ms - a.ms || (a.finding_id < b.finding_id ? -1 : 1);
|
|
19665
|
-
function formatFixable(l) {
|
|
19963
|
+
function formatFixable(l, o = {}) {
|
|
19666
19964
|
const where = l.branch === null ? l.pr === null ? "the app" : `PR #${l.pr}` : `${l.branch}${l.pr === null ? "" : ` (PR #${l.pr})`}`;
|
|
19965
|
+
if (o.blocked && l.findings.length)
|
|
19966
|
+
return [`Nothing on ${where} can be fixed now: ${o.blocked}`, ...l.findings.map((f) => ` ${f.finding_id} ${f.word} ${f.title} ${f.felt_cost ?? "—"} not fixable: ${f.why_not}`)].join(`
|
|
19967
|
+
`);
|
|
19667
19968
|
if (!l.findings.some((f) => f.fixable))
|
|
19668
19969
|
return `No Worse or Slightly worse finding is present on ${where} now; nothing to fix.`;
|
|
19669
19970
|
return [`Findings of ${where}, what can be fixed first and the biggest felt cost first:`, ...l.findings.map((f) => ` ${f.finding_id} ${f.word} ${f.title} ${f.felt_cost ?? "—"} ${f.fixable ? `${f.hypotheses} ${f.hypotheses === 1 ? "hypothesis" : "hypotheses"}` : `not fixable: ${f.why_not}`}`)].join(`
|
|
@@ -19674,14 +19975,14 @@ function formatFixable(l) {
|
|
|
19674
19975
|
import { execFile as execFile2 } from "node:child_process";
|
|
19675
19976
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
19676
19977
|
import { chmod as chmod2, mkdir as mkdir5, mkdtemp as mkdtemp3, readFile as readFile7, readdir as readdir4, realpath as realpath4, rm as rm3, writeFile as writeFile5 } from "node:fs/promises";
|
|
19677
|
-
import { tmpdir as
|
|
19678
|
-
import { basename as
|
|
19978
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
19979
|
+
import { basename as basename3, join as join18, resolve as resolve9 } from "node:path";
|
|
19679
19980
|
import { parseArgs as parseArgs4 } from "node:util";
|
|
19680
19981
|
|
|
19681
19982
|
// apps/agent/src/fix-agent.ts
|
|
19682
19983
|
import { execFile } from "node:child_process";
|
|
19683
19984
|
import { mkdir as mkdir4, mkdtemp as mkdtemp2, readFile as readFile5, realpath as realpath2, rm as rm2, stat as stat4, writeFile as writeFile4 } from "node:fs/promises";
|
|
19684
|
-
import { join as
|
|
19985
|
+
import { join as join16 } from "node:path";
|
|
19685
19986
|
|
|
19686
19987
|
// apps/agent/src/fix-env.ts
|
|
19687
19988
|
var NEVER = /^(GH_|GITHUB_|GITLAB_|SSH_|GPG_|AWS_|AZURE_|GOOGLE_|GCLOUD_|NPM_|YARN_|KEEPSNAPPY_|VERCEL_|EXPO_|EAS_)|(_TOKEN|_SECRET|_PASSWORD)$/;
|
|
@@ -19800,17 +20101,17 @@ async function codexFixGate(o) {
|
|
|
19800
20101
|
else if (on)
|
|
19801
20102
|
problems.push(`${name} is still on after --disable ${name}: codex may give the model a shell`);
|
|
19802
20103
|
}
|
|
19803
|
-
const dir = await realpath2(await mkdtemp2(
|
|
20104
|
+
const dir = await realpath2(await mkdtemp2(join16(await realpath2(o.tmpdir), "keepsnappy-codex-gate-")));
|
|
19804
20105
|
try {
|
|
19805
|
-
const wt =
|
|
20106
|
+
const wt = join16(dir, "code");
|
|
19806
20107
|
await mkdir4(wt);
|
|
19807
|
-
await writeFile4(
|
|
20108
|
+
await writeFile4(join16(wt, ".git"), `gitdir: /nonexistent
|
|
19808
20109
|
`);
|
|
19809
|
-
await mkdir4(
|
|
19810
|
-
const decoy =
|
|
20110
|
+
await mkdir4(join16(dir, "home"));
|
|
20111
|
+
const decoy = join16(dir, "home", ".zshrc");
|
|
19811
20112
|
await writeFile4(decoy, `HOME-SECRET-decoy
|
|
19812
20113
|
`);
|
|
19813
|
-
const outside =
|
|
20114
|
+
const outside = join16(dir, "outside.txt");
|
|
19814
20115
|
const sh = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
19815
20116
|
const script = [
|
|
19816
20117
|
'( echo x > in.txt ) 2>/dev/null; echo "in:$?"',
|
|
@@ -19849,12 +20150,12 @@ async function codexFixGate(o) {
|
|
|
19849
20150
|
const outsideExists = await stat4(outside).then(() => true, () => false);
|
|
19850
20151
|
if (outsideExists)
|
|
19851
20152
|
problems.push("a file was written outside the worktree by the sandbox probe");
|
|
19852
|
-
if (await stat4(
|
|
20153
|
+
if (await stat4(join16(wt, "hardlink.txt")).then(() => true, () => false))
|
|
19853
20154
|
problems.push("a hard link to a home file stand-in exists in the worktree after the sandbox probe");
|
|
19854
20155
|
if (await readFile5(decoy, "utf8") !== `HOME-SECRET-decoy
|
|
19855
20156
|
`)
|
|
19856
20157
|
problems.push("a home file stand-in was changed by the sandbox probe");
|
|
19857
|
-
if (await readFile5(
|
|
20158
|
+
if (await readFile5(join16(wt, ".git"), "utf8") !== `gitdir: /nonexistent
|
|
19858
20159
|
`)
|
|
19859
20160
|
problems.push("the worktree’s .git file was changed by the sandbox probe");
|
|
19860
20161
|
} finally {
|
|
@@ -19977,7 +20278,7 @@ function packageScriptsChanged(before, after) {
|
|
|
19977
20278
|
// apps/agent/src/fix-git.ts
|
|
19978
20279
|
import { spawn as spawn4 } from "node:child_process";
|
|
19979
20280
|
import { lstat, readFile as readFile6, readdir as readdir3, readlink, realpath as realpath3 } from "node:fs/promises";
|
|
19980
|
-
import { dirname as dirname5, join as
|
|
20281
|
+
import { dirname as dirname5, join as join17, resolve as resolve8, sep as sep2 } from "node:path";
|
|
19981
20282
|
var SAFE = ["-c", "core.hooksPath=/dev/null", "-c", "core.fsmonitor=false"];
|
|
19982
20283
|
var CAP = 16 * 1024 * 1024;
|
|
19983
20284
|
function makeFixGit(prefix, env) {
|
|
@@ -20003,10 +20304,10 @@ function makeFixGit(prefix, env) {
|
|
|
20003
20304
|
});
|
|
20004
20305
|
}
|
|
20005
20306
|
async function worktreeGitDir(worktree) {
|
|
20006
|
-
const gitFile = await readFile6(
|
|
20307
|
+
const gitFile = await readFile6(join17(worktree, ".git"), "utf8");
|
|
20007
20308
|
const m = /^gitdir: (.+)\n?$/.exec(gitFile);
|
|
20008
20309
|
if (!m)
|
|
20009
|
-
throw new Error(`${
|
|
20310
|
+
throw new Error(`${join17(worktree, ".git")} is not a worktree file`);
|
|
20010
20311
|
return { gitDir: resolve8(worktree, m[1]), gitFile };
|
|
20011
20312
|
}
|
|
20012
20313
|
var MAX_ENTRIES = 500000;
|
|
@@ -20015,7 +20316,7 @@ async function resolveLoose(path) {
|
|
|
20015
20316
|
let at = path;
|
|
20016
20317
|
for (;; ) {
|
|
20017
20318
|
try {
|
|
20018
|
-
return
|
|
20319
|
+
return join17(await realpath3(at), ...rest.reverse());
|
|
20019
20320
|
} catch (e) {
|
|
20020
20321
|
const code = e.code;
|
|
20021
20322
|
if (code === "ELOOP")
|
|
@@ -20038,7 +20339,7 @@ async function scanSymlinks(worktree) {
|
|
|
20038
20339
|
continue;
|
|
20039
20340
|
if (++seen > MAX_ENTRIES)
|
|
20040
20341
|
throw new Error("the checkout has too many files to scan");
|
|
20041
|
-
const full =
|
|
20342
|
+
const full = join17(dir, e.name);
|
|
20042
20343
|
const rel = full.slice(root.length + 1);
|
|
20043
20344
|
if (e.isSymbolicLink()) {
|
|
20044
20345
|
const target = await readlink(full);
|
|
@@ -20089,7 +20390,7 @@ async function collectChanges(git, worktree) {
|
|
|
20089
20390
|
const sizes = new Map;
|
|
20090
20391
|
for (const rec of raw.stdout.split("\x00"))
|
|
20091
20392
|
if (rec && !rec.startsWith(":"))
|
|
20092
|
-
sizes.set(rec, (await lstat(
|
|
20393
|
+
sizes.set(rec, (await lstat(join17(worktree, rec)).catch(() => {
|
|
20093
20394
|
return;
|
|
20094
20395
|
}))?.size ?? 0);
|
|
20095
20396
|
return parseChanges(raw.stdout, num.stdout, (path) => sizes.get(path) ?? 0);
|
|
@@ -20292,21 +20593,22 @@ var FIX_USAGE = `keepsnappy fix <pr-number|branch> all present Wors
|
|
|
20292
20593
|
a binary or more than ${DEFAULT_CAPS.files} files / ${DEFAULT_CAPS.lines} lines (fix.max_files, fix.max_lines), and commits one commit per fix, then checks the committed tree again.
|
|
20293
20594
|
A workflow file, .gitattributes, .npmrc, a hook or the config of an AI tool is refused even with --allow-path; a build or install script is allowed and flagged in the PR.
|
|
20294
20595
|
Then, with your own git and gh login, it pushes the branch and opens a draft PR into the PR's branch (never the default branch; a fork is refused) and records it.
|
|
20295
|
-
--dry-run keeps the branch local: nothing is pushed, opened or recorded. --list prints the fixable findings, biggest felt cost first, and exits
|
|
20596
|
+
--dry-run keeps the branch local: nothing is pushed, opened or recorded. --list prints the fixable findings, biggest felt cost first, and exits
|
|
20597
|
+
(no agent, no git write: it reads git rev-parse and one gh pr view, so a finding the run would refuse shows as not fixable and why).
|
|
20296
20598
|
--json prints one event per line, each with the run's fix_id (started, commit, skipped_finding, flagged, refused, ended with status, text, pr_url, proposal_id and notify); the steps are the
|
|
20297
20599
|
KEEPSNAPPY-STEP lines on stderr. --fix-id ID names the run (fix-<ms>-<6 hex>); watch gives each run one. --branch B names the PR branch (the same as the positional).
|
|
20298
20600
|
Budget: ${FIX_BUDGET.toolCalls + 1} turns, ${FIX_BUDGET.minutes} minutes (fix.max_turns, fix.max_minutes). Claude must be ${MIN_CLAUDE_VERSION} or newer. Codex is used only when its sandbox probe passes.
|
|
20299
20601
|
(key: KEEPSNAPPY_KEY or --key-env NAME; API: --api URL or KEEPSNAPPY_API; gh must be logged in)`;
|
|
20300
20602
|
var FIX_STEP_LABELS = ["reading the findings", "checking out", "editing", "checking the diff", "committing", "opening the draft PR"];
|
|
20301
20603
|
var STEPS2 = FIX_STEP_LABELS.length;
|
|
20302
|
-
var FIX_END_STATUSES = ["dry-run", "opened", "none", "refused", "main", "fork", "closed", "exists", "unsafe-checkout", "timeout", "failed", "not-json", "schema", "stopped", "push", "gh", "api"];
|
|
20604
|
+
var FIX_END_STATUSES = ["dry-run", "opened", "none", "refused", "main", "fork", "closed", "exists", "unsafe-checkout", "timeout", "failed", "not-json", "schema", "stopped", "push", "gh", "gh-auth", "no-pr", "no-repo", "no-findings", "api"];
|
|
20303
20605
|
var FINDING_ID2 = /^f_[0-9a-f]{16}$/;
|
|
20304
20606
|
var REF = /^[A-Za-z0-9_][A-Za-z0-9_./-]{0,254}$/;
|
|
20305
20607
|
var REMOTE2 = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,99}$/;
|
|
20306
20608
|
var MAX_TURNS_LIMIT2 = 200;
|
|
20307
20609
|
var MAX_MINUTES_LIMIT2 = 120;
|
|
20308
20610
|
var SIGINT_EXIT2 = 130;
|
|
20309
|
-
var
|
|
20611
|
+
var MAX_FIX_FINDINGS = 20;
|
|
20310
20612
|
var SECTION_KEYS = ["max_turns", "max_minutes", "max_files", "max_lines"];
|
|
20311
20613
|
async function readFixSettings(explicit, env, cwd) {
|
|
20312
20614
|
const layers = await readConfigLayers({ explicit, env, cwd });
|
|
@@ -20353,7 +20655,7 @@ async function readFixSettings(explicit, env, cwd) {
|
|
|
20353
20655
|
var runTool = (cmd, args, o) => new Promise((done) => {
|
|
20354
20656
|
execFile2(cmd, args, { cwd: o.cwd, env: o.env, timeout: o.timeoutMs ?? 60000, maxBuffer: 4 << 20 }, (err, stdout, stderr) => {
|
|
20355
20657
|
const code = err ? typeof err.code === "number" ? err.code : null : 0;
|
|
20356
|
-
done({ code, stdout: String(stdout ?? ""), stderr: String(stderr ?? err?.message ?? "") });
|
|
20658
|
+
done({ code, stdout: String(stdout ?? ""), stderr: String(stderr ?? err?.message ?? ""), timedOut: err?.killed === true });
|
|
20357
20659
|
});
|
|
20358
20660
|
});
|
|
20359
20661
|
function parsePr(text) {
|
|
@@ -20370,6 +20672,28 @@ function parsePr(text) {
|
|
|
20370
20672
|
return;
|
|
20371
20673
|
return { headRefName, isCrossRepository, state, url, number };
|
|
20372
20674
|
}
|
|
20675
|
+
var GH_AUTH_REQUIRED = 4;
|
|
20676
|
+
async function describePr(ref, o) {
|
|
20677
|
+
const view = await runTool("gh", ["pr", "view", ref, "--json", "headRefName,isCrossRepository,state,url,number"], o);
|
|
20678
|
+
const pr = view.code === 0 ? parsePr(view.stdout) : undefined;
|
|
20679
|
+
if (pr)
|
|
20680
|
+
return { pr };
|
|
20681
|
+
if (view.timedOut)
|
|
20682
|
+
return { status: "gh", text: `gh did not describe the pull request ${ref} in ${Math.round((o.timeoutMs ?? 60000) / 1000)} s` };
|
|
20683
|
+
const text = `gh could not describe the pull request ${ref}${view.stderr.trim() ? `: ${oneLine(view.stderr.trim().slice(0, 200))}` : ""}`;
|
|
20684
|
+
if (view.code === GH_AUTH_REQUIRED)
|
|
20685
|
+
return { status: "gh-auth", text };
|
|
20686
|
+
return { status: view.code !== 0 && /no pull requests found|could not resolve to a pullrequest/i.test(view.stderr) ? "no-pr" : "gh", text };
|
|
20687
|
+
}
|
|
20688
|
+
function prBlock(pr) {
|
|
20689
|
+
if (pr.isCrossRepository)
|
|
20690
|
+
return { status: "fork", text: `PR #${pr.number} comes from a fork: the fix branch could not be pushed to it` };
|
|
20691
|
+
if (pr.state !== "OPEN")
|
|
20692
|
+
return { status: "closed", text: `PR #${pr.number} is ${pr.state.toLowerCase()}` };
|
|
20693
|
+
return;
|
|
20694
|
+
}
|
|
20695
|
+
var LIST_WHY_NOT = { "gh-auth": FIX_WHY_NOT.ghAuth, "no-pr": FIX_WHY_NOT.noPr, gh: FIX_WHY_NOT.gh, fork: FIX_WHY_NOT.fork, closed: FIX_WHY_NOT.closed };
|
|
20696
|
+
var LIST_GH_TIMEOUT_MS = 1e4;
|
|
20373
20697
|
function expectedText(e) {
|
|
20374
20698
|
if (!isObj4(e) || typeof e.metric !== "string")
|
|
20375
20699
|
return "";
|
|
@@ -20388,7 +20712,7 @@ var sanitizeComponent = (c, files) => {
|
|
|
20388
20712
|
const clean = c.replace(/[^A-Za-z0-9_.$-]/g, "").slice(0, 40);
|
|
20389
20713
|
if (clean)
|
|
20390
20714
|
return clean;
|
|
20391
|
-
const fromFile =
|
|
20715
|
+
const fromFile = basename3(files[0] ?? "").replace(/\.[^.]*$/, "").replace(/[^A-Za-z0-9_.$-]/g, "").slice(0, 40);
|
|
20392
20716
|
return fromFile || "app";
|
|
20393
20717
|
};
|
|
20394
20718
|
var oneLineText = plainLine;
|
|
@@ -20543,42 +20867,54 @@ async function fixCommand(argv, rt) {
|
|
|
20543
20867
|
list = await listFindings(target, { limit: 1 });
|
|
20544
20868
|
app = list.app;
|
|
20545
20869
|
}
|
|
20546
|
-
|
|
20547
|
-
|
|
20548
|
-
|
|
20549
|
-
repoPath = saved.repos?.[app];
|
|
20550
|
-
if (!repoPath)
|
|
20551
|
-
throw new ConfigError([`no repo for the app ${app}: give --repo PATH, or add investigate.repos.${app}: <path> to keepsnappy.yml`]);
|
|
20552
|
-
}
|
|
20870
|
+
const listing = o.list === true && sourceBranch !== undefined;
|
|
20871
|
+
let blocked;
|
|
20872
|
+
const repoPath = o.repo ?? saved.repos?.[app];
|
|
20553
20873
|
let root = "";
|
|
20554
20874
|
let userGit;
|
|
20555
|
-
|
|
20556
|
-
|
|
20557
|
-
|
|
20558
|
-
|
|
20559
|
-
|
|
20560
|
-
|
|
20561
|
-
|
|
20562
|
-
|
|
20563
|
-
|
|
20564
|
-
userGit = makeFixGit(["-C", root], rt.env);
|
|
20565
|
-
}
|
|
20875
|
+
const top = repoPath ? await makeFixGit(["-C", resolve9(cwd, expandHome(repoPath, home))], rt.env)(["rev-parse", "--show-toplevel"]) : undefined;
|
|
20876
|
+
if (!top || top.code !== 0) {
|
|
20877
|
+
const text = repoPath ? `${resolve9(cwd, expandHome(repoPath, home))} is not a git repository (--repo, or investigate.repos in keepsnappy.yml)` : `no repo for the app ${app}: give --repo PATH, or add investigate.repos.${app}: <path> to keepsnappy.yml`;
|
|
20878
|
+
if (!listing)
|
|
20879
|
+
return finishEarly("no-repo", [{ path: app, rule: "no-repo" }], text);
|
|
20880
|
+
blocked = fixNoRepoText(app);
|
|
20881
|
+
} else {
|
|
20882
|
+
root = top.stdout.trim();
|
|
20883
|
+
userGit = makeFixGit(["-C", root], rt.env);
|
|
20566
20884
|
}
|
|
20567
20885
|
let pr;
|
|
20568
|
-
if (!
|
|
20569
|
-
const
|
|
20570
|
-
|
|
20571
|
-
if (
|
|
20572
|
-
|
|
20573
|
-
if (
|
|
20574
|
-
|
|
20886
|
+
if (!blocked) {
|
|
20887
|
+
const ref = String(sourcePr ?? sourceBranch);
|
|
20888
|
+
const described = await describePr(ref, { cwd: root, env: rt.env, ...o.list ? { timeoutMs: rt.listGhTimeoutMs ?? LIST_GH_TIMEOUT_MS } : {} });
|
|
20889
|
+
if ("pr" in described)
|
|
20890
|
+
pr = described.pr;
|
|
20891
|
+
else if (listing)
|
|
20892
|
+
blocked = LIST_WHY_NOT[described.status];
|
|
20893
|
+
else
|
|
20894
|
+
return finishEarly(described.status, [{ path: ref, rule: "no-pull-request" }], described.text);
|
|
20895
|
+
}
|
|
20896
|
+
if (pr) {
|
|
20897
|
+
if (sourceBranch !== undefined && sourceBranch !== pr.headRefName) {
|
|
20898
|
+
if (!listing)
|
|
20899
|
+
return finishEarly("gh", [{ path: sourceBranch, rule: "not-the-pr-branch" }], `the pull request ${pr.number} is on ${pr.headRefName}, not ${sourceBranch}`);
|
|
20900
|
+
blocked = FIX_WHY_NOT.noPr;
|
|
20901
|
+
}
|
|
20575
20902
|
if (sourceBranch === undefined) {
|
|
20576
20903
|
sourceBranch = pr.headRefName;
|
|
20577
20904
|
list = await listOf(sourceBranch);
|
|
20578
20905
|
if (sourceBranch === list.baseline_branch || list.findings.some((f) => f.where.on === "main"))
|
|
20579
20906
|
return finishEarly("main", [{ path: sourceBranch, rule: "main" }], `${sourceBranch} is the baseline branch: there is no branch off main to fix`);
|
|
20580
20907
|
}
|
|
20581
|
-
|
|
20908
|
+
const refused = prBlock(pr);
|
|
20909
|
+
if (o.list && !blocked && refused)
|
|
20910
|
+
blocked = LIST_WHY_NOT[refused.status];
|
|
20911
|
+
if (sourceBranch === pr.headRefName)
|
|
20912
|
+
sourcePr = pr.number;
|
|
20913
|
+
if (o.list && !blocked && userGit) {
|
|
20914
|
+
const left = `${FIX_BRANCH_PREFIX}${pr.number}`;
|
|
20915
|
+
if ((await userGit(["rev-parse", "--verify", "--quiet", `refs/heads/${left}`])).code === 0)
|
|
20916
|
+
blocked = fixEndText("exists", 0, { fixBranch: left, repo: root }) ?? undefined;
|
|
20917
|
+
}
|
|
20582
20918
|
}
|
|
20583
20919
|
const branch = sourceBranch;
|
|
20584
20920
|
if (auto && branch.startsWith(FIX_BRANCH_PREFIX))
|
|
@@ -20597,10 +20933,14 @@ async function fixCommand(argv, rt) {
|
|
|
20597
20933
|
}
|
|
20598
20934
|
picks.sort((a, b) => b.feltMs - a.feltMs || (a.finding.id < b.finding.id ? -1 : 1));
|
|
20599
20935
|
const hypothesesOfFinding = new Map(picks.map((p) => [p.finding.id, p.hypotheses.length]));
|
|
20600
|
-
const dropped = picks.length >
|
|
20936
|
+
const dropped = picks.length > MAX_FIX_FINDINGS ? picks.splice(MAX_FIX_FINDINGS) : [];
|
|
20601
20937
|
if (o.list) {
|
|
20602
|
-
const
|
|
20603
|
-
|
|
20938
|
+
const item = (f) => {
|
|
20939
|
+
const one = fixableItem(f, laneOf2(list, f), hypothesesOfFinding.get(f.id) ?? 0);
|
|
20940
|
+
return blocked && one.fixable ? { ...one, fixable: false, why_not: blocked } : one;
|
|
20941
|
+
};
|
|
20942
|
+
const shown = { version: 1, app: appName, pr: prNumber, branch, findings: list.findings.map(item).sort(byFeltCost) };
|
|
20943
|
+
rt.print(json ? JSON.stringify({ ...shown, max_findings: MAX_FIX_FINDINGS }) : formatFixable(shown, { blocked }));
|
|
20604
20944
|
return 0;
|
|
20605
20945
|
}
|
|
20606
20946
|
const findingIds = picks.map((p) => p.finding.id);
|
|
@@ -20609,18 +20949,17 @@ async function fixCommand(argv, rt) {
|
|
|
20609
20949
|
for (const id of notFixable)
|
|
20610
20950
|
emit("skipped_finding", { finding_id: id, reason: "not present on the branch now (resolved, not Worse, or not on this PR)" });
|
|
20611
20951
|
log(picks.length === 0 ? `Nothing to fix: no Worse or Slightly worse finding is present on ${branch} now.` : `${notFixable.join(", ")}: not fixable now; nothing was started.`);
|
|
20612
|
-
return finishDone("
|
|
20952
|
+
return finishDone("no-findings", { commits: 0, fixBranch: "", text: "nothing to fix" }, notFixable.length ? 1 : 0);
|
|
20613
20953
|
}
|
|
20614
20954
|
for (const d of dropped)
|
|
20615
|
-
emit("skipped_finding", { finding_id: d.finding.id, reason: `more than ${
|
|
20955
|
+
emit("skipped_finding", { finding_id: d.finding.id, reason: `more than ${MAX_FIX_FINDINGS} findings: the smaller ones were left out` });
|
|
20616
20956
|
const git = userGit;
|
|
20617
20957
|
const info = pr;
|
|
20618
|
-
|
|
20619
|
-
|
|
20620
|
-
|
|
20621
|
-
return finishEarly("closed", [{ path: info.url, rule: "closed" }], `PR #${info.number} is ${info.state.toLowerCase()}`);
|
|
20958
|
+
const refused = prBlock(info);
|
|
20959
|
+
if (refused)
|
|
20960
|
+
return finishEarly(refused.status, [{ path: info.url, rule: refused.status }], refused.text);
|
|
20622
20961
|
if (agent === "codex") {
|
|
20623
|
-
const gate = await (rt.codexGate ?? ((g) => codexFixGate({ command: g.command, env: rt.env, tmpdir: rt.tmpdir ??
|
|
20962
|
+
const gate = await (rt.codexGate ?? ((g) => codexFixGate({ command: g.command, env: rt.env, tmpdir: rt.tmpdir ?? tmpdir3() })))({ command });
|
|
20624
20963
|
if (!gate.ok) {
|
|
20625
20964
|
log(["codex is not used for fix: its sandbox probe did not pass.", ...gate.problems.map((p) => ` ${p}`)].join(`
|
|
20626
20965
|
`));
|
|
@@ -20628,7 +20967,7 @@ async function fixCommand(argv, rt) {
|
|
|
20628
20967
|
}
|
|
20629
20968
|
}
|
|
20630
20969
|
if (agent === "claude") {
|
|
20631
|
-
const seen = await checkClaudeVersion({ command, env: fixAgentEnv("claude", rt.env, { scratch: rt.tmpdir ??
|
|
20970
|
+
const seen = await checkClaudeVersion({ command, env: fixAgentEnv("claude", rt.env, { scratch: rt.tmpdir ?? tmpdir3(), extra: rt.agentEnvExtra ?? [] }) });
|
|
20632
20971
|
if (!seen.ok)
|
|
20633
20972
|
return finishEarly("refused", [{ path: "claude", rule: "agent-version" }], seen.problem);
|
|
20634
20973
|
}
|
|
@@ -20640,10 +20979,10 @@ async function fixCommand(argv, rt) {
|
|
|
20640
20979
|
const taken = async (name) => {
|
|
20641
20980
|
const local = await git(["rev-parse", "--verify", "--quiet", `refs/heads/${name}`]);
|
|
20642
20981
|
if (local.code === 0)
|
|
20643
|
-
return
|
|
20982
|
+
return "local";
|
|
20644
20983
|
const far = await git(["ls-remote", "--exit-code", "--heads", remote, `refs/heads/${name}`]);
|
|
20645
20984
|
if (far.code === 0)
|
|
20646
|
-
return
|
|
20985
|
+
return "remote";
|
|
20647
20986
|
return far.code === 2 ? false : undefined;
|
|
20648
20987
|
};
|
|
20649
20988
|
let fixBranch = `${FIX_BRANCH_PREFIX}${info.number}`;
|
|
@@ -20654,7 +20993,7 @@ async function fixCommand(argv, rt) {
|
|
|
20654
20993
|
if (!t)
|
|
20655
20994
|
break;
|
|
20656
20995
|
if (!o.new || n > 20)
|
|
20657
|
-
return finishEarly("exists", [{ path: fixBranch, rule: "exists" }], `${fixBranch} already exists${o.new ? " (and so do the next suffixes)" : "; --new takes a fresh suffix"}
|
|
20996
|
+
return finishEarly("exists", [{ path: fixBranch, rule: "exists" }], `${fixBranch} already exists${o.new ? " (and so do the next suffixes)" : "; --new takes a fresh suffix"}`, { fixBranch, ...t === "local" ? { repo: root } : {} });
|
|
20658
20997
|
fixBranch = `${FIX_BRANCH_PREFIX}${info.number}-${n}`;
|
|
20659
20998
|
}
|
|
20660
20999
|
const abort = new AbortController;
|
|
@@ -20669,8 +21008,8 @@ async function fixCommand(argv, rt) {
|
|
|
20669
21008
|
process.once("SIGINT", stopped);
|
|
20670
21009
|
process.once("SIGTERM", stopped);
|
|
20671
21010
|
step(2);
|
|
20672
|
-
scratch = await mkdtemp3(
|
|
20673
|
-
worktree =
|
|
21011
|
+
scratch = await mkdtemp3(join18(await realpath4(rt.tmpdir ?? tmpdir3()), "keepsnappy-fix-"));
|
|
21012
|
+
worktree = join18(scratch, "code");
|
|
20674
21013
|
const add = await git(["worktree", "add", "-b", fixBranch, worktree, baseSha]);
|
|
20675
21014
|
if (add.code !== 0)
|
|
20676
21015
|
throw new Error(`git worktree add failed: ${add.stderr.trim()}`);
|
|
@@ -20682,26 +21021,26 @@ async function fixCommand(argv, rt) {
|
|
|
20682
21021
|
if (links.length) {
|
|
20683
21022
|
return await refuse("unsafe-checkout", links, "the checkout has a symlink that points outside the worktree: nothing was started");
|
|
20684
21023
|
}
|
|
20685
|
-
await mkdir5(
|
|
21024
|
+
await mkdir5(join18(scratch, "tmp"));
|
|
20686
21025
|
const expected = new Set(["code", "tmp", "mcp.json", "last-message.txt"]);
|
|
20687
21026
|
let mcpConfig = "";
|
|
20688
21027
|
let mcpBefore = "";
|
|
20689
21028
|
let args;
|
|
20690
21029
|
let lastMessage;
|
|
20691
21030
|
if (agent === "claude") {
|
|
20692
|
-
mcpConfig =
|
|
21031
|
+
mcpConfig = join18(scratch, "mcp.json");
|
|
20693
21032
|
const server = rt.mcpServer ?? mcpServerCommand();
|
|
20694
21033
|
await writeFile5(mcpConfig, JSON.stringify({ mcpServers: { keepsnappy: { ...server, env: { KEEPSNAPPY_KEY: target.key, KEEPSNAPPY_API: target.url } } } }), { mode: 384 });
|
|
20695
21034
|
await chmod2(mcpConfig, 384);
|
|
20696
21035
|
mcpBefore = await readFile7(mcpConfig, "utf8");
|
|
20697
21036
|
args = claudeFixArgs({ maxTurns, mcpConfig });
|
|
20698
21037
|
} else {
|
|
20699
|
-
lastMessage =
|
|
21038
|
+
lastMessage = join18(scratch, "last-message.txt");
|
|
20700
21039
|
args = codexFixArgs({ lastMessage, worktree: await realpath4(worktree), model: codexModel });
|
|
20701
21040
|
}
|
|
20702
21041
|
const packages = [];
|
|
20703
21042
|
for (const p of picks)
|
|
20704
|
-
packages.push({ id: p.finding.id, pkg: await getInvestigation(target, p.finding.id) });
|
|
21043
|
+
packages.push({ id: p.finding.id, pkg: await getInvestigation(target, p.finding.id, { branch }) });
|
|
20705
21044
|
const prompt = fixPrompt({
|
|
20706
21045
|
agent,
|
|
20707
21046
|
budget: { toolCalls: maxTurns - 1, minutes: maxMinutes },
|
|
@@ -20709,7 +21048,7 @@ async function fixCommand(argv, rt) {
|
|
|
20709
21048
|
});
|
|
20710
21049
|
step(3);
|
|
20711
21050
|
log(`Fixing with ${agent}: ${baseSha.slice(0, 7)} checked out as ${fixBranch} in a separate worktree (your working copy is not touched).`);
|
|
20712
|
-
const env = fixAgentEnv(agent, rt.env, { scratch:
|
|
21051
|
+
const env = fixAgentEnv(agent, rt.env, { scratch: join18(scratch, "tmp"), extra: rt.agentEnvExtra ?? [], accountDir });
|
|
20713
21052
|
const startedAt = Date.now();
|
|
20714
21053
|
const run = await runAgentProcess({ agent, cmd: command, args, cwd: worktree, env, stdin: prompt, timeoutMs: maxMinutes * 60000, killGraceMs: rt.killGraceMs ?? 5000, signal: abort.signal });
|
|
20715
21054
|
const measuredMs = Date.now() - startedAt;
|
|
@@ -20743,7 +21082,7 @@ async function fixCommand(argv, rt) {
|
|
|
20743
21082
|
return ended("failed", "error", "The agent printed more than it may and was stopped; nothing was committed.");
|
|
20744
21083
|
step(4);
|
|
20745
21084
|
const problems = [];
|
|
20746
|
-
if (await readFile7(
|
|
21085
|
+
if (await readFile7(join18(worktree, ".git"), "utf8").catch(() => "") !== gitFile)
|
|
20747
21086
|
problems.push({ path: ".git", rule: "git-dir-changed" });
|
|
20748
21087
|
for (const name of await readdir4(scratch))
|
|
20749
21088
|
if (!expected.has(name))
|
|
@@ -20867,7 +21206,7 @@ async function fixCommand(argv, rt) {
|
|
|
20867
21206
|
}
|
|
20868
21207
|
const flagged = [];
|
|
20869
21208
|
for (const c of committed) {
|
|
20870
|
-
const rule = flagPath(c.path) ?? (
|
|
21209
|
+
const rule = flagPath(c.path) ?? (basename3(c.path) === "package.json" && packageScriptsChanged(await showBlob(wt, baseSha, c.path, worktree), await showBlob(wt, headSha, c.path, worktree)) ? "package-scripts" : undefined);
|
|
20871
21210
|
if (rule) {
|
|
20872
21211
|
flagged.push({ path: c.path, rule });
|
|
20873
21212
|
emit("flagged", { path: plainLine(c.path, 300), rule }, `Note: ${plainLine(c.path, 300)} is a build script (${rule}); the draft PR says so.`);
|
|
@@ -20984,19 +21323,19 @@ ${shown.map((p) => ` ${p.path}: ${p.rule}`).join(`
|
|
|
20984
21323
|
`)}`);
|
|
20985
21324
|
return finishDone(status, { commits: 0, fixBranch: "", text, ...usage ? { usage } : {} }, 1);
|
|
20986
21325
|
}
|
|
20987
|
-
function finishEarly(status, problems, text) {
|
|
21326
|
+
function finishEarly(status, problems, text, at = {}) {
|
|
20988
21327
|
if (status === "main") {
|
|
20989
21328
|
emit("skipped", { app: app ?? null, pr: sourcePr ?? null, branch: sourceBranch ?? problems[0]?.path ?? null, finding_id: ids[0] ?? null, reason: "main" }, text);
|
|
20990
21329
|
} else
|
|
20991
21330
|
emit("refused", { problems: plainProblems(problems) }, text);
|
|
20992
21331
|
log(plainLine(text, 600));
|
|
20993
|
-
return finishDone(status, { commits: 0, fixBranch: "", text }, 1);
|
|
21332
|
+
return finishDone(status, { commits: 0, fixBranch: at.fixBranch ?? "", text, ...at.repo ? { repo: at.repo } : {} }, 1);
|
|
20994
21333
|
}
|
|
20995
21334
|
function finishDone(status, d, code, errorText) {
|
|
20996
21335
|
if (errorText)
|
|
20997
21336
|
log(plainLine(errorText, 600));
|
|
20998
21337
|
const detail = plainLine(d.text, 300);
|
|
20999
|
-
emit("ended", { status, text: fixEndText(status, d.commits), detail, pr_url: d.prUrl ?? null, proposal_id: d.proposalId ?? null, fix_branch: d.fixBranch, commits: d.commits, usage: d.usage ?? null, notify: auto && FIX_NOTIFY_STATUSES.includes(status) }, d.text);
|
|
21338
|
+
emit("ended", { status, text: fixEndText(status, d.commits, { fixBranch: d.fixBranch, ...d.repo ? { repo: d.repo } : {} }), detail, repo: d.repo ?? null, pr_url: d.prUrl ?? null, proposal_id: d.proposalId ?? null, fix_branch: d.fixBranch, commits: d.commits, usage: d.usage ?? null, notify: auto && FIX_NOTIFY_STATUSES.includes(status) }, d.text);
|
|
21000
21339
|
end(status, d.commits || undefined);
|
|
21001
21340
|
return code;
|
|
21002
21341
|
}
|
|
@@ -21023,8 +21362,10 @@ var WATCH_USAGE = `keepsnappy watch [--app NAME ...] [--branch GLOB] [--mine] [-
|
|
|
21023
21362
|
--once poll once and exit
|
|
21024
21363
|
--json for the macOS menu bar app: no notifications are shown; one JSON object per line on stdout (hello, connected, key_rejected, history, poll, event, burst,
|
|
21025
21364
|
investigation, fix, log; a poll line says when a poll worked, or why it did not, and how long until the next), {"cmd":"investigate","finding_id":"f_…"} lines on stdin start an
|
|
21026
|
-
investigation like a click, {"cmd":"refresh"} polls now (at most one extra poll per 5 s), and
|
|
21027
|
-
\`keepsnappy fix\` and streams its lines (started, progress, commit,
|
|
21365
|
+
investigation like a click (with "branch":"<PR branch>": that PR's result, as investigate --branch), {"cmd":"refresh"} polls now (at most one extra poll per 5 s), and
|
|
21366
|
+
{"cmd":"fix","finding_ids":["f_…"]}, {"cmd":"fix","branch":"feat/x"} or {"cmd":"fix","pr":N} runs \`keepsnappy fix\` and streams its lines (started, progress, commit,
|
|
21367
|
+
skipped_finding, flagged, refused, ended); the end of stdin stops watching (docs/plans/82a); an investigation still running from a watcher before this one (same app,
|
|
21368
|
+
since the last boot) is announced again at the start ("resumed":true on its started line) and followed to its end
|
|
21028
21369
|
--agent, --claude-path, --codex-path, --claude-config-dir, --codex-home which agent an [Investigate] click runs, where its executable is and which account folder it runs on: handed to \`keepsnappy investigate\`, which reads
|
|
21029
21370
|
the same keys (investigate.agent, investigate.claudePath, investigate.codexPath, investigate.claudeConfigDir, investigate.codexHome) from keepsnappy.yml and from your own config file
|
|
21030
21371
|
(keepsnappy config set investigate.claudePath ~/.local/bin/claude); --auto-on-worse runs claude whatever the agent is
|
|
@@ -21064,6 +21405,23 @@ var isStep = (m) => {
|
|
|
21064
21405
|
};
|
|
21065
21406
|
var FIX_STEP_LINE = /^KEEPSNAPPY-STEP (\d{1,2})\/(\d{1,2}) ([^\r\n]{1,60})$/;
|
|
21066
21407
|
var PROGRESS_POLL_MS = 1000;
|
|
21408
|
+
var PID_PREFIX = "KEEPSNAPPY-PID";
|
|
21409
|
+
var PID_LINE = /^KEEPSNAPPY-PID (\d+)$/m;
|
|
21410
|
+
var APP_PREFIX = "KEEPSNAPPY-APP";
|
|
21411
|
+
var APP_LINE = /^KEEPSNAPPY-APP (\S+)$/m;
|
|
21412
|
+
var BRANCH_PREFIX = "KEEPSNAPPY-BRANCH";
|
|
21413
|
+
var BRANCH_LINE = /^KEEPSNAPPY-BRANCH ([^\r\n]+)$/m;
|
|
21414
|
+
var INVESTIGATE_LOG = /^investigate-(f_[0-9a-f]{16})(?:-([0-9a-f]{8}))?-(\d+)\.log$/;
|
|
21415
|
+
var branchHash = (branch) => createHash2("sha256").update(branch).digest("hex").slice(0, 8);
|
|
21416
|
+
var RESUME_WINDOW_MS = (MAX_MINUTES_LIMIT + 10) * 60000;
|
|
21417
|
+
var processAlive = (pid) => {
|
|
21418
|
+
try {
|
|
21419
|
+
process.kill(pid, 0);
|
|
21420
|
+
return true;
|
|
21421
|
+
} catch {
|
|
21422
|
+
return false;
|
|
21423
|
+
}
|
|
21424
|
+
};
|
|
21067
21425
|
var REFRESH_MIN_GAP_MS = 5000;
|
|
21068
21426
|
var SHOWN_KEPT = 200;
|
|
21069
21427
|
var AUTO_FIXED_KEPT = 200;
|
|
@@ -21245,7 +21603,7 @@ function selfCommand(env, proc = process, real = realpathSync) {
|
|
|
21245
21603
|
}
|
|
21246
21604
|
}
|
|
21247
21605
|
function fileStore(dir, apiUrl) {
|
|
21248
|
-
const file =
|
|
21606
|
+
const file = join19(dir, `watch-${createHash2("sha256").update(apiUrl.replace(/\/+$/, "")).digest("hex").slice(0, 12)}.json`);
|
|
21249
21607
|
return {
|
|
21250
21608
|
async load() {
|
|
21251
21609
|
try {
|
|
@@ -21273,13 +21631,14 @@ function fileStore(dir, apiUrl) {
|
|
|
21273
21631
|
await writeFile6(tmp, JSON.stringify({ cursor: s.cursor, seen: s.seen.slice(-SEEN_KEPT), ...s.savedAt !== undefined ? { saved_at: s.savedAt } : {}, ...s.shown ? { shown: Object.fromEntries(Object.entries(s.shown).slice(-SHOWN_KEPT)) } : {}, ...s.autoFixed ? { auto_fixed: s.autoFixed.slice(-AUTO_FIXED_KEPT) } : {} }), { mode: 384 });
|
|
21274
21632
|
await rename2(tmp, file);
|
|
21275
21633
|
},
|
|
21276
|
-
logPath: (name) =>
|
|
21277
|
-
readLog: (p) => readFile8(p, "utf8").catch(() => "")
|
|
21634
|
+
logPath: (name) => join19(dir, `${name}.log`),
|
|
21635
|
+
readLog: (p) => readFile8(p, "utf8").catch(() => ""),
|
|
21636
|
+
investigationLogs: async () => (await readdir5(dir).catch(() => [])).filter((n) => INVESTIGATE_LOG.test(n)).map((n) => join19(dir, n))
|
|
21278
21637
|
};
|
|
21279
21638
|
}
|
|
21280
|
-
var stateDir = (env) => env.KEEPSNAPPY_STATE_DIR?.trim() ||
|
|
21639
|
+
var stateDir = (env) => env.KEEPSNAPPY_STATE_DIR?.trim() || join19(env.XDG_STATE_HOME?.trim() || join19(env.HOME ?? homedir2(), ".local", "state"), "keepsnappy");
|
|
21281
21640
|
var nodeBackground = {
|
|
21282
|
-
start(cmd, args, logFile, env) {
|
|
21641
|
+
start(cmd, args, logFile, env, header = []) {
|
|
21283
21642
|
const exits = [];
|
|
21284
21643
|
let done = null;
|
|
21285
21644
|
const finish = (code, error) => {
|
|
@@ -21293,6 +21652,9 @@ var nodeBackground = {
|
|
|
21293
21652
|
try {
|
|
21294
21653
|
fd = openSync(logFile, "a", 384);
|
|
21295
21654
|
const child = spawn6(cmd, [...args], { detached: true, stdio: ["ignore", fd, fd], env: { ...process.env, ...env } });
|
|
21655
|
+
if (child.pid !== undefined)
|
|
21656
|
+
writeSync(fd, [`${PID_PREFIX} ${child.pid}`, ...header.map((l) => l.replace(/[\r\n]+/g, " "))].map((l) => `${l}
|
|
21657
|
+
`).join(""));
|
|
21296
21658
|
child.on("error", (e) => finish(null, e.message));
|
|
21297
21659
|
child.on("exit", (code) => finish(code));
|
|
21298
21660
|
child.unref();
|
|
@@ -21305,7 +21667,8 @@ var nodeBackground = {
|
|
|
21305
21667
|
return { onExit: (cb) => done ? cb(done.code, done.error) : void exits.push(cb) };
|
|
21306
21668
|
}
|
|
21307
21669
|
};
|
|
21308
|
-
var
|
|
21670
|
+
var CLIENT_ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
21671
|
+
var MAX_ECHOED_IDS = 200;
|
|
21309
21672
|
var GITHUB_PR_URL = /^https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/pull\/\d+$/;
|
|
21310
21673
|
var BRANCH_REF = /^[A-Za-z0-9_][A-Za-z0-9_./-]{0,254}$/;
|
|
21311
21674
|
var CONTROL4 = /[\u0000-\u001f\u007f]/g;
|
|
@@ -21372,7 +21735,8 @@ function relayedFixLine(line, run) {
|
|
|
21372
21735
|
const used = v.usage === null || v.usage === undefined ? null : readUsageLine(`${USAGE_PREFIX} ${JSON.stringify(v.usage)}`);
|
|
21373
21736
|
if (!status || prUrl === undefined || proposalId === undefined || branch === null || commits === null)
|
|
21374
21737
|
return null;
|
|
21375
|
-
|
|
21738
|
+
const repo = typeof v.repo === "string" && v.repo.startsWith("/") && !v.repo.split("/").includes("..") ? plainText(v.repo, 1000) ?? undefined : undefined;
|
|
21739
|
+
return endedLine(run, { status, commits, prUrl, proposalId, fixBranch: branch, usage: used ? usageJson(used) : null, ...repo && branch ? { repo } : {} });
|
|
21376
21740
|
}
|
|
21377
21741
|
default:
|
|
21378
21742
|
return null;
|
|
@@ -21384,7 +21748,7 @@ function endedLine(run, e) {
|
|
|
21384
21748
|
fix_id: run.fixId,
|
|
21385
21749
|
state: "ended",
|
|
21386
21750
|
status: e.status,
|
|
21387
|
-
text: fixEndText(e.status, e.commits),
|
|
21751
|
+
text: fixEndText(e.status, e.commits, { fixBranch: e.fixBranch, ...e.repo ? { repo: e.repo } : {} }),
|
|
21388
21752
|
pr_url: e.status === "dry-run" ? null : e.prUrl,
|
|
21389
21753
|
proposal_id: e.proposalId,
|
|
21390
21754
|
fix_branch: e.fixBranch,
|
|
@@ -21393,7 +21757,8 @@ function endedLine(run, e) {
|
|
|
21393
21757
|
notify: run.auto && FIX_NOTIFY_STATUSES.includes(e.status)
|
|
21394
21758
|
};
|
|
21395
21759
|
}
|
|
21396
|
-
var jobOf = (ev, findingId, title) => ({ findingId, app: ev.app, branch: ev.branch, url: ev.url, title });
|
|
21760
|
+
var jobOf = (ev, findingId, title) => ({ findingId, app: ev.app, branch: ev.branch, url: ev.url, title, about: ev.on === "main" ? "main" : ev.branch });
|
|
21761
|
+
var jobKey = (j) => `${j.findingId}\x00${j.about ?? ""}`;
|
|
21397
21762
|
async function readEventPages(events, each, o = {}) {
|
|
21398
21763
|
let next = null;
|
|
21399
21764
|
let more = true;
|
|
@@ -21477,6 +21842,7 @@ async function runWatch(s, d, outer) {
|
|
|
21477
21842
|
const appNameOf = (ev) => ev.app === pageApp ? pageAppName : null;
|
|
21478
21843
|
const titleOf = (ev) => notificationTitle({ app: appNameOf(ev) ?? ev.app, branch: ev.branch });
|
|
21479
21844
|
const investigating = (app) => [...running.values()].some((j) => j.app === app);
|
|
21845
|
+
const busy = (job) => [...running.values(), ...queue].some((q) => q.findingId === job.findingId && (q.about === null || job.about === null || q.about === job.about));
|
|
21480
21846
|
const fixRunning = new Map;
|
|
21481
21847
|
const fixQueue = [];
|
|
21482
21848
|
const autoFixed = new Set;
|
|
@@ -21505,16 +21871,23 @@ async function runWatch(s, d, outer) {
|
|
|
21505
21871
|
await d.open(ev.url).catch((e) => log(`keepsnappy watch: cannot open ${ev.url}: ${e.message}`));
|
|
21506
21872
|
}, (e) => log(`keepsnappy watch: the notification failed: ${e.message}`));
|
|
21507
21873
|
};
|
|
21508
|
-
const later = (job, o) => track(d.notifier.notify({ title: job.title, body: o.text, url: job.url, findingId: null, about: { kind: "ended", findingId: o.findingId, status: o.status, count: o.count, text: o.text, ...o.usage ? { usage: usageJson(o.usage) } : {} } }).then(() => {
|
|
21874
|
+
const later = (job, o) => track(d.notifier.notify({ title: job.title, body: o.text, url: job.url, findingId: null, about: { kind: "ended", findingId: o.findingId, branch: job.about, status: o.status, count: o.count, text: o.text, ...o.usage ? { usage: usageJson(o.usage) } : {} } }).then(() => {
|
|
21509
21875
|
return;
|
|
21510
21876
|
}, (e) => log(`keepsnappy watch: the notification failed: ${e.message}`)));
|
|
21511
21877
|
function start(job, auto) {
|
|
21512
|
-
if (
|
|
21878
|
+
if (busy(job))
|
|
21513
21879
|
return;
|
|
21514
|
-
const
|
|
21515
|
-
|
|
21880
|
+
const on = job.onBranch ? `-${branchHash(job.onBranch)}` : "";
|
|
21881
|
+
const logFile = d.store.logPath(`investigate-${job.findingId}${on}-${d.now()}`);
|
|
21516
21882
|
log(`keepsnappy watch: investigating ${job.findingId} (${job.app} · ${job.branch})${auto ? " [auto]" : ""}`);
|
|
21517
|
-
|
|
21883
|
+
const header = [`${APP_PREFIX} ${job.app}`, ...job.about ? [`${BRANCH_PREFIX} ${job.about}`] : []];
|
|
21884
|
+
const args = [...d.binArgs ?? [], "investigate", ...d.forward, ...auto ? ["--agent", "claude"] : [], ...job.onBranch ? [`--branch=${job.onBranch}`] : [], job.findingId];
|
|
21885
|
+
follow(job, logFile, { auto, resumed: false }, (exited) => d.background.start(d.bin, args, logFile, undefined, header).onExit(exited));
|
|
21886
|
+
}
|
|
21887
|
+
function follow(job, logFile, o, run) {
|
|
21888
|
+
const branch = job.about;
|
|
21889
|
+
running.set(jobKey(job), job);
|
|
21890
|
+
d.json?.emit({ type: "investigation", finding_id: job.findingId, branch, state: "started", auto: o.auto, log: logFile, ...o.resumed ? { resumed: true } : {} });
|
|
21518
21891
|
let steps = 0;
|
|
21519
21892
|
let over = false;
|
|
21520
21893
|
const drain = async () => {
|
|
@@ -21522,7 +21895,7 @@ async function runWatch(s, d, outer) {
|
|
|
21522
21895
|
return;
|
|
21523
21896
|
const found = [...(await d.store.readLog(logFile)).matchAll(STEP_LINE)].filter(isStep);
|
|
21524
21897
|
for (const m of found.slice(steps))
|
|
21525
|
-
d.json.emit({ type: "investigation", finding_id: job.findingId, state: "progress", step: Number(m[1]), of: Number(m[2]), label: m[3] });
|
|
21898
|
+
d.json.emit({ type: "investigation", finding_id: job.findingId, branch, state: "progress", step: Number(m[1]), of: Number(m[2]), label: m[3] });
|
|
21526
21899
|
steps = Math.max(steps, found.length);
|
|
21527
21900
|
};
|
|
21528
21901
|
if (d.json) {
|
|
@@ -21537,10 +21910,10 @@ async function runWatch(s, d, outer) {
|
|
|
21537
21910
|
}
|
|
21538
21911
|
})();
|
|
21539
21912
|
}
|
|
21540
|
-
|
|
21913
|
+
run((code, error) => {
|
|
21541
21914
|
track((async () => {
|
|
21542
21915
|
over = true;
|
|
21543
|
-
running.delete(job
|
|
21916
|
+
running.delete(jobKey(job));
|
|
21544
21917
|
await drain().catch(() => {
|
|
21545
21918
|
return;
|
|
21546
21919
|
});
|
|
@@ -21555,6 +21928,62 @@ async function runWatch(s, d, outer) {
|
|
|
21555
21928
|
})());
|
|
21556
21929
|
});
|
|
21557
21930
|
}
|
|
21931
|
+
async function resume(watches) {
|
|
21932
|
+
if (!d.json || !d.store.investigationLogs)
|
|
21933
|
+
return;
|
|
21934
|
+
const json = d.json;
|
|
21935
|
+
const alive = d.alive ?? processAlive;
|
|
21936
|
+
const bootedAt = d.bootedAt?.() ?? Date.now() - uptime() * 1000;
|
|
21937
|
+
const newest = new Map;
|
|
21938
|
+
for (const logFile of await d.store.investigationLogs()) {
|
|
21939
|
+
const m = INVESTIGATE_LOG.exec(basename4(logFile));
|
|
21940
|
+
if (!m)
|
|
21941
|
+
continue;
|
|
21942
|
+
const at = Number(m[3]);
|
|
21943
|
+
if (at < bootedAt || d.now() - at > RESUME_WINDOW_MS)
|
|
21944
|
+
continue;
|
|
21945
|
+
const key = `${m[1]}-${m[2] ?? ""}`;
|
|
21946
|
+
const seen = newest.get(key);
|
|
21947
|
+
if (!seen || seen.at < at)
|
|
21948
|
+
newest.set(key, { findingId: m[1], hash: m[2], logFile, at });
|
|
21949
|
+
}
|
|
21950
|
+
for (const { findingId, hash, logFile, at } of newest.values()) {
|
|
21951
|
+
const text = await d.store.readLog(logFile);
|
|
21952
|
+
const pid = Number(PID_LINE.exec(text)?.[1] ?? 0);
|
|
21953
|
+
const app = APP_LINE.exec(text)?.[1];
|
|
21954
|
+
if (!pid || !app || !watches(app) || [...text.matchAll(STATUS_LINE)].length > 0 || !alive(pid))
|
|
21955
|
+
continue;
|
|
21956
|
+
const about = BRANCH_LINE.exec(text)?.[1]?.trim() || null;
|
|
21957
|
+
if (hash !== undefined && (about === null || branchHash(about) !== hash))
|
|
21958
|
+
continue;
|
|
21959
|
+
const known = shown.get(findingId);
|
|
21960
|
+
const branch = about ?? known?.branch ?? "";
|
|
21961
|
+
const job = {
|
|
21962
|
+
findingId,
|
|
21963
|
+
app,
|
|
21964
|
+
branch,
|
|
21965
|
+
url: known?.url ?? null,
|
|
21966
|
+
title: known?.title ?? notificationTitle({ app: appNameOf({ app }) ?? app, branch }),
|
|
21967
|
+
about,
|
|
21968
|
+
...hash !== undefined && about !== null ? { onBranch: about } : {}
|
|
21969
|
+
};
|
|
21970
|
+
if (busy(job))
|
|
21971
|
+
continue;
|
|
21972
|
+
follow(job, logFile, { auto: false, resumed: true }, (exited) => void (async () => {
|
|
21973
|
+
while (!signal.aborted) {
|
|
21974
|
+
await json.wait(PROGRESS_POLL_MS, signal);
|
|
21975
|
+
if (signal.aborted)
|
|
21976
|
+
return;
|
|
21977
|
+
if (!alive(pid) || d.now() > at + RESUME_WINDOW_MS)
|
|
21978
|
+
return exited(null);
|
|
21979
|
+
}
|
|
21980
|
+
})());
|
|
21981
|
+
}
|
|
21982
|
+
}
|
|
21983
|
+
const resumeFor = (watches) => {
|
|
21984
|
+
if (!s.once)
|
|
21985
|
+
track(resume(watches).catch((e) => log(`keepsnappy watch: running investigations could not be read: ${e.message}`)));
|
|
21986
|
+
};
|
|
21558
21987
|
let wakeNow = null;
|
|
21559
21988
|
let lastRefreshAt = -Infinity;
|
|
21560
21989
|
let refreshDue = null;
|
|
@@ -21587,49 +22016,65 @@ async function runWatch(s, d, outer) {
|
|
|
21587
22016
|
const id = c.finding_id;
|
|
21588
22017
|
if (typeof id !== "string" || !FINDING_ID.test(id))
|
|
21589
22018
|
return log("keepsnappy watch: investigate needs a finding_id (f_ and 16 hex characters); nothing started");
|
|
21590
|
-
|
|
21591
|
-
if (
|
|
21592
|
-
|
|
21593
|
-
|
|
21594
|
-
|
|
21595
|
-
|
|
21596
|
-
|
|
21597
|
-
|
|
21598
|
-
|
|
21599
|
-
|
|
21600
|
-
|
|
21601
|
-
|
|
21602
|
-
|
|
21603
|
-
|
|
22019
|
+
const onBranch = c.branch;
|
|
22020
|
+
if (onBranch !== undefined && (typeof onBranch !== "string" || onBranch.trim() === ""))
|
|
22021
|
+
return log(`keepsnappy watch: investigate needs a branch name in branch, or no branch; ${id} not started`);
|
|
22022
|
+
const job = await jobOfFinding(id);
|
|
22023
|
+
if (typeof job === "string")
|
|
22024
|
+
return log(job);
|
|
22025
|
+
start(onBranch ? { ...job, branch: onBranch, onBranch, about: onBranch } : job, false);
|
|
22026
|
+
}
|
|
22027
|
+
async function jobOfFinding(id) {
|
|
22028
|
+
const known = shown.get(id);
|
|
22029
|
+
if (known)
|
|
22030
|
+
return { findingId: id, ...known, about: null };
|
|
22031
|
+
let found;
|
|
22032
|
+
try {
|
|
22033
|
+
found = await d.api.finding(id);
|
|
22034
|
+
} catch (e) {
|
|
22035
|
+
return `keepsnappy watch: cannot look up ${id}: ${e.message}; nothing started`;
|
|
21604
22036
|
}
|
|
21605
|
-
|
|
22037
|
+
if (!found)
|
|
22038
|
+
return `keepsnappy watch: ${id} is not a finding the API knows; nothing started`;
|
|
22039
|
+
const app = pageApp ?? s.apps[0];
|
|
22040
|
+
if (!app)
|
|
22041
|
+
return `keepsnappy watch: ${id} is not a result shown by this watcher and the app it belongs to is not known yet; nothing started`;
|
|
22042
|
+
return { findingId: id, app, branch: found.branch, url: found.url, title: notificationTitle({ app: appNameOf({ app }) ?? app, branch: found.branch }), about: null };
|
|
21606
22043
|
}
|
|
21607
22044
|
function fixClick(c) {
|
|
21608
|
-
const
|
|
21609
|
-
|
|
21610
|
-
return none("needs exactly one of pr (a whole number), branch or finding_ids");
|
|
22045
|
+
const clientId = typeof c.client_id === "string" && CLIENT_ID.test(c.client_id) ? c.client_id : null;
|
|
22046
|
+
const sentIds = Array.isArray(c.finding_ids) && c.finding_ids.length <= MAX_ECHOED_IDS && c.finding_ids.every((x) => typeof x === "string" && FINDING_ID.test(x)) ? c.finding_ids : null;
|
|
21611
22047
|
const appOf = (id) => (id ? shown.get(id)?.app : undefined) ?? pageApp ?? s.apps[0] ?? null;
|
|
22048
|
+
const asked = { client_id: clientId, app: appOf(sentIds?.[0]), pr: wholeNumber(c.pr, 1, 2000000000), branch: typeof c.branch === "string" && BRANCH_REF.test(c.branch) && !c.branch.includes("..") ? c.branch : null, finding_ids: sentIds };
|
|
22049
|
+
const none = (reason, why) => {
|
|
22050
|
+
log(`keepsnappy watch: fix ${why}; nothing started`);
|
|
22051
|
+
d.json?.emit({ type: "fix", fix_id: newFixId(`rejected|${clientId}|${reason}`), state: "rejected", ...asked, reason, text: fixRejectText(reason, { count: sentIds?.length ?? 0, max: MAX_FIX_FINDINGS }) });
|
|
22052
|
+
};
|
|
22053
|
+
if ([c.pr, c.branch, c.finding_ids].filter((x) => x !== undefined).length !== 1)
|
|
22054
|
+
return none("bad-command", "needs exactly one of pr (a whole number), branch or finding_ids");
|
|
21612
22055
|
const make = (o) => ({ ...o, fixId: newFixId(o.key), auto: false });
|
|
22056
|
+
const duplicate = () => none("duplicate", "is already running or queued for these findings");
|
|
21613
22057
|
if (c.pr !== undefined || c.branch !== undefined) {
|
|
21614
|
-
const
|
|
21615
|
-
const branch = c.branch !== undefined ? typeof c.branch === "string" && BRANCH_REF.test(c.branch) && !c.branch.includes("..") ? c.branch : null : null;
|
|
22058
|
+
const { pr, branch } = asked;
|
|
21616
22059
|
if (c.pr !== undefined && pr === null)
|
|
21617
|
-
return none("needs a pr that is a whole number of 1 or more");
|
|
22060
|
+
return none("bad-command", "needs a pr that is a whole number of 1 or more");
|
|
21618
22061
|
if (c.branch !== undefined && branch === null)
|
|
21619
|
-
return none("needs a branch name (letters, digits and ._/-, not starting with a dash)");
|
|
22062
|
+
return none("bad-command", "needs a branch name (letters, digits and ._/-, not starting with a dash)");
|
|
21620
22063
|
const owner = appOf();
|
|
21621
22064
|
if (!owner)
|
|
21622
|
-
return none("does not know the app yet");
|
|
21623
|
-
return requestFix(make({ key: pr !== null ? `pr:${pr}` : `branch:${branch}`, app: owner, branch, pr, findingId: null, target: [pr !== null ? String(pr) : branch] }));
|
|
22065
|
+
return none("unknown-app", "does not know the app yet");
|
|
22066
|
+
return requestFix(make({ key: pr !== null ? `pr:${pr}` : `branch:${branch}`, app: owner, branch, pr, findingId: null, target: [pr !== null ? String(pr) : branch] }), duplicate);
|
|
21624
22067
|
}
|
|
22068
|
+
if (sentIds && sentIds.length > MAX_FIX_FINDINGS)
|
|
22069
|
+
return none("too-many", `takes at most ${MAX_FIX_FINDINGS} finding ids, and got ${sentIds.length}`);
|
|
21625
22070
|
const ids = findingIdList(c.finding_ids);
|
|
21626
22071
|
if (!ids)
|
|
21627
|
-
return none(`needs 1 to ${MAX_FIX_FINDINGS} finding ids (f_ and 16 hex characters)`);
|
|
22072
|
+
return none("bad-command", `needs 1 to ${MAX_FIX_FINDINGS} finding ids (f_ and 16 hex characters)`);
|
|
21628
22073
|
const unique = [...new Set(ids)];
|
|
21629
22074
|
const owner = appOf(unique[0]);
|
|
21630
22075
|
if (!owner)
|
|
21631
|
-
return none("does not know the app yet");
|
|
21632
|
-
requestFix(make({ key: `ids:${[...unique].sort().join(",")}`, app: owner, branch: shown.get(unique[0])?.branch ?? null, pr: null, findingId: unique[0], target: unique.flatMap((id) => ["--finding", id]) }));
|
|
22076
|
+
return none("unknown-app", "does not know the app yet");
|
|
22077
|
+
requestFix(make({ key: `ids:${[...unique].sort().join(",")}`, app: owner, branch: shown.get(unique[0])?.branch ?? null, pr: null, findingId: unique[0], target: unique.flatMap((id) => ["--finding", id]) }), duplicate);
|
|
21633
22078
|
}
|
|
21634
22079
|
let fixSeq = 0;
|
|
21635
22080
|
const newFixId = (tag) => `fix-${d.now()}-${createHash2("sha256").update(`${tag}|${d.now()}|${fixSeq++}`).digest("hex").slice(0, 6)}`;
|
|
@@ -21637,9 +22082,9 @@ async function runWatch(s, d, outer) {
|
|
|
21637
22082
|
log(`keepsnappy watch: fix of ${j.pr !== null ? `PR #${j.pr}` : j.branch ?? j.findingId ?? "the findings"} (${j.app}) skipped: ${reason}`);
|
|
21638
22083
|
d.json?.emit({ type: "fix", fix_id: j.fixId ?? newFixId(`skip|${j.app}|${j.pr ?? j.branch}|${reason}`), state: "skipped", app: j.app, pr: j.pr, branch: j.branch, finding_id: j.findingId, reason });
|
|
21639
22084
|
};
|
|
21640
|
-
function requestFix(job) {
|
|
22085
|
+
function requestFix(job, onDuplicate = () => {}) {
|
|
21641
22086
|
if ([...fixRunning.values()].some((j) => j.key === job.key) || fixQueue.some((q) => q.key === job.key))
|
|
21642
|
-
return;
|
|
22087
|
+
return onDuplicate();
|
|
21643
22088
|
if (fixRunning.has(job.app)) {
|
|
21644
22089
|
fixQueue.push(job);
|
|
21645
22090
|
return skipLine(job, "busy");
|
|
@@ -21780,7 +22225,16 @@ async function runWatch(s, d, outer) {
|
|
|
21780
22225
|
}
|
|
21781
22226
|
if (!found || found.state !== "worse" || found.on !== "pr" && found.on !== "main")
|
|
21782
22227
|
return;
|
|
21783
|
-
|
|
22228
|
+
const other = job.onBranch !== undefined && job.onBranch !== found.branch;
|
|
22229
|
+
await autoFix({
|
|
22230
|
+
app: job.app,
|
|
22231
|
+
branch: other ? job.onBranch : found.branch,
|
|
22232
|
+
pr: other ? null : found.pr ?? null,
|
|
22233
|
+
on: other ? "pr" : found.on,
|
|
22234
|
+
findingId: job.findingId,
|
|
22235
|
+
sha: other ? null : found.sha ?? null,
|
|
22236
|
+
open: o.status === "stored" && o.count !== null ? o.count : null
|
|
22237
|
+
});
|
|
21784
22238
|
}
|
|
21785
22239
|
async function outcome(findingId, logFile, code, error, branch) {
|
|
21786
22240
|
if (error)
|
|
@@ -21805,7 +22259,7 @@ async function runWatch(s, d, outer) {
|
|
|
21805
22259
|
return [WATCH_WORDS.autoNotWithCodex];
|
|
21806
22260
|
}
|
|
21807
22261
|
const job = jobOf(ev, ev.finding_id, titleOf(ev));
|
|
21808
|
-
if (
|
|
22262
|
+
if (busy(job))
|
|
21809
22263
|
return [];
|
|
21810
22264
|
if (investigating(ev.app)) {
|
|
21811
22265
|
queue.push(job);
|
|
@@ -21866,6 +22320,8 @@ async function runWatch(s, d, outer) {
|
|
|
21866
22320
|
if (connected)
|
|
21867
22321
|
return;
|
|
21868
22322
|
connected = true;
|
|
22323
|
+
if (s.apps.length === 0)
|
|
22324
|
+
resumeFor((app) => app === page.app);
|
|
21869
22325
|
d.json?.emit({ type: "connected", app: page.app, app_name: page.app_name ?? null, org: page.org_name ?? null });
|
|
21870
22326
|
}
|
|
21871
22327
|
async function fromNow() {
|
|
@@ -21911,6 +22367,8 @@ async function runWatch(s, d, outer) {
|
|
|
21911
22367
|
seen.add(id);
|
|
21912
22368
|
for (const [id, f] of Object.entries(prior?.shown ?? {}))
|
|
21913
22369
|
shown.set(id, f);
|
|
22370
|
+
if (s.apps.length > 0)
|
|
22371
|
+
resumeFor((app) => s.apps.includes(app));
|
|
21914
22372
|
for (const head of prior?.autoFixed ?? [])
|
|
21915
22373
|
autoFixed.add(head);
|
|
21916
22374
|
const header = () => log(`keepsnappy watch: ${s.apps.length ? `apps ${s.apps.join(", ")}` : "every app"}${s.branch ? `, branches ${s.branch}` : ""}${s.mine ? ", my PRs + main" : ", all PRs + main"}, every ${s.intervalSeconds} s, notifications by ${d.notifier.name}${s.autoOnWorse ? ", auto-investigating Worse" : ""}${s.autoFix ? ", auto-fixing PRs" : ""}`);
|
|
@@ -22503,7 +22961,7 @@ class FileRiSource {
|
|
|
22503
22961
|
}
|
|
22504
22962
|
|
|
22505
22963
|
// apps/agent/src/aa.ts
|
|
22506
|
-
import { join as
|
|
22964
|
+
import { join as join20 } from "node:path";
|
|
22507
22965
|
var g = (x, d = 4) => x === undefined || !Number.isFinite(x) ? "-" : Number(x.toPrecision(d)).toString();
|
|
22508
22966
|
var pad = (rows) => {
|
|
22509
22967
|
const w = rows[0]?.map((_, i) => Math.max(...rows.map((r) => (r[i] ?? "").length))) ?? [];
|
|
@@ -22511,7 +22969,7 @@ var pad = (rows) => {
|
|
|
22511
22969
|
`);
|
|
22512
22970
|
};
|
|
22513
22971
|
async function load(deps, outDir) {
|
|
22514
|
-
return readRunDataset(await deps.fs.readFile(
|
|
22972
|
+
return readRunDataset(await deps.fs.readFile(join20(outDir, "metrics.csv")));
|
|
22515
22973
|
}
|
|
22516
22974
|
async function aa(deps, outDir, opts = {}) {
|
|
22517
22975
|
const dataset = await load(deps, outDir);
|
|
@@ -22524,14 +22982,14 @@ async function aa(deps, outDir, opts = {}) {
|
|
|
22524
22982
|
rows.push([r.metric, String(r.pool.n), g(r.pool.median), g(r.pool.robustCv, 3), r.rows.map((x) => `${x.n}:${x.mde === null ? ">max" : g(x.mde, 2)}`).join(" "), r.suggestedMargin.reason]);
|
|
22525
22983
|
}
|
|
22526
22984
|
const table = pad(rows);
|
|
22527
|
-
await deps.fs.writeFile(
|
|
22985
|
+
await deps.fs.writeFile(join20(outDir, "aa.json"), `${JSON.stringify({ kind: "aa", results }, null, 2)}
|
|
22528
22986
|
`);
|
|
22529
22987
|
return { results, table };
|
|
22530
22988
|
}
|
|
22531
22989
|
|
|
22532
22990
|
// apps/agent/src/browserstack.ts
|
|
22533
22991
|
import { randomUUID } from "node:crypto";
|
|
22534
|
-
import { basename as
|
|
22992
|
+
import { basename as basename5, join as join21 } from "node:path";
|
|
22535
22993
|
import { readFile as readFile9 } from "node:fs/promises";
|
|
22536
22994
|
var BASE2 = "https://api-cloud.browserstack.com/app-automate/maestro/v2";
|
|
22537
22995
|
var TERMINAL = new Set(["passed", "failed", "error", "timedout", "skipped", "stopped"]);
|
|
@@ -22662,22 +23120,22 @@ class BrowserStack {
|
|
|
22662
23120
|
if (app.startsWith("bs://") || app.startsWith("custom_id:"))
|
|
22663
23121
|
return app;
|
|
22664
23122
|
const form = new FormData;
|
|
22665
|
-
form.append("file", new Blob([await this.bytes(app)]),
|
|
23123
|
+
form.append("file", new Blob([await this.bytes(app)]), basename5(app));
|
|
22666
23124
|
if (customId)
|
|
22667
23125
|
form.append("custom_id", customId);
|
|
22668
23126
|
const r = await this.call("/app", { method: "POST", form });
|
|
22669
23127
|
if (typeof r.app_url !== "string")
|
|
22670
|
-
throw new FarmApiError(`app upload of ${
|
|
23128
|
+
throw new FarmApiError(`app upload of ${basename5(app)} returned no app_url`);
|
|
22671
23129
|
return r.app_url;
|
|
22672
23130
|
}
|
|
22673
23131
|
async runMaestro(b) {
|
|
22674
23132
|
const appUrl = await this.uploadApp(b.app, b.customId);
|
|
22675
|
-
const work =
|
|
23133
|
+
const work = join21(this.o.tmpDir ?? "/tmp", `keepsnappy-suite-${(this.o.uniqueId ?? randomUUID)()}`);
|
|
22676
23134
|
let suiteUrl;
|
|
22677
23135
|
try {
|
|
22678
23136
|
const zip = await this.zipSuite(b, work);
|
|
22679
23137
|
const form = new FormData;
|
|
22680
|
-
form.append("file", new Blob([await this.bytes(zip)]),
|
|
23138
|
+
form.append("file", new Blob([await this.bytes(zip)]), basename5(zip));
|
|
22681
23139
|
const r = await this.call("/test-suite", { method: "POST", form });
|
|
22682
23140
|
if (typeof r.test_suite_url !== "string")
|
|
22683
23141
|
throw new FarmApiError("suite upload returned no test_suite_url");
|
|
@@ -22705,17 +23163,17 @@ class BrowserStack {
|
|
|
22705
23163
|
return this.poll(buildId, b);
|
|
22706
23164
|
}
|
|
22707
23165
|
async zipSuite(b, work) {
|
|
22708
|
-
const zipPath =
|
|
22709
|
-
const stage =
|
|
23166
|
+
const zipPath = join21(work, "suite.zip");
|
|
23167
|
+
const stage = join21(work, "stage");
|
|
22710
23168
|
await this.deps.fs.mkdir(stage);
|
|
22711
23169
|
const cp = await this.deps.spawner.run("cp", ["-R", b.suiteDir.replace(/\/$/, ""), stage]);
|
|
22712
23170
|
if (cp.code !== 0)
|
|
22713
23171
|
throw new FarmApiError(`cannot copy the suite ${b.suiteDir}: ${cp.stderr || cp.error}`);
|
|
22714
|
-
const folder =
|
|
22715
|
-
if (!await this.deps.fs.exists(
|
|
23172
|
+
const folder = basename5(b.suiteDir.replace(/\/$/, ""));
|
|
23173
|
+
if (!await this.deps.fs.exists(join21(stage, folder, "config.yaml")))
|
|
22716
23174
|
throw new FarmApiError(`${b.suiteDir} has no config.yaml: is that the Maestro workspace?`);
|
|
22717
|
-
const target =
|
|
22718
|
-
await this.deps.fs.mkdir(
|
|
23175
|
+
const target = join21(stage, folder, b.tokensPath);
|
|
23176
|
+
await this.deps.fs.mkdir(join21(target, ".."));
|
|
22719
23177
|
await this.deps.fs.writeFile(target, await this.deps.fs.readFile(b.tokensJs));
|
|
22720
23178
|
const include = SUITE_EXTENSIONS.flatMap((e) => ["-i", `${folder}/*${e}`, "-i", `${folder}/**/*${e}`]);
|
|
22721
23179
|
const z = await this.deps.spawner.run("sh", ["-c", 'cd "$1" && shift && zip -qr "$@"', "sh", stage, zipPath, folder, ...include, "-x", `${folder}/runs/*`, "-x", "*/node_modules/*"]);
|
|
@@ -22774,9 +23232,9 @@ class BrowserStackLocal {
|
|
|
22774
23232
|
const url = TUNNEL_ZIP[this.o.platform ?? process.platform];
|
|
22775
23233
|
if (!url)
|
|
22776
23234
|
throw new FarmApiError(`no BrowserStackLocal binary known for ${this.o.platform ?? process.platform}`);
|
|
22777
|
-
const dir =
|
|
22778
|
-
const bin =
|
|
22779
|
-
const zip =
|
|
23235
|
+
const dir = join21(this.o.dir, identifier);
|
|
23236
|
+
const bin = join21(dir, "BrowserStackLocal");
|
|
23237
|
+
const zip = join21(dir, "bs-local.zip");
|
|
22780
23238
|
await this.deps.fs.mkdir(dir);
|
|
22781
23239
|
for (const [cmd, args] of [
|
|
22782
23240
|
["curl", ["-fsSL", "-o", zip, url]],
|
|
@@ -22855,83 +23313,6 @@ async function maestroCliVersion(deps) {
|
|
|
22855
23313
|
return /^\s*v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\s*$/m.exec(r.stdout)?.[1];
|
|
22856
23314
|
}
|
|
22857
23315
|
|
|
22858
|
-
// apps/agent/src/device-model.ts
|
|
22859
|
-
import { tmpdir as tmpdir3 } from "node:os";
|
|
22860
|
-
import { join as join21 } from "node:path";
|
|
22861
|
-
function parseDevicectlList(json) {
|
|
22862
|
-
let data;
|
|
22863
|
-
try {
|
|
22864
|
-
data = JSON.parse(json);
|
|
22865
|
-
} catch {
|
|
22866
|
-
return [];
|
|
22867
|
-
}
|
|
22868
|
-
const devices = Array.isArray(data.result?.devices) ? data.result.devices : [];
|
|
22869
|
-
return devices.flatMap((d) => {
|
|
22870
|
-
const hw = d.hardwareProperties ?? {};
|
|
22871
|
-
const conn = d.connectionProperties ?? {};
|
|
22872
|
-
if (typeof hw.productType !== "string" || !hw.productType)
|
|
22873
|
-
return [];
|
|
22874
|
-
const text = (v) => typeof v === "string" ? v : "";
|
|
22875
|
-
return [{ productType: hw.productType, udid: text(hw.udid), identifier: text(d.identifier), serial: text(hw.serialNumber), connected: conn.tunnelState === "connected" }];
|
|
22876
|
-
});
|
|
22877
|
-
}
|
|
22878
|
-
var parseSimulators = (json) => {
|
|
22879
|
-
try {
|
|
22880
|
-
const data = JSON.parse(json);
|
|
22881
|
-
return Object.values(data.devices ?? {}).flat();
|
|
22882
|
-
} catch {
|
|
22883
|
-
return [];
|
|
22884
|
-
}
|
|
22885
|
-
};
|
|
22886
|
-
var withSimulatorWord = (name) => /simulator/i.test(name) ? name : `${name} Simulator`;
|
|
22887
|
-
var simulatorModel = (s) => withSimulatorWord(s.name);
|
|
22888
|
-
async function androidModel(deps, id) {
|
|
22889
|
-
const r = await deps.spawner.run("adb", [...id ? ["-s", id] : [], "shell", "getprop", "ro.product.model"]);
|
|
22890
|
-
const model = r.code === 0 ? (r.stdout.split(`
|
|
22891
|
-
`)[0] ?? "").trim() : "";
|
|
22892
|
-
return model || null;
|
|
22893
|
-
}
|
|
22894
|
-
async function simulators(deps, id) {
|
|
22895
|
-
const r = await deps.spawner.run("xcrun", ["simctl", "list", "devices", ...id ? [] : ["booted"], "-j"]);
|
|
22896
|
-
if (r.code !== 0)
|
|
22897
|
-
return [];
|
|
22898
|
-
return parseSimulators(r.stdout).filter((s) => id ? s.udid === id : s.state === "Booted");
|
|
22899
|
-
}
|
|
22900
|
-
async function phones(deps) {
|
|
22901
|
-
const out = join21(tmpdir3(), `keepsnappy-devicectl-${deps.clock.now()}.json`);
|
|
22902
|
-
try {
|
|
22903
|
-
const r = await deps.spawner.run("xcrun", ["devicectl", "list", "devices", "--quiet", "--json-output", out]);
|
|
22904
|
-
const text = r.code === 0 ? await deps.fs.readFile(out).catch(() => "") : "";
|
|
22905
|
-
return parseDevicectlList(text);
|
|
22906
|
-
} finally {
|
|
22907
|
-
await deps.spawner.run("rm", ["-f", out]);
|
|
22908
|
-
}
|
|
22909
|
-
}
|
|
22910
|
-
async function iosModel(deps, id) {
|
|
22911
|
-
const sims = await simulators(deps, id);
|
|
22912
|
-
if (id) {
|
|
22913
|
-
if (sims.length === 1)
|
|
22914
|
-
return simulatorModel(sims[0]);
|
|
22915
|
-
const found = (await phones(deps)).filter((p) => [p.udid, p.identifier, p.serial].some((x) => x.toLowerCase() === id.toLowerCase()));
|
|
22916
|
-
return found.length === 1 ? found[0].productType : null;
|
|
22917
|
-
}
|
|
22918
|
-
const connected = (await phones(deps)).filter((p) => p.connected);
|
|
22919
|
-
if (sims.length + connected.length !== 1)
|
|
22920
|
-
return null;
|
|
22921
|
-
return sims[0] ? simulatorModel(sims[0]) : connected[0].productType;
|
|
22922
|
-
}
|
|
22923
|
-
async function detectDeviceModel(deps, q) {
|
|
22924
|
-
if (q.platform === "android")
|
|
22925
|
-
return androidModel(deps, q.deviceId);
|
|
22926
|
-
if (q.platform === "ios")
|
|
22927
|
-
return iosModel(deps, q.deviceId);
|
|
22928
|
-
return null;
|
|
22929
|
-
}
|
|
22930
|
-
async function detectSimulator(deps, udid) {
|
|
22931
|
-
const sims = await simulators(deps, udid);
|
|
22932
|
-
return sims.length === 1 ? simulatorModel(sims[0]) : null;
|
|
22933
|
-
}
|
|
22934
|
-
|
|
22935
23316
|
// apps/agent/src/links.ts
|
|
22936
23317
|
import { parseArgs as parseArgs7 } from "node:util";
|
|
22937
23318
|
var LINK_COMMANDS = new Set(["activation-link", "flush-link", "gc-link"]);
|
|
@@ -23845,8 +24226,8 @@ async function farmRun(deps, f, rt, ports) {
|
|
|
23845
24226
|
|
|
23846
24227
|
// apps/agent/src/sourcemaps.ts
|
|
23847
24228
|
import { createHash as createHash4, randomUUID as randomUUID2 } from "node:crypto";
|
|
23848
|
-
import { access as access2, readFile as readFile10, readdir as
|
|
23849
|
-
import { basename as
|
|
24229
|
+
import { access as access2, readFile as readFile10, readdir as readdir6, rename as rename3, rm as rm4, writeFile as writeFile7 } from "node:fs/promises";
|
|
24230
|
+
import { basename as basename6, join as join25, resolve as resolve13 } from "node:path";
|
|
23850
24231
|
import { parseArgs as parseArgs8 } from "node:util";
|
|
23851
24232
|
import { gzipSync } from "node:zlib";
|
|
23852
24233
|
var MAX_COMPLETE_ROUNDS = 20;
|
|
@@ -23854,7 +24235,7 @@ var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
|
23854
24235
|
async function prepareComponents(projectRoot, options = {}) {
|
|
23855
24236
|
const root = resolve13(projectRoot);
|
|
23856
24237
|
const directory = join25(root, ".keepsnappy", "components");
|
|
23857
|
-
const configurations = await
|
|
24238
|
+
const configurations = await readdir6(directory, { withFileTypes: true }).catch((error) => {
|
|
23858
24239
|
if (error.code === "ENOENT")
|
|
23859
24240
|
return [];
|
|
23860
24241
|
throw error;
|
|
@@ -23883,7 +24264,7 @@ async function prepareComponents(projectRoot, options = {}) {
|
|
|
23883
24264
|
if (!isComponentConfiguration(config) || config.directory !== selectedConfiguration) {
|
|
23884
24265
|
throw new ConfigError(["invalid KeepSnappy component fragment configuration"]);
|
|
23885
24266
|
}
|
|
23886
|
-
const files = (await
|
|
24267
|
+
const files = (await readdir6(selected)).filter((file) => file.endsWith(".json") && !file.startsWith(".")).sort();
|
|
23887
24268
|
const components = [];
|
|
23888
24269
|
const hashes = new Map;
|
|
23889
24270
|
let matched = 0;
|
|
@@ -23973,7 +24354,7 @@ async function uploadSourcemaps(opts) {
|
|
|
23973
24354
|
const text = Buffer.from(JSON.stringify(map));
|
|
23974
24355
|
const gz = gzipSync(text);
|
|
23975
24356
|
const sha256 = createHash4("sha256").update(gz).digest("hex");
|
|
23976
|
-
const name =
|
|
24357
|
+
const name = basename6(file);
|
|
23977
24358
|
const announced = await call(f, opts.url, `/v1/builds/${encodeURIComponent(opts.build)}/sourcemaps`, headers, {
|
|
23978
24359
|
platform: opts.platform,
|
|
23979
24360
|
name,
|
|
@@ -24354,10 +24735,12 @@ var BRANCHES_USAGE = `keepsnappy branches [--days N] [--sort needs-look|newest]
|
|
|
24354
24735
|
current result and its phones; branches still running or waiting for main last.
|
|
24355
24736
|
--sort needs-look (default: the worst result first) or newest (the newest result first)
|
|
24356
24737
|
--app the app the key must be for (a key reads one app); another app is an error
|
|
24357
|
-
--json one JSON object: {"version":1,"app","app_name","days","sort","generated_at","more","branches":[…]} in the order asked for (the Mac app reads it);
|
|
24738
|
+
--json one JSON object: {"version":1,"app","app_name","days","sort","generated_at","more","branches":[…],"main":[…]} in the order asked for (the Mac app reads it);
|
|
24358
24739
|
more: the window held more pushes than one answer reads, the oldest are left out
|
|
24359
24740
|
each phone: "finding_id" (the finding its result is about, or null) and "hypotheses" ({count, latest_submission_id, latest_at}: what the
|
|
24360
24741
|
ledger holds about that finding, from any runner, or null)
|
|
24742
|
+
main: one row per phone that ran main in the window, its newest session, newest first (the sort does not change it):
|
|
24743
|
+
phone, phone_name, scenario_name, word, state, last_run_at, url, run_set_id, finding_id, hypotheses
|
|
24361
24744
|
--config accepted for the Mac app, which passes it to every command; nothing in it changes the list
|
|
24362
24745
|
(key: KEEPSNAPPY_KEY or --key-env NAME, read or run scope; API: --api URL or KEEPSNAPPY_API)`;
|
|
24363
24746
|
var BRANCHES_COMMANDS = new Set(["branches"]);
|
|
@@ -24383,7 +24766,7 @@ function settingsOf(argv) {
|
|
|
24383
24766
|
return { days, sort, app: o.app?.trim().toLowerCase() || null, json: o.json ?? false, api: o.api ?? null, keyEnv: o["key-env"] ?? "KEEPSNAPPY_KEY", help: o.help ?? false };
|
|
24384
24767
|
}
|
|
24385
24768
|
function branchesJson(page, generatedAt) {
|
|
24386
|
-
return { version: BRANCHES_JSON_VERSION, app: page.app, app_name: page.app_name, days: page.days, sort: page.sort, generated_at: new Date(generatedAt).toISOString(), more: page.more, branches: page.branches };
|
|
24769
|
+
return { version: BRANCHES_JSON_VERSION, app: page.app, app_name: page.app_name, days: page.days, sort: page.sort, generated_at: new Date(generatedAt).toISOString(), more: page.more, branches: page.branches, main: page.main ?? [] };
|
|
24387
24770
|
}
|
|
24388
24771
|
function branchesText(page) {
|
|
24389
24772
|
const n = page.branches.length;
|
|
@@ -24417,7 +24800,7 @@ async function branchesCommand(argv, rt) {
|
|
|
24417
24800
|
var USAGE = `keepsnappy <command>
|
|
24418
24801
|
run [--config keepsnappy.yml] [--repeat N] [--out DIR]
|
|
24419
24802
|
[--upload [--wait] [--fail-on-worse] [--no-notify] [--ingest-url URL] [--commit SHA] [--branch NAME] [--base-commit SHA]]
|
|
24420
|
-
[--device-model NAME --device-os VERSION] [--scenario NAME] [--replay <out dir>[:ARM] ...]
|
|
24803
|
+
[--device UDID] [--device-model NAME --device-os VERSION] [--scenario NAME] [--replay <out dir>[:ARM] ...]
|
|
24421
24804
|
[--no-markers] [-- <e2e command...>]
|
|
24422
24805
|
(--fail-on-worse: exit 3 only when the verdict computed on that run set is Worse and blocking; closing never computes one,
|
|
24423
24806
|
so with no computed verdict it checks nothing: "${UNCHECKED_GATE}", exit 0)
|