@fusengine/harness 0.1.41 → 0.1.42

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/README.md +47 -0
  2. package/dist/adapters/claude/index.mjs +1 -1
  3. package/dist/adapters/cline/index.mjs +1 -1
  4. package/dist/adapters/codex/index.mjs +1 -1
  5. package/dist/adapters/cursor/index.d.mts +4 -2
  6. package/dist/adapters/cursor/index.mjs +1 -1
  7. package/dist/adapters/gemini/index.mjs +1 -1
  8. package/dist/cache/index.mjs +1 -1
  9. package/dist/{claude-BnFrSfMo.mjs → claude-CeRYMOaG.mjs} +1 -1
  10. package/dist/cli/bin.mjs +95 -4
  11. package/dist/cli/index.mjs +1 -1
  12. package/dist/config/index.d.mts +1 -1
  13. package/dist/detect/index.d.mts +2 -2
  14. package/dist/{evaluate-B1n1ti0N.mjs → evaluate-CeivW6G0.mjs} +4 -4
  15. package/dist/freshness/index.mjs +1 -1
  16. package/dist/{freshness-43gxYpiX.mjs → freshness-BV3PQkDB.mjs} +1 -1
  17. package/dist/{handle-VhWQxvyN.mjs → handle-BTHcKWQ5.mjs} +1007 -1012
  18. package/dist/{harness-DwJskkz_.d.mts → harness-BPPu5CrN.d.mts} +4 -8
  19. package/dist/{index-DTrjSNmI.d.mts → index-DN4cZDbU.d.mts} +7 -5
  20. package/dist/{index-COBvvc3L.d.mts → index-DXQfL1u8.d.mts} +1 -1
  21. package/dist/index.d.mts +4 -4
  22. package/dist/index.mjs +6 -6
  23. package/dist/init/index.d.mts +1 -1
  24. package/dist/{mcp-store-CDUVqtJ0.mjs → mcp-store-CnsW9oFj.mjs} +1 -1
  25. package/dist/memory/index.mjs +1 -1
  26. package/dist/policy/index.d.mts +2 -2
  27. package/dist/policy/index.mjs +2 -2
  28. package/dist/{registry-BkoEbdec.mjs → registry-CymilZiZ.mjs} +3 -2
  29. package/dist/{run-D-Ydrw3D.mjs → run-jgivVDv6.mjs} +1 -1
  30. package/dist/runtime/index.d.mts +13 -12
  31. package/dist/runtime/index.mjs +3 -2
  32. package/dist/state/index.mjs +1 -1
  33. package/dist/{state-BthKK4Jj.mjs → state-9lWvvWk8.mjs} +1 -1
  34. package/dist/{store-3WBr37Xz.mjs → store-CdWOQ9zD.mjs} +50 -3
  35. package/dist/tracking/index.mjs +1 -1
  36. package/dist/{validate-B3wkqEoN.mjs → validate-xi-zc-22.mjs} +1 -1
  37. package/package.json +1 -1
  38. package/dist/{json-io-CvSumjtz.mjs → json-io-DisYd2fb.mjs} +1 -1
package/README.md CHANGED
@@ -45,6 +45,11 @@ automatically under `<project>/.harness/` (track, cache, memory).
45
45
  | `harness init [id]` | Write the pre+post hook wiring for the detected (or named) harness. |
