@friedbotstudio/create-baseline 0.2.1 → 0.4.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 (87) hide show
  1. package/README.md +17 -7
  2. package/bin/cli.js +197 -119
  3. package/obj/template/.claude/commands/grant-push.md +19 -0
  4. package/obj/template/.claude/commands/init-project.md +26 -4
  5. package/obj/template/.claude/hooks/consent_gate_grant.mjs +107 -0
  6. package/obj/template/.claude/hooks/git_commit_guard.mjs +224 -0
  7. package/obj/template/.claude/hooks/harness_continuation.sh +101 -34
  8. package/obj/template/.claude/hooks/lib/common.mjs +283 -0
  9. package/obj/template/.claude/hooks/lib/common.sh +1 -1
  10. package/obj/template/.claude/hooks/memory_session_start.sh +20 -6
  11. package/obj/template/.claude/hooks/memory_stop.sh +161 -2
  12. package/obj/template/.claude/hooks/spec_approval_guard.sh +1 -1
  13. package/obj/template/.claude/hooks/swarm_approval_guard.sh +1 -1
  14. package/obj/template/.claude/hooks/tests/fixtures/ac008_byte_equal_reference.txt +7 -7
  15. package/obj/template/.claude/hooks/tests/fixtures/memory_stop_landmark_baseline.txt +21 -0
  16. package/obj/template/.claude/hooks/tests/fixtures/regenerate-ac008.sh +47 -0
  17. package/obj/template/.claude/hooks/tests/memory_session_start_test.sh +7 -3
  18. package/obj/template/.claude/hooks/tests/memory_stop_intent_test.sh +329 -0
  19. package/obj/template/.claude/hooks/tests/regenerate_ac008_test.sh +99 -0
  20. package/obj/template/.claude/memory/README.md +8 -3
  21. package/obj/template/.claude/memory/backlog.md +12 -0
  22. package/obj/template/.claude/project.json +6 -1
  23. package/obj/template/.claude/settings.json +3 -4
  24. package/obj/template/.claude/skills/audit-baseline/audit.sh +28 -16
  25. package/obj/template/.claude/skills/audit-baseline/tests/fixtures/_pending_opener_only.md +3 -0
  26. package/obj/template/.claude/skills/audit-baseline/tests/fixtures/preamble_full_empty_body.md +4 -0
  27. package/obj/template/.claude/skills/audit-baseline/tests/fixtures/preamble_full_with_entries.md +9 -0
  28. package/obj/template/.claude/skills/audit-baseline/tests/fixtures/preamble_no_opener.md +3 -0
  29. package/obj/template/.claude/skills/audit-baseline/tests/fixtures/preamble_opener_only.md +3 -0
  30. package/obj/template/.claude/skills/audit-baseline/tests/preamble_check_test.sh +147 -0
  31. package/obj/template/.claude/skills/changelog/SKILL.md +69 -0
  32. package/obj/template/.claude/skills/changelog/changelog.mjs +163 -0
  33. package/obj/template/.claude/skills/changelog/classifier.mjs +49 -0
  34. package/obj/template/.claude/skills/changelog/state-writer.mjs +19 -0
  35. package/obj/template/.claude/skills/changelog/tests/consent-expired_test.sh +126 -0
  36. package/obj/template/.claude/skills/changelog/tests/golden-path_test.sh +191 -0
  37. package/obj/template/.claude/skills/changelog/tests/idempotent-reentry_test.sh +121 -0
  38. package/obj/template/.claude/skills/changelog/tests/keepachangelog-unreleased-preserved_test.mjs +149 -0
  39. package/obj/template/.claude/skills/changelog/tests/non-git-shortcircuit_test.sh +98 -0
  40. package/obj/template/.claude/skills/changelog/tests/preview-only_test.sh +96 -0
  41. package/obj/template/.claude/skills/changelog/tests/run.sh +28 -0
  42. package/obj/template/.claude/skills/changelog/unreleased-writer.mjs +155 -0
  43. package/obj/template/.claude/skills/changelog/version-preview.mjs +124 -0
  44. package/obj/template/.claude/skills/chore/SKILL.md +5 -3
  45. package/obj/template/.claude/skills/commit/SKILL.md +5 -4
  46. package/obj/template/.claude/skills/copywriting/LICENSE +21 -0
  47. package/obj/template/.claude/skills/copywriting/NOTICE +23 -0
  48. package/obj/template/.claude/skills/copywriting/SKILL.md +1 -1
  49. package/obj/template/.claude/skills/design-ui/SKILL.md +23 -5
  50. package/obj/template/.claude/skills/design-ui/references/design-vs-development.md +26 -5
  51. package/obj/template/.claude/skills/design-ui/references/orchestration.md +1 -0
  52. package/obj/template/.claude/skills/design-ui/references/state-machine.md +5 -3
  53. package/obj/template/.claude/skills/documentation/LICENSE +202 -0
  54. package/obj/template/.claude/skills/documentation/NOTICE +22 -0
  55. package/obj/template/.claude/skills/harness/SKILL.md +5 -1
  56. package/obj/template/.claude/skills/humanizer/LICENSE +21 -0
  57. package/obj/template/.claude/skills/humanizer/NOTICE +21 -0
  58. package/obj/template/.claude/skills/impeccable/LICENSE +202 -0
  59. package/obj/template/.claude/skills/impeccable/NOTICE +24 -0
  60. package/obj/template/.claude/skills/memory-flush/SKILL.md +20 -4
  61. package/obj/template/.claude/skills/memory-flush/sweep.py +74 -6
  62. package/obj/template/.claude/skills/memory-flush/tests/run.sh +300 -1
  63. package/obj/template/.claude/skills/tdd/SKILL.md +2 -1
  64. package/obj/template/.claude/skills/tdd/drift_check.py +180 -0
  65. package/obj/template/.claude/skills/tdd/tests/drift_check_test.sh +190 -0
  66. package/obj/template/.claude/skills/tdd/tests/run.sh +21 -0
  67. package/obj/template/.claude/skills/technical-tutorials/LICENSE +21 -0
  68. package/obj/template/.claude/skills/technical-tutorials/NOTICE +23 -0
  69. package/obj/template/.claude/skills/technical-tutorials/SKILL.md +1 -1
  70. package/obj/template/.claude/skills/triage/SKILL.md +11 -5
  71. package/obj/template/CLAUDE.md +36 -25
  72. package/obj/template/docs/init/seed.md +39 -24
  73. package/obj/template/manifest.json +73 -33
  74. package/package.json +5 -2
  75. package/src/CLAUDE.template.md +36 -25
  76. package/src/cli/merge.js +15 -10
  77. package/src/cli/tui/doctor.js +56 -0
  78. package/src/cli/tui/install.js +79 -0
  79. package/src/cli/tui/meta.js +30 -0
  80. package/src/cli/tui/tokens.js +38 -0
  81. package/src/cli/tui/upgrade.js +100 -0
  82. package/src/memory/backlog.template.md +12 -0
  83. package/src/project.template.json +6 -1
  84. package/src/seed.template.md +39 -24
  85. package/src/settings.template.json +3 -4
  86. package/obj/template/.claude/hooks/consent_gate_grant.sh +0 -89
  87. package/obj/template/.claude/hooks/git_commit_guard.sh +0 -93
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env bash
2
+ # Fixture-based integration test for AC-012: the changelog actuator's
3
+ # --preview-only mode prints projected next semver + draft fragment to stdout,
4
+ # requires no commit_consent gesture, and writes no files.
5
+ #
6
+ # Pre-implement RED: actuator does not exist; test fails on missing file.
7
+
8
+ set -uo pipefail
9
+
10
+ HERE="$(cd "$(dirname "$0")" && pwd)"
11
+ REPO_ROOT="$(cd "$HERE/../../../.." && pwd)"
12
+ ACTUATOR="$REPO_ROOT/.claude/skills/changelog/changelog.mjs"
13
+
14
+ PASS=0; FAIL=0; FAILED=()
15
+
16
+ fail() { echo " FAIL: $*"; return 1; }
17
+
18
+ # Build a tempdir project WITHOUT commit_consent (preview must work without it).
19
+ seed_preview_project() {
20
+ local proj="$1"
21
+ mkdir -p "$proj/.claude/state"
22
+ cd "$proj"
23
+ git init -q
24
+ git config user.email "test@example.com"
25
+ git config user.name "Test"
26
+ git commit --allow-empty -q -m "chore: initial"
27
+ git tag v0.1.0
28
+ echo "preview" > thing.txt
29
+ git add thing.txt
30
+ git commit -q -m "feat: preview path"
31
+ # NO commit_consent file.
32
+ # NO workflow.json — preview must not require one.
33
+ cat > "$proj/.releaserc.json" <<'EOF'
34
+ {
35
+ "branches": ["main"],
36
+ "plugins": [
37
+ ["@semantic-release/commit-analyzer", { "preset": "angular" }]
38
+ ]
39
+ }
40
+ EOF
41
+ }
42
+
43
+ run() {
44
+ local name="$1"
45
+ echo "RUN $name"
46
+ if "$name"; then
47
+ PASS=$((PASS+1)); echo "PASS $name"
48
+ else
49
+ FAIL=$((FAIL+1)); FAILED+=("$name"); echo "FAIL $name"
50
+ fi
51
+ }
52
+
53
+ # --- AC-012 -------------------------------------------------------------------
54
+
55
+ test_when_preview_only_flag_then_stdout_projection_no_writes() {
56
+ local proj; proj="$(mktemp -d)"; trap "rm -rf $proj" RETURN
57
+ seed_preview_project "$proj"
58
+ if [ ! -f "$ACTUATOR" ]; then
59
+ fail "AC-012 actuator not yet at $ACTUATOR — expected pre-implement RED state"
60
+ return 1
61
+ fi
62
+ local out
63
+ out="$(node "$ACTUATOR" --preview-only --slug demo --project-root "$proj" 2>/tmp/preview-stderr.$$)"
64
+ local ec=$?
65
+ if [ "$ec" -ne 0 ]; then
66
+ fail "AC-012 preview-only must exit 0; got $ec; stderr: $(cat /tmp/preview-stderr.$$)"
67
+ return 1
68
+ fi
69
+ # stdout matches Projected: <semver>.
70
+ if ! printf '%s' "$out" | grep -qE 'Projected:\s*[0-9]+\.[0-9]+\.[0-9]+'; then
71
+ fail "AC-012 stdout must contain Projected: <semver>; got: $out"
72
+ return 1
73
+ fi
74
+ # No state file written.
75
+ if [ -f "$proj/.claude/state/changelog/demo.json" ]; then
76
+ fail "AC-012 preview-only must NOT write state file"
77
+ return 1
78
+ fi
79
+ # CHANGELOG.md is either absent (we never created one) or unchanged.
80
+ if [ -f "$proj/CHANGELOG.md" ]; then
81
+ fail "AC-012 preview-only must NOT create CHANGELOG.md when absent"
82
+ return 1
83
+ fi
84
+ }
85
+
86
+ # --- runner -------------------------------------------------------------------
87
+
88
+ run test_when_preview_only_flag_then_stdout_projection_no_writes
89
+
90
+ echo "----"
91
+ echo "Passed: $PASS Failed: $FAIL"
92
+ if [ "$FAIL" -gt 0 ]; then
93
+ echo "Failed tests:"
94
+ for t in "${FAILED[@]}"; do echo " - $t"; done
95
+ fi
96
+ exit $((FAIL > 0))
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env bash
2
+ # Aggregate test runner for .claude/skills/changelog/.
3
+ # Invokes each sibling *_test.sh AND any *_test.mjs (node --test) and exits
4
+ # non-zero if any fail.
5
+
6
+ set -uo pipefail
7
+
8
+ HERE="$(cd "$(dirname "$0")" && pwd)"
9
+ FAIL=0
10
+
11
+ for t in "$HERE"/*_test.sh; do
12
+ [ -f "$t" ] || continue
13
+ echo "=== $(basename "$t") ==="
14
+ bash "$t" || FAIL=$((FAIL+1))
15
+ done
16
+
17
+ for t in "$HERE"/*_test.mjs; do
18
+ [ -f "$t" ] || continue
19
+ echo "=== $(basename "$t") ==="
20
+ node --test "$t" || FAIL=$((FAIL+1))
21
+ done
22
+
23
+ if [ "$FAIL" -gt 0 ]; then
24
+ echo "changelog/tests: $FAIL suite(s) failed"
25
+ exit 1
26
+ fi
27
+ echo "changelog/tests: all suites passed"
28
+ exit 0
@@ -0,0 +1,155 @@
1
+ // CHANGELOG.md curation under ## [Unreleased].
2
+ //
3
+ // Two exports:
4
+ // appendUnderUnreleased(changelogPath, entries) — per-commit RMW.
5
+ // reinsertUnreleasedHeading(changelogPath) — AC-013 fallback for the
6
+ // case where @semantic-release/changelog has prepended versioned notes
7
+ // above the `# Changelog` and `## [Unreleased]` headings. Restores the
8
+ // canonical order: `# Changelog\n\n## [Unreleased]\n\n<rest>`.
9
+ //
10
+ // File shape we maintain (keepachangelog 1.0.0):
11
+ //
12
+ // # Changelog
13
+ //
14
+ // ## [Unreleased]
15
+ //
16
+ // ### Added
17
+ // - <entry>
18
+ //
19
+ // ### Fixed
20
+ // - <entry>
21
+ //
22
+ // ## [0.1.0] - 2026-01-01
23
+ //
24
+ // ...
25
+
26
+ import { readFile, writeFile } from 'node:fs/promises';
27
+ import { existsSync } from 'node:fs';
28
+
29
+ const UNRELEASED_HEADING = '## [Unreleased]';
30
+ const CHANGELOG_HEADING = '# Changelog';
31
+
32
+ const CATEGORY_ORDER = ['Added', 'Changed', 'Deprecated', 'Removed', 'Fixed', 'Security'];
33
+
34
+ function groupBySection(entries) {
35
+ const map = new Map();
36
+ for (const entry of entries) {
37
+ if (!map.has(entry.section)) map.set(entry.section, []);
38
+ map.get(entry.section).push(entry);
39
+ }
40
+ return map;
41
+ }
42
+
43
+ function renderUnreleasedBody(entries) {
44
+ if (entries.length === 0) return '';
45
+ const grouped = groupBySection(entries);
46
+ const lines = [];
47
+ for (const section of CATEGORY_ORDER) {
48
+ const items = grouped.get(section);
49
+ if (!items || items.length === 0) continue;
50
+ lines.push('');
51
+ lines.push(`### ${section}`);
52
+ lines.push('');
53
+ for (const item of items) {
54
+ const prefix = item.breaking ? '**BREAKING:** ' : '';
55
+ lines.push(`- ${prefix}${item.body}`);
56
+ }
57
+ }
58
+ return lines.join('\n');
59
+ }
60
+
61
+ // Split text into { preamble, unreleasedBody, rest } where preamble is
62
+ // everything up to and including the `## [Unreleased]` line, unreleasedBody
63
+ // is the content between that and the next `##` heading, and rest is from
64
+ // the next `##` heading onward.
65
+ function splitAroundUnreleased(text) {
66
+ const unreleasedIdx = text.indexOf(UNRELEASED_HEADING);
67
+ if (unreleasedIdx < 0) return null;
68
+ const afterHeading = text.indexOf('\n', unreleasedIdx);
69
+ const headingEnd = afterHeading < 0 ? text.length : afterHeading + 1;
70
+ // Find the next `## ` heading after the Unreleased heading.
71
+ const restMatch = text.slice(headingEnd).match(/\n## [^\n]+\n/);
72
+ const restOffset = restMatch ? headingEnd + restMatch.index + 1 : text.length;
73
+ return {
74
+ preamble: text.slice(0, headingEnd),
75
+ unreleasedBody: text.slice(headingEnd, restOffset),
76
+ rest: text.slice(restOffset),
77
+ };
78
+ }
79
+
80
+ function defaultChangelogText() {
81
+ return `# Changelog\n\n## [Unreleased]\n\n`;
82
+ }
83
+
84
+ export async function appendUnderUnreleased(changelogPath, entries) {
85
+ let text;
86
+ if (existsSync(changelogPath)) {
87
+ text = await readFile(changelogPath, 'utf8');
88
+ } else {
89
+ text = defaultChangelogText();
90
+ }
91
+ if (!text.includes(CHANGELOG_HEADING)) {
92
+ text = `${CHANGELOG_HEADING}\n\n${text}`;
93
+ }
94
+ if (!text.includes(UNRELEASED_HEADING)) {
95
+ text = text.replace(
96
+ new RegExp(`(${CHANGELOG_HEADING}\\n)`, ''),
97
+ `$1\n${UNRELEASED_HEADING}\n\n`,
98
+ );
99
+ }
100
+ const parts = splitAroundUnreleased(text);
101
+ if (!parts) {
102
+ // Defensive: should be unreachable after the insertions above.
103
+ throw new Error(`could not locate ${UNRELEASED_HEADING} in ${changelogPath}`);
104
+ }
105
+ const body = renderUnreleasedBody(entries);
106
+ const merged = `${parts.preamble}${body}\n${parts.rest.startsWith('\n') ? parts.rest : '\n' + parts.rest}`;
107
+ await writeFile(changelogPath, merged, 'utf8');
108
+ }
109
+
110
+ export async function reinsertUnreleasedHeading(changelogPath) {
111
+ const text = await readFile(changelogPath, 'utf8');
112
+ // If the first `##` heading in the file is already `## [Unreleased]`, the
113
+ // file is structurally correct; do nothing.
114
+ const firstH2 = text.match(/^## .+$/m);
115
+ if (firstH2 && firstH2[0].includes('[Unreleased]')) {
116
+ return;
117
+ }
118
+ // Otherwise: find the existing Unreleased heading (which may sit deeper in
119
+ // the file because @semantic-release/changelog prepended notes above it)
120
+ // and lift it to the canonical top position.
121
+ const lines = text.split('\n');
122
+ // Identify the Unreleased section's start and the next `##` start.
123
+ let unreleasedStart = -1;
124
+ let unreleasedEnd = -1;
125
+ for (let i = 0; i < lines.length; i++) {
126
+ if (unreleasedStart < 0 && lines[i] === UNRELEASED_HEADING) {
127
+ unreleasedStart = i;
128
+ continue;
129
+ }
130
+ if (unreleasedStart >= 0 && /^## /.test(lines[i])) {
131
+ unreleasedEnd = i;
132
+ break;
133
+ }
134
+ }
135
+ if (unreleasedStart < 0) {
136
+ // No Unreleased heading anywhere; insert a fresh one at the top.
137
+ const top = `${CHANGELOG_HEADING}\n\n${UNRELEASED_HEADING}\n\n`;
138
+ await writeFile(changelogPath, top + text, 'utf8');
139
+ return;
140
+ }
141
+ if (unreleasedEnd < 0) unreleasedEnd = lines.length;
142
+ const unreleasedBlock = lines.slice(unreleasedStart, unreleasedEnd);
143
+ const without = lines.slice(0, unreleasedStart).concat(lines.slice(unreleasedEnd));
144
+ // Strip any leading blank lines from the without-block so the result starts
145
+ // with the `# Changelog` heading (we'll insert one if absent).
146
+ const withoutText = without.join('\n');
147
+ const head = withoutText.startsWith(CHANGELOG_HEADING)
148
+ ? withoutText
149
+ : `${CHANGELOG_HEADING}\n\n${withoutText.replace(/^\n+/, '')}`;
150
+ const restored = head.replace(
151
+ new RegExp(`^(${CHANGELOG_HEADING})\\n\\n?`),
152
+ `$1\n\n${unreleasedBlock.join('\n')}\n\n`,
153
+ );
154
+ await writeFile(changelogPath, restored, 'utf8');
155
+ }
@@ -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
+ }
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: chore
3
3
  owner: baseline
4
- description: Workflow track for tasks that need no TDD — documentation edits, governance count bumps, vendored-skill content updates, configuration tweaks, formatting, typo fixes, dependency bumps where no project code changes. Skips `/scenario` and `/implement` (no failing test to drive) and runs the work directly. `verify`, `archive`, `/grant-commit`, and `/commit` remain mandatory. `simplify`, `integrate`, and `document` are conditional — required when the diff hits one of the listed triggers, optional otherwise. Chore is a stripped-down pipeline, not a bypass; never silently skip a conditional phase whose triggers apply.
4
+ description: Workflow track for tasks that need no TDD — documentation edits, governance count bumps, vendored-skill content updates, configuration tweaks, formatting, typo fixes, dependency bumps where no project code changes. Skips `/scenario` and `/implement` (no failing test to drive) and runs the work directly. `verify`, `archive`, `memory-flush`, `/grant-commit`, and `/commit` remain mandatory. `simplify`, `integrate`, and `document` are conditional — required when the diff hits one of the listed triggers, optional otherwise. Chore is a stripped-down pipeline, not a bypass; never silently skip a conditional phase whose triggers apply.
5
5
  argument-hint: "<one-line description of the chore>"
6
6
  ---
7
7
 
@@ -44,7 +44,8 @@ The classification rule is: *if there is no failing test that should exist for t
44
44
  1. **Edit** — apply the change directly. No `/scenario`, no `/implement` — there is no failing test to drive.
45
45
  2. **`verify`** — run the project test command and stamp `.claude/state/last_test_result`. FAIL means stop, surface, and route the user to `/triage` for a proper bugfix track. The verdict is binding (the `verify_pass_guard` hook reads this file).
46
46
  3. **`archive`** — empty bundle is fine; `/commit`'s prereq requires `archive` in `completed`.
47
- 4. **`/grant-commit` then `/commit`** — user-required consent + commit. Same as every other workflow.
47
+ 4. **`memory-flush`** — Phase 10.6. Empty pending is fine (fast-path runs Step 0 sweeps and short-circuits). `/commit`'s prereq requires `memory-flush` in `completed`.
48
+ 5. **`/grant-commit` then `/commit`** — user-required consent + commit. Same as every other workflow.
48
49
 
49
50
  ### Conditional phases (required when triggers apply, optional otherwise)
50
51
 
@@ -81,7 +82,8 @@ If a conditional phase is required, run it **before** `/grant-commit`. If you sk
81
82
  - **Required** → invoke the phase skill and append it to `workflow.json → completed`.
82
83
  - **Skipped** → record the rationale in your end-of-chore summary; do not append to `completed`.
83
84
  6. Invoke `Skill(archive)` — mandatory.
84
- 7. Append `"chore"`, `"archive"`, and any conditional phases that ran to `workflow.json completed`. Update `updated_at` to the current epoch.
85
+ 6.5. Invoke `Skill(memory-flush)` mandatory (Phase 10.6). Runs Step 0 canonical sweeps and, if `_pending.md` is non-empty, full triage. On empty pending the fast-path returns success in ≤ 3 sweep.py invocations.
86
+ 7. Append `"chore"`, `"archive"`, `"memory-flush"`, and any conditional phases that ran to `workflow.json → completed`. Update `updated_at` to the current epoch.
85
87
  8. **Marker op FIRST, then write `harness_state`, then emit end-of-chore summary.** On `state: "continue"` (more phases follow, e.g. archive is still pending): `echo "<slug>" > .claude/state/.harness_active` to refresh the active marker, then write `.claude/state/harness_state` with `{state: "continue", slug, reason}`. On `state: "done"` (archive just appended and no further phases remain): `rm -f .claude/state/.harness_active`, then write `harness_state` with `{state: "done", slug, reason}`. The state file carries exactly three keys; no `written_at`, no `tick_count`. Then tell the user:
86
88
  - "Chore green."
87
89
  - Files changed.
@@ -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: `archive` in `completed` (i.e., Phase 10.5 has moved all slug artifacts to `docs/archive/<date>/<slug>/`) AND a valid consent token at `.claude/state/commit_consent` (the Git Commit Guard hook enforces this independently).
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
 
@@ -13,9 +13,10 @@ Steps:
13
13
 
14
14
  0. **Git-repo precheck.** Run `git rev-parse --is-inside-work-tree 2>/dev/null`. If exit non-zero, exit cleanly with: "Not a git repository — `/commit` is inapplicable. Per CLAUDE.md Article IV, `commit` is auto-excepted on non-git projects; the workflow ended at `/archive`. Persistence outside git is your responsibility." Do not run any subsequent step.
15
15
  1. **Archive `workflow.json` itself.** This is the final piece of the archival bundle, held back until now so phase-ordering checks worked up through this point. Read `.claude/state/workflow.json` to get the slug, then move the file into the already-existing archive bundle: `docs/archive/<date>/<slug>/workflow.json`. Use the bundle's `<date>` directory (the one `/archive` created — inspect `docs/archive/` to find the most recent bundle matching the slug).
16
- 2. Verify workflow prereq: `archive` is the final non-commit entry in `completed`; no open consent gates remain.
16
+ 2. Verify workflow prereq: memory-flush is the final non-commit entry in `completed`; `archive` is the entry immediately before it; no open consent gates remain.
17
17
  3. `git status` + `git diff --stat` to confirm the change set. The diff now includes: production code changes + archive bundle additions + the workflow.json move. Stage named paths explicitly (never `git add -A` / `git add .` — seed.md forbids it).
18
18
  4. Draft the commit message from the spec + diff. Conventional-style prefix (`feat:` / `fix:` / `refactor:` / `docs:` / `test:`) followed by a 1-line summary and a short body explaining the WHY. The subject line is a fixed-register one-liner — leave it alone. The body is reviewer-facing prose — pass it through `Skill(humanizer)` before step 5 so AI-writing tells (em-dash overuse, rule of three, inflated verbs, vague attributions) get scrubbed. Keep the brief tight: tell humanizer the register is "factual reviewer-facing commit body — describe the diff faithfully, do not invent rationale, preserve any spec quotes verbatim".
19
19
  5. Run `git commit` with the message via HEREDOC. The Git Commit Guard hook will verify consent. If consent is missing/expired, stop and ask the user to run `/grant-commit`.
20
- 6. Do NOT run `git push`, `git commit --amend`, or pass `--no-verify`/`--no-gpg-sign` unless the user explicitly named the operation in their current request.
21
- 7. Append `"commit"` to `completed` but note this only matters for logs; the workflow.json is now in the archive and the live `.claude/state/workflow.json` no longer exists. Report the commit SHA to the user.
20
+ 6. **Stamp source backlog entries (post-commit, only when populated).** Read `workflow.json source_backlog_keys` BEFORE Step 1 archived the file (or read the archived copy at `docs/archive/<date>/<slug>/workflow.json`). If the array is absent OR empty, skip this step entirely. Otherwise invoke `python3 .claude/skills/memory-flush/sweep.py --mode stamp-closure --memory-dir .claude/memory --backlog-keys <comma-separated keys>`. Parse the JSON report and report `stamped`/`missing`/`already_closed` counts in the terminal message alongside the commit SHA. `sweep.py` is the only writer to `backlog.md`; `commit/SKILL.md` SHALL NOT edit canonical memory files directly. If the stamp invocation fails (filesystem error), surface it but do NOT roll back the commit — the entries can be stamped manually or by the next workflow's `/memory-flush`.
21
+ 7. Do NOT run `git push`, `git commit --amend`, or pass `--no-verify`/`--no-gpg-sign` unless the user explicitly named the operation in their current request.
22
+ 8. Append `"commit"` to `completed` — but note this only matters for logs; the workflow.json is now in the archive and the live `.claude/state/workflow.json` no longer exists. Report the commit SHA to the user.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) coreyhaines31 (https://github.com/coreyhaines31/marketingskills)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,23 @@
1
+ copywriting
2
+ ===========
3
+
4
+ This skill is vendored from the `copywriting` skill in the `marketingskills`
5
+ collection published by coreyhaines31 and is redistributed here under the
6
+ terms of the MIT License (see LICENSE in this directory).
7
+
8
+ Upstream:
9
+ Repository: https://github.com/coreyhaines31/marketingskills
10
+ Author: coreyhaines31
11
+ Skill: copywriting/
12
+
13
+ Vendored into: .claude/skills/copywriting/
14
+ Vendored on: 2026-05-15 (this commit) — prior install date unknown; LICENSE
15
+ + NOTICE added retroactively as part of the licensing-attribution
16
+ drift fix in the branch-aware-git-policy workflow.
17
+
18
+ Local changes:
19
+ - References to five companion skills (`copy-editing`, `email-sequence`,
20
+ `popup-cro`, etc.) that do not ship in this baseline are flagged in the
21
+ SKILL.md body with explicit "do not ship with this baseline" notes.
22
+ - Frontmatter `owner: baseline` added so the audit-baseline drift check
23
+ tracks this file as a shipped baseline artifact.
@@ -243,7 +243,7 @@ For headlines and CTAs, provide 2-3 options:
243
243
 
244
244
  ## Related Skills
245
245
 
246
- These five are upstream companion skills (from the `claude-code-setup` plugin family) and **do not ship with this baseline**. Listed for reference only — for the capabilities they describe, ask the user inline or install the plugin separately.
246
+ These five are upstream companion skills from the same source repo as `copywriting` ([`coreyhaines31/marketingskills`](https://github.com/coreyhaines31/marketingskills)) and **do not ship with this baseline**. Listed for reference only — for the capabilities they describe, ask the user inline or vendor them separately following the same MIT terms.
247
247
 
248
248
  - **copy-editing**: For polishing existing copy (use after your draft)
249
249
  - **page-cro**: If page structure/strategy needs work, not just copy
@@ -17,7 +17,7 @@ These are not preferences. They are structural commitments locked by spec `docs/
17
17
  - **You ALWAYS invoke impeccable.** Every design move goes through `Skill(impeccable, …)`. No exceptions, no shortcuts.
18
18
  - **You NEVER pick aesthetic direction.** Register, palette, type scale, motion vocabulary — all decided inside impeccable's subcommands in main context. You decide *which* subcommand to invoke, not *what* design to produce.
19
19
  - **You NEVER write product code.** Files under `app/`, `site-src/`, `components/`, `src/` — all flow through impeccable's writing subcommands (`craft`, `polish`, refines, enhances, fixes). You write only thin glue: state JSON, brief snapshots, audit snapshots.
20
- - **You ALWAYS classify before acting.** A misrouted `task_brief` (development or copy concern) returns immediately with `final_state: "not_a_design_task"` and a pointer to the correct lane. Design tasks proceed; everything else stops at Stage 0.
20
+ - **You ALWAYS classify before acting.** A misrouted `task_brief` returns at Stage 0 with one of two terminal states: **`not_a_design_task`** (when all surfaces classify as a single non-design lane — pure development or pure copy) with a `correct_lane` pointer, OR **`mixed_brief`** (when target_files span ≥ 2 lanes per the per-concern rule in `references/design-vs-development.md`) with a structured `lane_split` array. Design tasks proceed; everything else stops at Stage 0 without invoking impeccable or writing product code.
21
21
 
22
22
  ## Mandatory first step
23
23
 
@@ -47,7 +47,9 @@ Decide which lane this `task_brief` belongs to. The classification rule lives in
47
47
 
48
48
  Stage 0 evaluates two signals: (1) the intent string against the [`references/intent-table.md`](references/intent-table.md) rows, and (2) the `target_files` extensions as a tie-breaker.
49
49
 
50
- If the classification is anything other than **design**, return immediately:
50
+ If the classification is anything other than **design**, return immediately with one of two misroute terminals.
51
+
52
+ **Single-lane misroute** (all surfaces classify as pure development OR pure copy):
51
53
 
52
54
  ```jsonc
53
55
  {
@@ -58,7 +60,22 @@ If the classification is anything other than **design**, return immediately:
58
60
  }
59
61
  ```
60
62
 
61
- design-ui still writes a checkpoint state file even on misroute — the orchestration history is traceable.
63
+ **Multi-lane misroute** (target_files span 2 lanes per the per-concern rule in `references/design-vs-development.md`):
64
+
65
+ ```jsonc
66
+ {
67
+ "final_state": "mixed_brief",
68
+ "lane_split": [
69
+ { "surface": "<path>", "lane": "design" | "development" | "copy", "reason": "<plain-language>" }
70
+ ],
71
+ "reason": "task_brief spans <N> lanes",
72
+ "state_file": ".claude/state/design/<slug>.json"
73
+ }
74
+ ```
75
+
76
+ When `target_files` span ≥ 2 lanes, Stage 0 returns `mixed_brief` with a `lane_split` array (one entry per surface) instead of `not_a_design_task`. The caller fans out per row; design-ui does NOT execute any lane in this case (no impeccable invocation, no product-code writes). SKILL.md is the canonical source for this contract; `references/design-vs-development.md` mirrors it.
77
+
78
+ design-ui still writes a checkpoint state file even on misroute — the orchestration history is traceable. See `references/state-machine.md` for the sticky-resume rule that applies to both misroute terminals.
62
79
 
63
80
  ### Stage 1 — Capture
64
81
 
@@ -117,12 +134,13 @@ Return a structured `Report`:
117
134
  "slug": "<the slug>",
118
135
  "intent": "<the intent>",
119
136
  "recipe_executed": ["shape", "craft", "audit", "polish"],
120
- "final_state": "complete" | "needs_human" | "blocked" | "not_a_design_task",
137
+ "final_state": "complete" | "needs_human" | "blocked" | "not_a_design_task" | "mixed_brief",
121
138
  "files_touched": ["<path>", ...],
122
139
  "verifications": { "audit_score": "19/20", "p0": 0, "p1": 0 },
123
140
  "next_actions": ["<human-readable>"],
124
141
  "state_file": ".claude/state/design/<slug>.json",
125
- "thin_glue_written": ["docs/design/<slug>.brief.md", "docs/design/<slug>.audit.md"]
142
+ "thin_glue_written": ["docs/design/<slug>.brief.md", "docs/design/<slug>.audit.md"],
143
+ "lane_split": [ { "surface": "<path>", "lane": "design" | "development" | "copy", "reason": "<plain-language>" } ] // present only when final_state == "mixed_brief"
126
144
  }
127
145
  ```
128
146
 
@@ -21,7 +21,7 @@ When a `task_brief` arrives, Stage 0 evaluates **two signals** in order:
21
21
  - All paths match `tdd.ui_globs` AND no logic-file extensions → **design**.
22
22
  - All paths are logic files (`.ts`, `.js`, `.go`, `.py`, `.rs`, etc., excluding `.tsx` / `.jsx` / `.vue` / `.svelte`) → **development**.
23
23
  - All paths are `.md` / `.mdx` and the intent mentions "write", "rewrite", "improve", "draft" → **copy**.
24
- - Mixed → return to step 1 with the user surfaced; ask which concern they mean.
24
+ - Mixed → Stage 0 returns `final_state: "mixed_brief"` with a `lane_split` array (one entry per surface). See `SKILL.md` (canonical) for the return shape.
25
25
 
26
26
  ## Overlap is normal — same file, three lanes
27
27
 
@@ -67,7 +67,11 @@ Backend / data-fetching performance, query optimization, memoization for re-rend
67
67
 
68
68
  ## Misroute handling
69
69
 
70
- If Stage 0 classifies the intent as **development** or **copy** (not design), `design-ui` immediately returns:
70
+ *`SKILL.md` is the canonical source for Stage 0 misroute prose; this file mirrors it.*
71
+
72
+ Stage 0 has two misroute terminals.
73
+
74
+ **Single-lane misroute** — all surfaces classify as one non-design lane (pure development OR pure copy):
71
75
 
72
76
  ```jsonc
73
77
  {
@@ -78,12 +82,29 @@ If Stage 0 classifies the intent as **development** or **copy** (not design), `d
78
82
  }
79
83
  ```
80
84
 
81
- The caller (a workflow phase or the user) reads `correct_lane` and re-routes. `design-ui` never silently passes a non-design brief through to `impeccable` — that would muddy impeccable's contract.
85
+ The caller reads `correct_lane` and re-routes.
86
+
87
+ **Multi-lane misroute** — target_files span ≥ 2 lanes:
88
+
89
+ ```jsonc
90
+ {
91
+ "final_state": "mixed_brief",
92
+ "lane_split": [
93
+ { "surface": "<path>", "lane": "design" | "development" | "copy", "reason": "<plain-language>" }
94
+ ],
95
+ "reason": "task_brief spans <N> lanes",
96
+ "state_file": ".claude/state/design/<slug>.json"
97
+ }
98
+ ```
99
+
100
+ The caller reads `lane_split` and fans out per row; see `references/orchestration.md` caller-policy. `design-ui` never silently passes a non-design brief through to `impeccable` — that would muddy impeccable's contract. On a `mixed_brief`, `design-ui` invokes nothing and writes no product code: the structured `lane_split` is the entire response.
82
101
 
83
102
  ## When in doubt
84
103
 
85
- If the per-concern split is ambiguous *and* both signals (intent string + target_files) fail to resolve, surface to the user with a one-line question:
104
+ Multi-lane briefs (target_files span ≥ 2 lanes) route automatically to `mixed_brief` — Stage 0 returns the structured `lane_split` without asking the user. The interactive-ask path below is reserved for the rarer **single-lane ambiguity** case: the intent matches no row in `references/intent-table.md` AND `target_files` doesn't disambiguate (e.g., a single `.md` file but the intent reads like a design ask, not a copy ask).
105
+
106
+ In that case, surface to the user with a one-line question:
86
107
 
87
- > "This task seems to span design and development: <intent>. Which concern are you asking about? (a) design — surface, motion, visual a11y; (b) development — behavior, logic, data; (c) both, in sequence start with `/tdd` for behavior, then this skill for design."
108
+ > "This task is ambiguous within a single lane: <intent>. Which concern are you asking about? (a) design — surface, motion, visual a11y; (b) development — behavior, logic, data; (c) copyprose rewrite."
88
109
 
89
110
  Do not guess. The clean separation is what makes the lanes structural.
@@ -119,3 +119,4 @@ This is the same cap-3 shape as the polish loop, with `critique` as the gating s
119
119
  | `needs_human` | Warn and continue. design-ui has surfaced; the user can re-invoke later. /tdd Step 6 does NOT fail. The audit report path goes in /tdd's notes. |
120
120
  | `blocked` | Stop /tdd Step 6. Surface the blocker to the user. /tdd's `## 7. Decide on the result` step receives this and decides whether to escalate to a spec change. |
121
121
  | `not_a_design_task` | Stage 0 misroute. /tdd surfaces "design-ui returned not_a_design_task — was this design_call mis-classified in the spec?" and stops to reconcile. |
122
+ | `mixed_brief` | Stage 0 multi-lane misroute. Read `lane_split`. For each row: lane=design → re-invoke design-ui with a surface-scoped sub-brief; lane=development → record on `next_actions` and surface to the user; lane=copy → record on `next_actions` and surface to the user. Do NOT auto-invoke `/tdd` or `prose` in this tick — the spec author can split the `## Design calls` row deliberately. /tdd Step 6 surfaces a one-line summary and proceeds. |