harnery 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/README.md +2 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commander.js +8 -0
  4. package/dist/commands/agents.d.ts +24 -0
  5. package/dist/commands/agents.d.ts.map +1 -1
  6. package/dist/commands/agents.js +244 -13
  7. package/dist/commands/backup.d.ts +5 -4
  8. package/dist/commands/backup.d.ts.map +1 -1
  9. package/dist/commands/backup.js +15 -14
  10. package/dist/commands/browse.d.ts.map +1 -1
  11. package/dist/commands/browse.js +27 -1
  12. package/dist/commands/claude-desktop.d.ts +19 -0
  13. package/dist/commands/claude-desktop.d.ts.map +1 -0
  14. package/dist/commands/claude-desktop.js +168 -0
  15. package/dist/commands/context.d.ts.map +1 -1
  16. package/dist/commands/context.js +149 -1
  17. package/dist/commands/doctor.d.ts.map +1 -1
  18. package/dist/commands/doctor.js +37 -0
  19. package/dist/commands/eml.d.ts +32 -0
  20. package/dist/commands/eml.d.ts.map +1 -1
  21. package/dist/commands/eml.js +16 -3
  22. package/dist/commands/grep.d.ts +35 -2
  23. package/dist/commands/grep.d.ts.map +1 -1
  24. package/dist/commands/grep.js +427 -139
  25. package/dist/commands/harness.d.ts +11 -0
  26. package/dist/commands/harness.d.ts.map +1 -0
  27. package/dist/commands/harness.js +118 -0
  28. package/dist/commands/init.d.ts +15 -5
  29. package/dist/commands/init.d.ts.map +1 -1
  30. package/dist/commands/init.js +125 -14
  31. package/dist/commands/presence.d.ts +9 -4
  32. package/dist/commands/presence.d.ts.map +1 -1
  33. package/dist/commands/presence.js +88 -5
  34. package/dist/commands/relay.d.ts +9 -0
  35. package/dist/commands/relay.d.ts.map +1 -0
  36. package/dist/commands/relay.js +143 -0
  37. package/dist/commands/sync.d.ts.map +1 -1
  38. package/dist/commands/sync.js +5 -0
  39. package/dist/commands/workflow.d.ts +4 -0
  40. package/dist/commands/workflow.d.ts.map +1 -0
  41. package/dist/commands/workflow.js +82 -0
  42. package/dist/core/agents/canonical-emit.d.ts +15 -0
  43. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  44. package/dist/core/agents/canonical-emit.js +41 -3
  45. package/dist/core/agents/cli.js +14 -5
  46. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  47. package/dist/core/agents/render/prompt-context.js +44 -3
  48. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  49. package/dist/core/agents/render/session-context.js +26 -1
  50. package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
  51. package/dist/core/agents/rules/claim-conflict.js +26 -1
  52. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
  53. package/dist/core/agents/rules/commit-conflict.js +4 -37
  54. package/dist/core/agents/rules/stop-hook.d.ts +8 -0
  55. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  56. package/dist/core/agents/rules/stop-hook.js +8 -0
  57. package/dist/core/agents/session-events.d.ts.map +1 -1
  58. package/dist/core/agents/session-events.js +16 -43
  59. package/dist/core/agents/state/heartbeat-projector.d.ts +2 -0
  60. package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
  61. package/dist/core/agents/state/heartbeat-projector.js +24 -4
  62. package/dist/core/agents/state/heartbeat-writer.d.ts +16 -2
  63. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  64. package/dist/core/agents/state/heartbeat-writer.js +24 -7
  65. package/dist/core/agents/state/names.d.ts +38 -3
  66. package/dist/core/agents/state/names.d.ts.map +1 -1
  67. package/dist/core/agents/state/names.js +46 -7
  68. package/dist/core/agents/state/pidmap.d.ts +5 -1
  69. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  70. package/dist/core/agents/state/pidmap.js +58 -2
  71. package/dist/core/agents/state/stale-sweep.d.ts +3 -2
  72. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  73. package/dist/core/agents/state/stale-sweep.js +6 -6
  74. package/dist/core/config.d.ts +77 -11
  75. package/dist/core/config.d.ts.map +1 -1
  76. package/dist/core/config.js +210 -25
  77. package/dist/core/context/index.d.ts +144 -0
  78. package/dist/core/context/index.d.ts.map +1 -0
  79. package/dist/core/context/index.js +380 -0
  80. package/dist/core/harnesses/bench.d.ts +29 -0
  81. package/dist/core/harnesses/bench.d.ts.map +1 -0
  82. package/dist/core/harnesses/bench.js +151 -0
  83. package/dist/core/harnesses/index.d.ts +9 -0
  84. package/dist/core/harnesses/index.d.ts.map +1 -0
  85. package/dist/core/harnesses/index.js +4 -0
  86. package/dist/core/harnesses/profiles.d.ts +62 -0
  87. package/dist/core/harnesses/profiles.d.ts.map +1 -0
  88. package/dist/core/harnesses/profiles.js +115 -0
  89. package/dist/core/harnesses/registry.d.ts +15 -0
  90. package/dist/core/harnesses/registry.d.ts.map +1 -0
  91. package/dist/core/harnesses/registry.js +113 -0
  92. package/dist/core/harnesses/types.d.ts +64 -0
  93. package/dist/core/harnesses/types.d.ts.map +1 -0
  94. package/dist/core/harnesses/types.js +20 -0
  95. package/dist/core/hooks/cli.js +309 -66
  96. package/dist/core/hooks/events/schema.d.ts +42 -1
  97. package/dist/core/hooks/events/schema.d.ts.map +1 -1
  98. package/dist/core/hooks/harness/events.d.ts +7 -0
  99. package/dist/core/hooks/harness/events.d.ts.map +1 -1
  100. package/dist/core/hooks/harness/events.js +4 -0
  101. package/dist/core/hooks/harness/parse.d.ts +1 -1
  102. package/dist/core/hooks/harness/parse.d.ts.map +1 -1
  103. package/dist/core/hooks/harness/parse.js +4 -0
  104. package/dist/core/hooks/resolve/coord-root.d.ts +11 -0
  105. package/dist/core/hooks/resolve/coord-root.d.ts.map +1 -1
  106. package/dist/core/hooks/resolve/coord-root.js +28 -6
  107. package/dist/core/presence/blob.d.ts +40 -0
  108. package/dist/core/presence/blob.d.ts.map +1 -0
  109. package/dist/core/presence/blob.js +91 -0
  110. package/dist/core/presence/git.d.ts +68 -0
  111. package/dist/core/presence/git.d.ts.map +1 -0
  112. package/dist/core/presence/git.js +173 -0
  113. package/dist/core/presence/index.d.ts +74 -0
  114. package/dist/core/presence/index.d.ts.map +1 -0
  115. package/dist/core/presence/index.js +224 -0
  116. package/dist/core/presence/relay-client.d.ts +39 -0
  117. package/dist/core/presence/relay-client.d.ts.map +1 -0
  118. package/dist/core/presence/relay-client.js +295 -0
  119. package/dist/core/presence/relay-protocol.d.ts +94 -0
  120. package/dist/core/presence/relay-protocol.d.ts.map +1 -0
  121. package/dist/core/presence/relay-protocol.js +169 -0
  122. package/dist/core/workflow/billing.d.ts +48 -0
  123. package/dist/core/workflow/billing.d.ts.map +1 -0
  124. package/dist/core/workflow/billing.js +108 -0
  125. package/dist/core/workflow/child-env.d.ts +30 -0
  126. package/dist/core/workflow/child-env.d.ts.map +1 -0
  127. package/dist/core/workflow/child-env.js +43 -0
  128. package/dist/core/workflow/engine.d.ts +21 -0
  129. package/dist/core/workflow/engine.d.ts.map +1 -0
  130. package/dist/core/workflow/engine.js +349 -0
  131. package/dist/core/workflow/harnesses.d.ts +17 -0
  132. package/dist/core/workflow/harnesses.d.ts.map +1 -0
  133. package/dist/core/workflow/harnesses.js +21 -0
  134. package/dist/core/workflow/spawn-claude.d.ts +25 -0
  135. package/dist/core/workflow/spawn-claude.d.ts.map +1 -0
  136. package/dist/core/workflow/spawn-claude.js +103 -0
  137. package/dist/core/workflow/spawn-codex.d.ts +22 -0
  138. package/dist/core/workflow/spawn-codex.d.ts.map +1 -0
  139. package/dist/core/workflow/spawn-codex.js +88 -0
  140. package/dist/core/workflow/spawn-cursor.d.ts +29 -0
  141. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -0
  142. package/dist/core/workflow/spawn-cursor.js +91 -0
  143. package/dist/core/workflow/types.d.ts +152 -0
  144. package/dist/core/workflow/types.d.ts.map +1 -0
  145. package/dist/core/workflow/types.js +9 -0
  146. package/dist/core/workflow/validate.d.ts +15 -0
  147. package/dist/core/workflow/validate.d.ts.map +1 -0
  148. package/dist/core/workflow/validate.js +70 -0
  149. package/dist/lib/browser/client.d.ts +16 -0
  150. package/dist/lib/browser/client.d.ts.map +1 -1
  151. package/dist/lib/browser/client.js +22 -0
  152. package/dist/lib/browser/index.d.ts +1 -0
  153. package/dist/lib/browser/index.d.ts.map +1 -1
  154. package/dist/lib/browser/index.js +1 -0
  155. package/dist/lib/browser/launch-args.d.ts +21 -0
  156. package/dist/lib/browser/launch-args.d.ts.map +1 -0
  157. package/dist/lib/browser/launch-args.js +32 -0
  158. package/dist/lib/claude-desktop.d.ts +105 -0
  159. package/dist/lib/claude-desktop.d.ts.map +1 -0
  160. package/dist/lib/claude-desktop.js +217 -0
  161. package/dist/lib/docs-lint.d.ts.map +1 -1
  162. package/dist/lib/docs-lint.js +5 -2
  163. package/dist/lib/identities/assume.d.ts +51 -0
  164. package/dist/lib/identities/assume.d.ts.map +1 -0
  165. package/dist/lib/identities/assume.js +274 -0
  166. package/dist/lib/identities/index.d.ts +7 -7
  167. package/dist/lib/identities/index.d.ts.map +1 -1
  168. package/dist/lib/identities/index.js +24 -24
  169. package/dist/lib/instructions/templates.d.ts.map +1 -1
  170. package/dist/lib/instructions/templates.js +15 -2
  171. package/package.json +12 -1
  172. package/schemas/config.schema.json +88 -19
  173. package/src/commander.ts +8 -0
  174. package/src/commands/agents.ts +280 -14
  175. package/src/commands/backup.ts +17 -16
  176. package/src/commands/browse.ts +32 -0
  177. package/src/commands/claude-desktop.ts +215 -0
  178. package/src/commands/context.ts +174 -1
  179. package/src/commands/doctor.ts +44 -0
  180. package/src/commands/eml.ts +22 -4
  181. package/src/commands/grep.ts +535 -142
  182. package/src/commands/harness.ts +147 -0
  183. package/src/commands/init.ts +138 -17
  184. package/src/commands/presence.ts +111 -5
  185. package/src/commands/relay.ts +160 -0
  186. package/src/commands/sync.ts +4 -0
  187. package/src/commands/workflow.ts +123 -0
  188. package/src/core/agents/canonical-emit.ts +41 -3
  189. package/src/core/agents/cli.ts +15 -6
  190. package/src/core/agents/render/prompt-context.ts +48 -3
  191. package/src/core/agents/render/session-context.ts +27 -1
  192. package/src/core/agents/rules/claim-conflict.ts +26 -1
  193. package/src/core/agents/rules/commit-conflict.ts +4 -33
  194. package/src/core/agents/rules/stop-hook.ts +17 -0
  195. package/src/core/agents/session-events.ts +15 -39
  196. package/src/core/agents/state/heartbeat-projector.ts +23 -2
  197. package/src/core/agents/state/heartbeat-writer.ts +33 -7
  198. package/src/core/agents/state/names.ts +68 -9
  199. package/src/core/agents/state/pidmap.ts +59 -2
  200. package/src/core/agents/state/stale-sweep.ts +6 -11
  201. package/src/core/config.ts +271 -24
  202. package/src/core/context/index.ts +575 -0
  203. package/src/core/harnesses/bench.ts +214 -0
  204. package/src/core/harnesses/index.ts +31 -0
  205. package/src/core/harnesses/profiles.ts +141 -0
  206. package/src/core/harnesses/registry.ts +140 -0
  207. package/src/core/harnesses/types.ts +90 -0
  208. package/src/core/hooks/cli.ts +361 -65
  209. package/src/core/hooks/events/schema.ts +72 -0
  210. package/src/core/hooks/harness/events.ts +11 -0
  211. package/src/core/hooks/harness/parse.ts +7 -1
  212. package/src/core/hooks/resolve/coord-root.ts +28 -6
  213. package/src/core/presence/blob.ts +125 -0
  214. package/src/core/presence/git.ts +191 -0
  215. package/src/core/presence/index.ts +274 -0
  216. package/src/core/presence/relay-client.ts +322 -0
  217. package/src/core/presence/relay-protocol.ts +246 -0
  218. package/src/core/workflow/billing.ts +152 -0
  219. package/src/core/workflow/child-env.ts +47 -0
  220. package/src/core/workflow/engine.ts +403 -0
  221. package/src/core/workflow/harnesses.ts +33 -0
  222. package/src/core/workflow/spawn-claude.ts +118 -0
  223. package/src/core/workflow/spawn-codex.ts +93 -0
  224. package/src/core/workflow/spawn-cursor.ts +108 -0
  225. package/src/core/workflow/types.ts +160 -0
  226. package/src/core/workflow/validate.ts +75 -0
  227. package/src/lib/browser/client.ts +37 -0
  228. package/src/lib/browser/index.ts +1 -0
  229. package/src/lib/browser/launch-args.ts +33 -0
  230. package/src/lib/claude-desktop.ts +293 -0
  231. package/src/lib/docs-lint.ts +5 -2
  232. package/src/lib/identities/assume.ts +363 -0
  233. package/src/lib/identities/index.ts +28 -24
  234. package/src/lib/instructions/templates.ts +16 -2
