@orkestrel/scaffold 0.0.76 → 0.0.78
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.
- package/dist/agents/skills/orkestrel-dispatch/scripts/bench.js +204 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +102 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +95 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +207 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +108 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +114 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +108 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/sweep.js +156 -0
- package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +196 -0
- package/dist/agents/skills/orkestrel-publish/scripts/compare.js +206 -0
- package/dist/agents/skills/orkestrel-publish/scripts/pins.js +93 -0
- package/dist/agents/skills/orkestrel-publish/scripts/wave.js +458 -0
- package/dist/agents/skills/orkestrel-publish/scripts/window.js +188 -0
- package/dist/agents/skills/orkestrel-scout/scripts/map.js +300 -0
- package/dist/agents/templates/brief.md +55 -0
- package/dist/bin/main.js +4 -2
- package/dist/bin/main.js.map +1 -1
- package/dist/host/AGENTS.md +77 -135
- package/dist/host/agents/orchestration.md +147 -927
- package/dist/host/agents/skills/enterprise-bootstrap/SKILL.md +2 -2
- package/dist/host/agents/skills/enterprise-bootstrap/references/inspection.md +1 -1
- package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/SKILL.md +6 -13
- package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/agents/openai.yaml +1 -1
- package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/references/fleet.md +5 -7
- package/dist/host/agents/skills/{orkestrel-build-application → orkestrel-build}/SKILL.md +11 -22
- package/dist/host/agents/skills/{orkestrel-build-application → orkestrel-build}/agents/openai.yaml +1 -1
- package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +8 -16
- package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +3 -3
- package/dist/host/agents/skills/orkestrel-debrief/references/retention.md +13 -13
- package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +61 -0
- package/dist/host/agents/skills/orkestrel-dispatch/agents/openai.yaml +4 -0
- package/dist/host/agents/skills/orkestrel-dispatch/references/bench.md +25 -0
- package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +32 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/bench.ts +259 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +110 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +115 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +239 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +124 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +123 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +129 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/sweep.ts +157 -0
- package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +42 -193
- package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +38 -108
- package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +35 -134
- package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/SKILL.md +10 -14
- package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/agents/openai.yaml +1 -1
- package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/hardening.md +3 -4
- package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +228 -0
- package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/SKILL.md +15 -23
- package/dist/host/agents/skills/orkestrel-journey/agents/openai.yaml +4 -0
- package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/captures.md +1 -1
- package/dist/host/agents/skills/{orkestrel-polish-surface → orkestrel-polish}/SKILL.md +25 -33
- package/dist/host/agents/skills/{orkestrel-polish-surface → orkestrel-polish}/agents/openai.yaml +1 -1
- package/dist/host/agents/skills/{orkestrel-polish-surface → orkestrel-polish}/references/capture-harness.md +3 -3
- package/dist/host/agents/skills/orkestrel-publish/SKILL.md +33 -20
- package/dist/host/agents/skills/orkestrel-publish/references/release.md +39 -0
- package/dist/host/agents/skills/orkestrel-publish/references/wave.md +22 -21
- package/dist/host/agents/skills/orkestrel-publish/references/window.md +27 -14
- package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +220 -0
- package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +114 -0
- package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +629 -0
- package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +242 -0
- package/dist/host/agents/skills/orkestrel-scout/SKILL.md +28 -0
- package/dist/host/agents/skills/orkestrel-scout/agents/openai.yaml +4 -0
- package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +352 -0
- package/dist/host/agents/templates/brief.md +21 -142
- package/dist/host/agents/transports/claude-cli.md +21 -0
- package/dist/host/agents/transports/codex.md +38 -159
- package/dist/host/agents/transports/cursor.md +16 -65
- package/dist/host/claude/AGENTS.md +38 -0
- package/dist/host/claude/agents/analyst.md +14 -53
- package/dist/host/claude/agents/astra.md +26 -0
- package/dist/host/claude/agents/builder.md +14 -30
- package/dist/host/claude/agents/checker.md +13 -57
- package/dist/host/claude/agents/distiller.md +11 -26
- package/dist/host/claude/agents/grok.md +12 -35
- package/dist/host/claude/agents/opus.md +14 -30
- package/dist/host/claude/agents/planner.md +10 -44
- package/dist/host/claude/agents/researcher.md +11 -30
- package/dist/host/claude/agents/reviewer.md +11 -95
- package/dist/host/claude/agents/scout.md +9 -23
- package/dist/host/claude/agents/verifier.md +15 -33
- package/dist/host/claude/rules/documentation.md +8 -2
- package/dist/host/claude/rules/portability.md +7 -1
- package/dist/host/claude/rules/quality.md +36 -96
- package/dist/host/claude/rules/styles.md +3 -0
- package/dist/host/claude/rules/tests.md +6 -3
- package/dist/host/claude/rules/workspace.md +19 -15
- package/dist/host/claude/rules/writing.md +57 -108
- package/dist/host/claude/settings.json +5 -3
- package/dist/host/claude/skills/enterprise-bootstrap/SKILL.md +1 -1
- package/dist/host/claude/skills/{orkestrel-align-packages → orkestrel-align}/SKILL.md +2 -2
- package/dist/host/claude/skills/{orkestrel-build-application → orkestrel-build}/SKILL.md +2 -2
- package/dist/host/claude/skills/orkestrel-dispatch/SKILL.md +11 -0
- package/dist/host/claude/skills/orkestrel-falsify/SKILL.md +2 -1
- package/dist/host/claude/skills/{orkestrel-harden-package → orkestrel-harden}/SKILL.md +2 -2
- package/dist/host/claude/skills/{orkestrel-prove-journey → orkestrel-journey}/SKILL.md +2 -2
- package/dist/host/claude/skills/orkestrel-polish/SKILL.md +12 -0
- package/dist/host/claude/skills/orkestrel-scout/SKILL.md +11 -0
- package/dist/host/codex/agents/analyst.toml +15 -32
- package/dist/host/codex/agents/astra.toml +25 -0
- package/dist/host/codex/agents/builder.toml +13 -20
- package/dist/host/codex/agents/checker.toml +13 -27
- package/dist/host/codex/agents/distiller.toml +9 -22
- package/dist/host/codex/agents/grok.toml +11 -30
- package/dist/host/codex/agents/opus.toml +14 -22
- package/dist/host/codex/agents/orkestrel.toml +1 -1
- package/dist/host/codex/agents/planner.toml +11 -28
- package/dist/host/codex/agents/researcher.toml +10 -22
- package/dist/host/codex/agents/reviewer.toml +11 -27
- package/dist/host/codex/agents/scout.toml +11 -17
- package/dist/host/codex/agents/verifier.toml +16 -12
- package/dist/host/codex/config.toml +20 -23
- package/dist/host/cursor/mcp.json +0 -4
- package/dist/host/cursor/rules/orchestration.mdc +12 -20
- package/dist/host/dotfiles/mcp.json +0 -4
- package/dist/host/dotfiles/oxlintrc.json +7 -0
- package/dist/host/guides/probe.md +9 -9
- package/dist/host/guides/scaffold.md +117 -71
- package/dist/host/guides/test.md +1 -1
- package/dist/host/manifest.json +322 -185
- package/dist/host/scripts/codex.sh +2 -2
- package/dist/host/tests/config.test.ts +68 -46
- package/dist/host/tests/policy.test.ts +1 -5
- package/dist/host/tests/setupPolicy.ts +179 -4
- package/dist/src/core/index.cjs +255 -84
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +94 -29
- package/dist/src/core/index.d.ts +94 -29
- package/dist/src/core/index.js +253 -85
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +55 -9
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +29 -4
- package/dist/src/server/index.d.ts +29 -4
- package/dist/src/server/index.js +56 -11
- package/dist/src/server/index.js.map +1 -1
- package/package.json +15 -11
- package/dist/host/CLAUDE.md +0 -61
- package/dist/host/agents/skills/orkestrel-prove-journey/agents/openai.yaml +0 -4
- package/dist/host/agents/transports/claude.md +0 -49
- package/dist/host/claude/agents/application.md +0 -36
- package/dist/host/claude/agents/sol.md +0 -61
- package/dist/host/claude/skills/orkestrel-polish-surface/SKILL.md +0 -12
- package/dist/host/codex/agents/application.toml +0 -25
- package/dist/host/codex/agents/sol.toml +0 -19
- /package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/references/integration.md +0 -0
- /package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/centralization.md +0 -0
- /package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/contract.md +0 -0
- /package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/research.md +0 -0
- /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/decide.md +0 -0
- /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/layer.md +0 -0
- /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/statechart.md +0 -0
- /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))
|