@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,33 +5,178 @@
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.js";
19
- const SCRIPTS = dirname(fileURLToPath(import.meta.url));
20
- // The checkout runs the `.ts` sources and a built twin under `dist/agents` runs `.js` siblings.
21
- const EXTENSION = extname(fileURLToPath(import.meta.url));
22
- const PROMPT = 'Reply with the single word READY and nothing else.';
23
- const CURSOR_MODEL = process.env.CURSOR_GROK_MODEL ?? 'grok-4.7-xhigh';
24
- const CODEX_MODEL = process.env.CODEX_ASTRA_MODEL ?? 'gpt-6-astra';
25
- const CLAUDE_MODEL = 'opus';
26
- const DEFAULT_CAP = 180;
27
- function readVersion(command) {
23
+ /** Names the directory this script runs from, which holds the launch and result siblings. */
24
+ export const SCRIPTS = dirname(fileURLToPath(import.meta.url));
25
+ /** Names the extension of the running file: the checkout runs `.ts` sources and a built twin runs `.js` siblings. */
26
+ export const EXTENSION = extname(fileURLToPath(import.meta.url));
27
+ /** Holds the one-word request every bench round trip sends. */
28
+ export const PROMPT = 'Reply with the single word READY and nothing else.';
29
+ /** Names the Cursor model a round trip uses unless the call names another. */
30
+ export const CURSOR_MODEL = process.env.CURSOR_GROK_MODEL ?? 'grok-4.7-xhigh';
31
+ /** Names the Codex model a round trip uses unless the call names another. */
32
+ export const CODEX_MODEL = process.env.CODEX_ASTRA_MODEL ?? 'gpt-6-astra';
33
+ /** Names the Claude model alias a round trip uses unless the call names another. */
34
+ export const CLAUDE_MODEL = 'opus';
35
+ /** Holds the seconds a round trip may run when the call gives no cap. */
36
+ export const DEFAULT_CAP = 180;
37
+ /** Holds the longest cap in whole seconds that a Node timer carries through the launcher. */
38
+ export const MAX_CAP_SECONDS = 2_147_483;
39
+ /** Lists the sandbox modes `--sandbox` may name, which the Codex CLI reads. */
40
+ export const SANDBOX_MODES = [
41
+ 'read-only',
42
+ 'workspace-write',
43
+ 'danger-full-access',
44
+ ];
45
+ /** Lists the flags that each select one bench. */
46
+ export const BENCH_FLAGS = ['--cursor', '--codex', '--claude'];
47
+ /** Lists the flags that take a value. */
48
+ export const VALUE_FLAGS = ['--model', '--cap', '--sandbox'];
49
+ /** Lists every flag the script reads, value flags and switches alike. */
50
+ export const FLAGS = [...BENCH_FLAGS, '--resolve', ...VALUE_FLAGS];
51
+ /** Holds the usage line printed when a call names no bench or more than one. */
52
+ export const USAGE = 'usage: bench.ts --cursor [--resolve] [--model ID] [--cap SECONDS] | --codex [--model ID] [--sandbox MODE] [--cap SECONDS] | --claude [--model ALIAS] [--cap SECONDS]; name one bench';
53
+ /**
54
+ * Reads the value that follows a flag.
55
+ *
56
+ * @remarks
57
+ * The first occurrence of a flag wins, a following value that opens with `--` is a missing value,
58
+ * and the `--flag=value` form is not read.
59
+ *
60
+ * @param argv - The arguments after the script path.
61
+ * @param name - The flag, such as `--cap`.
62
+ * @returns The value after the flag, or undefined when the flag is absent, last, or followed by
63
+ * another flag.
64
+ */
65
+ export function readOption(argv, name) {
66
+ const index = argv.indexOf(name);
67
+ const value = index === -1 ? undefined : argv[index + 1];
68
+ return value === undefined || value.startsWith('--') ? undefined : value;
69
+ }
70
+ /**
71
+ * Names the value flags a caller gave with no value.
72
+ *
73
+ * @param argv - The arguments after the script path.
74
+ * @param names - The flags that take a value.
75
+ * @returns Each named flag that appears last or is followed by another flag, in argument order.
76
+ */
77
+ export function readMissingFlags(argv, names) {
78
+ return names.filter((name) => argv.includes(name) && readOption(argv, name) === undefined);
79
+ }
80
+ /**
81
+ * Names the flags a caller gave that the script does not read.
82
+ *
83
+ * @remarks
84
+ * A `--flag=value` argument is unknown, because the script reads no such form. A bare `--` ends the
85
+ * scan.
86
+ *
87
+ * @param argv - The arguments after the script path.
88
+ * @param flags - Every flag the script reads, value flags and switches alike.
89
+ * @returns Each argument that opens with `--` and is not a read flag, in argument order.
90
+ */
91
+ export function readUnknownFlags(argv, flags) {
92
+ const end = argv.indexOf('--');
93
+ return (end === -1 ? argv : argv.slice(0, end)).filter((argument) => argument.startsWith('--') && !flags.includes(argument));
94
+ }
95
+ /**
96
+ * Quotes an untrusted text for a diagnostic, JSON-encoded and cut to a bounded length.
97
+ *
98
+ * @param text - The text to quote.
99
+ * @returns The JSON string literal of the text, ending in an ellipsis inside the quotes when cut.
100
+ */
101
+ export function quote(text) {
102
+ return JSON.stringify(text.length > 80 ? `${text.slice(0, 80)}…` : text);
103
+ }
104
+ /**
105
+ * Reads the last non-empty line of a text as a JSON object.
106
+ *
107
+ * @param text - Text whose last line a script printed as JSON.
108
+ * @returns The parsed object, or undefined when the last line is not a JSON object.
109
+ */
110
+ export function parseJsonLine(text) {
111
+ const line = text
112
+ .split(/\r\n|\n/)
113
+ .filter((candidate) => candidate.trim() !== '')
114
+ .at(-1);
115
+ if (line === undefined)
116
+ return undefined;
117
+ try {
118
+ const parsed = JSON.parse(line);
119
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
120
+ return Object.fromEntries(Object.entries(parsed));
121
+ }
122
+ }
123
+ catch { }
124
+ return undefined;
125
+ }
126
+ /**
127
+ * Reads a string member of a record.
128
+ *
129
+ * @param record - The record, or undefined.
130
+ * @param key - The member name.
131
+ * @returns The string, or undefined when the member is absent or not a string.
132
+ */
133
+ export function readString(record, key) {
134
+ const value = record?.[key];
135
+ return typeof value === 'string' ? value : undefined;
136
+ }
137
+ /**
138
+ * Reads a finite number member of a record.
139
+ *
140
+ * @param record - The record, or undefined.
141
+ * @param key - The member name.
142
+ * @returns The number, or undefined when the member is absent, not a number, or not finite.
143
+ */
144
+ export function readNumber(record, key) {
145
+ const value = record?.[key];
146
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
147
+ }
148
+ /**
149
+ * Parses the `--cap` value into a number of seconds.
150
+ *
151
+ * @param text - The value after `--cap`, or undefined when the flag is absent.
152
+ * @returns The default cap when the flag is absent, the number when it is finite, above 0, and at
153
+ * most the longest cap, and undefined otherwise.
154
+ */
155
+ export function parseCap(text) {
156
+ if (text === undefined)
157
+ return DEFAULT_CAP;
158
+ const cap = Number(text);
159
+ return Number.isFinite(cap) && cap > 0 && cap <= MAX_CAP_SECONDS ? cap : undefined;
160
+ }
161
+ /**
162
+ * Reads the version a command prints for `--version`.
163
+ *
164
+ * @param command - The executable and the arguments that precede the flag.
165
+ * @returns The trimmed output, or undefined when the command is empty, cannot start, or exits non-zero.
166
+ */
167
+ export function readVersion(command) {
28
168
  const [file, ...args] = command;
29
169
  if (file === undefined)
30
170
  return undefined;
31
171
  const result = spawnSync(file, [...args, '--version'], { encoding: 'utf8', windowsHide: true });
32
172
  return result.status === 0 ? result.stdout.trim() : undefined;
33
173
  }
