harnery 0.9.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 (201) hide show
  1. package/README.md +1 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commander.js +6 -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 +2 -3
  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/harness.d.ts +11 -0
  23. package/dist/commands/harness.d.ts.map +1 -0
  24. package/dist/commands/harness.js +118 -0
  25. package/dist/commands/presence.d.ts +9 -4
  26. package/dist/commands/presence.d.ts.map +1 -1
  27. package/dist/commands/presence.js +88 -5
  28. package/dist/commands/relay.d.ts +9 -0
  29. package/dist/commands/relay.d.ts.map +1 -0
  30. package/dist/commands/relay.js +143 -0
  31. package/dist/commands/sync.d.ts.map +1 -1
  32. package/dist/commands/sync.js +5 -0
  33. package/dist/commands/workflow.d.ts.map +1 -1
  34. package/dist/commands/workflow.js +8 -15
  35. package/dist/core/agents/canonical-emit.d.ts +15 -0
  36. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  37. package/dist/core/agents/canonical-emit.js +41 -3
  38. package/dist/core/agents/cli.js +4 -2
  39. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  40. package/dist/core/agents/render/prompt-context.js +44 -3
  41. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  42. package/dist/core/agents/render/session-context.js +26 -1
  43. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
  44. package/dist/core/agents/rules/commit-conflict.js +4 -37
  45. package/dist/core/agents/session-events.d.ts.map +1 -1
  46. package/dist/core/agents/session-events.js +16 -43
  47. package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
  48. package/dist/core/agents/state/heartbeat-projector.js +11 -2
  49. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  50. package/dist/core/agents/state/heartbeat-writer.js +3 -3
  51. package/dist/core/agents/state/names.d.ts +38 -3
  52. package/dist/core/agents/state/names.d.ts.map +1 -1
  53. package/dist/core/agents/state/names.js +46 -7
  54. package/dist/core/agents/state/pidmap.d.ts +5 -1
  55. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  56. package/dist/core/agents/state/pidmap.js +58 -2
  57. package/dist/core/agents/state/stale-sweep.d.ts +3 -2
  58. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  59. package/dist/core/agents/state/stale-sweep.js +6 -6
  60. package/dist/core/config.d.ts +68 -11
  61. package/dist/core/config.d.ts.map +1 -1
  62. package/dist/core/config.js +191 -25
  63. package/dist/core/context/index.d.ts +144 -0
  64. package/dist/core/context/index.d.ts.map +1 -0
  65. package/dist/core/context/index.js +380 -0
  66. package/dist/core/harnesses/bench.d.ts +29 -0
  67. package/dist/core/harnesses/bench.d.ts.map +1 -0
  68. package/dist/core/harnesses/bench.js +151 -0
  69. package/dist/core/harnesses/index.d.ts +9 -0
  70. package/dist/core/harnesses/index.d.ts.map +1 -0
  71. package/dist/core/harnesses/index.js +4 -0
  72. package/dist/core/harnesses/profiles.d.ts +62 -0
  73. package/dist/core/harnesses/profiles.d.ts.map +1 -0
  74. package/dist/core/harnesses/profiles.js +115 -0
  75. package/dist/core/harnesses/registry.d.ts +15 -0
  76. package/dist/core/harnesses/registry.d.ts.map +1 -0
  77. package/dist/core/harnesses/registry.js +113 -0
  78. package/dist/core/harnesses/types.d.ts +64 -0
  79. package/dist/core/harnesses/types.d.ts.map +1 -0
  80. package/dist/core/harnesses/types.js +20 -0
  81. package/dist/core/hooks/cli.js +280 -63
  82. package/dist/core/hooks/events/schema.d.ts +38 -1
  83. package/dist/core/hooks/events/schema.d.ts.map +1 -1
  84. package/dist/core/hooks/harness/events.d.ts.map +1 -1
  85. package/dist/core/hooks/harness/events.js +3 -0
  86. package/dist/core/hooks/harness/parse.d.ts +1 -1
  87. package/dist/core/hooks/harness/parse.d.ts.map +1 -1
  88. package/dist/core/hooks/harness/parse.js +4 -0
  89. package/dist/core/presence/blob.d.ts +40 -0
  90. package/dist/core/presence/blob.d.ts.map +1 -0
  91. package/dist/core/presence/blob.js +91 -0
  92. package/dist/core/presence/git.d.ts +68 -0
  93. package/dist/core/presence/git.d.ts.map +1 -0
  94. package/dist/core/presence/git.js +173 -0
  95. package/dist/core/presence/index.d.ts +74 -0
  96. package/dist/core/presence/index.d.ts.map +1 -0
  97. package/dist/core/presence/index.js +224 -0
  98. package/dist/core/presence/relay-client.d.ts +39 -0
  99. package/dist/core/presence/relay-client.d.ts.map +1 -0
  100. package/dist/core/presence/relay-client.js +295 -0
  101. package/dist/core/presence/relay-protocol.d.ts +94 -0
  102. package/dist/core/presence/relay-protocol.d.ts.map +1 -0
  103. package/dist/core/presence/relay-protocol.js +169 -0
  104. package/dist/core/workflow/billing.d.ts +1 -1
  105. package/dist/core/workflow/billing.d.ts.map +1 -1
  106. package/dist/core/workflow/billing.js +7 -1
  107. package/dist/core/workflow/engine.d.ts.map +1 -1
  108. package/dist/core/workflow/engine.js +11 -2
  109. package/dist/core/workflow/harnesses.d.ts +3 -3
  110. package/dist/core/workflow/harnesses.d.ts.map +1 -1
  111. package/dist/core/workflow/harnesses.js +8 -17
  112. package/dist/core/workflow/spawn-claude.d.ts +4 -1
  113. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  114. package/dist/core/workflow/spawn-claude.js +39 -18
  115. package/dist/core/workflow/spawn-codex.d.ts +4 -1
  116. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  117. package/dist/core/workflow/spawn-codex.js +44 -22
  118. package/dist/core/workflow/spawn-cursor.d.ts +4 -1
  119. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  120. package/dist/core/workflow/spawn-cursor.js +35 -16
  121. package/dist/core/workflow/types.d.ts +9 -2
  122. package/dist/core/workflow/types.d.ts.map +1 -1
  123. package/dist/lib/browser/client.d.ts +16 -0
  124. package/dist/lib/browser/client.d.ts.map +1 -1
  125. package/dist/lib/browser/client.js +22 -0
  126. package/dist/lib/browser/index.d.ts +1 -0
  127. package/dist/lib/browser/index.d.ts.map +1 -1
  128. package/dist/lib/browser/index.js +1 -0
  129. package/dist/lib/browser/launch-args.d.ts +21 -0
  130. package/dist/lib/browser/launch-args.d.ts.map +1 -0
  131. package/dist/lib/browser/launch-args.js +32 -0
  132. package/dist/lib/claude-desktop.d.ts +105 -0
  133. package/dist/lib/claude-desktop.d.ts.map +1 -0
  134. package/dist/lib/claude-desktop.js +217 -0
  135. package/dist/lib/docs-lint.d.ts.map +1 -1
  136. package/dist/lib/docs-lint.js +5 -2
  137. package/dist/lib/identities/assume.d.ts +51 -0
  138. package/dist/lib/identities/assume.d.ts.map +1 -0
  139. package/dist/lib/identities/assume.js +274 -0
  140. package/dist/lib/identities/index.d.ts +7 -7
  141. package/dist/lib/identities/index.d.ts.map +1 -1
  142. package/dist/lib/identities/index.js +24 -24
  143. package/dist/lib/instructions/templates.d.ts.map +1 -1
  144. package/dist/lib/instructions/templates.js +15 -2
  145. package/package.json +12 -1
  146. package/schemas/config.schema.json +88 -19
  147. package/src/commander.ts +6 -0
  148. package/src/commands/agents.ts +280 -14
  149. package/src/commands/backup.ts +17 -16
  150. package/src/commands/browse.ts +32 -0
  151. package/src/commands/claude-desktop.ts +215 -0
  152. package/src/commands/context.ts +174 -1
  153. package/src/commands/doctor.ts +2 -3
  154. package/src/commands/eml.ts +22 -4
  155. package/src/commands/harness.ts +147 -0
  156. package/src/commands/presence.ts +111 -5
  157. package/src/commands/relay.ts +160 -0
  158. package/src/commands/sync.ts +4 -0
  159. package/src/commands/workflow.ts +9 -18
  160. package/src/core/agents/canonical-emit.ts +41 -3
  161. package/src/core/agents/cli.ts +4 -2
  162. package/src/core/agents/render/prompt-context.ts +48 -3
  163. package/src/core/agents/render/session-context.ts +27 -1
  164. package/src/core/agents/rules/commit-conflict.ts +4 -33
  165. package/src/core/agents/session-events.ts +15 -39
  166. package/src/core/agents/state/heartbeat-projector.ts +10 -1
  167. package/src/core/agents/state/heartbeat-writer.ts +3 -2
  168. package/src/core/agents/state/names.ts +68 -9
  169. package/src/core/agents/state/pidmap.ts +59 -2
  170. package/src/core/agents/state/stale-sweep.ts +6 -11
  171. package/src/core/config.ts +248 -24
  172. package/src/core/context/index.ts +575 -0
  173. package/src/core/harnesses/bench.ts +214 -0
  174. package/src/core/harnesses/index.ts +31 -0
  175. package/src/core/harnesses/profiles.ts +141 -0
  176. package/src/core/harnesses/registry.ts +140 -0
  177. package/src/core/harnesses/types.ts +90 -0
  178. package/src/core/hooks/cli.ts +331 -62
  179. package/src/core/hooks/events/schema.ts +68 -0
  180. package/src/core/hooks/harness/events.ts +3 -0
  181. package/src/core/hooks/harness/parse.ts +7 -1
  182. package/src/core/presence/blob.ts +125 -0
  183. package/src/core/presence/git.ts +191 -0
  184. package/src/core/presence/index.ts +274 -0
  185. package/src/core/presence/relay-client.ts +322 -0
  186. package/src/core/presence/relay-protocol.ts +246 -0
  187. package/src/core/workflow/billing.ts +8 -2
  188. package/src/core/workflow/engine.ts +11 -2
  189. package/src/core/workflow/harnesses.ts +14 -19
  190. package/src/core/workflow/spawn-claude.ts +39 -20
  191. package/src/core/workflow/spawn-codex.ts +47 -28
  192. package/src/core/workflow/spawn-cursor.ts +37 -18
  193. package/src/core/workflow/types.ts +9 -2
  194. package/src/lib/browser/client.ts +37 -0
  195. package/src/lib/browser/index.ts +1 -0
  196. package/src/lib/browser/launch-args.ts +33 -0
  197. package/src/lib/claude-desktop.ts +293 -0
  198. package/src/lib/docs-lint.ts +5 -2
  199. package/src/lib/identities/assume.ts +363 -0
  200. package/src/lib/identities/index.ts +28 -24
  201. 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,
