@friedbotstudio/create-baseline 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +10 -4
  2. package/bin/cli.js +252 -127
  3. package/obj/template/{manifest.json → .claude/manifest.json} +22 -8
  4. package/obj/template/.claude/skills/audit-baseline/audit.sh +16 -9
  5. package/obj/template/.claude/skills/changelog/SKILL.md +69 -0
  6. package/obj/template/.claude/skills/changelog/changelog.mjs +163 -0
  7. package/obj/template/.claude/skills/changelog/classifier.mjs +49 -0
  8. package/obj/template/.claude/skills/changelog/state-writer.mjs +19 -0
  9. package/obj/template/.claude/skills/changelog/tests/consent-expired_test.sh +126 -0
  10. package/obj/template/.claude/skills/changelog/tests/golden-path_test.sh +191 -0
  11. package/obj/template/.claude/skills/changelog/tests/idempotent-reentry_test.sh +121 -0
  12. package/obj/template/.claude/skills/changelog/tests/keepachangelog-unreleased-preserved_test.mjs +149 -0
  13. package/obj/template/.claude/skills/changelog/tests/non-git-shortcircuit_test.sh +98 -0
  14. package/obj/template/.claude/skills/changelog/tests/preview-only_test.sh +96 -0
  15. package/obj/template/.claude/skills/changelog/tests/run.sh +28 -0
  16. package/obj/template/.claude/skills/changelog/unreleased-writer.mjs +155 -0
  17. package/obj/template/.claude/skills/changelog/version-preview.mjs +124 -0
  18. package/obj/template/.claude/skills/commit/SKILL.md +1 -1
  19. package/obj/template/.claude/skills/harness/SKILL.md +3 -1
  20. package/obj/template/.claude/skills/triage/SKILL.md +6 -5
  21. package/obj/template/CLAUDE.md +5 -5
  22. package/obj/template/docs/init/seed.md +6 -6
  23. package/package.json +5 -2
  24. package/src/CLAUDE.template.md +5 -5
  25. package/src/cli/install.js +7 -9
  26. package/src/cli/merge.js +15 -10
  27. package/src/cli/tui/doctor.js +56 -0
  28. package/src/cli/tui/install.js +81 -0
  29. package/src/cli/tui/meta.js +63 -0
  30. package/src/cli/tui/splash.js +111 -0
  31. package/src/cli/tui/tokens.js +45 -0
  32. package/src/cli/tui/upgrade.js +119 -0
  33. package/src/seed.template.md +6 -6
@@ -0,0 +1,124 @@
1
+ // Projected-version preview via semantic-release JS API.
2
+ //
3
+ // Returns { version, type, commits } where commits is the analyzer's list
4
+ // of conventional-parsed commits between the last release tag and HEAD.
5
+
6
+ import { execFileSync } from 'node:child_process';
7
+
8
+ // Parse conventional-commit subject lines: type(scope)?: subject + breaking-suffix.
9
+ function parseConventional(subject) {
10
+ const match = subject.match(/^([a-z]+)(?:\(([^)]+)\))?(!)?:\s*(.+)$/i);
11
+ if (!match) return { type: 'unknown', scope: null, breaking: false, subject };
12
+ return {
13
+ type: match[1].toLowerCase(),
14
+ scope: match[2] || null,
15
+ breaking: Boolean(match[3]),
16
+ subject: match[4],
17
+ };
18
+ }
19
+
20
+ // List commits between the latest tag and HEAD via plain git (no semantic-release
21
+ // dep needed for the commit list itself; semantic-release is only used for the
22
+ // projected version computation).
23
+ function listCommitsSinceLastTag(cwd) {
24
+ let lastTag;
25
+ try {
26
+ lastTag = execFileSync('git', ['describe', '--tags', '--abbrev=0'], {
27
+ cwd, encoding: 'utf8',
28
+ }).trim();
29
+ } catch {
30
+ lastTag = null;
31
+ }
32
+ const range = lastTag ? `${lastTag}..HEAD` : 'HEAD';
33
+ let raw;
34
+ try {
35
+ raw = execFileSync('git', ['log', range, '--format=%H%x09%s%x09%b%x00'], {
36
+ cwd, encoding: 'utf8',
37
+ });
38
+ } catch {
39
+ return [];
40
+ }
41
+ return raw.split('\0').filter(Boolean).map((entry) => {
42
+ const [sha, subject, body] = entry.split('\t');
43
+ const parsed = parseConventional(subject || '');
44
+ const breakingBody = /^BREAKING CHANGE:/m.test(body || '');
45
+ return {
46
+ sha,
47
+ subject: parsed.subject,
48
+ body: (body || '').trim(),
49
+ type: parsed.type,
50
+ scope: parsed.scope,
51
+ breaking: parsed.breaking || breakingBody,
52
+ };
53
+ });
54
+ }
55
+
56
+ // Use semantic-release's JS API to compute the next version.
57
+ async function semanticReleaseDryRun(cwd) {
58
+ const mod = await import('semantic-release');
59
+ const semanticRelease = mod.default || mod;
60
+ const noopWritable = { write: () => true, end: () => {} };
61
+ const result = await semanticRelease(
62
+ {
63
+ dryRun: true,
64
+ ci: false,
65
+ branches: ['main', 'master'],
66
+ },
67
+ {
68
+ cwd,
69
+ env: { ...process.env },
70
+ stdout: noopWritable,
71
+ stderr: noopWritable,
72
+ },
73
+ );
74
+ if (result && result.nextRelease) {
75
+ return { version: result.nextRelease.version, type: result.nextRelease.type };
76
+ }
77
+ return { version: null, type: null };
78
+ }
79
+
80
+ // Fallback: derive a projection locally if semantic-release rejects the run
81
+ // (e.g., no .releaserc.json, no remote, no commits since last tag).
82
+ function localProjection(cwd, commits) {
83
+ let lastTag;
84
+ try {
85
+ lastTag = execFileSync('git', ['describe', '--tags', '--abbrev=0'], {
86
+ cwd, encoding: 'utf8',
87
+ }).trim();
88
+ } catch {
89
+ lastTag = 'v0.0.0';
90
+ }
91
+ const baseSemver = lastTag.replace(/^v/, '');
92
+ const baseParts = baseSemver.split('.').map((s) => parseInt(s, 10) || 0);
93
+ let bumpType = null;
94
+ for (const commit of commits) {
95
+ if (commit.breaking) { bumpType = bumpType === 'major' ? 'major' : 'minor'; continue; }
96
+ if (commit.type === 'feat') { bumpType = bumpType === 'major' ? 'major' : (bumpType === 'minor' ? 'minor' : 'minor'); continue; }
97
+ if (commit.type === 'fix' || commit.type === 'perf' || commit.type === 'refactor') {
98
+ if (!bumpType) bumpType = 'patch';
99
+ }
100
+ }
101
+ if (!bumpType) return { version: baseSemver, type: null };
102
+ const [maj, min, pat] = baseParts;
103
+ if (bumpType === 'major') return { version: `${maj + 1}.0.0`, type: 'major' };
104
+ if (bumpType === 'minor') return { version: `${maj}.${min + 1}.0`, type: 'minor' };
105
+ return { version: `${maj}.${min}.${pat + 1}`, type: 'patch' };
106
+ }
107
+
108
+ export async function previewProjectedVersion(cwd) {
109
+ const commits = listCommitsSinceLastTag(cwd);
110
+ let projection;
111
+ try {
112
+ projection = await semanticReleaseDryRun(cwd);
113
+ if (!projection.version) {
114
+ projection = localProjection(cwd, commits);
115
+ }
116
+ } catch {
117
+ projection = localProjection(cwd, commits);
118
+ }
119
+ return {
120
+ version: projection.version || '0.0.0',
121
+ type: projection.type,
122
+ commits,
123
+ };
124
+ }
@@ -5,7 +5,7 @@ description: Workflow Phase 11 — Commit Preparation and Execution. Stages and
5
5
  argument-hint: "[optional commit message; otherwise drafted from the spec/intake]"
