@fusengine/harness 0.1.40 → 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.
- package/README.md +54 -4
- package/dist/adapters/claude/index.mjs +1 -1
- package/dist/adapters/cline/index.mjs +1 -1
- package/dist/adapters/codex/index.mjs +1 -1
- package/dist/adapters/cursor/index.d.mts +4 -2
- package/dist/adapters/cursor/index.mjs +1 -1
- package/dist/adapters/gemini/index.mjs +1 -1
- package/dist/cache/index.mjs +1 -1
- package/dist/{claude-BnFrSfMo.mjs → claude-CeRYMOaG.mjs} +1 -1
- package/dist/cli/bin.mjs +160 -6
- package/dist/cli/index.mjs +1 -1
- package/dist/config/index.d.mts +1 -1
- package/dist/detect/index.d.mts +2 -2
- package/dist/{doc-helpers-B4XYL1v8.mjs → doc-helpers-BhzDmJ18.mjs} +2 -2
- package/dist/{evaluate-B1n1ti0N.mjs → evaluate-CeivW6G0.mjs} +4 -4
- package/dist/freshness/index.mjs +2 -2
- package/dist/{freshness-43gxYpiX.mjs → freshness-BV3PQkDB.mjs} +1 -1
- package/dist/{handle-gYSq5znf.mjs → handle-BTHcKWQ5.mjs} +892 -882
- package/dist/{harness-DwJskkz_.d.mts → harness-BPPu5CrN.d.mts} +4 -8
- package/dist/{index-DTrjSNmI.d.mts → index-DN4cZDbU.d.mts} +7 -5
- package/dist/{index-COBvvc3L.d.mts → index-DXQfL1u8.d.mts} +1 -1
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +7 -7
- package/dist/init/index.d.mts +1 -1
- package/dist/{mcp-store-CDUVqtJ0.mjs → mcp-store-CnsW9oFj.mjs} +1 -1
- package/dist/memory/index.mjs +1 -1
- package/dist/policy/index.d.mts +2 -2
- package/dist/policy/index.mjs +2 -2
- package/dist/{registry-BkoEbdec.mjs → registry-CymilZiZ.mjs} +3 -2
- package/dist/{run-D-Ydrw3D.mjs → run-jgivVDv6.mjs} +1 -1
- package/dist/runtime/index.d.mts +14 -13
- package/dist/runtime/index.mjs +3 -2
- package/dist/state/index.mjs +1 -1
- package/dist/{state-BthKK4Jj.mjs → state-9lWvvWk8.mjs} +1 -1
- package/dist/{store-3WBr37Xz.mjs → store-CdWOQ9zD.mjs} +50 -3
- package/dist/tracking/index.mjs +1 -1
- package/dist/{validate-BCRqp9XG.mjs → validate-xi-zc-22.mjs} +2 -2
- package/package.json +1 -1
- package/dist/{json-io-CvSumjtz.mjs → json-io-DisYd2fb.mjs} +1 -1
package/README.md
CHANGED
|
@@ -26,7 +26,9 @@ bun add @fusengine/harness # Bun reads the TS source directly — no build
|
|
|
26
26
|
```sh
|
|
27
27
|
cd your-project
|
|
28
28
|
harness init # detects the harness, writes its pre+post hooks
|
|
29
|
-
|
|
29
|
+
# The SOLID-read gate auto-activates from discovered skills (default marketplace: fusengine-plugins).
|
|
30
|
+
export FUSE_HARNESS_MARKETPLACES=fusengine-plugins # (optional) which marketplaces to auto-scan
|
|
31
|
+
export FUSE_HARNESS_REFS=.claude/skills # (optional) explicit refs dir, overrides auto-discovery
|
|
30
32
|
```
|
|
31
33
|
|
|
32
34
|
That's it. `init` writes the wiring file for the detected harness
|
|
@@ -43,6 +45,11 @@ automatically under `<project>/.harness/` (track, cache, memory).
|
|
|
43
45
|
| `harness init [id]` | Write the pre+post hook wiring for the detected (or named) harness. |
|
|
44
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.) |
|
|
45
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.
|
|
46
53
|
|
|
47
54
|
cli-mode (Aider / Windsurf / OpenHands), as a pre-commit step:
|
|
48
55
|
|
|
@@ -51,6 +58,48 @@ cli-mode (Aider / Windsurf / OpenHands), as a pre-commit step:
|
|
|
51
58
|
npx harness check
|
|
52
59
|
```
|
|
53
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
|
+
|
|
54
103
|
## What it enforces
|
|
55
104
|
|
|
56
105
|
Ten portable guards + the APEX gate chain, all evaluated before a tool runs:
|
|
@@ -65,8 +114,8 @@ Ten portable guards + the APEX gate chain, all evaluated before a tool runs:
|
|
|
65
114
|
| interface-separation | top-level interface/type/protocol in a component/controller |
|
|
66
115
|
| protected-path | edits to `.claude/plugins\|logs\|cache`, `.git/` |
|
|
67
116
|
| APEX freshness | `explore-codebase` + `research-expert` not run within the window |
|
|
68
|
-
| APEX doc-consulted | Context7
|
|
69
|
-
| APEX solid-read | required SOLID refs (
|
|
117
|
+
| APEX doc-consulted | no doc source (Context7 / Exa / fuse-browser / WebSearch / WebFetch) consulted this session |
|
|
118
|
+
| APEX solid-read | required SOLID refs (auto-discovered, or `FUSE_HARNESS_REFS`) not read |
|
|
70
119
|
| brainstorm | creating a new file without brainstorming (when flagged) |
|
|
71
120
|
| MCP verbosity / cache | caps exa `numResults`; serves a fresh cached MCP/WebFetch result |
|
|
72
121
|
|
|
@@ -78,7 +127,8 @@ through per window without the full APEX gates.
|
|
|
78
127
|
| Var | Effect |
|
|
79
128
|
|---|---|
|
|
80
129
|
| `FUSE_SOLID_MAX_LINES` | SOLID file-size limit (default `100`). |
|
|
81
|
-
| `FUSE_HARNESS_REFS` |
|
|
130
|
+
| `FUSE_HARNESS_REFS` | Explicit `path.delimiter`-list of `.md` SOLID-reference dirs → activates `solidReadGate`. Overrides auto-discovery. |
|
|
131
|
+
| `FUSE_HARNESS_MARKETPLACES` | Comma-list of marketplace names whose `solid-*` skill refs are auto-discovered when `FUSE_HARNESS_REFS` is unset (default `fusengine-plugins`; an absent marketplace contributes nothing). Standalone `.claude`/`.codex`/`.cursor`/`.agents` skills are always scanned. |
|
|
82
132
|
| `FUSE_ENFORCE_TTL_SEC` | APEX freshness window in seconds. |
|
|
83
133
|
| `FUSE_LESSONS_THROTTLE_MIN` | Lessons-injection throttle (memory module). |
|
|
84
134
|
|
|
@@ -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-
|
|
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,2 +1,2 @@
|
|
|
1
|
-
import { a as guard, n as contextResponse, o as readClaudeInput, r as denyResponse } from "../../claude-
|
|
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/
|
|
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 };
|
package/dist/cache/index.mjs
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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 {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
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";
|
|
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`
|
|
@@ -76,6 +78,151 @@ async function scanChangelog(now = Date.now(), home = homedir()) {
|
|
|
76
78
|
};
|
|
77
79
|
}
|
|
78
80
|
//#endregion
|
|
81
|
+
//#region src/refs/discover.ts
|
|
82
|
+
/** Immediate subdirectory names of `p`, or `[]` when it is missing/inaccessible. */
|
|
83
|
+
function subdirs(p) {
|
|
84
|
+
try {
|
|
85
|
+
return readdirSync(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
86
|
+
} catch {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Every `skills/` parent dir across the Open-Agent-Skills layouts
|
|
92
|
+
* (agentskills.io) of Claude Code, Codex and Cursor: the standalone skill roots
|
|
93
|
+
* (`<base>/{.claude,.codex,.cursor,.agents}/skills` + `/etc/codex/skills`) plus
|
|
94
|
+
* installed-plugin roots (`<plugin>/skills` under the Claude marketplace and the
|
|
95
|
+
* Claude/Codex version caches). Shallow `readdir` only — no deep walk — so it is
|
|
96
|
+
* cheap enough to run on every hook.
|
|
97
|
+
*/
|
|
98
|
+
function skillParents(home, cwd, marketplaces) {
|
|
99
|
+
const out = [];
|
|
100
|
+
for (const base of [cwd, home]) for (const tool of [
|
|
101
|
+
".claude",
|
|
102
|
+
".codex",
|
|
103
|
+
".cursor",
|
|
104
|
+
".agents"
|
|
105
|
+
]) out.push(join(base, tool, "skills"));
|
|
106
|
+
out.push(join("/etc", "codex", "skills"));
|
|
107
|
+
const mktRoot = join(home, ".claude", "plugins", "marketplaces");
|
|
108
|
+
for (const mkt of subdirs(mktRoot)) {
|
|
109
|
+
if (!marketplaces.includes(mkt)) continue;
|
|
110
|
+
const pluginsDir = join(mktRoot, mkt, "plugins");
|
|
111
|
+
for (const plugin of subdirs(pluginsDir)) out.push(join(pluginsDir, plugin, "skills"));
|
|
112
|
+
}
|
|
113
|
+
for (const cacheRoot of [join(home, ".claude", "plugins", "cache"), join(home, ".codex", "plugins", "cache")]) for (const mkt of subdirs(cacheRoot)) {
|
|
114
|
+
if (!marketplaces.includes(mkt)) continue;
|
|
115
|
+
for (const plugin of subdirs(join(cacheRoot, mkt))) for (const ver of subdirs(join(cacheRoot, mkt, plugin))) out.push(join(cacheRoot, mkt, plugin, ver, "skills"));
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Auto-discover SOLID reference dirs (`<skill>/references` for every `solid-*`
|
|
121
|
+
* skill) across the Claude/Codex/Cursor skill layouts — the fallback used when
|
|
122
|
+
* `FUSE_HARNESS_REFS` is unset. Deduped by skill name (first source wins, so a
|
|
123
|
+
* marketplace skill shadows its version-cache copy). Returns a path-delimiter
|
|
124
|
+
* list, or `""` when none is found — with no refs the SOLID-read gate stays off.
|
|
125
|
+
* @param home - User home dir (`os.homedir()`).
|
|
126
|
+
* @param cwd - Current project dir (`process.cwd()`).
|
|
127
|
+
* @param marketplaces - Allowlist of marketplace names to scan (env
|
|
128
|
+
* `FUSE_HARNESS_MARKETPLACES`, default `["fusengine-plugins"]`); a marketplace
|
|
129
|
+
* absent from disk simply contributes nothing. Standalone
|
|
130
|
+
* `.claude`/`.codex`/`.cursor`/`.agents` skill roots are always scanned.
|
|
131
|
+
* @returns A `path.delimiter`-joined list of `references` dirs, or `""`.
|
|
132
|
+
*/
|
|
133
|
+
function discoverRefs(home, cwd, marketplaces) {
|
|
134
|
+
const bySkill = /* @__PURE__ */ new Map();
|
|
135
|
+
for (const parent of skillParents(home, cwd, marketplaces)) for (const skill of subdirs(parent)) {
|
|
136
|
+
if (!skill.startsWith("solid-") || bySkill.has(skill)) continue;
|
|
137
|
+
if (subdirs(join(parent, skill)).includes("references")) bySkill.set(skill, join(parent, skill, "references"));
|
|
138
|
+
}
|
|
139
|
+
return [...bySkill.values()].join(delimiter);
|
|
140
|
+
}
|
|
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
|
|
79
226
|
//#region src/cli/bin.ts
|
|
80
227
|
/**
|
|
81
228
|
* harness — CLI for @fusengine/harness.
|
|
@@ -97,7 +244,12 @@ async function readStdin() {
|
|
|
97
244
|
}
|
|
98
245
|
}
|
|
99
246
|
const cmd = process.argv[2];
|
|
100
|
-
|
|
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") {
|
|
101
253
|
const id = process.argv[3] ?? detectHarness().id;
|
|
102
254
|
loadDotenv(id);
|
|
103
255
|
const scopeArg = process.argv[4];
|
|
@@ -112,10 +264,12 @@ if (cmd === "hook") {
|
|
|
112
264
|
"seo",
|
|
113
265
|
"memory"
|
|
114
266
|
])).has(scopeArg) ? scopeArg : "core";
|
|
267
|
+
const marketplaces = (process.env.FUSE_HARNESS_MARKETPLACES ?? "fusengine-plugins").split(",").map((s) => s.trim()).filter(Boolean);
|
|
268
|
+
const refsDir = process.env.FUSE_HARNESS_REFS || discoverRefs(homedir(), process.cwd(), marketplaces) || void 0;
|
|
115
269
|
const outcome = await handleHook(id, await readStdin(), {
|
|
116
270
|
now: Date.now(),
|
|
117
271
|
cwd: process.cwd(),
|
|
118
|
-
refsDir
|
|
272
|
+
refsDir,
|
|
119
273
|
windowMs: resolveTtlSec(process.env) * 1e3,
|
|
120
274
|
scope
|
|
121
275
|
});
|
package/dist/cli/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-
|
|
1
|
+
import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-jgivVDv6.mjs";
|
|
2
2
|
export { checkStaged, stagedContent, stagedFiles };
|
package/dist/config/index.d.mts
CHANGED
|
@@ -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-
|
|
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 };
|
package/dist/detect/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
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 };
|
|
@@ -13,7 +13,7 @@ function evaluateDoc(auths) {
|
|
|
13
13
|
const readPaths = auths.flatMap((a) => a.read_paths ?? []);
|
|
14
14
|
const liveC7 = sources.some((s) => /context7/i.test(s));
|
|
15
15
|
const liveExa = sources.some((s) => /exa/i.test(s));
|
|
16
|
-
const liveWeb = sources.some((s) => /web(search|fetch)/i.test(s));
|
|
16
|
+
const liveWeb = sources.some((s) => /web(search|fetch)|fuse-browser/i.test(s));
|
|
17
17
|
const cacheC7 = readPaths.some((p) => /\/context\/mcp\/context7-/.test(p));
|
|
18
18
|
const cacheExa = readPaths.some((p) => /\/context\/mcp\/(exa-search|exa-code-context)-/.test(p));
|
|
19
19
|
return {
|
|
@@ -36,7 +36,7 @@ function formatDocSatisfactionStatus(authorizations, sessionId) {
|
|
|
36
36
|
function formatDocDeny(framework) {
|
|
37
37
|
return [
|
|
38
38
|
`APEX: Online documentation not consulted for ${framework}!`,
|
|
39
|
-
"Use ANY ONE of: mcp__context7__query-docs, mcp__exa__web_search_exa, WebSearch, or WebFetch.",
|
|
39
|
+
"Use ANY ONE of: mcp__context7__query-docs, mcp__exa__web_search_exa (or — if Exa is down — mcp__fuse-browser__browser_fetch / browser_crawl / browser_serp_batch), WebSearch, or WebFetch.",
|
|
40
40
|
"This check is once per session — after consulting one source, Write/Edit will be allowed."
|
|
41
41
|
].join("\n");
|
|
42
42
|
}
|
|
@@ -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: `~/.
|
|
193
|
-
* the harness owns (lessons, MCP cache, per-type state) — only the
|
|
194
|
-
* `
|
|
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
|
-
".
|
|
200
|
+
".fuse-harness/cache/sessions",
|
|
201
201
|
".claude/apex/",
|
|
202
202
|
".harness/track",
|
|
203
203
|
".harness/memory/state"
|
package/dist/freshness/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { i as resolveSessions, n as formatDocSatisfactionStatus, r as isDocConsulted, t as formatDocDeny } from "../doc-helpers-
|
|
2
|
-
import { t as incrementTrivialEditCounter } from "../freshness-
|
|
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-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-
|
|
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
|
/**
|