@orkestrel/scaffold 0.0.5 → 0.0.6

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.
@@ -276,7 +276,13 @@ journal head as the recovery handle.
276
276
  `tmp/codex/<unit>-brief.md`, one `codex exec --json` streaming to `tmp/codex/<unit>.jsonl`
277
277
  with `--output-last-message`, foreground when it fits the shell cap, backgrounded with the
278
278
  turn ended when it may not — the harness re-invocation is the wait; placeholder loops and
279
- wait-promise reports are deviations. Recovery ladder on interruption: persisted-id
279
+ wait-promise reports are deviations. Every exec names its working directory with `-C`, and an
280
+ exec rooted outside a trusted git repository dies at launch unless `--skip-git-repo-check` is
281
+ passed, so cross-repo and fleet-container work rooted outside a checkout always passes it. A
282
+ launch is not a launch until the journal grows past its header: the bridge confirms the event
283
+ stream advanced beyond the session-configured head
284
+ before it reports the exec started, and treats an instantly-dead journal as a failed launch
285
+ whose tail is the evidence. Recovery ladder on interruption: persisted-id
280
286
  `codex-reply` re-emission → fresh CLI session with the same brief file → for an interrupted
281
287
  CLI exec, the journal survives and the Orchestrator chooses resume or fresh.
282
288
  - **The inverse bridge exists too:** Claude Code exposes `claude mcp serve`, registered in
@@ -297,9 +303,17 @@ journal head as the recovery handle.
297
303
  - At the start of each live Cloud session the user runs `codex login --device-auth` and
298
304
  completes ChatGPT approval in the browser. `scripts/codex.sh` only reports readiness; it
299
305
  never installs, authenticates, logs out, reads the auth cache, or performs a model call.
300
- - If ChatGPT device login is unavailable or expires, the Codex bench is dark. Fall back to
301
- `planner`/`reviewer` (Opus 5) and `builder`, and say so. Never substitute an API key, access
302
- token, copied `auth.json`, or another login flow unless the user changes this policy.
306
+ - A probe that finds the binary present but authentication unavailable starts recovery in the
307
+ same turn instead of recording the bench dark and waiting: the Orchestrator backgrounds
308
+ `codex login --device-auth` with its output captured to `tmp/codex/login.log`, surfaces the
309
+ verification URL and one-time code to the user the moment they appear there, arms a watcher
310
+ on completion, and re-probes `codex login status` when it fires. The bench comes live
311
+ mid-session with no restart; a session that sits dark until the user asks for the login has
312
+ failed the probe, not the bench.
313
+ - If that recovery cannot complete — device login unavailable, declined, or expired — the
314
+ Codex bench is dark. Fall back to `planner`/`reviewer` (Opus 5) and `builder`, and say so.
315
+ Never substitute an API key, access token, copied `auth.json`, or another login flow unless
316
+ the user changes this policy.
303
317
 
304
318
  Codex environment defaults:
305
319
 
@@ -123,14 +123,14 @@ then runs the configured scoped checks that prove environment isolation.
123
123
  - Lint is a separate complementary gate; neither lint nor root checking replaces
124
124
  environment-isolation checks.
125
125
 
126
- | Scope | `lib` | `types` | Permitted host globals |
127
- | ---------------------------- | --------------------------------- | ----------------- | --------------------------------------------------- |
128
- | `src:core`, `app:core` | `["ESNext"]` | `[]` | None: no DOM, Node, host crypto, console, or timers |
129
- | `src:browser`, `app:browser` | `["ESNext","DOM","DOM.Iterable"]` | default | DOM; no Node |
130
- | `src:server`, `app:server` | `["ESNext"]` | `["node"]` | Node; no DOM |
131
- | `src:styles` | `["ESNext"]` | `["vite/client"]` | Vite SCSS module declaration only |
132
-
133
- Strict core is load-bearing. A host-dependent helper belongs in its host environment; for example, a `generateId` using host `crypto` belongs in server, not core.
126
+ | Scope | `lib` | `types` | Permitted host globals |
127
+ | ---------------------------- | --------------------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
128
+ | `src:core`, `app:core` | `["ESNext","WebWorker"]` | `[]` | WHATWG web interop: fetch family, streams, URL, Abort, encoders, crypto, timers, console, DOMException, structuredClone; no DOM, no Node |
129
+ | `src:browser`, `app:browser` | `["ESNext","DOM","DOM.Iterable"]` | default | DOM; no Node |
130
+ | `src:server`, `app:server` | `["ESNext"]` | `["node"]` | Node; no DOM |
131
+ | `src:styles` | `["ESNext"]` | `["vite/client"]` | Vite SCSS module declaration only |
132
+
133
+ Strict core is load-bearing. A host-dependent helper belongs in its host environment; for example, a `generateId` reading `node:crypto` belongs in server, not core. The worker-only globals — `name`, `onrtctransform`, `close`, `postMessage`, `dispatchEvent`, `location`, `onerror`, `onlanguagechange`, `onoffline`, `ononline`, `onrejectionhandled`, `onunhandledrejection`, `self`, `importScripts`, `fonts`, `caches`, `crossOriginIsolated`, `indexedDB`, `isSecureContext`, `origin`, `scheduler`, `createImageBitmap`, `reportError`, `cancelAnimationFrame`, `requestAnimationFrame`, `onmessage`, `onmessageerror`, `addEventListener`, and `removeEventListener` — are policy-fenced out of core sources, so the `WebWorker` declarations widen the interop surface without admitting a worker host.
134
134
 