46
46
  | `harness hook <id>` | Runtime: read a hook payload on stdin, gate (pre) or record (post), print the native response. (Hooks call this — you don't.) |
47
47
  | `harness check` | cli-mode: check staged files in a pre-commit step, exit non-zero on a violation. For harnesses without hooks. |
48
+ | `harness doctor` | Print the version + resolved path of the harness *actually executing*, and compare it to npm's latest — the fast way to catch a stale global (see Pinning). |
49
+ | `harness --version` | Print the running version (bare, on stdout) and exit. |
50
+
51
+ Every invocation writes a `@fusengine/harness vX.Y.Z` banner to **stderr** (never
52
+ stdout — the hook JSON contract stays clean) so you can see which version ran.
48
53
 
49
54
  cli-mode (Aider / Windsurf / OpenHands), as a pre-commit step:
50
55
 
@@ -53,6 +58,48 @@ cli-mode (Aider / Windsurf / OpenHands), as a pre-commit step:
53
58
  npx harness check
54
59
  ```
55
60
 
61
+ ### Pinning (required for hook consumers)
62
+
63
+ Any consumer `hooks.json` / `settings.json` that runs the harness via `bunx`
64
+ **MUST pin an exact version** — `@fusengine/harness@X.Y.Z` — never a range
65
+ (`^` / `~`) and never the bare or `@latest` spec:
66
+
67
+ ```jsonc
68
+ // .claude/settings.json — correct: exact pin
69
+ "command": "bunx @fusengine/harness@0.1.41 hook claude"
70
+ // WRONG — may silently keep running a stale global install:
71
+ "command": "bunx @fusengine/harness hook claude"
72
+ ```
73
+
74
+ **Why.** A still-open bun bug ([oven-sh/bun#5791]) makes an unpinned `bunx <pkg>`
75
+ prefer an already-installed **global** copy over npm-latest, and publishing a new
76
+ version updates neither that global nor the bunx cache (`bun pm cache rm` does
77
+ **not** remove the global). A hook wired without an exact pin can therefore keep
78
+ executing an old harness indefinitely after you publish a fix — the failure is
79
+ silent because the smoke test (importing the local dist) never exercises the
80
+ `bunx`-resolved binary. Pinning `@X.Y.Z` forces the exact version to resolve.
81
+
82
+ Run `harness doctor` to see which version is actually executing and whether npm
83
+ has a newer one:
84
+
85
+ ```sh
86
+ harness doctor
87
+ # @fusengine/harness doctor
88
+ # running: 0.1.41
89
+ # package: /Users/you/.bun/install/global/node_modules/@fusengine/harness
90
+ # runtime: /Users/you/.bun/bin/bun
91
+ # npm latest: 0.1.42
92
+ # ! stale — npm serves 0.1.42. Pin "@fusengine/harness@0.1.42" in hooks.json.
93
+ ```
94
+
95
+ If `doctor` reports a stale global, clear it and re-pin:
96
+
97
+ ```sh
98
+ bun remove -g @fusengine/harness && bun pm cache rm
99
+ ```
100
+
101
+ [oven-sh/bun#5791]: https://github.com/oven-sh/bun/issues/5791
102
+
56
103
  ## What it enforces
57
104
 
58
105
  Ten portable guards + the APEX gate chain, all evaluated before a tool runs:
@@ -1,2 +1,2 @@
1
- import { a as guard, i as fileSizeGuard, n as contextResponse, o as readClaudeInput, r as denyResponse, s as toClaudeResponse, t as blockResponse } from "../../claude-BnFrSfMo.mjs";
1
+ import { a as guard, i as fileSizeGuard, n as contextResponse, o as readClaudeInput, r as denyResponse, s as toClaudeResponse, t as blockResponse } from "../../claude-CeRYMOaG.mjs";
2
2
  export { blockResponse, contextResponse, denyResponse, fileSizeGuard, guard, readClaudeInput, toClaudeResponse };
@@ -1,4 +1,4 @@
1
- import { t as evaluate } from "../../evaluate-B1n1ti0N.mjs";
1
+ import { t as evaluate } from "../../evaluate-CeivW6G0.mjs";
2
2
  import { t as formatPrompt } from "../../types-ernB1Dy3.mjs";
3
3
  //#region src/adapters/cline/index.ts
4
4
  /**
@@ -1,2 +1,2 @@
1
- import { a as guard, n as contextResponse, o as readClaudeInput, r as denyResponse } from "../../claude-BnFrSfMo.mjs";
1
+ import { a as guard, n as contextResponse, o as readClaudeInput, r as denyResponse } from "../../claude-CeRYMOaG.mjs";
2
2
  export { contextResponse, denyResponse, guard, readClaudeInput as readCodexInput };
@@ -1,4 +1,4 @@
1
- //#region src/adapters/cursor/index.d.ts
1
+ //#region src/adapters/cursor/interfaces/types.d.ts
2
2
  /** `beforeShellExecution` stdin payload (subset). */
3
3
  interface CursorShellPayload {
4
4
  command?: string;
@@ -21,6 +21,8 @@ interface CursorResponse {
21
21
  userMessage?: string;
22
22
  agentMessage?: string;
23
23
  }
24
+ //#endregion
25
+ //#region src/adapters/cursor/index.d.ts
24
26
  /** Guard a shell command (git/install policies). */
25
27
  declare function beforeShellExecution(payload: CursorShellPayload): CursorResponse;
26
28
  /** Observe a file edit (Cursor cannot block here). Returns the verdict for logging. */
@@ -28,4 +30,4 @@ declare function afterFileEdit(payload: CursorEditPayload): {
28
30
  violation: string | null;
29
31
  };
30
32
  //#endregion
31
- export { CursorEditPayload, CursorResponse, CursorShellPayload, afterFileEdit, beforeShellExecution };
33
+ export { type CursorEditPayload, type CursorResponse, type CursorShellPayload, afterFileEdit, beforeShellExecution };
@@ -1,4 +1,4 @@
1
- import { t as evaluate } from "../../evaluate-B1n1ti0N.mjs";
1
+ import { t as evaluate } from "../../evaluate-CeivW6G0.mjs";
2
2
  import { t as formatPrompt } from "../../types-ernB1Dy3.mjs";
3
3
  //#region src/adapters/cursor/index.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { t as evaluate } from "../../evaluate-B1n1ti0N.mjs";
1
+ import { t as evaluate } from "../../evaluate-CeivW6G0.mjs";
2
2
  import { t as formatPrompt } from "../../types-ernB1Dy3.mjs";
3
3
  //#region src/adapters/gemini/index.ts
4
4
  /**
@@ -1,3 +1,3 @@
1
- import { a as cachePath, c as extractText, d as compactMarkdown, f as jaccardSimilar, i as cacheLookupSubstring, l as loadIndex, n as webfetchCacheWrite, o as cacheStore, p as queryHash, r as cacheLookup, s as mcpCacheKey, t as mcpCacheWrite, u as summarizeIndex } from "../mcp-store-CDUVqtJ0.mjs";
1
+ import { a as cachePath, c as extractText, d as compactMarkdown, f as jaccardSimilar, i as cacheLookupSubstring, l as loadIndex, n as webfetchCacheWrite, o as cacheStore, p as queryHash, r as cacheLookup, s as mcpCacheKey, t as mcpCacheWrite, u as summarizeIndex } from "../mcp-store-CnsW9oFj.mjs";
2
2
  import "../cache-la_KkjCS.mjs";
3
3
  export { cacheLookup, cacheLookupSubstring, cachePath, cacheStore, compactMarkdown, extractText, jaccardSimilar, loadIndex, mcpCacheKey, mcpCacheWrite, queryHash, summarizeIndex, webfetchCacheWrite };
@@ -1,4 +1,4 @@
1
- import { t as evaluate } from "./evaluate-B1n1ti0N.mjs";
1
+ import { t as evaluate } from "./evaluate-CeivW6G0.mjs";
2
2
  import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
3
3
  import { dirname, join } from "node:path";
4
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
package/dist/cli/bin.mjs CHANGED
@@ -2,12 +2,14 @@
2
2
  import { r as resolveTtlSec } from "../ttl-BG55s6HZ.mjs";
3
3
  import { r as loadDotenv } from "../dotenv-DGyLln7U.mjs";
4
4
  import { t as detectHarness } from "../harness-C8Nxxyn_.mjs";
5
- import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-D-Ydrw3D.mjs";
5
+ import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-jgivVDv6.mjs";
6
6
  import { n as writeInitFile, t as initFor } from "../run-Do2JltgU.mjs";
7
- import { Ot as todayUtc, kt as claudeHome, t as handleHook } from "../handle-VhWQxvyN.mjs";
8
- import { delimiter, join } from "node:path";
7
+ import { f as claudeHome } from "../store-CdWOQ9zD.mjs";
8
+ import { Ot as todayUtc, t as handleHook } from "../handle-BTHcKWQ5.mjs";
9
+ import { delimiter, dirname, join } from "node:path";
9
10
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
10
11
  import { homedir } from "node:os";
12
+ import { fileURLToPath } from "node:url";
11
13
  //#region src/changelog/fetch.ts
12
14
  /**
13
15
  * Changelog scanner — ports the changelog-watcher plugin's `fetch-changelog`
@@ -137,6 +139,90 @@ function discoverRefs(home, cwd, marketplaces) {
137
139
  return [...bySkill.values()].join(delimiter);
138
140
  }
139
141
  //#endregion
142
+ //#region src/cli/doctor.ts
143
+ /**
144
+ * `harness doctor` — diagnose which `@fusengine/harness` is actually running.
145
+ *
146
+ * A confirmed, still-open bun bug (oven-sh/bun #5791; scoped-pkg behaviour
147
+ * reinforced by #32019/#32150) makes `bunx <pkg>` (unpinned) prefer a stale
148
+ * GLOBAL install over npm-latest, so a consumer can silently run an old harness
149
+ * after a publish. This command surfaces the truth: the resolved version +
150
+ * package path of the code executing right now, the runtime binary, and the
151
+ * latest version published on npm. It queries the registry over HTTP (not
152
+ * `npm view`, whose exit code is 0 even on an empty result — npm/cli#6408) and
153
+ * never throws: an offline environment yields `latest: null`, never a crash.
154
+ */
155
+ const PKG = "@fusengine/harness";
156
+ /** Walk up from `startDir` for the `@fusengine/harness` `package.json`. */
157
+ function findPackage(startDir) {
158
+ let dir = startDir;
159
+ for (let depth = 0; depth < 6; depth++) {
160
+ try {
161
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
162
+ if (pkg.name === PKG) return {
163
+ version: pkg.version ?? "unknown",
164
+ path: dir
165
+ };
166
+ } catch {}
167
+ const parent = dirname(dir);
168
+ if (parent === dir) break;
169
+ dir = parent;
170
+ }
171
+ return null;
172
+ }
173
+ /** Resolve the running version + package path (no network), from a module URL. */
174
+ function runningVersion(moduleUrl) {
175
+ const found = findPackage(dirname(fileURLToPath(moduleUrl)));
176
+ return {
177
+ version: found?.version ?? "unknown",
178
+ path: found?.path ?? "unknown"
179
+ };
180
+ }
181
+ /** One-line `pkg vX.Y.Z` banner (written to stderr on every CLI invocation). */
182
+ function versionBanner(moduleUrl) {
183
+ return `${PKG} v${runningVersion(moduleUrl).version}`;
184
+ }
185
+ /** Latest published version via the npm registry HTTP API. `null` on any failure. */
186
+ async function npmLatest() {
187
+ try {
188
+ const res = await fetch(`https://registry.npmjs.org/${PKG}/latest`, { signal: AbortSignal.timeout(8e3) });
189
+ if (!res.ok) return null;
190
+ return (await res.json()).version ?? null;
191
+ } catch {
192
+ return null;
193
+ }
194
+ }
195
+ /** Build the full diagnostic report for the module at `moduleUrl`. */
196
+ async function buildDoctorReport(moduleUrl) {
197
+ const { version, path } = runningVersion(moduleUrl);
198
+ const latest = await npmLatest();
199
+ return {
200
+ running: version,
201
+ packagePath: path,
202
+ runtime: process.execPath,
203
+ latest,
204
+ stale: latest !== null && latest !== version
205
+ };
206
+ }
207
+ /** Render a {@link DoctorReport} as human-readable stdout text. */
208
+ function formatDoctor(r) {
209
+ const lines = [
210
+ `${PKG} doctor`,
211
+ ` running: ${r.running}`,
212
+ ` package: ${r.packagePath}`,
213
+ ` runtime: ${r.runtime}`,
214
+ ` npm latest: ${r.latest ?? "(unavailable — offline or unreachable)"}`
215
+ ];
216
+ if (r.stale) lines.push(` ! stale — npm serves ${r.latest}. Pin "@fusengine/harness@${r.latest}" in hooks.json (see README).`);
217
+ else if (r.latest !== null) lines.push(` ok — running the latest published version.`);
218
+ return lines.join("\n");
219
+ }
220
+ /** Run `harness doctor`: print the diagnostic to stdout. Always resolves 0 (pure info). */
221
+ async function runDoctor(moduleUrl) {
222
+ process.stdout.write(formatDoctor(await buildDoctorReport(moduleUrl)) + "\n");
223
+ return 0;
224
+ }
225
+ //#endregion
140
226
  //#region src/cli/bin.ts
