Skip to content

fix(create-objectstack): install the skills bundle for one agent so a scaffolded project commits it once - #16401

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-16331-skills-single-copy
Sep 6, 2026
Merged

fix(create-objectstack): install the skills bundle for one agent so a scaffolded project commits it once#16401
os-sam merged 2 commits into
mainfrom
claude/issue-16331-skills-single-copy

Conversation

@claude

@claude claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #16331

Route B, chosen on measurement

The card offered two routes and made the choice an in-card decision. Route B — install for one agent runtime by default and print the one-liner for the others — was taken, and every alternative was built and cloned rather than argued about.

Measured against skills@1.5.23 and this repo's 11-skill catalog, each into an empty directory:

command result on disk
--all (= --skill '*' --agent '*' -y) .agents/ 46 real files 604,102 B · agent/ 46 real files 602,682 B · .claude/ 11 symlinks into .agents/
--skill '*' --agent claude-code -y .claude/ 46 real files 604,102 B, and nothing else
--skill '*' --agent universal -y .agents/ 46 real files 604,102 B, and nothing else
--all --copy 56 destination directories, 33.8 MB

Then, with the template's .gitignore in place, git init && git add -A && git commit, then git clone into a clean directory:

shape staged SKILL.md staged symlinks fresh clone
--all, nothing ignored (today) 22 11 readable, but the bundle is committed twice
--all, ignore .agents/ + agent/ (Route A, naive) 0 11 11 dangling symlinks, 0 readable SKILL.md
--all, ignore agent/ only (Route A, viable) 11 11 readable on POSIX; on a core.symlinks=false clone the 11 links become ordinary files whose whole content is ../../.agents/skills/NAME
--skill '*' --agent claude-code -y (this PR) 11 0 readable everywhere

Route A's naive shape is the trap the triage comment predicted, and it is reproduced above by actually cloning, not by reasoning. Route A's viable shape does satisfy the card's acceptance on POSIX, and it was rejected for two reasons: it commits symlinks that git-for-Windows' default checkout materialises as stray text files, and — the structural one — a _gitignore deny-list is the wrong shape regardless of which paths it names. This package does not choose the destination set; the skills CLI does, and it moves with that package's releases. created-summary.ts already documents the same property for the same reason. An ignore list has to chase it silently, in the direction that re-commits duplicates. Naming our own destination is explicit composition instead of default magic.

The cost is the multi-runtime default, and it is paid in the open (see the new closing block below).

Acceptance, verified end to end

A real scaffold with the real skills CLI (only the package manager was stubbed; an install is irrelevant to the skills bundle), then git add -A, commit, and git clone into a clean directory:

staged total          : 62
staged SKILL.md paths : 11
staged symlinks       : 0
untracked after add   : 0
staged roots          : .claude .dockerignore .github .gitignore AGENTS.md Dockerfile
                        README.md docker-compose.yml objectstack.config.ts
                        objectstack.manifest.json package.json pnpm-workspace.yaml
                        skills-lock.json src tsconfig.json

CLONE into a clean directory
  cloned SKILL.md files : 11
  dangling symlinks     : 0
  .claude/ total        : 604,102 bytes
  ./.claude/skills/objectstack-ai/SKILL.md        21,866 bytes
  ./.claude/skills/objectstack-api/SKILL.md       18,535 bytes
  ./.claude/skills/objectstack-automation/SKILL.md 50,043 bytes

No .agents/, no agent/, nothing left untracked.

What changed

  • src/skills-install.ts (new) — one source for the command. The scaffolder runs it and the closing summary prints the same builder's output with a different agent name; a drifted pair is how a project acquires the second copy. The header carries the measurement table above.
  • src/index.ts — runs SKILLS_INSTALL_COMMAND; the closing summary now prints an AI Skills block on every path, not only the skipped one. A single-agent install leaves every other runtime uninstalled, and an absent capability has to say so with the remedy:
      AI Skills:
        Installed for claude-code → .claude/skills/ — commit it with your project.
        Using a different agent? Add it one runtime at a time:
          npx skills add objectstack-ai/objectstack/skills --skill '*' --agent codex -y
          (`npx skills add --help` lists every agent name)
    
  • src/scaffold-skills-single-copy.test.ts (new) — the acceptance assertion the card asked for, counting staged SKILL.md paths. It runs the real CLI through tsx under a stubbed PATH; the skills CLI is stubbed from the measured destination map, and the stub refuses an argv it has no measured row for (exit 3) so a command nobody measured fails loudly instead of passing against an invented layout. A vacuity block replays the --all row and requires the same assertions to fail.
  • src/template-consistency.test.ts — the /skills subpath boundary is unchanged and still enforced; the pin now asserts the value the scaffolder runs rather than the source text that happens to hold it, and a second case keeps a hand-written duplicate invocation from creeping back in.
  • src/created-summary.ts, README.md, src/templates/AGENTS.md, src/templates/blank/README.md — prose. The two template files told a project's future readers to run the bare interactive form; picking "all" there re-creates the triplicate inside the user's own repository, so both now name the per-agent form and refuse --all with the reason.
  • Changeset: create-objectstack minor — Route B changes what gets installed, per the card.

_gitignore is deliberately unchanged: with one destination there is nothing to exclude, and a deny-list for directories we no longer create is dead text.

