@thebassclef/lite 1.1.1 → 1.2.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 (135) hide show
  1. package/dist/cli.cjs +23 -6
  2. package/dist/cli.js +23 -6
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/lite/.claude/bassclef-configs.jsonc +56 -0
  7. package/dist/lite/.claude/bassclef-orientation.md +120 -0
  8. package/dist/lite/.claude/hooks/adr-discipline-check.sh +132 -0
  9. package/dist/lite/.claude/hooks/atomic-pr-check.sh +169 -0
  10. package/dist/lite/.claude/hooks/auto-save-idle.sh +51 -0
  11. package/dist/lite/.claude/hooks/bassclef-sync.sh +51 -13
  12. package/dist/lite/.claude/hooks/bet-doc-gate.sh +283 -0
  13. package/dist/lite/.claude/hooks/bug-diagnosis.sh +64 -0
  14. package/dist/lite/.claude/hooks/destructive-command-guard.sh +136 -0
  15. package/dist/lite/.claude/hooks/file-rename-discipline-check.sh +147 -0
  16. package/dist/lite/.claude/hooks/hook-classifier-canary.sh +158 -0
  17. package/dist/lite/.claude/hooks/kiss-default-check.sh +129 -0
  18. package/dist/lite/.claude/hooks/multi-tenancy-check.sh +83 -0
  19. package/dist/lite/.claude/hooks/oo-ad-entry-point-check.sh +216 -0
  20. package/dist/lite/.claude/hooks/operator-private-path-check.sh +203 -0
  21. package/dist/lite/.claude/hooks/pattern-annotation-validate.sh +142 -0
  22. package/dist/lite/.claude/hooks/plain-english-steering.sh +15 -1
  23. package/dist/lite/.claude/hooks/post-write-trace.sh +66 -0
  24. package/dist/lite/.claude/hooks/pr-body-scrub-check.sh +446 -0
  25. package/dist/lite/.claude/hooks/pre-commit-identifier-leak-scrub.sh +137 -0
  26. package/dist/lite/.claude/hooks/pre-gh-pr-body-scrub.sh +192 -0
  27. package/dist/lite/.claude/hooks/pre-gh-pr-title-scrub.sh +172 -0
  28. package/dist/lite/.claude/hooks/role-entry-gate.sh +115 -0
  29. package/dist/lite/.claude/hooks/save-state.sh +268 -0
  30. package/dist/lite/.claude/hooks/session-reflection.d/08-settings-drift.sh +8 -1
  31. package/dist/lite/.claude/hooks/session-reflection.d/10-abrupt-stop-recovery.sh +52 -0
  32. package/dist/lite/.claude/hooks/session-reflection.d/55-orientation-gate.sh +40 -4
  33. package/dist/lite/.claude/hooks/session-reflection.d/80-hook-heartbeat-check.sh +12 -5
  34. package/dist/lite/.claude/hooks/session-reflection.d/81-hook-manifest-staleness.sh +18 -2
  35. package/dist/lite/.claude/hooks/session-reflection.d/91-npm-version-drift.sh +54 -0
  36. package/dist/lite/.claude/hooks/session-reflection.d/tests/10-abrupt-stop-recovery.test.sh +95 -0
  37. package/dist/lite/.claude/hooks/session-reflection.sh +62 -15
  38. package/dist/lite/.claude/hooks/tool-failure-diagnosis.sh +50 -0
  39. package/dist/lite/.claude/luminaries/andrej-karpathy.md +165 -0
  40. package/dist/lite/.claude/luminaries/dave-gray.md +155 -0
  41. package/dist/lite/.claude/luminaries/gary-klein.md +166 -0
  42. package/dist/lite/.claude/luminaries/kaoru-ishikawa.md +162 -0
  43. package/dist/lite/.claude/luminaries/lean-product-canvas.md +33 -0
  44. package/dist/lite/.claude/luminaries/luke-wroblewski.md +224 -0
  45. package/dist/lite/.claude/luminaries/ryan-singer.md +113 -0
  46. package/dist/lite/.claude/luminaries/steve-krug.md +168 -0
  47. package/dist/lite/.claude/luminaries/tony-ulwick.md +134 -0
  48. package/dist/lite/.claude/rules/arc-level-orientation-before-leaf.md +60 -0
  49. package/dist/lite/.claude/rules/architect-review-discipline.md +71 -0
  50. package/dist/lite/.claude/rules/auto-fix-criteria.md +126 -0
  51. package/dist/lite/.claude/rules/bassclef-hook-drift-blocker.md +144 -0
  52. package/dist/lite/.claude/rules/bassclef-models-discovery.md +107 -0
  53. package/dist/lite/.claude/rules/bassclef-workflow-parity.md +104 -0
  54. package/dist/lite/.claude/rules/cost-governance.md +154 -0
  55. package/dist/lite/.claude/rules/file-rename-discipline.md +95 -0
  56. package/dist/lite/.claude/rules/mechanism-fidelity.md +88 -0
  57. package/dist/lite/.claude/rules/prototype-production-coupling.md +125 -0
  58. package/dist/lite/.claude/rules/quick-fix-criteria.md +90 -0
  59. package/dist/lite/.claude/rules/session-artifacts.md +30 -0
  60. package/dist/lite/.claude/rules/sibling-smoke-after-substrate-change.md +107 -0
  61. package/dist/lite/.claude/rules/substrate-as-system.md +67 -0
  62. package/dist/lite/.claude/rules/usability.md +22 -0
  63. package/dist/lite/.claude/settings.json +20 -0
  64. package/dist/lite/.claude/skills/agent-research-spawn/SKILL.md +100 -0
  65. package/dist/lite/.claude/skills/architect-review/SKILL.md +201 -0
  66. package/dist/lite/.claude/skills/auto-fix/SKILL.md +297 -0
  67. package/dist/lite/.claude/skills/autonomous/SKILL.md +300 -0
  68. package/dist/lite/.claude/skills/bind-subdomain/SKILL.md +232 -0
  69. package/dist/lite/.claude/skills/canvas/SKILL.md +41 -0
  70. package/dist/lite/.claude/skills/deploy-prod/SKILL.md +106 -0
  71. package/dist/lite/.claude/skills/docs-sync/SKILL.md +188 -0
  72. package/dist/lite/.claude/skills/eisenhower/SKILL.md +273 -0
  73. package/dist/lite/.claude/skills/empathy-map/SKILL.md +168 -0
  74. package/dist/lite/.claude/skills/frontend-design/SKILL.md +53 -0
  75. package/dist/lite/.claude/skills/interview-me/SKILL.md +171 -0
  76. package/dist/lite/.claude/skills/journal-export/SKILL.md +293 -0
  77. package/dist/lite/.claude/skills/jtbd-tasks/SKILL.md +203 -0
  78. package/dist/lite/.claude/skills/objectory-decompose/SKILL.md +256 -0
  79. package/dist/lite/.claude/skills/pick-luminaries/SKILL.md +100 -0
  80. package/dist/lite/.claude/skills/pre-mortem/SKILL.md +180 -0
  81. package/dist/lite/.claude/skills/prototype-gallery/SKILL.md +264 -0
  82. package/dist/lite/.claude/skills/provision-deploy-host/SKILL.md +257 -0
  83. package/dist/lite/.claude/skills/quick-fix/SKILL.md +273 -0
  84. package/dist/lite/.claude/skills/release/SKILL.md +311 -0
  85. package/dist/lite/.claude/skills/release-notes/SKILL.md +202 -0
  86. package/dist/lite/.claude/skills/rfc/SKILL.md +222 -0
  87. package/dist/lite/.claude/skills/riff-prototypes/SKILL.md +637 -0
  88. package/dist/lite/.claude/skills/secrets-bootstrap/SKILL.md +209 -0
  89. package/dist/lite/.claude/skills/skill/SKILL.md +304 -0
  90. package/dist/lite/.claude/skills/smoke-test/SKILL.md +100 -0
  91. package/dist/lite/.claude/skills/state/SKILL.md +204 -0
  92. package/dist/lite/.claude/skills/substrate-check/SKILL.md +218 -0
  93. package/dist/lite/.claude/skills/synthetic-user/SKILL.md +381 -0
  94. package/dist/lite/.claude/skills/task-scenarios/SKILL.md +105 -0
  95. package/dist/lite/.claude/skills/tier-dependency-audit/SKILL.md +139 -0
  96. package/dist/lite/.claude/skills/ux-migration/SKILL.md +424 -0
  97. package/dist/lite/.claude/skills/value-prop-canvas/SKILL.md +297 -0
  98. package/dist/lite/README.md +5 -1
  99. package/dist/lite/gitignore +6 -0
  100. package/dist/lite/lib/bassclef-dir-resolver.sh +85 -0
  101. package/dist/lite/lib/clone-failure-classifier.sh +59 -0
  102. package/dist/lite/lib/fixture-builder.sh +190 -0
  103. package/dist/lite/lib/fresh-install-check.sh +49 -0
  104. package/dist/lite/lib/hook-heartbeat.sh +0 -1
  105. package/dist/lite/lib/mechanism-fidelity.sh +569 -0
  106. package/dist/lite/lib/npm-version-check.sh +112 -0
  107. package/dist/lite/lib/preview-state.sh +229 -0
  108. package/dist/lite/lib/session-rescue.sh +0 -1
  109. package/dist/lite/scripts/generate-lite-manifest.sh +145 -4
  110. package/dist/lite/scripts/lite-manifest-drift-check.sh +5 -1
  111. package/dist/lite/standards/architect-review-discipline.md +222 -0
  112. package/dist/lite/standards/bassclef-platform.md +249 -0
  113. package/dist/lite/standards/bassclef-test-repos.md +97 -0
  114. package/dist/lite/standards/bassclef-wiring-manifest.json +25 -1
  115. package/dist/lite/standards/cold-adopter-harness.md +205 -0
  116. package/dist/lite/standards/data-management.md +135 -0
  117. package/dist/lite/standards/frontend-stack/nextjs-prisma.md +124 -0
  118. package/dist/lite/standards/lite-manifest-schema-changes.md +27 -0
  119. package/dist/lite/standards/lite-manifest.json +1456 -14
  120. package/dist/lite/standards/mechanism-fidelity.md +205 -0
  121. package/dist/lite/standards/operator-private-discipline.md +200 -0
  122. package/dist/lite/standards/operator-to-public-flow.md +195 -0
  123. package/dist/lite/standards/prototype-gallery.md +251 -0
  124. package/dist/lite/standards/pseudonym-key-schema.md +109 -0
  125. package/dist/lite/standards/sibling-validation-checklist.md +181 -0
  126. package/dist/lite/standards/slack-notifications.md +161 -0
  127. package/dist/lite/standards/synthetic-user-testing.md +173 -0
  128. package/dist/lite/standards/vendor-bindings.md +165 -0
  129. package/dist/lite/standards/vendor-plugins/frontend-design.md +66 -0
  130. package/dist/lite/standards/vpc-fit-validation.md +167 -0
  131. package/dist/lite/templates/gallery-yml.md +113 -0
  132. package/dist/lite/templates/product-canvas.md +169 -0
  133. package/dist/lite/templates/value-prop-canvas.md +180 -0
  134. package/package.json +3 -3
  135. /package/dist/lite/{whereami.md → docs/whereami.md} +0 -0
