UNRELEASED DEVELOPMENT SNAPSHOT · READ-ONLY EXCERPT
lib/pack-resolver.sh
This is the cited technical source at local revision 275a354, inspected on 22 September 2026. It differs from the public release. This reference publishes only the cited sections, not an installable release or the development branch.
Line numbers refer to the complete recorded file. Uncited lines are omitted. Full-file SHA-256: 88fc2b3154cbac8fed57fde709cfcf555b05f446e852088ddfbe5ed641638cba.
1
#!/usr/bin/env bash2
# lib/pack-resolver.sh — Lintel PackResolver (v4.0 Phase 1)3
#4
# Critical-path interface used by 30+ skills. Reads identity-bound state5
# from the active pack with explicit failure semantics + per-session cache.6
#7
# Per v4.0 design doc §2.4:8
# - Parse pack.yaml once at SENSE Step 1 (cached for cycle lifetime)9
# - Hard-fail SENSE on parse-error10
# - Refuse activation on extends: cycle11
# - Reject load on missing required field12
# - Fall back to _default on internal bug + warn13
# - Cached value immutable for current cycle (mid-cycle pack-switch deferred);14
# exception: a pack.yaml whose mtime is NEWER than the cache re-resolves —15
# manifest edits must not be invisible until a manual clear_pack_cache16
#… omitted uncited lines …
24
# in a sourced library leaks them into every caller (skills, hooks, tests).25
# Positional params are hardened with ${1:-} instead.27
LINTEL_HOME="${LINTEL_HOME:-$HOME/.lintel}"28
LINTEL_SOURCE_ROOT="${LINTEL_SOURCE_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." 2>/dev/null && pwd)}"29
LINTEL_REPO_ROOT="${LINTEL_REPO_ROOT:-$LINTEL_SOURCE_ROOT}"30
LINTEL_PACKS_DIR="${LINTEL_PACKS_DIR:-$LINTEL_HOME/packs}"31
LINTEL_ACTIVE_PACK_FILE="${LINTEL_ACTIVE_PACK_FILE:-$LINTEL_PACKS_DIR/active-pack}"32
LINTEL_AUDIT_DIR="${LINTEL_AUDIT_DIR:-$LINTEL_HOME/audit}"34
# Per-session cache (lives at ~/.lintel/sessions/<id>-pack-cache.yaml).35
# Key precedence: explicit LINTEL_SESSION_ID (the documented test seam — must36
# out-rank ambient CLI env or sandboxed tests break) > the CLI's session id >37
# PPID-$$. Bare PPID was a CONSTANT on platforms where every tool call spawns38
# from PID 1 → one permanent shared cache, pack.yaml edits invisible.39
LINTEL_SESSION_ID="${LINTEL_SESSION_ID:-${CLAUDE_SESSION_ID:-${PPID:-1}-$$}}"40
PACK_CACHE_FILE="${LINTEL_HOME}/sessions/${LINTEL_SESSION_ID}-pack-cache.yaml"42
mkdir -p "$LINTEL_AUDIT_DIR" "${LINTEL_HOME}/sessions" 2>/dev/null || true44
# Unified audit writer (resolved via repo-root). Idempotent source.45
command -v audit_log >/dev/null 2>&1 || source "${LINTEL_SOURCE_ROOT}/bin/_audit.sh"47
# ─── Internal logging ──────────────────────────────────────────────────────48
_resolver_audit() {49
local kind msg50
kind="${1:-info}"51
msg="${2:-}"52
audit_log "pack-resolver" "pack_resolver_${kind}" "msg=$msg"53
}55
_resolver_warn() {56
echo "[lintel/pack-resolver] WARN: $1" >&257
_resolver_audit warn "$1"58
}60
_resolver_fail() {61
echo "[lintel/pack-resolver] FAIL: $1" >&262
_resolver_audit fail "$1"63
}65
# ─── Active pack discovery ─────────────────────────────────────────────────66
get_active_pack_name() {… omitted uncited lines …
240
!skip { print }241
' "$out" > "$out.tmp2" 2>/dev/null && mv "$out.tmp2" "$out"242
done244
# Append this manifest's content to $out (skipping comments/blanks for clean cache)245
cat "$manifest" >> "$out"246
printf '\n' >> "$out"247
done248
}250
# ─── Cache management ──────────────────────────────────────────────────────251
_prime_cache_for_session() {252
# Reads active pack, validates, copies merged manifest to cache.253
# Run once per session. Subsequent resolve_pack_field calls hit cache.254
if [ -f "$PACK_CACHE_FILE" ]; then255
# mtime invalidation: a pack.yaml edited AFTER the cache was primed must not256
# stay invisible until a manual clear_pack_cache. The active-pack POINTER is257
# deliberately not checked — mid-session pack-switch keeps the cached value258
# per the §2.4 contract (fallback-harness scenario 7); a deleted pack keeps259
# the cache too (scenario 8: _pack_dir fails → manifest not checked).260
local _name _p _pd _stale=0261
_name=$(get_active_pack_name)262
for _p in $(_resolve_extends_chain "$_name"); do263
_pd=$(_pack_dir "$_p" 2>/dev/null) || continue264
[ "$_pd/pack.yaml" -nt "$PACK_CACHE_FILE" ] && _stale=1265
done266
if [ "$_stale" = 0 ]; then267
return 0 # already primed and fresh268
fi269
_resolver_audit cache_invalidated "pack=$_name reason=manifest-newer-than-cache session=$LINTEL_SESSION_ID"270
rm -f "$PACK_CACHE_FILE" 2>/dev/null || true271
fi273
local name274
name=$(get_active_pack_name)276
if ! validate_pack "$name"; then277
# Failure mode 4: internal bug or invalid active pack → fall back to _default278
_resolver_warn "active pack '$name' invalid; falling back to _default"279
name="_default"280
if ! validate_pack "$name"; then281
_resolver_fail "_default pack ALSO invalid — repo is broken"282
return 1283
fi284
fi286
# Resolve extends: chain (parent → child order)287
local chain288
chain=$(_resolve_extends_chain "$name")290
# Count chain length (whitespace-separated tokens)291
local chain_count292
# shellcheck disable=SC2086293
chain_count=$(printf '%s' "$chain" | awk '{print NF}')295
if [ "${chain_count:-1}" -le 1 ]; then296
# No inheritance — single pack, simple copy297
local pack_dir298
pack_dir=$(_pack_dir "$name")299
cp "$pack_dir/pack.yaml" "$PACK_CACHE_FILE" 2>/dev/null || {300
_resolver_fail "could not cache pack.yaml to $PACK_CACHE_FILE"301
return 1302
}303
else