@orkestrel/scaffold 0.0.77 → 0.0.79

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 (158) hide show
  1. package/dist/agents/skills/orkestrel-dispatch/scripts/bench.js +204 -0
  2. package/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +102 -0
  3. package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +95 -0
  4. package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +207 -0
  5. package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +108 -0
  6. package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +114 -0
  7. package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +108 -0
  8. package/dist/agents/skills/orkestrel-dispatch/scripts/sweep.js +156 -0
  9. package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +196 -0
  10. package/dist/agents/skills/orkestrel-publish/scripts/compare.js +206 -0
  11. package/dist/agents/skills/orkestrel-publish/scripts/pins.js +93 -0
  12. package/dist/agents/skills/orkestrel-publish/scripts/wave.js +458 -0
  13. package/dist/agents/skills/orkestrel-publish/scripts/window.js +188 -0
  14. package/dist/agents/skills/orkestrel-scout/scripts/map.js +300 -0
  15. package/dist/agents/templates/brief.md +55 -0
  16. package/dist/bin/main.js +58 -6
  17. package/dist/bin/main.js.map +1 -1
  18. package/dist/host/AGENTS.md +77 -135
  19. package/dist/host/agents/orchestration.md +147 -998
  20. package/dist/host/agents/skills/enterprise-bootstrap/SKILL.md +2 -2
  21. package/dist/host/agents/skills/enterprise-bootstrap/references/inspection.md +1 -1
  22. package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/SKILL.md +6 -13
  23. package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/agents/openai.yaml +1 -1
  24. package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/references/fleet.md +5 -7
  25. package/dist/host/agents/skills/{orkestrel-build-application → orkestrel-build}/SKILL.md +11 -22
  26. package/dist/host/agents/skills/{orkestrel-build-application → orkestrel-build}/agents/openai.yaml +1 -1
  27. package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +8 -16
  28. package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +3 -3
  29. package/dist/host/agents/skills/orkestrel-debrief/references/retention.md +13 -13
  30. package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +61 -0
  31. package/dist/host/agents/skills/orkestrel-dispatch/agents/openai.yaml +4 -0
  32. package/dist/host/agents/skills/orkestrel-dispatch/references/bench.md +25 -0
  33. package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +32 -0
  34. package/dist/host/agents/skills/orkestrel-dispatch/scripts/bench.ts +259 -0
  35. package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +110 -0
  36. package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +115 -0
  37. package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +239 -0
  38. package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +124 -0
  39. package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +123 -0
  40. package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +129 -0
  41. package/dist/host/agents/skills/orkestrel-dispatch/scripts/sweep.ts +157 -0
  42. package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +42 -193
  43. package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +38 -108
  44. package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +35 -134
  45. package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/SKILL.md +10 -14
  46. package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/agents/openai.yaml +1 -1
  47. package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/hardening.md +3 -4
  48. package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +228 -0
  49. package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/SKILL.md +15 -23
  50. package/dist/host/agents/skills/orkestrel-journey/agents/openai.yaml +4 -0
  51. package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/captures.md +1 -1
  52. package/dist/host/agents/skills/{orkestrel-polish-surface → orkestrel-polish}/SKILL.md +25 -33
  53. package/dist/host/agents/skills/{orkestrel-polish-surface → orkestrel-polish}/agents/openai.yaml +1 -1
  54. package/dist/host/agents/skills/{orkestrel-polish-surface → orkestrel-polish}/references/capture-harness.md +3 -3
  55. package/dist/host/agents/skills/orkestrel-publish/SKILL.md +33 -20
  56. package/dist/host/agents/skills/orkestrel-publish/references/release.md +39 -0
  57. package/dist/host/agents/skills/orkestrel-publish/references/wave.md +22 -21
  58. package/dist/host/agents/skills/orkestrel-publish/references/window.md +27 -14
  59. package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +220 -0
  60. package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +114 -0
  61. package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +629 -0
  62. package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +242 -0
  63. package/dist/host/agents/skills/orkestrel-scout/SKILL.md +28 -0
  64. package/dist/host/agents/skills/orkestrel-scout/agents/openai.yaml +4 -0
  65. package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +352 -0
  66. package/dist/host/agents/templates/brief.md +21 -142
  67. package/dist/host/agents/transports/claude-cli.md +21 -0
  68. package/dist/host/agents/transports/codex.md +38 -159
  69. package/dist/host/agents/transports/cursor.md +16 -65
  70. package/dist/host/claude/AGENTS.md +38 -0
  71. package/dist/host/claude/agents/analyst.md +14 -53
  72. package/dist/host/claude/agents/astra.md +26 -0
  73. package/dist/host/claude/agents/builder.md +14 -30
  74. package/dist/host/claude/agents/checker.md +13 -57
  75. package/dist/host/claude/agents/distiller.md +11 -26
  76. package/dist/host/claude/agents/grok.md +12 -35
  77. package/dist/host/claude/agents/opus.md +14 -30
  78. package/dist/host/claude/agents/orkestrel.md +4 -4
  79. package/dist/host/claude/agents/planner.md +10 -44
  80. package/dist/host/claude/agents/researcher.md +11 -30
  81. package/dist/host/claude/agents/reviewer.md +11 -95
  82. package/dist/host/claude/agents/scout.md +9 -23
  83. package/dist/host/claude/agents/verifier.md +15 -33
  84. package/dist/host/claude/rules/documentation.md +8 -2
  85. package/dist/host/claude/rules/portability.md +7 -1
  86. package/dist/host/claude/rules/quality.md +36 -96
  87. package/dist/host/claude/rules/styles.md +3 -0
  88. package/dist/host/claude/rules/tests.md +6 -3
  89. package/dist/host/claude/rules/workspace.md +21 -15
  90. package/dist/host/claude/rules/writing.md +57 -108
  91. package/dist/host/claude/settings.json +5 -3
  92. package/dist/host/claude/skills/enterprise-bootstrap/SKILL.md +1 -1
  93. package/dist/host/claude/skills/{orkestrel-align-packages → orkestrel-align}/SKILL.md +2 -2
  94. package/dist/host/claude/skills/{orkestrel-build-application → orkestrel-build}/SKILL.md +2 -2
  95. package/dist/host/claude/skills/orkestrel-dispatch/SKILL.md +11 -0
  96. package/dist/host/claude/skills/orkestrel-falsify/SKILL.md +2 -1
  97. package/dist/host/claude/skills/{orkestrel-harden-package → orkestrel-harden}/SKILL.md +2 -2
  98. package/dist/host/claude/skills/{orkestrel-prove-journey → orkestrel-journey}/SKILL.md +2 -2
  99. package/dist/host/claude/skills/orkestrel-polish/SKILL.md +12 -0
  100. package/dist/host/claude/skills/orkestrel-scout/SKILL.md +11 -0
  101. package/dist/host/codex/agents/analyst.toml +14 -31
  102. package/dist/host/codex/agents/astra.toml +25 -0
  103. package/dist/host/codex/agents/builder.toml +13 -20
  104. package/dist/host/codex/agents/checker.toml +13 -27
  105. package/dist/host/codex/agents/distiller.toml +9 -22
  106. package/dist/host/codex/agents/grok.toml +11 -30
  107. package/dist/host/codex/agents/opus.toml +14 -22
  108. package/dist/host/codex/agents/orkestrel.toml +1 -1
  109. package/dist/host/codex/agents/planner.toml +11 -28
  110. package/dist/host/codex/agents/researcher.toml +10 -22
  111. package/dist/host/codex/agents/reviewer.toml +11 -27
  112. package/dist/host/codex/agents/scout.toml +11 -17
  113. package/dist/host/codex/agents/verifier.toml +16 -12
  114. package/dist/host/codex/config.toml +18 -21
  115. package/dist/host/cursor/mcp.json +0 -4
  116. package/dist/host/cursor/rules/orchestration.mdc +12 -20
  117. package/dist/host/dotfiles/mcp.json +0 -4
  118. package/dist/host/dotfiles/oxlintrc.json +7 -0
  119. package/dist/host/guides/probe.md +18 -14
  120. package/dist/host/guides/scaffold.md +147 -83
  121. package/dist/host/guides/test.md +442 -148
  122. package/dist/host/manifest.json +322 -185
  123. package/dist/host/scripts/codex.sh +0 -0
  124. package/dist/host/scripts/cursor.sh +0 -0
  125. package/dist/host/scripts/deps.sh +0 -0
  126. package/dist/host/scripts/ollama.sh +0 -0
  127. package/dist/host/tests/config.test.ts +86 -55
  128. package/dist/host/tests/policy.test.ts +1 -5
  129. package/dist/host/tests/setupPolicy.ts +179 -4
  130. package/dist/src/core/index.cjs +264 -89
  131. package/dist/src/core/index.cjs.map +1 -1
  132. package/dist/src/core/index.d.cts +95 -30
  133. package/dist/src/core/index.d.ts +95 -30
  134. package/dist/src/core/index.js +262 -90
  135. package/dist/src/core/index.js.map +1 -1
  136. package/dist/src/server/index.cjs +55 -9
  137. package/dist/src/server/index.cjs.map +1 -1
  138. package/dist/src/server/index.d.cts +29 -4
  139. package/dist/src/server/index.d.ts +29 -4
  140. package/dist/src/server/index.js +56 -11
  141. package/dist/src/server/index.js.map +1 -1
  142. package/package.json +16 -12
  143. package/dist/host/CLAUDE.md +0 -61
  144. package/dist/host/agents/skills/orkestrel-prove-journey/agents/openai.yaml +0 -4
  145. package/dist/host/agents/transports/claude.md +0 -49
  146. package/dist/host/claude/agents/application.md +0 -36
  147. package/dist/host/claude/agents/sol.md +0 -61
  148. package/dist/host/claude/skills/orkestrel-polish-surface/SKILL.md +0 -12
  149. package/dist/host/codex/agents/application.toml +0 -25
  150. package/dist/host/codex/agents/sol.toml +0 -19
  151. /package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/references/integration.md +0 -0
  152. /package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/centralization.md +0 -0
  153. /package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/contract.md +0 -0
  154. /package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/research.md +0 -0
  155. /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/decide.md +0 -0
  156. /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/layer.md +0 -0
  157. /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/statechart.md +0 -0
  158. /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/styles.md +0 -0
