@fusengine/harness 0.1.63 → 0.1.64

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.
@@ -1,2 +1,2 @@
1
- import { a as fileSizeGuard, c as readClaudeInput, i as denyResponse, l as systemMessage, n as blockResponse, o as guard, r as contextResponse, s as informResponse, t as attachSystemMessage, u as toClaudeResponse } from "../../claude-C0xGMbeA.mjs";
1
+ import { a as fileSizeGuard, c as readClaudeInput, i as denyResponse, l as systemMessage, n as blockResponse, o as guard, r as contextResponse, s as informResponse, t as attachSystemMessage, u as toClaudeResponse } from "../../claude-CaBe4eBY.mjs";
2
2
  export { attachSystemMessage, blockResponse, contextResponse, denyResponse, fileSizeGuard, guard, informResponse, readClaudeInput, systemMessage, toClaudeResponse };
@@ -2,7 +2,7 @@ import { f as countLines } from "../../home-state-D0RLWP8J.mjs";
2
2
  import { t as evaluate } from "../../evaluate-wDDbojx6.mjs";
3
3
  import { t as formatPrompt } from "../../types-ernB1Dy3.mjs";
4
4
  import { t as parseApplyPatch } from "../../apply-patch-CIS2EZ_q.mjs";
5
- import { _ as commandToString, c as readClaudeInput, i as denyResponse, r as contextResponse } from "../../claude-C0xGMbeA.mjs";
5
+ import { c as readClaudeInput, i as denyResponse, r as contextResponse, v as commandToString } from "../../claude-CaBe4eBY.mjs";
6
6
  //#region src/adapters/codex/index.ts
7
7
  /**
8
8
  * OpenAI Codex CLI adapter (hook-mode). Codex's `PreToolUse` hook (since 2026)
@@ -1,2 +1,2 @@
1
- import { n as readHermesInput, r as toHermesResponse, t as guard } from "../../hermes-C1gt6ooE.mjs";
1
+ import { n as readHermesInput, r as toHermesResponse, t as guard } from "../../hermes-BdwBBemn.mjs";
2
2
  export { guard, readHermesInput, toHermesResponse };
@@ -81,11 +81,23 @@ function spawnCapture(cmd, args, cwd) {
81
81
  return "";
82
82
  }
83
83
  }
84
- /** Read the full process stdin as UTF-8 text (works under Node and Bun). */
84
+ /**
85
+ * Read the full process stdin as UTF-8 text (works under Node and Bun).
86
+ * Synchronous fd-0 read — never `for await (const c of process.stdin)` —
87
+ * because Bun has a confirmed Linux-only bug where merely adding a new
88
+ * top-level module import shifts stdin's internal init order and makes
89
+ * that async iterator silently yield zero bytes (oven-sh/bun#25320,
90
+ * #27849): exactly what broke every `harness hook` invocation on CI the
91
+ * moment `figures` became this package's first runtime dependency.
92
+ * `readFileSync(0)` bypasses that stream/ownership machinery entirely and
93
+ * is well-supported for piped (non-TTY) stdin under both runtimes.
94
+ */
85
95
  async function readStdin() {
86
- const chunks = [];
87
- for await (const chunk of process.stdin) chunks.push(chunk);
88
- return Buffer.concat(chunks).toString("utf8");
96
+ try {
97
+ return readFileSync(0, "utf8");
98
+ } catch {
99
+ return "";
100
+ }
89
101
  }
90
102
  /**
91
103
  * Recursively collect files under `dir` whose extension is in `exts`, skipping
@@ -214,4 +226,4 @@ function guard(input) {
214
226
  /** @deprecated use {@link guard}. Kept for back-compat. */
215
227
  const fileSizeGuard = guard;
216
228
  //#endregion
217
- export { commandToString as _, fileSizeGuard as a, readClaudeInput as c, collectFiles as d, pathExists as f, writeText as g, spawnCapture as h, denyResponse as i, systemMessage as l, sleep as m, blockResponse as n, guard as o, readText as p, contextResponse as r, informResponse as s, attachSystemMessage as t, toClaudeResponse as u };
229
+ export { writeText as _, fileSizeGuard as a, readClaudeInput as c, collectFiles as d, pathExists as f, spawnCapture as g, sleep as h, denyResponse as i, systemMessage as l, readText as m, blockResponse as n, guard as o, readStdin as p, contextResponse as r, informResponse as s, attachSystemMessage as t, toClaudeResponse as u, commandToString as v };
package/dist/cli/bin.mjs CHANGED
@@ -4,7 +4,8 @@ import { t as detectHarness } from "../harness-Cb9xR8dC.mjs";
4
4
  import { t as claudeHome } from "../home-state-D0RLWP8J.mjs";
