@orkestrel/scaffold 0.0.1

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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +114 -0
  3. package/dist/bin/scaffold.js +1539 -0
  4. package/dist/bin/scaffold.js.map +1 -0
  5. package/dist/host/AGENTS.md +939 -0
  6. package/dist/host/CLAUDE.md +495 -0
  7. package/dist/host/LICENSE +21 -0
  8. package/dist/host/claude/agents/builder.md +48 -0
  9. package/dist/host/claude/agents/checker.md +37 -0
  10. package/dist/host/claude/agents/composer.md +64 -0
  11. package/dist/host/claude/agents/grok.md +50 -0
  12. package/dist/host/claude/agents/orkestrel.md +236 -0
  13. package/dist/host/claude/agents/planner.md +44 -0
  14. package/dist/host/claude/agents/researcher.md +38 -0
  15. package/dist/host/claude/agents/reviewer.md +47 -0
  16. package/dist/host/claude/agents/scout.md +35 -0
  17. package/dist/host/claude/agents/verifier.md +34 -0
  18. package/dist/host/claude/settings.json +26 -0
  19. package/dist/host/dotfiles/editorconfig +17 -0
  20. package/dist/host/dotfiles/gitattributes +3 -0
  21. package/dist/host/dotfiles/gitignore +40 -0
  22. package/dist/host/dotfiles/oxfmtrc.json +18 -0
  23. package/dist/host/dotfiles/oxlintignore +20 -0
  24. package/dist/host/dotfiles/oxlintrc.json +58 -0
  25. package/dist/host/dotfiles/prettierignore +5 -0
  26. package/dist/host/github/workflows/ci.yml +64 -0
  27. package/dist/host/guides/src/guide.md +312 -0
  28. package/dist/host/guides/src/scaffold.md +2152 -0
  29. package/dist/host/manifest.json +137 -0
  30. package/dist/host/scripts/cursor.sh +74 -0
  31. package/dist/host/scripts/deps.sh +38 -0
  32. package/dist/host/scripts/ollama.sh +163 -0
  33. package/dist/src/core/index.cjs +3728 -0
  34. package/dist/src/core/index.cjs.map +1 -0
  35. package/dist/src/core/index.d.cts +1941 -0
  36. package/dist/src/core/index.d.ts +1941 -0
  37. package/dist/src/core/index.js +3636 -0
  38. package/dist/src/core/index.js.map +1 -0
  39. package/dist/src/server/index.cjs +1595 -0
  40. package/dist/src/server/index.cjs.map +1 -0
  41. package/dist/src/server/index.d.cts +779 -0
  42. package/dist/src/server/index.d.ts +779 -0
  43. package/dist/src/server/index.js +1572 -0
  44. package/dist/src/server/index.js.map +1 -0
  45. package/package.json +113 -0
