@orkestrel/scaffold 0.0.103 → 0.0.104

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 (88) hide show
  1. package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +84 -1
  2. package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +21 -6
  3. package/dist/agents/skills/orkestrel-harden/scripts/titles.js +338 -0
  4. package/dist/agents/skills/orkestrel-publish/scripts/swap.js +3 -39
  5. package/dist/agents/skills/orkestrel-publish/scripts/wave.js +433 -47
  6. package/dist/agents/skills/orkestrel-publish/scripts/window.js +2 -1
  7. package/dist/bin/main.js +16 -9
  8. package/dist/bin/main.js.map +1 -1
  9. package/dist/host/agents/orchestration.md +3 -3
  10. package/dist/host/agents/skills/enterprise-bootstrap/references/components.md +2 -2
  11. package/dist/host/agents/skills/enterprise-bootstrap/references/inspection.md +1 -1
  12. package/dist/host/agents/skills/orkestrel-build/SKILL.md +2 -2
  13. package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +2 -2
  14. package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +1 -1
  15. package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +3 -3
  16. package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +10 -10
  17. package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +1 -0
  18. package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +100 -1
  19. package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +27 -6
  20. package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +4 -4
  21. package/dist/host/agents/skills/orkestrel-falsify/agents/openai.yaml +1 -1
  22. package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +8 -7
  23. package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +6 -6
  24. package/dist/host/agents/skills/orkestrel-harden/SKILL.md +6 -1
  25. package/dist/host/agents/skills/orkestrel-harden/references/bindings.md +1700 -0
  26. package/dist/host/agents/skills/orkestrel-harden/references/contract.md +2 -2
  27. package/dist/host/agents/skills/orkestrel-harden/references/equivalence.md +48 -0
  28. package/dist/host/agents/skills/orkestrel-harden/references/hardening.md +4 -2
  29. package/dist/host/agents/skills/orkestrel-harden/scripts/titles.ts +425 -0
  30. package/dist/host/agents/skills/orkestrel-journey/SKILL.md +2 -2
  31. package/dist/host/agents/skills/orkestrel-journey/references/captures.md +9 -8
  32. package/dist/host/agents/skills/orkestrel-polish/SKILL.md +3 -3
  33. package/dist/host/agents/skills/orkestrel-publish/SKILL.md +2 -2
  34. package/dist/host/agents/skills/orkestrel-publish/references/release.md +1 -0
  35. package/dist/host/agents/skills/orkestrel-publish/references/wave.md +56 -13
  36. package/dist/host/agents/skills/orkestrel-publish/references/window.md +6 -6
  37. package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +5 -49
  38. package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +646 -49
  39. package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +2 -1
  40. package/dist/host/agents/transports/codex.md +1 -1
  41. package/dist/host/claude/agents/orkestrel.md +54 -53
  42. package/dist/host/claude/agents/reviewer.md +4 -4
  43. package/dist/host/claude/rules/documentation.md +2 -1
  44. package/dist/host/claude/rules/patterns.md +1 -1
  45. package/dist/host/claude/rules/quality.md +2 -2
  46. package/dist/host/claude/rules/styles.md +1 -1
  47. package/dist/host/claude/rules/tests.md +34 -8
  48. package/dist/host/claude/rules/typescript.md +29 -0
  49. package/dist/host/claude/rules/workspace.md +32 -22
  50. package/dist/host/claude/rules/writing.md +34 -26
  51. package/dist/host/claude/settings.json +2 -0
  52. package/dist/host/claude/skills/orkestrel-debrief/SKILL.md +1 -1
  53. package/dist/host/claude/skills/orkestrel-falsify/SKILL.md +1 -1
  54. package/dist/host/codex/agents/analyst.toml +3 -3
  55. package/dist/host/codex/agents/reviewer.toml +4 -4
  56. package/dist/host/configs/policy.ts +13 -2
  57. package/dist/host/dotfiles/editorconfig +0 -1
  58. package/dist/host/dotfiles/gitattributes +0 -1
  59. package/dist/host/dotfiles/gitignore +0 -1
  60. package/dist/host/dotfiles/oxlintrc.json +24 -0
  61. package/dist/host/guides/agent.md +1638 -598
  62. package/dist/host/guides/budget.md +89 -29
  63. package/dist/host/guides/form.md +477 -91
  64. package/dist/host/guides/mcp.md +14 -2
  65. package/dist/host/guides/ollama.md +97 -73
  66. package/dist/host/guides/probe.md +1 -1
  67. package/dist/host/guides/scaffold.md +158 -101
  68. package/dist/host/guides/study.md +2635 -0
  69. package/dist/host/guides/table.md +225 -15
  70. package/dist/host/guides/terminal.md +22 -20
  71. package/dist/host/guides/test.md +689 -255
  72. package/dist/host/manifest.json +87 -63
  73. package/dist/host/tests/config.test.ts +136 -82
  74. package/dist/host/tests/policy.test.ts +39 -1
  75. package/dist/host/tests/setupPolicy.ts +86 -28
  76. package/dist/src/core/index.cjs +1387 -611
  77. package/dist/src/core/index.cjs.map +1 -1
  78. package/dist/src/core/index.d.cts +35 -20
  79. package/dist/src/core/index.d.ts +35 -20
  80. package/dist/src/core/index.js +1385 -612
  81. package/dist/src/core/index.js.map +1 -1
  82. package/dist/src/server/index.cjs +12 -12
  83. package/dist/src/server/index.cjs.map +1 -1
  84. package/dist/src/server/index.d.cts +12 -12
  85. package/dist/src/server/index.d.ts +12 -12
  86. package/dist/src/server/index.js +12 -12
  87. package/dist/src/server/index.js.map +1 -1
  88. package/package.json +4 -4
