@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.
- package/dist/adapters/claude/index.mjs +1 -1
- package/dist/adapters/cline/index.mjs +1 -1
- package/dist/adapters/codex/index.mjs +1 -1
- package/dist/adapters/cursor/index.mjs +1 -1
- package/dist/adapters/gemini/index.mjs +1 -1
- package/dist/{claude-BatVYnAf.mjs → claude-3PqBGt_7.mjs} +1 -1
- package/dist/cli/bin.mjs +85 -3
- package/dist/cli/index.mjs +1 -1
- package/dist/{evaluate-9ch1K2kt.mjs → evaluate-zyxeVZPB.mjs} +44 -10
- package/dist/freshness/index.mjs +1 -1
- package/dist/{freshness-otdUpuvP.mjs → freshness-43gxYpiX.mjs} +1 -1
- package/dist/{handle-Cxgzd4pZ.mjs → handle-CYlA6TeF.mjs} +227 -112
- package/dist/{index-DmbOUJK8.d.mts → index-QzK2dv0V.d.mts} +12 -2
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +4 -4
- package/dist/{json-io-CAn72gI4.mjs → json-io-CvSumjtz.mjs} +6 -2
- package/dist/policy/index.d.mts +1 -1
- package/dist/policy/index.mjs +2 -2
- package/dist/{run-CQbtlAKa.mjs → run-CUL70W0k.mjs} +1 -1
- package/dist/runtime/index.d.mts +29 -2
- package/dist/runtime/index.mjs +2 -2
- package/dist/state/index.mjs +1 -1
- package/dist/{state-ByhLeKyD.mjs → state-BthKK4Jj.mjs} +1 -1
- package/dist/store-7j02oGjt.mjs +184 -0
- package/dist/tracking/index.d.mts +12 -2
- package/dist/tracking/index.mjs +1 -1
- package/dist/{validate-CccewDwk.mjs → validate-DLrWtaDR.mjs} +1 -1
- package/package.json +1 -1
- package/dist/store-D-ge2ZPI.mjs +0 -89
|
@@ -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-
|
|
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,2 +1,2 @@
|
|
|
1
|
-
import { a as readClaudeInput, i as guard, n as denyResponse, t as contextResponse } from "../../claude-
|
|
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-
|
|
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-
|
|
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-
|
|
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);
|
package/dist/cli/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
}
|
package/dist/freshness/index.mjs
CHANGED
|
@@ -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-
|
|
2
|
+
import { t as incrementTrivialEditCounter } from "../freshness-43gxYpiX.mjs";
|
|
3
3
|
export { formatDocDeny, formatDocSatisfactionStatus, incrementTrivialEditCounter, isDocConsulted, resolveSessions };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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-
|
|
5
|
-
import { j as detectFramework, k as countLines, n as FAIL_CLOSED, t as evaluate } from "./evaluate-
|
|
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 {
|
|
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-
|
|
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-
|
|
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
|
|
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
|
-
/**
|
|
173
|
-
|
|
174
|
-
|
|
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:
|
|
3745
|
+
agentsFresh: freshnessFor([...REQUIRED_AGENTS]),
|
|
3632
3746
|
brainstormRequired: track.brainstormRequired,
|
|
3633
|
-
brainstormFresh:
|
|
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,
|
|
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 {
|
|
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
|
|
173
|
+
/** Path fragments that mark a location as internal/generated state. */
|
|
174
174
|
declare const PROTECTED_FRAGMENTS: readonly string[];
|
|
175
|
-
/**
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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,
|
|
44
|
+
export { writeJsonFile as a, readJsonFile as i, ensureDir as n, hashText as r, atomicWrite as t };
|
package/dist/policy/index.d.mts
CHANGED
|
@@ -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-
|
|
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 };
|
package/dist/policy/index.mjs
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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
|
package/dist/runtime/index.d.mts
CHANGED
|
@@ -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
|
-
/**
|
|
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 };
|
package/dist/runtime/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { r as projectLayout } from "../layout-C0jaaCQC.mjs";
|
|
2
|
-
import { $ as
|
|
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 };
|
package/dist/state/index.mjs
CHANGED
|
@@ -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-
|
|
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 {
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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 };
|
package/dist/tracking/index.mjs
CHANGED
|
@@ -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-
|
|
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-
|
|
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.
|
|
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",
|
package/dist/store-D-ge2ZPI.mjs
DELETED
|
@@ -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 };
|