harnery 0.8.0 → 0.10.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 (234) hide show
  1. package/README.md +2 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commander.js +8 -0
  4. package/dist/commands/agents.d.ts +24 -0
  5. package/dist/commands/agents.d.ts.map +1 -1
  6. package/dist/commands/agents.js +244 -13
  7. package/dist/commands/backup.d.ts +5 -4
  8. package/dist/commands/backup.d.ts.map +1 -1
  9. package/dist/commands/backup.js +15 -14
  10. package/dist/commands/browse.d.ts.map +1 -1
  11. package/dist/commands/browse.js +27 -1
  12. package/dist/commands/claude-desktop.d.ts +19 -0
  13. package/dist/commands/claude-desktop.d.ts.map +1 -0
  14. package/dist/commands/claude-desktop.js +168 -0
  15. package/dist/commands/context.d.ts.map +1 -1
  16. package/dist/commands/context.js +149 -1
  17. package/dist/commands/doctor.d.ts.map +1 -1
  18. package/dist/commands/doctor.js +37 -0
  19. package/dist/commands/eml.d.ts +32 -0
  20. package/dist/commands/eml.d.ts.map +1 -1
  21. package/dist/commands/eml.js +16 -3
  22. package/dist/commands/grep.d.ts +35 -2
  23. package/dist/commands/grep.d.ts.map +1 -1
  24. package/dist/commands/grep.js +427 -139
  25. package/dist/commands/harness.d.ts +11 -0
  26. package/dist/commands/harness.d.ts.map +1 -0
  27. package/dist/commands/harness.js +118 -0
  28. package/dist/commands/init.d.ts +15 -5
  29. package/dist/commands/init.d.ts.map +1 -1
  30. package/dist/commands/init.js +125 -14
  31. package/dist/commands/presence.d.ts +9 -4
  32. package/dist/commands/presence.d.ts.map +1 -1
  33. package/dist/commands/presence.js +88 -5
  34. package/dist/commands/relay.d.ts +9 -0
  35. package/dist/commands/relay.d.ts.map +1 -0
  36. package/dist/commands/relay.js +143 -0
  37. package/dist/commands/sync.d.ts.map +1 -1
  38. package/dist/commands/sync.js +5 -0
  39. package/dist/commands/workflow.d.ts +4 -0
  40. package/dist/commands/workflow.d.ts.map +1 -0
  41. package/dist/commands/workflow.js +82 -0
  42. package/dist/core/agents/canonical-emit.d.ts +15 -0
  43. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  44. package/dist/core/agents/canonical-emit.js +41 -3
  45. package/dist/core/agents/cli.js +14 -5
  46. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  47. package/dist/core/agents/render/prompt-context.js +44 -3
  48. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  49. package/dist/core/agents/render/session-context.js +26 -1
  50. package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
  51. package/dist/core/agents/rules/claim-conflict.js +26 -1
  52. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
  53. package/dist/core/agents/rules/commit-conflict.js +4 -37
  54. package/dist/core/agents/rules/stop-hook.d.ts +8 -0
  55. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  56. package/dist/core/agents/rules/stop-hook.js +8 -0
  57. package/dist/core/agents/session-events.d.ts.map +1 -1
  58. package/dist/core/agents/session-events.js +16 -43
  59. package/dist/core/agents/state/heartbeat-projector.d.ts +2 -0
  60. package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
  61. package/dist/core/agents/state/heartbeat-projector.js +24 -4
  62. package/dist/core/agents/state/heartbeat-writer.d.ts +16 -2
  63. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  64. package/dist/core/agents/state/heartbeat-writer.js +24 -7
  65. package/dist/core/agents/state/names.d.ts +38 -3
  66. package/dist/core/agents/state/names.d.ts.map +1 -1
  67. package/dist/core/agents/state/names.js +46 -7
  68. package/dist/core/agents/state/pidmap.d.ts +5 -1
  69. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  70. package/dist/core/agents/state/pidmap.js +58 -2
  71. package/dist/core/agents/state/stale-sweep.d.ts +3 -2
  72. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  73. package/dist/core/agents/state/stale-sweep.js +6 -6
  74. package/dist/core/config.d.ts +77 -11
  75. package/dist/core/config.d.ts.map +1 -1
  76. package/dist/core/config.js +210 -25
  77. package/dist/core/context/index.d.ts +144 -0
  78. package/dist/core/context/index.d.ts.map +1 -0
  79. package/dist/core/context/index.js +380 -0
  80. package/dist/core/harnesses/bench.d.ts +29 -0
  81. package/dist/core/harnesses/bench.d.ts.map +1 -0
  82. package/dist/core/harnesses/bench.js +151 -0
  83. package/dist/core/harnesses/index.d.ts +9 -0
  84. package/dist/core/harnesses/index.d.ts.map +1 -0
  85. package/dist/core/harnesses/index.js +4 -0
  86. package/dist/core/harnesses/profiles.d.ts +62 -0
  87. package/dist/core/harnesses/profiles.d.ts.map +1 -0
  88. package/dist/core/harnesses/profiles.js +115 -0
  89. package/dist/core/harnesses/registry.d.ts +15 -0
  90. package/dist/core/harnesses/registry.d.ts.map +1 -0
  91. package/dist/core/harnesses/registry.js +113 -0
  92. package/dist/core/harnesses/types.d.ts +64 -0
  93. package/dist/core/harnesses/types.d.ts.map +1 -0
  94. package/dist/core/harnesses/types.js +20 -0
  95. package/dist/core/hooks/cli.js +309 -66
  96. package/dist/core/hooks/events/schema.d.ts +42 -1
  97. package/dist/core/hooks/events/schema.d.ts.map +1 -1
  98. package/dist/core/hooks/harness/events.d.ts +7 -0
  99. package/dist/core/hooks/harness/events.d.ts.map +1 -1
  100. package/dist/core/hooks/harness/events.js +4 -0
  101. package/dist/core/hooks/harness/parse.d.ts +1 -1
  102. package/dist/core/hooks/harness/parse.d.ts.map +1 -1
  103. package/dist/core/hooks/harness/parse.js +4 -0
  104. package/dist/core/hooks/resolve/coord-root.d.ts +11 -0
  105. package/dist/core/hooks/resolve/coord-root.d.ts.map +1 -1
  106. package/dist/core/hooks/resolve/coord-root.js +28 -6
  107. package/dist/core/presence/blob.d.ts +40 -0
  108. package/dist/core/presence/blob.d.ts.map +1 -0
  109. package/dist/core/presence/blob.js +91 -0
  110. package/dist/core/presence/git.d.ts +68 -0
  111. package/dist/core/presence/git.d.ts.map +1 -0
  112. package/dist/core/presence/git.js +173 -0
  113. package/dist/core/presence/index.d.ts +74 -0
  114. package/dist/core/presence/index.d.ts.map +1 -0
  115. package/dist/core/presence/index.js +224 -0
  116. package/dist/core/presence/relay-client.d.ts +39 -0
  117. package/dist/core/presence/relay-client.d.ts.map +1 -0
  118. package/dist/core/presence/relay-client.js +295 -0
  119. package/dist/core/presence/relay-protocol.d.ts +94 -0
  120. package/dist/core/presence/relay-protocol.d.ts.map +1 -0
  121. package/dist/core/presence/relay-protocol.js +169 -0
  122. package/dist/core/workflow/billing.d.ts +48 -0
  123. package/dist/core/workflow/billing.d.ts.map +1 -0
  124. package/dist/core/workflow/billing.js +108 -0
  125. package/dist/core/workflow/child-env.d.ts +30 -0
  126. package/dist/core/workflow/child-env.d.ts.map +1 -0
  127. package/dist/core/workflow/child-env.js +43 -0
  128. package/dist/core/workflow/engine.d.ts +21 -0
  129. package/dist/core/workflow/engine.d.ts.map +1 -0
  130. package/dist/core/workflow/engine.js +349 -0
  131. package/dist/core/workflow/harnesses.d.ts +17 -0
  132. package/dist/core/workflow/harnesses.d.ts.map +1 -0
  133. package/dist/core/workflow/harnesses.js +21 -0
  134. package/dist/core/workflow/spawn-claude.d.ts +25 -0
  135. package/dist/core/workflow/spawn-claude.d.ts.map +1 -0
  136. package/dist/core/workflow/spawn-claude.js +103 -0
  137. package/dist/core/workflow/spawn-codex.d.ts +22 -0
  138. package/dist/core/workflow/spawn-codex.d.ts.map +1 -0
  139. package/dist/core/workflow/spawn-codex.js +88 -0
  140. package/dist/core/workflow/spawn-cursor.d.ts +29 -0
  141. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -0
  142. package/dist/core/workflow/spawn-cursor.js +91 -0
  143. package/dist/core/workflow/types.d.ts +152 -0
  144. package/dist/core/workflow/types.d.ts.map +1 -0
  145. package/dist/core/workflow/types.js +9 -0
  146. package/dist/core/workflow/validate.d.ts +15 -0
  147. package/dist/core/workflow/validate.d.ts.map +1 -0
  148. package/dist/core/workflow/validate.js +70 -0
  149. package/dist/lib/browser/client.d.ts +16 -0
  150. package/dist/lib/browser/client.d.ts.map +1 -1
  151. package/dist/lib/browser/client.js +22 -0
  152. package/dist/lib/browser/index.d.ts +1 -0
  153. package/dist/lib/browser/index.d.ts.map +1 -1
  154. package/dist/lib/browser/index.js +1 -0
  155. package/dist/lib/browser/launch-args.d.ts +21 -0
  156. package/dist/lib/browser/launch-args.d.ts.map +1 -0
  157. package/dist/lib/browser/launch-args.js +32 -0
  158. package/dist/lib/claude-desktop.d.ts +105 -0
  159. package/dist/lib/claude-desktop.d.ts.map +1 -0
  160. package/dist/lib/claude-desktop.js +217 -0
  161. package/dist/lib/docs-lint.d.ts.map +1 -1
  162. package/dist/lib/docs-lint.js +5 -2
  163. package/dist/lib/identities/assume.d.ts +51 -0
  164. package/dist/lib/identities/assume.d.ts.map +1 -0
  165. package/dist/lib/identities/assume.js +274 -0
  166. package/dist/lib/identities/index.d.ts +7 -7
  167. package/dist/lib/identities/index.d.ts.map +1 -1
  168. package/dist/lib/identities/index.js +24 -24
  169. package/dist/lib/instructions/templates.d.ts.map +1 -1
  170. package/dist/lib/instructions/templates.js +15 -2
  171. package/package.json +12 -1
  172. package/schemas/config.schema.json +88 -19
  173. package/src/commander.ts +8 -0
  174. package/src/commands/agents.ts +280 -14
  175. package/src/commands/backup.ts +17 -16
  176. package/src/commands/browse.ts +32 -0
  177. package/src/commands/claude-desktop.ts +215 -0
  178. package/src/commands/context.ts +174 -1
  179. package/src/commands/doctor.ts +44 -0
  180. package/src/commands/eml.ts +22 -4
  181. package/src/commands/grep.ts +535 -142
  182. package/src/commands/harness.ts +147 -0
  183. package/src/commands/init.ts +138 -17
  184. package/src/commands/presence.ts +111 -5
  185. package/src/commands/relay.ts +160 -0
  186. package/src/commands/sync.ts +4 -0
  187. package/src/commands/workflow.ts +123 -0
  188. package/src/core/agents/canonical-emit.ts +41 -3
  189. package/src/core/agents/cli.ts +15 -6
  190. package/src/core/agents/render/prompt-context.ts +48 -3
  191. package/src/core/agents/render/session-context.ts +27 -1
  192. package/src/core/agents/rules/claim-conflict.ts +26 -1
  193. package/src/core/agents/rules/commit-conflict.ts +4 -33
  194. package/src/core/agents/rules/stop-hook.ts +17 -0
  195. package/src/core/agents/session-events.ts +15 -39
  196. package/src/core/agents/state/heartbeat-projector.ts +23 -2
  197. package/src/core/agents/state/heartbeat-writer.ts +33 -7
  198. package/src/core/agents/state/names.ts +68 -9
  199. package/src/core/agents/state/pidmap.ts +59 -2
  200. package/src/core/agents/state/stale-sweep.ts +6 -11
  201. package/src/core/config.ts +271 -24
  202. package/src/core/context/index.ts +575 -0
  203. package/src/core/harnesses/bench.ts +214 -0
  204. package/src/core/harnesses/index.ts +31 -0
  205. package/src/core/harnesses/profiles.ts +141 -0
  206. package/src/core/harnesses/registry.ts +140 -0
  207. package/src/core/harnesses/types.ts +90 -0
  208. package/src/core/hooks/cli.ts +361 -65
  209. package/src/core/hooks/events/schema.ts +72 -0
  210. package/src/core/hooks/harness/events.ts +11 -0
  211. package/src/core/hooks/harness/parse.ts +7 -1
  212. package/src/core/hooks/resolve/coord-root.ts +28 -6
  213. package/src/core/presence/blob.ts +125 -0
  214. package/src/core/presence/git.ts +191 -0
  215. package/src/core/presence/index.ts +274 -0
  216. package/src/core/presence/relay-client.ts +322 -0
  217. package/src/core/presence/relay-protocol.ts +246 -0
  218. package/src/core/workflow/billing.ts +152 -0
  219. package/src/core/workflow/child-env.ts +47 -0
  220. package/src/core/workflow/engine.ts +403 -0
  221. package/src/core/workflow/harnesses.ts +33 -0
  222. package/src/core/workflow/spawn-claude.ts +118 -0
  223. package/src/core/workflow/spawn-codex.ts +93 -0
  224. package/src/core/workflow/spawn-cursor.ts +108 -0
  225. package/src/core/workflow/types.ts +160 -0
  226. package/src/core/workflow/validate.ts +75 -0
  227. package/src/lib/browser/client.ts +37 -0
  228. package/src/lib/browser/index.ts +1 -0
  229. package/src/lib/browser/launch-args.ts +33 -0
  230. package/src/lib/claude-desktop.ts +293 -0
  231. package/src/lib/docs-lint.ts +5 -2
  232. package/src/lib/identities/assume.ts +363 -0
  233. package/src/lib/identities/index.ts +28 -24
  234. package/src/lib/instructions/templates.ts +16 -2
