Skip to content

refactor(bootstrap): extract @workglow/bootstrap from duplicated registries - #672

Draft
sroussey wants to merge 13 commits into
mainfrom
claude/libs-issues-triage-prs-mh6x2o-574
Draft

refactor(bootstrap): extract @workglow/bootstrap from duplicated registries#672
sroussey wants to merge 13 commits into
mainfrom
claude/libs-issues-triage-prs-mh6x2o-574

Conversation

@sroussey

@sroussey sroussey commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Closes #574

bootstrapWorkglow() (in packages/workglow/src/bootstrap.ts) and bootstrapTestRegistry() (in packages/test/src/binding/) each made the same 14 default-registration calls in the same order. They were provably equivalent — every one of those 14 functions defaults its registry parameter to globalServiceRegistry, so the no-argument test variant and the explicit-registry production variant did identical work. There was no deliberate test-vs-prod divergence to preserve.

This promotes the shared logic to a single new @workglow/bootstrap package.

⚠️ This adds a new publicly published npm package

@workglow/bootstrap is declared with publishConfig.access: "public", and scripts/publish-workspaces.ts discovers packages via findWorkspaces() globbing the root workspaces patterns — so it will be picked up and published automatically on the next publish-workspaces run, with no allowlist to add it to.

Please confirm the npm name @workglow/bootstrap is available (and owned by the org) before merging. Version starts at 0.3.37 to stay in lockstep with the rest of the release train.

Other reviewer-facing notes

  • registerAllDefaults becomes public API. It was a private function inside the meta-package; it is now exported from @workglow/bootstrap and, via the unchanged export * from "./bootstrap" in the root barrel, from workglow itself. Its signature was widened to registerAllDefaults(registry: ServiceRegistry = globalServiceRegistry): void so it serves both former call styles.
  • Downstream link chains should re-run linking. Anything wired through bun run link-all / dev-link (sec, embarc-data, builder) needs a re-link to pick up the new workspace package.
  • The issue's stated cause was not literally a package-graph cycle. workglow never depended on @workglow/test, so no import cycle existed. The real constraints were (a) the isolated linker (bunfig.toml sets linker = "isolated", and the repo root has no @workglow/* in node_modules), which is why vitest.setup.ts could not import a package specifier; and (b) not wanting packages/test to pull in the meta-package and all 25 providers just to register defaults. The extraction is still the right fix — it removes the duplication and gives both consumers a package strictly below them in the graph.

What changed

  • New packages/bootstrap, mirroring packages/knowledge-base's packaging (script block, conditional exports map, files, publishConfig, peerDependencies + devDependencies mirror). Depends on exactly six workspace packages: util, storage, task-graph, knowledge-base, ai, mcp.
    • src/bootstrap/registerAllDefaults.ts — the 14 calls moved verbatim, comments intact.
    • src/bootstrap/bootstrapWorkglow.tsBootstrapOptions, WorkglowContext, bootstrapWorkglow(), createOrchestrationContext().
    • src/{browser,node,bun}.tsexport * from "./common".
  • packages/workglow/src/bootstrap.ts is now just export * from "@workglow/bootstrap", preserving its license header and module doc. This keeps the workglow/bootstrap subpath and the root barrel working with zero public API change.
  • auto-bootstrap.ts deliberately stays in workglow — it imports TsLogLogger, which needs the tslog dependency only the meta-package carries, and it is listed in that package's sideEffects. It required no edit.
  • vitest.setup.ts now calls registerAllDefaults(), imported by source path (./packages/bootstrap/src/bootstrap/registerAllDefaults) because of the isolated linker noted above.
  • Deleted packages/test/src/binding/bootstrapTestRegistry.ts — verified nothing else imported it (it was not in packages/test/src/common.ts; vitest.setup.ts was its only consumer).

Verification

All commands run locally; real results below.

Command Result
bun run build:types 42/42 tasks successful. @workglow/bootstrap:build-types and workglow:build-types both executed (cache miss) — proves the tsc project-reference graph is acyclic.
bun run build 86/86 tasks successful.
bun run rebuild (cold, clean:dist + --force) 86/86 successful, 0 cached, 1m57s. Run specifically to check the ordering hazard called out in turbo.json (build-js declares dependsOn: ["^build-types"], not ^build-js). No ordering problem appeared — and this constraint already applied identically to the meta-package's 25 existing deps, so no new risk is introduced.
packages/workglow bun run build-js (direct) ✅ exit 0 — assertNoExportCollisions ran clean over the barrels, and dynamically imported the new starred source without a resolution failure. Confirmed registerAllDefaults collides with no existing export.
bun scripts/test.ts util vitest 46 files, 672 passed / 10 skipped.
bun scripts/test.ts task vitest 72 files, 1132 passed / 24 skipped.
bun scripts/test.ts mcp vitest 7 files, 109 passed. Covers registerMcpServerDefaults.
bun run format ✅ "All matched files use Prettier code style!" — no changes needed.

