@esoteric-logic/praxis-harness 2.9.0 → 2.10.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/base/CLAUDE.md CHANGED
@@ -56,6 +56,13 @@ Vault path and backend are machine-specific. Read from `~/.claude/praxis.config.
56
56
  If config file is missing: tell the user to run `praxis/install.sh`.
57
57
  All `{vault_path}` references in rules and skills resolve from this config.
58
58
 
59
+ ## Model & Context Policy
60
+ - Default model: `claude-opus-4-6` (set `ANTHROPIC_MODEL` in shell profile)
61
+ - Sub-agents spawned by Praxis skills: use `haiku` for polling, search, and lint tasks
62
+ - Compact trigger: when context approaches ceiling, finish the current milestone first
63
+ - Never compact mid-plan — complete the milestone, write phase summary to vault, then compact
64
+ - After compaction: re-bootstrap from § After Compaction below, re-run quality checks fresh
65
+
59
66
  ## Durable Memory
60
67
  Context is volatile. Files are permanent. Act accordingly.
61
68
 
@@ -164,6 +171,7 @@ Kit manifests live in `~/.claude/kits/<name>/KIT.md`.
164
171
  | `~/.claude/rules/powershell.md` | `**/*.ps1`, `**/*.psm1` |
165
172
  | `~/.claude/rules/dependency-freshness.md` | `package.json`, `go.mod`, `requirements.txt`, `Cargo.toml`, `pyproject.toml` |
166
173
  | `~/.claude/rules/live-docs-required.md` | Dependency manifests, files importing external packages |
174
+ | `~/.claude/rules/desktop-protocol.md` | Claude Desktop ↔ Claude Code handoff sessions |
167
175
 
168
176
  ### Auto-invocable skills (replace former universal rules)
169
177
  | Skill | Triggers when |
@@ -101,7 +101,8 @@
101
101
  {"path": "base/hooks/file-guard.sh", "event": "PreToolUse", "matcher": "Write|Edit|MultiEdit"},
102
102
  {"path": "base/hooks/identity-check.sh", "event": "PreToolUse", "matcher": "Bash"},
103
103
  {"path": "base/hooks/credential-guard.sh", "event": "PreToolUse", "matcher": "Bash"},
104
- {"path": "base/hooks/session-data-collect.sh", "event": "Stop", "matcher": ""}
104
+ {"path": "base/hooks/session-data-collect.sh", "event": "Stop", "matcher": ""},
105
+ {"path": "base/hooks/on-stop-failure.sh", "event": "StopFailure", "matcher": ""}
105
106
  ],
106
107
  "optional": [
107
108
  {"path": "base/hooks/recursion-guard.sh", "event": "PreToolUse", "matcher": "", "feature": "recursion-detection"},
@@ -2,6 +2,7 @@
2
2
  # PostToolUse hook — auto-formats files after edit.
3
3
  # Always exits 0 (advisory, never blocks).
4
4
  set -uo pipefail
5
+ trap 'exit 0' ERR
5
6
 
6
7
  INPUT=$(cat)
7
8
  FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty' 2>/dev/null)
@@ -2,7 +2,8 @@
2
2
  # dep-audit.sh — PostToolUse:Write|Edit|MultiEdit hook
3
3
  # Runs dependency vulnerability checks when manifest files are modified.
4
4
  # Always exits 0 (advisory only — PostToolUse cannot hard-block).
5
- set -euo pipefail
5
+ set -uo pipefail
6
+ trap 'exit 0' ERR
6
7
 
7
8
  INPUT=$(cat)
8
9
  FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty' 2>/dev/null)
@@ -20,28 +21,40 @@ case "$BASENAME" in
20
21
  package.json)
21
22
  ECOSYSTEM="npm"
22
23
  if command -v npm &>/dev/null && [[ -f "$DIR/package-lock.json" || -f "$DIR/node_modules/.package-lock.json" ]]; then
23
- AUDIT_RESULT=$(cd "$DIR" && npm audit --audit-level=high --json 2>/dev/null || true)
24
+ # npm audit exits non-zero when vulnerabilities exist — capture output regardless
25
+ AUDIT_RESULT=$(cd "$DIR" && npm audit --audit-level=high --json 2>/dev/null) || {
26
+ if [[ -n "$AUDIT_RESULT" ]]; then
27
+ : # non-zero with output = vulnerabilities found (expected)
28
+ else
29
+ echo "DEP-AUDIT (npm): audit command failed" >&2
30
+ fi
31
+ }
24
32
  fi
25
33
  ;;
26
34
  go.mod)
27
35
  ECOSYSTEM="go"
28
36
  if command -v govulncheck &>/dev/null; then
29
- AUDIT_RESULT=$(cd "$DIR" && govulncheck -json ./... 2>/dev/null || true)
37
+ AUDIT_RESULT=$(cd "$DIR" && govulncheck -json ./... 2>/dev/null) || {
38
+ [[ -z "$AUDIT_RESULT" ]] && echo "DEP-AUDIT (go): govulncheck failed" >&2
39
+ }
30
40
  elif command -v go &>/dev/null; then
31
- # Fallback: at least check for known issues via go list
32
- AUDIT_RESULT=$(cd "$DIR" && go list -m -json all 2>/dev/null | head -c 5000 || true)
41
+ AUDIT_RESULT=$(cd "$DIR" && go list -m -json all 2>/dev/null | head -c 5000) || AUDIT_RESULT=""
33
42
  fi
34
43
  ;;
35
44
  requirements.txt|pyproject.toml)
36
45
  ECOSYSTEM="python"
37
46
  if command -v pip-audit &>/dev/null; then
38
- AUDIT_RESULT=$(cd "$DIR" && pip-audit --format=json 2>/dev/null || true)
47
+ AUDIT_RESULT=$(cd "$DIR" && pip-audit --format=json 2>/dev/null) || {
48
+ [[ -z "$AUDIT_RESULT" ]] && echo "DEP-AUDIT (python): pip-audit failed" >&2
49
+ }
39
50
  fi
40
51
  ;;
41
52
  Cargo.toml)
42
53
  ECOSYSTEM="rust"
43
54
  if command -v cargo-audit &>/dev/null; then
44
- AUDIT_RESULT=$(cd "$DIR" && cargo audit --json 2>/dev/null || true)
55
+ AUDIT_RESULT=$(cd "$DIR" && cargo audit --json 2>/dev/null) || {
56
+ [[ -z "$AUDIT_RESULT" ]] && echo "DEP-AUDIT (rust): cargo-audit failed" >&2
57
+ }
45
58
  fi
46
59
  ;;
47
60
  *)
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env bash
2
+ # on-stop-failure.sh — StopFailure hook (Auto mode failure classifier)
3
+ # Reads Claude's stop context, classifies the failure, and either:
4
+ # - Auto-repairs (transient: test fail, lint fail, tool error) → exit 0 (continue)
5
+ # - Escalates to human (boundary/spec/security violation) → exit 1 (halt)
6
+ # Tracks repair attempts per failure fingerprint to prevent infinite loops.
7
+ # Reuses PPID-scoped state pattern from recursion-guard.sh.
8
+ set -euo pipefail
9
+
10
+ if ! command -v jq &>/dev/null; then
11
+ echo "on-stop-failure: jq required but not found. Cannot classify failure." >&2
12
+ exit 1
13
+ fi
14
+
15
+ # ── Parse input (single jq call) ──
16
+ INPUT=$(cat)
17
+ PARSED=$(echo "$INPUT" | jq -r '[
18
+ (.exit_code // 1 | tostring),
19
+ (.stop_reason // ""),
20
+ (.last_tool_use.name // ""),
21
+ (.last_tool_use.error // "")
22
+ ] | join("\t")' 2>/dev/null || echo "1\t\t\t")
23
+
24
+ IFS=$'\t' read -r EXIT_CODE STOP_REASON LAST_TOOL TOOL_ERROR <<< "$PARSED"
25
+
26
+ MAX_REPAIR_ATTEMPTS=3
27
+ MAX_FINGERPRINT_LEN=200
28
+
29
+ # ── Repair attempt tracker (PPID-scoped, same pattern as recursion-guard.sh) ──
30
+ REPAIR_STATE="/tmp/praxis-repair-${PPID}.json"
31
+ if [[ ! -f "$REPAIR_STATE" ]]; then
32
+ echo '{"repair_attempts":0,"last_fingerprint":""}' > "$REPAIR_STATE"
33
+ fi
34
+
35
+ # Compare raw fingerprint strings — no hash needed for equality checks
36
+ FINGERPRINT="${EXIT_CODE}:${LAST_TOOL}:${STOP_REASON:0:$MAX_FINGERPRINT_LEN}"
37
+
38
+ # Single jq call to read both fields
39
+ STATE_READ=$(jq -r '[(.last_fingerprint // ""), (.repair_attempts // 0 | tostring)] | join("\t")' "$REPAIR_STATE" 2>/dev/null || echo $'\t0')
40
+ IFS=$'\t' read -r LAST_FINGERPRINT CURRENT_ATTEMPTS <<< "$STATE_READ"
41
+
42
+ # Same fingerprint = looping on the same error
43
+ if [[ "$FINGERPRINT" == "$LAST_FINGERPRINT" ]]; then
44
+ CURRENT_ATTEMPTS=$((CURRENT_ATTEMPTS + 1))
45
+ else
46
+ CURRENT_ATTEMPTS=1
47
+ fi
48
+
49
+ # Atomic state file update (tmp + mv)
50
+ TMP_STATE="${REPAIR_STATE}.tmp"
51
+ jq -n --argjson attempts "$CURRENT_ATTEMPTS" --arg fp "$FINGERPRINT" \
52
+ '{"repair_attempts": $attempts, "last_fingerprint": $fp}' \
53
+ > "$TMP_STATE" && mv "$TMP_STATE" "$REPAIR_STATE"
54
+
55
+ # ── Hard escalation: too many repair attempts on same failure ──
56
+ if [[ $CURRENT_ATTEMPTS -gt $MAX_REPAIR_ATTEMPTS ]]; then
57
+ echo "AUTO-REPAIR EXHAUSTED: $CURRENT_ATTEMPTS attempts on same failure." >&2
58
+ echo "Failure: exit=$EXIT_CODE tool=$LAST_TOOL" >&2
59
+ echo "Halting — human review required." >&2
60
+
61
+ CONFIG="$HOME/.claude/praxis.config.json"
62
+ if [[ -f "$CONFIG" ]]; then
63
+ VAULT_PATH=$(jq -r '.vault_path // ""' "$CONFIG" 2>/dev/null)
64
+ if [[ -n "$VAULT_PATH" && -d "$VAULT_PATH" ]]; then
65
+ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
66
+ ESCALATION_FILE="$VAULT_PATH/notes/escalations.md"
67
+
68
+ if [[ ! -f "$ESCALATION_FILE" ]]; then
69
+ printf -- "---\ntags: [escalation, auto-repair]\ndate: %s\nsource: agent\n---\n\n# Auto-Repair Escalations\n" \
70
+ "$(date +%Y-%m-%d)" > "$ESCALATION_FILE"
71
+ fi
72
+
73
+ printf "\n## %s\n- **Failure**: exit=%s tool=%s\n- **Attempts**: %s\n- **Reason**: %s\n- **Status**: HALTED — needs human review\n" \
74
+ "$TIMESTAMP" "$EXIT_CODE" "$LAST_TOOL" "$CURRENT_ATTEMPTS" "${STOP_REASON:0:$MAX_FINGERPRINT_LEN}" \
75
+ >> "$ESCALATION_FILE"
76
+ fi
77
+ fi
78
+
79
+ exit 1
80
+ fi
81
+
82
+ # ── Hard escalation patterns (single regex, never auto-repair these) ──
83
+ HARD_REGEX="BLOCKED:|secret detected|Protected path|out of scope|permission denied|identity mismatch"
84
+ COMBINED_CONTEXT="$STOP_REASON $TOOL_ERROR"
85
+
86
+ if echo "$COMBINED_CONTEXT" | grep -qiE "$HARD_REGEX"; then
87
+ MATCHED=$(echo "$COMBINED_CONTEXT" | grep -oiE "$HARD_REGEX" | head -1)
88
+ echo "ESCALATING: Hard violation — '$MATCHED' matched." >&2
89
+ echo "Auto-repair suppressed. Human review required." >&2
90
+ exit 1
91
+ fi
92
+
93
+ # Exit code 2 = guard hard-block (recursion-guard.sh, secret-scan.sh, file-guard.sh convention)
94
+ if [[ "$EXIT_CODE" == "2" ]]; then
95
+ echo "ESCALATING: Exit code 2 = guard hard-block. No auto-repair." >&2
96
+ exit 1
97
+ fi
98
+
99
+ # ── Transient failure → emit repair prompt (Auto mode continues) ──
100
+ # Claude Code reads stdout from StopFailure hooks as an injected prompt.
101
+
102
+ cat <<REPAIR
103
+ Auto-repair attempt $CURRENT_ATTEMPTS of $MAX_REPAIR_ATTEMPTS.
104
+
105
+ Failure context:
106
+ - Exit code: $EXIT_CODE
107
+ - Last tool: $LAST_TOOL
108
+ - Stop reason: ${STOP_REASON:0:500}
109
+ - Tool error: ${TOOL_ERROR:0:$MAX_FINGERPRINT_LEN}
110
+
111
+ Instructions:
112
+ 1. Read the error above carefully. Identify the root cause from actual output.
113
+ 2. Apply the minimum fix required. Do not expand scope.
114
+ 3. Re-run validation (tests + lint) after fixing.
115
+ 4. If this is attempt $CURRENT_ATTEMPTS of $MAX_REPAIR_ATTEMPTS and the fix is not obvious:
116
+ report What / So What / Now What and halt.
117
+
118
+ Do NOT re-attempt the same approach that just failed.
119
+ REPAIR
120
+
121
+ exit 0
@@ -2,6 +2,7 @@
2
2
  # Stop hook — collects structured session data and stages it for the Stop prompt.
3
3
  # Always exits 0 (advisory, never blocks session end).
4
4
  set -uo pipefail
5
+ trap 'exit 0' ERR
5
6
 
6
7
  CONFIG_FILE="$HOME/.claude/praxis.config.json"
7
8
 
@@ -68,6 +68,17 @@
68
68
  ]
69
69
  }
70
70
  ],
71
+ "StopFailure": [
72
+ {
73
+ "matcher": "",
74
+ "hooks": [
75
+ {
76
+ "type": "command",
77
+ "command": "bash ~/.claude/hooks/on-stop-failure.sh"
78
+ }
79
+ ]
80
+ }
81
+ ],
71
82
  "PreCompact": [
72
83
  {
73
84
  "matcher": "",
@@ -2,6 +2,7 @@
2
2
  # PreCompact hook — writes minimal checkpoint to vault before context compaction.
3
3
  # Always exits 0 (advisory, never blocks compaction).
4
4
  set -uo pipefail
5
+ trap 'exit 0' ERR
5
6
 
6
7
  CONFIG_FILE="$HOME/.claude/praxis.config.json"
7
8
 
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env bash
2
+ # ════════════════════════════════════════════════════════════════
3
+ # Praxis — Shared Kit Install Check
4
+ # Source this file in kit install.sh scripts for consistent
5
+ # tool-checking output and counters.
6
+ #
7
+ # Usage:
8
+ # source "$(dirname "$0")/../../base/lib/kit-check.sh"
9
+ # check "jq" "brew install jq"
10
+ # check "curl" "pre-installed on macOS/Linux"
11
+ # kit_check_summary
12
+ # ════════════════════════════════════════════════════════════════
13
+
14
+ KIT_CHECK_PASS=0
15
+ KIT_CHECK_TOTAL=0
16
+
17
+ check() {
18
+ local cmd="$1"
19
+ local install_hint="$2"
20
+ local optional="${3:-}"
21
+
22
+ KIT_CHECK_TOTAL=$((KIT_CHECK_TOTAL + 1))
23
+ if command -v "$cmd" &>/dev/null; then
24
+ echo " ✓ $cmd found ($(command -v "$cmd"))"
25
+ KIT_CHECK_PASS=$((KIT_CHECK_PASS + 1))
26
+ else
27
+ if [[ "$optional" == "optional" ]]; then
28
+ echo " ⚠ $cmd not found (optional)"
29
+ else
30
+ echo " ✗ $cmd not found"
31
+ fi
32
+ echo " Install: $install_hint"
33
+ fi
34
+ }
35
+
36
+ kit_check_summary() {
37
+ echo ""
38
+ echo " $KIT_CHECK_PASS/$KIT_CHECK_TOTAL tools found"
39
+ if [[ $KIT_CHECK_PASS -lt $KIT_CHECK_TOTAL ]]; then
40
+ echo " ⚠ Some tools missing. Install them before using kit commands."
41
+ fi
42
+ }
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env bash
2
+ # ═══════��════════════════════════════════════════════════════════
3
+ # Praxis — Shared Output Helpers
4
+ # Source this file instead of defining colors/helpers inline.
5
+ #
6
+ # Usage:
7
+ # source "$(dirname "$0")/../base/lib/output.sh"
8
+ # # or with absolute path:
9
+ # source "$HOME/.claude/lib/output.sh"
10
+ #
11
+ # Safe to source multiple times — guards against redefinition.
12
+ # ════��═══════════════════════════════════════════════════════════
13
+
14
+ # ─── Colors (skip if already set) ───
15
+ RED="${RED:-\033[0;31m}"
16
+ GREEN="${GREEN:-\033[0;32m}"
17
+ YELLOW="${YELLOW:-\033[0;33m}"
18
+ CYAN="${CYAN:-\033[0;36m}"
19
+ BOLD="${BOLD:-\033[1m}"
20
+ DIM="${DIM:-\033[2m}"
21
+ NC="${NC:-\033[0m}"
22
+
23
+ # ─── Output helpers (skip if already defined) ───
24
+ if ! declare -f ok &>/dev/null; then
25
+ ok() { echo -e " ${GREEN}✓${NC} $1"; }
26
+ fi
27
+ if ! declare -f warn &>/dev/null; then
28
+ warn() { echo -e " ${YELLOW}⚠${NC} $1"; }
29
+ fi
30
+ if ! declare -f fail &>/dev/null; then
31
+ fail() { echo -e " ${RED}✗${NC} $1"; }
32
+ fi
33
+ if ! declare -f step &>/dev/null; then
34
+ step() { echo -e "\n${CYAN}${BOLD}$1${NC}"; }
35
+ fi
36
+ if ! declare -f dim &>/dev/null; then
37
+ dim() { echo -e " ${DIM}$1${NC}"; }
38
+ fi
@@ -0,0 +1,64 @@
1
+ # Desktop Protocol — Rules
2
+ # Scope: Sessions involving Claude Desktop ↔ Claude Code handoff
3
+ # Defines role boundaries and structured handoff format
4
+
5
+ ## Role Boundaries
6
+
7
+ | Surface | Role | Responsible for |
8
+ |---------|------|-----------------|
9
+ | Claude Desktop | Architect / Reviewer | ADR review, security audit, diff validation, prompt engineering |
10
+ | Claude Code | Executor | File writes, git ops, test runs, tool use, vault updates |
11
+
12
+ Desktop generates structured intent. Code executes it.
13
+ Never use Desktop for file writes. Never use Code for open-ended architecture debate.
14
+
15
+ ## Handoff Format — Desktop → Code
16
+
17
+ When Desktop completes a review or decision, it emits a structured handoff block.
18
+ Code reads this block as its initial task context.
19
+
20
+ ```
21
+ HANDOFF:
22
+ TASK: [one-line description]
23
+ SPEC: [link to vault plan or spec file]
24
+ CONSTRAINTS: [hard limits — what must NOT change]
25
+ ACCEPTANCE: [how to know it's done]
26
+ CONTEXT: [optional — key decisions or rationale from review]
27
+ ```
28
+
29
+ Code MUST NOT start implementation without at least TASK and ACCEPTANCE filled.
30
+ If SPEC is missing, Code asks before proceeding.
31
+
32
+ ## Handoff Format — Code → Desktop
33
+
34
+ When Code completes implementation and wants architectural review:
35
+
36
+ ```
37
+ REVIEW-REQUEST:
38
+ TASK: [what was implemented]
39
+ DIFF: [git diff summary or branch name]
40
+ SPEC: [link to plan that drove this work]
41
+ QUESTIONS: [specific things to review — not "does this look good"]
42
+ ```
43
+
44
+ ## Vault Write-Back
45
+
46
+ After Desktop review, append the decision to `{vault_path}/notes/review-decisions.md`:
47
+
48
+ ```
49
+ ## YYYY-MM-DD | [task-slug]
50
+ - **Decision**: APPROVED | CHANGES_REQUESTED | DEFERRED
51
+ - **Rationale**: [1-2 sentences]
52
+ - **Action items**: [if CHANGES_REQUESTED — specific items for Code to address]
53
+ ```
54
+
55
+ ## Conventions — WARN on violation
56
+
57
+ - Desktop review decisions that affect architecture go to `{vault_path}/specs/` as ADRs
58
+ - Desktop review decisions that are tactical (naming, style, small refactors) stay in `review-decisions.md`
59
+ - Code must not self-approve architectural changes — route through Desktop review
60
+ - If Desktop is unavailable, Code may proceed but must flag the decision in `status.md` for later review
61
+
62
+ ## Removal Condition
63
+ Remove when a unified Claude surface handles both architecture review and code execution
64
+ natively, eliminating the need for explicit handoff protocols.
@@ -4,13 +4,10 @@
4
4
  # Also implements: set-key subcommand for secret management.
5
5
  set -euo pipefail
6
6
 
7
- # ─── Colors ───
8
- RED='\033[0;31m'
9
- GREEN='\033[0;32m'
10
- YELLOW='\033[0;33m'
11
- CYAN='\033[0;36m'
12
- BOLD='\033[1m'
13
- NC='\033[0m'
7
+ # ─── Colors (shared) ───
8
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9
+ REPO_DIR="$(dirname "$SCRIPT_DIR")"
10
+ source "$REPO_DIR/base/lib/output.sh"
14
11
 
15
12
  PASS=0
16
13
  WARN=0
@@ -19,10 +16,10 @@ PRAXIS_DIR="$HOME/.praxis"
19
16
  SECRETS_FILE="$PRAXIS_DIR/secrets"
20
17
  REPORT_FILE="$PRAXIS_DIR/preflight-report.json"
21
18
 
19
+ # Override shared helpers with counter-incrementing versions
22
20
  ok() { echo -e " ${GREEN}✓${NC} $1"; PASS=$((PASS + 1)); }
23
21
  warn() { echo -e " ${YELLOW}⚠${NC} $1"; WARN=$((WARN + 1)); }
24
22
  fail() { echo -e " ${RED}✗${NC} $1"; BLOCK=$((BLOCK + 1)); }
25
- step() { echo -e "\n${CYAN}${BOLD}$1${NC}"; }
26
23
 
27
24
  # ═══════════════════════════════════════════
28
25
  # Subcommand: set-key
@@ -4,19 +4,7 @@ set -euo pipefail
4
4
  echo "=== Praxis: Installing api kit ==="
5
5
  echo ""
6
6
 
7
- PASS=0
8
- TOTAL=0
9
-
10
- check() {
11
- TOTAL=$((TOTAL + 1))
12
- if command -v "$1" &>/dev/null; then
13
- echo " ✓ $1 found ($(command -v "$1"))"
14
- PASS=$((PASS + 1))
15
- else
16
- echo " ✗ $1 not found"
17
- echo " Install: $2"
18
- fi
19
- }
7
+ source "$(dirname "$0")/../../base/lib/kit-check.sh"
20
8
 
21
9
  echo "Checking optional CLI tools..."
22
10
  echo ""
@@ -24,10 +12,9 @@ echo ""
24
12
  check "jq" "brew install jq OR apt-get install jq"
25
13
  check "curl" "pre-installed on macOS/Linux"
26
14
 
27
- echo ""
28
- echo " $PASS/$TOTAL tools found"
29
- echo ""
15
+ kit_check_summary
30
16
 
17
+ echo ""
31
18
  echo "Note: This kit uses Claude's built-in analysis capabilities."
32
19
  echo "No external API linting tools required."
33
20
  echo ""
@@ -8,6 +8,25 @@ BASELINE="$REPO_ROOT/.quality-baseline.json"
8
8
  TMP="/tmp/praxis-quality-$$"
9
9
  mkdir -p "$TMP"
10
10
 
11
+ # Safe jq wrapper: validates JSON before querying; returns fallback on parse failure
12
+ # and logs a warning so gate operators know a scan produced bad output.
13
+ safe_jq() {
14
+ local query="$1"
15
+ local file="$2"
16
+ local fallback="${3:-0}"
17
+ if [[ ! -s "$file" ]]; then
18
+ echo "$fallback"
19
+ return
20
+ fi
21
+ if ! jq empty "$file" 2>/dev/null; then
22
+ echo " ⚠ WARNING: $file is not valid JSON — treating as scan failure" >&2
23
+ GATE_WARNINGS+=("PARSE: $(basename "$file") produced invalid output")
24
+ echo "$fallback"
25
+ return
26
+ fi
27
+ jq -r "$query" "$file" 2>/dev/null || echo "$fallback"
28
+ }
29
+
11
30
  echo ""
12
31
  echo "Praxis Code Quality Gate"
13
32
  echo "------------------------"
@@ -32,8 +51,8 @@ if [ -s "$TMP/code-files.txt" ]; then
32
51
  FILES=$(cat "$TMP/code-files.txt" | tr '\n' ' ')
33
52
  opengrep scan --config auto --json $FILES > "$TMP/sast.json" 2>/dev/null || true
34
53
 
35
- CRITICAL=$(jq '[.results[] | select(.extra.severity == "ERROR")] | length' "$TMP/sast.json" 2>/dev/null || echo 0)
36
- HIGH=$(jq '[.results[] | select(.extra.severity == "WARNING")] | length' "$TMP/sast.json" 2>/dev/null || echo 0)
54
+ CRITICAL=$(safe_jq '[.results[] | select(.extra.severity == "ERROR")] | length' "$TMP/sast.json")
55
+ HIGH=$(safe_jq '[.results[] | select(.extra.severity == "WARNING")] | length' "$TMP/sast.json")
37
56
 
38
57
  [ "$CRITICAL" -gt 0 ] && GATE_FAILURES+=("SAST: $CRITICAL critical findings")
39
58
  [ "$HIGH" -gt 0 ] && GATE_WARNINGS+=("SAST: $HIGH high findings")
@@ -44,7 +63,11 @@ SAST_PID=$!
44
63
  # -- SECRETS (TruffleHog) --
45
64
  echo " Secrets scan (TruffleHog)..."
46
65
  trufflehog git file://. --since-commit HEAD~1 --only-verified --json > "$TMP/secrets.json" 2>/dev/null || true
47
- SECRETS=$(jq -s 'length' "$TMP/secrets.json" 2>/dev/null || echo 0)
66
+ if [[ -s "$TMP/secrets.json" ]]; then
67
+ SECRETS=$(jq -s 'length' "$TMP/secrets.json" 2>/dev/null || echo 0)
68
+ else
69
+ SECRETS=0
70
+ fi
48
71
  [ "$SECRETS" -gt 0 ] && GATE_FAILURES+=("SECRETS: $SECRETS verified secrets found")
49
72
  echo " Verified secrets: $SECRETS" &
50
73
  SECRETS_PID=$!
@@ -52,8 +75,8 @@ SECRETS_PID=$!
52
75
  # -- SCA (OSV-Scanner) --
53
76
  echo " Dependency scan (OSV-Scanner)..."
54
77
  osv-scanner scan --format json "$REPO_ROOT" > "$TMP/sca.json" 2>/dev/null || true
55
- SCA_CRITICAL=$(jq '[.vulns[]? | select(.database_specific.severity? == "CRITICAL")] | length' "$TMP/sca.json" 2>/dev/null || echo 0)
56
- SCA_HIGH=$(jq '[.vulns[]? | select(.database_specific.severity? == "HIGH")] | length' "$TMP/sca.json" 2>/dev/null || echo 0)
78
+ SCA_CRITICAL=$(safe_jq '[.vulns[]? | select(.database_specific.severity? == "CRITICAL")] | length' "$TMP/sca.json")
79
+ SCA_HIGH=$(safe_jq '[.vulns[]? | select(.database_specific.severity? == "HIGH")] | length' "$TMP/sca.json")
57
80
  [ "$SCA_CRITICAL" -gt 0 ] && GATE_FAILURES+=("SCA: $SCA_CRITICAL critical CVEs in dependencies")
58
81
  [ "$SCA_HIGH" -gt 0 ] && GATE_WARNINGS+=("SCA: $SCA_HIGH high CVEs in dependencies")
59
82
  echo " Critical CVEs: $SCA_CRITICAL High CVEs: $SCA_HIGH" &
@@ -63,7 +86,7 @@ SCA_PID=$!
63
86
  if [ -s "$TMP/iac-files.txt" ]; then
64
87
  echo " IaC scan (Checkov)..."
65
88
  checkov -d "$REPO_ROOT" --output json --quiet --compact 2>/dev/null > "$TMP/iac.json" || true
66
- IaC_FAIL=$(jq '.results.failed_checks | length' "$TMP/iac.json" 2>/dev/null || echo 0)
89
+ IaC_FAIL=$(safe_jq '.results.failed_checks | length' "$TMP/iac.json")
67
90
  [ "$IaC_FAIL" -gt 0 ] && GATE_WARNINGS+=("IaC: $IaC_FAIL policy violations")
68
91
  echo " Policy violations: $IaC_FAIL"
69
92
  fi &
@@ -73,9 +96,9 @@ IaC_PID=$!
73
96
  wait $SAST_PID $SECRETS_PID $SCA_PID $IaC_PID 2>/dev/null || true
74
97
 
75
98
  # -- COVERAGE --
76
- COVERAGE_THRESHOLD=$(jq -r '.coverage.line_min' "$CONFIG" 2>/dev/null || echo 80)
99
+ COVERAGE_THRESHOLD=$(safe_jq '.coverage.line_min' "$CONFIG" 80)
77
100
  if [ -f "$REPO_ROOT/coverage/coverage-summary.json" ]; then
78
- COVERAGE=$(jq '.total.lines.pct' "$REPO_ROOT/coverage/coverage-summary.json" 2>/dev/null || echo 100)
101
+ COVERAGE=$(safe_jq '.total.lines.pct' "$REPO_ROOT/coverage/coverage-summary.json" 100)
79
102
  BELOW=$(echo "$COVERAGE < $COVERAGE_THRESHOLD" | bc -l 2>/dev/null || echo 0)
80
103
  [ "$BELOW" = "1" ] && GATE_WARNINGS+=("COVERAGE: ${COVERAGE}% below threshold ${COVERAGE_THRESHOLD}%")
81
104
  echo " Coverage: ${COVERAGE}% (threshold: ${COVERAGE_THRESHOLD}%)"
@@ -4,32 +4,19 @@ set -euo pipefail
4
4
  echo "=== Praxis: Installing data kit ==="
5
5
  echo ""
6
6
 
7
- PASS=0
8
- TOTAL=0
9
-
10
- check() {
11
- TOTAL=$((TOTAL + 1))
12
- if command -v "$1" &>/dev/null; then
13
- echo " ✓ $1 found ($(command -v "$1"))"
14
- PASS=$((PASS + 1))
15
- else
16
- echo " ✗ $1 not found (optional)"
17
- echo " Install: $2"
18
- fi
19
- }
7
+ source "$(dirname "$0")/../../base/lib/kit-check.sh"
20
8
 
21
9
  echo "Checking optional CLI tools..."
22
10
  echo ""
23
11
 
24
- check "psql" "brew install postgresql OR apt-get install postgresql-client"
25
- check "mysql" "brew install mysql-client OR apt-get install mysql-client"
26
- check "mongosh" "brew install mongosh OR https://www.mongodb.com/try/download/shell"
12
+ check "psql" "brew install postgresql OR apt-get install postgresql-client" "optional"
13
+ check "mysql" "brew install mysql-client OR apt-get install mysql-client" "optional"
14
+ check "mongosh" "brew install mongosh OR https://www.mongodb.com/try/download/shell" "optional"
27
15
  check "jq" "brew install jq OR apt-get install jq"
28
16
 
29
- echo ""
30
- echo " $PASS/$TOTAL tools found"
31
- echo ""
17
+ kit_check_summary
32
18
 
19
+ echo ""
33
20
  echo "Note: This kit uses Claude's built-in analysis for schema and query review."
34
21
  echo "Database CLI tools are needed only for live query testing."
35
22
  echo ""
@@ -4,19 +4,7 @@ set -euo pipefail
4
4
  echo "=== Praxis: Installing infrastructure kit ==="
5
5
  echo ""
6
6
 
7
- PASS=0
8
- TOTAL=0
9
-
10
- check() {
11
- TOTAL=$((TOTAL + 1))
12
- if command -v "$1" &>/dev/null; then
13
- echo " ✓ $1 found ($(command -v "$1"))"
14
- PASS=$((PASS + 1))
15
- else
16
- echo " ✗ $1 not found"
17
- echo " Install: $2"
18
- fi
19
- }
7
+ source "$(dirname "$0")/../../base/lib/kit-check.sh"
20
8
 
21
9
  echo "Checking required CLI tools..."
22
10
  echo ""
@@ -26,13 +14,7 @@ check "terraform" "https://developer.hashicorp.com/terraform/install"
26
14
  check "tflint" "brew install tflint OR https://github.com/terraform-linters/tflint"
27
15
  check "jq" "brew install jq OR apt-get install jq"
28
16
 
29
- echo ""
30
- echo " $PASS/$TOTAL tools found"
31
- echo ""
32
-
33
- if [[ $PASS -lt $TOTAL ]]; then
34
- echo " ⚠ Some tools missing. Install them before using infrastructure commands."
35
- fi
17
+ kit_check_summary
36
18
 
37
19
  echo ""
38
20
  echo "Note: Skills chain phases are status: planned."
@@ -4,31 +4,18 @@ set -euo pipefail
4
4
  echo "=== Praxis: Installing security kit ==="
5
5
  echo ""
6
6
 
7
- PASS=0
8
- TOTAL=0
9
-
10
- check() {
11
- TOTAL=$((TOTAL + 1))
12
- if command -v "$1" &>/dev/null; then
13
- echo " ✓ $1 found ($(command -v "$1"))"
14
- PASS=$((PASS + 1))
15
- else
16
- echo " ✗ $1 not found (optional)"
17
- echo " Install: $2"
18
- fi
19
- }
7
+ source "$(dirname "$0")/../../base/lib/kit-check.sh"
20
8
 
21
9
  echo "Checking optional CLI tools..."
22
10
  echo ""
23
11
 
24
- check "trivy" "brew install trivy OR https://aquasecurity.github.io/trivy"
25
- check "deepsource" "curl -fsSL https://cli.deepsource.com/install | sh"
12
+ check "trivy" "brew install trivy OR https://aquasecurity.github.io/trivy" "optional"
13
+ check "deepsource" "curl -fsSL https://cli.deepsource.com/install | sh" "optional"
26
14
  check "rg" "brew install ripgrep OR apt-get install ripgrep"
27
15
 
28
- echo ""
29
- echo " $PASS/$TOTAL tools found"
30
- echo ""
16
+ kit_check_summary
31
17
 
18
+ echo ""
32
19
  echo "Note: This kit uses Claude's built-in analysis for most checks."
33
20
  echo "External tools enhance scanning but are not required."
34
21
  echo ""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@esoteric-logic/praxis-harness",
3
- "version": "2.9.0",
3
+ "version": "2.10.0",
4
4
  "description": "Layered Claude Code harness — workflow discipline, AI-Kits, persistent vault integration",
5
5
  "bin": {
6
6
  "praxis-harness": "./bin/praxis.js"
@@ -20,24 +20,9 @@ set -euo pipefail
20
20
  CLAUDE_DIR="${CLAUDE_DIR:-$HOME/.claude}"
21
21
  CONFIG_FILE="${CONFIG_FILE:-$CLAUDE_DIR/praxis.config.json}"
22
22
 
23
- # ─── Colors (safe defaults if not already set) ───
24
- RED="${RED:-\033[0;31m}"
25
- GREEN="${GREEN:-\033[0;32m}"
26
- YELLOW="${YELLOW:-\033[0;33m}"
27
- CYAN="${CYAN:-\033[0;36m}"
28
- BOLD="${BOLD:-\033[1m}"
29
- NC="${NC:-\033[0m}"
30
-
31
- # ─── Output helpers (no-op if already defined by install.sh) ───
32
- if ! declare -f ok &>/dev/null; then
33
- ok() { echo -e " $GREEN✓$NC $1"; }
34
- fi
35
- if ! declare -f warn &>/dev/null; then
36
- warn() { echo -e " $YELLOW⚠$NC $1"; }
37
- fi
38
- if ! declare -f fail &>/dev/null; then
39
- fail() { echo -e " $RED✗$NC $1"; }
40
- fi
23
+ # ─── Colors & output helpers (safe to source multiple times) ───
24
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
25
+ source "$SCRIPT_DIR/../base/lib/output.sh"
41
26
 
42
27
  # ═══════════════════════════════════════════
43
28
  # Utilities