portable-agent-layer 0.47.0 โ†’ 0.49.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.
package/README.md CHANGED
@@ -86,6 +86,7 @@ pal cli status # check your setup
86
86
  | `pal cli usage` | Summarize token usage and estimated cost |
87
87
  | `pal cli knowledge` | Query & manage the knowledge store (search, graph, stats, hubs, find, show, add, ls, ingest) |
88
88
  | `pal cli skill link <name>` | Link a personal `~/.pal/skills/<name>/` into every installed agent so it is discoverable |
89
+ | `pal cli skill doctor <name>` | Evaluate a skill against the authoring best practices (folder/file-name match, name, description, body length, point-of-view, reference depth) |
89
90
 
90
91
  ### Target flags
91
92
 
@@ -0,0 +1,102 @@
1
+ # PAL Status Line
2
+
3
+ A customizable status line for Claude Code that displays context usage, active model, session cost, current directory, and git branch.
4
+
5
+ ## What It Shows
6
+
7
+ **Line 1:** Model name, current directory, git branch, hook health, open ISCs, signal trend
8
+ **Line 2:** Context usage progress bar, percentage used, session cost, remaining context, rate limits
9
+ **Line 3:** Update notice (only when a new PAL version is available)
10
+
11
+ Example output (macOS/Linux):
12
+ ```
13
+ [Opus] ๐Ÿ“ portable-agent-layer ๐ŸŒฟ main ๐Ÿ“‹ 21 open ISCs sig: 3.0/10 โ†“
14
+ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ 45% โ”‚ $0.12 โ”‚ 55% free
15
+ ```
16
+
17
+ Example output (Windows):
18
+ ```
19
+ [Opus] folder: portable-agent-layer (git: main) 21 open ISCs sig: 3.0/10 v
20
+ ########------------ 45% - $0.12 - 55% free
21
+ ```
22
+
23
+ Colors adapt based on context usage:
24
+ - ๐ŸŸข Green: 0-60% used
25
+ - ๐ŸŸก Yellow: 60-80% used
26
+ - ๐Ÿ”ด Red: >80% used
27
+
28
+ ## Setup
29
+
30
+ ### On macOS / Linux
31
+
32
+ 1. Make the script executable:
33
+ ```bash
34
+ chmod +x ~/.claude/statusline.sh
35
+ ```
36
+
37
+ 2. Add to `~/.claude/settings.json`:
38
+ ```json
39
+ {
40
+ "statusLine": {
41
+ "type": "command",
42
+ "command": "~/.claude/statusline.sh",
43
+ "padding": 2
44
+ }
45
+ }
46
+ ```
47
+
48
+ The statusline script is at: `portable-agent-layer/assets/statusline.sh`
49
+
50
+ ### On Windows
51
+
52
+ 1. Add to `~/.claude/settings.json`:
53
+ ```json
54
+ {
55
+ "statusLine": {
56
+ "type": "command",
57
+ "command": "powershell -NoProfile -Command \"Get-Content -Raw | & 'C:\\path\\to\\statusline.ps1'\"",
58
+ "padding": 2
59
+ }
60
+ }
61
+ ```
62
+
63
+ Or copy `statusline.ps1` to `~/.claude/statusline.ps1` and reference it from settings.
64
+
65
+ The statusline script is at: `portable-agent-layer/assets/statusline.ps1`
66
+
67
+ ## Data Displayed
68
+
69
+ - **Model:** Active Claude model (Opus, Sonnet, Haiku, etc.)
70
+ - **Directory:** Repository name (if in a git repo) or folder name
71
+ - **Git Branch:** Current branch, fetched from git worktree or `git` command
72
+ - **Hook Health:** Count of ERROR lines in `~/.pal/memory/state/debug.log` from last 24h (hidden when 0)
73
+ - **Open ISCs:** Count of unchecked ISCs for the current project (hidden when 0)
74
+ - **Signal Trend:** Session quality score from `~/.pal/memory/state/signal-cache.json` with trend arrow
75
+ - **Context Bar:** Visual progress bar showing token usage
76
+ - **Context %:** Percentage of context window used
77
+ - **Cost:** Estimated session cost in USD (shows "free" if <$0.01 on macOS/Linux, `$0.00` on Windows)
78
+ - **Remaining %:** Percentage of context window remaining
79
+ - **Rate Limits:** 5h and 7d usage percentages (Pro/Max plans only โ€” hidden on other plans)
80
+ - **Update Notice:** Shown on line 3 when `~/.pal/memory/state/update-available.json` flags a new version
81
+
82
+ ## Dependencies
83
+
84
+ - **macOS/Linux:** `bash`, `jq`, `git` (for branch detection)
85
+ - **Windows:** PowerShell 7+, `git` (for branch detection)
86
+
87
+ ## Customization
88
+
89
+ Both scripts use `jq` to parse the JSON data Claude Code sends. You can modify them to:
90
+ - Add or remove fields
91
+ - Change colors (ANSI escape codes)
92
+ - Adjust progress bar width
93
+ - Show additional data like effort level, rate limits, or PR status
94
+
95
+ Available fields are documented in the Claude Code statusline reference:
96
+ https://code.claude.com/docs/en/statusline#available-data
97
+
98
+ ## Updates
99
+
100
+ The status line runs after each assistant message, after `/compact`, and when vim mode toggles. It does not consume API tokens.
101
+
102
+ Changes to the script appear on your next interaction with Claude Code.
@@ -57,7 +57,12 @@ The `description` should state **both what the skill does and when to invoke it*
57
57
  pal cli skill link <name>