@@ -0,0 +1,779 @@
1
+ import { Audit } from '../core/index.ts';
2
+ import { Blueprint } from '../core/index.ts';
3
+ import { CatalogEntry } from '../core/index.ts';
4
+ import { Dependency } from '../core/index.ts';
5
+ import { EmitterErrorHandler } from '@orkestrel/emitter';
6
+ import { EmitterHooks } from '@orkestrel/emitter';
7
+ import { EmitterInterface } from '@orkestrel/emitter';
8
+ import { GuideSync } from '../core/index.ts';
9
+ import { Plan } from '../core/index.ts';
10
+ import { SyncReport } from '../core/index.ts';
11
+ import { VersionSync } from '../core/index.ts';
12
+
13
+ /**
14
+ * Build the fleet package catalog — one `CatalogEntry` per `@orkestrel/*`
15
+ * package discovered under each root, its description drawn from its own
16
+ * guide's FIRST blockquote.
17
+ *
18
+ * @param roots - The fleet root directories to scan (each walked via `discoverPackages`).
19
+ * @remarks
20
+ * Per discovered package directory: `name` / `version` come from its own
21
+ * `package.json`; `description` is the first paragraph of the guide's opening
22
+ * blockquote — the flattened text of the FIRST `ParagraphNode` among the
23
+ * FIRST `BlockquoteNode` found's (depth-first, pre-order, via `walkNodes`)
24
+ * TOP-LEVEL children in its `guides/src/<short>.md` (`<short>` = `name` with
25
+ * the `@orkestrel/` prefix stripped), parsed with `@orkestrel/markdown`'s
26
+ * `parseDocument` — a multi-paragraph blockquote overview yields only its
27
+ * FIRST paragraph, never the whole quote glued together; embedded newlines
28
+ * collapse to single spaces, and surrounding whitespace trims. A
29
+ * missing/unreadable guide, a guide carrying no blockquote, or a blockquote
30
+ * with no top-level paragraph child, yields `description: ''`, never a
31
+ * thrown error. Entries merge across `roots` (a later root's entry for a
32
+ * repeated `name` wins), then code-unit sort by `name`. An unreadable ROOT
33
+ * itself is NOT wrapped here — whatever `discoverPackages` throws for it
34
+ * propagates as-is; the bin layer is responsible for coding that failure
35
+ * `TARGET`.
36
+ * @returns The merged, sorted `CatalogEntry[]`.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * import { catalogPackages } from '@orkestrel/scaffold/server'
41
+ *
42
+ * catalogPackages(['/repos']) // [{ name: '@orkestrel/contract', version: '0.0.5', description: '…' }, …]
43
+ * ```
44
+ */
45
+ export declare function catalogPackages(roots: readonly string[]): readonly CatalogEntry[];
46
+
47
+ /**
48
+ * Create a `MaterializerInterface` (server) — the materialization entity,
49
+ * seeded from `MaterializerOptions`.
50
+ *
51
+ * @param options - Optional `host` root override, emitter hooks, and error handler
52
+ * @returns A {@link MaterializerInterface}
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * import { createMaterializer } from '@orkestrel/scaffold/server'
57
+ *
58
+ * const materializer = createMaterializer()
59
+ * materializer.destroy()
60
+ * ```
61
+ */
62
+ export declare function createMaterializer(options?: MaterializerOptions): MaterializerInterface;
63
+
64
+ /**
65
+ * Create a `SyncInterface` (server) — the upstream-synchronization entity,
66
+ * seeded from `SyncOptions`.
67
+ *
68
+ * @param options - Optional endpoint bases/branch, concurrency, retries, strict, emitter hooks, and error handler
69
+ * @returns A {@link SyncInterface}
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * import { createSync } from '@orkestrel/scaffold/server'
74
+ *
75
+ * const sync = createSync()
76
+ * sync.destroy()
77
+ * ```
78
+ */
79
+ export declare function createSync(options?: SyncOptions): SyncInterface;
80
+
81
+ /**
82
+ * Reconstruct a `Blueprint` from an EXISTING repo at `target` — the faithful
83
+ * inverse `audit` / `repair` / `mirror` need to diff a live package against
84
+ * its own would-be scaffold, rather than a fresh, dependency-less stand-in.
85
+ *
86
+ * @param target - The existing package directory to derive a `Blueprint` from.
87
+ * @remarks
88
+ * `name` strips the `@orkestrel/` prefix off `manifest.name` — a non-`@orkestrel`
89
+ * name is a coded `TARGET` failure, since this tool derives only `@orkestrel`
90
+ * packages. `surfaces` is read off the LIVE line: every surface the package
91
+ * carries has a `src/<surface>/` directory, so each of `'core' | 'browser' |
92
+ * 'server'` is included iff that directory exists at `target`; a target with
93
+ * NONE of the three is also a coded `TARGET` failure. `dependencies` /
94
+ * `peers` are the `@orkestrel/`-prefixed entries of `manifest.dependencies` /
95
+ * `manifest.peerDependencies` (a peer flagged `peerDependenciesMeta[name]
96
+ * .optional === true` carries `optional: true`). `extras` is EVERY entry of
97
+ * `manifest.devDependencies` (not only `@orkestrel/`-prefixed ones — an
98
+ * external extra like `zod` must round-trip too), EXCLUDING the generated
99
+ * devDependency baseline (`devDependenciesFor([])`'s keys, which already
100
+ * cover `@orkestrel/guide` and `@orkestrel/scaffold`) every scaffolded
101
+ * package already carries, never a package-specific extra. A devDependency
102
+ * ALSO present in
103
+ * `manifest.peerDependencies` or `manifest.dependencies` (e.g. a peer
104
+ * dev-installed for local testing) is likewise excluded from `extras` — it
105
+ * already surfaces as a `peer`/`dependency` above, and double-counting it as
106
+ * an `extra` would land it in `peers ∩ extras`, a blocking `validateBlueprint`
107
+ * gate. `overrides` is always `[]` — derivation cannot know a caller's
108
+ * template-override intent.
109
+ * @returns The reconstructed `Blueprint`.
110
+ * @throws `ScaffoldError('TARGET', …)` when `target`'s manifest is unreadable
111
+ * (via `readManifest`), is not valid JSON, its `name` is not `@orkestrel`-
112
+ * prefixed, or `target` carries none of the three surface directories.
113
+ *
114
+ * @example
115
+ * ```ts
116
+ * import { deriveBlueprint } from '@orkestrel/scaffold/server'
117
+ *
118
+ * deriveBlueprint('./packages/router') // { name: 'router', surfaces: ['core', 'server'], … }
119
+ * ```
120
+ */
121
+ export declare function deriveBlueprint(target: string): Blueprint;
122
+
123
+ /**
124
+ * List a fleet root's `@orkestrel/*` package directories.
125
+ *
126
+ * @param root - The fleet root directory to scan.
127
+ * @returns Absolute, code-unit-sorted paths of `root`'s immediate child
128
+ * directories whose `package.json` parses and whose `name` starts with
129
+ * `@orkestrel/`. A child with an unreadable or unparsable `package.json`,
130
+ * or a non-`@orkestrel` name, is skipped silently — it simply is not a
131
+ * fleet member.
132
+ *
133
+ * @example
134
+ * ```ts
135
+ * import { discoverPackages } from '@orkestrel/scaffold/server'
136
+ *
137
+ * discoverPackages('./packages') // ['/abs/packages/router', '/abs/packages/budget']
138
+ * ```
139
+ */
140
+ export declare function discoverPackages(root: string): readonly string[];
141
+
142
+ /**
143
+ * Locate this MODULE's own installed package root — the nearest ancestor of
144
+ * `import.meta.url` holding a `package.json` — and return its vendored
145
+ * `dist/host` data root. THE single source of truth for the default
146
+ * `Materializer` / `scaffold` bin host: once installed, walking up from the
147
+ * module's own file (not `process.cwd()`, which points at whichever project
148
+ * happens to be running) resolves to `node_modules/@orkestrel/scaffold`, the
149
+ * correct default host — the package ships its vendored data with itself.
150
+ * `dist/host` may not exist yet when this resolves from SOURCE under a test
151
+ * runner; that is fine — existence is checked at the point of use, not here.
152
+ *
153
+ * @returns The absolute vendored `dist/host` path.
154
+ * @throws `ScaffoldError('TARGET', …)` when no ancestor of this module's own
155
+ * location holds a `package.json`.
156
+ *
157
+ * @example
158
+ * ```ts
159
+ * import { hostRoot } from '@orkestrel/scaffold/server'
160
+ *
161
+ * hostRoot() // '/…/node_modules/@orkestrel/scaffold/dist/host'
162
+ * ```
163
+ */
164
+ export declare function hostRoot(): string;
165
+
166
+ /**
167
+ * Rehydrate a `Plan`'s `host`-origin artifacts with their real byte content
168
+ * read from `host` — manifest-aware, via `locateHostSource`.
169
+ *
170
+ * @param plan - The plan to hydrate.
171
+ * @param host - The resolved host root to read from.
172
+ * @returns A new `Plan` whose file-shaped `host` artifacts carry `content`;
173
+ * `template` / `computed` artifacts and directory-shaped `host` artifacts
174
+ * (no single storage file to read) pass through untouched.
175
+ * @throws `ScaffoldError('TARGET', …)` when a resolved, existing host source
176
+ * file fails to read.
177
+ *
178
+ * @example
179
+ * ```ts
180
+ * import { hydratePlan } from '@orkestrel/scaffold/server'
181
+ *
182
+ * const hydrated = hydratePlan(plan, './dist/host')
183
+ * ```
184
+ */
185
+ export declare function hydratePlan(plan: Plan, host: string): Plan;
186
+
187
+ /**
188
+ * Whether `value` is a well-formed `host/manifest.json` entry — a string
189
+ * `storage`, a string `destination`, and a boolean `executable`.
190
+ *
191
+ * @param value - The candidate raw manifest entry.
192
+ * @returns `true` when `value` narrows to `ManifestEntry`.
193
+ *
194
+ * @example
195
+ * ```ts
196
+ * import { isManifestEntry } from '@orkestrel/scaffold/server'
197
+ *
198
+ * isManifestEntry({ storage: 'a', destination: 'b', executable: false }) // true
199
+ * isManifestEntry({ storage: 'a', destination: 'b' }) // false — missing `executable`
200
+ * ```
201
+ */
202
+ export declare function isManifestEntry(value: unknown): value is ManifestEntry;
203
+
204
+ /**
205
+ * Whether `value` is a plain object (not `null`, not an array).
206
+ *
207
+ * @param value - The candidate value.
208
+ * @returns `true` when `value` narrows to `Record<string, unknown>`.
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * import { isRecord } from '@orkestrel/scaffold/server'
213
+ *
214
+ * isRecord({ a: 1 }) // true
215
+ * isRecord(null) // false
216
+ * ```
217
+ */
218
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
219
+
220
+ /**
221
+ * Whether a target path is absent, empty, or contains nothing but a `.git`
222
+ * directory — the green-field target law `Materializer.materialize` enforces.
223
+ *
224
+ * @param target - The candidate target directory path.
225
+ * @returns `true` when `target` is safe to materialize a fresh package into.
226
+ *
227
+ * @example
228
+ * ```ts
229
+ * import { isVacant } from '@orkestrel/scaffold/server'
230
+ *
231
+ * isVacant('./packages/router-new') // true — absent, empty, or only a .git dir
232
+ * ```
233
+ */
234
+ export declare function isVacant(target: string): boolean;
235
+
236
+ /**
237
+ * Recursively list a directory's files as root-relative paths.
238
+ *
239
+ * @param root - The directory to list.
240
+ * @returns Root-relative file paths (posix-style `/` separators), or `[]`
241
+ * when `root` is absent.
242
+ *
243
+ * @example
244
+ * ```ts
245
+ * import { listFiles } from '@orkestrel/scaffold/server'
246
+ *
247
+ * listFiles('./dist/host/.claude/agents') // ['scout.md', 'builder.md', …]
248
+ * ```
249
+ */
250
+ export declare function listFiles(root: string): readonly string[];
251
+
252
+ /**
253
+ * Resolve the absolute host-storage path for a host-origin artifact's
254
+ * `source`, manifest-aware.
255
+ *
256
+ * @param manifest - The host's parsed `manifest.json` entries, or `undefined`
257
+ * when the host carries none (raw-repo-root fallback).
258
+ * @param source - The artifact's `source` (or `path`) to resolve.
259
+ * @param host - The resolved host root the path is joined against.
260
+ * @returns `join(host, source)` when `manifest` is `undefined` (no vendored
261
+ * staging indirection); when `manifest` is present, `join(host,
262
+ * entries[0].storage)` for the SINGLE manifest entry whose `destination`
263
+ * equals `source`, or `undefined` when zero or more than one entry matches
264
+ * (`source` names a directory, or the manifest is ambiguous — no single
265
+ * storage file to point at).
266
+ *
267
+ * @example
268
+ * ```ts
269
+ * import { locateHostSource } from '@orkestrel/scaffold/server'
270
+ *
271
+ * locateHostSource(undefined, 'package.json', './dist/host') // './dist/host/package.json'
272
+ * locateHostSource([{ storage: 'pkg.tmpl', destination: 'package.json', executable: false }], 'package.json', './dist/host')
273
+ * // './dist/host/pkg.tmpl'
274
+ * ```
275
+ */
276
+ export declare function locateHostSource(manifest: readonly ManifestEntry[] | undefined, source: string, host: string): string | undefined;
277
+
278
+ /** One entry of the vendored host's `manifest.json` (server). */
279
+ export declare interface ManifestEntry {
280
+ readonly storage: string;
281
+ readonly destination: string;
282
+ readonly executable: boolean;
283
+ }
284
+
285
+ /**
286
+ * The materialization entity (server) — the only impure surface in the
287
+ * package, writing a `Plan` to `node:fs` behind an explicit call.
288
+ *
289
+ * @remarks
290
+ * `materialize` is green-field: it refuses any target `isVacant` rejects
291
+ * (`ScaffoldError('TARGET', …)`), then byte-copies each `host` artifact from
292
+ * the `host` root and writes each `template` / `computed` artifact's rendered
293
+ * `content`, failing fast on any write error (`ScaffoldError('WRITE', …)`).
294
+ * `repair` is into-existing: it skips the vacancy check and writes ONLY the
295
+ * `missing` / `stale` artifacts an `Audit` names, leaving `aligned` ones
296
+ * untouched. `prune` deletes stale files under `target/.claude/agents/` and
297
+ * `target/scripts/` that the vendored `host` no longer names — the retired
298
+ * `mirror.sh`/`scaffold.sh` cleanup step, now a method. After `destroy()`
299
+ * every method throws `DESTROYED`; teardown is idempotent, emitter last.
300
+ *
301
+ * @remarks
302
+ * `host`-origin copies are MANIFEST-AWARE: when the resolved `host` root
303
+ * carries a `manifest.json` (this package's own vendored `dist/host`), each
304
+ * artifact's `source` (a destination-relative path) is looked up in the
305
+ * manifest to find its un-dotted STORAGE path plus an `executable` bit
306
+ * (applied via `chmodSync` after the copy) — the vendored-package shape,
307
+ * where storage names avoid leading dots npm would otherwise mangle. When
308
+ * `host` carries no `manifest.json` (a caller-supplied raw repo root, e.g. a
309
+ * sibling checkout or a test fixture), `source` maps to `host` 1:1, exactly
310
+ * as before.
311
+ *
312
+ * @remarks
313
+ * A manifest-present `source` with ZERO matching entries degrades to a stub
314
+ * ONLY when that `source` is a dependency-guide pointer — one that starts
315
+ * with `guides/src/` and ends with `.md` (the `guides/src/<dep>.md` pointer
316
+ * `Compiler` emits for any dependency outside this package's vendored set).
317
+ * That is the ONLY zero-match case that is legitimate: every `HOST_PATHS`
318
+ * source is always staged by `stageHost`, so a non-guide zero-match means a
319
+ * corrupted or truncated `manifest.json`, not an intentionally-unvendored
320
+ * artifact. For a guide pointer, a short stub file is written at the
321
+ * destination and reported exactly like a successful copy, mirroring the
322
+ * READ path (`hydratePlan` leaves such artifacts `content`-undefined, so
323
+ * `diffPlan` audits them by PRESENCE only). For every OTHER zero-match, the
324
+ * fail-closed `ScaffoldError('TARGET', …)` is thrown — degrading an
325
+ * unscoped zero-match would otherwise let a corrupted manifest silently stub
326
+ * an unrecoverable artifact (e.g. `AGENTS.md` — `pull` only ever fetches
327
+ * dependency guides) or write a FILE named `.claude` over what should be a
328
+ * directory artifact. The raw-root fallback (`manifest === undefined`, a
329
+ * caller-supplied `--from`) keeps its own throw regardless: an EXPLICITLY
330
+ * named source failing to resolve is a different, caller-error failure
331
+ * class, not a "not vendored" degrade.
332
+ *
333
+ * @remarks
334
+ * Defense in depth at the filesystem trust boundary: EVERY resolved
335
+ * destination (`materialize` and `repair`, both origins) is asserted to stay
336
+ * within `resolve(target)` before any write, and every `host`-origin copy
337
+ * source is asserted to stay within `resolve(host)` before any read — a
338
+ * traversal segment (`../`) in an artifact's `path` or `source` cannot escape
339
+ * either root, even if a gate upstream (e.g. an ungated `Plan` built by hand)
340
+ * let it through. A destination violation throws `ScaffoldError('WRITE', …)`;
341
+ * a source violation throws `ScaffoldError('TARGET', …)`.
342
+ *
343
+ * @remarks
344
+ * The containment check is REAL-PATH aware, not merely lexical: both the
345
+ * root (`target` / `host`) and the candidate destination/source are resolved
346
+ * through `realpathSync` on their DEEPEST EXISTING ancestor before the prefix
347
+ * comparison, so a symlinked subdirectory planted inside an otherwise
348
+ * legitimate root cannot smuggle a write (or read) outside it — `repair`,
349
+ * which has no `isVacant` gate, is covered exactly like `materialize`. A path
350
+ * segment that does not yet exist on disk (the still-to-be-created file/dir
351
+ * a write is about to create) is rejoined onto the resolved existing
352
+ * ancestor rather than realpath'd itself.
353
+ *
354
+ * @example
355
+ * ```ts
356
+ * import { blueprint, blueprintToPlan } from '@orkestrel/scaffold'
357
+ * import { createMaterializer } from '@orkestrel/scaffold/server'
358
+ *
359
+ * const plan = blueprintToPlan(blueprint('budget', { surfaces: ['core'] }))
360
+ * const materializer = createMaterializer()
361
+ * materializer.materialize(plan, './packages/budget-new')
362
+ * materializer.destroy()
363
+ * ```
364
+ */
365
+ export declare class Materializer implements MaterializerInterface {
366
+ #private;
367
+ constructor(options?: MaterializerOptions);
368
+ get emitter(): EmitterInterface<MaterializerEventMap>;
369
+ materialize(plan: Plan, target: string): MaterializeResult;
370
+ repair(plan: Plan, audit: Audit, target: string): MaterializeResult;
371
+ prune(target: string): MaterializeResult;
372
+ destroy(): void;
373
+ }
374
+
375
+ /** The outcome of one materialization (server). */
376
+ export declare interface MaterializeResult {
377
+ readonly target: string;
378
+ readonly written: readonly string[];
379
+ readonly copied: readonly string[];
380
+ readonly skipped: readonly string[];
381
+ readonly removed: readonly string[];
382
+ }
383
+
384
+ /** `Materializer`'s push observation surface (AGENTS §13, server). */
385
+ export declare type MaterializerEventMap = {
386
+ readonly copy: readonly [path: string];
387
+ readonly write: readonly [path: string];
388
+ readonly remove: readonly [path: string];
389
+ readonly done: readonly [result: MaterializeResult];
390
+ readonly error: readonly [error: unknown];
391
+ readonly destroy: readonly [];
392
+ };
393
+
394
+ /** The materialization contract (server) — the only impure entity in the package. */
395
+ export declare interface MaterializerInterface {
396
+ readonly emitter: EmitterInterface<MaterializerEventMap>;
397
+ materialize(plan: Plan, target: string): MaterializeResult;
398
+ repair(plan: Plan, audit: Audit, target: string): MaterializeResult;
399
+ prune(target: string): MaterializeResult;
400
+ destroy(): void;
401
+ }
402
+
403
+ /**
404
+ * Options for `createMaterializer` / the `Materializer` constructor (server).
405
+ *
406
+ * @remarks
407
+ * `host` is the vendored-data root `host`-origin artifacts are copied FROM;
408
+ * defaults to THIS PACKAGE'S OWN vendored data root (`dist/host`, resolved
409
+ * from the installed module's own location) — the package vendors its host
410
+ * data and ships it with itself, so the default host is never the caller's
411
+ * working directory. A caller-supplied `host` pointing at a raw repo root
412
+ * (no `manifest.json` alongside it) maps artifact paths 1:1 instead of
413
+ * through the manifest — the sibling-repo / test-fixture shape.
414
+ */
415
+ export declare interface MaterializerOptions {
416
+ readonly host?: string;
417
+ readonly on?: EmitterHooks<MaterializerEventMap>;
418
+ readonly error?: EmitterErrorHandler;
419
+ }
420
+
421
+ /**
422
+ * The `prune`-owned directories — a hard allowlist; `pruneTargets` (and the
423
+ * `Materializer.prune` that consumes it) never scans anything outside these two.
424
+ *
425
+ * @example
426
+ * ```ts
427
+ * import { PRUNE_DIRECTORIES } from '@orkestrel/scaffold/server'
428
+ *
429
+ * PRUNE_DIRECTORIES // ['.claude/agents', 'scripts']
430
+ * ```
431
+ */
432
+ export declare const PRUNE_DIRECTORIES: readonly [".claude/agents", "scripts"];
433
+
434
+ /**
435
+ * List the repo-relative POSIX paths under `target`'s prune directories
436
+ * (`.claude/agents`, `scripts`) that the vendored `host` allowlist does NOT
437
+ * declare — THE single source of truth for prune drift, consumed by both
438
+ * `Materializer.prune` (which deletes exactly these paths) and the bin's
439
+ * audit/preview UX (which now shows them honestly instead of a
440
+ * structurally-always-zero `audit.foreign`).
441
+ *
442
+ * @param target - The target directory to scan for unexpected files.
443
+ * @param host - The vendored host root the allowlist is derived from.
444
+ * @returns The unexpected relative paths (e.g. `.claude/agents/rogue.md`); `[]`
445
+ * when a prune directory is absent under `target`, or when none of its
446
+ * files are unexpected. Pure read — never deletes anything.
447
+ * @throws `ScaffoldError('TARGET', …)` when `host` cannot positively
448
+ * establish a vendored allowlist for a prune directory that DOES exist
449
+ * under `target` (see `vendoredPruneSet`'s fail-closed remarks).
450
+ *
451
+ * @example
452
+ * ```ts
453
+ * import { pruneTargets } from '@orkestrel/scaffold/server'
454
+ *
455
+ * pruneTargets('./packages/router', hostRoot()) // ['.claude/agents/rogue.md']
456
+ * ```
457
+ */
458
+ export declare function pruneTargets(target: string, host: string): readonly string[];
459
+
460
+ /**
461
+ * Read and validate a vendored host root's `manifest.json`, when present.
462
+ *
463
+ * @param host - The host root to probe.
464
+ * @returns The parsed entries, or `undefined` when `host` has no
465
+ * `manifest.json` — the raw-repo-root fallback (`Materializer` then maps
466
+ * an artifact's `source` to `host` 1:1, no vendored staging indirection).
467
+ * @throws `ScaffoldError('TARGET', …)` when `manifest.json` exists but is
468
+ * unreadable, is not valid JSON, or is not an array of `ManifestEntry`.
469
+ *
470
+ * @example
471
+ * ```ts
472
+ * import { readHostManifest } from '@orkestrel/scaffold/server'
473
+ *
474
+ * readHostManifest('./dist/host') // readonly ManifestEntry[] | undefined
475
+ * ```
476
+ */
477
+ export declare function readHostManifest(host: string): readonly ManifestEntry[] | undefined;
478
+
479
+ /**
480
+ * Read `target/package.json` text — the read that feeds `manifestToDependencies`.
481
+ *
482
+ * @param target - The target directory to read the manifest from.
483
+ * @returns The manifest file's raw text.
484
+ * @throws `ScaffoldError('TARGET', …)` when the manifest is absent or
485
+ * unreadable (e.g. `EACCES` / `EPERM`) — carries the resolved `full` path
486
+ * in `context`.
487
+ *
488
+ * @example
489
+ * ```ts
490
+ * import { readManifest } from '@orkestrel/scaffold/server'
491
+ *
492
+ * readManifest('./packages/router') // '{ "name": "@orkestrel/router", … }'
493
+ * ```
494
+ */
495
+ export declare function readManifest(target: string): string;
496
+
497
+ /**
498
+ * Read a target's current content at a set of relative paths into a
499
+ * `Record<string, string>` — the I/O that feeds the pure `diffPlan`.
500
+ *
501
+ * @param target - The target directory to read from.
502
+ * @param paths - The plan-relative artifact paths to probe.
503
+ * @returns A record keyed by path; a directory entry maps to `''` (presence
504
+ * only — a `host`-origin directory artifact is audited by presence, never
505
+ * content), an absent path is OMITTED entirely (never an empty-string
506
+ * placeholder for a missing file, so `diffPlan` reports it `missing`).
507
+ * @throws `ScaffoldError('TARGET', …)` when an EXISTING path fails to read
508
+ * (e.g. `EACCES` / `EPERM`) — carries the offending relative `path` (and
509
+ * the resolved `full` path) in `context`. An absent path is never an
510
+ * error — it is simply omitted, per the return contract above.
511
+ *
512
+ * @example
513
+ * ```ts
514
+ * import { readTarget } from '@orkestrel/scaffold/server'
515
+ *
516
+ * readTarget('./packages/router', ['package.json', 'src/core/index.ts'])
517
+ * // { 'package.json': '{ "name": … }', 'src/core/index.ts': '…' }
518
+ * ```
519
+ */
520
+ export declare function readTarget(target: string, paths: readonly string[]): Readonly<Record<string, string>>;
521
+
522
+ /**
523
+ * Filter a manifest record's entries down to `@orkestrel/`-prefixed keys with
524
+ * string values — the shared `dependencies` / `peerDependencies` /
525
+ * `devDependencies` reader `deriveBlueprint` uses for every dependency-shaped
526
+ * field.
527
+ *
528
+ * @param value - The candidate manifest field value (e.g. `parsed.dependencies`).
529
+ * @returns The `@orkestrel/`-prefixed `[name, range]` entries; `[]` when
530
+ * `value` is not a plain object (per `isRecord`).
531
+ *
532
+ * @example
533
+ * ```ts
534
+ * import { selectOrkestrelEntries } from '@orkestrel/scaffold/server'
535
+ *
536
+ * selectOrkestrelEntries({ '@orkestrel/core': '^1.0.0', lodash: '^4.0.0' })
537
+ * // [['@orkestrel/core', '^1.0.0']]
538
+ * ```
539
+ */
540
+ export declare function selectOrkestrelEntries(value: unknown): readonly (readonly [string, string])[];
541
+
542
+ /**
543
+ * Stage the vendored host set (byte-preserved copies + `manifest.json`) from
544
+ * a repo root into an output directory — the BUILD-time primitive the
545
+ * `build:host` npm script now calls directly (replacing a standalone build
546
+ * script); `Materializer.materialize` is the RUNTIME reader of what this
547
+ * writes (via `hostRoot` / `readHostManifest`).
548
+ *
549
+ * @param root - The repo root every `paths` entry resolves against.
550
+ * @param out - The output directory to wipe and stage into (typically `dist/host`).
551
+ * @param paths - The repo-relative file/directory entries to stage; defaults
552
+ * to the package's own vendored set (`HOST_PATHS`) — a caller passes an
553
+ * explicit list only to stage an arbitrary/test set.
554
+ * @remarks
555
+ * `out` is wiped (`rmSync(out, { recursive: true, force: true })`) BEFORE
556
+ * staging, so a stale file left over from a prior run never lingers. Each
557
+ * `paths` entry is walked to its per-file leaves (a directory recursively,
558
+ * via `listFiles`; a file, itself) and copied byte-for-byte
559
+ * (`copyFileSync`) to `<out>/<storagePath(path)>`. The `executable` flag
560
+ * `entries` reports is derived from the `.sh` suffix on `destination` —
561
+ * deterministic on every build platform (Windows `stat` carries no execute
562
+ * bit); all vendored executables are shell scripts by construction.
563
+ * `manifest.json` is written LAST, as `entries` code-unit sorted by
564
+ * `destination`, tab-indented JSON with a trailing newline.
565
+ * @returns The written manifest's entries (`{ storage, destination, executable }`).
566
+ * @throws `ScaffoldError('TARGET', …)` naming the offending path when a
567
+ * `paths` entry has no source under `root`, or naming BOTH colliding
568
+ * destinations when two entries map to the same `storagePath` (the guard
569
+ * run BEFORE `manifest.json` is written).
570
+ *
571
+ * @example
572
+ * ```ts
573
+ * import { stageHost } from '@orkestrel/scaffold/server'
574
+ *
575
+ * const entries = stageHost(process.cwd(), 'dist/host')
576
+ * entries.length // number of files staged
577
+ * ```
578
+ */
579
+ export declare function stageHost(root: string, out: string, paths?: readonly string[]): readonly ManifestEntry[];
580
+
581
+ /**
582
+ * Map a repo-relative path to its vendored-host STAGING path, per the
583
+ * dotfile-mapping rule `stageHost` writes into `manifest.json`.
584
+ *
585
+ * @param path - The repo-relative source path (e.g. `.claude/agents/scout.md`).
586
+ * @returns The mapped storage path: a leading-dot TOP-LEVEL FILE maps to
587
+ * `dotfiles/<name-without-dot>`; a leading-dot DIRECTORY segment loses its
588
+ * dot wherever it appears; an undotted path is unchanged.
589
+ *
590
+ * @example
591
+ * ```ts
592
+ * import { storagePath } from '@orkestrel/scaffold/server'
593
+ *
594
+ * storagePath('.gitignore') // 'dotfiles/gitignore'
595
+ * storagePath('.claude/agents/scout.md') // 'claude/agents/scout.md'
596
+ * storagePath('.github/workflows/ci.yml') // 'github/workflows/ci.yml'
597
+ * storagePath('AGENTS.md') // 'AGENTS.md'
598
+ * ```
599
+ */
600
+ export declare function storagePath(path: string): string;
601
+
602
+ /**
603
+ * The upstream-synchronization entity (server) — the impure FETCH sibling of
604
+ * `Materializer`, Promise-based and network-only.
605
+ *
606
+ * @remarks
607
+ * Every method reads upstream over HTTPS with a 10-second per-request
608
+ * `AbortSignal.timeout` and bounded `concurrency` (default 6, never an
609
+ * unbounded `Promise.all`). The default COLLECT posture captures each
610
+ * dependency's `freshness` (`404` → `missing`, transport / other non-2xx →
611
+ * `failed`) into the result; `strict` mode instead throws
612
+ * `ScaffoldError('FETCH', …)` naming the failing URL. `guides`'s optional
613
+ * `current` parameter is a caller-supplied local-mirror content map keyed by
614
+ * dependency NAME (the `diffPlan` caller-supplied-reference pattern): WITH the
615
+ * map, a fetched guide byte-equal to its entry verdicts `current`, anything
616
+ * differing or absent from the map verdicts `behind`; WITHOUT the map, every
617
+ * successful fetch verdicts `behind` (no reference means it needs syncing).
618
+ * `pull` builds that map itself from the TARGET's own `guides/src/<short>.md`
619
+ * mirrors, so its verdicts are target-relative; `write` commits only the
620
+ * `behind` guides (never `current`, `missing`, or `failed`, which carries no
621
+ * trustworthy content) under the same realpath-anchored containment law
622
+ * `Materializer` enforces. After `destroy()` every method throws `DESTROYED`;
623
+ * teardown is idempotent, emitter last.
624
+ *
625
+ * @example
626
+ * ```ts
627
+ * import { createSync } from '@orkestrel/scaffold/server'
628
+ *
629
+ * const sync = createSync()
630
+ * const report = await sync.pull('.')
631
+ * if (report.failed === 0) await sync.write(report, '.')
632
+ * sync.destroy()
633
+ * ```
634
+ */
635
+ export declare class Sync implements SyncInterface {
636
+ #private;
637
+ constructor(options?: SyncOptions);
638
+ get emitter(): EmitterInterface<SyncEventMap>;
639
+ guides(deps: readonly Dependency[], current?: Readonly<Record<string, string>>): Promise<readonly GuideSync[]>;
640
+ versions(deps: readonly Dependency[]): Promise<readonly VersionSync[]>;
641
+ /**
642
+ * The fleet package catalog, sourced from the npm registry — the
643
+ * AUTHORITATIVE enumeration (never a caller-supplied root).
644
+ *
645
+ * @returns One {@link CatalogEntry} per `@orkestrel/*` package the registry
646
+ * lists, code-unit sorted by `name`.
647
+ * @remarks
648
+ * Three fetches build each entry: (1) the org's exact package-list
649
+ * (`-/org/orkestrel/package`) enumerates every published name — an
650
+ * unreachable or malformed response throws a coded `ScaffoldError('FETCH')`
651
+ * UNCONDITIONALLY (never gated by `strict`), since without it there is no
652
+ * catalog to build; (2) each name's own registry packument supplies
653
+ * `version` (`dist-tags.latest`) and a registry-path `description`
654
+ * fallback — a failed/malformed packument keeps the entry (degraded:
655
+ * `version: ''`) rather than dropping it, since the org list already
656
+ * proved the package exists; (3) each name's own guide
657
+ * (`guides/src/<short>.md`, same canonical URL as `guides()`, fetched
658
+ * unauthenticated — every fleet repo is public) supplies the PREFERRED
659
+ * `description` — its first blockquote's first paragraph — falling back
660
+ * to the packument description when the guide 404s, faults, or carries no
661
+ * blockquote; a 404 STAYS LISTED (it is a reachability signal, not an
662
+ * absence) with a note reading `guide unreachable (HTTP 404 — repo
663
+ * private or guide missing?)`. Emits `package` once per entry with a
664
+ * combined human-readable `note` (empty when both fetches succeeded)
665
+ * alongside the existing `error` events for each degraded sub-fetch.
666
+ */
667
+ catalog(): Promise<readonly CatalogEntry[]>;
668
+ pull(target: string): Promise<SyncReport>;
669
+ write(report: SyncReport, target: string): Promise<readonly string[]>;
670
+ destroy(): void;
671
+ }
672
+
673
+ /**
674
+ * `Sync`'s push observation surface (AGENTS §13, server).
675
+ *
676
+ * @remarks
677
+ * `package` fires once per `catalog()` entry processed; its `note` is the
678
+ * empty string when both the registry packument and the guide fetch
679
+ * succeeded, else a human-readable explanation of which one degraded (and
680
+ * why) — the same `''`-means-nothing convention `GuideSync.note` /
681
+ * `VersionSync.note` use, just non-optional here since every `catalog()`
682
+ * entry emits exactly once regardless of outcome.
683
+ */
684
+ export declare type SyncEventMap = {
685
+ readonly guide: readonly [name: string];
686
+ readonly version: readonly [name: string];
687
+ readonly package: readonly [name: string, note: string];
688
+ readonly write: readonly [path: string];
689
+ readonly done: readonly [report: SyncReport];
690
+ readonly error: readonly [error: unknown];
691
+ readonly destroy: readonly [];
692
+ };
693
+
694
+ /**
695
+ * The upstream-synchronization contract (server) — the impure FETCH sibling
696
+ * of `MaterializerInterface`.
697
+ */
698
+ export declare interface SyncInterface {
699
+ readonly emitter: EmitterInterface<SyncEventMap>;
700
+ guides(deps: readonly Dependency[], current?: Readonly<Record<string, string>>): Promise<readonly GuideSync[]>;
701
+ versions(deps: readonly Dependency[]): Promise<readonly VersionSync[]>;
702
+ catalog(): Promise<readonly CatalogEntry[]>;
703
+ pull(target: string): Promise<SyncReport>;
704
+ write(report: SyncReport, target: string): Promise<readonly string[]>;
705
+ destroy(): void;
706
+ }
707
+
708
+ /**
709
+ * Options for `createSync` / the `Sync` constructor (server).
710
+ *
711
+ * @remarks
712
+ * The endpoint bases + branch are INJECTABLE — `guides.base` defaults to
713
+ * `raw.githubusercontent.com`, `guides.branch` to `main`, `registry.base` to
714
+ * `registry.npmjs.org`, and `guides.timeout` / `registry.timeout` to 10
715
+ * seconds each. `concurrency` bounds in-flight requests (default 6, never an
716
+ * unbounded `Promise.all`); `retries` opts into per-request retry on a
717
+ * transport fault (default 0); `strict` flips a collect-mode failure into a
718
+ * thrown `ScaffoldError('FETCH', …)` (default `false`). `limit` bounds the
719
+ * bytes read from a single response body (declared `Content-Length` or
720
+ * streamed total, whichever trips first) — default 5,242,880 (5 MiB); a body
721
+ * that would exceed it is a transport fault, handled exactly like any other
722
+ * (retry-eligible per `retries`, then `failed` / strict `FETCH`). Every fetch
723
+ * is unauthenticated — no token, no `Authorization` header, anywhere; every
724
+ * fleet repo is public, so plain reachability is the only signal (a guide
725
+ * `404` degrades gracefully rather than needing credentials).
726
+ * `registry.base` also anchors `catalog()`'s org package-list lookup
727
+ * (`<registry.base>/-/org/orkestrel/package`) and its per-package packument
728
+ * fetches (`<registry.base>/<name>`) — the same base every other registry
729
+ * read already uses.
730
+ */
731
+ export declare interface SyncOptions {
732
+ readonly guides?: {
733
+ readonly base?: string;
734
+ readonly branch?: string;
735
+ readonly timeout?: number;
736
+ };
737
+ readonly registry?: {
738
+ readonly base?: string;
739
+ readonly timeout?: number;
740
+ };
741
+ readonly concurrency?: number;
742
+ readonly retries?: number;
743
+ readonly strict?: boolean;
744
+ readonly limit?: number;
745
+ readonly on?: EmitterHooks<SyncEventMap>;
746
+ readonly error?: EmitterErrorHandler;
747
+ }
748
+
749
+ /**
750
+ * The vendored set of destination-relative paths under `directory` (one of
751
+ * `PRUNE_DIRECTORIES`) that `pruneTargets` must NOT report — read from the
752
+ * manifest's `destination`s when `host` has one, else listed straight off
753
+ * `host/<directory>`.
754
+ *
755
+ * @param host - The vendored host root to establish the allowlist from.
756
+ * @param directory - The prune directory (one of `PRUNE_DIRECTORIES`) to scope the allowlist to.
757
+ * @remarks
758
+ * FAIL CLOSED: before returning any allowlist (even an empty one), the
759
+ * vendored source must be POSITIVELY established, or a caller would treat an
760
+ * unresolved host as "vendors nothing" and report every file under
761
+ * `target/<directory>` as unexpected. A missing `host` root, or (no
762
+ * `manifest.json` AND no `host/<directory>`), is a coded `TARGET` failure —
763
+ * the distinction this guards is missing-host vs genuinely-empty-vendor: a
764
+ * `host` that EXISTS and vendors zero files in `directory` (an existing empty
765
+ * dir, or a manifest with zero entries for it) remains a valid empty allowlist.
766
+ * @returns The allowed destination-relative paths under `directory`.
767
+ * @throws `ScaffoldError('TARGET', …)` when `host` does not exist, or when
768
+ * `host` has no `manifest.json` and no `host/<directory>` either.
769
+ *
770
+ * @example
771
+ * ```ts
772
+ * import { vendoredPruneSet } from '@orkestrel/scaffold/server'
773
+ *
774
+ * vendoredPruneSet('./dist/host', '.claude/agents') // Set { '.claude/agents/scout.md', … }
775
+ * ```
776
+ */
777
+ export declare function vendoredPruneSet(host: string, directory: string): ReadonlySet<string>;
778
+
779
+ export { }