@@ -386,9 +397,49 @@ function buildEventData(
386
397
  ...(summary.truncated ? { truncated: true } : {}),
387
398
  };
388
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
+ }
389
426
  }
390
427
  }
391
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
+
392
443
  async function main(): Promise<number> {
393
444
  const { eventName, extra } = parseArgv(process.argv.slice(2));
394
445
  const harness = detectHarness(process.argv.slice(2));
@@ -489,6 +540,84 @@ async function main(): Promise<number> {
489
540
  event_id: envelope.event_id,
490
541
  });
491
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
+
492
621
  // Phase 8: SessionStart post-emit: project the event so the heartbeat
493
622
  // lands synchronously, run stale-sweep, and emit the harness-shaped
494
623
  // systemMessage JSON (peer table + wiring check + council invites).
@@ -507,11 +636,61 @@ async function main(): Promise<number> {
507
636
  } catch (err) {
508
637
  logError(coordRoot, err, { phase: "session-start-image-janitor" });
509
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
+ }
510
662
  try {
511
- 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
+ }
512
681
  } catch (err) {
513
682
  logError(coordRoot, err, { phase: "session-start-systemMessage" });
514
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
+ }
515
694
  }
516
695
 
517
696
  // Phase 8: SessionEnd cleanup: delete heartbeat + pid-map rows. Harness-
