promptwheel 0.8.2 → 0.8.3

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.
@@ -16,7 +16,7 @@ import { execSync, execFileSync } from 'node:child_process';
16
16
  import { createHash } from 'node:crypto';
17
17
  import { readFileSync, writeFileSync, existsSync, mkdtempSync, symlinkSync, rmSync, mkdirSync, realpathSync, readdirSync, lstatSync, copyFileSync } from 'node:fs';
18
18
  import { tmpdir } from 'node:os';
19
- import { join, dirname, basename, isAbsolute, relative, sep } from 'node:path';
19
+ import { join, dirname, basename, isAbsolute, relative, sep, resolve } from 'node:path';
20
20
  import { fileURLToPath } from 'node:url';
21
21
 
22
22
  // ---------------------------------------------------------------------------
@@ -36,6 +36,19 @@ const spread = (xs) => { const a = xs.filter((x) => x != null); return a.length
36
36
  // repo is still measuring in (a long gate elsewhere must not lose its worktree mid-run)
37
37
  function tmpOwnerTag(repo) { let r = repo; try { r = realpathSync(repo); } catch { /* keep as given */ } return createHash('sha1').update(String(r)).digest('hex').slice(0, 8); }
38
38
  const tmpPrefix = (repo, kind) => `${kind}-${tmpOwnerTag(repo)}-`;
39
+ // where the engine makes its clean copies: the system temp folder, or, where that is read-only (Claude Code's sandbox,
40
+ // a locked-down CI runner), a folder inside the repo's own .git, which git status never shows
41
+ const tmpBases = new Map();
42
+ function tmpBase(repo) {
43
+ if (tmpBases.has(repo)) return tmpBases.get(repo);
44
+ let base = tmpdir();
45
+ try { rmSync(mkdtempSync(join(base, 'pw-probe-')), { recursive: true, force: true }); }
46
+ catch {
47
+ let gd = ''; try { gd = resolve(repo, execFileSync('git', ['-C', repo, 'rev-parse', '--git-common-dir'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()); } catch { /* */ }
48
+ if (gd) { base = join(gd, 'promptwheel-tmp'); try { mkdirSync(base, { recursive: true }); } catch { base = tmpdir(); } }
49
+ }
50
+ tmpBases.set(repo, base); return base;
51
+ }
39
52
  // exactly the names mkdtemp makes from tmpPrefix: the kind, the 8-hex owner tag, then mkdtemp's 6 random
40
53
  // characters. Self-heal removes nothing else, so a folder that only starts with "promptwheel-" is never ours.
41
54
  const ENGINE_TMP = /^(promptwheel-src|promptwheel|pw-idx)-([0-9a-f]{8})-[A-Za-z0-9]{6}$/;
@@ -273,7 +286,7 @@ const setupFails = new WeakMap(); // a measureAt result -> how its setup faile
273
286
 
274
287
  // measure every metric `repeat` times at a ref, in a throwaway worktree (never touches your tree)
275
288
  function measureAt(repo, ref, cfg, repeat) {
276
- const wt = mkdtempSync(join(tmpdir(), tmpPrefix(repo, 'promptwheel')));
289
+ const wt = mkdtempSync(join(tmpBase(repo), tmpPrefix(repo, 'promptwheel')));
277
290
  git(['worktree', 'add', '--quiet', '--detach', wt, ref], repo);
278
291
  try {
279
292
  const env = bridgeEnv(repo, wt, cfg);
@@ -489,7 +502,7 @@ function measureSourceOnlyAll(repo, base, head, defs, cfg, repeat) {
489
502
  if (!defs.length) return out;
490
503
  const { source } = changedSourcePaths(repo, base, head, evaluatorMatcher(cfg));
491
504
  if (!source.length) { for (const m of defs) out[m.name] = { samples: [], hadSourceChange: false }; return out; } // a "win" with zero source edits = goalposts moved
492
- const wt = mkdtempSync(join(tmpdir(), tmpPrefix(repo, 'promptwheel-src')));
505
+ const wt = mkdtempSync(join(tmpBase(repo), tmpPrefix(repo, 'promptwheel-src')));
493
506
  git(['worktree', 'add', '--quiet', '--detach', wt, base], repo);
494
507
  try {
495
508
  const env = bridgeEnv(repo, wt, cfg);
@@ -550,7 +563,7 @@ function judgeGaming(m, srcResult, threshold = 0.5) {
550
563
  // touching the real index or files, so a gate sees a file an agent just added.
551
564
  // Returns 'HEAD' when nothing changed.
552
565
  function workingSnapshot(repo) {
553
- const idxDir = mkdtempSync(join(tmpdir(), tmpPrefix(repo, 'pw-idx')));
566
+ const idxDir = mkdtempSync(join(tmpBase(repo), tmpPrefix(repo, 'pw-idx')));
554
567
  const env = {
555
568
  ...process.env, GIT_INDEX_FILE: join(idxDir, 'index'),
556
569
  GIT_AUTHOR_NAME: 'promptwheel', GIT_AUTHOR_EMAIL: 'promptwheel@local',
@@ -589,7 +602,7 @@ function selfHeal(repo) {
589
602
  git(['worktree', 'list', '--porcelain'], repo)
590
603
  .split('\n').filter((l) => l.startsWith('worktree ')).map((l) => l.slice(9).trim()),
591
604
  );
592
- const tmp = tmpdir(), now = Date.now(), mine = tmpOwnerTag(repo);
605
+ const tmp = tmpBase(repo), now = Date.now(), mine = tmpOwnerTag(repo);
593
606
  for (const name of readdirSync(tmp)) {
594
607
  const made = ENGINE_TMP.exec(name);
595
608
  if (!made) continue; // not a name this engine generates (worktrees + temp-index dirs): never removed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptwheel",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "description": "A deterministic, zero-LLM check for your coding loop. A win must survive a re-run from the agent's source edits alone, so a test that went green by editing the test reads GAMED. Runs in CI or a Claude Code plugin.",
5
5
  "type": "module",
6
6
  "bin": {