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
@@ -0,0 +1,380 @@
1
+ /**
2
+ * Context continuity: durable, structured state around native harness
3
+ * compaction. Harnery does not decide when a harness compacts. It records
4
+ * context telemetry when the harness exposes it, checkpoints external work
5
+ * state before compaction, and renders a verified recovery briefing after it.
6
+ */
7
+ import { spawnSync } from "node:child_process";
8
+ import { createHash, randomUUID } from "node:crypto";
9
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
10
+ import { dirname, join } from "node:path";
11
+ export const CONTEXT_SCHEMA_VERSION = 1;
12
+ export const MAX_CAPSULE_BYTES = 32 * 1024;
13
+ const MAX_SNAPSHOT_PATHS = 15;
14
+ const MAX_SNAPSHOT_PATH_CHARS = 500;
15
+ /** Parse the context-window shapes exposed by current harness hook/status data. */
16
+ export function extractContextSample(payload, options) {
17
+ const contextValue = payload.context_window;
18
+ const context = record(contextValue);
19
+ const usage = record(context?.current_usage) ?? record(payload.usage);
20
+ const windowTokens = number(context?.context_window_size) ??
21
+ number(context?.window_tokens) ??
22
+ number(payload.context_window_size) ??
23
+ (typeof contextValue === "number" ? finiteNonNegative(contextValue) : undefined);
24
+ const reportedUsed = number(context?.used_tokens) ?? number(context?.input_tokens) ?? number(payload.used_tokens);
25
+ const usageParts = usage
26
+ ? [
27
+ number(usage.input_tokens),
28
+ number(usage.cache_read_input_tokens),
29
+ number(usage.cache_creation_input_tokens),
30
+ ]
31
+ : [];
32
+ const hasUsageParts = usageParts.some((part) => part !== undefined);
33
+ const usedTokens = reportedUsed ??
34
+ (hasUsageParts
35
+ ? usageParts.reduce((total, part) => total + (part ?? 0), 0)
36
+ : undefined);
37
+ let usedPercent = number(context?.used_percentage) ?? number(payload.used_percentage);
38
+ if (usedPercent === undefined && usedTokens !== undefined && windowTokens && windowTokens > 0) {
39
+ usedPercent = (usedTokens / windowTokens) * 100;
40
+ }
41
+ if (usedPercent !== undefined)
42
+ usedPercent = clamp(usedPercent, 0, 100);
43
+ if (usedTokens === undefined && windowTokens === undefined && usedPercent === undefined)
44
+ return null;
45
+ const model = options.model ??
46
+ (typeof payload.model === "string" ? payload.model : undefined) ??
47
+ string(record(payload.model)?.id) ??
48
+ string(record(payload.model)?.display_name);
49
+ return {
50
+ session_id: options.sessionId,
51
+ harness: options.harness,
52
+ model,
53
+ used_tokens: usedTokens,
54
+ window_tokens: windowTokens,
55
+ used_percent: usedPercent === undefined ? undefined : round(usedPercent, 2),
56
+ source: options.source ?? "hook",
57
+ confidence: options.confidence ?? "reported",
58
+ observed_at: options.observedAt ?? new Date().toISOString(),
59
+ };
60
+ }
61
+ /** Persist a latest context sample. Identical measurements are a no-op. */
62
+ export function recordContextSample(coordRoot, instanceId, sample) {
63
+ const current = readContextState(coordRoot, sample.session_id);
64
+ const state = current ?? newState({ sessionId: sample.session_id, instanceId, now: sample.observed_at });
65
+ const changed = !sameMeasurement(state.latest_context, sample);
66
+ if (!changed)
67
+ return { state, changed: false };
68
+ const next = {
69
+ ...state,
70
+ instance_id: instanceId,
71
+ latest_context: sample,
72
+ updated_at: sample.observed_at,
73
+ };
74
+ writeState(coordRoot, next);
75
+ return { state: next, changed: true };
76
+ }
77
+ /**
78
+ * Snapshot external work state. Repeated PreCompact events reuse the existing
79
+ * checkpoint until a matching recovery completes; manual checkpoints always
80
+ * create a new generation.
81
+ */
82
+ export function checkpointContext(coordRoot, input) {
83
+ const now = new Date().toISOString();
84
+ const current = readContextState(coordRoot, input.sessionId) ??
85
+ newState({ sessionId: input.sessionId, instanceId: input.instanceId, now });
86
+ if (input.reason === "pre_compact" && current.phase === "checkpointed") {
87
+ const existing = readLatestCapsule(coordRoot, input.sessionId);
88
+ if (existing && current.latest_capsule) {
89
+ return {
90
+ capsule: existing,
91
+ path: join(coordRoot, ".harnery", current.latest_capsule),
92
+ state: current,
93
+ reused: true,
94
+ };
95
+ }
96
+ }
97
+ const heartbeat = readHeartbeat(coordRoot, input.instanceId);
98
+ const generation = current.generation + 1;
99
+ const capsule = {
100
+ schema_version: CONTEXT_SCHEMA_VERSION,
101
+ capsule_id: randomUUID(),
102
+ generation,
103
+ created_at: now,
104
+ reason: input.reason,
105
+ session: {
106
+ session_id: input.sessionId,
107
+ instance_id: input.instanceId,
108
+ harness: input.harness,
109
+ model: input.model ?? current.latest_context?.model ?? heartbeat?.model,
110
+ },
111
+ context: current.latest_context,
112
+ work: buildWorkSnapshot(heartbeat, input.continuationNote),
113
+ repo: snapshotRepo(input.cwd),
114
+ };
115
+ const relativePath = capsuleRelativePath(input.sessionId, generation);
116
+ const absolutePath = join(coordRoot, ".harnery", relativePath);
117
+ writeBoundedJson(absolutePath, capsule, MAX_CAPSULE_BYTES);
118
+ const next = {
119
+ ...current,
120
+ instance_id: input.instanceId,
121
+ phase: "checkpointed",
122
+ generation,
123
+ latest_capsule: relativePath,
124
+ updated_at: now,
125
+ recovered_at: undefined,
126
+ compaction_completed_at: undefined,
127
+ degraded_reason: undefined,
128
+ };
129
+ writeState(coordRoot, next);
130
+ return { capsule, path: absolutePath, state: next, reused: false };
131
+ }
132
+ /** Prepare a briefing without claiming that a harness has accepted it yet. */
133
+ export function prepareContextRecovery(coordRoot, input) {
134
+ const now = new Date().toISOString();
135
+ const current = readContextState(coordRoot, input.sessionId) ??
136
+ newState({ sessionId: input.sessionId, instanceId: input.instanceId, now });
137
+ const capsule = readLatestCapsule(coordRoot, input.sessionId);
138
+ if (!capsule) {
139
+ const degraded = {
140
+ ...current,
141
+ instance_id: input.instanceId,
142
+ phase: "degraded",
143
+ updated_at: now,
144
+ degraded_reason: "post-compaction signal arrived without a continuity checkpoint",
145
+ };
146
+ writeState(coordRoot, degraded);
147
+ return null;
148
+ }
149
+ const liveRepo = snapshotRepo(input.cwd);
150
+ const briefing = renderRecoveryBriefing(capsule, liveRepo);
151
+ return { state: current, capsule, briefing };
152
+ }
153
+ /** Advance continuity only after the harness context channel accepted the briefing. */
154
+ export function completeContextRecovery(coordRoot, input) {
155
+ const now = new Date().toISOString();
156
+ const current = readContextState(coordRoot, input.sessionId) ??
157
+ newState({ sessionId: input.sessionId, instanceId: input.instanceId, now });
158
+ const recovered = {
159
+ ...current,
160
+ instance_id: input.instanceId,
161
+ phase: "recovered",
162
+ updated_at: now,
163
+ recovered_at: now,
164
+ degraded_reason: undefined,
165
+ };
166
+ writeState(coordRoot, recovered);
167
+ return recovered;
168
+ }
169
+ /** Record that the harness, rather than Harnery, reported compaction complete. */
170
+ export function markContextCompactionCompleted(coordRoot, input) {
171
+ const now = input.observedAt ?? new Date().toISOString();
172
+ const current = readContextState(coordRoot, input.sessionId) ??
173
+ newState({ sessionId: input.sessionId, instanceId: input.instanceId, now });
174
+ const next = {
175
+ ...current,
176
+ instance_id: input.instanceId,
177
+ phase: current.latest_capsule ? current.phase : "degraded",
178
+ compaction_completed_at: now,
179
+ updated_at: now,
180
+ degraded_reason: current.latest_capsule
181
+ ? current.degraded_reason
182
+ : "post-compaction signal arrived without a continuity checkpoint",
183
+ };
184
+ writeState(coordRoot, next);
185
+ return next;
186
+ }
187
+ /** Convenience API for callers whose injection is synchronous and guaranteed. */
188
+ export function recoverContext(coordRoot, input) {
189
+ const prepared = prepareContextRecovery(coordRoot, input);
190
+ if (!prepared) {
191
+ const state = readContextState(coordRoot, input.sessionId);
192
+ return { state, capsule: null, briefing: "", recovered: false };
193
+ }
194
+ const recovered = completeContextRecovery(coordRoot, input);
195
+ return {
196
+ state: recovered,
197
+ capsule: prepared.capsule,
198
+ briefing: prepared.briefing,
199
+ recovered: true,
200
+ };
201
+ }
202
+ export function readContextState(coordRoot, sessionId) {
203
+ return readJson(statePath(coordRoot, sessionId));
204
+ }
205
+ export function readLatestCapsule(coordRoot, sessionId) {
206
+ const state = readContextState(coordRoot, sessionId);
207
+ if (!state?.latest_capsule)
208
+ return null;
209
+ return readJson(join(coordRoot, ".harnery", state.latest_capsule));
210
+ }
211
+ export function renderRecoveryBriefing(capsule, liveRepo) {
212
+ const lines = [
213
+ `[harnery context continuity] Recovered generation ${capsule.generation} after native context compaction.`,
214
+ ];
215
+ if (capsule.work.task)
216
+ lines.push(`Task: ${capsule.work.task}`);
217
+ if (capsule.work.turn_summary)
218
+ lines.push(`Last progress: ${capsule.work.turn_summary}`);
219
+ if (capsule.work.continuation_note) {
220
+ lines.push(`Continuation note: ${capsule.work.continuation_note}`);
221
+ }
222
+ if (capsule.work.files_held.length > 0) {
223
+ lines.push(`Files held: ${capsule.work.files_held.join(", ")}`);
224
+ }
225
+ if (capsule.work.last_tool) {
226
+ lines.push(`Last tool: ${capsule.work.last_tool}${capsule.work.last_tool_target ? ` (${capsule.work.last_tool_target})` : ""}`);
227
+ }
228
+ const drift = [];
229
+ if (capsule.repo.branch && liveRepo.branch && capsule.repo.branch !== liveRepo.branch) {
230
+ drift.push(`branch changed ${capsule.repo.branch} -> ${liveRepo.branch}`);
231
+ }
232
+ if (capsule.repo.head && liveRepo.head && capsule.repo.head !== liveRepo.head) {
233
+ drift.push(`HEAD changed ${capsule.repo.head.slice(0, 8)} -> ${liveRepo.head.slice(0, 8)}`);
234
+ }
235
+ const beforeDirty = new Set(capsule.repo.dirty_paths);
236
+ const afterDirty = new Set(liveRepo.dirty_paths);
237
+ const added = [...afterDirty].filter((path) => !beforeDirty.has(path));
238
+ const cleared = [...beforeDirty].filter((path) => !afterDirty.has(path));
239
+ if (added.length > 0)
240
+ drift.push(`new dirty paths: ${added.slice(0, 5).join(", ")}`);
241
+ if (cleared.length > 0)
242
+ drift.push(`paths no longer dirty: ${cleared.slice(0, 5).join(", ")}`);
243
+ lines.push(drift.length > 0
244
+ ? `Repository changed since checkpoint: ${drift.join("; ")}. Re-check before editing.`
245
+ : "Repository state matches the checkpoint.");
246
+ return lines.join("\n");
247
+ }
248
+ export function snapshotRepo(cwd) {
249
+ const root = git(cwd, ["rev-parse", "--show-toplevel"]);
250
+ if (!root)
251
+ return { cwd, dirty_paths: [] };
252
+ const status = git(cwd, ["status", "--porcelain=v1", "--untracked-files=normal"]);
253
+ const dirty = status
254
+ ? status
255
+ .split("\n")
256
+ .map((line) => line.trimEnd())
257
+ .filter(Boolean)
258
+ .map((line) => clampString(line.slice(3).trim(), MAX_SNAPSHOT_PATH_CHARS) ?? "")
259
+ .filter(Boolean)
260
+ : [];
261
+ return {
262
+ cwd: clampString(cwd, 2_000) ?? cwd,
263
+ root: clampString(root, 2_000),
264
+ branch: git(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]),
265
+ head: git(cwd, ["rev-parse", "HEAD"]),
266
+ dirty_paths: dirty.slice(0, MAX_SNAPSHOT_PATHS),
267
+ ...(dirty.length > MAX_SNAPSHOT_PATHS ? { dirty_paths_truncated: true } : {}),
268
+ };
269
+ }
270
+ function buildWorkSnapshot(heartbeat, continuationNote) {
271
+ const files = Array.isArray(heartbeat?.files_touched)
272
+ ? heartbeat.files_touched
273
+ .map((path) => clampString(path, MAX_SNAPSHOT_PATH_CHARS) ?? "")
274
+ .filter(Boolean)
275
+ : [];
276
+ return {
277
+ task: clampString(heartbeat?.task, 1_000),
278
+ turn_summary: clampString(heartbeat?.turn_summary, 3_000),
279
+ continuation_note: clampString(continuationNote, 3_000),
280
+ files_held: files.slice(0, MAX_SNAPSHOT_PATHS),
281
+ ...(files.length > MAX_SNAPSHOT_PATHS ? { files_held_truncated: true } : {}),
282
+ last_tool: clampString(heartbeat?.last_tool, 200),
283
+ last_tool_target: clampString(heartbeat?.last_tool_target, 1_000),
284
+ };
285
+ }
286
+ function readHeartbeat(coordRoot, instanceId) {
287
+ return readJson(join(coordRoot, ".harnery", "active", `${instanceId}.json`));
288
+ }
289
+ function newState(input) {
290
+ return {
291
+ schema_version: CONTEXT_SCHEMA_VERSION,
292
+ session_id: input.sessionId,
293
+ instance_id: input.instanceId,
294
+ phase: "observing",
295
+ generation: 0,
296
+ updated_at: input.now,
297
+ };
298
+ }
299
+ function writeState(coordRoot, state) {
300
+ writeBoundedJson(statePath(coordRoot, state.session_id), state, MAX_CAPSULE_BYTES);
301
+ }
302
+ function statePath(coordRoot, sessionId) {
303
+ return join(contextDir(coordRoot, sessionId), "state.json");
304
+ }
305
+ function capsuleRelativePath(sessionId, generation) {
306
+ return join("context", sessionKey(sessionId), "capsules", `${String(generation).padStart(6, "0")}.json`);
307
+ }
308
+ function contextDir(coordRoot, sessionId) {
309
+ return join(coordRoot, ".harnery", "context", sessionKey(sessionId));
310
+ }
311
+ function sessionKey(sessionId) {
312
+ return createHash("sha256").update(sessionId).digest("hex").slice(0, 24);
313
+ }
314
+ function writeBoundedJson(path, value, maxBytes) {
315
+ const text = `${JSON.stringify(value, null, 2)}\n`;
316
+ const bytes = Buffer.byteLength(text, "utf8");
317
+ if (bytes > maxBytes) {
318
+ throw new Error(`context continuity record exceeds ${maxBytes} bytes (${bytes})`);
319
+ }
320
+ mkdirSync(dirname(path), { recursive: true });
321
+ const temp = `${path}.tmp.${process.pid}.${randomUUID()}`;
322
+ writeFileSync(temp, text, { encoding: "utf8", mode: 0o600 });
323
+ renameSync(temp, path);
324
+ }
325
+ function readJson(path) {
326
+ if (!existsSync(path))
327
+ return null;
328
+ try {
329
+ return JSON.parse(readFileSync(path, "utf8"));
330
+ }
331
+ catch {
332
+ return null;
333
+ }
334
+ }
335
+ function git(cwd, args) {
336
+ try {
337
+ const result = spawnSync("git", args, { cwd, encoding: "utf8", timeout: 2_000 });
338
+ return result.status === 0 ? result.stdout.trim() || undefined : undefined;
339
+ }
340
+ catch {
341
+ return undefined;
342
+ }
343
+ }
344
+ function sameMeasurement(a, b) {
345
+ if (!a)
346
+ return false;
347
+ return (a.harness === b.harness &&
348
+ a.model === b.model &&
349
+ a.used_tokens === b.used_tokens &&
350
+ a.window_tokens === b.window_tokens &&
351
+ a.used_percent === b.used_percent &&
352
+ a.source === b.source &&
353
+ a.confidence === b.confidence);
354
+ }
355
+ function record(value) {
356
+ return typeof value === "object" && value !== null && !Array.isArray(value)
357
+ ? value
358
+ : undefined;
359
+ }
360
+ function string(value) {
361
+ return typeof value === "string" && value.length > 0 ? value : undefined;
362
+ }
363
+ function number(value) {
364
+ return typeof value === "number" ? finiteNonNegative(value) : undefined;
365
+ }
366
+ function finiteNonNegative(value) {
367
+ return Number.isFinite(value) && value >= 0 ? value : undefined;
368
+ }
369
+ function clamp(value, min, max) {
370
+ return Math.min(max, Math.max(min, value));
371
+ }
372
+ function round(value, precision) {
373
+ const factor = 10 ** precision;
374
+ return Math.round(value * factor) / factor;
375
+ }
376
+ function clampString(value, max) {
377
+ if (!value)
378
+ return undefined;
379
+ return value.length <= max ? value : value.slice(0, max);
380
+ }
@@ -0,0 +1,29 @@
1
+ import type { HarnessRegistry } from "./registry.js";
2
+ import type { CapabilitySupport, HarnessCapabilityDimension, HarnessId } from "./types.js";
3
+ export type BenchVerdict = "supported" | "partial" | "unsupported" | "not_applicable" | "unknown" | "skipped" | "drift";
4
+ export type BenchDimension = "registration" | "binary" | HarnessCapabilityDimension;
5
+ export interface BenchResult {
6
+ harness: HarnessId;
7
+ dimension: BenchDimension;
8
+ declared: CapabilitySupport | "not_applicable";
9
+ observed: BenchVerdict;
10
+ verdict: BenchVerdict;
11
+ note?: string;
12
+ }
13
+ export interface HarnessBenchReport {
14
+ generatedAt: string;
15
+ mode: "offline";
16
+ harnesses: HarnessId[];
17
+ results: BenchResult[];
18
+ summary: Record<BenchVerdict, number>;
19
+ drift: boolean;
20
+ skipped: boolean;
21
+ }
22
+ export interface HarnessBenchOptions {
23
+ harnesses?: readonly string[];
24
+ dimensions?: readonly HarnessCapabilityDimension[];
25
+ /** Test seam and alternate host probe. A null version means unavailable. */
26
+ versionProbe?: (binary: string) => string | null;
27
+ }
28
+ export declare function runHarnessBench(registry: HarnessRegistry, opts?: HarnessBenchOptions): HarnessBenchReport;
29
+ //# sourceMappingURL=bench.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bench.d.ts","sourceRoot":"","sources":["../../../src/core/harnesses/bench.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EACV,iBAAiB,EAEjB,0BAA0B,EAC1B,SAAS,EACV,MAAM,YAAY,CAAC;AAGpB,MAAM,MAAM,YAAY,GACpB,WAAW,GACX,SAAS,GACT,aAAa,GACb,gBAAgB,GAChB,SAAS,GACT,SAAS,GACT,OAAO,CAAC;AAEZ,MAAM,MAAM,cAAc,GAAG,cAAc,GAAG,QAAQ,GAAG,0BAA0B,CAAC;AAEpF,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,SAAS,CAAC;IACnB,SAAS,EAAE,cAAc,CAAC;IAC1B,QAAQ,EAAE,iBAAiB,GAAG,gBAAgB,CAAC;IAC/C,QAAQ,EAAE,YAAY,CAAC;IACvB,OAAO,EAAE,YAAY,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,SAAS,CAAC;IAChB,SAAS,EAAE,SAAS,EAAE,CAAC;IACvB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IACtC,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,UAAU,CAAC,EAAE,SAAS,0BAA0B,EAAE,CAAC;IACnD,4EAA4E;IAC5E,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;CAClD;AAYD,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,eAAe,EACzB,IAAI,GAAE,mBAAwB,GAC7B,kBAAkB,CAuDpB"}
@@ -0,0 +1,151 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { HARNESS_SPECS } from "../hooks/harness/events.js";
3
+ import { HARNESS_CAPABILITY_DIMENSIONS } from "./types.js";
4
+ const EMPTY_SUMMARY = {
5
+ supported: 0,
6
+ partial: 0,
7
+ unsupported: 0,
8
+ not_applicable: 0,
9
+ unknown: 0,
10
+ skipped: 0,
11
+ drift: 0,
12
+ };
13
+ export function runHarnessBench(registry, opts = {}) {
14
+ const ids = opts.harnesses?.length ? [...opts.harnesses] : registry.ids();
15
+ const dimensions = opts.dimensions?.length
16
+ ? [...new Set(opts.dimensions)]
17
+ : [...HARNESS_CAPABILITY_DIMENSIONS];
18
+ const versionProbe = opts.versionProbe ?? probeVersion;
19
+ const results = [];
20
+ for (const id of ids) {
21
+ const adapter = registry.require(id);
22
+ results.push({
23
+ harness: id,
24
+ dimension: "registration",
25
+ declared: "supported",
26
+ observed: "supported",
27
+ verdict: "supported",
28
+ note: `${adapter.profile.integrationMode}; ${adapter.profile.authModel}`,
29
+ });
30
+ const version = versionProbe(adapter.profile.binary);
31
+ results.push({
32
+ harness: id,
33
+ dimension: "binary",
34
+ declared: "supported",
35
+ observed: version ? "supported" : "skipped",
36
+ verdict: version ? "supported" : "skipped",
37
+ note: version ?? `${adapter.profile.binary} not found on PATH`,
38
+ });
39
+ const observations = observeAdapter(adapter);
40
+ for (const dimension of dimensions) {
41
+ const claim = adapter.profile.capabilities[dimension];
42
+ const observed = observations[dimension];
43
+ results.push({
44
+ harness: id,
45
+ dimension,
46
+ declared: claim.support,
47
+ observed,
48
+ verdict: reconcile(claim.support, observed),
49
+ note: claim.note,
50
+ });
51
+ }
52
+ }
53
+ const summary = { ...EMPTY_SUMMARY };
54
+ for (const result of results)
55
+ summary[result.verdict]++;
56
+ return {
57
+ generatedAt: new Date().toISOString(),
58
+ mode: "offline",
59
+ harnesses: ids,
60
+ results,
61
+ summary,
62
+ drift: summary.drift > 0,
63
+ skipped: summary.skipped > 0,
64
+ };
65
+ }
66
+ function observeAdapter(adapter) {
67
+ const profile = adapter.profile;
68
+ const effort = profile.effortValues[0];
69
+ const request = {
70
+ prompt: "HARNERY_BENCH_PROMPT",
71
+ model: "harnery-bench-model",
72
+ effort,
73
+ timeoutMs: 1_000,
74
+ maxTurns: 7,
75
+ cwd: "/harnery-bench",
76
+ };
77
+ let argv = [];
78
+ let planningFailed = false;
79
+ try {
80
+ argv = adapter.buildInvocation(request, "/harnery-bench/final.txt").argv;
81
+ }
82
+ catch {
83
+ planningFailed = true;
84
+ }
85
+ let normalized = null;
86
+ try {
87
+ normalized = adapter.normalizeResult(adapter.fixture.raw);
88
+ }
89
+ catch {
90
+ normalized = null;
91
+ }
92
+ const fixture = adapter.fixture.expected;
93
+ const finalMatches = normalized?.ok === fixture.ok && normalized?.text === fixture.text;
94
+ const sessionObserved = normalized?.sessionId !== undefined ? "supported" : "unsupported";
95
+ const costObserved = normalized?.costUsd !== undefined ? "supported" : "unsupported";
96
+ const hookSpec = adapter.profile.id in HARNESS_SPECS
97
+ ? HARNESS_SPECS[adapter.profile.id]
98
+ : undefined;
99
+ const hookSubcommands = new Set(hookSpec?.events.map((event) => event.subcommand) ?? []);
100
+ return {
101
+ invocation: !planningFailed && argv[0] === profile.binary && argv.includes(request.prompt)
102
+ ? "supported"
103
+ : "unsupported",
104
+ modelSelection: !planningFailed && argv.includes(request.model ?? "") ? "supported" : "unsupported",
105
+ effortSelection: effort === undefined
106
+ ? "unsupported"
107
+ : !planningFailed && argv.some((arg) => arg.includes(effort))
108
+ ? "supported"
109
+ : "unsupported",
110
+ maxTurns: !planningFailed && argv.includes(String(request.maxTurns)) ? "supported" : "unsupported",
111
+ finalResult: finalMatches ? "supported" : "unsupported",
112
+ sessionId: sessionObserved === "supported" && normalized?.sessionId !== fixture.sessionId
113
+ ? "unsupported"
114
+ : sessionObserved,
115
+ cost: costObserved === "supported" && normalized?.costUsd !== fixture.costUsd
116
+ ? "unsupported"
117
+ : costObserved,
118
+ toolEvidence: normalized && "toolEvidence" in normalized ? "supported" : "unsupported",
119
+ policyMapping: "unknown",
120
+ interruption: "unknown",
121
+ streaming: "unknown",
122
+ steering: "unknown",
123
+ resume: "unknown",
124
+ images: "unknown",
125
+ contextTelemetry: "unknown",
126
+ preCompactionSignal: hookSpec
127
+ ? hookSubcommands.has("pre-compact")
128
+ ? "supported"
129
+ : "unsupported"
130
+ : "unknown",
131
+ postCompactionSignal: hookSpec
132
+ ? hookSubcommands.has("post-compact") ||
133
+ (adapter.profile.id === "claude-code" && hookSubcommands.has("session-start"))
134
+ ? "supported"
135
+ : "unsupported"
136
+ : "unknown",
137
+ compaction: "unknown",
138
+ };
139
+ }
140
+ function reconcile(declared, observed) {
141
+ if (observed === "unknown" || observed === "skipped" || observed === "not_applicable") {
142
+ return observed;
143
+ }
144
+ return declared === observed ? observed : "drift";
145
+ }
146
+ function probeVersion(binary) {
147
+ const result = spawnSync(binary, ["--version"], { encoding: "utf8", timeout: 5_000 });
148
+ if (result.error || result.status !== 0)
149
+ return null;
150
+ return (result.stdout || result.stderr).trim().split("\n")[0] || "installed";
151
+ }
@@ -0,0 +1,9 @@
1
+ export type { Spawner, SpawnRequest, SpawnResult } from "../workflow/types.js";
2
+ export type { BenchDimension, BenchResult, BenchVerdict, HarnessBenchOptions, HarnessBenchReport, } from "./bench.js";
3
+ export { runHarnessBench } from "./bench.js";
4
+ export type { BuiltinHarnessId } from "./profiles.js";
5
+ export { BUILTIN_HARNESS_IDS, BUILTIN_HARNESS_PROFILES, builtinHarnessProfile, isBuiltinHarness, validateHarnessEffort, } from "./profiles.js";
6
+ export { createBuiltinHarnessRegistry, HarnessRegistry } from "./registry.js";
7
+ export type { CapabilityClaim, CapabilitySupport, HarnessAdapter, HarnessBenchFixture, HarnessCapabilities, HarnessCapabilityDimension, HarnessId, HarnessInvocation, HarnessProfile, HarnessRawResult, } from "./types.js";
8
+ export { HARNESS_CAPABILITY_DIMENSIONS } from "./types.js";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/harnesses/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC/E,YAAY,EACV,cAAc,EACd,WAAW,EACX,YAAY,EACZ,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,qBAAqB,EACrB,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,4BAA4B,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC9E,YAAY,EACV,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,mBAAmB,EACnB,mBAAmB,EACnB,0BAA0B,EAC1B,SAAS,EACT,iBAAiB,EACjB,cAAc,EACd,gBAAgB,GACjB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,6BAA6B,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { runHarnessBench } from "./bench.js";
2
+ export { BUILTIN_HARNESS_IDS, BUILTIN_HARNESS_PROFILES, builtinHarnessProfile, isBuiltinHarness, validateHarnessEffort, } from "./profiles.js";
3
+ export { createBuiltinHarnessRegistry, HarnessRegistry } from "./registry.js";
4
+ export { HARNESS_CAPABILITY_DIMENSIONS } from "./types.js";
@@ -0,0 +1,62 @@
1
+ import type { HarnessCapabilities, HarnessProfile } from "./types.js";
2
+ /** The one built-in profile catalog. Adding a fourth harness starts here; CLI
3
+ * choices, doctor metadata, workflow dispatch, and the bench derive from it. */
4
+ export declare const BUILTIN_HARNESS_PROFILES: {
5
+ readonly "claude-code": {
6
+ readonly id: "claude-code";
7
+ readonly displayName: "Claude Code";
8
+ readonly binary: "claude";
9
+ readonly installHint: "npm install -g @anthropic-ai/claude-code";
10
+ readonly loginHint: "run `claude` and use /login";
11
+ readonly apiKeyEnv: "ANTHROPIC_API_KEY";
12
+ readonly integrationMode: "cli-subprocess";
13
+ readonly authModel: "own-auth";
14
+ readonly modelFamily: "claude";
15
+ readonly effortValues: readonly ["low", "medium", "high", "xhigh", "max"];
16
+ readonly verified: {
17
+ readonly date: "2026-07-20";
18
+ readonly version: "current CLI contract";
19
+ };
20
+ readonly capabilities: HarnessCapabilities;
21
+ };
22
+ readonly codex: {
23
+ readonly id: "codex";
24
+ readonly displayName: "Codex";
25
+ readonly binary: "codex";
26
+ readonly installHint: "npm install -g @openai/codex";
27
+ readonly loginHint: "codex login";
28
+ readonly apiKeyEnv: "OPENAI_API_KEY";
29
+ readonly integrationMode: "cli-subprocess";
30
+ readonly authModel: "own-auth";
31
+ readonly modelFamily: "gpt";
32
+ readonly effortValues: readonly ["none", "minimal", "low", "medium", "high", "xhigh"];
33
+ readonly verified: {
34
+ readonly date: "2026-07-21";
35
+ readonly version: "codex-cli 0.145.0-alpha.18";
36
+ };
37
+ readonly capabilities: HarnessCapabilities;
38
+ };
39
+ readonly cursor: {
40
+ readonly id: "cursor";
41
+ readonly displayName: "Cursor Agent";
42
+ readonly binary: "cursor-agent";
43
+ readonly installHint: "curl https://cursor.com/install -fsS | bash";
44
+ readonly loginHint: "cursor-agent login";
45
+ readonly apiKeyEnv: "CURSOR_API_KEY";
46
+ readonly integrationMode: "cli-subprocess";
47
+ readonly authModel: "own-auth";
48
+ readonly modelFamily: "multi";
49
+ readonly effortValues: readonly [];
50
+ readonly verified: {
51
+ readonly date: "2026-07-21";
52
+ readonly version: "2026.07.16-899851b";
53
+ };
54
+ readonly capabilities: HarnessCapabilities;
55
+ };
56
+ };
57
+ export type BuiltinHarnessId = keyof typeof BUILTIN_HARNESS_PROFILES;
58
+ export declare const BUILTIN_HARNESS_IDS: readonly ("cursor" | "claude-code" | "codex")[];
59
+ export declare function isBuiltinHarness(id: string): id is BuiltinHarnessId;
60
+ export declare function builtinHarnessProfile(id: string): HarnessProfile | undefined;
61
+ export declare function validateHarnessEffort(id: string, effort: string | undefined): void;
62
+ //# sourceMappingURL=profiles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../../../src/core/harnesses/profiles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAmB,mBAAmB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AA+BvF;gFACgF;AAChF,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiFc,CAAC;AAEpD,MAAM,MAAM,gBAAgB,GAAG,MAAM,OAAO,wBAAwB,CAAC;AAErE,eAAO,MAAM,mBAAmB,iDAE/B,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,EAAE,IAAI,gBAAgB,CAEnE;AAED,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAE5E;AAED,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAUlF"}