@orkestrel/scaffold 0.0.76 → 0.0.78

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 (154) 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 +4 -2
  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 -927
  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/planner.md +10 -44
  79. package/dist/host/claude/agents/researcher.md +11 -30
  80. package/dist/host/claude/agents/reviewer.md +11 -95
  81. package/dist/host/claude/agents/scout.md +9 -23
  82. package/dist/host/claude/agents/verifier.md +15 -33
  83. package/dist/host/claude/rules/documentation.md +8 -2
  84. package/dist/host/claude/rules/portability.md +7 -1
  85. package/dist/host/claude/rules/quality.md +36 -96
  86. package/dist/host/claude/rules/styles.md +3 -0
  87. package/dist/host/claude/rules/tests.md +6 -3
  88. package/dist/host/claude/rules/workspace.md +19 -15
  89. package/dist/host/claude/rules/writing.md +57 -108
  90. package/dist/host/claude/settings.json +5 -3
  91. package/dist/host/claude/skills/enterprise-bootstrap/SKILL.md +1 -1
  92. package/dist/host/claude/skills/{orkestrel-align-packages → orkestrel-align}/SKILL.md +2 -2
  93. package/dist/host/claude/skills/{orkestrel-build-application → orkestrel-build}/SKILL.md +2 -2
  94. package/dist/host/claude/skills/orkestrel-dispatch/SKILL.md +11 -0
  95. package/dist/host/claude/skills/orkestrel-falsify/SKILL.md +2 -1
  96. package/dist/host/claude/skills/{orkestrel-harden-package → orkestrel-harden}/SKILL.md +2 -2
  97. package/dist/host/claude/skills/{orkestrel-prove-journey → orkestrel-journey}/SKILL.md +2 -2
  98. package/dist/host/claude/skills/orkestrel-polish/SKILL.md +12 -0
  99. package/dist/host/claude/skills/orkestrel-scout/SKILL.md +11 -0
  100. package/dist/host/codex/agents/analyst.toml +15 -32
  101. package/dist/host/codex/agents/astra.toml +25 -0
  102. package/dist/host/codex/agents/builder.toml +13 -20
  103. package/dist/host/codex/agents/checker.toml +13 -27
  104. package/dist/host/codex/agents/distiller.toml +9 -22
  105. package/dist/host/codex/agents/grok.toml +11 -30
  106. package/dist/host/codex/agents/opus.toml +14 -22
  107. package/dist/host/codex/agents/orkestrel.toml +1 -1
  108. package/dist/host/codex/agents/planner.toml +11 -28
  109. package/dist/host/codex/agents/researcher.toml +10 -22
  110. package/dist/host/codex/agents/reviewer.toml +11 -27
  111. package/dist/host/codex/agents/scout.toml +11 -17
  112. package/dist/host/codex/agents/verifier.toml +16 -12
  113. package/dist/host/codex/config.toml +20 -23
  114. package/dist/host/cursor/mcp.json +0 -4
  115. package/dist/host/cursor/rules/orchestration.mdc +12 -20
  116. package/dist/host/dotfiles/mcp.json +0 -4
  117. package/dist/host/dotfiles/oxlintrc.json +7 -0
  118. package/dist/host/guides/probe.md +9 -9
  119. package/dist/host/guides/scaffold.md +117 -71
  120. package/dist/host/guides/test.md +1 -1
  121. package/dist/host/manifest.json +322 -185
  122. package/dist/host/scripts/codex.sh +2 -2
  123. package/dist/host/tests/config.test.ts +68 -46
  124. package/dist/host/tests/policy.test.ts +1 -5
  125. package/dist/host/tests/setupPolicy.ts +179 -4
  126. package/dist/src/core/index.cjs +255 -84
  127. package/dist/src/core/index.cjs.map +1 -1
  128. package/dist/src/core/index.d.cts +94 -29
  129. package/dist/src/core/index.d.ts +94 -29
  130. package/dist/src/core/index.js +253 -85
  131. package/dist/src/core/index.js.map +1 -1
  132. package/dist/src/server/index.cjs +55 -9
  133. package/dist/src/server/index.cjs.map +1 -1
  134. package/dist/src/server/index.d.cts +29 -4
  135. package/dist/src/server/index.d.ts +29 -4
  136. package/dist/src/server/index.js +56 -11
  137. package/dist/src/server/index.js.map +1 -1
  138. package/package.json +15 -11
  139. package/dist/host/CLAUDE.md +0 -61
  140. package/dist/host/agents/skills/orkestrel-prove-journey/agents/openai.yaml +0 -4
  141. package/dist/host/agents/transports/claude.md +0 -49
  142. package/dist/host/claude/agents/application.md +0 -36
  143. package/dist/host/claude/agents/sol.md +0 -61
  144. package/dist/host/claude/skills/orkestrel-polish-surface/SKILL.md +0 -12
  145. package/dist/host/codex/agents/application.toml +0 -25
  146. package/dist/host/codex/agents/sol.toml +0 -19
  147. /package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/references/integration.md +0 -0
  148. /package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/centralization.md +0 -0
  149. /package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/contract.md +0 -0
  150. /package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/research.md +0 -0
  151. /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/decide.md +0 -0
  152. /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/layer.md +0 -0
  153. /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/statechart.md +0 -0
  154. /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/styles.md +0 -0
