@fusengine/harness 0.1.55 → 0.1.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/bin.mjs CHANGED
@@ -4,7 +4,7 @@ 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-DZvP_9xB.mjs";
6
6
  import { n as writeInitFile, t as initFor } from "../run-Do2JltgU.mjs";
7
- import { F as runDoctor, I as runningVersion, L as versionBanner, Lt as todayUtc, t as handleHook } from "../handle-L4ZNmpwN.mjs";
7
+ import { F as runDoctor, I as runningVersion, L as versionBanner, Lt as todayUtc, t as handleHook } from "../handle-CoyvRORV.mjs";
8
8
  import { delimiter, join } from "node:path";
9
9
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
10
10
  import { homedir } from "node:os";
@@ -2361,20 +2361,29 @@ function firstSentence(s) {
2361
2361
  return (s.split(/(?<=\.)\s/)[0] ?? s).trim();
2362
2362
  }
2363
2363
  /**
2364
- * Distil the actionable rule from a bullet body. With no "→" the whole bullet is
2365
- * the ruleits first sentence. Otherwise the rule is everything after the FIRST
2366
- * "→"; among its arrow-delimited segments (trimmed, empty dropped) take the first
2367
- * sentence of the LONGEST the information-dense clause, not whichever short
2368
- * aside the author appended last. When that clause is under {@link MIN_RULE}
2369
- * chars, fall back to the first sentence of the WHOLE rule part (never the
2370
- * narrative), so a short trailing segment never yields an illegible stub yet a
2371
- * legitimately terse rule is still shown intact.
2364
+ * The bullet's `narrative rule` delimiter: a SPACED arrow only. A GLUED arrow
2365
+ * between tokens (e.g. `120s300s`, `s→3`) is prose the author wrote, never a
2366
+ * delimiter matching on `→` alone chopped rules mid-token (bug: `300s) pensant
2367
+ * corriger…`). Also used to split rule-internal clauses.
2368
+ */
2369
+ const RULE_ARROW = /\s+→\s+/;
2370
+ /**
2371
+ * Distil the actionable rule from a bullet body. With no spaced arrow the whole
2372
+ * bullet is the rule → its first sentence. Otherwise the rule is everything after
2373
+ * the FIRST spaced arrow; its spaced-arrow-delimited segments are kept whole
2374
+ * except for TRAILING short asides (< {@link MIN_RULE} chars, e.g. `→ (cf.
2375
+ * lecture).`) which are dropped — so an arrow used as PROSE inside a rule (`maps
2376
+ * X → Y doit…`) is preserved intact rather than chopped at the arrow. When the
2377
+ * kept rule is still under {@link MIN_RULE} chars, fall back to the first sentence
2378
+ * of the WHOLE rule part (never the narrative), avoiding an illegible stub.
2372
2379
  */
2373
2380
  function distillRule(text) {
2374
- const arrow = text.indexOf("→");
2375
- if (arrow < 0) return firstSentence(text);
2376
- const rulePart = text.slice(arrow + 1);
2377
- const rule = firstSentence(rulePart.split("→").map((s) => s.trim()).filter(Boolean).reduce((a, b) => b.length > a.length ? b : a, ""));
2381
+ const sep = text.search(RULE_ARROW);
2382
+ if (sep < 0) return firstSentence(text);
2383
+ const rulePart = text.slice(sep).replace(RULE_ARROW, "").trim();
2384
+ const segments = rulePart.split(RULE_ARROW).map((s) => s.trim()).filter(Boolean);
2385
+ while (segments.length > 1 && (segments[segments.length - 1]?.length ?? 0) < 40) segments.pop();
2386
+ const rule = firstSentence(segments.join(" → "));
2378
2387
  return rule.length >= 40 ? rule : firstSentence(rulePart);
2379
2388
  }
2380
2389
  /** Collapse one older bullet to `- [date] <rule>`: {@link distillRule}, capped. */
@@ -5587,6 +5596,50 @@ function frameworkSkillGate(input, refsRead, existingCodeLines) {
5587
5596
  return skillTriggerGate(input.framework, content, refsRead, forced, input.cwd, input.filePath);
5588
5597
  }
5589
5598
  //#endregion
