@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.
- package/README.md +10 -4
- package/bin/cli.js +252 -127
- package/obj/template/{manifest.json → .claude/manifest.json} +22 -8
- package/obj/template/.claude/skills/audit-baseline/audit.sh +16 -9
- package/obj/template/.claude/skills/changelog/SKILL.md +69 -0
- package/obj/template/.claude/skills/changelog/changelog.mjs +163 -0
- package/obj/template/.claude/skills/changelog/classifier.mjs +49 -0
- package/obj/template/.claude/skills/changelog/state-writer.mjs +19 -0
- package/obj/template/.claude/skills/changelog/tests/consent-expired_test.sh +126 -0
- package/obj/template/.claude/skills/changelog/tests/golden-path_test.sh +191 -0
- package/obj/template/.claude/skills/changelog/tests/idempotent-reentry_test.sh +121 -0
- package/obj/template/.claude/skills/changelog/tests/keepachangelog-unreleased-preserved_test.mjs +149 -0
- package/obj/template/.claude/skills/changelog/tests/non-git-shortcircuit_test.sh +98 -0
- package/obj/template/.claude/skills/changelog/tests/preview-only_test.sh +96 -0
- package/obj/template/.claude/skills/changelog/tests/run.sh +28 -0
- package/obj/template/.claude/skills/changelog/unreleased-writer.mjs +155 -0
- package/obj/template/.claude/skills/changelog/version-preview.mjs +124 -0
- package/obj/template/.claude/skills/commit/SKILL.md +1 -1
- package/obj/template/.claude/skills/harness/SKILL.md +3 -1
- package/obj/template/.claude/skills/triage/SKILL.md +6 -5
- package/obj/template/CLAUDE.md +5 -5
- package/obj/template/docs/init/seed.md +6 -6
- package/package.json +5 -2
- package/src/CLAUDE.template.md +5 -5
- package/src/cli/install.js +7 -9
- package/src/cli/merge.js +15 -10
- package/src/cli/tui/doctor.js +56 -0
- package/src/cli/tui/install.js +81 -0
- package/src/cli/tui/meta.js +63 -0
- package/src/cli/tui/splash.js +111 -0
- package/src/cli/tui/tokens.js +45 -0
- package/src/cli/tui/upgrade.js +119 -0
- package/src/seed.template.md +6 -6
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Domain — branded renderers for the meta commands (--help, --version) and
|
|
2
|
+
// for usage-class errors. In a TTY, the splash marquee (wordmark + brand
|
|
3
|
+
// strip from splash.js) frames the canonical body; in non-TTY the body is
|
|
4
|
+
// emitted unchanged so that piped consumers (`$(cli --version)`,
|
|
5
|
+
// `cli --help | grep ...`) keep working byte-clean.
|
|
6
|
+
|
|
7
|
+
import { accent, muted, rule, error as errorPaint } from './tokens.js';
|
|
8
|
+
import {
|
|
9
|
+
renderSplash,
|
|
10
|
+
renderBrandStrip,
|
|
11
|
+
renderVersionMarquee,
|
|
12
|
+
wordmarkFits,
|
|
13
|
+
} from './splash.js';
|
|
14
|
+
|
|
15
|
+
const DISCOVER_URL = 'https://baseline.friedbotstudio.com/';
|
|
16
|
+
|
|
17
|
+
export function renderHelp(helpText, _version) {
|
|
18
|
+
const body = helpText.endsWith('\n') ? helpText : helpText + '\n';
|
|
19
|
+
if (!process.stdout.isTTY || !wordmarkFits()) {
|
|
20
|
+
process.stdout.write(body);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
process.stdout.write(renderSplash({
|
|
24
|
+
tryLine: 'npx @friedbotstudio/create-baseline ./my-project',
|
|
25
|
+
discoverUrl: DISCOVER_URL,
|
|
26
|
+
}));
|
|
27
|
+
process.stdout.write(body);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function renderVersion(version) {
|
|
31
|
+
if (!process.stdout.isTTY) {
|
|
32
|
+
process.stdout.write(`${version}\n`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (wordmarkFits()) {
|
|
36
|
+
process.stdout.write(renderVersionMarquee(version));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
process.stdout.write(renderBrandStrip({ version }));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Usage errors always print to stderr (so a `cli ... 2>/dev/null` pipeline
|
|
43
|
+
// can still consume stdout cleanly). In a TTY we wrap the message and the
|
|
44
|
+
// HELP_TEXT body in the same brand banner used by --help, with the error
|
|
45
|
+
// label painted in --mac-red. In non-TTY we emit a plain `Error: <msg>`
|
|
46
|
+
// line followed by the canonical help body — same body, no ANSI.
|
|
47
|
+
export function renderUsageError(msg, helpText, version) {
|
|
48
|
+
const body = helpText.endsWith('\n') ? helpText : helpText + '\n';
|
|
49
|
+
if (!process.stderr.isTTY) {
|
|
50
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
51
|
+
process.stderr.write(body);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const banner = [
|
|
55
|
+
'',
|
|
56
|
+
` ${errorPaint('Error')} ${msg}`,
|
|
57
|
+
` ${muted(`@friedbotstudio/create-baseline v${version}`)}`,
|
|
58
|
+
` ${rule('─'.repeat(48))}`,
|
|
59
|
+
'',
|
|
60
|
+
].join('\n');
|
|
61
|
+
process.stderr.write(banner + '\n');
|
|
62
|
+
process.stderr.write(body);
|
|
63
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Domain — branded splash surfaces for the CLI. Renders a chunky pixel-art
|
|
2
|
+
// "BASELINE" wordmark in three bands of FBS orange (bevel: shadow / mid /
|
|
3
|
+
// highlight / mid / shadow) so the marquee surfaces (--help, --version,
|
|
4
|
+
// no-arg landing) share a single visual identity. Slimmer brand strip is
|
|
5
|
+
// reused by install / upgrade intros and inside the usage-error renderer.
|
|
6
|
+
//
|
|
7
|
+
// All renderers degrade cleanly when stdout is not a TTY or NO_COLOR is set
|
|
8
|
+
// (paintRGB short-circuits to plain text). When the terminal is narrower
|
|
9
|
+
// than the wordmark, callers should fall through to the plain banner via
|
|
10
|
+
// `wordmarkFits(width)` instead of letting the glyphs wrap.
|
|
11
|
+
|
|
12
|
+
import { paintRGB, PALETTE, accent, muted } from './tokens.js';
|
|
13
|
+
|
|
14
|
+
// ANSI-Shadow style block-letter wordmark for "BASELINE". 5 rows × ~60 cols.
|
|
15
|
+
// Kept as raw strings (not paint-wrapped) so renderWordmark can apply a
|
|
16
|
+
// per-row shade. Trailing spaces matter — they're part of the glyph shape.
|
|
17
|
+
const WORDMARK = [
|
|
18
|
+
'██████ █████ ███████ ███████ ██ ██ ███ ██ ███████',
|
|
19
|
+
'██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ',
|
|
20
|
+
'██████ ███████ ███████ █████ ██ ██ ██ ██ ██ █████ ',
|
|
21
|
+
'██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ',
|
|
22
|
+
'██████ ██ ██ ███████ ███████ ███████ ██ ██ ████ ███████',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
// Outline trace — mirrors the bottom row of the wordmark using the upper
|
|
26
|
+
// one-eighth block (▔) so it visually kisses the base of every letter,
|
|
27
|
+
// producing the subtle "letters are sitting on a baseline" shadow from
|
|
28
|
+
// the skills.sh reference. Painted in accentShadow so it reads as a
|
|
29
|
+
// trace, not a fifth band of the letter body.
|
|
30
|
+
const WORDMARK_OUTLINE = WORDMARK[4].replace(/█/g, '▔');
|
|
31
|
+
|
|
32
|
+
// Bevel banding: dim → mid → bright → mid → dim. Produces the chiseled
|
|
33
|
+
// pixel-art look of the skills.sh reference (substituting FBS oranges for
|
|
34
|
+
// the reference's grayscale palette).
|
|
35
|
+
const SHADES = [
|
|
36
|
+
PALETTE.accentShadow,
|
|
37
|
+
PALETTE.accent,
|
|
38
|
+
PALETTE.accentLight,
|
|
39
|
+
PALETTE.accent,
|
|
40
|
+
PALETTE.accentShadow,
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const WORDMARK_WIDTH = Math.max(...WORDMARK.map((row) => row.length));
|
|
44
|
+
|
|
45
|
+
export const SPLASH_COMMANDS = Object.freeze([
|
|
46
|
+
['$ npx @friedbotstudio/create-baseline <target>', 'Install the baseline'],
|
|
47
|
+
['$ npx @friedbotstudio/create-baseline upgrade', 'Three-way merge upgrade'],
|
|
48
|
+
['$ npx @friedbotstudio/create-baseline doctor', 'Drift report'],
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
// `process.stdout.columns` is 0 (not undefined) under `script(1)` and some
|
|
52
|
+
// CI ptys; treat any falsy value as "unknown, assume wide enough" so the
|
|
53
|
+
// marquee renders rather than silently degrading to the plain banner.
|
|
54
|
+
export function wordmarkFits(columns) {
|
|
55
|
+
const cols = columns ?? process.stdout.columns;
|
|
56
|
+
if (!cols) return true;
|
|
57
|
+
return cols >= WORDMARK_WIDTH;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function renderWordmark() {
|
|
61
|
+
const bands = WORDMARK.map((row, i) => paintRGB(SHADES[i], row));
|
|
62
|
+
bands.push(paintRGB(PALETTE.accentShadow, WORDMARK_OUTLINE));
|
|
63
|
+
return bands.join('\n');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Full marquee splash. Used by --help in TTY and the no-arg landing. The
|
|
67
|
+
// version is intentionally NOT rendered here — `--version` already surfaces
|
|
68
|
+
// it via renderVersionMarquee, and embedding it in the splash would force
|
|
69
|
+
// docs-site screenshots to re-render every release.
|
|
70
|
+
export function renderSplash({ tagline, tryLine, discoverUrl } = {}) {
|
|
71
|
+
const lines = [
|
|
72
|
+
'',
|
|
73
|
+
`${muted('▲')} ${muted('~/')} ${muted('npx @friedbotstudio/create-baseline@latest')}`,
|
|
74
|
+
'',
|
|
75
|
+
renderWordmark(),
|
|
76
|
+
'',
|
|
77
|
+
muted(tagline ?? 'The Claude Code baseline — hooks, skills, MCP, governance.'),
|
|
78
|
+
'',
|
|
79
|
+
];
|
|
80
|
+
for (const [cmd, desc] of SPLASH_COMMANDS) {
|
|
81
|
+
const left = ` ${cmd}`;
|
|
82
|
+
lines.push(`${left.padEnd(54)}${muted(desc)}`);
|
|
83
|
+
}
|
|
84
|
+
if (tryLine) {
|
|
85
|
+
lines.push('');
|
|
86
|
+
lines.push(`${muted('try:')} ${tryLine}`);
|
|
87
|
+
}
|
|
88
|
+
if (discoverUrl) {
|
|
89
|
+
lines.push('');
|
|
90
|
+
lines.push(`${muted('Discover more at')} ${discoverUrl}`);
|
|
91
|
+
}
|
|
92
|
+
lines.push('');
|
|
93
|
+
lines.push(`${muted('▲')} ${muted('~/')}`);
|
|
94
|
+
lines.push('');
|
|
95
|
+
return lines.join('\n');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Slim two-line brand strip. Used by install / upgrade intros, --version,
|
|
99
|
+
// and the top of the usage-error renderer. Cheap and width-safe (~32 cols).
|
|
100
|
+
export function renderBrandStrip({ version, subtitle } = {}) {
|
|
101
|
+
const left = `${accent('▲ BASELINE')}`;
|
|
102
|
+
const right = version ? ` ${muted(`v${version}`)}` : '';
|
|
103
|
+
const sub = subtitle ? `\n ${muted(subtitle)}` : '';
|
|
104
|
+
return ['', `${left}${right}${sub}`, ''].join('\n');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --version flourish: the wordmark + a version line. Wider than the strip;
|
|
108
|
+
// callers should fall back to renderBrandStrip when the terminal is narrow.
|
|
109
|
+
export function renderVersionMarquee(version) {
|
|
110
|
+
return ['', renderWordmark(), '', ` ${muted(`v${version}`)}`, ''].join('\n');
|
|
111
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Foundation — ANSI brand-color helpers for the TUI presentation layer.
|
|
2
|
+
// Translates Friedbot Studio's oklch tokens (site-src/assets/site.css :root) to
|
|
3
|
+
// 24-bit truecolor escape sequences. Respects NO_COLOR (https://no-color.org)
|
|
4
|
+
// and skips emission when stdout is not a TTY.
|
|
5
|
+
|
|
6
|
+
const NO_COLOR = process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== '';
|
|
7
|
+
|
|
8
|
+
// oklch -> approximate sRGB hex used in the rendered docs site:
|
|
9
|
+
// --accent-shadow oklch(35% 0.15 41.5) ~ #7a2907 (dark band of the wordmark bevel)
|
|
10
|
+
// --accent oklch(55.8% 0.187 41.5) ~ #c2410c (orange-700)
|
|
11
|
+
// --accent-light oklch(70.3% 0.187 41.5) ~ #ea6a25 (orange-500)
|
|
12
|
+
// --muted oklch(45% 0.026 257) ~ #6b7280
|
|
13
|
+
// --cli-success oklch(70% 0.15 145) ~ #4ade80
|
|
14
|
+
// --warn oklch(58% 0.13 60) ~ #d97706
|
|
15
|
+
// --mac-red oklch(70% 0.21 24) ~ #ef4444
|
|
16
|
+
// --rule oklch(89% 0.013 257) ~ #d1d5db
|
|
17
|
+
const RGB = {
|
|
18
|
+
accentShadow: [122, 41, 7],
|
|
19
|
+
accent: [194, 65, 12],
|
|
20
|
+
accentLight: [234, 106, 37],
|
|
21
|
+
muted: [107, 114, 128],
|
|
22
|
+
success: [74, 222, 128],
|
|
23
|
+
warn: [217, 119, 6],
|
|
24
|
+
error: [239, 68, 68],
|
|
25
|
+
rule: [209, 213, 219],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Exposed for the splash wordmark, which paints individual rows in their own
|
|
29
|
+
// shade. All other UI tokens funnel through the named helpers below.
|
|
30
|
+
export function paintRGB(rgb, text) {
|
|
31
|
+
if (NO_COLOR || !process.stdout.isTTY) return text;
|
|
32
|
+
const [r, g, b] = rgb;
|
|
33
|
+
return `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const PALETTE = Object.freeze(RGB);
|
|
37
|
+
|
|
38
|
+
export const accentShadow = (text) => paintRGB(RGB.accentShadow, text);
|
|
39
|
+
export const accent = (text) => paintRGB(RGB.accent, text);
|
|
40
|
+
export const accentLight = (text) => paintRGB(RGB.accentLight, text);
|
|
41
|
+
export const muted = (text) => paintRGB(RGB.muted, text);
|
|
42
|
+
export const success = (text) => paintRGB(RGB.success, text);
|
|
43
|
+
export const warn = (text) => paintRGB(RGB.warn, text);
|
|
44
|
+
export const error = (text) => paintRGB(RGB.error, text);
|
|
45
|
+
export const rule = (text) => paintRGB(RGB.rule, text);
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Domain — branded upgrade flow with interactive per-file conflict resolution.
|
|
2
|
+
// Plan/apply split:
|
|
3
|
+
// 1. dry-run threeWayMerge → enumerate SKIP_CUSTOMIZED conflicts
|
|
4
|
+
// 2. prompt the user once per conflict
|
|
5
|
+
// 3. on cancel/abort: bail before any write
|
|
6
|
+
// 4. on resolve: real threeWayMerge with onSkipCustomized backed by the Map
|
|
7
|
+
|
|
8
|
+
import * as clackModule from '@clack/prompts';
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
11
|
+
import { join, relative, sep } from 'node:path';
|
|
12
|
+
import { threeWayMerge, ACTION_KINDS } from '../merge.js';
|
|
13
|
+
import { loadManifest, buildManifestFromDir } from '../manifest.js';
|
|
14
|
+
import { COPY_EXCLUDE } from '../install.js';
|
|
15
|
+
import { renderBrandStrip } from './splash.js';
|
|
16
|
+
|
|
17
|
+
const SUCCESS = 0;
|
|
18
|
+
const ERR_ABORT = 1;
|
|
19
|
+
const ERR_NO_MANIFEST = 2;
|
|
20
|
+
const ERR_DIVERGENCE = 3;
|
|
21
|
+
|
|
22
|
+
const CHOICE_OPTIONS = [
|
|
23
|
+
{ value: 'keep-mine', label: 'Keep mine', hint: 'preserve target file as-is' },
|
|
24
|
+
{ value: 'take-theirs', label: 'Take theirs', hint: 'overwrite with new baseline' },
|
|
25
|
+
{ value: 'abort', label: 'Abort', hint: 'exit without changes' },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export async function run({ target, opts = {}, prompts = clackModule } = {}) {
|
|
29
|
+
if (!target || typeof target !== 'string') {
|
|
30
|
+
throw new Error('tui.upgrade.run requires a non-empty string target');
|
|
31
|
+
}
|
|
32
|
+
if (!opts.templateDir) {
|
|
33
|
+
throw new Error('tui.upgrade.run requires opts.templateDir');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const manifestPath = join(target, '.claude/.baseline-manifest.json');
|
|
37
|
+
if (!existsSync(manifestPath)) {
|
|
38
|
+
prompts.log.error(`No baseline manifest at ${manifestPath}. Run a fresh install first.`);
|
|
39
|
+
return ERR_NO_MANIFEST;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const version = await readPackageVersion();
|
|
43
|
+
process.stdout.write(renderBrandStrip({ version, subtitle: 'upgrade' }));
|
|
44
|
+
prompts.intro('create-baseline upgrade');
|
|
45
|
+
|
|
46
|
+
const { oldManifest, newManifest } = await loadManifests(opts.templateDir, manifestPath);
|
|
47
|
+
const dryReport = await threeWayMerge(opts.templateDir, target, oldManifest, newManifest, { dryRun: true });
|
|
48
|
+
const conflicts = dryReport.actions.filter((a) => a.kind === ACTION_KINDS.SKIP_CUSTOMIZED);
|
|
49
|
+
|
|
50
|
+
const choices = new Map();
|
|
51
|
+
for (const conflict of conflicts) {
|
|
52
|
+
const choice = await prompts.select({
|
|
53
|
+
message: `${conflict.path} has been customized — choose:`,
|
|
54
|
+
options: CHOICE_OPTIONS,
|
|
55
|
+
});
|
|
56
|
+
if (prompts.isCancel(choice) || choice === 'abort') {
|
|
57
|
+
prompts.cancel('Upgrade aborted; tree unchanged.');
|
|
58
|
+
return ERR_ABORT;
|
|
59
|
+
}
|
|
60
|
+
choices.set(conflict.path, choice);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (opts.dryRun) {
|
|
64
|
+
for (const action of dryReport.actions) {
|
|
65
|
+
prompts.log.info(`${action.kind.padEnd(24)} ${action.path}`);
|
|
66
|
+
}
|
|
67
|
+
prompts.outro('Dry run complete; no changes written.');
|
|
68
|
+
return SUCCESS;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const onSkipCustomized = (rel) => choices.get(rel) ?? 'keep-mine';
|
|
72
|
+
const finalReport = await threeWayMerge(opts.templateDir, target, oldManifest, newManifest, { onSkipCustomized });
|
|
73
|
+
|
|
74
|
+
const applied = finalReport.actions.filter((a) => isApplied(a.kind)).length;
|
|
75
|
+
const skipped = finalReport.actions.filter((a) => a.kind === ACTION_KINDS.SKIP_CUSTOMIZED).length;
|
|
76
|
+
prompts.outro(`Applied ${applied}; ${skipped} skipped.`);
|
|
77
|
+
return finalReport.exitCode === 3 ? ERR_DIVERGENCE : SUCCESS;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isApplied(kind) {
|
|
81
|
+
return (
|
|
82
|
+
kind === ACTION_KINDS.ADD ||
|
|
83
|
+
kind === ACTION_KINDS.OVERWRITE ||
|
|
84
|
+
kind === ACTION_KINDS.PRUNE ||
|
|
85
|
+
kind === ACTION_KINDS.SPECIAL_MERGE ||
|
|
86
|
+
kind === ACTION_KINDS.NEVER_TOUCH_ADD
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function loadManifests(templateDir, manifestPath) {
|
|
91
|
+
const oldManifest = await loadManifest(manifestPath);
|
|
92
|
+
const tplFiles = await listShippedFiles(templateDir);
|
|
93
|
+
const newManifest = await buildManifestFromDir(templateDir, tplFiles);
|
|
94
|
+
return { oldManifest, newManifest };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function readPackageVersion() {
|
|
98
|
+
try {
|
|
99
|
+
const url = new URL('../../../package.json', import.meta.url);
|
|
100
|
+
const pkg = JSON.parse(await readFile(url, 'utf8'));
|
|
101
|
+
return pkg.version || '0.0.0';
|
|
102
|
+
} catch {
|
|
103
|
+
return '0.0.0';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function listShippedFiles(root, base = root, acc = []) {
|
|
108
|
+
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
109
|
+
const full = join(root, entry.name);
|
|
110
|
+
if (entry.isDirectory()) await listShippedFiles(full, base, acc);
|
|
111
|
+
else if (entry.isFile()) acc.push(relative(base, full).split(sep).join('/'));
|
|
112
|
+
}
|
|
113
|
+
// COPY_EXCLUDE (single source of truth in install.js) now lists no paths —
|
|
114
|
+
// the shipped manifest moved into `.claude/manifest.json` so the recursive
|
|
115
|
+
// walk picks it up at the same path the consumer expects. The filter stays
|
|
116
|
+
// for forward-compat; if a future path needs to be kept out of the merge,
|
|
117
|
+
// add it to install.js → COPY_EXCLUDE in one place.
|
|
118
|
+
return acc.filter((p) => !COPY_EXCLUDE.includes(p));
|
|
119
|
+
}
|
package/src/seed.template.md
CHANGED
|
@@ -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-
|
|
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/ #
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|