58
58
  ```
59
59
  This creates the per-skill discovery symlink in each installed agent's skills directory (Claude Code, Cursor, Copilot, Codex); opencode discovers it automatically via `~/.pal/skills/`.
60
- 6. Validate before declaring done:
60
+ 6. Run the doctor and resolve every error it reports:
61
+ ```bash
62
+ pal cli skill doctor <name>
63
+ ```
64
+ It checks the mechanical rules (folder/file-name match, name length/charset, description length, point-of-view, body length, reference depth). Fix all `โœ—` errors; weigh each `โš ` warning. A name/folder mismatch or a misnamed file makes the skill silently fail to load, so never skip this.
65
+ 7. Validate the rest by hand โ€” the doctor can't judge these:
61
66
  - **Trigger clarity** โ€” could a model decide *not* to invoke this from the description alone? If so, tighten it.
62
67
  - **Step concreteness** โ€” every step has a verb and an object.
63
68
  - **Output specification** โ€” the caller knows what they get back.
@@ -0,0 +1,142 @@
1
+ # PAL Status Line - Windows PowerShell version
2
+ # Reads JSON from stdin (Claude Code session data) and prints a formatted status line
3
+
4
+ $data = $input | Out-String | ConvertFrom-Json
5
+
6
+ # Extract data with fallbacks (PowerShell 5.1 compatible)
7
+ $MODEL = if ($data.model.display_name) { $data.model.display_name } else { "Unknown" }
8
+ $USED_RAW = $data.context_window.used_percentage
9
+ $USED = if ($USED_RAW -ne $null) { [int]$USED_RAW } else { 0 }
10
+ $REM_RAW = $data.context_window.remaining_percentage
11
+ $REM = if ($REM_RAW -ne $null) { [int]$REM_RAW } else { 0 }
12
+ $COST_RAW = $data.cost.total_cost_usd
13
+ $COST = if ($COST_RAW -ne $null) { [double]$COST_RAW } else { 0 }
14
+ $CWD = if ($data.workspace.current_dir) { $data.workspace.current_dir } else { "~" }
15
+ $REPO = if ($data.workspace.repo.name) { $data.workspace.repo.name } else { "" }
16
+
17
+ # Extract git branch
18
+ $BRANCH = ""
19
+ try { $BRANCH = & git -C $CWD rev-parse --abbrev-ref HEAD 2>$null } catch { $BRANCH = "" }
20
+
21
+ # Format directory
22
+ $DIR_DISPLAY = if ($REPO) { $REPO } else { Split-Path -Leaf $CWD }
23
+
24
+ # Format git branch indicator
25
+ $GIT_INDICATOR = if ($BRANCH) { " (git: $BRANCH)" } else { "" }
26
+
27
+ # Format cost
28
+ $COST_STR = '$' + ([math]::Round($COST, 2)).ToString("0.00")
29
+
30
+ # PAL: Hook health - count ERROR lines in debug.log from last 24h
31
+ $HOOK_ERRORS = 0
32
+ $debugLog = Join-Path $env:USERPROFILE ".pal\memory\state\debug.log"
33
+ if (Test-Path $debugLog) {
34
+ $cutoff = (Get-Date).AddHours(-24)
35
+ try {
36
+ $HOOK_ERRORS = (Get-Content $debugLog -ErrorAction SilentlyContinue |
37
+ Where-Object { $_ -match '\] ERROR ' } |
38
+ Where-Object {
39
+ $m = [regex]::Match($_, '^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]')
40
+ if ($m.Success) { [datetime]$m.Groups[1].Value -gt $cutoff } else { $false }
41
+ }).Count
42
+ } catch { $HOOK_ERRORS = 0 }
43
+ }
44
+
45
+ # PAL: Open ISC count - find project matching current directory
46
+ $OPEN_ISCS = 0
47
+ $PROJECT_NAME = ""
48
+ $projectsDir = Join-Path $env:USERPROFILE ".pal\memory\projects"
49
+ if (Test-Path $projectsDir) {
50
+ try {
51
+ $CWD_NORM = $CWD.Replace('/', '\').TrimEnd('\')
52
+ foreach ($slug in Get-ChildItem $projectsDir -Directory | Select-Object -ExpandProperty Name) {
53
+ $isa = Join-Path $projectsDir "$slug\ISA.md"
54
+ if (-not (Test-Path $isa)) { continue }
55
+ $content = Get-Content $isa -Raw -ErrorAction SilentlyContinue
56
+ $pathMatch = [regex]::Match($content, 'path:\s*"?([^"\n\r]+)"?')
57
+ if ($pathMatch.Success) {
58
+ $projPath = $pathMatch.Groups[1].Value.Trim().Replace('/', '\').TrimEnd('\')
59
+ if ($projPath -eq $CWD_NORM -or $CWD_NORM.StartsWith($projPath)) {
60
+ $PROJECT_NAME = $slug
61
+ $OPEN_ISCS = ([regex]::Matches($content, '- \[ \] ISC-')).Count
62
+ break
63
+ }
64
+ }
65
+ }
66
+ } catch { $OPEN_ISCS = 0 }
67
+ }
68
+
69
+ # Rate limits (Pro/Max only - absent for other plans)
70
+ $FIVE_H_RAW = $data.rate_limits.five_hour.used_percentage
71
+ $SEVEN_D_RAW = $data.rate_limits.seven_day.used_percentage
72
+ $RATE_PARTS = @()
73
+ if ($FIVE_H_RAW -ne $null) { $RATE_PARTS += "5h: $([int]$FIVE_H_RAW)%" }
74
+ if ($SEVEN_D_RAW -ne $null) { $RATE_PARTS += "7d: $([int]$SEVEN_D_RAW)%" }
75
+ $RATE_STR = if ($RATE_PARTS.Count -gt 0) { " - " + ($RATE_PARTS -join " | ") } else { "" }
76
+
77
+ # Create context progress bar - if both are 0, data not yet available (pre-first API call)
78
+ $NO_DATA = ($USED_RAW -eq $null -and $REM_RAW -eq $null)
79
+ if ($NO_DATA) { $USED = 0; $REM = 100 }
80
+ $FILLED = [int]($USED / 5)
81
+ $EMPTY = 20 - $FILLED
82
+ $BAR = ("#" * $FILLED) + ("-" * $EMPTY)
83
+
84
+ # ANSI color codes (PowerShell 5.1 compatible - use concatenation to avoid array-index expansion)
85
+ $ESC = [char]27
86
+ $CYAN = $ESC + "[36m"
87
+ $GREEN = $ESC + "[32m"
88
+ $YELLOW = $ESC + "[33m"
89
+ $RED = $ESC + "[31m"
90
+ $DIM = $ESC + "[90m"
91
+ $RESET = $ESC + "[0m"
92
+
93
+ # Choose bar color based on context usage
94
+ $BAR_COLOR = if ($USED -gt 80) { $RED } elseif ($USED -gt 60) { $YELLOW } else { $GREEN }
95
+
96
+ # PAL: Signal trend (reads 10-min cache written by session intelligence)
97
+ $SIGNAL_STR = ""
98
+ $signalCache = Join-Path $env:USERPROFILE ".pal\memory\state\signal-cache.json"
99
+ if (Test-Path $signalCache) {
100
+ try {
101
+ $sc = Get-Content $signalCache -Raw -ErrorAction SilentlyContinue | ConvertFrom-Json
102
+ if ($sc.today -ne $null) {
103
+ $ARROW = switch ($sc.trend) {
104
+ "up" { "^"; break }
105
+ "down" { "v"; break }
106
+ "stable" { "-"; break }
107
+ default { "?"; break }
108
+ }
109
+ $SIG_TODAY = ([math]::Round([double]$sc.today, 1)).ToString("0.0")
110
+ $SIG_COLOR = if ($sc.trend -eq "up") { $GREEN } elseif ($sc.trend -eq "down") { $RED } else { $DIM }
111
+ $SIGNAL_STR = " " + $SIG_COLOR + "sig: $SIG_TODAY/10 $ARROW" + $RESET
112
+ }
113
+ } catch {}
114
+ }
115
+
116
+ # PAL: Update available check (reads cached result, no network call)
117
+ # Emoji constructed at runtime to avoid PS5.1 UTF-8-without-BOM encoding issues
118
+ $UPDATE_LINE = ""
119
+ $updateCache = Join-Path $env:USERPROFILE ".pal\memory\state\update-available.json"
120
+ if (Test-Path $updateCache) {
121
+ try {
122
+ $uc = Get-Content $updateCache -Raw -ErrorAction SilentlyContinue | ConvertFrom-Json
123
+ if ($uc.available -eq $true) {
124
+ $versionStr = if ($uc.current -ne $uc.latest) { "$($uc.current) -> $($uc.latest)" } else { $uc.current + " (new commits)" }
125
+ $UPDATE_LINE = "[update] $versionStr run: pal cli update"
126
+ }
127
+ } catch {}
128
+ }
129
+
130
+ # Build PAL indicators
131
+ $HOOK_STR = if ($HOOK_ERRORS -gt 0) { " " + $RED + "! $HOOK_ERRORS hook err" + $RESET } else { "" }
132
+ $ISC_STR = if ($OPEN_ISCS -gt 0) { " " + $DIM + "$OPEN_ISCS open ISCs" + $RESET } else { "" }
133
+
134
+ # Line 1: Model, Directory, Git Branch, Hook Health, Open ISCs
135
+ Write-Host ($CYAN + "[$MODEL]" + $RESET + " folder: $DIR_DISPLAY" + $DIM + $GIT_INDICATOR + $RESET + $HOOK_STR + $ISC_STR + $SIGNAL_STR)
136
+
137
+ # Line 2: Context bar with usage percentage, cost, and rate limits
138
+ $CTX_STR = "$USED% - $COST_STR - ${REM}% free"
139
+ Write-Host ($BAR_COLOR + $BAR + $RESET + " " + $CTX_STR + $DIM + $RATE_STR + $RESET)
140
+
141
+ # Line 3: Update available (only when cache says available AND versions differ)
142
+ if ($UPDATE_LINE) { Write-Host ($YELLOW + $UPDATE_LINE + $RESET) }
@@ -0,0 +1,167 @@
1
+ #!/bin/bash
2
+ # PAL Status Line โ€” macOS/Linux
3
+ # Reads JSON from stdin (Claude Code session data) and prints a formatted status line
4
+
5
+ input=$(cat)
6
+
7
+ # Extract data with fallbacks
8
+ MODEL=$(echo "$input" | jq -r '.model.display_name // "Unknown"')
9
+ USED_RAW=$(echo "$input" | jq -r '.context_window.used_percentage // empty')
10
+ REM_RAW=$(echo "$input" | jq -r '.context_window.remaining_percentage // empty')
11
+ USED=$( [ -n "$USED_RAW" ] && echo "${USED_RAW%.*}" || echo 0 )
12
+ REMAINING=$( [ -n "$REM_RAW" ] && echo "${REM_RAW%.*}" || echo 0 )
13
+ COST=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')
14
+ CWD=$(echo "$input" | jq -r '.workspace.current_dir // "~"')
15
+ REPO=$(echo "$input" | jq -r '.workspace.repo.name // ""')
16
+
17
+ # No data yet (pre-first API call)
18
+ if [ -z "$USED_RAW" ] && [ -z "$REM_RAW" ]; then
19
+ USED=0; REMAINING=100
20
+ fi
21
+
22
+ # Extract git branch โ€” try multiple sources for robustness
23
+ BRANCH=$(echo "$input" | jq -r '.worktree.branch // ""')
24
+ if [ -z "$BRANCH" ]; then
25
+ BRANCH=$(git -C "$CWD" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
26
+ fi
27
+
28
+ # Format directory โ€” show repo name if available, else just folder name
29
+ if [ -n "$REPO" ]; then
30
+ DIR_DISPLAY="$REPO"
31
+ else
32
+ DIR_DISPLAY="${CWD##*/}"
33
+ fi
34
+
35
+ # Format git branch with emoji indicator
36
+ if [ -n "$BRANCH" ]; then
37
+ GIT_INDICATOR=" (๐ŸŒฟ ${BRANCH})"
38
+ else
39
+ GIT_INDICATOR=""
40
+ fi
41
+
42
+ # Format cost โ€” show as $X.XX or 'free' if < $0.01
43
+ if (( $(echo "$COST < 0.01" | bc -l) )); then
44
+ COST_STR="free"
45
+ else
46
+ COST_STR=$(printf '$%.2f' "$COST")
47
+ fi
48
+
49
+ # PAL: Hook health โ€” count ERROR lines in debug.log from last 24h
50
+ HOOK_ERRORS=0
51
+ DEBUG_LOG="$HOME/.pal/memory/state/debug.log"
52
+ if [ -f "$DEBUG_LOG" ]; then
53
+ # Cross-platform 24h cutoff: macOS uses date -v, GNU Linux uses date -d
54
+ CUTOFF=$(date -v-24H "+%Y-%m-%d %H:%M:%S" 2>/dev/null || date -d '24 hours ago' "+%Y-%m-%d %H:%M:%S" 2>/dev/null)
55
+ if [ -n "$CUTOFF" ]; then
56
+ # Log format: [YYYY-MM-DD HH:MM:SS] LEVEL ... โ€” split on [ or ] to get $2 = timestamp
57
+ HOOK_ERRORS=$(grep '\] ERROR ' "$DEBUG_LOG" | awk -F'[][]]' '$2 > "'"$CUTOFF"'"' | wc -l | tr -d ' ')
58
+ else
59
+ HOOK_ERRORS=$(grep -c '\] ERROR ' "$DEBUG_LOG" 2>/dev/null || echo 0)
60
+ fi
61
+ fi
62
+
63
+ # PAL: Open ISC count โ€” find project matching current directory
64
+ OPEN_ISCS=0
65
+ PROJECTS_DIR="$HOME/.pal/memory/projects"
66
+ if [ -d "$PROJECTS_DIR" ]; then
67
+ CWD_NORM="${CWD%/}"
68
+ for ISA in "$PROJECTS_DIR"/*/ISA.md; do
69
+ [ -f "$ISA" ] || continue
70
+ PROJ_PATH=$(grep -m1 'path:' "$ISA" | sed 's/.*path:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs)
71
+ PROJ_PATH="${PROJ_PATH%/}"
72
+ if [ "$PROJ_PATH" = "$CWD_NORM" ] || [[ "$CWD_NORM" == "${PROJ_PATH}/"* ]]; then
73
+ OPEN_ISCS=$(grep -c '- \[ \] ISC-' "$ISA" 2>/dev/null || echo 0)
74
+ break
75
+ fi
76
+ done
77
+ fi
78
+
79
+ # Rate limits (Pro/Max only โ€” absent for other plans)
80
+ FIVE_H=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')
81
+ SEVEN_D=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty')
82
+ RATE_PARTS=()
83
+ [ -n "$FIVE_H" ] && RATE_PARTS+=("5h: ${FIVE_H%.*}%")
84
+ [ -n "$SEVEN_D" ] && RATE_PARTS+=("7d: ${SEVEN_D%.*}%")
85
+ RATE_STR=""
86
+ if [ ${#RATE_PARTS[@]} -gt 0 ]; then
87
+ RATE_STR=" โ”‚ $(IFS=" | "; echo "${RATE_PARTS[*]}")"
88
+ fi
89
+
90
+ # Create context progress bar (20 chars wide)
91
+ FILLED=$((USED / 5))
92
+ EMPTY=$((20 - FILLED))
93
+ if [ "$FILLED" -le 0 ]; then
94
+ BAR="โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘"
95
+ elif [ "$FILLED" -ge 20 ]; then
96
+ BAR="โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ"
97
+ else
98
+ BAR=$(printf 'โ–ˆ%.0s' $(seq 1 $FILLED))$(printf 'โ–‘%.0s' $(seq 1 $EMPTY))
99
+ fi
100
+
101
+ # Color codes (ANSI)
102
+ CYAN='\033[36m'
103
+ GREEN='\033[32m'
104
+ YELLOW='\033[33m'
105
+ RED='\033[31m'
106
+ DIM='\033[90m'
107
+ RESET='\033[0m'
108
+
109
+ # Choose bar color based on context usage
110
+ if [ "$USED" -gt 80 ]; then
111
+ CONTEXT_COLOR=$RED
112
+ elif [ "$USED" -gt 60 ]; then
113
+ CONTEXT_COLOR=$YELLOW
114
+ else
115
+ CONTEXT_COLOR=$GREEN
116
+ fi
117
+
118
+ # PAL: Signal trend (reads 10-min cache written by session intelligence)
119
+ SIGNAL_STR=""
120
+ SIGNAL_CACHE="$HOME/.pal/memory/state/signal-cache.json"
121
+ if [ -f "$SIGNAL_CACHE" ]; then
122
+ SC_TODAY=$(jq -r '.today // empty' "$SIGNAL_CACHE" 2>/dev/null)
123
+ SC_TREND=$(jq -r '.trend // empty' "$SIGNAL_CACHE" 2>/dev/null)
124
+ if [ -n "$SC_TODAY" ]; then
125
+ case "$SC_TREND" in
126
+ up) ARROW="โ†‘"; SIG_COLOR=$GREEN ;;
127
+ down) ARROW="โ†“"; SIG_COLOR=$RED ;;
128
+ stable) ARROW="โ†’"; SIG_COLOR=$DIM ;;
129
+ *) ARROW="?"; SIG_COLOR=$DIM ;;
130
+ esac
131
+ SIG_TODAY=$(printf "%.1f" "$SC_TODAY")
132
+ SIGNAL_STR=" ${SIG_COLOR}sig: ${SIG_TODAY}/10 ${ARROW}${RESET}"
133
+ fi
134
+ fi
135
+
136
+ # PAL: Update available check (reads cached result, no network call)
137
+ UPDATE_LINE=""
138
+ UPDATE_CACHE="$HOME/.pal/memory/state/update-available.json"
139
+ if [ -f "$UPDATE_CACHE" ]; then
140
+ UC_AVAIL=$(jq -r '.available // false' "$UPDATE_CACHE" 2>/dev/null)
141
+ if [ "$UC_AVAIL" = "true" ]; then
142
+ UC_CURRENT=$(jq -r '.current // ""' "$UPDATE_CACHE" 2>/dev/null)
143
+ UC_LATEST=$(jq -r '.latest // ""' "$UPDATE_CACHE" 2>/dev/null)
144
+ if [ "$UC_CURRENT" != "$UC_LATEST" ]; then
145
+ VERSION_STR="$UC_CURRENT โ†’ $UC_LATEST"
146
+ else
147
+ VERSION_STR="$UC_CURRENT (new commits)"
148
+ fi
149
+ UPDATE_LINE="๐Ÿ“ฆ update: $VERSION_STR run: pal cli update"
150
+ fi
151
+ fi
152
+
153
+ # Build PAL indicators
154
+ HOOK_STR=""
155
+ [ "$HOOK_ERRORS" -gt 0 ] && HOOK_STR=" ${RED}โš ๏ธ ${HOOK_ERRORS} hook err${RESET}"
156
+ ISC_STR=""
157
+ [ "$OPEN_ISCS" -gt 0 ] && ISC_STR=" ${DIM}๐Ÿ“‹ ${OPEN_ISCS} open ISCs${RESET}"
158
+
159
+ # Line 1: Model, Directory, Git Branch, Hook Health, Open ISCs, Signal
160
+ echo -e "${CYAN}[${MODEL}]${RESET} ๐Ÿ“ ${DIR_DISPLAY}${DIM}${GIT_INDICATOR}${RESET}${HOOK_STR}${ISC_STR}${SIGNAL_STR}"
161
+
162
+ # Line 2: Context bar with usage percentage, cost, and rate limits
163
+ echo -e "${CONTEXT_COLOR}${BAR}${RESET} ${USED}% โ”‚ ${COST_STR} โ”‚ ${REMAINING}% free${DIM}${RATE_STR}${RESET}"
164
+
165
+ # Line 3: Update available (only when cache says available AND versions differ)
166
+ [ -n "$UPDATE_LINE" ] && echo -e "${YELLOW}${UPDATE_LINE}${RESET}"
167
+ exit 0
@@ -165,6 +165,18 @@ Refine criteria if the pressure test reveals gaps. Add criteria for uncovered fa
165
165
  - Decide execution order โ€” what's serial, what can parallelize
166
166
  - **Extended+:** use EnterPlanMode for user alignment before executing
167
167
 
168
+ **Grounding Gate โ€” verify the premises before EXECUTE.**
169
+
170
+ Don't just *list* what must be true โ€” actively confirm the load-bearing facts the criteria rest on. For each premise (a reported behavior, a provider/API contract, an external tool's CLI, a file's contents, a claim about system state):
171
+ - **Reproduce** the reported behavior before designing a fix โ€” no fix on an unreproduced bug.
172
+ - **Fetch the authoritative source** (live docs, the actual file, `gh api`) before asserting how something works.
173
+ - **Probe the dependency/contract** (tool availability, MCP account, env var, DB connectivity, exact CLI) before building on it.
174
+ - **Confirm one concrete artifact** (a commit, a shebang, a line of output) behind any external claim.
175
+
176
+ No criterion may rest on an unverified premise. If a premise can't be verified now, mark it explicitly as an assumption and add a criterion to check it during EXECUTE. After 2 failed attempts at the same sub-problem, stop and re-ground โ€” re-run OBSERVE rather than iterating on a bad premise.
177
+
178
+ Output: `๐Ÿงญ GROUNDING: [premises verified, or assumptions flagged]`
179
+
168
180
  ### โ”โ”โ” โšก EXECUTE โ”โ”โ” 3/5
169
181
 
170
182
  Do the work. Invoke selected capabilities via tool calls.
@@ -217,9 +229,11 @@ Focus: reasoning approach, problem decomposition, anticipation, blind spots.
217
229
 
218
230
  ```bash
