Skip to content

docs(spec): cli-extension TSDoc step 2 "Discover" says what loads an oclif plugin into os - #21443

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-21286-cli-extension-tsdoc
Oct 2, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-21286-cli-extension-tsdoc

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21286

Clause-②: no

What changes

  • packages/spec/src/kernel/cli-extension.zod.ts: the module TSDoc, "How It Works", step 2 "Discover" is rewritten. No schema, export, type or .describe() change.
  • content/docs/references/kernel/cli-extension.mdx: regenerated with the repo's generator (pnpm --filter @objectstack/spec build, then gen:docs), not edited by hand. It moves by exactly the same five lines.
  • .changeset/21286-cli-extension-tsdoc-discover.md: an @objectstack/spec patch. The package's files[] lists src/**/*.zod.ts, so this source text ships in the tarball.

Sentences changed (before, then after)

Only one sentence in the module TSDoc was false. It is step 2, "Discover".

Before (the placeholder PKG stands for an angle-bracketed placeholder in the source):

  1. Discover — The main CLI (@objectstack/cli) lists the plugin in its oclif.plugins array, or users install it via os plugins install PKG.

After:

  1. Discover — oclif loads a plugin that the CLI's own package.json lists in both oclif.plugins and dependencies. @objectstack/cli lists none and ships no plugin manager, so os plugins is not a command. To add a plugin's commands to os, build an os distribution: a package whose own package.json lists the plugin in both places.

Both halves of the old sentence were false. @objectstack/cli keeps no oclif.plugins list, and os plugins install is not a command. The new text matches packages/cli/README.md ("How Plugin Extension Works", step 3) and the callout in content/docs/plugins/index.mdx, so all three surfaces say the same thing.

Sentences measured and kept

Each sentence was checked against @objectstack/cli and @oclif/core 5.1.2 on origin/main 3a6d92f78b:

  • Intro: "This enables third-party packages … to register new CLI commands via oclif's built-in plugin system." True. Probe legs C and E (below) show a third-party plugin registering a command through oclif's plugin loader.
  • Step 1, "Declare": true. The plugin's own oclif section is what the loader reads.
  • Step 3, "Load": true. Once a plugin is loaded, its pattern-strategy commands register (acme-hello in legs B, C, E and F).
  • "Plugin Package Contract" and the two examples: true. They describe a valid oclif plugin.
  • "Migration from Commander.js": true. git grep finds no commander and no contributes.commands read in packages/cli/src, so objectstack.config.ts plugins do not decide CLI commands.

Measurements

What packages/cli/package.json declares. jq '.oclif.plugins' answers null. The oclif keys are bin, dirname, commands, hooks and topicSeparator, and no dependency field names an @oclif/plugin-* package. This confirms the PM's mechanism assumption 1.

Loader probe. It calls Config.load() from @oclif/core 5.1.2, which is the call bin/run.js's run() makes. A throwaway plugin @acme/os-ext carries one command, acme-hello. packages/cli/dist was not built, so the root's own command count is 0 in every leg. The probe reads the plugin list and whether acme-hello registered.

leg root data dir (OS_DATA_DIR) plugins loaded acme-hello
A packages/cli empty @objectstack/cli only no
B packages/cli package.json lists the plugin as type: user @objectstack/cli, @acme/os-ext (user) yes
C a distribution: oclif.plugins plus dependencies empty dist root, @acme/os-ext (core) yes
D a distribution: oclif.plugins plus devDependencies only empty dist root only no
E a distribution listing @objectstack/cli and the plugin empty dist root, @objectstack/cli, @acme/os-ext (all core) yes
F packages/cli package.json lists the plugin as type: link @objectstack/cli, @acme/os-ext (link) yes

Legs C and D are the rule the new step states, with its control. Leg E shows that a distribution loads @objectstack/cli itself as a core plugin. That reading is at the plugin level only: @objectstack/cli's own commands are NOT MEASURED there, because its dist was not built.