5
5
  import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-kVHXFVug.mjs";
6
6
  import { n as writeInitFile, t as initFor } from "../run-Do2JltgU.mjs";
7
- import { J as runningVersion, Lt as todayUtc, Y as versionBanner, q as runDoctor, t as handleHook } from "../handle-CjwRTMOv.mjs";
7
+ import { J as runningVersion, Lt as todayUtc, Y as versionBanner, q as runDoctor, t as handleHook } from "../handle-DE-753Mr.mjs";
8
+ import { p as readStdin$1 } from "../claude-CaBe4eBY.mjs";
8
9
  import { delimiter, join } from "node:path";
9
10
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
10
11
  import { homedir } from "node:os";
@@ -361,15 +362,19 @@ function discoverRefs(home, cwd, marketplaces) {
361
362
  * harness hook <id> runtime: read a hook payload on stdin, route to the adapter, print the response
362
363
  * harness changelog fetch + diff the Claude Code changelog, print a JSON summary (changelog-watcher)
363
364
  */
365
+ const hookDebug = process.env.FUSE_HARNESS_DEBUG === "1";
366
+ function traceHook(label, data) {
367
+ if (hookDebug) process.stderr.write(`[hook-debug] ${label}: ${typeof data === "string" ? data : JSON.stringify(data)}\n`);
368
+ }
364
369
  async function readStdin() {
365
- const chunks = [];
366
- for await (const c of process.stdin) chunks.push(c);
367
- const text = Buffer.concat(chunks).toString("utf8").trim();
370
+ const text = (await readStdin$1()).trim();
371
+ traceHook("stdin-text-length", text.length);
368
372
  if (!text) return {};
369
373
  try {
370
374
  const parsed = JSON.parse(text);
371
375
  return typeof parsed === "object" && parsed !== null ? parsed : {};
372
- } catch {
376
+ } catch (e) {
377
+ traceHook("stdin-parse-error", e instanceof Error ? e.message : String(e));
373
378
  return {};
374
379
  }
375
380
  }
@@ -399,13 +404,27 @@ if (cmd === "--version" || cmd === "-v") {
399
404
  ])).has(scopeArg) ? scopeArg : "core";
400
405
  const marketplaces = (process.env.FUSE_HARNESS_MARKETPLACES ?? "fusengine-plugins").split(",").map((s) => s.trim()).filter(Boolean);
401
406
  const refsDir = process.env.FUSE_HARNESS_REFS || discoverRefs(homedir(), process.cwd(), marketplaces) || void 0;