141
227
  /**
142
228
  * harness — CLI for @fusengine/harness.
@@ -158,7 +244,12 @@ async function readStdin() {
158
244
  }
159
245
  }
160
246
  const cmd = process.argv[2];
161
- if (cmd === "hook") {
247
+ process.stderr.write(versionBanner(import.meta.url) + "\n");
248
+ if (cmd === "--version" || cmd === "-v") {
249
+ process.stdout.write(runningVersion(import.meta.url).version + "\n");
250
+ process.exit(0);
251
+ } else if (cmd === "doctor") process.exit(await runDoctor(import.meta.url));
252
+ else if (cmd === "hook") {
162
253
  const id = process.argv[3] ?? detectHarness().id;
163
254
  loadDotenv(id);
164
255
  const scopeArg = process.argv[4];
@@ -1,2 +1,2 @@
1
- import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-D-Ydrw3D.mjs";
1
+ import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-jgivVDv6.mjs";
2
2
  export { checkStaged, stagedContent, stagedFiles };
@@ -1,2 +1,2 @@
1
- import { _ as parseEnvInt, a as ProjectLayout, c as projectLayout, d as resolveMaxLines, f as splitTarget, g as ttlLabel, h as resolveTtlSec, i as parseEnvFile, l as DEFAULT_MAX_LINES, m as TTL_ENV_KEY, n as envCandidates, o as STATE_GITIGNORE, p as DEFAULT_TTL_SEC, r as loadDotenv, s as STATE_ROOT, t as HOME_DIR, u as MAX_LINES_ENV_KEY } from "../index-COBvvc3L.mjs";
1
+ import { _ as parseEnvInt, a as ProjectLayout, c as projectLayout, d as resolveMaxLines, f as splitTarget, g as ttlLabel, h as resolveTtlSec, i as parseEnvFile, l as DEFAULT_MAX_LINES, m as TTL_ENV_KEY, n as envCandidates, o as STATE_GITIGNORE, p as DEFAULT_TTL_SEC, r as loadDotenv, s as STATE_ROOT, t as HOME_DIR, u as MAX_LINES_ENV_KEY } from "../index-DXQfL1u8.mjs";
2
2
  export { DEFAULT_MAX_LINES, DEFAULT_TTL_SEC, HOME_DIR, MAX_LINES_ENV_KEY, ProjectLayout, STATE_GITIGNORE, STATE_ROOT, TTL_ENV_KEY, envCandidates, loadDotenv, parseEnvFile, parseEnvInt, projectLayout, resolveMaxLines, resolveTtlSec, splitTarget, ttlLabel };
@@ -1,2 +1,2 @@
1
- import { a as detectHarness, i as HarnessVia, n as HarnessInfo, o as detectMode, r as HarnessMode, s as modeFor, t as HarnessId } from "../harness-DwJskkz_.mjs";
2
- export { HarnessId, HarnessInfo, HarnessMode, HarnessVia, detectHarness, detectMode, modeFor };
1
+ import { a as HarnessInfo, i as HarnessId, n as detectMode, o as HarnessMode, r as modeFor, s as HarnessVia, t as detectHarness } from "../harness-BPPu5CrN.mjs";
2
+ export { type HarnessId, type HarnessInfo, type HarnessMode, type HarnessVia, detectHarness, detectMode, modeFor };
@@ -189,15 +189,15 @@ function securityGuard(ctx) {
189
189
  /**
190
190
  * Path fragments that mark a location as internal/generated state.
191
191
  *
192
- * Parity with safe_paths.py: `~/.claude/fusengine-cache` is a *writable* cache
193
- * the harness owns (lessons, MCP cache, per-type state) — only the
194
- * `fusengine-cache/sessions` subtree is protected, not the whole cache.
192
+ * Parity with safe_paths.py: `~/.fuse-harness/cache` is a *writable*
193
+ * cache the harness owns (lessons, MCP cache, per-type state) — only the
194
+ * `cache/sessions` subtree is protected, not the whole cache.
195
195
  */
