@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
@@ -1,40 +1,201 @@
1
1
  // Recover a bench login. Run from the checkout root:
2
2
  // node .agents/skills/orkestrel-dispatch/scripts/login.ts --codex [--cap SECONDS]
3
3
  // node .agents/skills/orkestrel-dispatch/scripts/login.ts --claude
4
- // --codex reads `codex login status`; when it reports no login, the script runs
5
- // `codex login --device-auth` with its output journaled to tmp/codex/login.log, prints the
6
- // verification URL and the one-time code the moment the journal carries them, polls
7
- // `codex login status` every 5 seconds until it answers or the cap (default 600 seconds) ends, then
8
- // kills the login by its pid. --claude reads `claude auth status` and prints it. The JSON summary
9
- // carries `live`; exit 0 when the bench is logged in, 3 when it is not, 64 on usage.
4
+ // --codex reads `codex login status`; a Codex that cannot start reports live false at once. When the
5
+ // status reports no login, the script runs `codex login --device-auth` with its output journaled to
6
+ // tmp/codex/login.log, prints the verification URL and the one-time code the moment the journal
7
+ // carries them, polls `codex login status` every 5 seconds until it answers or the cap (default 600
8
+ // seconds) ends, then kills the login by its pid. Each status read gets the time left to the cap
9
+ // and is ended when it passes. --cap is a number of seconds above 0 and at most 2147483, checked
10
+ // before anything starts.
11
+ // --claude reads `claude auth status` and prints it. Name one bench per run; the script refuses a
12
+ // flag it does not read, the `--flag=value` form included. The JSON summary carries `live`; exit 0
13
+ // when the bench is logged in, 3 when it is not, 64 on usage.
10
14
  import { spawn, spawnSync } from 'node:child_process'
11
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync } from 'node:fs'
12
- import { readMissingFlags, readOption } from './helpers.ts'
15
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, realpathSync } from 'node:fs'
16
+ import { dirname } from 'node:path'
13
17
 