@@ -528,6 +707,13 @@ async function main(): Promise<number> {
528
707
  scratchArchive(coordRoot, owner.instance_id);
529
708
  runSessionSyncExtension(coordRoot, true);
530
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
+ }
531
717
  }
532
718
 
533
719
  // Phase 8: SubagentStart: sync-project to create the subagent heartbeat,
@@ -591,8 +777,47 @@ async function main(): Promise<number> {
591
777
  const prompt = (payload?.raw?.prompt as string | undefined) ?? "";
592
778
  if (prompt) detectPresence(prompt);
593
779
  }
780
+ // Presence: throttled pull of peer machines' presence refs (default 60s
781
+ // interval; the systemMessage below reads whatever is locally known).
594
782
  try {
595
- 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
+ }
596
821
  } catch (err) {
597
822
  logError(coordRoot, err, { phase: "user-prompt-submit-systemMessage" });
598
823
  }
@@ -645,6 +870,17 @@ async function main(): Promise<number> {
645
870
  logError(coordRoot, err, { phase: "stop-projection" });
646
871
  }
647
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
+
648
884
  // Stop verdict (status-box + set-task gate). Direct in-process call: the
649
885
  // rule lives in harnery. agent-hook already emitted this turn.stop (with
650
886
  // status_box_present) above, so the evidence is in the stream.