@@ -27,6 +27,17 @@ import { consumeSince, writeCursor } from "../agents/events/consume.ts";
27
27
  import { evaluateStopHook } from "../agents/rules/stop-hook.ts";
28
28
  import { projectHeartbeats } from "../agents/state/heartbeat-projector.ts";
29
29
  import { shellMutationPaths } from "../agents/state/shell-mutation.ts";
30
+ import {
31
+ checkpointContext,
32
+ completeContextRecovery,
33
+ extractContextSample,
34
+ markContextCompactionCompleted,
35
+ type PreparedContextRecovery,
36
+ prepareContextRecovery,
37
+ readContextState,
38
+ recordContextSample,
39
+ } from "../context/index.ts";
40
+ import { ensureRelayDaemon, fetchPresence, publishPresence } from "../presence/index.ts";
30
41
  import {
31
42
  captureImages,
32
43
  detectPresence,
@@ -94,6 +105,17 @@ async function readStdin(): Promise<string> {
94
105
  }
95
106
  }
96
107
 
108
+ /**
109
+ * Env for child agent-coord spawns, with the coord root pinned. The hook
110
+ * process may be running with the session shell's cwd (inside a submodule or
111
+ * scratch dir), and agent-coord resolves its root from cwd unless overridden —
112
+ * without the pin a child could write state to a different .harnery than the
113
+ * one this hook resolved.
114
+ */
115
+ function childEnv(coordRoot: string): NodeJS.ProcessEnv {
116
+ return { ...process.env, HARNERY_COORD_ROOT_OVERRIDE: coordRoot };
117
+ }
118
+
97
119
  function appendDebug(coordRoot: string, entry: Record<string, unknown>): void {
98
120
  const path = join(coordRoot, ".harnery", "debug", "agent-hook.ndjson");
99
121
  try {
@@ -143,6 +165,7 @@ function assignNameViaAgentCoord(
143
165
  const result = spawnSync(binary, ["assign-name", instanceId, kind], {
144
166
  encoding: "utf8",
145
167
  timeout: 2000,
168
+ env: childEnv(coordRoot),
146
169
  });
147
170
  if (result.status !== 0 || !result.stdout) return null;
148
171
  const parsed = JSON.parse(result.stdout.trim()) as {
@@ -254,6 +277,12 @@ function buildEventData(
254
277
  name: assigned?.name,
255
278
  kind: "session",
256
279
  agent_id: ctx.instanceId,
280
+ // Workflow-child linkage: `workflow run` children carry the run id in
281
+ // env (spawn adapters set it via buildChildEnv), so their sessions and
282
+ // heartbeats join back to the run journal + /workflows web view.
283
+ ...(coordEnv("WORKFLOW_CHILD") === "1" && coordEnv("WORKFLOW_RUN_ID")
284
+ ? { workflow_run_id: coordEnv("WORKFLOW_RUN_ID") }
285
+ : {}),
257
286
  };
258
287
  }
259
288
 
@@ -368,9 +397,49 @@ function buildEventData(
368
397
  ...(summary.truncated ? { truncated: true } : {}),
369
398
  };
370
399
  }
400
+
401
+ case "context.compaction.started": {
402
+ const metadata = objectRecord(p?.raw.compact_metadata);
403
+ return {
404
+ trigger: stringField(p?.raw.trigger) ?? stringField(metadata?.trigger),
405
+ pre_tokens:
406
+ numberField(p?.raw.pre_tokens) ??
407
+ numberField(metadata?.pre_tokens) ??
408
+ numberField(metadata?.pre_compact_tokens),
409
+ };
410
+ }
411
+
412
+ case "context.compaction.completed": {
413
+ const metadata = objectRecord(p?.raw.compact_metadata);
414
+ return {
415
+ trigger: stringField(p?.raw.trigger) ?? stringField(metadata?.trigger),
416
+ pre_tokens:
417
+ numberField(p?.raw.pre_tokens) ??
418
+ numberField(metadata?.pre_tokens) ??
419
+ numberField(metadata?.pre_compact_tokens),
420
+ post_tokens:
421
+ numberField(p?.raw.post_tokens) ??
422
+ numberField(metadata?.post_tokens) ??
423
+ numberField(metadata?.post_compact_tokens),
424
+ };
425
+ }
371
426
  }
372
427
  }
