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
@@ -19,6 +19,8 @@
19
19
  */
20
20
 
21
21
  import { exec } from "../../lib/exec.ts";
22
+ import { validateHarnessEffort } from "../harnesses/profiles.ts";
23
+ import type { HarnessInvocation, HarnessRawResult } from "../harnesses/types.ts";
22
24
  import { buildChildEnv } from "./child-env.ts";
23
25
  import { notFoundError } from "./harnesses.ts";
24
26
  import type { Spawner, SpawnRequest, SpawnResult } from "./types.ts";
@@ -33,9 +35,8 @@ interface ClaudeEnvelope {
33
35
  errors?: string[];
34
36
  }
35
37
 
36
- export const claudeCodeSpawner: Spawner = async (req: SpawnRequest): Promise<SpawnResult> => {
37
- const t0 = Date.now();
38
-
38
+ export function buildClaudeInvocation(req: SpawnRequest): HarnessInvocation {
39
+ validateHarnessEffort("claude-code", req.effort);
39
40
  const argv = [
40
41
  "claude",
41
42
  "-p",
@@ -46,35 +47,37 @@ export const claudeCodeSpawner: Spawner = async (req: SpawnRequest): Promise<Spa
46
47
  String(req.maxTurns),
47
48
  ];
48
49
  if (req.model) argv.push("--model", req.model);
50
+ if (req.effort) argv.push("--effort", req.effort);
51
+ return { argv };
52
+ }
49
53
 
50
- const r = await exec(argv, {
51
- cwd: req.cwd,
52
- env: buildChildEnv(req.runId, { subscriptionOnly: req.subscriptionOnly }),
53
- timeout: req.timeoutMs,
54
- });
55
- const durationMs = Date.now() - t0;
56
-
57
- if (r.exitCode === 127) {
58
- return { ok: false, text: "", durationMs, error: notFoundError("claude-code") };
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
+ };
59
62
  }
60
- if (r.exitCode !== 0) {
63
+ if (raw.exitCode !== 0) {
61
64
  return {
62
65
  ok: false,
63
66
  text: "",
64
- durationMs,
65
- error: `claude exited ${r.exitCode}: ${(r.stderr || r.stdout).slice(0, 500)}`,
67
+ durationMs: raw.durationMs,
68
+ error: `claude exited ${raw.exitCode}: ${(raw.stderr || raw.stdout).slice(0, 500)}`,
66
69
  };
67
70
  }
68
71
 
69
72
  let envelope: ClaudeEnvelope;
70
73
  try {
71
- envelope = JSON.parse(r.stdout) as ClaudeEnvelope;
74
+ envelope = JSON.parse(raw.stdout) as ClaudeEnvelope;
72
75
  } catch {
73
76
  return {
74
77
  ok: false,
75
78
  text: "",
76
- durationMs,
77
- error: `result envelope was not JSON: ${r.stdout.slice(0, 300)}`,
79
+ durationMs: raw.durationMs,
80
+ error: `result envelope was not JSON: ${raw.stdout.slice(0, 300)}`,
78
81
  };
79
82
  }
80
83
 
@@ -84,7 +87,7 @@ export const claudeCodeSpawner: Spawner = async (req: SpawnRequest): Promise<Spa
84
87
  text: String(envelope.result ?? ""),
85
88
  sessionId: envelope.session_id,
86
89
  costUsd: envelope.total_cost_usd,
87
- durationMs,
90
+ durationMs: raw.durationMs,
88
91
  error: `harness error (${envelope.subtype ?? "unknown"}): ${(envelope.errors ?? []).join("; ") || "see envelope"}`,
89
92
  };
90
93
  }
@@ -94,6 +97,22 @@ export const claudeCodeSpawner: Spawner = async (req: SpawnRequest): Promise<Spa
94
97
  text: String(envelope.result ?? ""),
95
98
  sessionId: envelope.session_id,
96
99
  costUsd: envelope.total_cost_usd,
97
- durationMs,
100
+ durationMs: raw.durationMs,
98
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 });
99
118
  };
