@orkestrel/scaffold 0.0.108 → 0.0.109

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 (157) hide show
  1. package/dist/agents/skills/orkestrel-dispatch/scripts/bench.js +326 -91
  2. package/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +110 -14
  3. package/dist/agents/skills/orkestrel-dispatch/scripts/chain.js +280 -0
  4. package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +188 -26
  5. package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +142 -43
  6. package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +240 -62
  7. package/dist/agents/skills/orkestrel-dispatch/scripts/patch.js +323 -0
  8. package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +169 -32
  9. package/dist/agents/skills/orkestrel-dispatch/scripts/sweep.js +290 -69
  10. package/dist/agents/skills/orkestrel-falsify/scripts/mutate.js +560 -0
  11. package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +1052 -205
  12. package/dist/agents/skills/orkestrel-harden/scripts/measure.js +361 -0
  13. package/dist/agents/skills/orkestrel-harden/scripts/titles.js +455 -80
  14. package/dist/agents/skills/orkestrel-parallel/scripts/cases.js +327 -0
  15. package/dist/agents/skills/orkestrel-publish/scripts/compare.js +298 -51
  16. package/dist/agents/skills/orkestrel-publish/scripts/pins.js +170 -13
  17. package/dist/agents/skills/orkestrel-publish/scripts/swap.js +956 -191
  18. package/dist/agents/skills/orkestrel-publish/scripts/wave.js +707 -112
  19. package/dist/agents/skills/orkestrel-publish/scripts/window.js +235 -41
  20. package/dist/agents/skills/orkestrel-scout/scripts/map.js +341 -54
  21. package/dist/agents/templates/brief.md +1 -1
  22. package/dist/bin/main.js +51 -46
  23. package/dist/bin/main.js.map +1 -1
  24. package/dist/host/AGENTS.md +23 -21
  25. package/dist/host/agents/orchestration.md +43 -18
  26. package/dist/host/agents/platforms/windows.md +3 -3
  27. package/dist/host/agents/skills/orkestrel-align/SKILL.md +10 -13
  28. package/dist/host/agents/skills/orkestrel-align/references/fleet.md +4 -10
  29. package/dist/host/agents/skills/orkestrel-align/references/integration.md +1 -1
  30. package/dist/host/agents/skills/orkestrel-build/SKILL.md +16 -48
  31. package/dist/host/agents/skills/orkestrel-build/agents/openai.yaml +1 -1
  32. package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +19 -16
  33. package/dist/host/agents/skills/orkestrel-debrief/agents/openai.yaml +1 -1
  34. package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +15 -28
  35. package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +5 -6
  36. package/dist/host/agents/skills/orkestrel-debrief/references/retention.md +4 -6
  37. package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +31 -20
  38. package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +3 -4
  39. package/dist/host/agents/skills/orkestrel-dispatch/scripts/bench.ts +366 -75
  40. package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +126 -16
  41. package/dist/host/agents/skills/orkestrel-dispatch/scripts/chain.ts +323 -0
  42. package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +220 -31
  43. package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +176 -33
  44. package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +294 -59
  45. package/dist/host/agents/skills/orkestrel-dispatch/scripts/patch.ts +401 -0
  46. package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +199 -34
  47. package/dist/host/agents/skills/orkestrel-dispatch/scripts/sweep.ts +330 -69
  48. package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +10 -8
  49. package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +1 -5
  50. package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +1 -1
  51. package/dist/host/agents/skills/orkestrel-falsify/scripts/mutate.ts +701 -0
  52. package/dist/host/agents/skills/orkestrel-harden/SKILL.md +16 -12
  53. package/dist/host/agents/skills/orkestrel-harden/references/bindings.md +23 -81
  54. package/dist/host/agents/skills/orkestrel-harden/references/centralization.md +4 -5
  55. package/dist/host/agents/skills/orkestrel-harden/references/contract.md +2 -0
  56. package/dist/host/agents/skills/orkestrel-harden/references/document.md +33 -0
  57. package/dist/host/agents/skills/orkestrel-harden/references/equivalence.md +6 -3
  58. package/dist/host/agents/skills/orkestrel-harden/references/hardening.md +8 -4
  59. package/dist/host/agents/skills/orkestrel-harden/references/research.md +2 -0
  60. package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +1272 -279
  61. package/dist/host/agents/skills/orkestrel-harden/scripts/measure.ts +425 -0
  62. package/dist/host/agents/skills/orkestrel-harden/scripts/titles.ts +603 -98
  63. package/dist/host/agents/skills/orkestrel-journey/SKILL.md +75 -98
  64. package/dist/host/agents/skills/orkestrel-journey/references/captures.md +54 -17
  65. package/dist/host/agents/skills/orkestrel-journey/references/decide.md +1 -20
  66. package/dist/host/agents/skills/orkestrel-journey/references/layer.md +21 -31
  67. package/dist/host/agents/skills/orkestrel-journey/references/styles.md +1 -1
  68. package/dist/host/agents/skills/orkestrel-parallel/SKILL.md +28 -24
  69. package/dist/host/agents/skills/orkestrel-parallel/references/claude.md +15 -13
  70. package/dist/host/agents/skills/orkestrel-parallel/references/codex.md +1 -0
  71. package/dist/host/agents/skills/orkestrel-parallel/references/cursor.md +13 -13
  72. package/dist/host/agents/skills/orkestrel-parallel/scripts/cases.ts +389 -0
  73. package/dist/host/agents/skills/orkestrel-polish/SKILL.md +16 -20
  74. package/dist/host/agents/skills/orkestrel-polish/references/capture-harness.md +16 -26
  75. package/dist/host/agents/skills/orkestrel-publish/SKILL.md +12 -26
  76. package/dist/host/agents/skills/orkestrel-publish/agents/openai.yaml +2 -2
  77. package/dist/host/agents/skills/orkestrel-publish/references/adopt.md +23 -0
  78. package/dist/host/agents/skills/orkestrel-publish/references/release.md +4 -4
  79. package/dist/host/agents/skills/orkestrel-publish/references/wave.md +10 -9
  80. package/dist/host/agents/skills/orkestrel-publish/references/window.md +20 -25
  81. package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +348 -59
  82. package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +202 -22
  83. package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1061 -191
  84. package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +812 -140
  85. package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +280 -38
  86. package/dist/host/agents/skills/orkestrel-scout/SKILL.md +3 -3
  87. package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +428 -70
  88. package/dist/host/agents/templates/brief.md +1 -1
  89. package/dist/host/agents/transports/claude-cli.md +3 -3
  90. package/dist/host/agents/transports/codex.md +2 -7
  91. package/dist/host/agents/transports/cursor.md +2 -3
  92. package/dist/host/claude/AGENTS.md +2 -2
  93. package/dist/host/claude/agents/analyst.md +3 -3
  94. package/dist/host/claude/agents/astra.md +3 -3
  95. package/dist/host/claude/agents/builder.md +1 -1
  96. package/dist/host/claude/agents/checker.md +1 -1
  97. package/dist/host/claude/agents/distiller.md +1 -1
  98. package/dist/host/claude/agents/grok.md +1 -1
  99. package/dist/host/claude/agents/opus.md +2 -2
  100. package/dist/host/claude/agents/orkestrel.md +57 -90
  101. package/dist/host/claude/agents/researcher.md +1 -1
  102. package/dist/host/claude/agents/reviewer.md +4 -0
  103. package/dist/host/claude/agents/scout.md +1 -1
  104. package/dist/host/claude/agents/verifier.md +1 -1
  105. package/dist/host/claude/rules/application.md +14 -21
  106. package/dist/host/claude/rules/architecture.md +34 -44
  107. package/dist/host/claude/rules/browser.md +3 -4
  108. package/dist/host/claude/rules/campaign.md +2 -2
  109. package/dist/host/claude/rules/documentation.md +25 -12
  110. package/dist/host/claude/rules/names.md +9 -4
  111. package/dist/host/claude/rules/patterns.md +6 -7
  112. package/dist/host/claude/rules/portability.md +1 -1
  113. package/dist/host/claude/rules/quality.md +4 -5
  114. package/dist/host/claude/rules/styles.md +2 -2
  115. package/dist/host/claude/rules/tests.md +57 -114
  116. package/dist/host/claude/rules/typescript.md +7 -9
  117. package/dist/host/claude/rules/workspace.md +21 -19
  118. package/dist/host/claude/rules/writing.md +2 -5
  119. package/dist/host/claude/skills/orkestrel-dispatch/SKILL.md +1 -1
  120. package/dist/host/claude/skills/orkestrel-harden/SKILL.md +1 -1
  121. package/dist/host/claude/skills/orkestrel-publish/SKILL.md +1 -1
  122. package/dist/host/claude/skills/orkestrel-scout/SKILL.md +1 -1
  123. package/dist/host/codex/agents/analyst.toml +9 -3
  124. package/dist/host/codex/agents/astra.toml +3 -4
  125. package/dist/host/codex/agents/builder.toml +1 -1
  126. package/dist/host/codex/agents/opus.toml +3 -4
  127. package/dist/host/codex/agents/orkestrel.toml +1 -1
  128. package/dist/host/codex/agents/researcher.toml +2 -2
  129. package/dist/host/codex/agents/verifier.toml +1 -1
  130. package/dist/host/configs/helpers.ts +21 -1
  131. package/dist/host/configs/policy.ts +299 -15
  132. package/dist/host/dotfiles/oxlintrc.json +137 -3
  133. package/dist/host/guides/agent.md +111 -134
  134. package/dist/host/guides/capture.md +801 -0
  135. package/dist/host/guides/probe.md +27 -8
  136. package/dist/host/guides/scaffold.md +96 -18
  137. package/dist/host/guides/study.md +534 -440
  138. package/dist/host/guides/test.md +111 -44
  139. package/dist/host/manifest.json +160 -240
  140. package/dist/host/tests/config.test.ts +971 -187
  141. package/dist/host/tests/policy.test.ts +127 -1
  142. package/dist/host/tests/setupPolicy.ts +893 -33
  143. package/dist/src/core/index.cjs +214 -86
  144. package/dist/src/core/index.cjs.map +1 -1
  145. package/dist/src/core/index.d.cts +122 -38
  146. package/dist/src/core/index.d.ts +122 -38
  147. package/dist/src/core/index.js +209 -85
  148. package/dist/src/core/index.js.map +1 -1
  149. package/dist/src/server/index.cjs +20 -27
  150. package/dist/src/server/index.cjs.map +1 -1
  151. package/dist/src/server/index.js +21 -28
  152. package/dist/src/server/index.js.map +1 -1
  153. package/package.json +11 -11
  154. package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +0 -283
  155. package/dist/host/agents/skills/orkestrel-dispatch/references/bench.md +0 -25
  156. package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +0 -333
  157. package/dist/host/guides/supervisor.md +0 -4890
