Skip to content
19 changes: 12 additions & 7 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,19 @@ Portable, installable workflow enforcement system for Claude Code. One setup scr

## Status: Complete

## Session Handoff
## Completed (005: Workflows in hook-runner)
- [x] T001 Spec and workflow YAML (PR #9)
- [x] T002 Port workflow.js to hook-runner (PR #10)
- [x] T003 Add workflow-gate PreToolUse module (PR #11)
- [x] T004 Add --workflow CLI commands (PR #12)
- [x] T005 Built-in templates: enforce-shtd.yml, cross-project-reset.yml (PR #13)
- [x] T006 E2E tests — 9/9 lifecycle assertions (PR #14)
- [x] T007 enforce-shtd meta-workflow — self-enforcing (PR #15)
- [x] T008 Delegation verification (PR #16)
- [x] T009 Final cross-project verification — 14/14 checks (PR #17)
- [x] Hook-runner PR: grobomo/hook-runner#66

User requested new project: **ccc-central** — Central monitoring dispatcher for Claude Code.
- Created `~/Documents/ProjectsCL1/ccc-central/TODO.md` with architecture and tasks
- Next: start new session in ccc-central project directory
- Spec-hook is fully complete (T001-T027, 5 PRs merged, real evidence report)

## Completed
## Completed (earlier)

- [x] T001-T021 (all core tasks — see git log)
- [x] T022 Initial evidence report (PDF with tables, user rejected — needs real screenshots)
Expand Down
34 changes: 34 additions & 0 deletions scripts/test/test-T001-workflow-spec.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# Test: spec and workflow YAML exist for 005-add-workflows-to-hookrunner
set -euo pipefail
cd "$(dirname "$0")/../.."

errors=0

# Spec exists
if [ -f "specs/005-add-workflows-to-hookrunner/spec.md" ]; then
echo "[OK] spec.md exists"
else
echo "[FAIL] specs/005-add-workflows-to-hookrunner/spec.md missing"
errors=$((errors + 1))
fi

# Spec has required sections
for section in "Problem" "Solution" "Architecture"; do
if grep -qi "$section" specs/005-add-workflows-to-hookrunner/spec.md 2>/dev/null; then
echo "[OK] spec has $section section"
else
echo "[FAIL] spec missing $section section"
errors=$((errors + 1))
fi
done

# Workflow YAML exists
if [ -f "workflows/005-add-workflows-to-hookrunner.yml" ]; then
echo "[OK] workflow YAML exists"
else
echo "[FAIL] workflows/005-add-workflows-to-hookrunner.yml missing"
errors=$((errors + 1))
fi

exit $errors
141 changes: 141 additions & 0 deletions scripts/test/test-T002-hookrunner-engine.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
#!/usr/bin/env bash
# Test: workflow.js exists in hook-runner and works correctly
set -euo pipefail

HR_DIR="$HOME/Documents/ProjectsCL1/hook-runner"

to_node() { cygpath -m "$1" 2>/dev/null || echo "$1"; }

errors=0

# workflow.js exists in hook-runner
if [ -f "$HR_DIR/workflow.js" ]; then
echo "[OK] workflow.js exists in hook-runner"
else
echo "[FAIL] workflow.js missing from hook-runner"
exit 1
fi

WF_JS=$(to_node "$HR_DIR/workflow.js")

# Can load without error
if node -e "require('$WF_JS')" 2>/dev/null; then
echo "[OK] workflow.js loads without error"
else
echo "[FAIL] workflow.js fails to load"
errors=$((errors + 1))
fi

# parseYaml works
RESULT=$(node -e "
const wf = require('$WF_JS');
const parsed = wf.parseYaml('name: test\nversion: 1\nsteps:\n - id: s1\n name: Step 1');
console.log(JSON.stringify(parsed));
")
if echo "$RESULT" | grep -q '"name":"test"'; then
echo "[OK] parseYaml works"
else
echo "[FAIL] parseYaml returned: $RESULT"
errors=$((errors + 1))
fi

# State management works
TMPDIR=$(mktemp -d)
trap "rm -rf $TMPDIR" EXIT

# Create a test workflow YAML
cat > "$TMPDIR/test.yml" <<'YAML'
name: test-wf
steps:
- id: step1
name: First
gate:
require_files: []
completion:
require_files: []
- id: step2
name: Second
gate:
require_step: step1
completion:
require_files: []
YAML

TP=$(to_node "$TMPDIR")
TY=$(to_node "$TMPDIR/test.yml")

# initState + currentStep
CURRENT=$(node -e "
const wf = require('$WF_JS');
wf.initState('test-wf', '$TY', '$TP');
console.log(wf.currentStep('$TP'));
")
if [ "$CURRENT" = "step1" ]; then
echo "[OK] initState + currentStep works"
else
echo "[FAIL] expected step1, got: $CURRENT"
errors=$((errors + 1))
fi

# completeStep + advance
NEXT=$(node -e "
const wf = require('$WF_JS');
wf.completeStep('step1', '$TP');
console.log(wf.currentStep('$TP'));
")
if [ "$NEXT" = "step2" ]; then
echo "[OK] completeStep advances correctly"
else
echo "[FAIL] expected step2, got: $NEXT"
errors=$((errors + 1))
fi

# checkGate blocks when prerequisite not met
GATE=$(node -e "
const wf = require('$WF_JS');
const tmpDir2 = require('os').tmpdir() + '/wf-test-' + Date.now();
require('fs').mkdirSync(tmpDir2, {recursive:true});
require('fs').writeFileSync(tmpDir2 + '/test.yml', \`name: gate-test
steps:
- id: a
name: A
gate:
require_files: []
completion:
require_files: []
- id: b
name: B
gate:
require_step: a
completion:
require_files: []
\`);
const p = '$TP'.replace(/\\\\/g,'/');
wf.initState('gate-test', tmpDir2.replace(/\\\\/g,'/') + '/test.yml', tmpDir2.replace(/\\\\/g,'/'));
const check = wf.checkGate('b', tmpDir2.replace(/\\\\/g,'/'));
console.log(check.allowed ? 'allowed' : 'blocked');
")
if [ "$GATE" = "blocked" ]; then
echo "[OK] checkGate blocks when prerequisite not met"
else
echo "[FAIL] expected blocked, got: $GATE"
errors=$((errors + 1))
fi

# Exports all expected functions
EXPORTS=$(node -e "
const wf = require('$WF_JS');
const fns = ['parseYaml','loadWorkflow','findWorkflows','readState','writeState','initState','completeStep','currentStep','checkGate','checkEditAllowed'];
const missing = fns.filter(f => typeof wf[f] !== 'function');
console.log(missing.length === 0 ? 'all' : 'missing:' + missing.join(','));
")
if [ "$EXPORTS" = "all" ]; then
echo "[OK] all expected functions exported"
else
echo "[FAIL] $EXPORTS"
errors=$((errors + 1))
fi

echo ""
echo "=== $((7 - errors))/7 tests passed ==="
exit $errors
148 changes: 148 additions & 0 deletions scripts/test/test-T003-hookrunner-gate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#!/usr/bin/env bash
# Test: workflow-gate PreToolUse module exists in hook-runner and works
set -euo pipefail

HR_DIR="$HOME/Documents/ProjectsCL1/hook-runner"

to_node() { cygpath -m "$1" 2>/dev/null || echo "$1"; }

errors=0

# Module exists
if [ -f "$HR_DIR/modules/PreToolUse/workflow-gate.js" ]; then
echo "[OK] workflow-gate.js exists"
else
echo "[FAIL] modules/PreToolUse/workflow-gate.js missing"
exit 1
fi

MOD_JS=$(to_node "$HR_DIR/modules/PreToolUse/workflow-gate.js")

# Module loads
if node -e "require('$MOD_JS')" 2>/dev/null; then
echo "[OK] module loads without error"
else
echo "[FAIL] module fails to load"
errors=$((errors + 1))
fi

# Module exports a function
TYPEOF=$(node -e "console.log(typeof require('$MOD_JS'))")
if [ "$TYPEOF" = "function" ]; then
echo "[OK] exports a function"
else
echo "[FAIL] expected function, got $TYPEOF"
errors=$((errors + 1))
fi

# Returns null when no active workflow
RESULT=$(node -e "
const gate = require('$MOD_JS');
process.env.CLAUDE_PROJECT_DIR = require('os').tmpdir();
const r = gate({tool_name:'Write', tool_input:{file_path:'/tmp/foo.js'}});
console.log(r === null ? 'null' : JSON.stringify(r));
")
if [ "$RESULT" = "null" ]; then
echo "[OK] returns null when no active workflow"
else
echo "[FAIL] expected null, got: $RESULT"
errors=$((errors + 1))
fi

# Returns null for allowed paths (TODO.md, specs/, etc.)
RESULT=$(node -e "
const gate = require('$MOD_JS');
process.env.CLAUDE_PROJECT_DIR = require('os').tmpdir();
const r = gate({tool_name:'Write', tool_input:{file_path:'/tmp/project/TODO.md'}});
console.log(r === null ? 'null' : JSON.stringify(r));
")
if [ "$RESULT" = "null" ]; then
echo "[OK] allows TODO.md edits"
else
echo "[FAIL] expected null for TODO.md, got: $RESULT"
errors=$((errors + 1))
fi

# Blocks when gate unsatisfied
WF_JS=$(to_node "$HR_DIR/workflow.js")
TMPDIR=$(mktemp -d)
trap "rm -rf $TMPDIR" EXIT
TP=$(to_node "$TMPDIR")

cat > "$TMPDIR/test.yml" <<'YAML'
name: gate-test
steps:
- id: step1
name: First
gate:
require_files: ["nonexistent.txt"]
completion:
require_files: []
YAML

TY=$(to_node "$TMPDIR/test.yml")

RESULT=$(node -e "
const wf = require('$WF_JS');
wf.initState('gate-test', '$TY', '$TP');
const gate = require('$MOD_JS');
process.env.CLAUDE_PROJECT_DIR = '$TP';
const r = gate({tool_name:'Write', tool_input:{file_path:'$TP/src/main.js'}});
console.log(r && r.decision === 'block' ? 'blocked' : 'not-blocked');
")
if [ "$RESULT" = "blocked" ]; then
echo "[OK] blocks when gate unsatisfied"
else
echo "[FAIL] expected blocked, got: $RESULT"
errors=$((errors + 1))
fi

# Passes when gate satisfied
RESULT=$(node -e "
const wf = require('$WF_JS');
const fs = require('fs');
// Reset and use a workflow with no gate requirements
fs.unlinkSync('$TP/.workflow-state.json');
fs.writeFileSync('$TP/test2.yml', \`name: pass-test
steps:
- id: step1
name: First
gate:
require_files: []
completion:
require_files: []
\`);
wf.initState('pass-test', '$TP/test2.yml', '$TP');
const gate = require('$MOD_JS');
process.env.CLAUDE_PROJECT_DIR = '$TP';
// Clear require cache to get fresh module
delete require.cache[require.resolve('$MOD_JS')];
const gate2 = require('$MOD_JS');
const r = gate2({tool_name:'Write', tool_input:{file_path:'$TP/src/main.js'}});
console.log(r === null ? 'passed' : JSON.stringify(r));
")
if [ "$RESULT" = "passed" ]; then
echo "[OK] passes when gate satisfied"
else
echo "[FAIL] expected passed, got: $RESULT"
errors=$((errors + 1))
fi

# Only triggers on Write/Edit
RESULT=$(node -e "
delete require.cache[require.resolve('$MOD_JS')];
const gate = require('$MOD_JS');
process.env.CLAUDE_PROJECT_DIR = '$TP';
const r = gate({tool_name:'Bash', tool_input:{command:'echo hi'}});
console.log(r === null ? 'skipped' : 'triggered');
")
if [ "$RESULT" = "skipped" ]; then
echo "[OK] skips non-Write/Edit tools"
else
echo "[FAIL] expected skipped, got: $RESULT"
errors=$((errors + 1))
fi

echo ""
echo "=== $((8 - errors))/8 tests passed ==="
exit $errors
Loading