@orchestrator-claude/definitions 3.12.1 → 3.12.2

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.
@@ -0,0 +1,57 @@
1
+ #!/bin/bash
2
+ # dangling-workflow-guard.sh — ADR-013 Phase 5 Hook (JSON Structured Output)
3
+ # Trigger: Stop
4
+ # Purpose: Before conversation ends, check for workflows in in_progress state.
5
+ # Blocks stop and instructs LLM to call completeWorkflow.
6
+ #
7
+ # Output: JSON with decision: "block" + reason (forces LLM to continue)
8
+ # Exit 0 with block JSON = prevent stop
9
+ # Check stop_hook_active to prevent infinite loops
10
+
11
+ set -euo pipefail
12
+
13
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+ source "$SCRIPT_DIR/orch-helpers.sh"
15
+
16
+ STDIN_DATA=$(orch_read_stdin)
17
+ orch_log "DANGLING-GUARD: Stop hook triggered"
18
+
19
+ # Prevent infinite loops
20
+ STOP_ACTIVE=$(orch_json_field "$STDIN_DATA" "stop_hook_active")
21
+ if [ "$STOP_ACTIVE" = "true" ]; then
22
+ orch_log "DANGLING-GUARD: stop_hook_active=true, allowing stop to prevent loop"
23
+ exit 0
24
+ fi
25
+
26
+ # Find active workflow
27
+ WORKFLOW_ID=$(orch_get_active_workflow 2>/dev/null) || WORKFLOW_ID=""
28
+
29
+ if [ -z "$WORKFLOW_ID" ]; then
30
+ orch_log "DANGLING-GUARD: No active workflow, clean exit"
31
+ exit 0
32
+ fi
33
+
34
+ # Get workflow status
35
+ TOKEN=$(orch_get_token 2>/dev/null) || TOKEN=""
36
+ if [ -z "$TOKEN" ]; then
37
+ orch_log "DANGLING-GUARD: Auth failed, allowing stop"
38
+ exit 0
39
+ fi
40
+
41
+ STATUS_RESP=$(curl -sf --max-time 5 "${API_URL}/api/v1/workflows/${WORKFLOW_ID}/status" \
42
+ -H "Authorization: Bearer $TOKEN" \
43
+ -H "X-Project-ID: $PROJECT_ID" 2>/dev/null) || STATUS_RESP=""
44
+
45
+ PHASE=$(orch_json_field "$STATUS_RESP" "currentPhase")
46
+ STATUS=$(orch_json_field "$STATUS_RESP" "status")
47
+
48
+ orch_log "DANGLING-GUARD: workflow=$WORKFLOW_ID status=$STATUS phase=$PHASE"
49
+
50
+ if [ "$STATUS" = "in_progress" ] || [ "$STATUS" = "awaiting_agent" ] || [ "$STATUS" = "awaiting_approval" ]; then
51
+ orch_log "DANGLING-GUARD: BLOCK stop (workflow still active)"
52
+ echo "{\"decision\":\"block\",\"reason\":\"[DANGLING-GUARD] Workflow ${WORKFLOW_ID} is still ${STATUS} (phase: ${PHASE}). You must call mcp__orchestrator-extended__completeWorkflow({ workflowId: '${WORKFLOW_ID}' }) before ending the session.\"}"
53
+ exit 0
54
+ fi
55
+
56
+ orch_log "DANGLING-GUARD: Workflow completed or not active, allowing stop"
57
+ exit 0
@@ -0,0 +1,79 @@
1
+ #!/bin/bash
2
+ # gate-guardian.sh — ADR-013 Phase 5 Hook (JSON Structured Output)
3
+ # Trigger: PreToolUse on mcp__orchestrator-tools__advancePhase
4
+ # Purpose: Evaluate gate before phase advance. FAIL-CLOSED: blocks when uncertain.
5
+ #
6
+ # Output: JSON with permissionDecision (deny/allow) + additionalContext
7
+ # Exit 0 with JSON = structured decision
8
+
9
+ set -euo pipefail
10
+
11
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
+ source "$SCRIPT_DIR/orch-helpers.sh"
13
+
14
+ STDIN_DATA=$(orch_read_stdin)
15
+ orch_log "GATE-GUARDIAN: PreToolUse advancePhase triggered"
16
+
17
+ # Extract parameters
18
+ WORKFLOW_ID=$(orch_json_field "$STDIN_DATA" "tool_input.workflowId")
19
+ TARGET_PHASE=$(orch_json_field "$STDIN_DATA" "tool_input.targetPhase")
20
+ [ -z "$WORKFLOW_ID" ] && WORKFLOW_ID=$(orch_json_field "$STDIN_DATA" "workflowId")
21
+ [ -z "$TARGET_PHASE" ] && TARGET_PHASE=$(orch_json_field "$STDIN_DATA" "targetPhase")
22
+
23
+ orch_log "GATE-GUARDIAN: workflow=$WORKFLOW_ID targetPhase=$TARGET_PHASE"
24
+
25
+ # FAIL-CLOSED: if we can't parse input, DENY
26
+ if [ -z "$WORKFLOW_ID" ] || [ -z "$TARGET_PHASE" ]; then
27
+ orch_log "GATE-GUARDIAN: DENY (could not parse input)"
28
+ echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Gate Guardian: Could not parse workflowId or targetPhase.","additionalContext":"Ensure you pass both workflowId and targetPhase to advancePhase."}}'
29
+ exit 0
30
+ fi
31
+
32
+ # Get auth token — FAIL-CLOSED on auth failure
33
+ TOKEN=$(orch_get_token 2>/dev/null) || TOKEN=""
34
+ if [ -z "$TOKEN" ]; then
35
+ orch_log "GATE-GUARDIAN: DENY (auth failed)"
36
+ echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Gate Guardian: Authentication failed.","additionalContext":"Check ORCHESTRATOR_ADMIN_EMAIL, ORCHESTRATOR_ADMIN_PASSWORD, ORCHESTRATOR_PROJECT_ID env vars."}}'
37
+ exit 0
38
+ fi
39
+
40
+ # Special handling for IMPLEMENT phase: require approval
41
+ TARGET_LOWER=$(echo "$TARGET_PHASE" | tr '[:upper:]' '[:lower:]')
42
+ if [ "$TARGET_LOWER" = "implement" ]; then
43
+ orch_log "GATE-GUARDIAN: Checking approval for IMPLEMENT phase"
44
+
45
+ PENDING=$(curl -sf --max-time 5 "${API_URL}/api/v1/workflows/${WORKFLOW_ID}/pending-action" \
46
+ -H "Authorization: Bearer $TOKEN" \
47
+ -H "X-Project-ID: $PROJECT_ID" 2>/dev/null) || PENDING=""
48
+
49
+ if [ -n "$PENDING" ]; then
50
+ PENDING_STATUS=$(orch_json_field "$PENDING" "status")
51
+ if [ "$PENDING_STATUS" != "approved" ] && [ "$PENDING_STATUS" != "awaiting_agent" ]; then
52
+ orch_log "GATE-GUARDIAN: DENY (IMPLEMENT requires approval, status=$PENDING_STATUS)"
53
+ echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Gate Guardian: IMPLEMENT requires human approval. Status: ${PENDING_STATUS}.\",\"additionalContext\":\"Ask the user for approval first. Then call mcp__orchestrator-tools__approveAction.\"}}"
54
+ exit 0
55
+ fi
56
+ fi
57
+ fi
58
+
59
+ # Evaluate gate via API — FAIL-CLOSED on failure
60
+ GATE_RESULT=$(orch_evaluate_gate "$WORKFLOW_ID" "$TARGET_PHASE" 2>/dev/null) || GATE_RESULT=""
61
+ if [ -z "$GATE_RESULT" ]; then
62
+ orch_log "GATE-GUARDIAN: DENY (gate evaluation failed)"
63
+ echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Gate Guardian: Gate evaluation failed for phase '${TARGET_PHASE}'.\",\"additionalContext\":\"Required artifacts may be missing. Complete the current phase before advancing.\"}}"
64
+ exit 0
65
+ fi
66
+
67
+ PASSED=$(orch_json_field "$GATE_RESULT" "passed")
68
+
69
+ if [ "$PASSED" = "true" ]; then
70
+ orch_log "GATE-GUARDIAN: ALLOW (gate passed for $TARGET_PHASE)"
71
+ echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"additionalContext\":\"Gate to '${TARGET_PHASE}' passed.\"}}"
72
+ exit 0
73
+ fi
74
+
75
+ # Gate failed
76
+ REASONS=$(orch_json_field "$GATE_RESULT" "reasons")
77
+ orch_log "GATE-GUARDIAN: DENY (gate failed: $REASONS)"
78
+ echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Gate Guardian: Gate to '${TARGET_PHASE}' did not pass.\",\"additionalContext\":\"Reasons: ${REASONS}. Complete current phase requirements before advancing.\"}}"
79
+ exit 0
@@ -0,0 +1,133 @@
1
+ #!/bin/bash
2
+ # orch-helpers.sh — Shared helper functions for Orchestrator hooks
3
+ # ADR-013: Deterministic Orchestration
4
+ # Sourced by all hooks. NOT called directly.
5
+
6
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
8
+ STATE_DIR="$PROJECT_ROOT/.orchestrator/.state"
9
+ DEBUG_LOG="$STATE_DIR/hook-debug.log"
10
+
11
+ # API Configuration (from .env or defaults)
12
+ API_URL="${ORCHESTRATOR_API_URL:-http://localhost:3001}"
13
+ AUTH_EMAIL="${ORCHESTRATOR_ADMIN_EMAIL:-${ORCHESTRATOR_AUTH_EMAIL:-admin@orchestrator.local}}"
14
+ AUTH_PASSWORD="${ORCHESTRATOR_ADMIN_PASSWORD:-${ORCHESTRATOR_AUTH_PASSWORD:-admin123}}"
15
+ PROJECT_ID="${ORCHESTRATOR_PROJECT_ID}"
16
+
17
+ mkdir -p "$STATE_DIR"
18
+
19
+ orch_log() {
20
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" >> "$DEBUG_LOG" 2>/dev/null || true
21
+ }
22
+
23
+ # Read stdin (Claude Code passes JSON via stdin)
24
+ orch_read_stdin() {
25
+ if [ ! -t 0 ]; then
26
+ cat
27
+ else
28
+ echo ""
29
+ fi
30
+ }
31
+
32
+ # Get cached JWT token (login if expired)
33
+ orch_get_token() {
34
+ local cached="$STATE_DIR/jwt-token"
35
+ if [ -f "$cached" ]; then
36
+ local age
37
+ age=$(( $(date +%s) - $(stat -c%Y "$cached" 2>/dev/null || stat -f%m "$cached" 2>/dev/null || echo 0) ))
38
+ if [ "$age" -lt 3500 ]; then
39
+ cat "$cached"
40
+ return
41
+ fi
42
+ fi
43
+
44
+ [ -z "$PROJECT_ID" ] && return 1
45
+
46
+ local resp
47
+ resp=$(curl -sf --max-time 5 -X POST "${API_URL}/api/v1/auth/login" \
48
+ -H "Content-Type: application/json" \
49
+ -H "X-Project-ID: $PROJECT_ID" \
50
+ -d "{\"email\":\"${AUTH_EMAIL}\",\"password\":\"${AUTH_PASSWORD}\"}" 2>/dev/null) || return 1
51
+
52
+ local token
53
+ token=$(echo "$resp" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const j=JSON.parse(d);console.log(j.accessToken||j.data?.accessToken||'')}catch{console.log('')}})" 2>/dev/null)
54
+
55
+ if [ -n "$token" ] && [ "$token" != "" ]; then
56
+ echo "$token" > "$cached"
57
+ echo "$token"
58
+ else
59
+ return 1
60
+ fi
61
+ }
62
+
63
+ # Get the active (non-terminal) workflow ID
64
+ # Checks in_progress, awaiting_agent, and awaiting_approval statuses
65
+ orch_get_active_workflow() {
66
+ local token
67
+ token=$(orch_get_token) || return 1
68
+
69
+ local status id
70
+ for status in in_progress awaiting_agent awaiting_approval; do
71
+ local resp
72
+ resp=$(curl -sf --max-time 3 "${API_URL}/api/v1/workflows?status=${status}&limit=1" \
73
+ -H "Authorization: Bearer $token" \
74
+ -H "X-Project-ID: $PROJECT_ID" 2>/dev/null) || continue
75
+
76
+ id=$(echo "$resp" | node -e "
77
+ let d='';
78
+ process.stdin.on('data',c=>d+=c);
79
+ process.stdin.on('end',()=>{
80
+ try {
81
+ const j=JSON.parse(d);
82
+ const wfs=j.data||j;
83
+ const wf=Array.isArray(wfs)?wfs[0]:wfs;
84
+ const id=wf?.id||'';
85
+ if(id) console.log(id); else process.exit(1);
86
+ } catch { process.exit(1); }
87
+ });" 2>/dev/null) && [ -n "$id" ] && echo "$id" && return 0
88
+ done
89
+ return 1
90
+ }
91
+
92
+ # Call getNextAction for a workflow
93
+ orch_get_next_action() {
94
+ local workflow_id="$1"
95
+ local token
96
+ token=$(orch_get_token) || return 1
97
+
98
+ curl -sf --max-time 5 "${API_URL}/api/v1/workflows/${workflow_id}/pending-action" \
99
+ -H "Authorization: Bearer $token" \
100
+ -H "X-Project-ID: $PROJECT_ID" 2>/dev/null
101
+ }
102
+
103
+ # Call evaluateGate for a workflow
104
+ orch_evaluate_gate() {
105
+ local workflow_id="$1"
106
+ local target_phase="$2"
107
+ local token
108
+ token=$(orch_get_token) || return 1
109
+
110
+ curl -sf --max-time 10 -X POST "${API_URL}/api/v1/workflows/${workflow_id}/gate/evaluate" \
111
+ -H "Content-Type: application/json" \
112
+ -H "Authorization: Bearer $token" \
113
+ -H "X-Project-ID: $PROJECT_ID" \
114
+ -d "{\"targetPhase\":\"${target_phase}\"}" 2>/dev/null
115
+ }
116
+
117
+ # Extract a field from JSON string using node
118
+ orch_json_field() {
119
+ local json="$1"
120
+ local field="$2"
121
+ echo "$json" | node -e "
122
+ let d='';
123
+ process.stdin.on('data',c=>d+=c);
124
+ process.stdin.on('end',()=>{
125
+ try {
126
+ const j=JSON.parse(d);
127
+ const parts='${field}'.split('.');
128
+ let v=j;
129
+ for(const p of parts) v=v?.[p];
130
+ console.log(v||'');
131
+ } catch { console.log(''); }
132
+ });" 2>/dev/null
133
+ }
@@ -0,0 +1,58 @@
1
+ #!/bin/bash
2
+ # ping-pong-enforcer.sh — ADR-013 Phase 5 Hook (JSON Structured Output)
3
+ # Trigger: PostToolUse on Task (Agent tool)
4
+ # Purpose: After ANY sub-agent returns, call getNextAction and inject
5
+ # the result as additionalContext into the main conversation.
6
+ #
7
+ # Output: JSON with additionalContext containing next action instructions
8
+
9
+ set -euo pipefail
10
+
11
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
+ source "$SCRIPT_DIR/orch-helpers.sh"
13
+
14
+ orch_log "PING-PONG: PostToolUse Agent triggered"
15
+
16
+ # Find active workflow
17
+ WORKFLOW_ID=$(orch_get_active_workflow 2>/dev/null) || WORKFLOW_ID=""
18
+
19
+ if [ -z "$WORKFLOW_ID" ]; then
20
+ orch_log "PING-PONG: No active workflow, skipping"
21
+ exit 0
22
+ fi
23
+
24
+ orch_log "PING-PONG: Active workflow=$WORKFLOW_ID"
25
+
26
+ # Call getNextAction
27
+ NEXT_ACTION=$(orch_get_next_action "$WORKFLOW_ID" 2>/dev/null) || NEXT_ACTION=""
28
+
29
+ if [ -z "$NEXT_ACTION" ]; then
30
+ orch_log "PING-PONG: getNextAction failed or empty"
31
+ echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PostToolUse\",\"additionalContext\":\"[PING-PONG] Workflow ${WORKFLOW_ID} is active but getNextAction returned empty. Check workflow status with mcp__orchestrator-tools__getStatus.\"}}"
32
+ exit 0
33
+ fi
34
+
35
+ # Extract key fields for the context message
36
+ HAS_ACTION=$(orch_json_field "$NEXT_ACTION" "hasAction")
37
+ AGENT=$(orch_json_field "$NEXT_ACTION" "pendingAction.agent")
38
+ STATUS=$(orch_json_field "$NEXT_ACTION" "pendingAction.status")
39
+ PROMPT=$(orch_json_field "$NEXT_ACTION" "pendingAction.prompt")
40
+
41
+ orch_log "PING-PONG: hasAction=$HAS_ACTION agent=$AGENT status=$STATUS"
42
+
43
+ if [ "$HAS_ACTION" = "true" ]; then
44
+ if [ "$STATUS" = "awaiting_agent" ]; then
45
+ CONTEXT="[PING-PONG] Next action for workflow ${WORKFLOW_ID}: Invoke agent '${AGENT}' via Agent tool. Status: awaiting_agent."
46
+ [ -n "$PROMPT" ] && CONTEXT="${CONTEXT} Prompt: ${PROMPT}"
47
+ elif [ "$STATUS" = "awaiting_approval" ]; then
48
+ CONTEXT="[PING-PONG] Workflow ${WORKFLOW_ID} requires human approval before continuing. Ask the user to approve, then call mcp__orchestrator-tools__approveAction."
49
+ else
50
+ CONTEXT="[PING-PONG] Workflow ${WORKFLOW_ID} pending action: agent=${AGENT}, status=${STATUS}."
51
+ fi
52
+ else
53
+ CONTEXT="[PING-PONG] No pending actions for workflow ${WORKFLOW_ID}. If current phase is implement, call mcp__orchestrator-extended__completeWorkflow to finalize."
54
+ fi
55
+
56
+ # Output structured JSON
57
+ echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PostToolUse\",\"additionalContext\":$(echo "$CONTEXT" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.stringify(d)))" 2>/dev/null)}}"
58
+ exit 0