@orkestrel/scaffold 0.0.108 → 0.0.109

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/dist/agents/skills/orkestrel-dispatch/scripts/bench.js +326 -91
  2. package/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +110 -14
  3. package/dist/agents/skills/orkestrel-dispatch/scripts/chain.js +280 -0
  4. package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +188 -26
  5. package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +142 -43
  6. package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +240 -62
  7. package/dist/agents/skills/orkestrel-dispatch/scripts/patch.js +323 -0
  8. package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +169 -32
  9. package/dist/agents/skills/orkestrel-dispatch/scripts/sweep.js +290 -69
  10. package/dist/agents/skills/orkestrel-falsify/scripts/mutate.js +560 -0
  11. package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +1052 -205
  12. package/dist/agents/skills/orkestrel-harden/scripts/measure.js +361 -0
  13. package/dist/agents/skills/orkestrel-harden/scripts/titles.js +455 -80
  14. package/dist/agents/skills/orkestrel-parallel/scripts/cases.js +327 -0
  15. package/dist/agents/skills/orkestrel-publish/scripts/compare.js +298 -51
  16. package/dist/agents/skills/orkestrel-publish/scripts/pins.js +170 -13
  17. package/dist/agents/skills/orkestrel-publish/scripts/swap.js +956 -191
  18. package/dist/agents/skills/orkestrel-publish/scripts/wave.js +707 -112
  19. package/dist/agents/skills/orkestrel-publish/scripts/window.js +235 -41
  20. package/dist/agents/skills/orkestrel-scout/scripts/map.js +341 -54
  21. package/dist/agents/templates/brief.md +1 -1
  22. package/dist/bin/main.js +51 -46
  23. package/dist/bin/main.js.map +1 -1
  24. package/dist/host/AGENTS.md +23 -21
  25. package/dist/host/agents/orchestration.md +43 -18
  26. package/dist/host/agents/platforms/windows.md +3 -3
  27. package/dist/host/agents/skills/orkestrel-align/SKILL.md +10 -13
  28. package/dist/host/agents/skills/orkestrel-align/references/fleet.md +4 -10
  29. package/dist/host/agents/skills/orkestrel-align/references/integration.md +1 -1
  30. package/dist/host/agents/skills/orkestrel-build/SKILL.md +16 -48
  31. package/dist/host/agents/skills/orkestrel-build/agents/openai.yaml +1 -1
  32. package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +19 -16
  33. package/dist/host/agents/skills/orkestrel-debrief/agents/openai.yaml +1 -1
  34. package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +15 -28
  35. package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +5 -6
  36. package/dist/host/agents/skills/orkestrel-debrief/references/retention.md +4 -6
  37. package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +31 -20
  38. package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +3 -4
  39. package/dist/host/agents/skills/orkestrel-dispatch/scripts/bench.ts +366 -75
  40. package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +126 -16
  41. package/dist/host/agents/skills/orkestrel-dispatch/scripts/chain.ts +323 -0
  42. package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +220 -31
  43. package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +176 -33
  44. package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +294 -59
  45. package/dist/host/agents/skills/orkestrel-dispatch/scripts/patch.ts +401 -0
  46. package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +199 -34
  47. package/dist/host/agents/skills/orkestrel-dispatch/scripts/sweep.ts +330 -69
  48. package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +10 -8
  49. package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +1 -5
  50. package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +1 -1
  51. package/dist/host/agents/skills/orkestrel-falsify/scripts/mutate.ts +701 -0
  52. package/dist/host/agents/skills/orkestrel-harden/SKILL.md +16 -12
  53. package/dist/host/agents/skills/orkestrel-harden/references/bindings.md +23 -81
  54. package/dist/host/agents/skills/orkestrel-harden/references/centralization.md +4 -5
  55. package/dist/host/agents/skills/orkestrel-harden/references/contract.md +2 -0
  56. package/dist/host/agents/skills/orkestrel-harden/references/document.md +33 -0
  57. package/dist/host/agents/skills/orkestrel-harden/references/equivalence.md +6 -3
  58. package/dist/host/agents/skills/orkestrel-harden/references/hardening.md +8 -4
  59. package/dist/host/agents/skills/orkestrel-harden/references/research.md +2 -0
  60. package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +1272 -279
  61. package/dist/host/agents/skills/orkestrel-harden/scripts/measure.ts +425 -0
  62. package/dist/host/agents/skills/orkestrel-harden/scripts/titles.ts +603 -98
  63. package/dist/host/agents/skills/orkestrel-journey/SKILL.md +75 -98
  64. package/dist/host/agents/skills/orkestrel-journey/references/captures.md +54 -17
  65. package/dist/host/agents/skills/orkestrel-journey/references/decide.md +1 -20
  66. package/dist/host/agents/skills/orkestrel-journey/references/layer.md +21 -31
  67. package/dist/host/agents/skills/orkestrel-journey/references/styles.md +1 -1
  68. package/dist/host/agents/skills/orkestrel-parallel/SKILL.md +28 -24
  69. package/dist/host/agents/skills/orkestrel-parallel/references/claude.md +15 -13
  70. package/dist/host/agents/skills/orkestrel-parallel/references/codex.md +1 -0
  71. package/dist/host/agents/skills/orkestrel-parallel/references/cursor.md +13 -13
  72. package/dist/host/agents/skills/orkestrel-parallel/scripts/cases.ts +389 -0
  73. package/dist/host/agents/skills/orkestrel-polish/SKILL.md +16 -20
  74. package/dist/host/agents/skills/orkestrel-polish/references/capture-harness.md +16 -26
  75. package/dist/host/agents/skills/orkestrel-publish/SKILL.md +12 -26
  76. package/dist/host/agents/skills/orkestrel-publish/agents/openai.yaml +2 -2
  77. package/dist/host/agents/skills/orkestrel-publish/references/adopt.md +23 -0
  78. package/dist/host/agents/skills/orkestrel-publish/references/release.md +4 -4
  79. package/dist/host/agents/skills/orkestrel-publish/references/wave.md +10 -9
  80. package/dist/host/agents/skills/orkestrel-publish/references/window.md +20 -25
  81. package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +348 -59
  82. package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +202 -22
  83. package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1061 -191
  84. package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +812 -140
  85. package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +280 -38
  86. package/dist/host/agents/skills/orkestrel-scout/SKILL.md +3 -3
  87. package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +428 -70
  88. package/dist/host/agents/templates/brief.md +1 -1
  89. package/dist/host/agents/transports/claude-cli.md +3 -3
  90. package/dist/host/agents/transports/codex.md +2 -7
  91. package/dist/host/agents/transports/cursor.md +2 -3
  92. package/dist/host/claude/AGENTS.md +2 -2
  93. package/dist/host/claude/agents/analyst.md +3 -3
  94. package/dist/host/claude/agents/astra.md +3 -3
  95. package/dist/host/claude/agents/builder.md +1 -1
  96. package/dist/host/claude/agents/checker.md +1 -1
  97. package/dist/host/claude/agents/distiller.md +1 -1
  98. package/dist/host/claude/agents/grok.md +1 -1
  99. package/dist/host/claude/agents/opus.md +2 -2
  100. package/dist/host/claude/agents/orkestrel.md +57 -90
  101. package/dist/host/claude/agents/researcher.md +1 -1
  102. package/dist/host/claude/agents/reviewer.md +4 -0
  103. package/dist/host/claude/agents/scout.md +1 -1
  104. package/dist/host/claude/agents/verifier.md +1 -1
  105. package/dist/host/claude/rules/application.md +14 -21
  106. package/dist/host/claude/rules/architecture.md +34 -44
  107. package/dist/host/claude/rules/browser.md +3 -4
  108. package/dist/host/claude/rules/campaign.md +2 -2
  109. package/dist/host/claude/rules/documentation.md +25 -12
  110. package/dist/host/claude/rules/names.md +9 -4
  111. package/dist/host/claude/rules/patterns.md +6 -7
  112. package/dist/host/claude/rules/portability.md +1 -1
  113. package/dist/host/claude/rules/quality.md +4 -5
  114. package/dist/host/claude/rules/styles.md +2 -2
  115. package/dist/host/claude/rules/tests.md +57 -114
  116. package/dist/host/claude/rules/typescript.md +7 -9
  117. package/dist/host/claude/rules/workspace.md +21 -19
  118. package/dist/host/claude/rules/writing.md +2 -5
  119. package/dist/host/claude/skills/orkestrel-dispatch/SKILL.md +1 -1
  120. package/dist/host/claude/skills/orkestrel-harden/SKILL.md +1 -1
  121. package/dist/host/claude/skills/orkestrel-publish/SKILL.md +1 -1
  122. package/dist/host/claude/skills/orkestrel-scout/SKILL.md +1 -1
  123. package/dist/host/codex/agents/analyst.toml +9 -3
  124. package/dist/host/codex/agents/astra.toml +3 -4
  125. package/dist/host/codex/agents/builder.toml +1 -1
  126. package/dist/host/codex/agents/opus.toml +3 -4
  127. package/dist/host/codex/agents/orkestrel.toml +1 -1
  128. package/dist/host/codex/agents/researcher.toml +2 -2
  129. package/dist/host/codex/agents/verifier.toml +1 -1
  130. package/dist/host/configs/helpers.ts +21 -1
  131. package/dist/host/configs/policy.ts +299 -15
  132. package/dist/host/dotfiles/oxlintrc.json +137 -3
  133. package/dist/host/guides/agent.md +111 -134
  134. package/dist/host/guides/capture.md +801 -0
  135. package/dist/host/guides/probe.md +27 -8
  136. package/dist/host/guides/scaffold.md +96 -18
  137. package/dist/host/guides/study.md +534 -440
  138. package/dist/host/guides/test.md +111 -44
  139. package/dist/host/manifest.json +160 -240
  140. package/dist/host/tests/config.test.ts +971 -187
  141. package/dist/host/tests/policy.test.ts +127 -1
  142. package/dist/host/tests/setupPolicy.ts +893 -33
  143. package/dist/src/core/index.cjs +214 -86
  144. package/dist/src/core/index.cjs.map +1 -1
  145. package/dist/src/core/index.d.cts +122 -38
  146. package/dist/src/core/index.d.ts +122 -38
  147. package/dist/src/core/index.js +209 -85
  148. package/dist/src/core/index.js.map +1 -1
  149. package/dist/src/server/index.cjs +20 -27
  150. package/dist/src/server/index.cjs.map +1 -1
  151. package/dist/src/server/index.js +21 -28
  152. package/dist/src/server/index.js.map +1 -1
  153. package/package.json +11 -11
  154. package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +0 -283
  155. package/dist/host/agents/skills/orkestrel-dispatch/references/bench.md +0 -25
  156. package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +0 -333
  157. package/dist/host/guides/supervisor.md +0 -4890
