Skip to content

feat(create-objectstack): a scaffolded project installs its skills from @objectstack/skills at its spec version, never from GitHub main (#22659) - #23248

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-22659-skills-from-package
Oct 13, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-22659-skills-from-package

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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

…k records it synced from the package

The sync skips, exit 0, a skill directory it did not install (one from
skills add) and leaves it in place, so neither the exit code nor the
directory is the verdict.

Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling labels Oct 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): create-objectstack, objectstack-blank, touching 21 documentable anchor(s). ⚠️ 4 changed file(s) yielded no anchor (packages/create-objectstack/README.md, packages/create-objectstack/src/created-summary.ts, packages/create-objectstack/src/templates/AGENTS.md, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

14 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 skillsPackageDir; a string literal in skillsSyncedFromPackage), package.json (literal, a string literal in installedSkillsVersion))
  • content/docs/ai/skills.mdx (via node_modules (literal, a string literal in skillsPackageDir; a string literal in skillsSyncedFromPackage), package.json (literal, a string literal in installedSkillsVersion))
  • content/docs/deployment/cli.mdx (via node_modules (literal, a string literal in skillsPackageDir; a string literal in skillsSyncedFromPackage), package.json (literal, a string literal in installedSkillsVersion))
  • content/docs/deployment/self-hosting.mdx (via package.json (literal, a string literal in installedSkillsVersion))
  • content/docs/deployment/tenancy-modes.mdx (via package.json (literal, a string literal in installedSkillsVersion))
  • content/docs/deployment/troubleshooting.mdx (via package.json (literal, a string literal in installedSkillsVersion))
  • content/docs/getting-started/examples.mdx (via package.json (literal, a string literal in installedSkillsVersion))
  • content/docs/getting-started/your-first-project.mdx (via package.json (literal, a string literal in installedSkillsVersion))
  • content/docs/plugins/development.mdx (via package.json (literal, a string literal in installedSkillsVersion))
  • content/docs/plugins/index.mdx (via package.json (literal, a string literal in installedSkillsVersion))
  • content/docs/protocol/kernel/index.mdx (via package.json (literal, a string literal in installedSkillsVersion))
  • content/docs/protocol/kernel/plugin-spec.mdx (via package.json (literal, a string literal in installedSkillsVersion))
  • content/docs/protocol/objectql/schema.mdx (via package.json (literal, a string literal in installedSkillsVersion))
  • content/docs/upgrading.mdx (via node_modules (literal, a string literal in skillsPackageDir; a string literal in skillsSyncedFromPackage), package.json (literal, a string literal in installedSkillsVersion))

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

  • content/docs/releases/v17/17-3.mdx (via package.json (literal, a string literal in installedSkillsVersion))
  • content/docs/releases/v17/17-4.mdx (via package.json (literal, a string literal in installedSkillsVersion))

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
  • 4 changed file(s) yielded no anchor (packages/create-objectstack/README.md, packages/create-objectstack/src/created-summary.ts, packages/create-objectstack/src/templates/AGENTS.md, …) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 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.
  • 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 — 11 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 6f4c518b012a3959ba6c7d414b5fa085ad135c17 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from e108996bd44e62dcf0dfbdeebf11398b955827c8 — the merge of head 610d0c6adf1f7f880812207ce9035bd042007de1 into base 6f4c518b012a3959ba6c7d414b5fa085ad135c17, 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 e108996bd44e62dcf0dfbdeebf11398b955827c8 && git checkout e108996bd44e62dcf0dfbdeebf11398b955827c8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6f4c518b012a3959ba6c7d414b5fa085ad135c17 610d0c6adf1f7f880812207ce9035bd042007de1 && git checkout -B drift-repro 6f4c518b012a3959ba6c7d414b5fa085ad135c17 && git merge --no-ff 610d0c6adf1f7f880812207ce9035bd042007de1

node scripts/docs-audit/affected-docs.mjs --json 6f4c518b012a3959ba6c7d414b5fa085ad135c17

⚠️ 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 6f4c518b012a3959ba6c7d414b5fa085ad135c17 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 610d0c6adf1f7f880812207ce9035bd042007de1
Local-runs: probe — npm view of @objectstack/skills and create-objectstack (versions, dist-tags, dependencies), the brief's one allowed registry read; nothing built, run or re-run

