plumbbob 0.2.1 → 0.2.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.
@@ -2,10 +2,18 @@
2
2
  // recent step, or `--to n`, with the baseline as fallback), then remove untracked
3
3
  // files under the SEAM only. The sidecar is git-excluded (D17), so the reset
4
4
  // never touches it — park lines and intent edits survive the revert (C4).
5
- import { readFileSync, rmSync } from 'node:fs';
5
+ //
6
+ // Plumbbob also installs its driver skills INTO the repo (.claude/skills/<driver>/
7
+ // for a self-contained install), so a blunt reset would discard an out-of-seam
8
+ // skill edit — or a `pnpm up plumbbob` re-setup — together with the half-done
9
+ // step. revert discards the step's WORK, never plumbbob's own machinery, so those
10
+ // paths are carried across the reset unchanged.
11
+ import { cpSync, existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from 'node:fs';
12
+ import { tmpdir } from 'node:os';
6
13
  import { join } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
7
15
  import { findRepoRoot, resetHard, untrackedPaths } from "../lib/git.js";
8
- import { checkpointsPath, hasSession, seamPath, stepPath, writeState } from "../lib/sidecar.js";
16
+ import { checkpointsPath, hasSession, seamPath, sidecarDir, stepPath, writeState } from "../lib/sidecar.js";
9
17
  import { matchesSeam } from "../lib/intent.js";
10
18
  export function revert(cwd, args) {
11
19
  const root = findRepoRoot(cwd);
@@ -39,7 +47,7 @@ export function revert(cwd, args) {
39
47
  // ignored files alone, so they must be removed explicitly afterward).
40
48
  const seam = readSeamTokens(root);
41
49
  const toRemove = untrackedPaths(root).filter((p) => matchesSeam(p, seam));
42
- resetHard(root, sha);
50
+ resetPreserving(root, sha, plumbbobOwnedPaths(root));
43
51
  for (const rel of toRemove) {
44
52
  rmSync(join(root, rel), { force: true, recursive: true });
45
53
  }
@@ -49,6 +57,38 @@ export function revert(cwd, args) {
49
57
  process.stdout.write(`plumbbob: reverted to ${sha.slice(0, 9)} — STATE=DESIGN. Park lines and intent edits were preserved.\n`);
50
58
  return 0;
51
59
  }
60
+ // The repo paths that belong to plumbbob, not to the work being reverted: the
61
+ // sidecar (already git-excluded, listed so revert is robust even where `.plumbbob/`
62
+ // was tracked by mistake) and each installed driver skill under .claude/skills/.
63
+ // Skill names come from plumbbob's own bundled `skills/` dir — the same source
64
+ // `setup` copies from — so only plumbbob's own skills are protected, never the
65
+ // user's. Only paths that currently exist are returned.
66
+ function plumbbobOwnedPaths(root) {
67
+ const paths = [sidecarDir(root)];
68
+ try {
69
+ for (const name of readdirSync(fileURLToPath(new URL('../../skills', import.meta.url)))) {
70
+ paths.push(join(root, '.claude', 'skills', name));
71
+ }
72
+ }
73
+ catch {
74
+ // Bundled skills dir not resolvable (unexpected) — protect just the sidecar.
75
+ }
76
+ return paths.filter((p) => existsSync(p));
77
+ }
78
+ // `git reset --hard <sha>` is repo-wide, so paths that must survive it are copied
79
+ // to a temp snapshot first, then copied back over whatever the reset produced.
80
+ // Restoring on top (no pre-delete) keeps the live sidecar safe if a copy throws.
81
+ function resetPreserving(root, sha, preserve) {
82
+ const snap = mkdtempSync(join(tmpdir(), 'plumbbob-revert-'));
83
+ try {
84
+ preserve.forEach((p, i) => cpSync(p, join(snap, String(i)), { recursive: true }));
85
+ resetHard(root, sha);
86
+ preserve.forEach((p, i) => cpSync(join(snap, String(i)), p, { recursive: true }));
87
+ }
88
+ finally {
89
+ rmSync(snap, { force: true, recursive: true });
90
+ }
91
+ }
52
92
  function parseTo(args) {
53
93
  const idx = args.indexOf('--to');
54
94
  if (idx === -1) {
package/hooks/pre-edit.sh CHANGED
@@ -84,6 +84,23 @@ case "$rel" in
84
84
  ;;
85
85
  esac
86
86
 
87
+ # The seam muzzle below governs repo code only (D23). A path *outside* the repo
88
+ # (Claude's own plan-mode scratch in ~/.claude/plans, an editor tmpfile) and a
89
+ # git-ignored path *inside* it (fallow data, dist/, coverage/) are none of its
90
+ # business — never block them. `.plumbbob/` is itself git-ignored but is
91
+ # plumbbob's own control surface, so its non-doc files (STATE/SEAM/...) must stay
92
+ # governed by the muzzle, never skipped here.
93
+ case "$rel" in
94
+ .plumbbob/*) ;; # control state: stays governed by the muzzle, never skipped
95
+ *)
96
+ # git resolves the path via the repo toplevel (robust to symlinked cwds).
97
+ # check-ignore exit: 0 = ignored, 128 = outside the repo — neither is the
98
+ # muzzle's business; 1 = in-repo, tracked territory -> fall through to it.
99
+ git -C "$root" check-ignore -q -- "$path" 2>/dev/null
100
+ [ "$?" -ne 1 ] && exit 0
101
+ ;;
102
+ esac
103
+
87
104
  # 4. Everything else is code: allowed only in BUILD, confined to the SEAM (D23).
88
105
  # SPIKE locks the main tree like DESIGN (D18) — spike edits live in dormant
89
106
  # worktrees, so the muzzle never needs to allow SPIKE here.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plumbbob",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Attention-first build process: a CLI + hooks that enforce the deciding/executing boundary.",
5
5
  "keywords": [
6
6
  "claude-code",