@fusengine/harness 0.1.32 → 0.1.34

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.
@@ -11,7 +11,7 @@ function claudeInit(command) {
11
11
  path: ".claude/settings.json",
12
12
  content: json({ hooks: {
13
13
  PreToolUse: [{
14
- matcher: "Write|Edit",
14
+ matcher: "Write|Edit|Bash",
15
15
  hooks: [{
16
16
  type: "command",
17
17
  command
@@ -113,7 +113,7 @@ const RUNNERS = {
113
113
  * Build the wiring file(s) for a harness, or null when it has no hook
114
114
  * integration (cli-mode harnesses use `harness check` in a pre-commit step).
115
115
  */
116
- function initFor(id, command = `npx harness hook ${id}`) {
116
+ function initFor(id, command = `npx -y @fusengine/harness hook ${id}`) {
117
117
  const make = RUNNERS[id];
118
118
  if (!make) return null;
119
119
  return [...make(command), {
@@ -3,7 +3,32 @@ import { t as RefMeta } from "../types-CY5qT2X1.mjs";
3
3
  import { t as AgentQuality } from "../session-state-DxNkIBZ2.mjs";
4
4
 
5
5
  //#region src/runtime/paths.d.ts
6
- /** Path to a session's track file (under a per-tool base dir). */
6
+ /**
7
+ * Compute a stable 8-char hex hash for a project directory path.
8
+ * Delegates to `hashText` (MD5, non-cryptographic — used as a stable dir key only).
9
+ *
10
+ * @param projectDir - Absolute path to the project root; defaults to CLAUDE_PROJECT_DIR/cwd.
11
+ * @returns 8-char lowercase hex string.
12
+ */
13
+ declare function projectHash(projectDir?: string): string;
14
+ /**
15
+ * Canonical base directory for per-project harness state.
16
+ * Resolves to: ~/.claude/fuse-harness/state/<projectHash>/
17
+ *
18
+ * @param projectDir - Optional override for hashing; defaults to CLAUDE_PROJECT_DIR/cwd.
19
+ * @returns Absolute directory path (not yet created on disk).
20
+ */
21
+ declare function defaultStateDir(projectDir?: string): string;
22
+ /**
23
+ * Absolute path to a session's track JSON file.
24
+ *
25
+ * The session identifier is sanitised to `[A-Za-z0-9_-]` before use in the filename.
26
+ *
27
+ * @param sessionId - Claude session identifier (raw value accepted; sanitised internally).
28
+ * @param baseDir - Override the base directory. Omit in production; pass an explicit
29
+ * temp path in unit tests to avoid touching $HOME.
30
+ * @returns Absolute path, e.g. ~/.claude/fuse-harness/state/a1b2c3d4/track-abc123.json
31
+ */
7
32
  declare function trackFile(sessionId: string, baseDir?: string): string;
8
33
  //#endregion
9
34
  //#region src/runtime/storage.d.ts
@@ -69,6 +94,8 @@ interface GateInput {
69
94
  windowMs?: number;
70
95
  isReplaceAll?: boolean;
71
96
  agentType?: string;
97
+ /** Absolute path to the session transcript (Claude `transcript_path`) for evidence-based freshness. */
98
+ transcriptPath?: string;
72
99
  }
73
100
  //#endregion
74
101
  //#region src/runtime/gate.d.ts
@@ -678,4 +705,4 @@ interface PreContext {
678
705
  */
679
706
  declare function handlePre(ctx: PreContext): Promise<HandleOutcome>;
680
707
  //#endregion
681
- export { Activity, DEFAULT_WINDOW_MS, DuplicationVerdict, type GateInput, HandleOptions, HandleOutcome, LifecycleInput, MCP_TTL_MS, McpIntercept, NormalizedEvent, PluginScope, PreContext, REQUIRED_AGENTS, SolidProfile, TRIVIAL_BUDGET, ToolEvent, activityFor, aipilotPostToolUse, cartoSessionStart, claudeHome, cleanupSession, countFiles, detectDuplication, detectSolidProfile, devContext, dispatchAipilot, dispatchLessons, dispatchLifecycle, dryGate, extractSymbols, fusengineCache, gate, generateEcosystemMap, generateProjectMap, getFileDesc, gitContext, handleHook, handlePre, harnessStateDir, injectRules, isMcpTool, isProject, isoUtc, lessonsFileFor, lessonsStateFileFor, lifecycleStdout, listChildren, loadEnriched, loadSecurityState, loadSessionState, logToolFailure, mcpPostStore, mcpPreIntercept, mergeLines, normalizeEvent, postEditContext, postEditTypescript, postTrackingSideEffects, preCommitGate, projectContext, promptSubmitContext, pruneEmptyDirs, purgeTtlTree, queryOf, readRules, recordActivity, removeOldFiles, respond, runSessionStartCleanups, sanitizeSessionId, saveApexState, saveSecurityState, saveSessionState, securityAdvisory, securityStateDir, securityStatePath, seoPostToolUse, seoPostToolUseResponse, sessionStartCore, sessionStatePath, sessionsDir, solidDetectStart, subagentCacheContext, taskContext, todayUtc, trackAgentMemory, trackEnrichment, trackFile, trackMcpResearch, trackSessionChanges, trackSkillRead, trackWatchResearch, trimLogFile, validateRulesLoaded, validateTeammateOutput, writePluginMap, writeTree };
708
+ export { Activity, DEFAULT_WINDOW_MS, DuplicationVerdict, type GateInput, HandleOptions, HandleOutcome, LifecycleInput, MCP_TTL_MS, McpIntercept, NormalizedEvent, PluginScope, PreContext, REQUIRED_AGENTS, SolidProfile, TRIVIAL_BUDGET, ToolEvent, activityFor, aipilotPostToolUse, cartoSessionStart, claudeHome, cleanupSession, countFiles, defaultStateDir, detectDuplication, detectSolidProfile, devContext, dispatchAipilot, dispatchLessons, dispatchLifecycle, dryGate, extractSymbols, fusengineCache, gate, generateEcosystemMap, generateProjectMap, getFileDesc, gitContext, handleHook, handlePre, harnessStateDir, injectRules, isMcpTool, isProject, isoUtc, lessonsFileFor, lessonsStateFileFor, lifecycleStdout, listChildren, loadEnriched, loadSecurityState, loadSessionState, logToolFailure, mcpPostStore, mcpPreIntercept, mergeLines, normalizeEvent, postEditContext, postEditTypescript, postTrackingSideEffects, preCommitGate, projectContext, projectHash, promptSubmitContext, pruneEmptyDirs, purgeTtlTree, queryOf, readRules, recordActivity, removeOldFiles, respond, runSessionStartCleanups, sanitizeSessionId, saveApexState, saveSecurityState, saveSessionState, securityAdvisory, securityStateDir, securityStatePath, seoPostToolUse, seoPostToolUseResponse, sessionStartCore, sessionStatePath, sessionsDir, solidDetectStart, subagentCacheContext, taskContext, todayUtc, trackAgentMemory, trackEnrichment, trackFile, trackMcpResearch, trackSessionChanges, trackSkillRead, trackWatchResearch, trimLogFile, validateRulesLoaded, validateTeammateOutput, writePluginMap, writeTree };
@@ -1,5 +1,5 @@
1
1
  import { r as projectLayout } from "../layout-C0jaaCQC.mjs";
2
- import { $ as detectSolidProfile, A as dispatchLessons, At as activityFor, B as mergeLines, C as securityStateDir, Ct as trackFile, D as dispatchLifecycle, Dt as mcpPostStore, E as trackEnrichment, Et as isMcpTool, F as writePluginMap, G as trackSessionChanges, H as getFileDesc, I as generateProjectMap, J as saveApexState, K as validateRulesLoaded, L as isProject, M as lessonsStateFileFor, N as cartoSessionStart, O as aipilotPostToolUse, Ot as mcpPreIntercept, P as generateEcosystemMap, Q as subagentCacheContext, R as writeTree, S as saveSecurityState, St as recordActivity, T as todayUtc, Tt as MCP_TTL_MS, U as listChildren, V as countFiles, W as postEditTypescript, X as validateTeammateOutput, Y as logToolFailure, Z as trackAgentMemory, _ as trackWatchResearch, _t as sessionStatePath, a as TRIVIAL_BUDGET, at as pruneEmptyDirs, b as isoUtc, bt as taskContext, c as detectDuplication, ct as trimLogFile, d as lifecycleStdout, dt as projectContext, et as solidDetectStart, f as postEditContext, ft as claudeHome, g as postTrackingSideEffects, gt as saveSessionState, h as securityAdvisory, ht as sanitizeSessionId, i as REQUIRED_AGENTS, it as sessionStartCore, j as lessonsFileFor, k as dispatchAipilot, kt as queryOf, l as dryGate, lt as devContext, m as seoPostToolUseResponse, mt as loadSessionState, n as handlePre, nt as readRules, o as gate, ot as purgeTtlTree, p as seoPostToolUse, pt as fusengineCache, q as cleanupSession, r as DEFAULT_WINDOW_MS, rt as runSessionStartCleanups, s as preCommitGate, st as removeOldFiles, t as handleHook, tt as injectRules, u as extractSymbols, ut as gitContext, v as trackMcpResearch, vt as sessionsDir, w as securityStatePath, wt as normalizeEvent, x as loadSecurityState, xt as respond, y as trackSkillRead, yt as promptSubmitContext, z as loadEnriched } from "../handle-DJqHZcml.mjs";
2
+ import { $ as detectSolidProfile, A as dispatchLessons, At as mcpPreIntercept, B as mergeLines, C as securityStateDir, Ct as defaultStateDir, D as dispatchLifecycle, Dt as MCP_TTL_MS, E as trackEnrichment, Et as normalizeEvent, F as writePluginMap, G as trackSessionChanges, H as getFileDesc, I as generateProjectMap, J as saveApexState, K as validateRulesLoaded, L as isProject, M as lessonsStateFileFor, Mt as activityFor, N as cartoSessionStart, O as aipilotPostToolUse, Ot as isMcpTool, P as generateEcosystemMap, Q as subagentCacheContext, R as writeTree, S as saveSecurityState, St as recordActivity, T as todayUtc, Tt as trackFile, U as listChildren, V as countFiles, W as postEditTypescript, X as validateTeammateOutput, Y as logToolFailure, Z as trackAgentMemory, _ as trackWatchResearch, _t as sessionStatePath, a as TRIVIAL_BUDGET, at as pruneEmptyDirs, b as isoUtc, bt as taskContext, c as detectDuplication, ct as trimLogFile, d as lifecycleStdout, dt as projectContext, et as solidDetectStart, f as postEditContext, ft as claudeHome, g as postTrackingSideEffects, gt as saveSessionState, h as securityAdvisory, ht as sanitizeSessionId, i as REQUIRED_AGENTS, it as sessionStartCore, j as lessonsFileFor, jt as queryOf, k as dispatchAipilot, kt as mcpPostStore, l as dryGate, lt as devContext, m as seoPostToolUseResponse, mt as loadSessionState, n as handlePre, nt as readRules, o as gate, ot as purgeTtlTree, p as seoPostToolUse, pt as fusengineCache, q as cleanupSession, r as DEFAULT_WINDOW_MS, rt as runSessionStartCleanups, s as preCommitGate, st as removeOldFiles, t as handleHook, tt as injectRules, u as extractSymbols, ut as gitContext, v as trackMcpResearch, vt as sessionsDir, w as securityStatePath, wt as projectHash, x as loadSecurityState, xt as respond, y as trackSkillRead, yt as promptSubmitContext, z as loadEnriched } from "../handle-DMHjTY1E.mjs";
3
3
  //#region src/runtime/storage.ts
4
4
  /**
5
5
  * The project's single state dir (`<root>/.harness`) — neutral + harness-agnostic,
@@ -9,4 +9,4 @@ function harnessStateDir(root) {
9
9
  return projectLayout(root).stateDir;
10
10
  }
11
11
  //#endregion
12
- export { DEFAULT_WINDOW_MS, MCP_TTL_MS, REQUIRED_AGENTS, TRIVIAL_BUDGET, activityFor, aipilotPostToolUse, cartoSessionStart, claudeHome, cleanupSession, countFiles, detectDuplication, detectSolidProfile, devContext, dispatchAipilot, dispatchLessons, dispatchLifecycle, dryGate, extractSymbols, fusengineCache, gate, generateEcosystemMap, generateProjectMap, getFileDesc, gitContext, handleHook, handlePre, harnessStateDir, injectRules, isMcpTool, isProject, isoUtc, lessonsFileFor, lessonsStateFileFor, lifecycleStdout, listChildren, loadEnriched, loadSecurityState, loadSessionState, logToolFailure, mcpPostStore, mcpPreIntercept, mergeLines, normalizeEvent, postEditContext, postEditTypescript, postTrackingSideEffects, preCommitGate, projectContext, promptSubmitContext, pruneEmptyDirs, purgeTtlTree, queryOf, readRules, recordActivity, removeOldFiles, respond, runSessionStartCleanups, sanitizeSessionId, saveApexState, saveSecurityState, saveSessionState, securityAdvisory, securityStateDir, securityStatePath, seoPostToolUse, seoPostToolUseResponse, sessionStartCore, sessionStatePath, sessionsDir, solidDetectStart, subagentCacheContext, taskContext, todayUtc, trackAgentMemory, trackEnrichment, trackFile, trackMcpResearch, trackSessionChanges, trackSkillRead, trackWatchResearch, trimLogFile, validateRulesLoaded, validateTeammateOutput, writePluginMap, writeTree };
12
+ export { DEFAULT_WINDOW_MS, MCP_TTL_MS, REQUIRED_AGENTS, TRIVIAL_BUDGET, activityFor, aipilotPostToolUse, cartoSessionStart, claudeHome, cleanupSession, countFiles, defaultStateDir, detectDuplication, detectSolidProfile, devContext, dispatchAipilot, dispatchLessons, dispatchLifecycle, dryGate, extractSymbols, fusengineCache, gate, generateEcosystemMap, generateProjectMap, getFileDesc, gitContext, handleHook, handlePre, harnessStateDir, injectRules, isMcpTool, isProject, isoUtc, lessonsFileFor, lessonsStateFileFor, lifecycleStdout, listChildren, loadEnriched, loadSecurityState, loadSessionState, logToolFailure, mcpPostStore, mcpPreIntercept, mergeLines, normalizeEvent, postEditContext, postEditTypescript, postTrackingSideEffects, preCommitGate, projectContext, projectHash, promptSubmitContext, pruneEmptyDirs, purgeTtlTree, queryOf, readRules, recordActivity, removeOldFiles, respond, runSessionStartCleanups, sanitizeSessionId, saveApexState, saveSecurityState, saveSessionState, securityAdvisory, securityStateDir, securityStatePath, seoPostToolUse, seoPostToolUseResponse, sessionStartCore, sessionStatePath, sessionsDir, solidDetectStart, subagentCacheContext, taskContext, todayUtc, trackAgentMemory, trackEnrichment, trackFile, trackMcpResearch, trackSessionChanges, trackSkillRead, trackWatchResearch, trimLogFile, validateRulesLoaded, validateTeammateOutput, writePluginMap, writeTree };
@@ -1,2 +1,2 @@
1
- import { a as ensureStateDir, c as stateFilePath, i as apexStateDir, l as acquireLock, n as taskCreate, o as loadState, r as taskStart, s as saveState, t as taskComplete } from "../state-ByhLeKyD.mjs";
1
+ import { a as ensureStateDir, c as stateFilePath, i as apexStateDir, l as acquireLock, n as taskCreate, o as loadState, r as taskStart, s as saveState, t as taskComplete } from "../state-BthKK4Jj.mjs";
2
2
  export { acquireLock, apexStateDir, ensureStateDir, loadState, saveState, stateFilePath, taskComplete, taskCreate, taskStart };
@@ -1,4 +1,4 @@
1
- import { i as writeJsonFile, n as ensureDir, r as readJsonFile } from "./json-io-CAn72gI4.mjs";
1
+ import { a as writeJsonFile, i as readJsonFile, n as ensureDir } from "./json-io-CvSumjtz.mjs";
2
2
  import { mkdir, rmdir } from "node:fs/promises";
3
3
  //#region src/state/lock.ts
4
4
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -0,0 +1,184 @@
1
+ import { a as writeJsonFile, i as readJsonFile } from "./json-io-CvSumjtz.mjs";
2
+ import { join } from "node:path";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { homedir } from "node:os";
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 = join(homedir(), ".claude", "fuse-harness");
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 { recordAgent as a, recordRefRead as c, emptyTrack as i, recordTrivialEdit as l, saveTrack as n, recordBrainstormRequired as o, agentsFresh as r, recordDoc as s, loadTrack as t, trivialCount as u };
@@ -1,9 +1,19 @@
1
1
  import { a as recordAgent, c as recordRefRead, i as emptyTrack, l as recordTrivialEdit, n as SessionTrack, o as recordBrainstormRequired, r as agentsFresh, s as recordDoc, t as AgentQuality, u as trivialCount } from "../session-state-DxNkIBZ2.mjs";
2
2
 
3
3
  //#region src/tracking/store.d.ts
4
- /** Load a session track from a file (an empty track if absent/corrupt). */
4
+ /**
5
+ * Load and verify a session track from a signed envelope file.
6
+ *
7
+ * Returns {@link emptyTrack} (fail closed) when the file is absent, corrupt, or
8
+ * fails MAC validation — so the gates re-require real agents rather than trust a
9
+ * forged track. Only a MAC mismatch triggers fail-closed; the nonce is advisory
10
+ * and is never checked during load.
11
+ */
5
12
  declare function loadTrack(file: string): Promise<SessionTrack>;
6
- /** Persist a session track. */
13
+ /**
14
+ * Sign and persist a session track as a tamper-evident envelope, then write the
15
+ * advisory nonce watermark.
16
+ */
7
17
  declare function saveTrack(file: string, track: SessionTrack): Promise<void>;
8
18
  //#endregion
9
19
  export { AgentQuality, SessionTrack, agentsFresh, emptyTrack, loadTrack, recordAgent, recordBrainstormRequired, recordDoc, recordRefRead, recordTrivialEdit, saveTrack, trivialCount };
@@ -1,2 +1,2 @@
1
- import { a as recordAgent, c as recordRefRead, i as emptyTrack, l as recordTrivialEdit, n as saveTrack, o as recordBrainstormRequired, r as agentsFresh, s as recordDoc, t as loadTrack, u as trivialCount } from "../store-D-ge2ZPI.mjs";
1
+ import { a as recordAgent, c as recordRefRead, i as emptyTrack, l as recordTrivialEdit, n as saveTrack, o as recordBrainstormRequired, r as agentsFresh, s as recordDoc, t as loadTrack, u as trivialCount } from "../store-7j02oGjt.mjs";
2
2
  export { agentsFresh, emptyTrack, loadTrack, recordAgent, recordBrainstormRequired, recordDoc, recordRefRead, recordTrivialEdit, saveTrack, trivialCount };
@@ -1,5 +1,5 @@
1
1
  import { r as resolveMaxLines } from "./limits-CHn8AIL1.mjs";
2
- import { k as countLines } from "./evaluate-9ch1K2kt.mjs";
2
+ import { k as countLines } from "./evaluate-zyxeVZPB.mjs";
3
3
  import { r as isDocConsulted, t as formatDocDeny } from "./doc-helpers-Dd_x1-tZ.mjs";
4
4
  import { t as routeReferences } from "./router-D8cVrI-s.mjs";
5
5
  import { join } from "node:path";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fusengine/harness",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
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",
@@ -1,66 +0,0 @@
1
- import { t as evaluate } from "./evaluate-9ch1K2kt.mjs";
2
- import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
3
- //#region src/adapters/claude/index.ts
4
- /**
5
- * Claude Code adapter — the thin Claude-only shim over the portable policy core.
6
- * Reads the hook stdin payload and emits hookSpecificOutput responses.
7
- */
8
- /** Read & parse the Claude hook payload from stdin (empty object on bad input). */
9
- async function readClaudeInput() {
10
- const text = await Bun.stdin.text();
11
- if (!text.trim()) return {};
12
- try {
13
- const parsed = JSON.parse(text);
14
- return typeof parsed === "object" && parsed !== null ? parsed : {};
15
- } catch {
16
- return {};
17
- }
18
- }
19
- /** A `deny` hook response for a given event. */
20
- function denyResponse(event, reason) {
21
- return JSON.stringify({ hookSpecificOutput: {
22
- hookEventName: event,
23
- permissionDecision: "deny",
24
- permissionDecisionReason: reason
25
- } });
26
- }
27
- /** An `additionalContext` injection response. */
28
- function contextResponse(event, text) {
29
- return JSON.stringify({ hookSpecificOutput: {
30
- hookEventName: event,
31
- additionalContext: text
32
- } });
33
- }
34
- /**
35
- * Render a portable {@link Prompt} as a Claude Code hook response:
36
- * `block` → `permissionDecision: deny`, `ask` → `permissionDecision: ask`
37
- * (interactive confirm), `inform` → `additionalContext`.
38
- */
39
- function toClaudeResponse(event, prompt) {
40
- const reason = formatPrompt(prompt);
41
- if (prompt.kind === "block") return denyResponse(event, reason);
42
- if (prompt.kind === "ask") return JSON.stringify({ hookSpecificOutput: {
43
- hookEventName: event,
44
- permissionDecision: "ask",
45
- permissionDecisionReason: reason
46
- } });
47
- return contextResponse(event, reason);
48
- }
49
- /**
50
- * Run the bundled policy over a Claude payload and return the native response
51
- * string (deny/ask/additionalContext), or null to allow.
52
- */
53
- function guard(input) {
54
- const result = evaluate({
55
- tool: input.tool_name ?? "Write",
56
- filePath: input.tool_input?.file_path,
57
- content: input.tool_input?.content ?? input.tool_input?.new_string,
58
- command: input.tool_input?.command
59
- });
60
- if (result.decision === "allow" || !result.prompt) return null;
61
- return toClaudeResponse(input.hook_event_name ?? "PreToolUse", result.prompt);
62
- }
63
- /** @deprecated use {@link guard}. Kept for back-compat. */
64
- const fileSizeGuard = guard;
65
- //#endregion
66
- export { readClaudeInput as a, guard as i, denyResponse as n, toClaudeResponse as o, fileSizeGuard as r, contextResponse as t };
@@ -1,89 +0,0 @@
1
- import { i as writeJsonFile, r as readJsonFile } from "./json-io-CAn72gI4.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
- trivialEdits: []
10
- };
11
- }
12
- /** Record a doc consultation (Context7/Exa) for a framework in this session. Immutable. */
13
- function recordDoc(track, framework, sessionId, source) {
14
- const prev = track.authorizations[framework] ?? {};
15
- const sessions = new Set(prev.doc_sessions ?? []);
16
- sessions.add(sessionId);
17
- const sources = new Set(prev.sources ?? (prev.source ? [prev.source] : []));
18
- sources.add(source);
19
- return {
20
- ...track,
21
- authorizations: {
22
- ...track.authorizations,
23
- [framework]: {
24
- ...prev,
25
- doc_sessions: [...sessions],
26
- sources: [...sources]
27
- }
28
- }
29
- };
30
- }
31
- /** Record that a SOLID reference file was read (deduped). Immutable. */
32
- function recordRefRead(track, path) {
33
- return track.refsRead.includes(path) ? track : {
34
- ...track,
35
- refsRead: [...track.refsRead, path]
36
- };
37
- }
38
- /** Record an agent/tool call with a timestamp + optional quality. Immutable. */
39
- function recordAgent(track, name, ts, quality) {
40
- const entry = quality ? {
41
- name,
42
- ts,
43
- quality
44
- } : {
45
- name,
46
- ts
47
- };
48
- return {
49
- ...track,
50
- agents: [...track.agents, entry]
51
- };
52
- }
53
- /** True when ALL of `names` ran within `windowMs` with non-insufficient quality. */
54
- function agentsFresh(track, names, windowMs, now) {
55
- const cutoff = now - windowMs;
56
- return names.every((n) => track.agents.some((a) => a.name === n && a.ts > cutoff && a.quality !== "insufficient"));
57
- }
58
- /** Record a trivial edit timestamp (sliding window; old evicted). Immutable. */
59
- function recordTrivialEdit(track, ts, windowMs, now) {
60
- const cutoff = now - windowMs;
61
- return {
62
- ...track,
63
- trivialEdits: [...(track.trivialEdits ?? []).filter((t) => t > cutoff), ts]
64
- };
65
- }
66
- /** Count trivial edits within the sliding window. */
67
- function trivialCount(track, windowMs, now) {
68
- const cutoff = now - windowMs;
69
- return (track.trivialEdits ?? []).filter((t) => t > cutoff).length;
70
- }
71
- /** Set the brainstorm-required flag (from creation-intent detection). Immutable. */
72
- function recordBrainstormRequired(track, required) {
73
- return {
74
- ...track,
75
- brainstormRequired: required
76
- };
77
- }
78
- //#endregion
79
- //#region src/tracking/store.ts
80
- /** Load a session track from a file (an empty track if absent/corrupt). */
81
- async function loadTrack(file) {
82
- return await readJsonFile(file) ?? emptyTrack();
83
- }
84
- /** Persist a session track. */
85
- async function saveTrack(file, track) {
86
- await writeJsonFile(file, track);
87
- }
88
- //#endregion
89
- export { recordAgent as a, recordRefRead as c, emptyTrack as i, recordTrivialEdit as l, saveTrack as n, recordBrainstormRequired as o, agentsFresh as r, recordDoc as s, loadTrack as t, trivialCount as u };