@orchestrator-claude/cli 3.10.1 → 3.11.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/dist/index.d.ts CHANGED
@@ -12,5 +12,5 @@
12
12
  /**
13
13
  * CLI version
14
14
  */
15
- export declare const CLI_VERSION = "3.10.1";
15
+ export declare const CLI_VERSION = "3.11.0";
16
16
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -24,7 +24,7 @@ import { OutputFormatter } from './formatters/OutputFormatter.js';
24
24
  /**
25
25
  * CLI version
26
26
  */
27
- export const CLI_VERSION = '3.10.1';
27
+ export const CLI_VERSION = '3.11.0';
28
28
  /**
29
29
  * Main CLI function
30
30
  */
@@ -0,0 +1,65 @@
1
+ #!/bin/bash
2
+ # dangling-workflow-guard.sh — ADR-013 Core Hook
3
+ # Trigger: Stop (session end)
4
+ # Purpose: Before the conversation ends, check for workflows still
5
+ # in "in_progress" state. Warn the user and attempt to
6
+ # complete the workflow automatically.
7
+ #
8
+ # Exit 0 always (non-blocking — we don't want to prevent session exit).
9
+
10
+ set -euo pipefail
11
+
12
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
+ source "$SCRIPT_DIR/orch-helpers.sh"
14
+
15
+ orch_log "DANGLING-GUARD: Stop hook triggered"
16
+
17
+ # Find active workflow
18
+ WORKFLOW_ID=$(orch_get_active_workflow 2>/dev/null) || {
19
+ orch_log "DANGLING-GUARD: No active workflow, clean exit"
20
+ exit 0
21
+ }
22
+
23
+ orch_log "DANGLING-GUARD: Found active workflow=$WORKFLOW_ID"
24
+
25
+ # Get workflow status
26
+ TOKEN=$(orch_get_token 2>/dev/null) || exit 0
27
+
28
+ STATUS_RESP=$(curl -sf --max-time 5 "${API_URL}/api/v1/workflows/${WORKFLOW_ID}/status" \
29
+ -H "Authorization: Bearer $TOKEN" \
30
+ -H "X-Project-ID: $PROJECT_ID" 2>/dev/null) || exit 0
31
+
32
+ CURRENT_PHASE=$(orch_json_field "$STATUS_RESP" "currentPhase")
33
+ WF_STATUS=$(orch_json_field "$STATUS_RESP" "status")
34
+
35
+ orch_log "DANGLING-GUARD: status=$WF_STATUS phase=$CURRENT_PHASE"
36
+
37
+ if [ "$WF_STATUS" = "in_progress" ] || [ "$WF_STATUS" = "running" ]; then
38
+ echo ""
39
+ echo "[DANGLING-GUARD] WARNING: Workflow $WORKFLOW_ID is still active (status: $WF_STATUS, phase: $CURRENT_PHASE)."
40
+
41
+ # If in implement phase with no pending actions, auto-complete
42
+ if [ "$CURRENT_PHASE" = "implement" ]; then
43
+ NEXT_ACTION=$(orch_get_next_action "$WORKFLOW_ID" 2>/dev/null) || exit 0
44
+ HAS_ACTION=$(orch_json_field "$NEXT_ACTION" "hasAction")
45
+
46
+ if [ "$HAS_ACTION" != "true" ]; then
47
+ echo "[DANGLING-GUARD] Auto-completing workflow (implement phase, no pending actions)..."
48
+ curl -sf --max-time 10 -X POST "${API_URL}/api/v1/workflows/${WORKFLOW_ID}/complete" \
49
+ -H "Content-Type: application/json" \
50
+ -H "Authorization: Bearer $TOKEN" \
51
+ -H "X-Project-ID: $PROJECT_ID" \
52
+ -d "{}" >> "$DEBUG_LOG" 2>&1 || true
53
+ echo "[DANGLING-GUARD] Workflow completed."
54
+ orch_log "DANGLING-GUARD: Auto-completed workflow $WORKFLOW_ID"
55
+ else
56
+ echo "[DANGLING-GUARD] Workflow has pending actions — cannot auto-complete."
57
+ echo "Resume this session to continue the workflow."
58
+ fi
59
+ else
60
+ echo "[DANGLING-GUARD] Workflow is in '$CURRENT_PHASE' phase — not auto-completing."
61
+ echo "Resume this session to continue the workflow."
62
+ fi
63
+ fi
64
+
65
+ exit 0
@@ -0,0 +1,80 @@
1
+ #!/bin/bash
2
+ # gate-guardian.sh — ADR-013 Core Hook
3
+ # Trigger: PreToolUse on mcp__orchestrator-tools__advancePhase
4
+ # Purpose: Before any phase advance, evaluate the gate.
5
+ # BLOCKS the advance if the gate fails (exit 1).
6
+ # Also enforces human approval for IMPLEMENT phase.
7
+ #
8
+ # Exit 0 = allow advancePhase to proceed
9
+ # Exit 1 = BLOCK advancePhase (gate failed or approval required)
10
+
11
+ set -euo pipefail
12
+
13
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+ source "$SCRIPT_DIR/orch-helpers.sh"
15
+
16
+ # Read stdin (Claude Code passes tool input JSON)
17
+ STDIN_DATA=$(orch_read_stdin)
18
+ orch_log "GATE-GUARDIAN: PreToolUse advancePhase triggered"
19
+
20
+ # Extract workflowId and targetPhase from tool input
21
+ WORKFLOW_ID=$(orch_json_field "$STDIN_DATA" "tool_input.workflowId")
22
+ TARGET_PHASE=$(orch_json_field "$STDIN_DATA" "tool_input.targetPhase")
23
+
24
+ # Fallback: try without tool_input wrapper
25
+ [ -z "$WORKFLOW_ID" ] && WORKFLOW_ID=$(orch_json_field "$STDIN_DATA" "workflowId")
26
+ [ -z "$TARGET_PHASE" ] && TARGET_PHASE=$(orch_json_field "$STDIN_DATA" "targetPhase")
27
+
28
+ orch_log "GATE-GUARDIAN: workflow=$WORKFLOW_ID targetPhase=$TARGET_PHASE"
29
+
30
+ # If we can't parse the input, allow (don't break on edge cases)
31
+ if [ -z "$WORKFLOW_ID" ] || [ -z "$TARGET_PHASE" ]; then
32
+ orch_log "GATE-GUARDIAN: Could not parse input, allowing"
33
+ exit 0
34
+ fi
35
+
36
+ # --- IMPLEMENT phase: check for human approval ---
37
+ TARGET_LOWER=$(echo "$TARGET_PHASE" | tr '[:upper:]' '[:lower:]')
38
+
39
+ if [ "$TARGET_LOWER" = "implement" ]; then
40
+ orch_log "GATE-GUARDIAN: Checking approval for IMPLEMENT phase"
41
+
42
+ # Check pending action status
43
+ NEXT_ACTION=$(orch_get_next_action "$WORKFLOW_ID" 2>/dev/null) || {
44
+ orch_log "GATE-GUARDIAN: Could not check approval, allowing"
45
+ exit 0
46
+ }
47
+
48
+ STATUS=$(orch_json_field "$NEXT_ACTION" "pendingAction.status")
49
+
50
+ if [ "$STATUS" = "awaiting_approval" ]; then
51
+ echo ""
52
+ echo "[GATE-GUARDIAN] BLOCKED: Cannot advance to IMPLEMENT without human approval."
53
+ echo "The workflow requires user approval before implementation begins."
54
+ echo "Call mcp__orchestrator-tools__approveAction({ workflowId: '$WORKFLOW_ID' }) after user approves."
55
+ orch_log "GATE-GUARDIAN: BLOCKED — awaiting_approval for IMPLEMENT"
56
+ exit 1
57
+ fi
58
+ fi
59
+
60
+ # --- All phases: evaluate gate ---
61
+ GATE_RESULT=$(orch_evaluate_gate "$WORKFLOW_ID" "$TARGET_PHASE" 2>/dev/null) || {
62
+ orch_log "GATE-GUARDIAN: evaluateGate call failed, allowing (API may not support this gate)"
63
+ exit 0
64
+ }
65
+
66
+ PASSED=$(orch_json_field "$GATE_RESULT" "passed")
67
+
68
+ if [ "$PASSED" = "false" ]; then
69
+ REASONS=$(orch_json_field "$GATE_RESULT" "reasons")
70
+ echo ""
71
+ echo "[GATE-GUARDIAN] BLOCKED: Gate to '$TARGET_PHASE' did not pass."
72
+ echo "Reasons: $REASONS"
73
+ echo "Fix the issues before advancing."
74
+ orch_log "GATE-GUARDIAN: BLOCKED — gate failed for $TARGET_PHASE: $REASONS"
75
+ exit 1
76
+ fi
77
+
78
+ orch_log "GATE-GUARDIAN: Gate to $TARGET_PHASE PASSED"
79
+ echo "[GATE-GUARDIAN] Gate to '$TARGET_PHASE' PASSED."
80
+ exit 0
@@ -0,0 +1,128 @@
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 (in_progress) workflow ID
64
+ orch_get_active_workflow() {
65
+ local token
66
+ token=$(orch_get_token) || return 1
67
+
68
+ local resp
69
+ resp=$(curl -sf --max-time 5 "${API_URL}/api/v1/workflows?status=in_progress&limit=1" \
70
+ -H "Authorization: Bearer $token" \
71
+ -H "X-Project-ID: $PROJECT_ID" 2>/dev/null) || return 1
72
+
73
+ echo "$resp" | node -e "
74
+ let d='';
75
+ process.stdin.on('data',c=>d+=c);
76
+ process.stdin.on('end',()=>{
77
+ try {
78
+ const j=JSON.parse(d);
79
+ const wfs=j.data||j;
80
+ const wf=Array.isArray(wfs)?wfs[0]:wfs;
81
+ const id=wf?.id||'';
82
+ if(id) console.log(id); else process.exit(1);
83
+ } catch { process.exit(1); }
84
+ });" 2>/dev/null
85
+ }
86
+
87
+ # Call getNextAction for a workflow
88
+ orch_get_next_action() {
89
+ local workflow_id="$1"
90
+ local token
91
+ token=$(orch_get_token) || return 1
92
+
93
+ curl -sf --max-time 5 "${API_URL}/api/v1/workflows/${workflow_id}/next-action" \
94
+ -H "Authorization: Bearer $token" \
95
+ -H "X-Project-ID: $PROJECT_ID" 2>/dev/null
96
+ }
97
+
98
+ # Call evaluateGate for a workflow
99
+ orch_evaluate_gate() {
100
+ local workflow_id="$1"
101
+ local target_phase="$2"
102
+ local token
103
+ token=$(orch_get_token) || return 1
104
+
105
+ curl -sf --max-time 10 -X POST "${API_URL}/api/v1/workflows/${workflow_id}/evaluate-gate" \
106
+ -H "Content-Type: application/json" \
107
+ -H "Authorization: Bearer $token" \
108
+ -H "X-Project-ID: $PROJECT_ID" \
109
+ -d "{\"targetPhase\":\"${target_phase}\"}" 2>/dev/null
110
+ }
111
+
112
+ # Extract a field from JSON string using node
113
+ orch_json_field() {
114
+ local json="$1"
115
+ local field="$2"
116
+ echo "$json" | node -e "
117
+ let d='';
118
+ process.stdin.on('data',c=>d+=c);
119
+ process.stdin.on('end',()=>{
120
+ try {
121
+ const j=JSON.parse(d);
122
+ const parts='${field}'.split('.');
123
+ let v=j;
124
+ for(const p of parts) v=v?.[p];
125
+ console.log(v||'');
126
+ } catch { console.log(''); }
127
+ });" 2>/dev/null
128
+ }
@@ -0,0 +1,72 @@
1
+ #!/bin/bash
2
+ # ping-pong-enforcer.sh — ADR-013 Core Hook
3
+ # Trigger: PostToolUse on Agent
4
+ # Purpose: After ANY sub-agent returns, automatically call getNextAction
5
+ # and inject the result into the conversation.
6
+ #
7
+ # This makes Ping-Pong DETERMINISTIC: the LLM always receives the next
8
+ # action regardless of whether it "remembers" to call getNextAction.
9
+ #
10
+ # Output goes to stdout → injected into Claude conversation context.
11
+ # Exit 0 always (non-blocking — informational injection).
12
+
13
+ set -euo pipefail
14
+
15
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
+ source "$SCRIPT_DIR/orch-helpers.sh"
17
+
18
+ # Read stdin (Claude Code passes tool result JSON)
19
+ STDIN_DATA=$(orch_read_stdin)
20
+ orch_log "PING-PONG: PostToolUse Agent triggered"
21
+
22
+ # Find active workflow
23
+ WORKFLOW_ID=$(orch_get_active_workflow 2>/dev/null) || {
24
+ orch_log "PING-PONG: No active workflow, skipping"
25
+ exit 0
26
+ }
27
+
28
+ orch_log "PING-PONG: Active workflow=$WORKFLOW_ID"
29
+
30
+ # Call getNextAction
31
+ NEXT_ACTION=$(orch_get_next_action "$WORKFLOW_ID" 2>/dev/null) || {
32
+ orch_log "PING-PONG: getNextAction failed, skipping"
33
+ exit 0
34
+ }
35
+
36
+ # Parse response
37
+ HAS_ACTION=$(orch_json_field "$NEXT_ACTION" "hasAction")
38
+ STATUS=$(orch_json_field "$NEXT_ACTION" "pendingAction.status")
39
+ AGENT=$(orch_json_field "$NEXT_ACTION" "pendingAction.agent")
40
+ CURRENT_PHASE=$(orch_json_field "$NEXT_ACTION" "currentPhase")
41
+
42
+ orch_log "PING-PONG: hasAction=$HAS_ACTION status=$STATUS agent=$AGENT phase=$CURRENT_PHASE"
43
+
44
+ # Inject result into conversation
45
+ if [ "$HAS_ACTION" = "true" ]; then
46
+ if [ "$STATUS" = "awaiting_agent" ]; then
47
+ echo ""
48
+ echo "[ORCHESTRATOR] Workflow $WORKFLOW_ID — next action: invoke agent '$AGENT'"
49
+ echo "You MUST invoke this agent via Task tool with subagent_type='$AGENT'."
50
+ echo "Workflow ID: $WORKFLOW_ID | Phase: $CURRENT_PHASE"
51
+ elif [ "$STATUS" = "awaiting_approval" ]; then
52
+ echo ""
53
+ echo "[ORCHESTRATOR] Workflow $WORKFLOW_ID — AWAITING HUMAN APPROVAL"
54
+ echo "Phase: $CURRENT_PHASE. Ask the user to review artifacts and approve before continuing."
55
+ echo "After approval, call: mcp__orchestrator-tools__approveAction({ workflowId: '$WORKFLOW_ID' })"
56
+ else
57
+ echo ""
58
+ echo "[ORCHESTRATOR] Workflow $WORKFLOW_ID — pending action status: $STATUS"
59
+ echo "Full response: $NEXT_ACTION"
60
+ fi
61
+ else
62
+ if [ "$CURRENT_PHASE" = "implement" ]; then
63
+ echo ""
64
+ echo "[ORCHESTRATOR] Workflow $WORKFLOW_ID — no more actions. Phase: implement."
65
+ echo "You MUST call: mcp__orchestrator-tools__completeWorkflow({ workflowId: '$WORKFLOW_ID' })"
66
+ else
67
+ echo ""
68
+ echo "[ORCHESTRATOR] Workflow $WORKFLOW_ID — no pending actions. Phase: $CURRENT_PHASE."
69
+ fi
70
+ fi
71
+
72
+ exit 0
@@ -19,6 +19,7 @@
19
19
  "Write(tests/**)",
