@orkestrel/scaffold 0.0.77 → 0.0.79

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 (158) hide show
  1. package/dist/agents/skills/orkestrel-dispatch/scripts/bench.js +204 -0
  2. package/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +102 -0
  3. package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +95 -0
  4. package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +207 -0
  5. package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +108 -0
  6. package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +114 -0
  7. package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +108 -0
  8. package/dist/agents/skills/orkestrel-dispatch/scripts/sweep.js +156 -0
  9. package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +196 -0
  10. package/dist/agents/skills/orkestrel-publish/scripts/compare.js +206 -0
  11. package/dist/agents/skills/orkestrel-publish/scripts/pins.js +93 -0
  12. package/dist/agents/skills/orkestrel-publish/scripts/wave.js +458 -0
  13. package/dist/agents/skills/orkestrel-publish/scripts/window.js +188 -0
  14. package/dist/agents/skills/orkestrel-scout/scripts/map.js +300 -0
  15. package/dist/agents/templates/brief.md +55 -0
  16. package/dist/bin/main.js +58 -6
  17. package/dist/bin/main.js.map +1 -1
  18. package/dist/host/AGENTS.md +77 -135
  19. package/dist/host/agents/orchestration.md +147 -998
  20. package/dist/host/agents/skills/enterprise-bootstrap/SKILL.md +2 -2
  21. package/dist/host/agents/skills/enterprise-bootstrap/references/inspection.md +1 -1
  22. package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/SKILL.md +6 -13
  23. package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/agents/openai.yaml +1 -1
  24. package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/references/fleet.md +5 -7
  25. package/dist/host/agents/skills/{orkestrel-build-application → orkestrel-build}/SKILL.md +11 -22
  26. package/dist/host/agents/skills/{orkestrel-build-application → orkestrel-build}/agents/openai.yaml +1 -1
  27. package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +8 -16
  28. package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +3 -3
  29. package/dist/host/agents/skills/orkestrel-debrief/references/retention.md +13 -13
  30. package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +61 -0
  31. package/dist/host/agents/skills/orkestrel-dispatch/agents/openai.yaml +4 -0
  32. package/dist/host/agents/skills/orkestrel-dispatch/references/bench.md +25 -0
  33. package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +32 -0
  34. package/dist/host/agents/skills/orkestrel-dispatch/scripts/bench.ts +259 -0
  35. package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +110 -0
  36. package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +115 -0
  37. package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +239 -0
  38. package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +124 -0
  39. package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +123 -0
  40. package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +129 -0
  41. package/dist/host/agents/skills/orkestrel-dispatch/scripts/sweep.ts +157 -0
  42. package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +42 -193
  43. package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +38 -108
  44. package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +35 -134
  45. package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/SKILL.md +10 -14
  46. package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/agents/openai.yaml +1 -1
  47. package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/hardening.md +3 -4
  48. package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +228 -0
  49. package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/SKILL.md +15 -23
  50. package/dist/host/agents/skills/orkestrel-journey/agents/openai.yaml +4 -0
  51. package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/captures.md +1 -1
  52. package/dist/host/agents/skills/{orkestrel-polish-surface → orkestrel-polish}/SKILL.md +25 -33
  53. package/dist/host/agents/skills/{orkestrel-polish-surface → orkestrel-polish}/agents/openai.yaml +1 -1
  54. package/dist/host/agents/skills/{orkestrel-polish-surface → orkestrel-polish}/references/capture-harness.md +3 -3
  55. package/dist/host/agents/skills/orkestrel-publish/SKILL.md +33 -20
  56. package/dist/host/agents/skills/orkestrel-publish/references/release.md +39 -0
  57. package/dist/host/agents/skills/orkestrel-publish/references/wave.md +22 -21
  58. package/dist/host/agents/skills/orkestrel-publish/references/window.md +27 -14
  59. package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +220 -0
  60. package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +114 -0
  61. package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +629 -0
  62. package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +242 -0
  63. package/dist/host/agents/skills/orkestrel-scout/SKILL.md +28 -0
  64. package/dist/host/agents/skills/orkestrel-scout/agents/openai.yaml +4 -0
  65. package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +352 -0
  66. package/dist/host/agents/templates/brief.md +21 -142
  67. package/dist/host/agents/transports/claude-cli.md +21 -0
  68. package/dist/host/agents/transports/codex.md +38 -159
  69. package/dist/host/agents/transports/cursor.md +16 -65
  70. package/dist/host/claude/AGENTS.md +38 -0
  71. package/dist/host/claude/agents/analyst.md +14 -53
  72. package/dist/host/claude/agents/astra.md +26 -0
  73. package/dist/host/claude/agents/builder.md +14 -30
  74. package/dist/host/claude/agents/checker.md +13 -57
  75. package/dist/host/claude/agents/distiller.md +11 -26
  76. package/dist/host/claude/agents/grok.md +12 -35
  77. package/dist/host/claude/agents/opus.md +14 -30
  78. package/dist/host/claude/agents/orkestrel.md +4 -4
  79. package/dist/host/claude/agents/planner.md +10 -44
  80. package/dist/host/claude/agents/researcher.md +11 -30
  81. package/dist/host/claude/agents/reviewer.md +11 -95
  82. package/dist/host/claude/agents/scout.md +9 -23
  83. package/dist/host/claude/agents/verifier.md +15 -33
  84. package/dist/host/claude/rules/documentation.md +8 -2
  85. package/dist/host/claude/rules/portability.md +7 -1
  86. package/dist/host/claude/rules/quality.md +36 -96
  87. package/dist/host/claude/rules/styles.md +3 -0
  88. package/dist/host/claude/rules/tests.md +6 -3
  89. package/dist/host/claude/rules/workspace.md +21 -15
  90. package/dist/host/claude/rules/writing.md +57 -108
  91. package/dist/host/claude/settings.json +5 -3
  92. package/dist/host/claude/skills/enterprise-bootstrap/SKILL.md +1 -1
  93. package/dist/host/claude/skills/{orkestrel-align-packages → orkestrel-align}/SKILL.md +2 -2
  94. package/dist/host/claude/skills/{orkestrel-build-application → orkestrel-build}/SKILL.md +2 -2
  95. package/dist/host/claude/skills/orkestrel-dispatch/SKILL.md +11 -0
  96. package/dist/host/claude/skills/orkestrel-falsify/SKILL.md +2 -1
  97. package/dist/host/claude/skills/{orkestrel-harden-package → orkestrel-harden}/SKILL.md +2 -2
  98. package/dist/host/claude/skills/{orkestrel-prove-journey → orkestrel-journey}/SKILL.md +2 -2
  99. package/dist/host/claude/skills/orkestrel-polish/SKILL.md +12 -0
  100. package/dist/host/claude/skills/orkestrel-scout/SKILL.md +11 -0
  101. package/dist/host/codex/agents/analyst.toml +14 -31
  102. package/dist/host/codex/agents/astra.toml +25 -0
  103. package/dist/host/codex/agents/builder.toml +13 -20
  104. package/dist/host/codex/agents/checker.toml +13 -27
  105. package/dist/host/codex/agents/distiller.toml +9 -22
  106. package/dist/host/codex/agents/grok.toml +11 -30
  107. package/dist/host/codex/agents/opus.toml +14 -22
  108. package/dist/host/codex/agents/orkestrel.toml +1 -1
  109. package/dist/host/codex/agents/planner.toml +11 -28
  110. package/dist/host/codex/agents/researcher.toml +10 -22
  111. package/dist/host/codex/agents/reviewer.toml +11 -27
  112. package/dist/host/codex/agents/scout.toml +11 -17
  113. package/dist/host/codex/agents/verifier.toml +16 -12
  114. package/dist/host/codex/config.toml +18 -21
  115. package/dist/host/cursor/mcp.json +0 -4
  116. package/dist/host/cursor/rules/orchestration.mdc +12 -20
  117. package/dist/host/dotfiles/mcp.json +0 -4
  118. package/dist/host/dotfiles/oxlintrc.json +7 -0
  119. package/dist/host/guides/probe.md +18 -14
  120. package/dist/host/guides/scaffold.md +147 -83
  121. package/dist/host/guides/test.md +442 -148
  122. package/dist/host/manifest.json +322 -185
  123. package/dist/host/scripts/codex.sh +0 -0
  124. package/dist/host/scripts/cursor.sh +0 -0
  125. package/dist/host/scripts/deps.sh +0 -0
  126. package/dist/host/scripts/ollama.sh +0 -0
  127. package/dist/host/tests/config.test.ts +86 -55
  128. package/dist/host/tests/policy.test.ts +1 -5
  129. package/dist/host/tests/setupPolicy.ts +179 -4
  130. package/dist/src/core/index.cjs +264 -89
  131. package/dist/src/core/index.cjs.map +1 -1
  132. package/dist/src/core/index.d.cts +95 -30
  133. package/dist/src/core/index.d.ts +95 -30
  134. package/dist/src/core/index.js +262 -90
  135. package/dist/src/core/index.js.map +1 -1
  136. package/dist/src/server/index.cjs +55 -9
  137. package/dist/src/server/index.cjs.map +1 -1
  138. package/dist/src/server/index.d.cts +29 -4
  139. package/dist/src/server/index.d.ts +29 -4
  140. package/dist/src/server/index.js +56 -11
  141. package/dist/src/server/index.js.map +1 -1
  142. package/package.json +16 -12
  143. package/dist/host/CLAUDE.md +0 -61
  144. package/dist/host/agents/skills/orkestrel-prove-journey/agents/openai.yaml +0 -4
  145. package/dist/host/agents/transports/claude.md +0 -49
  146. package/dist/host/claude/agents/application.md +0 -36
  147. package/dist/host/claude/agents/sol.md +0 -61
  148. package/dist/host/claude/skills/orkestrel-polish-surface/SKILL.md +0 -12
  149. package/dist/host/codex/agents/application.toml +0 -25
  150. package/dist/host/codex/agents/sol.toml +0 -19
  151. /package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/references/integration.md +0 -0
  152. /package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/centralization.md +0 -0
  153. /package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/contract.md +0 -0
  154. /package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/research.md +0 -0
  155. /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/decide.md +0 -0
  156. /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/layer.md +0 -0
  157. /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/statechart.md +0 -0
  158. /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/styles.md +0 -0
