@orchestrator-claude/cli 3.17.1 → 3.19.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 +1 -1
- package/dist/index.js +1 -1
- package/dist/templates/base/CLAUDE.md.hbs +45 -28
- package/dist/templates/base/claude/agents/orchestrator.md +84 -117
- package/dist/templates/base/claude/hooks/dangling-guard.ts +53 -0
- package/dist/templates/base/claude/hooks/gate-guardian.ts +102 -0
- package/dist/templates/base/claude/hooks/lib/api-client.ts +293 -0
- package/dist/templates/base/claude/hooks/lib/git-checkpoint.ts +91 -0
- package/dist/templates/base/claude/hooks/package.json +13 -0
- package/dist/templates/base/claude/hooks/post-compact.ts +44 -0
- package/dist/templates/base/claude/hooks/session-start.ts +97 -0
- package/dist/templates/base/claude/hooks/subagent-start.ts +50 -0
- package/dist/templates/base/claude/hooks/subagent-stop.ts +57 -0
- package/dist/templates/base/claude/hooks/tsconfig.json +18 -0
- package/dist/templates/base/claude/hooks/user-prompt.ts +95 -0
- package/dist/templates/base/claude/hooks/workflow-guard.ts +120 -0
- package/dist/templates/base/claude/settings.json +23 -22
- package/dist/templates/base/claude/skills/orchestrator/SKILL.md +108 -0
- package/package.json +1 -1
- package/templates/base/CLAUDE.md.hbs +45 -28
- package/templates/base/claude/agents/orchestrator.md +84 -117
- package/templates/base/claude/hooks/dangling-guard.ts +53 -0
- package/templates/base/claude/hooks/gate-guardian.ts +102 -0
- package/templates/base/claude/hooks/lib/api-client.ts +293 -0
- package/templates/base/claude/hooks/lib/git-checkpoint.ts +91 -0
- package/templates/base/claude/hooks/package.json +13 -0
- package/templates/base/claude/hooks/post-compact.ts +44 -0
- package/templates/base/claude/hooks/session-start.ts +97 -0
- package/templates/base/claude/hooks/subagent-start.ts +50 -0
- package/templates/base/claude/hooks/subagent-stop.ts +57 -0
- package/templates/base/claude/hooks/tsconfig.json +18 -0
- package/templates/base/claude/hooks/user-prompt.ts +95 -0
- package/templates/base/claude/hooks/workflow-guard.ts +120 -0
- package/templates/base/claude/settings.json +23 -22
- package/templates/base/claude/skills/orchestrator/SKILL.md +108 -0
- package/dist/templates/base/claude/hooks/approval-guardian.sh +0 -62
- package/dist/templates/base/claude/hooks/dangling-workflow-guard.sh +0 -57
- package/dist/templates/base/claude/hooks/gate-guardian.sh +0 -84
- package/dist/templates/base/claude/hooks/orch-helpers.sh +0 -135
- package/dist/templates/base/claude/hooks/ping-pong-enforcer.sh +0 -58
- package/dist/templates/base/claude/hooks/post-phase-checkpoint.sh +0 -203
- package/dist/templates/base/claude/hooks/prompt-orchestrator.sh +0 -41
- package/dist/templates/base/claude/hooks/session-orchestrator.sh +0 -54
- package/dist/templates/base/claude/hooks/track-agent-invocation.sh +0 -230
- package/dist/templates/base/claude/hooks/workflow-guard.sh +0 -79
- package/templates/base/claude/hooks/approval-guardian.sh +0 -62
- package/templates/base/claude/hooks/dangling-workflow-guard.sh +0 -57
- package/templates/base/claude/hooks/gate-guardian.sh +0 -84
- package/templates/base/claude/hooks/orch-helpers.sh +0 -135
- package/templates/base/claude/hooks/ping-pong-enforcer.sh +0 -58
- package/templates/base/claude/hooks/post-phase-checkpoint.sh +0 -203
- package/templates/base/claude/hooks/prompt-orchestrator.sh +0 -41
- package/templates/base/claude/hooks/session-orchestrator.sh +0 -54
- package/templates/base/claude/hooks/track-agent-invocation.sh +0 -230
- package/templates/base/claude/hooks/workflow-guard.sh +0 -79
|
@@ -1,57 +0,0 @@
|
|
|
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
|
|
@@ -1,84 +0,0 @@
|
|
|
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: Guard IMPLEMENT phase advance (requires human approval). ALLOWs all other phases.
|
|
5
|
-
# Note: POST /gate/evaluate endpoint removed (TD-127). Phase transitions are now handled
|
|
6
|
-
# atomically by setPendingAction() via the ping-pong-enforcer hook.
|
|
7
|
-
#
|
|
8
|
-
# Output: JSON with permissionDecision (deny/allow) + additionalContext
|
|
9
|
-
# Exit 0 with JSON = structured decision
|
|
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 "GATE-GUARDIAN: PreToolUse advancePhase triggered"
|
|
18
|
-
|
|
19
|
-
# Extract parameters
|
|
20
|
-
WORKFLOW_ID=$(orch_json_field "$STDIN_DATA" "tool_input.workflowId")
|
|
21
|
-
TARGET_PHASE=$(orch_json_field "$STDIN_DATA" "tool_input.targetPhase")
|
|
22
|
-
[ -z "$WORKFLOW_ID" ] && WORKFLOW_ID=$(orch_json_field "$STDIN_DATA" "workflowId")
|
|
23
|
-
[ -z "$TARGET_PHASE" ] && TARGET_PHASE=$(orch_json_field "$STDIN_DATA" "targetPhase")
|
|
24
|
-
|
|
25
|
-
orch_log "GATE-GUARDIAN: workflow=$WORKFLOW_ID targetPhase=$TARGET_PHASE"
|
|
26
|
-
|
|
27
|
-
# ADR-013 Phase 6: Check workflow mode before gate evaluation
|
|
28
|
-
# quick and interactive modes skip gate evaluation entirely
|
|
29
|
-
if [ -n "$WORKFLOW_ID" ]; then
|
|
30
|
-
MODE=$(orch_get_workflow_mode "$WORKFLOW_ID" 2>/dev/null) || MODE=""
|
|
31
|
-
orch_log "GATE-GUARDIAN: workflow=$WORKFLOW_ID mode=${MODE:-legacy}"
|
|
32
|
-
|
|
33
|
-
if [ "$MODE" = "quick" ]; then
|
|
34
|
-
orch_log "GATE-GUARDIAN: ALLOW (quick mode skips gate evaluation)"
|
|
35
|
-
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"additionalContext\":\"Gate to '${TARGET_PHASE}' allowed: quick mode skips gate evaluation.\"}}"
|
|
36
|
-
exit 0
|
|
37
|
-
fi
|
|
38
|
-
|
|
39
|
-
if [ "$MODE" = "interactive" ]; then
|
|
40
|
-
orch_log "GATE-GUARDIAN: ALLOW (interactive mode skips gate evaluation, artifact gates not required)"
|
|
41
|
-
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"additionalContext\":\"Gate to '${TARGET_PHASE}' allowed: interactive mode skips artifact gate evaluation.\"}}"
|
|
42
|
-
exit 0
|
|
43
|
-
fi
|
|
44
|
-
fi
|
|
45
|
-
|
|
46
|
-
# FAIL-CLOSED: if we can't parse input, DENY
|
|
47
|
-
if [ -z "$WORKFLOW_ID" ] || [ -z "$TARGET_PHASE" ]; then
|
|
48
|
-
orch_log "GATE-GUARDIAN: DENY (could not parse input)"
|
|
49
|
-
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."}}'
|
|
50
|
-
exit 0
|
|
51
|
-
fi
|
|
52
|
-
|
|
53
|
-
# Get auth token — FAIL-CLOSED on auth failure
|
|
54
|
-
TOKEN=$(orch_get_token 2>/dev/null) || TOKEN=""
|
|
55
|
-
if [ -z "$TOKEN" ]; then
|
|
56
|
-
orch_log "GATE-GUARDIAN: DENY (auth failed)"
|
|
57
|
-
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Gate Guardian: Authentication failed.","additionalContext":"Check ORCHESTRATOR_ADMIN_EMAIL, ORCHESTRATOR_ADMIN_PASSWORD, ORCHESTRATOR_PROJECT_ID env vars."}}'
|
|
58
|
-
exit 0
|
|
59
|
-
fi
|
|
60
|
-
|
|
61
|
-
# Special handling for IMPLEMENT phase: require approval
|
|
62
|
-
TARGET_LOWER=$(echo "$TARGET_PHASE" | tr '[:upper:]' '[:lower:]')
|
|
63
|
-
if [ "$TARGET_LOWER" = "implement" ]; then
|
|
64
|
-
orch_log "GATE-GUARDIAN: Checking approval for IMPLEMENT phase"
|
|
65
|
-
|
|
66
|
-
PENDING=$(curl -sf --max-time 5 "${API_URL}/api/v1/workflows/${WORKFLOW_ID}/pending-action" \
|
|
67
|
-
-H "Authorization: Bearer $TOKEN" \
|
|
68
|
-
-H "X-Project-ID: $PROJECT_ID" 2>/dev/null) || PENDING=""
|
|
69
|
-
|
|
70
|
-
if [ -n "$PENDING" ]; then
|
|
71
|
-
PENDING_STATUS=$(orch_json_field "$PENDING" "status")
|
|
72
|
-
if [ "$PENDING_STATUS" != "approved" ] && [ "$PENDING_STATUS" != "awaiting_agent" ]; then
|
|
73
|
-
orch_log "GATE-GUARDIAN: DENY (IMPLEMENT requires approval, status=$PENDING_STATUS)"
|
|
74
|
-
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.\"}}"
|
|
75
|
-
exit 0
|
|
76
|
-
fi
|
|
77
|
-
fi
|
|
78
|
-
fi
|
|
79
|
-
|
|
80
|
-
# Non-IMPLEMENT phases: ALLOW directly.
|
|
81
|
-
# Phase transitions are handled atomically by setPendingAction() via ping-pong-enforcer.
|
|
82
|
-
orch_log "GATE-GUARDIAN: ALLOW (non-IMPLEMENT phase, no gate evaluation required)"
|
|
83
|
-
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"additionalContext\":\"Gate to '${TARGET_PHASE}' allowed.\"}}"
|
|
84
|
-
exit 0
|
|
@@ -1,135 +0,0 @@
|
|
|
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
|
-
# ADR-013 Phase 6: Get the mode of a workflow (quick/standard/full/interactive)
|
|
104
|
-
# Returns the mode string, or empty string for legacy workflows (mode=null)
|
|
105
|
-
# Returns exit code 1 on network/auth error
|
|
106
|
-
orch_get_workflow_mode() {
|
|
107
|
-
local workflow_id="$1"
|
|
108
|
-
local token
|
|
109
|
-
token=$(orch_get_token) || return 1
|
|
110
|
-
|
|
111
|
-
local resp
|
|
112
|
-
resp=$(curl -sf --max-time 3 "${API_URL}/api/v1/workflows/${workflow_id}/status" \
|
|
113
|
-
-H "Authorization: Bearer $token" \
|
|
114
|
-
-H "X-Project-ID: $PROJECT_ID" 2>/dev/null) || return 1
|
|
115
|
-
|
|
116
|
-
orch_json_field "$resp" "mode"
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
# Extract a field from JSON string using node
|
|
120
|
-
orch_json_field() {
|
|
121
|
-
local json="$1"
|
|
122
|
-
local field="$2"
|
|
123
|
-
echo "$json" | node -e "
|
|
124
|
-
let d='';
|
|
125
|
-
process.stdin.on('data',c=>d+=c);
|
|
126
|
-
process.stdin.on('end',()=>{
|
|
127
|
-
try {
|
|
128
|
-
const j=JSON.parse(d);
|
|
129
|
-
const parts='${field}'.split('.');
|
|
130
|
-
let v=j;
|
|
131
|
-
for(const p of parts) v=v?.[p];
|
|
132
|
-
console.log(v||'');
|
|
133
|
-
} catch { console.log(''); }
|
|
134
|
-
});" 2>/dev/null
|
|
135
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
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
|
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Post-Phase Checkpoint Hook (RFC-012 compatible)
|
|
3
|
-
# Registers a checkpoint in PostgreSQL via the Orchestrator REST API.
|
|
4
|
-
# Replaces the deprecated orchestrator-index.json approach (TD-105 F-04).
|
|
5
|
-
#
|
|
6
|
-
# Triggered by: PostToolUse on Task tool (after agent completes)
|
|
7
|
-
# Behavior: Infers the just-completed phase from the active workflow state
|
|
8
|
-
# and registers a checkpoint via the REST API.
|
|
9
|
-
#
|
|
10
|
-
# Required env vars:
|
|
11
|
-
# ORCHESTRATOR_API_URL - Base URL (e.g. http://localhost:3000)
|
|
12
|
-
# ORCHESTRATOR_PROJECT_TOKEN - Bearer token for API authentication
|
|
13
|
-
#
|
|
14
|
-
# Optional env vars:
|
|
15
|
-
# ORCHESTRATOR_PROJECT_ID - Project ID for project-scoped workflow query
|
|
16
|
-
# ORCH_CHECKPOINT_DEBUG=1 - Enable verbose logging
|
|
17
|
-
#
|
|
18
|
-
# Always exits 0 (non-blocking).
|
|
19
|
-
|
|
20
|
-
set -euo pipefail
|
|
21
|
-
|
|
22
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
23
|
-
STATE_DIR="$SCRIPT_DIR/../../.orchestrator/.state"
|
|
24
|
-
LOG_FILE="${STATE_DIR}/checkpoint-hook.log"
|
|
25
|
-
LAST_PHASE_FILE="${STATE_DIR}/last-checkpointed-phase"
|
|
26
|
-
DEBUG="${ORCH_CHECKPOINT_DEBUG:-0}"
|
|
27
|
-
|
|
28
|
-
mkdir -p "$STATE_DIR"
|
|
29
|
-
|
|
30
|
-
log_info() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [INFO] $1" >> "$LOG_FILE"; }
|
|
31
|
-
log_warn() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [WARN] $1" >> "$LOG_FILE"; echo "[WARN] $1" >&2; }
|
|
32
|
-
log_debug() { [ "$DEBUG" = "1" ] && echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [DEBUG] $1" >> "$LOG_FILE" || true; }
|
|
33
|
-
|
|
34
|
-
# ── prerequisites ─────────────────────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
check_prerequisites() {
|
|
37
|
-
if ! command -v curl &>/dev/null; then
|
|
38
|
-
log_warn "curl not found. Checkpoints disabled."
|
|
39
|
-
return 1
|
|
40
|
-
fi
|
|
41
|
-
if ! command -v jq &>/dev/null; then
|
|
42
|
-
log_warn "jq not found. Checkpoints disabled."
|
|
43
|
-
return 1
|
|
44
|
-
fi
|
|
45
|
-
if [ -z "${ORCHESTRATOR_API_URL:-}" ]; then
|
|
46
|
-
log_debug "ORCHESTRATOR_API_URL not set. Checkpoints disabled."
|
|
47
|
-
return 1
|
|
48
|
-
fi
|
|
49
|
-
if [ -z "${ORCHESTRATOR_PROJECT_TOKEN:-}" ]; then
|
|
50
|
-
log_debug "ORCHESTRATOR_PROJECT_TOKEN not set. Checkpoints disabled."
|
|
51
|
-
return 1
|
|
52
|
-
fi
|
|
53
|
-
return 0
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
# ── API helpers ────────────────────────────────────────────────────────────────
|
|
57
|
-
|
|
58
|
-
get_active_workflow() {
|
|
59
|
-
local project_id="${ORCHESTRATOR_PROJECT_ID:-}"
|
|
60
|
-
local url="${ORCHESTRATOR_API_URL}/api/v1/workflows?status=in_progress&limit=1"
|
|
61
|
-
[ -n "$project_id" ] && url="${url}&projectId=${project_id}"
|
|
62
|
-
|
|
63
|
-
local tmpfile
|
|
64
|
-
tmpfile=$(mktemp 2>/dev/null) || return 1
|
|
65
|
-
|
|
66
|
-
curl -sf \
|
|
67
|
-
-H "Authorization: Bearer ${ORCHESTRATOR_PROJECT_TOKEN}" \
|
|
68
|
-
-H "Content-Type: application/json" \
|
|
69
|
-
"${url}" \
|
|
70
|
-
-o "$tmpfile" 2>/dev/null || { rm -f "$tmpfile"; return 1; }
|
|
71
|
-
|
|
72
|
-
local result
|
|
73
|
-
result=$(node -e "
|
|
74
|
-
const fs=require('fs');
|
|
75
|
-
try {
|
|
76
|
-
const d=fs.readFileSync('${tmpfile}','utf8');
|
|
77
|
-
const j=JSON.parse(d);
|
|
78
|
-
const wf=Array.isArray(j)?j[0]:j;
|
|
79
|
-
if(wf&&wf.id){ process.stdout.write(JSON.stringify(wf)); process.exit(0); }
|
|
80
|
-
process.exit(1);
|
|
81
|
-
} catch { process.exit(1); }" 2>/dev/null)
|
|
82
|
-
local rc=$?
|
|
83
|
-
rm -f "$tmpfile"
|
|
84
|
-
[ $rc -eq 0 ] && echo "$result" || return 1
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
create_checkpoint() {
|
|
88
|
-
local workflow_id="$1"
|
|
89
|
-
local phase="$2"
|
|
90
|
-
local description="$3"
|
|
91
|
-
|
|
92
|
-
local body
|
|
93
|
-
body=$(jq -n \
|
|
94
|
-
--arg phase "$phase" \
|
|
95
|
-
--arg desc "$description" \
|
|
96
|
-
'{ phase: $phase, description: $desc }')
|
|
97
|
-
|
|
98
|
-
local http_code
|
|
99
|
-
http_code=$(curl -sf -o /dev/null -w "%{http_code}" \
|
|
100
|
-
-X POST \
|
|
101
|
-
-H "Authorization: Bearer ${ORCHESTRATOR_PROJECT_TOKEN}" \
|
|
102
|
-
-H "Content-Type: application/json" \
|
|
103
|
-
-d "$body" \
|
|
104
|
-
"${ORCHESTRATOR_API_URL}/api/v1/workflows/${workflow_id}/checkpoints" 2>/dev/null) || echo ""
|
|
105
|
-
|
|
106
|
-
echo "$http_code"
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
# ── dedup guard ────────────────────────────────────────────────────────────────
|
|
110
|
-
|
|
111
|
-
get_saved_last_phase() {
|
|
112
|
-
[ -f "$LAST_PHASE_FILE" ] && cat "$LAST_PHASE_FILE" || echo ""
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
save_last_phase() {
|
|
116
|
-
echo "$1" > "$LAST_PHASE_FILE"
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
# ── phase inference ────────────────────────────────────────────────────────────
|
|
120
|
-
|
|
121
|
-
# When workflow is at PLAN, it means SPECIFY just completed, etc.
|
|
122
|
-
get_completed_phase() {
|
|
123
|
-
local current="$1"
|
|
124
|
-
local status="$2"
|
|
125
|
-
case "$status" in
|
|
126
|
-
"completed") echo "implement"; return ;;
|
|
127
|
-
esac
|
|
128
|
-
case "${current,,}" in
|
|
129
|
-
"plan") echo "specify" ;;
|
|
130
|
-
"tasks") echo "plan" ;;
|
|
131
|
-
"implement") echo "tasks" ;;
|
|
132
|
-
*) echo "" ;;
|
|
133
|
-
esac
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
# ── main ───────────────────────────────────────────────────────────────────────
|
|
137
|
-
|
|
138
|
-
main() {
|
|
139
|
-
log_debug "Hook triggered"
|
|
140
|
-
|
|
141
|
-
if ! check_prerequisites; then
|
|
142
|
-
exit 0
|
|
143
|
-
fi
|
|
144
|
-
|
|
145
|
-
local workflow_json
|
|
146
|
-
workflow_json=$(get_active_workflow 2>/dev/null) || workflow_json=""
|
|
147
|
-
|
|
148
|
-
if [ -z "$workflow_json" ]; then
|
|
149
|
-
log_debug "No active workflow found via API."
|
|
150
|
-
exit 0
|
|
151
|
-
fi
|
|
152
|
-
|
|
153
|
-
local workflow_id current_phase workflow_status
|
|
154
|
-
workflow_id=$(echo "$workflow_json" | jq -r '.id // empty' 2>/dev/null || echo "")
|
|
155
|
-
current_phase=$(echo "$workflow_json" | jq -r '.currentPhase // empty' 2>/dev/null || echo "")
|
|
156
|
-
workflow_status=$(echo "$workflow_json" | jq -r '.status // empty' 2>/dev/null || echo "")
|
|
157
|
-
|
|
158
|
-
if [ -z "$workflow_id" ] || [ -z "$current_phase" ]; then
|
|
159
|
-
log_debug "Could not parse workflow ID or phase."
|
|
160
|
-
exit 0
|
|
161
|
-
fi
|
|
162
|
-
|
|
163
|
-
local completed_phase
|
|
164
|
-
completed_phase=$(get_completed_phase "$current_phase" "$workflow_status")
|
|
165
|
-
|
|
166
|
-
if [ -z "$completed_phase" ]; then
|
|
167
|
-
log_debug "No completed phase inferred from current_phase=${current_phase}."
|
|
168
|
-
exit 0
|
|
169
|
-
fi
|
|
170
|
-
|
|
171
|
-
# Dedup guard — skip if we already checkpointed this phase
|
|
172
|
-
local saved_last
|
|
173
|
-
saved_last=$(get_saved_last_phase)
|
|
174
|
-
if [ "$completed_phase" = "$saved_last" ]; then
|
|
175
|
-
log_debug "Phase ${completed_phase} already checkpointed. Skipping."
|
|
176
|
-
exit 0
|
|
177
|
-
fi
|
|
178
|
-
|
|
179
|
-
local description="Auto-checkpoint after ${completed_phase} phase"
|
|
180
|
-
local http_code
|
|
181
|
-
http_code=$(create_checkpoint "$workflow_id" "$completed_phase" "$description")
|
|
182
|
-
|
|
183
|
-
case "$http_code" in
|
|
184
|
-
201|200)
|
|
185
|
-
save_last_phase "$completed_phase"
|
|
186
|
-
log_info "Checkpoint registered: workflow=${workflow_id}, phase=${completed_phase}"
|
|
187
|
-
;;
|
|
188
|
-
409)
|
|
189
|
-
save_last_phase "$completed_phase"
|
|
190
|
-
log_info "Checkpoint already exists (409): phase=${completed_phase}"
|
|
191
|
-
;;
|
|
192
|
-
"")
|
|
193
|
-
log_warn "API unreachable. Checkpoint skipped for phase=${completed_phase}."
|
|
194
|
-
;;
|
|
195
|
-
*)
|
|
196
|
-
log_warn "Unexpected HTTP ${http_code} creating checkpoint for phase=${completed_phase}."
|
|
197
|
-
;;
|
|
198
|
-
esac
|
|
199
|
-
|
|
200
|
-
exit 0
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
main "$@"
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# prompt-orchestrator.sh — ADR-013 Phase 5 Hook (JSON Structured Output)
|
|
3
|
-
# Trigger: UserPromptSubmit
|
|
4
|
-
# Purpose: On every user prompt, inject orchestrator context.
|
|
5
|
-
# If no workflow active, remind about workflow requirement.
|
|
6
|
-
# If workflow active, inject current state.
|
|
7
|
-
#
|
|
8
|
-
# Output: JSON with additionalContext (never blocks prompts)
|
|
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 "PROMPT-ORCH: UserPromptSubmit triggered"
|
|
16
|
-
|
|
17
|
-
# Find active workflow
|
|
18
|
-
WORKFLOW_ID=$(orch_get_active_workflow 2>/dev/null) || WORKFLOW_ID=""
|
|
19
|
-
|
|
20
|
-
if [ -z "$WORKFLOW_ID" ]; then
|
|
21
|
-
# No workflow — inject reminder (lightweight, no API calls)
|
|
22
|
-
orch_log "PROMPT-ORCH: No active workflow, injecting reminder"
|
|
23
|
-
echo '{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"[ORCHESTRATOR] No active workflow. If this request involves creating, modifying, or fixing code, you must start a workflow first (detectWorkflow → startWorkflow). The workflow-guard hook will block writes to src/ without a workflow."}}'
|
|
24
|
-
exit 0
|
|
25
|
-
fi
|
|
26
|
-
|
|
27
|
-
# Active workflow — inject state
|
|
28
|
-
NEXT_ACTION=$(orch_get_next_action "$WORKFLOW_ID" 2>/dev/null) || NEXT_ACTION=""
|
|
29
|
-
HAS_ACTION=$(orch_json_field "$NEXT_ACTION" "hasAction")
|
|
30
|
-
AGENT=$(orch_json_field "$NEXT_ACTION" "pendingAction.agent")
|
|
31
|
-
PA_STATUS=$(orch_json_field "$NEXT_ACTION" "pendingAction.status")
|
|
32
|
-
|
|
33
|
-
if [ "$HAS_ACTION" = "true" ]; then
|
|
34
|
-
CONTEXT="[ORCHESTRATOR] Active workflow: ${WORKFLOW_ID}. Next: invoke '${AGENT}' (${PA_STATUS})."
|
|
35
|
-
else
|
|
36
|
-
CONTEXT="[ORCHESTRATOR] Active workflow: ${WORKFLOW_ID}. No pending actions."
|
|
37
|
-
fi
|
|
38
|
-
|
|
39
|
-
orch_log "PROMPT-ORCH: workflow=$WORKFLOW_ID hasAction=$HAS_ACTION"
|
|
40
|
-
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"UserPromptSubmit\",\"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)}}"
|
|
41
|
-
exit 0
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# session-orchestrator.sh — ADR-013 Phase 5 Hook (JSON Structured Output)
|
|
3
|
-
# Trigger: SessionStart
|
|
4
|
-
# Purpose: On session start, check for active workflow and inject context.
|
|
5
|
-
# Helps the LLM resume work from where it left off.
|
|
6
|
-
#
|
|
7
|
-
# Output: JSON with additionalContext containing workflow state
|
|
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 "SESSION-ORCH: SessionStart hook 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 "SESSION-ORCH: No active workflow"
|
|
21
|
-
echo '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"[ORCHESTRATOR] No active workflow. For feature requests, bug fixes, or refactoring, start with:\n1. mcp__orchestrator-tools__detectWorkflow\n2. mcp__orchestrator-tools__startWorkflow\n3. Follow the nextStep returned\n\nDirect implementation is blocked by the workflow-guard hook."}}'
|
|
22
|
-
exit 0
|
|
23
|
-
fi
|
|
24
|
-
|
|
25
|
-
# Get workflow details
|
|
26
|
-
TOKEN=$(orch_get_token 2>/dev/null) || TOKEN=""
|
|
27
|
-
if [ -z "$TOKEN" ]; then
|
|
28
|
-
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"SessionStart\",\"additionalContext\":\"[ORCHESTRATOR] Active workflow: ${WORKFLOW_ID} (could not fetch details — auth failed).\"}}"
|
|
29
|
-
exit 0
|
|
30
|
-
fi
|
|
31
|
-
|
|
32
|
-
STATUS_RESP=$(curl -sf --max-time 5 "${API_URL}/api/v1/workflows/${WORKFLOW_ID}/status" \
|
|
33
|
-
-H "Authorization: Bearer $TOKEN" \
|
|
34
|
-
-H "X-Project-ID: $PROJECT_ID" 2>/dev/null) || STATUS_RESP=""
|
|
35
|
-
|
|
36
|
-
PHASE=$(orch_json_field "$STATUS_RESP" "currentPhase")
|
|
37
|
-
STATUS=$(orch_json_field "$STATUS_RESP" "status")
|
|
38
|
-
WF_TYPE=$(orch_json_field "$STATUS_RESP" "type")
|
|
39
|
-
|
|
40
|
-
# Get next action
|
|
41
|
-
NEXT_ACTION=$(orch_get_next_action "$WORKFLOW_ID" 2>/dev/null) || NEXT_ACTION=""
|
|
42
|
-
HAS_ACTION=$(orch_json_field "$NEXT_ACTION" "hasAction")
|
|
43
|
-
AGENT=$(orch_json_field "$NEXT_ACTION" "pendingAction.agent")
|
|
44
|
-
PA_STATUS=$(orch_json_field "$NEXT_ACTION" "pendingAction.status")
|
|
45
|
-
|
|
46
|
-
CONTEXT="[ORCHESTRATOR] Active workflow: ${WORKFLOW_ID}\nType: ${WF_TYPE}\nPhase: ${PHASE}\nStatus: ${STATUS}"
|
|
47
|
-
|
|
48
|
-
if [ "$HAS_ACTION" = "true" ]; then
|
|
49
|
-
CONTEXT="${CONTEXT}\nNext action: invoke '${AGENT}' (${PA_STATUS})"
|
|
50
|
-
fi
|
|
51
|
-
|
|
52
|
-
orch_log "SESSION-ORCH: Injecting workflow context (wf=$WORKFLOW_ID phase=$PHASE)"
|
|
53
|
-
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"SessionStart\",\"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)}}"
|
|
54
|
-
exit 0
|