@orkestrel/scaffold 0.0.107 → 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 (164) 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 +368 -27
  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 +26 -20
  25. package/dist/host/agents/orchestration.md +98 -58
  26. package/dist/host/agents/platforms/posix.md +15 -0
  27. package/dist/host/agents/platforms/windows.md +37 -0
  28. package/dist/host/agents/skills/orkestrel-align/SKILL.md +10 -13
  29. package/dist/host/agents/skills/orkestrel-align/references/fleet.md +4 -10
  30. package/dist/host/agents/skills/orkestrel-align/references/integration.md +1 -1
  31. package/dist/host/agents/skills/orkestrel-build/SKILL.md +16 -48
  32. package/dist/host/agents/skills/orkestrel-build/agents/openai.yaml +1 -1
  33. package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +19 -16
  34. package/dist/host/agents/skills/orkestrel-debrief/agents/openai.yaml +1 -1
  35. package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +15 -28
  36. package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +5 -6
  37. package/dist/host/agents/skills/orkestrel-debrief/references/retention.md +12 -14
  38. package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +36 -24
  39. package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +14 -11
  40. package/dist/host/agents/skills/orkestrel-dispatch/scripts/bench.ts +366 -75
  41. package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +126 -16
  42. package/dist/host/agents/skills/orkestrel-dispatch/scripts/chain.ts +323 -0
  43. package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +220 -31
  44. package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +176 -33
  45. package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +294 -59
  46. package/dist/host/agents/skills/orkestrel-dispatch/scripts/patch.ts +401 -0
  47. package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +199 -34
  48. package/dist/host/agents/skills/orkestrel-dispatch/scripts/sweep.ts +428 -27
  49. package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +10 -8
  50. package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +1 -5
  51. package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +1 -1
  52. package/dist/host/agents/skills/orkestrel-falsify/scripts/mutate.ts +701 -0
  53. package/dist/host/agents/skills/orkestrel-harden/SKILL.md +16 -12
  54. package/dist/host/agents/skills/orkestrel-harden/references/bindings.md +23 -81
  55. package/dist/host/agents/skills/orkestrel-harden/references/centralization.md +4 -5
  56. package/dist/host/agents/skills/orkestrel-harden/references/contract.md +2 -0
  57. package/dist/host/agents/skills/orkestrel-harden/references/document.md +33 -0
  58. package/dist/host/agents/skills/orkestrel-harden/references/equivalence.md +6 -3
  59. package/dist/host/agents/skills/orkestrel-harden/references/hardening.md +8 -4
  60. package/dist/host/agents/skills/orkestrel-harden/references/research.md +2 -0
  61. package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +1272 -279
  62. package/dist/host/agents/skills/orkestrel-harden/scripts/measure.ts +425 -0
  63. package/dist/host/agents/skills/orkestrel-harden/scripts/titles.ts +603 -98
  64. package/dist/host/agents/skills/orkestrel-journey/SKILL.md +75 -98
  65. package/dist/host/agents/skills/orkestrel-journey/references/captures.md +54 -17
  66. package/dist/host/agents/skills/orkestrel-journey/references/decide.md +1 -20
  67. package/dist/host/agents/skills/orkestrel-journey/references/layer.md +21 -31
  68. package/dist/host/agents/skills/orkestrel-journey/references/styles.md +1 -1
  69. package/dist/host/agents/skills/orkestrel-parallel/SKILL.md +91 -30
  70. package/dist/host/agents/skills/orkestrel-parallel/agents/openai.yaml +2 -2
  71. package/dist/host/agents/skills/orkestrel-parallel/references/claude.md +34 -0
  72. package/dist/host/agents/skills/orkestrel-parallel/references/codex.md +29 -0
  73. package/dist/host/agents/skills/orkestrel-parallel/references/cursor.md +29 -0
  74. package/dist/host/agents/skills/orkestrel-parallel/references/t3.md +31 -0
  75. package/dist/host/agents/skills/orkestrel-parallel/scripts/cases.ts +389 -0
  76. package/dist/host/agents/skills/orkestrel-polish/SKILL.md +16 -20
  77. package/dist/host/agents/skills/orkestrel-polish/references/capture-harness.md +16 -26
  78. package/dist/host/agents/skills/orkestrel-publish/SKILL.md +12 -26
  79. package/dist/host/agents/skills/orkestrel-publish/agents/openai.yaml +2 -2
  80. package/dist/host/agents/skills/orkestrel-publish/references/adopt.md +23 -0
  81. package/dist/host/agents/skills/orkestrel-publish/references/release.md +4 -4
  82. package/dist/host/agents/skills/orkestrel-publish/references/wave.md +12 -10
  83. package/dist/host/agents/skills/orkestrel-publish/references/window.md +20 -25
  84. package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +348 -59
  85. package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +202 -22
  86. package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1061 -191
  87. package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +812 -140
  88. package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +280 -38
  89. package/dist/host/agents/skills/orkestrel-scout/SKILL.md +3 -3
  90. package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +428 -70
  91. package/dist/host/agents/templates/brief.md +1 -1
  92. package/dist/host/agents/transports/claude-cli.md +3 -3
  93. package/dist/host/agents/transports/codex.md +3 -8
  94. package/dist/host/agents/transports/cursor.md +2 -3
  95. package/dist/host/claude/AGENTS.md +7 -10
  96. package/dist/host/claude/agents/analyst.md +3 -3
  97. package/dist/host/claude/agents/astra.md +3 -3
  98. package/dist/host/claude/agents/builder.md +1 -1
  99. package/dist/host/claude/agents/checker.md +1 -1
  100. package/dist/host/claude/agents/distiller.md +1 -1
  101. package/dist/host/claude/agents/grok.md +1 -1
  102. package/dist/host/claude/agents/opus.md +2 -2
  103. package/dist/host/claude/agents/orkestrel.md +57 -90
  104. package/dist/host/claude/agents/researcher.md +1 -1
  105. package/dist/host/claude/agents/reviewer.md +4 -0
  106. package/dist/host/claude/agents/scout.md +1 -1
  107. package/dist/host/claude/agents/verifier.md +1 -1
  108. package/dist/host/claude/rules/application.md +14 -21
  109. package/dist/host/claude/rules/architecture.md +34 -44
  110. package/dist/host/claude/rules/browser.md +3 -4
  111. package/dist/host/claude/rules/campaign.md +55 -0
  112. package/dist/host/claude/rules/documentation.md +26 -11
  113. package/dist/host/claude/rules/names.md +9 -4
  114. package/dist/host/claude/rules/patterns.md +6 -7
  115. package/dist/host/claude/rules/portability.md +2 -3
  116. package/dist/host/claude/rules/quality.md +4 -5
  117. package/dist/host/claude/rules/styles.md +2 -2
  118. package/dist/host/claude/rules/tests.md +57 -114
  119. package/dist/host/claude/rules/typescript.md +8 -10
  120. package/dist/host/claude/rules/workspace.md +21 -19
  121. package/dist/host/claude/rules/writing.md +3 -3
  122. package/dist/host/claude/skills/orkestrel-dispatch/SKILL.md +1 -1
  123. package/dist/host/claude/skills/orkestrel-harden/SKILL.md +1 -1
  124. package/dist/host/claude/skills/orkestrel-parallel/SKILL.md +1 -1
  125. package/dist/host/claude/skills/orkestrel-publish/SKILL.md +1 -1
  126. package/dist/host/claude/skills/orkestrel-scout/SKILL.md +1 -1
  127. package/dist/host/codex/agents/analyst.toml +9 -3
  128. package/dist/host/codex/agents/astra.toml +3 -4
  129. package/dist/host/codex/agents/builder.toml +1 -1
  130. package/dist/host/codex/agents/opus.toml +3 -4
  131. package/dist/host/codex/agents/orkestrel.toml +1 -1
  132. package/dist/host/codex/agents/researcher.toml +2 -2
  133. package/dist/host/codex/agents/verifier.toml +1 -1
  134. package/dist/host/codex/config.toml +3 -3
  135. package/dist/host/configs/helpers.ts +21 -1
  136. package/dist/host/configs/policy.ts +405 -16
  137. package/dist/host/cursor/rules/orchestration.mdc +1 -0
  138. package/dist/host/dotfiles/oxlintrc.json +141 -5
  139. package/dist/host/guides/agent.md +111 -134
  140. package/dist/host/guides/capture.md +801 -0
  141. package/dist/host/guides/probe.md +27 -8
  142. package/dist/host/guides/scaffold.md +152 -19
  143. package/dist/host/guides/study.md +534 -440
  144. package/dist/host/guides/test.md +111 -44
  145. package/dist/host/manifest.json +199 -242
  146. package/dist/host/tests/config.test.ts +1558 -219
  147. package/dist/host/tests/policy.test.ts +127 -1
  148. package/dist/host/tests/setupPolicy.ts +1389 -39
  149. package/dist/src/core/index.cjs +239 -87
  150. package/dist/src/core/index.cjs.map +1 -1
  151. package/dist/src/core/index.d.cts +123 -39
  152. package/dist/src/core/index.d.ts +123 -39
  153. package/dist/src/core/index.js +234 -86
  154. package/dist/src/core/index.js.map +1 -1
  155. package/dist/src/server/index.cjs +20 -27
  156. package/dist/src/server/index.cjs.map +1 -1
  157. package/dist/src/server/index.js +21 -28
  158. package/dist/src/server/index.js.map +1 -1
  159. package/package.json +11 -11
  160. package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +0 -283
  161. package/dist/host/agents/skills/orkestrel-dispatch/references/bench.md +0 -25
  162. package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +0 -333
  163. package/dist/host/agents/skills/orkestrel-parallel/references/harnesses.md +0 -19
  164. package/dist/host/guides/supervisor.md +0 -4890