@@ -21,7 +21,7 @@ Inventory local code involving:
21
21
  - JSON parsing and narrowing;
22
22
  - `Result`, success/failure outcomes, and safe exception capture;
23
23
  - contract shapes, compiled guards/parsers/schemas/generators/reporters;
24
- - finite/integer checks, error narrowing, and hostile-boundary containment.
24
+ - finite/integer checks, error narrowing, and untrusted-boundary containment.
25
25
 
26
26
  For each local symbol, record:
27
27
 
@@ -46,7 +46,7 @@ Do not force contract abstractions into plain control flow where they obscure be
46
46
 
47
47
  Test every adopted boundary for relevant invariants:
48
48
 
49
- - guards are total for adversarial values and hostile prototypes;
49
+ - guards are total for any value, including revoked proxies, throwing accessors, and exotic prototypes;
50
50
  - parsed values satisfy the paired guard;
51
51
  - guard-valid values retain identity when promised;
52
52
  - invalid or ambiguous values fail in the documented form;
@@ -0,0 +1,48 @@
1
+ # Equivalence of a rewrite
2
+
3
+ Judge a structural rewrite that claims unchanged behavior, such as a sweep under
4
+ `.claude/rules/typescript.md` § Bindings, a centralization move, or a nested-function extraction, by
5
+ this contract against its baseline revision. Prove it with a differential under
6
+ `.claude/rules/quality.md` § Instruments that runs the baseline and the rewrite on the same inputs,
7
+ with a row for each class this contract names that the changed code admits.
8
+
9
+ ## Hold the contract
10
+
11
+ - Treat every value the declared parameter types admit as inside the contract: holes, entries read
12
+ through getters (their count and order), entries that throw, an array whose `Symbol.iterator` is
13
+ missing or replaced, and an iterator's `return()`.
14
+ - Match the outcome (the return value, or the thrown error's constructor), the element reads (which
15
+ indices, in what order, how often), and every effect before a throw. Let a message differ only
16
+ where the throwing expression differs, and name each such row.
17
+ - For an entry whose own getters are pure, returning the same value on every read with no side
18
+ effect, match the outcome. For an entry that throws, match the thrown constructor. Leave the count
19
+ and order of an entry's own property reads outside the contract.
20
+ - Leave outside the contract a value outside the declared types, such as a number or a plain
21
+ array-like where an array is declared; a `Proxy` trap other than element `get` and
22
+ `Symbol.iterator`; the order of a record's own property reads when each is read as often as before
23
+ and no outcome changes; and tampering with a global prototype.
24
+
25
+ ## Keep the iterator protocol
26
+
27
+ When a rewrite replaces a `for…of` over a caller's array, refuse each form that changes what it
28
+ reads:
29
+
30
+ - `Array.from(source, mapper)` falls back to index access when `Symbol.iterator` is missing, so it
31
+ returns where `for…of` threw a `TypeError`.
32
+ - `source.values()` and the array methods read by index, so they accept a missing iterator and
33
+ bypass a replaced or closing one; `flatMap`, `filter`, `reduce`, `forEach`, `some`, and `every`
34
+ skip a hole that `for…of` reads.
35
+ - A spread copies every element first, so it reads past a hole or ahead of a throwing entry where
36
+ the baseline threw at the earlier element.
37
+
38
+ Consume the source through `Iterator.from(source)` and its helpers, or through a generator whose
39
+ body is the baseline `for…of`. To keep `Array.from`, hand it an iterable that forwards only
40
+ `[Symbol.iterator]()`. Never bound a hand-written stepper by `length`: a replaced iterator that
41
+ yields more entries than `length` is truncated.
42
+
43
+ ## Keep every test
44
+
45
+ Before a claim that a rewrite lost no test, run
46
+ `node .agents/skills/orkestrel-harden/scripts/titles.ts --base REV`, where `REV` is the baseline
47
+ revision. Restore each missing title, or rule its removal with the behavior it proved, and confirm
48
+ each moved title by reading the registration that uses it.
@@ -10,7 +10,7 @@ List every boundary where assumptions can fail:
10
10
  - partial work, abort, timeout, retry, pause/resume, stop/destroy, and cleanup;
11
11
  - concurrency, reentrancy, ordering, idempotency, fairness, and backpressure;
12
12
  - filesystem, process, socket, protocol, browser, model, and network boundaries;
13
- - hostile strings, paths, headers, frames, JSON, prototypes, getters, and proxies;
13
+ - malformed or unexpected strings, paths, headers, frames, JSON, prototypes, getters, and proxies;
14
14
  - resource ownership, leaks, teardown, and pressure;
15
15
  - environment isolation and unsupported-host behavior;
16
16
  - serialization, restore, public exports, and consumer ergonomics.
@@ -78,7 +78,7 @@ For every fetch, write, delete, extraction, path, protocol, or authentication bo
78
78
  - fail closed when an allowlist or target cannot be established;
79
79
  - avoid logging secrets or untrusted full payloads;
80
80
  - make destructive scope explicit and recoverable where practical;