The reference page moves by exactly the changed sentences. These are the generator's own checks:

  • With the base tree's page written back on disk (the old sentence present, the new one absent), pnpm check:docs exits 1 and names one file, content/docs/references/kernel/cli-extension.mdx (out of date).
  • The page was then restored from HEAD, and git hash-object matched the HEAD blob df95a843d9.
  • pnpm --filter @objectstack/spec check:generated (15 artifacts) is green at that tree.

Other mentions. git grep over packages/spec/** and content/docs/** for os plugins, plugins install, plugin-plugins and oclif.plugins found no other spec-surface hit. The remaining hits are content/docs/plugins/index.mdx:402-410, the callout, which is already correct. See the acceptance notes for objectstack plugin install (singular).

Acceptance notes (not changed here)

  • os also loads plugins from its data directory. Legs B and F show this. @oclif/core's user-plugin loader runs whether or not a plugin manager is installed. It reads package.json in the data directory ($OS_DATA_DIR, else $XDG_DATA_HOME/objectstack, else ~/.local/share/objectstack) and loads type: user and type: link entries. Nothing os ships writes that file: it is the store @oclif/plugin-plugins maintains. The new step does not name it and does not claim the distribution is the only route. It also bears on the separate OclifPluginConfig enforce-or-remove question. One more consequence: packages/cli/bin/run.js's comment that a linked-plugin path "is not reachable today" holds only for os plugins link. A hand-written type: link entry still loads (leg F). Carrier: none (domain:cli's file).
  • content/docs/protocol/kernel/lifecycle.mdx (lines 194, 287, 357-403) documents objectstack plugin install, plugin enable and plugin test. packages/cli/src/commands/plugin/ holds only build, publish and sign, and packages/cli/README.md says the group has no install. This was found by reading the files, not by running a built binary. It is a hand-written docs page outside this card's surface. Carrier: none.
  • The code comment under the module TSDoc in cli-extension.zod.ts (the retirement note) calls OclifPluginConfigSchema "that live surface". It is not part of the TSDoc block, and whether the surface is live is the maintainer's enforce-or-remove question, so it is left alone.

Verification

Every reading below was taken at 52ece7d949, the PR's head.

  • pnpm --filter @objectstack/spec build exits 0 (the lock printed VERDICT command-exit 0). Then gen:docs exits 0 ("Generated 227 files"), and check:generated exits 0 ("All 15 generated artifacts are up to date").
  • pnpm --filter @objectstack/spec test exits 0: 600 test files, 17681 passed, 1 todo.
  • pnpm --filter @objectstack/spec typecheck exits 0, with check:test-typecheck: OK.
  • The red leg of check:docs is described under Measurements above.
  • Gate derivation: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, with no paths, listed 104 commands for this change set. Each was run with its exit code captured before any pipe.
    • 103 exit 0. Five of them first refused with exit 3 (PREREQUISITE NOT MET): check:doc-formula-expressions, check:doc-security-posture, check:skill-examples, check:docs-transcript-drift and check:lean-entry-closure. Each exits 0 after its prerequisite was built with turbo run build --filter=@objectstack/formula --filter=@objectstack/lint --filter=@objectstack/client-react --filter=@objectstack/objectql (exit 0).
    • check:dual-build-cjs-loads is NOT MEASURED. It needs every package's dist (a full pnpm build, 86 packages). This narrowing is declared: CI's Lint & Repo Gates runs it on a full build.
    • --ran reconciliation: "104 derived famil(ies) accounted for — 103 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)".
  • Left to CI and not run here: the artifact-roster, wide-population and path-scheduled CI families, plus the type-check lanes that the derivation lists outside its 104.
  • The loader probe is a throwaway script and is not committed.

Generated by Claude Code

claude added 2 commits October 2, 2026 16:33
…in into os

The module TSDoc's step 2 said @objectstack/cli lists the plugin in its
oclif.plugins array, or that users install it with `os plugins install`.
@objectstack/cli declares no oclif.plugins and ships no plugin manager, so
neither is true. The step now names the oclif rule (oclif.plugins plus
dependencies of the CLI's own package.json) and the os-distribution route.

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <noreply@anthropic.com>
Regenerated with `pnpm --filter @objectstack/spec gen:docs` after a spec
build; the page lifts the module TSDoc, so only step 2 moves.

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/s label Oct 2, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation tooling labels Oct 2, 2026
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 1 changed file(s) yielded no anchor (packages/spec/src/kernel/cli-extension.zod.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/src/kernel/cli-extension.zod.ts) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 138 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json c2c21f357ce3cfc7a5942bcc27ea4921196f8ccb → packageMentionDocs.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 52ece7d949d95d10fcb9e7b7f597f512009d0d3a
Local-runs: none

Inputs: card #21286 (body; triage 5944162302, claim 5956684619, dev report 5957400136); PR #21443 (body, file list, the net diff of claude/issue-21286-cli-extension-tsdoc against origin/main, merge-base 3a6d92f78b, 3 files, +19/-4); the check-runs on this head; origin/main blobs of packages/cli/package.json, packages/cli/bin/run.js, packages/cli/README.md, content/docs/plugins/index.mdx, packages/spec/src/kernel/cli-extension.zod.ts, packages/spec/src/kernel/manifest.zod.ts, content/docs/protocol/kernel/lifecycle.mdx, .github/workflows/ci.yml and lint.yml. The @oclif/core source was read from the registry tarball of @oclif/core@5.1.2, the version pnpm-lock.yaml pins for packages/cli, because this checkout carries no node_modules; nothing was installed, built, run or re-run.

① Derived judgments

Accept set and public surface:

  • packages/spec/src/kernel/cli-extension.zod.ts: the only hunk is the module TSDoc, old lines 17-18 to new lines 17-21. OclifPluginConfigSchema, every .describe() string, OclifPluginConfig, the retirement comment and the imports are byte-identical to origin/main. No schema, export, type, default or .describe() moves. RIGHT.
  • content/docs/references/kernel/cli-extension.mdx: an AUTO-GEN surface (packages/spec/scripts/build-docs.ts lifts the module doc block). Its one hunk, lines 20-24, is the TSDoc hunk with the comment prefix stripped, word for word; front matter, generated banner and every other line are unchanged. The page moves by exactly the TSDoc lines. RIGHT.
  • .changeset/21286-cli-extension-tsdoc-discover.md: new file, judged under ②.
  • No exports map, files[], generated JSON schema, liveness ledger or API-surface baseline moves. The accept set is unchanged, so Clause-②: no is the right declaration.

Each new sentence, read against the code at origin/main:

  1. "oclif loads a plugin that the CLI's own package.json lists in both oclif.plugins and dependencies." TRUE. lib/config/plugin-loader.js loadCorePlugins() reads rootPlugin.pjson.oclif.plugins and filters it through findMatchingDependencies(rootPlugin.pjson.dependencies ?? {}, corePlugins) (lines 58-63), so a name under devDependencies alone never loads as a core plugin; that is the dev's leg D control, and the README's "matches oclif.plugins names only against dependencies" is the same reading.
  2. "@objectstack/cli lists none and ships no plugin manager, so os plugins is not a command." TRUE. packages/cli/package.json on origin/main has oclif.bin, dirname, commands, hooks and topicSeparator and no plugins key; no dependency field of any kind names @oclif/plugin-plugins (the card's "sits only in devDependencies" predates cli: drop the never-loaded @oclif/plugin-help and @oclif/plugin-plugins from oclif.plugins, and correct every published text that says os plugins works #21285 and is now stale); packages/cli/src/commands/ holds a plugin/ directory (build, publish, sign) and no plugins, so the root plugin's pattern discovery over dist/commands cannot yield a plugins topic either.
  3. "To add a plugin's commands to os, build an os distribution: a package whose own package.json lists the plugin in both places." TRUE as a route, by sentence 1: the distribution is oclif's root plugin, and loadCorePlugins reads that root's package.json. It agrees with packages/cli/README.md ("How Plugin Extension Works", step 3) and the callout in content/docs/plugins/index.mdx (Step 3), so the three author-facing surfaces now say one thing.

Overstate or understate? The dev's probe finding is CONFIRMED from source. loadChildren() calls loadUserPlugins() ahead of loadCorePlugins() (plugin-loader.js 27-34); loadUserPlugins() runs whenever opts.userPlugins !== false (169-188), and Config.load passes this.options.userPlugins straight through (config.js 302-310), which bin/run.js's bare run(argv, import.meta.url) leaves undefined. It reads package.json under dataDir and loads the entries typed user (line 180) and link (line 181). dataDir is this.scopedEnvVar('DATA_DIR') || this.dir('data') (config.js 279); with oclif.bin = os the scoped key is OS_DATA_DIR (scopedEnvVarKeys, 534-538); dir('data') is $XDG_DATA_HOME, else ~/.local/share, joined with oclif.dirname = objectstack (165-169). So a second loader route is live in the published os, with no plugin manager installed, exactly as reported.

Is the new step a false exclusivity claim? No. The words are "oclif loads a plugin that the CLI's own package.json lists in both" and "To add a plugin's commands to os, build an os distribution". Neither sentence carries "only", "the only way" or "otherwise never": the first is a true positive rule, the third names the route all three surfaces document. The step UNDERSTATES by not naming the data-dir route, and that is the safe direction under Prime Directive #10's corollary: nothing os ships writes that file, and with settings.enableAutoTranspile = false a type: link TypeScript plugin is not transpiled (ts-path.js 262-265), so naming it as a route would advertise an unsupported path. Reading: the step names the supported route without claiming exclusivity; not a false claim.

Kept block spot-check, all TRUE at origin/main: the intro ("via oclif's built-in plugin system"); step 1 (plugin.js reads pjson.oclif.commands at 202 and pjson.oclif.topics at 114); step 3 (_manifest() discovery and command registration, 204-212); the "Plugin Package Contract" example (strategy and target are what determineCommandDiscoveryOptions requires, plugin.js 60-74); "Migration from Commander.js" (manifest.zod.ts 580-612 records commands among the tombstoned contributes members, and git grep over packages/cli/src finds no commander import, no contributes.commands read and no loadPluginCommands).

One reading, not a defect: neither the new step nor the README nor the plugins doc says how a distribution gets os's own commands (the dev's leg E: list @objectstack/cli itself in both places). The three surfaces are silent together, so the step opens no new gap.

② Semver level

@objectstack/spec patch, Clause-②: no. RIGHT. packages/spec/package.json files[] includes src/**/*.zod.ts, so the corrected source text ships in the tarball: skip-changeset ("publishes nothing from any released package") would be wrong, and the card is a bug, which Post-Task Checklist step 3 gives a patch, never none. Nothing in the accept set widens or narrows, so minor has no basis and Clause-②: no is right. .changeset/config.json keeps @objectstack/spec in the fixed group, so this patch bumps the group; that is the standing convention, not this PR's choice.