20
20
  "Write(docs/**)",
21
21
  "mcp__orchestrator-tools__*",
22
+ "mcp__orchestrator-extended__*",
22
23
  "mcp__perplexity__*",
23
24
  "mcp__knowledge-base__*"
24
25
  ],
@@ -47,6 +48,17 @@
47
48
  "on_failure": "ignore"
48
49
  }
49
50
  ]
51
+ },
52
+ {
53
+ "matcher": "mcp__orchestrator-tools__advancePhase",
54
+ "hooks": [
55
+ {
56
+ "type": "command",
57
+ "command": ".claude/hooks/gate-guardian.sh",
58
+ "timeout": 15000,
59
+ "on_failure": "warn"
60
+ }
61
+ ]
50
62
  }
51
63
  ],
52
64
  "PostToolUse": [
@@ -61,14 +73,8 @@
61
73
  },
62
74
  {
63
75
  "type": "command",
64
- "command": ".claude/hooks/post-agent-artifact-relay.sh",
76
+ "command": ".claude/hooks/ping-pong-enforcer.sh",
65
77
  "timeout": 15000,
66
- "on_failure": "ignore"
67
- },
68
- {
69
- "type": "command",
70
- "command": ".claude/hooks/post-implement-validate.sh",
71
- "timeout": 10000,
72
78
  "on_failure": "warn"
73
79
  },
