@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.
- package/dist/agents/skills/orkestrel-dispatch/scripts/bench.js +326 -91
- package/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +110 -14
- package/dist/agents/skills/orkestrel-dispatch/scripts/chain.js +280 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +188 -26
- package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +142 -43
- package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +240 -62
- package/dist/agents/skills/orkestrel-dispatch/scripts/patch.js +323 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +169 -32
- package/dist/agents/skills/orkestrel-dispatch/scripts/sweep.js +290 -69
- package/dist/agents/skills/orkestrel-falsify/scripts/mutate.js +560 -0
- package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +1052 -205
- package/dist/agents/skills/orkestrel-harden/scripts/measure.js +361 -0
- package/dist/agents/skills/orkestrel-harden/scripts/titles.js +455 -80
- package/dist/agents/skills/orkestrel-parallel/scripts/cases.js +327 -0
- package/dist/agents/skills/orkestrel-publish/scripts/compare.js +298 -51
- package/dist/agents/skills/orkestrel-publish/scripts/pins.js +170 -13
- package/dist/agents/skills/orkestrel-publish/scripts/swap.js +956 -191
- package/dist/agents/skills/orkestrel-publish/scripts/wave.js +707 -112
- package/dist/agents/skills/orkestrel-publish/scripts/window.js +235 -41
- package/dist/agents/skills/orkestrel-scout/scripts/map.js +341 -54
- package/dist/agents/templates/brief.md +1 -1
- package/dist/bin/main.js +51 -46
- package/dist/bin/main.js.map +1 -1
- package/dist/host/AGENTS.md +23 -21
- package/dist/host/agents/orchestration.md +43 -18
- package/dist/host/agents/platforms/windows.md +3 -3
- package/dist/host/agents/skills/orkestrel-align/SKILL.md +10 -13
- package/dist/host/agents/skills/orkestrel-align/references/fleet.md +4 -10
- package/dist/host/agents/skills/orkestrel-align/references/integration.md +1 -1
- package/dist/host/agents/skills/orkestrel-build/SKILL.md +16 -48
- package/dist/host/agents/skills/orkestrel-build/agents/openai.yaml +1 -1
- package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +19 -16
- package/dist/host/agents/skills/orkestrel-debrief/agents/openai.yaml +1 -1
- package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +15 -28
- package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +5 -6
- package/dist/host/agents/skills/orkestrel-debrief/references/retention.md +4 -6
- package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +31 -20
- package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +3 -4
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/bench.ts +366 -75
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +126 -16
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/chain.ts +323 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +220 -31
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +176 -33
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +294 -59
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/patch.ts +401 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +199 -34
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/sweep.ts +330 -69
- package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +10 -8
- package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +1 -5
- package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +1 -1
- package/dist/host/agents/skills/orkestrel-falsify/scripts/mutate.ts +701 -0
- package/dist/host/agents/skills/orkestrel-harden/SKILL.md +16 -12
- package/dist/host/agents/skills/orkestrel-harden/references/bindings.md +23 -81
- package/dist/host/agents/skills/orkestrel-harden/references/centralization.md +4 -5
- package/dist/host/agents/skills/orkestrel-harden/references/contract.md +2 -0
- package/dist/host/agents/skills/orkestrel-harden/references/document.md +33 -0
- package/dist/host/agents/skills/orkestrel-harden/references/equivalence.md +6 -3
- package/dist/host/agents/skills/orkestrel-harden/references/hardening.md +8 -4
- package/dist/host/agents/skills/orkestrel-harden/references/research.md +2 -0
- package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +1272 -279
- package/dist/host/agents/skills/orkestrel-harden/scripts/measure.ts +425 -0
- package/dist/host/agents/skills/orkestrel-harden/scripts/titles.ts +603 -98
- package/dist/host/agents/skills/orkestrel-journey/SKILL.md +75 -98
- package/dist/host/agents/skills/orkestrel-journey/references/captures.md +54 -17
- package/dist/host/agents/skills/orkestrel-journey/references/decide.md +1 -20
- package/dist/host/agents/skills/orkestrel-journey/references/layer.md +21 -31
- package/dist/host/agents/skills/orkestrel-journey/references/styles.md +1 -1
- package/dist/host/agents/skills/orkestrel-parallel/SKILL.md +28 -24
- package/dist/host/agents/skills/orkestrel-parallel/references/claude.md +15 -13
- package/dist/host/agents/skills/orkestrel-parallel/references/codex.md +1 -0
- package/dist/host/agents/skills/orkestrel-parallel/references/cursor.md +13 -13
- package/dist/host/agents/skills/orkestrel-parallel/scripts/cases.ts +389 -0
- package/dist/host/agents/skills/orkestrel-polish/SKILL.md +16 -20
- package/dist/host/agents/skills/orkestrel-polish/references/capture-harness.md +16 -26
- package/dist/host/agents/skills/orkestrel-publish/SKILL.md +12 -26
- package/dist/host/agents/skills/orkestrel-publish/agents/openai.yaml +2 -2
- package/dist/host/agents/skills/orkestrel-publish/references/adopt.md +23 -0
- package/dist/host/agents/skills/orkestrel-publish/references/release.md +4 -4
- package/dist/host/agents/skills/orkestrel-publish/references/wave.md +10 -9
- package/dist/host/agents/skills/orkestrel-publish/references/window.md +20 -25
- package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +348 -59
- package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +202 -22
- package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1061 -191
- package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +812 -140
- package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +280 -38
- package/dist/host/agents/skills/orkestrel-scout/SKILL.md +3 -3
- package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +428 -70
- package/dist/host/agents/templates/brief.md +1 -1
- package/dist/host/agents/transports/claude-cli.md +3 -3
- package/dist/host/agents/transports/codex.md +2 -7
- package/dist/host/agents/transports/cursor.md +2 -3
- package/dist/host/claude/AGENTS.md +2 -2
- package/dist/host/claude/agents/analyst.md +3 -3
- package/dist/host/claude/agents/astra.md +3 -3
- package/dist/host/claude/agents/builder.md +1 -1
- package/dist/host/claude/agents/checker.md +1 -1
- package/dist/host/claude/agents/distiller.md +1 -1
- package/dist/host/claude/agents/grok.md +1 -1
- package/dist/host/claude/agents/opus.md +2 -2
- package/dist/host/claude/agents/orkestrel.md +57 -90
- package/dist/host/claude/agents/researcher.md +1 -1
- package/dist/host/claude/agents/reviewer.md +4 -0
- package/dist/host/claude/agents/scout.md +1 -1
- package/dist/host/claude/agents/verifier.md +1 -1
- package/dist/host/claude/rules/application.md +14 -21
- package/dist/host/claude/rules/architecture.md +34 -44
- package/dist/host/claude/rules/browser.md +3 -4
- package/dist/host/claude/rules/campaign.md +2 -2
- package/dist/host/claude/rules/documentation.md +25 -12
- package/dist/host/claude/rules/names.md +9 -4
- package/dist/host/claude/rules/patterns.md +6 -7
- package/dist/host/claude/rules/portability.md +1 -1
- package/dist/host/claude/rules/quality.md +4 -5
- package/dist/host/claude/rules/styles.md +2 -2
- package/dist/host/claude/rules/tests.md +57 -114
- package/dist/host/claude/rules/typescript.md +7 -9
- package/dist/host/claude/rules/workspace.md +21 -19
- package/dist/host/claude/rules/writing.md +2 -5
- package/dist/host/claude/skills/orkestrel-dispatch/SKILL.md +1 -1
- package/dist/host/claude/skills/orkestrel-harden/SKILL.md +1 -1
- package/dist/host/claude/skills/orkestrel-publish/SKILL.md +1 -1
- package/dist/host/claude/skills/orkestrel-scout/SKILL.md +1 -1
- package/dist/host/codex/agents/analyst.toml +9 -3
- package/dist/host/codex/agents/astra.toml +3 -4
- package/dist/host/codex/agents/builder.toml +1 -1
- package/dist/host/codex/agents/opus.toml +3 -4
- package/dist/host/codex/agents/orkestrel.toml +1 -1
- package/dist/host/codex/agents/researcher.toml +2 -2
- package/dist/host/codex/agents/verifier.toml +1 -1
- package/dist/host/configs/helpers.ts +21 -1
- package/dist/host/configs/policy.ts +299 -15
- package/dist/host/dotfiles/oxlintrc.json +137 -3
- package/dist/host/guides/agent.md +111 -134
- package/dist/host/guides/capture.md +801 -0
- package/dist/host/guides/probe.md +27 -8
- package/dist/host/guides/scaffold.md +96 -18
- package/dist/host/guides/study.md +534 -440
- package/dist/host/guides/test.md +111 -44
- package/dist/host/manifest.json +160 -240
- package/dist/host/tests/config.test.ts +971 -187
- package/dist/host/tests/policy.test.ts +127 -1
- package/dist/host/tests/setupPolicy.ts +893 -33
- package/dist/src/core/index.cjs +214 -86
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +122 -38
- package/dist/src/core/index.d.ts +122 -38
- package/dist/src/core/index.js +209 -85
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +20 -27
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.js +21 -28
- package/dist/src/server/index.js.map +1 -1
- package/package.json +11 -11
- package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +0 -283
- package/dist/host/agents/skills/orkestrel-dispatch/references/bench.md +0 -25
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +0 -333
- package/dist/host/guides/supervisor.md +0 -4890
|
@@ -1,37 +1,92 @@
|
|
|
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`. A fractional cap
|
|
6
|
-
// truncates to whole milliseconds, with 1 ms the least. --status records
|
|
7
|
-
// `git status --porcelain` before and after beside the journal, which is how a read-only lane's
|
|
8
|
-
// containment is checked. Stdout carries up to two JSON lines. The spawn line
|
|
9
|
-
// `{"pid","journal","errors"}` prints as soon as the command starts, and the pid, as decimal digits
|
|
10
|
-
// and a newline, goes to `<journal>.pid`. A pid file an earlier run left on the same journal is
|
|
11
|
-
// removed before the spawn, so neither appears when the command cannot start. The summary line
|
|
12
|
-
// prints last, after the command exits, and the errors file ends with `exit=<code>`. At the cap,
|
|
13
|
-
// Windows kills the process tree; any other host kills the command alone, not its descendants.
|
|
14
|
-
// The launcher's own flags are read before `--` alone. Exit: the command's code; 124 when capped;
|
|
15
|
-
// 127 when the command could not start (the errors file names why); 64 on usage, including a
|
|
16
|
-
// journal and errors path that are the same file and a cap that is not a number of seconds from 0
|
|
17
|
-
// to 2147483.
|
|
18
1
|
import { spawn, spawnSync } from 'node:child_process';
|
|
19
|
-
import { appendFileSync, closeSync, mkdirSync, openSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { appendFileSync, closeSync, mkdirSync, openSync, realpathSync, rmSync, writeFileSync, } from 'node:fs';
|
|
20
3
|
import { dirname } from 'node:path';
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Holds the longest cap in whole seconds.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* The longest wait a Node timer holds is 2147483647 ms; floored to whole seconds, a longer cap would
|
|
9
|
+
* throw from `AbortSignal.timeout` after the spawn or clamp to 1 ms and kill the command at once.
|
|
10
|
+
*/
|
|
11
|
+
export const MAX_CAP_SECONDS = 2_147_483;
|
|
12
|
+
/** Lists the flags that take a value. */
|
|
13
|
+
export const VALUE_FLAGS = ['--journal', '--errors', '--cap'];
|
|
14
|
+
/** Lists every flag the launcher reads before the separator, value flags and switches alike. */
|
|
15
|
+
export const FLAGS = [...VALUE_FLAGS, '--status'];
|
|
16
|
+
/**
|
|
17
|
+
* Reads the value that follows a flag.
|
|
18
|
+
*
|
|
19
|
+
* @remarks
|
|
20
|
+
* The first occurrence of a flag wins, a following value that opens with `--` is a missing value,
|
|
21
|
+
* and the `--flag=value` form is not read.
|
|
22
|
+
*
|
|
23
|
+
* @param argv - The arguments after the script path.
|
|
24
|
+
* @param name - The flag, such as `--journal`.
|
|
25
|
+
* @returns The value after the flag, or undefined when the flag is absent, last, or followed by
|
|
26
|
+
* another flag.
|
|
27
|
+
*/
|
|
28
|
+
export function readOption(argv, name) {
|
|
29
|
+
const index = argv.indexOf(name);
|
|
30
|
+
const value = index === -1 ? undefined : argv[index + 1];
|
|
31
|
+
return value === undefined || value.startsWith('--') ? undefined : value;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Names the value flags a caller gave with no value.
|
|
35
|
+
*
|
|
36
|
+
* @param argv - The arguments after the script path.
|
|
37
|
+
* @param names - The flags that take a value.
|
|
38
|
+
* @returns Each named flag that appears last or is followed by another flag, in argument order.
|
|
39
|
+
*/
|
|
40
|
+
export function readMissingFlags(argv, names) {
|
|
41
|
+
return names.filter((name) => argv.includes(name) && readOption(argv, name) === undefined);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Names the flags a caller gave that the script does not read.
|
|
45
|
+
*
|
|
46
|
+
* @remarks
|
|
47
|
+
* A `--flag=value` argument is unknown, because the script reads no such form. A bare `--` ends the
|
|
48
|
+
* scan, so the command after it keeps every argument.
|
|
49
|
+
*
|
|
50
|
+
* @param argv - The arguments after the script path.
|
|
51
|
+
* @param flags - Every flag the script reads, value flags and switches alike.
|
|
52
|
+
* @returns Each argument that opens with `--` and is not a read flag, in argument order.
|
|
53
|
+
*/
|
|
54
|
+
export function readUnknownFlags(argv, flags) {
|
|
55
|
+
const end = argv.indexOf('--');
|
|
56
|
+
return (end === -1 ? argv : argv.slice(0, end)).filter((argument) => argument.startsWith('--') && !flags.includes(argument));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Computes the cap in whole milliseconds that the timer takes.
|
|
60
|
+
*
|
|
61
|
+
* @remarks
|
|
62
|
+
* `AbortSignal.timeout` throws on a fractional delay, which `setTimeout` truncates and raises to 1 ms
|
|
63
|
+
* from below, so the cap takes that form before the command starts.
|
|
64
|
+
*
|
|
65
|
+
* @param cap - The cap in seconds.
|
|
66
|
+
* @returns The whole milliseconds, at least 1.
|
|
67
|
+
*/
|
|
68
|
+
export function computeCapMilliseconds(cap) {
|
|
28
69
|
return Math.max(1, Math.trunc(cap * 1000));
|
|
29
70
|
}
|
|
30
|
-
|
|
31
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Reads the working tree's `git status --porcelain` output.
|
|
73
|
+
*
|
|
74
|
+
* @param cwd - The directory to read the status in. Default: the current directory.
|
|
75
|
+
* @returns The porcelain text, or a `git status failed:` line carrying git's own message.
|
|
76
|
+
*/
|
|
77
|
+
export function readGitStatus(cwd) {
|
|
78
|
+
const result = spawnSync('git', ['status', '--porcelain'], {
|
|
79
|
+
encoding: 'utf8',
|
|
80
|
+
...(cwd === undefined ? {} : { cwd }),
|
|
81
|
+
});
|
|
32
82
|
return result.status === 0 ? result.stdout : `git status failed: ${result.stderr}`;
|
|
33
83
|
}
|
|
34
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Kills a process and every process it started, at once and without a chance to clean up.
|
|
86
|
+
*
|
|
87
|
+
* @param pid - The process to kill. A process that is already gone is not an error.
|
|
88
|
+
*/
|
|
89
|
+
export function forceKillTree(pid) {
|
|
35
90
|
if (process.platform === 'win32') {
|
|
36
91
|
spawnSync('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore' });
|
|
37
92
|
return;
|
|
@@ -41,7 +96,33 @@ function killTree(pid) {
|
|
|
41
96
|
}
|
|
42
97
|
catch { }
|
|
43
98
|
}
|
|
44
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Ends a command that is still running and aborts the controller to say the cap ended it.
|
|
101
|
+
*
|
|
102
|
+
* @remarks
|
|
103
|
+
* The cap's timer cannot be cleared, so it can elapse after the command exited on its own. The
|
|
104
|
+
* controller aborts only when the command is still running at that moment, which is how the run
|
|
105
|
+
* records that the cap, and not the command, ended it; a reused pid is never this command's.
|
|
106
|
+
*
|
|
107
|
+
* @param child - The running command.
|
|
108
|
+
* @param ended - The controller that aborts when the cap ended the command.
|
|
109
|
+
*/
|
|
110
|
+
export function endRunning(child, ended) {
|
|
111
|
+
if (child.pid !== undefined && child.exitCode === null && child.signalCode === null) {
|
|
112
|
+
ended.abort();
|
|
113
|
+
forceKillTree(child.pid);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Starts the command with its output sent to the journal and errors files.
|
|
118
|
+
*
|
|
119
|
+
* @param command - The executable.
|
|
120
|
+
* @param args - The command's arguments, passed verbatim.
|
|
121
|
+
* @param journal - The open journal file descriptor that receives stdout.
|
|
122
|
+
* @param errors - The open errors file descriptor that receives stderr.
|
|
123
|
+
* @returns The child process, or the failure when the spawn throws.
|
|
124
|
+
*/
|
|
125
|
+
export function startCommand(command, args, journal, errors) {
|
|
45
126
|
try {
|
|
46
127
|
return spawn(command, args, { stdio: ['ignore', journal, errors], windowsHide: true });
|
|
47
128
|
}
|
|
@@ -53,16 +134,33 @@ function startCommand(command, args, journal, errors) {
|
|
|
53
134
|
};
|
|
54
135
|
}
|
|
55
136
|
}
|
|
56
|
-
|
|
137
|
+
/**
|
|
138
|
+
* Waits for a started command to close.
|
|
139
|
+
*
|
|
140
|
+
* @param child - The started command.
|
|
141
|
+
* @returns How it ended: its code and signal, or the error that stopped it.
|
|
142
|
+
*/
|
|
143
|
+
export function waitForExit(child) {
|
|
57
144
|
return new Promise((resolve) => {
|
|
58
145
|
child.on('error', (error) => resolve({ code: null, signal: null, error: error.message }));
|
|
59
146
|
child.on('close', (code, signal) => resolve({ code, signal, error: undefined }));
|
|
60
147
|
});
|
|
61
148
|
}
|
|
62
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Runs the launcher on its arguments.
|
|
151
|
+
*
|
|
152
|
+
* @param argv - The arguments after the script path.
|
|
153
|
+
* @returns The exit code: the command's, 124 when capped, 127 when it could not start, 64 on usage.
|
|
154
|
+
*/
|
|
155
|
+
export async function main(argv) {
|
|
63
156
|
const separator = argv.indexOf('--');
|
|
64
157
|
// The launcher's own flags end at the separator; everything after it belongs to the command.
|
|
65
158
|
const own = separator === -1 ? argv : argv.slice(0, separator);
|
|
159
|
+
const unknown = readUnknownFlags(argv, FLAGS);
|
|
160
|
+
if (unknown.length > 0) {
|
|
161
|
+
console.error(`launch: ${unknown.join(', ')} is not a flag this launcher reads before --; flags: ${FLAGS.join(' ')}`);
|
|
162
|
+
return 64;
|
|
163
|
+
}
|
|
66
164
|
const journal = readOption(own, '--journal');
|
|
67
165
|
const errors = readOption(own, '--errors');
|
|
68
166
|
const cap = readMissingFlags(own, ['--cap']).length > 0
|
|
@@ -86,7 +184,7 @@ async function main(argv) {
|
|
|
86
184
|
mkdirSync(dirname(journal), { recursive: true });
|
|
87
185
|
mkdirSync(dirname(errors), { recursive: true });
|
|
88
186
|
if (status)
|
|
89
|
-
writeFileSync(`${journal}.status-before.txt`,
|
|
187
|
+
writeFileSync(`${journal}.status-before.txt`, readGitStatus());
|
|
90
188
|
const journalFd = openSync(journal, 'w');
|
|
91
189
|
const errorsFd = openSync(errors, 'w');
|
|
92
190
|
rmSync(`${journal}.pid`, { force: true });
|
|
@@ -97,25 +195,24 @@ async function main(argv) {
|
|
|
97
195
|
writeFileSync(`${journal}.pid`, `${child.pid}\n`);
|
|
98
196
|
console.log(JSON.stringify({ pid: child.pid, journal, errors }));
|
|
99
197
|
}
|
|
100
|
-
// The
|
|
101
|
-
//
|
|
102
|
-
|
|
198
|
+
// The ended controller aborts only when the cap found the command still running, so whether it
|
|
199
|
+
// aborted is whether the cap ended the run.
|
|
200
|
+
const ended = new AbortController();
|
|
103
201
|
const deadline = child === undefined || capMilliseconds === undefined
|
|
104
202
|
? undefined
|
|
105
203
|
: AbortSignal.timeout(capMilliseconds);
|
|
106
204
|
deadline?.addEventListener('abort', () => {
|
|
107
|
-
if (child
|
|
108
|
-
|
|
109
|
-
}
|
|
205
|
+
if (child !== undefined)
|
|
206
|
+
endRunning(child, ended);
|
|
110
207
|
}, { once: true });
|
|
111
208
|
const exit = 'error' in launched ? launched : await waitForExit(launched);
|
|
112
|
-
const capped =
|
|
209
|
+
const capped = ended.signal.aborted;
|
|
113
210
|
closeSync(journalFd);
|
|
114
211
|
closeSync(errorsFd);
|
|
115
212
|
const durationMs = Date.now() - started;
|
|
116
213
|
appendFileSync(errors, `${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`);
|
|
117
214
|
if (status)
|
|
118
|
-
writeFileSync(`${journal}.status-after.txt`,
|
|
215
|
+
writeFileSync(`${journal}.status-after.txt`, readGitStatus());
|
|
119
216
|
console.log(JSON.stringify({
|
|
120
217
|
pid: child?.pid,
|
|
121
218
|
exit: exit.error === undefined ? exit.code : 127,
|
|
@@ -130,6 +227,8 @@ async function main(argv) {
|
|
|
130
227
|
return 127;
|
|
131
228
|
return capped ? 124 : (exit.code ?? 1);
|
|
132
229
|
}
|
|
133
|
-
|
|
134
|
-
process.
|
|
135
|
-
|
|
230
|
+
if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename) {
|
|
231
|
+
main(process.argv.slice(2)).then((code) => {
|
|
232
|
+
process.exitCode = code;
|
|
233
|
+
});
|
|
234
|
+
}
|
|
@@ -1,26 +1,139 @@
|
|
|
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`;
|
|
5
|
-
// `codex login --device-auth` with its output journaled to
|
|
6
|
-
// verification URL and the one-time code the moment the journal
|
|
7
|
-
// `codex login status` every 5 seconds until it answers or the cap (default 600
|
|
8
|
-
// kills the login by its pid.
|
|
9
|
-
//
|
|
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 {
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, realpathSync } from 'node:fs';
|
|
16
|
+
import { dirname } from 'node:path';
|
|
17
|
+
/** Names the file that journals the device-auth login output. */
|
|
18
|
+
export const JOURNAL = 'tmp/codex/login.log';
|
|
19
|
+
/** Holds the milliseconds between two reads of the login status. */
|
|
20
|
+
export const POLL_MS = 5_000;
|
|
21
|
+
/** Holds the seconds the poll runs when the call gives no cap. */
|
|
22
|
+
export const DEFAULT_CAP = 600;
|
|
23
|
+
/** Holds the longest cap in whole seconds, so the cap in milliseconds stays a finite timer value. */
|
|
24
|
+
export const MAX_CAP_SECONDS = 2_147_483;
|
|
25
|
+
/** Holds the fewest milliseconds one status read gets, so a read that starts at the cap can still answer. */
|
|
26
|
+
export const STATUS_GRACE_MS = 1_000;
|
|
27
|
+
/** Matches the verification URL the login prints. */
|
|
28
|
+
export const URL_PATTERN = /https?:\/\/[^\s"'<>]+/u;
|
|
29
|
+
/** Matches the one-time code the login prints, such as `ABCD-12345`. */
|
|
30
|
+
export const CODE_PATTERN = /\b([A-Z0-9]{4,5}-[A-Z0-9]{4,5})\b/u;
|
|
31
|
+
/** Lists the flags that take a value. */
|
|
32
|
+
export const VALUE_FLAGS = ['--cap'];
|
|
33
|
+
/** Lists every flag the script reads, value flags and switches alike. */
|
|
34
|
+
export const FLAGS = [...VALUE_FLAGS, '--codex', '--claude'];
|
|
35
|
+
/** Holds the real Codex commands and the journal and interval the script uses. */
|
|
36
|
+
export const CODEX_FLOW = {
|
|
37
|
+
status: { command: 'codex', args: ['login', 'status'] },
|
|
38
|
+
device: { command: 'codex', args: ['login', '--device-auth'] },
|
|
39
|
+
journal: JOURNAL,
|
|
40
|
+
intervalMs: POLL_MS,
|
|
41
|
+
};
|
|
42
|
+
/** Holds the real Claude status command. */
|
|
43
|
+
export const CLAUDE_STATUS = { command: 'claude', args: ['auth', 'status'] };
|
|
44
|
+
/**
|
|
45
|
+
* Reads the value that follows a flag.
|
|
46
|
+
*
|
|
47
|
+
* @remarks
|
|
48
|
+
* The first occurrence of a flag wins, a following value that opens with `--` is a missing value,
|
|
49
|
+
* and the `--flag=value` form is not read.
|
|
50
|
+
*
|
|
51
|
+
* @param argv - The arguments after the script path.
|
|
52
|
+
* @param name - The flag, such as `--cap`.
|
|
53
|
+
* @returns The value after the flag, or undefined when the flag is absent, last, or followed by
|
|
54
|
+
* another flag.
|
|
55
|
+
*/
|
|
56
|
+
export function readOption(argv, name) {
|
|
57
|
+
const index = argv.indexOf(name);
|
|
58
|
+
const value = index === -1 ? undefined : argv[index + 1];
|
|
59
|
+
return value === undefined || value.startsWith('--') ? undefined : value;
|
|
22
60
|
}
|
|
23
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Names the value flags a caller gave with no value.
|
|
63
|
+
*
|
|
64
|
+
* @param argv - The arguments after the script path.
|
|
65
|
+
* @param names - The flags that take a value.
|
|
66
|
+
* @returns Each named flag that appears last or is followed by another flag, in argument order.
|
|
67
|
+
*/
|
|
68
|
+
export function readMissingFlags(argv, names) {
|
|
69
|
+
return names.filter((name) => argv.includes(name) && readOption(argv, name) === undefined);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Names the flags a caller gave that the script does not read.
|
|
73
|
+
*
|
|
74
|
+
* @remarks
|
|
75
|
+
* A `--flag=value` argument is unknown, because the script reads no such form. A bare `--` ends the
|
|
76
|
+
* scan.
|
|
77
|
+
*
|
|
78
|
+
* @param argv - The arguments after the script path.
|
|
79
|
+
* @param flags - Every flag the script reads, value flags and switches alike.
|
|
80
|
+
* @returns Each argument that opens with `--` and is not a read flag, in argument order.
|
|
81
|
+
*/
|
|
82
|
+
export function readUnknownFlags(argv, flags) {
|
|
83
|
+
const end = argv.indexOf('--');
|
|
84
|
+
return (end === -1 ? argv : argv.slice(0, end)).filter((argument) => argument.startsWith('--') && !flags.includes(argument));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Parses the `--cap` value into a number of seconds.
|
|
88
|
+
*
|
|
89
|
+
* @param text - The value after `--cap`, or undefined when the flag is absent.
|
|
90
|
+
* @returns The default cap when the flag is absent, the number when it is finite, above 0, and at
|
|
91
|
+
* most the longest cap, and undefined otherwise.
|
|
92
|
+
*/
|
|
93
|
+
export function parseCap(text) {
|
|
94
|
+
if (text === undefined)
|
|
95
|
+
return DEFAULT_CAP;
|
|
96
|
+
const cap = Number(text);
|
|
97
|
+
return Number.isFinite(cap) && cap > 0 && cap <= MAX_CAP_SECONDS ? cap : undefined;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Runs a status command and reads whether it reports a login.
|
|
101
|
+
*
|
|
102
|
+
* @param command - The executable.
|
|
103
|
+
* @param args - The status arguments.
|
|
104
|
+
* @param timeoutMs - The milliseconds the command may run before it is ended, above 0; omit it for no
|
|
105
|
+
* bound.
|
|
106
|
+
* @returns Live when the command exits 0, with the text it printed, the reason it could not start,
|
|
107
|
+
* or the note that it did not answer in time.
|
|
108
|
+
*/
|
|
109
|
+
export function readLoginStatus(command, args, timeoutMs) {
|
|
110
|
+
const result = spawnSync(command, args, {
|
|
111
|
+
encoding: 'utf8',
|
|
112
|
+
windowsHide: true,
|
|
113
|
+
timeout: timeoutMs,
|
|
114
|
+
});
|
|
115
|
+
if (result.error !== undefined) {
|
|
116
|
+
if ('code' in result.error && result.error.code === 'ETIMEDOUT') {
|
|
117
|
+
return { started: true, live: false, text: `${command} did not answer within the cap` };
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
started: false,
|
|
121
|
+
live: false,
|
|
122
|
+
text: `${command} could not start: ${result.error.message}`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
started: true,
|
|
127
|
+
live: result.status === 0,
|
|
128
|
+
text: `${result.stdout ?? ''}${result.stderr ?? ''}`.trim(),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Asks a process and the processes it started to end.
|
|
133
|
+
*
|
|
134
|
+
* @param pid - The process to end. A process that is already gone is not an error.
|
|
135
|
+
*/
|
|
136
|
+
export function terminateTree(pid) {
|
|
24
137
|
if (process.platform === 'win32') {
|
|
25
138
|
spawnSync('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore' });
|
|
26
139
|
return;
|
|
@@ -30,63 +143,113 @@ function killTree(pid) {
|
|
|
30
143
|
}
|
|
31
144
|
catch { }
|
|
32
145
|
}
|
|
33
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Waits a number of milliseconds.
|
|
148
|
+
*
|
|
149
|
+
* @param ms - The milliseconds to wait.
|
|
150
|
+
* @returns A promise that resolves when the time has passed.
|
|
151
|
+
*/
|
|
152
|
+
export function waitFor(ms) {
|
|
34
153
|
return new Promise((resolve) => {
|
|
35
154
|
setTimeout(resolve, ms);
|
|
36
155
|
});
|
|
37
156
|
}
|
|
38
|
-
|
|
39
|
-
|
|
157
|
+
/**
|
|
158
|
+
* Polls the login journal and the login status until the bench is logged in or the cap ends.
|
|
159
|
+
*
|
|
160
|
+
* @param started - The time the login began, in milliseconds since the epoch.
|
|
161
|
+
* @param capSeconds - The seconds the poll may run.
|
|
162
|
+
* @param flow - The status command, the journal to read, and the milliseconds between polls.
|
|
163
|
+
* @param announce - Receives each line the poll tells the user, such as the URL to open.
|
|
164
|
+
* @param url - The verification URL already read, if any.
|
|
165
|
+
* @param code - The one-time code already read, if any.
|
|
166
|
+
* @returns The URL, the code, and whether the bench is logged in.
|
|
167
|
+
*/
|
|
168
|
+
export async function pollLogin(started, capSeconds, flow, announce, url, code) {
|
|
169
|
+
const deadline = started + capSeconds * 1000;
|
|
170
|
+
if (Date.now() >= deadline)
|
|
40
171
|
return { url, code, live: false };
|
|
41
|
-
await waitFor(
|
|
42
|
-
const text = existsSync(
|
|
172
|
+
await waitFor(flow.intervalMs);
|
|
173
|
+
const text = existsSync(flow.journal) ? readFileSync(flow.journal, 'utf8') : '';
|
|
43
174
|
const nextUrl = url ?? text.match(URL_PATTERN)?.[0];
|
|
44
175
|
if (url === undefined && nextUrl !== undefined)
|
|
45
|
-
|
|
176
|
+
announce(`login: open ${nextUrl}`);
|
|
46
177
|
const nextCode = code ?? text.match(CODE_PATTERN)?.[1];
|
|
47
|
-
if (code === undefined && nextCode !== undefined)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (
|
|
178
|
+
if (code === undefined && nextCode !== undefined)
|
|
179
|
+
announce(`login: enter the code ${nextCode}`);
|
|
180
|
+
const remaining = Math.max(deadline - Date.now(), STATUS_GRACE_MS);
|
|
181
|
+
if (readLoginStatus(flow.status.command, flow.status.args, remaining).live) {
|
|
51
182
|
return { url: nextUrl, code: nextCode, live: true };
|
|
52
183
|
}
|
|
53
|
-
return pollLogin(started, capSeconds, nextUrl, nextCode);
|
|
184
|
+
return pollLogin(started, capSeconds, flow, announce, nextUrl, nextCode);
|
|
54
185
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
186
|
+
/**
|
|
187
|
+
* Recovers the Codex login with the device-auth flow.
|
|
188
|
+
*
|
|
189
|
+
* @param capSeconds - The seconds the poll may run.
|
|
190
|
+
* @param flow - The status and device-auth commands, the journal, and the poll interval.
|
|
191
|
+
* @param announce - Receives each line the poll tells the user, such as the URL to open.
|
|
192
|
+
* @returns Exit 0 when logged in, 3 when not, with the report to print.
|
|
193
|
+
*/
|
|
194
|
+
export async function loginCodex(capSeconds, flow, announce) {
|
|
195
|
+
const before = readLoginStatus(flow.status.command, flow.status.args, capSeconds * 1000);
|
|
196
|
+
// A Codex that cannot start cannot log in either, so the device-auth login never spawns.
|
|
197
|
+
if (before.live || !before.started) {
|
|
198
|
+
return {
|
|
199
|
+
report: { bench: 'codex', live: before.live, status: before.text },
|
|
200
|
+
exit: before.live ? 0 : 3,
|
|
201
|
+
};
|
|
60
202
|
}
|
|
61
|
-
mkdirSync(
|
|
62
|
-
const journal = openSync(
|
|
203
|
+
mkdirSync(dirname(flow.journal), { recursive: true });
|
|
204
|
+
const journal = openSync(flow.journal, 'w');
|
|
63
205
|
const started = Date.now();
|
|
64
|
-
const child = spawn(
|
|
206
|
+
const child = spawn(flow.device.command, flow.device.args, {
|
|
65
207
|
stdio: ['ignore', journal, journal],
|
|
66
208
|
windowsHide: true,
|
|
67
209
|
});
|
|
68
|
-
const { url, code, live } = await pollLogin(started, capSeconds);
|
|
210
|
+
const { url, code, live } = await pollLogin(started, capSeconds, flow, announce);
|
|
69
211
|
if (child.exitCode === null && child.pid !== undefined)
|
|
70
|
-
|
|
212
|
+
terminateTree(child.pid);
|
|
71
213
|
closeSync(journal);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
214
|
+
return {
|
|
215
|
+
report: {
|
|
216
|
+
bench: 'codex',
|
|
217
|
+
live,
|
|
218
|
+
url,
|
|
219
|
+
code,
|
|
220
|
+
durationMs: Date.now() - started,
|
|
221
|
+
journal: flow.journal,
|
|
222
|
+
status: readLoginStatus(flow.status.command, flow.status.args, STATUS_GRACE_MS).text,
|
|
223
|
+
},
|
|
224
|
+
exit: live ? 0 : 3,
|
|
225
|
+
};
|
|
82
226
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
227
|
+
/**
|
|
228
|
+
* Reads the Claude login status.
|
|
229
|
+
*
|
|
230
|
+
* @param status - The command that reports the Claude login.
|
|
231
|
+
* @returns Exit 0 when logged in, 3 when not, with the report to print.
|
|
232
|
+
*/
|
|
233
|
+
export function loginClaude(status) {
|
|
234
|
+
const read = readLoginStatus(status.command, status.args);
|
|
235
|
+
return {
|
|
236
|
+
report: { bench: 'claude', live: read.live, status: read.text },
|
|
237
|
+
exit: read.live ? 0 : 3,
|
|
238
|
+
};
|
|
87
239
|
}
|
|
88
|
-
|
|
89
|
-
|
|
240
|
+
/**
|
|
241
|
+
* Runs the script on its arguments.
|
|
242
|
+
*
|
|
243
|
+
* @param argv - The arguments after the script path.
|
|
244
|
+
* @returns The exit code: 0 when logged in, 3 when not, 64 on usage.
|
|
245
|
+
*/
|
|
246
|
+
export async function main(argv) {
|
|
247
|
+
const unknown = readUnknownFlags(argv, FLAGS);
|
|
248
|
+
if (unknown.length > 0) {
|
|
249
|
+
console.error(`login: ${unknown.join(', ')} is not a flag this script reads; usage: login.ts --codex [--cap SECONDS] | --claude`);
|
|
250
|
+
return 64;
|
|
251
|
+
}
|
|
252
|
+
const missing = readMissingFlags(argv, VALUE_FLAGS);
|
|
90
253
|
if (missing.length > 0) {
|
|
91
254
|
console.error(`login: ${missing.join(', ')} given with no value`);
|
|
92
255
|
return 64;
|
|
@@ -95,13 +258,28 @@ async function main(argv) {
|
|
|
95
258
|
console.error('usage: login.ts names one bench: --codex [--cap SECONDS] | --claude');
|
|
96
259
|
return 64;
|
|
97
260
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return
|
|
261
|
+
const cap = parseCap(readOption(argv, '--cap'));
|
|
262
|
+
if (cap === undefined) {
|
|
263
|
+
console.error(`login: --cap takes a number of seconds above 0 and at most ${String(MAX_CAP_SECONDS)}`);
|
|
264
|
+
return 64;
|
|
265
|
+
}
|
|
266
|
+
if (argv.includes('--codex')) {
|
|
267
|
+
const outcome = await loginCodex(cap, CODEX_FLOW, (line) => {
|
|
268
|
+
console.error(line);
|
|
269
|
+
});
|
|
270
|
+
console.log(JSON.stringify(outcome.report));
|
|
271
|
+
return outcome.exit;
|
|
272
|
+
}
|
|
273
|
+
if (argv.includes('--claude')) {
|
|
274
|
+
const outcome = loginClaude(CLAUDE_STATUS);
|
|
275
|
+
console.log(JSON.stringify(outcome.report));
|
|
276
|
+
return outcome.exit;
|
|
277
|
+
}
|
|
102
278
|
console.error('usage: login.ts --codex [--cap SECONDS] | --claude');
|
|
103
279
|
return 64;
|
|
104
280
|
}
|
|
105
|
-
|
|
106
|
-
process.
|
|
107
|
-
|
|
281
|
+
if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename) {
|
|
282
|
+
main(process.argv.slice(2)).then((code) => {
|
|
283
|
+
process.exitCode = code;
|
|
284
|
+
});
|
|
285
|
+
}
|