5599
+ //#region src/freshness/ref-evidence.ts
5600
+ /**
5601
+ * Platform-transcript reconciliation for `.md` reference reads — the durable
5602
+ * counterpart to {@link agentsRanFromTranscript} for agent freshness.
5603
+ *
5604
+ * WHY: the session track is persisted by a non-atomic load→mutate→save. Under
5605
+ * the multi-plugin hook fan-out (one hook process per installed plugin, ×N) plus
5606
+ * back-to-back tool events, concurrent writers clobber each other (lost update).
5607
+ * `agents`/`authorizations` self-heal — every explore/research/doc call rewrites
5608
+ * them, so a lost update lands again on the next of hundreds of writes — but a
5609
+ * `refsRead` entry is written ONCE (when the `.md` is Read), so a single lost
5610
+ * update erases it permanently, and the lead has no SubagentStop
5611
+ * {@link harvestAgentEvidence} pass to reconcile it (sub-agents do, which is why
5612
+ * only the LEAD's solidReadGate never credited). The Claude-authored transcript
5613
+ * is append-only and race-immune, so folding its `.md` Reads back into the track
5614
+ * restores the lost evidence; each gate still applies its own TTL/session policy.
5615
+ */
5616
+ /**
5617
+ * Fold every `.md` `Read` in the transcript into `track` as a timestamped ref
5618
+ * read (immutably). PURE reconciliation — the caller owns the track; each read
5619
+ * is stamped with its transcript timestamp (unstamped → `now`, lenient, matching
5620
+ * {@link agentsRanFromTranscript}), and an existing MORE-recent stamp is never
5621
+ * rolled back. Fail-open: an absent/unreadable transcript returns `track`
5622
+ * unchanged (same reference).
5623
+ * @param track - The current (possibly race-damaged) session track.
5624
+ * @param transcriptPath - Claude `transcript_path` for this session.
5625
+ * @param now - Fallback epoch-ms for transcript entries the platform left unstamped.
5626
+ * @returns The track with transcript `.md` reads merged into `refsRead`/`refsReadAt`.
5627
+ */
5628
+ function reconcileRefReadsFromTranscript(track, transcriptPath, now) {
5629
+ const uses = readAgentToolUses(transcriptPath);
5630
+ if (!uses) return track;
5631
+ let next = track;
5632
+ for (const u of uses) {
5633
+ if (u.name !== "Read") continue;
5634
+ const path = String(u.input?.file_path ?? u.input?.path ?? "");
5635
+ if (!path.endsWith(".md")) continue;
5636
+ const ts = u.ts ?? now;
5637
+ const prev = next.refsReadAt?.[path];
5638
+ if (prev === void 0 || prev < ts) next = recordRefRead(next, path, ts);
5639
+ }
5640
+ return next;
5641
+ }
5642
+ //#endregion
5590
5643
  //#region src/policy/shadcn-skill-gate.ts
5591
5644
  /** File extensions the shadcn gate polices (source: `\.(tsx|jsx|css|scss|json)$`). */
5592
5645
  const SHADCN_FILE_RE = /\.(tsx|jsx|css|scss|json)$/;
@@ -6048,8 +6101,7 @@ async function runGates(input) {
6048
6101
  if (modular) return modular;
6049
6102
  if (!input.filePath) return null;
6050
6103
  const filePath = input.filePath;
6051
- const window = input.windowMs ?? 12e4;
6052
- const track = await loadTrack(input.trackFile);
6104
+ const track = reconcileRefReadsFromTranscript(await loadTrack(input.trackFile), input.transcriptPath, input.now);
6053
6105
  const solidOrSkill = frameworkSkillGate(input, track.refsRead, existingCodeLines);
6054
6106
  if (solidOrSkill) return solidOrSkill;
6055
6107
  if (isShadcnWrite(input.tool, filePath)) {
@@ -6070,7 +6122,7 @@ async function runGates(input) {
6070
6122
  });
6071
6123
  if (geminiBlock) return geminiBlock;
6072
6124
  if (isApexScoped(input.filePath)) {
6073
- const apex = await apexScopedGate(input, track, window);
6125
+ const apex = await apexScopedGate(input, track, input.windowMs ?? 12e4);
6074
6126
  if (apex) return apex;
6075
6127
  }
6076
6128
  return dryGate(input.tool, input.filePath, input.content, input.cwd);
@@ -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 lessonsFileFor, C as seoPostToolUse, Ct as gitContext, D as postTrackingSideEffects, Dt as taskContext, E as securityAdvisory, Et as promptSubmitContext, Ft as securityStateDir, G as generateProjectMap, H as cartoSessionStart, It as securityStatePath, J as loadEnriched, K as isProject, 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 listChildren, R as dispatchLessons, S as postEditContext, St as devContext, T as dispatchMemory, Tt as claudeMdKey, U as generateEcosystemMap, V as lessonsStateFileFor, W as writePluginMap, X as countFiles, Y as mergeLines, Z as getFileDesc, _ 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, q as writeTree, 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 lessonsArchiveFileFor } from "../handle-L4ZNmpwN.mjs";
3
+ import { $ as postEditTypescript, A as trackSkillRead, At as trackFile, B as lessonsFileFor, C as seoPostToolUse, Ct as gitContext, D as postTrackingSideEffects, Dt as taskContext, E as securityAdvisory, Et as promptSubmitContext, Ft as securityStateDir, G as generateProjectMap, H as cartoSessionStart, It as securityStatePath, J as loadEnriched, K as isProject, 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 listChildren, R as dispatchLessons, S as postEditContext, St as devContext, T as dispatchMemory, Tt as claudeMdKey, U as generateEcosystemMap, V as lessonsStateFileFor, W as writePluginMap, X as countFiles, Y as mergeLines, Z as getFileDesc, _ 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, q as writeTree, 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 lessonsArchiveFileFor } from "../handle-CoyvRORV.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.55",
3
+ "version": "0.1.56",
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",