@fusengine/harness 0.1.91 → 0.1.93
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/README.md +17 -0
- package/dist/adapters/claude/index.mjs +1 -1
- package/dist/adapters/codex/index.mjs +1 -1
- package/dist/adapters/cursor/index.mjs +1 -1
- package/dist/adapters/hermes/index.mjs +1 -1
- package/dist/adapters/kimi/index.mjs +1 -1
- package/dist/apex-target-Xc2M32Pl.mjs +48 -0
- package/dist/apex-task-store-Cb65rE5i.mjs +467 -0
- package/dist/{claude-Ckv2_TgP.mjs → claude-D62hkUfS.mjs} +2 -84
- package/dist/cli/bin.mjs +7 -5
- package/dist/cli/index.d.mts +69 -1
- package/dist/cli/index.mjs +2 -2
- package/dist/config/index.mjs +2 -1
- package/dist/{dotenv-BLBkBTww.mjs → dotenv-C1LkcfW-.mjs} +1 -26
- package/dist/{handle-C43gA-Pr.mjs → handle-B-g9KL_9.mjs} +4837 -3466
- package/dist/{hermes-B9-p_3IF.mjs → hermes-ByopGx6C.mjs} +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +3 -2
- package/dist/{kimi-G2wcSh5-.mjs → kimi-C-Oia9q-.mjs} +1 -1
- package/dist/{normalize-BjG6unTj.mjs → normalize-Dy8g9Ybl.mjs} +83 -3
- package/dist/policy/index.mjs +1 -1
- package/dist/prd-PvK7PKWS.mjs +494 -0
- package/dist/runtime/index.d.mts +13 -1
- package/dist/runtime/index.mjs +1 -1
- package/dist/runtime-io-DuumUeE6.mjs +84 -0
- package/dist/{session-state-D5gLr66m.d.mts → session-state-COg7Ej_2.d.mts} +32 -1
- package/dist/{skill-path-DVML3zfp.mjs → skill-path-Cz8WFaGu.mjs} +1 -1
- package/dist/{store-5-ZPKb0u.mjs → store-BVY6gIYM.mjs} +75 -3
- package/dist/tracking/index.d.mts +2 -2
- package/dist/tracking/index.mjs +2 -2
- package/dist/ttl-Dgwg_QAv.mjs +26 -0
- package/dist/{validate-KjZ1X9tH.mjs → validate-Dcjl0LUS.mjs} +4 -49
- package/package.json +1 -1
- package/src/adapters/cursor/context-budget.ts +144 -0
- package/src/adapters/cursor/context-limit.ts +115 -0
- package/src/adapters/cursor/context.ts +21 -2
- package/src/adapters/cursor/interfaces/context-budget.ts +25 -0
- package/src/adapters/cursor/native-response.ts +10 -129
- package/src/adapters/cursor/native-schemas.ts +161 -0
- package/src/adapters/cursor/normalize.ts +65 -0
- package/src/adapters/cursor/plugin-root.ts +103 -0
- package/src/adapters/cursor/respond.ts +94 -47
- package/src/cli/bin.ts +3 -0
- package/src/cli/index.ts +1 -0
- package/src/cli/prd/compact.ts +53 -0
- package/src/cli/prd/format.ts +23 -0
- package/src/cli/prd/index.ts +23 -0
- package/src/cli/prd/resolve.ts +88 -0
- package/src/cli/prd/shared.ts +80 -0
- package/src/cli/prd/status.ts +87 -0
- package/src/cli/prd/validate.ts +90 -0
- package/src/policy/prd/index.ts +36 -0
- package/src/policy/prd/interfaces/types.ts +103 -0
- package/src/policy/prd/prd-compact.ts +28 -0
- package/src/policy/prd/prd-context.ts +131 -0
- package/src/policy/prd/prd-crosscheck.ts +76 -0
- package/src/policy/prd/prd-enabled.ts +39 -0
- package/src/policy/prd/prd-io.ts +89 -0
- package/src/policy/prd/prd-ownership.ts +99 -0
- package/src/policy/prd/prd-paths.ts +88 -0
- package/src/policy/prd/prd-schema.ts +151 -0
- package/src/runtime/handle-post.ts +2 -0
- package/src/runtime/handle-pre.ts +11 -0
- package/src/runtime/handle.ts +87 -6
- package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +7 -1
- package/src/runtime/lifecycle/dispatch.ts +18 -5
- package/src/runtime/lifecycle/failure-lesson.ts +6 -2
- package/src/runtime/lifecycle/rules-root.ts +18 -2
- package/src/runtime/normalize.ts +4 -0
- package/src/runtime/prd/index.ts +8 -0
- package/src/runtime/prd/prd-bash-targets.ts +199 -0
- package/src/runtime/prd/prd-candidate-files.ts +23 -0
- package/src/runtime/prd/prd-canon.ts +50 -0
- package/src/runtime/prd/prd-identity.ts +32 -0
- package/src/runtime/prd/prd-post-check.ts +73 -0
- package/src/runtime/prd/prd-pre-gate.ts +172 -0
- package/src/runtime/prd/prd-stop-gate.ts +101 -0
- package/src/runtime/prd/prd-subagent-context.ts +69 -0
- package/src/runtime/prd/prd-subagent-stop.ts +152 -0
- package/src/tracking/session-state.ts +46 -0
- package/src/tracking/track-diff.ts +3 -0
- package/src/tracking/track-journal.ts +10 -1
- package/dist/run-DkrzC0gb.mjs +0 -42
package/dist/runtime/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as Prompt } from "../types-Bh0jEF3_.mjs";
|
|
2
2
|
import { t as RefMeta } from "../types-CY5qT2X1.mjs";
|
|
3
|
-
import { t as AgentQuality } from "../session-state-
|
|
3
|
+
import { t as AgentQuality } from "../session-state-COg7Ej_2.mjs";
|
|
4
4
|
//#region src/runtime/paths.d.ts
|
|
5
5
|
/**
|
|
6
6
|
* Compute a stable 8-char hex hash for a project directory path.
|
|
@@ -187,6 +187,8 @@ interface NormalizedEvent {
|
|
|
187
187
|
permissionMode?: string;
|
|
188
188
|
/** Codex logical tool-use identity, shared by sibling hook callbacks. */
|
|
189
189
|
toolUseId?: string;
|
|
190
|
+
/** Sub-agent identifier, if the tool-use came from one (Claude/Codex only — Cursor/Kimi never send this field, confirmed live). */
|
|
191
|
+
agentId?: string;
|
|
190
192
|
/** Harness-reported working directory used to scope Codex authorization. */
|
|
191
193
|
cwd?: string;
|
|
192
194
|
/** Validated Cursor multi-root workspace paths in wire order. */
|
|
@@ -906,6 +908,16 @@ interface HandleOutcome {
|
|
|
906
908
|
/**
|
|
907
909
|
* Run one hook and adapt every Cursor scope outcome at the common runtime exit.
|
|
908
910
|
* Other harnesses retain the core handler's stdout and exit status unchanged.
|
|
911
|
+
* Cursor's shared `additional_context` budget context (see
|
|
912
|
+
* `../adapters/cursor/context-budget.ts`) is assembled here too — this is
|
|
913
|
+
* the single point every Cursor stdout passes through exactly once, so it's
|
|
914
|
+
* also the single point that reserves from and records into the registry.
|
|
915
|
+
* With no `session_id`/`conversation_id` at all, `sessionId` is `""` — the
|
|
916
|
+
* registry key would degenerate to one bucket shared by every session-less
|
|
917
|
+
* call on the same (cwd, event) pair, so `budget` stays `undefined` instead
|
|
918
|
+
* (falls back to the flat per-response cap in `toCursorLifecycleResponse`,
|
|
919
|
+
* with zero registry I/O). `stateDir` honors `opts.home` (test-only OS home
|
|
920
|
+
* override, see `HandleOptions`) so tests never need the real `os.homedir()`.
|
|
909
921
|
*/
|
|
910
922
|
declare function handleHook(id: string, payload: Record<string, unknown>, opts: HandleOptions): Promise<HandleOutcome>;
|
|
911
923
|
//#endregion
|
package/dist/runtime/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { r as projectLayout } from "../layout-KWoE_Mqn.mjs";
|
|
2
2
|
import { a as sanitizeSessionId, c as sessionsDir, i as loadSessionState, n as fuseHarnessHome, o as saveSessionState, r as fusengineCache, s as sessionStatePath, t as claudeHome } from "../home-state-oUGFB4ds.mjs";
|
|
3
|
-
import { A as
|
|
3
|
+
import { A as gate, At as taskContext, B as writePluginMap, Bt as todayUtc, C as trackEnrichment, Ct as removeOldFiles, D as DEFAULT_WINDOW_MS, Dt as projectContext, E as dispatchAipilot, Et as gitContext, F as extractSymbols, Ft as isoUtc, G as mergeLines, H as isProject, I as respond, It as loadSecurityState, J as listChildren, K as countFiles, L as dispatchLessons, Lt as saveSecurityState, M as preCommitGate, Mt as projectHash, N as detectDuplication, Nt as trackFile, O as REQUIRED_AGENTS, Ot as claudeMdKey, P as dryGate, Pt as normalizeEvent, R as cartoSessionStart, Rt as securityStateDir, S as trackSkillRead, St as purgeTtlTree, T as aipilotPostToolUse, Tt as devContext, U as writeTree, V as generateProjectMap, W as loadEnriched, X as lessonsFileFor, Y as lessonsArchiveFileFor, Z as lessonsStateFileFor, _ as securityAdvisory, _t as injectRules, a as mcpPostStore, at as cleanupSession, b as trackWatchResearch, bt as sessionStartCore, c as WEBFETCH_TTL_MS, ct as validateTeammateOutput, d as queryOf, dt as validateTailwind, f as lifecycleStdout, ft as validateSolidGate, g as dispatchMemory, gt as solidDetectStart, h as seoPostToolUseResponse, ht as detectSolidProfile, i as recordActivity, it as validateRulesLoaded, j as gateCommandCandidates, jt as defaultStateDir, k as TRIVIAL_BUDGET, kt as promptSubmitContext, l as cacheQueryOf, lt as trackAgentMemory, m as seoPostToolUse, mt as countLoc, n as activityFor, nt as postEditTypescript, o as mcpPreIntercept, ot as saveApexState, p as postEditContext, pt as checkFileSize, q as getFileDesc, r as handlePre, rt as trackSessionChanges, s as MCP_TTL_MS, st as logToolFailure, t as handleHook, u as isMcpTool, ut as subagentCacheContext, v as securityAdvisoryForPatch, vt as readRules, w as dispatchLifecycle, wt as trimLogFile, x as trackMcpResearch, xt as pruneEmptyDirs, y as postTrackingSideEffects, yt as runSessionStartCleanups, z as generateEcosystemMap, zt as securityStatePath } from "../handle-B-g9KL_9.mjs";
|
|
4
4
|
//#region src/runtime/storage.ts
|
|
5
5
|
/**
|
|
6
6
|
* The project's single state dir (`<root>/.harness`) — neutral + harness-agnostic,
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
//#region src/util/runtime-io.ts
|
|
5
|
+
/**
|
|
6
|
+
* Cross-runtime I/O helpers (Node + Bun). The published CLI ships a `node`
|
|
7
|
+
* shebang and is invoked via `npx`/`npm`/`bunx`/`bun`; using only `node:*` APIs
|
|
8
|
+
* here keeps the bundle runnable under EVERY runtime — replacing the Bun-only
|
|
9
|
+
* `Bun.file/write/spawn/sleep/stdin` calls and the `bun` `Glob` import that made
|
|
10
|
+
* the package crash with `Cannot find package 'bun'` under plain Node.
|
|
11
|
+
*/
|
|
12
|
+
/** Read a file as UTF-8 text (throws on missing/unreadable — callers catch). */
|
|
13
|
+
function readText(path) {
|
|
14
|
+
return readFileSync(path, "utf8");
|
|
15
|
+
}
|
|
16
|
+
/** True when `path` exists. */
|
|
17
|
+
function pathExists(path) {
|
|
18
|
+
return existsSync(path);
|
|
19
|
+
}
|
|
20
|
+
/** Write `data` to `path`, creating parent dirs (mirrors `Bun.write`). */
|
|
21
|
+
function writeText(path, data) {
|
|
22
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
23
|
+
writeFileSync(path, data, { encoding: "utf8" });
|
|
24
|
+
}
|
|
25
|
+
/** Resolve after `ms` milliseconds (`Bun.sleep` replacement). */
|
|
26
|
+
function sleep(ms) {
|
|
27
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
28
|
+
}
|
|
29
|
+
/** Run `cmd args` in `cwd` and capture stdout text ("" on failure/non-zero). */
|
|
30
|
+
function spawnCapture(cmd, args, cwd) {
|
|
31
|
+
try {
|
|
32
|
+
const r = spawnSync(cmd, args, {
|
|
33
|
+
cwd,
|
|
34
|
+
encoding: "utf8"
|
|
35
|
+
});
|
|
36
|
+
return r.status === 0 ? r.stdout ?? "" : "";
|
|
37
|
+
} catch {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Read the full process stdin as UTF-8 text (works under Node and Bun).
|
|
43
|
+
* Synchronous fd-0 read — never `for await (const c of process.stdin)` —
|
|
44
|
+
* because Bun has a confirmed Linux-only bug where merely adding a new
|
|
45
|
+
* top-level module import shifts stdin's internal init order and makes
|
|
46
|
+
* that async iterator silently yield zero bytes (oven-sh/bun#25320,
|
|
47
|
+
* #27849): exactly what broke every `harness hook` invocation on CI the
|
|
48
|
+
* moment `figures` became this package's first runtime dependency.
|
|
49
|
+
* `readFileSync(0)` bypasses that stream/ownership machinery entirely and
|
|
50
|
+
* is well-supported for piped (non-TTY) stdin under both runtimes.
|
|
51
|
+
*/
|
|
52
|
+
async function readStdin() {
|
|
53
|
+
try {
|
|
54
|
+
return readFileSync(0, "utf8");
|
|
55
|
+
} catch {
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Recursively collect files under `dir` whose extension is in `exts`, skipping
|
|
61
|
+
* `node_modules` and dot-dirs, capped at `cap`. `Bun.Glob().scan()` replacement.
|
|
62
|
+
* @param dir - Directory to walk.
|
|
63
|
+
* @param exts - Allowed extensions including the dot (e.g. `.ts`).
|
|
64
|
+
* @param out - Accumulator (mutated in place).
|
|
65
|
+
* @param cap - Max files to collect.
|
|
66
|
+
*/
|
|
67
|
+
function collectFiles(dir, exts, out, cap) {
|
|
68
|
+
if (out.length >= cap) return;
|
|
69
|
+
let entries;
|
|
70
|
+
try {
|
|
71
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
72
|
+
} catch {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
for (const e of entries) {
|
|
76
|
+
if (out.length >= cap) return;
|
|
77
|
+
if (e.name === "node_modules" || e.name.startsWith(".")) continue;
|
|
78
|
+
const full = join(dir, e.name);
|
|
79
|
+
if (e.isDirectory()) collectFiles(full, exts, out, cap);
|
|
80
|
+
else if (exts.has(e.name.slice(e.name.lastIndexOf(".")))) out.push(full);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
84
|
+
export { sleep as a, readText as i, pathExists as n, spawnCapture as o, readStdin as r, writeText as s, collectFiles as t };
|
|
@@ -50,7 +50,23 @@ interface SessionTrack {
|
|
|
50
50
|
brainstormRequired?: boolean;
|
|
51
51
|
/** Verification receipts (tsc/test) at PostToolUse; absent/empty reads as unverified in the TaskCompleted gate (backward compat, fail-closed). See {@link Receipt}. */
|
|
52
52
|
receipts?: Receipt[];
|
|
53
|
+
/** PRD (task/agent ownership coordination) — `agent_id` bound to its resolved agent-report name. Optional: absent on any track predating the PRD module (backward compat). */
|
|
54
|
+
prdOwners?: Record<string, string>;
|
|
55
|
+
/** PRD cross-check violations detected at PostToolUse (capped, append-only). */
|
|
56
|
+
prdViolations?: PrdViolationRecord[];
|
|
57
|
+
/** PRD SubagentStop/Stop one-shot block markers, keyed `"sessionId:event[:agent]"` -> the epoch-ms the block fired. */
|
|
58
|
+
prdStopBlocked?: Record<string, number>;
|
|
53
59
|
}
|
|
60
|
+
/** One PRD cross-check violation, timestamped for the journal/dedup. */
|
|
61
|
+
interface PrdViolationRecord {
|
|
62
|
+
ts: number;
|
|
63
|
+
task: string;
|
|
64
|
+
agent: string;
|
|
65
|
+
sub: string;
|
|
66
|
+
reason: string;
|
|
67
|
+
}
|
|
68
|
+
/** Cap on {@link SessionTrack.prdViolations} (mirrors the `receipts` style bound — oldest evicted first). */
|
|
69
|
+
declare const PRD_VIOLATIONS_CAP = 50;
|
|
54
70
|
/** A fresh, empty track. */
|
|
55
71
|
declare function emptyTrack(): SessionTrack;
|
|
56
72
|
/**
|
|
@@ -85,5 +101,20 @@ declare function recordTrivialEdit(track: SessionTrack, ts: number, windowMs: nu
|
|
|
85
101
|
declare function trivialCount(track: SessionTrack, windowMs: number, now: number): number;
|
|
86
102
|
/** Set the brainstorm-required flag (from creation-intent detection). Immutable. */
|
|
87
103
|
declare function recordBrainstormRequired(track: SessionTrack, required: boolean): SessionTrack;
|
|
104
|
+
/** Bind `agentId` to its resolved PRD agent-report `name` (merge; idempotent no-op if already bound to the same name). Immutable. */
|
|
105
|
+
declare function recordPrdOwner(track: SessionTrack, agentId: string, name: string): SessionTrack;
|
|
106
|
+
/**
|
|
107
|
+
* Append one PRD cross-check violation. Immutable, uncapped in the mutator
|
|
108
|
+
* itself — same append-only shape as {@link Receipt} — so the journal diff's
|
|
109
|
+
* prefix comparison (`tail`, track-diff.ts) never sees an eviction it would
|
|
110
|
+
* mistake for a bulk rewrite. The {@link PRD_VIOLATIONS_CAP} bound is applied
|
|
111
|
+
* only when the journal is folded/read (track-journal.ts's `foldEvents`),
|
|
112
|
+
* mirroring how `trivialEdits`'s sliding window is re-derived at fold time.
|
|
113
|
+
*/
|
|
114
|
+
declare function recordPrdViolation(track: SessionTrack, v: PrdViolationRecord): SessionTrack;
|
|
115
|
+
/** Record a PRD SubagentStop/Stop one-shot block marker (merge). Immutable. */
|
|
116
|
+
declare function recordPrdStopBlocked(track: SessionTrack, key: string, ts: number): SessionTrack;
|
|
117
|
+
/** True when the PRD one-shot block for `key` has already fired. */
|
|
118
|
+
declare function prdAlreadyBlocked(track: SessionTrack, key: string): boolean;
|
|
88
119
|
//#endregion
|
|
89
|
-
export {
|
|
120
|
+
export { trivialCount as _, agentsFresh as a, recordAgent as c, recordPrdOwner as d, recordPrdStopBlocked as f, recordTrivialEdit as g, recordTarget as h, SessionTrack as i, recordBrainstormRequired as l, recordRefRead as m, PRD_VIOLATIONS_CAP as n, emptyTrack as o, recordPrdViolation as p, PrdViolationRecord as r, prdAlreadyBlocked as s, AgentQuality as t, recordDoc as u };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as HOME_DIR } from "./dotenv-
|
|
1
|
+
import { t as HOME_DIR } from "./dotenv-C1LkcfW-.mjs";
|
|
2
2
|
import { t as detectHarness } from "./harness-BMuLJ9lm.mjs";
|
|
3
3
|
import { l as PLUGINS_DIR } from "./home-state-oUGFB4ds.mjs";
|
|
4
4
|
import { join } from "node:path";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { i as ttlLabel } from "./ttl-Dgwg_QAv.mjs";
|
|
2
2
|
import { n as fuseHarnessHome } from "./home-state-oUGFB4ds.mjs";
|
|
3
3
|
import { a as writeJsonFile, i as readJsonFile, t as atomicWrite } from "./json-io-DisYd2fb.mjs";
|
|
4
4
|
import { i as resolveSessions } from "./doc-helpers-CWZegVdR.mjs";
|
|
5
5
|
import { t as routeReferences } from "./router-PKVNBHge.mjs";
|
|
6
|
-
import { t as resolveSkillPath } from "./skill-path-
|
|
6
|
+
import { t as resolveSkillPath } from "./skill-path-Cz8WFaGu.mjs";
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
8
8
|
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { createHmac, randomBytes } from "node:crypto";
|
|
@@ -142,6 +142,8 @@ const apexAuthorizationGate = (ctx) => {
|
|
|
142
142
|
};
|
|
143
143
|
//#endregion
|
|
144
144
|
//#region src/tracking/session-state.ts
|
|
145
|
+
/** Cap on {@link SessionTrack.prdViolations} (mirrors the `receipts` style bound — oldest evicted first). */
|
|
146
|
+
const PRD_VIOLATIONS_CAP = 50;
|
|
145
147
|
/** A fresh, empty track. */
|
|
146
148
|
function emptyTrack() {
|
|
147
149
|
return {
|
|
@@ -244,6 +246,45 @@ function recordBrainstormRequired(track, required) {
|
|
|
244
246
|
brainstormRequired: required
|
|
245
247
|
};
|
|
246
248
|
}
|
|
249
|
+
/** Bind `agentId` to its resolved PRD agent-report `name` (merge; idempotent no-op if already bound to the same name). Immutable. */
|
|
250
|
+
function recordPrdOwner(track, agentId, name) {
|
|
251
|
+
if (track.prdOwners?.[agentId] === name) return track;
|
|
252
|
+
return {
|
|
253
|
+
...track,
|
|
254
|
+
prdOwners: {
|
|
255
|
+
...track.prdOwners,
|
|
256
|
+
[agentId]: name
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Append one PRD cross-check violation. Immutable, uncapped in the mutator
|
|
262
|
+
* itself — same append-only shape as {@link Receipt} — so the journal diff's
|
|
263
|
+
* prefix comparison (`tail`, track-diff.ts) never sees an eviction it would
|
|
264
|
+
* mistake for a bulk rewrite. The {@link PRD_VIOLATIONS_CAP} bound is applied
|
|
265
|
+
* only when the journal is folded/read (track-journal.ts's `foldEvents`),
|
|
266
|
+
* mirroring how `trivialEdits`'s sliding window is re-derived at fold time.
|
|
267
|
+
*/
|
|
268
|
+
function recordPrdViolation(track, v) {
|
|
269
|
+
return {
|
|
270
|
+
...track,
|
|
271
|
+
prdViolations: [...track.prdViolations ?? [], v]
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
/** Record a PRD SubagentStop/Stop one-shot block marker (merge). Immutable. */
|
|
275
|
+
function recordPrdStopBlocked(track, key, ts) {
|
|
276
|
+
return {
|
|
277
|
+
...track,
|
|
278
|
+
prdStopBlocked: {
|
|
279
|
+
...track.prdStopBlocked,
|
|
280
|
+
[key]: ts
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
/** True when the PRD one-shot block for `key` has already fired. */
|
|
285
|
+
function prdAlreadyBlocked(track, key) {
|
|
286
|
+
return track.prdStopBlocked?.[key] !== void 0;
|
|
287
|
+
}
|
|
247
288
|
//#endregion
|
|
248
289
|
//#region src/tracking/integrity.ts
|
|
249
290
|
/**
|
|
@@ -572,6 +613,9 @@ function foldEvents(events, base = emptyTrack()) {
|
|
|
572
613
|
const at = { ...base.refsReadAt };
|
|
573
614
|
const receipts = [...base.receipts ?? []];
|
|
574
615
|
const trivial = new Set(base.trivialEdits ?? []);
|
|
616
|
+
const prdOwners = { ...base.prdOwners };
|
|
617
|
+
const prdViolations = [...base.prdViolations ?? []];
|
|
618
|
+
const prdStopBlocked = { ...base.prdStopBlocked };
|
|
575
619
|
let targetTs = base.target ? Date.parse(base.target.set_at) || 0 : 0, brainstormTs = 0;
|
|
576
620
|
for (const ev of [...events].sort((a, b) => a.ts - b.ts)) if (ev.field === "refsRead") {
|
|
577
621
|
const p = String(ev.value);
|
|
@@ -591,9 +635,19 @@ function foldEvents(events, base = emptyTrack()) {
|
|
|
591
635
|
} else if (ev.field === "brainstormRequired" && ev.ts >= brainstormTs) {
|
|
592
636
|
t.brainstormRequired = Boolean(ev.value);
|
|
593
637
|
brainstormTs = ev.ts;
|
|
638
|
+
} else if (ev.field === "prdOwners") {
|
|
639
|
+
const [aid, name] = ev.value;
|
|
640
|
+
prdOwners[aid] = name;
|
|
641
|
+
} else if (ev.field === "prdViolations") prdViolations.push(ev.value);
|
|
642
|
+
else if (ev.field === "prdStopBlocked") {
|
|
643
|
+
const [key, ts] = ev.value;
|
|
644
|
+
prdStopBlocked[key] = ts;
|
|
594
645
|
}
|
|
595
646
|
if (Object.keys(at).length) t.refsReadAt = at;
|
|
596
647
|
if (receipts.length) t.receipts = receipts;
|
|
648
|
+
if (Object.keys(prdOwners).length) t.prdOwners = prdOwners;
|
|
649
|
+
if (prdViolations.length) t.prdViolations = prdViolations.slice(-50);
|
|
650
|
+
if (Object.keys(prdStopBlocked).length) t.prdStopBlocked = prdStopBlocked;
|
|
597
651
|
const maxT = Math.max(0, ...trivial);
|
|
598
652
|
t.trivialEdits = [...trivial].filter((x) => x > maxT - TRIVIAL_WINDOW_MS).sort((a, b) => a - b);
|
|
599
653
|
return t;
|
|
@@ -673,6 +727,24 @@ function diffTrackEvents(prev, next, now) {
|
|
|
673
727
|
value: next.brainstormRequired,
|
|
674
728
|
ts: now
|
|
675
729
|
});
|
|
730
|
+
for (const [aid, name] of Object.entries(next.prdOwners ?? {})) if (prev.prdOwners?.[aid] !== name) out.push({
|
|
731
|
+
field: "prdOwners",
|
|
732
|
+
op: "merge",
|
|
733
|
+
value: [aid, name],
|
|
734
|
+
ts: now
|
|
735
|
+
});
|
|
736
|
+
for (const v of tail(prev.prdViolations ?? [], next.prdViolations ?? [])) out.push({
|
|
737
|
+
field: "prdViolations",
|
|
738
|
+
op: "append",
|
|
739
|
+
value: v,
|
|
740
|
+
ts: v.ts ?? now
|
|
741
|
+
});
|
|
742
|
+
for (const [key, ts] of Object.entries(next.prdStopBlocked ?? {})) if (prev.prdStopBlocked?.[key] !== ts) out.push({
|
|
743
|
+
field: "prdStopBlocked",
|
|
744
|
+
op: "merge",
|
|
745
|
+
value: [key, ts],
|
|
746
|
+
ts
|
|
747
|
+
});
|
|
676
748
|
return out;
|
|
677
749
|
}
|
|
678
750
|
/** The journal log path twin of a track snapshot path. */
|
|
@@ -842,4 +914,4 @@ async function withTrack(file, mutate) {
|
|
|
842
914
|
}) !== LOCK_FAILED;
|
|
843
915
|
}
|
|
844
916
|
//#endregion
|
|
845
|
-
export {
|
|
917
|
+
export { recordPrdViolation as C, trivialCount as D, recordTrivialEdit as E, apexAuthorizationGate as O, recordPrdStopBlocked as S, recordTarget as T, prdAlreadyBlocked as _, journalLogPath as a, recordDoc as b, appendEvent as c, signTrack as d, verifyTrack as f, emptyTrack as g, agentsFresh as h, withTrack as i, withTrackLockSync as l, PRD_VIOLATIONS_CAP as m, saveTrack as n, readTrackSync as o, writeLastNonce as p, trackJournalEnabled as r, diffTrackEvents as s, loadTrack as t, LOCK_FAILED as u, recordAgent as v, recordRefRead as w, recordPrdOwner as x, recordBrainstormRequired as y };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { _ as trivialCount, a as agentsFresh, c as recordAgent, d as recordPrdOwner, f as recordPrdStopBlocked, g as recordTrivialEdit, h as recordTarget, i as SessionTrack, l as recordBrainstormRequired, m as recordRefRead, n as PRD_VIOLATIONS_CAP, o as emptyTrack, p as recordPrdViolation, r as PrdViolationRecord, s as prdAlreadyBlocked, t as AgentQuality, u as recordDoc } from "../session-state-COg7Ej_2.mjs";
|
|
2
2
|
//#region src/tracking/store.d.ts
|
|
3
3
|
/** Journal mode is the default; `FUSE_TRACK_JOURNAL=0` reverts to the legacy locked RMW (see the module NOTE: =0 ignores the uncompacted journal — recoverable, keep the `.log`). */
|
|
4
4
|
declare function trackJournalEnabled(): boolean;
|
|
@@ -26,4 +26,4 @@ declare function saveTrack(file: string, track: SessionTrack): Promise<void>;
|
|
|
26
26
|
*/
|
|
27
27
|
declare function withTrack(file: string, mutate: (track: SessionTrack) => SessionTrack | Promise<SessionTrack>): Promise<boolean>;
|
|
28
28
|
//#endregion
|
|
29
|
-
export { AgentQuality, SessionTrack, agentsFresh, emptyTrack, loadTrack, recordAgent, recordBrainstormRequired, recordDoc, recordRefRead, recordTarget, recordTrivialEdit, saveTrack, trackJournalEnabled, trivialCount, withTrack };
|
|
29
|
+
export { AgentQuality, PRD_VIOLATIONS_CAP, PrdViolationRecord, SessionTrack, agentsFresh, emptyTrack, loadTrack, prdAlreadyBlocked, recordAgent, recordBrainstormRequired, recordDoc, recordPrdOwner, recordPrdStopBlocked, recordPrdViolation, recordRefRead, recordTarget, recordTrivialEdit, saveTrack, trackJournalEnabled, trivialCount, withTrack };
|
package/dist/tracking/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { agentsFresh, emptyTrack, loadTrack, recordAgent, recordBrainstormRequired, recordDoc, recordRefRead, recordTarget, recordTrivialEdit, saveTrack, trackJournalEnabled, trivialCount, withTrack };
|
|
1
|
+
import { C as recordPrdViolation, D as trivialCount, E as recordTrivialEdit, S as recordPrdStopBlocked, T as recordTarget, _ as prdAlreadyBlocked, b as recordDoc, g as emptyTrack, h as agentsFresh, i as withTrack, m as PRD_VIOLATIONS_CAP, n as saveTrack, r as trackJournalEnabled, t as loadTrack, v as recordAgent, w as recordRefRead, x as recordPrdOwner, y as recordBrainstormRequired } from "../store-BVY6gIYM.mjs";
|
|
2
|
+
export { PRD_VIOLATIONS_CAP, agentsFresh, emptyTrack, loadTrack, prdAlreadyBlocked, recordAgent, recordBrainstormRequired, recordDoc, recordPrdOwner, recordPrdStopBlocked, recordPrdViolation, recordRefRead, recordTarget, recordTrivialEdit, saveTrack, trackJournalEnabled, trivialCount, withTrack };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { l as parseEnvInt } from "./limits-DG5pl-5s.mjs";
|
|
2
|
+
//#region src/config/ttl.ts
|
|
3
|
+
/**
|
|
4
|
+
* Default enforcement-freshness window, in seconds (2 minutes). Matches the
|
|
5
|
+
* plugin's original `FUSE_ENFORCE_TTL_SEC` default. This is the constant that
|
|
6
|
+
* actually reaches production (via `resolveTtlSec` -> `bin.ts`); `gate.ts`'s
|
|
7
|
+
* `DEFAULT_WINDOW_MS` is only a fallback for direct programmatic callers that
|
|
8
|
+
* omit `windowMs` (e.g. tests) and never applies on the real CLI path.
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_TTL_SEC = 120;
|
|
11
|
+
/** Default env var name carrying the TTL override. */
|
|
12
|
+
const TTL_ENV_KEY = "FUSE_ENFORCE_TTL_SEC";
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the enforcement TTL (seconds) from an env map.
|
|
15
|
+
* @param env - environment map (defaults to `process.env`)
|
|
16
|
+
* @param key - env var name (defaults to `FUSE_ENFORCE_TTL_SEC`)
|
|
17
|
+
*/
|
|
18
|
+
function resolveTtlSec(env = process.env, key = TTL_ENV_KEY) {
|
|
19
|
+
return parseEnvInt(env[key], 120);
|
|
20
|
+
}
|
|
21
|
+
/** Human label for a TTL: 120 -> "2min", 240 -> "4min", 90 -> "90s". */
|
|
22
|
+
function ttlLabel(sec) {
|
|
23
|
+
return sec % 60 === 0 ? `${sec / 60}min` : `${sec}s`;
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { ttlLabel as i, TTL_ENV_KEY as n, resolveTtlSec as r, DEFAULT_TTL_SEC as t };
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { a as resolveMaxLines, c as storeBudget, i as hookBudget } from "./limits-DG5pl-5s.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { i as ttlLabel } from "./ttl-Dgwg_QAv.mjs";
|
|
3
3
|
import { A as moduleAwarePath, S as isHooksPath, T as isStoresPath, _ as rolloutVerdict, at as maskCommentsOnly, it as maskCommentsAndStrings, nt as nearestManifestDir, ot as DEV_KEYWORDS, rt as projectCaps, x as isComponentsPath, y as declaresInterface } from "./evaluate-CMiIqHeH.mjs";
|
|
4
4
|
import { d as countFrameworkCodeLines, l as PLUGINS_DIR, u as SOLID_REF } from "./home-state-oUGFB4ds.mjs";
|
|
5
5
|
import { r as isDocConsulted, t as formatDocDeny } from "./doc-helpers-CWZegVdR.mjs";
|
|
6
6
|
import { t as routeReferences } from "./router-PKVNBHge.mjs";
|
|
7
|
-
import { n as findMarketplacePlugins, r as readPluginMeta, t as resolveSkillPath } from "./skill-path-
|
|
7
|
+
import { n as findMarketplacePlugins, r as readPluginMeta, t as resolveSkillPath } from "./skill-path-Cz8WFaGu.mjs";
|
|
8
|
+
import { n as apexPlanTool, r as harnessHomeSegment, t as apexDocName } from "./apex-target-Xc2M32Pl.mjs";
|
|
8
9
|
import { dirname, extname, join, resolve } from "node:path";
|
|
9
10
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
10
11
|
import { homedir } from "node:os";
|
|
@@ -1446,52 +1447,6 @@ function getExpertAgent(type, pluginsDirOverride) {
|
|
|
1446
1447
|
return "general-purpose";
|
|
1447
1448
|
}
|
|
1448
1449
|
//#endregion
|
|
1449
|
-
//#region src/policy/apex-target.ts
|
|
1450
|
-
/**
|
|
1451
|
-
* Harness-target helpers for the APEX context builders. Single source of
|
|
1452
|
-
* truth for the home-dir segment / root doc name / plan-tool idiom that
|
|
1453
|
-
* differ between Claude Code and Codex (and beyond), so the builders in
|
|
1454
|
-
* `claude-md-context.ts`, `apex-task-context.ts`, and `inject-apex.ts` never
|
|
1455
|
-
* hardcode `.claude`/`CLAUDE.md`/`TaskCreate` again. Runtime `id` values flow
|
|
1456
|
-
* through the pipeline as plain `string` (see `handleHook(id: string, ...)`),
|
|
1457
|
-
* so these helpers accept `string` and narrow internally — same idiom as the
|
|
1458
|
-
* existing `HOME_DIR[id] ?? ".claude"` precedent in `cartographer/detect.ts`.
|
|
1459
|
-
*/
|
|
1460
|
-
/**
|
|
1461
|
-
* Home-dir segment for a harness target (".claude", ".codex", ...). Falls
|
|
1462
|
-
* back to ".claude" for any harness without a {@link HOME_DIR} entry.
|
|
1463
|
-
* @param id - Harness id (e.g. "claude-code", "codex").
|
|
1464
|
-
* @returns The home-dir segment, e.g. ".claude".
|
|
1465
|
-
*/
|
|
1466
|
-
function harnessHomeSegment(id) {
|
|
1467
|
-
return HOME_DIR[id] ?? ".claude";
|
|
1468
|
-
}
|
|
1469
|
-
/**
|
|
1470
|
-
* Root instructions doc name for a harness target: "AGENTS.md" for Codex and
|
|
1471
|
-
* Kimi Code CLI (both documented conventions), "CLAUDE.md" for every other
|
|
1472
|
-
* target (including the default/unknown fallback).
|
|
1473
|
-
* @param id - Harness id.
|
|
1474
|
-
* @returns The doc file name.
|
|
1475
|
-
*/
|
|
1476
|
-
function apexDocName(id) {
|
|
1477
|
-
return id === "codex" || id === "kimi" ? "AGENTS.md" : "CLAUDE.md";
|
|
1478
|
-
}
|
|
1479
|
-
/**
|
|
1480
|
-
* Native plan/track tool name for a harness target: Codex's `update_plan`,
|
|
1481
|
-
* Kimi Code CLI's `TodoList`, or Claude's `TaskCreate` (default). Covers only
|
|
1482
|
-
* the PLAN-step line — other target-specific tool references (`TaskList`,
|
|
1483
|
-
* `TaskUpdate`) are resolved via `apexTaskSteps`/`apexAgentSteps` in
|
|
1484
|
-
* `apex-target-steps.ts` since their surrounding prose differs structurally,
|
|
1485
|
-
* not just by token.
|
|
1486
|
-
* @param id - Harness id.
|
|
1487
|
-
* @returns The plan-tool name.
|
|
1488
|
-
*/
|
|
1489
|
-
function apexPlanTool(id) {
|
|
1490
|
-
if (id === "codex") return "update_plan";
|
|
1491
|
-
if (id === "kimi") return "TodoList";
|
|
1492
|
-
return "TaskCreate";
|
|
1493
|
-
}
|
|
1494
|
-
//#endregion
|
|
1495
1450
|
//#region src/policy/detect-claude-md-project-type.ts
|
|
1496
1451
|
/**
|
|
1497
1452
|
* Detect the project type from the cwd, reproducing the legacy Python logic:
|
|
@@ -1907,4 +1862,4 @@ function missingSeoElements(html) {
|
|
|
1907
1862
|
return missing;
|
|
1908
1863
|
}
|
|
1909
1864
|
//#endregion
|
|
1910
|
-
export {
|
|
1865
|
+
export { countStores as A, PRE_AUTH_GATES as B, usesTailwindUtilities as C, declaresQueryHook as D, declaresCustomHook as E, MAX_TOKENS as F, solidReadGate as G, docConsultedGate as H, capVerbosity as I, detectCreationIntent as L, isExcludedJsPath as M, isExcludedSwiftPath as N, importsTanstackQuery as O, MAX_EXA_RESULTS as P, APEX_GATES as R, skillTriggerGate as S, frameworkSolidGate as T, evaluateApex as U, brainstormGate as V, freshnessGate as W, detectClaudeMdProjectType as _, firstHeading as a, parseField as b, EXCLUDE_DIRS as c, buildApexTaskInjection as d, loadApexTaskState as f, buildClaudeMdContext as g, buildApexInstruction as h, firstComment as i, declaresStore as j, queryCapActive as k, PROJECT_INDICATORS as l, DEV_VERBS as m, missingSeoElements as n, parseEnrichment as o, apexAgentSteps as p, descFromText as r, parseEntry as s, isHtmlLike as t, buildApexTaskContext as u, scanPlugin as v, SKILL_TRIGGERS as w, detectRequiredSkills as x, parseBodyDesc as y, POST_AUTH_GATES as z };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusengine/harness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.93",
|
|
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",
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module context-budget
|
|
3
|
+
* Cursor-only shared `additional_context` budget registry. Cursor 3.18.25
|
|
4
|
+
* runs every hook plugin configured on an event in its OWN process, then
|
|
5
|
+
* merges their `additional_context` outputs with a 9-char `"\n\n---\n\n"`
|
|
6
|
+
* separator and drops the WHOLE merge past 10,000 UTF-16 units — so no
|
|
7
|
+
* single process can know the total by itself. This module gives every
|
|
8
|
+
* plugin's process a shared, best-effort view of that total via a small
|
|
9
|
+
* JSON registry file under the project's state dir (see `../../runtime/paths.ts`),
|
|
10
|
+
* keyed by `${sessionId}|${event}|${generationId ?? ""}|${toolUseId ?? ""}`
|
|
11
|
+
* (one key per merge group — Cursor merges preToolUse/postToolUse/
|
|
12
|
+
* postToolUseFailure PER TOOL CALL, so `toolUseId` joins the key on those
|
|
13
|
+
* three events), with entries older than 10s ignored (concurrent hooks on one
|
|
14
|
+
* event fire within the same second). Best-effort, fail-open throughout: any
|
|
15
|
+
* I/O or JSON error degrades to "no shared budget", i.e. the flat
|
|
16
|
+
* per-response cap in `./context-limit.ts` alone — never a thrown error, and
|
|
17
|
+
* never a Cursor-side regression.
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { atomicWrite } from "../../util/json-io";
|
|
22
|
+
import {
|
|
23
|
+
ADDITIONAL_CONTEXT_LIMIT, TRUNCATION_MARKER, additionalContextLength, capAdditionalContext, omitAdditionalContext,
|
|
24
|
+
} from "./context-limit";
|
|
25
|
+
import type { CursorBudgetContext } from "./interfaces/context-budget";
|
|
26
|
+
|
|
27
|
+
const REGISTRY_FILE = "cursor-context-budget.json";
|
|
28
|
+
/** Matches Cursor 3.18.25's observed `"\n\n---\n\n"` merge separator length. */
|
|
29
|
+
const SEPARATOR_LENGTH = 9;
|
|
30
|
+
const ENTRY_WINDOW_MS = 10_000;
|
|
31
|
+
/** Below this, a truncated value would carry more marker than budget — omit the field instead. */
|
|
32
|
+
const OMIT_THRESHOLD = TRUNCATION_MARKER.length + 100;
|
|
33
|
+
|
|
34
|
+
interface BudgetEntry {
|
|
35
|
+
at: number;
|
|
36
|
+
length: number;
|
|
37
|
+
}
|
|
38
|
+
type BudgetRegistry = Record<string, BudgetEntry[]>;
|
|
39
|
+
|
|
40
|
+
function isRegistry(value: unknown): value is BudgetRegistry {
|
|
41
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function registryPath(stateDir: string): string {
|
|
45
|
+
return join(stateDir, REGISTRY_FILE);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function budgetKey(ctx: Pick<CursorBudgetContext, "sessionId" | "event" | "generationId" | "toolUseId">): string {
|
|
49
|
+
return `${ctx.sessionId}|${ctx.event}|${ctx.generationId ?? ""}|${ctx.toolUseId ?? ""}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function loadRegistry(path: string): BudgetRegistry {
|
|
53
|
+
try {
|
|
54
|
+
if (!existsSync(path)) return {};
|
|
55
|
+
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
56
|
+
return isRegistry(parsed) ? parsed : {};
|
|
57
|
+
} catch {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function freshEntries(entries: BudgetEntry[] | undefined, now: number): BudgetEntry[] {
|
|
63
|
+
return (entries ?? []).filter((entry) => now - entry.at <= ENTRY_WINDOW_MS);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Sum of a key's fresh entry lengths plus the separators already joining them. */
|
|
67
|
+
function consumed(entries: BudgetEntry[]): number {
|
|
68
|
+
return entries.reduce((total, entry) => total + entry.length, 0) + SEPARATOR_LENGTH * Math.max(0, entries.length - 1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** {@link reserveAdditionalContext} input: budget context plus the length the caller wants to emit. */
|
|
72
|
+
export type ReserveInput = CursorBudgetContext & { wanted: number };
|
|
73
|
+
/** {@link recordAdditionalContext} input: budget context plus the length actually emitted. */
|
|
74
|
+
export type RecordInput = CursorBudgetContext & { emitted: number };
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Reserve room in the shared budget for one hook's `additional_context`
|
|
78
|
+
* contribution to one (session, event, generation) merge group. `wanted` is
|
|
79
|
+
* accepted for a symmetric call shape with {@link recordAdditionalContext}
|
|
80
|
+
* but does not shrink `allowed` itself — the ceiling only depends on what
|
|
81
|
+
* OTHER entries already hold; a smaller `wanted` simply means the caller
|
|
82
|
+
* won't need all of it. Best-effort, fail-open: any I/O/JSON error returns
|
|
83
|
+
* the full flat ceiling, as if no other plugin had run.
|
|
84
|
+
* @param input - Registry location, reservation key, and the wanted length.
|
|
85
|
+
*/
|
|
86
|
+
export function reserveAdditionalContext(input: ReserveInput): { allowed: number } {
|
|
87
|
+
try {
|
|
88
|
+
const now = input.now ?? Date.now();
|
|
89
|
+
const registry = loadRegistry(registryPath(input.stateDir));
|
|
90
|
+
const fresh = freshEntries(registry[budgetKey(input)], now);
|
|
91
|
+
const separator = fresh.length > 0 ? SEPARATOR_LENGTH : 0;
|
|
92
|
+
return { allowed: Math.max(0, ADDITIONAL_CONTEXT_LIMIT - consumed(fresh) - separator) };
|
|
93
|
+
} catch {
|
|
94
|
+
return { allowed: ADDITIONAL_CONTEXT_LIMIT };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Record the length actually emitted for one reservation, best-effort.
|
|
100
|
+
* Prunes every key's stale entries while it holds the write so the registry
|
|
101
|
+
* file stays bounded. Silently no-ops on any I/O error (fail-open).
|
|
102
|
+
* @param input - Registry location, reservation key, and the emitted length.
|
|
103
|
+
*/
|
|
104
|
+
export function recordAdditionalContext(input: RecordInput): void {
|
|
105
|
+
try {
|
|
106
|
+
const now = input.now ?? Date.now();
|
|
107
|
+
const path = registryPath(input.stateDir);
|
|
108
|
+
const registry = loadRegistry(path);
|
|
109
|
+
const pruned: BudgetRegistry = {};
|
|
110
|
+
for (const [key, entries] of Object.entries(registry)) {
|
|
111
|
+
const fresh = freshEntries(entries, now);
|
|
112
|
+
if (fresh.length > 0) pruned[key] = fresh;
|
|
113
|
+
}
|
|
114
|
+
const key = budgetKey(input);
|
|
115
|
+
pruned[key] = [...(pruned[key] ?? []), { at: now, length: input.emitted }];
|
|
116
|
+
atomicWrite(path, JSON.stringify(pruned));
|
|
117
|
+
} catch {
|
|
118
|
+
// Best-effort: a lost entry only makes the NEXT reservation over-generous
|
|
119
|
+
// (never under), which is the safe direction to fail in.
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Cap a Cursor stdout JSON's `additional_context` against the shared budget
|
|
125
|
+
* instead of the flat per-response ceiling alone. Falls back to the plain
|
|
126
|
+
* cap (`./context-limit.ts`), unbudgeted, when `budget` is `undefined` or
|
|
127
|
+
* the stdout carries no `additional_context` at all.
|
|
128
|
+
* @param stdout - A native Cursor JSON stdout candidate.
|
|
129
|
+
* @param budget - Shared budget context, or `undefined` to skip it.
|
|
130
|
+
*/
|
|
131
|
+
export function capAdditionalContextWithBudget(stdout: string, budget: CursorBudgetContext | undefined): string {
|
|
132
|
+
if (!budget) return capAdditionalContext(stdout);
|
|
133
|
+
const wanted = additionalContextLength(stdout);
|
|
134
|
+
if (wanted === 0) return stdout;
|
|
135
|
+
const { allowed } = reserveAdditionalContext({ ...budget, wanted });
|
|
136
|
+
if (allowed < OMIT_THRESHOLD) {
|
|
137
|
+
process.stderr.write(`[fuse-harness] cursor: additional_context budget exhausted for ${budget.event} (allowed=${allowed})\n`);
|
|
138
|
+
return omitAdditionalContext(stdout);
|
|
139
|
+
}
|
|
140
|
+
const limit = Math.min(ADDITIONAL_CONTEXT_LIMIT, allowed);
|
|
141
|
+
const capped = capAdditionalContext(stdout, limit);
|
|
142
|
+
recordAdditionalContext({ ...budget, emitted: additionalContextLength(capped) });
|
|
143
|
+
return capped;
|
|
144
|
+
}
|