@fusengine/harness 0.1.79 → 0.1.81
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 +14 -1
- package/dist/adapters/claude/index.d.mts +1 -1
- package/dist/adapters/claude/index.mjs +1 -1
- package/dist/adapters/cline/index.mjs +1 -1
- package/dist/adapters/codex/index.d.mts +11 -12
- package/dist/adapters/codex/index.mjs +3 -3
- package/dist/adapters/cursor/index.d.mts +29 -29
- package/dist/adapters/cursor/index.mjs +1 -1
- package/dist/adapters/gemini/index.mjs +1 -1
- package/dist/adapters/hermes/index.d.mts +22 -23
- package/dist/adapters/hermes/index.mjs +1 -1
- package/dist/adapters/kimi/index.d.mts +87 -0
- package/dist/adapters/kimi/index.mjs +2 -0
- package/dist/cache/index.d.mts +1 -1
- package/dist/{claude-51RSspJ1.mjs → claude-D93HY8dq.mjs} +2 -2
- package/dist/cli/bin.d.mts +1 -1
- package/dist/cli/bin.mjs +130 -39
- package/dist/cli/index.d.mts +3 -3
- package/dist/cli/index.mjs +1 -1
- package/dist/config/index.d.mts +2 -2
- package/dist/config/index.mjs +4 -4
- package/dist/detect/index.d.mts +2 -1
- package/dist/detect/index.mjs +1 -1
- package/dist/{doc-helpers-CEKzGg2u.d.mts → doc-helpers-BGCzP9VF.d.mts} +12 -12
- package/dist/{dotenv-Jj8aL1FL.mjs → dotenv-DoqNlALG.mjs} +31 -3
- package/dist/{evaluate-ClfbCDiY.mjs → evaluate-B4-MhOy_.mjs} +322 -32
- package/dist/freshness/index.d.mts +2 -2
- package/dist/{handle-Dthtlp7Z.mjs → handle-J5r2f_O_.mjs} +665 -164
- package/dist/{harness-Cb9xR8dC.mjs → harness-BMuLJ9lm.mjs} +4 -2
- package/dist/{hermes-rTmVdhUL.mjs → hermes-1PUEoY8q.mjs} +2 -2
- package/dist/{home-state-BXf38Zi1.mjs → home-state-oUGFB4ds.mjs} +4 -2
- package/dist/index-84heAAm_.d.mts +12 -0
- package/dist/index-CaxrMD1A.d.mts +1 -0
- package/dist/index-CigBu4K9.d.mts +14 -0
- package/dist/{index-DGxNjXW7.d.mts → index-D4C2-IIc.d.mts} +6 -8
- package/dist/index-DBgnIZn9.d.mts +756 -0
- package/dist/{index-mmRF3KNp.d.mts → index-DEfz82BP.d.mts} +12 -13
- package/dist/{index-BUwEmIK-.d.mts → index-DhmV2MPh.d.mts} +31 -31
- package/dist/index-oXXPs2xl.d.mts +119 -0
- package/dist/index-p9fLrSOD.d.mts +27 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +9 -9
- package/dist/init/index.d.mts +8 -9
- package/dist/init/index.mjs +1 -1
- package/dist/kimi-ChbcU2uI.mjs +94 -0
- package/dist/{layout-C0jaaCQC.mjs → layout-KWoE_Mqn.mjs} +1 -0
- package/dist/limits-DG5pl-5s.mjs +59 -0
- package/dist/memory/index.mjs +1 -1
- package/dist/policy/index.d.mts +2 -3
- package/dist/policy/index.mjs +4 -4
- package/dist/prompt/index.d.mts +2 -1
- package/dist/refs/index.d.mts +1 -1
- package/dist/{registry-CymilZiZ.mjs → registry-IhHk2KlT.mjs} +1 -1
- package/dist/{run-3tyTW9Ud.mjs → run-D_WchZ3R.mjs} +1 -1
- package/dist/{run-Do2JltgU.mjs → run-TAXNRSpD.mjs} +1 -1
- package/dist/runtime/index.d.mts +455 -446
- package/dist/runtime/index.mjs +3 -3
- package/dist/{session-state-CY4iohn_.d.mts → session-state-D5gLr66m.d.mts} +29 -30
- package/dist/{skill-path-BO2N0XvB.mjs → skill-path-DhBGlQe3.mjs} +3 -3
- package/dist/{store-Bcn08y9m.mjs → store-yr_QFsZY.mjs} +101 -13
- package/dist/tracking/index.d.mts +22 -13
- package/dist/tracking/index.mjs +2 -2
- package/dist/{types-DVbIl9md.d.mts → types-C0-igRKr.d.mts} +8 -8
- package/dist/types-DvXTI71X.d.mts +15 -0
- package/dist/util/index.d.mts +1 -1
- package/dist/{validate-DhOX5hDK.mjs → validate-Bf-EsbDh.mjs} +348 -32
- package/package.json +163 -158
- package/dist/apex-Wdi1nq_w.d.mts +0 -73
- package/dist/harness-s1Fce_dL.d.mts +0 -27
- package/dist/index-BEMumjOw.d.mts +0 -27
- package/dist/index-BIGVNQB8.d.mts +0 -576
- package/dist/index-BKZ67WMa.d.mts +0 -1
- package/dist/index-BOBXQ91y.d.mts +0 -12
- package/dist/index-BVVgDSdq.d.mts +0 -1
- package/dist/index-jj_EzZRv.d.mts +0 -96
- package/dist/limits-CHn8AIL1.mjs +0 -31
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { s as resolveTtlSec } from "./dotenv-
|
|
3
|
-
import { r as projectLayout } from "./layout-
|
|
1
|
+
import { a as resolveMaxLines, l as parseEnvInt, s as splitTarget } from "./limits-DG5pl-5s.mjs";
|
|
2
|
+
import { s as resolveTtlSec } from "./dotenv-DoqNlALG.mjs";
|
|
3
|
+
import { r as projectLayout } from "./layout-KWoE_Mqn.mjs";
|
|
4
4
|
import { i as walkUpFor, n as projectRoot, r as projectRootOrNull, t as isCodeFile } from "./project-root-3kk7gCOp.mjs";
|
|
5
|
-
import {
|
|
6
|
-
import { a as sanitizeSessionId, c as sessionsDir, d as countFrameworkCodeLines, f as countLines, i as loadSessionState, l as PLUGINS_DIR, n as fuseHarnessHome, o as saveSessionState, r as fusengineCache, t as claudeHome } from "./home-state-
|
|
5
|
+
import { D as isVendorPath, G as isFileSizeScoped, K as resolveSolidRefFramework, W as computeEditResultLines, b as interfaceDeclLevel, et as detectFramework, g as conventionsMode, lt as requiredArchSkill, n as FAIL_CLOSED, ot as detectModularArchitecture, rt as maskCommentsAndStrings, st as detectProjectType$1, t as evaluate } from "./evaluate-B4-MhOy_.mjs";
|
|
6
|
+
import { a as sanitizeSessionId, c as sessionsDir, d as countFrameworkCodeLines, f as countLines, i as loadSessionState, l as PLUGINS_DIR, n as fuseHarnessHome, o as saveSessionState, p as evaluateFileSize, r as fusengineCache, t as claudeHome } from "./home-state-oUGFB4ds.mjs";
|
|
7
7
|
import { a as writeJsonFile, i as readJsonFile, r as hashText, t as atomicWrite } from "./json-io-DisYd2fb.mjs";
|
|
8
8
|
import { r as isDocConsulted } from "./doc-helpers-CWZegVdR.mjs";
|
|
9
|
-
import {
|
|
10
|
-
import { n as findMarketplacePlugins, r as readPluginMeta, t as resolveSkillPath } from "./skill-path-
|
|
9
|
+
import { D as frameworkSolidGate, F as isExcludedSwiftPath, G as evaluateApex, H as PRE_AUTH_GATES, P as isExcludedJsPath, R as capVerbosity, S as parseField, T as usesTailwindUtilities, V as POST_AUTH_GATES, W as docConsultedGate, b as scanPlugin, c as EXCLUDE_DIRS$1, d as buildApexTaskInjection, g as buildClaudeMdContext, l as PROJECT_INDICATORS, n as missingSeoElements, o as parseEnrichment, p as apexAgentSteps, r as descFromText, s as parseEntry, t as isHtmlLike, v as apexDocName, w as skillTriggerGate, y as harnessHomeSegment, z as detectCreationIntent } from "./validate-Bf-EsbDh.mjs";
|
|
10
|
+
import { n as findMarketplacePlugins, r as readPluginMeta, t as resolveSkillPath } from "./skill-path-DhBGlQe3.mjs";
|
|
11
11
|
import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
|
|
12
|
-
import { a as nowStamp, l as throttleMs, n as readRoots, o as readState, s as setStateField, t as addRoot } from "./registry-
|
|
12
|
+
import { a as nowStamp, l as throttleMs, n as readRoots, o as readState, s as setStateField, t as addRoot } from "./registry-IhHk2KlT.mjs";
|
|
13
13
|
import { d as loadIndex, i as cacheLookupMeta, n as webfetchCacheWrite, o as cacheLookupSubstringMeta, t as mcpCacheWrite, u as extractText } from "./mcp-store-BkBDmuxN.mjs";
|
|
14
14
|
import { t as loadRefs } from "./loader-AGz4nK7d.mjs";
|
|
15
|
-
import { a as
|
|
15
|
+
import { _ as trivialCount, a as LOCK_TTL_MS, c as writeLastNonce, d as recordAgent, f as recordBrainstormRequired, g as recordTrivialEdit, h as recordTarget, i as LOCK_FAILED, l as agentsFresh, m as recordRefRead, o as signTrack, p as recordDoc, r as withTrack, s as verifyTrack, t as loadTrack, u as emptyTrack, v as apexAuthorizationGate } from "./store-yr_QFsZY.mjs";
|
|
16
16
|
import { n as parseApplyPatch, t as isBypassPermissions } from "./permission-mode-BN3MNgbm.mjs";
|
|
17
|
-
import { _ as
|
|
18
|
-
import {
|
|
17
|
+
import { _ as commandToString, d as collectFiles, f as pathExists, g as writeText, h as spawnCapture, i as denyResponse, l as systemMessage, m as sleep, n as blockResponse, p as readText$1, r as contextResponse, s as informResponse, t as attachSystemMessage } from "./claude-D93HY8dq.mjs";
|
|
18
|
+
import { i as toKimiResponse, n as kimiDenyResponse } from "./kimi-ChbcU2uI.mjs";
|
|
19
|
+
import { r as toHermesResponse } from "./hermes-1PUEoY8q.mjs";
|
|
19
20
|
import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
20
|
-
import { appendFileSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, renameSync, rmSync, rmdirSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { appendFileSync, closeSync, copyFileSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, readlinkSync, renameSync, rmSync, rmdirSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
21
22
|
import { homedir } from "node:os";
|
|
22
23
|
import { mkdir, rmdir } from "node:fs/promises";
|
|
23
24
|
import { createHash } from "node:crypto";
|
|
@@ -350,6 +351,34 @@ function designLifecycle(payload, cacheDir, cwd, stamp, now) {
|
|
|
350
351
|
return false;
|
|
351
352
|
}
|
|
352
353
|
//#endregion
|
|
354
|
+
//#region src/runtime/inform.ts
|
|
355
|
+
/**
|
|
356
|
+
* Lifecycle context-injection renderer. Every emitter that injects text into
|
|
357
|
+
* the model's context (rules, lessons, CLAUDE.md preamble, ...) goes through
|
|
358
|
+
* here instead of calling the Claude builders directly:
|
|
359
|
+
* - kimi: raw text on stdout — Kimi Code CLI appends stdout to the context on
|
|
360
|
+
* exit 0 (UserPromptSubmit is blockable; the Claude-shaped
|
|
361
|
+
* `additionalContext` envelope would be ignored or leak as raw JSON text).
|
|
362
|
+
* - every other harness: the historical Claude-shaped envelope, byte-identical
|
|
363
|
+
* to what the call sites produced before this module existed (zero-regression
|
|
364
|
+
* default — `attachSystemMessage` concatenates notices with "\n", so a single
|
|
365
|
+
* combined notice equals the previous chained calls).
|
|
366
|
+
*/
|
|
367
|
+
/**
|
|
368
|
+
* Render a context injection for the firing harness.
|
|
369
|
+
* @param id - Harness id.
|
|
370
|
+
* @param event - The firing hook event name (envelope tag for Claude shapes).
|
|
371
|
+
* @param text - The context body to inject.
|
|
372
|
+
* @param notice - Optional human-facing notice (systemMessage on Claude
|
|
373
|
+
* shapes; appended as plain text for kimi).
|
|
374
|
+
* @returns The native hook stdout.
|
|
375
|
+
*/
|
|
376
|
+
function renderInform(id, event, text, notice) {
|
|
377
|
+
if (id === "kimi") return notice ? `${text}\n\n${notice}` : text;
|
|
378
|
+
const base = contextResponse(event, text);
|
|
379
|
+
return notice ? attachSystemMessage(base, notice) : base;
|
|
380
|
+
}
|
|
381
|
+
//#endregion
|
|
353
382
|
//#region src/runtime/inject-dedup-exclusive.ts
|
|
354
383
|
/**
|
|
355
384
|
* @module inject-dedup-exclusive
|
|
@@ -598,7 +627,7 @@ function promptSubmitContext(prompt, cwd, id = "claude-code") {
|
|
|
598
627
|
const ctx = buildClaudeMdContext(prompt, cwd, id);
|
|
599
628
|
if (!ctx) return "";
|
|
600
629
|
if (!oncePerWindow(claudeMdKey(prompt, ctx), 3e3)) return "";
|
|
601
|
-
return
|
|
630
|
+
return renderInform(id, "UserPromptSubmit", ctx, `${apexDocName(id)} injected`);
|
|
602
631
|
}
|
|
603
632
|
/**
|
|
604
633
|
* PreToolUse Task context injection: render the APEX sub-agent context as a
|
|
@@ -738,9 +767,15 @@ const PURGEABLE = {
|
|
|
738
767
|
doc: 48 * 3600,
|
|
739
768
|
explore: 48 * 3600
|
|
740
769
|
};
|
|
741
|
-
/**
|
|
742
|
-
|
|
743
|
-
|
|
770
|
+
/**
|
|
771
|
+
* Read the target's root instructions doc (`~/.claude/CLAUDE.md`,
|
|
772
|
+
* `~/.codex/AGENTS.md`, `~/.kimi-code/AGENTS.md`, ...), or "" when
|
|
773
|
+
* missing/unreadable.
|
|
774
|
+
* @param home - Home dir.
|
|
775
|
+
* @param id - Harness target id (defaults to "claude-code" — zero-regression default).
|
|
776
|
+
*/
|
|
777
|
+
function claudeMd(home, id = "claude-code") {
|
|
778
|
+
const path = join(home, harnessHomeSegment(id), apexDocName(id));
|
|
744
779
|
try {
|
|
745
780
|
return existsSync(path) ? readFileSync(path, "utf-8") : "";
|
|
746
781
|
} catch {
|
|
@@ -765,14 +800,15 @@ function runSessionStartCleanups(home = homedir(), now = Date.now()) {
|
|
|
765
800
|
* @param cwd - Project root for dev-context detection.
|
|
766
801
|
* @param home - Home dir (defaults to `~`).
|
|
767
802
|
* @param now - Clock for TTL cleanup (defaults to `Date.now()`).
|
|
803
|
+
* @param id - Harness target id (defaults to "claude-code" — zero-regression default).
|
|
768
804
|
* @returns The native hook stdout (possibly empty).
|
|
769
805
|
*/
|
|
770
|
-
function sessionStartCore(cwd, home = homedir(), now = Date.now()) {
|
|
771
|
-
const md = claudeMd(home);
|
|
806
|
+
function sessionStartCore(cwd, home = homedir(), now = Date.now(), id = "claude-code") {
|
|
807
|
+
const md = claudeMd(home, id);
|
|
772
808
|
const dev = devContext(cwd);
|
|
773
809
|
runSessionStartCleanups(home, now);
|
|
774
810
|
const ctx = [md, dev].filter(Boolean).join("\n");
|
|
775
|
-
return ctx ? attachSystemMessage(contextResponse("SessionStart", ctx),
|
|
811
|
+
return ctx ? attachSystemMessage(contextResponse("SessionStart", ctx), `${apexDocName(id)} injected`) : "";
|
|
776
812
|
}
|
|
777
813
|
//#endregion
|
|
778
814
|
//#region src/runtime/lifecycle/inject-rules.ts
|
|
@@ -793,17 +829,19 @@ function readRules(rulesDir) {
|
|
|
793
829
|
}
|
|
794
830
|
/**
|
|
795
831
|
* Build the rules injection for claude-rules (SessionStart, UserPromptSubmit,
|
|
796
|
-
* SubagentStart): read `<pluginRoot>/rules/*.md` and emit
|
|
797
|
-
* or
|
|
798
|
-
* spec requires it to match the firing
|
|
799
|
-
* non-conforming and may be dropped on
|
|
832
|
+
* SubagentStart): read `<pluginRoot>/rules/*.md` and emit via {@link renderInform}
|
|
833
|
+
* (Claude-shaped `additionalContext`, or raw text for kimi). Tags the output
|
|
834
|
+
* with the *actual* `hookEventName` — the spec requires it to match the firing
|
|
835
|
+
* event (a hardcoded "SessionStart" is non-conforming and may be dropped on
|
|
836
|
+
* UserPromptSubmit/SubagentStart).
|
|
800
837
|
* @param pluginRoot - `CLAUDE_PLUGIN_ROOT` of the claude-rules plugin.
|
|
801
838
|
* @param event - The firing hook event name (e.g. "SessionStart").
|
|
839
|
+
* @param id - Harness target id (defaults to "claude-code" — zero-regression default).
|
|
802
840
|
* @returns The native hook stdout (possibly empty).
|
|
803
841
|
*/
|
|
804
|
-
function injectRules(pluginRoot, event) {
|
|
842
|
+
function injectRules(pluginRoot, event, id = "claude-code") {
|
|
805
843
|
const content = readRules(join(pluginRoot, "rules"));
|
|
806
|
-
return content ?
|
|
844
|
+
return content ? renderInform(id, event, content, "rules 00-08 injected") : "";
|
|
807
845
|
}
|
|
808
846
|
//#endregion
|
|
809
847
|
//#region src/runtime/lifecycle/solid-detect.ts
|
|
@@ -952,7 +990,7 @@ function checkFileSize(tool, filePath, env = process.env) {
|
|
|
952
990
|
if (!filePath || !pathExists(filePath)) return "";
|
|
953
991
|
let content;
|
|
954
992
|
try {
|
|
955
|
-
content = readText(filePath);
|
|
993
|
+
content = readText$1(filePath);
|
|
956
994
|
} catch {
|
|
957
995
|
return "";
|
|
958
996
|
}
|
|
@@ -974,20 +1012,32 @@ function checkFileSize(tool, filePath, env = process.env) {
|
|
|
974
1012
|
* gated behind `SOLID_PROJECT_TYPE`. Inert when `SOLID_PROJECT_TYPE` is
|
|
975
1013
|
* absent/"unknown"/nextjs/laravel/swift/rust (parity: the Python hook only
|
|
976
1014
|
* has a `go`/`python` validator).
|
|
1015
|
+
*
|
|
1016
|
+
* Detection rides the conventions module (MASKED content, column-0 anchored
|
|
1017
|
+
* for Go): the old naive regexes matched inside comments/strings and
|
|
1018
|
+
* indented locals — all killed. LEGACY-level detections keep the exact hard
|
|
1019
|
+
* deny (byte-parity); EXTENDED-level ones (Go `interface{` without the
|
|
1020
|
+
* space, Python `Protocol`) follow `FUSE_CONVENTIONS_MODE` — advisory
|
|
1021
|
+
* (non-blocking additionalContext) by default, deny only on `"deny"`
|
|
1022
|
+
* (owner Amendment 5, external audit D0.2).
|
|
977
1023
|
*/
|
|
978
|
-
/** Top-level Go interface declaration, e.g. `type Foo interface {`. */
|
|
979
|
-
const GO_INTERFACE_RE = /^type.*interface \{/m;
|
|
980
|
-
/** Python class whose header mentions `ABC` (naive, parity with the Python regex). */
|
|
981
|
-
const PY_ABC_RE = /class.*ABC/;
|
|
982
1024
|
/** Go: deny a top-level interface declared outside `/interfaces/`. */
|
|
983
|
-
function checkGo(filePath, content) {
|
|
1025
|
+
function checkGo(filePath, content, env) {
|
|
984
1026
|
if (!filePath.endsWith(".go") || filePath.includes("/interfaces/")) return null;
|
|
985
|
-
|
|
1027
|
+
const level = interfaceDeclLevel(filePath, content);
|
|
1028
|
+
if (level === null) return null;
|
|
1029
|
+
const msg = "SOLID: Interfaces must be in internal/interfaces/";
|
|
1030
|
+
if (level === "legacy" || conventionsMode(env) === "deny") return denyResponse("PreToolUse", msg);
|
|
1031
|
+
return contextResponse("PreToolUse", msg);
|
|
986
1032
|
}
|
|
987
1033
|
/** Python: deny an ABC subclass declared outside `/interfaces/`. */
|
|
988
|
-
function checkPython(filePath, content) {
|
|
1034
|
+
function checkPython(filePath, content, env) {
|
|
989
1035
|
if (!filePath.endsWith(".py") || filePath.includes("/interfaces/")) return null;
|
|
990
|
-
|
|
1036
|
+
const level = interfaceDeclLevel(filePath, content);
|
|
1037
|
+
if (level === null) return null;
|
|
1038
|
+
const msg = "SOLID: Abstract classes must be in src/interfaces/";
|
|
1039
|
+
if (level === "legacy" || conventionsMode(env) === "deny") return denyResponse("PreToolUse", msg);
|
|
1040
|
+
return contextResponse("PreToolUse", msg);
|
|
991
1041
|
}
|
|
992
1042
|
/** `SOLID_PROJECT_TYPE` values this gate validates (parity subset — see module doc). */
|
|
993
1043
|
const VALIDATORS = {
|
|
@@ -995,20 +1045,19 @@ const VALIDATORS = {
|
|
|
995
1045
|
python: checkPython
|
|
996
1046
|
};
|
|
997
1047
|
/**
|
|
998
|
-
* Deny a Write/Edit that violates the Go/Python interface-location SOLID rule.
|
|
1048
|
+
* Deny (or advise on) a Write/Edit that violates the Go/Python interface-location SOLID rule.
|
|
999
1049
|
* @param tool - The tool name (only "Write"/"Edit" are checked).
|
|
1000
1050
|
* @param filePath - The written file's absolute path.
|
|
1001
1051
|
* @param content - The written/edited content (`new_string` snippet on Edit).
|
|
1002
1052
|
* @param env - Environment (defaults to `process.env`).
|
|
1003
|
-
* @returns The PreToolUse
|
|
1053
|
+
* @returns The PreToolUse response, or `""` when clean/inert.
|
|
1004
1054
|
*/
|
|
1005
1055
|
function validateSolidGate(tool, filePath, content, env = process.env) {
|
|
1006
1056
|
const ptype = env.SOLID_PROJECT_TYPE ?? "";
|
|
1007
1057
|
if (!ptype || ptype === "unknown") return "";
|
|
1008
1058
|
if (tool !== "Write" && tool !== "Edit") return "";
|
|
1009
1059
|
if (!filePath) return "";
|
|
1010
|
-
|
|
1011
|
-
return violation ? denyResponse("PreToolUse", violation) : "";
|
|
1060
|
+
return VALIDATORS[ptype]?.(filePath, content, env) ?? "";
|
|
1012
1061
|
}
|
|
1013
1062
|
//#endregion
|
|
1014
1063
|
//#region src/runtime/lifecycle/validate-tailwind.ts
|
|
@@ -1044,7 +1093,7 @@ function isTailwindProject(filePath) {
|
|
|
1044
1093
|
const pkgPath = join(root, "package.json");
|
|
1045
1094
|
if (!pathExists(pkgPath)) return false;
|
|
1046
1095
|
try {
|
|
1047
|
-
const pkg = JSON.parse(readText(pkgPath));
|
|
1096
|
+
const pkg = JSON.parse(readText$1(pkgPath));
|
|
1048
1097
|
return Boolean(pkg.dependencies?.tailwindcss) || Boolean(pkg.devDependencies?.tailwindcss);
|
|
1049
1098
|
} catch {
|
|
1050
1099
|
return false;
|
|
@@ -1062,7 +1111,7 @@ function validateTailwind(tool, filePath) {
|
|
|
1062
1111
|
if (!pathExists(filePath) || !isTailwindProject(filePath)) return "";
|
|
1063
1112
|
let content;
|
|
1064
1113
|
try {
|
|
1065
|
-
content = readText(filePath);
|
|
1114
|
+
content = readText$1(filePath);
|
|
1066
1115
|
} catch {
|
|
1067
1116
|
return "";
|
|
1068
1117
|
}
|
|
@@ -1219,7 +1268,7 @@ function freshReceiptFromFile(file, windowMs, now) {
|
|
|
1219
1268
|
async function captureReceipt(file, command, output, exitCode, now) {
|
|
1220
1269
|
const receipt = classifyReceipt(command, output, exitCode, now);
|
|
1221
1270
|
if (!receipt) return;
|
|
1222
|
-
await
|
|
1271
|
+
await withTrack(file, (track) => recordReceipt(track, receipt));
|
|
1223
1272
|
}
|
|
1224
1273
|
//#endregion
|
|
1225
1274
|
//#region src/runtime/lifecycle/agent-transcript.ts
|
|
@@ -1251,7 +1300,7 @@ function readAgentToolUses(transcriptPath) {
|
|
|
1251
1300
|
if (!transcriptPath) return null;
|
|
1252
1301
|
let text;
|
|
1253
1302
|
try {
|
|
1254
|
-
text = readText(transcriptPath);
|
|
1303
|
+
text = readText$1(transcriptPath);
|
|
1255
1304
|
} catch {
|
|
1256
1305
|
return null;
|
|
1257
1306
|
}
|
|
@@ -1705,6 +1754,102 @@ function postEditTypescript(filePath) {
|
|
|
1705
1754
|
return contextResponse("PostToolUse", `Lint issues in ${basename(filePath)}: ${issues.join(" | ")}`);
|
|
1706
1755
|
}
|
|
1707
1756
|
//#endregion
|
|
1757
|
+
//#region src/util/semver.ts
|
|
1758
|
+
/**
|
|
1759
|
+
* Dependency-free semver ordering. `Array.prototype.sort()` compares strings
|
|
1760
|
+
* lexicographically — `"1.0.9" > "1.0.23"` — so versioned directories (plugin
|
|
1761
|
+
* caches) must be ordered with a numeric, segment-by-segment comparison.
|
|
1762
|
+
*/
|
|
1763
|
+
/**
|
|
1764
|
+
* Compare two dotted version strings numerically, segment by segment
|
|
1765
|
+
* (shorter version wins when a prefix is equal: `1.0` < `1.0.1`). A segment
|
|
1766
|
+
* that is not a plain integer (prerelease tag like `0-beta`) falls back to
|
|
1767
|
+
* lexicographic comparison of that segment — never NaN, so `sort()` stays
|
|
1768
|
+
* defined; identical strings compare 0.
|
|
1769
|
+
* @returns Negative when `a < b`, positive when `a > b`, 0 when equal.
|
|
1770
|
+
*/
|
|
1771
|
+
function compareSemver(a, b) {
|
|
1772
|
+
const pa = a.split("."), pb = b.split(".");
|
|
1773
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
1774
|
+
const sa = pa[i] ?? "0", sb = pb[i] ?? "0";
|
|
1775
|
+
if (sa === sb) continue;
|
|
1776
|
+
const na = Number(sa), nb = Number(sb);
|
|
1777
|
+
if (Number.isInteger(na) && Number.isInteger(nb) && na !== nb) return na - nb;
|
|
1778
|
+
return sa < sb ? -1 : 1;
|
|
1779
|
+
}
|
|
1780
|
+
return 0;
|
|
1781
|
+
}
|
|
1782
|
+
/** The highest version of the list, or null when empty. */
|
|
1783
|
+
function maxSemver(versions) {
|
|
1784
|
+
return versions.length === 0 ? null : versions.reduce((a, b) => compareSemver(a, b) >= 0 ? a : b);
|
|
1785
|
+
}
|
|
1786
|
+
//#endregion
|
|
1787
|
+
//#region src/runtime/lifecycle/rules-root.ts
|
|
1788
|
+
/**
|
|
1789
|
+
* Dynamic rules-plugin root resolution. The historical `CLAUDE_PLUGIN_ROOT ??
|
|
1790
|
+
* cwd` chain only worked when the harness exported the plugin root — Kimi
|
|
1791
|
+
* injects `KIMI_PLUGIN_ROOT` instead, and a bare cwd fallback never held a
|
|
1792
|
+
* `rules/` dir. Resolution order (first hit wins):
|
|
1793
|
+
* 1. `CLAUDE_PLUGIN_ROOT` (claude-code/codex plugin-declared hooks);
|
|
1794
|
+
* 2. `KIMI_PLUGIN_ROOT` (kimi plugin-declared hooks);
|
|
1795
|
+
* 3. Per-harness install probe (claude marketplace, codex versioned cache,
|
|
1796
|
+
* kimi managed plugins) — first `<plugin>/rules` dir whose plugin folder
|
|
1797
|
+
* name contains "rules";
|
|
1798
|
+
* 4. `cwd` (historical fallback, unchanged behavior).
|
|
1799
|
+
*/
|
|
1800
|
+
/** Immediate child dir names of `dir`, or [] when unreadable. */
|
|
1801
|
+
function children$1(dir) {
|
|
1802
|
+
try {
|
|
1803
|
+
return readdirSync(dir);
|
|
1804
|
+
} catch {
|
|
1805
|
+
return [];
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
/** First existing `<base>/<child>/rules` where `child` names a rules plugin. */
|
|
1809
|
+
function probe(base, childFilter) {
|
|
1810
|
+
for (const child of children$1(base).sort()) {
|
|
1811
|
+
if (!childFilter(child)) continue;
|
|
1812
|
+
if (existsSync(join(base, child, "rules"))) return join(base, child);
|
|
1813
|
+
}
|
|
1814
|
+
return null;
|
|
1815
|
+
}
|
|
1816
|
+
/** Claude: `~/.claude/plugins/marketplaces/<marketplace>/plugins/<rules-plugin>/`. */
|
|
1817
|
+
function probeClaude(home) {
|
|
1818
|
+
const markets = join(home, ".claude", "plugins", "marketplaces");
|
|
1819
|
+
for (const m of children$1(markets).sort()) {
|
|
1820
|
+
const hit = probe(join(markets, m, "plugins"), (n) => n.includes("rules"));
|
|
1821
|
+
if (hit) return hit;
|
|
1822
|
+
}
|
|
1823
|
+
return null;
|
|
1824
|
+
}
|
|
1825
|
+
/** Codex: `~/.codex/plugins/cache/<marketplace>/<rules-plugin>/<version>/` (highest semver). */
|
|
1826
|
+
function probeCodex(home) {
|
|
1827
|
+
const cache = join(home, ".codex", "plugins", "cache");
|
|
1828
|
+
for (const m of children$1(cache).sort()) for (const p of children$1(join(cache, m)).sort()) {
|
|
1829
|
+
if (!p.includes("rules")) continue;
|
|
1830
|
+
const latest = maxSemver(children$1(join(cache, m, p)).filter((v) => existsSync(join(cache, m, p, v, "rules"))));
|
|
1831
|
+
if (latest) return join(cache, m, p, latest);
|
|
1832
|
+
}
|
|
1833
|
+
return null;
|
|
1834
|
+
}
|
|
1835
|
+
/** Kimi: `<KIMI_CODE_HOME>/plugins/managed/<rules-plugin>/` (managed install copy). */
|
|
1836
|
+
function probeKimi(env, home) {
|
|
1837
|
+
return probe(join(env.KIMI_CODE_HOME ?? join(home, ".kimi-code"), "plugins", "managed"), (n) => n.includes("rules"));
|
|
1838
|
+
}
|
|
1839
|
+
/**
|
|
1840
|
+
* Resolve the directory whose `rules/` subtree holds the `*.md` rule files.
|
|
1841
|
+
* @param id - Harness id (selects the install-layout probe).
|
|
1842
|
+
* @param cwd - Historical fallback root.
|
|
1843
|
+
* @param env - Environment (defaults to `process.env`).
|
|
1844
|
+
* @returns The plugin root to read `rules/` from.
|
|
1845
|
+
*/
|
|
1846
|
+
function resolveRulesRoot(id, cwd, env = process.env) {
|
|
1847
|
+
if (env.CLAUDE_PLUGIN_ROOT) return env.CLAUDE_PLUGIN_ROOT;
|
|
1848
|
+
if (env.KIMI_PLUGIN_ROOT) return env.KIMI_PLUGIN_ROOT;
|
|
1849
|
+
const home = env.HOME ?? homedir();
|
|
1850
|
+
return (id === "codex" ? probeCodex(home) : id === "kimi" ? probeKimi(env, home) : id === "claude-code" ? probeClaude(home) : null) ?? cwd;
|
|
1851
|
+
}
|
|
1852
|
+
//#endregion
|
|
1708
1853
|
//#region src/freshness/explore-tools.ts
|
|
1709
1854
|
/** Native exploration tools (parity `apex_constants.EXPLORE_TOOLS`). */
|
|
1710
1855
|
const EXPLORE_TOOLS = /* @__PURE__ */ new Set(["Glob", "Grep"]);
|
|
@@ -1724,6 +1869,7 @@ const RESEARCH_TOOLS = /* @__PURE__ */ new Set([
|
|
|
1724
1869
|
"mcp__exa__deep_researcher_start",
|
|
1725
1870
|
"WebSearch",
|
|
1726
1871
|
"WebFetch",
|
|
1872
|
+
"FetchURL",
|
|
1727
1873
|
"mcp__fuse-browser__browser_fetch",
|
|
1728
1874
|
"mcp__fuse-browser__browser_fetch_batch",
|
|
1729
1875
|
"mcp__fuse-browser__browser_crawl",
|
|
@@ -1800,6 +1946,29 @@ function classifyExplore(tool, input) {
|
|
|
1800
1946
|
return null;
|
|
1801
1947
|
}
|
|
1802
1948
|
//#endregion
|
|
1949
|
+
//#region src/runtime/is-agent-tool.ts
|
|
1950
|
+
/**
|
|
1951
|
+
* Sub-agent dispatch tool detection — the single source of truth for "this
|
|
1952
|
+
* tool call spawns a sub-agent". Claude Code and Codex dispatch via `Task`;
|
|
1953
|
+
* Kimi Code CLI dispatches via `Agent` (and `AgentSwarm` for parallel fan-out).
|
|
1954
|
+
* Centralized so the APEX task-context injection, the activity tracking and
|
|
1955
|
+
* the PostToolUse evidence credit stop drifting on inline `"Task" | "Agent"`
|
|
1956
|
+
* unions: every consumer imports from HERE, never re-declares the list.
|
|
1957
|
+
*/
|
|
1958
|
+
/** Canonical set of sub-agent dispatch tool names (the ONLY definition). */
|
|
1959
|
+
const AGENT_TOOLS = /* @__PURE__ */ new Set([
|
|
1960
|
+
"Task",
|
|
1961
|
+
"Agent",
|
|
1962
|
+
"AgentSwarm"
|
|
1963
|
+
]);
|
|
1964
|
+
/**
|
|
1965
|
+
* True when the tool is a sub-agent dispatch primitive on any harness.
|
|
1966
|
+
* @param tool - The normalized tool name from the hook payload.
|
|
1967
|
+
*/
|
|
1968
|
+
function isAgentTool(tool) {
|
|
1969
|
+
return AGENT_TOOLS.has(tool);
|
|
1970
|
+
}
|
|
1971
|
+
//#endregion
|
|
1803
1972
|
//#region src/freshness/agent-evidence-record.ts
|
|
1804
1973
|
/**
|
|
1805
1974
|
* Session-track agent evidence — writer (parity track-subagent-research.py) +
|
|
@@ -1808,8 +1977,6 @@ function classifyExplore(tool, input) {
|
|
|
1808
1977
|
* session track the freshness gate scans FIRST — sidechain research and
|
|
1809
1978
|
* Workflow-spawned agents count, unlike the lead-transcript scan.
|
|
1810
1979
|
*/
|
|
1811
|
-
/** Tools the existing Task tracking already credits as agent LAUNCHES. */
|
|
1812
|
-
const AGENT_LAUNCH_TOOLS = /* @__PURE__ */ new Set(["Task", "Agent"]);
|
|
1813
1980
|
/**
|
|
1814
1981
|
* `JSON.stringify` length of the raw `tool_response` OBJECT (parity Python
|
|
1815
1982
|
* `len(str(tool_response))`); 0 when absent or unserializable (circular).
|
|
@@ -1835,7 +2002,7 @@ function responseJsonLength(toolResponse) {
|
|
|
1835
2002
|
* @returns The evidence to record, or null to skip.
|
|
1836
2003
|
*/
|
|
1837
2004
|
function classifyAgentEvidence(tool, input, toolResponse) {
|
|
1838
|
-
if (
|
|
2005
|
+
if (isAgentTool(tool)) return null;
|
|
1839
2006
|
const hit = classifyExplore(tool, input);
|
|
1840
2007
|
if (!hit) return null;
|
|
1841
2008
|
return {
|
|
@@ -1855,16 +2022,18 @@ function classifyAgentEvidence(tool, input, toolResponse) {
|
|
|
1855
2022
|
* @param agentId - Optional Claude `agent_id` — metadata tag only.
|
|
1856
2023
|
*/
|
|
1857
2024
|
async function recordAgentEvidence(file, evidence, ts, agentId) {
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
2025
|
+
await withTrack(file, (track) => {
|
|
2026
|
+
const next = recordAgent(track, evidence.name, ts, evidence.quality);
|
|
2027
|
+
const last = next.agents[next.agents.length - 1];
|
|
2028
|
+
if (agentId && last) {
|
|
2029
|
+
const tagged = {
|
|
2030
|
+
...last,
|
|
2031
|
+
agentId
|
|
2032
|
+
};
|
|
2033
|
+
next.agents[next.agents.length - 1] = tagged;
|
|
2034
|
+
}
|
|
2035
|
+
return next;
|
|
2036
|
+
});
|
|
1868
2037
|
}
|
|
1869
2038
|
/**
|
|
1870
2039
|
* Parity `_scan_agents` (apex_agent_helpers.py): reverse-scan `track.agents`,
|
|
@@ -1946,6 +2115,62 @@ function harvestAgentEvidence(transcriptPath, track, now) {
|
|
|
1946
2115
|
return uses.reduce((acc, u) => applyToolUse(acc, u, now), track);
|
|
1947
2116
|
}
|
|
1948
2117
|
//#endregion
|
|
2118
|
+
//#region src/tracking/track-lock-sync.ts
|
|
2119
|
+
/**
|
|
2120
|
+
* Synchronous twin of {@link withTrackLock} for lifecycle paths that CANNOT
|
|
2121
|
+
* float async work (the SubagentStop dispatch runs synchronously in a
|
|
2122
|
+
* short-lived hook process — an async write could be dropped before exit,
|
|
2123
|
+
* see `evidence-harvest-io.ts`). Same semantics: `wx` lockfile, bounded total
|
|
2124
|
+
* retry, named skipped write on contention, stale reclamation. The wait uses
|
|
2125
|
+
* `Atomics.wait` (allowed on the main thread in both Node and Bun).
|
|
2126
|
+
*/
|
|
2127
|
+
/** Block the thread for `ms` (bounded by the caller's total budget). */
|
|
2128
|
+
function sleepSync(ms) {
|
|
2129
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
2130
|
+
}
|
|
2131
|
+
/** True when the lockfile is older than the TTL (orphaned by a dead process). */
|
|
2132
|
+
function isStale(lock) {
|
|
2133
|
+
try {
|
|
2134
|
+
return Date.now() - statSync(lock).mtimeMs > LOCK_TTL_MS;
|
|
2135
|
+
} catch {
|
|
2136
|
+
return false;
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
/**
|
|
2140
|
+
* Run `fn` under the track lock of `dir`, synchronously.
|
|
2141
|
+
* @param dir - The track state directory (lockfile lives inside).
|
|
2142
|
+
* @param fn - The protected read-modify-write.
|
|
2143
|
+
* @returns `fn`'s result, or {@link LOCK_FAILED} when the lock stayed busy.
|
|
2144
|
+
*/
|
|
2145
|
+
function withTrackLockSync(dir, fn) {
|
|
2146
|
+
mkdirSync(dir, { recursive: true });
|
|
2147
|
+
const lock = join(dir, "track.lock");
|
|
2148
|
+
const deadline = Date.now() + 400;
|
|
2149
|
+
for (;;) try {
|
|
2150
|
+
closeSync(openSync(lock, "wx"));
|
|
2151
|
+
break;
|
|
2152
|
+
} catch {
|
|
2153
|
+
if (isStale(lock)) {
|
|
2154
|
+
try {
|
|
2155
|
+
unlinkSync(lock);
|
|
2156
|
+
} catch {}
|
|
2157
|
+
continue;
|
|
2158
|
+
}
|
|
2159
|
+
if (Date.now() >= deadline) {
|
|
2160
|
+
process.stderr.write(`harness: track lock busy, write skipped (${lock})\n`);
|
|
2161
|
+
return LOCK_FAILED;
|
|
2162
|
+
}
|
|
2163
|
+
sleepSync(8);
|
|
2164
|
+
}
|
|
2165
|
+
try {
|
|
2166
|
+
return fn();
|
|
2167
|
+
} finally {
|
|
2168
|
+
try {
|
|
2169
|
+
unlinkSync(lock);
|
|
2170
|
+
} catch {}
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
//#endregion
|
|
1949
2174
|
//#region src/freshness/evidence-harvest-io.ts
|
|
1950
2175
|
/**
|
|
1951
2176
|
* SubagentStop wiring for {@link harvestAgentEvidence}. The core SubagentStop
|
|
@@ -1979,14 +2204,16 @@ function harvestSubagentTrack(payload, cwd, now, baseDir = defaultStateDir(cwd))
|
|
|
1979
2204
|
const transcriptPath = typeof payload.agent_transcript_path === "string" ? payload.agent_transcript_path : void 0;
|
|
1980
2205
|
if (!transcriptPath) return;
|
|
1981
2206
|
const file = trackFile(typeof payload.session_id === "string" ? payload.session_id : "unknown", baseDir);
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
2207
|
+
if (withTrackLockSync(dirname(file), () => {
|
|
2208
|
+
const track = loadTrackSync(file);
|
|
2209
|
+
const next = harvestAgentEvidence(transcriptPath, track, now);
|
|
2210
|
+
if (next === track) return;
|
|
2211
|
+
try {
|
|
2212
|
+
const envelope = signTrack(next);
|
|
2213
|
+
atomicWrite(file, JSON.stringify(envelope, null, 2));
|
|
2214
|
+
writeLastNonce(envelope.nonce);
|
|
2215
|
+
} catch {}
|
|
2216
|
+
}) === LOCK_FAILED) return;
|
|
1990
2217
|
}
|
|
1991
2218
|
//#endregion
|
|
1992
2219
|
//#region src/cli/doctor.ts
|
|
@@ -2540,8 +2767,8 @@ function formatSummary(s) {
|
|
|
2540
2767
|
* The proprietary rule "NEVER propose the same fix twice" is prose a model under
|
|
2541
2768
|
* pressure ignores. This makes it machine-enforced: when a call whose
|
|
2542
2769
|
* `(tool + normalized input)` hash was ALREADY denied in-window is retried, the
|
|
2543
|
-
* harness keeps the deny but rewrites the message — `[REPEAT]` title,
|
|
2544
|
-
*
|
|
2770
|
+
* harness keeps the deny but rewrites the message — `[REPEAT]` title, a
|
|
2771
|
+
* mid-sentence STOP warning, forced `research-expert` action. State + wiring live in the sidecar
|
|
2545
2772
|
* store ({@link module:deny-loop-store}); this file is IO-free and pure.
|
|
2546
2773
|
* @packageDocumentation
|
|
2547
2774
|
*/
|
|
@@ -2606,7 +2833,7 @@ function denyLoopCheck(hash, priorDenies, opts) {
|
|
|
2606
2833
|
* @returns A block prompt with `[REPEAT]` title, STOP-prefixed reason, forced research action.
|
|
2607
2834
|
*/
|
|
2608
2835
|
function enrichRepeatDeny(prompt, count) {
|
|
2609
|
-
const stop = `Identical attempt #${count} already denied for the same reason.
|
|
2836
|
+
const stop = `Identical attempt #${count} already denied for the same reason. Do not retry this same call UNCHANGED — if you have since completed the required reads/actions, retry now; otherwise STOP and pick a DIFFERENT approach. `;
|
|
2610
2837
|
const action = "Launch fuse-ai-pilot:research-expert to find a DIFFERENT approach";
|
|
2611
2838
|
return {
|
|
2612
2839
|
...prompt,
|
|
@@ -3984,7 +4211,7 @@ function persistCuration(file, root, curated, archive, original) {
|
|
|
3984
4211
|
* was this exact block silently reaching ~44k tokens. Any curation report
|
|
3985
4212
|
* surfaces via systemMessage.
|
|
3986
4213
|
*/
|
|
3987
|
-
function injectMemory(cwd, event, now) {
|
|
4214
|
+
function injectMemory(cwd, event, now, id) {
|
|
3988
4215
|
const root = projectRoot(cwd);
|
|
3989
4216
|
const file = lessonsFileFor(root);
|
|
3990
4217
|
if (!existsSync(file)) return "";
|
|
@@ -3997,8 +4224,7 @@ function injectMemory(cwd, event, now) {
|
|
|
3997
4224
|
if (!content) return "";
|
|
3998
4225
|
const { content: curated, archive, report } = curateLessons(content, now, root);
|
|
3999
4226
|
if (curated !== content) content = persistCuration(file, root, curated, archive, content);
|
|
4000
|
-
|
|
4001
|
-
return report ? attachSystemMessage(withNotice, `LESSON.md curation:\n${report}`) : withNotice;
|
|
4227
|
+
return renderInform(id, event, `Project lessons — never reproduce these:\n${capFragment("lessons", compressInjection(content))}\nYou may append OR refine/merge/dedupe bullets in MEMORY/LESSON.md — keep it terse.`, report ? `lessons injected\nLESSON.md curation:\n${report}` : "lessons injected");
|
|
4002
4228
|
}
|
|
4003
4229
|
/**
|
|
4004
4230
|
* Route a fuse-lessons event to its handler. Returns the native stdout for
|
|
@@ -4008,13 +4234,14 @@ function injectMemory(cwd, event, now) {
|
|
|
4008
4234
|
* @param payload - The raw hook payload.
|
|
4009
4235
|
* @param cwd - Project root for memory injection.
|
|
4010
4236
|
* @param now - Clock.
|
|
4237
|
+
* @param id - Harness target id (defaults to "claude-code" — zero-regression default).
|
|
4011
4238
|
* @returns The native stdout (possibly empty).
|
|
4012
4239
|
*/
|
|
4013
|
-
function dispatchLessons(event, payload, cwd, now) {
|
|
4240
|
+
function dispatchLessons(event, payload, cwd, now, id = "claude-code") {
|
|
4014
4241
|
switch (event) {
|
|
4015
4242
|
case "SessionStart":
|
|
4016
4243
|
case "SubagentStart":
|
|
4017
|
-
case "UserPromptSubmit": return injectMemory(cwd, event, now);
|
|
4244
|
+
case "UserPromptSubmit": return injectMemory(cwd, event, now, id);
|
|
4018
4245
|
case "Stop": return remindWrite(payload, now);
|
|
4019
4246
|
case "PostToolUse":
|
|
4020
4247
|
markWrite(payload, now);
|
|
@@ -4091,13 +4318,11 @@ async function injectApexSubagentContext(cwd, home = homedir(), id = "claude-cod
|
|
|
4091
4318
|
const apexDir = join(projectRoot, seg, "apex");
|
|
4092
4319
|
if (!existsSync(apexDir)) return "";
|
|
4093
4320
|
const agentsPath = join(apexDir, "AGENTS.md");
|
|
4094
|
-
const agents = existsSync(agentsPath) ? readText(agentsPath).slice(0, 4e3) : "";
|
|
4321
|
+
const agents = existsSync(agentsPath) ? readText$1(agentsPath).slice(0, 4e3) : "";
|
|
4095
4322
|
const taskData = await readJsonFile(join(apexDir, "task.json"));
|
|
4096
4323
|
const completed = taskData ? completedTasks(taskData.tasks) : "none";
|
|
4097
4324
|
const pending = taskData ? pendingTasks(taskData.tasks) : "none";
|
|
4098
|
-
const
|
|
4099
|
-
const beforeStart = isCodex ? "Use update_plan → mark the active step in_progress before starting" : "Use TaskUpdate(taskId, status: in_progress) before starting";
|
|
4100
|
-
const whenDone = isCodex ? "update_plan → mark the step completed when done" : "TaskUpdate(taskId, status: completed) triggers auto-commit";
|
|
4325
|
+
const { beforeStart, whenDone } = apexAgentSteps(id);
|
|
4101
4326
|
return contextResponse("SubagentStart", capFragment("apex-subagent", `## APEX Sub-Agent Instructions
|
|
4102
4327
|
|
|
4103
4328
|
You are a sub-agent in APEX workflow. Follow these rules:
|
|
@@ -4148,7 +4373,7 @@ function cacheAge(ts, now = Date.now()) {
|
|
|
4148
4373
|
/** Full SHA-256 hex checksum of a file's text; "" when unreadable. */
|
|
4149
4374
|
async function fileChecksum(path) {
|
|
4150
4375
|
try {
|
|
4151
|
-
return createHash("sha256").update(readText(path)).digest("hex");
|
|
4376
|
+
return createHash("sha256").update(readText$1(path)).digest("hex");
|
|
4152
4377
|
} catch {
|
|
4153
4378
|
return "";
|
|
4154
4379
|
}
|
|
@@ -4218,7 +4443,7 @@ async function cacheAnalyticsSave(home = homedir(), now = Date.now()) {
|
|
|
4218
4443
|
const dir = join(cacheBaseDir(home), "analytics");
|
|
4219
4444
|
const sessionsFile = join(dir, "sessions.jsonl");
|
|
4220
4445
|
if (!pathExists(sessionsFile)) return;
|
|
4221
|
-
const raw = readText(sessionsFile);
|
|
4446
|
+
const raw = readText$1(sessionsFile);
|
|
4222
4447
|
if (!raw.trim()) return;
|
|
4223
4448
|
const entries = parseEntries(raw);
|
|
4224
4449
|
if (entries.length === 0) return;
|
|
@@ -4300,7 +4525,7 @@ async function injectExploreCache(cwd, home = homedir(), now = Date.now()) {
|
|
|
4300
4525
|
const cfgHash = await configHash(projPath);
|
|
4301
4526
|
let context = "";
|
|
4302
4527
|
const meta = await readJsonFile(metaFile);
|
|
4303
|
-
const snapshot = pathExists(snapFile) ? readText(snapFile) : "";
|
|
4528
|
+
const snapshot = pathExists(snapFile) ? readText$1(snapFile) : "";
|
|
4304
4529
|
if (meta?.timestamp && snapshot) {
|
|
4305
4530
|
const age = cacheAge(meta.timestamp, now);
|
|
4306
4531
|
if (age < TTL_SECONDS$1 && meta.config_hash === cfgHash) {
|
|
@@ -4337,7 +4562,7 @@ async function buildDocsContext(entries, docsDir, now) {
|
|
|
4337
4562
|
seen.add(entry.hash);
|
|
4338
4563
|
const docPath = join(docsDir, `${entry.hash}.md`);
|
|
4339
4564
|
if (!pathExists(docPath)) continue;
|
|
4340
|
-
const content = readText(docPath);
|
|
4565
|
+
const content = readText$1(docPath);
|
|
4341
4566
|
if (!content) continue;
|
|
4342
4567
|
ctx += `\n${content}\n`;
|
|
4343
4568
|
count++;
|
|
@@ -4630,7 +4855,7 @@ function projectRootFromPaths(filePaths) {
|
|
|
4630
4855
|
}
|
|
4631
4856
|
/** Extract absolute file paths from tool_use entries in a JSONL transcript, optionally restricted to `toolNames` (default: any tool). */
|
|
4632
4857
|
async function transcriptFilePaths(transcriptPath, toolNames) {
|
|
4633
|
-
const text = readText(transcriptPath);
|
|
4858
|
+
const text = readText$1(transcriptPath);
|
|
4634
4859
|
const paths = /* @__PURE__ */ new Set();
|
|
4635
4860
|
for (const line of text.split("\n").filter(Boolean)) try {
|
|
4636
4861
|
const content = JSON.parse(line)?.message?.content;
|
|
@@ -4646,7 +4871,7 @@ async function transcriptFilePaths(transcriptPath, toolNames) {
|
|
|
4646
4871
|
}
|
|
4647
4872
|
/** Extract deduplicated Edit tool_use entries (keyed by basename) from a transcript. */
|
|
4648
4873
|
async function transcriptEdits(transcriptPath) {
|
|
4649
|
-
const text = readText(transcriptPath);
|
|
4874
|
+
const text = readText$1(transcriptPath);
|
|
4650
4875
|
const edits = [];
|
|
4651
4876
|
for (const line of text.split("\n").filter(Boolean)) try {
|
|
4652
4877
|
const content = JSON.parse(line)?.message?.content;
|
|
@@ -4663,7 +4888,7 @@ async function transcriptEdits(transcriptPath) {
|
|
|
4663
4888
|
}
|
|
4664
4889
|
/** Extract the last assistant text report (first 500 lines) from a transcript. */
|
|
4665
4890
|
async function transcriptReport(transcriptPath) {
|
|
4666
|
-
const text = readText(transcriptPath);
|
|
4891
|
+
const text = readText$1(transcriptPath);
|
|
4667
4892
|
let lastReport = "";
|
|
4668
4893
|
for (const line of text.split("\n").filter(Boolean)) try {
|
|
4669
4894
|
const entry = JSON.parse(line);
|
|
@@ -4691,7 +4916,7 @@ const RETRY_DELAYS = [
|
|
|
4691
4916
|
];
|
|
4692
4917
|
/** Extract the longest assistant synthesis + queried library ids from a transcript. */
|
|
4693
4918
|
async function extractSynthesis(path) {
|
|
4694
|
-
const lines = readText(path).split("\n").filter(Boolean);
|
|
4919
|
+
const lines = readText$1(path).split("\n").filter(Boolean);
|
|
4695
4920
|
const libraries = [];
|
|
4696
4921
|
let synthesis = "";
|
|
4697
4922
|
for (const line of lines) try {
|
|
@@ -4899,7 +5124,7 @@ async function cacheSniperLessons(transcript, cwd, home = homedir()) {
|
|
|
4899
5124
|
*/
|
|
4900
5125
|
/** Extract linter-related command/output text from a JSONL transcript. */
|
|
4901
5126
|
async function extractLinterOutput(path) {
|
|
4902
|
-
const text = readText(path);
|
|
5127
|
+
const text = readText$1(path);
|
|
4903
5128
|
const outputs = [];
|
|
4904
5129
|
for (const line of text.split("\n").filter(Boolean)) try {
|
|
4905
5130
|
const content = JSON.parse(line)?.message?.content;
|
|
@@ -5151,8 +5376,8 @@ async function docCacheGate(payload, cwd, now = Date.now(), home = homedir()) {
|
|
|
5151
5376
|
* Ports `check-solid-compliance.py`.
|
|
5152
5377
|
*/
|
|
5153
5378
|
const CODE_EXTENSIONS$1 = /\.(ts|tsx|js|jsx|py|php|swift|go|rs|rb|java|astro)$/;
|
|
5154
|
-
const COMPONENT_DIR =
|
|
5155
|
-
const BACKEND_DIR =
|
|
5379
|
+
const COMPONENT_DIR = /\/(components|pages|views)\//;
|
|
5380
|
+
const BACKEND_DIR = /\/(Controllers|Models|Services)\//;
|
|
5156
5381
|
const TS_INTERFACE_RE = /^(export )?(interface|type) [A-Z]/m;
|
|
5157
5382
|
const PHP_INTERFACE_RE = /^interface /m;
|
|
5158
5383
|
/** Count non-empty, non-comment (`//`/`#`/`*`-prefixed) lines. Shared with {@link checkSolidFromTranscript}. */
|
|
@@ -5176,9 +5401,10 @@ function checkSolidCompliance(payload) {
|
|
|
5176
5401
|
const input = payload.tool_input ?? {};
|
|
5177
5402
|
const filePath = typeof input.file_path === "string" ? input.file_path : "";
|
|
5178
5403
|
if (!filePath || !CODE_EXTENSIONS$1.test(filePath) || !pathExists(filePath)) return "";
|
|
5404
|
+
if (isVendorPath(filePath)) return "";
|
|
5179
5405
|
let content;
|
|
5180
5406
|
try {
|
|
5181
|
-
content = readText(filePath);
|
|
5407
|
+
content = readText$1(filePath);
|
|
5182
5408
|
} catch {
|
|
5183
5409
|
return "";
|
|
5184
5410
|
}
|
|
@@ -5188,15 +5414,64 @@ function checkSolidCompliance(payload) {
|
|
|
5188
5414
|
const lc = countCodeLines(content);
|
|
5189
5415
|
if (lc > max) violations.push(`FILE SIZE: ${lc} lines (max: ${max})`);
|
|
5190
5416
|
else if (lc > split) violations.push(`FILE SIZE WARNING: ${lc} lines (split at ${split})`);
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5417
|
+
const masked = maskCommentsAndStrings(content, filePath.endsWith(".php") ? "php" : filePath.endsWith(".py") ? "py" : "c");
|
|
5418
|
+
if (COMPONENT_DIR.test(filePath) && /\.(ts|tsx|js|jsx|astro)$/.test(filePath)) {
|
|
5419
|
+
if (TS_INTERFACE_RE.test(masked)) violations.push("INTERFACE LOCATION: Move to src/interfaces/");
|
|
5420
|
+
} else if (BACKEND_DIR.test(filePath) && filePath.endsWith(".php")) {
|
|
5421
|
+
if (PHP_INTERFACE_RE.test(masked)) violations.push("INTERFACE LOCATION: Move to app/Contracts/");
|
|
5195
5422
|
}
|
|
5196
5423
|
if (violations.length === 0) return "";
|
|
5197
5424
|
return contextResponse("PostToolUse", `SOLID COMPLIANCE CHECK: ${filePath.split("/").pop() ?? filePath}\n\n${violations.join("\n")}\nINSTRUCTION: Fix violations before continuing.\nRun sniper agent for full validation.`);
|
|
5198
5425
|
}
|
|
5199
5426
|
//#endregion
|
|
5427
|
+
//#region src/runtime/lifecycle/aipilot/solid-notice.ts
|
|
5428
|
+
/**
|
|
5429
|
+
* SubagentStop SOLID-notice dedup (anti-loop): `checkSolidFromTranscript` is
|
|
5430
|
+
* stateless, so an unchanged violation set re-emitted the same
|
|
5431
|
+
* `additionalContext` on EVERY SubagentStop — re-invoking the agent in an
|
|
5432
|
+
* observed ~15-repeat loop. The dedup fingerprint is a SHA-256 hash of the
|
|
5433
|
+
* sorted `file:message` set, persisted per agent key in the canonical
|
|
5434
|
+
* `.harness/track/solid-notice.json` sidecar (`config/layout.ts`), written
|
|
5435
|
+
* under the synchronous track lock (this hook runs in an ephemeral sync
|
|
5436
|
+
* process). Semantics: identical fingerprint within the TTL → silence;
|
|
5437
|
+
* changed/absent/expired fingerprint → emit once and record (the TTL derives
|
|
5438
|
+
* from `FUSE_ENFORCE_TTL_SEC`, no new env var). On lock contention the notice
|
|
5439
|
+
* is emitted anyway (visibility beats a muted violation) but not recorded.
|
|
5440
|
+
*/
|
|
5441
|
+
/** SHA-256 (truncated) of the sorted violation set — the dedup fingerprint. */
|
|
5442
|
+
function noticeFingerprint(violations) {
|
|
5443
|
+
return createHash("sha256").update([...violations].sort().join("\n")).digest("hex").slice(0, 16);
|
|
5444
|
+
}
|
|
5445
|
+
/**
|
|
5446
|
+
* Decide whether the notice for `key` may be emitted, recording it when so.
|
|
5447
|
+
* @param noticeFile - `projectLayout(cwd).solidNoticeFile`.
|
|
5448
|
+
* @param key - Agent/transcript identity (e.g. `agent_transcript_path`).
|
|
5449
|
+
* @param hash - The current set's fingerprint ({@link noticeFingerprint}).
|
|
5450
|
+
* @param now - Clock (epoch ms).
|
|
5451
|
+
* @param ttlMs - Reminder window (derived from `FUSE_ENFORCE_TTL_SEC`).
|
|
5452
|
+
*/
|
|
5453
|
+
function shouldEmitNotice(noticeFile, key, hash, now, ttlMs) {
|
|
5454
|
+
let state = {};
|
|
5455
|
+
try {
|
|
5456
|
+
state = JSON.parse(readFileSync(noticeFile, "utf8"));
|
|
5457
|
+
} catch {}
|
|
5458
|
+
const prev = state[key];
|
|
5459
|
+
if (prev && prev.hash === hash && now - prev.at < ttlMs) return false;
|
|
5460
|
+
const next = {
|
|
5461
|
+
...state,
|
|
5462
|
+
[key]: {
|
|
5463
|
+
hash,
|
|
5464
|
+
at: now
|
|
5465
|
+
}
|
|
5466
|
+
};
|
|
5467
|
+
withTrackLockSync(dirname(noticeFile), () => {
|
|
5468
|
+
try {
|
|
5469
|
+
atomicWrite(noticeFile, JSON.stringify(next));
|
|
5470
|
+
} catch {}
|
|
5471
|
+
});
|
|
5472
|
+
return true;
|
|
5473
|
+
}
|
|
5474
|
+
//#endregion
|
|
5200
5475
|
//#region src/runtime/lifecycle/aipilot/solid-transcript.ts
|
|
5201
5476
|
/**
|
|
5202
5477
|
* SubagentStop (matcher "") for the ai-pilot scope: check SOLID file-size and
|
|
@@ -5206,19 +5481,22 @@ function checkSolidCompliance(payload) {
|
|
|
5206
5481
|
*/
|
|
5207
5482
|
const CODE_EXTENSIONS = /\.(ts|tsx|js|jsx|py|go|rs|java|php|cpp|c|rb|swift|kt|dart|vue|svelte|astro)$/;
|
|
5208
5483
|
const INTERFACE_PATTERN = /^(export )?(interface|type) [A-Z]/m;
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
];
|
|
5484
|
+
/** Segment-anchored dirs (a substring like `app/` matched `livetest-app/`). */
|
|
5485
|
+
const INTERFACE_DIRS_RE = /\/(components|pages|views|app)\//;
|
|
5486
|
+
/** Canonical homes — never a violation there (owner decision: types/ is
|
|
5487
|
+
* canonical; Contracts/ is the Laravel home taught by solid-php/laravelGate). */
|
|
5488
|
+
const INTERFACE_EXEMPT_RE = /\/(interfaces|types|Contracts|contracts)\//;
|
|
5215
5489
|
/**
|
|
5216
5490
|
* Check every Write/Edit target in a subagent transcript against the SOLID
|
|
5217
|
-
* line-count ceiling and interface-location convention.
|
|
5491
|
+
* line-count ceiling and interface-location convention. Deduped per
|
|
5492
|
+
* transcript (`solid-notice.ts`): an unchanged violation set is emitted once
|
|
5493
|
+
* per TTL window, never in a loop.
|
|
5218
5494
|
* @param transcript - Path to the agent JSONL transcript (`agent_transcript_path`).
|
|
5219
|
-
* @
|
|
5495
|
+
* @param cwd - Project root (locates the `.harness/track` dedup sidecar).
|
|
5496
|
+
* @param now - Clock (tests inject it).
|
|
5497
|
+
* @returns The SubagentStop `additionalContext` response, or `""` when clean/deduped.
|
|
5220
5498
|
*/
|
|
5221
|
-
async function checkSolidFromTranscript(transcript) {
|
|
5499
|
+
async function checkSolidFromTranscript(transcript, cwd = process.cwd(), now = Date.now()) {
|
|
5222
5500
|
if (!transcript || !pathExists(transcript)) return "";
|
|
5223
5501
|
const files = await transcriptFilePaths(transcript, ["Write", "Edit"]);
|
|
5224
5502
|
const max = resolveMaxLines();
|
|
@@ -5228,15 +5506,19 @@ async function checkSolidFromTranscript(transcript) {
|
|
|
5228
5506
|
const name = fp.split("/").pop() ?? fp;
|
|
5229
5507
|
let content;
|
|
5230
5508
|
try {
|
|
5231
|
-
content = readText(fp);
|
|
5509
|
+
content = readText$1(fp);
|
|
5232
5510
|
} catch {
|
|
5233
5511
|
continue;
|
|
5234
5512
|
}
|
|
5235
5513
|
const lc = countCodeLines(content);
|
|
5236
5514
|
if (lc > max) violations.push(`SOLID: ${name} = ${lc} lines (max ${max})`);
|
|
5237
|
-
if (
|
|
5515
|
+
if (INTERFACE_DIRS_RE.test(fp) && !INTERFACE_EXEMPT_RE.test(fp) && INTERFACE_PATTERN.test(content)) {
|
|
5516
|
+
const dest = fp.endsWith(".php") ? "app/Contracts/" : "interfaces/";
|
|
5517
|
+
violations.push(`SOLID: ${name}: move interfaces to ${dest}`);
|
|
5518
|
+
}
|
|
5238
5519
|
}
|
|
5239
5520
|
if (violations.length === 0) return "";
|
|
5521
|
+
if (!shouldEmitNotice(projectLayout(cwd).solidNoticeFile, transcript, noticeFingerprint(violations), now, resolveTtlSec(process.env) * 1e3)) return "";
|
|
5240
5522
|
return contextResponse("SubagentStop", `## SOLID VIOLATIONS DETECTED (subagent output)\n${violations.join("\n")}\nRun sniper to fix these issues.`);
|
|
5241
5523
|
}
|
|
5242
5524
|
//#endregion
|
|
@@ -5296,7 +5578,7 @@ async function onSubagentStop(payload, cwd, home) {
|
|
|
5296
5578
|
await cacheSniperLessons(transcript, cwd, home);
|
|
5297
5579
|
await cacheTestResults(transcript, cwd, home);
|
|
5298
5580
|
}
|
|
5299
|
-
return checkSolidFromTranscript(transcript);
|
|
5581
|
+
return checkSolidFromTranscript(transcript, cwd);
|
|
5300
5582
|
}
|
|
5301
5583
|
/**
|
|
5302
5584
|
* Dispatch an ai-pilot-scope lifecycle event. Returns the native stdout, or
|
|
@@ -5322,10 +5604,10 @@ async function aipilotPostToolUse(payload, cwd, id = "claude-code") {
|
|
|
5322
5604
|
/** SessionStart handler keyed on plugin scope. */
|
|
5323
5605
|
function sessionStart(input) {
|
|
5324
5606
|
if (input.scope === "solid") return solidDetectStart();
|
|
5325
|
-
if (input.scope === "rules") return injectRules(
|
|
5607
|
+
if (input.scope === "rules") return injectRules(resolveRulesRoot(input.id ?? "claude-code", input.cwd), input.event, input.id ?? "claude-code");
|
|
5326
5608
|
if (input.scope === "carto") return cartoSessionStart(input.cwd, input.now);
|
|
5327
|
-
if (input.scope === "lessons") return dispatchLessons("SessionStart", input.payload, input.cwd, input.now);
|
|
5328
|
-
const core = sessionStartCore(input.cwd, void 0, input.now);
|
|
5609
|
+
if (input.scope === "lessons") return dispatchLessons("SessionStart", input.payload, input.cwd, input.now, input.id ?? "claude-code");
|
|
5610
|
+
const core = sessionStartCore(input.cwd, void 0, input.now, input.id ?? "claude-code");
|
|
5329
5611
|
return input.scope === "core" ? withSnapshot(core, input.cwd, import.meta.url) : core;
|
|
5330
5612
|
}
|
|
5331
5613
|
/**
|
|
@@ -5339,16 +5621,16 @@ function dispatchLifecycle(input) {
|
|
|
5339
5621
|
switch (input.event) {
|
|
5340
5622
|
case "SessionStart": return sessionStart(input);
|
|
5341
5623
|
case "UserPromptSubmit":
|
|
5342
|
-
if (input.scope === "rules") return injectRules(
|
|
5343
|
-
if (input.scope === "lessons") return dispatchLessons("UserPromptSubmit", input.payload, input.cwd, input.now);
|
|
5624
|
+
if (input.scope === "rules") return injectRules(resolveRulesRoot(input.id ?? "claude-code", input.cwd), input.event, input.id ?? "claude-code");
|
|
5625
|
+
if (input.scope === "lessons") return dispatchLessons("UserPromptSubmit", input.payload, input.cwd, input.now, input.id ?? "claude-code");
|
|
5344
5626
|
return null;
|
|
5345
5627
|
case "SubagentStart":
|
|
5346
|
-
if (input.scope === "rules") return injectRules(
|
|
5628
|
+
if (input.scope === "rules") return injectRules(resolveRulesRoot(input.id ?? "claude-code", input.cwd), input.event, input.id ?? "claude-code");
|
|
5347
5629
|
if (input.scope === "aipilot") return "";
|
|
5348
|
-
if (input.scope === "lessons") return dispatchLessons("SubagentStart", input.payload, input.cwd, input.now);
|
|
5630
|
+
if (input.scope === "lessons") return dispatchLessons("SubagentStart", input.payload, input.cwd, input.now, input.id ?? "claude-code");
|
|
5349
5631
|
return subagentCacheContext(input.payload.session_id);
|
|
5350
5632
|
case "Stop":
|
|
5351
|
-
if (input.scope === "lessons") return dispatchLessons("Stop", input.payload, input.cwd, input.now);
|
|
5633
|
+
if (input.scope === "lessons") return dispatchLessons("Stop", input.payload, input.cwd, input.now, input.id ?? "claude-code");
|
|
5352
5634
|
return input.scope === "core" ? stopCore(input.payload, input.cwd, input.now) : null;
|
|
5353
5635
|
case "SubagentStop":
|
|
5354
5636
|
if (input.scope === "aipilot") return "";
|
|
@@ -6354,7 +6636,11 @@ function nextModular(filePath, content, cwd) {
|
|
|
6354
6636
|
}
|
|
6355
6637
|
/** Laravel FuseCore architecture: `app/` domain ban + module.json + cross-module `use` rules. */
|
|
6356
6638
|
function fusecore(filePath, content, cwd) {
|
|
6357
|
-
for (const b of PHP_BLOCKED_IN_APP) if (filePath.includes(b))
|
|
6639
|
+
for (const b of PHP_BLOCKED_IN_APP) if (filePath.includes(b)) {
|
|
6640
|
+
const from = b.replace(/^\/|\/$/g, "");
|
|
6641
|
+
const to = b.replaceAll("/app/", "/FuseCore/{Module}/App/").replace(/^\/|\/$/g, "");
|
|
6642
|
+
return block$1(`BLOCKED: FuseCore project. Code in '${from}' FORBIDDEN. Move to '${to}'.`);
|
|
6643
|
+
}
|
|
6358
6644
|
const mod = filePath.match(/\/FuseCore\/([A-Za-z]+)\//);
|
|
6359
6645
|
if (!mod) return null;
|
|
6360
6646
|
const name = mod[1] ?? "";
|
|
@@ -6545,7 +6831,7 @@ function appendRefRead(dir, path, ts) {
|
|
|
6545
6831
|
function reconcileRefReadsFromJournal(track, dir, now) {
|
|
6546
6832
|
let text;
|
|
6547
6833
|
try {
|
|
6548
|
-
text = readText(join(dir, JOURNAL));
|
|
6834
|
+
text = readText$1(join(dir, JOURNAL));
|
|
6549
6835
|
} catch {
|
|
6550
6836
|
return track;
|
|
6551
6837
|
}
|
|
@@ -6748,10 +7034,9 @@ function geminiMcpGate(tool, filePath, content, ev) {
|
|
|
6748
7034
|
//#endregion
|
|
6749
7035
|
//#region src/freshness/agent-evidence.ts
|
|
6750
7036
|
/**
|
|
6751
|
-
* Platform-authored transcript evidence for APEX agent freshness.
|
|
6752
|
-
*
|
|
6753
|
-
*
|
|
6754
|
-
* which the Claude Code platform controls, unlike the self-recorded track.
|
|
7037
|
+
* Platform-authored transcript evidence for APEX agent freshness. Parses the
|
|
7038
|
+
* Claude Code session JSONL transcript for genuine dispatch tool_use entries
|
|
7039
|
+
* — forging requires writing into the platform-controlled transcript file.
|
|
6755
7040
|
*/
|
|
6756
7041
|
/** Parse a raw `timestamp` field to epoch ms; `undefined` when absent or invalid. */
|
|
6757
7042
|
function parseTs(raw) {
|
|
@@ -6761,23 +7046,14 @@ function parseTs(raw) {
|
|
|
6761
7046
|
return Number.isFinite(ms) ? ms : void 0;
|
|
6762
7047
|
}
|
|
6763
7048
|
/**
|
|
6764
|
-
* Return `true` ONLY when, for EVERY name in `names`, the
|
|
6765
|
-
*
|
|
6766
|
-
*
|
|
6767
|
-
*
|
|
6768
|
-
*
|
|
6769
|
-
*
|
|
6770
|
-
*
|
|
6771
|
-
* `
|
|
6772
|
-
*
|
|
6773
|
-
* **Timestamp note:** when a transcript entry carries no `timestamp` field it
|
|
6774
|
-
* is counted as within-window (we cannot prove staleness). This is
|
|
6775
|
-
* intentionally lenient to stay robust across transcript-format evolution; the
|
|
6776
|
-
* tamper-resistance guarantee derives from the platform authoring the file —
|
|
6777
|
-
* not from the timestamp alone.
|
|
6778
|
-
*
|
|
6779
|
-
* @param transcriptPath - Absolute path to the session `.jsonl` transcript
|
|
6780
|
-
* (hook payload field: `transcript_path`). Returns `false` when `undefined`.
|
|
7049
|
+
* Return `true` ONLY when, for EVERY name in `names`, the transcript at
|
|
7050
|
+
* `transcriptPath` shows a within-window genuine `tool_use`: a dispatch tool
|
|
7051
|
+
* ({@link isAgentTool}) whose `subagent_type`/`name` (plugin prefix stripped)
|
|
7052
|
+
* matches, OR a direct exploration/research call classified into that name by
|
|
7053
|
+
* {@link classifyExplore} — issued by any sub-agent or the lead (the transcript
|
|
7054
|
+
* carries no author field). Entries without `timestamp` count as within-window
|
|
7055
|
+
* (format-evolution leniency; tamper-resistance comes from platform authoring).
|
|
7056
|
+
* @param transcriptPath - Session `.jsonl` transcript (`transcript_path`); false when undefined.
|
|
6781
7057
|
* @param names - Required agent `subagent_type` values — ALL must appear.
|
|
6782
7058
|
* @param windowMs - Freshness window in milliseconds.
|
|
6783
7059
|
* @param now - Current epoch ms (pass `Date.now()` at the call-site).
|
|
@@ -6787,7 +7063,7 @@ function agentsRanFromTranscript(transcriptPath, names, windowMs, now) {
|
|
|
6787
7063
|
if (!transcriptPath || names.length === 0) return false;
|
|
6788
7064
|
let text;
|
|
6789
7065
|
try {
|
|
6790
|
-
text = readText(transcriptPath);
|
|
7066
|
+
text = readText$1(transcriptPath);
|
|
6791
7067
|
} catch {
|
|
6792
7068
|
return false;
|
|
6793
7069
|
}
|
|
@@ -6807,7 +7083,7 @@ function agentsRanFromTranscript(transcriptPath, names, windowMs, now) {
|
|
|
6807
7083
|
if (!Array.isArray(content)) continue;
|
|
6808
7084
|
for (const block of content) {
|
|
6809
7085
|
if (block?.type !== "tool_use") continue;
|
|
6810
|
-
if (block.name
|
|
7086
|
+
if (block.name !== void 0 && isAgentTool(block.name)) {
|
|
6811
7087
|
const raw = block.input?.subagent_type ?? block.input?.name;
|
|
6812
7088
|
const agent = typeof raw === "string" ? raw.split(":").pop() ?? raw : void 0;
|
|
6813
7089
|
if (agent !== void 0 && names.includes(agent)) found.add(agent);
|
|
@@ -6841,7 +7117,7 @@ async function apexScopedGate(input, track, window) {
|
|
|
6841
7117
|
if (protectedDeny) return protectedDeny;
|
|
6842
7118
|
if (SKIP_DIRS.test(input.filePath ?? "")) return null;
|
|
6843
7119
|
if (isTrivialEdit(input.tool, input.content, input.isReplaceAll) && trivialCount(track, window, input.now) < 4) {
|
|
6844
|
-
await
|
|
7120
|
+
await withTrack(input.trackFile, (fresh) => recordTrivialEdit(fresh, input.now, window, input.now));
|
|
6845
7121
|
return null;
|
|
6846
7122
|
}
|
|
6847
7123
|
const freshnessFor = (names, windowMs = window) => agentsFreshInTrack(track, names, windowMs, input.now) || (input.transcriptPath ? agentsRanFromTranscript(input.transcriptPath, names, windowMs, input.now) : agentsFresh(track, names, windowMs, input.now));
|
|
@@ -6867,7 +7143,7 @@ async function apexScopedGate(input, track, window) {
|
|
|
6867
7143
|
const authDeny = apexAuthorizationGate(ctx);
|
|
6868
7144
|
if (authDeny) {
|
|
6869
7145
|
const set_at = new Date(input.now).toISOString();
|
|
6870
|
-
await
|
|
7146
|
+
await withTrack(input.trackFile, (fresh) => recordTarget(fresh, {
|
|
6871
7147
|
project: input.cwd ?? "",
|
|
6872
7148
|
framework: input.framework,
|
|
6873
7149
|
set_by: "apex-authorization",
|
|
@@ -7071,13 +7347,13 @@ const MCP_TTL_MS = parseEnvInt(process.env.FUSE_MCP_TTL_SEC, MCP_TTL_DEFAULT_SEC
|
|
|
7071
7347
|
const WEBFETCH_TTL_MS = parseEnvInt(process.env.FUSE_WEBFETCH_TTL_SEC, WEBFETCH_TTL_DEFAULT_SEC) * 1e3;
|
|
7072
7348
|
/** WebFetch prompt slice folded into its cache key (parity with Python prompt[:500]). */
|
|
7073
7349
|
const PROMPT_TRUNC = 500;
|
|
7074
|
-
/** True for the WebFetch tool (exact name). */
|
|
7350
|
+
/** True for the WebFetch tool (exact name) or Kimi Code CLI's `FetchURL` equivalent. */
|
|
7075
7351
|
function isWebFetch(tool) {
|
|
7076
|
-
return tool === "WebFetch";
|
|
7352
|
+
return tool === "WebFetch" || tool === "FetchURL";
|
|
7077
7353
|
}
|
|
7078
|
-
/** MCP doc tools + WebFetch whose calls are cached / verbosity-capped. */
|
|
7354
|
+
/** MCP doc tools + WebFetch/FetchURL whose calls are cached / verbosity-capped. */
|
|
7079
7355
|
function isMcpTool(tool) {
|
|
7080
|
-
return /context7|exa|webfetch|web_fetch/i.test(tool) || tool
|
|
7356
|
+
return /context7|exa|webfetch|web_fetch/i.test(tool) || isWebFetch(tool);
|
|
7081
7357
|
}
|
|
7082
7358
|
/** The query/url that keys the cache. */
|
|
7083
7359
|
function queryOf(input) {
|
|
@@ -7105,6 +7381,7 @@ function denyWith(id, content) {
|
|
|
7105
7381
|
decision: "deny",
|
|
7106
7382
|
reason: content
|
|
7107
7383
|
});
|
|
7384
|
+
if (id === "kimi") return kimiDenyResponse(content);
|
|
7108
7385
|
return "";
|
|
7109
7386
|
}
|
|
7110
7387
|
function mutateWith(id, input) {
|
|
@@ -7173,8 +7450,7 @@ function mcpPostStore(tool, input, response, dir, now = Date.now()) {
|
|
|
7173
7450
|
//#region src/runtime/record.ts
|
|
7174
7451
|
/** Apply an activity to a session's track and persist it (PostToolUse path). */
|
|
7175
7452
|
async function recordActivity(file, activity) {
|
|
7176
|
-
|
|
7177
|
-
await saveTrack(file, activity.kind === "agent" ? recordAgent(track, activity.name, activity.ts, activity.quality) : activity.kind === "doc" ? recordDoc(track, activity.framework, activity.sessionId, activity.source, activity.ts) : recordRefRead(track, activity.path, activity.ts));
|
|
7453
|
+
await withTrack(file, (track) => activity.kind === "agent" ? recordAgent(track, activity.name, activity.ts, activity.quality) : activity.kind === "doc" ? recordDoc(track, activity.framework, activity.sessionId, activity.source, activity.ts) : recordRefRead(track, activity.path, activity.ts));
|
|
7178
7454
|
if (activity.kind === "ref") appendRefRead(dirname(file), activity.path, activity.ts ?? Date.now());
|
|
7179
7455
|
}
|
|
7180
7456
|
//#endregion
|
|
@@ -7195,6 +7471,12 @@ async function recordActivity(file, activity) {
|
|
|
7195
7471
|
* - hermes: delegated to the adapter's `toHermesResponse` — `block` ->
|
|
7196
7472
|
* `{decision:"block",reason}`; `ask`/`inform` degrade to non-blocking
|
|
7197
7473
|
* `{context}` (Hermes has no interactive "ask" state).
|
|
7474
|
+
* - kimi: delegated to the adapter's `toKimiResponse` — `block`/`ask` ->
|
|
7475
|
+
* `hookSpecificOutput.permissionDecision:"deny"` (Kimi Code CLI's hooks
|
|
7476
|
+
* contract documents only "deny"; `ask` downgrades with a prefix, mirroring
|
|
7477
|
+
* the codex branch above), `inform` -> raw text on stdout (Kimi appends
|
|
7478
|
+
* stdout to context on exit 0 — no `hookSpecificOutput` envelope documented
|
|
7479
|
+
* for non-blocking context injection).
|
|
7198
7480
|
*
|
|
7199
7481
|
* User-visible pass notices (`prompt.userMessage`, Python `hook_output.allow_pass`
|
|
7200
7482
|
* / `post_pass` parity), by harness:
|
|
@@ -7259,6 +7541,7 @@ function respond(id, prompt, event = "PreToolUse") {
|
|
|
7259
7541
|
agent_message: message
|
|
7260
7542
|
});
|
|
7261
7543
|
case "hermes": return toHermesResponse(prompt);
|
|
7544
|
+
case "kimi": return toKimiResponse(prompt);
|
|
7262
7545
|
case "cline":
|
|
7263
7546
|
if (kind === "block") return JSON.stringify({
|
|
7264
7547
|
cancel: true,
|
|
@@ -7462,7 +7745,7 @@ function uiDesignSkillGate(tool, filePath, content, ev) {
|
|
|
7462
7745
|
if (!isUiWrite(tool, filePath, content)) return null;
|
|
7463
7746
|
if (!designSkillRead(ev.refsRead)) return block("BLOCKED: design skill not consulted before writing UI. Read a design SKILL.md (design-system, design-web, or design-motion), then retry.", "Read a design skill reference, then retry");
|
|
7464
7747
|
const missing = missingDomainSkills(content, ev.refsRead);
|
|
7465
|
-
if (missing.length) return block(`BLOCKED: code uses ${missing.join(", ")} but the matching design skill(s) were not consulted. Read the SKILL.md for: ${missing.join(", ")}, then retry.`, `Read design skill(s): ${missing.join(", ")}, then retry`);
|
|
7748
|
+
if (missing.length) return block(`BLOCKED: code uses ${missing.join(", ")} but the matching design skill(s) were not consulted. Read the SKILL.md FILE (with the Read tool) for: ${missing.join(", ")}, then retry.`, `Read design skill(s): ${missing.join(", ")}, then retry`);
|
|
7466
7749
|
if (!ev.docConsulted) return block("BLOCKED: no documentation consulted. Use BOTH mcp__context7__query-docs AND mcp__exa__web_search_exa, or a web fallback alone (WebSearch/WebFetch). Gemini is NOT required.", "Consult Context7+Exa, or web docs alone (Gemini not required), then retry");
|
|
7467
7750
|
return null;
|
|
7468
7751
|
}
|
|
@@ -7732,6 +8015,201 @@ function designGate(payload, event, cacheDir, cwd) {
|
|
|
7732
8015
|
return null;
|
|
7733
8016
|
}
|
|
7734
8017
|
//#endregion
|
|
8018
|
+
//#region src/runtime/solid-file-size-gate.ts
|
|
8019
|
+
/**
|
|
8020
|
+
* PreToolUse file-size deny for the solid scope — the same SOLID ceiling the
|
|
8021
|
+
* core gate chain enforces (`policy/evaluate.ts`), extracted so
|
|
8022
|
+
* `harness hook <id> solid` blocks oversized Write/Edit WITHOUT the core-only
|
|
8023
|
+
* APEX freshness gates (they require explore/research agents that not every
|
|
8024
|
+
* harness runs). Pure and read-only (on-disk read via {@link existingLineCounts},
|
|
8025
|
+
* no session state), so the sibling-plugin fan-out stays idempotent.
|
|
8026
|
+
*/
|
|
8027
|
+
/**
|
|
8028
|
+
* Deny a Write/Edit whose resulting file would exceed the SOLID line ceiling.
|
|
8029
|
+
* Mirrors `policy/evaluate.ts` exactly: a computable Edit outcome passes when
|
|
8030
|
+
* the result is compliant OR strictly shrinks an already-oversized file; the
|
|
8031
|
+
* block message reports the pre-existing on-disk count for a Write.
|
|
8032
|
+
* @param tool - The tool name (only "Write"/"Edit" are checked).
|
|
8033
|
+
* @param filePath - The written file's absolute path.
|
|
8034
|
+
* @param content - Write: the full new content; Edit: the `new_string` snippet.
|
|
8035
|
+
* @param oldString - Edit only: the `old_string` being replaced.
|
|
8036
|
+
* @param isReplaceAll - Edit only: whether every occurrence is replaced.
|
|
8037
|
+
* @param agentType - Subagent type, when known (Explore/Plan are exempt).
|
|
8038
|
+
* @returns The blocking prompt, or null when compliant or out of scope.
|
|
8039
|
+
*/
|
|
8040
|
+
function solidFileSizeGate(tool, filePath, content, oldString, isReplaceAll = false, agentType) {
|
|
8041
|
+
if (tool !== "Write" && tool !== "Edit") return null;
|
|
8042
|
+
if (!filePath || !isFileSizeScoped(filePath)) return null;
|
|
8043
|
+
if (agentType === "Explore" || agentType === "Plan") return null;
|
|
8044
|
+
const { raw: existingLines, content: existingContent } = existingLineCounts(filePath);
|
|
8045
|
+
const incoming = content !== void 0 ? countLines(content) : 0;
|
|
8046
|
+
const existing = existingLines ?? 0;
|
|
8047
|
+
const max = resolveMaxLines();
|
|
8048
|
+
const editResult = tool === "Edit" && existingContent !== void 0 && content !== void 0 ? computeEditResultLines(existingContent, oldString, content, isReplaceAll) : null;
|
|
8049
|
+
if (editResult !== null && (editResult <= max || editResult < existing)) return null;
|
|
8050
|
+
const lines = tool === "Edit" ? editResult ?? Math.max(incoming, existing) : incoming || existing;
|
|
8051
|
+
const displayLines = tool === "Write" ? existingLines ?? lines : lines;
|
|
8052
|
+
const verdict = evaluateFileSize(lines, max, filePath, resolveSolidRefFramework(filePath), displayLines);
|
|
8053
|
+
if (lines === 0 || verdict.ok) return null;
|
|
8054
|
+
return {
|
|
8055
|
+
kind: "block",
|
|
8056
|
+
title: "SOLID file-size limit",
|
|
8057
|
+
reason: verdict.message ?? "",
|
|
8058
|
+
actions: [`Split into modules under ${verdict.max} lines (Single Responsibility)`, "Then re-run the write"]
|
|
8059
|
+
};
|
|
8060
|
+
}
|
|
8061
|
+
//#endregion
|
|
8062
|
+
//#region src/runtime/core-guards-active.ts
|
|
8063
|
+
/**
|
|
8064
|
+
* Per-harness ACTIVATION probing for the core-guards plugin, behind
|
|
8065
|
+
* {@link coreGuardsWired}. The owner decision: the solid scope abstains from
|
|
8066
|
+
* its file-size deny ONLY when core-guards is installed AND active — an
|
|
8067
|
+
* installed-but-disabled plugin must leave the solid deny firing. Safe
|
|
8068
|
+
* default, per harness: anything undetectable (unreadable/missing state)
|
|
8069
|
+
* reads as NOT active, so the worst case is a duplicate deny — never zero.
|
|
8070
|
+
*/
|
|
8071
|
+
/** File content, or null when unreadable (undetectable → caller's safe default). */
|
|
8072
|
+
function readText(path) {
|
|
8073
|
+
try {
|
|
8074
|
+
return readFileSync(path, "utf8");
|
|
8075
|
+
} catch {
|
|
8076
|
+
return null;
|
|
8077
|
+
}
|
|
8078
|
+
}
|
|
8079
|
+
/** First `core-guards@…` value in a name→bool map; undefined when absent. */
|
|
8080
|
+
function enabledEntry(map) {
|
|
8081
|
+
if (typeof map !== "object" || map === null || Array.isArray(map)) return void 0;
|
|
8082
|
+
for (const [k, v] of Object.entries(map)) if (k.startsWith("core-guards@")) return v !== false;
|
|
8083
|
+
}
|
|
8084
|
+
/**
|
|
8085
|
+
* Claude Code: `~/.claude/settings.json` → `enabledPlugins`. core-guards is a
|
|
8086
|
+
* REQUIRED marketplace plugin: no entry means active whenever the marketplace
|
|
8087
|
+
* copy exists (`installed`); an explicit `false` means disabled. Unreadable
|
|
8088
|
+
* settings → false (undetectable).
|
|
8089
|
+
* @param home - OS home.
|
|
8090
|
+
* @param installed - Whether the marketplace copy of core-guards exists.
|
|
8091
|
+
*/
|
|
8092
|
+
function claudeCoreGuardsActive(home, installed) {
|
|
8093
|
+
const text = readText(join(home, ".claude", "settings.json"));
|
|
8094
|
+
if (text === null) return false;
|
|
8095
|
+
try {
|
|
8096
|
+
return enabledEntry(JSON.parse(text).enabledPlugins) ?? installed;
|
|
8097
|
+
} catch {
|
|
8098
|
+
return false;
|
|
8099
|
+
}
|
|
8100
|
+
}
|
|
8101
|
+
/**
|
|
8102
|
+
* Codex: `[plugins."core-guards@<mkt>"] enabled = …` in `~/.codex/config.toml`
|
|
8103
|
+
* (minimal section-scoped extraction — no TOML dependency). Missing section,
|
|
8104
|
+
* `enabled = false`, or unreadable file → false.
|
|
8105
|
+
* @param home - OS home.
|
|
8106
|
+
*/
|
|
8107
|
+
function codexCoreGuardsActive(home) {
|
|
8108
|
+
const text = readText(join(home, ".codex", "config.toml"));
|
|
8109
|
+
if (text === null) return false;
|
|
8110
|
+
return /^\[plugins\."core-guards@[^"]+"\]\s*enabled\s*=\s*(true|false)/m.exec(text)?.[1] === "true";
|
|
8111
|
+
}
|
|
8112
|
+
/**
|
|
8113
|
+
* Kimi: `<KIMI_CODE_HOME>/plugins/installed.json` — map form (`id →
|
|
8114
|
+
* {enabled?}`) or array form (`[{id, enabled?}]`); an entry counts active
|
|
8115
|
+
* unless `enabled === false`. File absent here today → format read
|
|
8116
|
+
* tolerantly; missing/unreadable → false (undetectable).
|
|
8117
|
+
* @param root - Kimi data root (`KIMI_CODE_HOME` ?? `~/.kimi-code`).
|
|
8118
|
+
*/
|
|
8119
|
+
function kimiCoreGuardsActive(root) {
|
|
8120
|
+
const text = readText(join(root, "plugins", "installed.json"));
|
|
8121
|
+
if (text === null) return false;
|
|
8122
|
+
try {
|
|
8123
|
+
const data = JSON.parse(text).plugins;
|
|
8124
|
+
if (Array.isArray(data)) {
|
|
8125
|
+
const hit = data.find((p) => typeof p === "object" && p !== null && p.id === "core-guards");
|
|
8126
|
+
return hit !== void 0 && hit.enabled !== false;
|
|
8127
|
+
}
|
|
8128
|
+
if (typeof data !== "object" || data === null) return false;
|
|
8129
|
+
for (const [k, v] of Object.entries(data)) if (k === "core-guards" || k.startsWith("core-guards@")) return typeof v === "object" && v !== null ? v.enabled !== false : v !== false;
|
|
8130
|
+
return false;
|
|
8131
|
+
} catch {
|
|
8132
|
+
return false;
|
|
8133
|
+
}
|
|
8134
|
+
}
|
|
8135
|
+
//#endregion
|
|
8136
|
+
//#region src/runtime/core-guards-wired.ts
|
|
8137
|
+
/**
|
|
8138
|
+
* Detect whether the core-guards plugin is installed AND ACTIVE for a harness.
|
|
8139
|
+
* When it is, its `core` scope already owns the PreToolUse file-size deny
|
|
8140
|
+
* (`policy/evaluate.ts`), so the `solid` scope MUST abstain on the same
|
|
8141
|
+
* tool-call — otherwise a user with BOTH plugins wired on PreToolUse
|
|
8142
|
+
* Write|Edit receives the identical deny twice. Activation probing is
|
|
8143
|
+
* per-harness (`core-guards-active.ts`, owner decision):
|
|
8144
|
+
* - claude-code: marketplace copy present + `settings.json` `enabledPlugins`
|
|
8145
|
+
* (required core: no entry = active; explicit `false` = disabled);
|
|
8146
|
+
* - codex: `[plugins."core-guards@…"] enabled = true` in `~/.codex/config.toml`;
|
|
8147
|
+
* - kimi: `<KIMI_CODE_HOME>/plugins/installed.json` entry not disabled.
|
|
8148
|
+
* SAFE DEFAULT: anything undetectable (unreadable/missing state) reads as
|
|
8149
|
+
* NOT active — the solid scope then fires its own deny, so the worst case is
|
|
8150
|
+
* a duplicate deny, NEVER zero deny.
|
|
8151
|
+
*/
|
|
8152
|
+
/** Immediate child dir names of `dir`, or [] when unreadable. */
|
|
8153
|
+
function children(dir) {
|
|
8154
|
+
try {
|
|
8155
|
+
return readdirSync(dir);
|
|
8156
|
+
} catch {
|
|
8157
|
+
return [];
|
|
8158
|
+
}
|
|
8159
|
+
}
|
|
8160
|
+
/** Claude: marketplace copy `~/.claude/plugins/marketplaces/<m>/plugins/core-guards`. */
|
|
8161
|
+
function claudeInstalled(home) {
|
|
8162
|
+
const markets = join(home, ".claude", "plugins", "marketplaces");
|
|
8163
|
+
return children(markets).some((m) => existsSync(join(markets, m, "plugins", "core-guards")));
|
|
8164
|
+
}
|
|
8165
|
+
/**
|
|
8166
|
+
* True when core-guards is installed AND active for the harness.
|
|
8167
|
+
* @param id - Harness id.
|
|
8168
|
+
* @param env - Environment (defaults to `process.env`).
|
|
8169
|
+
* @param home - OS home (defaults to `env.HOME` / `os.homedir()`; injectable).
|
|
8170
|
+
*/
|
|
8171
|
+
function coreGuardsWired(id, env = process.env, home = env.HOME ?? homedir()) {
|
|
8172
|
+
if (id === "claude-code") return claudeCoreGuardsActive(home, claudeInstalled(home));
|
|
8173
|
+
if (id === "codex") return codexCoreGuardsActive(home);
|
|
8174
|
+
if (id === "kimi") return kimiCoreGuardsActive(env.KIMI_CODE_HOME ?? join(home, ".kimi-code"));
|
|
8175
|
+
return false;
|
|
8176
|
+
}
|
|
8177
|
+
//#endregion
|
|
8178
|
+
//#region src/runtime/solid-pre.ts
|
|
8179
|
+
/**
|
|
8180
|
+
* Solid-scope PreToolUse outcome: the file-size deny ({@link solidFileSizeGate})
|
|
8181
|
+
* first — rendered via {@link respond} so every harness gets its native shape
|
|
8182
|
+
* (Kimi finally receives a real `hookSpecificOutput` deny instead of a leaked
|
|
8183
|
+
* Claude envelope) — then the ported Go/Python interface-location check
|
|
8184
|
+
* ({@link validateSolidGate}, kept byte-identical for claude-code/codex).
|
|
8185
|
+
* Owns the whole solid-scope branch so `handle-pre` stays a dispatcher and the
|
|
8186
|
+
* branch NEVER falls through to the core APEX/SOLID gate chain: core-guards
|
|
8187
|
+
* owns it, so falling through would run `gate()` twice per edit when both
|
|
8188
|
+
* plugins wire PreToolUse Write|Edit (duplicate denies + added latency). For
|
|
8189
|
+
* the same reason the file-size gate itself ABSTAINS when core-guards is
|
|
8190
|
+
* installed ({@link coreGuardsWired}): one tool-call, one deny, whoever fires.
|
|
8191
|
+
*/
|
|
8192
|
+
/**
|
|
8193
|
+
* Run the solid-scope PreToolUse gates and render the native hook outcome.
|
|
8194
|
+
* @param id - Harness id (selects the response shape via {@link respond}).
|
|
8195
|
+
* @param event - The normalized hook event.
|
|
8196
|
+
* @param file - Session track file (the deny-notice state derives from its dir).
|
|
8197
|
+
* @param now - Current time in ms.
|
|
8198
|
+
* @param env - Environment for the core-guards wiring probe (injectable).
|
|
8199
|
+
* @returns The hook outcome: file-size deny, Go/Python deny, or allow.
|
|
8200
|
+
*/
|
|
8201
|
+
function solidScopeOutcome(id, event, file, now, env = process.env) {
|
|
8202
|
+
const prompt = coreGuardsWired(id, env) ? null : solidFileSizeGate(event.tool, event.filePath, event.content, event.oldString, event.input.replace_all === true, event.agentType);
|
|
8203
|
+
if (prompt) return {
|
|
8204
|
+
stdout: withDenyNotice(id, respond(id, prompt), prompt, event.sessionId, dirname(file), now),
|
|
8205
|
+
exit: 0
|
|
8206
|
+
};
|
|
8207
|
+
return {
|
|
8208
|
+
stdout: validateSolidGate(event.tool, event.filePath ?? "", event.content ?? ""),
|
|
8209
|
+
exit: 0
|
|
8210
|
+
};
|
|
8211
|
+
}
|
|
8212
|
+
//#endregion
|
|
7735
8213
|
//#region src/runtime/pre-allow.ts
|
|
7736
8214
|
/**
|
|
7737
8215
|
* PreToolUse ALLOW-path response assembly. Reached only after every gate
|
|
@@ -7890,11 +8368,8 @@ async function handlePre(ctx) {
|
|
|
7890
8368
|
stdout: event.files?.length ? securityAdvisoryForPatch(event.files, opts.now) : securityAdvisory(event.tool, event.filePath ?? "", opts.now),
|
|
7891
8369
|
exit: 0
|
|
7892
8370
|
};
|
|
7893
|
-
if (opts.scope === "solid") return
|
|
7894
|
-
|
|
7895
|
-
exit: 0
|
|
7896
|
-
};
|
|
7897
|
-
if (event.tool === "Task") {
|
|
8371
|
+
if (opts.scope === "solid") return solidScopeOutcome(id, event, file, opts.now);
|
|
8372
|
+
if (isAgentTool(event.tool)) {
|
|
7898
8373
|
const taskCtx = taskContext(opts.cwd, id);
|
|
7899
8374
|
if (taskCtx) return {
|
|
7900
8375
|
stdout: taskCtx,
|
|
@@ -8116,9 +8591,8 @@ function shellReadRefPaths(command) {
|
|
|
8116
8591
|
}
|
|
8117
8592
|
//#endregion
|
|
8118
8593
|
//#region src/runtime/activity.ts
|
|
8119
|
-
/**
|
|
8594
|
+
/** Quality thresholds (chars): lead agent calls vs direct explore/research (parity Python `> 50`). */
|
|
8120
8595
|
const AGENT_QUALITY_MIN = 500;
|
|
8121
|
-
/** Min response length (chars) for direct exploration/research (parity Python `> 50`). */
|
|
8122
8596
|
const EXPLORE_QUALITY_MIN = 50;
|
|
8123
8597
|
/** Read tools across harnesses (Claude `Read`, Gemini/Cline `read_file`, …). */
|
|
8124
8598
|
const READ_TOOLS = /* @__PURE__ */ new Set([
|
|
@@ -8163,7 +8637,7 @@ function docSourceOf(tool) {
|
|
|
8163
8637
|
* Map a live tool-use to the activity to record, or null when nothing is
|
|
8164
8638
|
* tracked. Works across harnesses — tool names are globally distinct:
|
|
8165
8639
|
* - MCP doc calls (`context7` / `exa`, any separator) → `doc`
|
|
8166
|
-
* - `Task`/`Agent` + `subagent_type` (Claude/Cursor) → `agent` (bare agent name)
|
|
8640
|
+
* - `Task`/`Agent`/`AgentSwarm` + `subagent_type` (Claude/Cursor/Kimi) → `agent` (bare agent name)
|
|
8167
8641
|
* - direct exploration/research (Glob/Grep, explore Bash, web, MCP cache reads)
|
|
8168
8642
|
* → `agent` credited to the matching REQUIRED_AGENTS phase
|
|
8169
8643
|
* - a read tool opening a `.md` reference → `ref`
|
|
@@ -8178,7 +8652,7 @@ function activityFor(event) {
|
|
|
8178
8652
|
source: docSource,
|
|
8179
8653
|
ts: event.now
|
|
8180
8654
|
});
|
|
8181
|
-
if (event.tool
|
|
8655
|
+
if (isAgentTool(event.tool)) {
|
|
8182
8656
|
const name = String(event.input?.subagent_type ?? event.input?.name ?? "").split(":").pop() ?? "";
|
|
8183
8657
|
if (name) out.push(agentActivity(name, event.now, qualityFor(event.responseLength, AGENT_QUALITY_MIN)));
|
|
8184
8658
|
return out;
|
|
@@ -8284,9 +8758,37 @@ function creditCodexSpawnAgent(id, tool, input, track, ts) {
|
|
|
8284
8758
|
*/
|
|
8285
8759
|
async function recordCodexSpawnEvidence(file, id, tool, input, ts) {
|
|
8286
8760
|
if (id !== "codex" || !isCodexSpawnAgentTool(tool)) return;
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8761
|
+
await withTrack(file, (track) => {
|
|
8762
|
+
const next = creditCodexSpawnAgent(id, tool, input, track, ts);
|
|
8763
|
+
return next !== track ? next : track;
|
|
8764
|
+
});
|
|
8765
|
+
}
|
|
8766
|
+
//#endregion
|
|
8767
|
+
//#region src/runtime/receipt-capture.ts
|
|
8768
|
+
/**
|
|
8769
|
+
* Bash verification-receipt extraction for PostToolUse: a `tsc`/`bun test`
|
|
8770
|
+
* run is parsed (exit code + output) into a signed receipt the TaskCompleted
|
|
8771
|
+
* gate later demands. Structured responses ONLY: Kimi Code CLI sends
|
|
8772
|
+
* `tool_output` as a truncated STRING (no exit_code, no stdout/stderr
|
|
8773
|
+
* channels) — parsing it would forge a success receipt (`exit 0`, empty
|
|
8774
|
+
* output) for possibly-failed runs, so a non-object source is skipped.
|
|
8775
|
+
*/
|
|
8776
|
+
/**
|
|
8777
|
+
* Record a verification receipt for a Bash run when the response is structured.
|
|
8778
|
+
* @param file - Session track file.
|
|
8779
|
+
* @param tool - The tool name (only "Bash" receipts exist).
|
|
8780
|
+
* @param command - The executed command.
|
|
8781
|
+
* @param toolResult - `payload.tool_result` (preferred), when present.
|
|
8782
|
+
* @param response - `payload.tool_response ?? payload.tool_output` fallback.
|
|
8783
|
+
* @param now - Clock.
|
|
8784
|
+
*/
|
|
8785
|
+
async function captureBashReceipt(file, tool, command, toolResult, response, now) {
|
|
8786
|
+
if (tool !== "Bash" || !command) return;
|
|
8787
|
+
const r = toolResult ?? response;
|
|
8788
|
+
if (!r || typeof r !== "object") return;
|
|
8789
|
+
const out = `${typeof r.stdout === "string" ? r.stdout : ""}\n${typeof r.stderr === "string" ? r.stderr : ""}`;
|
|
8790
|
+
const exit = Number(r.exit_code ?? 0);
|
|
8791
|
+
await captureReceipt(file, command, out, Number.isFinite(exit) ? exit : 0, now);
|
|
8290
8792
|
}
|
|
8291
8793
|
//#endregion
|
|
8292
8794
|
//#region src/tracking/codex-post-failure.ts
|
|
@@ -8429,12 +8931,7 @@ async function handlePost(ctx) {
|
|
|
8429
8931
|
const evidence = classifyAgentEvidence(event.tool, event.input, response);
|
|
8430
8932
|
if (evidence) await recordAgentEvidence(file, evidence, opts.now, typeof payload.agent_id === "string" ? payload.agent_id : void 0);
|
|
8431
8933
|
await recordCodexSpawnEvidence(file, id, event.tool, event.input, opts.now);
|
|
8432
|
-
|
|
8433
|
-
const r = payload.tool_result ?? response;
|
|
8434
|
-
const out = `${typeof r?.stdout === "string" ? r.stdout : ""}\n${typeof r?.stderr === "string" ? r.stderr : ""}`;
|
|
8435
|
-
const exit = Number(r?.exit_code ?? 0);
|
|
8436
|
-
await captureReceipt(file, event.command, out, Number.isFinite(exit) ? exit : 0, opts.now);
|
|
8437
|
-
}
|
|
8934
|
+
await captureBashReceipt(file, event.tool, event.command, payload.tool_result, response, opts.now);
|
|
8438
8935
|
if (id === "codex") recordCodexPostFailure(event.tool, payload.tool_result ?? response, {
|
|
8439
8936
|
now: opts.now,
|
|
8440
8937
|
dir: defaultStateDir(opts.cwd),
|
|
@@ -8511,6 +9008,10 @@ async function handlePost(ctx) {
|
|
|
8511
9008
|
stdout: attachSystemMessage(extra, userMessage),
|
|
8512
9009
|
exit: 0
|
|
8513
9010
|
};
|
|
9011
|
+
if (id === "kimi") return {
|
|
9012
|
+
stdout: respond(id, withUserMessage, "PostToolUse"),
|
|
9013
|
+
exit: 0
|
|
9014
|
+
};
|
|
8514
9015
|
return {
|
|
8515
9016
|
stdout: respond(id, withUserMessage, "PostToolUse") || extra,
|
|
8516
9017
|
exit: 0
|
|
@@ -8960,7 +9461,7 @@ async function handleHook(id, payload, opts) {
|
|
|
8960
9461
|
};
|
|
8961
9462
|
const userPrompt = typeof payload.prompt === "string" ? payload.prompt : void 0;
|
|
8962
9463
|
if (userPrompt !== void 0) {
|
|
8963
|
-
await
|
|
9464
|
+
await withTrack(file, (track) => recordBrainstormRequired(track, detectCreationIntent(userPrompt)));
|
|
8964
9465
|
return {
|
|
8965
9466
|
stdout: promptSubmitContext(userPrompt, opts.cwd, id),
|
|
8966
9467
|
exit: 0
|