diff --git a/CHANGELOG.md b/CHANGELOG.md
index 65e443b..7975773 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,20 @@ All notable changes to atom land here. Format: [Keep a Changelog](https://keepac
No changes pending. v0.3 candidate list at `docs/planning/v0.3.md`.
+## [0.3.0] — 2026-06-05
+
+Opens the v0.3 line with the first candidate to ship ahead of scope-lock: a global Claude skill that runs atom's bootstrap from inside any session. The five CLIs align to 0.3.0; `bin/atom-update-check` stays at 0.1.0.
+
+### Added
+
+- **`/atom-new` — bootstrap a project from inside Claude** (v0.3 #23). A global Claude Code skill that drives the Mode 1 bootstrap conversationally from any directory or session — no `cd ~/.atom/atom`, no terminal round-trip. New top-level `skills/` directory holds **global user-facing** skills (distinct from `scaffold/.claude/skills/`, which are copied *into* bootstrapped projects). The skill is a **thin launcher**: it locates the installed atom (`$ATOM_SOURCE_DIR` → `$ATOM_INSTALL` → `~/.atom/atom/`), reads `AGENTS.md` Mode 1 + the docs it references at invoke time, and runs that flow against a target directory it asks the user to confirm. It deliberately does *not* restate the bootstrap steps, so it can't drift from `AGENTS.md` — the single source of truth. Scoped to Mode 1 only; Modes 2 (maintain atom) and 3 (build atom features) are dev-facing and stay out. Fails gracefully with the `install.sh` one-liner when no atom checkout is found.
+- **Global-skill linking in `install.sh` and `atom upgrade`.** Both symlink each subdirectory of `skills/` into `~/.claude/skills/` (e.g. `~/.claude/skills/atom-new` → `~/.atom/atom/skills/atom-new`). Because it's a symlink into the git checkout, `atom upgrade`'s `git pull` refreshes skill content with no copy step — no new distribution channel, consistent with git-not-npm. Idempotent: re-runs repoint stale symlinks. Safe: a real (non-symlink) file or directory the user owns at the target name is left untouched and the skip is reported, never silently clobbered. `atom upgrade` gained an exported `linkGlobalSkills(installDir)` that runs after the CLI re-install (covers users who installed before global skills existed). Best-effort in both paths — a link failure never fails the install or upgrade.
+
+### Tests
+
+- **Test 21 in `scripts/test-atom-setup.sh`** (14 assertions): the skill ships and is shaped as a discoverable thin launcher (frontmatter `name`/`description`, references `AGENTS.md` Mode 1, has the not-installed fallback); both install paths wire the symlink and protect user-owned dirs; and a hermetic functional pass exercising `linkGlobalSkills` directly across fresh-link / idempotent-repoint / user-owned-dir-skip, using a scratch `HOME` so the runner's real `~/.claude` is never touched. Suite total 164 → 178. Added a `bin/atom` dependency pre-flight (Test 20 and 21 both load `picocolors` via `upgrade.js`).
+- **Fixed two CI-only test failures** (pre-existing, environment-specific). `14.15` asserted on `name is ready`, but picocolors auto-enables ANSI codes when `$CI` is set (GitHub Actions), splitting the string — pinned `NO_COLOR=1` in the harness so output is plain text everywhere. `20.7` double-quoted backticks in its grep pattern, producing `` \` `` which GNU grep (Ubuntu CI) treats as a buffer-start anchor that never matches mid-pattern, while BSD grep (macOS) took it literally — single-quoted the pattern so backticks stay literal. Verified against GNU grep 3.12. The wizard-e2e CI job, red since 2026-05-14, is green again.
+
## [0.2.1] — 2026-05-14
Bundled release for Wave 2 (user-facing features) + Wave 3 (distribution architecture). All five CLIs aligned to 0.2.1; new `bin/atom-update-check` ships at 0.1.0.
diff --git a/README.md b/README.md
index e902444..53261a5 100644
--- a/README.md
+++ b/README.md
@@ -76,6 +76,9 @@ To keep atom itself up to date later, run `atom upgrade`.
> [!TIP]
> Want zero questions? Run `atom-setup new my-project --bare` and you're done in under 5 seconds. All flags pass through (`--minimal`, `--full`, `--dry-run`, `--resume`, etc.).
+> [!TIP]
+> Live in Claude Code? Install puts an **`/atom-new`** skill on your machine. Type `/atom-new` in any session — Claude walks you through the same bootstrap conversationally, no trip back to the terminal. The wizard above is still there when you want the fast, no-questions path.
+
### Don't trust curl-pipe-bash? Install manually.
```bash
diff --git a/VERSION b/VERSION
index 0c62199..0d91a54 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.2.1
+0.3.0
diff --git a/bin/atom-setup/package.json b/bin/atom-setup/package.json
index 118132b..e0bac6a 100644
--- a/bin/atom-setup/package.json
+++ b/bin/atom-setup/package.json
@@ -1,6 +1,6 @@
{
"name": "@atom/atom-setup",
- "version": "0.2.1",
+ "version": "0.3.0",
"description": "Interactive wizard that transforms a cloned atom checkout into a personalized project.",
"type": "module",
"bin": {
diff --git a/bin/atom/package.json b/bin/atom/package.json
index 8291923..1b56828 100644
--- a/bin/atom/package.json
+++ b/bin/atom/package.json
@@ -1,6 +1,6 @@
{
"name": "@atom/atom",
- "version": "0.2.1",
+ "version": "0.3.0",
"description": "Top-level help dispatcher for atom's tooling. Lists every CLI in one place.",
"type": "module",
"bin": {
diff --git a/bin/atom/src/upgrade.js b/bin/atom/src/upgrade.js
index 46d5e55..74e8fa2 100644
--- a/bin/atom/src/upgrade.js
+++ b/bin/atom/src/upgrade.js
@@ -13,7 +13,17 @@
// Refuses to upgrade if the install dir's git tree is dirty — the
// user has been editing it and a pull might lose work.
-import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
+import {
+ existsSync,
+ lstatSync,
+ mkdirSync,
+ readFileSync,
+ readdirSync,
+ realpathSync,
+ rmSync,
+ statSync,
+ symlinkSync,
+} from 'node:fs';
import { homedir } from 'node:os';
import { dirname, join } from 'node:path';
import { spawnSync } from 'node:child_process';
@@ -130,9 +140,66 @@ export async function runUpgrade(args) {
}
}
+ linkGlobalSkills(installDir);
+
console.log(`\n ${color.green('✓')} now on atom ${color.bold(upstreamVersion)}`);
}
+// Symlink each skill in /skills/ into ~/.claude/skills/ so it's
+// invocable from any Claude session. Mirrors the install.sh block; this
+// path covers users who installed before global skills existed and
+// repoints stale links after the git pull. Best-effort: a failure here
+// never fails the upgrade (CLIs are already updated by this point).
+export function linkGlobalSkills(installDir) {
+ const skillsSrc = join(installDir, 'skills');
+ if (!existsSync(skillsSrc)) return;
+
+ const claudeSkillsDir = join(homedir(), '.claude', 'skills');
+ let entries;
+ try {
+ entries = readdirSync(skillsSrc, { withFileTypes: true });
+ } catch {
+ return;
+ }
+
+ for (const entry of entries) {
+ if (!entry.isDirectory()) continue;
+ const name = entry.name;
+ const target = join(skillsSrc, name);
+ const dest = join(claudeSkillsDir, name);
+ try {
+ mkdirSync(claudeSkillsDir, { recursive: true });
+ const link = lstatExisting(dest);
+ if (link === 'symlink') {
+ rmSync(dest);
+ symlinkSync(target, dest);
+ console.log(` ${color.dim('→')} skill ${name} ${color.dim('linked')}`);
+ } else if (link === 'other') {
+ // A real file/dir the user owns — never clobber it.
+ console.log(
+ ` ${color.dim('→')} skill ${name} ${color.dim(`skipped (~/.claude/skills/${name} exists, not a link)`)}`,
+ );
+ } else {
+ symlinkSync(target, dest);
+ console.log(` ${color.dim('→')} skill ${name} ${color.dim('linked')}`);
+ }
+ } catch {
+ // Non-fatal: the CLI upgrade already succeeded.
+ console.log(` ${color.dim('→')} skill ${name} ${color.dim('(link skipped)')}`);
+ }
+ }
+}
+
+// Returns 'symlink' if dest is a symlink, 'other' if it's a real
+// file/dir, null if it doesn't exist.
+function lstatExisting(dest) {
+ try {
+ return lstatSync(dest).isSymbolicLink() ? 'symlink' : 'other';
+ } catch {
+ return null;
+ }
+}
+
function findInstallDir() {
// 1. env override
const env = process.env.ATOM_INSTALL;
diff --git a/bin/learnings/package.json b/bin/learnings/package.json
index 831b32f..0f00b1c 100644
--- a/bin/learnings/package.json
+++ b/bin/learnings/package.json
@@ -1,6 +1,6 @@
{
"name": "@atom/learnings",
- "version": "0.2.1",
+ "version": "0.3.0",
"description": "Your local playbook of generalized patterns. Lives at ~/.atom/learnings, optionally synced to a private GitHub repo. Auto-copied into every new atom-bootstrapped project.",
"type": "module",
"bin": {
diff --git a/bin/model-race/package.json b/bin/model-race/package.json
index 90a0c20..4b14030 100644
--- a/bin/model-race/package.json
+++ b/bin/model-race/package.json
@@ -1,6 +1,6 @@
{
"name": "@atom/model-race",
- "version": "0.2.1",
+ "version": "0.3.0",
"description": "Run the same feature spec through multiple AI models in parallel via Git worktrees. Compare, score, merge the winner.",
"type": "module",
"bin": {
diff --git a/bin/nucleus/package.json b/bin/nucleus/package.json
index 8ad7109..a494900 100644
--- a/bin/nucleus/package.json
+++ b/bin/nucleus/package.json
@@ -1,6 +1,6 @@
{
"name": "@atom/nucleus",
- "version": "0.2.1",
+ "version": "0.3.0",
"description": "Cross-project learning store. Captures session learnings into ~/.nucleus, syncs across machines via GitHub.",
"type": "module",
"bin": {
diff --git a/docs/planning/v0.3.md b/docs/planning/v0.3.md
index 19bdc8e..bc98f34 100644
--- a/docs/planning/v0.3.md
+++ b/docs/planning/v0.3.md
@@ -167,6 +167,73 @@ doctor command answers them in one paste.
---
+### #23 `/atom-new` bootstrap skill
+
+> **Status: built** (shipped ahead of v0.3 scope-lock, see `[Unreleased]`
+> in `CHANGELOG.md`). Lives in `skills/atom-new/`; linked into
+> `~/.claude/skills/` by `install.sh` + `atom upgrade`. Locked decisions
+> below held: conversational, Mode 1 only, thin launcher, symlink
+> distribution. Open questions resolved during build: named `atom-new`;
+> target dir is confirmed per-invocation; not-installed path points at
+> `install.sh`. **Released in 0.3.0 (2026-06-05)** — opened the v0.3
+> line ahead of full scope-lock.
+
+**What**: ship a global Claude Code skill (`/atom-new`) that drives the
+Mode 1 bootstrap flow conversationally from **any** directory or
+session — no `cd ~/.atom/atom`, no terminal round-trip. The skill is a
+**thin launcher**: it locates the installed atom (`~/.atom/atom/`),
+reads `AGENTS.md` Mode 1 + `docs/` from there, and executes the
+conversational bootstrap against the current working dir. It does *not*
+restate the steps — `AGENTS.md` stays the single source of truth.
+
+**Why**: today, to bootstrap you have to *be inside atom* — either
+`cd ~/.atom/atom` so `CLAUDE.md → AGENTS.md` auto-loads, or run the
+`atom-setup` wizard in a terminal. Both mean leaving wherever you
+actually are. A global skill removes that friction, and — the real
+unlock — its description makes Claude *proactively offer it* when a user
+says "let's start a new project" in any session. You stop having to
+remember atom exists. The `atom-setup` wizard already serves the
+terminal-first path; this is the in-Claude conversational counterpart.
+
+**Scope (locked)**: **Mode 1 only.** Modes 2 (maintain atom) and 3
+(build atom features) are dev-facing — they only make sense inside the
+`atom-dev` repo. Folding them into a global skill makes it ambiguous
+("start a project... or maintain the template?") for zero benefit. One
+focused skill = the bootstrap flow.
+
+**Flow (locked)**: **conversational**, not a wizard shell-out. The
+guided dialogue is atom's differentiator *inside* Claude; the
+deterministic 10-section wizard already exists for terminal lovers.
+
+**Distribution**: piggyback on existing machinery, no new channel. The
+skill installs to `~/.claude/skills/atom-new/` as one more global
+symlink that `install.sh` creates and `atom upgrade` refreshes —
+consistent with the dev/use globals separation
+(`atom_dev_use_separation_state` memory) and the git-not-npm decision
+(`atom_distribution_decision` memory).
+
+**Open questions**:
+- Name: `/atom-new` vs. `/new-project` vs. `/atom`. `/atom` is
+ ambiguous against the `atom` CLI; lean `/atom-new`.
+- Where does the new project get created? The wizard handles target
+ dir; the skill must ask (cwd vs. a named subdir) since it can fire
+ from anywhere.
+- Thin-launcher contract: the skill reads `AGENTS.md` Mode 1 from the
+ install at invoke time. Confirm it fails gracefully (points to
+ `install.sh`) when atom isn't installed.
+- Discovery tuning: the skill `description:` is what makes Claude
+ proactively offer it. Get the trigger phrasing right ("start a new
+ project", "bootstrap from a template") without over-firing.
+- Compatibility: Claude Code only. Other AI tools keep the
+ `cd` + AGENTS.md path and the `atom-setup` wizard.
+
+**Effort**: small. A thin `SKILL.md` (~20–40 lines pointing at the
+installed `AGENTS.md`/`docs`) + one symlink wired into `install.sh` and
+`atom upgrade`. The risk is *not* effort — it's the fork-the-instructions
+trap, which the thin-launcher design kills.
+
+---
+
## Tier 2 — interesting, requires deliberate scoping
These could fit atom but only if v0.3 explicitly decides atom is
@@ -347,16 +414,20 @@ it — the embedding column belongs in the DB.
material already exists; the work is curation + voice rewrite.
Almost certainly yes if the maintainer is comfortable shipping the
distilled patterns as opinionated defaults.
-4. **Decide on #18 (cost tracking)**. The §1 wizard question forces
+4. **Decide on #23 (`/atom-new` skill)**. Standalone, cheap (thin
+ launcher + one symlink), reinforces proactive discovery. Almost
+ certainly yes — the only real risk is the fork-the-instructions
+ trap, which the thin-launcher design already mitigates.
+5. **Decide on #18 (cost tracking)**. The §1 wizard question forces
the issue: ship the tracker, or drop the question.
-5. **Decide on #16 (SQLite)**. Architectural; gate on whether #20
+6. **Decide on #16 (SQLite)**. Architectural; gate on whether #20
(semantic) is in scope and on whether scale demands it yet.
-6. **Decide on #17 (research)**. Lowest urgency; defer to v0.4 if
+7. **Decide on #17 (research)**. Lowest urgency; defer to v0.4 if
v0.3 already fills up.
-7. **Ship #22 Phase A now (out-of-band v0.2.x)**. WSL2 callout is a
+8. **Ship #22 Phase A now (out-of-band v0.2.x)**. WSL2 callout is a
~1hr stopgap independent of the v0.3 wave. Phase B (native port) is
demand-gated — re-evaluate after Phase A surfaces signal.
-8. **#19, #20** ride along with whichever wave fits.
+9. **#19, #20** ride along with whichever wave fits.
---
diff --git a/install.sh b/install.sh
index bb76134..971099a 100755
--- a/install.sh
+++ b/install.sh
@@ -140,6 +140,33 @@ if [ ${#MISSING[@]} -gt 0 ]; then
exit 1
fi
+# ─── global Claude skills ───────────────────────────────────────────
+# Symlink each skill in $TARGET/skills/ into ~/.claude/skills/ so it's
+# invocable from any Claude session. Symlinks (not copies) into the git
+# checkout mean `atom upgrade`'s git pull refreshes them for free.
+# Best-effort: a skill link failing must not fail the whole install.
+SKILLS_SRC="$TARGET/skills"
+CLAUDE_SKILLS_DIR="$HOME/.claude/skills"
+if [ -d "$SKILLS_SRC" ]; then
+ for skill_path in "$SKILLS_SRC"/*/; do
+ [ -d "$skill_path" ] || continue
+ skill_name="$(basename "$skill_path")"
+ dest="$CLAUDE_SKILLS_DIR/$skill_name"
+ mkdir -p "$CLAUDE_SKILLS_DIR"
+ if [ -L "$dest" ]; then
+ # Existing symlink — repoint it (idempotent across reinstalls).
+ ln -sfn "${skill_path%/}" "$dest"
+ printf " → skill %-12s linked\n" "$skill_name"
+ elif [ -e "$dest" ]; then
+ # A real file/dir the user owns — never clobber it.
+ printf " → skill %-12s skipped (\$HOME/.claude/skills/%s exists, not a link)\n" "$skill_name" "$skill_name"
+ else
+ ln -sfn "${skill_path%/}" "$dest"
+ printf " → skill %-12s linked\n" "$skill_name"
+ fi
+ done
+fi
+
# ─── success ────────────────────────────────────────────────────────
echo ""
echo "✓ atom installed. Run \`atom-setup new \` to start your first project."
@@ -147,6 +174,7 @@ echo ""
echo "Quick reference:"
echo " atom --help see every atom command"
echo " atom-setup new my-project scaffold a new project"
+echo " /atom-new bootstrap a project from inside Claude"
echo " atom upgrade update atom in place"
echo " nucleus init one-time setup for your session memory"
echo ""
diff --git a/scripts/test-atom-setup.sh b/scripts/test-atom-setup.sh
index 2397609..7500cde 100755
--- a/scripts/test-atom-setup.sh
+++ b/scripts/test-atom-setup.sh
@@ -16,6 +16,14 @@
set -u
+# Pin color OFF so captured output is deterministic across environments.
+# picocolors auto-enables ANSI codes when the `CI` env var is present
+# (GitHub Actions sets it), but not on a local non-TTY run. That made
+# assertions on human-readable strings — e.g. `name is ready`
+# — pass locally and fail in CI, where the escape codes split the string.
+# NO_COLOR makes both paths emit plain text. Children inherit it.
+export NO_COLOR=1
+
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
ATOM=$(cd "$SCRIPT_DIR/.." && pwd)
SETUP="node $ATOM/bin/atom-setup/bin/atom-setup.js"
@@ -105,13 +113,26 @@ if [ ! -d "$ATOM/bin/atom-setup/node_modules" ]; then
}
fi
+# Pre-flight: atom (dispatcher) deps must be installed — Test 20 runs
+# `atom upgrade`, Test 21 imports upgrade.js; both load picocolors.
+if [ ! -d "$ATOM/bin/atom/node_modules" ]; then
+ echo "atom dependencies missing. Installing..."
+ (cd "$ATOM/bin/atom" && npm install --silent) || {
+ echo "Failed to install atom deps; aborting."
+ exit 99
+ }
+fi
+
# =============================================================
section "Test 1: pre-flight detection runs and --version works"
# =============================================================
$SETUP --version > "$LOG_DIR/t1-version.log" 2>&1
assert "1.1 atom-setup --version exits 0" test $? -eq 0
-assert_grep "1.2 prints 0.2.1" "0.2.1" "$LOG_DIR/t1-version.log"
+# Assert against the repo's VERSION file, not a hardcoded string, so a
+# release bump doesn't break this test. CLIs align to the root VERSION.
+ATOM_VERSION=$(cat "$ATOM/VERSION")
+assert_grep "1.2 prints version $ATOM_VERSION" "$ATOM_VERSION" "$LOG_DIR/t1-version.log"
$SETUP --help > "$LOG_DIR/t1-help.log" 2>&1
assert_grep "1.3 --help mentions --bare" "\\-\\-bare" "$LOG_DIR/t1-help.log"
@@ -857,7 +878,11 @@ ATOM_STATE_DIR="$T20_HOME/state" \
ATOM_INSTALL="$T20_INSTALL" \
node "$ATOM/bin/atom/bin/atom.js" --version > "$LOG_DIR/t20c-notice.log" 2>&1
assert_grep "20.6 inlined client prints upgrade notice (atom CLI)" "0.2.5 is available" "$LOG_DIR/t20c-notice.log"
-assert_grep "20.7 notice mentions \`atom upgrade --snooze\`" "snooze: \\\`atom upgrade --snooze 7d\\\`" "$LOG_DIR/t20c-notice.log"
+# Pattern is single-quoted: backticks must stay literal. Double-quoting
+# forced `\`` escaping, which GNU grep (CI) reads as a buffer-start
+# anchor mid-pattern and never matches; BSD grep (macOS) took it as a
+# literal, hiding the bug locally. Backtick is not a regex metachar.
+assert_grep "20.7 notice mentions \`atom upgrade --snooze\`" 'snooze: `atom upgrade --snooze 7d`' "$LOG_DIR/t20c-notice.log"
# 20d: second invocation within 24h does NOT re-print the notice.
ATOM_STATE_DIR="$T20_HOME/state" \
@@ -951,6 +976,55 @@ assert_grep "20.19 missing-duration error mentions valid tiers" "24h, 48h, 7d" "
node "$ATOM/bin/atom/bin/atom.js" --help > "$LOG_DIR/t20k-help.log" 2>&1
assert_grep "20.20 atom --help advertises --snooze" "atom upgrade --snooze" "$LOG_DIR/t20k-help.log"
+# =============================================================
+section "Test 21: global /atom-new skill + skill-linking"
+# =============================================================
+#
+# Item #23: skills/ holds global Claude skills. install.sh and
+# atom upgrade symlink each into ~/.claude/skills/ so they're invocable
+# from any session. The skill itself is a thin launcher over AGENTS.md
+# Mode 1 (it must NOT restate the bootstrap steps). Symlink logic must
+# repoint stale links and never clobber a real dir the user owns.
+
+# 21a: the skill ships and is shaped like a discoverable thin launcher.
+assert "21.1 skills/atom-new/SKILL.md exists" test -f "$ATOM/skills/atom-new/SKILL.md"
+assert_grep "21.2 skill frontmatter names it atom-new" "^name: atom-new" "$ATOM/skills/atom-new/SKILL.md"
+assert_grep "21.3 skill has a description (drives discovery)" "^description: " "$ATOM/skills/atom-new/SKILL.md"
+assert_grep "21.4 skill points at AGENTS.md Mode 1 (thin launcher)" "Mode 1" "$ATOM/skills/atom-new/SKILL.md"
+assert_grep "21.5 skill fails gracefully when atom not installed" "install.sh" "$ATOM/skills/atom-new/SKILL.md"
+assert "21.6 skills/README.md exists" test -f "$ATOM/skills/README.md"
+
+# 21b: both install paths wire the symlink and protect user-owned dirs.
+assert_grep "21.7 install.sh links skills into ~/.claude/skills" '.claude/skills' "$ATOM/install.sh"
+assert_grep "21.8 install.sh won't clobber a real dir" "not a link" "$ATOM/install.sh"
+assert_grep "21.9 atom upgrade re-links global skills" "linkGlobalSkills" "$ATOM/bin/atom/src/upgrade.js"
+
+# 21c: functional — real symlink creation, hermetic (scratch HOME, no
+# pollution of the runner's ~/.claude). Exercises linkGlobalSkills directly.
+T21_HOME=$SCRATCH/test-21-home
+T21_INST=$SCRATCH/test-21-install
+rm -rf "$T21_HOME" "$T21_INST"
+mkdir -p "$T21_INST/skills/atom-new" "$T21_HOME/.claude/skills"
+echo "stub" > "$T21_INST/skills/atom-new/SKILL.md"
+T21_RUN='import { linkGlobalSkills } from "file://'"$ATOM"'/bin/atom/src/upgrade.js"; linkGlobalSkills(process.env.INST);'
+
+# fresh link
+INST="$T21_INST" HOME="$T21_HOME" node --input-type=module -e "$T21_RUN" > "$LOG_DIR/t21-fresh.log" 2>&1
+assert "21.10 fresh install creates ~/.claude/skills/atom-new symlink" test -L "$T21_HOME/.claude/skills/atom-new"
+
+# idempotent re-run repoints, stays a symlink
+INST="$T21_INST" HOME="$T21_HOME" node --input-type=module -e "$T21_RUN" > "$LOG_DIR/t21-rerun.log" 2>&1
+assert "21.11 idempotent re-run keeps it a symlink" test -L "$T21_HOME/.claude/skills/atom-new"
+
+# user-owned real dir is never clobbered
+rm "$T21_HOME/.claude/skills/atom-new"
+mkdir "$T21_HOME/.claude/skills/atom-new"
+echo "USER DATA" > "$T21_HOME/.claude/skills/atom-new/SKILL.md"
+INST="$T21_INST" HOME="$T21_HOME" node --input-type=module -e "$T21_RUN" > "$LOG_DIR/t21-skip.log" 2>&1
+assert_not "21.12 user-owned dir not converted to a symlink" test -L "$T21_HOME/.claude/skills/atom-new"
+assert_grep "21.13 user-owned skill file left intact" "USER DATA" "$T21_HOME/.claude/skills/atom-new/SKILL.md"
+assert_grep "21.14 skip is reported, not silent" "not a link" "$LOG_DIR/t21-skip.log"
+
# =============================================================
# Report
# =============================================================
diff --git a/skills/README.md b/skills/README.md
new file mode 100644
index 0000000..1d4df66
--- /dev/null
+++ b/skills/README.md
@@ -0,0 +1,30 @@
+# skills/ — global Claude skills
+
+Skills in this directory are **global, user-facing** Claude Code skills.
+They install once per machine (symlinked into `~/.claude/skills/` by
+`install.sh`, kept fresh by `atom upgrade`) and are invocable from
+**any** directory or session.
+
+Do not confuse this with `scaffold/.claude/skills/`:
+
+| Directory | Audience | Scope |
+| --------------------------- | ------------------- | --------------------------------------- |
+| `skills/` (here) | the atom **user** | global — works anywhere `atom` is installed |
+| `scaffold/.claude/skills/` | a **new project** | project-local — copied into each bootstrapped repo |
+
+## What's here
+
+- **`atom-new/`** — bootstrap a new project from atom, conversationally,
+ without returning to the terminal. A thin launcher over Mode 1 of the
+ installed `AGENTS.md` (it reads the real instructions at invoke time;
+ it does not copy them). See [`docs/planning/v0.3.md`](../docs/planning/v0.3.md)
+ item #23 for the design rationale.
+
+## How they install
+
+`install.sh` symlinks each subdirectory here into `~/.claude/skills/`
+(e.g. `~/.claude/skills/atom-new` → `~/.atom/atom/skills/atom-new`).
+Because it's a symlink into the git checkout, `atom upgrade`'s `git
+pull` refreshes the skill content with no copy step. If a user already
+has a non-symlink file or directory at the target name, the installer
+leaves it alone and warns rather than clobbering it.
diff --git a/skills/atom-new/SKILL.md b/skills/atom-new/SKILL.md
new file mode 100644
index 0000000..d135f21
--- /dev/null
+++ b/skills/atom-new/SKILL.md
@@ -0,0 +1,89 @@
+---
+name: atom-new
+description: Bootstrap a new project from atom — the project-starter template — without leaving Claude. Use when the user says "start a new project", "new project from atom", "bootstrap from a template", "scaffold a new repo from atom", or similar. Drives the conversational atom bootstrap (project context → scaffold → constitution → first phase) from any directory. Only for STARTING a project; not for maintaining atom itself or building atom's features.
+---
+
+# atom-new — bootstrap a new project from atom
+
+This skill is a **thin launcher**. It does not contain the bootstrap
+steps. The single source of truth is **Mode 1 of `AGENTS.md` in the
+installed atom checkout**. Your job is to find that file, read it, and
+run its flow conversationally against the user's chosen directory.
+
+Why thin: atom is distributed as a git checkout that the user upgrades
+in place (`atom upgrade`). If this skill restated the bootstrap steps,
+the two would drift. Instead it points at the live instructions so the
+flow is always whatever the installed atom says it is.
+
+## Step 1 — locate the installed atom
+
+Resolve the atom source directory, in this order:
+
+1. `$ATOM_SOURCE_DIR` (if set)
+2. `$ATOM_INSTALL` (if set)
+3. `~/.atom/atom/` (the canonical install location)
+
+A directory is a valid atom checkout if it contains **both** a
+`VERSION` file and `AGENTS.md`. Verify before proceeding.
+
+**If no atom checkout is found**, stop and tell the user — do not
+improvise a bootstrap from memory:
+
+> I couldn't find an atom install (looked in `$ATOM_SOURCE_DIR`,
+> `$ATOM_INSTALL`, and `~/.atom/atom/`). Install it once with:
+>
+> curl -fsSL https://raw.githubusercontent.com/machbuilds/atom/main/install.sh | bash
+>
+> Then re-run `/atom-new`.
+
+## Step 2 — read the real instructions
+
+Read these from the resolved atom directory (not from this skill, not
+from memory — they evolve between releases):
+
+- `AGENTS.md` → **Mode 1: Bootstrap a new project**. This is the
+ canonical, ordered flow. Follow it exactly.
+- The docs `AGENTS.md` tells you to read, in the order it lists them
+ (`docs/VOICE.md`, `docs/WORKFLOW.md`, `docs/PATTERNS.md`,
+ `docs/LESSONS_LEARNED.md`, `docs/HOW_TO_WRITE_CONSTITUTION.md`, and
+ the conditional ones).
+
+Whatever Mode 1 says wins over anything summarized here.
+
+## Step 3 — ask where the project goes
+
+This skill can fire from **any** directory, so you cannot assume the
+current working directory is where the new project should live. Before
+scaffolding, confirm the target:
+
+- A new subdirectory of the cwd (e.g. `./my-project/`)? — the default,
+ and what `atom-setup new ` does.
+- The current directory itself (only if it's empty)?
+- Some other path the user names?
+
+Get an explicit answer. Do not write into a non-empty directory.
+
+## Step 4 — run the bootstrap, conversationally
+
+Drive Mode 1 as a **conversation**, not a form. Ask the project-context
+questions from Mode 1 step 1 (name, stack, deploy target, cost
+envelope, solo vs multi-agent, public vs internal), then proceed
+through scaffold copy, Docker tier, placeholder fill, constitution,
+tooling install, and the GSD/Spec Kit kickoff — in the order `AGENTS.md`
+specifies.
+
+The deterministic terminal wizard (`atom-setup new `) is the
+fast, non-conversational path and already exists for users who want it.
+This skill is the in-Claude, guided counterpart — lean into the
+dialogue: explain tradeoffs when a choice matters, suggest sensible
+defaults, and let the user steer.
+
+## Scope — Mode 1 only
+
+This skill handles **bootstrapping a new project** and nothing else.
+
+`AGENTS.md` also defines Mode 2 (maintain atom itself) and Mode 3
+(build atom features). Those are **dev-facing** — they only make sense
+when working inside the `atom-dev` repo, not from an arbitrary session.
+If the user actually wants one of those, point them at the `atom-dev`
+checkout and stop; do not try to run Mode 2 or 3 from here.