---
phase: 1.5
plan: 1
subsystem: governance-runtime-bugfix
tags: [governance, hook, guard, semantic-check, audit-trail, BUG-01, BUG-02]
dependency_graph:
  requires: []
  provides: [BUG-01-fixed, BUG-02-fixed, blocked-status-accepted, handle-block-audit-trail]
  affects: [app/lib/guard.js, hooks/dashclaw_pretool.py, app/lib/validate.js]
tech_stack:
  added: []
  patterns: [require_approval-middle-path-fallback, opportunistic-audit-recording]
key_files:
  created:
    - .planning/phases/01.5-governance-bugfix/01.5-DIAGNOSIS.md
    - hooks/tests/test_handle_block_audit.py
  modified:
    - app/lib/guard.js
    - hooks/dashclaw_pretool.py
    - app/lib/validate.js
    - .env.example
decisions:
  - BUG-01 fallback direction: require_approval (middle path) when LLM key absent — not fail-open (allow), not fail-closed (block). Governed by Phase 1.5 Plan 1 design constraint.
  - BUG-02 recording is opportunistic: create_action() call is fire-and-forget; if the network write fails, sys.exit(2) still runs. Governance decision is authoritative regardless of audit write success.
metrics:
  duration: ~45min
  completed: 2026-04-11
  tasks_completed: 5
  tasks_deferred: 1 (T6 — human action)
  files_changed: 4
  commits: 5
---

# Phase 1.5 Plan 1: Governance Runtime Bugfix Summary

**One-liner:** Fixed two stacked governance bugs — semantic_check missing-key fallback now uses `require_approval` instead of silent block, and `handle_block` now writes `status=blocked` audit records before exiting.

---

## Tasks Completed

| Task | Name | Commit | Files |
|------|------|--------|-------|
| T1 | Diagnose BUG-01 | `b6b89610` | `.planning/phases/01.5-governance-bugfix/01.5-DIAGNOSIS.md` |
| T2 | Fix BUG-01 semantic check fallback | `b8706570` | `app/lib/guard.js`, `.env.example` |
| T3 | Fix BUG-02 handle_block client-side | `e9ce9aaa` | `hooks/dashclaw_pretool.py` |
| T4 | Accept "blocked" status server-side | `6f0a57bd` | `app/lib/validate.js` |
| T5 | Regression test for handle_block | `a4e69b05` | `hooks/tests/test_handle_block_audit.py` |
| T6 | End-to-end validation | — | **DEFERRED — Wes must execute manually** |

---

## Requirements Addressed

- **BUG-01**: Secret Exposure Guard semantic check no longer deterministic-blocks when no LLM API key is configured. Root cause: `checkSemanticGuardrail` returned `null` (no-key skip), and the policy's `rules.fallback='block'` was honored. Fix: detect the no-key case before calling the LLM and return `require_approval` (middle path) — not fail-open, not fail-closed. When a key IS present but the API call fails, the policy's explicit `fallback` setting is still honored (preserving operator intent).
- **BUG-02 (client)**: `handle_block()` in `hooks/dashclaw_pretool.py` now calls `create_action(context, status="blocked")` before exiting, both in enforce mode (exit 2) and observe mode (exit 0). Every block is now recorded with an audit trail row — matching the behavior of all other decision handlers.
- **BUG-02 (server)**: `ACTION_STATUSES` in `app/lib/validate.js` now includes `"blocked"`, so `POST /api/actions` with `status: "blocked"` passes validation. The `/decisions` and `/mission-control` UIs already rendered `"blocked"` status with full icon/colour mapping — no UI changes were required.

---

## BUG-01 Root Cause (one-liner)

No `GUARD_LLM_KEY` or `OPENAI_API_KEY` set in Vercel env vars — `checkSemanticGuardrail` returns `null` (clean skip, not an error), and the "Secret Exposure Guard" policy's `rules.fallback='block'` caused a deterministic block on every governed command regardless of its content.

---

## Design Decision Flag

**Fallback direction for "no LLM key" case:** Chose `require_approval` as the middle path per plan guidance. Rationale: fail-open (`allow`) undermines the point of having the policy; fail-closed (`block`) creates silent invisible blocks with no audit trail; `require_approval` keeps the operator in the loop and creates a visible action row. If Wes wants to change this default for specific policies, he can set `DASHCLAW_GUARD_FALLBACK=allow` or `block` in Vercel env vars, or set `rules.fallback` on the policy directly (which still applies when a key IS present and the API call fails).

---

## Deviations from Plan

### Plan Spec Quirk (not a bug)