Changeset prose, sentence by sentence: (1) "no longer tells plugin authors to run os plugins install" — true, the clause is gone. (2) the paraphrase of the old step — matches origin/main lines 17-18. (3) "@objectstack/cli declares no oclif.plugins and ships no plugin manager, so os plugins is not a command" — true (① sentence 2). (4) "oclif loads a plugin that the CLI's own package.json lists in both oclif.plugins and dependencies" — true (plugin-loader.js 61). (5) "build an os distribution whose own package.json lists the plugin in both places" — the same reading as ① sentence 3: a route, not an exclusivity claim. (6) "The generated reference page carries the same text" — true per the diff. (7) "Documentation only. No schema, export or type changes." — true. The body carries no model identifier. (contract-review.md places .changeset prose with the dispatching seat at ACCEPT; this is the reading it can adopt.)

③ Boundary flags

Dev deviations:

  • check:dual-build-cjs-loads NOT MEASURED. The check-run on this head that measures it is Build Core (ci.yml job build-core, step "Every published require entry point actually loads", line 2265), NOT Lint & Repo Gates as the dev report says; lint.yml 6097-6101 records that Lint & Repo Gates never builds and that the gate lives in Build Core for that reason. Build Core is completed with conclusion success on this head, so CI closes the declared narrowing; the misnaming is a reporting slip with no effect on the verdict.
  • Trailer choice. Both commits end with the model-free pair AGENTS.md prescribes (Claude-Session: with the seat's session URL, then the Co-authored-by: Claude line), and the PR body closes with the session-URL footer AGENTS.md assigns to PR bodies. RIGHT: AGENTS.md's own precedence sentence settles the harness reminder, and the pre-push hook refuses a model identifier in that pair.
  • No labels written; skip-changeset inapplicable; size/s set by another actor and left. RIGHT, by the files[] reasoning in ②.

open_questions: none declared; nothing to answer.

out_of_scope_findings, each a reading and an escalation to the seat; this review files nothing:

  1. content/docs/protocol/kernel/lifecycle.mdx lines 194, 287 and 357-403 document objectstack plugin install, plugin enable and plugin test. CONFIRMED by reading: git ls-tree origin/main packages/cli/src/commands/plugin/ lists build.ts, publish.ts and sign.ts only, and packages/cli/README.md line 151 says the group has no install (ADR-0025's install half not implemented). Reading: a hand-written docs page advertising three commands the CLI does not register; Prime Directive chore: version packages #10's corollary (declared is not delivered) with a named landing site and a published-docs reach, so it meets the filing gate. ESCALATED to the seat as a filing candidate for domain:cli or the docs lane. Outside this card; leaving it untouched here is RIGHT.
  2. The user-plugin loader is live through the data dir. CONFIRMED from plugin-loader.js 169-188 and config.js 279, 165-169 and 534-538 (quoted in ①). Two consequences for the seat to carry: (a) packages/cli/bin/run.js lines 88-89 say the linked-plugin path "is not reachable today"; it is unreachable through os plugins link, but a hand-written type: link entry under $OS_DATA_DIR (else $XDG_DATA_HOME/objectstack, else ~/.local/share/objectstack) still loads, and with settings.enableAutoTranspile = false tsPath returns at ts-path.js 263 before the plugin.type !== 'link' exemption at 269, so a TypeScript-source link plugin gets its compiled path only; the comment overstates by one clause. (b) Because oclif.bin is os, oclif reads OS_DATA_DIR, OS_CACHE_DIR, OS_CONFIG_DIR, OS_BINPATH, OS_NPM_REGISTRY, OS_DEBUG and OS_DISABLE_THEME (config.js 277-281, 325, 712), inside the OS_ namespace Prime Directive [WIP] Create a new release version #9 reserves for ObjectStack-owned vars, and none follows OS_{DOMAIN}_{FEATURE}. Both are input for the maintainer's OclifPluginConfig enforce-or-remove question (card body, "Related maintainer question"). ESCALATED; owner domain:cli.
  3. The retirement comment under the TSDoc calls OclifPluginConfigSchema "that live surface". Reading: oclif does read the shape the schema describes (plugin.js 202 and 114), so "live" holds for oclif's reader; whether an os user can reach it short of a distribution or a hand-written data-dir entry is the maintainer's open question. The comment sits outside the TSDoc block and outside this card; leaving it is RIGHT. ESCALATED as part of that standing question.

Check-runs on this head at the final read (2026-10-02T17:32Z): 35 check-runs, 33 completed with conclusion success, 2 completed skipped (Console Pin Gate, Packed-tarball smoke (opt-in), both by their path filters), 0 failure, 0 still running. Build Core, Lint & Repo Gates, all six Test Core shards, all four Type Check lanes, Build Docs, Check Changeset and Governed Surface Queue Guard are among the 33. The PR is a draft with auto_merge null, and its file list touches no governed path, so Prime Directive #14 reserves nothing here; this record exists because packages/spec/src/** non-test is a contract surface.

Implemented-by: claude/issue-21286-cli-extension-tsdoc
Reviewed-by: session_01UtnxvdiN376GF3sgXwAw4d

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 2, 2026 17:36
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 2, 2026 17:36
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit 53fd35e Oct 2, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21286-cli-extension-tsdoc branch October 2, 2026 17:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec(kernel): the cli-extension.zod.ts TSDoc tells plugin authors to os plugins install, a command os has never registered

2 participants