135
135
  Build/check config alignment:
136
136
 
@@ -29,7 +29,7 @@ that projection, expressed as data. Rendered defaults ship as versioned package
29
29
  here rather than a hand edit in every workspace.
30
30
 
31
31
  The module is mechanism, never product policy. The judgment calls — the name, the description, the
32
- keywords, which src and app environments, which dependencies, any template override —
32
+ keywords, which src and app environments, which dependencies, any artifact override —
33
33
  belong to the caller. What this module supplies is the closed vocabularies, the variant matrix as
34
34
  data, exact-record validation, a fail-closed gate, a deterministic pin, and lossless projections.
35
35
 
@@ -370,7 +370,9 @@ value lists behind their literal unions. `SRC_MATRIX` is the `src` environment m
370
370
  data — each environment's `configs/src` files, test-project label, `exports` subpath, and build
371
371
  formats. `APP_MATRIX` is its application sibling, adding the runtime entry where an environment produces
372
372
  one (`app/browser/index.html`, `app/server/main.ts`). `HOST_PATHS` is the ordered list of
373
- byte-copied host artifacts.
373
+ byte-copied host artifacts, and it is the staging manifest rather than the per-plan carried set:
374
+ `stageHost` vendors every path on it, while each plan carries the subset `selectHostPaths` selects
375
+ for that one workspace.
374
376
 
375
377
  The bounds are public because they are part of the contract, not implementation trivia.
376
378
  `MAX_ARTIFACT_BYTES` caps one artifact at 5 MiB and `MAX_TOTAL_ARTIFACT_BYTES` caps one blueprint,
@@ -544,7 +546,8 @@ entity allocation.
544
546
  then checks every override against the exact artifact set the plan would write. An override whose
545
547
  `path` matches no planned artifact, targets a `host`-origin artifact, or targets the
546
548
  blueprint-owned `package.json` publication boundary is a blocking question rather than a silent
547
- no-op.
549
+ no-op. An override that clears all three lands a `warnings` entry naming the path it replaces — the
550
+ declaration is accepted, and it is never accepted silently.
548
551
 
549
552
  ### Validators — server
550
553
 
@@ -736,11 +739,13 @@ From [`helpers.ts`](../../src/core/helpers.ts).
736
739
  | `contentByteLength` | function |
737
740
  | `contentToHex` | function |
738
741
  | `snapshotOf` | function |
742
+ | `selectHostPaths` | function |
739
743
  | `findPathConflict` | function |
740
744
  | `findFileConflict` | function |
741
745
  | `validateDependencyArray` | function |
742
746
  | `validateBlueprint` | function |
743
747
  | `manifestToDependencies` | function |
748
+ | `manifestToName` | function |
744
749
  | `rangeToFreshness` | function |
745
750
  | `computeHash` | function |
746
751
  | `stableStringify` | function |
@@ -786,14 +791,20 @@ report projections count with.
786
791
  `snapshotOf`, `contentToHex`, `contentToBytes`, `contentByteLength`, `contentCodePoint`, and
787
792
  `bytesToHex` are the host-independent byte leaves that make exact comparison possible without a
788
793
  host encoder or buffer; an unpaired surrogate encodes as `U+FFFD` rather than throwing.
789
- `findPathConflict` finds the first exact or case-insensitive collision in a path list, and
790
- `findFileConflict` additionally rejects a file that would sit inside another planned path.
794
+ `selectHostPaths` is the one-owner filter plan assembly applies before it carries anything: it
795
+ returns the host paths in input order minus `guides/src/<name>.md`, so a workspace never plans a
796
+ vendored mirror of the guide it writes itself. `findPathConflict` finds the first exact or
797
+ case-insensitive collision in a path list, and `findFileConflict` additionally rejects a file that
798
+ would sit inside another planned path — the loud backstop behind that selection.
791
799
 
792
800
  `validateBlueprint` and `validateDependencyArray` are the semantic pass. The array validator is
793
801
  pure — it returns its questions and the set of names it saw, so the caller can apply the
794
802
  cross-array overlap rules on top. `manifestToDependencies` reads a manifest's `dependencies`,
795
803
  `devDependencies`, and `peerDependencies` in that order, keeps only own data sections and scoped
796
- names, deduplicates, and never throws. `rangeToFreshness` applies the exact-pin comparison; the `missing` and `failed`
804
+ names, deduplicates, and never throws. `manifestToName` is its self-reading sibling over the same
805
+ text: the manifest's own string `name`, or `undefined` when the text is oversized, malformed,
806
+ rootless, or nameless — the projection that lets a target recognize itself in its own declared
807
+ dependencies. `rangeToFreshness` applies the exact-pin comparison; the `missing` and `failed`
797
808
  verdicts come from the fetch layer, never from this pure comparison.