373
428
 
429
+ function objectRecord(value: unknown): Record<string, unknown> | undefined {
430
+ return typeof value === "object" && value !== null && !Array.isArray(value)
431
+ ? (value as Record<string, unknown>)
432
+ : undefined;
433
+ }
434
+
435
+ function stringField(value: unknown): string | undefined {
436
+ return typeof value === "string" && value.length > 0 ? value : undefined;
437
+ }
438
+
439
+ function numberField(value: unknown): number | undefined {
440
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
441
+ }
442
+
374
443
  async function main(): Promise<number> {
375
444
  const { eventName, extra } = parseArgv(process.argv.slice(2));
376
445
  const harness = detectHarness(process.argv.slice(2));
@@ -471,6 +540,84 @@ async function main(): Promise<number> {
471
540
  event_id: envelope.event_id,
472
541
  });
473
542
 
543
+ // Context telemetry is opportunistic and truthful: only persist a sample
544
+ // when the harness payload actually exposes usage/window data. Identical
545
+ // measurements are de-duplicated before they reach the canonical stream.
546
+ if (payload?.raw) {
547
+ try {
548
+ const sample = extractContextSample(payload.raw, {
549
+ sessionId,
550
+ harness,
551
+ model: payload.model,
552
+ source: "hook",
553
+ confidence: "reported",
554
+ });
555
+ if (sample) {
556
+ const recorded = recordContextSample(coordRoot, owner.instance_id, sample);
557
+ if (recorded.changed) {
558
+ emit(coordRoot, {
559
+ event_type: "context.sampled",
560
+ instance_id: owner.instance_id,
561
+ session_id: sessionId,
562
+ harness,
563
+ data: {
564
+ model: sample.model,
565
+ used_tokens: sample.used_tokens,
566
+ window_tokens: sample.window_tokens,
567
+ used_percent: sample.used_percent,
568
+ telemetry_source: sample.source,
569
+ confidence: sample.confidence,
570
+ },
571
+ });
572
+ }
573
+ }
574
+ } catch (err) {
575
+ logError(coordRoot, err, { phase: "context-sample" });
576
+ }
577
+ }
578
+
579
+ // A native pre-compaction signal is the safest available point to capture
580
+ // external work state. The operation is idempotent until recovery, so a
581
+ // harness retry cannot create a storm of near-identical capsules.
582
+ if (norm.event_type === "context.compaction.started") {
583
+ try {
584
+ const checkpoint = checkpointContext(coordRoot, {
585
+ sessionId,
586
+ instanceId: owner.instance_id,
587
+ harness,
588
+ cwd: payload?.cwd ?? process.cwd(),
589
+ reason: "pre_compact",
590
+ model: payload?.model,
591
+ });
592
+ emit(coordRoot, {
593
+ event_type: "context.checkpoint.created",
594
+ instance_id: owner.instance_id,
595
+ session_id: sessionId,
596
+ harness,
597
+ data: {
598
+ capsule_id: checkpoint.capsule.capsule_id,
599
+ generation: checkpoint.capsule.generation,
600
+ path: checkpoint.state.latest_capsule ?? "",
601
+ reason: checkpoint.capsule.reason,
602
+ reused: checkpoint.reused,
603
+ },
604
+ });
605
+ } catch (err) {
606
+ logError(coordRoot, err, { phase: "context-checkpoint" });
607
+ }
608
+ }
609
+
610
+ if (norm.event_type === "context.compaction.completed") {
611
+ try {
612
+ markContextCompactionCompleted(coordRoot, {
613
+ sessionId,
614
+ instanceId: owner.instance_id,
615
+ });
616
+ } catch (err) {
617
+ logError(coordRoot, err, { phase: "context-compaction-completed" });
618
+ }
619
+ }
620
+
474
621
  // Phase 8: SessionStart post-emit: project the event so the heartbeat