34
- function resolveCursor() {
174
+ /**
175
+ * Resolves the Cursor CLI entry for this host.
176
+ *
177
+ * @returns The command and its version, or the reason the entry did not resolve.
178
+ */
179
+ export function resolveCursor() {
35
180
  if (process.platform !== 'win32') {
36
181
  return { command: ['agent'], version: readVersion(['agent']), error: undefined };
37
182
  }
@@ -57,7 +202,16 @@ function resolveCursor() {
57
202
  }
58
203
  return { command: [node, index], version: readVersion([node, index]), error: undefined };
59
204
  }
60
- function runRoundTrip(bench, command, cap) {
205
+ /**
206
+ * Runs one bounded round trip through the launch and result scripts beside this one.
207
+ *
208
+ * @param bench - The bench whose journal, errors, and answer files receive the run.
209
+ * @param command - The bench command with its arguments and prompt.
210
+ * @param cap - The seconds the launcher allows the command.
211
+ * @param root - The directory the children run in, which holds the `tmp/<bench>` files.
212
+ * @returns What the launcher and the result reader reported.
213
+ */
214
+ export function runRoundTrip(bench, command, cap, root) {
61
215
  const journal = `tmp/${bench}/bench.jsonl`;
62
216
  const errors = `tmp/${bench}/bench.err`;
63
217
  const answer = `tmp/${bench}/bench-answer.md`;
@@ -71,37 +225,54 @@ function runRoundTrip(bench, command, cap) {
71
225
  String(cap),
72
226
  '--',
73
227
  ...command,
74
- ], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit'], windowsHide: true });
75
- const launched = readJsonObject(launch.stdout);
76
- const result = spawnSync(process.execPath, [join(SCRIPTS, `result${EXTENSION}`), `--${bench}`, journal, '--out', answer], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit'], windowsHide: true });
77
- const read = readJsonObject(result.stdout);
228
+ ], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit'], windowsHide: true });
229
+ const launched = parseJsonLine(launch.stdout);
230
+ const result = spawnSync(process.execPath, [join(SCRIPTS, `result${EXTENSION}`), `--${bench}`, journal, '--out', answer], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit'], windowsHide: true });
231
+ const read = parseJsonLine(result.stdout);
232
+ const answerPath = join(root, answer);
78
233
  return {
79
234
  session: readString(read, 'session'),
80
- answer: existsSync(answer) ? readFileSync(answer, 'utf8').trim().slice(0, 200) : undefined,
235
+ answer: existsSync(answerPath)
236
+ ? readFileSync(answerPath, 'utf8').trim().slice(0, 200)
237
+ : undefined,
81
238
  exit: readNumber(launched, 'exit'),
82
239
  capped: launched?.capped === true,
83
240
  durationMs: readNumber(launched, 'durationMs'),
84
241
  };
85
242
  }
86
- function isLive(summary) {
243
+ /**
244
+ * Checks whether a round trip shows a live bench.
245
+ *
246
+ * @param summary - What the round trip observed.
247
+ * @returns True if the run has a session, exited 0 uncapped, and answered READY; false otherwise.
248
+ */
249
+ export function isLive(summary) {
87
250
  return (summary.session !== undefined &&
88
251
  summary.exit === 0 &&
89
252
  !summary.capped &&
90
253
  summary.answer !== undefined &&
91
254
  /^READY\.?$/u.test(summary.answer.trim()));
92
255
  }
93
- function probeCursor(argv) {
256
+ /**
257
+ * Probes the Cursor bench, or prints its entry when the call names `--resolve`.
258
+ *
259
+ * @param argv - The arguments after the script path.
260
+ * @param cap - The validated cap in seconds.
261
+ * @param root - The directory the round trip runs in.
262
+ * @returns Exit 0 when live or resolved, 3 when dark, with the report to print.
263
+ */
264
+ export function probeCursor(argv, cap, root) {
94
265
  const entry = resolveCursor();
95
266
  const model = readOption(argv, '--model') ?? CURSOR_MODEL;
96
267
  if (entry.error !== undefined || entry.version === undefined) {
97
- console.log(JSON.stringify({ bench: 'cursor', ...entry, live: false }));
98
- return 3;
268
+ return { report: { bench: 'cursor', ...entry, live: false }, exit: 3 };
99
269
  }
100
270
  if (argv.includes('--resolve')) {
101
- console.log(JSON.stringify({ bench: 'cursor', command: entry.command, version: entry.version }));
102
- return 0;
271
+ return {
272
+ report: { bench: 'cursor', command: entry.command, version: entry.version },
273
+ exit: 0,
274
+ };
103
275
  }
104
- const cap = Number(readOption(argv, '--cap') ?? DEFAULT_CAP);
105
276
  const summary = runRoundTrip('cursor', [
106
277
  ...entry.command,
107
278
  '-p',
@@ -112,37 +283,49 @@ function probeCursor(argv) {
112
283
  '--output-format',
113
284
  'stream-json',
114
285
  PROMPT,
115
- ], cap);
286
+ ], cap, root);
116
287
  const live = isLive(summary);
117
- console.log(JSON.stringify({
118
- bench: 'cursor',
119
- command: entry.command,
120
- version: entry.version,
121
- model,
122
- ...summary,
123
- live,
124
- journal: 'tmp/cursor/bench.jsonl',
125
- }));
126
- return live ? 0 : 3;
127
- }
128
- function probeCodex(argv) {
129
- const version = readVersion(['codex']);
130
- const login = spawnSync('codex', ['login', 'status'], { encoding: 'utf8', windowsHide: true });
288
+ return {
289
+ report: {
290
+ bench: 'cursor',
291
+ command: entry.command,
292
+ version: entry.version,
293
+ model,
294
+ ...summary,
295
+ live,
296
+ journal: 'tmp/cursor/bench.jsonl',
297
+ },
298
+ exit: live ? 0 : 3,
299
+ };
300
+ }
301
+ /**
302
+ * Probes the Codex bench: its version, its login, and one bounded exec.
303
+ *
304
+ * @param command - The executable and the arguments that precede the Codex arguments, such as `['codex']`.
305
+ * @param argv - The arguments after the script path.
306
+ * @param cap - The validated cap in seconds.
307
+ * @param root - The directory the round trip runs in.
308
+ * @returns Exit 0 when live, 3 when dark, with the report to print.
309
+ */
310
+ export function probeCodex(command, argv, cap, root) {
311
+ const [file, ...prefix] = command;
312
+ const version = readVersion(command);
313
+ const login = file === undefined
314
+ ? undefined
315
+ : spawnSync(file, [...prefix, 'login', 'status'], { encoding: 'utf8', windowsHide: true });
131
316
  const model = readOption(argv, '--model') ?? CODEX_MODEL;
132
317
  const sandbox = readOption(argv, '--sandbox') ??
133
318
  (process.platform === 'win32' ? 'danger-full-access' : 'read-only');
134
- const status = `${login.stdout}${login.stderr}`.trim();
135
- if (version === undefined || login.status !== 0) {
136
- console.log(JSON.stringify({ bench: 'codex', version, login: status, live: false }));
137
- return 3;
319
+ const status = login === undefined ? '' : `${login.stdout ?? ''}${login.stderr ?? ''}`.trim();
320
+ if (version === undefined || login?.status !== 0) {
321
+ return { report: { bench: 'codex', version, login: status, live: false }, exit: 3 };
138
322
  }
139
- const cap = Number(readOption(argv, '--cap') ?? DEFAULT_CAP);
140
323
  const summary = runRoundTrip('codex', [
141
- 'codex',
324
+ ...command,
142
325
  'exec',
143
326
  '--json',
144
327
  '-C',
145
- process.cwd(),
328
+ root,
146
329
  '--sandbox',
147
330
  sandbox,
148
331
  '--model',
@@ -152,53 +335,105 @@ function probeCodex(argv) {
152
335
  '--output-last-message',
153
336
  'tmp/codex/bench-last.md',
154
337
  PROMPT,
155
- ], cap);
338
+ ], cap, root);
156
339
  const live = isLive(summary);
157
- console.log(JSON.stringify({
158
- bench: 'codex',
159
- version,
160
- login: status,
161
- model,
162
- sandbox,
163
- ...summary,
164
- live,
165
- journal: 'tmp/codex/bench.jsonl',
166
- }));
167
- return live ? 0 : 3;
168
- }
169
- function probeClaude(argv) {
170
- const version = readVersion(['claude']);
340
+ return {
341
+ report: {
342
+ bench: 'codex',
343
+ version,
344
+ login: status,
345
+ model,
346
+ sandbox,
347
+ ...summary,
348
+ live,
349
+ journal: 'tmp/codex/bench.jsonl',
350
+ },
351
+ exit: live ? 0 : 3,
352
+ };
353
+ }
354
+ /**
355
+ * Probes the Claude bench: its version and one bounded print-mode prompt.
356
+ *
357
+ * @param command - The executable and the arguments that precede the Claude arguments, such as `['claude']`.
358
+ * @param argv - The arguments after the script path.
359
+ * @param cap - The validated cap in seconds.
360
+ * @param root - The directory the round trip runs in.
361
+ * @returns Exit 0 when live, 3 when dark, with the report to print.
362
+ */
363
+ export function probeClaude(command, argv, cap, root) {
364
+ const version = readVersion(command);
171
365
  const model = readOption(argv, '--model') ?? CLAUDE_MODEL;
172
366
  if (version === undefined) {
173
- console.log(JSON.stringify({ bench: 'claude', version, live: false }));
174
- return 3;
367
+ return { report: { bench: 'claude', version, live: false }, exit: 3 };
175
368
  }
176
- const cap = Number(readOption(argv, '--cap') ?? DEFAULT_CAP);
177
- const summary = runRoundTrip('claude', ['claude', '-p', PROMPT, '--output-format', 'stream-json', '--verbose', '--model', model], cap);
369
+ const summary = runRoundTrip('claude', [...command, '-p', PROMPT, '--output-format', 'stream-json', '--verbose', '--model', model], cap, root);
178
370
  const live = isLive(summary);
179
- console.log(JSON.stringify({
180
- bench: 'claude',
181
- version,
182
- model,
183
- ...summary,
184
- live,
185
- journal: 'tmp/claude/bench.jsonl',
186
- }));
187
- return live ? 0 : 3;
188
- }
189
- function main(argv) {
190
- const missing = readMissingFlags(argv, ['--model', '--cap', '--sandbox']);
371
+ return {
372
+ report: {
373
+ bench: 'claude',
374
+ version,
375
+ model,
376
+ ...summary,
377
+ live,
378
+ journal: 'tmp/claude/bench.jsonl',
379
+ },
380
+ exit: live ? 0 : 3,
381
+ };
382
+ }
383
+ /**
384
+ * Runs the probe the call names, against the real `codex` or `claude` command.
385
+ *
386
+ * @param argv - The arguments after the script path, which name one bench.
387
+ * @param cap - The validated cap in seconds.
388
+ * @param root - The directory the round trip runs in.
389
+ * @returns What the probe decided.
390
+ */
391
+ export function selectProbe(argv, cap, root) {
392
+ if (argv.includes('--cursor'))
393
+ return probeCursor(argv, cap, root);
394
+ if (argv.includes('--codex'))
395
+ return probeCodex(['codex'], argv, cap, root);
396
+ return probeClaude(['claude'], argv, cap, root);
397
+ }
398
+ /**
399
+ * Runs the script on its arguments.
400
+ *
401
+ * @param argv - The arguments after the script path.
402
+ * @returns The exit code: 0 live, 3 dark, 64 on usage.
403
+ */
404
+ export function main(argv) {
405
+ const unknown = readUnknownFlags(argv, FLAGS);
406
+ if (unknown.length > 0) {
407
+ console.error(`bench: ${unknown.join(', ')} is not a flag this script reads; ${USAGE}`);
408
+ return 64;
409
+ }
410
+ const missing = readMissingFlags(argv, VALUE_FLAGS);
191
411
  if (missing.length > 0) {
192
412
  console.error(`bench: ${missing.join(', ')} given with no value`);
193
413
  return 64;
194
414
  }
195
- if (argv.includes('--cursor'))
196
- return probeCursor(argv);
197
- if (argv.includes('--codex'))
198
- return probeCodex(argv);
199
- if (argv.includes('--claude'))
200
- return probeClaude(argv);
201
- console.error('usage: bench.ts --cursor [--resolve] [--model ID] [--cap SECONDS] | --codex [--model ID] [--sandbox MODE] [--cap SECONDS] | --claude [--model ALIAS] [--cap SECONDS]');
202
- return 64;
415
+ if (BENCH_FLAGS.filter((flag) => argv.includes(flag)).length !== 1) {
416
+ console.error(USAGE);
417
+ return 64;
418
+ }
419
+ if (argv.includes('--resolve') && !argv.includes('--cursor')) {
420
+ console.error('bench: --resolve belongs to --cursor and reads nothing for another bench');
421
+ return 64;
422
+ }
423
+ const sandbox = readOption(argv, '--sandbox');
424
+ if (sandbox !== undefined && !SANDBOX_MODES.includes(sandbox)) {
425
+ console.error(`bench: --sandbox takes one of ${SANDBOX_MODES.join(', ')}; got ${quote(sandbox)}`);
426
+ return 64;
427
+ }
428
+ const cap = parseCap(readOption(argv, '--cap'));
429
+ if (cap === undefined) {
430
+ console.error(`bench: --cap takes a number of seconds above 0 and at most ${String(MAX_CAP_SECONDS)}`);
431
+ return 64;
432
+ }
433
+ const probe = selectProbe(argv, cap, process.cwd());
434
+ console.log(JSON.stringify(probe.report));
435
+ return probe.exit;
436
+ }
437
+ if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename) {
438
+ process.exitCode = main(process.argv.slice(2));
203
439
  }
204
- process.exitCode = main(process.argv.slice(2));
@@ -8,19 +8,76 @@
8
8
  // edited. The second form reads every backticked token carrying a slash (a backslash counts and is
9
9
  // read as a slash), strips a `:line` or
10
10
  // `:line-line` suffix, resolves it from the working directory, and prints the ones that do not
11
- // exist; a bare filename is outside the check. Exit 3 when any is missing, 64 on usage.
12
- import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
11
+ // exist; a bare filename is outside the check. The two forms are two modes; name one. The script
12
+ // refuses a flag it does not read, the `--flag=value` form included. Exit 3 when any is missing, 64
13
+ // on usage.
14
+ import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
13
15
  import { dirname, join, resolve } from 'node:path';
14
16
  import { fileURLToPath } from 'node:url';
15
- import { readMissingFlags, readOption } from "./helpers.js";
16
- const TEMPLATE = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'templates', 'brief.md');
17
- const LANES = Object.freeze({
17
+ /** Names the brief template, found from this script's own location. */
18
+ export const TEMPLATE = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'templates', 'brief.md');
19
+ /** Maps each lane name to the directory its briefs are written under. */
20
+ export const LANES = Object.freeze({
18
21
  units: 'tmp/units',
19
22
  cursor: 'tmp/cursor',
20
23
  codex: 'tmp/codex',
21
24
  claude: 'tmp/claude',
22
25
  });
23
- function readsAsPath(token) {
26
+ /** Lists the flags that take a value. */
27
+ export const VALUE_FLAGS = ['--unit', '--lane', '--subject', '--check'];
28
+ /** Lists every flag the script reads, value flags and switches alike. */
29
+ export const FLAGS = VALUE_FLAGS;
30
+ /** Holds the usage line printed when a call names no mode. */
31
+ export const USAGE = 'usage: brief.ts --unit UNIT --lane units|cursor|codex|claude [--subject TEXT] | --check PATH';
32
+ /**
33
+ * Reads the value that follows a flag.
34
+ *
35
+ * @remarks
36
+ * The first occurrence of a flag wins, a following value that opens with `--` is a missing value,
37
+ * and the `--flag=value` form is not read.
38
+ *
39
+ * @param argv - The arguments after the script path.
40
+ * @param name - The flag, such as `--unit`.
41
+ * @returns The value after the flag, or undefined when the flag is absent, last, or followed by
42
+ * another flag.
43
+ */
44
+ export function readOption(argv, name) {
45
+ const index = argv.indexOf(name);
46
+ const value = index === -1 ? undefined : argv[index + 1];
47
+ return value === undefined || value.startsWith('--') ? undefined : value;
48
+ }
49
+ /**
50
+ * Names the value flags a caller gave with no value.
51
+ *
52
+ * @param argv - The arguments after the script path.
53
+ * @param names - The flags that take a value.
54
+ * @returns Each named flag that appears last or is followed by another flag, in argument order.
55
+ */
56
+ export function readMissingFlags(argv, names) {
57
+ return names.filter((name) => argv.includes(name) && readOption(argv, name) === undefined);
58
+ }
59
+ /**
60
+ * Names the flags a caller gave that the script does not read.
61
+ *
62
+ * @remarks
63
+ * A `--flag=value` argument is unknown, because the script reads no such form. A bare `--` ends the
64
+ * scan.
65
+ *
66
+ * @param argv - The arguments after the script path.
67
+ * @param flags - Every flag the script reads, value flags and switches alike.
68
+ * @returns Each argument that opens with `--` and is not a read flag, in argument order.
69
+ */
70
+ export function readUnknownFlags(argv, flags) {
71
+ const end = argv.indexOf('--');
72
+ return (end === -1 ? argv : argv.slice(0, end)).filter((argument) => argument.startsWith('--') && !flags.includes(argument));
73
+ }
74
+ /**
75
+ * Checks whether a backticked token reads as a repository path.
76
+ *
77
+ * @param token - The text between two backticks, with its line suffix already removed.
78
+ * @returns True if the token carries a slash and no URL, placeholder, glob, or flag form; false otherwise.
79
+ */
80
+ export function readsAsPath(token) {
24
81
  if (token.includes('://') || /[<>*{}\s]/u.test(token))
25
82
  return false;
26
83
  if (/[A-Z]{2,}_[A-Z]/u.test(token))
@@ -31,17 +88,37 @@ function readsAsPath(token) {
31
88
  return false;
32
89
  return token.includes('/');
33
90
  }
34
- function extractPaths(content) {
91
+ /**
92
+ * Extracts the slash-bearing paths a brief names.
93
+ *
94
+ * @param content - The brief text.
95
+ * @returns Each distinct path in sorted order, a backslash read as a slash and a line suffix removed.
96
+ */
97
+ export function extractPaths(content) {
35
98
  const found = new Set([...content.matchAll(/`([^`\n]+)`/gu)]
36
99
  .map((match) => (match[1] ?? '').replace(/\\/gu, '/').replace(/:\d+(?:[-–]\d+)?$/u, ''))
37
100
  .filter(readsAsPath));
38
101
  return [...found].sort();
39
102
  }
40
- function resolveBriefPath(directory, unit, count = 1) {
103
+ /**
104
+ * Resolves the first brief path for a unit that no file occupies.
105
+ *
106
+ * @param directory - The lane directory.
107
+ * @param unit - The unit name.
108
+ * @param count - The brief number to try first. Default: 1.
109
+ * @returns `<unit>-brief.md` for the first brief and `<unit>-brief-<n>.md` for a successor.
110
+ */
111
+ export function resolveBriefPath(directory, unit, count = 1) {
41
112
  const path = join(directory, count === 1 ? `${unit}-brief.md` : `${unit}-brief-${count}.md`);
42
113
  return existsSync(path) ? resolveBriefPath(directory, unit, count + 1) : path;
43
114
  }
44
- function writeBrief(argv) {
115
+ /**
116
+ * Writes a brief from the template.
117
+ *
118
+ * @param argv - The arguments after the script path.
119
+ * @returns The exit code: 0 on success, 64 on usage or a missing template.
120
+ */
121
+ export function writeBrief(argv) {
45
122
  const unit = readOption(argv, '--unit');
46
123
  const lane = readOption(argv, '--lane');
47
124
  const directory = lane === undefined ? undefined : LANES[lane];
@@ -63,7 +140,13 @@ function writeBrief(argv) {
63
140
  console.log(JSON.stringify({ brief: path, template: TEMPLATE }));
64
141
  return 0;
65
142
  }
66
- function checkBrief(path) {
143
+ /**
144
+ * Checks that every path a brief names exists.
145
+ *
146
+ * @param path - The brief file.
147
+ * @returns The exit code: 0 when every path exists, 3 when one is missing, 64 when the brief is not a file.
148
+ */
149
+ export function checkBrief(path) {
67
150
  if (!existsSync(path) || !statSync(path).isFile()) {
68
151
  console.error(`brief: ${path} is not a file`);
69
152
  return 64;
@@ -73,8 +156,19 @@ function checkBrief(path) {
73
156
  console.log(JSON.stringify({ brief: path, checked: paths.length, missing }));
74
157
  return missing.length === 0 ? 0 : 3;
75
158
  }
76
- function main(argv) {
77
- const missing = readMissingFlags(argv, ['--unit', '--lane', '--subject', '--check']);
159
+ /**
160
+ * Runs the script on its arguments.
161
+ *
162
+ * @param argv - The arguments after the script path.
163
+ * @returns The exit code: 0 on success, 3 when a named path is missing, 64 on usage.
164
+ */
165
+ export function main(argv) {
166
+ const unknown = readUnknownFlags(argv, FLAGS);
167
+ if (unknown.length > 0) {
168
+ console.error(`brief: ${unknown.join(', ')} is not a flag this script reads; ${USAGE}`);
169
+ return 64;
170
+ }
171
+ const missing = readMissingFlags(argv, VALUE_FLAGS);
78
172
  if (missing.length > 0) {
79
173
  console.error(`brief: ${missing.join(', ')} given with no value`);
80
174
  return 64;
@@ -88,7 +182,9 @@ function main(argv) {
88
182
  return checkBrief(check);
89
183
  if (argv.includes('--unit'))
90
184
  return writeBrief(argv);
91
- console.error('usage: brief.ts --unit UNIT --lane units|cursor|codex|claude [--subject TEXT] | --check PATH');
185
+ console.error(USAGE);
92
186
  return 64;
93
187
  }
94
- process.exitCode = main(process.argv.slice(2));
188
+ if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename) {
189
+ process.exitCode = main(process.argv.slice(2));
190
+ }