The three vitest suites all exercise the rewritten vitest.setup.ts, so they confirm the new entry point populates the global registry exactly as before.

Runtime smoke test (against the built dist, not just types) verifying the public surface is unchanged:

subpath exports: bootstrapWorkglow, createOrchestrationContext, registerAllDefaults
root has bootstrapWorkglow: function
root has registerAllDefaults: function
root has createOrchestrationContext: function
same binding as subpath: true
bootstrapWorkglow() ok, logger: true
isolated ctx registry distinct: true
dispose ok

workglow/bootstrap and the workglow root barrel resolve to the same function bindings (no ESM ambiguity), and createOrchestrationContext() still yields an isolated, disposable registry.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PwyJuFrJnibKvrrk8Fa4Fn


Generated by Claude Code

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 59.85% 37246 / 62230
🔵 Statements 59.35% 39080 / 65836
🔵 Functions 60.84% 7215 / 11858
🔵 Branches 48.06% 18906 / 39338
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
eslint.config.js 100% 100% 100% 100%
vitest.setup.ts 100% 100% 100% 100%
packages/bootstrap/src/bootstrap/bootstrapWorkglow.ts 23.07% 33.33% 16.66% 23.07% 51, 56-97
packages/bootstrap/src/bootstrap/registerAllDefaults.ts 100% 100% 100% 100%
Generated in workflow #3053 for commit 88bc7ca by the Vitest Coverage Report Action

sroussey and others added 6 commits August 13, 2026 03:16
…stries

`bootstrapWorkglow()` and the vitest harness's `bootstrapTestRegistry()` each
made the same 14 default-registration calls in the same order. The two were
provably equivalent — every registration function defaults its registry
parameter to `globalServiceRegistry`, so the no-argument test variant and the
explicit-registry production variant did identical work.

Promote the shared logic to a new `@workglow/bootstrap` package sitting below
both consumers, depending only on util, storage, task-graph, knowledge-base,
ai, and mcp.

- `registerAllDefaults(registry = globalServiceRegistry)` is now the single
  definition, with the signature widened so both call styles are served.
- `bootstrapWorkglow()`, `createOrchestrationContext()`, `BootstrapOptions`,
  and `WorkglowContext` move alongside it.
- `packages/workglow/src/bootstrap.ts` becomes a re-export, so the
  `workglow/bootstrap` subpath and the root barrel keep working unchanged.
  `auto-bootstrap.ts` stays in the meta-package, which carries the tslog
  dependency its logger needs.
- `vitest.setup.ts` calls `registerAllDefaults()` directly, imported by source
  path because the isolated linker keeps workspace packages out of the repo
  root's node_modules.

Claude-Session: https://claude.ai/code/session_01PwyJuFrJnibKvrrk8Fa4Fn
…ts test, docs

Follow-up polish on the @workglow/bootstrap extraction. No correctness,
security, or data-integrity change — the registration set and order are
untouched.

- Add the missing `license` field (Apache-2.0) and a LICENSE file. The package
  sets `publishConfig.access: "public"` and its sources carry SPDX Apache-2.0
  headers, but npm would have listed it as unlicensed. (Note: no other
  workspace in this repo declares a `license` field either — a repo-wide sweep
  is worth doing separately.)
- Align the version to 0.3.38. It was 0.3.37 while root and all 13 siblings
  were 0.3.38, so `bunset --patch --all` would have opened a permanent
  one-patch offset between bootstrap and everything that depends on it.
- Make `registerAllDefaults(registry)` required again, as it was when the
  function was module-private. It mutates whichever container it is handed, and
  it is now exported three ways; a defaulted global turns "installed defaults on
  the wrong registry" from a compile error into silent behavior. This matches
  the repo convention of forcing callers to be explicit rather than defaulting.
- `vitest.setup.ts` now calls `bootstrapWorkglow()` (equivalent with no logger
  option). It cannot pass `globalServiceRegistry` explicitly: `@workglow/util`
  does not resolve from the repo root under the isolated linker, and importing
  `packages/util/src` directly would load a second copy of the module and
  populate a different registry object than the tests read.
- Add `packages/test/src/test/util/BootstrapPackageExports.test.ts`, covering
  the published contract: every `exports` leaf resolves to a file that exists,
  and the node entry point really exports the bootstrap API. Nothing in CI
  exercised the build output before, so a typo'd exports path or a build that
  emitted nothing would have shipped silently.
