Source index · Technical reference · Home

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 bash
2# lib/pack-resolver.sh — Lintel PackResolver (v4.0 Phase 1)
3#
4# Critical-path interface used by 30+ skills. Reads identity-bound state
5# 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-error
10# - Refuse activation on extends: cycle
11# - Reject load on missing required field
12# - Fall back to _default on internal bug + warn
13# - 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_cache
… omitted uncited lines …
24# in a sourced library leaks them into every caller (skills, hooks, tests).
25# Positional params are hardened with ${1:-} instead.
27LINTEL_HOME="${LINTEL_HOME:-$HOME/.lintel}"
28LINTEL_SOURCE_ROOT="${LINTEL_SOURCE_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." 2>/dev/null && pwd)}"
29LINTEL_REPO_ROOT="${LINTEL_REPO_ROOT:-$LINTEL_SOURCE_ROOT}"
30LINTEL_PACKS_DIR="${LINTEL_PACKS_DIR:-$LINTEL_HOME/packs}"
31LINTEL_ACTIVE_PACK_FILE="${LINTEL_ACTIVE_PACK_FILE:-$LINTEL_PACKS_DIR/active-pack}"
32LINTEL_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 — must
36# 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 spawns
38# from PID 1 → one permanent shared cache, pack.yaml edits invisible.
39LINTEL_SESSION_ID="${LINTEL_SESSION_ID:-${CLAUDE_SESSION_ID:-${PPID:-1}-$$}}"
40PACK_CACHE_FILE="${LINTEL_HOME}/sessions/${LINTEL_SESSION_ID}-pack-cache.yaml"
42mkdir -p "$LINTEL_AUDIT_DIR" "${LINTEL_HOME}/sessions" 2>/dev/null || true
44# Unified audit writer (resolved via repo-root). Idempotent source.
45command -v audit_log >/dev/null 2>&1 || source "${LINTEL_SOURCE_ROOT}/bin/_audit.sh"
47# ─── Internal logging ──────────────────────────────────────────────────────
48_resolver_audit() {
49 local kind msg
50 kind="${1:-info}"
51 msg="${2:-}"
52 audit_log "pack-resolver" "pack_resolver_${kind}" "msg=$msg"
55_resolver_warn() {
56 echo "[lintel/pack-resolver] WARN: $1" >&2
57 _resolver_audit warn "$1"
60_resolver_fail() {
61 echo "[lintel/pack-resolver] FAIL: $1" >&2
62 _resolver_audit fail "$1"
65# ─── Active pack discovery ─────────────────────────────────────────────────
66get_active_pack_name() {
… omitted uncited lines …
240 !skip { print }
241 ' "$out" > "$out.tmp2" 2>/dev/null && mv "$out.tmp2" "$out"
242 done
244 # Append this manifest's content to $out (skipping comments/blanks for clean cache)
245 cat "$manifest" >> "$out"
246 printf '\n' >> "$out"
247 done
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" ]; then
255 # mtime invalidation: a pack.yaml edited AFTER the cache was primed must not
256 # stay invisible until a manual clear_pack_cache. The active-pack POINTER is
257 # deliberately not checked — mid-session pack-switch keeps the cached value
258 # per the §2.4 contract (fallback-harness scenario 7); a deleted pack keeps
259 # the cache too (scenario 8: _pack_dir fails → manifest not checked).
260 local _name _p _pd _stale=0
261 _name=$(get_active_pack_name)
262 for _p in $(_resolve_extends_chain "$_name"); do
263 _pd=$(_pack_dir "$_p" 2>/dev/null) || continue
264 [ "$_pd/pack.yaml" -nt "$PACK_CACHE_FILE" ] && _stale=1
265 done
266 if [ "$_stale" = 0 ]; then
267 return 0 # already primed and fresh
268 fi
269 _resolver_audit cache_invalidated "pack=$_name reason=manifest-newer-than-cache session=$LINTEL_SESSION_ID"
270 rm -f "$PACK_CACHE_FILE" 2>/dev/null || true
271 fi
273 local name
274 name=$(get_active_pack_name)
276 if ! validate_pack "$name"; then
277 # Failure mode 4: internal bug or invalid active pack → fall back to _default
278 _resolver_warn "active pack '$name' invalid; falling back to _default"
279 name="_default"
280 if ! validate_pack "$name"; then
281 _resolver_fail "_default pack ALSO invalid — repo is broken"
282 return 1
283 fi
284 fi
286 # Resolve extends: chain (parent → child order)
287 local chain
288 chain=$(_resolve_extends_chain "$name")
290 # Count chain length (whitespace-separated tokens)
291 local chain_count
292 # shellcheck disable=SC2086
293 chain_count=$(printf '%s' "$chain" | awk '{print NF}')
295 if [ "${chain_count:-1}" -le 1 ]; then
296 # No inheritance — single pack, simple copy
297 local pack_dir
298 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 1
302 }
303 else