@@ -20,54 +20,73 @@ import { existsSync, readFileSync, rmSync } from "node:fs";
20
20
  import { tmpdir } from "node:os";
21
21
  import { join } from "node:path";
22
22
  import { exec } from "../../lib/exec.ts";
23
+ import { validateHarnessEffort } from "../harnesses/profiles.ts";
24
+ import type { HarnessInvocation, HarnessRawResult } from "../harnesses/types.ts";
23
25
  import { buildChildEnv } from "./child-env.ts";
24
26
  import { notFoundError } from "./harnesses.ts";
25
27
  import type { Spawner, SpawnRequest, SpawnResult } from "./types.ts";
26
28
 
27
- export const codexSpawner: Spawner = async (req: SpawnRequest): Promise<SpawnResult> => {
28
- const t0 = Date.now();
29
- const outFile = join(
30
- tmpdir(),
31
- `harnery-codex-${process.pid}-${randomBytes(4).toString("hex")}.txt`,
32
- );
33
-
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");
34
32
  const argv = [
35
33
  "codex",
36
34
  "exec",
37
35
  req.prompt,
38
36
  "--output-last-message",
39
- outFile,
37
+ resultFile,
40
38
  "--skip-git-repo-check",
41
39
  "--sandbox",
42
40
  "workspace-write",
43
41
  ];
44
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
+ );
45
72
 
46
73
  try {
47
- const r = await exec(argv, {
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, {
48
81
  cwd: req.cwd,
49
82
  env: buildChildEnv(req.runId, { subscriptionOnly: req.subscriptionOnly }),
50
83
  timeout: req.timeoutMs,
51
84
  });
52
- const durationMs = Date.now() - t0;
53
-
54
- if (r.exitCode === 127) {
55
- return { ok: false, text: "", durationMs, error: notFoundError("codex") };
56
- }
57
- if (r.exitCode !== 0) {
58
- return {
59
- ok: false,
60
- text: "",
61
- durationMs,
62
- error: `codex exited ${r.exitCode}: ${(r.stderr || r.stdout).slice(0, 500)}`,
63
- };
64
- }
65
-
66
- if (!existsSync(outFile)) {
67
- // Exit 0 but no last-message file: fall back to stdout (contract drift guard).
68
- return { ok: true, text: r.stdout, durationMs };
69
- }
70
- return { ok: true, text: readFileSync(outFile, "utf8").trim(), durationMs };
85
+ return normalizeCodexResult({
86
+ ...r,
87
+ durationMs: Date.now() - t0,
88
+ resultFileText: existsSync(outFile) ? readFileSync(outFile, "utf8") : undefined,
89
+ });
71
90
  } finally {
72
91
  rmSync(outFile, { force: true });
73
92
  }
@@ -17,6 +17,8 @@
17
17
  */
18
18
 
19
19
  import { exec } from "../../lib/exec.ts";
20
+ import { validateHarnessEffort } from "../harnesses/profiles.ts";
21
+ import type { HarnessInvocation, HarnessRawResult } from "../harnesses/types.ts";
20
22
  import { buildChildEnv } from "./child-env.ts";
21
23
  import { notFoundError } from "./harnesses.ts";
22
24
  import type { Spawner, SpawnRequest, SpawnResult } from "./types.ts";
@@ -46,44 +48,61 @@ export function parseCursorOutput(stdout: string): {
46
48
  }
47
49
  }
48
50
 
