@fusengine/harness 0.1.80 → 0.1.83
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/claude/index.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 +54 -46
- package/dist/adapters/kimi/index.mjs +2 -2
- 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 +129 -38
- 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/{doc-helpers-CEKzGg2u.d.mts → doc-helpers-BGCzP9VF.d.mts} +12 -12
- package/dist/{dotenv-B26N_uGr.mjs → dotenv-DoqNlALG.mjs} +29 -2
- package/dist/{evaluate-ClfbCDiY.mjs → evaluate-B4-MhOy_.mjs} +322 -32
- package/dist/freshness/index.d.mts +2 -2
- package/dist/{handle-B7YufFz9.mjs → handle-F35Gx-QS.mjs} +596 -174
- 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 +8 -8
- package/dist/init/index.d.mts +8 -9
- package/dist/init/index.mjs +1 -1
- package/dist/{kimi-C7FGprQz.mjs → kimi-ChbcU2uI.mjs} +17 -3
- 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 +453 -451
- 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-CDvR4bbF.mjs → skill-path-DhBGlQe3.mjs} +2 -2
- package/dist/store-Dw08Amqh.mjs +845 -0
- package/dist/tracking/index.d.mts +23 -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-CkmquRDV.mjs → validate-Bf-EsbDh.mjs} +279 -20
- package/package.json +163 -163
- package/dist/apex-Wdi1nq_w.d.mts +0 -73
- package/dist/harness-BQ5_Wnva.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-B_VHbsaJ.d.mts +0 -96
- package/dist/limits-CHn8AIL1.mjs +0 -31
- package/dist/store-nDZ3eLBQ.mjs +0 -349
|
@@ -1,22 +1,22 @@
|
|
|
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 { D as frameworkSolidGate, F as
|
|
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 { C as apexAuthorizationGate, S as trivialCount, _ as recordBrainstormRequired, a as journalLogPath, b as recordTarget, c as appendEvent, d as signTrack, f as verifyTrack, g as recordAgent, h as emptyTrack, i as withTrack, l as withTrackLockSync, m as agentsFresh, o as readTrackSync, p as writeLastNonce, r as trackJournalEnabled, s as diffTrackEvents, t as loadTrack, u as LOCK_FAILED, v as recordDoc, x as recordTrivialEdit, y as recordRefRead } from "./store-Dw08Amqh.mjs";
|
|
16
16
|
import { n as parseApplyPatch, t as isBypassPermissions } from "./permission-mode-BN3MNgbm.mjs";
|
|
17
|
-
import { _ as
|
|
18
|
-
import {
|
|
19
|
-
import { r as
|
|
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";
|
|
20
20
|
import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
21
21
|
import { appendFileSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, renameSync, rmSync, rmdirSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
22
22
|
import { homedir } from "node:os";
|
|
@@ -351,6 +351,34 @@ function designLifecycle(payload, cacheDir, cwd, stamp, now) {
|
|
|
351
351
|
return false;
|
|
352
352
|
}
|
|
353
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
|
|
354
382
|
//#region src/runtime/inject-dedup-exclusive.ts
|
|
355
383
|
/**
|
|
356
384
|
* @module inject-dedup-exclusive
|
|
@@ -599,7 +627,7 @@ function promptSubmitContext(prompt, cwd, id = "claude-code") {
|
|
|
599
627
|
const ctx = buildClaudeMdContext(prompt, cwd, id);
|
|
600
628
|
if (!ctx) return "";
|
|
601
629
|
if (!oncePerWindow(claudeMdKey(prompt, ctx), 3e3)) return "";
|
|
602
|
-
return
|
|
630
|
+
return renderInform(id, "UserPromptSubmit", ctx, `${apexDocName(id)} injected`);
|
|
603
631
|
}
|
|
604
632
|
/**
|
|
605
633
|
* PreToolUse Task context injection: render the APEX sub-agent context as a
|
|
@@ -801,17 +829,19 @@ function readRules(rulesDir) {
|
|
|
801
829
|
}
|
|
802
830
|
/**
|
|
803
831
|
* Build the rules injection for claude-rules (SessionStart, UserPromptSubmit,
|
|
804
|
-
* SubagentStart): read `<pluginRoot>/rules/*.md` and emit
|
|
805
|
-
* or
|
|
806
|
-
* spec requires it to match the firing
|
|
807
|
-
* 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).
|
|
808
837
|
* @param pluginRoot - `CLAUDE_PLUGIN_ROOT` of the claude-rules plugin.
|
|
809
838
|
* @param event - The firing hook event name (e.g. "SessionStart").
|
|
839
|
+
* @param id - Harness target id (defaults to "claude-code" — zero-regression default).
|
|
810
840
|
* @returns The native hook stdout (possibly empty).
|
|
811
841
|
*/
|
|
812
|
-
function injectRules(pluginRoot, event) {
|
|
842
|
+
function injectRules(pluginRoot, event, id = "claude-code") {
|
|
813
843
|
const content = readRules(join(pluginRoot, "rules"));
|
|
814
|
-
return content ?
|
|
844
|
+
return content ? renderInform(id, event, content, "rules 00-08 injected") : "";
|
|
815
845
|
}
|
|
816
846
|
//#endregion
|
|
817
847
|
//#region src/runtime/lifecycle/solid-detect.ts
|
|
@@ -960,7 +990,7 @@ function checkFileSize(tool, filePath, env = process.env) {
|
|
|
960
990
|
if (!filePath || !pathExists(filePath)) return "";
|
|
961
991
|
let content;
|
|
962
992
|
try {
|
|
963
|
-
content = readText(filePath);
|
|
993
|
+
content = readText$1(filePath);
|
|
964
994
|
} catch {
|
|
965
995
|
return "";
|
|
966
996
|
}
|
|
@@ -982,20 +1012,32 @@ function checkFileSize(tool, filePath, env = process.env) {
|
|
|
982
1012
|
* gated behind `SOLID_PROJECT_TYPE`. Inert when `SOLID_PROJECT_TYPE` is
|
|
983
1013
|
* absent/"unknown"/nextjs/laravel/swift/rust (parity: the Python hook only
|
|
984
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).
|
|
985
1023
|
*/
|
|
986
|
-
/** Top-level Go interface declaration, e.g. `type Foo interface {`. */
|
|
987
|
-
const GO_INTERFACE_RE = /^type.*interface \{/m;
|
|
988
|
-
/** Python class whose header mentions `ABC` (naive, parity with the Python regex). */
|
|
989
|
-
const PY_ABC_RE = /class.*ABC/;
|
|
990
1024
|
/** Go: deny a top-level interface declared outside `/interfaces/`. */
|
|
991
|
-
function checkGo(filePath, content) {
|
|
1025
|
+
function checkGo(filePath, content, env) {
|
|
992
1026
|
if (!filePath.endsWith(".go") || filePath.includes("/interfaces/")) return null;
|
|
993
|
-
|
|
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);
|
|
994
1032
|
}
|
|
995
1033
|
/** Python: deny an ABC subclass declared outside `/interfaces/`. */
|
|
996
|
-
function checkPython(filePath, content) {
|
|
1034
|
+
function checkPython(filePath, content, env) {
|
|
997
1035
|
if (!filePath.endsWith(".py") || filePath.includes("/interfaces/")) return null;
|
|
998
|
-
|
|
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);
|
|
999
1041
|
}
|
|
1000
1042
|
/** `SOLID_PROJECT_TYPE` values this gate validates (parity subset — see module doc). */
|
|
1001
1043
|
const VALIDATORS = {
|
|
@@ -1003,20 +1045,19 @@ const VALIDATORS = {
|
|
|
1003
1045
|
python: checkPython
|
|
1004
1046
|
};
|
|
1005
1047
|
/**
|
|
1006
|
-
* 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.
|
|
1007
1049
|
* @param tool - The tool name (only "Write"/"Edit" are checked).
|
|
1008
1050
|
* @param filePath - The written file's absolute path.
|
|
1009
1051
|
* @param content - The written/edited content (`new_string` snippet on Edit).
|
|
1010
1052
|
* @param env - Environment (defaults to `process.env`).
|
|
1011
|
-
* @returns The PreToolUse
|
|
1053
|
+
* @returns The PreToolUse response, or `""` when clean/inert.
|
|
1012
1054
|
*/
|
|
1013
1055
|
function validateSolidGate(tool, filePath, content, env = process.env) {
|
|
1014
1056
|
const ptype = env.SOLID_PROJECT_TYPE ?? "";
|
|
1015
1057
|
if (!ptype || ptype === "unknown") return "";
|
|
1016
1058
|
if (tool !== "Write" && tool !== "Edit") return "";
|
|
1017
1059
|
if (!filePath) return "";
|
|
1018
|
-
|
|
1019
|
-
return violation ? denyResponse("PreToolUse", violation) : "";
|
|
1060
|
+
return VALIDATORS[ptype]?.(filePath, content, env) ?? "";
|
|
1020
1061
|
}
|
|
1021
1062
|
//#endregion
|
|
1022
1063
|
//#region src/runtime/lifecycle/validate-tailwind.ts
|
|
@@ -1052,7 +1093,7 @@ function isTailwindProject(filePath) {
|
|
|
1052
1093
|
const pkgPath = join(root, "package.json");
|
|
1053
1094
|
if (!pathExists(pkgPath)) return false;
|
|
1054
1095
|
try {
|
|
1055
|
-
const pkg = JSON.parse(readText(pkgPath));
|
|
1096
|
+
const pkg = JSON.parse(readText$1(pkgPath));
|
|
1056
1097
|
return Boolean(pkg.dependencies?.tailwindcss) || Boolean(pkg.devDependencies?.tailwindcss);
|
|
1057
1098
|
} catch {
|
|
1058
1099
|
return false;
|
|
@@ -1070,7 +1111,7 @@ function validateTailwind(tool, filePath) {
|
|
|
1070
1111
|
if (!pathExists(filePath) || !isTailwindProject(filePath)) return "";
|
|
1071
1112
|
let content;
|
|
1072
1113
|
try {
|
|
1073
|
-
content = readText(filePath);
|
|
1114
|
+
content = readText$1(filePath);
|
|
1074
1115
|
} catch {
|
|
1075
1116
|
return "";
|
|
1076
1117
|
}
|
|
@@ -1207,15 +1248,13 @@ function freshPassingReceipt(track, windowMs, now) {
|
|
|
1207
1248
|
return hits.length ? hits.reduce((a, b) => b.ts > a.ts ? b : a) : null;
|
|
1208
1249
|
}
|
|
1209
1250
|
/**
|
|
1210
|
-
* Sync variant reading the
|
|
1211
|
-
* (TaskCompleted / SubagentStop). Returns the newest passing receipt, or
|
|
1212
|
-
* on any read/verify failure (fail-closed: no proof ⇒
|
|
1251
|
+
* Sync variant reading the track directly (snapshot ⊕ journal) — for the sync
|
|
1252
|
+
* gates (TaskCompleted / SubagentStop). Returns the newest passing receipt, or
|
|
1253
|
+
* `null` on any read/verify failure (fail-closed: no proof ⇒ unverified).
|
|
1213
1254
|
*/
|
|
1214
1255
|
function freshReceiptFromFile(file, windowMs, now) {
|
|
1215
1256
|
try {
|
|
1216
|
-
|
|
1217
|
-
const track = verifyTrack(JSON.parse(readFileSync(file, "utf-8")));
|
|
1218
|
-
return track ? freshPassingReceipt(track, windowMs, now) : null;
|
|
1257
|
+
return freshPassingReceipt(readTrackSync(file, trackJournalEnabled()), windowMs, now);
|
|
1219
1258
|
} catch {
|
|
1220
1259
|
return null;
|
|
1221
1260
|
}
|
|
@@ -1227,7 +1266,7 @@ function freshReceiptFromFile(file, windowMs, now) {
|
|
|
1227
1266
|
async function captureReceipt(file, command, output, exitCode, now) {
|
|
1228
1267
|
const receipt = classifyReceipt(command, output, exitCode, now);
|
|
1229
1268
|
if (!receipt) return;
|
|
1230
|
-
await
|
|
1269
|
+
await withTrack(file, (track) => recordReceipt(track, receipt));
|
|
1231
1270
|
}
|
|
1232
1271
|
//#endregion
|
|
1233
1272
|
//#region src/runtime/lifecycle/agent-transcript.ts
|
|
@@ -1259,7 +1298,7 @@ function readAgentToolUses(transcriptPath) {
|
|
|
1259
1298
|
if (!transcriptPath) return null;
|
|
1260
1299
|
let text;
|
|
1261
1300
|
try {
|
|
1262
|
-
text = readText(transcriptPath);
|
|
1301
|
+
text = readText$1(transcriptPath);
|
|
1263
1302
|
} catch {
|
|
1264
1303
|
return null;
|
|
1265
1304
|
}
|
|
@@ -1713,6 +1752,102 @@ function postEditTypescript(filePath) {
|
|
|
1713
1752
|
return contextResponse("PostToolUse", `Lint issues in ${basename(filePath)}: ${issues.join(" | ")}`);
|
|
1714
1753
|
}
|
|
1715
1754
|
//#endregion
|
|
1755
|
+
//#region src/util/semver.ts
|
|
1756
|
+
/**
|
|
1757
|
+
* Dependency-free semver ordering. `Array.prototype.sort()` compares strings
|
|
1758
|
+
* lexicographically — `"1.0.9" > "1.0.23"` — so versioned directories (plugin
|
|
1759
|
+
* caches) must be ordered with a numeric, segment-by-segment comparison.
|
|
1760
|
+
*/
|
|
1761
|
+
/**
|
|
1762
|
+
* Compare two dotted version strings numerically, segment by segment
|
|
1763
|
+
* (shorter version wins when a prefix is equal: `1.0` < `1.0.1`). A segment
|
|
1764
|
+
* that is not a plain integer (prerelease tag like `0-beta`) falls back to
|
|
1765
|
+
* lexicographic comparison of that segment — never NaN, so `sort()` stays
|
|
1766
|
+
* defined; identical strings compare 0.
|
|
1767
|
+
* @returns Negative when `a < b`, positive when `a > b`, 0 when equal.
|
|
1768
|
+
*/
|
|
1769
|
+
function compareSemver(a, b) {
|
|
1770
|
+
const pa = a.split("."), pb = b.split(".");
|
|
1771
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
1772
|
+
const sa = pa[i] ?? "0", sb = pb[i] ?? "0";
|
|
1773
|
+
if (sa === sb) continue;
|
|
1774
|
+
const na = Number(sa), nb = Number(sb);
|
|
1775
|
+
if (Number.isInteger(na) && Number.isInteger(nb) && na !== nb) return na - nb;
|
|
1776
|
+
return sa < sb ? -1 : 1;
|
|
1777
|
+
}
|
|
1778
|
+
return 0;
|
|
1779
|
+
}
|
|
1780
|
+
/** The highest version of the list, or null when empty. */
|
|
1781
|
+
function maxSemver(versions) {
|
|
1782
|
+
return versions.length === 0 ? null : versions.reduce((a, b) => compareSemver(a, b) >= 0 ? a : b);
|
|
1783
|
+
}
|
|
1784
|
+
//#endregion
|
|
1785
|
+
//#region src/runtime/lifecycle/rules-root.ts
|
|
1786
|
+
/**
|
|
1787
|
+
* Dynamic rules-plugin root resolution. The historical `CLAUDE_PLUGIN_ROOT ??
|
|
1788
|
+
* cwd` chain only worked when the harness exported the plugin root — Kimi
|
|
1789
|
+
* injects `KIMI_PLUGIN_ROOT` instead, and a bare cwd fallback never held a
|
|
1790
|
+
* `rules/` dir. Resolution order (first hit wins):
|
|
1791
|
+
* 1. `CLAUDE_PLUGIN_ROOT` (claude-code/codex plugin-declared hooks);
|
|
1792
|
+
* 2. `KIMI_PLUGIN_ROOT` (kimi plugin-declared hooks);
|
|
1793
|
+
* 3. Per-harness install probe (claude marketplace, codex versioned cache,
|
|
1794
|
+
* kimi managed plugins) — first `<plugin>/rules` dir whose plugin folder
|
|
1795
|
+
* name contains "rules";
|
|
1796
|
+
* 4. `cwd` (historical fallback, unchanged behavior).
|
|
1797
|
+
*/
|
|
1798
|
+
/** Immediate child dir names of `dir`, or [] when unreadable. */
|
|
1799
|
+
function children$1(dir) {
|
|
1800
|
+
try {
|
|
1801
|
+
return readdirSync(dir);
|
|
1802
|
+
} catch {
|
|
1803
|
+
return [];
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
/** First existing `<base>/<child>/rules` where `child` names a rules plugin. */
|
|
1807
|
+
function probe(base, childFilter) {
|
|
1808
|
+
for (const child of children$1(base).sort()) {
|
|
1809
|
+
if (!childFilter(child)) continue;
|
|
1810
|
+
if (existsSync(join(base, child, "rules"))) return join(base, child);
|
|
1811
|
+
}
|
|
1812
|
+
return null;
|
|
1813
|
+
}
|
|
1814
|
+
/** Claude: `~/.claude/plugins/marketplaces/<marketplace>/plugins/<rules-plugin>/`. */
|
|
1815
|
+
function probeClaude(home) {
|
|
1816
|
+
const markets = join(home, ".claude", "plugins", "marketplaces");
|
|
1817
|
+
for (const m of children$1(markets).sort()) {
|
|
1818
|
+
const hit = probe(join(markets, m, "plugins"), (n) => n.includes("rules"));
|
|
1819
|
+
if (hit) return hit;
|
|
1820
|
+
}
|
|
1821
|
+
return null;
|
|
1822
|
+
}
|
|
1823
|
+
/** Codex: `~/.codex/plugins/cache/<marketplace>/<rules-plugin>/<version>/` (highest semver). */
|
|
1824
|
+
function probeCodex(home) {
|
|
1825
|
+
const cache = join(home, ".codex", "plugins", "cache");
|
|
1826
|
+
for (const m of children$1(cache).sort()) for (const p of children$1(join(cache, m)).sort()) {
|
|
1827
|
+
if (!p.includes("rules")) continue;
|
|
1828
|
+
const latest = maxSemver(children$1(join(cache, m, p)).filter((v) => existsSync(join(cache, m, p, v, "rules"))));
|
|
1829
|
+
if (latest) return join(cache, m, p, latest);
|
|
1830
|
+
}
|
|
1831
|
+
return null;
|
|
1832
|
+
}
|
|
1833
|
+
/** Kimi: `<KIMI_CODE_HOME>/plugins/managed/<rules-plugin>/` (managed install copy). */
|
|
1834
|
+
function probeKimi(env, home) {
|
|
1835
|
+
return probe(join(env.KIMI_CODE_HOME ?? join(home, ".kimi-code"), "plugins", "managed"), (n) => n.includes("rules"));
|
|
1836
|
+
}
|
|
1837
|
+
/**
|
|
1838
|
+
* Resolve the directory whose `rules/` subtree holds the `*.md` rule files.
|
|
1839
|
+
* @param id - Harness id (selects the install-layout probe).
|
|
1840
|
+
* @param cwd - Historical fallback root.
|
|
1841
|
+
* @param env - Environment (defaults to `process.env`).
|
|
1842
|
+
* @returns The plugin root to read `rules/` from.
|
|
1843
|
+
*/
|
|
1844
|
+
function resolveRulesRoot(id, cwd, env = process.env) {
|
|
1845
|
+
if (env.CLAUDE_PLUGIN_ROOT) return env.CLAUDE_PLUGIN_ROOT;
|
|
1846
|
+
if (env.KIMI_PLUGIN_ROOT) return env.KIMI_PLUGIN_ROOT;
|
|
1847
|
+
const home = env.HOME ?? homedir();
|
|
1848
|
+
return (id === "codex" ? probeCodex(home) : id === "kimi" ? probeKimi(env, home) : id === "claude-code" ? probeClaude(home) : null) ?? cwd;
|
|
1849
|
+
}
|
|
1850
|
+
//#endregion
|
|
1716
1851
|
//#region src/freshness/explore-tools.ts
|
|
1717
1852
|
/** Native exploration tools (parity `apex_constants.EXPLORE_TOOLS`). */
|
|
1718
1853
|
const EXPLORE_TOOLS = /* @__PURE__ */ new Set(["Glob", "Grep"]);
|
|
@@ -1809,6 +1944,29 @@ function classifyExplore(tool, input) {
|
|
|
1809
1944
|
return null;
|
|
1810
1945
|
}
|
|
1811
1946
|
//#endregion
|
|
1947
|
+
//#region src/runtime/is-agent-tool.ts
|
|
1948
|
+
/**
|
|
1949
|
+
* Sub-agent dispatch tool detection — the single source of truth for "this
|
|
1950
|
+
* tool call spawns a sub-agent". Claude Code and Codex dispatch via `Task`;
|
|
1951
|
+
* Kimi Code CLI dispatches via `Agent` (and `AgentSwarm` for parallel fan-out).
|
|
1952
|
+
* Centralized so the APEX task-context injection, the activity tracking and
|
|
1953
|
+
* the PostToolUse evidence credit stop drifting on inline `"Task" | "Agent"`
|
|
1954
|
+
* unions: every consumer imports from HERE, never re-declares the list.
|
|
1955
|
+
*/
|
|
1956
|
+
/** Canonical set of sub-agent dispatch tool names (the ONLY definition). */
|
|
1957
|
+
const AGENT_TOOLS = /* @__PURE__ */ new Set([
|
|
1958
|
+
"Task",
|
|
1959
|
+
"Agent",
|
|
1960
|
+
"AgentSwarm"
|
|
1961
|
+
]);
|
|
1962
|
+
/**
|
|
1963
|
+
* True when the tool is a sub-agent dispatch primitive on any harness.
|
|
1964
|
+
* @param tool - The normalized tool name from the hook payload.
|
|
1965
|
+
*/
|
|
1966
|
+
function isAgentTool(tool) {
|
|
1967
|
+
return AGENT_TOOLS.has(tool);
|
|
1968
|
+
}
|
|
1969
|
+
//#endregion
|
|
1812
1970
|
//#region src/freshness/agent-evidence-record.ts
|
|
1813
1971
|
/**
|
|
1814
1972
|
* Session-track agent evidence — writer (parity track-subagent-research.py) +
|
|
@@ -1818,17 +1976,6 @@ function classifyExplore(tool, input) {
|
|
|
1818
1976
|
* Workflow-spawned agents count, unlike the lead-transcript scan.
|
|
1819
1977
|
*/
|
|
1820
1978
|
/**
|
|
1821
|
-
* Tools the existing Task tracking already credits as agent LAUNCHES.
|
|
1822
|
-
* `AgentSwarm` is Kimi Code's batch launcher (`prompt_template` + `items`,
|
|
1823
|
-
* one `subagent_type` applied to every spawned sub-agent) — same launch
|
|
1824
|
-
* semantics as `Task`/`Agent`, so it is excluded here too (anti-double-count).
|
|
1825
|
-
*/
|
|
1826
|
-
const AGENT_LAUNCH_TOOLS = /* @__PURE__ */ new Set([
|
|
1827
|
-
"Task",
|
|
1828
|
-
"Agent",
|
|
1829
|
-
"AgentSwarm"
|
|
1830
|
-
]);
|
|
1831
|
-
/**
|
|
1832
1979
|
* `JSON.stringify` length of the raw `tool_response` OBJECT (parity Python
|
|
1833
1980
|
* `len(str(tool_response))`); 0 when absent or unserializable (circular).
|
|
1834
1981
|
*/
|
|
@@ -1853,7 +2000,7 @@ function responseJsonLength(toolResponse) {
|
|
|
1853
2000
|
* @returns The evidence to record, or null to skip.
|
|
1854
2001
|
*/
|
|
1855
2002
|
function classifyAgentEvidence(tool, input, toolResponse) {
|
|
1856
|
-
if (
|
|
2003
|
+
if (isAgentTool(tool)) return null;
|
|
1857
2004
|
const hit = classifyExplore(tool, input);
|
|
1858
2005
|
if (!hit) return null;
|
|
1859
2006
|
return {
|
|
@@ -1873,16 +2020,18 @@ function classifyAgentEvidence(tool, input, toolResponse) {
|
|
|
1873
2020
|
* @param agentId - Optional Claude `agent_id` — metadata tag only.
|
|
1874
2021
|
*/
|
|
1875
2022
|
async function recordAgentEvidence(file, evidence, ts, agentId) {
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
2023
|
+
await withTrack(file, (track) => {
|
|
2024
|
+
const next = recordAgent(track, evidence.name, ts, evidence.quality);
|
|
2025
|
+
const last = next.agents[next.agents.length - 1];
|
|
2026
|
+
if (agentId && last) {
|
|
2027
|
+
const tagged = {
|
|
2028
|
+
...last,
|
|
2029
|
+
agentId
|
|
2030
|
+
};
|
|
2031
|
+
next.agents[next.agents.length - 1] = tagged;
|
|
2032
|
+
}
|
|
2033
|
+
return next;
|
|
2034
|
+
});
|
|
1886
2035
|
}
|
|
1887
2036
|
/**
|
|
1888
2037
|
* Parity `_scan_agents` (apex_agent_helpers.py): reverse-scan `track.agents`,
|
|
@@ -1997,14 +2146,24 @@ function harvestSubagentTrack(payload, cwd, now, baseDir = defaultStateDir(cwd))
|
|
|
1997
2146
|
const transcriptPath = typeof payload.agent_transcript_path === "string" ? payload.agent_transcript_path : void 0;
|
|
1998
2147
|
if (!transcriptPath) return;
|
|
1999
2148
|
const file = trackFile(typeof payload.session_id === "string" ? payload.session_id : "unknown", baseDir);
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
const
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
}
|
|
2149
|
+
if (trackJournalEnabled()) {
|
|
2150
|
+
const track = readTrackSync(file, true);
|
|
2151
|
+
const next = harvestAgentEvidence(transcriptPath, track, now);
|
|
2152
|
+
if (next === track) return;
|
|
2153
|
+
const log = journalLogPath(file);
|
|
2154
|
+
for (const ev of diffTrackEvents(track, next, now)) appendEvent(log, ev.field, ev.op, ev.value, ev.ts);
|
|
2155
|
+
return;
|
|
2156
|
+
}
|
|
2157
|
+
if (withTrackLockSync(dirname(file), () => {
|
|
2158
|
+
const track = loadTrackSync(file);
|
|
2159
|
+
const next = harvestAgentEvidence(transcriptPath, track, now);
|
|
2160
|
+
if (next === track) return;
|
|
2161
|
+
try {
|
|
2162
|
+
const envelope = signTrack(next);
|
|
2163
|
+
atomicWrite(file, JSON.stringify(envelope, null, 2));
|
|
2164
|
+
writeLastNonce(envelope.nonce);
|
|
2165
|
+
} catch {}
|
|
2166
|
+
}) === LOCK_FAILED) return;
|
|
2008
2167
|
}
|
|
2009
2168
|
//#endregion
|
|
2010
2169
|
//#region src/cli/doctor.ts
|
|
@@ -2558,8 +2717,8 @@ function formatSummary(s) {
|
|
|
2558
2717
|
* The proprietary rule "NEVER propose the same fix twice" is prose a model under
|
|
2559
2718
|
* pressure ignores. This makes it machine-enforced: when a call whose
|
|
2560
2719
|
* `(tool + normalized input)` hash was ALREADY denied in-window is retried, the
|
|
2561
|
-
* harness keeps the deny but rewrites the message — `[REPEAT]` title,
|
|
2562
|
-
*
|
|
2720
|
+
* harness keeps the deny but rewrites the message — `[REPEAT]` title, a
|
|
2721
|
+
* mid-sentence STOP warning, forced `research-expert` action. State + wiring live in the sidecar
|
|
2563
2722
|
* store ({@link module:deny-loop-store}); this file is IO-free and pure.
|
|
2564
2723
|
* @packageDocumentation
|
|
2565
2724
|
*/
|
|
@@ -2624,7 +2783,7 @@ function denyLoopCheck(hash, priorDenies, opts) {
|
|
|
2624
2783
|
* @returns A block prompt with `[REPEAT]` title, STOP-prefixed reason, forced research action.
|
|
2625
2784
|
*/
|
|
2626
2785
|
function enrichRepeatDeny(prompt, count) {
|
|
2627
|
-
const stop = `Identical attempt #${count} already denied for the same reason.
|
|
2786
|
+
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. `;
|
|
2628
2787
|
const action = "Launch fuse-ai-pilot:research-expert to find a DIFFERENT approach";
|
|
2629
2788
|
return {
|
|
2630
2789
|
...prompt,
|
|
@@ -4002,7 +4161,7 @@ function persistCuration(file, root, curated, archive, original) {
|
|
|
4002
4161
|
* was this exact block silently reaching ~44k tokens. Any curation report
|
|
4003
4162
|
* surfaces via systemMessage.
|
|
4004
4163
|
*/
|
|
4005
|
-
function injectMemory(cwd, event, now) {
|
|
4164
|
+
function injectMemory(cwd, event, now, id) {
|
|
4006
4165
|
const root = projectRoot(cwd);
|
|
4007
4166
|
const file = lessonsFileFor(root);
|
|
4008
4167
|
if (!existsSync(file)) return "";
|
|
@@ -4015,8 +4174,7 @@ function injectMemory(cwd, event, now) {
|
|
|
4015
4174
|
if (!content) return "";
|
|
4016
4175
|
const { content: curated, archive, report } = curateLessons(content, now, root);
|
|
4017
4176
|
if (curated !== content) content = persistCuration(file, root, curated, archive, content);
|
|
4018
|
-
|
|
4019
|
-
return report ? attachSystemMessage(withNotice, `LESSON.md curation:\n${report}`) : withNotice;
|
|
4177
|
+
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");
|
|
4020
4178
|
}
|
|
4021
4179
|
/**
|
|
4022
4180
|
* Route a fuse-lessons event to its handler. Returns the native stdout for
|
|
@@ -4026,13 +4184,14 @@ function injectMemory(cwd, event, now) {
|
|
|
4026
4184
|
* @param payload - The raw hook payload.
|
|
4027
4185
|
* @param cwd - Project root for memory injection.
|
|
4028
4186
|
* @param now - Clock.
|
|
4187
|
+
* @param id - Harness target id (defaults to "claude-code" — zero-regression default).
|
|
4029
4188
|
* @returns The native stdout (possibly empty).
|
|
4030
4189
|
*/
|
|
4031
|
-
function dispatchLessons(event, payload, cwd, now) {
|
|
4190
|
+
function dispatchLessons(event, payload, cwd, now, id = "claude-code") {
|
|
4032
4191
|
switch (event) {
|
|
4033
4192
|
case "SessionStart":
|
|
4034
4193
|
case "SubagentStart":
|
|
4035
|
-
case "UserPromptSubmit": return injectMemory(cwd, event, now);
|
|
4194
|
+
case "UserPromptSubmit": return injectMemory(cwd, event, now, id);
|
|
4036
4195
|
case "Stop": return remindWrite(payload, now);
|
|
4037
4196
|
case "PostToolUse":
|
|
4038
4197
|
markWrite(payload, now);
|
|
@@ -4109,7 +4268,7 @@ async function injectApexSubagentContext(cwd, home = homedir(), id = "claude-cod
|
|
|
4109
4268
|
const apexDir = join(projectRoot, seg, "apex");
|
|
4110
4269
|
if (!existsSync(apexDir)) return "";
|
|
4111
4270
|
const agentsPath = join(apexDir, "AGENTS.md");
|
|
4112
|
-
const agents = existsSync(agentsPath) ? readText(agentsPath).slice(0, 4e3) : "";
|
|
4271
|
+
const agents = existsSync(agentsPath) ? readText$1(agentsPath).slice(0, 4e3) : "";
|
|
4113
4272
|
const taskData = await readJsonFile(join(apexDir, "task.json"));
|
|
4114
4273
|
const completed = taskData ? completedTasks(taskData.tasks) : "none";
|
|
4115
4274
|
const pending = taskData ? pendingTasks(taskData.tasks) : "none";
|
|
@@ -4164,7 +4323,7 @@ function cacheAge(ts, now = Date.now()) {
|
|
|
4164
4323
|
/** Full SHA-256 hex checksum of a file's text; "" when unreadable. */
|
|
4165
4324
|
async function fileChecksum(path) {
|
|
4166
4325
|
try {
|
|
4167
|
-
return createHash("sha256").update(readText(path)).digest("hex");
|
|
4326
|
+
return createHash("sha256").update(readText$1(path)).digest("hex");
|
|
4168
4327
|
} catch {
|
|
4169
4328
|
return "";
|
|
4170
4329
|
}
|
|
@@ -4234,7 +4393,7 @@ async function cacheAnalyticsSave(home = homedir(), now = Date.now()) {
|
|
|
4234
4393
|
const dir = join(cacheBaseDir(home), "analytics");
|
|
4235
4394
|
const sessionsFile = join(dir, "sessions.jsonl");
|
|
4236
4395
|
if (!pathExists(sessionsFile)) return;
|
|
4237
|
-
const raw = readText(sessionsFile);
|
|
4396
|
+
const raw = readText$1(sessionsFile);
|
|
4238
4397
|
if (!raw.trim()) return;
|
|
4239
4398
|
const entries = parseEntries(raw);
|
|
4240
4399
|
if (entries.length === 0) return;
|
|
@@ -4316,7 +4475,7 @@ async function injectExploreCache(cwd, home = homedir(), now = Date.now()) {
|
|
|
4316
4475
|
const cfgHash = await configHash(projPath);
|
|
4317
4476
|
let context = "";
|
|
4318
4477
|
const meta = await readJsonFile(metaFile);
|
|
4319
|
-
const snapshot = pathExists(snapFile) ? readText(snapFile) : "";
|
|
4478
|
+
const snapshot = pathExists(snapFile) ? readText$1(snapFile) : "";
|
|
4320
4479
|
if (meta?.timestamp && snapshot) {
|
|
4321
4480
|
const age = cacheAge(meta.timestamp, now);
|
|
4322
4481
|
if (age < TTL_SECONDS$1 && meta.config_hash === cfgHash) {
|
|
@@ -4353,7 +4512,7 @@ async function buildDocsContext(entries, docsDir, now) {
|
|
|
4353
4512
|
seen.add(entry.hash);
|
|
4354
4513
|
const docPath = join(docsDir, `${entry.hash}.md`);
|
|
4355
4514
|
if (!pathExists(docPath)) continue;
|
|
4356
|
-
const content = readText(docPath);
|
|
4515
|
+
const content = readText$1(docPath);
|
|
4357
4516
|
if (!content) continue;
|
|
4358
4517
|
ctx += `\n${content}\n`;
|
|
4359
4518
|
count++;
|
|
@@ -4646,7 +4805,7 @@ function projectRootFromPaths(filePaths) {
|
|
|
4646
4805
|
}
|
|
4647
4806
|
/** Extract absolute file paths from tool_use entries in a JSONL transcript, optionally restricted to `toolNames` (default: any tool). */
|
|
4648
4807
|
async function transcriptFilePaths(transcriptPath, toolNames) {
|
|
4649
|
-
const text = readText(transcriptPath);
|
|
4808
|
+
const text = readText$1(transcriptPath);
|
|
4650
4809
|
const paths = /* @__PURE__ */ new Set();
|
|
4651
4810
|
for (const line of text.split("\n").filter(Boolean)) try {
|
|
4652
4811
|
const content = JSON.parse(line)?.message?.content;
|
|
@@ -4662,7 +4821,7 @@ async function transcriptFilePaths(transcriptPath, toolNames) {
|
|
|
4662
4821
|
}
|
|
4663
4822
|
/** Extract deduplicated Edit tool_use entries (keyed by basename) from a transcript. */
|
|
4664
4823
|
async function transcriptEdits(transcriptPath) {
|
|
4665
|
-
const text = readText(transcriptPath);
|
|
4824
|
+
const text = readText$1(transcriptPath);
|
|
4666
4825
|
const edits = [];
|
|
4667
4826
|
for (const line of text.split("\n").filter(Boolean)) try {
|
|
4668
4827
|
const content = JSON.parse(line)?.message?.content;
|
|
@@ -4679,7 +4838,7 @@ async function transcriptEdits(transcriptPath) {
|
|
|
4679
4838
|
}
|
|
4680
4839
|
/** Extract the last assistant text report (first 500 lines) from a transcript. */
|
|
4681
4840
|
async function transcriptReport(transcriptPath) {
|
|
4682
|
-
const text = readText(transcriptPath);
|
|
4841
|
+
const text = readText$1(transcriptPath);
|
|
4683
4842
|
let lastReport = "";
|
|
4684
4843
|
for (const line of text.split("\n").filter(Boolean)) try {
|
|
4685
4844
|
const entry = JSON.parse(line);
|
|
@@ -4707,7 +4866,7 @@ const RETRY_DELAYS = [
|
|
|
4707
4866
|
];
|
|
4708
4867
|
/** Extract the longest assistant synthesis + queried library ids from a transcript. */
|
|
4709
4868
|
async function extractSynthesis(path) {
|
|
4710
|
-
const lines = readText(path).split("\n").filter(Boolean);
|
|
4869
|
+
const lines = readText$1(path).split("\n").filter(Boolean);
|
|
4711
4870
|
const libraries = [];
|
|
4712
4871
|
let synthesis = "";
|
|
4713
4872
|
for (const line of lines) try {
|
|
@@ -4915,7 +5074,7 @@ async function cacheSniperLessons(transcript, cwd, home = homedir()) {
|
|
|
4915
5074
|
*/
|
|
4916
5075
|
/** Extract linter-related command/output text from a JSONL transcript. */
|
|
4917
5076
|
async function extractLinterOutput(path) {
|
|
4918
|
-
const text = readText(path);
|
|
5077
|
+
const text = readText$1(path);
|
|
4919
5078
|
const outputs = [];
|
|
4920
5079
|
for (const line of text.split("\n").filter(Boolean)) try {
|
|
4921
5080
|
const content = JSON.parse(line)?.message?.content;
|
|
@@ -5167,8 +5326,8 @@ async function docCacheGate(payload, cwd, now = Date.now(), home = homedir()) {
|
|
|
5167
5326
|
* Ports `check-solid-compliance.py`.
|
|
5168
5327
|
*/
|
|
5169
5328
|
const CODE_EXTENSIONS$1 = /\.(ts|tsx|js|jsx|py|php|swift|go|rs|rb|java|astro)$/;
|
|
5170
|
-
const COMPONENT_DIR =
|
|
5171
|
-
const BACKEND_DIR =
|
|
5329
|
+
const COMPONENT_DIR = /\/(components|pages|views)\//;
|
|
5330
|
+
const BACKEND_DIR = /\/(Controllers|Models|Services)\//;
|
|
5172
5331
|
const TS_INTERFACE_RE = /^(export )?(interface|type) [A-Z]/m;
|
|
5173
5332
|
const PHP_INTERFACE_RE = /^interface /m;
|
|
5174
5333
|
/** Count non-empty, non-comment (`//`/`#`/`*`-prefixed) lines. Shared with {@link checkSolidFromTranscript}. */
|
|
@@ -5192,9 +5351,10 @@ function checkSolidCompliance(payload) {
|
|
|
5192
5351
|
const input = payload.tool_input ?? {};
|
|
5193
5352
|
const filePath = typeof input.file_path === "string" ? input.file_path : "";
|
|
5194
5353
|
if (!filePath || !CODE_EXTENSIONS$1.test(filePath) || !pathExists(filePath)) return "";
|
|
5354
|
+
if (isVendorPath(filePath)) return "";
|
|
5195
5355
|
let content;
|
|
5196
5356
|
try {
|
|
5197
|
-
content = readText(filePath);
|
|
5357
|
+
content = readText$1(filePath);
|
|
5198
5358
|
} catch {
|
|
5199
5359
|
return "";
|
|
5200
5360
|
}
|
|
@@ -5204,15 +5364,64 @@ function checkSolidCompliance(payload) {
|
|
|
5204
5364
|
const lc = countCodeLines(content);
|
|
5205
5365
|
if (lc > max) violations.push(`FILE SIZE: ${lc} lines (max: ${max})`);
|
|
5206
5366
|
else if (lc > split) violations.push(`FILE SIZE WARNING: ${lc} lines (split at ${split})`);
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5367
|
+
const masked = maskCommentsAndStrings(content, filePath.endsWith(".php") ? "php" : filePath.endsWith(".py") ? "py" : "c");
|
|
5368
|
+
if (COMPONENT_DIR.test(filePath) && /\.(ts|tsx|js|jsx|astro)$/.test(filePath)) {
|
|
5369
|
+
if (TS_INTERFACE_RE.test(masked)) violations.push("INTERFACE LOCATION: Move to src/interfaces/");
|
|
5370
|
+
} else if (BACKEND_DIR.test(filePath) && filePath.endsWith(".php")) {
|
|
5371
|
+
if (PHP_INTERFACE_RE.test(masked)) violations.push("INTERFACE LOCATION: Move to app/Contracts/");
|
|
5211
5372
|
}
|
|
5212
5373
|
if (violations.length === 0) return "";
|
|
5213
5374
|
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.`);
|
|
5214
5375
|
}
|
|
5215
5376
|
//#endregion
|
|
5377
|
+
//#region src/runtime/lifecycle/aipilot/solid-notice.ts
|
|
5378
|
+
/**
|
|
5379
|
+
* SubagentStop SOLID-notice dedup (anti-loop): `checkSolidFromTranscript` is
|
|
5380
|
+
* stateless, so an unchanged violation set re-emitted the same
|
|
5381
|
+
* `additionalContext` on EVERY SubagentStop — re-invoking the agent in an
|
|
5382
|
+
* observed ~15-repeat loop. The dedup fingerprint is a SHA-256 hash of the
|
|
5383
|
+
* sorted `file:message` set, persisted per agent key in the canonical
|
|
5384
|
+
* `.harness/track/solid-notice.json` sidecar (`config/layout.ts`), written
|
|
5385
|
+
* under the synchronous track lock (this hook runs in an ephemeral sync
|
|
5386
|
+
* process). Semantics: identical fingerprint within the TTL → silence;
|
|
5387
|
+
* changed/absent/expired fingerprint → emit once and record (the TTL derives
|
|
5388
|
+
* from `FUSE_ENFORCE_TTL_SEC`, no new env var). On lock contention the notice
|
|
5389
|
+
* is emitted anyway (visibility beats a muted violation) but not recorded.
|
|
5390
|
+
*/
|
|
5391
|
+
/** SHA-256 (truncated) of the sorted violation set — the dedup fingerprint. */
|
|
5392
|
+
function noticeFingerprint(violations) {
|
|
5393
|
+
return createHash("sha256").update([...violations].sort().join("\n")).digest("hex").slice(0, 16);
|
|
5394
|
+
}
|
|
5395
|
+
/**
|
|
5396
|
+
* Decide whether the notice for `key` may be emitted, recording it when so.
|
|
5397
|
+
* @param noticeFile - `projectLayout(cwd).solidNoticeFile`.
|
|
5398
|
+
* @param key - Agent/transcript identity (e.g. `agent_transcript_path`).
|
|
5399
|
+
* @param hash - The current set's fingerprint ({@link noticeFingerprint}).
|
|
5400
|
+
* @param now - Clock (epoch ms).
|
|
5401
|
+
* @param ttlMs - Reminder window (derived from `FUSE_ENFORCE_TTL_SEC`).
|
|
5402
|
+
*/
|
|
5403
|
+
function shouldEmitNotice(noticeFile, key, hash, now, ttlMs) {
|
|
5404
|
+
let state = {};
|
|
5405
|
+
try {
|
|
5406
|
+
state = JSON.parse(readFileSync(noticeFile, "utf8"));
|
|
5407
|
+
} catch {}
|
|
5408
|
+
const prev = state[key];
|
|
5409
|
+
if (prev && prev.hash === hash && now - prev.at < ttlMs) return false;
|
|
5410
|
+
const next = {
|
|
5411
|
+
...state,
|
|
5412
|
+
[key]: {
|
|
5413
|
+
hash,
|
|
5414
|
+
at: now
|
|
5415
|
+
}
|
|
5416
|
+
};
|
|
5417
|
+
withTrackLockSync(dirname(noticeFile), () => {
|
|
5418
|
+
try {
|
|
5419
|
+
atomicWrite(noticeFile, JSON.stringify(next));
|
|
5420
|
+
} catch {}
|
|
5421
|
+
});
|
|
5422
|
+
return true;
|
|
5423
|
+
}
|
|
5424
|
+
//#endregion
|
|
5216
5425
|
//#region src/runtime/lifecycle/aipilot/solid-transcript.ts
|
|
5217
5426
|
/**
|
|
5218
5427
|
* SubagentStop (matcher "") for the ai-pilot scope: check SOLID file-size and
|
|
@@ -5222,19 +5431,22 @@ function checkSolidCompliance(payload) {
|
|
|
5222
5431
|
*/
|
|
5223
5432
|
const CODE_EXTENSIONS = /\.(ts|tsx|js|jsx|py|go|rs|java|php|cpp|c|rb|swift|kt|dart|vue|svelte|astro)$/;
|
|
5224
5433
|
const INTERFACE_PATTERN = /^(export )?(interface|type) [A-Z]/m;
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
];
|
|
5434
|
+
/** Segment-anchored dirs (a substring like `app/` matched `livetest-app/`). */
|
|
5435
|
+
const INTERFACE_DIRS_RE = /\/(components|pages|views|app)\//;
|
|
5436
|
+
/** Canonical homes — never a violation there (owner decision: types/ is
|
|
5437
|
+
* canonical; Contracts/ is the Laravel home taught by solid-php/laravelGate). */
|
|
5438
|
+
const INTERFACE_EXEMPT_RE = /\/(interfaces|types|Contracts|contracts)\//;
|
|
5231
5439
|
/**
|
|
5232
5440
|
* Check every Write/Edit target in a subagent transcript against the SOLID
|
|
5233
|
-
* line-count ceiling and interface-location convention.
|
|
5441
|
+
* line-count ceiling and interface-location convention. Deduped per
|
|
5442
|
+
* transcript (`solid-notice.ts`): an unchanged violation set is emitted once
|
|
5443
|
+
* per TTL window, never in a loop.
|
|
5234
5444
|
* @param transcript - Path to the agent JSONL transcript (`agent_transcript_path`).
|
|
5235
|
-
* @
|
|
5445
|
+
* @param cwd - Project root (locates the `.harness/track` dedup sidecar).
|
|
5446
|
+
* @param now - Clock (tests inject it).
|
|
5447
|
+
* @returns The SubagentStop `additionalContext` response, or `""` when clean/deduped.
|
|
5236
5448
|
*/
|
|
5237
|
-
async function checkSolidFromTranscript(transcript) {
|
|
5449
|
+
async function checkSolidFromTranscript(transcript, cwd = process.cwd(), now = Date.now()) {
|
|
5238
5450
|
if (!transcript || !pathExists(transcript)) return "";
|
|
5239
5451
|
const files = await transcriptFilePaths(transcript, ["Write", "Edit"]);
|
|
5240
5452
|
const max = resolveMaxLines();
|
|
@@ -5244,15 +5456,19 @@ async function checkSolidFromTranscript(transcript) {
|
|
|
5244
5456
|
const name = fp.split("/").pop() ?? fp;
|
|
5245
5457
|
let content;
|
|
5246
5458
|
try {
|
|
5247
|
-
content = readText(fp);
|
|
5459
|
+
content = readText$1(fp);
|
|
5248
5460
|
} catch {
|
|
5249
5461
|
continue;
|
|
5250
5462
|
}
|
|
5251
5463
|
const lc = countCodeLines(content);
|
|
5252
5464
|
if (lc > max) violations.push(`SOLID: ${name} = ${lc} lines (max ${max})`);
|
|
5253
|
-
if (
|
|
5465
|
+
if (INTERFACE_DIRS_RE.test(fp) && !INTERFACE_EXEMPT_RE.test(fp) && INTERFACE_PATTERN.test(content)) {
|
|
5466
|
+
const dest = fp.endsWith(".php") ? "app/Contracts/" : "interfaces/";
|
|
5467
|
+
violations.push(`SOLID: ${name}: move interfaces to ${dest}`);
|
|
5468
|
+
}
|
|
5254
5469
|
}
|
|
5255
5470
|
if (violations.length === 0) return "";
|
|
5471
|
+
if (!shouldEmitNotice(projectLayout(cwd).solidNoticeFile, transcript, noticeFingerprint(violations), now, resolveTtlSec(process.env) * 1e3)) return "";
|
|
5256
5472
|
return contextResponse("SubagentStop", `## SOLID VIOLATIONS DETECTED (subagent output)\n${violations.join("\n")}\nRun sniper to fix these issues.`);
|
|
5257
5473
|
}
|
|
5258
5474
|
//#endregion
|
|
@@ -5312,7 +5528,7 @@ async function onSubagentStop(payload, cwd, home) {
|
|
|
5312
5528
|
await cacheSniperLessons(transcript, cwd, home);
|
|
5313
5529
|
await cacheTestResults(transcript, cwd, home);
|
|
5314
5530
|
}
|
|
5315
|
-
return checkSolidFromTranscript(transcript);
|
|
5531
|
+
return checkSolidFromTranscript(transcript, cwd);
|
|
5316
5532
|
}
|
|
5317
5533
|
/**
|
|
5318
5534
|
* Dispatch an ai-pilot-scope lifecycle event. Returns the native stdout, or
|
|
@@ -5338,9 +5554,9 @@ async function aipilotPostToolUse(payload, cwd, id = "claude-code") {
|
|
|
5338
5554
|
/** SessionStart handler keyed on plugin scope. */
|
|
5339
5555
|
function sessionStart(input) {
|
|
5340
5556
|
if (input.scope === "solid") return solidDetectStart();
|
|
5341
|
-
if (input.scope === "rules") return injectRules(
|
|
5557
|
+
if (input.scope === "rules") return injectRules(resolveRulesRoot(input.id ?? "claude-code", input.cwd), input.event, input.id ?? "claude-code");
|
|
5342
5558
|
if (input.scope === "carto") return cartoSessionStart(input.cwd, input.now);
|
|
5343
|
-
if (input.scope === "lessons") return dispatchLessons("SessionStart", input.payload, input.cwd, input.now);
|
|
5559
|
+
if (input.scope === "lessons") return dispatchLessons("SessionStart", input.payload, input.cwd, input.now, input.id ?? "claude-code");
|
|
5344
5560
|
const core = sessionStartCore(input.cwd, void 0, input.now, input.id ?? "claude-code");
|
|
5345
5561
|
return input.scope === "core" ? withSnapshot(core, input.cwd, import.meta.url) : core;
|
|
5346
5562
|
}
|
|
@@ -5355,16 +5571,16 @@ function dispatchLifecycle(input) {
|
|
|
5355
5571
|
switch (input.event) {
|
|
5356
5572
|
case "SessionStart": return sessionStart(input);
|
|
5357
5573
|
case "UserPromptSubmit":
|
|
5358
|
-
if (input.scope === "rules") return injectRules(
|
|
5359
|
-
if (input.scope === "lessons") return dispatchLessons("UserPromptSubmit", input.payload, input.cwd, input.now);
|
|
5574
|
+
if (input.scope === "rules") return injectRules(resolveRulesRoot(input.id ?? "claude-code", input.cwd), input.event, input.id ?? "claude-code");
|
|
5575
|
+
if (input.scope === "lessons") return dispatchLessons("UserPromptSubmit", input.payload, input.cwd, input.now, input.id ?? "claude-code");
|
|
5360
5576
|
return null;
|
|
5361
5577
|
case "SubagentStart":
|
|
5362
|
-
if (input.scope === "rules") return injectRules(
|
|
5578
|
+
if (input.scope === "rules") return injectRules(resolveRulesRoot(input.id ?? "claude-code", input.cwd), input.event, input.id ?? "claude-code");
|
|
5363
5579
|
if (input.scope === "aipilot") return "";
|
|
5364
|
-
if (input.scope === "lessons") return dispatchLessons("SubagentStart", input.payload, input.cwd, input.now);
|
|
5580
|
+
if (input.scope === "lessons") return dispatchLessons("SubagentStart", input.payload, input.cwd, input.now, input.id ?? "claude-code");
|
|
5365
5581
|
return subagentCacheContext(input.payload.session_id);
|
|
5366
5582
|
case "Stop":
|
|
5367
|
-
if (input.scope === "lessons") return dispatchLessons("Stop", input.payload, input.cwd, input.now);
|
|
5583
|
+
if (input.scope === "lessons") return dispatchLessons("Stop", input.payload, input.cwd, input.now, input.id ?? "claude-code");
|
|
5368
5584
|
return input.scope === "core" ? stopCore(input.payload, input.cwd, input.now) : null;
|
|
5369
5585
|
case "SubagentStop":
|
|
5370
5586
|
if (input.scope === "aipilot") return "";
|
|
@@ -6370,7 +6586,11 @@ function nextModular(filePath, content, cwd) {
|
|
|
6370
6586
|
}
|
|
6371
6587
|
/** Laravel FuseCore architecture: `app/` domain ban + module.json + cross-module `use` rules. */
|
|
6372
6588
|
function fusecore(filePath, content, cwd) {
|
|
6373
|
-
for (const b of PHP_BLOCKED_IN_APP) if (filePath.includes(b))
|
|
6589
|
+
for (const b of PHP_BLOCKED_IN_APP) if (filePath.includes(b)) {
|
|
6590
|
+
const from = b.replace(/^\/|\/$/g, "");
|
|
6591
|
+
const to = b.replaceAll("/app/", "/FuseCore/{Module}/App/").replace(/^\/|\/$/g, "");
|
|
6592
|
+
return block$1(`BLOCKED: FuseCore project. Code in '${from}' FORBIDDEN. Move to '${to}'.`);
|
|
6593
|
+
}
|
|
6374
6594
|
const mod = filePath.match(/\/FuseCore\/([A-Za-z]+)\//);
|
|
6375
6595
|
if (!mod) return null;
|
|
6376
6596
|
const name = mod[1] ?? "";
|
|
@@ -6561,7 +6781,7 @@ function appendRefRead(dir, path, ts) {
|
|
|
6561
6781
|
function reconcileRefReadsFromJournal(track, dir, now) {
|
|
6562
6782
|
let text;
|
|
6563
6783
|
try {
|
|
6564
|
-
text = readText(join(dir, JOURNAL));
|
|
6784
|
+
text = readText$1(join(dir, JOURNAL));
|
|
6565
6785
|
} catch {
|
|
6566
6786
|
return track;
|
|
6567
6787
|
}
|
|
@@ -6764,10 +6984,9 @@ function geminiMcpGate(tool, filePath, content, ev) {
|
|
|
6764
6984
|
//#endregion
|
|
6765
6985
|
//#region src/freshness/agent-evidence.ts
|
|
6766
6986
|
/**
|
|
6767
|
-
* Platform-authored transcript evidence for APEX agent freshness.
|
|
6768
|
-
*
|
|
6769
|
-
*
|
|
6770
|
-
* which the Claude Code platform controls, unlike the self-recorded track.
|
|
6987
|
+
* Platform-authored transcript evidence for APEX agent freshness. Parses the
|
|
6988
|
+
* Claude Code session JSONL transcript for genuine dispatch tool_use entries
|
|
6989
|
+
* — forging requires writing into the platform-controlled transcript file.
|
|
6771
6990
|
*/
|
|
6772
6991
|
/** Parse a raw `timestamp` field to epoch ms; `undefined` when absent or invalid. */
|
|
6773
6992
|
function parseTs(raw) {
|
|
@@ -6777,23 +6996,14 @@ function parseTs(raw) {
|
|
|
6777
6996
|
return Number.isFinite(ms) ? ms : void 0;
|
|
6778
6997
|
}
|
|
6779
6998
|
/**
|
|
6780
|
-
* Return `true` ONLY when, for EVERY name in `names`, the
|
|
6781
|
-
*
|
|
6782
|
-
*
|
|
6783
|
-
*
|
|
6784
|
-
*
|
|
6785
|
-
*
|
|
6786
|
-
*
|
|
6787
|
-
* `
|
|
6788
|
-
*
|
|
6789
|
-
* **Timestamp note:** when a transcript entry carries no `timestamp` field it
|
|
6790
|
-
* is counted as within-window (we cannot prove staleness). This is
|
|
6791
|
-
* intentionally lenient to stay robust across transcript-format evolution; the
|
|
6792
|
-
* tamper-resistance guarantee derives from the platform authoring the file —
|
|
6793
|
-
* not from the timestamp alone.
|
|
6794
|
-
*
|
|
6795
|
-
* @param transcriptPath - Absolute path to the session `.jsonl` transcript
|
|
6796
|
-
* (hook payload field: `transcript_path`). Returns `false` when `undefined`.
|
|
6999
|
+
* Return `true` ONLY when, for EVERY name in `names`, the transcript at
|
|
7000
|
+
* `transcriptPath` shows a within-window genuine `tool_use`: a dispatch tool
|
|
7001
|
+
* ({@link isAgentTool}) whose `subagent_type`/`name` (plugin prefix stripped)
|
|
7002
|
+
* matches, OR a direct exploration/research call classified into that name by
|
|
7003
|
+
* {@link classifyExplore} — issued by any sub-agent or the lead (the transcript
|
|
7004
|
+
* carries no author field). Entries without `timestamp` count as within-window
|
|
7005
|
+
* (format-evolution leniency; tamper-resistance comes from platform authoring).
|
|
7006
|
+
* @param transcriptPath - Session `.jsonl` transcript (`transcript_path`); false when undefined.
|
|
6797
7007
|
* @param names - Required agent `subagent_type` values — ALL must appear.
|
|
6798
7008
|
* @param windowMs - Freshness window in milliseconds.
|
|
6799
7009
|
* @param now - Current epoch ms (pass `Date.now()` at the call-site).
|
|
@@ -6803,7 +7013,7 @@ function agentsRanFromTranscript(transcriptPath, names, windowMs, now) {
|
|
|
6803
7013
|
if (!transcriptPath || names.length === 0) return false;
|
|
6804
7014
|
let text;
|
|
6805
7015
|
try {
|
|
6806
|
-
text = readText(transcriptPath);
|
|
7016
|
+
text = readText$1(transcriptPath);
|
|
6807
7017
|
} catch {
|
|
6808
7018
|
return false;
|
|
6809
7019
|
}
|
|
@@ -6823,7 +7033,7 @@ function agentsRanFromTranscript(transcriptPath, names, windowMs, now) {
|
|
|
6823
7033
|
if (!Array.isArray(content)) continue;
|
|
6824
7034
|
for (const block of content) {
|
|
6825
7035
|
if (block?.type !== "tool_use") continue;
|
|
6826
|
-
if (block.name
|
|
7036
|
+
if (block.name !== void 0 && isAgentTool(block.name)) {
|
|
6827
7037
|
const raw = block.input?.subagent_type ?? block.input?.name;
|
|
6828
7038
|
const agent = typeof raw === "string" ? raw.split(":").pop() ?? raw : void 0;
|
|
6829
7039
|
if (agent !== void 0 && names.includes(agent)) found.add(agent);
|
|
@@ -6857,7 +7067,7 @@ async function apexScopedGate(input, track, window) {
|
|
|
6857
7067
|
if (protectedDeny) return protectedDeny;
|
|
6858
7068
|
if (SKIP_DIRS.test(input.filePath ?? "")) return null;
|
|
6859
7069
|
if (isTrivialEdit(input.tool, input.content, input.isReplaceAll) && trivialCount(track, window, input.now) < 4) {
|
|
6860
|
-
await
|
|
7070
|
+
await withTrack(input.trackFile, (fresh) => recordTrivialEdit(fresh, input.now, window, input.now));
|
|
6861
7071
|
return null;
|
|
6862
7072
|
}
|
|
6863
7073
|
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));
|
|
@@ -6883,7 +7093,7 @@ async function apexScopedGate(input, track, window) {
|
|
|
6883
7093
|
const authDeny = apexAuthorizationGate(ctx);
|
|
6884
7094
|
if (authDeny) {
|
|
6885
7095
|
const set_at = new Date(input.now).toISOString();
|
|
6886
|
-
await
|
|
7096
|
+
await withTrack(input.trackFile, (fresh) => recordTarget(fresh, {
|
|
6887
7097
|
project: input.cwd ?? "",
|
|
6888
7098
|
framework: input.framework,
|
|
6889
7099
|
set_by: "apex-authorization",
|
|
@@ -7121,6 +7331,7 @@ function denyWith(id, content) {
|
|
|
7121
7331
|
decision: "deny",
|
|
7122
7332
|
reason: content
|
|
7123
7333
|
});
|
|
7334
|
+
if (id === "kimi") return kimiDenyResponse(content);
|
|
7124
7335
|
return "";
|
|
7125
7336
|
}
|
|
7126
7337
|
function mutateWith(id, input) {
|
|
@@ -7189,8 +7400,7 @@ function mcpPostStore(tool, input, response, dir, now = Date.now()) {
|
|
|
7189
7400
|
//#region src/runtime/record.ts
|
|
7190
7401
|
/** Apply an activity to a session's track and persist it (PostToolUse path). */
|
|
7191
7402
|
async function recordActivity(file, activity) {
|
|
7192
|
-
|
|
7193
|
-
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));
|
|
7403
|
+
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));
|
|
7194
7404
|
if (activity.kind === "ref") appendRefRead(dirname(file), activity.path, activity.ts ?? Date.now());
|
|
7195
7405
|
}
|
|
7196
7406
|
//#endregion
|
|
@@ -7485,22 +7695,16 @@ function uiDesignSkillGate(tool, filePath, content, ev) {
|
|
|
7485
7695
|
if (!isUiWrite(tool, filePath, content)) return null;
|
|
7486
7696
|
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");
|
|
7487
7697
|
const missing = missingDomainSkills(content, ev.refsRead);
|
|
7488
|
-
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`);
|
|
7698
|
+
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`);
|
|
7489
7699
|
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");
|
|
7490
7700
|
return null;
|
|
7491
7701
|
}
|
|
7492
7702
|
//#endregion
|
|
7493
7703
|
//#region src/policy/design/skill-evidence.ts
|
|
7494
7704
|
/**
|
|
7495
|
-
*
|
|
7496
|
-
*
|
|
7497
|
-
*
|
|
7498
|
-
* {@link uiDesignSkillGate}. Kept separate from the pure gate logic (SRP).
|
|
7499
|
-
* @packageDocumentation
|
|
7500
|
-
*/
|
|
7501
|
-
/**
|
|
7502
|
-
* Read the verified session track and derive {@link DesignEvidence}. Fail-closed
|
|
7503
|
-
* on a missing/corrupt/tampered track (no evidence → the gate blocks).
|
|
7705
|
+
* Read the verified session track (snapshot ⊕ journal) and derive
|
|
7706
|
+
* {@link DesignEvidence}. Fail-closed on a missing/corrupt/tampered track (no
|
|
7707
|
+
* evidence → the gate blocks).
|
|
7504
7708
|
* @param sessionId - the Claude session id.
|
|
7505
7709
|
* @param cwd - the project root (selects the per-project state dir).
|
|
7506
7710
|
* @param baseDir - override the track base dir (tests); defaults to the project state dir.
|
|
@@ -7508,7 +7712,7 @@ function uiDesignSkillGate(tool, filePath, content, ev) {
|
|
|
7508
7712
|
function collectDesignEvidence(sessionId, cwd, baseDir = defaultStateDir(cwd)) {
|
|
7509
7713
|
let track = null;
|
|
7510
7714
|
try {
|
|
7511
|
-
track =
|
|
7715
|
+
track = readTrackSync(trackFile(sessionId, baseDir), trackJournalEnabled());
|
|
7512
7716
|
} catch {
|
|
7513
7717
|
track = null;
|
|
7514
7718
|
}
|
|
@@ -7755,6 +7959,201 @@ function designGate(payload, event, cacheDir, cwd) {
|
|
|
7755
7959
|
return null;
|
|
7756
7960
|
}
|
|
7757
7961
|
//#endregion
|
|
7962
|
+
//#region src/runtime/solid-file-size-gate.ts
|
|
7963
|
+
/**
|
|
7964
|
+
* PreToolUse file-size deny for the solid scope — the same SOLID ceiling the
|
|
7965
|
+
* core gate chain enforces (`policy/evaluate.ts`), extracted so
|
|
7966
|
+
* `harness hook <id> solid` blocks oversized Write/Edit WITHOUT the core-only
|
|
7967
|
+
* APEX freshness gates (they require explore/research agents that not every
|
|
7968
|
+
* harness runs). Pure and read-only (on-disk read via {@link existingLineCounts},
|
|
7969
|
+
* no session state), so the sibling-plugin fan-out stays idempotent.
|
|
7970
|
+
*/
|
|
7971
|
+
/**
|
|
7972
|
+
* Deny a Write/Edit whose resulting file would exceed the SOLID line ceiling.
|
|
7973
|
+
* Mirrors `policy/evaluate.ts` exactly: a computable Edit outcome passes when
|
|
7974
|
+
* the result is compliant OR strictly shrinks an already-oversized file; the
|
|
7975
|
+
* block message reports the pre-existing on-disk count for a Write.
|
|
7976
|
+
* @param tool - The tool name (only "Write"/"Edit" are checked).
|
|
7977
|
+
* @param filePath - The written file's absolute path.
|
|
7978
|
+
* @param content - Write: the full new content; Edit: the `new_string` snippet.
|
|
7979
|
+
* @param oldString - Edit only: the `old_string` being replaced.
|
|
7980
|
+
* @param isReplaceAll - Edit only: whether every occurrence is replaced.
|
|
7981
|
+
* @param agentType - Subagent type, when known (Explore/Plan are exempt).
|
|
7982
|
+
* @returns The blocking prompt, or null when compliant or out of scope.
|
|
7983
|
+
*/
|
|
7984
|
+
function solidFileSizeGate(tool, filePath, content, oldString, isReplaceAll = false, agentType) {
|
|
7985
|
+
if (tool !== "Write" && tool !== "Edit") return null;
|
|
7986
|
+
if (!filePath || !isFileSizeScoped(filePath)) return null;
|
|
7987
|
+
if (agentType === "Explore" || agentType === "Plan") return null;
|
|
7988
|
+
const { raw: existingLines, content: existingContent } = existingLineCounts(filePath);
|
|
7989
|
+
const incoming = content !== void 0 ? countLines(content) : 0;
|
|
7990
|
+
const existing = existingLines ?? 0;
|
|
7991
|
+
const max = resolveMaxLines();
|
|
7992
|
+
const editResult = tool === "Edit" && existingContent !== void 0 && content !== void 0 ? computeEditResultLines(existingContent, oldString, content, isReplaceAll) : null;
|
|
7993
|
+
if (editResult !== null && (editResult <= max || editResult < existing)) return null;
|
|
7994
|
+
const lines = tool === "Edit" ? editResult ?? Math.max(incoming, existing) : incoming || existing;
|
|
7995
|
+
const displayLines = tool === "Write" ? existingLines ?? lines : lines;
|
|
7996
|
+
const verdict = evaluateFileSize(lines, max, filePath, resolveSolidRefFramework(filePath), displayLines);
|
|
7997
|
+
if (lines === 0 || verdict.ok) return null;
|
|
7998
|
+
return {
|
|
7999
|
+
kind: "block",
|
|
8000
|
+
title: "SOLID file-size limit",
|
|
8001
|
+
reason: verdict.message ?? "",
|
|
8002
|
+
actions: [`Split into modules under ${verdict.max} lines (Single Responsibility)`, "Then re-run the write"]
|
|
8003
|
+
};
|
|
8004
|
+
}
|
|
8005
|
+
//#endregion
|
|
8006
|
+
//#region src/runtime/core-guards-active.ts
|
|
8007
|
+
/**
|
|
8008
|
+
* Per-harness ACTIVATION probing for the core-guards plugin, behind
|
|
8009
|
+
* {@link coreGuardsWired}. The owner decision: the solid scope abstains from
|
|
8010
|
+
* its file-size deny ONLY when core-guards is installed AND active — an
|
|
8011
|
+
* installed-but-disabled plugin must leave the solid deny firing. Safe
|
|
8012
|
+
* default, per harness: anything undetectable (unreadable/missing state)
|
|
8013
|
+
* reads as NOT active, so the worst case is a duplicate deny — never zero.
|
|
8014
|
+
*/
|
|
8015
|
+
/** File content, or null when unreadable (undetectable → caller's safe default). */
|
|
8016
|
+
function readText(path) {
|
|
8017
|
+
try {
|
|
8018
|
+
return readFileSync(path, "utf8");
|
|
8019
|
+
} catch {
|
|
8020
|
+
return null;
|
|
8021
|
+
}
|
|
8022
|
+
}
|
|
8023
|
+
/** First `core-guards@…` value in a name→bool map; undefined when absent. */
|
|
8024
|
+
function enabledEntry(map) {
|
|
8025
|
+
if (typeof map !== "object" || map === null || Array.isArray(map)) return void 0;
|
|
8026
|
+
for (const [k, v] of Object.entries(map)) if (k.startsWith("core-guards@")) return v !== false;
|
|
8027
|
+
}
|
|
8028
|
+
/**
|
|
8029
|
+
* Claude Code: `~/.claude/settings.json` → `enabledPlugins`. core-guards is a
|
|
8030
|
+
* REQUIRED marketplace plugin: no entry means active whenever the marketplace
|
|
8031
|
+
* copy exists (`installed`); an explicit `false` means disabled. Unreadable
|
|
8032
|
+
* settings → false (undetectable).
|
|
8033
|
+
* @param home - OS home.
|
|
8034
|
+
* @param installed - Whether the marketplace copy of core-guards exists.
|
|
8035
|
+
*/
|
|
8036
|
+
function claudeCoreGuardsActive(home, installed) {
|
|
8037
|
+
const text = readText(join(home, ".claude", "settings.json"));
|
|
8038
|
+
if (text === null) return false;
|
|
8039
|
+
try {
|
|
8040
|
+
return enabledEntry(JSON.parse(text).enabledPlugins) ?? installed;
|
|
8041
|
+
} catch {
|
|
8042
|
+
return false;
|
|
8043
|
+
}
|
|
8044
|
+
}
|
|
8045
|
+
/**
|
|
8046
|
+
* Codex: `[plugins."core-guards@<mkt>"] enabled = …` in `~/.codex/config.toml`
|
|
8047
|
+
* (minimal section-scoped extraction — no TOML dependency). Missing section,
|
|
8048
|
+
* `enabled = false`, or unreadable file → false.
|
|
8049
|
+
* @param home - OS home.
|
|
8050
|
+
*/
|
|
8051
|
+
function codexCoreGuardsActive(home) {
|
|
8052
|
+
const text = readText(join(home, ".codex", "config.toml"));
|
|
8053
|
+
if (text === null) return false;
|
|
8054
|
+
return /^\[plugins\."core-guards@[^"]+"\]\s*enabled\s*=\s*(true|false)/m.exec(text)?.[1] === "true";
|
|
8055
|
+
}
|
|
8056
|
+
/**
|
|
8057
|
+
* Kimi: `<KIMI_CODE_HOME>/plugins/installed.json` — map form (`id →
|
|
8058
|
+
* {enabled?}`) or array form (`[{id, enabled?}]`); an entry counts active
|
|
8059
|
+
* unless `enabled === false`. File absent here today → format read
|
|
8060
|
+
* tolerantly; missing/unreadable → false (undetectable).
|
|
8061
|
+
* @param root - Kimi data root (`KIMI_CODE_HOME` ?? `~/.kimi-code`).
|
|
8062
|
+
*/
|
|
8063
|
+
function kimiCoreGuardsActive(root) {
|
|
8064
|
+
const text = readText(join(root, "plugins", "installed.json"));
|
|
8065
|
+
if (text === null) return false;
|
|
8066
|
+
try {
|
|
8067
|
+
const data = JSON.parse(text).plugins;
|
|
8068
|
+
if (Array.isArray(data)) {
|
|
8069
|
+
const hit = data.find((p) => typeof p === "object" && p !== null && p.id === "core-guards");
|
|
8070
|
+
return hit !== void 0 && hit.enabled !== false;
|
|
8071
|
+
}
|
|
8072
|
+
if (typeof data !== "object" || data === null) return false;
|
|
8073
|
+
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;
|
|
8074
|
+
return false;
|
|
8075
|
+
} catch {
|
|
8076
|
+
return false;
|
|
8077
|
+
}
|
|
8078
|
+
}
|
|
8079
|
+
//#endregion
|
|
8080
|
+
//#region src/runtime/core-guards-wired.ts
|
|
8081
|
+
/**
|
|
8082
|
+
* Detect whether the core-guards plugin is installed AND ACTIVE for a harness.
|
|
8083
|
+
* When it is, its `core` scope already owns the PreToolUse file-size deny
|
|
8084
|
+
* (`policy/evaluate.ts`), so the `solid` scope MUST abstain on the same
|
|
8085
|
+
* tool-call — otherwise a user with BOTH plugins wired on PreToolUse
|
|
8086
|
+
* Write|Edit receives the identical deny twice. Activation probing is
|
|
8087
|
+
* per-harness (`core-guards-active.ts`, owner decision):
|
|
8088
|
+
* - claude-code: marketplace copy present + `settings.json` `enabledPlugins`
|
|
8089
|
+
* (required core: no entry = active; explicit `false` = disabled);
|
|
8090
|
+
* - codex: `[plugins."core-guards@…"] enabled = true` in `~/.codex/config.toml`;
|
|
8091
|
+
* - kimi: `<KIMI_CODE_HOME>/plugins/installed.json` entry not disabled.
|
|
8092
|
+
* SAFE DEFAULT: anything undetectable (unreadable/missing state) reads as
|
|
8093
|
+
* NOT active — the solid scope then fires its own deny, so the worst case is
|
|
8094
|
+
* a duplicate deny, NEVER zero deny.
|
|
8095
|
+
*/
|
|
8096
|
+
/** Immediate child dir names of `dir`, or [] when unreadable. */
|
|
8097
|
+
function children(dir) {
|
|
8098
|
+
try {
|
|
8099
|
+
return readdirSync(dir);
|
|
8100
|
+
} catch {
|
|
8101
|
+
return [];
|
|
8102
|
+
}
|
|
8103
|
+
}
|
|
8104
|
+
/** Claude: marketplace copy `~/.claude/plugins/marketplaces/<m>/plugins/core-guards`. */
|
|
8105
|
+
function claudeInstalled(home) {
|
|
8106
|
+
const markets = join(home, ".claude", "plugins", "marketplaces");
|
|
8107
|
+
return children(markets).some((m) => existsSync(join(markets, m, "plugins", "core-guards")));
|
|
8108
|
+
}
|
|
8109
|
+
/**
|
|
8110
|
+
* True when core-guards is installed AND active for the harness.
|
|
8111
|
+
* @param id - Harness id.
|
|
8112
|
+
* @param env - Environment (defaults to `process.env`).
|
|
8113
|
+
* @param home - OS home (defaults to `env.HOME` / `os.homedir()`; injectable).
|
|
8114
|
+
*/
|
|
8115
|
+
function coreGuardsWired(id, env = process.env, home = env.HOME ?? homedir()) {
|
|
8116
|
+
if (id === "claude-code") return claudeCoreGuardsActive(home, claudeInstalled(home));
|
|
8117
|
+
if (id === "codex") return codexCoreGuardsActive(home);
|
|
8118
|
+
if (id === "kimi") return kimiCoreGuardsActive(env.KIMI_CODE_HOME ?? join(home, ".kimi-code"));
|
|
8119
|
+
return false;
|
|
8120
|
+
}
|
|
8121
|
+
//#endregion
|
|
8122
|
+
//#region src/runtime/solid-pre.ts
|
|
8123
|
+
/**
|
|
8124
|
+
* Solid-scope PreToolUse outcome: the file-size deny ({@link solidFileSizeGate})
|
|
8125
|
+
* first — rendered via {@link respond} so every harness gets its native shape
|
|
8126
|
+
* (Kimi finally receives a real `hookSpecificOutput` deny instead of a leaked
|
|
8127
|
+
* Claude envelope) — then the ported Go/Python interface-location check
|
|
8128
|
+
* ({@link validateSolidGate}, kept byte-identical for claude-code/codex).
|
|
8129
|
+
* Owns the whole solid-scope branch so `handle-pre` stays a dispatcher and the
|
|
8130
|
+
* branch NEVER falls through to the core APEX/SOLID gate chain: core-guards
|
|
8131
|
+
* owns it, so falling through would run `gate()` twice per edit when both
|
|
8132
|
+
* plugins wire PreToolUse Write|Edit (duplicate denies + added latency). For
|
|
8133
|
+
* the same reason the file-size gate itself ABSTAINS when core-guards is
|
|
8134
|
+
* installed ({@link coreGuardsWired}): one tool-call, one deny, whoever fires.
|
|
8135
|
+
*/
|
|
8136
|
+
/**
|
|
8137
|
+
* Run the solid-scope PreToolUse gates and render the native hook outcome.
|
|
8138
|
+
* @param id - Harness id (selects the response shape via {@link respond}).
|
|
8139
|
+
* @param event - The normalized hook event.
|
|
8140
|
+
* @param file - Session track file (the deny-notice state derives from its dir).
|
|
8141
|
+
* @param now - Current time in ms.
|
|
8142
|
+
* @param env - Environment for the core-guards wiring probe (injectable).
|
|
8143
|
+
* @returns The hook outcome: file-size deny, Go/Python deny, or allow.
|
|
8144
|
+
*/
|
|
8145
|
+
function solidScopeOutcome(id, event, file, now, env = process.env) {
|
|
8146
|
+
const prompt = coreGuardsWired(id, env) ? null : solidFileSizeGate(event.tool, event.filePath, event.content, event.oldString, event.input.replace_all === true, event.agentType);
|
|
8147
|
+
if (prompt) return {
|
|
8148
|
+
stdout: withDenyNotice(id, respond(id, prompt), prompt, event.sessionId, dirname(file), now),
|
|
8149
|
+
exit: 0
|
|
8150
|
+
};
|
|
8151
|
+
return {
|
|
8152
|
+
stdout: validateSolidGate(event.tool, event.filePath ?? "", event.content ?? ""),
|
|
8153
|
+
exit: 0
|
|
8154
|
+
};
|
|
8155
|
+
}
|
|
8156
|
+
//#endregion
|
|
7758
8157
|
//#region src/runtime/pre-allow.ts
|
|
7759
8158
|
/**
|
|
7760
8159
|
* PreToolUse ALLOW-path response assembly. Reached only after every gate
|
|
@@ -7913,11 +8312,8 @@ async function handlePre(ctx) {
|
|
|
7913
8312
|
stdout: event.files?.length ? securityAdvisoryForPatch(event.files, opts.now) : securityAdvisory(event.tool, event.filePath ?? "", opts.now),
|
|
7914
8313
|
exit: 0
|
|
7915
8314
|
};
|
|
7916
|
-
if (opts.scope === "solid") return
|
|
7917
|
-
|
|
7918
|
-
exit: 0
|
|
7919
|
-
};
|
|
7920
|
-
if (event.tool === "Task") {
|
|
8315
|
+
if (opts.scope === "solid") return solidScopeOutcome(id, event, file, opts.now);
|
|
8316
|
+
if (isAgentTool(event.tool)) {
|
|
7921
8317
|
const taskCtx = taskContext(opts.cwd, id);
|
|
7922
8318
|
if (taskCtx) return {
|
|
7923
8319
|
stdout: taskCtx,
|
|
@@ -8139,9 +8535,8 @@ function shellReadRefPaths(command) {
|
|
|
8139
8535
|
}
|
|
8140
8536
|
//#endregion
|
|
8141
8537
|
//#region src/runtime/activity.ts
|
|
8142
|
-
/**
|
|
8538
|
+
/** Quality thresholds (chars): lead agent calls vs direct explore/research (parity Python `> 50`). */
|
|
8143
8539
|
const AGENT_QUALITY_MIN = 500;
|
|
8144
|
-
/** Min response length (chars) for direct exploration/research (parity Python `> 50`). */
|
|
8145
8540
|
const EXPLORE_QUALITY_MIN = 50;
|
|
8146
8541
|
/** Read tools across harnesses (Claude `Read`, Gemini/Cline `read_file`, …). */
|
|
8147
8542
|
const READ_TOOLS = /* @__PURE__ */ new Set([
|
|
@@ -8201,7 +8596,7 @@ function activityFor(event) {
|
|
|
8201
8596
|
source: docSource,
|
|
8202
8597
|
ts: event.now
|
|
8203
8598
|
});
|
|
8204
|
-
if (event.tool
|
|
8599
|
+
if (isAgentTool(event.tool)) {
|
|
8205
8600
|
const name = String(event.input?.subagent_type ?? event.input?.name ?? "").split(":").pop() ?? "";
|
|
8206
8601
|
if (name) out.push(agentActivity(name, event.now, qualityFor(event.responseLength, AGENT_QUALITY_MIN)));
|
|
8207
8602
|
return out;
|
|
@@ -8307,9 +8702,37 @@ function creditCodexSpawnAgent(id, tool, input, track, ts) {
|
|
|
8307
8702
|
*/
|
|
8308
8703
|
async function recordCodexSpawnEvidence(file, id, tool, input, ts) {
|
|
8309
8704
|
if (id !== "codex" || !isCodexSpawnAgentTool(tool)) return;
|
|
8310
|
-
|
|
8311
|
-
|
|
8312
|
-
|
|
8705
|
+
await withTrack(file, (track) => {
|
|
8706
|
+
const next = creditCodexSpawnAgent(id, tool, input, track, ts);
|
|
8707
|
+
return next !== track ? next : track;
|
|
8708
|
+
});
|
|
8709
|
+
}
|
|
8710
|
+
//#endregion
|
|
8711
|
+
//#region src/runtime/receipt-capture.ts
|
|
8712
|
+
/**
|
|
8713
|
+
* Bash verification-receipt extraction for PostToolUse: a `tsc`/`bun test`
|
|
8714
|
+
* run is parsed (exit code + output) into a signed receipt the TaskCompleted
|
|
8715
|
+
* gate later demands. Structured responses ONLY: Kimi Code CLI sends
|
|
8716
|
+
* `tool_output` as a truncated STRING (no exit_code, no stdout/stderr
|
|
8717
|
+
* channels) — parsing it would forge a success receipt (`exit 0`, empty
|
|
8718
|
+
* output) for possibly-failed runs, so a non-object source is skipped.
|
|
8719
|
+
*/
|
|
8720
|
+
/**
|
|
8721
|
+
* Record a verification receipt for a Bash run when the response is structured.
|
|
8722
|
+
* @param file - Session track file.
|
|
8723
|
+
* @param tool - The tool name (only "Bash" receipts exist).
|
|
8724
|
+
* @param command - The executed command.
|
|
8725
|
+
* @param toolResult - `payload.tool_result` (preferred), when present.
|
|
8726
|
+
* @param response - `payload.tool_response ?? payload.tool_output` fallback.
|
|
8727
|
+
* @param now - Clock.
|
|
8728
|
+
*/
|
|
8729
|
+
async function captureBashReceipt(file, tool, command, toolResult, response, now) {
|
|
8730
|
+
if (tool !== "Bash" || !command) return;
|
|
8731
|
+
const r = toolResult ?? response;
|
|
8732
|
+
if (!r || typeof r !== "object") return;
|
|
8733
|
+
const out = `${typeof r.stdout === "string" ? r.stdout : ""}\n${typeof r.stderr === "string" ? r.stderr : ""}`;
|
|
8734
|
+
const exit = Number(r.exit_code ?? 0);
|
|
8735
|
+
await captureReceipt(file, command, out, Number.isFinite(exit) ? exit : 0, now);
|
|
8313
8736
|
}
|
|
8314
8737
|
//#endregion
|
|
8315
8738
|
//#region src/tracking/codex-post-failure.ts
|
|
@@ -8452,12 +8875,7 @@ async function handlePost(ctx) {
|
|
|
8452
8875
|
const evidence = classifyAgentEvidence(event.tool, event.input, response);
|
|
8453
8876
|
if (evidence) await recordAgentEvidence(file, evidence, opts.now, typeof payload.agent_id === "string" ? payload.agent_id : void 0);
|
|
8454
8877
|
await recordCodexSpawnEvidence(file, id, event.tool, event.input, opts.now);
|
|
8455
|
-
|
|
8456
|
-
const r = payload.tool_result ?? response;
|
|
8457
|
-
const out = `${typeof r?.stdout === "string" ? r.stdout : ""}\n${typeof r?.stderr === "string" ? r.stderr : ""}`;
|
|
8458
|
-
const exit = Number(r?.exit_code ?? 0);
|
|
8459
|
-
await captureReceipt(file, event.command, out, Number.isFinite(exit) ? exit : 0, opts.now);
|
|
8460
|
-
}
|
|
8878
|
+
await captureBashReceipt(file, event.tool, event.command, payload.tool_result, response, opts.now);
|
|
8461
8879
|
if (id === "codex") recordCodexPostFailure(event.tool, payload.tool_result ?? response, {
|
|
8462
8880
|
now: opts.now,
|
|
8463
8881
|
dir: defaultStateDir(opts.cwd),
|
|
@@ -8534,6 +8952,10 @@ async function handlePost(ctx) {
|
|
|
8534
8952
|
stdout: attachSystemMessage(extra, userMessage),
|
|
8535
8953
|
exit: 0
|
|
8536
8954
|
};
|
|
8955
|
+
if (id === "kimi") return {
|
|
8956
|
+
stdout: respond(id, withUserMessage, "PostToolUse"),
|
|
8957
|
+
exit: 0
|
|
8958
|
+
};
|
|
8537
8959
|
return {
|
|
8538
8960
|
stdout: respond(id, withUserMessage, "PostToolUse") || extra,
|
|
8539
8961
|
exit: 0
|
|
@@ -8983,7 +9405,7 @@ async function handleHook(id, payload, opts) {
|
|
|
8983
9405
|
};
|
|
8984
9406
|
const userPrompt = typeof payload.prompt === "string" ? payload.prompt : void 0;
|
|
8985
9407
|
if (userPrompt !== void 0) {
|
|
8986
|
-
await
|
|
9408
|
+
await withTrack(file, (track) => recordBrainstormRequired(track, detectCreationIntent(userPrompt)));
|
|
8987
9409
|
return {
|
|
8988
9410
|
stdout: promptSubmitContext(userPrompt, opts.cwd, id),
|
|
8989
9411
|
exit: 0
|