798
809
 
799
810
  `computeHash` is a deterministic FNV-1a digest and `stableStringify` a key-order-independent
@@ -904,7 +915,9 @@ dependency-less stand-in. Environments come from `src/<environment>/` and `app/<
904
915
  scoped entries, with an optional peer recovered from `peerDependenciesMeta`; and `extras` is every
905
916
  development dependency minus the generated baseline and minus anything already declared as a
906
917
  dependency or peer, so a hand-added development dependency round-trips and stays audit-clean.
907
- `overrides` is always empty, because derivation cannot know a caller's override intent.
918
+ Derivation yields no `overrides`: they are caller-time inputs, not repository state. A computed
919
+ artifact that must differ reveals a gap in the canon; the blueprint grows an axis for that
920
+ distinction rather than the repository forking the file.
908
921
 
909
922
  `storagePath`, `stageHost`, `readHostManifest`, `locateHostSource`, `remapArtifactPath`, and
910
923
  `hydratePlan` are the vendored-host path. `storagePath` maps a repo-relative path to its un-dotted
@@ -926,7 +939,9 @@ anything.
926
939
  `consumeCatalogAllowance` decrements the single shared entry allowance and throws `TARGET` before an
927
940
  over-budget traversal continues. `discoverPackages` requires a real, unlinked root and lists its
928
941
  immediate child directories whose bounded manifest names a scoped package, skipping anything else
929
- silently. `catalogPackages` applies one allowance across every root and directory rather than
942
+ silently. A control-bearing child directory fails closed before its manifest is read and the
943
+ untrusted name is never reflected in the diagnostic. `catalogPackages` applies one allowance across
944
+ every root and directory rather than
930
945
  resetting a per-root budget, then draws each description from the first paragraph of the first
931
946
  blockquote of that package's own bounded guide via `guideToDescription`; a missing guide, an
932
947
  unreadable or oversized one, or one with no blockquote yields an empty description rather than an
@@ -994,7 +1009,13 @@ optional metadata, and engines.
994
1009
 
995
1010
  `rootTsconfig` emits the root compiler options and one path alias per declared environment;
996
1011
  `coreTsconfig`, `srcTsconfig`, and `appTsconfig` emit the scoped configurations that remove the
997
- wrong host's globals from each environment. `viteHeader` renders the shared header the alias block
1012
+ wrong host's globals from each environment. A core scope is the interesting one: `lib` is
1013
+ `["ESNext", "WebWorker"]` and `types` stays `[]`, which declares the WHATWG surface that is
1014
+ identical across Node, browsers, and workers — `fetch` and its request/response/header types,
1015
+ streams, `URL`, `AbortController`, the text encoders, `crypto`, timers, `console`, `DOMException`,
1016
+ `structuredClone` — while leaving `document`, `window`, and every `node:*` type unresolvable. That
1017
+ is one declaration set for a host-independent module, not a host. `viteHeader` renders the shared
1018
+ header — the alias block
998
1019
  derived from the tsconfig paths, plus the environment-boundary plugin — and `viteMachinery` is the
999
1020
  one place the header's axes are derived, read by `rootViteConfig`, `singleSrcViteConfig`,
1000
1021
  `applicationViteConfig`, and `configArtifacts` alike so no caller can invent a fourth answer.
@@ -1125,8 +1146,9 @@ The public methods of each behavioral interface, one table per type.
1125
1146
  `Scaffolding`; the optional group selection scopes the plan to those artifact groups.
1126
1147
  `audit(blueprint, current, groups?)` compiles and then diffs the resulting plan against the
1127
1148
  caller-supplied current content; a gated blueprint returns `complete: false` with the gate's
1128
- blocking questions and zero findings. `destroy()` is idempotent teardown. The interface also
1129
- exposes the readonly `emitter`.
1149
+ blocking questions and zero findings, and a complete one carries the gate's advisories on that same
1150
+ `questions` field. `destroy()` is idempotent teardown. The interface also exposes the readonly
1151
+ `emitter`.
1130
1152
 
1131
1153
  #### `PlanManagerInterface`
1132
1154
 
@@ -1190,12 +1212,14 @@ in-flight request. The interface also exposes the readonly `emitter`.
1190
1212
  `compile` runs three stages in fixed order and records each as a `CompileRecord` carrying its input,
1191
1213
  its output, whether it failed, and any error text.
1192
1214
 
1193
- 1. **draft** — `blueprintToPlan` selects the covered groups, drafts each group's artifacts, appends
1194
- the host set, applies overrides, and pins the draft. A throw here records a `draft` failure coded
1215
+ 1. **draft** — `blueprintToPlan` selects the covered groups, drafts each group's artifacts, carries
1216
+ the selected host set every vendored host path except the workspace's own guide applies
1217
+ overrides, and pins the draft. A throw here records a `draft` failure coded
1195
1218
  `INVALID`, emits `error`, marks the remaining two stages skipped, and returns incomplete.
1196
1219
  2. **gate** — `validatePlan` runs the semantic pass over the blueprint and checks every override
1197
- against the drafted artifact set. Blocking questions fail the stage; a dependency outside the
1198
- vendored guide set contributes a non-blocking advisory question instead.
1220
+ against the drafted artifact set. Blocking questions fail the stage; an accepted override and a
1221
+ dependency outside the vendored guide set each contribute a non-blocking advisory question
1222
+ instead.
1199
1223
  3. **pin** — a host-origin pointer artifact is appended for each non-vendored dependency, and
1200
1224
  `pinPlan` fills `trace` and `hash` from the plan's own content.
1201
1225
 
@@ -1227,7 +1251,8 @@ how it is audited, and whether it may ever be overwritten.
1227
1251
  - **`host`** — byte-copied from the vendored data root. These are the shared files a whole fleet
1228
1252
  keeps identical: the root instruction documents and licence, the agent, rule, and skill
1229
1253
  directories, the session scripts, the repository coding-law policy module, the byte-identical root
1230
- dotfiles, and the two guide mirrors every workspace carries. `HOST_PATHS` is the exact list.
1254
+ dotfiles, and the two line guide mirrors a workspace carries for contracts other than its own.
1255
+ `HOST_PATHS` is the exact vendored list; what a given plan carries is `selectHostPaths` of it.
1231
1256
  - **`template`** — filled from a frozen template definition by a pure fill engine. These are
1232
1257
  starter files: source stubs, test stubs, the starter guide, the README.
1233
1258
  - **`computed`** — derived by this package's own combination logic. These are the structural files:
@@ -1256,18 +1281,31 @@ anything, so a mature workspace's hand-written source, tests, guides, and manife
1256
1281
  overwritten with a stub. A consequence worth stating plainly: the generated
1257
1282
  `.github/workflows/ci.yml` is a **computed** artifact, so **user-owned CI is never repaired**. Once
1258
1283
  a workspace has its own workflow, that copy stands, and any change to it is an ordinary edit in that
1259
- workspace.
1284
+ workspace. Audit still compares it because computed artifacts are content-aware canon. A legitimate
1285
+ difference that the blueprint cannot express is a canon gap: add the missing blueprint axis rather
1286
+ than forking the computed file in one repository.
1260
1287
 
1261
1288
  Overrides respect the same boundary from the other direction. `applyOverrides` never replaces a
1262
1289
  host-origin artifact and never replaces `package.json`; the gate turns either attempt — and an
1263
1290
  override matching no planned artifact at all — into a blocking question rather than a silent no-op.
1264
-
1265
- Guide mirrors are the one place ownership is conditional. A dependency this package vendors a
1266
- byte-identical mirror for gets a real host-origin copy of `guides/src/<short>.md`. Any other
1267
- dependency gets a host-origin _pointer_ artifact plus a non-blocking question, never a fabricated
1268
- mirror; on materialization that pointer degrades to a short stub, and `scaffold pull` fetches the
1269
- real thing. That degrade is scoped exactly to guide pointers: any other missing manifest entry
1270
- means a corrupt or truncated vendored manifest, and fails closed.
1291
+ What survives those three refusals is applied and announced: the gate carries a non-blocking
1292
+ advisory naming each replaced path, and that advisory rides the `Scaffolding` and the `Audit` all
1293
+ the way through the library result.
1294
+
1295
+ Guide mirrors are the one place ownership is conditional, and the law is one owner per guide path.
1296
+ **A workspace mirrors every line guide except its own.** When the name matches the guide package
1297
+ on `guides/src/guide.md`, this package on `guides/src/scaffold.md` the workspace itself is the
1298
+ owner, keeping that path as its **template**-origin starter guide, and `selectHostPaths` drops the
1299
+ vendored mirror so the path is contributed exactly once. For every other contract the mirror is the
1300
+ owner: a dependency this package vendors a byte-identical mirror for gets a real host-origin copy of
1301
+ `guides/src/<short>.md`, contributed once whether it arrives through the host set or through the
1302
+ dependency, so a package depending on `@orkestrel/guide` plans one `guides/src/guide.md` rather than
1303
+ two. Any other dependency gets a host-origin _pointer_ artifact plus a non-blocking question, never
1304
+ a fabricated mirror; on materialization that pointer degrades to a short stub, and `scaffold pull`
1305
+ fetches the real thing. That degrade is scoped exactly to guide pointers: any other missing manifest
1306
+ entry means a corrupt or truncated vendored manifest, and fails closed. Selection is the law and
1307
+ `findFileConflict` is its backstop: two artifacts at one path refuse the plan rather than racing to
1308
+ be the last writer.
1271
1309
 
1272
1310
  ## Audit, repair, and prune
1273
1311
 
@@ -1317,10 +1355,15 @@ because there is nothing to explain.
1317
1355
  `pull` is the target-aware composition. It reads the target's declared scoped dependencies from its
1318
1356
  manifest, rejects any explicit selection the target does not declare, builds the reference map from
1319
1357
  the target's own `guides/src/<short>.md` mirrors, fetches guides and versions under one shared
1320
- allowance, and assembles a report whose `clean` flag requires both no drift and no failures. `write`
1321
- then commits only the `behind` guides never `current`, `missing`, or `failed`, none of which carry
1322
- trustworthy content under the same containment and precondition law `Materializer` enforces,
1323
- including a baseline digest check against what is actually on disk.
1358
+ allowance, and assembles a report whose `clean` flag requires both no drift and no failures. A
1359
+ target that declares itself is the one asymmetry, and it follows the same single-owner law: the
1360
+ guide pass drops the self dependency, so `pull` never fetches or writes a workspace's own contract
1361
+ guide over the copy that workspace owns, while the version pass keeps it and still reports its
1362
+ freshness. A `--live` audit reads upstream through the same two passes and applies the same
1363
+ self-exclusion, so the freshness a workspace reports about itself never depends on which verb asked.
1364
+ `write` then commits only the `behind` guides — never `current`, `missing`, or `failed`,
1365
+ none of which carry trustworthy content — under the same containment and precondition law
1366
+ `Materializer` enforces, including a baseline digest check against what is actually on disk.
1324
1367
 
1325
1368
  `catalog` builds the fleet package catalog from three reads per entry. The registry's exact
1326
1369
  organization package list is authoritative and unconditionally required. Each package's own registry
@@ -1362,10 +1405,20 @@ a fixed, interleaved order so aggregates sit immediately before their per-enviro
1362
1405
  live generated-consumer integration gate for the scaffold engine itself
1363
1406
 
1364
1407
  **Environment isolation.** Scoped TypeScript projects remove the wrong host's globals from each
1365
- environment: core scopes carry no DOM, no Node, and no host globals at all; browser scopes carry DOM
1366
- and no Node; server scopes carry Node and no DOM. Lint restricts declared package, alias, and
1367
- conventional relative imports in the same directions. Neither replaces the other, and neither
1368
- replaces the build.
1408
+ environment: core scopes carry the WHATWG web-interop surface and no host at all no DOM, no Node,
1409
+ no `vite/client`; browser scopes carry DOM and no Node; server scopes carry Node and no DOM. The
1410
+ worker-only globals the `WebWorker` declarations would otherwise admit `name`, `onrtctransform`,
1411
+ `close`, `postMessage`, `dispatchEvent`, `location`, `onerror`, `onlanguagechange`, `onoffline`,
1412
+ `ononline`, `onrejectionhandled`, `onunhandledrejection`, `self`, `importScripts`, `fonts`, `caches`,
1413
+ `crossOriginIsolated`, `indexedDB`, `isSecureContext`, `origin`, `scheduler`, `createImageBitmap`,
1414
+ `reportError`, `cancelAnimationFrame`, `requestAnimationFrame`, `onmessage`, `onmessageerror`,
1415
+ `addEventListener`, and `removeEventListener` — are fenced out of `src/core` and `app/core` sources
1416
+ by the policy suite, so the declarations widen what a host-independent module may call without
1417
+ widening where it may run. On every TypeScript bump, derive this list from the module-scope
1418
+ global-object `declare var` and `declare function` declarations in the installed
1419
+ `lib.webworker.d.ts`, then subtract values supplied by `lib.esnext*` or current Node globals. Lint
1420
+ restricts declared package, alias, and conventional relative imports in the same directions.
1421
+ Neither replaces the other, and neither replaces the build.
1369
1422
 
1370
1423
  **The generated build boundary.** The emitted configuration carries an environment-boundary plugin
1371
1424
  that resolves the real module graph rather than re-implementing a parser. **TypeScript and
@@ -1485,7 +1538,8 @@ cannot express — that a centralized module exports every top-level declaration
1485
1538
  implementation files hold one class and no stray module-scope declaration, that no function is
1486
1539
  declared inside another function outside a directly-passed callback, that interface properties are
1487
1540
  readonly, that privacy is a runtime `#` field rather than a TypeScript modifier, that a barrel