**Acceptance criteria for T3** specifies `grep -A3 "def handle_block" | grep "create_action"`. The actual function has a 6-line header (docstring + 4 variable assignments + blank) before the `create_action` call, so `-A3` doesn't reach it. The fix is correctly applied at line 353. The test suite at `hooks/tests/test_handle_block_audit.py` provides definitive proof — all 4 tests pass including explicit assertion that `create_action` is called with `status="blocked"`.

---

## T6 Handoff for Wes

T6 requires Wes to re-enable the Secret Exposure Guard policy and re-fire the originally blocked command to verify both bugs are fixed end-to-end. Complete these steps when ready:

### Step 1: Re-enable the policy

Visit your DashClaw instance at `/policies` (or Mission Control → Policies). Find the "Secret Exposure Guard" policy (`gp_178772c27d0f40e69240f82f`) and re-enable it.

**Important:** This policy uses `semantic_check` type and will now fall back to `require_approval` instead of `block` when no LLM key is configured. If you want the semantic check to actually run (LLM evaluation instead of just requiring approval), set `GUARD_LLM_KEY=<your-openai-key>` in your Vercel environment variables and redeploy.

### Step 2: Re-fire the originally blocked command

In a fresh terminal in your DashClaw project directory:
```bash
cd C:/Projects/DashClaw
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init progress
```

The hook will intercept this command and POST to `/api/guard`. With the fix applied, you should see one of:
- `decision: require_approval` (if no LLM key — hook waits for your approval)
- `decision: allow` (if LLM key is set and the semantic check passes)
- `decision: block` with a reason other than "Semantic check failed (fallback: block)" (if LLM key set and check determines it's a security violation — unlikely for this command)

### Step 3: Verify the audit trail

Visit `/decisions` in your DashClaw UI. You should see a new row with:
- `declared_goal: "Bash: node $HOME/.claude/get-shit-done/bin/gsd-tools.cjs init progress"`
- `status`: `blocked` (if it was blocked) or `pending_approval` (if approval required) or `running` (if allowed)
- The policy name `gp_178772c27d0f40e69240f82f` in the matched policies

Before this fix, a `block` decision would have produced **no row at all** in the decisions ledger. After this fix, every decision — including blocks — is recorded.

### Step 4: Fill in the validation file

Create `.planning/phases/01.5-governance-bugfix/01.5-VALIDATION.md` with:
```markdown
# T6 Validation Result

- **Date:** [today]
- **Command:** node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init progress
- **Decision returned:** [allow / require_approval / block]
- **Action ID in ledger:** [act_xxx — from /decisions page]
- **Screenshot or curl output:** [paste curl response or describe what you saw in the UI]
- **BUG-01 confirmed fixed:** [yes/no — "Semantic check failed (fallback: block)" no longer appears]
- **BUG-02 confirmed fixed:** [yes/no — action row visible in /decisions regardless of decision]
```

---

## Verification Results

```
npm run lint          — PASSED
npm run route-sql:check — PASSED (87 SQL uses, baseline 90, no increase)
npm run openapi:check  — PASSED (artifact up to date)
npm run api:inventory:check — PASSED (artifacts up to date)
npm test -- --run actions  — 21/21 PASSED
npm test -- --run guard    — 106/106 PASSED
npm test -- --run validate — 9/9 PASSED
python -m pytest hooks/tests/test_handle_block_audit.py — 4/4 PASSED
```

---

## Dogfood Attribution

This plan exists because Wes was dogfooding DashClaw against his own Claude Code workflow and the product bit him. That is the closed-loop flywheel from PROJECT.md working exactly as designed — the founder using the product on the product finds real bugs before external users do. This is a positive signal, not a disaster. The feedback loop is functioning.

---

## GitNexus Note

GitNexus MCP tools were not available in this executor's runtime environment. Impact analysis (`gitnexus_impact`) and change detection (`gitnexus_detect_changes`) were skipped. All three modified symbols — `evaluatePolicy` in `guard.js`, `handle_block` in `dashclaw_pretool.py`, and `ACTION_STATUSES` in `validate.js` — have low blast radius: guard.js changes are additive (new branch in a switch case), pretool.py changes are additive (one new function call), and validate.js changes are additive (one new array member). No callers break; all tests pass.

---

## Known Stubs

None. All changes are functional fixes — no hardcoded empty values, no placeholder text, no mock data.

## Threat Flags

None. No new network endpoints, auth paths, file access patterns, or schema changes were introduced. The `ACTION_STATUSES` extension is a validation relaxation (adding a value), not a new trust boundary.