@@ -0,0 +1,118 @@
1
+ /**
2
+ * claude-code spawn adapter: runs one subagent as a headless `claude -p`
3
+ * subprocess with `--output-format json` and unwraps the result envelope.
4
+ *
5
+ * Two hard-won rules from the Phase 1 spike, both load-bearing:
6
+ *
7
+ * 1. **Scrub inherited `CLAUDE*` env vars — delete, don't blank.** A workflow
8
+ * launched from inside a Claude Code session inherits session env that makes
9
+ * the nested CLI exit 1 with empty output. Setting a var to "" still reads
10
+ * as set; only deletion works.
11
+ * 2. **Mark the child as a workflow child instead of disabling hooks.** With
12
+ * the host repo's hooks active, the coordination Stop hook blocks a headless
13
+ * child for skipping the end-of-turn ritual (observed: num_turns burned on
14
+ * re-prompts → error_max_turns). `--settings '{"disableAllHooks":true}'`
15
+ * fixes that but also kills the coord capture that makes workflow children
16
+ * visible to peers — the point of running them under harnery. So the child
17
+ * gets HARNERY_WORKFLOW_CHILD=1 and the stop-hook rule exempts it
18
+ * (stop-hook.ts), keeping heartbeats + events on.
19
+ */
20
+
21
+ import { exec } from "../../lib/exec.ts";
22
+ import { validateHarnessEffort } from "../harnesses/profiles.ts";
23
+ import type { HarnessInvocation, HarnessRawResult } from "../harnesses/types.ts";
24
+ import { buildChildEnv } from "./child-env.ts";
25
+ import { notFoundError } from "./harnesses.ts";
26
+ import type { Spawner, SpawnRequest, SpawnResult } from "./types.ts";
27
+
28
+ interface ClaudeEnvelope {
29
+ type?: string;
30
+ subtype?: string;
31
+ is_error?: boolean;
32
+ result?: string;
33
+ session_id?: string;
34
+ total_cost_usd?: number;
35
+ errors?: string[];
36
+ }
37
+
38
+ export function buildClaudeInvocation(req: SpawnRequest): HarnessInvocation {
39
+ validateHarnessEffort("claude-code", req.effort);
40
+ const argv = [
41
+ "claude",
42
+ "-p",
43
+ req.prompt,
44
+ "--output-format",
45
+ "json",
46
+ "--max-turns",
47
+ String(req.maxTurns),
48
+ ];
49
+ if (req.model) argv.push("--model", req.model);
50
+ if (req.effort) argv.push("--effort", req.effort);
51
+ return { argv };
52
+ }
53
+
54
+ export function normalizeClaudeResult(raw: HarnessRawResult): SpawnResult {
55
+ if (raw.exitCode === 127) {
56
+ return {
57
+ ok: false,
58
+ text: "",
59
+ durationMs: raw.durationMs,
60
+ error: notFoundError("claude-code"),
61
+ };
62
+ }
63
+ if (raw.exitCode !== 0) {
64
+ return {
65
+ ok: false,
66
+ text: "",
67
+ durationMs: raw.durationMs,
68
+ error: `claude exited ${raw.exitCode}: ${(raw.stderr || raw.stdout).slice(0, 500)}`,
69
+ };
70
+ }
71
+
72
+ let envelope: ClaudeEnvelope;
73
+ try {
74
+ envelope = JSON.parse(raw.stdout) as ClaudeEnvelope;
75
+ } catch {
76
+ return {
77
+ ok: false,
78
+ text: "",
79
+ durationMs: raw.durationMs,
80
+ error: `result envelope was not JSON: ${raw.stdout.slice(0, 300)}`,
81
+ };
82
+ }
83
+
84
+ if (envelope.is_error) {
85
+ return {
86
+ ok: false,
87
+ text: String(envelope.result ?? ""),
88
+ sessionId: envelope.session_id,
89
+ costUsd: envelope.total_cost_usd,
90
+ durationMs: raw.durationMs,
91
+ error: `harness error (${envelope.subtype ?? "unknown"}): ${(envelope.errors ?? []).join("; ") || "see envelope"}`,
92
+ };
93
+ }
94
+
95
+ return {
96
+ ok: true,
97
+ text: String(envelope.result ?? ""),
98
+ sessionId: envelope.session_id,
99
+ costUsd: envelope.total_cost_usd,
100
+ durationMs: raw.durationMs,
101
+ };
102
+ }
103
+
104
+ export const claudeCodeSpawner: Spawner = async (req: SpawnRequest): Promise<SpawnResult> => {
105
+ const t0 = Date.now();
106
+ let invocation: HarnessInvocation;
107
+ try {
108
+ invocation = buildClaudeInvocation(req);
109
+ } catch (error) {
110
+ return { ok: false, text: "", durationMs: 0, error: (error as Error).message };
111
+ }
112
+ const r = await exec(invocation.argv, {
113
+ cwd: req.cwd,
114
+ env: buildChildEnv(req.runId, { subscriptionOnly: req.subscriptionOnly }),
115
+ timeout: req.timeoutMs,
116
+ });
117
+ return normalizeClaudeResult({ ...r, durationMs: Date.now() - t0 });
118
+ };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * codex spawn adapter: runs one subagent as a headless `codex exec`
3
+ * subprocess.
4
+ *
5
+ * Contract notes (LIVE-VERIFIED 2026-07-16 against codex-cli 0.144.5: flags
6
+ * present, schema-gated triage + text stages round-trip via `--harness codex`):
7
+ * - `codex exec "<prompt>"` is the non-interactive mode.
8
+ * - The final assistant message is captured via `--output-last-message <file>`
9
+ * (a temp file), which is far more drift-tolerant than parsing the
10
+ * experimental `--json` JSONL event stream.
11
+ * - `--skip-git-repo-check` keeps non-repo cwds working; `--sandbox
12
+ * workspace-write` matches workflow-stage expectations (children may edit).
13
+ * - No per-run cost or session-id surface in this mode → both left undefined.
14
+ * - No max-turns equivalent → `maxTurns` is accepted and ignored (documented
15
+ * in the CLI docs page).
16
+ */
17
+
18
+ import { randomBytes } from "node:crypto";
19
+ import { existsSync, readFileSync, rmSync } from "node:fs";
20
+ import { tmpdir } from "node:os";
21
+ import { join } from "node:path";
22
+ import { exec } from "../../lib/exec.ts";
23
+ import { validateHarnessEffort } from "../harnesses/profiles.ts";
24
+ import type { HarnessInvocation, HarnessRawResult } from "../harnesses/types.ts";
25
+ import { buildChildEnv } from "./child-env.ts";
26
+ import { notFoundError } from "./harnesses.ts";
27
+ import type { Spawner, SpawnRequest, SpawnResult } from "./types.ts";
28
+
29
+ export function buildCodexInvocation(req: SpawnRequest, resultFile?: string): HarnessInvocation {
30
+ validateHarnessEffort("codex", req.effort);
31
+ if (!resultFile) throw new Error("codex adapter requires a final-message result file");
32
+ const argv = [
33
+ "codex",
34
+ "exec",
35
+ req.prompt,
36
+ "--output-last-message",
37
+ resultFile,
38
+ "--skip-git-repo-check",
39
+ "--sandbox",
40
+ "workspace-write",
41
+ ];
42
+ if (req.model) argv.push("--model", req.model);
43
+ if (req.effort) argv.push("-c", `model_reasoning_effort=${JSON.stringify(req.effort)}`);
44
+ return { argv, resultFile };
45
+ }
46
+
47
+ export function normalizeCodexResult(raw: HarnessRawResult): SpawnResult {
48
+ if (raw.exitCode === 127) {
49
+ return { ok: false, text: "", durationMs: raw.durationMs, error: notFoundError("codex") };
50
+ }
51
+ if (raw.exitCode !== 0) {
52
+ return {
53
+ ok: false,
54
+ text: "",
55
+ durationMs: raw.durationMs,
56
+ error: `codex exited ${raw.exitCode}: ${(raw.stderr || raw.stdout).slice(0, 500)}`,
57
+ };
58
+ }
59
+ return {
60
+ ok: true,
61
+ text: (raw.resultFileText ?? raw.stdout).trim(),
62
+ durationMs: raw.durationMs,
63
+ };
64
+ }
65
+
66
+ export const codexSpawner: Spawner = async (req: SpawnRequest): Promise<SpawnResult> => {
67
+ const t0 = Date.now();
68
+ const outFile = join(
69
+ tmpdir(),
70
+ `harnery-codex-${process.pid}-${randomBytes(4).toString("hex")}.txt`,
71
+ );
72
+
73
+ try {
74
+ let invocation: HarnessInvocation;
75
+ try {
76
+ invocation = buildCodexInvocation(req, outFile);
77
+ } catch (error) {
78
+ return { ok: false, text: "", durationMs: 0, error: (error as Error).message };
79
+ }
80
+ const r = await exec(invocation.argv, {
81
+ cwd: req.cwd,
82
+ env: buildChildEnv(req.runId, { subscriptionOnly: req.subscriptionOnly }),
83
+ timeout: req.timeoutMs,
84
+ });
85
+ return normalizeCodexResult({
86
+ ...r,
87
+ durationMs: Date.now() - t0,
88
+ resultFileText: existsSync(outFile) ? readFileSync(outFile, "utf8") : undefined,
89
+ });
90
+ } finally {
91
+ rmSync(outFile, { force: true });
92
+ }
93
+ };
@@ -0,0 +1,108 @@
1
+ /**
2
+ * cursor spawn adapter: runs one subagent as a headless `cursor-agent -p`
3
+ * subprocess with `--output-format json`.
4
+ *
5
+ * Contract notes (LIVE-VERIFIED 2026-07-17 against cursor-agent
6
+ * 2026.07.16-899851b: schema-gated triage + text stages round-trip via
7
+ * `--harness cursor`, session_id parses from the envelope):
8
+ * - `cursor-agent -p "<prompt>" --output-format json` prints a single result
9
+ * envelope modeled on Claude Code's (`{type: "result", is_error, result,
10
+ * session_id, …}`).
11
+ * - `--trust` is required: headless runs refuse untrusted workspaces (exit 1,
12
+ * "Workspace Trust Required") — see the argv comment below.
13
+ * - Envelope drift guard: when stdout doesn't parse as JSON but the process
14
+ * exited 0, the raw stdout is returned as the reply text.
15
+ * - No per-run cost surface → undefined. No max-turns equivalent → `maxTurns`
16
+ * accepted and ignored (documented in the CLI docs page).
17
+ */
18
+
19
+ import { exec } from "../../lib/exec.ts";
20
+ import { validateHarnessEffort } from "../harnesses/profiles.ts";
21
+ import type { HarnessInvocation, HarnessRawResult } from "../harnesses/types.ts";
22
+ import { buildChildEnv } from "./child-env.ts";
23
+ import { notFoundError } from "./harnesses.ts";
24
+ import type { Spawner, SpawnRequest, SpawnResult } from "./types.ts";
25
+
26
+ interface CursorEnvelope {
27
+ type?: string;
28
+ is_error?: boolean;
29
+ result?: string;
30
+ session_id?: string;
31
+ }
32
+
33
+ /** Exported for unit tests (no live binary to test against). */
34
+ export function parseCursorOutput(stdout: string): {
35
+ text: string;
36
+ sessionId?: string;
37
+ isError: boolean;
38
+ } {
39
+ try {
40
+ const envelope = JSON.parse(stdout) as CursorEnvelope;
41
+ return {
42
+ text: String(envelope.result ?? ""),
43
+ sessionId: envelope.session_id,
44
+ isError: Boolean(envelope.is_error),
45
+ };
46
+ } catch {
47
+ return { text: stdout, isError: false };
48
+ }
49
+ }
50
+
51
+ export function buildCursorInvocation(req: SpawnRequest): HarnessInvocation {
52
+ validateHarnessEffort("cursor", req.effort);
53
+ // --trust: headless cursor-agent refuses untrusted workspaces (exit 1,
54
+ // "Workspace Trust Required"). Workflow children run in the engine's cwd
55
+ // deliberately and may edit files — the same posture as the codex adapter's
56
+ // `--sandbox workspace-write` — so trusting that directory is implied.
57
+ const argv = ["cursor-agent", "-p", req.prompt, "--output-format", "json", "--trust"];
58
+ if (req.model) argv.push("--model", req.model);
59
+ return { argv };
60
+ }
61
+
62
+ export function normalizeCursorResult(raw: HarnessRawResult): SpawnResult {
63
+ if (raw.exitCode === 127) {
64
+ return { ok: false, text: "", durationMs: raw.durationMs, error: notFoundError("cursor") };
65
+ }
66
+ if (raw.exitCode !== 0) {
67
+ return {
68
+ ok: false,
69
+ text: "",
70
+ durationMs: raw.durationMs,
71
+ error: `cursor-agent exited ${raw.exitCode}: ${(raw.stderr || raw.stdout).slice(0, 500)}`,
72
+ };
73
+ }
74
+
75
+ const parsed = parseCursorOutput(raw.stdout);
76
+ if (parsed.isError) {
77
+ return {
78
+ ok: false,
79
+ text: parsed.text,
80
+ sessionId: parsed.sessionId,
81
+ durationMs: raw.durationMs,
82
+ error: `cursor-agent reported is_error: ${parsed.text.slice(0, 300)}`,
83
+ };
84
+ }
85
+ return {
86
+ ok: true,
87
+ text: parsed.text,
88
+ sessionId: parsed.sessionId,
89
+ durationMs: raw.durationMs,
90
+ };
91
+ }
92
+
93
+ export const cursorSpawner: Spawner = async (req: SpawnRequest): Promise<SpawnResult> => {
94
+ const t0 = Date.now();
95
+ let invocation: HarnessInvocation;
96
+ try {
97
+ invocation = buildCursorInvocation(req);
98
+ } catch (error) {
99
+ return { ok: false, text: "", durationMs: 0, error: (error as Error).message };
100
+ }
101
+
102
+ const r = await exec(invocation.argv, {
103
+ cwd: req.cwd,
104
+ env: buildChildEnv(req.runId, { subscriptionOnly: req.subscriptionOnly }),
105
+ timeout: req.timeoutMs,
106
+ });
107
+ return normalizeCursorResult({ ...r, durationMs: Date.now() - t0 });
108
+ };
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Workflow engine contracts. A workflow is a small throwaway JS script with
3
+ * bounded, schema-gated stages that fan work out to headless harness-CLI
4
+ * subagents; the SCRIPT (deterministic code), not any model, decides routing
5
+ * between stages, and the run always terminates when the script returns.
6
+ *
7
+ * Design record: decision 0015 (portable coordination-aware workflows).
8
+ */
9
+
10
+ import type { BillingMode, BillingProber } from "./billing.ts";
11
+
12
+ /** JSON-schema *subset* accepted by stage gates (see validate.ts). */
13
+ export interface StageSchema {
14
+ type: "object" | "array" | "string" | "number" | "boolean";
15
+ /** type=object */
16
+ properties?: Record<string, StageSchema>;
17
+ required?: string[];
18
+ /** type=array */
19
+ items?: StageSchema;
20
+ /** any type: closed value set (compared with ===) */
21
+ enum?: Array<string | number | boolean>;
22
+ }
23
+
24
+ export interface AgentOpts {
25
+ /** Stage gate: when present, the agent's reply must strict-parse as JSON and
26
+ * validate; the engine retries with the validation error appended, up to
27
+ * `maxAttempts`. Without it, `agent()` resolves to the raw reply text. */
28
+ schema?: StageSchema;
29
+ /** Model slug passed through to the harness CLI (default: the CLI's default). */
30
+ model?: string;
31
+ /** Reasoning effort mapped through the selected harness profile. Unsupported
32
+ * values fail before the vendor process starts. */
33
+ effort?: string;
34
+ /** Attempt ceiling for the schema-retry loop (default 2). */
35
+ maxAttempts?: number;
36
+ /** Subprocess timeout ms (default 300_000). */
37
+ timeoutMs?: number;
38
+ /** Harness-turn ceiling for the child (default 25; use 1 for pure
39
+ * classification stages — cheaper and faster). */
40
+ maxTurns?: number;
41
+ /** Display label in the journal (default: prompt head). */
42
+ label?: string;
43
+ /** Which harness runs this agent (default: the run's default harness).
44
+ * Mixed-harness workflows are legal: triage on one CLI, deep work on
45
+ * another. */
46
+ harness?: HarnessName;
47
+ }
48
+
49
+ /** Open registry key. The built-in catalog currently contains Claude Code,
50
+ * Codex, and Cursor; consumers may register another adapter without widening
51
+ * a package-owned union first. */
52
+ export type HarnessName = string;
53
+
54
+ /** What a spawn adapter returns for one subagent run. */
55
+ export interface SpawnResult {
56
+ ok: boolean;
57
+ /** The model's final reply text (envelope-unwrapped). */
58
+ text: string;
59
+ /** Child harness session id when the envelope carries one. */
60
+ sessionId?: string;
61
+ costUsd?: number;
62
+ durationMs: number;
63
+ /** Populated when ok=false. */
64
+ error?: string;
65
+ }
66
+
67
+ export interface SpawnRequest {
68
+ prompt: string;
69
+ model?: string;
70
+ effort?: string;
71
+ timeoutMs: number;
72
+ maxTurns: number;
73
+ cwd: string;
74
+ /** Run id, stamped into the child env (HARNERY_WORKFLOW_RUN_ID) so the
75
+ * coord layer can associate child sessions with their workflow run. */
76
+ runId?: string;
77
+ /** Scrub all API-key vars from the child env so it can only authenticate
78
+ * via its stored (subscription) login. See billing.ts. */
79
+ subscriptionOnly?: boolean;
80
+ }
81
+
82
+ /** One headless-subagent runner. The engine is adapter-agnostic; claude-code
83
+ * ships first, codex/cursor land behind the same signature (plan Phase 4). */
84
+ export type Spawner = (req: SpawnRequest) => Promise<SpawnResult>;
85
+
86
+ /** The API surface injected into a workflow script's default export. Explicit
87
+ * injection (no ambient globals): keeps scripts portable and unit-testable. */
88
+ export interface WorkflowContext {
89
+ /** Spawn one subagent; resolves to validated JSON (schema) or reply text. */
90
+ agent: (prompt: string, opts?: AgentOpts) => Promise<unknown>;
91
+ /** Run thunks with bounded concurrency; a rejected thunk resolves to null. */
92
+ parallel: <T>(thunks: Array<() => Promise<T>>) => Promise<Array<T | null>>;
93
+ /** Declare the current stage (journal + progress grouping). */
94
+ stage: (title: string) => void;
95
+ /** Narrate progress (stderr + journal). */
96
+ log: (message: string) => void;
97
+ }
98
+
99
+ export interface WorkflowMeta {
100
+ name: string;
101
+ description?: string;
102
+ }
103
+
104
+ /** Loaded script shape: `export const meta` + `export default async (ctx) => …`. */
105
+ export interface WorkflowModule {
106
+ meta?: WorkflowMeta;
107
+ default: (ctx: WorkflowContext) => Promise<unknown>;
108
+ }
109
+
110
+ export interface EngineOpts {
111
+ /** Repo root whose .harnery/ receives the run journal. */
112
+ coordRoot: string;
113
+ /** Spawner registry keyed by harness. A single-harness caller registers one
114
+ * entry and names it in `defaultHarness`. */
115
+ spawners: Readonly<Record<HarnessName, Spawner | undefined>>;
116
+ /** Harness used when an agent() call doesn't name one (default "claude-code"). */
117
+ defaultHarness?: HarnessName;
118
+ /** Resume: run id of a prior run whose journal supplies cached results.
119
+ * agent() calls whose (stage, prompt, model, maxTurns, schema) key matches a
120
+ * completed prior agent return the journaled result without spawning. */
121
+ resumeFrom?: string;
122
+ /** Total-agent ceiling for the run (default 50): the runaway backstop. */
123
+ maxAgents?: number;
124
+ /** Concurrent-subagent cap for parallel() (default 4). */
125
+ concurrency?: number;
126
+ /** Working directory children spawn in (default: coordRoot). */
127
+ cwd?: string;
128
+ /** Progress sink (default: process.stderr). */
129
+ onLog?: (line: string) => void;
130
+ /** Guarantee subscription billing: API-key vars are scrubbed from every
131
+ * child env, and a harness whose stored login is provably absent fails
132
+ * loud before spawning. */
133
+ subscriptionOnly?: boolean;
134
+ /** Permit the api-key-override billing state (an exported API key silently
135
+ * shadowing a stored subscription login), which the engine otherwise
136
+ * refuses. Deliberate key-only hosts don't need this — only the
137
+ * both-present case does. */
138
+ allowApiBilling?: boolean;
139
+ /** Billing-probe override for tests (default: the real probeBilling). */
140
+ probeBilling?: BillingProber;
141
+ }
142
+
143
+ export interface RunReport {
144
+ runId: string;
145
+ name: string;
146
+ /** What the script's default export returned. */
147
+ result: unknown;
148
+ agentsSpawned: number;
149
+ /** agent() calls satisfied from the resumeFrom journal without spawning. */
150
+ agentsCached: number;
151
+ costUsd: number;
152
+ durationMs: number;
153
+ journalPath: string;
154
+ /** Estimated tokens of repo instructions (CLAUDE.md/AGENTS.md at the child
155
+ * cwd) that EVERY child cache-writes on spawn — the fixed per-child context
156
+ * overhead a fan-out multiplies. bytes/4 heuristic; 0 when no such file. */
157
+ contextTokensPerChildEstimate: number;
158
+ /** Billing mode per harness actually used this run (probed on first use). */
159
+ billing: Array<{ harness: HarnessName; mode: BillingMode }>;
160
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Minimal validator for the StageSchema JSON-schema subset. Deliberately tiny:
3
+ * a full JSON Schema implementation would pull a dependency (ajv) for
4
+ * validation depth workflow gates don't need. Supported: type, properties,
5
+ * required, items, enum. Returns a list of human-readable problems (empty =
6
+ * valid) so the engine can feed failures back into the retry prompt verbatim.
7
+ */
8
+
9
+ import type { StageSchema } from "./types.ts";
10
+
11
+ export function validateAgainstSchema(value: unknown, schema: StageSchema, path = "$"): string[] {
12
+ const problems: string[] = [];
13
+
14
+ if (schema.enum) {
15
+ if (!schema.enum.some((v) => v === value)) {
16
+ problems.push(`${path}: expected one of ${JSON.stringify(schema.enum)}, got ${short(value)}`);
17
+ }
18
+ return problems; // enum is exhaustive; type check is implied by membership
19
+ }
20
+
21
+ switch (schema.type) {
22
+ case "object": {
23
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
24
+ return [`${path}: expected object, got ${short(value)}`];
25
+ }
26
+ const obj = value as Record<string, unknown>;
27
+ for (const key of schema.required ?? []) {
28
+ if (!(key in obj)) problems.push(`${path}.${key}: required property missing`);
29
+ }
30
+ for (const [key, sub] of Object.entries(schema.properties ?? {})) {
31
+ if (key in obj) problems.push(...validateAgainstSchema(obj[key], sub, `${path}.${key}`));
32
+ }
33
+ return problems;
34
+ }
35
+ case "array": {
36
+ if (!Array.isArray(value)) return [`${path}: expected array, got ${short(value)}`];
37
+ if (schema.items) {
38
+ for (const [i, item] of value.entries()) {
39
+ problems.push(
40
+ ...validateAgainstSchema(item, schema.items as StageSchema, `${path}[${i}]`),
41
+ );
42
+ }
43
+ }
44
+ return problems;
45
+ }
46
+ case "string":
47
+ case "number":
48
+ case "boolean": {
49
+ if (typeof value !== schema.type) {
50
+ problems.push(`${path}: expected ${schema.type}, got ${short(value)}`);
51
+ }
52
+ return problems;
53
+ }
54
+ default:
55
+ return [`${path}: unsupported schema type ${String((schema as { type?: unknown }).type)}`];
56
+ }
57
+ }
58
+
59
+ /** Strip accidental markdown code fences, then strict-parse JSON. */
60
+ export function parseStageOutput(text: string): { value?: unknown; error?: string } {
61
+ const stripped = text
62
+ .trim()
63
+ .replace(/^```(?:json)?\s*/i, "")
64
+ .replace(/\s*```$/, "");
65
+ try {
66
+ return { value: JSON.parse(stripped) };
67
+ } catch (err) {
68
+ return { error: `not valid JSON: ${(err as Error).message}` };
69
+ }
70
+ }
71
+
72
+ function short(value: unknown): string {
73
+ const s = JSON.stringify(value);
74
+ return s === undefined ? String(value) : s.length > 60 ? `${s.slice(0, 60)}…` : s;
75
+ }
@@ -74,6 +74,14 @@ export interface BrowserOptions {
74
74
  * via this callback (e.g., a Cloudflare-bypass header for specific zones).
75
75
  */
76
76
  extraHeaders?: (url: string) => Record<string, string>;
77
+ /**
78
+ * Extra Chromium command-line flags, passed through to Playwright's
79
+ * `launchPersistentContext` `args`. Used for environment-specific
80
+ * workarounds — most notably `--disable-gpu` for headed windows under
81
+ * WSLg (see `./launch-args.ts`). Empty/undefined means Playwright's
82
+ * defaults only.
83
+ */
84
+ launchArgs?: string[];
77
85
  }