@@ -2,18 +2,26 @@
2
2
  // node .agents/skills/orkestrel-dispatch/scripts/sweep.ts --report
3
3
  // node .agents/skills/orkestrel-dispatch/scripts/sweep.ts --tmp [--older-than MINUTES]
4
4
  // node .agents/skills/orkestrel-dispatch/scripts/sweep.ts --unit UNIT [--delete]
5
- // --report lists leftovers under tmp/ and .orkestrel/ by age and deletes nothing.
5
+ // --report lists leftovers under tmp/ and .orkestrel/ by age and deletes nothing. It also checks
6
+ // every .md file under .orkestrel/: a backticked repository-relative path that git ignores prints
7
+ // `<file>:<line> cites ignored path <path>` (a path git tracks is never reported, nor is a glob or
8
+ // placeholder whose literal prefix git tracks), and an absolute machine path on a line prints
9
+ // `<file>:<line> names a machine path`. Both lines are report-only.
6
10
  // --tmp deletes launch files older than the threshold (default 60 minutes), removes emptied
7
11
  // directories, and refuses while any launch file changed in the last 2 minutes.
8
12
  // --unit lists one accepted unit's files under the launch directories (the brief, report, claims,
9
13
  // journal, errors, status, pid, and last-message files whose name is UNIT followed by `-` or `.`), and
