harnery 0.34.1 → 0.35.0

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.
Files changed (212) hide show
  1. package/dist/commander.d.ts.map +1 -1
  2. package/dist/commander.js +2 -0
  3. package/dist/commands/agents.d.ts +15 -4
  4. package/dist/commands/agents.d.ts.map +1 -1
  5. package/dist/commands/agents.js +89 -175
  6. package/dist/commands/browse.d.ts.map +1 -1
  7. package/dist/commands/browse.js +26 -2
  8. package/dist/commands/decision.d.ts.map +1 -1
  9. package/dist/commands/decision.js +2 -2
  10. package/dist/commands/events.d.ts.map +1 -1
  11. package/dist/commands/events.js +18 -1
  12. package/dist/commands/presence.d.ts.map +1 -1
  13. package/dist/commands/presence.js +2 -2
  14. package/dist/commands/semantic.d.ts +4 -0
  15. package/dist/commands/semantic.d.ts.map +1 -0
  16. package/dist/commands/semantic.js +147 -0
  17. package/dist/commands/web.d.ts.map +1 -1
  18. package/dist/commands/web.js +17 -0
  19. package/dist/core/agents/canonical-emit.d.ts +3 -0
  20. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  21. package/dist/core/agents/canonical-emit.js +12 -0
  22. package/dist/core/agents/claim-path.d.ts +11 -0
  23. package/dist/core/agents/claim-path.d.ts.map +1 -0
  24. package/dist/core/agents/claim-path.js +22 -0
  25. package/dist/core/agents/coord-client.d.ts +29 -3
  26. package/dist/core/agents/coord-client.d.ts.map +1 -1
  27. package/dist/core/agents/coord-client.js +88 -9
  28. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  29. package/dist/core/agents/live-authority-v3.js +1 -3
  30. package/dist/core/agents/live-lifecycle-v3.js +3 -3
  31. package/dist/core/agents/live-observation-v3.d.ts.map +1 -1
  32. package/dist/core/agents/live-observation-v3.js +2 -2
  33. package/dist/core/agents/render/prompt-context.js +12 -10
  34. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  35. package/dist/core/agents/rules/stop-hook.js +23 -11
  36. package/dist/core/agents/session-name-display.d.ts +1 -0
  37. package/dist/core/agents/session-name-display.d.ts.map +1 -1
  38. package/dist/core/agents/session-name-display.js +8 -1
  39. package/dist/core/agents/state/heartbeat-reader.d.ts +46 -0
  40. package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -0
  41. package/dist/core/agents/state/heartbeat-reader.js +25 -0
  42. package/dist/core/agents/state/heartbeat-writer.d.ts +3 -48
  43. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  44. package/dist/core/agents/state/heartbeat-writer.js +16 -34
  45. package/dist/core/agents/state/live-coordination-view.d.ts +1 -1
  46. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  47. package/dist/core/agents/state/live-coordination-view.js +25 -1
  48. package/dist/core/agents/state/names.d.ts +9 -1
  49. package/dist/core/agents/state/names.d.ts.map +1 -1
  50. package/dist/core/agents/state/names.js +190 -182
  51. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  52. package/dist/core/agents/state/stale-sweep.js +23 -5
  53. package/dist/core/artifacts/constants.d.ts +5 -0
  54. package/dist/core/artifacts/constants.d.ts.map +1 -0
  55. package/dist/core/artifacts/constants.js +4 -0
  56. package/dist/core/artifacts/index.d.ts +3 -3
  57. package/dist/core/artifacts/index.d.ts.map +1 -1
  58. package/dist/core/artifacts/index.js +50 -7
  59. package/dist/core/events/v3/base-contract.d.ts +795 -0
  60. package/dist/core/events/v3/base-contract.d.ts.map +1 -1
  61. package/dist/core/events/v3/base-contract.js +33 -0
  62. package/dist/core/events/v3/capabilities.d.ts +11 -0
  63. package/dist/core/events/v3/capabilities.d.ts.map +1 -1
  64. package/dist/core/events/v3/capabilities.js +23 -0
  65. package/dist/core/events/v3/generated.d.ts +1 -1
  66. package/dist/core/events/v3/generated.js +1 -1
  67. package/dist/core/events/v3/index.d.ts +4 -3
  68. package/dist/core/events/v3/index.d.ts.map +1 -1
  69. package/dist/core/events/v3/index.js +4 -3
  70. package/dist/core/events/v3/latency.d.ts +31 -1
  71. package/dist/core/events/v3/latency.d.ts.map +1 -1
  72. package/dist/core/events/v3/latency.js +127 -3
  73. package/dist/core/events/v3/live-routing.d.ts +30 -0
  74. package/dist/core/events/v3/live-routing.d.ts.map +1 -1
  75. package/dist/core/events/v3/live-routing.js +109 -1
  76. package/dist/core/events/v3/producers/command-recorder.d.ts.map +1 -1
  77. package/dist/core/events/v3/producers/command-recorder.js +10 -1
  78. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  79. package/dist/core/events/v3/producers/coordination-recorder.js +14 -3
  80. package/dist/core/events/v3/producers/hook-base.d.ts +17 -1
  81. package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
  82. package/dist/core/events/v3/producers/hook-base.js +82 -1
  83. package/dist/core/events/v3/producers/intake.d.ts +6 -0
  84. package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
  85. package/dist/core/events/v3/producers/intake.js +11 -0
  86. package/dist/core/events/v3/producers/recorder.d.ts +62 -0
  87. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  88. package/dist/core/events/v3/producers/recorder.js +654 -18
  89. package/dist/core/events/v3/runtime-telemetry-capabilities.d.ts +51 -0
  90. package/dist/core/events/v3/runtime-telemetry-capabilities.d.ts.map +1 -0
  91. package/dist/core/events/v3/runtime-telemetry-capabilities.js +54 -0
  92. package/dist/core/events/v3/turn-telemetry.d.ts +9 -0
  93. package/dist/core/events/v3/turn-telemetry.d.ts.map +1 -1
  94. package/dist/core/hooks/adapter/parse.d.ts +3 -0
  95. package/dist/core/hooks/adapter/parse.d.ts.map +1 -1
  96. package/dist/core/hooks/adapter/parse.js +14 -0
  97. package/dist/core/hooks/adapter/runtime-telemetry.d.ts +119 -0
  98. package/dist/core/hooks/adapter/runtime-telemetry.d.ts.map +1 -0
  99. package/dist/core/hooks/adapter/runtime-telemetry.js +568 -0
  100. package/dist/core/hooks/cli.js +111 -36
  101. package/dist/core/hooks/remediation-cap.d.ts +34 -0
  102. package/dist/core/hooks/remediation-cap.d.ts.map +1 -0
  103. package/dist/core/hooks/remediation-cap.js +71 -0
  104. package/dist/core/hooks/resolve/transcript.d.ts +25 -9
  105. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  106. package/dist/core/hooks/resolve/transcript.js +22 -10
  107. package/dist/core/semantic/adapters.d.ts +34 -0
  108. package/dist/core/semantic/adapters.d.ts.map +1 -0
  109. package/dist/core/semantic/adapters.js +222 -0
  110. package/dist/core/semantic/contract.d.ts +588 -0
  111. package/dist/core/semantic/contract.d.ts.map +1 -0
  112. package/dist/core/semantic/contract.js +230 -0
  113. package/dist/core/semantic/evidence.d.ts +11 -0
  114. package/dist/core/semantic/evidence.d.ts.map +1 -0
  115. package/dist/core/semantic/evidence.js +262 -0
  116. package/dist/core/semantic/index.d.ts +10 -0
  117. package/dist/core/semantic/index.d.ts.map +1 -0
  118. package/dist/core/semantic/index.js +9 -0
  119. package/dist/core/semantic/once.d.ts +33 -0
  120. package/dist/core/semantic/once.d.ts.map +1 -0
  121. package/dist/core/semantic/once.js +293 -0
  122. package/dist/core/semantic/prompt.d.ts +12 -0
  123. package/dist/core/semantic/prompt.d.ts.map +1 -0
  124. package/dist/core/semantic/prompt.js +70 -0
  125. package/dist/core/semantic/scheduler.d.ts +16 -0
  126. package/dist/core/semantic/scheduler.d.ts.map +1 -0
  127. package/dist/core/semantic/scheduler.js +67 -0
  128. package/dist/core/semantic/service-status.d.ts +31 -0
  129. package/dist/core/semantic/service-status.d.ts.map +1 -0
  130. package/dist/core/semantic/service-status.js +84 -0
  131. package/dist/core/semantic/service.d.ts +51 -0
  132. package/dist/core/semantic/service.d.ts.map +1 -0
  133. package/dist/core/semantic/service.js +470 -0
  134. package/dist/core/semantic/storage.d.ts +71 -0
  135. package/dist/core/semantic/storage.d.ts.map +1 -0
  136. package/dist/core/semantic/storage.js +179 -0
  137. package/dist/core/semantic/validate.d.ts +16 -0
  138. package/dist/core/semantic/validate.d.ts.map +1 -0
  139. package/dist/core/semantic/validate.js +174 -0
  140. package/dist/lib/browser/client.d.ts +11 -0
  141. package/dist/lib/browser/client.d.ts.map +1 -1
  142. package/dist/lib/browser/client.js +13 -0
  143. package/dist/lib/browser/index.d.ts +1 -0
  144. package/dist/lib/browser/index.d.ts.map +1 -1
  145. package/dist/lib/browser/standalone-html.d.ts +25 -0
  146. package/dist/lib/browser/standalone-html.d.ts.map +1 -0
  147. package/dist/lib/browser/standalone-html.js +284 -0
  148. package/dist/lib/instructions/apply.d.ts.map +1 -1
  149. package/dist/lib/instructions/apply.js +1 -0
  150. package/dist/lib/instructions/templates.d.ts +2 -0
  151. package/dist/lib/instructions/templates.d.ts.map +1 -1
  152. package/dist/lib/instructions/templates.js +144 -4
  153. package/package.json +6 -1
  154. package/schemas/event-v3.schema.json +2758 -478
  155. package/src/commander.ts +2 -0
  156. package/src/commands/agents.ts +102 -167
  157. package/src/commands/browse.ts +25 -2
  158. package/src/commands/decision.ts +8 -2
  159. package/src/commands/events.ts +21 -1
  160. package/src/commands/presence.ts +7 -2
  161. package/src/commands/semantic.ts +179 -0
  162. package/src/commands/web.ts +15 -0
  163. package/src/core/agents/canonical-emit.ts +15 -0
  164. package/src/core/agents/claim-path.ts +23 -0
  165. package/src/core/agents/coord-client.ts +92 -8
  166. package/src/core/agents/live-authority-v3.ts +1 -4
  167. package/src/core/agents/live-lifecycle-v3.ts +8 -3
  168. package/src/core/agents/live-observation-v3.ts +7 -2
  169. package/src/core/agents/render/prompt-context.ts +13 -10
  170. package/src/core/agents/rules/stop-hook.ts +24 -11
  171. package/src/core/agents/session-name-display.ts +9 -1
  172. package/src/core/agents/state/heartbeat-reader.ts +69 -0
  173. package/src/core/agents/state/heartbeat-writer.ts +20 -84
  174. package/src/core/agents/state/live-coordination-view.ts +26 -1
  175. package/src/core/agents/state/names.ts +190 -182
  176. package/src/core/agents/state/stale-sweep.ts +25 -5
  177. package/src/core/artifacts/constants.ts +4 -0
  178. package/src/core/artifacts/index.ts +49 -7
  179. package/src/core/events/v3/base-contract.ts +39 -0
  180. package/src/core/events/v3/capabilities.ts +34 -0
  181. package/src/core/events/v3/generated.ts +1 -1
  182. package/src/core/events/v3/index.ts +21 -0
  183. package/src/core/events/v3/latency.ts +213 -2
  184. package/src/core/events/v3/live-routing.ts +120 -1
  185. package/src/core/events/v3/producers/command-recorder.ts +10 -1
  186. package/src/core/events/v3/producers/coordination-recorder.ts +17 -5
  187. package/src/core/events/v3/producers/hook-base.ts +115 -1
  188. package/src/core/events/v3/producers/intake.ts +18 -0
  189. package/src/core/events/v3/producers/recorder.ts +856 -21
  190. package/src/core/events/v3/runtime-telemetry-capabilities.ts +137 -0
  191. package/src/core/events/v3/turn-telemetry.ts +10 -0
  192. package/src/core/hooks/adapter/parse.ts +16 -0
  193. package/src/core/hooks/adapter/runtime-telemetry.ts +805 -0
  194. package/src/core/hooks/cli.ts +123 -39
  195. package/src/core/hooks/remediation-cap.ts +77 -0
  196. package/src/core/hooks/resolve/transcript.ts +37 -11
  197. package/src/core/semantic/adapters.ts +293 -0
  198. package/src/core/semantic/contract.ts +298 -0
  199. package/src/core/semantic/evidence.ts +336 -0
  200. package/src/core/semantic/index.ts +127 -0
  201. package/src/core/semantic/once.ts +410 -0
  202. package/src/core/semantic/prompt.ts +84 -0
  203. package/src/core/semantic/scheduler.ts +107 -0
  204. package/src/core/semantic/service-status.ts +118 -0
  205. package/src/core/semantic/service.ts +580 -0
  206. package/src/core/semantic/storage.ts +286 -0
  207. package/src/core/semantic/validate.ts +212 -0
  208. package/src/lib/browser/client.ts +21 -0
  209. package/src/lib/browser/index.ts +4 -0
  210. package/src/lib/browser/standalone-html.ts +312 -0
  211. package/src/lib/instructions/apply.ts +1 -0
  212. package/src/lib/instructions/templates.ts +149 -4
