@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.
- package/README.md +17 -7
- package/bin/cli.js +197 -119
- package/obj/template/.claude/commands/grant-push.md +19 -0
- package/obj/template/.claude/commands/init-project.md +26 -4
- package/obj/template/.claude/hooks/consent_gate_grant.mjs +107 -0
- package/obj/template/.claude/hooks/git_commit_guard.mjs +224 -0
- package/obj/template/.claude/hooks/harness_continuation.sh +101 -34
- package/obj/template/.claude/hooks/lib/common.mjs +283 -0
- package/obj/template/.claude/hooks/lib/common.sh +1 -1
- package/obj/template/.claude/hooks/memory_session_start.sh +20 -6
- package/obj/template/.claude/hooks/memory_stop.sh +161 -2
- package/obj/template/.claude/hooks/spec_approval_guard.sh +1 -1
- package/obj/template/.claude/hooks/swarm_approval_guard.sh +1 -1
- package/obj/template/.claude/hooks/tests/fixtures/ac008_byte_equal_reference.txt +7 -7
- package/obj/template/.claude/hooks/tests/fixtures/memory_stop_landmark_baseline.txt +21 -0
- package/obj/template/.claude/hooks/tests/fixtures/regenerate-ac008.sh +47 -0
- package/obj/template/.claude/hooks/tests/memory_session_start_test.sh +7 -3
- package/obj/template/.claude/hooks/tests/memory_stop_intent_test.sh +329 -0
- package/obj/template/.claude/hooks/tests/regenerate_ac008_test.sh +99 -0
- package/obj/template/.claude/memory/README.md +8 -3
- package/obj/template/.claude/memory/backlog.md +12 -0
- package/obj/template/.claude/project.json +6 -1
- package/obj/template/.claude/settings.json +3 -4
- package/obj/template/.claude/skills/audit-baseline/audit.sh +28 -16
- package/obj/template/.claude/skills/audit-baseline/tests/fixtures/_pending_opener_only.md +3 -0
- package/obj/template/.claude/skills/audit-baseline/tests/fixtures/preamble_full_empty_body.md +4 -0
- package/obj/template/.claude/skills/audit-baseline/tests/fixtures/preamble_full_with_entries.md +9 -0
- package/obj/template/.claude/skills/audit-baseline/tests/fixtures/preamble_no_opener.md +3 -0
- package/obj/template/.claude/skills/audit-baseline/tests/fixtures/preamble_opener_only.md +3 -0
- package/obj/template/.claude/skills/audit-baseline/tests/preamble_check_test.sh +147 -0
- 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/chore/SKILL.md +5 -3
- package/obj/template/.claude/skills/commit/SKILL.md +5 -4
- package/obj/template/.claude/skills/copywriting/LICENSE +21 -0
- package/obj/template/.claude/skills/copywriting/NOTICE +23 -0
- package/obj/template/.claude/skills/copywriting/SKILL.md +1 -1
- package/obj/template/.claude/skills/design-ui/SKILL.md +23 -5
- package/obj/template/.claude/skills/design-ui/references/design-vs-development.md +26 -5
- package/obj/template/.claude/skills/design-ui/references/orchestration.md +1 -0
- package/obj/template/.claude/skills/design-ui/references/state-machine.md +5 -3
- package/obj/template/.claude/skills/documentation/LICENSE +202 -0
- package/obj/template/.claude/skills/documentation/NOTICE +22 -0
- package/obj/template/.claude/skills/harness/SKILL.md +5 -1
- package/obj/template/.claude/skills/humanizer/LICENSE +21 -0
- package/obj/template/.claude/skills/humanizer/NOTICE +21 -0
- package/obj/template/.claude/skills/impeccable/LICENSE +202 -0
- package/obj/template/.claude/skills/impeccable/NOTICE +24 -0
- package/obj/template/.claude/skills/memory-flush/SKILL.md +20 -4
- package/obj/template/.claude/skills/memory-flush/sweep.py +74 -6
- package/obj/template/.claude/skills/memory-flush/tests/run.sh +300 -1
- package/obj/template/.claude/skills/tdd/SKILL.md +2 -1
- package/obj/template/.claude/skills/tdd/drift_check.py +180 -0
- package/obj/template/.claude/skills/tdd/tests/drift_check_test.sh +190 -0
- package/obj/template/.claude/skills/tdd/tests/run.sh +21 -0
- package/obj/template/.claude/skills/technical-tutorials/LICENSE +21 -0
- package/obj/template/.claude/skills/technical-tutorials/NOTICE +23 -0
- package/obj/template/.claude/skills/technical-tutorials/SKILL.md +1 -1
- package/obj/template/.claude/skills/triage/SKILL.md +11 -5
- package/obj/template/CLAUDE.md +36 -25
- package/obj/template/docs/init/seed.md +39 -24
- package/obj/template/manifest.json +73 -33
- package/package.json +5 -2
- package/src/CLAUDE.template.md +36 -25
- package/src/cli/merge.js +15 -10
- package/src/cli/tui/doctor.js +56 -0
- package/src/cli/tui/install.js +79 -0
- package/src/cli/tui/meta.js +30 -0
- package/src/cli/tui/tokens.js +38 -0
- package/src/cli/tui/upgrade.js +100 -0
- package/src/memory/backlog.template.md +12 -0
- package/src/project.template.json +6 -1
- package/src/seed.template.md +39 -24
- package/src/settings.template.json +3 -4
- package/obj/template/.claude/hooks/consent_gate_grant.sh +0 -89
- 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.
|
|
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
|
-
|
|
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` (
|
|
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:
|
|
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.
|
|
21
|
-
7.
|
|
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
|
|
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`
|
|
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
|
-
|
|
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 →
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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) copy — prose 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. |
|