196
196
  const PROTECTED_FRAGMENTS = [
197
197
  ".claude/plugins/marketplaces",
198
198
  ".claude/plugins/cache",
199
199
  ".claude/logs/00-apex",
200
- ".claude/fusengine-cache/sessions",
200
+ ".fuse-harness/cache/sessions",
201
201
  ".claude/apex/",
202
202
  ".harness/track",
203
203
  ".harness/memory/state"
@@ -1,3 +1,3 @@
1
1
  import { i as resolveSessions, n as formatDocSatisfactionStatus, r as isDocConsulted, t as formatDocDeny } from "../doc-helpers-BhzDmJ18.mjs";
2
- import { t as incrementTrivialEditCounter } from "../freshness-43gxYpiX.mjs";
2
+ import { t as incrementTrivialEditCounter } from "../freshness-BV3PQkDB.mjs";
3
3
  export { formatDocDeny, formatDocSatisfactionStatus, incrementTrivialEditCounter, isDocConsulted, resolveSessions };
@@ -1,4 +1,4 @@
1
- import { a as writeJsonFile, i as readJsonFile, n as ensureDir } from "./json-io-CvSumjtz.mjs";
1
+ import { a as writeJsonFile, i as readJsonFile, n as ensureDir } from "./json-io-DisYd2fb.mjs";
2
2
  import { dirname } from "node:path";
3
3
  //#region src/freshness/trivial-edit-counter.ts
4
4
  /**