- Add README (including the all-defaults dependency-weight tradeoff) and
  CHANGELOG, and document the package in .claude/CLAUDE.md so contributors add
  registrations here rather than in the now-inert re-export shim.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ated-context example

The README and `.claude/CLAUDE.md` both stated the package's raison d'être as
"Workglow does not self-register defaults at import time" / "Nothing
self-registers at import time". That is inverted: all 14 `register*Defaults`
functions run at module scope against `globalServiceRegistry`, and each defaults
its `registry` parameter to it. A reader who trusts the old text concludes the
global registry is empty until `bootstrapWorkglow()` runs and will mis-diagnose
ordering bugs — `getLogger()` also lazily self-registers, so "it worked without
bootstrap" looks impossible.

State the real invariant instead: registrars self-register on the global
registry when their module happens to be imported, so what is populated is
import-order dependent; `bootstrapWorkglow()` is the guarantee that installs the
full set in dependency order; an isolated registry gets nothing until
`registerAllDefaults(registry)` is called explicitly.

Second defect: the isolated-context example passed an option no run API accepts.
`Task.run(overrides, runConfig)` takes input overrides first, and neither
`IRunConfig` nor `TaskGraphRunConfig` has a `context` key — `IRunConfig` carries
`registry?: ServiceRegistry`. With a loosely typed Input, `run({ context: ctx })`
is treated as an input override named `context`, `TaskRunner` keeps its
`globalServiceRegistry` default, and `ctx.dispose()` tears down a registry the
run never touched — the exact process-wide-mutation hazard the rest of the
README argues against. Corrected to `run({}, { registry: ctx.registry })` in the
README and in the `createOrchestrationContext` JSDoc, and pinned by a new
doc-conformance test that asserts both the correct routing and the old snippet's
silent fallback to the global registry.

Also splits `ServiceRegistry` into a top-level `import type` in
`registerAllDefaults.ts` (it is used solely as a parameter type), and syncs
`bun.lock` with the 0.3.38 version bump.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6huUY7hSkRbjun1P9HKsz
The bootstrap package shipped a third `bun` entry whose source was
byte-identical to `src/node.ts`, so Bun loaded the same code through a
separate bundle and a separate declaration file. Removing it makes Bun
resolve the default `import` condition to `dist/node.js` and restores the
two-target build every other non-vendor package uses.

This also unbreaks the BunExportConditions fixture, which pins the set of
packages whose Bun implementation genuinely differs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013oVdDSRMJeALBPLDQf3DgH
The package holds no test files, so `bun test` exits non-zero with
"0 test files matching". Every workspace that does declare `test` uses the
shared vitest project form, and a package with no tests omits the key
entirely — turbo then produces no task for it, and the dependent
`@workglow/test` project still covers the package on a changed-files run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013oVdDSRMJeALBPLDQf3DgH
…havior

The file claimed both halves of the isolated-context example were pinned,
but it only reproduced the shapes in TypeScript — nothing read the README or
the `createOrchestrationContext` JSDoc, so either could rot back to
`task.run({ context: ctx })` with the suite still green.

Adds three cases that read the two documents: the README shows the registry
in the run config, no README fence passes the context as an input override,
and the JSDoc example matches. Blocks are selected by content rather than by
the heading above them, so a heading rename cannot make them pass vacuously.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013oVdDSRMJeALBPLDQf3DgH
claude and others added 7 commits August 13, 2026 17:22
…from run-fns

Two guards for the package this branch adds.

Every workspace here is released as one family: `bunset --patch --all` bumps
each manifest from its OWN current value, and `workglow` pulls its siblings in
as `workspace:*`, resolved at publish time. So a workspace left one version
behind is not cosmetic but permanent -- the next `--all` takes the family to
N+1 and republishes the straggler at N, still behind, while the meta-package
ships declaring a stale dependency. Nothing in the release path notices.
WorkspaceVersions asserts the root and all workspaces agree, private ones
included, since `--all` bumps those too.