@@ -0,0 +1,629 @@
1
+ // Run one repository's release visit, or print the fleet's layer order. Run from the checkout root:
2
+ // node .agents/skills/orkestrel-publish/scripts/wave.ts --visit [--target DIR] [--offline] [--from STEP] [--to STEP] [--prior VERSION] [--dry-run] [--json] [--out FILE]
3
+ // node .agents/skills/orkestrel-publish/scripts/wave.ts --plan [--catalog PATH] [--json]
4
+ // --visit runs the visit `wave.md` § Visit a repository fixes, in order, stopping at the first step
5
+ // that fails: pin (read `scaffold audit --json`, whose drift exit is the reading the pin closes, then
6
+ // re-pin every @orkestrel range to the registry caret and install), commit (`git commit --only` of
7
+ // the manifest and lockfile as the preparation commit), overwrite (`scaffold overwrite --json`, then
8
+ // `scaffold audit` exiting 0; offline, an exit of 1 whose note names the catalog refusal is
9
+ // accepted), verify (every declared range matches the registry), install, pins (the self-pin sweep
10
+ // with --prior or the manifest version and every range the visit moved; hits are reported, not
11
+ // fatal), format, gates (format:check, lint:check, check, build, test), and compare (the rebuilt
12
+ // dist against the published tarball, and the final runtime dependency set against the published
13
+ // manifest `npm view NAME --json` serves; both readings are reported, not fatal). The summary
14
+ // carries each step's exit and duration, the bump ruling, and a Markdown row for
15
+ // `.orkestrel/release.md`. --dry-run prints the commands and runs nothing, so its ruling is
16
+ // unanswered. --plan reads the marker-bounded catalog table in `.claude/agents/orkestrel.md` and
17
+ // prints the packages by layer. Exit 0 when every step passed, 1 when a step failed, 2 when a
18
+ // reading failed, 64 on usage.
19
+ import type { SpawnSyncReturns } from 'node:child_process'
20
+ import { spawnSync } from 'node:child_process'
21
+ import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'
22
+ import { basename, dirname, extname, join } from 'node:path'
23
+ import { fileURLToPath } from 'node:url'
24
+ import {
25
+ parseJsonRecord,
26
+ readJsonObject,
27
+ readMissingFlags,
28
+ readOption,
29
+ readString,
30
+ runNpm,
31
+ } from '../../orkestrel-dispatch/scripts/helpers.ts'
32
+
33
+ const STEPS: readonly string[] = [
34
+ 'pin',
35
+ 'commit',
36
+ 'overwrite',
37
+ 'verify',
38
+ 'install',
39
+ 'pins',
40
+ 'format',
41
+ 'gates',
42
+ 'compare',
43
+ ]
44
+ const GATES: readonly string[] = ['format:check', 'lint:check', 'check', 'build', 'test']
45
+ const RUNTIME_FIELDS: readonly string[] = ['dependencies', 'peerDependencies']
46
+ const RANGE_FIELDS: readonly string[] = [...RUNTIME_FIELDS, 'devDependencies']
47
+ const CATALOG = '.claude/agents/orkestrel.md'
48
+ const OFFLINE_REFUSAL = "'catalog' does not take --offline"
49
+ const VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u
50
+ // The checkout runs the `.ts` sources and a built twin under `dist/agents` runs `.js` siblings.
51
+ const EXTENSION = extname(fileURLToPath(import.meta.url))
52
+ const COMPARE = fileURLToPath(new URL(`./compare${EXTENSION}`, import.meta.url))
53
+ const PINS = fileURLToPath(new URL(`./pins${EXTENSION}`, import.meta.url))
54
+
55
+ interface Release {
56
+ readonly name: string
57
+ readonly range: string
58
+ readonly latest: string | undefined
59
+ }
60
+
61
+ interface StepResult {
62
+ readonly step: string
63
+ readonly command: string
64
+ readonly exit: number | undefined
65
+ readonly durationMs: number
66
+ readonly note: string | undefined
67
+ }
68
+
69
+ interface Ruling {
70
+ readonly distMoved: boolean | undefined
71
+ readonly rangesMoved: boolean | undefined
72
+ readonly ranges: readonly string[] | undefined
73
+ readonly hits: number | undefined
74
+ }
75
+
76
+ interface Visit {
77
+ readonly target: string
78
+ readonly name: string | undefined
79
+ readonly version: string | undefined
80
+ readonly steps: readonly StepResult[]
81
+ readonly ruling: Ruling
82
+ readonly row: string
83
+ }
84
+
85
+ interface Runner {
86
+ readonly target: string
87
+ readonly dry: boolean
88
+ readonly offline: boolean
89
+ readonly steps: StepResult[]
90
+ }
91
+
92
+ function readManifest(target: string): Record<string, unknown> | undefined {
93
+ try {
94
+ return parseJsonRecord(readFileSync(join(target, 'package.json'), 'utf8'))
95
+ } catch {
96
+ return undefined
97
+ }
98
+ }
99
+
100
+ function readRanges(
101
+ manifest: Record<string, unknown>,
102
+ fields: readonly string[],
103
+ ): ReadonlyMap<string, string> {
104
+ const ranges = new Map<string, string>()
105
+ for (const field of fields) {
106
+ const block = manifest[field]
107
+ if (typeof block !== 'object' || block === null) continue
108
+ for (const [name, range] of Object.entries(block)) {
109
+ if (typeof range === 'string') ranges.set(`${field} ${name}`, range)
110
+ }
111
+ }
112
+ return ranges
113
+ }
114
+
115
+ function compareRanges(
116
+ published: ReadonlyMap<string, string>,
117
+ final: ReadonlyMap<string, string>,
118
+ ): readonly string[] {
119
+ const names = [...new Set([...published.keys(), ...final.keys()])].sort()
120
+ return names.flatMap((name) => {
121
+ const before = published.get(name)
122
+ const after = final.get(name)
123
+ if (before === after) return []
124
+ if (before === undefined) return [`${name} ${String(after)} added`]
125
+ if (after === undefined) return [`${name} ${before} removed`]
126
+ return [`${name} ${before} -> ${after}`]
127
+ })
128
+ }
129
+
130
+ function resolveScaffold(target: string): readonly string[] {
131
+ const installed = join(target, 'node_modules', '@orkestrel', 'scaffold', 'dist', 'bin', 'main.js')
132
+ if (existsSync(installed)) return [process.execPath, installed]
133
+ const own = join(target, 'dist', 'bin', 'main.js')
134
+ if (existsSync(own)) return [process.execPath, own]
135
+ return []
136
+ }
137
+
138
+ function readReleases(text: string): readonly Release[] | undefined {
139
+ const releases = readJsonObject(text)?.releases
140
+ if (!Array.isArray(releases)) return undefined
141
+ return releases.flatMap((entry) => {
142
+ if (typeof entry !== 'object' || entry === null) return []
143
+ const record = Object.fromEntries(Object.entries(entry))
144
+ const name = readString(record, 'name')
145
+ const range = readString(record, 'range')
146
+ if (name === undefined || range === undefined) return []
147
+ return [{ name, range, latest: readString(record, 'latest') }]
148
+ })
149
+ }
150
+
151
+ function describeCommand(file: string, args: readonly string[]): string {
152
+ return [file === process.execPath ? 'node' : file, ...args].join(' ')
153
+ }
154
+
155
+ function describeFailure(result: SpawnSyncReturns<string>): string | undefined {
156
+ if (result.status === 0) return undefined
157
+ return result.stderr
158
+ .trim()
159
+ .split(/\r\n|\n/)
160
+ .slice(-3)
161
+ .join(' | ')
162
+ }
163
+
164
+ function runStep(
165
+ runner: Runner,
166
+ step: string,
167
+ file: string,
168
+ args: readonly string[],
169
+ cwd: string,
170
+ ): SpawnSyncReturns<string> | undefined {
171
+ const command = describeCommand(file, args)
172
+ if (runner.dry) {
173
+ runner.steps.push({ step, command, exit: undefined, durationMs: 0, note: 'dry run' })
174
+ return undefined
175
+ }
176
+ const started = Date.now()
177
+ const result = spawnSync(file, args, {
178
+ cwd,
179
+ encoding: 'utf8',
180
+ windowsHide: true,
181
+ maxBuffer: 64 * 1024 * 1024,
182
+ })
183
+ runner.steps.push({
184
+ step,
185
+ command,
186
+ exit: result.status ?? undefined,
187
+ durationMs: Date.now() - started,
188
+ note: describeFailure(result),
189
+ })
190
+ return result
191
+ }
192
+
193
+ function runNpmStep(
194
+ runner: Runner,
195
+ step: string,
196
+ args: readonly string[],
197
+ ): SpawnSyncReturns<string> | undefined {
198
+ const command = `npm ${args.join(' ')}`
199
+ if (runner.dry) {
200
+ runner.steps.push({ step, command, exit: undefined, durationMs: 0, note: 'dry run' })
201
+ return undefined
202
+ }
203
+ const started = Date.now()
204
+ const result = runNpm(args, runner.target)
205
+ runner.steps.push({
206
+ step,
207
+ command,
208
+ exit: result.status ?? undefined,
209
+ durationMs: Date.now() - started,
210
+ note: describeFailure(result),
211
+ })
212
+ return result
213
+ }
214
+
215
+ function recordStep(runner: Runner, step: string, command: string, note: string): void {
216
+ runner.steps.push({ step, command, exit: runner.dry ? undefined : 0, durationMs: 0, note })
217
+ }
218
+
219
+ function replaceLastStep(runner: Runner, exit: number | undefined, note: string | undefined): void {
220
+ const last = runner.steps.pop()
221
+ if (last !== undefined) runner.steps.push({ ...last, exit, note })
222
+ }
223
+
224
+ function failed(runner: Runner): boolean {
225
+ return runner.steps.some(
226
+ (entry) =>
227
+ entry.exit !== undefined &&
228
+ entry.exit !== 0 &&
229
+ entry.step !== 'pins' &&
230
+ entry.step !== 'compare',
231
+ )
232
+ }
233
+
234
+ function writeRanges(target: string, releases: readonly Release[]): readonly string[] {
235
+ const manifest = readManifest(target)
236
+ if (manifest === undefined) return []
237
+ const moved: string[] = []
238
+ for (const field of RANGE_FIELDS) {
239
+ const block = manifest[field]
240
+ if (typeof block !== 'object' || block === null) continue
241
+ const ranges = Object.fromEntries(Object.entries(block))
242
+ for (const release of releases) {
243
+ if (release.latest === undefined || !(release.name in ranges)) continue
244
+ const caret = `^${release.latest}`
245
+ if (ranges[release.name] !== caret) {
246
+ ranges[release.name] = caret
247
+ moved.push(`${release.name} ${String(release.range)} -> ${caret}`)
248
+ }
249
+ }
250
+ manifest[field] = ranges
251
+ }
252
+ if (moved.length > 0)
253
+ writeFileSync(join(target, 'package.json'), `${JSON.stringify(manifest, null, '\t')}\n`)
254
+ return moved
255
+ }
256
+
257
+ function runPin(
258
+ runner: Runner,
259
+ scaffoldFile: string,
260
+ scaffoldPrefix: readonly string[],
261
+ offline: readonly string[],
262
+ ): void {
263
+ const audit = runStep(
264
+ runner,
265
+ 'pin',
266
+ scaffoldFile,
267
+ [...scaffoldPrefix, 'audit', '--json', ...offline],
268
+ runner.target,
269
+ )
270
+ if (audit !== undefined) {
271
+ const releases = readReleases(audit.stdout)
272
+ if (releases === undefined || (audit.status !== 0 && audit.status !== 1)) {
273
+ replaceLastStep(
274
+ runner,
275
+ audit.status === null || audit.status === 0 ? 2 : audit.status,
276
+ releases === undefined ? 'the audit printed no releases envelope' : describeFailure(audit),
277
+ )
278
+ return
279
+ }
280
+ // The audit exits 1 on the drift this step exists to close, so its exit is a reading here.
281
+ replaceLastStep(runner, 0, `${releases.length} release reading(s)`)
282
+ const moved = writeRanges(runner.target, releases)
283
+ recordStep(
284
+ runner,
285
+ 'pin',
286
+ 'rewrite @orkestrel ranges to the registry caret',
287
+ moved.length === 0 ? 'no range moved' : moved.join(', '),
288
+ )
289
+ } else {
290
+ recordStep(runner, 'pin', 'rewrite @orkestrel ranges to the registry caret', 'dry run')
291
+ }
292
+ runNpmStep(runner, 'pin', ['install'])
293
+ }
294
+
295
+ function runCommit(runner: Runner): void {
296
+ const status = runStep(
297
+ runner,
298
+ 'commit',
299
+ 'git',
300
+ ['status', '--porcelain', '--', 'package.json', 'package-lock.json'],
301
+ runner.target,
302
+ )
303
+ if (status === undefined || status.stdout.trim() !== '') {
304
+ runStep(
305
+ runner,
306
+ 'commit',
307
+ 'git',
308
+ [
309
+ 'commit',
310
+ '--only',
311
+ '-m',
312
+ 'Re-pin the @orkestrel ranges for the release visit',
313
+ '--',
314
+ 'package.json',
315
+ 'package-lock.json',
316
+ ],
317
+ runner.target,
318
+ )
319
+ } else {
320
+ recordStep(runner, 'commit', 'git commit', 'manifest and lockfile already committed')
321
+ }
322
+ }
323
+
324
+ function runOverwrite(
325
+ runner: Runner,
326
+ scaffoldFile: string,
327
+ scaffoldPrefix: readonly string[],
328
+ offline: readonly string[],
329
+ ): void {
330
+ const written = runStep(
331
+ runner,
332
+ 'overwrite',
333
+ scaffoldFile,
334
+ [...scaffoldPrefix, 'overwrite', '--json', ...offline],
335
+ runner.target,
336
+ )
337
+ if (runner.offline && written !== undefined && written.status === 1) {
338
+ const note = readString(readJsonObject(written.stdout) ?? {}, 'note')
339
+ if (note !== undefined && note.includes(OFFLINE_REFUSAL)) {
340
+ replaceLastStep(runner, 0, 'offline overwrite skipped the catalog step by design')
341
+ } else {
342
+ replaceLastStep(runner, 1, note ?? describeFailure(written))
343
+ }
344
+ }
345
+ if (!failed(runner)) {
346
+ runStep(
347
+ runner,
348
+ 'overwrite',
349
+ scaffoldFile,
350
+ [...scaffoldPrefix, 'audit', ...offline],
351
+ runner.target,
352
+ )
353
+ }
354
+ }
355
+
356
+ function runVerify(
357
+ runner: Runner,
358
+ scaffoldFile: string,
359
+ scaffoldPrefix: readonly string[],
360
+ offline: readonly string[],
361
+ ): void {
362
+ const audit = runStep(
363
+ runner,
364
+ 'verify',
365
+ scaffoldFile,
366
+ [...scaffoldPrefix, 'audit', '--json', ...offline],
367
+ runner.target,
368
+ )
369
+ if (audit === undefined) return
370
+ const releases = readReleases(audit.stdout)
371
+ if (releases === undefined) {
372
+ replaceLastStep(
373
+ runner,
374
+ audit.status === null || audit.status === 0 ? 2 : audit.status,
375
+ 'the audit printed no releases envelope',
376
+ )
377
+ return
378
+ }
379
+ const drift = releases.filter(
380
+ (release) => release.latest !== undefined && release.range !== `^${release.latest}`,
381
+ )
382
+ runner.steps.push({
383
+ step: 'verify',
384
+ command: 'compare every declared range with the registry',
385
+ exit: drift.length === 0 ? 0 : 1,
386
+ durationMs: 0,
387
+ note:
388
+ drift.length === 0
389
+ ? undefined
390
+ : drift
391
+ .map((release) => `${release.name} ${release.range} vs ${String(release.latest)}`)
392
+ .join(', '),
393
+ })
394
+ }
395
+
396
+ function runPins(
397
+ runner: Runner,
398
+ before: ReadonlyMap<string, string>,
399
+ version: string | undefined,
400
+ ): number | undefined {
401
+ const after = readRanges(readManifest(runner.target) ?? {}, RANGE_FIELDS)
402
+ const prior = [...before]
403
+ .filter(([name, range]) => after.get(name) !== range)
404
+ .flatMap(([, range]) => ['--range', range])
405
+ if (version === undefined) {
406
+ recordStep(runner, 'pins', basename(PINS), 'no prior version to sweep for; pass --prior')
407
+ return undefined
408
+ }
409
+ const swept = runStep(
410
+ runner,
411
+ 'pins',
412
+ process.execPath,
413
+ [PINS, '--version', version, ...prior],
414
+ runner.target,
415
+ )
416
+ return swept === undefined
417
+ ? undefined
418
+ : swept.stdout.split(/\r\n|\n/).filter((line) => /^\S+:\d+: /u.test(line)).length
419
+ }
420
+
421
+ function runCompare(runner: Runner, name: string | undefined): Ruling {
422
+ const compared = runStep(runner, 'compare', process.execPath, [COMPARE, '--json'], runner.target)
423
+ const distMoved =
424
+ compared === undefined
425
+ ? undefined
426
+ : compared.status === 3
427
+ ? true
428
+ : compared.status === 0
429
+ ? false
430
+ : undefined
431
+ if (name === undefined) {
432
+ recordStep(runner, 'compare', 'npm view', 'the manifest names no package to read')
433
+ return { distMoved, rangesMoved: undefined, ranges: undefined, hits: undefined }
434
+ }
435
+ const view = runNpmStep(runner, 'compare', ['view', name, '--json'])
436
+ const published =
437
+ view === undefined || view.status !== 0 ? undefined : parseJsonRecord(view.stdout)
438
+ if (published === undefined) {
439
+ return { distMoved, rangesMoved: undefined, ranges: undefined, hits: undefined }
440
+ }
441
+ const ranges = compareRanges(
442
+ readRanges(published, RUNTIME_FIELDS),
443
+ readRanges(readManifest(runner.target) ?? {}, RUNTIME_FIELDS),
444
+ )
445
+ return { distMoved, rangesMoved: ranges.length > 0, ranges, hits: undefined }
446
+ }
447
+
448
+ function describeReading(moved: boolean | undefined, movedWord: string, sameWord: string): string {
449
+ return moved === undefined ? 'unanswered' : moved ? movedWord : sameWord
450
+ }
451
+
452
+ function runVisit(argv: readonly string[]): Visit | undefined {
453
+ const target = readOption(argv, '--target') ?? process.cwd()
454
+ if (!existsSync(join(target, 'package.json'))) {
455
+ console.error(`wave: ${target} carries no package.json`)
456
+ return undefined
457
+ }
458
+ const scaffold = resolveScaffold(target)
459
+ if (scaffold.length === 0) {
460
+ console.error(`wave: no scaffold executable under ${target}; run npm install first`)
461
+ return undefined
462
+ }
463
+ const [scaffoldFile, ...scaffoldPrefix] = scaffold
464
+ if (scaffoldFile === undefined) return undefined
465
+ const runner: Runner = {
466
+ target,
467
+ dry: argv.includes('--dry-run'),
468
+ offline: argv.includes('--offline'),
469
+ steps: [],
470
+ }
471
+ const from = STEPS.indexOf(readOption(argv, '--from') ?? 'pin')
472
+ const to = STEPS.indexOf(readOption(argv, '--to') ?? 'compare')
473
+ const selected = new Set(STEPS.slice(from, to + 1))
474
+ const manifest = readManifest(target)
475
+ if (manifest === undefined) {
476
+ console.error(`wave: ${target} carries an unreadable package.json`)
477
+ return undefined
478
+ }
479
+ const name = readString(manifest, 'name')
480
+ const version = readString(manifest, 'version')
481
+ const before = readRanges(manifest, RANGE_FIELDS)
482
+ const offline = runner.offline ? ['--offline'] : []
483
+ let hits: number | undefined
484
+ let ruling: Ruling = {
485
+ distMoved: undefined,
486
+ rangesMoved: undefined,
487
+ ranges: undefined,
488
+ hits: undefined,
489
+ }
490
+ if (selected.has('pin')) runPin(runner, scaffoldFile, scaffoldPrefix, offline)
491
+ if (selected.has('commit') && !failed(runner)) runCommit(runner)
492
+ if (selected.has('overwrite') && !failed(runner)) {
493
+ runOverwrite(runner, scaffoldFile, scaffoldPrefix, offline)
494
+ }
495
+ if (selected.has('verify') && !failed(runner)) {
496
+ runVerify(runner, scaffoldFile, scaffoldPrefix, offline)
497
+ }
498
+ if (selected.has('install') && !failed(runner)) runNpmStep(runner, 'install', ['install'])
499
+ if (selected.has('pins') && !failed(runner)) {
500
+ hits = runPins(runner, before, readOption(argv, '--prior') ?? version)
501
+ }
502
+ if (selected.has('format') && !failed(runner)) runNpmStep(runner, 'format', ['run', 'format'])
503
+ if (selected.has('gates') && !failed(runner)) {
504
+ for (const gate of GATES) {
505
+ if (failed(runner)) break
506
+ runNpmStep(runner, 'gates', ['run', gate])
507
+ }
508
+ }
509
+ if (selected.has('compare') && !failed(runner)) ruling = runCompare(runner, name)
510
+ ruling = { ...ruling, hits }
511
+ const bump =
512
+ ruling.distMoved === true || ruling.rangesMoved === true
513
+ ? 'bump'
514
+ : ruling.distMoved === undefined || ruling.rangesMoved === undefined
515
+ ? 'unanswered'
516
+ : 'no bump'
517
+ const reading = `dist ${describeReading(ruling.distMoved, 'moved', 'same')}, ranges ${describeReading(ruling.rangesMoved, 'moved', 'same')}${hits === undefined ? '' : `, ${hits} self-pin hit(s)`}`
518
+ return {
519
+ target,
520
+ name,
521
+ version,
522
+ steps: runner.steps,
523
+ ruling,
524
+ row: `| ${name ?? target} | ${version ?? '?'} | ${bump} | ${reading} | round |`,
525
+ }
526
+ }
527
+
528
+ function readLayers(catalogPath: string): ReadonlyMap<string, readonly string[]> | undefined {
529
+ if (!existsSync(catalogPath)) return undefined
530
+ const text = readFileSync(catalogPath, 'utf8')
531
+ const start = text.indexOf('<!-- orkestrel:catalog -->')
532
+ const end = text.indexOf('<!-- /orkestrel:catalog -->')
533
+ if (start === -1 || end === -1) return undefined
534
+ const layers = new Map<string, string[]>()
535
+ for (const line of text.slice(start, end).split(/\r\n|\n/)) {
536
+ const match = line.match(
537
+ /^\|\s*`(@orkestrel\/[a-z0-9-]+)`\s*\|\s*`([^`]+)`\s*\|\s*(L\d+)\s*\|/u,
538
+ )
539
+ if (match?.[1] === undefined || match[3] === undefined) continue
540
+ const members = layers.get(match[3]) ?? []
541
+ members.push(`${match[1]} ${match[2] ?? ''}`.trim())
542
+ layers.set(match[3], members)
543
+ }
544
+ return new Map(
545
+ [...layers.entries()].sort(([left], [right]) => Number(left.slice(1)) - Number(right.slice(1))),
546
+ )
547
+ }
548
+
549
+ function renderVisit(visit: Visit): string {
550
+ const lines = visit.steps.map(
551
+ (entry) =>
552
+ `wave: ${entry.step.padEnd(9)} exit=${entry.exit === undefined ? 'dry' : String(entry.exit)} ${String(entry.durationMs).padStart(7)} ms ${entry.command}${entry.note === undefined ? '' : ` (${entry.note})`}`,
553
+ )
554
+ lines.push(
555
+ `wave: ruling dist ${describeReading(visit.ruling.distMoved, 'moved', 'same')}, ranges ${describeReading(visit.ruling.rangesMoved, 'moved', 'same')}${visit.ruling.hits === undefined ? '' : `, ${visit.ruling.hits} self-pin hit(s)`}`,
556
+ )
557
+ for (const range of visit.ruling.ranges ?? []) lines.push(`wave: range ${range}`)
558
+ lines.push(visit.row)
559
+ return `${lines.join('\n')}\n`
560
+ }
561
+
562
+ function main(argv: readonly string[]): number {
563
+ const missing = readMissingFlags(argv, [
564
+ '--target',
565
+ '--from',
566
+ '--to',
567
+ '--prior',
568
+ '--catalog',
569
+ '--out',
570
+ ])
571
+ if (missing.length > 0) {
572
+ console.error(`wave: ${missing.join(', ')} given with no value`)
573
+ return 64
574
+ }
575
+ if (argv.includes('--plan') && argv.includes('--visit')) {
576
+ console.error('wave: --plan and --visit are two modes; name one')
577
+ return 64
578
+ }
579
+ if (argv.includes('--plan')) {
580
+ const layers = readLayers(readOption(argv, '--catalog') ?? CATALOG)
581
+ if (layers === undefined) {
582
+ console.error('wave: no marker-bounded catalog table; run scaffold catalog first')
583
+ return 2
584
+ }
585
+ if (argv.includes('--json')) console.log(JSON.stringify(Object.fromEntries(layers)))
586
+ else for (const [layer, members] of layers) console.log(`${layer}\t${members.join(', ')}`)
587
+ return 0
588
+ }
589
+ if (!argv.includes('--visit')) {
590
+ console.error(
591
+ 'usage: wave.ts --visit [--target DIR] [--offline] [--from STEP] [--to STEP] [--prior VERSION] [--dry-run] [--json] [--out FILE] | --plan [--catalog PATH] [--json]',
592
+ )
593
+ return 64
594
+ }
595
+ const from = STEPS.indexOf(readOption(argv, '--from') ?? 'pin')
596
+ const to = STEPS.indexOf(readOption(argv, '--to') ?? 'compare')
597
+ if (from === -1 || to === -1 || to < from) {
598
+ console.error(`wave: --from and --to name steps in order from: ${STEPS.join(', ')}`)
599
+ return 64
600
+ }
601
+ const prior = readOption(argv, '--prior')
602
+ if (prior !== undefined && !VERSION.test(prior)) {
603
+ console.error(
604
+ 'wave: --prior takes the version the package carried before its bump, MAJOR.MINOR.PATCH',
605
+ )
606
+ return 64
607
+ }
608
+ const visit = runVisit(argv)
609
+ if (visit === undefined) return 2
610
+ const output = argv.includes('--json') ? `${JSON.stringify(visit)}\n` : renderVisit(visit)
611
+ const out = readOption(argv, '--out')
612
+ if (out === undefined) process.stdout.write(output)
613
+ else {
614
+ mkdirSync(dirname(out), { recursive: true })
615
+ writeFileSync(out, output)
616
+ console.log(`wave: wrote ${out}`)
617
+ }
618
+ return visit.steps.some(
619
+ (entry) =>
620
+ entry.exit !== undefined &&
621
+ entry.exit !== 0 &&
622
+ entry.step !== 'pins' &&
623
+ entry.step !== 'compare',
624
+ )
625
+ ? 1
626
+ : 0
627
+ }
628
+
629
+ process.exitCode = main(process.argv.slice(2))