@@ -0,0 +1,112 @@
1
+ #!/bin/bash
2
+ # tier: lite
3
+ #
4
+ # npm-version-check.sh — detect when installed @thebassclef/lite drifts behind
5
+ # npm-latest. Ships as part of the Fork 2 finding cure — no signal today
6
+ # tells adopters when a newer lite version publishes.
7
+ #
8
+ # Contract: reads .bassclef/init.manifest.json for installed version;
9
+ # reads/writes cache at DRIFT_CACHE_DIR/npm-drift-cache.json (default
10
+ # $HOME/.claude/state); consults cache first (24h TTL); on cache miss
11
+ # runs `npm view @thebassclef/lite version` with 2s timeout; compares
12
+ # installed vs npm-latest via semver; emits banner + exit 1 when
13
+ # installed < npm-latest; silent + exit 0 in every other case
14
+ # (including all failure classes).
15
+ #
16
+ # Never crashes. Every failure returns silent-pass.
17
+ #
18
+ # Anchor luminaries:
19
+ # @luminary john-ousterhout — deep module (network + cache + semver behind narrow interface)
20
+ # @luminary michael-nygard — silent-pass on every failure class; 2s timeout; 24h cache TTL
21
+ # @luminary alan-cooper — adopter hears drift once per day, acts on `bassclef sync`
22
+
23
+ _semver_cmp() {
24
+ # Compare two semver strings. Prints -1, 0, or 1 for A<B, A=B, A>B.
25
+ # Handles X.Y.Z form. Non-semver → treat as 0 (equal) — caller stays silent.
26
+ local a="$1" b="$2"
27
+ [ "$a" = "$b" ] && echo 0 && return 0
28
+ local -a pa pb
29
+ IFS='.' read -r -a pa <<< "$a"
30
+ IFS='.' read -r -a pb <<< "$b"
31
+ local i
32
+ for i in 0 1 2; do
33
+ local ai="${pa[i]:-0}" bi="${pb[i]:-0}"
34
+ # Ensure integer
35
+ [[ "$ai" =~ ^[0-9]+$ ]] || { echo 0; return 0; }
36
+ [[ "$bi" =~ ^[0-9]+$ ]] || { echo 0; return 0; }
37
+ if [ "$ai" -lt "$bi" ]; then echo -1; return 0; fi
38
+ if [ "$ai" -gt "$bi" ]; then echo 1; return 0; fi
39
+ done
40
+ echo 0
41
+ }
42
+
43
+ check_lite_version_drift() {
44
+ local pkg="@thebassclef/lite"
45
+ local cache_dir="${DRIFT_CACHE_DIR:-$HOME/.claude/state}"
46
+ local cache_file="$cache_dir/npm-drift-cache.json"
47
+ local manifest="${INIT_MANIFEST_OVERRIDE:-.bassclef/init.manifest.json}"
48
+ local ttl_seconds="${DRIFT_CACHE_TTL_SECONDS:-86400}"
49
+
50
+ # Precondition — init manifest present (adopter ran `bassclef init`).
51
+ [ -f "$manifest" ] || return 0
52
+
53
+ # Read installed version.
54
+ local installed
55
+ installed=$(jq -r '.version // ""' "$manifest" 2>/dev/null) || installed=""
56
+ [ -z "$installed" ] && return 0
57
+
58
+ # Try cache first.
59
+ local now npm_latest cache_age
60
+ now=$(date +%s)
61
+ npm_latest=""
62
+ if [ -f "$cache_file" ]; then
63
+ local cache_ts
64
+ cache_ts=$(jq -r '.last_checked_at // 0' "$cache_file" 2>/dev/null) || cache_ts=0
65
+ cache_age=$((now - cache_ts))
66
+ if [ "$cache_age" -ge 0 ] && [ "$cache_age" -lt "$ttl_seconds" ]; then
67
+ npm_latest=$(jq -r '.npm_latest // ""' "$cache_file" 2>/dev/null) || npm_latest=""
68
+ fi
69
+ fi
70
+
71
+ # Cache miss — fetch from npm.
72
+ if [ -z "$npm_latest" ]; then
73
+ command -v npm >/dev/null 2>&1 || return 0
74
+ # 2s timeout; silent on any failure.
75
+ local fetched
76
+ if command -v timeout >/dev/null 2>&1; then
77
+ fetched=$(timeout 2 npm view "$pkg" version 2>/dev/null | tail -n 1)
78
+ else
79
+ fetched=$(npm view "$pkg" version 2>/dev/null | tail -n 1)
80
+ fi
81
+ # Validate semver shape (X.Y.Z).
82
+ if ! [[ "$fetched" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
83
+ return 0
84
+ fi
85
+ npm_latest="$fetched"
86
+
87
+ # Update cache — silent on write failure.
88
+ if mkdir -p "$cache_dir" 2>/dev/null; then
89
+ printf '{"last_checked_at": %d, "npm_latest": "%s", "package": "%s"}\n' \
90
+ "$now" "$npm_latest" "$pkg" > "$cache_file" 2>/dev/null || true
91
+ fi
92
+ fi
93
+
94
+ # Compare + banner on drift.
95
+ local cmp
96
+ cmp=$(_semver_cmp "$installed" "$npm_latest")
97
+ if [ "$cmp" = "-1" ]; then
98
+ cat <<BANNER
99
+ ### Bassclef lite update available
100
+ Installed: ${installed} → npm-latest: ${npm_latest}
101
+
102
+ Update:
103
+ npm install -g @thebassclef/lite@latest
104
+ bassclef sync
105
+ BANNER
106
+ return 1
107
+ fi
108
+
109
+ return 0
110
+ }
111
+
112
+ export -f check_lite_version_drift 2>/dev/null || true
@@ -0,0 +1,229 @@
1
+ #!/usr/bin/env bash
2
+ # tier: lite
3
+ # lib/preview-state.sh — typed accessor for docs/preview-state/<slug>.yml
4
+ #
5
+ # Schema per docs/roadmaps/2026-04-25-doppelganger-fanout.md "Substrate
6
+ # self-awareness — preview-state". Single source of truth for /preview
7
+ # cycle state across sessions. Phase B (/preview composer) consumes this.
8
+ #
9
+ # Public API:
10
+ # ps_dir → echo path to docs/preview-state/
11
+ # ps_path <slug> → echo full path for a slug
12
+ # ps_create <slug> → initialize new state file with defaults
13
+ # ps_exists <slug> → exit 0 if file exists, 1 if not
14
+ # ps_get <slug> <field> → read scalar field
15
+ # ps_set <slug> <field> <value> → set scalar field
16
+ # ps_status <slug> → echo status field
17
+ # ps_list → list all known slugs
18
+ # ps_archive <slug> → mv to archive/ (for /preview reset)
19
+ #
20
+ # Schema (MVP scalar fields supported via ps_get/ps_set):
21
+ # slug, created, current_tier, status, selected_variant, identity_ref
22
+ #
23
+ # Multi-line / list fields (current_variants, luminaries_used,
24
+ # escalation_history, rejection_history) are appended via dedicated
25
+ # helpers in Phase B WS-4 — out of scope for WS-1 MVP. The schema
26
+ # template file is the source of truth for those.
27
+
28
+ set -uo pipefail
29
+
30
+ ps_dir() {
31
+ if [[ -n "${PREVIEW_STATE_DIR:-}" ]]; then
32
+ echo "$PREVIEW_STATE_DIR"
33
+ elif [[ -n "${BASSCLEF_REPO_ROOT:-}" ]]; then
34
+ echo "$BASSCLEF_REPO_ROOT/docs/preview-state"
35
+ else
36
+ echo "docs/preview-state"
37
+ fi
38
+ }
39
+
40
+ ps_path() {
41
+ local slug="$1"
42
+ echo "$(ps_dir)/${slug}.yml"
43
+ }
44
+
45
+ ps_exists() {
46
+ local slug="$1"
47
+ [[ -f "$(ps_path "$slug")" ]]
48
+ }
49
+
50
+ ps_create() {
51
+ local slug="$1"
52
+ local path
53
+ path="$(ps_path "$slug")"
54
+ mkdir -p "$(dirname "$path")"
55
+ if [[ -f "$path" ]]; then
56
+ echo "ps_create: already exists: $path" >&2
57
+ return 1
58
+ fi
59
+ cat > "$path" <<EOF
60
+ slug: ${slug}
61
+ created: $(date -u +'%Y-%m-%dT%H:%M:%S+0000')
62
+ current_tier: pending
63
+ status: pending
64
+ selected_variant: null
65
+ current_variants: []
66
+ luminaries_used: []
67
+ identity_ref: null
68
+ escalation_history: []
69
+ rejection_history: []
70
+ EOF
71
+ }
72
+
73
+ ps_get() {
74
+ local slug="$1"
75
+ local field="$2"
76
+ local path
77
+ path="$(ps_path "$slug")"
78
+ [[ -f "$path" ]] || { echo "ps_get: not-found: $path" >&2; return 1; }
79
+ awk -v f="$field" '
80
+ $0 ~ "^" f ":" {
81
+ sub("^" f ":[[:space:]]*", "")
82
+ print
83
+ exit
84
+ }
85
+ ' "$path"
86
+ }
87
+
88
+ ps_set() {
89
+ local slug="$1"
90
+ local field="$2"
91
+ shift 2
92
+ local value="$*"
93
+ local path
94
+ path="$(ps_path "$slug")"
95
+ [[ -f "$path" ]] || { echo "ps_set: not-found: $path" >&2; return 1; }
96
+ local tmp
97
+ tmp="$(mktemp)"
98
+ awk -v f="$field" -v v="$value" '
99
+ BEGIN { replaced = 0 }
100
+ !replaced && $0 ~ "^" f ":" {
101
+ print f ": " v
102
+ replaced = 1
103
+ next
104
+ }
105
+ { print }
106
+ END {
107
+ if (!replaced) {
108
+ print f ": " v
109
+ }
110
+ }
111
+ ' "$path" > "$tmp"
112
+ mv "$tmp" "$path"
113
+ }
114
+
115
+ ps_status() {
116
+ ps_get "$1" "status"
117
+ }
118
+
119
+ ps_list() {
120
+ local dir
121
+ dir="$(ps_dir)"
122
+ [[ -d "$dir" ]] || return 0
123
+ find "$dir" -maxdepth 1 -type f -name '*.yml' 2>/dev/null \
124
+ | sed -e 's|.*/||' -e 's|\.yml$||' | sort
125
+ }
126
+
127
+ ps_archive() {
128
+ local slug="$1"
129
+ local src dst archive_dir
130
+ src="$(ps_path "$slug")"
131
+ [[ -f "$src" ]] || { echo "ps_archive: not-found: $src" >&2; return 1; }
132
+ archive_dir="$(ps_dir)/archive"
133
+ mkdir -p "$archive_dir"
134
+ dst="$archive_dir/${slug}-$(date -u +'%Y%m%dT%H%M%S').yml"
135
+ mv "$src" "$dst"
136
+ echo "$dst"
137
+ }
138
+
139
+ # Reset: archive variants + record rejection rationale + reset to pending.
140
+ # Reads current_variants and current_tier from the YAML, appends a
141
+ # rejection_history entry, archives the variants block, sets status=reset.
142
+ # After reset, status:reset signals next /preview <tier> to read
143
+ # rejection_history as input ("what didn't work").
144
+ #
145
+ # Per standards/preview-state-schema.md "Reset is not erasure".
146
+ ps_reset() {
147
+ local slug="$1"
148
+ local reason="${2:-no reason given}"
149
+ local path
150
+ path="$(ps_path "$slug")"
151
+ [[ -f "$path" ]] || { echo "ps_reset: not-found: $path" >&2; return 1; }
152
+
153
+ local now
154
+ now="$(date -u +'%Y-%m-%dT%H:%M:%S+0000')"
155
+ local current_tier current_variants
156
+ current_tier="$(ps_get "$slug" current_tier 2>/dev/null || echo "unknown")"
157
+ # Read current_variants — single-line YAML list e.g. "[a, b, c]"
158
+ current_variants="$(ps_get "$slug" current_variants 2>/dev/null || echo "[]")"
159
+
160
+ # Build rejection_history entry as YAML block in a tempfile
161
+ # (awk -v doesn't accept multi-line values).
162
+ local entry_file
163
+ entry_file="$(mktemp)"
164
+ cat > "$entry_file" <<EOF
165
+ - tier_when_rejected: ${current_tier}
166
+ variants_archived: ${current_variants}
167
+ rejection_reason: "${reason}"
168
+ at: ${now}
169
+ EOF
170
+
171
+ # Append entry under rejection_history: line. If the line is "[]",
172
+ # replace with the first entry; if it has the bare line, just append.
173
+ local tmp
174
+ tmp="$(mktemp)"
175
+ awk -v entry_file="$entry_file" '
176
+ function emit_entry() {
177
+ while ((getline line < entry_file) > 0) print line
178
+ close(entry_file)
179
+ }
180
+ /^rejection_history:[[:space:]]*\[\][[:space:]]*$/ {
181
+ print "rejection_history:"
182
+ emit_entry()
183
+ next
184
+ }
185
+ /^rejection_history:[[:space:]]*$/ {
186
+ print
187
+ emit_entry()
188
+ next
189
+ }
190
+ { print }
191
+ ' "$path" > "$tmp"
192
+ mv "$tmp" "$path"
193
+ rm -f "$entry_file"
194
+
195
+ # Reset cycle-state fields
196
+ ps_set "$slug" status "reset"
197
+ ps_set "$slug" current_tier "pending"
198
+ ps_set "$slug" selected_variant "null"
199
+ ps_set "$slug" current_variants "[]"
200
+ }
201
+
202
+ # Kill: hard-archive the entire state file. Caller (typically /preview kill)
203
+ # is responsible for any --confirm gating. ps_kill is unconditional.
204
+ ps_kill() {
205
+ local slug="$1"
206
+ ps_archive "$slug"
207
+ }
208
+
209
+ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
210
+ cmd="${1:-}"
211
+ shift || true
212
+ case "$cmd" in
213
+ dir) ps_dir ;;
214
+ path) ps_path "$@" ;;
215
+ create) ps_create "$@" ;;
216
+ exists) ps_exists "$@" ;;
217
+ get) ps_get "$@" ;;
218
+ set) ps_set "$@" ;;
219
+ status) ps_status "$@" ;;
220
+ list) ps_list ;;
221
+ archive) ps_archive "$@" ;;
222
+ reset) ps_reset "$@" ;;
223
+ kill) ps_kill "$@" ;;
224
+ *)
225
+ echo "usage: $0 {dir|path|create|exists|get|set|status|list|archive|reset|kill} [args]" >&2
226
+ exit 2
227
+ ;;
228
+ esac
229
+ fi
@@ -6,7 +6,6 @@
6
6
  # the tracked-only check, soft cap, git-mv preference, banner emission, and