475
622
  // lands synchronously, run stale-sweep, and emit the harness-shaped
476
623
  // systemMessage JSON (peer table + wiring check + council invites).
@@ -489,11 +636,61 @@ async function main(): Promise<number> {
489
636
  } catch (err) {
490
637
  logError(coordRoot, err, { phase: "session-start-image-janitor" });
491
638
  }
639
+ let recovery: PreparedContextRecovery | null = null;
640
+ if (payload?.source === "compact") {
641
+ try {
642
+ emit(coordRoot, {
643
+ event_type: "context.compaction.completed",
644
+ instance_id: owner.instance_id,
645
+ session_id: sessionId,
646
+ harness,
647
+ data: { trigger: "auto" },
648
+ });
649
+ markContextCompactionCompleted(coordRoot, {
650
+ sessionId,
651
+ instanceId: owner.instance_id,
652
+ });
653
+ recovery = prepareContextRecovery(coordRoot, {
654
+ instanceId: owner.instance_id,
655
+ sessionId,
656
+ cwd: payload?.cwd ?? process.cwd(),
657
+ });
658
+ } catch (err) {
659
+ logError(coordRoot, err, { phase: "context-recovery-session-start" });
660
+ }
661
+ }
492
662
  try {
493
- await emitSessionStartSystemMessage(coordRoot, owner.instance_id, sessionId, data, harness);
663
+ const injected = await emitSessionStartSystemMessage(
664
+ coordRoot,
665
+ owner.instance_id,
666
+ sessionId,
667
+ data,
668
+ harness,
669
+ recovery?.briefing ?? "",
670
+ );
671
+ if (injected && recovery) {
672
+ completeRecoveryInjection(
673
+ coordRoot,
674
+ owner.instance_id,
675
+ sessionId,
676
+ harness,
677
+ recovery,
678
+ "SessionStart",
679
+ );
680
+ }
494
681
  } catch (err) {
495
682
  logError(coordRoot, err, { phase: "session-start-systemMessage" });
496
683
  }