219
231
  bun ~/.pal/tools/algorithm-reflect.ts --task "description" --criteria N --passed N --failed N --sentiment 1-10 \
220
- --q1 "self reflection" --q2 "algorithm reflection" --q3 "AI reflection"
232
+ --q1 "self reflection" --q2 "algorithm reflection" --q3 "AI reflection" --scope general
221
233
  ```
222
234
 
235
+ Set `--scope task-specific` when the Q2 idea is bound to this one task (e.g. a criterion that only matters for the specific file, API, or dataset you were handling) and would not generalize to the algorithm; use `general` (the default) for reusable structural improvements. This keeps the algorithm-update synthesis focused on changes worth folding into ALGORITHM.md.
236
+
223
237
  **3. Relationship note** โ€” write one Session entry capturing what was done this session:
224
238
 
225
239
  ```bash
@@ -316,6 +330,7 @@ Only write if the insight is **genuine and reusable** โ€” not every session prod
316
330
  โ”โ”โ” ๐Ÿง  PLAN โ”โ”โ” 2/5
317
331
  ๐Ÿง  RISKS: [risks]
318
332
  ๐Ÿง  PREMORTEM: [failure modes]
333
+ ๐Ÿงญ GROUNDING: [premises verified, or assumptions flagged]
319
334
  ๐Ÿ“ APPROACH: [execution plan]