7
7
  # silent-fail behavior.
8
8
  #
9
- # tier: upstream
10
9
  # install-class: dual
11
10
  #
12
11
  # Interface (what callers know):
@@ -20,20 +20,23 @@
20
20
  # scripts/generate-lite-manifest.sh --dry-run # prints JSON to stdout; no file write
21
21
  # scripts/generate-lite-manifest.sh --output PATH # writes to specified path (test harness)
22
22
  # scripts/generate-lite-manifest.sh --root PATH # override repo root (test harness)
23
+ # scripts/generate-lite-manifest.sh --no-closure-check # skip reachability gate (logged)
23
24
  #
24
25
  # Exit codes:
25
26
  # 0 — manifest written or dry-run printed
26
27
  # 1 — invalid arg or missing dependency (jq, git, shasum/sha256sum)
27
28
  # 2 — no lite entries found (schema requires minItems: 1)
29
+ # 3 — closure gate: files reachable from manifest entries lack the tier tag (goal 2026-09-16a)
28
30
 
29
31
  set -euo pipefail
30
32
 
31
- MANIFEST_VERSION="1.6.1"
33
+ MANIFEST_VERSION="1.8.2"
32
34
 
33
35
  # --- arg parse ---
34
36
  DRY_RUN=0
35
37
  OUTPUT_PATH=""