@@ -5,34 +5,78 @@
5
5
  // --cursor resolves the Cursor CLI entry (the newest versioned install under LOCALAPPDATA on
6
6
  // Windows, `agent` on PATH elsewhere), reads its version (an entry that cannot answer `--version` is
7
7
  // dark, with or without --resolve), and runs one bounded prompt through
8
- // launch.ts into tmp/cursor/bench.jsonl; --resolve stops after printing the entry. --codex reads
8
+ // launch.ts into tmp/cursor/bench.jsonl; --resolve, which only --cursor takes, stops after printing
9
+ // the entry. --codex reads
9
10
  // `codex --version` and `codex login status`, then runs one bounded exec into tmp/codex/bench.jsonl
10
- // under the sandbox the host allows (danger-full-access on Windows, read-only elsewhere). --claude
11
+ // under the sandbox the host allows (danger-full-access on Windows, read-only elsewhere) unless
12
+ // --sandbox names read-only, workspace-write, or danger-full-access. --claude
11
13
  // reads `claude --version` and runs one bounded print-mode prompt into tmp/claude/bench.jsonl. The
12
- // JSON summary carries the command, the version, the session id, the answer, and `live`. Exit 0 when
13
- // the bench answered READY, 3 when it is dark, 64 on usage.
14
+ // script runs launch.ts and result.ts as child processes beside it and imports neither. The JSON
15
+ // summary carries the command, the version, the session id, the answer, and `live`. --cap is a
16
+ // number of seconds above 0 and at most 2147483, checked before any probe runs. Name one bench per
17
+ // run; the script refuses a flag it does not read, the `--flag=value` form included. Exit 0 when the
18
+ // bench answered READY, 3 when it is dark, 64 on usage.
14
19
  import { spawnSync } from 'node:child_process'