320
335
 
321
336
  โ”โ”โ” โšก EXECUTE โ”โ”โ” 3/5
@@ -25,6 +25,27 @@
25
25
  "Bash(node --experimental-strip-types ~/.pal/skills/consulting-report/tools/generate-pdf.ts *)"
26
26
  ]
27
27
  },
28
+ "skillOverrides": {
29
+ "claude-api": "off",
30
+ "update-config": "off",
31
+ "deep-research": "off",
32
+ "code-review": "off"
33
+ },
34
+ "attribution": {
35
+ "commit": "",
36
+ "pr": ""
37
+ },
38
+ "showClearContextOnPlanAccept": true,
39
+ "respectGitignore": true,
40
+ "spinnerTipsEnabled": true,
41
+ "spinnerTipsOverride": {
42
+ "tips": [
43
+ "Need something reusable? Use the /create-skill command.",
44
+ "PAL learns from your feedback and improves over time. Don't be shy to give feedback!",
45
+ "For simple PDFs use the /create-pdf skill. Need a styled one? Use /consulting-report instead.",
46
+ "Use the /research skill to run comprehensive web research with multiple parallel agents."
47
+ ]
48
+ },
28
49
  "hooks": {
29
50
  "SessionStart": [
30
51
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.47.0",
3
+ "version": "0.49.0",
4
4
  "description": "PAL โ€” Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli/index.ts CHANGED
@@ -17,6 +17,7 @@
17
17
  * doctor Check prerequisites and system health
18
18
  * usage Summarize token usage and cost
19
19
  * skill link <name> Link a personal ~/.pal/skills/<name>/ into installed agents
20
+ * skill doctor <name> Evaluate a skill against the authoring best practices
20
21
  */
21
22
 
22
23
  import { spawnSync } from "node:child_process";
@@ -242,6 +243,7 @@ function showHelp() {
242
243
  pal cli knowledge <sub> [args] Query & manage the knowledge store
243
244
  (search ยท graph ยท stats ยท hubs ยท find ยท show ยท add ยท ls)
244
245
  pal cli skill link <name> Link a personal ~/.pal/skills/<name>/ into installed agents
246
+ pal cli skill doctor <name> Evaluate a skill against the authoring best practices
245
247
 
246
248
  Environment:
247
249
  PAL_HOME Override user state directory (default: ~/.pal or repo root)
package/src/cli/skill.ts CHANGED
@@ -1,15 +1,30 @@
1
1
  /**
2
2
  * pal cli skill โ€” manage personal skills under ~/.pal/skills/.
3
3
  *
4
- * pal cli skill link <name> Link an existing ~/.pal/skills/<name>/ into
5
- * every installed agent so it is discoverable.
4
+ * pal cli skill link <name> Link an existing ~/.pal/skills/<name>/ into
5
+ * every installed agent so it is discoverable.
6
+ * pal cli skill doctor <name> Evaluate ~/.pal/skills/<name>/ against the
7
+ * skill-authoring best practices.
6
8
  */
7
9
 
10
+ import { resolve } from "node:path";
11
+ import { palHome } from "../hooks/lib/paths";
8
12
  import { linkPersonalSkill, log } from "../targets/lib";
13
+ import { formatReport, lintSkill } from "../tools/skill-doctor";
9
14
 
10
15
  export async function runSkill(args: string[]): Promise<number> {
11
16
  const [sub, name] = args;
12
17
 
18
+ if (sub === "doctor") {
19
+ if (!name) {
20
+ log.error("Usage: pal cli skill doctor <name>");
21
+ return 1;
22
+ }
23
+ const report = lintSkill(resolve(palHome(), "skills", name));
24
+ console.log(formatReport(report));
25
+ return report.errors > 0 ? 1 : 0;
26
+ }
27
+
13
28
  if (sub === "link") {
14
29
  if (!name) {
15
30
  log.error("Usage: pal cli skill link <name>");
@@ -35,6 +50,6 @@ export async function runSkill(args: string[]): Promise<number> {
35
50
  }
36
51
  }
37
52
 
38
- log.error("Usage: pal cli skill link <name>");
53
+ log.error("Usage: pal cli skill <link|doctor> <name>");
39
54
  return 1;
40
55
  }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Algorithm-review cadence โ€” decides when to nudge the MAINTAINER to run an
3
+ * `/algorithm-update` session (fold accumulated Q2 reflections into ALGORITHM.md).
4
+ *
5
+ * Maintainer-only by construction: the nudge fires solely in a PAL repo checkout,
6
+ * detected by the presence of the repo-only `.agents/skills/algorithm-update`
7
+ * skill. That path never ships to downstream installs โ€” package.json `files`
8
+ * lists only `src/` and `assets/`, so `.agents/` is absent everywhere except a
9
+ * source checkout. Downstream users therefore never see this nudge.
10
+ */
11
+
12
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { resolve } from "node:path";
14
+ import { readReflections } from "./learning-store";
15
+ import { palPkg, paths } from "./paths";
16
+
17
+ const NUDGE_MIN_COUNT = 25;
18
+ const NUDGE_MIN_DAYS = 7;
19
+ const DAY_MS = 86_400_000;
20
+
21
+ function markFile(): string {
22
+ return resolve(paths.state(), "algorithm-review.json");
23
+ }
24
+
25
+ export function readReviewMark(): string | null {
26
+ try {
27
+ const parsed = JSON.parse(readFileSync(markFile(), "utf-8"));
28
+ return typeof parsed.lastReviewedTs === "string" ? parsed.lastReviewedTs : null;
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ export function writeReviewMark(ts: string): void {
35
+ writeFileSync(markFile(), `${JSON.stringify({ lastReviewedTs: ts }, null, 2)}\n`);
36
+ }
37
+
38
+ /** True only in a maintainer repo checkout โ€” the repo-only update skill is present. */
39
+ export function isMaintainerEnv(): boolean {
40
+ return existsSync(
41
+ resolve(palPkg(), ".agents", "skills", "algorithm-update", "SKILL.md")
42
+ );
43
+ }
44
+
45
+ /** Count reflections newer than the last review mark (all of them, if never reviewed). */
46
+ export function countUnreviewed(mark: string | null = readReviewMark()): number {
47
+ const reflections = readReflections(paths.reflectionsFile());
48
+ if (!mark) return reflections.length;
49
+ const cutoff = new Date(mark).getTime();
50
+ return reflections.filter((r) => r.ts && new Date(r.ts).getTime() > cutoff).length;
51
+ }
52
+
53
+ export interface ReviewNudge {
54
+ count: number;
55
+ sinceDays: number;
56
+ since: string | null;
57
+ }
58
+
59
+ /**
60
+ * Nudge data when BOTH thresholds are met (โ‰ฅ25 new AND โ‰ฅ7 days) in a maintainer
61
+ * env, else null. `now` is injected for testability.
62
+ */
63
+ export function algorithmReviewNudge(now: Date): ReviewNudge | null {
64
+ if (!isMaintainerEnv()) return null;
65
+ const mark = readReviewMark();
66
+ const count = countUnreviewed(mark);
67
+ if (count < NUDGE_MIN_COUNT) return null;
68
+ const sinceDays = mark
69
+ ? Math.floor((now.getTime() - new Date(mark).getTime()) / DAY_MS)
70
+ : Number.POSITIVE_INFINITY;
71
+ if (sinceDays < NUDGE_MIN_DAYS) return null;
72
+ return { count, sinceDays, since: mark };
73
+ }
74
+
75
+ /** Formatted nudge section for the session reminder, or "" when it shouldn't fire. */
76
+ export function loadAlgorithmReviewNudge(now: Date = new Date()): string {
77
+ const n = algorithmReviewNudge(now);
78
+ if (!n) return "";
79
+ const since = n.since ? new Date(n.since).toISOString().slice(0, 10) : "the start";
80
+ const age = Number.isFinite(n.sinceDays) ? `, ${n.sinceDays}d` : "";
81
+ return [
82
+ "## Algorithm Review Due",
83
+ `๐Ÿ”ง ${n.count} new algorithm reflections since ${since}${age} โ€” run \`/algorithm-update\` to fold the recurring ones into ALGORITHM.md.`,
84
+ ].join("\n");
85
+ }