36
38
  REPO_ROOT_OVERRIDE=""
39
+ NO_CLOSURE_CHECK=0
37
40
  TARGET_TIER="lite" # per #1295 — script now tier-parameterizable; default preserves prior API
38
41
 
39
42
  while [[ $# -gt 0 ]]; do
@@ -42,6 +45,7 @@ while [[ $# -gt 0 ]]; do
42
45
  --output) OUTPUT_PATH="${2:-}"; shift 2 ;;
43
46
  --root) REPO_ROOT_OVERRIDE="${2:-}"; shift 2 ;;
44
47
  --tier) TARGET_TIER="${2:-}"; shift 2 ;;
48
+ --no-closure-check) NO_CLOSURE_CHECK=1; shift ;;
45
49
  -h|--help)
46
50
  grep -E '^# ' "$0" | sed 's/^# \?//'
47
51
  exit 0
@@ -117,6 +121,25 @@ extract_hook_tier() {
117
121
  || true
118
122
  }
119
123
 
124
+ # Count tier DECLARATIONS in a bash file header.
125
+ #
126
+ # A declaration is "# tier: <word>" and nothing else on the line. A line like
127
+ # "# tier: upstream because this script audits orphans" explains the tier; it
128
+ # is prose, not a second declaration, and three legitimate files carry one.
129
+ #
130
+ # #1724: apply-lite-widen.sh used to insert a tag above an existing one rather
131
+ # than replacing it, leaving 26 files with two tier lines that disagreed.
132
+ # Every reader takes the first match, so nothing shipped wrong and nothing
133
+ # warned. This is the check that would have caught it. It lives here because
134
+ # the generator already visits every substrate file and runs on every PR via
135
+ # the drift check; the write-time hook fires on the Write tool only, and the
136
+ # widen edits through a shell redirect.
137
+ count_hook_tier_declarations() {
138
+ head -10 "$1" 2>/dev/null \
139
+ | grep -cE '^#[[:space:]]*tier:[[:space:]]*[a-z]+[[:space:]]*$' \
140
+ || true
141
+ }
142
+
120
143
  # Normalize `—` YAML escape to literal em-dash character. Closes
