@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
|
@@ -6,32 +6,183 @@
|
|
|
6
6
|
// break ends the last line. A citation naming a bare filename that no root-relative
|
|
7
7
|
// file matches resolves when exactly one file under the root carries that name, outside
|
|
8
8
|
// node_modules, dist, tmp, .git, .orkestrel, and .idea. The summary lists every citation that does
|
|
9
|
-
// not resolve, with the reason.
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
9
|
+
// not resolve, with the reason. The script refuses a flag it does not read, the `--flag=value` form
|
|
10
|
+
// included. Exit 0 when all resolve, 3 when any does not, 64 on usage.
|
|
11
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs'
|
|
12
|
+
import { basename, join, relative, resolve } from 'node:path'
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
/** Matches one `file:line` or `file:line-line` citation in a text. */
|
|
15
|
+
export const CITATION =
|
|
15
16
|
/(?<![\w/\\.-])((?:\.{1,2}[/\\])?\.?[A-Za-z0-9_@][A-Za-z0-9_./\\@-]*\.[a-z]{1,5}):(\d+)(?:[-–](\d+))?(?![\w:])/gu
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
/** Names the directories a tree walk skips. */
|
|
19
|
+
export const SKIPPED_DIRECTORIES: ReadonlySet<string> = new Set([
|
|
20
|
+
'node_modules',
|
|
21
|
+
'.git',
|
|
22
|
+
'dist',
|
|
23
|
+
'tmp',
|
|
24
|
+
'.orkestrel',
|
|
25
|
+
'.idea',
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
/** Lists the flags that take a value. */
|
|
29
|
+
export const VALUE_FLAGS: readonly string[] = ['--root']
|
|
30
|
+
|
|
31
|
+
/** Lists every flag the script reads, value flags and switches alike. */
|
|
32
|
+
export const FLAGS: readonly string[] = [...VALUE_FLAGS, '--json']
|
|
33
|
+
|
|
34
|
+
/** Holds the usage line printed when a call names no report. */
|
|
35
|
+
export const USAGE = 'usage: cite.ts PATH [--root DIR] [--json]'
|
|
36
|
+
|
|
37
|
+
/** Describes one citation that did not resolve and why. */
|
|
38
|
+
export interface Unresolved {
|
|
18
39
|
readonly citation: string
|
|
19
40
|
readonly reason: string
|
|
20
41
|
}
|
|
21
42
|
|
|
22
|
-
|
|
43
|
+
/** Describes where a citation's file resolved, or why it did not. */
|
|
44
|
+
export interface Resolution {
|
|
23
45
|
readonly path: string | undefined
|
|
24
46
|
readonly reason: string | undefined
|
|
25
47
|
}
|
|
26
48
|
|
|
27
|
-
|
|
49
|
+
/** Describes one citation read from a text: its file, its line, and the end of its range. */
|
|
50
|
+
export interface Citation {
|
|
28
51
|
readonly citation: string
|
|
29
52
|
readonly file: string
|
|
30
53
|
readonly line: number
|
|
31
54
|
readonly end: number | undefined
|
|
32
55
|
}
|
|
33
56
|
|
|
34
|
-
|
|
57
|
+
/** Describes one file a tree walk found, with the modification time it carries. */
|
|
58
|
+
export interface WalkedFile {
|
|
59
|
+
readonly path: string
|
|
60
|
+
readonly modified: number
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Reads the value that follows a flag.
|
|
65
|
+
*
|
|
66
|
+
* @remarks
|
|
67
|
+
* The first occurrence of a flag wins, a following value that opens with `--` is a missing value,
|
|
68
|
+
* and the `--flag=value` form is not read.
|
|
69
|
+
*
|
|
70
|
+
* @param argv - The arguments after the script path.
|
|
71
|
+
* @param name - The flag, such as `--root`.
|
|
72
|
+
* @returns The value after the flag, or undefined when the flag is absent, last, or followed by
|
|
73
|
+
* another flag.
|
|
74
|
+
*/
|
|
75
|
+
export function readOption(argv: readonly string[], name: string): string | undefined {
|
|
76
|
+
const index = argv.indexOf(name)
|
|
77
|
+
const value = index === -1 ? undefined : argv[index + 1]
|
|
78
|
+
return value === undefined || value.startsWith('--') ? undefined : value
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Names the value flags a caller gave with no value.
|
|
83
|
+
*
|
|
84
|
+
* @param argv - The arguments after the script path.
|
|
85
|
+
* @param names - The flags that take a value.
|
|
86
|
+
* @returns Each named flag that appears last or is followed by another flag, in argument order.
|
|
87
|
+
*/
|
|
88
|
+
export function readMissingFlags(
|
|
89
|
+
argv: readonly string[],
|
|
90
|
+
names: readonly string[],
|
|
91
|
+
): readonly string[] {
|
|
92
|
+
return names.filter((name) => argv.includes(name) && readOption(argv, name) === undefined)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Names the flags a caller gave that the script does not read.
|
|
97
|
+
*
|
|
98
|
+
* @remarks
|
|
99
|
+
* A `--flag=value` argument is unknown, because the script reads no such form. A bare `--` ends the
|
|
100
|
+
* scan.
|
|
101
|
+
*
|
|
102
|
+
* @param argv - The arguments after the script path.
|
|
103
|
+
* @param flags - Every flag the script reads, value flags and switches alike.
|
|
104
|
+
* @returns Each argument that opens with `--` and is not a read flag, in argument order.
|
|
105
|
+
*/
|
|
106
|
+
export function readUnknownFlags(
|
|
107
|
+
argv: readonly string[],
|
|
108
|
+
flags: readonly string[],
|
|
109
|
+
): readonly string[] {
|
|
110
|
+
const end = argv.indexOf('--')
|
|
111
|
+
return (end === -1 ? argv : argv.slice(0, end)).filter(
|
|
112
|
+
(argument) => argument.startsWith('--') && !flags.includes(argument),
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Normalizes a host path to forward slashes.
|
|
118
|
+
*
|
|
119
|
+
* @param path - A host path.
|
|
120
|
+
* @returns The same path with every backslash replaced by a slash.
|
|
121
|
+
*/
|
|
122
|
+
export function normalizeSlashes(path: string): string {
|
|
123
|
+
return path.split('\\').join('/')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Lists every regular file beneath a directory, skipping the directories the script never reads.
|
|
128
|
+
*
|
|
129
|
+
* @remarks
|
|
130
|
+
* A skipped name matches a directory at any depth, exactly and case-sensitively.
|
|
131
|
+
*
|
|
132
|
+
* @param directory - The directory to walk.
|
|
133
|
+
* @param skipped - The directory names to skip. Default: the skipped directories.
|
|
134
|
+
* @returns Each file's forward-slash path and modification time, in walk order.
|
|
135
|
+
* @throws Thrown when `directory` does not exist or cannot be read.
|
|
136
|
+
*/
|
|
137
|
+
export function listFiles(
|
|
138
|
+
directory: string,
|
|
139
|
+
skipped: ReadonlySet<string> = SKIPPED_DIRECTORIES,
|
|
140
|
+
): readonly WalkedFile[] {
|
|
141
|
+
const files: WalkedFile[] = []
|
|
142
|
+
const pending = [directory]
|
|
143
|
+
while (pending.length > 0) {
|
|
144
|
+
const current = pending.pop()
|
|
145
|
+
if (current === undefined) break
|
|
146
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
147
|
+
if (skipped.has(entry.name)) continue
|
|
148
|
+
const path = join(current, entry.name)
|
|
149
|
+
if (entry.isDirectory()) pending.push(path)
|
|
150
|
+
else if (entry.isFile()) {
|
|
151
|
+
files.push({ path: normalizeSlashes(path), modified: statSync(path).mtimeMs })
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return files
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Extracts every citation a text carries.
|
|
160
|
+
*
|
|
161
|
+
* @param text - The report text.
|
|
162
|
+
* @returns Each citation in text order, with its file, its line, and the end of a range.
|
|
163
|
+
*/
|
|
164
|
+
export function extractCitations(text: string): readonly Citation[] {
|
|
165
|
+
return [...text.matchAll(CITATION)].flatMap((match): Citation[] =>
|
|
166
|
+
match[1] === undefined
|
|
167
|
+
? []
|
|
168
|
+
: [
|
|
169
|
+
{
|
|
170
|
+
citation: match[0],
|
|
171
|
+
file: match[1],
|
|
172
|
+
line: Number(match[2]),
|
|
173
|
+
end: match[3] === undefined ? undefined : Number(match[3]),
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Indexes every file under a root by its basename.
|
|
181
|
+
*
|
|
182
|
+
* @param root - The directory to walk.
|
|
183
|
+
* @returns Each basename mapped to the root-relative forward-slash paths that carry it.
|
|
184
|
+
*/
|
|
185
|
+
export function indexBasenames(root: string): ReadonlyMap<string, readonly string[]> {
|
|
35
186
|
return new Map(
|
|
36
187
|
[...Map.groupBy(listFiles(root), (file) => basename(file.path))].map(([name, files]) => [
|
|
37
188
|
name,
|
|
@@ -40,7 +191,13 @@ function indexBasenames(root: string): ReadonlyMap<string, readonly string[]> {
|
|
|
40
191
|
)
|
|
41
192
|
}
|
|
42
193
|
|
|
43
|
-
|
|
194
|
+
/**
|
|
195
|
+
* Counts the lines of a file.
|
|
196
|
+
*
|
|
197
|
+
* @param path - The file to read.
|
|
198
|
+
* @returns The number of lines, where a trailing line break ends the last line and an empty file has none.
|
|
199
|
+
*/
|
|
200
|
+
export function countLines(path: string): number {
|
|
44
201
|
const text = readFileSync(path, 'utf8')
|
|
45
202
|
if (text === '') return 0
|
|
46
203
|
const lines = text.split(/\r\n|\n/)
|
|
@@ -48,17 +205,38 @@ function countLines(path: string): number {
|
|
|
48
205
|
return lines.at(-1) === '' ? lines.length - 1 : lines.length
|
|
49
206
|
}
|
|
50
207
|
|
|
51
|
-
|
|
208
|
+
/**
|
|
209
|
+
* Resolves a citation's file directly under the root.
|
|
210
|
+
*
|
|
211
|
+
* @param root - The directory a citation's path is relative to.
|
|
212
|
+
* @param file - The cited path, a backslash read as a slash.
|
|
213
|
+
* @returns The absolute path of the file, or undefined when no regular file sits there.
|
|
214
|
+
*/
|
|
215
|
+
export function resolveDirect(root: string, file: string): string | undefined {
|
|
52
216
|
const direct = resolve(root, normalizeSlashes(file))
|
|
53
217
|
return existsSync(direct) && statSync(direct).isFile() ? direct : undefined
|
|
54
218
|
}
|
|
55
219
|
|
|
56
|
-
|
|
57
|
-
|
|
220
|
+
/**
|
|
221
|
+
* Checks whether a citation can resolve only through the basename index.
|
|
222
|
+
*
|
|
223
|
+
* @param root - The directory a citation's path is relative to.
|
|
224
|
+
* @param file - The cited path.
|
|
225
|
+
* @returns True if no root-relative file matches and the path is a bare filename; false otherwise.
|
|
226
|
+
*/
|
|
227
|
+
export function needsIndex(root: string, file: string): boolean {
|
|
58
228
|
return resolveDirect(root, file) === undefined && !/[/\\]/u.test(file)
|
|
59
229
|
}
|
|
60
230
|
|
|
61
|
-
|
|
231
|
+
/**
|
|
232
|
+
* Resolves a citation's file, directly or through the basename index.
|
|
233
|
+
*
|
|
234
|
+
* @param root - The directory a citation's path is relative to.
|
|
235
|
+
* @param file - The cited path.
|
|
236
|
+
* @param index - The basename index, or undefined when no citation needed one.
|
|
237
|
+
* @returns The resolved absolute path, or the reason the file did not resolve.
|
|
238
|
+
*/
|
|
239
|
+
export function resolveFile(
|
|
62
240
|
root: string,
|
|
63
241
|
file: string,
|
|
64
242
|
index: ReadonlyMap<string, readonly string[]> | undefined,
|
|
@@ -73,7 +251,16 @@ function resolveFile(
|
|
|
73
251
|
return { path: undefined, reason: `ambiguous basename: ${candidates.join(', ')}` }
|
|
74
252
|
}
|
|
75
253
|
|
|
76
|
-
|
|
254
|
+
/**
|
|
255
|
+
* Judges whether a cited line or range lies within a file.
|
|
256
|
+
*
|
|
257
|
+
* @param path - The resolved file.
|
|
258
|
+
* @param line - The cited line.
|
|
259
|
+
* @param end - The end of a cited range, or undefined for a single line.
|
|
260
|
+
* @param counts - The line counts already read, by file.
|
|
261
|
+
* @returns The reason the line or range is outside the file, or undefined when it is inside.
|
|
262
|
+
*/
|
|
263
|
+
export function judgeLines(
|
|
77
264
|
path: string,
|
|
78
265
|
line: number,
|
|
79
266
|
end: number | undefined,
|
|
@@ -86,8 +273,19 @@ function judgeLines(
|
|
|
86
273
|
return undefined
|
|
87
274
|
}
|
|
88
275
|
|
|
89
|
-
|
|
90
|
-
|
|
276
|
+
/**
|
|
277
|
+
* Runs the script on its arguments.
|
|
278
|
+
*
|
|
279
|
+
* @param argv - The arguments after the script path.
|
|
280
|
+
* @returns The exit code: 0 when every citation resolves, 3 when one does not, 64 on usage.
|
|
281
|
+
*/
|
|
282
|
+
export function main(argv: readonly string[]): number {
|
|
283
|
+
const unknown = readUnknownFlags(argv, FLAGS)
|
|
284
|
+
if (unknown.length > 0) {
|
|
285
|
+
console.error(`cite: ${unknown.join(', ')} is not a flag this script reads; ${USAGE}`)
|
|
286
|
+
return 64
|
|
287
|
+
}
|
|
288
|
+
const missing = readMissingFlags(argv, VALUE_FLAGS)
|
|
91
289
|
if (missing.length > 0) {
|
|
92
290
|
console.error(`cite: ${missing.join(', ')} given with no value`)
|
|
93
291
|
return 64
|
|
@@ -97,21 +295,10 @@ function main(argv: readonly string[]): number {
|
|
|
97
295
|
(argument, position) => !argument.startsWith('--') && argv[position - 1] !== '--root',
|
|
98
296
|
)
|
|
99
297
|
if (path === undefined || !existsSync(path)) {
|
|
100
|
-
console.error(
|
|
298
|
+
console.error(USAGE)
|
|
101
299
|
return 64
|
|
102
300
|
}
|
|
103
|
-
const named =
|
|
104
|
-
match[1] === undefined
|
|
105
|
-
? []
|
|
106
|
-
: [
|
|
107
|
-
{
|
|
108
|
-
citation: match[0],
|
|
109
|
-
file: match[1],
|
|
110
|
-
line: Number(match[2]),
|
|
111
|
-
end: match[3] === undefined ? undefined : Number(match[3]),
|
|
112
|
-
},
|
|
113
|
-
],
|
|
114
|
-
)
|
|
301
|
+
const named = extractCitations(readFileSync(path, 'utf8'))
|
|
115
302
|
const citations = named.length
|
|
116
303
|
// The basename index walks the root, so it is built once and only when a citation needs it.
|
|
117
304
|
const index = named.some(({ file }) => needsIndex(root, file)) ? indexBasenames(root) : undefined
|
|
@@ -142,4 +329,6 @@ function main(argv: readonly string[]): number {
|
|
|
142
329
|
return unresolved.length === 0 ? 0 : 3
|
|
143
330
|
}
|
|
144
331
|
|
|
145
|
-
process.
|
|
332
|
+
if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename) {
|
|
333
|
+
process.exitCode = main(process.argv.slice(2))
|
|
334
|
+
}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
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
3
|
// The command's stdout goes to --journal and its stderr to --errors; stdin is closed. Arguments after
|
|
4
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
|
|
5
|
+
// expires the process tree is killed and the errors file records `capped=true`; a command that had
|
|
6
|
+
// already exited when the cap expired is not capped. A fractional cap
|
|
6
7
|
// truncates to whole milliseconds, with 1 ms the least. --status records
|
|
7
8
|
// `git status --porcelain` before and after beside the journal, which is how a read-only lane's
|
|
8
9
|
// containment is checked. Stdout carries up to two JSON lines. The spawn line
|
|
@@ -11,31 +12,133 @@
|
|
|
11
12
|
// removed before the spawn, so neither appears when the command cannot start. The summary line
|
|
12
13
|
// prints last, after the command exits, and the errors file ends with `exit=<code>`. At the cap,
|
|
13
14
|
// 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
|
|
15
|
+
// The launcher's own flags are read before `--` alone, and it refuses a flag it does not read there,
|
|
16
|
+
// the `--flag=value` form included. Exit: the command's code; 124 when capped;
|
|
15
17
|
// 127 when the command could not start (the errors file names why); 64 on usage, including a
|
|
16
18
|
// journal and errors path that are the same file and a cap that is not a number of seconds from 0
|
|
17
19
|
// to 2147483.
|
|
20
|
+
import type { ChildProcess } from 'node:child_process'
|
|
18
21
|
import { spawn, spawnSync } from 'node:child_process'
|
|
19
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
appendFileSync,
|
|
24
|
+
closeSync,
|
|
25
|
+
mkdirSync,
|
|
26
|
+
openSync,
|
|
27
|
+
realpathSync,
|
|
28
|
+
rmSync,
|
|
29
|
+
writeFileSync,
|
|
30
|
+
} from 'node:fs'
|
|
20
31
|
import { dirname } from 'node:path'
|
|
21
|
-
import { readMissingFlags, readOption } from './helpers.ts'
|
|
22
32
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Holds the longest cap in whole seconds.
|
|
35
|
+
*
|
|
36
|
+
* @remarks
|
|
37
|
+
* The longest wait a Node timer holds is 2147483647 ms; floored to whole seconds, a longer cap would
|
|
38
|
+
* throw from `AbortSignal.timeout` after the spawn or clamp to 1 ms and kill the command at once.
|
|
39
|
+
*/
|
|
40
|
+
export const MAX_CAP_SECONDS = 2_147_483
|
|
26
41
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
42
|
+
/** Lists the flags that take a value. */
|
|
43
|
+
export const VALUE_FLAGS: readonly string[] = ['--journal', '--errors', '--cap']
|
|
44
|
+
|
|
45
|
+
/** Lists every flag the launcher reads before the separator, value flags and switches alike. */
|
|
46
|
+
export const FLAGS: readonly string[] = [...VALUE_FLAGS, '--status']
|
|
47
|
+
|
|
48
|
+
/** Describes how the command ended: its code, its signal, or the reason it could not start. */
|
|
49
|
+
export interface Exit {
|
|
50
|
+
readonly code: number | null
|
|
51
|
+
readonly signal: NodeJS.Signals | null
|
|
52
|
+
readonly error: string | undefined
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Reads the value that follows a flag.
|
|
57
|
+
*
|
|
58
|
+
* @remarks
|
|
59
|
+
* The first occurrence of a flag wins, a following value that opens with `--` is a missing value,
|
|
60
|
+
* and the `--flag=value` form is not read.
|
|
61
|
+
*
|
|
62
|
+
* @param argv - The arguments after the script path.
|
|
63
|
+
* @param name - The flag, such as `--journal`.
|
|
64
|
+
* @returns The value after the flag, or undefined when the flag is absent, last, or followed by
|
|
65
|
+
* another flag.
|
|
66
|
+
*/
|
|
67
|
+
export function readOption(argv: readonly string[], name: string): string | undefined {
|
|
68
|
+
const index = argv.indexOf(name)
|
|
69
|
+
const value = index === -1 ? undefined : argv[index + 1]
|
|
70
|
+
return value === undefined || value.startsWith('--') ? undefined : value
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Names the value flags a caller gave with no value.
|
|
75
|
+
*
|
|
76
|
+
* @param argv - The arguments after the script path.
|
|
77
|
+
* @param names - The flags that take a value.
|
|
78
|
+
* @returns Each named flag that appears last or is followed by another flag, in argument order.
|
|
79
|
+
*/
|
|
80
|
+
export function readMissingFlags(
|
|
81
|
+
argv: readonly string[],
|
|
82
|
+
names: readonly string[],
|
|
83
|
+
): readonly string[] {
|
|
84
|
+
return names.filter((name) => argv.includes(name) && readOption(argv, name) === undefined)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Names the flags a caller gave that the script does not read.
|
|
89
|
+
*
|
|
90
|
+
* @remarks
|
|
91
|
+
* A `--flag=value` argument is unknown, because the script reads no such form. A bare `--` ends the
|
|
92
|
+
* scan, so the command after it keeps every argument.
|
|
93
|
+
*
|
|
94
|
+
* @param argv - The arguments after the script path.
|
|
95
|
+
* @param flags - Every flag the script reads, value flags and switches alike.
|
|
96
|
+
* @returns Each argument that opens with `--` and is not a read flag, in argument order.
|
|
97
|
+
*/
|
|
98
|
+
export function readUnknownFlags(
|
|
99
|
+
argv: readonly string[],
|
|
100
|
+
flags: readonly string[],
|
|
101
|
+
): readonly string[] {
|
|
102
|
+
const end = argv.indexOf('--')
|
|
103
|
+
return (end === -1 ? argv : argv.slice(0, end)).filter(
|
|
104
|
+
(argument) => argument.startsWith('--') && !flags.includes(argument),
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Computes the cap in whole milliseconds that the timer takes.
|
|
110
|
+
*
|
|
111
|
+
* @remarks
|
|
112
|
+
* `AbortSignal.timeout` throws on a fractional delay, which `setTimeout` truncates and raises to 1 ms
|
|
113
|
+
* from below, so the cap takes that form before the command starts.
|
|
114
|
+
*
|
|
115
|
+
* @param cap - The cap in seconds.
|
|
116
|
+
* @returns The whole milliseconds, at least 1.
|
|
117
|
+
*/
|
|
118
|
+
export function computeCapMilliseconds(cap: number): number {
|
|
30
119
|
return Math.max(1, Math.trunc(cap * 1000))
|
|
31
120
|
}
|
|
32
121
|
|
|
33
|
-
|
|
34
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Reads the working tree's `git status --porcelain` output.
|
|
124
|
+
*
|
|
125
|
+
* @param cwd - The directory to read the status in. Default: the current directory.
|
|
126
|
+
* @returns The porcelain text, or a `git status failed:` line carrying git's own message.
|
|
127
|
+
*/
|
|
128
|
+
export function readGitStatus(cwd?: string): string {
|
|
129
|
+
const result = spawnSync('git', ['status', '--porcelain'], {
|
|
130
|
+
encoding: 'utf8',
|
|
131
|
+
...(cwd === undefined ? {} : { cwd }),
|
|
132
|
+
})
|
|
35
133
|
return result.status === 0 ? result.stdout : `git status failed: ${result.stderr}`
|
|
36
134
|
}
|
|
37
135
|
|
|
38
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Kills a process and every process it started, at once and without a chance to clean up.
|
|
138
|
+
*
|
|
139
|
+
* @param pid - The process to kill. A process that is already gone is not an error.
|
|
140
|
+
*/
|
|
141
|
+
export function forceKillTree(pid: number): void {
|
|
39
142
|
if (process.platform === 'win32') {
|
|
40
143
|
spawnSync('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore' })
|
|
41
144
|
return
|
|
@@ -45,18 +148,39 @@ function killTree(pid: number): void {
|
|
|
45
148
|
} catch {}
|
|
46
149
|
}
|
|
47
150
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Ends a command that is still running and aborts the controller to say the cap ended it.
|
|
153
|
+
*
|
|
154
|
+
* @remarks
|
|
155
|
+
* The cap's timer cannot be cleared, so it can elapse after the command exited on its own. The
|
|
156
|
+
* controller aborts only when the command is still running at that moment, which is how the run
|
|
157
|
+
* records that the cap, and not the command, ended it; a reused pid is never this command's.
|
|
158
|
+
*
|
|
159
|
+
* @param child - The running command.
|
|
160
|
+
* @param ended - The controller that aborts when the cap ended the command.
|
|
161
|
+
*/
|
|
162
|
+
export function endRunning(child: ChildProcess, ended: AbortController): void {
|
|
163
|
+
if (child.pid !== undefined && child.exitCode === null && child.signalCode === null) {
|
|
164
|
+
ended.abort()
|
|
165
|
+
forceKillTree(child.pid)
|
|
166
|
+
}
|
|
52
167
|
}
|
|
53
168
|
|
|
54
|
-
|
|
169
|
+
/**
|
|
170
|
+
* Starts the command with its output sent to the journal and errors files.
|
|
171
|
+
*
|
|
172
|
+
* @param command - The executable.
|
|
173
|
+
* @param args - The command's arguments, passed verbatim.
|
|
174
|
+
* @param journal - The open journal file descriptor that receives stdout.
|
|
175
|
+
* @param errors - The open errors file descriptor that receives stderr.
|
|
176
|
+
* @returns The child process, or the failure when the spawn throws.
|
|
177
|
+
*/
|
|
178
|
+
export function startCommand(
|
|
55
179
|
command: string,
|
|
56
180
|
args: readonly string[],
|
|
57
181
|
journal: number,
|
|
58
182
|
errors: number,
|
|
59
|
-
):
|
|
183
|
+
): ChildProcess | Exit {
|
|
60
184
|
try {
|
|
61
185
|
return spawn(command, args, { stdio: ['ignore', journal, errors], windowsHide: true })
|
|
62
186
|
} catch (error) {
|
|
@@ -68,17 +192,36 @@ function startCommand(
|
|
|
68
192
|
}
|
|
69
193
|
}
|
|
70
194
|
|
|
71
|
-
|
|
195
|
+
/**
|
|
196
|
+
* Waits for a started command to close.
|
|
197
|
+
*
|
|
198
|
+
* @param child - The started command.
|
|
199
|
+
* @returns How it ended: its code and signal, or the error that stopped it.
|
|
200
|
+
*/
|
|
201
|
+
export function waitForExit(child: ChildProcess): Promise<Exit> {
|
|
72
202
|
return new Promise((resolve) => {
|
|
73
203
|
child.on('error', (error) => resolve({ code: null, signal: null, error: error.message }))
|
|
74
204
|
child.on('close', (code, signal) => resolve({ code, signal, error: undefined }))
|
|
75
205
|
})
|
|
76
206
|
}
|
|
77
207
|
|
|
78
|
-
|
|
208
|
+
/**
|
|
209
|
+
* Runs the launcher on its arguments.
|
|
210
|
+
*
|
|
211
|
+
* @param argv - The arguments after the script path.
|
|
212
|
+
* @returns The exit code: the command's, 124 when capped, 127 when it could not start, 64 on usage.
|
|
213
|
+
*/
|
|
214
|
+
export async function main(argv: readonly string[]): Promise<number> {
|
|
79
215
|
const separator = argv.indexOf('--')
|
|
80
216
|
// The launcher's own flags end at the separator; everything after it belongs to the command.
|
|
81
217
|
const own = separator === -1 ? argv : argv.slice(0, separator)
|
|
218
|
+
const unknown = readUnknownFlags(argv, FLAGS)
|
|
219
|
+
if (unknown.length > 0) {
|
|
220
|
+
console.error(
|
|
221
|
+
`launch: ${unknown.join(', ')} is not a flag this launcher reads before --; flags: ${FLAGS.join(' ')}`,
|
|
222
|
+
)
|
|
223
|
+
return 64
|
|
224
|
+
}
|
|
82
225
|
const journal = readOption(own, '--journal')
|
|
83
226
|
const errors = readOption(own, '--errors')
|
|
84
227
|
const cap =
|
|
@@ -106,7 +249,7 @@ async function main(argv: readonly string[]): Promise<number> {
|
|
|
106
249
|
const capMilliseconds = cap > 0 ? computeCapMilliseconds(cap) : undefined
|
|
107
250
|
mkdirSync(dirname(journal), { recursive: true })
|
|
108
251
|
mkdirSync(dirname(errors), { recursive: true })
|
|
109
|
-
if (status) writeFileSync(`${journal}.status-before.txt`,
|
|
252
|
+
if (status) writeFileSync(`${journal}.status-before.txt`, readGitStatus())
|
|
110
253
|
const journalFd = openSync(journal, 'w')
|
|
111
254
|
const errorsFd = openSync(errors, 'w')
|
|
112
255
|
rmSync(`${journal}.pid`, { force: true })
|
|
@@ -117,9 +260,9 @@ async function main(argv: readonly string[]): Promise<number> {
|
|
|
117
260
|
writeFileSync(`${journal}.pid`, `${child.pid}\n`)
|
|
118
261
|
console.log(JSON.stringify({ pid: child.pid, journal, errors }))
|
|
119
262
|
}
|
|
120
|
-
// The
|
|
121
|
-
//
|
|
122
|
-
|
|
263
|
+
// The ended controller aborts only when the cap found the command still running, so whether it
|
|
264
|
+
// aborted is whether the cap ended the run.
|
|
265
|
+
const ended = new AbortController()
|
|
123
266
|
const deadline =
|
|
124
267
|
child === undefined || capMilliseconds === undefined
|
|
125
268
|
? undefined
|
|
@@ -127,14 +270,12 @@ async function main(argv: readonly string[]): Promise<number> {
|
|
|
127
270
|
deadline?.addEventListener(
|
|
128
271
|
'abort',
|
|
129
272
|
() => {
|
|
130
|
-
if (child
|
|
131
|
-
killTree(child.pid)
|
|
132
|
-
}
|
|
273
|
+
if (child !== undefined) endRunning(child, ended)
|
|
133
274
|
},
|
|
134
275
|
{ once: true },
|
|
135
276
|
)
|
|
136
277
|
const exit = 'error' in launched ? launched : await waitForExit(launched)
|
|
137
|
-
const capped =
|
|
278
|
+
const capped = ended.signal.aborted
|
|
138
279
|
closeSync(journalFd)
|
|
139
280
|
closeSync(errorsFd)
|
|
140
281
|
const durationMs = Date.now() - started
|
|
@@ -142,7 +283,7 @@ async function main(argv: readonly string[]): Promise<number> {
|
|
|
142
283
|
errors,
|
|
143
284
|
`${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`,
|
|
144
285
|
)
|
|
145
|
-
if (status) writeFileSync(`${journal}.status-after.txt`,
|
|
286
|
+
if (status) writeFileSync(`${journal}.status-after.txt`, readGitStatus())
|
|
146
287
|
console.log(
|
|
147
288
|
JSON.stringify({
|
|
148
289
|
pid: child?.pid,
|
|
@@ -159,6 +300,8 @@ async function main(argv: readonly string[]): Promise<number> {
|
|
|
159
300
|
return capped ? 124 : (exit.code ?? 1)
|
|
160
301
|
}
|
|
161
302
|
|
|
162
|
-
|
|
163
|
-
process.
|
|
164
|
-
|
|
303
|
+
if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename) {
|
|
304
|
+
main(process.argv.slice(2)).then((code) => {
|
|
305
|
+
process.exitCode = code
|
|
306
|
+
})
|
|
307
|
+
}
|