@fusengine/harness 0.1.33 → 0.1.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1,2 @@
1
- import { a as readClaudeInput, i as guard, n as denyResponse, o as toClaudeResponse, r as fileSizeGuard, t as contextResponse } from "../../claude-BatVYnAf.mjs";
1
+ import { a as readClaudeInput, i as guard, n as denyResponse, o as toClaudeResponse, r as fileSizeGuard, t as contextResponse } from "../../claude-3PqBGt_7.mjs";
2
2
  export { contextResponse, denyResponse, fileSizeGuard, guard, readClaudeInput, toClaudeResponse };
@@ -1,4 +1,4 @@
1
- import { t as evaluate } from "../../evaluate-9ch1K2kt.mjs";
1
+ import { t as evaluate } from "../../evaluate-zyxeVZPB.mjs";
2
2
  import { t as formatPrompt } from "../../types-ernB1Dy3.mjs";
3
3
  //#region src/adapters/cline/index.ts
4
4
  /**
@@ -1,2 +1,2 @@
1
- import { a as readClaudeInput, i as guard, n as denyResponse, t as contextResponse } from "../../claude-BatVYnAf.mjs";
1
+ import { a as readClaudeInput, i as guard, n as denyResponse, t as contextResponse } from "../../claude-3PqBGt_7.mjs";
2
2
  export { contextResponse, denyResponse, guard, readClaudeInput as readCodexInput };
@@ -1,4 +1,4 @@
1
- import { t as evaluate } from "../../evaluate-9ch1K2kt.mjs";
1
+ import { t as evaluate } from "../../evaluate-zyxeVZPB.mjs";
2
2
  import { t as formatPrompt } from "../../types-ernB1Dy3.mjs";
3
3
  //#region src/adapters/cursor/index.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { t as evaluate } from "../../evaluate-9ch1K2kt.mjs";
1
+ import { t as evaluate } from "../../evaluate-zyxeVZPB.mjs";
2
2
  import { t as formatPrompt } from "../../types-ernB1Dy3.mjs";
3
3
  //#region src/adapters/gemini/index.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { t as evaluate } from "./evaluate-9ch1K2kt.mjs";
1
+ import { t as evaluate } from "./evaluate-zyxeVZPB.mjs";
2
2
  import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
3
3
  import { dirname, join } from "node:path";
4
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
package/dist/cli/bin.mjs CHANGED
@@ -1,15 +1,87 @@
1
1
  #!/usr/bin/env node
2
2
  import { r as resolveTtlSec } from "../ttl-BG55s6HZ.mjs";
3
3
  import { t as detectHarness } from "../harness-C8Nxxyn_.mjs";
4
- import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-CQbtlAKa.mjs";
4
+ import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-CUL70W0k.mjs";
5
5
  import { n as writeInitFile, t as initFor } from "../run-Do2JltgU.mjs";
6
- import { t as handleHook } from "../handle-Cxgzd4pZ.mjs";
6
+ import { Et as claudeHome, Tt as todayUtc, t as handleHook } from "../handle-CYlA6TeF.mjs";
7
+ import { join } from "node:path";
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ //#region src/changelog/fetch.ts
11
+ /**
12
+ * Changelog scanner — ports the changelog-watcher plugin's `fetch-changelog`
13
+ * into the harness (exposed as the `harness changelog` CLI verb). Fetches the
14
+ * official Claude Code changelog, detects how many versions are new since the
15
+ * last check, persists per-day state, and returns a JSON summary. Dual-runtime:
16
+ * global `fetch` + `node:fs` (works under Node 20+ and Bun, no imports needed).
17
+ */
18
+ const CHANGELOG_URL = "https://code.claude.com/docs/en/changelog.md";
19
+ /**
20
+ * Parse up to 10 semver versions from the changelog, newest first. Matches the
21
+ * current docs format (`<Update label="X.Y.Z" …>` MDX blocks) AND the legacy
22
+ * markdown headers (`## vX.Y.Z` / `## X.Y.Z`) so it survives a format rollback.
23
+ */
24
+ function parseVersions(md) {
25
+ return [...md.matchAll(/<Update\s+label="v?(\d+\.\d+\.\d+)"|^##\s+v?(\d+\.\d+\.\d+)/gm)].map((m) => m[1] ?? m[2] ?? "").filter(Boolean).slice(0, 10);
26
+ }
27
+ /** Count versions newer than `lastKnown` (stops at the first match). */
28
+ function countNew(versions, lastKnown) {
29
+ if (!lastKnown) return 0;
30
+ let n = 0;
31
+ for (const v of versions) {
32
+ if (v === lastKnown) break;
33
+ n++;
34
+ }
35
+ return n;
36
+ }
37
+ /** Read the saved `last_version` for today's state file ("" when absent/corrupt). */
38
+ function lastKnownVersion(stateFile) {
39
+ if (!existsSync(stateFile)) return "";
40
+ try {
41
+ return JSON.parse(readFileSync(stateFile, "utf8")).last_version ?? "";
42
+ } catch {
43
+ return "";
44
+ }
45
+ }
46
+ /**
47
+ * Fetch + parse the changelog, diff against the saved state, persist, and return
48
+ * the scan summary. Throws on network failure (the CLI maps it to exit 1).
49
+ * @param now - Clock (ms).
50
+ * @param home - Home dir.
51
+ */
52
+ async function scanChangelog(now = Date.now(), home = homedir()) {
53
+ const res = await fetch(CHANGELOG_URL, { signal: AbortSignal.timeout(1e4) });
54
+ if (!res.ok) throw new Error(`changelog fetch failed: ${res.status}`);
55
+ const versions = parseVersions(await res.text());
56
+ const latest = versions[0] ?? "";
57
+ const dir = join(claudeHome(home), "logs", "00-changelog");
58
+ const today = todayUtc(now);
59
+ const stateFile = join(dir, `${today}-state.json`);
60
+ const lastKnown = lastKnownVersion(stateFile);
61
+ const newCount = countNew(versions, lastKnown);
62
+ try {
63
+ mkdirSync(dir, { recursive: true });
64
+ writeFileSync(stateFile, JSON.stringify({
65
+ last_version: latest,
66
+ previous: lastKnown,
67
+ new_versions: newCount,
68
+ checked: today
69
+ }, null, 2));
70
+ } catch {}
71
+ return {
72
+ latest,
73
+ new_since_last_check: newCount,
74
+ recent_versions: versions
75
+ };
76
+ }
77
+ //#endregion
7
78
  //#region src/cli/bin.ts
8
79
  /**
9
80
  * harness — CLI for @fusengine/harness.
10
81
  * harness check cli-mode: check staged files (pre-commit), exit non-zero on a violation
11
82
  * harness init [id] write the wiring file for a harness (defaults to the detected one)
12
83
  * harness hook <id> runtime: read a hook payload on stdin, route to the adapter, print the response
84
+ * harness changelog fetch + diff the Claude Code changelog, print a JSON summary (changelog-watcher)
13
85
  */
14
86
  async function readStdin() {
15
87
  const chunks = [];
@@ -56,7 +128,17 @@ if (cmd === "hook") {
56
128
  const written = files.map((f) => writeInitFile(process.cwd(), f));
57
129
  process.stdout.write(`harness: wired ${id} -> ${written.join(", ")}\n`);
58
130
  process.exit(0);
59
- } else {
131
+ } else if (cmd === "changelog") try {
132
+ process.stdout.write(JSON.stringify(await scanChangelog()) + "\n");
133
+ process.exit(0);
134
+ } catch (e) {
135
+ process.stdout.write(JSON.stringify({
136
+ status: "error",
137
+ message: e instanceof Error ? e.message : "changelog fetch failed"
138
+ }) + "\n");
139
+ process.exit(1);
140
+ }
141
+ else {
60
142
  const files = stagedFiles();
61
143
  if (files.length === 0) process.exit(0);
62
144
  const violations = checkStaged(files, stagedContent);
@@ -1,2 +1,2 @@
1
- import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-CQbtlAKa.mjs";
1
+ import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-CUL70W0k.mjs";
2
2
  export { checkStaged, stagedContent, stagedFiles };
@@ -192,24 +192,58 @@ function securityGuard(ctx) {
192
192
  }
193
193
  //#endregion
194
194
  //#region src/policy/guards/protected-path.ts
195
- /** Path fragments that mark a location as internal/generated state (off-limits to Write/Edit). */
195
+ /** Path fragments that mark a location as internal/generated state. */
196
196
  const PROTECTED_FRAGMENTS = [
197
197
  ".claude/plugins/marketplaces",
198
198
  ".claude/plugins/cache",
199
199
  ".claude/logs/00-apex",
200
200
  ".claude/fusengine-cache",
201
- ".git/"
201
+ ".git/",
202
+ ".claude/apex/",
203
+ "/fuse-harness/",
204
+ ".harness/track",
205
+ ".harness/memory/state"
202
206
  ];
203
- /** Blocks direct edits to internal/generated state directories. */
207
+ /** Standard block response for any protected-path violation. */
208
+ const BLOCK = {
209
+ kind: "block",
210
+ title: "Protected path",
211
+ reason: "This is internal/generated enforcement state — do not edit it directly.",
212
+ actions: ["Edit the source, not the generated/cache/state copy"]
213
+ };
214
+ /** Returns true if `str` contains any protected fragment. */
215
+ function containsProtected(str) {
216
+ return PROTECTED_FRAGMENTS.some((f) => str.includes(f));
217
+ }
218
+ /**
219
+ * Returns true if `cmd` contains a recognisable shell write operation.
220
+ *
221
+ * Best-effort: matches `>` / `>>` redirections, `tee`, `cp`, `mv`, `dd`, `sed -i`.
222
+ * Obfuscated shell (base64-decoded payloads, variable indirection, process
223
+ * substitution) can still evade this check — residual risk, documented. The
224
+ * real guarantee against a forged track is the transcript-grounded freshness
225
+ * gate (see `freshness/agent-evidence`), not this guard.
226
+ */
227
+ function bashHasWriteOp(cmd) {
228
+ return />/.test(cmd) || /\btee\b/.test(cmd) || /\bcp\b/.test(cmd) || /\bmv\b/.test(cmd) || /\bdd\b/.test(cmd) || /\bsed\s+-[a-zA-Z]*i/.test(cmd);
229
+ }
230
+ /**
231
+ * Blocks direct edits to internal/generated state directories.
232
+ *
233
+ * Covers:
234
+ * - Write / Edit tool calls whose `filePath` targets a protected fragment.
235
+ * - Bash commands that both reference a protected fragment *and* contain a
236
+ * recognisable shell write operation (best-effort; see `bashHasWriteOp`).
237
+ *
238
+ * @param ctx - The guard context (tool, filePath, command).
239
+ * @returns A blocking {@link Prompt}, or null to allow.
240
+ */
204
241
  function protectedPathGuard(ctx) {
205
242
  if ((ctx.tool === "Write" || ctx.tool === "Edit") && ctx.filePath) {
206
- const path = ctx.filePath;
207
- if (PROTECTED_FRAGMENTS.some((fragment) => path.includes(fragment))) return {
208
- kind: "block",
209
- title: "Protected path",
210
- reason: "This is internal/generated state — do not edit it directly.",
211
- actions: ["Edit the source, not the generated/cache copy"]
212
- };
243
+ if (containsProtected(ctx.filePath)) return BLOCK;
244
+ }
245
+ if (ctx.tool === "Bash" && ctx.command) {
246
+ if (containsProtected(ctx.command) && bashHasWriteOp(ctx.command)) return BLOCK;
213
247
  }
214
248
  return null;
215
249
  }
@@ -1,3 +1,3 @@
1
1
  import { i as resolveSessions, n as formatDocSatisfactionStatus, r as isDocConsulted, t as formatDocDeny } from "../doc-helpers-Dd_x1-tZ.mjs";
2
- import { t as incrementTrivialEditCounter } from "../freshness-otdUpuvP.mjs";
2
+ import { t as incrementTrivialEditCounter } from "../freshness-43gxYpiX.mjs";
3
3
  export { formatDocDeny, formatDocSatisfactionStatus, incrementTrivialEditCounter, isDocConsulted, resolveSessions };
@@ -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 { dirname } from "node:path";
3
3
  //#region src/freshness/trivial-edit-counter.ts
4
4
  /**
@@ -1,21 +1,103 @@
1
1
  import { r as resolveMaxLines } from "./limits-CHn8AIL1.mjs";
2
2
  import { r as projectLayout } from "./layout-C0jaaCQC.mjs";
3
3
  import { i as walkUpFor, n as projectRoot, r as projectRootOrNull, t as isCodeFile } from "./project-root-3kk7gCOp.mjs";
4
- import { A as evaluateApex, E as detectCreationIntent, L as requiredArchSkill, P as detectModularArchitecture, S as frameworkSolidGate, T as capVerbosity, _ as buildClaudeMdContext, b as skillTriggerGate, c as parseBodyDesc, d as PROJECT_INDICATORS, l as parseField, n as missingSeoElements, o as parseEnrichment, p as buildApexTaskInjection, r as descFromText, s as parseEntry, t as isHtmlLike, u as EXCLUDE_DIRS$1 } from "./validate-CccewDwk.mjs";
5
- import { j as detectFramework, k as countLines, n as FAIL_CLOSED, t as evaluate } from "./evaluate-9ch1K2kt.mjs";
4
+ import { A as evaluateApex, E as detectCreationIntent, L as requiredArchSkill, P as detectModularArchitecture, S as frameworkSolidGate, T as capVerbosity, _ as buildClaudeMdContext, b as skillTriggerGate, c as parseBodyDesc, d as PROJECT_INDICATORS, l as parseField, n as missingSeoElements, o as parseEnrichment, p as buildApexTaskInjection, r as descFromText, s as parseEntry, t as isHtmlLike, u as EXCLUDE_DIRS$1 } from "./validate-DLrWtaDR.mjs";
5
+ import { j as detectFramework, k as countLines, n as FAIL_CLOSED, t as evaluate } from "./evaluate-zyxeVZPB.mjs";
6
6
  import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
7
7
  import { a as nowStamp, l as throttleMs, n as readRoots, o as readState, s as setStateField, t as addRoot } from "./registry-BkoEbdec.mjs";
8
8
  import { a as extractText, o as loadIndex, r as cacheStore, t as cacheLookup } from "./store-PrNPm6So.mjs";
9
- import { i as writeJsonFile, r as readJsonFile, t as atomicWrite } from "./json-io-CAn72gI4.mjs";
9
+ import { a as writeJsonFile, i as readJsonFile, r as hashText, t as atomicWrite } from "./json-io-CvSumjtz.mjs";
10
10
  import { t as loadRefs } from "./loader-CyAoJv2W.mjs";
11
- import { a as recordAgent, c as recordRefRead, 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";
12
- import { c as pathExists, d as spawnCapture, f as writeText, l as readText, n as denyResponse, s as collectFiles, t as contextResponse, u as sleep } from "./claude-BatVYnAf.mjs";
11
+ import { a as recordAgent, c as recordRefRead, 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";
12
+ import { c as pathExists, d as spawnCapture, f as writeText, l as readText, n as denyResponse, s as collectFiles, t as contextResponse, u as sleep } from "./claude-3PqBGt_7.mjs";
13
13
  import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
14
14
  import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
15
- import { homedir, tmpdir } from "node:os";
15
+ import { homedir } from "node:os";
16
16
  import { createHash } from "node:crypto";
17
17
  import { mkdir, rmdir } from "node:fs/promises";
18
18
  import { execFileSync } from "node:child_process";
19
+ //#region src/runtime/home-state.ts
20
+ /** Home `~/.claude` dir (single source for every home-based hook path). */
21
+ function claudeHome(home = homedir()) {
22
+ return join(home, ".claude");
23
+ }
24
+ /** `~/.claude/fusengine-cache` base dir for legacy session/cache state. */
25
+ function fusengineCache(home = homedir()) {
26
+ return join(claudeHome(home), "fusengine-cache");
27
+ }
28
+ /** `~/.claude/fusengine-cache/sessions` — per-session JSON state dir. */
29
+ function sessionsDir(home = homedir()) {
30
+ return join(fusengineCache(home), "sessions");
31
+ }
32
+ const SID_RE = /^[a-zA-Z0-9_-]{1,128}$/;
33
+ /** Validate a session id (1-128 url-safe chars); null when invalid. */
34
+ function sanitizeSessionId(sid) {
35
+ const s = String(sid ?? "").trim();
36
+ return SID_RE.test(s) ? s : null;
37
+ }
38
+ /** Unified per-session state file path: `sessions/session-<sid>.json`. */
39
+ function sessionStatePath(sid, home = homedir()) {
40
+ return join(sessionsDir(home), `session-${sid}.json`);
41
+ }
42
+ /** Load a session-state dict, or `{}` when missing/corrupt (mirrors Python). */
43
+ function loadSessionState(sid, home = homedir()) {
44
+ const path = sessionStatePath(sid, home);
45
+ try {
46
+ if (!existsSync(path)) return {};
47
+ const data = JSON.parse(readFileSync(path, "utf-8"));
48
+ return typeof data === "object" && data !== null && !Array.isArray(data) ? data : {};
49
+ } catch {
50
+ return {};
51
+ }
52
+ }
53
+ /** Atomically persist a session-state dict (0o600 via atomicWrite, indent 2). */
54
+ function saveSessionState(sid, state, home = homedir()) {
55
+ mkdirSync(sessionsDir(home), {
56
+ recursive: true,
57
+ mode: 448
58
+ });
59
+ atomicWrite(sessionStatePath(sid, home), JSON.stringify(state, null, 2));
60
+ }
61
+ //#endregion
62
+ //#region src/runtime/lifecycle/security/skill-state.ts
63
+ /**
64
+ * Shared security-tracker state: per-UTC-day JSON under
65
+ * `~/.claude/logs/00-security`. Ports the state helpers of
66
+ * `check-security-skill.py` / `track-skill-read.py` / `track-mcp-research.py`.
67
+ */
68
+ /** `~/.claude/logs/00-security` state directory. */
69
+ function securityStateDir(home = homedir()) {
70
+ return join(claudeHome(home), "logs", "00-security");
71
+ }
72
+ /** Current UTC date as `YYYY-MM-DD`. */
73
+ function todayUtc(now = Date.now()) {
74
+ return new Date(now).toISOString().slice(0, 10);
75
+ }
76
+ /** Current UTC instant as `YYYY-MM-DDTHH:MM:SSZ` (seconds, no millis). */
77
+ function isoUtc(now = Date.now()) {
78
+ return new Date(now).toISOString().replace(/\.\d{3}Z$/, "Z");
79
+ }
80
+ /** Today's security-state file path. */
81
+ function securityStatePath(now = Date.now(), home = homedir()) {
82
+ return join(securityStateDir(home), `${todayUtc(now)}-state.json`);
83
+ }
84
+ /** Load today's security state, or `{}` when missing/corrupt. */
85
+ function loadSecurityState(now = Date.now(), home = homedir()) {
86
+ const path = securityStatePath(now, home);
87
+ try {
88
+ if (!existsSync(path)) return {};
89
+ const data = JSON.parse(readFileSync(path, "utf-8"));
90
+ return typeof data === "object" && data !== null && !Array.isArray(data) ? data : {};
91
+ } catch {
92
+ return {};
93
+ }
94
+ }
95
+ /** Persist today's security state (indent 2, no trailing newline). */
96
+ function saveSecurityState(state, now = Date.now(), home = homedir()) {
97
+ mkdirSync(securityStateDir(home), { recursive: true });
98
+ writeFileSync(securityStatePath(now, home), JSON.stringify(state, null, 2), "utf-8");
99
+ }
100
+ //#endregion
19
101
  //#region src/runtime/activity.ts
20
102
  /** Min response length (chars) for a lead agent call to count as `sufficient`. */
21
103
  const AGENT_QUALITY_MIN = 500;
@@ -169,9 +251,54 @@ function normalizeEvent(id, payload) {
169
251
  }
170
252
  //#endregion
171
253
  //#region src/runtime/paths.ts
172
- /** Path to a session's track file (under a per-tool base dir). */
173
- function trackFile(sessionId, baseDir = join(tmpdir(), "fuse-harness")) {
174
- return join(baseDir, `track-${sessionId.replace(/[^A-Za-z0-9_-]/g, "_") || "default"}.json`);
254
+ /**
255
+ * @module paths
256
+ * Runtime path helpers — per-project, out-of-tree harness state.
257
+ *
258
+ * Default base: ~/.claude/fuse-harness/state/<projectHash>/
259
+ * where projectHash = 8-char MD5 of CLAUDE_PROJECT_DIR (or cwd).
260
+ * Persistent and not world-writable (unlike /tmp), and OUTSIDE the repo so the
261
+ * agent has no "legitimate" reason to write it (the protected-path guard denies
262
+ * it, and the gate verifies freshness from the transcript, not this file).
263
+ *
264
+ * @packageDocumentation
265
+ */
266
+ /** Resolve the project root from the environment or fall back to cwd. */
267
+ function resolveProjectDir() {
268
+ return process.env["CLAUDE_PROJECT_DIR"] ?? process.cwd();
269
+ }
270
+ /**
271
+ * Compute a stable 8-char hex hash for a project directory path.
272
+ * Delegates to `hashText` (MD5, non-cryptographic — used as a stable dir key only).
273
+ *
274
+ * @param projectDir - Absolute path to the project root; defaults to CLAUDE_PROJECT_DIR/cwd.
275
+ * @returns 8-char lowercase hex string.
276
+ */
277
+ function projectHash$1(projectDir) {
278
+ return hashText(projectDir ?? resolveProjectDir());
279
+ }
280
+ /**
281
+ * Canonical base directory for per-project harness state.
282
+ * Resolves to: ~/.claude/fuse-harness/state/<projectHash>/
283
+ *
284
+ * @param projectDir - Optional override for hashing; defaults to CLAUDE_PROJECT_DIR/cwd.
285
+ * @returns Absolute directory path (not yet created on disk).
286
+ */
287
+ function defaultStateDir(projectDir) {
288
+ return join(homedir(), ".claude", "fuse-harness", "state", projectHash$1(projectDir));
289
+ }
290
+ /**
291
+ * Absolute path to a session's track JSON file.
292
+ *
293
+ * The session identifier is sanitised to `[A-Za-z0-9_-]` before use in the filename.
294
+ *
295
+ * @param sessionId - Claude session identifier (raw value accepted; sanitised internally).
296
+ * @param baseDir - Override the base directory. Omit in production; pass an explicit
297
+ * temp path in unit tests to avoid touching $HOME.
298
+ * @returns Absolute path, e.g. ~/.claude/fuse-harness/state/a1b2c3d4/track-abc123.json
299
+ */
300
+ function trackFile(sessionId, baseDir) {
301
+ return join(baseDir ?? defaultStateDir(), `track-${sessionId.replace(/[^A-Za-z0-9_-]/g, "_") || "default"}.json`);
175
302
  }
176
303
  //#endregion
177
304
  //#region src/runtime/record.ts
@@ -626,49 +753,6 @@ function taskContext(cwd) {
626
753
  return ctx ? contextResponse("PreToolUse", ctx) : "";
627
754
  }
628
755
  //#endregion
629
- //#region src/runtime/home-state.ts
630
- /** Home `~/.claude` dir (single source for every home-based hook path). */
631
- function claudeHome(home = homedir()) {
632
- return join(home, ".claude");
633
- }
634
- /** `~/.claude/fusengine-cache` base dir for legacy session/cache state. */
635
- function fusengineCache(home = homedir()) {
636
- return join(claudeHome(home), "fusengine-cache");
637
- }
638
- /** `~/.claude/fusengine-cache/sessions` — per-session JSON state dir. */
639
- function sessionsDir(home = homedir()) {
640
- return join(fusengineCache(home), "sessions");
641
- }
642
- const SID_RE = /^[a-zA-Z0-9_-]{1,128}$/;
643
- /** Validate a session id (1-128 url-safe chars); null when invalid. */
644
- function sanitizeSessionId(sid) {
645
- const s = String(sid ?? "").trim();
646
- return SID_RE.test(s) ? s : null;
647
- }
648
- /** Unified per-session state file path: `sessions/session-<sid>.json`. */
649
- function sessionStatePath(sid, home = homedir()) {
650
- return join(sessionsDir(home), `session-${sid}.json`);
651
- }
652
- /** Load a session-state dict, or `{}` when missing/corrupt (mirrors Python). */
653
- function loadSessionState(sid, home = homedir()) {
654
- const path = sessionStatePath(sid, home);
655
- try {
656
- if (!existsSync(path)) return {};
657
- const data = JSON.parse(readFileSync(path, "utf-8"));
658
- return typeof data === "object" && data !== null && !Array.isArray(data) ? data : {};
659
- } catch {
660
- return {};
661
- }
662
- }
663
- /** Atomically persist a session-state dict (0o600 via atomicWrite, indent 2). */
664
- function saveSessionState(sid, state, home = homedir()) {
665
- mkdirSync(sessionsDir(home), {
666
- recursive: true,
667
- mode: 448
668
- });
669
- atomicWrite(sessionStatePath(sid, home), JSON.stringify(state, null, 2));
670
- }
671
- //#endregion
672
756
  //#region src/runtime/dev-context.ts
673
757
  /** Run a git subcommand in `cwd`, returning trimmed stdout or "" on error. */
674
758
  function git(cwd, args) {
@@ -2972,45 +3056,6 @@ function trackEnrichment(filePath) {
2972
3056
  } catch {}
2973
3057
  }
2974
3058
  //#endregion
2975
- //#region src/runtime/lifecycle/security/skill-state.ts
2976
- /**
2977
- * Shared security-tracker state: per-UTC-day JSON under
2978
- * `~/.claude/logs/00-security`. Ports the state helpers of
2979
- * `check-security-skill.py` / `track-skill-read.py` / `track-mcp-research.py`.
2980
- */
2981
- /** `~/.claude/logs/00-security` state directory. */
2982
- function securityStateDir(home = homedir()) {
2983
- return join(claudeHome(home), "logs", "00-security");
2984
- }
2985
- /** Current UTC date as `YYYY-MM-DD`. */
2986
- function todayUtc(now = Date.now()) {
2987
- return new Date(now).toISOString().slice(0, 10);
2988
- }
2989
- /** Current UTC instant as `YYYY-MM-DDTHH:MM:SSZ` (seconds, no millis). */
2990
- function isoUtc(now = Date.now()) {
2991
- return new Date(now).toISOString().replace(/\.\d{3}Z$/, "Z");
2992
- }
2993
- /** Today's security-state file path. */
2994
- function securityStatePath(now = Date.now(), home = homedir()) {
2995
- return join(securityStateDir(home), `${todayUtc(now)}-state.json`);
2996
- }
2997
- /** Load today's security state, or `{}` when missing/corrupt. */
2998
- function loadSecurityState(now = Date.now(), home = homedir()) {
2999
- const path = securityStatePath(now, home);
3000
- try {
3001
- if (!existsSync(path)) return {};
3002
- const data = JSON.parse(readFileSync(path, "utf-8"));
3003
- return typeof data === "object" && data !== null && !Array.isArray(data) ? data : {};
3004
- } catch {
3005
- return {};
3006
- }
3007
- }
3008
- /** Persist today's security state (indent 2, no trailing newline). */
3009
- function saveSecurityState(state, now = Date.now(), home = homedir()) {
3010
- mkdirSync(securityStateDir(home), { recursive: true });
3011
- writeFileSync(securityStatePath(now, home), JSON.stringify(state, null, 2), "utf-8");
3012
- }
3013
- //#endregion
3014
3059
  //#region src/runtime/lifecycle/security/track-skill-read.ts
3015
3060
  /**
3016
3061
  * Security skill-read tracker (PostToolUse Read). Ports `track-skill-read.py`:
@@ -3235,6 +3280,90 @@ function postEditContext(scope, event, now) {
3235
3280
  return trackSessionChanges(event.sessionId, event.filePath, void 0, now) || postEditTypescript(event.filePath);
3236
3281
  }
3237
3282
  //#endregion
3283
+ //#region src/runtime/gate-helpers.ts
3284
+ /**
3285
+ * Code-only line count of the existing on-disk file (undefined if
3286
+ * absent/unreadable). Uses {@link countLines} (skips blank/comment lines) so a
3287
+ * partial Edit judges the full file by the SAME metric as the incoming snippet —
3288
+ * a raw `split("\n").length` would over-count JSDoc/blank lines (and add a
3289
+ * trailing-newline off-by-one), falsely blocking well-documented files.
3290
+ * @param path - Absolute path of the file being edited (or undefined).
3291
+ * @returns Code-only line count, or undefined when the file is absent/unreadable.
3292
+ */
3293
+ function existingLineCount(path) {
3294
+ if (!path) return void 0;
3295
+ try {
3296
+ return existsSync(path) ? countLines(readFileSync(path, "utf8")) : void 0;
3297
+ } catch {
3298
+ return;
3299
+ }
3300
+ }
3301
+ //#endregion
3302
+ //#region src/freshness/agent-evidence.ts
3303
+ /**
3304
+ * Platform-authored transcript evidence for APEX agent freshness.
3305
+ * Parses the Claude Code session JSONL transcript to find genuine Task
3306
+ * tool_use entries — forging this requires writing into the transcript file
3307
+ * which the Claude Code platform controls, unlike the self-recorded track.
3308
+ */
3309
+ /** Parse a raw `timestamp` field to epoch ms; `undefined` when absent or invalid. */
3310
+ function parseTs(raw) {
3311
+ if (raw === void 0) return void 0;
3312
+ if (typeof raw === "number") return raw;
3313
+ const ms = Date.parse(raw);
3314
+ return Number.isFinite(ms) ? ms : void 0;
3315
+ }
3316
+ /**
3317
+ * Return `true` ONLY when, for EVERY name in `names`, the Claude Code
3318
+ * transcript at `transcriptPath` contains a genuine `tool_use` of the `Task`
3319
+ * tool whose `subagent_type` (or `name`) input field matches, with the entry
3320
+ * timestamp within `windowMs` of `now`.
3321
+ *
3322
+ * **Timestamp note:** when a transcript entry carries no `timestamp` field it
3323
+ * is counted as within-window (we cannot prove staleness). This is
3324
+ * intentionally lenient to stay robust across transcript-format evolution; the
3325
+ * tamper-resistance guarantee derives from the platform authoring the file —
3326
+ * not from the timestamp alone.
3327
+ *
3328
+ * @param transcriptPath - Absolute path to the session `.jsonl` transcript
3329
+ * (hook payload field: `transcript_path`). Returns `false` when `undefined`.
3330
+ * @param names - Required agent `subagent_type` values — ALL must appear.
3331
+ * @param windowMs - Freshness window in milliseconds.
3332
+ * @param now - Current epoch ms (pass `Date.now()` at the call-site).
3333
+ * @returns `true` when ALL agents have real, within-window transcript evidence.
3334
+ */
3335
+ function agentsRanFromTranscript(transcriptPath, names, windowMs, now) {
3336
+ if (!transcriptPath || names.length === 0) return false;
3337
+ let text;
3338
+ try {
3339
+ text = readText(transcriptPath);
3340
+ } catch {
3341
+ return false;
3342
+ }
3343
+ const cutoff = now - windowMs;
3344
+ const found = /* @__PURE__ */ new Set();
3345
+ for (const line of text.split("\n")) {
3346
+ if (!line.trim()) continue;
3347
+ let entry;
3348
+ try {
3349
+ entry = JSON.parse(line);
3350
+ } catch {
3351
+ continue;
3352
+ }
3353
+ const ts = parseTs(entry.timestamp);
3354
+ if (ts !== void 0 && ts <= cutoff) continue;
3355
+ const content = entry.message?.content;
3356
+ if (!Array.isArray(content)) continue;
3357
+ for (const block of content) {
3358
+ if (block?.type !== "tool_use" || block.name !== "Task") continue;
3359
+ const agent = block.input?.subagent_type ?? block.input?.name;
3360
+ if (typeof agent === "string" && names.includes(agent)) found.add(agent);
3361
+ }
3362
+ if (found.size === names.length) return true;
3363
+ }
3364
+ return names.every((n) => found.has(n));
3365
+ }
3366
+ //#endregion
3238
3367
  //#region src/runtime/dry-patterns.ts
3239
3368
  /** Short identifiers never worth a duplication check (control flow, tiny names). */
3240
3369
  const DRY_KEYWORDS = /* @__PURE__ */ new Set([
@@ -3570,22 +3699,6 @@ const DEFAULT_WINDOW_MS = 12e4;
3570
3699
  /** Trivial edits allowed within the window before the full APEX gates apply. */
3571
3700
  const TRIVIAL_BUDGET = 4;
3572
3701
  /**
3573
- * Code-only line count of the existing on-disk file (undefined if
3574
- * absent/unreadable). Uses {@link countLines} (skips blank/comment lines) to
3575
- * mirror the Python `count_code_lines(get_full_file_content(...))`, so a partial
3576
- * Edit judges the full file by the SAME metric as the incoming snippet — a raw
3577
- * `split("\n").length` would over-count JSDoc/blank lines (and add a
3578
- * trailing-newline off-by-one), falsely blocking well-documented files.
3579
- */
3580
- function existingLineCount(path) {
3581
- if (!path) return void 0;
3582
- try {
3583
- return existsSync(path) ? countLines(readFileSync(path, "utf8")) : void 0;
3584
- } catch {
3585
- return;
3586
- }
3587
- }
3588
- /**
3589
3702
  * Full gate: the stateless guards (file-size, git, security...) first, then a
3590
3703
  * trivial-edit fast path, then the stateful APEX gates fed from the session
3591
3704
  * track. Returns the first blocking prompt, or null to allow.
@@ -3620,6 +3733,7 @@ async function gate(input) {
3620
3733
  await saveTrack(input.trackFile, recordTrivialEdit(track, input.now, window, input.now));
3621
3734
  return null;
3622
3735
  }
3736
+ const freshnessFor = (names) => input.transcriptPath ? agentsRanFromTranscript(input.transcriptPath, names, window, input.now) : agentsFresh(track, names, window, input.now);
3623
3737
  const ctx = {
3624
3738
  sessionId: input.sessionId,
3625
3739
  framework: input.framework,
@@ -3628,9 +3742,9 @@ async function gate(input) {
3628
3742
  authorizations: track.authorizations,
3629
3743
  refs: input.refs,
3630
3744
  refsRead: track.refsRead,
3631
- agentsFresh: agentsFresh(track, [...REQUIRED_AGENTS], window, input.now),
3745
+ agentsFresh: freshnessFor([...REQUIRED_AGENTS]),
3632
3746
  brainstormRequired: track.brainstormRequired,
3633
- brainstormFresh: agentsFresh(track, ["brainstorming"], window, input.now)
3747
+ brainstormFresh: freshnessFor(["brainstorming"])
3634
3748
  };
3635
3749
  try {
3636
3750
  const apex = evaluateApex(ctx);
@@ -3693,7 +3807,8 @@ async function handlePre(ctx) {
3693
3807
  agentType: event.agentType,
3694
3808
  windowMs: opts.windowMs,
3695
3809
  now: opts.now,
3696
- trackFile: file
3810
+ trackFile: file,
3811
+ transcriptPath: typeof payload.transcript_path === "string" ? payload.transcript_path : void 0
3697
3812
  });
3698
3813
  return prompt ? {
3699
3814
  stdout: respond(id, prompt),
@@ -3718,7 +3833,7 @@ function rawEventName(payload) {
3718
3833
  async function handleHook(id, payload, opts) {
3719
3834
  const event = normalizeEvent(id, payload);
3720
3835
  const layout = projectLayout(opts.cwd);
3721
- const file = trackFile(event.sessionId, layout.trackDir);
3836
+ const file = trackFile(event.sessionId, defaultStateDir(opts.cwd));
3722
3837
  const mcpDir = layout.cacheDir;
3723
3838
  const framework = detectFramework(event.filePath ?? "", event.content ?? "");
3724
3839
  if (designLifecycle(payload, mcpDir, opts.cwd, String(opts.now), opts.now)) return {
@@ -3788,4 +3903,4 @@ async function handleHook(id, payload, opts) {
3788
3903
  });
3789
3904
  }
3790
3905
  //#endregion
3791
- export { detectSolidProfile as $, dispatchLessons as A, activityFor as At, mergeLines as B, securityStateDir as C, trackFile as Ct, dispatchLifecycle as D, mcpPostStore as Dt, trackEnrichment as E, isMcpTool as Et, writePluginMap as F, trackSessionChanges as G, getFileDesc as H, generateProjectMap as I, saveApexState as J, validateRulesLoaded as K, isProject as L, lessonsStateFileFor as M, cartoSessionStart as N, aipilotPostToolUse as O, mcpPreIntercept as Ot, generateEcosystemMap as P, subagentCacheContext as Q, writeTree as R, saveSecurityState as S, recordActivity as St, todayUtc as T, MCP_TTL_MS as Tt, listChildren as U, countFiles as V, postEditTypescript as W, validateTeammateOutput as X, logToolFailure as Y, trackAgentMemory as Z, trackWatchResearch as _, sessionStatePath as _t, TRIVIAL_BUDGET as a, pruneEmptyDirs as at, isoUtc as b, taskContext as bt, detectDuplication as c, trimLogFile as ct, lifecycleStdout as d, projectContext as dt, solidDetectStart as et, postEditContext as f, claudeHome as ft, postTrackingSideEffects as g, saveSessionState as gt, securityAdvisory as h, sanitizeSessionId as ht, REQUIRED_AGENTS as i, sessionStartCore as it, lessonsFileFor as j, dispatchAipilot as k, queryOf as kt, dryGate as l, devContext as lt, seoPostToolUseResponse as m, loadSessionState as mt, handlePre as n, readRules as nt, gate as o, purgeTtlTree as ot, seoPostToolUse as p, fusengineCache as pt, cleanupSession as q, DEFAULT_WINDOW_MS as r, runSessionStartCleanups as rt, preCommitGate as s, removeOldFiles as st, handleHook as t, injectRules as tt, extractSymbols as u, gitContext as ut, trackMcpResearch as v, sessionsDir as vt, securityStatePath as w, normalizeEvent as wt, loadSecurityState as x, respond as xt, trackSkillRead as y, promptSubmitContext as yt, loadEnriched as z };
3906
+ export { pruneEmptyDirs as $, generateProjectMap as A, saveSessionState as At, validateRulesLoaded as B, dispatchAipilot as C, securityStateDir as Ct, cartoSessionStart as D, fusengineCache as Dt, lessonsStateFileFor as E, claudeHome as Et, countFiles as F, trackAgentMemory as G, saveApexState as H, getFileDesc as I, solidDetectStart as J, subagentCacheContext as K, listChildren as L, writeTree as M, sessionsDir as Mt, loadEnriched as N, generateEcosystemMap as O, loadSessionState as Ot, mergeLines as P, sessionStartCore as Q, postEditTypescript as R, aipilotPostToolUse as S, saveSecurityState as St, lessonsFileFor as T, todayUtc as Tt, logToolFailure as U, cleanupSession as V, validateTeammateOutput as W, readRules as X, injectRules as Y, runSessionStartCleanups as Z, trackWatchResearch as _, mcpPreIntercept as _t, TRIVIAL_BUDGET as a, projectContext as at, trackEnrichment as b, isoUtc as bt, detectDuplication as c, respond as ct, lifecycleStdout as d, projectHash$1 as dt, purgeTtlTree as et, postEditContext as f, trackFile as ft, postTrackingSideEffects as g, mcpPostStore as gt, securityAdvisory as h, isMcpTool as ht, REQUIRED_AGENTS as i, gitContext as it, isProject as j, sessionStatePath as jt, writePluginMap as k, sanitizeSessionId as kt, dryGate as l, recordActivity as lt, seoPostToolUseResponse as m, MCP_TTL_MS as mt, handlePre as n, trimLogFile as nt, gate as o, promptSubmitContext as ot, seoPostToolUse as p, normalizeEvent as pt, detectSolidProfile as q, DEFAULT_WINDOW_MS as r, devContext as rt, preCommitGate as s, taskContext as st, handleHook as t, removeOldFiles as tt, extractSymbols as u, defaultStateDir as ut, trackMcpResearch as v, queryOf as vt, dispatchLessons as w, securityStatePath as wt, dispatchLifecycle as x, loadSecurityState as xt, trackSkillRead as y, activityFor as yt, trackSessionChanges as z };
@@ -170,9 +170,19 @@ declare const ASK_PATTERNS: RegExp[];
170
170
  declare function securityGuard(ctx: GuardContext): Prompt | null;
171
171
  //#endregion
172
172
  //#region src/policy/guards/protected-path.d.ts
173
- /** Path fragments that mark a location as internal/generated state (off-limits to Write/Edit). */
173
+ /** Path fragments that mark a location as internal/generated state. */
174
174
  declare const PROTECTED_FRAGMENTS: readonly string[];
175
- /** Blocks direct edits to internal/generated state directories. */
175
+ /**
176
+ * Blocks direct edits to internal/generated state directories.
177
+ *
178
+ * Covers:
179
+ * - Write / Edit tool calls whose `filePath` targets a protected fragment.
180
+ * - Bash commands that both reference a protected fragment *and* contain a
181
+ * recognisable shell write operation (best-effort; see `bashHasWriteOp`).
182
+ *
183
+ * @param ctx - The guard context (tool, filePath, command).
184
+ * @returns A blocking {@link Prompt}, or null to allow.
185
+ */
176
186
  declare function protectedPathGuard(ctx: GuardContext): Prompt | null;
177
187
  //#endregion
178
188
  //#region src/policy/guards/bash-write.d.ts
package/dist/index.d.mts CHANGED
@@ -5,7 +5,7 @@ import { a as detectHarness, i as HarnessVia, n as HarnessInfo, o as detectMode,
5
5
  import { a as isDocConsulted, i as formatDocSatisfactionStatus, n as DocSatisfactionStatus, o as resolveSessions, r as formatDocDeny, t as AuthEntry } from "./doc-helpers-CG1nuf-c.mjs";
6
6
  import { t as incrementTrivialEditCounter } from "./index-BOBXQ91y.mjs";
7
7
  import { a as compactJson, i as walkUpFor, n as projectRoot, r as projectRootOrNull, t as isCodeFile } from "./index-BEMumjOw.mjs";
8
- import { $ as APEX_GATES, A as GUARDS, B as TS_DECL_RE, C as SKILL_TRIGGERS, Ct as detectProjectType, D as capVerbosity, E as MAX_TOKENS, F as GO_DECL_RE, G as bashWriteGuard, H as ASK_WRITERS, I as JAVA_DECL_RE, J as ASK_PATTERNS, K as PROTECTED_FRAGMENTS, L as PHP_DECL_RE, M as registerGuard, N as runGuards, O as detectCreationIntent, P as installGuard, Q as GuardContext, R as PY_MODEL_RE, S as skillTriggerGate, St as detectModularArchitecture, T as MAX_EXA_RESULTS, Tt as requiredArchSkill, U as CODE_MUTATORS, V as interfaceSeparationGuard, W as CODE_REDIRECT, X as securityGuard, Y as CRITICAL_PATTERNS, Z as Guard, _ as DEV_VERBS, _t as evaluateFileSize, a as firstHeading, at as freshnessGate, b as detectClaudeMdProjectType, bt as ModularArchitecture, c as parseEntry, ct as PolicyResult, d as EXCLUDE_DIRS, dt as GIT_BLOCKED, et as ApexContext, f as PROJECT_INDICATORS, ft as PROJECT_INSTALL, g as loadApexTaskState, gt as countLines, h as buildApexTaskInjection, ht as FileSizeVerdict, i as firstComment, it as evaluateApex, j as clearUserGuards, k as FAIL_CLOSED, l as parseBodyDesc, lt as evaluate, m as buildApexTaskContext, mt as matchPatterns, n as missingSeoElements, nt as brainstormGate, o as TreeEntry, ot as solidReadGate, p as ApexTaskState, pt as SYSTEM_INSTALL, q as protectedPathGuard, r as descFromText, rt as docConsultedGate, s as parseEnrichment, st as PolicyContext, t as isHtmlLike, tt as ApexGate, u as parseField, ut as GIT_ASK, v as buildApexInstruction, vt as detectFramework, w as frameworkSolidGate, wt as isApexCommand, x as detectRequiredSkills, xt as ProjectType, y as buildClaudeMdContext, yt as DEV_KEYWORDS, z as SWIFT_PROTO_RE } from "./index-DmbOUJK8.mjs";
8
+ import { $ as APEX_GATES, A as GUARDS, B as TS_DECL_RE, C as SKILL_TRIGGERS, Ct as detectProjectType, D as capVerbosity, E as MAX_TOKENS, F as GO_DECL_RE, G as bashWriteGuard, H as ASK_WRITERS, I as JAVA_DECL_RE, J as ASK_PATTERNS, K as PROTECTED_FRAGMENTS, L as PHP_DECL_RE, M as registerGuard, N as runGuards, O as detectCreationIntent, P as installGuard, Q as GuardContext, R as PY_MODEL_RE, S as skillTriggerGate, St as detectModularArchitecture, T as MAX_EXA_RESULTS, Tt as requiredArchSkill, U as CODE_MUTATORS, V as interfaceSeparationGuard, W as CODE_REDIRECT, X as securityGuard, Y as CRITICAL_PATTERNS, Z as Guard, _ as DEV_VERBS, _t as evaluateFileSize, a as firstHeading, at as freshnessGate, b as detectClaudeMdProjectType, bt as ModularArchitecture, c as parseEntry, ct as PolicyResult, d as EXCLUDE_DIRS, dt as GIT_BLOCKED, et as ApexContext, f as PROJECT_INDICATORS, ft as PROJECT_INSTALL, g as loadApexTaskState, gt as countLines, h as buildApexTaskInjection, ht as FileSizeVerdict, i as firstComment, it as evaluateApex, j as clearUserGuards, k as FAIL_CLOSED, l as parseBodyDesc, lt as evaluate, m as buildApexTaskContext, mt as matchPatterns, n as missingSeoElements, nt as brainstormGate, o as TreeEntry, ot as solidReadGate, p as ApexTaskState, pt as SYSTEM_INSTALL, q as protectedPathGuard, r as descFromText, rt as docConsultedGate, s as parseEnrichment, st as PolicyContext, t as isHtmlLike, tt as ApexGate, u as parseField, ut as GIT_ASK, v as buildApexInstruction, vt as detectFramework, w as frameworkSolidGate, wt as isApexCommand, x as detectRequiredSkills, xt as ProjectType, y as buildClaudeMdContext, yt as DEV_KEYWORDS, z as SWIFT_PROTO_RE } from "./index-QzK2dv0V.mjs";
9
9
  import { n as RouteResult, r as ScoredRef, t as RefMeta } from "./types-CY5qT2X1.mjs";
10
10
  import { a as ReminderState, c as readState, d as throttleMs, i as registryFile, l as setStateField, n as addRoot, o as lessonsFileFor, r as readRoots, s as nowStamp, t as ensureMemoryGitignore, u as stateFileFor } from "./index-DLYhervv.mjs";
11
11
  import { a as globToRe, i as scoreReferences, n as toRefMeta, o as parseFrontmatter, r as routeReferences, t as loadRefs } from "./index-DL8MxjuP.mjs";
package/dist/index.mjs CHANGED
@@ -4,16 +4,16 @@ import { n as STATE_ROOT, r as projectLayout, t as STATE_GITIGNORE } from "./lay
4
4
  import { t as compactJson } from "./compact-json-DK2nX-MK.mjs";
5
5
  import { i as walkUpFor, n as projectRoot, r as projectRootOrNull, t as isCodeFile } from "./project-root-3kk7gCOp.mjs";
6
6
  import { n as detectMode, r as modeFor, t as detectHarness } from "./harness-C8Nxxyn_.mjs";
7
- import { A as evaluateApex, C as MAX_EXA_RESULTS, D as APEX_GATES, E as detectCreationIntent, F as detectProjectType, I as isApexCommand, L as requiredArchSkill, M as solidReadGate, N as DEV_KEYWORDS, O as brainstormGate, P as detectModularArchitecture, S as frameworkSolidGate, T as capVerbosity, _ as buildClaudeMdContext, a as firstHeading, b as skillTriggerGate, c as parseBodyDesc, d as PROJECT_INDICATORS, f as buildApexTaskContext, g as buildApexInstruction, h as DEV_VERBS, i as firstComment, j as freshnessGate, k as docConsultedGate, l as parseField, m as loadApexTaskState, n as missingSeoElements, o as parseEnrichment, p as buildApexTaskInjection, r as descFromText, s as parseEntry, t as isHtmlLike, u as EXCLUDE_DIRS, v as detectClaudeMdProjectType, w as MAX_TOKENS, x as SKILL_TRIGGERS, y as detectRequiredSkills } from "./validate-CccewDwk.mjs";
8
- import { A as evaluateFileSize, C as securityGuard, D as SYSTEM_INSTALL, E as PROJECT_INSTALL, O as matchPatterns, S as CRITICAL_PATTERNS, T as GIT_BLOCKED, _ as CODE_REDIRECT, a as registerGuard, b as protectedPathGuard, c as GO_DECL_RE, d as PY_MODEL_RE, f as SWIFT_PROTO_RE, g as CODE_MUTATORS, h as ASK_WRITERS, i as clearUserGuards, j as detectFramework, k as countLines, l as JAVA_DECL_RE, m as interfaceSeparationGuard, n as FAIL_CLOSED, o as runGuards, p as TS_DECL_RE, r as GUARDS, s as installGuard, t as evaluate, u as PHP_DECL_RE, v as bashWriteGuard, w as GIT_ASK, x as ASK_PATTERNS, y as PROTECTED_FRAGMENTS } from "./evaluate-9ch1K2kt.mjs";
7
+ import { A as evaluateApex, C as MAX_EXA_RESULTS, D as APEX_GATES, E as detectCreationIntent, F as detectProjectType, I as isApexCommand, L as requiredArchSkill, M as solidReadGate, N as DEV_KEYWORDS, O as brainstormGate, P as detectModularArchitecture, S as frameworkSolidGate, T as capVerbosity, _ as buildClaudeMdContext, a as firstHeading, b as skillTriggerGate, c as parseBodyDesc, d as PROJECT_INDICATORS, f as buildApexTaskContext, g as buildApexInstruction, h as DEV_VERBS, i as firstComment, j as freshnessGate, k as docConsultedGate, l as parseField, m as loadApexTaskState, n as missingSeoElements, o as parseEnrichment, p as buildApexTaskInjection, r as descFromText, s as parseEntry, t as isHtmlLike, u as EXCLUDE_DIRS, v as detectClaudeMdProjectType, w as MAX_TOKENS, x as SKILL_TRIGGERS, y as detectRequiredSkills } from "./validate-DLrWtaDR.mjs";
8
+ import { A as evaluateFileSize, C as securityGuard, D as SYSTEM_INSTALL, E as PROJECT_INSTALL, O as matchPatterns, S as CRITICAL_PATTERNS, T as GIT_BLOCKED, _ as CODE_REDIRECT, a as registerGuard, b as protectedPathGuard, c as GO_DECL_RE, d as PY_MODEL_RE, f as SWIFT_PROTO_RE, g as CODE_MUTATORS, h as ASK_WRITERS, i as clearUserGuards, j as detectFramework, k as countLines, l as JAVA_DECL_RE, m as interfaceSeparationGuard, n as FAIL_CLOSED, o as runGuards, p as TS_DECL_RE, r as GUARDS, s as installGuard, t as evaluate, u as PHP_DECL_RE, v as bashWriteGuard, w as GIT_ASK, x as ASK_PATTERNS, y as PROTECTED_FRAGMENTS } from "./evaluate-zyxeVZPB.mjs";
9
9
  import { i as resolveSessions, n as formatDocSatisfactionStatus, r as isDocConsulted, t as formatDocDeny } from "./doc-helpers-Dd_x1-tZ.mjs";
10
10
  import { i as parseFrontmatter, n as scoreReferences, r as globToRe, t as routeReferences } from "./router-D8cVrI-s.mjs";
11
11
  import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
12
12
  import { a as nowStamp, c as stateFileFor, i as lessonsFileFor, l as throttleMs, n as readRoots, o as readState, r as registryFile, s as setStateField, t as addRoot, u as ensureMemoryGitignore } from "./registry-BkoEbdec.mjs";
13
13
  import { n as jaccardSimilar, r as queryHash, t as compactMarkdown } from "./cache-C9z9LclL.mjs";
14
14
  import { a as extractText, i as mcpCacheKey, n as cachePath, o as loadIndex, r as cacheStore, s as summarizeIndex, t as cacheLookup } from "./store-PrNPm6So.mjs";
15
- import { t as incrementTrivialEditCounter } from "./freshness-otdUpuvP.mjs";
15
+ import { t as incrementTrivialEditCounter } from "./freshness-43gxYpiX.mjs";
16
16
  import { n as toRefMeta, t as loadRefs } from "./loader-CyAoJv2W.mjs";
17
- 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";
17
+ 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";
18
18
  import { a as formatPath, c as colors, d as GRADIENT_BLOCKS, f as PROGRESS_BAR_DEFAULTS, i as formatCost, l as progressiveColor, m as TIME_INTERVALS, n as generateProgressBar, o as formatTimeLeft, p as PROGRESS_CHARS, r as formatBasename, s as formatTokens, t as generateGradientBar, u as COLOR_THRESHOLDS } from "./statusline-D87eUNXl.mjs";
19
19
  export { APEX_GATES, ASK_PATTERNS, ASK_WRITERS, CODE_MUTATORS, CODE_REDIRECT, COLOR_THRESHOLDS, CRITICAL_PATTERNS, DEFAULT_MAX_LINES, DEFAULT_TTL_SEC, DEV_KEYWORDS, DEV_VERBS, EXCLUDE_DIRS, FAIL_CLOSED, GIT_ASK, GIT_BLOCKED, GO_DECL_RE, GRADIENT_BLOCKS, GUARDS, JAVA_DECL_RE, MAX_EXA_RESULTS, MAX_LINES_ENV_KEY, MAX_TOKENS, PHP_DECL_RE, PROGRESS_BAR_DEFAULTS, PROGRESS_CHARS, PROJECT_INDICATORS, PROJECT_INSTALL, PROTECTED_FRAGMENTS, PY_MODEL_RE, SKILL_TRIGGERS, STATE_GITIGNORE, STATE_ROOT, SWIFT_PROTO_RE, SYSTEM_INSTALL, TIME_INTERVALS, TS_DECL_RE, TTL_ENV_KEY, acquireLock, addRoot, apexStateDir, bashWriteGuard, brainstormGate, buildApexInstruction, buildApexTaskContext, buildApexTaskInjection, buildClaudeMdContext, cacheLookup, cachePath, cacheStore, capVerbosity, clearUserGuards, colors, compactJson, compactMarkdown, countLines, descFromText, detectClaudeMdProjectType, detectCreationIntent, detectFramework, detectHarness, detectMode, detectModularArchitecture, detectProjectType, detectRequiredSkills, docConsultedGate, ensureMemoryGitignore, ensureStateDir, evaluate, evaluateApex, evaluateFileSize, extractText, firstComment, firstHeading, formatBasename, formatCost, formatDocDeny, formatDocSatisfactionStatus, formatPath, formatPrompt, formatTimeLeft, formatTokens, frameworkSolidGate, freshnessGate, generateGradientBar, generateProgressBar, globToRe, incrementTrivialEditCounter, installGuard, interfaceSeparationGuard, isApexCommand, isCodeFile, isDocConsulted, isHtmlLike, jaccardSimilar, lessonsFileFor, loadApexTaskState, loadIndex, loadRefs, loadState, matchPatterns, mcpCacheKey, missingSeoElements, modeFor, nowStamp, parseBodyDesc, parseEnrichment, parseEntry, parseEnvInt, parseField, parseFrontmatter, progressiveColor, projectLayout, projectRoot, projectRootOrNull, protectedPathGuard, queryHash, readRoots, readState, registerGuard, registryFile, requiredArchSkill, resolveMaxLines, resolveSessions, resolveTtlSec, routeReferences, runGuards, saveState, scoreReferences, securityGuard, setStateField, skillTriggerGate, solidReadGate, splitTarget, stateFileFor, stateFilePath, summarizeIndex, taskComplete, taskCreate, taskStart, throttleMs, toRefMeta, ttlLabel, walkUpFor };
@@ -1,7 +1,7 @@
1
1
  import { t as compactJson } from "./compact-json-DK2nX-MK.mjs";
2
2
  import { dirname } from "node:path";
3
3
  import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
4
- import { randomUUID } from "node:crypto";
4
+ import { createHash, randomUUID } from "node:crypto";
5
5
  import { mkdir } from "node:fs/promises";
6
6
  //#region src/util/json-io.ts
7
7
  /** Atomically write `data` to `path` (temp + rename, 0o600). Cross-FS safe on macOS/Linux. */
@@ -36,5 +36,9 @@ async function readJsonFile(path) {
36
36
  async function writeJsonFile(path, data, compact = false) {
37
37
  atomicWrite(path, compact ? compactJson(data) : JSON.stringify(data, null, 2));
38
38
  }
39
+ /** 8-char MD5 of text (cache key; non-cryptographic). Portable Node+Bun. */
40
+ function hashText(text) {
41
+ return createHash("md5").update(text).digest("hex").slice(0, 8);
42
+ }
39
43
  //#endregion
40
- export { writeJsonFile as i, ensureDir as n, readJsonFile as r, atomicWrite as t };
44
+ export { writeJsonFile as a, readJsonFile as i, ensureDir as n, hashText as r, atomicWrite as t };
@@ -1,2 +1,2 @@
1
- import { $ as APEX_GATES, A as GUARDS, B as TS_DECL_RE, C as SKILL_TRIGGERS, Ct as detectProjectType, D as capVerbosity, E as MAX_TOKENS, F as GO_DECL_RE, G as bashWriteGuard, H as ASK_WRITERS, I as JAVA_DECL_RE, J as ASK_PATTERNS, K as PROTECTED_FRAGMENTS, L as PHP_DECL_RE, M as registerGuard, N as runGuards, O as detectCreationIntent, P as installGuard, Q as GuardContext, R as PY_MODEL_RE, S as skillTriggerGate, St as detectModularArchitecture, T as MAX_EXA_RESULTS, Tt as requiredArchSkill, U as CODE_MUTATORS, V as interfaceSeparationGuard, W as CODE_REDIRECT, X as securityGuard, Y as CRITICAL_PATTERNS, Z as Guard, _ as DEV_VERBS, _t as evaluateFileSize, a as firstHeading, at as freshnessGate, b as detectClaudeMdProjectType, bt as ModularArchitecture, c as parseEntry, ct as PolicyResult, d as EXCLUDE_DIRS, dt as GIT_BLOCKED, et as ApexContext, f as PROJECT_INDICATORS, ft as PROJECT_INSTALL, g as loadApexTaskState, gt as countLines, h as buildApexTaskInjection, ht as FileSizeVerdict, i as firstComment, it as evaluateApex, j as clearUserGuards, k as FAIL_CLOSED, l as parseBodyDesc, lt as evaluate, m as buildApexTaskContext, mt as matchPatterns, n as missingSeoElements, nt as brainstormGate, o as TreeEntry, ot as solidReadGate, p as ApexTaskState, pt as SYSTEM_INSTALL, q as protectedPathGuard, r as descFromText, rt as docConsultedGate, s as parseEnrichment, st as PolicyContext, t as isHtmlLike, tt as ApexGate, u as parseField, ut as GIT_ASK, v as buildApexInstruction, vt as detectFramework, w as frameworkSolidGate, wt as isApexCommand, x as detectRequiredSkills, xt as ProjectType, y as buildClaudeMdContext, yt as DEV_KEYWORDS, z as SWIFT_PROTO_RE } from "../index-DmbOUJK8.mjs";
1
+ import { $ as APEX_GATES, A as GUARDS, B as TS_DECL_RE, C as SKILL_TRIGGERS, Ct as detectProjectType, D as capVerbosity, E as MAX_TOKENS, F as GO_DECL_RE, G as bashWriteGuard, H as ASK_WRITERS, I as JAVA_DECL_RE, J as ASK_PATTERNS, K as PROTECTED_FRAGMENTS, L as PHP_DECL_RE, M as registerGuard, N as runGuards, O as detectCreationIntent, P as installGuard, Q as GuardContext, R as PY_MODEL_RE, S as skillTriggerGate, St as detectModularArchitecture, T as MAX_EXA_RESULTS, Tt as requiredArchSkill, U as CODE_MUTATORS, V as interfaceSeparationGuard, W as CODE_REDIRECT, X as securityGuard, Y as CRITICAL_PATTERNS, Z as Guard, _ as DEV_VERBS, _t as evaluateFileSize, a as firstHeading, at as freshnessGate, b as detectClaudeMdProjectType, bt as ModularArchitecture, c as parseEntry, ct as PolicyResult, d as EXCLUDE_DIRS, dt as GIT_BLOCKED, et as ApexContext, f as PROJECT_INDICATORS, ft as PROJECT_INSTALL, g as loadApexTaskState, gt as countLines, h as buildApexTaskInjection, ht as FileSizeVerdict, i as firstComment, it as evaluateApex, j as clearUserGuards, k as FAIL_CLOSED, l as parseBodyDesc, lt as evaluate, m as buildApexTaskContext, mt as matchPatterns, n as missingSeoElements, nt as brainstormGate, o as TreeEntry, ot as solidReadGate, p as ApexTaskState, pt as SYSTEM_INSTALL, q as protectedPathGuard, r as descFromText, rt as docConsultedGate, s as parseEnrichment, st as PolicyContext, t as isHtmlLike, tt as ApexGate, u as parseField, ut as GIT_ASK, v as buildApexInstruction, vt as detectFramework, w as frameworkSolidGate, wt as isApexCommand, x as detectRequiredSkills, xt as ProjectType, y as buildClaudeMdContext, yt as DEV_KEYWORDS, z as SWIFT_PROTO_RE } from "../index-QzK2dv0V.mjs";
2
2
  export { APEX_GATES, ASK_PATTERNS, ASK_WRITERS, ApexContext, ApexGate, ApexTaskState, CODE_MUTATORS, CODE_REDIRECT, CRITICAL_PATTERNS, DEV_KEYWORDS, DEV_VERBS, EXCLUDE_DIRS, FAIL_CLOSED, FileSizeVerdict, GIT_ASK, GIT_BLOCKED, GO_DECL_RE, GUARDS, Guard, GuardContext, JAVA_DECL_RE, MAX_EXA_RESULTS, MAX_TOKENS, ModularArchitecture, PHP_DECL_RE, PROJECT_INDICATORS, PROJECT_INSTALL, PROTECTED_FRAGMENTS, PY_MODEL_RE, PolicyContext, PolicyResult, ProjectType, SKILL_TRIGGERS, SWIFT_PROTO_RE, SYSTEM_INSTALL, TS_DECL_RE, TreeEntry, bashWriteGuard, brainstormGate, buildApexInstruction, buildApexTaskContext, buildApexTaskInjection, buildClaudeMdContext, capVerbosity, clearUserGuards, countLines, descFromText, detectClaudeMdProjectType, detectCreationIntent, detectFramework, detectModularArchitecture, detectProjectType, detectRequiredSkills, docConsultedGate, evaluate, evaluateApex, evaluateFileSize, firstComment, firstHeading, frameworkSolidGate, freshnessGate, installGuard, interfaceSeparationGuard, isApexCommand, isHtmlLike, loadApexTaskState, matchPatterns, missingSeoElements, parseBodyDesc, parseEnrichment, parseEntry, parseField, protectedPathGuard, registerGuard, requiredArchSkill, runGuards, securityGuard, skillTriggerGate, solidReadGate };
@@ -1,4 +1,4 @@
1
- import { A as evaluateApex, C as MAX_EXA_RESULTS, D as APEX_GATES, E as detectCreationIntent, F as detectProjectType, I as isApexCommand, L as requiredArchSkill, M as solidReadGate, N as DEV_KEYWORDS, O as brainstormGate, P as detectModularArchitecture, S as frameworkSolidGate, T as capVerbosity, _ as buildClaudeMdContext, a as firstHeading, b as skillTriggerGate, c as parseBodyDesc, d as PROJECT_INDICATORS, f as buildApexTaskContext, g as buildApexInstruction, h as DEV_VERBS, i as firstComment, j as freshnessGate, k as docConsultedGate, l as parseField, m as loadApexTaskState, n as missingSeoElements, o as parseEnrichment, p as buildApexTaskInjection, r as descFromText, s as parseEntry, t as isHtmlLike, u as EXCLUDE_DIRS, v as detectClaudeMdProjectType, w as MAX_TOKENS, x as SKILL_TRIGGERS, y as detectRequiredSkills } from "../validate-CccewDwk.mjs";
2
- import { A as evaluateFileSize, C as securityGuard, D as SYSTEM_INSTALL, E as PROJECT_INSTALL, O as matchPatterns, S as CRITICAL_PATTERNS, T as GIT_BLOCKED, _ as CODE_REDIRECT, a as registerGuard, b as protectedPathGuard, c as GO_DECL_RE, d as PY_MODEL_RE, f as SWIFT_PROTO_RE, g as CODE_MUTATORS, h as ASK_WRITERS, i as clearUserGuards, j as detectFramework, k as countLines, l as JAVA_DECL_RE, m as interfaceSeparationGuard, n as FAIL_CLOSED, o as runGuards, p as TS_DECL_RE, r as GUARDS, s as installGuard, t as evaluate, u as PHP_DECL_RE, v as bashWriteGuard, w as GIT_ASK, x as ASK_PATTERNS, y as PROTECTED_FRAGMENTS } from "../evaluate-9ch1K2kt.mjs";
1
+ import { A as evaluateApex, C as MAX_EXA_RESULTS, D as APEX_GATES, E as detectCreationIntent, F as detectProjectType, I as isApexCommand, L as requiredArchSkill, M as solidReadGate, N as DEV_KEYWORDS, O as brainstormGate, P as detectModularArchitecture, S as frameworkSolidGate, T as capVerbosity, _ as buildClaudeMdContext, a as firstHeading, b as skillTriggerGate, c as parseBodyDesc, d as PROJECT_INDICATORS, f as buildApexTaskContext, g as buildApexInstruction, h as DEV_VERBS, i as firstComment, j as freshnessGate, k as docConsultedGate, l as parseField, m as loadApexTaskState, n as missingSeoElements, o as parseEnrichment, p as buildApexTaskInjection, r as descFromText, s as parseEntry, t as isHtmlLike, u as EXCLUDE_DIRS, v as detectClaudeMdProjectType, w as MAX_TOKENS, x as SKILL_TRIGGERS, y as detectRequiredSkills } from "../validate-DLrWtaDR.mjs";
2
+ import { A as evaluateFileSize, C as securityGuard, D as SYSTEM_INSTALL, E as PROJECT_INSTALL, O as matchPatterns, S as CRITICAL_PATTERNS, T as GIT_BLOCKED, _ as CODE_REDIRECT, a as registerGuard, b as protectedPathGuard, c as GO_DECL_RE, d as PY_MODEL_RE, f as SWIFT_PROTO_RE, g as CODE_MUTATORS, h as ASK_WRITERS, i as clearUserGuards, j as detectFramework, k as countLines, l as JAVA_DECL_RE, m as interfaceSeparationGuard, n as FAIL_CLOSED, o as runGuards, p as TS_DECL_RE, r as GUARDS, s as installGuard, t as evaluate, u as PHP_DECL_RE, v as bashWriteGuard, w as GIT_ASK, x as ASK_PATTERNS, y as PROTECTED_FRAGMENTS } from "../evaluate-zyxeVZPB.mjs";
3
3
  import "../policy-la_KkjCS.mjs";
4
4
  export { APEX_GATES, ASK_PATTERNS, ASK_WRITERS, CODE_MUTATORS, CODE_REDIRECT, CRITICAL_PATTERNS, DEV_KEYWORDS, DEV_VERBS, EXCLUDE_DIRS, FAIL_CLOSED, GIT_ASK, GIT_BLOCKED, GO_DECL_RE, GUARDS, JAVA_DECL_RE, MAX_EXA_RESULTS, MAX_TOKENS, PHP_DECL_RE, PROJECT_INDICATORS, PROJECT_INSTALL, PROTECTED_FRAGMENTS, PY_MODEL_RE, SKILL_TRIGGERS, SWIFT_PROTO_RE, SYSTEM_INSTALL, TS_DECL_RE, bashWriteGuard, brainstormGate, buildApexInstruction, buildApexTaskContext, buildApexTaskInjection, buildClaudeMdContext, capVerbosity, clearUserGuards, countLines, descFromText, detectClaudeMdProjectType, detectCreationIntent, detectFramework, detectModularArchitecture, detectProjectType, detectRequiredSkills, docConsultedGate, evaluate, evaluateApex, evaluateFileSize, firstComment, firstHeading, frameworkSolidGate, freshnessGate, installGuard, interfaceSeparationGuard, isApexCommand, isHtmlLike, loadApexTaskState, matchPatterns, missingSeoElements, parseBodyDesc, parseEnrichment, parseEntry, parseField, protectedPathGuard, registerGuard, requiredArchSkill, runGuards, securityGuard, skillTriggerGate, solidReadGate };
@@ -1,5 +1,5 @@
1
1
  import { t as isCodeFile } from "./project-root-3kk7gCOp.mjs";
2
- import { t as evaluate } from "./evaluate-9ch1K2kt.mjs";
2
+ import { t as evaluate } from "./evaluate-zyxeVZPB.mjs";
3
3
  import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
4
4
  import { execSync } from "node:child_process";
5
5
  //#region src/cli/run.ts
@@ -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-Cxgzd4pZ.mjs";
2
+ import { $ as pruneEmptyDirs, A as generateProjectMap, At as saveSessionState, B as validateRulesLoaded, C as dispatchAipilot, Ct as securityStateDir, D as cartoSessionStart, Dt as fusengineCache, E as lessonsStateFileFor, Et as claudeHome, F as countFiles, G as trackAgentMemory, H as saveApexState, I as getFileDesc, J as solidDetectStart, K as subagentCacheContext, L as listChildren, M as writeTree, Mt as sessionsDir, N as loadEnriched, O as generateEcosystemMap, Ot as loadSessionState, P as mergeLines, Q as sessionStartCore, R as postEditTypescript, S as aipilotPostToolUse, St as saveSecurityState, T as lessonsFileFor, Tt as todayUtc, U as logToolFailure, V as cleanupSession, W as validateTeammateOutput, X as readRules, Y as injectRules, Z as runSessionStartCleanups, _ as trackWatchResearch, _t as mcpPreIntercept, a as TRIVIAL_BUDGET, at as projectContext, b as trackEnrichment, bt as isoUtc, c as detectDuplication, ct as respond, d as lifecycleStdout, dt as projectHash, et as purgeTtlTree, f as postEditContext, ft as trackFile, g as postTrackingSideEffects, gt as mcpPostStore, h as securityAdvisory, ht as isMcpTool, i as REQUIRED_AGENTS, it as gitContext, j as isProject, jt as sessionStatePath, k as writePluginMap, kt as sanitizeSessionId, l as dryGate, lt as recordActivity, m as seoPostToolUseResponse, mt as MCP_TTL_MS, n as handlePre, nt as trimLogFile, o as gate, ot as promptSubmitContext, p as seoPostToolUse, pt as normalizeEvent, q as detectSolidProfile, r as DEFAULT_WINDOW_MS, rt as devContext, s as preCommitGate, st as taskContext, t as handleHook, tt as removeOldFiles, u as extractSymbols, ut as defaultStateDir, v as trackMcpResearch, vt as queryOf, w as dispatchLessons, wt as securityStatePath, x as dispatchLifecycle, xt as loadSecurityState, y as trackSkillRead, yt as activityFor, z as trackSessionChanges } from "../handle-CYlA6TeF.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.33",
3
+ "version": "0.1.35",
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,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 };