121
144
  # bassclef-upstream#1548 — YAML frontmatter with double-quoted strings can
122
145
  # carry `—` as a 6-char literal; jq preserves it as `\\u2014` in JSON
@@ -392,6 +415,21 @@ while IFS= read -r f; do
392
415
  SCRIPT_FILES+=("$f")
393
416
  done < <(find "$REPO_ROOT/scripts" -maxdepth 1 -type f -name '*.sh' 2>/dev/null | sort)
394
417
 
418
+ # Hook fragments (bash) — per goal 2026-09-16a (bassclef-upstream#1690): session-reflection.sh
419
+ # iterates session-reflection.d/*.sh at SessionStart; fragments ship as type=hook.
420
+ FRAGMENT_FILES=()
421
+ while IFS= read -r f; do
422
+ FRAGMENT_FILES+=("$f")
423
+ done < <(find "$REPO_ROOT/.claude/hooks" -mindepth 2 -type f -name '*.sh' \
424
+ -not -path '*/tests/*' -not -path '*/fixtures/*' 2>/dev/null | sort)
425
+
426
+ # Top-level .claude/*.md (bassclef-orientation.md) — vendored via @import from adopter
427
+ # CLAUDE.md; ships as root-doc. Per goal 2026-09-16a.
428
+ CLAUDE_DOC_FILES=()
429
+ while IFS= read -r f; do
430
+ CLAUDE_DOC_FILES+=("$f")
431
+ done < <(find "$REPO_ROOT/.claude" -maxdepth 1 -type f -name '*.md' 2>/dev/null | sort)
432
+
395
433
  # Presence templates (bash + md) — per #1392 Shape 3; adopter install dispatches through these