81
- - prove malformed, partial, oversized, and adversarial input behavior.
81
+ - prove malformed, partial, oversized, and unexpected input behavior.
82
82
 
83
83
  ## Audit the tests themselves
84
84
 
@@ -92,6 +92,8 @@ Run `node .agents/skills/orkestrel-harden/scripts/discovery.ts` for the discover
92
92
  - helpers do not reimplement production logic;
93
93
  - real cleanup runs even after assertion or setup failure.
94
94
 
95
+ Collection evaluates every collected project's `setupFiles`, so the census runs each setup module's load-time work, and `--projects` narrows the report, never that listing. When a setup module contacts a live service at load, such as a `service` project's readiness gate in `tests/setupService.ts`, run the census only while an instance the run may contact is up and the service's address points at it; with the address on a closed port, that project's listing fails and the census exits 2 naming its gate.
96
+
95
97
  ## Inspect generated outputs
96
98
 
97
99
  - build all supported environments;
@@ -0,0 +1,425 @@
1
+ // Run from the checkout root:
2
+ // node .agents/skills/orkestrel-harden/scripts/titles.ts --base REV [--root DIR] [--json] [--out FILE]
3
+ // Compare exact decoded describe/it/test title counts in tests/**/*.test.ts against REV. Report
4
+ // deficits with their baseline files, or as moved when another tests/**/*.ts string literal holds
5
+ // the title. Confirm moved registrations by reading their holder. Decode quote and backslash
6
+ // escapes only. Balance each-table parentheses over code tokens, skipping lexical regions.
7
+ // Print text by default or one JSON line with --json; --out writes .json or .txt independently.
8
+ // Compare interpolated templates by full raw spelling. Refuse unclosed lexical tokens.
9
+ // Track lexical brace context without a syntax tree. Function-expression and arrow bodies cannot
10
+ // establish a following slash's grammar; refuse that detectable ambiguity instead of claiming a move.
11
+ // Exit 0 with no missing titles, 3 with missing titles, 2 on git, filesystem or token refusal, 64 on usage.
12
+ import { spawnSync } from 'node:child_process'
13
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
14
+ import { dirname, extname, join, relative, resolve } from 'node:path'
15
+ import {
16
+ listFiles,
17
+ normalizeSlashes,
18
+ readMissingFlags,
19
+ readOption,
20
+ } from '../../orkestrel-dispatch/scripts/helpers.ts'
21
+
22
+ interface Title {
23
+ readonly file: string
24
+ readonly title: string
25
+ }
26
+
27
+ interface Missing extends Title {
28
+ readonly deficit: number
29
+ }
30
+
31
+ interface Moved extends Title {
32
+ readonly holder: string
33
+ }
34
+
35
+ interface Report {
36
+ readonly base: string
37
+ readonly root: string
38
+ readonly count: number
39
+ readonly missing: readonly Missing[]
40
+ readonly moved: readonly Moved[]
41
+ }
42
+
43
+ interface Token {
44
+ readonly category: 'code' | 'line' | 'block' | 'string' | 'template' | 'regex'
45
+ readonly start: number
46
+ readonly end: number
47
+ readonly children: readonly Token[]
48
+ readonly prefix?: boolean
49
+ readonly ambiguous?: boolean
50
+ }
51
+
52
+ interface Brace {
53
+ readonly category: 'block' | 'expression' | 'template'
54
+ readonly ambiguous: boolean
55
+ }
56
+
57
+ interface Parenthesis {
58
+ readonly prefix: boolean
59
+ readonly ambiguous: boolean
60
+ }
61
+
62
+ const TITLE_PATTERN = /^\s*(?:it|test|describe)(?:\.\w+)*\(/gmu
63
+ const REGEX_PREFIX =
64
+ /^(?:return|throw|case|delete|void|typeof|instanceof|in|of|yield|await|else|do)$/u
65
+
66
+ function decodeTitle(text: string, token: Token): string {
67
+ const raw = text.slice(token.start + 1, token.end - 1)
68
+ return token.category === 'template' && token.children.length > 0
69
+ ? raw
70
+ : raw.replace(/\\(['"`\\])/gu, '$1')
71
+ }
72
+
73
+ class TokenScanner {
74
+ readonly #text: string
75
+ readonly #file: string
76
+ readonly #parentheses: Parenthesis[] = []
77
+ readonly #braces: Brace[] = []
78
+ #position = 0
79
+
80
+ constructor(text: string, file: string) {
81
+ this.#text = text
82
+ this.#file = file
83
+ }
84
+
85
+ scan(): readonly Token[] {
86
+ return this.#readCode(false, 0)
87
+ }
88
+
89
+ #readCode(expression: boolean, depth: number): readonly Token[] {
90
+ if (depth > 256) throw new Error(`${this.#file}: token nesting exceeds 256`)
91
+ const tokens: Token[] = []
92
+ while (this.#position < this.#text.length) {
93
+ const start = this.#position
94
+ const rest = this.#text.slice(start)
95
+ const character = this.#text[start]
96
+ const previous = tokens.findLast(
97
+ (token) =>
98
+ token.category !== 'line' &&
99
+ token.category !== 'block' &&
100
+ this.#text.slice(token.start, token.end).trim().length > 0,
101
+ )
102
+ const spelling = previous === undefined ? '' : this.#text.slice(previous.start, previous.end)
103
+ if (character === '}' && expression) {
104
+ this.#position += 1
105
+ return tokens
106
+ }
107
+ if (character === '{') {
108
+ const block =
109
+ previous === undefined
110
+ ? this.#braces.at(-1)?.category !== 'template'
111
+ : previous.category === 'code' &&
112
+ /^(?:\)|=>|;|\{|\}|else|try|finally|do)$/u.test(spelling)
113
+ this.#braces.push({
114
+ category: block ? 'block' : 'expression',
115
+ ambiguous: block && (spelling === '=>' || previous?.ambiguous === true),
116
+ })
117
+ this.#position += 1
118
+ tokens.push({ category: 'code', start, end: this.#position, children: [] })
119
+ tokens.push(...this.#readCode(true, depth + 1))
120
+ const brace = this.#braces.pop()
121
+ tokens.push({
122
+ category: 'code',
123
+ start: this.#position - 1,
124
+ end: this.#position,
125
+ children: [],
126
+ prefix: brace?.category === 'block',
127
+ ambiguous: brace?.ambiguous === true,
128
+ })
129
+ continue
130
+ }
131
+ if (character === '`') {
132
+ tokens.push(this.#readTemplate(depth + 1))
133
+ continue
134
+ }
135
+ if (
136
+ character === '/' &&
137
+ !rest.startsWith('//') &&
138
+ !rest.startsWith('/*') &&
139
+ previous?.ambiguous === true
140
+ )
141
+ throw new Error(`${this.#file}:${start}: Ambiguous brace before slash`)
142
+ const regex =
143
+ character === '/' &&
144
+ !rest.startsWith('//') &&
145
+ !rest.startsWith('/*') &&
146
+ (previous === undefined ||
147
+ previous.prefix === true ||
148
+ (previous.category === 'code' &&
149
+ (REGEX_PREFIX.test(spelling) ||
150
+ !/^(?:[\p{ID_Continue}$]+|[\])}]|\+\+|--)$/u.test(spelling))))
151
+ const category = rest.startsWith('//')
152
+ ? 'line'
153
+ : rest.startsWith('/*')
154
+ ? 'block'
155
+ : character === "'" || character === '"'
156
+ ? 'string'
157
+ : regex
158
+ ? 'regex'
159
+ : 'code'
160
+ const match =
161
+ category === 'line'
162
+ ? /^\/\/[^\r\n]*/u.exec(rest)
163
+ : category === 'block'
164
+ ? /^\/\*[\s\S]*?\*\//u.exec(rest)
165
+ : category === 'string'
166
+ ? (character === "'"
167
+ ? /^'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n\u2028\u2029])*'/u
168
+ : /^"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n\u2028\u2029])*"/u
169
+ ).exec(rest)
170
+ : category === 'regex'
171
+ ? /^\/(?:\\[^\r\n]|\[(?:\\[^\r\n]|[^\]\\\r\n])*\]|[^/\\[\r\n])*\/[a-z]*/iu.exec(
172
+ rest,
173
+ )
174
+ : /^(?:[\p{ID_Start}_$][\p{ID_Continue}$]*|\d[\w.]*|\s+|\+\+|--|=>|.)/u.exec(rest)
175
+ if (match === null) throw new Error(`${this.#file}:${start}: Unclosed ${category} token`)
176
+ this.#position += match[0].length
177
+ if (category === 'code' && character === '(') {
178
+ const words = tokens
179
+ .filter((token) => token.category === 'code')
180
+ .map((token) => this.#text.slice(token.start, token.end))
181
+ .filter((word) => word.trim().length > 0)
182
+ const keyword = words.findLastIndex((word) => word === 'function')
183
+ const callable =
184
+ keyword >= 0 &&
185
+ words
186
+ .slice(keyword + 1)
187
+ .every((word) => word === '*' || /^[\p{ID_Start}_$][\p{ID_Continue}$]*$/u.test(word))
188
+ const before = words[words[keyword - 1] === 'async' ? keyword - 2 : keyword - 1]
189
+ this.#parentheses.push({
190
+ prefix: /^(?:if|while|for|with|switch|catch)$/u.test(spelling),
191
+ ambiguous:
192
+ callable &&
193
+ (before === undefined
194
+ ? this.#braces.at(-1)?.category === 'template'
195
+ : /^(?:[=(,:[?+\-*/%&|^!~<>]|=>|return|throw|yield|await|case|new|void|typeof|delete|instanceof|in|of)$/u.test(
196
+ before,
197
+ )),
198
+ })
199
+ }
200
+ const parenthesis =
201
+ category === 'code' && character === ')' ? this.#parentheses.pop() : undefined
202
+ tokens.push({
203
+ category,
204
+ start,
205
+ end: this.#position,
206
+ children: [],
207
+ prefix: parenthesis?.prefix === true,
208
+ ambiguous: parenthesis?.ambiguous === true,
209
+ })
210
+ }
211
+ if (expression)
212
+ throw new Error(`${this.#file}:${this.#position}: Unclosed template expression or brace`)
213
+ return tokens
214
+ }
215
+
216
+ #readTemplate(depth: number): Token {
217
+ const start = this.#position
218
+ const children: Token[] = []
219
+ this.#position += 1
220
+ while (this.#position < this.#text.length) {
221
+ if (this.#text[this.#position] === '\\') {
222
+ this.#position += 2
223
+ } else if (this.#text[this.#position] === '`') {
224
+ this.#position += 1
225
+ return { category: 'template', start, end: this.#position, children }
226
+ } else if (this.#text.startsWith('${', this.#position)) {
227
+ const opening = this.#position
228
+ this.#position += 2
229
+ this.#braces.push({ category: 'template', ambiguous: false })
230
+ const expression = this.#readCode(true, depth)
231
+ this.#braces.pop()
232
+ children.push({
233
+ category: 'code',
234
+ start: opening,
235
+ end: this.#position,
236
+ children: expression,
237
+ })
238
+ } else {
239
+ this.#position += 1
240
+ }
241
+ }
242
+ throw new Error(`${this.#file}:${start}: Unclosed template token`)
243
+ }
244
+ }
245
+
246
+ function readArguments(
247
+ text: string,
248
+ tokens: IterableIterator<Token>,
249
+ location: string,
250
+ depth: number,
251
+ ): number {
252
+ if (depth > 256) throw new Error(`${location}: each argument nesting exceeds 256`)
253
+ for (const token of tokens) {
254
+ if (token.category !== 'code') continue
255
+ const spelling = text.slice(token.start, token.end)
256
+ if (spelling === '(') readArguments(text, tokens, location, depth + 1)
257
+ else if (spelling === ')') return token.end
258
+ }
259
+ throw new Error(`${location}: Unbalanced each arguments`)
260
+ }
261
+
262
+ function readRegistrations(text: string, tokens: readonly Token[], file: string): readonly Token[] {
263
+ const code = tokens
264
+ .map((token) => {
265
+ const raw = text.slice(token.start, token.end)
266
+ if (token.category === 'code') return raw
267
+ const blank = raw
268
+ .split('')
269
+ .map((character) => (/[\r\n]/u.test(character) ? character : ' '))
270
+ .join('')
271
+ return token.category === 'string' || token.category === 'template'
272
+ ? `\0${blank.slice(1)}`
273
+ : blank
274
+ })
275
+ .join('')
276
+ const offset = tokens[0]?.start ?? 0
277
+ const positions = new Map(tokens.map((token, index) => [token.start, index]))
278
+ const starts = new Set(
279
+ Array.from(code.matchAll(TITLE_PATTERN)).flatMap((match) => {
280
+ const opening = offset + match.index + match[0].length - 1
281
+ const direct = /^\s*\0/u.exec(code.slice(opening + 1 - offset))
282
+ if (!/\.each\(/u.test(match[0]) && direct !== null) return [opening + direct[0].length]
283
+ const index = positions.get(opening)
284
+ if (index === undefined) return []
285
+ const line = text.slice(0, opening).split(/\r\n|\n/u).length
286
+ const end = readArguments(text, tokens.slice(index + 1).values(), `${file}:${line}`, 0)
287
+ const title = /^\s*\(\s*\0/u.exec(code.slice(end - offset))
288
+ return title === null ? [] : [end + title[0].length - 1]
289
+ }),
290
+ )
291
+ return [
292
+ ...tokens.filter((token) => starts.has(token.start)),
293
+ ...tokens.flatMap((token) => readRegistrations(text, token.children, file)),
294
+ ]
295
+ }
296
+
297
+ function collectLiterals(tokens: readonly Token[]): readonly Token[] {
298
+ return tokens.flatMap((token) => [
299
+ ...(token.category === 'string' || token.category === 'template' ? [token] : []),
300
+ ...collectLiterals(token.children),
301
+ ])
302
+ }
303
+
304
+ function readGit(root: string, args: readonly string[]): string {
305
+ const result = spawnSync('git', ['-C', root, ...args], {
306
+ encoding: 'utf8',
307
+ windowsHide: true,
308
+ maxBuffer: 256 * 1024 * 1024,
309
+ })
310
+ if (result.error !== undefined || result.status !== 0) {
311
+ throw new Error(`git: ${result.error?.message ?? result.stderr.trim()}`)
312
+ }
313
+ return result.stdout
314
+ }
315
+
316
+ function readReport(base: string, root: string): Report {
317
+ const baseline = readGit(root, ['ls-tree', '-r', '-z', '--name-only', base, '--', 'tests'])
318
+ .split('\0')
319
+ .filter((file) => file.endsWith('.test.ts'))
320
+ .flatMap((file) => {
321
+ const text = readGit(root, ['show', `${base}:${file}`])
322
+ return readRegistrations(
323
+ text,
324
+ new TokenScanner(text, `${base}:${file}`).scan(),
325
+ `${base}:${file}`,
326
+ ).map((token) => ({
327
+ file,
328
+ title: decodeTitle(text, token),
329
+ }))
330
+ })
331
+ const directory = join(root, 'tests')
332
+ const files = (existsSync(directory) ? listFiles(directory) : [])
333
+ .filter(({ path }) => path.endsWith('.ts'))
334
+ .map(({ path }) => {
335
+ const file = normalizeSlashes(relative(root, path))
336
+ const text = readFileSync(path, 'utf8')
337
+ const tokens = new TokenScanner(text, file).scan()
338
+ return {
339
+ file,
340
+ text,
341
+ tokens,
342
+ registrations: file.endsWith('.test.ts') ? readRegistrations(text, tokens, file) : [],
343
+ }
344
+ })
345
+ .sort((left, right) => left.file.localeCompare(right.file))
346
+ const titles = files
347
+ .filter(({ file }) => file.endsWith('.test.ts'))
348
+ .flatMap(({ file, text, registrations }) =>
349
+ registrations.map((token) => ({ file, title: decodeTitle(text, token) })),
350
+ )
351
+ // A surviving registration supplies its title count, never evidence that a lost duplicate moved.
352
+ const holders = files.flatMap(({ file, text, tokens, registrations }) =>
353
+ collectLiterals(tokens)
354
+ .filter((token) => !registrations.includes(token))
355
+ .map((token) => ({ file, title: decodeTitle(text, token) })),
356
+ )
357
+ const deficits = [...Map.groupBy(baseline, ({ title }) => title)].flatMap(([title, entries]) => {
358
+ const survivors = titles.filter((candidate) => candidate.title === title)
359
+ const unmatched = [...Map.groupBy(entries, ({ file }) => file)].flatMap(([file, local]) =>
360
+ local.slice(survivors.filter((candidate) => candidate.file === file).length),
361
+ )
362
+ const surplus = [...Map.groupBy(survivors, ({ file }) => file)].flatMap(([file, local]) =>
363
+ local.slice(entries.filter((candidate) => candidate.file === file).length),
364
+ )
365
+ return [...Map.groupBy(unmatched.slice(surplus.length), ({ file }) => file)].map(
366
+ ([file, lost]) => ({
367
+ file,
368
+ title,
369
+ deficit: lost.length,
370
+ }),
371
+ )
372
+ })
373
+ const classified = deficits.map((entry) => ({
374
+ entry,
375
+ holder: holders.find(({ title }) => title === entry.title)?.file,
376
+ }))
377
+ return {
378
+ base,
379
+ root: normalizeSlashes(root),
380
+ count: baseline.length,
381
+ missing: classified.flatMap(({ entry, holder }) => (holder === undefined ? [entry] : [])),
382
+ moved: classified.flatMap(({ entry, holder }) =>
383
+ holder === undefined ? [] : [{ file: entry.file, title: entry.title, holder }],
384
+ ),
385
+ }
386
+ }
387
+
388
+ function renderReport(report: Report): string {
389
+ return [
390
+ `titles: ${report.count} baseline title(s) at ${report.base}, ${report.missing.length} missing, ${report.moved.length} moved`,
391
+ ...report.missing.map(({ file, title, deficit }) => `missing\t${file}\t${title}\t${deficit}`),
392
+ ...report.moved.map(({ file, title, holder }) => `moved\t${file}\t${title}\t${holder}`),
393
+ ].join('\n')
394
+ }
395
+
396
+ function main(argv: readonly string[]): number {
397
+ const missing = readMissingFlags(argv, ['--base', '--root', '--out'])
398
+ if (missing.length > 0) {
399
+ console.error(`titles: ${missing.join(', ')} given with no value`)
400
+ return 64
401
+ }
402
+ const base = readOption(argv, '--base')
403
+ const out = readOption(argv, '--out')
404
+ if (base === undefined || (out !== undefined && !['.json', '.txt'].includes(extname(out)))) {
405
+ console.error('usage: titles.ts --base REV [--root DIR] [--json] [--out FILE.json|FILE.txt]')
406
+ return 64
407
+ }
408
+ try {
409
+ const report = readReport(base, resolve(readOption(argv, '--root') ?? process.cwd()))
410
+ const json = JSON.stringify(report)
411
+ const text = renderReport(report)
412
+ if (out !== undefined) {
413
+ const target = resolve(out)
414
+ mkdirSync(dirname(target), { recursive: true })
415
+ writeFileSync(target, `${extname(target) === '.json' ? json : text}\n`)
416
+ }
417
+ console.log(argv.includes('--json') ? json : text)
418
+ return report.missing.length > 0 ? 3 : 0
419
+ } catch (error) {
420
+ console.error(`titles: ${error instanceof Error ? error.message : String(error)}`)
421
+ return 2
422
+ }
423
+ }
424
+
425
+ process.exitCode = main(process.argv.slice(2))
@@ -201,8 +201,8 @@ await PORTFOLIO.place('home')
201
201
  control, a Tab away, or a named button. Report a screen that commits on a timer, on an
202
202
  unpredictable event, or only after work the person cannot observe as a screen finding, and never
203
203
  work around it in the layer.
204
- 7. **Type only what a person would.** Journeys carry trusted input; adversarial payloads belong to
205
- the transport family and the parser suites.
204
+ 7. **Type only what a person would.** Journeys carry trusted input; inputs no person would type
205
+ belong to the transport family and the parser suites.
206
206
  8. **Perform every interaction step unconditionally.** Never gate a step on whether the control it
207
207
  is about to drive exists or is reachable, and never branch a journey on `readRefusal`. Let the
208
208
  resolver's failure voice name what the interface withheld. A guarded step passes whether or not
@@ -30,7 +30,8 @@ file is written to, and the flag as `enabled`.
30
30
  picture is one element.
31
31
  - Take `enabled` from `inject('capture')`, which the generated root configuration provides from the
32
32
  `CAPTURE` environment variable set to `1` in the shell that runs the axis. An ordinary run leaves
33
- it false, so `place` returns `undefined`, resizes nothing, writes nothing, and records nothing.
33
+ it false, so `place` returns `undefined`, resizes nothing, writes nothing, records no placement,
34
+ and still refuses an unregistered state and a second placement of one state.
34
35
  - Read `files` for the registry expanded across every variant, `placements` for what this run placed,
35
36
  and `paths` for what it wrote. Each hands back a snapshot.
36
37
  - Reach for `readFrame` where the written image itself is the subject — its decoded size, or its
@@ -42,8 +43,8 @@ The package already refuses these, so assert none of them again:
42
43
  | Refusal | Raised by |
43
44
  | -------------------------------------------------------------------- | --------------------- |
44
45
  | `Capture variant "<name>" is not registered` | `createPortfolio` |
45
- | `Capture state "<state>" is not registered` | `place`, when enabled |
46
- | `Capture state "<state>" is already placed` | `place`, when enabled |
46
+ | `Capture state "<state>" is not registered` | `place`, in every run |
47
+ | `Capture state "<state>" is already placed` | `place`, in every run |
47
48
  | `Capture frame at <path> is not the one this run shot` | `captureFrame` |
48
49
  | `Capture frame was written to <written> where <asked> was asked for` | `captureFrame` |
49
50
 
@@ -92,11 +93,11 @@ Write each of the following proofs into the suite. The package asserts none of t
92
93
  - Keep the filename proof always-on, so a registry edit that introduces a duplicate or a collision
93
94
  fails the ordinary run. Compare `expandCaptures(states, variants)` against `files` and against a
94
95
  set built from it; a length equal to the set's size is what proves the expansion unique.
95
- - Assert placement as set equality against the registry, in every run. Never assert a count: a count
96
- passes while one state is placed twice and another never.
97
- - Read the placement proof from the suite's own set rather than from `placements`. An ordinary run's
98
- `place` returns before it reads the registry, so `placements` is empty there and an unregistered name
99
- reaches no refusal until a capture run. The always-on placement proof is what catches it.
96
+ - Assert placement as set equality against the registry, in every run.
97
+ - Read the placement proof from the suite's own set rather than from `placements`, which stays empty
98
+ in an ordinary run. `place` refuses an unregistered name and a second placement of one state in
99
+ every run, but a registered state that no journey placed reaches no refusal; the always-on
100
+ placement proof catches it.
100
101
  - Under the flag, assert the written filenames equal the registry expanded for the run's variant.
101
102
  `captureFrame` already reads each file back and compares its bytes against the frame it shot, so
102
103
  the path `place` returns is proof the file exists and holds that frame.
@@ -55,8 +55,8 @@ over the same surface needs an instruction from the user.
55
55
  subagent with a clean context, blind to the others until reconciliation, run once, and no
56
56
  lane is dropped because an engine is dark. A narrow verdict request runs the selected lane once
57
57
  and stops.
58
- 4. **Reconcile.** Confirmed findings get carriers; refuted findings die on the record with
59
- the evidence that killed them; conflicts get a dated ruling. Then walk the reconciled
58
+ 4. **Reconcile.** Confirmed findings get carriers; refuted findings close on the record with
59
+ the evidence that refuted them; conflicts get a dated ruling. Then walk the reconciled
60
60
  list once and name the fix-brief item carrying each finding — a finding with no carrier
61
61
  is a dropped finding, and it will cost the next round.
62
62
  5. **Dispatch serialized fix units,** objective lane first because it changes the ground
@@ -82,7 +82,7 @@ over the same surface needs an instruction from the user.
82
82
  - Do not narrow a selector, add a local exception, or restyle one instance to make a
83
83
  capture pass while the underlying rule stays wrong.
84
84
  - When a law of the repository genuinely conflicts with the surface's required anatomy,
85
- stop and report both horns with evidence. An improvised local exception poisons an
85
+ stop and report both horns with evidence. An improvised local exception voids an
86
86
  enforced invariant.
87
87
 
88
88
  ## Return the fixed verdict shape
@@ -25,7 +25,7 @@ description: Run an Orkestrel release from layer order to registry confirmation.
25
25
  | `scripts/compare.ts` | `[--package NAME] [--version V] [--tarball PATH] [--dist dist] [--json]`, run from the package root after its build: fetches the published tarball and lists the material differences from the rebuilt `dist/` (sourcemaps excluded, whitespace-only differences ignored). Exit 0 when nothing differs, 3 when something does, 2 when the tarball cannot be read, 64 on usage (no package name, no `dist/`, or `--tarball` with no path). |
26
26
  | `scripts/pins.ts` | `--version PRIOR [--range PRIOR_RANGE ...] [--paths src,tests] [--json]`, run from the package root: lists every line under the paths carrying the prior version literal or a prior range literal, once per line, sweeping a one-comparator range such as `^8.3.0` for its bare version in any form. Exit 0 with no hits, 3 with hits, 64 on usage (no literal, or a flag with no value). |
27
27
  | `scripts/swap.ts` | One mode per run, from any directory: `--pin --consumer DIR --source DIR [--source DIR ...] [--override NAME ...] [--no-build] [--dry-run] [--json]` refuses a source whose packed manifest names a local dependency spec in any dependency field (a `file:` or `link:` prefix, a path start of `.`, `~/`, `/`, `\`, or a drive letter, or a protocol-free archive name ending in `.tgz`, `.tar.gz`, or `.tar` that is not a `user/repo` shorthand), builds and packs each source into `<consumer>/tmp/tarballs/` under a name carrying its SHA-256, records each replaced range in the ledger `pins.json` there, the record of which build the consumer runs, writes each tarball's absolute `file:` spec and each `--override` (repeatable, like `--source`) where oxfmt sorts it, installs, and proves that every pinned source, an override-only one included, is installed with the source's bytes; `--restore --consumer DIR [--package NAME ...] [--json]` puts back each recorded range and override, keeps an `overrides` block that existed before the pin, installs, proves the restored copy, and lists the re-pin arguments, and refuses with 64, before it writes anything, a ledger whose package or override keys are not package names or whose removal would leave `node_modules`; `--status --consumer DIR [--json]` writes nothing and reports drift between the ledger, the manifest, the tarballs, the installed bytes, and the source commits, a manifest spec holding its tarball only when it names the ledger archive's location. `--pin` and `--restore` write a receipt under `tmp/units/`. Exit 0 on success, 1 when a command or a proof failed, 3 when `--status` reports drift, 64 on usage. |
28
- | `scripts/wave.ts` | `--visit [--target DIR] [--offline] [--from STEP] [--to STEP] [--prior VERSION] [--dry-run] [--json] [--out FILE]` runs one repository's visit in the order [wave.md](references/wave.md) § Visit a repository fixes (pin, commit, overwrite, verify, install, pins, format, gates, compare), stops at the first failing step, and prints each step's exit (a failed step names the `code: message` refusal or the `note` a JSON verb writes on stdout, else the last stderr lines), the bump ruling (the rebuilt dist against the published tarball, and the final runtime dependency set against the published manifest), and a row for `.orkestrel/release.md`; `--prior` names the version a pre-ruled bump replaced so the self-pin sweep finds it; `--plan` prints the packages by layer from the catalog table. Exit 0 when every step passed, 1 when one failed, 2 when a reading failed, 64 on usage. |
28
+ | `scripts/wave.ts` | `--visit [--target DIR] [--offline] [--regenerate] [--from STEP] [--to STEP] [--prior VERSION] [--dry-run] [--json] [--out FILE]` runs one repository's visit in the order [wave.md](references/wave.md) § Visit a repository fixes (pin, commit, overwrite, verify, install, pins, format, record, gates, compare), stops at the first failing step, journals each executed step's full output under the target's `tmp/units/wave/` when the work tree ignores `tmp/` and names the journal in its row, and prints each step's exit (a failed step names the `code: message` refusal or the `note` a JSON verb writes on stdout, else the last stderr lines), the bump ruling (the rebuilt dist against the published tarball, and the final runtime dependency set against the published manifest), and a row for `.orkestrel/release.md`; `--prior` names the version a pre-ruled bump replaced so the self-pin sweep finds it; `--plan` prints the packages by layer from the catalog table. An install that fails `ERESOLVE` names the rerun command, and `--regenerate` replaces the lockfile and `node_modules` from the manifest under the refusals and proof [wave.md](references/wave.md) § Regenerate a lock the install cannot move states; `record` commits the overwrite's writes as `Overwrite with scaffold VERSION`. Exit 0 when every step passed, 1 when one failed, 2 when a reading failed, 64 on usage, including `--regenerate` with `--plan`. |
29
29
  | `scripts/window.ts` | `--whoami [--wait SECONDS]` reads or polls `npm whoami`; `--login` prints the operator's login command and polls `whoami` until it answers; `--publish DIR... --otp CODE` refuses a batch whose nearest `.git` entry is a file, including ancestor entries, with exit 3 before contacting npm; it names a linked worktree's primary clone or identifies a submodule, then uploads back to back with `npm publish --ignore-scripts --otp`, journals each upload under `tmp/units/`, stops at the first refusal naming where to resume, and confirms each accepted version against the registry; `--confirm NAME@VERSION...` re-reads the registry until it serves the version. Exit 0 on success, 3 when not. |
30
30
 
31
31
  The user's current instruction wins. `references/release.md` owns the credential
@@ -53,7 +53,7 @@ following the skill.
53
53
  Derive each pin from that reading, never from a local manifest.
54
54
  3. **Visit each repository.** Run `node .agents/skills/orkestrel-publish/scripts/wave.ts --visit --target <repository>`,
55
55
  which runs the visit in [wave.md](references/wave.md) in its stated order and stops at the first
56
- failing step; repair that step by hand and re-run from it with `--from`. Run visits in parallel
56
+ failing step; repair that step by hand and re-run from it with `--from`; for an `ERESOLVE`, follow [wave.md](references/wave.md) § Regenerate a lock the install cannot move. Run visits in parallel
57
57
  slices of disjoint repositories, each slice serial inside itself.
58
58
  4. **Rule on each package's bump.** Take the visit's comparison and range reading; repeat `compare.ts` only after the artifact changes or an unanswered result. Apply the triggers in [wave.md](references/wave.md) § Rule on
59
59
  the bump; `references/release.md` § What a bump obliges owns the blast radius.
@@ -34,6 +34,7 @@ When a consumer needs a dependency's unpublished source, to meet a defect that l
34
34
 
35
35
  ## The scaffold surface
36
36
 
37
+ - After a package's first publish, run `scaffold catalog --all` in the scaffold checkout and release scaffold, so the `surface` rule reads the package's guide from that release.
37
38
  - `scaffold` is a development dependency of every package, so it publishes on its own and propagates as files, never as a cascade. Each package builds against the published `scaffold`.
38
39
  - `scaffold` ships `dist/host`, the vendored file set every target receives through `repair`. Bump and publish `scaffold` when any vendored byte or the set of vendored paths changes.
39
40
  - After a vendored-only release, re-pin `@orkestrel/scaffold` in each target, run `repair` there, and prove that target's gates still green. A target bumps only when its own published surface moved.