74
80
  {
@@ -79,6 +85,19 @@
79
85
  }
80
86
  ]
81
87
  }
88
+ ],
89
+ "Stop": [
90
+ {
91
+ "matcher": "",
92
+ "hooks": [
93
+ {
94
+ "type": "command",
95
+ "command": ".claude/hooks/dangling-workflow-guard.sh",
96
+ "timeout": 15000,
97
+ "on_failure": "ignore"
98
+ }
99
+ ]
100
+ }
82
101
  ]
83
102
  },
84
103
  "enableAllProjectMcpServers": true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchestrator-claude/cli",
3
- "version": "3.10.1",
3
+ "version": "3.11.0",
4
4
  "description": "Orchestrator CLI - Project scaffolding, migration and management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,65 @@
1
+ #!/bin/bash
2
+ # dangling-workflow-guard.sh — ADR-013 Core Hook
3
+ # Trigger: Stop (session end)
4
+ # Purpose: Before the conversation ends, check for workflows still
5
+ # in "in_progress" state. Warn the user and attempt to
6
+ # complete the workflow automatically.
7
+ #
8
+ # Exit 0 always (non-blocking — we don't want to prevent session exit).
9
+
10
+ set -euo pipefail
11
+
12
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
+ source "$SCRIPT_DIR/orch-helpers.sh"
14
+
15
+ orch_log "DANGLING-GUARD: Stop hook triggered"
16
+
17
+ # Find active workflow
18
+ WORKFLOW_ID=$(orch_get_active_workflow 2>/dev/null) || {
19
+ orch_log "DANGLING-GUARD: No active workflow, clean exit"
20
+ exit 0
21
+ }
22
+
23
+ orch_log "DANGLING-GUARD: Found active workflow=$WORKFLOW_ID"
24
+
25
+ # Get workflow status
26
+ TOKEN=$(orch_get_token 2>/dev/null) || exit 0
27
+
28
+ STATUS_RESP=$(curl -sf --max-time 5 "${API_URL}/api/v1/workflows/${WORKFLOW_ID}/status" \
29
+ -H "Authorization: Bearer $TOKEN" \
30
+ -H "X-Project-ID: $PROJECT_ID" 2>/dev/null) || exit 0
31
+
32
+ CURRENT_PHASE=$(orch_json_field "$STATUS_RESP" "currentPhase")
33
+ WF_STATUS=$(orch_json_field "$STATUS_RESP" "status")
34
+
35
+ orch_log "DANGLING-GUARD: status=$WF_STATUS phase=$CURRENT_PHASE"
36
+
37
+ if [ "$WF_STATUS" = "in_progress" ] || [ "$WF_STATUS" = "running" ]; then
38
+ echo ""
39
+ echo "[DANGLING-GUARD] WARNING: Workflow $WORKFLOW_ID is still active (status: $WF_STATUS, phase: $CURRENT_PHASE)."
40
+
41
+ # If in implement phase with no pending actions, auto-complete
42
+ if [ "$CURRENT_PHASE" = "implement" ]; then
43
+ NEXT_ACTION=$(orch_get_next_action "$WORKFLOW_ID" 2>/dev/null) || exit 0
44
+ HAS_ACTION=$(orch_json_field "$NEXT_ACTION" "hasAction")
45
+
46
+ if [ "$HAS_ACTION" != "true" ]; then
47
+ echo "[DANGLING-GUARD] Auto-completing workflow (implement phase, no pending actions)..."
48
+ curl -sf --max-time 10 -X POST "${API_URL}/api/v1/workflows/${WORKFLOW_ID}/complete" \
49
+ -H "Content-Type: application/json" \
50
+ -H "Authorization: Bearer $TOKEN" \
51
+ -H "X-Project-ID: $PROJECT_ID" \
52
+ -d "{}" >> "$DEBUG_LOG" 2>&1 || true
53
+ echo "[DANGLING-GUARD] Workflow completed."
54
+ orch_log "DANGLING-GUARD: Auto-completed workflow $WORKFLOW_ID"
55
+ else
56
+ echo "[DANGLING-GUARD] Workflow has pending actions — cannot auto-complete."
57
+ echo "Resume this session to continue the workflow."
58
+ fi
59
+ else
60
+ echo "[DANGLING-GUARD] Workflow is in '$CURRENT_PHASE' phase — not auto-completing."
61
+ echo "Resume this session to continue the workflow."
62
+ fi
63
+ fi
64
+
65
+ exit 0
@@ -0,0 +1,80 @@
1
+ #!/bin/bash
2
+ # gate-guardian.sh — ADR-013 Core Hook
3
+ # Trigger: PreToolUse on mcp__orchestrator-tools__advancePhase
4
+ # Purpose: Before any phase advance, evaluate the gate.
5
+ # BLOCKS the advance if the gate fails (exit 1).
6
+ # Also enforces human approval for IMPLEMENT phase.
7
+ #
8
+ # Exit 0 = allow advancePhase to proceed
9
+ # Exit 1 = BLOCK advancePhase (gate failed or approval required)
10
+
11
+ set -euo pipefail
12
+
13
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+ source "$SCRIPT_DIR/orch-helpers.sh"
15
+
16
+ # Read stdin (Claude Code passes tool input JSON)
17
+ STDIN_DATA=$(orch_read_stdin)
18
+ orch_log "GATE-GUARDIAN: PreToolUse advancePhase triggered"
19
+
20
+ # Extract workflowId and targetPhase from tool input
21
+ WORKFLOW_ID=$(orch_json_field "$STDIN_DATA" "tool_input.workflowId")
22
+ TARGET_PHASE=$(orch_json_field "$STDIN_DATA" "tool_input.targetPhase")
23
+
24
+ # Fallback: try without tool_input wrapper
25
+ [ -z "$WORKFLOW_ID" ] && WORKFLOW_ID=$(orch_json_field "$STDIN_DATA" "workflowId")
26
+ [ -z "$TARGET_PHASE" ] && TARGET_PHASE=$(orch_json_field "$STDIN_DATA" "targetPhase")
27
+
28
+ orch_log "GATE-GUARDIAN: workflow=$WORKFLOW_ID targetPhase=$TARGET_PHASE"
29
+
30
+ # If we can't parse the input, allow (don't break on edge cases)
31
+ if [ -z "$WORKFLOW_ID" ] || [ -z "$TARGET_PHASE" ]; then
32
+ orch_log "GATE-GUARDIAN: Could not parse input, allowing"
33
+ exit 0
34
+ fi
35
+
36
+ # --- IMPLEMENT phase: check for human approval ---
37
+ TARGET_LOWER=$(echo "$TARGET_PHASE" | tr '[:upper:]' '[:lower:]')
38
+
39
+ if [ "$TARGET_LOWER" = "implement" ]; then
40
+ orch_log "GATE-GUARDIAN: Checking approval for IMPLEMENT phase"
41
+
42
+ # Check pending action status
43
+ NEXT_ACTION=$(orch_get_next_action "$WORKFLOW_ID" 2>/dev/null) || {
44
+ orch_log "GATE-GUARDIAN: Could not check approval, allowing"
45
+ exit 0
46
+ }
47
+
48
+ STATUS=$(orch_json_field "$NEXT_ACTION" "pendingAction.status")
49
+
50
+ if [ "$STATUS" = "awaiting_approval" ]; then
51
+ echo ""
52
+ echo "[GATE-GUARDIAN] BLOCKED: Cannot advance to IMPLEMENT without human approval."
53
+ echo "The workflow requires user approval before implementation begins."
54
+ echo "Call mcp__orchestrator-tools__approveAction({ workflowId: '$WORKFLOW_ID' }) after user approves."
55
+ orch_log "GATE-GUARDIAN: BLOCKED — awaiting_approval for IMPLEMENT"
56
+ exit 1
57
+ fi
58
+ fi
59
+
60
+ # --- All phases: evaluate gate ---
61
+ GATE_RESULT=$(orch_evaluate_gate "$WORKFLOW_ID" "$TARGET_PHASE" 2>/dev/null) || {
62
+ orch_log "GATE-GUARDIAN: evaluateGate call failed, allowing (API may not support this gate)"
63
+ exit 0
64
+ }
65
+
66
+ PASSED=$(orch_json_field "$GATE_RESULT" "passed")
67
+
68
+ if [ "$PASSED" = "false" ]; then
69
+ REASONS=$(orch_json_field "$GATE_RESULT" "reasons")
70
+ echo ""
71
+ echo "[GATE-GUARDIAN] BLOCKED: Gate to '$TARGET_PHASE' did not pass."
72
+ echo "Reasons: $REASONS"
73
+ echo "Fix the issues before advancing."
74
+ orch_log "GATE-GUARDIAN: BLOCKED — gate failed for $TARGET_PHASE: $REASONS"
75
+ exit 1
76
+ fi
77
+
78
+ orch_log "GATE-GUARDIAN: Gate to $TARGET_PHASE PASSED"
79
+ echo "[GATE-GUARDIAN] Gate to '$TARGET_PHASE' PASSED."
80
+ exit 0
@@ -0,0 +1,128 @@
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 (in_progress) workflow ID
64
+ orch_get_active_workflow() {
65
+ local token
66
+ token=$(orch_get_token) || return 1
67
+
68
+ local resp
69
+ resp=$(curl -sf --max-time 5 "${API_URL}/api/v1/workflows?status=in_progress&limit=1" \
70
+ -H "Authorization: Bearer $token" \
71
+ -H "X-Project-ID: $PROJECT_ID" 2>/dev/null) || return 1
72
+
73
+ echo "$resp" | node -e "
74
+ let d='';
75
+ process.stdin.on('data',c=>d+=c);
76
+ process.stdin.on('end',()=>{
77
+ try {
78
+ const j=JSON.parse(d);
79
+ const wfs=j.data||j;
80
+ const wf=Array.isArray(wfs)?wfs[0]:wfs;
81
+ const id=wf?.id||'';
82
+ if(id) console.log(id); else process.exit(1);
83
+ } catch { process.exit(1); }
84
+ });" 2>/dev/null
85
+ }
86
+
87
+ # Call getNextAction for a workflow
88
+ orch_get_next_action() {
89
+ local workflow_id="$1"
90
+ local token
91
+ token=$(orch_get_token) || return 1
92
+
93
+ curl -sf --max-time 5 "${API_URL}/api/v1/workflows/${workflow_id}/next-action" \
94
+ -H "Authorization: Bearer $token" \
95
+ -H "X-Project-ID: $PROJECT_ID" 2>/dev/null
96
+ }
97
+
98
+ # Call evaluateGate for a workflow
99
+ orch_evaluate_gate() {
100
+ local workflow_id="$1"
101
+ local target_phase="$2"
102
+ local token
103
+ token=$(orch_get_token) || return 1
104
+
105
+ curl -sf --max-time 10 -X POST "${API_URL}/api/v1/workflows/${workflow_id}/evaluate-gate" \
106
+ -H "Content-Type: application/json" \
107
+ -H "Authorization: Bearer $token" \
108
+ -H "X-Project-ID: $PROJECT_ID" \
109
+ -d "{\"targetPhase\":\"${target_phase}\"}" 2>/dev/null
110
+ }
111
+
112
+ # Extract a field from JSON string using node
113
+ orch_json_field() {
114
+ local json="$1"
115
+ local field="$2"
116
+ echo "$json" | node -e "
117
+ let d='';
118
+ process.stdin.on('data',c=>d+=c);
119
+ process.stdin.on('end',()=>{
120
+ try {
121
+ const j=JSON.parse(d);
122
+ const parts='${field}'.split('.');
123
+ let v=j;
124
+ for(const p of parts) v=v?.[p];
125
+ console.log(v||'');
126
+ } catch { console.log(''); }
127
+ });" 2>/dev/null
128
+ }
@@ -0,0 +1,72 @@
1
+ #!/bin/bash
2
+ # ping-pong-enforcer.sh — ADR-013 Core Hook
3
+ # Trigger: PostToolUse on Agent
4
+ # Purpose: After ANY sub-agent returns, automatically call getNextAction
5
+ # and inject the result into the conversation.
6
+ #
7
+ # This makes Ping-Pong DETERMINISTIC: the LLM always receives the next
8
+ # action regardless of whether it "remembers" to call getNextAction.
9
+ #
10
+ # Output goes to stdout → injected into Claude conversation context.
11
+ # Exit 0 always (non-blocking — informational injection).
12
+
13
+ set -euo pipefail
14
+
15
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
+ source "$SCRIPT_DIR/orch-helpers.sh"
17
+
18
+ # Read stdin (Claude Code passes tool result JSON)
19
+ STDIN_DATA=$(orch_read_stdin)
20
+ orch_log "PING-PONG: PostToolUse Agent triggered"
21
+
22
+ # Find active workflow
23
+ WORKFLOW_ID=$(orch_get_active_workflow 2>/dev/null) || {
24
+ orch_log "PING-PONG: No active workflow, skipping"
25
+ exit 0
26
+ }
27
+
28
+ orch_log "PING-PONG: Active workflow=$WORKFLOW_ID"
29
+
30
+ # Call getNextAction
31
+ NEXT_ACTION=$(orch_get_next_action "$WORKFLOW_ID" 2>/dev/null) || {
32
+ orch_log "PING-PONG: getNextAction failed, skipping"
33
+ exit 0
34
+ }
35
+
36
+ # Parse response
37
+ HAS_ACTION=$(orch_json_field "$NEXT_ACTION" "hasAction")
38
+ STATUS=$(orch_json_field "$NEXT_ACTION" "pendingAction.status")
39
+ AGENT=$(orch_json_field "$NEXT_ACTION" "pendingAction.agent")
40
+ CURRENT_PHASE=$(orch_json_field "$NEXT_ACTION" "currentPhase")
41
+
42
+ orch_log "PING-PONG: hasAction=$HAS_ACTION status=$STATUS agent=$AGENT phase=$CURRENT_PHASE"
43
+
44
+ # Inject result into conversation
45
+ if [ "$HAS_ACTION" = "true" ]; then
46
+ if [ "$STATUS" = "awaiting_agent" ]; then
47
+ echo ""
48
+ echo "[ORCHESTRATOR] Workflow $WORKFLOW_ID — next action: invoke agent '$AGENT'"
49
+ echo "You MUST invoke this agent via Task tool with subagent_type='$AGENT'."
50
+ echo "Workflow ID: $WORKFLOW_ID | Phase: $CURRENT_PHASE"
51
+ elif [ "$STATUS" = "awaiting_approval" ]; then
52
+ echo ""
53
+ echo "[ORCHESTRATOR] Workflow $WORKFLOW_ID — AWAITING HUMAN APPROVAL"
54
+ echo "Phase: $CURRENT_PHASE. Ask the user to review artifacts and approve before continuing."
55
+ echo "After approval, call: mcp__orchestrator-tools__approveAction({ workflowId: '$WORKFLOW_ID' })"
56
+ else
57
+ echo ""
58
+ echo "[ORCHESTRATOR] Workflow $WORKFLOW_ID — pending action status: $STATUS"
59
+ echo "Full response: $NEXT_ACTION"
60
+ fi
61
+ else
62
+ if [ "$CURRENT_PHASE" = "implement" ]; then
63
+ echo ""
64
+ echo "[ORCHESTRATOR] Workflow $WORKFLOW_ID — no more actions. Phase: implement."
65
+ echo "You MUST call: mcp__orchestrator-tools__completeWorkflow({ workflowId: '$WORKFLOW_ID' })"
66
+ else
67
+ echo ""
68
+ echo "[ORCHESTRATOR] Workflow $WORKFLOW_ID — no pending actions. Phase: $CURRENT_PHASE."
69
+ fi
70
+ fi
71
+
72
+ exit 0
@@ -19,6 +19,7 @@
19
19
  "Write(tests/**)",
