@fusengine/harness 0.1.34 → 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/cli/bin.mjs
CHANGED
|
@@ -3,13 +3,85 @@ import { r as resolveTtlSec } from "../ttl-BG55s6HZ.mjs";
|
|
|
3
3
|
import { t as detectHarness } from "../harness-C8Nxxyn_.mjs";
|
|
4
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);
|
|
@@ -16,6 +16,88 @@ 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;
|
|
@@ -671,49 +753,6 @@ function taskContext(cwd) {
|
|
|
671
753
|
return ctx ? contextResponse("PreToolUse", ctx) : "";
|
|
672
754
|
}
|
|
673
755
|
//#endregion
|
|
674
|
-
//#region src/runtime/home-state.ts
|
|
675
|
-
/** Home `~/.claude` dir (single source for every home-based hook path). */
|
|
676
|
-
function claudeHome(home = homedir()) {
|
|
677
|
-
return join(home, ".claude");
|
|
678
|
-
}
|
|
679
|
-
/** `~/.claude/fusengine-cache` base dir for legacy session/cache state. */
|
|
680
|
-
function fusengineCache(home = homedir()) {
|
|
681
|
-
return join(claudeHome(home), "fusengine-cache");
|
|
682
|
-
}
|
|
683
|
-
/** `~/.claude/fusengine-cache/sessions` — per-session JSON state dir. */
|
|
684
|
-
function sessionsDir(home = homedir()) {
|
|
685
|
-
return join(fusengineCache(home), "sessions");
|
|
686
|
-
}
|
|
687
|
-
const SID_RE = /^[a-zA-Z0-9_-]{1,128}$/;
|
|
688
|
-
/** Validate a session id (1-128 url-safe chars); null when invalid. */
|
|
689
|
-
function sanitizeSessionId(sid) {
|
|
690
|
-
const s = String(sid ?? "").trim();
|
|
691
|
-
return SID_RE.test(s) ? s : null;
|
|
692
|
-
}
|
|
693
|
-
/** Unified per-session state file path: `sessions/session-<sid>.json`. */
|
|
694
|
-
function sessionStatePath(sid, home = homedir()) {
|
|
695
|
-
return join(sessionsDir(home), `session-${sid}.json`);
|
|
696
|
-
}
|
|
697
|
-
/** Load a session-state dict, or `{}` when missing/corrupt (mirrors Python). */
|
|
698
|
-
function loadSessionState(sid, home = homedir()) {
|
|
699
|
-
const path = sessionStatePath(sid, home);
|
|
700
|
-
try {
|
|
701
|
-
if (!existsSync(path)) return {};
|
|
702
|
-
const data = JSON.parse(readFileSync(path, "utf-8"));
|
|
703
|
-
return typeof data === "object" && data !== null && !Array.isArray(data) ? data : {};
|
|
704
|
-
} catch {
|
|
705
|
-
return {};
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
/** Atomically persist a session-state dict (0o600 via atomicWrite, indent 2). */
|
|
709
|
-
function saveSessionState(sid, state, home = homedir()) {
|
|
710
|
-
mkdirSync(sessionsDir(home), {
|
|
711
|
-
recursive: true,
|
|
712
|
-
mode: 448
|
|
713
|
-
});
|
|
714
|
-
atomicWrite(sessionStatePath(sid, home), JSON.stringify(state, null, 2));
|
|
715
|
-
}
|
|
716
|
-
//#endregion
|
|
717
756
|
//#region src/runtime/dev-context.ts
|
|
718
757
|
/** Run a git subcommand in `cwd`, returning trimmed stdout or "" on error. */
|
|
719
758
|
function git(cwd, args) {
|
|
@@ -3017,45 +3056,6 @@ function trackEnrichment(filePath) {
|
|
|
3017
3056
|
} catch {}
|
|
3018
3057
|
}
|
|
3019
3058
|
//#endregion
|
|
3020
|
-
//#region src/runtime/lifecycle/security/skill-state.ts
|
|
3021
|
-
/**
|
|
3022
|
-
* Shared security-tracker state: per-UTC-day JSON under
|
|
3023
|
-
* `~/.claude/logs/00-security`. Ports the state helpers of
|
|
3024
|
-
* `check-security-skill.py` / `track-skill-read.py` / `track-mcp-research.py`.
|
|
3025
|
-
*/
|
|
3026
|
-
/** `~/.claude/logs/00-security` state directory. */
|
|
3027
|
-
function securityStateDir(home = homedir()) {
|
|
3028
|
-
return join(claudeHome(home), "logs", "00-security");
|
|
3029
|
-
}
|
|
3030
|
-
/** Current UTC date as `YYYY-MM-DD`. */
|
|
3031
|
-
function todayUtc(now = Date.now()) {
|
|
3032
|
-
return new Date(now).toISOString().slice(0, 10);
|
|
3033
|
-
}
|
|
3034
|
-
/** Current UTC instant as `YYYY-MM-DDTHH:MM:SSZ` (seconds, no millis). */
|
|
3035
|
-
function isoUtc(now = Date.now()) {
|
|
3036
|
-
return new Date(now).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
3037
|
-
}
|
|
3038
|
-
/** Today's security-state file path. */
|
|
3039
|
-
function securityStatePath(now = Date.now(), home = homedir()) {
|
|
3040
|
-
return join(securityStateDir(home), `${todayUtc(now)}-state.json`);
|
|
3041
|
-
}
|
|
3042
|
-
/** Load today's security state, or `{}` when missing/corrupt. */
|
|
3043
|
-
function loadSecurityState(now = Date.now(), home = homedir()) {
|
|
3044
|
-
const path = securityStatePath(now, home);
|
|
3045
|
-
try {
|
|
3046
|
-
if (!existsSync(path)) return {};
|
|
3047
|
-
const data = JSON.parse(readFileSync(path, "utf-8"));
|
|
3048
|
-
return typeof data === "object" && data !== null && !Array.isArray(data) ? data : {};
|
|
3049
|
-
} catch {
|
|
3050
|
-
return {};
|
|
3051
|
-
}
|
|
3052
|
-
}
|
|
3053
|
-
/** Persist today's security state (indent 2, no trailing newline). */
|
|
3054
|
-
function saveSecurityState(state, now = Date.now(), home = homedir()) {
|
|
3055
|
-
mkdirSync(securityStateDir(home), { recursive: true });
|
|
3056
|
-
writeFileSync(securityStatePath(now, home), JSON.stringify(state, null, 2), "utf-8");
|
|
3057
|
-
}
|
|
3058
|
-
//#endregion
|
|
3059
3059
|
//#region src/runtime/lifecycle/security/track-skill-read.ts
|
|
3060
3060
|
/**
|
|
3061
3061
|
* Security skill-read tracker (PostToolUse Read). Ports `track-skill-read.py`:
|
|
@@ -3903,4 +3903,4 @@ async function handleHook(id, payload, opts) {
|
|
|
3903
3903
|
});
|
|
3904
3904
|
}
|
|
3905
3905
|
//#endregion
|
|
3906
|
-
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 };
|
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,
|
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",
|