pi-gauntlet 8.3.0 → 8.4.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +3 -3
  3. package/bin/gauntlet-performance.mjs +477 -64
  4. package/bin/gauntlet-performance.test.mjs +27 -27
  5. package/bin/gauntlet-spec-index.mjs +290 -47
  6. package/bin/gauntlet-spec-index.test.mjs +42 -18
  7. package/bin/gauntlet-telemetry-seal.mjs +220 -77
  8. package/bin/gauntlet-telemetry-seal.test.mjs +222 -457
  9. package/extensions/lib/gauntlet-settings.test.ts +12 -14
  10. package/extensions/lib/gauntlet-settings.ts +7 -16
  11. package/extensions/lib/telemetry-branch.test.ts +102 -0
  12. package/extensions/lib/telemetry-branch.ts +145 -0
  13. package/extensions/lib/telemetry-diff.test.ts +2 -2
  14. package/extensions/lib/telemetry-diff.ts +1 -1
  15. package/extensions/lib/telemetry-leg.ts +163 -0
  16. package/extensions/lib/telemetry-record.ts +2 -0
  17. package/extensions/lib/vcs.ts +6 -23
  18. package/extensions/telemetry-leg-failures.test.ts +138 -0
  19. package/extensions/telemetry-leg-fields.test.ts +130 -0
  20. package/extensions/telemetry-leg-no-write.test.ts +90 -0
  21. package/extensions/telemetry-leg-rounds.test.ts +157 -0
  22. package/extensions/telemetry-leg-trackers.test.ts +114 -0
  23. package/extensions/telemetry-leg.test.ts +151 -0
  24. package/extensions/telemetry-settlement-contract.test.ts +67 -0
  25. package/extensions/telemetry.test.ts +44 -27
  26. package/extensions/telemetry.ts +68 -48
  27. package/extensions/test-support/git-env.mjs +13 -0
  28. package/package.json +1 -1
  29. package/skills/brainstorming/SKILL.md +1 -1
  30. package/skills/check-delivery/SKILL.md +2 -0
  31. package/skills/finishing-a-development-branch/SKILL.md +13 -13
  32. package/skills/finishing-a-development-branch/reference/verification-set.md +2 -2
  33. package/skills/gatekeep-pr/reference/batch.md +3 -1
  34. package/skills/gatekeep-pr/reference/cleanup.md +1 -1
  35. package/skills/gatekeep-pr/reference/post-selection-loop.md +7 -1
  36. package/skills/gauntlet-resume/reference/seed.md +1 -6
  37. package/skills/shape-ticket/SKILL.md +1 -1
  38. package/skills/shape-ticket/reference/repo-audit.md +2 -2
@@ -1,16 +1,18 @@
1
1
  import { parse as parseYaml } from "yaml";
2
2
  import { test } from "node:test";
3
3
  import assert from "node:assert/strict";
4
- import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "node:fs";
4
+ import { mkdtempSync, mkdirSync, writeFileSync, realpathSync, rmSync } from "node:fs";
5
5
  import { join, dirname } from "node:path";
6
6
  import { tmpdir } from "node:os";
7
7
  import { spawnSync } from "node:child_process";
8
8
  import { fileURLToPath } from "node:url";
9
+ import { isolatedGitEnv } from "../extensions/test-support/git-env.mjs";
9
10
 
10
11
  const CLI = join(dirname(fileURLToPath(import.meta.url)), "gauntlet-performance.mjs");
11
12
  const TDIR = ".pi/gauntlet/telemetry/doc/specs";
12
13
  const EMPTY_AGENT = mkdtempSync(join(tmpdir(), "gp-empty-agent-"));
13
14
  process.on("exit", () => rmSync(EMPTY_AGENT, { recursive: true, force: true }));
15
+ const GIT_ENV = isolatedGitEnv(EMPTY_AGENT);
14
16
 