396
434
  PRESENCE_TEMPLATE_SH_FILES=()
397
435
  while IFS= read -r f; do
@@ -437,14 +475,70 @@ extract_json_description() {
437
475
  echo ""
438
476
  }
439
477
 
478
+ # ---------------------------------------------------------------------------
479
+ # Tier-declaration uniqueness (#1724)
480
+ #
481
+ # One file, one tier declaration. Every reader takes the first match, so a
482
+ # second line changes nothing at runtime and warns nobody — which is how 26
483
+ # files carried two disagreeing tiers for a day without any check noticing.
484
+ #
485
+ # Runs over tracked .sh files rather than the per-category arrays below, so a
486
+ # new emit surface inherits the check without a second edit.
487
+ #
488
+ # Bypass: SKIP_TIER_DECL_CHECK=1
489
+ # ---------------------------------------------------------------------------
490
+ if [[ "${SKIP_TIER_DECL_CHECK:-}" != "1" ]]; then
491
+ _dupe_count=0
492
+ _dupe_report=""
493
+ while IFS= read -r _f; do
494
+ [[ -f "$REPO_ROOT/$_f" ]] || continue
495
+ _n=$(count_hook_tier_declarations "$REPO_ROOT/$_f")
496
+ if [[ "${_n:-0}" -gt 1 ]]; then
497
+ _dupe_count=$((_dupe_count + 1))
498
+ _vals=$(head -10 "$REPO_ROOT/$_f" \
499
+ | grep -E '^#[[:space:]]*tier:[[:space:]]*[a-z]+[[:space:]]*$' \
500
+ | sed -E 's/^#[[:space:]]*tier:[[:space:]]*//' | tr '\n' '/')
501
+ _dupe_report+=" $_f — $_n declarations [$_vals]"$'\n'
502
+ fi
503
+ done < <(cd "$REPO_ROOT" && git ls-files '*.sh' 2>/dev/null)
504
+ if [[ "$_dupe_count" -gt 0 ]]; then
505
+ {
506
+ echo ""
507
+ echo "============================================================"
508
+ echo "TIER DECLARED MORE THAN ONCE in $_dupe_count file(s)"
509
+ echo "============================================================"
510
+ echo ""
511
+ printf '%s' "$_dupe_report"
512
+ echo ""
513
+ echo "----- Cure -----"
514
+ echo " Keep the first declaration. Delete the rest."
515
+ echo " The first is what every reader already uses, so deleting the"
516
+ echo " others changes no behavior."
517
+ echo ""
518
+ echo " If the value itself is wrong, fix it with:"
519
+ echo " bash scripts/apply-lite-widen.sh --apply"
520
+ echo " which retags from reachability per ADR-056 D7."
521
+ echo ""
522
+ echo " Bypass: SKIP_TIER_DECL_CHECK=1"
523
+ echo ""
524
+ } >&2
525
+ exit 5
526
+ fi
527
+ fi
528
+
440
529
  ENTRIES_JSON=$(
441
530
  {
442
531
  emit_entries "skill" extract_yaml_tier extract_yaml_description dirname "$REPO_ROOT/.claude/skills/"*/SKILL.md
443
532
  emit_entries "rule" extract_yaml_tier extract_yaml_description basename "$REPO_ROOT/.claude/rules/"*.md
444
533
  emit_entries "hook" extract_hook_tier extract_hook_description basename "$REPO_ROOT/.claude/hooks/"*.sh
534
+ if [[ ${#FRAGMENT_FILES[@]} -gt 0 ]]; then
535
+ emit_entries "hook" extract_hook_tier extract_hook_description basename "${FRAGMENT_FILES[@]}"
536
+ fi
445
537
  emit_entries "luminary" extract_yaml_tier extract_yaml_description basename "$REPO_ROOT/.claude/luminaries/"*.md
446
538
  emit_entries "agent" extract_yaml_tier extract_yaml_description basename "$REPO_ROOT/.claude/agents/"*.md
447
- emit_entries "standard" extract_yaml_tier extract_yaml_description basename "${STANDARDS_FILES[@]}"
539
+ if [[ ${#STANDARDS_FILES[@]} -gt 0 ]]; then
540
+ emit_entries "standard" extract_yaml_tier extract_yaml_description basename "${STANDARDS_FILES[@]}"
541
+ fi
448
542
  emit_entries "adr" extract_yaml_tier extract_yaml_description adr "$REPO_ROOT/architecture/decisions/"ADR-*.md
449
543
  if [[ ${#EMBEDDING_FILES[@]} -gt 0 ]]; then
450
544
  emit_entries "embedding" extract_json_tier extract_json_description basename "${EMBEDDING_FILES[@]}"
@@ -468,9 +562,23 @@ ENTRIES_JSON=$(
468
562
  if [[ ${#ROOT_DOC_FILES[@]} -gt 0 ]]; then
469
563
  emit_entries "root-doc" extract_yaml_tier extract_yaml_description basename "${ROOT_DOC_FILES[@]}"
470
564
  fi
565
+ if [[ ${#CLAUDE_DOC_FILES[@]} -gt 0 ]]; then
566
+ emit_entries "root-doc" extract_yaml_tier extract_yaml_description basename "${CLAUDE_DOC_FILES[@]}"
567
+ fi
471
568
  } | jq -s 'sort_by(.type, .slug)'
472
569
  )
473
570
 
571
+ # --- spill entries to a file (#1709 sister) ---
572
+ # Linux caps a SINGLE argv entry at MAX_ARG_STRLEN = 131072 bytes; macOS has
573
+ # no per-argument cap. Passing the whole payload as `--argjson entries "$X"`
574
+ # worked at 292 entries (107383 bytes) and died on the runner at 417 entries
575
+ # (159457 bytes) with "jq: Argument list too long". Every jq call that needs
576
+ # the payload now reads this file, so the size stops mattering.
577
+ ENTRIES_TMP=$(mktemp -t bassclef-manifest-entries.XXXXXX)
578
+ trap 'rm -f "$ENTRIES_TMP"' EXIT
579
+ printf '%s' "$ENTRIES_JSON" > "$ENTRIES_TMP"
580
+
581
+
474
582
  ENTRY_COUNT=$(echo "$ENTRIES_JSON" | jq 'length')
475
583
 
476
584
  if [[ "$ENTRY_COUNT" -lt 1 ]]; then
@@ -478,6 +586,39 @@ if [[ "$ENTRY_COUNT" -lt 1 ]]; then
478
586
  exit 2
479
587
  fi
480
588
 
589
+ # --- closure gate (goal 2026-09-16a, bassclef-upstream#1690; extends ADR-056 D1) ---
590
+ # Tags stay the source of what ships. Reachability is the validator: every
591
+ # file reachable from a manifest entry (source, glob-source dir, composes_with,
592
+ # settings.json wire, @luminary, path citation, /skill dispatch) must itself be
593
+ # tagged for this tier. Fails loud so the cure is a retag, never a silent drop.
594
+ # Excluded categories (adrs) are bassclef-internal governance: cited by rules
595
+ # as footnotes, not needed at adopter runtime.
596
+ CLOSURE_EXCLUDE_CATEGORIES="${CLOSURE_EXCLUDE_CATEGORIES:-adrs}"
597
+ if [[ "${SKIP_LITE_CLOSURE_CHECK:-0}" == "1" ]]; then
598
+ echo "SKIP: closure gate bypassed via SKIP_LITE_CLOSURE_CHECK=1" >&2
599
+ elif [[ "$NO_CLOSURE_CHECK" == "1" ]]; then
600
+ echo "SKIP: closure gate bypassed via --no-closure-check" >&2
601
+ elif ! command -v python3 >/dev/null 2>&1; then
602
+ echo "SKIP: closure gate needs python3 (not found)" >&2
603
+ else
604
+ WALKER="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/walk-substrate-graph.py"
605
+ CANDIDATE_TMP=$(mktemp -t bassclef-manifest-candidate.XXXXXX)
606
+ jq -n --arg mv "$MANIFEST_VERSION" --slurpfile entries "$ENTRIES_TMP" '{manifest_version:$mv, entries:$entries[0]}' > "$CANDIDATE_TMP"
607
+ EXCL_ARGS=()
608
+ IFS=',' read -ra _excl <<< "$CLOSURE_EXCLUDE_CATEGORIES"
609
+ for c in "${_excl[@]}"; do [[ -n "$c" ]] && EXCL_ARGS+=(--exclude-category "$c"); done
610
+ CLOSURE_JSON=$(python3 "$WALKER" --root "$REPO_ROOT" --tier "$TARGET_TIER" --closure-vs-manifest "$CANDIDATE_TMP" "${EXCL_ARGS[@]}" 2>/dev/null || echo '{}')
611
+ rm -f "$CANDIDATE_TMP"
612
+ WIDEN_N=$(echo "$CLOSURE_JSON" | jq -r '.counts.widen // 0')
613
+ if [[ "$WIDEN_N" -gt 0 ]]; then
614
+ echo "generate-${TARGET_TIER}-manifest: CLOSURE GATE — $WIDEN_N file(s) reachable from ${TARGET_TIER} entries but not tagged tier: ${TARGET_TIER}" >&2
615
+ echo "$CLOSURE_JSON" | jq -r '.widen_by_category | to_entries[] | .key as $c | .value[] | "CLOSURE: \(.path) (category=\($c) tier=\(.tier) reached_from=\(.reached_from))"' >&2
616
+ echo "Cure: bash scripts/apply-lite-widen.sh --apply (retags to fixed point, then re-run this generator)" >&2
617
+ echo "Override (logged): SKIP_LITE_CLOSURE_CHECK=1 or --no-closure-check" >&2
618
+ exit 3
619
+ fi
620
+ fi
621
+
481
622
  # --- assemble manifest ---
482
623
  # Includes tier root-key per #1295 — the manifest itself carries its own tier tag
483
624
  # so tier-tag-required-at-write.sh recognizes it. Per Saltzer-Schroeder complete
@@ -486,8 +627,8 @@ MANIFEST_JSON=$(jq -n \
486
627
  --arg tier "$TARGET_TIER" \
487
628
  --arg mv "$MANIFEST_VERSION" \
488
629
  --arg ga "$GENERATED_AT" \
489
- --argjson entries "$ENTRIES_JSON" \
490
- '{tier:$tier, manifest_version:$mv, generated_at:$ga, entries:$entries}')
630
+ --slurpfile entries "$ENTRIES_TMP" \
631
+ '{tier:$tier, manifest_version:$mv, generated_at:$ga, entries:$entries[0]}')
491
632
 
492
633
  # Per bassclef-upstream#1508 Cure a: upstream_commit dropped from manifest
493
634
  # content. Every rebase over a merged sibling PR guaranteed a conflict
@@ -80,7 +80,11 @@ done
80
80
  TMP_CANDIDATE=$(mktemp)
81
81
  trap 'rm -f "$TMP_CANDIDATE"' EXIT
82
82
 
83
- bash "${REPO_ROOT}/scripts/generate-lite-manifest.sh" --tier "$TARGET_TIER" --root "$REPO_ROOT" --output "$TMP_CANDIDATE" 2>/dev/null
83
+ # stderr surfaces so the closure gate's CLOSURE: lines reach the operator (goal 2026-09-16a)
84
+ if ! bash "${REPO_ROOT}/scripts/generate-lite-manifest.sh" --tier "$TARGET_TIER" --root "$REPO_ROOT" --output "$TMP_CANDIDATE"; then
85
+ echo "${TARGET_TIER}-manifest drift check: generator failed (see lines above)" >&2
86
+ exit 1
87
+ fi
84
88
 
85
89
  # Compare entries[] arrays (sorted; ignoring generated_at + upstream_commit)
86
90
  COMMITTED_ENTRIES=$(jq -S '.entries' "$MANIFEST_FILE")