78
86
 
79
87
  export interface NavigateResult {
@@ -98,6 +106,14 @@ export interface FailedRequest {
98
106
  method: string;
99
107
  failure: string;
100
108
  resourceType: string;
109
+ /** HTTP status for kind "http" entries; null for network-level failures. */
110
+ status: number | null;
111
+ /** "http" = request completed with a >=400 response; "network" = never completed (DNS, TLS, aborts, tunnel). */
112
+ kind: "http" | "network";
113
+ /** True when the entry is the main frame's document response — lets consumers
114
+ * distinguish an expected error-page status (a 404 route under test) from a
115
+ * broken subresource. */
116
+ document?: boolean;
101
117
  }
102
118
 
103
119
  export interface Diagnostics {
@@ -137,6 +153,9 @@ export class Browser {
137
153
  this.context = await chromium.launchPersistentContext(this.profileDir, {
138
154
  headless: !this.opts.headed,
139
155
  viewport: this.opts.viewport ?? { width: 1280, height: 800 },
156
+ ...(this.opts.launchArgs && this.opts.launchArgs.length > 0
157
+ ? { args: this.opts.launchArgs }
158
+ : {}),
140
159
  ...(this.opts.recordHarPath
141
160
  ? { recordHar: { path: this.opts.recordHarPath, mode: "full" as const } }
142
161
  : {}),
@@ -192,6 +211,24 @@ export class Browser {
192
211
  method: req.method(),
193
212
  failure: req.failure()?.errorText ?? "unknown",
194
213
  resourceType: req.resourceType(),
214
+ status: null,
215
+ kind: "network",
216
+ });
217
+ });
218
+ // HTTP-level failures: `requestfailed` only fires for requests that never
219
+ // complete (DNS, TLS, aborts), so a script/stylesheet answered with a
220
+ // 4xx/5xx would otherwise be invisible to failedRequests-based gates.
221
+ page.on("response", (res) => {
222
+ if (res.status() < 400) return;
223
+ const req = res.request();
224
+ this.failedRequests.push({
225
+ url: res.url(),
226
+ method: req.method(),
227
+ failure: `HTTP ${res.status()}`,
228
+ resourceType: req.resourceType(),
229
+ status: res.status(),
230
+ kind: "http",
231
+ document: req.resourceType() === "document" && res.frame() === page.mainFrame(),
195
232
  });
196
233
  });
197
234
  }
@@ -12,6 +12,7 @@ export {
12
12
  type DevOverlayError,
13
13
  type DevOverlayResult,
14
14
  } from "./dev-overlay.js";
15
+ export { isWSL, wslHeadedLaunchArgs } from "./launch-args.js";
15
16
  export type {
16
17
  OverflowElement,
17
18
  OverflowResult,
@@ -0,0 +1,33 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ /**
4
+ * Chromium launch-arg helpers for environment-specific workarounds.
5
+ *
6
+ * The one that matters today is WSLg: a headed Chromium window renders its
7
+ * page to a GPU-backed surface that WSLg composites over an RDP stream to the
8
+ * Windows host. On a range of Windows / GPU-driver / WSLg combinations that
9
+ * GPU-composited surface never presents, so the window shows in the taskbar
10
+ * but paints blank — even though the page itself runs fine (JS, navigation,
11
+ * and DOM all work). Forcing Chromium onto its software compositor with
12
+ * `--disable-gpu` restores on-screen paint. It only matters for headed mode
13
+ * (headless never composites to a display) and only under WSL.
14
+ */
15
+
16
+ /** True when running under WSL (WSL1 or WSL2). */
17
+ export function isWSL(): boolean {
18
+ if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) return true;
19
+ try {
20
+ return /microsoft|wsl/i.test(readFileSync("/proc/version", "utf8"));
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Chromium launch flags to make a headed window paint under WSLg. Returns
28
+ * `["--disable-gpu"]` on WSL, `[]` elsewhere. Callers apply these only for
29
+ * headed launches. See the module doc for the failure mode.
30
+ */
31
+ export function wslHeadedLaunchArgs(): string[] {
32
+ return isWSL() ? ["--disable-gpu"] : [];
33
+ }