@holmes-lab/holmes-kit 0.23.3 → 0.24.1

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.
@@ -34,12 +34,33 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.EMPTY_REGISTRY = void 0;
37
+ exports.holmesHome = holmesHome;
37
38
  exports.readRegistry = readRegistry;
38
39
  exports.mergeWorkspaceEntry = mergeWorkspaceEntry;
40
+ exports.isTempTarget = isTempTarget;
41
+ exports.pruneRegistry = pruneRegistry;
39
42
  exports.recordWorkspace = recordWorkspace;
40
43
  // @implements A-SPEC-543.1
41
44
  const fs = __importStar(require("node:fs"));
45
+ const os = __importStar(require("node:os"));
42
46
  const path = __importStar(require("node:path"));
47
+ /**
48
+ * @implements A-SPEC-678
49
+ * Where the registry lives — with one seam a test can reach.
50
+ *
51
+ * Measured 2026-09-18: inside jest, `process.env` is a plain object copy in the sandbox, so
52
+ * assigning `HOME` never reaches `setenv` and `os.homedir()` keeps answering the developer's real
53
+ * home. Plain node follows `$HOME`; jest does not. An in-process test therefore cannot isolate a
54
+ * module that calls `os.homedir()` directly — which is how 599 of this machine's 603 registry
55
+ * entries came to be test temp directories.
56
+ *
57
+ * jest DOES control `process.env`, so one lookup there is the smallest seam that works. Consumers
58
+ * set nothing and get `os.homedir()`; an empty value is not a setting.
59
+ */
60
+ function holmesHome(env, fallback) {
61
+ const override = env.HOLMES_HOME;
62
+ return typeof override === 'string' && override.trim() !== '' ? override : fallback;
63
+ }
43
64
  exports.EMPTY_REGISTRY = { version: 1, workspaces: [] };
44
65
  /**
45
66
  * @implements A-SPEC-543.1
@@ -79,16 +100,59 @@ function mergeWorkspaceEntry(reg, entry) {
79
100
  };
80
101
  }
81
102
  /**
82
- * @implements A-SPEC-543.1
83
- * Thin best-effort recorder: read merge write `~/.holmes/workspaces.json`. Every failure is
84
- * swallowed (returns false) — a registry problem must never fail the init that feeds it.
103
+ * @implements A-SPEC-677
104
+ * Whether a target lives under the OS temp root and therefore is ephemeral by construction.
105
+ *
106
+ * macOS spells that root two ways: `os.tmpdir()` answers `/var/folders/...` while a realpath gives
107
+ * `/private/var/folders/...`. Both appear in this machine's registry, so both are judged. A `/tmp`
108
+ * literal would have missed all 599 of them.
109
+ *
110
+ * An empty root filters NOTHING. When the judgement cannot be made, keeping an entry costs a line
111
+ * and dropping one the user wanted costs them something they cannot get back.
85
112
  */
113
+ function isTempTarget(target, tmpdir) {
114
+ if (typeof target !== 'string' || typeof tmpdir !== 'string' || tmpdir.trim() === '')
115
+ return false;
116
+ const roots = new Set([tmpdir, tmpdir.startsWith('/private') ? tmpdir.slice('/private'.length) : `/private${tmpdir}`]);
117
+ return [...roots].some((r) => r !== '' && (target === r || target.startsWith(r.endsWith('/') ? r : `${r}/`)));
118
+ }
119
+ /**
120
+ * @implements A-SPEC-677
121
+ * Drop what has certainly gone; COUNT what merely cannot be seen.
122
+ *
123
+ * The asymmetry is the whole judgement. A vanished temp path is gone by construction — nothing
124
+ * re-creates a `mkdtemp` directory. A vanished normal path may be an unmounted volume, an external
125
+ * disk or a network share, and `upgrade` already skips it harmlessly with one line. Deleting it is
126
+ * the one outcome the user cannot undo, so absence outside the temp root is reported, never acted on.
127
+ */
128
+ function pruneRegistry(reg, p) {
129
+ let dropped = 0, absent = 0;
130
+ const workspaces = reg.workspaces.filter((w) => {
131
+ if (p.exists(w.target))
132
+ return true;
133
+ if (p.isTemp(w.target)) {
134
+ dropped += 1;
135
+ return false;
136
+ }
137
+ absent += 1;
138
+ return true;
139
+ });
140
+ return { registry: { version: 1, workspaces }, dropped, absent };
141
+ }
86
142
  function recordWorkspace(home, entry, io = {
87
143
  read: (p) => fs.readFileSync(p, 'utf8'),
88
144
  write: (p, c) => fs.writeFileSync(p, c),
89
145
  mkdir: (p) => { fs.mkdirSync(p, { recursive: true }); },
146
+ exists: (p) => fs.existsSync(p),
147
+ tmpdir: () => os.tmpdir(),
90
148
  }) {
91
149
  try {
150
+ // @implements A-SPEC-677 — an ephemeral target is not recorded at all. What never enters cannot
151
+ // pile up, and 599 of this machine's 603 entries entered exactly this way. Returning true is
152
+ // honest: nothing failed, there was simply nothing worth remembering.
153
+ const tmpdir = io.tmpdir?.() ?? '';
154
+ if (isTempTarget(entry.target, tmpdir))
155
+ return true;
92
156
  const dir = path.join(home, '.holmes');
93
157
  const file = path.join(dir, 'workspaces.json');
94
158
  let raw = null;
@@ -98,9 +162,18 @@ function recordWorkspace(home, entry, io = {
98
162
  catch {
99
163
  raw = null;
100
164
  }
101
- const merged = mergeWorkspaceEntry(readRegistry(raw), entry);
165
+ const exists = io.exists;
166
+ // @implements A-SPEC-677 — prune while we are already here. Without an `exists` probe there is
167
+ // no way to tell gone from present, so the registry is merged unpruned rather than guessed at.
168
+ const base = exists
169
+ ? pruneRegistry(readRegistry(raw), { exists, isTemp: (t) => isTempTarget(t, tmpdir) })
170
+ : { registry: readRegistry(raw), dropped: 0, absent: 0 };
171
+ const merged = mergeWorkspaceEntry(base.registry, entry);
102
172
  io.mkdir(dir);
103
173
  io.write(file, `${JSON.stringify(merged, null, 2)}\n`);
174
+ // Never a silent deletion: whoever asked can learn how many entries went.
175
+ if (base.dropped > 0)
176
+ io.onPrune?.(base.dropped);
104
177
  return true;
105
178
  }
106
179
  catch {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "//": "@implements A-SPEC-209",
3
3
  "name": "@holmes-lab/holmes-kit",
4
- "version": "0.23.3",
4
+ "version": "0.24.1",
5
5
  "description": "Holmes-Kit — deterministic Agentic Software Engineering (ASE) harness with causal traceability (spec chain + D-CPG + RTM + phase guardrail)",
6
6
  "main": "dist/holmes/mcp/server.js",
7
7
  "types": "dist/holmes/mcp/server.d.ts",