@fusengine/harness 0.1.47 → 0.1.49
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 +2 -2
- package/dist/adapters/claude/index.mjs +2 -2
- package/dist/adapters/cline/index.mjs +1 -1
- package/dist/adapters/codex/index.d.mts +1 -1
- package/dist/adapters/codex/index.mjs +1 -1
- package/dist/adapters/cursor/index.d.mts +10 -3
- package/dist/adapters/cursor/index.mjs +3 -3
- package/dist/adapters/gemini/index.mjs +1 -1
- package/dist/adapters/hermes/index.d.mts +54 -0
- package/dist/adapters/hermes/index.mjs +2 -0
- package/dist/apex-Wdi1nq_w.d.mts +73 -0
- package/dist/{claude-U1KTaRG4.mjs → claude-BxC9semG.mjs} +30 -9
- package/dist/cli/bin.mjs +5 -5
- package/dist/cli/index.mjs +1 -1
- package/dist/config/index.d.mts +1 -1
- package/dist/config/index.mjs +1 -1
- package/dist/detect/index.d.mts +1 -1
- package/dist/detect/index.mjs +1 -1
- package/dist/{doc-helpers-BNfYWvYv.d.mts → doc-helpers-CEKzGg2u.d.mts} +2 -0
- package/dist/{dotenv-B9nM4cuQ.mjs → dotenv-Jj8aL1FL.mjs} +2 -1
- package/dist/{evaluate-qYC5xClb.mjs → evaluate-CNAzgxnN.mjs} +3 -85
- package/dist/freshness/index.d.mts +1 -1
- package/dist/{handle-DUlUbhyF.mjs → handle-D1hHqHvh.mjs} +422 -62
- package/dist/{harness-C8Nxxyn_.mjs → harness-Cb9xR8dC.mjs} +6 -3
- package/dist/{harness-BPPu5CrN.d.mts → harness-s1Fce_dL.d.mts} +1 -1
- package/dist/hermes-DWXCRFZU.mjs +54 -0
- package/dist/home-state-D0RLWP8J.mjs +134 -0
- package/dist/{index-l9Xvxrx7.d.mts → index-BfMsILmg.d.mts} +2 -52
- package/dist/{index-BxjzFraL.d.mts → index-DZCLmSoO.d.mts} +11 -9
- package/dist/{index-BXPySPxE.d.mts → index-jj_EzZRv.d.mts} +1 -1
- package/dist/index.d.mts +7 -6
- package/dist/index.mjs +6 -5
- package/dist/init/index.d.mts +1 -1
- package/dist/policy/index.d.mts +3 -2
- package/dist/policy/index.mjs +4 -3
- package/dist/prompt/index.d.mts +1 -1
- package/dist/{run-CaqeQFYp.mjs → run-DZvP_9xB.mjs} +1 -1
- package/dist/runtime/index.d.mts +23 -8
- package/dist/runtime/index.mjs +2 -2
- package/dist/session-state-DMpotbRz.d.mts +73 -0
- package/dist/skill-path-DhItkBzk.mjs +95 -0
- package/dist/store-QSSTO3lY.mjs +349 -0
- package/dist/tracking/index.d.mts +2 -2
- package/dist/tracking/index.mjs +2 -2
- package/dist/{types-D56jSgD9.d.mts → types-DVbIl9md.d.mts} +6 -0
- package/dist/{validate-DVV4nRfr.mjs → validate-DCQ8dkdL.mjs} +34 -103
- package/package.json +7 -2
- package/dist/home-state-mKZxP4oZ.mjs +0 -52
- package/dist/session-state-D4F_Dub6.d.mts +0 -35
- package/dist/store-CNjFenWe.mjs +0 -184
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { r as resolveMaxLines } from "./limits-CHn8AIL1.mjs";
|
|
2
|
-
import { c as ttlLabel
|
|
3
|
-
import {
|
|
4
|
-
import { F as PLUGINS_DIR, I as SOLID_REF, L as countFrameworkCodeLines } from "./evaluate-qYC5xClb.mjs";
|
|
2
|
+
import { c as ttlLabel } from "./dotenv-Jj8aL1FL.mjs";
|
|
3
|
+
import { d as countFrameworkCodeLines, l as PLUGINS_DIR, u as SOLID_REF } from "./home-state-D0RLWP8J.mjs";
|
|
5
4
|
import { r as isDocConsulted, t as formatDocDeny } from "./doc-helpers-CWZegVdR.mjs";
|
|
6
5
|
import { t as routeReferences } from "./router-PKVNBHge.mjs";
|
|
6
|
+
import { n as findMarketplacePlugins, r as readPluginMeta, t as resolveSkillPath } from "./skill-path-DhItkBzk.mjs";
|
|
7
7
|
import { extname, join } from "node:path";
|
|
8
8
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
9
9
|
import { homedir } from "node:os";
|
|
@@ -109,11 +109,26 @@ function detectFramework(filePath, content) {
|
|
|
109
109
|
}
|
|
110
110
|
//#endregion
|
|
111
111
|
//#region src/policy/apex-gates.ts
|
|
112
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* True when the SOLID ref read at `path` still counts. Parity require-solid-read.py
|
|
114
|
+
* `_check_solid_read` (FUSE_ENFORCE_TTL_SEC, default 120s): a SOLID read older than
|
|
115
|
+
* the window has expired and must be redone. PARITY NOTE: the TTL applies ONLY to
|
|
116
|
+
* this gate — Python TTL-izes SOLID reads exclusively; the other refsRead consumers
|
|
117
|
+
* (skillTriggerGate, shadcn/tailwind/design gates) stay session-scoped, so this
|
|
118
|
+
* predicate must never leak into them. Backward compat: a path with no `refsReadAt`
|
|
119
|
+
* stamp (tracks recorded before the field existed) or a context without a clock
|
|
120
|
+
* (`now` absent) counts as read.
|
|
121
|
+
*/
|
|
122
|
+
function refReadFresh(ctx, path, windowMs) {
|
|
123
|
+
const at = ctx.refsReadAt?.[path];
|
|
124
|
+
return at === void 0 || ctx.now === void 0 || ctx.now - at < windowMs;
|
|
125
|
+
}
|
|
126
|
+
/** Gate: the routed SOLID references for this edit must have been read within the TTL. */
|
|
113
127
|
const solidReadGate = (ctx) => {
|
|
114
128
|
if (!ctx.refs?.length) return null;
|
|
115
129
|
const routed = routeReferences(ctx.refs, ctx.filePath, ctx.content);
|
|
116
|
-
const
|
|
130
|
+
const windowMs = ctx.windowMs ?? 120 * 1e3;
|
|
131
|
+
const ttl = ttlLabel(Math.round(windowMs / 1e3));
|
|
117
132
|
if (!routed) {
|
|
118
133
|
const ref = SOLID_REF[ctx.framework] ?? "generic/";
|
|
119
134
|
return {
|
|
@@ -123,7 +138,7 @@ const solidReadGate = (ctx) => {
|
|
|
123
138
|
actions: [`Read ${PLUGINS_DIR}/${ref}SKILL.md`]
|
|
124
139
|
};
|
|
125
140
|
}
|
|
126
|
-
const read = new Set(ctx.refsRead ?? []);
|
|
141
|
+
const read = new Set((ctx.refsRead ?? []).filter((p) => refReadFresh(ctx, p, windowMs)));
|
|
127
142
|
const missing = !!routed.skillPath && read.has(routed.skillPath) ? [] : routed.required.map((r) => r.meta.filePath).filter((p) => !read.has(p));
|
|
128
143
|
if (missing.length === 0) return null;
|
|
129
144
|
const optional = routed.optional.map((r) => r.meta.filePath);
|
|
@@ -167,13 +182,17 @@ const brainstormGate = (ctx) => ctx.brainstormRequired && ctx.brainstormFresh ==
|
|
|
167
182
|
reason: `Creation intent detected — brainstorm before creating new ${ctx.framework} files.`,
|
|
168
183
|
actions: ["Launch the brainstorming agent"]
|
|
169
184
|
} : null;
|
|
170
|
-
/**
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
185
|
+
/** Gates that run BEFORE Check 1 (harness-specific brainstorm + agent freshness, pre-dating enforce-apex-phases). */
|
|
186
|
+
const PRE_AUTH_GATES = [brainstormGate, freshnessGate];
|
|
187
|
+
/** Gates that run AFTER Check 1: Check 2 (doc consulted once per session) then the SOLID refs. */
|
|
188
|
+
const POST_AUTH_GATES = [docConsultedGate, solidReadGate];
|
|
189
|
+
/**
|
|
190
|
+
* Default APEX gate chain (brainstorm, freshness, docs, SOLID refs). Check 1
|
|
191
|
+
* (`apexAuthorizationGate`, policy/apex-authorization.ts) is wired BETWEEN the
|
|
192
|
+
* PRE and POST sub-chains by the runtime (gate-apex.ts), which owns its
|
|
193
|
+
* target-write side effect — parity enforce-apex-phases.ts Check1 -> Check2.
|
|
194
|
+
*/
|
|
195
|
+
const APEX_GATES = [...PRE_AUTH_GATES, ...POST_AUTH_GATES];
|
|
177
196
|
/**
|
|
178
197
|
* Run the APEX gates (chain-of-responsibility): the first failing gate's prompt
|
|
179
198
|
* wins; null means every gate passed (allow).
|
|
@@ -804,94 +823,6 @@ function isShadcnProject(cwd) {
|
|
|
804
823
|
return UI_DIRS.some((dir) => existsSync(join(cwd, dir)));
|
|
805
824
|
}
|
|
806
825
|
//#endregion
|
|
807
|
-
//#region src/runtime/lifecycle/cartographer/detect.ts
|
|
808
|
-
/**
|
|
809
|
-
* Plugin discovery (fs). Ports `detect_plugins.py`: marketplace `plugins` dir
|
|
810
|
-
* resolution + `plugin.json` meta reading.
|
|
811
|
-
*/
|
|
812
|
-
/** Sorted entry names of `dir` (alpha, byte-order), or `[]` on error. */
|
|
813
|
-
function sortedNames$1(dir) {
|
|
814
|
-
try {
|
|
815
|
-
return readdirSync(dir).sort((a, b) => a.localeCompare(b, "en"));
|
|
816
|
-
} catch {
|
|
817
|
-
return [];
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
/**
|
|
821
|
-
* Read `[version, name]` from `<pluginPath>/.claude-plugin/plugin.json`.
|
|
822
|
-
* @param pluginPath - Absolute plugin directory.
|
|
823
|
-
* @returns The `[version, name]` pair (both "" when absent/unreadable).
|
|
824
|
-
*/
|
|
825
|
-
function readPluginMeta(pluginPath) {
|
|
826
|
-
const pj = join(pluginPath, ".claude-plugin", "plugin.json");
|
|
827
|
-
if (!existsSync(pj)) return ["", ""];
|
|
828
|
-
try {
|
|
829
|
-
const meta = JSON.parse(readFileSync(pj, "utf-8"));
|
|
830
|
-
return [meta.version ?? "", meta.name ?? ""];
|
|
831
|
-
} catch {
|
|
832
|
-
return ["", ""];
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
/**
|
|
836
|
-
* Auto-detect the marketplace `plugins` dir that contains `cartographer`,
|
|
837
|
-
* falling back to the first marketplace with a `plugins` dir, else `cwd`.
|
|
838
|
-
* Ports `find_marketplace_plugins`, but harness-agnostic: the config dir is
|
|
839
|
-
* derived from the detected harness (`.claude`, `.codex`, `.cursor`, …) via the
|
|
840
|
-
* shared `HOME_DIR` mapping instead of a hardcoded `.claude`.
|
|
841
|
-
* @param home - Home directory (defaults to `~`).
|
|
842
|
-
* @param id - Detected harness id (defaults to runtime detection).
|
|
843
|
-
* @returns The resolved plugins directory.
|
|
844
|
-
*/
|
|
845
|
-
function findMarketplacePlugins(home = homedir(), id = detectHarness().id) {
|
|
846
|
-
const mp = join(home, HOME_DIR[id] ?? ".claude", "plugins", "marketplaces");
|
|
847
|
-
const markets = sortedNames$1(mp);
|
|
848
|
-
for (const m of markets) if (existsSync(join(mp, m, "plugins", "cartographer"))) return join(mp, m, "plugins");
|
|
849
|
-
for (const m of markets) if (existsSync(join(mp, m, "plugins"))) return join(mp, m, "plugins");
|
|
850
|
-
return process.cwd();
|
|
851
|
-
}
|
|
852
|
-
//#endregion
|
|
853
|
-
//#region src/policy/skill-path.ts
|
|
854
|
-
/**
|
|
855
|
-
* Dynamic skill-path resolution (fs). Replaces a hardcoded `PLUGIN_DIR`
|
|
856
|
-
* table (framework -> plugin dir name), which can drift from what's actually
|
|
857
|
-
* installed: scans installed marketplace plugins for a `skills/<name>/SKILL.md`
|
|
858
|
-
* that exists ON DISK, returning the first real absolute path found — never a
|
|
859
|
-
* guessed one.
|
|
860
|
-
*
|
|
861
|
-
* Deliberately lighter than the cartographer's own `scanSkills` (which
|
|
862
|
-
* frontmatter-parses every skill in a plugin): this runs once per missing
|
|
863
|
-
* sub-skill on every gated Write/Edit, so it does a single flat `readdirSync`
|
|
864
|
-
* of the plugins dir + one `existsSync` per candidate — no file reads, no
|
|
865
|
-
* recursion, no `statSync`.
|
|
866
|
-
*/
|
|
867
|
-
/** Sorted entry names of `dir` (alpha, byte-order), or `[]` on error. */
|
|
868
|
-
function pluginDirNames(dir) {
|
|
869
|
-
try {
|
|
870
|
-
return readdirSync(dir).sort((a, b) => a.localeCompare(b, "en"));
|
|
871
|
-
} catch {
|
|
872
|
-
return [];
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
/**
|
|
876
|
-
* Resolve the real, installed `SKILL.md` path for `skillName`.
|
|
877
|
-
* @param skillName - the sub-skill directory name (e.g. "solid-react",
|
|
878
|
-
* "shadcn-detection", "react-shadcn").
|
|
879
|
-
* @param pluginsDirOverride - override for the marketplace plugins dir
|
|
880
|
-
* (tests); defaults to the auto-detected marketplace root.
|
|
881
|
-
* @returns the absolute `<pluginDir>/skills/<skillName>/SKILL.md` of the
|
|
882
|
-
* first installed plugin that actually has it, or the generic
|
|
883
|
-
* `${PLUGINS_DIR}/<skillName>` fallback when none is found (CI/test
|
|
884
|
-
* environments without a marketplace).
|
|
885
|
-
*/
|
|
886
|
-
function resolveSkillPath(skillName, pluginsDirOverride) {
|
|
887
|
-
const pluginsDir = pluginsDirOverride ?? findMarketplacePlugins();
|
|
888
|
-
for (const dir of pluginDirNames(pluginsDir)) {
|
|
889
|
-
const candidate = join(pluginsDir, dir, "skills", skillName, "SKILL.md");
|
|
890
|
-
if (existsSync(candidate)) return candidate;
|
|
891
|
-
}
|
|
892
|
-
return `${PLUGINS_DIR}/${skillName}`;
|
|
893
|
-
}
|
|
894
|
-
//#endregion
|
|
895
826
|
//#region src/policy/skill-triggers.ts
|
|
896
827
|
/**
|
|
897
828
|
* Detect which sub-skills the written `content` requires for a `framework`.
|
|
@@ -1227,7 +1158,7 @@ function buildApexInstruction(projectType, maxLines) {
|
|
|
1227
1158
|
const expertAgent = getExpertAgent(projectType);
|
|
1228
1159
|
return `INSTRUCTION: This is a development task. Use APEX methodology:
|
|
1229
1160
|
|
|
1230
|
-
**TRACKING FILE**: [project]/.claude/apex/task.json (
|
|
1161
|
+
**TRACKING FILE**: [project]/.claude/apex/task.json (created by the /apex command)
|
|
1231
1162
|
|
|
1232
1163
|
1. **ANALYZE** (MANDATORY - 3 AGENTS IN PARALLEL):
|
|
1233
1164
|
- explore-codebase + research-expert + ${expertAgent} (framework expertise)\n - Project type detected: ${projectType}\n\n2. **PLAN**: Use TaskCreate to break down tasks (<${maxLines} lines per file)\n\n3. **EXECUTE**: ${expertAgent}, follow SOLID principles, split at ${maxLines - 10} lines\n\n4. **EXAMINE**: Run sniper agent after ANY modification\n\n**IMPORTANT**: Read .claude/apex/task.json to check documentation status before writing code.`;
|
|
@@ -1545,4 +1476,4 @@ function missingSeoElements(html) {
|
|
|
1545
1476
|
return missing;
|
|
1546
1477
|
}
|
|
1547
1478
|
//#endregion
|
|
1548
|
-
export {
|
|
1479
|
+
export { detectCreationIntent as A, DEV_KEYWORDS as B, SKILL_TRIGGERS as C, MAX_EXA_RESULTS as D, isExcludedSwiftPath as E, docConsultedGate as F, detectProjectType as H, evaluateApex as I, freshnessGate as L, POST_AUTH_GATES as M, PRE_AUTH_GATES as N, MAX_TOKENS as O, brainstormGate as P, solidReadGate as R, usesTailwindUtilities as S, isExcludedJsPath as T, isApexCommand as U, detectModularArchitecture as V, requiredArchSkill as W, scanPlugin as _, firstHeading as a, detectRequiredSkills as b, EXCLUDE_DIRS as c, buildApexTaskInjection as d, loadApexTaskState as f, detectClaudeMdProjectType as g, buildClaudeMdContext as h, firstComment as i, APEX_GATES as j, capVerbosity as k, PROJECT_INDICATORS as l, buildApexInstruction as m, missingSeoElements as n, parseEnrichment as o, DEV_VERBS as p, descFromText as r, parseEntry as s, isHtmlLike as t, buildApexTaskContext as u, parseBodyDesc as v, frameworkSolidGate as w, skillTriggerGate as x, parseField as y, detectFramework as z };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusengine/harness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.49",
|
|
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",
|
|
@@ -108,6 +108,11 @@
|
|
|
108
108
|
"bun": "./src/adapters/gemini/index.ts",
|
|
109
109
|
"types": "./dist/adapters/gemini/index.d.mts",
|
|
110
110
|
"import": "./dist/adapters/gemini/index.mjs"
|
|
111
|
+
},
|
|
112
|
+
"./adapters/hermes": {
|
|
113
|
+
"bun": "./src/adapters/hermes/index.ts",
|
|
114
|
+
"types": "./dist/adapters/hermes/index.d.mts",
|
|
115
|
+
"import": "./dist/adapters/hermes/index.mjs"
|
|
111
116
|
}
|
|
112
117
|
},
|
|
113
118
|
"files": [
|
|
@@ -127,7 +132,7 @@
|
|
|
127
132
|
"test": "bun test",
|
|
128
133
|
"typecheck": "tsc --noEmit",
|
|
129
134
|
"docs:api": "typedoc",
|
|
130
|
-
"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/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",
|
|
135
|
+
"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/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 src/adapters/hermes/index.ts --dts --format esm --clean --out-dir dist",
|
|
131
136
|
"prepublishOnly": "bun test && tsc --noEmit && bun run build"
|
|
132
137
|
},
|
|
133
138
|
"publishConfig": {
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { t as atomicWrite } from "./json-io-DisYd2fb.mjs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
|
-
//#region src/runtime/home-state.ts
|
|
6
|
-
/** Home `~/.claude` dir — per-harness config (CLAUDE.md, logs, plugins). */
|
|
7
|
-
function claudeHome(home = homedir()) {
|
|
8
|
-
return join(home, ".claude");
|
|
9
|
-
}
|
|
10
|
-
/** Neutral, harness-agnostic home for fuse-harness's OWN cache/state: `~/.fuse-harness`. */
|
|
11
|
-
function fuseHarnessHome(home = homedir()) {
|
|
12
|
-
return join(home, ".fuse-harness");
|
|
13
|
-
}
|
|
14
|
-
/** `~/.fuse-harness/cache` base dir for session/cache state (shared across harnesses). */
|
|
15
|
-
function fusengineCache(home = homedir()) {
|
|
16
|
-
return join(fuseHarnessHome(home), "cache");
|
|
17
|
-
}
|
|
18
|
-
/** `~/.fuse-harness/cache/sessions` — per-session JSON state dir. */
|
|
19
|
-
function sessionsDir(home = homedir()) {
|
|
20
|
-
return join(fusengineCache(home), "sessions");
|
|
21
|
-
}
|
|
22
|
-
const SID_RE = /^[a-zA-Z0-9_-]{1,128}$/;
|
|
23
|
-
/** Validate a session id (1-128 url-safe chars); null when invalid. */
|
|
24
|
-
function sanitizeSessionId(sid) {
|
|
25
|
-
const s = String(sid ?? "").trim();
|
|
26
|
-
return SID_RE.test(s) ? s : null;
|
|
27
|
-
}
|
|
28
|
-
/** Unified per-session state file path: `sessions/session-<sid>.json`. */
|
|
29
|
-
function sessionStatePath(sid, home = homedir()) {
|
|
30
|
-
return join(sessionsDir(home), `session-${sid}.json`);
|
|
31
|
-
}
|
|
32
|
-
/** Load a session-state dict, or `{}` when missing/corrupt (mirrors Python). */
|
|
33
|
-
function loadSessionState(sid, home = homedir()) {
|
|
34
|
-
const path = sessionStatePath(sid, home);
|
|
35
|
-
try {
|
|
36
|
-
if (!existsSync(path)) return {};
|
|
37
|
-
const data = JSON.parse(readFileSync(path, "utf-8"));
|
|
38
|
-
return typeof data === "object" && data !== null && !Array.isArray(data) ? data : {};
|
|
39
|
-
} catch {
|
|
40
|
-
return {};
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
/** Atomically persist a session-state dict (0o600 via atomicWrite, indent 2). */
|
|
44
|
-
function saveSessionState(sid, state, home = homedir()) {
|
|
45
|
-
mkdirSync(sessionsDir(home), {
|
|
46
|
-
recursive: true,
|
|
47
|
-
mode: 448
|
|
48
|
-
});
|
|
49
|
-
atomicWrite(sessionStatePath(sid, home), JSON.stringify(state, null, 2));
|
|
50
|
-
}
|
|
51
|
-
//#endregion
|
|
52
|
-
export { sanitizeSessionId as a, sessionsDir as c, loadSessionState as i, fuseHarnessHome as n, saveSessionState as o, fusengineCache as r, sessionStatePath as s, claudeHome as t };
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { t as AuthEntry } from "./doc-helpers-BNfYWvYv.mjs";
|
|
2
|
-
|
|
3
|
-
//#region src/tracking/session-state.d.ts
|
|
4
|
-
/** Quality of a recorded agent call (the freshness gate ignores insufficient ones). */
|
|
5
|
-
type AgentQuality = "sufficient" | "insufficient";
|
|
6
|
-
/** Recorded session activity that feeds the APEX gates. */
|
|
7
|
-
interface SessionTrack {
|
|
8
|
-
authorizations: Record<string, AuthEntry>;
|
|
9
|
-
refsRead: string[];
|
|
10
|
-
agents: {
|
|
11
|
-
name: string;
|
|
12
|
-
ts: number;
|
|
13
|
-
quality?: AgentQuality;
|
|
14
|
-
}[];
|
|
15
|
-
trivialEdits: number[];
|
|
16
|
-
brainstormRequired?: boolean;
|
|
17
|
-
}
|
|
18
|
-
/** A fresh, empty track. */
|
|
19
|
-
declare function emptyTrack(): SessionTrack;
|
|
20
|
-
/** Record a doc consultation (Context7/Exa) for a framework in this session. Immutable. */
|
|
21
|
-
declare function recordDoc(track: SessionTrack, framework: string, sessionId: string, source: string): SessionTrack;
|
|
22
|
-
/** Record that a SOLID reference file was read (deduped). Immutable. */
|
|
23
|
-
declare function recordRefRead(track: SessionTrack, path: string): SessionTrack;
|
|
24
|
-
/** Record an agent/tool call with a timestamp + optional quality. Immutable. */
|
|
25
|
-
declare function recordAgent(track: SessionTrack, name: string, ts: number, quality?: AgentQuality): SessionTrack;
|
|
26
|
-
/** True when ALL of `names` ran within `windowMs` with non-insufficient quality. */
|
|
27
|
-
declare function agentsFresh(track: SessionTrack, names: string[], windowMs: number, now: number): boolean;
|
|
28
|
-
/** Record a trivial edit timestamp (sliding window; old evicted). Immutable. */
|
|
29
|
-
declare function recordTrivialEdit(track: SessionTrack, ts: number, windowMs: number, now: number): SessionTrack;
|
|
30
|
-
/** Count trivial edits within the sliding window. */
|
|
31
|
-
declare function trivialCount(track: SessionTrack, windowMs: number, now: number): number;
|
|
32
|
-
/** Set the brainstorm-required flag (from creation-intent detection). Immutable. */
|
|
33
|
-
declare function recordBrainstormRequired(track: SessionTrack, required: boolean): SessionTrack;
|
|
34
|
-
//#endregion
|
|
35
|
-
export { recordAgent as a, recordRefRead as c, emptyTrack as i, recordTrivialEdit as l, SessionTrack as n, recordBrainstormRequired as o, agentsFresh as r, recordDoc as s, AgentQuality as t, trivialCount as u };
|
package/dist/store-CNjFenWe.mjs
DELETED
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import { a as writeJsonFile, i as readJsonFile } from "./json-io-DisYd2fb.mjs";
|
|
2
|
-
import { n as fuseHarnessHome } from "./home-state-mKZxP4oZ.mjs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { createHmac, randomBytes } from "node:crypto";
|
|
6
|
-
//#region src/tracking/session-state.ts
|
|
7
|
-
/** A fresh, empty track. */
|
|
8
|
-
function emptyTrack() {
|
|
9
|
-
return {
|
|
10
|
-
authorizations: {},
|
|
11
|
-
refsRead: [],
|
|
12
|
-
agents: [],
|
|
13
|
-
trivialEdits: []
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
/** Record a doc consultation (Context7/Exa) for a framework in this session. Immutable. */
|
|
17
|
-
function recordDoc(track, framework, sessionId, source) {
|
|
18
|
-
const prev = track.authorizations[framework] ?? {};
|
|
19
|
-
const sessions = new Set(prev.doc_sessions ?? []);
|
|
20
|
-
sessions.add(sessionId);
|
|
21
|
-
const sources = new Set(prev.sources ?? (prev.source ? [prev.source] : []));
|
|
22
|
-
sources.add(source);
|
|
23
|
-
return {
|
|
24
|
-
...track,
|
|
25
|
-
authorizations: {
|
|
26
|
-
...track.authorizations,
|
|
27
|
-
[framework]: {
|
|
28
|
-
...prev,
|
|
29
|
-
doc_sessions: [...sessions],
|
|
30
|
-
sources: [...sources]
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
/** Record that a SOLID reference file was read (deduped). Immutable. */
|
|
36
|
-
function recordRefRead(track, path) {
|
|
37
|
-
return track.refsRead.includes(path) ? track : {
|
|
38
|
-
...track,
|
|
39
|
-
refsRead: [...track.refsRead, path]
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
/** Record an agent/tool call with a timestamp + optional quality. Immutable. */
|
|
43
|
-
function recordAgent(track, name, ts, quality) {
|
|
44
|
-
const entry = quality ? {
|
|
45
|
-
name,
|
|
46
|
-
ts,
|
|
47
|
-
quality
|
|
48
|
-
} : {
|
|
49
|
-
name,
|
|
50
|
-
ts
|
|
51
|
-
};
|
|
52
|
-
return {
|
|
53
|
-
...track,
|
|
54
|
-
agents: [...track.agents, entry]
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
/** True when ALL of `names` ran within `windowMs` with non-insufficient quality. */
|
|
58
|
-
function agentsFresh(track, names, windowMs, now) {
|
|
59
|
-
const cutoff = now - windowMs;
|
|
60
|
-
return names.every((n) => track.agents.some((a) => a.name === n && a.ts > cutoff && a.quality !== "insufficient"));
|
|
61
|
-
}
|
|
62
|
-
/** Record a trivial edit timestamp (sliding window; old evicted). Immutable. */
|
|
63
|
-
function recordTrivialEdit(track, ts, windowMs, now) {
|
|
64
|
-
const cutoff = now - windowMs;
|
|
65
|
-
return {
|
|
66
|
-
...track,
|
|
67
|
-
trivialEdits: [...(track.trivialEdits ?? []).filter((t) => t > cutoff), ts]
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
/** Count trivial edits within the sliding window. */
|
|
71
|
-
function trivialCount(track, windowMs, now) {
|
|
72
|
-
const cutoff = now - windowMs;
|
|
73
|
-
return (track.trivialEdits ?? []).filter((t) => t > cutoff).length;
|
|
74
|
-
}
|
|
75
|
-
/** Set the brainstorm-required flag (from creation-intent detection). Immutable. */
|
|
76
|
-
function recordBrainstormRequired(track, required) {
|
|
77
|
-
return {
|
|
78
|
-
...track,
|
|
79
|
-
brainstormRequired: required
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
//#endregion
|
|
83
|
-
//#region src/tracking/integrity.ts
|
|
84
|
-
/**
|
|
85
|
-
* @module integrity
|
|
86
|
-
* HMAC-SHA256 tamper-evident wrapping for {@link SessionTrack}.
|
|
87
|
-
*
|
|
88
|
-
* Failure policy: ONLY a MAC mismatch causes fail-closed (returns null).
|
|
89
|
-
* The nonce is included in the signed payload and written to disk for advisory
|
|
90
|
-
* diagnostics, but is NOT checked during verification — concurrent hook
|
|
91
|
-
* invocations (PostToolUse, SubagentStop, …) legitimately load the same
|
|
92
|
-
* envelope after the nonce watermark advances; a monotonic check would trigger
|
|
93
|
-
* spurious fail-closed behaviour mid-session.
|
|
94
|
-
*
|
|
95
|
-
* The machine key is readable by the same agent process, so this deters naive
|
|
96
|
-
* out-of-band tampering — not a determined re-sign. Item B (transcript-grounded
|
|
97
|
-
* freshness) is the primary guarantee.
|
|
98
|
-
* @packageDocumentation
|
|
99
|
-
*/
|
|
100
|
-
const HARNESS_DIR = fuseHarnessHome();
|
|
101
|
-
const KEY_PATH = join(HARNESS_DIR, ".key");
|
|
102
|
-
const NONCE_PATH = join(HARNESS_DIR, ".nonce");
|
|
103
|
-
/** Load (or create on first use) the per-machine HMAC key stored at mode 0600. */
|
|
104
|
-
function loadOrCreateKey() {
|
|
105
|
-
mkdirSync(HARNESS_DIR, { recursive: true });
|
|
106
|
-
if (!existsSync(KEY_PATH)) {
|
|
107
|
-
const key = randomBytes(32).toString("hex");
|
|
108
|
-
writeFileSync(KEY_PATH, key, {
|
|
109
|
-
encoding: "utf8",
|
|
110
|
-
mode: 384
|
|
111
|
-
});
|
|
112
|
-
return key;
|
|
113
|
-
}
|
|
114
|
-
return readFileSync(KEY_PATH, "utf8").trim();
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* Persist the last-seen nonce for advisory diagnostics (mode 0600).
|
|
118
|
-
* Never read during {@link verifyTrack} — see module JSDoc for rationale.
|
|
119
|
-
*/
|
|
120
|
-
function writeLastNonce(nonce) {
|
|
121
|
-
mkdirSync(HARNESS_DIR, { recursive: true });
|
|
122
|
-
writeFileSync(NONCE_PATH, String(nonce), {
|
|
123
|
-
encoding: "utf8",
|
|
124
|
-
mode: 384
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
/** Compute HMAC-SHA256 over the canonical message `"${nonce}:${data}"`. */
|
|
128
|
-
function computeMac(key, data, nonce) {
|
|
129
|
-
return createHmac("sha256", key).update(`${nonce}:${data}`).digest("hex");
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Sign a {@link SessionTrack} into a tamper-evident envelope. The nonce
|
|
133
|
-
* (`Date.now()`) is embedded in the MAC to bind the timestamp to the payload.
|
|
134
|
-
*/
|
|
135
|
-
function signTrack(track) {
|
|
136
|
-
const key = loadOrCreateKey();
|
|
137
|
-
const data = JSON.stringify(track);
|
|
138
|
-
const nonce = Date.now();
|
|
139
|
-
return {
|
|
140
|
-
data,
|
|
141
|
-
nonce,
|
|
142
|
-
mac: computeMac(key, data, nonce)
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Verify a {@link TrackEnvelope}. Returns the parsed {@link SessionTrack} on
|
|
147
|
-
* success, or `null` ONLY on MAC mismatch / parse failure (fail-closed on
|
|
148
|
-
* tampering). The nonce is NOT checked — see module JSDoc.
|
|
149
|
-
*/
|
|
150
|
-
function verifyTrack(envelope) {
|
|
151
|
-
try {
|
|
152
|
-
const key = loadOrCreateKey();
|
|
153
|
-
if (envelope.mac !== computeMac(key, envelope.data, envelope.nonce)) return null;
|
|
154
|
-
return JSON.parse(envelope.data);
|
|
155
|
-
} catch {
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
//#endregion
|
|
160
|
-
//#region src/tracking/store.ts
|
|
161
|
-
/**
|
|
162
|
-
* Load and verify a session track from a signed envelope file.
|
|
163
|
-
*
|
|
164
|
-
* Returns {@link emptyTrack} (fail closed) when the file is absent, corrupt, or
|
|
165
|
-
* fails MAC validation — so the gates re-require real agents rather than trust a
|
|
166
|
-
* forged track. Only a MAC mismatch triggers fail-closed; the nonce is advisory
|
|
167
|
-
* and is never checked during load.
|
|
168
|
-
*/
|
|
169
|
-
async function loadTrack(file) {
|
|
170
|
-
const envelope = await readJsonFile(file);
|
|
171
|
-
if (!envelope) return emptyTrack();
|
|
172
|
-
return verifyTrack(envelope) ?? emptyTrack();
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Sign and persist a session track as a tamper-evident envelope, then write the
|
|
176
|
-
* advisory nonce watermark.
|
|
177
|
-
*/
|
|
178
|
-
async function saveTrack(file, track) {
|
|
179
|
-
const envelope = signTrack(track);
|
|
180
|
-
await writeJsonFile(file, envelope);
|
|
181
|
-
writeLastNonce(envelope.nonce);
|
|
182
|
-
}
|
|
183
|
-
//#endregion
|
|
184
|
-
export { emptyTrack as a, recordDoc as c, trivialCount as d, agentsFresh as i, recordRefRead as l, saveTrack as n, recordAgent as o, verifyTrack as r, recordBrainstormRequired as s, loadTrack as t, recordTrivialEdit as u };
|