1488
- re-exports only through `export *`, and that a computed dynamic import cannot smuggle a
1541
+ re-exports only through `export *`, that a core source never names a worker-only global the
1542
+ `WebWorker` declarations expose, and that a computed dynamic import cannot smuggle a
1489
1543
  cross-environment dependency past the declared import rules. Vue components are inspected for the
1490
1544
  same evasions. It is a complement to lint and typecheck, never a second type system, and it is not a
1491
1545
  general-purpose source analyzer. Generated workspaces receive the same exported policy module as a
@@ -1495,7 +1549,11 @@ host-origin file and run it as a dedicated Node-only `policy` test project over
1495
1549
  **Real browser capability.** Browser test projects are gated on the real executable: the generated
1496
1550
  configuration and the generated policy test both probe `existsSync(chromium.executablePath())`. A
1497
1551
  browser suite runs when a real Chromium is installed and is skipped honestly when it is not, rather
1498
- than being faked.
1552
+ than being faked. The gate is applied at registration, not only inside the project: without a
1553
+ Chromium the browser project is left out of the emitted `projects` list entirely, so the runner
1554
+ never has to reconcile a registered project whose include set resolves to nothing, and one printed
1555
+ warning names every omitted project label. A machine with a browser runs the browser suite; a
1556
+ machine without one runs the remaining projects and says so.
1499
1557
 