684
+ // Cross-machine presence (ADR 0016): announce this machine's sessions and
685
+ // pull peers'. Both are throttled + fail-silent; network runs detached.
686
+ // When a relay is configured, also make sure the live-socket daemon runs.
687
+ try {
688
+ publishPresence(coordRoot);
689
+ fetchPresence(coordRoot);
690
+ ensureRelayDaemon(coordRoot);
691
+ } catch (err) {
692
+ logError(coordRoot, err, { phase: "session-start-presence" });
693
+ }
497
694
  }
498
695
 
499
696
  // Phase 8: SessionEnd cleanup: delete heartbeat + pid-map rows. Harness-
@@ -510,6 +707,13 @@ async function main(): Promise<number> {
510
707
  scratchArchive(coordRoot, owner.instance_id);
511
708
  runSessionSyncExtension(coordRoot, true);
512
709
  }
710
+ // Presence: publish the post-cleanup state so peers see this session gone
711
+ // promptly rather than waiting out the remote-stale window.
712
+ try {
713
+ publishPresence(coordRoot);
714
+ } catch (err) {
715
+ logError(coordRoot, err, { phase: "session-end-presence" });
716
+ }
513
717
  }
514
718
 
515
719
  // Phase 8: SubagentStart: sync-project to create the subagent heartbeat,
@@ -519,7 +723,11 @@ async function main(): Promise<number> {
519
723
  try {
520
724
  const agentCoordBin = join(coordRoot, "harnery", "bin", "agent-coord");
521
725
  if (existsSync(agentCoordBin)) {
522
- spawnSync(agentCoordBin, ["project"], { encoding: "utf8", timeout: 3000 });
726
+ spawnSync(agentCoordBin, ["project"], {
727
+ encoding: "utf8",
728
+ timeout: 3000,
729
+ env: childEnv(coordRoot),
730
+ });
523
731
  spawnSync(
524
732
  agentCoordBin,
525
733
  [
@@ -528,7 +736,7 @@ async function main(): Promise<number> {
528
736
  "--instance",
529
737
  owner.instance_id,
530
738
  ],
531
- { encoding: "utf8", timeout: 2000 },
739
+ { encoding: "utf8", timeout: 2000, env: childEnv(coordRoot) },
532
740
  );
533
741
  }
534
742
  emitSubagentStartContext(coordRoot, owner.instance_id, sessionId, data, harness);
@@ -551,7 +759,7 @@ async function main(): Promise<number> {
551
759
  "--instance",
552
760
  owner.instance_id,
553
761
  ],
554
- { encoding: "utf8", timeout: 2000 },
762
+ { encoding: "utf8", timeout: 2000, env: childEnv(coordRoot) },
555
763
  );
556
764
  }
557
765
  } catch (err) {
@@ -569,8 +777,47 @@ async function main(): Promise<number> {
569
777
  const prompt = (payload?.raw?.prompt as string | undefined) ?? "";
570
778
  if (prompt) detectPresence(prompt);
571
779
  }
780
+ // Presence: throttled pull of peer machines' presence refs (default 60s
781
+ // interval; the systemMessage below reads whatever is locally known).
572
782
  try {
573
- await emitUserPromptSubmitSystemMessage(coordRoot, owner.instance_id, sessionId, harness);
783
+ fetchPresence(coordRoot);
784
+ } catch (err) {
785
+ logError(coordRoot, err, { phase: "user-prompt-submit-presence" });
786
+ }
787
+ let recovery: PreparedContextRecovery | null = null;
788
+ const continuityState = readContextState(coordRoot, sessionId);
789
+ if (
790
+ continuityState?.phase === "checkpointed" &&
791
+ continuityState.compaction_completed_at !== undefined
792
+ ) {
793
+ try {
794
+ recovery = prepareContextRecovery(coordRoot, {
795
+ instanceId: owner.instance_id,
796
+ sessionId,
797
+ cwd: payload?.cwd ?? process.cwd(),
798
+ });
799
+ } catch (err) {
800
+ logError(coordRoot, err, { phase: "context-recovery-user-prompt" });
801
+ }
802
+ }
803
+ try {
804
+ const injected = await emitUserPromptSubmitSystemMessage(
805
+ coordRoot,
806
+ owner.instance_id,
807
+ sessionId,
808
+ harness,
809
+ recovery?.briefing ?? "",
810
+ );
811
+ if (injected && recovery) {
812
+ completeRecoveryInjection(
813
+ coordRoot,
814
+ owner.instance_id,
815
+ sessionId,
816
+ harness,
817
+ recovery,
818
+ "UserPromptSubmit",
819
+ );
820
+ }
574
821
  } catch (err) {
575
822
  logError(coordRoot, err, { phase: "user-prompt-submit-systemMessage" });
576
823
  }
@@ -623,6 +870,17 @@ async function main(): Promise<number> {
623
870
  logError(coordRoot, err, { phase: "stop-projection" });
624
871
  }
625
872
 
873
+ // Presence: publish AFTER the projection above so the blob carries this
874
+ // turn's fresh task/turn_summary/files. Change-batched + keepalive'd;
875
+ // the push itself runs detached. The relay daemon (when configured) is
876
+ // re-ensured here too — it self-exits on idle, and the next turn revives it.
877
+ try {
878
+ publishPresence(coordRoot);
879
+ ensureRelayDaemon(coordRoot);
880
+ } catch (err) {
881
+ logError(coordRoot, err, { phase: "stop-presence" });
882
+ }
883
+
626
884
  // Stop verdict (status-box + set-task gate). Direct in-process call: the
627
885
  // rule lives in harnery. agent-hook already emitted this turn.stop (with
628
886
  // status_box_present) above, so the evidence is in the stream.
@@ -632,6 +890,7 @@ async function main(): Promise<number> {
632
890
  session_id: sessionId,
633
891
  harness,
634
892
  bypass: coordEnv("AGENT_COORD_BYPASS_STOP") === "1",
893
+ workflow_child: coordEnv("WORKFLOW_CHILD") === "1",
635
894
  });