49
- export const cursorSpawner: Spawner = async (req: SpawnRequest): Promise<SpawnResult> => {
50
- const t0 = Date.now();
51
-
51
+ export function buildCursorInvocation(req: SpawnRequest): HarnessInvocation {
52
+ validateHarnessEffort("cursor", req.effort);
52
53
  // --trust: headless cursor-agent refuses untrusted workspaces (exit 1,
53
54
  // "Workspace Trust Required"). Workflow children run in the engine's cwd
54
55
  // deliberately and may edit files — the same posture as the codex adapter's
55
56
  // `--sandbox workspace-write` — so trusting that directory is implied.
56
57
  const argv = ["cursor-agent", "-p", req.prompt, "--output-format", "json", "--trust"];
57
58
  if (req.model) argv.push("--model", req.model);
59
+ return { argv };
60
+ }
58
61
 
59
- const r = await exec(argv, {
60
- cwd: req.cwd,
61
- env: buildChildEnv(req.runId, { subscriptionOnly: req.subscriptionOnly }),
62
- timeout: req.timeoutMs,
63
- });
64
- const durationMs = Date.now() - t0;
65
-
66
- if (r.exitCode === 127) {
67
- return { ok: false, text: "", durationMs, error: notFoundError("cursor") };
62
+ export function normalizeCursorResult(raw: HarnessRawResult): SpawnResult {
63
+ if (raw.exitCode === 127) {
64
+ return { ok: false, text: "", durationMs: raw.durationMs, error: notFoundError("cursor") };
68
65
  }
69
- if (r.exitCode !== 0) {
66
+ if (raw.exitCode !== 0) {
70
67
  return {
71
68
  ok: false,
72
69
  text: "",
73
- durationMs,
74
- error: `cursor-agent exited ${r.exitCode}: ${(r.stderr || r.stdout).slice(0, 500)}`,
70
+ durationMs: raw.durationMs,
71
+ error: `cursor-agent exited ${raw.exitCode}: ${(raw.stderr || raw.stdout).slice(0, 500)}`,
75
72
  };
76
73
  }
77
74
 
78
- const parsed = parseCursorOutput(r.stdout);
75
+ const parsed = parseCursorOutput(raw.stdout);
79
76
  if (parsed.isError) {
80
77
  return {
81
78
  ok: false,
82
79
  text: parsed.text,
83
80
  sessionId: parsed.sessionId,
84
- durationMs,
81
+ durationMs: raw.durationMs,
85
82
  error: `cursor-agent reported is_error: ${parsed.text.slice(0, 300)}`,
86
83
  };
87
84
  }
88
- return { ok: true, text: parsed.text, sessionId: parsed.sessionId, durationMs };
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 });
89
108
  };
@@ -28,6 +28,9 @@ export interface AgentOpts {
28
28
  schema?: StageSchema;
29
29
  /** Model slug passed through to the harness CLI (default: the CLI's default). */
30
30
  model?: string;
31
+ /** Reasoning effort mapped through the selected harness profile. Unsupported
32
+ * values fail before the vendor process starts. */
33
+ effort?: string;
31
34
  /** Attempt ceiling for the schema-retry loop (default 2). */
32
35
  maxAttempts?: number;
33
36
  /** Subprocess timeout ms (default 300_000). */
@@ -43,7 +46,10 @@ export interface AgentOpts {
43
46
  harness?: HarnessName;
44
47
  }
45
48
 
46
- export type HarnessName = "claude-code" | "codex" | "cursor";
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;
47
53
 
48
54
  /** What a spawn adapter returns for one subagent run. */
49
55
  export interface SpawnResult {
@@ -61,6 +67,7 @@ export interface SpawnResult {
61
67
  export interface SpawnRequest {
62
68
  prompt: string;
63
69
  model?: string;
70
+ effort?: string;
64
71
  timeoutMs: number;
65
72
  maxTurns: number;
66
73
  cwd: string;
@@ -105,7 +112,7 @@ export interface EngineOpts {
105
112
  coordRoot: string;
106
113
  /** Spawner registry keyed by harness. A single-harness caller registers one
107
114
  * entry and names it in `defaultHarness`. */
108
- spawners: Partial<Record<HarnessName, Spawner>>;
115
+ spawners: Readonly<Record<HarnessName, Spawner | undefined>>;
109
116
  /** Harness used when an agent() call doesn't name one (default "claude-code"). */
110
117
  defaultHarness?: HarnessName;
111
118
  /** Resume: run id of a prior run whose journal supplies cached results.
@@ -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
+ }