6
6
  ---
7
7
 
8
- Prereq: BOTH `archive` AND `memory-flush` in `completed` (Phase 10.5 moved slug artifacts to `docs/archive/<date>/<slug>/`; Phase 10.6 curated `_pending.md` and applied canonical memory writes) AND a valid consent token at `.claude/state/commit_consent` (the Git Commit Guard hook enforces this independently). On any workflow where `memory-flush` is in `exceptions` (rare), this skill SHALL refuse to proceed unless that exception is explicit in `workflow.json`.
8
+ Prereq: ALL of `archive` AND `memory-flush` AND `changelog` in `completed` (Phase 10.5 archives slug artifacts; Phase 10.6 curates `_pending.md`; Phase 11.5 appends keepachangelog entries under `## [Unreleased]` in `CHANGELOG.md`) AND a valid consent token at `.claude/state/commit_consent` (the Git Commit Guard hook enforces this independently). On any workflow where `memory-flush` or `changelog` is in `exceptions` (e.g. non-git projects auto-except both), this skill SHALL refuse to proceed unless those exceptions are explicit in `workflow.json`.
9
9
 
10
10
  **Applicability.** This skill applies only when the project is a git repository. Non-git projects auto-except `commit` at `/triage` time (CLAUDE.md Article IV); the workflow ends after `/archive`.
11
11
 
@@ -106,7 +106,7 @@ The phases the harness loops through, in order:
106
106
  ```
107
107
  intake → scout → research → spec → /approve-spec → tdd → simplify →
108
108
  security → integrate → document → archive → memory-flush →
109
- /grant-commit → commit
109
+ /grant-commit → changelog → commit
110
110
  ```
111
111
 
112
112
  - Phases listed in `workflow.json → exceptions` are skipped.
@@ -158,6 +158,8 @@ On each `/harness` invocation, read `workflow.json` and decide whether to enter
158
158
  | `completed` contains `spec` **and** approval token present, but `tdd`/`swarm-dispatch` not in `completed` | Enter loop; decide swarm-vs-solo at first iteration; invoke the next phase |
159
159
  | `completed` contains `swarm-plan` but no `swarm_approvals/<slug>.approval` | Enter loop; loop exits with `state: yielded` (approve-swarm gate) |
160
160
  | `completed` contains `archive` but no `commit_consent` (git project) | Enter loop; loop exits with `state: yielded` (grant-commit gate) |
161
+ | `completed` contains `grant-commit` consent (token fresh) but no `changelog` | Enter loop; invoke `Skill(changelog)` (Phase 11.5); on success continue to commit |
162
+ | `completed` contains `changelog` but no commit yet (git project) | Enter loop; invoke `Skill(commit)` (Phase 11) |
161
163
  | Phase skill returned an error this invocation | Loop exits with phase-failure reason; user investigates |
162
164
 
163
165
  ## Constraints
@@ -17,7 +17,7 @@ Triage the user's request and set up `.claude/state/workflow.json` so downstream
17
17
  # Steps
18
18
 
19
19
  1. Restate the request back to the user in 1-2 sentences, and name the entry phase you've chosen and why.