package/src/commander.ts CHANGED
@@ -53,6 +53,7 @@ import { registerPolicyCommand } from "./commands/policy.ts";
53
53
  import { registerPresenceCommand } from "./commands/presence.ts";
54
54
  import { registerReadCommand } from "./commands/read.ts";
55
55
  import { registerRelayCommand } from "./commands/relay.ts";
56
+ import { registerSemanticCommand } from "./commands/semantic.ts";
56
57
  import { registerSyncCommand } from "./commands/sync.ts";
57
58
  import { registerSectionCommand, registerTocCommand } from "./commands/toc.ts";
58
59
  import { registerTokensCommand } from "./commands/tokens.ts";
@@ -290,6 +291,7 @@ export function createHarneryProgram(opts: HarneryContextOpts = {}): Command {
290
291
  registerCheckpointCommand(program, emit, opts.context);
291
292
  registerJournalCommand(program, emit);
292
293
  registerLedgerV3Command(program, emit, opts.context);
294
+ registerSemanticCommand(program, emit, opts.context);
293
295
  registerEventsCommand(program, emit, opts.context);
294
296
  registerArtifactsCommand(program, emit, opts.context);
295
297
  registerDecisionCommand(program, emit);
@@ -21,7 +21,6 @@ import {
21
21
  readdirSync,
22
22
  readFileSync,
23
23
  rmSync,
24
- statSync,
25
24
  writeFileSync,
26
25
  } from "node:fs";
27
26
  import { homedir, tmpdir } from "node:os";
@@ -38,6 +37,7 @@ import {
38
37
  import {
39
38
  emitEventV3,
40
39
  monorepoRoot,
40
+ nativeSessionIdentity,
41
41
  normalizeAdapter,
42
42
  resolveOwner,
43
43
  resolveOwnerBySessionEnv,
@@ -52,7 +52,10 @@ import {
52
52
  requestSessionEndExplicitV3,
53
53
  type SessionFinalizationRequestV3,
54
54
  } from "../core/agents/session-finalizer-v3.ts";
55
- import { SESSION_NAME_DISPLAY_NOTE } from "../core/agents/session-name-display.ts";
55
+ import {
56
+ SESSION_NAME_DISPLAY_NOTE,
57
+ sessionNameDisplayPending,
58
+ } from "../core/agents/session-name-display.ts";
56
59
  import {
57
60
  buildLifecycleSuggestedName,
58
61
  buildSuggestedName,
@@ -89,6 +92,7 @@ import { readLedgerV3 } from "../core/events/v3/reader.ts";
89
92
  import { EVENT_V3_LEDGER_RELATIVE_ROOT } from "../core/events/v3/writer.ts";
90
93
  import type { RunQualitySnapshot, RunQualityStatus } from "../core/guard/index.ts";
91
94
  import { evaluateRunQualityIfDue } from "../core/guard/index.ts";
95
+ import { readRuntimeContextUsage } from "../core/hooks/adapter/runtime-telemetry.ts";
92
96
  import { type RemoteMachine, readRemoteMachines } from "../core/presence/index.ts";
93
97
  import { registerContextCommand } from "./context.ts";
94
98
 
@@ -152,9 +156,8 @@ function freshnessCutoffSecs(): number {
152
156
  }
153
157
 
154
158
  const SUBAGENT_NOTE =
155
- "Bash identity is process-level; if you're running inside a subagent, " +
156
- "this resolves to the parent group's name, not the subagent's. A subagent-aware " +
157
- "bridge (per-shell marker file at .harnery/shells/<pid>) is out of scope.";
159
+ "Native adapter subagents resolve to their own child generation after the hook bridge " +
160
+ "observes delegation. An unbridged subprocess shell still resolves through its parent.";
158
161
 
159
162
  /**
160
163
  * Spawn options for agent-coord child processes: pin the coord root the
@@ -248,19 +251,7 @@ function taskStateOf(hb: Pick<Heartbeat, "task_state">): TaskState {
248
251
  return hb.task_state ?? "active";
249
252
  }
250
253
 
251
- /** Producer joins use the private native session ID; `session_id` is the canonical V3 fingerprint. */
252
- export function nativeSessionIdentity(
253
- row: Pick<Heartbeat, "native_session_id" | "session_id"> | null | undefined,
254
- fallback: string,
255
- ): string {
256
- if (row?.native_session_id) return row.native_session_id;
257
- // A projection-only row carries the privacy-safe canonical session
258
- // fingerprint, not the adapter's native session ID needed to join the hook
259
- // producer. Until a disposable cache exists, the resolved owner is the only
260
- // native identity available to command surfaces such as `agents status`.
261
- if (row?.session_id && !/^sid_[a-f0-9]{64}$/.test(row.session_id)) return row.session_id;
262
- return fallback;
263
- }
254
+ export { nativeSessionIdentity };
264
255
 
265
256
  function lifecycleLabel(hb: Pick<Heartbeat, "task_state" | "task_state_reason">): string {
266
257
  const state = taskStateOf(hb);
@@ -329,9 +320,9 @@ export function registerAgentsCommand(
329
320
  .command("suggest-name [description...]")
330
321
  .description(
331
322
  'Reprint a session name ("Agent <you> - <description>") for the operator to set as their ' +
332
- "adapter tab title. With no arg, derives it from your current task. The primary naming path " +
323
+ "adapter tab title. With no arg, prefers the exact pending name, then derives from your current task. The primary naming path " +
333
324
  "is set-task (it suggests a name on the first focus declaration); reach for this to reprint " +
334
- "or re-suggest after a topic pivot. Read-only.",
325
+ "or to create an explicit pending-display retry with --json. Read-only.",
335
326
  )
336
327
  .option("--json", "JSON output instead of the bare name")
337
328
  .option(
@@ -1854,27 +1845,18 @@ function runSetTask(task: string, opts?: { sessionId?: string }): void {
1854
1845
  // the first time. `first_of_session: true` means exactly "this call produced
1855
1846
  // the session name" — it is never true with a null name (an empty first
1856
1847
  // declaration or a `--session-id` relay both keep the window open instead).
1857
- const firstSuggestedName =
1858
- !priorHb?.suggested_session_name && hb?.suggested_session_name
1859
- ? hb.suggested_session_name
1860
- : null;
1861
-
1862
- // A readable but misclassified first display would otherwise leave the
1863
- // session permanently latched: set-task is allowed through the gate, but a
1864
- // normal repeat used to return no name and therefore could not establish a
1865
- // fresh ordered transcript boundary. Re-emit the unchanged pending name as
1866
- // an explicit retry. This does not rename the session or claim that the
1867
- // display was seen; it only gives the next exact block new mint evidence.
1868
- const retriedSuggestedName =
1869
- !firstSuggestedName &&
1870
- priorHb?.suggested_session_name &&
1871
- priorHb.session_name_seen_for !== priorHb.suggested_session_name &&
1872
- hb?.suggested_session_name === priorHb.suggested_session_name
1873
- ? hb.suggested_session_name
1874
- : null;
1875
- const suggestedName = firstSuggestedName ?? retriedSuggestedName;
1848
+ // A native generation restart may rebuild the current cache during the
1849
+ // mutation and carry an already-seen suggestion from the prior generation.
1850
+ // That title is continuity evidence, not a new display request.
1851
+ const pendingName = sessionNameDisplayPending(hb);
1852
+ const firstSuggestedName = !priorHb?.suggested_session_name && pendingName ? pendingName : null;
1853
+
1854
+ // Routine focus declarations stay title-silent. A pending display no longer
1855
+ // turns every repeated set-task into a fresh name instruction; explicit
1856
+ // recovery belongs to `agents suggest-name --json`.
1857
+ const suggestedName = firstSuggestedName;
1876
1858
  const firstOfSession = firstSuggestedName !== null;
1877
- const sessionNameRetry = retriedSuggestedName !== null;
1859
+ const sessionNameRetry = false;
1878
1860
 
1879
1861
  emit.data({
1880
1862
  instance_id: myOwner,
@@ -2110,12 +2092,16 @@ function runSuggestName(
2110
2092
 
2111
2093
  const hb = readCurrentCoordinationRow(myOwner);
2112
2094
  const agentName = hb?.name || "unknown";
2113
- // Description resolution: an explicit arg wins; with no arg, fall back to the
2114
- // agent's current declared task, so a bare `suggest-name` reprints the running
2115
- // session's name (handy after a topic pivot). Read-only — mutates no state;
2116
- // the primary naming path is set-task's first-of-session suggestion.
2095
+ // A bare suggest-name must prefer the exact pending suggestion over rebuilding
2096
+ // from the current task. The task may have changed since the title was minted;
2097
+ // reconstructing it would produce a block the display latch can never accept.
2098
+ // An explicit description keeps the read-only re-suggest behavior.
2099
+ const pendingName = sessionNameDisplayPending(hb);
2117
2100
  const parts = descriptionParts.length > 0 ? descriptionParts : hb?.task ? [hb.task] : [];
2118
- const built = buildSuggestedName(agentName, parts);
2101
+ const built =
2102
+ descriptionParts.length === 0 && pendingName
2103
+ ? { suggestedName: pendingName, description: hb?.task ?? pendingName }
2104
+ : buildSuggestedName(agentName, parts);
2119
2105
  if (!built) {
2120
2106
  emit.error({
2121
2107
  code: "no_description",
@@ -2132,6 +2118,7 @@ function runSuggestName(
2132
2118
  emit.data({
2133
2119
  name: displayName,
2134
2120
  suggested_session_name: suggestedName,
2121
+ session_name_retry: pendingName === suggestedName,
2135
2122
  agent_name: agentName,
2136
2123
  description,
2137
2124
  });
@@ -2536,9 +2523,9 @@ function readContextUsage(
2536
2523
  // Cursor doesn't surface token counts in its transcript JSONL but DOES
2537
2524
  // store `contextUsagePercent` per composer in workspaceStorage's state.vscdb;
2538
2525
  // readCursorContextUsage reads that via bun:sqlite (percent-only).
2539
- if (platform === "codex") return readCodexContextUsage(sessionId);
2526
+ if (platform === "codex") return readRuntimeContextUsage("codex", sessionId);
2540
2527
  if (platform === "cursor") return readCursorContextUsage(sessionId);
2541
- return readClaudeContextUsage(sessionId);
2528
+ return readRuntimeContextUsage("claude-code", sessionId);
2542
2529
  }
2543
2530
 
2544
2531
  function readCursorContextUsage(
@@ -2609,126 +2596,6 @@ function readCursorContextUsage(
2609
2596
  return null;
2610
2597
  }
2611
2598
 
2612
- function readClaudeContextUsage(sessionId: string): { used: number; window: number } | null {
2613
- const root = monorepoRoot();
2614
- if (!root) return null;
2615
- // Claude Code's project dir scheme: prepend "-", replace "/" → "-".
2616
- const encoded = `-${root.replace(/^\//, "").replace(/\//g, "-")}`;
2617
- const transcriptPath = resolve(homedir(), ".claude", "projects", encoded, `${sessionId}.jsonl`);
2618
- if (!existsSync(transcriptPath)) return null;
2619
- let raw: string;
2620
- try {
2621
- raw = readFileSync(transcriptPath, "utf8");
2622
- } catch {
2623
- return null;
2624
- }
2625
- const lines = raw.split("\n");
2626
- for (let i = lines.length - 1; i >= 0; i--) {
2627
- const line = lines[i].trim();
2628
- if (!line) continue;
2629
- try {
2630
- const entry = JSON.parse(line);
2631
- const usage = entry?.message?.usage;
2632
- if (entry?.type === "assistant" && usage) {
2633
- const used =
2634
- (usage.input_tokens ?? 0) +
2635
- (usage.cache_creation_input_tokens ?? 0) +
2636
- (usage.cache_read_input_tokens ?? 0);
2637
- // Window: hardcode 1M for Opus 4.7 1M (current default). Refine to
2638
- // model-aware lookup when other models become routine.
2639
- return { used, window: 1000000 };
2640
- }
2641
- } catch {
2642
- // skip malformed line
2643
- }
2644
- }
2645
- return null;
2646
- }
2647
-
2648
- function readCodexContextUsage(sessionId: string): { used: number; window: number } | null {
2649
- // Codex transcripts: ~/.codex/sessions/YYYY/MM/DD/rollout-<TS>-<sessionId>.jsonl
2650
- // On WSL the active install often lives on the Windows side under
2651
- // /mnt/c/Users/<user>/.codex/sessions/; both are searched.
2652
- const path = findCodexTranscript(sessionId);
2653
- if (!path) return null;
2654
- let raw: string;
2655
- try {
2656
- raw = readFileSync(path, "utf8");
2657
- } catch {
2658
- return null;
2659
- }
2660
- const lines = raw.split("\n");
2661
- // Walk backwards: most recent token_count event wins.
2662
- for (let i = lines.length - 1; i >= 0; i--) {
2663
- const line = lines[i].trim();
2664
- if (!line) continue;
2665
- try {
2666
- const entry = JSON.parse(line);
2667
- if (entry?.type === "event_msg" && entry?.payload?.type === "token_count") {
2668
- const info = entry.payload.info;
2669
- const used = info?.last_token_usage?.input_tokens;
2670
- const window = info?.model_context_window;
2671
- if (typeof used === "number" && typeof window === "number" && window > 0) {
2672
- return { used, window };
2673
- }
2674
- }
2675
- } catch {
2676
- // skip malformed line
2677
- }
2678
- }
2679
- return null;
2680
- }
2681
-
2682
- function findCodexTranscript(sessionId: string): string | null {
2683
- // Candidate roots in priority order.
2684
- const homeRoot = resolve(homedir(), ".codex", "sessions");
2685
- const wslRoots: string[] = [];
2686
- try {
2687
- if (existsSync("/mnt/c/Users")) {
2688
- for (const entry of readdirSync("/mnt/c/Users")) {
2689
- wslRoots.push(`/mnt/c/Users/${entry}/.codex/sessions`);
2690
- }
2691
- }
2692
- } catch {
2693
- // ignore
2694
- }
2695
- const roots = [homeRoot, ...wslRoots];
2696
- const suffix = `-${sessionId}.jsonl`;
2697
- for (const root of roots) {
2698
- if (!existsSync(root)) continue;
2699
- try {
2700
- // Recursive scan: sessions are partitioned by YYYY/MM/DD/, so depth
2701
- // is bounded at 3 + one file per session. Cheap enough at status time.
2702
- const stack: string[] = [root];
2703
- while (stack.length) {
2704
- const dir = stack.pop()!;
2705
- let entries: string[];
2706
- try {
2707
- entries = readdirSync(dir);
2708
- } catch {
2709
- continue;
2710
- }
2711
- for (const name of entries) {
2712
- const full = `${dir}/${name}`;
2713
- try {
2714
- const stat = statSync(full);
2715
- if (stat.isDirectory()) {
2716
- stack.push(full);
2717
- } else if (name.endsWith(suffix)) {
2718
- return full;
2719
- }
2720
- } catch {
2721
- // ignore
2722
- }
2723
- }
2724
- }
2725
- } catch {
2726
- // ignore root scan failures
2727
- }
2728
- }
2729
- return null;
2730
- }
2731
-
2732
2599
  function formatLocalTime(d: Date): string {
2733
2600
  // "Sat, May 9, 2026, 3:48 AM CDT", rendered in the Chicago timezone.
2734
2601
  return new Intl.DateTimeFormat("en-US", {
@@ -3136,6 +3003,15 @@ interface HealthReport {
3136
3003
  // V3 event-ledger producer health: open tool spans, pending finalization
3137
3004
  // requests, intake/diagnostics spool depth, span-count pressure.
3138
3005
  event_ledger: EventLedgerHealthV3;
3006
+ // Stop-hook remediation cap exhaustions in the window: sessions whose
3007
+ // end-of-turn evidence never landed, so the hook stopped bouncing them and
3008
+ // let the turn end unenforced. Each one is a session that could not comply —
3009
+ // the signal that used to be visible only as a repeating notification sound.
3010
+ stop_remediation: {
3011
+ total: number;
3012
+ latest_at: string | null;
3013
+ sessions: string[];
3014
+ };
3139
3015
  anomalies: string[];
3140
3016
  }
3141
3017
 
@@ -3342,6 +3218,50 @@ export function collectEventLedgerHealthV3(root: string, nowMs = Date.now()): Ev
3342
3218
  };
3343
3219
  }
3344
3220
 
3221
+ /**
3222
+ * Tally stop-hook remediation-cap exhaustions in the window from
3223
+ * `.harnery/debug/agent-hook.ndjson` (rows with
3224
+ * `skipped: "stop-remediation-cap-exhausted"`). Each one is a session whose
3225
+ * end-of-turn evidence never landed, so the Stop hook gave up bouncing it and
3226
+ * let the turn end unenforced — a compliance gap an operator should see
3227
+ * without grepping the debug ledger.
3228
+ */
3229
+ export function readStopRemediationExhaustions(
3230
+ root: string,
3231
+ cutoffMs: number,
3232
+ ): { total: number; latestAt: string | null; sessions: string[] } {
3233
+ const p = resolve(root, ".harnery", "debug", "agent-hook.ndjson");
3234
+ if (!existsSync(p)) return { total: 0, latestAt: null, sessions: [] };
3235
+ let raw: string;
3236
+ try {
3237
+ raw = readFileSync(p, "utf8");
3238
+ } catch {
3239
+ return { total: 0, latestAt: null, sessions: [] };
3240
+ }
3241
+ let total = 0;
3242
+ let latestMs = Number.NEGATIVE_INFINITY;
3243
+ const sessions = new Set<string>();
3244
+ for (const line of raw.split("\n")) {
3245
+ if (!line.includes("stop-remediation-cap-exhausted")) continue;
3246
+ try {
3247
+ const e = JSON.parse(line) as { ts?: string; skipped?: string; session_id?: string };
3248
+ if (e.skipped !== "stop-remediation-cap-exhausted") continue;
3249
+ const tsMs = e.ts ? Date.parse(e.ts) : Number.NaN;
3250
+ if (!Number.isFinite(tsMs) || tsMs < cutoffMs) continue;
3251
+ total++;
3252
+ latestMs = Math.max(latestMs, tsMs);
3253
+ if (e.session_id && sessions.size < 5) sessions.add(e.session_id);
3254
+ } catch {
3255
+ /* skip malformed */
3256
+ }
3257
+ }
3258
+ return {
3259
+ total,
3260
+ latestAt: Number.isFinite(latestMs) ? new Date(latestMs).toISOString() : null,
3261
+ sessions: [...sessions],
3262
+ };
3263
+ }
3264
+
3345
3265
  /** Tally agent-hook failures (.harnery/debug/agent-hook.errors.ndjson) in the
3346
3266
  * window by exact error and phase, while separating current-hour failures from
3347
3267
  * historical rows. Each line is {ts, error, phase, ...}. */
@@ -3902,6 +3822,7 @@ function runHealth(opts: { since: string; json?: boolean }): void {
3902
3822
  }
3903
3823
 
3904
3824
  const hookErrors = readHookErrors(root, cutoffMs, nowMs);
3825
+ const stopRemediation = readStopRemediationExhaustions(root, cutoffMs);
3905
3826
  const stream = readStreamStats(root);
3906
3827
  const eventLedger = collectEventLedgerHealthV3(root);
3907
3828
 
@@ -4021,6 +3942,15 @@ function runHealth(opts: { since: string; json?: boolean }): void {
4021
3942
  `${zombieCount} zombie heartbeat(s) in active/ (${zombieSamples.join(", ")}); broken files the sweep isn't reaping`,
4022
3943
  );
4023
3944
  }
3945
+ if (stopRemediation.total > 0) {
3946
+ const who =
3947
+ stopRemediation.sessions.length > 0
3948
+ ? ` (sessions: ${stopRemediation.sessions.map((s) => s.slice(0, 8)).join(", ")})`
3949
+ : "";
3950
+ anomalies.push(
3951
+ `stop-hook remediation cap exhausted ${stopRemediation.total}x in ${opts.since}${who}; these sessions ended turns without the end-of-turn ritual — check why their evidence never lands`,
3952
+ );
3953
+ }
4024
3954
  if (eventLedger.state === "live") {
4025
3955
  // Open spans whose generation has no open turn: the turn ended without its
4026
3956
  // tool spans closing, so an explicit end can only queue, never finalize.
@@ -4089,6 +4019,11 @@ function runHealth(opts: { since: string; json?: boolean }): void {
4089
4019
  stream,
4090
4020
  zombies: { count: zombieCount, samples: zombieSamples },
4091
4021
  event_ledger: eventLedger,
4022
+ stop_remediation: {
4023
+ total: stopRemediation.total,
4024
+ latest_at: stopRemediation.latestAt,
4025
+ sessions: stopRemediation.sessions,
4026
+ },
4092
4027
  anomalies,
4093
4028
  };
4094
4029
 
@@ -24,6 +24,7 @@ import {
24
24
  parseAssertSpec,
25
25
  type RuntsResult,
26
26
  runCritique,
27
+ type StandaloneHtmlResult,
27
28
  type TargetSizeProfile,
28
29
  type TargetSizeResult,
29
30
  tilesFromFullPage,
@@ -1529,9 +1530,22 @@ async function runTrioMode(
1529
1530
  throw new Error("--capture-evaluate requires a screenshot; remove --no-screenshot.");
1530
1531
  }
1531
1532
 
1533
+ // A trio `.html` is opened in a browser later — from the files origin, from
1534
+ // another machine, from disk — so it has to carry its own CSS and media. A
1535
+ // selector capture is a DOM fragment, not a document, and stays raw.
1532
1536
  const htmlPath = `${prefix}.html`;
1533
- const html = await browser.htmlContent(opts.selector);
1534
- writeFileSync(htmlPath, html);
1537
+ let snapshot: StandaloneHtmlResult | undefined;
1538
+ if (opts.selector) {
1539
+ writeFileSync(htmlPath, await browser.htmlContent(opts.selector));
1540
+ } else {
1541
+ try {
1542
+ snapshot = await browser.standaloneHtml();
1543
+ writeFileSync(htmlPath, snapshot.html);
1544
+ } catch {
1545
+ // Inlining is best-effort; a raw serialization beats no snapshot at all.
1546
+ writeFileSync(htmlPath, await browser.htmlContent());
1547
+ }
1548
+ }
1535
1549
  written.push(htmlPath);
1536
1550
 
1537
1551
  const diag = browser.diagnostics();
@@ -1548,6 +1562,15 @@ async function runTrioMode(
1548
1562
  ...summarizeDiagnostics(diag),
1549
1563
  };
1550
1564
  if (pngBytes !== undefined) envelope.screenshotBytes = pngBytes;
1565
+ if (snapshot) {
1566
+ envelope.htmlSnapshot = {
1567
+ stylesheetsInlined: snapshot.stylesheetsInlined,
1568
+ stylesheetsLinked: snapshot.stylesheetsLinked,
1569
+ resourcesInlined: snapshot.resourcesInlined,
1570
+ resourcesLinked: snapshot.resourcesLinked,
1571
+ inlinedBytes: snapshot.inlinedBytes,
1572
+ };
1573
+ }
1551
1574
  if (opts.evaluate) envelope.eval = evalResult;
1552
1575
  if (opts.captureEvaluate) {
1553
1576
  envelope.captureEval = captureEvalResult;
@@ -1,7 +1,13 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import type { Command } from "commander";
3
3
  import type { EmitContext } from "../commander.ts";
4
- import { emitEventV3, monorepoRoot, normalizeAdapter, resolveOwner } from "../core/agents/index.ts";
4
+ import {
5
+ emitEventV3,
6
+ monorepoRoot,
7
+ nativeSessionIdentity,
8
+ normalizeAdapter,
9
+ resolveOwner,
10
+ } from "../core/agents/index.ts";
5
11
  import { readLiveCoordinationRow } from "../core/agents/state/live-coordination-view.ts";
6
12
  import { resolveBinName } from "../core/config.ts";
7
13
  import {
@@ -442,7 +448,7 @@ function emitDecisionEvent(
442
448
  const hb = root ? readLiveCoordinationRow(root, owner) : null;
443
449
  emitEventV3({
444
450
  owner,
445
- session: hb?.session_id ?? owner,
451
+ session: nativeSessionIdentity(hb, owner),
446
452
  adapter: normalizeAdapter(hb?.platform),
447
453
  observation: {
448
454
  event_type: "decision.state_changed",
@@ -25,6 +25,7 @@ interface GenerationLatencyRow {
25
25
  tool_ms: LatencyMetricV3;
26
26
  command_ms: LatencyMetricV3;
27
27
  wait_ms: LatencyMetricV3;
28
+ agent_action_ms: LatencyMetricV3;
28
29
  inference_ms: LatencyMetricV3;
29
30
  harness_ms: LatencyMetricV3;
30
31
  residual_ms: LatencyMetricV3;
@@ -112,6 +113,9 @@ function summarizeByGeneration(turns: TurnLatencyV3[]): GenerationLatencyRow[] {
112
113
  tool_ms: sumMetrics(group.map(({ tool_ms }) => tool_ms)),
113
114
  command_ms: sumMetrics(group.map(({ command_exclusive_ms }) => command_exclusive_ms)),
114
115
  wait_ms: sumMetrics(group.map(({ wait_ms }) => wait_ms)),
116
+ agent_action_ms: sumMetrics(
117
+ group.map(({ response_latency }) => response_latency.agent_action_ms),
118
+ ),
115
119
  inference_ms: sumMetrics(group.map(({ inference_ms }) => inference_ms)),
116
120
  harness_ms: sumMetrics(group.map(({ harness_ms }) => harness_ms)),
117
121
  residual_ms: sumMetrics(group.map(({ residual_ms }) => residual_ms)),
@@ -180,6 +184,8 @@ function renderTurns(turns: TurnLatencyV3[]): string {
180
184
  "TOOL",
181
185
  "COMMAND",
182
186
  "WAIT",
187
+ "ACTION",
188
+ "POST-TOOL",
183
189
  "INFERENCE",
184
190
  "HARNESS",
185
191
  "RESIDUAL",
@@ -192,6 +198,8 @@ function renderTurns(turns: TurnLatencyV3[]): string {
192
198
  formatMetric(turn.tool_ms),
193
199
  formatMetric(turn.command_exclusive_ms),
194
200
  formatMetric(turn.wait_ms),
201
+ formatMetric(turn.response_latency.agent_action_ms),
202
+ formatMetric(turn.response_latency.post_tool_response_ms),
195
203
  formatMetric(turn.inference_ms),
196
204
  formatMetric(turn.harness_ms),
197
205
  formatMetric(turn.residual_ms),
@@ -202,7 +210,18 @@ function renderTurns(turns: TurnLatencyV3[]): string {
202
210
 
203
211
  function renderGenerations(rows: GenerationLatencyRow[]): string {
204
212
  return renderTable(
205
- ["GENERATION", "TURNS", "WALL", "TOOL", "COMMAND", "WAIT", "INFERENCE", "HARNESS", "RESIDUAL"],
213
+ [
214
+ "GENERATION",
215
+ "TURNS",
216
+ "WALL",
217
+ "TOOL",
218
+ "COMMAND",
219
+ "WAIT",
220
+ "ACTION",
221
+ "INFERENCE",
222
+ "HARNESS",
223
+ "RESIDUAL",
224
+ ],
206
225
  rows.map((row) => [
207
226
  row.generation_id,
208
227
  String(row.turns),
@@ -210,6 +229,7 @@ function renderGenerations(rows: GenerationLatencyRow[]): string {
210
229
  formatMetric(row.tool_ms),
211
230
  formatMetric(row.command_ms),
212
231
  formatMetric(row.wait_ms),
232
+ formatMetric(row.agent_action_ms),
213
233
  formatMetric(row.inference_ms),
214
234
  formatMetric(row.harness_ms),
215
235
  formatMetric(row.residual_ms),
@@ -1,7 +1,12 @@
1
1
  import type { Command } from "commander";
2
2
  import type { EmitContext } from "../commander.ts";
3
3
  import { monorepoRoot } from "../core/agents/coord-client.ts";
4
- import { emitEventV3, normalizeAdapter, resolveOwner } from "../core/agents/index.ts";
4
+ import {
5
+ emitEventV3,
6
+ nativeSessionIdentity,
7
+ normalizeAdapter,
8
+ resolveOwner,
9
+ } from "../core/agents/index.ts";
5
10
  import { readLiveCoordinationRow } from "../core/agents/state/live-coordination-view.ts";
6
11
  import {
7
12
  fetchPresence,
@@ -253,7 +258,7 @@ function emitPresenceChange(
253
258
  const hb = root ? readLiveCoordinationRow(root, owner) : null;
254
259
  emitEventV3({
255
260
  owner,
256
- session: hb?.session_id ?? owner,
261
+ session: nativeSessionIdentity(hb, owner),
257
262
  adapter: normalizeAdapter(hb?.platform),
258
263
  observation: {
259
264
  event_type: "coord.presence_changed",