harnery 0.34.0 → 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 +91 -162
  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 +2 -1
  37. package/dist/core/agents/session-name-display.d.ts.map +1 -1
  38. package/dist/core/agents/session-name-display.js +10 -3
  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 +114 -38
  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 +35 -17
  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 +104 -152
  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 +11 -3
  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 +126 -41
  195. package/src/core/hooks/remediation-cap.ts +77 -0
  196. package/src/core/hooks/resolve/transcript.ts +49 -17
  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,11 +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 suggestedName =
1858
- !priorHb?.suggested_session_name && hb?.suggested_session_name
1859
- ? hb.suggested_session_name
1860
- : null;
1861
- const firstOfSession = suggestedName !== null;
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;
1858
+ const firstOfSession = firstSuggestedName !== null;
1859
+ const sessionNameRetry = false;
1862
1860
 
1863
1861
  emit.data({
1864
1862
  instance_id: myOwner,
@@ -1866,6 +1864,7 @@ function runSetTask(task: string, opts?: { sessionId?: string }): void {
1866
1864
  task: hb?.task ?? null,
1867
1865
  cleared: !task || task.length === 0,
1868
1866
  first_of_session: firstOfSession,
1867
+ session_name_retry: sessionNameRetry,
1869
1868
  suggested_session_name: suggestedName,
1870
1869
  ...(lifecycleWarning ? { warning: lifecycleWarning } : {}),
1871
1870
  // Right-time instruction: the UserPromptSubmit nudge fires before the name
@@ -2093,12 +2092,16 @@ function runSuggestName(
2093
2092
 
2094
2093
  const hb = readCurrentCoordinationRow(myOwner);
2095
2094
  const agentName = hb?.name || "unknown";
2096
- // Description resolution: an explicit arg wins; with no arg, fall back to the
2097
- // agent's current declared task, so a bare `suggest-name` reprints the running
2098
- // session's name (handy after a topic pivot). Read-only — mutates no state;
2099
- // 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);
2100
2100
  const parts = descriptionParts.length > 0 ? descriptionParts : hb?.task ? [hb.task] : [];
2101
- const built = buildSuggestedName(agentName, parts);
2101
+ const built =
2102
+ descriptionParts.length === 0 && pendingName
2103
+ ? { suggestedName: pendingName, description: hb?.task ?? pendingName }
2104
+ : buildSuggestedName(agentName, parts);
2102
2105
  if (!built) {
2103
2106
  emit.error({
2104
2107
  code: "no_description",
@@ -2115,6 +2118,7 @@ function runSuggestName(
2115
2118
  emit.data({
2116
2119
  name: displayName,
2117
2120
  suggested_session_name: suggestedName,
2121
+ session_name_retry: pendingName === suggestedName,
2118
2122
  agent_name: agentName,
2119
2123
  description,
2120
2124
  });
@@ -2519,9 +2523,9 @@ function readContextUsage(
2519
2523
  // Cursor doesn't surface token counts in its transcript JSONL but DOES
2520
2524
  // store `contextUsagePercent` per composer in workspaceStorage's state.vscdb;
2521
2525
  // readCursorContextUsage reads that via bun:sqlite (percent-only).
2522
- if (platform === "codex") return readCodexContextUsage(sessionId);
2526
+ if (platform === "codex") return readRuntimeContextUsage("codex", sessionId);
2523
2527
  if (platform === "cursor") return readCursorContextUsage(sessionId);
2524
- return readClaudeContextUsage(sessionId);
2528
+ return readRuntimeContextUsage("claude-code", sessionId);
2525
2529
  }
2526
2530
 
2527
2531
  function readCursorContextUsage(
@@ -2592,126 +2596,6 @@ function readCursorContextUsage(
2592
2596
  return null;
2593
2597
  }
2594
2598
 
2595
- function readClaudeContextUsage(sessionId: string): { used: number; window: number } | null {
2596
- const root = monorepoRoot();
2597
- if (!root) return null;
2598
- // Claude Code's project dir scheme: prepend "-", replace "/" → "-".
2599
- const encoded = `-${root.replace(/^\//, "").replace(/\//g, "-")}`;
2600
- const transcriptPath = resolve(homedir(), ".claude", "projects", encoded, `${sessionId}.jsonl`);
2601
- if (!existsSync(transcriptPath)) return null;
2602
- let raw: string;
2603
- try {
2604
- raw = readFileSync(transcriptPath, "utf8");
2605
- } catch {
2606
- return null;
2607
- }
2608
- const lines = raw.split("\n");
2609
- for (let i = lines.length - 1; i >= 0; i--) {
2610
- const line = lines[i].trim();
2611
- if (!line) continue;
2612
- try {
2613
- const entry = JSON.parse(line);
2614
- const usage = entry?.message?.usage;
2615
- if (entry?.type === "assistant" && usage) {
2616
- const used =
2617
- (usage.input_tokens ?? 0) +
2618
- (usage.cache_creation_input_tokens ?? 0) +
2619
- (usage.cache_read_input_tokens ?? 0);
2620
- // Window: hardcode 1M for Opus 4.7 1M (current default). Refine to
2621
- // model-aware lookup when other models become routine.
2622
- return { used, window: 1000000 };
2623
- }
2624
- } catch {
2625
- // skip malformed line
2626
- }
2627
- }
2628
- return null;
2629
- }
2630
-
2631
- function readCodexContextUsage(sessionId: string): { used: number; window: number } | null {
2632
- // Codex transcripts: ~/.codex/sessions/YYYY/MM/DD/rollout-<TS>-<sessionId>.jsonl
2633
- // On WSL the active install often lives on the Windows side under
2634
- // /mnt/c/Users/<user>/.codex/sessions/; both are searched.
2635
- const path = findCodexTranscript(sessionId);
2636
- if (!path) return null;
2637
- let raw: string;
2638
- try {
2639
- raw = readFileSync(path, "utf8");
2640
- } catch {
2641
- return null;
2642
- }
2643
- const lines = raw.split("\n");
2644
- // Walk backwards: most recent token_count event wins.
2645
- for (let i = lines.length - 1; i >= 0; i--) {
2646
- const line = lines[i].trim();
2647
- if (!line) continue;
2648
- try {
2649
- const entry = JSON.parse(line);
2650
- if (entry?.type === "event_msg" && entry?.payload?.type === "token_count") {
2651
- const info = entry.payload.info;
2652
- const used = info?.last_token_usage?.input_tokens;
2653
- const window = info?.model_context_window;
2654
- if (typeof used === "number" && typeof window === "number" && window > 0) {
2655
- return { used, window };
2656
- }
2657
- }
2658
- } catch {
2659
- // skip malformed line
2660
- }
2661
- }
2662
- return null;
2663
- }
2664
-
2665
- function findCodexTranscript(sessionId: string): string | null {
2666
- // Candidate roots in priority order.
2667
- const homeRoot = resolve(homedir(), ".codex", "sessions");
2668
- const wslRoots: string[] = [];
2669
- try {
2670
- if (existsSync("/mnt/c/Users")) {
2671
- for (const entry of readdirSync("/mnt/c/Users")) {
2672
- wslRoots.push(`/mnt/c/Users/${entry}/.codex/sessions`);
2673
- }
2674
- }
2675
- } catch {
2676
- // ignore
2677
- }
2678
- const roots = [homeRoot, ...wslRoots];
2679
- const suffix = `-${sessionId}.jsonl`;
2680
- for (const root of roots) {
2681
- if (!existsSync(root)) continue;
2682
- try {
2683
- // Recursive scan: sessions are partitioned by YYYY/MM/DD/, so depth
2684
- // is bounded at 3 + one file per session. Cheap enough at status time.
2685
- const stack: string[] = [root];
2686
- while (stack.length) {
2687
- const dir = stack.pop()!;
2688
- let entries: string[];
2689
- try {
2690
- entries = readdirSync(dir);
2691
- } catch {
2692
- continue;
2693
- }
2694
- for (const name of entries) {
2695
- const full = `${dir}/${name}`;
2696
- try {
2697
- const stat = statSync(full);
2698
- if (stat.isDirectory()) {
2699
- stack.push(full);
2700
- } else if (name.endsWith(suffix)) {
2701
- return full;
2702
- }
2703
- } catch {
2704
- // ignore
2705
- }
2706
- }
2707
- }
2708
- } catch {
2709
- // ignore root scan failures
2710
- }
2711
- }
2712
- return null;
2713
- }
2714
-
2715
2599
  function formatLocalTime(d: Date): string {
2716
2600
  // "Sat, May 9, 2026, 3:48 AM CDT", rendered in the Chicago timezone.
2717
2601
  return new Intl.DateTimeFormat("en-US", {
@@ -3119,6 +3003,15 @@ interface HealthReport {
3119
3003
  // V3 event-ledger producer health: open tool spans, pending finalization
3120
3004
  // requests, intake/diagnostics spool depth, span-count pressure.
3121
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
+ };
3122
3015
  anomalies: string[];
3123
3016
  }
3124
3017
 
@@ -3325,6 +3218,50 @@ export function collectEventLedgerHealthV3(root: string, nowMs = Date.now()): Ev
3325
3218
  };
3326
3219
  }
3327
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
+
3328
3265
  /** Tally agent-hook failures (.harnery/debug/agent-hook.errors.ndjson) in the
3329
3266
  * window by exact error and phase, while separating current-hour failures from
3330
3267
  * historical rows. Each line is {ts, error, phase, ...}. */
@@ -3885,6 +3822,7 @@ function runHealth(opts: { since: string; json?: boolean }): void {
3885
3822
  }
3886
3823
 
3887
3824
  const hookErrors = readHookErrors(root, cutoffMs, nowMs);
3825
+ const stopRemediation = readStopRemediationExhaustions(root, cutoffMs);
3888
3826
  const stream = readStreamStats(root);
3889
3827
  const eventLedger = collectEventLedgerHealthV3(root);
3890
3828
 
@@ -4004,6 +3942,15 @@ function runHealth(opts: { since: string; json?: boolean }): void {
4004
3942
  `${zombieCount} zombie heartbeat(s) in active/ (${zombieSamples.join(", ")}); broken files the sweep isn't reaping`,
4005
3943
  );
4006
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
+ }
4007
3954
  if (eventLedger.state === "live") {
4008
3955
  // Open spans whose generation has no open turn: the turn ended without its
4009
3956
  // tool spans closing, so an explicit end can only queue, never finalize.
@@ -4072,6 +4019,11 @@ function runHealth(opts: { since: string; json?: boolean }): void {
4072
4019
  stream,
4073
4020
  zombies: { count: zombieCount, samples: zombieSamples },
4074
4021
  event_ledger: eventLedger,
4022
+ stop_remediation: {
4023
+ total: stopRemediation.total,
4024
+ latest_at: stopRemediation.latestAt,
4025
+ sessions: stopRemediation.sessions,
4026
+ },
4075
4027
  anomalies,
4076
4028
  };
4077
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",