1500
1558
  **Continuous integration.** The generated workflow runs on push and pull request, on
1501
1559
  `ubuntu-latest`, with read-only contents permission, a 60-minute timeout, and a matrix that **tests
@@ -1903,6 +1961,7 @@ import {
1903
1961
  hostGroup,
1904
1962
  packageManifest,
1905
1963
  paritySpecifiers,
1964
+ selectHostPaths,
1906
1965
  sourceArtifacts,
1907
1966
  srcVariant,
1908
1967
  testArtifacts,
@@ -1912,6 +1971,7 @@ const spec = blueprint('router', { src: ['core'], app: ['core', 'server'] })
1912
1971
  const members = blueprintToMembers(spec)
1913
1972
 
1914
1973
  hostGroup('AGENTS.md') // 'docs'
1974
+ selectHostPaths(['guides/src/router.md', 'LICENSE'], spec.name) // ['LICENSE'] — never its own guide
1915
1975
  srcVariant(['core', 'server']) // 'multi'
1916
1976
  entryFields(['browser']).main // './dist/src/browser/index.js'
1917
1977
  dualCondition('./dist/src/core/index')
@@ -1979,7 +2039,12 @@ applicationViteConfig([], ['core', 'server']).includes('appServer') // true
1979
2039
  ### Reading declared dependencies and comparing freshness
1980
2040
 
1981
2041
  ```ts
1982
- import { isBehind, manifestToDependencies, rangeToFreshness } from '@orkestrel/scaffold'
2042
+ import {
2043
+ isBehind,
2044
+ manifestToDependencies,
2045
+ manifestToName,
2046
+ rangeToFreshness,
2047
+ } from '@orkestrel/scaffold'
1983
2048
  import {
1984
2049
  guideStub,
1985
2050
  packageShortName,
@@ -1988,6 +2053,7 @@ import {
1988
2053
  } from '@orkestrel/scaffold/server'
1989
2054
 
1990
2055
  manifestToDependencies('{"dependencies":{"@orkestrel/contract":"^0.0.7"}}')
2056
+ manifestToName('{"name":"@orkestrel/router"}') // '@orkestrel/router' — the target's own name
1991
2057
  rangeToFreshness('^0.0.7', '0.0.7') // 'current'
1992
2058
  isBehind(rangeToFreshness('^0.0.7', '0.0.9')) // true
1993
2059
 
@@ -2220,7 +2286,8 @@ isMissingPathError(caught) // true only for an ENOENT error
2220
2286
  ## Tests
2221
2287
 
2222
2288
  - [`tests/src/core/helpers.test.ts`](../../tests/src/core/helpers.test.ts) — the pure leaves: table
2223
- alignment, byte encoding, snapshots, conflicts, projections, hashing, and format-stable JSON.
2289
+ alignment, byte encoding, snapshots, host selection, conflicts, projections, hashing, and
2290
+ format-stable JSON.
2224
2291
  - [`tests/src/core/builders.test.ts`](../../tests/src/core/builders.test.ts) — the blueprint,
2225
2292
  dependency, override, and member builders, including optional-field omission.
2226
2293
  - [`tests/src/core/validators.test.ts`](../../tests/src/core/validators.test.ts) — every guard and
@@ -2228,7 +2295,8 @@ isMissingPathError(caught) // true only for an ENOENT error
2228
2295
  - [`tests/src/core/shapers.test.ts`](../../tests/src/core/shapers.test.ts) — per-shape guard
2229
2296
  exactness, schema essentials, seeded generation, and parse round-trips.
2230
2297
  - [`tests/src/core/compilers.test.ts`](../../tests/src/core/compilers.test.ts) — every drafted
2231
- group, the manifest and exports combination rules, and the emitted configuration text.
2298
+ group, the manifest and exports combination rules, the one-owner guide law for a workspace that
2299
+ names a line guide, and the emitted configuration text.
2232
2300
  - [`tests/src/core/Compiler.test.ts`](../../tests/src/core/Compiler.test.ts) — the three-stage
2233
2301
  pipeline, the fail-closed gate, the emission sequences, and post-destroy behavior.
2234
2302
  - [`tests/src/core/PlanManager.test.ts`](../../tests/src/core/PlanManager.test.ts) — content-hash
@@ -32,8 +32,10 @@ valid_effort() {
32
32
 
33
33
  if codex login status >/dev/null 2>&1; then
34
34
  auth="authentication ready"
35
+ recovery=""
35
36
  else
36
37
  auth="authentication unavailable"
38
+ recovery='codex login --device-auth'
37
39
  fi
38
40
 
39
41
  if valid_model "$analyst_model" &&
@@ -46,4 +48,7 @@ else
46
48
  fi
47
49
 
48
50
  echo "codex.sh: bench detected; ${auth}; ${routes}."
51
+ if [ -n "$recovery" ]; then
52
+ echo "codex.sh: to recover, run \`${recovery}\`."
53
+ fi
49
54
  exit 0
@@ -55,6 +55,42 @@ export const DATA_SOURCE_FILES: readonly string[] = Object.freeze([
55
55
  'validators.ts',
56
56
  ])
57
57
 
58
+ /** Worker-only value globals that WebWorker typing must not expose to core implementations. */
59
+ export const WORKER_SCOPE_VALUE_GLOBALS: readonly string[] = Object.freeze([
60
+ 'name',
61
+ 'onrtctransform',
62
+ 'close',
63
+ 'postMessage',
64
+ 'dispatchEvent',
65
+ 'location',
66
+ 'onerror',
67
+ 'onlanguagechange',
68
+ 'onoffline',
69
+ 'ononline',
70
+ 'onrejectionhandled',
71
+ 'onunhandledrejection',
72
+ 'self',
73
+ 'importScripts',
74
+ 'fonts',
75
+ 'caches',
76
+ 'crossOriginIsolated',
77
+ 'indexedDB',
78
+ 'isSecureContext',
79
+ 'origin',
80
+ 'scheduler',
81
+ 'createImageBitmap',
82
+ 'reportError',
83
+ 'cancelAnimationFrame',
84
+ 'requestAnimationFrame',
85
+ 'onmessage',
86
+ 'onmessageerror',
87
+ 'addEventListener',
88
+ 'removeEventListener',
89
+ ])
90
+
91
+ /** Virtual source text used while binding one policy-inspected module. */
92
+ export const POLICY_SOURCE_TEXTS: Map<string, string> = new Map()
93
+
58
94
  /** One script block extracted from a Vue SFC by the official compiler. */
59
95
  export interface VueScriptBlockInterface {
60
96
  readonly content: string
@@ -162,6 +198,126 @@ export function hasAllowedTripleSlashReference(path: string, source: ts.SourceFi
162
198
  )
163
199
  }
164
200
 
201
+ /**
202
+ * Whether the policy compiler can read one source path.
203
+ *
204
+ * @param path - The source path to inspect
205
+ * @returns `true` when the virtual or physical source exists
206
+ */
207
+ export function hasPolicySource(path: string): boolean {
208
+ return POLICY_SOURCE_TEXTS.has(path) || ts.sys.fileExists(path)
209
+ }
210
+
211
+ /**
212
+ * Read one virtual or physical policy source.
213
+ *
214
+ * @param path - The source path to read
215
+ * @returns The source text when present
216
+ */
217
+ export function readPolicySource(path: string): string | undefined {
218
+ return POLICY_SOURCE_TEXTS.get(path) ?? ts.sys.readFile(path)
219
+ }
220
+
221
+ /**
222
+ * Parse one virtual or physical policy source for the compiler host.
223
+ *
224
+ * @param path - The source path to parse
225
+ * @param language - The requested TypeScript language target
226
+ * @returns The parsed source file when present
227
+ */
228
+ export function createPolicySource(
229
+ path: string,
230
+ language: ts.ScriptTarget | ts.CreateSourceFileOptions,
231
+ ): ts.SourceFile | undefined {
232
+ const content = readPolicySource(path)
233
+ return content === undefined ? undefined : ts.createSourceFile(path, content, language, true)
234
+ }
235
+
236
+ /**
237
+ * Bind one policy-inspected module without loading ambient host declarations.
238
+ *
239
+ * @param path - The source path used in diagnostics
240
+ * @param content - The TypeScript source text to bind
241
+ * @returns A one-file TypeScript program whose checker resolves lexical bindings
242
+ */
243
+ export function createPolicyProgram(path: string, content: string): ts.Program {
244
+ const options: ts.CompilerOptions = {
245
+ allowJs: true,
246
+ noLib: true,
247
+ noResolve: true,
248
+ target: ts.ScriptTarget.Latest,
249
+ types: [],
250
+ }
251
+ POLICY_SOURCE_TEXTS.set(path, content)
252
+ const host = ts.createCompilerHost(options)
253
+ host.fileExists = hasPolicySource
254
+ host.readFile = readPolicySource
255
+ host.getSourceFile = createPolicySource
256
+ const program = ts.createProgram([path], options, host)
257
+ POLICY_SOURCE_TEXTS.delete(path)
258
+ return program
259
+ }
260
+
261
+ /**
262
+ * Whether an identifier is a standalone runtime value reference.
263
+ *
264
+ * @param node - The identifier occurrence to classify
265
+ * @param checker - The binder used to distinguish lexical values from ambient globals
266
+ * @returns `true` only when the occurrence reads or writes a runtime value
267
+ */
268
+ export function isValueReferenceIdentifier(node: ts.Identifier, checker: ts.TypeChecker): boolean {
269
+ if (ts.isPartOfTypeNode(node)) return false
270
+
271
+ let ancestor: ts.Node = node.parent
272
+ while (!ts.isSourceFile(ancestor) && !ts.isStatement(ancestor)) {
273
+ if (ts.isTypeQueryNode(ancestor)) return false
274
+ if (ts.isComputedPropertyName(ancestor) && ts.isTypeElement(ancestor.parent)) return false
275
+ ancestor = ancestor.parent
276
+ }
277
+
278
+ const parent = node.parent
279
+ if (
280
+ (ts.isPropertyAccessExpression(parent) && parent.name === node) ||
281
+ (ts.isPropertyAssignment(parent) && parent.name === node) ||
282
+ (ts.isPropertyDeclaration(parent) && parent.name === node) ||
283
+ (ts.isPropertySignature(parent) && parent.name === node) ||
284
+ (ts.isMethodDeclaration(parent) && parent.name === node) ||
285
+ (ts.isMethodSignature(parent) && parent.name === node) ||
286
+ (ts.isGetAccessorDeclaration(parent) && parent.name === node) ||
287
+ (ts.isSetAccessorDeclaration(parent) && parent.name === node) ||
288
+ (ts.isVariableDeclaration(parent) && parent.name === node) ||
289
+ (ts.isParameter(parent) && parent.name === node) ||
290
+ (ts.isBindingElement(parent) && (parent.name === node || parent.propertyName === node)) ||
291
+ (ts.isFunctionDeclaration(parent) && parent.name === node) ||
292
+ (ts.isFunctionExpression(parent) && parent.name === node) ||
293
+ (ts.isClassDeclaration(parent) && parent.name === node) ||
294
+ (ts.isClassExpression(parent) && parent.name === node) ||
295
+ (ts.isInterfaceDeclaration(parent) && parent.name === node) ||
296
+ (ts.isTypeAliasDeclaration(parent) && parent.name === node) ||
297
+ (ts.isTypeParameterDeclaration(parent) && parent.name === node) ||
298
+ (ts.isEnumDeclaration(parent) && parent.name === node) ||
299
+ (ts.isEnumMember(parent) && parent.name === node) ||
300
+ (ts.isModuleDeclaration(parent) && parent.name === node) ||
301
+ ts.isImportClause(parent) ||
302
+ ts.isImportSpecifier(parent) ||
303
+ ts.isNamespaceImport(parent) ||
304
+ ts.isImportEqualsDeclaration(parent) ||
305
+ ts.isExportSpecifier(parent) ||
306
+ ts.isNamespaceExport(parent) ||
307
+ ts.isNamespaceExportDeclaration(parent) ||
308
+ (ts.isLabeledStatement(parent) && parent.label === node) ||
309
+ (ts.isBreakOrContinueStatement(parent) && parent.label === node) ||
310
+ (ts.isJsxAttribute(parent) && parent.name === node)
311
+ ) {
312
+ return false
313
+ }
314
+ return (
315
+ (ts.isShorthandPropertyAssignment(parent)
316
+ ? checker.getShorthandAssignmentValueSymbol(parent)
317
+ : checker.getSymbolAtLocation(node)) === undefined
318
+ )
319
+ }
320
+
165
321
  /** Inspect a Vue single-file component for syntax that can bypass declared import policy. */
166
322
  export function inspectVueCodingLaw(
167
323
  path: string,
@@ -187,7 +343,22 @@ export function inspectVueCodingLaw(
187
343
  }
188
344
 
189
345
  /** Add syntax-wide coding-law violations while traversing one source tree. */
190
- export function inspectCodingNode(path: string, node: ts.Node, violations: string[]): void {
346
+ export function inspectCodingNode(
347
+ path: string,
348
+ node: ts.Node,
349
+ violations: string[],
350
+ checker: ts.TypeChecker,
351
+ ): void {
352
+ if (
353
+ /^(?:app|src)[\\/]core[\\/]/u.test(path) &&
354
+ ts.isIdentifier(node) &&
355
+ WORKER_SCOPE_VALUE_GLOBALS.includes(node.text) &&
356
+ isValueReferenceIdentifier(node, checker)
357
+ ) {
358
+ violations.push(
359
+ `${path}:${formatPolicyPosition(node)} forbids worker-scope global ${node.text} in core`,
360
+ )
361
+ }
191
362
  if (
192
363
  ts.isAsExpression(node) ||
193
364
  ts.isTypeAssertionExpression(node) ||
@@ -242,13 +413,16 @@ export function inspectCodingNode(path: string, node: ts.Node, violations: strin
242
413
  ) {
243
414
  violations.push(`${path}:${formatPolicyPosition(node)} forbids hidden function assignments`)
244
415
  }
245
- ts.forEachChild(node, (child) => inspectCodingNode(path, child, violations))
416
+ ts.forEachChild(node, (child) => inspectCodingNode(path, child, violations, checker))
246
417
  }
247
418
 
248
419
  /** Inspect one TypeScript source module for repository coding-law violations. */
249
420
  export function inspectCodingLaw(path: string, content: string): readonly string[] {
250
421
  const violations: string[] = []
251
- const source = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true)
422
+ const program = createPolicyProgram(path, content)
423
+ const source = program.getSourceFile(path)
424
+ if (source === undefined) throw new Error(`Policy source was not bound at ${path}`)
425
+ const checker = program.getTypeChecker()
252
426
  const file = basename(path)
253
427
 
254
428
  if (/\.[cm]?jsx?$/u.test(path)) {
@@ -362,7 +536,7 @@ export function inspectCodingLaw(path: string, content: string): readonly string
362
536
  }
363
537
  }
364
538
 
365
- inspectCodingNode(path, source, violations)
539
+ inspectCodingNode(path, source, violations, checker)
366
540
  return violations
367
541
  }
368
542