@fusengine/harness 0.1.1 → 0.1.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.
- package/dist/adapters/claude/index.d.mts +1 -35
- package/dist/adapters/claude/index.mjs +1 -65
- package/dist/adapters/cline/index.mjs +1 -24
- package/dist/adapters/codex/index.d.mts +2 -0
- package/dist/adapters/codex/index.mjs +2 -0
- package/dist/adapters/cursor/index.mjs +1 -36
- package/dist/adapters/gemini/index.mjs +1 -24
- package/dist/cache/index.d.mts +1 -29
- package/dist/cache/index.mjs +1 -109
- package/dist/cache-DbPSJ9bC.mjs +110 -0
- package/dist/claude-DbzjbxmO.mjs +66 -0
- package/dist/cli/bin.mjs +84 -10
- package/dist/cline-BxslHtBG.mjs +25 -0
- package/dist/config/index.d.mts +1 -36
- package/dist/config/index.mjs +1 -18
- package/dist/config-BG55s6HZ.mjs +20 -0
- package/dist/cursor-Bh7eh9y_.mjs +37 -0
- package/dist/detect/index.d.mts +1 -30
- package/dist/detect/index.mjs +2 -81
- package/dist/detect-la_KkjCS.mjs +1 -0
- package/dist/doc-helpers-Dd_x1-tZ.mjs +42 -0
- package/dist/freshness/index.d.mts +1 -12
- package/dist/freshness/index.mjs +2 -63
- package/dist/freshness-BK9Xg6oG.mjs +23 -0
- package/dist/gemini-SrK_fFAr.mjs +25 -0
- package/dist/harness-C8Nxxyn_.mjs +83 -0
- package/dist/harness-DwJskkz_.d.mts +31 -0
- package/dist/index-B-z0CCiU.d.mts +37 -0
- package/dist/index-B3Ve_bBu.d.mts +30 -0
- package/dist/index-BEM-mQMC.d.mts +30 -0
- package/dist/index-BKZ67WMa.d.mts +1 -0
- package/dist/index-BOBXQ91y.d.mts +12 -0
- package/dist/index-BVVgDSdq.d.mts +1 -0
- package/dist/index-BWK8slRi.d.mts +84 -0
- package/dist/index-BXBWKbL8.d.mts +114 -0
- package/dist/index-C1vLIMwN.d.mts +20 -0
- package/dist/index-C2Lz-cwJ.d.mts +21 -0
- package/dist/index-CPoF_hLP.d.mts +59 -0
- package/dist/index-FrWgmkbP.d.mts +36 -0
- package/dist/index.d.mts +12 -11
- package/dist/index.mjs +11 -12
- package/dist/init/index.d.mts +34 -0
- package/dist/init/index.mjs +2 -0
- package/dist/memory/index.d.mts +1 -29
- package/dist/memory/index.mjs +1 -93
- package/dist/memory-BVNt4Ary.mjs +94 -0
- package/dist/policy/index.d.mts +2 -80
- package/dist/policy/index.mjs +2 -33
- package/dist/policy-DMpyaPZ_.mjs +68 -0
- package/dist/prompt/index.mjs +1 -0
- package/dist/prompt-la_KkjCS.mjs +1 -0
- package/dist/refs/index.d.mts +2 -43
- package/dist/refs/index.mjs +2 -69
- package/dist/refs-la_KkjCS.mjs +1 -0
- package/dist/router-Dj3AfgBE.mjs +70 -0
- package/dist/run-Cc98348q.mjs +94 -0
- package/dist/state/index.d.mts +1 -58
- package/dist/state/index.mjs +1 -103
- package/dist/state-Bc4wdnCG.mjs +104 -0
- package/dist/statusline/index.d.mts +1 -83
- package/dist/statusline/index.mjs +1 -147
- package/dist/statusline-D87eUNXl.mjs +148 -0
- package/dist/types-CY5qT2X1.d.mts +26 -0
- package/dist/util/index.d.mts +1 -19
- package/dist/util/index.mjs +1 -0
- package/dist/util-la_KkjCS.mjs +1 -0
- package/package.json +5 -3
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
//#region src/memory/gitignore.ts
|
|
4
|
+
/** Ensure `<memoryDir>/.gitignore` ignores the machine-local `state.json`. */
|
|
5
|
+
function ensureMemoryGitignore(memoryDir) {
|
|
6
|
+
const file = `${memoryDir}/.gitignore`;
|
|
7
|
+
try {
|
|
8
|
+
const existing = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
9
|
+
if (/^state\.json$/m.test(existing)) return;
|
|
10
|
+
writeFileSync(file, existing.trim() ? `${existing.trimEnd()}\nstate.json\n` : "state.json\n");
|
|
11
|
+
} catch {}
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/memory/state.ts
|
|
15
|
+
/** Absolute path of the per-project throttle state file. */
|
|
16
|
+
function stateFileFor(root) {
|
|
17
|
+
return `${root}/MEMORY/state.json`;
|
|
18
|
+
}
|
|
19
|
+
function num(v) {
|
|
20
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
21
|
+
}
|
|
22
|
+
/** Read both throttle timestamps; missing/corrupt fields default to 0. */
|
|
23
|
+
function readState(file) {
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
26
|
+
return {
|
|
27
|
+
lastRemindedAt: num(parsed?.lastRemindedAt),
|
|
28
|
+
lastCodeEditAt: num(parsed?.lastCodeEditAt)
|
|
29
|
+
};
|
|
30
|
+
} catch {
|
|
31
|
+
return {
|
|
32
|
+
lastRemindedAt: 0,
|
|
33
|
+
lastCodeEditAt: 0
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Persist one field without clobbering the other (read-modify-write). */
|
|
38
|
+
function setStateField(file, key, value) {
|
|
39
|
+
const next = {
|
|
40
|
+
...readState(file),
|
|
41
|
+
[key]: value
|
|
42
|
+
};
|
|
43
|
+
const dir = dirname(file);
|
|
44
|
+
mkdirSync(dir, { recursive: true });
|
|
45
|
+
ensureMemoryGitignore(dir);
|
|
46
|
+
writeFileSync(file, JSON.stringify(next));
|
|
47
|
+
}
|
|
48
|
+
/** Local wall-clock timestamp for a lesson bullet: `YYYY-MM-DD HH:MM`. */
|
|
49
|
+
function nowStamp() {
|
|
50
|
+
const d = /* @__PURE__ */ new Date();
|
|
51
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
52
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
|
53
|
+
}
|
|
54
|
+
/** Throttle window (ms) from `FUSE_LESSONS_THROTTLE_MIN` (default 5 min). */
|
|
55
|
+
function throttleMs(env = process.env) {
|
|
56
|
+
const raw = env.FUSE_LESSONS_THROTTLE_MIN?.trim();
|
|
57
|
+
const min = raw ? Number(raw) : 5;
|
|
58
|
+
return (Number.isFinite(min) ? Math.max(0, min) : 5) * 6e4;
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/memory/registry.ts
|
|
62
|
+
/** Registry path relative to the home dir. */
|
|
63
|
+
const SUBPATH = ".claude/fusengine-cache/lessons/roots.json";
|
|
64
|
+
/** Absolute path of the global roots registry, or null when home is unusable. */
|
|
65
|
+
function registryFile(home = process.env.HOME) {
|
|
66
|
+
const h = home?.trim();
|
|
67
|
+
if (!h || !h.startsWith("/")) return null;
|
|
68
|
+
return `${h}/${SUBPATH}`;
|
|
69
|
+
}
|
|
70
|
+
/** Read the registered project roots (deduplicated string entries only). */
|
|
71
|
+
function readRoots(home) {
|
|
72
|
+
const file = registryFile(home);
|
|
73
|
+
if (!file) return [];
|
|
74
|
+
try {
|
|
75
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
76
|
+
return Array.isArray(parsed) ? parsed.filter((e) => typeof e === "string") : [];
|
|
77
|
+
} catch {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Register a project root once (deduplicated, read-modify-write, non-throwing). */
|
|
82
|
+
function addRoot(root, home) {
|
|
83
|
+
const file = registryFile(home);
|
|
84
|
+
if (!file) return;
|
|
85
|
+
try {
|
|
86
|
+
const roots = new Set(readRoots(home));
|
|
87
|
+
if (roots.has(root)) return;
|
|
88
|
+
roots.add(root);
|
|
89
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
90
|
+
writeFileSync(file, JSON.stringify([...roots]));
|
|
91
|
+
} catch {}
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
export { readState as a, throttleMs as c, nowStamp as i, ensureMemoryGitignore as l, readRoots as n, setStateField as o, registryFile as r, stateFileFor as s, addRoot as t };
|
package/dist/policy/index.d.mts
CHANGED
|
@@ -1,80 +1,2 @@
|
|
|
1
|
-
import { t as
|
|
2
|
-
|
|
3
|
-
//#region src/policy/detect-project.d.ts
|
|
4
|
-
/** Project types detected from filesystem indicators. */
|
|
5
|
-
type ProjectType = "nextjs" | "nuxt" | "angular" | "svelte" | "vue" | "react" | "tailwind" | "laravel" | "rails" | "django" | "python" | "go" | "rust" | "swift" | "java" | "scala" | "elixir" | "ruby" | "generic";
|
|
6
|
-
/** Keywords that signal a development task (APEX trigger). */
|
|
7
|
-
declare const DEV_KEYWORDS: RegExp;
|
|
8
|
-
/** True when the prompt invokes the /apex command. */
|
|
9
|
-
declare function isApexCommand(prompt: string): boolean;
|
|
10
|
-
/** Detect the project type by scanning config files in `dir`. */
|
|
11
|
-
declare function detectProjectType(dir: string): ProjectType;
|
|
12
|
-
//#endregion
|
|
13
|
-
//#region src/policy/detect-framework.d.ts
|
|
14
|
-
/**
|
|
15
|
-
* Detect the framework from a file path extension + content patterns.
|
|
16
|
-
* Aligned with the fusengine require-solid-read detection (distinct from
|
|
17
|
-
* {@link detectProjectType}, which scans config files on disk).
|
|
18
|
-
*/
|
|
19
|
-
declare function detectFramework(filePath: string, content: string): string;
|
|
20
|
-
//#endregion
|
|
21
|
-
//#region src/policy/file-size.d.ts
|
|
22
|
-
/** Verdict from {@link evaluateFileSize}. */
|
|
23
|
-
interface FileSizeVerdict {
|
|
24
|
-
ok: boolean;
|
|
25
|
-
lines: number;
|
|
26
|
-
max: number;
|
|
27
|
-
message: string | null;
|
|
28
|
-
}
|
|
29
|
-
/** Count lines in file content (empty string = 0). */
|
|
30
|
-
declare function countLines(content: string): number;
|
|
31
|
-
/**
|
|
32
|
-
* Evaluate a file's line count against the SOLID limit.
|
|
33
|
-
* @param lines - the file's line count
|
|
34
|
-
* @param max - the limit (defaults to `resolveMaxLines()`)
|
|
35
|
-
*/
|
|
36
|
-
declare function evaluateFileSize(lines: number, max?: number): FileSizeVerdict;
|
|
37
|
-
//#endregion
|
|
38
|
-
//#region src/policy/patterns.d.ts
|
|
39
|
-
/**
|
|
40
|
-
* Guard pattern data, ported verbatim from the fusengine git/install guards.
|
|
41
|
-
* Note (faithful): `git push.*--force` also matches `--force-with-lease` —
|
|
42
|
-
* preserved from the source guard.
|
|
43
|
-
*/
|
|
44
|
-
/** Destructive git operations to block outright. */
|
|
45
|
-
declare const GIT_BLOCKED: ReadonlyArray<RegExp>;
|
|
46
|
-
/** Git operations that warrant a confirmation prompt. */
|
|
47
|
-
declare const GIT_ASK: ReadonlyArray<RegExp>;
|
|
48
|
-
/** System-level package installs (need confirmation). */
|
|
49
|
-
declare const SYSTEM_INSTALL: ReadonlyArray<RegExp>;
|
|
50
|
-
/** Project-level package installs. */
|
|
51
|
-
declare const PROJECT_INSTALL: ReadonlyArray<RegExp>;
|
|
52
|
-
/** True when `cmd` matches any pattern in `patterns`. */
|
|
53
|
-
declare function matchPatterns(cmd: string, patterns: ReadonlyArray<RegExp>): boolean;
|
|
54
|
-
//#endregion
|
|
55
|
-
//#region src/policy/evaluate.d.ts
|
|
56
|
-
/** Harness-agnostic input to {@link evaluate}. */
|
|
57
|
-
interface PolicyContext {
|
|
58
|
-
/** Tool name (e.g. "Write", "Edit", "Bash"). */
|
|
59
|
-
tool: string;
|
|
60
|
-
filePath?: string;
|
|
61
|
-
content?: string;
|
|
62
|
-
command?: string;
|
|
63
|
-
/** Optional override for the SOLID max-lines limit. */
|
|
64
|
-
maxLines?: number;
|
|
65
|
-
}
|
|
66
|
-
/** Harness-agnostic policy decision (+ a portable prompt for adapters to render). */
|
|
67
|
-
interface PolicyResult {
|
|
68
|
-
decision: "allow" | "deny" | "warn";
|
|
69
|
-
message: string | null;
|
|
70
|
-
prompt?: Prompt;
|
|
71
|
-
meta?: Record<string, unknown>;
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Evaluate a single tool-use against the bundled policies, returning a pure
|
|
75
|
-
* decision plus a portable {@link Prompt}. Adapters translate the prompt into
|
|
76
|
-
* their harness's native response (Claude `permissionDecision`, etc.).
|
|
77
|
-
*/
|
|
78
|
-
declare function evaluate(ctx: PolicyContext): PolicyResult;
|
|
79
|
-
//#endregion
|
|
80
|
-
export { DEV_KEYWORDS, FileSizeVerdict, GIT_ASK, GIT_BLOCKED, PROJECT_INSTALL, PolicyContext, PolicyResult, ProjectType, SYSTEM_INSTALL, countLines, detectFramework, detectProjectType, evaluate, evaluateFileSize, isApexCommand, matchPatterns };
|
|
1
|
+
import { S as isApexCommand, _ as evaluateFileSize, a as evaluateApex, b as ProjectType, c as PolicyResult, d as GIT_BLOCKED, f as PROJECT_INSTALL, g as countLines, h as FileSizeVerdict, i as docConsultedGate, l as evaluate, m as matchPatterns, n as ApexContext, o as solidReadGate, p as SYSTEM_INSTALL, r as ApexGate, s as PolicyContext, t as APEX_GATES, u as GIT_ASK, v as detectFramework, x as detectProjectType, y as DEV_KEYWORDS } from "../index-BXBWKbL8.mjs";
|
|
2
|
+
export { APEX_GATES, ApexContext, ApexGate, DEV_KEYWORDS, FileSizeVerdict, GIT_ASK, GIT_BLOCKED, PROJECT_INSTALL, PolicyContext, PolicyResult, ProjectType, SYSTEM_INSTALL, countLines, detectFramework, detectProjectType, docConsultedGate, evaluate, evaluateApex, evaluateFileSize, isApexCommand, matchPatterns, solidReadGate };
|
package/dist/policy/index.mjs
CHANGED
|
@@ -1,34 +1,3 @@
|
|
|
1
|
+
import { a as DEV_KEYWORDS, i as solidReadGate, n as docConsultedGate, o as detectProjectType, r as evaluateApex, s as isApexCommand, t as APEX_GATES } from "../policy-DMpyaPZ_.mjs";
|
|
1
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";
|
|
2
|
-
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
//#region src/policy/detect-project.ts
|
|
5
|
-
/** Keywords that signal a development task (APEX trigger). */
|
|
6
|
-
const DEV_KEYWORDS = /\b(implement|create|build|fix|add|refactor|develop|feature|bug|update|modify|change|write|code)\b/i;
|
|
7
|
-
/** True when the prompt invokes the /apex command. */
|
|
8
|
-
function isApexCommand(prompt) {
|
|
9
|
-
return /(?:^|\s)\/apex|\/fuse-ai-pilot:apex/i.test(prompt);
|
|
10
|
-
}
|
|
11
|
-
/** Detect the project type by scanning config files in `dir`. */
|
|
12
|
-
function detectProjectType(dir) {
|
|
13
|
-
const has = (f) => existsSync(join(dir, f));
|
|
14
|
-
if (has("next.config.js") || has("next.config.ts") || has("next.config.mjs")) return "nextjs";
|
|
15
|
-
if (has("nuxt.config.ts") || has("nuxt.config.js")) return "nuxt";
|
|
16
|
-
if (has("angular.json")) return "angular";
|
|
17
|
-
if (has("svelte.config.js") || has("svelte.config.ts")) return "svelte";
|
|
18
|
-
if (has("vite.config.ts") && has("src/App.vue")) return "vue";
|
|
19
|
-
if (has("vite.config.ts") || has("vite.config.js")) return "react";
|
|
20
|
-
if (has("tailwind.config.js") || has("tailwind.config.ts")) return "tailwind";
|
|
21
|
-
if (has("composer.json") && has("artisan")) return "laravel";
|
|
22
|
-
if (has("Gemfile") && has("config/routes.rb")) return "rails";
|
|
23
|
-
if (has("requirements.txt") || has("pyproject.toml") || has("setup.py")) return has("manage.py") ? "django" : "python";
|
|
24
|
-
if (has("go.mod")) return "go";
|
|
25
|
-
if (has("Cargo.toml")) return "rust";
|
|
26
|
-
if (has("Package.swift")) return "swift";
|
|
27
|
-
if (has("pom.xml") || has("build.gradle") || has("build.gradle.kts")) return "java";
|
|
28
|
-
if (has("build.sbt")) return "scala";
|
|
29
|
-
if (has("mix.exs")) return "elixir";
|
|
30
|
-
if (has("Gemfile")) return "ruby";
|
|
31
|
-
return "generic";
|
|
32
|
-
}
|
|
33
|
-
//#endregion
|
|
34
|
-
export { DEV_KEYWORDS, GIT_ASK, GIT_BLOCKED, PROJECT_INSTALL, SYSTEM_INSTALL, countLines, detectFramework, detectProjectType, evaluate, evaluateFileSize, isApexCommand, matchPatterns };
|
|
3
|
+
export { APEX_GATES, DEV_KEYWORDS, GIT_ASK, GIT_BLOCKED, PROJECT_INSTALL, SYSTEM_INSTALL, countLines, detectFramework, detectProjectType, docConsultedGate, evaluate, evaluateApex, evaluateFileSize, isApexCommand, matchPatterns, solidReadGate };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { r as isDocConsulted, t as formatDocDeny } from "./doc-helpers-Dd_x1-tZ.mjs";
|
|
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
|
+
//#region src/policy/apex.ts
|
|
36
|
+
/** Gate: Context7 + Exa must have been consulted this session. */
|
|
37
|
+
const docConsultedGate = (ctx) => isDocConsulted(ctx.authorizations, ctx.sessionId) ? null : {
|
|
38
|
+
kind: "block",
|
|
39
|
+
title: "APEX: documentation not consulted",
|
|
40
|
+
reason: formatDocDeny(ctx.framework),
|
|
41
|
+
actions: ["Call mcp__context7__query-docs", "Call mcp__exa__web_search_exa"]
|
|
42
|
+
};
|
|
43
|
+
/** Gate: the routed SOLID references for this edit must have been read. */
|
|
44
|
+
const solidReadGate = (ctx) => {
|
|
45
|
+
if (!ctx.refs?.length) return null;
|
|
46
|
+
const routed = routeReferences(ctx.refs, ctx.filePath, ctx.content);
|
|
47
|
+
if (!routed) return null;
|
|
48
|
+
const read = new Set(ctx.refsRead ?? []);
|
|
49
|
+
const missing = routed.required.map((r) => r.meta.filePath).filter((p) => !read.has(p));
|
|
50
|
+
if (missing.length === 0) return null;
|
|
51
|
+
return {
|
|
52
|
+
kind: "block",
|
|
53
|
+
title: `APEX: read SOLID references for ${ctx.framework}`,
|
|
54
|
+
reason: `Read these before editing ${ctx.filePath}:`,
|
|
55
|
+
actions: missing
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
/** Default APEX gate chain. */
|
|
59
|
+
const APEX_GATES = [docConsultedGate, solidReadGate];
|
|
60
|
+
/**
|
|
61
|
+
* Run the APEX gates (chain-of-responsibility): the first failing gate's prompt
|
|
62
|
+
* wins; null means every gate passed (allow).
|
|
63
|
+
*/
|
|
64
|
+
function evaluateApex(ctx, gates = APEX_GATES) {
|
|
65
|
+
return gates.reduce((hit, gate) => hit ?? gate(ctx), null);
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
export { DEV_KEYWORDS as a, solidReadGate as i, docConsultedGate as n, detectProjectType as o, evaluateApex as r, isApexCommand as s, APEX_GATES as t };
|
package/dist/prompt/index.mjs
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/refs/index.d.mts
CHANGED
|
@@ -1,44 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
interface RefMeta {
|
|
4
|
-
name: string;
|
|
5
|
-
description: string;
|
|
6
|
-
keywords: string;
|
|
7
|
-
priority: string;
|
|
8
|
-
related: string;
|
|
9
|
-
appliesTo: string;
|
|
10
|
-
triggerOnEdit: string;
|
|
11
|
-
level: string;
|
|
12
|
-
filePath: string;
|
|
13
|
-
}
|
|
14
|
-
/** A reference with its computed routing score. */
|
|
15
|
-
interface ScoredRef {
|
|
16
|
-
meta: RefMeta;
|
|
17
|
-
score: number;
|
|
18
|
-
}
|
|
19
|
-
/** Routing result with categorized references. */
|
|
20
|
-
interface RouteResult {
|
|
21
|
-
required: ScoredRef[];
|
|
22
|
-
optional: ScoredRef[];
|
|
23
|
-
skillPath: string;
|
|
24
|
-
}
|
|
25
|
-
//#endregion
|
|
26
|
-
//#region src/refs/frontmatter.d.ts
|
|
27
|
-
/** Extract frontmatter key/value pairs from markdown content (quotes stripped). */
|
|
28
|
-
declare function parseFrontmatter(content: string): Record<string, string>;
|
|
29
|
-
/** Convert a simple glob (`**`, `*`) to an anchored RegExp. */
|
|
30
|
-
declare function globToRe(g: string): RegExp;
|
|
31
|
-
//#endregion
|
|
32
|
-
//#region src/refs/router.d.ts
|
|
33
|
-
/**
|
|
34
|
-
* Score references against a file edit (pure):
|
|
35
|
-
* +10 per `applies-to` glob match, +5 per `trigger-on-edit` fragment, +1 per keyword.
|
|
36
|
-
*/
|
|
37
|
-
declare function scoreReferences(refs: RefMeta[], filePath: string, content: string): ScoredRef[];
|
|
38
|
-
/**
|
|
39
|
-
* Route references for a file edit: top-2 required + next-2 optional, ensuring a
|
|
40
|
-
* `principle` and a `template` appear in the top 4. Returns null when nothing scores.
|
|
41
|
-
*/
|
|
42
|
-
declare function routeReferences(refs: RefMeta[], filePath: string, content: string, skillPath?: string): RouteResult | null;
|
|
43
|
-
//#endregion
|
|
1
|
+
import { n as RouteResult, r as ScoredRef, t as RefMeta } from "../types-CY5qT2X1.mjs";
|
|
2
|
+
import { i as parseFrontmatter, n as scoreReferences, r as globToRe, t as routeReferences } from "../index-C2Lz-cwJ.mjs";
|
|
44
3
|
export { RefMeta, RouteResult, ScoredRef, globToRe, parseFrontmatter, routeReferences, scoreReferences };
|
package/dist/refs/index.mjs
CHANGED
|
@@ -1,70 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
function parseFrontmatter(content) {
|
|
4
|
-
const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
5
|
-
if (!m?.[1]) return {};
|
|
6
|
-
const result = {};
|
|
7
|
-
for (const line of m[1].split("\n")) {
|
|
8
|
-
const idx = line.indexOf(":");
|
|
9
|
-
if (idx > 0) result[line.slice(0, idx).trim()] = line.slice(idx + 1).trim().replace(/^["']|["']$/g, "");
|
|
10
|
-
}
|
|
11
|
-
return result;
|
|
12
|
-
}
|
|
13
|
-
/** Convert a simple glob (`**`, `*`) to an anchored RegExp. */
|
|
14
|
-
function globToRe(g) {
|
|
15
|
-
const escaped = g.trim().replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(/\0/g, ".*");
|
|
16
|
-
return new RegExp(`^${escaped}$`);
|
|
17
|
-
}
|
|
18
|
-
//#endregion
|
|
19
|
-
//#region src/refs/router.ts
|
|
20
|
-
/**
|
|
21
|
-
* Score references against a file edit (pure):
|
|
22
|
-
* +10 per `applies-to` glob match, +5 per `trigger-on-edit` fragment, +1 per keyword.
|
|
23
|
-
*/
|
|
24
|
-
function scoreReferences(refs, filePath, content) {
|
|
25
|
-
const scored = [];
|
|
26
|
-
for (const meta of refs) {
|
|
27
|
-
let score = 0;
|
|
28
|
-
if (meta.appliesTo) {
|
|
29
|
-
for (const g of meta.appliesTo.split(", ")) if (globToRe(g).test(filePath)) score += 10;
|
|
30
|
-
}
|
|
31
|
-
if (meta.triggerOnEdit) {
|
|
32
|
-
for (const frag of meta.triggerOnEdit.split(", ")) if (filePath.includes(frag.trim())) score += 5;
|
|
33
|
-
}
|
|
34
|
-
if (meta.keywords) for (const kw of meta.keywords.split(", ")) {
|
|
35
|
-
const k = kw.trim();
|
|
36
|
-
if (k && (filePath.includes(k) || content.includes(k))) score += 1;
|
|
37
|
-
}
|
|
38
|
-
if (score > 0) scored.push({
|
|
39
|
-
meta,
|
|
40
|
-
score
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
return scored;
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Route references for a file edit: top-2 required + next-2 optional, ensuring a
|
|
47
|
-
* `principle` and a `template` appear in the top 4. Returns null when nothing scores.
|
|
48
|
-
*/
|
|
49
|
-
function routeReferences(refs, filePath, content, skillPath = "") {
|
|
50
|
-
const scored = scoreReferences(refs, filePath, content);
|
|
51
|
-
if (!scored.length) return null;
|
|
52
|
-
scored.sort((a, b) => b.score - a.score);
|
|
53
|
-
const hoist = (level) => {
|
|
54
|
-
if (scored.slice(0, 4).some((r) => r.meta.level === level)) return;
|
|
55
|
-
const found = scored.find((r) => r.meta.level === level);
|
|
56
|
-
if (found) {
|
|
57
|
-
scored.splice(scored.indexOf(found), 1);
|
|
58
|
-
scored.splice(3, 0, found);
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
hoist("principle");
|
|
62
|
-
hoist("template");
|
|
63
|
-
return {
|
|
64
|
-
required: scored.slice(0, 2),
|
|
65
|
-
optional: scored.slice(2, 4),
|
|
66
|
-
skillPath
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
//#endregion
|
|
1
|
+
import { i as parseFrontmatter, n as scoreReferences, r as globToRe, t as routeReferences } from "../router-Dj3AfgBE.mjs";
|
|
2
|
+
import "../refs-la_KkjCS.mjs";
|
|
70
3
|
export { globToRe, parseFrontmatter, routeReferences, scoreReferences };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
//#region src/refs/frontmatter.ts
|
|
2
|
+
/** Extract frontmatter key/value pairs from markdown content (quotes stripped). */
|
|
3
|
+
function parseFrontmatter(content) {
|
|
4
|
+
const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
5
|
+
if (!m?.[1]) return {};
|
|
6
|
+
const result = {};
|
|
7
|
+
for (const line of m[1].split("\n")) {
|
|
8
|
+
const idx = line.indexOf(":");
|
|
9
|
+
if (idx > 0) result[line.slice(0, idx).trim()] = line.slice(idx + 1).trim().replace(/^["']|["']$/g, "");
|
|
10
|
+
}
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
/** Convert a simple glob (`**`, `*`) to an anchored RegExp. */
|
|
14
|
+
function globToRe(g) {
|
|
15
|
+
const escaped = g.trim().replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(/\0/g, ".*");
|
|
16
|
+
return new RegExp(`^${escaped}$`);
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/refs/router.ts
|
|
20
|
+
/**
|
|
21
|
+
* Score references against a file edit (pure):
|
|
22
|
+
* +10 per `applies-to` glob match, +5 per `trigger-on-edit` fragment, +1 per keyword.
|
|
23
|
+
*/
|
|
24
|
+
function scoreReferences(refs, filePath, content) {
|
|
25
|
+
const scored = [];
|
|
26
|
+
for (const meta of refs) {
|
|
27
|
+
let score = 0;
|
|
28
|
+
if (meta.appliesTo) {
|
|
29
|
+
for (const g of meta.appliesTo.split(", ")) if (globToRe(g).test(filePath)) score += 10;
|
|
30
|
+
}
|
|
31
|
+
if (meta.triggerOnEdit) {
|
|
32
|
+
for (const frag of meta.triggerOnEdit.split(", ")) if (filePath.includes(frag.trim())) score += 5;
|
|
33
|
+
}
|
|
34
|
+
if (meta.keywords) for (const kw of meta.keywords.split(", ")) {
|
|
35
|
+
const k = kw.trim();
|
|
36
|
+
if (k && (filePath.includes(k) || content.includes(k))) score += 1;
|
|
37
|
+
}
|
|
38
|
+
if (score > 0) scored.push({
|
|
39
|
+
meta,
|
|
40
|
+
score
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return scored;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Route references for a file edit: top-2 required + next-2 optional, ensuring a
|
|
47
|
+
* `principle` and a `template` appear in the top 4. Returns null when nothing scores.
|
|
48
|
+
*/
|
|
49
|
+
function routeReferences(refs, filePath, content, skillPath = "") {
|
|
50
|
+
const scored = scoreReferences(refs, filePath, content);
|
|
51
|
+
if (!scored.length) return null;
|
|
52
|
+
scored.sort((a, b) => b.score - a.score);
|
|
53
|
+
const hoist = (level) => {
|
|
54
|
+
if (scored.slice(0, 4).some((r) => r.meta.level === level)) return;
|
|
55
|
+
const found = scored.find((r) => r.meta.level === level);
|
|
56
|
+
if (found) {
|
|
57
|
+
scored.splice(scored.indexOf(found), 1);
|
|
58
|
+
scored.splice(3, 0, found);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
hoist("principle");
|
|
62
|
+
hoist("template");
|
|
63
|
+
return {
|
|
64
|
+
required: scored.slice(0, 2),
|
|
65
|
+
optional: scored.slice(2, 4),
|
|
66
|
+
skillPath
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { parseFrontmatter as i, scoreReferences as n, globToRe as r, routeReferences as t };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
//#region src/init/templates.ts
|
|
4
|
+
function json(obj) {
|
|
5
|
+
return `${JSON.stringify(obj, null, 2)}\n`;
|
|
6
|
+
}
|
|
7
|
+
/** Claude Code: PreToolUse(Write|Edit) -> command. Target: `.claude/settings.json`. */
|
|
8
|
+
function claudeInit(command) {
|
|
9
|
+
return {
|
|
10
|
+
path: ".claude/settings.json",
|
|
11
|
+
content: json({ hooks: { PreToolUse: [{
|
|
12
|
+
matcher: "Write|Edit",
|
|
13
|
+
hooks: [{
|
|
14
|
+
type: "command",
|
|
15
|
+
command
|
|
16
|
+
}]
|
|
17
|
+
}] } })
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** Codex CLI: `.codex/hooks.json` — PreToolUse (Claude-compatible shape). */
|
|
21
|
+
function codexInit(command) {
|
|
22
|
+
return {
|
|
23
|
+
path: ".codex/hooks.json",
|
|
24
|
+
content: json({ hooks: { PreToolUse: [{
|
|
25
|
+
matcher: "Bash|apply_patch",
|
|
26
|
+
hooks: [{
|
|
27
|
+
type: "command",
|
|
28
|
+
command
|
|
29
|
+
}]
|
|
30
|
+
}] } })
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** Cursor: `.cursor/hooks.json` (version 1) — shell + file-edit hooks. */
|
|
34
|
+
function cursorInit(command) {
|
|
35
|
+
return {
|
|
36
|
+
path: ".cursor/hooks.json",
|
|
37
|
+
content: json({
|
|
38
|
+
version: 1,
|
|
39
|
+
hooks: {
|
|
40
|
+
beforeShellExecution: [{ command }],
|
|
41
|
+
afterFileEdit: [{ command }]
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Gemini CLI: `.gemini/settings.json` — BeforeTool, timeout in ms. */
|
|
47
|
+
function geminiInit(command) {
|
|
48
|
+
return {
|
|
49
|
+
path: ".gemini/settings.json",
|
|
50
|
+
content: json({ hooks: { BeforeTool: [{
|
|
51
|
+
matcher: "write_file|edit_file|replace",
|
|
52
|
+
hooks: [{
|
|
53
|
+
type: "command",
|
|
54
|
+
command,
|
|
55
|
+
timeout: 3e4
|
|
56
|
+
}]
|
|
57
|
+
}] } })
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** Cline: an executable `.clinerules/hooks/PreToolUse` that pipes stdin to the command. */
|
|
61
|
+
function clineInit(command) {
|
|
62
|
+
return {
|
|
63
|
+
path: ".clinerules/hooks/PreToolUse",
|
|
64
|
+
content: `#!/usr/bin/env bash\n# fuse-harness Cline hook — evaluate each tool use\ncat | ${command}\n`,
|
|
65
|
+
executable: true
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/init/run.ts
|
|
70
|
+
const RUNNERS = {
|
|
71
|
+
"claude-code": claudeInit,
|
|
72
|
+
codex: codexInit,
|
|
73
|
+
cursor: cursorInit,
|
|
74
|
+
"gemini-cli": geminiInit,
|
|
75
|
+
cline: clineInit
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Build the wiring file for a harness, or null when it has no hook integration
|
|
79
|
+
* (cli-mode harnesses use `harness check` in a pre-commit step instead).
|
|
80
|
+
*/
|
|
81
|
+
function initFor(id, command = `npx harness hook ${id}`) {
|
|
82
|
+
const make = RUNNERS[id];
|
|
83
|
+
return make ? make(command) : null;
|
|
84
|
+
}
|
|
85
|
+
/** Write an {@link InitFile} under `root` (creates dirs; `chmod +x` when executable). */
|
|
86
|
+
function writeInitFile(root, file) {
|
|
87
|
+
const full = join(root, file.path);
|
|
88
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
89
|
+
writeFileSync(full, file.content);
|
|
90
|
+
if (file.executable) chmodSync(full, 493);
|
|
91
|
+
return full;
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
export { codexInit as a, clineInit as i, writeInitFile as n, cursorInit as o, claudeInit as r, geminiInit as s, initFor as t };
|
package/dist/state/index.d.mts
CHANGED
|
@@ -1,59 +1,2 @@
|
|
|
1
|
-
import { t as
|
|
2
|
-
|
|
3
|
-
//#region src/state/lock.d.ts
|
|
4
|
-
/**
|
|
5
|
-
* Acquire a directory-based lock (atomic `mkdir`, EEXIST = already held) with a timeout.
|
|
6
|
-
* @returns a release function, or null if the lock could not be acquired in time.
|
|
7
|
-
*/
|
|
8
|
-
declare function acquireLock(lockDir: string, timeoutMs?: number): Promise<(() => Promise<void>) | null>;
|
|
9
|
-
//#endregion
|
|
10
|
-
//#region src/state/apex-state.d.ts
|
|
11
|
-
declare const DEFAULT_STATE: {
|
|
12
|
-
$schema: string;
|
|
13
|
-
description: string;
|
|
14
|
-
target: Record<string, string>;
|
|
15
|
-
authorizations: Record<string, AuthEntry & {
|
|
16
|
-
doc_consulted?: string;
|
|
17
|
-
}>;
|
|
18
|
-
};
|
|
19
|
-
/** APEX state shape. */
|
|
20
|
-
type ApexState = typeof DEFAULT_STATE;
|
|
21
|
-
/** APEX state directory under a home dir. */
|
|
22
|
-
declare function apexStateDir(home?: string): string;
|
|
23
|
-
/** Daily state file path: `<home>/.claude/logs/00-apex/<YYYY-MM-DD>-state.json`. */
|
|
24
|
-
declare function stateFilePath(home?: string, today?: string): string;
|
|
25
|
-
/** Ensure the state directory exists and return its path. */
|
|
26
|
-
declare function ensureStateDir(home?: string): Promise<string>;
|
|
27
|
-
/** Load APEX state, or a fresh default. */
|
|
28
|
-
declare function loadState(path: string): Promise<ApexState>;
|
|
29
|
-
/** Save APEX state. */
|
|
30
|
-
declare function saveState(path: string, state: ApexState): Promise<void>;
|
|
31
|
-
//#endregion
|
|
32
|
-
//#region src/state/task-helpers.d.ts
|
|
33
|
-
/** A task entry in task.json. */
|
|
34
|
-
interface ApexTask {
|
|
35
|
-
subject: string;
|
|
36
|
-
description: string;
|
|
37
|
-
status: string;
|
|
38
|
-
phase: string;
|
|
39
|
-
started_at?: string;
|
|
40
|
-
completed_at?: string;
|
|
41
|
-
created_at?: string;
|
|
42
|
-
doc_consulted: Record<string, unknown>;
|
|
43
|
-
files_modified: string[];
|
|
44
|
-
blockedBy?: string[];
|
|
45
|
-
}
|
|
46
|
-
/** Structure of `.claude/apex/task.json`. */
|
|
47
|
-
interface ApexTaskFile {
|
|
48
|
-
current_task: string;
|
|
49
|
-
created_at: string;
|
|
50
|
-
tasks: Record<string, ApexTask>;
|
|
51
|
-
}
|
|
52
|
-
/** Add a new pending task. */
|
|
53
|
-
declare function taskCreate(file: string, id: string, subject: string, desc: string): Promise<void>;
|
|
54
|
-
/** Mark a task in_progress (creating a stub if absent). */
|
|
55
|
-
declare function taskStart(file: string, id: string, subject?: string, desc?: string, blocked?: string): Promise<void>;
|
|
56
|
-
/** Mark a task completed. */
|
|
57
|
-
declare function taskComplete(file: string, id: string): Promise<void>;
|
|
58
|
-
//#endregion
|
|
1
|
+
import { a as taskStart, c as ensureStateDir, d as stateFilePath, f as acquireLock, i as taskCreate, l as loadState, n as ApexTaskFile, o as ApexState, r as taskComplete, s as apexStateDir, t as ApexTask, u as saveState } from "../index-CPoF_hLP.mjs";
|
|
59
2
|
export { ApexState, ApexTask, ApexTaskFile, acquireLock, apexStateDir, ensureStateDir, loadState, saveState, stateFilePath, taskComplete, taskCreate, taskStart };
|