402
- const outcome = await handleHook(id, await readStdin(), {
403
- now: Date.now(),
404
- cwd: process.cwd(),
405
- refsDir,
406
- windowMs: resolveTtlSec(process.env) * 1e3,
407
+ traceHook("args", {
408
+ id,
407
409
  scope
408
410
  });
411
+ let outcome;
412
+ try {
413
+ outcome = await handleHook(id, await readStdin(), {
414
+ now: Date.now(),
415
+ cwd: process.cwd(),
416
+ refsDir,
417
+ windowMs: resolveTtlSec(process.env) * 1e3,
418
+ scope
419
+ });
420
+ } catch (e) {
421
+ traceHook("handleHook-threw", e instanceof Error ? `${e.message}\n${e.stack}` : String(e));
422
+ throw e;
423
+ }
424
+ traceHook("outcome", {
425
+ stdoutLength: outcome.stdout.length,
426
+ exit: outcome.exit
427
+ });
409
428
  if (outcome.stdout) process.stdout.write(outcome.stdout);
410
429
  process.exit(outcome.exit);
411
430
  } else if (cmd === "init") {
@@ -14,8 +14,8 @@ import { d as loadIndex, i as cacheLookupMeta, n as webfetchCacheWrite, o as cac
14
14
  import { t as loadRefs } from "./loader-AGz4nK7d.mjs";
15
15
  import { a as writeLastNonce, c as recordAgent, d as recordRefRead, f as recordTarget, h as apexAuthorizationGate, i as verifyTrack, l as recordBrainstormRequired, m as trivialCount, n as saveTrack, o as agentsFresh, p as recordTrivialEdit, r as signTrack, s as emptyTrack, t as loadTrack, u as recordDoc } from "./store-CQ4roWrU.mjs";
16
16
  import { t as parseApplyPatch } from "./apply-patch-CIS2EZ_q.mjs";
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, r as contextResponse, s as informResponse, t as attachSystemMessage } from "./claude-C0xGMbeA.mjs";
18
- import { r as toHermesResponse } from "./hermes-C1gt6ooE.mjs";
17
+ import { _ as writeText, d as collectFiles, f as pathExists, g as spawnCapture, h as sleep, i as denyResponse, l as systemMessage, m as readText, n as blockResponse, r as contextResponse, s as informResponse, t as attachSystemMessage, v as commandToString } from "./claude-CaBe4eBY.mjs";
18
+ import { r as toHermesResponse } from "./hermes-BdwBBemn.mjs";
19
19
  import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
20
20
  import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
21
21
  import { homedir } from "node:os";
@@ -23,6 +23,7 @@ import { mkdir, rmdir } from "node:fs/promises";
23
23
  import { createHash } from "node:crypto";
24
24
  import { execFileSync, execSync } from "node:child_process";
25
25
  import { fileURLToPath } from "node:url";
26
+ import figures from "figures";
26
27
  //#region src/runtime/lifecycle/security/skill-state.ts
27
28
  /**
28
29
  * Shared security-tracker state: per-UTC-day JSON under
@@ -7053,6 +7054,56 @@ function respond(id, prompt) {
7053
7054
  }
7054
7055
  }
7055
7056
  //#endregion
7057
+ //#region src/runtime/deny-notice.ts
7058
+ /**
7059
+ * @module deny-notice
7060
+ * Attach a user-visible notice to a deny/ask hook response — the owner-reported
7061
+ * gap where `permissionDecision: deny/ask` (the agent-only channel) left the
7062
+ * human staring at a silent terminal. Mirrors {@link module:notices}'s
7063
+ * compliance notices but for the BLOCKING outcomes those never covered.
7064
+ * @packageDocumentation
7065
+ */
7066
+ /**
7067
+ * The human-facing line for a block/ask outcome: the gate's own
7068
+ * {@link Prompt.userMessage} when it set one, else a generic symbol + title —
7069
+ * text-presentation Unicode only (`figures.cross`/`?`), never emoji: terminal-safe,
7070
+ * single-cell width, with the Windows fallbacks `figures` already resolves, matching
7071
+ * the existing notice family (`notices.ts`'s `✓`/`⚠`, left untouched by this module).
7072
+ * Null for `inform` ({@link module:respond}'s own `userMessage` path already
7073
+ * covers it) or when neither applies.
7074
+ */
7075
+ function denyAskNotice(prompt) {
7076
+ if (prompt.kind === "block") return prompt.userMessage ?? `${figures.cross} ${prompt.title}`;
7077
+ if (prompt.kind === "ask") return prompt.userMessage ?? `? ${prompt.title}`;
7078
+ return null;
7079
+ }
7080
+ /**
7081
+ * Attach {@link denyAskNotice} onto an already-rendered deny/ask `stdout`, for
7082
+ * the harnesses whose human channel is `systemMessage` (claude-code/codex —
7083
+ * `permissionDecision`/`permissionDecisionReason` stay byte-intact, only the
7084
+ * top-level field is added). Every other harness passes `stdout` through
7085
+ * unchanged: Cursor already emits `user_message` natively (respond.ts), Hermes
7086
+ * and cline have no human channel. Deduped via {@link onceExclusive} against
7087
+ * the ~11 sibling-plugin fan-out for one real event (same window as the
7088
+ * sniper reminder / compliance notices).
7089
+ * @param id - Harness id (`ctx.id` from `PreContext`).
7090
+ * @param stdout - The rendered hook response (from {@link module:respond.respond}).
7091
+ * @param prompt - The {@link Prompt} that produced `stdout`.
7092
+ * @param sessionId - Current session id (dedup scope).
7093
+ * @param dir - State-dir for the dedup marker (per-project state dir).
7094
+ * @param now - Event clock (tests pass a fake one).
7095
+ */
7096
+ function withDenyNotice(id, stdout, prompt, sessionId, dir, now) {
7097
+ if (id !== "claude-code" && id !== "codex") return stdout;
7098
+ const notice = denyAskNotice(prompt);
7099
+ if (!notice) return stdout;
7100
+ if (!onceExclusive(`deny-notice:${sessionId}:${prompt.title}`, 2e3, {
7101
+ now,
7102
+ dir
7103
+ })) return stdout;
7104
+ return attachSystemMessage(stdout, notice);
7105
+ }
7106
+ //#endregion
7056
7107
  //#region src/policy/design/content-checks.ts
7057
7108
  /** Accessibility warnings: icon buttons need aria-label, images need alt. */
7058
7109
  function checkAccessibility(content) {
@@ -7618,7 +7669,7 @@ async function handlePre(ctx) {
7618
7669
  }
7619
7670
  const designBlock = designGate(payload, event, mcpDir, opts.cwd);
7620
7671
  if (designBlock) return {
7621
- stdout: respond(id, designBlock),
7672
+ stdout: withDenyNotice(id, respond(id, designBlock), designBlock, event.sessionId, dirname(file), opts.now),
7622
7673
  exit: 0
7623
7674
  };
7624
7675
  if (opts.scope === "security") return {
@@ -7639,7 +7690,7 @@ async function handlePre(ctx) {
7639
7690
  if (event.files && event.files.length > 0) {
7640
7691
  const patchPrompt = applyPatchGate(event.files, opts.cwd);
7641
7692
  if (patchPrompt) return {
7642
- stdout: respond(id, patchPrompt),
7693
+ stdout: withDenyNotice(id, respond(id, patchPrompt), patchPrompt, event.sessionId, dirname(file), opts.now),
7643
7694
  exit: 0
7644
7695
  };
7645
7696
  }
@@ -7661,7 +7712,7 @@ async function handlePre(ctx) {
7661
7712
  transcriptPath: typeof payload.transcript_path === "string" ? payload.transcript_path : void 0
7662
7713
  });
7663
7714
  if (prompt) return {
7664
- stdout: respond(id, prompt),
7715
+ stdout: withDenyNotice(id, respond(id, prompt), prompt, event.sessionId, dirname(file), opts.now),
7665
7716
  exit: 0
7666
7717
  };
7667
7718
  return allowOutcome(id, event, payload, mcpDir, opts.cwd, {
@@ -1,6 +1,6 @@
1
1
  import { t as evaluate } from "./evaluate-wDDbojx6.mjs";
2
2
  import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
3
- import { _ as commandToString, c as readClaudeInput } from "./claude-C0xGMbeA.mjs";
3
+ import { c as readClaudeInput, v as commandToString } from "./claude-CaBe4eBY.mjs";
4
4
  //#region src/adapters/hermes/index.ts
5
5
  /**
6
6
  * Hermes Agent adapter (hook-mode). Nous Research's hermes-agent (2026) pipes a
@@ -1,6 +1,6 @@
1
1
  import { r as projectLayout } from "../layout-C0jaaCQC.mjs";
2
2
  import { a as sanitizeSessionId, c as sessionsDir, i as loadSessionState, n as fuseHarnessHome, o as saveSessionState, r as fusengineCache, s as sessionStatePath, t as claudeHome } from "../home-state-D0RLWP8J.mjs";
3
- import { $ as postEditTypescript, A as trackSkillRead, At as trackFile, B as isProject, C as seoPostToolUse, Ct as gitContext, D as postTrackingSideEffects, Dt as taskContext, E as securityAdvisory, Et as promptSubmitContext, F as dispatchLessons, Ft as securityStateDir, G as getFileDesc, H as loadEnriched, I as cartoSessionStart, It as securityStatePath, K as listChildren, L as generateEcosystemMap, Lt as todayUtc, M as dispatchLifecycle, Mt as isoUtc, N as aipilotPostToolUse, Nt as loadSecurityState, O as trackWatchResearch, Ot as defaultStateDir, P as dispatchAipilot, Pt as saveSecurityState, Q as lessonsStateFileFor, R as writePluginMap, S as postEditContext, St as devContext, T as dispatchMemory, Tt as claudeMdKey, U as mergeLines, V as writeTree, W as countFiles, X as lessonsArchiveFileFor, Z as lessonsFileFor, _ as preCommitGate, _t as sessionStartCore, a as recordActivity, at as validateTeammateOutput, b as extractSymbols, bt as removeOldFiles, c as MCP_TTL_MS, ct as validateTailwind, d as isMcpTool, dt as countLoc, et as trackSessionChanges, f as queryOf, ft as detectSolidProfile, g as gate, gt as runSessionStartCleanups, h as TRIVIAL_BUDGET, ht as readRules, i as respond, it as logToolFailure, j as trackEnrichment, jt as normalizeEvent, k as trackMcpResearch, kt as projectHash, l as WEBFETCH_TTL_MS, lt as validateSolidGate, m as REQUIRED_AGENTS, mt as injectRules, n as activityFor, nt as cleanupSession, o as mcpPostStore, ot as trackAgentMemory, p as DEFAULT_WINDOW_MS, pt as solidDetectStart, r as handlePre, rt as saveApexState, s as mcpPreIntercept, st as subagentCacheContext, t as handleHook, tt as validateRulesLoaded, u as cacheQueryOf, ut as checkFileSize, v as detectDuplication, vt as pruneEmptyDirs, w as seoPostToolUseResponse, wt as projectContext, x as lifecycleStdout, xt as trimLogFile, y as dryGate, yt as purgeTtlTree, z as generateProjectMap } from "../handle-CjwRTMOv.mjs";
3
+ import { $ as postEditTypescript, A as trackSkillRead, At as trackFile, B as isProject, C as seoPostToolUse, Ct as gitContext, D as postTrackingSideEffects, Dt as taskContext, E as securityAdvisory, Et as promptSubmitContext, F as dispatchLessons, Ft as securityStateDir, G as getFileDesc, H as loadEnriched, I as cartoSessionStart, It as securityStatePath, K as listChildren, L as generateEcosystemMap, Lt as todayUtc, M as dispatchLifecycle, Mt as isoUtc, N as aipilotPostToolUse, Nt as loadSecurityState, O as trackWatchResearch, Ot as defaultStateDir, P as dispatchAipilot, Pt as saveSecurityState, Q as lessonsStateFileFor, R as writePluginMap, S as postEditContext, St as devContext, T as dispatchMemory, Tt as claudeMdKey, U as mergeLines, V as writeTree, W as countFiles, X as lessonsArchiveFileFor, Z as lessonsFileFor, _ as preCommitGate, _t as sessionStartCore, a as recordActivity, at as validateTeammateOutput, b as extractSymbols, bt as removeOldFiles, c as MCP_TTL_MS, ct as validateTailwind, d as isMcpTool, dt as countLoc, et as trackSessionChanges, f as queryOf, ft as detectSolidProfile, g as gate, gt as runSessionStartCleanups, h as TRIVIAL_BUDGET, ht as readRules, i as respond, it as logToolFailure, j as trackEnrichment, jt as normalizeEvent, k as trackMcpResearch, kt as projectHash, l as WEBFETCH_TTL_MS, lt as validateSolidGate, m as REQUIRED_AGENTS, mt as injectRules, n as activityFor, nt as cleanupSession, o as mcpPostStore, ot as trackAgentMemory, p as DEFAULT_WINDOW_MS, pt as solidDetectStart, r as handlePre, rt as saveApexState, s as mcpPreIntercept, st as subagentCacheContext, t as handleHook, tt as validateRulesLoaded, u as cacheQueryOf, ut as checkFileSize, v as detectDuplication, vt as pruneEmptyDirs, w as seoPostToolUseResponse, wt as projectContext, x as lifecycleStdout, xt as trimLogFile, y as dryGate, yt as purgeTtlTree, z as generateProjectMap } from "../handle-DE-753Mr.mjs";
4
4
  //#region src/runtime/storage.ts
5
5
  /**
6
6
  * The project's single state dir (`<root>/.harness`) — neutral + harness-agnostic,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fusengine/harness",
3
- "version": "0.1.63",
3
+ "version": "0.1.64",
4
4
  "description": "Harness-agnostic toolkit for AI coding agents: runtime harness detection (Claude Code, Codex, Cursor, Cline, Gemini, Aider...), pure policy core (env config, project/framework detection, SOLID/file-size limits, APEX freshness, guard patterns, portable prompts), cache, project memory, ref routing, state/locks, statusline, per-harness adapters (Claude/Cursor/Cline/Gemini) and a cli-mode harness-check binary. Bun-native, with a built dist for Node + bundlers.",
5
5
  "type": "module",
6
6
  "module": "src/index.ts",
@@ -152,5 +152,8 @@
152
152
  "tsdown": "^0.22.3",
153
153
  "typedoc": "^0.28.19",
154
154
  "typescript": "^6.0.3"
155
+ },
156
+ "dependencies": {
157
+ "figures": "^6.1.0"
155
158
  }
156
159
  }