refactor(bootstrap): extract @workglow/bootstrap from duplicated registries - #672
Draft
sroussey wants to merge 13 commits into
Draft
refactor(bootstrap): extract @workglow/bootstrap from duplicated registries#672sroussey wants to merge 13 commits into
sroussey wants to merge 13 commits into
Conversation
Coverage Report
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This was referenced Aug 5, 2026
Merged
sroussey
force-pushed
the
claude/libs-issues-triage-prs-mh6x2o-574
branch
from
August 6, 2026 18:40
dc40e15 to
37d71db
Compare
sroussey
marked this pull request as draft
August 7, 2026 01:38
sroussey
force-pushed
the
claude/libs-issues-triage-prs-mh6x2o-574
branch
from
August 8, 2026 18:21
37d71db to
d71631c
Compare
…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
sroussey
force-pushed
the
claude/libs-issues-triage-prs-mh6x2o-574
branch
from
August 13, 2026 03:17
afe6a4e to
71cea61
Compare
…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
…iage-prs-mh6x2o-574
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #574
bootstrapWorkglow()(inpackages/workglow/src/bootstrap.ts) andbootstrapTestRegistry()(inpackages/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 toglobalServiceRegistry, 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/bootstrappackage.@workglow/bootstrapis declared withpublishConfig.access: "public", andscripts/publish-workspaces.tsdiscovers packages viafindWorkspaces()globbing the rootworkspacespatterns — so it will be picked up and published automatically on the nextpublish-workspacesrun, with no allowlist to add it to.Please confirm the npm name
@workglow/bootstrapis available (and owned by the org) before merging. Version starts at0.3.37to stay in lockstep with the rest of the release train.Other reviewer-facing notes
registerAllDefaultsbecomes public API. It was a private function inside the meta-package; it is now exported from@workglow/bootstrapand, via the unchangedexport * from "./bootstrap"in the root barrel, fromworkglowitself. Its signature was widened toregisterAllDefaults(registry: ServiceRegistry = globalServiceRegistry): voidso it serves both former call styles.bun run link-all/dev-link(sec, embarc-data, builder) needs a re-link to pick up the new workspace package.workglownever depended on@workglow/test, so no import cycle existed. The real constraints were (a) the isolated linker (bunfig.tomlsetslinker = "isolated", and the repo root has no@workglow/*innode_modules), which is whyvitest.setup.tscould not import a package specifier; and (b) not wantingpackages/testto 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
packages/bootstrap, mirroringpackages/knowledge-base's packaging (script block, conditionalexportsmap,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.ts—BootstrapOptions,WorkglowContext,bootstrapWorkglow(),createOrchestrationContext().src/{browser,node,bun}.ts→export * from "./common".packages/workglow/src/bootstrap.tsis now justexport * from "@workglow/bootstrap", preserving its license header and module doc. This keeps theworkglow/bootstrapsubpath and the root barrel working with zero public API change.auto-bootstrap.tsdeliberately stays inworkglow— it importsTsLogLogger, which needs thetslogdependency only the meta-package carries, and it is listed in that package'ssideEffects. It required no edit.vitest.setup.tsnow callsregisterAllDefaults(), imported by source path (./packages/bootstrap/src/bootstrap/registerAllDefaults) because of the isolated linker noted above.packages/test/src/binding/bootstrapTestRegistry.ts— verified nothing else imported it (it was not inpackages/test/src/common.ts;vitest.setup.tswas its only consumer).Verification
All commands run locally; real results below.
bun run build:types@workglow/bootstrap:build-typesandworkglow:build-typesboth executed (cache miss) — proves the tsc project-reference graph is acyclic.bun run buildbun run rebuild(cold,clean:dist+--force)turbo.json(build-jsdeclaresdependsOn: ["^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/workglowbun run build-js(direct)assertNoExportCollisionsran clean over the barrels, and dynamically imported the new starred source without a resolution failure. ConfirmedregisterAllDefaultscollides with no existing export.bun scripts/test.ts util vitestbun scripts/test.ts task vitestbun scripts/test.ts mcp vitestregisterMcpServerDefaults.bun run formatThe 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:workglow/bootstrapand theworkglowroot barrel resolve to the same function bindings (no ESM ambiguity), andcreateOrchestrationContext()still yields an isolated, disposable registry.🤖 Generated with Claude Code
https://claude.ai/code/session_01PwyJuFrJnibKvrrk8Fa4Fn
Generated by Claude Code