The eslint restriction blocks `@workglow/bootstrap` (and `workglow/bootstrap`)
from run-fns: a run-fn executes in a worker with its own globalServiceRegistry,
so bootstrapWorkglow would mutate a registry the main thread never sees. Blocked
wholesale rather than by importNames -- every export registers services, so a
name list would be the entire surface and would drift on the next addition.
eslintRestrictedImports pins the config so the rule cannot be dropped silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014A7TyLctSy4L1VpxViqhjg
Merging main brought the family to 0.3.39, but packages/bootstrap is new on
this branch so nothing carried it along -- leaving the one straggler the
WorkspaceVersions guard exists to catch. Left alone it would stay behind
permanently: `bunset --patch --all` bumps each manifest from its own current
value, so the next release takes the family to 0.3.40 and republishes this at
0.3.39, while workglow ships declaring a stale workspace:* dependency.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014A7TyLctSy4L1VpxViqhjg
Extracting `registerAllDefaults` preserved all 14 calls, but nothing asserted
the resulting set — and on this code base the obvious test cannot fail. Every
`register*Defaults` module ALSO self-registers on the GLOBAL registry at module
scope (`registerLoggerDefaults();` at the bottom of `LoggerRegistry.ts`, and
thirteen siblings), and `registerAllDefaults.ts` imports all of them. Delete
`registerTabularStorageDefaults(registry);` from the body and
`bootstrapWorkglow()` still yields a fully populated global registry by import
side effect; the whole suite passes.

The damage lands on `createOrchestrationContext()`, whose registry gets nothing
it was not explicitly handed: a task run with `{ registry: ctx.registry }` and a
`format: "storage:tabular"` input then receives the raw id string instead of the
repository. No error, wrong value.

So the fixtures are asserted against a bare `new ServiceRegistry(new
Container())`, the only place the assertion means anything, and the
anti-vacuity case comes first — a bare registry has none of the 15 tokens
before the call, so the rest cannot pass on an already-populated one. Exact
fixtures for the tokens, the 7 resolver prefixes and the 6 compactor prefixes
(`image` resolves a data URI it cannot round-trip, so it registers no
compactor), failing in both directions.

Ordering gets a source-text guard instead, because the runtime cannot see it:
`getInputResolvers` SELF-HEALS — handed a registry with no `INPUT_RESOLVERS`
map it registers one and carries on — so moving a primitive container after its
nine consumers produces an identical registry and a green suite, until a
pre-registered map is silently replaced by the healed one.

Verified both directions by hand: deleting the tabular call fails 5 of these 6
cases and none of the three existing bootstrap tests; moving
`registerInputResolverDefaults` to the end fails only the ordering case.
…okens

The README and the `registerAllDefaults` JSDoc both said defaults register with
`registerIfAbsent`, so an earlier explicit registration "is never overwritten".
That holds for the `registry.registerIfAbsent(TOKEN, ...)` half and not for the
other one: `registerInputResolver` / `registerInputCompactor` are an
unconditional `resolvers.set(formatPrefix, fn)` — last writer wins — and nine of
the fourteen calls register one of each.

The half-truth is worse than a plain error would have been, because the example
the README picked to illustrate it (a custom storage backend, a factory token)
is exactly the case that DOES survive. A consumer following it registers
`registerInputResolver("model", myCatalogResolver)` during plugin init, calls
`bootstrapWorkglow()` at startup, and every `format: "model"` input silently
resolves from the built-in MODEL_REPOSITORY instead of their catalog — no
warning, and nothing in the registry to inspect.

Both documents now state the split and the rule it implies: custom resolvers go
in AFTER `bootstrapWorkglow()` / `registerAllDefaults()`. The README shows the
wrong order and the right one side by side.

Four cases make it executable rather than prose: a pre-registered factory token
survives, a pre-registered resolver and compactor are both overwritten, and the
documented order keeps the custom resolver without disturbing its neighbours.
…3.39

The version bump reached `packages/bootstrap/package.json` but not the
lockfile: `bun.lock` still recorded `0.3.38` for that workspace while the other
41 read `0.3.39`, and `packages/bootstrap/CHANGELOG.md` was still headed
`## 0.3.38`.

`bun install --frozen-lockfile` — any consumer CI, any reproducible install —
fails outright on that. A plain `bun i` succeeds and rewrites `bun.lock`, so
every developer instead gets an unexplained dirty lockfile on first install,
and repo CI (which runs bare `bun i`) never reports it.

`bun install` touched exactly the one line, which is the whole diff here.
`bun install --frozen-lockfile` now exits 0.

`WorkspaceVersions.test.ts` gains the cross-check that would have caught it: it
read manifests only, so the straggler it was added for slipped past. It now
parses `bun.lock` (JSONC — trailing commas stripped) and compares the version
recorded per workspace against that workspace's manifest, naming any entry that
disagrees. It asserts more than 40 entries first, so a degraded parse or a
moved `workspaces` key fails loudly instead of comparing an empty set and
passing vacuously.
…hxoj5s-bootstrap

fix(bootstrap): pin the registered set, correct the overwrite claim, regenerate bun.lock
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Extract @workglow/bootstrap from duplicated bootstrap registries

2 participants