@fusengine/harness 0.1.4 → 0.1.5

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.
@@ -1,37 +1,5 @@
1
1
  import { r as isDocConsulted, t as formatDocDeny } from "./doc-helpers-Dd_x1-tZ.mjs";
2
2
  import { t as routeReferences } from "./router-Dj3AfgBE.mjs";
3
- import { existsSync } from "node:fs";
4
- import { join } from "node:path";
5
- //#region src/policy/detect-project.ts
6
- /** Keywords that signal a development task (APEX trigger). */
7
- const DEV_KEYWORDS = /\b(implement|create|build|fix|add|refactor|develop|feature|bug|update|modify|change|write|code)\b/i;
8
- /** True when the prompt invokes the /apex command. */
9
- function isApexCommand(prompt) {
10
- return /(?:^|\s)\/apex|\/fuse-ai-pilot:apex/i.test(prompt);
11
- }
12
- /** Detect the project type by scanning config files in `dir`. */
13
- function detectProjectType(dir) {
14
- const has = (f) => existsSync(join(dir, f));
15
- if (has("next.config.js") || has("next.config.ts") || has("next.config.mjs")) return "nextjs";
16
- if (has("nuxt.config.ts") || has("nuxt.config.js")) return "nuxt";
17
- if (has("angular.json")) return "angular";
18
- if (has("svelte.config.js") || has("svelte.config.ts")) return "svelte";
19
- if (has("vite.config.ts") && has("src/App.vue")) return "vue";
20
- if (has("vite.config.ts") || has("vite.config.js")) return "react";
21
- if (has("tailwind.config.js") || has("tailwind.config.ts")) return "tailwind";
22
- if (has("composer.json") && has("artisan")) return "laravel";
23
- if (has("Gemfile") && has("config/routes.rb")) return "rails";
24
- if (has("requirements.txt") || has("pyproject.toml") || has("setup.py")) return has("manage.py") ? "django" : "python";
25
- if (has("go.mod")) return "go";
26
- if (has("Cargo.toml")) return "rust";
27
- if (has("Package.swift")) return "swift";
28
- if (has("pom.xml") || has("build.gradle") || has("build.gradle.kts")) return "java";
29
- if (has("build.sbt")) return "scala";
30
- if (has("mix.exs")) return "elixir";
31
- if (has("Gemfile")) return "ruby";
32
- return "generic";
33
- }
34
- //#endregion
35
3
  //#region src/policy/apex.ts
36
4
  /** Gate: Context7 + Exa must have been consulted this session. */
