Skip to content

docs(bootstrap): correct the self-registration invariant and the isolated-context example - #725

Merged
sroussey merged 1 commit into
claude/wonderful-turing-rjtcnx-bootstrap-polishfrom
claude/bootstrap-docs-fixes-p4d8qo
Aug 8, 2026
Merged

docs(bootstrap): correct the self-registration invariant and the isolated-context example#725
sroussey merged 1 commit into
claude/wonderful-turing-rjtcnx-bootstrap-polishfrom
claude/bootstrap-docs-fixes-p4d8qo

Conversation

@sroussey

@sroussey sroussey commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #720 — base is claude/wonderful-turing-rjtcnx-bootstrap-polish, not main.

#720's mechanical work was reviewed and is correct: the version bump, the exports/files shape, and making registerAllDefaults's registry parameter required all hold up. This PR is documentation corrections only, plus one import-style fix and a test that pins the corrected snippet.

1. "Nothing self-registers at import time" is false

packages/bootstrap/README.md:5 and .claude/CLAUDE.md:236 stated the package's raison d'être as "Workglow does not self-register defaults at import time" / "Nothing self-registers at import time, so a runtime has to install the defaults before any task runs".

All 14 register*Defaults functions do self-register on globalServiceRegistry at module scope, and each also defaults its registry parameter to it. Verified individually (line numbers as of dd97b0d):

module module-scope call
packages/util/src/logging/LoggerRegistry.ts :59
packages/util/src/di/InputResolverRegistry.ts :43
packages/util/src/di/InputCompactorRegistry.ts :42
packages/util/src/telemetry/TelemetryRegistry.ts :46
packages/util/src/worker/WorkerManager.ts :616
packages/util/src/media/imageHydrationResolver.ts :42
packages/util/src/credentials/CredentialStoreRegistry.ts :96
packages/ai/src/model/ModelRegistry.ts :94
packages/ai/src/provider/AiProviderRegistry.ts :400
packages/knowledge-base/src/knowledge-base/KnowledgeBaseRegistry.ts :177
packages/mcp/src/util/_server-registry/McpServerRegistry.ts :124
packages/storage/src/tabular/TabularStorageRegistry.ts :96
packages/task-graph/src/task/TaskRegistry.ts :216
packages/task-graph/src/task-graph/TransformRegistry.ts :38

(Two entries differ from the numbers a reader might expect: WorkerManager.ts is :616 and AiProviderRegistry.ts is :400 on this branch.)

Why it matters: a reader who trusts the old text concludes the global registry is empty until bootstrapWorkglow() runs, and will mis-diagnose ordering bugs. getLogger() (LoggerRegistry.ts:66-71) additionally self-registers lazily when the registry lacks LOGGER, so "it worked without calling bootstrap" looks impossible under the old doc.

Replaced with the real invariant in both files: registrars self-register on the global registry when their module happens to be imported, so what is populated depends on the import graph and is import-order dependent; bootstrapWorkglow() is the guarantee that installs the full set in dependency order, idempotently; an isolated registry gets nothing until registerAllDefaults(registry) (or createOrchestrationContext()) is called explicitly. The .claude/CLAUDE.md edit is confined to that one inverted sentence — the "implementation lives here / shim" sentences are untouched.

2. The isolated-context example passed an option no run API accepts

README.md:52-56 (and the createOrchestrationContext JSDoc at bootstrapWorkglow.ts:73, inherited from main) showed:

await task.run({ context: ctx });

Task.run(overrides, runConfig) takes input overrides first (packages/task-graph/src/task/Task.ts:222), and neither IRunConfig (ITask.ts:120) nor TaskGraphRunConfig (TaskGraph.ts:39) has a context key — IRunConfig carries registry?: ServiceRegistry (ITask.ts:252).

Failure mode, confirmed by running it: with a loosely typed Input the object is accepted as an input override named context; TaskRunner keeps its globalServiceRegistry default (TaskRunner.ts:140) because it only overrides when config.registry is set (TaskRunner.ts:1012); the run therefore mutates and reads process-wide state, and ctx.dispose() tears down a registry nothing ever touched — the exact hazard the rest of the README argues against. With a strictly typed Input it is a compile error instead.

Corrected in both places to await task.run({}, { registry: ctx.registry }), with a sentence explaining that the registry travels in the run config (second argument).

Pinned by a doc-conformance test

