@orkestrel/scaffold 0.0.77 → 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 (157) 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 -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/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 +14 -31
  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 +18 -21
  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 +321 -184
  122. package/dist/host/scripts/codex.sh +0 -0
  123. package/dist/host/scripts/cursor.sh +0 -0
  124. package/dist/host/scripts/deps.sh +0 -0
  125. package/dist/host/scripts/ollama.sh +0 -0
  126. package/dist/host/tests/config.test.ts +68 -46
  127. package/dist/host/tests/policy.test.ts +1 -5
  128. package/dist/host/tests/setupPolicy.ts +179 -4
  129. package/dist/src/core/index.cjs +255 -84
  130. package/dist/src/core/index.cjs.map +1 -1
  131. package/dist/src/core/index.d.cts +94 -29
  132. package/dist/src/core/index.d.ts +94 -29
  133. package/dist/src/core/index.js +253 -85
  134. package/dist/src/core/index.js.map +1 -1
  135. package/dist/src/server/index.cjs +55 -9
  136. package/dist/src/server/index.cjs.map +1 -1
  137. package/dist/src/server/index.d.cts +29 -4
  138. package/dist/src/server/index.d.ts +29 -4
  139. package/dist/src/server/index.js +56 -11
  140. package/dist/src/server/index.js.map +1 -1
  141. package/package.json +15 -11
  142. package/dist/host/CLAUDE.md +0 -61
  143. package/dist/host/agents/skills/orkestrel-prove-journey/agents/openai.yaml +0 -4
  144. package/dist/host/agents/transports/claude.md +0 -49
  145. package/dist/host/claude/agents/application.md +0 -36
  146. package/dist/host/claude/agents/sol.md +0 -61
  147. package/dist/host/claude/skills/orkestrel-polish-surface/SKILL.md +0 -12
  148. package/dist/host/codex/agents/application.toml +0 -25
  149. package/dist/host/codex/agents/sol.toml +0 -19
  150. /package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/references/integration.md +0 -0
  151. /package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/centralization.md +0 -0
  152. /package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/contract.md +0 -0
  153. /package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/research.md +0 -0
  154. /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/decide.md +0 -0
  155. /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/layer.md +0 -0
  156. /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/statechart.md +0 -0
  157. /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/styles.md +0 -0