636
895
  if (!verdict.allow) {
637
896
  // Harness-aware enforcement channel: Claude Code / Codex honor exit-2 +
@@ -796,6 +1055,7 @@ async function runPreToolUseGuard(
796
1055
  input: verdictReq,
797
1056
  encoding: "utf8",
798
1057
  timeout: 3000,
1058
+ env: childEnv(coordRoot),
799
1059
  });
800
1060
  if (result.status !== 0 || !result.stdout) continue;
801
1061
  let parsed: { allow?: boolean; reason?: string } = {};
@@ -893,6 +1153,7 @@ function healHeartbeatViaCli(
893
1153
  spawnSync(agentCoordBin, ["heal-heartbeat", instanceId, sessionId, `--harness=${harness}`], {
894
1154
  encoding: "utf8",
895
1155
  timeout: 2000,
1156
+ env: childEnv(coordRoot),
896
1157
  });
897
1158
  }
898
1159
 
@@ -1000,6 +1261,7 @@ function stampToolActivity(
1000
1261
  spawnSync(agentCoordBin, ["stamp-tool-activity", instanceId, toolName, target], {
1001
1262
  encoding: "utf8",
1002
1263
  timeout: 2000,
1264
+ env: childEnv(coordRoot),
1003
1265
  });
1004
1266
  }
1005
1267
 
@@ -1051,6 +1313,7 @@ function releaseClaimOnFailure(
1051
1313
  spawnSync(agentCoordBin, ["release-claim", instanceId, canonical], {
1052
1314
  encoding: "utf8",
1053
1315
  timeout: 2000,
1316
+ env: childEnv(coordRoot),
1054
1317
  });
1055
1318
  }
1056
1319
 