@@ -0,0 +1,259 @@
1
+ // Probe a bench with one bounded round trip and resolve its entry. Run from the checkout root:
2
+ // node .agents/skills/orkestrel-dispatch/scripts/bench.ts --cursor [--resolve] [--model ID] [--cap SECONDS]
3
+ // node .agents/skills/orkestrel-dispatch/scripts/bench.ts --codex [--model ID] [--sandbox MODE] [--cap SECONDS]
4
+ // node .agents/skills/orkestrel-dispatch/scripts/bench.ts --claude [--model ALIAS] [--cap SECONDS]
5
+ // --cursor resolves the Cursor CLI entry (the newest versioned install under LOCALAPPDATA on
6
+ // Windows, `agent` on PATH elsewhere), reads its version (an entry that cannot answer `--version` is
7
+ // dark, with or without --resolve), and runs one bounded prompt through
8
+ // launch.ts into tmp/cursor/bench.jsonl; --resolve stops after printing the entry. --codex reads
9
+ // `codex --version` and `codex login status`, then runs one bounded exec into tmp/codex/bench.jsonl
10
+ // under the sandbox the host allows (danger-full-access on Windows, read-only elsewhere). --claude
11
+ // reads `claude --version` and runs one bounded print-mode prompt into tmp/claude/bench.jsonl. The
12
+ // JSON summary carries the command, the version, the session id, the answer, and `live`. Exit 0 when
13
+ // the bench answered READY, 3 when it is dark, 64 on usage.
14
+ import { spawnSync } from 'node:child_process'
15
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
16
+ import { dirname, extname, join } from 'node:path'
17
+ import { fileURLToPath } from 'node:url'
18
+ import { readJsonObject, readMissingFlags, readNumber, readOption, readString } from './helpers.ts'
19
+
20
+ const SCRIPTS = dirname(fileURLToPath(import.meta.url))
21
+ // The checkout runs the `.ts` sources and a built twin under `dist/agents` runs `.js` siblings.
22
+ const EXTENSION = extname(fileURLToPath(import.meta.url))
23
+ const PROMPT = 'Reply with the single word READY and nothing else.'
24
+ const CURSOR_MODEL = process.env.CURSOR_GROK_MODEL ?? 'grok-4.7-high'
25
+ const CODEX_MODEL = process.env.CODEX_ASTRA_MODEL ?? 'gpt-6-astra'
26
+ const CLAUDE_MODEL = 'opus'
27
+ const DEFAULT_CAP = 180
28
+
29
+ interface Entry {
30
+ readonly command: readonly string[]
31
+ readonly version: string | undefined
32
+ readonly error: string | undefined
33
+ }
34
+
35
+ interface Summary {
36
+ readonly session: string | undefined
37
+ readonly answer: string | undefined
38
+ readonly exit: number | undefined
39
+ readonly capped: boolean
40
+ readonly durationMs: number | undefined
41
+ }
42
+
43
+ function readVersion(command: readonly string[]): string | undefined {
44
+ const [file, ...args] = command
45
+ if (file === undefined) return undefined
46
+ const result = spawnSync(file, [...args, '--version'], { encoding: 'utf8', windowsHide: true })
47
+ return result.status === 0 ? result.stdout.trim() : undefined
48
+ }
49
+
50
+ function resolveCursor(): Entry {
51
+ if (process.platform !== 'win32') {
52
+ return { command: ['agent'], version: readVersion(['agent']), error: undefined }
53
+ }
54
+ const local = process.env.LOCALAPPDATA
55
+ if (local === undefined)
56
+ return { command: [], version: undefined, error: 'LOCALAPPDATA is not set' }
57
+ const versions = join(local, 'cursor-agent', 'versions')
58
+ if (!existsSync(versions) || !statSync(versions).isDirectory()) {
59
+ return { command: [], version: undefined, error: `${versions} is not a directory` }
60
+ }
61
+ const newest = readdirSync(versions, { withFileTypes: true })
62
+ .filter((entry) => entry.isDirectory() && /^\d{4}\.\d{2}\.\d{2}-[0-9a-f]+$/u.test(entry.name))
63
+ .map((entry) => entry.name)
64
+ .sort()
65
+ .at(-1)
66
+ if (newest === undefined) {
67
+ return { command: [], version: undefined, error: `no versioned install under ${versions}` }
68
+ }
69
+ const node = join(versions, newest, 'node.exe')
70
+ const index = join(versions, newest, 'index.js')
71
+ if (!existsSync(node) || !existsSync(index)) {
72
+ return { command: [], version: undefined, error: `${newest} lacks node.exe or index.js` }
73
+ }
74
+ return { command: [node, index], version: readVersion([node, index]), error: undefined }
75
+ }
76
+
77
+ function runRoundTrip(
78
+ bench: 'cursor' | 'codex' | 'claude',
79
+ command: readonly string[],
80
+ cap: number,
81
+ ): Summary {
82
+ const journal = `tmp/${bench}/bench.jsonl`
83
+ const errors = `tmp/${bench}/bench.err`
84
+ const answer = `tmp/${bench}/bench-answer.md`
85
+ const launch = spawnSync(
86
+ process.execPath,
87
+ [
88
+ join(SCRIPTS, `launch${EXTENSION}`),
89
+ '--journal',
90
+ journal,
91
+ '--errors',
92
+ errors,
93
+ '--cap',
94
+ String(cap),
95
+ '--',
96
+ ...command,
97
+ ],
98
+ { encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit'], windowsHide: true },
99
+ )
100
+ const launched = readJsonObject(launch.stdout)
101
+ const result = spawnSync(
102
+ process.execPath,
103
+ [join(SCRIPTS, `result${EXTENSION}`), `--${bench}`, journal, '--out', answer],
104
+ { encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit'], windowsHide: true },
105
+ )
106
+ const read = readJsonObject(result.stdout)
107
+ return {
108
+ session: readString(read, 'session'),
109
+ answer: existsSync(answer) ? readFileSync(answer, 'utf8').trim().slice(0, 200) : undefined,
110
+ exit: readNumber(launched, 'exit'),
111
+ capped: launched?.capped === true,
112
+ durationMs: readNumber(launched, 'durationMs'),
113
+ }
114
+ }
115
+
116
+ function isLive(summary: Summary): boolean {
117
+ return (
118
+ summary.session !== undefined &&
119
+ summary.exit === 0 &&
120
+ !summary.capped &&
121
+ summary.answer !== undefined &&
122
+ /^READY\.?$/u.test(summary.answer.trim())
123
+ )
124
+ }
125
+
126
+ function probeCursor(argv: readonly string[]): number {
127
+ const entry = resolveCursor()
128
+ const model = readOption(argv, '--model') ?? CURSOR_MODEL
129
+ if (entry.error !== undefined || entry.version === undefined) {
130
+ console.log(JSON.stringify({ bench: 'cursor', ...entry, live: false }))
131
+ return 3
132
+ }
133
+ if (argv.includes('--resolve')) {
134
+ console.log(JSON.stringify({ bench: 'cursor', command: entry.command, version: entry.version }))
135
+ return 0
136
+ }
137
+ const cap = Number(readOption(argv, '--cap') ?? DEFAULT_CAP)
138
+ const summary = runRoundTrip(
139
+ 'cursor',
140
+ [
141
+ ...entry.command,
142
+ '-p',
143
+ '--trust',
144
+ '--mode=ask',
145
+ '--model',
146
+ model,
147
+ '--output-format',
148
+ 'stream-json',
149
+ PROMPT,
150
+ ],
151
+ cap,
152
+ )
153
+ const live = isLive(summary)
154
+ console.log(
155
+ JSON.stringify({
156
+ bench: 'cursor',
157
+ command: entry.command,
158
+ version: entry.version,
159
+ model,
160
+ ...summary,
161
+ live,
162
+ journal: 'tmp/cursor/bench.jsonl',
163
+ }),
164
+ )
165
+ return live ? 0 : 3
166
+ }
167
+
168
+ function probeCodex(argv: readonly string[]): number {
169
+ const version = readVersion(['codex'])
170
+ const login = spawnSync('codex', ['login', 'status'], { encoding: 'utf8', windowsHide: true })
171
+ const model = readOption(argv, '--model') ?? CODEX_MODEL
172
+ const sandbox =
173
+ readOption(argv, '--sandbox') ??
174
+ (process.platform === 'win32' ? 'danger-full-access' : 'read-only')
175
+ const status = `${login.stdout}${login.stderr}`.trim()
176
+ if (version === undefined || login.status !== 0) {
177
+ console.log(JSON.stringify({ bench: 'codex', version, login: status, live: false }))
178
+ return 3
179
+ }
180
+ const cap = Number(readOption(argv, '--cap') ?? DEFAULT_CAP)
181
+ const summary = runRoundTrip(
182
+ 'codex',
183
+ [
184
+ 'codex',
185
+ 'exec',
186
+ '--json',
187
+ '-C',
188
+ process.cwd(),
189
+ '--sandbox',
190
+ sandbox,
191
+ '--model',
192
+ model,
193
+ '-c',
194
+ 'model_reasoning_effort="low"',
195
+ '--output-last-message',
196
+ 'tmp/codex/bench-last.md',
197
+ PROMPT,
198
+ ],
199
+ cap,
200
+ )
201
+ const live = isLive(summary)
202
+ console.log(
203
+ JSON.stringify({
204
+ bench: 'codex',
205
+ version,
206
+ login: status,
207
+ model,
208
+ sandbox,
209
+ ...summary,
210
+ live,
211
+ journal: 'tmp/codex/bench.jsonl',
212
+ }),
213
+ )
214
+ return live ? 0 : 3
215
+ }
216
+
217
+ function probeClaude(argv: readonly string[]): number {
218
+ const version = readVersion(['claude'])
219
+ const model = readOption(argv, '--model') ?? CLAUDE_MODEL
220
+ if (version === undefined) {
221
+ console.log(JSON.stringify({ bench: 'claude', version, live: false }))
222
+ return 3
223
+ }
224
+ const cap = Number(readOption(argv, '--cap') ?? DEFAULT_CAP)
225
+ const summary = runRoundTrip(
226
+ 'claude',
227
+ ['claude', '-p', PROMPT, '--output-format', 'stream-json', '--verbose', '--model', model],
228
+ cap,
229
+ )
230
+ const live = isLive(summary)
231
+ console.log(
232
+ JSON.stringify({
233
+ bench: 'claude',
234
+ version,
235
+ model,
236
+ ...summary,
237
+ live,
238
+ journal: 'tmp/claude/bench.jsonl',
239
+ }),
240
+ )
241
+ return live ? 0 : 3
242
+ }
243
+
244
+ function main(argv: readonly string[]): number {
245
+ const missing = readMissingFlags(argv, ['--model', '--cap', '--sandbox'])
246
+ if (missing.length > 0) {
247
+ console.error(`bench: ${missing.join(', ')} given with no value`)
248
+ return 64
249
+ }
250
+ if (argv.includes('--cursor')) return probeCursor(argv)
251
+ if (argv.includes('--codex')) return probeCodex(argv)
252
+ if (argv.includes('--claude')) return probeClaude(argv)
253
+ console.error(
254
+ 'usage: bench.ts --cursor [--resolve] [--model ID] [--cap SECONDS] | --codex [--model ID] [--sandbox MODE] [--cap SECONDS] | --claude [--model ALIAS] [--cap SECONDS]',
255
+ )
256
+ return 64
257
+ }
258
+
259
+ process.exitCode = main(process.argv.slice(2))
@@ -0,0 +1,110 @@
1
+ // Write a unit brief from the template, or check that every path a brief names resolves. Run from
2
+ // the checkout root:
3
+ // node .agents/skills/orkestrel-dispatch/scripts/brief.ts --unit UNIT --lane units|cursor|codex|claude [--subject TEXT]
4
+ // node .agents/skills/orkestrel-dispatch/scripts/brief.ts --check tmp/codex/UNIT-brief.md
5
+ // The first form copies the brief template beside this skill (`../../../templates/brief.md`, so a
6
+ // target's installed copy finds it too) to tmp/<lane>/<unit>-brief.md with UNIT_ID and SHORT_SUBJECT
7
+ // filled, and writes <unit>-brief-<n>.md when a brief already exists, so a launched brief is never
8
+ // edited. The second form reads every backticked token carrying a slash (a backslash counts and is
9
+ // read as a slash), strips a `:line` or
10
+ // `:line-line` suffix, resolves it from the working directory, and prints the ones that do not
11
+ // exist; a bare filename is outside the check. Exit 3 when any is missing, 64 on usage.
12
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
13
+ import { dirname, join, resolve } from 'node:path'
14
+ import { fileURLToPath } from 'node:url'
15
+ import { readMissingFlags, readOption } from './helpers.ts'
16
+
17
+ const TEMPLATE = resolve(
18
+ dirname(fileURLToPath(import.meta.url)),
19
+ '..',
20
+ '..',
21
+ '..',
22
+ 'templates',
23
+ 'brief.md',
24
+ )
25
+ const LANES: Readonly<Record<string, string>> = Object.freeze({
26
+ units: 'tmp/units',
27
+ cursor: 'tmp/cursor',
28
+ codex: 'tmp/codex',
29
+ claude: 'tmp/claude',
30
+ })
31
+
32
+ function readsAsPath(token: string): boolean {
33
+ if (token.includes('://') || /[<>*{}\s]/u.test(token)) return false
34
+ if (/[A-Z]{2,}_[A-Z]/u.test(token)) return false
35
+ if (token.startsWith('@') || token.startsWith('-') || token.startsWith('$')) return false
36
+ if (!/^[A-Za-z0-9_.~/-]+$/u.test(token)) return false
37
+ return token.includes('/')
38
+ }
39
+
40
+ function extractPaths(content: string): readonly string[] {
41
+ const found = new Set<string>()
42
+ for (const match of content.matchAll(/`([^`\n]+)`/gu)) {
43
+ const token = (match[1] ?? '').replace(/\\/gu, '/').replace(/:\d+(?:[-–]\d+)?$/u, '')
44
+ if (readsAsPath(token)) found.add(token)
45
+ }
46
+ return [...found].sort()
47
+ }
48
+
49
+ function resolveBriefPath(directory: string, unit: string): string {
50
+ const base = join(directory, `${unit}-brief.md`)
51
+ if (!existsSync(base)) return base
52
+ let n = 2
53
+ while (existsSync(join(directory, `${unit}-brief-${n}.md`))) n += 1
54
+ return join(directory, `${unit}-brief-${n}.md`)
55
+ }
56
+
57
+ function writeBrief(argv: readonly string[]): number {
58
+ const unit = readOption(argv, '--unit')
59
+ const lane = readOption(argv, '--lane')
60
+ const directory = lane === undefined ? undefined : LANES[lane]
61
+ if (unit === undefined || directory === undefined || !/^[A-Za-z0-9._-]+$/u.test(unit)) {
62
+ console.error('usage: brief.ts --unit UNIT --lane units|cursor|codex|claude [--subject TEXT]')
63
+ return 64
64
+ }
65
+ if (!existsSync(TEMPLATE)) {
66
+ console.error(`brief: the template is missing at ${TEMPLATE}`)
67
+ return 64
68
+ }
69
+ mkdirSync(directory, { recursive: true })
70
+ const path = resolveBriefPath(directory, unit)
71
+ const subject = readOption(argv, '--subject') ?? 'SHORT_SUBJECT'
72
+ const content = readFileSync(TEMPLATE, 'utf8')
73
+ .replace(/UNIT_ID/gu, unit)
74
+ .replace(/SHORT_SUBJECT/gu, subject)
75
+ writeFileSync(path, content)
76
+ console.log(JSON.stringify({ brief: path, template: TEMPLATE }))
77
+ return 0
78
+ }
79
+
80
+ function checkBrief(path: string): number {
81
+ if (!existsSync(path) || !statSync(path).isFile()) {
82
+ console.error(`brief: ${path} is not a file`)
83
+ return 64
84
+ }
85
+ const paths = extractPaths(readFileSync(path, 'utf8'))
86
+ const missing = paths.filter((candidate) => !existsSync(resolve(candidate)))
87
+ console.log(JSON.stringify({ brief: path, checked: paths.length, missing }))
88
+ return missing.length === 0 ? 0 : 3
89
+ }
90
+
91
+ function main(argv: readonly string[]): number {
92
+ const missing = readMissingFlags(argv, ['--unit', '--lane', '--subject', '--check'])
93
+ if (missing.length > 0) {
94
+ console.error(`brief: ${missing.join(', ')} given with no value`)
95
+ return 64
96
+ }
97
+ const check = readOption(argv, '--check')
98
+ if (check !== undefined && argv.includes('--unit')) {
99
+ console.error('brief: --check and --unit are two modes; name one')
100
+ return 64
101
+ }
102
+ if (check !== undefined) return checkBrief(check)
103
+ if (argv.includes('--unit')) return writeBrief(argv)
104
+ console.error(
105
+ 'usage: brief.ts --unit UNIT --lane units|cursor|codex|claude [--subject TEXT] | --check PATH',
106
+ )
107
+ return 64
108
+ }
109
+
110
+ process.exitCode = main(process.argv.slice(2))
@@ -0,0 +1,115 @@
1
+ // Check that every `file:line` citation in a report, verdict, or distillate resolves. Run from the
2
+ // checkout root:
3
+ // node .agents/skills/orkestrel-dispatch/scripts/cite.ts PATH [--root DIR] [--json]
4
+ // A citation resolves when its path exists under the root (a path relative to the root resolves
5
+ // first, and a backslash reads as a slash) and the line is within the file, where a trailing line
6
+ // break ends the last line. A citation naming a bare filename that no root-relative
7
+ // file matches resolves when exactly one file under the root carries that name, outside
8
+ // node_modules, dist, tmp, .git, .orkestrel, and .idea. The summary lists every citation that does
9
+ // not resolve, with the reason. Exit 0 when all resolve, 3 when any does not, 64 on usage.
10
+ import { existsSync, readFileSync, statSync } from 'node:fs'
11
+ import { basename, relative, resolve } from 'node:path'
12
+ import { listFiles, normalizeSlashes, readMissingFlags, readOption } from './helpers.ts'
13
+
14
+ const CITATION =
15
+ /(?<![\w/\\.-])((?:\.{1,2}[/\\])?\.?[A-Za-z0-9_@][A-Za-z0-9_./\\@-]*\.[a-z]{1,5}):(\d+)(?:[-–](\d+))?(?![\w:])/gu
16
+
17
+ interface Unresolved {
18
+ readonly citation: string
19
+ readonly reason: string
20
+ }
21
+
22
+ interface Resolution {
23
+ readonly path: string | undefined
24
+ readonly reason: string | undefined
25
+ }
26
+
27
+ function indexBasenames(root: string): ReadonlyMap<string, readonly string[]> {
28
+ const index = new Map<string, string[]>()
29
+ for (const file of listFiles(root)) {
30
+ const name = basename(file.path)
31
+ const paths = index.get(name) ?? []
32
+ paths.push(normalizeSlashes(relative(root, file.path)))
33
+ index.set(name, paths)
34
+ }
35
+ return index
36
+ }
37
+
38
+ function countLines(path: string): number {
39
+ const text = readFileSync(path, 'utf8')
40
+ if (text === '') return 0
41
+ const lines = text.split(/\r\n|\n/)
42
+ // A trailing line break ends the last line rather than opening a phantom one.
43
+ return lines.at(-1) === '' ? lines.length - 1 : lines.length
44
+ }
45
+
46
+ function resolveFile(
47
+ root: string,
48
+ file: string,
49
+ index: () => ReadonlyMap<string, readonly string[]>,
50
+ ): Resolution {
51
+ const direct = resolve(root, normalizeSlashes(file))
52
+ if (existsSync(direct) && statSync(direct).isFile()) return { path: direct, reason: undefined }
53
+ if (/[/\\]/u.test(file)) return { path: undefined, reason: 'file does not exist' }
54
+ const candidates = index().get(basename(file)) ?? []
55
+ const only = candidates.length === 1 ? candidates[0] : undefined
56
+ if (only !== undefined) return { path: resolve(root, only), reason: undefined }
57
+ if (candidates.length === 0) return { path: undefined, reason: 'file does not exist' }
58
+ return { path: undefined, reason: `ambiguous basename: ${candidates.join(', ')}` }
59
+ }
60
+
61
+ function judgeLines(
62
+ path: string,
63
+ line: number,
64
+ end: number | undefined,
65
+ counts: Map<string, number>,
66
+ ): string | undefined {
67
+ const total = counts.get(path) ?? countLines(path)
68
+ counts.set(path, total)
69
+ if (line < 1 || line > total) return `line ${line} is outside 1-${total}`
70
+ if (end !== undefined && (end < line || end > total))
71
+ return `range end ${end} is outside ${line}-${total}`
72
+ return undefined
73
+ }
74
+
75
+ function main(argv: readonly string[]): number {
76
+ const missing = readMissingFlags(argv, ['--root'])
77
+ if (missing.length > 0) {
78
+ console.error(`cite: ${missing.join(', ')} given with no value`)
79
+ return 64
80
+ }
81
+ const root = readOption(argv, '--root') ?? process.cwd()
82
+ const path = argv.find(
83
+ (argument, position) => !argument.startsWith('--') && argv[position - 1] !== '--root',
84
+ )
85
+ if (path === undefined || !existsSync(path)) {
86
+ console.error('usage: cite.ts PATH [--root DIR] [--json]')
87
+ return 64
88
+ }
89
+ let index: ReadonlyMap<string, readonly string[]> | undefined
90
+ const counts = new Map<string, number>()
91
+ const unresolved: Unresolved[] = []
92
+ let citations = 0
93
+ for (const match of readFileSync(path, 'utf8').matchAll(CITATION)) {
94
+ const file = match[1]
95
+ const line = Number(match[2])
96
+ const end = match[3] === undefined ? undefined : Number(match[3])
97
+ if (file === undefined) continue
98
+ citations += 1
99
+ const resolved = resolveFile(root, file, () => (index ??= indexBasenames(root)))
100
+ const reason =
101
+ resolved.path === undefined
102
+ ? (resolved.reason ?? 'file does not exist')
103
+ : judgeLines(resolved.path, line, end, counts)
104
+ if (reason !== undefined) unresolved.push({ citation: match[0], reason })
105
+ }
106
+ if (argv.includes('--json')) {
107
+ console.log(JSON.stringify({ path, root, citations, unresolved }))
108
+ } else {
109
+ console.log(`cite: ${citations} citation(s) in ${path}, ${unresolved.length} unresolved`)
110
+ for (const entry of unresolved) console.log(`cite: ${entry.citation} — ${entry.reason}`)
111
+ }
112
+ return unresolved.length === 0 ? 0 : 3
113
+ }
114
+
115
+ process.exitCode = main(process.argv.slice(2))