@@ -0,0 +1,242 @@
1
+ // Work the npm authentication window. Run from the checkout root:
2
+ // node .agents/skills/orkestrel-publish/scripts/window.ts --whoami [--wait SECONDS]
3
+ // node .agents/skills/orkestrel-publish/scripts/window.ts --login [--wait SECONDS]
4
+ // node .agents/skills/orkestrel-publish/scripts/window.ts --publish DIR... --otp CODE [--json]
5
+ // node .agents/skills/orkestrel-publish/scripts/window.ts --confirm NAME@VERSION... [--wait SECONDS]
6
+ // --whoami reads `npm whoami` once, or polls it every 5 seconds until it answers or the wait ends.
7
+ // --login prints the command the operator runs in a real terminal (npm offers its approval only to a
8
+ // TTY, so no child of this script can hold it), then polls `npm whoami` until it answers or the wait
9
+ // ends. --publish re-reads `npm whoami` immediately before the first upload and refuses when it
10
+ // answers nothing, then runs `npm publish --ignore-scripts --browser=false --otp=CODE` in each
11
+ // directory back to back under captured pipes (npm's non-TTY guard turns a refused code into
12
+ // EOTP with no prompt, so the path needs no TTY), journals each upload to tmp/units/publish-<name>.log, reads the `+ name@version`
13
+ // acceptance line, stops at the first refusal and names the package to resume from, then confirms
14
+ // each accepted version against the registry. --confirm re-reads the registry for each name@version every 5
15
+ // seconds until it serves the version or the wait (default 120 seconds) ends. Exit 0 when the mode
16
+ // succeeded, 3 when it did not, 64 on usage.
17
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
18
+ import { join } from 'node:path'
19
+ import {
20
+ readList,
21
+ readMissingFlags,
22
+ readOption,
23
+ readString,
24
+ runNpm,
25
+ } from '../../orkestrel-dispatch/scripts/helpers.ts'
26
+
27
+ const POLL_MS = 5_000
28
+ const ACCEPTED = /^\+ (\S+)@(\S+)\s*$/mu
29
+
30
+ interface Upload {
31
+ readonly directory: string
32
+ readonly name: string | undefined
33
+ readonly version: string | undefined
34
+ readonly exit: number | undefined
35
+ readonly accepted: boolean
36
+ readonly confirmed: boolean
37
+ readonly code: string | undefined
38
+ readonly journal: string
39
+ }
40
+
41
+ function waitFor(ms: number): Promise<void> {
42
+ return new Promise((resolve) => {
43
+ setTimeout(resolve, ms)
44
+ })
45
+ }
46
+
47
+ function readWhoami(): string | undefined {
48
+ const result = runNpm(['whoami'])
49
+ const user = result.stdout.trim()
50
+ return result.status === 0 && user !== '' ? user : undefined
51
+ }
52
+
53
+ async function pollWhoami(waitSeconds: number): Promise<string | undefined> {
54
+ const started = Date.now()
55
+ let user = readWhoami()
56
+ while (user === undefined && Date.now() - started < waitSeconds * 1000) {
57
+ await waitFor(POLL_MS)
58
+ user = readWhoami()
59
+ }
60
+ return user
61
+ }
62
+
63
+ function readServed(name: string, version: string): boolean {
64
+ const result = runNpm(['view', `${name}@${version}`, 'version', '--json'])
65
+ return result.status === 0 && result.stdout.replace(/["\s]/gu, '') === version
66
+ }
67
+
68
+ async function confirmServed(name: string, version: string, waitSeconds: number): Promise<boolean> {
69
+ const started = Date.now()
70
+ let served = readServed(name, version)
71
+ while (!served && Date.now() - started < waitSeconds * 1000) {
72
+ await waitFor(POLL_MS)
73
+ served = readServed(name, version)
74
+ }
75
+ return served
76
+ }
77
+
78
+ function readErrorCode(text: string): string | undefined {
79
+ return text.match(/\b(E[A-Z0-9]{3,}|EOTP|E401|E403|E404)\b/u)?.[1]
80
+ }
81
+
82
+ function readManifestName(directory: string): {
83
+ readonly name: string | undefined
84
+ readonly version: string | undefined
85
+ } {
86
+ const path = join(directory, 'package.json')
87
+ if (!existsSync(path)) return { name: undefined, version: undefined }
88
+ try {
89
+ const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
90
+ if (typeof parsed !== 'object' || parsed === null)
91
+ return { name: undefined, version: undefined }
92
+ const record = Object.fromEntries(Object.entries(parsed))
93
+ return { name: readString(record, 'name'), version: readString(record, 'version') }
94
+ } catch {
95
+ return { name: undefined, version: undefined }
96
+ }
97
+ }
98
+
99
+ async function publishAll(
100
+ directories: readonly string[],
101
+ otp: string,
102
+ waitSeconds: number,
103
+ ): Promise<readonly Upload[]> {
104
+ mkdirSync('tmp/units', { recursive: true })
105
+ const uploads: Upload[] = []
106
+ for (const directory of directories) {
107
+ const manifest = readManifestName(directory)
108
+ const journal = `tmp/units/publish-${(manifest.name ?? directory).replace(/[^A-Za-z0-9.-]+/gu, '-')}.log`
109
+ const result = runNpm(
110
+ ['publish', '--ignore-scripts', '--browser=false', `--otp=${otp}`],
111
+ directory,
112
+ )
113
+ writeFileSync(journal, `${result.stdout}\n${result.stderr}\nexit=${result.status ?? 'null'}\n`)
114
+ const match = `${result.stdout}\n${result.stderr}`.match(ACCEPTED)
115
+ const accepted = result.status === 0 && match !== null
116
+ const name = match?.[1] ?? manifest.name
117
+ const version = match?.[2] ?? manifest.version
118
+ const upload: Upload = {
119
+ directory,
120
+ name,
121
+ version,
122
+ exit: result.status ?? undefined,
123
+ accepted,
124
+ confirmed: false,
125
+ code: accepted ? undefined : readErrorCode(result.stderr),
126
+ journal,
127
+ }
128
+ if (!accepted) {
129
+ uploads.push(upload)
130
+ console.error(
131
+ `window: ${name ?? directory} refused (${upload.code ?? `exit ${String(result.status)}`}); resume from it on a fresh code`,
132
+ )
133
+ break
134
+ }
135
+ console.error(`window: accepted ${name ?? '?'}@${version ?? '?'}`)
136
+ uploads.push(upload)
137
+ }
138
+ const confirmed: Upload[] = []
139
+ for (const upload of uploads) {
140
+ if (!upload.accepted || upload.name === undefined || upload.version === undefined) {
141
+ confirmed.push(upload)
142
+ continue
143
+ }
144
+ confirmed.push({
145
+ ...upload,
146
+ confirmed: await confirmServed(upload.name, upload.version, waitSeconds),
147
+ })
148
+ }
149
+ return confirmed
150
+ }
151
+
152
+ async function main(argv: readonly string[]): Promise<number> {
153
+ const missing = readMissingFlags(argv, ['--wait', '--otp'])
154
+ if (missing.length > 0) {
155
+ console.error(`window: ${missing.join(', ')} given with no value`)
156
+ return 64
157
+ }
158
+ const wait = Number(readOption(argv, '--wait') ?? 120)
159
+ const modes = ['--whoami', '--login', '--publish', '--confirm'].filter((flag) =>
160
+ argv.includes(flag),
161
+ )
162
+ if (modes.length > 1 || (argv.includes('--wait') && (!Number.isFinite(wait) || wait < 0))) {
163
+ console.error(
164
+ 'usage: window.ts names one mode (--whoami | --login | --publish | --confirm), and --wait takes a number of seconds',
165
+ )
166
+ return 64
167
+ }
168
+ if (argv.includes('--whoami')) {
169
+ const user = await pollWhoami(argv.includes('--wait') ? wait : 0)
170
+ console.log(JSON.stringify({ mode: 'whoami', user, live: user !== undefined }))
171
+ return user === undefined ? 3 : 0
172
+ }
173
+ if (argv.includes('--login')) {
174
+ const command =
175
+ process.platform === 'win32'
176
+ ? 'npm login --browser=false'
177
+ : "script -qfc 'npm login --browser=false' tmp/units/npm-login.log"
178
+ console.error(
179
+ `window: run this in a real terminal, then approve the URL it prints on its first line: ${command}`,
180
+ )
181
+ const user = await pollWhoami(argv.includes('--wait') ? wait : 300)
182
+ console.log(JSON.stringify({ mode: 'login', command, user, live: user !== undefined }))
183
+ return user === undefined ? 3 : 0
184
+ }
185
+ if (argv.includes('--publish')) {
186
+ const directories = readList(argv, '--publish')
187
+ const otp = readOption(argv, '--otp')
188
+ if (
189
+ directories.length === 0 ||
190
+ otp === undefined ||
191
+ !/^\d{6,8}$/u.test(otp) ||
192
+ directories.some((directory) => !existsSync(join(directory, 'package.json')))
193
+ ) {
194
+ console.error(
195
+ "usage: window.ts --publish DIR... --otp CODE [--wait SECONDS] [--json]; every DIR carries a package.json and CODE is the account's one-time code",
196
+ )
197
+ return 64
198
+ }
199
+ // A stored credential expires mid-session, so the session-start answer does not hold here.
200
+ if (readWhoami() === undefined) {
201
+ console.error(
202
+ 'window: npm whoami answers nothing immediately before the upload; run --login first',
203
+ )
204
+ return 3
205
+ }
206
+ const uploads = await publishAll(directories, otp, wait)
207
+ if (argv.includes('--json')) console.log(JSON.stringify({ mode: 'publish', uploads }))
208
+ else {
209
+ for (const upload of uploads) {
210
+ console.log(
211
+ `window: ${upload.directory}\t${upload.name ?? '?'}@${upload.version ?? '?'}\t${upload.accepted ? 'accepted' : `refused ${upload.code ?? String(upload.exit)}`}\t${upload.confirmed ? 'confirmed' : 'unconfirmed'}\t${upload.journal}`,
212
+ )
213
+ }
214
+ }
215
+ return uploads.every((upload) => upload.accepted && upload.confirmed) ? 0 : 3
216
+ }
217
+ if (argv.includes('--confirm')) {
218
+ const targets = readList(argv, '--confirm')
219
+ if (targets.length === 0 || targets.some((target) => !/^@?[^@]+@\S+$/u.test(target))) {
220
+ console.error('usage: window.ts --confirm NAME@VERSION... [--wait SECONDS]')
221
+ return 64
222
+ }
223
+ const readings: Array<{ readonly target: string; readonly served: boolean }> = []
224
+ for (const target of targets) {
225
+ const at = target.lastIndexOf('@')
226
+ readings.push({
227
+ target,
228
+ served: await confirmServed(target.slice(0, at), target.slice(at + 1), wait),
229
+ })
230
+ }
231
+ console.log(JSON.stringify({ mode: 'confirm', readings }))
232
+ return readings.every((reading) => reading.served) ? 0 : 3
233
+ }
234
+ console.error(
235
+ 'usage: window.ts --whoami [--wait SECONDS] | --login [--wait SECONDS] | --publish DIR... --otp CODE [--json] | --confirm NAME@VERSION... [--wait SECONDS]',
236
+ )
237
+ return 64
238
+ }
239
+
240
+ main(process.argv.slice(2)).then((code) => {
241
+ process.exitCode = code
242
+ })
@@ -0,0 +1,28 @@
1
+ ---
2
+ name: orkestrel-scout
3
+ description: >-
4
+ Map terrain mechanically before anyone reads it: file trees with line counts, Markdown headings, export lines, a term census, the files that name a path, and unresolved `§` cross-references, written once and handed to the lane that reads. Use before writing a brief, before a Grok absorption lane, before a rename or a sweep, when a dispatch names the `scout` role, or whenever a question is "where is", "how big is", "what names this", or "which files carry this term".
5
+ ---
6
+
7
+ # Scout
8
+
9
+ A map is mechanical; reading is not. Run the map first, hand it to the reader, and let the reader spend its context on judgment.
10
+
11
+ ## Scripts
12
+
13
+ | Script | Does |
14
+ | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
15
+ | `scripts/map.ts` | `--tree PATH...` files with line counts and sizes; `--headings PATH...` Markdown headings with lines; `--frontmatter PATH...` every key of a frontmatter block, a TOML file, or a YAML file with its whole value, nested keys dotted (`interface.default_prompt`); `--exports PATH...` `export` lines in TypeScript files (a text reading); `--census TERM...` every line carrying a term; `--refs PATH` every line naming a path or its basename; `--anchors PATH...` every `§ Heading` reference whose heading its target lacks. `--paths a,b` narrows a census or refs walk; `--ignore-case`; `--json`; `--out FILE` writes the map instead of printing it. Walks skip `node_modules`, `.git`, `dist`, `tmp`, `.orkestrel`, `.idea`, and `host.json`. |
16
+
17
+ ## Map
18
+
19
+ 1. Name the question and the roots it covers. Refuse an unbounded one.
20
+ 2. Run `node .agents/skills/orkestrel-scout/scripts/map.ts` with the modes the question needs, in one call. Write the map to `tmp/units/<unit>-map.txt` (or `.json`) when a lane will read it; print it when you will.
21
+ 3. For a rename or a deletion, run `--refs <path>` and `--census <name>` and own every file that comes back.
22
+ 4. Before an absorption lane, put the map's path in the brief and strike from the brief every question the map already answers; the lane reads and judges, it does not enumerate.
23
+ 5. Before a brief, read `--anchors` over the files the brief names; repair the references that block the brief and report the rest.
24
+
25
+ ## Refuse
26
+
27
+ - A question that needs reading at depth or a judgment: route it to `grok`, `distiller`, or the reviewer the size gate names, with the map attached.
28
+ - A census over a term shorter than three characters without `--paths`.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: 'Scout Orkestrel Terrain'
3
+ short_description: 'Map files, headings, exports, terms, and references mechanically'
4
+ default_prompt: 'Use $orkestrel-scout to map the terrain a brief or a lane needs before anyone reads it.'
@@ -0,0 +1,352 @@
1
+ // Map terrain before a brief, an absorption lane, or a rename. Run from the checkout root:
2
+ // node .agents/skills/orkestrel-scout/scripts/map.ts --tree PATH... [--headings PATH...] [--frontmatter PATH...] [--exports PATH...] [--census TERM...] [--refs PATH] [--anchors PATH...] [--paths a,b] [--ignore-case] [--json] [--out FILE]
3
+ // --tree lists every file under each path with its line count and size. --headings lists every
4
+ // Markdown heading with its line. --frontmatter lists every key of a Markdown frontmatter block, a
5
+ // TOML file, or a YAML file with its whole value, a nested key dotted under its parents
6
+ // (`interface.default_prompt`) and a TOML key under its table. --exports lists every `export`
7
+ // declaration line in a TypeScript file (a text reading, not the compiler's). --census lists every line carrying a term. --refs lists
8
+ // every line naming a path or its basename. --anchors lists every `§ Heading` reference whose
9
+ // heading the named file, or the same file, does not carry. Walks skip node_modules, .git, dist,
10
+ // tmp, .orkestrel, .idea, and host.json; --paths narrows a census or refs walk. Modes combine, each
11
+ // printing its section; --json prints one object; --out writes the output to FILE instead of the
12
+ // terminal. Give each flag once; a later duplicate is not read. Exit 0, or 64 when no mode is
13
+ // given or --refs names no path.
14
+ import { existsSync, readFileSync, statSync, mkdirSync, writeFileSync } from 'node:fs'
15
+ import { basename, dirname } from 'node:path'
16
+ import {
17
+ listFiles,
18
+ normalizeSlashes,
19
+ readList,
20
+ readMissingFlags,
21
+ readOption,
22
+ } from '../../orkestrel-dispatch/scripts/helpers.ts'
23
+
24
+ const EXPORT =
25
+ /^export\s+(?:default\s+)?(?:async\s+)?(?:abstract\s+)?(function\*?|const|let|class|interface|type|enum|namespace)\s+([A-Za-z_$][\w$]*)/u
26
+ const HEADING = /^(#{1,6})\s+(.+?)\s*#*\s*$/u
27
+ const ANCHOR =
28
+ /(?:`([^`\n]+\.md)`\s*)?§\s*"?([^"`\n|;,.)]+?)"?(?=\s*(?:[,.;)|]|\s(?:and|or|names|fixes|owns|states|binds|governs|decides|before|after)\b|$))/gu
29
+ const BINARY = /\.(png|jpg|jpeg|gif|webp|ico|woff2?|ttf|otf|eot|zip|tgz|gz|pdf|wasm)$/u
30
+
31
+ interface TreeRow {
32
+ readonly path: string
33
+ readonly lines: number
34
+ readonly bytes: number
35
+ }
36
+
37
+ interface LineRow {
38
+ readonly path: string
39
+ readonly line: number
40
+ readonly text: string
41
+ }
42
+
43
+ interface TermRows {
44
+ readonly term: string
45
+ readonly rows: readonly LineRow[]
46
+ }
47
+
48
+ interface AnchorRow {
49
+ readonly path: string
50
+ readonly line: number
51
+ readonly target: string
52
+ readonly heading: string
53
+ }
54
+
55
+ interface Report {
56
+ readonly tree?: readonly TreeRow[]
57
+ readonly headings?: readonly LineRow[]
58
+ readonly frontmatter?: readonly LineRow[]
59
+ readonly exports?: readonly LineRow[]
60
+ readonly census?: readonly TermRows[]
61
+ readonly refs?: readonly LineRow[]
62
+ readonly anchors?: readonly AnchorRow[]
63
+ }
64
+
65
+ function isText(path: string): boolean {
66
+ return !BINARY.test(path) && basename(path) !== 'host.json'
67
+ }
68
+
69
+ function expandPaths(paths: readonly string[]): readonly string[] {
70
+ return paths.flatMap((path) => {
71
+ if (!existsSync(path)) return []
72
+ if (statSync(path).isDirectory()) return listFiles(path).map((file) => file.path)
73
+ return [normalizeSlashes(path)]
74
+ })
75
+ }
76
+
77
+ function readLines(path: string): readonly string[] {
78
+ return readFileSync(path, 'utf8').split(/\r\n|\n/)
79
+ }
80
+
81
+ function mapTree(paths: readonly string[]): readonly TreeRow[] {
82
+ return expandPaths(paths)
83
+ .filter(isText)
84
+ .map((path) => {
85
+ const text = readFileSync(path, 'utf8')
86
+ return {
87
+ path,
88
+ lines: text === '' ? 0 : text.split(/\r\n|\n/).length,
89
+ bytes: Buffer.byteLength(text),
90
+ }
91
+ })
92
+ .sort((left, right) => left.path.localeCompare(right.path))
93
+ }
94
+
95
+ function mapHeadings(paths: readonly string[]): readonly LineRow[] {
96
+ const rows: LineRow[] = []
97
+ for (const path of expandPaths(paths).filter((candidate) => candidate.endsWith('.md'))) {
98
+ let fenced = false
99
+ readLines(path).forEach((text, index) => {
100
+ if (text.startsWith('```')) fenced = !fenced
101
+ if (fenced) return
102
+ const match = text.match(HEADING)
103
+ if (match?.[1] !== undefined && match[2] !== undefined) {
104
+ rows.push({ path, line: index + 1, text: `${match[1]} ${match[2]}` })
105
+ }
106
+ })
107
+ }
108
+ return rows
109
+ }
110
+
111
+ function mapExports(paths: readonly string[]): readonly LineRow[] {
112
+ const rows: LineRow[] = []
113
+ for (const path of expandPaths(paths).filter((candidate) => /\.[cm]?ts$/u.test(candidate))) {
114
+ readLines(path).forEach((text, index) => {
115
+ const match = text.match(EXPORT)
116
+ if (match?.[1] !== undefined && match[2] !== undefined) {
117
+ rows.push({ path, line: index + 1, text: `${match[1]} ${match[2]}` })
118
+ } else if (/^export\s+\*\s+from\s+/u.test(text)) {
119
+ rows.push({ path, line: index + 1, text: text.trim() })
120
+ }
121
+ })
122
+ }
123
+ return rows
124
+ }
125
+
126
+ function buildMatcher(term: string, ignoreCase: boolean): (text: string) => boolean {
127
+ const needle = ignoreCase ? term.toLowerCase() : term
128
+ return (text) => (ignoreCase ? text.toLowerCase() : text).includes(needle)
129
+ }
130
+
131
+ function mapCensus(
132
+ terms: readonly string[],
133
+ roots: readonly string[],
134
+ ignoreCase: boolean,
135
+ ): readonly TermRows[] {
136
+ const files = expandPaths(roots).filter(isText)
137
+ return terms.map((term) => {
138
+ const matches = buildMatcher(term, ignoreCase)
139
+ const rows: LineRow[] = []
140
+ for (const path of files) {
141
+ readLines(path).forEach((text, index) => {
142
+ if (matches(text)) rows.push({ path, line: index + 1, text: text.trim().slice(0, 160) })
143
+ })
144
+ }
145
+ return { term, rows }
146
+ })
147
+ }
148
+
149
+ function mapRefs(target: string, roots: readonly string[]): readonly LineRow[] {
150
+ const needle = normalizeSlashes(target)
151
+ const name = basename(needle)
152
+ const rows: LineRow[] = []
153
+ for (const path of expandPaths(roots).filter(isText)) {
154
+ if (path === needle) continue
155
+ readLines(path).forEach((text, index) => {
156
+ if (text.includes(needle) || text.includes(name)) {
157
+ rows.push({ path, line: index + 1, text: text.trim().slice(0, 160) })
158
+ }
159
+ })
160
+ }
161
+ return rows
162
+ }
163
+
164
+ function readYamlKeys(path: string, lines: readonly string[], offset: number): readonly LineRow[] {
165
+ const rows: LineRow[] = []
166
+ const stack: Array<{ readonly indent: number; readonly key: string }> = []
167
+ lines.forEach((text, index) => {
168
+ const content = text.trimStart()
169
+ if (content === '' || content.startsWith('#')) return
170
+ const indent = text.length - content.length
171
+ const match = content.match(/^([A-Za-z_][\w-]*)\s*:(?:\s+(.*))?$/u)
172
+ if (match?.[1] === undefined) {
173
+ // A list item, a block scalar line, or a wrapped value extends the key before it.
174
+ const last = rows.at(-1)
175
+ if (last !== undefined && indent >= (stack.at(-1)?.indent ?? 0)) {
176
+ rows[rows.length - 1] = { ...last, text: `${last.text} ${content.trim()}`.trimEnd() }
177
+ }
178
+ return
179
+ }
180
+ while (stack.length > 0 && (stack.at(-1)?.indent ?? 0) >= indent) stack.pop()
181
+ const key = [...stack.map((entry) => entry.key), match[1]].join('.')
182
+ stack.push({ indent, key: match[1] })
183
+ rows.push({ path, line: index + offset, text: `${key}: ${(match[2] ?? '').trim()}`.trimEnd() })
184
+ })
185
+ return rows
186
+ }
187
+
188
+ function readTomlKeys(path: string, lines: readonly string[], offset: number): readonly LineRow[] {
189
+ const rows: LineRow[] = []
190
+ let table = ''
191
+ let fence: string | undefined
192
+ lines.forEach((text, index) => {
193
+ if (fence !== undefined) {
194
+ // The body of a multi-line string carries no key.
195
+ if (text.includes(fence)) fence = undefined
196
+ return
197
+ }
198
+ const header = text.match(/^\s*\[\[?([^\]]+)\]\]?\s*$/u)
199
+ if (header?.[1] !== undefined) {
200
+ table = header[1].trim()
201
+ return
202
+ }
203
+ const match = text.match(/^\s*([A-Za-z0-9_.-]+|"[^"]+")\s*=\s*(.*)$/u)
204
+ if (match?.[1] === undefined) return
205
+ const value = (match[2] ?? '').trim()
206
+ const opening = value.match(/^("""|''')/u)?.[1]
207
+ if (opening !== undefined && value.indexOf(opening, 3) === -1) fence = opening
208
+ rows.push({
209
+ path,
210
+ line: index + offset,
211
+ text: `${table === '' ? '' : `${table}.`}${match[1]}: ${value}`.trimEnd(),
212
+ })
213
+ })
214
+ return rows
215
+ }
216
+
217
+ function mapFrontmatter(paths: readonly string[]): readonly LineRow[] {
218
+ const rows: LineRow[] = []
219
+ for (const path of expandPaths(paths).filter((candidate) =>
220
+ /\.(md|mdc|toml|ya?ml)$/u.test(candidate),
221
+ )) {
222
+ const lines = readLines(path)
223
+ const markdown = /\.mdc?$/u.test(path)
224
+ const end = markdown ? lines.indexOf('---', 1) : lines.length
225
+ if (markdown && (lines[0] !== '---' || end === -1)) continue
226
+ const body = lines.slice(markdown ? 1 : 0, end)
227
+ const offset = markdown ? 2 : 1
228
+ rows.push(
229
+ ...(path.endsWith('.toml')
230
+ ? readTomlKeys(path, body, offset)
231
+ : readYamlKeys(path, body, offset)),
232
+ )
233
+ }
234
+ return rows
235
+ }
236
+
237
+ function readHeadingTexts(path: string): ReadonlySet<string> {
238
+ return new Set(mapHeadings([path]).map((row) => row.text.replace(/^#+\s+/u, '').toLowerCase()))
239
+ }
240
+
241
+ function mapAnchors(paths: readonly string[]): readonly AnchorRow[] {
242
+ const headings = new Map<string, ReadonlySet<string>>()
243
+ const rows: AnchorRow[] = []
244
+ for (const path of expandPaths(paths).filter((candidate) => candidate.endsWith('.md'))) {
245
+ readLines(path).forEach((text, index) => {
246
+ for (const match of text.matchAll(ANCHOR)) {
247
+ const target = match[1] === undefined ? path : normalizeSlashes(match[1])
248
+ const heading = (match[2] ?? '').trim()
249
+ if (heading === '') continue
250
+ if (!existsSync(target)) {
251
+ rows.push({ path, line: index + 1, target, heading })
252
+ continue
253
+ }
254
+ const known = headings.get(target) ?? readHeadingTexts(target)
255
+ headings.set(target, known)
256
+ if (!known.has(heading.toLowerCase())) rows.push({ path, line: index + 1, target, heading })
257
+ }
258
+ })
259
+ }
260
+ return rows
261
+ }
262
+
263
+ function renderReport(report: Report): string {
264
+ const lines: string[] = []
265
+ if (report.tree !== undefined) {
266
+ lines.push('## Tree')
267
+ for (const row of report.tree) lines.push(`${row.path}\t${row.lines} lines\t${row.bytes} bytes`)
268
+ lines.push(
269
+ `total\t${report.tree.length} files\t${report.tree.reduce((sum, row) => sum + row.lines, 0)} lines`,
270
+ )
271
+ }
272
+ if (report.headings !== undefined) {
273
+ lines.push('## Headings')
274
+ for (const row of report.headings) lines.push(`${row.path}:${row.line}\t${row.text}`)
275
+ }
276
+ if (report.frontmatter !== undefined) {
277
+ lines.push('## Frontmatter')
278
+ for (const row of report.frontmatter) lines.push(`${row.path}:${row.line} ${row.text}`)
279
+ }
280
+ if (report.exports !== undefined) {
281
+ lines.push('## Exports (text reading)')
282
+ for (const row of report.exports) lines.push(`${row.path}:${row.line}\t${row.text}`)
283
+ }
284
+ if (report.census !== undefined) {
285
+ for (const entry of report.census) {
286
+ lines.push(`## Census: ${entry.term} (${entry.rows.length})`)
287
+ for (const row of entry.rows) lines.push(`${row.path}:${row.line}\t${row.text}`)
288
+ }
289
+ }
290
+ if (report.refs !== undefined) {
291
+ lines.push(`## Refs (${report.refs.length})`)
292
+ for (const row of report.refs) lines.push(`${row.path}:${row.line}\t${row.text}`)
293
+ }
294
+ if (report.anchors !== undefined) {
295
+ lines.push(`## Unresolved anchors (${report.anchors.length})`)
296
+ for (const row of report.anchors)
297
+ lines.push(`${row.path}:${row.line}\t${row.target} § ${row.heading}`)
298
+ }
299
+ return `${lines.join('\n')}\n`
300
+ }
301
+
302
+ function main(argv: readonly string[]): number {
303
+ const missing = readMissingFlags(argv, ['--refs', '--paths', '--out'])
304
+ if (missing.length > 0) {
305
+ console.error(`map: ${missing.join(', ')} given with no value`)
306
+ return 64
307
+ }
308
+ const roots = (readOption(argv, '--paths') ?? '.').split(',')
309
+ const ignoreCase = argv.includes('--ignore-case')
310
+ const tree = readList(argv, '--tree')
311
+ const headings = readList(argv, '--headings')
312
+ const frontmatter = readList(argv, '--frontmatter')
313
+ const exports = readList(argv, '--exports')
314
+ const census = readList(argv, '--census')
315
+ const refs = readOption(argv, '--refs')
316
+ const anchors = readList(argv, '--anchors')
317
+ if (
318
+ tree.length +
319
+ headings.length +
320
+ frontmatter.length +
321
+ exports.length +
322
+ census.length +
323
+ anchors.length ===
324
+ 0 &&
325
+ refs === undefined
326
+ ) {
327
+ console.error(
328
+ 'usage: map.ts [--tree PATH...] [--headings PATH...] [--frontmatter PATH...] [--exports PATH...] [--census TERM...] [--refs PATH] [--anchors PATH...] [--paths a,b] [--ignore-case] [--json] [--out FILE]',
329
+ )
330
+ return 64
331
+ }
332
+ const report: Report = {
333
+ ...(tree.length > 0 ? { tree: mapTree(tree) } : {}),
334
+ ...(headings.length > 0 ? { headings: mapHeadings(headings) } : {}),
335
+ ...(frontmatter.length > 0 ? { frontmatter: mapFrontmatter(frontmatter) } : {}),
336
+ ...(exports.length > 0 ? { exports: mapExports(exports) } : {}),
337
+ ...(census.length > 0 ? { census: mapCensus(census, roots, ignoreCase) } : {}),
338
+ ...(refs === undefined ? {} : { refs: mapRefs(refs, roots) }),
339
+ ...(anchors.length > 0 ? { anchors: mapAnchors(anchors) } : {}),
340
+ }
341
+ const output = argv.includes('--json') ? `${JSON.stringify(report)}\n` : renderReport(report)
342
+ const out = readOption(argv, '--out')
343
+ if (out === undefined) process.stdout.write(output)
344
+ else {
345
+ mkdirSync(dirname(out), { recursive: true })
346
+ writeFileSync(out, output)
347
+ console.log(`map: wrote ${out}`)
348
+ }
349
+ return 0
350
+ }
351
+
352
+ process.exitCode = main(process.argv.slice(2))