@@ -1101,36 +1364,42 @@ async function emitUserPromptSubmitSystemMessage(
1101
1364
  instanceId: string,
1102
1365
  sessionId: string,
1103
1366
  harness: Harness,
1104
- ): Promise<void> {
1367
+ recoveryBriefing = "",
1368
+ ): Promise<boolean> {
1105
1369
  const agentCoordBin = join(coordRoot, "harnery", "bin", "agent-coord");
1106
- if (!existsSync(agentCoordBin)) return;
1370
+ let additionalContext = "";
1107
1371
 
1108
- // Look up the agent's name from its heartbeat (for council pending rendering).
1109
- let agentName = "";
1110
- try {
1111
- const fs = require("node:fs") as typeof import("node:fs");
1112
- const hbPath = join(coordRoot, ".harnery", "active", `${instanceId}.json`);
1113
- if (fs.existsSync(hbPath)) {
1114
- const hb = JSON.parse(fs.readFileSync(hbPath, "utf8")) as { name?: string };
1115
- agentName = hb.name ?? "";
1372
+ if (existsSync(agentCoordBin)) {
1373
+ // Look up the agent's name from its heartbeat (for council pending rendering).
1374
+ let agentName = "";
1375
+ try {
1376
+ const fs = require("node:fs") as typeof import("node:fs");
1377
+ const hbPath = join(coordRoot, ".harnery", "active", `${instanceId}.json`);
1378
+ if (fs.existsSync(hbPath)) {
1379
+ const hb = JSON.parse(fs.readFileSync(hbPath, "utf8")) as { name?: string };
1380
+ agentName = hb.name ?? "";
1381
+ }
1382
+ } catch {
1383
+ /* fall through with empty name; peer table still renders */
1116
1384
  }
1117
- } catch {
1118
- /* fall through with empty name; peer table still renders */
1119
- }
1120
1385
 
1121
- const args = ["prompt-context", "--instance", instanceId, "--session", sessionId];
1122
- if (agentName) args.push("--name", agentName);
1123
- // Cursor + Codex sessions get the set-task staleness nudge. CC enforces it
1124
- // via the Stop-hook transcript scan; the nudge replaces that for harnesses
1125
- // that don't reliably expose a transcript_path during stop.
1126
- if (harness === "cursor" || harness === "codex") args.push("--task-nudge");
1127
- const result = spawnSync(agentCoordBin, args, { encoding: "utf8", timeout: 3000 });
1128
- if (result.status !== 0 || !result.stdout) return;
1129
- const additionalContext = result.stdout.trim();
1130
- if (!additionalContext) return;
1386
+ const args = ["prompt-context", "--instance", instanceId, "--session", sessionId];
1387
+ if (agentName) args.push("--name", agentName);
1388
+ // Cursor + Codex sessions get the set-task staleness nudge. CC enforces it
1389
+ // via the Stop-hook transcript scan; the nudge replaces that for harnesses
1390
+ // that don't reliably expose a transcript_path during stop.
1391
+ if (harness === "cursor" || harness === "codex") args.push("--task-nudge");
1392
+ const result = spawnSync(agentCoordBin, args, { encoding: "utf8", timeout: 3000 });
1393
+ if (result.status === 0 && result.stdout) additionalContext = result.stdout.trim();
1394
+ }
1395
+ if (recoveryBriefing) {
1396
+ additionalContext = [additionalContext, recoveryBriefing].filter(Boolean).join("\n\n");
1397
+ }
1398
+ if (!additionalContext) return false;
1131
1399
 
1132
1400
  const { emitContext } = await import("./harness/output.ts");
1133
1401
  emitContext(harness, "UserPromptSubmit", additionalContext);
1402
+ return true;
1134
1403
  }
1135
1404
 
1136
1405
  function emitSubagentStartContext(
@@ -1183,45 +1452,45 @@ async function emitSessionStartSystemMessage(
1183
1452
  sessionId: string,
1184
1453
  emittedData: Record<string, unknown>,
1185
1454
  harness: Harness,
1186
- ): Promise<void> {
1455
+ recoveryBriefing = "",
1456
+ ): Promise<boolean> {
1187
1457
  const agentCoordBin = join(coordRoot, "harnery", "bin", "agent-coord");
1188
- if (!existsSync(agentCoordBin)) return;
1458
+ let additionalContext = "";
1459
+ if (existsSync(agentCoordBin)) {
1460
+ // Sync-project so the heartbeat exists for downstream readers (peer table,
1461
+ // wiring check, council invites).
1462
+ spawnSync(agentCoordBin, ["project"], { encoding: "utf8", timeout: 3000 });
1463
+ // Stale-sweep dead peers before rendering peer table.
1464
+ spawnSync(agentCoordBin, ["stale-sweep"], { encoding: "utf8", timeout: 3000 });
1465
+
1466
+ // SESSION_START activity log line, fired across all harnesses.
1467
+ const model = (emittedData.model as string | undefined) ?? "unknown";
1468
+ const source = (emittedData.source as string | undefined) ?? "startup";
1469
+ const platform = harnessPlatform(harness);
1470
+ spawnSync(
1471
+ agentCoordBin,
1472
+ [
1473
+ "log",
1474
+ `SESSION_START model=${model} source=${source} platform=${platform}`,
1475
+ "--instance",
1476
+ instanceId,
1477
+ ],
1478
+ { encoding: "utf8", timeout: 2000 },
1479
+ );
1189
1480
 
1190
- // Sync-project so the heartbeat exists for downstream readers (peer table,
1191
- // wiring check, council invites).
1192
- spawnSync(agentCoordBin, ["project"], { encoding: "utf8", timeout: 3000 });
1193
- // Stale-sweep dead peers before rendering peer table.
1194
- spawnSync(agentCoordBin, ["stale-sweep"], { encoding: "utf8", timeout: 3000 });
1195
-
1196
- // SESSION_START activity log line, fired across all harnesses.
1197
- const model = (emittedData.model as string | undefined) ?? "unknown";
1198
- const source = (emittedData.source as string | undefined) ?? "startup";
1199
- const platform = harnessPlatform(harness);
1200
- spawnSync(
1201
- agentCoordBin,
1202
- [
1203
- "log",
1204
- `SESSION_START model=${model} source=${source} platform=${platform}`,
1205
- "--instance",
1206
- instanceId,
1207
- ],
1208
- { encoding: "utf8", timeout: 2000 },
1209
- );
1210
-
1211
- // Render the systemMessage via agent-coord.
1212
- const agentName = (emittedData.name as string | undefined) ?? "";
1213
- const args = ["session-context", "--instance", instanceId, "--session", sessionId];
1214
- if (agentName) args.push("--name", agentName);
1215
- // The "You are agent-X." prefix in session-context renders unqualified by
1216
- // default (claude-code-style). For cursor/codex the bash dispatchers add
1217
- // a "(Cursor)" / "(Codex)" suffix; pass it through as --platform-label.
1218
- if (harness !== "claude-code") {
1219
- args.push("--platform-label", platform === "cursor" ? "Cursor" : "Codex");
1481
+ // Render the systemMessage via agent-coord.
1482
+ const agentName = (emittedData.name as string | undefined) ?? "";
1483
+ const args = ["session-context", "--instance", instanceId, "--session", sessionId];
1484
+ if (agentName) args.push("--name", agentName);
1485
+ // The "You are agent-X." prefix in session-context renders unqualified by
1486
+ // default (claude-code-style). For cursor/codex the bash dispatchers add
1487
+ // a "(Cursor)" / "(Codex)" suffix; pass it through as --platform-label.
1488
+ if (harness !== "claude-code") {
1489
+ args.push("--platform-label", platform === "cursor" ? "Cursor" : "Codex");
1490
+ }
1491
+ const result = spawnSync(agentCoordBin, args, { encoding: "utf8", timeout: 3000 });
1492
+ if (result.status === 0 && result.stdout) additionalContext = result.stdout.trim();
1220
1493
  }
1221
- const result = spawnSync(agentCoordBin, args, { encoding: "utf8", timeout: 3000 });
1222
- if (result.status !== 0 || !result.stdout) return;
1223
- let additionalContext = result.stdout.trim();
1224
- if (!additionalContext) return;
1225
1494
 
1226
1495
  // Effect (claude-code): merge the scratch recovery cue into the session-start
1227
1496
  // context. Was a standalone additionalContext emission from the previous
@@ -1229,11 +1498,38 @@ async function emitSessionStartSystemMessage(
1229
1498
  // entry, it folds in here.
1230
1499
  if (harness === "claude-code") {
1231
1500
  const cue = scratchRecoveryCue(coordRoot);
1232
- if (cue) additionalContext = `${additionalContext}\n\n${cue}`;
1501
+ if (cue) additionalContext = [additionalContext, cue].filter(Boolean).join("\n\n");
1502
+ }
1503
+ if (recoveryBriefing) {
1504
+ additionalContext = [additionalContext, recoveryBriefing].filter(Boolean).join("\n\n");
1233
1505
  }
1506
+ if (!additionalContext) return false;
1234
1507
 
1235
1508
  const { emitContext } = await import("./harness/output.ts");
1236
1509
  emitContext(harness, "SessionStart", additionalContext);
1510
+ return true;
1511
+ }
1512
+
1513
+ function completeRecoveryInjection(
1514
+ coordRoot: string,
1515
+ instanceId: string,
1516
+ sessionId: string,
1517
+ harness: Harness,
1518
+ recovery: PreparedContextRecovery,
1519
+ injectionEvent: "SessionStart" | "UserPromptSubmit",
1520
+ ): void {
1521
+ completeContextRecovery(coordRoot, { sessionId, instanceId });
1522
+ emit(coordRoot, {
1523
+ event_type: "context.recovery.injected",
1524
+ instance_id: instanceId,
1525
+ session_id: sessionId,
1526
+ harness,
1527
+ data: {
1528
+ capsule_id: recovery.capsule.capsule_id,
1529
+ generation: recovery.capsule.generation,
1530
+ injection_event: injectionEvent,
1531
+ },
1532
+ });
1237
1533
  }
1238
1534
 
1239
1535
  main()
@@ -47,6 +47,10 @@ export type SessionStart = EventEnvelope<
47
47
  cwd: string;
48
48
  model?: string;
49
49
  pid: number;
50
+ /** Present iff this session is a `workflow run` child: the run id whose
51
+ * journal owns it (child env HARNERY_WORKFLOW_RUN_ID). Optional-field
52
+ * addition per the schema evolution rules (minor bump). */
53
+ workflow_run_id?: string;
50
54
  }
51
55
  >;
52
56
 
@@ -190,6 +194,56 @@ export type ImageCaptured = EventEnvelope<
190
194
  }
191
195
  >;
192
196
 
197
+ // Context continuity
198
+ export type ContextSampled = EventEnvelope<
199
+ "context.sampled",
200
+ {
201
+ model?: string;
202
+ used_tokens?: number;
203
+ window_tokens?: number;
204
+ used_percent?: number;
205
+ telemetry_source: "hook" | "native_event" | "result" | "transcript" | "estimate";
206
+ confidence: "exact" | "reported" | "estimated";
207
+ }
208
+ >;
209
+
210
+ export type ContextCheckpointCreated = EventEnvelope<
211
+ "context.checkpoint.created",
212
+ {
213
+ capsule_id: string;
214
+ generation: number;
215
+ path: string;
216
+ reason: "manual" | "pressure" | "pre_compact" | "session_end";
217
+ reused: boolean;
218
+ }
219
+ >;
220
+
221
+ export type ContextCompactionStarted = EventEnvelope<
222
+ "context.compaction.started",
223
+ {
224
+ trigger?: string;
225
+ pre_tokens?: number;
226
+ }
227
+ >;
228
+
229
+ export type ContextCompactionCompleted = EventEnvelope<
230
+ "context.compaction.completed",
231
+ {
232
+ trigger?: string;
233
+ pre_tokens?: number;
234
+ post_tokens?: number;
235
+ }
236
+ >;
237
+
238
+ export type ContextRecoveryInjected = EventEnvelope<
239
+ "context.recovery.injected",
240
+ {
241
+ capsule_id: string;
242
+ generation: number;
243
+ injection_event: "SessionStart" | "UserPromptSubmit";
244
+ }
245
+ >;
246
+
193
247
  // File claims
194
248
  export type ClaimAcquire = EventEnvelope<
195
249
  "claim.acquire",
@@ -253,6 +307,18 @@ export type StatePresenceChange = EventEnvelope<
253
307
 
254
308
  export type StateHeartbeat = EventEnvelope<"state.heartbeat", Record<string, never>>;
255
309
 
310
+ /** A live session adopted a durable persona/role. The event is authoritative
311
+ * for replay; `.name-history` carries the same binding for heartbeat healing. */
312
+ export type IdentityAssumed = EventEnvelope<
313
+ "identity.assumed",
314
+ {
315
+ name: string;
316
+ agent_id: string;
317
+ previous_name?: string;
318
+ previous_agent_id?: string;
319
+ }
320
+ >;
321
+
256
322
  // Council
257
323
  export type CouncilOpen = EventEnvelope<
258
324
  "council.open",
@@ -394,6 +460,11 @@ export type Event =
394
460
  | ToolPostUseFailure
395
461
  | ToolOutputChunk
396
462
  | ImageCaptured
463
+ | ContextSampled
464
+ | ContextCheckpointCreated
465
+ | ContextCompactionStarted
466
+ | ContextCompactionCompleted
467
+ | ContextRecoveryInjected
397
468
  | ClaimAcquire
398
469
  | ClaimRelease
399
470
  | ClaimConflict
@@ -402,6 +473,7 @@ export type Event =
402
473
  | StateScratchAppend
403
474
  | StatePresenceChange
404
475
  | StateHeartbeat
476
+ | IdentityAssumed
405
477
  | CouncilOpen
406
478
  | CouncilRoundOpen
407
479
  | CouncilContribution