20
20
  "Write(docs/**)",
21
21
  "mcp__orchestrator-tools__*",
22
+ "mcp__orchestrator-extended__*",
22
23
  "mcp__perplexity__*",
23
24
  "mcp__knowledge-base__*"
24
25
  ],
@@ -47,6 +48,17 @@
47
48
  "on_failure": "ignore"
48
49
  }
49
50
  ]
51
+ },
52
+ {
53
+ "matcher": "mcp__orchestrator-tools__advancePhase",
54
+ "hooks": [
55
+ {
56
+ "type": "command",
57
+ "command": ".claude/hooks/gate-guardian.sh",
58
+ "timeout": 15000,
59
+ "on_failure": "warn"
60
+ }
61
+ ]
50
62
  }
51
63
  ],
52
64
  "PostToolUse": [
@@ -61,14 +73,8 @@
61
73
  },
62
74
  {
63
75
  "type": "command",
64
- "command": ".claude/hooks/post-agent-artifact-relay.sh",
76
+ "command": ".claude/hooks/ping-pong-enforcer.sh",
65
77
  "timeout": 15000,
66
- "on_failure": "ignore"
67
- },
68
- {
69
- "type": "command",
70
- "command": ".claude/hooks/post-implement-validate.sh",
71
- "timeout": 10000,
72
78
  "on_failure": "warn"
73
79
  },
74
80
  {
@@ -79,6 +85,19 @@
79
85
  }
80
86
  ]
81
87
  }
88
+ ],
89
+ "Stop": [
90
+ {
91
+ "matcher": "",
92
+ "hooks": [
93
+ {
94
+ "type": "command",
95
+ "command": ".claude/hooks/dangling-workflow-guard.sh",
96
+ "timeout": 15000,
97
+ "on_failure": "ignore"
98
+ }
99
+ ]
100
+ }
82
101
  ]
83
102
  },
84
103
  "enableAllProjectMcpServers": true,