15
- import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
20
+ import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs'
16
21
  import { dirname, extname, join } from 'node:path'
17
22
  import { fileURLToPath } from 'node:url'
18
- import { readJsonObject, readMissingFlags, readNumber, readOption, readString } from './helpers.ts'
19
-
20
- const SCRIPTS = dirname(fileURLToPath(import.meta.url))
21
- // The checkout runs the `.ts` sources and a built twin under `dist/agents` runs `.js` siblings.
22
- const EXTENSION = extname(fileURLToPath(import.meta.url))
23
- const PROMPT = 'Reply with the single word READY and nothing else.'
24
- const CURSOR_MODEL = process.env.CURSOR_GROK_MODEL ?? 'grok-4.7-xhigh'
25
- const CODEX_MODEL = process.env.CODEX_ASTRA_MODEL ?? 'gpt-6-astra'
26
- const CLAUDE_MODEL = 'opus'
27
- const DEFAULT_CAP = 180
28
-
29
- interface Entry {
23
+
24
+ /** Names the directory this script runs from, which holds the launch and result siblings. */
25
+ export const SCRIPTS = dirname(fileURLToPath(import.meta.url))
26
+
27
+ /** Names the extension of the running file: the checkout runs `.ts` sources and a built twin runs `.js` siblings. */
28
+ export const EXTENSION = extname(fileURLToPath(import.meta.url))
29
+
30
+ /** Holds the one-word request every bench round trip sends. */
31
+ export const PROMPT = 'Reply with the single word READY and nothing else.'
32
+
33
+ /** Names the Cursor model a round trip uses unless the call names another. */
34
+ export const CURSOR_MODEL = process.env.CURSOR_GROK_MODEL ?? 'grok-4.7-xhigh'
35
+
36
+ /** Names the Codex model a round trip uses unless the call names another. */
37
+ export const CODEX_MODEL = process.env.CODEX_ASTRA_MODEL ?? 'gpt-6-astra'
38
+
39
+ /** Names the Claude model alias a round trip uses unless the call names another. */
40
+ export const CLAUDE_MODEL = 'opus'
41
+
42
+ /** Holds the seconds a round trip may run when the call gives no cap. */
43
+ export const DEFAULT_CAP = 180
44
+
45
+ /** Holds the longest cap in whole seconds that a Node timer carries through the launcher. */
46
+ export const MAX_CAP_SECONDS = 2_147_483
47
+
48
+ /** Lists the sandbox modes `--sandbox` may name, which the Codex CLI reads. */
49
+ export const SANDBOX_MODES: readonly string[] = [
50
+ 'read-only',
51
+ 'workspace-write',
52
+ 'danger-full-access',
53
+ ]
54
+
55
+ /** Lists the flags that each select one bench. */
56
+ export const BENCH_FLAGS: readonly string[] = ['--cursor', '--codex', '--claude']
57
+
58
+ /** Lists the flags that take a value. */
59
+ export const VALUE_FLAGS: readonly string[] = ['--model', '--cap', '--sandbox']
60
+
61
+ /** Lists every flag the script reads, value flags and switches alike. */
62
+ export const FLAGS: readonly string[] = [...BENCH_FLAGS, '--resolve', ...VALUE_FLAGS]
63
+
64
+ /** Holds the usage line printed when a call names no bench or more than one. */
65
+ export const USAGE =
66
+ 'usage: bench.ts --cursor [--resolve] [--model ID] [--cap SECONDS] | --codex [--model ID] [--sandbox MODE] [--cap SECONDS] | --claude [--model ALIAS] [--cap SECONDS]; name one bench'
67
+
68
+ /** Names one bench a probe can run against. */
69
+ export type Bench = 'cursor' | 'codex' | 'claude'
70
+
71
+ /** Describes a resolved bench entry: the command that starts it, its version, and why it did not resolve. */
72
+ export interface Entry {
30
73
  readonly command: readonly string[]
31
74
  readonly version: string | undefined
32
75
  readonly error: string | undefined
33
76
  }
34
77
 
35
- interface Summary {
78
+ /** Describes what one round trip observed. */
79
+ export interface Summary {
36
80
  readonly session: string | undefined
37
81
  readonly answer: string | undefined
38
82
  readonly exit: number | undefined
@@ -40,14 +84,158 @@ interface Summary {
40
84
  readonly durationMs: number | undefined
41
85
  }
42
86
 
43
- function readVersion(command: readonly string[]): string | undefined {
87
+ /** Describes what a probe decided: the JSON report to print and the exit code to return. */
88
+ export interface Probe {
89
+ readonly report: Record<string, unknown>
90
+ readonly exit: number
91
+ }
92
+
93
+ /**
94
+ * Reads the value that follows a flag.
95
+ *
96
+ * @remarks
97
+ * The first occurrence of a flag wins, a following value that opens with `--` is a missing value,
98
+ * and the `--flag=value` form is not read.
99
+ *
100
+ * @param argv - The arguments after the script path.
101
+ * @param name - The flag, such as `--cap`.
102
+ * @returns The value after the flag, or undefined when the flag is absent, last, or followed by
103
+ * another flag.
104
+ */
105
+ export function readOption(argv: readonly string[], name: string): string | undefined {
106
+ const index = argv.indexOf(name)
107
+ const value = index === -1 ? undefined : argv[index + 1]
108
+ return value === undefined || value.startsWith('--') ? undefined : value
109
+ }
110
+
111
+ /**
112
+ * Names the value flags a caller gave with no value.
113
+ *
114
+ * @param argv - The arguments after the script path.
115
+ * @param names - The flags that take a value.
116
+ * @returns Each named flag that appears last or is followed by another flag, in argument order.
117
+ */
118
+ export function readMissingFlags(
119
+ argv: readonly string[],
120
+ names: readonly string[],
121
+ ): readonly string[] {
122
+ return names.filter((name) => argv.includes(name) && readOption(argv, name) === undefined)
123
+ }
124
+
125
+ /**
126
+ * Names the flags a caller gave that the script does not read.
127
+ *
128
+ * @remarks
129
+ * A `--flag=value` argument is unknown, because the script reads no such form. A bare `--` ends the
130
+ * scan.
131
+ *
132
+ * @param argv - The arguments after the script path.
133
+ * @param flags - Every flag the script reads, value flags and switches alike.
134
+ * @returns Each argument that opens with `--` and is not a read flag, in argument order.
135
+ */
136
+ export function readUnknownFlags(
137
+ argv: readonly string[],
138
+ flags: readonly string[],
139
+ ): readonly string[] {
140
+ const end = argv.indexOf('--')
141
+ return (end === -1 ? argv : argv.slice(0, end)).filter(
142
+ (argument) => argument.startsWith('--') && !flags.includes(argument),
143
+ )
144
+ }
145
+
146
+ /**
147
+ * Quotes an untrusted text for a diagnostic, JSON-encoded and cut to a bounded length.
148
+ *
149
+ * @param text - The text to quote.
150
+ * @returns The JSON string literal of the text, ending in an ellipsis inside the quotes when cut.
151
+ */
152
+ export function quote(text: string): string {
153
+ return JSON.stringify(text.length > 80 ? `${text.slice(0, 80)}…` : text)
154
+ }
155
+
156
+ /**
157
+ * Reads the last non-empty line of a text as a JSON object.
158
+ *
159
+ * @param text - Text whose last line a script printed as JSON.
160
+ * @returns The parsed object, or undefined when the last line is not a JSON object.
161
+ */
162
+ export function parseJsonLine(text: string): Record<string, unknown> | undefined {
163
+ const line = text
164
+ .split(/\r\n|\n/)
165
+ .filter((candidate) => candidate.trim() !== '')
166
+ .at(-1)
167
+ if (line === undefined) return undefined
168
+ try {
169
+ const parsed: unknown = JSON.parse(line)
170
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
171
+ return Object.fromEntries(Object.entries(parsed))
172
+ }
173
+ } catch {}
174
+ return undefined
175
+ }
176
+
177
+ /**
178
+ * Reads a string member of a record.
179
+ *
180
+ * @param record - The record, or undefined.
181
+ * @param key - The member name.
182
+ * @returns The string, or undefined when the member is absent or not a string.
183
+ */
184
+ export function readString(
185
+ record: Record<string, unknown> | undefined,
186
+ key: string,
187
+ ): string | undefined {
188
+ const value = record?.[key]
189
+ return typeof value === 'string' ? value : undefined
190
+ }
191
+
192
+ /**
193
+ * Reads a finite number member of a record.
194
+ *
195
+ * @param record - The record, or undefined.
196
+ * @param key - The member name.
197
+ * @returns The number, or undefined when the member is absent, not a number, or not finite.
198
+ */
199
+ export function readNumber(
200
+ record: Record<string, unknown> | undefined,
201
+ key: string,
202
+ ): number | undefined {
203
+ const value = record?.[key]
204
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined
205
+ }
206
+
207
+ /**
208
+ * Parses the `--cap` value into a number of seconds.
209
+ *
210
+ * @param text - The value after `--cap`, or undefined when the flag is absent.
211
+ * @returns The default cap when the flag is absent, the number when it is finite, above 0, and at
212
+ * most the longest cap, and undefined otherwise.
213
+ */
214
+ export function parseCap(text: string | undefined): number | undefined {
215
+ if (text === undefined) return DEFAULT_CAP
216
+ const cap = Number(text)
217
+ return Number.isFinite(cap) && cap > 0 && cap <= MAX_CAP_SECONDS ? cap : undefined
218
+ }
219
+
220
+ /**
221
+ * Reads the version a command prints for `--version`.
222
+ *
223
+ * @param command - The executable and the arguments that precede the flag.
224
+ * @returns The trimmed output, or undefined when the command is empty, cannot start, or exits non-zero.
225
+ */
226
+ export function readVersion(command: readonly string[]): string | undefined {
44
227
  const [file, ...args] = command
45
228
  if (file === undefined) return undefined
46
229
  const result = spawnSync(file, [...args, '--version'], { encoding: 'utf8', windowsHide: true })
47
230
  return result.status === 0 ? result.stdout.trim() : undefined
48
231
  }
49
232
 
50
- function resolveCursor(): Entry {
233
+ /**
234
+ * Resolves the Cursor CLI entry for this host.
235
+ *
236
+ * @returns The command and its version, or the reason the entry did not resolve.
237
+ */
238
+ export function resolveCursor(): Entry {
51
239
  if (process.platform !== 'win32') {
52
240
  return { command: ['agent'], version: readVersion(['agent']), error: undefined }
53
241
  }
@@ -74,10 +262,20 @@ function resolveCursor(): Entry {
74
262
  return { command: [node, index], version: readVersion([node, index]), error: undefined }
75
263
  }
76
264
 
77
- function runRoundTrip(
78
- bench: 'cursor' | 'codex' | 'claude',
265
+ /**
266
+ * Runs one bounded round trip through the launch and result scripts beside this one.
267
+ *
268
+ * @param bench - The bench whose journal, errors, and answer files receive the run.
269
+ * @param command - The bench command with its arguments and prompt.
270
+ * @param cap - The seconds the launcher allows the command.
271
+ * @param root - The directory the children run in, which holds the `tmp/<bench>` files.
272
+ * @returns What the launcher and the result reader reported.
273
+ */
274
+ export function runRoundTrip(
275
+ bench: Bench,
79
276
  command: readonly string[],
80
277
  cap: number,
278
+ root: string,
81
279
  ): Summary {
82
280
  const journal = `tmp/${bench}/bench.jsonl`
83
281
  const errors = `tmp/${bench}/bench.err`
@@ -95,25 +293,34 @@ function runRoundTrip(
95
293
  '--',
96
294
  ...command,
97
295
  ],
98
- { encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit'], windowsHide: true },
296
+ { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit'], windowsHide: true },
99
297
  )
100
- const launched = readJsonObject(launch.stdout)
298
+ const launched = parseJsonLine(launch.stdout)
101
299
  const result = spawnSync(
102
300
  process.execPath,
103
301
  [join(SCRIPTS, `result${EXTENSION}`), `--${bench}`, journal, '--out', answer],
104
- { encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit'], windowsHide: true },
302
+ { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit'], windowsHide: true },
105
303
  )
106
- const read = readJsonObject(result.stdout)
304
+ const read = parseJsonLine(result.stdout)
305
+ const answerPath = join(root, answer)
107
306
  return {
108
307
  session: readString(read, 'session'),
109
- answer: existsSync(answer) ? readFileSync(answer, 'utf8').trim().slice(0, 200) : undefined,
308
+ answer: existsSync(answerPath)
309
+ ? readFileSync(answerPath, 'utf8').trim().slice(0, 200)
310
+ : undefined,
110
311
  exit: readNumber(launched, 'exit'),
111
312
  capped: launched?.capped === true,
112
313
  durationMs: readNumber(launched, 'durationMs'),
113
314
  }
114
315
  }
115
316
 
116
- function isLive(summary: Summary): boolean {
317
+ /**
318
+ * Checks whether a round trip shows a live bench.
319
+ *
320
+ * @param summary - What the round trip observed.
321
+ * @returns True if the run has a session, exited 0 uncapped, and answered READY; false otherwise.
322
+ */
323
+ export function isLive(summary: Summary): boolean {
117
324
  return (
118
325
  summary.session !== undefined &&
119
326
  summary.exit === 0 &&
@@ -123,18 +330,26 @@ function isLive(summary: Summary): boolean {
123
330
  )
124
331
  }
125
332
 
126
- function probeCursor(argv: readonly string[]): number {
333
+ /**
334
+ * Probes the Cursor bench, or prints its entry when the call names `--resolve`.
335
+ *
336
+ * @param argv - The arguments after the script path.
337
+ * @param cap - The validated cap in seconds.
338
+ * @param root - The directory the round trip runs in.
339
+ * @returns Exit 0 when live or resolved, 3 when dark, with the report to print.
340
+ */
341
+ export function probeCursor(argv: readonly string[], cap: number, root: string): Probe {
127
342
  const entry = resolveCursor()
128
343
  const model = readOption(argv, '--model') ?? CURSOR_MODEL
129
344
  if (entry.error !== undefined || entry.version === undefined) {
130
- console.log(JSON.stringify({ bench: 'cursor', ...entry, live: false }))
131
- return 3
345
+ return { report: { bench: 'cursor', ...entry, live: false }, exit: 3 }
132
346
  }
133
347
  if (argv.includes('--resolve')) {
134
- console.log(JSON.stringify({ bench: 'cursor', command: entry.command, version: entry.version }))
135
- return 0
348
+ return {
349
+ report: { bench: 'cursor', command: entry.command, version: entry.version },
350
+ exit: 0,
351
+ }
136
352
  }
137
- const cap = Number(readOption(argv, '--cap') ?? DEFAULT_CAP)
138
353
  const summary = runRoundTrip(
139
354
  'cursor',
140
355
  [
@@ -149,10 +364,11 @@ function probeCursor(argv: readonly string[]): number {
149
364
  PROMPT,
150
365
  ],
151
366
  cap,
367
+ root,
152
368
  )
153
369
  const live = isLive(summary)
154
- console.log(
155
- JSON.stringify({
370
+ return {
371
+ report: {
156
372
  bench: 'cursor',
157
373
  command: entry.command,
158
374
  version: entry.version,
@@ -160,32 +376,48 @@ function probeCursor(argv: readonly string[]): number {
160
376
  ...summary,
161
377
  live,
162
378
  journal: 'tmp/cursor/bench.jsonl',
163
- }),
164
- )
165
- return live ? 0 : 3
379
+ },
380
+ exit: live ? 0 : 3,
381
+ }
166
382
  }
167
383
 
168
- function probeCodex(argv: readonly string[]): number {
169
- const version = readVersion(['codex'])
170
- const login = spawnSync('codex', ['login', 'status'], { encoding: 'utf8', windowsHide: true })
384
+ /**
385
+ * Probes the Codex bench: its version, its login, and one bounded exec.
386
+ *
387
+ * @param command - The executable and the arguments that precede the Codex arguments, such as `['codex']`.
388
+ * @param argv - The arguments after the script path.
389
+ * @param cap - The validated cap in seconds.
390
+ * @param root - The directory the round trip runs in.
391
+ * @returns Exit 0 when live, 3 when dark, with the report to print.
392
+ */
393
+ export function probeCodex(
394
+ command: readonly string[],
395
+ argv: readonly string[],
396
+ cap: number,
397
+ root: string,
398
+ ): Probe {
399
+ const [file, ...prefix] = command
400
+ const version = readVersion(command)
401
+ const login =
402
+ file === undefined
403
+ ? undefined
404
+ : spawnSync(file, [...prefix, 'login', 'status'], { encoding: 'utf8', windowsHide: true })
171
405
  const model = readOption(argv, '--model') ?? CODEX_MODEL
172
406
  const sandbox =
173
407
  readOption(argv, '--sandbox') ??
174
408
  (process.platform === 'win32' ? 'danger-full-access' : 'read-only')
175
- const status = `${login.stdout}${login.stderr}`.trim()
176
- if (version === undefined || login.status !== 0) {
177
- console.log(JSON.stringify({ bench: 'codex', version, login: status, live: false }))
178
- return 3
409
+ const status = login === undefined ? '' : `${login.stdout ?? ''}${login.stderr ?? ''}`.trim()
410
+ if (version === undefined || login?.status !== 0) {
411
+ return { report: { bench: 'codex', version, login: status, live: false }, exit: 3 }
179
412
  }
180
- const cap = Number(readOption(argv, '--cap') ?? DEFAULT_CAP)
181
413
  const summary = runRoundTrip(
182
414
  'codex',
183
415
  [
184
- 'codex',
416
+ ...command,
185
417
  'exec',
186
418
  '--json',
187
419
  '-C',
188
- process.cwd(),
420
+ root,
189
421
  '--sandbox',
190
422
  sandbox,
191
423
  '--model',
@@ -197,10 +429,11 @@ function probeCodex(argv: readonly string[]): number {
197
429
  PROMPT,
198
430
  ],
199
431
  cap,
432
+ root,
200
433
  )
201
434
  const live = isLive(summary)
202
- console.log(
203
- JSON.stringify({
435
+ return {
436
+ report: {
204
437
  bench: 'codex',
205
438
  version,
206
439
  login: status,
@@ -209,51 +442,109 @@ function probeCodex(argv: readonly string[]): number {
209
442
  ...summary,
210
443
  live,
211
444
  journal: 'tmp/codex/bench.jsonl',
212
- }),
213
- )
214
- return live ? 0 : 3
445
+ },
446
+ exit: live ? 0 : 3,
447
+ }
215
448
  }
216
449
 
217
- function probeClaude(argv: readonly string[]): number {
218
- const version = readVersion(['claude'])
450
+ /**
451
+ * Probes the Claude bench: its version and one bounded print-mode prompt.
452
+ *
453
+ * @param command - The executable and the arguments that precede the Claude arguments, such as `['claude']`.
454
+ * @param argv - The arguments after the script path.
455
+ * @param cap - The validated cap in seconds.
456
+ * @param root - The directory the round trip runs in.
457
+ * @returns Exit 0 when live, 3 when dark, with the report to print.
458
+ */
459
+ export function probeClaude(
460
+ command: readonly string[],
461
+ argv: readonly string[],
462
+ cap: number,
463
+ root: string,
464
+ ): Probe {
465
+ const version = readVersion(command)
219
466
  const model = readOption(argv, '--model') ?? CLAUDE_MODEL
220
467
  if (version === undefined) {
221
- console.log(JSON.stringify({ bench: 'claude', version, live: false }))
222
- return 3
468
+ return { report: { bench: 'claude', version, live: false }, exit: 3 }
223
469
  }
224
- const cap = Number(readOption(argv, '--cap') ?? DEFAULT_CAP)
225
470
  const summary = runRoundTrip(
226
471
  'claude',
227
- ['claude', '-p', PROMPT, '--output-format', 'stream-json', '--verbose', '--model', model],
472
+ [...command, '-p', PROMPT, '--output-format', 'stream-json', '--verbose', '--model', model],
228
473
  cap,
474
+ root,
229
475
  )
230
476
  const live = isLive(summary)
231
- console.log(
232
- JSON.stringify({
477
+ return {
478
+ report: {
233
479
  bench: 'claude',
234
480
  version,
235
481
  model,
236
482
  ...summary,
237
483
  live,
238
484
  journal: 'tmp/claude/bench.jsonl',
239
- }),
240
- )
241
- return live ? 0 : 3
485
+ },
486
+ exit: live ? 0 : 3,
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Runs the probe the call names, against the real `codex` or `claude` command.
492
+ *
493
+ * @param argv - The arguments after the script path, which name one bench.
494
+ * @param cap - The validated cap in seconds.
495
+ * @param root - The directory the round trip runs in.
496
+ * @returns What the probe decided.
497
+ */
498
+ export function selectProbe(argv: readonly string[], cap: number, root: string): Probe {
499
+ if (argv.includes('--cursor')) return probeCursor(argv, cap, root)
500
+ if (argv.includes('--codex')) return probeCodex(['codex'], argv, cap, root)
501
+ return probeClaude(['claude'], argv, cap, root)
242
502
  }
243
503
 
244
- function main(argv: readonly string[]): number {
245
- const missing = readMissingFlags(argv, ['--model', '--cap', '--sandbox'])
504
+ /**
505
+ * Runs the script on its arguments.
506
+ *
507
+ * @param argv - The arguments after the script path.
508
+ * @returns The exit code: 0 live, 3 dark, 64 on usage.
509
+ */
510
+ export function main(argv: readonly string[]): number {
511
+ const unknown = readUnknownFlags(argv, FLAGS)
512
+ if (unknown.length > 0) {
513
+ console.error(`bench: ${unknown.join(', ')} is not a flag this script reads; ${USAGE}`)
514
+ return 64
515
+ }
516
+ const missing = readMissingFlags(argv, VALUE_FLAGS)
246
517
  if (missing.length > 0) {
247
518
  console.error(`bench: ${missing.join(', ')} given with no value`)
248
519
  return 64
249
520
  }
250
- if (argv.includes('--cursor')) return probeCursor(argv)
251
- if (argv.includes('--codex')) return probeCodex(argv)
252
- if (argv.includes('--claude')) return probeClaude(argv)
253
- console.error(
254
- 'usage: bench.ts --cursor [--resolve] [--model ID] [--cap SECONDS] | --codex [--model ID] [--sandbox MODE] [--cap SECONDS] | --claude [--model ALIAS] [--cap SECONDS]',
255
- )
256
- return 64
521
+ if (BENCH_FLAGS.filter((flag) => argv.includes(flag)).length !== 1) {
522
+ console.error(USAGE)
523
+ return 64
524
+ }
525
+ if (argv.includes('--resolve') && !argv.includes('--cursor')) {
526
+ console.error('bench: --resolve belongs to --cursor and reads nothing for another bench')
527
+ return 64
528
+ }
529
+ const sandbox = readOption(argv, '--sandbox')
530
+ if (sandbox !== undefined && !SANDBOX_MODES.includes(sandbox)) {
531
+ console.error(
532
+ `bench: --sandbox takes one of ${SANDBOX_MODES.join(', ')}; got ${quote(sandbox)}`,
533
+ )
534
+ return 64
535
+ }
536
+ const cap = parseCap(readOption(argv, '--cap'))
537
+ if (cap === undefined) {
538
+ console.error(
539
+ `bench: --cap takes a number of seconds above 0 and at most ${String(MAX_CAP_SECONDS)}`,
540
+ )
541
+ return 64
542
+ }
543
+ const probe = selectProbe(argv, cap, process.cwd())
544
+ console.log(JSON.stringify(probe.report))
545
+ return probe.exit
257
546
  }
258
547
 
259
- process.exitCode = main(process.argv.slice(2))
548
+ if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename) {
549
+ process.exitCode = main(process.argv.slice(2))
550
+ }