@thebassclef/lite 1.3.0 → 1.4.1
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 +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lite/.bassclef-source.json +2 -2
- package/dist/lite/.claude/hooks/_lib/wirings.sh +237 -0
- package/dist/lite/.claude/hooks/pre-commit-manifest-autoregen.sh +187 -0
- package/dist/lite/.claude/hooks/session-reflection.d/08-settings-drift.sh +66 -6
- package/dist/lite/.claude/hooks/session-reflection.d/20-artifact-staleness.sh +25 -5
- package/dist/lite/.claude/hooks/session-reflection.d/30-metrics-staleness.sh +11 -0
- package/dist/lite/.claude/hooks/session-reflection.d/60-deferred-actions.sh +2 -1
- package/dist/lite/.claude/hooks/session-reflection.d/80-hook-heartbeat-check.sh +20 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/20-artifact-staleness.test.sh +89 -0
- package/dist/lite/.claude/hooks/session-reflection.sh +12 -1
- package/dist/lite/.claude/luminaries/david-farley.md +115 -0
- package/dist/lite/.claude/luminaries/jez-humble.md +124 -0
- package/dist/lite/.claude/luminaries/martin-fowler.md +18 -0
- package/dist/lite/.claude/skills/longrun/SKILL.md +51 -0
- package/dist/lite/.claude/skills/onboard-repo/SKILL.md +39 -11
- package/dist/lite/.claude/skills/provision-deploy-host/SKILL.md +2 -2
- package/dist/lite/lib/ancestor-claude-check.sh +101 -0
- package/dist/lite/lib/fixture-builder.sh +324 -0
- package/dist/lite/lib/workflow-metrics.sh +166 -0
- package/dist/lite/presence/install/bassclef-configs.template.jsonc +76 -0
- package/dist/lite/presence/install/bassclef-sync.template.sh +7 -4
- package/dist/lite/scripts/generate-lite-manifest.sh +151 -1
- package/dist/lite/scripts/workflow-metrics-query.sh +57 -0
- package/dist/lite/standards/lite-manifest-schema-changes.md +24 -0
- package/dist/lite/standards/lite-manifest.json +79 -68
- package/package.json +1 -1
- package/dist/lite/.claude/skills/journal-export/SKILL.md +0 -293
- package/dist/lite/.claude/skills/release/SKILL.md +0 -311
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
#
|
|
4
|
+
# ancestor-claude-check.sh — walks up from a starting directory to $HOME
|
|
5
|
+
# (or a provided ceiling) and echoes each ancestor .claude/ directory found.
|
|
6
|
+
# Empty output = no ancestor .claude/ directories.
|
|
7
|
+
#
|
|
8
|
+
# Claude Code walks up from cwd loading .claude/ at every ancestor level.
|
|
9
|
+
# A stale ancestor .claude/skills/ produces duplicate + often-outdated skill
|
|
10
|
+
# entries in every child repo. Cold adopter can hit this on first install if
|
|
11
|
+
# they have any legacy .claude/ in a parent dir. /onboard-repo Phase X calls
|
|
12
|
+
# this helper to surface the class before proceeding.
|
|
13
|
+
#
|
|
14
|
+
# Contract:
|
|
15
|
+
# Args: $1 = starting directory (required; must exist)
|
|
16
|
+
# $2 = ceiling directory (optional; defaults to $HOME)
|
|
17
|
+
# Output: one absolute path per line, each an ancestor .claude/ directory
|
|
18
|
+
# between $1 (exclusive) and the ceiling (inclusive).
|
|
19
|
+
# Exit: 0 on clean walk (empty output OR one-or-more matches)
|
|
20
|
+
# 1 on error (missing start dir, unreadable ancestor)
|
|
21
|
+
#
|
|
22
|
+
# Adopter cure hint printed to stderr per match — non-blocking:
|
|
23
|
+
# "cure: mv <path> <path>.stale-<date>"
|
|
24
|
+
#
|
|
25
|
+
# Ships bassclef-upstream#573. Sister to sibling ticket that adds the manual
|
|
26
|
+
# audit step to the onboarding checklist.
|
|
27
|
+
#
|
|
28
|
+
# Anchors:
|
|
29
|
+
# @luminary saltzer-schroeder — complete mediation (every ancestor level checked)
|
|
30
|
+
# @luminary donald-norman — signifier + cure hint (adopter sees the issue + fix)
|
|
31
|
+
|
|
32
|
+
set -euo pipefail
|
|
33
|
+
|
|
34
|
+
_ancestor_claude_check_usage() {
|
|
35
|
+
cat >&2 <<'EOF'
|
|
36
|
+
ancestor-claude-check.sh — walks up from <start> to <ceiling> reporting ancestor .claude/ dirs
|
|
37
|
+
|
|
38
|
+
Usage:
|
|
39
|
+
bash lib/ancestor-claude-check.sh <start-dir> [ceiling-dir]
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
<start-dir> directory to start walking up from (required, must exist)
|
|
43
|
+
[ceiling-dir] stop above this dir (default: $HOME)
|
|
44
|
+
|
|
45
|
+
Output:
|
|
46
|
+
One absolute path per line for each ancestor .claude/ dir found.
|
|
47
|
+
Empty output = no ancestor .claude/ dirs (clean).
|
|
48
|
+
|
|
49
|
+
Exit:
|
|
50
|
+
0 = clean walk (regardless of match count)
|
|
51
|
+
1 = error (missing start dir OR unreadable ancestor)
|
|
52
|
+
|
|
53
|
+
Cure hint per match echoed to stderr, non-blocking.
|
|
54
|
+
EOF
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
ancestor_claude_check() {
|
|
58
|
+
local start="${1:-}"
|
|
59
|
+
local ceiling="${2:-${HOME}}"
|
|
60
|
+
|
|
61
|
+
if [ -z "$start" ]; then
|
|
62
|
+
_ancestor_claude_check_usage
|
|
63
|
+
return 1
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
if [ ! -d "$start" ]; then
|
|
67
|
+
echo "ancestor-claude-check: start dir not found: $start" >&2
|
|
68
|
+
return 1
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# Canonicalize both paths so comparison + traversal work under symlinks
|
|
72
|
+
local start_abs ceiling_abs
|
|
73
|
+
start_abs=$(cd "$start" && pwd -P)
|
|
74
|
+
ceiling_abs=$(cd "$ceiling" 2>/dev/null && pwd -P) || {
|
|
75
|
+
echo "ancestor-claude-check: ceiling dir not found: $ceiling" >&2
|
|
76
|
+
return 1
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Walk from parent of start up to (and including) ceiling
|
|
80
|
+
local cur="$start_abs"
|
|
81
|
+
local today
|
|
82
|
+
today=$(date -u +%Y-%m-%d)
|
|
83
|
+
while [ "$cur" != "/" ]; do
|
|
84
|
+
cur=$(dirname "$cur")
|
|
85
|
+
if [ -d "$cur/.claude" ]; then
|
|
86
|
+
echo "$cur/.claude"
|
|
87
|
+
echo "cure: mv \"$cur/.claude\" \"$cur/.claude.stale-$today\"" >&2
|
|
88
|
+
fi
|
|
89
|
+
# Stop after processing ceiling — do not walk above it
|
|
90
|
+
if [ "$cur" = "$ceiling_abs" ]; then
|
|
91
|
+
break
|
|
92
|
+
fi
|
|
93
|
+
done
|
|
94
|
+
|
|
95
|
+
return 0
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# When invoked directly (not sourced), run against argv
|
|
99
|
+
if [ "${BASH_SOURCE[0]:-}" = "${0}" ]; then
|
|
100
|
+
ancestor_claude_check "$@"
|
|
101
|
+
fi
|
|
@@ -12,6 +12,20 @@
|
|
|
12
12
|
# fixture_setup_substrate_sibling(fixture_dir, substrate_name)
|
|
13
13
|
# fixture_inject_pre_rename_refs(adopter_dir, canonical_substrate_dir)
|
|
14
14
|
#
|
|
15
|
+
# Cold-adopter-harness fixture (per bassclef-upstream#1789 goal 2026-09-19b):
|
|
16
|
+
# fixture_setup_cold_adopter_harness(target) → echo cache path; runs
|
|
17
|
+
# harness once per shard, caches
|
|
18
|
+
# stdout+stderr for reuse
|
|
19
|
+
# harness_check_output(target) → echo cached content
|
|
20
|
+
# harness_cache_path(target) → echo cache path (no side effects)
|
|
21
|
+
# harness_exit_code(target) → echo harness exit code from run
|
|
22
|
+
# fixture_cleanup_cold_adopter_harness() → sweep harness fixture cache
|
|
23
|
+
#
|
|
24
|
+
# The harness fixture uses BASSCLEF_HARNESS_FIXTURE_ROOT env var if set,
|
|
25
|
+
# otherwise $TMPDIR/bassclef-harness-fixture-$$ (per-shard PID scope).
|
|
26
|
+
# Cache key is a short SHA of the canonicalized target path so different
|
|
27
|
+
# targets do not collide.
|
|
28
|
+
#
|
|
15
29
|
# Override: SKIP_FIXTURE_BUILDER=1 — every public function exits 0 silently
|
|
16
30
|
# (fixture_init echoes empty path).
|
|
17
31
|
# Logged via trace-helper per bassclef hook idiom.
|
|
@@ -188,3 +202,313 @@ fixture_inject_pre_rename_refs() {
|
|
|
188
202
|
EOF
|
|
189
203
|
return 0
|
|
190
204
|
}
|
|
205
|
+
|
|
206
|
+
# =============================================================================
|
|
207
|
+
# Cold-adopter-harness fixture — bassclef-upstream#1789 goal 2026-09-19b
|
|
208
|
+
# Runs the cold-adopter harness once per shard, caches stdout+stderr, exposes
|
|
209
|
+
# an accessor. Tests grep cached content instead of re-invoking harness.
|
|
210
|
+
# =============================================================================
|
|
211
|
+
|
|
212
|
+
# _harness_fixture_root — echo the cache root path
|
|
213
|
+
# Precedence:
|
|
214
|
+
# 1. BASSCLEF_HARNESS_FIXTURE_ROOT env (explicit override — tests, prewarm job)
|
|
215
|
+
# 2. SHARD_INDEX env (CI matrix — per-shard scope so tests in the SAME shard
|
|
216
|
+
# SHARE cache; without this, each test's own PID gives a fresh cache and
|
|
217
|
+
# the fixture saves nothing across tests)
|
|
218
|
+
# 3. $$ (current shell PID — local single-test runs, per-process isolated)
|
|
219
|
+
_harness_fixture_root() {
|
|
220
|
+
if [ -n "${BASSCLEF_HARNESS_FIXTURE_ROOT:-}" ]; then
|
|
221
|
+
echo "$BASSCLEF_HARNESS_FIXTURE_ROOT"
|
|
222
|
+
elif [ -n "${SHARD_INDEX:-}" ]; then
|
|
223
|
+
echo "${TMPDIR:-/tmp}/bassclef-harness-fixture-shard-${SHARD_INDEX}"
|
|
224
|
+
else
|
|
225
|
+
echo "${TMPDIR:-/tmp}/bassclef-harness-fixture-$$"
|
|
226
|
+
fi
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
# harness_cache_path <target>
|
|
230
|
+
# Echo the cache file path for this target (public accessor). No side effects.
|
|
231
|
+
# Target need not exist — the path is a function of the canonicalized target
|
|
232
|
+
# string (or the raw string when canonicalization fails).
|
|
233
|
+
harness_cache_path() {
|
|
234
|
+
local target="${1:?harness_cache_path requires target}"
|
|
235
|
+
# Canonicalize if possible; fall back to raw string when target doesn't exist
|
|
236
|
+
local canonical
|
|
237
|
+
canonical=$(cd "$target" 2>/dev/null && pwd) || canonical="$target"
|
|
238
|
+
local root
|
|
239
|
+
root=$(_harness_fixture_root)
|
|
240
|
+
local hash
|
|
241
|
+
# Use shasum on macOS + Linux; fall back to md5 hash if shasum unavailable
|
|
242
|
+
if command -v shasum >/dev/null 2>&1; then
|
|
243
|
+
hash=$(echo "$canonical" | shasum -a 256 | cut -c1-16)
|
|
244
|
+
elif command -v sha256sum >/dev/null 2>&1; then
|
|
245
|
+
hash=$(echo "$canonical" | sha256sum | cut -c1-16)
|
|
246
|
+
else
|
|
247
|
+
# Last resort — path basename + length; not collision-proof but works
|
|
248
|
+
hash=$(echo "$canonical" | tr -c 'a-zA-Z0-9' '_' | tail -c 16)
|
|
249
|
+
fi
|
|
250
|
+
echo "$root/harness-$hash.out"
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
# -----------------------------------------------------------------------------
|
|
254
|
+
# fixture_setup_cold_adopter_harness <target>
|
|
255
|
+
# Run the cold-adopter harness once per shard against <target>. Cache stdout
|
|
256
|
+
# + stderr to a per-target file. Second call reuses cache. Echoes cache path.
|
|
257
|
+
# Override SKIP_FIXTURE_BUILDER=1 — return 0 silently.
|
|
258
|
+
# -----------------------------------------------------------------------------
|
|
259
|
+
fixture_setup_cold_adopter_harness() {
|
|
260
|
+
if [ "${SKIP_FIXTURE_BUILDER:-0}" = "1" ]; then
|
|
261
|
+
return 0
|
|
262
|
+
fi
|
|
263
|
+
local target="${1:?fixture_setup_cold_adopter_harness requires target}"
|
|
264
|
+
# Canonicalize target path so different spellings share cache
|
|
265
|
+
local target_canonical
|
|
266
|
+
target_canonical=$(cd "$target" 2>/dev/null && pwd) || {
|
|
267
|
+
echo "fixture_setup_cold_adopter_harness: target does not exist: $target" >&2
|
|
268
|
+
return 1
|
|
269
|
+
}
|
|
270
|
+
local root
|
|
271
|
+
root=$(_harness_fixture_root)
|
|
272
|
+
mkdir -p "$root"
|
|
273
|
+
local cache_file
|
|
274
|
+
cache_file=$(harness_cache_path "$target_canonical")
|
|
275
|
+
local elapsed_marker="${cache_file}.elapsed"
|
|
276
|
+
local exit_marker="${cache_file}.exit"
|
|
277
|
+
|
|
278
|
+
# Reuse cache if present — cache lifetime is per-shard by design
|
|
279
|
+
if [ -f "$cache_file" ]; then
|
|
280
|
+
echo "$cache_file"
|
|
281
|
+
return 0
|
|
282
|
+
fi
|
|
283
|
+
|
|
284
|
+
# Locate the harness — derived from this lib's location
|
|
285
|
+
local this_lib="${BASH_SOURCE[0]}"
|
|
286
|
+
local lib_dir
|
|
287
|
+
lib_dir=$(cd "$(dirname "$this_lib")" && pwd)
|
|
288
|
+
local repo
|
|
289
|
+
repo=$(cd "$lib_dir/.." && pwd)
|
|
290
|
+
local harness="$repo/scripts/cold-adopter-harness-sync.sh"
|
|
291
|
+
|
|
292
|
+
if [ ! -f "$harness" ]; then
|
|
293
|
+
echo "fixture_setup_cold_adopter_harness: harness not found at $harness" >&2
|
|
294
|
+
return 1
|
|
295
|
+
fi
|
|
296
|
+
|
|
297
|
+
# Write to temp file + atomic rename so concurrent writers (rare — same PID
|
|
298
|
+
# scope) do not leave a partial cache readable to other callers.
|
|
299
|
+
local tmp_file="${cache_file}.tmp.$$"
|
|
300
|
+
local start end exit_code
|
|
301
|
+
# K1 instrumentation — record elapsed setup time
|
|
302
|
+
start=$(date +%s)
|
|
303
|
+
bash "$harness" --mode sync --target "$target_canonical" > "$tmp_file" 2>&1
|
|
304
|
+
exit_code=$?
|
|
305
|
+
end=$(date +%s)
|
|
306
|
+
|
|
307
|
+
# Atomic promotion — first writer wins; race loser removes their tmp file.
|
|
308
|
+
# POSIX rename is atomic within a filesystem. -n avoids clobbering a
|
|
309
|
+
# concurrent winner's cache (BSD + GNU mv both support -n since 2007).
|
|
310
|
+
if [ ! -f "$cache_file" ]; then
|
|
311
|
+
mv "$tmp_file" "$cache_file"
|
|
312
|
+
echo "$exit_code" > "$exit_marker"
|
|
313
|
+
echo "$((end - start))" > "$elapsed_marker"
|
|
314
|
+
else
|
|
315
|
+
rm -f "$tmp_file"
|
|
316
|
+
fi
|
|
317
|
+
|
|
318
|
+
echo "$cache_file"
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
# -----------------------------------------------------------------------------
|
|
322
|
+
# harness_exit_code <target>
|
|
323
|
+
# Echo the harness exit code from the cached run. Empty if cache absent.
|
|
324
|
+
# Callers check this before trusting cache content — a non-zero exit means
|
|
325
|
+
# the harness bailed out and content may be partial.
|
|
326
|
+
# Override SKIP_FIXTURE_BUILDER=1 — echo nothing, return 0.
|
|
327
|
+
# -----------------------------------------------------------------------------
|
|
328
|
+
harness_exit_code() {
|
|
329
|
+
if [ "${SKIP_FIXTURE_BUILDER:-0}" = "1" ]; then
|
|
330
|
+
return 0
|
|
331
|
+
fi
|
|
332
|
+
local target="${1:?harness_exit_code requires target}"
|
|
333
|
+
local canonical
|
|
334
|
+
canonical=$(cd "$target" 2>/dev/null && pwd) || canonical="$target"
|
|
335
|
+
local cache_file
|
|
336
|
+
cache_file=$(harness_cache_path "$canonical")
|
|
337
|
+
local exit_marker="${cache_file}.exit"
|
|
338
|
+
if [ -f "$exit_marker" ]; then
|
|
339
|
+
cat "$exit_marker"
|
|
340
|
+
fi
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
# -----------------------------------------------------------------------------
|
|
344
|
+
# harness_check_output <target>
|
|
345
|
+
# Echo the cached harness stdout+stderr for <target>. Empty output if cache
|
|
346
|
+
# not yet built (caller should invoke fixture_setup_cold_adopter_harness first).
|
|
347
|
+
# Override SKIP_FIXTURE_BUILDER=1 — echo nothing, return 0.
|
|
348
|
+
# -----------------------------------------------------------------------------
|
|
349
|
+
harness_check_output() {
|
|
350
|
+
if [ "${SKIP_FIXTURE_BUILDER:-0}" = "1" ]; then
|
|
351
|
+
return 0
|
|
352
|
+
fi
|
|
353
|
+
local target="${1:?harness_check_output requires target}"
|
|
354
|
+
local target_canonical
|
|
355
|
+
target_canonical=$(cd "$target" 2>/dev/null && pwd) || return 0
|
|
356
|
+
local cache_file
|
|
357
|
+
cache_file=$(harness_cache_path "$target_canonical")
|
|
358
|
+
if [ -f "$cache_file" ]; then
|
|
359
|
+
cat "$cache_file"
|
|
360
|
+
fi
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
# -----------------------------------------------------------------------------
|
|
364
|
+
# fixture_cleanup_cold_adopter_harness
|
|
365
|
+
# Sweep the harness fixture cache. Idempotent.
|
|
366
|
+
# Override SKIP_FIXTURE_BUILDER=1 — return 0 silently.
|
|
367
|
+
# -----------------------------------------------------------------------------
|
|
368
|
+
fixture_cleanup_cold_adopter_harness() {
|
|
369
|
+
if [ "${SKIP_FIXTURE_BUILDER:-0}" = "1" ]; then
|
|
370
|
+
return 0
|
|
371
|
+
fi
|
|
372
|
+
local root
|
|
373
|
+
root=$(_harness_fixture_root)
|
|
374
|
+
if [ -d "$root" ]; then
|
|
375
|
+
rm -f "$root"/harness-*.out "$root"/harness-*.elapsed "$root"/harness-*.exit "$root"/harness-*.tmp.* 2>/dev/null || true
|
|
376
|
+
fi
|
|
377
|
+
return 0
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
# -----------------------------------------------------------------------------
|
|
381
|
+
# harness_adopter_shape_cache_path <source_target>
|
|
382
|
+
# Echo the cache file path for adopter-shape output derived from <source_target>.
|
|
383
|
+
# Public accessor. No side effects. Same shape as harness_cache_path but with
|
|
384
|
+
# "-adopter-shape" suffix so upstream + adopter-shape caches never collide.
|
|
385
|
+
# -----------------------------------------------------------------------------
|
|
386
|
+
harness_adopter_shape_cache_path() {
|
|
387
|
+
local target="${1:?harness_adopter_shape_cache_path requires source target}"
|
|
388
|
+
local canonical
|
|
389
|
+
canonical=$(cd "$target" 2>/dev/null && pwd) || canonical="$target"
|
|
390
|
+
local root
|
|
391
|
+
root=$(_harness_fixture_root)
|
|
392
|
+
local hash
|
|
393
|
+
if command -v shasum >/dev/null 2>&1; then
|
|
394
|
+
hash=$(echo "$canonical" | shasum -a 256 | cut -c1-16)
|
|
395
|
+
elif command -v sha256sum >/dev/null 2>&1; then
|
|
396
|
+
hash=$(echo "$canonical" | sha256sum | cut -c1-16)
|
|
397
|
+
else
|
|
398
|
+
hash=$(echo "$canonical" | tr -c 'a-zA-Z0-9' '_' | tail -c 16)
|
|
399
|
+
fi
|
|
400
|
+
echo "$root/harness-adopter-shape-$hash.out"
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
# -----------------------------------------------------------------------------
|
|
404
|
+
# fixture_setup_cold_adopter_shape <source_target>
|
|
405
|
+
# Build one synthetic adopter-shape tmpdir per shard from <source_target>
|
|
406
|
+
# (cp -R substrate + rm tests dirs), run the harness once against it, cache
|
|
407
|
+
# stdout + stderr. Second call reuses cache. Echoes cache path.
|
|
408
|
+
#
|
|
409
|
+
# bassclef-upstream#1800 — closes the shard 1 duplicate for
|
|
410
|
+
# cold-adopter-harness-adopter-mode.test.sh Test 1. Test 4 keeps its own
|
|
411
|
+
# tmpdir invocation because rule injection invalidates this fixture's cache;
|
|
412
|
+
# a rule-parameterized variant tracked as follow-on.
|
|
413
|
+
#
|
|
414
|
+
# Override SKIP_FIXTURE_BUILDER=1 — return 0 silently.
|
|
415
|
+
# -----------------------------------------------------------------------------
|
|
416
|
+
fixture_setup_cold_adopter_shape() {
|
|
417
|
+
if [ "${SKIP_FIXTURE_BUILDER:-0}" = "1" ]; then
|
|
418
|
+
return 0
|
|
419
|
+
fi
|
|
420
|
+
local source_target="${1:?fixture_setup_cold_adopter_shape requires source target}"
|
|
421
|
+
local source_canonical
|
|
422
|
+
source_canonical=$(cd "$source_target" 2>/dev/null && pwd) || {
|
|
423
|
+
echo "fixture_setup_cold_adopter_shape: source target does not exist: $source_target" >&2
|
|
424
|
+
return 1
|
|
425
|
+
}
|
|
426
|
+
local root
|
|
427
|
+
root=$(_harness_fixture_root)
|
|
428
|
+
mkdir -p "$root"
|
|
429
|
+
local cache_file
|
|
430
|
+
cache_file=$(harness_adopter_shape_cache_path "$source_canonical")
|
|
431
|
+
local elapsed_marker="${cache_file}.elapsed"
|
|
432
|
+
local exit_marker="${cache_file}.exit"
|
|
433
|
+
|
|
434
|
+
# Reuse cache if present — cache lifetime is per-shard by design
|
|
435
|
+
if [ -f "$cache_file" ]; then
|
|
436
|
+
echo "$cache_file"
|
|
437
|
+
return 0
|
|
438
|
+
fi
|
|
439
|
+
|
|
440
|
+
# Locate the harness — derived from this lib's location
|
|
441
|
+
local this_lib="${BASH_SOURCE[0]}"
|
|
442
|
+
local lib_dir
|
|
443
|
+
lib_dir=$(cd "$(dirname "$this_lib")" && pwd)
|
|
444
|
+
local repo
|
|
445
|
+
repo=$(cd "$lib_dir/.." && pwd)
|
|
446
|
+
local harness="$repo/scripts/cold-adopter-harness-sync.sh"
|
|
447
|
+
|
|
448
|
+
if [ ! -f "$harness" ]; then
|
|
449
|
+
echo "fixture_setup_cold_adopter_shape: harness not found at $harness" >&2
|
|
450
|
+
return 1
|
|
451
|
+
fi
|
|
452
|
+
|
|
453
|
+
# Build the adopter-shape tmpdir — cp -R substrate + rm tests dirs
|
|
454
|
+
local adopter_tmpdir
|
|
455
|
+
adopter_tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/adopter-shape-fixture-XXXXXX")
|
|
456
|
+
# cp only the surfaces the harness reads; suppress errors on optional dirs
|
|
457
|
+
cp -R \
|
|
458
|
+
"$source_canonical/.claude" \
|
|
459
|
+
"$source_canonical/scripts" \
|
|
460
|
+
"$source_canonical/lib" \
|
|
461
|
+
"$source_canonical/standards" \
|
|
462
|
+
"$source_canonical/architecture" \
|
|
463
|
+
"$source_canonical/presence" \
|
|
464
|
+
"$adopter_tmpdir/" 2>/dev/null || true
|
|
465
|
+
# rm tests dirs to force adopter-shape (0 tests)
|
|
466
|
+
rm -rf \
|
|
467
|
+
"$adopter_tmpdir/.claude/hooks/tests" \
|
|
468
|
+
"$adopter_tmpdir/scripts/tests" \
|
|
469
|
+
"$adopter_tmpdir/lib/tests" 2>/dev/null || true
|
|
470
|
+
|
|
471
|
+
# Run harness with SHAPE_D_BLOCK=0 to match adopter-mode.test.sh semantics
|
|
472
|
+
# (forces ADVISORY output so DEAD-LETTER surfaces without failing the process)
|
|
473
|
+
local tmp_file="${cache_file}.tmp.$$"
|
|
474
|
+
local start end exit_code
|
|
475
|
+
start=$(date +%s)
|
|
476
|
+
SHAPE_D_BLOCK=0 bash "$harness" --mode sync --target "$adopter_tmpdir" > "$tmp_file" 2>&1
|
|
477
|
+
exit_code=$?
|
|
478
|
+
end=$(date +%s)
|
|
479
|
+
|
|
480
|
+
# Cleanup the tmpdir — cache holds the output; tmpdir was ephemeral
|
|
481
|
+
rm -rf "$adopter_tmpdir" 2>/dev/null || true
|
|
482
|
+
|
|
483
|
+
# Atomic promotion — first writer wins; race loser removes their tmp file
|
|
484
|
+
if [ ! -f "$cache_file" ]; then
|
|
485
|
+
mv "$tmp_file" "$cache_file"
|
|
486
|
+
echo "$exit_code" > "$exit_marker"
|
|
487
|
+
echo "$((end - start))" > "$elapsed_marker"
|
|
488
|
+
else
|
|
489
|
+
rm -f "$tmp_file"
|
|
490
|
+
fi
|
|
491
|
+
|
|
492
|
+
echo "$cache_file"
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
# -----------------------------------------------------------------------------
|
|
496
|
+
# harness_check_output_adopter_shape <source_target>
|
|
497
|
+
# Echo the cached harness stdout+stderr for the adopter-shape variant of
|
|
498
|
+
# <source_target>. Empty output if cache not yet built (caller should invoke
|
|
499
|
+
# fixture_setup_cold_adopter_shape first).
|
|
500
|
+
# Override SKIP_FIXTURE_BUILDER=1 — echo nothing, return 0.
|
|
501
|
+
# -----------------------------------------------------------------------------
|
|
502
|
+
harness_check_output_adopter_shape() {
|
|
503
|
+
if [ "${SKIP_FIXTURE_BUILDER:-0}" = "1" ]; then
|
|
504
|
+
return 0
|
|
505
|
+
fi
|
|
506
|
+
local target="${1:?harness_check_output_adopter_shape requires source target}"
|
|
507
|
+
local canonical
|
|
508
|
+
canonical=$(cd "$target" 2>/dev/null && pwd) || return 0
|
|
509
|
+
local cache_file
|
|
510
|
+
cache_file=$(harness_adopter_shape_cache_path "$canonical")
|
|
511
|
+
if [ -f "$cache_file" ]; then
|
|
512
|
+
cat "$cache_file"
|
|
513
|
+
fi
|
|
514
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# Workflow metrics measurement library — Module B4 per
|
|
4
|
+
# docs/decompositions/2026-09-18-ci-cd-subsystem.md.
|
|
5
|
+
#
|
|
6
|
+
# Records CI workflow run durations + shard breakdowns. Enables Phase
|
|
7
|
+
# checkpoint sampling per canvas activity diagram. Every downstream
|
|
8
|
+
# Phase 1-4 cure references this lib to prove KPI moves.
|
|
9
|
+
#
|
|
10
|
+
# Closes sunj-labs/bassclef-upstream#1307 (parent epic #1750, rank 0).
|
|
11
|
+
#
|
|
12
|
+
# Anchor luminaries:
|
|
13
|
+
# @luminary nicole-forsgren — DORA metrics
|
|
14
|
+
# @luminary john-ousterhout — deep module (hides store shape)
|
|
15
|
+
# @luminary michael-nygard — fail-soft on missing dir / jq / disk
|
|
16
|
+
#
|
|
17
|
+
# Storage: atomic file per run at
|
|
18
|
+
# ${STORE_DIR}/YYYY-MM-DD-<workflow>-<run_id>.json
|
|
19
|
+
#
|
|
20
|
+
# Usage:
|
|
21
|
+
# source lib/workflow-metrics.sh
|
|
22
|
+
# record_metric <workflow> <run_id> <duration_seconds> <shard_breakdown> [store_dir]
|
|
23
|
+
# query_metrics [store_dir] [workflow_filter]
|
|
24
|
+
# compute_median [store_dir] [workflow_filter]
|
|
25
|
+
#
|
|
26
|
+
# Fail-soft everywhere — missing dir, missing jq, disk full all return
|
|
27
|
+
# non-error exits. Missing metrics never turn CI red.
|
|
28
|
+
|
|
29
|
+
# Default store dir
|
|
30
|
+
_workflow_metrics_default_store() {
|
|
31
|
+
local repo_root
|
|
32
|
+
repo_root=$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD")
|
|
33
|
+
echo "$repo_root/state/workflow-metrics"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# record_metric <workflow> <run_id> <duration_seconds> <shard_breakdown> [store_dir]
|
|
37
|
+
#
|
|
38
|
+
# Writes a JSON entry to $store_dir/YYYY-MM-DD-<workflow>-<run_id>.json.
|
|
39
|
+
# Fail-soft in every branch — dir creation, jq missing, disk full all
|
|
40
|
+
# return exit 0.
|
|
41
|
+
record_metric() {
|
|
42
|
+
local workflow="${1:-}"
|
|
43
|
+
local run_id="${2:-}"
|
|
44
|
+
local duration="${3:-}"
|
|
45
|
+
local shard_breakdown="${4:-}"
|
|
46
|
+
local store_dir="${5:-$(_workflow_metrics_default_store)}"
|
|
47
|
+
|
|
48
|
+
# Fail-soft on missing args — never break the calling workflow
|
|
49
|
+
if [ -z "$workflow" ] || [ -z "$run_id" ] || [ -z "$duration" ]; then
|
|
50
|
+
return 0
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# Fail-soft on dir creation failure
|
|
54
|
+
mkdir -p "$store_dir" 2>/dev/null || return 0
|
|
55
|
+
|
|
56
|
+
local date_prefix
|
|
57
|
+
date_prefix=$(date -u +"%Y-%m-%d")
|
|
58
|
+
local timestamp
|
|
59
|
+
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
60
|
+
|
|
61
|
+
local file_path="$store_dir/${date_prefix}-${workflow}-${run_id}.json"
|
|
62
|
+
|
|
63
|
+
# Build JSON. Prefer jq for correctness; fall back to raw string.
|
|
64
|
+
if command -v jq >/dev/null 2>&1; then
|
|
65
|
+
jq -n \
|
|
66
|
+
--arg workflow "$workflow" \
|
|
67
|
+
--arg run_id "$run_id" \
|
|
68
|
+
--argjson duration_seconds "$duration" \
|
|
69
|
+
--arg shard_breakdown "$shard_breakdown" \
|
|
70
|
+
--arg timestamp "$timestamp" \
|
|
71
|
+
'{workflow: $workflow, run_id: $run_id, duration_seconds: $duration_seconds, shard_breakdown: $shard_breakdown, timestamp: $timestamp}' \
|
|
72
|
+
> "$file_path" 2>/dev/null || return 0
|
|
73
|
+
else
|
|
74
|
+
# Fallback — construct JSON by hand
|
|
75
|
+
printf '{"workflow":"%s","run_id":"%s","duration_seconds":%s,"shard_breakdown":"%s","timestamp":"%s"}\n' \
|
|
76
|
+
"$workflow" "$run_id" "$duration" "$shard_breakdown" "$timestamp" > "$file_path" 2>/dev/null || return 0
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
return 0
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# query_metrics [store_dir] [workflow_filter]
|
|
83
|
+
#
|
|
84
|
+
# Emits one line per entry to stdout: run_id duration_seconds workflow
|
|
85
|
+
# If workflow_filter is set, only entries matching that workflow are emitted.
|
|
86
|
+
# Empty store or missing dir returns empty output + exit 0.
|
|
87
|
+
query_metrics() {
|
|
88
|
+
local store_dir="${1:-$(_workflow_metrics_default_store)}"
|
|
89
|
+
local workflow_filter="${2:-}"
|
|
90
|
+
|
|
91
|
+
# Missing dir → empty output
|
|
92
|
+
if [ ! -d "$store_dir" ]; then
|
|
93
|
+
return 0
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# Find all JSON files under store_dir; fail-soft on empty
|
|
97
|
+
local files
|
|
98
|
+
files=$(find "$store_dir" -maxdepth 1 -name '*.json' -type f 2>/dev/null)
|
|
99
|
+
if [ -z "$files" ]; then
|
|
100
|
+
return 0
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
local file workflow run_id duration
|
|
104
|
+
while IFS= read -r file; do
|
|
105
|
+
[ -z "$file" ] && continue
|
|
106
|
+
if command -v jq >/dev/null 2>&1; then
|
|
107
|
+
workflow=$(jq -r '.workflow // ""' "$file" 2>/dev/null)
|
|
108
|
+
run_id=$(jq -r '.run_id // ""' "$file" 2>/dev/null)
|
|
109
|
+
duration=$(jq -r '.duration_seconds // 0' "$file" 2>/dev/null)
|
|
110
|
+
else
|
|
111
|
+
workflow=$(grep -o '"workflow":"[^"]*"' "$file" | cut -d'"' -f4)
|
|
112
|
+
run_id=$(grep -o '"run_id":"[^"]*"' "$file" | cut -d'"' -f4)
|
|
113
|
+
duration=$(grep -o '"duration_seconds":[0-9]*' "$file" | cut -d':' -f2)
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
if [ -n "$workflow_filter" ] && [ "$workflow" != "$workflow_filter" ]; then
|
|
117
|
+
continue
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
echo "$run_id $duration $workflow"
|
|
121
|
+
done <<< "$files"
|
|
122
|
+
|
|
123
|
+
return 0
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# compute_median [store_dir] [workflow_filter]
|
|
127
|
+
#
|
|
128
|
+
# Emits the median duration for entries in the store, optionally filtered
|
|
129
|
+
# by workflow. Empty store returns 0.
|
|
130
|
+
compute_median() {
|
|
131
|
+
local store_dir="${1:-$(_workflow_metrics_default_store)}"
|
|
132
|
+
local workflow_filter="${2:-}"
|
|
133
|
+
|
|
134
|
+
local durations
|
|
135
|
+
durations=$(query_metrics "$store_dir" "$workflow_filter" | awk '{print $2}' | sort -n)
|
|
136
|
+
|
|
137
|
+
if [ -z "$durations" ]; then
|
|
138
|
+
echo "0"
|
|
139
|
+
return 0
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
local count
|
|
143
|
+
count=$(echo "$durations" | wc -l | tr -d ' ')
|
|
144
|
+
|
|
145
|
+
if [ "$count" -eq 0 ]; then
|
|
146
|
+
echo "0"
|
|
147
|
+
return 0
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
local mid
|
|
151
|
+
if [ $((count % 2)) -eq 1 ]; then
|
|
152
|
+
# Odd count — middle element
|
|
153
|
+
mid=$(( (count + 1) / 2 ))
|
|
154
|
+
echo "$durations" | sed -n "${mid}p"
|
|
155
|
+
else
|
|
156
|
+
# Even count — average of two middle elements
|
|
157
|
+
local upper=$((count / 2 + 1))
|
|
158
|
+
local lower=$((count / 2))
|
|
159
|
+
local u l
|
|
160
|
+
u=$(echo "$durations" | sed -n "${upper}p")
|
|
161
|
+
l=$(echo "$durations" | sed -n "${lower}p")
|
|
162
|
+
echo $(( (u + l) / 2 ))
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
return 0
|
|
166
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{ "tier": "lite",
|
|
2
|
+
// Adopter-facing default for `.claude/bassclef-configs.jsonc`.
|
|
3
|
+
// Ships via lite manifest per bassclef-cli#104. `bassclef init` copies this
|
|
4
|
+
// file to `.claude/bassclef-configs.jsonc` on first-run. Adopters edit it
|
|
5
|
+
// to tune per-repo behavior.
|
|
6
|
+
//
|
|
7
|
+
// Full field catalog + validation: standards/state-spine/schemas/bassclef-configs.schema.json
|
|
8
|
+
// Field reference doc: standards/bassclef-configs-schema.md
|
|
9
|
+
//
|
|
10
|
+
// Every field below is optional. Absent fields fall back to substrate defaults.
|
|
11
|
+
// ----- tech stack -----
|
|
12
|
+
// Declares your primary framework so per-stack skills (SDLC gates,
|
|
13
|
+
// dependency check, migration discipline) load the right sibling docs.
|
|
14
|
+
// Common values: "nextjs", "vite-react", "remix", "fastapi", "rails",
|
|
15
|
+
// "express", "go-http", or omit for framework-agnostic.
|
|
16
|
+
//
|
|
17
|
+
// "tech_stack": {
|
|
18
|
+
// "frontend": "nextjs",
|
|
19
|
+
// "pkg_manager": "pnpm",
|
|
20
|
+
// "orm": "prisma",
|
|
21
|
+
// "lang": "typescript"
|
|
22
|
+
// },
|
|
23
|
+
|
|
24
|
+
// ----- prose discipline -----
|
|
25
|
+
// Controls the /kiss words Stop hook. Values:
|
|
26
|
+
// true — advisory (default; findings to stderr, session continues)
|
|
27
|
+
// "strict" — blocks the stop on jargon findings, forces a rewrite turn
|
|
28
|
+
// false — disabled
|
|
29
|
+
//
|
|
30
|
+
// "prose_discipline": {
|
|
31
|
+
// "kiss_words_turn_prose": true
|
|
32
|
+
// },
|
|
33
|
+
|
|
34
|
+
// ----- testing tier per path -----
|
|
35
|
+
// Extends .claude/rules/testing-tier-config.md with per-repo path matchers.
|
|
36
|
+
// Tier 0 = strict TDD (test mtime <= source mtime, BLOCK at pre-commit)
|
|
37
|
+
// Tier 1 = test-with (WARN at pre-commit if new source has no matching test)
|
|
38
|
+
// Tier 2 = smoke (advisory)
|
|
39
|
+
// Tier 3 = manual verify (silent)
|
|
40
|
+
//
|
|
41
|
+
// "testing": {
|
|
42
|
+
// "global_floor": 1,
|
|
43
|
+
// "path_matchers": [
|
|
44
|
+
// { "match": "src/middleware.ts", "tier": 0 },
|
|
45
|
+
// { "match": "src/lib/auth/**", "tier": 0 },
|
|
46
|
+
// { "match": "src/app/**", "tier": 2 }
|
|
47
|
+
// ]
|
|
48
|
+
// },
|
|
49
|
+
|
|
50
|
+
// ----- /promote target -----
|
|
51
|
+
// Where /promote sends substrate proposals. Two shapes:
|
|
52
|
+
// "<owner>/<repo>" — gh issue create --repo <value>
|
|
53
|
+
// "email:<address>" — mailto: URL printed for you to send
|
|
54
|
+
// Absent — /promote falls back to current-repo filing.
|
|
55
|
+
//
|
|
56
|
+
// "promote_target": "your-org/your-substrate-fork",
|
|
57
|
+
|
|
58
|
+
// ----- longrun orchestrator merge mode -----
|
|
59
|
+
// Controls how /longrun handles PR merges within its bet scope.
|
|
60
|
+
// "operator-gated" — every PR pauses for your review (default)
|
|
61
|
+
// "agent-merges-within-scope" — orchestrator merges autonomously within
|
|
62
|
+
// bet scope; hard_ceilings stay blocking
|
|
63
|
+
//
|
|
64
|
+
// "longrun": {
|
|
65
|
+
// "orchestrator_merge": {
|
|
66
|
+
// "mode": "operator-gated",
|
|
67
|
+
// "hard_ceilings": ["auth", "schema", "security", "prod-deploy", "blast-radius-floor"]
|
|
68
|
+
// }
|
|
69
|
+
// },
|
|
70
|
+
|
|
71
|
+
// ----- adopter defaults -----
|
|
72
|
+
// Per-adopter tunings. See standards/bassclef-configs-schema.md
|
|
73
|
+
// for the full field list.
|
|
74
|
+
//
|
|
75
|
+
// "adopter_defaults": {}
|
|
76
|
+
}
|