Isolated review at this tier, owed because the diff edits .github/workflows/scaffold-e2e.yml (domain:spec, declared after dispatch) from a domain:cli claim; the whole PR is judged. Inputs: card #22659 (body and all five comments: the hold, the unlock, claim 6142493926, the os-dev-report 6144849147, and ACCEPT 6144918545 read as a claim under test), the ruling record 6096124407 on #22649, #22658 and PR #22691, PR #23248's body and 13-file list, the net diff of refs/pull/23248/head against merge-base 8024e1243a (+857 / -363), the registry probe above, and the check-runs on the head. Reading time: 2026-10-13T12:03Z.

Gate verdicts on the head (latest run per name, 42 names): 32 success, 9 skipped, 1 in progress at the reading (Test Core (3/6)); 0 failed. Scaffold with repo dist is success with all 18 steps success, step 15 (pinned shape, now scaffolded without --skip-skills) and the new step 16 among them. The check-run's annotations hold no ::warning:: from either step's degraded branch (the only annotation is the runner-image migration notice), so step 16 took its full equality path against the registry-installed @objectstack/skills@18.0.0-next.0, not the window's exit 0. Check Changeset, Governed Surface Queue Guard, Validate Package Dependencies, every Lint · and Type Check · family, Lint & Repo Gates, TypeScript Type Check, Build Core, Dogfood Regression Gate: success. Job log text is not readable from this container (the log store is refused at the proxy); the step-level reading is from the steps list and the annotations.

① Derived judgments

