@thebassclef/lite 1.1.1 → 1.2.0-alpha.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/dist/cli.cjs +23 -6
- package/dist/cli.js +23 -6
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lite/.claude/bassclef-configs.jsonc +56 -0
- package/dist/lite/.claude/bassclef-orientation.md +120 -0
- package/dist/lite/.claude/hooks/adr-discipline-check.sh +132 -0
- package/dist/lite/.claude/hooks/atomic-pr-check.sh +169 -0
- package/dist/lite/.claude/hooks/auto-save-idle.sh +51 -0
- package/dist/lite/.claude/hooks/bassclef-sync.sh +51 -13
- package/dist/lite/.claude/hooks/bet-doc-gate.sh +283 -0
- package/dist/lite/.claude/hooks/bug-diagnosis.sh +64 -0
- package/dist/lite/.claude/hooks/destructive-command-guard.sh +136 -0
- package/dist/lite/.claude/hooks/file-rename-discipline-check.sh +147 -0
- package/dist/lite/.claude/hooks/hook-classifier-canary.sh +158 -0
- package/dist/lite/.claude/hooks/kiss-default-check.sh +129 -0
- package/dist/lite/.claude/hooks/multi-tenancy-check.sh +83 -0
- package/dist/lite/.claude/hooks/oo-ad-entry-point-check.sh +216 -0
- package/dist/lite/.claude/hooks/operator-private-path-check.sh +203 -0
- package/dist/lite/.claude/hooks/pattern-annotation-validate.sh +142 -0
- package/dist/lite/.claude/hooks/plain-english-steering.sh +15 -1
- package/dist/lite/.claude/hooks/post-write-trace.sh +66 -0
- package/dist/lite/.claude/hooks/pr-body-scrub-check.sh +446 -0
- package/dist/lite/.claude/hooks/pre-commit-identifier-leak-scrub.sh +137 -0
- package/dist/lite/.claude/hooks/pre-gh-pr-body-scrub.sh +192 -0
- package/dist/lite/.claude/hooks/pre-gh-pr-title-scrub.sh +172 -0
- package/dist/lite/.claude/hooks/role-entry-gate.sh +115 -0
- package/dist/lite/.claude/hooks/save-state.sh +268 -0
- package/dist/lite/.claude/hooks/session-reflection.d/08-settings-drift.sh +8 -1
- package/dist/lite/.claude/hooks/session-reflection.d/10-abrupt-stop-recovery.sh +52 -0
- package/dist/lite/.claude/hooks/session-reflection.d/55-orientation-gate.sh +40 -4
- package/dist/lite/.claude/hooks/session-reflection.d/80-hook-heartbeat-check.sh +12 -5
- package/dist/lite/.claude/hooks/session-reflection.d/81-hook-manifest-staleness.sh +18 -2
- package/dist/lite/.claude/hooks/session-reflection.d/91-npm-version-drift.sh +54 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/10-abrupt-stop-recovery.test.sh +95 -0
- package/dist/lite/.claude/hooks/session-reflection.sh +62 -15
- package/dist/lite/.claude/hooks/tool-failure-diagnosis.sh +50 -0
- package/dist/lite/.claude/luminaries/andrej-karpathy.md +165 -0
- package/dist/lite/.claude/luminaries/dave-gray.md +155 -0
- package/dist/lite/.claude/luminaries/gary-klein.md +166 -0
- package/dist/lite/.claude/luminaries/kaoru-ishikawa.md +162 -0
- package/dist/lite/.claude/luminaries/lean-product-canvas.md +33 -0
- package/dist/lite/.claude/luminaries/luke-wroblewski.md +224 -0
- package/dist/lite/.claude/luminaries/ryan-singer.md +113 -0
- package/dist/lite/.claude/luminaries/steve-krug.md +168 -0
- package/dist/lite/.claude/luminaries/tony-ulwick.md +134 -0
- package/dist/lite/.claude/rules/arc-level-orientation-before-leaf.md +60 -0
- package/dist/lite/.claude/rules/architect-review-discipline.md +71 -0
- package/dist/lite/.claude/rules/auto-fix-criteria.md +126 -0
- package/dist/lite/.claude/rules/bassclef-hook-drift-blocker.md +144 -0
- package/dist/lite/.claude/rules/bassclef-models-discovery.md +107 -0
- package/dist/lite/.claude/rules/bassclef-workflow-parity.md +104 -0
- package/dist/lite/.claude/rules/cost-governance.md +154 -0
- package/dist/lite/.claude/rules/file-rename-discipline.md +95 -0
- package/dist/lite/.claude/rules/mechanism-fidelity.md +88 -0
- package/dist/lite/.claude/rules/prototype-production-coupling.md +125 -0
- package/dist/lite/.claude/rules/quick-fix-criteria.md +90 -0
- package/dist/lite/.claude/rules/session-artifacts.md +30 -0
- package/dist/lite/.claude/rules/sibling-smoke-after-substrate-change.md +107 -0
- package/dist/lite/.claude/rules/substrate-as-system.md +67 -0
- package/dist/lite/.claude/rules/usability.md +22 -0
- package/dist/lite/.claude/settings.json +20 -0
- package/dist/lite/.claude/skills/agent-research-spawn/SKILL.md +100 -0
- package/dist/lite/.claude/skills/architect-review/SKILL.md +201 -0
- package/dist/lite/.claude/skills/auto-fix/SKILL.md +297 -0
- package/dist/lite/.claude/skills/autonomous/SKILL.md +300 -0
- package/dist/lite/.claude/skills/bind-subdomain/SKILL.md +232 -0
- package/dist/lite/.claude/skills/canvas/SKILL.md +41 -0
- package/dist/lite/.claude/skills/deploy-prod/SKILL.md +106 -0
- package/dist/lite/.claude/skills/docs-sync/SKILL.md +188 -0
- package/dist/lite/.claude/skills/eisenhower/SKILL.md +273 -0
- package/dist/lite/.claude/skills/empathy-map/SKILL.md +168 -0
- package/dist/lite/.claude/skills/frontend-design/SKILL.md +53 -0
- package/dist/lite/.claude/skills/interview-me/SKILL.md +171 -0
- package/dist/lite/.claude/skills/journal-export/SKILL.md +293 -0
- package/dist/lite/.claude/skills/jtbd-tasks/SKILL.md +203 -0
- package/dist/lite/.claude/skills/objectory-decompose/SKILL.md +256 -0
- package/dist/lite/.claude/skills/pick-luminaries/SKILL.md +100 -0
- package/dist/lite/.claude/skills/pre-mortem/SKILL.md +180 -0
- package/dist/lite/.claude/skills/prototype-gallery/SKILL.md +264 -0
- package/dist/lite/.claude/skills/provision-deploy-host/SKILL.md +257 -0
- package/dist/lite/.claude/skills/quick-fix/SKILL.md +273 -0
- package/dist/lite/.claude/skills/release/SKILL.md +311 -0
- package/dist/lite/.claude/skills/release-notes/SKILL.md +202 -0
- package/dist/lite/.claude/skills/rfc/SKILL.md +222 -0
- package/dist/lite/.claude/skills/riff-prototypes/SKILL.md +637 -0
- package/dist/lite/.claude/skills/secrets-bootstrap/SKILL.md +209 -0
- package/dist/lite/.claude/skills/skill/SKILL.md +304 -0
- package/dist/lite/.claude/skills/smoke-test/SKILL.md +100 -0
- package/dist/lite/.claude/skills/state/SKILL.md +204 -0
- package/dist/lite/.claude/skills/substrate-check/SKILL.md +218 -0
- package/dist/lite/.claude/skills/synthetic-user/SKILL.md +381 -0
- package/dist/lite/.claude/skills/task-scenarios/SKILL.md +105 -0
- package/dist/lite/.claude/skills/tier-dependency-audit/SKILL.md +139 -0
- package/dist/lite/.claude/skills/ux-migration/SKILL.md +424 -0
- package/dist/lite/.claude/skills/value-prop-canvas/SKILL.md +297 -0
- package/dist/lite/README.md +5 -1
- package/dist/lite/gitignore +6 -0
- package/dist/lite/lib/bassclef-dir-resolver.sh +85 -0
- package/dist/lite/lib/clone-failure-classifier.sh +59 -0
- package/dist/lite/lib/fixture-builder.sh +190 -0
- package/dist/lite/lib/fresh-install-check.sh +49 -0
- package/dist/lite/lib/hook-heartbeat.sh +0 -1
- package/dist/lite/lib/mechanism-fidelity.sh +569 -0
- package/dist/lite/lib/npm-version-check.sh +112 -0
- package/dist/lite/lib/preview-state.sh +229 -0
- package/dist/lite/lib/session-rescue.sh +0 -1
- package/dist/lite/scripts/generate-lite-manifest.sh +145 -4
- package/dist/lite/scripts/lite-manifest-drift-check.sh +5 -1
- package/dist/lite/standards/architect-review-discipline.md +222 -0
- package/dist/lite/standards/bassclef-platform.md +249 -0
- package/dist/lite/standards/bassclef-test-repos.md +97 -0
- package/dist/lite/standards/bassclef-wiring-manifest.json +25 -1
- package/dist/lite/standards/cold-adopter-harness.md +205 -0
- package/dist/lite/standards/data-management.md +135 -0
- package/dist/lite/standards/frontend-stack/nextjs-prisma.md +124 -0
- package/dist/lite/standards/lite-manifest-schema-changes.md +27 -0
- package/dist/lite/standards/lite-manifest.json +1456 -14
- package/dist/lite/standards/mechanism-fidelity.md +205 -0
- package/dist/lite/standards/operator-private-discipline.md +200 -0
- package/dist/lite/standards/operator-to-public-flow.md +195 -0
- package/dist/lite/standards/prototype-gallery.md +251 -0
- package/dist/lite/standards/pseudonym-key-schema.md +109 -0
- package/dist/lite/standards/sibling-validation-checklist.md +181 -0
- package/dist/lite/standards/slack-notifications.md +161 -0
- package/dist/lite/standards/synthetic-user-testing.md +173 -0
- package/dist/lite/standards/vendor-bindings.md +165 -0
- package/dist/lite/standards/vendor-plugins/frontend-design.md +66 -0
- package/dist/lite/standards/vpc-fit-validation.md +167 -0
- package/dist/lite/templates/gallery-yml.md +113 -0
- package/dist/lite/templates/product-canvas.md +169 -0
- package/dist/lite/templates/value-prop-canvas.md +180 -0
- package/package.json +3 -3
- /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
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
--
|
|
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
|
-
|
|
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")
|