@@ -14,12 +14,13 @@
14
14
  // --delete removes them, refusing while one changed in the last 2 minutes.
15
15
  // --tmp and --unit --delete never delete a file git tracks: each prints `kept tracked <path>` and the
16
16
  // refusal ignores a tracked file; --unit alone marks a tracked file in its listing.
17
- // Exit 0 on success, 2 when a deletion is refused, 64 on usage.
17
+ // Name one mode per run; the script refuses a flag it does not read, the `--flag=value` form
18
+ // included. Exit 0 on success, 2 when a deletion is refused, 64 on usage.
18
19
  import { spawnSync } from 'node:child_process';
19
- import { existsSync, readFileSync, readdirSync, rmSync } from 'node:fs';
20
+ import { existsSync, readdirSync, readFileSync, realpathSync, rmSync, statSync } from 'node:fs';
20
21
  import { basename, join } from 'node:path';
21
- import { listFiles, readMissingFlags, readOption } from "./helpers.js";
22
- const LAUNCH_DIRECTORIES = [
22
+ /** Lists the directories under `tmp/` that hold launch files. */
23
+ export const LAUNCH_DIRECTORIES = [
23
24
  'tmp/units',
24
25
  'tmp/cursor',
25
26
  'tmp/codex',
@@ -28,20 +29,136 @@ const LAUNCH_DIRECTORIES = [
28
29
  'tmp/type',
29
30
  'tmp/captures',
30
31
  ];
31
- const RECENT_MS = 2 * 60_000;
32
- // A launch directory holds nothing a walk skips, so every file beneath it counts.
33
- const NOTHING_SKIPPED = new Set();
34
- // Git's output is bounded by the repository's file count, so a generous buffer is the only limit.
35
- const GIT_BUFFER = 64 * 1024 * 1024;
36
- // An absolute machine path: a drive-letter path, or one that opens `/Users/`, `/home/`, or `~/`. The
37
- // lookbehind keeps `https://`, `src/home/`, and `./home/` from matching.
38
- const MACHINE_PATH = /(?<![\w.-])(?:[A-Za-z]:[\\/]|\/(?:Users|home)\/|~\/)/u;
39
- const BACKTICKED = /`([^`\n]+)`/gu;
40
- const PLACEHOLDER = /[*?<{]/u;
41
- function formatDay(modified) {
32
+ /** Holds the milliseconds within which a changed launch file counts as still in use. */
33
+ export const RECENT_MS = 2 * 60_000;
34
+ /** Holds the minutes a launch file must age before `--tmp` deletes it, when the call gives no threshold. */
35
+ export const DEFAULT_THRESHOLD = 60;
36
+ /** Names the directories a tree walk skips. */
37
+ export const SKIPPED_DIRECTORIES = new Set([
38
+ 'node_modules',
39
+ '.git',
40
+ 'dist',
41
+ 'tmp',
42
+ '.orkestrel',
43
+ '.idea',
44
+ ]);
45
+ /** Names no directory: a launch directory holds nothing a walk skips, so every file beneath it counts. */
46
+ export const NOTHING_SKIPPED = new Set();
47
+ /** Holds the buffer size for git's output, which the repository's file count bounds. */
48
+ export const GIT_BUFFER = 64 * 1024 * 1024;
49
+ /**
50
+ * Matches an absolute machine path: a drive-letter path, or one that opens `/Users/`, `/home/`, or
51
+ * `~/`. The lookbehind keeps `https://`, `src/home/`, and `./home/` from matching.
52
+ */
53
+ export const MACHINE_PATH = /(?<![\w.-])(?:[A-Za-z]:[\\/]|\/(?:Users|home)\/|~\/)/u;
54
+ /** Matches one backticked token in a line. */
55
+ export const BACKTICKED = /`([^`\n]+)`/gu;
56
+ /** Matches the first glob or placeholder character in a path. */
57
+ export const PLACEHOLDER = /[*?<{]/u;
58
+ /** Lists the flags that take a value. */
59
+ export const VALUE_FLAGS = ['--unit', '--older-than'];
60
+ /** Lists every flag the script reads, value flags and switches alike. */
61
+ export const FLAGS = [...VALUE_FLAGS, '--report', '--tmp', '--delete'];
62
+ /** Holds the usage line printed when a call names an unknown mode. */
63
+ export const USAGE = 'usage: sweep.ts --report | --tmp [--older-than MINUTES] | --unit UNIT [--delete]';
64
+ /**
65
+ * Reads the value that follows a flag.
66
+ *
67
+ * @remarks
68
+ * The first occurrence of a flag wins, a following value that opens with `--` is a missing value,
69
+ * and the `--flag=value` form is not read.
70
+ *
71
+ * @param argv - The arguments after the script path.
72
+ * @param name - The flag, such as `--unit`.
73
+ * @returns The value after the flag, or undefined when the flag is absent, last, or followed by
74
+ * another flag.
75
+ */
76
+ export function readOption(argv, name) {
77
+ const index = argv.indexOf(name);
78
+ const value = index === -1 ? undefined : argv[index + 1];
79
+ return value === undefined || value.startsWith('--') ? undefined : value;
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(argv, names) {
89
+ return names.filter((name) => argv.includes(name) && readOption(argv, name) === undefined);
90
+ }
91
+ /**
92
+ * Names the flags a caller gave that the script does not read.
93
+ *
94
+ * @remarks
95
+ * A `--flag=value` argument is unknown, because the script reads no such form. A bare `--` ends the
96
+ * scan.
97
+ *
98
+ * @param argv - The arguments after the script path.
99
+ * @param flags - Every flag the script reads, value flags and switches alike.
100
+ * @returns Each argument that opens with `--` and is not a read flag, in argument order.
101
+ */
102
+ export function readUnknownFlags(argv, flags) {
103
+ const end = argv.indexOf('--');
104
+ return (end === -1 ? argv : argv.slice(0, end)).filter((argument) => argument.startsWith('--') && !flags.includes(argument));
105
+ }
106
+ /**
107
+ * Normalizes a host path to forward slashes.
108
+ *
109
+ * @param path - A host path.
110
+ * @returns The same path with every backslash replaced by a slash.
111
+ */
112
+ export function normalizeSlashes(path) {
113
+ return path.split('\\').join('/');
114
+ }
115
+ /**
116
+ * Lists every regular file beneath a directory, skipping the directories the script never reads.
117
+ *
118
+ * @remarks
119
+ * A skipped name matches a directory at any depth, exactly and case-sensitively.
120
+ *
121
+ * @param directory - The directory to walk.
122
+ * @param skipped - The directory names to skip. Default: the skipped directories.
123
+ * @returns Each file's forward-slash path and modification time, in walk order.
124
+ * @throws Thrown when `directory` does not exist or cannot be read.
125
+ */
126
+ export function listFiles(directory, skipped = SKIPPED_DIRECTORIES) {
127
+ const files = [];
128
+ const pending = [directory];
129
+ while (pending.length > 0) {
130
+ const current = pending.pop();
131
+ if (current === undefined)
132
+ break;
133
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
134
+ if (skipped.has(entry.name))
135
+ continue;
136
+ const path = join(current, entry.name);
137
+ if (entry.isDirectory())
138
+ pending.push(path);
139
+ else if (entry.isFile()) {
140
+ files.push({ path: normalizeSlashes(path), modified: statSync(path).mtimeMs });
141
+ }
142
+ }
143
+ }
144
+ return files;
145
+ }
146
+ /**
147
+ * Formats a modification time as its UTC day.
148
+ *
149
+ * @param modified - A time in milliseconds since the epoch.
150
+ * @returns The day as `YYYY-MM-DD`.
151
+ */
152
+ export function formatDay(modified) {
42
153
  return new Date(modified).toISOString().slice(0, 10);
43
154
  }
44
- function reportDirectory(directory) {
155
+ /**
156
+ * Reports how many files a launch directory holds and the age of the oldest.
157
+ *
158
+ * @param directory - The directory to report on.
159
+ * @returns The report line, or undefined when the directory is absent or empty.
160
+ */
161
+ export function reportDirectory(directory) {
45
162
  if (!existsSync(directory))
46
163
  return undefined;
47
164
  const files = listFiles(directory, NOTHING_SKIPPED);
@@ -50,14 +167,24 @@ function reportDirectory(directory) {
50
167
  const oldest = files.reduce((low, file) => Math.min(low, file.modified), Number.POSITIVE_INFINITY);
51
168
  return `sweep: ${directory} holds ${files.length} file(s), oldest ${formatDay(oldest)}`;
52
169
  }
53
- function listCampaignDirectories() {
170
+ /**
171
+ * Lists the campaign directories under `.orkestrel/`.
172
+ *
173
+ * @returns Each directory's path, or nothing when `.orkestrel/` is absent.
174
+ */
175
+ export function listCampaignDirectories() {
54
176
  if (!existsSync('.orkestrel'))
55
177
  return [];
56
178
  return readdirSync('.orkestrel', { withFileTypes: true })
57
179
  .filter((entry) => entry.isDirectory())
58
180
  .map((entry) => join('.orkestrel', entry.name));
59
181
  }
60
- function reportCampaignFiles() {
182
+ /**
183
+ * Reports the ecosystem files that sit directly under `.orkestrel/`.
184
+ *
185
+ * @returns The report line, or undefined when there are none.
186
+ */
187
+ export function reportCampaignFiles() {
61
188
  if (!existsSync('.orkestrel'))
62
189
  return undefined;
63
190
  const files = readdirSync('.orkestrel', { withFileTypes: true }).filter((entry) => entry.isFile());
@@ -65,7 +192,12 @@ function reportCampaignFiles() {
65
192
  return undefined;
66
193
  return `sweep: .orkestrel holds ${files.length} ecosystem file(s): ${files.map((entry) => entry.name).join(', ')}`;
67
194
  }
68
- function removeEmptyDirectories(directory) {
195
+ /**
196
+ * Removes every empty directory beneath a directory, deepest first.
197
+ *
198
+ * @param directory - The directory whose empty descendants are removed; the directory itself stays.
199
+ */
200
+ export function removeEmptyDirectories(directory) {
69
201
  for (const entry of readdirSync(directory, { withFileTypes: true })) {
70
202
  if (!entry.isDirectory())
71
203
  continue;
@@ -75,24 +207,45 @@ function removeEmptyDirectories(directory) {
75
207
  rmSync(path, { recursive: true });
76
208
  }
77
209
  }
78
- function readThreshold(argv) {
210
+ /**
211
+ * Reads the `--older-than` threshold in minutes.
212
+ *
213
+ * @param argv - The arguments after the script path.
214
+ * @returns The default threshold when the flag is absent, the number when it is finite and above 0,
215
+ * and undefined otherwise.
216
+ */
217
+ export function readThreshold(argv) {
79
218
  if (!argv.includes('--older-than'))
80
- return 60;
219
+ return DEFAULT_THRESHOLD;
81
220
  const value = Number(readOption(argv, '--older-than'));
82
221
  return Number.isFinite(value) && value > 0 ? value : undefined;
83
222
  }
84
- // Lists the files git tracks beneath the scope (the whole repository when empty), as forward-slash
85
- // paths from the root. Any git failure reads as nothing tracked, so the run keeps its old behavior.
86
- function listTracked(scope) {
223
+ /**
224
+ * Lists the files git tracks beneath a scope, as forward-slash paths from the root.
225
+ *
226
+ * @remarks
227
+ * Any git failure reads as nothing tracked, so the run keeps its old behavior.
228
+ *
229
+ * @param scope - The paths to list beneath; the whole repository when empty.
230
+ * @returns The tracked paths.
231
+ */
232
+ export function listTracked(scope) {
87
233
  const args = ['ls-files', '-z', ...(scope.length === 0 ? [] : ['--', ...scope])];
88
234
  const run = spawnSync('git', args, { encoding: 'utf8', windowsHide: true, maxBuffer: GIT_BUFFER });
89
235
  if (run.status !== 0)
90
236
  return [];
91
237
  return run.stdout.split('\0').filter((path) => path !== '');
92
238
  }
93
- // Names the probes git ignores, from one batched `git check-ignore`: status 0 and 1 both answer,
94
- // and any other status reads as nothing ignored.
95
- function readIgnored(probes) {
239
+ /**
240
+ * Names the probes git ignores, from one batched `git check-ignore`.
241
+ *
242
+ * @remarks
243
+ * Status 0 and 1 both answer, and any other status reads as nothing ignored.
244
+ *
245
+ * @param probes - The paths to ask about.
246
+ * @returns The probes git ignores.
247
+ */
248
+ export function readIgnored(probes) {
96
249
  if (probes.length === 0)
97
250
  return new Set();
98
251
  const run = spawnSync('git', ['check-ignore', '--stdin', '-z'], {
@@ -105,11 +258,18 @@ function readIgnored(probes) {
105
258
  return new Set();
106
259
  return new Set(run.stdout.split('\0').filter((path) => path !== ''));
107
260
  }
108
- // Reads a backticked token as a repository-relative citation. A token counts when it has no
109
- // whitespace, contains `/`, and is no URL; the path stops at its first glob or placeholder character
110
- // and drops a trailing `/`. A machine path, a path outside the repository, and a backslash path are
111
- // no citation.
112
- function readCitation(token) {
261
+ /**
262
+ * Reads a backticked token as a repository-relative citation.
263
+ *
264
+ * @remarks
265
+ * A token counts when it has no whitespace, contains `/`, and is no URL; the path stops at its first
266
+ * glob or placeholder character and drops a trailing `/`. A machine path, a path outside the
267
+ * repository, and a backslash path are no citation.
268
+ *
269
+ * @param token - The text between two backticks.
270
+ * @returns The citation, or undefined when the token is none.
271
+ */
272
+ export function readCitation(token) {
113
273
  if (/\s/u.test(token) || !token.includes('/') || token.includes('://'))
114
274
  return undefined;
115
275
  const stop = token.search(PLACEHOLDER);
@@ -120,7 +280,13 @@ function readCitation(token) {
120
280
  }
121
281
  return { path, directory: cut.endsWith('/'), prefix: stop === -1 ? undefined : cut };
122
282
  }
123
- function listFindings(file) {
283
+ /**
284
+ * Lists the findings a campaign Markdown file carries: machine paths and backticked citations.
285
+ *
286
+ * @param file - The Markdown file to read.
287
+ * @returns Each finding with its file and line, a machine path before the citations of its line.
288
+ */
289
+ export function listFindings(file) {
124
290
  const lines = readFileSync(file, 'utf8').split(/\r\n|\n/u);
125
291
  return lines.flatMap((text, index) => {
126
292
  const line = index + 1;
@@ -132,16 +298,50 @@ function listFindings(file) {
132
298
  return [...machine, ...cited];
133
299
  });
134
300
  }
135
- function probeOf(citation) {
301
+ /**
302
+ * Builds the path git is asked about for a citation.
303
+ *
304
+ * @param citation - The citation.
305
+ * @returns The path, with a trailing `/` when the citation names a directory.
306
+ */
307
+ export function probeOf(citation) {
136
308
  return citation.directory ? `${citation.path}/` : citation.path;
137
309
  }
138
- // A citation is covered when git tracks the path, a file beneath it, or a file its glob prefix opens.
139
- function isCovered(citation, tracked) {
310
+ /**
311
+ * Checks whether git tracks a citation's path, a file beneath it, or a file its glob prefix opens.
312
+ *
313
+ * @param citation - The citation.
314
+ * @param tracked - The tracked paths.
315
+ * @returns True if a tracked path covers the citation; false otherwise.
316
+ */
317
+ export function isCovered(citation, tracked) {
140
318
  return tracked.some((path) => path === citation.path ||
141
319
  path.startsWith(`${citation.path}/`) ||
142
320
  (citation.prefix !== undefined && path.startsWith(citation.prefix)));
143
321
  }
144
- function reportCitations() {
322
+ /**
323
+ * Describes a finding as the message the report prints for it.
324
+ *
325
+ * @param finding - The finding.
326
+ * @param ignored - The probes git ignores.
327
+ * @param tracked - The tracked paths.
328
+ * @returns `names a machine path`, `cites ignored path <path>`, or undefined when git tracks the
329
+ * cited path or does not ignore it.
330
+ */
331
+ export function describeFinding(finding, ignored, tracked) {
332
+ const { citation } = finding;
333
+ if (citation === undefined)
334
+ return 'names a machine path';
335
+ return ignored.has(probeOf(citation)) && !isCovered(citation, tracked)
336
+ ? `cites ignored path ${citation.path}`
337
+ : undefined;
338
+ }
339
+ /**
340
+ * Reports the campaign Markdown lines that cite an ignored path or name a machine path.
341
+ *
342
+ * @returns One report line per finding, each file and line and message once.
343
+ */
344
+ export function reportCitations() {
145
345
  if (!existsSync('.orkestrel'))
146
346
  return [];
147
347
  const findings = listFiles('.orkestrel', NOTHING_SKIPPED)
@@ -149,32 +349,21 @@ function reportCitations() {
149
349
  .filter((path) => /\.md$/iu.test(path))
150
350
  .sort()
151
351
  .flatMap((path) => listFindings(path));
152
- const probes = new Set();
153
- for (const finding of findings) {
154
- if (finding.citation !== undefined)
155
- probes.add(probeOf(finding.citation));
156
- }
352
+ const probes = new Set(findings.flatMap(({ citation }) => (citation === undefined ? [] : [probeOf(citation)])));
157
353
  const ignored = readIgnored([...probes]);
158
354
  const tracked = ignored.size === 0 ? [] : listTracked([]);
159
- const seen = new Set();
160
- const lines = [];
161
- for (const { file, line, citation } of findings) {
162
- const text = citation === undefined
163
- ? 'names a machine path'
164
- : ignored.has(probeOf(citation)) && !isCovered(citation, tracked)
165
- ? `cites ignored path ${citation.path}`
166
- : undefined;
167
- if (text === undefined)
168
- continue;
169
- const key = `${file}:${line} ${text}`;
170
- if (seen.has(key))
171
- continue;
172
- seen.add(key);
173
- lines.push(`sweep: ${key}`);
174
- }
175
- return lines;
355
+ const lines = new Set(findings.flatMap((finding) => {
356
+ const text = describeFinding(finding, ignored, tracked);
357
+ return text === undefined ? [] : [`${finding.file}:${finding.line} ${text}`];
358
+ }));
359
+ return [...lines].map((key) => `sweep: ${key}`);
176
360
  }
177
- function runReport() {
361
+ /**
362
+ * Prints the leftover report.
363
+ *
364
+ * @returns The exit code, always 0.
365
+ */
366
+ export function runReport() {
178
367
  for (const directory of [...LAUNCH_DIRECTORIES, ...listCampaignDirectories()]) {
179
368
  const line = reportDirectory(directory);
180
369
  if (line !== undefined)
@@ -187,7 +376,13 @@ function runReport() {
187
376
  console.log(line);
188
377
  return 0;
189
378
  }
190
- function runSweep(thresholdMinutes) {
379
+ /**
380
+ * Deletes the launch files older than a threshold and keeps every tracked file.
381
+ *
382
+ * @param thresholdMinutes - The minutes a launch file must age before it is deleted.
383
+ * @returns The exit code: 0 on success, 2 when a launch file changed within the recent window.
384
+ */
385
+ export function runSweep(thresholdMinutes) {
191
386
  const now = Date.now();
192
387
  const present = LAUNCH_DIRECTORIES.filter((directory) => existsSync(directory));
193
388
  const tracked = new Set(listTracked(LAUNCH_DIRECTORIES));
@@ -211,7 +406,13 @@ function runSweep(thresholdMinutes) {
211
406
  removeEmptyDirectories(directory);
212
407
  return 0;
213
408
  }
214
- function listUnitFiles(unit) {
409
+ /**
410
+ * Lists one unit's files under the launch directories.
411
+ *
412
+ * @param unit - The unit name; a file belongs to it when its name is the unit followed by `-` or `.`.
413
+ * @returns Each file's path and modification time.
414
+ */
415
+ export function listUnitFiles(unit) {
215
416
  return LAUNCH_DIRECTORIES.filter((directory) => existsSync(directory))
216
417
  .flatMap((directory) => listFiles(directory, NOTHING_SKIPPED))
217
418
  .filter((file) => {
@@ -219,7 +420,14 @@ function listUnitFiles(unit) {
219
420
  return name.startsWith(unit) && /^[-.]/u.test(name.slice(unit.length));
220
421
  });
221
422
  }
222
- function runUnit(unit, remove) {
423
+ /**
424
+ * Lists or deletes one unit's launch files.
425
+ *
426
+ * @param unit - The unit name, or undefined when the call gave none.
427
+ * @param remove - If true, deletes the untracked files; if false, lists them.
428
+ * @returns The exit code: 0 on success, 2 when a deletion is refused, 64 on usage.
429
+ */
430
+ export function runUnit(unit, remove) {
223
431
  if (unit === undefined || !/^[A-Za-z0-9._][A-Za-z0-9._-]*$/u.test(unit)) {
224
432
  console.error('usage: sweep.ts --unit UNIT [--delete]');
225
433
  return 64;
@@ -243,16 +451,27 @@ function runUnit(unit, remove) {
243
451
  console.log(`sweep: ${files.length} file(s) for ${unit}`);
244
452
  return 0;
245
453
  }
246
- function main(argv) {
247
- if (!existsSync('.git')) {
248
- console.error('sweep: run from the repository root');
454
+ /**
455
+ * Runs the script on its arguments.
456
+ *
457
+ * @param argv - The arguments after the script path.
458
+ * @returns The exit code: 0 on success, 2 when a deletion is refused, 64 on usage.
459
+ */
460
+ export function main(argv) {
461
+ const unknown = readUnknownFlags(argv, FLAGS);
462
+ if (unknown.length > 0) {
463
+ console.error(`sweep: ${unknown.join(', ')} is not a flag this script reads; ${USAGE}`);
249
464
  return 64;
250
465
  }
251
- const missing = readMissingFlags(argv, ['--unit', '--older-than']);
466
+ const missing = readMissingFlags(argv, VALUE_FLAGS);
252
467
  if (missing.length > 0) {
253
468
  console.error(`sweep: ${missing.join(', ')} given with no value`);
254
469
  return 64;
255
470
  }
471
+ if (!existsSync('.git')) {
472
+ console.error('sweep: run from the repository root');
473
+ return 64;
474
+ }
256
475
  const modes = ['--report', '--tmp', '--unit'].filter((flag) => argv.includes(flag));
257
476
  if (modes.length > 1) {
258
477
  console.error('usage: sweep.ts names one mode: --report | --tmp [--older-than MINUTES] | --unit UNIT [--delete]');
@@ -270,7 +489,9 @@ function main(argv) {
270
489
  return runUnit(readOption(argv, '--unit'), argv.includes('--delete'));
271
490
  if (argv.length === 0 || argv.includes('--report'))
272
491
  return runReport();
273
- console.error('usage: sweep.ts --report | --tmp [--older-than MINUTES] | --unit UNIT [--delete]');
492
+ console.error(USAGE);
274
493
  return 64;
275
494
  }
276
- process.exitCode = main(process.argv.slice(2));
495
+ if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename) {
496
+ process.exitCode = main(process.argv.slice(2));
497
+ }