Add a shared log taxonomy (error<warn<info<debug<trace, default info) wired
through all three layers, plus a machine-readable per-step run log.
- scripts/casan-log.mjs + .specify/scripts/bash/casan-log.sh: shared logger
([LEVEL] ts [component] msg), stderr-only so stdout contracts and exit codes
are byte-identical. Node side includes trace-level redaction (secret/PII).
- run-casan-pipeline.mjs: per-STEP info line, loop-activation warnings
(BACK-TO-PLAN, FAIL->STEP6), debug harness rc + trace_id, trace payload
excerpt, end-of-run 13-STEP summary table, and one JSONL line/step in
.specify/logs/pipeline-run.jsonl. New --dry-run stubs all agents but keeps
the real wrapper in the loop (deterministic, offline, full 13 STEP + loops).
- casan-step.mjs: debug logs for judge verdict, checkpoint, rollback; logger
import degrades to noop when the file is copied standalone (T1 test).
- casan-harness.sh: debug-log each phase H4-in -> H5 -> [H2-gate] -> H6-exec
-> H4-out with its rc; optional CASAN_PHASE_REPORT JSON for the Boss.
Default level (info) keeps output close to before; behavior opt-in via env.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two comprehensive CI fixes:
1. Adversarial harness flake (PASS=42 FAIL=2, state-dependent):
governance-check.sh re-exported audit-public.pem only when GENERATING a
new signing key. On a CI runner where the off-repo private key PERSISTS
across runs, the checked-out (Vault-signed) audit-public.pem drifted out
of sync with the local re-signing key, so verify-audit-chain.sh rejected a
genuine head ("H5/H2 verifies the genuine signed chain"). Now always
re-export the public key matching the signing key — mirrors the same fix
already applied to tool-audit-lib.sh (c31987c). Reproduced the exact
FAIL=2 locally with a drifted persisted key; now deterministically 44/44.
2. Deploy docker.sock permission denied:
Added a self-healing preflight to deploy-okr that ensures the deploy user
is in the docker group on the web VPS (idempotent, passwordless sudo) and
proves a fresh SSH session can reach the daemon before streaming images.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Previously the public key was only written on first key generation.
If a CI step (sign-audit-head.sh via Vault KMS) overwrote audit-public.pem
after the key was generated, subsequent calls to append_tool_audit signed
with the local key while audit-public.pem held the Vault key — causing
verify-tool-audit.sh to fail with signature mismatch.
Now the public key is re-exported on every call so audit-public.pem always
matches the private key used to sign tool-calls-head.sig.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: tool-audit-lib.sh signs tool-calls-head.sig with a local RSA
key (~/.casan/audit-keys/), but sign-audit-head.sh (called as a CI step)
overwrites audit-public.pem with the Vault KMS public key. On the second
run inside security-gate.sh, the local key still exists so audit-public.pem
is NOT updated, leaving a Vault key vs local-key mismatch that causes
verify-tool-audit.sh to exit 1.
Fix 1 — sign-audit-head.sh: after signing the audit.jsonl chain via Vault
KMS, also re-sign the tool-calls chain head with the same casan-audit-key.
Both chains are now anchored to the same Vault public key in audit-public.pem.
Fix 2 — run-casan4-harness-tests.sh: call sign-audit-head.sh just before
the inline verify-tool-audit.sh check (line 217). This re-signs both chains
with Vault KMS so the inline check sees anchor=signed instead of mismatched
local key vs Vault pub.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Embedding $response as Python triple-quoted string caused json.loads()
to fail with 'Invalid control character' when Vault's JSON contained
\n sequences in the PEM public key (bash \n → Python newline → invalid
JSON control char).
Fix: write response to mktemp, pass path as argv, read with open().
Instead of skipping the frontend test when vitest is not installed,
security-gate.sh now runs `npm ci -w frontend` automatically.
Also add `cache: "npm"` to security-gate's actions/setup-node so the
npm cache from the frontend-tests job is reused — prevents OOM on
the 1GB VPS (cache restore is disk-only, not 300MB download).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
security-gate.sh checked only `command -v node` before running
`npm test -w frontend`. In the CI security-gate job, node is in PATH
(from actions/setup-node) but root node_modules are NOT installed
(npm ci was removed to prevent OOM). This caused the frontend test to
fail with "Cannot find module vitest".
Fix: also require node_modules/.bin/vitest to exist. Without it the
step SKIPs gracefully — frontend tests are already covered by the
dedicated frontend-tests CI job which runs first.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
T1 (H7): casan-step.mjs now calls rollback-manager.sh checkpoint before
overwriting plan.md at attempt-2, writes tx-id to plan.checkpoint.txid
sidecar, and executes rollback on REJECTED verdict. rollback-transactions.jsonl
records a real cp restore command. Adversarial test: checkpoint exists,
real cp command recorded, plan hash matches pre-overwrite content.
T4 (H6): casan-harness.sh exports CASAN_STEP_NAME=$ACTION_NAME before
agent-metrics.sh so nested model calls (model-call.py) and the provider-
cost-lookup.py query share the same step label. metrics.jsonl now writes
cost_source=provider_telemetry instead of word_count_estimate when a real
Ollama call is made within the same step. Adversarial test: verified with
CASAN_STEP_NAME=t4-telemetry-test end-to-end.
adversarial-harness-tests.sh: 40 → 44 PASS / 0 FAIL (+3 T1, +1 T4)
security-gate.sh: PASS=10 FAIL=0 SKIP=0 (verified, local ornith:9b)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>