Skip to content

fix(cli): make os create emit a project that installs outside this monorepo - #15535

Merged
os-litant merged 3 commits into
mainfrom
claude/issue-14824-os-create-installs-outside-the-monorepo
Sep 4, 2026
Merged

fix(cli): make os create emit a project that installs outside this monorepo#15535
os-litant merged 3 commits into
mainfrom
claude/issue-14824-os-create-installs-outside-the-monorepo

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes #14824

os create is presented on four public documentation pages as a user-facing scaffolder. Every project it emitted was monorepo-shaped, so a developer who followed those docs got a project pnpm install refuses. The maintainer ruled that a documented developer-facing command must work for the developer who follows the docs — neither retired nor narrowed to an internal generator — so the default emission is now standalone, and a CI pin holds it that way.

The defect, reproduced before it was fixed

Both templates rendered into a temporary directory outside this repository, then installed:

$ cd /tmp/.../emit-old/plugin && pnpm install --no-frozen-lockfile
 ERR_PNPM_WORKSPACE_PKG_NOT_FOUND  In : "@objectstack/spec@workspace:*" is in the dependencies
 but no package named "@objectstack/spec" is present in the workspace

Identical for example, which declares two such specs. The emitted tsconfig.json carried extends: "../../tsconfig.json" in both, and — measured, not assumed — that did not resolve even inside this monorepo for the plugin template: packages/plugins/PKG/../../tsconfig.json is packages/tsconfig.json, which does not exist. Every real plugin here spells ../../../.

What the emission is now

The default placement is standalone:

before after
@objectstack/* specs workspace:* ^17.3.0 — the running CLI's own version
tsconfig.json extends: "../../tsconfig.json" self-contained, no extends
output location packages/plugins/plugin-NAME or examples/NAME ./plugin-NAME or ./NAME (or --dir)
pnpm build approvals none pnpm-workspace.yaml, rendered by init's own renderer

The version comes from getCliVersion() and the workspace settings from renderPnpmWorkspaceYaml() — both imported from init.ts rather than restated, because a restatement is the two-producer defect test/scaffold-workspace-consistency.test.ts already exists to catch, and it is how these templates drifted in the first place. os init itself is untouched.

The monorepo-internal placement survives as the explicit, documented --in-repo flag: workspace:* specs, an extends that resolves to the repository root (derived from where the template lands, so it can no longer be wrong by transcription), and no pnpm-workspace.yaml — a nested one would declare the directory its own workspace root and sever the very workspace:* links that placement exists for. The command refuses the flag outright when the current directory is not a pnpm workspace root, rather than emit the one thing it is no longer allowed to emit.

A fourth defect, found by the pin

Making the emission real made it type-check for the first time, and it did not:

src/index.ts(10,20): error TS7006: Parameter 'context' implicitly has an 'any' type.

The plugin template wrote an initialize method. Plugin carries an index signature, so the excess property was accepted but got no contextual type — and packages/core/src/plugin-loader.ts:384 refuses a plugin without init outright (Plugin init function is required). The template emits init now, and the warn callout in content/docs/protocol/kernel/index.mdx that told readers to rename it by hand is replaced by an accurate info note about the lifecycle.

The pin the ruling asked for

scripts/create-scaffold-smoke.sh scaffolds every template — enumerated from the built CLI, never a hand list — into a temp directory outside the repository, pins each publishable package to its pnpm pack tarball through the project's own overrides, installs, asserts nothing leaked to the registry, then runs the project's build and typecheck. The dependency specs in package.json are never rewritten: they are what is on trial. Tarballs are the honest stand-in for the registry because on a pull request the version the scaffold pins is by definition not published yet.

== os create plugin smoke-plugin  (from /tmp/objectstack-create-smoke.BlNkox/scaffold-plugin, default location)
  ok — 1 @objectstack dependency spec(s), all published ranges; tsconfig extends nothing
  ok — 1 pinned package(s) resolved from tarballs, 0 registry leaks
  ok — dist/ populated by 'tsc'
  ok — 2 @objectstack dependency spec(s), all published ranges; tsconfig extends nothing
  ok — 55 pinned package(s) resolved from tarballs, 0 registry leaks
  ok — dist/objectstack.json written: the stack loaded and its manifest parsed outside this monorepo
== os create scaffold smoke passed for: plugin example

It runs from .github/workflows/os-create-smoke.yml, paths-filtered rather than label-gated opt-in, plus a nightly. The sibling pack-smoke-optin.yml is label-gated because its defect class can only be recognised by the author; this one can only be introduced by editing a bounded, nameable set of files, and those files are the paths: list. init.ts is in that list even though this gate never runs os init: the standalone emission calls its exports, so naming the consumer and not the producer is the shape of coupling that lets a gate sit green through the change that breaks it. Not a required context — a paths-filtered job cannot be.

Ablation — both legs, proved on disk, from a committed tree

Predicted directions first, then measured. Restore leg for each: git checkout HEAD -- ABSOLUTE_PATH, proved by blob-hash equality and an empty git diff HEAD.

Leg 1 — restore workspace:* (predicted: the install pin reds, the schema/load pins stay green). The pre-fix create.ts was restored from the branch point (hash-object 779e4fec vs HEAD 3beda2c1; injected marker present twice, the fix's own symbol absent). The smoke reads packages/cli/dist, so the package was rebuilt and the mutation confirmed to have reached the artifact — ablation-dist-preflight: "marker present in 1 built file". The pin then went RED, at the default-location limb, which fires before the manifest limb:

::error::os create plugin wrote no package.json into .../scaffold-plugin/packages
 — the default location is still not the developer's directory

The manifest limb's own red was measured separately, by running that assertion's real bytes against the same pre-fix emission: workspace-protocol dependencies, which resolve nowhere outside this monorepo: @objectstack/spec: workspace:* / @objectstack/cli: workspace:*, exit 1 — green on the post-fix emission. test/scaffold-manifest-schema.test.ts stayed green throughout, as predicted.

Leg 2 — restore extends "../../tsconfig.json" alone (predicted: the typecheck half reds, the install half stays green). Held exactly: pnpm install exit 0, pnpm run typecheck exit 2 with error TS5083: Cannot read file '.../tsconfig.json', and the smoke's tsconfig limb red while its dependency limb passed. The two halves are independently held.

Restore proved: working tree clean against HEAD, and the fix's own exported symbol back in dist ("marker present in 2 built files"). ⚠️ workspace:* is deliberately not usable as the absent-marker here — the fixed file still contains that literal on purpose, because it is what --in-repo emits.

Verification

Local union re-run at 7ab7671bfcd, after the last commit:

  • bash scripts/create-scaffold-smoke.sh — exit 0, both templates (transcript above).
  • pnpm --filter @objectstack/cli exec vitest run over the scaffold suites — 7 files, 145 passed, 6 expected-fail. pnpm --filter @objectstack/cli typecheck — exit 0.
  • pnpm lint (repo-wide eslint . --no-inline-config) — exit 0.
  • The derived gate union (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, 109 commands after this diff) plus the 14 artifact-roster families whose silence this derivation says is evidence in neither direction. All green except two, both named below.

Two ratchets moved and were updated as part of the diff, not around it:

  • check:bash32-floor caught mapfile in the new smoke script (bash 4.0; the floor is 3.2, what macOS ships). Rewritten without it.
  • check-ci-filter-parity --self-test pins how many declarations a rollback of crosspkg would uncover; the three new content/docs inputs move it 16 → 19. The count, the prose and the verdict line are updated and each new page is asserted by name, the convention that block already uses. Measured green at the branch point beforehand, so the move is this diff's and nothing else's.

Two non-green results, neither a verdict on this tree:

Deliberately not in this PR


Generated by Claude Code

…onorepo

`os create` is presented on four public documentation pages as a user-facing
scaffolder, and every project it emitted was monorepo-shaped: `workspace:*`
dependency specs, a `tsconfig.json` extending `'../../tsconfig.json'`, and a
default output directory inside this repository. A developer who followed the
docs got a project `pnpm install` refuses.

The default emission is now standalone — published semver ranges pinned to the
running CLI's own version, a self-contained `tsconfig.json`, a
`pnpm-workspace.yaml` carrying the build approvals pnpm 11 requires, and the
developer's own directory as the output location. The monorepo-internal
placement survives as the explicit `--in-repo` flag.

The pin is `scripts/create-scaffold-smoke.sh`, wired into
`.github/workflows/os-create-smoke.yml`: it scaffolds every template into a
temp directory outside the repository, installs from packed tarballs, and runs
the project's own build and typecheck.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
…ffold on CI

Three follow-ons measured while building the pin for this card:

- the `plugin` template emitted an `initialize` method. `Plugin` carries an
  index signature, so the excess property was accepted but got no contextual
  type: the scaffold failed its own `strict` type-check with TS7006, and the
  kernel loader refuses a plugin without `init` outright. It emits `init` now,
  and the kernel protocol docs lose the warning that told readers to rename it.
- `test/create-plugin-docs-parity.test.ts` holds the template's emitted file set
  equal to the tree each of the three plugin doc pages prints. Both sides are
  derived, so a template that grows a file reddens every page that does not say
  so. The three pages are declared as cross-package test inputs and mirrored
  into turbo.json; `check-ci-filter-parity`'s rollback pin records them by name.
- `scripts/create-scaffold-smoke.sh` avoids `mapfile`, which does not exist on
  the bash 3.2 floor `check:bash32-floor` holds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
The `initialize` -> `init` change and its header paragraph were uncommitted when
an ablation's restore leg ran `git checkout HEAD -- create.ts`, which is a
normal, silent, exit-0 discard of a working tree that was the only copy. Both
edits are re-applied here, byte for byte, and the tree is committed before the
ablation is repeated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 22 documentable anchor(s).

13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/skills-reference.mdx (via node_modules (literal, a string literal in plugin; a string literal on a changed line))
  • content/docs/deployment/cli.mdx (via node_modules (literal, a string literal in plugin; a string literal on a changed line), package.json (literal, a string literal in plugin; a string literal on a changed line), tsconfig.json (literal, a string literal in plugin; a string literal on a changed line), os create (command, read off packages/cli/src/commands/create.ts))
  • content/docs/deployment/self-hosting.mdx (via package.json (literal, a string literal in plugin; a string literal on a changed line))
  • content/docs/deployment/tenancy-modes.mdx (via package.json (literal, a string literal in plugin; a string literal on a changed line))
  • content/docs/deployment/troubleshooting.mdx (via package.json (literal, a string literal in plugin; a string literal on a changed line))
  • content/docs/getting-started/examples.mdx (via package.json (literal, a string literal in plugin; a string literal on a changed line), tsconfig.json (literal, a string literal in plugin; a string literal on a changed line))
  • content/docs/getting-started/your-first-project.mdx (via package.json (literal, a string literal in plugin; a string literal on a changed line), tsconfig.json (literal, a string literal in plugin; a string literal on a changed line))
  • content/docs/plugins/development.mdx (via moduleResolution (symbol, a field of const object STANDALONE_COMPILER_OPTIONS), package.json (literal, a string literal in plugin; a string literal on a changed line), tsconfig.json (literal, a string literal in plugin; a string literal on a changed line))
  • content/docs/plugins/index.mdx (via package.json (literal, a string literal in plugin; a string literal on a changed line), tsconfig.json (literal, a string literal in plugin; a string literal on a changed line), os create (command, read off packages/cli/src/commands/create.ts))
  • content/docs/protocol/kernel/index.mdx (via package.json (literal, a string literal in plugin; a string literal on a changed line), tsconfig.json (literal, a string literal in plugin; a string literal on a changed line), os create (command, read off packages/cli/src/commands/create.ts))
  • content/docs/protocol/kernel/plugin-spec.mdx (via package.json (literal, a string literal in plugin; a string literal on a changed line), tsconfig.json (literal, a string literal in plugin; a string literal on a changed line), os create (command, read off packages/cli/src/commands/create.ts))
  • content/docs/protocol/objectql/schema.mdx (via package.json (literal, a string literal in plugin; a string literal on a changed line))
  • content/docs/upgrading.mdx (via node_modules (literal, a string literal in plugin; a string literal on a changed line), package.json (literal, a string literal in plugin; a string literal on a changed line))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx (via package.json (literal, a string literal in plugin; a string literal on a changed line))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: objectstack.config.ts (literal, 32 pages)
  • 8 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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.

Coarse fallback — 22 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 ed9d87653eb94b41c769206737110793ffd348bepackageMentionDocs.

Which tree this was computed on

This run read content/docs from 599f9553cda27151aa70554934b3ac36a9b41038 — the merge of head 7ab7671bfcd6b79acbdaef60b3d0f78ce6da134b into base ed9d87653eb94b41c769206737110793ffd348be, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 599f9553cda27151aa70554934b3ac36a9b41038 && git checkout 599f9553cda27151aa70554934b3ac36a9b41038
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ed9d87653eb94b41c769206737110793ffd348be 7ab7671bfcd6b79acbdaef60b3d0f78ce6da134b && git checkout -B drift-repro ed9d87653eb94b41c769206737110793ffd348be && git merge --no-ff 7ab7671bfcd6b79acbdaef60b3d0f78ce6da134b

node scripts/docs-audit/affected-docs.mjs --json ed9d87653eb94b41c769206737110793ffd348be

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs ed9d87653eb94b41c769206737110793ffd348be → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added ci/cd documentation Improvements or additions to documentation tests tooling labels Sep 4, 2026
@os-litant
os-litant marked this pull request as ready for review September 4, 2026 20:39
@os-litant
os-litant enabled auto-merge September 4, 2026 20:39
@os-litant
os-litant added this pull request to the merge queue Sep 4, 2026
Merged via the queue into main with commit cf6b671 Sep 4, 2026
37 checks passed
@os-litant
os-litant deleted the claude/issue-14824-os-create-installs-outside-the-monorepo branch September 4, 2026 21:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] Two scaffolders, one of which emits output that cannot install outside this monorepo: os create vs os init

2 participants