14
- const JOURNAL = 'tmp/codex/login.log'
15
- const POLL_MS = 5_000
16
- const URL_PATTERN = /https?:\/\/[^\s"'<>]+/u
17
- const CODE_PATTERN = /\b([A-Z0-9]{4,5}-[A-Z0-9]{4,5})\b/u
18
+ /** Names the file that journals the device-auth login output. */
19
+ export const JOURNAL = 'tmp/codex/login.log'
18
20
 
19
- interface Status {
21
+ /** Holds the milliseconds between two reads of the login status. */
22
+ export const POLL_MS = 5_000
23
+
24
+ /** Holds the seconds the poll runs when the call gives no cap. */
25
+ export const DEFAULT_CAP = 600
26
+
27
+ /** Holds the longest cap in whole seconds, so the cap in milliseconds stays a finite timer value. */
28
+ export const MAX_CAP_SECONDS = 2_147_483
29
+
30
+ /** Holds the fewest milliseconds one status read gets, so a read that starts at the cap can still answer. */
31
+ export const STATUS_GRACE_MS = 1_000
32
+
33
+ /** Matches the verification URL the login prints. */
34
+ export const URL_PATTERN = /https?:\/\/[^\s"'<>]+/u
35
+
36
+ /** Matches the one-time code the login prints, such as `ABCD-12345`. */
37
+ export const CODE_PATTERN = /\b([A-Z0-9]{4,5}-[A-Z0-9]{4,5})\b/u
38
+
39
+ /** Lists the flags that take a value. */
40
+ export const VALUE_FLAGS: readonly string[] = ['--cap']
41
+
42
+ /** Lists every flag the script reads, value flags and switches alike. */
43
+ export const FLAGS: readonly string[] = [...VALUE_FLAGS, '--codex', '--claude']
44
+
45
+ /** Describes whether a bench's status command started, whether it reports a login, and its text. */
46
+ export interface Status {
47
+ readonly started: boolean
20
48
  readonly live: boolean
21
49
  readonly text: string
22
50
  }
23
51
 
24
- interface Poll {
52
+ /** Describes what one poll of the login saw: the URL, the code, and whether the bench is logged in. */
53
+ export interface Poll {
25
54
  readonly url: string | undefined
26
55
  readonly code: string | undefined
27
56
  readonly live: boolean
28
57
  }
29
58
 
30
- function readStatus(command: string, args: readonly string[]): Status {
31
- const result = spawnSync(command, args, { encoding: 'utf8', windowsHide: true })
32
- if (result.error !== undefined)
33
- return { live: false, text: `${command} could not start: ${result.error.message}` }
34
- return { live: result.status === 0, text: `${result.stdout ?? ''}${result.stderr ?? ''}`.trim() }
59
+ /** Describes a command to run: the executable and the arguments it takes. */
60
+ export interface Invocation {
61
+ readonly command: string
62
+ readonly args: readonly string[]
63
+ }
64
+
65
+ /** Describes the commands and files a Codex login drives, and how often it polls. */
66
+ export interface Flow {
67
+ readonly status: Invocation
68
+ readonly device: Invocation
69
+ readonly journal: string
70
+ readonly intervalMs: number
71
+ }
72
+
73
+ /** Describes what a login decided: the JSON report to print and the exit code to return. */
74
+ export interface Outcome {
75
+ readonly report: Record<string, unknown>
76
+ readonly exit: number
77
+ }
78
+
79
+ /** Holds the real Codex commands and the journal and interval the script uses. */
80
+ export const CODEX_FLOW: Flow = {
81
+ status: { command: 'codex', args: ['login', 'status'] },
82
+ device: { command: 'codex', args: ['login', '--device-auth'] },
83
+ journal: JOURNAL,
84
+ intervalMs: POLL_MS,
85
+ }
86
+
87
+ /** Holds the real Claude status command. */
88
+ export const CLAUDE_STATUS: Invocation = { command: 'claude', args: ['auth', 'status'] }
89
+
90
+ /**
91
+ * Reads the value that follows a flag.
92
+ *
93
+ * @remarks
94
+ * The first occurrence of a flag wins, a following value that opens with `--` is a missing value,
95
+ * and the `--flag=value` form is not read.
96
+ *
97
+ * @param argv - The arguments after the script path.
98
+ * @param name - The flag, such as `--cap`.
99
+ * @returns The value after the flag, or undefined when the flag is absent, last, or followed by
100
+ * another flag.
101
+ */
102
+ export function readOption(argv: readonly string[], name: string): string | undefined {
103
+ const index = argv.indexOf(name)
104
+ const value = index === -1 ? undefined : argv[index + 1]
105
+ return value === undefined || value.startsWith('--') ? undefined : value
106
+ }
107
+
108
+ /**
109
+ * Names the value flags a caller gave with no value.
110
+ *
111
+ * @param argv - The arguments after the script path.
112
+ * @param names - The flags that take a value.
113
+ * @returns Each named flag that appears last or is followed by another flag, in argument order.
114
+ */
115
+ export function readMissingFlags(
116
+ argv: readonly string[],
117
+ names: readonly string[],
118
+ ): readonly string[] {
119
+ return names.filter((name) => argv.includes(name) && readOption(argv, name) === undefined)
120
+ }
121
+
122
+ /**
123
+ * Names the flags a caller gave that the script does not read.
124
+ *
125
+ * @remarks
126
+ * A `--flag=value` argument is unknown, because the script reads no such form. A bare `--` ends the
127
+ * scan.
128
+ *
129
+ * @param argv - The arguments after the script path.
130
+ * @param flags - Every flag the script reads, value flags and switches alike.
131
+ * @returns Each argument that opens with `--` and is not a read flag, in argument order.
132
+ */
133
+ export function readUnknownFlags(
134
+ argv: readonly string[],
135
+ flags: readonly string[],
136
+ ): readonly string[] {
137
+ const end = argv.indexOf('--')
138
+ return (end === -1 ? argv : argv.slice(0, end)).filter(
139
+ (argument) => argument.startsWith('--') && !flags.includes(argument),
140
+ )
35
141
  }
36
142
 
37
- function killTree(pid: number): void {
143
+ /**
144
+ * Parses the `--cap` value into a number of seconds.
145
+ *
146
+ * @param text - The value after `--cap`, or undefined when the flag is absent.
147
+ * @returns The default cap when the flag is absent, the number when it is finite, above 0, and at
148
+ * most the longest cap, and undefined otherwise.
149
+ */
150
+ export function parseCap(text: string | undefined): number | undefined {
151
+ if (text === undefined) return DEFAULT_CAP
152
+ const cap = Number(text)
153
+ return Number.isFinite(cap) && cap > 0 && cap <= MAX_CAP_SECONDS ? cap : undefined
154
+ }
155
+
156
+ /**
157
+ * Runs a status command and reads whether it reports a login.
158
+ *
159
+ * @param command - The executable.
160
+ * @param args - The status arguments.
161
+ * @param timeoutMs - The milliseconds the command may run before it is ended, above 0; omit it for no
162
+ * bound.
163
+ * @returns Live when the command exits 0, with the text it printed, the reason it could not start,
164
+ * or the note that it did not answer in time.
165
+ */
166
+ export function readLoginStatus(
167
+ command: string,
168
+ args: readonly string[],
169
+ timeoutMs?: number,
170
+ ): Status {
171
+ const result = spawnSync(command, args, {
172
+ encoding: 'utf8',
173
+ windowsHide: true,
174
+ timeout: timeoutMs,
175
+ })
176
+ if (result.error !== undefined) {
177
+ if ('code' in result.error && result.error.code === 'ETIMEDOUT') {
178
+ return { started: true, live: false, text: `${command} did not answer within the cap` }
179
+ }
180
+ return {
181
+ started: false,
182
+ live: false,
183
+ text: `${command} could not start: ${result.error.message}`,
184
+ }
185
+ }
186
+ return {
187
+ started: true,
188
+ live: result.status === 0,
189
+ text: `${result.stdout ?? ''}${result.stderr ?? ''}`.trim(),
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Asks a process and the processes it started to end.
195
+ *
196
+ * @param pid - The process to end. A process that is already gone is not an error.
197
+ */
198
+ export function terminateTree(pid: number): void {
38
199
  if (process.platform === 'win32') {
39
200
  spawnSync('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore' })
40
201
  return
@@ -44,71 +205,126 @@ function killTree(pid: number): void {
44
205
  } catch {}
45
206
  }
46
207
 
47
- function waitFor(ms: number): Promise<void> {
208
+ /**
209
+ * Waits a number of milliseconds.
210
+ *
211
+ * @param ms - The milliseconds to wait.
212
+ * @returns A promise that resolves when the time has passed.
213
+ */
214
+ export function waitFor(ms: number): Promise<void> {
48
215
  return new Promise((resolve) => {
49
216
  setTimeout(resolve, ms)
50
217
  })
51
218
  }
52
219
 
53
- async function pollLogin(
220
+ /**
221
+ * Polls the login journal and the login status until the bench is logged in or the cap ends.
222
+ *
223
+ * @param started - The time the login began, in milliseconds since the epoch.
224
+ * @param capSeconds - The seconds the poll may run.
225
+ * @param flow - The status command, the journal to read, and the milliseconds between polls.
226
+ * @param announce - Receives each line the poll tells the user, such as the URL to open.
227
+ * @param url - The verification URL already read, if any.
228
+ * @param code - The one-time code already read, if any.
229
+ * @returns The URL, the code, and whether the bench is logged in.
230
+ */
231
+ export async function pollLogin(
54
232
  started: number,
55
233
  capSeconds: number,
234
+ flow: Flow,
235
+ announce: (line: string) => void,
56
236
  url?: string,
57
237
  code?: string,
58
238
  ): Promise<Poll> {
59
- if (Date.now() - started >= capSeconds * 1000) return { url, code, live: false }
60
- await waitFor(POLL_MS)
61
- const text = existsSync(JOURNAL) ? readFileSync(JOURNAL, 'utf8') : ''
239
+ const deadline = started + capSeconds * 1000
240
+ if (Date.now() >= deadline) return { url, code, live: false }
241
+ await waitFor(flow.intervalMs)
242
+ const text = existsSync(flow.journal) ? readFileSync(flow.journal, 'utf8') : ''
62
243
  const nextUrl = url ?? text.match(URL_PATTERN)?.[0]
63
- if (url === undefined && nextUrl !== undefined) console.error(`login: open ${nextUrl}`)
244
+ if (url === undefined && nextUrl !== undefined) announce(`login: open ${nextUrl}`)
64
245
  const nextCode = code ?? text.match(CODE_PATTERN)?.[1]
65
- if (code === undefined && nextCode !== undefined) {
66
- console.error(`login: enter the code ${nextCode}`)
67
- }
68
- if (readStatus('codex', ['login', 'status']).live) {
246
+ if (code === undefined && nextCode !== undefined) announce(`login: enter the code ${nextCode}`)
247
+ const remaining = Math.max(deadline - Date.now(), STATUS_GRACE_MS)
248
+ if (readLoginStatus(flow.status.command, flow.status.args, remaining).live) {
69
249
  return { url: nextUrl, code: nextCode, live: true }
70
250
  }
71
- return pollLogin(started, capSeconds, nextUrl, nextCode)
251
+ return pollLogin(started, capSeconds, flow, announce, nextUrl, nextCode)
72
252
  }
73
253
 
74
- async function loginCodex(capSeconds: number): Promise<number> {
75
- const before = readStatus('codex', ['login', 'status'])
76
- if (before.live) {
77
- console.log(JSON.stringify({ bench: 'codex', live: true, status: before.text }))
78
- return 0
254
+ /**
255
+ * Recovers the Codex login with the device-auth flow.
256
+ *
257
+ * @param capSeconds - The seconds the poll may run.
258
+ * @param flow - The status and device-auth commands, the journal, and the poll interval.
259
+ * @param announce - Receives each line the poll tells the user, such as the URL to open.
260
+ * @returns Exit 0 when logged in, 3 when not, with the report to print.
261
+ */
262
+ export async function loginCodex(
263
+ capSeconds: number,
264
+ flow: Flow,
265
+ announce: (line: string) => void,
266
+ ): Promise<Outcome> {
267
+ const before = readLoginStatus(flow.status.command, flow.status.args, capSeconds * 1000)
268
+ // A Codex that cannot start cannot log in either, so the device-auth login never spawns.
269
+ if (before.live || !before.started) {
270
+ return {
271
+ report: { bench: 'codex', live: before.live, status: before.text },
272
+ exit: before.live ? 0 : 3,
273
+ }
79
274
  }
80
- mkdirSync('tmp/codex', { recursive: true })
81
- const journal = openSync(JOURNAL, 'w')
275
+ mkdirSync(dirname(flow.journal), { recursive: true })
276
+ const journal = openSync(flow.journal, 'w')
82
277
  const started = Date.now()
83
- const child = spawn('codex', ['login', '--device-auth'], {
278
+ const child = spawn(flow.device.command, flow.device.args, {
84
279
  stdio: ['ignore', journal, journal],
85
280
  windowsHide: true,
86
281
  })
87
- const { url, code, live } = await pollLogin(started, capSeconds)
88
- if (child.exitCode === null && child.pid !== undefined) killTree(child.pid)
282
+ const { url, code, live } = await pollLogin(started, capSeconds, flow, announce)
283
+ if (child.exitCode === null && child.pid !== undefined) terminateTree(child.pid)
89
284
  closeSync(journal)
90
- console.log(
91
- JSON.stringify({
285
+ return {
286
+ report: {
92
287
  bench: 'codex',
93
288
  live,
94
289
  url,
95
290
  code,
96
291
  durationMs: Date.now() - started,
97
- journal: JOURNAL,
98
- status: readStatus('codex', ['login', 'status']).text,
99
- }),
100
- )
101
- return live ? 0 : 3
292
+ journal: flow.journal,
293
+ status: readLoginStatus(flow.status.command, flow.status.args, STATUS_GRACE_MS).text,
294
+ },
295
+ exit: live ? 0 : 3,
296
+ }
102
297
  }
103
298
 
104
- function loginClaude(): number {
105
- const status = readStatus('claude', ['auth', 'status'])
106
- console.log(JSON.stringify({ bench: 'claude', live: status.live, status: status.text }))
107
- return status.live ? 0 : 3
299
+ /**
300
+ * Reads the Claude login status.
301
+ *
302
+ * @param status - The command that reports the Claude login.
303
+ * @returns Exit 0 when logged in, 3 when not, with the report to print.
304
+ */
305
+ export function loginClaude(status: Invocation): Outcome {
306
+ const read = readLoginStatus(status.command, status.args)
307
+ return {
308
+ report: { bench: 'claude', live: read.live, status: read.text },
309
+ exit: read.live ? 0 : 3,
310
+ }
108
311
  }
109
312
 
110
- async function main(argv: readonly string[]): Promise<number> {
111
- const missing = readMissingFlags(argv, ['--cap'])
313
+ /**
314
+ * Runs the script on its arguments.
315
+ *
316
+ * @param argv - The arguments after the script path.
317
+ * @returns The exit code: 0 when logged in, 3 when not, 64 on usage.
318
+ */
319
+ export async function main(argv: readonly string[]): Promise<number> {
320
+ const unknown = readUnknownFlags(argv, FLAGS)
321
+ if (unknown.length > 0) {
322
+ console.error(
323
+ `login: ${unknown.join(', ')} is not a flag this script reads; usage: login.ts --codex [--cap SECONDS] | --claude`,
324
+ )
325
+ return 64
326
+ }
327
+ const missing = readMissingFlags(argv, VALUE_FLAGS)
112
328
  if (missing.length > 0) {
113
329
  console.error(`login: ${missing.join(', ')} given with no value`)
114
330
  return 64
@@ -117,12 +333,31 @@ async function main(argv: readonly string[]): Promise<number> {
117
333
  console.error('usage: login.ts names one bench: --codex [--cap SECONDS] | --claude')
118
334
  return 64
119
335
  }
120
- if (argv.includes('--codex')) return loginCodex(Number(readOption(argv, '--cap') ?? 600))
121
- if (argv.includes('--claude')) return loginClaude()
336
+ const cap = parseCap(readOption(argv, '--cap'))
337
+ if (cap === undefined) {
338
+ console.error(
339
+ `login: --cap takes a number of seconds above 0 and at most ${String(MAX_CAP_SECONDS)}`,
340
+ )
341
+ return 64
342
+ }
343
+ if (argv.includes('--codex')) {
344
+ const outcome = await loginCodex(cap, CODEX_FLOW, (line) => {
345
+ console.error(line)
346
+ })
347
+ console.log(JSON.stringify(outcome.report))
348
+ return outcome.exit
349
+ }
350
+ if (argv.includes('--claude')) {
351
+ const outcome = loginClaude(CLAUDE_STATUS)
352
+ console.log(JSON.stringify(outcome.report))
353
+ return outcome.exit
354
+ }
122
355
  console.error('usage: login.ts --codex [--cap SECONDS] | --claude')
123
356
  return 64
124
357
  }
125
358
 
126
- main(process.argv.slice(2)).then((code) => {
127
- process.exitCode = code
128
- })
359
+ if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename) {
360
+ main(process.argv.slice(2)).then((code) => {
361
+ process.exitCode = code
362
+ })
363
+ }