Verification

  • pnpm --filter create-objectstack test — 16 files, 203 passed. typecheck clean, and tsc --listFiles confirms both new files are in the program (a typecheck that misses the test files is not coverage).
  • Ablation (from a committed implementation, on-disk mutation proved by blob hash, restored and re-verified against the HEAD blob): reverting SKILLS_INSTALL_COMMAND to --all turns 9 of 14 cases red, with the defect's own signature — staged roots ['.agents','agent'], 6 staged SKILL.md instead of 3, 3 symlinks. Restore leg: git diff HEAD empty, blob hash identical to HEAD.
  • Gates: node scripts/pm/dispatch-gates.mjs --ran reports 55 derived families, 55 run, 0 NOT-MEASURED, 0 UNRUN — including the two template families this path set pulls in (check:template-manifests, check:template-version-sync) and check:published-files.
  • pnpm lint — the full repo-wide eslint . --no-inline-config, exit 0 in 100s at 604dc4d2. Not a narrowing.
  • packages/cli is in turbo ls --affected (it declares create-objectstack as a dependency): closure built, --project unit green — 181 files, 2438 passed. The other six affected packages reach this diff only through the one published entry point, create-objectstack/created-summary, and that entry point is byte-identical across the diff (dist/created-summary.js and .d.ts sha256 unchanged when the file is reverted to origin/main and rebuilt).

Three gate results needed a note rather than a re-run: check:dual-build-cjs-loads and check:type-check-debt first answered PREREQUISITE NOT MET (exit 3) because the workspace closure was unbuilt — both green once it was built. check:type-check-debt then OOM'd under the container's default --max-old-space-size=4096, which is tighter than the 6144 ceiling the gate pins for itself; green at its own ceiling. check-plugin-teardown-shape --self-test could not reach its pinned fixture commit on a shallow checkout, and is green after git fetch origin 621a4876.

Out of scope, filed

#16400 — five documentation surfaces still tell readers to install with --all, and two of them describe what the scaffolder does, so they go stale with this PR. content/docs/** and skills/** are outside this card's file surface and pull in a different gate family (skills/** is governed). Following those instructions in a git repository reproduces the triplication independently of the scaffolder, so closing this card does not close that.

Not touched, per the card: the CI workflow (#16330, landed as 4998efa7), packages/cli/src/commands/init.ts (#16350), the scaffolded-project SyntaxError (#16373), and content/docs/releases/**.


Generated by Claude Code

… scaffolded project commits it once

`skills add … --all` is shorthand for `--skill '*' --agent '*' -y`, and the
`--agent '*'` half wrote the same 11-skill catalog to three destinations:
`.agents/` (46 real files), `agent/` (46 real files, identical bodies) and
`.claude/` (11 symlinks into `.agents/`). The template `.gitignore` excluded
none of it, so a new project's first `git add -A` staged 22 `SKILL.md` paths
plus 11 symlinks — measured against skills@1.5.23.

The command now names one agent: `--skill '*' --agent claude-code -y`, which
writes 46 real files to `.claude/skills/` and nothing else. Staged once, no
symlinks, and a clone of that commit reads on every platform.

Route A (keep `--all`, deny-list the duplicates) was built and cloned rather
than reasoned about: ignoring `.agents/` leaves a cloner 11 dangling symlinks
and zero readable SKILL.md; ignoring only `agent/` works on POSIX but commits
symlinks that a `core.symlinks=false` clone materialises as plain files; and
`--all --copy` fans out to 56 directories totalling 33.8 MB. A deny-list is
also the wrong shape — the destination set belongs to the skills CLI and moves
with its releases.

The command and the printed hint now come from one builder in
`src/skills-install.ts`, and the closing summary always prints an AI Skills
block naming where the bundle landed plus the one-liner for other runtimes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PbJ5Cy9KDAzeQHo8bsMadG
…per-agent install form

`templates/AGENTS.md` and the blank template README both told a project's
future readers to run the bare `npx skills add objectstack-ai/objectstack/skills`,
which prompts for agents interactively — picking "all" there re-creates the
triplicate this card removes, inside the user's own repository.

Both now name the single-agent form, say where the committed copy lives, and
refuse `--all` with the reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PbJ5Cy9KDAzeQHo8bsMadG
@github-actions github-actions Bot added the size/l label Sep 6, 2026
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

10 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. ⚠️ 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.

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
  • 1 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 — 9 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 3890244554ce94651dfdd7c1cb9fc1e1c19377abpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 3c47b6a6f495bd9262f8bd6ca26283f4dc372547 — the merge of head 604dc4d28785917f18d7db7aefd9da1602f5b451 into base 3890244554ce94651dfdd7c1cb9fc1e1c19377ab, 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 3c47b6a6f495bd9262f8bd6ca26283f4dc372547 && git checkout 3c47b6a6f495bd9262f8bd6ca26283f4dc372547
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3890244554ce94651dfdd7c1cb9fc1e1c19377ab 604dc4d28785917f18d7db7aefd9da1602f5b451 && git checkout -B drift-repro 3890244554ce94651dfdd7c1cb9fc1e1c19377ab && git merge --no-ff 604dc4d28785917f18d7db7aefd9da1602f5b451

node scripts/docs-audit/affected-docs.mjs --json 3890244554ce94651dfdd7c1cb9fc1e1c19377ab

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 6, 2026
@os-sam
os-sam marked this pull request as ready for review September 6, 2026 18:52
@os-sam
os-sam added this pull request to the merge queue Sep 6, 2026
Merged via the queue into main with commit fd75728 Sep 6, 2026
37 checks passed
@os-sam
os-sam deleted the claude/issue-16331-skills-single-copy branch September 6, 2026 20:36
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/l tests tooling

Projects

None yet

2 participants