pi-gauntlet 5.18.7 → 5.20.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.
@@ -1,273 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/bins/gauntlet-telemetry-salvage.mjs
4
- import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5
- import { homedir } from "node:os";
6
- import { isAbsolute, join } from "node:path";
7
- import { spawnSync } from "node:child_process";
8
- import process from "node:process";
9
-
10
- // extensions/lib/gauntlet-settings.ts
11
- import path from "node:path";
12
- function mergeGauntlet(preset, repo) {
13
- return { ...preset ?? {}, ...repo ?? {} };
14
- }
15
- var nonEmptyString = (v) => typeof v === "string" && v.trim().length > 0;
16
- var joinWarn = (ws) => ws.length ? ws.join("; ") : void 0;
17
- var DEFAULT_TELEMETRY_DIR = ".pi/gauntlet/telemetry";
18
- var DEFAULT_TELEMETRY_BUCKETS = [
19
- ["test", ["**/test/**", "**/tests/**", "**/__tests__/**", "**/*.test.*", "**/*.spec.*", "**/*_test.*"]],
20
- ["docs", ["**/*.md"]],
21
- ["config", ["**/*.json", "**/*.yaml", "**/*.yml", "**/*.toml", "**/*.lock", "**/*-lock.*"]]
22
- ];
23
- function resolveTelemetry(g) {
24
- const t = g.telemetry;
25
- const warnings = [];
26
- const enabled = t?.enabled !== false;
27
- let dir = DEFAULT_TELEMETRY_DIR;
28
- if (t?.dir !== void 0) {
29
- const value = nonEmptyString(t.dir) ? t.dir.trim().replace(/\/+$/, "") : "";
30
- const canonical = value.replace(/\\/g, "/");
31
- const normalized = path.posix.normalize(canonical);
32
- if (value && path.win32.parse(canonical).root === "" && normalized !== ".." && !normalized.startsWith("../")) dir = normalized;
33
- else warnings.push("telemetry.dir must be a non-empty path relative to the git toplevel; using the default");
34
- }
35
- let buckets = DEFAULT_TELEMETRY_BUCKETS;
36
- if (t?.buckets !== void 0) {
37
- const b = t.buckets;
38
- const valid = b !== null && typeof b === "object" && !Array.isArray(b) && Object.keys(b).length > 0 && Object.values(b).every((v) => Array.isArray(v) && v.length > 0 && v.every(nonEmptyString));
39
- if (valid) buckets = Object.entries(b).map(([name, globs]) => [name, [...globs]]);
40
- else warnings.push("telemetry.buckets is not an object of non-empty glob arrays; using the defaults");
41
- }
42
- return { enabled, dir, buckets, warning: joinWarn(warnings) };
43
- }
44
-
45
- // extensions/lib/phase-tracker-helpers.ts
46
- var STMT_START = "(?:^|[\\n;&|(])\\s*";
47
-
48
- // extensions/lib/telemetry-paths.ts
49
- var isSpecPath = (rel) => /(^|\/)doc\/specs\/[^/]+\.md$/.test(rel);
50
- var recordPathFor = (dir, specRel) => `${dir}/${specRel.replace(/\.md$/, ".yaml")}`;
51
- var GIT_FLAGS = "git\\s+(?:-\\S+(?:\\s+\\S+)?\\s+)*";
52
- var SHIP_RE = new RegExp(STMT_START + "(" + GIT_FLAGS + "(?:merge\\s+--squash|push)(?=\\s|$)|gh\\s+pr\\s+create)");
53
- var DISCARD_RE = new RegExp(STMT_START + "(" + GIT_FLAGS + "(?:worktree\\s+remove|branch\\s+-D)(?=\\s|$))");
54
- var SQUASH_RE = new RegExp("^" + GIT_FLAGS + "merge\\s+--squash");
55
-
56
- // extensions/lib/telemetry-ship.ts
57
- var BASE_REFS = ["origin/HEAD", "main", "master"];
58
-
59
- // extensions/lib/telemetry-record.ts
60
- import { Document, isCollection, isMap, isSeq, parse as parseYaml, stringify as stringifyYaml } from "yaml";
61
- var emptyAccumulators = () => ({ phases: {}, personas: {}, reviews: {}, spec_writes: {}, gates: { spec_rounds: 0, plan_rounds: 0, fix_round_grants: 0, task_reopens: 0 }, conformance_loops: 0, amendments: 0, spec_edits_after_ship: 0, events_dropped: 0 });
62
- function newRecord(o) {
63
- return { schema: 1, spec: o.spec, run_id: o.runId, branch: o.branch, status: "in_progress", created_at: o.now, sessions: [o.session], derived: { duration_s: 0, phases: {}, personas: {}, conformance_loops: 0, gates: { spec_rounds: 0, plan_rounds: 0, fix_round_grants: 0, task_reopens: 0 }, amendments: 0, spec_edits_after_ship: 0, spec_writes: {}, events_dropped: 0 }, accumulators: {}, events: [] };
64
- }
65
- var stripUndefined = (v) => {
66
- if (Array.isArray(v)) return v.map(stripUndefined);
67
- if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).filter(([, x]) => x !== void 0).map(([k, x]) => [k, stripUndefined(x)]));
68
- return v;
69
- };
70
- var flowLeafChildren = (map, blockLists = /* @__PURE__ */ new Set()) => {
71
- for (const pair of map.items) {
72
- const key = String(pair.key);
73
- const child = pair.value;
74
- if (!isCollection(child) || blockLists.has(key)) continue;
75
- if (isMap(child) && child.items.length > 0 && child.items.every((item) => isMap(item.value))) {
76
- for (const item of child.items) if (isCollection(item.value)) item.value.flow = true;
77
- } else {
78
- child.flow = true;
79
- }
80
- }
81
- };
82
- var flowModelLists = (node) => {
83
- if (isMap(node)) {
84
- for (const pair of node.items) {
85
- if (String(pair.key) === "models" && isSeq(pair.value)) pair.value.flow = true;
86
- flowModelLists(pair.value);
87
- }
88
- } else if (isSeq(node)) {
89
- for (const item of node.items) flowModelLists(item);
90
- }
91
- };
92
- function serializeRecord(rec) {
93
- const { derived, accumulators, events, ...head } = rec;
94
- const body = stripUndefined({ ...head, derived, accumulators });
95
- const doc = new Document(body);
96
- const derivedNode = doc.get("derived", true);
97
- if (isMap(derivedNode)) flowLeafChildren(derivedNode, /* @__PURE__ */ new Set(["modified_files"]));
98
- const accumulatorNode = doc.get("accumulators", true);
99
- if (isMap(accumulatorNode)) {
100
- for (const session of accumulatorNode.items) if (isMap(session.value)) flowLeafChildren(session.value);
101
- }
102
- flowModelLists(doc.contents);
103
- const bodyText = doc.toString({ lineWidth: 0 });
104
- const eventLines = events.map((e) => " - " + stringifyYaml(stripUndefined(e), { collectionStyle: "flow", lineWidth: 0 }).trim());
105
- return bodyText + "events:\n" + (eventLines.length ? eventLines.join("\n") + "\n" : "");
106
- }
107
- function parseRecord(text) {
108
- let doc;
109
- try {
110
- doc = parseYaml(text);
111
- } catch {
112
- return void 0;
113
- }
114
- if (!doc || typeof doc !== "object") return void 0;
115
- const d = doc;
116
- if (d.schema !== 1 || typeof d.spec !== "string" || typeof d.run_id !== "string") return void 0;
117
- const accumulators = Object.fromEntries(Object.entries(d.accumulators ?? {}).flatMap(
118
- ([session, block]) => block && typeof block === "object" && !Array.isArray(block) ? [[session, { ...emptyAccumulators(), ...block }]] : []
119
- ));
120
- return { ...d, sessions: Array.isArray(d.sessions) ? d.sessions : [], derived: d.derived ?? newRecord({ spec: d.spec, session: "", now: d.created_at ?? "", runId: d.run_id }).derived, accumulators, events: Array.isArray(d.events) ? d.events : [] };
121
- }
122
-
123
- // src/bins/gauntlet-telemetry-salvage.mjs
124
- var GIT_TIMEOUT_MS = 1e4;
125
- var COMMIT_TIMEOUT_MS = Number(process.env.GAUNTLET_SALVAGE_COMMIT_TIMEOUT_MS) || 3e4;
126
- var GIT_ENV = { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_EDITOR: "true" };
127
- var usage = () => {
128
- process.stderr.write("usage: gauntlet-telemetry-salvage --worktree <abs path> [--base <ref>] [--dir <telemetry dir>] [--check]\n");
129
- process.exit(1);
130
- };
131
- function parseArgs(argv) {
132
- const opts = { worktree: void 0, base: void 0, dir: void 0, check: false };
133
- for (let i = 0; i < argv.length; i++) {
134
- const a = argv[i];
135
- if (a === "--worktree" && argv[i + 1] !== void 0 && !argv[i + 1].startsWith("--")) opts.worktree = argv[++i];
136
- else if (a === "--base" && argv[i + 1] !== void 0 && !argv[i + 1].startsWith("--")) opts.base = argv[++i];
137
- else if (a === "--dir" && argv[i + 1] !== void 0 && !argv[i + 1].startsWith("--")) opts.dir = argv[++i];
138
- else if (a === "--check") opts.check = true;
139
- else usage();
140
- }
141
- if (!opts.worktree || !isAbsolute(opts.worktree)) usage();
142
- return opts;
143
- }
144
- function git(cwd, args, timeout = GIT_TIMEOUT_MS) {
145
- const r = spawnSync("git", args, { cwd, encoding: "utf8", env: GIT_ENV, timeout });
146
- const timedOut = r.error?.code === "ETIMEDOUT";
147
- const stderr = timedOut ? "timed out" : (r.stderr ?? "").trim().split("\n")[0] || r.error?.message || `git exited ${r.status}`;
148
- return { ok: !timedOut && r.status === 0, stdout: (r.stdout ?? "").trim(), stderr, timedOut };
149
- }
150
- function readLayer(file) {
151
- if (!existsSync(file)) return {};
152
- try {
153
- return JSON.parse(readFileSync(file, "utf8"));
154
- } catch (e) {
155
- process.stderr.write(`warning: ${file}: ${e.message}; using {} for this layer
156
- `);
157
- return {};
158
- }
159
- }
160
- function telemetrySettings(root) {
161
- const agentDir = process.env.PI_CODING_AGENT_DIR || join(homedir(), ".pi", "agent");
162
- const preset = readLayer(join(agentDir, "settings.json"));
163
- const repo = readLayer(join(root, ".pi", "settings.json"));
164
- return resolveTelemetry(mergeGauntlet(preset?.piGauntlet, repo?.piGauntlet));
165
- }
166
- function resolveBase(root, explicit) {
167
- for (const ref of explicit ? [explicit] : BASE_REFS) {
168
- if (git(root, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]).ok) return ref;
169
- }
170
- return void 0;
171
- }
172
- var unfinished = (rec) => {
173
- const phases = rec?.derived?.phases;
174
- return rec?.status === "in_progress" && (phases === null || typeof phases !== "object" || !("ship" in phases));
175
- };
176
- var stamped = (rec, now) => serializeRecord({ ...rec, status: "shipped", shipped_at: now });
177
- function stampPresent(root, rec, check) {
178
- const line = `present ${rec}`;
179
- const abs = join(root, rec);
180
- if (!existsSync(abs) || !git(root, ["diff", "--quiet", "HEAD", "--", rec]).ok || !git(root, ["diff", "--quiet", "--cached", "HEAD", "--", rec]).ok) return line;
181
- const parsed = parseRecord(readFileSync(abs, "utf8"));
182
- if (!unfinished(parsed)) return line;
183
- if (check) return `unfinished ${rec}`;
184
- const prior = readFileSync(abs);
185
- const failed = (reason) => {
186
- writeFileSync(abs, prior);
187
- git(root, ["reset", "-q", "--", rec]);
188
- return `restore failed ${rec}: ${reason}`;
189
- };
190
- writeFileSync(abs, stamped(parsed, (/* @__PURE__ */ new Date()).toISOString()));
191
- const add = git(root, ["add", "-f", "--", rec]);
192
- if (!add.ok) return failed(add.stderr);
193
- const commit = git(root, ["commit", "-q", "-m", `telemetry: mark ${rec} shipped at landing`, "--", rec], COMMIT_TIMEOUT_MS);
194
- if (!commit.ok) return failed(commit.stderr);
195
- return `${line} (marked shipped)`;
196
- }
197
- function salvage(root, rec, base, check) {
198
- const presence = git(root, ["cat-file", "-e", `HEAD:${rec}`]);
199
- if (presence.timedOut) return `restore failed ${rec}: timed out`;
200
- if (presence.ok) return stampPresent(root, rec, check);
201
- const history = git(root, ["log", `${base}..HEAD`, "--grep=^telemetry: ", "-1", "--format=%H"]);
202
- if (history.timedOut) return `restore failed ${rec}: timed out`;
203
- if (!history.stdout) return `no telemetry run ${rec}`;
204
- const deletion = git(root, ["log", "-1", "--diff-filter=D", "--format=%H", `${base}..HEAD`, "--", rec]);
205
- if (deletion.timedOut) return `restore failed ${rec}: timed out`;
206
- const del = deletion.stdout;
207
- if (!del) return `never written ${rec}`;
208
- if (check) return `stripped ${rec} in ${del}`;
209
- const abs = join(root, rec);
210
- const onDisk = existsSync(abs);
211
- const preStaged = git(root, ["ls-files", "--", rec]).stdout !== "";
212
- if (preStaged && (!onDisk || !git(root, ["diff", "--quiet", "--", rec]).ok)) {
213
- return `restore failed ${rec}: staged copy differs from worktree`;
214
- }
215
- let created = false;
216
- let stagedByScript = false;
217
- let priorBytes;
218
- const rollback = () => {
219
- if (priorBytes !== void 0) {
220
- writeFileSync(abs, priorBytes);
221
- if (preStaged) git(root, ["add", "-f", "--", rec]);
222
- }
223
- if (stagedByScript) git(root, ["reset", "-q", "--", rec]);
224
- if (created) rmSync(abs, { force: true });
225
- };
226
- const failed = (reason) => {
227
- rollback();
228
- return `restore failed ${rec}: ${reason}`;
229
- };
230
- if (!onDisk) {
231
- const co = git(root, ["checkout", `${del}^`, "--", rec]);
232
- if (!co.ok) return `restore failed ${rec}: ${co.stderr}`;
233
- created = true;
234
- }
235
- stagedByScript = !preStaged;
236
- let suffix = "";
237
- const parsed = parseRecord(readFileSync(abs, "utf8"));
238
- if (unfinished(parsed)) {
239
- priorBytes = readFileSync(abs);
240
- writeFileSync(abs, stamped(parsed, (/* @__PURE__ */ new Date()).toISOString()));
241
- suffix = " (marked shipped)";
242
- }
243
- const add = git(root, ["add", "-f", "--", rec]);
244
- if (!add.ok) return failed(add.stderr);
245
- const commit = git(root, ["commit", "-q", "-m", `telemetry: restore record stripped in ${del.slice(0, 10)}`, "--", rec], COMMIT_TIMEOUT_MS);
246
- if (!commit.ok) return failed(commit.stderr);
247
- return `restored ${rec} from ${del}${suffix}`;
248
- }
249
- function main() {
250
- const opts = parseArgs(process.argv.slice(2));
251
- const override = opts.dir === void 0 ? void 0 : resolveTelemetry({ telemetry: { enabled: true, dir: opts.dir } });
252
- if (override?.warning) usage();
253
- const top = git(opts.worktree, ["rev-parse", "--show-toplevel"]);
254
- if (!existsSync(opts.worktree) || !top.ok) {
255
- process.stderr.write(`not a git worktree: ${opts.worktree}
256
- `);
257
- return;
258
- }
259
- const root = top.stdout;
260
- const telemetry = telemetrySettings(root);
261
- if (telemetry.warning) process.stderr.write(`warning: ${telemetry.warning}
262
- `);
263
- if (!telemetry.enabled) return console.log("telemetry disabled");
264
- const dir = override?.dir ?? telemetry.dir;
265
- const base = resolveBase(root, opts.base);
266
- if (!base) return console.log("no base ref");
267
- const diff = git(root, ["diff", "--name-only", `${base}...HEAD`]);
268
- if (!diff.ok) return console.log("no base ref");
269
- const specs = diff.stdout.split("\n").filter((f) => f && isSpecPath(f) && git(root, ["cat-file", "-e", `HEAD:${f}`]).ok);
270
- if (specs.length === 0) return console.log("no spec on branch");
271
- for (const spec of specs) console.log(salvage(root, recordPathFor(dir, spec), base, opts.check));
272
- }
273
- main();