@@ -1128,36 +1364,42 @@ async function emitUserPromptSubmitSystemMessage(
1128
1364
  instanceId: string,
1129
1365
  sessionId: string,
1130
1366
  harness: Harness,
1131
- ): Promise<void> {
1367
+ recoveryBriefing = "",
1368
+ ): Promise<boolean> {
1132
1369
  const agentCoordBin = join(coordRoot, "harnery", "bin", "agent-coord");
1133
- if (!existsSync(agentCoordBin)) return;
1370
+ let additionalContext = "";
1134
1371
 
1135
- // Look up the agent's name from its heartbeat (for council pending rendering).
1136
- let agentName = "";
1137
- try {
1138
- const fs = require("node:fs") as typeof import("node:fs");
1139
- const hbPath = join(coordRoot, ".harnery", "active", `${instanceId}.json`);
1140
- if (fs.existsSync(hbPath)) {
1141
- const hb = JSON.parse(fs.readFileSync(hbPath, "utf8")) as { name?: string };
1142
- 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 */
1143
1384
  }
1144
- } catch {
1145
- /* fall through with empty name; peer table still renders */
1146
- }
1147
1385
 
1148
- const args = ["prompt-context", "--instance", instanceId, "--session", sessionId];
1149
- if (agentName) args.push("--name", agentName);
1150
- // Cursor + Codex sessions get the set-task staleness nudge. CC enforces it
1151
- // via the Stop-hook transcript scan; the nudge replaces that for harnesses
1152
- // that don't reliably expose a transcript_path during stop.
1153
- if (harness === "cursor" || harness === "codex") args.push("--task-nudge");
1154
- const result = spawnSync(agentCoordBin, args, { encoding: "utf8", timeout: 3000 });
1155
- if (result.status !== 0 || !result.stdout) return;
1156
- const additionalContext = result.stdout.trim();
1157
- 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;
1158
1399
 
1159
1400
  const { emitContext } = await import("./harness/output.ts");
1160
1401
  emitContext(harness, "UserPromptSubmit", additionalContext);
1402
+ return true;
1161
1403
  }
1162
1404
 
1163
1405
  function emitSubagentStartContext(
@@ -1210,45 +1452,45 @@ async function emitSessionStartSystemMessage(
1210
1452
  sessionId: string,
1211
1453
  emittedData: Record<string, unknown>,
1212
1454
  harness: Harness,
1213
- ): Promise<void> {
1455
+ recoveryBriefing = "",
1456
+ ): Promise<boolean> {
1214
1457
  const agentCoordBin = join(coordRoot, "harnery", "bin", "agent-coord");
1215
- 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
+ );
1216
1480
 
1217
- // Sync-project so the heartbeat exists for downstream readers (peer table,
1218
- // wiring check, council invites).
1219
- spawnSync(agentCoordBin, ["project"], { encoding: "utf8", timeout: 3000 });
1220
- // Stale-sweep dead peers before rendering peer table.
1221
- spawnSync(agentCoordBin, ["stale-sweep"], { encoding: "utf8", timeout: 3000 });
1222
-
1223
- // SESSION_START activity log line, fired across all harnesses.
1224
- const model = (emittedData.model as string | undefined) ?? "unknown";
1225
- const source = (emittedData.source as string | undefined) ?? "startup";
1226
- const platform = harnessPlatform(harness);
1227
- spawnSync(
1228
- agentCoordBin,
1229
- [
1230
- "log",
1231
- `SESSION_START model=${model} source=${source} platform=${platform}`,
1232
- "--instance",
1233
- instanceId,
1234
- ],
1235
- { encoding: "utf8", timeout: 2000 },
1236
- );
1237
-
1238
- // Render the systemMessage via agent-coord.
1239
- const agentName = (emittedData.name as string | undefined) ?? "";
1240
- const args = ["session-context", "--instance", instanceId, "--session", sessionId];
1241
- if (agentName) args.push("--name", agentName);
1242
- // The "You are agent-X." prefix in session-context renders unqualified by
1243
- // default (claude-code-style). For cursor/codex the bash dispatchers add
1244
- // a "(Cursor)" / "(Codex)" suffix; pass it through as --platform-label.
1245
- if (harness !== "claude-code") {
1246
- 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();
1247
1493
  }
1248
- const result = spawnSync(agentCoordBin, args, { encoding: "utf8", timeout: 3000 });
1249
- if (result.status !== 0 || !result.stdout) return;
1250
- let additionalContext = result.stdout.trim();
1251
- if (!additionalContext) return;
1252
1494
 
1253
1495
  // Effect (claude-code): merge the scratch recovery cue into the session-start
1254
1496
  // context. Was a standalone additionalContext emission from the previous
@@ -1256,11 +1498,38 @@ async function emitSessionStartSystemMessage(
1256
1498
  // entry, it folds in here.
1257
1499
  if (harness === "claude-code") {
1258
1500
  const cue = scratchRecoveryCue(coordRoot);
1259
- if (cue) additionalContext = `${additionalContext}\n\n${cue}`;
1501
+ if (cue) additionalContext = [additionalContext, cue].filter(Boolean).join("\n\n");
1260
1502
  }
1503
+ if (recoveryBriefing) {
1504
+ additionalContext = [additionalContext, recoveryBriefing].filter(Boolean).join("\n\n");
1505
+ }
1506
+ if (!additionalContext) return false;
1261
1507
 
1262
1508
  const { emitContext } = await import("./harness/output.ts");
1263
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
+ });
1264
1533
  }
