Repository navigation
Commit 5da2c0f
Fixes #22659
Clause-②: no
A project scaffolded by `create-objectstack` now gets its AI skills from
the `@objectstack/skills` package it depends on, at its own
`@objectstack/spec` version, instead of from the repository's `main`
branch. This is the cli-lane third of ruling A on #22649 (record
6096124407).
## What changes
- **The template depends on the catalog.**
`src/templates/blank/package.json` `devDependencies` gain
`"@objectstack/skills": "^18.0.0"`. At scaffold time `pkg-utils.ts`
rewrites it to the same `^VERSION` as every other `@objectstack/*`
dependency, so a project's skills version equals its spec version. The
fixed group publishes both at one version (71 members,
`create-objectstack` among them).
- **One installer, reading `node_modules`.** `src/skills-install.ts`
builds the command in one function, `skillsSyncArgs(agent)`. The
scaffolder runs `npx -y skills@1.7.2 experimental_sync --agent
claude-code --copy -y`. The summary prints the same command without the
first `-y`, for re-installing after an upgrade and for another runtime.
The GitHub constant `SKILLS_CATALOG` is gone. No GitHub fetch remains
anywhere on the scaffolder's path; the only network fetch left is `npx`
downloading the pinned `skills` CLI from npm.
- **The run checks before it reports.** `src/index.ts` runs the skills
step only after `PM install` succeeded. It reads the package's catalog
(`node_modules/@objectstack/skills/dist/skills/SKILL/SKILL.md`) before
the sync. After the sync it checks that every catalog skill landed in
`.claude/skills/` as a real directory, and that `skills-lock.json`
records it as synced from the package. If any check fails, the run warns
and prints the remedy, and the summary does not say "Installed". The
checks are needed because the sync exits 0 both when there is no package
("Nothing to sync.") and when it skips a skill that `skills add` put
there earlier. In the second case the old copy stays in place, so the
directory alone proves nothing.
- **The scaffolded guides follow.** The `AGENTS.md` and `README.md` the
scaffolder writes into a project, and this package's README, now give
the package command. Before, they gave `npx skills add
objectstack-ai/objectstack/skills`, which reads `main`. A new case in
`template-consistency.test.ts` holds all three to the exact command the
scaffolder runs, pin included.
- **The scaffold e2e asserts the catalog.** In
`.github/workflows/scaffold-e2e.yml`, the pinned-shape project is now
scaffolded without `--skip-skills`. A new step checks that
`.claude/skills` equals the installed `@objectstack/skills` catalog, by
names and count, and that the skills were copied, not linked. During the
unpublished-version window it degrades the same way the pinned-shape
step does. The existing "Skills boundary" step stays, and now guards the
GitHub `next` channel that the docs keep.
## The installer choice: `experimental_sync --copy`, measured
PR #22691 measured the layout and found it fits. I measured it again
with `skills@1.7.2` against the published
`@objectstack/skills@18.0.0-next.0`. The tarball has 69 entries, with 10
skills under `package/dist/skills/SKILL/SKILL.md`. I installed it into
throwaway projects with npm and with pnpm:
| command | what it writes |
|---|---|
| `experimental_sync --agent claude-code -y` | 10 symlinks in
`.agents/skills/` pointing into
`node_modules/@objectstack/skills/dist/skills/`, plus 10 symlinks in
`.claude/skills/` pointing into `.agents/skills/`. 0 real `SKILL.md`
outside `node_modules`. |
| `experimental_sync --agent claude-code --copy -y` | `.claude/skills/`
with 10 skills, 64 real files, 0 symlinks. `diff -r` against the package
copy: identical except the catalog README. Plus `skills-lock.json`,
every entry `source: @objectstack/skills`, `sourceType: node_modules`,
`version: 18.0.0-next.0`. Same result under pnpm's symlinked
`node_modules`. Re-running it is idempotent. |
| `experimental_sync --agent '*' --copy -y` | 54 agent directories, 540
`SKILL.md`, 48 MB. |
| the same with no package installed | `No skills found`, `Nothing to
sync.`, exit 0 |
| the same over a `.claude/skills/objectstack-data` that `skills add`
installed (lock `sourceType: github`) | `Skipped objectstack-data from
@objectstack/skills: installed with \`skills add\` from
objectstack-ai/objectstack`, `Synced 9 skills`, exit 0. The old copy and
its lock entry stay. After `npx skills remove objectstack-data -y`, or
after `npx skills remove --all` (which cleared all 10 directories and
the lock), a re-sync gives a tree identical to the package. |
| `--agent no-such-agent` | exit 1, and the CLI prints `Valid agents: …`
|
The linked default does not work for a project that commits its skills.
`node_modules` is gitignored, so a clone gets only dangling links.
`--copy` gives exactly the one-agent destination rule from the
`skills-install.ts` header: real files, in one directory, committed
once.
`experimental_sync` scans only the project's direct dependencies. I
listed every direct dependency of the template at the versions a
scaffold resolves (`@objectstack/*@18.0.0-next.0`, `typescript@5.9.3`,
`vitest@4.0.18`). Only `@objectstack/skills` ships any skills, and none
declares a `skills` field. So the synced set is exactly that package's
catalog.
The CLI is pinned to `skills@1.7.2`, the version measured.
`experimental_` is that tool's own word for an unstable interface, and
an unpinned `npx skills` would change the layout under every scaffold
without notice.
## Registry and ranges
- `@objectstack/skills` dist-tags: `{ latest: 18.0.0-next.0, next:
18.0.0-next.0 }`. Published versions: `0.0.0-stage` and `18.0.0-next.0`.
`create-objectstack` dist-tags: `{ rc: 17.0.0-rc.6, latest: 17.7.0,
next: 18.0.0-next.0 }`, the same as `@objectstack/spec`.
- The template's literal `^18.0.0` does not admit `18.0.0-next.N`, but
no project installs that literal. `rewriteProjectIdentity` calls
`syncObjectStackDeps(pkg, readCliVersion())`, which rewrites every
`@objectstack/*` range to the scaffolder's own `^VERSION`. A scaffold
from this tree writes `^18.0.0-next.0`. A real scaffold resolved
`@objectstack/skills@18.0.0-next.0` (below).
- Today, `npm create objectstack@next` runs
`create-objectstack@18.0.0-next.0`. I read its published
`dist/index.js`: `SKILLS_CATALOG = "objectstack-ai/objectstack/skills"`,
and its template has no `@objectstack/skills`. So it fetches skills from
GitHub `main`. Once this ships in the next prerelease of the fixed
group, `@next` writes `@objectstack/skills: ^18.0.0-next.N` and installs
that version's catalog. `npm create objectstack` (`latest`, 17.7.0) does
not change: it has no skills dependency, and no 17.x
`@objectstack/skills` is needed. I did not change the prerelease range.
## The scaffold e2e: what it installs from, and the count and names
- **Hermetic, in this package's suite**
(`scaffold-skills-single-copy.test.ts`, 22 cases, 4.2 s of tests). It
runs the real CLI through `tsx`, with `pnpm` and `npx` stubbed on
`PATH`. The `pnpm install` stub installs `@objectstack/skills` only when
the project's `package.json` declares it, in the published layout, with
a 3-skill stub catalog. The `npx` stub implements the measured
`experimental_sync` discovery, its destination rows and its skip of a
skill it did not install. It refuses (exit 3) any argv nobody measured:
any `skills add`, the unpinned CLI, or an unknown agent. The test
asserts:
- the dependency is at `@objectstack/spec`'s range;
- installed set ==
`listSkills(node_modules/@objectstack/skills/dist/skills)` == the stub
catalog, with the same count;
- one directory, one `SKILL.md` per skill, 0 symlinks;
- the lock names the package;
- a clone of the commit is readable;
- the summary names the package version and the re-install command, and
no GitHub source.
Vacuity cases: the linked shape stages 6 links and 0 `SKILL.md`, and its
clone is 6 dangling links; "Nothing to sync" exits 0.
- **Registry, in CI.** The new `scaffold-e2e.yml` step reads the catalog
from the registry-installed `node_modules/@objectstack/skills`.
- **Real, run locally once on this branch.** Steps:
1. With the repo-built bin, `create-objectstack pinned-probe`, with no
flags, so its own `pnpm install` ran against the registry.
2. The sync reported `Synced 10 skills` from `@objectstack/skills`, and
the Dockerfile was pinned to `18.0.0-next.0`.
3. I ran the new workflow step's `run:` script, taken out of the YAML,
under `bash -e` with `RUNNER_TEMP` pointed at that project. It printed
`catalog (10 skills)` == `installed in .claude/skills (10 skills)` for
`objectstack-ai`, `objectstack-api`, `objectstack-automation`,
`objectstack-data`, `objectstack-formula`, `objectstack-i18n`,
`objectstack-platform`, `objectstack-query`, `objectstack-ui` and
`objectstack-upgrade`, and exited 0.
4. `git init && git add -A` then staged 10 `SKILL.md`, all under
`.claude/`, and 0 symlinks. The generated `AGENTS.md` and `README.md`
contain no `skills add objectstack-ai` and do contain the package
command.
## `--skip-install` and `--skip-skills`
Both are tested end to end in the same file:
- `--skip-install`: no `npx` call, no `node_modules`, no `.claude`. The
summary says "Not installed yet … After `pnpm install`:" and gives the
command.
- `--skip-skills`: the install runs (the catalog is in `node_modules`),
no sync runs, and the summary prints "AI Skills (recommended)" with the
command.
- A third case: the install leaves no catalog. The run prints
"node_modules holds no @objectstack/skills catalog" and never prints
"Installed for".
- A fourth case: the project is scaffolded into the current directory,
which already holds an `objectstack-data` from `skills add`. The sync
skips that skill, and the run reports "1 of 3 skill(s) …
(objectstack-data)" instead of "Installed for".
## Reverse verification
I applied each mutation with `scripts/ablation-replace.mjs` (wrap mode,
restore on exit). The script also had its own `trap … EXIT INT TERM`
that restores from `HEAD` and compares blob hashes. Both ran on
`scaffold-skills-single-copy.test.ts` at the final head, `610d0c6adf`.
The earlier run at `026b6bc8fb` gave the same direction. The subject is
`src`: the test spawns `tsx src/index.ts` and imports
`./skills-install.js` from `src`, so there is no `dist` leg.
| leg | mutation (on disk) | result |
|---|---|---|
| control | none | 22 passed |
| A: drop the template dependency | anchor x1 → x0, blob `c1862cd7199a`
→ `525b315283bd` | 10 failed / 12 passed. The catalog case fails with
"the stub install did not put the catalog in node_modules: expected []
to deeply equal [ 'objectstack-data', …(2) ]" |
| B: point the installer back at GitHub (`skills add
objectstack-ai/objectstack/skills --skill '*' --agent AGENT -y`) |
anchor x1 → x0, replacement x0 → x1, blob `8ec0100b6330` →
`1e948aaf99ba` | 13 failed / 9 passed. The catalog case fails with
"expected [] to have a length of 3 but got +0" |
After both legs, each path's blob equals `HEAD` and `git diff HEAD` is
empty. Both ablations go red, as expected.
## Gates, at the final head `610d0c6adf`
- I derived the list with `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` with no paths: 103 families, the same
set as at `026b6bc8fb`. I ran each with its exit code captured before
any pipe, and reconciled with `--ran`: "103 derived, 103 run, 0
NOT-MEASURED, 0 UNRUN". All 103 exited 0, including:
- `check:pm-dispatch-gates` (fast tier: 1833 cases pass, 6 slow sections
deferred);
- `check:template-version-sync`, `check:cross-package-test-inputs`,
`check:published-files`, `check:doc-authoring`, `check:nul-bytes`;
- `check:workflow-step-name-quoting`, `check:workflow-status-functions`,
`check:pm-governed-merges`;
- `check:dual-build-cjs-loads` and `check:lean-entry-closure`, measured
on a fully built tree;
- the three changeset gates against `origin/main`.
Six of them overlapped my ablation run in time, so I re-ran those six on
the clean tree. They exited 0 again.
- Also run: `check:published-readme-links` 0 and
`check:published-readme-exports` 0, because the published README
changed.
- Package, under the shared verify lock:
- `pnpm --filter 'create-objectstack^...' build` 0;
- `pnpm --filter create-objectstack build` 0 (`check-dts-emitted: 1/1`);
- `typecheck` 0;
- `vitest run`: 18 files, 274 tests passed.
- Template stamping: `node scripts/sync-template-versions.mjs` printed
"blank/package.json already pins ^18.0.0 across 9 @objectstack/* dep(s)"
(8 on the base tree) and left the tree clean. So the new entry is
stamped with the rest, and the script is unchanged.
- Lint, as a measured narrowing at `610d0c6adf`: `eslint
--no-inline-config --format json` over the 7 TS files this PR touches
gave 7 files, 0 errors, 0 warnings. The population is
`eslint.config.mjs` judging `.`, the `pnpm lint` command; none of the 7
is ignored (an ignored file would carry the "File ignored" warning).
Invariance: the config enables no type-aware linting (its own note says
there is no `parserOptions.project` and there are no typed rules), so
this diff cannot change the verdict on any untouched file. The repo-wide
run is left to CI.
- Not run locally, left to CI: `@objectstack/cli`'s
`test/scaffold-emission-policy.e2e.test.ts` reads the template
`package.json` as a declared cross-package input. It compares only
third-party ranges (`thirdPartyOnly` drops every `@objectstack/` name).
## Files beyond the claim's listed surface
The card requires the GitHub command to disappear from the scaffolder's
output, and four files outside the claim had to change for that:
- the two project guides the scaffolder writes:
`src/templates/AGENTS.md` and `src/templates/blank/README.md`;
- the two tests in this package that pinned the old command:
`template-consistency.test.ts`, which imported `SKILLS_CATALOG`, and
`starter-comments-self-contained.test.ts`, which pinned the README's
command.
All of these are in `packages/create-objectstack`. No other package, no
`skills/**`, no `content/docs/**`, and no script was touched.
## Docs
No `content/docs/**` file is edited here. The devx-lane docs PR #23220
for #22668 has already landed on `main`. It describes the scaffolder of
18.0.0-next.0, which installs from `main`, and tells the reader to
replace that with the package and `npx skills experimental_sync --agent
claude-code -y`:
- `content/docs/getting-started/build-with-claude-code.mdx:60-67`
- `content/docs/getting-started/your-first-project.mdx:47-54`
Once this lands, the next prerelease's scaffolder does that itself. Two
follow-ups belong to #22668, not this PR:
- Those two passages need a sentence for the versions after
18.0.0-next.0.
- The docs and `packages/skills/README.md` give the linked sync, without
`--copy`. The scaffolder runs with `--copy`, and the `AGENTS.md` it
writes says to keep `--copy`, because the links point into the
gitignored `node_modules` (measured above).
## Acceptance notes
- `docs/qa/platform-checklist/areas/cli.json` keeps its "skills boundary
probe" item, `skills add CHECKOUT/skills --all --copy`. That probe
checks the GitHub channel, not the scaffolder. It is not changed here.
- Moving the `skills@1.7.2` pin means measuring again. The `npx` stub's
`MEASURED_CLI` is written out separately from the source, so moving the
pin in `skills-install.ts` alone turns the e2e red.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8258181 commit 5da2c0f
13 files changed
Lines changed: 857 additions & 363 deletions
File tree
- .changeset
- .github/workflows
- packages/create-objectstack
- src
- templates
- blank
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
235 | 235 | | |
236 | 236 | | |
237 | 237 | | |
238 | | - | |
239 | | - | |
240 | | - | |
241 | | - | |
242 | | - | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
243 | 246 | | |
244 | 247 | | |
245 | 248 | | |
| |||
293 | 296 | | |
294 | 297 | | |
295 | 298 | | |
296 | | - | |
297 | | - | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
298 | 303 | | |
299 | 304 | | |
300 | 305 | | |
| |||
337 | 342 | | |
338 | 343 | | |
339 | 344 | | |
340 | | - | |
| 345 | + | |
341 | 346 | | |
342 | 347 | | |
343 | 348 | | |
| |||
377 | 382 | | |
378 | 383 | | |
379 | 384 | | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
380 | 437 | | |
381 | 438 | | |
382 | 439 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
55 | 55 | | |
56 | 56 | | |
57 | 57 | | |
58 | | - | |
59 | | - | |
60 | | - | |
61 | | - | |
62 | | - | |
63 | | - | |
64 | | - | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
65 | 71 | | |
66 | 72 | | |
67 | 73 | | |
| |||
0 commit comments