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 @@
1
+ {"version":3,"file":"relay-client.d.ts","sourceRoot":"","sources":["../../../src/core/presence/relay-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAwFH;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAwBzD;AAED,sEAAsE;AACtE,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAqKvE;AAED,0DAA0D;AAC1D,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG;IACpD,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAMA"}
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Relay client transport for cross-machine presence (ADR 0016, phase 2c).
3
+ *
4
+ * The hooks are short-lived processes, so a live WebSocket needs a home: a
5
+ * small per-repo-per-machine DAEMON that the hooks lazy-start
6
+ * (`ensureRelayDaemon`) and that exits itself when the machine goes idle.
7
+ * The daemon:
8
+ *
9
+ * - derives the room credentials from the repo identity (root-commit SHA +
10
+ * origin URL + optional committed `.harnery/presence-salt`)
11
+ * - holds the socket open with auto-reconnect + jittered backoff
12
+ * - watches `.harnery/active/` and publishes the encrypted presence blob on
13
+ * change (debounced), plus a 60s keepalive so peers' staleness math works
14
+ * - decrypts received peer frames and writes them to
15
+ * `.harnery/presence/remote/<machine>.json`, where `readRemoteMachines`
16
+ * merges them with the git-refs floor (freshest source per machine wins)
17
+ * - exits after ~5 minutes with no live local sessions (hooks restart it
18
+ * when sessions return), and exits if its pid file is superseded
19
+ *
20
+ * Everything fails silent; the git-refs transport is always the floor.
21
+ * Requires a global WebSocket (Bun, Node >= 21, Workers) — on older Nodes the
22
+ * daemon refuses to start and presence stays on git-refs.
23
+ */
24
+ import { spawn } from "node:child_process";
25
+ import { existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, watch, writeFileSync, } from "node:fs";
26
+ import { join } from "node:path";
27
+ import { resolveMachineLabel } from "../../lib/machine.js";
28
+ import { presenceRelayUrl } from "../config.js";
29
+ import { buildPresenceBlob } from "./blob.js";
30
+ import { originUrl, rootCommitSha, sanitizeRefComponent } from "./git.js";
31
+ import { computeSenderId, decryptPayload, deriveRoomCredentials, encryptPayload, parseRelayFrame, } from "./relay-protocol.js";
32
+ const KEEPALIVE_MS = 60_000;
33
+ const WATCH_DEBOUNCE_MS = 500;
34
+ const IDLE_EXIT_MS = 5 * 60_000;
35
+ const BACKOFF_MIN_MS = 1_000;
36
+ const BACKOFF_MAX_MS = 60_000;
37
+ function presenceDir(coordRoot) {
38
+ return join(coordRoot, ".harnery", "presence");
39
+ }
40
+ function remoteCacheDir(coordRoot) {
41
+ return join(presenceDir(coordRoot), "remote");
42
+ }
43
+ function pidFilePath(coordRoot) {
44
+ return join(presenceDir(coordRoot), "relay-daemon.json");
45
+ }
46
+ function logFilePath(coordRoot) {
47
+ return join(presenceDir(coordRoot), "relay-daemon.log");
48
+ }
49
+ function readPidRecord(coordRoot) {
50
+ try {
51
+ return JSON.parse(readFileSync(pidFilePath(coordRoot), "utf8"));
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ function pidAlive(pid) {
58
+ try {
59
+ process.kill(pid, 0);
60
+ return true;
61
+ }
62
+ catch {
63
+ return false;
64
+ }
65
+ }
66
+ /** The committed rotatable room salt, when the repo minted one. */
67
+ function readRoomSalt(coordRoot) {
68
+ try {
69
+ const s = readFileSync(join(coordRoot, ".harnery", "presence-salt"), "utf8").trim();
70
+ return s || undefined;
71
+ }
72
+ catch {
73
+ return undefined;
74
+ }
75
+ }
76
+ function atomicWriteJson(path, value) {
77
+ mkdirSync(join(path, ".."), { recursive: true });
78
+ const tmp = `${path}.tmp.${process.pid}`;
79
+ writeFileSync(tmp, JSON.stringify(value), "utf8");
80
+ renameSync(tmp, path);
81
+ }
82
+ /**
83
+ * Lazy-start the relay daemon for this repo if a relay is configured and no
84
+ * live daemon already owns the pid file. Called from the hook path — must
85
+ * return fast and never throw.
86
+ */
87
+ export function ensureRelayDaemon(coordRoot) {
88
+ try {
89
+ const url = presenceRelayUrl(coordRoot);
90
+ if (!url)
91
+ return;
92
+ const rec = readPidRecord(coordRoot);
93
+ if (rec && rec.url === url && pidAlive(rec.pid))
94
+ return;
95
+ // Resolve the harn bin at the package root (works from src/ under Bun and
96
+ // dist/ under Node — both sit two levels below the root).
97
+ const harnBin = new URL("../../../bin/harn", import.meta.url).pathname;
98
+ if (!existsSync(harnBin))
99
+ return;
100
+ mkdirSync(presenceDir(coordRoot), { recursive: true });
101
+ const log = openSync(logFilePath(coordRoot), "a");
102
+ const child = spawn(harnBin, ["presence", "relay-daemon"], {
103
+ cwd: coordRoot,
104
+ detached: true,
105
+ stdio: ["ignore", log, log],
106
+ env: { ...process.env, HARNERY_OUTPUT_SESSION_TEE: "0" },
107
+ });
108
+ child.unref();
109
+ }
110
+ catch {
111
+ /* best-effort */
112
+ }
113
+ }
114
+ /** Foreground daemon loop (spawned detached by ensureRelayDaemon). */
115
+ export async function runRelayDaemon(coordRoot) {
116
+ const url = presenceRelayUrl(coordRoot);
117
+ if (!url) {
118
+ console.error("relay-daemon: no relay configured (presence.relay)");
119
+ return 2;
120
+ }
121
+ if (typeof WebSocket === "undefined") {
122
+ console.error("relay-daemon: no global WebSocket in this runtime (need Bun or Node >= 21)");
123
+ return 2;
124
+ }
125
+ const sha = rootCommitSha(coordRoot);
126
+ const origin = originUrl(coordRoot);
127
+ if (!sha || !origin) {
128
+ console.error("relay-daemon: repo has no commits or no origin remote");
129
+ return 2;
130
+ }
131
+ // Take (or take over) the pid file. A superseded daemon notices and exits.
132
+ const myRecord = {
133
+ pid: process.pid,
134
+ url,
135
+ started_at: new Date().toISOString(),
136
+ };
137
+ atomicWriteJson(pidFilePath(coordRoot), myRecord);
138
+ const creds = await deriveRoomCredentials({
139
+ rootCommitSha: sha,
140
+ originUrl: origin,
141
+ salt: readRoomSalt(coordRoot),
142
+ });
143
+ const machine = sanitizeRefComponent(resolveMachineLabel());
144
+ const senderId = await computeSenderId(creds, machine);
145
+ const wsUrl = `${url.replace(/\/+$/, "")}/v1/room/${creds.roomId}`;
146
+ let ws = null;
147
+ let backoff = BACKOFF_MIN_MS;
148
+ let lastBasisHash = "";
149
+ let lastSentAt = 0;
150
+ let idleSince = null;
151
+ let shuttingDown = false;
152
+ const log = (msg) => {
153
+ console.log(`[${new Date().toISOString()}] ${msg}`);
154
+ };
155
+ const publishNow = async (force = false) => {
156
+ if (!ws || ws.readyState !== WebSocket.OPEN)
157
+ return;
158
+ try {
159
+ const { blob, basisHash, json } = buildPresenceBlob(coordRoot);
160
+ const keepaliveDue = Date.now() - lastSentAt >= KEEPALIVE_MS;
161
+ if (!force && basisHash === lastBasisHash && !keepaliveDue)
162
+ return;
163
+ const enc = await encryptPayload(creds, json);
164
+ ws.send(JSON.stringify({ t: "pub", sender: senderId, ...enc }));
165
+ lastBasisHash = basisHash;
166
+ lastSentAt = Date.now();
167
+ idleSince = blob.agents.length === 0 ? (idleSince ?? Date.now()) : null;
168
+ }
169
+ catch {
170
+ /* next trigger retries */
171
+ }
172
+ };
173
+ const handleFrame = async (raw) => {
174
+ const frame = parseRelayFrame(raw);
175
+ if (frame?.t !== "pub")
176
+ return;
177
+ if (frame.sender === senderId)
178
+ return; // our own warm-join echo
179
+ const pt = await decryptPayload(creds, frame);
180
+ if (!pt)
181
+ return;
182
+ let blob;
183
+ try {
184
+ blob = JSON.parse(pt);
185
+ }
186
+ catch {
187
+ return;
188
+ }
189
+ if (blob?.v !== 1 || typeof blob.machine !== "string")
190
+ return;
191
+ const m = sanitizeRefComponent(blob.machine);
192
+ if (m === machine)
193
+ return;
194
+ try {
195
+ atomicWriteJson(join(remoteCacheDir(coordRoot), `${m}.json`), blob);
196
+ }
197
+ catch {
198
+ /* best-effort */
199
+ }
200
+ };
201
+ const connect = () => {
202
+ if (shuttingDown)
203
+ return;
204
+ try {
205
+ const sock = new WebSocket(wsUrl);
206
+ ws = sock;
207
+ sock.onopen = () => {
208
+ backoff = BACKOFF_MIN_MS;
209
+ log(`connected room=${creds.roomId.slice(0, 8)}… as ${machine}`);
210
+ void publishNow(true);
211
+ };
212
+ sock.onmessage = (ev) => {
213
+ void handleFrame(String(ev.data));
214
+ };
215
+ const scheduleReconnect = () => {
216
+ if (shuttingDown)
217
+ return;
218
+ const jitter = Math.random() * backoff * 0.3;
219
+ const delay = Math.min(BACKOFF_MAX_MS, backoff) + jitter;
220
+ backoff = Math.min(BACKOFF_MAX_MS, backoff * 2);
221
+ setTimeout(connect, delay);
222
+ };
223
+ sock.onclose = scheduleReconnect;
224
+ sock.onerror = () => {
225
+ try {
226
+ sock.close();
227
+ }
228
+ catch {
229
+ /* ignore */
230
+ }
231
+ };
232
+ }
233
+ catch {
234
+ setTimeout(connect, backoff);
235
+ backoff = Math.min(BACKOFF_MAX_MS, backoff * 2);
236
+ }
237
+ };
238
+ connect();
239
+ // Publish on heartbeat change (debounced).
240
+ let debounce = null;
241
+ const activeDir = join(coordRoot, ".harnery", "active");
242
+ mkdirSync(activeDir, { recursive: true });
243
+ try {
244
+ watch(activeDir, () => {
245
+ if (debounce)
246
+ clearTimeout(debounce);
247
+ debounce = setTimeout(() => void publishNow(), WATCH_DEBOUNCE_MS);
248
+ });
249
+ }
250
+ catch {
251
+ /* keepalive interval still covers publication */
252
+ }
253
+ // Keepalive + lifecycle checks.
254
+ const interval = setInterval(() => {
255
+ void publishNow();
256
+ // Superseded by a newer daemon → exit quietly.
257
+ const rec = readPidRecord(coordRoot);
258
+ if (!rec || rec.pid !== process.pid) {
259
+ log("pid file superseded; exiting");
260
+ shuttingDown = true;
261
+ clearInterval(interval);
262
+ try {
263
+ ws?.close();
264
+ }
265
+ catch {
266
+ /* ignore */
267
+ }
268
+ process.exit(0);
269
+ }
270
+ // Idle machine (no live sessions for a while) → exit; hooks restart us.
271
+ if (idleSince && Date.now() - idleSince > IDLE_EXIT_MS) {
272
+ log("no live sessions; idle exit");
273
+ shuttingDown = true;
274
+ clearInterval(interval);
275
+ try {
276
+ rmSync(pidFilePath(coordRoot), { force: true });
277
+ ws?.close();
278
+ }
279
+ catch {
280
+ /* ignore */
281
+ }
282
+ process.exit(0);
283
+ }
284
+ }, KEEPALIVE_MS);
285
+ // Never resolves in normal operation; exits happen above.
286
+ return await new Promise(() => { });
287
+ }
288
+ /** Daemon status for `presence peers`/doctor surfaces. */
289
+ export function relayDaemonStatus(coordRoot) {
290
+ const rec = readPidRecord(coordRoot);
291
+ if (rec && pidAlive(rec.pid)) {
292
+ return { running: true, pid: rec.pid, url: rec.url, started_at: rec.started_at };
293
+ }
294
+ return { running: false };
295
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Relay protocol for cross-machine presence (ADR 0016, phase 2): the shared,
3
+ * host-agnostic logic both relay hosts (the Cloudflare Durable Objects worker
4
+ * and `harn relay serve`) and the client transport build on.
5
+ *
6
+ * Capability-based rooms: the room id and the E2E symmetric key are BOTH
7
+ * derived client-side via HKDF-SHA256 from (repo root-commit SHA, normalized
8
+ * origin URL, a rotatable salt). Anyone who can read the repo can derive them;
9
+ * nobody else can — including the relay operator, who sees only an opaque
10
+ * room id, opaque sender ids, and AES-GCM ciphertext. Rotating the salt
11
+ * rotates the room.
12
+ *
13
+ * The committed salt is NOT a secret and must never be treated as one: the
14
+ * derivation needs the root-commit SHA + origin URL too, which only
15
+ * repo-readers have. Repo access is the trust boundary (same as the git-refs
16
+ * transport).
17
+ *
18
+ * Wire format (JSON text frames over WebSocket):
19
+ * client → relay: { t: "pub", sender, iv, ct } broadcast + cache
20
+ * relay → client: { t: "pub", sender, iv, ct } live or warm-join replay
21
+ * relay → client: { t: "hello", peers } on join (peer count)
22
+ * The relay never sees plaintext: `ct` is AES-GCM over the presence-blob JSON,
23
+ * `sender` is an HMAC-derived opaque id, `iv` is the per-message nonce.
24
+ *
25
+ * Uses WebCrypto only (globalThis.crypto.subtle) so the same module runs on
26
+ * Bun, Node ≥ 20, and Cloudflare Workers.
27
+ */
28
+ /** Default derivation salt when a repo hasn't minted `.harnery/presence-salt`.
29
+ * Public by design (see module docs); committing a random per-repo salt just
30
+ * rotates the room away from anything derived before. */
31
+ export declare const DEFAULT_ROOM_SALT = "harnery-presence-salt/v1";
32
+ export interface RoomInputs {
33
+ /** SHA of the repo's root commit (`git rev-list --max-parents=0 HEAD`,
34
+ * first line). Stable for the repo's whole life. */
35
+ rootCommitSha: string;
36
+ /** The origin remote URL, any format — normalized internally so ssh and
37
+ * https clones of the same repo land in the same room. */
38
+ originUrl: string;
39
+ /** Rotatable room salt (default DEFAULT_ROOM_SALT). */
40
+ salt?: string;
41
+ }
42
+ export interface RoomCredentials {
43
+ /** Opaque 32-hex-char room id — the only room identity the relay sees. */
44
+ roomId: string;
45
+ /** AES-GCM-256 key for payload E2E encryption. Never leaves the client. */
46
+ key: CryptoKey;
47
+ /** Raw bytes for deriving per-machine opaque sender ids. */
48
+ senderKeyBytes: Uint8Array;
49
+ }
50
+ /**
51
+ * Normalize a git remote URL so every clone shape of the same repo derives
52
+ * the same room: `git@github.com:Org/Repo.git`, `ssh://git@github.com/org/repo`,
53
+ * and `https://github.com/org/repo.git` all → `github.com/org/repo`.
54
+ */
55
+ export declare function normalizeOriginUrl(raw: string): string;
56
+ /** Derive the room id, E2E key, and sender-id key from the repo identity. */
57
+ export declare function deriveRoomCredentials(inputs: RoomInputs): Promise<RoomCredentials>;
58
+ /** Opaque, stable per-machine sender id: HMAC(senderKey, machineLabel). The
59
+ * relay caches last-message-per-sender by this without learning the label. */
60
+ export declare function computeSenderId(creds: RoomCredentials, machineLabel: string): Promise<string>;
61
+ /** AES-GCM-encrypt a plaintext payload → { iv, ct } base64 pair. */
62
+ export declare function encryptPayload(creds: RoomCredentials, plaintext: string): Promise<{
63
+ iv: string;
64
+ ct: string;
65
+ }>;
66
+ /** Decrypt an { iv, ct } pair. Returns null on any failure (wrong room key,
67
+ * tampered frame) — callers drop the frame silently. */
68
+ export declare function decryptPayload(creds: RoomCredentials, frame: {
69
+ iv: string;
70
+ ct: string;
71
+ }): Promise<string | null>;
72
+ /** Client → relay and relay → client publication frame. */
73
+ export interface PubFrame {
74
+ t: "pub";
75
+ /** Opaque sender id (computeSenderId). The relay keys its warm-join cache on this. */
76
+ sender: string;
77
+ iv: string;
78
+ ct: string;
79
+ }
80
+ /** Relay → client greeting on join. */
81
+ export interface HelloFrame {
82
+ t: "hello";
83
+ /** Live sockets in the room at join time (including the joiner). */
84
+ peers: number;
85
+ }
86
+ export type RelayFrame = PubFrame | HelloFrame;
87
+ /** Hard cap a relay enforces per text frame (ciphertext of a ~2KB blob is far
88
+ * smaller; anything bigger is abuse or a bug). */
89
+ export declare const MAX_FRAME_BYTES: number;
90
+ /** Parse + shape-validate an incoming relay frame. Null on anything off. */
91
+ export declare function parseRelayFrame(raw: string): RelayFrame | null;
92
+ /** Room ids are 32 lowercase hex chars; relays reject anything else. */
93
+ export declare function isValidRoomId(roomId: string): boolean;
94
+ //# sourceMappingURL=relay-protocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relay-protocol.d.ts","sourceRoot":"","sources":["../../../src/core/presence/relay-protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAOH;;yDAEyD;AACzD,eAAO,MAAM,iBAAiB,6BAA6B,CAAC;AAE5D,MAAM,WAAW,UAAU;IACzB;wDACoD;IACpD,aAAa,EAAE,MAAM,CAAC;IACtB;8DAC0D;IAC1D,SAAS,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,GAAG,EAAE,SAAS,CAAC;IACf,4DAA4D;IAC5D,cAAc,EAAE,UAAU,CAAC;CAC5B;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAiBtD;AAyBD,6EAA6E;AAC7E,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,eAAe,CAAC,CAgBxF;AAED;8EAC8E;AAC9E,wBAAsB,eAAe,CACnC,KAAK,EAAE,eAAe,EACtB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,MAAM,CAAC,CAUjB;AAeD,oEAAoE;AACpE,wBAAsB,cAAc,CAClC,KAAK,EAAE,eAAe,EACtB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC,CAQrC;AAED;wDACwD;AACxD,wBAAsB,cAAc,CAClC,KAAK,EAAE,eAAe,EACtB,KAAK,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAChC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAWxB;AAID,2DAA2D;AAC3D,MAAM,WAAW,QAAQ;IACvB,CAAC,EAAE,KAAK,CAAC;IACT,sFAAsF;IACtF,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,uCAAuC;AACvC,MAAM,WAAW,UAAU;IACzB,CAAC,EAAE,OAAO,CAAC;IACX,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,UAAU,CAAC;AAE/C;kDACkD;AAClD,eAAO,MAAM,eAAe,QAAY,CAAC;AAEzC,4EAA4E;AAC5E,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CA6B9D;AAED,wEAAwE;AACxE,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAErD"}
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Relay protocol for cross-machine presence (ADR 0016, phase 2): the shared,
3
+ * host-agnostic logic both relay hosts (the Cloudflare Durable Objects worker
4
+ * and `harn relay serve`) and the client transport build on.
5
+ *
6
+ * Capability-based rooms: the room id and the E2E symmetric key are BOTH
7
+ * derived client-side via HKDF-SHA256 from (repo root-commit SHA, normalized
8
+ * origin URL, a rotatable salt). Anyone who can read the repo can derive them;
9
+ * nobody else can — including the relay operator, who sees only an opaque
10
+ * room id, opaque sender ids, and AES-GCM ciphertext. Rotating the salt
11
+ * rotates the room.
12
+ *
13
+ * The committed salt is NOT a secret and must never be treated as one: the
14
+ * derivation needs the root-commit SHA + origin URL too, which only
15
+ * repo-readers have. Repo access is the trust boundary (same as the git-refs
16
+ * transport).
17
+ *
18
+ * Wire format (JSON text frames over WebSocket):
19
+ * client → relay: { t: "pub", sender, iv, ct } broadcast + cache
20
+ * relay → client: { t: "pub", sender, iv, ct } live or warm-join replay
21
+ * relay → client: { t: "hello", peers } on join (peer count)
22
+ * The relay never sees plaintext: `ct` is AES-GCM over the presence-blob JSON,
23
+ * `sender` is an HMAC-derived opaque id, `iv` is the per-message nonce.
24
+ *
25
+ * Uses WebCrypto only (globalThis.crypto.subtle) so the same module runs on
26
+ * Bun, Node ≥ 20, and Cloudflare Workers.
27
+ */
28
+ const ROOM_INFO = "harnery-presence/room-id/v1";
29
+ const KEY_INFO = "harnery-presence/room-key/v1";
30
+ const SENDER_INFO = "harnery-presence/sender-id/v1";
31
+ const HKDF_SALT = "harnery-presence/hkdf-salt/v1";
32
+ /** Default derivation salt when a repo hasn't minted `.harnery/presence-salt`.
33
+ * Public by design (see module docs); committing a random per-repo salt just
34
+ * rotates the room away from anything derived before. */
35
+ export const DEFAULT_ROOM_SALT = "harnery-presence-salt/v1";
36
+ /**
37
+ * Normalize a git remote URL so every clone shape of the same repo derives
38
+ * the same room: `git@github.com:Org/Repo.git`, `ssh://git@github.com/org/repo`,
39
+ * and `https://github.com/org/repo.git` all → `github.com/org/repo`.
40
+ */
41
+ export function normalizeOriginUrl(raw) {
42
+ let s = raw.trim();
43
+ // scp-like syntax: [user@]host:path
44
+ const scp = /^(?:[^@/]+@)?([^:/]+):(?!\/\/)(.+)$/.exec(s);
45
+ if (scp) {
46
+ s = `${scp[1]}/${scp[2]}`;
47
+ }
48
+ else {
49
+ // URL syntax: strip scheme, then credentials.
50
+ s = s.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "");
51
+ s = s.replace(/^[^@/]+@/, "");
52
+ }
53
+ // Drop an explicit port, trailing slashes, trailing .git; lowercase.
54
+ // (Slashes first so `…/repo.git/` still sheds its .git suffix.)
55
+ s = s.replace(/^([^/:]+):\d+\//, "$1/");
56
+ s = s.replace(/\/+$/, "");
57
+ s = s.replace(/\.git$/i, "");
58
+ return s.toLowerCase();
59
+ }
60
+ async function hkdfBits(ikm, info, bits) {
61
+ const key = await crypto.subtle.importKey("raw", ikm, "HKDF", false, [
62
+ "deriveBits",
63
+ ]);
64
+ return crypto.subtle.deriveBits({
65
+ name: "HKDF",
66
+ hash: "SHA-256",
67
+ salt: new TextEncoder().encode(HKDF_SALT),
68
+ info: new TextEncoder().encode(info),
69
+ }, key, bits);
70
+ }
71
+ function hex(buf) {
72
+ const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
73
+ return Array.from(bytes)
74
+ .map((b) => b.toString(16).padStart(2, "0"))
75
+ .join("");
76
+ }
77
+ /** Derive the room id, E2E key, and sender-id key from the repo identity. */
78
+ export async function deriveRoomCredentials(inputs) {
79
+ const ikm = new TextEncoder().encode([
80
+ inputs.rootCommitSha.trim().toLowerCase(),
81
+ normalizeOriginUrl(inputs.originUrl),
82
+ inputs.salt ?? DEFAULT_ROOM_SALT,
83
+ ].join("\n"));
84
+ const roomBits = await hkdfBits(ikm, ROOM_INFO, 128);
85
+ const keyBits = await hkdfBits(ikm, KEY_INFO, 256);
86
+ const senderBits = await hkdfBits(ikm, SENDER_INFO, 256);
87
+ const key = await crypto.subtle.importKey("raw", keyBits, { name: "AES-GCM" }, false, [
88
+ "encrypt",
89
+ "decrypt",
90
+ ]);
91
+ return { roomId: hex(roomBits), key, senderKeyBytes: new Uint8Array(senderBits) };
92
+ }
93
+ /** Opaque, stable per-machine sender id: HMAC(senderKey, machineLabel). The
94
+ * relay caches last-message-per-sender by this without learning the label. */
95
+ export async function computeSenderId(creds, machineLabel) {
96
+ const hmacKey = await crypto.subtle.importKey("raw", creds.senderKeyBytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
97
+ const sig = await crypto.subtle.sign("HMAC", hmacKey, new TextEncoder().encode(machineLabel));
98
+ return hex(sig).slice(0, 32);
99
+ }
100
+ function toB64(bytes) {
101
+ let bin = "";
102
+ for (const b of bytes)
103
+ bin += String.fromCharCode(b);
104
+ return btoa(bin);
105
+ }
106
+ function fromB64(s) {
107
+ const bin = atob(s);
108
+ const out = new Uint8Array(bin.length);
109
+ for (let i = 0; i < bin.length; i++)
110
+ out[i] = bin.charCodeAt(i);
111
+ return out;
112
+ }
113
+ /** AES-GCM-encrypt a plaintext payload → { iv, ct } base64 pair. */
114
+ export async function encryptPayload(creds, plaintext) {
115
+ const iv = crypto.getRandomValues(new Uint8Array(12));
116
+ const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv: iv }, creds.key, new TextEncoder().encode(plaintext));
117
+ return { iv: toB64(iv), ct: toB64(new Uint8Array(ct)) };
118
+ }
119
+ /** Decrypt an { iv, ct } pair. Returns null on any failure (wrong room key,
120
+ * tampered frame) — callers drop the frame silently. */
121
+ export async function decryptPayload(creds, frame) {
122
+ try {
123
+ const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv: fromB64(frame.iv) }, creds.key, fromB64(frame.ct));
124
+ return new TextDecoder().decode(pt);
125
+ }
126
+ catch {
127
+ return null;
128
+ }
129
+ }
130
+ /** Hard cap a relay enforces per text frame (ciphertext of a ~2KB blob is far
131
+ * smaller; anything bigger is abuse or a bug). */
132
+ export const MAX_FRAME_BYTES = 16 * 1024;
133
+ /** Parse + shape-validate an incoming relay frame. Null on anything off. */
134
+ export function parseRelayFrame(raw) {
135
+ if (raw.length > MAX_FRAME_BYTES)
136
+ return null;
137
+ let j;
138
+ try {
139
+ j = JSON.parse(raw);
140
+ }
141
+ catch {
142
+ return null;
143
+ }
144
+ if (!j || typeof j !== "object")
145
+ return null;
146
+ const f = j;
147
+ if (f.t === "pub") {
148
+ if (typeof f.sender !== "string" ||
149
+ f.sender.length === 0 ||
150
+ f.sender.length > 64 ||
151
+ typeof f.iv !== "string" ||
152
+ typeof f.ct !== "string" ||
153
+ f.iv.length > 64 ||
154
+ f.ct.length > MAX_FRAME_BYTES) {
155
+ return null;
156
+ }
157
+ return { t: "pub", sender: f.sender, iv: f.iv, ct: f.ct };
158
+ }
159
+ if (f.t === "hello") {
160
+ if (typeof f.peers !== "number")
161
+ return null;
162
+ return { t: "hello", peers: f.peers };
163
+ }
164
+ return null;
165
+ }
166
+ /** Room ids are 32 lowercase hex chars; relays reject anything else. */
167
+ export function isValidRoomId(roomId) {
168
+ return /^[0-9a-f]{32}$/.test(roomId);
169
+ }
@@ -20,7 +20,7 @@ import type { HarnessName } from "./types.js";
20
20
  /** The env var each harness CLI reads as an API key (preferred over a stored
21
21
  * login when set). Shared with the child-env builder's subscription-only
22
22
  * scrub. */
23
- export declare const API_KEY_VARS: Record<HarnessName, string>;
23
+ export declare const API_KEY_VARS: Record<string, string>;
24
24
  export type LoginState = "present" | "absent" | "unknown";
25
25
  export type BillingMode =
26
26
  /** No API key exported; the child rides the stored (subscription) login. */
@@ -1 +1 @@
1
- {"version":3,"file":"billing.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/billing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;YAEY;AACZ,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAIpD,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE1D,MAAM,MAAM,WAAW;AACrB,4EAA4E;AAC1E,cAAc;AAChB,6EAA6E;GAC3E,SAAS;AACX;yEACyE;GACvE,kBAAkB,CAAC;AAEvB,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,WAAW,CAAC;IACrB;gEAC4D;IAC5D,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,aAAa,EAAE,OAAO,CAAC;IACvB,KAAK,EAAE,UAAU,CAAC;IAClB,IAAI,EAAE,WAAW,CAAC;CACnB;AAED,MAAM,WAAW,OAAO;IACtB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,WAAW,KAAK,YAAY,CAAC;AAEnE,wBAAgB,YAAY,CAAC,OAAO,EAAE,WAAW,EAAE,EAAE,GAAE,OAAY,GAAG,YAAY,CAyCjF"}
1
+ {"version":3,"file":"billing.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/billing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;YAEY;AACZ,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAI/C,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE1D,MAAM,MAAM,WAAW;AACrB,4EAA4E;AAC1E,cAAc;AAChB,6EAA6E;GAC3E,SAAS;AACX;yEACyE;GACvE,kBAAkB,CAAC;AAEvB,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,WAAW,CAAC;IACrB;gEAC4D;IAC5D,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,aAAa,EAAE,OAAO,CAAC;IACvB,KAAK,EAAE,UAAU,CAAC;IAClB,IAAI,EAAE,WAAW,CAAC;CACnB;AAED,MAAM,WAAW,OAAO;IACtB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,WAAW,KAAK,YAAY,CAAC;AAEnE,wBAAgB,YAAY,CAAC,OAAO,EAAE,WAAW,EAAE,EAAE,GAAE,OAAY,GAAG,YAAY,CA+CjF"}
@@ -31,7 +31,7 @@ export function probeBilling(harness, io = {}) {
31
31
  const env = io.env ?? process.env;
32
32
  const home = io.home ?? homedir();
33
33
  const keyVar = API_KEY_VARS[harness];
34
- const envKey = Boolean(env[keyVar]?.trim());
34
+ const envKey = keyVar ? Boolean(env[keyVar]?.trim()) : false;
35
35
  let login;
36
36
  let apiKeyPresent = envKey;
37
37
  let apiKeySource = envKey ? keyVar : null;
@@ -56,6 +56,12 @@ export function probeBilling(harness, io = {}) {
56
56
  // presence or absence we can't prove.
57
57
  login = "unknown";
58
58
  break;
59
+ default:
60
+ // External adapters own their authentication. The generic engine must
61
+ // not guess credential locations or fail a plugin on a built-in-only
62
+ // heuristic.
63
+ login = "unknown";
64
+ break;
59
65
  }
60
66
  const mode = apiKeyPresent && login === "present"
61
67
  ? "api-key-override"
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAOH,OAAO,KAAK,EAEV,UAAU,EAEV,SAAS,EAIV,MAAM,YAAY,CAAC;AASpB,wBAAsB,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAwR1F"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAOH,OAAO,KAAK,EAEV,UAAU,EAEV,SAAS,EAIV,MAAM,YAAY,CAAC;AASpB,wBAAsB,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAgS1F"}
@@ -104,7 +104,7 @@ export async function runWorkflow(scriptPath, opts) {
104
104
  const id = `a${++agentSeq}`;
105
105
  const label = agentOpts.label ?? `${prompt.slice(0, 60).replace(/\s+/g, " ")}…`;
106
106
  const maxAttempts = agentOpts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
107
- // Call identity for resume: same stage + harness + model + turns + schema
107
+ // Call identity for resume: same stage + harness + model + effort + turns + schema
108
108
  // + ORIGINAL prompt → same key. Retry-mutated prompts never enter the key.
109
109
  const key = agentCallKey(currentStage, harness, agentOpts, prompt);
110
110
  const cached = resumeCache.get(key);
@@ -159,7 +159,14 @@ export async function runWorkflow(scriptPath, opts) {
159
159
  agentsSpawned++;
160
160
  await acquire();
161
161
  try {
162
- journal("agent.start", { id, label, key, harness, model: agentOpts.model ?? null });
162
+ journal("agent.start", {
163
+ id,
164
+ label,
165
+ key,
166
+ harness,
167
+ model: agentOpts.model ?? null,
168
+ effort: agentOpts.effort ?? null,
169
+ });
163
170
  log(`[${name}] ${currentStage || "(no stage)"} → ${id} [${harness}] ${label}`);
164
171
  let attemptPrompt = prompt;
165
172
  let last = null;
@@ -167,6 +174,7 @@ export async function runWorkflow(scriptPath, opts) {
167
174
  last = await spawner({
168
175
  prompt: attemptPrompt,
169
176
  model: agentOpts.model,
177
+ effort: agentOpts.effort,
170
178
  timeoutMs: agentOpts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
171
179
  maxTurns: agentOpts.maxTurns ?? DEFAULT_MAX_TURNS,
172
180
  cwd,
@@ -287,6 +295,7 @@ function agentCallKey(stage, harness, agentOpts, prompt) {
287
295
  stage,
288
296
  harness,
289
297
  agentOpts.model ?? null,
298
+ agentOpts.effort ?? null,
290
299
  agentOpts.maxTurns ?? DEFAULT_MAX_TURNS,
291
300
  agentOpts.schema ?? null,
292
301
  prompt,
@@ -7,11 +7,11 @@
7
7
  * but they do drift — keep this module the single place they live.
8
8
  */
9
9
  import type { HarnessName } from "./types.js";
10
- export declare const HARNESS_BINARIES: Record<HarnessName, string>;
11
- export declare const HARNESS_INSTALL_HINTS: Record<HarnessName, string>;
10
+ export declare const HARNESS_BINARIES: Record<string, string>;
11
+ export declare const HARNESS_INSTALL_HINTS: Record<string, string>;
12
12
  /** How to authenticate each CLI with a subscription login (the billing
13
13
  * default — see billing.ts). */
14
- export declare const HARNESS_LOGIN_HINTS: Record<HarnessName, string>;
14
+ export declare const HARNESS_LOGIN_HINTS: Record<string, string>;
15
15
  /** One-line "it's missing, here's the fix" string for spawn adapters. */
16
16
  export declare function notFoundError(harness: HarnessName): string;
17
17
  //# sourceMappingURL=harnesses.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"harnesses.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/harnesses.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAIxD,CAAC;AAEF,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAI7D,CAAC;AAEF;gCACgC;AAChC,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAI3D,CAAC;AAEF,yEAAyE;AACzE,wBAAgB,aAAa,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAK1D"}
1
+ {"version":3,"file":"harnesses.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/harnesses.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAEnD,CAAC;AAEF,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAExD,CAAC;AAEF;gCACgC;AAChC,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAEtD,CAAC;AAEF,yEAAyE;AACzE,wBAAgB,aAAa,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAK1D"}