20
- 2. **Git-repo detection (mandatory).** Run `git rev-parse --is-inside-work-tree 2>/dev/null` at the project root. If the exit status is non-zero, the project is not a git repository: gate C / `commit` are inapplicable AND the swarm path is unavailable because worktree isolation (the swarm contract's physical safety mechanism) requires git (CLAUDE.md Article IV "Phase 6c and Phase 11 are git-conditional", Article VII). Append `"swarm-plan"`, `"approve-swarm"`, `"swarm-dispatch"`, `"grant-commit"`, and `"commit"` to the exceptions array you'll write in step 4. Tell the user: "Non-git project detected — `swarm-plan`, `approve-swarm`, `swarm-dispatch`, `grant-commit`, and `commit` auto-excepted. Phase 6 routes to solo `/tdd`. Workflow ends after `/archive`. Persistence outside git is your responsibility."
20
+ 2. **Git-repo detection (mandatory).** Run `git rev-parse --is-inside-work-tree 2>/dev/null` at the project root. If the exit status is non-zero, the project is not a git repository: gate C / `commit` are inapplicable AND the swarm path is unavailable because worktree isolation (the swarm contract's physical safety mechanism) requires git (CLAUDE.md Article IV "Phase 6c and Phase 11 are git-conditional", Article VII). Append `"swarm-plan"`, `"approve-swarm"`, `"swarm-dispatch"`, `"grant-commit"`, `"changelog"`, and `"commit"` to the exceptions array you'll write in step 4. `"changelog"` is auto-excepted alongside `"commit"` because Phase 11.5 is a pre-commit curator with no purpose outside a commit-bearing workflow. Tell the user: "Non-git project detected — `swarm-plan`, `approve-swarm`, `swarm-dispatch`, `grant-commit`, `changelog`, and `commit` auto-excepted. Phase 6 routes to solo `/tdd`. Workflow ends after `/archive`. Persistence outside git is your responsibility."
21
21
  3. If the user has not confirmed yet, ask: "Entry phase = <X>. Exceptions = <Y>. Proceed? (or tell me a different entry)"
22
22
  4. On confirmation, write `.claude/state/workflow.json`:
23
23
  ```json
@@ -36,18 +36,19 @@ Triage the user's request and set up `.claude/state/workflow.json` so downstream
36
36
  The `source_backlog_keys` field is optional. When the user's request explicitly names one or more backlog entries this workflow picks up (the common framing is a `Source:` line listing backlog keys), populate the array with those keys. `/commit` (Phase 11) reads this field and invokes `sweep.py --mode stamp-closure` after the commit lands, stamping each named entry with `status: picked-up` + `superseded-at: <today>` so the next `/memory-flush` Step 0a auto-closes them. Absent / empty array → `/commit` skips the stamp step entirely (backward-compatible for any workflow that pre-dates the field). `/triage` does NOT auto-detect backlog keys from free-form prose — the user populates the field (or names them in the triage prompt and you populate it during step 4).
37
37
  5. **Seed the workflow tasklist.** Use the `TaskCreate` tool to register one task per non-excepted phase plus each applicable consent gate. The tasks are the running checklist that `/harness` (or any direct phase invocation) reads to decide the next action; consent-gate tasks block the workflow until the user runs the corresponding command. **When `grant-commit` and `commit` are in exceptions (non-git project), do NOT seed those two tasks** — the workflow ends after `/archive`. Use these canonical templates:
38
38
 
39
- **For `chore` track** (single phase + memory-flush + commit gate):
39
+ **For `chore` track** (single phase + memory-flush + changelog + commit gate):
40
40
  - `Run /chore for <slug>` — activeForm: "Running chore", metadata: `{"phase": "chore"}`
41
41
  - `Run /memory-flush for <slug>` — activeForm: "Running memory-flush", metadata: `{"phase": "memory-flush"}`, addBlockedBy previous
42
42
  - `Wait for /grant-commit` — metadata: `{"phase": "grant-commit", "needs_user": true}`, addBlockedBy previous
43
+ - `Run /changelog for <slug>` — activeForm: "Running changelog", metadata: `{"phase": "changelog"}`, addBlockedBy previous
43
44
  - `Run /commit for <slug>` — activeForm: "Running commit", metadata: `{"phase": "commit"}`, addBlockedBy previous
44
45
 
45
46
  **For `tdd`-entry quickfix track** (skip intake/scout/research/spec/review):
46
- - `Run /tdd`, `Run /simplify`, `Run /security` (only if not in exceptions), `Run /integrate`, `Run /document`, `Run /archive`, `Run /memory-flush`, `Wait for /grant-commit` (`needs_user`), `Run /commit` — each with `addBlockedBy` set to the previous task's id.
47
+ - `Run /tdd`, `Run /simplify`, `Run /security` (only if not in exceptions), `Run /integrate`, `Run /document`, `Run /archive`, `Run /memory-flush`, `Wait for /grant-commit` (`needs_user`), `Run /changelog`, `Run /commit` — each with `addBlockedBy` set to the previous task's id.
47
48
 
48
- **For `spec`-entry track** (skip upstream): start from `Run /spec`, then `Wait for /approve-spec <path>` (`needs_user`), then continue per the full track.
49
+ **For `spec`-entry track** (skip upstream): `Run /spec`, `Wait for /approve-spec <path>` (`needs_user`), `Run /tdd`, `Run /simplify`, `Run /security` (unless excepted), `Run /integrate`, `Run /document`, `Run /archive`, `Run /memory-flush`, `Wait for /grant-commit` (`needs_user`), `Run /changelog`, `Run /commit` — each with `addBlockedBy` set to the previous task's id.
49
50
 
50
- **For `intake`-entry full track**: `Run /intake`, `Run /brd` (only if stakeholder-heavy), `Run /scout`, `Run /research`, `Run /spec`, `Wait for /approve-spec <path>` (`needs_user`), `Run /tdd` OR (`Run /swarm-plan`, `Wait for /approve-swarm <slug>` (`needs_user`), `Run /swarm-dispatch`), `Run /simplify`, `Run /security` (unless excepted), `Run /integrate`, `Run /document`, `Run /archive`, `Run /memory-flush`, `Wait for /grant-commit` (`needs_user`), `Run /commit`. **On non-git projects the swarm branch SHALL NOT be seeded** — only `Run /tdd` goes in the list, regardless of expected component count. Swarm-vs-solo is a Phase-6 main-context decision (per CLAUDE.md Article V) only on git projects; non-git workflows resolve to solo at triage time because `swarm-plan`, `approve-swarm`, and `swarm-dispatch` are already in `exceptions`.
51
+ **For `intake`-entry full track**: `Run /intake`, `Run /brd` (only if stakeholder-heavy), `Run /scout`, `Run /research`, `Run /spec`, `Wait for /approve-spec <path>` (`needs_user`), `Run /tdd` OR (`Run /swarm-plan`, `Wait for /approve-swarm <slug>` (`needs_user`), `Run /swarm-dispatch`), `Run /simplify`, `Run /security` (unless excepted), `Run /integrate`, `Run /document`, `Run /archive`, `Run /memory-flush`, `Wait for /grant-commit` (`needs_user`), `Run /changelog`, `Run /commit`. **On non-git projects the swarm branch SHALL NOT be seeded** — only `Run /tdd` goes in the list, regardless of expected component count. Swarm-vs-solo is a Phase-6 main-context decision (per CLAUDE.md Article V) only on git projects; non-git workflows resolve to solo at triage time because `swarm-plan`, `approve-swarm`, and `swarm-dispatch` are already in `exceptions`. On non-git projects `changelog` is also auto-excepted alongside `commit` (Phase 11.5 only has purpose with a downstream commit).
51
52
 
52
53
  For every task: `subject` is imperative ("Run /scout for <slug>" / "Wait for /approve-spec <path>"); `description` names the phase + the slug; `metadata.phase` carries the phase name; consent-gate tasks set `metadata.needs_user: true`. Wire `addBlockedBy` so each task blocks until its predecessor completes — this surfaces the workflow's true dependency graph and prevents `/harness` from racing past a gate.
53
54
 
@@ -40,7 +40,7 @@ On every new session, before any work, you SHALL:
40
40
 
41
41
  1. **Read** `.claude/project.json` and check the `configured` field.
42
42
  2. **If `configured: false`** — `/init-project` has not run. The repository is in a sanctioned operating state called **project-agnostic mode**: hooks are active but `test_runner` and `lint_runner` run in guide mode and nothing is tailored to the user's stack. You SHALL greet the user with this exact framing:
43
- > "This repo has the Claude Code baseline installed (22 hooks, 1 subagent, 36 skills). It's in **project-agnostic mode** — `test_runner` and `lint_runner` are in guide mode and nothing is tailored to your stack. Run **`/init-project`** to scout the codebase, run the recommender, and generate a config. Skip it if you want baseline-only behavior, but you'll miss stack-specific tailoring."
43
+ > "This repo has the Claude Code baseline installed (22 hooks, 1 subagent, 37 skills). It's in **project-agnostic mode** — `test_runner` and `lint_runner` are in guide mode and nothing is tailored to your stack. Run **`/init-project`** to scout the codebase, run the recommender, and generate a config. Skip it if you want baseline-only behavior, but you'll miss stack-specific tailoring."
44
44
  You SHALL then proceed with whatever the user asks. Project-agnostic mode is **allowed** — the user is not required to run `/init-project` to use the baseline. The `setup_guard` hook surfaces a one-shot reminder on Write/Edit/MultiEdit (rate-limited to 10 minutes); it does **not** block writes. Other guards (commit, env, spec-approval, verify-pass, track, swarm-boundary) remain hard regardless of `configured` state.
45
45
  3. **If `configured: true`** — read `docs/init/seed.md` §16 if present so you know what was added. Tell the user:
46
46
  > "Configured for `<stack>`. Run `/triage \"<request>\"` to start a workflow, or `/harness` for the full pipeline."
@@ -272,13 +272,13 @@ The vendored `impeccable` skill stays untouched (Article IX). `design-ui` is the
272
272
 
273
273
  ## Article XI — Skill provenance and the baseline manifest
274
274
 
275
- A skill at `.claude/skills/<slug>/SKILL.md` is **baseline-owned** iff its YAML frontmatter declares `owner: baseline`. Every other skill on disk — those without an `owner:` field, or those declaring `owner: user` — is user/third-party and out-of-scope of baseline audit checks. Absence-of-`owner` is the deliberate default so a project with pre-existing skills can install the baseline without annotating any of its own files. The build script `scripts/build-manifest.mjs` reads each `owner:` value and emits the canonical baseline-skill set into `obj/template/manifest.json` under `owners.skills` (a JSON object mapping slug → `"baseline"`). The CLI mirrors this manifest verbatim to `<target>/.claude/.baseline-manifest.json` on install. The audit at `.claude/skills/audit-baseline/audit.sh` consumes `manifest.owners.skills` as the canonical baseline-skill enumeration — the previous hard-coded `EXPECTED_SKILLS` set is removed.
275
+ A skill at `.claude/skills/<slug>/SKILL.md` is **baseline-owned** iff its YAML frontmatter declares `owner: baseline`. Every other skill on disk — those without an `owner:` field, or those declaring `owner: user` — is user/third-party and out-of-scope of baseline audit checks. Absence-of-`owner` is the deliberate default so a project with pre-existing skills can install the baseline without annotating any of its own files. The build script `scripts/build-manifest.mjs` reads each `owner:` value and emits the canonical baseline-skill set into the shipped manifest at `obj/template/.claude/manifest.json` under `owners.skills` (a JSON object mapping slug → `"baseline"`). The recursive install copies the manifest straight to `<target>/.claude/manifest.json` (same path inside the `.claude/` subtree, no special-case). The CLI separately writes `<target>/.claude/.baseline-manifest.json` post-install as a runtime sha256 table of the target's actual on-disk contents (used by `doctor` and `upgrade`). The audit at `.claude/skills/audit-baseline/audit.sh` consumes `manifest.owners.skills` from the shipped `.claude/manifest.json` as the canonical baseline-skill enumeration — the previous hard-coded `EXPECTED_SKILLS` set is removed.
276
276
 
277
277
  You SHALL:
278
278
 
279
279
  1. **Declare baseline ownership only.** A SKILL.md that ships in the baseline SHALL declare `owner: baseline` in its frontmatter directly after `name:`. Authoring a user/third-party skill does NOT require any `owner:` annotation — absence is the default. Explicit `owner: user` is permitted but never required. The only frontmatter-related FAIL the audit emits is `invalid owner=<value>` (a present-but-malformed `owner:` field, e.g. typo). Missing-`owner:` is silently skipped.
280
- 2. **Trust the manifest.** The shipped `obj/template/manifest.json` (mirrored to `<target>/.claude/.baseline-manifest.json` on install) is the canonical record of baseline-owned skills and their content hashes. You SHALL NOT maintain a separate hard-coded list of baseline-skill slugs anywhere in the codebase.
281
- 3. **Re-derive on drift.** The audit re-derives sha256 hashes from `manifest.files` for every path under `.claude/skills/<slug>/` whose slug appears in `owners.skills`, and compares against on-disk content. Mismatches surface as `hash mismatch at <path>`. A baseline-listed slug missing from disk surfaces as `baseline skill missing`. These are hard FAIL — drift detection has no opt-out.
280
+ 2. **Trust the manifest.** The shipped manifest at `obj/template/.claude/manifest.json` (delivered to `<target>/.claude/manifest.json` by the recursive install copy) is the canonical record of baseline-owned skills and their content hashes. The runtime `<target>/.claude/.baseline-manifest.json` written by the CLI post-install is a separate file that captures the target's actual on-disk hashes for `doctor`/`upgrade` — do not conflate the two. You SHALL NOT maintain a separate hard-coded list of baseline-skill slugs anywhere in the codebase.
281
+ 3. **Re-derive on drift.** The audit reads the manifest from `<root>/.claude/manifest.json` (consumer projects) with a fallback to `<root>/obj/template/.claude/manifest.json` (the baseline dev repo). It re-derives sha256 hashes from `manifest.files` for every path under `.claude/skills/<slug>/` whose slug appears in `owners.skills`, and compares against on-disk content. Mismatches surface as `hash mismatch at <path>`. A baseline-listed slug missing from disk surfaces as `baseline skill missing`. These are hard FAIL — drift detection has no opt-out.
282
282
  4. **Preserve constitutional citation.** This Article XI SHALL remain in CLAUDE.md AND in `src/CLAUDE.template.md` (byte-equal mirror). The genesis §17 in `docs/init/seed.md` SHALL remain present, with `src/seed.template.md` mirroring it. The audit verifies both citations and reports `CLAUDE.md missing Article XI citation` or `seed.md missing §17 citation` on absence.
283
283
  5. **Out-of-scope skills don't break the audit.** Any skill on disk that doesn't declare `owner: baseline` is out-of-scope: excluded from the baseline count, the names-match check, and the hash-drift check. Installing the baseline into a project that already has its own skills is zero-friction — no per-file annotation required. Maintenance of those skills is the user's responsibility.
284
284
 
@@ -292,7 +292,7 @@ Cryptographic supply-chain attestation, signed lock files, and per-skill aggrega
292
292
  |---|---|
293
293
  | `.claude/hooks/` | 22 hook scripts (17 write/run-boundary + 4 lifecycle + 1 input-boundary). Bash + python3, no jq. |
294
294
  | `.claude/agents/` | 1 baseline subagent: `swarm-worker` (rendered from `src/agents/swarm-worker.template.md`) |
295
- | `.claude/skills/` | 36 skills: artifact (4) + phases (10) + workers (5) + spec helpers (4) + orchestration (3) + memory (1) + shared globals (7) + audit (1) + alt tracks (1) |
295
+ | `.claude/skills/` | 37 skills: artifact (4) + phases (11) + workers (5) + spec helpers (4) + orchestration (3) + memory (1) + shared globals (7) + audit (1) + alt tracks (1) |
296
296
  | `.claude/commands/` | 5 consent/bootstrap gates: `approve-spec`, `approve-swarm`, `grant-commit`, `grant-push`, `init-project` |
297
297
  | `.claude/memory/` | 7 canonical knowledge files + `_pending.md` (staging) + `_resume.md` (continuity snapshot) + `README.md` |
298
298
  | `.claude/project.json` | per-project config (test/lint cmd, TDD globs, destructive patterns, swarm config, additions). Populated by `/init-project`. |
@@ -11,7 +11,7 @@
11
11
 
12
12
  **Mandatory binding language.** Each numbered section (§) below specifies a binding requirement for the baseline. Implementations SHALL conform; `CLAUDE.md` Articles SHALL reference the corresponding §; project amendments (per `CLAUDE.md` Art. X) SHALL NOT contradict any § here.
13
13
 
14
- The baseline turns soft engineering rules (no unauthorized commits, no stubs, no mocks of internal code, no self-approved specs) into structural guarantees enforced by write-boundary hooks. Eleven workflow phases plus one stripped-down chore track (skips TDD; runs verify + archive mandatorily, simplify/integrate/document conditionally), seventeen write/run-boundary guards plus four lifecycle hooks plus one input-boundary hook (twenty-two hook scripts total — twenty `.sh` + two `.mjs` after the JS-port pilot), thirty-six skills, one subagent, and four consent gates. Decisions live in main context; the lone subagent (`swarm-worker`) executes pre-decided recipes in parallel worktrees during `/swarm-dispatch`. Every artifact is archived; every third-party API is looked up against live docs. Project memory accumulates across sessions in `.claude/memory/` — auto-extracted by a Stop hook, curated in main context via `/memory-flush`, self-healing via re-verification.
14
+ The baseline turns soft engineering rules (no unauthorized commits, no stubs, no mocks of internal code, no self-approved specs) into structural guarantees enforced by write-boundary hooks. Eleven workflow phases plus one stripped-down chore track (skips TDD; runs verify + archive mandatorily, simplify/integrate/document conditionally), seventeen write/run-boundary guards plus four lifecycle hooks plus one input-boundary hook (twenty-two hook scripts total — twenty `.sh` + two `.mjs` after the JS-port pilot), thirty-seven skills, one subagent, and four consent gates. Decisions live in main context; the lone subagent (`swarm-worker`) executes pre-decided recipes in parallel worktrees during `/swarm-dispatch`. Every artifact is archived; every third-party API is looked up against live docs. Project memory accumulates across sessions in `.claude/memory/` — auto-extracted by a Stop hook, curated in main context via `/memory-flush`, self-healing via re-verification.
15
15
 
16
16
  ---
17
17
 
@@ -110,7 +110,7 @@ Applies to every language. Mappings for TSX, Node, Python, Go, Rust ship inside
110
110
  │ │ └── lib/common.sh # shared helpers
111
111
  │ ├── agents/ # 1 subagent: swarm-worker (rendered from src/agents/swarm-worker.template.md)
112
112
  │ ├── commands/ # 5 consent/bootstrap gates (user-only — structurally)
113
- │ ├── skills/ # 36 skills: artifact (4) + phases (10) + workers (5) + spec helpers (4) + orchestration (3) + memory (1) + shared globals (7) + audit (1) + alt tracks (1)
113
+ │ ├── skills/ # 37 skills: artifact (4) + phases (11) + workers (5) + spec helpers (4) + orchestration (3) + memory (1) + shared globals (7) + audit (1) + alt tracks (1)
114
114
  │ ├── memory/ # project memory: 7 canonical files + _pending.md (gitignored body) + README.md
115
115
  │ └── state/ # runtime: workflow.json, approvals, swarm plans, verdicts, logs
116
116
  ├── src/ # pristine ship-time templates (overlay source for `npx @friedbotstudio/create-baseline`)
@@ -181,7 +181,7 @@ The baseline ships exactly one subagent. The architectural reason: subagents los
181
181
 
182
182
  **Automated re-rendering by `/init-project`.** Step 6.4 re-renders `swarm-worker.md` from the template, driven by the recommender's `additions.swarm_worker_skills`. The recommender does **not** propose new subagent types — only stack-skill additions for the existing worker. Specialization happens via skills loaded into the worker's context, not via parallel agent personas; new decision-making roles belong in skills, which run in main context.
183
183
 
184
- ### §4.3 Skills (36)
184
+ ### §4.3 Skills (37)
185
185
 
186
186
  Each at `.claude/skills/<name>/SKILL.md`, frontmatter `name` + `description`, plus optional `template.md` (artifact skills) or helper scripts.
187
187
 
@@ -516,7 +516,7 @@ Seed-level requirement: no stale workflow artifacts in the working tree after co
516
516
 
517
517
  **Step 4:** Write `src/agents/swarm-worker.template.md` (canonical-body store, per §4.2) — the only subagent template. Then render `.claude/agents/swarm-worker.md` from it with default tokens. The template carries four tokens — `{{NAME}}`, `{{DESCRIPTION}}`, `{{SKILLS}}`, `{{ROLE_LINE}}`. Default `SKILLS` is the YAML list block ` - scenario\n - implement` (the worker's two mandatory sub-skills). Render-parity holds at this stage. `/init-project` later re-renders the worker with stack-aware tokens when the recommender flags stack-specific skills to preload via `additions.swarm_worker_skills`.
518
518
 
519
- **Step 5:** Write `.claude/skills/` for the 36 skills (§4.3) — 28 workflow/worker/orchestration/memory/alt-track skills you author plus 7 shared globals plus 1 audit skill. The breakdown: artifact drafting (4) + workflow phases (10) + phase workers (5: `scenario`, `implement`, `verify`, `prose`, `design-ui`) + spec helpers (4: `spec-lint`, `spec-render`, `spec-diagram-review`, `spec-traceability-review`) + orchestration (3: `harness`, `swarm-plan`, `swarm-dispatch`) + memory (1: `memory-flush`) + shared globals (7: `claude-automation-recommender`, `code-structure`, `humanizer`, `documentation`, `technical-tutorials`, `copywriting`, `impeccable`) + drift defender (1: `audit-baseline`) + alternate tracks (1: `chore`). The vendored `claude-automation-recommender` (Apache 2.0, from `claude-code-setup`), the writing/quality globals, and the design global ship unchanged with their licenses intact. Artifact skills (intake, brd, spec, rca) each ship a `template.md`. Helper scripts: swarm-plan gets `validate.sh`, swarm-dispatch gets `swarm_merge.sh`, spec-render gets `render.sh`, spec-lint gets `lint.sh`, archive gets `archive.sh`, audit-baseline gets `audit.sh`. All helper scripts `chmod +x`.
519
+ **Step 5:** Write `.claude/skills/` for the 37 skills (§4.3) — 29 workflow/worker/orchestration/memory/alt-track skills you author (the +1 over 28 is the `changelog` Phase 11.5 skill) plus 7 shared globals plus 1 audit skill. The breakdown: artifact drafting (4) + workflow phases (10) + phase workers (5: `scenario`, `implement`, `verify`, `prose`, `design-ui`) + spec helpers (4: `spec-lint`, `spec-render`, `spec-diagram-review`, `spec-traceability-review`) + orchestration (3: `harness`, `swarm-plan`, `swarm-dispatch`) + memory (1: `memory-flush`) + shared globals (7: `claude-automation-recommender`, `code-structure`, `humanizer`, `documentation`, `technical-tutorials`, `copywriting`, `impeccable`) + drift defender (1: `audit-baseline`) + alternate tracks (1: `chore`). The vendored `claude-automation-recommender` (Apache 2.0, from `claude-code-setup`), the writing/quality globals, and the design global ship unchanged with their licenses intact. Artifact skills (intake, brd, spec, rca) each ship a `template.md`. Helper scripts: swarm-plan gets `validate.sh`, swarm-dispatch gets `swarm_merge.sh`, spec-render gets `render.sh`, spec-lint gets `lint.sh`, archive gets `archive.sh`, audit-baseline gets `audit.sh`. All helper scripts `chmod +x`.
520
520
 
521
521
  **Step 6:** Write `.claude/commands/*.md` for the 4 gates (§4.4). All carry `disable-model-invocation: true` as belt-and-braces; structural user-only is enforced by their directory.
522
522
 
@@ -591,9 +591,9 @@ Until `/init-project` runs, this section stays empty. Once populated, every fiel
591
591
 
592
592
  ## §17 — Skill provenance and the baseline manifest
593
593
 
594
- A skill at `.claude/skills/<slug>/SKILL.md` is **baseline-owned** iff its YAML frontmatter declares `owner: baseline`. Baseline-owned skills are those that ship with the baseline; every other skill on disk — those without an `owner:` field, or those declaring `owner: user` — is user/third-party and out-of-scope of baseline audit checks. Absence-of-`owner` is the deliberate default so a project that already has its own skills can install the baseline without annotating any of those files. The build script `scripts/build-manifest.mjs` reads each `owner:` value at release time and emits the canonical baseline-skill set into `obj/template/manifest.json` under `owners.skills` (a JSON object mapping slug → `"baseline"`). The CLI mirrors this manifest verbatim to `<target>/.claude/.baseline-manifest.json` on `freshInstall`/`forceInstall`/`merge`.
594
+ A skill at `.claude/skills/<slug>/SKILL.md` is **baseline-owned** iff its YAML frontmatter declares `owner: baseline`. Baseline-owned skills are those that ship with the baseline; every other skill on disk — those without an `owner:` field, or those declaring `owner: user` — is user/third-party and out-of-scope of baseline audit checks. Absence-of-`owner` is the deliberate default so a project that already has its own skills can install the baseline without annotating any of those files. The build script `scripts/build-manifest.mjs` reads each `owner:` value at release time and emits the canonical baseline-skill set into the shipped manifest at `obj/template/.claude/manifest.json` under `owners.skills` (a JSON object mapping slug → `"baseline"`). The recursive install copies the manifest into the consumer target at `<target>/.claude/manifest.json` (same in-tree path, no special-case). The CLI separately writes `<target>/.claude/.baseline-manifest.json` post-install on `freshInstall`/`forceInstall`/`merge` — that file is the runtime snapshot of the target's actual on-disk hashes, consumed by `doctor` and `upgrade`. The two files coexist by design: the shipped manifest is frozen at release time and carries `owners.skills`; the runtime manifest is generated at install time and is hash-only.
595
595
 
596
- The audit at `.claude/skills/audit-baseline/audit.sh` consumes `manifest.owners.skills` as the canonical baseline-skill enumeration (replacing the previous hard-coded `EXPECTED_SKILLS` set). For every baseline-owned skill, the audit re-derives sha256 hashes from `manifest.files` and compares against on-disk content; a mismatch is reported as `hash mismatch at <path>` against the named slug. A baseline skill present in the manifest but absent from disk is reported as `baseline skill missing`. A SKILL.md whose `owner:` field is present but carries an invalid value (anything other than `baseline` or `user`) is reported as `invalid owner=<value>`. SKILL.md files without an `owner:` field are treated as user/third-party and silently skipped — they are excluded from the baseline count, the names-match check, and the hash-drift check, so installing the baseline into a project that already has its own skills never breaks the audit.
596
+ The audit at `.claude/skills/audit-baseline/audit.sh` consumes `manifest.owners.skills` as the canonical baseline-skill enumeration (replacing the previous hard-coded `EXPECTED_SKILLS` set). It reads the manifest from `<root>/.claude/manifest.json` first (consumer projects) and falls back to `<root>/obj/template/.claude/manifest.json` (the baseline dev repo where `npm run build` writes the manifest). For every baseline-owned skill, the audit re-derives sha256 hashes from `manifest.files` and compares against on-disk content; a mismatch is reported as `hash mismatch at <path>` against the named slug. A baseline skill present in the manifest but absent from disk is reported as `baseline skill missing`. A SKILL.md whose `owner:` field is present but carries an invalid value (anything other than `baseline` or `user`) is reported as `invalid owner=<value>`. SKILL.md files without an `owner:` field are treated as user/third-party and silently skipped — they are excluded from the baseline count, the names-match check, and the hash-drift check, so installing the baseline into a project that already has its own skills never breaks the audit.
597
597
 
598
598
  The audit also verifies constitutional citation: CLAUDE.md SHALL contain the literal string "Article XI" and a reference to the manifest, and `docs/init/seed.md` SHALL contain "§17" and a manifest reference. Missing citations trigger FAIL with `CLAUDE.md missing Article XI citation` or `seed.md missing §17 citation`.
599
599
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friedbotstudio/create-baseline",
3
- "version": "0.3.0",
4
- "description": "Zero-dependency Node CLI scaffolder that materializes the Claude Code baseline (hooks, skills, commands, MCP servers, governance docs) into a target project. Run via `npx @friedbotstudio/create-baseline <target>`.",
3
+ "version": "0.5.0",
4
+ "description": "Node CLI scaffolder that materializes the Claude Code baseline (hooks, skills, commands, MCP servers, governance docs) into a target project, with branded interactive install / upgrade / doctor flows. Run via `npx @friedbotstudio/create-baseline <target>`.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "create-baseline": "bin/cli.js"
@@ -43,6 +43,9 @@
43
43
  "email": "hello@friedbotstudio.com"
44
44
  },
45
45
  "homepage": "https://baseline.friedbotstudio.com",
46
+ "dependencies": {
47
+ "@clack/prompts": "1.4.0"
48
+ },
46
49
  "devDependencies": {
47
50
  "@11ty/eleventy": "3.1.5",
48
51
  "@semantic-release/changelog": "6.0.3",
@@ -40,7 +40,7 @@ On every new session, before any work, you SHALL:
40
40
 
41
41
  1. **Read** `.claude/project.json` and check the `configured` field.
42
42
  2. **If `configured: false`** — `/init-project` has not run. The repository is in a sanctioned operating state called **project-agnostic mode**: hooks are active but `test_runner` and `lint_runner` run in guide mode and nothing is tailored to the user's stack. You SHALL greet the user with this exact framing:
43
- > "This repo has the Claude Code baseline installed (22 hooks, 1 subagent, 36 skills). It's in **project-agnostic mode** — `test_runner` and `lint_runner` are in guide mode and nothing is tailored to your stack. Run **`/init-project`** to scout the codebase, run the recommender, and generate a config. Skip it if you want baseline-only behavior, but you'll miss stack-specific tailoring."
43
+ > "This repo has the Claude Code baseline installed (22 hooks, 1 subagent, 37 skills). It's in **project-agnostic mode** — `test_runner` and `lint_runner` are in guide mode and nothing is tailored to your stack. Run **`/init-project`** to scout the codebase, run the recommender, and generate a config. Skip it if you want baseline-only behavior, but you'll miss stack-specific tailoring."
44
44
  You SHALL then proceed with whatever the user asks. Project-agnostic mode is **allowed** — the user is not required to run `/init-project` to use the baseline. The `setup_guard` hook surfaces a one-shot reminder on Write/Edit/MultiEdit (rate-limited to 10 minutes); it does **not** block writes. Other guards (commit, env, spec-approval, verify-pass, track, swarm-boundary) remain hard regardless of `configured` state.
45
45
  3. **If `configured: true`** — read `docs/init/seed.md` §16 if present so you know what was added. Tell the user:
46
46
  > "Configured for `<stack>`. Run `/triage \"<request>\"` to start a workflow, or `/harness` for the full pipeline."
@@ -272,13 +272,13 @@ The vendored `impeccable` skill stays untouched (Article IX). `design-ui` is the
272
272
 
273
273
  ## Article XI — Skill provenance and the baseline manifest
274
274
 
275
- A skill at `.claude/skills/<slug>/SKILL.md` is **baseline-owned** iff its YAML frontmatter declares `owner: baseline`. Every other skill on disk — those without an `owner:` field, or those declaring `owner: user` — is user/third-party and out-of-scope of baseline audit checks. Absence-of-`owner` is the deliberate default so a project with pre-existing skills can install the baseline without annotating any of its own files. The build script `scripts/build-manifest.mjs` reads each `owner:` value and emits the canonical baseline-skill set into `obj/template/manifest.json` under `owners.skills` (a JSON object mapping slug → `"baseline"`). The CLI mirrors this manifest verbatim to `<target>/.claude/.baseline-manifest.json` on install. The audit at `.claude/skills/audit-baseline/audit.sh` consumes `manifest.owners.skills` as the canonical baseline-skill enumeration — the previous hard-coded `EXPECTED_SKILLS` set is removed.
275
+ A skill at `.claude/skills/<slug>/SKILL.md` is **baseline-owned** iff its YAML frontmatter declares `owner: baseline`. Every other skill on disk — those without an `owner:` field, or those declaring `owner: user` — is user/third-party and out-of-scope of baseline audit checks. Absence-of-`owner` is the deliberate default so a project with pre-existing skills can install the baseline without annotating any of its own files. The build script `scripts/build-manifest.mjs` reads each `owner:` value and emits the canonical baseline-skill set into the shipped manifest at `obj/template/.claude/manifest.json` under `owners.skills` (a JSON object mapping slug → `"baseline"`). The recursive install copies the manifest straight to `<target>/.claude/manifest.json` (same path inside the `.claude/` subtree, no special-case). The CLI separately writes `<target>/.claude/.baseline-manifest.json` post-install as a runtime sha256 table of the target's actual on-disk contents (used by `doctor` and `upgrade`). The audit at `.claude/skills/audit-baseline/audit.sh` consumes `manifest.owners.skills` from the shipped `.claude/manifest.json` as the canonical baseline-skill enumeration — the previous hard-coded `EXPECTED_SKILLS` set is removed.
276
276
 
277
277
  You SHALL:
278
278
 
279
279
  1. **Declare baseline ownership only.** A SKILL.md that ships in the baseline SHALL declare `owner: baseline` in its frontmatter directly after `name:`. Authoring a user/third-party skill does NOT require any `owner:` annotation — absence is the default. Explicit `owner: user` is permitted but never required. The only frontmatter-related FAIL the audit emits is `invalid owner=<value>` (a present-but-malformed `owner:` field, e.g. typo). Missing-`owner:` is silently skipped.
280
- 2. **Trust the manifest.** The shipped `obj/template/manifest.json` (mirrored to `<target>/.claude/.baseline-manifest.json` on install) is the canonical record of baseline-owned skills and their content hashes. You SHALL NOT maintain a separate hard-coded list of baseline-skill slugs anywhere in the codebase.
281
- 3. **Re-derive on drift.** The audit re-derives sha256 hashes from `manifest.files` for every path under `.claude/skills/<slug>/` whose slug appears in `owners.skills`, and compares against on-disk content. Mismatches surface as `hash mismatch at <path>`. A baseline-listed slug missing from disk surfaces as `baseline skill missing`. These are hard FAIL — drift detection has no opt-out.
280
+ 2. **Trust the manifest.** The shipped manifest at `obj/template/.claude/manifest.json` (delivered to `<target>/.claude/manifest.json` by the recursive install copy) is the canonical record of baseline-owned skills and their content hashes. The runtime `<target>/.claude/.baseline-manifest.json` written by the CLI post-install is a separate file that captures the target's actual on-disk hashes for `doctor`/`upgrade` — do not conflate the two. You SHALL NOT maintain a separate hard-coded list of baseline-skill slugs anywhere in the codebase.
281
+ 3. **Re-derive on drift.** The audit reads the manifest from `<root>/.claude/manifest.json` (consumer projects) with a fallback to `<root>/obj/template/.claude/manifest.json` (the baseline dev repo). It re-derives sha256 hashes from `manifest.files` for every path under `.claude/skills/<slug>/` whose slug appears in `owners.skills`, and compares against on-disk content. Mismatches surface as `hash mismatch at <path>`. A baseline-listed slug missing from disk surfaces as `baseline skill missing`. These are hard FAIL — drift detection has no opt-out.
282
282
  4. **Preserve constitutional citation.** This Article XI SHALL remain in CLAUDE.md AND in `src/CLAUDE.template.md` (byte-equal mirror). The genesis §17 in `docs/init/seed.md` SHALL remain present, with `src/seed.template.md` mirroring it. The audit verifies both citations and reports `CLAUDE.md missing Article XI citation` or `seed.md missing §17 citation` on absence.
283
283
  5. **Out-of-scope skills don't break the audit.** Any skill on disk that doesn't declare `owner: baseline` is out-of-scope: excluded from the baseline count, the names-match check, and the hash-drift check. Installing the baseline into a project that already has its own skills is zero-friction — no per-file annotation required. Maintenance of those skills is the user's responsibility.
284
284
 
@@ -292,7 +292,7 @@ Cryptographic supply-chain attestation, signed lock files, and per-skill aggrega
292
292
  |---|---|
293
293
  | `.claude/hooks/` | 22 hook scripts (17 write/run-boundary + 4 lifecycle + 1 input-boundary). Bash + python3, no jq. |
294
294
  | `.claude/agents/` | 1 baseline subagent: `swarm-worker` (rendered from `src/agents/swarm-worker.template.md`) |
295
- | `.claude/skills/` | 36 skills: artifact (4) + phases (10) + workers (5) + spec helpers (4) + orchestration (3) + memory (1) + shared globals (7) + audit (1) + alt tracks (1) |
295
+ | `.claude/skills/` | 37 skills: artifact (4) + phases (11) + workers (5) + spec helpers (4) + orchestration (3) + memory (1) + shared globals (7) + audit (1) + alt tracks (1) |
296
296
  | `.claude/commands/` | 5 consent/bootstrap gates: `approve-spec`, `approve-swarm`, `grant-commit`, `grant-push`, `init-project` |
297
297
  | `.claude/memory/` | 7 canonical knowledge files + `_pending.md` (staging) + `_resume.md` (continuity snapshot) + `README.md` |
298
298
  | `.claude/project.json` | per-project config (test/lint cmd, TDD globs, destructive patterns, swarm config, additions). Populated by `/init-project`. |
@@ -12,15 +12,13 @@ const NPMRC_TEMPLATE_PATH = join(PACKAGE_ROOT, 'src/.npmrc.template');
12
12
 
13
13
  export const NEVER_TOUCH = Object.freeze(['.claude/project.json']);
14
14
  export const SPECIAL_MERGE = Object.freeze(['.mcp.json']);
15
- // Files present in the shipped template that must NOT be cp'd to target. These
16
- // are reference artifacts the CLI consults from templateDir (or that ship for
17
- // inspection-time provenance), never materialized at consumer project root.
18
- // `manifest.json`: the shipped sha256 table. The CLI's runtime manifest lives
19
- // at `target/.claude/.baseline-manifest.json` (written by writeBaselineManifest);
20
- // `target/manifest.json` would be a confusing duplicate. Keep the file in the
21
- // published tarball so anyone inspecting `node_modules/<pkg>/obj/template/` can
22
- // see what shipped, but exclude it from the fresh/force install copy.
23
- export const COPY_EXCLUDE = Object.freeze(['manifest.json']);
15
+ // The shipped manifest now lives at `.claude/manifest.json` (inside the
16
+ // template's .claude/ subtree), so the recursive cp drops it at the correct
17
+ // consumer path without any special-case filtering. The consumer-side audit
18
+ // (`.claude/skills/audit-baseline/audit.sh`) reads it from there for
19
+ // hash-drift detection. COPY_EXCLUDE stays as a list (currently empty) so
20
+ // future never-copy artifacts can be added without API churn at the callers.
21
+ export const COPY_EXCLUDE = Object.freeze([]);
24
22
 
25
23
  async function listFiles(root, base = root, acc = []) {
26
24
  for (const entry of await readdir(root, { withFileTypes: true })) {
package/src/cli/merge.js CHANGED
@@ -22,7 +22,8 @@ async function copyFile(src, dst) {
22
22
  await cp(src, dst, { force: true });
23
23
  }
24
24
 
25
- export async function threeWayMerge(templateDir, target, oldManifest, newManifest) {
25
+ export async function threeWayMerge(templateDir, target, oldManifest, newManifest, opts = {}) {
26
+ const { dryRun = false, onSkipCustomized = null } = opts;
26
27
  const actions = [];
27
28
  const oldFiles = oldManifest?.files ?? {};
28
29
  const newFiles = newManifest?.files ?? {};
@@ -36,7 +37,7 @@ export async function threeWayMerge(templateDir, target, oldManifest, newManifes
36
37
  if (await pathExists(tgtPath)) {
37
38
  actions.push({ kind: ACTION_KINDS.NEVER_TOUCH_PRESERVE, path: rel, reason: 'NEVER_TOUCH path present in target' });
38
39
  } else if (rel in newFiles) {
39
- await copyFile(tplPath, tgtPath);
40
+ if (!dryRun) await copyFile(tplPath, tgtPath);
40
41
  actions.push({ kind: ACTION_KINDS.NEVER_TOUCH_ADD, path: rel, reason: 'NEVER_TOUCH path absent; written from template' });
41
42
  }
42
43
  continue;
@@ -44,7 +45,7 @@ export async function threeWayMerge(templateDir, target, oldManifest, newManifes
44
45
 
45
46
  if (SPECIAL_MERGE.includes(rel)) {
46
47
  if (rel in newFiles && await pathExists(tplPath)) {
47
- await deepMergeMcpServers(tplPath, tgtPath);
48
+ if (!dryRun) await deepMergeMcpServers(tplPath, tgtPath);
48
49
  actions.push({ kind: ACTION_KINDS.SPECIAL_MERGE, path: rel, reason: 'additive deep-merge applied' });
49
50
  }
50
51
  continue;
@@ -56,7 +57,7 @@ export async function threeWayMerge(templateDir, target, oldManifest, newManifes
56
57
  const tgtHash = targetExists ? await hashFile(tgtPath) : null;
57
58
 
58
59
  if (!targetExists && newHash) {
59
- await copyFile(tplPath, tgtPath);
60
+ if (!dryRun) await copyFile(tplPath, tgtPath);
60
61
  actions.push({ kind: ACTION_KINDS.ADD, path: rel, reason: 'new in template; not present in target' });
61
62
  continue;
62
63
  }
@@ -67,13 +68,19 @@ export async function threeWayMerge(templateDir, target, oldManifest, newManifes
67
68
  }
68
69
 
69
70
  if (newHash && oldHash && tgtHash === oldHash) {
70
- await copyFile(tplPath, tgtPath);
71
+ if (!dryRun) await copyFile(tplPath, tgtPath);
71
72
  actions.push({ kind: ACTION_KINDS.OVERWRITE, path: rel, reason: 'target untouched since last install; updated' });
72
73
  continue;
73
74
  }
74
75
 
75
76
  if (newHash && tgtHash && tgtHash !== oldHash) {
76
- actions.push({ kind: ACTION_KINDS.SKIP_CUSTOMIZED, path: rel, reason: 'target customized since last install' });
77
+ const choice = onSkipCustomized ? await onSkipCustomized(rel) : 'keep-mine';
78
+ if (choice === 'take-theirs') {
79
+ if (!dryRun) await copyFile(tplPath, tgtPath);
80
+ actions.push({ kind: ACTION_KINDS.OVERWRITE, path: rel, reason: 'customized file; user chose take-theirs' });
81
+ } else {
82
+ actions.push({ kind: ACTION_KINDS.SKIP_CUSTOMIZED, path: rel, reason: 'target customized since last install' });
83
+ }
77
84
  continue;
78
85
  }
79
86
 
@@ -84,10 +91,8 @@ export async function threeWayMerge(templateDir, target, oldManifest, newManifes
84
91
  // prune. Otherwise the user accumulates stale baseline files forever.
85
92
  // - target customized (tgtHash != oldHash) → preserve to avoid
86
93
  // destroying user work; report drift via exit 3.
87
- // Pruning only runs when --merge already applies; there is no separate
88
- // flag (decision recorded in README).
89
94
  if (targetExists && tgtHash === oldHash) {
90
- await unlink(tgtPath);
95
+ if (!dryRun) await unlink(tgtPath);
91
96
  actions.push({ kind: ACTION_KINDS.PRUNE, path: rel, reason: 'removed from new template; target was untouched — deleted' });
92
97
  } else if (targetExists) {
93
98
  actions.push({ kind: ACTION_KINDS.PRUNE_SKIPPED_CUSTOMIZED, path: rel, reason: 'removed from new template; target customized — preserved' });
@@ -96,7 +101,7 @@ export async function threeWayMerge(templateDir, target, oldManifest, newManifes
96
101
  }
97
102
  }
98
103
 
99
- if (newManifest) {
104
+ if (newManifest && !dryRun) {
100
105
  await mkdir(join(target, '.claude'), { recursive: true });
101
106
  await saveManifest(join(target, '.claude/.baseline-manifest.json'), newManifest);
102
107
  }
@@ -0,0 +1,56 @@
1
+ // Domain — branded sectioned doctor report. Consumes the structured
2
+ // DoctorReport from src/cli/doctor.js (unchanged) and writes a colorized,
3
+ // sectioned rendering to stdout. The non-TTY plain path stays on doctor.js's
4
+ // formatReport — this renderer is only invoked when stdout is a TTY.
5
+
6
+ import { accent, muted, success, warn, error, accentLight } from './tokens.js';
7
+
8
+ function brandHeader(target, manifestInfo) {
9
+ const lines = [accent('Baseline doctor')];
10
+ if (target) lines.push(muted(`target: ${target}`));
11
+ if (manifestInfo) lines.push(muted(`manifest: ${manifestInfo}`));
12
+ return lines;
13
+ }
14
+
15
+ export function render(report) {
16
+ if (report.error) {
17
+ const headerLines = brandHeader(report.target);
18
+ process.stdout.write(headerLines.join('\n') + '\n\n');
19
+ process.stdout.write(`${error('doctor:')} ${report.error}\n`);
20
+ return;
21
+ }
22
+ const lines = brandHeader(report.target, `version ${report.manifestVersion}, installed ${report.generatedAt}`);
23
+ lines.push('');
24
+ lines.push(` ${success('matched')}: ${report.matched.length}`);
25
+ lines.push(` ${accentLight('customized')}: ${report.customized.length}`);
26
+ lines.push(` ${error('missing')}: ${report.missing.length}`);
27
+ lines.push(` ${warn('added')}: ${report.added.length}`);
28
+
29
+ if (report.missing.length > 0) {
30
+ lines.push('');
31
+ lines.push(error('Missing (deleted from disk; exit 1):'));
32
+ for (const p of report.missing) lines.push(` - ${p}`);
33
+ }
34
+ if (report.customized.length > 0) {
35
+ lines.push('');
36
+ const header = report.strict
37
+ ? accentLight('Customized (strict mode → exit 1):')
38
+ : accentLight('Customized (informational):');
39
+ lines.push(header);
40
+ if (Array.isArray(report.tampered) && report.tampered.length > 0) {
41
+ for (const entry of report.tampered) {
42
+ lines.push(` ${warn('TAMPERED')}: ${entry.path}`);
43
+ lines.push(` shipped=${muted(entry.shipped)} observed=${muted(entry.observed)}`);
44
+ }
45
+ } else {
46
+ for (const p of report.customized) lines.push(` - ${p}`);
47
+ }
48
+ }
49
+ if (report.added.length > 0) {
50
+ lines.push('');
51
+ lines.push(warn('Added under .claude/ since install (likely /init-project; informational):'));
52
+ for (const p of report.added) lines.push(` - ${p}`);
53
+ }
54
+
55
+ process.stdout.write(lines.join('\n') + '\n');
56
+ }
@@ -0,0 +1,81 @@
1
+ // Domain — branded install flow. Composes the pure-data install + plantuml
2
+ // foundations behind a clack-style presentation seam. The `prompts` parameter
3
+ // defaults to @clack/prompts but is injected in tests.
4
+
5
+ import * as clackModule from '@clack/prompts';
6
+ import { readFile } from 'node:fs/promises';
7
+ import { freshInstall, forceInstall } from '../install.js';
8
+ import { fetchPlantumlIfMissing, FETCH_OUTCOMES } from '../plantuml.js';
9
+ import { renderBrandStrip } from './splash.js';
10
+
11
+ const SUCCESS = 0;
12
+ const ERR_INSTALL_FAILED = 1;
13
+ const ERR_PLANTUML_REQUIRED = 4;
14
+
15
+ export async function run({ target, opts = {}, prompts = clackModule } = {}) {
16
+ if (!target || typeof target !== 'string') {
17
+ throw new Error('tui.install.run requires a non-empty string target');
18
+ }
19
+ if (!opts.templateDir) {
20
+ throw new Error('tui.install.run requires opts.templateDir');
21
+ }
22
+
23
+ const version = await readPackageVersion();
24
+ process.stdout.write(renderBrandStrip({ version, subtitle: 'install' }));
25
+ prompts.intro('create-baseline');
26
+
27
+ const spinner = prompts.spinner();
28
+ spinner.start('Copying baseline files');
29
+
30
+ try {
31
+ await copyTemplate(target, opts);
32
+ } catch (err) {
33
+ spinner.error('Install failed');
34
+ prompts.outro(err.message);
35
+ return ERR_INSTALL_FAILED;
36
+ }
37
+
38
+ const plantumlExit = await fetchPlantumlBranded(target, opts, prompts, spinner);
39
+ if (plantumlExit !== SUCCESS) return plantumlExit;
40
+
41
+ spinner.stop('Baseline installed');
42
+ prompts.outro(`Installed at ${target}`);
43
+ return SUCCESS;
44
+ }
45
+
46
+ async function copyTemplate(target, opts) {
47
+ const installOpts = { withNpmrc: !!opts.withNpmrc };
48
+ if (opts.force) await forceInstall(opts.templateDir, target, installOpts);
49
+ else await freshInstall(opts.templateDir, target, installOpts);
50
+ }
51
+
52
+ async function fetchPlantumlBranded(target, opts, prompts, spinner) {
53
+ if (opts.noPlantuml) return SUCCESS;
54
+ spinner.message('Fetching PlantUML jar');
55
+ const result = await fetchPlantumlIfMissing(target, {
56
+ noPlantuml: opts.noPlantuml,
57
+ requirePlantuml: opts.requirePlantuml,
58
+ });
59
+ if (result.outcome === FETCH_OUTCOMES.ERRORED_REQUIRE_PLANTUML) {
60
+ spinner.error('PlantUML required but unavailable');
61
+ prompts.outro(result.reason);
62
+ return ERR_PLANTUML_REQUIRED;
63
+ }
64
+ if (
65
+ result.outcome === FETCH_OUTCOMES.WARNED_NETWORK_FAILURE ||
66
+ result.outcome === FETCH_OUTCOMES.WARNED_HASH_MISMATCH
67
+ ) {
68
+ prompts.log.warn(`PlantUML jar: ${result.reason} — install continued`);
69
+ }
70
+ return SUCCESS;
71
+ }
72
+
73
+ async function readPackageVersion() {
74
+ try {
75
+ const url = new URL('../../../package.json', import.meta.url);
76
+ const pkg = JSON.parse(await readFile(url, 'utf8'));
77
+ return pkg.version || '0.0.0';
78
+ } catch {
79
+ return '0.0.0';
80
+ }
81
+ }