The repo already has the "transcribe the doc example into a test" pattern (packages/test/src/test/util/readme.test.ts), so the corrected snippet is now executable: packages/test/src/test/util/BootstrapReadme.test.ts asserts both halves — that run({}, { registry: ctx.registry }) reaches the isolated registry, and that the old run({ context: ctx }) shape silently falls back to the global one. Mutation-checked: reverting the first test to the old snippet fails it with expected true to be false.

This required adding @workglow/bootstrap to packages/test's devDependencies and tsconfig references (one line each). It is the only structural change in the PR — happy to drop the test and both lines if you'd rather keep this strictly docs-only.

3. Import-style fix

registerAllDefaults.ts kept ServiceRegistry in the value-import list after globalServiceRegistry was removed, though it is used solely as a parameter type. Split into a top-level import type, per CLAUDE.md's convention.

4. Lockfile sync (incidental)

bun.lock still recorded packages/bootstrap at 0.3.37 while #720 bumped package.json to 0.3.38. A plain bun install corrects it; included here so the next contributor's install doesn't produce unrelated churn. CI runs bun i (not --frozen-lockfile), so this was not breaking anything.

Two suggested LOW fixes I did not apply

Both were requested, but the stated premises do not survive checking, so applying them would have traded one wrong doc for another. Flagging rather than silently skipping.

packages/bootstrap/CHANGELOG.md heading. The premise was that every sibling opens with its package name and that leaving # Changelog would make the next release run produce a two-headed file. Neither holds:

  • Five packages open with # Changelog, not one: bootstrap, browser-control, indexeddb, javascript, mcp. All are public and released (all at 0.3.38).
  • bunset's writeChangelog (node_modules/bunset/src/changelog.ts:98-122) writes # Changelog only when the file does not exist; for an existing file it preserves the first line verbatim and inserts the new entry after it. browser-control has carried # Changelog across six releases with no second heading. So # Changelog is exactly what the tool generated for this new package, and renaming it would fight the generator rather than align with it.

The separator in the .claude/CLAUDE.md dependency graph. The parenthetical reason given was that providers/* does not depend on bootstrap — which is true (verified: nothing under providers/ lists @workglow/bootstrap; the only dependent in the repo is packages/workglow), but it argues against the change. In that graph means "tier below"; inserting one between bootstrap and providers/* would assert that providers depend on bootstrap. They are genuine siblings: both sit below ai, neither depends on the other. Consecutive un-arrowed lines are already the graph's sibling notation — the bottom tier lists test / workglow / debug the same way. The current rendering is correct as-is.

Verification

$ bunx tsc -b packages/bootstrap
(exit 0, no output)

$ bunx tsc --noEmit -p packages/bootstrap
(exit 0, no output)

$ bunx tsc -b --force packages/test
65 errors — identical file set to the pre-change baseline (also 65);
zero in any file this PR touches. All pre-existing, all TS2307/TS7006 from
optional providers not built in this environment (cactus, chrome-ai, mlx,
llamacpp-server, openrouter, stable-diffusion-server).

$ bun scripts/test.ts util vitest
Test Files  50 passed (50)
     Tests  685 passed | 10 skipped (695)
(49 files / 683 tests before this PR's new file)

$ bunx eslint packages/test/src/test/util/BootstrapReadme.test.ts
(exit 0)

$ bunx prettier --check <every changed file>
All matched files use Prettier code style!

The util section is where BootstrapPackageExports.test.ts and the new BootstrapReadme.test.ts live. Vitest runs were done in use-source mode; the tree was returned to dist mode before committing, so no package.json export churn is in the diff.


🤖 Generated with Claude Code

https://claude.ai/code/session_01K6huUY7hSkRbjun1P9HKsz


Generated by Claude Code

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 63.23% 29435 / 46546
🔵 Statements 63.1% 30511 / 48346
🔵 Functions 63.44% 5588 / 8807
🔵 Branches 52.5% 14709 / 28017
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
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 #2935 for commit 99be055 by the Vitest Coverage Report Action

…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
@sroussey
sroussey force-pushed the claude/bootstrap-docs-fixes-p4d8qo branch from 99be055 to 6f80e70 Compare August 8, 2026 18:21
@sroussey
sroussey merged commit 0618e10 into main Aug 8, 2026
11 of 13 checks passed
@sroussey
sroussey deleted the claude/bootstrap-docs-fixes-p4d8qo branch August 13, 2026 05:02
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.

2 participants