Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .changeset/scaffold-skills-single-committed-copy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
"create-objectstack": minor
---

Scaffolded projects now install the AI skills bundle for **one** agent runtime
instead of every runtime the skills CLI knows, so the bundle is committed once.

**Route B of the two the card offered was taken**, and the choice was measured
rather than argued. Against `skills@1.5.23` and the 11-skill catalog, the old
`--all` (shorthand for `--skill '*' --agent '*' -y`) wrote the same bundle to
three destinations — `.agents/` (46 real files, 604,102 B), `agent/` (46 real
files, 602,682 B, identical bodies with re-serialised frontmatter) and
`.claude/` (11 symlinks into `.agents/`). The template's `.gitignore` excluded
none of it, so a new project's first `git add -A` staged 22 `SKILL.md` paths
plus 11 symlinks. That reached the initial commit of a real app before anyone
noticed.

The scaffolder now runs
`npx skills add objectstack-ai/objectstack/skills --skill '*' --agent claude-code -y`,
which writes 46 real files to `.claude/skills/` and nothing else: 11 staged
`SKILL.md` paths, no symlinks, and a clone of that commit has readable skill
files on every platform.

Route A (keep `--all`, exclude the duplicates in the template `_gitignore`) was
built and cloned, not reasoned about, and both of its shapes were rejected.
Ignoring `.agents/` and `agent/` while committing `.claude/` gives a fresh
cloner 11 dangling symlinks and zero readable `SKILL.md`. Ignoring only
`agent/` works on POSIX but commits 11 symlinks that a `core.symlinks=false`
clone — git-for-Windows' default — materialises as ordinary files whose whole
content is the link target. `--all --copy`, the other way to make `.claude/`
real, fans out to 56 destination directories totalling 33.8 MB. A denylist is
also 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.

The cost is the multi-runtime default, and it is paid in the open: the closing
summary now always prints an **AI Skills** block naming where the bundle landed
and the one-line command for any other runtime, one agent at a time. The
bundle is identical whichever agent is named.

Existing projects are unaffected. To shrink one that already carries the
triplicate, delete `.agents/` and `agent/` and re-run the single-agent command
above; `skills-lock.json` records source and hash, not paths, so it does not
change.
9 changes: 6 additions & 3 deletions packages/create-objectstack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,12 @@ you so rather than failing as an unknown template.
every object name in the template is re-prefixed to match
(`blank_note` → `my_app_note`).
2. Installs dependencies (pnpm if available, otherwise npm).
3. Installs the ObjectStack AI skills bundle for coding agents
(`npx skills add objectstack-ai/objectstack/skills --all` — scoped to the
curated `skills/` catalog).
3. Installs the ObjectStack AI skills bundle for **one** coding agent —
`npx skills add objectstack-ai/objectstack/skills --skill '*' --agent claude-code -y`,
scoped to the curated `skills/` catalog. One agent, not `--all`, so the
bundle lands in exactly one directory (`.claude/skills/`) and a scaffolded
project commits it once; `src/skills-install.ts` carries the measurement.
The closing summary prints the same command for any other runtime.
4. Writes `AGENTS.md` and `.github/copilot-instructions.md` with the project
conventions — unless the template ships its own.