37
5
  const docConsultedGate = (ctx) => isDocConsulted(ctx.authorizations, ctx.sessionId) ? null : {
@@ -76,4 +44,4 @@ function evaluateApex(ctx, gates = APEX_GATES) {
76
44
  return gates.reduce((hit, gate) => hit ?? gate(ctx), null);
77
45
  }
78
46
  //#endregion
79
- export { solidReadGate as a, isApexCommand as c, freshnessGate as i, docConsultedGate as n, DEV_KEYWORDS as o, evaluateApex as r, detectProjectType as s, APEX_GATES as t };
47
+ export { solidReadGate as a, freshnessGate as i, docConsultedGate as n, evaluateApex as r, APEX_GATES as t };
package/dist/index.mjs CHANGED
@@ -3,10 +3,11 @@ import { i as ttlLabel, n as TTL_ENV_KEY, r as resolveTtlSec, t as DEFAULT_TTL_S
3
3
  import { t as compactJson } from "./compact-json-DK2nX-MK.mjs";
4
4
  import { n as projectRoot, r as projectRootOrNull, t as isCodeFile } from "./project-root-C4ks_q1G.mjs";
5
5
  import { n as detectMode, r as modeFor, t as detectHarness } from "./harness-C8Nxxyn_.mjs";
6
- import { a as solidReadGate, c as isApexCommand, i as freshnessGate, n as docConsultedGate, o as DEV_KEYWORDS, r as evaluateApex, s as detectProjectType, t as APEX_GATES } from "./policy-C4zmyZR-.mjs";
6
+ import { n as detectProjectType, r as isApexCommand, t as DEV_KEYWORDS } from "./policy-C_pXmeNB.mjs";
7
7
  import { a as SYSTEM_INSTALL, c as evaluateFileSize, i as PROJECT_INSTALL, l as detectFramework, n as GIT_ASK, o as matchPatterns, r as GIT_BLOCKED, s as countLines, t as evaluate } from "./evaluate-CsYyUucy.mjs";
8
8
  import { i as resolveSessions, n as formatDocSatisfactionStatus, r as isDocConsulted, t as formatDocDeny } from "./doc-helpers-Dd_x1-tZ.mjs";
9
9
  import { i as parseFrontmatter, n as scoreReferences, r as globToRe, t as routeReferences } from "./router-Dj3AfgBE.mjs";
10
+ import { a as solidReadGate, i as freshnessGate, n as docConsultedGate, r as evaluateApex, t as APEX_GATES } from "./apex-gGrHzvM2.mjs";
10
11
  import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
11
12
  import { a as readState, c as throttleMs, i as nowStamp, l as ensureMemoryGitignore, n as readRoots, o as setStateField, r as registryFile, s as stateFileFor, t as addRoot } from "./memory-BVNt4Ary.mjs";
12
13
  import { a as jaccardSimilar, i as compactMarkdown, n as loadIndex, o as queryHash, r as summarizeIndex, t as extractText } from "./cache-DbPSJ9bC.mjs";
@@ -1,3 +1,4 @@
1
- import { a as solidReadGate, c as isApexCommand, i as freshnessGate, n as docConsultedGate, o as DEV_KEYWORDS, r as evaluateApex, s as detectProjectType, t as APEX_GATES } from "../policy-C4zmyZR-.mjs";
1
+ import { n as detectProjectType, r as isApexCommand, t as DEV_KEYWORDS } from "../policy-C_pXmeNB.mjs";
2
2
  import { a as SYSTEM_INSTALL, c as evaluateFileSize, i as PROJECT_INSTALL, l as detectFramework, n as GIT_ASK, o as matchPatterns, r as GIT_BLOCKED, s as countLines, t as evaluate } from "../evaluate-CsYyUucy.mjs";
3
+ import { a as solidReadGate, i as freshnessGate, n as docConsultedGate, r as evaluateApex, t as APEX_GATES } from "../apex-gGrHzvM2.mjs";
3
4
  export { APEX_GATES, DEV_KEYWORDS, GIT_ASK, GIT_BLOCKED, PROJECT_INSTALL, SYSTEM_INSTALL, countLines, detectFramework, detectProjectType, docConsultedGate, evaluate, evaluateApex, evaluateFileSize, freshnessGate, isApexCommand, matchPatterns, solidReadGate };
@@ -0,0 +1,33 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ //#region src/policy/detect-project.ts
4
+ /** Keywords that signal a development task (APEX trigger). */
5
+ const DEV_KEYWORDS = /\b(implement|create|build|fix|add|refactor|develop|feature|bug|update|modify|change|write|code)\b/i;
6
+ /** True when the prompt invokes the /apex command. */
7
+ function isApexCommand(prompt) {
8
+ return /(?:^|\s)\/apex|\/fuse-ai-pilot:apex/i.test(prompt);
9
+ }
10
+ /** Detect the project type by scanning config files in `dir`. */
11
+ function detectProjectType(dir) {
12
+ const has = (f) => existsSync(join(dir, f));
13
+ if (has("next.config.js") || has("next.config.ts") || has("next.config.mjs")) return "nextjs";
14
+ if (has("nuxt.config.ts") || has("nuxt.config.js")) return "nuxt";
15
+ if (has("angular.json")) return "angular";
16
+ if (has("svelte.config.js") || has("svelte.config.ts")) return "svelte";
17
+ if (has("vite.config.ts") && has("src/App.vue")) return "vue";
18
+ if (has("vite.config.ts") || has("vite.config.js")) return "react";
19
+ if (has("tailwind.config.js") || has("tailwind.config.ts")) return "tailwind";
20
+ if (has("composer.json") && has("artisan")) return "laravel";
21
+ if (has("Gemfile") && has("config/routes.rb")) return "rails";
22
+ if (has("requirements.txt") || has("pyproject.toml") || has("setup.py")) return has("manage.py") ? "django" : "python";
23
+ if (has("go.mod")) return "go";
24
+ if (has("Cargo.toml")) return "rust";
25
+ if (has("Package.swift")) return "swift";
26
+ if (has("pom.xml") || has("build.gradle") || has("build.gradle.kts")) return "java";
27
+ if (has("build.sbt")) return "scala";
28
+ if (has("mix.exs")) return "elixir";
29
+ if (has("Gemfile")) return "ruby";
30
+ return "generic";
31
+ }
32
+ //#endregion
33
+ export { detectProjectType as n, isApexCommand as r, DEV_KEYWORDS as t };
@@ -0,0 +1,51 @@
1
+ import { t as Prompt } from "../types-D56jSgD9.mjs";
2
+ import { t as RefMeta } from "../types-CY5qT2X1.mjs";
3
+
4
+ //#region src/runtime/paths.d.ts
5
+ /** Path to a session's track file (under a per-tool base dir). */
6
+ declare function trackFile(sessionId: string, baseDir?: string): string;
7
+ //#endregion
8
+ //#region src/runtime/record.d.ts
9
+ /** A unit of session activity to record (discriminated union on `kind`). */
10
+ type Activity = {
11
+ kind: "agent";
12
+ name: string;
13
+ ts: number;
14
+ } | {
15
+ kind: "doc";
16
+ framework: string;
17
+ sessionId: string;
18
+ source: string;
19
+ } | {
20
+ kind: "ref";
21
+ path: string;
22
+ };
23
+ /** Apply an activity to a session's track and persist it (PostToolUse path). */
24
+ declare function recordActivity(file: string, activity: Activity): Promise<void>;
25
+ //#endregion
26
+ //#region src/runtime/gate.d.ts
27
+ /** Prior agents the freshness gate requires before a code edit. */
28
+ declare const REQUIRED_AGENTS: ReadonlyArray<string>;
29
+ /** Default freshness window for {@link REQUIRED_AGENTS} (4 min, the APEX TTL). */
30
+ declare const DEFAULT_WINDOW_MS = 24e4;
31
+ /** A tool-use to gate, plus the session pointers needed for the stateful gates. */
32
+ interface GateInput {
33
+ sessionId: string;
34
+ framework: string;
35
+ tool: string;
36
+ filePath?: string;
37
+ content?: string;
38
+ command?: string;
39
+ refs?: RefMeta[];
40
+ now: number;
41
+ trackFile: string;
42
+ windowMs?: number;
43
+ }
44
+ /**
45
+ * Full gate: the stateless guards (file-size, git) first, then the stateful
46
+ * APEX gates fed from the session track. Returns the first blocking prompt, or
47
+ * null to allow. APEX gates apply only to code edits (a `filePath`).
48
+ */
49
+ declare function gate(input: GateInput): Promise<Prompt | null>;
50
+ //#endregion
51
+ export { Activity, DEFAULT_WINDOW_MS, GateInput, REQUIRED_AGENTS, gate, recordActivity, trackFile };
@@ -0,0 +1,51 @@
1
+ import { t as evaluate } from "../evaluate-CsYyUucy.mjs";
2
+ import { r as evaluateApex } from "../apex-gGrHzvM2.mjs";
3
+ import { a as recordAgent, n as saveTrack, o as recordDoc, r as agentsFresh, s as recordRefRead, t as loadTrack } from "../store-BWvwnnf6.mjs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ //#region src/runtime/paths.ts
7
+ /** Path to a session's track file (under a per-tool base dir). */
8
+ function trackFile(sessionId, baseDir = join(tmpdir(), "fuse-harness")) {
9
+ return join(baseDir, `track-${sessionId.replace(/[^A-Za-z0-9_-]/g, "_") || "default"}.json`);
10
+ }
11
+ //#endregion
12
+ //#region src/runtime/record.ts
13
+ /** Apply an activity to a session's track and persist it (PostToolUse path). */
14
+ async function recordActivity(file, activity) {
15
+ const track = await loadTrack(file);
16
+ await saveTrack(file, activity.kind === "agent" ? recordAgent(track, activity.name, activity.ts) : activity.kind === "doc" ? recordDoc(track, activity.framework, activity.sessionId, activity.source) : recordRefRead(track, activity.path));
17
+ }
18
+ //#endregion
19
+ //#region src/runtime/gate.ts
20
+ /** Prior agents the freshness gate requires before a code edit. */
21
+ const REQUIRED_AGENTS = ["explore-codebase", "research-expert"];
22
+ /** Default freshness window for {@link REQUIRED_AGENTS} (4 min, the APEX TTL). */
23
+ const DEFAULT_WINDOW_MS = 24e4;
24
+ /**
25
+ * Full gate: the stateless guards (file-size, git) first, then the stateful
26
+ * APEX gates fed from the session track. Returns the first blocking prompt, or
27
+ * null to allow. APEX gates apply only to code edits (a `filePath`).
28
+ */
29
+ async function gate(input) {
30
+ const quick = evaluate({
31
+ tool: input.tool,
32
+ filePath: input.filePath,
33
+ content: input.content,
34
+ command: input.command
35
+ });
36
+ if (quick.decision !== "allow" && quick.prompt) return quick.prompt;
37
+ if (!input.filePath) return null;
38
+ const track = await loadTrack(input.trackFile);
39
+ return evaluateApex({
40
+ sessionId: input.sessionId,
41
+ framework: input.framework,
42
+ filePath: input.filePath,
43
+ content: input.content ?? "",
44
+ authorizations: track.authorizations,
45
+ refs: input.refs,
46
+ refsRead: track.refsRead,
47
+ agentsFresh: agentsFresh(track, [...REQUIRED_AGENTS], input.windowMs ?? 24e4, input.now)
48
+ });
49
+ }
50
+ //#endregion
51
+ export { DEFAULT_WINDOW_MS, REQUIRED_AGENTS, gate, recordActivity, trackFile };
@@ -0,0 +1,63 @@
1
+ import { n as readJsonFile, r as writeJsonFile } from "./json-io-RH82El2J.mjs";
2
+ //#region src/tracking/session-state.ts
3
+ /** A fresh, empty track. */
4
+ function emptyTrack() {
5
+ return {
6
+ authorizations: {},
7
+ refsRead: [],
8
+ agents: []
9
+ };
10
+ }
11
+ /** Record a doc consultation (Context7/Exa) for a framework in this session. Immutable. */
12
+ function recordDoc(track, framework, sessionId, source) {
13
+ const prev = track.authorizations[framework] ?? {};
14
+ const sessions = new Set(prev.doc_sessions ?? []);
15
+ sessions.add(sessionId);
16
+ const sources = new Set(prev.sources ?? (prev.source ? [prev.source] : []));
17
+ sources.add(source);
18
+ return {
19
+ ...track,
20
+ authorizations: {
21
+ ...track.authorizations,
22
+ [framework]: {
23
+ ...prev,
24
+ doc_sessions: [...sessions],
25
+ sources: [...sources]
26
+ }
27
+ }
28
+ };
29
+ }
30
+ /** Record that a SOLID reference file was read (deduped). Immutable. */
31
+ function recordRefRead(track, path) {
32
+ return track.refsRead.includes(path) ? track : {
33
+ ...track,
34
+ refsRead: [...track.refsRead, path]
35
+ };
36
+ }
37
+ /** Record an agent/tool call with a timestamp. Immutable. */
38
+ function recordAgent(track, name, ts) {
39
+ return {
40
+ ...track,
41
+ agents: [...track.agents, {
42
+ name,
43
+ ts
44
+ }]
45
+ };
46
+ }
47
+ /** True when ALL of `names` were called within `windowMs` before `now`. */
48
+ function agentsFresh(track, names, windowMs, now) {
49
+ const cutoff = now - windowMs;
50
+ return names.every((n) => track.agents.some((a) => a.name === n && a.ts > cutoff));
51
+ }
52
+ //#endregion
53
+ //#region src/tracking/store.ts
54
+ /** Load a session track from a file (an empty track if absent/corrupt). */
55
+ async function loadTrack(file) {
56
+ return await readJsonFile(file) ?? emptyTrack();
57
+ }
58
+ /** Persist a session track. */
59
+ async function saveTrack(file, track) {
60
+ await writeJsonFile(file, track);
61
+ }
62
+ //#endregion
63
+ export { recordAgent as a, emptyTrack as i, saveTrack as n, recordDoc as o, agentsFresh as r, recordRefRead as s, loadTrack as t };
@@ -1,63 +1,2 @@
1
- import { n as readJsonFile, r as writeJsonFile } from "../json-io-RH82El2J.mjs";
2
- //#region src/tracking/session-state.ts
3
- /** A fresh, empty track. */
4
- function emptyTrack() {
5
- return {
6
- authorizations: {},
7
- refsRead: [],
8
- agents: []
9
- };
10
- }
11
- /** Record a doc consultation (Context7/Exa) for a framework in this session. Immutable. */
12
- function recordDoc(track, framework, sessionId, source) {
13
- const prev = track.authorizations[framework] ?? {};
14
- const sessions = new Set(prev.doc_sessions ?? []);
15
- sessions.add(sessionId);
16
- const sources = new Set(prev.sources ?? (prev.source ? [prev.source] : []));
17
- sources.add(source);
18
- return {
19
- ...track,
20
- authorizations: {
21
- ...track.authorizations,
22
- [framework]: {
23
- ...prev,
24
- doc_sessions: [...sessions],
25
- sources: [...sources]
26
- }
27
- }
28
- };
29
- }
30
- /** Record that a SOLID reference file was read (deduped). Immutable. */
31
- function recordRefRead(track, path) {
32
- return track.refsRead.includes(path) ? track : {
33
- ...track,
34
- refsRead: [...track.refsRead, path]
35
- };
36
- }
37
- /** Record an agent/tool call with a timestamp. Immutable. */
38
- function recordAgent(track, name, ts) {
39
- return {
40
- ...track,
41
- agents: [...track.agents, {
42
- name,
43
- ts
44
- }]
45
- };
46
- }
47
- /** True when ALL of `names` were called within `windowMs` before `now`. */
48
- function agentsFresh(track, names, windowMs, now) {
49
- const cutoff = now - windowMs;
50
- return names.every((n) => track.agents.some((a) => a.name === n && a.ts > cutoff));
51
- }
52
- //#endregion
53
- //#region src/tracking/store.ts
54
- /** Load a session track from a file (an empty track if absent/corrupt). */
55
- async function loadTrack(file) {
56
- return await readJsonFile(file) ?? emptyTrack();
57
- }
58
- /** Persist a session track. */
59
- async function saveTrack(file, track) {
60
- await writeJsonFile(file, track);
61
- }
62
- //#endregion
1
+ import { a as recordAgent, i as emptyTrack, n as saveTrack, o as recordDoc, r as agentsFresh, s as recordRefRead, t as loadTrack } from "../store-BWvwnnf6.mjs";
63
2
  export { agentsFresh, emptyTrack, loadTrack, recordAgent, recordDoc, recordRefRead, saveTrack };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fusengine/harness",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Harness-agnostic toolkit for AI coding agents: runtime harness detection (Claude Code, Codex, Cursor, Cline, Gemini, Aider...), pure policy core (env config, project/framework detection, SOLID/file-size limits, APEX freshness, guard patterns, portable prompts), cache, project memory, ref routing, state/locks, statusline, per-harness adapters (Claude/Cursor/Cline/Gemini) and a cli-mode harness-check binary. Bun-native, with a built dist for Node + bundlers.",
5
5
  "type": "module",
6
6
  "module": "src/index.ts",
@@ -24,6 +24,7 @@
24
24
  "./cli": { "bun": "./src/cli/index.ts", "types": "./dist/cli/index.d.mts", "import": "./dist/cli/index.mjs" },
25
25
  "./init": { "bun": "./src/init/index.ts", "types": "./dist/init/index.d.mts", "import": "./dist/init/index.mjs" },
26
26
  "./tracking": { "bun": "./src/tracking/index.ts", "types": "./dist/tracking/index.d.mts", "import": "./dist/tracking/index.mjs" },
27
+ "./runtime": { "bun": "./src/runtime/index.ts", "types": "./dist/runtime/index.d.mts", "import": "./dist/runtime/index.mjs" },
27
28
  "./adapters/claude": { "bun": "./src/adapters/claude/index.ts", "types": "./dist/adapters/claude/index.d.mts", "import": "./dist/adapters/claude/index.mjs" },
28
29
  "./adapters/codex": { "bun": "./src/adapters/codex/index.ts", "types": "./dist/adapters/codex/index.d.mts", "import": "./dist/adapters/codex/index.mjs" },
29
30
  "./adapters/cursor": { "bun": "./src/adapters/cursor/index.ts", "types": "./dist/adapters/cursor/index.d.mts", "import": "./dist/adapters/cursor/index.mjs" },
@@ -36,7 +37,7 @@
36
37
  "scripts": {
37
38
  "test": "bun test",
38
39
  "typecheck": "tsc --noEmit",
39
- "build": "tsdown src/index.ts src/config/index.ts src/util/index.ts src/detect/index.ts src/policy/index.ts src/prompt/index.ts src/memory/index.ts src/cache/index.ts src/freshness/index.ts src/refs/index.ts src/state/index.ts src/statusline/index.ts src/cli/index.ts src/cli/bin.ts src/init/index.ts src/tracking/index.ts src/adapters/claude/index.ts src/adapters/codex/index.ts src/adapters/cursor/index.ts src/adapters/cline/index.ts src/adapters/gemini/index.ts --dts --format esm --clean --out-dir dist",
40
+ "build": "tsdown src/index.ts src/config/index.ts src/util/index.ts src/detect/index.ts src/policy/index.ts src/prompt/index.ts src/memory/index.ts src/cache/index.ts src/freshness/index.ts src/refs/index.ts src/state/index.ts src/statusline/index.ts src/cli/index.ts src/cli/bin.ts src/init/index.ts src/tracking/index.ts src/runtime/index.ts src/adapters/claude/index.ts src/adapters/codex/index.ts src/adapters/cursor/index.ts src/adapters/cline/index.ts src/adapters/gemini/index.ts --dts --format esm --clean --out-dir dist",
40
41
  "prepublishOnly": "bun test && tsc --noEmit && bun run build"
41
42
  },
42
43
  "publishConfig": {