The workflow edit (.github/workflows/scaffold-e2e.yml) — right.

  • Faithful to the Done-when: skills_in lists the immediate subdirectories holding a SKILL.md, symlinks excluded, for node_modules/@objectstack/skills/dist/skills and for .claude/skills; sorted-string equality is names AND count; an empty catalog is red ("the layout the scaffolder reads has moved"); and .agents present or any symlink under .claude is red (copied, never linked). That is "the installed skill set equals the package's catalog (count and names)", read off the registry-installed package, never this checkout's skills/.
  • No loosening: step 14's run: is byte-unchanged (comment only, now naming the GitHub path as the next channel it guards); step 15's run: changes in exactly one token, --skip-skills dropped from the scaffold line, and its three-way registry classification, FROM-tag equality and degraded-shape assertion all stand.
  • Degradation, evidenced not blanket: the new step probes nothing itself. Its exit-0 branch is reachable only when the skills package is absent AND @objectstack/cli is absent AND .claude/skills does not exist; because steps run in sequence, that state survives only step 15's warn-and-continue branch (registry reachable, range unsatisfiable); an unreachable registry or a satisfiable range already red step 15. The degraded shape (no skills from any source) is asserted, and a resolved CLI without the skills package reds as a lost dependency. Same shape as the pinned-shape step, inherited through the step order rather than re-probed; sound.
  • Network: yes, step 15 now runs npx -y skills@1.7.2 experimental_sync …, a fetch of the skills CLI from npm the pinned-probe leg did not make before. The scaffolder swallows a failed fetch (warning "Skills installation failed. Run manually", null, exit 0), so step 15 stays green and step 16 then reds on "the skills the scaffolder installed are not the installed package's catalog". Direction right (fails closed; this workflow's own rule is that an unclassifiable answer is never evidence of a window; no false green exists), but the message names a scaffolder defect where the cause may be the CLI fetch; the real cause sits one step up, in the scaffolder's own warning in step 15's output. Not new to the job: step 14 already runs npx -y skills add unpinned against npm and GitHub with no classification at all. Note-level, not a FAIL: a re-run disambiguates.
  • New gate? No. The card's Done-when names this assertion, and the card is execution card ③ of ruling 6096124407, whose workload line names "skills-install.ts and its e2e" as the cli lane's precedent. The assertion lands as one step inside an existing pipeline job whose header still reads dispatch-gates: no-check-families; no check family, workflow, patrol line or ratchet joins the roster. The doctrine "new gates default to no" (pm-dispatch skill) governs the roster, and the roster is unchanged.

The installer — right.

  • Pin: skills@1.7.2, the version measured against the PUBLISHED 18.0.0-next.0 tarball. Pinning an experimental_ command is right: the destination layout is that CLI's to choose, and an unpinned npx skills would move it under every scaffold silently. The hermetic stub writes MEASURED_CLI out rather than importing it and refuses the unpinned argv with exit 3 (case "refuses an argv it has no measured shape for"), so moving the pin without re-measuring reds the e2e. The cost is in the open: the pin is baked into every scaffolded AGENTS.md and README.md, which keep working as long as 1.7.2 stays on npm.
  • Outcome verification replaces the exit code: installSkillsFromPackage reads the catalog out of node_modules/@objectstack/skills/dist/skills BEFORE the sync (no version or empty catalog: warning, null) and AFTER it requires every catalog name to be a real directory in .claude/skills/ (listSkills reads Dirent.isDirectory, so a symlink is not counted) AND a skills-lock.json entry with source: @objectstack/skills and sourceType: node_modules (skillsSyncedFromPackage). That closes both measured exit-0 lies: "Nothing to sync." with no package, and a skipped skills add copy whose directory is present but whose lock entry says github. Sound.
  • Silent paths: none. The four failure returns (install did not complete; no catalog; execSync threw; incomplete after the sync) each print a warning carrying the remedy and return null; the summary then prints the "AI Skills (recommended)" block and never "Installed for". The hermetic suite pins three of them (no catalog; a skipped legacy skill reported as "1 of 3 skill(s)"; --skip-install), and the two ablations (template dependency dropped: 10 of 22 red; installer pointed back at GitHub: 13 of 22 red) agree with the stub's refusals.
  • Edge not closed and not required: scaffolding into a directory already holding a package-synced .claude/skills from an older package version passes the lock check (the lock's version is not compared); the sync overwrites node_modules-sourced entries on re-run (measured idempotent), so no stale copy survives. Note only.

The dependency — right.

  • @objectstack/skills joins the blank template's devDependencies at ^18.0.0, rewritten at scaffold time by syncObjectStackDeps (walks dependencies AND devDependencies for @objectstack/*, pinned by its own unit test) to ^ plus the scaffolder's own version — the range @objectstack/spec gets; the e2e asserts the two ranges equal. devDependencies is the right field: the catalog is consumed only by the skills CLI at development time; the copied .claude/skills/ is committed, so a clone needs no install at all; the template's Dockerfile build stage installs from the lockfile (every field) and its runtime stage copies only dist/objectstack.json; @objectstack/cli and @objectstack/verify sit in the same field. experimental_sync discovers that field (the real run and CI step 16 synced 10 skills from it).
  • create-objectstack@17.7.0 (latest) is a published, immutable tarball: npm view reads its dependencies unchanged (chalk, commander), and this PR writes nothing to it, so a scaffold from npm create objectstack keeps the GitHub skills add path and no @objectstack/skills — unchanged, as required. create-objectstack@18.0.0-next.0 (next) is likewise untouched until the next fixed-group prerelease carries this diff. @objectstack/skills on the registry: versions 0.0.0-stage and 18.0.0-next.0, dist-tags latest and next both 18.0.0-next.0 — triage's unlock reading and the dev's hold.
  • No lockfile change is owed (the template is not a workspace member); Validate Package Dependencies success.

The removed text — right.

  • SKILLS_CATALOG and skillsAddArgs: zero readers remain at the head (git grep over the tree). The published entry: create-objectstack's manifest exposes bin (dist/index.js) and an exports map naming only ./created-summary; dist/skills-install.js ships under files: dist but is sealed by that map, so no consumer could ever import the constant. Not a published export.
  • The old command skills add objectstack-ai/objectstack/skills survives only where it describes the GitHub next channel the ruling keeps: content/docs/** (devx lane, [Ruling A on #22649] docs: the skills install command reads the catalog from the installed @objectstack/skills package; the GitHub path is documented as the next (unreleased main) channel only #22668), packages/skills/README.md, skills/README.md, script and workflow comments, CHANGELOG history, and the template-consistency grep test, whose population is still non-vacuous (it greps content/docs, skills and packages/create-objectstack for the root-install spelling). No code reader is broken. The scaffolder's run, its summary, both guides it writes and this package's README now name the package command; the new template-consistency case holds all three to the exact command the scaffolder runs, pin included, and the starter-comments regex follows.

The four surface-extension files — required by the Done-when, and in-lane.

  • src/templates/AGENTS.md and src/templates/blank/README.md are files the scaffolder writes into every project; "the GitHub command disappears from the scaffolder's output" cannot hold while the scaffolder writes that command into the project's own guides.
  • template-consistency.test.ts imported the deleted SKILLS_CATALOG (the suite does not compile without the edit); starter-comments-self-contained.test.ts pinned the README's old command. All four live under packages/create-objectstack/, the card's domain:cli surface. The ACCEPT's claim that they are in-lane is tested and holds.

Other derived judgments.

  • created-summary.test.ts: the phase-3 layout follows the new installer (one real tree .claude/skills/, plus the catalog under node_modules/@objectstack/skills/dist/skills), and the symlink-counting property moves into a tree of its own instead of being dropped. Right. created-summary.ts is comment-only.
  • --skip-skills keeps its meaning (install runs, no sync, the summary recommends the command); --skip-install runs no sync and prints "Not installed yet … After pnpm install:" with the command — both as the Done-when reads, both pinned end to end.
  • No skills/**, no .claude/**, no governed path (GOVERNED_SURFACES carries no workflow row); Governed Surface Queue Guard success. The head repo is the base repo (not a fork); the PR is a draft with needs:contract-review.

② Semver level

create-objectstack: minor, Clause-②: no, no arm, no ! — right under Post-Task Checklist step 3. A functional change to a released package (a scaffolded project's dependency set and the scaffolder's output change) is a feature-level entry, never patch and never skip-changeset. Clause-②: no holds: nothing a consumer authors (a spec key, an export, a config field) moves. SKILLS_CATALOG is not an export of the published entry (the exports map above seals dist/skills-install.js), so its deletion is not a narrowing and needs no (narrowing), no ! and no ADR-0087 disposition; Check Changeset success reads the same. The changeset body carries the migration an upgrading project greps for (add the dependency at the spec range, remove the skills add copies, re-sync with --copy). The fixed group of 71 moves together; that is the group's design, not a cost this PR adds.

③ Boundary flags

Implemented-by: claude/issue-22659-skills-from-package
Reviewed-by: session_01VoSxBQujKLZKPwK2u5ehQ6

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读 · PR #23248(#22659)· 受管面 Tier H,等你合并或批准

domain:cli seat 2 (#22648) · os-steve · session session_01VoSxBQujKLZKPwK2u5ehQ6 · 2026-10-13T12:18Z。PR 改了 .github/workflows/scaffold-e2e.yml,这个路径属于受管登记里的 .github/workflows/**(#23048 起),所以落地档位是 H:你亲手合并,或者 os-zhuang / hotlong 给出 APPROVED 后由本席上队。

改了什么

  • npm create objectstack 新建的项目,现在把 @objectstack/skills 列为依赖,版本跟 @objectstack/spec 一致。
  • 安装完依赖后,脚手架从这个已装好的包里把技能目录实拷到 .claude/skills/(npx -y skills@1.7.2 experimental_sync --agent claude-code --copy -y),不再从 GitHub main 拉。
  • 输出、总结,以及写进新项目的 AGENTS.md、README.md 里,那条 GitHub 安装命令都去掉了。
  • CI 的 scaffold-e2e 工作流有两处改动:
    • pinned-probe 项目改成按真实用户的方式建,不再加 --skip-skills;
    • 新增一步,核对装上的技能和包里的目录名称、数量完全一致,并且没有符号链接。

为什么改

风险与代价(含回滚)

  • 对现有用户无影响:create-objectstack@17.7.0(latest)不受影响。这个改动从下一个 next 预发布版生效,那时会写成 ^18.0.0-next.N。
  • CI 多一次网络拉取:工作流新增的那次 skills@1.7.2 拉取,如果失败会让 job 变红。这是安全方向的失败,不会出现假绿,原因写在上一步的脚手架警告里。
  • 回滚:revert 本 PR 即可,不涉及数据和迁移。
  • 合约审查:CONTRACT_REVIEW_TIER 复核 PASS(6145120134)。CI 头部全绿,只有 roster 内的预期 skip。

席位意见

  • 建议合并。代码面、两个 e2e(hermetic 单测和工作流里的 registry 实装)以及消融实验都齐了。
  • 唯一的受管点是那个工作流文件,改动正是卡的验收要求(「scaffold e2e 断言装上的技能集合等于包的目录」),不是新加的门禁。
  • 席位的失误要说明一下:本席之前用了过期的本地脚本,判成「不受管」,把它转 ready 上了队,被队列的 Governed Surface Guard 拦下(exit 3)。现已退回 draft,关闭自动合并,不再自行上队。

你要做的(一个动作)


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review October 13, 2026 12:23
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 13, 2026
Merged via the queue into main with commit 5da2c0f Oct 13, 2026
59 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22659-skills-from-package branch October 13, 2026 12:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

3 participants