Expand Down
15 changes: 9 additions & 6 deletions packages/create-objectstack/src/created-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,18 @@
//
// 1. template copy + identity rewrite + AGENTS.md/copilot-instructions.md
// 2. `<pm> install` -> pnpm-lock.yaml, node_modules/
// 3. `npx skills add … --all` -> .agents/, agent/, .claude/,
// skills-lock.json
// 3. `npx skills add … --agent …` -> .claude/, skills-lock.json
//
// and the list was printed between (1) and (2). Phases 2 and 3 are third-party
// processes whose outputs this package does not choose and cannot enumerate
// ahead of time — the `skills` CLI fans out to every agent runtime it knows,
// and that set changes with ITS releases, not ours. So any hand-maintained
// list is not merely incomplete, it is unmaintainable: it drifts the next time
// a dependency learns a new destination, silently, in the one direction that
// ahead of time. Phase 3's destination set was the sharpest case: the run
// measured above used `--all`, which fanned the catalog out to `.agents/`,
// `agent/` AND `.claude/` — three trees, all committed, which is the defect
// `skills-install.ts` now closes by naming one agent. Naming it does not make
// the destination OURS to enumerate: the path is still the skills CLI's
// choice, and it moves with ITS releases. So any hand-maintained list is not
// merely incomplete, it is unmaintainable: it drifts the next time a
// dependency learns a new destination, silently, in the one direction that
// hides files rather than inventing them.
//
// Reading the directory afterwards is what makes the summary self-correcting.
Expand Down
59 changes: 44 additions & 15 deletions packages/create-objectstack/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,12 @@
* `--skip-install` there is no resolved version, so the template keeps `latest`
* and its comment keeps telling the reader to pin by hand — true in that path.
*
* Finally we (best-effort) install the ObjectStack skills bundle via
* `npx skills add objectstack-ai/objectstack/skills --all`.
* The `/skills` subpath scopes discovery to the curated, customer-published
* catalog — repo-internal skills (e.g. under `.claude/skills/`) must never
* reach scaffolded projects.
* Finally we (best-effort) install the ObjectStack skills bundle, for ONE
* agent runtime (`skills-install.ts` carries the command and the measurement
* behind that choice: `--all` writes the same bundle to three destinations and
* the project then commits all three). The `/skills` subpath scopes discovery
* to the curated, customer-published catalog — repo-internal skills (e.g.
* under `.claude/skills/` in THIS repo) must never reach scaffolded projects.
*
* Only THEN is the "Created files" summary printed, and it is a walk of the
* finished project directory rather than a list accumulated during the copy
Expand Down Expand Up @@ -72,6 +73,13 @@ import {
import { lookupTemplate, templateNames } from './template-registry.js';
import { readResolvedCliVersion, pinRuntimeImage } from './runtime-image.js';
import { summarizeTree, describeEntry } from './created-summary.js';
import {
DEFAULT_SKILLS_AGENT,
DEFAULT_SKILLS_DIR,
EXAMPLE_OTHER_AGENT,
SKILLS_INSTALL_COMMAND,
skillsInstallHint,
} from './skills-install.js';
import { renderVersionBanner } from './banner.js';

const __filename = fileURLToPath(import.meta.url);
Expand Down Expand Up @@ -521,24 +529,26 @@ const program = new Command()
}

// Which top-level paths belong to the skills install is measured, not
// assumed: `skills add --all` fans the catalog out to every agent
// runtime IT knows about (77 at the version measured), so the
// destination set moves with that package's releases. Diffing the
// directory across the call keeps the ⚠ marks correct without this file
// carrying a list it cannot keep current.
// assumed. Naming one agent makes the destination set small and
// knowable (`skills-install.ts`), but it is still THEIR path to choose,
// and it moves with that package's releases — so the ⚠ marks come from
// diffing the directory across the call rather than from a list this
// file would have to keep current.
const beforeSkills = topLevelNames(targetDir);
let skillsInstalled = false;
if (!options.skipInstall && !options.skipSkills) {
printStep('Installing AI skills for your coding agent...');
try {
execSync('npx -y skills add objectstack-ai/objectstack/skills --all', {
execSync(SKILLS_INSTALL_COMMAND, {
stdio: 'inherit',
cwd: targetDir,
});
skillsInstalled = true;
console.log('');
} catch {
printWarning(
'Skills installation skipped. Run manually:\n' +
' npx skills add objectstack-ai/objectstack/skills',
` ${skillsInstallHint(DEFAULT_SKILLS_AGENT)}`,
);
console.log('');
}
Expand Down Expand Up @@ -569,10 +579,29 @@ const program = new Command()
console.log(chalk.dim(` ${devLabel.padEnd(labelWidth)}# Start development server`));
console.log(chalk.dim(` ${validateLabel.padEnd(labelWidth)}# Verify metadata: schema + predicates + bindings`));
console.log(chalk.dim(` ${' '.repeat(labelWidth)}# (run after every metadata edit — see AGENTS.md)`));
if (options.skipInstall || options.skipSkills) {
console.log('');
// The skills block is printed on EVERY path, not only the skipped one.
// The install now targets a single agent runtime, so a run that
// succeeded still leaves every other runtime uninstalled — and a
// capability that is absent has to say so, with the remedy, rather than
// leave the reader to discover the gap. The command printed here is the
// one this scaffolder ran, with a different agent name.
console.log('');
if (skillsInstalled) {
console.log(chalk.bold(' AI Skills:'));
console.log(
chalk.dim(
` Installed for ${DEFAULT_SKILLS_AGENT} → ${DEFAULT_SKILLS_DIR} — commit it with your project.`,
),
);
console.log(chalk.dim(' Using a different agent? Add it one runtime at a time:'));
console.log(chalk.dim(` ${skillsInstallHint(EXAMPLE_OTHER_AGENT)}`));
console.log(chalk.dim(' (`npx skills add --help` lists every agent name)'));
} else {
console.log(chalk.bold(' AI Skills (recommended):'));
console.log(chalk.dim(' npx skills add objectstack-ai/objectstack/skills'));
console.log(chalk.dim(` ${skillsInstallHint(DEFAULT_SKILLS_AGENT)}`));
console.log(
chalk.dim(` Replace \`${DEFAULT_SKILLS_AGENT}\` with your own agent runtime if it differs.`),
);
}
console.log('');
} catch (error) {
Expand Down
Loading
Loading