10
14
  // --delete removes them, refusing while one changed in the last 2 minutes.
11
- // Exit 0 on success, 2 when a deletion is refused, 64 on usage.
12
- import { existsSync, readdirSync, rmSync } from 'node:fs'
15
+ // --tmp and --unit --delete never delete a file git tracks: each prints `kept tracked <path>` and the
16
+ // refusal ignores a tracked file; --unit alone marks a tracked file in its listing.
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.
19
+ import { spawnSync } from 'node:child_process'
20
+ import { existsSync, readdirSync, readFileSync, realpathSync, rmSync, statSync } from 'node:fs'
13
21
  import { basename, join } from 'node:path'
14
- import { listFiles, readMissingFlags, readOption } from './helpers.ts'
15
22
 
16
- const LAUNCH_DIRECTORIES = [
23
+ /** Lists the directories under `tmp/` that hold launch files. */
24
+ export const LAUNCH_DIRECTORIES: readonly string[] = [
17
25
  'tmp/units',
18
26
  'tmp/cursor',
19
27
  'tmp/codex',
@@ -22,16 +30,183 @@ const LAUNCH_DIRECTORIES = [
22
30
  'tmp/type',
23
31
  'tmp/captures',
24
32
  ]
25
- const RECENT_MS = 2 * 60_000
26
33
 
27
- // A launch directory holds nothing a walk skips, so every file beneath it counts.
28
- const NOTHING_SKIPPED: ReadonlySet<string> = new Set()
34
+ /** Holds the milliseconds within which a changed launch file counts as still in use. */
35
+ export const RECENT_MS = 2 * 60_000
29
36
 
30
- function formatDay(modified: number): string {
37
+ /** Holds the minutes a launch file must age before `--tmp` deletes it, when the call gives no threshold. */
38
+ export const DEFAULT_THRESHOLD = 60
39
+
40
+ /** Names the directories a tree walk skips. */
41
+ export const SKIPPED_DIRECTORIES: ReadonlySet<string> = new Set([
42
+ 'node_modules',
43
+ '.git',
44
+ 'dist',
45
+ 'tmp',
46
+ '.orkestrel',
47
+ '.idea',
48
+ ])
49
+
50
+ /** Names no directory: a launch directory holds nothing a walk skips, so every file beneath it counts. */
51
+ export const NOTHING_SKIPPED: ReadonlySet<string> = new Set()
52
+
53
+ /** Holds the buffer size for git's output, which the repository's file count bounds. */
54
+ export const GIT_BUFFER = 64 * 1024 * 1024
55
+
56
+ /**
57
+ * Matches an absolute machine path: a drive-letter path, or one that opens `/Users/`, `/home/`, or
58
+ * `~/`. The lookbehind keeps `https://`, `src/home/`, and `./home/` from matching.
59
+ */
60
+ export const MACHINE_PATH = /(?<![\w.-])(?:[A-Za-z]:[\\/]|\/(?:Users|home)\/|~\/)/u
61
+
62
+ /** Matches one backticked token in a line. */
63
+ export const BACKTICKED = /`([^`\n]+)`/gu
64
+
65
+ /** Matches the first glob or placeholder character in a path. */
66
+ export const PLACEHOLDER = /[*?<{]/u
67
+
68
+ /** Lists the flags that take a value. */
69
+ export const VALUE_FLAGS: readonly string[] = ['--unit', '--older-than']
70
+
71
+ /** Lists every flag the script reads, value flags and switches alike. */
72
+ export const FLAGS: readonly string[] = [...VALUE_FLAGS, '--report', '--tmp', '--delete']
73
+
74
+ /** Holds the usage line printed when a call names an unknown mode. */
75
+ export const USAGE =
76
+ 'usage: sweep.ts --report | --tmp [--older-than MINUTES] | --unit UNIT [--delete]'
77
+
78
+ /** Describes one file a tree walk found, with the modification time the sweep reads. */
79
+ export interface WalkedFile {
80
+ readonly path: string
81
+ readonly modified: number
82
+ }
83
+
84
+ /** Describes a backticked repository path: the path, whether it names a directory, and its glob prefix. */
85
+ export interface Citation {
86
+ readonly path: string
87
+ readonly directory: boolean
88
+ readonly prefix: string | undefined
89
+ }
90
+
91
+ /** Describes one report finding: a machine path when the citation is absent, a cited path otherwise. */
92
+ export interface Finding {
93
+ readonly file: string
94
+ readonly line: number
95
+ readonly citation: Citation | undefined
96
+ }
97
+
98
+ /**
99
+ * Reads the value that follows a flag.
100
+ *
101
+ * @remarks
102
+ * The first occurrence of a flag wins, a following value that opens with `--` is a missing value,
103
+ * and the `--flag=value` form is not read.
104
+ *
105
+ * @param argv - The arguments after the script path.
106
+ * @param name - The flag, such as `--unit`.
107
+ * @returns The value after the flag, or undefined when the flag is absent, last, or followed by
108
+ * another flag.
109
+ */
110
+ export function readOption(argv: readonly string[], name: string): string | undefined {
111
+ const index = argv.indexOf(name)
112
+ const value = index === -1 ? undefined : argv[index + 1]
113
+ return value === undefined || value.startsWith('--') ? undefined : value
114
+ }
115
+
116
+ /**
117
+ * Names the value flags a caller gave with no value.
118
+ *
119
+ * @param argv - The arguments after the script path.
120
+ * @param names - The flags that take a value.
121
+ * @returns Each named flag that appears last or is followed by another flag, in argument order.
122
+ */
123
+ export function readMissingFlags(
124
+ argv: readonly string[],
125
+ names: readonly string[],
126
+ ): readonly string[] {
127
+ return names.filter((name) => argv.includes(name) && readOption(argv, name) === undefined)
128
+ }
129
+
130
+ /**
131
+ * Names the flags a caller gave that the script does not read.
132
+ *
133
+ * @remarks
134
+ * A `--flag=value` argument is unknown, because the script reads no such form. A bare `--` ends the
135
+ * scan.
136
+ *
137
+ * @param argv - The arguments after the script path.
138
+ * @param flags - Every flag the script reads, value flags and switches alike.
139
+ * @returns Each argument that opens with `--` and is not a read flag, in argument order.
140
+ */
141
+ export function readUnknownFlags(
142
+ argv: readonly string[],
143
+ flags: readonly string[],
144
+ ): readonly string[] {
145
+ const end = argv.indexOf('--')
146
+ return (end === -1 ? argv : argv.slice(0, end)).filter(
147
+ (argument) => argument.startsWith('--') && !flags.includes(argument),
148
+ )
149
+ }
150
+
151
+ /**
152
+ * Normalizes a host path to forward slashes.
153
+ *
154
+ * @param path - A host path.
155
+ * @returns The same path with every backslash replaced by a slash.
156
+ */
157
+ export function normalizeSlashes(path: string): string {
158
+ return path.split('\\').join('/')
159
+ }
160
+
161
+ /**
162
+ * Lists every regular file beneath a directory, skipping the directories the script never reads.
163
+ *
164
+ * @remarks
165
+ * A skipped name matches a directory at any depth, exactly and case-sensitively.
166
+ *
167
+ * @param directory - The directory to walk.
168
+ * @param skipped - The directory names to skip. Default: the skipped directories.
169
+ * @returns Each file's forward-slash path and modification time, in walk order.
170
+ * @throws Thrown when `directory` does not exist or cannot be read.
171
+ */
172
+ export function listFiles(
173
+ directory: string,
174
+ skipped: ReadonlySet<string> = SKIPPED_DIRECTORIES,
175
+ ): readonly WalkedFile[] {
176
+ const files: WalkedFile[] = []
177
+ const pending = [directory]
178
+ while (pending.length > 0) {
179
+ const current = pending.pop()
180
+ if (current === undefined) break
181
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
182
+ if (skipped.has(entry.name)) continue
183
+ const path = join(current, entry.name)
184
+ if (entry.isDirectory()) pending.push(path)
185
+ else if (entry.isFile()) {
186
+ files.push({ path: normalizeSlashes(path), modified: statSync(path).mtimeMs })
187
+ }
188
+ }
189
+ }
190
+ return files
191
+ }
192
+
193
+ /**
194
+ * Formats a modification time as its UTC day.
195
+ *
196
+ * @param modified - A time in milliseconds since the epoch.
197
+ * @returns The day as `YYYY-MM-DD`.
198
+ */
199
+ export function formatDay(modified: number): string {
31
200
  return new Date(modified).toISOString().slice(0, 10)
32
201
  }
33
202
 
34
- function reportDirectory(directory: string): string | undefined {
203
+ /**
204
+ * Reports how many files a launch directory holds and the age of the oldest.
205
+ *
206
+ * @param directory - The directory to report on.
207
+ * @returns The report line, or undefined when the directory is absent or empty.
208
+ */
209
+ export function reportDirectory(directory: string): string | undefined {
35
210
  if (!existsSync(directory)) return undefined
36
211
  const files = listFiles(directory, NOTHING_SKIPPED)
37
212
  if (files.length === 0) return undefined
@@ -39,21 +214,36 @@ function reportDirectory(directory: string): string | undefined {
39
214
  return `sweep: ${directory} holds ${files.length} file(s), oldest ${formatDay(oldest)}`
40
215
  }
41
216
 
42
- function listCampaignDirectories(): readonly string[] {
217
+ /**
218
+ * Lists the campaign directories under `.orkestrel/`.
219
+ *
220
+ * @returns Each directory's path, or nothing when `.orkestrel/` is absent.
221
+ */
222
+ export function listCampaignDirectories(): readonly string[] {
43
223
  if (!existsSync('.orkestrel')) return []
44
224
  return readdirSync('.orkestrel', { withFileTypes: true })
45
225
  .filter((entry) => entry.isDirectory())
46
226
  .map((entry) => join('.orkestrel', entry.name))
47
227
  }
48
228
 
49
- function reportCampaignFiles(): string | undefined {
229
+ /**
230
+ * Reports the ecosystem files that sit directly under `.orkestrel/`.
231
+ *
232
+ * @returns The report line, or undefined when there are none.
233
+ */
234
+ export function reportCampaignFiles(): string | undefined {
50
235
  if (!existsSync('.orkestrel')) return undefined
51
236
  const files = readdirSync('.orkestrel', { withFileTypes: true }).filter((entry) => entry.isFile())
52
237
  if (files.length === 0) return undefined
53
238
  return `sweep: .orkestrel holds ${files.length} ecosystem file(s): ${files.map((entry) => entry.name).join(', ')}`
54
239
  }
55
240
 
56
- function removeEmptyDirectories(directory: string): void {
241
+ /**
242
+ * Removes every empty directory beneath a directory, deepest first.
243
+ *
244
+ * @param directory - The directory whose empty descendants are removed; the directory itself stays.
245
+ */
246
+ export function removeEmptyDirectories(directory: string): void {
57
247
  for (const entry of readdirSync(directory, { withFileTypes: true })) {
58
248
  if (!entry.isDirectory()) continue
59
249
  const path = join(directory, entry.name)
@@ -62,33 +252,208 @@ function removeEmptyDirectories(directory: string): void {
62
252
  }
63
253
  }
64
254
 
65
- function readThreshold(argv: readonly string[]): number | undefined {
66
- if (!argv.includes('--older-than')) return 60
255
+ /**
256
+ * Reads the `--older-than` threshold in minutes.
257
+ *
258
+ * @param argv - The arguments after the script path.
259
+ * @returns The default threshold when the flag is absent, the number when it is finite and above 0,
260
+ * and undefined otherwise.
261
+ */
262
+ export function readThreshold(argv: readonly string[]): number | undefined {
263
+ if (!argv.includes('--older-than')) return DEFAULT_THRESHOLD
67
264
  const value = Number(readOption(argv, '--older-than'))
68
265
  return Number.isFinite(value) && value > 0 ? value : undefined
69
266
  }
70
267
 
71
- function runReport(): number {
268
+ /**
269
+ * Lists the files git tracks beneath a scope, as forward-slash paths from the root.
270
+ *
271
+ * @remarks
272
+ * Any git failure reads as nothing tracked, so the run keeps its old behavior.
273
+ *
274
+ * @param scope - The paths to list beneath; the whole repository when empty.
275
+ * @returns The tracked paths.
276
+ */
277
+ export function listTracked(scope: readonly string[]): readonly string[] {
278
+ const args = ['ls-files', '-z', ...(scope.length === 0 ? [] : ['--', ...scope])]
279
+ const run = spawnSync('git', args, { encoding: 'utf8', windowsHide: true, maxBuffer: GIT_BUFFER })
280
+ if (run.status !== 0) return []
281
+ return run.stdout.split('\0').filter((path) => path !== '')
282
+ }
283
+
284
+ /**
285
+ * Names the probes git ignores, from one batched `git check-ignore`.
286
+ *
287
+ * @remarks
288
+ * Status 0 and 1 both answer, and any other status reads as nothing ignored.
289
+ *
290
+ * @param probes - The paths to ask about.
291
+ * @returns The probes git ignores.
292
+ */
293
+ export function readIgnored(probes: readonly string[]): ReadonlySet<string> {
294
+ if (probes.length === 0) return new Set()
295
+ const run = spawnSync('git', ['check-ignore', '--stdin', '-z'], {
296
+ encoding: 'utf8',
297
+ windowsHide: true,
298
+ maxBuffer: GIT_BUFFER,
299
+ input: probes.join('\0'),
300
+ })
301
+ if (run.status !== 0 && run.status !== 1) return new Set()
302
+ return new Set(run.stdout.split('\0').filter((path) => path !== ''))
303
+ }
304
+
305
+ /**
306
+ * Reads a backticked token as a repository-relative citation.
307
+ *
308
+ * @remarks
309
+ * A token counts when it has no whitespace, contains `/`, and is no URL; the path stops at its first
310
+ * glob or placeholder character and drops a trailing `/`. A machine path, a path outside the
311
+ * repository, and a backslash path are no citation.
312
+ *
313
+ * @param token - The text between two backticks.
314
+ * @returns The citation, or undefined when the token is none.
315
+ */
316
+ export function readCitation(token: string): Citation | undefined {
317
+ if (/\s/u.test(token) || !token.includes('/') || token.includes('://')) return undefined
318
+ const stop = token.search(PLACEHOLDER)
319
+ const cut = (stop === -1 ? token : token.slice(0, stop)).replace(/^\.\//u, '')
320
+ const path = cut.replace(/\/+$/u, '')
321
+ if (path === '' || /^(?:\/|~|\.\.(?:\/|$)|-|[A-Za-z]:)/u.test(path) || path.includes('\\')) {
322
+ return undefined
323
+ }
324
+ return { path, directory: cut.endsWith('/'), prefix: stop === -1 ? undefined : cut }
325
+ }
326
+
327
+ /**
328
+ * Lists the findings a campaign Markdown file carries: machine paths and backticked citations.
329
+ *
330
+ * @param file - The Markdown file to read.
331
+ * @returns Each finding with its file and line, a machine path before the citations of its line.
332
+ */
333
+ export function listFindings(file: string): readonly Finding[] {
334
+ const lines = readFileSync(file, 'utf8').split(/\r\n|\n/u)
335
+ return lines.flatMap((text, index) => {
336
+ const line = index + 1
337
+ const machine = MACHINE_PATH.test(text) ? [{ file, line, citation: undefined }] : []
338
+ const cited = [...text.matchAll(BACKTICKED)].flatMap((match) => {
339
+ const citation = readCitation(match[1] ?? '')
340
+ return citation === undefined ? [] : [{ file, line, citation }]
341
+ })
342
+ return [...machine, ...cited]
343
+ })
344
+ }
345
+
346
+ /**
347
+ * Builds the path git is asked about for a citation.
348
+ *
349
+ * @param citation - The citation.
350
+ * @returns The path, with a trailing `/` when the citation names a directory.
351
+ */
352
+ export function probeOf(citation: Citation): string {
353
+ return citation.directory ? `${citation.path}/` : citation.path
354
+ }
355
+
356
+ /**
357
+ * Checks whether git tracks a citation's path, a file beneath it, or a file its glob prefix opens.
358
+ *
359
+ * @param citation - The citation.
360
+ * @param tracked - The tracked paths.
361
+ * @returns True if a tracked path covers the citation; false otherwise.
362
+ */
363
+ export function isCovered(citation: Citation, tracked: readonly string[]): boolean {
364
+ return tracked.some(
365
+ (path) =>
366
+ path === citation.path ||
367
+ path.startsWith(`${citation.path}/`) ||
368
+ (citation.prefix !== undefined && path.startsWith(citation.prefix)),
369
+ )
370
+ }
371
+
372
+ /**
373
+ * Describes a finding as the message the report prints for it.
374
+ *
375
+ * @param finding - The finding.
376
+ * @param ignored - The probes git ignores.
377
+ * @param tracked - The tracked paths.
378
+ * @returns `names a machine path`, `cites ignored path <path>`, or undefined when git tracks the
379
+ * cited path or does not ignore it.
380
+ */
381
+ export function describeFinding(
382
+ finding: Finding,
383
+ ignored: ReadonlySet<string>,
384
+ tracked: readonly string[],
385
+ ): string | undefined {
386
+ const { citation } = finding
387
+ if (citation === undefined) return 'names a machine path'
388
+ return ignored.has(probeOf(citation)) && !isCovered(citation, tracked)
389
+ ? `cites ignored path ${citation.path}`
390
+ : undefined
391
+ }
392
+
393
+ /**
394
+ * Reports the campaign Markdown lines that cite an ignored path or name a machine path.
395
+ *
396
+ * @returns One report line per finding, each file and line and message once.
397
+ */
398
+ export function reportCitations(): readonly string[] {
399
+ if (!existsSync('.orkestrel')) return []
400
+ const findings = listFiles('.orkestrel', NOTHING_SKIPPED)
401
+ .map((file) => file.path)
402
+ .filter((path) => /\.md$/iu.test(path))
403
+ .sort()
404
+ .flatMap((path) => listFindings(path))
405
+ const probes = new Set(
406
+ findings.flatMap(({ citation }) => (citation === undefined ? [] : [probeOf(citation)])),
407
+ )
408
+ const ignored = readIgnored([...probes])
409
+ const tracked = ignored.size === 0 ? [] : listTracked([])
410
+ const lines = new Set(
411
+ findings.flatMap((finding) => {
412
+ const text = describeFinding(finding, ignored, tracked)
413
+ return text === undefined ? [] : [`${finding.file}:${finding.line} ${text}`]
414
+ }),
415
+ )
416
+ return [...lines].map((key) => `sweep: ${key}`)
417
+ }
418
+
419
+ /**
420
+ * Prints the leftover report.
421
+ *
422
+ * @returns The exit code, always 0.
423
+ */
424
+ export function runReport(): number {
72
425
  for (const directory of [...LAUNCH_DIRECTORIES, ...listCampaignDirectories()]) {
73
426
  const line = reportDirectory(directory)
74
427
  if (line !== undefined) console.log(line)
75
428
  }
76
429
  const files = reportCampaignFiles()
77
430
  if (files !== undefined) console.log(files)
431
+ for (const line of reportCitations()) console.log(line)
78
432
  return 0
79
433
  }
80
434
 
81
- function runSweep(thresholdMinutes: number): number {
435
+ /**
436
+ * Deletes the launch files older than a threshold and keeps every tracked file.
437
+ *
438
+ * @param thresholdMinutes - The minutes a launch file must age before it is deleted.
439
+ * @returns The exit code: 0 on success, 2 when a launch file changed within the recent window.
440
+ */
441
+ export function runSweep(thresholdMinutes: number): number {
82
442
  const now = Date.now()
83
443
  const present = LAUNCH_DIRECTORIES.filter((directory) => existsSync(directory))
444
+ const tracked = new Set(listTracked(LAUNCH_DIRECTORIES))
84
445
  const files = present.flatMap((directory) => listFiles(directory, NOTHING_SKIPPED))
85
- if (files.some((file) => now - file.modified < RECENT_MS)) {
446
+ if (files.some((file) => !tracked.has(file.path) && now - file.modified < RECENT_MS)) {
86
447
  console.error('sweep: a launch file changed within the last 2 minutes; refusing to delete')
87
448
  return 2
88
449
  }
89
450
  const cutoff = now - thresholdMinutes * 60_000
90
451
  for (const file of files) {
91
452
  if (file.modified > cutoff) continue
453
+ if (tracked.has(file.path)) {
454
+ console.log(`sweep: kept tracked ${file.path}`)
455
+ continue
456
+ }
92
457
  rmSync(file.path, { force: true })
93
458
  console.log(`sweep: deleted ${file.path}`)
94
459
  }
@@ -96,7 +461,13 @@ function runSweep(thresholdMinutes: number): number {
96
461
  return 0
97
462
  }
98
463
 
99
- function listUnitFiles(unit: string): ReadonlyArray<{ path: string; modified: number }> {
464
+ /**
465
+ * Lists one unit's files under the launch directories.
466
+ *
467
+ * @param unit - The unit name; a file belongs to it when its name is the unit followed by `-` or `.`.
468
+ * @returns Each file's path and modification time.
469
+ */
470
+ export function listUnitFiles(unit: string): ReadonlyArray<{ path: string; modified: number }> {
100
471
  return LAUNCH_DIRECTORIES.filter((directory) => existsSync(directory))
101
472
  .flatMap((directory) => listFiles(directory, NOTHING_SKIPPED))
102
473
  .filter((file) => {
@@ -105,17 +476,34 @@ function listUnitFiles(unit: string): ReadonlyArray<{ path: string; modified: nu
105
476
  })
106
477
  }
107
478
 
108
- function runUnit(unit: string | undefined, remove: boolean): number {
479
+ /**
480
+ * Lists or deletes one unit's launch files.
481
+ *
482
+ * @param unit - The unit name, or undefined when the call gave none.
483
+ * @param remove - If true, deletes the untracked files; if false, lists them.
484
+ * @returns The exit code: 0 on success, 2 when a deletion is refused, 64 on usage.
485
+ */
486
+ export function runUnit(unit: string | undefined, remove: boolean): number {
109
487
  if (unit === undefined || !/^[A-Za-z0-9._][A-Za-z0-9._-]*$/u.test(unit)) {
110
488
  console.error('usage: sweep.ts --unit UNIT [--delete]')
111
489
  return 64
112
490
  }
113
491
  const files = listUnitFiles(unit)
114
- if (remove && files.some((file) => Date.now() - file.modified < RECENT_MS)) {
492
+ const tracked = new Set(listTracked(LAUNCH_DIRECTORIES))
493
+ if (
494
+ remove &&
495
+ files.some((file) => !tracked.has(file.path) && Date.now() - file.modified < RECENT_MS)
496
+ ) {
115
497
  console.error(`sweep: a file of ${unit} changed within the last 2 minutes; refusing to delete`)
116
498
  return 2
117
499
  }
118
500
  for (const file of files) {
501
+ if (tracked.has(file.path)) {
502
+ console.log(
503
+ remove ? `sweep: kept tracked ${file.path}` : `sweep: found ${file.path} (tracked)`,
504
+ )
505
+ continue
506
+ }
119
507
  if (remove) rmSync(file.path, { force: true })
120
508
  console.log(`sweep: ${remove ? 'deleted' : 'found'} ${file.path}`)
121
509
  }
@@ -123,16 +511,27 @@ function runUnit(unit: string | undefined, remove: boolean): number {
123
511
  return 0
124
512
  }
125
513
 
126
- function main(argv: readonly string[]): number {
127
- if (!existsSync('.git')) {
128
- console.error('sweep: run from the repository root')
514
+ /**
515
+ * Runs the script on its arguments.
516
+ *
517
+ * @param argv - The arguments after the script path.
518
+ * @returns The exit code: 0 on success, 2 when a deletion is refused, 64 on usage.
519
+ */
520
+ export function main(argv: readonly string[]): number {
521
+ const unknown = readUnknownFlags(argv, FLAGS)
522
+ if (unknown.length > 0) {
523
+ console.error(`sweep: ${unknown.join(', ')} is not a flag this script reads; ${USAGE}`)
129
524
  return 64
130
525
  }
131
- const missing = readMissingFlags(argv, ['--unit', '--older-than'])
526
+ const missing = readMissingFlags(argv, VALUE_FLAGS)
132
527
  if (missing.length > 0) {
133
528
  console.error(`sweep: ${missing.join(', ')} given with no value`)
134
529
  return 64
135
530
  }
531
+ if (!existsSync('.git')) {
532
+ console.error('sweep: run from the repository root')
533
+ return 64
534
+ }
136
535
  const modes = ['--report', '--tmp', '--unit'].filter((flag) => argv.includes(flag))
137
536
  if (modes.length > 1) {
138
537
  console.error(
@@ -150,8 +549,10 @@ function main(argv: readonly string[]): number {
150
549
  }
151
550
  if (argv.includes('--unit')) return runUnit(readOption(argv, '--unit'), argv.includes('--delete'))
152
551
  if (argv.length === 0 || argv.includes('--report')) return runReport()
153
- console.error('usage: sweep.ts --report | --tmp [--older-than MINUTES] | --unit UNIT [--delete]')
552
+ console.error(USAGE)
154
553
  return 64
155
554
  }
156
555
 
157
- process.exitCode = main(process.argv.slice(2))
556
+ if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename) {
557
+ process.exitCode = main(process.argv.slice(2))
558
+ }
@@ -6,17 +6,19 @@ description: >-
6
6
 
7
7
  # Falsify
8
8
 
9
- One round. It ends with a ruling, and a second round needs an added or repaired claim.
9
+ One round. It ends with a ruling. `.agents/orchestration.md` § Size gate decides when a round runs, what a fix round re-audits, and when an all-confirmed round ends the audit.
10
10
 
11
- ## Run a round only when
11
+ ## Scripts
12
+
13
+ Run from the checkout root. The script's opening comment owns its behavior.
14
+
15
+ - `scripts/mutate.ts --edits EDITS.md -- COMMAND [ARGS...]` runs the mutation probe. It resolves an edit file, the `### <path>` block shape that `.agents/skills/orkestrel-dispatch/scripts/patch.ts` reads, before it touches a file; saves the originals under `tmp/probes/mutate/`; writes the mutated files; runs the command with no shell and its output on standard error; restores the bytes and proves each file's SHA-256 digest; runs the command again; and prints one JSON line with both exits. A mutation that leaves the suite uncollected also reads as a failing run, so read the command's own test count on standard error. A run killed mid-command leaves the tree mutated: the next run refuses to start while a run directory remains and names the line that restores it, `scripts/mutate.ts --restore tmp/probes/mutate/run-NAME`, which writes the saved originals back, proves each digest from the manifest, and deletes the directory. Exit 0 when the mutated run failed and the restored run passed (or a `--restore` is proven), 3 when either did not, 2 when a restore is not proven or the command cannot start, 64 usage, an edit that does not resolve, or a run directory left behind.
16
+
17
+ ## Run a round the size gate does not name when
12
18
 
13
- - the size gate names a review (medium: one pass; large: one round on the integrated result);
14
- - a fix round is about to be accepted and the fix departed from the reviewer's prescription;
15
19
  - a version is about to be bumped, packed, or published;
16
20
  - the same defect class appeared in more than one round.
17
21
 
18
- Do not run one for a small change, a rename, or work whose failure the touched test already shows. Do not re-run a round whose claims all held when nothing was added or repaired since. A fix that adopted the prescription verbatim closes with a mutation probe instead of a round.
19
-
20
22
  ## Write the claims
21
23
 
22
24
  Write `tmp/units/<unit>-claims.md` and point every lane at it. Read `references/brief.md` for the claim form.
@@ -31,9 +33,9 @@ Write `tmp/units/<unit>-claims.md` and point every lane at it. Read `references/
31
33
 
32
34
  ## Dispatch the lanes
33
35
 
34
- - Medium: one lane, with an independent reviewer per `.agents/orchestration.md` § Engines (`reviewer` for Astra-written work, `analyst` for Opus-written work). Large: the objective lane and the subjective lane, same claims file, each in a clean context, neither shown the other's answer before reconciliation. Add `checker` when criteria are mechanical.
36
+ - Medium: one lane, with an independent reviewer per `.agents/orchestration.md` § Engines. Large: the objective lane and the subjective lane, same claims file, each in a clean context, neither shown the other's answer before reconciliation. Add `checker` when criteria are mechanical.
35
37
  - Tell a lane when its own engine wrote the half it audits.
36
- - Give a lane the evidence a read-only allowlist cannot produce: run the probe yourself, record its control and output, and hand it over.
38
+ - Give a lane the evidence a read-only allowlist cannot produce: run the probe yourself, record its control and output, and hand it over. Run a mutation probe with `scripts/mutate.ts`; its JSON line carries both exits.
37
39
  - Auditors edit no source and spawn nothing. Their reports are immutable after they return.
38
40
 
39
41
  ## Verdict shape
@@ -12,7 +12,6 @@ Give every claims brief these rows.
12
12
  - **Review evidence.** The actual diff and the actual `git status --porcelain` output, by path. For a rendered or externally driven surface, the capture; source is corroboration.
13
13
  - **Numbered falsifiable claims.** Written to `tmp/units/<unit>-claims.md`, one file both lanes read. Each claim names a property a concrete input, state, or interleaving could show false. Assign a primary lane where the lanes differ in strength; no lane skips a claim. The claim set is the round's scope: cover what the subject owns, then hold it closed. A finding against something no claim names enters the verdict only when substantiated to the `BROKEN` standard; otherwise it is a claim for the successor brief.
14
14
  - **Unknowns.** What the Orchestrator does not know that the round needs, and how the lane reports on it.
15
- - **The threshold.** State that a finding is worth more than a clean pass: the alternative is a consumer finding it after publication.
16
15
 
17
16
  ## The lane's brief
18
17
 
@@ -36,7 +35,6 @@ A re-run takes a successor brief named per `.agents/orchestration.md` § Dispatc
36
35
  - "The instruments bind." Test the instrument's rule: name a change it would not catch.
37
36
  - "No instrument is vacuous." Require a control that cannot produce its failing verdict; name a tautology a previous round shipped.
38
37
  - "The guide is true." Ask whether a false universal was replaced by an unfalsifiable one.
39
- - "The package is coherent as a whole. Would you ship this?"
40
38
  - "The self-declared sound-and-unchanged verdicts are sound." Require the lane to test the ones it judges most likely wrong.
41
39
  - "The rewrite preserves behavior." Judge it by `.agents/skills/orkestrel-harden/references/equivalence.md`, and require a differential row for each class it names that the changed code admits.
42
40
 
@@ -44,9 +42,7 @@ A re-run takes a successor brief named per `.agents/orchestration.md` § Dispatc
44
42
 
45
43
  - "CONFIRMED requires naming the falsification you tried that failed."
46
44
  - "A claim you cannot decide is UNRESOLVED, not CONFIRMED; say what would settle it."
47
- - "Assume this chain has one more." Name the prior rounds and the defect a previous round believed closed.
48
- - "Do not hedge toward an imagined consensus."
49
- - Never write "an audit returning only confirmations has not tried"; a lane that manufactures a finding to satisfy the brief costs a fix unit and the credibility of the true findings beside it. Test the adequacy of an all-confirmed round afterwards, against the brief.
45
+ - Write no sentence that presses a lane toward a finding. Test the adequacy of an all-confirmed round afterwards, against the brief.
50
46
 
51
47
  ## Keep out of a brief
52
48
 
@@ -53,5 +53,5 @@ Record which engine was right and on what.
53
53
  - Every retained finding names the fix-brief item that carries it; walk the list once.
54
54
  - Drop, on the record, anything no lane can substantiate against the evidence.
55
55
  - Promote anything that must outlive the round into a durable artifact before the working files are swept.
56
- - Assign the fix round's lane to an independent reviewer per `.agents/orchestration.md` § Engines. The next round's brief is this round's successor.
56
+ - Assign the fix round per `.agents/orchestration.md` § Size gate. The next round's brief is this round's successor.
57
57
  - Accept when the claims are satisfied on evidence (`VERDICT: PASS` against a claim set covering what the subject owns), never on green gates alone. Bound the claim set at the brief, rule on what it returned, and close; never re-run because a counterexample can still be imagined.