pi-gauntlet 5.10.2 → 5.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +7 -3
- package/bin/gauntlet-performance.mjs +283 -0
- package/bin/gauntlet-performance.test.mjs +350 -0
- package/bin/gauntlet-telemetry-salvage.mjs +52 -10
- package/bin/gauntlet-telemetry-salvage.test.mjs +104 -2
- package/extensions/lib/checkout.test.ts +65 -2
- package/extensions/lib/checkout.ts +54 -14
- package/extensions/lib/gauntlet-settings-loader.test.ts +13 -0
- package/extensions/lib/gauntlet-settings-loader.ts +6 -5
- package/extensions/telemetry.test.ts +22 -1
- package/extensions/telemetry.ts +25 -10
- package/package.json +3 -2
- package/skills/finishing-a-development-branch/SKILL.md +1 -1
- package/skills/gatekeep-pr/SKILL.md +3 -2
- package/skills/gauntlet-performance/SKILL.md +43 -0
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
// One checkout resolver for every extension that needs "which git checkout owns this
|
|
2
2
|
// path" (#37): plan_check, the settings loader, Guard 2, telemetry. `--path-format=absolute`
|
|
3
3
|
// (git >= 2.31) makes git-dir and common-dir comparable as strings from any subdirectory.
|
|
4
|
+
// A plain (non-colocated) jj checkout has no .git for rev-parse, so when git fails the
|
|
5
|
+
// resolver falls back to `jj root`; colocated jj repos still resolve through git first.
|
|
6
|
+
// checkoutOfSync is the same resolution for sync callers (the settings loader).
|
|
4
7
|
import { execFileSync } from "node:child_process";
|
|
5
8
|
import { existsSync, statSync } from "node:fs";
|
|
6
|
-
import { dirname } from "node:path";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
7
10
|
|
|
8
11
|
export type GitResult = { code: number; stdout: string };
|
|
9
|
-
|
|
12
|
+
// `via` records which binary resolved the checkout, so callers with git-only follow-ups
|
|
13
|
+
// (telemetry's checkpoint commit) can skip them in a plain jj workspace.
|
|
14
|
+
export type Checkout = { toplevel: string; isPrimary: boolean; via: "git" | "jj" };
|
|
15
|
+
type ResolvedCheckout = { toplevel: string; isPrimary: boolean };
|
|
10
16
|
|
|
11
17
|
export const CHECKOUT_ARGS = ["rev-parse", "--path-format=absolute", "--show-toplevel", "--git-dir", "--git-common-dir"];
|
|
12
18
|
|
|
13
19
|
// isPrimary = gitDir === commonDir (both absolute thanks to --path-format).
|
|
14
|
-
export function parseCheckout(r: GitResult):
|
|
20
|
+
export function parseCheckout(r: GitResult): ResolvedCheckout | undefined {
|
|
15
21
|
if (r.code !== 0) return undefined;
|
|
16
22
|
const lines = r.stdout.trim().split("\n").map((l) => l.trim());
|
|
17
23
|
if (lines.length < 3 || !lines[0]) return undefined;
|
|
@@ -38,20 +44,54 @@ export function nearestExistingDir(absPath: string): string {
|
|
|
38
44
|
return p;
|
|
39
45
|
}
|
|
40
46
|
|
|
47
|
+
export const JJ_ROOT_ARGS = ["root"];
|
|
48
|
+
|
|
49
|
+
// Added jj workspaces keep .jj/repo as a pointer file into the main repo; the primary
|
|
50
|
+
// checkout keeps it as the real directory.
|
|
51
|
+
function parseJjRoot(r: GitResult): ResolvedCheckout | undefined {
|
|
52
|
+
if (r.code !== 0) return undefined;
|
|
53
|
+
const toplevel = r.stdout.trim();
|
|
54
|
+
if (!toplevel) return undefined;
|
|
55
|
+
return { toplevel, isPrimary: isDir(join(toplevel, ".jj", "repo")) };
|
|
56
|
+
}
|
|
57
|
+
|
|
41
58
|
export async function checkoutOf(
|
|
42
59
|
absPath: string,
|
|
43
60
|
git: (args: string[], cwd: string) => GitResult | Promise<GitResult>,
|
|
61
|
+
jj: (args: string[], cwd: string) => GitResult | Promise<GitResult> = jjSync,
|
|
44
62
|
): Promise<Checkout | undefined> {
|
|
45
|
-
|
|
63
|
+
const cwd = nearestExistingDir(absPath);
|
|
64
|
+
const g = parseCheckout(await git(CHECKOUT_ARGS, cwd));
|
|
65
|
+
if (g) return { ...g, via: "git" };
|
|
66
|
+
const j = parseJjRoot(await jj(JJ_ROOT_ARGS, cwd));
|
|
67
|
+
return j ? { ...j, via: "jj" } : undefined;
|
|
46
68
|
}
|
|
47
69
|
|
|
48
|
-
//
|
|
49
|
-
export
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
70
|
+
// Same git-then-jj resolution for sync callers; either side injectable for tests.
|
|
71
|
+
export function checkoutOfSync(
|
|
72
|
+
absPath: string,
|
|
73
|
+
git: (args: string[], cwd: string) => GitResult = gitSync,
|
|
74
|
+
jj: (args: string[], cwd: string) => GitResult = jjSync,
|
|
75
|
+
): Checkout | undefined {
|
|
76
|
+
const cwd = nearestExistingDir(absPath);
|
|
77
|
+
const g = parseCheckout(git(CHECKOUT_ARGS, cwd));
|
|
78
|
+
if (g) return { ...g, via: "git" };
|
|
79
|
+
const j = parseJjRoot(jj(JJ_ROOT_ARGS, cwd));
|
|
80
|
+
return j ? { ...j, via: "jj" } : undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Never throws: any failure (not a repo, missing dir, missing binary) is a nonzero code.
|
|
84
|
+
const commandSync =
|
|
85
|
+
(bin: string) =>
|
|
86
|
+
(args: string[], cwd: string): GitResult => {
|
|
87
|
+
try {
|
|
88
|
+
const stdout = execFileSync(bin, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 });
|
|
89
|
+
return { code: 0, stdout };
|
|
90
|
+
} catch (e) {
|
|
91
|
+
const status = (e as { status?: unknown }).status;
|
|
92
|
+
return { code: typeof status === "number" ? status : 1, stdout: "" };
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const gitSync = commandSync("git");
|
|
97
|
+
export const jjSync = commandSync("jj");
|
|
@@ -30,3 +30,16 @@ test("outside any checkout the loader falls back to cwd", () => {
|
|
|
30
30
|
assert.equal(loaded.root, dir);
|
|
31
31
|
assert.deepEqual(loaded.gauntlet.flowGuards, { enforce: false });
|
|
32
32
|
});
|
|
33
|
+
|
|
34
|
+
test("settings resolve through the jj fallback in a plain jj workspace (no .git)", () => {
|
|
35
|
+
const dir = tmp();
|
|
36
|
+
mkdirSync(join(dir, ".jj", "repo"), { recursive: true }); // primary jj checkout marker
|
|
37
|
+
mkdirSync(join(dir, ".pi"));
|
|
38
|
+
mkdirSync(join(dir, "doc"));
|
|
39
|
+
writeFileSync(join(dir, ".pi", "settings.json"), JSON.stringify({ piGauntlet: { specCouncil: { members: ["p/jj"] } } }));
|
|
40
|
+
const jjCheckout = () => ({ toplevel: dir, isPrimary: true, via: "jj" as const });
|
|
41
|
+
const loaded = loadGauntletSettings(join(dir, "doc"), "/tmp/pi-gauntlet-test-agent", jjCheckout);
|
|
42
|
+
assert.equal(loaded.root, dir);
|
|
43
|
+
assert.deepEqual(loaded.gauntlet.specCouncil, { members: ["p/jj"] });
|
|
44
|
+
assert.deepEqual(loaded.errors, []);
|
|
45
|
+
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// ci.mjs unit tests. Only pi-loaded extensions import this file.
|
|
3
3
|
import { SettingsManager, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { mergeGauntlet, type PiGauntlet } from "./gauntlet-settings.ts";
|
|
5
|
-
import {
|
|
5
|
+
import { checkoutOfSync, type Checkout } from "./checkout.ts";
|
|
6
6
|
|
|
7
7
|
export interface LoadedGauntlet {
|
|
8
8
|
gauntlet: PiGauntlet;
|
|
@@ -12,12 +12,13 @@ export interface LoadedGauntlet {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
// Reads the preset (agentDir/settings.json) and repo (<root>/.pi/settings.json) layers
|
|
15
|
-
// via pi's own SettingsManager, where <root> is the
|
|
16
|
-
//
|
|
15
|
+
// via pi's own SettingsManager, where <root> is the checkout toplevel of cwd (git, falling
|
|
16
|
+
// back to `jj root` in a plain jj workspace) - pi launched in a subdirectory or a linked
|
|
17
|
+
// worktree still finds that checkout's file (#37). SettingsManager
|
|
17
18
|
// never throws on a bad file - it substitutes {} for that layer and records the error,
|
|
18
19
|
// surfaced here via errors[] so callers can report a degraded read instead of failing silent.
|
|
19
|
-
export function loadGauntletSettings(cwd: string, agentDir: string = getAgentDir()): LoadedGauntlet {
|
|
20
|
-
const root =
|
|
20
|
+
export function loadGauntletSettings(cwd: string, agentDir: string = getAgentDir(), checkout: (path: string) => Checkout | undefined = checkoutOfSync): LoadedGauntlet {
|
|
21
|
+
const root = checkout(cwd)?.toplevel ?? cwd;
|
|
21
22
|
const sm = SettingsManager.create(root, agentDir);
|
|
22
23
|
const preset = sm.getGlobalSettings() as { piGauntlet?: Record<string, unknown> };
|
|
23
24
|
const repo = sm.getProjectSettings() as { piGauntlet?: Record<string, unknown> };
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
type Handler = (event: any, ctx: any) => unknown;
|
|
22
22
|
|
|
23
|
-
function harness(o: { branch?: unknown[]; enabled?: boolean; gitFail?: (args: string[], cwd: string) => GitResult | undefined; cwdSub?: string; sessionId?: string; model?: { provider: string; id: string }; thinkingLevel?: string; contextTokens?: number | null; telemetryWarning?: string; telemetryDir?: string } = {}) {
|
|
23
|
+
function harness(o: { branch?: unknown[]; enabled?: boolean; gitFail?: (args: string[], cwd: string) => GitResult | undefined; jjWorkspace?: boolean; cwdSub?: string; sessionId?: string; model?: { provider: string; id: string }; thinkingLevel?: string; contextTokens?: number | null; telemetryWarning?: string; telemetryDir?: string } = {}) {
|
|
24
24
|
const root = mkdtempSync(join(tmpdir(), "telemetry-test-"));
|
|
25
25
|
tempDirs.push(root);
|
|
26
26
|
mkdirSync(join(root, "doc/specs"), { recursive: true });
|
|
@@ -52,6 +52,8 @@
|
|
|
52
52
|
return { code: 0, stdout: "", stderr: "" };
|
|
53
53
|
},
|
|
54
54
|
settings: () => ({ telemetry: { enabled: o.enabled ?? true, dir: o.telemetryDir ?? ".pi/gauntlet/telemetry", buckets: DEFAULT_TELEMETRY_BUCKETS, warning: o.telemetryWarning }, errors: [], agentOverrides: { implementer: { model: "p/x" } }, versions: { pi: "0.85.1" } }),
|
|
55
|
+
// A plain jj workspace: `jj root` answers the temp root while git rev-parse fails.
|
|
56
|
+
...(o.jjWorkspace ? { jj: async () => ({ code: 0, stdout: root + "\n", stderr: "" }) } : {}),
|
|
55
57
|
};
|
|
56
58
|
let branch = o.branch ?? [];
|
|
57
59
|
const ctx = {
|
|
@@ -281,6 +283,25 @@
|
|
|
281
283
|
assert.deepEqual(nogit.readRecord().events.filter((e) => e.kind === "warning").map((e: any) => e.message), [`spec ${outsideSpec} is outside a git checkout; telemetry not recorded`]);
|
|
282
284
|
});
|
|
283
285
|
|
|
286
|
+
test("jj-bound checkout: record is written, never committed, and the warning fires once", async () => {
|
|
287
|
+
const h = harness({
|
|
288
|
+
jjWorkspace: true,
|
|
289
|
+
gitFail: (args) =>
|
|
290
|
+
args[0] === "rev-parse" && args.includes("--path-format=absolute")
|
|
291
|
+
? { code: 128, stdout: "", stderr: "fatal: not a git repository" }
|
|
292
|
+
: undefined,
|
|
293
|
+
});
|
|
294
|
+
await h.phaseResult("start", P({ brainstorm: "in_progress" }));
|
|
295
|
+
await h.writeSpec("doc/specs/a.md");
|
|
296
|
+
await h.phaseResult("complete", P({ brainstorm: "complete" }));
|
|
297
|
+
const rec = h.readRecord();
|
|
298
|
+
assert.deepEqual(
|
|
299
|
+
rec.events.filter((e) => e.kind === "warning").map((e: any) => e.message),
|
|
300
|
+
["record written, not committed: not a git checkout"],
|
|
301
|
+
);
|
|
302
|
+
assert.equal(h.commits().length, 0);
|
|
303
|
+
});
|
|
304
|
+
|
|
284
305
|
test("worktree spec from a primary cwd: record keyed under the worktree, committed there (AC 8)", async () => {
|
|
285
306
|
const h = harness();
|
|
286
307
|
await h.emit("session_start", { type: "session_start", reason: "startup" });
|
package/extensions/telemetry.ts
CHANGED
|
@@ -48,6 +48,8 @@
|
|
|
48
48
|
export interface Deps {
|
|
49
49
|
fs: FsPort;
|
|
50
50
|
git: (args: string[], cwd: string) => Promise<GitResult>;
|
|
51
|
+
// Optional jj override for tests; production resolves plain jj workspaces via jjSync.
|
|
52
|
+
jj?: (args: string[], cwd: string) => GitResult | Promise<GitResult>;
|
|
51
53
|
now: () => string;
|
|
52
54
|
settings: (cwd: string) => SettingsSnapshot;
|
|
53
55
|
}
|
|
@@ -160,6 +162,7 @@
|
|
|
160
162
|
export default function (pi: ExtensionAPI, deps: Deps = realDeps) {
|
|
161
163
|
let phases: PhaseMap = emptyPhases();
|
|
162
164
|
let toplevel: string | undefined; // toplevel of the bound record's checkout; undefined until bind
|
|
165
|
+
let checkoutVia: "git" | "jj" | undefined; // which binary resolved toplevel; undefined until bind
|
|
163
166
|
let boundSpec: string | undefined; // repo-relative
|
|
164
167
|
let record: TelemetryRecord | undefined;
|
|
165
168
|
const predecessorLinks = new Set<string>();
|
|
@@ -170,6 +173,7 @@
|
|
|
170
173
|
let lastCommitted = "";
|
|
171
174
|
let frozen = false;
|
|
172
175
|
let settingsWarned = false;
|
|
176
|
+
let notGitWarned = false;
|
|
173
177
|
let sessionId = "";
|
|
174
178
|
|
|
175
179
|
// Per-event settings read; undefined => this event is a no-op.
|
|
@@ -182,11 +186,11 @@
|
|
|
182
186
|
|
|
183
187
|
// Resolves a tool path (relative to ctx.cwd or absolute) to its owning checkout and
|
|
184
188
|
// checkout-relative key. undefined outside any checkout.
|
|
185
|
-
const locate = async (ctx: ExtensionContext, p: string): Promise<{ toplevel: string; rel: string } | undefined> => {
|
|
189
|
+
const locate = async (ctx: ExtensionContext, p: string): Promise<{ toplevel: string; rel: string; via: "git" | "jj" } | undefined> => {
|
|
186
190
|
const candidateAbs = isAbsolute(p) ? p : resolve(ctx.cwd, p);
|
|
187
|
-
const co = await checkoutOf(candidateAbs, deps.git);
|
|
191
|
+
const co = await checkoutOf(candidateAbs, deps.git, deps.jj);
|
|
188
192
|
const rel = co ? repoRelativeToolPath(co.toplevel, ctx.cwd, p) : undefined;
|
|
189
|
-
return co && rel ? { toplevel: co.toplevel, rel } : undefined;
|
|
193
|
+
return co && rel ? { toplevel: co.toplevel, rel, via: co.via } : undefined;
|
|
190
194
|
};
|
|
191
195
|
|
|
192
196
|
const live = (): Accumulators => record
|
|
@@ -230,6 +234,15 @@
|
|
|
230
234
|
|
|
231
235
|
const commitRecord = async () => {
|
|
232
236
|
if (!record || !recordRel || frozen || !toplevel) return;
|
|
237
|
+
if (checkoutVia === "jj") {
|
|
238
|
+
// A jj-bound checkout has no .git to commit into; say so once per session instead
|
|
239
|
+
// of failing `git commit` at every checkpoint.
|
|
240
|
+
if (!notGitWarned) {
|
|
241
|
+
notGitWarned = true;
|
|
242
|
+
warn("record written, not committed: not a git checkout");
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
233
246
|
const text = deps.fs.readFile(abs(recordRel)) ?? "";
|
|
234
247
|
if (text === lastCommitted) return;
|
|
235
248
|
const paths = [recordRel, ...(oldRecordRel ? [oldRecordRel] : [])];
|
|
@@ -274,8 +287,9 @@
|
|
|
274
287
|
return newRecord({ spec: specRel, session: sessionId, now: buffer[0]?.ts ?? deps.now(), runId: randomUUID() });
|
|
275
288
|
};
|
|
276
289
|
|
|
277
|
-
const bind = async (specRel: string, snap: SettingsSnapshot, specToplevel: string) => {
|
|
290
|
+
const bind = async (specRel: string, snap: SettingsSnapshot, specToplevel: string, via: "git" | "jj") => {
|
|
278
291
|
toplevel = specToplevel;
|
|
292
|
+
checkoutVia = via;
|
|
279
293
|
currentDir = snap.telemetry.dir;
|
|
280
294
|
boundSpec = specRel;
|
|
281
295
|
recordRel = recordPathFor(currentDir, specRel);
|
|
@@ -314,6 +328,7 @@
|
|
|
314
328
|
|
|
315
329
|
const unbind = () => {
|
|
316
330
|
toplevel = undefined;
|
|
331
|
+
checkoutVia = undefined;
|
|
317
332
|
boundSpec = undefined;
|
|
318
333
|
record = undefined;
|
|
319
334
|
recordRel = undefined;
|
|
@@ -416,7 +431,7 @@
|
|
|
416
431
|
const candidate = replay.planCheckSpec ?? replay.lastSpecWrite;
|
|
417
432
|
const loc = candidate ? await locate(ctx, candidate) : undefined;
|
|
418
433
|
if (loc && isSpecPath(loc.rel)) {
|
|
419
|
-
await bind(loc.rel, snap, loc.toplevel);
|
|
434
|
+
await bind(loc.rel, snap, loc.toplevel, loc.via);
|
|
420
435
|
await flush(false);
|
|
421
436
|
}
|
|
422
437
|
});
|
|
@@ -450,7 +465,7 @@
|
|
|
450
465
|
const pass = d?.status === "pass";
|
|
451
466
|
if (pass && specLoc && (specLoc.rel !== boundSpec || specLoc.toplevel !== toplevel)) {
|
|
452
467
|
if (record) unbind();
|
|
453
|
-
await bind(specLoc.rel, snap, specLoc.toplevel);
|
|
468
|
+
await bind(specLoc.rel, snap, specLoc.toplevel, specLoc.via);
|
|
454
469
|
}
|
|
455
470
|
live().gates.plan_rounds += 1;
|
|
456
471
|
pushEvent({ kind: "plan_check", pass, spec, plan });
|
|
@@ -498,11 +513,11 @@
|
|
|
498
513
|
});
|
|
499
514
|
|
|
500
515
|
// Binding + spec-write bookkeeping; the bound-spec hooks are defined in block 2.
|
|
501
|
-
const onSpecInteraction = async (tool: "write" | "edit" | "read", loc: { toplevel: string; rel: string }, event: { input: unknown; content: unknown[] }, snap: SettingsSnapshot): Promise<unknown> => {
|
|
516
|
+
const onSpecInteraction = async (tool: "write" | "edit" | "read", loc: { toplevel: string; rel: string; via: "git" | "jj" }, event: { input: unknown; content: unknown[] }, snap: SettingsSnapshot): Promise<unknown> => {
|
|
502
517
|
const rel = loc.rel;
|
|
503
518
|
if (!boundSpec) {
|
|
504
519
|
if ((tool === "write" || tool === "edit") && isSpecPath(rel)) {
|
|
505
|
-
await bind(rel, snap, loc.toplevel);
|
|
520
|
+
await bind(rel, snap, loc.toplevel, loc.via);
|
|
506
521
|
const patch = onBoundSpecWrite(tool, rel, event);
|
|
507
522
|
await flush(true);
|
|
508
523
|
return patch;
|
|
@@ -510,7 +525,7 @@
|
|
|
510
525
|
const spec = planSpecHeader(deps.fs.readFile(join(loc.toplevel, rel)) ?? "");
|
|
511
526
|
const specRel = spec ? repoRelativeToolPath(loc.toplevel, loc.toplevel, spec) : undefined;
|
|
512
527
|
if (specRel && deps.fs.exists(join(loc.toplevel, specRel))) {
|
|
513
|
-
await bind(specRel, snap, loc.toplevel);
|
|
528
|
+
await bind(specRel, snap, loc.toplevel, loc.via);
|
|
514
529
|
await flush(true);
|
|
515
530
|
}
|
|
516
531
|
}
|
|
@@ -520,7 +535,7 @@
|
|
|
520
535
|
// Another checkout: a fresh run, never a rename of this record.
|
|
521
536
|
if ((tool === "write" || tool === "edit") && isSpecPath(rel)) {
|
|
522
537
|
unbind();
|
|
523
|
-
await bind(rel, snap, loc.toplevel);
|
|
538
|
+
await bind(rel, snap, loc.toplevel, loc.via);
|
|
524
539
|
const patch = onBoundSpecWrite(tool, rel, event);
|
|
525
540
|
await flush(true);
|
|
526
541
|
return patch;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-gauntlet",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.12.0",
|
|
4
4
|
"description": "Opinionated, gated workflow skills, subagent personas, and runtime extensions for the pi coding agent.",
|
|
5
5
|
"author": "Jacek Juraszek",
|
|
6
6
|
"type": "module",
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
],
|
|
34
34
|
"bin": {
|
|
35
35
|
"gauntlet-spec-index": "bin/gauntlet-spec-index.mjs",
|
|
36
|
-
"gauntlet-telemetry-salvage": "bin/gauntlet-telemetry-salvage.mjs"
|
|
36
|
+
"gauntlet-telemetry-salvage": "bin/gauntlet-telemetry-salvage.mjs",
|
|
37
|
+
"gauntlet-performance": "bin/gauntlet-performance.mjs"
|
|
37
38
|
},
|
|
38
39
|
"engines": {
|
|
39
40
|
"node": ">=24.15.0"
|
|
@@ -177,7 +177,7 @@ fi
|
|
|
177
177
|
node <bin>/gauntlet-telemetry-salvage.mjs --worktree "$WORKTREE" --base <base-branch>
|
|
178
178
|
```
|
|
179
179
|
|
|
180
|
-
The salvage prints one line per spec on the branch (`present`, `restored <path> from <sha>`, `no telemetry run`, `never written`, `restore failed <path>: <reason>`) and always exits 0. Print its stdout verbatim in the ship completion message. A `restore failed` line is reported, never retried, and never blocks the ship - the record stays recoverable from the branch ref.
|
|
180
|
+
The salvage prints one line per spec on the branch (`present`, `present <path> (marked shipped)`, `restored <path> from <sha>`, `restored <path> from <sha> (marked shipped)`, `no telemetry run`, `never written`, `restore failed <path>: <reason>`) and always exits 0. `(marked shipped)` means the record was still `in_progress` with no ship phase (the recorder lost its binding) and the salvage committed `status: shipped` + `shipped_at` as one `telemetry:` commit; it rides the squash or push like any branch commit. Print its stdout verbatim in the ship completion message. A `restore failed` line is reported, never retried, and never blocks the ship - the record stays recoverable from the branch ref.
|
|
181
181
|
|
|
182
182
|
#### Option 1: Squash-merge to base
|
|
183
183
|
|
|
@@ -174,6 +174,7 @@ verification command may write to the tree while the Reviewer reads it):
|
|
|
174
174
|
squash. On a cell with no push row (fork overlay, report-only states) the same
|
|
175
175
|
finding is a non-blocking follow-up instead: the record stays recoverable from the
|
|
176
176
|
PR head ref after merge, and blocking would stop a ship the gate cannot repair.
|
|
177
|
+
`unfinished <path>` (record still `in_progress` with no ship phase) also lands in `## Evidence` as one line and is non-blocking: pre-landing `in_progress` is normal, and the merge course's salvage run stamps it.
|
|
177
178
|
- **Evidence:** On the CI path, list each satisfying check's name, conclusion, assessed SHA, and run URL - there is no command or raw_tail to paste. On the local path, paste each run's `command` and `raw_tail` verbatim, fenced - never
|
|
178
179
|
paraphrased. Any authored summary is labeled as a summary and never substitutes for
|
|
179
180
|
`raw_tail`.
|
|
@@ -256,7 +257,7 @@ are never bundled into one selection, with one scoped exception: the selected me
|
|
|
256
257
|
course first runs `node <bin>/gauntlet-telemetry-salvage.mjs --worktree <provisioned
|
|
257
258
|
path> --base origin/<baseRefName>` (no `--check`). `present` -> merge as-is. `restored
|
|
258
259
|
<path> from <sha>` -> push that single `telemetry: restore` commit as part of this
|
|
259
|
-
course, re-fetch `headRefOid`, and pass the new SHA to `--match-head-commit`. `restore
|
|
260
|
+
course, re-fetch `headRefOid`, and pass the new SHA to `--match-head-commit`. A line ending `(marked shipped)` (`present` or `restored`) is handled the same way: push that single `telemetry:` commit, re-fetch `headRefOid`, pass the new SHA. `restore
|
|
260
261
|
failed` -> merge proceeds, the reason is printed, and the follow-up names recovery
|
|
261
262
|
from the PR head ref.
|
|
262
263
|
|
|
@@ -508,7 +509,7 @@ The menu is a state machine, not a one-shot report:
|
|
|
508
509
|
reviewed doc edits selected alongside them (one commit, or one per batch
|
|
509
510
|
sequentially; subjects name the fixes) - then re-resolves the evidence for the new head **once** (the brief's stale-head row: prior evidence is stale; the local command executes only on a fallback/opt-out resolution). Before the push, run
|
|
510
511
|
`node <bin>/gauntlet-telemetry-salvage.mjs --worktree <provisioned path> --base
|
|
511
|
-
origin/<baseRefName>` (no `--check`); a `restored` commit rides the wave's single
|
|
512
|
+
origin/<baseRefName>` (no `--check`); a `restored` or `(marked shipped)` commit rides the wave's single
|
|
512
513
|
push and the pushed SHA becomes the assessed head under the course's-own-push rule
|
|
513
514
|
in step 1. Print its stdout in the re-rendered report's `## Evidence`. **On
|
|
514
515
|
green**, push **once**; gate and push are per-wave invariants, never per-fix or
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gauntlet-performance
|
|
3
|
+
description: Use when a human asks how gauntlet runs perform across the recorded telemetry - explicit invocation only (/skill:gauntlet-performance [--dir <path>]... [--since <version>]).
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Gauntlet Performance
|
|
8
|
+
|
|
9
|
+
The CLI parses and aggregates; you reason. Never open a telemetry YAML record yourself.
|
|
10
|
+
|
|
11
|
+
## Run the digest
|
|
12
|
+
|
|
13
|
+
1. Resolve the CLI: `<directory of this SKILL.md>/../../bin/gauntlet-performance.mjs`.
|
|
14
|
+
2. Run it from the repo root, passing the user's `--dir` and `--since` arguments verbatim:
|
|
15
|
+
`node <bin>/gauntlet-performance.mjs [--dir <path>]... [--since <version>]`.
|
|
16
|
+
Default corpus is the current repo; each `--dir` adds a repo root or a telemetry dir.
|
|
17
|
+
3. If the command is missing or the output contains `no records found`, relay that line and stop - no menu.
|
|
18
|
+
|
|
19
|
+
## Read the digest
|
|
20
|
+
|
|
21
|
+
`runs` has one row per record; `shipped*` marks a truncated run (no ship phase recorded - salvage stamped it at landing; its `wall` is `-` and it feeds no aggregate). `by version` groups by pi-gauntlet version: `n` counts every row, `shipped` counts the rows behind the p50/max columns. `grants` is `fix_round_grants` - the fix-round proxy (human-granted extra review rounds); schema 1 has no code-review round count.
|
|
22
|
+
|
|
23
|
+
## Reply - exactly this, in this order
|
|
24
|
+
|
|
25
|
+
1. **Recommendation** (2-4 sentences). One claim, led by the run that exemplifies it: quote its `spec` slug, `run_id`, and the 1-3 numbers that carry the claim. When `grants` is the evidence, call it the fix-round proxy. If no version group has `shipped >= 2`, the recommendation is "sample too small" with the `n`/`shipped` counts per version.
|
|
26
|
+
2. **Cornerstones**: 3-5 bullets of aggregate facts from `by version` - corpus size, truncated count, the p50s and model tallies that moved between versions.
|
|
27
|
+
3. **Menu**, numbered, at most 3 items, rendered exactly as:
|
|
28
|
+
- `1. render report` - ask for a target path; write markdown there: the digest verbatim, then the recommendation and cornerstones above. If the file exists, ask before overwriting. Write nothing unless this item is chosen.
|
|
29
|
+
- `2. open recommendation as ticket` - hand the claim and its numbers to `/skill:shape-ticket`; never create a ticket directly.
|
|
30
|
+
- `3. drill into <slug>` - re-run the CLI with `--json` and show that run's fields.
|
|
31
|
+
|
|
32
|
+
Nothing else: no preamble, no restated digest, no file written before item 1 is chosen.
|
|
33
|
+
|
|
34
|
+
## Project overrides
|
|
35
|
+
|
|
36
|
+
If a gauntlet overrides file exists - checked in order:
|
|
37
|
+
`.pi/gauntlet-overrides.md`, `<repo root>/gauntlet-overrides.md`,
|
|
38
|
+
`<repo root>/doc/gauntlet-overrides.md`; first found wins - read it. Any
|
|
39
|
+
sections relevant to this skill - by name match, by topic (routing,
|
|
40
|
+
verification, worktrees, etc.), or by workflow convention - override or
|
|
41
|
+
extend the instructions above. Project-local `AGENTS.md` is already in
|
|
42
|
+
context - check it for project-specific routing tables, service paths, and
|
|
43
|
+
verification commands.
|