@@ -0,0 +1,239 @@
1
+ // What the skill scripts share: argument reading, tree walking, JSON line reading, and the npm
2
+ // command. This module runs nothing. Import it from a sibling or another skill's script with the
3
+ // `.ts` extension, for example `import { readOption } from '../../orkestrel-dispatch/scripts/helpers.ts'`.
4
+ import type { SpawnSyncReturns } from 'node:child_process'
5
+ import { spawnSync } from 'node:child_process'
6
+ import { existsSync, readdirSync, statSync } from 'node:fs'
7
+ import { dirname, join } from 'node:path'
8
+
9
+ /** Names the directories every tree walk in a skill script skips. */
10
+ export const SKIPPED_DIRECTORIES: ReadonlySet<string> = new Set([
11
+ 'node_modules',
12
+ '.git',
13
+ 'dist',
14
+ 'tmp',
15
+ '.orkestrel',
16
+ '.idea',
17
+ ])
18
+
19
+ /** Describes one file a tree walk found, with the modification time the sweep reads. */
20
+ export interface WalkedFile {
21
+ readonly path: string
22
+ readonly modified: number
23
+ }
24
+
25
+ /** Describes how to spawn npm on this host without a shell where the CLI entry is present. */
26
+ export interface NpmCommand {
27
+ readonly file: string
28
+ readonly prefix: readonly string[]
29
+ readonly shell: boolean
30
+ }
31
+
32
+ /**
33
+ * Reads the value that follows a flag.
34
+ *
35
+ * @remarks
36
+ * Every skill script reads `--flag value` pairs: the first occurrence of a flag wins, a following
37
+ * value that opens with `--` is a missing value, 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 `--journal`.
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: readonly string[], name: string): string | undefined {
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
+ /**
51
+ * Names the value flags a caller gave with no value.
52
+ *
53
+ * @param argv - The arguments after the script path.
54
+ * @param names - The flags that take a value.
55
+ * @returns Each named flag that appears last or is followed by another flag, in argument order.
56
+ */
57
+ export function readMissingFlags(
58
+ argv: readonly string[],
59
+ names: readonly string[],
60
+ ): readonly string[] {
61
+ return names.filter((name) => argv.includes(name) && readOption(argv, name) === undefined)
62
+ }
63
+
64
+ /**
65
+ * Reads every value that follows a repeatable flag.
66
+ *
67
+ * @param argv - The arguments after the script path.
68
+ * @param name - The flag, such as `--range`.
69
+ * @returns The values in order, empty when the flag never appears.
70
+ */
71
+ export function readOptions(argv: readonly string[], name: string): readonly string[] {
72
+ const values: string[] = []
73
+ argv.forEach((argument, index) => {
74
+ const value = argv[index + 1]
75
+ if (argument === name && value !== undefined && !value.startsWith('--')) values.push(value)
76
+ })
77
+ return values
78
+ }
79
+
80
+ /**
81
+ * Reads the values that follow a flag up to the next flag.
82
+ *
83
+ * @param argv - The arguments after the script path.
84
+ * @param name - The flag, such as `--tree`.
85
+ * @returns The values between the flag and the next `--` argument, empty when the flag is absent.
86
+ */
87
+ export function readList(argv: readonly string[], name: string): readonly string[] {
88
+ const index = argv.indexOf(name)
89
+ if (index === -1) return []
90
+ const values: string[] = []
91
+ for (const argument of argv.slice(index + 1)) {
92
+ if (argument.startsWith('--')) break
93
+ values.push(argument)
94
+ }
95
+ return values
96
+ }
97
+
98
+ /**
99
+ * Normalizes a host path to forward slashes.
100
+ *
101
+ * @param path - A host path.
102
+ * @returns The same path with every backslash replaced by a slash.
103
+ */
104
+ export function normalizeSlashes(path: string): string {
105
+ return path.split('\\').join('/')
106
+ }
107
+
108
+ /**
109
+ * Lists every regular file beneath a directory, skipping the directories skill scripts never read.
110
+ *
111
+ * @remarks
112
+ * A skipped name matches a directory at any depth, exactly and case-sensitively, so a
113
+ * `src/dist/` directory is skipped with the build output and a `Node_Modules/` directory is not.
114
+ *
115
+ * @param directory - The directory to walk.
116
+ * @param skipped - The directory names to skip. Default: {@link SKIPPED_DIRECTORIES}.
117
+ * @returns Each file's forward-slash path and modification time, in walk order.
118
+ * @throws Thrown when `directory` does not exist or cannot be read; check `existsSync` first.
119
+ */
120
+ export function listFiles(
121
+ directory: string,
122
+ skipped: ReadonlySet<string> = SKIPPED_DIRECTORIES,
123
+ ): readonly WalkedFile[] {
124
+ const files: WalkedFile[] = []
125
+ const pending = [directory]
126
+ while (pending.length > 0) {
127
+ const current = pending.pop()
128
+ if (current === undefined) break
129
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
130
+ if (skipped.has(entry.name)) continue
131
+ const path = join(current, entry.name)
132
+ if (entry.isDirectory()) pending.push(path)
133
+ else if (entry.isFile()) {
134
+ files.push({ path: normalizeSlashes(path), modified: statSync(path).mtimeMs })
135
+ }
136
+ }
137
+ }
138
+ return files
139
+ }
140
+
141
+ /**
142
+ * Reads the last non-empty line of a text as a JSON object.
143
+ *
144
+ * @param text - Text whose last line a script printed as JSON.
145
+ * @returns The parsed object, or undefined when the last line is not a JSON object.
146
+ */
147
+ export function readJsonObject(text: string): Record<string, unknown> | undefined {
148
+ const line = text
149
+ .split(/\r\n|\n/)
150
+ .filter((candidate) => candidate.trim() !== '')
151
+ .at(-1)
152
+ if (line === undefined) return undefined
153
+ try {
154
+ const parsed: unknown = JSON.parse(line)
155
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
156
+ return Object.fromEntries(Object.entries(parsed))
157
+ }
158
+ } catch {}
159
+ return undefined
160
+ }
161
+
162
+ /**
163
+ * Parses a whole text as one JSON object.
164
+ *
165
+ * @param text - A JSON document, formatted or not, such as a manifest or `npm view --json` output.
166
+ * @returns The parsed object, or undefined when the text is not a JSON object.
167
+ */
168
+ export function parseJsonRecord(text: string): Record<string, unknown> | undefined {
169
+ try {
170
+ const parsed: unknown = JSON.parse(text)
171
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
172
+ return Object.fromEntries(Object.entries(parsed))
173
+ }
174
+ } catch {}
175
+ return undefined
176
+ }
177
+
178
+ /**
179
+ * Reads a string member of a record.
180
+ *
181
+ * @param record - The record, or undefined.
182
+ * @param key - The member name.
183
+ * @returns The string, or undefined when the member is absent or not a string.
184
+ */
185
+ export function readString(
186
+ record: Record<string, unknown> | undefined,
187
+ key: string,
188
+ ): string | undefined {
189
+ const value = record?.[key]
190
+ return typeof value === 'string' ? value : undefined
191
+ }
192
+
193
+ /**
194
+ * Reads a finite number member of a record.
195
+ *
196
+ * @param record - The record, or undefined.
197
+ * @param key - The member name.
198
+ * @returns The number, or undefined when the member is absent, not a number, or not finite.
199
+ */
200
+ export function readNumber(
201
+ record: Record<string, unknown> | undefined,
202
+ key: string,
203
+ ): number | undefined {
204
+ const value = record?.[key]
205
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined
206
+ }
207
+
208
+ /**
209
+ * Resolves the npm command for this host.
210
+ *
211
+ * @remarks
212
+ * The `npm-cli.js` entry beside the running Node binary spawns without a shell on every host; the
213
+ * `npm` shim on Windows is a `.cmd` file that needs one, so that path is the fallback alone.
214
+ *
215
+ * @returns The file to spawn, the arguments that precede npm's own, and whether a shell is needed.
216
+ */
217
+ export function resolveNpm(): NpmCommand {
218
+ const cli = join(dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js')
219
+ if (existsSync(cli)) return { file: process.execPath, prefix: [cli], shell: false }
220
+ return { file: 'npm', prefix: [], shell: process.platform === 'win32' }
221
+ }
222
+
223
+ /**
224
+ * Runs npm with the given arguments and returns the finished child.
225
+ *
226
+ * @param args - The npm arguments, such as `['view', '@orkestrel/scaffold', 'version']`.
227
+ * @param cwd - The working directory. Default: the current directory.
228
+ * @returns The spawn result with `utf8` stdout and stderr.
229
+ */
230
+ export function runNpm(args: readonly string[], cwd?: string): SpawnSyncReturns<string> {
231
+ const npm = resolveNpm()
232
+ return spawnSync(npm.file, [...npm.prefix, ...args], {
233
+ encoding: 'utf8',
234
+ windowsHide: true,
235
+ shell: npm.shell,
236
+ maxBuffer: 64 * 1024 * 1024,
237
+ ...(cwd === undefined ? {} : { cwd }),
238
+ })
239
+ }
@@ -0,0 +1,124 @@
1
+ // Journaled launch of a bench exec or any command that outlives a turn. Run from the checkout root:
2
+ // node .agents/skills/orkestrel-dispatch/scripts/launch.ts --journal tmp/codex/UNIT.jsonl --errors tmp/codex/UNIT.err --cap 1500 [--status] -- codex exec --json ...
3
+ // The command's stdout goes to --journal and its stderr to --errors; stdin is closed. Arguments after
4
+ // `--` pass to the command verbatim, so no shell quoting is involved. --cap is in seconds: when it
5
+ // expires the process tree is killed and the errors file records `capped=true`. --status records
6
+ // `git status --porcelain` before and after beside the journal, which is how a read-only lane's
7
+ // containment is checked. Stdout carries up to two JSON lines. The spawn line
8
+ // `{"pid","journal","errors"}` prints as soon as the command starts, and the pid, as decimal digits
9
+ // and a newline, goes to `<journal>.pid`. A pid file an earlier run left on the same journal is
10
+ // removed before the spawn, so neither appears when the command cannot start. The summary line
11
+ // prints last, after the command exits, and the errors file ends with `exit=<code>`. At the cap,
12
+ // Windows kills the process tree; any other host kills the command alone, not its descendants.
13
+ // The launcher's own flags are read before `--` alone. Exit: the command's code; 124 when capped;
14
+ // 127 when the command could not start (the errors file names why); 64 on usage, including a
15
+ // journal and errors path that are the same file and a cap that is not a number of seconds.
16
+ import { spawn, spawnSync } from 'node:child_process'
17
+ import { appendFileSync, closeSync, mkdirSync, openSync, rmSync, writeFileSync } from 'node:fs'
18
+ import { dirname } from 'node:path'
19
+ import { readMissingFlags, readOption } from './helpers.ts'
20
+
21
+ function readStatus(): string {
22
+ const result = spawnSync('git', ['status', '--porcelain'], { encoding: 'utf8' })
23
+ return result.status === 0 ? result.stdout : `git status failed: ${result.stderr}`
24
+ }
25
+
26
+ function killTree(pid: number): void {
27
+ if (process.platform === 'win32') {
28
+ spawnSync('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore' })
29
+ return
30
+ }
31
+ try {
32
+ process.kill(pid, 'SIGKILL')
33
+ } catch {}
34
+ }
35
+
36
+ interface Exit {
37
+ readonly code: number | null
38
+ readonly signal: NodeJS.Signals | null
39
+ readonly error: string | undefined
40
+ }
41
+
42
+ function waitForExit(child: ReturnType<typeof spawn>): Promise<Exit> {
43
+ return new Promise((resolve) => {
44
+ child.on('error', (error) => resolve({ code: null, signal: null, error: error.message }))
45
+ child.on('close', (code, signal) => resolve({ code, signal, error: undefined }))
46
+ })
47
+ }
48
+
49
+ async function main(argv: readonly string[]): Promise<number> {
50
+ const separator = argv.indexOf('--')
51
+ // The launcher's own flags end at the separator; everything after it belongs to the command.
52
+ const own = separator === -1 ? argv : argv.slice(0, separator)
53
+ const journal = readOption(own, '--journal')
54
+ const errors = readOption(own, '--errors')
55
+ const cap =
56
+ readMissingFlags(own, ['--cap']).length > 0
57
+ ? Number.NaN
58
+ : Number(readOption(own, '--cap') ?? '0')
59
+ const [command, ...args] = separator === -1 ? [] : argv.slice(separator + 1)
60
+ if (
61
+ separator === -1 ||
62
+ journal === undefined ||
63
+ errors === undefined ||
64
+ journal === errors ||
65
+ !Number.isFinite(cap) ||
66
+ cap < 0 ||
67
+ command === undefined ||
68
+ command === ''
69
+ ) {
70
+ console.error(
71
+ 'usage: launch.ts --journal PATH --errors PATH --cap SECONDS [--status] -- COMMAND [ARGS...]; the journal and the errors file differ, and the cap is a number of seconds (0 for none)',
72
+ )
73
+ return 64
74
+ }
75
+ const status = own.includes('--status')
76
+ mkdirSync(dirname(journal), { recursive: true })
77
+ mkdirSync(dirname(errors), { recursive: true })
78
+ if (status) writeFileSync(`${journal}.status-before.txt`, readStatus())
79
+ const journalFd = openSync(journal, 'w')
80
+ const errorsFd = openSync(errors, 'w')
81
+ rmSync(`${journal}.pid`, { force: true })
82
+ const started = Date.now()
83
+ const child = spawn(command, args, { stdio: ['ignore', journalFd, errorsFd], windowsHide: true })
84
+ if (child.pid !== undefined) {
85
+ writeFileSync(`${journal}.pid`, `${child.pid}\n`)
86
+ console.log(JSON.stringify({ pid: child.pid, journal, errors }))
87
+ }
88
+ let capped = false
89
+ const timer =
90
+ cap > 0
91
+ ? setTimeout(() => {
92
+ capped = true
93
+ if (child.pid !== undefined) killTree(child.pid)
94
+ }, cap * 1000)
95
+ : undefined
96
+ const exit = await waitForExit(child)
97
+ if (timer !== undefined) clearTimeout(timer)
98
+ closeSync(journalFd)
99
+ closeSync(errorsFd)
100
+ const durationMs = Date.now() - started
101
+ appendFileSync(
102
+ errors,
103
+ `${exit.error === undefined ? '' : `launch: the command could not start: ${exit.error}\n`}\nexit=${exit.error === undefined ? (exit.code ?? 'null') : '127'} signal=${exit.signal ?? 'none'} capped=${capped} duration_ms=${durationMs}\n`,
104
+ )
105
+ if (status) writeFileSync(`${journal}.status-after.txt`, readStatus())
106
+ console.log(
107
+ JSON.stringify({
108
+ pid: child.pid,
109
+ exit: exit.error === undefined ? exit.code : 127,
110
+ signal: exit.signal,
111
+ capped,
112
+ durationMs,
113
+ journal,
114
+ errors,
115
+ error: exit.error,
116
+ }),
117
+ )
118
+ if (exit.error !== undefined) return 127
119
+ return capped ? 124 : (exit.code ?? 1)
120
+ }
121
+
122
+ main(process.argv.slice(2)).then((code) => {
123
+ process.exitCode = code
124
+ })
@@ -0,0 +1,123 @@
1
+ // Recover a bench login. Run from the checkout root:
2
+ // node .agents/skills/orkestrel-dispatch/scripts/login.ts --codex [--cap SECONDS]
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.
10
+ import { spawn, spawnSync } from 'node:child_process'
11
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync } from 'node:fs'
12
+ import { readMissingFlags, readOption } from './helpers.ts'
13
+
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
+
19
+ interface Status {
20
+ readonly live: boolean
21
+ readonly text: string
22
+ }
23
+
24
+ function readStatus(command: string, args: readonly string[]): Status {
25
+ const result = spawnSync(command, args, { encoding: 'utf8', windowsHide: true })
26
+ if (result.error !== undefined)
27
+ return { live: false, text: `${command} could not start: ${result.error.message}` }
28
+ return { live: result.status === 0, text: `${result.stdout ?? ''}${result.stderr ?? ''}`.trim() }
29
+ }
30
+
31
+ function killTree(pid: number): void {
32
+ if (process.platform === 'win32') {
33
+ spawnSync('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore' })
34
+ return
35
+ }
36
+ try {
37
+ process.kill(pid, 'SIGTERM')
38
+ } catch {}
39
+ }
40
+
41
+ function waitFor(ms: number): Promise<void> {
42
+ return new Promise((resolve) => {
43
+ setTimeout(resolve, ms)
44
+ })
45
+ }
46
+
47
+ async function loginCodex(capSeconds: number): Promise<number> {
48
+ const before = readStatus('codex', ['login', 'status'])
49
+ if (before.live) {
50
+ console.log(JSON.stringify({ bench: 'codex', live: true, status: before.text }))
51
+ return 0
52
+ }
53
+ mkdirSync('tmp/codex', { recursive: true })
54
+ const journal = openSync(JOURNAL, 'w')
55
+ const started = Date.now()
56
+ const child = spawn('codex', ['login', '--device-auth'], {
57
+ stdio: ['ignore', journal, journal],
58
+ windowsHide: true,
59
+ })
60
+ let url: string | undefined
61
+ let code: string | undefined
62
+ let live = false
63
+ while (Date.now() - started < capSeconds * 1000) {
64
+ await waitFor(POLL_MS)
65
+ const text = existsSync(JOURNAL) ? readFileSync(JOURNAL, 'utf8') : ''
66
+ if (url === undefined) {
67
+ url = text.match(URL_PATTERN)?.[0]
68
+ if (url !== undefined) console.error(`login: open ${url}`)
69
+ }
70
+ if (code === undefined) {
71
+ code = text.match(CODE_PATTERN)?.[1]
72
+ if (code !== undefined) console.error(`login: enter the code ${code}`)
73
+ }
74
+ if (child.exitCode !== null && readStatus('codex', ['login', 'status']).live) {
75
+ live = true
76
+ break
77
+ }
78
+ if (child.exitCode === null && readStatus('codex', ['login', 'status']).live) {
79
+ live = true
80
+ break
81
+ }
82
+ }
83
+ if (child.exitCode === null && child.pid !== undefined) killTree(child.pid)
84
+ closeSync(journal)
85
+ console.log(
86
+ JSON.stringify({
87
+ bench: 'codex',
88
+ live,
89
+ url,
90
+ code,
91
+ durationMs: Date.now() - started,
92
+ journal: JOURNAL,
93
+ status: readStatus('codex', ['login', 'status']).text,
94
+ }),
95
+ )
96
+ return live ? 0 : 3
97
+ }
98
+
99
+ function loginClaude(): number {
100
+ const status = readStatus('claude', ['auth', 'status'])
101
+ console.log(JSON.stringify({ bench: 'claude', live: status.live, status: status.text }))
102
+ return status.live ? 0 : 3
103
+ }
104
+
105
+ async function main(argv: readonly string[]): Promise<number> {
106
+ const missing = readMissingFlags(argv, ['--cap'])
107
+ if (missing.length > 0) {
108
+ console.error(`login: ${missing.join(', ')} given with no value`)
109
+ return 64
110
+ }
111
+ if (argv.includes('--codex') && argv.includes('--claude')) {
112
+ console.error('usage: login.ts names one bench: --codex [--cap SECONDS] | --claude')
113
+ return 64
114
+ }
115
+ if (argv.includes('--codex')) return loginCodex(Number(readOption(argv, '--cap') ?? 600))
116
+ if (argv.includes('--claude')) return loginClaude()
117
+ console.error('usage: login.ts --codex [--cap SECONDS] | --claude')
118
+ return 64
119
+ }
120
+
121
+ main(process.argv.slice(2)).then((code) => {
122
+ process.exitCode = code
123
+ })
@@ -0,0 +1,129 @@
1
+ // Read a bench lane's answer, session id, and liveness from its journal. Run from the checkout root:
2
+ // node .agents/skills/orkestrel-dispatch/scripts/result.ts --cursor tmp/cursor/UNIT.jsonl [--out tmp/cursor/UNIT-result.md]
3
+ // node .agents/skills/orkestrel-dispatch/scripts/result.ts --claude tmp/claude/UNIT.jsonl [--out tmp/claude/UNIT-result.md]
4
+ // node .agents/skills/orkestrel-dispatch/scripts/result.ts --codex tmp/codex/UNIT.jsonl [--out tmp/codex/UNIT-result.md]
5
+ // A Cursor or Claude journal is the `--output-format stream-json` stream: the `init` event carries
6
+ // the session id and the `result` event carries the answer. A Codex journal is the `--json` stream:
7
+ // the `thread.started` event carries the thread id and the answer sits in the sibling `UNIT-last.md`
8
+ // file that `--output-last-message` wrote. The summary also reads the sibling `UNIT.err` file that
9
+ // launch.ts wrote (`exit`, `capped`, `durationMs`) and the journal's `bytes`, `ageMs`, and last
10
+ // event, which is the liveness reading for a run still going. The answer is written to --out when
11
+ // given, and a journal with no session or answer exits 3 so a driver cannot pass off an empty lane.
12
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
13
+ import { dirname } from 'node:path'
14
+ import { readMissingFlags, readNumber, readOption, readString } from './helpers.ts'
15
+
16
+ interface Reading {
17
+ readonly session: string | undefined
18
+ readonly answer: string | undefined
19
+ readonly durationMs: number | undefined
20
+ }
21
+
22
+ interface Errors {
23
+ readonly exit: number | undefined
24
+ readonly capped: boolean | undefined
25
+ readonly durationMs: number | undefined
26
+ }
27
+
28
+ function readEvents(path: string): ReadonlyArray<Record<string, unknown>> {
29
+ const events: Array<Record<string, unknown>> = []
30
+ for (const line of readFileSync(path, 'utf8').split(/\r\n|\n/)) {
31
+ if (line.trim() === '') continue
32
+ try {
33
+ const parsed: unknown = JSON.parse(line)
34
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
35
+ events.push(Object.fromEntries(Object.entries(parsed)))
36
+ }
37
+ } catch {}
38
+ }
39
+ return events
40
+ }
41
+
42
+ function readAnswer(text: string | undefined): string | undefined {
43
+ return text === undefined || text.trim() === '' ? undefined : text
44
+ }
45
+
46
+ function readStream(events: ReadonlyArray<Record<string, unknown>>): Reading {
47
+ const init = events.find((event) => event.type === 'system' && event.subtype === 'init')
48
+ const result = events.find((event) => event.type === 'result')
49
+ return {
50
+ session: init === undefined ? undefined : readString(init, 'session_id'),
51
+ answer: result === undefined ? undefined : readAnswer(readString(result, 'result')),
52
+ durationMs: result === undefined ? undefined : readNumber(result, 'duration_ms'),
53
+ }
54
+ }
55
+
56
+ function readCodex(path: string, events: ReadonlyArray<Record<string, unknown>>): Reading {
57
+ const started = events.find((event) => event.type === 'thread.started')
58
+ const last = path.replace(/\.jsonl$/u, '-last.md')
59
+ return {
60
+ session: started === undefined ? undefined : readString(started, 'thread_id'),
61
+ answer: existsSync(last) ? readAnswer(readFileSync(last, 'utf8')) : undefined,
62
+ durationMs: undefined,
63
+ }
64
+ }
65
+
66
+ function readErrors(path: string): Errors {
67
+ const errors = path.replace(/\.jsonl$/u, '.err')
68
+ if (!existsSync(errors)) return { exit: undefined, capped: undefined, durationMs: undefined }
69
+ const match = readFileSync(errors, 'utf8').match(
70
+ /exit=(\S+) signal=\S+ capped=(true|false) duration_ms=(\d+)\s*$/u,
71
+ )
72
+ if (match === null) return { exit: undefined, capped: undefined, durationMs: undefined }
73
+ const exit = Number(match[1])
74
+ return {
75
+ exit: Number.isFinite(exit) ? exit : undefined,
76
+ capped: match[2] === 'true',
77
+ durationMs: Number(match[3]),
78
+ }
79
+ }
80
+
81
+ function describeEvent(event: Record<string, unknown> | undefined): string | undefined {
82
+ if (event === undefined) return undefined
83
+ const type = readString(event, 'type')
84
+ const subtype = readString(event, 'subtype')
85
+ return subtype === undefined ? type : `${type}/${subtype}`
86
+ }
87
+
88
+ function main(argv: readonly string[]): number {
89
+ const missing = readMissingFlags(argv, ['--cursor', '--claude', '--codex', '--out'])
90
+ if (missing.length > 0) {
91
+ console.error(`result: ${missing.join(', ')} given with no value`)
92
+ return 64
93
+ }
94
+ const modes = ['--cursor', '--claude', '--codex'].filter((flag) => argv.includes(flag))
95
+ const mode = modes[0]
96
+ const path = mode === undefined ? undefined : readOption(argv, mode)
97
+ const out = readOption(argv, '--out')
98
+ if (modes.length !== 1 || mode === undefined || path === undefined || !existsSync(path)) {
99
+ console.error(
100
+ 'usage: result.ts (--cursor JOURNAL | --claude JOURNAL | --codex JOURNAL) [--out PATH]; name one mode',
101
+ )
102
+ return 64
103
+ }
104
+ const events = readEvents(path)
105
+ const reading = mode === '--codex' ? readCodex(path, events) : readStream(events)
106
+ const errors = readErrors(path)
107
+ const stat = statSync(path)
108
+ if (reading.answer !== undefined && out !== undefined) {
109
+ mkdirSync(dirname(out), { recursive: true })
110
+ writeFileSync(out, reading.answer)
111
+ }
112
+ console.log(
113
+ JSON.stringify({
114
+ journal: path,
115
+ session: reading.session,
116
+ durationMs: reading.durationMs ?? errors.durationMs,
117
+ exit: errors.exit,
118
+ capped: errors.capped,
119
+ bytes: stat.size,
120
+ ageMs: Math.max(0, Math.round(Date.now() - stat.mtimeMs)),
121
+ lastEvent: describeEvent(events.at(-1)),
122
+ answerLength: reading.answer?.length ?? 0,
123
+ out: reading.answer === undefined ? undefined : out,
124
+ }),
125
+ )
126
+ return reading.session === undefined || reading.answer === undefined ? 3 : 0
127
+ }
128
+
129
+ process.exitCode = main(process.argv.slice(2))