15
17
  const write = (root, rel, text) => {
16
18
  mkdirSync(join(root, dirname(rel)), { recursive: true });
@@ -18,13 +20,13 @@ const write = (root, rel, text) => {
18
20
  };
19
21
  const gitRepo = (prefix = "gp-") => {
20
22
  const root = mkdtempSync(join(tmpdir(), prefix));
21
- spawnSync("git", ["init", "-q"], { cwd: root });
23
+ spawnSync("git", ["init", "-q"], { cwd: root, env: { ...process.env, ...GIT_ENV } });
22
24
  return root;
23
25
  };
24
26
  const SOURCE_CLI = join(dirname(CLI), "../src/bins/gauntlet-performance.mjs");
25
27
  const invoke = (cwd, args = [], env = {}, cli = CLI) => {
26
- const r = spawnSync(process.execPath, [cli, ...args], {
27
- cwd, encoding: "utf8", env: { ...process.env, PI_CODING_AGENT_DIR: EMPTY_AGENT, ...env },
28
+ const r = spawnSync(process.execPath, [cli, ...(cli.endsWith("gauntlet-performance.mjs") ? ["--dir", cwd] : []), ...args], {
29
+ cwd, encoding: "utf8", env: { ...process.env, ...GIT_ENV, PI_CODING_AGENT_DIR: EMPTY_AGENT, ...env },
28
30
  });
29
31
  return { status: r.status, stdout: r.stdout, stderr: r.stderr.trim(), lines: r.stdout.split("\n").filter(Boolean) };
30
32
  };
@@ -159,7 +161,7 @@ test("per-run fields: typed reads, unphased excluded, truncated wall null, findi
159
161
  assert.deepEqual(d.corpus, { [root.split("/").pop()]: 8 });
160
162
  assert.equal(d.since, null);
161
163
  const a = byId(d.runs, "aaaaaaaa");
162
- assert.equal(a.repo, root.split("/").pop());
164
+ assert.equal(a.repo, "unknown repo", "legacy schema-1 records need no repo field");
163
165
  assert.equal(a.spec, "a");
164
166
  assert.equal(a.version, "5.9.1");
165
167
  assert.equal(a.status, "shipped");
@@ -329,8 +331,8 @@ test("text output: header, shipped* marker, - cells, by version block, skipped l
329
331
  const aRow = r.lines.find((l) => l.startsWith("aaaaaaaa"));
330
332
  assert.match(aRow, /\sshipped\s+waves\s+60m\s+10\/10\/10\/10\/10\s+6\.6k\s+2\.25\s+m\/alpha,m\/beta\s+7\s+n\/a\s+no\s+1\s+0\s+2\s+1\/2\/3\s+4\s+-$/);
331
333
  assert.ok(r.lines.includes("by version"));
332
- const v = r.lines.find((l) => l.startsWith("5.9.1"));
333
- assert.match(v, /^5\.9\.1\s+4\s+3\s+1\s+60m\/120m\s/);
334
+ const v = r.lines.find((l) => /^unknown repo\s+5\.9\.1\s/.test(l));
335
+ assert.match(v, /^unknown repo\s+5\.9\.1\s+4\s+3\s+1\s+60m\/120m\s/);
334
336
  assert.ok(r.lines.some((l) => /^skipped: .*broken\.yaml: unparseable$/.test(l)));
335
337
  });
336
338
 
@@ -349,30 +351,29 @@ test("--since numeric compare: 5.10.0 > 5.9.1, unknown and malformed versions dr
349
351
  const r = invoke(root, ["--since", "9.9.9"]);
350
352
  assert.equal(r.status, 0);
351
353
  assert.equal(r.lines[0], `corpus: ${root.split("/").pop()}=0 since: 9.9.9`);
352
- assert.match(r.lines[1], /^no records found in .*\.pi\/gauntlet\/telemetry$/);
354
+ assert.equal(r.lines[1], `no records found in ${realpathSync(root)}`);
353
355
  assert.equal(r.lines.length, 2);
354
356
  });
355
357
 
356
- test("--dir: repo root with telemetry dir, repo root without (skipped, checkout not walked), bare telemetry dir; duplicate run_id collapses", (t) => {
358
+ test("--dir reads bare directories without fetching and collapses duplicate run IDs", (t) => {
357
359
  const root = corpus(gitRepo());
358
360
  const other = gitRepo("gp-other-");
359
361
  write(other, `${TDIR}/dup.yaml`, COMPLETE);
360
362
  write(other, `${TDIR}/h.yaml`, COMPLETE.replace("run_id: aaaaaaaa-0000-0000-0000-000000000001", "run_id: 88888888-0000-0000-0000-000000000008"));
361
363
  const empty = gitRepo("gp-empty-");
362
- write(empty, "doc/specs/decoy.yaml", COMPLETE.replace("run_id: aaaaaaaa-0000-0000-0000-000000000001", "run_id: 77777777-0000-0000-0000-000000000007"));
364
+ write(empty, "README.md", "not telemetry\n");
363
365
  const bare = mkdtempSync(join(tmpdir(), "gp-bare-"));
364
366
  write(bare, "x.yaml", COMPLETE.replace("run_id: aaaaaaaa-0000-0000-0000-000000000001", "run_id: 66666666-0000-0000-0000-000000000006"));
365
367
  cleanup(t, root, other, empty, bare);
366
368
  const d = json(root, ["--dir", other, "--dir", empty, "--dir", bare, "--dir", join(bare, "missing")]);
367
369
  assert.equal(d.corpus[other.split("/").pop()], 1);
368
370
  assert.equal(d.corpus[bare.split("/").pop()], 1);
369
- assert.equal(d.corpus[empty.split("/").pop()], undefined);
371
+ assert.equal(d.corpus[empty.split("/").pop()], 0);
370
372
  assert.ok(d.runs.some((r) => r.run_id.startsWith("88888888")));
371
373
  assert.ok(d.runs.some((r) => r.run_id.startsWith("66666666")));
372
- assert.ok(!d.runs.some((r) => r.run_id.startsWith("77777777")));
373
374
  assert.equal(d.runs.filter((r) => r.run_id.startsWith("aaaaaaaa")).length, 1);
374
375
  assert.ok(d.skipped.some((s) => s.file.endsWith("dup.yaml") && s.reason === "duplicate run_id"));
375
- assert.ok(d.skipped.some((s) => s.file === empty && s.reason === "no telemetry dir"));
376
+ assert.equal(invoke(root, ["--dir", bare]).stderr, "", "explicit directories do not fetch, even without origin");
376
377
  assert.ok(d.skipped.some((s) => s.file === join(bare, "missing") && s.reason === "not found"));
377
378
  });
378
379
 
@@ -387,14 +388,6 @@ test("--dir skips an existing regular file as not a directory", (t) => {
387
388
  assert.ok(d.skipped.some((s) => s.file === file && s.reason === "not a directory"));
388
389
  });
389
390
 
390
- test("repo .pi/settings.json telemetry.dir is honoured for the default corpus", (t) => {
391
- const root = gitRepo(); cleanup(t, root);
392
- write(root, ".pi/settings.json", JSON.stringify({ piGauntlet: { telemetry: { dir: "custom/dir" } } }));
393
- write(root, "custom/dir/doc/specs/a.yaml", COMPLETE);
394
- const d = json(root);
395
- assert.equal(d.runs.length, 1);
396
- });
397
-
398
391
  test("usage error exits 1", (t) => {
399
392
  const root = gitRepo(); cleanup(t, root);
400
393
  assert.equal(invoke(root, ["--bogus"]).status, 1);
@@ -474,8 +467,9 @@ test("council section: rosters, sums, chairs and flags", (t) => {
474
467
  const at = r.lines.indexOf("council");
475
468
  assert.ok(at > 0);
476
469
  assert.equal(r.lines[at + 1], "severity is the chair's consolidated severity (highest any co-raiser assigned), not the member's own");
477
- assert.equal(r.lines[at + 2], "roster (2 runs): p/alpha:xhigh, p/delta:high");
478
- assert.match(r.lines[at + 3], /^ member\s+total\s+unique\s+applied\s+uniq_appl\s+deferred\s+rejected\s+uniq_appl_nonminor\/run$/);
470
+ assert.equal(r.lines[at + 2], "repo: unknown repo");
471
+ assert.equal(r.lines[at + 3], "roster (2 runs): p/alpha:xhigh, p/delta:high");
472
+ assert.match(r.lines[at + 4], /^ member\s+total\s+unique\s+applied\s+uniq_appl\s+deferred\s+rejected\s+uniq_appl_nonminor\/run$/);
479
473
  assert.ok(r.lines.includes(" not enough runs to assess (2 of 5)"));
480
474
  assert.ok(r.lines.includes("roster (6 runs): p/alpha:xhigh, p/beta:high, p/gamma:high, p/zero:high"));
481
475
  assert.match(r.lines.find((l) => /^ p\/alpha:xhigh\s+6\/18\/6\s/.test(l)), /^ p\/alpha:xhigh\s+6\/18\/6\s+0\/6\/6\s+6\/18\/0\s+0\/6\/0\s+0\/0\/0\s+0\/0\/6\s+1\.00$/);
@@ -568,7 +562,7 @@ test("shipped seal preserves verification runs and plan totals for performance",
568
562
  const root = gitRepo(); cleanup(t, root);
569
563
  const git = (args) => {
570
564
  const result = spawnSync("git", ["-c", "user.name=t", "-c", "user.email=t@t",
571
- "-c", "commit.gpgsign=false", ...args], { cwd: root, encoding: "utf8" });
565
+ "-c", "commit.gpgsign=false", ...args], { cwd: root, encoding: "utf8", env: { ...process.env, ...GIT_ENV } });
572
566
  assert.equal(result.status, 0, result.stderr);
573
567
  return result.stdout.trim();
574
568
  };
@@ -577,7 +571,9 @@ test("shipped seal preserves verification runs and plan totals for performance",
577
571
  const base = git(["rev-parse", "HEAD"]);
578
572
  write(root, "doc/specs/seal.md", "# Seal\n");
579
573
  git(["add", "doc/specs/seal.md"]); git(["commit", "-qm", "Spec"]);
580
- const path = `${TDIR}/seal.yaml`;
574
+ git(["remote", "add", "origin", "https://example.test/team/product.git"]);
575
+ git(["config", `url.${root}.insteadOf`, "https://example.test/team/product.git"]);
576
+ const path = ".git/gauntlet/telemetry/doc/specs/seal.yaml";
581
577
  write(root, path, JSON.stringify({
582
578
  schema: 1, spec: "doc/specs/seal.md", run_id: "seal", status: "in_progress",
583
579
  created_at: "2026-09-01T00:00:00Z", sessions: ["s"], accumulators: {},
@@ -589,9 +585,13 @@ test("shipped seal preserves verification runs and plan totals for performance",
589
585
  GIT_AUTHOR_NAME: "t", GIT_AUTHOR_EMAIL: "t@t", GIT_COMMITTER_NAME: "t", GIT_COMMITTER_EMAIL: "t@t",
590
586
  }, join(dirname(CLI), "gauntlet-telemetry-seal.mjs"));
591
587
  assert.equal(result.status, 0, result.stderr);
592
- const sealed = parseYaml(readFileSync(join(root, path), "utf8"));
588
+ const sealed = parseYaml(git(["show", "gauntlet/telemetry:team/product/brainstorming/doc/specs/seal.yaml"]));
593
589
  assert.equal(sealed.status, "shipped");
594
590
  assert.equal(sealed.derived.verification_runs, 2);
595
591
  assert.deepEqual(sealed.derived.plan, { tasks: 2, complete: 1, skipped: 1, failed: 0 });
596
- assert.equal(json(root).runs[0].dispatches_per_task, 1);
592
+ const output = spawnSync(process.execPath, [CLI, "--json"], {
593
+ cwd: root, encoding: "utf8", env: { ...process.env, ...GIT_ENV, PI_CODING_AGENT_DIR: EMPTY_AGENT },
594
+ });
595
+ assert.equal(output.status, 0, output.stderr);
596
+ assert.equal(JSON.parse(output.stdout).runs[0].dispatches_per_task, 1);
597
597
  });
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/bins/gauntlet-spec-index.mjs
4
- import { readFileSync, appendFileSync, existsSync, statSync, lstatSync, readdirSync, mkdirSync, rmSync, realpathSync, openSync, readSync, closeSync } from "node:fs";
5
- import { join, dirname, basename, isAbsolute, matchesGlob } from "node:path";
4
+ import { readFileSync as readFileSync2, appendFileSync, existsSync as existsSync3, statSync as statSync2, lstatSync, readdirSync, mkdirSync, rmSync as rmSync2, realpathSync, openSync, readSync, closeSync } from "node:fs";
5
+ import { join as join3, dirname as dirname2, basename as basename2, isAbsolute, matchesGlob } from "node:path";
6
6
  import { homedir } from "node:os";
7
- import { execFileSync } from "node:child_process";
8
- import process from "node:process";
7
+ import { execFileSync as execFileSync2 } from "node:child_process";
8
+ import process2 from "node:process";
9
9
  import { parse as parseYaml } from "yaml";
10
10
 
11
11
  // extensions/lib/gauntlet-settings.ts
@@ -13,6 +13,7 @@ function mergeGauntlet(preset, repo) {
13
13
  return { ...preset ?? {}, ...repo ?? {} };
14
14
  }
15
15
  var nonEmptyString = (v) => typeof v === "string" && v.trim().length > 0;
16
+ var joinWarn = (ws) => ws.length ? ws.join("; ") : void 0;
16
17
  var DEFAULT_SPEC_DIRS = ["doc/specs", "docs/specs"];
17
18
  var normalizeDir = (d) => d.trim().replace(/^\.\//, "").replace(/\/+$/, "");
18
19
  function resolveFlowGuards(g) {
@@ -31,6 +32,232 @@ function planDirsFor(specDirs) {
31
32
  }
32
33
  return [...out];
33
34
  }
35
+ var DEFAULT_TELEMETRY_BUCKETS = [
36
+ ["test", ["**/test/**", "**/tests/**", "**/__tests__/**", "**/*.test.*", "**/*.spec.*", "**/*_test.*"]],
37
+ ["docs", ["**/*.md"]],
38
+ ["config", ["**/*.json", "**/*.yaml", "**/*.yml", "**/*.toml", "**/*.lock", "**/*-lock.*"]]
39
+ ];
40
+ function resolveTelemetry(g) {
41
+ const t = g.telemetry;
42
+ const warnings = [];
43
+ const enabled = t?.enabled !== false;
44
+ if (t && Object.hasOwn(t, "dir")) {
45
+ warnings.push("piGauntlet.telemetry.dir is retired; records live on the gauntlet/telemetry branch - set piGauntlet.telemetry.remote to redirect them");
46
+ }
47
+ let buckets = DEFAULT_TELEMETRY_BUCKETS;
48
+ if (t?.buckets !== void 0) {
49
+ const b = t.buckets;
50
+ 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));
51
+ if (valid) buckets = Object.entries(b).map(([name, globs]) => [name, [...globs]]);
52
+ else warnings.push("telemetry.buckets is not an object of non-empty glob arrays; using the defaults");
53
+ }
54
+ const remote = nonEmptyString(t?.remote) ? t.remote.trim() : void 0;
55
+ if (t?.remote !== void 0 && !remote) warnings.push("telemetry.remote must be a non-empty URL; using origin");
56
+ return { enabled, ...remote ? { remote } : {}, buckets, warning: joinWarn(warnings) };
57
+ }
58
+
59
+ // extensions/lib/telemetry-branch.ts
60
+ import { existsSync as existsSync2, mkdtempSync, readFileSync, rmSync } from "node:fs";
61
+ import { tmpdir } from "node:os";
62
+ import { basename, join as join2 } from "node:path";
63
+
64
+ // extensions/lib/checkout.ts
65
+ import { execFileSync } from "node:child_process";
66
+ import { existsSync, statSync } from "node:fs";
67
+ import { dirname, join } from "node:path";
68
+ var CHECKOUT_ARGS = ["rev-parse", "--path-format=absolute", "--show-toplevel", "--git-dir", "--git-common-dir"];
69
+ function parseCheckout(r) {
70
+ if (r.code !== 0) return void 0;
71
+ const lines = r.stdout.trim().split("\n").map((l) => l.trim());
72
+ if (lines.length < 3 || !lines[0]) return void 0;
73
+ return { toplevel: lines[0], isPrimary: lines[1] === lines[2] };
74
+ }
75
+ var isDir = (p) => {
76
+ try {
77
+ return statSync(p).isDirectory();
78
+ } catch {
79
+ return false;
80
+ }
81
+ };
82
+ function nearestExistingDir(absPath) {
83
+ let p = absPath;
84
+ while (!(existsSync(p) && isDir(p))) {
85
+ const parent = dirname(p);
86
+ if (parent === p) return p;
87
+ p = parent;
88
+ }
89
+ return p;
90
+ }
91
+ var JJ_ROOT_ARGS = ["root"];
92
+ function parseJjRoot(r) {
93
+ if (r.code !== 0) return void 0;
94
+ const toplevel = r.stdout.trim();
95
+ if (!toplevel) return void 0;
96
+ return { toplevel, isPrimary: isDir(join(toplevel, ".jj", "repo")) };
97
+ }
98
+ function checkoutOfSync(absPath, git2 = gitSync, jj = jjSync) {
99
+ const cwd = nearestExistingDir(absPath);
100
+ const g = parseCheckout(git2(CHECKOUT_ARGS, cwd));
101
+ if (g) return { ...g, via: "git" };
102
+ const j = parseJjRoot(jj(JJ_ROOT_ARGS, cwd));
103
+ return j ? { ...j, via: "jj" } : void 0;
104
+ }
105
+ var commandSync = (bin) => (args, cwd) => {
106
+ try {
107
+ const stdout = execFileSync(bin, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5e3 });
108
+ return { code: 0, stdout };
109
+ } catch (e) {
110
+ const status = e.status;
111
+ return { code: typeof status === "number" ? status : 1, stdout: "" };
112
+ }
113
+ };
114
+ var gitSync = commandSync("git");
115
+ var jjSync = commandSync("jj");
116
+
117
+ // extensions/lib/vcs.ts
118
+ import { spawnSync } from "node:child_process";
119
+
120
+ // extensions/lib/phase-tracker-helpers.ts
121
+ var STMT_START = "(?:^|[\\n;&|(])\\s*";
122
+
123
+ // extensions/lib/telemetry-paths.ts
124
+ var GIT_FLAGS = "git\\s+(?:-\\S+(?:\\s+\\S+)?\\s+)*";
125
+ var SHIP_RE = new RegExp(STMT_START + "(" + GIT_FLAGS + "(?:merge\\s+--squash|push)(?=\\s|$)|gh\\s+pr\\s+create)");
126
+ var SQUASH_RE = new RegExp("^" + GIT_FLAGS + "merge\\s+--squash");
127
+
128
+ // extensions/lib/telemetry-diff.ts
129
+ var JJ_MAINLINE = "coalesce(trunk() ~ root(), present(main), present(master))";
130
+ var JJ_FORK_POINT = `fork_point(${JJ_MAINLINE} | @) ~ root() & ::${JJ_MAINLINE}`;
131
+
132
+ // extensions/lib/vcs.ts
133
+ var NO_PROMPT_ENV = { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_EDITOR: "true" };
134
+ var runBinary = (bin) => (args, cwd, timeoutMs = 1e4, options = {}) => {
135
+ const r = spawnSync(bin, args, { cwd, encoding: "utf8", input: options.input, env: { ...NO_PROMPT_ENV, ...options.env }, timeout: timeoutMs, maxBuffer: 64 * 1024 * 1024 });
136
+ const err = r.error;
137
+ const timedOut = err?.code === "ETIMEDOUT";
138
+ const stderr = timedOut ? "timed out" : (r.stderr ?? "").trim().split("\n")[0] || err?.message || `${bin} exited ${r.status}`;
139
+ return { ok: !timedOut && r.status === 0, code: timedOut ? 1 : r.status ?? 1, stdout: options.raw ? r.stdout ?? "" : (r.stdout ?? "").trim(), stderr };
140
+ };
141
+ var gitRun = runBinary("git");
142
+
143
+ // extensions/lib/telemetry-branch.ts
144
+ var TELEMETRY_REF = "refs/heads/gauntlet/telemetry";
145
+ function telemetryRepo(root) {
146
+ const result = gitRun(["config", "--get", "remote.origin.url"], root);
147
+ const match = result.stdout.match(/^(?:https?:\/\/[^/]+\/|ssh:\/\/(?:[^@/]+@)?[^/]+\/|(?:[^@\s]+@)?[^:\s/]+:)(.+)$/);
148
+ const path = match?.[1].replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
149
+ if (result.ok && path) return path;
150
+ const top = checkoutOfSync(root)?.toplevel ?? root;
151
+ for (const file of [".pi/gauntlet-overrides.md", "gauntlet-overrides.md", "doc/gauntlet-overrides.md"]) {
152
+ const path2 = join2(top, file);
153
+ if (!existsSync2(path2)) continue;
154
+ const section = readFileSync(path2, "utf8").split(/^##\s+Issue tracker\s*$/m)[1]?.split(/^##\s/m)[0] ?? "";
155
+ const repo = /^-[\t ]*repo:[\t ]*(\S[^\r\n]*)$/m.exec(section)?.[1].trim();
156
+ if (repo) return repo;
157
+ break;
158
+ }
159
+ const worktrees = gitRun(["worktree", "list", "--porcelain", "-z"], root);
160
+ const primary = worktrees.ok ? /^worktree ([^\0]+)/.exec(worktrees.stdout)?.[1] : void 0;
161
+ return `local/${basename(primary ?? top)}`;
162
+ }
163
+ var trackingRef = (remote) => `refs/remotes/${remote === "origin" ? "origin" : "telemetry"}/gauntlet/telemetry`;
164
+ var fetchBranch = (root, remote) => gitRun(["fetch", "--no-tags", remote, `+${TELEMETRY_REF}:${trackingRef(remote)}`], root);
165
+ function syncTelemetryBranch(root, records, remote = "origin") {
166
+ const scratch = mkdtempSync(join2(tmpdir(), "gauntlet-telemetry-index-"));
167
+ const env = { GIT_INDEX_FILE: join2(scratch, "index") };
168
+ const git2 = (args, input) => {
169
+ const result = gitRun(args, root, void 0, { env, input });
170
+ if (!result.ok) throw new Error(result.stderr);
171
+ return result.stdout;
172
+ };
173
+ const tracking = trackingRef(remote);
174
+ let fetchWarning;
175
+ const fetch = () => {
176
+ const result = fetchBranch(root, remote);
177
+ fetchWarning = result.ok ? void 0 : `fetch failed: ${result.stderr}`;
178
+ return result;
179
+ };
180
+ const resolve = (ref) => {
181
+ const result = gitRun(["rev-parse", "--verify", ref], root);
182
+ return result.ok ? result.stdout : void 0;
183
+ };
184
+ const commit = (tree, parent, message) => git2(["-c", "commit.gpgsign=false", "commit-tree", tree, ...parent ? ["-p", parent] : [], "-m", message]);
185
+ let committed = false;
186
+ try {
187
+ let tip = resolve(TELEMETRY_REF);
188
+ committed = records.size === 0 && !!tip;
189
+ fetch();
190
+ let upstream = resolve(tracking);
191
+ if (records.size) {
192
+ const parent = tip ?? upstream;
193
+ git2(parent ? ["read-tree", parent] : ["read-tree", "--empty"]);
194
+ for (const [path, content] of records) {
195
+ const blob = git2(["hash-object", "-w", "--stdin"], content);
196
+ git2(["update-index", "--add", "--cacheinfo", "100644", blob, path]);
197
+ }
198
+ const tree = git2(["write-tree"]);
199
+ const next = parent && git2(["rev-parse", `${parent}^{tree}`]) === tree ? parent : commit(tree, parent, "telemetry: update records");
200
+ git2(["update-ref", TELEMETRY_REF, next, tip ?? ""]);
201
+ tip = next;
202
+ committed = true;
203
+ }
204
+ const pending = () => tip ? git2(["rev-list", "--reverse", "--topo-order", tip, ...upstream ? [`^${upstream}`] : []]).split("\n").filter(Boolean) : [];
205
+ if (!pending().length) return { ok: true, committed, pushed: false, warning: fetchWarning };
206
+ const push = () => gitRun(["push", remote, `${TELEMETRY_REF}:${TELEMETRY_REF}`], root);
207
+ const first = push();
208
+ if (first.ok) return { ok: true, committed, pushed: true, warning: fetchWarning };
209
+ const fetched = fetch();
210
+ if (!fetched.ok) throw new Error(`${first.stderr}; refresh failed: ${fetched.stderr}`);
211
+ upstream = resolve(tracking);
212
+ let replayed = upstream;
213
+ for (const sha of pending()) {
214
+ git2(replayed ? ["read-tree", replayed] : ["read-tree", "--empty"]);
215
+ const changes = git2(["diff-tree", "--root", "--no-commit-id", "--raw", "--no-abbrev", "--no-renames", "-r", "-z", sha]).split("\0");
216
+ for (let i = 0; i + 1 < changes.length; i += 2) {
217
+ const [, mode, , blob] = changes[i].split(" ");
218
+ const path = changes[i + 1];
219
+ git2(mode === "000000" ? ["update-index", "--force-remove", "--", path] : ["update-index", "--add", "--cacheinfo", mode, blob, path]);
220
+ }
221
+ replayed = commit(git2(["write-tree"]), replayed, git2(["log", "-1", "--format=%B", sha]));
222
+ }
223
+ if (replayed) git2(["update-ref", TELEMETRY_REF, replayed, tip]);
224
+ const second = push();
225
+ if (!second.ok) throw new Error(second.stderr);
226
+ return { ok: true, committed, pushed: true };
227
+ } catch (error) {
228
+ const reason = error.message;
229
+ return { ok: false, committed, pushed: false, reason, warning: [fetchWarning, `telemetry: not pushed (${reason})`].filter(Boolean).join("; ") };
230
+ } finally {
231
+ rmSync(scratch, { recursive: true, force: true });
232
+ }
233
+ }
234
+ function readTelemetryBranch(root, { remote = "origin", prefix = "" } = {}) {
235
+ const records = [];
236
+ let warning;
237
+ try {
238
+ const synced = syncTelemetryBranch(root, /* @__PURE__ */ new Map(), remote);
239
+ warning = synced.warning;
240
+ if (synced.pushed) {
241
+ const fetched = fetchBranch(root, remote);
242
+ warning = fetched.ok ? void 0 : `fetch failed: ${fetched.stderr}`;
243
+ }
244
+ const tip = gitRun(["rev-parse", "--verify", trackingRef(remote)], root);
245
+ if (!tip.ok) return { records, warning: [warning, "no telemetry ref fetched yet"].filter(Boolean).join("; ") };
246
+ const tree = gitRun(["ls-tree", "-r", "-z", tip.stdout, ...prefix ? ["--", prefix] : []], root);
247
+ if (!tree.ok) throw new Error(tree.stderr);
248
+ for (const entry of tree.stdout.split("\0").filter(Boolean)) {
249
+ const [meta, path] = entry.split(" ");
250
+ const [, type, sha] = meta.split(" ");
251
+ if (type !== "blob" || !path.startsWith(prefix) || !path.endsWith(".yaml")) continue;
252
+ const blob = gitRun(["cat-file", "blob", sha], root, void 0, { raw: true });
253
+ if (!blob.ok) throw new Error(blob.stderr);
254
+ records.push({ path, bytes: blob.stdout });
255
+ }
256
+ return { records, warning };
257
+ } catch (error) {
258
+ return { records, warning: [warning, `read failed: ${error.message}`].filter(Boolean).join("; ") };
259
+ }
260
+ }
34
261
 
35
262
  // src/bins/gauntlet-spec-index.mjs
36
263
  var SCHEMA_VERSION = 3;
@@ -58,13 +285,13 @@ var SCHEMA = `
58
285
  tokenize = 'porter unicode61');
59
286
  `;
60
287
  var usage = () => {
61
- process.stderr.write('usage: gauntlet-spec-index --query "<text>" [--corpus specs|docs] [--limit N] [--exclude <repo-relative path>]...\n');
62
- process.exit(1);
288
+ process2.stderr.write('usage: gauntlet-spec-index --query "<text>" [--corpus specs|docs] [--limit N] [--exclude <repo-relative path>]...\n');
289
+ process2.exit(1);
63
290
  };
64
291
  var die = (msg) => {
65
- process.stderr.write(`${msg}
292
+ process2.stderr.write(`${msg}
66
293
  `);
67
- process.exit(2);
294
+ process2.exit(2);
68
295
  };
69
296
  function parseArgs(argv) {
70
297
  let query2;
@@ -82,43 +309,45 @@ function parseArgs(argv) {
82
309
  return { query: query2, corpus, limit, exclude };
83
310
  }
84
311
  function nodeOk() {
85
- const cur = process.versions.node.split(".").map(Number);
312
+ const cur = process2.versions.node.split(".").map(Number);
86
313
  for (let i = 0; i < 3; i++) if (cur[i] !== MIN_NODE[i]) return cur[i] > MIN_NODE[i];
87
314
  return true;
88
315
  }
89
- var git = (cwd, args) => execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
316
+ var git = (cwd, args) => execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
90
317
  function repoRoot() {
91
318
  try {
92
- return git(process.cwd(), ["rev-parse", "--show-toplevel"]);
319
+ return git(process2.cwd(), ["rev-parse", "--show-toplevel"]);
93
320
  } catch {
94
321
  return die("gauntlet-spec-index: not inside a git repository");
95
322
  }
96
323
  }
97
324
  function readLayer(file) {
98
- if (!existsSync(file)) return {};
325
+ if (!existsSync3(file)) return {};
99
326
  try {
100
- return JSON.parse(readFileSync(file, "utf8"));
327
+ return JSON.parse(readFileSync2(file, "utf8"));
101
328
  } catch (e) {
102
- process.stderr.write(`warning: ${file}: ${e.message}; using {} for this layer
329
+ process2.stderr.write(`warning: ${file}: ${e.message}; using {} for this layer
103
330
  `);
104
331
  return {};
105
332
  }
106
333
  }
334
+ function settingsFor(root) {
335
+ const agentDir = process2.env.PI_CODING_AGENT_DIR || join3(homedir(), ".pi", "agent");
336
+ return mergeGauntlet(readLayer(join3(agentDir, "settings.json"))?.piGauntlet, readLayer(join3(root, ".pi", "settings.json"))?.piGauntlet);
337
+ }
107
338
  function specDirsFor(root) {
108
- const agentDir = process.env.PI_CODING_AGENT_DIR || join(homedir(), ".pi", "agent");
109
- const merged = mergeGauntlet(readLayer(join(agentDir, "settings.json"))?.piGauntlet, readLayer(join(root, ".pi", "settings.json"))?.piGauntlet);
110
- const specDirs = resolveFlowGuards(merged).specDirs;
339
+ const specDirs = resolveFlowGuards(settingsFor(root)).specDirs;
111
340
  return { specDirs, planDirs: planDirsFor(specDirs) };
112
341
  }
113
342
  function discover(root) {
114
343
  const out = [];
115
344
  const seen = /* @__PURE__ */ new Set();
116
345
  const collect = (relDir, service) => {
117
- const abs = join(root, relDir);
346
+ const abs = join3(root, relDir);
118
347
  let real;
119
348
  try {
120
349
  real = realpathSync(abs);
121
- if (!statSync(real).isDirectory()) return;
350
+ if (!statSync2(real).isDirectory()) return;
122
351
  } catch {
123
352
  return;
124
353
  }
@@ -127,7 +356,7 @@ function discover(root) {
127
356
  for (const name of readdirSync(abs).sort()) {
128
357
  if (!name.endsWith(".md")) continue;
129
358
  const rel = `${relDir}/${name}`;
130
- const st = statSync(join(root, rel));
359
+ const st = statSync2(join3(root, rel));
131
360
  if (st.isFile()) out.push({ path: rel, service, mtime_ms: Math.trunc(st.mtimeMs), size: st.size });
132
361
  }
133
362
  };
@@ -143,13 +372,13 @@ function services(root) {
143
372
  return readdirSync(root, { withFileTypes: true }).filter((e) => (e.isDirectory() || e.isSymbolicLink()) && !e.name.startsWith(".") && !SKIP_DIRS.has(e.name)).map((e) => e.name);
144
373
  }
145
374
  function includes(root) {
146
- const file = OVERRIDES_FILES.map((rel) => join(root, rel)).find((abs) => existsSync(abs));
375
+ const file = OVERRIDES_FILES.map((rel) => join3(root, rel)).find((abs) => existsSync3(abs));
147
376
  if (!file) return DOC_INCLUDES;
148
377
  const globs = [];
149
378
  let inSection = false;
150
379
  let text;
151
380
  try {
152
- text = readFileSync(file, "utf8");
381
+ text = readFileSync2(file, "utf8");
153
382
  } catch (e) {
154
383
  die(`gauntlet-spec-index: unreadable overrides file ${file}: ${e.message}`);
155
384
  }
@@ -187,15 +416,15 @@ function discoverDocs(root, globs) {
187
416
  const skip = [...specDirs, ...planDirs];
188
417
  const svc = services(root);
189
418
  const patterns = globs.flatMap((g) => g.includes("/") ? [g] : [g, ...svc.map((s) => `${s}/${g}`)]);
190
- const listed = execFileSync("git", ["-C", root, "ls-files", "-co", "--exclude-standard", "-z", "--", "*.md"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).split("\0").filter(Boolean);
419
+ const listed = execFileSync2("git", ["-C", root, "ls-files", "-co", "--exclude-standard", "-z", "--", "*.md"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).split("\0").filter(Boolean);
191
420
  const out = [];
192
421
  for (const rel of listed) {
193
422
  if (!patterns.some((p) => matchesGlob(rel, p))) continue;
194
423
  if (DOC_EXCLUDE_GLOBS.some((p) => matchesGlob(rel, p)) || underDirs(rel, skip)) continue;
195
424
  let st;
196
425
  try {
197
- st = lstatSync(join(root, rel));
198
- if (!st.isFile() || firstLine(join(root, rel)) === DRAFT_MARKER) continue;
426
+ st = lstatSync(join3(root, rel));
427
+ if (!st.isFile() || firstLine(join3(root, rel)) === DRAFT_MARKER) continue;
199
428
  } catch {
200
429
  continue;
201
430
  }
@@ -205,18 +434,18 @@ function discoverDocs(root, globs) {
205
434
  }
206
435
  function ensureExclude(root) {
207
436
  const rel = git(root, ["rev-parse", "--git-path", "info/exclude"]);
208
- const abs = isAbsolute(rel) ? rel : join(root, rel);
209
- const cur = existsSync(abs) ? readFileSync(abs, "utf8") : "";
437
+ const abs = isAbsolute(rel) ? rel : join3(root, rel);
438
+ const cur = existsSync3(abs) ? readFileSync2(abs, "utf8") : "";
210
439
  if (cur.split("\n").includes(EXCLUDE_LINE)) return;
211
- mkdirSync(dirname(abs), { recursive: true });
440
+ mkdirSync(dirname2(abs), { recursive: true });
212
441
  appendFileSync(abs, `${cur.length && !cur.endsWith("\n") ? "\n" : ""}${EXCLUDE_LINE}
213
442
  `);
214
443
  }
215
444
  async function openDb(root) {
216
445
  const { DatabaseSync } = await import("node:sqlite");
217
- const dbPath = join(root, ".pi/gauntlet/index.sqlite");
218
- mkdirSync(dirname(dbPath), { recursive: true });
219
- const fresh = !existsSync(dbPath);
446
+ const dbPath = join3(root, ".pi/gauntlet/index.sqlite");
447
+ mkdirSync(dirname2(dbPath), { recursive: true });
448
+ const fresh = !existsSync3(dbPath);
220
449
  const create = () => {
221
450
  const db2 = new DatabaseSync(dbPath);
222
451
  db2.exec("PRAGMA busy_timeout = 2000");
@@ -234,7 +463,7 @@ async function openDb(root) {
234
463
  db2?.close();
235
464
  } catch {
236
465
  }
237
- for (const suffix of ["", "-journal", "-wal", "-shm"]) rmSync(dbPath + suffix, { force: true });
466
+ for (const suffix of ["", "-journal", "-wal", "-shm"]) rmSync2(dbPath + suffix, { force: true });
238
467
  return create();
239
468
  };
240
469
  if (fresh) ensureExclude(root);
@@ -275,7 +504,7 @@ function extract(text, path) {
275
504
  const lines = text.split(/\r?\n/);
276
505
  const fenced = fenceMask(lines);
277
506
  const h1 = lines.findIndex((l, i) => !fenced[i] && l.startsWith("# "));
278
- const title = (h1 >= 0 ? lines[h1].slice(2).trim() : "") || basename(path, ".md");
507
+ const title = (h1 >= 0 ? lines[h1].slice(2).trim() : "") || basename2(path, ".md");
279
508
  const goal = lines.find((l) => l.startsWith("**Goal:**"))?.slice("**Goal:**".length).trim() ?? "";
280
509
  const headings = lines.filter((l, i) => !fenced[i] && /^##{1,2} /.test(l)).map((l) => l.replace(/^#+ /, "")).join("\n");
281
510
  let state = "live";
@@ -308,7 +537,7 @@ function refresh(db, root, corpus) {
308
537
  for (const f of found) {
309
538
  const k = known.get(f.path);
310
539
  if (k && k.mtime_ms === f.mtime_ms && k.size === f.size) continue;
311
- const text = readFileSync(join(root, f.path), "utf8");
540
+ const text = readFileSync2(join3(root, f.path), "utf8");
312
541
  delRow.run(f.path);
313
542
  if (text.split(/\r?\n/, 1)[0] === DRAFT_MARKER) {
314
543
  delFile.run(table, f.path);
@@ -370,21 +599,34 @@ function query(db, corpus, toks, { limit, exclude }) {
370
599
  const cut = best ? kept.filter((r) => Math.abs(r.score) >= SCORE_RATIO * Math.abs(best.score)) : kept;
371
600
  return { rows: cut.slice(0, limit), n };
372
601
  }
373
- function telemetry(root, specPath) {
602
+ function telemetryCorpus(root) {
603
+ const settings = resolveTelemetry(settingsFor(root));
604
+ if (settings.warning) process2.stderr.write(`warning: ${settings.warning}
605
+ `);
606
+ if (!settings.enabled) return /* @__PURE__ */ new Map();
607
+ const repo = telemetryRepo(root);
608
+ const prefix = `${repo}/brainstorming/`;
609
+ const result = readTelemetryBranch(root, { remote: settings.remote, prefix });
610
+ if (result.warning) process2.stderr.write(`warning: ${result.warning}
611
+ `);
612
+ return new Map(result.records.map(({ path, bytes }) => [path.slice(prefix.length), bytes]));
613
+ }
614
+ function telemetry(root, specPath, records) {
374
615
  const blank = { status: null, shipped_at: null, files: "" };
375
- const p = join(root, ".pi/gauntlet/telemetry", specPath.replace(/\.md$/, ".yaml"));
376
- if (!existsSync(p)) return blank;
616
+ const p = specPath.replace(/\.md$/, ".yaml");
617
+ const bytes = records.get(p);
618
+ if (bytes === void 0) return blank;
377
619
  let rec;
378
620
  try {
379
- rec = parseYaml(readFileSync(p, "utf8"));
621
+ rec = parseYaml(bytes);
380
622
  } catch {
381
- process.stderr.write(`gauntlet-spec-index: warning: unreadable telemetry ${p}
623
+ process2.stderr.write(`gauntlet-spec-index: warning: unreadable telemetry ${p}
382
624
  `);
383
625
  return blank;
384
626
  }
385
627
  if (!rec || typeof rec !== "object" || Array.isArray(rec)) return blank;
386
628
  const mf = rec.derived?.modified_files;
387
- const files = Array.isArray(mf) ? mf.filter((f) => typeof f === "string" && existsSync(join(root, f))).join(";") : "missing";
629
+ const files = Array.isArray(mf) ? mf.filter((f) => typeof f === "string" && existsSync3(join3(root, f))).join(";") : "missing";
388
630
  return { status: rec.status ?? null, shipped_at: rec.shipped_at ?? null, files };
389
631
  }
390
632
  var cell = (v) => v === null || v === void 0 ? "" : String(v).replace(/\s+/g, " ").trim();
@@ -408,8 +650,8 @@ var CORPORA = {
408
650
  const x = extract(text, f.path);
409
651
  return [f.path, f.service, x.title, x.goal, x.headings, text, x.state];
410
652
  },
411
- format: (root, r) => {
412
- const t = telemetry(root, r.path);
653
+ format: (root, r, records) => {
654
+ const t = telemetry(root, r.path, records);
413
655
  return [...[r.score.toFixed(3), r.path, r.service, r.title, t.status, t.shipped_at, r.state].map(cell), filesCell(t.files), cell(r.snippet)];
414
656
  }
415
657
  },
@@ -430,8 +672,8 @@ var CORPORA = {
430
672
  }
431
673
  };
432
674
  async function main() {
433
- if (!nodeOk()) die(`gauntlet-spec-index needs Node >=24.15.0 (found ${process.versions.node})`);
434
- const { query: text, corpus: name, limit, exclude } = parseArgs(process.argv.slice(2));
675
+ if (!nodeOk()) die(`gauntlet-spec-index needs Node >=24.15.0 (found ${process2.versions.node})`);
676
+ const { query: text, corpus: name, limit, exclude } = parseArgs(process2.argv.slice(2));
435
677
  const toks = tokens(text);
436
678
  if (!toks.length) usage();
437
679
  const root = repoRoot();
@@ -439,11 +681,12 @@ async function main() {
439
681
  const db = await openDb(root);
440
682
  refresh(db, root, corpus);
441
683
  const { rows, n } = query(db, corpus, toks, { limit, exclude });
684
+ const records = name === "specs" ? telemetryCorpus(root) : /* @__PURE__ */ new Map();
442
685
  const out = [corpus.header.join(" ")];
443
- for (const r of rows) out.push(corpus.format(root, r).join(" "));
444
- process.stdout.write(out.join("\n") + "\n");
686
+ for (const r of rows) out.push(corpus.format(root, r, records).join(" "));
687
+ process2.stdout.write(out.join("\n") + "\n");
445
688
  if (rows.length === 0 && n < MIN_CORPUS) {
446
- process.stderr.write(`gauntlet-spec-index: ${name} corpus has ${n} documents - too small for the confidence rule; no rows returned
689
+ process2.stderr.write(`gauntlet-spec-index: ${name} corpus has ${n} documents - too small for the confidence rule; no rows returned
447
690
  `);
448
691
  }
449
692
  db.close();