1265
1534
 
1266
1535
  main()
@@ -194,6 +194,56 @@ export type ImageCaptured = EventEnvelope<
194
194
  }
195
195
  >;
196
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
+
197
247
  // File claims
198
248
  export type ClaimAcquire = EventEnvelope<
199
249
  "claim.acquire",
@@ -257,6 +307,18 @@ export type StatePresenceChange = EventEnvelope<
257
307
 
258
308
  export type StateHeartbeat = EventEnvelope<"state.heartbeat", Record<string, never>>;
259
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
+
260
322
  // Council
261
323
  export type CouncilOpen = EventEnvelope<
262
324
  "council.open",
@@ -398,6 +460,11 @@ export type Event =
398
460
  | ToolPostUseFailure
399
461
  | ToolOutputChunk
400
462
  | ImageCaptured
463
+ | ContextSampled
464
+ | ContextCheckpointCreated
465
+ | ContextCompactionStarted
466
+ | ContextCompactionCompleted
467
+ | ContextRecoveryInjected
401
468
  | ClaimAcquire
402
469
  | ClaimRelease
403
470
  | ClaimConflict
@@ -406,6 +473,7 @@ export type Event =
406
473
  | StateScratchAppend
407
474
  | StatePresenceChange
408
475
  | StateHeartbeat
476
+ | IdentityAssumed
409
477
  | CouncilOpen
410
478
  | CouncilRoundOpen
411
479
  | CouncilContribution
@@ -61,6 +61,7 @@ export const CLAUDE_CODE_EVENTS: HookEvent[] = [
61
61
  { settingsKey: "PreToolUse", subcommand: "pre-tool-use" },
62
62
  { settingsKey: "PostToolUse", subcommand: "post-tool-use" },
63
63
  { settingsKey: "PostToolUseFailure", subcommand: "post-tool-use-failure" },
64
+ { settingsKey: "PreCompact", subcommand: "pre-compact" },
64
65
  ];
65
66
 
66
67
  /**
@@ -88,6 +89,8 @@ export const CODEX_EVENTS: HookEvent[] = [
88
89
  { settingsKey: "UserPromptSubmit", subcommand: "user-prompt-submit" },
89
90
  { settingsKey: "SubagentStart", subcommand: "sub-agent-start" },
90
91
  { settingsKey: "SubagentStop", subcommand: "sub-agent-stop" },
92
+ { settingsKey: "PreCompact", subcommand: "pre-compact" },
93
+ { settingsKey: "PostCompact", subcommand: "post-compact" },
91
94
  { settingsKey: "Stop", subcommand: "stop" },
92
95
  ];
93
96
 
@@ -163,6 +163,10 @@ export function normalizeEventName(
163
163
  return { event_type: "tool.post_use", intra_turn: true };
164
164
  case "post-tool-use-failure":
165
165
  return { event_type: "tool.post_use_failure", intra_turn: true };
166
+ case "pre-compact":
167
+ return { event_type: "context.compaction.started", intra_turn: false };
168
+ case "post-compact":
169
+ return { event_type: "context.compaction.completed", intra_turn: false };
166
170
  default:
167
171
  return null;
168
172
  }
@@ -177,4 +181,6 @@ export type NormalizedEventType =
177
181
  | "subagent.stop"
178
182
  | "tool.pre_use"
179
183
  | "tool.post_use"
180
- | "tool.post_use_failure";
184
+ | "tool.post_use_failure"
185
+ | "context.compaction.started"
186
+ | "context.compaction.completed";